Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,11 @@
"adjudication_complete": {
"type": "boolean",
"description": "True when every survivor (outside holes/abandoned) reached a terminal state — the trust signal that an empty fingerprints list is 'clean' rather than 'not adjudicated yet'."
},
"abandoned": {
"type": "integer",
"minimum": 0,
"description": "How many survivors adjudication gave up on after the retry cap — a terminal coverage gap, distinct from a still-pending survivor (which a later run retries). A count rather than a list because the list is already determined by survivors and adjudicated_commits. Nonzero alongside adjudication_complete means the queue drained partly by giving up: complete, but not a clean bill of health."
}
}
},
Expand Down Expand Up @@ -397,6 +402,10 @@
"type": "string",
"minLength": 1,
"description": "Why it never resolved, e.g. \"diff too large\", \"triage: <error>\", or an adjudication timeout"
},
"retryable": {
"type": "boolean",
"description": "Whether a later run should re-examine this commit: a transient diff-load, triage, or backend/adjudication failure. Absent (false) for a deterministic hole (oversize diff) or a context written before this field existed."
}
}
},
Expand Down Expand Up @@ -438,6 +447,15 @@
"type": "integer",
"minimum": 0,
"description": "Failed adjudication tries; at the cap the survivor is abandoned."
},
"verdict": {
"type": "string",
"enum": ["no_finding", "abstain", "rejected"],
Comment thread
matiasinsaurralde marked this conversation as resolved.
"description": "Terminal adjudication outcome for a survivor that produced no fingerprint. A survivor with a fingerprint is a finding and carries no verdict. Absent means: a finding, not adjudicated yet, or errored and still pending. Descriptive only — never affects scheduling."
},
"verdict_reason": {
Comment thread
matiasinsaurralde marked this conversation as resolved.
"type": "string",
"description": "The adjudicator's own short explanation, carried only for abstentions (verdict == \"abstain\"). Truncated by the producer; never the full rationale. Absent for every other verdict."
}
}
},
Expand Down
48 changes: 48 additions & 0 deletions internal/schemavalidators/schemavalidators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,54 @@ func TestValidateSecurityContextTriageFields(t *testing.T) {
scan["unexpected_field"] = "x"
require.ErrorContains(t, schemavalidators.ValidateSecurityContext(payload, ""), "additionalProperties")
})

t.Run("an unresolved entry carrying a retryable flag validates", func(t *testing.T) {
payload, scan := load(t)
scan["unresolved"] = []any{map[string]any{
"sha": "8c948c742bdfc09c4aae6b3c386faeb98f925ff2",
"reason": "adjudication: agent error",
"retryable": true,
}}
require.NoError(t, schemavalidators.ValidateSecurityContext(payload, ""))
})

t.Run("an unknown unresolved field is still rejected", func(t *testing.T) {
payload, scan := load(t)
scan["unresolved"] = []any{map[string]any{
"sha": "8c948c742bdfc09c4aae6b3c386faeb98f925ff2",
"reason": "x",
"unexpected_key": "y",
}}
require.ErrorContains(t, schemavalidators.ValidateSecurityContext(payload, ""), "additionalProperties")
})

t.Run("a survivor carrying an abstain verdict and reason validates", func(t *testing.T) {
payload, _ := load(t)
payload["survivors"] = []any{map[string]any{
"commit_sha": "8c948c742bdfc09c4aae6b3c386faeb98f925ff2",
"verdict": "abstain",
"verdict_reason": "insufficient context to decide reachability",
}}
require.NoError(t, schemavalidators.ValidateSecurityContext(payload, ""))
})

t.Run("a no_finding verdict without a reason validates", func(t *testing.T) {
payload, _ := load(t)
payload["survivors"] = []any{map[string]any{
"commit_sha": "8c948c742bdfc09c4aae6b3c386faeb98f925ff2",
"verdict": "no_finding",
}}
require.NoError(t, schemavalidators.ValidateSecurityContext(payload, ""))
})

t.Run("a verdict outside the enum is rejected", func(t *testing.T) {
payload, _ := load(t)
payload["survivors"] = []any{map[string]any{
"commit_sha": "8c948c742bdfc09c4aae6b3c386faeb98f925ff2",
"verdict": "finding",
}}
require.ErrorContains(t, schemavalidators.ValidateSecurityContext(payload, ""), "enum")
})
}

func TestValidateOpenAPI(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ type ScanWindow struct {
type Unresolved struct {
SHA string `json:"sha"`
Reason string `json:"reason"`
// Retryable is true when a later run should re-examine this commit: a transient
// diff-load, triage, or backend/adjudication failure. Absent (false) for a
// deterministic hole (an oversize diff) or a context written before this field
// existed.
Retryable bool `json:"retryable,omitempty"`
}

// ScanStats is the funnel every stage reports into, so that a silent failure
Expand Down Expand Up @@ -134,6 +139,11 @@ type ScanStats struct {
// reached a terminal state — the signal that an empty fingerprints list is
// "clean" rather than "not adjudicated yet".
AdjudicationComplete bool `json:"adjudication_complete,omitempty"`
// Abandoned is how many survivors adjudication gave up on after the retry cap — a
// terminal coverage gap, distinct from a still-pending survivor. Nonzero alongside
// AdjudicationComplete means the queue drained partly by giving up: complete, but
// not a clean bill of health.
Abandoned int `json:"abandoned,omitempty"`
}

// Survivor is one commit that survived Phase-1 triage: an entry in the
Expand All @@ -152,6 +162,13 @@ type Survivor struct {
// Attempts counts failed adjudication tries; at the cap the survivor is
// abandoned.
Attempts int `json:"attempts,omitempty"`
// Verdict is the terminal adjudication outcome for a survivor that produced no
// fingerprint: no_finding | abstain | rejected. A survivor with a fingerprint is
// a finding and carries no verdict. Descriptive only — never affects scheduling.
Verdict string `json:"verdict,omitempty"`
// VerdictReason is the adjudicator's short explanation, carried only for
// abstentions (Verdict == "abstain"). Truncated by the producer.
VerdictReason string `json:"verdict_reason,omitempty"`
}

// TopRisk is a component with a security-fix history, ranked by severity mass
Expand Down
Loading