@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 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
- // P1-4: Validate slug before passing to clawhub install
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
- // Try to install from ClawHub
53
- const tmpDir = '/tmp/clawsec-scan-' + uuidv4().slice(0, 8);
80
+ // Download from ClawHub into restricted temp directory
81
+ const tmpDir = createScanTempDir();
54
82
  try {
55
- execFileSync('clawhub', ['install', safeSlug, '--dir', tmpDir], {
56
- timeout: 60000, encoding: 'utf8'
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 = '/tmp/clawsec-scan-' + uuidv4().slice(0, 8);
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
- // P0: Sanitize filename against path traversal
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
- // P0: Ensure resolved path stays within tmpDir
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 && targetDir.startsWith('/tmp/clawsec-scan-')) {
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.0.0', uptime: process.uptime() });
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.0.0 listening on 0.0.0.0:" + PORT);
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.0"
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 @anthropic/clawhub", file=sys.stderr)
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.0.0"
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 /tmp/clawsec-results.XXXXXX.json)
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.0.0",version:"2.0.0",verdict:$verdict,skill_path:$path,checks:.,scan_duration_ms:$duration}')
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.0",
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 "$@"