Skip to main content

secure-code-agent

Deterministic security gate + bounded AI remediation prompts for repos with AI coding agents in the loop. Anchored to NIST SSDF · OWASP ASVS · OWASP Top 10 · MITRE CWE Top 25 · OpenSSF Scorecard · SARIF 2.1.0.

pip install 'secure-code-agent[required-scanners]'

secure-code-agent --fail-on-gate \
    --output secure-code-report.md \
    --prompt-output secure-code-remediation-prompt.md \
    --sarif-output secure-code.sarif

The sibling of maintainability-agent. Same shape: deterministic CI gate · plain-file outputs · per-host skill bundle. Different concern: security, not maintainability.


Try it in one command

pip install 'secure-code-agent[python-scanners]'
secure-code-agent --demo

--demo generates a small application carrying seven real defects — SQL injection, command injection, unsafe deserialization, a weak hash, dynamic evaluation — audits it, and writes the work order you would hand an agent. The tree is generated at runtime into a temp directory, so installing this tool never puts vulnerable source on your disk.

To audit something real:

secure-code-agent /path/to/repo

Any scanner that is missing is named, with the command to install it. This tool never installs anything itself — naming a command is not running one — and it will not grade what it could not examine. --preflight checks the whole floor before a run.

A pure-Python repository requires six of the ten floor scanners; the Ruby, JavaScript and container tools are not applicable rather than missing, so a single-language project is never permanently incomplete.

In your editor or an agent, not just a terminal

pip install 'secure-code-agent[mcp]'
secure-code-agent-mcp

Exposes audit_repository, preflight and agent_info. It never audits unasked — audit_repository without action="run" returns the question — and it hands back the work order first, because the score is second class.

Auditing a pull request

secure-code-agent . --changed-only origin/main

The whole tree is still scanned; the report is scoped to what changed. No grade is issued for a scoped run, because a run that looks at less must not score better.

Proving the agent stayed inside the order

secure-code-agent . --verify-against before.json

Reports what was fixed, what is still open, what was silenced rather than fixed — and the blast radius: which changed files the work order never cited.

work order verification  ·  improved: 1 fixed
  fixed      sca.python.subprocess.shell_true   bad.py:3
  scope: EXCEEDED — 1 of 2 changed file(s) were never cited: src/other.py

Why this exists

AI coding agents ship code at human-review-saturating speed. Point them at a security finding and the documented anti-patterns are:

Anti-pattern What the agent actually does
Crypto roulette "Replace MD5 with SHA-256" → rewrites the hashing module to use a library it saw in training data.
Auth-flow rewrite "Fix the IDOR" → refactors the session model. Now you have an unaudited new auth path.
Validation softening "Make the tests pass after the fix" → weakens the regex / removes the bounds check.
Test deletion "The security test is failing" → deletes the test.
Lint disable "This rule fires repeatedly" → # nosec, # noqa, eslint-disable everywhere.
Scope creep "I fixed the SQLi" → followed by 600 lines of unrelated refactoring.
Dependency thrash "Bumping the vulnerable package" → introduces 12 unrelated new dependencies.
Silent behavior change "It works now" → same input, different output. Downstream callers break.

Existing scanners (Semgrep, Bandit, CodeQL, Snyk, Trivy) emit findings. None of them ship a bounded prompt back to the agent that says "fix only these specific findings, do not touch crypto/auth/validation/logging, preserve behavior."

That gap is what this tool fills.

The output that matters

Every other security scanner stops at "here's a list of findings." secure-code-agent generates a remediation prompt:

# Security remediation — bounded scope

You are fixing the security findings listed in §FINDINGS below.
This is a constrained task, not a refactor.

## Hard constraints (MUST NOT violate)

1. Fix only the findings listed in §FINDINGS. Do not touch unrelated
   code, files, or modules.
2. Do not change cryptographic algorithms, key derivation, IV/nonce
   handling, padding modes, or random sources unless a finding in
   §FINDINGS explicitly names them as the defect.
