@lowwattlabs/clawsec 2.0.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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/api/public/index.html +87 -0
  4. package/api/src/badge.js +60 -0
  5. package/api/src/middleware.js +104 -0
  6. package/api/src/routes.js +184 -0
  7. package/api/src/server.js +58 -0
  8. package/api/src/verify-wrapper.sh +16 -0
  9. package/bin/clawsec-api.js +19 -0
  10. package/bin/clawsec.js +99 -0
  11. package/bin/setup-venv.js +35 -0
  12. package/cli/clawsec.py +263 -0
  13. package/lib/common/__init__.py +2 -0
  14. package/lib/common/colors.sh +17 -0
  15. package/lib/common/config.py +12 -0
  16. package/lib/common/config.sh +8 -0
  17. package/lib/common/log.sh +24 -0
  18. package/lib/common/utils.sh +69 -0
  19. package/lib/intel-sync/manifest.py +103 -0
  20. package/lib/intel-sync/sources/cisa-kev.sh +24 -0
  21. package/lib/intel-sync/sources/epss.sh +34 -0
  22. package/lib/intel-sync/sources/feodo.sh +27 -0
  23. package/lib/intel-sync/sources/malwarebazaar.sh +22 -0
  24. package/lib/intel-sync/sources/osv.sh +101 -0
  25. package/lib/intel-sync/sources/semgrep-rules.sh +28 -0
  26. package/lib/intel-sync/sources/threatfox.sh +28 -0
  27. package/lib/intel-sync/sources/urlhaus.sh +42 -0
  28. package/lib/intel-sync/sources/yara-rules.sh +38 -0
  29. package/lib/intel-sync/sync.sh +96 -0
  30. package/lib/skill-verify/checks/behavioral.py +252 -0
  31. package/lib/skill-verify/checks/dep-scan.py +456 -0
  32. package/lib/skill-verify/checks/ioc-match.py +382 -0
  33. package/lib/skill-verify/checks/prompt-inject.py +158 -0
  34. package/lib/skill-verify/checks/secret-scan.sh +61 -0
  35. package/lib/skill-verify/checks/static-analysis.sh +73 -0
  36. package/lib/skill-verify/checks/yara-scan.sh +73 -0
  37. package/lib/skill-verify/report.py +119 -0
  38. package/lib/skill-verify/verify.sh +326 -0
  39. package/package.json +42 -0
  40. package/requirements.txt +6 -0
  41. package/setup.sh +200 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * ⚡ ClawSec v2 - Cloud API Server
