finding

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 26, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package finding models a confirmed or candidate security issue in an engagement.

Index

Constants

View Source
const (
	ClassThirdParty         = "third_party"
	ClassFirstPartyHistoric = "first_party_historical"
	// ClassFirstParty is a first-party, ACTIONABLE weakness in the project's OWN source – e.g. a
	// deterministic pattern-SAST hit. Unlike ClassFirstPartyHistoric (unconfirmable advisory,
	// informational), it is real and remediable; unlike ClassThirdParty, it is not a dependency.
	ClassFirstParty = "first_party"
)

Finding classes: third-party findings are actionable; first-party historical advisories are matched against the project's own unversioned modules and are informational only – never counted in remediation/critical totals.

View Source
const EvidenceThreshold = verdict.EvidenceThreshold

EvidenceThreshold is the minimum evidence score for a finding to be promoted. It is the shared bar – defined once in internal/domain/verdict and aliased here, so finding + judgment can never drift apart.

View Source
const (
	// MaxDataFlowSteps caps the source-to-sink positions persisted with one finding.
	MaxDataFlowSteps = 64
)

Variables

View Source
var (
	ErrDataFlowLanguage = errors.New("data-flow language is invalid")
	ErrDataFlowSteps    = errors.New("data-flow steps are invalid")
)
View Source
var (
	ErrRuleKeyRequired       = errors.New("rule key is required")
	ErrRuleKeyForbidden      = errors.New("rule key is not allowed")
	ErrRuleKeyInvalid        = errors.New("rule key is invalid")
	ErrKindInvalid           = errors.New("finding kind is invalid")
	ErrKindReaderOnly        = errors.New("finding kind is reader-only")
	ErrSourceLocationFile    = errors.New("source location file is invalid")
	ErrSourceLocationLines   = errors.New("source location lines are invalid")
	ErrSourceLocationColumns = errors.New("source location columns are invalid")
)

Functions

func EqualDataFlowTrace added in v0.2.0

func EqualDataFlowTrace(left, right *DataFlowTrace) bool

func Identity

func Identity(f Finding) string

Identity returns the stable key used to associate a finding across scans.

Types

type Comment

type Comment struct {
	ID           shared.ID
	EngagementID shared.ID
	FindingID    shared.ID
	Author       string
	Body         string
	CreatedAt    time.Time
}

Comment is a persisted collaboration note on a finding – distinct from the append-only audit log; comments are the human activity thread.

func NewComment

func NewComment(id, engagementID, findingID shared.ID, author, body string, now time.Time) (Comment, error)

NewComment validates and builds a comment (non-empty body, attributed author).

type DASTInput

type DASTInput struct {
	JudgmentID  string          // confirmed judgment ID; legacy CapSAST runtime projections use this as the dedup anchor
	CWE         string          // the weakness, e.g. "CWE-89"
	Location    string          // where: URL path or deterministic check location
	Rule        string          // the check or taint rule that fired
	Source      string          // stable check source token for native CapDAST findings
	Fingerprint string          // stable check fingerprint for native CapDAST findings
	Severity    shared.Severity // optional; defaults to Unknown (re-triaged via the normal finding workflow)
}

DASTInput is the content for a finding promoted from a RUNTIME-verifier-confirmed judgment: a safe HTTP probe (governed by scope + sandbox egress + HITL approval upstream) confirmed the exploitability of an existing gated CapSAST hypothesis. It is filled DETERMINISTICALLY from the confirmed judgment (no LLM): the CWE, the location, and the taint rule, anchored to the source judgment id for dedup. It is the runtime twin of SASTInput — the SAME structured claim, but confirmed at runtime rather than by a static verifier, so it earns a distinct Kind (stronger, dynamically-proven evidence).

type DataFlowTrace added in v0.2.0

type DataFlowTrace struct {
	Language         string           `json:"language"`
	Source           SourceLocation   `json:"source"`
	Sink             SourceLocation   `json:"sink"`
	Steps            []SourceLocation `json:"steps"`
	CoverageComplete bool             `json:"coverage_complete"`
	GraphTruncated   bool             `json:"graph_truncated"`
}