3. Do not change authentication flows, session handling, token
   lifetime, cookie attributes, or authorization gates unless a
   finding in §FINDINGS explicitly names them.
4. Do not weaken input validation, output encoding, sanitization,
   bounds checks, regex strictness, or rate limits to make existing
   tests pass.
5. Do not disable, delete, or skip security tests. Do not remove
   `@_limiter.limit`, `@require_auth`, `@require_csrf`, or similar
   decorators.
6. Do not silence linter warnings via `# nosec`, `# noqa`, `# type:
   ignore`, `eslint-disable`, `sonar-disable`, or equivalent.
7. Do not introduce new third-party dependencies. Prefer stdlib or
   already-vendored libraries.
8. Preserve behavior. Same inputs must produce the same outputs
   unless a finding explicitly proves the current behavior is unsafe.
9. Add a focused test that exercises the specific security boundary
   you fixed. The test must FAIL on the pre-fix code and PASS on
   the post-fix code. No "TODO: add test later".
10. Keep the patch small. If you find yourself rewriting a function
    rather than patching it, stop and report the structural issue.

## §FIX — patch these
...

Hand the work order to Claude Code, Codex, Cursor, Copilot, or any agent. The agent now has explicit boundaries. The full template + rationale lives in docs/remediation.md.

It is written on every run, alongside the report — no flag required. The report describes your code; the work order changes it. Set outputs.prompt_path to null if you do not want one.

Findings are tiered, because not all of them deserve equal attention

A flat list gives a shell=True command injection and a PASSWORD_FIELD = "password" name-match the same billing, so an agent working top to bottom spends its care on noise.

tier what it means what the agent does
§FIX the scanner is confident and the rule has not been measured producing noise patch it
§REVIEW a rule measured producing non-defects, or a scanner reporting low confidence — the reason is stated per finding confirm it is real first; a justified suppression is a successful outcome here
§ACCEPT test tree and documentation propose a suppression, do not patch

A noisy rule is demoted, never dropped. Bandit's B105 produced zero useful hits out of 22 across the calibration corpus — empty defaults, field names, django-insecure- — and still caught a planted hardcoded credential, so the value is judged as well as the rule.

Proving the work order actually helped

A work order nobody checks is a suggestion. After the agent has worked:

secure-code-agent . --verify-against secure-code-report.json

This re-audits and reports what was fixed, what is still open, what was silenced rather than repaired, and what this work introduced. It exits nonzero unless the run passes, because a verification step that always passes verifies nothing.

work order verification  ·  not proven: 0 fixed, 2 silenced rather than fixed, 1 still open
  SILENCED   B602    app.py:4
  SILENCED   B324    app.py:7
  ! 2 finding(s) disappeared without the code being repaired — a suppression
    entry now covers them, or the reported line gained an inline marker such
    as `# nosec`.

That last case is the one worth having. Lint disable is in the anti-pattern table above, hard constraint 6 forbids it, and forbidding is not detecting — Bandit honours # nosec itself, so a silenced finding simply stops arriving and reads as fixed. Verification reads the source back and calls it what it is.

Findings in the test tree and documentation are reported but not required: §ACCEPT tells the agent not to patch them, so demanding them back would make any repository with fixtures impossible to verify.

Trend, which is what a score is actually for

A single B− tells you little. A B− that was an A− three runs ago tells you something happened. Every run appends one line to .secure-code/history.jsonl and prints the movement:

trend: 3.90 (B+) — up 3.86 from 0.04 (F), 3 scored runs

A run whose coverage was too thin to grade records null and is skipped rather than plotted as a collapse to zero — the same rule the rest of the tool follows: absence of evidence is not a bad grade.

Standards anchored, not invented

Known rules map to fields from five public standards. Unmapped and scanner-control findings retain null standards fields rather than receiving invented mappings.

