diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 649f97e..b6cd2da 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,9 +16,9 @@ jobs: uses: actions/checkout@v2 - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: 1.17 + go-version: "1.20.0" - name: Cache Go modules uses: actions/cache@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 36c6c18..7d7d330 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,9 +15,9 @@ jobs: uses: actions/checkout@v2 - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: 1.17 + go-version: "1.20.0" - name: Cache Go modules uses: actions/cache@v2 @@ -60,7 +60,7 @@ jobs: run: goveralls -coverprofile=c.out -service=github - name: Install staticcheck - run: go install honnef.co/go/tools/cmd/staticcheck@latest + run: go install honnef.co/go/tools/cmd/staticcheck@v0.4.3 - name: Run staticcheck for possible optimizations run: staticcheck -tests=false diff --git a/.gitignore b/.gitignore index 7d65d1f..d02b563 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ ## Golang +vendor/* ### Binaries for programs and plugins *.exe diff --git a/README.md b/README.md index 9638057..8e3e37c 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ - PkgGoDev github.com/Ullaakut/nmap/v2 - - + PkgGoDev github.com/Ullaakut/nmap/v3 + + @@ -40,13 +40,13 @@ Most pentest tools are currently written using Python and not Go, because it is - [x] All of `nmap`'s native options. - [x] Additional [idiomatic go filters](examples/service_detection/main.go#L19) for filtering hosts and ports. -- [x] [Cancellable contexts support](examples/basic_scan/main.go). - [x] Helpful enums for nmap commands. (time templates, os families, port states, etc.) - [x] Complete documentation of each option, mostly insipred from nmap's documentation. - -## TODO - -- [ ] Add asynchronous scan, send scan progress percentage and time estimation through channel +- [x] Run a nmap scan asynchronously. +- [x] Scan progress can be piped through a channel. +- [x] Write the nmap output to a given file while also parsing it to the struct. +- [x] Stream the nmap output to an `io.Writer` interface while also parsing it to the struct. +- [x] Functionality to show local interfaces and routes. ## Simple example @@ -59,47 +59,46 @@ import ( "log" "time" - "github.com/Ullaakut/nmap/v2" + "github.com/Ullaakut/nmap/v3" ) func main() { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() - - // Equivalent to `/usr/local/bin/nmap -p 80,443,843 google.com facebook.com youtube.com`, - // with a 5 minute timeout. - scanner, err := nmap.NewScanner( - nmap.WithTargets("google.com", "facebook.com", "youtube.com"), - nmap.WithPorts("80,443,843"), - nmap.WithContext(ctx), - ) - if err != nil { - log.Fatalf("unable to create nmap scanner: %v", err) - } - - result, warnings, err := scanner.Run() - if err != nil { - log.Fatalf("unable to run nmap scan: %v", err) - } - - if warnings != nil { - log.Printf("Warnings: \n %v", warnings) - } - - // Use the results to print an example output - for _, host := range result.Hosts { - if len(host.Ports) == 0 || len(host.Addresses) == 0 { - continue - } - - fmt.Printf("Host %q:\n", host.Addresses[0]) - - for _, port := range host.Ports { - fmt.Printf("\tPort %d/%s %s %s\n", port.ID, port.Protocol, port.State, port.Service.Name) - } - } - - fmt.Printf("Nmap done: %d hosts up scanned in %3f seconds\n", len(result.Hosts), result.Stats.Finished.Elapsed) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + // Equivalent to `/usr/local/bin/nmap -p 80,443,843 google.com facebook.com youtube.com`, + // with a 5-minute timeout. + scanner, err := nmap.NewScanner( + ctx, + nmap.WithTargets("google.com", "facebook.com", "youtube.com"), + nmap.WithPorts("80,443,843"), + ) + if err != nil { + log.Fatalf("unable to create nmap scanner: %v", err) + } + + result, warnings, err := scanner.Run() + if len(*warnings) > 0 { + log.Printf("run finished with warnings: %s\n", *warnings) // Warnings are non-critical errors from nmap. + } + if err != nil { + log.Fatalf("unable to run nmap scan: %v", err) + } + + // Use the results to print an example output + for _, host := range result.Hosts { + if len(host.Ports) == 0 || len(host.Addresses) == 0 { + continue + } + + fmt.Printf("Host %q:\n", host.Addresses[0]) + + for _, port := range host.Ports { + fmt.Printf("\tPort %d/%s %s %s\n", port.ID, port.Protocol, port.State, port.Service.Name) + } + } + + fmt.Printf("Nmap done: %d hosts up scanned in %.2f seconds\n", len(result.Hosts), result.Stats.Finished.Elapsed) } ``` @@ -131,9 +130,14 @@ Nmap done: 3 hosts up scanned in 1.29 seconds More examples: +- [Basic scan](examples/basic_scan/main.go) +- [Basic scan but asynchronously](examples/basic_scan_async/main.go) +- [Basic scan with nmap progress piped through](examples/basic_scan_progress/main.go) +- [Basic scan with output to a streamer](examples/basic_scan_streamer_interface/main.go) - [Count hosts for each operating system on a network](examples/count_hosts_by_os/main.go) - [Service detection](examples/service_detection/main.go) - [IP address spoofing and decoys](examples/spoof_and_decoys/main.go) +- [List local interfaces](examples/list_interfaces/main.go) ## External resources diff --git a/examples/basic_scan/main.go b/examples/basic_scan/main.go index 0e92557..02a0770 100644 --- a/examples/basic_scan/main.go +++ b/examples/basic_scan/main.go @@ -6,7 +6,7 @@ import ( "log" "time" - "github.com/Ullaakut/nmap/v2" + "github.com/Ullaakut/nmap/v3" ) func main() { @@ -14,17 +14,20 @@ func main() { defer cancel() // Equivalent to `/usr/local/bin/nmap -p 80,443,843 google.com facebook.com youtube.com`, - // with a 5 minute timeout. + // with a 5-minute timeout. scanner, err := nmap.NewScanner( + ctx, nmap.WithTargets("google.com", "facebook.com", "youtube.com"), nmap.WithPorts("80,443,843"), - nmap.WithContext(ctx), ) if err != nil { log.Fatalf("unable to create nmap scanner: %v", err) } - result, _, err := scanner.Run() + result, warnings, err := scanner.Run() + if len(*warnings) > 0 { + log.Printf("run finished with warnings: %s\n", *warnings) // Warnings are non-critical errors from nmap. + } if err != nil { log.Fatalf("unable to run nmap scan: %v", err) } diff --git a/examples/basic_scan_async/main.go b/examples/basic_scan_async/main.go index db94846..0f8f8fe 100644 --- a/examples/basic_scan_async/main.go +++ b/examples/basic_scan_async/main.go @@ -1,21 +1,18 @@ package main import ( + "context" "fmt" "log" - "strings" - "github.com/Ullaakut/nmap/v2" + "github.com/Ullaakut/nmap/v3" ) func main() { - var ( - resultBytes []byte - errorBytes []byte - ) // Equivalent to `/usr/local/bin/nmap -p 80,443,843 google.com facebook.com youtube.com`, - // with a 5 minute timeout. + // with a 5-minute timeout. s, err := nmap.NewScanner( + context.Background(), nmap.WithTargets("google.com", "facebook.com", "youtube.com"), nmap.WithPorts("80,443,843"), ) @@ -24,45 +21,18 @@ func main() { } // Executes asynchronously, allowing results to be streamed in real time. - if err := s.RunAsync(); err != nil { - panic(err) + done := make(chan error) + result, warnings, err := s.Async(done).Run() + if err != nil { + log.Fatal(err) } - // Connect to stdout of scanner. - stdout := s.GetStdout() - - // Connect to stderr of scanner. - stderr := s.GetStderr() - - // Goroutine to watch for stdout and print to screen. Additionally it stores - // the bytes intoa variable for processiing later. - go func() { - for stdout.Scan() { - fmt.Println(stdout.Text()) - resultBytes = append(resultBytes, stdout.Bytes()...) - } - }() - - // Goroutine to watch for stderr and print to screen. Additionally it stores - // the bytes intoa variable for processiing later. - go func() { - for stderr.Scan() { - errorBytes = append(errorBytes, stderr.Bytes()...) - } - }() - // Blocks main until the scan has completed. - if err := s.Wait(); err != nil { - panic(err) - } - - // Parsing the results into corresponding structs. - result, err := nmap.Parse(resultBytes) - - // Parsing the results into the NmapError slice of our nmap Struct. - result.NmapErrors = strings.Split(string(errorBytes), "\n") - if err != nil { - panic(err) + if err := <-done; err != nil { + if len(*warnings) > 0 { + log.Printf("run finished with warnings: %s\n", *warnings) // Warnings are non-critical errors from nmap. + } + log.Fatal(err) } // Use the results to print an example output diff --git a/examples/basic_scan_progress/main.go b/examples/basic_scan_progress/main.go index 668017c..93c9442 100644 --- a/examples/basic_scan_progress/main.go +++ b/examples/basic_scan_progress/main.go @@ -1,16 +1,18 @@ package main import ( + "context" "fmt" "log" - "github.com/Ullaakut/nmap/v2" + "github.com/Ullaakut/nmap/v3" ) func main() { scanner, err := nmap.NewScanner( + context.Background(), nmap.WithTargets("localhost"), - nmap.WithPorts("1-4000"), + nmap.WithPorts("1-10000"), nmap.WithServiceInfo(), nmap.WithVerbosity(3), ) @@ -27,7 +29,10 @@ func main() { } }() - result, _, err := scanner.RunWithProgress(progress) + result, warnings, err := scanner.Progress(progress).Run() + if len(*warnings) > 0 { + log.Printf("run finished with warnings: %s\n", *warnings) // Warnings are non-critical errors from nmap. + } if err != nil { log.Fatalf("unable to run nmap scan: %v", err) } diff --git a/examples/basic_scan_streamer_interface/main.go b/examples/basic_scan_streamer_interface/main.go index 6e0be56..8c536ac 100644 --- a/examples/basic_scan_streamer_interface/main.go +++ b/examples/basic_scan_streamer_interface/main.go @@ -1,45 +1,17 @@ package main import ( + "context" "fmt" - "io/ioutil" "log" - "strings" + "os" - "github.com/Ullaakut/nmap/v2" + "github.com/Ullaakut/nmap/v3" ) -// CustomType is your custom type in code. -// You just have to make it a Streamer. -type CustomType struct { - nmap.Streamer - File string -} - -// Write is a function that handles the normal nmap stdout. -func (c *CustomType) Write(d []byte) (int, error) { - lines := string(d) - - if strings.Contains(lines, "Stats: ") { - fmt.Print(lines) - } - return len(d), nil -} - -// Bytes returns scan result bytes. -func (c *CustomType) Bytes() []byte { - data, err := ioutil.ReadFile(c.File) - if err != nil { - data = append(data, "\ncould not read File"...) - } - return data -} - func main() { - cType := &CustomType{ - File: "/tmp/output.xml", - } scanner, err := nmap.NewScanner( + context.Background(), nmap.WithTargets("localhost"), nmap.WithPorts("1-4000"), nmap.WithServiceInfo(), @@ -49,16 +21,12 @@ func main() { log.Fatalf("unable to create nmap scanner: %v", err) } - warnings, err := scanner.RunWithStreamer(cType, cType.File) - if err != nil { - log.Fatalf("unable to run nmap scan: %v", err) + result, warnings, err := scanner.Streamer(os.Stdout).Run() + if len(*warnings) > 0 { + log.Printf("run finished with warnings: %s\n", *warnings) // Warnings are non-critical errors from nmap. } - - fmt.Printf("Nmap warnings: %v\n", warnings) - - result, err := nmap.Parse(cType.Bytes()) if err != nil { - log.Fatalf("unable to parse nmap output: %v", err) + log.Fatalf("unable to run nmap scan: %v", err) } fmt.Printf("Nmap done: %d hosts up scanned in %.2f seconds\n", len(result.Hosts), result.Stats.Finished.Elapsed) diff --git a/examples/count_hosts_by_os/main.go b/examples/count_hosts_by_os/main.go index a106bc7..3e1ad7f 100644 --- a/examples/count_hosts_by_os/main.go +++ b/examples/count_hosts_by_os/main.go @@ -1,26 +1,31 @@ package main import ( + "context" "fmt" "log" - "github.com/Ullaakut/nmap/v2" - osfamily "github.com/Ullaakut/nmap/v2/pkg/osfamilies" + "github.com/Ullaakut/nmap/v3" + osfamily "github.com/Ullaakut/nmap/v3/pkg/osfamilies" ) func main() { // Equivalent to // nmap -F -O 192.168.0.0/24 scanner, err := nmap.NewScanner( + context.Background(), nmap.WithTargets("192.168.0.0/24"), nmap.WithFastMode(), - nmap.WithOSDetection(), + nmap.WithOSDetection(), // Needs to run with sudo ) if err != nil { log.Fatalf("unable to create nmap scanner: %v", err) } - result, _, err := scanner.Run() + result, warnings, err := scanner.Run() + if len(*warnings) > 0 { + log.Printf("run finished with warnings: %s\n", *warnings) // Warnings are non-critical errors from nmap. + } if err != nil { log.Fatalf("nmap scan failed: %v", err) } diff --git a/examples/list_interfaces/main.go b/examples/list_interfaces/main.go index c3f8703..8f881e4 100644 --- a/examples/list_interfaces/main.go +++ b/examples/list_interfaces/main.go @@ -1,14 +1,16 @@ package main import ( + "context" "encoding/json" "fmt" - "github.com/Ullaakut/nmap/v2" "log" + + "github.com/Ullaakut/nmap/v3" ) func main() { - scanner, err := nmap.NewScanner() + scanner, err := nmap.NewScanner(context.Background()) if err != nil { log.Fatalf("unable to create nmap scanner: %v", err) } diff --git a/examples/service_detection/main.go b/examples/service_detection/main.go index 91318c6..d931847 100644 --- a/examples/service_detection/main.go +++ b/examples/service_detection/main.go @@ -1,18 +1,20 @@ package main import ( + "context" "fmt" "log" - "github.com/Ullaakut/nmap/v2" + "github.com/Ullaakut/nmap/v3" ) func main() { // Equivalent to // nmap -sV -T4 192.168.0.0/24 with a filter to remove non-RTSP ports. scanner, err := nmap.NewScanner( + context.Background(), nmap.WithTargets("192.168.0.0/24"), - nmap.WithPorts("554", "8554"), + nmap.WithPorts("80", "554", "8554"), nmap.WithServiceInfo(), nmap.WithTimingTemplate(nmap.TimingAggressive), // Filter out ports that are not RTSP @@ -35,7 +37,10 @@ func main() { log.Fatalf("unable to create nmap scanner: %v", err) } - result, _, err := scanner.Run() + result, warnings, err := scanner.Run() + if len(*warnings) > 0 { + log.Printf("run finished with warnings: %s\n", *warnings) // Warnings are non-critical errors from nmap. + } if err != nil { log.Fatalf("nmap scan failed: %v", err) } diff --git a/examples/spoof_and_decoys/main.go b/examples/spoof_and_decoys/main.go index 5626bb8..f011f55 100644 --- a/examples/spoof_and_decoys/main.go +++ b/examples/spoof_and_decoys/main.go @@ -1,18 +1,38 @@ package main import ( + "context" "fmt" "log" - "github.com/Ullaakut/nmap/v2" + "github.com/Ullaakut/nmap/v3" ) func main() { + ifaceScanner, err := nmap.NewScanner(context.Background()) + if err != nil { + log.Fatalf("unable to create nmap scanner: %v", err) + } + + interfaceList, err := ifaceScanner.GetInterfaceList() + if err != nil { + log.Fatalf("could not get interface list: %v", err) + } + + if len(interfaceList.Interfaces) == 0 { + log.Fatal("no interface to scan with") + } + + lastInterfaceIndex := len(interfaceList.Interfaces) - 1 + interfaceToScan := interfaceList.Interfaces[lastInterfaceIndex].Device + // Equivalent to - // nmap -sS 192.168.0.10 \ + // nmap -S 192.168.0.10 \ // -D 192.168.0.2,192.168.0.3,192.168.0.4,192.168.0.5,192.168.0.6,ME,192.168.0.8 \ // 192.168.0.72`. scanner, err := nmap.NewScanner( + context.Background(), + nmap.WithInterface(interfaceToScan), nmap.WithTargets("192.168.0.72"), nmap.WithSpoofIPAddress("192.168.0.10"), nmap.WithDecoys( @@ -29,7 +49,12 @@ func main() { log.Fatalf("unable to create nmap scanner: %v", err) } - result, _, err := scanner.Run() + fmt.Println("Running the following nmap command:", scanner.Args()) + + result, warnings, err := scanner.Run() + if len(*warnings) > 0 { + log.Printf("run finished with warnings: %s\n", *warnings) // Warnings are non-critical errors from nmap. + } if err != nil { log.Fatalf("nmap scan failed: %v", err) } diff --git a/examples_test.go b/examples_test.go index d23f680..9162463 100644 --- a/examples_test.go +++ b/examples_test.go @@ -1,6 +1,7 @@ package nmap import ( + "context" "fmt" "log" ) @@ -9,6 +10,7 @@ import ( // that are given to nmap. func ExampleScanner_simple() { s, err := NewScanner( + context.Background(), WithTargets("google.com", "facebook.com", "youtube.com"), WithCustomDNSServers("8.8.8.8", "8.8.4.4"), WithTimingTemplate(TimingFastest), @@ -34,6 +36,7 @@ func ExampleScanner_simple() { // and ports. func ExampleScanner_filters() { s, err := NewScanner( + context.Background(), WithTargets("google.com", "facebook.com"), WithPorts("843"), WithFilterHost(func(h Host) bool { diff --git a/go.mod b/go.mod index c3dadc3..8a805bc 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,14 @@ -module github.com/Ullaakut/nmap/v2 +module github.com/Ullaakut/nmap/v3 -go 1.15 +go 1.20 require ( - github.com/pkg/errors v0.9.1 - github.com/stretchr/testify v1.7.0 - golang.org/x/sync v0.0.0-20201207232520-09787c993a3a + github.com/stretchr/testify v1.8.2 + golang.org/x/sync v0.1.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b6905f6..c050204 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,19 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/iflist_test.go b/iflist_test.go index 5ecbc0f..fcfe1af 100644 --- a/iflist_test.go +++ b/iflist_test.go @@ -1,6 +1,7 @@ package nmap import ( + "context" "net" "testing" @@ -8,7 +9,7 @@ import ( ) func TestScanner_GetInterfaceList(t *testing.T) { - scanner, err := NewScanner(WithBinaryPath("tests/scripts/fake_nmap_iflist.sh")) + scanner, err := NewScanner(context.Background(), WithBinaryPath("tests/scripts/fake_nmap_iflist.sh")) assert.NoError(t, err) result, err := scanner.GetInterfaceList() diff --git a/nmap.go b/nmap.go index a8b07bc..79940b9 100644 --- a/nmap.go +++ b/nmap.go @@ -2,7 +2,6 @@ package nmap import ( - "bufio" "bytes" "context" "encoding/xml" @@ -13,7 +12,6 @@ import ( "syscall" "time" - "github.com/pkg/errors" "golang.org/x/sync/errgroup" ) @@ -22,15 +20,8 @@ type ScanRunner interface { Run() (result *Run, warnings []string, err error) } -// Streamer constantly streams the stdout. -type Streamer interface { - Write(d []byte) (int, error) - Bytes() []byte -} - -// Scanner represents an Nmap scanner. +// Scanner represents n Nmap scanner. type Scanner struct { - cmd *exec.Cmd modifySysProcAttr func(*syscall.SysProcAttr) args []string @@ -40,7 +31,10 @@ type Scanner struct { portFilter func(Port) bool hostFilter func(Host) bool - stderr, stdout bufio.Scanner + doneAsync chan error + liveProgress chan float32 + streamer io.Writer + toFile *string } // Option is a function that is used for grouping of Scanner options. @@ -48,8 +42,13 @@ type Scanner struct { type Option func(*Scanner) // NewScanner creates a new Scanner, and can take options to apply to the scanner. -func NewScanner(options ...Option) (*Scanner, error) { - scanner := &Scanner{} +func NewScanner(ctx context.Context, options ...Option) (*Scanner, error) { + scanner := &Scanner{ + doneAsync: nil, + liveProgress: nil, + streamer: nil, + ctx: ctx, + } for _, option := range options { option(scanner) @@ -63,360 +62,154 @@ func NewScanner(options ...Option) (*Scanner, error) { } } - if scanner.ctx == nil { - scanner.ctx = context.Background() - } - return scanner, nil } -// Run runs nmap synchronously and returns the result of the scan. -func (s *Scanner) Run() (result *Run, warnings []string, err error) { - var ( - stdout, stderr bytes.Buffer - resume bool - ) - - args := s.args - - for _, arg := range args { - if arg == "--resume" { - resume = true - break - } - } - - if !resume { - // Enable XML output - args = append(args, "-oX") - - // Get XML output in stdout instead of writing it in a file - args = append(args, "-") - } - - // Prepare nmap process - cmd := exec.Command(s.binaryPath, args...) - if s.modifySysProcAttr != nil { - s.modifySysProcAttr(cmd.SysProcAttr) - } - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - // Run nmap process - err = cmd.Start() - if err != nil { - return nil, warnings, err - } - - // Make a goroutine to notify the select when the scan is done. - done := make(chan error, 1) - go func() { - done <- cmd.Wait() - }() - - // Wait for nmap process or timeout - select { - case <-s.ctx.Done(): - - // Context was done before the scan was finished. - // The process is killed and a timeout error is returned. - _ = cmd.Process.Kill() - - return nil, warnings, ErrScanTimeout - case <-done: - - // Process nmap stderr output containing none-critical errors and warnings - // Everyone needs to check whether one or some of these warnings is a hard issue in their use case - if stderr.Len() > 0 { - warnings = strings.Split(strings.Trim(stderr.String(), "\n"), "\n") - } - - // Check for warnings that will inevitably lead to parsing errors, hence, have priority. - if err := analyzeWarnings(warnings); err != nil { - return nil, warnings, err - } - - // Parse nmap xml output. Usually nmap always returns valid XML, even if there is a scan error. - // Potentially available warnings are returned too, but probably not the reason for a broken XML. - result, err := Parse(stdout.Bytes()) - if err != nil { - warnings = append(warnings, err.Error()) // Append parsing error to warnings for those who are interested. - return nil, warnings, ErrParseOutput - } - - // Critical scan errors are reflected in the XML. - if result != nil && len(result.Stats.Finished.ErrorMsg) > 0 { - switch { - case strings.Contains(result.Stats.Finished.ErrorMsg, "Error resolving name"): - return result, warnings, ErrResolveName - // TODO: Add cases for other known errors we might want to guard. - default: - return result, warnings, fmt.Errorf(result.Stats.Finished.ErrorMsg) - } - } +// Async will run the nmap scan asynchronously. You need to provide a channel with error type. +// When the scan is finished an error or nil will be piped through this channel. +func (s *Scanner) Async(doneAsync chan error) *Scanner { + s.doneAsync = doneAsync + return s +} - // Call filters if they are set. - if s.portFilter != nil { - result = choosePorts(result, s.portFilter) - } - if s.hostFilter != nil { - result = chooseHosts(result, s.hostFilter) - } +// Progress pipes the progress of nmap every 100ms. It needs a channel of type float. +func (s *Scanner) Progress(liveProgress chan float32) *Scanner { + s.args = append(s.args, "--stats-every", "100ms") + s.liveProgress = liveProgress + return s +} - // Return result, optional warnings but no error - return result, warnings, nil - } +// ToFile enables the Scanner to write the nmap XML output to a given path. +// Nmap will write the normal CLI output to stdout. The XML is parsed from file after the scan is finished. +func (s *Scanner) ToFile(file string) *Scanner { + s.toFile = &file + return s } -// RunWithProgress runs nmap synchronously and returns the result of the scan. -// It needs a channel to constantly stream the progress. -func (s *Scanner) RunWithProgress(liveProgress chan<- float32) (result *Run, warnings []string, err error) { - var stdout, stderr bytes.Buffer +// Streamer takes an io.Writer that receives the XML output. +// So the stdout of nmap will be duplicated to the given stream and *Run. +// This will not disable parsing the output to the struct. +func (s *Scanner) Streamer(stream io.Writer) *Scanner { + s.streamer = stream + return s +} - args := s.args +// Run will run the Scanner with the enabled options. +// You need to create a Run struct and warnings array first so the function can parse it. +func (s *Scanner) Run() (result *Run, warnings *[]string, err error) { + var stdoutPipe io.ReadCloser + var stdout bytes.Buffer + var stderr bytes.Buffer - // Enable XML output. - args = append(args, "-oX") + warnings = &[]string{} // Instantiate warnings array - // Get XML output in stdout instead of writing it in a file. - args = append(args, "-") + args := s.args - // Enable progress output every second. - args = append(args, "--stats-every", "1s") + // Write XML to standard output. + // If toFile is set then write XML to file and normal nmap output to stdout. + if s.toFile != nil { + args = append(args, "-oX", *s.toFile, "-oN", "-") + } else { + args = append(args, "-oX", "-") + } // Prepare nmap process. - cmd := exec.Command(s.binaryPath, args...) + cmd := exec.CommandContext(s.ctx, s.binaryPath, args...) if s.modifySysProcAttr != nil { s.modifySysProcAttr(cmd.SysProcAttr) } + stdoutPipe, err = cmd.StdoutPipe() + if err != nil { + return result, warnings, err + } + stdoutDuplicate := io.TeeReader(stdoutPipe, &stdout) cmd.Stderr = &stderr - cmd.Stdout = &stdout + + var streamerErrs *errgroup.Group + if s.streamer != nil { + streamerErrs, _ = errgroup.WithContext(s.ctx) + streamerErrs.Go(func() error { + _, err = io.Copy(s.streamer, stdoutDuplicate) + return err + }) + } else { + go io.Copy(io.Discard, stdoutDuplicate) + } // Run nmap process. err = cmd.Start() if err != nil { - return nil, warnings, err + return result, warnings, err } - // Make a goroutine to notify the select when the scan is done. + // Add goroutine that updates chan when command is finished. done := make(chan error, 1) doneProgress := make(chan bool, 1) go func() { - done <- cmd.Wait() + err := cmd.Wait() + if streamerErrs != nil { + streamerError := streamerErrs.Wait() + if streamerError != nil { + *warnings = append(*warnings, fmt.Sprintf("read from stdout failed: %s", err)) + } + } + done <- err }() // Make goroutine to check the progress every second. // Listening for channel doneProgress. - go func() { - type progress struct { - TaskProgress []TaskProgress `xml:"taskprogress" json:"task_progress"` - } - p := &progress{} - for { - select { - case <-doneProgress: - close(liveProgress) - return - default: - time.Sleep(time.Second) - _ = xml.Unmarshal(stdout.Bytes(), p) - //result, _ := Parse(stdout.Bytes()) - if len(p.TaskProgress) > 0 { - liveProgress <- p.TaskProgress[len(p.TaskProgress)-1].Percent - } + if s.liveProgress != nil { + go func() { + type progress struct { + TaskProgress []TaskProgress `xml:"taskprogress" json:"task_progress"` } - } - }() - - // Wait for nmap process or timeout. - select { - case <-s.ctx.Done(): - // Trigger progress function exit. - close(doneProgress) - - // Context was done before the scan was finished. - // The process is killed and a timeout error is returned. - _ = cmd.Process.Kill() - - return nil, warnings, ErrScanTimeout - case <-done: - // Trigger progress function exit. - close(doneProgress) - - // Process nmap stderr output containing none-critical errors and warnings. - // Everyone needs to check whether one or some of these warnings is a hard issue in their use case. - if stderr.Len() > 0 { - warnings = strings.Split(strings.Trim(stderr.String(), "\n"), "\n") - } - - // Check for warnings that will inevitably lead to parsing errors, hence, have priority. - if err := analyzeWarnings(warnings); err != nil { - return nil, warnings, err - } - - // Parse nmap xml output. Usually nmap always returns valid XML, even if there is a scan error. - // Potentially available warnings are returned too, but probably not the reason for a broken XML. - result, err := Parse(stdout.Bytes()) - if err != nil { - warnings = append(warnings, err.Error()) // Append parsing error to warnings for those who are interested. - return nil, warnings, ErrParseOutput - } - - // Critical scan errors are reflected in the XML. - if result != nil && len(result.Stats.Finished.ErrorMsg) > 0 { - switch { - case strings.Contains(result.Stats.Finished.ErrorMsg, "Error resolving name"): - return result, warnings, ErrResolveName - // TODO: Add cases for other known errors we might want to guard. - default: - return result, warnings, fmt.Errorf(result.Stats.Finished.ErrorMsg) + p := &progress{} + for { + select { + case <-doneProgress: + close(s.liveProgress) + return + default: + time.Sleep(time.Millisecond * 100) + _ = xml.Unmarshal(stdout.Bytes(), p) + progressIndex := len(p.TaskProgress) - 1 + if progressIndex >= 0 { + s.liveProgress <- p.TaskProgress[progressIndex].Percent + } + } } - } - - // Call filters if they are set. - if s.portFilter != nil { - result = choosePorts(result, s.portFilter) - } - if s.hostFilter != nil { - result = chooseHosts(result, s.hostFilter) - } - - // Return result, optional warnings but no error. - return result, warnings, nil - } -} - -// RunWithStreamer runs nmap synchronously. The XML output is written directly to a file. -// It uses a streamer interface to constantly stream the stdout. -func (s *Scanner) RunWithStreamer(stream Streamer, file string) (warnings []string, err error) { - - args := s.args - - // Enable XML output. - args = append(args, "-oX") - - // Get XML output in stdout instead of writing it in a file. - args = append(args, file) - - // Enable progress output every second. - args = append(args, "--stats-every", "5s") - - // Prepare nmap process. - cmd := exec.CommandContext(s.ctx, s.binaryPath, args...) - if s.modifySysProcAttr != nil { - s.modifySysProcAttr(cmd.SysProcAttr) - } - - // Write stderr to buffer. - stderrBuf := bytes.Buffer{} - cmd.Stderr = &stderrBuf - - // Connect to the StdoutPipe. - stdoutIn, err := cmd.StdoutPipe() - if err != nil { - return warnings, errors.WithMessage(err, "connect to StdoutPipe failed") - } - stdout := stream - - // Run nmap process. - if err := cmd.Start(); err != nil { - return warnings, errors.WithMessage(err, "start command failed") - } - - // Copy stdout to pipe. - g, _ := errgroup.WithContext(s.ctx) - g.Go(func() error { - _, err = io.Copy(stdout, stdoutIn) - return err - }) - - cmdErr := cmd.Wait() - if err := g.Wait(); err != nil { - warnings = append(warnings, errors.WithMessage(err, "read from stdout failed").Error()) - } - if cmdErr != nil { - return warnings, errors.WithMessage(err, "nmap command failed") - } - // Process nmap stderr output containing none-critical errors and warnings. - // Everyone needs to check whether one or some of these warnings is a hard issue in their use case. - if stderrBuf.Len() > 0 { - warnings = append(warnings, strings.Split(strings.Trim(stderrBuf.String(), "\n"), "\n")...) - } - - // Check for warnings that will inevitably lead to parsing errors, hence, have priority. - if err := analyzeWarnings(warnings); err != nil { - return warnings, err - } - - // Return result, optional warnings but no error. - return warnings, nil -} - -// RunAsync runs nmap asynchronously and returns error. -// TODO: RunAsync should return warnings as well. -func (s *Scanner) RunAsync() error { - - args := s.args - - // Enable XML output. - args = append(args, "-oX") - - // Get XML output in stdout instead of writing it in a file. - args = append(args, "-") - s.cmd = exec.Command(s.binaryPath, args...) - - if s.modifySysProcAttr != nil { - s.modifySysProcAttr(s.cmd.SysProcAttr) - } - - stderr, err := s.cmd.StderrPipe() - if err != nil { - return fmt.Errorf("unable to get error output from asynchronous nmap run: %v", err) - } - - stdout, err := s.cmd.StdoutPipe() - if err != nil { - return fmt.Errorf("unable to get standard output from asynchronous nmap run: %v", err) + }() } - s.stdout = *bufio.NewScanner(stdout) - s.stderr = *bufio.NewScanner(stderr) - - if err := s.cmd.Start(); err != nil { - return fmt.Errorf("unable to execute asynchronous nmap run: %v", err) + // Check if function should run async. + // When async process nmap result in goroutine that waits for nmap command finish. + // Else block and process nmap result in this function scope. + result = &Run{} + if s.doneAsync != nil { + go func() { + s.doneAsync <- s.processNmapResult(result, warnings, &stdout, &stderr, done, doneProgress) + }() + } else { + err = s.processNmapResult(result, warnings, &stdout, &stderr, done, doneProgress) } - go func() { - <-s.ctx.Done() - _ = s.cmd.Process.Kill() - }() - - return nil -} - -// Wait waits for the cmd to finish and returns error. -func (s *Scanner) Wait() error { - return s.cmd.Wait() -} - -// GetStdout returns stdout variable for scanner. -func (s *Scanner) GetStdout() bufio.Scanner { - return s.stdout -} - -// GetStderr returns stderr variable for scanner. -func (s *Scanner) GetStderr() bufio.Scanner { - return s.stderr + return result, warnings, err } // AddOptions sets more scan options after the scan is created. -func (s *Scanner) AddOptions(options ...Option) { +func (s *Scanner) AddOptions(options ...Option) *Scanner { for _, option := range options { option(s) } + return s +} + +// Args return the list of nmap args. +func (s *Scanner) Args() []string { + return s.args } -func chooseHosts(result *Run, filter func(Host) bool) *Run { +func chooseHosts(result *Run, filter func(Host) bool) { var filteredHosts []Host for _, host := range result.Hosts { @@ -426,11 +219,9 @@ func chooseHosts(result *Run, filter func(Host) bool) *Run { } result.Hosts = filteredHosts - - return result } -func choosePorts(result *Run, filter func(Port) bool) *Run { +func choosePorts(result *Run, filter func(Port) bool) { for idx := range result.Hosts { var filteredPorts []Port @@ -442,42 +233,81 @@ func choosePorts(result *Run, filter func(Port) bool) *Run { result.Hosts[idx].Ports = filteredPorts } - - return result } -func analyzeWarnings(warnings []string) error { - // Check for warnings that will inevitably lead to parsing errors, hence, have priority. - for _, warning := range warnings { +func (s *Scanner) processNmapResult(result *Run, warnings *[]string, stdout, stderr *bytes.Buffer, done chan error, doneProgress chan bool) error { + // Wait for nmap to finish. + var err = <-done + close(doneProgress) + if err != nil { + return err + } + + // Check stderr output. + if err := checkStdErr(stderr, warnings); err != nil { + return err + } + + // Parse nmap xml output. Usually nmap always returns valid XML, even if there is a scan error. + // Potentially available warnings are returned too, but probably not the reason for a broken XML. + if s.toFile != nil { + err = result.FromFile(*s.toFile) + } else { + err = Parse(stdout.Bytes(), result) + } + if err != nil { + *warnings = append(*warnings, err.Error()) // Append parsing error to warnings for those who are interested. + return ErrParseOutput + } + + // Critical scan errors are reflected in the XML. + if result != nil && len(result.Stats.Finished.ErrorMsg) > 0 { switch { - case strings.Contains(warning, "Malloc Failed!"): - return ErrMallocFailed - // TODO: Add cases for other known errors we might want to guard. + case strings.Contains(result.Stats.Finished.ErrorMsg, "Error resolving name"): + return ErrResolveName default: + return fmt.Errorf(result.Stats.Finished.ErrorMsg) } } - return nil -} -// WithContext adds a context to a scanner, to make it cancellable and able to timeout. -func WithContext(ctx context.Context) Option { - return func(s *Scanner) { - s.ctx = ctx + // Call filters if they are set. + if s.portFilter != nil { + choosePorts(result, s.portFilter) } + if s.hostFilter != nil { + chooseHosts(result, s.hostFilter) + } + + return err } -// WithBinaryPath sets the nmap binary path for a scanner. -func WithBinaryPath(binaryPath string) Option { - return func(s *Scanner) { - s.binaryPath = binaryPath +// checkStdErr writes the output of stderr to the warnings array. +// It also processes nmap stderr output containing none-critical errors and warnings. +func checkStdErr(stderr *bytes.Buffer, warnings *[]string) error { + if stderr.Len() <= 0 { + return nil + } + + stderrSplit := strings.Split(strings.Trim(stderr.String(), "\n "), "\n") + + // Check for warnings that will inevitably lead to parsing errors, hence, have priority. + for _, warning := range stderrSplit { + warning = strings.Trim(warning, " ") + *warnings = append(*warnings, warning) + switch { + case strings.Contains(warning, "Malloc Failed!"): + return ErrMallocFailed + } } + return nil } // WithCustomArguments sets custom arguments to give to the nmap binary. // There should be no reason to use this, unless you are using a custom build // of nmap or that this repository isn't up to date with the latest options // of the official nmap release. -// You can use this as a quick way to paste an nmap command into your go code, +// +// Deprecated: You can use this as a quick way to paste an nmap command into your go code, // but remember that the whole purpose of this repository is to be idiomatic, // provide type checking, enums for the values that can be passed, etc. func WithCustomArguments(args ...string) Option { @@ -486,6 +316,13 @@ func WithCustomArguments(args ...string) Option { } } +// WithBinaryPath sets the nmap binary path for a scanner. +func WithBinaryPath(binaryPath string) Option { + return func(s *Scanner) { + s.binaryPath = binaryPath + } +} + // WithFilterPort allows to set a custom function to filter out ports that // don't fulfill a given condition. When the given function returns true, // the port is kept, otherwise it is removed from the result. Can be used @@ -505,1119 +342,3 @@ func WithFilterHost(hostFilter func(Host) bool) Option { s.hostFilter = hostFilter } } - -/*** Target specification ***/ - -// WithTargets sets the target of a scanner. -func WithTargets(targets ...string) Option { - return func(s *Scanner) { - s.args = append(s.args, targets...) - } -} - -// WithTargetExclusion sets the excluded targets of a scanner. -func WithTargetExclusion(target string) Option { - return func(s *Scanner) { - s.args = append(s.args, "--exclude") - s.args = append(s.args, target) - } -} - -// WithTargetInput sets the input file name to set the targets. -func WithTargetInput(inputFileName string) Option { - return func(s *Scanner) { - s.args = append(s.args, "-iL") - s.args = append(s.args, inputFileName) - } -} - -// WithTargetExclusionInput sets the input file name to set the target exclusions. -func WithTargetExclusionInput(inputFileName string) Option { - return func(s *Scanner) { - s.args = append(s.args, "--excludefile") - s.args = append(s.args, inputFileName) - } -} - -// WithRandomTargets sets the amount of targets to randomly choose from the targets. -func WithRandomTargets(randomTargets int) Option { - return func(s *Scanner) { - s.args = append(s.args, "-iR") - s.args = append(s.args, fmt.Sprint(randomTargets)) - } -} - -// WithUnique makes each address be scanned only once. -// The default behavior is to scan each address as many times -// as it is specified in the target list, such as when network -// ranges overlap or different hostnames resolve to the same -// address. -func WithUnique() Option { - return func(s *Scanner) { - s.args = append(s.args, "--unique") - } -} - -/*** Host discovery ***/ - -// WithListScan sets the discovery mode to simply list the targets to scan and not scan them. -func WithListScan() Option { - return func(s *Scanner) { - s.args = append(s.args, "-sL") - } -} - -// WithPingScan sets the discovery mode to simply ping the targets to scan and not scan them. -func WithPingScan() Option { - return func(s *Scanner) { - s.args = append(s.args, "-sn") - } -} - -// WithSkipHostDiscovery diables host discovery and considers all hosts as online. -func WithSkipHostDiscovery() Option { - return func(s *Scanner) { - s.args = append(s.args, "-Pn") - } -} - -// WithSYNDiscovery sets the discovery mode to use SYN packets. -// If the portList argument is empty, this will enable SYN discovery -// for all ports. Otherwise, it will be only for the specified ports. -func WithSYNDiscovery(ports ...string) Option { - portList := strings.Join(ports, ",") - - return func(s *Scanner) { - s.args = append(s.args, fmt.Sprintf("-PS%s", portList)) - } -} - -// WithACKDiscovery sets the discovery mode to use ACK packets. -// If the portList argument is empty, this will enable ACK discovery -// for all ports. Otherwise, it will be only for the specified ports. -func WithACKDiscovery(ports ...string) Option { - portList := strings.Join(ports, ",") - - return func(s *Scanner) { - s.args = append(s.args, fmt.Sprintf("-PA%s", portList)) - } -} - -// WithUDPDiscovery sets the discovery mode to use UDP packets. -// If the portList argument is empty, this will enable UDP discovery -// for all ports. Otherwise, it will be only for the specified ports. -func WithUDPDiscovery(ports ...string) Option { - portList := strings.Join(ports, ",") - - return func(s *Scanner) { - s.args = append(s.args, fmt.Sprintf("-PU%s", portList)) - } -} - -// WithSCTPDiscovery sets the discovery mode to use SCTP packets -// containing a minimal INIT chunk. -// If the portList argument is empty, this will enable SCTP discovery -// for all ports. Otherwise, it will be only for the specified ports. -// Warning: on Unix, only the privileged user root is generally -// able to send and receive raw SCTP packets. -func WithSCTPDiscovery(ports ...string) Option { - portList := strings.Join(ports, ",") - - return func(s *Scanner) { - s.args = append(s.args, fmt.Sprintf("-PY%s", portList)) - } -} - -// WithICMPEchoDiscovery sets the discovery mode to use an ICMP type 8 -// packet (an echo request), like the standard packets sent by the ping -// command. -// Many hosts and firewalls block these packets, so this is usually not -// the best for exploring networks. -func WithICMPEchoDiscovery() Option { - return func(s *Scanner) { - s.args = append(s.args, "-PE") - } -} - -// WithICMPTimestampDiscovery sets the discovery mode to use an ICMP type 13 -// packet (a timestamp request). -// This query can be valuable when administrators specifically block echo -// request packets while forgetting that other ICMP queries can be used -// for the same purpose. -func WithICMPTimestampDiscovery() Option { - return func(s *Scanner) { - s.args = append(s.args, "-PP") - } -} - -// WithICMPNetMaskDiscovery sets the discovery mode to use an ICMP type 17 -// packet (an address mask request). -// This query can be valuable when administrators specifically block echo -// request packets while forgetting that other ICMP queries can be used -// for the same purpose. -func WithICMPNetMaskDiscovery() Option { - return func(s *Scanner) { - s.args = append(s.args, "-PM") - } -} - -// WithIPProtocolPingDiscovery sets the discovery mode to use the IP -// protocol ping. -// If no protocols are specified, the default is to send multiple IP -// packets for ICMP (protocol 1), IGMP (protocol 2), and IP-in-IP -// (protocol 4). -func WithIPProtocolPingDiscovery(protocols ...string) Option { - protocolList := strings.Join(protocols, ",") - - return func(s *Scanner) { - s.args = append(s.args, fmt.Sprintf("-PO%s", protocolList)) - } -} - -// WithDisabledDNSResolution disables DNS resolution in the discovery -// step of the nmap scan. -func WithDisabledDNSResolution() Option { - return func(s *Scanner) { - s.args = append(s.args, "-n") - } -} - -// WithForcedDNSResolution enforces DNS resolution in the discovery -// step of the nmap scan. -func WithForcedDNSResolution() Option { - return func(s *Scanner) { - s.args = append(s.args, "-R") - } -} - -// WithCustomDNSServers sets custom DNS servers for the scan. -// List format: dns1[,dns2],... -func WithCustomDNSServers(dnsServers ...string) Option { - dnsList := strings.Join(dnsServers, ",") - - return func(s *Scanner) { - s.args = append(s.args, "--dns-servers") - s.args = append(s.args, dnsList) - } -} - -// WithSystemDNS sets the scanner's DNS to the system's DNS. -func WithSystemDNS() Option { - return func(s *Scanner) { - s.args = append(s.args, "--system-dns") - } -} - -// WithTraceRoute enables the tracing of the hop path to each host. -func WithTraceRoute() Option { - return func(s *Scanner) { - s.args = append(s.args, "--traceroute") - } -} - -/*** Scan techniques ***/ - -// WithSYNScan sets the scan technique to use SYN packets over TCP. -// This is the default method, as it is fast, stealthy and not -// hampered by restrictive firewalls. -func WithSYNScan() Option { - return func(s *Scanner) { - s.args = append(s.args, "-sS") - } -} - -// WithConnectScan sets the scan technique to use TCP connections. -// This is the default method used when a user does not have raw -// packet privileges. Target machines are likely to log these -// connections. -func WithConnectScan() Option { - return func(s *Scanner) { - s.args = append(s.args, "-sT") - } -} - -// WithACKScan sets the scan technique to use ACK packets over TCP. -// This scan is unable to determine if a port is open. -// When scanning unfiltered systems, open and closed ports will both -// return a RST packet. -// Nmap then labels them as unfiltered, meaning that they are reachable -// by the ACK packet, but whether they are open or closed is undetermined. -func WithACKScan() Option { - return func(s *Scanner) { - s.args = append(s.args, "-sA") - } -} - -// WithWindowScan sets the scan technique to use ACK packets over TCP and -// examining the TCP window field of the RST packets returned. -// Window scan is exactly the same as ACK scan except that it exploits -// an implementation detail of certain systems to differentiate open ports -// from closed ones, rather than always printing unfiltered when a RST -// is returned. -func WithWindowScan() Option { - return func(s *Scanner) { - s.args = append(s.args, "-sW") - } -} - -// WithMaimonScan sends the same packets as NULL, FIN, and Xmas scans, -// except that the probe is FIN/ACK. Many BSD-derived systems will drop -// these packets if the port is open. -func WithMaimonScan() Option { - return func(s *Scanner) { - s.args = append(s.args, "-sM") - } -} - -// WithUDPScan sets the scan technique to use UDP packets. -// It can be combined with a TCP scan type such as SYN scan -// to check both protocols during the same run. -// UDP scanning is generally slower than TCP, but should not -// be ignored. -func WithUDPScan() Option { - return func(s *Scanner) { - s.args = append(s.args, "-sU") - } -} - -// WithTCPNullScan sets the scan technique to use TCP null packets. -// (TCP flag header is 0). This scan method can be used to exploit -// a loophole in the TCP RFC. -// If an RST packet is received, the port is considered closed, -// while no response means it is open|filtered. -func WithTCPNullScan() Option { - return func(s *Scanner) { - s.args = append(s.args, "-sN") - } -} - -// WithTCPFINScan sets the scan technique to use TCP packets with -// the FIN flag set. -// This scan method can be used to exploit a loophole in the TCP RFC. -// If an RST packet is received, the port is considered closed, -// while no response means it is open|filtered. -func WithTCPFINScan() Option { - return func(s *Scanner) { - s.args = append(s.args, "-sF") - } -} - -// WithTCPXmasScan sets the scan technique to use TCP packets with -// the FIN, PSH and URG flags set. -// This scan method can be used to exploit a loophole in the TCP RFC. -// If an RST packet is received, the port is considered closed, -// while no response means it is open|filtered. -func WithTCPXmasScan() Option { - return func(s *Scanner) { - s.args = append(s.args, "-sX") - } -} - -// TCPFlag represents a TCP flag. -type TCPFlag int - -// Flag enumerations. -const ( - FlagNULL TCPFlag = 0 - FlagFIN TCPFlag = 1 - FlagSYN TCPFlag = 2 - FlagRST TCPFlag = 4 - FlagPSH TCPFlag = 8 - FlagACK TCPFlag = 16 - FlagURG TCPFlag = 32 - FlagECE TCPFlag = 64 - FlagCWR TCPFlag = 128 - FlagNS TCPFlag = 256 -) - -// WithTCPScanFlags sets the scan technique to use custom TCP flags. -func WithTCPScanFlags(flags ...TCPFlag) Option { - var total int - for _, flag := range flags { - total += int(flag) - } - - return func(s *Scanner) { - s.args = append(s.args, "--scanflags") - s.args = append(s.args, fmt.Sprintf("%x", total)) - } -} - -// WithIdleScan sets the scan technique to use a zombie host to -// allow for a truly blind TCP port scan of the target. -// Besides being extraordinarily stealthy (due to its blind nature), -// this scan type permits mapping out IP-based trust relationships -// between machines. -func WithIdleScan(zombieHost string, probePort int) Option { - return func(s *Scanner) { - s.args = append(s.args, "-sI") - - if probePort != 0 { - s.args = append(s.args, fmt.Sprintf("%s:%d", zombieHost, probePort)) - } else { - s.args = append(s.args, zombieHost) - } - } -} - -// WithSCTPInitScan sets the scan technique to use SCTP packets -// containing an INIT chunk. -// It can be performed quickly, scanning thousands of ports per -// second on a fast network not hampered by restrictive firewalls. -// Like SYN scan, INIT scan is relatively unobtrusive and stealthy, -// since it never completes SCTP associations. -func WithSCTPInitScan() Option { - return func(s *Scanner) { - s.args = append(s.args, "-sY") - } -} - -// WithSCTPCookieEchoScan sets the scan technique to use SCTP packets -// containing a COOKIE-ECHO chunk. -// The advantage of this scan type is that it is not as obvious a port -// scan than an INIT scan. Also, there may be non-stateful firewall -// rulesets blocking INIT chunks, but not COOKIE ECHO chunks. -func WithSCTPCookieEchoScan() Option { - return func(s *Scanner) { - s.args = append(s.args, "-sZ") - } -} - -// WithIPProtocolScan sets the scan technique to use the IP protocol. -// IP protocol scan allows you to determine which IP protocols -// (TCP, ICMP, IGMP, etc.) are supported by target machines. This isn't -// technically a port scan, since it cycles through IP protocol numbers -// rather than TCP or UDP port numbers. -func WithIPProtocolScan() Option { - return func(s *Scanner) { - s.args = append(s.args, "-sO") - } -} - -// WithFTPBounceScan sets the scan technique to use the an FTP relay host. -// It takes an argument of the form ":@:. ". -// You may omit :, in which case anonymous login credentials -// (user: anonymous password:-wwwuser@) are used. -// The port number (and preceding colon) may be omitted as well, in which case the -// default FTP port (21) on is used. -func WithFTPBounceScan(FTPRelayHost string) Option { - return func(s *Scanner) { - s.args = append(s.args, "-b") - s.args = append(s.args, FTPRelayHost) - } -} - -/*** Port specification and scan order ***/ - -// WithPorts sets the ports which the scanner should scan on each host. -func WithPorts(ports ...string) Option { - portList := strings.Join(ports, ",") - - return func(s *Scanner) { - // Find if any port is set. - var place int = -1 - for p, value := range s.args { - if value == "-p" { - place = p - break - } - } - - // Add ports. - if place >= 0 { - portList = s.args[place+1] + "," + portList - s.args[place+1] = portList - } else { - s.args = append(s.args, "-p") - s.args = append(s.args, portList) - } - } -} - -// WithPortExclusions sets the ports that the scanner should not scan on each host. -func WithPortExclusions(ports ...string) Option { - portList := strings.Join(ports, ",") - - return func(s *Scanner) { - s.args = append(s.args, "--exclude-ports") - s.args = append(s.args, portList) - } -} - -// WithFastMode makes the scan faster by scanning fewer ports than the default scan. -func WithFastMode() Option { - return func(s *Scanner) { - s.args = append(s.args, "-F") - } -} - -// WithConsecutivePortScanning makes the scan go through ports consecutively instead of -// picking them out randomly. -func WithConsecutivePortScanning() Option { - return func(s *Scanner) { - s.args = append(s.args, "-r") - } -} - -// WithMostCommonPorts sets the scanner to go through the provided number of most -// common ports. -func WithMostCommonPorts(number int) Option { - return func(s *Scanner) { - s.args = append(s.args, "--top-ports") - s.args = append(s.args, fmt.Sprint(number)) - } -} - -// WithPortRatio sets the scanner to go the ports more common than the given ratio. -// Ratio must be a float between 0 and 1. -func WithPortRatio(ratio float32) Option { - return func(s *Scanner) { - if ratio < 0 || ratio > 1 { - panic("value given to nmap.WithPortRatio() should be between 0 and 1") - } - - s.args = append(s.args, "--port-ratio") - s.args = append(s.args, fmt.Sprintf("%.1f", ratio)) - } -} - -/*** Service/Version detection ***/ - -// WithServiceInfo enables the probing of open ports to determine service and version -// info. -func WithServiceInfo() Option { - return func(s *Scanner) { - s.args = append(s.args, "-sV") - } -} - -// WithVersionIntensity sets the level of intensity with which nmap should -// probe the open ports to get version information. -// Intensity should be a value between 0 (light) and 9 (try all probes). The -// default value is 7. -func WithVersionIntensity(intensity int16) Option { - return func(s *Scanner) { - if intensity < 0 || intensity > 9 { - panic("value given to nmap.WithVersionIntensity() should be between 0 and 9") - } - - s.args = append(s.args, "--version-intensity") - s.args = append(s.args, fmt.Sprint(intensity)) - } -} - -// WithVersionLight sets the level of intensity with which nmap should probe the -// open ports to get version information to 2. This will make version scanning much -// faster, but slightly less likely to identify services. -func WithVersionLight() Option { - return func(s *Scanner) { - s.args = append(s.args, "--version-light") - } -} - -// WithVersionAll sets the level of intensity with which nmap should probe the -// open ports to get version information to 9. This will ensure that every single -// probe is attempted against each port. -func WithVersionAll() Option { - return func(s *Scanner) { - s.args = append(s.args, "--version-all") - } -} - -// WithVersionTrace causes Nmap to print out extensive debugging info about what -// version scanning is doing. -// TODO: See how this works along with XML output. -func WithVersionTrace() Option { - return func(s *Scanner) { - s.args = append(s.args, "--version-trace") - } -} - -/*** Script scan ***/ - -// WithDefaultScript sets the scanner to perform a script scan using the default -// set of scripts. It is equivalent to --script=default. Some of the scripts in -// this category are considered intrusive and should not be run against a target -// network without permission. -func WithDefaultScript() Option { - return func(s *Scanner) { - s.args = append(s.args, "-sC") - } -} - -// WithScripts sets the scanner to perform a script scan using the enumerated -// scripts, script directories and script categories. -func WithScripts(scripts ...string) Option { - scriptList := strings.Join(scripts, ",") - - return func(s *Scanner) { - s.args = append(s.args, fmt.Sprintf("--script=%s", scriptList)) - } -} - -// WithScriptArguments provides arguments for scripts. If a value is the empty string, the key will be used as a flag. -func WithScriptArguments(arguments map[string]string) Option { - var argList string - - // Properly format the argument list from the map. - // Complex example: - // user=foo,pass=",{}=bar",whois={whodb=nofollow+ripe},xmpp-info.server_name=localhost,vulns.showall - for key, value := range arguments { - str := "" - if value == "" { - str = key - } else { - str = fmt.Sprintf("%s=%s", key, value) - } - - argList = strings.Join([]string{argList, str}, ",") - } - - argList = strings.TrimLeft(argList, ",") - - return func(s *Scanner) { - s.args = append(s.args, fmt.Sprintf("--script-args=%s", argList)) - } -} - -// WithScriptArgumentsFile provides arguments for scripts from a file. -func WithScriptArgumentsFile(inputFilePath string) Option { - return func(s *Scanner) { - s.args = append(s.args, fmt.Sprintf("--script-args-file=%s", inputFilePath)) - } -} - -// WithScriptTrace makes the scripts show all data sent and received. -func WithScriptTrace() Option { - return func(s *Scanner) { - s.args = append(s.args, "--script-trace") - } -} - -// WithScriptUpdateDB updates the script database. -func WithScriptUpdateDB() Option { - return func(s *Scanner) { - s.args = append(s.args, "--script-updatedb") - } -} - -// WithScriptTimeout sets the script timeout. -func WithScriptTimeout(timeout time.Duration) Option { - milliseconds := timeout.Round(time.Nanosecond).Nanoseconds() / 1000000 - - return func(s *Scanner) { - s.args = append(s.args, "--script-timeout") - s.args = append(s.args, fmt.Sprintf("%dms", int(milliseconds))) - } -} - -/*** OS Detection ***/ - -// WithOSDetection enables OS detection. -func WithOSDetection() Option { - return func(s *Scanner) { - s.args = append(s.args, "-O") - } -} - -// WithOSScanLimit sets the scanner to not even try OS detection against -// hosts that do have at least one open TCP port, as it is unlikely to be effective. -// This can save substantial time, particularly on -Pn scans against many hosts. -// It only matters when OS detection is requested with -O or -A. -func WithOSScanLimit() Option { - return func(s *Scanner) { - s.args = append(s.args, "--osscan-limit") - } -} - -// WithOSScanGuess makes nmap attempt to guess the OS more aggressively. -func WithOSScanGuess() Option { - return func(s *Scanner) { - s.args = append(s.args, "--osscan-guess") - } -} - -/*** Timing and performance ***/ - -// Timing represents a timing template for nmap. -// These are meant to be used with the WithTimingTemplate method. -type Timing int16 - -const ( - // TimingSlowest also called paranoiac NO PARALLELISM | 5min timeout | 100ms to 10s round-trip time timeout | 5mn scan delay - TimingSlowest Timing = 0 - // TimingSneaky NO PARALLELISM | 15sec timeout | 100ms to 10s round-trip time timeout | 15s scan delay - TimingSneaky Timing = 1 - // TimingPolite NO PARALLELISM | 1sec timeout | 100ms to 10s round-trip time timeout | 400ms scan delay - TimingPolite Timing = 2 - // TimingNormal PARALLELISM | 1sec timeout | 100ms to 10s round-trip time timeout | 0s scan delay - TimingNormal Timing = 3 - // TimingAggressive PARALLELISM | 500ms timeout | 100ms to 1250ms round-trip time timeout | 0s scan delay - TimingAggressive Timing = 4 - // TimingFastest also called insane PARALLELISM | 250ms timeout | 50ms to 300ms round-trip time timeout | 0s scan delay - TimingFastest Timing = 5 -) - -// WithTimingTemplate sets the timing template for nmap. -func WithTimingTemplate(timing Timing) Option { - return func(s *Scanner) { - s.args = append(s.args, fmt.Sprintf("-T%d", timing)) - } -} - -// WithStatsEvery periodically prints a timing status message after each interval of time. -func WithStatsEvery(interval string) Option { - return func(s *Scanner) { - s.args = append(s.args, "--stats-every") - s.args = append(s.args, interval) - } -} - -// WithMinHostgroup sets the minimal parallel host scan group size. -func WithMinHostgroup(size int) Option { - return func(s *Scanner) { - s.args = append(s.args, "--min-hostgroup") - s.args = append(s.args, fmt.Sprint(size)) - } -} - -// WithMaxHostgroup sets the maximal parallel host scan group size. -func WithMaxHostgroup(size int) Option { - return func(s *Scanner) { - s.args = append(s.args, "--max-hostgroup") - s.args = append(s.args, fmt.Sprint(size)) - } -} - -// WithMinParallelism sets the minimal number of parallel probes. -func WithMinParallelism(probes int) Option { - return func(s *Scanner) { - s.args = append(s.args, "--min-parallelism") - s.args = append(s.args, fmt.Sprint(probes)) - } -} - -// WithMaxParallelism sets the maximal number of parallel probes. -func WithMaxParallelism(probes int) Option { - return func(s *Scanner) { - s.args = append(s.args, "--max-parallelism") - s.args = append(s.args, fmt.Sprint(probes)) - } -} - -// WithMinRTTTimeout sets the minimal probe round trip time. -func WithMinRTTTimeout(roundTripTime time.Duration) Option { - milliseconds := roundTripTime.Round(time.Nanosecond).Nanoseconds() / 1000000 - - return func(s *Scanner) { - s.args = append(s.args, "--min-rtt-timeout") - s.args = append(s.args, fmt.Sprintf("%dms", int(milliseconds))) - } -} - -// WithMaxRTTTimeout sets the maximal probe round trip time. -func WithMaxRTTTimeout(roundTripTime time.Duration) Option { - milliseconds := roundTripTime.Round(time.Nanosecond).Nanoseconds() / 1000000 - - return func(s *Scanner) { - s.args = append(s.args, "--max-rtt-timeout") - s.args = append(s.args, fmt.Sprintf("%dms", int(milliseconds))) - } -} - -// WithInitialRTTTimeout sets the initial probe round trip time. -func WithInitialRTTTimeout(roundTripTime time.Duration) Option { - milliseconds := roundTripTime.Round(time.Nanosecond).Nanoseconds() / 1000000 - - return func(s *Scanner) { - s.args = append(s.args, "--initial-rtt-timeout") - s.args = append(s.args, fmt.Sprintf("%dms", int(milliseconds))) - } -} - -// WithMaxRetries sets the maximal number of port scan probe retransmissions. -func WithMaxRetries(tries int) Option { - return func(s *Scanner) { - s.args = append(s.args, "--max-retries") - s.args = append(s.args, fmt.Sprint(tries)) - } -} - -// WithHostTimeout sets the time after which nmap should give up on a target host. -func WithHostTimeout(timeout time.Duration) Option { - milliseconds := timeout.Round(time.Nanosecond).Nanoseconds() / 1000000 - - return func(s *Scanner) { - s.args = append(s.args, "--host-timeout") - s.args = append(s.args, fmt.Sprintf("%dms", int(milliseconds))) - } -} - -// WithScanDelay sets the minimum time to wait between each probe sent to a host. -func WithScanDelay(timeout time.Duration) Option { - milliseconds := timeout.Round(time.Nanosecond).Nanoseconds() / 1000000 - - return func(s *Scanner) { - s.args = append(s.args, "--scan-delay") - s.args = append(s.args, fmt.Sprintf("%dms", int(milliseconds))) - } -} - -// WithMaxScanDelay sets the maximum time to wait between each probe sent to a host. -func WithMaxScanDelay(timeout time.Duration) Option { - milliseconds := timeout.Round(time.Nanosecond).Nanoseconds() / 1000000 - - return func(s *Scanner) { - s.args = append(s.args, "--max-scan-delay") - s.args = append(s.args, fmt.Sprintf("%dms", int(milliseconds))) - } -} - -// WithMinRate sets the minimal number of packets sent per second. -func WithMinRate(packetsPerSecond int) Option { - return func(s *Scanner) { - s.args = append(s.args, "--min-rate") - s.args = append(s.args, fmt.Sprint(packetsPerSecond)) - } -} - -// WithMaxRate sets the maximal number of packets sent per second. -func WithMaxRate(packetsPerSecond int) Option { - return func(s *Scanner) { - s.args = append(s.args, "--max-rate") - s.args = append(s.args, fmt.Sprint(packetsPerSecond)) - } -} - -/*** Firewalls/IDS evasion and spoofing ***/ - -// WithFragmentPackets enables the use of tiny fragmented IP packets in order to -// split up the TCP header over several packets to make it harder for packet -// filters, intrusion detection systems, and other annoyances to detect what -// you are doing. -// Some programs have trouble handling these tiny packets. -func WithFragmentPackets() Option { - return func(s *Scanner) { - s.args = append(s.args, "-f") - } -} - -// WithMTU allows you to specify your own offset size for fragmenting IP packets. -// Using fragmented packets allows to split up the TCP header over several packets -// to make it harder for packet filters, intrusion detection systems, and other -// annoyances to detect what you are doing. -// Some programs have trouble handling these tiny packets. -func WithMTU(offset int) Option { - return func(s *Scanner) { - s.args = append(s.args, "--mtu") - s.args = append(s.args, fmt.Sprint(offset)) - } -} - -// WithDecoys causes a decoy scan to be performed, which makes it appear to the -// remote host that the host(s) you specify as decoys are scanning the target -// network too. Thus their IDS might report 5–10 port scans from unique IP -// addresses, but they won't know which IP was scanning them and which were -// innocent decoys. -// While this can be defeated through router path tracing, response-dropping, -// and other active mechanisms, it is generally an effective technique for -// hiding your IP address. -// You can optionally use ME as one of the decoys to represent the position -// for your real IP address. -// If you put ME in the sixth position or later, some common port scan -// detectors are unlikely to show your IP address at all. -func WithDecoys(decoys ...string) Option { - decoyList := strings.Join(decoys, ",") - - return func(s *Scanner) { - s.args = append(s.args, "-D") - s.args = append(s.args, decoyList) - } -} - -// WithSpoofIPAddress spoofs the IP address of the machine which is running nmap. -// This can be used if nmap is unable to determine your source address. -// Another possible use of this flag is to spoof the scan to make the targets -// think that someone else is scanning them. The WithInterface option and -// WithSkipHostDiscovery are generally required for this sort of usage. Note -// that you usually won't receive reply packets back (they will be addressed to -// the IP you are spoofing), so Nmap won't produce useful reports. -func WithSpoofIPAddress(ip string) Option { - return func(s *Scanner) { - s.args = append(s.args, "-S") - s.args = append(s.args, ip) - } -} - -// WithInterface specifies which network interface to use for scanning. -func WithInterface(iface string) Option { - return func(s *Scanner) { - s.args = append(s.args, "-e") - s.args = append(s.args, iface) - } -} - -// WithSourcePort specifies from which port to scan. -func WithSourcePort(port uint16) Option { - return func(s *Scanner) { - s.args = append(s.args, "--source-port") - s.args = append(s.args, fmt.Sprint(port)) - } -} - -// WithProxies allows to relay connection through HTTP/SOCKS4 proxies. -func WithProxies(proxies ...string) Option { - proxyList := strings.Join(proxies, ",") - - return func(s *Scanner) { - s.args = append(s.args, "--proxies") - s.args = append(s.args, proxyList) - } -} - -// WithHexData appends a custom hex-encoded payload to sent packets. -func WithHexData(data string) Option { - return func(s *Scanner) { - s.args = append(s.args, "--data") - s.args = append(s.args, data) - } -} - -// WithASCIIData appends a custom ascii-encoded payload to sent packets. -func WithASCIIData(data string) Option { - return func(s *Scanner) { - s.args = append(s.args, "--data-string") - s.args = append(s.args, data) - } -} - -// WithDataLength appends a random payload of the given length to sent packets. -func WithDataLength(length int) Option { - return func(s *Scanner) { - s.args = append(s.args, "--data-length") - s.args = append(s.args, fmt.Sprint(length)) - } -} - -// WithIPOptions uses the specified IP options to send packets. -// You may be able to use the record route option to determine a -// path to a target even when more traditional traceroute-style -// approaches fail. See http://seclists.org/nmap-dev/2006/q3/52 -// for examples of use. -func WithIPOptions(options string) Option { - return func(s *Scanner) { - s.args = append(s.args, "--ip-options") - s.args = append(s.args, options) - } -} - -// WithIPTimeToLive sets the IP time-to-live field of IP packets. -func WithIPTimeToLive(ttl int16) Option { - return func(s *Scanner) { - if ttl < 0 || ttl > 255 { - panic("value given to nmap.WithIPTimeToLive() should be between 0 and 255") - } - - s.args = append(s.args, "--ttl") - s.args = append(s.args, fmt.Sprint(ttl)) - } -} - -// WithSpoofMAC uses the given MAC address for all of the raw -// ethernet frames the scanner sends. This option implies -// WithSendEthernet to ensure that Nmap actually sends ethernet-level -// packets. -// Valid argument examples are Apple, 0, 01:02:03:04:05:06, -// deadbeefcafe, 0020F2, and Cisco. -func WithSpoofMAC(argument string) Option { - return func(s *Scanner) { - s.args = append(s.args, "--spoof-mac") - s.args = append(s.args, argument) - } -} - -// WithBadSum makes nmap send an invalid TCP, UDP or SCTP checksum -// for packets sent to target hosts. Since virtually all host IP -// stacks properly drop these packets, any responses received are -// likely coming from a firewall or IDS that didn't bother to -// verify the checksum. -func WithBadSum() Option { - return func(s *Scanner) { - s.args = append(s.args, "--badsum") - } -} - -/*** Output ***/ - -// WithVerbosity sets and increases the verbosity level of nmap. -func WithVerbosity(level int) Option { - - return func(s *Scanner) { - if level < 0 || level > 10 { - panic("value given to nmap.WithVerbosity() should be between 0 and 10") - } - s.args = append(s.args, fmt.Sprintf("-v%d", level)) - } -} - -// WithDebugging sets and increases the debugging level of nmap. -func WithDebugging(level int) Option { - return func(s *Scanner) { - if level < 0 || level > 10 { - panic("value given to nmap.WithDebugging() should be between 0 and 10") - } - s.args = append(s.args, fmt.Sprintf("-d%d", level)) - } -} - -// WithReason makes nmap specify why a port is in a particular state. -func WithReason() Option { - return func(s *Scanner) { - s.args = append(s.args, "--reason") - } -} - -// WithOpenOnly makes nmap only show open ports. -func WithOpenOnly() Option { - return func(s *Scanner) { - s.args = append(s.args, "--open") - } -} - -// WithPacketTrace makes nmap show all packets sent and received. -func WithPacketTrace() Option { - return func(s *Scanner) { - s.args = append(s.args, "--packet-trace") - } -} - -// WithAppendOutput makes nmap append to files instead of overwriting them. -// Currently does nothing, since this library doesn't write in files. -func WithAppendOutput() Option { - return func(s *Scanner) { - s.args = append(s.args, "--append-output") - } -} - -// WithResumePreviousScan makes nmap continue a scan that was aborted, -// from an output file. -func WithResumePreviousScan(filePath string) Option { - return func(s *Scanner) { - s.args = append(s.args, "--resume") - s.args = append(s.args, filePath) - } -} - -// WithStylesheet makes nmap apply an XSL stylesheet to transform its -// XML output to HTML. -func WithStylesheet(stylesheetPath string) Option { - return func(s *Scanner) { - s.args = append(s.args, "--stylesheet") - s.args = append(s.args, stylesheetPath) - } -} - -// WithWebXML makes nmap apply the default nmap.org stylesheet to transform -// XML output to HTML. The stylesheet can be found at -// https://nmap.org/svn/docs/nmap.xsl -func WithWebXML() Option { - return func(s *Scanner) { - s.args = append(s.args, "--webxml") - } -} - -// WithNoStylesheet prevents the use of XSL stylesheets with the XML output. -func WithNoStylesheet() Option { - return func(s *Scanner) { - s.args = append(s.args, "--no-stylesheet") - } -} - -/*** Misc ***/ - -// WithIPv6Scanning enables the use of IPv6 scanning. -func WithIPv6Scanning() Option { - return func(s *Scanner) { - s.args = append(s.args, "-6") - } -} - -// WithAggressiveScan enables the use of aggressive scan options. This has -// the same effect as using WithOSDetection, WithServiceInfo, WithDefaultScript -// and WithTraceRoute at the same time. -// Because script scanning with the default set is considered intrusive, you -// should not use this method against target networks without permission. -func WithAggressiveScan() Option { - return func(s *Scanner) { - s.args = append(s.args, "-A") - } -} - -// WithDataDir specifies a custom data directory for nmap to get its -// nmap-service-probes, nmap-services, nmap-protocols, nmap-rpc, -// nmap-mac-prefixes, and nmap-os-db. -func WithDataDir(directoryPath string) Option { - return func(s *Scanner) { - s.args = append(s.args, "--datadir") - s.args = append(s.args, directoryPath) - } -} - -// WithSendEthernet makes nmap send packets at the raw ethernet (data link) -// layer rather than the higher IP (network) layer. By default, nmap chooses -// the one which is generally best for the platform it is running on. -func WithSendEthernet() Option { - return func(s *Scanner) { - s.args = append(s.args, "--send-eth") - } -} - -// WithSendIP makes nmap send packets via raw IP sockets rather than sending -// lower level ethernet frames. -func WithSendIP() Option { - return func(s *Scanner) { - s.args = append(s.args, "--send-ip") - } -} - -// WithPrivileged makes nmap assume that the user is fully privileged. -func WithPrivileged() Option { - return func(s *Scanner) { - s.args = append(s.args, "--privileged") - } -} - -// WithUnprivileged makes nmap assume that the user lacks raw socket privileges. -func WithUnprivileged() Option { - return func(s *Scanner) { - s.args = append(s.args, "--unprivileged") - } -} - -// WithNmapOutput makes nmap output standard output to the filename specified. -func WithNmapOutput(outputFileName string) Option { - return func(s *Scanner) { - s.args = append(s.args, "-oN") - s.args = append(s.args, outputFileName) - } -} - -// WithGrepOutput makes nmap output greppable output to the filename specified. -func WithGrepOutput(outputFileName string) Option { - return func(s *Scanner) { - s.args = append(s.args, "-oG") - s.args = append(s.args, outputFileName) - } -} - -// WithCustomSysProcAttr allows customizing the *syscall.SysProcAttr on the *exec.Cmd instance -func WithCustomSysProcAttr(f func(*syscall.SysProcAttr)) Option { - return func(s *Scanner) { - s.modifySysProcAttr = f - } -} - -// ReturnArgs return the list of nmap args -func (s *Scanner) Args() []string { - return s.args -} diff --git a/nmap_test.go b/nmap_test.go index 61fd3b6..b93a358 100644 --- a/nmap_test.go +++ b/nmap_test.go @@ -1,39 +1,31 @@ package nmap import ( + "bytes" "context" "encoding/xml" - "errors" "io/ioutil" "os" "os/exec" "reflect" - "strings" "testing" "time" "github.com/stretchr/testify/assert" ) -type testStreamer struct { - Streamer -} +type testStreamer struct{} // Write is a function that handles the normal nmap stdout. func (c *testStreamer) Write(d []byte) (int, error) { return len(d), nil } -// Bytes returns scan result bytes. -func (c *testStreamer) Bytes() []byte { - return []byte{} -} - func TestNmapNotInstalled(t *testing.T) { oldPath := os.Getenv("PATH") _ = os.Setenv("PATH", "") - s, err := NewScanner() + s, err := NewScanner(context.TODO()) if err == nil { t.Error("expected NewScanner to fail if nmap is not found in $PATH") } @@ -71,8 +63,8 @@ func TestRun(t *testing.T) { WithBinaryPath("/invalid"), }, - expectedErr: true, - expectedResult: nil, + expectedErr: true, + expectedWarnings: []string{}, }, { description: "output can't be parsed", @@ -94,7 +86,8 @@ func TestRun(t *testing.T) { testTimeout: true, - expectedErr: true, + expectedErr: true, + expectedWarnings: []string{}, }, { description: "scan localhost", @@ -108,6 +101,8 @@ func TestRun(t *testing.T) { Args: nmapPath + " -T5 -oX - localhost", Scanner: "nmap", }, + + expectedWarnings: []string{}, }, { description: "scan invalid target", @@ -129,7 +124,8 @@ func TestRun(t *testing.T) { WithCustomArguments("tests/xml/scan_error_resolving_name.xml"), }, - expectedErr: true, + expectedErr: true, + expectedWarnings: []string{}, expectedResult: &Run{ Scanner: "fake_nmap", Args: "nmap test", @@ -142,7 +138,8 @@ func TestRun(t *testing.T) { WithCustomArguments("tests/xml/scan_error_other.xml"), }, - expectedErr: true, + expectedErr: true, + expectedWarnings: []string{}, expectedResult: &Run{ Scanner: "fake_nmap", Args: "nmap test", @@ -162,7 +159,8 @@ func TestRun(t *testing.T) { WithTimingTemplate(TimingFastest), }, - compareWholeRun: true, + compareWholeRun: true, + expectedWarnings: []string{}, expectedResult: &Run{ XMLName: xml.Name{Local: "nmaprun"}, @@ -205,9 +203,10 @@ func TestRun(t *testing.T) { for _, test := range tests { t.Run(test.description, func(t *testing.T) { + ctx := context.Background() if test.testTimeout { - ctx, cancel := context.WithTimeout(context.Background(), 99*time.Hour) - test.options = append(test.options, WithContext(ctx)) + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), 99*time.Hour) go (func() { // Cancel context to force timeout @@ -216,7 +215,7 @@ func TestRun(t *testing.T) { })() } - s, err := NewScanner(test.options...) + s, err := NewScanner(ctx, test.options...) if err != nil { panic(err) // this is never supposed to err, as we are testing run and not new. } @@ -227,15 +226,9 @@ func TestRun(t *testing.T) { return } - assert.Equal(t, test.expectedWarnings, warns) + assert.Equal(t, test.expectedWarnings, *warns) - if result == nil && test.expectedResult == nil { - return - } else if result == nil && test.expectedResult != nil { - t.Error("expected non-nil result, got nil") - return - } else if result != nil && test.expectedResult == nil { - t.Error("expected nil result, got non-nil") + if test.expectedResult == nil { return } @@ -264,7 +257,8 @@ func TestRunWithProgress(t *testing.T) { panic(err) } - r, _ := Parse(dat) + var r = &Run{} + _ = Parse(dat, r) tests := []struct { description string @@ -294,13 +288,13 @@ func TestRunWithProgress(t *testing.T) { for _, test := range tests { t.Run(test.description, func(t *testing.T) { - s, err := NewScanner(test.options...) + s, err := NewScanner(context.TODO(), test.options...) if err != nil { panic(err) // this is never supposed to err, as we are testing run and not new. } progress := make(chan float32, 5) - result, _, err := s.RunWithProgress(progress) + result, _, err := s.Progress(progress).Run() assert.Equal(t, test.expectedErr, err) if err != nil { return @@ -338,22 +332,23 @@ func TestRunWithStreamer(t *testing.T) { WithBinaryPath("tests/scripts/fake_nmap.sh"), WithCustomArguments("tests/xml/scan_base.xml"), }, - expectedErr: nil, + expectedErr: nil, + expectedWarnings: []string{}, }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { - s, err := NewScanner(test.options...) + s, err := NewScanner(context.TODO(), test.options...) if err != nil { panic(err) // this is never supposed to err, as we are testing run and not new. } - warnings, err := s.RunWithStreamer(streamer, "/tmp/nmap-stream-test") + _, warnings, err := s.Streamer(streamer).Run() assert.Equal(t, test.expectedErr, err) - assert.Equal(t, test.expectedWarnings, warnings) + assert.Equal(t, test.expectedWarnings, *warnings) }) } } @@ -368,8 +363,7 @@ func TestRunAsync(t *testing.T) { compareWholeRun bool expectedResult *Run - expectedRunAsyncErr error - expectedParseErr error + expectedRunAsyncErr bool expectedWaitErr bool }{ { @@ -380,8 +374,7 @@ func TestRunAsync(t *testing.T) { WithBinaryPath("/invalid"), }, - expectedResult: nil, - expectedRunAsyncErr: errors.New("unable to execute asynchronous nmap run: fork/exec /invalid: no such file or directory"), + expectedRunAsyncErr: true, }, { description: "output can't be parsed", @@ -391,8 +384,7 @@ func TestRunAsync(t *testing.T) { WithBinaryPath("echo"), }, - expectedResult: nil, - expectedParseErr: errors.New("EOF"), + expectedWaitErr: true, }, { description: "context timeout", @@ -403,44 +395,39 @@ func TestRunAsync(t *testing.T) { testTimeout: true, - expectedResult: nil, expectedWaitErr: true, }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { + ctx := context.Background() if test.testTimeout { - ctx, cancel := context.WithTimeout(context.Background(), 99*time.Hour) - test.options = append(test.options, WithContext(ctx)) + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), 99*time.Hour) go (func() { // Cancel context to force timeout defer cancel() - time.Sleep(1 * time.Millisecond) + time.Sleep(10 * time.Millisecond) })() } - s, err := NewScanner(test.options...) + s, err := NewScanner(ctx, test.options...) if err != nil { panic(err) // this is never supposed to err, as we are testing run and not new. } - err = s.RunAsync() - assert.Equal(t, test.expectedRunAsyncErr, err) + done := make(chan error) + result, _, err := s.Async(done).Run() + if test.expectedRunAsyncErr { + assert.NotNil(t, err) + } if err != nil { return } - stdout := s.GetStdout() - var content []byte - go func() { - for stdout.Scan() { - content = stdout.Bytes() - } - }() - - err = s.Wait() + err = <-done if test.expectedWaitErr { assert.Error(t, err) } else { @@ -450,15 +437,7 @@ func TestRunAsync(t *testing.T) { return } - result, err := Parse(content) - assert.Equal(t, test.expectedParseErr, err) - - if result == nil && test.expectedResult == nil { - return - } else if result == nil && test.expectedResult != nil { - t.Error("expected non-nil result, got nil") - return - } else if test.expectedResult == nil { + if test.expectedResult == nil { return } @@ -467,1682 +446,41 @@ func TestRunAsync(t *testing.T) { if !reflect.DeepEqual(test.expectedResult, result) { t.Errorf("expected result to be %+v, got %+v", test.expectedResult, result) } - } else { - if result.Args != test.expectedResult.Args { - t.Errorf("expected args %q got %q", test.expectedResult.Args, result.Args) - } - - if result.Scanner != test.expectedResult.Scanner { - t.Errorf("expected scanner %q got %q", test.expectedResult.Scanner, result.Scanner) - } - } - }) - } -} - -func TestTargetSpecification(t *testing.T) { - tests := []struct { - description string - - options []Option - - expectedArgs []string - }{ - { - description: "custom arguments", - - options: []Option{ - WithTargets("0.0.0.0/24"), - WithCustomArguments("--invalid-argument"), - }, - - expectedArgs: []string{ - "0.0.0.0/24", - "--invalid-argument", - }, - }, - { - description: "set target", - - options: []Option{ - WithTargets("0.0.0.0/24"), - }, - - expectedArgs: []string{ - "0.0.0.0/24", - }, - }, - { - description: "set multiple targets", - - options: []Option{ - WithTargets("0.0.0.0", "192.168.1.1"), - }, - - expectedArgs: []string{ - "0.0.0.0", - "192.168.1.1", - }, - }, - { - description: "set target from file", - - options: []Option{ - WithTargetInput("/targets.txt"), - }, - - expectedArgs: []string{ - "-iL", - "/targets.txt", - }, - }, - { - description: "choose random targets", - - options: []Option{ - WithRandomTargets(4), - }, - - expectedArgs: []string{ - "-iR", - "4", - }, - }, - { - description: "unique addresses", - - options: []Option{ - WithUnique(), - }, - - expectedArgs: []string{ - "--unique", - }, - }, - { - description: "target exclusion", - - options: []Option{ - WithTargetExclusion("192.168.0.1,172.16.100.0/24"), - }, - - expectedArgs: []string{ - "--exclude", - "192.168.0.1,172.16.100.0/24", - }, - }, - { - description: "target exclusion from file", - - options: []Option{ - WithTargetExclusionInput("/exclude_targets.txt"), - }, - - expectedArgs: []string{ - "--excludefile", - "/exclude_targets.txt", - }, - }, - } - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - s, err := NewScanner(test.options...) - if err != nil { - panic(err) - } - - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) } }) } } -func TestHostDiscovery(t *testing.T) { +func TestCheckStdErr(t *testing.T) { tests := []struct { description string - - options []Option - - expectedArgs []string - }{ - { - description: "list targets to scan", - - options: []Option{ - WithListScan(), - }, - - expectedArgs: []string{ - "-sL", - }, - }, - { - description: "ping scan - disable port scan", - - options: []Option{ - WithPingScan(), - }, - - expectedArgs: []string{ - "-sn", - }, - }, - { - description: "skip host discovery", - - options: []Option{ - WithSkipHostDiscovery(), - }, - - expectedArgs: []string{ - "-Pn", - }, - }, - { - description: "TCP SYN packets for all ports", - - options: []Option{ - WithSYNDiscovery(), - }, - - expectedArgs: []string{ - "-PS", - }, - }, - { - description: "TCP SYN packets for specific ports", - - options: []Option{ - WithSYNDiscovery("443", "8443"), - }, - - expectedArgs: []string{ - "-PS443,8443", - }, - }, - { - description: "TCP ACK packets for all ports", - - options: []Option{ - WithACKDiscovery(), - }, - - expectedArgs: []string{ - "-PA", - }, - }, - { - description: "TCP ACK packets for specific ports", - - options: []Option{ - WithACKDiscovery("443", "8443"), - }, - - expectedArgs: []string{ - "-PA443,8443", - }, - }, - { - description: "UDP packets for all ports", - - options: []Option{ - WithUDPDiscovery(), - }, - - expectedArgs: []string{ - "-PU", - }, - }, - { - description: "UDP packets for specific ports", - - options: []Option{ - WithUDPDiscovery("443", "8443"), - }, - - expectedArgs: []string{ - "-PU443,8443", - }, - }, - { - description: "SCTP packets for all ports", - - options: []Option{ - WithSCTPDiscovery(), - }, - - expectedArgs: []string{ - "-PY", - }, - }, - { - description: "SCTP packets for specific ports", - - options: []Option{ - WithSCTPDiscovery("443", "8443"), - }, - - expectedArgs: []string{ - "-PY443,8443", - }, - }, - { - description: "ICMP echo request discovery probes", - - options: []Option{ - WithICMPEchoDiscovery(), - }, - - expectedArgs: []string{ - "-PE", - }, - }, - { - description: "ICMP Timestamp request discovery probes", - - options: []Option{ - WithICMPTimestampDiscovery(), - }, - - expectedArgs: []string{ - "-PP", - }, - }, - { - description: "ICMP NetMask request discovery probes", - - options: []Option{ - WithICMPNetMaskDiscovery(), - }, - - expectedArgs: []string{ - "-PM", - }, - }, - { - description: "IP protocol ping", - - options: []Option{ - WithIPProtocolPingDiscovery("1", "2", "4"), - }, - - expectedArgs: []string{ - "-PO1,2,4", - }, - }, - { - description: "disable DNS resolution during discovery", - - options: []Option{ - WithDisabledDNSResolution(), - }, - - expectedArgs: []string{ - "-n", - }, - }, - { - description: "enforce DNS resolution during discovery", - - options: []Option{ - WithForcedDNSResolution(), - }, - - expectedArgs: []string{ - "-R", - }, - }, - { - description: "custom DNS server", - - options: []Option{ - WithCustomDNSServers("8.8.8.8", "8.8.4.4"), - }, - - expectedArgs: []string{ - "--dns-servers", - "8.8.8.8,8.8.4.4", - }, - }, - { - description: "use system DNS", - - options: []Option{ - WithSystemDNS(), - }, - - expectedArgs: []string{ - "--system-dns", - }, - }, - { - description: "traceroute", - - options: []Option{ - WithTraceRoute(), - }, - - expectedArgs: []string{ - "--traceroute", - }, - }, - } - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - s, err := NewScanner(test.options...) - if err != nil { - panic(err) - } - - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) - } - }) - } -} - -func TestScanTechniques(t *testing.T) { - tests := []struct { - description string - - options []Option - - expectedArgs []string - }{ - { - description: "TCP SYN scan", - - options: []Option{ - WithSYNScan(), - }, - - expectedArgs: []string{ - "-sS", - }, - }, - { - description: "TCP Connect() scan", - - options: []Option{ - WithConnectScan(), - }, - - expectedArgs: []string{ - "-sT", - }, - }, - { - description: "TCP ACK scan", - - options: []Option{ - WithACKScan(), - }, - - expectedArgs: []string{ - "-sA", - }, - }, - { - description: "TCP Window scan", - - options: []Option{ - WithWindowScan(), - }, - - expectedArgs: []string{ - "-sW", - }, - }, - { - description: "Maimon scan", - - options: []Option{ - WithMaimonScan(), - }, - - expectedArgs: []string{ - "-sM", - }, - }, - { - description: "UDP scan", - - options: []Option{ - WithUDPScan(), - }, - - expectedArgs: []string{ - "-sU", - }, - }, - { - description: "TCP Null scan", - - options: []Option{ - WithTCPNullScan(), - }, - - expectedArgs: []string{ - "-sN", - }, - }, - { - description: "TCP FIN scan", - - options: []Option{ - WithTCPFINScan(), - }, - - expectedArgs: []string{ - "-sF", - }, - }, - { - description: "TCP Xmas scan", - - options: []Option{ - WithTCPXmasScan(), - }, - - expectedArgs: []string{ - "-sX", - }, - }, - { - description: "TCP custom scan flags", - - options: []Option{ - WithTCPScanFlags(FlagACK, FlagFIN, FlagNULL), - }, - - expectedArgs: []string{ - "--scanflags", - "11", - }, - }, - { - description: "idle scan through zombie host with probe port specified", - - options: []Option{ - WithIdleScan("192.168.1.1", 61436), - }, - - expectedArgs: []string{ - "-sI", - "192.168.1.1:61436", - }, - }, - { - description: "idle scan through zombie host without probe port specified", - - options: []Option{ - WithIdleScan("192.168.1.1", 0), - }, - - expectedArgs: []string{ - "-sI", - "192.168.1.1", - }, - }, - { - description: "SCTP INIT scan", - - options: []Option{ - WithSCTPInitScan(), - }, - - expectedArgs: []string{ - "-sY", - }, - }, - { - description: "SCTP COOKIE-ECHO scan", - - options: []Option{ - WithSCTPCookieEchoScan(), - }, - - expectedArgs: []string{ - "-sZ", - }, - }, - { - description: "IP protocol scan", - - options: []Option{ - WithIPProtocolScan(), - }, - - expectedArgs: []string{ - "-sO", - }, - }, - { - description: "FTP bounce scan", - - options: []Option{ - WithFTPBounceScan("192.168.0.254"), - }, - - expectedArgs: []string{ - "-b", - "192.168.0.254", - }, - }, - } - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - s, err := NewScanner(test.options...) - if err != nil { - panic(err) - } - - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) - } - }) - } -} - -func TestPortSpecAndScanOrder(t *testing.T) { - tests := []struct { - description string - - options []Option - - expectedPanic string - expectedArgs []string - }{ - { - description: "specify ports to scan", - - options: []Option{ - WithPorts("554", "8554"), - WithPorts("80-81"), - }, - - expectedArgs: []string{ - "-p", - "554,8554,80-81", - }, - }, - { - description: "exclude ports to scan", - - options: []Option{ - WithPortExclusions("554", "8554"), - }, - - expectedArgs: []string{ - "--exclude-ports", - "554,8554", - }, - }, - { - description: "fast mode - scan fewer ports than the default scan", - - options: []Option{ - WithFastMode(), - }, - - expectedArgs: []string{ - "-F", - }, - }, - { - description: "consecutive port scanning", - - options: []Option{ - WithConsecutivePortScanning(), - }, - - expectedArgs: []string{ - "-r", - }, - }, - { - description: "scan most commonly open ports", - - options: []Option{ - WithMostCommonPorts(5), - }, - - expectedArgs: []string{ - "--top-ports", - "5", - }, - }, - { - description: "scan most commonly open ports given a ratio - should be rounded to 0.4", - - options: []Option{ - WithPortRatio(0.42010101), - }, - - expectedArgs: []string{ - "--port-ratio", - "0.4", - }, - }, - { - description: "scan most commonly open ports given a ratio - should be invalid and panic", - - options: []Option{ - WithPortRatio(2), - }, - - expectedPanic: "value given to nmap.WithPortRatio() should be between 0 and 1", - }, - } - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - if test.expectedPanic != "" { - defer func() { - recoveredMessage := recover() - - if recoveredMessage != test.expectedPanic { - t.Errorf("expected panic message to be %q but got %q", test.expectedPanic, recoveredMessage) - } - }() - } - - s, err := NewScanner(test.options...) - if err != nil { - panic(err) - } - - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) - } - }) - } -} - -func TestServiceDetection(t *testing.T) { - tests := []struct { - description string - - options []Option - - expectedPanic string - expectedArgs []string - }{ - { - description: "service detection", - - options: []Option{ - WithServiceInfo(), - }, - - expectedArgs: []string{ - "-sV", - }, - }, - { - description: "service detection custom intensity", - - options: []Option{ - WithVersionIntensity(1), - }, - - expectedArgs: []string{ - "--version-intensity", - "1", - }, - }, - { - description: "service detection custom intensity - should panic since not between 0 and 9", - - options: []Option{ - WithVersionIntensity(42), - }, - - expectedPanic: "value given to nmap.WithVersionIntensity() should be between 0 and 9", - }, - { - description: "service detection light intensity", - - options: []Option{ - WithVersionLight(), - }, - - expectedArgs: []string{ - "--version-light", - }, - }, - { - description: "service detection highest intensity", - - options: []Option{ - WithVersionAll(), - }, - - expectedArgs: []string{ - "--version-all", - }, - }, - { - description: "service detection enable trace", - - options: []Option{ - WithVersionTrace(), - }, - - expectedArgs: []string{ - "--version-trace", - }, - }, - } - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - if test.expectedPanic != "" { - defer func() { - recoveredMessage := recover() - - if recoveredMessage != test.expectedPanic { - t.Errorf("expected panic message to be %q but got %q", test.expectedPanic, recoveredMessage) - } - }() - } - - s, err := NewScanner(test.options...) - if err != nil { - panic(err) - } - - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) - } - }) - } -} - -func TestScriptScan(t *testing.T) { - tests := []struct { - description string - - targets []string - options []Option - unorderedArgs bool - - expectedArgs []string - }{ - { - description: "default script scan", - - options: []Option{ - WithDefaultScript(), - }, - - expectedArgs: []string{ - "-sC", - }, - }, - { - description: "custom script list", - - options: []Option{ - WithScripts("./scripts/", "/etc/nmap/nse/scripts"), - }, - - expectedArgs: []string{ - "--script=./scripts/,/etc/nmap/nse/scripts", - }, - }, - { - description: "script arguments", - - options: []Option{ - WithScriptArguments(map[string]string{ - "user": "foo", - "pass": "\",{}=bar\"", - "whois": "{whodb=nofollow+ripe}", - "xmpp-info.server_name": "localhost", - "vulns.showall": "", - }), - }, - - unorderedArgs: true, - - expectedArgs: []string{ - "--script-args=", - "user=foo", - "pass=\",{}=bar\"", - "whois={whodb=nofollow+ripe}", - "xmpp-info.server_name=localhost", - "vulns.showall", - }, - }, - { - description: "script arguments file", - - options: []Option{ - WithScriptArgumentsFile("/script_args.txt"), - }, - - expectedArgs: []string{ - "--script-args-file=/script_args.txt", - }, - }, - { - description: "enable script trace", - - options: []Option{ - WithScriptTrace(), - }, - - expectedArgs: []string{ - "--script-trace", - }, - }, - { - description: "update script database", - - options: []Option{ - WithScriptUpdateDB(), - }, - - expectedArgs: []string{ - "--script-updatedb", - }, - }, - { - description: "set script timeout", - - options: []Option{ - WithScriptTimeout(40 * time.Second), - }, - - expectedArgs: []string{ - "--script-timeout", - "40000ms", - }, - }, - } - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - s, err := NewScanner(test.options...) - if err != nil { - panic(err) - } - - if test.unorderedArgs { - for _, expectedArg := range test.expectedArgs { - if !strings.Contains(s.args[0], expectedArg) { - t.Errorf("missing argument %s in %v", expectedArg, s.args) - } - } - return - } - - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) - } - }) - } -} - -func TestOSDetection(t *testing.T) { - tests := []struct { - description string - - options []Option - - expectedArgs []string - }{ - { - description: "enable OS detection", - - options: []Option{ - WithOSDetection(), - }, - - expectedArgs: []string{ - "-O", - }, - }, - { - description: "enable OS scan limit", - - options: []Option{ - WithOSScanLimit(), - }, - - expectedArgs: []string{ - "--osscan-limit", - }, - }, - { - description: "enable OS scan guess", - - options: []Option{ - WithOSScanGuess(), - }, - - expectedArgs: []string{ - "--osscan-guess", - }, - }, - } - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - s, err := NewScanner(test.options...) - if err != nil { - panic(err) - } - - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) - } - }) - } -} - -func TestTimingAndPerformance(t *testing.T) { - tests := []struct { - description string - - options []Option - - expectedArgs []string - }{ - { - description: "set timing template", - - options: []Option{ - WithTimingTemplate(TimingAggressive), - }, - - expectedArgs: []string{ - "-T4", - }, - }, - { - description: "set stats every", - - options: []Option{ - WithStatsEvery("5s"), - }, - - expectedArgs: []string{ - "--stats-every", - "5s", - }, - }, - { - description: "set min hostgroup", - - options: []Option{ - WithMinHostgroup(42), - }, - - expectedArgs: []string{ - "--min-hostgroup", - "42", - }, - }, - { - description: "set max hostgroup", - - options: []Option{ - WithMaxHostgroup(42), - }, - - expectedArgs: []string{ - "--max-hostgroup", - "42", - }, - }, - { - description: "set min parallelism", - - options: []Option{ - WithMinParallelism(42), - }, - - expectedArgs: []string{ - "--min-parallelism", - "42", - }, - }, - { - description: "set max parallelism", - - options: []Option{ - WithMaxParallelism(42), - }, - - expectedArgs: []string{ - "--max-parallelism", - "42", - }, - }, - { - description: "set min rtt-timeout", - - options: []Option{ - WithMinRTTTimeout(2 * time.Minute), - }, - - expectedArgs: []string{ - "--min-rtt-timeout", - "120000ms", - }, - }, - { - description: "set max rtt-timeout", - - options: []Option{ - WithMaxRTTTimeout(8 * time.Hour), - }, - - expectedArgs: []string{ - "--max-rtt-timeout", - "28800000ms", - }, - }, - { - description: "set initial rtt-timeout", - - options: []Option{ - WithInitialRTTTimeout(8 * time.Hour), - }, - - expectedArgs: []string{ - "--initial-rtt-timeout", - "28800000ms", - }, - }, - { - description: "set max retries", - - options: []Option{ - WithMaxRetries(42), - }, - - expectedArgs: []string{ - "--max-retries", - "42", - }, - }, - { - description: "set host timeout", - - options: []Option{ - WithHostTimeout(42 * time.Second), - }, - - expectedArgs: []string{ - "--host-timeout", - "42000ms", - }, - }, - { - description: "set scan delay", - - options: []Option{ - WithScanDelay(42 * time.Millisecond), - }, - - expectedArgs: []string{ - "--scan-delay", - "42ms", - }, - }, - { - description: "set max scan delay", - - options: []Option{ - WithMaxScanDelay(42 * time.Millisecond), - }, - - expectedArgs: []string{ - "--max-scan-delay", - "42ms", - }, - }, - { - description: "set min rate", - - options: []Option{ - WithMinRate(42), - }, - - expectedArgs: []string{ - "--min-rate", - "42", - }, - }, - { - description: "set max rate", - - options: []Option{ - WithMaxRate(42), - }, - - expectedArgs: []string{ - "--max-rate", - "42", - }, - }, - } - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - s, err := NewScanner(test.options...) - if err != nil { - panic(err) - } - - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) - } - }) - } -} - -func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { - tests := []struct { - description string - - options []Option - - expectedPanic string - expectedArgs []string - }{ - { - description: "fragment packets", - - options: []Option{ - WithFragmentPackets(), - }, - - expectedArgs: []string{ - "-f", - }, - }, - { - description: "custom fragment packet size", - - options: []Option{ - WithMTU(42), - }, - - expectedArgs: []string{ - "--mtu", - "42", - }, - }, - { - description: "enable decoys", - - options: []Option{ - WithDecoys( - "192.168.1.1", - "192.168.1.2", - "192.168.1.3", - "192.168.1.4", - "192.168.1.5", - "192.168.1.6", - "ME", - "192.168.1.8", - ), - }, - - expectedArgs: []string{ - "-D", - "192.168.1.1,192.168.1.2,192.168.1.3,192.168.1.4,192.168.1.5,192.168.1.6,ME,192.168.1.8", - }, - }, - { - description: "spoof IP address", - - options: []Option{ - WithSpoofIPAddress("192.168.1.1"), - }, - - expectedArgs: []string{ - "-S", - "192.168.1.1", - }, - }, - { - description: "set interface", - - options: []Option{ - WithInterface("eth0"), - }, - - expectedArgs: []string{ - "-e", - "eth0", - }, - }, - { - description: "set source port", - - options: []Option{ - WithSourcePort(65535), - }, - - expectedArgs: []string{ - "--source-port", - "65535", - }, - }, - { - description: "set proxies", - - options: []Option{ - WithProxies("4242", "8484"), - }, - - expectedArgs: []string{ - "--proxies", - "4242,8484", - }, - }, - { - description: "set custom hex payload", - - options: []Option{ - WithHexData("0x8b6c42"), - }, - - expectedArgs: []string{ - "--data", - "0x8b6c42", - }, - }, - { - description: "set custom ascii payload", - - options: []Option{ - WithASCIIData("pale brownish"), - }, - - expectedArgs: []string{ - "--data-string", - "pale brownish", - }, - }, - { - description: "set custom random payload length", - - options: []Option{ - WithDataLength(42), - }, - - expectedArgs: []string{ - "--data-length", - "42", - }, - }, - { - description: "set custom IP options", - - options: []Option{ - WithIPOptions("S 192.168.1.1 10.0.0.3"), - }, - - expectedArgs: []string{ - "--ip-options", - "S 192.168.1.1 10.0.0.3", - }, - }, - { - description: "set custom TTL", - - options: []Option{ - WithIPTimeToLive(254), - }, - - expectedArgs: []string{ - "--ttl", - "254", - }, - }, - { - description: "set custom TTL - invalid value should panic", - - options: []Option{ - WithIPTimeToLive(-254), - }, - - expectedPanic: "value given to nmap.WithIPTimeToLive() should be between 0 and 255", - }, - { - description: "spoof mac address", - - options: []Option{ - WithSpoofMAC("08:67:47:0A:78:E4"), - }, - - expectedArgs: []string{ - "--spoof-mac", - "08:67:47:0A:78:E4", - }, - }, - { - description: "send packets with bad checksum", - - options: []Option{ - WithBadSum(), - }, - - expectedArgs: []string{ - "--badsum", - }, - }, - } - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - if test.expectedPanic != "" { - defer func() { - recoveredMessage := recover() - - if recoveredMessage != test.expectedPanic { - t.Errorf("expected panic message to be %q but got %q", test.expectedPanic, recoveredMessage) - } - }() - } - - s, err := NewScanner(test.options...) - if err != nil { - panic(err) - } - - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) - } - }) - } -} - -func TestOutput(t *testing.T) { - tests := []struct { - description string - - options []Option - - expectedArgs []string - }{ - { - description: "set verbosity", - - options: []Option{ - WithVerbosity(5), - }, - - expectedArgs: []string{ - "-v5", - }, - }, - { - description: "set debugging", - - options: []Option{ - WithDebugging(3), - }, - - expectedArgs: []string{ - "-d3", - }, - }, - { - description: "display reason", - - options: []Option{ - WithReason(), - }, - - expectedArgs: []string{ - "--reason", - }, - }, - { - description: "show only open ports", - - options: []Option{ - WithOpenOnly(), - }, - - expectedArgs: []string{ - "--open", - }, - }, - { - description: "enable packet trace", - - options: []Option{ - WithPacketTrace(), - }, - - expectedArgs: []string{ - "--packet-trace", - }, - }, - { - description: "enable appending output", - - options: []Option{ - WithAppendOutput(), - }, - - expectedArgs: []string{ - "--append-output", - }, - }, - { - description: "resume scan from file", - - options: []Option{ - WithResumePreviousScan("/nmap_scan.xml"), - }, - - expectedArgs: []string{ - "--resume", - "/nmap_scan.xml", - }, - }, - { - description: "use stylesheet from file", - - options: []Option{ - WithStylesheet("/nmap_stylesheet.xsl"), - }, - - expectedArgs: []string{ - "--stylesheet", - "/nmap_stylesheet.xsl", - }, - }, - { - description: "use stylesheet from file", - - options: []Option{ - WithStylesheet("/nmap_stylesheet.xsl"), - }, - - expectedArgs: []string{ - "--stylesheet", - "/nmap_stylesheet.xsl", - }, - }, - { - description: "use default nmap stylesheet", - - options: []Option{ - WithWebXML(), - }, - - expectedArgs: []string{ - "--webxml", - }, - }, - { - description: "disable stylesheets", - - options: []Option{ - WithNoStylesheet(), - }, - - expectedArgs: []string{ - "--no-stylesheet", - }, - }, - } - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - s, err := NewScanner(test.options...) - if err != nil { - panic(err) - } - - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) - } - }) - } -} - -func TestMiscellaneous(t *testing.T) { - tests := []struct { - description string - - options []Option - - expectedArgs []string - }{ - { - description: "enable ipv6 scanning", - - options: []Option{ - WithIPv6Scanning(), - }, - - expectedArgs: []string{ - "-6", - }, - }, - { - description: "enable aggressive scanning", - - options: []Option{ - WithAggressiveScan(), - }, - - expectedArgs: []string{ - "-A", - }, - }, - { - description: "set data dir", - - options: []Option{ - WithDataDir("/etc/nmap/data"), - }, - - expectedArgs: []string{ - "--datadir", - "/etc/nmap/data", - }, - }, - { - description: "send packets over ethernet", - - options: []Option{ - WithSendEthernet(), - }, - - expectedArgs: []string{ - "--send-eth", - }, - }, - { - description: "send packets over IP", - - options: []Option{ - WithSendIP(), - }, - - expectedArgs: []string{ - "--send-ip", - }, - }, - { - description: "assume user is privileged", - - options: []Option{ - WithPrivileged(), - }, - - expectedArgs: []string{ - "--privileged", - }, - }, - { - description: "assume user is unprivileged", - - options: []Option{ - WithUnprivileged(), - }, - - expectedArgs: []string{ - "--unprivileged", - }, - }, - } - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - s, err := NewScanner(test.options...) - if err != nil { - panic(err) - } - - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) - } - }) - } -} - -func TestAnalyzeWarnings(t *testing.T) { - tests := []struct { - description string - - warnings []string - + stderr string + warnings []string expectedErr error }{ { description: "Find no error warning", + stderr: " NoWarning \nNoWarning ", warnings: []string{"NoWarning", "NoWarning"}, expectedErr: nil, }, { description: "Find malloc error", - warnings: []string{" Malloc Failed! with "}, + stderr: " Malloc Failed! with ", + warnings: []string{"Malloc Failed! with"}, expectedErr: ErrMallocFailed, }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { - err := analyzeWarnings(test.warnings) + buf := bytes.Buffer{} + _, _ = buf.Write([]byte(test.stderr)) + var warnings []string + err := checkStdErr(&buf, &warnings) assert.Equal(t, test.expectedErr, err) + assert.True(t, reflect.DeepEqual(test.warnings, warnings)) }) } } diff --git a/optionsFirewallSpoofing.go b/optionsFirewallSpoofing.go new file mode 100644 index 0000000..a528031 --- /dev/null +++ b/optionsFirewallSpoofing.go @@ -0,0 +1,162 @@ +package nmap + +import ( + "fmt" + "strings" +) + +// WithFragmentPackets enables the use of tiny fragmented IP packets in order to +// split up the TCP header over several packets to make it harder for packet +// filters, intrusion detection systems, and other annoyances to detect what +// you are doing. +// Some programs have trouble handling these tiny packets. +func WithFragmentPackets() Option { + return func(s *Scanner) { + s.args = append(s.args, "-f") + } +} + +// WithMTU allows you to specify your own offset size for fragmenting IP packets. +// Using fragmented packets allows to split up the TCP header over several packets +// to make it harder for packet filters, intrusion detection systems, and other +// annoyances to detect what you are doing. +// Some programs have trouble handling these tiny packets. +func WithMTU(offset int) Option { + return func(s *Scanner) { + s.args = append(s.args, "--mtu") + s.args = append(s.args, fmt.Sprint(offset)) + } +} + +// WithDecoys causes a decoy scan to be performed, which makes it appear to the +// remote host that the host(s) you specify as decoys are scanning the target +// network too. Thus their IDS might report 5–10 port scans from unique IP +// addresses, but they won't know which IP was scanning them and which were +// innocent decoys. +// While this can be defeated through router path tracing, response-dropping, +// and other active mechanisms, it is generally an effective technique for +// hiding your IP address. +// You can optionally use ME as one of the decoys to represent the position +// for your real IP address. +// If you put ME in the sixth position or later, some common port scan +// detectors are unlikely to show your IP address at all. +func WithDecoys(decoys ...string) Option { + decoyList := strings.Join(decoys, ",") + + return func(s *Scanner) { + s.args = append(s.args, "-D") + s.args = append(s.args, decoyList) + } +} + +// WithSpoofIPAddress spoofs the IP address of the machine which is running nmap. +// This can be used if nmap is unable to determine your source address. +// Another possible use of this flag is to spoof the scan to make the targets +// think that someone else is scanning them. The WithInterface option and +// WithSkipHostDiscovery are generally required for this sort of usage. Note +// that you usually won't receive reply packets back (they will be addressed to +// the IP you are spoofing), so Nmap won't produce useful reports. +func WithSpoofIPAddress(ip string) Option { + return func(s *Scanner) { + s.args = append(s.args, "-S") + s.args = append(s.args, ip) + } +} + +// WithInterface specifies which network interface to use for scanning. +func WithInterface(iface string) Option { + return func(s *Scanner) { + s.args = append(s.args, "-e") + s.args = append(s.args, iface) + } +} + +// WithSourcePort specifies from which port to scan. +func WithSourcePort(port uint16) Option { + return func(s *Scanner) { + s.args = append(s.args, "--source-port") + s.args = append(s.args, fmt.Sprint(port)) + } +} + +// WithProxies allows to relay connection through HTTP/SOCKS4 proxies. +func WithProxies(proxies ...string) Option { + proxyList := strings.Join(proxies, ",") + + return func(s *Scanner) { + s.args = append(s.args, "--proxies") + s.args = append(s.args, proxyList) + } +} + +// WithHexData appends a custom hex-encoded payload to sent packets. +func WithHexData(data string) Option { + return func(s *Scanner) { + s.args = append(s.args, "--data") + s.args = append(s.args, data) + } +} + +// WithASCIIData appends a custom ascii-encoded payload to sent packets. +func WithASCIIData(data string) Option { + return func(s *Scanner) { + s.args = append(s.args, "--data-string") + s.args = append(s.args, data) + } +} + +// WithDataLength appends a random payload of the given length to sent packets. +func WithDataLength(length int) Option { + return func(s *Scanner) { + s.args = append(s.args, "--data-length") + s.args = append(s.args, fmt.Sprint(length)) + } +} + +// WithIPOptions uses the specified IP options to send packets. +// You may be able to use the record route option to determine a +// path to a target even when more traditional traceroute-style +// approaches fail. See http://seclists.org/nmap-dev/2006/q3/52 +// for examples of use. +func WithIPOptions(options string) Option { + return func(s *Scanner) { + s.args = append(s.args, "--ip-options") + s.args = append(s.args, options) + } +} + +// WithIPTimeToLive sets the IP time-to-live field of IP packets. +func WithIPTimeToLive(ttl int16) Option { + return func(s *Scanner) { + if ttl < 0 || ttl > 255 { + panic("value given to nmap.WithIPTimeToLive() should be between 0 and 255") + } + + s.args = append(s.args, "--ttl") + s.args = append(s.args, fmt.Sprint(ttl)) + } +} + +// WithSpoofMAC uses the given MAC address for all of the raw +// ethernet frames the scanner sends. This option implies +// WithSendEthernet to ensure that Nmap actually sends ethernet-level +// packets. +// Valid argument examples are Apple, 0, 01:02:03:04:05:06, +// deadbeefcafe, 0020F2, and Cisco. +func WithSpoofMAC(argument string) Option { + return func(s *Scanner) { + s.args = append(s.args, "--spoof-mac") + s.args = append(s.args, argument) + } +} + +// WithBadSum makes nmap send an invalid TCP, UDP or SCTP checksum +// for packets sent to target hosts. Since virtually all host IP +// stacks properly drop these packets, any responses received are +// likely coming from a firewall or IDS that didn't bother to +// verify the checksum. +func WithBadSum() Option { + return func(s *Scanner) { + s.args = append(s.args, "--badsum") + } +} diff --git a/optionsFirewallSpoofing_test.go b/optionsFirewallSpoofing_test.go new file mode 100644 index 0000000..44e5663 --- /dev/null +++ b/optionsFirewallSpoofing_test.go @@ -0,0 +1,226 @@ +package nmap + +import ( + "context" + "reflect" + "testing" +) + +func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { + tests := []struct { + description string + + options []Option + + expectedPanic string + expectedArgs []string + }{ + { + description: "fragment packets", + + options: []Option{ + WithFragmentPackets(), + }, + + expectedArgs: []string{ + "-f", + }, + }, + { + description: "custom fragment packet size", + + options: []Option{ + WithMTU(42), + }, + + expectedArgs: []string{ + "--mtu", + "42", + }, + }, + { + description: "enable decoys", + + options: []Option{ + WithDecoys( + "192.168.1.1", + "192.168.1.2", + "192.168.1.3", + "192.168.1.4", + "192.168.1.5", + "192.168.1.6", + "ME", + "192.168.1.8", + ), + }, + + expectedArgs: []string{ + "-D", + "192.168.1.1,192.168.1.2,192.168.1.3,192.168.1.4,192.168.1.5,192.168.1.6,ME,192.168.1.8", + }, + }, + { + description: "spoof IP address", + + options: []Option{ + WithSpoofIPAddress("192.168.1.1"), + }, + + expectedArgs: []string{ + "-S", + "192.168.1.1", + }, + }, + { + description: "set interface", + + options: []Option{ + WithInterface("eth0"), + }, + + expectedArgs: []string{ + "-e", + "eth0", + }, + }, + { + description: "set source port", + + options: []Option{ + WithSourcePort(65535), + }, + + expectedArgs: []string{ + "--source-port", + "65535", + }, + }, + { + description: "set proxies", + + options: []Option{ + WithProxies("4242", "8484"), + }, + + expectedArgs: []string{ + "--proxies", + "4242,8484", + }, + }, + { + description: "set custom hex payload", + + options: []Option{ + WithHexData("0x8b6c42"), + }, + + expectedArgs: []string{ + "--data", + "0x8b6c42", + }, + }, + { + description: "set custom ascii payload", + + options: []Option{ + WithASCIIData("pale brownish"), + }, + + expectedArgs: []string{ + "--data-string", + "pale brownish", + }, + }, + { + description: "set custom random payload length", + + options: []Option{ + WithDataLength(42), + }, + + expectedArgs: []string{ + "--data-length", + "42", + }, + }, + { + description: "set custom IP options", + + options: []Option{ + WithIPOptions("S 192.168.1.1 10.0.0.3"), + }, + + expectedArgs: []string{ + "--ip-options", + "S 192.168.1.1 10.0.0.3", + }, + }, + { + description: "set custom TTL", + + options: []Option{ + WithIPTimeToLive(254), + }, + + expectedArgs: []string{ + "--ttl", + "254", + }, + }, + { + description: "set custom TTL - invalid value should panic", + + options: []Option{ + WithIPTimeToLive(-254), + }, + + expectedPanic: "value given to nmap.WithIPTimeToLive() should be between 0 and 255", + }, + { + description: "spoof mac address", + + options: []Option{ + WithSpoofMAC("08:67:47:0A:78:E4"), + }, + + expectedArgs: []string{ + "--spoof-mac", + "08:67:47:0A:78:E4", + }, + }, + { + description: "send packets with bad checksum", + + options: []Option{ + WithBadSum(), + }, + + expectedArgs: []string{ + "--badsum", + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + if test.expectedPanic != "" { + defer func() { + recoveredMessage := recover() + + if recoveredMessage != test.expectedPanic { + t.Errorf("expected panic message to be %q but got %q", test.expectedPanic, recoveredMessage) + } + }() + } + + s, err := NewScanner(context.TODO(), test.options...) + if err != nil { + panic(err) + } + + if !reflect.DeepEqual(s.args, test.expectedArgs) { + t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) + } + }) + } +} diff --git a/optionsHostDiscovery.go b/optionsHostDiscovery.go new file mode 100644 index 0000000..bb0d690 --- /dev/null +++ b/optionsHostDiscovery.go @@ -0,0 +1,161 @@ +package nmap + +import ( + "fmt" + "strings" +) + +// WithListScan sets the discovery mode to simply list the targets to scan and not scan them. +func WithListScan() Option { + return func(s *Scanner) { + s.args = append(s.args, "-sL") + } +} + +// WithPingScan sets the discovery mode to simply ping the targets to scan and not scan them. +func WithPingScan() Option { + return func(s *Scanner) { + s.args = append(s.args, "-sn") + } +} + +// WithSkipHostDiscovery diables host discovery and considers all hosts as online. +func WithSkipHostDiscovery() Option { + return func(s *Scanner) { + s.args = append(s.args, "-Pn") + } +} + +// WithSYNDiscovery sets the discovery mode to use SYN packets. +// If the portList argument is empty, this will enable SYN discovery +// for all ports. Otherwise, it will be only for the specified ports. +func WithSYNDiscovery(ports ...string) Option { + portList := strings.Join(ports, ",") + + return func(s *Scanner) { + s.args = append(s.args, fmt.Sprintf("-PS%s", portList)) + } +} + +// WithACKDiscovery sets the discovery mode to use ACK packets. +// If the portList argument is empty, this will enable ACK discovery +// for all ports. Otherwise, it will be only for the specified ports. +func WithACKDiscovery(ports ...string) Option { + portList := strings.Join(ports, ",") + + return func(s *Scanner) { + s.args = append(s.args, fmt.Sprintf("-PA%s", portList)) + } +} + +// WithUDPDiscovery sets the discovery mode to use UDP packets. +// If the portList argument is empty, this will enable UDP discovery +// for all ports. Otherwise, it will be only for the specified ports. +func WithUDPDiscovery(ports ...string) Option { + portList := strings.Join(ports, ",") + + return func(s *Scanner) { + s.args = append(s.args, fmt.Sprintf("-PU%s", portList)) + } +} + +// WithSCTPDiscovery sets the discovery mode to use SCTP packets +// containing a minimal INIT chunk. +// If the portList argument is empty, this will enable SCTP discovery +// for all ports. Otherwise, it will be only for the specified ports. +// Warning: on Unix, only the privileged user root is generally +// able to send and receive raw SCTP packets. +func WithSCTPDiscovery(ports ...string) Option { + portList := strings.Join(ports, ",") + + return func(s *Scanner) { + s.args = append(s.args, fmt.Sprintf("-PY%s", portList)) + } +} + +// WithICMPEchoDiscovery sets the discovery mode to use an ICMP type 8 +// packet (an echo request), like the standard packets sent by the ping +// command. +// Many hosts and firewalls block these packets, so this is usually not +// the best for exploring networks. +func WithICMPEchoDiscovery() Option { + return func(s *Scanner) { + s.args = append(s.args, "-PE") + } +} + +// WithICMPTimestampDiscovery sets the discovery mode to use an ICMP type 13 +// packet (a timestamp request). +// This query can be valuable when administrators specifically block echo +// request packets while forgetting that other ICMP queries can be used +// for the same purpose. +func WithICMPTimestampDiscovery() Option { + return func(s *Scanner) { + s.args = append(s.args, "-PP") + } +} + +// WithICMPNetMaskDiscovery sets the discovery mode to use an ICMP type 17 +// packet (an address mask request). +// This query can be valuable when administrators specifically block echo +// request packets while forgetting that other ICMP queries can be used +// for the same purpose. +func WithICMPNetMaskDiscovery() Option { + return func(s *Scanner) { + s.args = append(s.args, "-PM") + } +} + +// WithIPProtocolPingDiscovery sets the discovery mode to use the IP +// protocol ping. +// If no protocols are specified, the default is to send multiple IP +// packets for ICMP (protocol 1), IGMP (protocol 2), and IP-in-IP +// (protocol 4). +func WithIPProtocolPingDiscovery(protocols ...string) Option { + protocolList := strings.Join(protocols, ",") + + return func(s *Scanner) { + s.args = append(s.args, fmt.Sprintf("-PO%s", protocolList)) + } +} + +// WithDisabledDNSResolution disables DNS resolution in the discovery +// step of the nmap scan. +func WithDisabledDNSResolution() Option { + return func(s *Scanner) { + s.args = append(s.args, "-n") + } +} + +// WithForcedDNSResolution enforces DNS resolution in the discovery +// step of the nmap scan. +func WithForcedDNSResolution() Option { + return func(s *Scanner) { + s.args = append(s.args, "-R") + } +} + +// WithCustomDNSServers sets custom DNS servers for the scan. +// List format: dns1[,dns2],... +func WithCustomDNSServers(dnsServers ...string) Option { + dnsList := strings.Join(dnsServers, ",") + + return func(s *Scanner) { + s.args = append(s.args, "--dns-servers") + s.args = append(s.args, dnsList) + } +} + +// WithSystemDNS sets the scanner's DNS to the system's DNS. +func WithSystemDNS() Option { + return func(s *Scanner) { + s.args = append(s.args, "--system-dns") + } +} + +// WithTraceRoute enables the tracing of the hop path to each host. +func WithTraceRoute() Option { + return func(s *Scanner) { + s.args = append(s.args, "--traceroute") + } +} diff --git a/optionsHostDiscovery_test.go b/optionsHostDiscovery_test.go new file mode 100644 index 0000000..8811099 --- /dev/null +++ b/optionsHostDiscovery_test.go @@ -0,0 +1,252 @@ +package nmap + +import ( + "context" + "reflect" + "testing" +) + +func TestHostDiscovery(t *testing.T) { + tests := []struct { + description string + + options []Option + + expectedArgs []string + }{ + { + description: "list targets to scan", + + options: []Option{ + WithListScan(), + }, + + expectedArgs: []string{ + "-sL", + }, + }, + { + description: "ping scan - disable port scan", + + options: []Option{ + WithPingScan(), + }, + + expectedArgs: []string{ + "-sn", + }, + }, + { + description: "skip host discovery", + + options: []Option{ + WithSkipHostDiscovery(), + }, + + expectedArgs: []string{ + "-Pn", + }, + }, + { + description: "TCP SYN packets for all ports", + + options: []Option{ + WithSYNDiscovery(), + }, + + expectedArgs: []string{ + "-PS", + }, + }, + { + description: "TCP SYN packets for specific ports", + + options: []Option{ + WithSYNDiscovery("443", "8443"), + }, + + expectedArgs: []string{ + "-PS443,8443", + }, + }, + { + description: "TCP ACK packets for all ports", + + options: []Option{ + WithACKDiscovery(), + }, + + expectedArgs: []string{ + "-PA", + }, + }, + { + description: "TCP ACK packets for specific ports", + + options: []Option{ + WithACKDiscovery("443", "8443"), + }, + + expectedArgs: []string{ + "-PA443,8443", + }, + }, + { + description: "UDP packets for all ports", + + options: []Option{ + WithUDPDiscovery(), + }, + + expectedArgs: []string{ + "-PU", + }, + }, + { + description: "UDP packets for specific ports", + + options: []Option{ + WithUDPDiscovery("443", "8443"), + }, + + expectedArgs: []string{ + "-PU443,8443", + }, + }, + { + description: "SCTP packets for all ports", + + options: []Option{ + WithSCTPDiscovery(), + }, + + expectedArgs: []string{ + "-PY", + }, + }, + { + description: "SCTP packets for specific ports", + + options: []Option{ + WithSCTPDiscovery("443", "8443"), + }, + + expectedArgs: []string{ + "-PY443,8443", + }, + }, + { + description: "ICMP echo request discovery probes", + + options: []Option{ + WithICMPEchoDiscovery(), + }, + + expectedArgs: []string{ + "-PE", + }, + }, + { + description: "ICMP Timestamp request discovery probes", + + options: []Option{ + WithICMPTimestampDiscovery(), + }, + + expectedArgs: []string{ + "-PP", + }, + }, + { + description: "ICMP NetMask request discovery probes", + + options: []Option{ + WithICMPNetMaskDiscovery(), + }, + + expectedArgs: []string{ + "-PM", + }, + }, + { + description: "IP protocol ping", + + options: []Option{ + WithIPProtocolPingDiscovery("1", "2", "4"), + }, + + expectedArgs: []string{ + "-PO1,2,4", + }, + }, + { + description: "disable DNS resolution during discovery", + + options: []Option{ + WithDisabledDNSResolution(), + }, + + expectedArgs: []string{ + "-n", + }, + }, + { + description: "enforce DNS resolution during discovery", + + options: []Option{ + WithForcedDNSResolution(), + }, + + expectedArgs: []string{ + "-R", + }, + }, + { + description: "custom DNS server", + + options: []Option{ + WithCustomDNSServers("8.8.8.8", "8.8.4.4"), + }, + + expectedArgs: []string{ + "--dns-servers", + "8.8.8.8,8.8.4.4", + }, + }, + { + description: "use system DNS", + + options: []Option{ + WithSystemDNS(), + }, + + expectedArgs: []string{ + "--system-dns", + }, + }, + { + description: "traceroute", + + options: []Option{ + WithTraceRoute(), + }, + + expectedArgs: []string{ + "--traceroute", + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + s, err := NewScanner(context.TODO(), test.options...) + if err != nil { + panic(err) + } + + if !reflect.DeepEqual(s.args, test.expectedArgs) { + t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) + } + }) + } +} diff --git a/optionsMisc.go b/optionsMisc.go new file mode 100644 index 0000000..c00171f --- /dev/null +++ b/optionsMisc.go @@ -0,0 +1,85 @@ +package nmap + +import "syscall" + +// WithIPv6Scanning enables the use of IPv6 scanning. +func WithIPv6Scanning() Option { + return func(s *Scanner) { + s.args = append(s.args, "-6") + } +} + +// WithAggressiveScan enables the use of aggressive scan options. This has +// the same effect as using WithOSDetection, WithServiceInfo, WithDefaultScript +// and WithTraceRoute at the same time. +// Because script scanning with the default set is considered intrusive, you +// should not use this method against target networks without permission. +func WithAggressiveScan() Option { + return func(s *Scanner) { + s.args = append(s.args, "-A") + } +} + +// WithDataDir specifies a custom data directory for nmap to get its +// nmap-service-probes, nmap-services, nmap-protocols, nmap-rpc, +// nmap-mac-prefixes, and nmap-os-db. +func WithDataDir(directoryPath string) Option { + return func(s *Scanner) { + s.args = append(s.args, "--datadir") + s.args = append(s.args, directoryPath) + } +} + +// WithSendEthernet makes nmap send packets at the raw ethernet (data link) +// layer rather than the higher IP (network) layer. By default, nmap chooses +// the one which is generally best for the platform it is running on. +func WithSendEthernet() Option { + return func(s *Scanner) { + s.args = append(s.args, "--send-eth") + } +} + +// WithSendIP makes nmap send packets via raw IP sockets rather than sending +// lower level ethernet frames. +func WithSendIP() Option { + return func(s *Scanner) { + s.args = append(s.args, "--send-ip") + } +} + +// WithPrivileged makes nmap assume that the user is fully privileged. +func WithPrivileged() Option { + return func(s *Scanner) { + s.args = append(s.args, "--privileged") + } +} + +// WithUnprivileged makes nmap assume that the user lacks raw socket privileges. +func WithUnprivileged() Option { + return func(s *Scanner) { + s.args = append(s.args, "--unprivileged") + } +} + +// WithNmapOutput makes nmap output standard output to the filename specified. +func WithNmapOutput(outputFileName string) Option { + return func(s *Scanner) { + s.args = append(s.args, "-oN") + s.args = append(s.args, outputFileName) + } +} + +// WithGrepOutput makes nmap output greppable output to the filename specified. +func WithGrepOutput(outputFileName string) Option { + return func(s *Scanner) { + s.args = append(s.args, "-oG") + s.args = append(s.args, outputFileName) + } +} + +// WithCustomSysProcAttr allows customizing the *syscall.SysProcAttr on the *exec.Cmd instance +func WithCustomSysProcAttr(f func(*syscall.SysProcAttr)) Option { + return func(s *Scanner) { + s.modifySysProcAttr = f + } +} diff --git a/optionsMisc_test.go b/optionsMisc_test.go new file mode 100644 index 0000000..e7300bc --- /dev/null +++ b/optionsMisc_test.go @@ -0,0 +1,109 @@ +package nmap + +import ( + "context" + "reflect" + "testing" +) + +func TestMiscellaneous(t *testing.T) { + tests := []struct { + description string + + options []Option + + expectedArgs []string + }{ + { + description: "enable ipv6 scanning", + + options: []Option{ + WithIPv6Scanning(), + }, + + expectedArgs: []string{ + "-6", + }, + }, + { + description: "enable aggressive scanning", + + options: []Option{ + WithAggressiveScan(), + }, + + expectedArgs: []string{ + "-A", + }, + }, + { + description: "set data dir", + + options: []Option{ + WithDataDir("/etc/nmap/data"), + }, + + expectedArgs: []string{ + "--datadir", + "/etc/nmap/data", + }, + }, + { + description: "send packets over ethernet", + + options: []Option{ + WithSendEthernet(), + }, + + expectedArgs: []string{ + "--send-eth", + }, + }, + { + description: "send packets over IP", + + options: []Option{ + WithSendIP(), + }, + + expectedArgs: []string{ + "--send-ip", + }, + }, + { + description: "assume user is privileged", + + options: []Option{ + WithPrivileged(), + }, + + expectedArgs: []string{ + "--privileged", + }, + }, + { + description: "assume user is unprivileged", + + options: []Option{ + WithUnprivileged(), + }, + + expectedArgs: []string{ + "--unprivileged", + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + s, err := NewScanner(context.TODO(), test.options...) + if err != nil { + panic(err) + } + + if !reflect.DeepEqual(s.args, test.expectedArgs) { + t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) + } + }) + } +} diff --git a/optionsOS.go b/optionsOS.go new file mode 100644 index 0000000..55b40db --- /dev/null +++ b/optionsOS.go @@ -0,0 +1,25 @@ +package nmap + +// WithOSDetection enables OS detection. +func WithOSDetection() Option { + return func(s *Scanner) { + s.args = append(s.args, "-O") + } +} + +// WithOSScanLimit sets the scanner to not even try OS detection against +// hosts that do have at least one open TCP port, as it is unlikely to be effective. +// This can save substantial time, particularly on -Pn scans against many hosts. +// It only matters when OS detection is requested with -O or -A. +func WithOSScanLimit() Option { + return func(s *Scanner) { + s.args = append(s.args, "--osscan-limit") + } +} + +// WithOSScanGuess makes nmap attempt to guess the OS more aggressively. +func WithOSScanGuess() Option { + return func(s *Scanner) { + s.args = append(s.args, "--osscan-guess") + } +} diff --git a/optionsOS_test.go b/optionsOS_test.go new file mode 100644 index 0000000..a8f1f2b --- /dev/null +++ b/optionsOS_test.go @@ -0,0 +1,64 @@ +package nmap + +import ( + "context" + "reflect" + "testing" +) + +func TestOSDetection(t *testing.T) { + tests := []struct { + description string + + options []Option + + expectedArgs []string + }{ + { + description: "enable OS detection", + + options: []Option{ + WithOSDetection(), + }, + + expectedArgs: []string{ + "-O", + }, + }, + { + description: "enable OS scan limit", + + options: []Option{ + WithOSScanLimit(), + }, + + expectedArgs: []string{ + "--osscan-limit", + }, + }, + { + description: "enable OS scan guess", + + options: []Option{ + WithOSScanGuess(), + }, + + expectedArgs: []string{ + "--osscan-guess", + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + s, err := NewScanner(context.TODO(), test.options...) + if err != nil { + panic(err) + } + + if !reflect.DeepEqual(s.args, test.expectedArgs) { + t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) + } + }) + } +} diff --git a/optionsOutput.go b/optionsOutput.go new file mode 100644 index 0000000..dac37d6 --- /dev/null +++ b/optionsOutput.go @@ -0,0 +1,87 @@ +package nmap + +import "fmt" + +// WithVerbosity sets and increases the verbosity level of nmap. +func WithVerbosity(level int) Option { + + return func(s *Scanner) { + if level < 0 || level > 10 { + panic("value given to nmap.WithVerbosity() should be between 0 and 10") + } + s.args = append(s.args, fmt.Sprintf("-v%d", level)) + } +} + +// WithDebugging sets and increases the debugging level of nmap. +func WithDebugging(level int) Option { + return func(s *Scanner) { + if level < 0 || level > 10 { + panic("value given to nmap.WithDebugging() should be between 0 and 10") + } + s.args = append(s.args, fmt.Sprintf("-d%d", level)) + } +} + +// WithReason makes nmap specify why a port is in a particular state. +func WithReason() Option { + return func(s *Scanner) { + s.args = append(s.args, "--reason") + } +} + +// WithOpenOnly makes nmap only show open ports. +func WithOpenOnly() Option { + return func(s *Scanner) { + s.args = append(s.args, "--open") + } +} + +// WithPacketTrace makes nmap show all packets sent and received. +func WithPacketTrace() Option { + return func(s *Scanner) { + s.args = append(s.args, "--packet-trace") + } +} + +// WithAppendOutput makes nmap append to files instead of overwriting them. +// Currently does nothing, since this library doesn't write in files. +func WithAppendOutput() Option { + return func(s *Scanner) { + s.args = append(s.args, "--append-output") + } +} + +// WithResumePreviousScan makes nmap continue a scan that was aborted, +// from an output file. +func WithResumePreviousScan(filePath string) Option { + return func(s *Scanner) { + s.args = append(s.args, "--resume") + s.args = append(s.args, filePath) + } +} + +// WithStylesheet makes nmap apply an XSL stylesheet to transform its +// XML output to HTML. +func WithStylesheet(stylesheetPath string) Option { + return func(s *Scanner) { + s.args = append(s.args, "--stylesheet") + s.args = append(s.args, stylesheetPath) + } +} + +// WithWebXML makes nmap apply the default nmap.org stylesheet to transform +// XML output to HTML. The stylesheet can be found at +// https://nmap.org/svn/docs/nmap.xsl +func WithWebXML() Option { + return func(s *Scanner) { + s.args = append(s.args, "--webxml") + } +} + +// WithNoStylesheet prevents the use of XSL stylesheets with the XML output. +func WithNoStylesheet() Option { + return func(s *Scanner) { + s.args = append(s.args, "--no-stylesheet") + } +} diff --git a/optionsOutput_test.go b/optionsOutput_test.go new file mode 100644 index 0000000..e9eab48 --- /dev/null +++ b/optionsOutput_test.go @@ -0,0 +1,155 @@ +package nmap + +import ( + "context" + "reflect" + "testing" +) + +func TestOutput(t *testing.T) { + tests := []struct { + description string + + options []Option + + expectedArgs []string + }{ + { + description: "set verbosity", + + options: []Option{ + WithVerbosity(5), + }, + + expectedArgs: []string{ + "-v5", + }, + }, + { + description: "set debugging", + + options: []Option{ + WithDebugging(3), + }, + + expectedArgs: []string{ + "-d3", + }, + }, + { + description: "display reason", + + options: []Option{ + WithReason(), + }, + + expectedArgs: []string{ + "--reason", + }, + }, + { + description: "show only open ports", + + options: []Option{ + WithOpenOnly(), + }, + + expectedArgs: []string{ + "--open", + }, + }, + { + description: "enable packet trace", + + options: []Option{ + WithPacketTrace(), + }, + + expectedArgs: []string{ + "--packet-trace", + }, + }, + { + description: "enable appending output", + + options: []Option{ + WithAppendOutput(), + }, + + expectedArgs: []string{ + "--append-output", + }, + }, + { + description: "resume scan from file", + + options: []Option{ + WithResumePreviousScan("/nmap_scan.xml"), + }, + + expectedArgs: []string{ + "--resume", + "/nmap_scan.xml", + }, + }, + { + description: "use stylesheet from file", + + options: []Option{ + WithStylesheet("/nmap_stylesheet.xsl"), + }, + + expectedArgs: []string{ + "--stylesheet", + "/nmap_stylesheet.xsl", + }, + }, + { + description: "use stylesheet from file", + + options: []Option{ + WithStylesheet("/nmap_stylesheet.xsl"), + }, + + expectedArgs: []string{ + "--stylesheet", + "/nmap_stylesheet.xsl", + }, + }, + { + description: "use default nmap stylesheet", + + options: []Option{ + WithWebXML(), + }, + + expectedArgs: []string{ + "--webxml", + }, + }, + { + description: "disable stylesheets", + + options: []Option{ + WithNoStylesheet(), + }, + + expectedArgs: []string{ + "--no-stylesheet", + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + s, err := NewScanner(context.TODO(), test.options...) + if err != nil { + panic(err) + } + + if !reflect.DeepEqual(s.args, test.expectedArgs) { + t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) + } + }) + } +} diff --git a/optionsPortOrder.go b/optionsPortOrder.go new file mode 100644 index 0000000..fe52093 --- /dev/null +++ b/optionsPortOrder.go @@ -0,0 +1,82 @@ +package nmap + +import ( + "fmt" + "strings" +) + +// WithPorts sets the ports which the scanner should scan on each host. +func WithPorts(ports ...string) Option { + portList := strings.Join(ports, ",") + + return func(s *Scanner) { + // Find if any port is set. + var place = -1 + for p, value := range s.args { + if value == "-p" { + place = p + break + } + } + + // Add ports. + if place >= 0 { + if len(s.args)-1 == place { + s.args = append(s.args, "") + } else { + portList = s.args[place+1] + "," + portList + } + s.args[place+1] = portList + } else { + s.args = append(s.args, "-p") + s.args = append(s.args, portList) + } + } +} + +// WithPortExclusions sets the ports that the scanner should not scan on each host. +func WithPortExclusions(ports ...string) Option { + portList := strings.Join(ports, ",") + + return func(s *Scanner) { + s.args = append(s.args, "--exclude-ports") + s.args = append(s.args, portList) + } +} + +// WithFastMode makes the scan faster by scanning fewer ports than the default scan. +func WithFastMode() Option { + return func(s *Scanner) { + s.args = append(s.args, "-F") + } +} + +// WithConsecutivePortScanning makes the scan go through ports consecutively instead of +// picking them out randomly. +func WithConsecutivePortScanning() Option { + return func(s *Scanner) { + s.args = append(s.args, "-r") + } +} + +// WithMostCommonPorts sets the scanner to go through the provided number of most +// common ports. +func WithMostCommonPorts(number int) Option { + return func(s *Scanner) { + s.args = append(s.args, "--top-ports") + s.args = append(s.args, fmt.Sprint(number)) + } +} + +// WithPortRatio sets the scanner to go the ports more common than the given ratio. +// Ratio must be a float between 0 and 1. +func WithPortRatio(ratio float32) Option { + return func(s *Scanner) { + if ratio < 0 || ratio > 1 { + panic("value given to nmap.WithPortRatio() should be between 0 and 1") + } + + s.args = append(s.args, "--port-ratio") + s.args = append(s.args, fmt.Sprintf("%.1f", ratio)) + } +} diff --git a/optionsPortOrder_test.go b/optionsPortOrder_test.go new file mode 100644 index 0000000..f90e5db --- /dev/null +++ b/optionsPortOrder_test.go @@ -0,0 +1,122 @@ +package nmap + +import ( + "context" + "reflect" + "testing" +) + +func TestPortSpecAndScanOrder(t *testing.T) { + tests := []struct { + description string + + options []Option + + expectedPanic string + expectedArgs []string + }{ + { + description: "specify ports to scan", + + options: []Option{ + WithPorts("554", "8554"), + WithPorts("80-81"), + }, + + expectedArgs: []string{ + "-p", + "554,8554,80-81", + }, + }, + { + description: "exclude ports to scan", + + options: []Option{ + WithPortExclusions("554", "8554"), + }, + + expectedArgs: []string{ + "--exclude-ports", + "554,8554", + }, + }, + { + description: "fast mode - scan fewer ports than the default scan", + + options: []Option{ + WithFastMode(), + }, + + expectedArgs: []string{ + "-F", + }, + }, + { + description: "consecutive port scanning", + + options: []Option{ + WithConsecutivePortScanning(), + }, + + expectedArgs: []string{ + "-r", + }, + }, + { + description: "scan most commonly open ports", + + options: []Option{ + WithMostCommonPorts(5), + }, + + expectedArgs: []string{ + "--top-ports", + "5", + }, + }, + { + description: "scan most commonly open ports given a ratio - should be rounded to 0.4", + + options: []Option{ + WithPortRatio(0.42010101), + }, + + expectedArgs: []string{ + "--port-ratio", + "0.4", + }, + }, + { + description: "scan most commonly open ports given a ratio - should be invalid and panic", + + options: []Option{ + WithPortRatio(2), + }, + + expectedPanic: "value given to nmap.WithPortRatio() should be between 0 and 1", + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + if test.expectedPanic != "" { + defer func() { + recoveredMessage := recover() + + if recoveredMessage != test.expectedPanic { + t.Errorf("expected panic message to be %q but got %q", test.expectedPanic, recoveredMessage) + } + }() + } + + s, err := NewScanner(context.TODO(), test.options...) + if err != nil { + panic(err) + } + + if !reflect.DeepEqual(s.args, test.expectedArgs) { + t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) + } + }) + } +} diff --git a/optionsScanTechniques.go b/optionsScanTechniques.go new file mode 100644 index 0000000..0a93d6f --- /dev/null +++ b/optionsScanTechniques.go @@ -0,0 +1,193 @@ +package nmap + +import "fmt" + +// WithSYNScan sets the scan technique to use SYN packets over TCP. +// This is the default method, as it is fast, stealthy and not +// hampered by restrictive firewalls. +func WithSYNScan() Option { + return func(s *Scanner) { + s.args = append(s.args, "-sS") + } +} + +// WithConnectScan sets the scan technique to use TCP connections. +// This is the default method used when a user does not have raw +// packet privileges. Target machines are likely to log these +// connections. +func WithConnectScan() Option { + return func(s *Scanner) { + s.args = append(s.args, "-sT") + } +} + +// WithACKScan sets the scan technique to use ACK packets over TCP. +// This scan is unable to determine if a port is open. +// When scanning unfiltered systems, open and closed ports will both +// return a RST packet. +// Nmap then labels them as unfiltered, meaning that they are reachable +// by the ACK packet, but whether they are open or closed is undetermined. +func WithACKScan() Option { + return func(s *Scanner) { + s.args = append(s.args, "-sA") + } +} + +// WithWindowScan sets the scan technique to use ACK packets over TCP and +// examining the TCP window field of the RST packets returned. +// Window scan is exactly the same as ACK scan except that it exploits +// an implementation detail of certain systems to differentiate open ports +// from closed ones, rather than always printing unfiltered when a RST +// is returned. +func WithWindowScan() Option { + return func(s *Scanner) { + s.args = append(s.args, "-sW") + } +} + +// WithMaimonScan sends the same packets as NULL, FIN, and Xmas scans, +// except that the probe is FIN/ACK. Many BSD-derived systems will drop +// these packets if the port is open. +func WithMaimonScan() Option { + return func(s *Scanner) { + s.args = append(s.args, "-sM") + } +} + +// WithUDPScan sets the scan technique to use UDP packets. +// It can be combined with a TCP scan type such as SYN scan +// to check both protocols during the same run. +// UDP scanning is generally slower than TCP, but should not +// be ignored. +func WithUDPScan() Option { + return func(s *Scanner) { + s.args = append(s.args, "-sU") + } +} + +// WithTCPNullScan sets the scan technique to use TCP null packets. +// (TCP flag header is 0). This scan method can be used to exploit +// a loophole in the TCP RFC. +// If an RST packet is received, the port is considered closed, +// while no response means it is open|filtered. +func WithTCPNullScan() Option { + return func(s *Scanner) { + s.args = append(s.args, "-sN") + } +} + +// WithTCPFINScan sets the scan technique to use TCP packets with +// the FIN flag set. +// This scan method can be used to exploit a loophole in the TCP RFC. +// If an RST packet is received, the port is considered closed, +// while no response means it is open|filtered. +func WithTCPFINScan() Option { + return func(s *Scanner) { + s.args = append(s.args, "-sF") + } +} + +// WithTCPXmasScan sets the scan technique to use TCP packets with +// the FIN, PSH and URG flags set. +// This scan method can be used to exploit a loophole in the TCP RFC. +// If an RST packet is received, the port is considered closed, +// while no response means it is open|filtered. +func WithTCPXmasScan() Option { + return func(s *Scanner) { + s.args = append(s.args, "-sX") + } +} + +// TCPFlag represents a TCP flag. +type TCPFlag int + +// Flag enumerations. +const ( + FlagNULL TCPFlag = 0 + FlagFIN TCPFlag = 1 + FlagSYN TCPFlag = 2 + FlagRST TCPFlag = 4 + FlagPSH TCPFlag = 8 + FlagACK TCPFlag = 16 + FlagURG TCPFlag = 32 + FlagECE TCPFlag = 64 + FlagCWR TCPFlag = 128 + FlagNS TCPFlag = 256 +) + +// WithTCPScanFlags sets the scan technique to use custom TCP flags. +func WithTCPScanFlags(flags ...TCPFlag) Option { + var total int + for _, flag := range flags { + total += int(flag) + } + + return func(s *Scanner) { + s.args = append(s.args, "--scanflags") + s.args = append(s.args, fmt.Sprintf("%x", total)) + } +} + +// WithIdleScan sets the scan technique to use a zombie host to +// allow for a truly blind TCP port scan of the target. +// Besides being extraordinarily stealthy (due to its blind nature), +// this scan type permits mapping out IP-based trust relationships +// between machines. +func WithIdleScan(zombieHost string, probePort int) Option { + return func(s *Scanner) { + s.args = append(s.args, "-sI") + + if probePort != 0 { + s.args = append(s.args, fmt.Sprintf("%s:%d", zombieHost, probePort)) + } else { + s.args = append(s.args, zombieHost) + } + } +} + +// WithSCTPInitScan sets the scan technique to use SCTP packets +// containing an INIT chunk. +// It can be performed quickly, scanning thousands of ports per +// second on a fast network not hampered by restrictive firewalls. +// Like SYN scan, INIT scan is relatively unobtrusive and stealthy, +// since it never completes SCTP associations. +func WithSCTPInitScan() Option { + return func(s *Scanner) { + s.args = append(s.args, "-sY") + } +} + +// WithSCTPCookieEchoScan sets the scan technique to use SCTP packets +// containing a COOKIE-ECHO chunk. +// The advantage of this scan type is that it is not as obvious a port +// scan than an INIT scan. Also, there may be non-stateful firewall +// rulesets blocking INIT chunks, but not COOKIE ECHO chunks. +func WithSCTPCookieEchoScan() Option { + return func(s *Scanner) { + s.args = append(s.args, "-sZ") + } +} + +// WithIPProtocolScan sets the scan technique to use the IP protocol. +// IP protocol scan allows you to determine which IP protocols +// (TCP, ICMP, IGMP, etc.) are supported by target machines. This isn't +// technically a port scan, since it cycles through IP protocol numbers +// rather than TCP or UDP port numbers. +func WithIPProtocolScan() Option { + return func(s *Scanner) { + s.args = append(s.args, "-sO") + } +} + +// WithFTPBounceScan sets the scan technique to use the an FTP relay host. +// It takes an argument of the form ":@:. ". +// You may omit :, in which case anonymous login credentials +// (user: anonymous password:-wwwuser@) are used. +// The port number (and preceding colon) may be omitted as well, in which case the +// default FTP port (21) on is used. +func WithFTPBounceScan(FTPRelayHost string) Option { + return func(s *Scanner) { + s.args = append(s.args, "-b") + s.args = append(s.args, FTPRelayHost) + } +} diff --git a/optionsScanTechniques_test.go b/optionsScanTechniques_test.go new file mode 100644 index 0000000..0d6f9b5 --- /dev/null +++ b/optionsScanTechniques_test.go @@ -0,0 +1,211 @@ +package nmap + +import ( + "context" + "reflect" + "testing" +) + +func TestScanTechniques(t *testing.T) { + tests := []struct { + description string + + options []Option + + expectedArgs []string + }{ + { + description: "TCP SYN scan", + + options: []Option{ + WithSYNScan(), + }, + + expectedArgs: []string{ + "-sS", + }, + }, + { + description: "TCP Connect() scan", + + options: []Option{ + WithConnectScan(), + }, + + expectedArgs: []string{ + "-sT", + }, + }, + { + description: "TCP ACK scan", + + options: []Option{ + WithACKScan(), + }, + + expectedArgs: []string{ + "-sA", + }, + }, + { + description: "TCP Window scan", + + options: []Option{ + WithWindowScan(), + }, + + expectedArgs: []string{ + "-sW", + }, + }, + { + description: "Maimon scan", + + options: []Option{ + WithMaimonScan(), + }, + + expectedArgs: []string{ + "-sM", + }, + }, + { + description: "UDP scan", + + options: []Option{ + WithUDPScan(), + }, + + expectedArgs: []string{ + "-sU", + }, + }, + { + description: "TCP Null scan", + + options: []Option{ + WithTCPNullScan(), + }, + + expectedArgs: []string{ + "-sN", + }, + }, + { + description: "TCP FIN scan", + + options: []Option{ + WithTCPFINScan(), + }, + + expectedArgs: []string{ + "-sF", + }, + }, + { + description: "TCP Xmas scan", + + options: []Option{ + WithTCPXmasScan(), + }, + + expectedArgs: []string{ + "-sX", + }, + }, + { + description: "TCP custom scan flags", + + options: []Option{ + WithTCPScanFlags(FlagACK, FlagFIN, FlagNULL), + }, + + expectedArgs: []string{ + "--scanflags", + "11", + }, + }, + { + description: "idle scan through zombie host with probe port specified", + + options: []Option{ + WithIdleScan("192.168.1.1", 61436), + }, + + expectedArgs: []string{ + "-sI", + "192.168.1.1:61436", + }, + }, + { + description: "idle scan through zombie host without probe port specified", + + options: []Option{ + WithIdleScan("192.168.1.1", 0), + }, + + expectedArgs: []string{ + "-sI", + "192.168.1.1", + }, + }, + { + description: "SCTP INIT scan", + + options: []Option{ + WithSCTPInitScan(), + }, + + expectedArgs: []string{ + "-sY", + }, + }, + { + description: "SCTP COOKIE-ECHO scan", + + options: []Option{ + WithSCTPCookieEchoScan(), + }, + + expectedArgs: []string{ + "-sZ", + }, + }, + { + description: "IP protocol scan", + + options: []Option{ + WithIPProtocolScan(), + }, + + expectedArgs: []string{ + "-sO", + }, + }, + { + description: "FTP bounce scan", + + options: []Option{ + WithFTPBounceScan("192.168.0.254"), + }, + + expectedArgs: []string{ + "-b", + "192.168.0.254", + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + s, err := NewScanner(context.TODO(), test.options...) + if err != nil { + panic(err) + } + + if !reflect.DeepEqual(s.args, test.expectedArgs) { + t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) + } + }) + } +} diff --git a/optionsScriptScan.go b/optionsScriptScan.go new file mode 100644 index 0000000..5f19e02 --- /dev/null +++ b/optionsScriptScan.go @@ -0,0 +1,83 @@ +package nmap + +import ( + "fmt" + "strings" + "time" +) + +// WithDefaultScript sets the scanner to perform a script scan using the default +// set of scripts. It is equivalent to --script=default. Some of the scripts in +// this category are considered intrusive and should not be run against a target +// network without permission. +func WithDefaultScript() Option { + return func(s *Scanner) { + s.args = append(s.args, "-sC") + } +} + +// WithScripts sets the scanner to perform a script scan using the enumerated +// scripts, script directories and script categories. +func WithScripts(scripts ...string) Option { + scriptList := strings.Join(scripts, ",") + + return func(s *Scanner) { + s.args = append(s.args, fmt.Sprintf("--script=%s", scriptList)) + } +} + +// WithScriptArguments provides arguments for scripts. If a value is the empty string, the key will be used as a flag. +func WithScriptArguments(arguments map[string]string) Option { + var argList string + + // Properly format the argument list from the map. + // Complex example: + // user=foo,pass=",{}=bar",whois={whodb=nofollow+ripe},xmpp-info.server_name=localhost,vulns.showall + for key, value := range arguments { + str := "" + if value == "" { + str = key + } else { + str = fmt.Sprintf("%s=%s", key, value) + } + + argList = strings.Join([]string{argList, str}, ",") + } + + argList = strings.TrimLeft(argList, ",") + + return func(s *Scanner) { + s.args = append(s.args, fmt.Sprintf("--script-args=%s", argList)) + } +} + +// WithScriptArgumentsFile provides arguments for scripts from a file. +func WithScriptArgumentsFile(inputFilePath string) Option { + return func(s *Scanner) { + s.args = append(s.args, fmt.Sprintf("--script-args-file=%s", inputFilePath)) + } +} + +// WithScriptTrace makes the scripts show all data sent and received. +func WithScriptTrace() Option { + return func(s *Scanner) { + s.args = append(s.args, "--script-trace") + } +} + +// WithScriptUpdateDB updates the script database. +func WithScriptUpdateDB() Option { + return func(s *Scanner) { + s.args = append(s.args, "--script-updatedb") + } +} + +// WithScriptTimeout sets the script timeout. +func WithScriptTimeout(timeout time.Duration) Option { + milliseconds := timeout.Round(time.Nanosecond).Nanoseconds() / 1000000 + + return func(s *Scanner) { + s.args = append(s.args, "--script-timeout") + s.args = append(s.args, fmt.Sprintf("%dms", int(milliseconds))) + } +} diff --git a/optionsScriptScan_test.go b/optionsScriptScan_test.go new file mode 100644 index 0000000..308f28c --- /dev/null +++ b/optionsScriptScan_test.go @@ -0,0 +1,135 @@ +package nmap + +import ( + "context" + "reflect" + "strings" + "testing" + "time" +) + +func TestScriptScan(t *testing.T) { + tests := []struct { + description string + + targets []string + options []Option + unorderedArgs bool + + expectedArgs []string + }{ + { + description: "default script scan", + + options: []Option{ + WithDefaultScript(), + }, + + expectedArgs: []string{ + "-sC", + }, + }, + { + description: "custom script list", + + options: []Option{ + WithScripts("./scripts/", "/etc/nmap/nse/scripts"), + }, + + expectedArgs: []string{ + "--script=./scripts/,/etc/nmap/nse/scripts", + }, + }, + { + description: "script arguments", + + options: []Option{ + WithScriptArguments(map[string]string{ + "user": "foo", + "pass": "\",{}=bar\"", + "whois": "{whodb=nofollow+ripe}", + "xmpp-info.server_name": "localhost", + "vulns.showall": "", + }), + }, + + unorderedArgs: true, + + expectedArgs: []string{ + "--script-args=", + "user=foo", + "pass=\",{}=bar\"", + "whois={whodb=nofollow+ripe}", + "xmpp-info.server_name=localhost", + "vulns.showall", + }, + }, + { + description: "script arguments file", + + options: []Option{ + WithScriptArgumentsFile("/script_args.txt"), + }, + + expectedArgs: []string{ + "--script-args-file=/script_args.txt", + }, + }, + { + description: "enable script trace", + + options: []Option{ + WithScriptTrace(), + }, + + expectedArgs: []string{ + "--script-trace", + }, + }, + { + description: "update script database", + + options: []Option{ + WithScriptUpdateDB(), + }, + + expectedArgs: []string{ + "--script-updatedb", + }, + }, + { + description: "set script timeout", + + options: []Option{ + WithScriptTimeout(40 * time.Second), + }, + + expectedArgs: []string{ + "--script-timeout", + "40000ms", + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + s, err := NewScanner(context.TODO(), test.options...) + if err != nil { + panic(err) + } + + if test.unorderedArgs { + for _, expectedArg := range test.expectedArgs { + if !strings.Contains(s.args[0], expectedArg) { + t.Errorf("missing argument %s in %v", expectedArg, s.args) + } + } + return + } + + if !reflect.DeepEqual(s.args, test.expectedArgs) { + t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) + } + }) + } +} diff --git a/optionsServiceVersion.go b/optionsServiceVersion.go new file mode 100644 index 0000000..bb4d6f5 --- /dev/null +++ b/optionsServiceVersion.go @@ -0,0 +1,53 @@ +package nmap + +import "fmt" + +// WithServiceInfo enables the probing of open ports to determine service and version +// info. +func WithServiceInfo() Option { + return func(s *Scanner) { + s.args = append(s.args, "-sV") + } +} + +// WithVersionIntensity sets the level of intensity with which nmap should +// probe the open ports to get version information. +// Intensity should be a value between 0 (light) and 9 (try all probes). The +// default value is 7. +func WithVersionIntensity(intensity int16) Option { + return func(s *Scanner) { + if intensity < 0 || intensity > 9 { + panic("value given to nmap.WithVersionIntensity() should be between 0 and 9") + } + + s.args = append(s.args, "--version-intensity") + s.args = append(s.args, fmt.Sprint(intensity)) + } +} + +// WithVersionLight sets the level of intensity with which nmap should probe the +// open ports to get version information to 2. This will make version scanning much +// faster, but slightly less likely to identify services. +func WithVersionLight() Option { + return func(s *Scanner) { + s.args = append(s.args, "--version-light") + } +} + +// WithVersionAll sets the level of intensity with which nmap should probe the +// open ports to get version information to 9. This will ensure that every single +// probe is attempted against each port. +func WithVersionAll() Option { + return func(s *Scanner) { + s.args = append(s.args, "--version-all") + } +} + +// WithVersionTrace causes Nmap to print out extensive debugging info about what +// version scanning is doing. +// TODO: See how this works along with XML output. +func WithVersionTrace() Option { + return func(s *Scanner) { + s.args = append(s.args, "--version-trace") + } +} diff --git a/optionsServiceVersion_test.go b/optionsServiceVersion_test.go new file mode 100644 index 0000000..67c9af9 --- /dev/null +++ b/optionsServiceVersion_test.go @@ -0,0 +1,107 @@ +package nmap + +import ( + "context" + "reflect" + "testing" +) + +func TestServiceDetection(t *testing.T) { + tests := []struct { + description string + + options []Option + + expectedPanic string + expectedArgs []string + }{ + { + description: "service detection", + + options: []Option{ + WithServiceInfo(), + }, + + expectedArgs: []string{ + "-sV", + }, + }, + { + description: "service detection custom intensity", + + options: []Option{ + WithVersionIntensity(1), + }, + + expectedArgs: []string{ + "--version-intensity", + "1", + }, + }, + { + description: "service detection custom intensity - should panic since not between 0 and 9", + + options: []Option{ + WithVersionIntensity(42), + }, + + expectedPanic: "value given to nmap.WithVersionIntensity() should be between 0 and 9", + }, + { + description: "service detection light intensity", + + options: []Option{ + WithVersionLight(), + }, + + expectedArgs: []string{ + "--version-light", + }, + }, + { + description: "service detection highest intensity", + + options: []Option{ + WithVersionAll(), + }, + + expectedArgs: []string{ + "--version-all", + }, + }, + { + description: "service detection enable trace", + + options: []Option{ + WithVersionTrace(), + }, + + expectedArgs: []string{ + "--version-trace", + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + if test.expectedPanic != "" { + defer func() { + recoveredMessage := recover() + + if recoveredMessage != test.expectedPanic { + t.Errorf("expected panic message to be %q but got %q", test.expectedPanic, recoveredMessage) + } + }() + } + + s, err := NewScanner(context.TODO(), test.options...) + if err != nil { + panic(err) + } + + if !reflect.DeepEqual(s.args, test.expectedArgs) { + t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) + } + }) + } +} diff --git a/optionsTargetSpecification.go b/optionsTargetSpecification.go new file mode 100644 index 0000000..190214c --- /dev/null +++ b/optionsTargetSpecification.go @@ -0,0 +1,55 @@ +package nmap + +import ( + "fmt" +) + +// WithTargets sets the target of a scanner. +func WithTargets(targets ...string) Option { + return func(s *Scanner) { + s.args = append(s.args, targets...) + } +} + +// WithTargetExclusion sets the excluded targets of a scanner. +func WithTargetExclusion(target string) Option { + return func(s *Scanner) { + s.args = append(s.args, "--exclude") + s.args = append(s.args, target) + } +} + +// WithTargetInput sets the input file name to set the targets. +func WithTargetInput(inputFileName string) Option { + return func(s *Scanner) { + s.args = append(s.args, "-iL") + s.args = append(s.args, inputFileName) + } +} + +// WithTargetExclusionInput sets the input file name to set the target exclusions. +func WithTargetExclusionInput(inputFileName string) Option { + return func(s *Scanner) { + s.args = append(s.args, "--excludefile") + s.args = append(s.args, inputFileName) + } +} + +// WithRandomTargets sets the amount of targets to randomly choose from the targets. +func WithRandomTargets(randomTargets int) Option { + return func(s *Scanner) { + s.args = append(s.args, "-iR") + s.args = append(s.args, fmt.Sprint(randomTargets)) + } +} + +// WithUnique makes each address be scanned only once. +// The default behavior is to scan each address as many times +// as it is specified in the target list, such as when network +// ranges overlap or different hostnames resolve to the same +// address. +func WithUnique() Option { + return func(s *Scanner) { + s.args = append(s.args, "--unique") + } +} diff --git a/optionsTargetSpecification_test.go b/optionsTargetSpecification_test.go new file mode 100644 index 0000000..ca49462 --- /dev/null +++ b/optionsTargetSpecification_test.go @@ -0,0 +1,126 @@ +package nmap + +import ( + "context" + "reflect" + "testing" +) + +func TestTargetSpecification(t *testing.T) { + tests := []struct { + description string + + options []Option + + expectedArgs []string + }{ + { + description: "custom arguments", + + options: []Option{ + WithTargets("0.0.0.0/24"), + WithCustomArguments("--invalid-argument"), + }, + + expectedArgs: []string{ + "0.0.0.0/24", + "--invalid-argument", + }, + }, + { + description: "set target", + + options: []Option{ + WithTargets("0.0.0.0/24"), + }, + + expectedArgs: []string{ + "0.0.0.0/24", + }, + }, + { + description: "set multiple targets", + + options: []Option{ + WithTargets("0.0.0.0", "192.168.1.1"), + }, + + expectedArgs: []string{ + "0.0.0.0", + "192.168.1.1", + }, + }, + { + description: "set target from file", + + options: []Option{ + WithTargetInput("/targets.txt"), + }, + + expectedArgs: []string{ + "-iL", + "/targets.txt", + }, + }, + { + description: "choose random targets", + + options: []Option{ + WithRandomTargets(4), + }, + + expectedArgs: []string{ + "-iR", + "4", + }, + }, + { + description: "unique addresses", + + options: []Option{ + WithUnique(), + }, + + expectedArgs: []string{ + "--unique", + }, + }, + { + description: "target exclusion", + + options: []Option{ + WithTargetExclusion("192.168.0.1,172.16.100.0/24"), + }, + + expectedArgs: []string{ + "--exclude", + "192.168.0.1,172.16.100.0/24", + }, + }, + { + description: "target exclusion from file", + + options: []Option{ + WithTargetExclusionInput("/exclude_targets.txt"), + }, + + expectedArgs: []string{ + "--excludefile", + "/exclude_targets.txt", + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + s, err := NewScanner(context.TODO(), test.options...) + if err != nil { + panic(err) + } + + if !reflect.DeepEqual(s.args, test.expectedArgs) { + t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) + } + }) + } +} diff --git a/optionsTimingPerformance.go b/optionsTimingPerformance.go new file mode 100644 index 0000000..c29b533 --- /dev/null +++ b/optionsTimingPerformance.go @@ -0,0 +1,156 @@ +package nmap + +import ( + "fmt" + "time" +) + +// Timing represents a timing template for nmap. +// These are meant to be used with the WithTimingTemplate method. +type Timing int16 + +const ( + // TimingSlowest also called paranoiac NO PARALLELISM | 5min timeout | 100ms to 10s round-trip time timeout | 5mn scan delay + TimingSlowest Timing = 0 + // TimingSneaky NO PARALLELISM | 15sec timeout | 100ms to 10s round-trip time timeout | 15s scan delay + TimingSneaky Timing = 1 + // TimingPolite NO PARALLELISM | 1sec timeout | 100ms to 10s round-trip time timeout | 400ms scan delay + TimingPolite Timing = 2 + // TimingNormal PARALLELISM | 1sec timeout | 100ms to 10s round-trip time timeout | 0s scan delay + TimingNormal Timing = 3 + // TimingAggressive PARALLELISM | 500ms timeout | 100ms to 1250ms round-trip time timeout | 0s scan delay + TimingAggressive Timing = 4 + // TimingFastest also called insane PARALLELISM | 250ms timeout | 50ms to 300ms round-trip time timeout | 0s scan delay + TimingFastest Timing = 5 +) + +// WithTimingTemplate sets the timing template for nmap. +func WithTimingTemplate(timing Timing) Option { + return func(s *Scanner) { + s.args = append(s.args, fmt.Sprintf("-T%d", timing)) + } +} + +// WithStatsEvery periodically prints a timing status message after each interval of time. +func WithStatsEvery(interval string) Option { + return func(s *Scanner) { + s.args = append(s.args, "--stats-every") + s.args = append(s.args, interval) + } +} + +// WithMinHostgroup sets the minimal parallel host scan group size. +func WithMinHostgroup(size int) Option { + return func(s *Scanner) { + s.args = append(s.args, "--min-hostgroup") + s.args = append(s.args, fmt.Sprint(size)) + } +} + +// WithMaxHostgroup sets the maximal parallel host scan group size. +func WithMaxHostgroup(size int) Option { + return func(s *Scanner) { + s.args = append(s.args, "--max-hostgroup") + s.args = append(s.args, fmt.Sprint(size)) + } +} + +// WithMinParallelism sets the minimal number of parallel probes. +func WithMinParallelism(probes int) Option { + return func(s *Scanner) { + s.args = append(s.args, "--min-parallelism") + s.args = append(s.args, fmt.Sprint(probes)) + } +} + +// WithMaxParallelism sets the maximal number of parallel probes. +func WithMaxParallelism(probes int) Option { + return func(s *Scanner) { + s.args = append(s.args, "--max-parallelism") + s.args = append(s.args, fmt.Sprint(probes)) + } +} + +// WithMinRTTTimeout sets the minimal probe round trip time. +func WithMinRTTTimeout(roundTripTime time.Duration) Option { + milliseconds := roundTripTime.Round(time.Nanosecond).Nanoseconds() / 1000000 + + return func(s *Scanner) { + s.args = append(s.args, "--min-rtt-timeout") + s.args = append(s.args, fmt.Sprintf("%dms", int(milliseconds))) + } +} + +// WithMaxRTTTimeout sets the maximal probe round trip time. +func WithMaxRTTTimeout(roundTripTime time.Duration) Option { + milliseconds := roundTripTime.Round(time.Nanosecond).Nanoseconds() / 1000000 + + return func(s *Scanner) { + s.args = append(s.args, "--max-rtt-timeout") + s.args = append(s.args, fmt.Sprintf("%dms", int(milliseconds))) + } +} + +// WithInitialRTTTimeout sets the initial probe round trip time. +func WithInitialRTTTimeout(roundTripTime time.Duration) Option { + milliseconds := roundTripTime.Round(time.Nanosecond).Nanoseconds() / 1000000 + + return func(s *Scanner) { + s.args = append(s.args, "--initial-rtt-timeout") + s.args = append(s.args, fmt.Sprintf("%dms", int(milliseconds))) + } +} + +// WithMaxRetries sets the maximal number of port scan probe retransmissions. +func WithMaxRetries(tries int) Option { + return func(s *Scanner) { + s.args = append(s.args, "--max-retries") + s.args = append(s.args, fmt.Sprint(tries)) + } +} + +// WithHostTimeout sets the time after which nmap should give up on a target host. +func WithHostTimeout(timeout time.Duration) Option { + milliseconds := timeout.Round(time.Nanosecond).Nanoseconds() / 1000000 + + return func(s *Scanner) { + s.args = append(s.args, "--host-timeout") + s.args = append(s.args, fmt.Sprintf("%dms", int(milliseconds))) + } +} + +// WithScanDelay sets the minimum time to wait between each probe sent to a host. +func WithScanDelay(timeout time.Duration) Option { + milliseconds := timeout.Round(time.Nanosecond).Nanoseconds() / 1000000 + + return func(s *Scanner) { + s.args = append(s.args, "--scan-delay") + s.args = append(s.args, fmt.Sprintf("%dms", int(milliseconds))) + } +} + +// WithMaxScanDelay sets the maximum time to wait between each probe sent to a host. +func WithMaxScanDelay(timeout time.Duration) Option { + milliseconds := timeout.Round(time.Nanosecond).Nanoseconds() / 1000000 + + return func(s *Scanner) { + s.args = append(s.args, "--max-scan-delay") + s.args = append(s.args, fmt.Sprintf("%dms", int(milliseconds))) + } +} + +// WithMinRate sets the minimal number of packets sent per second. +func WithMinRate(packetsPerSecond int) Option { + return func(s *Scanner) { + s.args = append(s.args, "--min-rate") + s.args = append(s.args, fmt.Sprint(packetsPerSecond)) + } +} + +// WithMaxRate sets the maximal number of packets sent per second. +func WithMaxRate(packetsPerSecond int) Option { + return func(s *Scanner) { + s.args = append(s.args, "--max-rate") + s.args = append(s.args, fmt.Sprint(packetsPerSecond)) + } +} diff --git a/optionsTimingPerformance_test.go b/optionsTimingPerformance_test.go new file mode 100644 index 0000000..8b53321 --- /dev/null +++ b/optionsTimingPerformance_test.go @@ -0,0 +1,211 @@ +package nmap + +import ( + "context" + "reflect" + "testing" + "time" +) + +func TestTimingAndPerformance(t *testing.T) { + tests := []struct { + description string + + options []Option + + expectedArgs []string + }{ + { + description: "set timing template", + + options: []Option{ + WithTimingTemplate(TimingAggressive), + }, + + expectedArgs: []string{ + "-T4", + }, + }, + { + description: "set stats every", + + options: []Option{ + WithStatsEvery("5s"), + }, + + expectedArgs: []string{ + "--stats-every", + "5s", + }, + }, + { + description: "set min hostgroup", + + options: []Option{ + WithMinHostgroup(42), + }, + + expectedArgs: []string{ + "--min-hostgroup", + "42", + }, + }, + { + description: "set max hostgroup", + + options: []Option{ + WithMaxHostgroup(42), + }, + + expectedArgs: []string{ + "--max-hostgroup", + "42", + }, + }, + { + description: "set min parallelism", + + options: []Option{ + WithMinParallelism(42), + }, + + expectedArgs: []string{ + "--min-parallelism", + "42", + }, + }, + { + description: "set max parallelism", + + options: []Option{ + WithMaxParallelism(42), + }, + + expectedArgs: []string{ + "--max-parallelism", + "42", + }, + }, + { + description: "set min rtt-timeout", + + options: []Option{ + WithMinRTTTimeout(2 * time.Minute), + }, + + expectedArgs: []string{ + "--min-rtt-timeout", + "120000ms", + }, + }, + { + description: "set max rtt-timeout", + + options: []Option{ + WithMaxRTTTimeout(8 * time.Hour), + }, + + expectedArgs: []string{ + "--max-rtt-timeout", + "28800000ms", + }, + }, + { + description: "set initial rtt-timeout", + + options: []Option{ + WithInitialRTTTimeout(8 * time.Hour), + }, + + expectedArgs: []string{ + "--initial-rtt-timeout", + "28800000ms", + }, + }, + { + description: "set max retries", + + options: []Option{ + WithMaxRetries(42), + }, + + expectedArgs: []string{ + "--max-retries", + "42", + }, + }, + { + description: "set host timeout", + + options: []Option{ + WithHostTimeout(42 * time.Second), + }, + + expectedArgs: []string{ + "--host-timeout", + "42000ms", + }, + }, + { + description: "set scan delay", + + options: []Option{ + WithScanDelay(42 * time.Millisecond), + }, + + expectedArgs: []string{ + "--scan-delay", + "42ms", + }, + }, + { + description: "set max scan delay", + + options: []Option{ + WithMaxScanDelay(42 * time.Millisecond), + }, + + expectedArgs: []string{ + "--max-scan-delay", + "42ms", + }, + }, + { + description: "set min rate", + + options: []Option{ + WithMinRate(42), + }, + + expectedArgs: []string{ + "--min-rate", + "42", + }, + }, + { + description: "set max rate", + + options: []Option{ + WithMaxRate(42), + }, + + expectedArgs: []string{ + "--max-rate", + "42", + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + s, err := NewScanner(context.TODO(), test.options...) + if err != nil { + panic(err) + } + + if !reflect.DeepEqual(s.args, test.expectedArgs) { + t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) + } + }) + } +} diff --git a/tests/scripts/fake_nmap_delay.sh b/tests/scripts/fake_nmap_delay.sh index 48d090a..6b5bf6b 100755 --- a/tests/scripts/fake_nmap_delay.sh +++ b/tests/scripts/fake_nmap_delay.sh @@ -6,7 +6,7 @@ do echo "$line" if [ $count -gt 13 ] && [ $count -lt 23 ] then - sleep 1 + sleep 0.1 fi (( count++ )) done < "$input" diff --git a/xml.go b/xml.go index 7e1753c..bfb78a9 100644 --- a/xml.go +++ b/xml.go @@ -4,11 +4,11 @@ import ( "bytes" "encoding/xml" "io" - "io/ioutil" + "os" "strconv" "time" - family "github.com/Ullaakut/nmap/v2/pkg/osfamilies" + family "github.com/Ullaakut/nmap/v3/pkg/osfamilies" ) // Run represents an nmap scanning run. @@ -40,7 +40,15 @@ type Run struct { // ToFile writes a Run as XML into the specified file path. func (r Run) ToFile(filePath string) error { - return ioutil.WriteFile(filePath, r.rawXML, 0666) + file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return err + } + _, err = file.Write(r.rawXML) + if err != nil { + return err + } + return err } // ToReader writes the raw XML into an streamable buffer. @@ -48,6 +56,14 @@ func (r Run) ToReader() io.Reader { return bytes.NewReader(r.rawXML) } +func (r *Run) FromFile(filename string) error { + readFile, err := os.ReadFile(filename) + if err != nil { + return err + } + return Parse(readFile, r) +} + // ScanInfo represents the scan information. type ScanInfo struct { NumServices int `xml:"numservices,attr" json:"num_services"` @@ -424,14 +440,11 @@ func (t *Timestamp) UnmarshalXMLAttr(attr xml.Attr) (err error) { return t.ParseTime(attr.Value) } -// Parse takes a byte array of nmap xml data and unmarshals it into a -// Run struct. -func Parse(content []byte) (*Run, error) { - r := &Run{ - rawXML: content, - } +// Parse takes a byte array of nmap xml data and unmarshal it into a Run struct. +func Parse(content []byte, result *Run) error { + result.rawXML = content - err := xml.Unmarshal(content, r) + err := xml.Unmarshal(content, result) - return r, err + return err } diff --git a/xml_test.go b/xml_test.go index e923453..14169e3 100644 --- a/xml_test.go +++ b/xml_test.go @@ -11,7 +11,7 @@ import ( "testing" "time" - family "github.com/Ullaakut/nmap/v2/pkg/osfamilies" + family "github.com/Ullaakut/nmap/v3/pkg/osfamilies" ) func TestParseTime(t *testing.T) { @@ -319,7 +319,8 @@ func TestToReader(t *testing.T) { t.Fatal(err) } - result, err := Parse(rawXML) + var result Run + err = Parse(rawXML, &result) if err != nil { t.Fatal(err) } @@ -1082,14 +1083,15 @@ func TestParseRunXML(t *testing.T) { t.Fatal(err) } - result, err := Parse(rawXML) + var result Run + err = Parse(rawXML, &result) // Remove rawXML before comparing - if result != nil { + if err != nil { result.rawXML = []byte{} } - compareResults(t, test.expectedResult, result) + compareResults(t, test.expectedResult, &result) if err != test.expectedError { t.Errorf("expected %v got %v", test.expectedError, err)