Files
vmfunc 1237f3f09e feat(finding): normalized finding layer for notify and diff
scan results live in ~two dozen structs with no shared shape, so every
consumer that wants "what did this run turn up" reimplements the
type-switch. add internal/finding: an ordered Severity (info<low<medium<
high<critical, with parse/compare) and Flatten, the single type-switch
that collapses every scan result struct into flat, severity-ranked
Findings keyed module:identifier for stable dedup/diff.

wire collectFindings off Flatten in the run loop so notify and diff
(later bundles) build on one normalization path instead of re-deriving
it; the report path keeps emitting raw json blobs unchanged. expose
JavascriptScanResult.SupabaseFindings so the js internals stay private.

the guard test iterates a representative instance of every ResultType
and fails if Flatten lacks a case (falls through to :unhandled) - so a
new scanner can't ship without a Flatten case landing too.
2026-06-10 15:29:20 -07:00

79 lines
3.1 KiB
Go

/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package finding
import "strings"
// Severity is an ordered severity rank shared by every normalized finding.
// the order matters: notify gates on a threshold and diff sorts by it, so the
// underlying ints have to compare info < low < medium < high < critical.
type Severity int
// severity ranks, lowest to highest. SeverityUnknown sorts below everything so
// an unrecognized scanner string never silently outranks a real critical.
const (
SeverityUnknown Severity = iota
SeverityInfo
SeverityLow
SeverityMedium
SeverityHigh
SeverityCritical
)
// severityNames maps each rank to its canonical lowercase string. the wire
// format scanners emit ("info"/"low"/...) round-trips through this table.
var severityNames = map[Severity]string{
SeverityUnknown: "unknown",
SeverityInfo: "info",
SeverityLow: "low",
SeverityMedium: "medium",
SeverityHigh: "high",
SeverityCritical: "critical",
}
// String renders the canonical lowercase name for the rank.
func (s Severity) String() string {
if name, ok := severityNames[s]; ok {
return name
}
return severityNames[SeverityUnknown]
}
// ParseSeverity maps a scanner's free-form severity string onto a rank. it's
// case/space insensitive and folds the common synonyms ("informational",
// "warning", "moderate") so the dozen scanners that each picked their own
// spelling all land on the same ladder. an empty or unrecognized value is
// SeverityUnknown rather than a guess.
func ParseSeverity(raw string) Severity {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "critical":
return SeverityCritical
case "high":
return SeverityHigh
case "medium", "moderate", "warning":
return SeverityMedium
case "low":
return SeverityLow
case "info", "informational", "information", "none":
return SeverityInfo
default:
return SeverityUnknown
}
}
// AtLeast reports whether s is at or above threshold; notify uses it to drop
// findings below the configured floor.
func (s Severity) AtLeast(threshold Severity) bool {
return s >= threshold
}