A CWE comes from one of three places, in descending authority: an adapter's explicit override, the curated map in standards.py, then whatever the scanner itself declared. That last source was being discarded — Bandit publishes a CWE for all ~70 of its plugins and gosec for every rule, and both were dropped on the floor, leaving 14% of real findings with any CWE at all against a corpus measurement. Reading them takes it to 100%, and the OWASP category is derived from the CWE using OWASP's own published category-to-CWE lists where the curated map has none.

Derivation stops where the standard does. CWE-703 — Bandit's classification for a bare assert — belongs to no OWASP Top 10 category, so that field stays null. 62% of findings carry an OWASP category and the rest say nothing, which is the honest answer.

Source What we use it for
NIST SSDF SP 800-218 Process practice id (e.g. PW.5.1)
OWASP Top 10 (2021) Risk bucket (e.g. A03:2021-Injection)
OWASP ASVS 5.0 Verification requirement (e.g. V5.3)
MITRE CWE Top 25 (2025) Canonical weakness id used in stable fingerprints
OpenSSF Scorecard Repo + supply-chain hygiene
SARIF 2.1.0 Output format (and external scanner ingest)

When scanners map a finding to CWE-89, the canonical CWE participates in its stable fingerprint and baseline identity. Cross-scanner findings are not yet collapsed before scoring; reports preserve the original scanner evidence.

Architecture (orchestrator, not engine)

┌────────────────────────────────────────────────────────────────────┐
│  secure-code-agent CLI                                              │
│                                                                     │
│  Config → Scanners (subprocess) → Findings → Scoring → Renderers   │
│                                                                     │
│                                            ┌──────────────────┐    │
│                                            │ Markdown report  │    │
│                                            │ JSON             │    │
│                                            │ SARIF 2.1.0      │    │
│                                            │ PR comment       │    │
│                                            │ Remediation 🪄    │    │
│                                            │ Agent standards  │    │
│                                            └──────────────────┘    │
└────────────────────────────────────────────────────────────────────┘
        │
        │   Scanners (subprocess, version-isolated):
        │
        ├── Bandit            (Python SAST)
        ├── Semgrep           (multi-language SAST + SARIF ingest)
        ├── pip-audit         (Python SCA)
        ├── npm audit         (Node SCA)
        ├── Gitleaks          (secret scanning, history-aware)
        ├── TruffleHog        (verified secret scanning)
        ├── Trivy             (containers / IaC / k8s / vuln / secret)
        ├── Checkov           (Terraform / CloudFormation / Helm / k8s)
        ├── Hadolint          (Dockerfile lint)
        ├── OSV-Scanner       (multi-ecosystem SCA via osv.dev)
        ├── OpenSSF Scorecard (repo hygiene + supply chain)
        ├── njsscan           (JS/TS SAST, offline, no Node needed)
        ├── RuboCop           (Ruby security cops, --only Security)
        ├── gosec             (Go SAST — opt-in; needs the Go toolchain)
        ├── CodeQL SARIF      (ingest GitHub-hosted analysis)
        └── Built-in regex rules (high-confidence, low-FP)

We don't reimplement SAST. We invoke best-in-class scanners as subprocesses, parse their canonical output, normalize across CWE/OWASP/ASVS/SSDF, and produce one ranked view.

Full architecture in docs/design.md.

Audit categories (9 buckets, 1 grade)

Findings roll up to nine canonical categories. The grade is driven by the worst category — one CRITICAL secret in git history shouldn't be offset by a clean dependency tree.

Category Examples
secrets Hardcoded API keys, tokens in history, .env committed
dependencies CVE in pinned dep, yanked package, abandoned upstream
code_vulnerabilities SQLi, XSS, command-injection, path-traversal, SSRF, XXE, deserialization
auth_authz Missing auth gate, IDOR, broken access control, JWT misuse
crypto Weak alg, hardcoded IV, ECB, MD5/SHA-1 for security, missing constant-time
supply_chain Unpinned action, missing SBOM, no signed releases, low Scorecard
config_iac World-readable S3, public security group, Dockerfile USER root, k8s privileged
logging_observability Secrets in logs, PII in URLs, missing audit trail on auth events
policy_docs Missing SECURITY.md, no responsible-disclosure path, no threat model

