diff --git a/internal/scan/frameworks/cve_internal_test.go b/internal/scan/frameworks/cve_internal_test.go new file mode 100644 index 0000000..1a5d896 --- /dev/null +++ b/internal/scan/frameworks/cve_internal_test.go @@ -0,0 +1,36 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package frameworks + +import "testing" + +func TestVersionAffected(t *testing.T) { + tests := []struct { + version string + affected string + want bool + }{ + {"4.2", "4.2", true}, + {"4.2.1", "4.2", true}, + {"4.2.13", "4.2", true}, + {"4.20", "4.2", false}, // the boundary bug: 4.20 is not a 4.2.x release + {"4.20.0", "4.2", false}, + {"5.0", "4.2", false}, + } + + for _, tt := range tests { + if got := versionAffected(tt.version, tt.affected); got != tt.want { + t.Errorf("versionAffected(%q, %q) = %v, want %v", tt.version, tt.affected, got, tt.want) + } + } +} diff --git a/internal/scan/frameworks/detect.go b/internal/scan/frameworks/detect.go index 072333d..2ae683a 100644 --- a/internal/scan/frameworks/detect.go +++ b/internal/scan/frameworks/detect.go @@ -17,6 +17,7 @@ import ( "fmt" "io" "net/http" + "strings" "sync" "time" @@ -99,9 +100,11 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo }() // Find the best match + // results arrive in goroutine-completion order; tie-break on name so the + // winner is deterministic when two detectors land on the same confidence. var best detectionResult for r := range results { - if r.confidence > best.confidence { + if r.confidence > best.confidence || (r.confidence == best.confidence && r.name < best.name) { best = r } } @@ -169,7 +172,7 @@ func getVulnerabilities(framework, version string) ([]string, []string) { for _, entry := range entries { for _, affectedVer := range entry.AffectedVersions { - if version == affectedVer || hasPrefix(version, affectedVer) { + if versionAffected(version, affectedVer) { cves = append(cves, fmt.Sprintf("%s (%s)", entry.CVE, entry.Severity)) for _, rec := range entry.Recommendations { if !seenRecs[rec] { @@ -185,7 +188,9 @@ func getVulnerabilities(framework, version string) ([]string, []string) { return cves, recommendations } -// hasPrefix is a simple prefix check without importing strings. -func hasPrefix(s, prefix string) bool { - return len(s) >= len(prefix) && s[:len(prefix)] == prefix +// versionAffected reports whether version falls under an affected-version +// entry. the entry is a version prefix, matched only on dotted boundaries, so +// "4.2" covers 4.2 and 4.2.1 but not 4.20. +func versionAffected(version, affected string) bool { + return version == affected || strings.HasPrefix(version, affected+".") } diff --git a/internal/scan/frameworks/detectors/backend.go b/internal/scan/frameworks/detectors/backend.go index 16e5b62..4b324ae 100644 --- a/internal/scan/frameworks/detectors/backend.go +++ b/internal/scan/frameworks/detectors/backend.go @@ -47,9 +47,11 @@ func init() { fw.Register(&codeigniterDetector{}) } -// sigmoidConfidence converts a weighted score to a 0-1 confidence value. +// sigmoidConfidence maps the matched-weight fraction to a 0-1 confidence, +// centered at 0.3 so a single weak signature match no longer clears the 0.5 +// detection threshold (it used to: sigmoid(0) was 0.5, so any match "detected"). func sigmoidConfidence(score float32) float32 { - return float32(1.0 / (1.0 + math.Exp(-float64(score)*6.0))) + return float32(1.0 / (1.0 + math.Exp(-(float64(score)-0.3)*10.0))) } // laravelDetector detects Laravel framework. diff --git a/internal/scan/frameworks/detectors/confidence_test.go b/internal/scan/frameworks/detectors/confidence_test.go new file mode 100644 index 0000000..d9bf209 --- /dev/null +++ b/internal/scan/frameworks/detectors/confidence_test.go @@ -0,0 +1,30 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package detectors + +import "testing" + +func TestSigmoidConfidence(t *testing.T) { + // a weak match (small matched-weight fraction) must stay below the 0.5 + // detection threshold; a strong match must clear it. the old curve put any + // match above 0.5, which is what false-detected magento on a plain page. + if c := sigmoidConfidence(0); c >= 0.5 { + t.Errorf("no match conf = %.3f, want < 0.5", c) + } + if c := sigmoidConfidence(0.2); c >= 0.5 { + t.Errorf("weak match conf = %.3f, want < 0.5", c) + } + if c := sigmoidConfidence(0.5); c <= 0.5 { + t.Errorf("strong match conf = %.3f, want > 0.5", c) + } +}