Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix run with progress and add new Run function #69

Merged
merged 8 commits into from
Feb 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ dist: trusty
sudo: required
language: go

go:
- 1.15.x

before_install:
- sudo apt-get install -y nmap
- go mod tidy
- go get github.com/mattn/goveralls

script:
Expand Down
65 changes: 65 additions & 0 deletions examples/basic_scan_streamer_interface/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package main

import (
"fmt"
"io/ioutil"
"log"
"strings"

"github.com/Ullaakut/nmap/v2"
)

// CustomType is your custom type in code.
// You just have to make it a Streamer.
type CustomType struct {
nmap.Streamer
File string
}

// Write is a function that handles the normal nmap stdout.
func (c *CustomType) Write(d []byte) (int, error) {
lines := string(d)

if strings.Contains(lines, "Stats: ") {
fmt.Print(lines)
}
return len(d), nil
}

// Bytes returns scan result bytes.
func (c *CustomType) Bytes() []byte {
data, err := ioutil.ReadFile(c.File)
if err != nil {
data = append(data, "\ncould not read File"...)
}
return data
}

func main() {
cType := &CustomType{
File: "/tmp/output.xml",
}
scanner, err := nmap.NewScanner(
nmap.WithTargets("localhost"),
nmap.WithPorts("1-4000"),
nmap.WithServiceInfo(),
nmap.WithVerbosity(3),
)
if err != nil {
log.Fatalf("unable to create nmap scanner: %v", err)
}

warnings, err := scanner.RunWithStreamer(cType, cType.File)
if err != nil {
log.Fatalf("unable to run nmap scan: %v", err)
}

fmt.Printf("Nmap warnings: %v\n", warnings)

result, err := nmap.Parse(cType.Bytes())
if err != nil {
log.Fatalf("unable to parse nmap output: %v", err)
}

fmt.Printf("Nmap done: %d hosts up scanned in %.2f seconds\n", len(result.Hosts), result.Stats.Finished.Elapsed)
}
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ module github.com/Ullaakut/nmap/v2

go 1.15

