@lowwattlabs/clawsec 2.1.0 → 2.2.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 +72 -22
  2. package/package.json +1 -1
package/cli/clawsec.py CHANGED
@@ -14,12 +14,14 @@ Usage:
14
14
  import argparse
15
15
  import json
16
16
  import os
17
+ import shutil
17
18
  import subprocess
18
19
  import sys
20
+ import tempfile
19
21
  import time
20
22
  from pathlib import Path
21
23
 
22
- VERSION = "2.1.0"
24
+ VERSION = "2.2.0"
23
25
  CLAWSEC_DIR = os.environ.get("CLAWSEC_HOME", os.path.expanduser("~/.clawsec"))
24
26
  INTEL_DIR = os.environ.get("CLAWSEC_INTEL_DIR", os.path.join(CLAWSEC_DIR, "intel"))
25
27
  REPORTS_DIR = os.environ.get("CLAWSEC_REPORTS_DIR", os.path.join(CLAWSEC_DIR, "reports"))
@@ -46,29 +48,70 @@ def banner():
46
48
  ╚═════════════════════════════════════════╝{RESET}
47
49
  """)
48
50
 
51
+ def is_slug(target):
52
+ """Check if target looks like a ClawHub slug (no path separators, no dots, not a local path)."""
53
+ if os.path.exists(target):
54
+ return False
55
+ if "/" in target or target.startswith("."):
56
+ return False
57
+ # Slugs are alphanumeric with hyphens, no extensions
58
+ if target.startswith("-") or target.endswith("-"):
59
+ return False
60
+ return True
61
+
62
+ def download_slug(slug):
63
+ """Download a skill from ClawHub by slug. Returns the path or None."""
64
+ tmpdir = tempfile.mkdtemp(prefix="clawsec-scan-")
65
+ try:
66
+ result = subprocess.run(
67
+ ["clawhub", "install", slug, "--dir", tmpdir],
68
+ capture_output=True, text=True, timeout=120
69
+ )
70
+ if result.returncode != 0:
71
+ print(f"{R}Error:{RESET} Failed to install '{slug}' from ClawHub", file=sys.stderr)
72
+ if result.stderr:
73
+ print(f" {DIM}{result.stderr.strip()}{RESET}", file=sys.stderr)
74
+ shutil.rmtree(tmpdir, ignore_errors=True)
75
+ return None
76
+ # Find the installed skill directory
77
+ entries = list(Path(tmpdir).iterdir())
78
+ if not entries:
79
+ print(f"{R}Error:{RESET} ClawHub install produced no output for '{slug}'", file=sys.stderr)
80
+ shutil.rmtree(tmpdir, ignore_errors=True)
81
+ return None
82
+ # If only one directory, use it directly
83
+ 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
87
+ except FileNotFoundError:
88
+ print(f"{R}Error:{RESET} 'clawhub' CLI not found. Install it with: npm install -g @anthropic/clawhub", file=sys.stderr)
89
+ shutil.rmtree(tmpdir, ignore_errors=True)
90
+ return None
91
+ except subprocess.TimeoutExpired:
92
+ print(f"{R}Error:{RESET} ClawHub install timed out for '{slug}'", file=sys.stderr)
93
+ shutil.rmtree(tmpdir, ignore_errors=True)
94
+ return None
95
+
49
96
  def cmd_scan(args):
50
- """Run verification against a skill path."""
97
+ """Run verification against a skill path or ClawHub slug."""
51
98
  target = args.target
52
99
  json_mode = args.json
53
-
54
- # If it's a slug (no path separator and doesn't exist locally),
55
- # try to download from ClawHub
56
- if not os.path.exists(target) and "/" not in target and not target.startswith("."):
57
- try:
58
- result = subprocess.run(
59
- ["clawhub", "install", target, "--dir", "/tmp/clawsec-scan-temp"],
60
- capture_output=True, text=True, timeout=60
61
- )
62
- if result.returncode == 0:
63
- for d in Path("/tmp/clawsec-scan-temp").iterdir():
64
- if d.is_dir():
65
- target = str(d)
66
- break
67
- except Exception:
68
- pass
100
+ cleanup_dir = None
101
+
102
+ # If it looks like a slug, try to download from ClawHub
103
+ if is_slug(target):
104
+ if not json_mode:
105
+ print(f" {C}⚡ Downloading '{target}' from ClawHub...{RESET}")
106
+ result = download_slug(target)
107
+ if result is None:
108
+ sys.exit(2)
109
+ target, cleanup_dir = result
110
+ if not json_mode:
111
+ print(f" {G}✓{RESET} Downloaded to {target}")
69
112
 
70
113
  if not os.path.exists(target):
71
- print(f"{R}Error:{RESET} {target} not found", file=sys.stderr)
114
+ print(f"{R}Error:{RESET} '{args.target}' not found (not a local path and not a valid ClawHub slug)", file=sys.stderr)
72
115
  sys.exit(2)
73
116
 
74
117
  # Run verify.sh — resolve relative to package root
@@ -87,6 +130,11 @@ def cmd_scan(args):
87
130
  result = subprocess.run(cmd, capture_output=json_mode, text=True)
88
131
  if json_mode:
89
132
  print(result.stdout)
133
+
134
+ # Clean up downloaded skill
135
+ if cleanup_dir:
136
+ shutil.rmtree(cleanup_dir, ignore_errors=True)
137
+
90
138
  sys.exit(result.returncode)
91
139
 
92
140
  def cmd_sync(args):
@@ -96,6 +144,7 @@ def cmd_sync(args):
96
144
  for d in intel_dirs:
97
145
  os.makedirs(os.path.join(INTEL_DIR, d), exist_ok=True)
98
146
  os.makedirs(REPORTS_DIR, exist_ok=True)
147
+
99
148
  sync_sh = os.path.join(PKG_ROOT, "lib", "intel-sync", "sync.sh")
100
149
  if not os.path.exists(sync_sh):
101
150
  sync_sh = os.path.join(CLAWSEC_DIR, "lib", "intel-sync", "sync.sh")
@@ -214,7 +263,8 @@ def main():
214
263
  description="⚡ ClawSec v2 — Security Verification for ClawHub Skills",
215
264
  formatter_class=argparse.RawDescriptionHelpFormatter,
216
265
  epilog="""Examples:
217
- clawsec scan ./my-skill Verify a local skill
266
+ clawsec scan ./my-skill Verify a local skill directory
267
+ clawsec scan weather-forecast Download and scan from ClawHub
218
268
  clawsec scan ./my-skill --json Machine-readable output
219
269
  clawsec sync Refresh all intel sources
220
270
  clawsec sync cisa-kev epss Sync specific sources
@@ -226,8 +276,8 @@ def main():
226
276
  subparsers = parser.add_subparsers(dest="command", help="Available commands")
227
277
 
228
278
  # scan
229
- scan_parser = subparsers.add_parser("scan", help="Verify a skill")
230
- scan_parser.add_argument("target", help="Skill path or ClawHub slug")
279
+ scan_parser = subparsers.add_parser("scan", help="Verify a skill (local path or ClawHub slug)")
280
+ scan_parser.add_argument("target", help="Skill path or ClawHub slug (e.g. 'weather-forecast')")
231
281
  scan_parser.add_argument("--checks", help="Comma-separated list of checks to run")
232
282
  scan_parser.add_argument("--json", action="store_true", help="JSON output")
233
283
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowwattlabs/clawsec",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "ClawSec - Security Verification for ClawHub Skills",
5
5
  "bin": {
6
6
  "clawsec": "./bin/clawsec.js",