Scoring math + worked examples in docs/scoring.md.

Hard gates

{
  "gates": {
    "fail_on_severity":    ["critical", "high"],
    "fail_on_category":    ["secrets", "auth_authz"],
    "fail_on_new":         true,
    "min_score":           4.0,
    "require_scanners":    ["bandit", "gitleaks"],
    "max_unsuppressed":    { "critical": 0, "high": 0, "medium": 10 }
  }
}

Any tripped gate is a nonzero exit. Compose freely.

require_scanners is a coverage gate, not a vulnerability gate. A required scanner must resolve and complete successfully. Missing executables, timeouts, invalid output, unsupported inputs, or excluding the scanner with CLI filters fail coverage. Optional scanner failures produce PARTIAL coverage without turning a clean finding set into a false comprehensive result. Markdown and JSON reports record each scanner's outcome, resolved command, and version. When require_scanners is present it must name at least one scanner; an empty list is rejected instead of silently removing the structural coverage gate.

External scanners are not bundled, and the agent never installs one for you — a gate that fetches and runs binaries to satisfy its own coverage requirement is the supply-chain risk it is supposed to catch. Resolution order is an explicit scanners.<name>.command, the active PATH, then python -m <module> for supported Python scanners. Relative executable paths resolve from the scan target and are executed with shell=False.

secure-code-agent --preflight reports which enabled scanners resolve on this host, with versions and the install command for anything missing, and exits nonzero when a required scanner is unavailable — so a missing toolchain costs a second instead of a full audit. Bandit and pip-audit install as secure-code-agent[required-scanners]; Semgrep and Checkov add [python-scanners]. The remaining scanners are standalone binaries that cannot come from PyPI: install them with your package manager, or run their pinned upstream CI action and feed us the SARIF, which counts as coverage:

secure-code-agent --fail-on-gate --sarif-import trivy.sarif

An import satisfies require_scanners for the tool that produced it. An unreadable, malformed, or run-less import fails the gate rather than ingesting nothing quietly, an import reporting its own executionSuccessful: false is recorded as failed, and when a scanner reports both locally and by import the worse outcome wins. See docs/scanners.md for the full install matrix.

{
  "scanners": {
    "bandit": {
      "enabled": true,
      "command": [".audit-tools/bin/python", "-m", "bandit"]
    },
    "pip_audit": {
      "enabled": true,
      "command": [".audit-tools/bin/python", "-m", "pip_audit"],
      "mode": "project",
      "inputs": ["engine/pyproject.toml"]
    }
  }
}

The tool never downloads a scanner during an audit. Install and pin scanner versions in the audit environment or CI image.

This repository's own CI audits requirements-audit.txt, which pins the minimum supported runtime dependency version. Project mode remains available for repositories whose pyproject.toml is their authoritative audit input.

Time-bounded suppressions

.scignore.yaml — every suppression requires a reason AND an expires date (max 365 days). Past-expiry suppressions become CRITICAL findings on their own. You can't ship reason: "we'll fix it later" forever.

- file: services/legacy_billing.py
  rule_id: "*"
  reason:  "Slated for rewrite Q3 2026  gated by initiative INV-44."
  expires: "2026-09-30"

  fingerprint: 0aaa689f8a967d8c   # optional: pin to ONE finding (16 hex, from the report)

  line: 18                          # optional: with fingerprint, pins the exact location

- rule_id: "B101"
  paths:   ["tests/"]
  reason:  "assert statements are legitimate in test code."
  expires: "2027-05-13"

paths entries are repository-relative and use the same pattern syntax as exclude_patterns: a trailing / means that directory at any depth, a bare name matches at any depth, and **/ includes the root.

Wildcard rule (rule_id: "*") requires a file or paths scope — you cannot disable a rule globally.

