Skip to content
Draft
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
49 changes: 28 additions & 21 deletions cmd/naabu/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,33 +90,40 @@ func main() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
sig := <-c
gologger.Info().Msgf("Received signal: %s, exiting gracefully...\n", sig)
shutdownStarted := false
for sig := range c {
if shutdownStarted {
gologger.Warning().Msg("Received additional interrupt, forcing exit")
os.Exit(1)
}

// Cancel context to stop ongoing tasks
cancel()
shutdownStarted = true
gologger.Info().Msgf("Received signal: %s, exiting gracefully...\n", sig)

// Try to save resume config if needed
if options.ResumeCfg != nil && options.ResumeCfg.ShouldSaveResume() {
gologger.Info().Msgf("Creating resume file: %s\n", runner.DefaultResumeFilePath())
if err := options.ResumeCfg.SaveResumeConfig(); err != nil {
gologger.Error().Msgf("Couldn't create resume file: %s\n", err)
}
}
go func() {
// Cancel context to stop ongoing tasks
cancel()

// Show scan result if runner is available
if naabuRunner != nil {
naabuRunner.ShowScanResultOnExit()
// Try to save resume config if needed
if options.ResumeCfg != nil && options.ResumeCfg.ShouldSaveResume() {
gologger.Info().Msgf("Creating resume file: %s\n", runner.DefaultResumeFilePath())
if err := options.ResumeCfg.SaveResumeConfig(); err != nil {
gologger.Error().Msgf("Couldn't create resume file: %s\n", err)
}
}

if err := naabuRunner.Close(); err != nil {
gologger.Error().Msgf("Couldn't close runner: %s\n", err)
}
}
// Show scan result if runner is available
if naabuRunner != nil {
naabuRunner.ShowScanResultOnExit()

// Final flush if gologger has a Close method (placeholder if exists)
// Example: gologger.Close()
if err := naabuRunner.Close(); err != nil {
gologger.Error().Msgf("Couldn't close runner: %s\n", err)
}
}

os.Exit(1)
os.Exit(1)
}()
Comment on lines +103 to +125
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Graceful-shutdown work is detached, which can race with process exit.

Starting a second goroutine for cleanup makes resume-save/result-flush/close ordering non-deterministic under interruption. Keep shutdown steps in the signal-handler goroutine to make completion deterministic.

🔧 Suggested fix
-			go func() {
-				// Cancel context to stop ongoing tasks
-				cancel()
-
-				// Try to save resume config if needed
-				if options.ResumeCfg != nil && options.ResumeCfg.ShouldSaveResume() {
-					gologger.Info().Msgf("Creating resume file: %s\n", runner.DefaultResumeFilePath())
-					if err := options.ResumeCfg.SaveResumeConfig(); err != nil {
-						gologger.Error().Msgf("Couldn't create resume file: %s\n", err)
-					}
-				}
-
-				// Show scan result if runner is available
-				if naabuRunner != nil {
-					naabuRunner.ShowScanResultOnExit()
-
-					if err := naabuRunner.Close(); err != nil {
-						gologger.Error().Msgf("Couldn't close runner: %s\n", err)
-					}
-				}
-
-				os.Exit(1)
-			}()
+			// Cancel context to stop ongoing tasks
+			cancel()
+
+			// Try to save resume config if needed
+			if options.ResumeCfg != nil && options.ResumeCfg.ShouldSaveResume() {
+				gologger.Info().Msgf("Creating resume file: %s\n", runner.DefaultResumeFilePath())
+				if err := options.ResumeCfg.SaveResumeConfig(); err != nil {
+					gologger.Error().Msgf("Couldn't create resume file: %s\n", err)
+				}
+			}
+
+			// Show scan result if runner is available
+			if naabuRunner != nil {
+				naabuRunner.ShowScanResultOnExit()
+
+				if err := naabuRunner.Close(); err != nil {
+					gologger.Error().Msgf("Couldn't close runner: %s\n", err)
+				}
+			}
+
+			os.Exit(1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/naabu/main.go` around lines 103 - 125, The detached goroutine performing
shutdown (calling cancel(), saving resume via
options.ResumeCfg.SaveResumeConfig(), logging runner.DefaultResumeFilePath(),
invoking naabuRunner.ShowScanResultOnExit() and naabuRunner.Close(), then
os.Exit(1)) races with process exit; move these steps out of the anonymous go
func and execute them directly in the signal-handler goroutine so shutdown runs
synchronously and deterministically: call cancel(), then if options.ResumeCfg !=
nil && options.ResumeCfg.ShouldSaveResume() save and log the resume file, then
if naabuRunner != nil call ShowScanResultOnExit() and Close() (logging any
errors), and finally call os.Exit(1).

}
}()

// Start enumeration
Expand Down
16 changes: 11 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/miekg/dns v1.1.62
github.com/pkg/errors v0.9.1
github.com/praetorian-inc/nerva v1.0.0
github.com/projectdiscovery/blackrock v0.0.1
github.com/projectdiscovery/cdncheck v1.2.19
github.com/projectdiscovery/clistats v0.1.1
Expand Down Expand Up @@ -67,9 +68,10 @@ require (
github.com/google/go-github/v30 v30.1.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/ishidawataru/sctp v0.0.0-20251114114122-19ddcbc6aae2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
Expand Down Expand Up @@ -97,6 +99,7 @@ require (
github.com/projectdiscovery/hmap v0.0.99 // indirect
github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582 // indirect
github.com/projectdiscovery/retryabledns v1.0.112 // indirect
github.com/projectdiscovery/wappalyzergo v0.2.17 // indirect
github.com/refraction-networking/utls v1.7.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
Expand Down Expand Up @@ -127,12 +130,15 @@ require (
go.etcd.io/bbolt v1.3.7 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.39.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
97 changes: 87 additions & 10 deletions go.sum

Large diffs are not rendered by default.

256 changes: 256 additions & 0 deletions pkg/runner/nerva.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package runner

import (
"bytes"
"encoding/json"
"fmt"
"net/netip"
"strings"

"github.com/praetorian-inc/nerva/pkg/plugins"
"github.com/praetorian-inc/nerva/pkg/scan"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/naabu/v2/pkg/port"
"github.com/projectdiscovery/naabu/v2/pkg/protocol"
"github.com/projectdiscovery/naabu/v2/pkg/result"
)

func (r *Runner) handleServiceFingerprinting() error {
if !r.options.ServiceDiscovery && !r.options.ServiceVersion {
return nil
}

var tcpTargets, udpTargets []plugins.Target
for hostResult := range r.scanner.ScanResults.GetIPsPorts() {
for _, p := range hostResult.Ports {
if p.Port <= 0 || p.Port > 65535 {
continue
}

target := plugins.Target{
Host: hostResult.IP,
Address: joinAddrPort(hostResult.IP, p.Port),
}
if !target.Address.IsValid() {
continue
}

switch p.Protocol {
case protocol.UDP:
udpTargets = append(udpTargets, target)
default:
tcpTargets = append(tcpTargets, target)
}
}
}

if len(tcpTargets) == 0 && len(udpTargets) == 0 {
// gologger.Info().Msg("No hosts with open ports found for service fingerprinting")
return nil
}

baseCfg := scan.Config{
DefaultTimeout: r.options.Timeout,
Verbose: r.options.Verbose || r.options.Debug,
}

run := func(targets []plugins.Target, udp bool) {
if len(targets) == 0 {
return
}

cfg := baseCfg
cfg.UDP = udp

results, err := scan.ScanTargets(targets, cfg)
if err != nil {
transport := "tcp"
if udp {
transport = "udp"
}
gologger.Warning().Msgf("Could not fingerprint %s services: %s", transport, err)
return
}

r.integrateNervaResults(results)
}

run(tcpTargets, false)
run(udpTargets, true)

return nil
}

func joinAddrPort(ip string, portNum int) netip.AddrPort {
addr, err := netip.ParseAddr(ip)
if err != nil {
return netip.AddrPort{}
}

if portNum <= 0 || portNum > 65535 {
return netip.AddrPort{}
}

return netip.AddrPortFrom(addr, uint16(portNum))
}

func (r *Runner) integrateNervaResults(services []plugins.Service) {
for _, service := range services {
enhancedPort, ip, ok := r.convertNervaServiceToPort(service)
if !ok {
continue
}

r.updatePortWithServiceInfo(ip, enhancedPort)
}
}

func (r *Runner) convertNervaServiceToPort(service plugins.Service) (*port.Port, string, bool) {
if service.Port <= 0 {
return nil, "", false
}
Comment on lines +109 to +111
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing upper-bound port validation in Nerva service conversion.

Ports above 65535 are currently accepted and can leak invalid data into results.

🔧 Suggested fix
-	if service.Port <= 0 {
+	if service.Port <= 0 || service.Port > 65535 {
 		return nil, "", false
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if service.Port <= 0 {
return nil, "", false
}
if service.Port <= 0 || service.Port > 65535 {
return nil, "", false
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/runner/nerva.go` around lines 109 - 111, The current port validation only
checks for non-positive values (if service.Port <= 0) and allows ports > 65535;
update the validation to reject ports outside the valid 1–65535 range (e.g., if
service.Port <= 0 || service.Port > 65535) so the function that converts the
service (the block using service.Port and returning nil, "", false) returns the
same error path for out-of-range ports and prevents invalid data from entering
results.


proto := protocol.TCP
if strings.EqualFold(service.Transport, "udp") {
proto = protocol.UDP
}

enhancedPort := &port.Port{
Port: service.Port,
Protocol: proto,
TLS: service.TLS,
Service: &port.Service{
Name: service.Protocol,
},
}

if r.options.ServiceVersion {
enhancedPort.Service.Version = service.Version
enhancedPort.Service.Product = service.Protocol
enhancedPort.Service.ExtraInfo = normalizeServiceRawMetadata(service.Raw)
}

ip := service.IP
if ip == "" {
ip = service.Host
}
if ip == "" {
return nil, "", false
}

return enhancedPort, ip, true
}

func normalizeServiceRawMetadata(raw json.RawMessage) string {
trimmed := strings.TrimSpace(string(raw))
if trimmed == "" {
return ""
}

if !json.Valid(raw) {
return trimmed
}

var compact bytes.Buffer
if err := json.Compact(&compact, raw); err != nil {
return trimmed
}

return compact.String()
}

func (r *Runner) enrichHostResultPorts(hostResult *result.HostResult) []*port.Port {
if hostResult == nil || len(hostResult.Ports) == 0 {
return hostResult.Ports
}
Comment on lines +163 to +165
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Nil guard still dereferences hostResult on the return path.

When hostResult == nil, returning hostResult.Ports will panic.

🔧 Suggested fix
 func (r *Runner) enrichHostResultPorts(hostResult *result.HostResult) []*port.Port {
-	if hostResult == nil || len(hostResult.Ports) == 0 {
+	if hostResult == nil {
+		return nil
+	}
+	if len(hostResult.Ports) == 0 {
 		return hostResult.Ports
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/runner/nerva.go` around lines 163 - 165, The nil-check for hostResult is
wrong because it dereferences hostResult when returning hostResult.Ports; update
the guard in the function containing hostResult so that if hostResult == nil you
return nil (or an empty slice) immediately, and only access hostResult.Ports
after confirming hostResult != nil; i.e., change the conditional branch to
return a safe zero value instead of hostResult.Ports and then proceed to use
hostResult.Ports when non-nil.

if !r.options.ServiceDiscovery && !r.options.ServiceVersion {
return hostResult.Ports
}

baseCfg := scan.Config{
DefaultTimeout: r.options.Timeout,
Verbose: r.options.Verbose || r.options.Debug,
}

var tcpTargets, udpTargets []plugins.Target
for _, p := range hostResult.Ports {
if p.Port <= 0 || p.Port > 65535 {
continue
}

target := plugins.Target{
Host: hostResult.IP,
Address: joinAddrPort(hostResult.IP, p.Port),
}
if !target.Address.IsValid() {
continue
}

switch p.Protocol {
case protocol.UDP:
udpTargets = append(udpTargets, target)
default:
tcpTargets = append(tcpTargets, target)
}
}

resultsByPort := make(map[string]*port.Port)
key := func(proto protocol.Protocol, portNum int) string {
return fmt.Sprintf("%s:%d", proto.String(), portNum)
}
run := func(targets []plugins.Target, udp bool) {
if len(targets) == 0 {
return
}

cfg := baseCfg
cfg.UDP = udp

services, err := scan.ScanTargets(targets, cfg)
if err != nil {
transport := "tcp"
if udp {
transport = "udp"
}
gologger.Debug().Msgf("Could not fingerprint %s services for %s: %s", transport, hostResult.IP, err)
return
}

for _, service := range services {
enhancedPort, ip, ok := r.convertNervaServiceToPort(service)
if !ok {
continue
}
if ip != hostResult.IP {
continue
}

resultsByPort[key(enhancedPort.Protocol, enhancedPort.Port)] = enhancedPort
r.updatePortWithServiceInfo(ip, enhancedPort)
}
}

run(tcpTargets, false)
run(udpTargets, true)

for _, p := range hostResult.Ports {
if enhanced, ok := resultsByPort[key(p.Protocol, p.Port)]; ok {
p.TLS = enhanced.TLS

Check failure on line 238 in pkg/runner/nerva.go

View workflow job for this annotation

GitHub Actions / Lint

SA1019: p.TLS is deprecated: TLS field will be removed in a future version (staticcheck)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

CI blocker: deprecated TLS field assignment is failing staticcheck (SA1019).

This line is currently breaking the pipeline.

🔧 Suggested fix (temporary compatibility path)
 		if enhanced, ok := resultsByPort[key(p.Protocol, p.Port)]; ok {
-			p.TLS = enhanced.TLS
+			//nolint:staticcheck // keep deprecated field populated for backward compatibility.
+			p.TLS = enhanced.TLS
 			p.Service = enhanced.Service
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
p.TLS = enhanced.TLS
if enhanced, ok := resultsByPort[key(p.Protocol, p.Port)]; ok {
//nolint:staticcheck // keep deprecated field populated for backward compatibility.
p.TLS = enhanced.TLS
p.Service = enhanced.Service
}
🧰 Tools
🪛 GitHub Actions: 🔨 Build Test

[error] 238-238: SA1019: p.TLS is deprecated: TLS field will be removed in a future version (staticcheck)

🪛 GitHub Check: Lint

[failure] 238-238:
SA1019: p.TLS is deprecated: TLS field will be removed in a future version (staticcheck)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/runner/nerva.go` at line 238, The assignment to the deprecated field
p.TLS should be replaced with the new compatibility path: stop writing to p.TLS
and instead call the new setter or assign the new field (e.g., use
p.SetTLS(enhanced.TLS) or p.TLSConfig = enhanced.TLS) depending on the p type;
if neither exists, add a small compatibility method on p’s type named
SetTLS(tls) that stores the value into the new field (TLSConfig) and use that
here to avoid the deprecated SA1019 usage while preserving behavior.

p.Service = enhanced.Service
}
}

return hostResult.Ports
}

func (r *Runner) handleFingerprinting() error {
if err := r.handleServiceFingerprinting(); err != nil {
return err
}

if err := r.handleNmap(); err != nil {
return err
}

return nil
}
Loading
Loading