
Security Auditor
- 153 installs
- 178 repo stars
- Updated July 14, 2026
- erichowens/some_claude_skills
Security-auditor is an agent skill that guides OWASP Top 10 2021 detection and remediation while you review APIs and access control.
About
Security-auditor is an agent skill packaged as an OWASP Top 10 2021 reference guide for indie developers shipping web APIs and SaaS backends. It explains what each top-ten category means in plain language, shows common failure modes such as missing auth middleware, insecure direct object references, privilege escalation via parameters, JWT tampering, and CORS misconfiguration, and pairs each area with detection snippets you can compare against your codebase. The intended workflow is not a one-click scanner but guided review: you or your coding agent inspects endpoints and handlers using the patterns, then applies the numbered remediation practices like explicit authorization, ownership checks on object access, and alerting on repeated access-control failures. Use it when you are building new authenticated routes, refactoring authorization, or doing a pre-ship security pass without a dedicated AppSec team. It fits Claude Code, Cursor, and similar agents that can read repository files and reason over diffs. Complexity is intermediate because you need enough backend context to map patterns to your stack. Treat output as engineering guidance to validate with tests and your own threat m
- OWASP Top 10 2021 reference with per-category detection patterns and remediation
- Concrete BAD/GOOD code samples for access control, IDOR, JWT, and CORS issues
- Remediation themes: deny-by-default authorization, server-side checks, and failure logging
- Usable while implementing backend routes and again before deploy as a checklist pass
Security Auditor by the numbers
- 153 all-time installs (skills.sh)
- Ranked #879 of 2,203 Security skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/erichowens/some_claude_skills --skill security-auditorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 153 |
|---|---|
| repo stars | ★ 178 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 14, 2026 |
| Repository | erichowens/some_claude_skills ↗ |
What it does
Walk your app or API against OWASP Top 10 2021 with copy-paste detection patterns and remediation steps before you ship or hand off a security review.
Who is it for?
Best when you're implementing auth-heavy SaaS or REST APIs and want an OWASP-aligned manual review playbook inside your agent session.
Skip if: Skip if you need certified penetration tests, automated SAST/DAST pipelines only, or mobile-native threat models with no web/API surface.
When should I use this skill?
You need OWASP-aligned access-control and API security patterns while reviewing or implementing backend code.
What you get
You get category-by-category patterns to find likely vulnerabilities and concrete remediation steps to harden authorization before release.
- Prioritized list of likely OWASP-category issues mapped to your routes
- Remediation actions aligned to deny-by-default and server-side authorization
By the numbers
- OWASP Top 10 2021 categories with per-category detection and remediation sections
Files
Security Auditor
Comprehensive security scanning for codebases. Identifies vulnerabilities before they become incidents. Focuses on actionable findings with remediation guidance.
When to Use
Use for:
- Pre-deployment security audits
- Dependency vulnerability scanning
- Secret/credential leak detection
- Code-level SAST (Static Application Security Testing)
- Security posture reports for stakeholders
- OWASP Top 10 compliance checking
- Pre-PR security reviews
Do NOT use for:
- Runtime security (WAF, rate limiting) - use infrastructure tools
- Network security/firewall rules - use cloud/DevOps skills
- SOC2/HIPAA/PCI compliance - requires legal/organizational process
- Penetration testing execution - this is detection, not exploitation
Quick Start
Full Security Audit
# Run comprehensive scan
./scripts/full-audit.sh /path/to/project
# Output: security-report.json + summaryQuick Checks
# Dependency vulnerabilities only
npm audit --json > deps-audit.json
# Secret detection only
./scripts/detect-secrets.sh /path/to/project
# OWASP check specific file
./scripts/owasp-check.py /path/to/file.jsCore Scanning Capabilities
1. Dependency Scanning
| Package Manager | Command | Severity Levels |
|---|---|---|
| npm | npm audit --json | critical, high, moderate, low |
| yarn | yarn audit --json | same as npm |
| pip | pip-audit --format json | critical, high, medium, low |
| cargo | cargo audit --json | same |
Decision Tree:
Critical severity found?
├── YES → Block deployment, immediate fix required
│ └── Check if patch available → npm audit fix --force
├── NO → High severity?
├── YES → Fix within sprint, document if deferred
└── NO → Low/Moderate → Track, fix during maintenance2. Secret Detection
High-Risk Patterns:
- API keys:
/[A-Za-z0-9_]{20,}/near "key", "api", "secret" - AWS credentials:
AKIA[0-9A-Z]{16} - Private keys:
-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY----- - JWT tokens:
eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+ - Connection strings:
://[^:]+:[^@]+@
Entropy Analysis:
- Shannon entropy > 4.5 on strings > 20 chars = suspicious
- Base64-encoded blobs in source = investigate
False Positive Handling:
Secret-like pattern found?
├── In test file? → Lower severity, document
├── In example/docs? → Check if placeholder
├── High entropy + near "password"/"secret" → High confidence
└── In .env.example? → Acceptable if placeholder values3. OWASP Top 10 Static Analysis
| # | Vulnerability | Detection Pattern |
|---|---|---|
| A01 | Broken Access Control | Missing auth checks on routes |
| A02 | Cryptographic Failures | Weak algorithms (MD5, SHA1 for passwords) |
| A03 | Injection | Unparameterized queries, eval(), innerHTML |
| A04 | Insecure Design | Hardcoded credentials, missing rate limits |
| A05 | Security Misconfiguration | Debug mode in prod, default credentials |
| A06 | Vulnerable Components | Known CVEs in dependencies |
| A07 | Auth Failures | Weak password policies, session issues |
| A08 | Integrity Failures | Unsigned updates, untrusted deserialization |
| A09 | Logging Failures | Sensitive data in logs, missing audit trails |
| A10 | SSRF | Unvalidated URL inputs to fetch/request |
4. Language-Specific Checks
JavaScript/TypeScript:
eval(),new Function()- code injectioninnerHTML,outerHTML- XSS vectorsdocument.write()- DOM-based XSSchild_process.exec()with user input - command injection- Regex without timeout - ReDoS vulnerability
Python:
pickle.loads()with untrusted data - arbitrary code executionyaml.load()withoutLoader=SafeLoader- code injectionsubprocess.shell=True- command injectioneval(),exec()- code injection- SQL string concatenation - SQL injection
SQL:
- String concatenation in queries - SQL injection
LIKE '%' + input + '%'- injection via wildcards- Missing parameterization - critical vulnerability
Anti-Patterns
Anti-Pattern: Security by Obscurity
What it looks like: "Nobody will find this hardcoded password" Why wrong: Secrets in source always leak eventually Instead: Environment variables, secret managers, zero hardcoded secrets
Anti-Pattern: Audit Fatigue
What it looks like: 500 findings, all "medium", team ignores Why wrong: Critical issues buried in noise Instead: Prioritize by exploitability, start with critical/high only
Anti-Pattern: Fix Without Understanding
What it looks like: npm audit fix --force without review Why wrong: May introduce breaking changes, doesn't address root cause Instead: Review each fix, understand the vulnerability, test after
Anti-Pattern: One-Time Audit
What it looks like: "We did a security audit last year" Why wrong: New CVEs daily, code changes constantly Instead: CI/CD integration, weekly automated scans minimum
Security Report Format
{
"summary": {
"critical": 0,
"high": 2,
"medium": 5,
"low": 12,
"informational": 8
},
"findings": [
{
"id": "SEC-001",
"severity": "high",
"category": "A03:Injection",
"title": "SQL Injection in user search",
"location": "src/api/users.js:45",
"description": "User input concatenated directly into SQL query",
"evidence": "const query = `SELECT * FROM users WHERE name = '${input}'`",
"remediation": "Use parameterized queries: db.query('SELECT * FROM users WHERE name = $1', [input])",
"references": ["https://owasp.org/www-community/attacks/SQL_Injection"]
}
],
"recommendations": [
"Implement parameterized queries across all database access",
"Add input validation layer",
"Enable SQL query logging for monitoring"
]
}CI/CD Integration
GitHub Actions Example
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run security audit
run: |
npm audit --json > audit.json
./scripts/detect-secrets.sh . > secrets.json
./scripts/generate-report.py
- name: Fail on critical
run: |
if jq '.summary.critical > 0' report.json; then
echo "Critical vulnerabilities found!"
exit 1
fiScripts (in scripts/ folder)
| Script | Purpose |
|---|---|
full-audit.sh | Comprehensive security scan |
detect-secrets.sh | High-entropy string and pattern detection |
owasp-check.py | OWASP Top 10 static analysis |
generate-report.py | Combine findings into unified report |
Expert vs Novice Approach
| Novice | Expert |
|---|---|
| Runs audit once before release | CI/CD integration, every commit |
| Focuses on tool output only | Understands vulnerability context |
| Fixes everything or nothing | Triages by exploitability |
| Uses one scanner | Layers multiple tools |
| Ignores false positives | Tunes detection rules |
Success Metrics
| Metric | Target |
|---|---|
| Critical/High pre-production | 0 |
| Mean time to remediate critical | < 24 hours |
| False positive rate | < 10% |
| Scan coverage | 100% of deployable code |
Reference Files
references/owasp-top-10-2024.md- Detailed OWASP guidancereferences/secret-patterns.md- Comprehensive regex patternsreferences/remediation-playbook.md- Fix guidance by vulnerability typereferences/ci-cd-templates.md- Integration examplesscripts/- Working security scanning scripts
---
Detects: Dependency CVEs | Secret leaks | Injection vulnerabilities | OWASP violations | Security misconfigurations
Use with: site-reliability-engineer (deployment gates) | code-review (PR security checks)
OWASP Top 10 2021 Reference Guide
This reference provides detailed guidance for each OWASP Top 10 category with detection patterns and remediation strategies.
A01:2021 - Broken Access Control
What It Is
Failures in enforcing access control policies that allow users to act outside their intended permissions.
Common Vulnerabilities
- Missing authorization checks on API endpoints
- IDOR (Insecure Direct Object References)
- Elevation of privilege through parameter tampering
- JWT manipulation bypassing access controls
- CORS misconfigurations
Detection Patterns
Missing Auth Middleware:
// BAD: No auth check
app.get('/api/users/:id', (req, res) => {
const user = db.getUser(req.params.id);
res.json(user);
});
// GOOD: Auth middleware applied
app.get('/api/users/:id', authMiddleware, checkOwnership, (req, res) => {
const user = db.getUser(req.params.id);
res.json(user);
});IDOR Vulnerability:
# BAD: No ownership check
@app.route('/documents/<doc_id>')
def get_document(doc_id):
return Document.query.get(doc_id)
# GOOD: Ownership verification
@app.route('/documents/<doc_id>')
@login_required
def get_document(doc_id):
doc = Document.query.get(doc_id)
if doc.owner_id != current_user.id:
abort(403)
return docRemediation
1. Deny by default - require explicit authorization 2. Implement server-side access control 3. Log access control failures and alert on suspicious patterns 4. Rate limit APIs to prevent enumeration 5. Use indirect references (UUIDs vs sequential IDs)
---
A02:2021 - Cryptographic Failures
What It Is
Failures related to cryptography that expose sensitive data.
Common Vulnerabilities
- Weak or deprecated algorithms (MD5, SHA1 for passwords)
- Insufficient key length
- Improper key management
- Transmitting data in clear text
- Not enforcing encryption
Detection Patterns
Weak Hash Algorithms:
// BAD: MD5 for passwords
const hash = crypto.createHash('md5').update(password).digest('hex');
// GOOD: bcrypt with proper rounds
const hash = await bcrypt.hash(password, 12);Insecure Randomness:
# BAD: Predictable random
token = str(random.randint(100000, 999999))
# GOOD: Cryptographically secure
import secrets
token = secrets.token_urlsafe(32)Remediation
1. Use bcrypt/argon2 for passwords (work factor ≥10) 2. Use SHA-256+ for integrity 3. Use TLS 1.3 for data in transit 4. Use secrets/crypto.randomBytes for security-sensitive random values 5. Rotate keys regularly
---
A03:2021 - Injection
What It Is
User-supplied data sent to an interpreter as part of a command or query.
Common Vulnerabilities
- SQL injection
- Command injection
- LDAP injection
- XSS (Cross-Site Scripting)
- Template injection
Detection Patterns
SQL Injection:
# BAD: String concatenation
query = f"SELECT * FROM users WHERE name = '{name}'"
# GOOD: Parameterized query
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))Command Injection:
// BAD: User input in exec
exec(`convert ${userInput} output.pdf`);
// GOOD: Use execFile with array
execFile('convert', [sanitizedInput, 'output.pdf']);XSS:
// BAD: innerHTML with user content
element.innerHTML = userContent;
// GOOD: textContent for text, sanitize for HTML
element.textContent = userContent;
// OR
element.innerHTML = DOMPurify.sanitize(userContent);Remediation
1. Use parameterized queries/prepared statements 2. Validate and sanitize all input 3. Use allowlists for command arguments 4. Escape output based on context 5. Use CSP headers
---
A04:2021 - Insecure Design
What It Is
Missing or ineffective security controls at the design level.
Common Vulnerabilities
- Missing rate limiting
- No account lockout
- Credential recovery flaws
- Missing fraud detection
- Trust boundary violations
Detection Patterns
Missing Rate Limiting:
// BAD: No limits on sensitive endpoint
app.post('/login', (req, res) => {
// Unlimited attempts
});
// GOOD: Rate limiting applied
app.post('/login',
rateLimit({ windowMs: 15*60*1000, max: 5 }),
(req, res) => { }
);Remediation
1. Establish secure design patterns 2. Threat model for each feature 3. Implement defense in depth 4. Segment trust levels 5. Test abuse cases, not just use cases
---
A05:2021 - Security Misconfiguration
What It Is
Missing security hardening or improperly configured permissions.
Common Vulnerabilities
- Default credentials
- Unnecessary features enabled
- Verbose error messages
- Missing security headers
- Cloud storage misconfigurations
Detection Patterns
Debug Mode in Production:
# BAD
DEBUG = True # In production
# GOOD
DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'Missing Security Headers:
// GOOD: Security headers applied
app.use(helmet());
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"]
}
}));Remediation
1. Minimal platform without unnecessary features 2. Review and update configurations regularly 3. Implement security headers 4. Automated security configuration verification 5. Different credentials for each environment
---
A06:2021 - Vulnerable and Outdated Components
What It Is
Using components with known vulnerabilities.
Detection Commands
# Node.js
npm audit
npm audit --json | jq '.vulnerabilities'
# Python
pip-audit
safety check
# General
snyk test
trivy fs .Remediation
1. Remove unused dependencies 2. Subscribe to security bulletins 3. Scan dependencies in CI/CD 4. Plan for regular updates 5. Use LTS versions when possible
---
A07:2021 - Identification and Authentication Failures
What It Is
Weaknesses in authentication mechanisms.
Common Vulnerabilities
- Credential stuffing susceptibility
- Weak password policies
- Session fixation
- Missing MFA
- Improper session management
Detection Patterns
Weak Password Policy:
// BAD: No validation
if (password) { createUser(password); }
// GOOD: Strong validation
const strongPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{12,}$/;
if (strongPassword.test(password)) { createUser(password); }Remediation
1. Implement MFA where possible 2. Use strong password policies 3. Limit failed login attempts 4. Use secure session management 5. Invalidate sessions on logout/password change
---
A08:2021 - Software and Data Integrity Failures
What It Is
Code and infrastructure that doesn't protect against integrity violations.
Common Vulnerabilities
- Insecure deserialization
- CI/CD pipeline compromise
- Auto-update without verification
- Unsigned code
Detection Patterns
Insecure Deserialization:
# BAD: Pickle with untrusted data
data = pickle.loads(user_input)
# GOOD: JSON with validation
data = json.loads(user_input)
validate_schema(data, expected_schema)Remediation
1. Use digital signatures for data/code 2. Verify component integrity 3. Secure CI/CD pipelines 4. Avoid insecure serialization formats 5. Review code before deployment
---
A09:2021 - Security Logging and Monitoring Failures
What It Is
Insufficient logging, monitoring, or response to security events.
What to Log
- Authentication attempts (success/fail)
- Authorization failures
- Input validation failures
- Application errors
- Admin activities
Detection Patterns
Sensitive Data in Logs:
# BAD: Password in logs
logger.info(f"Login attempt: {username}, password: {password}")
# GOOD: Redacted sensitive data
logger.info(f"Login attempt: {username}, password: [REDACTED]")Remediation
1. Log security-relevant events 2. Use centralized log management 3. Set up alerting thresholds 4. Establish incident response procedures 5. Retain logs for forensics
---
A10:2021 - Server-Side Request Forgery (SSRF)
What It Is
Web application fetches a remote resource without validating the user-supplied URL.
Common Vulnerabilities
- URL parameter passed to fetch/request
- Webhook configurations
- File imports from URLs
- PDF generators with external resources
Detection Patterns
SSRF Vulnerability:
// BAD: User-controlled URL
const response = await fetch(req.query.url);
// GOOD: URL validation
const allowedDomains = ['api.trusted.com', 'cdn.trusted.com'];
const url = new URL(req.query.url);
if (!allowedDomains.includes(url.hostname)) {
throw new Error('Domain not allowed');
}
const response = await fetch(url);Remediation
1. Validate and sanitize all URLs 2. Use allowlists for permitted domains 3. Disable HTTP redirects or validate targets 4. Block requests to private IP ranges 5. Use network segmentation
---
Quick Reference Table
| Category | Key Detection Pattern | Critical Fix |
|---|---|---|
| A01 | Missing auth middleware | Add authorization checks |
| A02 | MD5/SHA1 for passwords | Use bcrypt/argon2 |
| A03 | String concat in queries | Parameterized queries |
| A04 | No rate limiting | Add rate limits |
| A05 | DEBUG=True in prod | Environment-based config |
| A06 | Old dependency versions | npm audit fix |
| A07 | No account lockout | Limit failed attempts |
| A08 | pickle.loads() | Use safe deserialization |
| A09 | Passwords in logs | Redact sensitive data |
| A10 | User URL in fetch() | URL allowlisting |
#!/bin/bash
# Secret Detection Script
# Scans for high-entropy strings, API keys, credentials, and sensitive patterns
set -euo pipefail
TARGET_DIR="${1:-.}"
OUTPUT_FILE="${2:-secrets-report.json}"
# Colors for output
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
echo "Scanning for secrets in: $TARGET_DIR"
echo "=================================="
# Initialize findings array
FINDINGS="[]"
CRITICAL_COUNT=0
HIGH_COUNT=0
MEDIUM_COUNT=0
# Function to add finding
add_finding() {
local severity="$1"
local pattern_name="$2"
local file="$3"
local line_num="$4"
local evidence="$5"
local finding=$(jq -n \
--arg sev "$severity" \
--arg pat "$pattern_name" \
--arg file "$file" \
--arg line "$line_num" \
--arg ev "$evidence" \
'{severity: $sev, pattern: $pat, file: $file, line: $line, evidence: $ev}')
FINDINGS=$(echo "$FINDINGS" | jq ". + [$finding]")
}
# Skip patterns for files we shouldn't scan
SKIP_PATTERNS=(
"node_modules"
".git"
"dist"
"build"
".next"
"__pycache__"
"*.min.js"
"*.bundle.js"
"package-lock.json"
"yarn.lock"
"*.woff"
"*.woff2"
"*.ttf"
"*.png"
"*.jpg"
"*.gif"
"*.ico"
"*.svg"
)
# Build find exclusion
FIND_EXCLUDES=""
for pattern in "${SKIP_PATTERNS[@]}"; do
FIND_EXCLUDES="$FIND_EXCLUDES -not -path '*/$pattern/*' -not -name '$pattern'"
done
echo -e "\n${YELLOW}[1/5] Checking for AWS credentials...${NC}"
# AWS Access Key ID
while IFS=: read -r file line content; do
if [[ -n "$content" ]]; then
echo -e "${RED}CRITICAL: AWS Key in $file:$line${NC}"
add_finding "critical" "AWS Access Key" "$file" "$line" "AKIA... pattern detected"
((CRITICAL_COUNT++))
fi
done < <(eval "find '$TARGET_DIR' -type f $FIND_EXCLUDES -exec grep -Hn 'AKIA[0-9A-Z]\{16\}' {} \; 2>/dev/null" || true)
# AWS Secret Key pattern
while IFS=: read -r file line content; do
if [[ -n "$content" ]]; then
echo -e "${RED}CRITICAL: Possible AWS Secret in $file:$line${NC}"
add_finding "critical" "AWS Secret Key" "$file" "$line" "40-char base64 near 'secret'"
((CRITICAL_COUNT++))
fi
done < <(eval "find '$TARGET_DIR' -type f $FIND_EXCLUDES -exec grep -Hin 'aws.*secret.*[A-Za-z0-9/+=]\{40\}' {} \; 2>/dev/null" || true)
echo -e "\n${YELLOW}[2/5] Checking for private keys...${NC}"
# Private keys
while IFS=: read -r file line content; do
if [[ -n "$content" ]]; then
echo -e "${RED}CRITICAL: Private key in $file:$line${NC}"
add_finding "critical" "Private Key" "$file" "$line" "BEGIN PRIVATE KEY detected"
((CRITICAL_COUNT++))
fi
done < <(eval "find '$TARGET_DIR' -type f $FIND_EXCLUDES -exec grep -Hn 'BEGIN.*PRIVATE KEY' {} \; 2>/dev/null" || true)
echo -e "\n${YELLOW}[3/5] Checking for API keys and tokens...${NC}"
# Generic API key patterns
while IFS=: read -r file line content; do
if [[ -n "$content" ]]; then
# Skip if it's a placeholder
if [[ ! "$content" =~ (YOUR_|PLACEHOLDER|EXAMPLE|xxx|XXX) ]]; then
echo -e "${YELLOW}HIGH: Possible API key in $file:$line${NC}"
add_finding "high" "API Key Pattern" "$file" "$line" "api_key or apikey assignment detected"
((HIGH_COUNT++))
fi
fi
done < <(eval "find '$TARGET_DIR' -type f $FIND_EXCLUDES -exec grep -HinE '(api[_-]?key|apikey)[[:space:]]*[:=][[:space:]]*[\"'"'"'][A-Za-z0-9_-]{20,}[\"'"'"']' {} \; 2>/dev/null" || true)
# JWT tokens
while IFS=: read -r file line content; do
if [[ -n "$content" ]]; then
echo -e "${YELLOW}HIGH: JWT token in $file:$line${NC}"
add_finding "high" "JWT Token" "$file" "$line" "eyJ... pattern detected"
((HIGH_COUNT++))
fi
done < <(eval "find '$TARGET_DIR' -type f $FIND_EXCLUDES -exec grep -Hn 'eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.' {} \; 2>/dev/null" || true)
echo -e "\n${YELLOW}[4/5] Checking for connection strings...${NC}"
# Database connection strings with passwords
while IFS=: read -r file line content; do
if [[ -n "$content" ]]; then
# Skip example/placeholder patterns
if [[ ! "$content" =~ (localhost|127\.0\.0\.1|example\.com|PLACEHOLDER|password123) ]]; then
echo -e "${RED}CRITICAL: Connection string with credentials in $file:$line${NC}"
add_finding "critical" "Connection String" "$file" "$line" "URL with embedded credentials"
((CRITICAL_COUNT++))
fi
fi
done < <(eval "find '$TARGET_DIR' -type f $FIND_EXCLUDES -exec grep -HnE '(mysql|postgres|mongodb|redis)://[^:]+:[^@]+@' {} \; 2>/dev/null" || true)
echo -e "\n${YELLOW}[5/5] Checking for hardcoded passwords...${NC}"
# Password assignments (excluding common false positives)
while IFS=: read -r file line content; do
if [[ -n "$content" ]]; then
# Skip if test file, mock, or placeholder
if [[ ! "$file" =~ (test|spec|mock|fixture) ]] && [[ ! "$content" =~ (PASSWORD|placeholder|example|changeme) ]]; then
echo -e "${YELLOW}MEDIUM: Possible hardcoded password in $file:$line${NC}"
add_finding "medium" "Hardcoded Password" "$file" "$line" "password assignment detected"
((MEDIUM_COUNT++))
fi
fi
done < <(eval "find '$TARGET_DIR' -type f $FIND_EXCLUDES -exec grep -HinE 'password[[:space:]]*[:=][[:space:]]*[\"'"'"'][^\"'"'"']{8,}[\"'"'"']' {} \; 2>/dev/null" || true)
# Generate report
REPORT=$(jq -n \
--arg target "$TARGET_DIR" \
--arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--argjson critical "$CRITICAL_COUNT" \
--argjson high "$HIGH_COUNT" \
--argjson medium "$MEDIUM_COUNT" \
--argjson findings "$FINDINGS" \
'{
scan_target: $target,
scan_date: $date,
summary: {
critical: $critical,
high: $high,
medium: $medium,
total: ($critical + $high + $medium)
},
findings: $findings
}')
echo "$REPORT" > "$OUTPUT_FILE"
echo ""
echo "=================================="
echo -e "Scan Complete!"
echo -e " Critical: ${RED}$CRITICAL_COUNT${NC}"
echo -e " High: ${YELLOW}$HIGH_COUNT${NC}"
echo -e " Medium: $MEDIUM_COUNT"
echo ""
echo "Report saved to: $OUTPUT_FILE"
# Exit with error if critical findings
if [ "$CRITICAL_COUNT" -gt 0 ]; then
echo -e "\n${RED}CRITICAL vulnerabilities found! Review and remediate before deployment.${NC}"
exit 1
fi
exit 0
#!/bin/bash
# Full Security Audit Script
# Runs all security checks and generates a unified report
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TARGET_DIR="${1:-.}"
OUTPUT_DIR="${2:-./security-reports}"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
# Colors
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ FULL SECURITY AUDIT ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo "Target: $TARGET_DIR"
echo "Output: $OUTPUT_DIR"
echo "Time: $(date)"
echo ""
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Track overall status
OVERALL_STATUS="pass"
CRITICAL_TOTAL=0
HIGH_TOTAL=0
# ─────────────────────────────────────────────────────────────────────────────
echo -e "\n${BLUE}[1/4] Dependency Vulnerability Scan${NC}"
echo "────────────────────────────────────"
DEP_REPORT="$OUTPUT_DIR/dependencies-$TIMESTAMP.json"
# Check for package managers and run appropriate audits
if [ -f "$TARGET_DIR/package.json" ]; then
echo " Found package.json - running npm audit..."
if npm audit --json --prefix "$TARGET_DIR" > "$DEP_REPORT" 2>/dev/null; then
echo -e " ${GREEN}✓ No vulnerabilities found${NC}"
else
# Parse npm audit output
VULN_CRITICAL=$(jq -r '.metadata.vulnerabilities.critical // 0' "$DEP_REPORT" 2>/dev/null || echo "0")
VULN_HIGH=$(jq -r '.metadata.vulnerabilities.high // 0' "$DEP_REPORT" 2>/dev/null || echo "0")
echo -e " ${RED}Critical: $VULN_CRITICAL${NC}"
echo -e " ${YELLOW}High: $VULN_HIGH${NC}"
CRITICAL_TOTAL=$((CRITICAL_TOTAL + VULN_CRITICAL))
HIGH_TOTAL=$((HIGH_TOTAL + VULN_HIGH))
if [ "$VULN_CRITICAL" -gt 0 ]; then
OVERALL_STATUS="fail"
fi
fi
fi
if [ -f "$TARGET_DIR/requirements.txt" ] || [ -f "$TARGET_DIR/pyproject.toml" ]; then
echo " Found Python project - checking for pip-audit..."
if command -v pip-audit &> /dev/null; then
PY_REPORT="$OUTPUT_DIR/pip-audit-$TIMESTAMP.json"
if pip-audit --format json --output "$PY_REPORT" -r "$TARGET_DIR/requirements.txt" 2>/dev/null; then
echo -e " ${GREEN}✓ No Python vulnerabilities found${NC}"
else
echo -e " ${YELLOW}Python vulnerabilities found - see $PY_REPORT${NC}"
fi
else
echo -e " ${YELLOW}pip-audit not installed. Run: pip install pip-audit${NC}"
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
echo -e "\n${BLUE}[2/4] Secret Detection Scan${NC}"
echo "────────────────────────────────────"
SECRET_REPORT="$OUTPUT_DIR/secrets-$TIMESTAMP.json"
if [ -f "$SCRIPT_DIR/detect-secrets.sh" ]; then
if bash "$SCRIPT_DIR/detect-secrets.sh" "$TARGET_DIR" "$SECRET_REPORT"; then
echo -e " ${GREEN}✓ No secrets detected${NC}"
else
SECRET_CRITICAL=$(jq -r '.summary.critical // 0' "$SECRET_REPORT" 2>/dev/null || echo "0")
SECRET_HIGH=$(jq -r '.summary.high // 0' "$SECRET_REPORT" 2>/dev/null || echo "0")
CRITICAL_TOTAL=$((CRITICAL_TOTAL + SECRET_CRITICAL))
HIGH_TOTAL=$((HIGH_TOTAL + SECRET_HIGH))
if [ "$SECRET_CRITICAL" -gt 0 ]; then
OVERALL_STATUS="fail"
fi
fi
else
echo -e " ${YELLOW}detect-secrets.sh not found${NC}"
fi
# ─────────────────────────────────────────────────────────────────────────────
echo -e "\n${BLUE}[3/4] OWASP Static Analysis${NC}"
echo "────────────────────────────────────"
OWASP_REPORT="$OUTPUT_DIR/owasp-$TIMESTAMP.json"
if [ -f "$SCRIPT_DIR/owasp-check.py" ]; then
if python3 "$SCRIPT_DIR/owasp-check.py" "$TARGET_DIR" --json --output "$OWASP_REPORT"; then
echo -e " ${GREEN}✓ No OWASP violations found${NC}"
else
OWASP_CRITICAL=$(jq -r '.summary.critical // 0' "$OWASP_REPORT" 2>/dev/null || echo "0")
OWASP_HIGH=$(jq -r '.summary.high // 0' "$OWASP_REPORT" 2>/dev/null || echo "0")
echo -e " ${RED}Critical: $OWASP_CRITICAL${NC}"
echo -e " ${YELLOW}High: $OWASP_HIGH${NC}"
CRITICAL_TOTAL=$((CRITICAL_TOTAL + OWASP_CRITICAL))
HIGH_TOTAL=$((HIGH_TOTAL + OWASP_HIGH))
if [ "$OWASP_CRITICAL" -gt 0 ]; then
OVERALL_STATUS="fail"
fi
fi
else
echo -e " ${YELLOW}owasp-check.py not found${NC}"
fi
# ─────────────────────────────────────────────────────────────────────────────
echo -e "\n${BLUE}[4/4] Configuration Security Check${NC}"
echo "────────────────────────────────────"
CONFIG_ISSUES=0
# Check for .env files in git
if [ -d "$TARGET_DIR/.git" ]; then
if git -C "$TARGET_DIR" ls-files --error-unmatch .env 2>/dev/null; then
echo -e " ${RED}✗ .env file is tracked in git!${NC}"
CONFIG_ISSUES=$((CONFIG_ISSUES + 1))
OVERALL_STATUS="fail"
else
echo -e " ${GREEN}✓ .env not in git${NC}"
fi
fi
# Check .gitignore for common security files
if [ -f "$TARGET_DIR/.gitignore" ]; then
MISSING_IGNORES=""
for pattern in ".env" "*.pem" "*.key" "credentials.json" "*.p12"; do
if ! grep -q "^$pattern" "$TARGET_DIR/.gitignore" 2>/dev/null; then
MISSING_IGNORES="$MISSING_IGNORES $pattern"
fi
done
if [ -n "$MISSING_IGNORES" ]; then
echo -e " ${YELLOW}Consider adding to .gitignore:$MISSING_IGNORES${NC}"
else
echo -e " ${GREEN}✓ Sensitive patterns in .gitignore${NC}"
fi
fi
# Check for debug flags in common config files
for config in "$TARGET_DIR/package.json" "$TARGET_DIR/tsconfig.json"; do
if [ -f "$config" ]; then
if grep -q '"debug"\s*:\s*true' "$config" 2>/dev/null; then
echo -e " ${YELLOW}Debug flag found in $(basename "$config")${NC}"
fi
fi
done
# ─────────────────────────────────────────────────────────────────────────────
echo ""
echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ AUDIT SUMMARY ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e " Critical Issues: ${RED}$CRITICAL_TOTAL${NC}"
echo -e " High Issues: ${YELLOW}$HIGH_TOTAL${NC}"
echo ""
# Generate unified report
UNIFIED_REPORT="$OUTPUT_DIR/security-report-$TIMESTAMP.json"
jq -n \
--arg target "$TARGET_DIR" \
--arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg status "$OVERALL_STATUS" \
--argjson critical "$CRITICAL_TOTAL" \
--argjson high "$HIGH_TOTAL" \
'{
audit_target: $target,
audit_date: $date,
overall_status: $status,
summary: {
critical: $critical,
high: $high
},
reports: {
dependencies: "dependencies-'$TIMESTAMP'.json",
secrets: "secrets-'$TIMESTAMP'.json",
owasp: "owasp-'$TIMESTAMP'.json"
}
}' > "$UNIFIED_REPORT"
echo "Reports saved to: $OUTPUT_DIR/"
echo " - security-report-$TIMESTAMP.json (unified)"
echo " - dependencies-$TIMESTAMP.json"
echo " - secrets-$TIMESTAMP.json"
echo " - owasp-$TIMESTAMP.json"
echo ""
if [ "$OVERALL_STATUS" = "fail" ]; then
echo -e "${RED}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${RED}║ AUDIT FAILED - Critical vulnerabilities require action ║${NC}"
echo -e "${RED}╚════════════════════════════════════════════════════════════╝${NC}"
exit 1
else
echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ AUDIT PASSED - No critical issues found ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
exit 0
fi
#!/usr/bin/env python3
"""
OWASP Top 10 Static Analysis Scanner
Detects common vulnerabilities in JavaScript, TypeScript, and Python code.
"""
import argparse
import json
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional
@dataclass
class Finding:
"""Security finding data class"""
id: str
severity: str
owasp_category: str
title: str
file: str
line: int
evidence: str
description: str
remediation: str
class OWASPScanner:
"""Static analysis scanner for OWASP Top 10 vulnerabilities"""
# Patterns organized by OWASP category
PATTERNS = {
"A03:Injection": {
"js": [
(r'eval\s*\(', "eval() usage", "critical",
"eval() can execute arbitrary code",
"Use JSON.parse() for data, avoid dynamic code execution"),
(r'new\s+Function\s*\(', "new Function() usage", "critical",
"new Function() can execute arbitrary code",
"Avoid dynamic function creation, use static alternatives"),
(r'child_process\.exec\s*\([^)]*\+', "Command injection risk", "critical",
"String concatenation in exec() allows command injection",
"Use execFile() with array arguments, validate input"),
(r'\.innerHTML\s*=', "innerHTML assignment", "high",
"innerHTML can execute scripts in user content",
"Use textContent for text, sanitize HTML with DOMPurify"),
(r'document\.write\s*\(', "document.write() usage", "high",
"document.write() can inject malicious content",
"Use DOM manipulation methods instead"),
(r'\$\{[^}]+\}.*(?:SELECT|INSERT|UPDATE|DELETE)', "SQL template injection", "critical",
"Template literals in SQL queries allow injection",
"Use parameterized queries with placeholders"),
],
"py": [
(r'eval\s*\(', "eval() usage", "critical",
"eval() executes arbitrary Python code",
"Use ast.literal_eval() for data, avoid eval()"),
(r'exec\s*\(', "exec() usage", "critical",
"exec() executes arbitrary Python code",
"Avoid exec(), use safer alternatives"),
(r'subprocess\..*shell\s*=\s*True', "Shell=True in subprocess", "critical",
"shell=True allows command injection",
"Use shell=False with list arguments"),
(r'pickle\.loads?\s*\(', "Pickle deserialization", "critical",
"Pickle can execute arbitrary code during deserialization",
"Use JSON or other safe serialization formats"),
(r'yaml\.load\s*\([^)]*\)(?!\s*,\s*Loader)', "Unsafe YAML load", "high",
"yaml.load() without SafeLoader can execute code",
"Use yaml.safe_load() or Loader=yaml.SafeLoader"),
(r'cursor\.execute\s*\([^)]*%', "SQL string formatting", "critical",
"String formatting in SQL queries allows injection",
"Use parameterized queries: cursor.execute(sql, (param,))"),
(r'cursor\.execute\s*\([^)]*\.format', "SQL .format() injection", "critical",
".format() in SQL queries allows injection",
"Use parameterized queries with placeholders"),
]
},
"A02:Cryptographic Failures": {
"js": [
(r'crypto\.createHash\s*\([\'"]md5[\'"]\)', "MD5 hash usage", "high",
"MD5 is cryptographically broken",
"Use SHA-256 or better for integrity, bcrypt/argon2 for passwords"),
(r'crypto\.createHash\s*\([\'"]sha1[\'"]\)', "SHA1 hash usage", "medium",
"SHA1 is deprecated for security purposes",
"Use SHA-256 or better"),
(r'Math\.random\s*\(', "Math.random() for security", "medium",
"Math.random() is not cryptographically secure",
"Use crypto.randomBytes() or crypto.getRandomValues()"),
],
"py": [
(r'hashlib\.md5\s*\(', "MD5 hash usage", "high",
"MD5 is cryptographically broken",
"Use hashlib.sha256() or better, bcrypt for passwords"),
(r'hashlib\.sha1\s*\(', "SHA1 hash usage", "medium",
"SHA1 is deprecated for security purposes",
"Use hashlib.sha256() or better"),
(r'random\.(random|randint|choice)', "random module for security", "medium",
"random module is not cryptographically secure",
"Use secrets module for security-sensitive randomness"),
]
},
"A05:Security Misconfiguration": {
"js": [
(r'cors\s*\(\s*\)', "CORS allow all", "high",
"Unrestricted CORS allows any origin",
"Configure specific allowed origins"),
(r'app\.use\s*\(\s*cors\s*\(\s*\{\s*origin\s*:\s*[\'"]?\*', "CORS wildcard origin", "high",
"CORS wildcard allows any origin",
"Specify allowed origins explicitly"),
(r'debug\s*[:=]\s*true', "Debug mode enabled", "medium",
"Debug mode may expose sensitive information",
"Disable debug mode in production"),
(r'NODE_ENV\s*!==?\s*[\'"]production', "Non-production check", "low",
"Ensure production settings in deployment",
"Use environment-specific configuration"),
],
"py": [
(r'DEBUG\s*=\s*True', "Django DEBUG=True", "high",
"Debug mode exposes sensitive error details",
"Set DEBUG=False in production"),
(r'ALLOWED_HOSTS\s*=\s*\[\s*[\'"]?\*', "Django wildcard hosts", "high",
"Wildcard ALLOWED_HOSTS is insecure",
"Specify exact hostnames"),
(r'app\.run\s*\([^)]*debug\s*=\s*True', "Flask debug mode", "high",
"Debug mode enables code execution via debugger",
"Disable debug mode in production"),
]
},
"A07:Authentication Failures": {
"js": [
(r'jwt\.sign\s*\([^)]*expiresIn\s*:\s*[\'"]?\d{8,}', "Long JWT expiry", "medium",
"Very long token expiration increases risk",
"Use shorter expiration times (hours, not days)"),
(r'password.*[\'"][a-zA-Z0-9]{1,7}[\'"]', "Short password constant", "high",
"Hardcoded short password is insecure",
"Remove hardcoded passwords, use secrets management"),
(r'bcrypt.*rounds?\s*[:=]\s*[1-9]\b', "Low bcrypt rounds", "medium",
"Low bcrypt rounds make passwords easier to crack",
"Use at least 10-12 rounds"),
],
"py": [
(r'password.*=\s*[\'"][a-zA-Z0-9]{1,7}[\'"]', "Short password constant", "high",
"Hardcoded short password is insecure",
"Remove hardcoded passwords, use secrets management"),
(r'SECRET_KEY\s*=\s*[\'"][^\'\"]{1,20}[\'"]', "Short SECRET_KEY", "high",
"Short secret key is vulnerable to brute force",
"Use at least 50 random characters"),
]
},
"A10:SSRF": {
"js": [
(r'fetch\s*\(\s*(?:req\.(?:query|body|params)|[a-zA-Z]+Input)', "SSRF risk in fetch", "high",
"User input directly in fetch URL allows SSRF",
"Validate and whitelist allowed domains"),
(r'axios\.\w+\s*\(\s*(?:req\.(?:query|body|params)|[a-zA-Z]+Input)', "SSRF risk in axios", "high",
"User input directly in axios URL allows SSRF",
"Validate and whitelist allowed domains"),
],
"py": [
(r'requests\.\w+\s*\(\s*(?:request\.\w+|user_input|url_param)', "SSRF risk in requests", "high",
"User input directly in request URL allows SSRF",
"Validate and whitelist allowed domains"),
(r'urllib\.request\.urlopen\s*\(\s*[a-zA-Z]', "SSRF risk in urllib", "high",
"Unvalidated URL in urlopen allows SSRF",
"Validate and whitelist allowed domains"),
]
}
}
# File extensions to language mapping
EXTENSIONS = {
'.js': 'js',
'.jsx': 'js',
'.ts': 'js',
'.tsx': 'js',
'.mjs': 'js',
'.cjs': 'js',
'.py': 'py',
}
# Directories to skip
SKIP_DIRS = {
'node_modules', '.git', 'dist', 'build', '.next', '__pycache__',
'venv', '.venv', 'env', '.env', 'coverage', '.nyc_output'
}
def __init__(self):
self.findings: List[Finding] = []
self.finding_counter = 0
def scan_file(self, filepath: Path) -> None:
"""Scan a single file for vulnerabilities"""
ext = filepath.suffix.lower()
lang = self.EXTENSIONS.get(ext)
if not lang:
return
try:
content = filepath.read_text(encoding='utf-8', errors='ignore')
lines = content.split('\n')
except Exception as e:
print(f"Warning: Could not read {filepath}: {e}", file=sys.stderr)
return
for owasp_cat, lang_patterns in self.PATTERNS.items():
patterns = lang_patterns.get(lang, [])
for pattern, title, severity, description, remediation in patterns:
for i, line in enumerate(lines, 1):
# Skip comments
stripped = line.strip()
if stripped.startswith('//') or stripped.startswith('#'):
continue
if stripped.startswith('/*') or stripped.startswith('"""') or stripped.startswith("'''"):
continue
if re.search(pattern, line, re.IGNORECASE):
self.finding_counter += 1
self.findings.append(Finding(
id=f"OWASP-{self.finding_counter:04d}",
severity=severity,
owasp_category=owasp_cat,
title=title,
file=str(filepath),
line=i,
evidence=line.strip()[:100],
description=description,
remediation=remediation
))
def scan_directory(self, path: Path) -> None:
"""Recursively scan a directory"""
if not path.exists():
print(f"Error: Path does not exist: {path}", file=sys.stderr)
sys.exit(1)
if path.is_file():
self.scan_file(path)
return
for item in path.rglob('*'):
# Skip excluded directories
if any(skip in item.parts for skip in self.SKIP_DIRS):
continue
if item.is_file():
self.scan_file(item)
def get_summary(self) -> dict:
"""Get summary counts by severity"""
summary = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0}
for f in self.findings:
if f.severity in summary:
summary[f.severity] += 1
summary['total'] = len(self.findings)
return summary
def to_json(self) -> str:
"""Export findings as JSON"""
return json.dumps({
'summary': self.get_summary(),
'findings': [
{
'id': f.id,
'severity': f.severity,
'owasp_category': f.owasp_category,
'title': f.title,
'file': f.file,
'line': f.line,
'evidence': f.evidence,
'description': f.description,
'remediation': f.remediation
}
for f in self.findings
]
}, indent=2)
def print_report(self) -> None:
"""Print human-readable report"""
summary = self.get_summary()
print("\n" + "=" * 60)
print("OWASP Top 10 Static Analysis Report")
print("=" * 60)
print(f"\nSummary:")
print(f" Critical: {summary['critical']}")
print(f" High: {summary['high']}")
print(f" Medium: {summary['medium']}")
print(f" Low: {summary['low']}")
print(f" Total: {summary['total']}")
if self.findings:
print("\nFindings:")
print("-" * 60)
for f in sorted(self.findings, key=lambda x: ['critical', 'high', 'medium', 'low'].index(x.severity)):
sev_color = {
'critical': '\033[91m', # Red
'high': '\033[93m', # Yellow
'medium': '\033[94m', # Blue
'low': '\033[90m' # Gray
}.get(f.severity, '')
reset = '\033[0m'
print(f"\n[{sev_color}{f.severity.upper()}{reset}] {f.id}: {f.title}")
print(f" Category: {f.owasp_category}")
print(f" Location: {f.file}:{f.line}")
print(f" Evidence: {f.evidence}")
print(f" Issue: {f.description}")
print(f" Fix: {f.remediation}")
def main():
parser = argparse.ArgumentParser(
description='OWASP Top 10 Static Analysis Scanner',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s /path/to/project
%(prog)s src/api/users.js --json
%(prog)s . --output report.json
"""
)
parser.add_argument('path', help='File or directory to scan')
parser.add_argument('--json', '-j', action='store_true', help='Output JSON format')
parser.add_argument('--output', '-o', help='Write report to file')
parser.add_argument('--fail-on', choices=['critical', 'high', 'medium', 'low'],
default='critical', help='Exit with error if findings at this level or above')
args = parser.parse_args()
scanner = OWASPScanner()
scanner.scan_directory(Path(args.path))
if args.json:
output = scanner.to_json()
if args.output:
Path(args.output).write_text(output)
print(f"Report written to {args.output}")
else:
print(output)
else:
scanner.print_report()
if args.output:
Path(args.output).write_text(scanner.to_json())
print(f"\nJSON report written to {args.output}")
# Exit code based on findings
summary = scanner.get_summary()
severity_levels = ['critical', 'high', 'medium', 'low']
fail_index = severity_levels.index(args.fail_on)
for level in severity_levels[:fail_index + 1]:
if summary[level] > 0:
sys.exit(1)
sys.exit(0)
if __name__ == '__main__':
main()
Related skills
How it compares
Use as a structured OWASP checklist during agent-assisted code review, not as a substitute for dedicated security scanning tools or MCP vulnerability servers.
FAQ
Who is security-auditor for?
Developers shipping web backends or agents who want OWASP Top 10 guidance while coding or reviewing with Claude Code, Cursor, or Codex.
When should I use security-auditor?
During Build when adding authenticated endpoints, during Ship security prep before production, and when debugging suspected IDOR or JWT bypass issues after a staging deploy.
Is security-auditor safe to install?
It is reference documentation for review workflows; check the Security Audits panel on this Prism page for install risk and repo signals before adding it to your agent.