Skip to content
For the complete documentation index, see llms.txt. This page is also available as Markdown.

Security Autofix

Prove which scanner findings are reachable, remediate only the eligible ones, and open a draft PR only when every safety condition holds.

Security Autofix ingests a SAST/SCA scanner's SARIF, proves reachability honestly, remediates only the findings that are genuinely fixable, and opens a draft pull request only when every safety condition holds — never on a guess. Use it when a scanner has produced hundreds of findings and you need to know which ones are real, and to auto-remediate the provable ones with an evidence-backed PR.

The command is codna secure. It runs in two tiers:

Tier
What it does
LLM tokens
Writes anything?

Tier-1

Classify reachability + autofix-eligibility (read-only)

Zero

No

Tier-2

Remediate eligible findings: fix, verify, re-prove closure

Yes (patch-gen)

Only a draft PR, and only past the gate

Start with Tier-1 — it is the fast, free wedge and the only step most findings ever need:

codna secure . --from-sarif results.sarif

The end-to-end loop

Security Autofix is a single closed loop. Each step feeds the next; a failure at any step stops the loop and is named in the report. The work spans three privilege domains — the read-only engine classifies and re-proves closure, the sandboxed worker runs untrusted repo code with no write token, and the writer holds a scoped token and opens the PR but runs no repo code.

1

Ingest SARIF

Codna reads the scanner's SARIF (CodeQL / Semgrep / Snyk / Trivy), validates its provenance (G1), and pins the snapshot. Only SARIF 2.1.0 is accepted, and the run must record a full 40-hex commit and a driver name + version.

2

Classify reachability

Every finding gets exactly one of four verdicts — exploitable, production-reachable, unreachable, or unknown. This is read-only and spends zero LLM tokens. "No path found" is reported as unknown under any incomplete envelope, never as unreachable.

3

Remediate eligible

Tier-2, opt-in. Only autofix-eligible findings are touched (eligibility is decided by policy gate G3). For each one, a patch is generated in an isolated ephemeral worktree pinned to the scan commit.

4

Verify build / tests

The patch is integrity-checked (G4) before it is ever applied — a rejected patch is never applied. Then it is applied and the manifest's build + test commands run; a regression blocks the finding (G5).

5

Re-prove closure

The original security obligation is independently re-proven closed (G7). Closure is a separate judgment from reachability: the sink can stay production-reachable and the obligation still be closed once a recognized barrier is added.

6

PR gate

A draft PR opens only on the logical-AND of nine conditions (G1…G9), through the privilege-separated writer with a narrowly-scoped token. A single failure blocks the PR and is named in the report.

Command reference

codna secure [repo] --from-sarif <results.sarif> [options]

Flag
Default
Purpose

repo (positional)

.

Local path or git URL of the repository under analysis

--from-sarif PATH

— (required)

Scanner SARIF output (CodeQL / Semgrep / Snyk / Trivy)

--ref REF

repo default

Branch, tag, or commit to snapshot

--engine {local,remote}

local

Reachability engine. local is the bounded in-process classifier and never claims exploitable; remote sends classification to the Codna engine over HTTP (the local runtime, or CODNA_ENGINE_URL)

--verification PATH

—

codna-security.yaml manifest; required for --open-pr and for remote --fix

--fix

off

Remediate eligible findings; opens no PR. The default --engine local applies the verified patch to your checkout (re-verified there); --engine remote without --open-pr is report-only

--open-pr

off

Open a fix PR per eligible finding (requires --engine remote and a resolved manifest)

--github-token TOK

$CODNA_GITHUB_TOKEN/$GITHUB_TOKEN

Write token for --open-pr

--base-branch NAME

main

PR base branch

--repo-slug owner/repo

derived from git remote

Target repo for --open-pr

Relevant environment variables:

Variable
Used for

CODNA_API_KEY

Your Codna key. Read only when CODNA_ENGINE_URL points --engine remote at a remote engine; the local runtime does not use it

OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY

Provider key for model-backed remediation

CODNA_GITHUB_TOKEN / GITHUB_TOKEN

Write token. secure --open-pr accepts either; secure-open-pr reads GITHUB_TOKEN only

