Skip to content

Security Auditor

The Security Auditor is a security engineer that reviews source code for vulnerabilities. It reads all files in scope, evaluates each against OWASP Top 10 categories, and reports findings in structured JSON with enough detail for a developer to act. It does not fix vulnerabilities.

PropertyValue
Commandspeed security [--create-defects]
Assembly functioncontext_assemble_security_auditor
Model tiersupport_model (Sonnet)
ToolsRead, Glob, Grep (read-only)
TriggerManual
InputSourceDescription
Feature nameCLI argument or auto-detectedIdentifies the feature under audit
Files in scopeTask file lists and code diffsFull content of each file, presented under path headings
Product specspecs/product/<feature>.mdIntended behavior context
Tech specspecs/tech/<feature>.mdIntended data flows and auth model
Project conventionsCLAUDE.mdNaming, architecture, and patterns
OutputLocationDescription
Security audit report.speed/features/<feature>/security-audit.jsonFindings with OWASP categories, severity, file locations, recommendations
Defect specs (optional)specs/defects/Generated when --create-defects flag is used, for findings above severity threshold
  1. Work through each file in scope. For every file, check against each OWASP category below. Flag a finding only when direct evidence exists in the code.

  2. Exclude test files. Do not flag test fixtures, test helpers, or files under tests/, test/, or __tests__/ directories. Do not flag example files or intentional security test cases.

  3. Report findings. Each finding includes a sequential ID (SEC-NNN), severity, OWASP category, file path, line number, code snippet (max 200 chars), and a concrete recommendation naming the specific function, library, or pattern to use.

CategoryIDFocus
Broken Access ControlA01Missing auth on route handlers, authorization gaps, direct object references without ownership checks
Cryptographic FailuresA02MD5/SHA1 for security purposes, hardcoded keys/secrets, unencrypted sensitive data, deprecated ciphers (ECB, RC4, DES)
InjectionA03SQL via string concatenation, OS commands via user input to subprocess/exec/eval, NoSQL injection, LDAP injection
Security MisconfigurationA05Debug mode in production paths, wildcard CORS on authenticated routes, default credentials, verbose error responses leaking internals
Auth FailuresA07Passwords without slow hash (bcrypt/argon2/scrypt), session fixation, missing rate limiting on login/reset, JWT alg: none
SSRFA10User-controlled URLs passed to HTTP clients, redirect endpoints following caller-supplied URLs, server-side URL fetchers
SeverityCriteriaPriority
criticalRemote code execution, auth bypass with no preconditions, secrets in public codeP0
highSQL injection, missing auth on sensitive ops, hardcoded credentials, SSRF with no filteringP1
mediumWeak crypto for security purposes, verbose error disclosure, missing rate limiting on authP2
lowDebug flags in non-production paths, permissive CORS on non-sensitive routesP3
infoBest-practice deviations with negligible exploitabilityInformational

The security audit pipeline collects all feature files, presents them with full source content, and runs the agent with a structured output template.

speed security [--create-defects]
├─ 1. Resolve feature name
├─ 2. Assemble audit context
│ ├─ Collect files_touched from all task JSON files
│ ├─ Load full file contents from worktree
│ ├─ Load product spec + tech spec
│ └─ Load CLAUDE.md conventions
├─ 3. Send to Security Auditor agent (Sonnet, read-only)
│ └─ Uses structured output template
├─ 4. Parse JSON findings
│ └─ Save to .speed/features/<feature>/logs/security-audit.json
└─ 5. (Optional) Generate defect specs
└─ For findings above severity threshold

security_audit_run (lib/security.sh:333-380) takes the feature name as argument. The feature’s task directory must exist at .speed/features/<feature>/tasks/.

