Skill: cr0m3/snyk-vulnerability-scanner (version 1.0.0)

--- name: snyk-vulnerability-scanner description: Automates Snyk security vulnerability scanning, GitHub issue reporting, and auto-fix PR creation for repositories. Use when scanning repositories for security vulnerabilities, generating vulnerability reports as GitHub issues, or automatically fixing vulnerabilities via pull requests. Requires Snyk CLI authentication, GitHub CLI authentication, and repository access permissions. Supports npm, Python, Gradle, and Maven projects. Creates PRs against dev branch by default. --- # Snyk Vulnerability Scanner Automated security vulnerability scanning and fixing for GitHub repositories using Snyk. ## What It Does 1. **Scans** repositories for security vulnerabilities using Snyk 2. **Reports** findings as GitHub issues (grouped by package) 3. **Auto-fixes** vulnerabilities by applying Snyk patches and creating PRs ## Prerequisites Before using this skill, ensure: 1. **Snyk CLI** is installed: `npm install -g snyk` and authenticated: `snyk auth` 2. **GitHub CLI** is installed: `brew install gh` and authenticated: `gh auth login` 3. **jq** is installed: `brew install jq` (for JSON processing) 4. You have **write access** to the target repository ## Workflow ``` Scan Repository → Create GitHub Issues → Auto-Fix → Create PR ``` ### Severity Levels - `critical` - Critical vulnerabilities only - `high` - High and critical vulnerabilities (default) - `medium` - Medium, high, and critical - `low` - All severities ## Usage ### Full Automated Workflow Run complete scan → report → fix workflow: ```bash ./scripts/run-full-workflow.sh <repo-url> [base-branch] [severity] [skip-issues] [skip-fix] [dry-run] ``` **Parameters:** - `repo-url` - Full GitHub URL (required) - `base-branch` - Target branch for PRs (default: `dev`) - `severity` - Comma-separated levels (default: `high,critical`) - `skip-issues` - `true` to skip GitHub issue creation (default: `false`) - `skip-fix` - `true` to skip auto-fix (default: `false`) - `dry-run` - `true` to simulate without making changes (default: `false`) **Examples:** ```bash # Full workflow with defaults ./scripts/run-full-workflow.sh https://github.com/owner/repo # Scan only, skip fixes ./scripts/run-full-workflow.sh https://github.com/owner/repo dev high false true false # Dry run - everything but no changes ./scripts/run-full-workflow.sh https://github.com/owner/repo dev high,critical false false true # Fix only (skip issues), target main branch ./scripts/run-full-workflow.sh https://github.com/owner/repo main high true false false ``` ### Step-by-Step Usage #### 1. Scan Only ```bash ./scripts/snyk-scan.sh <repo-url> [output-file] [severity-filter] ``` Generates a JSON file with vulnerability details. **Example:** ```bash ./scripts/snyk-scan.sh https://github.com/owner/repo results.json high,critical ``` #### 2. Create GitHub Issues ```bash python3 scripts/create-github-issues.py <results.json> <repo-url> ``` Creates one GitHub issue per vulnerable package, grouping all CVEs for that package. #### 3. Auto-Fix and Create PR ```bash ./scripts/snyk-auto-fix.sh <repo-url> [base-branch] [dry-run] ``` Applies Snyk fixes and creates a PR to the specified branch. **Example:** ```bash ./scripts/snyk-auto-fix.sh https://github.com/owner/repo dev false ``` ## Supported Package Managers - **npm/yarn** (`package.json`) - **Python** (`requirements.txt`, `Pipfile`, `pyproject.toml`) - **Gradle** (`build.gradle`) - **Maven** (`pom.xml`) ## GitHub Issues Issues are created with: - Labels: `security`, `vulnerability`, `snyk` - Package name in title with severity level - Detailed body with all CVEs for that package - Link to Snyk for more info - Auto-fix availability indicator Duplicate prevention: Issues won't be created if a similar issue already exists for the same package. ## Pull Requests PRs include: - Branch name: `snyk-fix-<timestamp>` - Body with fix summary, changed files, and remaining vulnerabilities - Labels: `security`, `dependencies`, `snyk` - Target: dev branch (configurable) ## Cron/Automated Runs For periodic scans, use the skill via cron: ```bash # Add to cron for daily scans at 9am 0 9 * * * cd ~/.openclaw/workspace/skills/snyk-vulnerability-scanner && ./scripts/run-full-workflow.sh https://github.com/owner/repo ``` Or via OpenClaw cron for direct integration: ```json { "name": "snyk-daily-scan", "schedule": { "kind": "cron", "expr": "0 9 * * *" }, "payload": { "kind": "agentTurn", "message": "Run Snyk vulnerability scan on https://github.com/owner/repo and create fixes for dev branch" } } ``` ## Scripts Reference | Script | Purpose | |--------|---------| | `run-full-workflow.sh` | Main entry point - runs complete workflow | | `snyk-scan.sh` | Scans repo, outputs JSON results | | `create-github-issues.py` | Creates GitHub issues from scan results | | `snyk-auto-fix.sh` | Applies fixes and creates PRs | ## Troubleshooting **"Snyk not authenticated"** → Run: `snyk auth` **"GitHub CLI not authenticated"** → Run: `gh auth login` **"No vulnerabilities found"** → Check Snyk dashboard for your project; may need to import repo first **"Permission denied" on scripts** → Run: `chmod +x scripts/*.sh` **Auto-fix not working** → Some vulnerabilities can't be auto-fixed; check Snyk dashboard for remediation advice **Dry run shows changes but real run doesn't** → Check that Snyk has fixable suggestions for the vulnerabilities; some require manual updates

