diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b6cd2da..de8cd3e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,28 +8,43 @@ on: - 'examples/*' jobs: - check: + build: runs-on: ubuntu-latest steps: - name: Check out source code - uses: actions/checkout@v2 + uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Install Go - uses: actions/setup-go@v4 + id: install-go + uses: actions/setup-go@v6 with: - go-version: "1.20.0" + go-version-file: 'go.mod' + cache: false - - name: Cache Go modules - uses: actions/cache@v2 + - name: Cache Go mod + id: gomod + uses: actions/cache@v5 with: path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} restore-keys: | - ${{ runner.os }}-go- + ${{ runner.os }}-go-mod- + + - name: Cache Go build + uses: actions/cache@v5 + with: + path: ~/.cache/go-build + key: ${{ runner.os }}-go-build-${{ github.ref_name }} + restore-keys: | + ${{ runner.os }}-go-build- + + - name: Download dependencies + run: go mod download + if: steps.gomod.outputs.cache-hit != 'true' - # Here, we simply print the exact go version, to have it as part of the - # action's output, which might be convenient. - name: Print Go version run: go version diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7d7d330..3480020 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,55 +12,67 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out source code - uses: actions/checkout@v2 + uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Install Go - uses: actions/setup-go@v4 + id: install-go + uses: actions/setup-go@v6 with: - go-version: "1.20.0" + go-version-file: 'go.mod' + cache: false - - name: Cache Go modules - uses: actions/cache@v2 + - name: Cache Go mod + id: gomod + uses: actions/cache@v5 with: path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} restore-keys: | - ${{ runner.os }}-go- - - # Here, we simply print the exact go version, to have it as part of the - # action's output, which might be convenient. - - name: Print Go version - run: go version - - # This check makes sure that the `go.mod` and `go.sum` files for Go - # modules are always up-to-date. - - name: Verify Go modules - run: go mod tidy && git status && git --no-pager diff && git diff-index --quiet HEAD -- - - # This check makes sure that the source code is formatted according to the - # Go standard `go fmt` formatting. - - name: Verify source code formatting - run: go fmt ./... && git status && git --no-pager diff && git diff-index --quiet HEAD -- + ${{ runner.os }}-go-mod- - - name: Install nmap - run: sudo apt install -y nmap + - name: Cache Go build + uses: actions/cache@v5 + with: + path: ~/.cache/go-build + key: ${{ runner.os }}-go-build-${{ github.ref_name }} + restore-keys: | + ${{ runner.os }}-go-build- - - name: Test - run: go test -coverprofile=c.out ./... + - name: Download dependencies + run: go mod download + if: steps.gomod.outputs.cache-hit != 'true' - - name: Install goveralls - run: go install github.com/mattn/goveralls@latest + - name: Download nmap + run: sudo apt-get install -y nmap - - name: Create coverage report - run: go tool cover -func=c.out + - name: Print Go version + run: go version + + - name: Run Linter + uses: golangci/golangci-lint-action@v9 + with: + version: v2.7.2 - - name: Send coverage report - env: - COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: goveralls -coverprofile=c.out -service=github + - name: Setup gotestsum + uses: gertd/action-gotestsum@v3.0.0 + with: + gotestsum_version: v1.13.0 - - name: Install staticcheck - run: go install honnef.co/go/tools/cmd/staticcheck@v0.4.3 + - name: Run Tests + env: + TEST_DIR: ${{ inputs.TEST_DIR }} + run: | + GOTESTSUM_FLAGS="--junitfile tests.xml --format pkgname -- -cover -race" + if [ -z "$TEST_DIR" ]; then + gotestsum $GOTESTSUM_FLAGS ./... + else + gotestsum $GOTESTSUM_FLAGS ./$TEST_DIR/... + fi - - name: Run staticcheck for possible optimizations - run: staticcheck -tests=false + - name: Test Summary + uses: test-summary/action@v2 + with: + paths: "tests.xml" + if: always() diff --git a/.gitignore b/.gitignore index d02b563..1521c8b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,100 +1 @@ -## Golang -vendor/* - -### Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -### Test binary, built with `go test -c` -*.test - -### Output of the go coverage tool, specifically when used with LiteIDE -*.out - -## MacOS - -### General -.DS_Store - -## IDEs - -### VSCode - -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json - -### JetBrains -.idea - -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm, Goland -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/modules.xml -# .idea/*.iml -# .idea/modules - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser +dist diff --git a/.golangci.yml b/.golangci.yml index 77d054b..b47cd56 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,159 +1,70 @@ -# This file contains all available configuration options -# with their default values. - -# options for analysis running +version: "2" run: - # timeout for analysis, e.g. 30s, 5m, default is 1m - deadline: 1m - tests: false - - # which dirs to skip: they won't be analyzed; - # can use regexp here: generated.*, regexp is applied on full path; - # default value is empty list, but next dirs are always skipped independently - # from this option's value: - # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ - skip-dirs: - - pkg/osfamilies - -# output configuration options -output: - # colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number" - format: colored-line-number - - # print lines of code with issue, default is true - print-issued-lines: true - - # print linter name in the end of issue text, default is true - print-linter-name: true - - -# all available settings of specific linters -linters-settings: - errcheck: - # report about not checking of errors in type assetions: `a := b.(MyStruct)`; - # default is false: such cases aren't reported by default. - check-type-assertions: false - - # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; - # default is false: such cases aren't reported by default. - check-blank: false - - # [deprecated] comma-separated list of pairs of the form pkg:regex - # the regex is used to ignore names within pkg. (default "fmt:.*"). - # see https://github.com/kisielk/errcheck#the-deprecated-method for details - ignore: fmt:.*,io/ioutil:^Read.*,os/exec:^Kill.* - - govet: - # report about shadowed variables - check-shadowing: true - golint: - # minimal confidence for issues, default is 0.8 - min-confidence: 0.8 - gofmt: - # simplify code: gofmt with `-s` option, true by default - simplify: true - goimports: - # put imports beginning with prefix after 3rd-party packages; - # it's a comma-separated list of prefixes - local-prefixes: github.com/org/project - gocyclo: - # minimal code complexity to report, 30 by default (but we recommend 10-20) - min-complexity: 10 - maligned: - # print struct with more effective memory layout or not, false by default - suggest-new: true - dupl: - # tokens count to trigger issue, 150 by default - threshold: 150 - goconst: - # minimal length of string constant, 3 by default - min-len: 3 - # minimal occurrences count to trigger, 3 by default - min-occurrences: 3 - depguard: - list-type: blacklist - include-go-root: false - packages: - - github.com/davecgh/go-spew/spew - misspell: - # Correct spellings using locale preferences for US or UK. - # Default is to use a neutral variety of English. - # Setting locale to US will correct the British spelling of 'colour' to 'color'. - locale: US - lll: - # max line length, lines longer will be reported. Default is 120. - # '\t' is counted as 1 character by default, and can be changed with the tab-width option - line-length: 120 - # tab width in spaces. Default to 1. - tab-width: 1 - unused: - # treat code as a program (not a library) and report unused exported identifiers; default is false. - # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: - # if it's called for subdir of a project it can't find funcs usages. All text editor integrations - # with golangci-lint call it on a directory with the changed file. - check-exported: false - unparam: - # call graph construction algorithm (cha, rta). In general, use cha for libraries, - # and rta for programs with main packages. Default is cha. - algo: cha - - # Inspect exported functions, default is false. Set to true if no external program/library imports your code. - # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: - # if it's called for subdir of a project it can't find external interfaces. All text editor integrations - # with golangci-lint call it on a directory with the changed file. - check-exported: false - nakedret: - # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 - max-func-lines: 30 - prealloc: - # XXX: we don't recommend using this linter before doing performance profiling. - # For most programs usage of prealloc will be a premature optimization. - - # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. - # True by default. - simple: true - range-loops: true # Report preallocation suggestions on range loops, true by default - for-loops: false # Report preallocation suggestions on for loops, false by default - gocritic: - # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint` run to see all tags and checks. - # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". - enabled-tags: - - performance - linters: - enable: - - megacheck - - govet - enable-all: false + default: all disable: - - maligned - - prealloc - disable-all: false - presets: - - bugs - - unused - fast: false - - -issues: - # List of regexps of issue texts to exclude, empty list by default. - # But independently from this option we use default exclude patterns, - # it can be disabled by `exclude-use-default: false`. To list all - # excluded by default patterns execute `golangci-lint run --help` - exclude: - - "Subprocess launching should be audited" - - # Maximum issues count per one linter. Set to 0 to disable. Default is 50. - max-per-linter: 0 - - # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. - max-same-issues: 0 - - # Show only new issues: if there are unstaged changes or untracked files, - # only those changes are analyzed, else only changes in HEAD~ are analyzed. - # It's a super-useful option for integration of golangci-lint into existing - # large codebase. It's not practical to fix all existing issues at the moment - # of integration: much better don't allow issues in new code. - # Default is false. - new: false + - depguard + - dupl + - err113 + - exhaustive + - exhaustruct + - forcetypeassert + - funcorder + - funlen + - gochecknoglobals + - gochecknoinits + - gocyclo + - godox + - gomoddirectives + - inamedparam + - ireturn + - mnd + - nilnil + - nlreturn + - nonamedreturns + - tagliatelle + - varnamelen + - wrapcheck + - wsl + - wsl_v5 + settings: + cyclop: + max-complexity: 15 + gosec: + excludes: + - G101 + - G402 + lll: + line-length: 160 + tagliatelle: + case: + rules: + json: pascal + use-field-name: true + exclusions: + generated: lax + rules: + - path: (.+)\.go$ + text: 'ST1000: at least one file in a package should have a package comment' + - path: (.+)\.go$ + text: 'package-comments: should have a package comment' + - path: (.+)\.go$ + text: 'Error return value of `.+\.Close` is not checked' + - linters: + - cyclop + path: (.+)_test\.go + paths: + - examples/ +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports + settings: + gofumpt: + extra-rules: true + exclusions: + generated: lax + paths: [] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e9e40f5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing + +Thanks for your interest in contributing! + +## Getting started + +- **Fork** the repo and create your branch from `master`. +- Use **clear commit messages** and keep changes focused. +- Update or add **tests** for behavior changes. + +## Development + +- Go version: use the version declared in `go.mod`. +- Format code: + - `make fmt` +- Lint: + - `make lint` + +## Tests + +> [!IMPORTANT] +> Running unit tests requires **both** a local **nmap** installation **and Docker** available on your machine. + +Run tests with: + +- `make test` + +## Pull requests + +- Describe the **what** and **why**. +- Link related issues if applicable. +- Ensure CI passes. diff --git a/LICENSE b/LICENSE index b92c230..3c1831c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Ullaakut +Copyright (c) 2026 Ullaakut Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ef537d9 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +# set this e.g. via `make build GORELEASER_FLAGS="--skip=docker"` for temporary flags +GORELEASER_FLAGS= + +#Format + +fmt: + @echo "==> Formatting source" + @gofmt -s -w $(shell find . -type f -name '*.go') + @echo "==> Done" +.PHONY: fmt + +#Test + +test: + @go test -cover -race ./... +.PHONY: test + +#Lint + +lint: + @golangci-lint run --config=.golangci.yml ./... +.PHONY: lint diff --git a/README.md b/README.md index 8e3e37c..b929968 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ - PkgGoDev github.com/Ullaakut/nmap/v3 - - + PkgGoDev github.com/Ullaakut/nmap/v4 + + @@ -32,34 +32,39 @@ Nmap (Network Mapper) is a free and open-source network scanner created by [Gord Nmap provides a number of features for probing computer networks, including host discovery and service and operating system detection. These features are extensible by scripts that provide more advanced service detection, vulnerability detection, and other features. Nmap can adapt to network conditions including latency and congestion during a scan. -## Why use go for penetration testing +## Why use Go for penetration testing Most pentest tools are currently written using Python and not Go, because it is easy to quickly write scripts, lots of libraries are available, and it's a simple language to use. However, for writing robust and reliable applications, Go is the better tool. It is statically compiled, has a static type system, much better performance, it is also a very simple language to use and goroutines are awesome... But I might be slighly biased, so feel free to disagree. -## Supported features +## How it works -- [x] All of `nmap`'s native options. -- [x] Additional [idiomatic go filters](examples/service_detection/main.go#L19) for filtering hosts and ports. -- [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. -- [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. +This library shells out to the `nmap` binary using Go's `exec` package and parses the XML output. +That means `nmap` must be installed and available on your PATH for this library to work. -## Simple example +Compatibility is confirmed with the current latest version of nmap, `7.98`. + +## Privileges + +Some scan types require elevated privileges (for example, SYN scans, OS detection, or raw socket usage). +If you enable those options, you may need to run your program with sudo or the appropriate capabilities for your platform. + +> [!TIP] +> For unprivileged runs, prefer connect scans (e.g. `-sT`). + +## Examples + +### Synchronous scan ```go package main import ( - "context" - "fmt" - "log" - "time" + "context" + "fmt" + "log" + "time" - "github.com/Ullaakut/nmap/v3" + "github.com/Ullaakut/nmap/v4" ) func main() { @@ -69,20 +74,21 @@ func main() { // 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.WithTargets("scanme.nmap.org"), nmap.WithPorts("80,443,843"), ) if err != nil { - log.Fatalf("unable to create nmap scanner: %v", err) + log.Fatalf("creating 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. - } + result, err := scanner.Run(ctx) if err != nil { - log.Fatalf("unable to run nmap scan: %v", err) + log.Fatalf("running network scan: %v", err) + } + + warnings := result.Warnings() + if len(warnings) > 0 { + log.Printf("warning: %v\n", warnings) // Warnings are non-critical errors from nmap. } // Use the results to print an example output @@ -105,35 +111,135 @@ func main() { The program above outputs: ```bash -Host "172.217.16.46": - Port 80/tcp open http - Port 443/tcp open https - Port 843/tcp filtered unknown -Host "31.13.81.36": - Port 80/tcp open http - Port 443/tcp open https - Port 843/tcp open unknown -Host "216.58.215.110": - Port 80/tcp open http - Port 443/tcp open https - Port 843/tcp filtered unknown -Nmap done: 3 hosts up scanned in 1.29 seconds +Host "45.33.32.156": + Port 80/tcp open http + Port 443/tcp closed https + Port 843/tcp closed +Nmap done: 1 hosts up scanned in 0.42 seconds +``` + +### Synchronous scan with progress (TTY only) + +> [!IMPORTANT] +> This relies on terminal escape sequences and only works when the process is attached to a TTY. + +> [!NOTE] +> Progress is not guaranteed to increase monotonically: nmap estimates time remaining and can revise that estimate, which may cause the reported percentage to go down. + +```go +package main + +import ( + "context" + "log" + "time" + + "github.com/Ullaakut/nmap/v4" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + scanner, err := nmap.NewScanner( + nmap.WithTargets("scanme.nmap.org"), + nmap.WithPorts("1-1024"), + nmap.WithTimingTemplate(nmap.TimingAggressive), + nmap.WithProgress(time.Second, handleProgress), + ) + if err != nil { + log.Fatalf("creating nmap scanner: %v", err) + } + + _, err = scanner.Run(ctx) + if err != nil { + log.Fatalf("running network scan: %v", err) + } +} + +func handleProgress(p nmap.TaskProgress) { + log.Println("Current progress: ", p.Percent) +} +``` + +This example outputs the following: + +```txt +2026/01/27 16:13:02 task "Connect Scan": 2.59% remaining 38 +2026/01/27 16:13:02 task "Connect Scan": 21.26% remaining 4 +2026/01/27 16:13:04 task "Connect Scan": 42.61% remaining 5 +2026/01/27 16:13:04 task "Connect Scan": 45.51% remaining 4 +2026/01/27 16:13:05 task "Connect Scan": 53.44% remaining 4 +2026/01/27 16:13:07 task "Connect Scan": 59.77% remaining 5 +2026/01/27 16:13:07 task "Connect Scan": 62.77% remaining 4 +2026/01/27 16:13:08 task "Connect Scan": 73.24% remaining 3 +2026/01/27 16:13:09 task "Connect Scan": 81.71% remaining 2 +2026/01/27 16:13:10 task "Connect Scan": 92.92% remaining 1 +2026/01/27 16:13:11 task "Connect Scan": 100.00% remaining 0 +``` + +### Asynchronous scan + +```go +package main + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/Ullaakut/nmap/v4" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + scanner, err := nmap.NewScanner( + nmap.WithTargets("scanme.nmap.org"), + nmap.WithPorts("1-1024"), + ) + if err != nil { + log.Fatalf("creating nmap scanner: %v", err) + } + + stdout, stderr, resultCh, err := scanner.RunAsync(ctx) + if err != nil { + log.Fatalf("running network scan: %v", err) + } + + for { + select { + case <-ctx.Done(): + log.Fatalf("scan timed out: %v", ctx.Err()) + case out := <-stdout: + fmt.Printf("nmap output: %s\n", out) + case errOut := <-stderr: + fmt.Printf("nmap error output: %s\n", errOut) + case result := <-resultCh: + if result.Err != nil { + log.Fatalf("running network scan: %v", result.Err) + } + + fmt.Printf("Nmap done: %d hosts up\n", len(result.Result.Hosts)) + return + } + } +} ``` +### More examples + +See the [examples](examples/) directory for more usage examples. + ## Advanced example [Cameradar](https://github.com/Ullaakut/cameradar) already uses this library at its core to communicate with nmap, discover RTSP streams and access them remotely. -

- -

