Skip to content

Improve port parsing to support combined specifications #23

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

Merged
merged 1 commit into from
Jul 21, 2025
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
70 changes: 43 additions & 27 deletions scan/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package scan

import (
"fmt"
"slices"
"strconv"
"strings"
"time"
Expand All @@ -28,17 +29,17 @@ func getDuration(period string) (time.Duration, error) {
return t, nil
}

// readPortsRange transforms a range of ports given in conf to an array of
// effective ports
// readPortsRange transforms a comma-separated string of ports into a unique,
// sorted slice of integers.
func readPortsRange(ranges string) ([]int, error) {
ports := []int{}

// Remove spaces
ranges = strings.Replace(ranges, " ", "", -1)
ranges = strings.ReplaceAll(ranges, " ", "")

parts := strings.Split(ranges, ",")
parts := strings.SplitSeq(ranges, ",")

for _, spec := range parts {
for spec := range parts {
if spec == "" {
continue
}
Expand All @@ -54,34 +55,49 @@ func readPortsRange(ranges string) ([]int, error) {
case "top1000":
ports = append(ports, top1000Ports...)
default:
var decomposedRange []string
if strings.Contains(spec, "-") {
decomposedRange := strings.Split(spec, "-")
if len(decomposedRange) != 2 || decomposedRange[0] == "" || decomposedRange[1] == "" {
return nil, fmt.Errorf("invalid port range format: %q", spec)
}

if !strings.Contains(spec, "-") {
decomposedRange = []string{spec, spec}
min, err := strconv.Atoi(decomposedRange[0])
if err != nil {
return nil, fmt.Errorf("invalid start port in range %q: %w", spec, err)
}
max, err := strconv.Atoi(decomposedRange[1])
if err != nil {
return nil, fmt.Errorf("invalid end port in range %q: %w", spec, err)
}

if min > max {
return nil, fmt.Errorf("start port %d is higher than end port %d in range %q", min, max, spec)
}

if min < 1 || max > 65535 {
return nil, fmt.Errorf("port range %q is out of the valid range (1-65535)", spec)
}

for i := min; i <= max; i++ {
ports = append(ports, i)
}
} else {
decomposedRange = strings.Split(spec, "-")
}
port, err := strconv.Atoi(spec)
if err != nil {
return nil, fmt.Errorf("invalid port specification %q: %w", spec, err)
}

min, err := strconv.Atoi(decomposedRange[0])
if err != nil {
return nil, err
}
max, err := strconv.Atoi(decomposedRange[len(decomposedRange)-1])
if err != nil {
return nil, err
}
if port < 1 || port > 65535 {
return nil, fmt.Errorf("port %d is out of the valid range (1-65535)", port)
}

if min > max {
return nil, fmt.Errorf("lower port %d is higher than high port %d", min, max)
}
if max > 65535 {
return nil, fmt.Errorf("port %d is higher than max port", max)
}
for i := min; i <= max; i++ {
ports = append(ports, i)
ports = append(ports, port)
}
}
}

return ports, nil
slices.Sort(ports)
uniquePorts := slices.Compact(ports)

return uniquePorts, nil
}
34 changes: 33 additions & 1 deletion scan/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ func Test_getDuration(t *testing.T) {
}

func Test_readPortsRange(t *testing.T) {
reservedPorts := make([]int, 1023)
for i := range 1023 {
reservedPorts[i] = i + 1
}

allPorts := make([]int, 65535)
for i := range 65535 {
allPorts[i] = i + 1
}

tests := []struct {
name string
ranges string
Expand All @@ -45,7 +55,29 @@ func Test_readPortsRange(t *testing.T) {
{name: "hyphen", ranges: "22-25", want: []int{22, 23, 24, 25}, wantErr: false},
{name: "comma and hyphen", ranges: "22,30-32", want: []int{22, 30, 31, 32}, wantErr: false},
{name: "comma, hyphen, comma", ranges: "22,30-32,50", want: []int{22, 30, 31, 32, 50}, wantErr: false},
{name: "unknown", ranges: "foobar", wantErr: true},

// Tests for keywords
{name: "keyword all", ranges: "all", want: allPorts, wantErr: false},
{name: "keyword reserved", ranges: "reserved", want: reservedPorts, wantErr: false},
{name: "keyword top1000", ranges: "top1000", want: top1000Ports, wantErr: false},

// Tests for combinations and uniqueness
{name: "duplicates", ranges: "80,81,443,79-88", want: []int{79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 443}, wantErr: false},
{name: "reserved with duplicates", ranges: "1,2,reserved", want: reservedPorts, wantErr: false},
{name: "all with others", ranges: "80,all,9000", want: allPorts, wantErr: false},

// Tests for edge cases
{name: "empty string", ranges: "", want: []int{}, wantErr: false},
{name: "whitespace and commas", ranges: " , ", want: []int{}, wantErr: false},

// Tests for error conditions
{name: "unknown keyword", ranges: "foobar", wantErr: true},
{name: "invalid range min > max", ranges: "100-20", wantErr: true},
{name: "port > 65535", ranges: "65536", wantErr: true},
{name: "port < 1", ranges: "0", wantErr: true},
{name: "range end > 65535", ranges: "65530-65536", wantErr: true},
{name: "malformed range end", ranges: "100-", wantErr: true},
{name: "malformed range start", ranges: "-100", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down