feat: v6 rewrite

This commit is contained in:
Brendan Le Glaunec
2025-07-08 17:36:48 +02:00
parent f586940b6c
commit e81eeb0c4d
81 changed files with 7430 additions and 4099 deletions
+129 -65
View File
@@ -1,96 +1,160 @@
package main
import (
"context"
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/Ullaakut/cameradar/v5"
"github.com/Ullaakut/disgo"
"github.com/Ullaakut/disgo/style"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"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"
)
func parseArguments() error {
viper.SetEnvPrefix("cameradar")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
func runCameradar(ctx context.Context, cmd *cli.Command) error {
targetInputs := cmd.StringSlice(flagTargets)
if len(targetInputs) == 0 {
return errors.New("at least one target must be specified")
}
pflag.StringSliceP("targets", "t", []string{}, "The targets on which to scan for open RTSP streams - required (ex: 172.16.100.0/24)")
pflag.StringSliceP("ports", "p", []string{"554", "5554", "8554"}, "The ports on which to search for RTSP streams")
pflag.StringP("custom-routes", "r", "${GOPATH}/src/github.com/Ullaakut/cameradar/dictionaries/routes", "The path on which to load a custom routes dictionary")
pflag.StringP("custom-credentials", "c", "${GOPATH}/src/github.com/Ullaakut/cameradar/dictionaries/credentials.json", "The path on which to load a custom credentials JSON dictionary")
pflag.IntP("scan-speed", "s", 4, "The nmap speed preset to use for scanning (lower is stealthier)")
pflag.DurationP("attack-interval", "I", 0, "The interval between each attack (i.e: 2000ms, higher is stealthier)")
pflag.DurationP("timeout", "T", 2000*time.Millisecond, "The timeout to use for attack attempts (i.e: 2000ms)")
pflag.BoolP("debug", "d", false, "Enable the debug logs")
pflag.BoolP("verbose", "v", false, "Enable the verbose logs")
pflag.BoolP("help", "h", false, "displays this help message")
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")
}
viper.AutomaticEnv()
ports := cmd.StringSlice(flagPorts)
if len(ports) == 0 {
return errors.New("at least one port must be specified")
}
pflag.Parse()
var credsPath, routesPath string
if cmd.IsSet(flagCustomCredentials) {
credsPath = os.ExpandEnv(cmd.String(flagCustomCredentials))
}
if cmd.IsSet(flagCustomRoutes) {
routesPath = os.ExpandEnv(cmd.String(flagCustomRoutes))
}
err := viper.BindPFlags(pflag.CommandLine)
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
}
if viper.GetBool("help") {
pflag.Usage()
fmt.Println("\nExamples of usage:")
fmt.Println("\tScanning your home network for RTSP streams:\tcameradar -t 192.168.0.0/24")
fmt.Println("\tScanning a remote camera on a specific port:\tcameradar -t 172.178.10.14 -p 18554 -s 2")
fmt.Println("\tScanning an unstable remote network: \t\tcameradar -t 172.178.10.14/24 -s 1 --timeout 10000 -l")
fmt.Println("\tStealthily scanning a remote network: \t\tcameradar -t 172.178.10.14/24 -s 1 -I 5000")
os.Exit(0)
var outputPath string
if cmd.IsSet(flagOutput) {
outputPath = os.ExpandEnv(cmd.String(flagOutput))
}
if len(viper.GetStringSlice("targets")) == 0 {
pflag.Usage()
return errors.New("targets (-t, --targets) argument required\n examples:\n - 172.16.100.0/24\n - localhost\n - 8.8.8.8")
}
return nil
}
func main() {
err := parseArguments()
interactive := isInteractiveTerminal()
reporter, err := ui.NewReporter(mode, cmd.Bool(flagDebug), os.Stdout, interactive)
if err != nil {
printErr(err)
return err
}
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),
}
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(
cameradar.WithTargets(viper.GetStringSlice("targets")),
cameradar.WithPorts(viper.GetStringSlice("ports")),
cameradar.WithDebug(viper.GetBool("debug")),
cameradar.WithVerbose(viper.GetBool("verbose")),
cameradar.WithCustomCredentials(viper.GetString("custom-credentials")),
cameradar.WithCustomRoutes(viper.GetString("custom-routes")),
cameradar.WithScanSpeed(viper.GetInt("scan-speed")),
cameradar.WithAttackInterval(viper.GetDuration("attack-interval")),
cameradar.WithTimeout(viper.GetDuration("timeout")),
scanner,
attacker,
targets,
ports,
reporter,
)
if err != nil {
printErr(err)
return fmt.Errorf("creating scanner: %w", err)
}
scanResult, err := c.Scan()
if err != nil {
printErr(err)
}
streams, err := c.Attack(scanResult)
if err != nil {
printErr(err)
}
c.PrintStreams(streams)
return c.Run(ctx)
}
func printErr(err error) {
disgo.Errorln(style.Failure(style.SymbolCross), err)
os.Exit(1)
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
}
+135
View File
@@ -0,0 +1,135 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"runtime/debug"
"syscall"
"time"
"github.com/ettle/strcase"
"github.com/hamba/cmd/v3"
"github.com/urfave/cli/v3"
)
const (
flagTargets = "targets"
flagPorts = "ports"
flagCustomRoutes = "custom-routes"
flagCustomCredentials = "custom-credentials"
flagScanSpeed = "scan-speed"
flagAttackInterval = "attack-interval"
flagTimeout = "timeout"
flagSkipScan = "skip-scan"
flagDebug = "debug"
flagUI = "ui"
flagOutput = "output"
)
var version = "dev"
var flags = cmd.Flags{
&cli.StringSliceFlag{
Name: flagTargets,
Usage: "The targets on which to scan for open RTSP streams in a network range format",
Aliases: []string{"t"},
Sources: cli.EnvVars(strcase.ToSNAKE(flagTargets)),
Required: true,
},
&cli.StringSliceFlag{
Name: flagPorts,
Usage: "The ports on which to search for RTSP streams",
Aliases: []string{"p"},
Sources: cli.EnvVars(strcase.ToSNAKE(flagPorts)),
Value: []string{"554", "5554", "8554", "http"},
},
&cli.StringFlag{
Name: flagCustomRoutes,
Usage: "The path on which to load a custom routes dictionary",
Aliases: []string{"r"},
Sources: cli.EnvVars(strcase.ToSNAKE(flagCustomRoutes)),
},
&cli.StringFlag{
Name: flagCustomCredentials,
Usage: "The path on which to load a custom credentials JSON dictionary",
Aliases: []string{"c"},
Sources: cli.EnvVars(strcase.ToSNAKE(flagCustomCredentials)),
},
&cli.Int16Flag{
Name: flagScanSpeed,
Usage: "The nmap speed preset to use for scanning (lower is stealthier)",
Aliases: []string{"s"},
Sources: cli.EnvVars(strcase.ToSNAKE(flagScanSpeed)),
Value: 4,
},
&cli.DurationFlag{
Name: flagAttackInterval,
Usage: "The interval between each attack (i.e: 2000ms, higher is stealthier)",
Aliases: []string{"I"},
Sources: cli.EnvVars(strcase.ToSNAKE(flagAttackInterval)),
Value: 0,
},
&cli.DurationFlag{
Name: flagTimeout,
Usage: "The timeout to use for attack attempts (i.e: 2000ms)",
Aliases: []string{"T"},
Sources: cli.EnvVars(strcase.ToSNAKE(flagTimeout)),
Value: 2000 * time.Millisecond,
},
&cli.BoolFlag{
Name: flagSkipScan,
Usage: "Skip discovery and treat every target and port as an RTSP stream",
Sources: cli.EnvVars(strcase.ToSNAKE(flagSkipScan)),
Value: false,
},
&cli.BoolFlag{
Name: flagDebug,
Usage: "Enable debug logs",
Aliases: []string{"d"},
Sources: cli.EnvVars(strcase.ToSNAKE(flagDebug)),
Value: false,
},
&cli.StringFlag{
Name: flagUI,
Usage: "UI mode: auto, tui, or plain",
Sources: cli.EnvVars(strcase.ToSNAKE(flagUI)),
Value: "auto",
},
&cli.StringFlag{
Name: flagOutput,
Usage: "Write discovered streams to an M3U file at the given path",
Sources: cli.EnvVars(strcase.ToSNAKE(flagOutput)),
},
}
func main() {
os.Exit(realMain())
}
func realMain() (code int) {
defer func() {
if v := recover(); v != nil {
_, _ = fmt.Fprintf(os.Stderr, "Panic: %v\n%s\n", v, debug.Stack())
code = 1
}
}()
app := &cli.Command{
Name: "Cameradar",
Version: version,
Flags: flags,
Action: runCameradar,
}
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
err := app.Run(ctx, os.Args)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
return 1
}
return 0
}