Argus — Code Intelligence Scanner

Argus — The Hundred-Eyed. Scan your Python or JavaScript codebase for bugs, security vulnerabilities, code smells, and common anti-patterns — get a prioritised findings report with line numbers, severity ratings, and fix suggestions. No external tools required.

Audits

Pass

Install

openclaw skills install argus1

Argus — Code Intelligence Scanner

Scan any Python or JavaScript codebase for bugs, security holes, and anti-patterns in seconds — with line numbers, severity ratings, and actionable fix suggestions.

What Argus catches

Security — SQL injection patterns, hardcoded secrets, eval/exec abuse, innerHTML XSS, open redirects, pickle deserialization
Bugs — mutable default arguments, bare except clauses, missing null checks, == None comparisons
Code quality — broad imports, print statements in production, TODO/FIXME markers, deeply nested code
JavaScript — var usage, loose equality (==), console.log left in, prototype pollution patterns

Quick start

SOURCE_PATH="./src" LANGUAGE="python" SEVERITY_FILTER="high" openclaw skills run argus1

🔒 Security

Reads local files only. Nothing transmitted outside your machine.


Step 1 — Install

pip3 install rich --break-system-packages --quiet

Step 2 — Scan for bugs and vulnerabilities

import os, re, json
from pathlib import Path
from datetime import date
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich import box

console = Console()

SRC_PATH   = os.environ.get("SOURCE_PATH", ".").strip()
LANGUAGE   = os.environ.get("LANGUAGE", "auto").lower().strip()
SEV_FILTER = os.environ.get("SEVERITY_FILTER", "all").lower().strip()
try:
    MAX_FINDINGS = int(os.environ.get("MAX_FINDINGS", "50"))
except ValueError:
    MAX_FINDINGS = 50
OUTPUT_JSON = os.environ.get("OUTPUT_JSON", "false").lower() == "true"
TODAY       = date.today()

src = Path(SRC_PATH)
if not src.exists():
    console.print(f"[red]❌ Path not found: {SRC_PATH}[/red]")
    raise SystemExit(1)

def detect_lang(path: Path) -> str:
    py = len(list(path.rglob("*.py") if path.is_dir() else ([path] if str(path).endswith(".py") else [])))
    js = len(list(path.rglob("*.js") if path.is_dir() else ([path] if str(path).endswith(".js") else [])))
    if py >= js:
        return "python"
    return "javascript"

lang = LANGUAGE if LANGUAGE != "auto" else detect_lang(src)
ext  = "*.py" if lang == "python" else "*.js"

