@lowwattlabs/clawsec 2.1.0 → 2.2.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/cli/clawsec.py +72 -22
- 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.
|
|
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
|
-
|
|
55
|
-
# try to download from ClawHub
|
|
56
|
-
if
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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}", flush=True)
|
|
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}", flush=True)
|
|
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
|
|