DataFlowTrace is the persistent, source-only witness for a confirmed taint finding. Steps are ordered from Source to Sink and contain positions only; source text, value identifiers, and parser output never cross this domain boundary.

func CloneDataFlowTrace added in v0.2.0

func CloneDataFlowTrace(in *DataFlowTrace) *DataFlowTrace

CloneDataFlowTrace deep-copies pointer columns and the ordered step list at repository boundaries.

func (DataFlowTrace) Validate added in v0.2.0

func (d DataFlowTrace) Validate() error

type ExploitationInput

type ExploitationInput struct {
	Title       string
	Description string
	Severity    shared.Severity
	CVSSVector  string
	CWE         string
	AssetID     shared.ID
}

ExploitationInput is the content an agent (or operator) PROPOSES for an AI/exploitation finding. Note what is absent: there is no EvidenceScore field – the score that gates promotion is never settable by the proposer; it moves only via a sealed adversarial verdict.

type Finding

type Finding struct {
	ID           shared.ID
	EngagementID shared.ID
	Title        string
	Description  string
	Severity     shared.Severity
	CVSSVector   string
	CWE          string
	Status       Status

	// Kind discriminates how the finding was produced (sca|recon|exploitation|
	// manual); it drives promotion gating. Empty is treated as KindSCA for
	// backward compatibility with legacy rows.
	Kind Kind

	// RuleKey is the stable catalog rule identifier emitted by the producer.
	RuleKey string

	// SourceLocation is the structured source position emitted by first-party
	// analyzers. Legacy DedupKey locations remain supported during migration.
	SourceLocation *SourceLocation

	// DataFlow is a bounded source-to-sink position trace for confirmed semantic taint findings.
	DataFlow *DataFlowTrace

	// Workflow: the human assignee, and an optimistic-concurrency version that
	// Kanban/status/assignee edits check to prevent lost updates.
	Assignee string
	Version  int

	// Risk priority (CISA KEV -> EPSS x CVSS), copied from the source vuln so
	// findings can be ordered by real risk. KEV findings rank above all.
	KEV bool
	// PublicExploit is true when a public exploit is known to exist for this finding's vulnerability
	// (an exploitation-risk signal from the advisory corpus, D1.3). Surfaced for triage; it does not itself
	// change ranking (KEV, actual exploitation, stays the top signal). Omitted from JSON when false.
	PublicExploit bool `json:",omitempty"`
	// EPSSPercentile is the EPSS score's rank among all scored CVEs (0..1), copied from the source vuln as
	// a triage aid alongside RiskScore. Omitted from JSON when unset (0) so a scan path with no EPSS
	// enrichment (e.g. an offline CLI scan without the corpus percentile) does not emit a misleading 0.
	EPSSPercentile float64 `json:",omitempty"`
	// RiskScore is the computed risk priority; omitted from JSON when unset (0) so a scan path that does
	// not populate it — e.g. the CLI, which has no KEV/EPSS enrichment — does not emit a misleading 0.00.
	RiskScore float64 `json:",omitempty"`

	// Detection provenance: the sources that detected the underlying
	// vulnerability and the multi-source confidence. Empty for non-SCA findings.
	Sources    []string
	Confidence string

	// Continuous vulnerability-intelligence provenance. Empty for legacy and
	// non-SCA findings; these fields are machine-owned projection metadata.
	AdvisoryID           string
	OccurrenceID         shared.ID
	ComponentFingerprint string
	FixedVersion         string
	// DirectBumps is the minimal set of DIRECT (top-level) dependencies to upgrade to remove this
	// transitive vulnerability from the resolved graph (EPIC #860 D3.8, the "upgrade path"). It is sorted
	// and deduplicated. A directly-declared vulnerable dependency lists itself. It is empty for a
	// first-party/non-SCA finding, when no dependency graph was resolved, or when the component is reachable
	// only through a dependency cycle (no clean set of direct introducers). Omitted from JSON when empty.
	DirectBumps []string `json:",omitempty"`
	// DetectionState is the continuous-intelligence projection's lifecycle state; empty on a one-shot
	// scan (e.g. the CLI) that has no stored occurrence history. Omitted from JSON when empty so a
	// consumer does not build logic on a field that is a constant blank on those paths.
	DetectionState   string `json:",omitempty"`
	RiskAssessmentID shared.ID
	EvaluatedAt      *time.Time

	// Class separates actionable third-party findings from historical
	// advisories matched against the project's own unversioned modules.
	Class string

	// Finding-quality signals: the component scope, metadata-only
	// reachability, action impact, and unified Synapse risk priority (1..5).
	Scope        string
	Reachability string
	Impact       string
	Priority     int

	// ClassReachability is the coarse JVM class-reachability verdict for the component:
	// "reachable" | "unreferenced" | "" (unknown). Advisory only – deprioritizes an unreferenced
	// component's finding, never suppresses it; lets a report/export SEPARATE used from unreferenced deps.
	ClassReachability string `json:",omitempty"`

	// DedupKey makes a finding idempotent across re-scans (e.g. advisory+component+version
	// for SCA vulns, or license:<id>); used as the upsert conflict key.
	DedupKey string

	// EvidenceScore gates promotion: candidates below the threshold are not
	// auto-promoted (deterministic evidence gating: AI-proposed findings are never
	// auto-promoted). Omitted from JSON when unset (0), so a scan path that does not score evidence
	// (e.g. the CLI) does not emit a meaningless 0.
	EvidenceScore int `json:",omitempty"`

	// ProposedBy is the actor that proposed an exploitation/AI finding (e.g. "agent:<sid>").
	// It exists so the adversarial verifier that later raises the score CANNOT be the same
	// actor that proposed it (a finding cannot confirm itself). Empty for
	// SCA/recon/manual findings.
	ProposedBy string

	Audit shared.Audit
}

