Module 5: Security Scanning and Smart Rollback
Integration with Existing Security Tools
Integration with Existing Security Tools
Overview
Claude Code is good at detecting contextual security patterns (capsule 02), but it's not the only tool you need. Snyk detects CVEs in dependencies better than any model. Dependabot creates automatic PRs to update vulnerable packages. npm audit and pip-audit run locally and are free. Each tool has its sweet spot — the professional pattern is to orchestrate them all, not choose one.
This capsule teaches you to integrate Claude Code with the security tool ecosystem: when to use each one, how to combine their outputs without duplication, and how to build a pipeline where each tool covers its zone of strength.
By the end, you'll have a workflow that combines 3-4 complementary security tools, each running in parallel where it's efficient, aggregating results into a single coherent report.
The Tool Map
┌──────────────────────────────────────────────────────────┐
│ DEPENDENCY VULNERABILITY SCANNERS │
│ ✅ CVEs in packages / versions │
│ ❌ Do NOT cover your own code │
│ │
│ → Snyk, Dependabot, npm audit, pip-audit, gemnasium │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ STATIC ANALYSIS (SAST) — RULES │
│ ✅ Known patterns in code (SQL inj, XSS) │
│ ❌ Don't detect logic bugs or contextual patterns │
│ │
│ → Semgrep, Bandit (Python), ESLint security plugins, │
│ SonarQube │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ SECRETS DETECTION │
│ ✅ API keys, tokens, secrets in code │
│ ❌ Only detects what you already leaked — doesn't prevent│
│ │
│ → gitleaks, TruffleHog, GitHub Secret Scanning │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ CLAUDE CODE (this path) │
│ ✅ Contextual patterns that static rules don't detect │
│ ✅ Explanation of the finding and remediation │
│ ❌ Slower and more expensive than static tools │
│ │
│ → Analysis with a specialized prompt (capsule 02) │
└──────────────────────────────────────────────────────────┘
The rule: each tool has a zone where it's the best option. Combining them efficiently is the professional pattern.
Recommended Stack by Language
Python
Layer 1 (fast, free):
- pip-audit → CVEs in dependencies
- bandit → static SAST
- gitleaks → secrets
Layer 2 (deeper, paid):
- Snyk → better CVE database
Layer 3 (contextual):
- Claude Code → patterns the rest don't detect
JavaScript / TypeScript
Layer 1:
- npm audit → CVEs in dependencies
- ESLint with → static SAST
security plugin
- gitleaks → secrets
Layer 2:
- Snyk → better CVE database
- Semgrep → advanced rules
Layer 3:
- Claude Code → contextual patterns
Go / Java / Ruby
Similar patterns — language-specific tools + Claude Code for contextual depth.
The Combined Pipeline
# .github/workflows/security-pipeline.yml
name: Security Pipeline
on:
pull_request:
types: [opened, synchronize]
schedule:
- cron: '0 6 * * *' # daily 6am UTC for a full scan
permissions:
contents: read
pull-requests: write
security-events: write
jobs:
# ============================================
# JOB 1: Dependency vulnerabilities
# ============================================
dependency-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.11' }
- run: pip install pip-audit
- name: pip-audit
run: |
pip-audit --format json --output pip-audit-results.json || true
# || true so the job doesn't fail even if there are findings
- name: Snyk Open Source
uses: snyk/actions/python@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --json-file-output=snyk-results.json --severity-threshold=high
continue-on-error: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: dependency-results
path: |
pip-audit-results.json
snyk-results.json
# ============================================
# JOB 2: SAST (Static Application Security Testing)
# ============================================
sast-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.11' }
- run: pip install bandit
- name: Bandit
run: |
bandit -r src/ -f json -o bandit-results.json || true
- name: Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: p/security-audit p/owasp-top-ten
continue-on-error: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: sast-results
path: |
bandit-results.json
semgrep-results.json
# ============================================
# JOB 3: Secrets detection
# ============================================
secrets-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ============================================
# JOB 4: Claude Code contextual scan
# ============================================
claude-scan:
runs-on: ubuntu-latest
needs: [dependency-scan, sast-scan] # ← consumes outputs from previous jobs
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-python@v5
with: { python-version: '3.11' }
- run: pip install anthropic requests
- uses: actions/download-artifact@v4
with:
name: dependency-results
- uses: actions/download-artifact@v4
with:
name: sast-results
- name: Run Claude Code scan with context from other tools
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: python scripts/security_scan_with_context.py
# ============================================
# JOB 5: Aggregate and publish
# ============================================
aggregate:
runs-on: ubuntu-latest
needs: [dependency-scan, sast-scan, secrets-scan, claude-scan]
if: always() # runs even if previous jobs fail
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.11' }
- run: pip install requests
- uses: actions/download-artifact@v4
with: { path: artifacts/ }
- name: Aggregate findings
run: python scripts/aggregate_security_findings.py
- name: Post to PR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: python scripts/post_aggregated_findings.py
The pattern:
- 3 jobs in parallel (dependency-scan, sast-scan, secrets-scan) — independent tools run simultaneously
- Claude Code afterward consuming context from the previous ones (avoids duplicating findings another tool already detected)
- Aggregate at the end combines everything into a single report
Claude Code with Context
The trick to avoid duplication: pass Claude the findings from the other tools as context, and ask it to only report patterns they didn't detect.
"""scripts/security_scan_with_context.py
Claude Code scan that avoids duplicating findings from other tools.
"""
import json
import os
import sys
from pathlib import Path
from anthropic import Anthropic
def load_other_findings() -> dict:
"""Load findings from other tools for context."""
findings = {}
# pip-audit
if Path("pip-audit-results.json").exists():
try:
data = json.loads(Path("pip-audit-results.json").read_text())
findings["dependency_cves"] = [
f"{vuln['name']} {vuln['version']} → {vuln['id']}"
for v in data.get("dependencies", [])
for vuln in v.get("vulns", [])
]
except Exception:
findings["dependency_cves"] = []
# Bandit
if Path("bandit-results.json").exists():
try:
data = json.loads(Path("bandit-results.json").read_text())
findings["sast_findings"] = [
f"{r['filename']}:{r['line_number']} — {r['issue_text']}"
for r in data.get("results", [])
]
except Exception:
findings["sast_findings"] = []
return findings
def build_prompt_with_context(diff: str, other_findings: dict) -> str:
"""Prompt that includes context from other tools."""
other_summary = "Other tools already reported:\n"
if other_findings.get("dependency_cves"):
other_summary += f"\n- CVEs in dependencies ({len(other_findings['dependency_cves'])} found):\n"
for cve in other_findings["dependency_cves"][:10]:
other_summary += f" • {cve}\n"
if other_findings.get("sast_findings"):
other_summary += f"\n- SAST findings ({len(other_findings['sast_findings'])} found):\n"
for finding in other_findings["sast_findings"][:10]:
other_summary += f" • {finding}\n"
return f"""You are a security auditor. Other tools (pip-audit, bandit) already
scanned this code. Your task is to detect **contextual patterns** that those
tools didn't detect — logic bugs, missing validation with context,
poorly applied auth checks, IDOR, etc.
{other_summary}
DO NOT REPORT:
- CVEs in dependencies (already covered by pip-audit/Snyk)
- Issues the SAST tools already reported
- Trivial patterns that static tools detect
REPORT ONLY:
- Incorrect auth/authz logic
- IDOR (Insecure Direct Object References)
- Missing validation in complex flows
- Injection patterns that require multi-file context
- Race conditions in concurrent code
- Issues that require understanding the business logic
OUTPUT: JSON {{"findings": [...]}} with the standard structure.
DIFF:
{diff[:30000]}
"""
def main() -> int:
diff_file = Path("filtered_diff.txt")
if not diff_file.exists() or not diff_file.read_text().strip():
Path("claude_findings.json").write_text(json.dumps({"findings": []}))
return 0
other_findings = load_other_findings()
diff = diff_file.read_text()
client = Anthropic()
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=4000,
messages=[{
"role": "user",
"content": build_prompt_with_context(diff, other_findings),
}],
)
text = response.content[0].text.strip()
if text.startswith("```"):
text = "\n".join(text.split("\n")[1:-1])
try:
data = json.loads(text)
except json.JSONDecodeError:
data = {"findings": []}
Path("claude_findings.json").write_text(json.dumps(data, indent=2))
print(f"Claude detected {len(data.get('findings', []))} contextual findings")
return 0
if __name__ == "__main__":
sys.exit(main())
Aggregator: A Single Report
"""scripts/aggregate_security_findings.py
Combines findings from all the tools into a unified report.
"""
import json
from pathlib import Path
from typing import TypedDict
class UnifiedFinding(TypedDict):
source: str # "pip-audit", "bandit", "snyk", "semgrep", "claude"
severity: str # "critical", "high", "medium", "low"
category: str
path: str
line: int
title: str
description: str
remediation: str
def normalize_pip_audit(data: dict) -> list[UnifiedFinding]:
findings = []
for dep in data.get("dependencies", []):
for vuln in dep.get("vulns", []):
findings.append({
"source": "pip-audit",
"severity": map_cve_severity(vuln.get("id", "")),
"category": "dependencies",
"path": "requirements.txt",
"line": 1,
"title": f"{dep['name']} {dep['version']} has {vuln['id']}",
"description": vuln.get("description", ""),
"remediation": f"Upgrade to {vuln.get('fix_versions', ['latest'])[0]}",
})
return findings
def normalize_bandit(data: dict) -> list[UnifiedFinding]:
findings = []
severity_map = {"HIGH": "high", "MEDIUM": "medium", "LOW": "low"}
for r in data.get("results", []):
findings.append({
"source": "bandit",
"severity": severity_map.get(r.get("issue_severity", "LOW"), "low"),
"category": "sast",
"path": r.get("filename", ""),
"line": r.get("line_number", 0),
"title": r.get("test_name", ""),
"description": r.get("issue_text", ""),
"remediation": r.get("issue_cwe", {}).get("link", ""),
})
return findings
def normalize_claude(data: dict) -> list[UnifiedFinding]:
findings = []
for f in data.get("findings", []):
findings.append({
"source": "claude",
"severity": f.get("severity", "low"),
"category": f.get("category", "contextual"),
"path": f.get("path", ""),
"line": f.get("line", 0),
"title": f.get("title", ""),
"description": f.get("description", ""),
"remediation": f.get("remediation", ""),
})
return findings
def map_cve_severity(cve_id: str) -> str:
"""Map a CVE ID to severity (simplified)."""
# In real production, query the NVD API for the CVSS score
return "high" # conservative default
def deduplicate(findings: list[UnifiedFinding]) -> list[UnifiedFinding]:
"""Remove duplicates (same path + line + category)."""
seen = set()
unique = []
for f in findings:
key = (f["path"], f["line"], f["category"])
if key not in seen:
seen.add(key)
unique.append(f)
return unique
def main() -> int:
all_findings: list[UnifiedFinding] = []
# pip-audit
pip_audit_path = Path("artifacts/dependency-results/pip-audit-results.json")
if pip_audit_path.exists():
all_findings.extend(normalize_pip_audit(json.loads(pip_audit_path.read_text())))
# Bandit
bandit_path = Path("artifacts/sast-results/bandit-results.json")
if bandit_path.exists():
all_findings.extend(normalize_bandit(json.loads(bandit_path.read_text())))
# Claude
claude_path = Path("artifacts/claude-findings/claude_findings.json")
if claude_path.exists():
all_findings.extend(normalize_claude(json.loads(claude_path.read_text())))
# Deduplicate
unique = deduplicate(all_findings)
# Stats
by_severity = {"critical": 0, "high": 0, "medium": 0, "low": 0}
by_source = {}
for f in unique:
by_severity[f["severity"]] = by_severity.get(f["severity"], 0) + 1
by_source[f["source"]] = by_source.get(f["source"], 0) + 1
report = {
"findings": unique,
"summary": {
"total": len(unique),
"by_severity": by_severity,
"by_source": by_source,
},
}
Path("aggregated_security_report.json").write_text(json.dumps(report, indent=2))
print(f"Aggregated: {len(unique)} unique findings")
print(f" By severity: {by_severity}")
print(f" By source: {by_source}")
return 0
if __name__ == "__main__":
import sys
sys.exit(main())
When to Use Each Tool
ONLY FOR SOME THINGS:
✅ pip-audit / npm audit:
- Free, fast
- Covers only dependencies
- Use it ALWAYS as the first line
✅ Snyk:
- Better CVE database
- Paid, but free tier for open source
- Worth it for serious teams
✅ Bandit / Semgrep:
- Static SAST, free
- Detects known patterns fast
- Use it ALWAYS for Python/JS
✅ gitleaks:
- Secrets detection in pre-commit and CI
- Free
- Use it ALWAYS
✅ GitHub Secret Scanning:
- Free for public repos
- Detects known tokens automatically
- Enable it if it's available
✅ Claude Code (this path):
- Contextual and logical patterns
- Expensive but deep
- As a complement, not a replacement
The typical recommended stack:
LAYER 1 (always, free): pip-audit + bandit + gitleaks
LAYER 2 (if budget): Snyk
LAYER 3 (always, $): Claude Code for context
Common Pitfalls
Error 1: Relying on a single tool
Symptom: Everything is Claude Code, no static scanners. Or everything is Snyk, no Claude.
Why it happens: Simplifying seemed like a good idea.
How to fix it: Every tool has gaps. Combining them covers the mutual gaps.
Error 2: Duplicate findings without deduplication
Symptom: The report has "SQL injection in payment.py:42" 3 times (Bandit, Semgrep, Claude).
Why it happens: There's no aggregator that deduplicates.
How to fix it: An aggregator with deduplication by (path, line, category) as the script shows.
Error 3: Claude reports what Bandit already reported
Symptom: Claude duplicates static SAST findings, spending tokens on what cheaper tools already covered.
Why it happens: A prompt without context from other findings.
How to fix it: Pass findings from other tools as context (the security_scan_with_context.py script). Ask it to only report what's new.
Error 4: Inconsistent severity between tools
Symptom: Bandit says "MEDIUM", Snyk says "HIGH", Claude says "CRITICAL" for the same issue.
Why it happens: Each tool has its own scale.
How to fix it: Normalize to the same schema (critical/high/medium/low). The aggregator does the mapping.
Error 5: Not running periodic scans
Symptom: You only scan on PRs. New CVEs in current dependencies aren't detected until the next PR.
Why it happens: A trigger only on pull_request.
How to fix it: Add a schedule: trigger to run daily/weekly against main. It detects new CVEs in stable dependencies.
Diagnosis
Question 1: How many security tools do you have in your current pipeline?
1 = gaps. 3-5 = professional stack. 10+ = possible over-engineering.
Question 2: Does your pipeline run dependency scan, SAST, secrets, and context analysis?
All 4 categories. If any is missing, you'll have specific gaps.
Question 3: Does your Claude Code scan have context from the other tools?
Without context, it duplicates findings and wastes tokens. With context, it adds value.
Question 4: Do you have deduplication between tools?
Without deduplication, the team sees "the same issue" several times and loses confidence in the bot.
Question 5: Do you run periodic scans against main, not just on PRs?
New CVEs in stable dependencies are only detected with periodic scans.
Exercises
Exercise 1: Minimal stack (Easy)
Configure in your repo:
- pip-audit (or npm audit) in CI
- Bandit (or ESLint security)
- gitleaks
- Claude Code (from capsule 02)
Verify that the 4 jobs run in parallel on each PR.
Exercise 2: Aggregator with deduplication (Medium)
Implement aggregate_security_findings.py that:
- Reads the outputs of 3 tools
- Normalizes to the unified schema
- Deduplicates by (path, line, category)
- Generates the final report
Exercise 3: Claude with context (Hard)
Modify the Claude scan to:
- Read findings from pip-audit and bandit
- Pass them as context in the prompt
- Ask it to only report what's new
- Compare findings with/without context — is there a difference in the count?
Summary
- Each tool has its zone of strength — combining them is the professional pattern
- 3 typical layers: dependency vulnerabilities, SAST, secrets, + Claude Code for context
- Claude with context avoids duplicating findings from other tools
- A unified aggregator normalizes schemas and deduplicates
- Homogeneous severity after normalization (critical/high/medium/low)
- Periodic scans against main detect new CVEs in stable dependencies
- Don't replace — complement: Claude Code adds what static tools can't
Next capsule: 04 — Automatic rollback with triggers. You have robust pre-merge security scanning. But some issues only show up in production. Capsule 04 covers the post-deploy safety net: automatic rollback when metrics degrade.
Additional Resources
- Snyk — Dependency and code scanner
- pip-audit — PyPA's vulnerability scanner
- Bandit — Python SAST
- Semgrep — Multi-language SAST with custom rules
- gitleaks — Secrets detection
- GitHub Advanced Security — GitHub's official suite
- SARIF Standard — Standard format for security findings