Scoring & Verdicts in Gadriel Code
Audience: engineers and security owners who need to understand how Gadriel Code turns a list of findings into a single 0–10 score, a PASS / PARTIAL / FAIL verdict, and a CI exit code.
Gadriel Code's scoring model is the Gadriel Scoring Model — Gadriel's shared, deterministic risk-scoring methodology. The pipeline is strictly bottom-up and fully auditable — every number on the wire can be reconstructed from the finding list:
per-finding risk → pillar score → overall score → verdict → exit code
This page walks each stage, with the exact constants and a worked example.
1. Per-finding risk
Each finding is scored multiplicatively:
risk (uncapped) = base × ∏(standard amplifiers) × ∏(graph amplifiers) × ∏(scan amplifiers) × confidence
risk (capped) = min(uncapped, 15.0)
baseis the rule's severity-derived base score.- Amplifiers are context multipliers drawn from the AST, scope, and the 3D codebase graph.
confidenceis the[0, 1]confidence damper;1.0is the no-op default, and it maps from the finding's confidence tier so a low-confidence finding contributes less risk. This is the same severity×confidence discounting described in false-positives.md.- The cap fires per finding, before aggregation (
FINDING_CAP = 15.0,finding_score.rs). Pillar aggregation only ever sees post-cap values. The uncapped value is preserved for ranking when many findings hit the cap.
Higher risk is worse here (this is a risk number, not a score-out-of-10 yet; the inversion happens at the pillar step).
2. Amplifiers
There are 18 amplifier conditions across three categories (amplifier.rs). A multiplier > 1.0 raises risk; a damper < 1.0 lowers it.
Standard (AST + scope)
| Amplifier | × | Meaning |
|---|---|---|
AgentHasToolAccess | 1.4 | Code path can reach a side-effecting tool/function. |
OutputUnvalidated | 1.2 | Output consumed without validation. |
ExternalDataSource | 1.1 | Inbound data originates externally. |
NoHumanOversight | 1.3 | Executes with no human-in-the-loop gate. |
ProductionConfigDetected | 1.5 | A production marker is present in adjacent config. |
MultiAgentBoundary | 1.2 | Path crosses an agent-to-agent boundary. |
RecursiveAgentLoop | 1.6 | Implicated function is reachable from itself. |
Graph (queried from the 3D codebase graph)
| Amplifier | × | Meaning |
|---|---|---|
HighCentrality | 1.5 | Node is top-N for a centrality metric. |
SimulationConfirmed | 1.8 | Attack-graph simulation reaches a high-impact sink. |
SupplyChainPath | 1.3 | Path exists from an external node within max_hops. |
CycleMember | 1.4 | Node is in a strongly-connected component ≥ min_size. |
DriftDegrading | 1.2 | Component shows a degrading risk-score trend. |
ShortestPathCritical | 1.5 | Shortest path from an external entry is below the criticality threshold. |
ProvenPathToAsset | 2.0 | A definitively-confirmed cross-layer attack path runs from a real entry point to a real high-value asset (Node::Asset) touching this finding. |
Scan (SCA-specific)
| Amplifier | × | Meaning |
|---|---|---|
DependencyReachable | 1.6 | Vulnerable dependency function confirmed reachable. |
DependencyAbandoned | 1.3 | No maintainer activity in 12+ months. |
TransitiveDependency | 0.8 (damper) | Vulnerability is in a transitive dependency. |
LicenseCommercialConflict | 1.0 | Compliance-only sentinel — does not change the security score. |
Notable: reachability and proven-path-to-asset are the strongest
signals — a CVE proven reachable (DependencyReachable, 1.6×) or a finding on a
proven attack path to an asset (ProvenPathToAsset, 2.0×) is amplified sharply,
while a transitive-only dependency is damped (0.8×). This is how the model
concentrates risk on the findings that actually matter.
3. Pillar scores
Findings are grouped into 8 pillars. Each pillar's score
inverts risk to a "higher is better" 0–10 scale (pillar_score.rs):
pillar_score = max(0.0, 10.0 − weighted_mean(capped_finding_risks))
weighted_mean = Σ(risk × severity_weight) / Σ(severity_weight)
severity_weight = { critical: 4.0, high: 2.0, medium: 1.0, low: 0.5, info: 0.1 }
So a clean pillar scores 10.0; risk drags it down, weighted toward the higher-severity findings.
Empty-pillar semantics (Wave D / D1). A pillar with rules that ran but
found nothing is genuinely clean → 10.0. A pillar with no rules
evaluated (rules_evaluated == 0) is meaningless, so it serializes as
null, not 10.0, and is excluded from the overall (see §4). This prevents,
e.g., a Python CLI with no LLM code getting a falsely inflated overall just
because five inapplicable pillars defaulted to perfect.
4. Finding domains — what actually gates
Before scoring, each finding is assigned a domain (FindingDomain,
finding.rs) that decides whether it participates in the security gate and
KPIs:
| Domain | Gates --fail-on? | Description |
|---|---|---|
CodeVuln | Yes | Exploitable code vulnerability (SAST injection/crypto, hardcoded secret, API flaw, security-primary config/container rule). |
SupplyChain | Yes | Known-vulnerable dependency (SCA / CVE). |
ContainerHardening | No (advisory) | Missing USER/HEALTHCHECK, unpinned digest. Severity capped at Low. |
CiConfig | No (advisory) | CI/IaC/config hygiene; style toggles. Severity capped at Info. |
Coverage | No (advisory) | Repo-level test-coverage signal. Capped at Info. |
AiAdvisory | Never | AI-only finding with no deterministic backing; confidence hard-floored to Low, severity capped at Low. |
Only CodeVuln and SupplyChain are is_security_gating(). The advisory
domains are shown and scored into their pillars but never block a build, and
they carry a severity cap so a coverage percentage can never masquerade as a
security Critical.
Two demotions route findings out of the gating domains — both covered in
false-positives.md: the low-confidence-SAST advisory gate and provenance demotion. A None scan
type is fail-safe → CodeVuln (an untagged finding keeps gating, so nothing
silently stops blocking).
5. Overall score
The overall is the weighted mean across the pillars that were evaluated
(overall_score.rs):
overall_score = Σ_present(pillar_score × weight) / Σ_present(weight)
Canonical pillar weights (sum = 1.00, enforced by a compile-time assertion):
| Pillar | Weight | Pillar | Weight | |
|---|---|---|---|---|
| Security | 0.20 | Operational | 0.15 | |
| Compliance | 0.20 | FinOps | 0.10 | |
| Safety | 0.15 | Coherence | 0.10 | |
| Teamwork | 0.05 | |||
| Bias | 0.05 |
Renormalisation over present pillars means a scan that only exercised 3
pillars is scored fairly against those 3. When every pillar is evaluated,
Σ_present(weight) == 1.0 and this collapses to the plain weighted sum. When
no pillar is evaluated, overall_score = 0.0 and the verdict is FAIL —
the only safe default when there is no scoreable signal.
The wire output carries a per-pillar derivation array (weight, raw_score,
contribution) so an auditor can verify Σ(contribution) == overall_score
directly.
6. Verdicts: PASS / PARTIAL / FAIL
The overall score (and each pillar score) maps onto the shared verdict lexicon
(verdict.rs). Boundaries are inclusive on the lower end,
exclusive on the upper:
| Verdict | Score range |
|---|---|
| PASS | 9.0 – 10.0 |
| PARTIAL | 6.0 – 8.9 (i.e. [6.0, 9.0)) |
| FAIL | 0.0 – 5.9 (i.e. [0.0, 6.0)) |
A score of exactly 9.0 is PASS; exactly 6.0 is PARTIAL. A NaN score is
conservatively mapped to FAIL (fail-safe). The same lexicon is used
identically by Gadriel's runtime verdicts — there are no code-layer-specific
verdicts. Thresholds are tenant-configurable via verdict_pass_threshold /
verdict_partial_threshold.
7. Coverage status: GREEN vs DEGRADED
Independently of the verdict, a scan reports a coverage status describing whether the analysis was complete:
| Status | Meaning |
|---|---|
GREEN (Complete) | All applicable analysis ran. |
| DEGRADED | Some analysis could not run — the score reflects less than the full picture. |
DEGRADED is triggered when, for example:
- The OSV snapshot is absent, so CVE detection could not run — GVL rules requiring OSV are skipped and their findings are unavailable until you sync. ("OSV data not available — CVE detection is DEGRADED (not GREEN).")
- SAST coverage is missing for some languages (no code-level analysis available for a file type present in the repo).
DEGRADED is additive — it does not replace the verdict, it annotates it
(the report renders · coverage DEGRADED alongside the verdict, and a
[DEGRADED] marker). The point is honesty: a PASS on a DEGRADED scan means
"passed what we were able to check", and you can choose to treat that as a
failure with --fail-on-degraded.
8. Verdict → CLI exit code
The verdict drives the process exit code so CI can gate on it:
| Situation | Exit code |
|---|---|
| PASS (and clean gate) | 0 |
PARTIAL / FAIL with --fail-on (verdict-driven block) | 1 |
Tooling error (I/O, parse, crash; incl. shallow clone on --git-history, needs fetch-depth: 0) | 2 |
| License gate failed (missing/invalid/expired token) | 7 |
Key points:
--fail-on <severity>(none | info | low | medium | high | critical) sets the gate threshold. It is evaluated against each gating-domain finding's effective risk (severity × confidence — see false-positives.md §2), not its raw severity. This is why a Low-confidence SASTHighdoes not trip a--fail-on highgate.- A verdict-driven non-zero exit (PARTIAL/FAIL →
1) can be collapsed to0with the field-feedbackverdict_exit_zerobehavior — useful for advisory / non-blocking runs. Genuine tooling errors still propagate as exit2, so disabling the verdict gate never hides a crash. - Only
CodeVulnandSupplyChainfindings feed the gate; advisory-domain findings never produce a non-zero exit.
9. Worked example
A Python service scanned with --fail-on high:
Findings
| # | Rule | Severity | Confidence | Domain | Amplifiers |
|---|---|---|---|---|---|
| 1 | SQLi via request.args → cursor | Critical | High | CodeVuln | ExternalDataSource 1.1, NoHumanOversight 1.3 |
| 2 | md5 for hashing (bare ast_call) | Medium | Low | CiConfig (demoted, §4) | — |
| 3 | Transitive CVE in a dependency | High | High | SupplyChain | TransitiveDependency 0.8 (damper) |
Per-finding risk (illustrative base values):
- #1:
base 9.0 × 1.1 × 1.3 × 1.0 (conf High) = 12.87, under the 15.0 cap. A real, high-confidence, externally-reachable critical. - #2: Low-confidence bare-pattern SAST → demoted to
CiConfig(advisory) by the confidence-to-domain gate. It scores into the CI/config pillar as low risk and does not gate. - #3:
base 7.0 × 0.8 (transitive damper) × 1.0 = 5.6— the damper reflects that a transitive-only CVE is lower priority.
Pillars → overall. Security and Supply-chain pillars are dragged down by #1
and #3; unexercised pillars serialize null and drop out of the renormalised
overall. Suppose the renormalised overall lands at 6.8.
Verdict: 6.8 ∈ [6.0, 9.0) → PARTIAL.
Exit code: finding #1 is a gating CodeVuln with effective band Critical,
above the --fail-on high threshold → the scan exits 1. Finding #2 never
counted toward the gate. If OSV had been unavailable, #3 would not have been
detected at all and the report would read PARTIAL · coverage DEGRADED.
Cross-references
- false-positives.md — confidence tiers, effective risk, provenance, waivers, and the FP-prevention program.
- Ground truth in source:
crates/code-security/gadriel-scoring/src/(finding_score.rs,amplifier.rs,pillar_score.rs,overall_score.rs,verdict.rs),crates/code-security/gadriel-finding-types/src/finding.rs(FindingDomain,domain()),crates/gadriel-cli/src/commands/code/scan.rs(coverage status, exit codes).