Finding is a confirmed or candidate security issue within an engagement.

func NewDAST

func NewDAST(id, engagementID shared.ID, in DASTInput, now time.Time) (Finding, error)

NewDAST builds a runtime-confirmed DAST finding (Kind=dast) from a judgment a DISTINCT runtime verifier confirmed via a safe probe. The title + description are TEMPLATED from the structured CWE/rule/location (never LLM prose). It is idempotent by the dast:ai:<judgmentID> dedup key — a re-confirm updates in place rather than duplicating; the key is distinct from the SAST projection's "sast:ai:<id>" so a claim confirmed statically (Kind=sast) and one confirmed at runtime (Kind=dast) never collide, and the runtime finding is its own row. Reachability is "reachable": unlike a static SAST hit, a runtime probe DEMONSTRATED the sink is reachable and exploitable. Severity defaults to Unknown so a human triages it through the standard workflow (a probe carries no CVSS). ProposedBy is deliberately LEFT EMPTY: the evidence gate already ran at the judgment layer (a distinct verifier sealed a verdict ≥ threshold), so this projection is publishable like a manual finding — setting it to the agent proposer would wrongly re-gate it stuck-at-score-0.

func NewExploitation

func NewExploitation(id, engagementID shared.ID, in ExploitationInput, proposer string, now time.Time) (Finding, error)

NewExploitation builds a KindExploitation finding from a proposal. It ALWAYS starts at EvidenceScore 0: an AI finding is an unproven CLAIM until an independent adversarial verifier seals a verdict (ApplyVerdict). Until then it can be neither confirmed (CanPromote == false, since exploitation findings are evidence-gated) nor included in a report. proposer is recorded so the verifier that later raises the score cannot be the proposer. id + now are supplied by the use case (deterministic, testable).

func NewHypothesis

func NewHypothesis(id, engagementID shared.ID, in HypothesisInput, proposer string, now time.Time) (Finding, error)

NewHypothesis builds an AI-proposed attack-chain hypothesis finding (Kind=hypothesis). Unlike a threat projection (ungated – its evidence gate ran at the judgment layer), a hypothesis is the agent's UNPROVEN claim, so ProposedBy is SET → it RequiresEvidenceGate and starts at score 0 → it is non-publishable: it can reach the report only once a DISTINCT human raises its EvidenceScore to the bar (>= EvidenceThreshold) via the standard finding verify path (exploitation.Service.Confirm → ApplyVerdict, which gates on RequiresEvidenceGate so it serves a hypothesis too); the agent can never raise its own score (SoD). Still pending: a CREATE path – the agent propose tool that calls NewHypothesis – until which nothing produces a hypothesis to verify; that tool MUST redact the prose at the agent edge (like propose_finding). The constituent finding ids are recorded in the TEMPLATED description (Finding has no structured cross-reference field; folding them in keeps the chain self-contained and the report path templated) – the constructor never loads or mutates the constituent findings (no auto-merge). Dedup is by the sorted constituent set, so re-proposing the same chain updates in place.