# ── Rule definitions: (id, severity, category, regex, message, fix) ──────────
PYTHON_RULES = [
    ("PY001", "critical", "security",  r"\beval\s*\(",                     "eval() executes arbitrary code — extremely dangerous with user input.",               "Use ast.literal_eval() for safe evaluation of literals."),
    ("PY002", "critical", "security",  r"\bexec\s*\(",                     "exec() executes arbitrary strings as Python code.",                                   "Refactor to avoid dynamic code execution."),
    ("PY003", "critical", "security",  r"\bpickle\.loads?\s*\(",           "pickle.load() can execute arbitrary code when deserialising untrusted data.",         "Use json.loads() or a safe serialisation format instead."),
    ("PY004", "high",     "security",  r"(?i)(password|secret|api_key|token|auth_key)\s*=\s*['\"].+['\"]", "Hardcoded credential detected.",                      "Store secrets in environment variables, never in source code."),
    ("PY005", "high",     "security",  r"shell\s*=\s*True",                "shell=True in subprocess is a command-injection risk.",                               "Pass a list of arguments instead: subprocess.run(['cmd', 'arg'])"),
    ("PY006", "high",     "security",  r"\.execute\s*\(.*(%|\.format\(|f['\"])", "Potential SQL injection via string formatting in execute().",                    "Use parameterised queries: cursor.execute(sql, (param,))"),
    ("PY007", "medium",   "bug",       r"def\s+\w+\s*\([^)]*=\s*\[\s*\]", "Mutable default argument [] └ shared across all calls.",                               "Use None as default and initialise inside the function."),
    ("PY008", "medium",   "bug",       r"def\s+\w+\s*\([^)]*=\s*\{\s*\}", "Mutable default argument {} — shared across all calls.",                             "Use None as default and initialise inside the function."),
    ("PY009", "medium",   "bug",       r"except\s*:",                      "Bare except: catches SystemExit and KeyboardInterrupt — almost never intentional.",   "Catch specific exceptions: except ValueError: or except Exception:"),
    ("PY010", "medium",   "bug",       r"==\s*None\b|\bNone\s*==",         "Use 'is None' instead of '== None'.",                                                "Replace '== None' with 'is None' and '!= None' with 'is not None'."),
    ("PY011", "medium",   "bug",       r"!=\s*None\b|\bNone\s*!=",         "Use 'is not None' instead of '!= None'.",                                            "Replace '!= None' with 'is not None'."),
    ("PY012", "medium",   "quality",   r"\bprint\s*\(",                    "print() statement found — likely debug output left in production code.",              "Remove or interface in logging.debug() / logging.info()."),
    ("PY013", "low",      "quality",   r"#\s*(TODO|FIXME|HACK|XXX|BUG)\b", "Unresolved TODO/FIXME marker.",                                                      "Resolve or create a tracked issue for this item."),
    ("PY014", "low",      "quality",   r"from\s+\w+\s+import\s+\*",        "Wildcard import pollutes namespace and makes dependencies unclear.",                  "Import only what you need: from module import specific_name"),
    ("PY015", "low",      "quality",   r"import\s+pdb\b",                  "Debugger import (pdb) left in code.",                                                 "Remove pdb import before committing to production."),
    ("PY016", "medium",   "security",  r"hashlib\.(md5|sha1)\s*\(",        "MD5/SHA1 are cryptographically weak — not suitable for passwords or signatures.",     "Use hashlib.sha256() or better; bcrypt/argon2 for passwords."),
    ("PY017", "high",     "security",  r"random\.(random|randint|choice)\s*\(", "random module is not cryptographically secure.",                                 "Use secrets module for security-sensitive random values."),
    ("PY018", "medium",   "bug",       r"open\s*\(.+\)(?!\s*as\b)(?!.*with\b)", "File opened without context manager — may leak file handle.",                   "Use 'with open(...) as f:' to ensure proper cleanup."),
    ("PY019", "low",      "quality",   r"lambda\s+\w+:\s*\w+\s*\(",        "Lambda used where a named function would be clearer.",                               "Consider replacing with a def statement for readability."),
    ("PY020", "medium",   "bug",       r"\btype\s*\(^w+\)\s*==\s*",        "Use isinstance() instead of type() == for type checks.",                             "Replace type(x) == SomeType with isinstance(x, SomeType)."),
]

JS_RULES = [
    ("JS001", "high",     "quality",   r"\bvar\s+",                        "var is function-scoped and hoisted — causes subtle bugs.",                            "Use const or let instead."),
    ("JS002", "critical", "security",  r"\.innerHTML\s*=",                 "innerHTML assignment is an XSS vector if content comes from user input.",             "Use textContent for text or DOMPurify to sanitise HTML."),
    ("JS003", "critical", "security",  r"\beval\s*\(",                     "eval() executes arbitrary JavaScript — a critical security risk.",                       "Refactor to eliminate eval() usage entirely."),
    ("JS004", "high",     "security",  r"(?i)(api_key|apikey|api_secret|access_token|auth_token|password)\s*=\s*['\"].+['\"]", "Hardcoded credential detected.", "Move secrets to environment variables or a secrets manager."),
    ("JS005", "medium",   "bug",       r"==\s*null\b(?!\=)|null\s*==(?!\=)", "Loose equality with null also matches undefined — usually unintentional.",           "Use === null or check explicitly for both null and undefined."),
    ("JS006", "medium",   "bug",       r"(?<!=)==(?!=)",                    "Loose equality (==) performs type coercion — a common source of bugs.",              "Use strict equality (===) instead."),
    ("JS007", "medium",   "quality",   r"\bconsole\.(log|warn|error|debug)\s*\(", "console.log left in production code.",                                         "Remove or replace with a proper logging library."),
    ("JS008", "medium",   "security",  r"document\.write\s*\(",            "document.write() can overwrite the entire page and is an XSS risk.",                 "Use DOM manipulation methods (createElement, appendChild) instead."),
    ("JS009", "low",      "quality",   r"//\s*(TODO|FIXME|HACK|XXX|BUG)\b", "Unresolved TODO/FIXME marker.",                                                    "Resolve or create a tracked issue for this item."),
    ("JS010", "high",     "security",  r"__proto__\s*=|Object\.assign\s*\(\s*\{\s*\},", "Potential prototype pollution pattern.",                                 "Validate that object keys don't include __proto__ or constructor."),
    ("JS011", "medium",   "bug",       r"setTimeout\s*\(\s*['\"]",         "setTimeout with a string argument calls eval internally.",                           "Pass a function reference instead: setTimeout(() => doThing(), ms)"),
    ("JS012", "low",      "quality",   r"function\s+\w+\s*\([^)]{60,}\)",  "Function has too many parameters — consider an options object.",                    "Refactor long parameter lists into a single options/config object."),
]