path: scripts/create-github-issues.py
content: #!/usr/bin/env python3
"""
Create GitHub Issues from Snyk scan results.
Groups vulnerabilities by package and creates one issue per package.
"""

import json
import sys
import os
import subprocess
from datetime import datetime
from urllib.parse import urlparse
import hashlib

def run_gh_command(args):
    """Run a GitHub CLI command and return output."""
    cmd = ['gh'] + args
    result = subprocess.run(cmd, capture_output=True, text=True)
    return result

def extract_repo_info(repo_url):
    """Extract owner and repo name from GitHub URL."""
    parsed = urlparse(repo_url)
    path_parts = parsed.path.strip('/').split('/')
    if len(path_parts) >= 2:
        return path_parts[0], path_parts[1].replace('.git', '')
    return None, None

def create_issue_title(vuln):
    """Create a descriptive issue title."""
    pkg = vuln.get('packageName', 'unknown')
    severity = vuln.get('severity', 'unknown').upper()
    cve = vuln.get('identifiers', {}).get('CVE', [''])[0] if vuln.get('identifiers') else ''
    title = f"[{severity}] Security vulnerability in {pkg}"
    if cve:
        title += f" ({cve})"
    return title

def create_issue_body(vuln, repo_url):
    """Create detailed issue body."""
    pkg = vuln.get('packageName', 'unknown')
    severity = vuln.get('severity', 'unknown')
    version = vuln.get('version', 'unknown')
    title = vuln.get('title', 'No title')
    description = vuln.get('description', 'No description available')
    
    cves = vuln.get('identifiers', {}).get('CVE', [])
    cve_text = ', '.join(cves) if cves else 'N/A'
    
    vuln_id = vuln.get('id', 'unknown')
    snyk_link = vuln.get('links', {}).get('paths', '')
    
    body = f"""## Security Vulnerability Report

**Package:** `{pkg}`  
**Severity:** {severity.upper()}  
**Current Version:** {version}  
**Snyk ID:** {vuln_id}

### Description
{title}

{description}

### CVEs
{cve_text}

### Impact
This vulnerability affects the following:
- From: {vuln.get('from', ['unknown'])}

### Snyk Details
[View in Snyk]({snyk_link})

### Auto-Fix Available
{'✅ Yes - Snyk can automatically fix this' if vuln.get('isUpgradable') or vuln.get('isPatchable') else '❌ No automatic fix available'}

---
*Reported by Snyk Vulnerability Scanner*  
*Repository: {repo_url}*  
*Date: {datetime.now().isoformat()}*
"""
    return body