CODNA_ATTESTATION_KEY

HMAC key shared between worker and writer (required for the two-job handoff)

CODNA_FIX_MODEL

Patch-generation model for --engine local --fix

CODNA_MODEL_PACK_DIGEST

Model-pack digest pinned into security provenance

Tier-1: honest reachability proof

Codna ingests the scanner's SARIF, validates its provenance, and assigns every finding exactly one of four verdicts. This classification is read-only and spends zero LLM tokens — no patch generation, no build, no scanner rerun.

Verdict
Meaning
Autofix-eligible?

exploitable

An independent source-to-sink taint path is proven

Yes (policy default)

production-reachable

The vulnerable operation is callable in production; taint not independently proven

Only with an explicit policy override

unreachable

No path, plus a complete, sound analysis envelope

Never

unknown

No path under an incomplete or unsupported envelope

Never

This output is from --engine remote. The default --engine local prints no banner and never claims exploitable — its strongest verdict is production-reachable — so a local run shows [production-reachable] in place of the [exploitable] row.

The leading → marks autofix-eligible findings; · marks the rest. The bracketed value is the reachability verdict; the parenthesized value is the finding kind (taint, sca, control_flow, config, secret, or unsupported).

Why an envelope can be incomplete

Reachability is only as trustworthy as the analysis that produced it. Codna keys completeness on (language, framework, build_mode) and falls back from most to least specific. Reflection-heavy stacks deliberately get a bounded envelope, because reflection makes "absence of a path" unprovable:

Stack
Envelope
Strongest verdict it can assert

python, javascript, typescript, go, java

complete

unreachable / production-reachable

java + spring

bounded

production-reachable (never unreachable)

ruby, php

bounded

production-reachable

any other language

unsupported

unknown only

A finding under a bounded envelope with no proven path is reported unknown (bounded envelope: production-reachability not independently provable), never unreachable. An off-matrix language yields unknown with proof type off-matrix (<language>).

Choosing the reachability engine

The same command runs against either engine. Pick the one that matches your trust and connectivity constraints:

The Codna engine over HTTP. It is the only path that can earn the exploitable verdict, and the only path that can drive --open-pr through the privilege-separated writer or write an evidence bundle with --evidence-dir.

Without further configuration, --engine remote talks to the local runtime Codna starts on your machine. Set CODNA_ENGINE_URL and CODNA_API_KEY to send classification to a remote Codna engine instead.

The bounded local reference engine — air-gapped, no external engine and no SAST scanner image. Its strongest verdict is production-reachable; it never claims exploitable. It can run a local fix loop that applies the verified patch to your checkout and re-runs verification there, but opens no PR — passing --open-pr with --engine local is refused.

For the local fix loop, the patch comes from the Codna agent with the model named in CODNA_FIX_MODEL.

Test-only sinks (paths matching tests/, spec/, __tests__/, or *_test.* / *.test.* / *_spec.*) are reported unreachable under a complete envelope (test-only sink; no production entrypoint), or unknown under a bounded one. Artifacts that escape the repository path are quarantined and never analyzed or fixed (they show as [quarantined] … artifact path escape).

Tier-2: remediation

Tier-2 is opt-in. It only ever touches autofix-eligible findings; ineligible findings are reported and never remediated (no patch is generated for them). Eligibility is decided by policy (gate G3) — by default only exploitable is eligible; production-reachable requires an explicit policy override (see the policy section below).

Local fix loop

codna secure --fix --engine local runs a local detect → fix → verify → re-prove-closure loop with no external engine and no SAST scanner image. Reachability and closure both come from the bounded local reference engine; the patch comes from the Codna agent (model from CODNA_FIX_MODEL).

For each eligible finding, in an isolated ephemeral worktree pinned to the scan commit:

  1. Detect — re-classify reachability with the bounded local reference engine.

  2. Fix — generate a patch with the Codna agent.

  3. Integrity — run the anti-evasion check (G4) before the patch is ever applied; a rejected patch is never applied.

  4. Verify — apply the patch and run the manifest's build + test commands.

  5. Re-prove closure — independently re-prove the original obligation is closed.

A finding is reported REMEDIATED only when the patch applied cleanly, passed integrity, did not regress the tests, and the obligation re-proves closed:

