GCA · OUTPUTS & REPORTING

    Worked Examples

    End-to-end walkthroughs of what a real Gadriel Code run looks like.

    Worked Examples

    Audience: engineers who want to see, end-to-end, what a real Gadriel Code finding looks like — the code that triggers it, the findings.json entry it produces, and how to remediate or dismiss it.

    Every finding below is real output from gadriel code scan against a small demo repository, not a hand-written mock. The JSON is trimmed to the fields called out in Reports §Finding fields (id, rule_id, severity, confidence_tier, code_context, what_was_tested, remediation, plus the extensions.effective_risk Gadriel adds); the on-disk envelope carries more.

    The scan that produced these:

    PREFLIGHT_OFFLINE=1 gadriel code scan ./demo-shop --no-osv --osv-auto-sync no

    1. SAST — command injection via taint flow

    A tainted request parameter flows into os.system:

    # app/views.py
    @app.route("/ping")
    def ping():
    host = request.args.get("host") # source: remote (HTTP request)
    os.system("ping -c 1 " + host) # sink: os.system — no sanitizer
    return "ok"
    {
    "id": "CODE-W1-L3-017",
    "rule_id": "CODE-W1-L3-017",
    "severity": "critical",
    "confidence_tier": "medium",
    "scan_type": "sast",
    "what_was_tested": {
    "compliance_mapping": { "cwe": ["CWE-78"], "owasp_web": ["A03:2021"],
    "pci_dss": ["Requirement_6"], "soc2": ["CC8.1"] },
    "pattern_type": "ast_flow",
    "rule_name": "subprocess With shell=True And User-Controlled Argument",
    "description": "Tests whether user-controlled input reaches a subprocess invocation ..."
    },
    "code_context": {
    "file": "views.py", "line_start": 18, "line_end": 18,
    "ast_summary": "ast_flow",
    "excerpt": "os.system(\"ping -c 1 \" + host)",
    "attack_path": {
    "nodes": [
    { "node_id": "source", "symbol": "source (via tainted alias `host`)", "role": "source" },
    { "node_id": "sink", "line": 18, "symbol": "os.system", "role": "sink" }
    ],
    "edges": [ { "from": "source", "to": "sink", "kind": "data_flow" } ],
    "confidence": 0.9, "confirmed": true, "sanitizer_present": false
    }
    },
    "remediation": {
    "effort": "low",
    "steps": [
    "Pass the command + arguments as a list — `subprocess.run([\"echo\", user])` — and never set `shell=True`.",
    "When a shell IS required, build the command via `shlex.quote` for every variable part.",
    "Validate every argument against an allow-list before passing it."
    ],
    "code_example": "# BAD\nsubprocess.run(f\"echo {user}\", shell=True)\n# GOOD\nsubprocess.run([\"echo\", user])\n"
    },
    "extensions": { "domain": "code_vuln",
    "effective_risk": { "band": "high", "score": 5.23, "unverified": false } }
    }

    Why it fires and how to fix it. This is an ast_flow finding — the engine traced the tainted alias host from the HTTP source to the os.system sink with no sanitizer between them (sanitizer_present: false), and an attack-graph simulation confirmed the path (SimulationConfirmed 1.8× amplifier drove the uncapped risk over the 15.0 cap). Remediate by passing the command as an argv list so each value stays one shell token: subprocess.run(["ping", "-c", "1", host]). Note the critical severity lands at effective band High because confidence is Medium — real, but pattern-confirmed rather than proven-exploitable.


    2. SCA — a known-vulnerable dependency (OSV)

    # pyproject.toml
    jinja2 = "3.1.2"

    SCA CVE detection needs the OSV snapshot; the --no-osv demo run above skips it and reports coverage DEGRADED. The finding below is a real OSV-confirmed entry from a benchmarked scan (with OSV synced).

    {
    "id": "CODE-W1-SCA-001",
    "rule_id": "SCA-CVE-001",
    "severity": "medium",
    "confidence_tier": "high",
    "scan_type": "sca",
    "what_was_tested": {
    "ecosystem": "PyPI",
    "method": "osv_query", "scanner": "gadriel-scanners-sca"
    },
    "what_we_analyzed": {
    "package_name": "jinja2", "package_version": "3.1.2",
    "dep_type": "direct", "file": "pyproject.toml"
    },
    "what_we_measured": [
    { "metric_id": "advisory_id", "value": "CVE-2024-56201" },
    { "metric_id": "transitive", "value": false },
    { "metric_id": "reachability", "value": "unknown" }
    ],
    "code_context": {
    "file": "pyproject.toml", "line_start": 1, "line_end": 1, "excerpt": "jinja2@3.1.2"
    },
    "remediation": {
    "effort": "medium",
    "steps": [
    "Upgrade jinja2 above the affected version range.",
    "Re-run `gadriel code scan` to confirm the advisory is resolved.",
    "If no fix is available, document an exception in `.gadriel-waivers.toml`."
    ]
    }
    }

    Why it fires and how to fix it. The resolved dependency jinja2@3.1.2 matched OSV advisory CVE-2024-56201 (a Jinja sandbox breakout). SCA findings are High confidence because a package+version hit against the curated OSV feed is unambiguous. Remediate by upgrading past the affected range (jinja2 >= 3.1.5) and re-scanning. Had the vulnerable function been proven reachable, a DependencyReachable 1.6× amplifier would have raised its risk; reachability: unknown here neither amplifies nor suppresses it.


    3. Secrets — a hardcoded Stripe live key

    # app/settings.py
    STRIPE_KEY = "sk_live_51H8xACME1234567890abcdefghijklmnZZ"
    {
    "id": "CODE-W1-SECRET-039",
    "rule_id": "CODE-W1-SECRET-039",
    "severity": "critical",
    "confidence_tier": "low",
    "scan_type": "secrets",
    "pillar": ["security", "compliance"],
    "what_was_tested": {
    "compliance_mapping": { "cwe": ["CWE-798"], "owasp_web": ["A07:2021"],
    "pci_dss": ["Requirement_8"], "hipaa": ["164.312(d)"] },
    "pattern_type": "regex_pattern",
    "rule_name": "Hardcoded Stripe Live API Key (sk_live_ / pk_live_ / rk_live_)"
    },
    "code_context": {
    "file": "app/settings.py", "line_start": 2, "line_end": 2,
    "excerpt": "STRIPE_KEY = \"[REDACTED]\"", "redacted": true
    },
    "remediation": {
    "effort": "medium",
    "steps": [
    "Roll the key in the Stripe dashboard immediately; the leaked value is presumed compromised.",
    "Audit the dashboard's API request logs for unfamiliar IPs during the leak window.",
    "Move to restricted keys with the minimum capability set; secrets should live in a secrets manager."
    ],
    "code_example": "# BAD\nSTRIPE_KEY = \"sk_live_51Hx...\"\n# GOOD\nSTRIPE_KEY = os.environ[\"STRIPE_SECRET_KEY\"]\n"
    },
    "extensions": { "domain": "code_vuln",
    "effective_risk": { "band": "low", "score": 2.38, "unverified": true } }
    }

    Why it fires and how to fix it. The sk_live_ pattern matched a Stripe live-key shape. Note the value is [REDACTED] on disk — secret values never enter findings.json. Confidence is Low because this rule has no Shannon-entropy gate, so the critical severity de-escalates to effective band Low and the finding is stamped unverified — "review, not panic." It still gates as a Secrets finding (the Low-confidence advisory demotion applies to SAST only). Remediate by rolling the key and reading it from the environment or a secrets manager.


    4. Config — over-permissioned GitHub Actions token

    # .github/workflows/ci.yml
    permissions: write-all
    {
    "id": "CODE-W1-CONFIG-001",
    "rule_id": "CONFIG-GHA-002",
    "severity": "high",
    "confidence_tier": "high",
    "scan_type": "config",
    "what_was_tested": {
    "sub_category": "cicd_github_actions",
    "title": "GitHub Actions workflow grants `permissions: write-all` to GITHUB_TOKEN — overpermissioned."
    },
    "code_context": {
    "file": ".github/workflows/ci.yml", "line_start": 3, "line_end": 3,
    "excerpt": "permissions: write-all"
    },
    "remediation": {
    "effort": "low",
    "steps": [
    "Replace `permissions: write-all` with the minimum permissions each job needs (e.g. `contents: read`).",
    "GitHub's docs: https://docs.github.com/actions/using-jobs/assigning-permissions-to-jobs"
    ]
    },
    "extensions": { "domain": "code_vuln",
    "effective_risk": { "band": "high", "score": 7.5, "unverified": false } }
    }

    Why it fires and how to fix it. write-all grants the workflow's GITHUB_TOKEN full write scope across the repo — a supply-chain risk if any step is compromised. Remediate by scoping permissions to the minimum each job needs (contents: read, and only the specific write scopes required).


    5. Container — Dockerfile runs as root

    # Dockerfile
    FROM python:latest
    COPY . /app
    CMD ["python", "app/views.py"]
    {
    "id": "CODE-W1-CONTAINER-001",
    "rule_id": "CODE-W1-CONTAINER-001",
    "severity": "high",
    "confidence_tier": "high",
    "scan_type": "container",
    "what_was_tested": {
    "check_id": "user_running_as_root",
    "description": "Container runs as root — no USER directive or USER resolves to root/0."
    },
    "code_context": {
    "file": "Dockerfile", "line_start": 1, "line_end": 1,
    "ast_summary": "dockerfile:user_running_as_root",
    "excerpt": "Dockerfile declares no USER directive; the container will run as root."
    },
    "remediation": {
    "effort": "low",
    "steps": [
    "Add a non-root USER directive before the final CMD/ENTRYPOINT (e.g. `USER 65532:65532`).",
    "If setup needs root, drop privilege with USER as the LAST instruction.",
    "Verify with `docker run --rm <image> id` that the effective uid is non-zero."
    ]
    },
    "extensions": { "domain": "code_vuln",
    "effective_risk": { "band": "high", "score": 7.5, "unverified": false } }
    }

    Why it fires and how to fix it. No USER directive means the container runs as root, widening the blast radius of any escape. Remediate by adding a non-root USER before the final CMD. (The same Dockerfile also raises advisory container-hardening findings — unpinned python:latest, missing HEALTHCHECK — which are shown but do not gate; see Scoring & Verdicts §4.)


    6. API — endpoint declares no authentication

    # openapi.yaml
    paths:
    /admin/users:
    get:
    summary: List all users
    responses:
    '200': { description: OK }
    {
    "id": "CODE-W1-API-001",
    "rule_id": "CODE-W1-API-001",
    "severity": "critical",
    "confidence_tier": "high",
    "scan_type": "api",
    "what_was_tested": {
    "check_id": "missing_auth", "rule_class": "openapi_static_check",
    "description": "Endpoint declares no `security:` block AND no global security is set; sensitive paths require auth."
    },
    "code_context": {
    "file": "openapi.yaml",
    "ast_summary": "openapi:missing_auth GET /admin/users",
    "excerpt": "GET /admin/users declares no `security:` block and no global security is set"
    },
    "remediation": {
    "effort": "low",
    "steps": [
    "Add a `security:` block to the operation OR a global `security:` block to the spec.",
    "Define the chosen scheme in `components.securitySchemes` (e.g. `bearerAuth`, `apiKey`)."
    ]
    },
    "extensions": { "domain": "code_vuln",
    "effective_risk": { "band": "critical", "score": 9.5, "unverified": false } }
    }

    Why it fires and how to fix it. The sensitive /admin/users path exposes a GET with no operation-level or global security:. Remediate by adding a security: requirement and defining the scheme under components.securitySchemes. Auth analysis here is presence-based — it confirms an auth declaration exists, not that the authorization logic is correct (no BOLA/BFLA analysis).


    The scan verdict line

    Every scan ends with a one-line verdict. For the demo repo above:

    verdict:  PARTIAL · coverage DEGRADED (overall 6.64/10.0)
    
    • PARTIAL — the overall 6.64 falls in [6.0, 9.0).
    • coverage DEGRADED — SCA ran without an OSV snapshot (--no-osv), so CVE detection was skipped; the score reflects less than the full picture.

    The one-line summary that precedes it:

    ✓ scan complete — 20 finding(s)
      security risk (14): 3 critical, 3 high, 2 medium, 5 low, 1 info
      advisory (6): container / CI / coverage hardening — informational, does not gate
      ⚠ 2 unverified high/critical-severity finding(s) at low confidence — review, not panic
      scan types: api: 5, config: 2, container: 10, sast: 1, secrets: 2
    

    The findings table (gadriel code findings)

    gadriel code findings renders .security/findings.json as a table, sorted by effective risk. An excerpt:

    ┌───────────────────────┬───────────────────────┬──────────┬──────┬───────────┬──────────────────────────┬──────┐
    │ ID                    ┆ Risk                  ┆ Severity ┆ Tier ┆ Type      ┆ File                     ┆ Line │
    ╞═══════════════════════╪═══════════════════════╪══════════╪══════╪═══════════╪══════════════════════════╪══════╡
    │ CODE-W1-API-001       ┆ critical (9.5)        ┆ critical ┆ HIGH ┆ api       ┆ openapi.yaml             ┆ -    │
    │ CODE-W1-CONTAINER-070 ┆ critical (9.5)        ┆ critical ┆ HIGH ┆ container ┆ Dockerfile               ┆ 1    │
    │ CODE-W1-CONFIG-001    ┆ high (7.5)            ┆ high     ┆ HIGH ┆ config    ┆ .github/workflows/ci.yml ┆ 3    │
    │ CODE-W1-CONTAINER-001 ┆ high (7.5)            ┆ high     ┆ HIGH ┆ container ┆ Dockerfile               ┆ 1    │
    │ CODE-W1-SECRET-039    ┆ low (2.4) ⚠UNVERIFIED ┆ critical ┆ LOW  ┆ secrets   ┆ app/settings.py          ┆ 2    │
    │ CODE-W3-L4-118        ┆ low (1.9) ⚠UNVERIFIED ┆ high     ┆ LOW  ┆ sast      ┆ app/settings.py          ┆ 1    │
    └───────────────────────┴───────────────────────┴──────────┴──────┴───────────┴──────────────────────────┴──────┘
    

    The Risk column is effective risk (severity × confidence), which is why the critical-severity CODE-W1-SECRET-039 sorts low and carries ⚠UNVERIFIED — triage on this column, not raw severity. Use --format json for a machine-readable dump.


    Dismissing a false positive

    Say CODE-W1-CONFIG-002 (an info-level actions/checkout pin note) is a false positive in your setup. Record it:

    gadriel code fix CODE-W1-CONFIG-002 --false-positive \
    --reason "info-only: pinned in a reusable workflow we control"
    ✓ durable waiver written to .gadriel-waivers.toml (segregated on future scans)
    
    ● recorded false-positive dismissal for CODE-W1-CONFIG-002
      • pattern:     CONFIG-GHA-005
      • destination: other
      • scope:       .github/workflows/ci.yml
      • posterior:   Beta(3, 3) → P(real) = 0.5000 (soft signal)
      • disposition: durable waiver in .gadriel-waivers.toml (segregated on future scans)
    
    ✓ audit log: .security/audit-log.jsonl
    

    This is a zero-LLM, deterministic path. It writes a durable waiver, nudges the per-repo Bayesian posterior, and appends an audit event. The resulting .gadriel-waivers.toml entry (committed with your code):

    # Gadriel durable false-positive waivers.
    # Each [[waiver]] suppresses or segregates a finding regardless of
    # re-detection count. Commit this file. See `gadriel code fix --help`.
    [[waiver]]
    rule_id = "CODE-W1-CONFIG-002"
    path = ".github/workflows/ci.yml"
    line = 8
    reason = "info-only: pinned in a reusable workflow we control"
    mode = "segregate"

    On the next scan the finding is segregated — kept in a separate "Waived (FP)" section, excluded from the gate and KPIs. See False Positives §4–5 for the full triage workflow, --confirm-real, and waiver expiry.


    The report bundle

    gadriel code report renders a self-contained, offline HTML bundle (plus a one-page executive PDF) into .security/. Contents after the demo scan:

    .security/
    ├── findings.json              # canonical OCSF envelope (source of truth)
    ├── pillar-scores.json         # per-pillar scores
    ├── metrics.json               # scan metrics
    ├── sbom.spdx.json             # SPDX 2.3 SBOM
    ├── sbom.cyclonedx.json        # CycloneDX 1.5 SBOM
    ├── audit-log.jsonl            # append-only reasoning/audit log (NDJSON)
    ├── reports/
    │   ├── index.html             # single-page report: Home / Findings / Trends / Compliance / Drift / Audit
    │   ├── findings.html
    │   ├── compliance.html
    │   ├── drift.html
    │   ├── trends.html
    │   ├── audit.html
    │   └── report.json            # full data dump backing the pages
    ├── compliance/                # per-framework reports (gadriel code report --compliance …)
    ├── store/                     # RVF operational data store (*.rvf) — trends, drift, audit search
    └── history/                   # dated findings/scores/metrics snapshots
    

    The bundle makes no network requests — fonts, CSS, data, and charts are all inlined or vendored. Full detail in Reports & Outputs.


    Cross-references