GCA · CORE CONCEPTS

    Scoring & Verdicts

    How severity, exploitability, and verdicts are computed for every finding.

    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)
    
    • base is the rule's severity-derived base score.
    • Amplifiers are context multipliers drawn from the AST, scope, and the 3D codebase graph.
    • confidence is the [0, 1] confidence damper; 1.0 is 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
    AgentHasToolAccess1.4Code path can reach a side-effecting tool/function.
    OutputUnvalidated1.2Output consumed without validation.
    ExternalDataSource1.1Inbound data originates externally.
    NoHumanOversight1.3Executes with no human-in-the-loop gate.
    ProductionConfigDetected1.5A production marker is present in adjacent config.
    MultiAgentBoundary1.2Path crosses an agent-to-agent boundary.
    RecursiveAgentLoop1.6Implicated function is reachable from itself.

    Graph (queried from the 3D codebase graph)

    Amplifier×Meaning
    HighCentrality1.5Node is top-N for a centrality metric.
    SimulationConfirmed1.8Attack-graph simulation reaches a high-impact sink.
    SupplyChainPath1.3Path exists from an external node within max_hops.
    CycleMember1.4Node is in a strongly-connected component ≥ min_size.
    DriftDegrading1.2Component shows a degrading risk-score trend.
    ShortestPathCritical1.5Shortest path from an external entry is below the criticality threshold.
    ProvenPathToAsset2.0A 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
    DependencyReachable1.6Vulnerable dependency function confirmed reachable.
    DependencyAbandoned1.3No maintainer activity in 12+ months.
    TransitiveDependency0.8 (damper)Vulnerability is in a transitive dependency.
    LicenseCommercialConflict1.0Compliance-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:

    DomainGates --fail-on?Description
    CodeVulnYesExploitable code vulnerability (SAST injection/crypto, hardcoded secret, API flaw, security-primary config/container rule).
    SupplyChainYesKnown-vulnerable dependency (SCA / CVE).
    ContainerHardeningNo (advisory)Missing USER/HEALTHCHECK, unpinned digest. Severity capped at Low.
    CiConfigNo (advisory)CI/IaC/config hygiene; style toggles. Severity capped at Info.
    CoverageNo (advisory)Repo-level test-coverage signal. Capped at Info.
    AiAdvisoryNeverAI-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):

    PillarWeightPillarWeight
    Security0.20Operational0.15
    Compliance0.20FinOps0.10
    Safety0.15Coherence0.10
    Teamwork0.05
    Bias0.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:

    VerdictScore range
    PASS9.0 – 10.0
    PARTIAL6.0 – 8.9 (i.e. [6.0, 9.0))
    FAIL0.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:

    StatusMeaning
    GREEN (Complete)All applicable analysis ran.
    DEGRADEDSome 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:

    SituationExit 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 SAST High does not trip a --fail-on high gate.
    • A verdict-driven non-zero exit (PARTIAL/FAIL → 1) can be collapsed to 0 with the field-feedback verdict_exit_zero behavior — useful for advisory / non-blocking runs. Genuine tooling errors still propagate as exit 2, so disabling the verdict gate never hides a crash.
    • Only CodeVuln and SupplyChain findings feed the gate; advisory-domain findings never produce a non-zero exit.

    9. Worked example

    A Python service scanned with --fail-on high:

    Findings

    #RuleSeverityConfidenceDomainAmplifiers
    1SQLi via request.args → cursorCriticalHighCodeVulnExternalDataSource 1.1, NoHumanOversight 1.3
    2md5 for hashing (bare ast_call)MediumLowCiConfig (demoted, §4)
    3Transitive CVE in a dependencyHighHighSupplyChainTransitiveDependency 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).