def issue_exists(owner, repo, title):
    """Check if an issue with similar title already exists."""
    result = run_gh_command([
        'issue', 'list',
        '--repo', f'{owner}/{repo}',
        '--state', 'open',
        '--json', 'title'
    ])
    
    if result.returncode != 0:
        return False
    
    try:
        issues = json.loads(result.stdout)
        for issue in issues:
            if title.split(']')[0] in issue.get('title', ''):
                return True
    except json.JSONDecodeError:
        pass
    
    return False

def create_issue(owner, repo, title, body, labels=None):
    """Create a GitHub issue."""
    labels = labels or ['security', 'vulnerability', 'snyk']
    
    args = [
        'issue', 'create',
        '--repo', f'{owner}/{repo}',
        '--title', title,
        '--body', body
    ]
    
    for label in labels:
        args.extend(['--label', label])
    
    result = run_gh_command(args)
    return result.returncode == 0

def main():
    if len(sys.argv) < 3:
        print("Usage: create-github-issues.py <snyk-results.json> <repo-url>")
        print("  snyk-results.json: Path to Snyk JSON output")
        print("  repo-url:          GitHub repository URL (e.g., https://github.com/owner/repo)")
        sys.exit(1)
    
    results_file = sys.argv[1]
    repo_url = sys.argv[2]
    
    # Check gh CLI
    result = run_gh_command(['auth', 'status'])
    if result.returncode != 0:
        print("Error: GitHub CLI not authenticated. Run: gh auth login")
        sys.exit(1)
    
    # Parse results
    try:
        with open(results_file, 'r') as f:
            data = json.load(f)
    except FileNotFoundError:
        print(f"Error: Results file not found: {results_file}")
        sys.exit(1)
    except json.JSONDecodeError:
        print(f"Error: Invalid JSON in results file")
        sys.exit(1)
    
    # Get vulnerabilities
    vulns = data.get('vulnerabilities', [])
    if not vulns:
        print("No vulnerabilities found.")
        sys.exit(0)
    
    owner, repo = extract_repo_info(repo_url)
    if not owner or not repo:
        print(f"Error: Could not extract repo info from URL: {repo_url}")
        sys.exit(1)
    
    print(f"Processing {len(vulns)} vulnerabilities for {owner}/{repo}...")
    
    created_count = 0
    skipped_count = 0
    
    # Group vulnerabilities by package
    packages = {}
    for vuln in vulns:
        pkg = vuln.get('packageName', 'unknown')
        if pkg not in packages:
            packages[pkg] = []
        packages[pkg].append(vuln)
    
    for pkg, pkg_vulns in packages.items():
        # Use the highest severity for the issue
        sorted_vulns = sorted(pkg_vulns, key=lambda x: {'critical': 0, 'high': 1, 'medium': 2, 'low': 3}.get(x.get('severity', 'low'), 4))
        primary_vuln = sorted_vulns[0]
        
        title = create_issue_title(primary_vuln)
        
        if issue_exists(owner, repo, title):
            print(f"  Skipping (already exists): {title}")
            skipped_count += 1
            continue
        
        # Include all CVEs in the body
        body = f"""## Security Vulnerabilities in {pkg}

**Package:** `{pkg}`  
**Repository:** [{owner}/{repo}]({repo_url})

### Found Vulnerabilities

"""
        for i, vuln in enumerate(sorted_vulns, 1):
            severity = vuln.get('severity', 'unknown').upper()
            cves = vuln.get('identifiers', {}).get('CVE', [])
            cve_text = ', '.join(cves) if cves else 'N/A'
            vuln_id = vuln.get('id', 'unknown')
            
            body += f"""#### {i}. [{severity}] {vuln.get('title', 'No title')}
- **CVE:** {cve_text}
- **Snyk ID:** {vuln_id}
- **Current Version:** {vuln.get('version', 'unknown')}
- **Auto-Fix:** {'✅ Available' if vuln.get('isUpgradable') or vuln.get('isPatchable') else '❌ Not available'}

"""
        
        body += f"""---
*Reported by Snyk Vulnerability Scanner*  
*Date: {datetime.now().isoformat()}*
"""
        
        if create_issue(owner, repo, title, body):
            print(f"  Created: {title}")
            created_count += 1
        else:
            print(f"  Failed to create: {title}")
    
    print(f"\nDone: {created_count} issues created, {skipped_count} skipped (already exist)")

