@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.
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/api/public/index.html +87 -0
- package/api/src/badge.js +60 -0
- package/api/src/middleware.js +104 -0
- package/api/src/routes.js +184 -0
- package/api/src/server.js +58 -0
- package/api/src/verify-wrapper.sh +16 -0
- package/bin/clawsec-api.js +19 -0
- package/bin/clawsec.js +99 -0
- package/bin/setup-venv.js +35 -0
- package/cli/clawsec.py +263 -0
- package/lib/common/__init__.py +2 -0
- package/lib/common/colors.sh +17 -0
- package/lib/common/config.py +12 -0
- package/lib/common/config.sh +8 -0
- package/lib/common/log.sh +24 -0
- package/lib/common/utils.sh +69 -0
- package/lib/intel-sync/manifest.py +103 -0
- package/lib/intel-sync/sources/cisa-kev.sh +24 -0
- package/lib/intel-sync/sources/epss.sh +34 -0
- package/lib/intel-sync/sources/feodo.sh +27 -0
- package/lib/intel-sync/sources/malwarebazaar.sh +22 -0
- package/lib/intel-sync/sources/osv.sh +101 -0
- package/lib/intel-sync/sources/semgrep-rules.sh +28 -0
- package/lib/intel-sync/sources/threatfox.sh +28 -0
- package/lib/intel-sync/sources/urlhaus.sh +42 -0
- package/lib/intel-sync/sources/yara-rules.sh +38 -0
- package/lib/intel-sync/sync.sh +96 -0
- package/lib/skill-verify/checks/behavioral.py +252 -0
- package/lib/skill-verify/checks/dep-scan.py +456 -0
- package/lib/skill-verify/checks/ioc-match.py +382 -0
- package/lib/skill-verify/checks/prompt-inject.py +158 -0
- package/lib/skill-verify/checks/secret-scan.sh +61 -0
- package/lib/skill-verify/checks/static-analysis.sh +73 -0
- package/lib/skill-verify/checks/yara-scan.sh +73 -0
- package/lib/skill-verify/report.py +119 -0
- package/lib/skill-verify/verify.sh +326 -0
- package/package.json +42 -0
- package/requirements.txt +6 -0
- 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,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
|
+
}
|