func NewManual

func NewManual(id, engagementID shared.ID, in ManualInput, now time.Time) (Finding, error)

NewManual validates operator input and builds a manual finding. Manual findings get a unique dedup key (manual:<id>) so distinct entries never merge, are Kind=manual (human-authored, not evidence-gated), and start Open at version 1. id + now are supplied by the use case (deterministic, testable).

func NewSAST

func NewSAST(id, engagementID shared.ID, in SASTInput, now time.Time) (Finding, error)

NewSAST builds a first-party SAST finding (Kind=sast) from a verifier-confirmed taint judgment. The title + description are TEMPLATED from the structured CWE/rule/location (never LLM prose). It is idempotent by the sast:ai:<judgmentID> dedup key – a re-confirm updates in place rather than duplicating (distinct from the pattern-SAST "sast:rule:file:line" key, so deterministic E38 hits and gated E39 hits never collide). Severity defaults to Unknown so a human triages it through the standard workflow (a taint hit carries no CVSS). ProposedBy is deliberately LEFT EMPTY: the evidence gate already ran at the judgment layer (gated propose→verify, score ≥ threshold, a DISTINCT verifier), so this projection is publishable like a manual finding – setting it to the agent proposer would wrongly re-gate it stuck-at-score-0.

func NewThreat

func NewThreat(id, engagementID shared.ID, in ThreatInput, now time.Time) (Finding, error)

NewThreat builds a first-party threat-model finding (Kind=threat) from a confirmed STRIDE threat. The title is TEMPLATED from the structured category + element (never LLM prose). It is idempotent by the threat:<judgmentID> dedup key – a re-confirm updates in place rather than duplicating. Severity defaults to Unknown so a human triages it through the standard finding workflow (a STRIDE threat carries no CVSS).

func Publishable

func Publishable(in []Finding) []Finding

Publishable filters a finding slice to those that may appear in a customer-facing deliverable, applying the deterministic evidence gate via CanPromote. It is the SINGLE rule every client-facing reader funnels through – directly, or via the repository's ListPublishableByEngagement – so no export/report surface (PDF, HTML, DOCX, SARIF, OpenVEX, engagement bundle) can leak an unproven exploitation finding. The input is not mutated; order is preserved.

func (Finding) ApplyVerdict

func (f Finding) ApplyVerdict(v Verdict, now time.Time) (Finding, error)

ApplyVerdict returns a copy of the finding with its EvidenceScore set to the verdict's score – the ONLY transition that moves an evidence-gated finding's score. It gates on RequiresEvidenceGate (PROVENANCE-keyed, not kind-keyed), so it serves EVERY gated finding – an AI/exploitation claim and an AI attack-chain hypothesis alike – and refuses an UNGATED finding (no agent proposer: a scanner SCA/recon finding or a human-authored manual finding, whose score is not verdict-driven) and an invalid verdict. The caller (the exploitation use case) MUST have sealed the verdict as evidence first; this method is the pure state change, not the provenance record. The verifier must be a DISTINCT actor from the proposer (SelfConfirm) regardless of kind, so a gated finding can never confirm itself.

func (*Finding) CanPromote

func (f *Finding) CanPromote() bool

CanPromote reports whether the finding may gain native confirmation/publication authority. Reader-only external and unknown origins are hard-denied regardless of evidence score. Other gated findings must meet the shared evidence bar; native ungated findings may promote normally. Confirmation and client-facing publication paths use this as the deterministic authority gate.

func (*Finding) MeetsEvidenceBar

func (f *Finding) MeetsEvidenceBar() bool

MeetsEvidenceBar reports whether the finding has enough evidence to be promoted.