RULES = PYTHON_RULES if lang == "python" else JS_RULES
SEV_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3}
SEV_FILTER_LEVEL = {"all": 3, "low": 3, "medium": 2, "high": 1, "critical": 0}.get(SEV_FILTER, 3)

files = list(src.rglob(ext) if src.is_dir() else [src])
if not files:
    console.print(f"[yellow]No {ext} files found in {SRC_PATH}[/yellow]")
    raise SystemExit(0)

console.print()
console.print(Panel.fit(
    f"[bold red]🐛 👁️  Argus — Code Intelligence Scanner[/bold red]\n"
    f"Scanning [yellow]{len(files)}[/yellow] {lang} file(s) in [cyan]{SRC_PATH}[/cyan] "
    f"| Severity: [white]{SEV_FILTER}[/white] | Rules: [white]{len(RULES)}[/white]",
    border_style="red"
))

findings = []
for filepath in files:
    try:
        source = filepath.read_text(encoding="utf-8", errors="replace")
        lines  = source.splitlines()
        for lineno, line in enumerate(lines, 1):
            for rule_id, sev, category, pattern, message, fix in RULES:
                if re.search(pattern, line):
                    if SEV_ORDER.get(sev, 3) <= SEV_FILTER_LEVEL:
                        findings.append({
                            "id":       rule_id,
                            "severity": sev,
                            "category": category,
                            "file":     str(filepath.relative_to(src) if src.is_dir() else filepath),
                            "line":     lineno,
                            "code":     line.strip()[:80],
                            "message":  message,
                            "fix":      fix,
                        })
    except Exception as e:
        console.print(f"[dim]Skipping {filepath}: {e}[/dim]")

# Deduplicate by (rule, file, line)
seen = set()
unique = []
for f in findings:
    key = (f["id"], f["file"], f["line"])
    if key not in seen:
        seen.add(key)
        unique.append(f)

# Sort by severity then file/line
unique.sort(key=lambda f: (SEV_ORDER.get(f["severity"], 3), f["file"], f["line"]))
display = unique[:MAX_FINDINGS]

# ── Display results ───────────────────────────────────────────────────────────
SEV_COLOUR = {"critical": "red", "high": "orange3", "medium": "yellow", "low": "dim"}

if not unique:
    console.print(Panel(
        "[green]✅ No issues found![/green]\n\n"
        f"Scanned {len(files)} file(s) with {len(RULES)} rules — all clear.",
        border_style="green"
    ))
