Claude Skill

sast-lite

Static security analysis for Python source via AST walking — finds command injection, insecure deserialization, eval/exec, weak crypto, SQL injection, disabled TLS verification, hardcoded secrets and more, each tagged with a CWE. Use when the user asks to "audit this code for vul

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download novacode37-claude-security-skills-skills_sast-lite-dd4e44f.zip · 7 KB
Part of novacode37/claude-security-skills — 5 skills

Install

skills CLI npx skills add https://github.com/NovaCode37/claude-security-skills/tree/main/skills/sast-lite
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install novacode37-claude-security-skills@llmmart
Git git clone https://github.com/NovaCode37/claude-security-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole novacode37/claude-security-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

SAST Lite

An AST-based static analyzer for Python. Instead of fragile regex matching, it parses each file into an abstract syntax tree and inspects how dangerous APIs are actually called — so subprocess.run(cmd, shell=True) is flagged while subprocess.run(["ls"]) is not. No third-party dependencies.

When to use this skill

  • "Audit / security-review this Python code."
  • "Run a SAST scan on the project."
  • Reviewing a PR or untrusted snippet before running it.
  • A pre-merge CI gate for security regressions.

What it detects

Rule CWE Severity
eval() / exec() on dynamic input CWE-95 critical/high
os.system / subprocess(shell=True) CWE-78 high
pickle/marshal deserialization CWE-502 high
yaml.load without SafeLoader CWE-20 high
SQL via f-string / concat / .format / % CWE-89 high
requests(verify=False) CWE-295 high
Hardcoded password/secret literal CWE-798 high
random used for a token, password, OTP, nonce or salt CWE-330 high
Weak hash (md5/sha1) CWE-327 medium
tempfile.mktemp CWE-377 medium
Flask(debug=True) CWE-489 medium
Jinja2 autoescape=False CWE-79 medium
assert used for a security check CWE-617 medium

How to run it

# Scan a directory
python skills/sast-lite/analyzer.py src/

# JSON for tooling / CI
python skills/sast-lite/analyzer.py . --json

# Only show high+ severity
python skills/sast-lite/analyzer.py . --min-severity high

Exit codes: 0 clean · 1 issues found · 2 usage/parse error — ready for CI gating.

Recommended workflow for Claude

  1. Run with --json and parse the issue list.
  2. For each issue, open path:line and confirm the data flow is genuinely attacker-controllable (the analyzer is intra-procedural, so it may flag patterns that are safe in context).
  3. Propose a concrete fix per finding — e.g. parameterized queries for py.sql-injection, yaml.safe_load for py.yaml-load, list-form subprocess calls for py.subprocess-shell.
  4. Summarize by severity and CWE.

Limitations

This is a lite analyzer: single-file, no cross-function taint tracking. It is designed for fast, high-signal triage — not a replacement for a full SAST suite. Treat findings as leads to verify, not automatic verdicts.