func (*Finding) RequiresEvidenceGate

func (f *Finding) RequiresEvidenceGate() bool

RequiresEvidenceGate reports whether a finding needs evidence authority before promotion. It gates on provenance, not only category: any AI/agent-proposed finding is an unproven claim, and KindExploitation is gated defensively even without proposer metadata. External and unknown origins also report gated so callers fail closed, but CanPromote hard-denies them regardless of score. Native deterministic/human findings with no proposer remain ungated.

func (Finding) ValidatePersistence added in v0.2.0

func (f Finding) ValidatePersistence() error

ValidatePersistence enforces the origin contract for the native findings store. External is a reader-only management projection and unknown kinds fail closed.

func (Finding) ValidateRuleKey

func (f Finding) ValidateRuleKey() error

ValidateRuleKey enforces the structural invariant for rule keys: rule-based findings must have a valid key, non-rule findings must have an empty key.

type HypothesisInput

type HypothesisInput struct {
	Title          string
	Description    string
	ConstituentIDs []string        // the finding ids this chain links (>= 2; a chain links multiple findings)
	Severity       shared.Severity // optional; defaults to Unknown (a human triages the chain's severity)
	AssetID        shared.ID
}

HypothesisInput is the content for an attack-chain HYPOTHESIS finding: the AI's narrative that a SET of existing findings chain into an attack path. It is a PROPOSAL – non-publishable until a distinct human verifies it (evidence-gated like an exploitation finding). The constituent finding ids are the chain being hypothesized; building the hypothesis NEVER modifies or merges them – it only NAMES them.

type Kind

type Kind string

Kind identifies how a finding was produced. Native kinds participate in the normal finding workflow; KindExternal is known to readers but deliberately has no native confirmation, publication, or persistence authority.

const (
	KindSCA          Kind = "sca"
	KindRecon        Kind = "recon"
	KindExploitation Kind = "exploitation"
	KindManual       Kind = "manual"
	KindSAST         Kind = "sast"          // first-party source-code issue (SAST)
	KindSecret       Kind = "secret"        // a hardcoded secret found in source (deterministic; ungated)
	KindMisconfig    Kind = "misconfig"     // an insecure IaC/config setting (deterministic; ungated)
	KindCloudPosture Kind = "cloud_posture" // a deterministic live cloud posture or IaC/live drift finding
	KindDAST         Kind = "dast"          // runtime-confirmed app issue: a safe probe verified exploitability of a gated hypothesis
	KindThreat       Kind = "threat"        // threat-model item
	KindHypothesis   Kind = "hypothesis"    // AI-proposed attack-chain hypothesis linking findings (gated until human-verified)
	KindQuality      Kind = "quality"       // maintainability / code-smell issue (deterministic; ungated)
	KindReliability  Kind = "reliability"   // likely bug (deterministic; ungated)
	// KindExternal is a reader-only management projection for an externally sourced work item.
	// It must never be persisted as a native finding or gain confirmation/publication authority.
	KindExternal Kind = "external"
)

func (Kind) IsRuleBased

func (k Kind) IsRuleBased() bool

IsRuleBased reports whether k is a kind that requires a catalog rule key.

func (Kind) Persistable added in v0.2.0

func (k Kind) Persistable() bool

Persistable reports whether k may be stored in the native findings repository. Empty remains the legacy alias for SCA; external is deliberately reader-only.

func (Kind) Valid

func (k Kind) Valid() bool

Valid reports whether k is a known finding kind.

type ManualInput

type ManualInput struct {
	Title       string
	Description string
	Severity    shared.Severity
	CVSSVector  string
	CWE         string
}

ManualInput is the operator-supplied content for a hand-authored finding.

type Retest

type Retest struct {
	ID           shared.ID     `json:"id"`
	EngagementID shared.ID     `json:"engagementId"`
	FindingID    shared.ID     `json:"findingId"`
	Outcome      RetestOutcome `json:"outcome"`
	Note         string        `json:"note,omitempty"`
	Tester       string        `json:"tester"`
	At           time.Time     `json:"at"`
}

Retest is one append-only retest record on a finding (who re-tested, the outcome, and a note), kept as history for chain-of-custody and consultancy reporting.