else:
    display_count = len(display)
    title_suffix  = f" — showing {display_count} of {len(unique)}" if len(unique) > MAX_FINDINGS else f" — {len(unique)} found"
    findings_table = Table(
        title=f"🔍 Findings{title_suffix}",
        box=box.ROUNDED, border_style="red", show_lines=True
    )
    findings_table.add_column("ID",       style="dim",    width=7)
    findings_table.add_column("Sev",      style="white",  width=9)
    findings_table.add_column("Cat",      style="cyan",   width=10)
    findings_table.add_column("File:Line",style="yellow", width=28)
    findings_table.add_column("Issue",    style="white",  width=45)

    for fi in display:
        sc   = SEV_COLOUR.get(fi["severity"], "white")
        loc  = f"{fi['file'][-24:]}:{fi['line']}" if len(fi["file"]) > 24 else f"{fi['file']}:{fi['line']}"
        findings_table.add_row(
            fi["id"],
            f"[{sc}]{fi['severity'].upper()}[/{sc}]",
            fi["category"],
            loc,
            fi["message"][:43],
        )
    console.print(findings_table)

    # Detailed panels for critical/high
    critical_high = [f for f in display if f["severity"] in ("critical", "high")]
    if critical_high:
        console.print()
        for fi in critical_high[:5]:
            sc = SEV_COLOUR.get(fi["severity"], "white")
            console.print(Panel(
                f"[dim]File:[/dim]  {fi['file']}:{fi['line']}\n"
                f"[dim]Code:[/dim]  [italic]{fi['code']}[/italic]\n\n"
                f"[white]{fi['message']}[/white]\n\n"
                f"[green]Fix:[/green] {fi['fix']}",
                title=f"[{sc}][bold]{fi['severity'].upper()}[/bold][/{sc}] — {fi['id']}",
                border_style=sc
            ))

    # Summary by severity
    console.print()
    sev_counts = {s: sum(1 for f in unique if f["severity"] == s)
                  for s in ("critical", "high", "medium", "low")}
    summary_lines = "  ".join(
        f"[{SEV_COLOUR[s]}]{s.title()}: {sev_counts[s]}[/{SEV_COLOUR[s]}]"
        for s in ("critical", "high", "medium", "low") if sev_counts[s]
    )
    console.print(Panel(
        f"Files scanned: [yellow]{len(files)}[/yellow]   "
        f"Total issues: [red]{len(unique)}[/red]   {summary_lines}",
        title="Summary",
        border_style="cyan"
    ))

# ── Save outputs ──────────────────────────────────────────────────────────────
report_file = f"argus_report_{TODAY}.md"
with open(report_file, "w", encoding="utf-8") as f:
    f.write(f"# 🐛 Argus Code Scan Report — {TODAY}\n\n")
    f.write(f"**Path:** `{SRC_PATH}`  **Language:** {lang}  **Files:** {len(files)}  **Issues:** {len(unique)}\n\n")
    sev_counts = {s: sum(1 for fi in unique if fi["severity"] == s) for s in ("critical","high","medium","low")}
    f.write(f"**Critical:** {sev_counts['critical']}  **High:** {sev_counts['high']}  **Medium:** {sev_counts['medium']}  **Low:** {sev_counts['low']}\n\n")
    f.write("## Findings\n\n| ID | Severity | Category | File:Line | Issue |\n|---|---|---|---|---|\n")
    for fi in unique:
        f.write(f"| {fi['id']} | {fi['severity'].upper()} | {fi['category']} | {fi['file'][:30]}:{fi['line']} | {fi['message'][:60]} |\n")
    f.write("\n## Fix Suggestions\n\n")
    for fi in [x for x in unique if x["severity"] in ("critical","high")]:
        f.write(f"### {fi['id']} — {fi['file']}:{fi['line']}\n\n")
        f.write(f"**Issue:** {fi['message']}\n\n")
        f.write(f"**Fix:** {fi['fix']}\n\n")
        f.write(f"```\n{fi['code']}\n```\n\n")

if OUTPUT_JSON:
    json_file = f"argus_report_{TODAY}.json"
    with open(json_file, "w", encoding="utf-8") as f:
        json.dump({"scanned": str(TODAY), "path": SRC_PATH, "language": lang,
                   "files": len(files), "findings": unique}, f, indent=2)
    console.print(f"[dim]JSON saved to {json_file}[/dim]")

console.print(Panel(
    f"[green]✅ Scan complete![/green]  Report saved to [cyan]{report_file}[/cyan]",
    border_style="green"
))