@lowwattlabs/clawsec 2.0.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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/api/public/index.html +87 -0
  4. package/api/src/badge.js +60 -0
  5. package/api/src/middleware.js +104 -0
  6. package/api/src/routes.js +184 -0
  7. package/api/src/server.js +58 -0
  8. package/api/src/verify-wrapper.sh +16 -0
  9. package/bin/clawsec-api.js +19 -0
  10. package/bin/clawsec.js +99 -0
  11. package/bin/setup-venv.js +35 -0
  12. package/cli/clawsec.py +263 -0
  13. package/lib/common/__init__.py +2 -0
  14. package/lib/common/colors.sh +17 -0
  15. package/lib/common/config.py +12 -0
  16. package/lib/common/config.sh +8 -0
  17. package/lib/common/log.sh +24 -0
  18. package/lib/common/utils.sh +69 -0
  19. package/lib/intel-sync/manifest.py +103 -0
  20. package/lib/intel-sync/sources/cisa-kev.sh +24 -0
  21. package/lib/intel-sync/sources/epss.sh +34 -0
  22. package/lib/intel-sync/sources/feodo.sh +27 -0
  23. package/lib/intel-sync/sources/malwarebazaar.sh +22 -0
  24. package/lib/intel-sync/sources/osv.sh +101 -0
  25. package/lib/intel-sync/sources/semgrep-rules.sh +28 -0
  26. package/lib/intel-sync/sources/threatfox.sh +28 -0
  27. package/lib/intel-sync/sources/urlhaus.sh +42 -0
  28. package/lib/intel-sync/sources/yara-rules.sh +38 -0
  29. package/lib/intel-sync/sync.sh +96 -0
  30. package/lib/skill-verify/checks/behavioral.py +252 -0
  31. package/lib/skill-verify/checks/dep-scan.py +456 -0
  32. package/lib/skill-verify/checks/ioc-match.py +382 -0
  33. package/lib/skill-verify/checks/prompt-inject.py +158 -0
  34. package/lib/skill-verify/checks/secret-scan.sh +61 -0
  35. package/lib/skill-verify/checks/static-analysis.sh +73 -0
  36. package/lib/skill-verify/checks/yara-scan.sh +73 -0
  37. package/lib/skill-verify/report.py +119 -0
  38. package/lib/skill-verify/verify.sh +326 -0
  39. package/package.json +42 -0
  40. package/requirements.txt +6 -0
  41. package/setup.sh +200 -0
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env python3
2
+ # ⚡ Low Watt Labs
3
+ """ClawSec v2 - Behavioral Heuristics
4
+
5
+ Flags dangerous behavior patterns in skill code:
6
+ - Shell execution without sanitization
7
+ - Writes to system paths
8
+ - Fetch + execute remote code
9
+ - Large base64-encoded payloads
10
+ - Capability overreach (requests more than declared)
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import re
16
+ import base64
17
+ import sys
18
+ import unicodedata
19
+ from pathlib import Path
20
+
21
+ DANGEROUS_SHELL_PATTERNS = [
22
+ (r'os\.system\s*\(', "os.system() call — unsanitized shell execution"),
23
+ (r'subprocess\.(call|run|Popen|check_output|check_call)\s*\([^)]*shell\s*=\s*True',
24
+ "subprocess with shell=True — injection risk"),
25
+ (r'exec\s*\(', "exec() call — dynamic code execution"),
26
+ (r'eval\s*\(', "eval() call — dynamic code evaluation"),
27
+ (r'child_process\.exec\b', "Node.js child_process.exec — shell injection risk"),
28
+ (r'\.execSync\s*\(', "Node.js execSync — shell injection risk"),
29
+ (r'\.execFile\s*\([^)]*shell\s*:\s*true', "Node.js execFile with shell:true"),
30
+ ]
31
+
32
+ SYSTEM_WRITE_PATTERNS = [
33
+ (r'open\s*\([^)]*[\'"]/(etc|usr|var|tmp|boot|srv)/', "Write to system path"),
34
+ (r'with\s+open\s*\([^)]*[\'"]/(etc|usr|var|tmp|boot|srv)/', "Write to system path"),
35
+ (r'fs\.(writeFileSync|appendFileSync|writeFile|appendFile)\s*\([^)]*[\'"]/(etc|usr|var|tmp|boot|srv)/',
36
+ "Node.js write to system path"),
37
+ (r'mkdir\s*\([^)]*[\'"]/(etc|usr|var|srv)/', "mkdir on system path"),
38
+ (r'os\.makedirs\s*\([^)]*[\'"]/(etc|usr|var|srv)/', "os.makedirs on system path"),
39
+ ]
40
+
41
+ PATH_TRAVERSAL_PATTERNS = [
42
+ (r'\.\.\/+(etc|var|usr|home|root|srv|tmp|boot)\b',
43
+ "Path traversal attempt: resolves outside workspace"),
44
+ (r'\.\.\\+(etc|var|usr|home|root|srv|tmp|boot)\b',
45
+ "Path traversal attempt (backslash): resolves outside workspace"),
46
+ (r'\.\./\.\./',
47
+ "Nested path traversal: relative path escapes workspace"),
48
+ (r'\.\.\\\\',
49
+ "Nested path traversal (backslash): relative path escapes workspace"),
50
+ ]
51
+
52
+ PATH_ABSOLUTE_FORBIDDEN = [
53
+ (r'["\']/(etc|var|usr(?!/local))/',
54
+ "Absolute system path access outside workspace"),
55
+ (r'["\']/root/',
56
+ "Absolute path to /root — should not access root home"),
57
+ (r'["\']/home/(?!openclaw/)',
58
+ "Absolute path to another user's home directory"),
59
+ ]
60
+
61
+ FETCH_EXEC_PATTERNS = [
62
+ (r'(requests\.get|urllib\.request\.urlopen|fetch|axios|http\.get)\s*.*\n.*exec\s*\(',
63
+ "fetch-then-exec pattern — remote code execution risk"),
64
+ (r'(requests\.get|urllib\.request\.urlopen|fetch|axios)\s*.*\n.*eval\s*\(',
65
+ "fetch-then-eval pattern — remote code execution risk"),
66
+ (r'curl\s.*\|\s*(ba)?sh', "curl pipe to shell — classic RCE pattern"),
67
+ (r'wget\s.*\|\s*(ba)?sh', "wget pipe to shell — classic RCE pattern"),
68
+ ]
69
+
70
+ CAPABILITY_PATTERNS = {
71
+ "network": [r'requests\.', r'socket\.', r'http\.', r'fetch\s*\(', r'axios\.'],
72
+ "filesystem": [r'open\s*\(', r'os\.path\.', r'os\.makedirs', r'fs\.', r'readFileSync', r'writeFileSync'],
73
+ "shell": [r'os\.system', r'subprocess\.', r'child_process', r'exec\s*\(', r'execSync'],
74
+ "environment": [r'os\.environ', r'process\.env'],
75
+ }
76
+
77
+ def find_base64_payloads(content, threshold=2048):
78
+ """Find suspiciously large base64 strings."""
79
+ findings = []
80
+ # Match base64-looking strings
81
+ b64_pattern = re.compile(r'[A-Za-z0-9+/]{%d,}={0,2}' % threshold)
82
+ for match in b64_pattern.finditer(content):
83
+ b64_str = match.group()
84
+ try:
85
+ decoded = base64.b64decode(b64_str)
86
+ # Check if it's binary-ish (non-printable content)
87
+ non_printable = sum(1 for b in decoded if b < 32 and b not in (9, 10, 13))
88
+ if non_printable > len(decoded) * 0.1:
89
+ # ELF binaries embedded as base64 are critical — they're trojan horses
90
+ is_elf = decoded[:4] == b'ELF' or decoded[:4] == b'ELF'
91
+ severity = "critical" if is_elf else "high"
92
+ desc = f"Embedded ELF binary ({len(decoded)} bytes)" if is_elf else f"Large base64 payload ({len(decoded)} bytes decoded, appears binary)"
93
+ findings.append({
94
+ "type": "embedded_elf_binary" if is_elf else "large_base64_payload",
95
+ "size_bytes": len(decoded),
96
+ "encoded_size": len(b64_str),
97
+ "severity": severity,
98
+ "description": desc
99
+ })
100
+ except Exception:
101
+ pass
102
+ return findings
103
+
104
+ def check_capabilities_declared(skill_path):
105
+ """Check if skill's declared capabilities match actual behavior."""
106
+ skill_md = Path(skill_path) / "SKILL.md"
107
+ declared = set()
108
+ if skill_md.exists():
109
+ content = skill_md.read_text(errors='ignore')
110
+ # Look for capability declarations
111
+ cap_section = re.search(r'##\s*(?:Capabilities|Permissions|Requires)\s*\n(.*?)(?=\n##|\Z)',
112
+ content, re.DOTALL | re.IGNORECASE)
113
+ if cap_section:
114
+ for cap in CAPABILITY_PATTERNS:
115
+ if re.search(r'\b' + cap + r'\b', cap_section.group(1), re.IGNORECASE):
116
+ declared.add(cap)
117
+
118
+ return declared
119
+
120
+ def check_behavioral(skill_path):
121
+ """Main behavioral heuristics check."""
122
+ results = {
123
+ "check": "behavioral_heuristics",
124
+ "status": "pass",
125
+ "findings": [],
126
+ "errors": []
127
+ }
128
+
129
+ skill_path = Path(skill_path)
130
+ all_code = ""
131
+ code_files = []
132
+
133
+ for fpath in skill_path.rglob("*"):
134
+ if fpath.is_dir() or fpath.suffix in ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.zip'):
135
+ continue
136
+ try:
137
+ content = fpath.read_text(errors='ignore')
138
+ content = unicodedata.normalize('NFKC', content) # P0-6: normalize homoglyphs
139
+ all_code += content + "\n"
140
+ code_files.append((fpath, content))
141
+ except Exception:
142
+ continue
143
+
144
+ if not all_code:
145
+ results["note"] = "No readable code files found"
146
+ return results
147
+
148
+ # 1. Shell execution without sanitization
149
+ for pattern, desc in DANGEROUS_SHELL_PATTERNS:
150
+ for match in re.finditer(pattern, all_code):
151
+ results["findings"].append({
152
+ "type": "dangerous_shell",
153
+ "pattern": pattern,
154
+ "description": desc,
155
+ "severity": "high"
156
+ })
157
+ break # one finding per pattern
158
+
159
+ # 2. System path writes
160
+ for pattern, desc in SYSTEM_WRITE_PATTERNS:
161
+ for match in re.finditer(pattern, all_code):
162
+ results["findings"].append({
163
+ "type": "system_write",
164
+ "pattern": pattern,
165
+ "description": desc,
166
+ "severity": "high"
167
+ })
168
+ break
169
+
170
+ # 3. Fetch + exec
171
+ for pattern, desc in FETCH_EXEC_PATTERNS:
172
+ if re.search(pattern, all_code, re.DOTALL):
173
+ results["findings"].append({
174
+ "type": "fetch_exec",
175
+ "pattern": pattern,
176
+ "description": desc,
177
+ "severity": "critical"
178
+ })
179
+
180
+ # 4. Path traversal detection (P0-5)
181
+ normalized_code = all_code.replace('\\' + '\\', '/') # normalize backslash paths
182
+ for pattern, desc in PATH_TRAVERSAL_PATTERNS:
183
+ if re.search(pattern, all_code):
184
+ results["findings"].append({
185
+ "type": "path_traversal",
186
+ "pattern": pattern,
187
+ "description": desc,
188
+ "severity": "critical"
189
+ })
190
+ for pattern, desc in PATH_ABSOLUTE_FORBIDDEN:
191
+ if re.search(pattern, all_code):
192
+ results["findings"].append({
193
+ "type": "path_traversal",
194
+ "pattern": pattern,
195
+ "description": desc,
196
+ "severity": "critical"
197
+ })
198
+
199
+ # 5. Large base64 payloads
200
+ b64_findings = find_base64_payloads(all_code)
201
+ results["findings"].extend(b64_findings)
202
+
203
+ # 6. Capability overreach
204
+ declared = check_capabilities_declared(skill_path)
205
+ if declared:
206
+ for cap, patterns in CAPABILITY_PATTERNS.items():
207
+ if cap not in declared:
208
+ for pattern in patterns:
209
+ if re.search(pattern, all_code):
210
+ results["findings"].append({
211
+ "type": "capability_overreach",
212
+ "capability": cap,
213
+ "description": f"Uses {cap} capabilities but doesn't declare them",
214
+ "severity": "medium"
215
+ })
216
+ break
217
+
218
+ # P0-7: Severity escalation — certain categories MUST be critical
219
+ ESCALATE_TO_CRITICAL = {"command_injection", "shell_injection", "path_traversal", "hardcoded_secret", "secret_in_code"}
220
+ SHELL_INJ_KEYWORDS = ["os.system", "shell=True", "execSync", "child_process.exec", "os.system()", "subprocess.*shell"]
221
+ for f in results["findings"]:
222
+ ftype = f.get("type", "")
223
+ category = f.get("category", "")
224
+ pattern = f.get("pattern", "")
225
+ desc = f.get("description", "")
226
+ if ftype in ESCALATE_TO_CRITICAL or category in ESCALATE_TO_CRITICAL:
227
+ f["severity"] = "critical"
228
+ # Also escalate dangerous_shell/shell injection findings to critical
229
+ if ftype == "dangerous_shell":
230
+ f["severity"] = "critical"
231
+ # Escalate any pattern containing shell injection keywords
232
+ for kw in SHELL_INJ_KEYWORDS:
233
+ if kw in pattern or kw in desc:
234
+ f["severity"] = "critical"
235
+ break
236
+
237
+ # Determine status
238
+ if any(f["severity"] == "critical" for f in results["findings"]):
239
+ results["status"] = "fail"
240
+ elif any(f["severity"] == "high" for f in results["findings"]):
241
+ results["status"] = "warn"
242
+ elif results["findings"]:
243
+ results["status"] = "warn"
244
+
245
+ return results
246
+
247
+ if __name__ == "__main__":
248
+ if len(sys.argv) < 2:
249
+ print("Usage: behavioral.py <skill_path>")
250
+ sys.exit(1)
251
+ result = check_behavioral(sys.argv[1])
252
+ print(json.dumps(result, indent=2))