if __name__ == '__main__':
    main()

path: scripts/run-full-workflow.sh
content: #!/bin/bash
#
# Snyk Full Workflow Script
# Complete automation: Scan -> Report Issues -> Auto-Fix
#

set -e

REPO_URL="${1:-}"
BASE_BRANCH="${2:-dev}"
SEVERITY="${3:-high,critical}"
SKIP_ISSUES="${4:-false}"
SKIP_FIX="${5:-false}"
DRY_RUN="${6:-false}"

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

banner() {
    echo -e "${BLUE}========================================${NC}"
    echo -e "${BLUE}$1${NC}"
    echo -e "${BLUE}========================================${NC}"
}

usage() {
    echo "Usage: run-full-workflow.sh <repo-url> [options]"
    echo ""
    echo "Options:"
    echo "  base-branch   Target branch for fixes (default: dev)"
    echo "  severity      Severity levels to report (default: high,critical)"
    echo "  skip-issues   true/false - Skip creating GitHub issues (default: false)"
    echo "  skip-fix      true/false - Skip auto-fix (default: false)"
    echo "  dry-run       true/false - Don't create PRs/issues (default: false)"
    echo ""
    echo "Examples:"
    echo "  run-full-workflow.sh https://github.com/owner/repo"
    echo "  run-full-workflow.sh https://github.com/owner/repo main high false false false"
    echo "  run-full-workflow.sh https://github.com/owner/repo dev high,critical false true true"
}

if [[ -z "$REPO_URL" ]]; then
    usage
    exit 1
fi

banner "Snyk Vulnerability Scanner - Full Workflow"
echo -e "${BLUE}Repository:${NC} $REPO_URL"
echo -e "${BLUE}Base Branch:${NC} $BASE_BRANCH"
echo -e "${BLUE}Severity:${NC} $SEVERITY"
echo -e "${BLUE}Skip Issues:${NC} $SKIP_ISSUES"
echo -e "${BLUE}Skip Fix:${NC} $SKIP_FIX"
echo -e "${BLUE}Dry Run:${NC} $DRY_RUN"
echo ""

# Check dependencies
echo -e "${BLUE}Checking dependencies...${NC}"
for cmd in snyk git gh jq python3; do
    if ! command -v "$cmd" &> /dev/null; then
        echo -e "${RED}Error: $cmd is not installed${NC}"
        exit 1
    fi
done
echo -e "${GREEN}✓ All dependencies found${NC}"
echo ""

# Generate unique output file
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
REPO_NAME=$(basename -s .git "$REPO_URL" 2>/dev/null || basename "$REPO_URL" .git)
OUTPUT_FILE="/tmp/snyk-${REPO_NAME}-${TIMESTAMP}.json"

# Step 1: Scan
banner "Step 1/3: Scanning Repository"
echo -e "${BLUE}Running Snyk scan...${NC}"

bash "$SCRIPT_DIR/snyk-scan.sh" "$REPO_URL" "$OUTPUT_FILE" "$SEVERITY" || {
    echo -e "${RED}Scan failed or no vulnerabilities found${NC}"
    exit 0
}

# Check scan results
if [[ ! -f "$OUTPUT_FILE" ]]; then
    echo -e "${YELLOW}No scan results generated${NC}"
    exit 0
fi

VULN_COUNT=$(jq '[.vulnerabilities[]?] | length' "$OUTPUT_FILE" 2>/dev/null || echo "0")
echo -e "${GREEN}✓ Scan complete - $VULN_COUNT vulnerabilities found${NC}"
echo ""

if [[ "$VULN_COUNT" -eq 0 ]]; then
    banner "No Vulnerabilities Found"
    rm -f "$OUTPUT_FILE"
    exit 0
fi

# Step 2: Create GitHub Issues
if [[ "$SKIP_ISSUES" != "true" ]]; then
    banner "Step 2/3: Creating GitHub Issues"
    
    if [[ "$DRY_RUN" == "true" ]]; then
        echo -e "${YELLOW}[DRY RUN] Would create issues for $VULN_COUNT vulnerabilities${NC}"
        python3 "$SCRIPT_DIR/create-github-issues.py" "$OUTPUT_FILE" "$REPO_URL" 2>&1 | sed 's/Created:/[DRY RUN] Would create:/' || true
    else
        python3 "$SCRIPT_DIR/create-github-issues.py" "$OUTPUT_FILE" "$REPO_URL" || {
            echo -e "${YELLOW}Issue creation completed with warnings${NC}"
        }
    fi
