From 8f8abe9b571a6c94e9df8532a8b481f1e49241e6 Mon Sep 17 00:00:00 2001 From: Charles Wong Date: Tue, 10 Mar 2026 09:37:23 -0700 Subject: [PATCH] fix: split comma-separated targets from -l file and stdin input (#859) The -u flag splits comma-separated targets via goflags, but -l (file) and stdin inputs were read line-by-line without splitting. This caused lines like '192.168.1.0/24,192.168.2.0/24' to be treated as a single target, resulting in connection errors. Changes: - Add splitInputEntries() helper that splits on commas, trims whitespace, and filters empty entries - Apply splitting to file input (-l), stdin, and -u inputs (safety net) - Increase scanner buffer to 4MB for long comma-separated lines - Add scanner.Err() checks for file and stdin reads - Add comprehensive unit tests for splitInputEntries (8 cases) Fixes #859 --- internal/runner/runner.go | 43 +++++++++++++++++++++----- internal/runner/runner_test.go | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 91e80136..4d5097ab 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -418,11 +418,18 @@ func (r *Runner) processInputElementWorker(inputs chan taskInput, wg *sync.WaitG } } +// maxInputScanTokenSize is the max token size for the input scanner to handle +// long comma-separated lines without truncation. +const maxInputScanTokenSize = 4 * 1024 * 1024 + // normalizeAndQueueInputs normalizes the inputs and queues them for execution func (r *Runner) normalizeAndQueueInputs(inputs chan taskInput) error { - // Process Normal Inputs + // Process Normal Inputs (-u flag already splits on commas via goflags, + // but we split again for safety in case of programmatic use) for _, text := range r.options.Inputs { - r.processInputItem(text, inputs) + for _, entry := range splitInputEntries(text) { + r.processInputItem(entry, inputs) + } } if r.options.InputList != "" { @@ -437,25 +444,45 @@ func (r *Runner) normalizeAndQueueInputs(inputs chan taskInput) error { }() scanner := bufio.NewScanner(file) + scanner.Buffer(make([]byte, 0, 64*1024), maxInputScanTokenSize) for scanner.Scan() { - text := scanner.Text() - if text != "" { - r.processInputItem(text, inputs) + for _, entry := range splitInputEntries(scanner.Text()) { + r.processInputItem(entry, inputs) } } + if err := scanner.Err(); err != nil { + return errkit.Wrap(err, "could not read input file") + } } if r.hasStdin { scanner := bufio.NewScanner(os.Stdin) + scanner.Buffer(make([]byte, 0, 64*1024), maxInputScanTokenSize) for scanner.Scan() { - text := scanner.Text() - if text != "" { - r.processInputItem(text, inputs) + for _, entry := range splitInputEntries(scanner.Text()) { + r.processInputItem(entry, inputs) } } + if err := scanner.Err(); err != nil { + return errkit.Wrap(err, "could not read stdin") + } } return nil } +// splitInputEntries splits a text line by commas and returns non-empty, +// trimmed entries. This ensures file (-l) and stdin inputs behave the +// same as -u which uses goflags.CommaSeparatedStringSliceOptions. +func splitInputEntries(text string) []string { + var entries []string + for _, entry := range strings.Split(text, ",") { + entry = strings.TrimSpace(entry) + if entry != "" { + entries = append(entries, entry) + } + } + return entries +} + // resolveFQDN resolves a FQDN and returns the IP addresses func (r *Runner) resolveFQDN(target string) ([]string, error) { // If the host is a Domain, then perform resolution and discover all IP diff --git a/internal/runner/runner_test.go b/internal/runner/runner_test.go index b4ed856e..00339d97 100644 --- a/internal/runner/runner_test.go +++ b/internal/runner/runner_test.go @@ -429,3 +429,59 @@ func Test_CTLogsModeOutputOptions(t *testing.T) { }) } } + +func Test_splitInputEntries(t *testing.T) { + tests := []struct { + name string + input string + expected []string + }{ + { + name: "single entry", + input: "192.168.1.0/24", + expected: []string{"192.168.1.0/24"}, + }, + { + name: "comma-separated entries", + input: "192.168.1.0/24,192.168.2.0/24,192.168.3.0/24", + expected: []string{"192.168.1.0/24", "192.168.2.0/24", "192.168.3.0/24"}, + }, + { + name: "comma-separated with spaces", + input: "192.168.1.0/24 , 192.168.2.0/24 , 192.168.3.0/24", + expected: []string{"192.168.1.0/24", "192.168.2.0/24", "192.168.3.0/24"}, + }, + { + name: "empty entries filtered", + input: "192.168.1.0/24,,192.168.2.0/24,", + expected: []string{"192.168.1.0/24", "192.168.2.0/24"}, + }, + { + name: "empty string", + input: "", + expected: nil, + }, + { + name: "only commas and spaces", + input: " , , , ", + expected: nil, + }, + { + name: "single host with port", + input: "example.com:443", + expected: []string{"example.com:443"}, + }, + { + name: "mixed hosts and CIDRs", + input: "example.com:443,10.0.0.0/8,scanme.sh", + expected: []string{"example.com:443", "10.0.0.0/8", "scanme.sh"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := splitInputEntries(tc.input) + assert.Equal(t, tc.expected, result) + }) + } +}