@lowwattlabs/clawsec 2.3.0 → 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 +2 -2
- 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
|
@@ -22,7 +22,7 @@ import tempfile
|
|
|
22
22
|
import time
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
|
|
25
|
-
VERSION = "2.3.
|
|
25
|
+
VERSION = "2.3.1"
|
|
26
26
|
CLAWSEC_DIR = os.environ.get("CLAWSEC_HOME", os.path.expanduser("~/.clawsec"))
|
|
27
27
|
INTEL_DIR = os.environ.get("CLAWSEC_INTEL_DIR", os.path.join(CLAWSEC_DIR, "intel"))
|
|
28
28
|
REPORTS_DIR = os.environ.get("CLAWSEC_REPORTS_DIR", os.path.join(CLAWSEC_DIR, "reports"))
|
|
@@ -129,7 +129,7 @@ def download_slug(slug):
|
|
|
129
129
|
return skill_path, tmpdir
|
|
130
130
|
|
|
131
131
|
except FileNotFoundError:
|
|
132
|
-
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)
|
|
133
133
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
134
134
|
return None
|
|
135
135
|
except subprocess.TimeoutExpired:
|
|
@@ -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.
|
|
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 "$@"
|