else
    banner "Step 2/3: Skipping GitHub Issues"
    echo -e "${YELLOW}Skipped (user request)${NC}"
fi
echo ""

# Step 3: Auto-Fix
if [[ "$SKIP_FIX" != "true" ]]; then
    banner "Step 3/3: Applying Auto-Fixes"
    
    if [[ "$DRY_RUN" == "true" ]]; then
        echo -e "${YELLOW}[DRY RUN] Mode enabled - no changes will be made${NC}"
    fi
    
    bash "$SCRIPT_DIR/snyk-auto-fix.sh" "$REPO_URL" "$BASE_BRANCH" "$DRY_RUN" || {
        echo -e "${YELLOW}Auto-fix completed with warnings${NC}"
    }
else
    banner "Step 3/3: Skipping Auto-Fix"
    echo -e "${YELLOW}Skipped (user request)${NC}"
fi

# Cleanup
rm -f "$OUTPUT_FILE"

echo ""
echo ""
banner "Workflow Complete"
echo -e "${GREEN}✓ Snyk security workflow finished for:${NC} $REPO_NAME"
echo -e "${BLUE}Repository:${NC} $REPO_URL"
echo -e "${BLUE}Target Branch:${NC} $BASE_BRANCH"

path: scripts/snyk-auto-fix.sh
content: #!/bin/bash
#
# Snyk Auto-Fix Script
# Applies Snyk fixes and creates a pull request to the dev branch
#

set -e

REPO_URL="${1:-}"
BASE_BRANCH="${2:-dev}"
DRY_RUN="${3:-false}"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

if [[ -z "$REPO_URL" ]]; then
    echo -e "${RED}Usage: snyk-auto-fix.sh <repo-url> [base-branch] [dry-run]${NC}"
    echo "  repo-url:     Full GitHub repository URL"
    echo "  base-branch:  Target branch for PR (default: dev)"
    echo "  dry-run:      true/false - Run without creating PR (default: false)"
    exit 1
fi

# Check dependencies
check_dependency() {
    if ! command -v "$1" &> /dev/null; then
        echo -e "${RED}Error: $1 is not installed${NC}"
        exit 1
    fi
}

check_dependency snyk
check_dependency git
check_dependency gh

# Verify GitHub auth
if ! gh auth status &> /dev/null; then
    echo -e "${RED}Error: GitHub CLI not authenticated. Run: gh auth login${NC}"
    exit 1
fi

# Verify Snyk auth
if ! snyk auth &> /dev/null; then
    echo -e "${RED}Error: Snyk not authenticated. Run: snyk auth${NC}"
    exit 1
fi

# Extract repo info
REPO_NAME=$(basename -s .git "$REPO_URL" 2>/dev/null || basename "$REPO_URL" .git)
REPO_OWNER=$(echo "$REPO_URL" | sed -n 's/.*github.com\/\([^\/]*\).*/\1/p')
WORKDIR=$(pwd)
TEMP_DIR=$(mktemp -d)
REPO_DIR="$TEMP_DIR/$REPO_NAME"

echo -e "${GREEN}Cloning repository: $REPO_URL${NC}"
git clone "$REPO_URL" "$REPO_DIR"
cd "$REPO_DIR"

# Checkout and pull base branch
echo -e "${GREEN}Checking out $BASE_BRANCH branch...${NC}"
git checkout "$BASE_BRANCH" 2>/dev/null || git checkout -b "$BASE_BRANCH"
git pull origin "$BASE_BRANCH" 2>/dev/null || true

# Run Snyk monitor to get project info
echo -e "${GREEN}Running initial Snyk scan...${NC}"
snyk test --severity-threshold=high --json > snyk-before.json 2>/dev/null || true

