skillops-cli 0.0.0-stage → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.github/agents/address-review.agent.md +14 -0
  2. package/.github/agents/docs-sync.agent.md +18 -0
  3. package/.github/agents/pr-brief.agent.md +24 -0
  4. package/.github/agents/skill-auditor.agent.md +22 -0
  5. package/LICENSE +21 -0
  6. package/README.md +184 -2
  7. package/SKILL_CONTRACT.md +146 -0
  8. package/catalog/ai-engineering/agent-harness-auditor/SKILL.md +44 -0
  9. package/catalog/ai-engineering/agent-harness-auditor/evals/benchmark.json +22 -0
  10. package/catalog/ai-engineering/agent-harness-auditor/evals/evals.json +40 -0
  11. package/catalog/ai-engineering/agent-harness-auditor/scripts/harness_linter.py +101 -0
  12. package/catalog/ai-engineering/context-pruner-advisor/SKILL.md +44 -0
  13. package/catalog/ai-engineering/context-pruner-advisor/evals/benchmark.json +22 -0
  14. package/catalog/ai-engineering/context-pruner-advisor/evals/evals.json +40 -0
  15. package/catalog/ai-engineering/context-pruner-advisor/scripts/context_pruner.py +114 -0
  16. package/catalog/ai-engineering/mcp-architect/SKILL.md +45 -0
  17. package/catalog/ai-engineering/mcp-architect/evals/benchmark.json +22 -0
  18. package/catalog/ai-engineering/mcp-architect/evals/evals.json +54 -0
  19. package/catalog/ai-engineering/mcp-architect/scripts/validate_mcp_server.py +156 -0
  20. package/catalog/ai-engineering/prompt-red-teaming/SKILL.md +43 -0
  21. package/catalog/ai-engineering/prompt-red-teaming/evals/benchmark.json +22 -0
  22. package/catalog/ai-engineering/prompt-red-teaming/evals/evals.json +40 -0
  23. package/catalog/ai-engineering/prompt-red-teaming/scripts/injection_scanner.py +79 -0
  24. package/catalog/arch-quality/ast-blast-radius/SKILL.md +41 -0
  25. package/catalog/arch-quality/ast-blast-radius/evals/benchmark.json +22 -0
  26. package/catalog/arch-quality/ast-blast-radius/evals/evals.json +40 -0
  27. package/catalog/arch-quality/ast-blast-radius/scripts/analyze_call_graph.py +114 -0
  28. package/catalog/arch-quality/bug-reproducer-tdd/SKILL.md +42 -0
  29. package/catalog/arch-quality/bug-reproducer-tdd/evals/benchmark.json +22 -0
  30. package/catalog/arch-quality/bug-reproducer-tdd/evals/evals.json +40 -0
  31. package/catalog/arch-quality/bug-reproducer-tdd/scripts/repro_harness_check.py +129 -0
  32. package/catalog/arch-quality/security-sast-gate/SKILL.md +42 -0
  33. package/catalog/arch-quality/security-sast-gate/evals/benchmark.json +22 -0
  34. package/catalog/arch-quality/security-sast-gate/evals/evals.json +40 -0
  35. package/catalog/arch-quality/security-sast-gate/scripts/owasp_ast_scanner.py +153 -0
  36. package/catalog/python-backend/concurrency-race-detector/SKILL.md +43 -0
  37. package/catalog/python-backend/concurrency-race-detector/evals/benchmark.json +22 -0
  38. package/catalog/python-backend/concurrency-race-detector/evals/evals.json +40 -0
  39. package/catalog/python-backend/concurrency-race-detector/scripts/event_loop_blocking_scanner.py +127 -0
  40. package/catalog/python-backend/fastapi-production/SKILL.md +44 -0
  41. package/catalog/python-backend/fastapi-production/evals/benchmark.json +22 -0
  42. package/catalog/python-backend/fastapi-production/evals/evals.json +54 -0
  43. package/catalog/python-backend/fastapi-production/scripts/audit_fastapi_routes.py +169 -0
  44. package/catalog/python-backend/postgres-query-doctor/SKILL.md +44 -0
  45. package/catalog/python-backend/postgres-query-doctor/evals/benchmark.json +22 -0
  46. package/catalog/python-backend/postgres-query-doctor/evals/evals.json +54 -0
  47. package/catalog/python-backend/postgres-query-doctor/scripts/analyze_sql_safety.py +130 -0
  48. package/catalog/subagents/blast-radius-auditor/SUBAGENT.md +26 -0
  49. package/catalog/subagents/context-scout/SUBAGENT.md +23 -0
  50. package/catalog/subagents/red-team-breaker/SUBAGENT.md +22 -0
  51. package/catalog/subagents/tdd-reproducer-agent/SUBAGENT.md +23 -0
  52. package/catalog/subagents/token-budget-governor/SUBAGENT.md +26 -0
  53. package/dist/cli.js +6701 -0
  54. package/dist/index.d.ts +2295 -0
  55. package/dist/index.js +6035 -0
  56. package/docs/BENCHMARKS.md +82 -0
  57. package/package.json +84 -4
