@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Low Watt Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# ⚡ ClawSec
|
|
2
|
+
|
|
3
|
+
Security verification tool that scans ClawHub skills against 10 continuously-updated threat intelligence sources using 7 autonomous security checks.
|
|
4
|
+
|
|
5
|
+
If you find it useful, [buy me a coffee](https://buymeacoffee.com/lowwattlabs) ⚡
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Install globally via npm
|
|
11
|
+
npm install -g @lowwattlabs/clawsec
|
|
12
|
+
|
|
13
|
+
# Scan a local skill directory
|
|
14
|
+
clawsec scan ./my-skill
|
|
15
|
+
|
|
16
|
+
# Scan with JSON output
|
|
17
|
+
clawsec scan ./my-skill --json
|
|
18
|
+
|
|
19
|
+
# Sync threat intel
|
|
20
|
+
clawsec sync
|
|
21
|
+
|
|
22
|
+
# Check intel cache status
|
|
23
|
+
clawsec status
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
First run automatically sets up a Python venv at `~/.clawsec/venv/` and installs dependencies.
|
|
27
|
+
|
|
28
|
+
## Docker
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Build
|
|
32
|
+
docker build -t lowwattlabs/clawsec .
|
|
33
|
+
|
|
34
|
+
# Run
|
|
35
|
+
docker run -p 3100:3100 lowwattlabs/clawsec
|
|
36
|
+
|
|
37
|
+
# Scan via API inside container
|
|
38
|
+
docker run lowwattlabs/clawsec clawsec scan /path/to/skill
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## CLI Usage
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Verify a skill
|
|
45
|
+
clawsec scan ./my-skill
|
|
46
|
+
clawsec scan ./my-skill --json # JSON output
|
|
47
|
+
clawsec scan ./my-skill --checks=dep-scan,secret-scan # Run specific checks
|
|
48
|
+
|
|
49
|
+
# Scan by ClawHub slug
|
|
50
|
+
clawsec scan my-awesome-skill
|
|
51
|
+
|
|
52
|
+
# Refresh intel cache
|
|
53
|
+
clawsec sync # All sources
|
|
54
|
+
clawsec sync cisa-kev epss # Specific sources
|
|
55
|
+
|
|
56
|
+
# Check cache status
|
|
57
|
+
clawsec status
|
|
58
|
+
clawsec status --json # JSON output
|
|
59
|
+
|
|
60
|
+
# View saved report
|
|
61
|
+
clawsec report abc12345
|
|
62
|
+
clawsec report abc12345 --json # JSON output
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Exit codes:** 0 = pass, 1 = warn, 2 = fail
|
|
66
|
+
|
|
67
|
+
## API Usage
|
|
68
|
+
|
|
69
|
+
### Start the API server
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Via npm global install
|
|
73
|
+
clawsec-api
|
|
74
|
+
|
|
75
|
+
# Or directly
|
|
76
|
+
node api/src/server.js
|
|
77
|
+
|
|
78
|
+
# With custom config
|
|
79
|
+
CLAWSEC_HOME=~/.clawsec CLAWSEC_PORT=3100 clawsec-api
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### API endpoints
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Scan by path
|
|
86
|
+
curl -X POST http://localhost:3100/api/v1/scan \
|
|
87
|
+
-H "Content-Type: application/json" \
|
|
88
|
+
-d '{"path": "/path/to/skill"}'
|
|
89
|
+
|
|
90
|
+
# Scan by ClawHub slug
|
|
91
|
+
curl -X POST http://localhost:3100/api/v1/scan \
|
|
92
|
+
-H "Content-Type: application/json" \
|
|
93
|
+
-d '{"slug": "my-skill"}'
|
|
94
|
+
|
|
95
|
+
# Scan by content
|
|
96
|
+
curl -X POST http://localhost:3100/api/v1/scan \
|
|
97
|
+
-H "Content-Type: application/json" \
|
|
98
|
+
-d '{"content": {"SKILL.md": "# My Skill\n..."}}'
|
|
99
|
+
|
|
100
|
+
# Get report
|
|
101
|
+
curl http://localhost:3100/api/v1/report/{id}
|
|
102
|
+
|
|
103
|
+
# Get trust badge
|
|
104
|
+
curl http://localhost:3100/api/v1/badge/{id}.svg
|
|
105
|
+
|
|
106
|
+
# Cache status
|
|
107
|
+
curl http://localhost:3100/api/v1/status
|
|
108
|
+
|
|
109
|
+
# Health check
|
|
110
|
+
curl http://localhost:3100/health
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Security Checks (7)
|
|
114
|
+
|
|
115
|
+
1. **Dependency Scan** — Matches declared dependencies against OSV, flags CISA KEV as critical, ranks by EPSS probability
|
|
116
|
+
2. **Static Analysis** — Semgrep with community rules for code vulnerabilities
|
|
117
|
+
3. **Secret Scan** — Gitleaks for leaked API keys, tokens, credentials
|
|
118
|
+
4. **YARA Scan** — Neo23x0 signature-base rules for malware/packer/suspicious patterns
|
|
119
|
+
5. **IOC Match** — Extracts URLs/IPs/domains/hashes, matches against URLhaus/ThreatFox/Feodo/MalwareBazaar
|
|
120
|
+
6. **Behavioral Heuristics** — Flags shell injection, system writes, fetch-exec, large base64 payloads, capability overreach
|
|
121
|
+
7. **Prompt Injection** — Detects instruction overrides, role manipulation, safety bypasses in SKILL.md
|
|
122
|
+
|
|
123
|
+
## Intel Sources (9)
|
|
124
|
+
|
|
125
|
+
| Source | Type | Records | Update Frequency |
|
|
126
|
+
|--------|------|---------|-----------------|
|
|
127
|
+
| CISA KEV | Known Exploited Vulnerabilities | ~1,600 | Daily |
|
|
128
|
+
| OSV (npm + PyPI) | Open Source Vulnerabilities | ~219,000 | Daily |
|
|
129
|
+
| EPSS | Exploit Prediction Scoring | ~334,000 | Daily |
|
|
130
|
+
| MalwareBazaar | Malware hashes + samples | ~3,500 | Daily |
|
|
131
|
+
| URLhaus | Malicious URLs | ~15,400 | Daily |
|
|
132
|
+
| ThreatFox | IOCs (IPs, domains, hashes) | ~3,200 | Daily |
|
|
133
|
+
| Feodo Tracker | C2 IP addresses | ~6 | Daily |
|
|
134
|
+
| YARA Rules (Neo23x0) | Malware/signature detection | ~746 rules | Daily |
|
|
135
|
+
| Semgrep Rules | Static analysis rules | ~2,183 rules | Daily |
|
|
136
|
+
|
|
137
|
+
All data cached under `~/.clawsec/intel/` with atomic writes and graceful degradation on failure.
|
|
138
|
+
|
|
139
|
+
## Configuration
|
|
140
|
+
|
|
141
|
+
ClawSec uses environment variables with sensible defaults:
|
|
142
|
+
|
|
143
|
+
| Variable | Default | Description |
|
|
144
|
+
|----------|---------|-------------|
|
|
145
|
+
| `CLAWSEC_HOME` | `~/.clawsec` | Root directory for venv, intel, reports |
|
|
146
|
+
| `CLAWSEC_INTEL_DIR` | `~/.clawsec/intel` | Intel cache directory |
|
|
147
|
+
| `CLAWSEC_REPORTS_DIR` | `~/.clawsec/reports` | Reports directory |
|
|
148
|
+
| `CLAWSEC_PORT` | `3100` | API server port |
|
|
149
|
+
|
|
150
|
+
## Local Development
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
git clone https://github.com/jchandler187/clawsec.git
|
|
154
|
+
cd clawsec
|
|
155
|
+
./setup.sh # Installs system deps + Python venv
|
|
156
|
+
clawsec sync # Populate intel cache (first run takes a few minutes for OSV)
|
|
157
|
+
clawsec scan ./path # Verify a skill
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Architecture
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
clawsec/
|
|
164
|
+
├── bin/
|
|
165
|
+
│ ├── clawsec.js (CLI entry point)
|
|
166
|
+
│ ├── clawsec-api.js (API entry point)
|
|
167
|
+
│ └── setup-venv.js (postinstall venv setup)
|
|
168
|
+
├── api/
|
|
169
|
+
│ └── src/
|
|
170
|
+
│ ├── server.js (Express server, port 3100)
|
|
171
|
+
│ ├── routes.js (API routes)
|
|
172
|
+
│ ├── middleware.js (Rate limiting + auth)
|
|
173
|
+
│ ├── badge.js (SVG trust badge generator)
|
|
174
|
+
│ └── verify-wrapper.sh
|
|
175
|
+
├── cli/
|
|
176
|
+
│ └── clawsec.py (Python CLI)
|
|
177
|
+
├── lib/
|
|
178
|
+
│ ├── intel-sync/ (Intel cache synchronization)
|
|
179
|
+
│ │ ├── sync.sh (Main sync orchestrator)
|
|
180
|
+
│ │ ├── sources/ (Per-source sync scripts)
|
|
181
|
+
│ │ └── manifest.py (Cache manifest management)
|
|
182
|
+
│ ├── skill-verify/ (Skill verification engine)
|
|
183
|
+
│ │ ├── verify.sh (Main verify orchestrator)
|
|
184
|
+
│ │ ├── checks/ (7 security check scripts)
|
|
185
|
+
│ │ └── report.py (Report generation & storage)
|
|
186
|
+
│ └── common/ (Shared utilities + config)
|
|
187
|
+
├── Dockerfile
|
|
188
|
+
├── package.json
|
|
189
|
+
├── requirements.txt
|
|
190
|
+
├── setup.sh
|
|
191
|
+
└── README.md
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## What ClawSec DOES Check
|
|
195
|
+
|
|
196
|
+
- **Known vulnerabilities in declared dependencies** (OSV + CISA KEV + EPSS)
|
|
197
|
+
- **Static code patterns** — shell injection, eval, exec, path traversal, system writes (Semgrep + behavioral heuristics)
|
|
198
|
+
- **Leaked secrets and credentials** — API keys, tokens, passwords (Gitleaks)
|
|
199
|
+
- **Known malware signatures** — YARA rules matching packers, ransomware, suspicious binaries
|
|
200
|
+
- **Threat intel IOC matches** — URLs, IPs, domains, and hashes from URLhaus, ThreatFox, Feodo, MalwareBazaar
|
|
201
|
+
- **Prompt injection attempts** — instruction overrides, role manipulation, jailbreak patterns in SKILL.md
|
|
202
|
+
- **Large encoded payloads** — suspicious base64 blobs above 2KB that decode to binary content
|
|
203
|
+
|
|
204
|
+
## What ClawSec Does NOT Check
|
|
205
|
+
|
|
206
|
+
- String concatenation obfuscation
|
|
207
|
+
- Lazy-loaded payloads (partially caught via eval/evalSub)
|
|
208
|
+
- Conditional/time-bomb behavior
|
|
209
|
+
- Dependency confusion/typo squatting
|
|
210
|
+
- Runtime behavior (requires dynamic analysis)
|
|
211
|
+
- Transitive vulnerabilities
|
|
212
|
+
- Novel/zero-day threats not in any feed
|
|
213
|
+
|
|
214
|
+
## Intel Cache Staleness
|
|
215
|
+
|
|
216
|
+
- **30+ days stale** → Warning (results may be outdated)
|
|
217
|
+
- **90+ days stale** → Critical failure (results unreliable, resync required)
|
|
218
|
+
|
|
219
|
+
Run `clawsec sync` to refresh stale intel sources.
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT — Low Watt Labs ⚡
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>ClawSec — Skill Security Verification</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
11
|
+
background: #0d1117; color: #c9d1d9; line-height: 1.6;
|
|
12
|
+
}
|
|
13
|
+
.container { max-width: 800px; margin: 0 auto; padding: 0 24px; }
|
|
14
|
+
header { padding: 48px 0 32px; text-align: center; }
|
|
15
|
+
h1 { font-size: 2.4em; color: #f0f6fc; margin-bottom: 8px; }
|
|
16
|
+
h1 span { color: #2ea043; }
|
|
17
|
+
.tagline { color: #8b949e; font-size: 1.1em; }
|
|
18
|
+
.badges { display: flex; gap: 12px; justify-content: center; margin: 24px 0; flex-wrap: wrap; }
|
|
19
|
+
.badge { background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 12px 20px; }
|
|
20
|
+
.badge-label { color: #8b949e; font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
21
|
+
.badge-value { color: #f0f6fc; font-weight: 600; font-size: 1.2em; }
|
|
22
|
+
.checks { padding: 32px 0; }
|
|
23
|
+
.check { display: flex; align-items: baseline; padding: 8px 0; border-bottom: 1px solid #21262d; }
|
|
24
|
+
.check-icon { color: #2ea043; font-weight: 700; margin-right: 12px; font-size: 1.1em; }
|
|
25
|
+
.check-name { color: #f0f6fc; font-weight: 600; min-width: 180px; }
|
|
26
|
+
.check-desc { color: #8b949e; }
|
|
27
|
+
.cta { text-align: center; padding: 40px 0; }
|
|
28
|
+
.cta a {
|
|
29
|
+
display: inline-block; background: #2ea043; color: #fff; padding: 14px 32px;
|
|
30
|
+
border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 1.1em;
|
|
31
|
+
}
|
|
32
|
+
.cta a:hover { background: #3fb950; }
|
|
33
|
+
.coffee { margin-top: 16px; }
|
|
34
|
+
.coffee a { color: #d4a72c; text-decoration: none; font-size: 0.95em; }
|
|
35
|
+
.coffee a:hover { text-decoration: underline; }
|
|
36
|
+
.footer { text-align: center; padding: 32px 0; color: #484f58; font-size: 0.85em; }
|
|
37
|
+
</style>
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
<div class="container">
|
|
41
|
+
<header>
|
|
42
|
+
<h1>⚡ Claw<span>Sec</span></h1>
|
|
43
|
+
<p class="tagline">Security verification for ClawHub skills</p>
|
|
44
|
+
</header>
|
|
45
|
+
|
|
46
|
+
<div class="badges">
|
|
47
|
+
<div class="badge">
|
|
48
|
+
<div class="badge-label">Checks</div>
|
|
49
|
+
<div class="badge-value">7</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="badge">
|
|
52
|
+
<div class="badge-label">Intel Sources</div>
|
|
53
|
+
<div class="badge-value">9</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="badge">
|
|
56
|
+
<div class="badge-label">License</div>
|
|
57
|
+
<div class="badge-value">MIT</div>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="badge">
|
|
60
|
+
<div class="badge-label">Cost</div>
|
|
61
|
+
<div class="badge-value">Free</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="checks">
|
|
66
|
+
<div class="check"><span class="check-icon">✓</span><span class="check-name">Dependency Scan</span><span class="check-desc">OSV + CISA KEV + EPSS — known vulnerabilities in declared deps</span></div>
|
|
67
|
+
<div class="check"><span class="check-icon">✓</span><span class="check-name">Static Analysis</span><span class="check-desc">Semgrep community rules — shell injection, path traversal, system writes</span></div>
|
|
68
|
+
<div class="check"><span class="check-icon">✓</span><span class="check-name">Secret Scan</span><span class="check-desc">Gitleaks + credential heuristics — leaked keys, tokens, passwords</span></div>
|
|
69
|
+
<div class="check"><span class="check-icon">✓</span><span class="check-name">YARA Scan</span><span class="check-desc">Neo23x0 signature-base — malware, packers, suspicious binaries</span></div>
|
|
70
|
+
<div class="check"><span class="check-icon">✓</span><span class="check-name">IOC Match</span><span class="check-desc">URLhaus + ThreatFox + Feodo + MalwareBazaar — malicious URLs, IPs, hashes</span></div>
|
|
71
|
+
<div class="check"><span class="check-icon">✓</span><span class="check-name">Behavioral</span><span class="check-desc">Shell injection, eval, fetch-exec, large base64, capability overreach</span></div>
|
|
72
|
+
<div class="check"><span class="check-icon">✓</span><span class="check-name">Prompt Injection</span><span class="check-desc">Instruction overrides, role manipulation, safety bypasses in SKILL.md</span></div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="cta">
|
|
76
|
+
<a href="https://github.com/jchandler187/clawsec">Get Started →</a>
|
|
77
|
+
<div class="coffee">
|
|
78
|
+
<a href="https://buymeacoffee.com/lowwattlabs">☕ Found ClawSec useful? Buy me a coffee</a>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="footer">
|
|
83
|
+
⚡ Low Watt Labs — MIT License
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</body>
|
|
87
|
+
</html>
|
package/api/src/badge.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawSec v2 - SVG Badge Generator
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function generateBadge(verdict) {
|
|
6
|
+
const configs = {
|
|
7
|
+
pass: {
|
|
8
|
+
label: 'clawsec',
|
|
9
|
+
value: 'verified',
|
|
10
|
+
labelColor: '#333',
|
|
11
|
+
valueColor: '#2ea043',
|
|
12
|
+
valueBorderColor: '#2ea04366'
|
|
13
|
+
},
|
|
14
|
+
warn: {
|
|
15
|
+
label: 'clawsec',
|
|
16
|
+
value: 'warnings',
|
|
17
|
+
labelColor: '#333',
|
|
18
|
+
valueColor: '#d29922',
|
|
19
|
+
valueBorderColor: '#d2992266'
|
|
20
|
+
},
|
|
21
|
+
fail: {
|
|
22
|
+
label: 'clawsec',
|
|
23
|
+
value: 'failed',
|
|
24
|
+
labelColor: '#333',
|
|
25
|
+
valueColor: '#da3633',
|
|
26
|
+
valueBorderColor: '#da363366'
|
|
27
|
+
},
|
|
28
|
+
unknown: {
|
|
29
|
+
label: 'clawsec',
|
|
30
|
+
value: 'unknown',
|
|
31
|
+
labelColor: '#333',
|
|
32
|
+
valueColor: '#6e7781',
|
|
33
|
+
valueBorderColor: '#6e778166'
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const config = configs[verdict] || configs.unknown;
|
|
38
|
+
const labelWidth = 62;
|
|
39
|
+
const valueWidth = config.value.length * 7.5 + 16;
|
|
40
|
+
const totalWidth = labelWidth + valueWidth;
|
|
41
|
+
const height = 20;
|
|
42
|
+
const rx = 3;
|
|
43
|
+
|
|
44
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${height}" viewBox="0 0 ${totalWidth} ${height}">
|
|
45
|
+
<clipPath id="round">
|
|
46
|
+
<rect width="${totalWidth}" height="${height}" rx="${rx}" fill="#fff"/>
|
|
47
|
+
</clipPath>
|
|
48
|
+
<g clip-path="url(#round)">
|
|
49
|
+
<rect width="${labelWidth}" height="${height}" fill="${config.labelColor}"/>
|
|
50
|
+
<rect x="${labelWidth}" width="${valueWidth}" height="${height}" fill="${config.valueColor}"/>
|
|
51
|
+
</g>
|
|
52
|
+
<rect width="${totalWidth}" height="${height}" rx="${rx}" fill="none" stroke="${config.valueBorderColor}" stroke-width="0.5"/>
|
|
53
|
+
<g fill="#fff" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif" font-size="11" font-weight="600">
|
|
54
|
+
<text x="${labelWidth / 2}" y="14.5" text-anchor="middle">${config.label}</text>
|
|
55
|
+
<text x="${labelWidth + valueWidth / 2}" y="14.5" text-anchor="middle">${config.value}</text>
|
|
56
|
+
</g>
|
|
57
|
+
</svg>`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { generateBadge };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚡ ClawSec v2 - API Middleware
|
|
3
|
+
* Rate limiting, auth, request logging
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { RateLimiterMemory } = require('rate-limiter-flexible');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
|
|
11
|
+
const CLAWSEC_DIR = process.env.CLAWSEC_HOME || path.join(os.homedir(), '.clawsec');
|
|
12
|
+
|
|
13
|
+
// Rate limiter: 5 scans/day for free tier, much higher for API key holders
|
|
14
|
+
const freeLimiter = new RateLimiterMemory({
|
|
15
|
+
points: 5,
|
|
16
|
+
duration: 86400, // 24 hours
|
|
17
|
+
blockDuration: 86400,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const proLimiter = new RateLimiterMemory({
|
|
21
|
+
points: 1000,
|
|
22
|
+
duration: 86400,
|
|
23
|
+
blockDuration: 3600,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Load API keys from config
|
|
27
|
+
function loadApiKeys() {
|
|
28
|
+
const keysFile = path.join(CLAWSEC_DIR, 'api', 'api-keys.json');
|
|
29
|
+
if (fs.existsSync(keysFile)) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(fs.readFileSync(keysFile, 'utf8'));
|
|
32
|
+
} catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Rate limiter middleware
|
|
40
|
+
const rateLimiter = async (req, res, next) => {
|
|
41
|
+
const apiKey = req.headers['x-api-key'] || (req.headers['authorization'] || '').replace('Bearer ', '');
|
|
42
|
+
const keys = loadApiKeys();
|
|
43
|
+
|
|
44
|
+
if (apiKey && keys[apiKey]) {
|
|
45
|
+
// Pro user
|
|
46
|
+
try {
|
|
47
|
+
await proLimiter.consume(apiKey);
|
|
48
|
+
req.userTier = 'pro';
|
|
49
|
+
req.userId = keys[apiKey].email || apiKey;
|
|
50
|
+
} catch {
|
|
51
|
+
return res.status(429).json({
|
|
52
|
+
error: 'Rate limit exceeded',
|
|
53
|
+
tier: 'pro',
|
|
54
|
+
retry_after: '1 hour'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
// Free tier - rate limit by IP
|
|
59
|
+
const clientIp = req.ip || req.connection.remoteAddress;
|
|
60
|
+
try {
|
|
61
|
+
await freeLimiter.consume(clientIp);
|
|
62
|
+
req.userTier = 'free';
|
|
63
|
+
} catch {
|
|
64
|
+
return res.status(429).json({
|
|
65
|
+
error: 'Rate limit exceeded',
|
|
66
|
+
tier: 'free',
|
|
67
|
+
limit: '5 scans/day',
|
|
68
|
+
upgrade: 'Get an API key for higher limits'
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
next();
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// API key auth (optional - works without, just gets free tier)
|
|
76
|
+
const apiKeyAuth = (req, res, next) => {
|
|
77
|
+
const apiKey = req.headers['x-api-key'] || (req.headers['authorization'] || '').replace('Bearer ', '');
|
|
78
|
+
if (apiKey) {
|
|
79
|
+
const keys = loadApiKeys();
|
|
80
|
+
if (keys[apiKey]) {
|
|
81
|
+
req.authenticated = true;
|
|
82
|
+
req.userTier = keys[apiKey].tier || 'pro';
|
|
83
|
+
req.userId = keys[apiKey].email || 'unknown';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
next();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Request logger
|
|
90
|
+
const requestLogger = (req, res, next) => {
|
|
91
|
+
const start = Date.now();
|
|
92
|
+
res.on('finish', () => {
|
|
93
|
+
const duration = Date.now() - start;
|
|
94
|
+
const tier = req.userTier || 'free';
|
|
95
|
+
console.log('[' + new Date().toISOString() + '] ' + req.method + ' ' + req.path + ' ' + res.statusCode + ' ' + duration + 'ms [' + tier + ']');
|
|
96
|
+
});
|
|
97
|
+
next();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
rateLimiter,
|
|
102
|
+
apiKeyAuth,
|
|
103
|
+
requestLogger
|
|
104
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚡ ClawSec v2 - API Routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const express = require('express');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const { execFileSync } = require('child_process');
|
|
10
|
+
const { v4: uuidv4 } = require('uuid');
|
|
11
|
+
const { generateBadge } = require('./badge');
|
|
12
|
+
|
|
13
|
+
const router = express.Router();
|
|
14
|
+
const CLAWSEC_DIR = process.env.CLAWSEC_HOME || path.join(os.homedir(), '.clawsec');
|
|
15
|
+
const REPORTS_DIR = process.env.CLAWSEC_REPORTS_DIR || path.join(CLAWSEC_DIR, 'reports');
|
|
16
|
+
const INTEL_DIR = process.env.CLAWSEC_INTEL_DIR || path.join(CLAWSEC_DIR, 'intel');
|
|
17
|
+
|
|
18
|
+
// Validate id param: alphanumeric + hyphens only, max 128 chars
|
|
19
|
+
function sanitizeId(id) {
|
|
20
|
+
if (!id || !/^[a-zA-Z0-9_-]+$/.test(id) || id.length > 128) return null;
|
|
21
|
+
return id;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Validate slug for clawhub install: same rules as id
|
|
25
|
+
function sanitizeSlug(slug) {
|
|
26
|
+
if (!slug || !/^[a-zA-Z0-9_-]+$/.test(slug) || slug.length > 128) return null;
|
|
27
|
+
return slug;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
router.post('/scan', (req, res) => {
|
|
31
|
+
const { slug, content, path: reqPath } = req.body;
|
|
32
|
+
|
|
33
|
+
if (!slug && !content && !reqPath) {
|
|
34
|
+
return res.status(400).json({
|
|
35
|
+
error: 'Must provide slug, content, or path',
|
|
36
|
+
required: ['slug | content | path']
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let targetDir;
|
|
41
|
+
let cleanup = false;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (reqPath && fs.existsSync(reqPath)) {
|
|
45
|
+
targetDir = reqPath;
|
|
46
|
+
} else if (slug) {
|
|
47
|
+
// P1-4: Validate slug before passing to clawhub install
|
|
48
|
+
const safeSlug = sanitizeSlug(slug);
|
|
49
|
+
if (!safeSlug) {
|
|
50
|
+
return res.status(400).json({ error: 'Invalid slug: must be alphanumeric with hyphens/underscores, max 128 chars' });
|
|
51
|
+
}
|
|
52
|
+
// Try to install from ClawHub
|
|
53
|
+
const tmpDir = '/tmp/clawsec-scan-' + uuidv4().slice(0, 8);
|
|
54
|
+
try {
|
|
55
|
+
execFileSync('clawhub', ['install', safeSlug, '--dir', tmpDir], {
|
|
56
|
+
timeout: 60000, encoding: 'utf8'
|
|
57
|
+
});
|
|
58
|
+
// Find the installed skill
|
|
59
|
+
const dirs = fs.readdirSync(tmpDir);
|
|
60
|
+
if (dirs.length > 0) {
|
|
61
|
+
targetDir = path.join(tmpDir, dirs[0]);
|
|
62
|
+
cleanup = true;
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return res.status(404).json({ error: 'Skill not found: ' + slug });
|
|
66
|
+
}
|
|
67
|
+
} else if (content) {
|
|
68
|
+
// Write content to temp dir
|
|
69
|
+
const tmpDir = '/tmp/clawsec-scan-' + uuidv4().slice(0, 8);
|
|
70
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
71
|
+
|
|
72
|
+
if (typeof content === 'string') {
|
|
73
|
+
fs.writeFileSync(path.join(tmpDir, 'SKILL.md'), content);
|
|
74
|
+
} else if (typeof content === 'object') {
|
|
75
|
+
// Object with file contents
|
|
76
|
+
for (const [filename, fileContent] of Object.entries(content)) {
|
|
77
|
+
// P0: Sanitize filename against path traversal
|
|
78
|
+
const safeName = path.basename(filename).replace(/\.\./g, '');
|
|
79
|
+
if (safeName !== filename || filename.includes('..')) {
|
|
80
|
+
return res.status(400).json({ error: 'Invalid filename: ' + filename });
|
|
81
|
+
}
|
|
82
|
+
// P0: Ensure resolved path stays within tmpDir
|
|
83
|
+
const filePath = path.resolve(tmpDir, filename);
|
|
84
|
+
if (!filePath.startsWith(path.resolve(tmpDir) + path.sep)) {
|
|
85
|
+
return res.status(400).json({ error: 'Path traversal in filename: ' + filename });
|
|
86
|
+
}
|
|
87
|
+
const dirPath = path.dirname(filePath);
|
|
88
|
+
if (!fs.existsSync(dirPath)) {
|
|
89
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
fs.writeFileSync(filePath, fileContent);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
targetDir = tmpDir;
|
|
95
|
+
cleanup = true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!targetDir) {
|
|
99
|
+
return res.status(400).json({ error: 'Could not resolve skill target' });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Run verification
|
|
103
|
+
const verifyWrapper = path.join(CLAWSEC_DIR, 'api', 'src', 'verify-wrapper.sh');
|
|
104
|
+
let result;
|
|
105
|
+
try {
|
|
106
|
+
const output = execFileSync('bash', [verifyWrapper, targetDir], {
|
|
107
|
+
timeout: 30000,
|
|
108
|
+
encoding: 'utf8',
|
|
109
|
+
env: { ...process.env, PATH: process.env.HOME + '/.local/bin:' + process.env.PATH }
|
|
110
|
+
});
|
|
111
|
+
result = JSON.parse(output.trim());
|
|
112
|
+
} catch (e) {
|
|
113
|
+
// Non-zero exit (warn=1, fail=2) still gives stdout via e.stdout
|
|
114
|
+
const output = e.stdout || e.stderr || '';
|
|
115
|
+
try {
|
|
116
|
+
result = JSON.parse(output.trim());
|
|
117
|
+
} catch(e2) {
|
|
118
|
+
result = {
|
|
119
|
+
verdict: 'error',
|
|
120
|
+
error: 'Verification failed',
|
|
121
|
+
details: e.message,
|
|
122
|
+
stdout_preview: (e.stdout || '').substring(0, 200)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Save report
|
|
128
|
+
const reportId = result.report_id || uuidv4().slice(0, 8);
|
|
129
|
+
const reportPath = path.join(REPORTS_DIR, reportId + '.json');
|
|
130
|
+
fs.writeFileSync(reportPath, JSON.stringify(result, null, 2));
|
|
131
|
+
|
|
132
|
+
// Add scan URL to response
|
|
133
|
+
result.report_url = '/api/v1/report/' + reportId;
|
|
134
|
+
result.badge_url = '/api/v1/badge/' + reportId + '.svg';
|
|
135
|
+
|
|
136
|
+
res.json({ report_id: reportId, ...result });
|
|
137
|
+
|
|
138
|
+
} finally {
|
|
139
|
+
if (cleanup && targetDir && targetDir.startsWith('/tmp/clawsec-scan-')) {
|
|
140
|
+
try { fs.rmSync(targetDir, { recursive: true }); } catch {}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// GET /api/v1/report/:id - Retrieve a saved report
|
|
146
|
+
router.get('/report/:id', (req, res) => {
|
|
147
|
+
const id = sanitizeId(req.params.id);
|
|
148
|
+
if (!id) return res.status(403).json({ error: 'invalid id' });
|
|
149
|
+
const reportPath = path.join(REPORTS_DIR, id + '.json');
|
|
150
|
+
const resolved = path.resolve(reportPath);
|
|
151
|
+
if (!resolved.startsWith(REPORTS_DIR + path.sep)) return res.status(403).json({ error: 'invalid id' });
|
|
152
|
+
if (!fs.existsSync(resolved)) {
|
|
153
|
+
return res.status(404).json({ error: 'Report not found' });
|
|
154
|
+
}
|
|
155
|
+
const report = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
156
|
+
res.json(report);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// GET /api/v1/badge/:id.svg - Trust badge
|
|
160
|
+
router.get('/badge/:id.svg', (req, res) => {
|
|
161
|
+
const rawId = req.params.id.replace('.svg', '');
|
|
162
|
+
const id = sanitizeId(rawId);
|
|
163
|
+
if (!id) return res.type('svg').status(403).send(generateBadge('unknown'));
|
|
164
|
+
const reportPath = path.join(REPORTS_DIR, id + '.json');
|
|
165
|
+
const resolved = path.resolve(reportPath);
|
|
166
|
+
if (!resolved.startsWith(REPORTS_DIR + path.sep)) return res.type('svg').status(403).send(generateBadge('unknown'));
|
|
167
|
+
if (!fs.existsSync(resolved)) {
|
|
168
|
+
return res.type('svg').status(404).send(generateBadge('unknown'));
|
|
169
|
+
}
|
|
170
|
+
const report = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
171
|
+
res.type('svg').send(generateBadge(report.verdict));
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// GET /api/v1/status - Intel cache status
|
|
175
|
+
router.get('/status', (req, res) => {
|
|
176
|
+
const manifestPath = path.join(INTEL_DIR, 'manifest.json');
|
|
177
|
+
if (!fs.existsSync(manifestPath)) {
|
|
178
|
+
return res.json({ sources: [], updated_at: null });
|
|
179
|
+
}
|
|
180
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
181
|
+
res.json(manifest);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
module.exports = router;
|