| name | security |
| description | Claude-powered SAST and SCA security scan skill. Invoked automatically as the Security Scan step (Step 7) of /aod.build (after Design Quality Gate, before Code Simplification) or standalone via /security. Analyzes all code files and dependency manifests changed on the feature branch relative to main for OWASP Top 10 vulnerability patterns and known CVE findings. Produces a human-readable security-scan.md report and machine-readable .security/ compliance artifacts (scan-log.jsonl, vulnerabilities.jsonl, SARIF 2.1.0, CycloneDX 1.5 SBOM). Blocks build progression on CRITICAL/HIGH findings with an explicit acknowledgment gate. Use --no-security in /aod.build to skip. Invoke /security directly for standalone analysis outside the build pipeline. |
/security — SAST/SCA Security Scan
Purpose: Analyze code files and dependency manifests changed on the feature branch for OWASP Top 10 vulnerabilities and known CVE patterns. Write permanent audit artifacts. Block on CRITICAL/HIGH findings until acknowledged or fixed.
When invoked from /aod.build: Called as the Security Scan step (Step 7) via the Skill tool. --no-security flag in /aod.build bypasses this skill entirely.
When invoked standalone: Run directly as /security from any project directory. security-scan.md is written to specs/{NNN}-*/ if inside an AOD project, or to the current working directory otherwise.
Step 0: Parse Arguments
If invoked standalone, parse optional flags from arguments:
-
--no-security: If present (standalone invocation context), output "Security scan skipped (--no-security)" and exit cleanly. This flag is normally handled by /aod.build Step 0 before invoking this skill; if present here in standalone mode, honor it.
-
Feature number extraction: Detect current feature number from branch name (git branch --show-current → extract leading digits) or from specs/*/ directory listing. Used for output path resolution.
-
Output path resolution:
- If inside an AOD project (
specs/{NNN}-*/ exists): write security-scan.md to specs/{NNN}-*/security-scan.md
- Otherwise: write
security-scan.md to current working directory
Step 1: File Detection
Detect all files changed on the feature branch relative to main.
1a: SAST File Detection (FR-004)
Run: git diff --name-only main...HEAD
Filter the output to code files by including only these extensions:
.py .js .ts .jsx .tsx .sh .go .rs .java .rb .swift .kt .php .cs .cpp .c .h
Exclude these patterns from the result:
- Generated files:
*.lock, *.min.js, *.min.css, *.map, *.generated.*
- Directories:
vendor/, dist/, build/, node_modules/, .aod/, docs/
Store result as code_files list. Count as code_file_count.
Zero-file path: If code_file_count is 0, set sast_status = "SKIPPED" with note "SAST: No code files changed — skipping static analysis". Proceed to Step 1b.
1b: SCA Manifest Detection (FR-005)
From the same git diff --name-only main...HEAD output, filter to dependency manifest filenames:
requirements.txt, pyproject.toml, setup.py, package.json, package-lock.json, yarn.lock, Gemfile, Gemfile.lock, go.mod, go.sum, pom.xml, build.gradle, Cargo.toml, Cargo.lock
Store result as manifest_files list. Count as manifest_count.
Zero-manifest path: If manifest_count is 0, set sca_status = "SKIPPED" with note "SCA: No dependency manifests changed — skipping dependency audit".
Step 2: Pre-Scan Checks
2a: Large-Diff Warning (FR-006)
If code_file_count > 50:
Warning: {code_file_count} code files changed on this branch.
SAST analysis of large diffs may take several minutes.
(A) Continue with SAST analysis
(B) Skip SAST for this run
Use AskUserQuestion with these options. If user selects (B): set sast_status = "SKIPPED" with reason "user-skip-large-diff".
2b: .security/ Directory Initialization (FR-020)
Check if .security/ exists at repo root:
if [ ! -d ".security" ]; then
mkdir -p .security/reports
fi
Auto-generate .security/README.md with content:
# .security/ — Compliance Evidence Store
This directory contains machine-readable security audit artifacts generated by the `/security` skill
(invoked automatically from `/aod.build` Security Scan step).
**Do NOT gitignore this directory.** These files serve as compliance evidence for security reviews,
PCI-DSS 4.0 requirements, and PR audit trails.
## Contents
| File | Format | Purpose |
|------|--------|---------|
| `scan-log.jsonl` | JSONL (append-only) | Immutable execution history with chain_hash integrity |
| `vulnerabilities.jsonl` | JSONL (append-only) | Vulnerability lifecycle events (DETECTED, RISK_ACCEPTED, EXPIRY_REOPENED, REMEDIATED) |
| `exceptions.jsonl` | JSONL (append-only) | Risk acceptance decisions with expiry dates |
| `reports/{commit-sha}.sarif` | SARIF 2.1.0 JSON | Machine-readable findings (importable to GitHub Security tab, Defect Dojo, SonarQube) |
| `reports/sca-{YYYY-MM-DD}.cdx.json` | CycloneDX 1.5 JSON | Software Bill of Materials for dependency scans |
## Verification
Chain hash integrity check (scan-log.jsonl):
Re-compute SHA256(entry_json_without_chain_hash) for the first entry.
For subsequent entries: SHA256(previous_chain_hash + current_entry_json_without_chain_hash).
All JSON keys sorted alphabetically, no trailing whitespace.
Check .gitignore for .security/ exclusion:
if grep -q "\.security" .gitignore 2>/dev/null; then
echo "WARNING: .security/ appears to be gitignored. This directory must NOT be excluded from version control — it contains compliance evidence. Please remove the .security/ entry from .gitignore."
fi
2c: Expired Acceptance Check (FR-018)
If .security/exceptions.jsonl exists:
Read all entries. For each entry, check whether acceptance_expiry has passed using cross-platform epoch comparison (see utility functions below).
Collect all expired entries as reopened_exceptions list.
If reopened_exceptions is non-empty:
- Display "REOPENED EXCEPTIONS" section (before any new findings):
REOPENED EXCEPTIONS
===================
The following previously acknowledged findings have expired and require re-acknowledgment:
{for each expired entry:}
vuln_id: {vuln_id}
Severity: {finding_severity}
Summary: {finding_summary}
File: {file from original finding — if available}
Accepted: {ts_accepted} by {accepted_by}
Expired: {acceptance_expiry}
Justification: {original justification}
-
Append EXPIRY_REOPENED event to .security/vulnerabilities.jsonl for each expired entry (see Step 6 for format). This MUST happen before the acknowledgment prompt.
-
Route each reopened exception through the severity gate in Step 5 (same blocking logic as new CRITICAL/HIGH findings).
Step 3: SAST Analysis
Skip if sast_status = "SKIPPED".
3a: File Batching (FR-009)
Group code_files into batches of 5–10 files per analysis invocation:
batch_1 = code_files[0:8]
batch_2 = code_files[8:16]
... etc.
Maintain all_sast_findings = [] across all batches.
3b: OWASP P0 Pattern Detection Per Batch (FR-007)
For each batch, analyze the files against these OWASP P0 patterns:
A01: Broken Access Control
- Open Redirect:
redirect(user_input) or redirect(request.args.get(...)) without allowlist validation
- Path Traversal:
open(user_input), os.path.join(base, user_input) without sanitization
A02: Cryptographic Failures
- Hardcoded Secrets: credential literals in non-test files (
password = "...", api_key = "...", secret = "...", token = "...")
- Weak Crypto:
hashlib.md5(password), sha1(pwd) used for password hashing (excludes file checksum use)
- Insecure Random:
random.random(), Math.random() used for token generation, session IDs, or nonces
A03: Injection
- SQL Injection: string concatenation into SQL/DB query with user input (
"SELECT" + var, f-string with request param)
- Command Injection:
os.system(var), subprocess.call(f"cmd {var}"), exec(user_input)
- Template Injection:
render_template_string(user_input) or equivalent
A05: Security Misconfiguration
- Debug Mode:
DEBUG = True in non-test config files, app.run(debug=True)
- Permissive CORS:
Access-Control-Allow-Origin: * with Allow-Credentials: true
- Verbose Errors:
traceback.format_exc() returned in HTTP response body
- Default Credentials:
admin/admin, root/root, user/password literals in config files
A07: Identification & Authentication Failures (partial)
- Insecure Cookie:
set_cookie(...) without secure=True, httponly=True
- Plaintext Credentials: password stored without hashing, credentials written to log
For each finding detected, collect:
{
"type": "SAST",
"owasp_category": "A03: Injection",
"file": "relative/path/from/repo/root.py",
"line": 42,
"description": "SQL injection via string concatenation with user input",
"recommendation": "Use parameterized queries or an ORM. Never concatenate user input directly into SQL strings."
}
Append to all_sast_findings.
3c: CVSS Category Rubric (FR-008)
After collecting raw findings, assign CVSS scores using this fixed lookup table. Label ALL SAST scores "estimated". Set kev_flag: "N/A" for all SAST findings.
| OWASP Category | Default Score | Default Severity | Escalation | Reduction |
|---|
| A03: SQL Injection | 8.8 | HIGH | No auth required → 9.8 CRITICAL | Server-side validation present → 6.5 MEDIUM |
| A03: Command Injection | 9.0 | CRITICAL | Remote exec confirmed → 9.8 CRITICAL | — |
| A02: Hardcoded Secret | 7.5 | HIGH | Production credential → 9.1 CRITICAL | Dev-only env file → 5.5 MEDIUM |
| A02: Weak Crypto (passwords) | 7.4 | HIGH | — | Read-only checksum use → 3.7 LOW |
| A02: Insecure Random | 6.5 | MEDIUM | Auth/session use → 8.0 HIGH | — |
| A01: Open Redirect | 6.1 | MEDIUM | Authenticated user targeted → 7.4 HIGH | — |
| A01: Path Traversal | 7.5 | HIGH | Unauthenticated endpoint → 9.1 CRITICAL | — |
| A05: Debug Mode | 5.3 | MEDIUM | Production config → 7.5 HIGH | — |
| A05: Permissive CORS * | 7.5 | HIGH | — | Non-credentialed endpoint → 5.4 MEDIUM |
| A05: Default Credentials | 9.8 | CRITICAL | — | — |
| A07: Missing secure/httponly cookie | 5.4 | MEDIUM | Session/auth cookie → 7.1 HIGH | — |
| A07: Plaintext credential storage | 8.0 | HIGH | — | — |
Apply escalation/reduction conditions based on code context detected during analysis.
3d: EPSS Qualitative Context (FR-010)
Assign qualitative EPSS context per vulnerability class (not a per-finding numeric score):
| Vulnerability Class | EPSS Context |
|---|
| SQL Injection | "SQL injection vulnerabilities are actively exploited in 8–15% of exposed systems within 30 days of disclosure" |
| Command Injection | "Command injection vulnerabilities are among the most exploited — exploitation rates exceed 20% within 30 days in exposed systems" |
| Hardcoded Secrets | "Hardcoded credentials are exploited in nearly all cases once discovered via source code access or repository scanning" |
| Weak Crypto (passwords) | "Weak password hashing enables offline cracking attacks; exploitation probability is high once credential databases are obtained" |
| Insecure Random | "Insecure random number generation for security-sensitive functions has a moderate exploitation rate depending on the use case" |
| Open Redirect | "Open redirects are commonly exploited in phishing campaigns; exploitation is straightforward for any attacker with knowledge of the endpoint" |
| Path Traversal | "Path traversal vulnerabilities are actively exploited in 5–12% of exposed systems; automated scanners commonly detect and exploit them" |
| Debug Mode | "Debug mode exposure is commonly exploited in reconnaissance and subsequent attacks; exploitation probability is high if internet-exposed" |
| Permissive CORS | "CORS misconfiguration is exploited in targeted attacks against authenticated users; exploitation requires user interaction" |
| Default Credentials | "Default credentials are exploited immediately upon discovery; automated scanners and credential stuffing tools target these actively" |
| Insecure Cookie | "Missing cookie security flags enable session hijacking; exploitation requires network position or XSS as a prerequisite" |
| Plaintext Credentials | "Plaintext credential storage is exploited whenever storage is accessed; exploitation probability is effectively 100% once storage is read" |
3e: Finding Deduplication
After aggregating findings across all batches:
Compute vuln_id for each finding using the fingerprint function (see Utility Functions).
If the same vuln_id appears multiple times (same vulnerability detected in multiple batches), retain only one instance.
Store deduplicated result as sast_findings.
Error handling: If any analysis step encounters an error (timeout, context limit, encoding error):
Security scan encountered an error: {error_message}
(A) Retry scan
(B) Skip and complete build
(C) Abort build
Use AskUserQuestion. If Skip: write security-scan.md with error type and timestamp; append scan-log.jsonl entry with status: "ERROR". Build continues. If Abort: halt.
Step 4: SCA Analysis
Skip if sca_status = "SKIPPED".
4a: Manifest Analysis (FR-011)
For each file in manifest_files, analyze:
- Known CVE patterns: Packages with known vulnerability patterns from Claude training data (cutoff: August 2025)
- Unsafe version ranges:
*, latest, >=0.0.0 in production manifests
- Abandoned packages: Packages where Claude has training knowledge of abandonment or takeover
- Supply chain risks: Known typosquatting targets, known package takeover victims
For each finding, collect raw fields:
{
"type": "SCA",
"package_name": "string",
"package_version": "string (declared version or constraint)",
"cve_id": "string | null",
"manifest_source": "requirements.txt",
"description": "string (with training cutoff note)",
"recommendation": "string"
}
4b: SCA Finding Format (FR-012)
For each SCA finding, populate all required fields:
severity: assigned based on CVSS score (CRITICAL ≥9.0, HIGH 7.0–8.9, MEDIUM 4.0–6.9, LOW <4.0)
cve_id: when known from training data; null otherwise
cvss_score: NVD v3.1 base score from training data; labeled "known official CVSS"
epss_context: qualitative context + note "(verify current score at first.org/epss)"
kev_flag: "YES" or "NO" from training data + note "(verify at cisa.gov/known-exploited-vulnerabilities)"
kev_verification_url: "https://www.cisa.gov/known-exploited-vulnerabilities-catalog"
purl: ecosystem-appropriate format (see purl formats below)
purl formats by ecosystem:
Python: pkg:pypi/{name}@{version}
npm: pkg:npm/{name}@{version}
npm scoped: pkg:npm/%40{scope}/{name}@{version}
Go: pkg:golang/{module_path}@{version}
Java: pkg:maven/{group_id}/{artifact_id}@{version}
Rust: pkg:cargo/{name}@{version}
Ruby: pkg:gem/{name}@{version}
For package.json version ranges (e.g., ^4.17.21): record the declared constraint in version field; add property aod:version_constraint noting "resolved version may differ — declare exact version in lock file for definitive audit".
4c: KEV=YES Auto-Elevation (FR-015)
Any SCA finding with kev_flag: "YES" MUST be elevated to CRITICAL severity regardless of its CVSS base score:
if kev_flag == "YES":
severity = "CRITICAL"
cvss_note = "(elevated to CRITICAL per KEV rule — actively exploited)"
This elevation propagates to the severity gate in Step 5: KEV=YES findings show only (A) Fix now and (C) Abort.
4d: CycloneDX SBOM Component Collection
For each package identified in manifest_files, create an SBOMComponent record:
{
"type": "library",
"name": "package-name",
"version": "declared-version-or-constraint",
"purl": "pkg:pypi/package-name@1.2.3",
"properties": [
{"name": "aod:manifest_source", "value": "requirements.txt"},
{"name": "aod:version_constraint", "value": "^1.2.3 (resolved version may differ)"}
]
}
Store as sbom_components list. Used by Step 6 (artifact writing) to generate the CycloneDX SBOM.
4e: SCA Training-Cutoff Disclaimer (FR-013)
Every SCA section in security-scan.md output MUST include verbatim:
SCA findings are based on Claude training knowledge (cutoff: August 2025). Supplement with real-time CVE scanning (npm audit, pip-audit, Snyk) for production workloads.
Error handling: Same retry/skip/abort pattern as Step 3 error handling.
Step 5: Severity Gate
5a: Aggregate All Findings
Combine sast_findings + sca_findings (after KEV elevation) into all_findings.
Group:
blocking_findings = findings with severity in ["CRITICAL", "HIGH"]
non_blocking_findings = findings with severity in ["MEDIUM", "LOW", "INFO"]
Also include reopened_exceptions (from Step 2c) in blocking_findings — they go through the same gate.
5b: Display Non-Blocking Findings
For MEDIUM findings, display with note: "Review recommended before deploy"
For LOW/INFO findings, display in report without note.
Non-blocking findings do not halt the build.
5c: Clean-Scan Path (US1 AC2, SC-006)
If blocking_findings is empty AND there are no reopened_exceptions:
Output:
Security Scan: PASSED — no issues found (AI-powered analysis; supplement with dedicated SAST tools for production-critical systems)
Set scan_status = "PASSED". Auto-proceed to Step 6 (artifact writing).
5d: Blocking Acknowledgment Gate (FR-014, FR-015)
For each finding in blocking_findings (one at a time):
Check KEV status:
-
If kev_flag == "YES":
{severity} Finding: {owasp_category or cve_id}
File: {file}:{line}
CVSS: {cvss_score} {severity} ({cvss_type})
KEV: YES (actively exploited — verify at cisa.gov/known-exploited-vulnerabilities-catalog)
EPSS: {epss_context}
Summary: {description}
Fix: {recommendation}
(A) Fix now — halt this session; re-run /aod.build to resume at security scan
(C) Abort
Note: Acknowledge option unavailable for KEV=YES findings — actively exploited vulnerability
-
If kev_flag != "YES":
{severity} Finding: {owasp_category or cve_id}
File: {file}:{line}
CVSS: {cvss_score} {severity} ({cvss_type})
KEV: {kev_flag}
EPSS: {epss_context}
Summary: {description}
Fix: {recommendation}
(A) Fix now — halt this session; re-run /aod.build to resume at security scan
(B) Acknowledge and proceed — requires justification (minimum 20 characters)
(C) Abort
Use AskUserQuestion.
On (A) Fix now: Halt build session. Developer fixes the issue and re-runs /aod.build.
On (B) Acknowledge and proceed:
- Prompt: "Enter justification (minimum 20 characters):"
- Validate:
len(justification.strip()) >= 20 — if too short, re-prompt
- Get
accepted_by via: git config user.email
- Compute
acceptance_expiry:
- CRITICAL: 30 days from now (ISO 8601 UTC)
- HIGH: 90 days from now (ISO 8601 UTC)
- Append
RiskAcceptance entry to .security/exceptions.jsonl (see Step 6)
- Record finding as acknowledged; add to
acknowledged_findings list
- Continue to next blocking finding
On (C) Abort: Halt build session entirely.
5e: Post-Gate Status
After processing all blocking findings:
- If all were acknowledged:
scan_status = "FINDINGS" (build proceeds)
- If any were not acknowledged (user selected A or C): build halted
Step 6: Artifact Writing
Write all compliance artifacts. This step executes after the severity gate completes (all blocking findings acknowledged or no blocking findings).
6a: security-scan.md (FR-026)
Always write specs/{NNN}-*/security-scan.md (or CWD for standalone). Content:
# Security Scan Report
**Feature**: {NNN} — {feature_name or branch_name}
**Branch**: {branch}
**Commit**: {commit_sha}
**Scan ID**: {scan_id}
**Timestamp**: {ts_started} UTC
**Status**: {PASSED | FINDINGS | SKIPPED | ERROR}
---
## Summary
| Category | Count |
|---|---|
| Files scanned (SAST) | {code_file_count} |
| Manifests audited (SCA) | {manifest_count} |
| CRITICAL findings | {count} |
| HIGH findings | {count} |
| MEDIUM findings | {count} |
| LOW findings | {count} |
| INFO findings | {count} |
{If --no-security: "Skipped (--no-security) — {timestamp}"}
---
## Findings
{if no findings: "No security findings detected."}
### CRITICAL
{for each CRITICAL finding:}
#### {vuln_id}: {owasp_category or cve_id}
- **Type**: {SAST | SCA}
- **File**: `{file}:{line}`
- **CVSS**: {cvss_score} CRITICAL ({cvss_type})
- **EPSS**: {epss_context}
- **KEV**: {kev_flag}{if YES: " — verify at cisa.gov/known-exploited-vulnerabilities-catalog"}
- **Description**: {description}
- **Recommendation**: {recommendation}
{if acknowledged: "- **Acknowledged**: Yes — Justification: {justification} | Accepted by: {accepted_by} | Expiry: {acceptance_expiry}"}
### HIGH
{same format}
### MEDIUM
{same format, with note: "(Review recommended before deploy)"}
### LOW / INFO
{same format}
---
## Acknowledgment Decisions
{if any acknowledged findings:}
| vuln_id | Severity | Justification | Accepted By | Expiry |
|---|---|---|---|---|
{rows}
{if no acknowledged findings: "No acknowledgment decisions made in this scan."}
---
## Artifacts
- Scan log: `.security/scan-log.jsonl`
- Vulnerability events: `.security/vulnerabilities.jsonl`
- Risk acceptances: `.security/exceptions.jsonl`
- SARIF report: `.security/reports/{commit_sha}.sarif`
{if sca ran: "- CycloneDX SBOM: `.security/reports/sca-{date}.cdx.json`"}
---
*Security Scan: AI-powered analysis; supplement with dedicated SAST tools for production-critical systems.*
{if sca ran: "*SCA findings are based on Claude training knowledge (cutoff: August 2025). Supplement with real-time CVE scanning (npm audit, pip-audit, Snyk) for production workloads.*"}
6b: scan-log.jsonl (FR-021)
Append exactly one ScanLogEntry to .security/scan-log.jsonl:
{"scan_id": "{uuid_v4}", "ts": "{ISO_8601_UTC}", "branch": "{branch}", "commit_sha": "{12_char_sha}", "status": "{PASSED|FINDINGS|SKIPPED|ERROR}", "files_scanned": {integer}, "manifests_audited": {integer}, "finding_counts": {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0}, "chain_hash": "{sha256_hex}"}
Compute chain_hash using the chain hash utility function (see Utility Functions).
Append-only. Never modify existing entries.
6c: vulnerabilities.jsonl (FR-022)
Append VulnerabilityEvent entries to .security/vulnerabilities.jsonl:
DETECTED (for each new finding where no existing event exists for vuln_id, OR most recent event is REMEDIATED):
{"vuln_id": "...", "event_type": "DETECTED", "ts": "{ISO_8601_UTC}", "scan_id": "{scan_id}", "detail": {"severity": "{severity}", "file": "{file}", "previous_expiry": null, "original_acceptor": null}}
Deduplication rule: If the most recent event for a vuln_id is RISK_ACCEPTED, do NOT append a new DETECTED event — surface the existing acceptance status instead.
RISK_ACCEPTED (for each acknowledged finding):
{"vuln_id": "...", "event_type": "RISK_ACCEPTED", "ts": "{ISO_8601_UTC}", "scan_id": "{scan_id}", "detail": {"severity": "{severity}", "file": "{file}", "previous_expiry": null, "original_acceptor": null}}
REMEDIATED (for each vuln_id in previous vulnerabilities.jsonl that is absent from current scan findings):
{"vuln_id": "...", "event_type": "REMEDIATED", "ts": "{ISO_8601_UTC}", "scan_id": "{scan_id}", "detail": {"severity": "{previous_severity}", "file": "{file}", "previous_expiry": null, "original_acceptor": null}}
EXPIRY_REOPENED (for each expired exception, written in Step 2c before acknowledgment prompt):
{"vuln_id": "...", "event_type": "EXPIRY_REOPENED", "ts": "{ISO_8601_UTC}", "scan_id": "{scan_id}", "detail": {"severity": "{finding_severity}", "file": "{file_from_exception}", "previous_expiry": "{expired_acceptance_expiry}", "original_acceptor": "{accepted_by_from_exception}"}}
Never truncate or rewrite this file.
6d: SARIF 2.1.0 Writer (FR-024)
Write .security/reports/{commit_sha}.sarif:
{
"version": "2.1.0",
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"runs": [
{
"tool": {
"driver": {
"name": "aod-security",
"version": "1.0.0",
"informationUri": "https://github.com/anthropics/agentic-oriented-development",
"rules": [
{
"id": "{owasp_category_id}",
"name": "{owasp_category_name}",
"shortDescription": {"text": "{category description}"},
"helpUri": "https://owasp.org/Top10/"
}
]
}
},
"results": [
{
"ruleId": "{owasp_category_id}",
"message": {"text": "{description}"},
"locations": [
{
"physicalLocation": {
"artifactLocation": {"uri": "{relative_file_path}"},
"region": {"startLine": {line_number}}
}
}
],
"partialFingerprints": {
"primaryLocationLineHash": "{deterministic_hash_of_file_line_description}"
},
"level": "{error|warning|note}"
}
]
}
]
}
Level mapping: CRITICAL/HIGH → "error", MEDIUM → "warning", LOW/INFO → "note"
One rules[] entry per distinct OWASP category detected. One results[] entry per finding.
Schema validation: Before writing, verify the JSON structure is valid SARIF 2.1.0 (check required fields: version, runs[0].tool.driver.name, runs[0].results[]). If validation fails: write security-scan.md with validation error note; skip SARIF write.
6e: CycloneDX SBOM Writer (FR-025)
Write .security/reports/sca-{YYYY-MM-DD}.cdx.json when manifest_count > 0:
{
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"serialNumber": "urn:uuid:{uuid_v4}",
"version": 1,
"metadata": {
"timestamp": "{ISO_8601_UTC}",
"tools": [{"vendor": "aod-security", "name": "aod-security", "version": "1.0.0"}],
"component": {"type": "application", "name": "{repo_name}", "version": "{commit_sha}"}
},
"components": [
{
"type": "library",
"name": "{package_name}",
"version": "{declared_version_or_constraint}",
"purl": "{ecosystem_purl}",
"properties": [
{"name": "aod:manifest_source", "value": "{manifest_filename}"},
{"name": "aod:version_constraint", "value": "{constraint_note_or_null}"}
]
}
]
}
6f: exceptions.jsonl Appender (FR-017)
For each acknowledged finding, append one RiskAcceptance entry to .security/exceptions.jsonl:
{"vuln_id": "...", "finding_severity": "HIGH", "finding_summary": "{one-line description}", "justification": "{user justification}", "accepted_by": "{git_user_email}", "ts_accepted": "{ISO_8601_UTC}", "acceptance_expiry": "{ISO_8601_UTC — 30d CRITICAL / 90d HIGH}"}
Append-only. One entry per acknowledgment decision.
Step 7: Commit Strategy (FR-030)
Commit 1 (always, when scan ran)
Stage: scan-log.jsonl + .security/reports/{commit_sha}.sarif + .security/reports/sca-{date}.cdx.json (if written) + specs/{NNN}-*/security-scan.md + .security/README.md (if first run)
Commit message:
security({NNN}): run security scan [{commit_sha}]
Commit 2 (only when findings acknowledged)
Stage: .security/vulnerabilities.jsonl + .security/exceptions.jsonl
Commit message:
security({NNN}): record risk acceptances
Commit 2 failure handling (FR-031):
If Commit 2 fails after Commit 1 succeeds:
- Catch the error
- Notify: "Scan evidence committed (Commit 1) but risk acceptances were not recorded (Commit 2 failed: {error})"
- Retry Commit 2 once
- If retry fails: display manual recovery steps:
Manual recovery required:
git add .security/vulnerabilities.jsonl .security/exceptions.jsonl
git commit -m "security({NNN}): record risk acceptances (manual recovery)"
- Build MUST NOT proceed silently — the developer must acknowledge the manual recovery requirement
Utility Functions
vuln_id Fingerprint (FR-023)
SAST findings:
input = owasp_category + file + str(line_number) + description[:50]
hash = SHA256(input)
vuln_id = "{REPO-SLUG}-VULN-" + hash[:12]
Where REPO-SLUG is the repository directory name (lowercase, hyphens for spaces).
SCA findings (when CVE known):
input = cve_id + package_name + package_version
hash = SHA256(input)
vuln_id = "{REPO-SLUG}-VULN-" + hash[:12]
SCA findings (no CVE):
input = "SCA:" + package_name + package_version + description[:50]
hash = SHA256(input)
vuln_id = "{REPO-SLUG}-VULN-" + hash[:12]
chain_hash Computation (FR-021, SC-008)
# Serialize entry JSON without the chain_hash field:
# - Sort all keys alphabetically
# - No trailing whitespace or newlines within values
# First entry:
chain_hash = SHA256(serialized_entry_json_without_chain_hash)
# Subsequent entries:
chain_hash = SHA256(previous_entry_chain_hash + serialized_entry_json_without_chain_hash)
To compute SHA256 as an AI agent: use Python hashlib.sha256(input.encode()).hexdigest() or equivalent. Keys must be sorted alphabetically, JSON compact (no spaces), no trailing newline in the input string.
Cross-Platform Date Comparison (FR-019)
For expiry checks (bash 3.2 compatible, macOS + Linux):
OS=$(uname -s)
acceptance_expiry="2026-06-01T12:00:00Z"
if [ "$OS" = "Darwin" ]; then
expiry_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$acceptance_expiry" +%s)
else
expiry_epoch=$(date -d "$acceptance_expiry" +%s)
fi
now_epoch=$(date +%s)
if [ "$now_epoch" -gt "$expiry_epoch" ]; then
echo "EXPIRED"
fi
No declare -A. No ${var^}. No readarray. Use case instead of associative arrays. Use tr for case modification. Use while read for line-by-line processing.
For computing future expiry dates (ISO 8601 UTC):
OS=$(uname -s)
if [ "$OS" = "Darwin" ]; then
expiry_30d=$(date -u -v+30d "+%Y-%m-%dT%H:%M:%SZ")
expiry_90d=$(date -u -v+90d "+%Y-%m-%dT%H:%M:%SZ")
else
expiry_30d=$(date -u -d "+30 days" "+%Y-%m-%dT%H:%M:%SZ")
expiry_90d=$(date -u -d "+90 days" "+%Y-%m-%dT%H:%M:%SZ")
fi