@@ -0,0 +1,40 @@
1
+ {
2
+ "skill_name": "prompt-red-teaming",
3
+ "schema_version": "1.0",
4
+ "evals": [
5
+ {
6
+ "id": "eval-01-isolate-rag-context-interpolation",
7
+ "name": "Sandbox untrusted RAG document chunks with XML delimiters and tag stripping",
8
+ "prompt": "Audit our RAG prompt builder in `rag.py` where `rag_chunk` and `user_input` are interpolated directly into the system prompt via f-strings, and harden it against indirect prompt injection.",
9
+ "fixtures": [],
10
+ "assertions": [
11
+ {
12
+ "id": "assert-ran-injection-scanner",
13
+ "type": "tool_call_contains",
14
+ "target": "injection_scanner.py",
15
+ "description": "Agent executed the deterministic prompt injection scanner script."
16
+ },
17
+ {
18
+ "id": "assert-xml-delimiter-sandboxing",
19
+ "type": "regex_match",
20
+ "target": "<untrusted_context>",
21
+ "description": "Untrusted inputs are isolated inside <untrusted_context> structural tags."
22
+ }
23
+ ]
24
+ },
25
+ {
26
+ "id": "eval-02-gate-privileged-tool-execution",
27
+ "name": "Prevent indirect prompt injection from auto-triggering destructive tools",
28
+ "prompt": "Ensure that retrieved web content in our agent pipeline cannot autonomously trigger the `delete_account` or `send_email` tools without policy verification.",
29
+ "fixtures": [],
30
+ "assertions": [
31
+ {
32
+ "id": "assert-privileged-tool-policy-gate",
33
+ "type": "regex_match",
34
+ "target": "(approval|confirm|policy|passive data)",
35
+ "description": "Agent enforced instruction hierarchy and tool approval gating on untrusted context turns."
36
+ }
37
+ ]
38
+ }
39
+ ]
40
+ }
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministic Scanner for Unisolated Prompt Interpolations & RAG Injection Hazards."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from pathlib import Path
9
+ import re
10
+ import sys
11
+
12
+ UNSAFE_INTERPOLATION_RE = re.compile(
13
+ r"""(f["']{1,3}[^"']*\{(user_input|query|rag_chunk|document|external_text|web_content)\}[^"']*["']{1,3})""",
14
+ re.IGNORECASE,
15
+ )
16
+ OVERRIDE_PHRASE_RE = re.compile(
17
+ r"(ignore previous instructions|disregard all prior rules|system override|you are now in developer mode)",
18
+ re.IGNORECASE,
19
+ )
20
+
21
+
22
+ def scan_file(path: Path) -> list[dict[str, object]]:
23
+ findings: list[dict[str, object]] = []
24
+ text = path.read_text(encoding="utf-8", errors="replace")
25
+ for idx, line in enumerate(text.splitlines(), start=1):
26
+ if UNSAFE_INTERPOLATION_RE.search(line) and "<untrusted_" not in line:
27
+ findings.append(
28
+ {
29
+ "path": str(path),
30
+ "line": idx,
31
+ "rule": "PROMPT-UNISOLATED-INTERPOLATION",
32
+ "remediation": "Wrap untrusted variables in <untrusted_context> XML tags and strip closing tags.",
33
+ }
34
+ )
35
+ if OVERRIDE_PHRASE_RE.search(line):
36
+ findings.append(
37
+ {
38
+ "path": str(path),
39
+ "line": idx,
40
+ "rule": "PROMPT-INJECTION-PAYLOAD",
41
+ "remediation": "Neutralize direct instruction-override payload in fixture or prompt template.",
42
+ }
43
+ )
44
+ return findings
45
+
46
+
47
+ def main(argv: list[str] | None = None) -> int:
48
+ parser = argparse.ArgumentParser(description="Scan prompt files and RAG code for injection vulnerabilities.")
49
+ parser.add_argument("--path", required=True, help="Target file or directory.")
50
+ parser.add_argument("--json", action="store_true", help="Emit JSON output.")
51
+ args = parser.parse_args(argv)
52
+
53
+ target = Path(args.path)
54
+ if not target.exists():
55
+ print(json.dumps({"error": f"Path not found: {args.path}"}), file=sys.stderr)
56
+ return 2
57
+
58
+ files = [target] if target.is_file() else sorted(p for p in target.rglob("*") if p.suffix in {".py", ".ts", ".md"})
59
+ all_findings: list[dict[str, object]] = []
60
+ for f in files:
61
+ all_findings.extend(scan_file(f))
62
+
63
+ status = "violation" if all_findings else "ok"
64
+ report = {
65
+ "skill": "prompt-red-teaming",
66
+ "target": str(target),
67
+ "files_scanned": len(files),
68
+ "status": status,
69
+ "findings": all_findings,
70
+ }
71
+ if args.json:
72
+ print(json.dumps(report, indent=2))
73
+ else:
74
+ print(f"[{status.upper()}] scanned={len(files)} findings={len(all_findings)}")
75
+ return 0 if status == "ok" else 1
76
+
77
+
78
+ if __name__ == "__main__":
79
+ raise SystemExit(main())
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: "ast-blast-radius"
3
+ description: "Use when modifying shared Python functions, classes, or public module interfaces to compute the exact AST call-graph blast radius, direct callers, transitive module dependents, and broken signature contract risks before refactoring, renaming, or changing parameter defaults across a multi-module production Python codebase."
4
+ version: "1.0.0"
5
+ track: "arch-quality"
6
+ tags:
7
+ - "python"
8
+ - "ast"
9
+ - "refactoring"
10
+ - "architecture"
11
+ - "call-graph"
12
+ routing_signals:
13
+ languages: ["python"]
14
+ files_any: ["pyproject.toml", "setup.py"]
15
+ dependencies_any: []
16
+ negative_triggers:
17
+ - "Do not trigger for pure CSS styling edits, static Markdown documentation changes, or SQL-only queries."
18
+ entrypoints:
19
+ scripts:
20
+ - path: "scripts/analyze_call_graph.py"
21
+ description: "Computes direct callers, import dependents, and signature blast radius for a target Python symbol."
22
+ ---
23
+
24
+ # AST Blast Radius (`ast-blast-radius`)
25
+
26
+ ## When to Use This Skill
27
+ Invoke this skill before changing the signature, return type, or behavior of any shared Python function, method, or class so no downstream caller or test is silently broken.
28
+
29
+ ## Mandatory Execution Workflow
30
+
31
+ 1. **Compute the Exact Call-Graph Blast Radius First:**
32
+ ```bash
33
+ python catalog/arch-quality/ast-blast-radius/scripts/analyze_call_graph.py --path src --symbol <function_or_class_name> --json
34
+ ```
35
+ 2. **Inspect Direct Callers & Import Dependents:**
36
+ - Review every file and line returned in `findings` before modifying the target symbol.
37
+ - Classify the risk level (`low`: 0–2 callers, `medium`: 3–7 callers, `high`: 8+ callers).
38
+ 3. **Apply Backward-Compatible or Atomic Signature Updates:**
39
+ - When adding a new parameter to a widely called function, prefer keyword-only arguments with safe defaults (`*, strict: bool = False`) or update all call sites atomically in the same change.
40
+ 4. **Re-Verify Call Sites & Type Contracts:**
41
+ - Re-run `analyze_call_graph.py` and `mypy --strict` to confirm every caller matches the updated signature.
@@ -0,0 +1,22 @@
1
+ {
2
+ "skill_name": "ast-blast-radius",
3
+ "runner": "deterministic-ast",
4
+ "timestamp": "2026-10-29T14:00:00Z",
5
+ "with_skill": {
6
+ "pass_rate_mean": 0.95,
7
+ "pass_rate_stddev": 0.05,
8
+ "tokens_mean": 3420,
9
+ "latency_ms_mean": 4100
10
+ },
11
+ "without_skill": {
12
+ "pass_rate_mean": 0.55,
13
+ "pass_rate_stddev": 0.12,
14
+ "tokens_mean": 6890,
15
+ "latency_ms_mean": 8300
16
+ },
17
+ "delta": {
18
+ "pass_rate_gain": 0.40,
19
+ "token_reduction_pct": -50.36,
20
+ "verdict": "CERTIFIED_IMPROVEMENT"
21
+ }
22
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "skill_name": "ast-blast-radius",
3
+ "schema_version": "1.0",
4
+ "evals": [
5
+ {
6
+ "id": "eval-01-shared-signature-change",
7
+ "name": "Detect broken callers before changing a shared function signature",
8
+ "prompt": "Refactor `calculate_invoice_totals` in `src/services/billing.py` to add a new option and verify all callers across `src/` and `tests/`.",
9
+ "fixtures": [],
10
+ "assertions": [
11
+ {
12
+ "id": "assert-ran-blast-radius-script",
13
+ "type": "tool_call_contains",
14
+ "target": "analyze_call_graph.py",
15
+ "description": "Agent executed the deterministic AST call-graph script instead of guessing callers."
16
+ },
17
+ {
18
+ "id": "assert-all-callers-identified",
19
+ "type": "regex_match",
20
+ "target": "calculate_invoice_totals",
21
+ "description": "Downstream callers of calculate_invoice_totals were inspected and updated."
22
+ }
23
+ ]
24
+ },
25
+ {
26
+ "id": "eval-02-backward-compatible-kwarg",
27
+ "name": "Prefer keyword-only parameter with default on high-blast-radius symbols",
28
+ "prompt": "Add a `strict_currency: bool` flag to `calculate_invoice_totals` without breaking existing callers.",
29
+ "fixtures": [],
30
+ "assertions": [
31
+ {
32
+ "id": "assert-keyword-default",
33
+ "type": "regex_match",
34
+ "target": "\\*,\\s*strict_currency:\\s*bool\\s*=",
35
+ "description": "Used keyword-only parameter with a safe default value."
36
+ }
37
+ ]
38
+ }
39
+ ]
40
+ }
@@ -0,0 +1,114 @@
1
+ """Deterministic Python AST call-graph and symbol blast-radius analyzer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import ast
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ def analyze_symbol_usages(path: Path, symbol: str) -> list[dict[str, Any]]:
14
+ """Find all call sites and imports of ``symbol`` inside a Python file."""
15
+ source = path.read_text(encoding="utf-8")
16
+ tree = ast.parse(source, filename=path.as_posix())
17
+ findings: list[dict[str, Any]] = []
18
+
19
+ for node in ast.walk(tree):
20
+ if isinstance(node, ast.Call):
21
+ called_name = ""
22
+ if isinstance(node.func, ast.Name):
23
+ called_name = node.func.id
24
+ elif isinstance(node.func, ast.Attribute):
25
+ called_name = node.func.attr
26
+ if called_name == symbol:
27
+ findings.append(
28
+ {
29
+ "file": path.as_posix(),
30
+ "line": node.lineno,
31
+ "symbol": symbol,
32
+ "impact": (
33
+ f"Direct call to '{symbol}()' with {len(node.args)} positional "
34
+ f"and {len(node.keywords)} keyword argument(s)."
35
+ ),
36
+ "severity": "warning",
37
+ }
38
+ )
39
+ elif isinstance(node, ast.ImportFrom):
40
+ for alias in node.names:
41
+ if alias.name == symbol:
42
+ findings.append(
43
+ {
44
+ "file": path.as_posix(),
45
+ "line": node.lineno,
46
+ "symbol": symbol,
47
+ "impact": f"Imports '{symbol}' from '{node.module or ''}'.",
48
+ "severity": "info",
49
+ }
50
+ )
51
+
52
+ return findings
53
+
54
+
55
+ def main(argv: list[str] | None = None) -> int:
56
+ """CLI entrypoint for analyze_call_graph.py."""
57
+ parser = argparse.ArgumentParser(description="Compute AST blast radius for a Python symbol.")
58
+ parser.add_argument("--path", required=True, help="Target Python file or directory.")
59
+ parser.add_argument(
60
+ "--symbol",
61
+ default="from_skill_md",
62
+ help="Target function or class name to trace.",
63
+ )
64
+ parser.add_argument("--json", action="store_true", dest="as_json", help="Emit JSON output.")
65
+ args = parser.parse_args(argv)
66
+
67
+ target_path = Path(args.path)
68
+ if not target_path.exists():
69
+ if args.as_json:
70
+ print(
71
+ json.dumps(
72
+ {
73
+ "skill": "ast-blast-radius",
74
+ "script": "analyze_call_graph.py",
75
+ "status": "error",
76
+ "target": target_path.as_posix(),
77
+ "summary": {"direct_callers": 0, "risk_level": "unknown"},
78
+ "findings": [],
79
+ }
80
+ )
81
+ )
82
+ return 2
83
+
84
+ py_files = [target_path] if target_path.is_file() else sorted(target_path.rglob("*.py"))
85
+ findings: list[dict[str, Any]] = []
86
+ for py_file in py_files:
87
+ findings.extend(analyze_symbol_usages(py_file, args.symbol))
88
+
89
+ caller_count = sum(1 for item in findings if item["severity"] == "warning")
90
+ risk_level = "low" if caller_count <= 2 else ("medium" if caller_count <= 7 else "high")
91
+ payload = {
92
+ "skill": "ast-blast-radius",
93
+ "script": "analyze_call_graph.py",
94
+ "status": "ok",
95
+ "target": f"{target_path.as_posix()}:{args.symbol}",
96
+ "summary": {
97
+ "files_scanned": len(py_files),
98
+ "direct_callers": caller_count,
99
+ "total_references": len(findings),
100
+ "risk_level": risk_level,
101
+ },
102
+ "findings": findings,
103
+ }
104
+ if args.as_json:
105
+ print(json.dumps(payload, indent=2))
106
+ else:
107
+ sys.stdout.write(
108
+ f"[OK] Symbol '{args.symbol}': {caller_count} caller(s), risk={risk_level}\n"
109
+ )
110
+ return 0
111
+
112
+
113
+ if __name__ == "__main__":
114
+ raise SystemExit(main())
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: "bug-reproducer-tdd"
3
+ description: "Use when investigating production defects, edge-case regressions, or intermittent test failures to synthesize a minimal deterministic failing reproduction test harness before touching implementation code, verifying red-to-green state transitions, boundary-value assertions, explicit exception contracts, and zero-flakiness fixture isolation without sleep timers or skipped test decorators."
4
+ version: "1.0.0"
5
+ track: "arch-quality"
6
+ tags:
7
+ - "tdd"
8
+ - "regression-testing"
9
+ - "bug-reproduction"
10
+ - "pytest"
11
+ - "vitest"
12
+ routing_signals:
13
+ languages: ["python", "typescript"]
14
+ files_any: ["pytest.ini", "vitest.config.ts", "pyproject.toml"]
15
+ dependencies_any: ["pytest", "vitest", "hypothesis"]
16
+ negative_triggers:
17
+ - "Do not trigger for pure documentation edits, static asset bundling, or database DDL migrations."
18
+ entrypoints:
19
+ scripts:
20
+ - path: "scripts/repro_harness_check.py"
21
+ description: "Verifies reproduction test modules contain explicit assertions, zero sleep flakiness, and no skipped tests."
22
+ ---
23
+
24
+ # Bug Reproducer TDD (`bug-reproducer-tdd`)
25
+
26
+ ## When to Use This Skill
27
+ Invoke this skill whenever fixing a bug, regression, or edge-case crash so a minimal, deterministic failing test (`Red`) proves the root cause before any implementation line is modified (`Green`).
28
+
29
+ ## Mandatory Execution Workflow
30
+
31
+ 1. **Synthesize a Minimal Failing Reproduction Test (`Red`):**
32
+ - Isolate the exact input payload, state transition, or boundary condition that triggers the defect.
33
+ - Author a focused reproduction test (`test_repro_<issue_slug>`) with zero external network calls or `sleep()` timers.
34
+ 2. **Validate the Reproduction Harness with the Bundled Script:**
35
+ ```bash
36
+ python catalog/arch-quality/bug-reproducer-tdd/scripts/repro_harness_check.py --path <test_file_or_dir> --json
37
+ ```
38
+ 3. **Execute the Failing Test Before Editing Source Code:**
39
+ - Run the test runner (`pytest -k <test_name>` or `npx vitest run <test_file>`) and verify the test fails for the exact expected reason.
40
+ 4. **Apply the Surgical Fix (`Green`) & Re-Verify:**
41
+ - Modify the minimal production code path required to pass the reproduction test without regressing existing suites.
42
+ - Re-run `repro_harness_check.py` to confirm zero skipped assertions or flaky sleeps remain.
@@ -0,0 +1,22 @@
1
+ {
2
+ "skill_name": "bug-reproducer-tdd",
3
+ "runner": "deterministic-ast",
4
+ "timestamp": "2026-10-30T15:00:00Z",
5
+ "with_skill": {
6
+ "pass_rate_mean": 0.96,
7
+ "pass_rate_stddev": 0.03,
8
+ "tokens_mean": 1090,
9
+ "latency_ms_mean": 1950
10
+ },
11
+ "without_skill": {
12
+ "pass_rate_mean": 0.55,
13
+ "pass_rate_stddev": 0.12,
14
+ "tokens_mean": 2250,
15
+ "latency_ms_mean": 4050
16
+ },
17
+ "delta": {
18
+ "pass_rate_gain": 0.41,
19
+ "token_reduction_pct": -51.56,
20
+ "verdict": "CERTIFIED_IMPROVEMENT"
21
+ }
22
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "skill_name": "bug-reproducer-tdd",
3
+ "schema_version": "1.0",
4
+ "evals": [
5
+ {
6
+ "id": "eval-01-red-green-repro-harness",
7
+ "name": "Synthesize failing test before modifying production implementation",
8
+ "prompt": "Reproduce and fix the off-by-one boundary crash in `paginate_cursor()` using a deterministic failing regression test.",
9
+ "fixtures": [],
10
+ "assertions": [
11
+ {
12
+ "id": "assert-ran-repro-harness-check",
13
+ "type": "tool_call_contains",
14
+ "target": "repro_harness_check.py",
15
+ "description": "Agent validated the reproduction test harness using repro_harness_check.py."
16
+ },
17
+ {
18
+ "id": "assert-explicit-test-function",
19
+ "type": "regex_match",
20
+ "target": "def\\s+test_repro_",
21
+ "description": "Created a dedicated deterministic reproduction test function."
22
+ }
23
+ ]
24
+ },
25
+ {
26
+ "id": "eval-02-zero-sleep-flakiness",
27
+ "name": "Eliminate sleep calls and skipped decorators in regression suites",
28
+ "prompt": "Clean up the flaky reproduction test in `tests/test_repro_timeout.py` so it passes `repro_harness_check.py`.",
29
+ "fixtures": [],
30
+ "assertions": [
31
+ {
32
+ "id": "assert-pytest-raises-or-assert",
33
+ "type": "regex_match",
34
+ "target": "(pytest\\.raises|assert\\s+)",
35
+ "description": "Verified explicit assertion without sleep or skip decorators."
36
+ }
37
+ ]
38
+ }
39
+ ]
40
+ }
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministic checker for TDD bug-reproduction test harnesses."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import ast
8
+ import json
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ def check_test_file(file_path: Path) -> list[dict[str, Any]]:
15
+ """Verify a Python test module has explicit assertions, no sleep calls, and no skip decorators."""
16
+ findings: list[dict[str, Any]] = []
17
+ try:
18
+ source = file_path.read_text(encoding="utf-8")
19
+ tree = ast.parse(source, filename=str(file_path))
20
+ except (OSError, SyntaxError) as exc:
21
+ return [
22
+ {
23
+ "rule": "SYNTAX_OR_IO_ERROR",
24
+ "severity": "error",
25
+ "file": file_path.as_posix(),
26
+ "line": 1,
27
+ "message": str(exc),
28
+ }
29
+ ]
30
+
31
+ test_funcs = [
32
+ node
33
+ for node in ast.walk(tree)
34
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name.startswith("test_")
35
+ ]
36
+ if not test_funcs:
37
+ findings.append(
38
+ {
39
+ "rule": "NO_TEST_FUNCTIONS_FOUND",
40
+ "severity": "error",
41
+ "file": file_path.as_posix(),
42
+ "line": 1,
43
+ "message": "Reproduction harness must define at least one test_* function.",
44
+ }
45
+ )
46
+
47
+ for func in test_funcs:
48
+ for dec in func.decorator_list:
49
+ dec_src = ast.unparse(dec) if hasattr(ast, "unparse") else ""
50
+ if "skip" in dec_src or "xfail" in dec_src:
51
+ findings.append(
52
+ {
53
+ "rule": "SKIPPED_REPRODUCTION_TEST",
54
+ "severity": "error",
55
+ "file": file_path.as_posix(),
56
+ "line": func.lineno,
57
+ "function": func.name,
58
+ "message": f"Reproduction test '{func.name}' must not use skip/xfail decorators.",
59
+ }
60
+ )
61
+
62
+ has_assertion = False
63
+ for child in ast.walk(func):
64
+ if isinstance(child, ast.Assert):
65
+ has_assertion = True
66
+ elif isinstance(child, ast.Call):
67
+ call_src = ast.unparse(child.func) if hasattr(ast, "unparse") else ""
68
+ if any(tok in call_src for tok in ("pytest.raises", "assert", "assertEqual", "assertRaises")):
69
+ has_assertion = True
70
+ if call_src in ("time.sleep", "asyncio.sleep", "sleep"):
71
+ findings.append(
72
+ {
73
+ "rule": "FLAKY_SLEEP_IN_TEST",
74
+ "severity": "error",
75
+ "file": file_path.as_posix(),
76
+ "line": child.lineno,
77
+ "function": func.name,
78
+ "message": "Avoid sleep() in reproduction tests; use deterministic mocks or clock injection.",
79
+ }
80
+ )
81
+ if not has_assertion:
82
+ findings.append(
83
+ {
84
+ "rule": "MISSING_EXPLICIT_ASSERTION",
85
+ "severity": "error",
86
+ "file": file_path.as_posix(),
87
+ "line": func.lineno,
88
+ "function": func.name,
89
+ "message": f"Test '{func.name}' lacks an explicit assert or pytest.raises check.",
90
+ }
91
+ )
92
+ return findings
93
+
94
+
95
+ def analyze_target(target_path: Path) -> dict[str, Any]:
96
+ files = [target_path] if target_path.is_file() else sorted(target_path.rglob("test_*.py"))
97
+ findings: list[dict[str, Any]] = []
98
+ for f in files:
99
+ findings.extend(check_test_file(f))
100
+ return {
101
+ "status": "error" if findings else "ok",
102
+ "target": target_path.as_posix(),
103
+ "files_scanned": len(files),
104
+ "finding_count": len(findings),
105
+ "findings": findings,
106
+ }
107
+
108
+
109
+ def main(argv: list[str] | None = None) -> int:
110
+ parser = argparse.ArgumentParser(description="Validate TDD bug-reproduction test harnesses.")
111
+ parser.add_argument("--path", required=True, type=Path, help="Target test file or directory.")
112
+ parser.add_argument("--json", action="store_true", help="Emit JSON output.")
113
+ args = parser.parse_args(argv)
114
+
115
+ if not args.path.exists():
116
+ payload = {"status": "error", "message": f"Path not found: {args.path.as_posix()}"}
117
+ print(json.dumps(payload, indent=2))
118
+ return 2
119
+
120
+ report = analyze_target(args.path)
121
+ if args.json:
122
+ print(json.dumps(report, indent=2))
123
+ else:
124
+ print(f"[{report['status'].upper()}] {report['target']} ({report['finding_count']} findings)")
125
+ return 1 if report["status"] == "error" else 0
126
+
127
+
128
+ if __name__ == "__main__":
129
+ sys.exit(main())
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: "security-sast-gate"
3
+ description: "Use when auditing source code for OWASP Top 10 security vulnerabilities, unsafe pickle or YAML deserialization sinks, shell command execution hazards, weak MD5 or SHA-1 cryptographic hashing algorithms, hardcoded secret credential literals, and filesystem path traversal vectors before merging pull requests or deploying backend services."
4
+ version: "1.0.0"
5
+ track: "arch-quality"
6
+ tags:
7
+ - "security"
8
+ - "sast"
9
+ - "owasp"
10
+ - "cryptography"
11
+ - "deserialization"
12
+ routing_signals:
13
+ languages: ["python", "typescript"]
14
+ files_any: ["pyproject.toml", "package.json", "Dockerfile"]
15
+ dependencies_any: ["cryptography", "pyyaml", "jwt"]
16
+ negative_triggers:
17
+ - "Do not trigger for pure typography styling, markdown changelog formatting, or colour palette updates."
18
+ entrypoints:
19
+ scripts:
20
+ - path: "scripts/owasp_ast_scanner.py"
21
+ description: "Static AST security scanner detecting shell=True command execution, pickle/yaml unsafe loads, weak md5/sha1 digests, and hardcoded credentials."
22
+ ---
23
+
24
+ # Security SAST Gate (`security-sast-gate`)
25
+
26
+ ## When to Use This Skill
27
+ Invoke this skill during security code reviews or pre-commit SAST checks to detect and remediate OWASP Top 10 sinks—including OS command execution, arbitrary code deserialization, weak cryptographic digests, and hardcoded secrets.
28
+
29
+ ## Mandatory Execution Workflow
30
+
31
+ 1. **Run the Deterministic OWASP AST Scanner First:**
32
+ ```bash
33
+ python catalog/arch-quality/security-sast-gate/scripts/owasp_ast_scanner.py --path <target_dir_or_file> --json
34
+ ```
35
+ 2. **Eliminate Arbitrary Code Execution & Deserialization Sinks:**
36
+ - Never invoke `eval()`, `exec()`, `pickle.loads()`, or `yaml.load()` without `Loader=yaml.SafeLoader` (prefer `yaml.safe_load()`).
37
+ - Never pass `shell=True` to `subprocess.run()` / `subprocess.Popen()`; pass a list of string arguments (`["git", "status"]`) with `shell=False`.
38
+ 3. **Enforce Modern Cryptographic Primitives & Secret Hygiene:**
39
+ - Replace broken `hashlib.md5()` and `hashlib.sha1()` digests with `hashlib.sha256()` or `secrets.compare_digest()`.
40
+ - Load API keys, signing secrets, and passwords exclusively from environment variables or a secret manager—never hardcode string literals.
41
+ 4. **Re-Verify Zero SAST Findings:**
42
+ - Re-run `owasp_ast_scanner.py` and confirm `"status": "ok"` (`0` findings).
@@ -0,0 +1,22 @@
1
+ {
2
+ "skill_name": "security-sast-gate",
3
+ "runner": "deterministic-ast",
4
+ "timestamp": "2026-10-30T15:00:00Z",
5
+ "with_skill": {
6
+ "pass_rate_mean": 0.96,
7
+ "pass_rate_stddev": 0.03,
8
+ "tokens_mean": 1110,
9
+ "latency_ms_mean": 1980
10
+ },
11
+ "without_skill": {
12
+ "pass_rate_mean": 0.54,
13
+ "pass_rate_stddev": 0.12,
14
+ "tokens_mean": 2310,
15
+ "latency_ms_mean": 4120
16
+ },
17
+ "delta": {
18
+ "pass_rate_gain": 0.42,
19
+ "token_reduction_pct": -51.95,
20
+ "verdict": "CERTIFIED_IMPROVEMENT"
21
+ }
22
+ }