func NewRetest

func NewRetest(id, engagementID, findingID shared.ID, outcome RetestOutcome, note, tester string, now time.Time) (Retest, error)

NewRetest validates and constructs a retest record.

type RetestOutcome

type RetestOutcome string

RetestOutcome is the verdict of re-testing a finding (retest tracking).

const (
	RetestRemediated      RetestOutcome = "remediated"
	RetestStillVulnerable RetestOutcome = "still_vulnerable"
	RetestNotReproducible RetestOutcome = "not_reproducible"
)

func (RetestOutcome) ResultingStatus

func (o RetestOutcome) ResultingStatus() Status

ResultingStatus maps a retest outcome to the finding status it implies, so a retest moves the finding consistently (remediated -> remediated, still vulnerable -> confirmed, not reproducible -> false positive).

func (RetestOutcome) Valid

func (o RetestOutcome) Valid() bool

Valid reports whether o is a known outcome.

type SASTInput

type SASTInput struct {
	JudgmentID string          // the confirmed CapSAST judgment (dedup anchor – re-confirming updates in place)
	CWE        string          // the weakness, e.g. "CWE-89"
	Location   string          // where: "path[:line]" or the importPath.Symbol of the sink-using function
	Rule       string          // the taint rule that fired, e.g. "taint-sqli"
	Severity   shared.Severity // optional; defaults to Unknown (re-triaged via the normal finding workflow)
	DataFlow   *DataFlowTrace  // optional bounded source-to-sink witness from the confirmed judgment
}

SASTInput is the content for a finding promoted from a verifier-confirmed CapSAST (taint) judgment . It is filled DETERMINISTICALLY from the confirmed judgment (no LLM): the CWE, the location (the sink-using function's importPath.Symbol – function-granular in the E39 MVP), and the taint rule, anchored to the source judgment id for dedup.

type SourceLocation added in v0.1.8

type SourceLocation struct {
	File        string `json:"file"`
	StartLine   int    `json:"start_line"`
	EndLine     int    `json:"end_line"`
	StartColumn *int   `json:"start_column,omitempty"`
	EndColumn   *int   `json:"end_column,omitempty"`
}

SourceLocation is a producer-owned source range. Lines are one-based and inclusive; columns are UTF-8 byte offsets, zero-based and end-exclusive. Nil columns mean that the producer knows only the line range.

func SourceLocationFromLegacy added in v0.1.8

func SourceLocationFromLegacy(value string) (SourceLocation, bool)

SourceLocationFromLegacy converts an unambiguous legacy file:line value. It deliberately rejects Windows paths and malformed locations rather than guessing.

func (SourceLocation) Validate added in v0.1.8

func (l SourceLocation) Validate() error

Validate rejects ambiguous, unsafe, and non-canonical source ranges.

type Status

type Status string

Status is the finding triage lifecycle state.

const (
	StatusOpen       Status = "open"
	StatusTriage     Status = "triage"
	StatusConfirmed  Status = "confirmed"
	StatusFalsePos   Status = "false_positive"
	StatusRemediated Status = "remediated"
)

func (Status) Valid

func (s Status) Valid() bool

Valid reports whether s is a known triage status.

type ThreatInput

type ThreatInput struct {
	JudgmentID string          // the confirmed threat judgment (dedup anchor – re-confirming updates in place)
	Category   string          // STRIDE category token (e.g. "spoofing", "info_disclosure")
	Element    string          // the threatened model element id (a component or data flow)
	Asset      string          // optional Asset.ID at risk ("" when none)
	Severity   shared.Severity // optional; defaults to Unknown (re-triaged via the normal finding workflow)
}

ThreatInput is the content for a finding promoted from a human-ratified STRIDE threat judgment. It is filled DETERMINISTICALLY from the confirmed judgment (no LLM): the STRIDE category + the threatened model element + the optional asset at risk, anchored to the source judgment id for dedup.

type Verdict

type Verdict = verdict.Verdict

Verdict is the shared adversarial "try to refute" outcome, defined in internal/domain/verdict so finding and judgment share ONE mechanism + bar (R1). Aliased here so existing call sites keep using finding.Verdict.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL