@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
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>
@@ -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;