Other per-finding statuses you may see: blocked: patch integrity (<reason>), blocked: tests failed (closure=<status>), and no tests configured (printed instead of "tests pass" when the manifest carries no test commands, since tests_passed is then None).

--engine local --fix opens no PR: it proves the fix in an isolated worktree pinned to the scan commit, then applies the verified diff to your local checkout with git apply and re-runs verification there. Opening a PR requires the privilege-separated writer and a scoped token; passing --open-pr together with --engine local is refused.

Closure is a separate judgment from reachability

This is a deliberate, load-bearing distinction.

The local engine's closure proof is a bounded heuristic:

  • The sink file must be in the patch's changed paths. If untouched, closure is open.

  • A recognized barrier must appear in the added lines: a parameterized / bound-parameter query (.execute(<sql>, (params))), shlex.quote, html.escape, bleach.clean, a prepared statement / placeholder / bindparam, or an escape/sanitize call.

  • Sink touched but no recognized barrier added → conservatively open (e.g. the sink merely moved).

  • Empty diff or no primary sink location → unknown.

The PR-open gate

A draft PR opens only on the logical-AND of nine conditions (G1…G9). A single failure blocks the PR, and the failing condition is named in the report. The gate is pure logic — it composes already-computed verdicts and never executes anything itself.

Expand each gate for the exact condition and what blocks the PR:

G1 — Immutable provenance

Blocks the PR when provenance is incomplete or any digest is unbound (incomplete/unbound provenance).

G2 — Baseline reproduction

Blocks the PR when the finding cannot be reproduced on the exact unpatched snapshot.

G3 — Policy-eligible proof

Blocks the PR when the verdict is unreachable / unknown, or eligibility is not granted by policy.

G4 — Patch integrity (anti-evasion)

Blocks the PR when the patch weakens verification or escapes the permitted change scope. This check runs before the patch is ever applied.

G5 — Regression safety

Blocks the PR when the build fails, tests fail, or the risk-simulation verdict is below the threshold.

G6 — Scanner confirmation

Blocks the PR when the scanner still reports the finding, or the patched scan is degraded versus baseline.

G7 — Independent closure proof

Blocks the PR when closure_status is not closed.

G8 — No alternate or new vulnerability

Blocks the PR when an equivalent alternate path remains, or the patch adds a new policy-blocking finding.

G9 — Attested handoff, base unchanged

Blocks the PR when the signed attestation fails verification, or the base moved before open.

Without --open-pr, a finding that passes every gate prints would open (report-only) instead of opening anything.

G6 and G7 are different checks. G6 is about the scan — the originating scanner confirms the finding is gone under an identical, non-degraded configuration (no quietly-dropped rules, files, or queries). G7 is about the obligation — an independent closure proof, satisfied by a sanitizer even while the operation stays production-reachable. Both must pass independently.

Privilege separation behind the gate

No single step holds the proof, the sandbox, and the GitHub token at once. The work is split across three privilege-separated roles:

  • Engine — independent reachability and closure proofs (the authority).

  • Worker — runs untrusted code (baseline scan, patch-gen, build/tests, scanner rerun) in a sandbox with no write token, and emits a signed attestation.

  • Writer — cannot execute repository code; it independently re-verifies the attestation, the patch digest, and that the base is unchanged, then opens the draft PR with a narrowly-scoped token.

There is exactly one verified finding per PR.

End-to-end: open a fix PR

--open-pr requires --engine remote. It and remote --fix require a --verification codna-security.yaml manifest pinning the scanner configuration and the build/test commands. For --open-pr the manifest must additionally be resolved (see the manifest section). The repo argument must be a local checkout — remediation reads the working tree and pins to the SARIF's commit.

Two-job CI handoff

So that the write token never lives in a step that runs repository code, the worker and writer can be split across two jobs. The worker emits a signed evidence bundle (attestation.json + patch.diff + finding.json); a separate writer-domain command verifies it and opens the PR:

The writer verifies the attestation signature against the shared CODNA_ATTESTATION_KEY and re-checks digest_of(diff) == patch_digest before it will open anything. If verification fails, no PR is opened.

