factory-ai/security-review
Scan code changes for security vulnerabilities using STRIDE threat modeling, validate findings for exploitability, and output structured results for downstream patch generation. Supports PR review, scheduled scans, and full repository audits.
npx skills add https://github.com/Factory-AI/factory-plugins --skill security-review
You are a senior security engineer conducting a focused security review using LLM-powered reasoning and STRIDE threat modeling. This skill scans code for vulnerabilities and validates findings for exploitability.
@droid security in PR comments.factory/threat-model.md (auto-generated if missing via threat-model-generation skill)┌──────────────────────┐
│ threat-model- │ ← Generates STRIDE threat model
│ generation │
└─────────┬────────────┘
↓ .factory/threat-model.md
┌──────────────────────┐
│ security-review │ ← THIS SKILL (scan + validate)
│ (commit-scan + │
│ validation) │
└─────────┬────────────┘
↓ validated-findings.json
┌──────────────────────┐
│ security-patch- │ ← Generates fixes
│ generation │
└──────────────────────┘
| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| Mode | pr, weekly, full, staged, commit-range | No | pr (auto-detected) |
| Base branch | Branch to diff against | No | Auto-detected from PR |
| CVE lookback | How far back to check dependency CVEs | No | 12 months |
| Severity threshold | Minimum severity to report | No | medium |
# Check if threat model exists
if [ -f ".factory/threat-model.md" ]; then
echo "Threat model found"
# Check age
LAST_MODIFIED=$(stat -f %m .factory/threat-model.md 2>/dev/null || stat -c %Y .factory/threat-model.md)
DAYS_OLD=$(( ($(date +%s) - $LAST_MODIFIED) / 86400 ))
if [ $DAYS_OLD -gt 90 ]; then
echo "WARNING: Threat model is $DAYS_OLD days old. Consider regenerating."
fi
else
echo "No threat model found. Generate one first using threat-model-generation skill."
fi
If missing:
If outdated (>90 days):
# PR mode - scan PR diff
git diff --name-only origin/HEAD...
git diff --merge-base origin/HEAD
# Weekly mode - last 7 days on default branch
git log --since="7 days ago" --name-only --pretty=format: | sort -u
# Full mode - entire repository
find . -type f \( -name "*.js" -o -name "*.ts" -o -name "*.py" -o -name "*.go" -o -name "*.java" \) | head -500
# Staged mode - staged changes only
git diff --staged --name-only
Document:
Load the threat model and scan code for vulnerabilities in each STRIDE category:
Look for:
Look for:
Look for:
Look for:
Look for:
Look for:
# SQL Injection (Tampering)
sql = f"SELECT * FROM users WHERE id = {user_id}" # VULNERABLE
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,)) # SAFE
# Command Injection (Tampering)
os.system(f"ping {user_input}") # VULNERABLE
subprocess.run(["ping", "-c", "1", user_input]) # SAFE
# XSS (Tampering)
element.innerHTML = userInput; // VULNERABLE
element.textContent = userInput; // SAFE
# IDOR (Information Disclosure)
def get_doc(doc_id):
return Doc.query.get(doc_id) # VULNERABLE - no ownership check
# Path Traversal (Tampering)
file_path = f"/uploads/{user_filename}" # VULNERABLE
filename = os.path.basename(user_input) # SAFE
Scan dependencies for known CVEs:
# Node.js
npm audit --json 2>/dev/null
# Python
pip-audit --format json 2>/dev/null
# Go
govulncheck -json ./... 2>/dev/null
# Rust
cargo audit --json 2>/dev/null
For each vulnerability:
REACHABLE, POTENTIALLY_REACHABLE, NOT_REACHABLEOutput security-findings.json:
{
"scan_id": "scan-<timestamp>",
"scan_date": "<ISO timestamp>",
"scan_mode": "pr | weekly | full",
"commit_range": "abc123..def456",
"threat_model_version": "1.0.0",
"findings": [
{
"id": "VULN-001",
"severity": "HIGH",
"stride_category": "Tampering",
"vulnerability_type": "SQL Injection",
"cwe": "CWE-89",
"file": "src/api/users.js",
"line_range": "45-49",
"code_context": "const sql = `SELECT * FROM users WHERE name LIKE '%${query}%'`",
"analysis": "User input from query parameter directly interpolated into SQL query without parameterization.",
"exploit_scenario": "Attacker submits: test' OR '1'='1 to bypass search filter and retrieve all users.",
"threat_model_reference": "Section 5.2 - SQL Injection",
"recommended_fix": "Use parameterized queries: db.query('SELECT * FROM users WHERE name LIKE $1', [`%${query}%`])",
"confidence": "HIGH"
}
],
"dependency_findings": [
{
"id": "DEP-001",
"package": "lodash",
"version": "4.17.20",
"ecosystem": "npm",
"vulnerability_id": "CVE-2021-23337",
"severity": "HIGH",
"cvss": 7.2,
"fixed_version": "4.17.21",
"reachability": "REACHABLE",
"reachability_evidence": "lodash.template() called in src/utils/email.js:15"
}
],
"summary": {
"total_findings": 5,
"by_severity": {"CRITICAL": 0, "HIGH": 2, "MEDIUM": 2, "LOW": 1},
"by_stride": {
"Spoofing": 0,
"Tampering": 2,
"Repudiation": 0,
"InfoDisclosure": 2,
"DoS": 0,
"ElevationOfPrivilege": 1
}
}
}
For each finding, assess exploitability:
HARD EXCLUSIONS - Automatically exclude:
10. Log injection/spoofing concerns
11. SSRF that only controls path (not host/protocol)
12. User-controlled content in AI prompts
13. ReDoS without demonstrated impact
14. Findings in documentation files
15. Missing audit logs (informational only)
PRECEDENTS:
dangerouslySetInnerHTML or bypassSecurityTrustHtmlOnly report findings with confidence >= 0.8
For CONFIRMED HIGH/CRITICAL findings, generate minimal PoC:
{
"proof_of_concept": {
"payload": "' OR '1'='1",
"request": "GET /api/users?search=test%27%20OR%20%271%27%3D%271",
"expected_behavior": "Returns users matching 'test'",
"actual_behavior": "Returns ALL users due to SQL injection"
}
}
Output validated-findings.json:
{
"validation_id": "val-<timestamp>",
"validation_date": "<ISO timestamp>",
"scan_id": "scan-<timestamp>",
"threat_model_path": ".factory/threat-model.md",
"validated_findings": [
{
"id": "VULN-001",
"original_severity": "HIGH",
"validated_severity": "HIGH",
"status": "CONFIRMED",
"stride_category": "Tampering",
"vulnerability_type": "SQL Injection",
"cwe": "CWE-89",
"exploitability": "EASY",
"reachability": "EXTERNAL",
"file": "src/api/users.js",
"line": 45,
"existing_mitigations": [],
"exploitation_path": [
"User submits search query via GET /api/users?search=<payload>",
"Express parses query string without validation",
"Query passed directly to SQL template literal",
"Database executes malicious SQL"
],
"proof_of_concept": {
"payload": "' OR '1'='1",
"request": "GET /api/users?search=test%27%20OR%20%271%27%3D%271",
"expected_behavior": "Returns users matching search",
"actual_behavior": "Returns all users"
},
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N",
"cvss_score": 9.1,
"recommendation": "Use parameterized queries",
"references": [
"https://cwe.mitre.org/data/definitions/89.html",
"https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html"
]
}
],
"false_positives": [
{
"id": "VULN-003",
"reason": "Input validated by Joi schema in middleware before reaching this endpoint",
"evidence": "Validation in src/middleware/validate.js:12"
}
],
"dependency_findings": [
{
"id": "DEP-001",
"status": "CONFIRMED",
"package": "lodash",
"version": "4.17.20",
"vulnerability_id": "CVE-2021-23337",
"severity": "HIGH",
"reachability": "REACHABLE",
"reachability_evidence": "lodash.template() called in src/utils/email.js:15",
"fixed_version": "4.17.21"
}
],
"summary": {
"total_scanned": 8,
"confirmed": 5,
"false_positives": 3,
"by_severity": {
"critical": 1,
"high": 2,
"medium": 1,
"low": 1
},
"by_stride": {
"Spoofing": 0,
"Tampering": 3,
"Repudiation": 0,
"InfoDisclosure": 1,
"DoS": 0,
"ElevationOfPrivilege": 1
}
}
}
For each finding, post inline PR comment:
🔴 **CRITICAL: SQL Injection (CWE-89)**
**STRIDE Category:** Tampering
**Confidence:** High
**File:** `src/api/users.js:45-49`
**Analysis:**
User input from `req.query.search` is directly interpolated into SQL query without parameterization.
**Suggested Fix:**
SELECT * FROM users WHERE name LIKE '%${search}%';+ const query = SELECT * FROM users WHERE name LIKE $1;
+ const results = await db.query(query, [%${search}%]);
[CWE-89: SQL Injection](https://cwe.mitre.org/data/definitions/89.html)
Post summary tracking comment:
## 🔒 Security Review Summary
| Severity | Count |
|----------|-------|
| 🔴 Critical | 1 |
| 🟠 High | 2 |
| 🟡 Medium | 3 |
| 🔵 Low | 0 |
### Findings
| ID | Severity | Type | File | Status |
|----|----------|------|------|--------|
| VULN-001 | Critical | SQL Injection | src/api/users.js:45 | Action required |
| VULN-002 | High | XSS | src/components/Comment.tsx:23 | Suggested fix |
---
*Reply `@droid dismiss VULN-XXX reason: <explanation>` to acknowledge a finding.*
Create branch: droid/security-report-{YYYY-MM-DD}
PR Title: fix(security): Security scan report - {date} ({N} findings)
Include:
.factory/security/reports/security-report-{YYYY-MM-DD}.mdvalidated-findings.json.factory/threat-model.md (if regenerated)| Severity | PR Mode | Weekly/Full Mode |
|----------|---------|------------------|
| CRITICAL | REQUEST_CHANGES - blocks merge | Create HIGH priority issue, notify security team |
| HIGH | REQUEST_CHANGES (configurable) | Create issue, require review |
| MEDIUM | COMMENT only | Create issue |
| LOW | COMMENT only | Include in report |
| Severity | Criteria | Examples |
|----------|----------|----------|
| CRITICAL | Immediately exploitable, high impact, no auth required | RCE, hardcoded production secrets, auth bypass |
| HIGH | Exploitable with some conditions, significant impact | SQL injection, stored XSS, IDOR |
| MEDIUM | Requires specific conditions, moderate impact | Reflected XSS, CSRF, info disclosure |
| LOW | Difficult to exploit, low impact | Verbose errors, missing security headers |
| STRIDE Category | Vulnerability Types |
|-----------------|---------------------|
| Spoofing | Weak auth, session hijacking, token exposure, credential stuffing |
| Tampering | SQL injection, XSS, command injection, mass assignment, path traversal |
| Repudiation | Missing audit logs, insufficient logging |
| Info Disclosure | IDOR, verbose errors, hardcoded secrets, data leaks |
| DoS | Missing rate limits, resource exhaustion, ReDoS |
| Elevation of Privilege | Missing authz, role manipulation, RBAC bypass |
validated-findings.json generatedPR security review:
Scan PR #123 for security vulnerabilities.
Manual trigger in PR:
@droid security
Full repository scan:
@droid security --full
Weekly scan (last 7 days):
Scan commits from the last 7 days on main for security vulnerabilities.
Scan and patch:
Run full security analysis on PR #123: scan, validate, and generate patches.
.factory/
├── threat-model.md # STRIDE threat model
├── security-config.json # Configuration
└── security/
├── acknowledged.json # Dismissed findings
└── reports/
└── security-report-{date}.md
PR Mode - Reply to inline comment:
@droid dismiss reason: Input is validated by Joi schema in middleware
Weekly/Full Mode - Comment on report PR:
@droid dismiss VULN-007 reason: Accepted risk for internal admin tool
Dismissed findings stored in .factory/security/acknowledged.json.
Cannot detect:
May not fully validate:
Take factory-ai/security-review from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.