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
+35
View File
@@ -0,0 +1,35 @@
package scan
import (
"github.com/Ullaakut/cameradar/v6"
"github.com/Ullaakut/cameradar/v6/internal/scan/nmap"
"github.com/Ullaakut/cameradar/v6/internal/scan/skip"
)
// Config configures how Cameradar discovers RTSP streams.
type Config struct {
SkipScan bool
Targets []string
Ports []string
ScanSpeed int16
}
// Reporter reports scan progress and debug information.
type Reporter interface {
Debug(step cameradar.Step, message string)
Progress(step cameradar.Step, message string)
}
// New builds a stream scanner based on the provided configuration.
func New(config Config, reporter Reporter) (cameradar.StreamScanner, error) {
expandedTargets, err := expandTargetsForScan(config.Targets)
if err != nil {
return nil, err
}
if config.SkipScan {
return skip.New(expandedTargets, config.Ports), nil
}
return nmap.New(config.ScanSpeed, expandedTargets, config.Ports, reporter)
}
+66
View File
@@ -0,0 +1,66 @@
package scan_test
import (
"net/netip"
"testing"
"github.com/Ullaakut/cameradar/v6"
"github.com/Ullaakut/cameradar/v6/internal/scan"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNew_UsesSkipScanner(t *testing.T) {
config := scan.Config{
SkipScan: true,
Targets: []string{
"192.0.2.0/30",
"192.0.2.10-11",
},
Ports: []string{"554", "8554-8555"},
ScanSpeed: 4,
}
scanner, err := scan.New(config, nil)
require.NoError(t, err)
streams, err := scanner.Scan(t.Context())
require.NoError(t, err)
addrs := []netip.Addr{
netip.MustParseAddr("192.0.2.0"),
netip.MustParseAddr("192.0.2.1"),
netip.MustParseAddr("192.0.2.2"),
netip.MustParseAddr("192.0.2.3"),
netip.MustParseAddr("192.0.2.10"),
netip.MustParseAddr("192.0.2.11"),
}
portsExpected := []uint16{554, 8554, 8555}
var expected []cameradar.Stream
for _, addr := range addrs {
for _, port := range portsExpected {
expected = append(expected, cameradar.Stream{
Address: addr,
Port: port,
})
}
}
assert.Equal(t, expected, streams)
}
func TestNew_SkipScanPropagatesErrors(t *testing.T) {
config := scan.Config{
SkipScan: true,
Targets: []string{"192.0.2.1"},
Ports: []string{"8555-8554"},
}
scanner, err := scan.New(config, nil)
require.NoError(t, err)
_, err = scanner.Scan(t.Context())
require.Error(t, err)
assert.ErrorContains(t, err, "invalid port range")
}
+106
View File
@@ -0,0 +1,106 @@
package nmap
import (
"context"
"fmt"
"net/netip"
"strings"
"github.com/Ullaakut/cameradar/v6"
nmaplib "github.com/Ullaakut/nmap/v4"
)
// Reporter reports scan progress and debug information.
type Reporter interface {
Debug(step cameradar.Step, message string)
Progress(step cameradar.Step, message string)
}
// Runner is something that can run an nmap scan.
type Runner interface {
Run(ctx context.Context) (*nmaplib.Run, error)
}
// Scanner scans targets and ports for RTSP streams.
type Scanner struct {
runner Runner
reporter Reporter
}
// New returns a Scanner configured with the provided terminal and scan speed.
func New(scanSpeed int16, targets, ports []string, reporter Reporter) (*Scanner, error) {
runner, err := nmaplib.NewScanner(
nmaplib.WithTargets(targets...),
nmaplib.WithPorts(ports...),
nmaplib.WithServiceInfo(),
nmaplib.WithTimingTemplate(nmaplib.Timing(scanSpeed)),
)
if err != nil {
return nil, fmt.Errorf("creating nmap scanner: %w", err)
}
return &Scanner{
runner: runner,
reporter: reporter,
}, nil
}
// Scan discovers RTSP streams on the configured targets and ports.
func (s *Scanner) Scan(ctx context.Context) ([]cameradar.Stream, error) {
return runScan(ctx, s.runner, s.reporter)
}
func runScan(ctx context.Context, nmap Runner, reporter Reporter) ([]cameradar.Stream, error) {
results, err := nmap.Run(ctx)
if err != nil {
return nil, fmt.Errorf("scanning network: %w", err)
}
for _, warning := range results.Warnings() {
reporter.Debug(cameradar.StepScan, "nmap warning: "+warning)
}
var streams []cameradar.Stream
for _, host := range results.Hosts {
for _, port := range host.Ports {
if port.Status() != "open" {
continue
}
if !strings.Contains(port.Service.Name, "rtsp") {
continue
}
for _, address := range host.Addresses {
addr, err := netip.ParseAddr(address.Addr)
if err != nil {
reporter.Progress(cameradar.StepScan, fmt.Sprintf("Skipping invalid address %q: %v", address.Addr, err))
continue
}
streams = append(streams, cameradar.Stream{
Device: port.Service.Product,
Address: addr,
Port: port.ID,
})
}
}
}
reporter.Progress(cameradar.StepScan, fmt.Sprintf("Found %d RTSP streams", len(streams)))
updateSummary(reporter, streams)
return streams, nil
}
type summaryUpdater interface {
UpdateSummary(streams []cameradar.Stream)
}
func updateSummary(reporter Reporter, streams []cameradar.Stream) {
updater, ok := reporter.(summaryUpdater)
if !ok {
return
}
updater.UpdateSummary(streams)
}
+187
View File
@@ -0,0 +1,187 @@
package nmap
import (
"context"
"errors"
"net/netip"
"sync"
"testing"
"github.com/Ullaakut/cameradar/v6"
nmaplib "github.com/Ullaakut/nmap/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestScanner_Scan(t *testing.T) {
ctx := context.WithValue(t.Context(), contextKey("trace"), "scan")
tests := []struct {
name string
result *nmaplib.Run
err error
wantStreams []cameradar.Stream
wantDebug []string
wantProgress string
wantErrContains string
}{
{
name: "filters non-rtsp and closed ports",
result: buildRun(nmaplib.Host{
Addresses: []nmaplib.Address{
{Addr: "127.0.0.1"},
{Addr: "not-an-ip"},
},
Ports: []nmaplib.Port{
openPort(8554, "rtsp", "ACME"),
closedPort(554, "rtsp", "ACME"),
openPort(80, "http", "ACME"),
},
}),
wantStreams: []cameradar.Stream{
{
Device: "ACME",
Address: netip.MustParseAddr("127.0.0.1"),
Port: 8554,
},
},
wantProgress: "Found 1 RTSP streams",
},
{
name: "collects multiple hosts",
result: buildRun(
nmaplib.Host{
Addresses: []nmaplib.Address{{Addr: "192.0.2.10"}, {Addr: "192.0.2.11"}},
Ports: []nmaplib.Port{
openPort(8554, "rtsp-alt", "Model A"),
},
},
nmaplib.Host{
Addresses: []nmaplib.Address{{Addr: "198.51.100.9"}},
Ports: []nmaplib.Port{
openPort(554, "rtsp", "Model B"),
},
},
),
wantStreams: []cameradar.Stream{
{
Device: "Model A",
Address: netip.MustParseAddr("192.0.2.10"),
Port: 8554,
},
{
Device: "Model A",
Address: netip.MustParseAddr("192.0.2.11"),
Port: 8554,
},
{
Device: "Model B",
Address: netip.MustParseAddr("198.51.100.9"),
Port: 554,
},
},
wantProgress: "Found 3 RTSP streams",
},
{
name: "returns error when scan fails",
err: errors.New("scan failed"),
wantErrContains: "scanning network",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
reporter := &recordingReporter{}
scanner, err := New(4, []string{"192.0.2.1"}, []string{"554", "8554"}, reporter)
require.NoError(t, err)
scanner.runner = fakeRunner{result: test.result, err: test.err}
streams, err := scanner.Scan(ctx)
if test.wantErrContains != "" {
require.Error(t, err)
assert.ErrorContains(t, err, test.wantErrContains)
assert.Empty(t, streams)
assert.Empty(t, reporter.progress)
assert.Equal(t, test.wantDebug, reporter.debug)
return
}
require.NoError(t, err)
assert.Equal(t, test.wantStreams, streams)
assert.Equal(t, test.wantDebug, reporter.debug)
assert.Contains(t, reporter.progress, test.wantProgress)
})
}
}
type contextKey string
type fakeRunner struct {
result *nmaplib.Run
err error
}
func (f fakeRunner) Run(context.Context) (*nmaplib.Run, error) {
return f.result, f.err
}
type recordingReporter struct {
mu sync.Mutex
debug []string
progress []string
}
func (r *recordingReporter) Start(cameradar.Step, string) {}
func (r *recordingReporter) Done(cameradar.Step, string) {}
func (r *recordingReporter) Progress(_ cameradar.Step, message string) {
r.mu.Lock()
defer r.mu.Unlock()
r.progress = append(r.progress, message)
}
func (r *recordingReporter) Debug(_ cameradar.Step, message string) {
r.mu.Lock()
defer r.mu.Unlock()
r.debug = append(r.debug, message)
}
func (r *recordingReporter) Error(cameradar.Step, error) {}
func (r *recordingReporter) Summary([]cameradar.Stream, error) {}
func (r *recordingReporter) Close() {}
func buildRun(hosts ...nmaplib.Host) *nmaplib.Run {
return &nmaplib.Run{Hosts: hosts}
}
func openPort(id uint16, serviceName, product string) nmaplib.Port {
return nmaplib.Port{
ID: id,
State: nmaplib.State{
State: string(nmaplib.Open),
},
Service: nmaplib.Service{
Name: serviceName,
Product: product,
},
}
}
func closedPort(id uint16, serviceName, product string) nmaplib.Port {
return nmaplib.Port{
ID: id,
State: nmaplib.State{
State: string(nmaplib.Closed),
},
Service: nmaplib.Service{
Name: serviceName,
Product: product,
},
}
}
+338
View File
@@ -0,0 +1,338 @@
package skip
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"strconv"
"strings"
"github.com/Ullaakut/cameradar/v6"
)
// Scanner is a stream scanner that skips discovery and treats every target/port as a stream.
type Scanner struct {
targets []string
ports []string
}
// New builds a scanner that skips discovery and treats every target/port as a stream.
func New(targets, ports []string) *Scanner {
return &Scanner{
targets: targets,
ports: ports,
}
}
// Scan returns the precomputed list of streams.
func (s *Scanner) Scan(ctx context.Context) ([]cameradar.Stream, error) {
return buildStreamsFromTargets(ctx, s.targets, s.ports)
}
func buildStreamsFromTargets(ctx context.Context, targets, ports []string) ([]cameradar.Stream, error) {
resolvedPorts, err := parsePorts(ctx, ports)
if err != nil {
return nil, err
}
if len(resolvedPorts) == 0 {
return nil, errors.New("no valid ports provided")
}
resolvedTargets, err := expandTargets(ctx, targets)
if err != nil {
return nil, err
}
if len(resolvedTargets) == 0 {
return nil, errors.New("no valid target addresses resolved")
}
streams := make([]cameradar.Stream, 0, len(resolvedTargets)*len(resolvedPorts))
for _, addr := range resolvedTargets {
for _, port := range resolvedPorts {
streams = append(streams, cameradar.Stream{
Address: addr,
Port: port,
})
}
}
return streams, nil
}
func parsePorts(ctx context.Context, ports []string) ([]uint16, error) {
seen := make(map[uint16]struct{})
resolved := make([]uint16, 0, len(ports))
for _, entry := range ports {
for raw := range strings.SplitSeq(entry, ",") {
value := strings.TrimSpace(raw)
if value == "" {
continue
}
values, err := parsePortValue(ctx, value)
if err != nil {
return nil, err
}
for _, port := range values {
if _, exists := seen[port]; exists {
continue
}
seen[port] = struct{}{}
resolved = append(resolved, port)
}
}
}
return resolved, nil
}
func parsePortValue(ctx context.Context, value string) ([]uint16, error) {
if strings.Contains(value, "-") {
parts := strings.SplitN(value, "-", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid port range %q", value)
}
start, err := parsePortNumber(strings.TrimSpace(parts[0]))
if err != nil {
return nil, fmt.Errorf("invalid port range %q: %w", value, err)
}
end, err := parsePortNumber(strings.TrimSpace(parts[1]))
if err != nil {
return nil, fmt.Errorf("invalid port range %q: %w", value, err)
}
if start > end {
return nil, fmt.Errorf("invalid port range %q", value)
}
ports := make([]uint16, 0, end-start+1)
for port := start; port <= end; port++ {
ports = append(ports, port)
}
return ports, nil
}
port, err := parsePortNumber(value)
if err == nil {
return []uint16{port}, nil
}
servicePort, lookupErr := net.DefaultResolver.LookupPort(ctx, "tcp", value)
if lookupErr != nil {
return nil, fmt.Errorf("invalid port %q", value)
}
if servicePort < 1 || servicePort > 65535 {
return nil, fmt.Errorf("port %d out of range", servicePort)
}
return []uint16{uint16(servicePort)}, nil
}
func parsePortNumber(value string) (uint16, error) {
port, err := strconv.Atoi(value)
if err != nil {
return 0, err
}
if port < 1 || port > 65535 {
return 0, fmt.Errorf("port %d out of range", port)
}
return uint16(port), nil
}
func expandTargets(ctx context.Context, targets []string) ([]netip.Addr, error) {
seen := make(map[netip.Addr]struct{})
resolved := make([]netip.Addr, 0, len(targets))
for _, target := range targets {
value := strings.TrimSpace(target)
if value == "" {
continue
}
addrs, err := parseTargetAddrs(ctx, value)
if err != nil {
return nil, err
}
for _, addr := range addrs {
if !addr.IsValid() {
continue
}
if _, exists := seen[addr]; exists {
continue
}
seen[addr] = struct{}{}
resolved = append(resolved, addr)
}
}
return resolved, nil
}
func parseTargetAddrs(ctx context.Context, target string) ([]netip.Addr, error) {
prefix, err := netip.ParsePrefix(target)
if err == nil { // Return early.
return expandPrefix(prefix), nil
}
if strings.Contains(target, "-") {
addrs, ok, err := parseIPv4Range(target)
if ok {
return addrs, err
}
}
addr, err := netip.ParseAddr(target)
if err == nil { // Return early.
return []netip.Addr{addr}, nil
}
ips, err := net.DefaultResolver.LookupIPAddr(ctx, target)
if err != nil {
return nil, fmt.Errorf("resolving hostname %q: %w", target, err)
}
addrs := make([]netip.Addr, 0, len(ips))
for _, ip := range ips {
addr, ok := netip.AddrFromSlice(ip.IP)
if !ok {
continue
}
addrs = append(addrs, addr.Unmap())
}
if len(addrs) == 0 {
return nil, fmt.Errorf("no ip addresses found for hostname %q", target)
}
return addrs, nil
}
func expandPrefix(prefix netip.Prefix) []netip.Addr {
if !prefix.IsValid() {
return nil
}
prefix = prefix.Masked()
addr := prefix.Addr()
addrs := make([]netip.Addr, 0, 16)
for current := addr; prefix.Contains(current); {
addrs = append(addrs, current)
next := current.Next()
if !next.IsValid() {
break
}
current = next
}
return addrs
}
type octetRange struct {
start int
end int
}
func parseIPv4Range(target string) ([]netip.Addr, bool, error) {
parts := strings.Split(target, ".")
if len(parts) != 4 {
return nil, false, nil
}
ranges := make([]octetRange, 4)
for i, part := range parts {
parsed, ok, err := parseOctetRange(part)
if err != nil {
return nil, true, err
}
if !ok {
return nil, false, nil
}
ranges[i] = parsed
}
addrs := make([]netip.Addr, 0, 16)
for first := ranges[0].start; first <= ranges[0].end; first++ {
for second := ranges[1].start; second <= ranges[1].end; second++ {
for third := ranges[2].start; third <= ranges[2].end; third++ {
for fourth := ranges[3].start; fourth <= ranges[3].end; fourth++ {
addrs = append(addrs, netip.AddrFrom4([4]byte{
byte(first),
byte(second),
byte(third),
byte(fourth),
}))
}
}
}
}
return addrs, true, nil
}
func parseOctetRange(value string) (octetRange, bool, error) {
value = strings.TrimSpace(value)
if value == "" {
return octetRange{}, false, nil
}
if strings.Contains(value, "-") {
parts := strings.SplitN(value, "-", 2)
if len(parts) != 2 {
return octetRange{}, true, fmt.Errorf("invalid range %q", value)
}
start, err := parseOctetValue(strings.TrimSpace(parts[0]))
if err != nil {
return octetRange{}, true, err
}
end, err := parseOctetValue(strings.TrimSpace(parts[1]))
if err != nil {
return octetRange{}, true, err
}
if start > end {
return octetRange{}, true, fmt.Errorf("invalid range %q", value)
}
return octetRange{start: start, end: end}, true, nil
}
if !isDigits(value) {
return octetRange{}, false, nil
}
octet, err := parseOctetValue(value)
if err != nil {
return octetRange{}, true, err
}
return octetRange{start: octet, end: octet}, true, nil
}
func parseOctetValue(value string) (int, error) {
if !isDigits(value) {
return 0, fmt.Errorf("invalid octet %q", value)
}
parsed, err := strconv.Atoi(value)
if err != nil {
return 0, fmt.Errorf("invalid octet %q", value)
}
if parsed < 0 || parsed > 255 {
return 0, fmt.Errorf("octet %d out of range", parsed)
}
return parsed, nil
}
func isDigits(value string) bool {
for _, r := range value {
if r < '0' || r > '9' {
return false
}
}
return value != ""
}
+104
View File
@@ -0,0 +1,104 @@
package skip_test
import (
"net/netip"
"strconv"
"testing"
"github.com/Ullaakut/cameradar/v6/internal/scan/skip"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNew_ExpandsTargetsAndPorts(t *testing.T) {
targets := []string{
"192.0.2.0/30",
"localhost",
"192.0.2.15",
"192.0.2.10-11",
}
ports := []string{"554", "8554-8555"}
scanner := skip.New(targets, ports)
streams, err := scanner.Scan(t.Context())
require.NoError(t, err)
addrs := []netip.Addr{
netip.MustParseAddr("127.0.0.1"),
netip.MustParseAddr("192.0.2.0"),
netip.MustParseAddr("192.0.2.1"),
netip.MustParseAddr("192.0.2.2"),
netip.MustParseAddr("192.0.2.3"),
netip.MustParseAddr("192.0.2.10"),
netip.MustParseAddr("192.0.2.11"),
netip.MustParseAddr("192.0.2.15"),
}
portsExpected := []uint16{554, 8554, 8555}
var want []string
for _, addr := range addrs {
for _, port := range portsExpected {
want = append(want, addr.String()+":"+strconv.Itoa(int(port)))
}
}
var got []string
for _, stream := range streams {
got = append(got, stream.Address.String()+":"+strconv.Itoa(int(stream.Port)))
}
assert.ElementsMatch(t, want, got)
}
func TestNew_ReturnsErrorOnInvalidPortRange(t *testing.T) {
scanner := skip.New([]string{"192.0.2.1"}, []string{"8555-8554"})
_, err := scanner.Scan(t.Context())
require.Error(t, err)
assert.ErrorContains(t, err, "invalid port range")
}
func TestNew_ReturnsErrorOnEmptyTargets(t *testing.T) {
scanner := skip.New([]string{}, []string{"554"})
_, err := scanner.Scan(t.Context())
require.Error(t, err)
assert.ErrorContains(t, err, "no valid target addresses resolved")
}
func TestNew_ResolvesServicePorts(t *testing.T) {
scanner := skip.New([]string{"127.0.0.1"}, []string{"http"})
streams, err := scanner.Scan(t.Context())
require.NoError(t, err)
require.Len(t, streams, 1)
assert.Equal(t, netip.MustParseAddr("127.0.0.1"), streams[0].Address)
assert.Equal(t, uint16(80), streams[0].Port)
}
func TestNew_ReturnsErrorOnUnknownServicePort(t *testing.T) {
scanner := skip.New([]string{"127.0.0.1"}, []string{"not-a-service"})
_, err := scanner.Scan(t.Context())
require.Error(t, err)
assert.ErrorContains(t, err, "invalid port")
}
func TestNew_ResolvesHostnames(t *testing.T) {
scanner := skip.New([]string{"localhost"}, []string{"554"})
streams, err := scanner.Scan(t.Context())
require.NoError(t, err)
require.NotEmpty(t, streams)
assert.Equal(t, netip.MustParseAddr("127.0.0.1"), streams[0].Address)
}
func TestNew_ReturnsErrorOnHostnameLookupFailure(t *testing.T) {
scanner := skip.New([]string{"does-not-exist.invalid"}, []string{"554"})
_, err := scanner.Scan(t.Context())
require.Error(t, err)
assert.ErrorContains(t, err, "resolving hostname")
}
+139
View File
@@ -0,0 +1,139 @@
package scan
import (
"fmt"
"math/bits"
"net/netip"
"strconv"
"strings"
)
func expandTargetsForScan(targets []string) ([]string, error) {
expanded := make([]string, 0, len(targets))
for _, target := range targets {
value := strings.TrimSpace(target)
if value == "" {
continue
}
addrs, ok, err := parseIPv4RangePair(value)
if err != nil {
return nil, err
}
if ok {
expanded = append(expanded, addrs...)
continue
}
expanded = append(expanded, value)
}
return expanded, nil
}
// Parse masscan range formats.
func parseIPv4RangePair(target string) ([]string, bool, error) {
parts := strings.SplitN(target, "-", 2)
if len(parts) != 2 {
return nil, false, nil
}
startValue := strings.TrimSpace(parts[0])
endValue := strings.TrimSpace(parts[1])
if startValue == "" || endValue == "" {
return nil, false, nil
}
// Fall through if this is in nmap range format.
if endIsOctet(endValue) {
return nil, false, nil
}
startAddr, startOK := parseIPv4Addr(startValue)
endAddr, endOK := parseIPv4Addr(endValue)
if !startOK && !endOK { // Allows the case where the target is just a hostname with a dash.
return nil, false, nil
}
if !startOK || !endOK { // Prevents the case where one is an address and the other part is not.
return nil, false, fmt.Errorf("invalid range %q", target)
}
startAddr = startAddr.Unmap()
endAddr = endAddr.Unmap()
if !startAddr.Is4() || !endAddr.Is4() {
return nil, true, fmt.Errorf("invalid range %q", target)
}
start := ipv4ToUint32(startAddr)
end := ipv4ToUint32(endAddr)
if start > end {
return nil, true, fmt.Errorf("invalid range %q", target)
}
return expandIPv4RangeToTargets(start, end), true, nil
}
func parseIPv4Addr(value string) (netip.Addr, bool) {
addr, err := netip.ParseAddr(value)
if err != nil {
return netip.Addr{}, false
}
return addr, true
}
func endIsOctet(value string) bool {
parsed, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil {
return false
}
return parsed >= 0 && parsed <= 255
}
func expandIPv4RangeToTargets(start, end uint32) []string {
if start > end {
return nil
}
const maxUint32 = uint64(^uint32(0))
remaining := uint64(end) - uint64(start) + 1
results := make([]string, 0, 16)
for current := uint64(start); remaining > 0; {
if current > maxUint32 {
return results
}
current32 := uint32(current)
maxSize := uint64(1) << bits.TrailingZeros32(current32)
for maxSize > remaining {
maxSize >>= 1
}
prefixLen := 32 - (bits.Len64(maxSize) - 1)
addr := uint32ToIPv4(current32)
if maxSize == 1 {
results = append(results, addr.String())
} else {
results = append(results, fmt.Sprintf("%s/%d", addr.String(), prefixLen))
}
current += maxSize
remaining -= maxSize
}
return results
}
func ipv4ToUint32(addr netip.Addr) uint32 {
value := addr.As4()
return uint32(value[0])<<24 | uint32(value[1])<<16 | uint32(value[2])<<8 | uint32(value[3])
}
func uint32ToIPv4(value uint32) netip.Addr {
return netip.AddrFrom4([4]byte{
byte(value >> 24),
byte(value >> 16),
byte(value >> 8),
byte(value),
})
}
+73
View File
@@ -0,0 +1,73 @@
package scan
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExpandTargetsForScan_ExpandsFullIPv4Range(t *testing.T) {
targets := []string{
"192.0.2.10-192.0.2.12",
"192.168.1.140-255",
"192.0.2.0/30",
"localhost",
"",
}
got, err := expandTargetsForScan(targets)
require.NoError(t, err)
assert.ElementsMatch(t, []string{
"192.0.2.10/31",
"192.0.2.12",
"192.168.1.140-255",
"192.0.2.0/30",
"localhost",
}, got)
}
func TestExpandTargetsForScan_ReturnsErrorOnInvalidRange(t *testing.T) {
t.Run("inverted range", func(t *testing.T) {
_, err := expandTargetsForScan([]string{"192.0.2.12-192.0.2.10"})
require.Error(t, err)
assert.ErrorContains(t, err, "invalid range")
})
t.Run("invalid range", func(t *testing.T) {
_, err := expandTargetsForScan([]string{"192.0.2.12-foo"})
require.Error(t, err)
assert.ErrorContains(t, err, "invalid range")
})
t.Run("hostname with dash", func(t *testing.T) {
tgts, err := expandTargetsForScan([]string{"my-host.com"})
require.NoError(t, err)
assert.Equal(t, []string{"my-host.com"}, tgts)
})
t.Run("ends with dash", func(t *testing.T) {
tgts, err := expandTargetsForScan([]string{"a-"})
require.NoError(t, err)
assert.Equal(t, []string{"a-"}, tgts)
})
t.Run("starts with dash", func(t *testing.T) {
tgts, err := expandTargetsForScan([]string{"-a"})
require.NoError(t, err)
assert.Equal(t, []string{"-a"}, tgts)
})
t.Run("only a dash", func(t *testing.T) {
tgts, err := expandTargetsForScan([]string{"-"})
require.NoError(t, err)
assert.Equal(t, []string{"-"}, tgts)
})
t.Run("nmap format", func(t *testing.T) {
tgts, err := expandTargetsForScan([]string{"192.168.1.10-255"})
require.NoError(t, err)
assert.Equal(t, []string{"192.168.1.10-255"}, tgts)
})
}