Files
2026-07-13 12:12:01 +08:00

225 lines
8.0 KiB
Python

#!/usr/bin/env python3
"""Scan all skills for security issues and produce SECURITY.md."""
import os
import time
from datetime import datetime, timezone
from pathlib import Path
from dotenv import load_dotenv
from skill_scanner import SkillScanner
from skill_scanner.core.analyzers import (
BehavioralAnalyzer,
LLMAnalyzer,
TriggerAnalyzer,
)
from skill_scanner.core.loader import SkillLoadError
from skill_scanner.core.models import Report
from skill_scanner.core.scan_policy import ScanPolicy
load_dotenv()
SKILLS_DIR = "skills"
OUTPUT_FILE = "SECURITY.md"
def build_scanner() -> SkillScanner:
policy = ScanPolicy.from_preset("balanced")
policy.llm_analysis.max_instruction_body_chars = 75_000
policy.llm_analysis.max_referenced_file_chars = 75_000
policy.llm_analysis.max_code_file_chars = 75_000
policy.llm_analysis.max_total_prompt_chars = 500_000
llm_model = os.getenv("SKILL_SCANNER_LLM_MODEL", "anthropic/claude-sonnet-4-6")
llm_key = os.getenv("SKILL_SCANNER_LLM_API_KEY")
scanner = SkillScanner(
analyzers=[
BehavioralAnalyzer(),
TriggerAnalyzer(),
LLMAnalyzer(model=llm_model, api_key=llm_key, policy=policy),
],
policy=policy,
)
return scanner
def severity_badge(sev: str) -> str:
icons = {
"CRITICAL": "🔴",
"HIGH": "🟠",
"MEDIUM": "🟡",
"LOW": "🔵",
"INFO": "⚪",
"SAFE": "🟢",
}
return f"{icons.get(sev, '⚫')} {sev}"
def generate_report(report) -> str:
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
lines: list[str] = []
lines.append("# Security Scan Report")
lines.append("")
lines.append(f"**Generated:** {now} ")
lines.append(f"**Skills scanned:** {report.total_skills_scanned} ")
lines.append(f"**Total findings:** {report.total_findings} ")
lines.append(f"**Critical:** {report.critical_count} | **High:** {report.high_count} | **Safe skills:** {report.safe_count}/{report.total_skills_scanned}")
lines.append("")
# Summary table
lines.append("## Summary")
lines.append("")
lines.append("| Skill | Severity | Findings | Safe | Duration |")
lines.append("|-------|----------|----------|------|----------|")
sorted_results = sorted(
report.scan_results,
key=lambda r: ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO", "SAFE"].index(
r.max_severity.value if hasattr(r.max_severity, "value") else str(r.max_severity)
),
)
for result in sorted_results:
sev = result.max_severity.value if hasattr(result.max_severity, "value") else str(result.max_severity)
safe = "✅" if result.is_safe else "❌"
duration = f"{result.scan_duration_seconds:.1f}s"
lines.append(f"| {result.skill_name} | {severity_badge(sev)} | {len(result.findings)} | {safe} | {duration} |")
lines.append("")
# Per-skill details (only for skills with findings)
flagged = [r for r in sorted_results if r.findings]
if flagged:
lines.append("## Detailed Findings")
lines.append("")
for result in flagged:
sev = result.max_severity.value if hasattr(result.max_severity, "value") else str(result.max_severity)
lines.append(f"### {result.skill_name}{severity_badge(sev)}")
lines.append("")
for finding in result.findings:
fsev = finding.severity.value if hasattr(finding.severity, "value") else str(finding.severity)
lines.append(f"- **{severity_badge(fsev)}** `{finding.rule_id}` — {finding.title}")
if finding.description:
lines.append(f" > {finding.description}")
if finding.file_path:
loc = finding.file_path
if finding.line_number:
loc += f":{finding.line_number}"
lines.append(f" > File: `{loc}`")
if finding.remediation:
lines.append(f" > **Remediation:** {finding.remediation}")
lines.append("")
else:
lines.append("## Detailed Findings")
lines.append("")
lines.append("No findings to report — all skills passed.")
lines.append("")
return "\n".join(lines)
def scan_with_progress(scanner: SkillScanner, skills_dir: str) -> Report:
"""Run scan_directory logic with per-skill progress output."""
base = Path(skills_dir)
if not base.exists():
raise FileNotFoundError(f"Directory does not exist: {base}")
skill_dirs = sorted(
{p.parent for p in base.rglob("SKILL.md")},
key=lambda p: p.name,
)
total = len(skill_dirs)
if total == 0:
print(" No skills found.")
return Report()
print(f" Found {total} skills to scan\n")
report = Report()
loaded_skills = []
scan_start = time.time()
width = len(str(total))
longest_name = 0
for i, skill_dir in enumerate(skill_dirs, 1):
name = skill_dir.name
longest_name = max(longest_name, len(name))
counter = f"[{i:>{width}}/{total}]"
print(f" {counter} {name} ...", end="", flush=True)
t0 = time.time()
try:
skill = scanner.loader.load_skill(skill_dir)
result = scanner._scan_single_skill(skill, skill_dir)
report.add_scan_result(result)
loaded_skills.append(skill)
elapsed = time.time() - t0
sev = result.max_severity.value if hasattr(result.max_severity, "value") else str(result.max_severity)
tag = severity_badge(sev)
n_findings = len(result.findings)
detail = f"{n_findings} finding{'s' if n_findings != 1 else ''}" if n_findings else ""
print(f"\r {counter} {name:{longest_name}} {tag:18} {detail:20} ({elapsed:.1f}s)")
except SkillLoadError as e:
elapsed = time.time() - t0
print(f"\r {counter} {name:{longest_name}} ⚠️ SKIP ({e}) ({elapsed:.1f}s)")
report.skills_skipped.append({"skill": str(skill_dir), "reason": str(e)})
except Exception as e:
elapsed = time.time() - t0
print(f"\r {counter} {name:{longest_name}} ❌ ERROR ({e}) ({elapsed:.1f}s)")
report.skills_skipped.append({"skill": str(skill_dir), "reason": str(e)})
wall = time.time() - scan_start
if len(loaded_skills) > 1:
print("\n Running cross-skill overlap analysis ...", end="", flush=True)
t0 = time.time()
try:
overlap = scanner._check_description_overlap(loaded_skills)
from skill_scanner.core.analyzers.cross_skill_scanner import CrossSkillScanner
cross = CrossSkillScanner().analyze_skill_set(loaded_skills)
all_cross = [*list(overlap or []), *list(cross or [])]
if scanner.policy.disabled_rules:
all_cross = [f for f in all_cross if f.rule_id not in scanner.policy.disabled_rules]
if all_cross:
scanner._apply_severity_overrides(all_cross)
report.add_cross_skill_findings(all_cross)
elapsed = time.time() - t0
print(f" {len(all_cross)} finding{'s' if len(all_cross) != 1 else ''} ({elapsed:.1f}s)")
except Exception as e:
print(f" error: {e}")
print(f"\n Done in {wall:.1f}s")
return report
def main():
print("Building scanner (LLM + behavioral + trigger + balanced policy)...")
scanner = build_scanner()
print(f"Analyzers: {scanner.list_analyzers()}\n")
print(f"Scanning {SKILLS_DIR}/...")
report = scan_with_progress(scanner, SKILLS_DIR)
print(f"\nResults: {report.total_skills_scanned} skills, {report.total_findings} findings")
print(f" Critical: {report.critical_count} High: {report.high_count} Safe: {report.safe_count}")
md = generate_report(report)
with open(OUTPUT_FILE, "w") as f:
f.write(md)
print(f"\nReport written to {OUTPUT_FILE}")
if __name__ == "__main__":
main()