New skills covering 10 categories: **Security & Audit**: 007 (STRIDE/PASTA/OWASP), cred-omega (secrets management) **AI Personas**: Karpathy, Hinton, Sutskever, LeCun (4 sub-skills), Altman, Musk, Gates, Jobs, Buffett **Multi-agent Orchestration**: agent-orchestrator, task-intelligence, multi-advisor **Code Analysis**: matematico-tao (Terence Tao-inspired mathematical code analysis) **Social & Messaging**: Instagram Graph API, Telegram Bot, WhatsApp Cloud API, social-orchestrator **Image Generation**: AI Studio (Gemini), Stability AI, ComfyUI Gateway, image-studio router **Brazilian Domain**: 6 auction specialist modules, 2 legal advisors, auctioneers data scraper **Product & Growth**: design, invention, monetization, analytics, growth engine **DevOps & LLM Ops**: Docker/CI-CD/AWS, RAG/embeddings/fine-tuning **Skill Governance**: installer, sentinel auditor, context management Each skill includes: - Standardized YAML frontmatter (name, description, risk, source, tags, tools) - Structured sections (Overview, When to Use, How it Works, Best Practices) - Python scripts and reference documentation where applicable - Cross-platform compatibility (Claude Code, Antigravity, Cursor, Gemini CLI, Codex CLI) Co-authored-by: ProgramadorBrasil <214873561+ProgramadorBrasil@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
482 lines
14 KiB
Python
482 lines
14 KiB
Python
"""007 Quick Scan -- Fast automated security scan of a target directory.
|
|
|
|
Recursively scans files in a target directory for secret patterns, dangerous
|
|
code constructs, permission issues, and oversized files. Produces a scored
|
|
summary report in text or JSON format.
|
|
|
|
Usage:
|
|
python quick_scan.py --target /path/to/project
|
|
python quick_scan.py --target /path/to/project --output json --verbose
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import stat
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Imports from the 007 config hub (same directory)
|
|
# ---------------------------------------------------------------------------
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
|
from config import (
|
|
SCANNABLE_EXTENSIONS,
|
|
SKIP_DIRECTORIES,
|
|
SECRET_PATTERNS,
|
|
DANGEROUS_PATTERNS,
|
|
LIMITS,
|
|
SEVERITY,
|
|
ensure_directories,
|
|
get_verdict,
|
|
get_timestamp,
|
|
log_audit_event,
|
|
setup_logging,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constants local to the quick scan
|
|
# ---------------------------------------------------------------------------
|
|
|
|
SCORE_DEDUCTIONS = {
|
|
"CRITICAL": 10,
|
|
"HIGH": 5,
|
|
"MEDIUM": 2,
|
|
"LOW": 1,
|
|
"INFO": 0,
|
|
}
|
|
|
|
REDACT_KEEP_CHARS = 6 # Number of leading chars to keep in redacted snippets
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _redact(text: str) -> str:
|
|
"""Return a redacted version of *text*, keeping only the first few chars."""
|
|
text = text.strip()
|
|
if len(text) <= REDACT_KEEP_CHARS:
|
|
return text
|
|
return text[:REDACT_KEEP_CHARS] + "****"
|
|
|
|
|
|
def _snippet(line: str, match_start: int, context: int = 40) -> str:
|
|
"""Extract a short redacted snippet around the match position."""
|
|
start = max(0, match_start - context // 2)
|
|
end = min(len(line), match_start + context)
|
|
raw = line[start:end].strip()
|
|
return _redact(raw)
|
|
|
|
|
|
def _should_skip_dir(name: str) -> bool:
|
|
"""Return True if directory *name* should be skipped."""
|
|
return name in SKIP_DIRECTORIES
|
|
|
|
|
|
def _is_scannable(path: Path) -> bool:
|
|
"""Return True if the file extension is in the SCANNABLE_EXTENSIONS set."""
|
|
# Handle compound suffixes like .env.example
|
|
name = path.name
|
|
for ext in SCANNABLE_EXTENSIONS:
|
|
if name.endswith(ext):
|
|
return True
|
|
# Also check the normal suffix
|
|
return path.suffix.lower() in SCANNABLE_EXTENSIONS
|
|
|
|
|
|
def _check_permissions(filepath: Path) -> dict | None:
|
|
"""Check for overly permissive file modes on Unix-like systems.
|
|
|
|
Returns a finding dict or None.
|
|
"""
|
|
# Only meaningful on systems that implement os.stat st_mode properly
|
|
if sys.platform == "win32":
|
|
return None
|
|
try:
|
|
mode = filepath.stat().st_mode
|
|
perms = stat.S_IMODE(mode)
|
|
if perms & 0o777 == 0o777:
|
|
return {
|
|
"type": "permission",
|
|
"pattern": "world_rwx_0777",
|
|
"severity": "HIGH",
|
|
"file": str(filepath),
|
|
"line": 0,
|
|
"snippet": f"mode={oct(perms)}",
|
|
}
|
|
if perms & 0o666 == 0o666:
|
|
return {
|
|
"type": "permission",
|
|
"pattern": "world_rw_0666",
|
|
"severity": "MEDIUM",
|
|
"file": str(filepath),
|
|
"line": 0,
|
|
"snippet": f"mode={oct(perms)}",
|
|
}
|
|
except OSError:
|
|
pass
|
|
return None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Core scanning logic
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def collect_files(target: Path, logger) -> list[Path]:
|
|
"""Walk *target* recursively and return scannable file paths.
|
|
|
|
Respects SKIP_DIRECTORIES and SCANNABLE_EXTENSIONS from config.
|
|
Stops at LIMITS['max_files_per_scan'] with a warning.
|
|
"""
|
|
files: list[Path] = []
|
|
max_files = LIMITS["max_files_per_scan"]
|
|
|
|
for root, dirs, filenames in os.walk(target):
|
|
# Prune skipped directories in-place so os.walk does not descend
|
|
dirs[:] = [d for d in dirs if not _should_skip_dir(d)]
|
|
|
|
for fname in filenames:
|
|
if len(files) >= max_files:
|
|
logger.warning(
|
|
"Reached max_files_per_scan limit (%d). Stopping collection.", max_files
|
|
)
|
|
return files
|
|
|
|
fpath = Path(root) / fname
|
|
if _is_scannable(fpath):
|
|
files.append(fpath)
|
|
|
|
return files
|
|
|
|
|
|
def scan_file(filepath: Path, verbose: bool = False, logger=None) -> list[dict]:
|
|
"""Scan a single file for secrets and dangerous patterns.
|
|
|
|
Returns a list of finding dicts.
|
|
"""
|
|
findings: list[dict] = []
|
|
max_findings = LIMITS["max_findings_per_file"]
|
|
|
|
try:
|
|
size = filepath.stat().st_size
|
|
except OSError:
|
|
return findings
|
|
|
|
# Large file check
|
|
if size > LIMITS["max_file_size_bytes"]:
|
|
findings.append({
|
|
"type": "large_file",
|
|
"pattern": "exceeds_max_size",
|
|
"severity": "INFO",
|
|
"file": str(filepath),
|
|
"line": 0,
|
|
"snippet": f"size={size} bytes (limit={LIMITS['max_file_size_bytes']})",
|
|
})
|
|
return findings
|
|
|
|
# Permission check
|
|
perm_finding = _check_permissions(filepath)
|
|
if perm_finding:
|
|
findings.append(perm_finding)
|
|
|
|
# Read file content
|
|
try:
|
|
text = filepath.read_text(encoding="utf-8", errors="replace")
|
|
except OSError as exc:
|
|
if verbose and logger:
|
|
logger.debug("Cannot read %s: %s", filepath, exc)
|
|
return findings
|
|
|
|
lines = text.splitlines()
|
|
|
|
for line_num, line in enumerate(lines, start=1):
|
|
if len(findings) >= max_findings:
|
|
break
|
|
|
|
# -- Secret patterns --
|
|
for pattern_name, regex, severity in SECRET_PATTERNS:
|
|
m = regex.search(line)
|
|
if m:
|
|
findings.append({
|
|
"type": "secret",
|
|
"pattern": pattern_name,
|
|
"severity": severity,
|
|
"file": str(filepath),
|
|
"line": line_num,
|
|
"snippet": _snippet(line, m.start()),
|
|
})
|
|
|
|
# -- Dangerous code patterns --
|
|
for pattern_name, regex, severity in DANGEROUS_PATTERNS:
|
|
m = regex.search(line)
|
|
if m:
|
|
findings.append({
|
|
"type": "dangerous_code",
|
|
"pattern": pattern_name,
|
|
"severity": severity,
|
|
"file": str(filepath),
|
|
"line": line_num,
|
|
"snippet": "",
|
|
})
|
|
|
|
return findings
|
|
|
|
|
|
def compute_score(findings: list[dict]) -> int:
|
|
"""Compute a quick score starting at 100, deducting by severity.
|
|
|
|
Returns an integer score clamped between 0 and 100.
|
|
"""
|
|
score = 100
|
|
for f in findings:
|
|
deduction = SCORE_DEDUCTIONS.get(f["severity"], 0)
|
|
score -= deduction
|
|
return max(0, score)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Aggregation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def aggregate_by_severity(findings: list[dict]) -> dict[str, int]:
|
|
"""Count findings per severity level."""
|
|
counts: dict[str, int] = {sev: 0 for sev in SEVERITY}
|
|
for f in findings:
|
|
sev = f.get("severity", "INFO")
|
|
if sev in counts:
|
|
counts[sev] += 1
|
|
return counts
|
|
|
|
|
|
def top_critical_findings(findings: list[dict], n: int = 10) -> list[dict]:
|
|
"""Return the top *n* most critical findings, sorted by severity weight."""
|
|
sorted_findings = sorted(
|
|
findings,
|
|
key=lambda f: SEVERITY.get(f.get("severity", "INFO"), 0),
|
|
reverse=True,
|
|
)
|
|
return sorted_findings[:n]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Report formatters
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def format_text_report(
|
|
target: str,
|
|
total_files: int,
|
|
findings: list[dict],
|
|
severity_counts: dict[str, int],
|
|
score: int,
|
|
verdict: dict,
|
|
elapsed: float,
|
|
) -> str:
|
|
"""Build a human-readable text report."""
|
|
lines: list[str] = []
|
|
|
|
lines.append("=" * 70)
|
|
lines.append(" 007 QUICK SCAN REPORT")
|
|
lines.append("=" * 70)
|
|
lines.append("")
|
|
|
|
# Metadata
|
|
lines.append(f" Target: {target}")
|
|
lines.append(f" Timestamp: {get_timestamp()}")
|
|
lines.append(f" Duration: {elapsed:.2f}s")
|
|
lines.append(f" Files scanned: {total_files}")
|
|
lines.append(f" Total findings: {len(findings)}")
|
|
lines.append("")
|
|
|
|
# Severity breakdown
|
|
lines.append("-" * 70)
|
|
lines.append(" FINDINGS BY SEVERITY")
|
|
lines.append("-" * 70)
|
|
for sev in ("CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"):
|
|
count = severity_counts.get(sev, 0)
|
|
bar = "#" * min(count, 40)
|
|
lines.append(f" {sev:<10} {count:>5} {bar}")
|
|
lines.append("")
|
|
|
|
# Top critical findings
|
|
top = top_critical_findings(findings)
|
|
if top:
|
|
lines.append("-" * 70)
|
|
lines.append(" TOP FINDINGS (most critical first)")
|
|
lines.append("-" * 70)
|
|
for i, f in enumerate(top, start=1):
|
|
loc = f"{f['file']}:{f['line']}"
|
|
snippet_part = f" [{_redact(f['snippet'])}]" if f.get("snippet") else ""
|
|
lines.append(
|
|
f" {i:>2}. [{f['severity']:<8}] {f['type']}/{f['pattern']}"
|
|
)
|
|
lines.append(
|
|
f" {loc}{snippet_part}"
|
|
)
|
|
lines.append("")
|
|
|
|
# Score and verdict
|
|
lines.append("=" * 70)
|
|
lines.append(f" QUICK SCORE: {score} / 100")
|
|
lines.append(f" VERDICT: {verdict['emoji']} {verdict['label']}")
|
|
lines.append(f" {verdict['description']}")
|
|
lines.append("=" * 70)
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def build_json_report(
|
|
target: str,
|
|
total_files: int,
|
|
findings: list[dict],
|
|
severity_counts: dict[str, int],
|
|
score: int,
|
|
verdict: dict,
|
|
elapsed: float,
|
|
) -> dict:
|
|
"""Build a structured JSON-serializable report dict."""
|
|
return {
|
|
"scan": "quick_scan",
|
|
"target": target,
|
|
"timestamp": get_timestamp(),
|
|
"duration_seconds": round(elapsed, 3),
|
|
"total_files_scanned": total_files,
|
|
"total_findings": len(findings),
|
|
"severity_counts": severity_counts,
|
|
"score": score,
|
|
"verdict": {
|
|
"label": verdict["label"],
|
|
"description": verdict["description"],
|
|
"emoji": verdict["emoji"],
|
|
},
|
|
"findings": findings,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main entry point
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def run_scan(target_path: str, output_format: str = "text", verbose: bool = False) -> dict:
|
|
"""Execute the quick scan and return the JSON-style report dict.
|
|
|
|
Also prints the report to stdout in the requested format.
|
|
"""
|
|
logger = setup_logging("007-quick-scan")
|
|
ensure_directories()
|
|
|
|
target = Path(target_path).resolve()
|
|
if not target.exists():
|
|
logger.error("Target path does not exist: %s", target)
|
|
sys.exit(1)
|
|
if not target.is_dir():
|
|
logger.error("Target is not a directory: %s", target)
|
|
sys.exit(1)
|
|
|
|
logger.info("Starting quick scan of %s", target)
|
|
start_time = time.time()
|
|
|
|
# Collect files
|
|
files = collect_files(target, logger)
|
|
total_files = len(files)
|
|
logger.info("Collected %d scannable files", total_files)
|
|
|
|
# Scan each file
|
|
all_findings: list[dict] = []
|
|
max_report_findings = LIMITS["max_report_findings"]
|
|
|
|
for fpath in files:
|
|
if len(all_findings) >= max_report_findings:
|
|
logger.warning(
|
|
"Reached max_report_findings limit (%d). Truncating.", max_report_findings
|
|
)
|
|
break
|
|
|
|
file_findings = scan_file(fpath, verbose=verbose, logger=logger)
|
|
remaining = max_report_findings - len(all_findings)
|
|
all_findings.extend(file_findings[:remaining])
|
|
|
|
elapsed = time.time() - start_time
|
|
logger.info(
|
|
"Scan complete: %d files, %d findings in %.2fs",
|
|
total_files, len(all_findings), elapsed,
|
|
)
|
|
|
|
# Aggregation
|
|
severity_counts = aggregate_by_severity(all_findings)
|
|
score = compute_score(all_findings)
|
|
verdict = get_verdict(score)
|
|
|
|
# Audit log
|
|
log_audit_event(
|
|
action="quick_scan",
|
|
target=str(target),
|
|
result=f"score={score}, findings={len(all_findings)}, verdict={verdict['label']}",
|
|
details={
|
|
"total_files": total_files,
|
|
"severity_counts": severity_counts,
|
|
"duration_seconds": round(elapsed, 3),
|
|
},
|
|
)
|
|
|
|
# Build structured report (always, for return value)
|
|
report = build_json_report(
|
|
target=str(target),
|
|
total_files=total_files,
|
|
findings=all_findings,
|
|
severity_counts=severity_counts,
|
|
score=score,
|
|
verdict=verdict,
|
|
elapsed=elapsed,
|
|
)
|
|
|
|
# Output
|
|
if output_format == "json":
|
|
print(json.dumps(report, indent=2, ensure_ascii=False))
|
|
else:
|
|
print(format_text_report(
|
|
target=str(target),
|
|
total_files=total_files,
|
|
findings=all_findings,
|
|
severity_counts=severity_counts,
|
|
score=score,
|
|
verdict=verdict,
|
|
elapsed=elapsed,
|
|
))
|
|
|
|
return report
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(
|
|
description="007 Quick Scan -- Fast automated security scan of a target directory.",
|
|
epilog="Example: python quick_scan.py --target ./my-project --output json --verbose",
|
|
)
|
|
parser.add_argument(
|
|
"--target",
|
|
required=True,
|
|
help="Path to the directory to scan (required).",
|
|
)
|
|
parser.add_argument(
|
|
"--output",
|
|
choices=["text", "json"],
|
|
default="text",
|
|
help="Output format: 'text' (default) or 'json'.",
|
|
)
|
|
parser.add_argument(
|
|
"--verbose",
|
|
action="store_true",
|
|
default=False,
|
|
help="Enable verbose logging (debug-level messages).",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
run_scan(target_path=args.target, output_format=args.output, verbose=args.verbose)
|