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 @@
-
-
-
+
+
+
@@ -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)