Expected result

Tier-1 always returns a per-finding verdict and never writes code. Tier-2 writes only after the finding is policy-eligible, the patch passes integrity checks, verification commands pass, closure is independently re-proven, and the writer verifies the signed evidence bundle. If any gate fails, Codna reports the blocking gate and does not open a PR.

The verification manifest (codna-security.yaml)

The manifest is the reproducible, digest-bound plan that replaces raw shell flags. The whole document is bound by one digest, so the baseline and patched phases provably run the same configuration — change any field and the digest flips and the gate aborts (MANIFEST-TAMPER). It is loaded with PyYAML if installed, else parsed as JSON.

scanner.id, scanner.command, and scanner.output are required; everything else has a default. --open-pr additionally requires a resolved manifest, enforced before any work begins:

  • scanner.image pinned by @sha256:<64-hex>,

  • scanner.rules_digest present,

  • sandbox.network is deny or none,

  • at least one verification.tests command.

Autofix policy

The policy block encodes gate G3 (which verdicts may receive a fix PR), the severities that block a PR when newly introduced (G8), and scanner-severity overrides. It carries a deterministic digest so it is provenance-pinned (G1).

Field
Default
Meaning

autofix_classifications

["exploitable"]

Verdicts that may be auto-fixed. Add "production-reachable" to opt in (the operator's explicit override). unreachable and unknown are never eligible regardless.

block_new_severities

["high", "critical"]

New-finding severities that block a PR under G8

severity_overrides

{}

Per-rule severity remapping applied during SARIF ingest

With the default policy, a production-reachable finding reports production-reachable requires an explicit policy override and is not remediated. After the opt-in above it reports production-reachable allowed by explicit policy override and becomes eligible.

Troubleshooting

codna secure needs --from-sarif <results.sarif>.

--from-sarif is required. Point it at your scanner's SARIF output.

--open-pr needs --verification <codna-security.yaml> …

--open-pr requires a manifest. Supply --verification.

--fix needs --verification … or use --engine local …

Remote --fix needs a manifest; for a scanner-less local fix, add --engine local.

SARIF provenance incomplete: …

Provenance (G1) failed. Common items: no versionControlProvenance revisionId, revisionId … is not a full 40-hex commit, driver name missing, driver version missing. Re-run the scanner so it records the full commit and driver. Only SARIF 2.1.0 is accepted.

manifest not resolved for --open-pr: …

The manifest is not fully pinned. Fix the listed items: pin scanner.image by @sha256, set scanner.rules_digest, set sandbox.network: deny, and add a verification.tests command.

cannot parse manifest …: PyYAML not installed and content is not JSON

Either install PyYAML or provide the manifest as JSON.

--open-pr needs the privilege-separated writer + a scoped token; `--engine local --fix` applies to the local checkout but opens no PR.

The local remediation lane applies the verified patch to your checkout but cannot open PRs. Use the privilege-separated writer flow for --open-pr.

--open-pr needs a write token: --github-token or $GITHUB_TOKEN.

Provide a write-scoped token via --github-token or GITHUB_TOKEN.

remediation needs a local repo checkout (pass a local path, not a URL).

--fix / --open-pr read the working tree. Pass a local path, not a git URL.

could not derive owner/repo from the git remote; pass --repo-slug owner/repo.

The origin remote is not a recognizable GitHub URL. Pass --repo-slug owner/repo.

set CODNA_ATTESTATION_KEY (shared with the worker) …

secure-open-pr needs the same HMAC key the worker signed with. Export CODNA_ATTESTATION_KEY in both jobs.

writer refused to open the PR: …

The writer's independent re-verification failed (attestation signature or digest_of(diff) == patch_digest). The PR is intentionally not opened.

For the bug-fix workflow (failing tests, --from-junit, --apply) see the CLI Reference; for repository understanding and context reduction see How It Works; for running Codna inside Cursor or Claude Desktop see MCP Server.

Next steps

  • CLI Reference — every codna secure and codna secure-open-pr flag.

  • GitHub Action — run secure mode in CI with the packaged action.

  • MCP Server — expose read-only secure triage to Cursor or Claude Desktop.

Last updated

Was this helpful?