require github.com/stretchr/testify v1.6.1
require (
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.6.1
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
Expand Down
134 changes: 105 additions & 29 deletions nmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,26 @@ import (
"context"
"encoding/xml"
"fmt"
"io"
"os/exec"
"strings"
"time"

"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)

// ScanRunner represents something that can run a scan.
type ScanRunner interface {
Run() (result *Run, warnings []string, err error)
}

// Streamer constantly streams the stdout.
type Streamer interface {
Write(d []byte) (int, error)
Bytes() []byte
}

// Scanner represents an Nmap scanner.
type Scanner struct {
cmd *exec.Cmd
Expand Down Expand Up @@ -110,14 +120,9 @@ func (s *Scanner) Run() (result *Run, warnings []string, err error) {
warnings = strings.Split(strings.Trim(stderr.String(), "\n"), "\n")
}

// Check for warnings that will inevitable lead to parsing errors, hence, have priority
for _, warning := range warnings {
switch {
case strings.Contains(warning, "Malloc Failed!"):
return nil, warnings, ErrMallocFailed
// TODO: Add cases for other known errors we might want to guard.
default:
}
// Check for warnings that will inevitably lead to parsing errors, hence, have priority.
if err := analyzeWarnings(warnings); err != nil {
return nil, warnings, err
}

// Parse nmap xml output. Usually nmap always returns valid XML, even if there is a scan error.
Expand Down Expand Up @@ -157,21 +162,21 @@ func (s *Scanner) Run() (result *Run, warnings []string, err error) {
func (s *Scanner) RunWithProgress(liveProgress chan<- float32) (result *Run, warnings []string, err error) {
var stdout, stderr bytes.Buffer

// Enable XML output
// Enable XML output.
s.args = append(s.args, "-oX")

// Get XML output in stdout instead of writing it in a file
// Get XML output in stdout instead of writing it in a file.
s.args = append(s.args, "-")

// Enable progress output every second
// Enable progress output every second.
s.args = append(s.args, "--stats-every", "1s")

// Prepare nmap process
// Prepare nmap process.
cmd := exec.Command(s.binaryPath, s.args...)
cmd.Stderr = &stderr
cmd.Stdout = &stdout

// Run nmap process
// Run nmap process.
err = cmd.Start()
if err != nil {
return nil, warnings, err
Expand All @@ -184,8 +189,8 @@ func (s *Scanner) RunWithProgress(liveProgress chan<- float32) (result *Run, war
done <- cmd.Wait()
}()

// Make goroutine to check the progress every second
// Listening for channel doneProgress
// Make goroutine to check the progress every second.
// Listening for channel doneProgress.
go func() {
type progress struct {
TaskProgress []TaskProgress `xml:"taskprogress" json:"task_progress"`
Expand All @@ -207,34 +212,30 @@ func (s *Scanner) RunWithProgress(liveProgress chan<- float32) (result *Run, war
}
}()

// Wait for nmap process or timeout
// Wait for nmap process or timeout.
select {
case <-s.ctx.Done():
// Trigger progress function exit.
close(doneProgress)

// Context was done before the scan was finished.
// The process is killed and a timeout error is returned.
_ = cmd.Process.Kill()

return nil, warnings, ErrScanTimeout
case <-done:

// Trigger progress function exit
// Trigger progress function exit.
close(doneProgress)

// Process nmap stderr output containing none-critical errors and warnings
// Everyone needs to check whether one or some of these warnings is a hard issue in their use case
// Process nmap stderr output containing none-critical errors and warnings.
// Everyone needs to check whether one or some of these warnings is a hard issue in their use case.
if stderr.Len() > 0 {
warnings = strings.Split(strings.Trim(stderr.String(), "\n"), "\n")
}

// Check for warnings that will inevitable lead to parsing errors, hence, have priority
for _, warning := range warnings {
switch {
case strings.Contains(warning, "Malloc Failed!"):
return nil, warnings, ErrMallocFailed
// TODO: Add cases for other known errors we might want to guard.
default:
}
// Check for warnings that will inevitably lead to parsing errors, hence, have priority.
if err := analyzeWarnings(warnings); err != nil {
return nil, warnings, err
}

// Parse nmap xml output. Usually nmap always returns valid XML, even if there is a scan error.
Expand Down Expand Up @@ -264,11 +265,73 @@ func (s *Scanner) RunWithProgress(liveProgress chan<- float32) (result *Run, war
result = chooseHosts(result, s.hostFilter)
}

// Return result, optional warnings but no error
// Return result, optional warnings but no error.
return result, warnings, nil
}
}

// RunWithStreamer runs nmap synchronously. The XML output is written directly to a file.
// It uses a streamer interface to constantly stream the stdout.
func (s *Scanner) RunWithStreamer(stream Streamer, file string) (warnings []string, err error) {
Ullaakut marked this conversation as resolved.
Show resolved Hide resolved
// Enable XML output.
s.args = append(s.args, "-oX")

// Get XML output in stdout instead of writing it in a file.
s.args = append(s.args, file)

// Enable progress output every second.
s.args = append(s.args, "--stats-every", "5s")

// Prepare nmap process.
cmd := exec.CommandContext(s.ctx, s.binaryPath, s.args...)

// Write stderr to buffer.
stderrBuf := bytes.Buffer{}
cmd.Stderr = &stderrBuf

// Connect to the StdoutPipe.
stdoutIn, err := cmd.StdoutPipe()
if err != nil {
return warnings, errors.WithMessage(err, "connect to StdoutPipe failed")
}
stdout := stream

// Run nmap process.
if err := cmd.Start(); err != nil {
return warnings, errors.WithMessage(err, "start command failed")
}

// Copy stdout to pipe.
g, _ := errgroup.WithContext(s.ctx)
g.Go(func() error {
_, err = io.Copy(stdout, stdoutIn)
return err
})

cmdErr := cmd.Wait()
if err := g.Wait(); err != nil {
warnings = append(warnings, errors.WithMessage(err, "read from stdout failed").Error())
}
if cmdErr != nil {
return warnings, errors.WithMessage(err, "nmap command failed")
}
// Process nmap stderr output containing none-critical errors and warnings.
// Everyone needs to check whether one or some of these warnings is a hard issue in their use case.
if stderrBuf.Len() > 0 {
for _, v := range strings.Split(strings.Trim(stderrBuf.String(), "\n"), "\n") {
warnings = append(warnings, v)
}
}

// Check for warnings that will inevitably lead to parsing errors, hence, have priority.
if err := analyzeWarnings(warnings); err != nil {
return warnings, err
}

// Return result, optional warnings but no error.
return warnings, nil
}

// RunAsync runs nmap asynchronously and returns error.
// TODO: RunAsync should return warnings as well.
func (s *Scanner) RunAsync() error {
Expand Down Expand Up @@ -356,6 +419,19 @@ func choosePorts(result *Run, filter func(Port) bool) *Run {
return result
}

func analyzeWarnings(warnings []string) error {
// Check for warnings that will inevitably lead to parsing errors, hence, have priority.
for _, warning := range warnings {
switch {
case strings.Contains(warning, "Malloc Failed!"):
return ErrMallocFailed
// TODO: Add cases for other known errors we might want to guard.
default:
}
}
return nil
}

// WithContext adds a context to a scanner, to make it cancellable and able to timeout.
func WithContext(ctx context.Context) func(*Scanner) {
return func(s *Scanner) {
Expand Down
Loading