Baseline + incremental adoption

secure-code-baseline.json fingerprints every current finding. On the next run:

  • Findings present in baseline → acknowledged; don't trip fail_on_new.
  • Findings missing from baseline → new; trip the gate.

--bump-baseline rewrites the baseline from the current findings. The file is plain JSON and must be reviewed like any other security-policy change. This release does not implement an interactive acknowledgment. Baseline entries record the best-effort local Git email, while repository review policy remains the approval boundary.

This lets legacy repos adopt the gate without a 200-finding day-one cleanup.

Quickstart

# Install the orchestrator with its pinned default Bandit + pip-audit toolchain
pip install 'secure-code-agent[required-scanners]'

# Initialize agent standards files for your AI coding tools
secure-code-agent --init-agent-standards \
    --target codex --target claude-code --target cursor --target copilot

# Run an audit with hard-gate exit
secure-code-agent --config secure-code-agent.json \
    --fail-on-gate \
    --output secure-code-report.md \
    --json-output secure-code-report.json \
    --sarif-output secure-code.sarif \
    --comment-output secure-code-pr-comment.md \
    --prompt-output secure-code-remediation-prompt.md

# Ingest external scanner SARIF (CodeQL, Snyk, Trivy, etc.)
secure-code-agent --sarif-import codeql-results.sarif \
                   --sarif-import snyk-results.sarif

--changed-only REF scopes the report to files changed since REF. The whole tree is still scanned — scanners read trees, not diffs — and no grade is issued, because a run that looks at less must not score better. A ref git cannot resolve is an error rather than an empty diff, since "nothing changed" and "your ref is wrong" otherwise produce the same finding count.

The current orchestrator accepts one repository root per invocation. Multiple positional roots fail with exit code 2 instead of silently ignoring coverage.

Invokable skill / slash command

For agents that support invokable skills, this repo ships a portable skill under skills/secure-code-agent/. The SKILL.md body is the source of truth; per-host adapters live under agents/ and copilot/.

Host Install destination Invocation
Codex / OpenAI wired via skills/secure-code-agent/agents/openai.yaml per Codex's skills convention
Claude Code cp -r skills/secure-code-agent ~/.claude/skills/ /secure-code-agent
GitHub Copilot (VS Code) cp skills/secure-code-agent/copilot/secure-code-agent.prompt.md .github/prompts/ /secure-code-agent in Copilot Chat

GitHub Action

- uses: marshallguillory86/secure-code-agent@v0.12.4
  with:
    config: secure-code-agent.json
    fail-on-gate: true

The action installs the exact source bundled with the referenced action plus the pinned required-scanners extra (Bandit and pip-audit), emits Markdown, JSON, SARIF, PR-comment, and remediation artifacts, and uploads SARIF by default. The calling workflow must grant security-events: write for SARIF upload.

What it uploads is secure-code.code-scanning.sarif, not the full SARIF. Code scanning opens an alert for every uploaded result and does not act on SARIF suppressions, so uploading the full file raises an alert, and a review thread on pull requests, for every test-tree and documentation finding and every reviewed .scignore.yaml entry. The upload file leaves those out; the full SARIF, with each suppression and its reason, is kept as an artifact. Outside the action, --code-scanning-sarif-output PATH writes the same file. Pin production usage to a full commit SHA; the version tag above is shown for readability. See action.yml and examples/github-actions/ for full workflows.

maintainability-agent integration

maintainability-agent declares Security a delegated pillar naming this tool, and reports it as NotApplicable so silence is never read as safety. This tool emits the artifact that completes the picture:

secure-code-agent . --security-pillar security-pillar.json
maintainability-agent . --security-pillar security-pillar.json

Standalone use is unaffected — without the flag nothing is written and every other output is identical. MA never executes this tool, and this tool never imports MA; two independently releasable packages exchanging one document.

