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:

  1. 3 jobs in parallel (dependency-scan, sast-scan, secrets-scan) — independent tools run simultaneously
  2. Claude Code afterward consuming context from the previous ones (avoids duplicating findings another tool already detected)
  3. 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:

  1. pip-audit (or npm audit) in CI
  2. Bandit (or ESLint security)
  3. gitleaks
  4. 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:

  1. Reads the outputs of 3 tools
  2. Normalizes to the unified schema
  3. Deduplicates by (path, line, category)
  4. Generates the final report

Exercise 3: Claude with context (Hard)

Modify the Claude scan to:

  1. Read findings from pip-audit and bandit
  2. Pass them as context in the prompt
  3. Ask it to only report what's new
  4. 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

  1. Snyk — Dependency and code scanner
  2. pip-audit — PyPA's vulnerability scanner
  3. Bandit — Python SAST
  4. Semgrep — Multi-language SAST with custom rules
  5. gitleaks — Secrets detection
  6. GitHub Advanced Security — GitHub's official suite
  7. SARIF Standard — Standard format for security findings