VULN_COUNT=$(jq '[.vulnerabilities[]?] | length' snyk-before.json 2>/dev/null || echo "0")
if [[ "$VULN_COUNT" -eq 0 ]]; then
    echo -e "${GREEN}No high/critical vulnerabilities found. Nothing to fix!${NC}"
    cd "$WORKDIR"
    rm -rf "$TEMP_DIR"
    exit 0
fi

echo -e "${YELLOW}Found $VULN_COUNT vulnerabilities to fix${NC}"

# Create fix branch
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
FIX_BRANCH="snyk-fix-$TIMESTAMP"
git checkout -b "$FIX_BRANCH"

# Try to apply Snyk fixes
echo -e "${GREEN}Attempting to auto-fix vulnerabilities...${NC}"

# Check for package manager files
HAS_NPM=0
HAS_PIP=0
HAS_GRADLE=0
HAS_MAVEN=0

[[ -f "package.json" ]] && HAS_NPM=1
[[ -f "requirements.txt" ]] && HAS_PIP=1
[[ -f "Pipfile" ]] && HAS_PIP=1
[[ -f "pyproject.toml" ]] && HAS_PIP=1
[[ -f "build.gradle" ]] && HAS_GRADLE=1
[[ -f "pom.xml" ]] && HAS_MAVEN=1

FIXES_APPLIED=0

# Run snyk fix for supported ecosystems
echo -e "${GREEN}Running snyk fix...${NC}"
if snyk fix --all-projects -d 2>&1 | tee snyk-fix-output.log; then
    echo -e "${GREEN}Snyk fix completed${NC}"
    FIXES_APPLIED=1
else
    echo -e "${YELLOW}Snyk fix may have partial results. Check output.${NC}"
    # Check if any files were modified
    if [[ -n "$(git status --porcelain)" ]]; then
        FIXES_APPLIED=1
    fi
fi

# Check again for vulnerabilities after fix
echo -e "${GREEN}Running post-fix Snyk scan...${NC}"
snyk test --severity-threshold=high --json > snyk-after.json 2>/dev/null || true

VULN_COUNT_AFTER=$(jq '[.vulnerabilities[]?] | length' snyk-after.json 2>/dev/null || echo "0")
FIXED_COUNT=$((VULN_COUNT - VULN_COUNT_AFTER))

echo -e "${GREEN}Fixed $FIXED_COUNT vulnerabilities${NC}"
echo -e "${YELLOW}$VULN_COUNT_AFTER vulnerabilities remain${NC}"

# Check if there are any changes to commit
if [[ -z "$(git status --porcelain)" ]]; then
    echo -e "${YELLOW}No changes to commit. Auto-fix may not be available for these vulnerabilities.${NC}"
    cd "$WORKDIR"
    rm -rf "$TEMP_DIR"
    exit 0
fi

# Generate PR description
PR_BODY="## 🔒 Snyk Security Fixes

This PR contains automated security fixes from Snyk.

### Changes Summary
- **Fixed:** $FIXED_COUNT vulnerabilities
- **Remaining:** $VULN_COUNT_AFTER vulnerabilities (require manual attention)
- **Branch:** $FIX_BRANCH