The artifact carries two values that are never averaged: a practice level read from configuration and CI (is anything preventing the next vulnerability?) and a code condition read from the scanners (what did they find?). condition is null whenever scanner coverage is incomplete — this tool's score is a rate over findings, so removing scanners makes the raw number go up, and an unscanned repository must not arrive at MA looking measured.

Full contract, invariants and the practice rubric: docs/ma-integration.md.

What this is NOT

  • Not a SAST engine. We delegate to Semgrep / Bandit / CodeQL / etc. — we don't write yet another AST analyzer.
  • Not a runtime defense. No WAF, no IDS, no agent in the request path. Static + supply-chain + config only.
  • Not a SaaS. Findings live as files in your repo. No telemetry. No version-check ping.
  • Not a license scanner. Pair with pip-licenses / license-checker separately.
  • Not an exploit generator. No DAST, no fuzzing.

Design principles

  1. Deterministic first, AI optional. The audit never calls an LLM by default. The remediation prompt is a generated artifact you choose to hand to an agent.
  2. Bounded scope. The remediation prompt explicitly forbids touching crypto, auth, validation, logging, and tests.
  3. Standards-anchored. Five public standards (NIST / OWASP-x3 / CWE) — no invented taxonomy.
  4. Stable finding identity. CWE, normalized path, and normalized evidence form the baseline fingerprint. Cross-scanner score deduplication remains future work.
  5. No vendor lock-in. Markdown, JSON, SARIF, plain files. Pipe anywhere.
  6. CI-first, local-first. Same binary in pre-commit, local CI, GitHub Actions, GitLab, Buildkite.

Full design philosophy in docs/design.md.

Documentation

Versioning

  • Semver. v0.x is pre-1.0 — the config schema may evolve. v1.0 locks it.
  • SARIF 2.1.0-shaped output is structurally unit-tested and round-tripped; full OASIS schema validation is not yet part of CI.

Get in touch

License

MIT — see LICENSE.


Built by Marshall Guillory. The companion to maintainability-agent — both tools encode a single thesis: AI agents need deterministic boundaries, not best-effort guardrails.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

secure_code_agent-0.12.4.tar.gz (169.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

secure_code_agent-0.12.4-py3-none-any.whl (186.8 kB view details)

Uploaded Python 3

File details

Details for the file secure_code_agent-0.12.4.tar.gz.

File metadata

  • Download URL: secure_code_agent-0.12.4.tar.gz
  • Upload date:
  • Size: 169.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for secure_code_agent-0.12.4.tar.gz
Algorithm Hash digest
SHA256 6e1e1e80b2e1d881d11e8201954b07a90824cc5b21a8c99337d9c9f5d9105eba
MD5 a3ec4177879ecd572bdb14e6c3ae4b8c
BLAKE2b-256 14c616e3a7d401d2d283c4e4ae28bda1233feba8d1e44f339e01307931eac37c

See more details on using hashes here.

Provenance

The following attestation bundles were made for secure_code_agent-0.12.4.tar.gz:

Publisher: release.yaml on marshallguillory86/secure-code-agent

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file secure_code_agent-0.12.4-py3-none-any.whl.

File metadata

File hashes

Hashes for secure_code_agent-0.12.4-py3-none-any.whl
Algorithm Hash digest
SHA256 37754a0088732ad2ff9ba52a1614b7284d4c918de838738284676fb2cadccf3f
MD5 ce0450be69ecc10d858f1dd27cc6c341
BLAKE2b-256 a9778e1fb906410a7de1987fe6a457db576b5b5f0ded91240b9be033b71f00c4

See more details on using hashes here.

Provenance

The following attestation bundles were made for secure_code_agent-0.12.4-py3-none-any.whl:

Publisher: release.yaml on marshallguillory86/secure-code-agent

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.12.4 This release

2 files

0.12.3

2 files

0.12.2

2 files

0.12.1

2 files

0.12.0

2 files

0.11.1

2 files

0.11.0

2 files

0.10.0

2 files

0.9.0

2 files

0.8.0

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page