Files (claude-security-skills)
  • tests
    • test_analyzer.py 5.5 KB
      import os
      import sys
      
      import pytest
      
      sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
      
      import analyzer
      
      
      def ids(src):
          return {i.rule_id for i in analyzer.analyze_source(src)}
      
      
      def test_eval_flagged():
          assert "py.eval-exec" in ids("eval(user_input)")
      
      
      def test_exec_flagged():
          assert "py.eval-exec" in ids("exec(payload)")
      
      
      def test_os_system_flagged():
          assert "py.os-system" in ids("import os\nos.system(cmd)")
      
      
      def test_subprocess_shell_true_flagged():
          src = "import subprocess\nsubprocess.run(cmd, shell=True)"
          assert "py.subprocess-shell" in ids(src)
      
      
      def test_pickle_loads_flagged():
          assert "py.insecure-deserialization" in ids("import pickle\npickle.loads(data)")
      
      
      def test_yaml_load_flagged():
          assert "py.yaml-load" in ids("import yaml\nyaml.load(data)")
      
      
      def test_weak_hash_flagged():
          assert "py.weak-hash" in ids("import hashlib\nhashlib.md5(x)")
      
      
      def test_tls_verify_false_flagged():
          src = "import requests\nrequests.get(url, verify=False)"
          assert "py.tls-verify-disabled" in ids(src)
      
      
      def test_hardcoded_secret_flagged():
          assert "py.hardcoded-secret" in ids("password = 'hunter2pass'")
      
      
      def test_sql_fstring_flagged():
          src = "cur.execute(f'SELECT * FROM t WHERE id = {uid}')"
          assert "py.sql-injection" in ids(src)
      
      
      def test_sql_concat_flagged():
          src = "cur.execute('SELECT * FROM t WHERE id = ' + uid)"
          assert "py.sql-injection" in ids(src)
      
      
      def test_sql_format_flagged():
          src = "cur.execute('SELECT * FROM t WHERE id = {}'.format(uid))"
          assert "py.sql-injection" in ids(src)
      
      
      def test_flask_debug_flagged():
          assert "py.flask-debug" in ids("app.run(debug=True)")
      
      
      def test_mktemp_flagged():
          assert "py.insecure-temp" in ids("import tempfile\ntempfile.mktemp()")
      
      
      def test_assert_security_flagged():
          assert "py.assert-security" in ids("assert user.is_admin")
      
      
      def test_jinja_autoescape_flagged():
          src = "from jinja2 import Environment\nEnvironment(autoescape=False)"
          assert "py.jinja-autoescape" in ids(src)
      
      
      def test_safe_subprocess_not_flagged():
          src = "import subprocess\nsubprocess.run(['ls', '-la'])"
          assert "py.subprocess-shell" not in ids(src)
      
      
      def test_yaml_safe_load_not_flagged():
          assert "py.yaml-load" not in ids("import yaml\nyaml.safe_load(data)")
      
      
      def test_yaml_load_with_safeloader_not_flagged():
          src = "import yaml\nyaml.load(data, Loader=yaml.SafeLoader)"
          assert "py.yaml-load" not in ids(src)
      
      
      def test_tls_verify_true_not_flagged():
          src = "import requests\nrequests.get(url, verify=True)"
          assert "py.tls-verify-disabled" not in ids(src)
      
      
      def test_parameterized_sql_not_flagged():
          src = "cur.execute('SELECT * FROM t WHERE id = ?', (uid,))"
          assert "py.sql-injection" not in ids(src)
      
      
      def test_sha256_not_flagged():
          assert "py.weak-hash" not in ids("import hashlib\nhashlib.sha256(x)")
      
      
      def test_normal_assert_not_flagged():
          assert "py.assert-security" not in ids("assert len(items) == 3")
      
      
      def test_clean_code_no_issues():
          src = "def add(a, b):\n    return a + b\n"
          assert analyzer.analyze_source(src) == []
      
      
      def test_syntax_error_reported():
          issues = analyzer.analyze_source("def (:\n")
          assert any(i.rule_id == "py.syntax-error" for i in issues)
      
      
      def test_severity_filter(tmp_path):
          f = tmp_path / "v.py"
          f.write_text("import hashlib\nhashlib.md5(x)\neval(y)\n")
          high_only = analyzer.analyze_paths([str(tmp_path)], min_severity="high")
          rule_ids = {i.rule_id for i in high_only}
          assert "py.eval-exec" in rule_ids
          assert "py.weak-hash" not in rule_ids
      
      
      def test_cli_exit_codes(tmp_path):
          clean = tmp_path / "clean.py"
          clean.write_text("x = 1\n")
          assert analyzer.main([str(clean)]) == 0
          bad = tmp_path / "bad.py"
          bad.write_text("eval(x)\n")
          assert analyzer.main([str(bad)]) == 1
      
      
      # --- random used for security values (issue #34) ----------------------------
      
      def test_random_token_flagged():
          src = "import random\ntoken = random.choice(alphabet)\n"
          assert "py.insecure-random" in ids(src)
      
      
      def test_random_randint_otp_flagged():
          src = "import random\notp = random.randint(100000, 999999)\n"
          assert "py.insecure-random" in ids(src)
      
      
      def test_random_shuffle_on_a_list_not_flagged():
          src = "import random\ndeck = list(range(52))\nrandom.shuffle(deck)\n"
          assert "py.insecure-random" not in ids(src)
      
      
      def test_random_sample_for_ordinary_values_not_flagged():
          src = "import random\nwinners = random.sample(players, 3)\n"
          assert "py.insecure-random" not in ids(src)
      
      
      def test_secrets_module_not_flagged():
          src = "import secrets\ntoken = secrets.choice(alphabet)\n"
          assert "py.insecure-random" not in ids(src)
      
      
      def test_insecure_random_carries_cwe_330():
          src = "import random\nsession_token = random.random()\n"
          issue = next(i for i in analyzer.analyze_source(src)
                       if i.rule_id == "py.insecure-random")
          assert issue.cwe == "CWE-330"
      
      
      # --- min-severity default (issue #48) ---------------------------------------
      
      def test_min_severity_defaults_to_info(tmp_path):
          """Every engine starts at info, so one pipeline filters the same way."""
          broken = tmp_path / "broken.py"
          broken.write_text("def (:\n")
          assert any(i.rule_id == "py.syntax-error"
                     for i in analyzer.analyze_paths([str(tmp_path)]))
      
      
      def test_cli_json(tmp_path, capsys):
          f = tmp_path / "bad.py"
          f.write_text("eval(x)\n")
          analyzer.main([str(f), "--json"])
          import json
          data = json.loads(capsys.readouterr().out)
          assert data and data[0]["cwe"]
      
  • analyzer.py 13.5 KB
    from __future__ import annotations
    
    import argparse
    import ast
    import json
    import os
    import sys
    from dataclasses import dataclass, asdict
    from typing import Iterable, Iterator
    
    SKIP_DIRS = {
        ".git", "node_modules", "venv", ".venv", "env", "__pycache__",
        "dist", "build", ".mypy_cache", ".pytest_cache", "site-packages",
    }
    
    SEVERITY_RANK = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
    
    
    @dataclass
    class Issue:
        rule_id: str
        cwe: str
        message: str
        severity: str
        path: str
        line: int
        col: int
        snippet: str
    
        def to_dict(self) -> dict:
            return asdict(self)
    
    
    def _attr_chain(node: ast.AST) -> str:
        parts: list[str] = []
        while isinstance(node, ast.Attribute):
            parts.append(node.attr)
            node = node.value
        if isinstance(node, ast.Name):
            parts.append(node.id)
            return ".".join(reversed(parts))
        return ".".join(reversed(parts)) if parts else ""
    
    
    def _kw(call: ast.Call, name: str):
        for kw in call.keywords:
            if kw.arg == name:
                return kw.value
        return None
    
    
    def _is_const_true(node: ast.AST | None) -> bool:
        return isinstance(node, ast.Constant) and node.value is True
    
    
    def _is_const_false(node: ast.AST | None) -> bool:
        return isinstance(node, ast.Constant) and node.value is False
    
    
    def _is_string_constant(node: ast.AST | None) -> bool:
        return isinstance(node, ast.Constant) and isinstance(node.value, str)
    
    
    PASSWORD_NAMES = {
        "password", "passwd", "pwd", "secret", "token", "api_key", "apikey",
        "access_key", "secret_key", "private_key", "auth_token",
    }
    
    SECURITY_VALUE_NAMES = (
        "token", "password", "passwd", "pwd", "secret", "otp", "nonce", "salt",
        "apikey", "api_key", "session",
    )
    
    RANDOM_BARE_FUNCS = {"random", "randint", "randrange", "getrandbits", "uniform"}
    
    
    def _random_call(node: ast.AST) -> str:
        """Return the name of a `random` call, or "" for anything else.
    
        A bare call is only treated as `random` when the name is unambiguous:
        `choice` on its own is just as likely to be someone's own helper, while
        `secrets.choice` must never match.
        """
        if not isinstance(node, ast.Call):
            return ""
        chain = _attr_chain(node.func)
        if chain.startswith("random."):
            return chain
        if isinstance(node.func, ast.Name) and node.func.id in RANDOM_BARE_FUNCS:
            return node.func.id
        return ""
    
    
    class SecurityVisitor(ast.NodeVisitor):
        def __init__(self, path: str, source_lines: list[str]):
            self.path = path
            self.lines = source_lines
            self.issues: list[Issue] = []
    
        def _add(self, node: ast.AST, rule_id: str, cwe: str, msg: str, sev: str):
            line = getattr(node, "lineno", 0)
            col = getattr(node, "col_offset", 0)
            snippet = self.lines[line - 1].strip() if 0 < line <= len(self.lines) else ""
            self.issues.append(Issue(rule_id, cwe, msg, sev, self.path, line,
                                     col + 1, snippet[:200]))
    
        def visit_Call(self, node: ast.Call):
            target = ""
            if isinstance(node.func, ast.Name):
                target = node.func.id
            elif isinstance(node.func, ast.Attribute):
                target = _attr_chain(node.func)
            last = target.split(".")[-1] if target else ""
    
            if target in ("eval", "exec") or last in ("eval", "exec"):
                dynamic = bool(node.args) and not _is_string_constant(node.args[0])
                sev = "critical" if dynamic else "high"
                self._add(node, "py.eval-exec", "CWE-95",
                          f"Use of {last}() can execute arbitrary code.", sev)
            if last == "compile" and target in ("compile",):
                self._add(node, "py.compile", "CWE-95",
                          "compile() of dynamic source can lead to code execution.",
                          "medium")
    
            if target in ("os.system", "os.popen") or last in ("system", "popen"):
                if target.startswith("os.") or last in ("system", "popen"):
                    self._add(node, "py.os-system", "CWE-78",
                              f"{target or last}() invokes a shell — command "
                              f"injection risk.", "high")
    
            if "subprocess" in target or last in (
                    "call", "run", "Popen", "check_output", "check_call"):
                if _is_const_true(_kw(node, "shell")):
                    self._add(node, "py.subprocess-shell", "CWE-78",
                              "subprocess called with shell=True — command "
                              "injection risk.", "high")
    
            if last in ("loads", "load") and any(
                    target.startswith(m + ".") or target == m + "." + last
                    for m in ("pickle", "cPickle", "marshal", "_pickle")):
                self._add(node, "py.insecure-deserialization", "CWE-502",
                          f"{target}() deserializes untrusted data unsafely.",
                          "high")
    
            if target.endswith("yaml.load") or (last == "load" and "yaml" in target):
                loader = _kw(node, "Loader")
                safe = False
                if loader is not None:
                    lname = _attr_chain(loader) if isinstance(
                        loader, (ast.Attribute, ast.Name)) else ""
                    safe = "Safe" in lname or "CSafe" in lname
                if not safe:
                    self._add(node, "py.yaml-load", "CWE-20",
                              "yaml.load() without SafeLoader can execute "
                              "arbitrary objects; use yaml.safe_load().", "high")
    
            if last in ("md5", "sha1") and (
                    "hashlib" in target or target in ("md5", "sha1")):
                self._add(node, "py.weak-hash", "CWE-327",
                          f"{last} is cryptographically broken; use SHA-256+.",
                          "medium")
    
            if _is_const_false(_kw(node, "verify")) and (
                    "requests" in target or last in (
                        "get", "post", "put", "delete", "patch", "head",
                        "request", "Session")):
                self._add(node, "py.tls-verify-disabled", "CWE-295",
                          "TLS certificate verification disabled (verify=False).",
                          "high")
    
            if target.endswith("tempfile.mktemp") or last == "mktemp":
                self._add(node, "py.insecure-temp", "CWE-377",
                          "tempfile.mktemp() is race-prone; use mkstemp()/"
                          "NamedTemporaryFile.", "medium")
    
            if last == "run" and _is_const_true(_kw(node, "debug")):
                self._add(node, "py.flask-debug", "CWE-489",
                          "Flask app.run(debug=True) exposes the Werkzeug "
                          "debugger in production.", "medium")
    
            if last == "Environment" and _is_const_false(_kw(node, "autoescape")):
                self._add(node, "py.jinja-autoescape", "CWE-79",
                          "Jinja2 Environment(autoescape=False) enables XSS.",
                          "medium")
    
            if last in ("execute", "executemany", "executescript") and node.args:
                arg0 = node.args[0]
                if isinstance(arg0, ast.JoinedStr):
                    self._add(node, "py.sql-injection", "CWE-89",
                              "SQL query built with an f-string — use "
                              "parameterized queries.", "high")
                elif isinstance(arg0, ast.BinOp) and isinstance(arg0.op, ast.Add):
                    self._add(node, "py.sql-injection", "CWE-89",
                              "SQL query built with string concatenation — use "
                              "parameterized queries.", "high")
                elif (isinstance(arg0, ast.Call)
                      and isinstance(arg0.func, ast.Attribute)
                      and arg0.func.attr == "format"):
                    self._add(node, "py.sql-injection", "CWE-89",
                              "SQL query built with str.format() — use "
                              "parameterized queries.", "high")
                elif isinstance(arg0, ast.BinOp) and isinstance(arg0.op, ast.Mod):
                    self._add(node, "py.sql-injection", "CWE-89",
                              "SQL query built with %-formatting — use "
                              "parameterized queries.", "high")
    
            self.generic_visit(node)
    
        def visit_Assign(self, node: ast.Assign):
            if _is_string_constant(node.value) and node.value.value:
                for tgt in node.targets:
                    name = tgt.id if isinstance(tgt, ast.Name) else (
                        tgt.attr if isinstance(tgt, ast.Attribute) else "")
                    if name.lower() in PASSWORD_NAMES and len(node.value.value) >= 4:
                        self._add(node, "py.hardcoded-secret", "CWE-798",
                                  f"Hardcoded secret in variable '{name}'.",
                                  "high")
    
            # Only when the value is being assigned to something that reads like a
            # credential. Flagging every random.choice would bury this in noise
            # from sampling and shuffling.
            rnd = _random_call(node.value)
            if rnd:
                for tgt in node.targets:
                    name = tgt.id if isinstance(tgt, ast.Name) else (
                        tgt.attr if isinstance(tgt, ast.Attribute) else "")
                    if any(w in name.lower() for w in SECURITY_VALUE_NAMES):
                        self._add(node, "py.insecure-random", "CWE-330",
                                  f"'{name}' is generated with {rnd}(); the random "
                                  "module is not cryptographically secure. Use "
                                  "the secrets module.", "high")
                        break
            self.generic_visit(node)
    
        def visit_Assert(self, node: ast.Assert):
            src = ast.dump(node.test).lower()
            if any(k in src for k in (
                    "auth", "permission", "is_admin", "isadmin", "role",
                    "access", "allowed", "verify", "valid")):
                self._add(node, "py.assert-security", "CWE-617",
                          "assert used for a security check — asserts are removed "
                          "with python -O.", "medium")
            self.generic_visit(node)
    
    
    def analyze_source(source: str, path: str = "<string>") -> list[Issue]:
        try:
            tree = ast.parse(source, filename=path)
        except SyntaxError as exc:
            return [Issue("py.syntax-error", "CWE-000",
                          f"Could not parse file: {exc.msg}", "info",
                          path, exc.lineno or 0, (exc.offset or 0), "")]
        visitor = SecurityVisitor(path, source.splitlines())
        visitor.visit(tree)
        return visitor.issues
    
    
    def iter_py_files(paths: Iterable[str]) -> Iterator[str]:
        for root in paths:
            if os.path.isfile(root):
                if root.endswith(".py"):
                    yield root
                continue
            for dirpath, dirnames, filenames in os.walk(root):
                dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
                for name in filenames:
                    if name.endswith(".py"):
                        yield os.path.join(dirpath, name)
    
    
    def analyze_paths(paths: Iterable[str],
                      min_severity: str = "info") -> list[Issue]:
        threshold = SEVERITY_RANK.get(min_severity, 3)
        issues: list[Issue] = []
        for path in iter_py_files(paths):
            try:
                with open(path, "r", encoding="utf-8", errors="ignore") as fh:
                    src = fh.read()
            except OSError:
                continue
            for issue in analyze_source(src, path):
                if SEVERITY_RANK.get(issue.severity, 3) <= threshold:
                    issues.append(issue)
        issues.sort(key=lambda i: (SEVERITY_RANK.get(i.severity, 9), i.path, i.line))
        return issues
    
    
    _SEV_COLOR = {
        "critical": "\033[1;91m", "high": "\033[91m",
        "medium": "\033[93m", "low": "\033[94m", "info": "\033[90m",
    }
    _RESET = "\033[0m"
    
    
    def render_pretty(issues: list[Issue]) -> str:
        if not issues:
            return "[sast-lite] No issues found. [OK]"
        color = sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
        out = [f"[sast-lite] {len(issues)} issue(s) found:\n"]
        for i in issues:
            tag = i.severity.upper()
            if color:
                tag = f"{_SEV_COLOR.get(i.severity, '')}{tag}{_RESET}"
            out.append(f"  {tag:<10} {i.path}:{i.line}:{i.col}  [{i.rule_id} / {i.cwe}]")
            out.append(f"             {i.message}")
            if i.snippet:
                out.append(f"             > {i.snippet}")
        counts: dict[str, int] = {}
        for i in issues:
            counts[i.severity] = counts.get(i.severity, 0) + 1
        out.append("\nSummary: " + ", ".join(
            f"{k}={counts[k]}" for k in sorted(counts, key=lambda s: SEVERITY_RANK.get(s, 9))))
        return "\n".join(out)
    
    
    def main(argv: list[str] | None = None) -> int:
        p = argparse.ArgumentParser(
            prog="sast-lite",
            description="Lightweight AST-based Python security analyzer.")
        p.add_argument("paths", nargs="+", help="files or directories to scan")
        p.add_argument("--json", action="store_true", help="emit JSON")
        p.add_argument("--min-severity", default="info",
                       choices=list(SEVERITY_RANK),
                       help="report issues at or above this severity "
                            "(default: info, i.e. report everything)")
        args = p.parse_args(argv)
    
        for path in args.paths:
            if not os.path.exists(path):
                print(f"error: path not found: {path}", file=sys.stderr)
                return 2
    
        issues = analyze_paths(args.paths, min_severity=args.min_severity)
        if args.json:
            print(json.dumps([i.to_dict() for i in issues], indent=2))
        else:
            print(render_pretty(issues))
        return 1 if issues else 0
    
    
    if __name__ == "__main__":
        try:
            sys.stdout.reconfigure(encoding="utf-8")
        except Exception:
            pass
        raise SystemExit(main())
    
  • SKILL.md 2.8 KB
    ---
    name: sast-lite
    description: >-
      Static security analysis for Python source via AST walking — finds command
      injection, insecure deserialization, eval/exec, weak crypto, SQL injection,
      disabled TLS verification, hardcoded secrets and more, each tagged with a
      CWE. Use when the user asks to "audit this code for vulnerabilities", "run a
      SAST scan", "security review this Python file", or before merging untrusted
      code.
    license: MIT
    ---
    
    # SAST Lite
    
    An AST-based static analyzer for Python. Instead of fragile regex matching, it
    parses each file into an abstract syntax tree and inspects how dangerous APIs
    are actually called — so `subprocess.run(cmd, shell=True)` is flagged while
    `subprocess.run(["ls"])` is not. **No third-party dependencies.**
    
    ## When to use this skill
    
    - "Audit / security-review this Python code."
    - "Run a SAST scan on the project."
    - Reviewing a PR or untrusted snippet before running it.
    - A pre-merge CI gate for security regressions.
    
    ## What it detects
    
    | Rule | CWE | Severity |
    |------|-----|----------|
    | `eval()` / `exec()` on dynamic input | CWE-95 | critical/high |
    | `os.system` / `subprocess(shell=True)` | CWE-78 | high |
    | `pickle`/`marshal` deserialization | CWE-502 | high |
    | `yaml.load` without SafeLoader | CWE-20 | high |
    | SQL via f-string / concat / `.format` / `%` | CWE-89 | high |
    | `requests(verify=False)` | CWE-295 | high |
    | Hardcoded password/secret literal | CWE-798 | high |
    | `random` used for a token, password, OTP, nonce or salt | CWE-330 | high |
    | Weak hash (md5/sha1) | CWE-327 | medium |
    | `tempfile.mktemp` | CWE-377 | medium |
    | `Flask(debug=True)` | CWE-489 | medium |
    | Jinja2 `autoescape=False` | CWE-79 | medium |
    | `assert` used for a security check | CWE-617 | medium |
    
    ## How to run it
    
    ```bash
    # Scan a directory
    python skills/sast-lite/analyzer.py src/
    
    # JSON for tooling / CI
    python skills/sast-lite/analyzer.py . --json
    
    # Only show high+ severity
    python skills/sast-lite/analyzer.py . --min-severity high
    ```
    
    **Exit codes:** `0` clean · `1` issues found · `2` usage/parse error — ready
    for CI gating.
    
    ## Recommended workflow for Claude
    
    1. Run with `--json` and parse the issue list.
    2. For each issue, open `path:line` and confirm the data flow is genuinely
       attacker-controllable (the analyzer is intra-procedural, so it may flag
       patterns that are safe in context).
    3. Propose a concrete fix per finding — e.g. parameterized queries for
       `py.sql-injection`, `yaml.safe_load` for `py.yaml-load`,
       list-form `subprocess` calls for `py.subprocess-shell`.
    4. Summarize by severity and CWE.
    
    ## Limitations
    
    This is a *lite* analyzer: single-file, no cross-function taint tracking. It is
    designed for fast, high-signal triage — not a replacement for a full SAST
    suite. Treat findings as leads to verify, not automatic verdicts.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related