3
+ * Express-based security verification service
4
+ */
5
+
6
+ const express = require('express');
7
+ const path = require('path');
8
+ const os = require('os');
9
+ const fs = require('fs');
10
+ const { v4: uuidv4 } = require('uuid');
11
+ const { RateLimiterMemory } = require('rate-limiter-flexible');
12
+ const routes = require('./routes');
13
+ const middleware = require('./middleware');
14
+
15
+ const CLAWSEC_DIR = process.env.CLAWSEC_HOME || path.join(os.homedir(), '.clawsec');
16
+ const REPORTS_DIR = process.env.CLAWSEC_REPORTS_DIR || path.join(CLAWSEC_DIR, 'reports');
17
+ const PORT = process.env.CLAWSEC_PORT || 3100;
18
+
19
+ const app = express();
20
+
21
+ // Middleware
22
+ app.use(express.json({ limit: '10mb' }));
23
+ app.use(middleware.rateLimiter);
24
+ app.use(middleware.apiKeyAuth);
25
+ app.use(middleware.requestLogger);
26
+
27
+ // CORS
28
+ app.use(require('cors')({
29
+ origin: '*',
30
+ methods: ['GET', 'POST'],
31
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key']
32
+ }));
33
+
34
+ // Static files
35
+ app.use(express.static(path.join(__dirname, '..', 'public')));
36
+
37
+ // API routes
38
+ app.use('/api/v1', routes);
39
+
40
+ // Health check
41
+ app.get('/health', (req, res) => {
42
+ res.json({ status: 'ok', version: '2.0.0', uptime: process.uptime() });
43
+ });
44
+
45
+ // Error handler
46
+ app.use((err, req, res, next) => {
47
+ console.error("[ERROR] " + err.message);
48
+ res.status(err.status || 500).json({
49
+ error: err.message || 'Internal server error',
50
+ status: err.status || 500
51
+ });
52
+ });
53
+
54
+ // Start
55
+ app.listen(PORT, '0.0.0.0', () => {
56
+ console.log("⚡ ClawSec API v2.0.0 listening on 0.0.0.0:" + PORT);
57
+ fs.mkdirSync(REPORTS_DIR, { recursive: true });
58
+ });
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bash
2
+ # ⚡ ClawSec API verification wrapper
3
+ # Activates venv and runs verify.sh --json
4
+ set -euo pipefail
5
+
6
+ # Resolve package root: this script is at <pkg>/api/src/verify-wrapper.sh
7
+ PKG_ROOT="$(cd "$(dirname "$0")"/../.. && pwd)"
8
+
9
+ source "${PKG_ROOT}/lib/common/config.sh"
10
+
11
+ VENV="${CLAWSEC_HOME}/venv"
12
+ if [[ -d "$VENV" ]]; then
13
+ source "$VENV/bin/activate"
14
+ fi
15
+ export PATH="$HOME/.local/bin:$PATH"
16
+ exec bash "${PKG_ROOT}/lib/skill-verify/verify.sh" --json "$1"
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ⚡ ClawSec API Server Entry Point
4
+ * Starts the Express API server
5
+ */
6
+
7
+ const path = require('path');
8
+
9
+ // Set defaults before requiring server
10
+ const CLAWSEC_HOME = process.env.CLAWSEC_HOME || path.join(require('os').homedir(), '.clawsec');
11
+ const INTEL_DIR = process.env.CLAWSEC_INTEL_DIR || path.join(CLAWSEC_HOME, 'intel');
12
+ const REPORTS_DIR = path.join(CLAWSEC_HOME, 'reports');
13
+
14
+ process.env.CLAWSEC_HOME = CLAWSEC_HOME;
15
+ process.env.CLAWSEC_INTEL_DIR = INTEL_DIR;
16
+ process.env.CLAWSEC_REPORTS_DIR = REPORTS_DIR;
17
+
18
+ // Require the server — it reads env vars at module level
19
+ require('../api/src/server');
package/bin/clawsec.js ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ⚡ ClawSec CLI Entry Point
4
+ * Bootstraps Python venv if needed, then delegates to clawsec.py
5
+ */
6
+
7
+ const { spawn, execSync } = require('child_process');
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+ const os = require('os');
11
+
12
+ const CLAWSEC_HOME = process.env.CLAWSEC_HOME || path.join(os.homedir(), '.clawsec');
13
+ const VENV_DIR = path.join(CLAWSEC_HOME, 'venv');
14
+ const PYTHON = path.join(VENV_DIR, 'bin', 'python3');
15
+
16
+ // Resolve package root — walk up from __dirname to find cli/clawsec.py
17
+ function findPackageRoot() {
18
+ let dir = __dirname;
19
+ // Check if we're running from installed package
20
+ if (fs.existsSync(path.join(dir, '..', 'cli', 'clawsec.py'))) {
21
+ return path.resolve(dir, '..');
22
+ }
23
+ // Fallback
24
+ return path.resolve(dir, '..');
25
+ }
26
+
27
+ const PKG_ROOT = findPackageRoot();
28
+ const CLAWSEC_PY = path.join(PKG_ROOT, 'cli', 'clawsec.py');
29
+ const REQUIREMENTS = path.join(PKG_ROOT, 'requirements.txt');
30
+
31
+ function log(msg) {
32
+ // Only show setup messages, not the ⚡ branding (Python handles that)
33
+ process.stderr.write(msg + '\n');
34
+ }
35
+
36
+ function setupVenv() {
37
+ if (fs.existsSync(PYTHON)) {
38
+ return true;
39
+ }
40
+
41
+ log('⚡ Setting up ClawSec Python environment (first run only)...');
42
+
43
+ try {
44
+ // Create CLAWSEC_HOME
45
+ fs.mkdirSync(CLAWSEC_HOME, { recursive: true });
46
+
47
+ // Create venv
48
+ log(' Creating Python venv...');
49
+ execSync(`python3 -m venv "${VENV_DIR}"`, { stdio: 'inherit' });
50
+
51
+ // Install requirements
52
+ if (fs.existsSync(REQUIREMENTS)) {
53
+ log(' Installing Python dependencies...');
54
+ execSync(`"${PYTHON}" -m pip install --quiet --upgrade pip`, { stdio: 'inherit' });
55
+ execSync(`"${PYTHON}" -m pip install --quiet -r "${REQUIREMENTS}"`, { stdio: 'inherit' });
56
+ }
57
+
58
+ log(' ✅ Python environment ready.');
59
+ return true;
60
+ } catch (err) {
61
+ log(`❌ Failed to set up Python environment: ${err.message}`);
62
+ log(' Try running: clawsec setup');
63
+ return false;
64
+ }
65
+ }
66
+
67
+ function run() {
68
+ // Ensure venv exists
69
+ if (!setupVenv()) {
70
+ process.exit(1);
71
+ }
72
+
73
+ // Build args — pass through all CLI args
74
+ const args = [CLAWSEC_PY, ...process.argv.slice(2)];
75
+
76
+ // Set env vars for Python subprocess
77
+ const env = {
78
+ ...process.env,
79
+ CLAWSEC_HOME: CLAWSEC_HOME,
80
+ CLAWSEC_INTEL_DIR: process.env.CLAWSEC_INTEL_DIR || path.join(CLAWSEC_HOME, 'intel'),
81
+ PATH: `${path.join(os.homedir(), '.local', 'bin')}:${process.env.PATH || '/usr/local/bin:/usr/bin:/bin'}`,
82
+ };
83
+
84
+ const child = spawn(PYTHON, args, {
85
+ env,
86
+ stdio: 'inherit',
87
+ });
88
+
89
+ child.on('close', (code) => {
90
+ process.exit(code ?? 1);
91
+ });
92
+
93
+ child.on('error', (err) => {
94
+ log(`❌ Failed to run clawsec: ${err.message}`);
95
+ process.exit(1);
96
+ });
97
+ }
98
+
99
+ run();
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ClawSec postinstall — sets up Python venv
4
+ * Called by npm postinstall. Safe to run multiple times.
5
+ */
6
+
7
+ const { execSync } = require('child_process');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ const CLAWSEC_HOME = process.env.CLAWSEC_HOME || path.join(os.homedir(), '.clawsec');
13
+ const VENV_DIR = path.join(CLAWSEC_HOME, 'venv');
14
+ const PYTHON = path.join(VENV_DIR, 'bin', 'python3');
15
+
16
+ // Don't fail install if setup fails — user can run clawsec setup later
17
+ try {
18
+ if (fs.existsSync(PYTHON)) {
19
+ process.exit(0); // Already set up
20
+ }
21
+
22
+ fs.mkdirSync(CLAWSEC_HOME, { recursive: true });
23
+ execSync(`python3 -m venv "${VENV_DIR}"`, { stdio: 'pipe' });
24
+
25
+ const PKG_ROOT = path.resolve(__dirname, '..');
26
+ const REQUIREMENTS = path.join(PKG_ROOT, 'requirements.txt');
27
+
28
+ if (fs.existsSync(REQUIREMENTS)) {
29
+ execSync(`"${PYTHON}" -m pip install --quiet --upgrade pip`, { stdio: 'pipe' });
30
+ execSync(`"${PYTHON}" -m pip install --quiet -r "${REQUIREMENTS}"`, { stdio: 'pipe' });
31
+ }
32
+ } catch {
33
+ // Postinstall is best-effort. The CLI will try again on first run.
34
+ process.exit(0);
35
+ }
package/cli/clawsec.py ADDED
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env python3
2
+ # ⚡ Low Watt Labs — ClawSec
3
+ """⚡ ClawSec v2 — Skill Security Verification
4
+
5
+ Usage:
6
+ clawsec scan <slug|path> Verify a skill
7
+ clawsec sync [source...] Refresh intel cache
8
+ clawsec status Show cache status
9
+ clawsec report <id> View a saved report
10
+ clawsec --help Show help
11
+ clawsec --version Show version
12
+ """
13
+
14
+ import argparse
15
+ import json
16
+ import os
17
+ import subprocess
18
+ import sys
19
+ import time
20
+ from pathlib import Path
21
+
22
+ VERSION = "2.0.0"
23
+ CLAWSEC_DIR = os.environ.get("CLAWSEC_HOME", os.path.expanduser("~/.clawsec"))
24
+ INTEL_DIR = os.environ.get("CLAWSEC_INTEL_DIR", os.path.join(CLAWSEC_DIR, "intel"))
25
+ REPORTS_DIR = os.environ.get("CLAWSEC_REPORTS_DIR", os.path.join(CLAWSEC_DIR, "reports"))
26
+
27
+ # Resolve package root — cli/clawsec.py -> parent = package root
28
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
29
+ PKG_ROOT = os.path.dirname(SCRIPT_DIR)
30
+
31
+ # ANSI colors
32
+ R = "\033[0;31m"
33
+ G = "\033[0;32m"
34
+ Y = "\033[0;33m"
35
+ B = "\033[0;34m"
36
+ C = "\033[0;36m"
37
+ BOLD = "\033[1m"
38
+ DIM = "\033[2m"
39
+ RESET = "\033[0m"
40
+
41
+ def banner():
42
+ print(f"""{BOLD}
43
+ ╔═════════════════════════════════════════╗
44
+ ║ ClawSec v{VERSION} ║
45
+ ║ ⚡ Security Verification for ClawHub ║
46
+ ╚═════════════════════════════════════════╝{RESET}
47
+ """)
48
+
49
+ def cmd_scan(args):
50
+ """Run verification against a skill path."""
51
+ target = args.target
52
+ 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
69
+
70
+ if not os.path.exists(target):
71
+ print(f"{R}Error:{RESET} {target} not found", file=sys.stderr)
72
+ sys.exit(2)
73
+
74
+ # Run verify.sh — resolve relative to package root
75
+ verify_sh = os.path.join(PKG_ROOT, "lib", "skill-verify", "verify.sh")
76
+ # Fallback: check CLAWSEC_HOME (for dev/local setups)
77
+ if not os.path.exists(verify_sh):
78
+ verify_sh = os.path.join(CLAWSEC_DIR, "lib", "skill-verify", "verify.sh")
79
+
80
+ cmd = ["bash", verify_sh]
81
+ if json_mode:
82
+ cmd.append("--json")
83
+ if args.checks:
84
+ cmd.append(f"--checks={args.checks}")
85
+ cmd.append(target)
86
+
87
+ result = subprocess.run(cmd, capture_output=json_mode, text=True)
88
+ if json_mode:
89
+ print(result.stdout)
90
+ sys.exit(result.returncode)
91
+
92
+ def cmd_sync(args):
93
+ """Run intel sync."""
94
+ sync_sh = os.path.join(PKG_ROOT, "lib", "intel-sync", "sync.sh")
95
+ if not os.path.exists(sync_sh):
96
+ sync_sh = os.path.join(CLAWSEC_DIR, "lib", "intel-sync", "sync.sh")
97
+
98
+ cmd = ["bash", sync_sh]
99
+ if args.json:
100
+ cmd.append("--json")
101
+ cmd.extend(args.sources)
102
+ result = subprocess.run(cmd, text=True)
103
+ sys.exit(result.returncode)
104
+
105
+ def cmd_status(args):
106
+ """Show cache status."""
107
+ manifest_py = os.path.join(PKG_ROOT, "lib", "intel-sync", "manifest.py")
108
+ if not os.path.exists(manifest_py):
109
+ manifest_py = os.path.join(CLAWSEC_DIR, "lib", "intel-sync", "manifest.py")
110
+ manifest_path = os.path.join(INTEL_DIR, "manifest.json")
111
+
112
+ if args.json:
113
+ result = subprocess.run(["python3", manifest_py, "status"], capture_output=True, text=True)
114
+ print(result.stdout)
115
+ return
116
+
117
+ if not os.path.exists(manifest_path):
118
+ print(f" {Y}No intel cache found{RESET}. Run {BOLD}clawsec sync{RESET} first.")
119
+ return
120
+
121
+ with open(manifest_path) as f:
122
+ manifest = json.load(f)
123
+
124
+ print(f" {BOLD}Intel Cache Status{RESET}")
125
+ print(f" {'─' * 50}")
126
+
127
+ for src in manifest.get("sources", []):
128
+ status_icon = G + "✓" + RESET if src["status"] == "success" else Y + "⚠" + RESET if src["status"] == "partial" else R + "✗" + RESET
129
+ last_sync = src.get("last_sync", "never")
130
+ if last_sync != "never":
131
+ try:
132
+ from datetime import datetime
133
+ dt = datetime.fromisoformat(last_sync.replace("Z", "+00:00"))
134
+ last_sync = dt.strftime("%Y-%m-%d %H:%M UTC")
135
+ except Exception:
136
+ pass
137
+ count = src.get("record_count", 0)
138
+ name = src["name"]
139
+ print(f" {status_icon} {name:<18} {count:>8} records {DIM}{last_sync}{RESET}")
140
+
141
+ print(f" {'─' * 50}")
142
+ updated = manifest.get("updated_at", "never")
143
+ try:
144
+ from datetime import datetime
145
+ dt = datetime.fromisoformat(updated.replace("Z", "+00:00"))
146
+ updated = dt.strftime("%Y-%m-%d %H:%M UTC")
147
+ except Exception:
148
+ pass
149
+ print(f" Last full sync: {updated}")
150
+
151
+ def cmd_report(args):
152
+ """View a saved report."""
153
+ report_id = args.report_id
154
+ report_path = os.path.join(REPORTS_DIR, f"{report_id}.json")
155
+
156
+ if not os.path.exists(report_path):
157
+ print(f"{R}Error:{RESET} Report {report_id} not found", file=sys.stderr)
158
+ sys.exit(1)
159
+
160
+ with open(report_path) as f:
161
+ report = json.load(f)
162
+
163
+ if args.json:
164
+ print(json.dumps(report, indent=2))
165
+ return
166
+
167
+ # Pretty-print
168
+ verdict = report.get("verdict", "unknown")
169
+ verdict_color = G if verdict == "pass" else Y if verdict == "warn" else R
170
+ summary = report.get("summary", {})
171
+
172
+ print(f"\n {BOLD}Report: {report_id}{RESET}")
173
+ print(f" {'─' * 50}")
174
+ print(f" Skill: {report.get('skill_path', 'unknown')}")
175
+ print(f" Verdict: {verdict_color}{BOLD}{verdict.upper()}{RESET}")
176
+ print(f" Findings: {summary.get('total_findings', 0)} total "
177
+ f"({R}{summary.get('critical', 0)} critical{RESET}, "
178
+ f"{Y}{summary.get('high', 0)} high{RESET}, "
179
+ f"{summary.get('medium', 0)} medium)")
180
+ print(f" Time: {report.get('timestamp', 'unknown')}")
181
+ print(f" {'─' * 50}")
182
+
183
+ for check in report.get("checks", []):
184
+ status = check.get("status", "unknown")
185
+ icon = G + "✓" + RESET if status == "pass" else Y + "⚠" + RESET if status == "warn" else R + "✗" + RESET
186
+ name = check.get("check", "unknown")
187
+ findings = check.get("findings", [])
188
+ errors = check.get("errors", [])
189
+ count = len(findings) if findings else 0
190
+
191
+ print(f"\n {icon} {name} ({status})")
192
+ if count > 0:
193
+ for f in findings[:5]:
194
+ desc = f.get("description", f.get("message", "No description"))
195
+ sev = f.get("severity", "unknown")
196
+ sev_color = R if sev in ("critical", "high") else Y if sev == "medium" else ""
197
+ print(f" {sev_color}● {desc}{RESET}")
198
+ if count > 5:
199
+ print(f" {DIM}... and {count - 5} more{RESET}")
200
+ if errors:
201
+ for e in errors[:3]:
202
+ print(f" {DIM}err: {e}{RESET}")
203
+
204
+ print()
205
+
206
+ def main():
207
+ parser = argparse.ArgumentParser(
208
+ prog="clawsec",
209
+ description="⚡ ClawSec v2 — Security Verification for ClawHub Skills",
210
+ formatter_class=argparse.RawDescriptionHelpFormatter,
211
+ epilog="""Examples:
212
+ clawsec scan ./my-skill Verify a local skill
213
+ clawsec scan ./my-skill --json Machine-readable output
214
+ clawsec sync Refresh all intel sources
215
+ clawsec sync cisa-kev epss Sync specific sources
216
+ clawsec status Show cache status
217
+ clawsec report abc123 View saved report"""
218
+ )
219
+ parser.add_argument("--version", action="version", version=f"clawsec v{VERSION}")
220
+
221
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
222
+
223
+ # scan
224
+ scan_parser = subparsers.add_parser("scan", help="Verify a skill")
225
+ scan_parser.add_argument("target", help="Skill path or ClawHub slug")
226
+ scan_parser.add_argument("--checks", help="Comma-separated list of checks to run")
227
+ scan_parser.add_argument("--json", action="store_true", help="JSON output")
228
+
229
+ # sync
230
+ sync_parser = subparsers.add_parser("sync", help="Refresh intel cache")
231
+ sync_parser.add_argument("sources", nargs="*", help="Specific sources to sync")
232
+ sync_parser.add_argument("--json", action="store_true", help="JSON output")
233
+
234
+ # status
235
+ status_parser = subparsers.add_parser("status", help="Show cache status")
236
+ status_parser.add_argument("--json", action="store_true", help="JSON output")
237
+
238
+ # report
239
+ report_parser = subparsers.add_parser("report", help="View saved report")
240
+ report_parser.add_argument("report_id", help="Report ID")
241
+ report_parser.add_argument("--json", action="store_true", help="JSON output")
242
+
243
+ args = parser.parse_args()
244
+
245
+ if args.command is None:
246
+ banner()
247
+ parser.print_help()
248
+ sys.exit(0)
249
+
250
+ if args.command == "scan":
251
+ cmd_scan(args)
252
+ elif args.command == "sync":
253
+ cmd_sync(args)
254
+ elif args.command == "status":
255
+ cmd_status(args)
256
+ elif args.command == "report":
257
+ cmd_report(args)
258
+ else:
259
+ parser.print_help()
260
+ sys.exit(1)
261
+
262
+ if __name__ == "__main__":
263
+ main()
@@ -0,0 +1,2 @@
1
+ """ClawSec Common - Configuration package."""
2
+ from .config import CLAWSEC_HOME, INTEL_DIR, REPORTS_DIR
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bash
2
+ # ClawSec Common - Terminal Colors
3
+ export RED='\033[0;31m'
4
+ export GREEN='\033[0;32m'
5
+ export YELLOW='\033[0;33m'
6
+ export BLUE='\033[0;34m'
7
+ export MAGENTA='\033[0;35m'
8
+ export CYAN='\033[0;36m'
9
+ export BOLD='\033[1m'
10
+ export DIM='\033[2m'
11
+ export RESET='\033[0m'
12
+
13
+ # Status markers
14
+ export CHECKMARK="${GREEN}✓${RESET}"
15
+ export CROSSMARK="${RED}✗${RESET}"
16
+ export WARNMARK="${YELLOW}⚠${RESET}"
17
+ export INFOMARK="${BLUE}ℹ${RESET}"
@@ -0,0 +1,12 @@
1
+ """ClawSec Common - Configuration
2
+
3
+ Centralizes INTEL_DIR and CLAWSEC_HOME with env var overrides.
4
+ Usage:
5
+ from lib.common.config import INTEL_DIR, CLAWSEC_HOME
6
+ """
7
+
8
+ import os
9
+
10
+ CLAWSEC_HOME = os.environ.get("CLAWSEC_HOME", os.path.expanduser("~/.clawsec"))
11
+ INTEL_DIR = os.environ.get("CLAWSEC_INTEL_DIR", os.path.join(CLAWSEC_HOME, "intel"))
12
+ REPORTS_DIR = os.environ.get("CLAWSEC_REPORTS_DIR", os.path.join(CLAWSEC_HOME, "reports"))
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ # ClawSec Common - Configuration
3
+ # Centralizes INTEL_DIR and CLAWSEC_HOME with env var overrides
4
+ # Usage: source this file from any script
5
+
6
+ export CLAWSEC_HOME="${CLAWSEC_HOME:-$HOME/.clawsec}"
7
+ export CLAWSEC_INTEL_DIR="${CLAWSEC_INTEL_DIR:-$CLAWSEC_HOME/intel}"
8
+ export CLAWSEC_REPORTS_DIR="${CLAWSEC_REPORTS_DIR:-$CLAWSEC_HOME/reports}"
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bash
2
+ # ⚡ ClawSec Common - Logging
3
+ # Usage: source this file, then use log_info/warn/err/ok
4
+
5
+ # Source config to get CLAWSEC_INTEL_DIR if not already set
6
+ if [[ -z "${CLAWSEC_HOME:-}" ]]; then
7
+ SCRIPT_DIR_LOG="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ source "${SCRIPT_DIR_LOG}/config.sh"
9
+ fi
10
+
11
+ export CLAWSEC_LOG_FILE="${CLAWSEC_HOME}/clawsec.log"
12
+
13
+ _log() {
14
+ local level="$1"; shift
15
+ local ts
16
+ ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
17
+ local msg="[$ts] [$level] $*"
18
+ echo "$msg" >> "$CLAWSEC_LOG_FILE" 2>/dev/null || true
19
+ }
20
+
21
+ log_info() { _log "INFO" "$@"; }
22
+ log_warn() { _log "WARN" "$@"; }
23
+ log_error() { _log "ERROR" "$@"; }
24
+ log_debug() { [[ "${CLAWSEC_DEBUG:-0}" == "1" ]] && _log "DEBUG" "$@"; }
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env bash
2
+ # ClawSec Common - Utilities
3
+
4
+ # Atomic write: write content to tmp file, validate, then mv
5
+ # Usage: atomic_write <target_path> <content_from_stdin>
6
+ atomic_write() {
7
+ local target="$1"
8
+ local tmp
9
+ tmp=$(mktemp "${target}.XXXXXX.new")
10
+ cat > "$tmp"
11
+ if [[ -s "$tmp" ]]; then
12
+ mv -f "$tmp" "$target"
13
+ return 0
14
+ else
15
+ rm -f "$tmp"
16
+ echo "atomic_write: refused to write empty file to $target" >&2
17
+ return 1
18
+ fi
19
+ }
20
+
21
+ # Validate JSON file
22
+ validate_json() {
23
+ local f="$1"
24
+ if jq empty "$f" 2>/dev/null; then
25
+ return 0
26
+ else
27
+ echo "validate_json: $f is not valid JSON" >&2
28
+ return 1
29
+ fi
30
+ }
31
+
32
+ # Safe download: fetch URL, validate, atomic write
33
+ # Usage: safe_download <url> <target_path> [validate_cmd]
34
+ safe_download() {
35
+ local url="$1"
36
+ local target="$2"
37
+ local validate_cmd="${3:-}"
38
+ local tmp
39
+ tmp=$(mktemp "${target}.XXXXXX.new")
40
+
41
+ if ! curl -fsSL --max-time 120 --retry 3 --retry-delay 5 "$url" -o "$tmp"; then
42
+ rm -f "$tmp"
43
+ echo "safe_download: FAILED to fetch $url" >&2
44
+ return 1
45
+ fi
46
+
47
+ if [[ -n "$validate_cmd" ]]; then
48
+ if ! eval "$validate_cmd" "$tmp"; then
49
+ rm -f "$tmp"
50
+ echo "safe_download: validation failed for $url" >&2
51
+ return 1
52
+ fi
53
+ fi
54
+
55
+ mv -f "$tmp" "$target"
56
+ return 0
57
+ }
58
+
59
+ # Get script directory
60
+ script_dir() {
61
+ local src="${BASH_SOURCE[0]}"
62
+ while [[ -L "$src" ]]; do
63
+ local dir
64
+ dir="$(cd -P "$(dirname "$src")" && pwd)"
65
+ src="$(readlink "$src")"
66
+ [[ "$src" != /* ]] && src="$dir/$src"
67
+ done
68
+ cd -P "$(dirname "$src")" && pwd
69
+ }