Conversation
Neo - PR Security ReviewNo security issues found Comment |
WalkthroughAdds a predictive "smart scan" mode: a thread-safe port correlation model, priority-queue–driven scan ordering, fast raw TCP SYN sender and IPv4 indexing, CLI flags to enable/configure predictions, and supporting result/query utilities and validation. Changes
Sequence DiagramsequenceDiagram
participant User as User
participant Runner as Runner
participant Model as Prediction Model
participant Queue as Port Queue
participant Scanner as Scanner
participant Sender as SYN Sender
participant Network as Network
User->>Runner: Start enumeration (SmartScan=true)
Runner->>Model: Load DefaultModel()
Model-->>Runner: Correlations ready
Runner->>Queue: Build initial heap from ports
Queue-->>Runner: Heap initialized
loop per retry
Runner->>Scanner: collectOpenPorts()
Scanner-->>Runner: []openPorts
Runner->>Queue: rebuild/prioritize using correlations
Queue-->>Runner: prioritized ports
loop while queue not empty
Runner->>Queue: pop() next port
Queue-->>Runner: port
Runner->>Sender: send probes for port (raw or enqueue)
Sender->>Network: emit SYNs
Network-->>Scanner: responses
Scanner-->>Runner: OnReceive(open port)
Runner->>Queue: boostCorrelated(newOpenPort)
Queue-->>Queue: heap.Fix() (priority update)
end
end
Runner-->>User: Scan complete
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (6)
pkg/runner/runner.go (1)
554-681: LGTM!The existing retry-based scanning logic is correctly preserved in the
elsebranch. The reorganization cleanly separates the two scanning strategies while maintaining backward compatibility for the standard scan flow.One observation: progress bar initialization is skipped for smart scan mode. Consider adding progress indication to
runPredictiveScanin a future iteration for user feedback during long scans.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/runner/runner.go` around lines 554 - 681, The predictive scan branch (runPredictiveScan) lacks the progress bar/statistics initialization present in the standard scan loop; update runPredictiveScan to initialize r.stats when r.options.EnableProgressBar is true by computing the same Range from targetsCount*portsCount, calling r.stats.AddStatic for "ports","hosts","retries","startedAt" and r.stats.AddCounter for "packets","errors","total" (using Range*uint64(r.options.Retries)+targetsWithPortCount) and "hosts_with_port", then call r.stats.Start() and handle the returned error similarly to the current loop; reference runPredictiveScan, r.options.EnableProgressBar, r.stats.AddStatic, r.stats.AddCounter, r.stats.Start, targetsCount, portsCount, targetsWithPortCount and Range to locate and mirror the initialization logic.pkg/runner/smartscan.go (2)
412-432: Silent error handling inbuildPopularityRankfor malformed port ranges.
strconv.Atoierrors are silently discarded (lines 419-420, 426). IfNmapTop1000contains malformed data, ports would be assigned rank 0 or the rank map would have incorrect entries.Consider logging a warning for malformed entries during development/debugging:
if strings.Contains(segment, "-") { parts := strings.Split(segment, "-") - start, _ := strconv.Atoi(parts[0]) - end, _ := strconv.Atoi(parts[1]) + start, err1 := strconv.Atoi(parts[0]) + end, err2 := strconv.Atoi(parts[1]) + if err1 != nil || err2 != nil { + continue // skip malformed range + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/runner/smartscan.go` around lines 412 - 432, The buildPopularityRank function silently ignores strconv.Atoi errors when parsing NmapTop1000 segments which can produce incorrect rank entries; update buildPopularityRank to validate Atoi errors for both single ports and range boundaries (using the existing NmapTop1000 string parsing), skip malformed segments instead of inserting zero/invalid entries into the rank map, and emit a concise warning/log (using the package logger or log.Printf) identifying the bad segment and reason so malformed inputs are detectable during debugging while preserving normal behavior for valid ports.
183-200: Consider extracting common skip-check logic to reduce duplication.The skip checks (excluded IPs, skipped hosts, port threshold, CDN, already-has-port) are duplicated between the fast path (lines 183-200) and standard path (lines 217-250). While the paths have slightly different IP types (
[4]byte+ string vs. string only), the logic is nearly identical.Extracting this into a helper that accepts the IP string could reduce maintenance burden.
Also applies to: 217-250
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/runner/smartscan.go` around lines 183 - 200, Extract the repeated skip-check sequence into a single helper (e.g., shouldSkip(ip string, p int) bool) and call it from both the fast and standard paths; move logic using r.excludedIpsNP.ValidateAddress, r.scanner.ScanResults.HasSkipped, r.options.PortThreshold with r.scanner.ScanResults.GetPortCount and r.scanner.IPRanger.GetHostsByIP (and r.scanner.ScanResults.AddSkipped), r.scanner.ScanResults.IPHasPort, and r.canIScanIfCDN into that helper so the checks and gologger.Info message are centralized; in the fast path convert the [4]byte+string IP to a string before calling shouldSkip and preserve existing behavior/short-circuits (return true to indicate skip, false to proceed).pkg/runner/fastsend.go (2)
46-48: Discarding the routing error may hide configuration problems.
scan.PkgRouter.Route()returns an error when no route is found, but it's silently ignored here. If routing fails,srcIPwill be nil and the subsequent check will returnerrNoSourceIP, but the actual root cause (routing error) is lost.Proposed fix
} else if scan.PkgRouter != nil { - _, _, srcIP, _ = scan.PkgRouter.Route(net.IPv4(1, 1, 1, 1)) + var routeErr error + _, _, srcIP, routeErr = scan.PkgRouter.Route(net.IPv4(1, 1, 1, 1)) + if routeErr != nil { + return nil, fmt.Errorf("route lookup failed: %w", routeErr) + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/runner/fastsend.go` around lines 46 - 48, The call to scan.PkgRouter.Route in fastsend.go discards its error, hiding routing failures; update the code around the scan.PkgRouter.Route(net.IPv4(1,1,1,1)) invocation (in the same block that currently assigns _, _, srcIP, _) to capture the returned error, check if err != nil and propagate or wrap that error instead of ignoring it (so the caller sees the routing failure rather than only errNoSourceIP), and only proceed to use srcIP when err == nil; reference scan.PkgRouter.Route and the surrounding fastsend logic to make this change.
199-217: Potential integer overflow when convertingindextouint32.On line 206,
uint32(index)can overflow ifindexexceedsmath.MaxUint32(4,294,967,295). For a/0CIDR (entire IPv4 space), the target count is 2^32, soindexcould reachMaxUint32, which is valid. However, if multiple CIDRs are used andindexaccumulates across entries, it could exceed this.In practice, the subtraction at line 214 keeps
indexwithin the current entry's count, so this should be safe for valid IPv4 CIDRs. Consider adding a defensive check or documenting this constraint.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/runner/fastsend.go` around lines 199 - 217, The pickIPv4 function can overflow when casting index to uint32; before computing val := e.baseIPv4 + uint32(index) add a defensive check that index is within the 32-bit unsigned range and that the sum won't wrap (e.g. ensure index <= math.MaxUint32 and uint64(e.baseIPv4)+uint64(index) <= math.MaxUint32), then perform the addition using a wider type (uint64) and cast safely to uint32; if the check fails, return the empty result (same behavior as when not v4). This touches pickIPv4 and the variables index, e.count and e.baseIPv4.pkg/prediction/model.go (1)
82-84: Non-deterministic ordering for ties inPredictresults.Unlike
Prioritize(which breaks ties by port number at lines 178-183),Predictuses an unstable sort without a tie-breaker. When multiple predictions have the same confidence, their order is non-deterministic.Proposed fix to add tie-breaker
sort.Slice(predictions, func(i, j int) bool { + if predictions[i].Confidence != predictions[j].Confidence { + return predictions[i].Confidence > predictions[j].Confidence + } + return predictions[i].Port < predictions[j].Port - return predictions[i].Confidence > predictions[j].Confidence })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/prediction/model.go` around lines 82 - 84, The sort in Predict uses sort.Slice on the predictions slice without a tie-breaker, causing non-deterministic ordering for equal Confidence; update the comparator in the sort.Slice call inside Predict to first compare Confidence (descending) and when equal compare a stable secondary key (e.g., predictions[i].Port < predictions[j].Port or another stable field on the prediction struct) so ties are deterministically ordered (mirror the Prioritize tie-breaker behavior).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pkg/prediction/model.go`:
- Around line 134-150: Model.Merge can deadlock if two goroutines call Merge on
each other; fix by avoiding holding other.mu while acquiring m.mu. In Merge,
acquire other.mu.RLock(), shallow-copy or deep-copy other.correlations into a
local variable, then release other.mu.RUnlock() before acquiring m.mu.Lock();
finally merge from the local copy into m.correlations. Reference: Model.Merge,
Model.mu, other.mu, and the correlations map when locating where to copy and
merge.
In `@pkg/runner/fastsend.go`:
- Line 8: The build fails due to a missing module import
"github.com/Mzack9999/gopacket/rawsend" in pkg/runner/fastsend.go; run "go get
github.com/Mzack9999/gopacket/rawsend" to add the dependency, then commit the
updated go.mod and go.sum so the import in fastsend.go resolves during CI.
---
Nitpick comments:
In `@pkg/prediction/model.go`:
- Around line 82-84: The sort in Predict uses sort.Slice on the predictions
slice without a tie-breaker, causing non-deterministic ordering for equal
Confidence; update the comparator in the sort.Slice call inside Predict to first
compare Confidence (descending) and when equal compare a stable secondary key
(e.g., predictions[i].Port < predictions[j].Port or another stable field on the
prediction struct) so ties are deterministically ordered (mirror the Prioritize
tie-breaker behavior).
In `@pkg/runner/fastsend.go`:
- Around line 46-48: The call to scan.PkgRouter.Route in fastsend.go discards
its error, hiding routing failures; update the code around the
scan.PkgRouter.Route(net.IPv4(1,1,1,1)) invocation (in the same block that
currently assigns _, _, srcIP, _) to capture the returned error, check if err !=
nil and propagate or wrap that error instead of ignoring it (so the caller sees
the routing failure rather than only errNoSourceIP), and only proceed to use
srcIP when err == nil; reference scan.PkgRouter.Route and the surrounding
fastsend logic to make this change.
- Around line 199-217: The pickIPv4 function can overflow when casting index to
uint32; before computing val := e.baseIPv4 + uint32(index) add a defensive check
that index is within the 32-bit unsigned range and that the sum won't wrap (e.g.
ensure index <= math.MaxUint32 and uint64(e.baseIPv4)+uint64(index) <=
math.MaxUint32), then perform the addition using a wider type (uint64) and cast
safely to uint32; if the check fails, return the empty result (same behavior as
when not v4). This touches pickIPv4 and the variables index, e.count and
e.baseIPv4.
In `@pkg/runner/runner.go`:
- Around line 554-681: The predictive scan branch (runPredictiveScan) lacks the
progress bar/statistics initialization present in the standard scan loop; update
runPredictiveScan to initialize r.stats when r.options.EnableProgressBar is true
by computing the same Range from targetsCount*portsCount, calling
r.stats.AddStatic for "ports","hosts","retries","startedAt" and
r.stats.AddCounter for "packets","errors","total" (using
Range*uint64(r.options.Retries)+targetsWithPortCount) and "hosts_with_port",
then call r.stats.Start() and handle the returned error similarly to the current
loop; reference runPredictiveScan, r.options.EnableProgressBar,
r.stats.AddStatic, r.stats.AddCounter, r.stats.Start, targetsCount, portsCount,
targetsWithPortCount and Range to locate and mirror the initialization logic.
In `@pkg/runner/smartscan.go`:
- Around line 412-432: The buildPopularityRank function silently ignores
strconv.Atoi errors when parsing NmapTop1000 segments which can produce
incorrect rank entries; update buildPopularityRank to validate Atoi errors for
both single ports and range boundaries (using the existing NmapTop1000 string
parsing), skip malformed segments instead of inserting zero/invalid entries into
the rank map, and emit a concise warning/log (using the package logger or
log.Printf) identifying the bad segment and reason so malformed inputs are
detectable during debugging while preserving normal behavior for valid ports.
- Around line 183-200: Extract the repeated skip-check sequence into a single
helper (e.g., shouldSkip(ip string, p int) bool) and call it from both the fast
and standard paths; move logic using r.excludedIpsNP.ValidateAddress,
r.scanner.ScanResults.HasSkipped, r.options.PortThreshold with
r.scanner.ScanResults.GetPortCount and r.scanner.IPRanger.GetHostsByIP (and
r.scanner.ScanResults.AddSkipped), r.scanner.ScanResults.IPHasPort, and
r.canIScanIfCDN into that helper so the checks and gologger.Info message are
centralized; in the fast path convert the [4]byte+string IP to a string before
calling shouldSkip and preserve existing behavior/short-circuits (return true to
indicate skip, false to proceed).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 724dccb5-0d09-4885-9a72-7955c1cd82ac
📒 Files selected for processing (13)
README.mdpkg/prediction/default_model.gopkg/prediction/model.gopkg/prediction/model_test.gopkg/result/results.gopkg/runner/fastsend.gopkg/runner/fastsend_test.gopkg/runner/options.gopkg/runner/runner.gopkg/runner/smartscan.gopkg/runner/validate.gopkg/scan/scan_raw.gopkg/scan/scan_type_test.go
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
pkg/runner/fastsend.go (1)
177-193: Keep the CIDR lookup out of the linear hot path.
pickIPv4walkst.entriesfrom the start on every probe, so a target list with many/32s or small CIDRs turns this back into O(len(targets)) work in the hottest loop. Consider storing cumulative end offsets inbuildTargetIndexand usingsort.Searchso the fast path stays cheap regardless of target shape.Also applies to: 204-220
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@pkg/runner/fastsend.go` around lines 177 - 193, The hot path currently linear-scans targetIndex.entries in pickIPv4; modify buildTargetIndex to compute and store cumulative end offsets (e.g., an []int64 cumulativeEnds or end field on indexEntry) as you append entries and set idx.total, then change pickIPv4 to use sort.Search (binary search) over those cumulative ends to pick the target by offset instead of iterating from the start; update references to targetIndex.entries and any uses of idx.total or indexEntry so the selection uses the new cumulative data for O(log n) lookups.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@pkg/runner/fastsend.go`:
- Around line 44-59: The code currently derives srcIP by calling
scan.PkgRouter.Route(net.IPv4(1,1,1,1)), which bakes a single address into
baseSum and breaks TCP checksums on multihomed/non-default-route hosts; instead,
stop using Route(1.1.1.1) and pick a fixed local IPv4 for checksum calculation
from handler.SourceIp4 if set, then from the bound connection address via
handler.TcpConn4.LocalAddr() (extract the IP and To4()), and only if neither is
available fall back to the standard sender behavior (return errNoSourceIP or use
the existing sender path). Update the srcIP computation and remove the
PkgRouter.Route fallback so baseSum is computed from a stable, connection-bound
source IP.
- Around line 129-154: parseIPv4Fast currently accepts dots even when no digits
were read for the current octet; update parseIPv4Fast to reject empty octets by
tracking whether at least one digit was seen for the current octet (e.g., a
sawDigit boolean set to true on digit) and on encountering '.' ensure sawDigit
is true before assigning ip[dots]=byte(octet), then reset sawDigit and octet for
the next octet; also verify sawDigit is true before assigning the final octet
(ip[3]) and return false if not.
---
Nitpick comments:
In `@pkg/runner/fastsend.go`:
- Around line 177-193: The hot path currently linear-scans targetIndex.entries
in pickIPv4; modify buildTargetIndex to compute and store cumulative end offsets
(e.g., an []int64 cumulativeEnds or end field on indexEntry) as you append
entries and set idx.total, then change pickIPv4 to use sort.Search (binary
search) over those cumulative ends to pick the target by offset instead of
iterating from the start; update references to targetIndex.entries and any uses
of idx.total or indexEntry so the selection uses the new cumulative data for
O(log n) lookups.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 841af42b-8b6c-4ccf-89fa-d9d5bc883558
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (5)
go.modpkg/prediction/model.gopkg/runner/fastsend.gopkg/runner/options.gopkg/runner/smartscan.go
✅ Files skipped from review due to trivial changes (1)
- go.mod
🚧 Files skipped from review as they are similar to previous changes (3)
- pkg/runner/options.go
- pkg/prediction/model.go
- pkg/runner/smartscan.go
| var srcIP net.IP | ||
| if handler.SourceIp4 != nil { | ||
| srcIP = handler.SourceIp4 | ||
| } else if scan.PkgRouter != nil { | ||
| var routeErr error | ||
| _, _, srcIP, routeErr = scan.PkgRouter.Route(net.IPv4(1, 1, 1, 1)) | ||
| if routeErr != nil { | ||
| return nil, fmt.Errorf("route lookup failed: %w", routeErr) | ||
| } | ||
| } | ||
| if srcIP == nil { | ||
| return nil, errNoSourceIP | ||
| } | ||
| src4 := srcIP.To4() | ||
| if src4 == nil { | ||
| return nil, errNoSourceIP |
There was a problem hiding this comment.
Don't derive the checksum source IP from Route(1.1.1.1).
This address is baked into baseSum and reused for every SYN. On multihomed hosts or non-default-route scans, the kernel can choose a different source IP for the real destination, which makes the TCP checksum invalid and silently drops probes. Prefer a fixed local address from handler.SourceIp4 or the bound handler.TcpConn4.LocalAddr(), and fall back to the standard sender when that is not known.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@pkg/runner/fastsend.go` around lines 44 - 59, The code currently derives
srcIP by calling scan.PkgRouter.Route(net.IPv4(1,1,1,1)), which bakes a single
address into baseSum and breaks TCP checksums on multihomed/non-default-route
hosts; instead, stop using Route(1.1.1.1) and pick a fixed local IPv4 for
checksum calculation from handler.SourceIp4 if set, then from the bound
connection address via handler.TcpConn4.LocalAddr() (extract the IP and To4()),
and only if neither is available fall back to the standard sender behavior
(return errNoSourceIP or use the existing sender path). Update the srcIP
computation and remove the PkgRouter.Route fallback so baseSum is computed from
a stable, connection-bound source IP.
| func parseIPv4Fast(s string) (ip [4]byte, ok bool) { | ||
| var octet, dots int | ||
| for i := 0; i < len(s); i++ { | ||
| c := s[i] | ||
| switch { | ||
| case c >= '0' && c <= '9': | ||
| octet = octet*10 + int(c-'0') | ||
| if octet > 255 { | ||
| return ip, false | ||
| } | ||
| case c == '.': | ||
| if dots >= 3 { | ||
| return ip, false | ||
| } | ||
| ip[dots] = byte(octet) | ||
| dots++ | ||
| octet = 0 | ||
| default: | ||
| return ip, false | ||
| } | ||
| } | ||
| if dots != 3 { | ||
| return ip, false | ||
| } | ||
| ip[3] = byte(octet) | ||
| return ip, true |
There was a problem hiding this comment.
Reject empty octets in parseIPv4Fast.
Inputs like .1.2.3, 1..2.3, and 1.2.3. currently parse successfully because . is accepted even when no digits were seen for the current octet. That violates the function contract and can silently rewrite malformed targets.
Proposed fix
func parseIPv4Fast(s string) (ip [4]byte, ok bool) {
- var octet, dots int
+ var octet, dots int
+ var hasDigit bool
for i := 0; i < len(s); i++ {
c := s[i]
switch {
case c >= '0' && c <= '9':
octet = octet*10 + int(c-'0')
if octet > 255 {
return ip, false
}
+ hasDigit = true
case c == '.':
- if dots >= 3 {
+ if dots >= 3 || !hasDigit {
return ip, false
}
ip[dots] = byte(octet)
dots++
octet = 0
+ hasDigit = false
default:
return ip, false
}
}
- if dots != 3 {
+ if dots != 3 || !hasDigit {
return ip, false
}
ip[3] = byte(octet)
return ip, true
}📝 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.
| func parseIPv4Fast(s string) (ip [4]byte, ok bool) { | |
| var octet, dots int | |
| for i := 0; i < len(s); i++ { | |
| c := s[i] | |
| switch { | |
| case c >= '0' && c <= '9': | |
| octet = octet*10 + int(c-'0') | |
| if octet > 255 { | |
| return ip, false | |
| } | |
| case c == '.': | |
| if dots >= 3 { | |
| return ip, false | |
| } | |
| ip[dots] = byte(octet) | |
| dots++ | |
| octet = 0 | |
| default: | |
| return ip, false | |
| } | |
| } | |
| if dots != 3 { | |
| return ip, false | |
| } | |
| ip[3] = byte(octet) | |
| return ip, true | |
| func parseIPv4Fast(s string) (ip [4]byte, ok bool) { | |
| var octet, dots int | |
| var hasDigit bool | |
| for i := 0; i < len(s); i++ { | |
| c := s[i] | |
| switch { | |
| case c >= '0' && c <= '9': | |
| octet = octet*10 + int(c-'0') | |
| if octet > 255 { | |
| return ip, false | |
| } | |
| hasDigit = true | |
| case c == '.': | |
| if dots >= 3 || !hasDigit { | |
| return ip, false | |
| } | |
| ip[dots] = byte(octet) | |
| dots++ | |
| octet = 0 | |
| hasDigit = false | |
| default: | |
| return ip, false | |
| } | |
| } | |
| if dots != 3 || !hasDigit { | |
| return ip, false | |
| } | |
| ip[3] = byte(octet) | |
| return ip, true | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@pkg/runner/fastsend.go` around lines 129 - 154, parseIPv4Fast currently
accepts dots even when no digits were read for the current octet; update
parseIPv4Fast to reject empty octets by tracking whether at least one digit was
seen for the current octet (e.g., a sawDigit boolean set to true on digit) and
on encountering '.' ensure sawDigit is true before assigning
ip[dots]=byte(octet), then reset sawDigit and octet for the next octet; also
verify sawDigit is true before assigning the final octet (ip[3]) and return
false if not.
Description
Adds a smart scan mode that reorders user-provided ports based on a port correlation model, prioritizing ports most likely to be open. When a port is found open, correlated ports get dynamically boosted in the scan queue. This doesn't add or remove any ports from the user's list, it only changes the order they're scanned in. In local benchmarks against a /24 subnet with 1000 ports, smart scan achieved full coverage parity with the standard scan while completing ~95x faster by finding open ports early and deprioritizing unlikely candidates.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation