Files
cameradar/cmd/cameradar/cameradar.go
T
2026-02-28 20:51:47 +01:00

235 lines
5.5 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/Ullaakut/cameradar/v6"
"github.com/Ullaakut/cameradar/v6/internal/attack"
"github.com/Ullaakut/cameradar/v6/internal/dict"
"github.com/Ullaakut/cameradar/v6/internal/output"
"github.com/Ullaakut/cameradar/v6/internal/scan"
"github.com/Ullaakut/cameradar/v6/internal/ui"
"github.com/urfave/cli/v3"
"golang.org/x/term"
)
//nolint:cyclop // Splitting this function does not make it clearer.
func runCameradar(ctx context.Context, cmd *cli.Command) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
targetInputs := cmd.StringSlice(flagTargets)
if len(targetInputs) == 0 {
return errors.New("at least one target must be specified")
}
targets, err := loadTargets(targetInputs)
if err != nil {
return fmt.Errorf("loading targets: %w", err)
}
if len(targets) == 0 {
return errors.New("no valid targets provided")
}
ports := cmd.StringSlice(flagPorts)
if len(ports) == 0 {
return errors.New("at least one port must be specified")
}
var credsPath, routesPath string
if cmd.IsSet(flagCustomCredentials) {
credsPath = os.ExpandEnv(cmd.String(flagCustomCredentials))
}
if cmd.IsSet(flagCustomRoutes) {
routesPath = os.ExpandEnv(cmd.String(flagCustomRoutes))
}
dictionary, err := dict.New(credsPath, routesPath)
if err != nil {
return fmt.Errorf("loading dictionaries: %w", err)
}
mode, err := cameradar.ParseMode(cmd.String(flagUI))
if err != nil {
return err
}
var outputPath string
if cmd.IsSet(flagOutput) {
outputPath = os.ExpandEnv(cmd.String(flagOutput))
}
interactive := isInteractiveTerminal()
buildInfo := ui.BuildInfo{Version: version, Commit: commit, Date: date}
reporter, err := ui.NewReporter(mode, cmd.Bool(flagDebug), os.Stdout, interactive, buildInfo, cancel)
if err != nil {
return err
}
if plainReporter, ok := reporter.(*ui.PlainReporter); ok {
resolvedMode := resolveMode(mode, interactive)
plainReporter.PrintStartup(buildInfo, buildStartupOptions(
targets,
ports,
routesPath,
credsPath,
outputPath,
cmd.String(flagScanner),
cmd.Int16(flagScanSpeed),
cmd.Duration(flagAttackInterval),
cmd.Duration(flagTimeout),
cmd.Bool(flagSkipScan),
cmd.Bool(flagDebug),
resolvedMode,
))
}
if outputPath != "" {
reporter = output.NewM3UReporter(reporter, outputPath)
}
defer reporter.Close()
config := scan.Config{
SkipScan: cmd.Bool(flagSkipScan),
Targets: targets,
Ports: ports,
ScanSpeed: cmd.Int16(flagScanSpeed),
Scanner: cmd.String(flagScanner),
}
var scanner cameradar.StreamScanner
scanner, err = scan.New(config, reporter)
if err != nil {
return fmt.Errorf("creating stream scanner: %w", err)
}
interval := cmd.Duration(flagAttackInterval)
timeout := cmd.Duration(flagTimeout)
attacker, err := attack.New(dictionary, interval, timeout, reporter)
if err != nil {
return fmt.Errorf("creating attacker: %w", err)
}
c, err := cameradar.New(
scanner,
attacker,
targets,
ports,
reporter,
)
if err != nil {
return fmt.Errorf("creating scanner: %w", err)
}
return c.Run(ctx)
}
func resolveMode(mode cameradar.Mode, interactive bool) cameradar.Mode {
if mode != cameradar.ModeAuto {
return mode
}
if interactive {
return cameradar.ModeTUI
}
return cameradar.ModePlain
}
func buildStartupOptions(
targets []string,
ports []string,
routesPath string,
credsPath string,
outputPath string,
scanner string,
scanSpeed int16,
attackInterval time.Duration,
timeout time.Duration,
skipScan bool,
debug bool,
mode cameradar.Mode,
) []string {
options := []string{
"targets: " + strings.Join(targets, ", "),
"ports: " + strings.Join(ports, ", "),
"custom-routes: " + fallbackValue(routesPath, "builtin"),
"custom-credentials: " + fallbackValue(credsPath, "builtin"),
"scanner: " + fallbackValue(scanner, "nmap"),
"scan-speed: " + strconv.FormatInt(int64(scanSpeed), 10),
"skip-scan: " + strconv.FormatBool(skipScan),
"attack-interval: " + attackInterval.String(),
"timeout: " + timeout.String(),
"debug: " + strconv.FormatBool(debug),
"ui: " + string(mode),
"output: " + fallbackValue(outputPath, "disabled"),
}
return options
}
func fallbackValue(value, fallback string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return fallback
}
return trimmed
}
func isInteractiveTerminal() bool {
if !term.IsTerminal(int(os.Stdout.Fd())) {
return false
}
if !term.IsTerminal(int(os.Stdin.Fd())) {
return false
}
termEnv := strings.TrimSpace(os.Getenv("TERM"))
if termEnv == "" || termEnv == "dumb" {
return false
}
return true
}
// loadTargets merges targets from command line and file paths.
// Valid targets are:
// - Single IP addresses (e.g., 192.168.1.10)
// - CIDR notations (e.g., 192.168.1.0/24)
// - Hostnames (e.g., localhost)
// - IP Ranges (e.g., 192.168.1.10-20)
func loadTargets(targets []string) ([]string, error) {
if len(targets) == 0 {
return nil, nil
}
var merged []string
for _, target := range targets {
trimmed := strings.TrimSpace(target)
if trimmed == "" {
continue
}
_, err := os.Stat(trimmed)
if err != nil {
merged = append(merged, trimmed)
continue
}
bytes, err := os.ReadFile(trimmed)
if err != nil {
return nil, fmt.Errorf("reading targets file %q: %w", trimmed, err)
}
for line := range strings.SplitSeq(string(bytes), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
merged = append(merged, line)
}
}
return merged, nil
}