context_assemble_security_auditor (lib/context_bridge.sh:672-750) runs a Python script that:

  1. Scans all task JSON files under the feature’s tasks/ directory
  2. Collects the union of files_touched from every task
  3. Reads full file contents from the project root (skips files that don’t exist)
  4. Loads product spec from specs/product/<feature>.md
  5. Loads tech spec from specs/tech/<feature>.md
  6. Loads project conventions from CLAUDE.md

The assembled prompt presents each file’s full content under path headings:

# Security Audit: <feature>
## Files in scope
<N> file(s) in scope.
### src/backend/services/borrow.py
\```
<full file content>
\```
## Product spec
<product spec content>
## Tech spec
<tech spec content>
## Project conventions
<CLAUDE.md content>

Lines 356-365 call provider_run_json with the security-auditor agent definition, Sonnet model, a structured output template (templates/security-audit-output.json), a maximum of 10 turns, and a 900-second timeout. The agent has read-only tools (Read, Glob, Grep) for additional investigation.

Lines 370-379 parse the agent output via parse_agent_json and write the result to security-audit.json in the feature’s logs directory.

When --create-defects is passed, findings above the configured severity threshold are converted to defect spec files in specs/defects/, formatted for the defect pipeline.

A user-auth feature with 4 tasks producing 6 files. The Borrowing service contains a SQL query built with string concatenation.

# Security Audit: user-auth
## Files in scope
6 file(s) in scope.
### src/backend/services/auth.py
def authenticate(username, password):
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
result = db.execute(query)
...
### src/backend/routes/login.py
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
user = authenticate(username, password)
...
{
"feature": "user-auth",
"timestamp": "2024-01-15T10:30:00Z",
"agent_model": "sonnet",
"findings": [
{
"id": "SEC-001",
"severity": "high",
"category": "injection",
"title": "SQL injection via string concatenation in authenticate()",
"description": "User-supplied username and password are interpolated directly into a SQL query string using f-string formatting. An attacker can inject arbitrary SQL.",
"file": "src/backend/services/auth.py",
"line": 3,
"snippet": "query = f\"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'\"",
"recommendation": "Use parameterized queries: db.execute('SELECT * FROM users WHERE username = ? AND password = ?', (username, password))"
},
{
"id": "SEC-002",
"severity": "medium",
"category": "auth",
"title": "Plain-text password comparison",
"description": "Password is compared directly against the database value without hashing. Passwords should be hashed with bcrypt or argon2.",
"file": "src/backend/services/auth.py",
"line": 3,
"snippet": "AND password = '{password}'",
"recommendation": "Store password hashes using bcrypt.hashpw(). Compare with bcrypt.checkpw() instead of SQL equality."
}
],
"summary": {
"total": 2,
"by_severity": {"critical": 0, "high": 1, "medium": 1, "low": 0, "info": 0}
}
}
Running Security Auditor agent on feature 'user-auth'...
Security Audit: 2 finding(s)
HIGH SEC-001 SQL injection via string concatenation in authenticate()
src/backend/services/auth.py:3
MEDIUM SEC-002 Plain-text password comparison
src/backend/services/auth.py:3
Report saved to .speed/features/user-auth/logs/security-audit.json
  • Read-only access. No writes, no shell commands, no code execution, no network requests.
  • Investigation only. Every action must be a file read (Read, Glob, or Grep).
  • Flag only when direct evidence exists in the code. Do not flag suspected issues elsewhere.
  • When in doubt about whether something is a test fixture, read surrounding code for context.
{
"feature": "feature-name",
"timestamp": "ISO 8601",
"agent_model": "sonnet",
"findings": [
{
"id": "SEC-001",
"severity": "critical | high | medium | low | info",
"category": "injection | auth | secrets | crypto | config | xss | path-traversal | info-disclosure",
"title": "Short description",
"description": "Detailed explanation",
"file": "relative/path/to/file.py",
"line": 42,
"snippet": "vulnerable code (max 200 chars)",
"recommendation": "Concrete fix naming the function/library/pattern to use"
}
],
"summary": {
"total": 0,
"by_severity": { "critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0 }
}
}