### Files Modified
\`\`\`
$(git diff --name-only)
\`\`\`

### Package Managers Updated
"

[[ "$HAS_NPM" -eq 1 ]] && PR_BODY+="- npm/yarn packages\n"
[[ "$HAS_PIP" -eq 1 ]] && PR_BODY+="- Python packages\n"
[[ "$HAS_GRADLE" -eq 1 ]] && PR_BODY+="- Gradle dependencies\n"
[[ "$HAS_MAVEN" -eq 1 ]] && PR_BODY+="- Maven dependencies\n"

PR_BODY+="
### Notes
- Auto-generated by Snyk Vulnerability Scanner
- Please review changes before merging
- Run tests to ensure compatibility

### Remaining Vulnerabilities
$VULN_COUNT_AFTER high/critical severity vulnerabilities still require manual attention.
"

# Commit changes
git config user.name "Snyk Security Bot" || true
git config user.email "[REDACTED_SECRET]" || true
git add -
git commit -m "fix(security): automated fixes by Snyk

Applied security patches for $FIXED_COUNT vulnerabilities
Remaining: $VULN_COUNT_AFTER vulnerabilities"

if [[ "$DRY_RUN" == "true" ]]; then
    echo -e "${GREEN}[DRY RUN] Would create PR with the following changes:${NC}"
    echo -e "${GREEN}Branch: $FIX_BRANCH${NC}"
    echo "---"
    git diff --stat HEAD~
    echo "---"
    cat snyk-after.json | jq '.vulnerabilities | group_by(.packageName) | map({package: .[0].packageName, count: length})' 2>/dev/null || true
    cd "$WORKDIR"
    rm -rf "$TEMP_DIR"
    exit 0
fi

# Push branch
echo -e "${GREEN}Pushing branch to origin...${NC}"
git push origin "$FIX_BRANCH"

# Create PR
echo -e "${GREEN}Creating Pull Request...${NC}"
PR_TITLE="🔒 Security: Automated fixes by Snyk"
cd "$WORKDIR"

PR_URL=$(cd "$TEMP_DIR/$REPO_NAME" && gh pr create \
    --title "$PR_TITLE" \
    --body "$PR_BODY" \
    --base "$BASE_BRANCH" \
    --head "$FIX_BRANCH" \
    --label "security" \
    --label "dependencies" \
    --label "snyk" 2>&1 | grep -o 'https://github.com[^[:space:]]*') || PR_URL=""

if [[ -n "$PR_URL" ]]; then
    echo -e "${GREEN}✅ Pull Request created: $PR_URL${NC}"
else
    echo -e "${YELLOW}⚠️ Could not get PR URL. Check GitHub for the PR.${NC}"
fi

# Cleanup
rm -rf "$TEMP_DIR"

echo -e "${GREEN}Done!${NC}"

path: scripts/snyk-scan.sh
content: #!/bin/bash
#
# Snyk Vulnerability Scanner Script
# Scans a GitHub repository for vulnerabilities and outputs JSON results
#

set -e

REPO_URL="${1:-}"
OUTPUT_FILE="${2:-snyk-scan-results.json}"
SEVERITY_FILTER="${3:-high,critical}"

if [[ -z "$REPO_URL" ]]; then
    echo "Usage: snyk-scan.sh <repo-url> [output-file] [severity-filter]"
    echo "  repo-url:        Full GitHub repository URL (e.g., https://github.com/owner/repo)"
    echo "  output-file:     Output JSON file (default: snyk-scan-results.json)"
    echo "  severity-filter: Comma-separated severity levels (default: high,critical)"
    exit 1
fi

# Check if snyk CLI is installed
if ! command -v snyk &> /dev/null; then
    echo "Error: Snyk CLI is not installed. Install via: npm install -g snyk"
    exit 1
fi

# Check if authenticated
if ! snyk auth &> /dev/null; then
    echo "Error: Snyk not authenticated. Run: snyk auth"
    exit 1
fi

# Extract repo name from URL
REPO_NAME=$(basename -s .git "$REPO_URL" 2>/dev/null || basename "$REPO_URL" .git)
TEMP_DIR=$(mktemp -d)
CLONE_DIR="$TEMP_DIR/$REPO_NAME"

echo "Cloning repository: $REPO_URL"
git clone --depth 1 "$REPO_URL" "$CLONE_DIR"
cd "$CLONE_DIR"

echo "Running Snyk test with severity filter: $SEVERITY_FILTER"

# Run snyk test and output JSON
snyk test --severity-threshold=${SEVERITY_FILTER%,*} --json --all-projects > "$OUTPUT_FILE" 2>/dev/null || true

# Check if output was created
if [[ ! -f "$OUTPUT_FILE" ]]; then
    # Try without --all-projects
    snyk test --severity-threshold=${SEVERITY_FILTER%,*} --json > "$OUTPUT_FILE" 2>/dev/null || true
fi

if [[ -f "$OUTPUT_FILE" && -s "$OUTPUT_FILE" ]]; then
    echo "Scan complete. Results saved to: $OUTPUT_FILE"
    cat "$OUTPUT_FILE"
else
    echo "No vulnerabilities found or scan failed."
    echo '{"vulnerabilities": [], "ok": true}' > "$OUTPUT_FILE"
    cat "$OUTPUT_FILE"
fi

# Cleanup
cd - > /dev/null
rm -rf "$TEMP_DIR"

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)