• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Ullaakut / nmap / 11294670448

11 Oct 2024 03:01PM UTC coverage: 94.51% (-0.3%) from 94.76%
11294670448

push

github

web-flow
fix: remove outdated example test (#135)

878 of 929 relevant lines covered (94.51%)

1.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.0
/nmap.go
1
// Package nmap provides idiomatic `nmap` bindings for go developers.
2
package nmap
3

4
import (
5
        "bytes"
6
        "context"
7
        "encoding/xml"
8
        "fmt"
9
        "io"
10
        "os/exec"
11
        "strings"
12
        "sync"
13
        "syscall"
14
        "time"
15

16
        "golang.org/x/sync/errgroup"
17
)
18

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

24
// Scanner represents n Nmap scanner.
25
type Scanner struct {
26
        modifySysProcAttr func(*syscall.SysProcAttr)
27

28
        args       []string
29
        binaryPath string
30
        ctx        context.Context
31

32
        portFilter func(Port) bool
33
        hostFilter func(Host) bool
34

35
        doneAsync    chan error
36
        liveProgress chan float32
37
        streamer     io.Writer
38
        toFile       *string
39
}
40

41
// Option is a function that is used for grouping of Scanner options.
42
// Option adds or removes nmap command line arguments.
43
type Option func(*Scanner)
44

45
// NewScanner creates a new Scanner, and can take options to apply to the scanner.
46
func NewScanner(ctx context.Context, options ...Option) (*Scanner, error) {
1✔
47
        scanner := &Scanner{
1✔
48
                doneAsync:    nil,
1✔
49
                liveProgress: nil,
1✔
50
                streamer:     nil,
1✔
51
                ctx:          ctx,
1✔
52
        }
1✔
53

1✔
54
        for _, option := range options {
2✔
55
                option(scanner)
1✔
56
        }
1✔
57

58
        if scanner.binaryPath == "" {
2✔
59
                var err error
1✔
60
                scanner.binaryPath, err = exec.LookPath("nmap")
1✔
61
                if err != nil {
2✔
62
                        return nil, ErrNmapNotInstalled
1✔
63
                }
1✔
64
        }
65

66
        return scanner, nil
1✔
67
}
68

69
// Async will run the nmap scan asynchronously. You need to provide a channel with error type.
70
// When the scan is finished an error or nil will be piped through this channel.
71
func (s *Scanner) Async(doneAsync chan error) *Scanner {
1✔
72
        s.doneAsync = doneAsync
1✔
73
        return s
1✔
74
}
1✔
75

76
// Progress pipes the progress of nmap every 100ms. It needs a channel of type float.
77
func (s *Scanner) Progress(liveProgress chan float32) *Scanner {
1✔
78
        s.args = append(s.args, "--stats-every", "100ms")
1✔
79
        s.liveProgress = liveProgress
1✔
80
        return s
1✔
81
}
1✔
82

83
// ToFile enables the Scanner to write the nmap XML output to a given path.
84
// Nmap will write the normal CLI output to stdout. The XML is parsed from file after the scan is finished.
85
func (s *Scanner) ToFile(file string) *Scanner {
×
86
        s.toFile = &file
×
87
        return s
×
88
}
×
89

90
// Streamer takes an io.Writer that receives the XML output.
91
// So the stdout of nmap will be duplicated to the given stream and *Run.
92
// This will not disable parsing the output to the struct.
93
func (s *Scanner) Streamer(stream io.Writer) *Scanner {
1✔
94
        s.streamer = stream
1✔
95
        return s
1✔
96
}
1✔
97

98
// Run will run the Scanner with the enabled options.
99
// You need to create a Run struct and warnings array first so the function can parse it.
100
func (s *Scanner) Run() (result *Run, warnings *[]string, err error) {
1✔
101
        var stdoutPipe io.ReadCloser
1✔
102
        var stdout bytes.Buffer
1✔
103
        var stderr bytes.Buffer
1✔
104

1✔
105
        warnings = &[]string{} // Instantiate warnings array
1✔
106

1✔
107
        args := s.args
1✔
108

1✔
109
        // Write XML to standard output.
1✔
110
        // If toFile is set then write XML to file.
1✔
111
        if s.toFile != nil {
1✔
112
                args = append(args, "-oX", *s.toFile)
×
113
        } else {
1✔
114
                args = append(args, "-oX", "-")
1✔
115
        }
1✔
116

117
        // Prepare nmap process.
118
        cmd := exec.CommandContext(s.ctx, s.binaryPath, args...)
1✔
119
        if s.modifySysProcAttr != nil {
1✔
120
                s.modifySysProcAttr(cmd.SysProcAttr)
×
121
        }
×
122
        stdoutPipe, err = cmd.StdoutPipe()
1✔
123
        if err != nil {
1✔
124
                return result, warnings, err
×
125
        }
×
126
        stdoutDuplicate := io.TeeReader(stdoutPipe, &stdout)
1✔
127
        cmd.Stderr = &stderr
1✔
128

1✔
129
        // According to cmd.StdoutPipe() doc, we must not "call Wait before all reads from the pipe have completed"
1✔
130
        // We use this WaitGroup to wait for all IO operations to finish before calling wait
1✔
131
        var wg sync.WaitGroup
1✔
132

1✔
133
        var streamerErrs *errgroup.Group
1✔
134
        if s.streamer != nil {
2✔
135
                streamerErrs, _ = errgroup.WithContext(s.ctx)
1✔
136
                wg.Add(1)
1✔
137
                streamerErrs.Go(func() error {
2✔
138
                        defer wg.Done()
1✔
139
                        _, err = io.Copy(s.streamer, stdoutDuplicate)
1✔
140
                        return err
1✔
141
                })
1✔
142
        } else {
1✔
143
                wg.Add(1)
1✔
144
                go func() {
2✔
145
                        defer wg.Done()
1✔
146
                        io.Copy(io.Discard, stdoutDuplicate)
1✔
147
                }()
1✔
148
        }
149

150
        // Run nmap process.
151
        err = cmd.Start()
1✔
152
        if err != nil {
2✔
153
                return result, warnings, err
1✔
154
        }
1✔
155

156
        // Add goroutine that updates chan when command is finished.
157
        done := make(chan error, 1)
1✔
158
        doneProgress := make(chan bool, 1)
1✔
159

1✔
160
        go func() {
2✔
161
                wg.Wait()
1✔
162
                err := cmd.Wait()
1✔
163
                if streamerErrs != nil {
2✔
164
                        streamerError := streamerErrs.Wait()
1✔
165
                        if streamerError != nil {
1✔
166
                                *warnings = append(*warnings, fmt.Sprintf("read from stdout failed: %s", err))
×
167
                        }
×
168
                }
169
                done <- err
1✔
170
        }()
171

172
        // Make goroutine to check the progress every second.
173
        // Listening for channel doneProgress.
174
        if s.liveProgress != nil {
2✔
175
                go func() {
2✔
176
                        type progress struct {
1✔
177
                                TaskProgress []TaskProgress `xml:"taskprogress" json:"task_progress"`
1✔
178
                        }
1✔
179
                        for {
2✔
180
                                select {
1✔
181
                                case <-doneProgress:
1✔
182
                                        close(s.liveProgress)
1✔
183
                                        return
1✔
184
                                default:
1✔
185
                                        time.Sleep(time.Millisecond * 100)
1✔
186
                                        var p progress
1✔
187
                                        _ = xml.Unmarshal(stdout.Bytes(), &p)
1✔
188
                                        progressIndex := len(p.TaskProgress) - 1
1✔
189
                                        if progressIndex >= 0 {
2✔
190
                                                s.liveProgress <- p.TaskProgress[progressIndex].Percent
1✔
191
                                        }
1✔
192
                                }
193
                        }
194
                }()
195
        }
196

197
        // Check if function should run async.
198
        // When async process nmap result in goroutine that waits for nmap command finish.
199
        // Else block and process nmap result in this function scope.
200
        result = &Run{}
1✔
201
        if s.doneAsync != nil {
2✔
202
                go func() {
2✔
203
                        s.doneAsync <- s.processNmapResult(result, warnings, &stdout, &stderr, done, doneProgress)
1✔
204
                }()
1✔
205
        } else {
1✔
206
                err = s.processNmapResult(result, warnings, &stdout, &stderr, done, doneProgress)
1✔
207
        }
1✔
208

209
        return result, warnings, err
1✔
210
}
211

212
// AddOptions sets more scan options after the scan is created.
213
func (s *Scanner) AddOptions(options ...Option) *Scanner {
×
214
        for _, option := range options {
×
215
                option(s)
×
216
        }
×
217
        return s
×
218
}
219

220
// Args return the list of nmap args.
221
func (s *Scanner) Args() []string {
1✔
222
        return s.args
1✔
223
}
1✔
224

225
func chooseHosts(result *Run, filter func(Host) bool) {
1✔
226
        var filteredHosts []Host
1✔
227

1✔
228
        for _, host := range result.Hosts {
2✔
229
                if filter(host) {
2✔
230
                        filteredHosts = append(filteredHosts, host)
1✔
231
                }
1✔
232
        }
233

234
        result.Hosts = filteredHosts
1✔
235
}
236

237
func choosePorts(result *Run, filter func(Port) bool) {
1✔
238
        for idx := range result.Hosts {
2✔
239
                var filteredPorts []Port
1✔
240

1✔
241
                for _, port := range result.Hosts[idx].Ports {
2✔
242
                        if filter(port) {
2✔
243
                                filteredPorts = append(filteredPorts, port)
1✔
244
                        }
1✔
245
                }
246

247
                result.Hosts[idx].Ports = filteredPorts
1✔
248
        }
249
}
250

251
func (s *Scanner) processNmapResult(result *Run, warnings *[]string, stdout, stderr *bytes.Buffer, done chan error, doneProgress chan bool) error {
1✔
252
        // Wait for nmap to finish.
1✔
253
        var err = <-done
1✔
254
        close(doneProgress)
1✔
255
        if err != nil {
2✔
256
                return err
1✔
257
        }
1✔
258

259
        // Check stderr output.
260
        if err := checkStdErr(stderr, warnings); err != nil {
1✔
261
                return err
×
262
        }
×
263

264
        // Parse nmap xml output. Usually nmap always returns valid XML, even if there is a scan error.
265
        // Potentially available warnings are returned too, but probably not the reason for a broken XML.
266
        if s.toFile != nil {
1✔
267
                err = result.FromFile(*s.toFile)
×
268
        } else {
1✔
269
                err = Parse(stdout.Bytes(), result)
1✔
270
        }
1✔
271
        if err != nil {
2✔
272
                *warnings = append(*warnings, err.Error()) // Append parsing error to warnings for those who are interested.
1✔
273
                return ErrParseOutput
1✔
274
        }
1✔
275

276
        // Critical scan errors are reflected in the XML.
277
        if result != nil && len(result.Stats.Finished.ErrorMsg) > 0 {
2✔
278
                switch {
1✔
279
                case strings.Contains(result.Stats.Finished.ErrorMsg, "Error resolving name"):
1✔
280
                        return ErrResolveName
1✔
281
                default:
1✔
282
                        return fmt.Errorf(result.Stats.Finished.ErrorMsg)
1✔
283
                }
284
        }
285

286
        // Call filters if they are set.
287
        if s.portFilter != nil {
2✔
288
                choosePorts(result, s.portFilter)
1✔
289
        }
1✔
290
        if s.hostFilter != nil {
2✔
291
                chooseHosts(result, s.hostFilter)
1✔
292
        }
1✔
293

294
        return err
1✔
295
}
296

297
// checkStdErr writes the output of stderr to the warnings array.
298
// It also processes nmap stderr output containing none-critical errors and warnings.
299
func checkStdErr(stderr *bytes.Buffer, warnings *[]string) error {
1✔
300
        if stderr.Len() <= 0 {
2✔
301
                return nil
1✔
302
        }
1✔
303

304
        stderrSplit := strings.Split(strings.Trim(stderr.String(), "\n "), "\n")
1✔
305

1✔
306
        // Check for warnings that will inevitably lead to parsing errors, hence, have priority.
1✔
307
        for _, warning := range stderrSplit {
2✔
308
                warning = strings.Trim(warning, " ")
1✔
309
                *warnings = append(*warnings, warning)
1✔
310
                switch {
1✔
311
                case strings.Contains(warning, "Malloc Failed!"):
1✔
312
                        return ErrMallocFailed
1✔
313
                }
314
        }
315
        return nil
1✔
316
}
317

318
// WithCustomArguments sets custom arguments to give to the nmap binary.
319
// There should be no reason to use this, unless you are using a custom build
320
// of nmap or that this repository isn't up to date with the latest options
321
// of the official nmap release.
322
//
323
// Deprecated: You can use this as a quick way to paste an nmap command into your go code,
324
// but remember that the whole purpose of this repository is to be idiomatic,
325
// provide type checking, enums for the values that can be passed, etc.
326
func WithCustomArguments(args ...string) Option {
1✔
327
        return func(s *Scanner) {
2✔
328
                s.args = append(s.args, args...)
1✔
329
        }
1✔
330
}
331

332
// WithBinaryPath sets the nmap binary path for a scanner.
333
func WithBinaryPath(binaryPath string) Option {
1✔
334
        return func(s *Scanner) {
2✔
335
                s.binaryPath = binaryPath
1✔
336
        }
1✔
337
}
338

339
// WithFilterPort allows to set a custom function to filter out ports that
340
// don't fulfill a given condition. When the given function returns true,
341
// the port is kept, otherwise it is removed from the result. Can be used
342
// along with WithFilterHost.
343
func WithFilterPort(portFilter func(Port) bool) Option {
1✔
344
        return func(s *Scanner) {
2✔
345
                s.portFilter = portFilter
1✔
346
        }
1✔
347
}
348

349
// WithFilterHost allows to set a custom function to filter out hosts that
350
// don't fulfill a given condition. When the given function returns true,
351
// the host is kept, otherwise it is removed from the result. Can be used
352
// along with WithFilterPort.
353
func WithFilterHost(hostFilter func(Host) bool) Option {
1✔
354
        return func(s *Scanner) {
2✔
355
                s.hostFilter = hostFilter
1✔
356
        }
1✔
357
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc