Documentation
¶
Overview ¶
Package finding models a confirmed or candidate security issue in an engagement.
Index ¶
- Constants
- Variables
- func EqualDataFlowTrace(left, right *DataFlowTrace) bool
- func Identity(f Finding) string
- type Comment
- type DASTInput
- type DataFlowTrace
- type ExploitationInput
- type Finding
- func NewDAST(id, engagementID shared.ID, in DASTInput, now time.Time) (Finding, error)
- func NewExploitation(id, engagementID shared.ID, in ExploitationInput, proposer string, ...) (Finding, error)
- func NewHypothesis(id, engagementID shared.ID, in HypothesisInput, proposer string, now time.Time) (Finding, error)
- func NewManual(id, engagementID shared.ID, in ManualInput, now time.Time) (Finding, error)
- func NewSAST(id, engagementID shared.ID, in SASTInput, now time.Time) (Finding, error)
- func NewThreat(id, engagementID shared.ID, in ThreatInput, now time.Time) (Finding, error)
- func Publishable(in []Finding) []Finding
- type HypothesisInput
- type Kind
- type ManualInput
- type Retest
- type RetestOutcome
- type SASTInput
- type SourceLocation
- type Status
- type ThreatInput
- type Verdict
Constants ¶
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.
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.
const (
// MaxDataFlowSteps caps the source-to-sink positions persisted with one finding.
MaxDataFlowSteps = 64
)
Variables ¶
var ( ErrDataFlowLanguage = errors.New("data-flow language is invalid") ErrDataFlowSteps = errors.New("data-flow steps are invalid") )
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
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
MeetsEvidenceBar reports whether the finding has enough evidence to be promoted.
func (*Finding) RequiresEvidenceGate ¶
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
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 ¶
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 ¶
IsRuleBased reports whether k is a kind that requires a catalog rule key.
func (Kind) Persistable ¶ added in v0.2.0
Persistable reports whether k may be stored in the native findings repository. Empty remains the legacy alias for SCA; external is deliberately reader-only.
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.
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 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.