@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.
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/api/public/index.html +87 -0
- package/api/src/badge.js +60 -0
- package/api/src/middleware.js +104 -0
- package/api/src/routes.js +184 -0
- package/api/src/server.js +58 -0
- package/api/src/verify-wrapper.sh +16 -0
- package/bin/clawsec-api.js +19 -0
- package/bin/clawsec.js +99 -0
- package/bin/setup-venv.js +35 -0
- package/cli/clawsec.py +263 -0
- package/lib/common/__init__.py +2 -0
- package/lib/common/colors.sh +17 -0
- package/lib/common/config.py +12 -0
- package/lib/common/config.sh +8 -0
- package/lib/common/log.sh +24 -0
- package/lib/common/utils.sh +69 -0
- package/lib/intel-sync/manifest.py +103 -0
- package/lib/intel-sync/sources/cisa-kev.sh +24 -0
- package/lib/intel-sync/sources/epss.sh +34 -0
- package/lib/intel-sync/sources/feodo.sh +27 -0
- package/lib/intel-sync/sources/malwarebazaar.sh +22 -0
- package/lib/intel-sync/sources/osv.sh +101 -0
- package/lib/intel-sync/sources/semgrep-rules.sh +28 -0
- package/lib/intel-sync/sources/threatfox.sh +28 -0
- package/lib/intel-sync/sources/urlhaus.sh +42 -0
- package/lib/intel-sync/sources/yara-rules.sh +38 -0
- package/lib/intel-sync/sync.sh +96 -0
- package/lib/skill-verify/checks/behavioral.py +252 -0
- package/lib/skill-verify/checks/dep-scan.py +456 -0
- package/lib/skill-verify/checks/ioc-match.py +382 -0
- package/lib/skill-verify/checks/prompt-inject.py +158 -0
- package/lib/skill-verify/checks/secret-scan.sh +61 -0
- package/lib/skill-verify/checks/static-analysis.sh +73 -0
- package/lib/skill-verify/checks/yara-scan.sh +73 -0
- package/lib/skill-verify/report.py +119 -0
- package/lib/skill-verify/verify.sh +326 -0
- package/package.json +42 -0
- package/requirements.txt +6 -0
- 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))
|