@lowwattlabs/clawsec 2.2.1 → 2.3.1
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/api/src/routes.js +54 -13
- package/api/src/server.js +2 -2
- package/cli/clawsec.py +59 -9
- package/lib/skill-verify/verify.sh +4 -3
- package/package.json +2 -3
- package/setup.sh +0 -200
package/api/src/routes.js
CHANGED
|
@@ -27,6 +27,34 @@ function sanitizeSlug(slug) {
|
|
|
27
27
|
return slug;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Strip execute permissions from all files in a directory tree.
|
|
31
|
+
// Security: downloaded skills are READ ONLY during scanning — never executed.
|
|
32
|
+
function hardenSkillDir(dir) {
|
|
33
|
+
try {
|
|
34
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const fullPath = path.join(dir, entry.name);
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
hardenSkillDir(fullPath);
|
|
39
|
+
} else {
|
|
40
|
+
try {
|
|
41
|
+
const mode = fs.statSync(fullPath).mode;
|
|
42
|
+
// Remove all execute bits (user, group, other)
|
|
43
|
+
fs.chmodSync(fullPath, mode & ~0o111);
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Create a restricted temp directory for skill downloads.
|
|
51
|
+
// Returns the path to the temp dir.
|
|
52
|
+
function createScanTempDir() {
|
|
53
|
+
const tmpDir = path.join(os.tmpdir(), 'clawsec-scan-' + uuidv4().slice(0, 8));
|
|
54
|
+
fs.mkdirSync(tmpDir, { recursive: true, mode: 0o700 });
|
|
55
|
+
return tmpDir;
|
|
56
|
+
}
|
|
57
|
+
|
|
30
58
|
router.post('/scan', (req, res) => {
|
|
31
59
|
const { slug, content, path: reqPath } = req.body;
|
|
32
60
|
|
|
@@ -44,42 +72,50 @@ router.post('/scan', (req, res) => {
|
|
|
44
72
|
if (reqPath && fs.existsSync(reqPath)) {
|
|
45
73
|
targetDir = reqPath;
|
|
46
74
|
} else if (slug) {
|
|
47
|
-
//
|
|
75
|
+
// Validate slug before passing to clawhub install
|
|
48
76
|
const safeSlug = sanitizeSlug(slug);
|
|
49
77
|
if (!safeSlug) {
|
|
50
78
|
return res.status(400).json({ error: 'Invalid slug: must be alphanumeric with hyphens/underscores, max 128 chars' });
|
|
51
79
|
}
|
|
52
|
-
//
|
|
53
|
-
const tmpDir =
|
|
80
|
+
// Download from ClawHub into restricted temp directory
|
|
81
|
+
const tmpDir = createScanTempDir();
|
|
54
82
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
83
|
+
// SECURITY: suppress npm postinstall scripts from the downloaded skill
|
|
84
|
+
execFileSync('clawhub', ['install', safeSlug, '--dir', tmpDir, '--no-input'], {
|
|
85
|
+
timeout: 60000,
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
env: { ...process.env, npm_config_ignore_scripts: 'true' }
|
|
57
88
|
});
|
|
58
|
-
// Find the installed skill
|
|
89
|
+
// Find the installed skill directory
|
|
59
90
|
const dirs = fs.readdirSync(tmpDir);
|
|
60
91
|
if (dirs.length > 0) {
|
|
61
92
|
targetDir = path.join(tmpDir, dirs[0]);
|
|
62
|
-
cleanup = true;
|
|
63
93
|
}
|
|
94
|
+
// SECURITY: strip execute permissions from all downloaded files
|
|
95
|
+
if (targetDir) {
|
|
96
|
+
hardenSkillDir(targetDir);
|
|
97
|
+
}
|
|
98
|
+
cleanup = true;
|
|
64
99
|
} catch (e) {
|
|
100
|
+
// Clean up on failure
|
|
101
|
+
try { fs.rmSync(tmpDir, { recursive: true }); } catch {}
|
|
65
102
|
return res.status(404).json({ error: 'Skill not found: ' + slug });
|
|
66
103
|
}
|
|
67
104
|
} else if (content) {
|
|
68
105
|
// Write content to temp dir
|
|
69
|
-
const tmpDir =
|
|
70
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
106
|
+
const tmpDir = createScanTempDir();
|
|
71
107
|
|
|
72
108
|
if (typeof content === 'string') {
|
|
73
109
|
fs.writeFileSync(path.join(tmpDir, 'SKILL.md'), content);
|
|
74
110
|
} else if (typeof content === 'object') {
|
|
75
111
|
// Object with file contents
|
|
76
112
|
for (const [filename, fileContent] of Object.entries(content)) {
|
|
77
|
-
//
|
|
113
|
+
// Sanitize filename against path traversal
|
|
78
114
|
const safeName = path.basename(filename).replace(/\.\./g, '');
|
|
79
115
|
if (safeName !== filename || filename.includes('..')) {
|
|
80
116
|
return res.status(400).json({ error: 'Invalid filename: ' + filename });
|
|
81
117
|
}
|
|
82
|
-
//
|
|
118
|
+
// Ensure resolved path stays within tmpDir
|
|
83
119
|
const filePath = path.resolve(tmpDir, filename);
|
|
84
120
|
if (!filePath.startsWith(path.resolve(tmpDir) + path.sep)) {
|
|
85
121
|
return res.status(400).json({ error: 'Path traversal in filename: ' + filename });
|
|
@@ -136,8 +172,13 @@ router.post('/scan', (req, res) => {
|
|
|
136
172
|
res.json({ report_id: reportId, ...result });
|
|
137
173
|
|
|
138
174
|
} finally {
|
|
139
|
-
if (cleanup && targetDir
|
|
175
|
+
if (cleanup && targetDir) {
|
|
140
176
|
try { fs.rmSync(targetDir, { recursive: true }); } catch {}
|
|
177
|
+
// Also try cleaning parent if it's a scan temp dir
|
|
178
|
+
const parentDir = path.dirname(targetDir);
|
|
179
|
+
if (parentDir.includes('clawsec-scan-')) {
|
|
180
|
+
try { fs.rmSync(parentDir, { recursive: true }); } catch {}
|
|
181
|
+
}
|
|
141
182
|
}
|
|
142
183
|
}
|
|
143
184
|
});
|
|
@@ -181,4 +222,4 @@ router.get('/status', (req, res) => {
|
|
|
181
222
|
res.json(manifest);
|
|
182
223
|
});
|
|
183
224
|
|
|
184
|
-
module.exports = router;
|
|
225
|
+
module.exports = router;
|
package/api/src/server.js
CHANGED
|
@@ -39,7 +39,7 @@ app.use('/api/v1', routes);
|
|
|
39
39
|
|
|
40
40
|
// Health check
|
|
41
41
|
app.get('/health', (req, res) => {
|
|
42
|
-
res.json({ status: 'ok', version: '2.
|
|
42
|
+
res.json({ status: 'ok', version: '2.3.1', uptime: process.uptime() });
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
// Error handler
|
|
@@ -53,6 +53,6 @@ app.use((err, req, res, next) => {
|
|
|
53
53
|
|
|
54
54
|
// Start
|
|
55
55
|
app.listen(PORT, '0.0.0.0', () => {
|
|
56
|
-
console.log("⚡ ClawSec API v2.
|
|
56
|
+
console.log("⚡ ClawSec API v2.3.1 listening on 0.0.0.0:" + PORT);
|
|
57
57
|
fs.mkdirSync(REPORTS_DIR, { recursive: true });
|
|
58
58
|
});
|
package/cli/clawsec.py
CHANGED
|
@@ -15,13 +15,14 @@ import argparse
|
|
|
15
15
|
import json
|
|
16
16
|
import os
|
|
17
17
|
import shutil
|
|
18
|
+
import stat
|
|
18
19
|
import subprocess
|
|
19
20
|
import sys
|
|
20
21
|
import tempfile
|
|
21
22
|
import time
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
|
|
24
|
-
VERSION = "2.
|
|
25
|
+
VERSION = "2.3.1"
|
|
25
26
|
CLAWSEC_DIR = os.environ.get("CLAWSEC_HOME", os.path.expanduser("~/.clawsec"))
|
|
26
27
|
INTEL_DIR = os.environ.get("CLAWSEC_INTEL_DIR", os.path.join(CLAWSEC_DIR, "intel"))
|
|
27
28
|
REPORTS_DIR = os.environ.get("CLAWSEC_REPORTS_DIR", os.path.join(CLAWSEC_DIR, "reports"))
|
|
@@ -40,6 +41,7 @@ BOLD = "\033[1m"
|
|
|
40
41
|
DIM = "\033[2m"
|
|
41
42
|
RESET = "\033[0m"
|
|
42
43
|
|
|
44
|
+
|
|
43
45
|
def banner():
|
|
44
46
|
print(f"""{BOLD}
|
|
45
47
|
╔═════════════════════════════════════════╗
|
|
@@ -48,6 +50,7 @@ def banner():
|
|
|
48
50
|
╚═════════════════════════════════════════╝{RESET}
|
|
49
51
|
""")
|
|
50
52
|
|
|
53
|
+
|
|
51
54
|
def is_slug(target):
|
|
52
55
|
"""Check if target looks like a ClawHub slug (no path separators, no dots, not a local path)."""
|
|
53
56
|
if os.path.exists(target):
|
|
@@ -59,13 +62,45 @@ def is_slug(target):
|
|
|
59
62
|
return False
|
|
60
63
|
return True
|
|
61
64
|
|
|
65
|
+
|
|
66
|
+
def harden_skill_dir(skill_dir):
|
|
67
|
+
"""
|
|
68
|
+
Remove execute permissions from all files in the skill directory.
|
|
69
|
+
This prevents any scripts from being executed accidentally before or during scanning.
|
|
70
|
+
We only need to READ these files, never execute them.
|
|
71
|
+
"""
|
|
72
|
+
for root, dirs, files in os.walk(skill_dir):
|
|
73
|
+
for f in files:
|
|
74
|
+
fpath = os.path.join(root, f)
|
|
75
|
+
try:
|
|
76
|
+
current_mode = os.stat(fpath).st_mode
|
|
77
|
+
# Remove all execute bits (user, group, other)
|
|
78
|
+
os.chmod(fpath, current_mode & ~(stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH))
|
|
79
|
+
except OSError:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
62
83
|
def download_slug(slug):
|
|
63
|
-
"""
|
|
84
|
+
"""
|
|
85
|
+
Download a skill from ClawHub by slug.
|
|
86
|
+
|
|
87
|
+
Security measures:
|
|
88
|
+
- Downloads to a restricted temp directory (0700 permissions)
|
|
89
|
+
- Strips execute permissions from all downloaded files before returning
|
|
90
|
+
- Uses --no-input flag to prevent any interactive prompts
|
|
91
|
+
- Does NOT run any postinstall scripts from the downloaded skill
|
|
92
|
+
|
|
93
|
+
Returns (skill_path, cleanup_dir) or None on failure.
|
|
94
|
+
"""
|
|
95
|
+
# Create temp dir with restricted permissions (owner only)
|
|
64
96
|
tmpdir = tempfile.mkdtemp(prefix="clawsec-scan-")
|
|
97
|
+
os.chmod(tmpdir, stat.S_IRWXU) # 0700 — owner read/write/exec only
|
|
98
|
+
|
|
65
99
|
try:
|
|
66
100
|
result = subprocess.run(
|
|
67
|
-
["clawhub", "install", slug, "--dir", tmpdir],
|
|
68
|
-
capture_output=True, text=True, timeout=120
|
|
101
|
+
["clawhub", "install", slug, "--dir", tmpdir, "--no-input"],
|
|
102
|
+
capture_output=True, text=True, timeout=120,
|
|
103
|
+
env={**os.environ, "npm_config_ignore_scripts": "true"} # Never run skill's postinstall
|
|
69
104
|
)
|
|
70
105
|
if result.returncode != 0:
|
|
71
106
|
print(f"{R}Error:{RESET} Failed to install '{slug}' from ClawHub", file=sys.stderr)
|
|
@@ -73,19 +108,28 @@ def download_slug(slug):
|
|
|
73
108
|
print(f" {DIM}{result.stderr.strip()}{RESET}", file=sys.stderr)
|
|
74
109
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
75
110
|
return None
|
|
111
|
+
|
|
76
112
|
# Find the installed skill directory
|
|
77
113
|
entries = list(Path(tmpdir).iterdir())
|
|
78
114
|
if not entries:
|
|
79
115
|
print(f"{R}Error:{RESET} ClawHub install produced no output for '{slug}'", file=sys.stderr)
|
|
80
116
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
81
117
|
return None
|
|
82
|
-
|
|
118
|
+
|
|
119
|
+
# Determine skill path
|
|
83
120
|
if len(entries) == 1 and entries[0].is_dir():
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
121
|
+
skill_path = str(entries[0])
|
|
122
|
+
else:
|
|
123
|
+
skill_path = tmpdir
|
|
124
|
+
|
|
125
|
+
# SECURITY: Strip execute permissions from ALL downloaded files.
|
|
126
|
+
# We only need to read them for scanning — never execute them.
|
|
127
|
+
harden_skill_dir(skill_path)
|
|
128
|
+
|
|
129
|
+
return skill_path, tmpdir
|
|
130
|
+
|
|
87
131
|
except FileNotFoundError:
|
|
88
|
-
print(f"{R}Error:{RESET} 'clawhub' CLI not found. Install it with: npm install -g
|
|
132
|
+
print(f"{R}Error:{RESET} 'clawhub' CLI not found. Install it with: npm install -g clawhub", file=sys.stderr)
|
|
89
133
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
90
134
|
return None
|
|
91
135
|
except subprocess.TimeoutExpired:
|
|
@@ -93,6 +137,7 @@ def download_slug(slug):
|
|
|
93
137
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
94
138
|
return None
|
|
95
139
|
|
|
140
|
+
|
|
96
141
|
def cmd_scan(args):
|
|
97
142
|
"""Run verification against a skill path or ClawHub slug."""
|
|
98
143
|
target = args.target
|
|
@@ -137,6 +182,7 @@ def cmd_scan(args):
|
|
|
137
182
|
|
|
138
183
|
sys.exit(result.returncode)
|
|
139
184
|
|
|
185
|
+
|
|
140
186
|
def cmd_sync(args):
|
|
141
187
|
"""Run intel sync."""
|
|
142
188
|
# Ensure intel directories exist
|
|
@@ -156,6 +202,7 @@ def cmd_sync(args):
|
|
|
156
202
|
result = subprocess.run(cmd, text=True)
|
|
157
203
|
sys.exit(result.returncode)
|
|
158
204
|
|
|
205
|
+
|
|
159
206
|
def cmd_status(args):
|
|
160
207
|
"""Show cache status."""
|
|
161
208
|
manifest_py = os.path.join(PKG_ROOT, "lib", "intel-sync", "manifest.py")
|
|
@@ -202,6 +249,7 @@ def cmd_status(args):
|
|
|
202
249
|
pass
|
|
203
250
|
print(f" Last full sync: {updated}")
|
|
204
251
|
|
|
252
|
+
|
|
205
253
|
def cmd_report(args):
|
|
206
254
|
"""View a saved report."""
|
|
207
255
|
report_id = args.report_id
|
|
@@ -257,6 +305,7 @@ def cmd_report(args):
|
|
|
257
305
|
|
|
258
306
|
print()
|
|
259
307
|
|
|
308
|
+
|
|
260
309
|
def main():
|
|
261
310
|
parser = argparse.ArgumentParser(
|
|
262
311
|
prog="clawsec",
|
|
@@ -314,5 +363,6 @@ def main():
|
|
|
314
363
|
parser.print_help()
|
|
315
364
|
sys.exit(1)
|
|
316
365
|
|
|
366
|
+
|
|
317
367
|
if __name__ == "__main__":
|
|
318
368
|
main()
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Runs all 7 security checks against a skill, produces JSON report
|
|
4
4
|
set -euo pipefail
|
|
5
5
|
|
|
6
|
-
VERSION="2.
|
|
6
|
+
VERSION="2.3.1"
|
|
7
7
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
8
|
CHECKS_DIR="${SCRIPT_DIR}/checks"
|
|
9
9
|
|
|
@@ -260,7 +260,8 @@ end_time=$(date +%s%N)
|
|
|
260
260
|
elapsed_ms=$(( (end_time - start_time) / 1000000 ))
|
|
261
261
|
|
|
262
262
|
# Generate report via safe temp file approach
|
|
263
|
-
results_tmpfile=$(mktemp
|
|
263
|
+
results_tmpfile=$(mktemp ${TMPDIR:-/tmp}/clawsec-results.XXXXXX.json)
|
|
264
|
+
trap "rm -f $results_tmpfile" EXIT INT TERM
|
|
264
265
|
echo "$check_results" > "$results_tmpfile"
|
|
265
266
|
|
|
266
267
|
report_json=$(python3 -c "
|
|
@@ -282,7 +283,7 @@ if [[ -z "$report_json" ]]; then
|
|
|
282
283
|
--arg verdict "$verdict" \
|
|
283
284
|
--arg path "$skill_path" \
|
|
284
285
|
--argjson duration "$elapsed_ms" \
|
|
285
|
-
'{schema_version:"2.
|
|
286
|
+
'{schema_version:"2.3.0",version:"2.3.0",verdict:$verdict,skill_path:$path,checks:.,scan_duration_ms:$duration}')
|
|
286
287
|
fi
|
|
287
288
|
|
|
288
289
|
verdict=$(echo "$report_json" | jq -r '.verdict')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowwattlabs/clawsec",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "ClawSec - Security Verification for ClawHub Skills",
|
|
5
5
|
"bin": {
|
|
6
6
|
"clawsec": "./bin/clawsec.js",
|
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
"cli/",
|
|
12
12
|
"lib/",
|
|
13
13
|
"api/",
|
|
14
|
-
"requirements.txt"
|
|
15
|
-
"setup.sh"
|
|
14
|
+
"requirements.txt"
|
|
16
15
|
],
|
|
17
16
|
"scripts": {
|
|
18
17
|
"start": "node api/src/server.js",
|
package/setup.sh
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# ⚡ ClawSec v2 Dependency Setup
|
|
3
|
-
# Installs all required tools for intel-sync and skill-verify
|
|
4
|
-
set -euo pipefail
|
|
5
|
-
|
|
6
|
-
VERSION="2.0.0"
|
|
7
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
-
source "${SCRIPT_DIR}/lib/common/config.sh"
|
|
9
|
-
INTEL_DIR="${CLAWSEC_INTEL_DIR}"
|
|
10
|
-
CLAWSEC_USER="$(whoami)"
|
|
11
|
-
|
|
12
|
-
RED='\033[0;31m'
|
|
13
|
-
GREEN='\033[0;32m'
|
|
14
|
-
YELLOW='\033[0;33m'
|
|
15
|
-
BLUE='\033[0;34m'
|
|
16
|
-
BOLD='\033[1m'
|
|
17
|
-
RESET='\033[0m'
|
|
18
|
-
|
|
19
|
-
log_info() { echo -e "${BLUE}[INFO]${RESET} $*"; }
|
|
20
|
-
log_ok() { echo -e "${GREEN}[ OK ]${RESET} $*"; }
|
|
21
|
-
log_warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
|
|
22
|
-
log_err() { echo -e "${RED}[ERR ]${RESET} $*"; }
|
|
23
|
-
|
|
24
|
-
banner() {
|
|
25
|
-
echo -e "${BOLD}"
|
|
26
|
-
echo " ╔═══════════════════════════════════════╗"
|
|
27
|
-
echo " ║ ClawSec v${VERSION} Setup ║"
|
|
28
|
-
echo " ║ ⚡ Security Verification for Skills ║"
|
|
29
|
-
echo " ╚═══════════════════════════════════════╝"
|
|
30
|
-
echo -e "${RESET}"
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
check_cmd() {
|
|
34
|
-
if command -v "$1" &>/dev/null; then
|
|
35
|
-
log_ok "$1 already installed: $(command -v "$1")"
|
|
36
|
-
return 0
|
|
37
|
-
else
|
|
38
|
-
return 1
|
|
39
|
-
fi
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
install_system_deps() {
|
|
43
|
-
log_info "Installing system dependencies..."
|
|
44
|
-
local needed=()
|
|
45
|
-
for pkg in curl wget git jq python3 python3-pip python3-venv libyara-dev yara; do
|
|
46
|
-
if ! dpkg -l "$pkg" &>/dev/null 2>&1; then
|
|
47
|
-
needed+=("$pkg")
|
|
48
|
-
fi
|
|
49
|
-
done
|
|
50
|
-
|
|
51
|
-
if [[ ${#needed[@]} -gt 0 ]]; then
|
|
52
|
-
sudo apt-get update -qq
|
|
53
|
-
sudo apt-get install -y -qq "${needed[@]}"
|
|
54
|
-
log_ok "System packages installed: ${needed[*]}"
|
|
55
|
-
else
|
|
56
|
-
log_ok "All system packages already installed"
|
|
57
|
-
fi
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
install_semgrep() {
|
|
61
|
-
if check_cmd semgrep; then return 0; fi
|
|
62
|
-
log_info "Installing Semgrep..."
|
|
63
|
-
pip3 install --user semgrep 2>/dev/null || pip install --user semgrep 2>/dev/null
|
|
64
|
-
export PATH="$HOME/.local/bin:$PATH"
|
|
65
|
-
if check_cmd semgrep; then
|
|
66
|
-
log_ok "Semgrep installed"
|
|
67
|
-
else
|
|
68
|
-
log_warn "Semgrep pip install failed, trying direct binary..."
|
|
69
|
-
curl -fsSL https://raw.githubusercontent.com/returntocorp/semgrep/main/install.sh | bash
|
|
70
|
-
log_ok "Semgrep installed via script"
|
|
71
|
-
fi
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
install_gitleaks() {
|
|
75
|
-
if check_cmd gitleaks; then return 0; fi
|
|
76
|
-
log_info "Installing Gitleaks..."
|
|
77
|
-
local arch="$(uname -m)"
|
|
78
|
-
local gitleaks_arch="x64"
|
|
79
|
-
[[ "$arch" == "aarch64" ]] && gitleaks_arch="arm64"
|
|
80
|
-
|
|
81
|
-
local latest
|
|
82
|
-
latest=$(curl -fsSL https://api.github.com/repos/gitleaks/gitleaks/releases/latest | jq -r '.tag_name')
|
|
83
|
-
local url="https://github.com/gitleaks/gitleaks/releases/download/${latest}/gitleaks_${latest:1}_linux_${gitleaks_arch}.tar.gz"
|
|
84
|
-
|
|
85
|
-
local tmpdir
|
|
86
|
-
tmpdir=$(mktemp -d)
|
|
87
|
-
curl -fsSL "$url" | tar -xz -C "$tmpdir"
|
|
88
|
-
mkdir -p "$HOME/.local/bin"
|
|
89
|
-
mv "$tmpdir/gitleaks" "$HOME/.local/bin/gitleaks"
|
|
90
|
-
chmod +x "$HOME/.local/bin/gitleaks"
|
|
91
|
-
rm -rf "$tmpdir"
|
|
92
|
-
log_ok "Gitleaks ${latest} installed"
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
install_yara_python() {
|
|
96
|
-
log_info "Checking yara-python..."
|
|
97
|
-
if python3 -c "import yara" 2>/dev/null; then
|
|
98
|
-
log_ok "yara-python already available"
|
|
99
|
-
return 0
|
|
100
|
-
fi
|
|
101
|
-
pip3 install --user yara-python 2>/dev/null || pip install --user yara-python 2>/dev/null
|
|
102
|
-
if python3 -c "import yara" 2>/dev/null; then
|
|
103
|
-
log_ok "yara-python installed"
|
|
104
|
-
else
|
|
105
|
-
log_warn "yara-python install failed — YARA scans may not work"
|
|
106
|
-
fi
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
setup_dirs() {
|
|
110
|
-
log_info "Setting up directory structure at ${CLAWSEC_HOME}..."
|
|
111
|
-
mkdir -p "${INTEL_DIR}"/{cisa-kev,osv,epss,malwarebazaar,urlhaus,threatfox,feodo,yara-rules,semgrep-rules}
|
|
112
|
-
mkdir -p "${CLAWSEC_HOME}/reports"
|
|
113
|
-
mkdir -p "${CLAWSEC_HOME}/venv"
|
|
114
|
-
log_ok "Directory structure ready at ${CLAWSEC_HOME}"
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
clone_rule_repos() {
|
|
118
|
-
log_info "Cloning/pulling rule repos..."
|
|
119
|
-
|
|
120
|
-
# YARA rules - Neo23x0/signature-base
|
|
121
|
-
local yara_dir="${INTEL_DIR}/yara-rules/repo"
|
|
122
|
-
if [[ -d "$yara_dir/.git" ]]; then
|
|
123
|
-
git -C "$yara_dir" pull --quiet 2>/dev/null && log_ok "YARA rules updated" || log_warn "YARA rules pull failed"
|
|
124
|
-
else
|
|
125
|
-
rm -rf "$yara_dir"
|
|
126
|
-
git clone --depth 1 https://github.com/Neo23x0/signature-base.git "$yara_dir" 2>/dev/null && log_ok "YARA rules cloned" || log_warn "YARA rules clone failed"
|
|
127
|
-
fi
|
|
128
|
-
|
|
129
|
-
# Semgrep rules
|
|
130
|
-
local semgrep_dir="${INTEL_DIR}/semgrep-rules/repo"
|
|
131
|
-
if [[ -d "$semgrep_dir/.git" ]]; then
|
|
132
|
-
git -C "$semgrep_dir" pull --quiet 2>/dev/null && log_ok "Semgrep rules updated" || log_warn "Semgrep rules pull failed"
|
|
133
|
-
else
|
|
134
|
-
rm -rf "$semgrep_dir"
|
|
135
|
-
git clone --depth 1 https://github.com/returntocorp/semgrep-rules.git "$semgrep_dir" 2>/dev/null && log_ok "Semgrep rules cloned" || log_warn "Semgrep rules clone failed"
|
|
136
|
-
fi
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
setup_python_env() {
|
|
140
|
-
log_info "Setting up Python virtual environment..."
|
|
141
|
-
local venv_dir="${CLAWSEC_HOME}/venv"
|
|
142
|
-
if [[ ! -d "$venv_dir" ]] || [[ ! -f "$venv_dir/bin/python3" ]]; then
|
|
143
|
-
python3 -m venv "$venv_dir"
|
|
144
|
-
fi
|
|
145
|
-
source "$venv_dir/bin/activate"
|
|
146
|
-
pip install --quiet --upgrade pip
|
|
147
|
-
if [[ -f "${SCRIPT_DIR}/requirements.txt" ]]; then
|
|
148
|
-
pip install --quiet -r "${SCRIPT_DIR}/requirements.txt"
|
|
149
|
-
fi
|
|
150
|
-
deactivate
|
|
151
|
-
log_ok "Python venv ready at $venv_dir"
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
verify_install() {
|
|
155
|
-
echo ""
|
|
156
|
-
log_info "Verifying installations..."
|
|
157
|
-
echo ""
|
|
158
|
-
local all_ok=true
|
|
159
|
-
|
|
160
|
-
for cmd in python3 jq curl git; do
|
|
161
|
-
if check_cmd "$cmd"; then :; else
|
|
162
|
-
log_err "$cmd NOT found"
|
|
163
|
-
all_ok=false
|
|
164
|
-
fi
|
|
165
|
-
done
|
|
166
|
-
|
|
167
|
-
for cmd in semgrep gitleaks yara; do
|
|
168
|
-
if check_cmd "$cmd"; then :; else
|
|
169
|
-
log_warn "$cmd NOT found — some checks will be unavailable"
|
|
170
|
-
fi
|
|
171
|
-
done
|
|
172
|
-
|
|
173
|
-
echo ""
|
|
174
|
-
if $all_ok; then
|
|
175
|
-
log_ok "Core dependencies verified"
|
|
176
|
-
else
|
|
177
|
-
log_err "Some core dependencies missing — review above"
|
|
178
|
-
fi
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
main() {
|
|
182
|
-
banner
|
|
183
|
-
|
|
184
|
-
export PATH="$HOME/.local/bin:$PATH"
|
|
185
|
-
|
|
186
|
-
install_system_deps
|
|
187
|
-
install_semgrep
|
|
188
|
-
install_gitleaks
|
|
189
|
-
install_yara_python
|
|
190
|
-
setup_dirs
|
|
191
|
-
clone_rule_repos
|
|
192
|
-
setup_python_env
|
|
193
|
-
verify_install
|
|
194
|
-
|
|
195
|
-
echo ""
|
|
196
|
-
log_ok "Setup complete. Run: clawsec scan <path> (to verify a skill)"
|
|
197
|
-
log_ok " clawsec sync (to populate intel cache)"
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
main "$@"
|