
Code Review Security
- 399 installs
- 8 repo stars
- Updated February 6, 2026
- hieutrtr/ai1-skills
code-review-security is an agent skill that runs AST-based Python security scans before merge or release for developers who need to catch eval/exec, injection, and hardcoded secret patterns with severitized JSON output.
About
code-review-security is an agent skill from hieutrtr/ai1-skills built around security-scan.py, an AST-based scanner for common Python vulnerability patterns. The script scans directories for eval(), exec(), compile(), subprocess with shell=True, pickle.loads() on untrusted data, raw SQL f-string construction, yaml.load() without SafeLoader, hardcoded API keys and passwords, weak MD5/SHA1 password hashes, and os.system() calls. Run with `python security-scan.py --path ./app --output-dir ./security-results` and optional `--severity high` filtering. Developers reach for code-review-security before merge when Python services need automated secret and injection detection with JSON severity output.
- AST-based security-scan.py for Python codebases
- Detects eval/exec/compile, subprocess shell=True, and os.system usage
- Flags pickle.loads, unsafe yaml.load, and raw SQL f-string construction
- Hardcoded secret and weak hash (MD5/SHA1) pattern rules with CWE metadata
- CLI emits JSON findings with configurable minimum severity filter
Code Review Security by the numbers
- 399 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #557 of 2,203 Security skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/hieutrtr/ai1-skills --skill code-review-securityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 399 |
|---|---|
| repo stars | ★ 8 |
| Security audit | 2 / 3 scanners passed |
| Last updated | February 6, 2026 |
| Repository | hieutrtr/ai1-skills ↗ |
How do you scan Python code for security vulnerabilities?
Run AST-based Python security scans before merge or release to catch eval/exec, injection, and secret patterns with severitized JSON output.
Who is it for?
Python backend developers who need AST-based pre-merge security scans with severitized JSON output before release.
Skip if: Non-Python codebases or teams needing full DAST or infrastructure penetration testing should skip code-review-security.
When should I use this skill?
A developer wants to security-scan Python source before merge, filter findings by severity, or detect hardcoded secrets and injection patterns.
What you get
Severitized JSON security scan results listing eval/exec, injection, secret, and weak-hash findings across scanned Python files.
- Severitized JSON scan report
- Per-file vulnerability findings
By the numbers
- Scans 9 Python vulnerability pattern categories including eval/exec, subprocess shell=True, and hardcoded secrets
- Supports --severity filtering and JSON output via security-scan.py CLI flags
Files
Code Review Security
When to Use
Activate this skill when:
- Reviewing pull requests for security vulnerabilities
- Auditing authentication or authorization code changes
- Reviewing code that handles user input, file uploads, or external data
- Checking for OWASP Top 10 vulnerabilities in new features
- Validating that secrets are not committed to the repository
- Scanning dependencies for known vulnerabilities
- Reviewing API endpoints that expose sensitive data
Output: Write findings to security-review.md with severity, file:line, description, and recommendations.
Do NOT use this skill for:
- Deployment infrastructure security (use
docker-best-practices) - Incident response procedures (use
incident-response) - General code quality review without security focus (use
pre-merge-checklist) - Writing implementation code (use
python-backend-expertorreact-frontend-expert)
Instructions
OWASP Top 10 Checklist
Review every PR against the OWASP Top 10 (2021 edition). Each category below includes specific checks for Python/FastAPI and React codebases.
---
A01: Broken Access Control
What to look for:
- Missing authorization checks on endpoints
- Direct object reference without ownership verification
- Endpoints that expose data without role-based filtering
- Missing
Depends()for auth on new routes
Python/FastAPI checks:
# BAD: No authorization check -- any authenticated user can access any user
@router.get("/users/{user_id}")
async def get_user(user_id: int, db: Session = Depends(get_db)):
return await user_repo.get(user_id)
# GOOD: Verify the requesting user owns the resource or is admin
@router.get("/users/{user_id}")
async def get_user(
user_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
if current_user.id != user_id and current_user.role != "admin":
raise HTTPException(status_code=403, detail="Forbidden")
return await user_repo.get(user_id)Review checklist:
- [ ] Every route has authentication (
Depends(get_current_user)) - [ ] Resource access is verified against the requesting user
- [ ] Admin-only endpoints check
role == "admin" - [ ] List endpoints filter by user ownership (unless admin)
- [ ] No IDOR (Insecure Direct Object Reference) vulnerabilities
---
A02: Cryptographic Failures
What to look for:
- Passwords stored in plaintext or with weak hashing
- Sensitive data in logs or error messages
- Hardcoded secrets, API keys, or tokens
- Weak JWT configuration
Python checks:
# BAD: Weak password hashing
import hashlib
password_hash = hashlib.md5(password.encode()).hexdigest()
# GOOD: Use bcrypt via passlib
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
password_hash = pwd_context.hash(password)
# BAD: Secret in code
SECRET_KEY = "my-super-secret-key-123"
# GOOD: Secret from environment
SECRET_KEY = os.environ["SECRET_KEY"]Review checklist:
- [ ] Passwords hashed with bcrypt (never MD5, SHA1, or plaintext)
- [ ] JWT secret loaded from environment, not hardcoded
- [ ] Sensitive data excluded from logs (passwords, tokens, PII)
- [ ] HTTPS enforced for all external communication
- [ ] No secrets in source code (check
.env.examplehas placeholders only)
---
A03: Injection
What to look for:
- Raw SQL queries with string interpolation
eval(),exec(),compile()with user inputsubprocesscalls withshell=True- Template injection
Python checks:
# BAD: SQL injection via string formatting
query = f"SELECT * FROM users WHERE email = '{email}'"
db.execute(text(query))
# GOOD: Parameterized query
db.execute(text("SELECT * FROM users WHERE email = :email"), {"email": email})
# GOOD: SQLAlchemy ORM (always parameterized)
user = db.query(User).filter(User.email == email).first()
# BAD: Command injection
subprocess.run(f"convert {filename}", shell=True)
# GOOD: Pass arguments as a list
subprocess.run(["convert", filename], shell=False)
# BAD: Code execution with user input
result = eval(user_input)
# GOOD: Never eval user input. Use ast.literal_eval for safe parsing.
result = ast.literal_eval(user_input) # Only for literal structuresReview checklist:
- [ ] No raw SQL with string interpolation (use ORM or parameterized queries)
- [ ] No
eval(),exec(), orcompile()with external input - [ ] No
subprocess.run(..., shell=True)with dynamic arguments - [ ] No
pickle.loads()on untrusted data - [ ] All user input validated by Pydantic schemas before use
---
A04: Insecure Design
What to look for:
- Missing rate limiting on authentication endpoints
- No account lockout after failed login attempts
- Missing CAPTCHA on public-facing forms
- Business logic flaws (e.g., negative amounts, self-privilege-escalation)
Review checklist:
- [ ] Rate limiting on login, registration, and password reset
- [ ] Account lockout or exponential backoff after 5+ failed attempts
- [ ] Business logic validates constraints (positive amounts, valid transitions)
- [ ] Sensitive operations require re-authentication
---
A05: Security Misconfiguration
What to look for:
- Debug mode enabled in production
- CORS configured with wildcard
*origins - Default credentials or admin accounts
- Verbose error messages exposing stack traces
Python/FastAPI checks:
# BAD: Wide-open CORS
app.add_middleware(CORSMiddleware, allow_origins=["*"])
# GOOD: Explicit allowed origins
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
# BAD: Debug mode in production
app = FastAPI(debug=True)
# GOOD: Debug only in development
app = FastAPI(debug=settings.DEBUG) # DEBUG=False in productionReview checklist:
- [ ] CORS origins are explicit (no wildcard in production)
- [ ] Debug mode disabled in production configuration
- [ ] Error responses do not expose stack traces or internal details
- [ ] Default admin credentials are changed or removed
- [ ] Security headers set (X-Content-Type-Options, X-Frame-Options, etc.)
---
A06: Vulnerable and Outdated Components
Review checklist:
- [ ] No known CVEs in Python dependencies (
pip-auditorsafety check) - [ ] No known CVEs in npm dependencies (
npm audit) - [ ] Dependencies pinned to specific versions in lock files
- [ ] No deprecated packages still in use
---
A07: Identification and Authentication Failures
What to look for:
- Weak password policies
- Session tokens that do not expire
- Missing multi-factor authentication for admin actions
- JWT tokens without expiration
Python checks:
# BAD: JWT without expiration
token = jwt.encode({"sub": user_id}, SECRET_KEY, algorithm="HS256")
# GOOD: JWT with expiration
token = jwt.encode(
{"sub": user_id, "exp": datetime.utcnow() + timedelta(minutes=30)},
SECRET_KEY,
algorithm="HS256",
)Review checklist:
- [ ] JWT tokens have expiration (
expclaim) - [ ] Refresh tokens are stored securely and can be revoked
- [ ] Password policy enforces minimum length (12+) and complexity
- [ ] Session invalidation on password change or logout
- [ ] No user enumeration via login error messages
---
A08: Software and Data Integrity Failures
Review checklist:
- [ ] CI/CD pipeline validates artifact integrity
- [ ] No unsigned or unverified packages
- [ ] Deserialization of untrusted data uses safe methods (no
pickle.loads) - [ ] Database migrations are reviewed before execution
---
A09: Security Logging and Monitoring Failures
Review checklist:
- [ ] Authentication events are logged (login, logout, failed attempts)
- [ ] Authorization failures are logged with context
- [ ] Sensitive data is NOT included in logs (passwords, tokens, PII)
- [ ] Log entries include timestamp, user ID, IP address, action
- [ ] Alerting configured for suspicious patterns (brute force, unusual access)
---
A10: Server-Side Request Forgery (SSRF)
What to look for:
- User-supplied URLs used in server-side requests
- Redirect endpoints that accept arbitrary URLs
Python checks:
# BAD: Fetch arbitrary URL from user input
url = request.query_params["url"]
response = httpx.get(url) # SSRF: can access internal services
# GOOD: Validate URL against allowlist
ALLOWED_HOSTS = {"api.example.com", "cdn.example.com"}
parsed = urlparse(url)
if parsed.hostname not in ALLOWED_HOSTS:
raise HTTPException(400, "URL not allowed")
response = httpx.get(url)Review checklist:
- [ ] No server-side requests to user-controlled URLs without validation
- [ ] URL allowlists used for external integrations
- [ ] Internal service URLs not exposed in error messages
---
Python-Specific Security Checks
Beyond OWASP, review Python code for these patterns:
| Pattern | Risk | Fix |
|---|---|---|
eval(user_input) | Remote code execution | Remove or use ast.literal_eval |
pickle.loads(data) | Arbitrary code execution | Use JSON or msgpack |
subprocess.run(cmd, shell=True) | Command injection | Pass args as list, shell=False |
yaml.load(data) | Code execution | Use yaml.safe_load(data) |
os.system(cmd) | Command injection | Use subprocess.run([...]) |
| Raw SQL strings | SQL injection | Use ORM or parameterized queries |
hashlib.md5(password) | Weak hashing | Use bcrypt via passlib |
jwt.decode(token, options={"verify_signature": False}) | Auth bypass | Always verify signature |
open(user_path) | Path traversal | Validate path, use pathlib.resolve() |
tempfile.mktemp() | Race condition | Use tempfile.mkstemp() |
React-Specific Security Checks
| Pattern | Risk | Fix |
|---|---|---|
dangerouslySetInnerHTML | XSS | Use text content or sanitize with DOMPurify |
javascript: in href | XSS | Validate URLs, allow only https: |
window.location = userInput | Open redirect | Validate against allowlist |
| Storing tokens in localStorage | Token theft via XSS | Use httpOnly cookies |
| Inline event handlers from data | XSS | Use React event handlers |
eval() or Function() | Code execution | Remove entirely |
| Rendering user HTML | XSS | Use a sanitization library |
React code review:
// BAD: XSS via dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: userBio }} />
// GOOD: Sanitize first, or use text content
import DOMPurify from "dompurify";
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userBio) }} />
// BETTER: Use text content when HTML is not needed
<p>{userBio}</p>
// BAD: javascript: URL
<a href={userLink}>Click</a> // userLink could be "javascript:alert(1)"
// GOOD: Validate protocol
const safeHref = /^https?:\/\//.test(userLink) ? userLink : "#";
<a href={safeHref}>Click</a>Severity Classification
Classify each finding by severity for prioritization:
| Severity | Description | Examples | SLA |
|---|---|---|---|
| Critical | Exploitable remotely, no auth needed, data breach | SQL injection, RCE, auth bypass | Block merge, fix immediately |
| High | Exploitable with auth, privilege escalation | IDOR, broken access control, XSS (stored) | Block merge, fix before release |
| Medium | Requires specific conditions to exploit | CSRF, XSS (reflected), open redirect | Fix within sprint |
| Low | Defense-in-depth, informational | Missing headers, verbose errors | Fix when convenient |
| Info | Best practice recommendations | Dependency updates, code style | Track in backlog |
Finding Report Format
When reporting security findings, use this format for consistency:
## Security Finding: [Title]
**Severity:** Critical | High | Medium | Low | Info
**Category:** OWASP A01-A10 or custom category
**File:** path/to/file.py:42
**CWE:** CWE-89 (if applicable)
### Description
Brief description of the vulnerability and its impact.
### Vulnerable CodeThe problematic code
vulnerable_function(user_input)
### Recommended FixThe secure alternative
safe_function(sanitize(user_input))
### Impact
What an attacker could achieve by exploiting this vulnerability.
### References
- Link to relevant OWASP page
- Link to relevant CWE entryAutomated Scanning
Use scripts/security-scan.py to perform AST-based scanning for common vulnerability patterns in Python code. The script scans for:
eval()/exec()/compile()callssubprocesswithshell=Truepickle.loads()on potentially untrusted data- Raw SQL string construction
yaml.load()withoutLoader=SafeLoader- Hardcoded secret patterns (API keys, passwords)
- Weak hash functions (MD5, SHA1 for passwords)
Run: python scripts/security-scan.py --path ./app --output-dir ./security-results
Dependency scanning (run separately):
# Python dependencies
pip-audit --requirement requirements.txt --output json > dep-audit.json
# npm dependencies
npm audit --json > npm-audit.jsonExamples
Example Review Comment (Critical)
SECURITY: SQL Injection (Critical, OWASP A03)
>
File: app/repositories/user_repository.py:47>
```python
query = f"SELECT * FROM users WHERE name LIKE '%{search_term}%'"
```
>
This constructs a raw SQL query with string interpolation, allowing SQL injection.
An attacker could input '; DROP TABLE users; -- to destroy data.>
Fix: Use SQLAlchemy ORM filtering:
```python
users = db.query(User).filter(User.name.ilike(f"%{search_term}%")).all()
```
Example Review Comment (Medium)
SECURITY: Missing Rate Limiting (Medium, OWASP A04)
>
File: app/routes/auth.py:12>
The /auth/login endpoint has no rate limiting. An attacker could perform brute-forcepassword attacks at unlimited speed.
>
Fix: Add rate limiting middleware:
```python
from slowapi import Limiter
limiter = Limiter(key_func=get_remote_address)
>
@router.post("/login")
@limiter.limit("5/minute")
async def login(request: Request, ...):
```
Output File
Write security findings to security-review.md:
# Security Review: [Feature/PR Name]
## Summary
- Critical: 0 | High: 1 | Medium: 2 | Low: 1
## Findings
### [CRITICAL] SQL Injection in user search
- **File:** app/routes/users.py:45
- **OWASP:** A03 Injection
- **Description:** Raw SQL with string interpolation
- **Recommendation:** Use SQLAlchemy ORM filtering
### [HIGH] Missing authorization check
...
## Passed Checks
- No hardcoded secrets found
- Dependencies up to date#!/usr/bin/env python3
"""
security-scan.py — AST-based security scanner for common Python vulnerability patterns.
Scans Python source files for:
- eval() / exec() / compile() calls
- subprocess with shell=True
- pickle.loads() on potentially untrusted data
- Raw SQL string construction (f-strings with SELECT/INSERT/UPDATE/DELETE)
- yaml.load() without SafeLoader
- Hardcoded secret patterns (API keys, passwords in source)
- Weak hash functions (MD5, SHA1 for passwords)
- os.system() calls
Usage:
python security-scan.py --path ./app --output-dir ./security-results
python security-scan.py --path ./app --output-dir ./results --severity high
Options:
--path Directory or file to scan (required)
--output-dir Directory to write JSON results (default: ./security-results)
--severity Minimum severity to report: critical, high, medium, low (default: low)
"""
import argparse
import ast
import json
import os
import re
import sys
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
# ─── Data Structures ─────────────────────────────────────────────────────────────
SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
@dataclass
class Finding:
"""A single security finding."""
rule_id: str
severity: str
category: str
message: str
file: str
line: int
col: int
snippet: str
cwe: Optional[str] = None
# ─── AST-Based Rules ─────────────────────────────────────────────────────────────
class SecurityVisitor(ast.NodeVisitor):
"""AST visitor that checks for common security anti-patterns."""
def __init__(self, filepath: str, source_lines: list[str]):
self.filepath = filepath
self.source_lines = source_lines
self.findings: list[Finding] = []
def _get_snippet(self, lineno: int) -> str:
"""Get the source line for a finding."""
if 1 <= lineno <= len(self.source_lines):
return self.source_lines[lineno - 1].strip()
return ""
def _add_finding(
self,
rule_id: str,
severity: str,
category: str,
message: str,
node: ast.AST,
cwe: Optional[str] = None,
):
self.findings.append(Finding(
rule_id=rule_id,
severity=severity,
category=category,
message=message,
file=self.filepath,
line=getattr(node, "lineno", 0),
col=getattr(node, "col_offset", 0),
snippet=self._get_snippet(getattr(node, "lineno", 0)),
cwe=cwe,
))
def visit_Call(self, node: ast.Call):
"""Check function calls for dangerous patterns."""
func_name = self._get_func_name(node)
# Rule: eval / exec / compile
if func_name in ("eval", "exec", "compile"):
self._add_finding(
rule_id="SEC001",
severity="critical",
category="OWASP A03: Injection",
message=f"Use of {func_name}() can lead to code execution. "
f"Remove or use ast.literal_eval() for safe parsing.",
node=node,
cwe="CWE-95",
)
# Rule: pickle.loads / pickle.load
if func_name in ("pickle.loads", "pickle.load"):
self._add_finding(
rule_id="SEC002",
severity="critical",
category="OWASP A08: Software and Data Integrity",
message="pickle.loads() can execute arbitrary code on untrusted data. "
"Use JSON or msgpack for deserialization.",
node=node,
cwe="CWE-502",
)
# Rule: os.system
if func_name == "os.system":
self._add_finding(
rule_id="SEC003",
severity="high",
category="OWASP A03: Injection",
message="os.system() is vulnerable to command injection. "
"Use subprocess.run([...], shell=False) instead.",
node=node,
cwe="CWE-78",
)
# Rule: subprocess with shell=True
if func_name in ("subprocess.run", "subprocess.call", "subprocess.Popen",
"subprocess.check_output", "subprocess.check_call"):
for kw in node.keywords:
if kw.arg == "shell" and isinstance(kw.value, ast.Constant) and kw.value.value is True:
self._add_finding(
rule_id="SEC004",
severity="high",
category="OWASP A03: Injection",
message=f"{func_name}() with shell=True is vulnerable to "
f"command injection. Use shell=False and pass args as a list.",
node=node,
cwe="CWE-78",
)
# Rule: yaml.load without SafeLoader
if func_name == "yaml.load":
has_safe_loader = False
for kw in node.keywords:
if kw.arg == "Loader":
if isinstance(kw.value, ast.Attribute) and "Safe" in kw.value.attr:
has_safe_loader = True
elif isinstance(kw.value, ast.Name) and "Safe" in kw.value.id:
has_safe_loader = True
if not has_safe_loader:
self._add_finding(
rule_id="SEC005",
severity="high",
category="OWASP A08: Software and Data Integrity",
message="yaml.load() without SafeLoader can execute arbitrary code. "
"Use yaml.safe_load() or yaml.load(data, Loader=yaml.SafeLoader).",
node=node,
cwe="CWE-502",
)
# Rule: hashlib.md5 / hashlib.sha1 (potential password hashing)
if func_name in ("hashlib.md5", "hashlib.sha1"):
self._add_finding(
rule_id="SEC006",
severity="medium",
category="OWASP A02: Cryptographic Failures",
message=f"{func_name}() is a weak hash function. "
f"If used for passwords, switch to bcrypt via passlib.",
node=node,
cwe="CWE-328",
)
self.generic_visit(node)
def visit_JoinedStr(self, node: ast.JoinedStr):
"""Check f-strings for potential SQL injection."""
# Reconstruct the f-string content to check for SQL keywords
string_parts = []
for value in node.values:
if isinstance(value, ast.Constant):
string_parts.append(str(value.value))
full_text = " ".join(string_parts).upper()
sql_keywords = ["SELECT ", "INSERT ", "UPDATE ", "DELETE ", "DROP ", "ALTER "]
if any(kw in full_text for kw in sql_keywords):
self._add_finding(
rule_id="SEC007",
severity="critical",
category="OWASP A03: Injection",
message="SQL query constructed with f-string interpolation. "
"This is vulnerable to SQL injection. Use parameterized queries.",
node=node,
cwe="CWE-89",
)
self.generic_visit(node)
def _get_func_name(self, node: ast.Call) -> str:
"""Extract the function name from a Call node."""
if isinstance(node.func, ast.Name):
return node.func.id
elif isinstance(node.func, ast.Attribute):
parts = []
current = node.func
while isinstance(current, ast.Attribute):
parts.append(current.attr)
current = current.value
if isinstance(current, ast.Name):
parts.append(current.id)
return ".".join(reversed(parts))
return ""
# ─── Regex-Based Rules (for patterns AST cannot catch) ────────────────────────
REGEX_RULES = [
{
"rule_id": "SEC008",
"severity": "high",
"category": "OWASP A02: Cryptographic Failures",
"message": "Potential hardcoded secret detected. Move secrets to environment variables.",
"cwe": "CWE-798",
"pattern": re.compile(
r"""(?:SECRET_KEY|API_KEY|PASSWORD|TOKEN|PRIVATE_KEY)\s*=\s*['"][^'"]{8,}['"]""",
re.IGNORECASE,
),
},
{
"rule_id": "SEC009",
"severity": "medium",
"category": "OWASP A09: Security Logging and Monitoring",
"message": "Potential sensitive data in log statement. Ensure passwords, tokens, "
"and PII are not logged.",
"cwe": "CWE-532",
"pattern": re.compile(
r"""(?:logger?\.|logging\.)(?:info|debug|warning|error)\(.*(?:password|token|secret|api_key)""",
re.IGNORECASE,
),
},
{
"rule_id": "SEC010",
"severity": "medium",
"category": "OWASP A07: Identification and Authentication",
"message": "JWT decode with signature verification disabled. Always verify JWT signatures.",
"cwe": "CWE-347",
"pattern": re.compile(
r"""jwt\.decode\(.*verify_signature.*False""",
re.IGNORECASE,
),
},
]
def regex_scan(filepath: str, source: str) -> list[Finding]:
"""Apply regex-based rules to source code."""
findings = []
lines = source.split("\n")
for rule in REGEX_RULES:
for i, line in enumerate(lines, start=1):
if rule["pattern"].search(line):
findings.append(Finding(
rule_id=rule["rule_id"],
severity=rule["severity"],
category=rule["category"],
message=rule["message"],
file=filepath,
line=i,
col=0,
snippet=line.strip(),
cwe=rule.get("cwe"),
))
return findings
# ─── Scanner ──────────────────────────────────────────────────────────────────
def scan_file(filepath: str) -> list[Finding]:
"""Scan a single Python file for security issues."""
try:
source = Path(filepath).read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as e:
print(f"WARNING: Could not read {filepath}: {e}", file=sys.stderr)
return []
findings = []
# AST-based scan
try:
tree = ast.parse(source, filename=filepath)
visitor = SecurityVisitor(filepath, source.split("\n"))
visitor.visit(tree)
findings.extend(visitor.findings)
except SyntaxError as e:
print(f"WARNING: Syntax error in {filepath}: {e}", file=sys.stderr)
# Regex-based scan
findings.extend(regex_scan(filepath, source))
return findings
def scan_directory(path: str) -> list[Finding]:
"""Recursively scan a directory for Python files."""
findings = []
scan_path = Path(path)
if scan_path.is_file():
if scan_path.suffix == ".py":
return scan_file(str(scan_path))
return []
for py_file in scan_path.rglob("*.py"):
# Skip common non-application directories
skip_dirs = {"__pycache__", ".venv", "venv", "node_modules", ".git", "migrations"}
if any(part in skip_dirs for part in py_file.parts):
continue
findings.extend(scan_file(str(py_file)))
return findings
# ─── Main ─────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="AST-based security scanner for Python code.",
)
parser.add_argument(
"--path",
required=True,
help="Directory or file to scan",
)
parser.add_argument(
"--output-dir",
default="./security-results",
help="Directory to write JSON results (default: ./security-results)",
)
parser.add_argument(
"--severity",
default="low",
choices=["critical", "high", "medium", "low", "info"],
help="Minimum severity to report (default: low)",
)
args = parser.parse_args()
# Validate path
if not Path(args.path).exists():
print(f"ERROR: Path does not exist: {args.path}", file=sys.stderr)
sys.exit(1)
# Scan
print(f"Scanning: {args.path}")
all_findings = scan_directory(args.path)
# Filter by severity
min_severity = SEVERITY_ORDER.get(args.severity, 3)
findings = [f for f in all_findings if SEVERITY_ORDER.get(f.severity, 4) <= min_severity]
# Sort by severity (critical first), then by file and line
findings.sort(key=lambda f: (SEVERITY_ORDER.get(f.severity, 4), f.file, f.line))
# Write results
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
output_file = output_dir / f"security-scan-{timestamp}.json"
report = {
"scan_timestamp": datetime.now(timezone.utc).isoformat(),
"scanned_path": str(Path(args.path).resolve()),
"min_severity": args.severity,
"total_findings": len(findings),
"by_severity": {
sev: len([f for f in findings if f.severity == sev])
for sev in ["critical", "high", "medium", "low", "info"]
},
"findings": [asdict(f) for f in findings],
}
output_file.write_text(json.dumps(report, indent=2), encoding="utf-8")
print(f"Results written to: {output_file}")
# Console summary
print(f"\n{'='*60}")
print(f"Security Scan Results")
print(f"{'='*60}")
print(f"Files scanned: {args.path}")
print(f"Total findings: {len(findings)}")
for sev in ["critical", "high", "medium", "low"]:
count = report["by_severity"][sev]
if count > 0:
print(f" {sev.upper():12s} {count}")
print(f"{'='*60}")
if findings:
print("\nFindings:\n")
for f in findings:
print(f" [{f.severity.upper()}] {f.rule_id}: {f.message}")
print(f" File: {f.file}:{f.line}")
print(f" Code: {f.snippet}")
if f.cwe:
print(f" CWE: {f.cwe}")
print()
# Exit code: non-zero if critical or high findings exist
critical_high = report["by_severity"]["critical"] + report["by_severity"]["high"]
if critical_high > 0:
print(f"FAIL: {critical_high} critical/high severity findings detected.")
sys.exit(1)
else:
print("PASS: No critical or high severity findings.")
sys.exit(0)
if __name__ == "__main__":
main()
Related skills
How it compares
Pick code-review-security over generic linters when you need Python-specific AST secret and injection detection with severitized JSON—not style or type checks.
FAQ
What vulnerability patterns does code-review-security detect?
code-review-security detects eval/exec/compile, subprocess shell=True, unsafe pickle and yaml.load, raw SQL f-strings, hardcoded secrets, weak MD5/SHA1 password hashes, and os.system() calls in Python source via AST analysis.
How do you run the code-review-security scanner?
code-review-security runs `python security-scan.py --path ./app --output-dir ./security-results`. Add `--severity high` to filter findings. Results are written as severitized JSON files in the output directory.
Is Code Review Security safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.