@lowwattlabs/clawsec 2.2.1 → 2.3.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 (2) hide show
  1. package/cli/clawsec.py +58 -8
  2. package/package.json +1 -1
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.2.0"
25
+ VERSION = "2.3.0"
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
- """Download a skill from ClawHub by slug. Returns the path or None."""
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,17 +108,26 @@ 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
- # If only one directory, use it directly
118
+
119
+ # Determine skill path
83
120
  if len(entries) == 1 and entries[0].is_dir():
84
- return str(entries[0]), tmpdir
85
- # Multiple entries — use the tmpdir itself
86
- return tmpdir, tmpdir
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
132
  print(f"{R}Error:{RESET} 'clawhub' CLI not found. Install it with: npm install -g @anthropic/clawhub", file=sys.stderr)
89
133
  shutil.rmtree(tmpdir, ignore_errors=True)
@@ -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()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowwattlabs/clawsec",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "ClawSec - Security Verification for ClawHub Skills",
5
5
  "bin": {
6
6
  "clawsec": "./bin/clawsec.js",