crucible-mcp 0.5.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
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.
- crucible/cli.py +109 -2
- crucible/enforcement/bundled/error-handling.yaml +84 -0
- crucible/enforcement/bundled/security.yaml +123 -0
- crucible/enforcement/bundled/smart-contract.yaml +110 -0
- crucible/enforcement/compliance.py +9 -5
- crucible/hooks/claudecode.py +388 -0
- crucible/hooks/precommit.py +117 -25
- crucible/knowledge/loader.py +186 -0
- crucible/knowledge/principles/API_DESIGN.md +176 -0
- crucible/knowledge/principles/COMMITS.md +127 -0
- crucible/knowledge/principles/DATABASE.md +138 -0
- crucible/knowledge/principles/DOCUMENTATION.md +201 -0
- crucible/knowledge/principles/ERROR_HANDLING.md +157 -0
- crucible/knowledge/principles/FP.md +162 -0
- crucible/knowledge/principles/GITIGNORE.md +218 -0
- crucible/knowledge/principles/OBSERVABILITY.md +147 -0
- crucible/knowledge/principles/PRECOMMIT.md +201 -0
- crucible/knowledge/principles/SECURITY.md +136 -0
- crucible/knowledge/principles/SMART_CONTRACT.md +153 -0
- crucible/knowledge/principles/SYSTEM_DESIGN.md +153 -0
- crucible/knowledge/principles/TESTING.md +129 -0
- crucible/knowledge/principles/TYPE_SAFETY.md +170 -0
- crucible/skills/accessibility-engineer/SKILL.md +71 -0
- crucible/skills/backend-engineer/SKILL.md +69 -0
- crucible/skills/customer-success/SKILL.md +69 -0
- crucible/skills/data-engineer/SKILL.md +70 -0
- crucible/skills/devops-engineer/SKILL.md +69 -0
- crucible/skills/fde-engineer/SKILL.md +69 -0
- crucible/skills/formal-verification/SKILL.md +86 -0
- crucible/skills/gas-optimizer/SKILL.md +89 -0
- crucible/skills/incident-responder/SKILL.md +91 -0
- crucible/skills/mev-researcher/SKILL.md +87 -0
- crucible/skills/mobile-engineer/SKILL.md +70 -0
- crucible/skills/performance-engineer/SKILL.md +68 -0
- crucible/skills/product-engineer/SKILL.md +68 -0
- crucible/skills/protocol-architect/SKILL.md +83 -0
- crucible/skills/security-engineer/SKILL.md +63 -0
- crucible/skills/tech-lead/SKILL.md +92 -0
- crucible/skills/uiux-engineer/SKILL.md +70 -0
- crucible/skills/web3-engineer/SKILL.md +79 -0
- crucible_mcp-1.0.1.dist-info/METADATA +198 -0
- crucible_mcp-1.0.1.dist-info/RECORD +66 -0
- crucible_mcp-0.5.0.dist-info/METADATA +0 -161
- crucible_mcp-0.5.0.dist-info/RECORD +0 -30
- {crucible_mcp-0.5.0.dist-info → crucible_mcp-1.0.1.dist-info}/WHEEL +0 -0
- {crucible_mcp-0.5.0.dist-info → crucible_mcp-1.0.1.dist-info}/entry_points.txt +0 -0
- {crucible_mcp-0.5.0.dist-info → crucible_mcp-1.0.1.dist-info}/top_level.txt +0 -0
crucible/cli.py
CHANGED
|
@@ -578,6 +578,27 @@ def _cmd_review_no_git(args: argparse.Namespace, path: str) -> int:
|
|
|
578
578
|
# Deduplicate
|
|
579
579
|
all_findings = deduplicate_findings(all_findings)
|
|
580
580
|
|
|
581
|
+
# Run enforcement assertions
|
|
582
|
+
from crucible.review.core import run_enforcement
|
|
583
|
+
|
|
584
|
+
compliance_config = _build_compliance_config(
|
|
585
|
+
config,
|
|
586
|
+
cli_token_budget=getattr(args, "token_budget", None),
|
|
587
|
+
cli_model=getattr(args, "compliance_model", None),
|
|
588
|
+
cli_no_compliance=getattr(args, "no_compliance", False),
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
# Use current directory as repo root for enforcement
|
|
592
|
+
enforcement_findings, enforcement_errors, assertions_checked, assertions_skipped, budget_state = (
|
|
593
|
+
run_enforcement(
|
|
594
|
+
".",
|
|
595
|
+
changed_files=files_to_analyze,
|
|
596
|
+
repo_root=".",
|
|
597
|
+
compliance_config=compliance_config,
|
|
598
|
+
)
|
|
599
|
+
)
|
|
600
|
+
tool_errors.extend(enforcement_errors)
|
|
601
|
+
|
|
581
602
|
# Compute severity summary
|
|
582
603
|
severity_counts = compute_severity_counts(all_findings)
|
|
583
604
|
|
|
@@ -607,6 +628,21 @@ def _cmd_review_no_git(args: argparse.Namespace, path: str) -> int:
|
|
|
607
628
|
}
|
|
608
629
|
for f in all_findings
|
|
609
630
|
],
|
|
631
|
+
"enforcement": {
|
|
632
|
+
"findings": [
|
|
633
|
+
{
|
|
634
|
+
"assertion_id": f.assertion_id,
|
|
635
|
+
"severity": f.severity,
|
|
636
|
+
"message": f.message,
|
|
637
|
+
"location": f.location,
|
|
638
|
+
"source": f.source,
|
|
639
|
+
}
|
|
640
|
+
for f in enforcement_findings
|
|
641
|
+
],
|
|
642
|
+
"assertions_checked": assertions_checked,
|
|
643
|
+
"assertions_skipped": assertions_skipped,
|
|
644
|
+
"tokens_used": budget_state.tokens_used if budget_state else 0,
|
|
645
|
+
},
|
|
610
646
|
"severity_counts": severity_counts,
|
|
611
647
|
"passed": passed,
|
|
612
648
|
"threshold": default_threshold.value if default_threshold else None,
|
|
@@ -616,7 +652,7 @@ def _cmd_review_no_git(args: argparse.Namespace, path: str) -> int:
|
|
|
616
652
|
else:
|
|
617
653
|
# Text output
|
|
618
654
|
if all_findings:
|
|
619
|
-
print(f"\nFound {len(all_findings)} issue(s):\n")
|
|
655
|
+
print(f"\nFound {len(all_findings)} static analysis issue(s):\n")
|
|
620
656
|
for f in all_findings:
|
|
621
657
|
sev_icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🔵", "info": "⚪"}.get(
|
|
622
658
|
f.severity.value, "⚪"
|
|
@@ -626,7 +662,18 @@ def _cmd_review_no_git(args: argparse.Namespace, path: str) -> int:
|
|
|
626
662
|
if f.suggestion:
|
|
627
663
|
print(f" 💡 {f.suggestion}")
|
|
628
664
|
print()
|
|
629
|
-
|
|
665
|
+
|
|
666
|
+
# Enforcement findings
|
|
667
|
+
if enforcement_findings:
|
|
668
|
+
print(f"\nEnforcement Assertions ({len(enforcement_findings)}):")
|
|
669
|
+
for f in enforcement_findings:
|
|
670
|
+
sev_icon = {"error": "🔴", "warning": "🟠", "info": "⚪"}.get(f.severity, "⚪")
|
|
671
|
+
source_tag = "[LLM]" if f.source == "llm" else "[Pattern]"
|
|
672
|
+
print(f" {sev_icon} [{f.severity.upper()}] {source_tag} {f.assertion_id}: {f.location}")
|
|
673
|
+
print(f" {f.message}")
|
|
674
|
+
print()
|
|
675
|
+
|
|
676
|
+
if not all_findings and not enforcement_findings:
|
|
630
677
|
print("\n✅ No issues found.")
|
|
631
678
|
|
|
632
679
|
# Summary
|
|
@@ -634,6 +681,11 @@ def _cmd_review_no_git(args: argparse.Namespace, path: str) -> int:
|
|
|
634
681
|
counts_str = ", ".join(f"{k}: {v}" for k, v in severity_counts.items() if v > 0)
|
|
635
682
|
print(f"Summary: {counts_str}")
|
|
636
683
|
|
|
684
|
+
if assertions_checked or assertions_skipped:
|
|
685
|
+
print(f"Assertions: {assertions_checked} checked, {assertions_skipped} skipped")
|
|
686
|
+
if budget_state and budget_state.tokens_used > 0:
|
|
687
|
+
print(f" LLM tokens used: {budget_state.tokens_used}")
|
|
688
|
+
|
|
637
689
|
if tool_errors and not args.quiet:
|
|
638
690
|
print(f"\n⚠️ {len(tool_errors)} tool error(s)")
|
|
639
691
|
for err in tool_errors[:5]:
|
|
@@ -1682,11 +1734,30 @@ include_context: false
|
|
|
1682
1734
|
if not gitignore_path.exists():
|
|
1683
1735
|
gitignore_path.write_text("# Local overrides (optional)\n*.local.md\n")
|
|
1684
1736
|
|
|
1737
|
+
# Create minimal CLAUDE.md if requested
|
|
1738
|
+
if args.with_claudemd:
|
|
1739
|
+
claudemd_path = project_path / "CLAUDE.md"
|
|
1740
|
+
if claudemd_path.exists() and not args.force:
|
|
1741
|
+
print(f"Warning: {claudemd_path} exists, skipping (use --force to overwrite)")
|
|
1742
|
+
else:
|
|
1743
|
+
project_name = project_path.name
|
|
1744
|
+
claudemd_content = f"""# {project_name}
|
|
1745
|
+
|
|
1746
|
+
Use Crucible for code review: `crucible review`
|
|
1747
|
+
|
|
1748
|
+
For full engineering principles and patterns, run:
|
|
1749
|
+
- `crucible knowledge list` - see available knowledge
|
|
1750
|
+
- `crucible skills list` - see available review personas
|
|
1751
|
+
"""
|
|
1752
|
+
claudemd_path.write_text(claudemd_content)
|
|
1753
|
+
print(f"Created {claudemd_path}")
|
|
1754
|
+
|
|
1685
1755
|
print(f"\nInitialized {crucible_dir}")
|
|
1686
1756
|
print("\nNext steps:")
|
|
1687
1757
|
print(" 1. Customize skills: crucible skills init <skill>")
|
|
1688
1758
|
print(" 2. Customize knowledge: crucible knowledge init <file>")
|
|
1689
1759
|
print(" 3. Install git hooks: crucible hooks install")
|
|
1760
|
+
print(" 4. Claude Code hooks: crucible hooks claudecode init")
|
|
1690
1761
|
return 0
|
|
1691
1762
|
|
|
1692
1763
|
|
|
@@ -1950,6 +2021,28 @@ def main() -> int:
|
|
|
1950
2021
|
"path", nargs="?", default=".", help="Repository path"
|
|
1951
2022
|
)
|
|
1952
2023
|
|
|
2024
|
+
# hooks claudecode
|
|
2025
|
+
hooks_claudecode_parser = hooks_sub.add_parser(
|
|
2026
|
+
"claudecode",
|
|
2027
|
+
help="Claude Code hooks integration"
|
|
2028
|
+
)
|
|
2029
|
+
hooks_claudecode_sub = hooks_claudecode_parser.add_subparsers(dest="claudecode_command")
|
|
2030
|
+
|
|
2031
|
+
# hooks claudecode init
|
|
2032
|
+
hooks_cc_init_parser = hooks_claudecode_sub.add_parser(
|
|
2033
|
+
"init",
|
|
2034
|
+
help="Initialize Claude Code hooks for project"
|
|
2035
|
+
)
|
|
2036
|
+
hooks_cc_init_parser.add_argument(
|
|
2037
|
+
"path", nargs="?", default=".", help="Project path"
|
|
2038
|
+
)
|
|
2039
|
+
|
|
2040
|
+
# hooks claudecode hook (called by Claude Code)
|
|
2041
|
+
hooks_claudecode_sub.add_parser(
|
|
2042
|
+
"hook",
|
|
2043
|
+
help="Run hook (reads JSON from stdin)"
|
|
2044
|
+
)
|
|
2045
|
+
|
|
1953
2046
|
# === review command ===
|
|
1954
2047
|
review_parser = subparsers.add_parser(
|
|
1955
2048
|
"review",
|
|
@@ -2044,6 +2137,10 @@ def main() -> int:
|
|
|
2044
2137
|
"--minimal", action="store_true",
|
|
2045
2138
|
help="Create minimal config without copying skills"
|
|
2046
2139
|
)
|
|
2140
|
+
init_proj_parser.add_argument(
|
|
2141
|
+
"--with-claudemd", action="store_true",
|
|
2142
|
+
help="Generate minimal CLAUDE.md that points to Crucible"
|
|
2143
|
+
)
|
|
2047
2144
|
init_proj_parser.add_argument(
|
|
2048
2145
|
"path", nargs="?", default=".",
|
|
2049
2146
|
help="Project path (default: current directory)"
|
|
@@ -2161,6 +2258,15 @@ def main() -> int:
|
|
|
2161
2258
|
return cmd_hooks_uninstall(args)
|
|
2162
2259
|
elif args.hooks_command == "status":
|
|
2163
2260
|
return cmd_hooks_status(args)
|
|
2261
|
+
elif args.hooks_command == "claudecode":
|
|
2262
|
+
from crucible.hooks.claudecode import main_init, run_hook
|
|
2263
|
+
if args.claudecode_command == "init":
|
|
2264
|
+
return main_init(args.path)
|
|
2265
|
+
elif args.claudecode_command == "hook":
|
|
2266
|
+
return run_hook()
|
|
2267
|
+
else:
|
|
2268
|
+
hooks_claudecode_parser.print_help()
|
|
2269
|
+
return 0
|
|
2164
2270
|
else:
|
|
2165
2271
|
hooks_parser.print_help()
|
|
2166
2272
|
return 0
|
|
@@ -2211,6 +2317,7 @@ def main() -> int:
|
|
|
2211
2317
|
print(" crucible hooks install Install pre-commit hook to .git/hooks/")
|
|
2212
2318
|
print(" crucible hooks uninstall Remove pre-commit hook")
|
|
2213
2319
|
print(" crucible hooks status Show hook installation status")
|
|
2320
|
+
print(" crucible hooks claudecode init Initialize Claude Code hooks")
|
|
2214
2321
|
print()
|
|
2215
2322
|
print(" crucible assertions list List assertion files from all sources")
|
|
2216
2323
|
print(" crucible assertions validate Validate assertion files")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
version: "1.0"
|
|
2
|
+
name: error-handling
|
|
3
|
+
description: Error handling best practices to prevent silent failures
|
|
4
|
+
linked_knowledge: ERROR_HANDLING.md
|
|
5
|
+
|
|
6
|
+
assertions:
|
|
7
|
+
# High: Silent failures
|
|
8
|
+
- id: no-bare-except
|
|
9
|
+
type: pattern
|
|
10
|
+
pattern: "except\\s*:"
|
|
11
|
+
message: "Bare except catches everything including SystemExit/KeyboardInterrupt - catch specific exceptions"
|
|
12
|
+
severity: warning
|
|
13
|
+
priority: high
|
|
14
|
+
languages: [python]
|
|
15
|
+
|
|
16
|
+
- id: no-pass-in-except
|
|
17
|
+
type: pattern
|
|
18
|
+
pattern: "except[^:]*:\\s*\\n\\s*pass\\s*$"
|
|
19
|
+
message: "Empty except block silently swallows errors - log or re-raise"
|
|
20
|
+
severity: warning
|
|
21
|
+
priority: high
|
|
22
|
+
languages: [python]
|
|
23
|
+
|
|
24
|
+
- id: no-empty-catch
|
|
25
|
+
type: pattern
|
|
26
|
+
pattern: "catch\\s*\\([^)]*\\)\\s*\\{\\s*\\}"
|
|
27
|
+
message: "Empty catch block silently swallows errors - log or re-throw"
|
|
28
|
+
severity: warning
|
|
29
|
+
priority: high
|
|
30
|
+
languages: [javascript, typescript]
|
|
31
|
+
|
|
32
|
+
# Medium: Pokemon exception handling
|
|
33
|
+
- id: no-catch-exception
|
|
34
|
+
type: pattern
|
|
35
|
+
pattern: "except\\s+Exception\\s*:"
|
|
36
|
+
message: "Catching Exception is too broad - catch specific exception types"
|
|
37
|
+
severity: info
|
|
38
|
+
priority: medium
|
|
39
|
+
languages: [python]
|
|
40
|
+
|
|
41
|
+
- id: no-catch-base-exception
|
|
42
|
+
type: pattern
|
|
43
|
+
pattern: "except\\s+BaseException\\s*:"
|
|
44
|
+
message: "Catching BaseException includes SystemExit/KeyboardInterrupt - use Exception or specific types"
|
|
45
|
+
severity: warning
|
|
46
|
+
priority: medium
|
|
47
|
+
languages: [python]
|
|
48
|
+
|
|
49
|
+
# Medium: Error suppression
|
|
50
|
+
- id: no-ignore-errors-flag
|
|
51
|
+
type: pattern
|
|
52
|
+
pattern: "errors\\s*=\\s*[\"']ignore[\"']"
|
|
53
|
+
message: "errors='ignore' silently drops data - use 'replace' or handle explicitly"
|
|
54
|
+
severity: warning
|
|
55
|
+
priority: medium
|
|
56
|
+
languages: [python]
|
|
57
|
+
|
|
58
|
+
# Info: Best practices
|
|
59
|
+
- id: prefer-contextlib-suppress
|
|
60
|
+
type: pattern
|
|
61
|
+
pattern: "except\\s+\\w+\\s*:\\s*\\n\\s*pass\\s*$"
|
|
62
|
+
message: "Consider contextlib.suppress() for intentionally ignoring specific exceptions"
|
|
63
|
+
severity: info
|
|
64
|
+
priority: low
|
|
65
|
+
languages: [python]
|
|
66
|
+
|
|
67
|
+
# LLM assertion for complex error handling patterns
|
|
68
|
+
- id: error-handling-semantic
|
|
69
|
+
type: llm
|
|
70
|
+
compliance: |
|
|
71
|
+
Check that error handling follows best practices:
|
|
72
|
+
1. Errors are not silently swallowed (no empty except/catch blocks that do nothing)
|
|
73
|
+
2. Exceptions are logged or reported before being suppressed
|
|
74
|
+
3. Error messages provide enough context to debug issues
|
|
75
|
+
4. Errors at API/system boundaries are handled (not just propagated)
|
|
76
|
+
message: "Error handling may need improvement"
|
|
77
|
+
severity: warning
|
|
78
|
+
priority: medium
|
|
79
|
+
languages: [python, javascript, typescript]
|
|
80
|
+
applicability:
|
|
81
|
+
exclude:
|
|
82
|
+
- "**/test_*.py"
|
|
83
|
+
- "**/*_test.py"
|
|
84
|
+
- "**/tests/**"
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
version: "1.0"
|
|
2
|
+
name: security
|
|
3
|
+
description: Security assertions to catch common vulnerabilities
|
|
4
|
+
linked_knowledge: SECURITY.md
|
|
5
|
+
|
|
6
|
+
assertions:
|
|
7
|
+
# Critical: Code execution risks
|
|
8
|
+
- id: no-eval
|
|
9
|
+
type: pattern
|
|
10
|
+
pattern: "\\beval\\s*\\("
|
|
11
|
+
message: "eval() is dangerous - use ast.literal_eval() for data or safer alternatives"
|
|
12
|
+
severity: error
|
|
13
|
+
priority: critical
|
|
14
|
+
languages: [python, javascript, typescript]
|
|
15
|
+
|
|
16
|
+
- id: no-exec
|
|
17
|
+
type: pattern
|
|
18
|
+
pattern: "\\bexec\\s*\\("
|
|
19
|
+
message: "exec() allows arbitrary code execution - avoid or sandbox carefully"
|
|
20
|
+
severity: error
|
|
21
|
+
priority: critical
|
|
22
|
+
languages: [python]
|
|
23
|
+
|
|
24
|
+
# Critical: Shell injection
|
|
25
|
+
- id: no-shell-true
|
|
26
|
+
type: pattern
|
|
27
|
+
pattern: "shell\\s*=\\s*True"
|
|
28
|
+
message: "shell=True enables shell injection - use shell=False with argument list"
|
|
29
|
+
severity: error
|
|
30
|
+
priority: critical
|
|
31
|
+
languages: [python]
|
|
32
|
+
|
|
33
|
+
- id: no-os-system
|
|
34
|
+
type: pattern
|
|
35
|
+
pattern: "os\\.system\\s*\\("
|
|
36
|
+
message: "os.system() is vulnerable to shell injection - use subprocess with shell=False"
|
|
37
|
+
severity: error
|
|
38
|
+
priority: critical
|
|
39
|
+
languages: [python]
|
|
40
|
+
|
|
41
|
+
# Critical: Deserialization
|
|
42
|
+
- id: no-pickle-load
|
|
43
|
+
type: pattern
|
|
44
|
+
pattern: "pickle\\.load|pickle\\.loads|cPickle\\.load"
|
|
45
|
+
message: "pickle can execute arbitrary code - use json or msgpack for untrusted data"
|
|
46
|
+
severity: error
|
|
47
|
+
priority: critical
|
|
48
|
+
languages: [python]
|
|
49
|
+
|
|
50
|
+
- id: no-yaml-unsafe-load
|
|
51
|
+
type: pattern
|
|
52
|
+
pattern: "yaml\\.load\\s*\\([^)]*\\)(?!.*Loader)"
|
|
53
|
+
message: "yaml.load() without Loader is unsafe - use yaml.safe_load()"
|
|
54
|
+
severity: error
|
|
55
|
+
priority: critical
|
|
56
|
+
languages: [python]
|
|
57
|
+
|
|
58
|
+
# High: Hardcoded secrets
|
|
59
|
+
- id: no-hardcoded-password
|
|
60
|
+
type: pattern
|
|
61
|
+
pattern: "(?i)(password|passwd|pwd)\\s*=\\s*[\"'][^\"']{4,}[\"']"
|
|
62
|
+
message: "Possible hardcoded password - use environment variables or secrets manager"
|
|
63
|
+
severity: error
|
|
64
|
+
priority: high
|
|
65
|
+
applicability:
|
|
66
|
+
exclude:
|
|
67
|
+
- "**/test_*.py"
|
|
68
|
+
- "**/*_test.py"
|
|
69
|
+
- "**/tests/**"
|
|
70
|
+
- "**/*.md"
|
|
71
|
+
|
|
72
|
+
- id: no-hardcoded-api-key
|
|
73
|
+
type: pattern
|
|
74
|
+
pattern: "(?i)(api[_-]?key|apikey|secret[_-]?key)\\s*=\\s*[\"'][a-zA-Z0-9]{16,}[\"']"
|
|
75
|
+
message: "Possible hardcoded API key - use environment variables or secrets manager"
|
|
76
|
+
severity: error
|
|
77
|
+
priority: high
|
|
78
|
+
applicability:
|
|
79
|
+
exclude:
|
|
80
|
+
- "**/test_*.py"
|
|
81
|
+
- "**/*_test.py"
|
|
82
|
+
- "**/tests/**"
|
|
83
|
+
- "**/*.md"
|
|
84
|
+
|
|
85
|
+
# High: SQL injection
|
|
86
|
+
- id: no-string-sql
|
|
87
|
+
type: pattern
|
|
88
|
+
pattern: "execute\\s*\\(\\s*[\"']\\s*SELECT|execute\\s*\\(\\s*f[\"']|execute\\s*\\([^)]*%\\s*\\("
|
|
89
|
+
message: "Possible SQL injection - use parameterized queries"
|
|
90
|
+
severity: error
|
|
91
|
+
priority: high
|
|
92
|
+
languages: [python]
|
|
93
|
+
|
|
94
|
+
# Medium: Weak crypto
|
|
95
|
+
- id: no-md5-security
|
|
96
|
+
type: pattern
|
|
97
|
+
pattern: "hashlib\\.md5|MD5\\s*\\(|md5\\.new"
|
|
98
|
+
message: "MD5 is cryptographically broken - use SHA-256 or better for security"
|
|
99
|
+
severity: warning
|
|
100
|
+
priority: medium
|
|
101
|
+
languages: [python]
|
|
102
|
+
|
|
103
|
+
- id: no-sha1-security
|
|
104
|
+
type: pattern
|
|
105
|
+
pattern: "hashlib\\.sha1|SHA1\\s*\\(|sha1\\.new"
|
|
106
|
+
message: "SHA-1 is deprecated for security - use SHA-256 or better"
|
|
107
|
+
severity: warning
|
|
108
|
+
priority: medium
|
|
109
|
+
languages: [python]
|
|
110
|
+
|
|
111
|
+
# Medium: Insecure random
|
|
112
|
+
- id: no-random-for-security
|
|
113
|
+
type: pattern
|
|
114
|
+
pattern: "\\brandom\\.(choice|randint|random|uniform)\\s*\\("
|
|
115
|
+
message: "random module is not cryptographically secure - use secrets module"
|
|
116
|
+
severity: warning
|
|
117
|
+
priority: medium
|
|
118
|
+
languages: [python]
|
|
119
|
+
applicability:
|
|
120
|
+
exclude:
|
|
121
|
+
- "**/test_*.py"
|
|
122
|
+
- "**/*_test.py"
|
|
123
|
+
- "**/tests/**"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
version: "1.0"
|
|
2
|
+
name: smart-contract
|
|
3
|
+
description: Smart contract security patterns for Solidity/Vyper
|
|
4
|
+
linked_knowledge: SMART_CONTRACT.md
|
|
5
|
+
|
|
6
|
+
assertions:
|
|
7
|
+
# Critical: Reentrancy
|
|
8
|
+
- id: state-after-external-call
|
|
9
|
+
type: pattern
|
|
10
|
+
pattern: "\\.call\\{[^}]*\\}\\([^)]*\\)[^;]*;[^}]*\\b(balances|_balances|userBalances|amounts)\\s*\\["
|
|
11
|
+
message: "State change after external call - potential reentrancy vulnerability (CEI violation)"
|
|
12
|
+
severity: error
|
|
13
|
+
priority: critical
|
|
14
|
+
languages: [solidity]
|
|
15
|
+
|
|
16
|
+
- id: transfer-without-reentrancy-guard
|
|
17
|
+
type: llm
|
|
18
|
+
compliance: |
|
|
19
|
+
Check for reentrancy vulnerabilities:
|
|
20
|
+
1. External calls (.call, .transfer, .send) should come AFTER state changes (CEI pattern)
|
|
21
|
+
2. Functions making external calls should have reentrancy guards (nonReentrant modifier)
|
|
22
|
+
3. State variables should be updated before external calls, not after
|
|
23
|
+
4. Check-effects-interactions pattern: checks first, then effects (state changes), then interactions (external calls)
|
|
24
|
+
message: "Potential reentrancy vulnerability - apply CEI pattern and/or nonReentrant modifier"
|
|
25
|
+
severity: error
|
|
26
|
+
priority: critical
|
|
27
|
+
model: opus
|
|
28
|
+
languages: [solidity]
|
|
29
|
+
|
|
30
|
+
# Critical: Access control
|
|
31
|
+
- id: missing-access-control
|
|
32
|
+
type: pattern
|
|
33
|
+
pattern: "function\\s+\\w+\\s*\\([^)]*\\)\\s+(external|public)(?![^{]*onlyOwner|[^{]*onlyRole|[^{]*require\\s*\\(\\s*msg\\.sender)"
|
|
34
|
+
message: "Public/external function may need access control"
|
|
35
|
+
severity: warning
|
|
36
|
+
priority: high
|
|
37
|
+
languages: [solidity]
|
|
38
|
+
|
|
39
|
+
- id: tx-origin-auth
|
|
40
|
+
type: pattern
|
|
41
|
+
pattern: "require\\s*\\(\\s*tx\\.origin\\s*==|tx\\.origin\\s*==\\s*msg\\.sender"
|
|
42
|
+
message: "tx.origin for auth is vulnerable to phishing - use msg.sender"
|
|
43
|
+
severity: error
|
|
44
|
+
priority: critical
|
|
45
|
+
languages: [solidity]
|
|
46
|
+
|
|
47
|
+
# High: Integer overflow (pre-0.8.0)
|
|
48
|
+
- id: unchecked-arithmetic
|
|
49
|
+
type: pattern
|
|
50
|
+
pattern: "unchecked\\s*\\{[^}]*[+\\-*/]"
|
|
51
|
+
message: "Unchecked arithmetic - ensure overflow/underflow is intentional and safe"
|
|
52
|
+
severity: warning
|
|
53
|
+
priority: high
|
|
54
|
+
languages: [solidity]
|
|
55
|
+
|
|
56
|
+
# High: Front-running
|
|
57
|
+
- id: block-timestamp-comparison
|
|
58
|
+
type: pattern
|
|
59
|
+
pattern: "block\\.timestamp\\s*[<>=]|[<>=]\\s*block\\.timestamp"
|
|
60
|
+
message: "block.timestamp can be manipulated by miners (~15 sec) - avoid for critical logic"
|
|
61
|
+
severity: warning
|
|
62
|
+
priority: medium
|
|
63
|
+
languages: [solidity]
|
|
64
|
+
|
|
65
|
+
# High: Denial of service
|
|
66
|
+
- id: unbounded-loop
|
|
67
|
+
type: pattern
|
|
68
|
+
pattern: "for\\s*\\([^;]*;[^;]*\\.length\\s*;"
|
|
69
|
+
message: "Loop bounded by dynamic array length - may cause out-of-gas DoS"
|
|
70
|
+
severity: warning
|
|
71
|
+
priority: high
|
|
72
|
+
languages: [solidity]
|
|
73
|
+
|
|
74
|
+
# Medium: Best practices
|
|
75
|
+
- id: missing-zero-address-check
|
|
76
|
+
type: pattern
|
|
77
|
+
pattern: "address\\s+\\w+\\s*=[^;]*;(?![^}]*require\\s*\\([^)]*!=\\s*address\\(0\\))"
|
|
78
|
+
message: "Consider checking for zero address on address parameters"
|
|
79
|
+
severity: info
|
|
80
|
+
priority: medium
|
|
81
|
+
languages: [solidity]
|
|
82
|
+
|
|
83
|
+
- id: hardcoded-gas
|
|
84
|
+
type: pattern
|
|
85
|
+
pattern: "\\.call\\{[^}]*gas:\\s*\\d+[^}]*\\}"
|
|
86
|
+
message: "Hardcoded gas values may break with EVM changes - use gasleft() or remove gas limit"
|
|
87
|
+
severity: warning
|
|
88
|
+
priority: medium
|
|
89
|
+
languages: [solidity]
|
|
90
|
+
|
|
91
|
+
# LLM: Complex security patterns
|
|
92
|
+
- id: smart-contract-security-review
|
|
93
|
+
type: llm
|
|
94
|
+
compliance: |
|
|
95
|
+
Perform a security review of this smart contract code:
|
|
96
|
+
1. Check for reentrancy vulnerabilities (external calls before state changes)
|
|
97
|
+
2. Verify access control on privileged functions
|
|
98
|
+
3. Check for integer overflow/underflow risks
|
|
99
|
+
4. Look for front-running vulnerabilities
|
|
100
|
+
5. Check for DoS vectors (unbounded loops, block gas limit issues)
|
|
101
|
+
6. Verify proper validation of external inputs
|
|
102
|
+
7. Check for unsafe delegatecall usage
|
|
103
|
+
8. Look for flash loan attack vectors
|
|
104
|
+
message: "Smart contract security issue detected"
|
|
105
|
+
severity: error
|
|
106
|
+
priority: critical
|
|
107
|
+
model: opus
|
|
108
|
+
languages: [solidity, vyper]
|
|
109
|
+
applicability:
|
|
110
|
+
glob: "**/*.sol"
|
|
@@ -6,6 +6,7 @@ Supports Sonnet (default) and Opus (for high-stakes assertions).
|
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
+
import sys
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
11
12
|
from crucible.enforcement.budget import (
|
|
@@ -79,8 +80,8 @@ def _load_api_key_from_config() -> str | None:
|
|
|
79
80
|
key = data.get("anthropic_api_key") or data.get("ANTHROPIC_API_KEY")
|
|
80
81
|
if key:
|
|
81
82
|
return key
|
|
82
|
-
except Exception:
|
|
83
|
-
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f"Warning: failed to read {config_path}: {e}", file=sys.stderr)
|
|
84
85
|
|
|
85
86
|
return None
|
|
86
87
|
|
|
@@ -294,27 +295,30 @@ def run_single_assertion(
|
|
|
294
295
|
)
|
|
295
296
|
|
|
296
297
|
except ImportError as e:
|
|
298
|
+
print(f"Warning: LLM assertion '{assertion.id}' skipped: {e}", file=sys.stderr)
|
|
297
299
|
return LLMAssertionResult(
|
|
298
300
|
assertion_id=assertion.id,
|
|
299
|
-
passed=True, #
|
|
301
|
+
passed=True, # Graceful degradation - don't fail on missing dependency
|
|
300
302
|
findings=(),
|
|
301
303
|
tokens_used=0,
|
|
302
304
|
model_used=model_name,
|
|
303
305
|
error=str(e),
|
|
304
306
|
)
|
|
305
307
|
except ValueError as e:
|
|
308
|
+
print(f"Warning: LLM assertion '{assertion.id}' skipped: {e}", file=sys.stderr)
|
|
306
309
|
return LLMAssertionResult(
|
|
307
310
|
assertion_id=assertion.id,
|
|
308
|
-
passed=True, #
|
|
311
|
+
passed=True, # Graceful degradation - don't fail on missing API key
|
|
309
312
|
findings=(),
|
|
310
313
|
tokens_used=0,
|
|
311
314
|
model_used=model_name,
|
|
312
315
|
error=str(e),
|
|
313
316
|
)
|
|
314
317
|
except Exception as e:
|
|
318
|
+
print(f"Warning: LLM assertion '{assertion.id}' failed: {e}", file=sys.stderr)
|
|
315
319
|
return LLMAssertionResult(
|
|
316
320
|
assertion_id=assertion.id,
|
|
317
|
-
passed=True, #
|
|
321
|
+
passed=True, # Graceful degradation - don't fail on API errors
|
|
318
322
|
findings=(),
|
|
319
323
|
tokens_used=0,
|
|
320
324
|
model_used=model_name,
|