- 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) diff --git a/errors.go b/errors.go index 71d04f7..16fa5c5 100644 --- a/errors.go +++ b/errors.go @@ -11,7 +11,7 @@ var ( ErrNmapNotInstalled = errors.New("nmap binary was not found") // ErrScanTimeout means that the provided context timeout triggered done before the scanner finished its scan. - // This error will *not* be returned if a scan timeout was configured using Nmap arguments, since Nmap would + // This error is *not* returned if a scan timeout was configured using Nmap arguments, since Nmap would // gracefully shut down it's scanning and return some results in that case. ErrScanTimeout = errors.New("nmap scan timed out") @@ -23,9 +23,9 @@ var ( ErrMallocFailed = errors.New("malloc failed, probably out of space") // ErrParseOutput means that nmap's output was not parsed successfully. - ErrParseOutput = errors.New("unable to parse nmap output, see warnings for details") + ErrParseOutput = errors.New("nmap output parsing failure, see warnings for details") - // ErrRequiresRoot means that a feature (e.g. OS detection) requires root privileges + // ErrRequiresRoot means that a feature (e.g. OS detection) requires root privileges. ErrRequiresRoot = errors.New("this feature requires root privileges") // ErrResolveName means that Nmap could not resolve a name. diff --git a/examples/async_scan/main.go b/examples/async_scan/main.go new file mode 100644 index 0000000..b4bb205 --- /dev/null +++ b/examples/async_scan/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "bytes" + "context" + "log" + "time" + + "github.com/Ullaakut/nmap/v4" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + scanner, err := nmap.NewScanner( + nmap.WithTargets("scanme.nmap.org"), + nmap.WithPorts("1-1024"), + ) + if err != nil { + log.Fatalf("creating nmap scanner: %v", err) + } + + stdout, stderr, resultCh, err := scanner.RunAsync(ctx) + if err != nil { + log.Fatalf("running network scan: %v", err) + } + + for { + select { + case <-ctx.Done(): + log.Fatalf("scan timed out: %v", ctx.Err()) + case out := <-stdout: + if len(bytes.TrimSpace(out)) == 0 { + continue + } + log.Printf("stdout: %s\n", out) + case errOut := <-stderr: + if len(bytes.TrimSpace(errOut)) == 0 { + continue + } + log.Printf("stderr: %s\n", errOut) + case result := <-resultCh: + if result.Err != nil { + log.Fatalf("running network scan: %v", result.Err) + } + + log.Printf("Nmap done: %d hosts up\n", len(result.Result.Hosts)) + return + } + } +} diff --git a/examples/basic_scan/main.go b/examples/basic_scan/main.go index 02a0770..acff22a 100644 --- a/examples/basic_scan/main.go +++ b/examples/basic_scan/main.go @@ -2,34 +2,30 @@ package main import ( "context" - "fmt" "log" - "time" - "github.com/Ullaakut/nmap/v3" + "github.com/Ullaakut/nmap/v4" ) 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`, + // Equivalent to `/usr/local/bin/nmap -p 80,443,843 scanme.nmap.org`, // with a 5-minute timeout. scanner, err := nmap.NewScanner( - ctx, - nmap.WithTargets("google.com", "facebook.com", "youtube.com"), + nmap.WithTargets("scanme.nmap.org"), nmap.WithPorts("80,443,843"), ) if err != nil { - log.Fatalf("unable to create nmap scanner: %v", err) + log.Fatalf("creating 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. - } + result, err := scanner.Run(context.Background()) if err != nil { - log.Fatalf("unable to run nmap scan: %v", err) + log.Fatalf("running network scan: %v", err) + } + + warnings := result.Warnings() + if len(warnings) > 0 { + log.Printf("warning: %v\n", warnings) // Warnings are non-critical errors from nmap. } // Use the results to print an example output @@ -38,12 +34,12 @@ func main() { continue } - fmt.Printf("Host %q:\n", host.Addresses[0]) + log.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) + log.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) + log.Printf("Nmap done: %d hosts up scanned in %.2f seconds\n", len(result.Hosts), result.Stats.Finished.Elapsed) } diff --git a/examples/basic_scan_async/main.go b/examples/basic_scan_async/main.go deleted file mode 100644 index 0f8f8fe..0000000 --- a/examples/basic_scan_async/main.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - - "github.com/Ullaakut/nmap/v3" -) - -func main() { - // Equivalent to `/usr/local/bin/nmap -p 80,443,843 google.com facebook.com youtube.com`, - // with a 5-minute timeout. - s, err := nmap.NewScanner( - context.Background(), - 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) - } - - // Executes asynchronously, allowing results to be streamed in real time. - done := make(chan error) - result, warnings, err := s.Async(done).Run() - if err != nil { - log.Fatal(err) - } - - // Blocks main until the scan has completed. - 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 - 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) - } - } -} diff --git a/examples/basic_scan_progress/main.go b/examples/basic_scan_progress/main.go deleted file mode 100644 index 93c9442..0000000 --- a/examples/basic_scan_progress/main.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - - "github.com/Ullaakut/nmap/v3" -) - -func main() { - scanner, err := nmap.NewScanner( - context.Background(), - nmap.WithTargets("localhost"), - nmap.WithPorts("1-10000"), - nmap.WithServiceInfo(), - nmap.WithVerbosity(3), - ) - if err != nil { - log.Fatalf("unable to create nmap scanner: %v", err) - } - - progress := make(chan float32, 1) - - // Function to listen and print the progress - go func() { - for p := range progress { - fmt.Printf("Progress: %v %%\n", p) - } - }() - - 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) - } - - fmt.Printf("Nmap done: %d hosts up scanned in %.2f seconds\n", len(result.Hosts), result.Stats.Finished.Elapsed) -} diff --git a/examples/basic_scan_streamer_interface/main.go b/examples/basic_scan_streamer_interface/main.go deleted file mode 100644 index 8c536ac..0000000 --- a/examples/basic_scan_streamer_interface/main.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "os" - - "github.com/Ullaakut/nmap/v3" -) - -func main() { - scanner, err := nmap.NewScanner( - context.Background(), - nmap.WithTargets("localhost"), - nmap.WithPorts("1-4000"), - nmap.WithServiceInfo(), - nmap.WithVerbosity(3), - ) - if err != nil { - log.Fatalf("unable to create nmap scanner: %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. - } - if err != nil { - 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 3e1ad7f..8eb5cfb 100644 --- a/examples/count_hosts_by_os/main.go +++ b/examples/count_hosts_by_os/main.go @@ -2,41 +2,39 @@ package main import ( "context" - "fmt" "log" - "github.com/Ullaakut/nmap/v3" - osfamily "github.com/Ullaakut/nmap/v3/pkg/osfamilies" + "github.com/Ullaakut/nmap/v4" + osfamily "github.com/Ullaakut/nmap/v4/pkg/osfamilies" ) func main() { // Equivalent to - // nmap -F -O 192.168.0.0/24 + // nmap -F -O scanme.nmap.org scanner, err := nmap.NewScanner( - context.Background(), - nmap.WithTargets("192.168.0.0/24"), + nmap.WithTargets("scanme.nmap.org"), nmap.WithFastMode(), nmap.WithOSDetection(), // Needs to run with sudo ) if err != nil { - log.Fatalf("unable to create nmap scanner: %v", err) + log.Fatalf("creating 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. - } + result, err := scanner.Run(context.Background()) if err != nil { - log.Fatalf("nmap scan failed: %v", err) + log.Fatalf("running network scan: %v", err) + } + + warnings := result.Warnings() + if len(warnings) > 0 { + log.Printf("warning: %v\n", warnings) // Warnings are non-critical errors from nmap. } countByOS(result) } func countByOS(result *nmap.Run) { - var ( - linux, windows int - ) + var linux, windows int // Count the number of each OS for all hosts. for _, host := range result.Hosts { @@ -49,9 +47,8 @@ func countByOS(result *nmap.Run) { windows++ } } - } } - fmt.Printf("Discovered %d linux hosts and %d windows hosts out of %d total up hosts.\n", linux, windows, result.Stats.Hosts.Up) + log.Printf("Discovered %d linux hosts and %d windows hosts out of %d total up hosts.\n", linux, windows, result.Stats.Hosts.Up) } diff --git a/examples/exclude_targets/main.go b/examples/exclude_targets/main.go new file mode 100644 index 0000000..fe09d77 --- /dev/null +++ b/examples/exclude_targets/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "context" + "log" + + "github.com/Ullaakut/nmap/v4" +) + +func main() { + scanner, err := nmap.NewScanner( + nmap.WithTargets("192.168.1.0/24"), + nmap.WithTargetExclusions("192.168.1.10", "192.168.1.11"), + nmap.WithPorts("22,80"), + ) + if err != nil { + log.Fatalf("creating nmap scanner: %v", err) + } + + result, err := scanner.Run(context.Background()) + if err != nil { + log.Fatalf("running network scan: %v", err) + } + + for _, host := range result.Hosts { + if len(host.Addresses) == 0 { + continue + } + + log.Printf("%s is %s", host.Addresses[0], host.Status.State) + } +} diff --git a/examples/host_discovery/main.go b/examples/host_discovery/main.go new file mode 100644 index 0000000..45a2786 --- /dev/null +++ b/examples/host_discovery/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "context" + "log" + + "github.com/Ullaakut/nmap/v4" +) + +func main() { + scanner, err := nmap.NewScanner( + nmap.WithTargets("192.168.1.0/24"), + nmap.WithPingScan(), + nmap.WithDisabledDNSResolution(), + ) + if err != nil { + log.Fatalf("creating nmap scanner: %v", err) + } + + result, err := scanner.Run(context.Background()) + if err != nil { + log.Fatalf("running network scan: %v", err) + } + + for _, host := range result.Hosts { + if len(host.Addresses) == 0 { + continue + } + + log.Printf("%s is %s", host.Addresses[0], host.Status.State) + } +} diff --git a/examples/ipv6_scan/main.go b/examples/ipv6_scan/main.go new file mode 100644 index 0000000..d9f4e4a --- /dev/null +++ b/examples/ipv6_scan/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "context" + "log" + + "github.com/Ullaakut/nmap/v4" +) + +func main() { + scanner, err := nmap.NewScanner( + nmap.WithTargets("2001:4860:4860::8888"), + nmap.WithSkipHostDiscovery(), + nmap.WithIPv6Scanning(), + ) + if err != nil { + log.Fatalf("creating nmap scanner: %v", err) + } + + result, err := scanner.Run(context.Background()) + if err != nil { + log.Fatalf("running network scan: %v", err) + } + + for _, host := range result.Hosts { + if len(host.Addresses) == 0 { + continue + } + + log.Printf("%s is %s", host.Addresses[0], host.Status.State) + } +} diff --git a/examples/list_interfaces/main.go b/examples/list_interfaces/main.go index 8f881e4..2ee8373 100644 --- a/examples/list_interfaces/main.go +++ b/examples/list_interfaces/main.go @@ -3,27 +3,28 @@ package main import ( "context" "encoding/json" - "fmt" "log" - "github.com/Ullaakut/nmap/v3" + "github.com/Ullaakut/nmap/v4" ) func main() { - scanner, err := nmap.NewScanner(context.Background()) + ctx := context.Background() + + scanner, err := nmap.NewScanner() if err != nil { - log.Fatalf("unable to create nmap scanner: %v", err) + log.Fatalf("creating nmap scanner: %v", err) } - interfaceList, err := scanner.GetInterfaceList() + interfaceList, err := scanner.InterfaceList(ctx) if err != nil { - log.Fatalf("could not get interface list: %v", err) + log.Fatalf("getting interface list: %v", err) } bytes, err := json.MarshalIndent(interfaceList, "", "\t") if err != nil { - log.Fatalf("unable to marshal: %v", err) + log.Fatalf("marshalling interface list: %v", err) } - fmt.Println(string(bytes)) + log.Println(string(bytes)) } diff --git a/examples/os_and_service_detection/main.go b/examples/os_and_service_detection/main.go new file mode 100644 index 0000000..75849e3 --- /dev/null +++ b/examples/os_and_service_detection/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + "log" + + "github.com/Ullaakut/nmap/v4" +) + +func main() { + scanner, err := nmap.NewScanner( + nmap.WithTargets("scanme.nmap.org"), + nmap.WithPorts("22,80,443"), + nmap.WithOSDetection(), + nmap.WithServiceInfo(), + ) + if err != nil { + log.Fatalf("creating nmap scanner: %v", err) + } + + result, err := scanner.Run(context.Background()) + if err != nil { + log.Fatalf("running network scan: %v", err) + } + + for _, host := range result.Hosts { + if len(host.Addresses) == 0 { + continue + } + + log.Printf("Host %q", host.Addresses[0]) + if len(host.OS.Matches) > 0 { + match := host.OS.Matches[0] + log.Printf("OS guess: %s (%d%%)", match.Name, match.Accuracy) + } + + for _, port := range host.Ports { + log.Printf("Port %d/%s %s %s", port.ID, port.Protocol, port.State, port.Service.Name) + } + } +} diff --git a/examples/output_files/main.go b/examples/output_files/main.go new file mode 100644 index 0000000..50249a9 --- /dev/null +++ b/examples/output_files/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "context" + "log" + + "github.com/Ullaakut/nmap/v4" +) + +func main() { + scanner, err := nmap.NewScanner( + nmap.WithTargets("scanme.nmap.org"), + nmap.WithPorts("80,443"), + nmap.WithNmapOutput("scan.txt"), + nmap.WithGrepOutput("scan.gnmap"), + ) + if err != nil { + log.Fatalf("creating nmap scanner: %v", err) + } + + scanner, err = scanner.ToFile("scan.xml") + if err != nil { + log.Fatalf("enabling xml output: %v", err) + } + + _, err = scanner.Run(context.Background()) + if err != nil { + log.Fatalf("running network scan: %v", err) + } +} diff --git a/examples/progress_scan/main.go b/examples/progress_scan/main.go new file mode 100644 index 0000000..00fe5cd --- /dev/null +++ b/examples/progress_scan/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "context" + "log" + "time" + + "github.com/Ullaakut/nmap/v4" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + scanner, err := nmap.NewScanner( + nmap.WithTargets("scanme.nmap.org"), + nmap.WithPorts("1-1024"), + nmap.WithTimingTemplate(nmap.TimingAggressive), + nmap.WithProgress(time.Second, handleProgress), + ) + if err != nil { + log.Fatalf("creating nmap scanner: %v", err) + } + + _, err = scanner.Run(ctx) + if err != nil { + log.Fatalf("running network scan: %v", err) + } +} + +func handleProgress(p nmap.TaskProgress) { + log.Printf("task %q: %.2f%% remaining %d", p.Task, p.Percent, p.Remaining) +} diff --git a/examples/script_scan/main.go b/examples/script_scan/main.go new file mode 100644 index 0000000..dd743c2 --- /dev/null +++ b/examples/script_scan/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "context" + "log" + + "github.com/Ullaakut/nmap/v4" +) + +func main() { + scanner, err := nmap.NewScanner( + nmap.WithTargets("scanme.nmap.org"), + nmap.WithPorts("80,443"), + nmap.WithScripts("http-title", "ssl-cert"), + ) + if err != nil { + log.Fatalf("creating nmap scanner: %v", err) + } + + result, err := scanner.Run(context.Background()) + if err != nil { + log.Fatalf("running network scan: %v", err) + } + + for _, host := range result.Hosts { + for _, port := range host.Ports { + for _, script := range port.Scripts { + log.Printf("Port %d/%s script %q: %s", port.ID, port.Protocol, script.ID, script.Output) + } + } + } +} diff --git a/examples/service_detection/main.go b/examples/service_detection/main.go index d931847..707c60e 100644 --- a/examples/service_detection/main.go +++ b/examples/service_detection/main.go @@ -2,25 +2,19 @@ package main import ( "context" - "fmt" "log" - "github.com/Ullaakut/nmap/v3" + "github.com/Ullaakut/nmap/v4" ) func main() { // Equivalent to - // nmap -sV -T4 192.168.0.0/24 with a filter to remove non-RTSP ports. + // nmap -sV -T4 scanme.nmap.org with a filter to remove hosts without open ports. scanner, err := nmap.NewScanner( - context.Background(), - nmap.WithTargets("192.168.0.0/24"), - nmap.WithPorts("80", "554", "8554"), + nmap.WithTargets("scanme.nmap.org"), + nmap.WithPorts("22", "80", "443"), nmap.WithServiceInfo(), nmap.WithTimingTemplate(nmap.TimingAggressive), - // Filter out ports that are not RTSP - nmap.WithFilterPort(func(p nmap.Port) bool { - return p.Service.Name == "rtsp" - }), // Filter out hosts that don't have any open ports nmap.WithFilterHost(func(h nmap.Host) bool { // Filter out hosts with no open ports. @@ -34,22 +28,28 @@ func main() { }), ) if err != nil { - log.Fatalf("unable to create nmap scanner: %v", err) + log.Fatalf("creating 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. - } + result, err := scanner.Run(context.Background()) if err != nil { - log.Fatalf("nmap scan failed: %v", err) + log.Fatalf("running network scan: %v", err) + } + + warnings := result.Warnings() + if len(warnings) > 0 { + log.Printf("warning: %v\n", warnings) // Warnings are non-critical errors from nmap. } for _, host := range result.Hosts { - fmt.Printf("Host %s\n", host.Addresses[0]) + log.Printf("Host %s\n", host.Addresses[0]) for _, port := range host.Ports { - fmt.Printf("\tPort %d open with RTSP service\n", port.ID) + if port.Status() != "open" { + continue + } + + log.Printf("\tPort %d open (%s)\n", port.ID, port.Service.Name) } } } diff --git a/examples/spoof_and_decoys/main.go b/examples/spoof_and_decoys/main.go index f011f55..fdb6c0b 100644 --- a/examples/spoof_and_decoys/main.go +++ b/examples/spoof_and_decoys/main.go @@ -2,21 +2,22 @@ package main import ( "context" - "fmt" "log" - "github.com/Ullaakut/nmap/v3" + "github.com/Ullaakut/nmap/v4" ) func main() { - ifaceScanner, err := nmap.NewScanner(context.Background()) + ctx := context.Background() + + scanner, err := nmap.NewScanner() if err != nil { - log.Fatalf("unable to create nmap scanner: %v", err) + log.Fatalf("creating nmap scanner: %v", err) } - interfaceList, err := ifaceScanner.GetInterfaceList() + interfaceList, err := scanner.InterfaceList(ctx) if err != nil { - log.Fatalf("could not get interface list: %v", err) + log.Fatalf("getting interface list: %v", err) } if len(interfaceList.Interfaces) == 0 { @@ -29,11 +30,10 @@ func main() { // Equivalent to // 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(), + // scanme.nmap.org`. + scanner, err = nmap.NewScanner( nmap.WithInterface(interfaceToScan), - nmap.WithTargets("192.168.0.72"), + nmap.WithTargets("scanme.nmap.org"), nmap.WithSpoofIPAddress("192.168.0.10"), nmap.WithDecoys( "192.168.0.2", @@ -46,17 +46,19 @@ func main() { ), ) if err != nil { - log.Fatalf("unable to create nmap scanner: %v", err) + log.Fatalf("creating nmap scanner: %v", err) } - fmt.Println("Running the following nmap command:", scanner.Args()) + log.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. - } + result, err := scanner.Run(ctx) if err != nil { - log.Fatalf("nmap scan failed: %v", err) + log.Fatalf("running network scan: %v", err) + } + + warnings := result.Warnings() + if len(warnings) > 0 { + log.Printf("warning: %v\n", warnings) // Warnings are non-critical errors from nmap. } printResults(result) @@ -69,12 +71,12 @@ func printResults(result *nmap.Run) { continue } - fmt.Printf("Host %q:\n", host.Addresses[0]) + log.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) + log.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) + log.Printf("Nmap done: %d hosts up scanned in %.2f seconds\n", len(result.Hosts), result.Stats.Finished.Elapsed) } diff --git a/examples/timing_templates/main.go b/examples/timing_templates/main.go new file mode 100644 index 0000000..eb47040 --- /dev/null +++ b/examples/timing_templates/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "context" + "log" + "time" + + "github.com/Ullaakut/nmap/v4" +) + +func main() { + scanner, err := nmap.NewScanner( + nmap.WithTargets("scanme.nmap.org"), + nmap.WithPorts("1-1024"), + nmap.WithTimingTemplate(nmap.TimingAggressive), + nmap.WithScanDelay(10*time.Millisecond), + nmap.WithMaxRetries(2), + ) + if err != nil { + log.Fatalf("creating nmap scanner: %v", err) + } + + result, err := scanner.Run(context.Background()) + if err != nil { + log.Fatalf("running network scan: %v", err) + } + + for _, host := range result.Hosts { + if len(host.Addresses) == 0 { + continue + } + + log.Printf("%s is %s", host.Addresses[0], host.Status.State) + } +} diff --git a/examples/top_ports/main.go b/examples/top_ports/main.go new file mode 100644 index 0000000..fcba591 --- /dev/null +++ b/examples/top_ports/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "context" + "log" + + "github.com/Ullaakut/nmap/v4" +) + +func main() { + scanner, err := nmap.NewScanner( + nmap.WithTargets("scanme.nmap.org"), + nmap.WithMostCommonPorts(100), + nmap.WithServiceInfo(), + ) + if err != nil { + log.Fatalf("creating nmap scanner: %v", err) + } + + result, err := scanner.Run(context.Background()) + if err != nil { + log.Fatalf("running network scan: %v", err) + } + + for _, host := range result.Hosts { + if len(host.Addresses) == 0 { + continue + } + + log.Printf("%s is %s", host.Addresses[0], host.Status.State) + } +} diff --git a/examples/udp_scan/main.go b/examples/udp_scan/main.go new file mode 100644 index 0000000..58916a9 --- /dev/null +++ b/examples/udp_scan/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "context" + "log" + + "github.com/Ullaakut/nmap/v4" +) + +func main() { + scanner, err := nmap.NewScanner( + nmap.WithTargets("scanme.nmap.org"), + nmap.WithUDPScan(), + nmap.WithPorts("53,123,161"), + ) + if err != nil { + log.Fatalf("creating nmap scanner: %v", err) + } + + result, err := scanner.Run(context.Background()) + if err != nil { + log.Fatalf("running network scan: %v", err) + } + + for _, host := range result.Hosts { + if len(host.Addresses) == 0 { + continue + } + + log.Printf("%s is %s", host.Addresses[0], host.Status.State) + } +} diff --git a/examples_test.go b/examples_test.go index 5bf716d..3e1fb88 100644 --- a/examples_test.go +++ b/examples_test.go @@ -10,19 +10,18 @@ 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), WithTCPScanFlags(FlagACK, FlagNULL, FlagRST), ) if err != nil { - log.Fatalf("unable to create nmap scanner: %v", err) + log.Fatalf("creating nmap scanner: %v", err) } - scanResult, _, err := s.Run() + scanResult, err := s.Run(context.Background()) if err != nil { - log.Fatalf("nmap encountered an error: %v", err) + log.Fatalf("running network scan: %v", err) } fmt.Printf( diff --git a/go.mod b/go.mod index 8a805bc..710a1aa 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,65 @@ -module github.com/Ullaakut/nmap/v3 +module github.com/Ullaakut/nmap/v4 -go 1.20 +go 1.25 require ( - github.com/stretchr/testify v1.8.2 - golang.org/x/sync v0.1.0 + github.com/hamba/testutils v0.7.0 + github.com/mattn/go-isatty v0.0.20 + github.com/stretchr/testify v1.11.1 + github.com/testcontainers/testcontainers-go v0.31.0 ) require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Microsoft/hcsshim v0.11.4 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/containerd v1.7.15 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/cpuguy83/dockercfg v0.3.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.5.0 // indirect + github.com/docker/docker v25.0.5+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.16.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/mod v0.16.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/tools v0.13.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d // indirect + google.golang.org/grpc v1.58.3 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c050204..3a47318 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,204 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= +github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/containerd v1.7.15 h1:afEHXdil9iAm03BmhjzKyXnnEBtjaLJefdU7DV0IFes= +github.com/containerd/containerd v1.7.15/go.mod h1:ISzRRTMF8EXNpJlTzyr2XMhN+j9K302C21/+cr3kUnY= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= +github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE= +github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/hamba/testutils v0.7.0 h1:GQ0RJbz4+aFauvEV5AFgPMOKltl8gWZVbzROS5b9qDc= +github.com/hamba/testutils v0.7.0/go.mod h1:5rw9ZvxgDegvi9j32U5s5LBDrOBhrCu4g53EM03KOF4= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U= +github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d h1:pgIUhmqwKOUlnKna4r6amKdUngdL8DrkpFeV8+VBElY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= +google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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= +gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..90841f3 --- /dev/null +++ b/helpers.go @@ -0,0 +1,207 @@ +package nmap + +import ( + "bytes" + "context" + "encoding/xml" + "errors" + "fmt" + "io" + "os" + "os/exec" + "strings" +) + +func (s *Scanner) buildArgs() []string { + args := append([]string{}, s.args...) + + // Write XML to standard output by default. + // If toFile is set then write XML to file. + outArg := "-" + if s.toFile != nil { + outArg = *s.toFile + } + args = append(args, "-oX", outArg) + + return args +} + +func (s *Scanner) newCmd(ctx context.Context) *exec.Cmd { + args := s.buildArgs() + + //nolint:gosec // Arguments are passed directly to nmap; users intentionally control args. + cmd := exec.CommandContext(ctx, s.binaryPath, args...) + if s.modifySysProcAttr != nil { + s.modifySysProcAttr(cmd.SysProcAttr) + } + return cmd +} + +func finalizeRun(ctx context.Context, runErr, parseErr error, result *Run, stdout, stderr *bytes.Buffer) (*Run, error) { + if runErr == nil { + return result, parseErr + } + + mappedErr := mapRunError(ctx, runErr) + if mappedErr != nil { + return result, mappedErr + } + + if parseErr != nil { + if stdout.Len() == 0 && stderr.Len() == 0 { + return result, nil + } + return result, parseErr + } + return result, mappedErr +} + +func streamTaskProgress(reader io.Reader, handler func(TaskProgress)) error { + decoder := xml.NewDecoder(reader) + for { + token, err := decoder.Token() + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + return err + } + start, ok := token.(xml.StartElement) + if !ok || start.Name.Local != "taskprogress" { + continue + } + + var progress TaskProgress + err = decoder.DecodeElement(&progress, &start) + if err != nil { + return err + } + handler(progress) + } +} + +type channelWriter struct { + ch chan<- []byte +} + +func (w channelWriter) Write(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + + chunk := make([]byte, len(p)) + copy(chunk, p) + w.ch <- chunk + return len(p), nil +} + +func (s *Scanner) processNmapResult(stdout, stderr *bytes.Buffer) (*Run, error) { + result := &Run{} + + // Check for errors indicated by stderr output. + var warnings []string + warnings, errStdout := checkStdErr(stderr) + if errStdout != nil { + return result, errStdout + } + + contents := stdout.Bytes() + + // 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. + var err error + if s.toFile != nil { + contents, err = os.ReadFile(*s.toFile) + if err != nil { + return result, fmt.Errorf("reading output file %s: %w", *s.toFile, err) + } + + chmodErr := os.Chmod(*s.toFile, 0o600) + if chmodErr != nil { + warnings = append(warnings, fmt.Sprintf("setting output file permissions: %s", chmodErr)) + } + } + + result, err = parse(contents) + if err != nil { + return nil, fmt.Errorf("parsing nmap XML output: %w", err) + } + + // Add warnings after parsing to avoid them being overwritten. + result.warnings = append(result.warnings, warnings...) + + // Critical scan errors are reflected in the XML. + if len(result.Stats.Finished.ErrorMsg) > 0 { + switch { + case strings.Contains(result.Stats.Finished.ErrorMsg, "Error resolving name"): + return result, ErrResolveName + default: + return result, errors.New(result.Stats.Finished.ErrorMsg) + } + } + + // Call filters if they are set. + if s.portFilter != nil { + choosePorts(result, s.portFilter) + } + if s.hostFilter != nil { + chooseHosts(result, s.hostFilter) + } + + return result, nil +} + +func mapRunError(ctx context.Context, err error) error { + if err == nil { + return nil + } + + switch { + case errors.Is(ctx.Err(), context.DeadlineExceeded): + return ErrScanTimeout + case errors.Is(ctx.Err(), context.Canceled): + return ErrScanInterrupt + case isInterruptExit(err): + return ErrScanInterrupt + default: + return err + } +} + +func isInterruptExit(err error) bool { + if err == nil { + return false + } + + switch err.Error() { + case "exit status 0xc000013a": // Exit code for ctrl+c on Windows + return true + case "exit status 130": // Exit code for ctrl+c on Linux + return true + default: + return false + } +} + +// 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, err error) { + if stderr.Len() <= 0 { + return nil, nil + } + + stderrSplit := strings.SplitSeq(strings.Trim(stderr.String(), "\n "), "\n") + + // Check for warnings that inevitably lead to parsing errors, hence, have priority. + for warning := range stderrSplit { + warnings = append(warnings, strings.Trim(warning, " ")) + switch { + case strings.Contains(warning, "Malloc Failed!"): + return warnings, ErrMallocFailed + case strings.Contains(warning, "requires root privileges."): + return warnings, ErrRequiresRoot + default: + } + } + return warnings, nil +} diff --git a/iflist.go b/iflist.go index a18aa45..a83951f 100644 --- a/iflist.go +++ b/iflist.go @@ -2,6 +2,7 @@ package nmap import ( "bytes" + "context" "net" "os/exec" "regexp" @@ -15,7 +16,7 @@ type InterfaceList struct { Routes []*Route `json:"routes"` } -// Interface is a interface object. +// Interface is an interface object. type Interface struct { Device string `json:"device"` Short string `json:"short"` @@ -36,36 +37,36 @@ type Route struct { Gateway net.IP `json:"gateway"` } -// GetInterfaceList runs nmap with the --iflist option. The output will be parsed. +// InterfaceList runs nmap with the --iflist option. // The return value is a struct containing all host interfaces and routes. -func (s *Scanner) GetInterfaceList() (result *InterfaceList, err error) { - var stdout, stderr bytes.Buffer - - args := append(s.args, "--iflist") +func (s *Scanner) InterfaceList(ctx context.Context) (*InterfaceList, error) { + args := append([]string{}, s.args...) + args = append(args, "--iflist") // Prepare nmap process - cmd := exec.Command(s.binaryPath, args...) + //nolint:gosec // Arguments are passed directly to nmap; users intentionally control args. + cmd := exec.CommandContext(ctx, s.binaryPath, args...) + + // Bind stdout and stderr. + var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr - // Run nmap process - err = cmd.Run() + // Run nmap process. + err := cmd.Run() if err != nil { return nil, err } - result = parseInterfaces(stdout.Bytes()) - - return result, nil + return parseInterfaces(stdout.String()), nil } -func parseInterfaces(content []byte) *InterfaceList { +func parseInterfaces(content string) *InterfaceList { list := InterfaceList{ Interfaces: make([]*Interface, 0), Routes: make([]*Route, 0), } - output := string(content) - lines := strings.Split(output, "\n") + lines := strings.Split(content, "\n") interfaceRegex := regexp.MustCompile(`\*INTERFACES\*`) routesRegex := regexp.MustCompile(`\*ROUTES\*`) @@ -92,28 +93,33 @@ func parseInterfaces(content []byte) *InterfaceList { func convertInterface(line string) *Interface { fields := strings.Fields(line) - if len(fields) < 6 { return nil } + iface := &Interface{ Device: fields[0], Short: fields[1], Type: fields[3], } - if ip, val, err := net.ParseCIDR(fields[2]); err == nil { + + ip, ipnet, err := net.ParseCIDR(fields[2]) + if err == nil { iface.IP = ip - iface.IPMask = net.IP(val.Mask) + iface.IPMask = net.IP(ipnet.Mask) } iface.Up = strings.ToLower(fields[4]) == "up" - if val, err := strconv.Atoi(fields[5]); err == nil { - iface.MTU = val + mtu, err := strconv.Atoi(fields[5]) + if err == nil { + iface.MTU = mtu } + if len(fields) > 6 { - if val, err := net.ParseMAC(fields[6]); err == nil { - iface.Mac = val + mac, err := net.ParseMAC(fields[6]) + if err == nil { + iface.Mac = mac } } return iface @@ -121,7 +127,6 @@ func convertInterface(line string) *Interface { func convertRoute(line string) *Route { fields := strings.Fields(line) - if len(fields) < 3 { return nil } @@ -129,15 +134,21 @@ func convertRoute(line string) *Route { route := &Route{ Device: fields[1], } - if ip, val, err := net.ParseCIDR(fields[0]); err == nil { + + ip, ipnet, err := net.ParseCIDR(fields[0]) + if err == nil { route.DestinationIP = ip - route.DestinationIPMask = net.IP(val.Mask) + route.DestinationIPMask = net.IP(ipnet.Mask) } - if val, err := strconv.Atoi(fields[2]); err == nil { - route.Metric = val + + metric, err := strconv.Atoi(fields[2]) + if err == nil { + route.Metric = metric } + if len(fields) > 3 { route.Gateway = net.ParseIP(fields[3]) } + return route } diff --git a/iflist_test.go b/iflist_test.go index fcfe1af..03e049a 100644 --- a/iflist_test.go +++ b/iflist_test.go @@ -1,7 +1,6 @@ package nmap import ( - "context" "net" "testing" @@ -9,10 +8,10 @@ import ( ) func TestScanner_GetInterfaceList(t *testing.T) { - scanner, err := NewScanner(context.Background(), WithBinaryPath("tests/scripts/fake_nmap_iflist.sh")) + scanner, err := NewScanner(WithBinaryPath("tests/scripts/fake_nmap_iflist.sh")) assert.NoError(t, err) - result, err := scanner.GetInterfaceList() + result, err := scanner.InterfaceList(t.Context()) assert.NoError(t, err) assert.NotNil(t, result) diff --git a/internal/testing/network_mapper.go b/internal/testing/network_mapper.go new file mode 100644 index 0000000..afd7b65 --- /dev/null +++ b/internal/testing/network_mapper.go @@ -0,0 +1,41 @@ +package testing + +import ( + "context" + "fmt" + "time" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +// ContainerName is the name given to the nmap test container. +const ContainerName = "nmap-test" + +// StartNetworkMapper starts a container with nmap installed and returns it. +func StartNetworkMapper() (testcontainers.Container, error) { + req := testcontainers.ContainerRequest{ + Image: "instrumentisto/nmap:7.98", + Name: ContainerName, + Cmd: []string{"sleep", "infinity"}, + WaitingFor: wait.ForExec([]string{"nmap", "--version"}). + WithStartupTimeout(time.Minute), + } + ctr, err := testcontainers.GenericContainer(context.Background(), testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + return nil, fmt.Errorf("starting nmap container: %w", err) + } + + return ctr, nil +} + +// StopContainer terminates a testcontainer. +func StopContainer(ctr testcontainers.Container) error { + if ctr == nil { + return nil + } + return ctr.Terminate(context.Background()) +} diff --git a/nmap.go b/nmap.go index f80ab39..77075c7 100644 --- a/nmap.go +++ b/nmap.go @@ -2,24 +2,24 @@ package nmap import ( - "bytes" "context" - "encoding/xml" "errors" "fmt" - "io" + "os" "os/exec" - "strings" - "sync" "syscall" - "time" - "golang.org/x/sync/errgroup" + "github.com/mattn/go-isatty" ) // ScanRunner represents something that can run a scan. type ScanRunner interface { - Run() (result *Run, warnings []string, err error) + Run(ctx context.Context) (*Run, error) +} + +// AsyncScanRunner represents something that can run a scan asynchronously. +type AsyncScanRunner interface { + RunAsync(ctx context.Context) (<-chan []byte, <-chan []byte, <-chan RunResult, error) } // Scanner represents n Nmap scanner. @@ -28,32 +28,37 @@ type Scanner struct { args []string binaryPath string - ctx context.Context portFilter func(Port) bool hostFilter func(Host) bool - doneAsync chan error - liveProgress chan float32 - streamer io.Writer - toFile *string + progressHandler func(TaskProgress) + + interactive bool + toFile *string +} + +// RunResult represents the result of an asynchronous run. +type RunResult struct { + Result *Run + Err error } // Option is a function that is used for grouping of Scanner options. // Option adds or removes nmap command line arguments. -type Option func(*Scanner) +type Option func(*Scanner) error // NewScanner creates a new Scanner, and can take options to apply to the scanner. -func NewScanner(ctx context.Context, options ...Option) (*Scanner, error) { - scanner := &Scanner{ - doneAsync: nil, - liveProgress: nil, - streamer: nil, - ctx: ctx, +func NewScanner(options ...Option) (*Scanner, error) { + scanner := Scanner{ + interactive: isatty.IsTerminal(os.Stdin.Fd()), } for _, option := range options { - option(scanner) + err := option(&scanner) + if err != nil { + return nil, fmt.Errorf("applying option: %w", err) + } } if scanner.binaryPath == "" { @@ -64,158 +69,48 @@ func NewScanner(ctx context.Context, options ...Option) (*Scanner, error) { } } - return scanner, nil -} - -// 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 -} - -// 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 &scanner, 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 -} - -// 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 -} - -// 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 - - warnings = &[]string{} // Instantiate warnings array - - args := s.args - - // Write XML to standard output. - // If toFile is set then write XML to file. - if s.toFile != nil { - args = append(args, "-oX", *s.toFile) - } else { - args = append(args, "-oX", "-") +// Nmap writes the normal CLI output to stdout. +// The XML is parsed from file after the scan is finished. +func (s *Scanner) ToFile(file string) (*Scanner, error) { + if s.progressHandler != nil { + return nil, errors.New("progress updates require XML on stdout; do not use WithProgress with ToFile") } - // Prepare nmap process. - 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 - - // According to cmd.StdoutPipe() doc, we must not "call Wait before all reads from the pipe have completed" - // We use this WaitGroup to wait for all IO operations to finish before calling wait - var wg sync.WaitGroup - - var streamerErrs *errgroup.Group - if s.streamer != nil { - streamerErrs, _ = errgroup.WithContext(s.ctx) - wg.Add(1) - streamerErrs.Go(func() error { - defer wg.Done() - _, err = io.Copy(s.streamer, stdoutDuplicate) - return err - }) - } else { - wg.Add(1) - go func() { - defer wg.Done() - io.Copy(io.Discard, stdoutDuplicate) - }() - } + s.toFile = &file + return s, nil +} - // Run nmap process. - err = cmd.Start() - if err != nil { - return result, warnings, err - } +// Run executes nmap with the enabled options and parses the resulting output. +func (s *Scanner) Run(ctx context.Context) (*Run, error) { + cmd := s.newCmd(ctx) - // Add goroutine that updates chan when command is finished. - done := make(chan error, 1) - doneProgress := make(chan bool, 1) - - go func() { - wg.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. - if s.liveProgress != nil { - go func() { - type progress struct { - TaskProgress []TaskProgress `xml:"taskprogress" json:"task_progress"` - } - for { - select { - case <-doneProgress: - close(s.liveProgress) - return - default: - time.Sleep(time.Millisecond * 100) - var p progress - _ = xml.Unmarshal(stdout.Bytes(), &p) - progressIndex := len(p.TaskProgress) - 1 - if progressIndex >= 0 { - s.liveProgress <- p.TaskProgress[progressIndex].Percent - } - } - } - }() + if s.progressHandler != nil { + return s.runAndParseWithProgress(ctx, cmd) } - // 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) - } + return s.runAndParse(ctx, cmd) +} - return result, warnings, err +// RunAsync executes nmap in a goroutine and streams stdout and stderr +// through channels. It also returns a channel that receives the final +// result and error when the scan completes. +func (s *Scanner) RunAsync(ctx context.Context) (<-chan []byte, <-chan []byte, <-chan RunResult, error) { + return s.runAsync(ctx) } // AddOptions sets more scan options after the scan is created. -func (s *Scanner) AddOptions(options ...Option) *Scanner { +func (s *Scanner) AddOptions(options ...Option) (*Scanner, error) { for _, option := range options { - option(s) + err := option(s) + if err != nil { + return s, fmt.Errorf("applying option: %w", err) + } } - return s + return s, nil } // Args return the list of nmap args. @@ -249,98 +144,6 @@ func choosePorts(result *Run, filter func(Port) bool) { } } -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 ( - errStatus = <-done - err error - ) - close(doneProgress) - - // Check for errors indicated by stderr output. - if errStdout := checkStdErr(stderr, warnings); errStdout != nil { - return errStdout - } - - // Check for errors indicated by context or return code. - switch { - case errors.Is(s.ctx.Err(), context.DeadlineExceeded): // Command context exceeded - return ErrScanTimeout - case errors.Is(s.ctx.Err(), context.Canceled): // Command context cancelled programmatically - return ErrScanInterrupt - case errStatus != nil: // Error with status code returned by Nmap - - // Return suitable error or pass through original exit status - switch { - case errStatus.Error() == "exit status 0xc000013a": // Exit code for ctrl+c on Windows - return ErrScanInterrupt - case errStatus.Error() == "exit status 130": // Exit code for ctrl+c on Linux - return ErrScanInterrupt - // TODO: Add clauses for other known exit codes we might want to define closer. - default: - return errStatus - } - default: - } - - // 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(result.Stats.Finished.ErrorMsg, "Error resolving name"): - return ErrResolveName - default: - return fmt.Errorf(result.Stats.Finished.ErrorMsg) - } - } - - // Call filters if they are set. - if s.portFilter != nil { - choosePorts(result, s.portFilter) - } - if s.hostFilter != nil { - chooseHosts(result, s.hostFilter) - } - - return err -} - -// 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 - case strings.Contains(warning, "requires root privileges."): - return ErrRequiresRoot - // TODO: Add cases for other known errors we might want to guard. - default: - } - } - 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 @@ -350,15 +153,17 @@ func checkStdErr(stderr *bytes.Buffer, warnings *[]string) error { // 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 { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, args...) + return nil } } // WithBinaryPath sets the nmap binary path for a scanner. func WithBinaryPath(binaryPath string) Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.binaryPath = binaryPath + return nil } } @@ -367,8 +172,9 @@ func WithBinaryPath(binaryPath string) Option { // the port is kept, otherwise it is removed from the result. Can be used // along with WithFilterHost. func WithFilterPort(portFilter func(Port) bool) Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.portFilter = portFilter + return nil } } @@ -377,7 +183,8 @@ func WithFilterPort(portFilter func(Port) bool) Option { // the host is kept, otherwise it is removed from the result. Can be used // along with WithFilterPort. func WithFilterHost(hostFilter func(Host) bool) Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.hostFilter = hostFilter + return nil } } diff --git a/nmap_test.go b/nmap_test.go index 02ca6c6..74ad1c3 100644 --- a/nmap_test.go +++ b/nmap_test.go @@ -2,456 +2,30 @@ package nmap import ( "bytes" - "context" - "encoding/xml" - "fmt" - "io/ioutil" + "log" "os" - "os/exec" "reflect" - "strings" - "sync" "testing" - "time" + nmaptesting "github.com/Ullaakut/nmap/v4/internal/testing" "github.com/stretchr/testify/assert" ) -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 -} - -func TestNmapNotInstalled(t *testing.T) { - oldPath := os.Getenv("PATH") - _ = os.Setenv("PATH", "") - - s, err := NewScanner(context.TODO()) - if err == nil { - t.Error("expected NewScanner to fail if nmap is not found in $PATH") - } - - if s != nil { - t.Error("expected NewScanner to return a nil scanner if nmap is not found in $PATH") - } - - _ = os.Setenv("PATH", oldPath) -} - -func TestRun(t *testing.T) { - nmapPath, err := exec.LookPath("nmap") +func TestMain(m *testing.M) { + ctr, err := nmaptesting.StartNetworkMapper() if err != nil { - panic("nmap is required to run those tests") + log.Println("unable to start nmap test container, skipping container-based tests:", err) + os.Exit(1) } - tests := []struct { - description string - - options []Option - - testTimeout bool - compareWholeRun bool - - expectedResult *Run - expectedErr bool - expectedWarnings []string - }{ - { - description: "invalid binary path", - - options: []Option{ - WithTargets("0.0.0.0"), - WithBinaryPath("/invalid"), - }, - - expectedErr: true, - expectedWarnings: []string{}, - }, - { - description: "output can't be parsed", - - options: []Option{ - WithTargets("0.0.0.0"), - WithBinaryPath("echo"), - }, - - expectedErr: true, - expectedWarnings: []string{"EOF"}, - }, - { - description: "context timeout", - - options: []Option{ - WithTargets("0.0.0.0/16"), - }, - - testTimeout: true, - - expectedErr: true, - expectedWarnings: []string{}, - }, - { - description: "scan localhost", - - options: []Option{ - WithTargets("localhost"), - WithTimingTemplate(TimingFastest), - }, - - expectedResult: &Run{ - Args: nmapPath + " -T5 -oX - localhost", - Scanner: "nmap", - }, - - expectedWarnings: []string{}, - }, - { - description: "scan invalid target", - - options: []Option{ - WithTimingTemplate(TimingFastest), - }, - - expectedWarnings: []string{"WARNING: No targets were specified, so 0 hosts scanned."}, - expectedResult: &Run{ - Scanner: "nmap", - Args: nmapPath + " -T5 -oX -", - }, - }, - { - description: "scan error resolving name", - options: []Option{ - WithBinaryPath("tests/scripts/fake_nmap.sh"), - WithCustomArguments("tests/xml/scan_error_resolving_name.xml"), - }, + code := m.Run() - expectedErr: true, - expectedWarnings: []string{}, - expectedResult: &Run{ - Scanner: "fake_nmap", - Args: "nmap test", - }, - }, - { - description: "scan unsupported error", - options: []Option{ - WithBinaryPath("tests/scripts/fake_nmap.sh"), - WithCustomArguments("tests/xml/scan_error_other.xml"), - }, - - expectedErr: true, - expectedWarnings: []string{}, - expectedResult: &Run{ - Scanner: "fake_nmap", - Args: "nmap test", - }, - }, - { - description: "scan localhost with filters", - options: []Option{ - WithBinaryPath("tests/scripts/fake_nmap.sh"), - WithCustomArguments("tests/xml/scan_invalid_services.xml"), - WithFilterHost(func(h Host) bool { - return len(h.Ports) == 2 - }), - WithFilterPort(func(p Port) bool { - return p.Service.Product == "VALID" - }), - WithTimingTemplate(TimingFastest), - }, - - compareWholeRun: true, - expectedWarnings: []string{}, - - expectedResult: &Run{ - XMLName: xml.Name{Local: "nmaprun"}, - Args: "nmap test", - Scanner: "fake_nmap", - Hosts: []Host{ - { - Addresses: []Address{ - { - Addr: "66.35.250.168", - }, - }, - Ports: []Port{ - { - ID: 80, - State: State{ - State: "open", - }, - Service: Service{ - Name: "http", - Product: "VALID", - }, - }, - { - ID: 443, - State: State{ - State: "open", - }, - Service: Service{ - Name: "https", - Product: "VALID", - }, - }, - }, - }, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - ctx := context.Background() - if test.testTimeout { - 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) - })() - } - - 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. - } - - result, warns, err := s.Run() - - if !assert.Equal(t, test.expectedErr, err != nil) { - return - } - - assert.Equal(t, test.expectedWarnings, *warns) - - if test.expectedResult == nil { - return - } - - if test.compareWholeRun { - result.rawXML = nil - 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 %s got %s", test.expectedResult.Args, result.Args) - } - - if result.Scanner != test.expectedResult.Scanner { - t.Errorf("expected scanner %s got %s", test.expectedResult.Scanner, result.Scanner) - } - } - }) - } -} - -func TestRunWithProgress(t *testing.T) { - // Open and parse sample result for testing - dat, err := ioutil.ReadFile("tests/xml/scan_base.xml") + err = nmaptesting.StopContainer(ctr) if err != nil { - panic(err) + log.Println(err) } - var r = &Run{} - _ = Parse(dat, r) - - tests := []struct { - description string - - options []Option - - compareWholeRun bool - - expectedResult *Run - expectedProgress []float32 - expectedErr error - expectedWarnings []string - }{ - { - description: "fake scan with slow output for progress streaming", - options: []Option{ - WithBinaryPath("tests/scripts/fake_nmap_delay.sh"), - WithCustomArguments("tests/xml/scan_base.xml"), - }, - - compareWholeRun: true, - expectedResult: r, - expectedProgress: []float32{3.22, 56.66, 77.02, 81.95, 86.79, 87.84}, - expectedErr: nil, - }, - } - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - 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.Progress(progress).Run() - assert.Equal(t, test.expectedErr, err) - if err != nil { - return - } - - // Test if channel data compares to given progress array - var progressOutput []float32 - for n := range progress { - progressOutput = append(progressOutput, n) - } - assert.Equal(t, test.expectedProgress, progressOutput) - - // Test if read output equals parsed xml file - if test.compareWholeRun { - assert.Equal(t, test.expectedResult.Hosts, result.Hosts) - } - }) - } -} - -func TestRunWithStreamer(t *testing.T) { - streamer := &testStreamer{} - - tests := []struct { - description string - - options []Option - - expectedErr error - expectedWarnings []string - }{ - { - description: "fake scan with streaming", - options: []Option{ - WithBinaryPath("tests/scripts/fake_nmap.sh"), - WithCustomArguments("tests/xml/scan_base.xml"), - }, - expectedErr: nil, - expectedWarnings: []string{}, - }, - } - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - 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.Streamer(streamer).Run() - - assert.Equal(t, test.expectedErr, err) - - assert.Equal(t, test.expectedWarnings, *warnings) - }) - } -} - -func TestRunAsync(t *testing.T) { - tests := []struct { - description string - - options []Option - - testTimeout bool - compareWholeRun bool - - expectedResult *Run - expectedRunAsyncErr bool - expectedWaitErr bool - }{ - { - description: "invalid binary path", - - options: []Option{ - WithTargets("0.0.0.0"), - WithBinaryPath("/invalid"), - }, - - expectedRunAsyncErr: true, - }, - { - description: "output can't be parsed", - - options: []Option{ - WithTargets("0.0.0.0"), - WithBinaryPath("echo"), - }, - - expectedWaitErr: true, - }, - { - description: "context timeout", - - options: []Option{ - WithTargets("0.0.0.0/16"), - }, - - testTimeout: true, - - expectedWaitErr: true, - }, - } - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - ctx := context.Background() - if test.testTimeout { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(context.Background(), 99*time.Hour) - - go (func() { - // Cancel context to force timeout - defer cancel() - time.Sleep(10 * time.Millisecond) - })() - } - - 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. - } - - done := make(chan error) - result, _, err := s.Async(done).Run() - if test.expectedRunAsyncErr { - assert.NotNil(t, err) - } - if err != nil { - return - } - - err = <-done - if test.expectedWaitErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - if err != nil { - return - } - - if test.expectedResult == nil { - return - } - - if test.compareWholeRun { - result.rawXML = nil - if !reflect.DeepEqual(test.expectedResult, result) { - t.Errorf("expected result to be %+v, got %+v", test.expectedResult, result) - } - } - }) - } + os.Exit(code) } func TestCheckStdErr(t *testing.T) { @@ -479,85 +53,10 @@ func TestCheckStdErr(t *testing.T) { t.Run(test.description, func(t *testing.T) { buf := bytes.Buffer{} _, _ = buf.Write([]byte(test.stderr)) - var warnings []string - err := checkStdErr(&buf, &warnings) + warnings, err := checkStdErr(&buf) assert.Equal(t, test.expectedErr, err) assert.True(t, reflect.DeepEqual(test.warnings, warnings)) }) } } - -// Test to verify the fix for a race condition works -// See: https://github.com/Ullaakut/nmap/issues/122 -func TestParseXMLOutputRaceCondition(t *testing.T) { - scans := make(chan int, 100) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - var wg sync.WaitGroup - - // Publish many scan orders - wg.Add(1) - go func() { - defer wg.Done() - for taskId := 0; taskId < 1000; taskId++ { - wg.Add(1) - scans <- taskId - } - }() - - // Consume scan orders with workers in parallel - for worker := 1; worker <= 10; worker++ { - wg.Add(1) - go func(w int) { - defer wg.Done() - for { - var taskId int - - select { - case <-ctx.Done(): - t.Logf("stopping worker %d", w) - return - case i, ok := <-scans: - if !ok { - t.Logf("stopping worker %d", w) - return - } - taskId = i - default: - t.Logf("stopping worker %d", w) - return - } - - _, err := getNmapVersion(ctx) - if err != nil { - t.Errorf("[w:%d] failed scan %d with err: %s", w, taskId, err) - } else { - t.Logf("[w:%d] completed scan %d", w, taskId) - } - wg.Done() - } - }(worker) - } - - wg.Wait() -} - -// getNmapVersion returns the version of nmap installed on the system. -// e.g. "7.80". -func getNmapVersion(ctx context.Context) (string, error) { - scanner, err := NewScanner(ctx) - if err != nil { - return "", fmt.Errorf("nmap.NewScanner: %w", err) - } - - var sb strings.Builder - scanner.Streamer(&sb) - results, warnings, err := scanner.Run() - - if err != nil { - return "", fmt.Errorf("nmap.Run: %w (%v). Result: %+v", err, warnings, sb.String()) - } - return results.Version, nil -} diff --git a/optionsFirewallSpoofing.go b/opt_firewall.go similarity index 72% rename from optionsFirewallSpoofing.go rename to opt_firewall.go index a528031..88867fa 100644 --- a/optionsFirewallSpoofing.go +++ b/opt_firewall.go @@ -2,6 +2,7 @@ package nmap import ( "fmt" + "strconv" "strings" ) @@ -11,8 +12,9 @@ import ( // you are doing. // Some programs have trouble handling these tiny packets. func WithFragmentPackets() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-f") + return nil } } @@ -22,9 +24,9 @@ func WithFragmentPackets() Option { // 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)) + return func(s *Scanner) error { + s.args = append(s.args, "--mtu", strconv.Itoa(offset)) + return nil } } @@ -43,9 +45,9 @@ func WithMTU(offset int) Option { 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) + return func(s *Scanner) error { + s.args = append(s.args, "-D", decoyList) + return nil } } @@ -54,28 +56,28 @@ func WithDecoys(decoys ...string) Option { // 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 +// that you usually won't receive reply packets back (they are 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) + return func(s *Scanner) error { + s.args = append(s.args, "-S", ip) + return nil } } // 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) + return func(s *Scanner) error { + s.args = append(s.args, "-e", iface) + return nil } } // 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)) + return func(s *Scanner) error { + s.args = append(s.args, "--source-port", strconv.FormatUint(uint64(port), 10)) + return nil } } @@ -83,33 +85,33 @@ func WithSourcePort(port uint16) Option { 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) + return func(s *Scanner) error { + s.args = append(s.args, "--proxies", proxyList) + return nil } } // 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) + return func(s *Scanner) error { + s.args = append(s.args, "--data", data) + return nil } } // 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) + return func(s *Scanner) error { + s.args = append(s.args, "--data-string", data) + return nil } } // 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)) + return func(s *Scanner) error { + s.args = append(s.args, "--data-length", strconv.Itoa(length)) + return nil } } @@ -119,34 +121,34 @@ func WithDataLength(length int) Option { // 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) + return func(s *Scanner) error { + s.args = append(s.args, "--ip-options", options) + return nil } } // WithIPTimeToLive sets the IP time-to-live field of IP packets. func WithIPTimeToLive(ttl int16) Option { - return func(s *Scanner) { + return func(s *Scanner) error { if ttl < 0 || ttl > 255 { - panic("value given to nmap.WithIPTimeToLive() should be between 0 and 255") + return fmt.Errorf("value given to nmap.WithIPTimeToLive() should be between 0 and 255: got %d", ttl) } - s.args = append(s.args, "--ttl") - s.args = append(s.args, fmt.Sprint(ttl)) + s.args = append(s.args, "--ttl", strconv.Itoa(int(ttl))) + return nil } } -// WithSpoofMAC uses the given MAC address for all of the raw +// WithSpoofMAC uses the given MAC address for 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) + return func(s *Scanner) error { + s.args = append(s.args, "--spoof-mac", argument) + return nil } } @@ -156,7 +158,8 @@ func WithSpoofMAC(argument string) Option { // likely coming from a firewall or IDS that didn't bother to // verify the checksum. func WithBadSum() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--badsum") + return nil } } diff --git a/optionsFirewallSpoofing_test.go b/opt_firewall_test.go similarity index 77% rename from optionsFirewallSpoofing_test.go rename to opt_firewall_test.go index 44e5663..d573093 100644 --- a/optionsFirewallSpoofing_test.go +++ b/opt_firewall_test.go @@ -1,19 +1,21 @@ package nmap import ( - "context" - "reflect" "testing" + + "github.com/stretchr/testify/require" ) func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { + baseOptions := nmapContainerOptions(t) + tests := []struct { description string options []Option - expectedPanic string - expectedArgs []string + expectedArgs []string + wantErr require.ErrorAssertionFunc }{ { description: "fragment packets", @@ -25,6 +27,7 @@ func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { expectedArgs: []string{ "-f", }, + wantErr: require.NoError, }, { description: "custom fragment packet size", @@ -37,6 +40,7 @@ func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { "--mtu", "42", }, + wantErr: require.NoError, }, { description: "enable decoys", @@ -58,6 +62,7 @@ func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { "-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", }, + wantErr: require.NoError, }, { description: "spoof IP address", @@ -70,6 +75,7 @@ func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { "-S", "192.168.1.1", }, + wantErr: require.NoError, }, { description: "set interface", @@ -82,6 +88,7 @@ func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { "-e", "eth0", }, + wantErr: require.NoError, }, { description: "set source port", @@ -94,6 +101,7 @@ func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { "--source-port", "65535", }, + wantErr: require.NoError, }, { description: "set proxies", @@ -106,6 +114,7 @@ func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { "--proxies", "4242,8484", }, + wantErr: require.NoError, }, { description: "set custom hex payload", @@ -118,6 +127,7 @@ func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { "--data", "0x8b6c42", }, + wantErr: require.NoError, }, { description: "set custom ascii payload", @@ -130,6 +140,7 @@ func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { "--data-string", "pale brownish", }, + wantErr: require.NoError, }, { description: "set custom random payload length", @@ -142,6 +153,7 @@ func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { "--data-length", "42", }, + wantErr: require.NoError, }, { description: "set custom IP options", @@ -154,6 +166,7 @@ func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { "--ip-options", "S 192.168.1.1 10.0.0.3", }, + wantErr: require.NoError, }, { description: "set custom TTL", @@ -166,15 +179,16 @@ func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { "--ttl", "254", }, + wantErr: require.NoError, }, { - description: "set custom TTL - invalid value should panic", + description: "set custom TTL - invalid value should error", options: []Option{ WithIPTimeToLive(-254), }, - expectedPanic: "value given to nmap.WithIPTimeToLive() should be between 0 and 255", + wantErr: require.Error, }, { description: "spoof mac address", @@ -187,6 +201,7 @@ func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { "--spoof-mac", "08:67:47:0A:78:E4", }, + wantErr: require.NoError, }, { description: "send packets with bad checksum", @@ -198,29 +213,23 @@ func TestFirewallAndIDSEvasionAndSpoofing(t *testing.T) { expectedArgs: []string{ "--badsum", }, + wantErr: require.NoError, }, } 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) - } - }() - } + options := append([]Option{}, baseOptions...) + options = append(options, test.options...) - s, err := NewScanner(context.TODO(), test.options...) + s, err := NewScanner(options...) + + test.wantErr(t, err) if err != nil { - panic(err) + return } - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) - } + assertArgsSuffix(t, s.args, test.expectedArgs) }) } } diff --git a/opt_helper_test.go b/opt_helper_test.go new file mode 100644 index 0000000..ce20bca --- /dev/null +++ b/opt_helper_test.go @@ -0,0 +1,56 @@ +package nmap + +import ( + "os/exec" + "strings" + "testing" + + nmaptesting "github.com/Ullaakut/nmap/v4/internal/testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func nmapContainerOptions(t *testing.T) []Option { + t.Helper() + + ctx := t.Context() + dockerPath, err := exec.LookPath("docker") + if err != nil { + t.Skip("docker is required to run container-based tests") + } + + inspectCmd := exec.CommandContext(ctx, dockerPath, "inspect", "-f", "{{.State.Running}}", nmaptesting.ContainerName) + if output, inspectErr := inspectCmd.Output(); inspectErr == nil { + if strings.TrimSpace(string(output)) == "true" { + return []Option{ + WithBinaryPath(dockerPath), + WithCustomArguments("exec", nmaptesting.ContainerName, "nmap"), + } + } + } + + ctr, err := nmaptesting.StartNetworkMapper() + if err != nil { + t.Skipf("unable to start nmap test container: %v", err) + } + if ctr != nil { + t.Cleanup(func() { + _ = nmaptesting.StopContainer(ctr) + }) + } + + return []Option{ + WithBinaryPath(dockerPath), + WithCustomArguments("exec", nmaptesting.ContainerName, "nmap"), + } +} + +func assertArgsSuffix(t *testing.T, args, expected []string) { + t.Helper() + + require.Len(t, args, len(expected)+3) // accounting for "exec", "", "nmap" + args = args[3:] // strip "exec", "", "nmap" + + require.Equal(t, len(args), len(expected)) + assert.Equal(t, args, expected) +} diff --git a/optionsHostDiscovery.go b/opt_host.go similarity index 68% rename from optionsHostDiscovery.go rename to opt_host.go index d612e59..cf37f36 100644 --- a/optionsHostDiscovery.go +++ b/opt_host.go @@ -1,75 +1,82 @@ 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) { + return func(s *Scanner) error { s.args = append(s.args, "-sL") + return nil } } // WithPingScan sets the discovery mode to simply ping the targets to scan and not scan them. func WithPingScan() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-sn") + return nil } } // WithSkipHostDiscovery disables host discovery and considers all hosts as online. func WithSkipHostDiscovery() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-Pn") + return nil } } // 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. +// If the portList argument is empty, this enables SYN discovery +// for all ports. Otherwise, it is 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)) + return func(s *Scanner) error { + s.args = append(s.args, "-PS"+portList) + return nil } } // 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. +// If the portList argument is empty, this enables ACK discovery +// for all ports. Otherwise, it is 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)) + return func(s *Scanner) error { + s.args = append(s.args, "-PA"+portList) + return nil } } // 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. +// If the portList argument is empty, this enables UDP discovery +// for all ports. Otherwise, it is 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)) + return func(s *Scanner) error { + s.args = append(s.args, "-PU"+portList) + return nil } } // 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 +// If the portList argument is empty, this enables SCTP discovery +// for all ports. Otherwise, it is 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)) + return func(s *Scanner) error { + s.args = append(s.args, "-PY"+portList) + return nil } } @@ -79,8 +86,9 @@ func WithSCTPDiscovery(ports ...string) Option { // Many hosts and firewalls block these packets, so this is usually not // the best for exploring networks. func WithICMPEchoDiscovery() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-PE") + return nil } } @@ -90,8 +98,9 @@ func WithICMPEchoDiscovery() Option { // request packets while forgetting that other ICMP queries can be used // for the same purpose. func WithICMPTimestampDiscovery() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-PP") + return nil } } @@ -101,8 +110,9 @@ func WithICMPTimestampDiscovery() Option { // request packets while forgetting that other ICMP queries can be used // for the same purpose. func WithICMPNetMaskDiscovery() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-PM") + return nil } } @@ -114,24 +124,27 @@ func WithICMPNetMaskDiscovery() Option { func WithIPProtocolPingDiscovery(protocols ...string) Option { protocolList := strings.Join(protocols, ",") - return func(s *Scanner) { - s.args = append(s.args, fmt.Sprintf("-PO%s", protocolList)) + return func(s *Scanner) error { + s.args = append(s.args, "-PO"+protocolList) + return nil } } // WithDisabledDNSResolution disables DNS resolution in the discovery // step of the nmap scan. func WithDisabledDNSResolution() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-n") + return nil } } // WithForcedDNSResolution enforces DNS resolution in the discovery // step of the nmap scan. func WithForcedDNSResolution() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-R") + return nil } } @@ -140,22 +153,24 @@ func WithForcedDNSResolution() Option { 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) + return func(s *Scanner) error { + s.args = append(s.args, "--dns-servers", dnsList) + return nil } } // WithSystemDNS sets the scanner's DNS to the system's DNS. func WithSystemDNS() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--system-dns") + return nil } } // WithTraceRoute enables the tracing of the hop path to each host. func WithTraceRoute() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--traceroute") + return nil } } diff --git a/optionsHostDiscovery_test.go b/opt_host_test.go similarity index 92% rename from optionsHostDiscovery_test.go rename to opt_host_test.go index 8811099..64ceb2f 100644 --- a/optionsHostDiscovery_test.go +++ b/opt_host_test.go @@ -1,12 +1,14 @@ package nmap import ( - "context" - "reflect" "testing" + + "github.com/stretchr/testify/require" ) func TestHostDiscovery(t *testing.T) { + baseOptions := nmapContainerOptions(t) + tests := []struct { description string @@ -239,14 +241,13 @@ func TestHostDiscovery(t *testing.T) { 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) - } + options := append([]Option{}, baseOptions...) + options = append(options, test.options...) + + s, err := NewScanner(options...) + require.NoError(t, err) + + assertArgsSuffix(t, s.args, test.expectedArgs) }) } } diff --git a/optionsMisc.go b/opt_misc.go similarity index 75% rename from optionsMisc.go rename to opt_misc.go index c00171f..00995be 100644 --- a/optionsMisc.go +++ b/opt_misc.go @@ -4,8 +4,9 @@ import "syscall" // WithIPv6Scanning enables the use of IPv6 scanning. func WithIPv6Scanning() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-6") + return nil } } @@ -15,8 +16,9 @@ func WithIPv6Scanning() Option { // 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) { + return func(s *Scanner) error { s.args = append(s.args, "-A") + return nil } } @@ -24,9 +26,9 @@ func WithAggressiveScan() Option { // 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) + return func(s *Scanner) error { + s.args = append(s.args, "--datadir", directoryPath) + return nil } } @@ -34,52 +36,57 @@ func WithDataDir(directoryPath string) Option { // 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) { + return func(s *Scanner) error { s.args = append(s.args, "--send-eth") + return nil } } // WithSendIP makes nmap send packets via raw IP sockets rather than sending // lower level ethernet frames. func WithSendIP() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--send-ip") + return nil } } // WithPrivileged makes nmap assume that the user is fully privileged. func WithPrivileged() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--privileged") + return nil } } // WithUnprivileged makes nmap assume that the user lacks raw socket privileges. func WithUnprivileged() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--unprivileged") + return nil } } // 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) + return func(s *Scanner) error { + s.args = append(s.args, "-oN", outputFileName) + return nil } } // 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) + return func(s *Scanner) error { + s.args = append(s.args, "-oG", outputFileName) + return nil } } -// WithCustomSysProcAttr allows customizing the *syscall.SysProcAttr on the *exec.Cmd instance +// WithCustomSysProcAttr allows customizing the *syscall.SysProcAttr on the *exec.Cmd instance. func WithCustomSysProcAttr(f func(*syscall.SysProcAttr)) Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.modifySysProcAttr = f + return nil } } diff --git a/optionsMisc_test.go b/opt_misc_test.go similarity index 61% rename from optionsMisc_test.go rename to opt_misc_test.go index e7300bc..c4b35af 100644 --- a/optionsMisc_test.go +++ b/opt_misc_test.go @@ -1,12 +1,15 @@ package nmap import ( - "context" - "reflect" + "syscall" "testing" + + "github.com/stretchr/testify/require" ) func TestMiscellaneous(t *testing.T) { + baseOptions := nmapContainerOptions(t) + tests := []struct { description string @@ -92,18 +95,50 @@ func TestMiscellaneous(t *testing.T) { "--unprivileged", }, }, + { + description: "nmap output path", + + options: []Option{ + WithNmapOutput("/tmp/nmap-output"), + }, + + expectedArgs: []string{ + "-oN", "/tmp/nmap-output", + }, + }, + { + description: "nmap grep output", + + options: []Option{ + WithGrepOutput("/tmp/nmap-output"), + }, + + expectedArgs: []string{ + "-oG", "/tmp/nmap-output", + }, + }, + { + description: "nmap grep output", + + options: []Option{ + WithCustomSysProcAttr(func(*syscall.SysProcAttr) {}), + }, + + expectedArgs: []string{ + // No specific args to check for this one. + }, + }, } 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) - } + options := append([]Option{}, baseOptions...) + options = append(options, test.options...) + + s, err := NewScanner(options...) + require.NoError(t, err) + + assertArgsSuffix(t, s.args, test.expectedArgs) }) } } diff --git a/optionsOS.go b/opt_os.go similarity index 82% rename from optionsOS.go rename to opt_os.go index 55b40db..76de67b 100644 --- a/optionsOS.go +++ b/opt_os.go @@ -2,8 +2,9 @@ package nmap // WithOSDetection enables OS detection. func WithOSDetection() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-O") + return nil } } @@ -12,14 +13,16 @@ func WithOSDetection() Option { // 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) { + return func(s *Scanner) error { s.args = append(s.args, "--osscan-limit") + return nil } } // WithOSScanGuess makes nmap attempt to guess the OS more aggressively. func WithOSScanGuess() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--osscan-guess") + return nil } } diff --git a/optionsOS_test.go b/opt_os_test.go similarity index 71% rename from optionsOS_test.go rename to opt_os_test.go index a8f1f2b..07af535 100644 --- a/optionsOS_test.go +++ b/opt_os_test.go @@ -1,12 +1,14 @@ package nmap import ( - "context" - "reflect" "testing" + + "github.com/stretchr/testify/require" ) func TestOSDetection(t *testing.T) { + baseOptions := nmapContainerOptions(t) + tests := []struct { description string @@ -51,14 +53,13 @@ func TestOSDetection(t *testing.T) { for _, test := range tests { t.Run(test.description, func(t *testing.T) { - s, err := NewScanner(context.TODO(), test.options...) - if err != nil { - panic(err) - } + options := append([]Option{}, baseOptions...) + options = append(options, test.options...) + + s, err := NewScanner(options...) + require.NoError(t, err) - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) - } + assertArgsSuffix(t, s.args, test.expectedArgs) }) } } diff --git a/optionsOutput.go b/opt_output.go similarity index 58% rename from optionsOutput.go rename to opt_output.go index dcc95d4..2f19be0 100644 --- a/optionsOutput.go +++ b/opt_output.go @@ -1,72 +1,83 @@ package nmap -import "fmt" +import ( + "errors" + "fmt" + "strconv" +) // WithVerbosity sets and increases the verbosity level of nmap. func WithVerbosity(level int) Option { - - return func(s *Scanner) { + return func(s *Scanner) error { if level < 0 || level > 10 { - panic("value given to nmap.WithVerbosity() should be between 0 and 10") + return fmt.Errorf("value given to nmap.WithVerbosity() should be between 0 and 10: got %d", level) } - s.args = append(s.args, fmt.Sprintf("-v%d", level)) + + s.args = append(s.args, "-v"+strconv.Itoa(level)) + return nil } } // WithDebugging sets and increases the debugging level of nmap. func WithDebugging(level int) Option { - return func(s *Scanner) { + return func(s *Scanner) error { if level < 0 || level > 10 { - panic("value given to nmap.WithDebugging() should be between 0 and 10") + return fmt.Errorf("value given to nmap.WithDebugging() should be between 0 and 10: got %d", level) } - s.args = append(s.args, fmt.Sprintf("-d%d", level)) + + s.args = append(s.args, "-d"+strconv.Itoa(level)) + return nil } } // WithReason makes nmap specify why a port is in a particular state. func WithReason() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--reason") + return nil } } // WithOpenOnly makes nmap only show open ports. func WithOpenOnly() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--open") + return nil } } // WithPacketTrace makes nmap show all packets sent and received. func WithPacketTrace() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--packet-trace") + return nil } } // 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) { + return func(s *Scanner) error { s.args = append(s.args, "--append-output") + return nil } } // 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) + return func(s *Scanner) error { + s.args = append(s.args, "--resume", filePath) + return nil } } // 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) + return func(s *Scanner) error { + s.args = append(s.args, "--stylesheet", stylesheetPath) + return nil } } @@ -74,21 +85,29 @@ func WithStylesheet(stylesheetPath string) Option { // XML output to HTML. The stylesheet can be found at // https://nmap.org/svn/docs/nmap.xsl func WithWebXML() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--webxml") + return nil } } // WithNoStylesheet prevents the use of XSL stylesheets with the XML output. func WithNoStylesheet() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--no-stylesheet") + return nil } } -// WithNonInteractive disable runtime interactions via keyboard +// WithNonInteractive disable runtime interactions via keyboard. func WithNonInteractive() Option { - return func(s *Scanner) { - s.args = append(s.Args(), "--noninteractive") + return func(s *Scanner) error { + if s.progressHandler != nil { + return errors.New("non-interactive mode cannot be used with progress updates") + } + + s.interactive = false + s.args = append(s.args, "--noninteractive") + return nil } } diff --git a/optionsOutput_test.go b/opt_output_test.go similarity index 88% rename from optionsOutput_test.go rename to opt_output_test.go index 4c110d6..aba69df 100644 --- a/optionsOutput_test.go +++ b/opt_output_test.go @@ -1,12 +1,14 @@ package nmap import ( - "context" - "reflect" "testing" + + "github.com/stretchr/testify/require" ) func TestOutput(t *testing.T) { + baseOptions := nmapContainerOptions(t) + tests := []struct { description string @@ -153,14 +155,13 @@ func TestOutput(t *testing.T) { 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) - } + options := append([]Option{}, baseOptions...) + options = append(options, test.options...) + + s, err := NewScanner(options...) + require.NoError(t, err) + + assertArgsSuffix(t, s.args, test.expectedArgs) }) } } diff --git a/optionsPortOrder.go b/opt_ports.go similarity index 68% rename from optionsPortOrder.go rename to opt_ports.go index fe52093..a4105ed 100644 --- a/optionsPortOrder.go +++ b/opt_ports.go @@ -2,6 +2,7 @@ package nmap import ( "fmt" + "strconv" "strings" ) @@ -9,9 +10,9 @@ import ( func WithPorts(ports ...string) Option { portList := strings.Join(ports, ",") - return func(s *Scanner) { + return func(s *Scanner) error { // Find if any port is set. - var place = -1 + place := -1 for p, value := range s.args { if value == "-p" { place = p @@ -27,10 +28,12 @@ func WithPorts(ports ...string) Option { portList = s.args[place+1] + "," + portList } s.args[place+1] = portList - } else { - s.args = append(s.args, "-p") - s.args = append(s.args, portList) + return nil } + + s.args = append(s.args, "-p", portList) + + return nil } } @@ -38,45 +41,47 @@ func WithPorts(ports ...string) Option { 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) + return func(s *Scanner) error { + s.args = append(s.args, "--exclude-ports", portList) + return nil } } // WithFastMode makes the scan faster by scanning fewer ports than the default scan. func WithFastMode() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-F") + return nil } } // WithConsecutivePortScanning makes the scan go through ports consecutively instead of // picking them out randomly. func WithConsecutivePortScanning() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-r") + return nil } } // 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)) + return func(s *Scanner) error { + s.args = append(s.args, "--top-ports", strconv.Itoa(number)) + return nil } } // 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) { + return func(s *Scanner) error { if ratio < 0 || ratio > 1 { - panic("value given to nmap.WithPortRatio() should be between 0 and 1") + return fmt.Errorf("value given to nmap.WithPortRatio() should be between 0 and 1: got %f", ratio) } - s.args = append(s.args, "--port-ratio") - s.args = append(s.args, fmt.Sprintf("%.1f", ratio)) + s.args = append(s.args, "--port-ratio", fmt.Sprintf("%.1f", ratio)) + return nil } } diff --git a/optionsPortOrder_test.go b/opt_ports_test.go similarity index 71% rename from optionsPortOrder_test.go rename to opt_ports_test.go index f90e5db..539a65a 100644 --- a/optionsPortOrder_test.go +++ b/opt_ports_test.go @@ -1,19 +1,21 @@ package nmap import ( - "context" - "reflect" "testing" + + "github.com/stretchr/testify/require" ) func TestPortSpecAndScanOrder(t *testing.T) { + baseOptions := nmapContainerOptions(t) + tests := []struct { description string options []Option - expectedPanic string - expectedArgs []string + expectedArgs []string + wantErr require.ErrorAssertionFunc }{ { description: "specify ports to scan", @@ -27,6 +29,7 @@ func TestPortSpecAndScanOrder(t *testing.T) { "-p", "554,8554,80-81", }, + wantErr: require.NoError, }, { description: "exclude ports to scan", @@ -39,6 +42,7 @@ func TestPortSpecAndScanOrder(t *testing.T) { "--exclude-ports", "554,8554", }, + wantErr: require.NoError, }, { description: "fast mode - scan fewer ports than the default scan", @@ -50,6 +54,7 @@ func TestPortSpecAndScanOrder(t *testing.T) { expectedArgs: []string{ "-F", }, + wantErr: require.NoError, }, { description: "consecutive port scanning", @@ -61,6 +66,7 @@ func TestPortSpecAndScanOrder(t *testing.T) { expectedArgs: []string{ "-r", }, + wantErr: require.NoError, }, { description: "scan most commonly open ports", @@ -73,6 +79,7 @@ func TestPortSpecAndScanOrder(t *testing.T) { "--top-ports", "5", }, + wantErr: require.NoError, }, { description: "scan most commonly open ports given a ratio - should be rounded to 0.4", @@ -85,6 +92,7 @@ func TestPortSpecAndScanOrder(t *testing.T) { "--port-ratio", "0.4", }, + wantErr: require.NoError, }, { description: "scan most commonly open ports given a ratio - should be invalid and panic", @@ -93,30 +101,23 @@ func TestPortSpecAndScanOrder(t *testing.T) { WithPortRatio(2), }, - expectedPanic: "value given to nmap.WithPortRatio() should be between 0 and 1", + wantErr: require.Error, }, } 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) - } - }() - } + options := append([]Option{}, baseOptions...) + options = append(options, test.options...) - s, err := NewScanner(context.TODO(), test.options...) + s, err := NewScanner(options...) + + test.wantErr(t, err) if err != nil { - panic(err) + return } - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) - } + assertArgsSuffix(t, s.args, test.expectedArgs) }) } } diff --git a/opt_progress.go b/opt_progress.go new file mode 100644 index 0000000..87ef5fb --- /dev/null +++ b/opt_progress.go @@ -0,0 +1,35 @@ +package nmap + +import ( + "errors" + "fmt" + "time" +) + +// WithProgress enables live progress updates by parsing elements +// from the XML stream. The interval controls nmap's --stats-every option. +// +// NOTE: progress updates require XML output on stdout. Using ToFile disables +// the live progress stream. +func WithProgress(interval time.Duration, handler func(TaskProgress)) Option { + return func(s *Scanner) error { + if handler == nil { + return errors.New("progress handler must not be nil") + } + if s.toFile != nil { + return errors.New("progress updates require XML on stdout; do not use WithProgress with ToFile") + } + if !s.interactive { + return errors.New("progress updates require interactive terminal; cannot use WithProgress in non-interactive mode") + } + + formatted, err := formatNmapDuration(interval) + if err != nil { + return fmt.Errorf("format progress interval: %w", err) + } + + s.args = append(s.args, "--stats-every", formatted) + s.progressHandler = handler + return nil + } +} diff --git a/optionsScanTechniques.go b/opt_scan.go similarity index 69% rename from optionsScanTechniques.go rename to opt_scan.go index 0a93d6f..8aa014a 100644 --- a/optionsScanTechniques.go +++ b/opt_scan.go @@ -1,13 +1,17 @@ package nmap -import "fmt" +import ( + "fmt" + "strings" +) // 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) { + return func(s *Scanner) error { s.args = append(s.args, "-sS") + return nil } } @@ -16,20 +20,22 @@ func WithSYNScan() Option { // packet privileges. Target machines are likely to log these // connections. func WithConnectScan() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-sT") + return nil } } // 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. +// When scanning unfiltered systems, open and closed ports both +// return an 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) { + return func(s *Scanner) error { s.args = append(s.args, "-sA") + return nil } } @@ -40,17 +46,19 @@ func WithACKScan() Option { // from closed ones, rather than always printing unfiltered when a RST // is returned. func WithWindowScan() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-sW") + return nil } } // WithMaimonScan sends the same packets as NULL, FIN, and Xmas scans, -// except that the probe is FIN/ACK. Many BSD-derived systems will drop +// except that the probe is FIN/ACK. Many BSD-derived systems drop // these packets if the port is open. func WithMaimonScan() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-sM") + return nil } } @@ -59,9 +67,12 @@ func WithMaimonScan() Option { // to check both protocols during the same run. // UDP scanning is generally slower than TCP, but should not // be ignored. +// +// NOTE: UDP scans might require elevated privileges. func WithUDPScan() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-sU") + return nil } } @@ -71,8 +82,9 @@ func WithUDPScan() Option { // 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) { + return func(s *Scanner) error { s.args = append(s.args, "-sN") + return nil } } @@ -82,8 +94,9 @@ func WithTCPNullScan() Option { // 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) { + return func(s *Scanner) error { s.args = append(s.args, "-sF") + return nil } } @@ -93,8 +106,9 @@ func WithTCPFINScan() Option { // 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) { + return func(s *Scanner) error { s.args = append(s.args, "-sX") + return nil } } @@ -116,15 +130,47 @@ const ( ) // WithTCPScanFlags sets the scan technique to use custom TCP flags. +// +// NOTE: Nmap supports specifying TCP scan flags either as a decimal value or as a +// string (e.g. "SYNACK"). However, the decimal form is limited to 0–255 because +// it maps strictly to the 8-bit TCP flags field (FIN through CWR). The NS flag +// does not live in this byte; it occupies a separate bit in the TCP reserved +// field and therefore cannot be represented in a single 8-bit integer. +// +// As a result, any flag combination involving NS (and, more generally, full +// TCP control-bit manipulation) can only be expressed using the string form. +// We therefore always emit string-based scan flags to ensure correctness and +// full feature coverage. func WithTCPScanFlags(flags ...TCPFlag) Option { - var total int - for _, flag := range flags { - total += int(flag) + var flag strings.Builder + for _, v := range flags { + switch v { + case FlagNULL: + continue + case FlagFIN: + flag.WriteString("FIN") + case FlagSYN: + flag.WriteString("SYN") + case FlagRST: + flag.WriteString("RST") + case FlagPSH: + flag.WriteString("PSH") + case FlagACK: + flag.WriteString("ACK") + case FlagURG: + flag.WriteString("URG") + case FlagECE: + flag.WriteString("ECE") + case FlagCWR: + flag.WriteString("CWR") + case FlagNS: + flag.WriteString("NS") + } } - return func(s *Scanner) { - s.args = append(s.args, "--scanflags") - s.args = append(s.args, fmt.Sprintf("%x", total)) + return func(s *Scanner) error { + s.args = append(s.args, "--scanflags="+flag.String()) + return nil } } @@ -134,14 +180,16 @@ func WithTCPScanFlags(flags ...TCPFlag) Option { // this scan type permits mapping out IP-based trust relationships // between machines. func WithIdleScan(zombieHost string, probePort int) Option { - return func(s *Scanner) { + return func(s *Scanner) error { 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) + return nil } + + s.args = append(s.args, zombieHost) + return nil } } @@ -152,8 +200,9 @@ func WithIdleScan(zombieHost string, probePort int) Option { // Like SYN scan, INIT scan is relatively unobtrusive and stealthy, // since it never completes SCTP associations. func WithSCTPInitScan() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-sY") + return nil } } @@ -163,8 +212,9 @@ func WithSCTPInitScan() Option { // 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) { + return func(s *Scanner) error { s.args = append(s.args, "-sZ") + return nil } } @@ -174,20 +224,21 @@ func WithSCTPCookieEchoScan() Option { // 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) { + return func(s *Scanner) error { s.args = append(s.args, "-sO") + return nil } } -// WithFTPBounceScan sets the scan technique to use the an FTP relay host. +// WithFTPBounceScan sets the scan technique to use 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) +func WithFTPBounceScan(ftpRelayHost string) Option { + return func(s *Scanner) error { + s.args = append(s.args, "-b", ftpRelayHost) + return nil } } diff --git a/optionsScanTechniques_test.go b/opt_scan_test.go similarity index 82% rename from optionsScanTechniques_test.go rename to opt_scan_test.go index 0d6f9b5..40ed8d8 100644 --- a/optionsScanTechniques_test.go +++ b/opt_scan_test.go @@ -1,12 +1,14 @@ package nmap import ( - "context" - "reflect" "testing" + + "github.com/stretchr/testify/require" ) func TestScanTechniques(t *testing.T) { + baseOptions := nmapContainerOptions(t) + tests := []struct { description string @@ -121,8 +123,18 @@ func TestScanTechniques(t *testing.T) { }, expectedArgs: []string{ - "--scanflags", - "11", + "--scanflags=ACKFIN", + }, + }, + { + description: "TCP scan flags ALL flags", + + options: []Option{ + WithTCPScanFlags(FlagNS, FlagCWR, FlagECE, FlagURG, FlagACK, FlagPSH, FlagRST, FlagSYN, FlagFIN, FlagNULL), + }, + + expectedArgs: []string{ + "--scanflags=NSCWRECEURGACKPSHRSTSYNFIN", }, }, { @@ -198,14 +210,13 @@ func TestScanTechniques(t *testing.T) { 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) - } + options := append([]Option{}, baseOptions...) + options = append(options, test.options...) + + s, err := NewScanner(options...) + require.NoError(t, err) + + assertArgsSuffix(t, s.args, test.expectedArgs) }) } } diff --git a/optionsScriptScan.go b/opt_script.go similarity index 59% rename from optionsScriptScan.go rename to opt_script.go index 5f19e02..4e81139 100644 --- a/optionsScriptScan.go +++ b/opt_script.go @@ -2,6 +2,7 @@ package nmap import ( "fmt" + "slices" "strings" "time" ) @@ -11,8 +12,9 @@ import ( // this category are considered intrusive and should not be run against a target // network without permission. func WithDefaultScript() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-sC") + return nil } } @@ -21,63 +23,71 @@ func WithDefaultScript() Option { func WithScripts(scripts ...string) Option { scriptList := strings.Join(scripts, ",") - return func(s *Scanner) { - s.args = append(s.args, fmt.Sprintf("--script=%s", scriptList)) + return func(s *Scanner) error { + s.args = append(s.args, "--script="+scriptList) + return nil } } -// WithScriptArguments provides arguments for scripts. If a value is the empty string, the key will be used as a flag. +// WithScriptArguments provides arguments for scripts. +// If a value is the empty string, the key is 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 + scriptArgs := make([]string, 0, len(arguments)) for key, value := range arguments { - str := "" - if value == "" { - str = key - } else { + str := key + if value != "" { str = fmt.Sprintf("%s=%s", key, value) } - argList = strings.Join([]string{argList, str}, ",") + scriptArgs = append(scriptArgs, str) } - argList = strings.TrimLeft(argList, ",") + // Ensure consistent ordering. + slices.Sort(scriptArgs) + args := strings.Join(scriptArgs, ",") - return func(s *Scanner) { - s.args = append(s.args, fmt.Sprintf("--script-args=%s", argList)) + return func(s *Scanner) error { + s.args = append(s.args, "--script-args="+args) + return nil } } // 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)) + return func(s *Scanner) error { + s.args = append(s.args, "--script-args-file="+inputFilePath) + return nil } } // WithScriptTrace makes the scripts show all data sent and received. func WithScriptTrace() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--script-trace") + return nil } } // WithScriptUpdateDB updates the script database. func WithScriptUpdateDB() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--script-updatedb") + return nil } } // WithScriptTimeout sets the script timeout. func WithScriptTimeout(timeout time.Duration) Option { - milliseconds := timeout.Round(time.Nanosecond).Nanoseconds() / 1000000 + return func(s *Scanner) error { + formatted, err := formatNmapDuration(timeout) + if err != nil { + return fmt.Errorf("format script timeout: %w", err) + } - return func(s *Scanner) { - s.args = append(s.args, "--script-timeout") - s.args = append(s.args, fmt.Sprintf("%dms", int(milliseconds))) + s.args = append(s.args, "--script-timeout", formatted) + return nil } } diff --git a/optionsScriptScan_test.go b/opt_script_test.go similarity index 66% rename from optionsScriptScan_test.go rename to opt_script_test.go index 308f28c..016cc4f 100644 --- a/optionsScriptScan_test.go +++ b/opt_script_test.go @@ -1,22 +1,23 @@ package nmap import ( - "context" - "reflect" - "strings" "testing" "time" + + "github.com/stretchr/testify/require" ) func TestScriptScan(t *testing.T) { + baseOptions := nmapContainerOptions(t) + tests := []struct { description string - targets []string - options []Option - unorderedArgs bool + targets []string + options []Option expectedArgs []string + wantErr require.ErrorAssertionFunc }{ { description: "default script scan", @@ -28,6 +29,7 @@ func TestScriptScan(t *testing.T) { expectedArgs: []string{ "-sC", }, + wantErr: require.NoError, }, { description: "custom script list", @@ -39,6 +41,7 @@ func TestScriptScan(t *testing.T) { expectedArgs: []string{ "--script=./scripts/,/etc/nmap/nse/scripts", }, + wantErr: require.NoError, }, { description: "script arguments", @@ -53,16 +56,10 @@ func TestScriptScan(t *testing.T) { }), }, - unorderedArgs: true, - expectedArgs: []string{ - "--script-args=", - "user=foo", - "pass=\",{}=bar\"", - "whois={whodb=nofollow+ripe}", - "xmpp-info.server_name=localhost", - "vulns.showall", + `--script-args=pass=",{}=bar",user=foo,vulns.showall,whois={whodb=nofollow+ripe},xmpp-info.server_name=localhost`, }, + wantErr: require.NoError, }, { description: "script arguments file", @@ -74,6 +71,7 @@ func TestScriptScan(t *testing.T) { expectedArgs: []string{ "--script-args-file=/script_args.txt", }, + wantErr: require.NoError, }, { description: "enable script trace", @@ -85,6 +83,7 @@ func TestScriptScan(t *testing.T) { expectedArgs: []string{ "--script-trace", }, + wantErr: require.NoError, }, { description: "update script database", @@ -96,6 +95,7 @@ func TestScriptScan(t *testing.T) { expectedArgs: []string{ "--script-updatedb", }, + wantErr: require.NoError, }, { description: "set script timeout", @@ -106,30 +106,33 @@ func TestScriptScan(t *testing.T) { expectedArgs: []string{ "--script-timeout", - "40000ms", + "40s", }, + wantErr: require.NoError, + }, + { + description: "set invalid script timeout", + + options: []Option{ + WithScriptTimeout(-40 * time.Second), + }, + + wantErr: require.Error, }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { - s, err := NewScanner(context.TODO(), test.options...) - if err != nil { - panic(err) - } + options := append([]Option{}, baseOptions...) + options = append(options, test.options...) - 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) - } - } + s, err := NewScanner(options...) + test.wantErr(t, err) + if err != nil { return } - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) - } + assertArgsSuffix(t, s.args, test.expectedArgs) }) } } diff --git a/optionsServiceVersion.go b/opt_service.go similarity index 69% rename from optionsServiceVersion.go rename to opt_service.go index 98a1966..c4db824 100644 --- a/optionsServiceVersion.go +++ b/opt_service.go @@ -1,12 +1,16 @@ package nmap -import "fmt" +import ( + "fmt" + "strconv" +) // WithServiceInfo enables the probing of open ports to determine service and version // info. func WithServiceInfo() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "-sV") + return nil } } @@ -14,8 +18,9 @@ func WithServiceInfo() Option { // including port 9100 which is excluded by default. // In other words, version detection is performed on all ports regardles of any Exclude directive. func WithVersionDetectionOnAllPorts() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--allports") + return nil } } @@ -24,31 +29,33 @@ func WithVersionDetectionOnAllPorts() Option { // 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) { + return func(s *Scanner) error { if intensity < 0 || intensity > 9 { - panic("value given to nmap.WithVersionIntensity() should be between 0 and 9") + return fmt.Errorf("value given to nmap.WithVersionIntensity() should be between 0 and 9, got %d", intensity) } - s.args = append(s.args, "--version-intensity") - s.args = append(s.args, fmt.Sprint(intensity)) + s.args = append(s.args, "--version-intensity", strconv.Itoa(int(intensity))) + return nil } } // 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 +// open ports to get version information to 2. This makes version scanning much // faster, but slightly less likely to identify services. func WithVersionLight() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--version-light") + return nil } } // 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 +// open ports to get version information to 9. This ensures that every single // probe is attempted against each port. func WithVersionAll() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--version-all") + return nil } } @@ -56,7 +63,8 @@ func WithVersionAll() Option { // version scanning is doing. // TODO: See how this works along with XML output. func WithVersionTrace() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--version-trace") + return nil } } diff --git a/optionsServiceVersion_test.go b/opt_service_test.go similarity index 69% rename from optionsServiceVersion_test.go rename to opt_service_test.go index c175cf6..9b4a99e 100644 --- a/optionsServiceVersion_test.go +++ b/opt_service_test.go @@ -1,19 +1,21 @@ package nmap import ( - "context" - "reflect" "testing" + + "github.com/stretchr/testify/require" ) func TestServiceDetection(t *testing.T) { + baseOptions := nmapContainerOptions(t) + tests := []struct { description string options []Option - expectedPanic string - expectedArgs []string + expectedArgs []string + wantErr require.ErrorAssertionFunc }{ { description: "service detection", @@ -25,6 +27,7 @@ func TestServiceDetection(t *testing.T) { expectedArgs: []string{ "-sV", }, + wantErr: require.NoError, }, { description: "service detection on all ports", @@ -36,6 +39,7 @@ func TestServiceDetection(t *testing.T) { expectedArgs: []string{ "--allports", }, + wantErr: require.NoError, }, { description: "service detection custom intensity", @@ -48,6 +52,7 @@ func TestServiceDetection(t *testing.T) { "--version-intensity", "1", }, + wantErr: require.NoError, }, { description: "service detection custom intensity - should panic since not between 0 and 9", @@ -56,7 +61,7 @@ func TestServiceDetection(t *testing.T) { WithVersionIntensity(42), }, - expectedPanic: "value given to nmap.WithVersionIntensity() should be between 0 and 9", + wantErr: require.Error, }, { description: "service detection light intensity", @@ -68,6 +73,7 @@ func TestServiceDetection(t *testing.T) { expectedArgs: []string{ "--version-light", }, + wantErr: require.NoError, }, { description: "service detection highest intensity", @@ -79,6 +85,7 @@ func TestServiceDetection(t *testing.T) { expectedArgs: []string{ "--version-all", }, + wantErr: require.NoError, }, { description: "service detection enable trace", @@ -90,29 +97,22 @@ func TestServiceDetection(t *testing.T) { expectedArgs: []string{ "--version-trace", }, + wantErr: require.NoError, }, } 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) - } - }() - } + options := append([]Option{}, baseOptions...) + options = append(options, test.options...) - s, err := NewScanner(context.TODO(), test.options...) + s, err := NewScanner(options...) + test.wantErr(t, err) if err != nil { - panic(err) + return } - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) - } + assertArgsSuffix(t, s.args, test.expectedArgs) }) } } diff --git a/optionsTargetSpecification.go b/opt_target.go similarity index 67% rename from optionsTargetSpecification.go rename to opt_target.go index f86de5e..3846b55 100644 --- a/optionsTargetSpecification.go +++ b/opt_target.go @@ -1,14 +1,15 @@ package nmap import ( - "fmt" + "strconv" "strings" ) // WithTargets sets the target of a scanner. func WithTargets(targets ...string) Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, targets...) + return nil } } @@ -16,33 +17,33 @@ func WithTargets(targets ...string) Option { func WithTargetExclusions(targets ...string) Option { targetList := strings.Join(targets, ",") - return func(s *Scanner) { - s.args = append(s.args, "--exclude") - s.args = append(s.args, targetList) + return func(s *Scanner) error { + s.args = append(s.args, "--exclude", targetList) + return nil } } // 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) + return func(s *Scanner) error { + s.args = append(s.args, "-iL", inputFileName) + return nil } } // 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) + return func(s *Scanner) error { + s.args = append(s.args, "--excludefile", inputFileName) + return nil } } // 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)) + return func(s *Scanner) error { + s.args = append(s.args, "-iR", strconv.Itoa(randomTargets)) + return nil } } @@ -52,7 +53,8 @@ func WithRandomTargets(randomTargets int) Option { // ranges overlap or different hostnames resolve to the same // address. func WithUnique() Option { - return func(s *Scanner) { + return func(s *Scanner) error { s.args = append(s.args, "--unique") + return nil } } diff --git a/optionsTargetSpecification_test.go b/opt_target_test.go similarity index 85% rename from optionsTargetSpecification_test.go rename to opt_target_test.go index 2096625..1cba05f 100644 --- a/optionsTargetSpecification_test.go +++ b/opt_target_test.go @@ -1,12 +1,14 @@ package nmap import ( - "context" - "reflect" "testing" + + "github.com/stretchr/testify/require" ) func TestTargetSpecification(t *testing.T) { + baseOptions := nmapContainerOptions(t) + tests := []struct { description string @@ -113,14 +115,13 @@ func TestTargetSpecification(t *testing.T) { 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) - } + options := append([]Option{}, baseOptions...) + options = append(options, test.options...) + + s, err := NewScanner(options...) + require.NoError(t, err) + + assertArgsSuffix(t, s.args, test.expectedArgs) }) } } diff --git a/opt_timing.go b/opt_timing.go new file mode 100644 index 0000000..ec1e4dd --- /dev/null +++ b/opt_timing.go @@ -0,0 +1,191 @@ +package nmap + +import ( + "fmt" + "strconv" + "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) error { + s.args = append(s.args, "-T"+strconv.Itoa(int(timing))) + return nil + } +} + +// WithMinHostgroup sets the minimal parallel host scan group size. +func WithMinHostgroup(size int) Option { + return func(s *Scanner) error { + s.args = append(s.args, "--min-hostgroup", strconv.Itoa(size)) + return nil + } +} + +// WithMaxHostgroup sets the maximal parallel host scan group size. +func WithMaxHostgroup(size int) Option { + return func(s *Scanner) error { + s.args = append(s.args, "--max-hostgroup", strconv.Itoa(size)) + return nil + } +} + +// WithMinParallelism sets the minimal number of parallel probes. +func WithMinParallelism(probes int) Option { + return func(s *Scanner) error { + s.args = append(s.args, "--min-parallelism", strconv.Itoa(probes)) + return nil + } +} + +// WithMaxParallelism sets the maximal number of parallel probes. +func WithMaxParallelism(probes int) Option { + return func(s *Scanner) error { + s.args = append(s.args, "--max-parallelism", strconv.Itoa(probes)) + return nil + } +} + +// WithMinRTTTimeout sets the minimal probe round trip time. +func WithMinRTTTimeout(roundTripTime time.Duration) Option { + return func(s *Scanner) error { + formatted, err := formatNmapDuration(roundTripTime) + if err != nil { + return fmt.Errorf("format round trip time: %w", err) + } + + s.args = append(s.args, "--min-rtt-timeout", formatted) + return nil + } +} + +// WithMaxRTTTimeout sets the maximal probe round trip time. +func WithMaxRTTTimeout(roundTripTime time.Duration) Option { + return func(s *Scanner) error { + formatted, err := formatNmapDuration(roundTripTime) + if err != nil { + return fmt.Errorf("format round trip time: %w", err) + } + + s.args = append(s.args, "--max-rtt-timeout", formatted) + return nil + } +} + +// WithInitialRTTTimeout sets the initial probe round trip time. +func WithInitialRTTTimeout(roundTripTime time.Duration) Option { + return func(s *Scanner) error { + formatted, err := formatNmapDuration(roundTripTime) + if err != nil { + return fmt.Errorf("format round trip time: %w", err) + } + + s.args = append(s.args, "--initial-rtt-timeout", formatted) + return nil + } +} + +// WithMaxRetries sets the maximal number of port scan probe retransmissions. +func WithMaxRetries(tries int) Option { + return func(s *Scanner) error { + s.args = append(s.args, "--max-retries", strconv.Itoa(tries)) + return nil + } +} + +// WithHostTimeout sets the time after which nmap should give up on a target host. +func WithHostTimeout(timeout time.Duration) Option { + return func(s *Scanner) error { + formatted, err := formatNmapDuration(timeout) + if err != nil { + return fmt.Errorf("format host timeout: %w", err) + } + + s.args = append(s.args, "--host-timeout", formatted) + return nil + } +} + +// WithScanDelay sets the minimum time to wait between each probe sent to a host. +func WithScanDelay(delay time.Duration) Option { + return func(s *Scanner) error { + formatted, err := formatNmapDuration(delay) + if err != nil { + return fmt.Errorf("format scan delay: %w", err) + } + + s.args = append(s.args, "--scan-delay", formatted) + return nil + } +} + +// WithMaxScanDelay sets the maximum time to wait between each probe sent to a host. +func WithMaxScanDelay(delay time.Duration) Option { + return func(s *Scanner) error { + formatted, err := formatNmapDuration(delay) + if err != nil { + return fmt.Errorf("format scan delay: %w", err) + } + + s.args = append(s.args, "--max-scan-delay", formatted) + return nil + } +} + +// WithMinRate sets the minimal number of packets sent per second. +func WithMinRate(packetsPerSecond int) Option { + return func(s *Scanner) error { + s.args = append(s.args, "--min-rate", strconv.Itoa(packetsPerSecond)) + return nil + } +} + +// WithMaxRate sets the maximal number of packets sent per second. +func WithMaxRate(packetsPerSecond int) Option { + return func(s *Scanner) error { + s.args = append(s.args, "--max-rate", strconv.Itoa(packetsPerSecond)) + return nil + } +} + +func formatNmapDuration(duration time.Duration) (string, error) { + if duration < 0 { + return "", fmt.Errorf("duration must be non-negative, got %s", duration) + } + if duration == 0 { + return "0s", nil + } + if duration%time.Millisecond != 0 { + return "", fmt.Errorf("duration must be a multiple of 1ms, got %s", duration) + } + + switch { + case duration%time.Hour == 0: + return fmt.Sprintf("%dh", duration/time.Hour), nil + case duration%time.Minute == 0: + return fmt.Sprintf("%dm", duration/time.Minute), nil + case duration%time.Second == 0: + return fmt.Sprintf("%ds", duration/time.Second), nil + default: + return fmt.Sprintf("%dms", duration/time.Millisecond), nil + } +} diff --git a/optionsTimingPerformance_test.go b/opt_timing_test.go similarity index 62% rename from optionsTimingPerformance_test.go rename to opt_timing_test.go index 8b53321..bdf3d62 100644 --- a/optionsTimingPerformance_test.go +++ b/opt_timing_test.go @@ -1,19 +1,22 @@ package nmap import ( - "context" - "reflect" "testing" "time" + + "github.com/stretchr/testify/require" ) func TestTimingAndPerformance(t *testing.T) { + baseOptions := nmapContainerOptions(t) + tests := []struct { description string options []Option expectedArgs []string + wantErr require.ErrorAssertionFunc }{ { description: "set timing template", @@ -25,18 +28,7 @@ func TestTimingAndPerformance(t *testing.T) { expectedArgs: []string{ "-T4", }, - }, - { - description: "set stats every", - - options: []Option{ - WithStatsEvery("5s"), - }, - - expectedArgs: []string{ - "--stats-every", - "5s", - }, + wantErr: require.NoError, }, { description: "set min hostgroup", @@ -49,6 +41,7 @@ func TestTimingAndPerformance(t *testing.T) { "--min-hostgroup", "42", }, + wantErr: require.NoError, }, { description: "set max hostgroup", @@ -61,6 +54,7 @@ func TestTimingAndPerformance(t *testing.T) { "--max-hostgroup", "42", }, + wantErr: require.NoError, }, { description: "set min parallelism", @@ -73,6 +67,7 @@ func TestTimingAndPerformance(t *testing.T) { "--min-parallelism", "42", }, + wantErr: require.NoError, }, { description: "set max parallelism", @@ -85,6 +80,7 @@ func TestTimingAndPerformance(t *testing.T) { "--max-parallelism", "42", }, + wantErr: require.NoError, }, { description: "set min rtt-timeout", @@ -95,8 +91,18 @@ func TestTimingAndPerformance(t *testing.T) { expectedArgs: []string{ "--min-rtt-timeout", - "120000ms", + "2m", + }, + wantErr: require.NoError, + }, + { + description: "set invalid rtt-timeout", + + options: []Option{ + WithMinRTTTimeout(-2 * time.Minute), }, + + wantErr: require.Error, }, { description: "set max rtt-timeout", @@ -107,8 +113,18 @@ func TestTimingAndPerformance(t *testing.T) { expectedArgs: []string{ "--max-rtt-timeout", - "28800000ms", + "8h", + }, + wantErr: require.NoError, + }, + { + description: "set invalid max rtt-timeout", + + options: []Option{ + WithMaxRTTTimeout(-8 * time.Hour), }, + + wantErr: require.Error, }, { description: "set initial rtt-timeout", @@ -119,8 +135,18 @@ func TestTimingAndPerformance(t *testing.T) { expectedArgs: []string{ "--initial-rtt-timeout", - "28800000ms", + "8h", + }, + wantErr: require.NoError, + }, + { + description: "set invalid initial rtt-timeout", + + options: []Option{ + WithInitialRTTTimeout(-8 * time.Hour), }, + + wantErr: require.Error, }, { description: "set max retries", @@ -133,6 +159,7 @@ func TestTimingAndPerformance(t *testing.T) { "--max-retries", "42", }, + wantErr: require.NoError, }, { description: "set host timeout", @@ -143,8 +170,18 @@ func TestTimingAndPerformance(t *testing.T) { expectedArgs: []string{ "--host-timeout", - "42000ms", + "42s", + }, + wantErr: require.NoError, + }, + { + description: "set invalid host timeout", + + options: []Option{ + WithHostTimeout(-42 * time.Second), }, + + wantErr: require.Error, }, { description: "set scan delay", @@ -157,6 +194,16 @@ func TestTimingAndPerformance(t *testing.T) { "--scan-delay", "42ms", }, + wantErr: require.NoError, + }, + { + description: "set invalid scan delay", + + options: []Option{ + WithScanDelay(-42 * time.Millisecond), + }, + + wantErr: require.Error, }, { description: "set max scan delay", @@ -169,6 +216,16 @@ func TestTimingAndPerformance(t *testing.T) { "--max-scan-delay", "42ms", }, + wantErr: require.NoError, + }, + { + description: "set invalid max scan delay", + + options: []Option{ + WithMaxScanDelay(-42 * time.Millisecond), + }, + + wantErr: require.Error, }, { description: "set min rate", @@ -181,6 +238,7 @@ func TestTimingAndPerformance(t *testing.T) { "--min-rate", "42", }, + wantErr: require.NoError, }, { description: "set max rate", @@ -193,19 +251,22 @@ func TestTimingAndPerformance(t *testing.T) { "--max-rate", "42", }, + wantErr: require.NoError, }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { - s, err := NewScanner(context.TODO(), test.options...) + options := append([]Option{}, baseOptions...) + options = append(options, test.options...) + + s, err := NewScanner(options...) + test.wantErr(t, err) if err != nil { - panic(err) + return } - if !reflect.DeepEqual(s.args, test.expectedArgs) { - t.Errorf("unexpected arguments, expected %s got %s", test.expectedArgs, s.args) - } + assertArgsSuffix(t, s.args, test.expectedArgs) }) } } diff --git a/optionsTimingPerformance.go b/optionsTimingPerformance.go deleted file mode 100644 index c29b533..0000000 --- a/optionsTimingPerformance.go +++ /dev/null @@ -1,156 +0,0 @@ -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/pkg/osfamilies/os_families.go b/pkg/osfamilies/os_families.go index 0047776..53fec97 100644 --- a/pkg/osfamilies/os_families.go +++ b/pkg/osfamilies/os_families.go @@ -480,7 +480,7 @@ const ( Sagem OSFamily = "Sagem" Sagemcom OSFamily = "Sagemcom" Samsung OSFamily = "Samsung" - Sandstrom OSFamily = "Sandstrom" + Sandstorm OSFamily = "Sandstorm" Sanyo OSFamily = "Sanyo" Sapling OSFamily = "Sapling" Satel OSFamily = "Satel" diff --git a/scan_async.go b/scan_async.go new file mode 100644 index 0000000..b0a9785 --- /dev/null +++ b/scan_async.go @@ -0,0 +1,82 @@ +package nmap + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" +) + +func (s *Scanner) runAsync(ctx context.Context) (<-chan []byte, <-chan []byte, <-chan RunResult, error) { + cmd := s.newCmd(ctx) + + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + return nil, nil, nil, err + } + + stderrPipe, err := cmd.StderrPipe() + if err != nil { + return nil, nil, nil, err + } + + err = cmd.Start() + if err != nil { + return nil, nil, nil, err + } + + stdoutCh := make(chan []byte, 16) + stderrCh := make(chan []byte, 16) + resultCh := make(chan RunResult, 1) + + var stdout, stderr bytes.Buffer + stdoutWriter := io.MultiWriter(&stdout, channelWriter{ch: stdoutCh}) + stderrWriter := io.MultiWriter(&stderr, channelWriter{ch: stderrCh}) + + stdoutErrCh := make(chan error, 1) + stderrErrCh := make(chan error, 1) + + // Start goroutine to read stdout. + go func() { + defer close(stdoutCh) + + // If progress handler is set, stream progress updates. + if s.progressHandler != nil { + tee := io.TeeReader(stdoutPipe, stdoutWriter) + stdoutErrCh <- streamTaskProgress(tee, s.progressHandler) + return + } + _, copyErr := io.Copy(stdoutWriter, stdoutPipe) + stdoutErrCh <- copyErr + }() + + // Start goroutine to read stderr. + go func() { + defer close(stderrCh) + _, copyErr := io.Copy(stderrWriter, stderrPipe) + stderrErrCh <- copyErr + }() + + // Start goroutine to wait for nmap to finish and process the result. + go func() { + defer close(resultCh) + + runErr := cmd.Wait() + stdoutErr := <-stdoutErrCh + stderrErr := <-stderrErrCh + + result, parseErr := s.processNmapResult(&stdout, &stderr) + if stdoutErr != nil && !errors.Is(stdoutErr, io.EOF) && result != nil { + result.warnings = append(result.warnings, fmt.Sprintf("stdout stream error: %s", stdoutErr)) + } + if stderrErr != nil && !errors.Is(stderrErr, io.EOF) && result != nil { + result.warnings = append(result.warnings, fmt.Sprintf("stderr stream error: %s", stderrErr)) + } + + finalResult, finalErr := finalizeRun(ctx, runErr, parseErr, result, &stdout, &stderr) + resultCh <- RunResult{Result: finalResult, Err: finalErr} + }() + + return stdoutCh, stderrCh, resultCh, nil +} diff --git a/scan_async_test.go b/scan_async_test.go new file mode 100644 index 0000000..5e0cd1c --- /dev/null +++ b/scan_async_test.go @@ -0,0 +1,75 @@ +package nmap + +import ( + "bytes" + "context" + "testing" + "time" + + "github.com/hamba/testutils/retry" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRunAsync(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + + s, err := NewScanner( + WithTargets("localhost"), + WithPorts("1-1024"), + WithTimingTemplate(TimingNormal), + ) + require.NoError(t, err) + + stdoutCh, stderrCh, resultCh, err := s.RunAsync(ctx) + require.NoError(t, err) + + var stdoutBuf bytes.Buffer + var stderrBuf bytes.Buffer + stdoutDone := make(chan struct{}) + stderrDone := make(chan struct{}) + + go func() { + defer close(stdoutDone) + for chunk := range stdoutCh { + _, _ = stdoutBuf.Write(chunk) + } + }() + + go func() { + defer close(stderrDone) + for chunk := range stderrCh { + _, _ = stderrBuf.Write(chunk) + } + }() + + var runResult RunResult + var gotResult bool + + retry.RunWith(t, retry.NewTimer(10*time.Second, time.Second), func(r *retry.SubT) { + if !gotResult { + select { + case rr, ok := <-resultCh: + if ok { + runResult = rr + gotResult = true + } + default: + } + } + + require.True(r, gotResult, "expected async result") + require.NoError(r, runResult.Err) + require.NotNil(r, runResult.Result) + assert.Equal(r, "nmap", runResult.Result.Scanner) + }) + + <-stdoutDone + <-stderrDone + + assert.Greater(t, stdoutBuf.Len(), 0) + if stderrBuf.Len() > 0 { + t.Logf("stderr: %s", stderrBuf.String()) + } +} diff --git a/scan_progress.go b/scan_progress.go new file mode 100644 index 0000000..7df5b28 --- /dev/null +++ b/scan_progress.go @@ -0,0 +1,41 @@ +package nmap + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os/exec" +) + +func (s *Scanner) runAndParseWithProgress(ctx context.Context, cmd *exec.Cmd) (*Run, error) { + var stdout, stderr bytes.Buffer + cmd.Stderr = &stderr + + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + err = cmd.Start() + if err != nil { + return nil, err + } + + readErrCh := make(chan error, 1) + go func() { + tee := io.TeeReader(stdoutPipe, &stdout) + readErrCh <- streamTaskProgress(tee, s.progressHandler) + }() + + runErr := cmd.Wait() + readErr := <-readErrCh + + result, parseErr := s.processNmapResult(&stdout, &stderr) + if readErr != nil && !errors.Is(readErr, io.EOF) && result != nil { + result.warnings = append(result.warnings, fmt.Sprintf("progress stream error: %s", readErr)) + } + + return finalizeRun(ctx, runErr, parseErr, result, &stdout, &stderr) +} diff --git a/scan_progress_test.go b/scan_progress_test.go new file mode 100644 index 0000000..8c24041 --- /dev/null +++ b/scan_progress_test.go @@ -0,0 +1,56 @@ +package nmap + +import ( + "os" + "sync" + "testing" + "time" + + isatty "github.com/mattn/go-isatty" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRunWithProgress(t *testing.T) { + var ( + mu sync.Mutex + progresses []TaskProgress + ) + + if !isatty.IsTerminal(os.Stdout.Fd()) { + t.Skip("skipping progress test since not running in a TTY") + } + + handler := func(p TaskProgress) { + mu.Lock() + progresses = append(progresses, p) + mu.Unlock() + } + + s, err := NewScanner( + WithTargets("localhost"), + WithPorts("1-1024"), + WithTimingTemplate(TimingNormal), + WithProgress(time.Second, handler), + WithScanDelay(10*time.Millisecond), + ) + require.NoError(t, err) + + ctx := t.Context() + result, err := s.Run(ctx) + require.NoError(t, err) + + mu.Lock() + count := len(progresses) + var last TaskProgress + if count > 0 { + last = progresses[count-1] + } + mu.Unlock() + + require.Greater(t, count, 0, "expected at least one progress update") + assert.InDelta(t, 100, last.Percent, 10.0) + + require.NotNil(t, result) + assert.Equal(t, "nmap", result.Scanner) +} diff --git a/scan_sync.go b/scan_sync.go new file mode 100644 index 0000000..bea1c0e --- /dev/null +++ b/scan_sync.go @@ -0,0 +1,17 @@ +package nmap + +import ( + "bytes" + "context" + "os/exec" +) + +func (s *Scanner) runAndParse(ctx context.Context, cmd *exec.Cmd) (*Run, error) { + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + runErr := cmd.Run() + result, parseErr := s.processNmapResult(&stdout, &stderr) + return finalizeRun(ctx, runErr, parseErr, result, &stdout, &stderr) +} diff --git a/scan_sync_test.go b/scan_sync_test.go new file mode 100644 index 0000000..9b678ca --- /dev/null +++ b/scan_sync_test.go @@ -0,0 +1,175 @@ +package nmap + +import ( + "context" + "encoding/xml" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestRun(t *testing.T) { + tests := []struct { + description string + + options []Option + + testTimeout bool + compareWholeRun bool + useContainer bool + + expectedResult *Run + wantErr require.ErrorAssertionFunc + }{ + { + description: "scan localhost", + + options: []Option{ + WithTargets("localhost"), + WithTimingTemplate(TimingFastest), + }, + useContainer: true, + + expectedResult: &Run{ + Scanner: "nmap", + Args: "nmap -T5 -oX - localhost", + }, + wantErr: require.NoError, + }, + { + description: "missing target", + + options: []Option{ + WithTimingTemplate(TimingFastest), + }, + useContainer: true, + + expectedResult: &Run{ + Scanner: "nmap", + Args: "nmap -T5 -oX -", + Stats: Stats{Hosts: HostStats{Total: 0}}, + }, + wantErr: require.NoError, + }, + { + description: "scan localhost with filters", + options: []Option{ + WithBinaryPath("tests/scripts/fake_nmap.sh"), + WithCustomArguments("tests/xml/scan_invalid_services.xml"), + WithFilterHost(func(h Host) bool { + return len(h.Ports) == 2 + }), + WithFilterPort(func(p Port) bool { + return p.Service.Product == "VALID" + }), + WithTimingTemplate(TimingFastest), + }, + + compareWholeRun: true, + + expectedResult: &Run{ + XMLName: xml.Name{Local: "nmaprun"}, + Args: "nmap test", + Scanner: "fake_nmap", + Hosts: []Host{{ + Addresses: []Address{{Addr: "66.35.250.168"}}, + Ports: []Port{ + {ID: 80, State: State{State: "open"}, Service: Service{Name: "http", Product: "VALID"}}, + {ID: 443, State: State{State: "open"}, Service: Service{Name: "https", Product: "VALID"}}, + }, + }, + }, + }, + wantErr: require.NoError, + }, + { + description: "invalid binary path", + + options: []Option{ + WithTargets("0.0.0.0"), + WithBinaryPath("/invalid"), + }, + + wantErr: require.Error, + }, + { + description: "output can't be parsed", + + options: []Option{ + WithTargets("0.0.0.0"), + WithBinaryPath("echo"), + }, + + wantErr: require.Error, + }, + { + description: "context timeout", + + options: []Option{ + WithTargets("0.0.0.0/16"), + }, + + testTimeout: true, + useContainer: true, + + wantErr: require.Error, + }, + { + description: "scan error resolving name", + options: []Option{ + WithBinaryPath("tests/scripts/fake_nmap.sh"), + WithCustomArguments("tests/xml/scan_error_resolving_name.xml"), + }, + + expectedResult: &Run{ + Scanner: "fake_nmap", + Args: "nmap test", + }, + wantErr: require.Error, + }, + { + description: "scan unsupported error", + options: []Option{ + WithBinaryPath("tests/scripts/fake_nmap.sh"), + WithCustomArguments("tests/xml/scan_error_other.xml"), + }, + + expectedResult: &Run{ + Scanner: "fake_nmap", + Args: "nmap test", + }, + wantErr: require.Error, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + ctx := t.Context() + if test.testTimeout { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, 99*time.Hour) + + go (func() { + // Cancel context to force timeout + defer cancel() + time.Sleep(1 * time.Millisecond) + })() + } + + options := append([]Option{}, test.options...) + if test.useContainer { + containerOptions := nmapContainerOptions(t) + options = append(containerOptions, options...) + } + + s, err := NewScanner(options...) + require.NoError(t, err) + + result, err := s.Run(ctx) + test.wantErr(t, err) + + compareResults(t, test.expectedResult, result) + }) + } +} diff --git a/xml.go b/xml.go index 64cc503..6d1df04 100644 --- a/xml.go +++ b/xml.go @@ -8,134 +8,123 @@ import ( "strconv" "time" - family "github.com/Ullaakut/nmap/v3/pkg/osfamilies" + family "github.com/Ullaakut/nmap/v4/pkg/osfamilies" ) // Run represents an nmap scanning run. type Run struct { XMLName xml.Name `xml:"nmaprun"` - Args string `xml:"args,attr" json:"args"` - ProfileName string `xml:"profile_name,attr" json:"profile_name"` - Scanner string `xml:"scanner,attr" json:"scanner"` - StartStr string `xml:"startstr,attr" json:"start_str"` - Version string `xml:"version,attr" json:"version"` - XMLOutputVersion string `xml:"xmloutputversion,attr" json:"xml_output_version"` - Debugging Debugging `xml:"debugging" json:"debugging"` - Stats Stats `xml:"runstats" json:"run_stats"` - ScanInfo ScanInfo `xml:"scaninfo" json:"scan_info"` - Start Timestamp `xml:"start,attr" json:"start"` - Verbose Verbose `xml:"verbose" json:"verbose"` - Hosts []Host `xml:"host" json:"hosts"` - PostScripts []Script `xml:"postscript>script" json:"post_scripts"` - PreScripts []Script `xml:"prescript>script" json:"pre_scripts"` - Targets []Target `xml:"target" json:"targets"` - TaskBegin []Task `xml:"taskbegin" json:"task_begin"` - TaskProgress []TaskProgress `xml:"taskprogress" json:"task_progress"` - TaskEnd []Task `xml:"taskend" json:"task_end"` - - NmapErrors []string - rawXML []byte + Args string `json:"args" xml:"args,attr"` + ProfileName string `json:"profile_name" xml:"profile_name,attr"` + Scanner string `json:"scanner" xml:"scanner,attr"` + StartStr string `json:"start_str" xml:"startstr,attr"` + Version string `json:"version" xml:"version,attr"` + XMLOutputVersion string `json:"xml_output_version" xml:"xmloutputversion,attr"` + Debugging Debugging `json:"debugging" xml:"debugging"` + Stats Stats `json:"run_stats" xml:"runstats"` + ScanInfo ScanInfo `json:"scan_info" xml:"scaninfo"` + Start Timestamp `json:"start" xml:"start,attr"` + Verbose Verbose `json:"verbose" xml:"verbose"` + Hosts []Host `json:"hosts" xml:"host"` + PostScripts []Script `json:"post_scripts" xml:"postscript>script"` + PreScripts []Script `json:"pre_scripts" xml:"prescript>script"` + Targets []Target `json:"targets" xml:"target"` + TaskBegin []Task `json:"task_begin" xml:"taskbegin"` + TaskProgress []TaskProgress `json:"task_progress" xml:"taskprogress"` + TaskEnd []Task `json:"task_end" xml:"taskend"` + + warnings []string + rawXML []byte } // ToFile writes a Run as XML into the specified file path. -func (r Run) ToFile(filePath string) error { - 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 +func (r *Run) ToFile(filePath string) error { + return os.WriteFile(filePath, r.rawXML, 0o600) } // ToReader writes the raw XML into an streamable buffer. -func (r Run) ToReader() io.Reader { +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) +// Warnings returns the warnings encountered during the nmap scan. +func (r *Run) Warnings() []string { + return r.warnings } // ScanInfo represents the scan information. type ScanInfo struct { - NumServices int `xml:"numservices,attr" json:"num_services"` - Protocol string `xml:"protocol,attr" json:"protocol"` - ScanFlags string `xml:"scanflags,attr" json:"scan_flags"` - Services string `xml:"services,attr" json:"services"` - Type string `xml:"type,attr" json:"type"` + NumServices int `json:"num_services" xml:"numservices,attr"` + Protocol string `json:"protocol" xml:"protocol,attr"` + ScanFlags string `json:"scan_flags" xml:"scanflags,attr"` + Services string `json:"services" xml:"services,attr"` + Type string `json:"type" xml:"type,attr"` } // Verbose contains the verbosity level of the scan. type Verbose struct { - Level int `xml:"level,attr" json:"level"` + Level int `json:"level" xml:"level,attr"` } // Debugging contains the debugging level of the scan. type Debugging struct { - Level int `xml:"level,attr" json:"level"` + Level int `json:"level" xml:"level,attr"` } // Task contains information about a task. type Task struct { - Time Timestamp `xml:"time,attr" json:"time"` - Task string `xml:"task,attr" json:"task"` - ExtraInfo string `xml:"extrainfo,attr" json:"extra_info"` + Time Timestamp `json:"time" xml:"time,attr"` + Task string `json:"task" xml:"task,attr"` + ExtraInfo string `json:"extra_info" xml:"extrainfo,attr"` } // TaskProgress contains information about the progression of a task. type TaskProgress struct { - Percent float32 `xml:"percent,attr" json:"percent"` - Remaining int `xml:"remaining,attr" json:"remaining"` - Task string `xml:"task,attr" json:"task"` - Etc Timestamp `xml:"etc,attr" json:"etc"` - Time Timestamp `xml:"time,attr" json:"time"` + Percent float32 `json:"percent" xml:"percent,attr"` + Remaining int `json:"remaining" xml:"remaining,attr"` + Task string `json:"task" xml:"task,attr"` + Etc Timestamp `json:"etc" xml:"etc,attr"` + Time Timestamp `json:"time" xml:"time,attr"` } // Target represents a target, how it was specified when passed to nmap, // its status and the reason for its status. Example: -// +// . type Target struct { - Specification string `xml:"specification,attr" json:"specification"` - Status string `xml:"status,attr" json:"status"` - Reason string `xml:"reason,attr" json:"reason"` + Specification string `json:"specification" xml:"specification,attr"` + Status string `json:"status" xml:"status,attr"` + Reason string `json:"reason" xml:"reason,attr"` } // Host represents a host that was scanned. type Host struct { - Distance Distance `xml:"distance" json:"distance"` - EndTime Timestamp `xml:"endtime,attr,omitempty" json:"end_time"` - IPIDSequence IPIDSequence `xml:"ipidsequence" json:"ip_id_sequence"` - OS OS `xml:"os" json:"os"` - StartTime Timestamp `xml:"starttime,attr,omitempty" json:"start_time"` - TimedOut bool `xml:"timedout,attr,omitempty" json:"timed_out"` - Status Status `xml:"status" json:"status"` - TCPSequence TCPSequence `xml:"tcpsequence" json:"tcp_sequence"` - TCPTSSequence TCPTSSequence `xml:"tcptssequence" json:"tcp_ts_sequence"` - Times Times `xml:"times" json:"times"` - Trace Trace `xml:"trace" json:"trace"` - Uptime Uptime `xml:"uptime" json:"uptime"` - Comment string `xml:"comment,attr" json:"comment"` - Addresses []Address `xml:"address" json:"addresses"` - ExtraPorts []ExtraPort `xml:"ports>extraports" json:"extra_ports"` - Hostnames []Hostname `xml:"hostnames>hostname" json:"hostnames"` - HostScripts []Script `xml:"hostscript>script" json:"host_scripts"` - Ports []Port `xml:"ports>port" json:"ports"` - Smurfs []Smurf `xml:"smurf" json:"smurfs"` + Distance Distance `json:"distance" xml:"distance"` + EndTime Timestamp `json:"end_time" xml:"endtime,attr,omitempty"` + IPIDSequence IPIDSequence `json:"ip_id_sequence" xml:"ipidsequence"` + OS OS `json:"os" xml:"os"` + StartTime Timestamp `json:"start_time" xml:"starttime,attr,omitempty"` + TimedOut bool `json:"timed_out" xml:"timedout,attr,omitempty"` + Status Status `json:"status" xml:"status"` + TCPSequence TCPSequence `json:"tcp_sequence" xml:"tcpsequence"` + TCPTSSequence TCPTSSequence `json:"tcp_ts_sequence" xml:"tcptssequence"` + Times Times `json:"times" xml:"times"` + Trace Trace `json:"trace" xml:"trace"` + Uptime Uptime `json:"uptime" xml:"uptime"` + Comment string `json:"comment" xml:"comment,attr"` + Addresses []Address `json:"addresses" xml:"address"` + ExtraPorts []ExtraPort `json:"extra_ports" xml:"ports>extraports"` + Hostnames []Hostname `json:"hostnames" xml:"hostnames>hostname"` + HostScripts []Script `json:"host_scripts" xml:"hostscript>script"` + Ports []Port `json:"ports" xml:"ports>port"` + Smurfs []Smurf `json:"smurfs" xml:"smurf"` } // Status represents a host's status. type Status struct { - State string `xml:"state,attr" json:"state"` - Reason string `xml:"reason,attr" json:"reason"` - ReasonTTL float32 `xml:"reason_ttl,attr" json:"reason_ttl"` + State string `json:"state" xml:"state,attr"` + Reason string `json:"reason" xml:"reason,attr"` + ReasonTTL float32 `json:"reason_ttl" xml:"reason_ttl,attr"` } func (s Status) String() string { @@ -144,9 +133,9 @@ func (s Status) String() string { // Address contains a IPv4 or IPv6 address for a host. type Address struct { - Addr string `xml:"addr,attr" json:"addr"` - AddrType string `xml:"addrtype,attr" json:"addr_type"` - Vendor string `xml:"vendor,attr" json:"vendor"` + Addr string `json:"addr" xml:"addr,attr"` + AddrType string `json:"addr_type" xml:"addrtype,attr"` + Vendor string `json:"vendor" xml:"vendor,attr"` } func (a Address) String() string { @@ -155,8 +144,8 @@ func (a Address) String() string { // Hostname is a name for a host. type Hostname struct { - Name string `xml:"name,attr" json:"name"` - Type string `xml:"type,attr" json:"type"` + Name string `json:"name" xml:"name,attr"` + Type string `json:"type" xml:"type,attr"` } func (h Hostname) String() string { @@ -165,31 +154,31 @@ func (h Hostname) String() string { // Smurf contains responses from a smurf attack. type Smurf struct { - Responses string `xml:"responses,attr" json:"responses"` + Responses string `json:"responses" xml:"responses,attr"` } // ExtraPort contains the information about the closed and filtered ports. type ExtraPort struct { - State string `xml:"state,attr" json:"state"` - Count int `xml:"count,attr" json:"count"` - Reasons []Reason `xml:"extrareasons" json:"reasons"` + State string `json:"state" xml:"state,attr"` + Count int `json:"count" xml:"count,attr"` + Reasons []Reason `json:"reasons" xml:"extrareasons"` } // Reason represents a reason why a port is closed or filtered. // This won't be in the scan results unless WithReason is used. type Reason struct { - Reason string `xml:"reason,attr" json:"reason"` - Count int `xml:"count,attr" json:"count"` + Reason string `json:"reason" xml:"reason,attr"` + Count int `json:"count" xml:"count,attr"` } // Port contains all the information about a scanned port. type Port struct { - ID uint16 `xml:"portid,attr" json:"id"` - Protocol string `xml:"protocol,attr" json:"protocol"` - Owner Owner `xml:"owner" json:"owner"` - Service Service `xml:"service" json:"service"` - State State `xml:"state" json:"state"` - Scripts []Script `xml:"script" json:"scripts"` + ID uint16 `json:"id" xml:"portid,attr"` + Protocol string `json:"protocol" xml:"protocol,attr"` + Owner Owner `json:"owner" xml:"owner"` + Service Service `json:"service" xml:"service"` + State State `json:"state" xml:"state"` + Scripts []Script `json:"scripts" xml:"script"` } // PortStatus represents a port's state. @@ -209,12 +198,12 @@ func (p Port) Status() PortStatus { } // State contains information about a given port's status. -// State will be open, closed, etc. +// State is open, closed, etc. type State struct { - State string `xml:"state,attr" json:"state"` - Reason string `xml:"reason,attr" json:"reason"` - ReasonIP string `xml:"reason_ip,attr" json:"reason_ip"` - ReasonTTL float32 `xml:"reason_ttl,attr" json:"reason_ttl"` + State string `json:"state" xml:"state,attr"` + Reason string `json:"reason" xml:"reason,attr"` + ReasonIP string `json:"reason_ip" xml:"reason_ip,attr"` + ReasonTTL float32 `json:"reason_ttl" xml:"reason_ttl,attr"` } func (s State) String() string { @@ -223,7 +212,7 @@ func (s State) String() string { // Owner contains the name of a port's owner. type Owner struct { - Name string `xml:"name,attr" json:"name"` + Name string `json:"name" xml:"name,attr"` } func (o Owner) String() string { @@ -232,22 +221,22 @@ func (o Owner) String() string { // Service contains detailed information about a service on an open port. type Service struct { - DeviceType string `xml:"devicetype,attr" json:"device_type"` - ExtraInfo string `xml:"extrainfo,attr" json:"extra_info"` - HighVersion string `xml:"highver,attr" json:"high_version"` - Hostname string `xml:"hostname,attr" json:"hostname"` - LowVersion string `xml:"lowver,attr" json:"low_version"` - Method string `xml:"method,attr" json:"method"` - Name string `xml:"name,attr" json:"name"` - OSType string `xml:"ostype,attr" json:"os_type"` - Product string `xml:"product,attr" json:"product"` - Proto string `xml:"proto,attr" json:"proto"` - RPCNum string `xml:"rpcnum,attr" json:"rpc_num"` - ServiceFP string `xml:"servicefp,attr" json:"service_fp"` - Tunnel string `xml:"tunnel,attr" json:"tunnel"` - Version string `xml:"version,attr" json:"version"` - Confidence int `xml:"conf,attr" json:"confidence"` - CPEs []CPE `xml:"cpe" json:"cpes"` + DeviceType string `json:"device_type" xml:"devicetype,attr"` + ExtraInfo string `json:"extra_info" xml:"extrainfo,attr"` + HighVersion string `json:"high_version" xml:"highver,attr"` + Hostname string `json:"hostname" xml:"hostname,attr"` + LowVersion string `json:"low_version" xml:"lowver,attr"` + Method string `json:"method" xml:"method,attr"` + Name string `json:"name" xml:"name,attr"` + OSType string `json:"os_type" xml:"ostype,attr"` + Product string `json:"product" xml:"product,attr"` + Proto string `json:"proto" xml:"proto,attr"` + RPCNum string `json:"rpc_num" xml:"rpcnum,attr"` + ServiceFP string `json:"service_fp" xml:"servicefp,attr"` + Tunnel string `json:"tunnel" xml:"tunnel,attr"` + Version string `json:"version" xml:"version,attr"` + Confidence int `json:"confidence" xml:"conf,attr"` + CPEs []CPE `json:"cpes" xml:"cpe"` } func (s Service) String() string { @@ -261,55 +250,55 @@ type CPE string // Script represents an Nmap Scripting Engine script. // The inner elements can be an arbitrary collection of Tables and Elements. Both of them can also be empty. type Script struct { - ID string `xml:"id,attr" json:"id"` - Output string `xml:"output,attr" json:"output"` - Elements []Element `xml:"elem,omitempty" json:"elements,omitempty"` - Tables []Table `xml:"table,omitempty" json:"tables,omitempty"` + ID string `json:"id" xml:"id,attr"` + Output string `json:"output" xml:"output,attr"` + Elements []Element `json:"elements,omitempty" xml:"elem,omitempty"` + Tables []Table `json:"tables,omitempty" xml:"table,omitempty"` } // Table is an arbitrary collection of (sub-)Tables and Elements. All its fields can be empty. type Table struct { - Key string `xml:"key,attr,omitempty" json:"key,omitempty"` - Tables []Table `xml:"table,omitempty" json:"tables,omitempty"` - Elements []Element `xml:"elem,omitempty" json:"elements,omitempty"` + Key string `json:"key,omitempty" xml:"key,attr,omitempty"` + Tables []Table `json:"tables,omitempty" xml:"table,omitempty"` + Elements []Element `json:"elements,omitempty" xml:"elem,omitempty"` } // Element is the smallest building block for scripts/tables. It can optionally(!) have a key. type Element struct { - Key string `xml:"key,attr,omitempty" json:"key,omitempty"` - Value string `xml:",innerxml" json:"value"` + Key string `json:"key,omitempty" xml:"key,attr,omitempty"` + Value string `json:"value" xml:",innerxml"` } // OS contains the fingerprinted operating system for a host. type OS struct { - PortsUsed []PortUsed `xml:"portused" json:"ports_used"` - Matches []OSMatch `xml:"osmatch" json:"os_matches"` - Fingerprints []OSFingerprint `xml:"osfingerprint" json:"os_fingerprints"` + PortsUsed []PortUsed `json:"ports_used" xml:"portused"` + Matches []OSMatch `json:"os_matches" xml:"osmatch"` + Fingerprints []OSFingerprint `json:"os_fingerprints" xml:"osfingerprint"` } // PortUsed is the port used to fingerprint an operating system. type PortUsed struct { - State string `xml:"state,attr" json:"state"` - Proto string `xml:"proto,attr" json:"proto"` - ID int `xml:"portid,attr" json:"port_id"` + State string `json:"state" xml:"state,attr"` + Proto string `json:"proto" xml:"proto,attr"` + ID int `json:"port_id" xml:"portid,attr"` } // OSMatch contains detailed information regarding an operating system fingerprint. type OSMatch struct { - Name string `xml:"name,attr" json:"name"` - Accuracy int `xml:"accuracy,attr" json:"accuracy"` - Line int `xml:"line,attr" json:"line"` - Classes []OSClass `xml:"osclass" json:"os_classes"` + Name string `json:"name" xml:"name,attr"` + Accuracy int `json:"accuracy" xml:"accuracy,attr"` + Line int `json:"line" xml:"line,attr"` + Classes []OSClass `json:"os_classes" xml:"osclass"` } // OSClass contains vendor information about an operating system. type OSClass struct { - Vendor string `xml:"vendor,attr" json:"vendor"` - OSGeneration string `xml:"osgen,attr" json:"os_generation"` - Type string `xml:"type,attr" json:"type"` - Accuracy int `xml:"accuracy,attr" json:"accuracy"` - Family string `xml:"osfamily,attr" json:"os_family"` - CPEs []CPE `xml:"cpe" json:"cpes"` + Vendor string `json:"vendor" xml:"vendor,attr"` + OSGeneration string `json:"os_generation" xml:"osgen,attr"` + Type string `json:"type" xml:"type,attr"` + Accuracy int `json:"accuracy" xml:"accuracy,attr"` + Family string `json:"os_family" xml:"osfamily,attr"` + CPEs []CPE `json:"cpes" xml:"cpe"` } // OSFamily returns the OS family in an enumerated format. @@ -319,31 +308,31 @@ func (o OSClass) OSFamily() family.OSFamily { // OSFingerprint is the actual fingerprint string of an operating system. type OSFingerprint struct { - Fingerprint string `xml:"fingerprint,attr" json:"fingerprint"` + Fingerprint string `json:"fingerprint" xml:"fingerprint,attr"` } // Distance is the amount of hops to a particular host. type Distance struct { - Value int `xml:"value,attr" json:"value"` + Value int `json:"value" xml:"value,attr"` } // Uptime is the amount of time the host has been up. type Uptime struct { - Seconds int `xml:"seconds,attr" json:"seconds"` - Lastboot string `xml:"lastboot,attr" json:"last_boot"` + Seconds int `json:"seconds" xml:"seconds,attr"` + Lastboot string `json:"last_boot" xml:"lastboot,attr"` } // Sequence represents a detected sequence. type Sequence struct { - Class string `xml:"class,attr" json:"class"` - Values string `xml:"values,attr" json:"values"` + Class string `json:"class" xml:"class,attr"` + Values string `json:"values" xml:"values,attr"` } // TCPSequence represents a detected TCP sequence. type TCPSequence struct { - Index int `xml:"index,attr" json:"index"` - Difficulty string `xml:"difficulty,attr" json:"difficulty"` - Values string `xml:"values,attr" json:"values"` + Index int `json:"index" xml:"index,attr"` + Difficulty string `json:"difficulty" xml:"difficulty,attr"` + Values string `json:"values" xml:"values,attr"` } // IPIDSequence represents a detected IP ID sequence. @@ -354,47 +343,47 @@ type TCPTSSequence Sequence // Trace represents the trace to a host, including the hops. type Trace struct { - Proto string `xml:"proto,attr" json:"proto"` - Port int `xml:"port,attr" json:"port"` - Hops []Hop `xml:"hop" json:"hops"` + Proto string `json:"proto" xml:"proto,attr"` + Port int `json:"port" xml:"port,attr"` + Hops []Hop `json:"hops" xml:"hop"` } // Hop is an IP hop to a host. type Hop struct { - TTL float32 `xml:"ttl,attr" json:"ttl"` - RTT string `xml:"rtt,attr" json:"rtt"` - IPAddr string `xml:"ipaddr,attr" json:"ip_addr"` - Host string `xml:"host,attr" json:"host"` + TTL float32 `json:"ttl" xml:"ttl,attr"` + RTT string `json:"rtt" xml:"rtt,attr"` + IPAddr string `json:"ip_addr" xml:"ipaddr,attr"` + Host string `json:"host" xml:"host,attr"` } // Times contains time statistics for an nmap scan. type Times struct { - SRTT string `xml:"srtt,attr" json:"srtt"` - RTT string `xml:"rttvar,attr" json:"rttv"` - To string `xml:"to,attr" json:"to"` + SRTT string `json:"srtt" xml:"srtt,attr"` + RTT string `json:"rttv" xml:"rttvar,attr"` + To string `json:"to" xml:"to,attr"` } // Stats contains statistics for an nmap scan. type Stats struct { - Finished Finished `xml:"finished" json:"finished"` - Hosts HostStats `xml:"hosts" json:"hosts"` + Finished Finished `json:"finished" xml:"finished"` + Hosts HostStats `json:"hosts" xml:"hosts"` } // Finished contains detailed statistics regarding a finished scan. type Finished struct { - Time Timestamp `xml:"time,attr" json:"time"` - TimeStr string `xml:"timestr,attr" json:"time_str"` - Elapsed float32 `xml:"elapsed,attr" json:"elapsed"` - Summary string `xml:"summary,attr" json:"summary"` - Exit string `xml:"exit,attr" json:"exit"` - ErrorMsg string `xml:"errormsg,attr" json:"error_msg"` + Time Timestamp `json:"time" xml:"time,attr"` + TimeStr string `json:"time_str" xml:"timestr,attr"` + Elapsed float32 `json:"elapsed" xml:"elapsed,attr"` + Summary string `json:"summary" xml:"summary,attr"` + Exit string `json:"exit" xml:"exit,attr"` + ErrorMsg string `json:"error_msg" xml:"errormsg,attr"` } // HostStats contains the amount of up and down hosts and the total count. type HostStats struct { - Up int `xml:"up,attr" json:"up"` - Down int `xml:"down,attr" json:"down"` - Total int `xml:"total,attr" json:"total"` + Up int `json:"up" xml:"up,attr"` + Down int `json:"down" xml:"down,attr"` + Total int `json:"total" xml:"total,attr"` } // Timestamp represents time as a UNIX timestamp in seconds. @@ -413,12 +402,12 @@ func (t *Timestamp) ParseTime(s string) error { } // FormatTime formats the time.Time value as a UNIX timestamp string. -func (t Timestamp) FormatTime() string { - return strconv.FormatInt(time.Time(t).Unix(), 10) +func (t *Timestamp) FormatTime() string { + return strconv.FormatInt(time.Time(*t).Unix(), 10) } // MarshalJSON implements the json.Marshaler interface. -func (t Timestamp) MarshalJSON() ([]byte, error) { +func (t *Timestamp) MarshalJSON() ([]byte, error) { return []byte(t.FormatTime()), nil } @@ -428,8 +417,8 @@ func (t *Timestamp) UnmarshalJSON(b []byte) error { } // MarshalXMLAttr implements the xml.MarshalerAttr interface. -func (t Timestamp) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { - if time.Time(t).IsZero() { +func (t *Timestamp) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { + if time.Time(*t).IsZero() { return xml.Attr{}, nil } @@ -441,11 +430,16 @@ func (t *Timestamp) UnmarshalXMLAttr(attr xml.Attr) (err error) { return t.ParseTime(attr.Value) } -// 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 +// parse takes a byte array of nmap xml data and unmarshal it into a Run struct. +func parse(content []byte) (*Run, error) { + result := Run{ + rawXML: content, + } - err := xml.Unmarshal(content, result) + err := xml.Unmarshal(content, &result) + if err != nil { + return nil, err + } - return err + return &result, nil } diff --git a/xml_test.go b/xml_test.go index d84759b..b7d7a6c 100644 --- a/xml_test.go +++ b/xml_test.go @@ -1,26 +1,24 @@ package nmap import ( - "bytes" "encoding/json" "encoding/xml" "fmt" - "io/ioutil" + "io" "os" - "reflect" "testing" "time" - family "github.com/Ullaakut/nmap/v3/pkg/osfamilies" + family "github.com/Ullaakut/nmap/v4/pkg/osfamilies" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestParseTime(t *testing.T) { ts := Timestamp{} err := ts.ParseTime("invalid") - if err == nil { - t.Errorf("expected strconv.ParseInt: parsing \"invalid\": invalid syntax got %v", err) - } + assert.Error(t, err) } func TestFormatTime(t *testing.T) { @@ -28,15 +26,10 @@ func TestFormatTime(t *testing.T) { ts := Timestamp{} err := ts.ParseTime(originalStr) - if err != nil { - panic(err) - } + require.NoError(t, err) result := ts.FormatTime() - - if result != originalStr { - t.Errorf("expected %s got %s", originalStr, result) - } + assert.Equal(t, originalStr, result) } func TestOSFamily(t *testing.T) { @@ -44,9 +37,7 @@ func TestOSFamily(t *testing.T) { Family: "Linux", } - if osc.OSFamily() != family.Linux { - t.Errorf("expected OSClass.OSFamily() to be equal to %v, got %v", family.Linux, osc.OSFamily()) - } + assert.Equal(t, family.Linux, osc.OSFamily()) } func TestParseTableXML(t *testing.T) { @@ -130,45 +121,21 @@ func TestParseTableXML(t *testing.T) { )) var table Table - err := xml.Unmarshal(input, &table) - if err != nil { - panic(err) - } + require.NoError(t, err) // Outermost table. - if table.Key != expectedTable.Key { - t.Errorf("expected %v got %v", expectedTable.Key, table.Key) - } - - if len(table.Elements) != len(expectedTable.Elements) { - t.Errorf("expected different number of elements in outermost table, want %v got %v", len(expectedTable.Elements), len(table.Elements)) - } - for ie := range table.Elements { - if table.Elements[ie].Value != expectedTable.Elements[ie].Value { - t.Errorf("expected %v got %v", expectedTable.Elements[ie].Value, table.Elements[ie].Value) - } + assert.Equal(t, expectedTable.Key, table.Key) + assert.Len(t, table.Elements, len(expectedTable.Elements)) + for idx := range table.Elements { + assert.Equal(t, expectedTable.Elements[idx].Key, table.Elements[idx].Key) } // Nested tables - if len(table.Tables) != len(expectedTable.Tables) { - t.Errorf("expected different amount of nested tables, want %v got %v", len(expectedTable.Tables), len(table.Tables)) - } - - for it := range table.Tables { - if table.Tables[it].Key != expectedTable.Tables[it].Key { - t.Errorf("expected %v got %v", expectedTable.Tables[0].Key, table.Tables[0].Key) - } - - if len(table.Tables[it].Elements) != len(expectedTable.Tables[it].Elements) { - t.Errorf("expected number of elements in nested table[%v], want %v got %v", - it, len(expectedTable.Tables[it].Elements), len(table.Tables[it].Elements)) - } - for ie := range table.Tables[it].Elements { - if table.Tables[it].Elements[ie].Value != expectedTable.Tables[it].Elements[ie].Value { - t.Errorf("expected %v got %v", expectedTable.Tables[it].Elements[ie].Value, table.Tables[it].Elements[ie].Value) - } - } + assert.Len(t, table.Tables, len(expectedTable.Tables)) + for idx := range table.Tables { + assert.Equal(t, expectedTable.Tables[idx].Key, table.Tables[idx].Key) + assert.ElementsMatch(t, expectedTable.Tables[idx].Elements, table.Tables[idx].Elements) } } @@ -176,47 +143,24 @@ func TestFormatTableXML(t *testing.T) { table := Table{ Key: "key123", Elements: []Element{ - { - Key: "key", - Value: "AAAAB3NzaC1yc2EAAAABIwAAAQEAwVKoTY/7GFG7BmKkG6qFAHY/f3ciDX2MXTBLMEJP0xyUJsoy/CVRYw2b4qUB/GCJ5lh2InP+LVnPD3ZdtpyIvbS0eRZs/BH+mVLGh9xA/wOEUiiCfzQRsHj1xn7cqeWViAzQtdGluk/5CVAvr1FU3HNaaWkg7KQOSiKAzgDwCBtQhlgI40xdXgbqMkrHeP4M1p4MxoEVpZMe4oObACWwazeHP/Xas1vy5rbnmE59MpEZaA8t7AfGlW4MrVMhAB1JsFMdd0qFLpy/l93H3ptSlx1+6PQ5gUyjhmDUjMR+k6fb0yOeGdOrjN8IrWPmebZRFBjK5aCJwubgY/03VsSBMQ==", - }, - { - Key: "fingerprint", - Value: "79f809acd4e232421049d3bd208285ec", - }, - { - Key: "type", - Value: "ssh-rsa", - }, - { - Key: "bits", - Value: "2048", - }, - { - Value: "just some value", - }, + {Key: "key", Value: "AAAAB3NzaC1yc2EAAAABIwAAAQEAwVKoTY/7GFG7BmKkG6qFAHY/f3ciDX2MXTBLMEJP0xyUJsoy/CVRYw2b4qUB/GCJ5lh2InP+LVnPD3ZdtpyIvbS0eRZs/BH+mVLGh9xA/wOEUiiCfzQRsHj1xn7cqeWViAzQtdGluk/5CVAvr1FU3HNaaWkg7KQOSiKAzgDwCBtQhlgI40xdXgbqMkrHeP4M1p4MxoEVpZMe4oObACWwazeHP/Xas1vy5rbnmE59MpEZaA8t7AfGlW4MrVMhAB1JsFMdd0qFLpy/l93H3ptSlx1+6PQ5gUyjhmDUjMR+k6fb0yOeGdOrjN8IrWPmebZRFBjK5aCJwubgY/03VsSBMQ=="}, + {Key: "fingerprint", Value: "79f809acd4e232421049d3bd208285ec"}, + {Key: "type", Value: "ssh-rsa"}, + {Key: "bits", Value: "2048"}, + {Value: "just some value"}, }, Tables: []Table{ { Elements: []Element{ - { - Key: "important element", - Value: "ssh-rsa", - }, - { - Value: "just some value", - }, + {Key: "important element", Value: "ssh-rsa"}, + {Value: "just some value"}, }, }, { Key: "dialects", Elements: []Element{ - { - Value: "2.02", - }, - { - Value: "2.10", - }, + {Value: "2.02"}, + {Value: "2.10"}, }, }, }, @@ -241,14 +185,10 @@ func TestFormatTableXML(t *testing.T) { } XML, err := xml.Marshal(table) - if err != nil { - panic(err) - } + require.NoError(t, err) for _, expectedXMLElement := range expectedXML { - if !bytes.Contains(XML, expectedXMLElement) { - t.Errorf("missing %s in %s", expectedXMLElement, XML) - } + assert.Contains(t, string(XML), string(expectedXMLElement)) } } @@ -256,84 +196,54 @@ func TestStringMethods(t *testing.T) { s := Status{ State: "up", } - - if s.String() != s.State { - t.Errorf("expected string method to output %s, got %s", s.State, s.String()) - } + assert.Equal(t, s.State, s.String()) a := Address{ Addr: "192.168.1.1", } - - if a.String() != a.Addr { - t.Errorf("expected string method to output %s, got %s", a.Addr, a.String()) - } + assert.Equal(t, a.Addr, a.String()) h := Hostname{ Name: "toto.test", } - - if h.String() != h.Name { - t.Errorf("expected string method to output %s, got %s", h.Name, h.String()) - } + assert.Equal(t, h.Name, h.String()) s2 := State{ State: "open", } - - if s2.String() != s2.State { - t.Errorf("expected string method to output %s, got %s", s2.State, s2.String()) - } + assert.Equal(t, s2.State, s2.String()) o := Owner{ Name: "test", } - - if o.String() != o.Name { - t.Errorf("expected string method to output %s, got %s", o.Name, o.String()) - } + assert.Equal(t, o.Name, o.String()) s3 := Service{ Name: "http", } - - if s3.String() != s3.Name { - t.Errorf("expected string method to output %s, got %s", s3.Name, s3.String()) - } + assert.Equal(t, s3.Name, s3.String()) } func TestToFile(t *testing.T) { r := &Run{} err := r.ToFile(os.TempDir() + string(os.PathSeparator) + "toto.txt") - - if err != nil { - t.Errorf("expected ToFile method to properly call ioutil.WriteFile, got %v", err) - } + require.NoError(t, err) } func TestToReader(t *testing.T) { inputFile := "tests/xml/scan_base.xml" - rawXML, err := ioutil.ReadFile(inputFile) - if err != nil { - t.Fatal(err) - } + rawXML, err := os.ReadFile(inputFile) + require.NoError(t, err) - var result Run - err = Parse(rawXML, &result) - if err != nil { - t.Fatal(err) - } + result, err := parse(rawXML) + require.NoError(t, err) reader := result.ToReader() - byteOutput, err := ioutil.ReadAll(reader) - if err != nil { - t.Fatal(err) - } + byteOutput, err := io.ReadAll(reader) + require.NoError(t, err) - if !bytes.Equal(byteOutput, result.rawXML) { - t.Error("expected ToReader method to return lexicographically identical results") - } + assert.Equal(t, string(byteOutput), string(rawXML)) } func TestTimestampJSONMarshaling(t *testing.T) { @@ -344,22 +254,13 @@ func TestTimestampJSONMarshaling(t *testing.T) { ts2 := Timestamp{} b, err := ts.MarshalJSON() - if err != nil { - t.Errorf("expected marshaljson to never return an error, got %v", err) - } - - if !bytes.Equal(b, dateBytes) { - t.Errorf("expected json-encoded timestamp to be %s got %s", dateBytes, b) - } + require.NoError(t, err) + assert.Equal(t, []byte("943920000"), b) err = json.Unmarshal(dateBytes, &ts2) - if err != nil { - t.Errorf("expected datebytes to be unmarshaled in ts2, got error %v", err) - } + require.NoError(t, err) - if ts.FormatTime() != ts2.FormatTime() { - t.Errorf("expected timestamps to be equal, got %s and %s", ts.FormatTime(), ts2.FormatTime()) - } + assert.Equal(t, ts.FormatTime(), ts2.FormatTime()) } func TestTimestampXMLMarshaling(t *testing.T) { @@ -370,33 +271,17 @@ func TestTimestampXMLMarshaling(t *testing.T) { ts := Timestamp(dateTime) ts2 := Timestamp{} - x, err := ts.MarshalXMLAttr(attrName) - if err != nil { - t.Errorf("expected marshaljson to never return an error, got %v", err) - } - - if x.Value != dateXML.Value { - t.Errorf("expected xml-encoded timestamp to be %s got %s", dateXML.Value, x.Value) - } - - x, err = ts2.MarshalXMLAttr(attrName) - if err != nil { - t.Errorf("expected marshaljson to never return an error, got %v", err) - } + got, err := ts.MarshalXMLAttr(attrName) + require.NoError(t, err) + assert.Equal(t, dateXML.Value, got.Value) - emptyAttr := xml.Attr{} - if x != emptyAttr { - t.Errorf("expected zero time to return empty attribute, got %v", x) - } + got, err = ts2.MarshalXMLAttr(attrName) + require.NoError(t, err) + assert.Equal(t, xml.Attr{}, got) err = ts2.UnmarshalXMLAttr(dateXML) - if err != nil { - t.Errorf("expected datebytes to be unmarshaled in ts2, got error %v", err) - } - - if ts.FormatTime() != ts2.FormatTime() { - t.Errorf("expected timestamps to be equal, got %s and %s", ts.FormatTime(), ts2.FormatTime()) - } + require.NoError(t, err) + assert.Equal(t, ts.FormatTime(), ts2.FormatTime()) } func TestParseRunXML(t *testing.T) { @@ -404,7 +289,7 @@ func TestParseRunXML(t *testing.T) { inputFile string expectedResult *Run - expectedError error + wantErr require.ErrorAssertionFunc }{ { inputFile: "tests/xml/scan_base.xml", @@ -1075,198 +960,265 @@ func TestParseRunXML(t *testing.T) { }, }, - expectedError: nil, + wantErr: require.NoError, }, } for _, test := range tests { t.Run(test.inputFile, func(t *testing.T) { - rawXML, err := ioutil.ReadFile(test.inputFile) - if err != nil { - t.Fatal(err) - } - - var result Run - err = Parse(rawXML, &result) + rawXML, err := os.ReadFile(test.inputFile) + require.NoError(t, err) - // Remove rawXML before comparing - if err != nil { - result.rawXML = []byte{} - } - - compareResults(t, test.expectedResult, &result) + result, err := parse(rawXML) + test.wantErr(t, err) - if err != test.expectedError { - t.Errorf("expected %v got %v", test.expectedError, err) - } + compareResults(t, test.expectedResult, result) }) } } func compareResults(t *testing.T, expected, got *Run) { - if got.Args != expected.Args { - t.Errorf("unexpected arguments, expected %v got %v", expected.Args, got.Args) + if expected == nil { + require.Nil(t, got) + return } - if got.ProfileName != expected.ProfileName { - t.Errorf("unexpected arguments, expected %v got %v", expected.ProfileName, got.ProfileName) + require.NotNil(t, got) + if len(expected.Args) > 0 { // We don't care if there are extra args, no need to check. + assert.Equal(t, expected.Args, got.Args, "unexpected arguments") } - - if got.Scanner != expected.Scanner { - t.Errorf("unexpected arguments, expected %v got %v", expected.Scanner, got.Scanner) + if expected.ProfileName != "" { + assert.Equal(t, expected.ProfileName, got.ProfileName, "unexpected profile name") } - - if got.StartStr != expected.StartStr { - t.Errorf("unexpected arguments, expected %v got %v", expected.StartStr, got.StartStr) + if expected.Scanner != "" { + assert.Equal(t, expected.Scanner, got.Scanner, "unexpected scanner") } - - if !reflect.DeepEqual(got.Debugging, expected.Debugging) { - t.Errorf("unexpected debugging, expected %+v got %+v", expected.Debugging, got.Debugging) + if expected.StartStr != "" { + assert.Equal(t, expected.StartStr, got.StartStr, "unexpected start string") } - - if !reflect.DeepEqual(got.ScanInfo, expected.ScanInfo) { - t.Errorf("unexpected scan info, expected %+v got %+v", expected.ScanInfo, got.ScanInfo) + if expected.Debugging.Level != 0 { + assert.Equal(t, expected.Debugging.Level, got.Debugging.Level, "unexpected debugging level") } - - if !reflect.DeepEqual(got.Start, expected.Start) { - t.Errorf("unexpected start time, expected %+v got %+v", expected.Start, got.Start) + if expected.ScanInfo.NumServices != 0 { + assert.Equal(t, expected.ScanInfo.NumServices, got.ScanInfo.NumServices, "unexpected scan info num services") } - - if !reflect.DeepEqual(got.Targets, expected.Targets) { - t.Errorf("unexpected targets, expected %+v got %+v", expected.Targets, got.Targets) + if expected.ScanInfo.Protocol != "" { + assert.Equal(t, expected.ScanInfo.Protocol, got.ScanInfo.Protocol, "unexpected scan info protocol") + } + if expected.ScanInfo.ScanFlags != "" { + assert.Equal(t, expected.ScanInfo.ScanFlags, got.ScanInfo.ScanFlags, "unexpected scan info scan flags") + } + if expected.ScanInfo.Services != "" { + assert.Equal(t, expected.ScanInfo.Services, got.ScanInfo.Services, "unexpected scan info services") + } + if expected.ScanInfo.Type != "" { + assert.Equal(t, expected.ScanInfo.Type, got.ScanInfo.Type, "unexpected scan info type") + } + if !time.Time(expected.Start).IsZero() { + assert.Equal(t, expected.Start, got.Start, "unexpected start time") + } + if len(expected.Targets) > 0 { + assert.Equal(t, expected.Targets, got.Targets, "unexpected targets") } - if len(expected.TaskBegin) != len(got.TaskBegin) { - t.Errorf("unexpected tasks begin entries, expected to have %d entries, got %d instead", len(expected.TaskBegin), len(got.TaskBegin)) - } else { - for idx := range expected.TaskBegin { - if !reflect.DeepEqual(got.TaskBegin[idx], expected.TaskBegin[idx]) { - t.Errorf("unexpected task begin entry, expected %+v got %+v", expected.TaskBegin[idx], got.TaskBegin[idx]) + if len(expected.TaskBegin) > 0 { + if assert.Len(t, got.TaskBegin, len(expected.TaskBegin), "unexpected tasks begin entries") { + for idx := range expected.TaskBegin { + if !time.Time(expected.TaskBegin[idx].Time).IsZero() { + assert.Equalf(t, expected.TaskBegin[idx].Time, got.TaskBegin[idx].Time, "unexpected task begin time at index %d", idx) + } + if expected.TaskBegin[idx].Task != "" { + assert.Equalf(t, expected.TaskBegin[idx].Task, got.TaskBegin[idx].Task, "unexpected task begin task at index %d", idx) + } + if expected.TaskBegin[idx].ExtraInfo != "" { + assert.Equalf(t, expected.TaskBegin[idx].ExtraInfo, got.TaskBegin[idx].ExtraInfo, "unexpected task begin extra info at index %d", idx) + } } } } - if len(expected.TaskProgress) != len(got.TaskProgress) { - t.Errorf("unexpected tasks progress entries, expected to have %d entries, got %d instead", len(expected.TaskProgress), len(got.TaskProgress)) - } else { - for idx := range expected.TaskProgress { - if !reflect.DeepEqual(got.TaskProgress[idx], expected.TaskProgress[idx]) { - t.Errorf("unexpected task progress entry, expected %+v got %+v", expected.TaskProgress[idx], got.TaskProgress[idx]) + if len(expected.TaskProgress) > 0 { + if assert.Len(t, got.TaskProgress, len(expected.TaskProgress), "unexpected tasks progress entries") { + for idx := range expected.TaskProgress { + if expected.TaskProgress[idx].Percent != 0 { + assert.Equalf(t, expected.TaskProgress[idx].Percent, got.TaskProgress[idx].Percent, "unexpected task progress percent at index %d", idx) + } + if expected.TaskProgress[idx].Remaining != 0 { + assert.Equalf(t, expected.TaskProgress[idx].Remaining, got.TaskProgress[idx].Remaining, "unexpected task progress remaining at index %d", idx) + } + if expected.TaskProgress[idx].Task != "" { + assert.Equalf(t, expected.TaskProgress[idx].Task, got.TaskProgress[idx].Task, "unexpected task progress task at index %d", idx) + } + if !time.Time(expected.TaskProgress[idx].Etc).IsZero() { + assert.Equalf(t, expected.TaskProgress[idx].Etc, got.TaskProgress[idx].Etc, "unexpected task progress etc at index %d", idx) + } + if !time.Time(expected.TaskProgress[idx].Time).IsZero() { + assert.Equalf(t, expected.TaskProgress[idx].Time, got.TaskProgress[idx].Time, "unexpected task progress time at index %d", idx) + } } } } - if len(expected.TaskEnd) != len(got.TaskEnd) { - t.Errorf("unexpected tasks end entries, expected to have %d entries, got %d instead", len(expected.TaskEnd), len(got.TaskEnd)) - } else { - for idx := range expected.TaskEnd { - if !reflect.DeepEqual(got.TaskEnd[idx], expected.TaskEnd[idx]) { - t.Errorf("unexpected task end entry, expected %+v got %+v", expected.TaskEnd[idx], got.TaskEnd[idx]) + if len(expected.TaskEnd) > 0 { + if assert.Len(t, got.TaskEnd, len(expected.TaskEnd), "unexpected tasks end entries") { + for idx := range expected.TaskEnd { + if !time.Time(expected.TaskEnd[idx].Time).IsZero() { + assert.Equalf(t, expected.TaskEnd[idx].Time, got.TaskEnd[idx].Time, "unexpected task end time at index %d", idx) + } + if expected.TaskEnd[idx].Task != "" { + assert.Equalf(t, expected.TaskEnd[idx].Task, got.TaskEnd[idx].Task, "unexpected task end task at index %d", idx) + } + if expected.TaskEnd[idx].ExtraInfo != "" { + assert.Equalf(t, expected.TaskEnd[idx].ExtraInfo, got.TaskEnd[idx].ExtraInfo, "unexpected task end extra info at index %d", idx) + } } } } - if len(expected.Hosts) != len(got.Hosts) { - t.Errorf("unexpected number of hosts, expected to have %d hosts, got %d instead", len(expected.Hosts), len(got.Hosts)) - } else { - for idx := range expected.Hosts { - if expected.Hosts[idx].Comment != got.Hosts[idx].Comment { - t.Errorf("unexpected host comment, expected %v got %v", expected.Hosts[idx].Comment, got.Hosts[idx].Comment) - } - - if !reflect.DeepEqual(expected.Hosts[idx].Addresses, got.Hosts[idx].Addresses) { - t.Errorf("unexpected host addresses, expected %+v got %+v", expected.Hosts[idx].Addresses, got.Hosts[idx].Addresses) - } - - if !reflect.DeepEqual(expected.Hosts[idx].Distance, got.Hosts[idx].Distance) { - t.Errorf("unexpected host distance, expected %+v got %+v", expected.Hosts[idx].Distance, got.Hosts[idx].Distance) - } - - if !reflect.DeepEqual(expected.Hosts[idx].EndTime, got.Hosts[idx].EndTime) { - t.Errorf("unexpected host end time, expected %+v got %+v", expected.Hosts[idx].EndTime, got.Hosts[idx].EndTime) - } + if len(expected.Hosts) == 0 { + return + } + if !assert.Len(t, got.Hosts, len(expected.Hosts), "unexpected number of hosts") { + return + } - if !reflect.DeepEqual(expected.Hosts[idx].ExtraPorts, got.Hosts[idx].ExtraPorts) { - t.Errorf("unexpected host extra ports, expected %+v got %+v", expected.Hosts[idx].ExtraPorts, got.Hosts[idx].ExtraPorts) - } + for idx := range expected.Hosts { + if expected.Hosts[idx].Comment != "" { + assert.Equalf(t, expected.Hosts[idx].Comment, got.Hosts[idx].Comment, "unexpected host comment at index %d", idx) + } + if len(expected.Hosts[idx].Addresses) > 0 { + assert.Equalf(t, expected.Hosts[idx].Addresses, got.Hosts[idx].Addresses, "unexpected host addresses at index %d", idx) + } + if expected.Hosts[idx].Distance.Value != 0 { + assert.Equalf(t, expected.Hosts[idx].Distance.Value, got.Hosts[idx].Distance.Value, "unexpected host distance at index %d", idx) + } + if !time.Time(expected.Hosts[idx].EndTime).IsZero() { + assert.Equalf(t, expected.Hosts[idx].EndTime, got.Hosts[idx].EndTime, "unexpected host end time at index %d", idx) + } + if len(expected.Hosts[idx].ExtraPorts) > 0 { + assert.Equalf(t, expected.Hosts[idx].ExtraPorts, got.Hosts[idx].ExtraPorts, "unexpected host extra ports at index %d", idx) + } + if len(expected.Hosts[idx].HostScripts) > 0 { + assert.Equalf(t, expected.Hosts[idx].HostScripts, got.Hosts[idx].HostScripts, "unexpected host host scripts at index %d", idx) + } + if len(expected.Hosts[idx].Hostnames) > 0 { + assert.Equalf(t, expected.Hosts[idx].Hostnames, got.Hosts[idx].Hostnames, "unexpected host host names at index %d", idx) + } + if expected.Hosts[idx].IPIDSequence.Class != "" { + assert.Equalf(t, expected.Hosts[idx].IPIDSequence.Class, got.Hosts[idx].IPIDSequence.Class, "unexpected host IPIDSequence class at index %d", idx) + } + if expected.Hosts[idx].IPIDSequence.Values != "" { + assert.Equalf(t, expected.Hosts[idx].IPIDSequence.Values, got.Hosts[idx].IPIDSequence.Values, "unexpected host IPIDSequence values at index %d", idx) + } - if !reflect.DeepEqual(expected.Hosts[idx].HostScripts, got.Hosts[idx].HostScripts) { - t.Errorf("unexpected host host scripts, expected %+v got %+v", expected.Hosts[idx].HostScripts, got.Hosts[idx].HostScripts) - } + if len(expected.Hosts[idx].OS.PortsUsed) > 0 { + assert.Equalf(t, expected.Hosts[idx].OS.PortsUsed, got.Hosts[idx].OS.PortsUsed, "unexpected host ports used at index %d", idx) + } + if len(expected.Hosts[idx].OS.Fingerprints) > 0 { + assert.Equalf(t, expected.Hosts[idx].OS.Fingerprints, got.Hosts[idx].OS.Fingerprints, "unexpected host os fingerprints at index %d", idx) + } - if !reflect.DeepEqual(expected.Hosts[idx].Hostnames, got.Hosts[idx].Hostnames) { - t.Errorf("unexpected host host names, expected %+v got %+v", expected.Hosts[idx].Hostnames, got.Hosts[idx].Hostnames) - } + if len(expected.Hosts[idx].Ports) > 0 { + assert.Equalf(t, expected.Hosts[idx].Ports, got.Hosts[idx].Ports, "unexpected host ports at index %d", idx) + } + if len(expected.Hosts[idx].Smurfs) > 0 { + assert.Equalf(t, expected.Hosts[idx].Smurfs, got.Hosts[idx].Smurfs, "unexpected host smurfs at index %d", idx) + } + if !time.Time(expected.Hosts[idx].StartTime).IsZero() { + assert.Equalf(t, expected.Hosts[idx].StartTime, got.Hosts[idx].StartTime, "unexpected host start time at index %d", idx) + } + if expected.Hosts[idx].TimedOut { + assert.Equalf(t, expected.Hosts[idx].TimedOut, got.Hosts[idx].TimedOut, "unexpected host timedout at index %d", idx) + } + if expected.Hosts[idx].Status.State != "" { + assert.Equalf(t, expected.Hosts[idx].Status.State, got.Hosts[idx].Status.State, "unexpected host status state at index %d", idx) + } + if expected.Hosts[idx].Status.Reason != "" { + assert.Equalf(t, expected.Hosts[idx].Status.Reason, got.Hosts[idx].Status.Reason, "unexpected host status reason at index %d", idx) + } + if expected.Hosts[idx].Status.ReasonTTL != 0 { + assert.Equalf(t, expected.Hosts[idx].Status.ReasonTTL, got.Hosts[idx].Status.ReasonTTL, "unexpected host status reason TTL at index %d", idx) + } + if expected.Hosts[idx].TCPSequence.Index != 0 { + assert.Equalf(t, expected.Hosts[idx].TCPSequence.Index, got.Hosts[idx].TCPSequence.Index, "unexpected host TCPSequence index at index %d", idx) + } + if expected.Hosts[idx].TCPSequence.Difficulty != "" { + assert.Equalf(t, expected.Hosts[idx].TCPSequence.Difficulty, got.Hosts[idx].TCPSequence.Difficulty, "unexpected host TCPSequence difficulty at index %d", idx) + } + if expected.Hosts[idx].TCPSequence.Values != "" { + assert.Equalf(t, expected.Hosts[idx].TCPSequence.Values, got.Hosts[idx].TCPSequence.Values, "unexpected host TCPSequence values at index %d", idx) + } + if expected.Hosts[idx].TCPTSSequence.Class != "" { + assert.Equalf(t, expected.Hosts[idx].TCPTSSequence.Class, got.Hosts[idx].TCPTSSequence.Class, "unexpected host TCPTSSequence class at index %d", idx) + } + if expected.Hosts[idx].TCPTSSequence.Values != "" { + assert.Equalf(t, expected.Hosts[idx].TCPTSSequence.Values, got.Hosts[idx].TCPTSSequence.Values, "unexpected host TCPTSSequence values at index %d", idx) + } + if expected.Hosts[idx].Times.SRTT != "" { + assert.Equalf(t, expected.Hosts[idx].Times.SRTT, got.Hosts[idx].Times.SRTT, "unexpected host times SRTT at index %d", idx) + } + if expected.Hosts[idx].Times.RTT != "" { + assert.Equalf(t, expected.Hosts[idx].Times.RTT, got.Hosts[idx].Times.RTT, "unexpected host times RTT at index %d", idx) + } + if expected.Hosts[idx].Times.To != "" { + assert.Equalf(t, expected.Hosts[idx].Times.To, got.Hosts[idx].Times.To, "unexpected host times To at index %d", idx) + } + if expected.Hosts[idx].Trace.Proto != "" { + assert.Equalf(t, expected.Hosts[idx].Trace.Proto, got.Hosts[idx].Trace.Proto, "unexpected host trace proto at index %d", idx) + } + if expected.Hosts[idx].Trace.Port != 0 { + assert.Equalf(t, expected.Hosts[idx].Trace.Port, got.Hosts[idx].Trace.Port, "unexpected host trace port at index %d", idx) + } + if len(expected.Hosts[idx].Trace.Hops) > 0 { + assert.Equalf(t, expected.Hosts[idx].Trace.Hops, got.Hosts[idx].Trace.Hops, "unexpected host trace hops at index %d", idx) + } + if expected.Hosts[idx].Uptime.Seconds != 0 { + assert.Equalf(t, expected.Hosts[idx].Uptime.Seconds, got.Hosts[idx].Uptime.Seconds, "unexpected host uptime seconds at index %d", idx) + } + if expected.Hosts[idx].Uptime.Lastboot != "" { + assert.Equalf(t, expected.Hosts[idx].Uptime.Lastboot, got.Hosts[idx].Uptime.Lastboot, "unexpected host uptime lastboot at index %d", idx) + } - if !reflect.DeepEqual(expected.Hosts[idx].IPIDSequence, got.Hosts[idx].IPIDSequence) { - t.Errorf("unexpected host IPIDSequence, expected %+v got %+v", expected.Hosts[idx].IPIDSequence, got.Hosts[idx].IPIDSequence) - } + if len(expected.Hosts[idx].OS.Matches) == 0 { + continue + } + if assert.Len(t, got.Hosts[idx].OS.Matches, len(expected.Hosts[idx].OS.Matches), "unexpected number of host matches at index %d", idx) { + for i := range expected.Hosts[idx].OS.Matches { + if expected.Hosts[idx].OS.Matches[i].Name != "" { + assert.Equalf(t, expected.Hosts[idx].OS.Matches[i].Name, got.Hosts[idx].OS.Matches[i].Name, "unexpected host os match name at index %d match %d", idx, i) + } + if expected.Hosts[idx].OS.Matches[i].Accuracy != 0 { + assert.Equalf(t, expected.Hosts[idx].OS.Matches[i].Accuracy, got.Hosts[idx].OS.Matches[i].Accuracy, "unexpected host os match accuracy at index %d match %d", idx, i) + } + if expected.Hosts[idx].OS.Matches[i].Line != 0 { + assert.Equalf(t, expected.Hosts[idx].OS.Matches[i].Line, got.Hosts[idx].OS.Matches[i].Line, "unexpected host os match line at index %d match %d", idx, i) + } - if len(expected.Hosts[idx].OS.Matches) != len(got.Hosts[idx].OS.Matches) { - t.Errorf("unexpected number of host matches, expected to have %d classes, got %d instead", - len(expected.Hosts[idx].OS.Matches), len(got.Hosts[idx].OS.Matches)) - } else { - for i := range expected.Hosts[idx].OS.Matches { - if len(expected.Hosts[idx].OS.Matches[i].Classes) != len(got.Hosts[idx].OS.Matches[i].Classes) { - t.Errorf("unexpected number of host classes, expected to have %d classes, got %d instead", - len(expected.Hosts[idx].OS.Matches[i].Classes), len(got.Hosts[idx].OS.Matches[i].Classes)) - } else { + if len(expected.Hosts[idx].OS.Matches[i].Classes) > 0 { + if assert.Len(t, got.Hosts[idx].OS.Matches[i].Classes, len(expected.Hosts[idx].OS.Matches[i].Classes), "unexpected number of host classes at index %d match %d", idx, i) { for j := range expected.Hosts[idx].OS.Matches[i].Classes { - if !reflect.DeepEqual(expected.Hosts[idx].OS.Matches[i].Classes[j], got.Hosts[idx].OS.Matches[i].Classes[j]) { - t.Errorf("unexpected host os class, expected %+v got %+v", expected.Hosts[idx].OS.Matches[i], got.Hosts[idx].OS.Matches[i].Classes[j]) + if expected.Hosts[idx].OS.Matches[i].Classes[j].Vendor != "" { + assert.Equalf(t, expected.Hosts[idx].OS.Matches[i].Classes[j].Vendor, got.Hosts[idx].OS.Matches[i].Classes[j].Vendor, "unexpected host os class vendor at index %d match %d class %d", idx, i, j) + } + if expected.Hosts[idx].OS.Matches[i].Classes[j].OSGeneration != "" { + assert.Equalf(t, expected.Hosts[idx].OS.Matches[i].Classes[j].OSGeneration, got.Hosts[idx].OS.Matches[i].Classes[j].OSGeneration, "unexpected host os class os generation at index %d match %d class %d", idx, i, j) + } + if expected.Hosts[idx].OS.Matches[i].Classes[j].Type != "" { + assert.Equalf(t, expected.Hosts[idx].OS.Matches[i].Classes[j].Type, got.Hosts[idx].OS.Matches[i].Classes[j].Type, "unexpected host os class type at index %d match %d class %d", idx, i, j) + } + if expected.Hosts[idx].OS.Matches[i].Classes[j].Accuracy != 0 { + assert.Equalf(t, expected.Hosts[idx].OS.Matches[i].Classes[j].Accuracy, got.Hosts[idx].OS.Matches[i].Classes[j].Accuracy, "unexpected host os class accuracy at index %d match %d class %d", idx, i, j) + } + if expected.Hosts[idx].OS.Matches[i].Classes[j].Family != "" { + assert.Equalf(t, expected.Hosts[idx].OS.Matches[i].Classes[j].Family, got.Hosts[idx].OS.Matches[i].Classes[j].Family, "unexpected host os class family at index %d match %d class %d", idx, i, j) + } + if len(expected.Hosts[idx].OS.Matches[i].Classes[j].CPEs) > 0 { + assert.Equalf(t, expected.Hosts[idx].OS.Matches[i].Classes[j].CPEs, got.Hosts[idx].OS.Matches[i].Classes[j].CPEs, "unexpected host os class CPEs at index %d match %d class %d", idx, i, j) } - } - - if !reflect.DeepEqual(expected.Hosts[idx].OS.Matches[i], got.Hosts[idx].OS.Matches[i]) { - t.Errorf("unexpected host os match, expected %+v got %+v", expected.Hosts[idx].OS.Matches[i], got.Hosts[idx].OS.Matches[i]) } } } } - - if !reflect.DeepEqual(expected.Hosts[idx].OS, got.Hosts[idx].OS) { - t.Errorf("unexpected host OS, expected %+v got %+v", expected.Hosts[idx].OS, got.Hosts[idx].OS) - } - - if !reflect.DeepEqual(expected.Hosts[idx].Ports, got.Hosts[idx].Ports) { - t.Errorf("unexpected host ports, expected %+v got %+v", expected.Hosts[idx].Ports, got.Hosts[idx].Ports) - } - - if !reflect.DeepEqual(expected.Hosts[idx].Smurfs, got.Hosts[idx].Smurfs) { - t.Errorf("unexpected host smurfs, expected %+v got %+v", expected.Hosts[idx].Smurfs, got.Hosts[idx].Smurfs) - } - - if !reflect.DeepEqual(expected.Hosts[idx].StartTime, got.Hosts[idx].StartTime) { - t.Errorf("unexpected host start time, expected %+v got %+v", expected.Hosts[idx].StartTime, got.Hosts[idx].StartTime) - } - - if !reflect.DeepEqual(expected.Hosts[idx].TimedOut, got.Hosts[idx].TimedOut) { - t.Errorf("unexpected host timedout, expected %+v got %+v", expected.Hosts[idx].TimedOut, got.Hosts[idx].TimedOut) - } - - if !reflect.DeepEqual(expected.Hosts[idx].Status, got.Hosts[idx].Status) { - t.Errorf("unexpected host status, expected %+v got %+v", expected.Hosts[idx].Status, got.Hosts[idx].Status) - } - - if !reflect.DeepEqual(expected.Hosts[idx].TCPSequence, got.Hosts[idx].TCPSequence) { - t.Errorf("unexpected host TCPSequence, expected %+v got %+v", expected.Hosts[idx].TCPSequence, got.Hosts[idx].TCPSequence) - } - - if !reflect.DeepEqual(expected.Hosts[idx].TCPTSSequence, got.Hosts[idx].TCPTSSequence) { - t.Errorf("unexpected host TCPTSSequence, expected %+v got %+v", expected.Hosts[idx].TCPTSSequence, got.Hosts[idx].TCPTSSequence) - } - - if !reflect.DeepEqual(expected.Hosts[idx].Times, got.Hosts[idx].Times) { - t.Errorf("unexpected host times, expected %+v got %+v", expected.Hosts[idx].Times, got.Hosts[idx].Times) - } - - if !reflect.DeepEqual(expected.Hosts[idx].Trace, got.Hosts[idx].Trace) { - t.Errorf("unexpected host trace, expected %+v got %+v", expected.Hosts[idx].Trace, got.Hosts[idx].Trace) - } - - if !reflect.DeepEqual(expected.Hosts[idx].Uptime, got.Hosts[idx].Uptime) { - t.Errorf("unexpected host uptime, expected %+v got %+v", expected.Hosts[idx].Uptime, got.Hosts[idx].Uptime) - } } } }