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:
bashPREFLIGHT_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:
python# 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 sanitizerreturn "ok"
jsonc{"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)
toml# pyproject.tomljinja2 = "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).
jsonc{"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
python# app/settings.pySTRIPE_KEY = "sk_live_51H8xACME1234567890abcdefghijklmnZZ"
jsonc{"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
yaml# .github/workflows/ci.ymlpermissions: write-all
jsonc{"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# DockerfileFROM python:latestCOPY . /appCMD ["python", "app/views.py"]
jsonc{"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
yaml# openapi.yamlpaths:/admin/users:get:summary: List all usersresponses:'200': { description: OK }
jsonc{"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.64falls 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:
bashgadriel 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):
toml# 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 = 8reason = "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
- Scan Coverage — the six scan types in depth.
- Reports & Outputs — the
findings.jsonschema and.security/layout. - Scoring & Verdicts — how these findings roll up to the verdict.
- False Positives — confidence, effective risk, waivers.
- Glossary — definitions of every term above.
