@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,73 @@
|
|
|
1
|
+
# ⚡ Low Watt Labs — ClawSec
|
|
2
|
+
# ClawSec v2 - YARA Scan
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
source "$(dirname "$0")/../../common/config.sh"
|
|
6
|
+
source "$(dirname "$0")/../../common/colors.sh"
|
|
7
|
+
|
|
8
|
+
INTEL_DIR="${CLAWSEC_INTEL_DIR}"
|
|
9
|
+
YARA_RULES_DIR="${INTEL_DIR}/yara-rules/repo/yara"
|
|
10
|
+
|
|
11
|
+
skill_path="${1:?Usage: yara-scan.sh <skill_path>}"
|
|
12
|
+
results='{"check":"yara_scan","status":"pass","findings":[],"errors":[]}'
|
|
13
|
+
|
|
14
|
+
if ! command -v yara &>/dev/null; then
|
|
15
|
+
echo '{"check":"yara_scan","status":"pass","findings":[],"errors":["yara not installed — skipping"]}'
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
if [[ ! -d "$YARA_RULES_DIR" ]]; then
|
|
20
|
+
echo '{"check":"yara_scan","status":"pass","findings":[],"errors":["YARA rules not synced — skipping"]}'
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Test-compile and collect valid rule files
|
|
25
|
+
good_rules=()
|
|
26
|
+
while IFS= read -r -d '' f; do
|
|
27
|
+
if yarac "$f" /dev/null 2>/dev/null; then
|
|
28
|
+
good_rules+=("$f")
|
|
29
|
+
fi
|
|
30
|
+
done < <(find "$YARA_RULES_DIR" -name '*.yar' -o -name '*.yara' 2>/dev/null | sort -z)
|
|
31
|
+
|
|
32
|
+
# If we have good rules, build a compiled ruleset and scan
|
|
33
|
+
if [[ ${#good_rules[@]} -gt 0 ]]; then
|
|
34
|
+
compiled_rules=$(mktemp /tmp/yara-compiled.XXXXXX)
|
|
35
|
+
# Build combined rule file for yarac
|
|
36
|
+
combined_rules=$(mktemp /tmp/yara-combined.XXXXXX.yar)
|
|
37
|
+
for rulefile in "${good_rules[@]}"; do
|
|
38
|
+
echo "include \"${rulefile}\"" >> "$combined_rules"
|
|
39
|
+
done
|
|
40
|
+
if yarac "$combined_rules" "$compiled_rules" 2>/dev/null; then
|
|
41
|
+
# Scan with compiled rules for performance
|
|
42
|
+
tmpout=$(mktemp /tmp/yara-scan.XXXXXX)
|
|
43
|
+
yara -r -C "$compiled_rules" "$skill_path" >> "$tmpout" 2>/dev/null || true
|
|
44
|
+
else
|
|
45
|
+
# Fallback: scan with source rules individually
|
|
46
|
+
tmpout=$(mktemp /tmp/yara-scan.XXXXXX)
|
|
47
|
+
for rulefile in "${good_rules[@]}"; do
|
|
48
|
+
yara -r "$rulefile" "$skill_path" >> "$tmpout" 2>/dev/null || true
|
|
49
|
+
done
|
|
50
|
+
fi
|
|
51
|
+
rm -f "$combined_rules"
|
|
52
|
+
|
|
53
|
+
if [[ -s "$tmpout" ]]; then
|
|
54
|
+
findings='[]'
|
|
55
|
+
while IFS=$'\t' read -r rule file; do
|
|
56
|
+
rel="${file#$skill_path/}"
|
|
57
|
+
findings=$(echo "$findings" | jq --arg rule "$rule" --arg file "$rel" \
|
|
58
|
+
'. + [{rule: $rule, file: $file, severity: "high"}]')
|
|
59
|
+
done < "$tmpout"
|
|
60
|
+
|
|
61
|
+
count=$(echo "$findings" | jq 'length')
|
|
62
|
+
if [[ "$count" -gt 0 ]]; then
|
|
63
|
+
results=$(jq -n --argjson findings "$findings" --arg count "$count" \
|
|
64
|
+
'{check:"yara_scan",status:"fail",findings:$findings,errors:[],total:$count}')
|
|
65
|
+
fi
|
|
66
|
+
fi
|
|
67
|
+
rm -f "$tmpout"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
if [[ -n "${compiled_rules:-}" ]]; then
|
|
71
|
+
rm -f "$compiled_rules"
|
|
72
|
+
fi
|
|
73
|
+
echo "$results"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# ⚡ Low Watt Labs — ClawSec Skill Verification Report
|
|
2
|
+
"""ClawSec v2 - Report Generator
|
|
3
|
+
|
|
4
|
+
Aggregates check results into a final JSON report with verdict.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import uuid
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
|
|
14
|
+
from config import CLAWSEC_HOME, INTEL_DIR
|
|
15
|
+
|
|
16
|
+
REPORTS_DIR = os.path.join(CLAWSEC_HOME, "reports")
|
|
17
|
+
|
|
18
|
+
def generate_report(skill_path, check_results):
|
|
19
|
+
"""Generate a final report from all check results."""
|
|
20
|
+
report_id = str(uuid.uuid4())[:8]
|
|
21
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
22
|
+
|
|
23
|
+
# Aggregate findings
|
|
24
|
+
all_findings = []
|
|
25
|
+
total_critical = 0
|
|
26
|
+
total_high = 0
|
|
27
|
+
total_medium = 0
|
|
28
|
+
total_low = 0
|
|
29
|
+
|
|
30
|
+
for result in check_results:
|
|
31
|
+
if "error" in result and not result.get("findings"):
|
|
32
|
+
continue
|
|
33
|
+
for f in result.get("findings", []):
|
|
34
|
+
# P0-7: Escalate specific finding categories to critical severity
|
|
35
|
+
ftype = f.get("type", "")
|
|
36
|
+
category = f.get("category", "")
|
|
37
|
+
pattern = f.get("pattern", "")
|
|
38
|
+
escalate_categories = {"command_injection", "shell_injection", "path_traversal", "hardcoded_secret", "secret_in_code"}
|
|
39
|
+
if ftype in escalate_categories or category in escalate_categories:
|
|
40
|
+
f["severity"] = "critical"
|
|
41
|
+
# Also escalate os.system and shell injection patterns from Semgrep
|
|
42
|
+
if any(kw in pattern for kw in ["os.system", "shell=True", "execSync", "child_process.exec"]):
|
|
43
|
+
f["severity"] = "critical"
|
|
44
|
+
|
|
45
|
+
severity = f.get("severity", "low")
|
|
46
|
+
if severity == "critical":
|
|
47
|
+
total_critical += 1
|
|
48
|
+
elif severity == "high":
|
|
49
|
+
total_high += 1
|
|
50
|
+
elif severity == "medium":
|
|
51
|
+
total_medium += 1
|
|
52
|
+
else:
|
|
53
|
+
total_low += 1
|
|
54
|
+
all_findings.append(f)
|
|
55
|
+
|
|
56
|
+
# Determine overall verdict
|
|
57
|
+
check_statuses = [r.get("status", "pass") for r in check_results]
|
|
58
|
+
if "fail" in check_statuses or total_critical > 0:
|
|
59
|
+
verdict = "fail"
|
|
60
|
+
elif "warn" in check_statuses or total_high > 0:
|
|
61
|
+
verdict = "warn"
|
|
62
|
+
else:
|
|
63
|
+
verdict = "pass"
|
|
64
|
+
|
|
65
|
+
report = {
|
|
66
|
+
"report_id": report_id,
|
|
67
|
+
"schema_version": "2.0.0",
|
|
68
|
+
"timestamp": now,
|
|
69
|
+
"skill_path": skill_path,
|
|
70
|
+
"verdict": verdict,
|
|
71
|
+
"summary": {
|
|
72
|
+
"total_findings": len(all_findings),
|
|
73
|
+
"critical": total_critical,
|
|
74
|
+
"high": total_high,
|
|
75
|
+
"medium": total_medium,
|
|
76
|
+
"low": total_low,
|
|
77
|
+
},
|
|
78
|
+
"checks": check_results,
|
|
79
|
+
"intel_cache": get_cache_timestamps(),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Save report
|
|
83
|
+
os.makedirs(REPORTS_DIR, exist_ok=True)
|
|
84
|
+
report_path = os.path.join(REPORTS_DIR, f"{report_id}.json")
|
|
85
|
+
with open(report_path, 'w') as f:
|
|
86
|
+
json.dump(report, f, indent=2)
|
|
87
|
+
|
|
88
|
+
return report, report_path
|
|
89
|
+
|
|
90
|
+
def get_cache_timestamps():
|
|
91
|
+
"""Get timestamps from manifest for report provenance."""
|
|
92
|
+
manifest_path = os.path.join(INTEL_DIR, "manifest.json")
|
|
93
|
+
if os.path.exists(manifest_path):
|
|
94
|
+
try:
|
|
95
|
+
with open(manifest_path) as f:
|
|
96
|
+
manifest = json.load(f)
|
|
97
|
+
return {s["name"]: s.get("last_sync", "unknown") for s in manifest.get("sources", [])}
|
|
98
|
+
except (json.JSONDecodeError, KeyError):
|
|
99
|
+
pass
|
|
100
|
+
return {}
|
|
101
|
+
|
|
102
|
+
def load_report(report_id):
|
|
103
|
+
"""Load a saved report by ID."""
|
|
104
|
+
path = os.path.join(REPORTS_DIR, f"{report_id}.json")
|
|
105
|
+
if os.path.exists(path):
|
|
106
|
+
with open(path) as f:
|
|
107
|
+
return json.load(f)
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
if len(sys.argv) < 2:
|
|
112
|
+
print("Usage: report.py <report_id>")
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
report = load_report(sys.argv[1])
|
|
115
|
+
if report:
|
|
116
|
+
print(json.dumps(report, indent=2))
|
|
117
|
+
else:
|
|
118
|
+
print(f"Report {sys.argv[1]} not found")
|
|
119
|
+
sys.exit(1)
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# ⚡ Low Watt Labs — ClawSec Skill Verify Orchestrator
|
|
2
|
+
# ClawSec v2 - Skill Verify Orchestrator
|
|
3
|
+
# Runs all 7 security checks against a skill, produces JSON report
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
VERSION="2.0.0"
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
CHECKS_DIR="${SCRIPT_DIR}/checks"
|
|
9
|
+
|
|
10
|
+
source "${SCRIPT_DIR}/../common/config.sh"
|
|
11
|
+
source "${SCRIPT_DIR}/../common/colors.sh"
|
|
12
|
+
source "${SCRIPT_DIR}/../common/log.sh"
|
|
13
|
+
|
|
14
|
+
# Activate Python venv if available
|
|
15
|
+
VENV="${CLAWSEC_HOME}/venv"
|
|
16
|
+
if [[ -d "$VENV" ]]; then
|
|
17
|
+
source "$VENV/bin/activate"
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
usage() {
|
|
21
|
+
echo "⚡ ClawSec v${VERSION} — Skill Verification"
|
|
22
|
+
echo ""
|
|
23
|
+
echo "Usage: verify.sh [OPTIONS] <skill_path>"
|
|
24
|
+
echo ""
|
|
25
|
+
echo "Options:"
|
|
26
|
+
echo " --json Output report as JSON only"
|
|
27
|
+
echo " --checks=LIST Run only specified checks (comma-separated)"
|
|
28
|
+
echo " --strict Fail if ANY intel source is missing (default: warn)"
|
|
29
|
+
echo " --help Show this help"
|
|
30
|
+
echo ""
|
|
31
|
+
echo "Staleness thresholds:"
|
|
32
|
+
echo " 30+ days stale → warn (results may be outdated)"
|
|
33
|
+
echo " 90+ days stale → fail (results unreliable, resync required)"
|
|
34
|
+
echo ""
|
|
35
|
+
echo "Checks: dep-scan, static-analysis, secret-scan, yara-scan,"
|
|
36
|
+
echo " ioc-match, behavioral, prompt-inject"
|
|
37
|
+
exit 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
skill_path=""
|
|
41
|
+
json_only=0
|
|
42
|
+
specific_checks=""
|
|
43
|
+
strict_mode=0
|
|
44
|
+
|
|
45
|
+
while [[ $# -gt 0 ]]; do
|
|
46
|
+
case "$1" in
|
|
47
|
+
--json) json_only=1; shift ;;
|
|
48
|
+
--checks=*) specific_checks="${1#--checks=}"; shift ;;
|
|
49
|
+
--strict) strict_mode=1; shift ;;
|
|
50
|
+
--help|-h) usage ;;
|
|
51
|
+
-*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
52
|
+
*) skill_path="$1"; shift ;;
|
|
53
|
+
esac
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
if [[ -z "$skill_path" ]]; then
|
|
57
|
+
echo "Error: skill path required" >&2
|
|
58
|
+
echo "Usage: verify.sh <skill_path>" >&2
|
|
59
|
+
exit 2
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if [[ ! -d "$skill_path" ]] && [[ ! -f "$skill_path" ]]; then
|
|
63
|
+
echo "Error: $skill_path not found" >&2
|
|
64
|
+
exit 2
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# Resolve to absolute path
|
|
68
|
+
skill_path="$(cd "$(dirname "$skill_path")" 2>/dev/null && pwd)/$(basename "$skill_path")" || skill_path="$(realpath "$skill_path")"
|
|
69
|
+
|
|
70
|
+
# P0-4: Validate intel cache before running any checks
|
|
71
|
+
INTEL_DIR="${CLAWSEC_INTEL_DIR}"
|
|
72
|
+
MANIFEST_JSON="${INTEL_DIR}/manifest.json"
|
|
73
|
+
intel_missing=0
|
|
74
|
+
intel_errors=()
|
|
75
|
+
strict_fail=0
|
|
76
|
+
|
|
77
|
+
if [[ ! -d "$INTEL_DIR" ]]; then
|
|
78
|
+
intel_missing=1
|
|
79
|
+
intel_errors+=("Intel cache directory ${INTEL_DIR} does not exist")
|
|
80
|
+
strict_fail=1
|
|
81
|
+
elif [[ ! -f "$MANIFEST_JSON" ]]; then
|
|
82
|
+
intel_missing=1
|
|
83
|
+
intel_errors+=("Intel manifest ${MANIFEST_JSON} missing — sync has never completed")
|
|
84
|
+
strict_fail=1
|
|
85
|
+
else
|
|
86
|
+
# Check individual cache files that checks depend on
|
|
87
|
+
if [[ ! -f "${INTEL_DIR}/cisa-kev/known_exploited_vulnerabilities.json" ]]; then
|
|
88
|
+
intel_errors+=("CISA KEV cache missing")
|
|
89
|
+
((strict_mode)) && strict_fail=1
|
|
90
|
+
fi
|
|
91
|
+
if [[ ! -d "${INTEL_DIR}/osv" ]]; then
|
|
92
|
+
intel_errors+=("OSV cache missing")
|
|
93
|
+
((strict_mode)) && strict_fail=1
|
|
94
|
+
fi
|
|
95
|
+
if [[ ! -f "${INTEL_DIR}/urlhaus/urls.csv" ]]; then
|
|
96
|
+
intel_errors+=("URLhaus cache missing")
|
|
97
|
+
((strict_mode)) && strict_fail=1
|
|
98
|
+
fi
|
|
99
|
+
if [[ ! -f "${INTEL_DIR}/malwarebazaar/recent_hashes.csv" ]]; then
|
|
100
|
+
intel_errors+=("MalwareBazaar cache missing")
|
|
101
|
+
((strict_mode)) && strict_fail=1
|
|
102
|
+
fi
|
|
103
|
+
if [[ ! -f "${INTEL_DIR}/feodo/c2_ips.csv" ]]; then
|
|
104
|
+
intel_errors+=("Feodo cache missing")
|
|
105
|
+
((strict_mode)) && strict_fail=1
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# P1-3: Staleness check — warn if 30+ days, fail if 90+ days
|
|
109
|
+
if [[ -f "$MANIFEST_JSON" ]]; then
|
|
110
|
+
stale_warn=0
|
|
111
|
+
stale_fail=0
|
|
112
|
+
while IFS= read -r line; do
|
|
113
|
+
src_name=$(echo "$line" | jq -r '.name // empty')
|
|
114
|
+
src_date=$(echo "$line" | jq -r '.last_sync // empty')
|
|
115
|
+
[[ -z "$src_name" || -z "$src_date" || "$src_date" == "never" ]] && continue
|
|
116
|
+
# Calculate age in days
|
|
117
|
+
sync_epoch=$(date -d "$src_date" +%s 2>/dev/null || echo 0)
|
|
118
|
+
now_epoch=$(date +%s)
|
|
119
|
+
if [[ "$sync_epoch" -gt 0 ]]; then
|
|
120
|
+
age_days=$(( (now_epoch - sync_epoch) / 86400 ))
|
|
121
|
+
if [[ $age_days -ge 90 ]]; then
|
|
122
|
+
stale_fail=1
|
|
123
|
+
intel_errors+=("${src_name} is ${age_days} days old (>= 90 days — scan results unreliable)")
|
|
124
|
+
elif [[ $age_days -ge 30 ]]; then
|
|
125
|
+
stale_warn=1
|
|
126
|
+
if [[ $json_only -eq 0 ]]; then
|
|
127
|
+
echo -e " ${WARNMARK} ${src_name} is ${age_days} days old (>= 30 days)" >&2
|
|
128
|
+
fi
|
|
129
|
+
fi
|
|
130
|
+
fi
|
|
131
|
+
done < <(jq -c '.sources[]' "$MANIFEST_JSON" 2>/dev/null)
|
|
132
|
+
|
|
133
|
+
if [[ $stale_fail -eq 1 ]]; then
|
|
134
|
+
strict_fail=1
|
|
135
|
+
for err in "${intel_errors[@]}"; do
|
|
136
|
+
[[ "$err" == *"90 days"* ]] && echo -e " ${RED}${BOLD}STALE:${RESET} $err" >&2
|
|
137
|
+
done
|
|
138
|
+
fi
|
|
139
|
+
fi
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
if [[ $strict_fail -eq 1 ]]; then
|
|
143
|
+
if [[ $json_only -eq 0 ]]; then
|
|
144
|
+
echo -e "${RED}${BOLD}ERROR:${RESET} Intel cache is incomplete or missing."
|
|
145
|
+
for err in "${intel_errors[@]}"; do
|
|
146
|
+
echo -e " ${CROSSMARK} ${err}"
|
|
147
|
+
done
|
|
148
|
+
echo " Run: bash lib/intel-sync/sync.sh --all"
|
|
149
|
+
echo " Or re-run without --strict to allow partial checks"
|
|
150
|
+
fi
|
|
151
|
+
# In strict mode with missing intel, abort with exit code 2 (fail)
|
|
152
|
+
exit 2
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# In non-strict mode, warn but continue
|
|
156
|
+
if [[ ${#intel_errors[@]} -gt 0 ]] && [[ $json_only -eq 0 ]]; then
|
|
157
|
+
echo -e "${YELLOW}${BOLD}WARNING:${RESET} Some intel sources are missing:"
|
|
158
|
+
for err in "${intel_errors[@]}"; do
|
|
159
|
+
echo -e " ${WARNMARK} ${err}"
|
|
160
|
+
done
|
|
161
|
+
echo " Results may be incomplete. Run: bash lib/intel-sync/sync.sh --all"
|
|
162
|
+
echo ""
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
# If intel cache directory doesn't exist at all, override verdict to "fail"
|
|
166
|
+
cache_completely_missing=0
|
|
167
|
+
if [[ ! -d "$INTEL_DIR" ]]; then
|
|
168
|
+
cache_completely_missing=1
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
ALL_CHECKS=(dep-scan static-analysis secret-scan yara-scan ioc-match behavioral prompt-inject)
|
|
172
|
+
if [[ -n "$specific_checks" ]]; then
|
|
173
|
+
IFS=',' read -ra CHECKS <<< "$specific_checks"
|
|
174
|
+
else
|
|
175
|
+
CHECKS=("${ALL_CHECKS[@]}")
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
if [[ $json_only -eq 0 ]]; then
|
|
179
|
+
echo -e "${BOLD}⚡═══════════════════════════════════════════⚡${RESET}"
|
|
180
|
+
echo -e "${BOLD}⚡ ClawSec v${VERSION} — Skill Verification ⚡${RESET}"
|
|
181
|
+
echo -e "${BOLD}⚡═══════════════════════════════════════════⚡${RESET}"
|
|
182
|
+
echo ""
|
|
183
|
+
echo -e " Target: ${CYAN}${skill_path}${RESET}"
|
|
184
|
+
echo -e " Checks: ${BOLD}${#CHECKS[@]}${RESET} of ${#ALL_CHECKS[@]}"
|
|
185
|
+
if [[ ${#intel_errors[@]} -gt 0 ]]; then
|
|
186
|
+
echo -e " ${WARNMARK} ${YELLOW}${#intel_errors[@]} intel source(s) missing${RESET}"
|
|
187
|
+
fi
|
|
188
|
+
echo ""
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
# Run checks and collect JSON results
|
|
192
|
+
check_results="[]"
|
|
193
|
+
start_time=$(date +%s%N)
|
|
194
|
+
|
|
195
|
+
for check in "${CHECKS[@]}"; do
|
|
196
|
+
if [[ $json_only -eq 0 ]]; then
|
|
197
|
+
echo -ne " ${DIM}▸${RESET} Running ${check}... "
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
result=""
|
|
201
|
+
case "$check" in
|
|
202
|
+
dep-scan)
|
|
203
|
+
result=$(python3 "${CHECKS_DIR}/dep-scan.py" "$skill_path" 2>/dev/null || \
|
|
204
|
+
echo '{"check":"dep-scan","status":"pass","findings":[],"errors":["check failed"]}')
|
|
205
|
+
;;
|
|
206
|
+
static-analysis)
|
|
207
|
+
result=$(bash "${CHECKS_DIR}/static-analysis.sh" "$skill_path" 2>/dev/null || \
|
|
208
|
+
echo '{"check":"static_analysis","status":"pass","findings":[],"errors":["check failed"]}')
|
|
209
|
+
;;
|
|
210
|
+
secret-scan)
|
|
211
|
+
result=$(bash "${CHECKS_DIR}/secret-scan.sh" "$skill_path" 2>/dev/null || \
|
|
212
|
+
echo '{"check":"secret_scan","status":"pass","findings":[],"errors":["check failed"]}')
|
|
213
|
+
;;
|
|
214
|
+
yara-scan)
|
|
215
|
+
result=$(bash "${CHECKS_DIR}/yara-scan.sh" "$skill_path" 2>/dev/null || \
|
|
216
|
+
echo '{"check":"yara_scan","status":"pass","findings":[],"errors":["check failed"]}')
|
|
217
|
+
;;
|
|
218
|
+
ioc-match)
|
|
219
|
+
result=$(python3 "${CHECKS_DIR}/ioc-match.py" "$skill_path" 2>/dev/null || \
|
|
220
|
+
echo '{"check":"ioc_match","status":"pass","findings":[],"errors":["check failed"]}')
|
|
221
|
+
;;
|
|
222
|
+
behavioral)
|
|
223
|
+
result=$(python3 "${CHECKS_DIR}/behavioral.py" "$skill_path" 2>/dev/null || \
|
|
224
|
+
echo '{"check":"behavioral_heuristics","status":"pass","findings":[],"errors":["check failed"]}')
|
|
225
|
+
;;
|
|
226
|
+
prompt-inject)
|
|
227
|
+
result=$(python3 "${CHECKS_DIR}/prompt-inject.py" "$skill_path" 2>/dev/null || \
|
|
228
|
+
echo '{"check":"prompt_injection","status":"pass","findings":[],"errors":["check failed"]}')
|
|
229
|
+
;;
|
|
230
|
+
*)
|
|
231
|
+
if [[ $json_only -eq 0 ]]; then
|
|
232
|
+
echo -e "${WARNMARK} unknown"
|
|
233
|
+
fi
|
|
234
|
+
continue
|
|
235
|
+
;;
|
|
236
|
+
esac
|
|
237
|
+
|
|
238
|
+
# Validate result is JSON
|
|
239
|
+
if ! echo "$result" | jq empty 2>/dev/null; then
|
|
240
|
+
result="{\"check\":\"$check\",\"status\":\"pass\",\"findings\":[],\"errors\":[\"invalid output\"]}"
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
# Append to results array
|
|
244
|
+
check_results=$(echo "$check_results" | jq --argjson r "$result" '. + [$r]')
|
|
245
|
+
|
|
246
|
+
# Console feedback
|
|
247
|
+
if [[ $json_only -eq 0 ]]; then
|
|
248
|
+
status=$(echo "$result" | jq -r '.status')
|
|
249
|
+
findings_count=$(echo "$result" | jq '.findings | length')
|
|
250
|
+
case "$status" in
|
|
251
|
+
pass) echo -e "${CHECKMARK} ${GREEN}pass${RESET} (${findings_count} findings)" ;;
|
|
252
|
+
warn) echo -e "${WARNMARK} ${YELLOW}warn${RESET} (${findings_count} findings)" ;;
|
|
253
|
+
fail) echo -e "${CROSSMARK} ${RED}fail${RESET} (${findings_count} findings)" ;;
|
|
254
|
+
*) echo -e " ${status} (${findings_count} findings)" ;;
|
|
255
|
+
esac
|
|
256
|
+
fi
|
|
257
|
+
done
|
|
258
|
+
|
|
259
|
+
end_time=$(date +%s%N)
|
|
260
|
+
elapsed_ms=$(( (end_time - start_time) / 1000000 ))
|
|
261
|
+
|
|
262
|
+
# Generate report via safe temp file approach
|
|
263
|
+
results_tmpfile=$(mktemp /tmp/clawsec-results.XXXXXX.json)
|
|
264
|
+
echo "$check_results" > "$results_tmpfile"
|
|
265
|
+
|
|
266
|
+
report_json=$(python3 -c "
|
|
267
|
+
import sys, json
|
|
268
|
+
sys.path.insert(0, sys.argv[1])
|
|
269
|
+
from report import generate_report
|
|
270
|
+
with open(sys.argv[2]) as f:
|
|
271
|
+
results = json.load(f)
|
|
272
|
+
report, path = generate_report(sys.argv[3], results)
|
|
273
|
+
report['scan_duration_ms'] = int(sys.argv[4])
|
|
274
|
+
print(json.dumps(report, indent=2))
|
|
275
|
+
" "${SCRIPT_DIR}" "$results_tmpfile" "${skill_path}" "${elapsed_ms}" 2>&1)
|
|
276
|
+
rm -f "$results_tmpfile"
|
|
277
|
+
|
|
278
|
+
if [[ -z "$report_json" ]]; then
|
|
279
|
+
# Fallback: assemble report via jq
|
|
280
|
+
verdict=$(echo "$check_results" | jq -r 'if any(.status == "fail") then "fail" elif any(.status == "warn") then "warn" else "pass" end')
|
|
281
|
+
report_json=$(echo "$check_results" | jq -s '.' | jq \
|
|
282
|
+
--arg verdict "$verdict" \
|
|
283
|
+
--arg path "$skill_path" \
|
|
284
|
+
--argjson duration "$elapsed_ms" \
|
|
285
|
+
'{schema_version:"2.0.0",version:"2.0.0",verdict:$verdict,skill_path:$path,checks:.,scan_duration_ms:$duration}')
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
verdict=$(echo "$report_json" | jq -r '.verdict')
|
|
289
|
+
|
|
290
|
+
# P0-4: If intel cache was completely missing, override verdict to "fail"
|
|
291
|
+
if [[ $cache_completely_missing -eq 1 ]]; then
|
|
292
|
+
verdict="fail"
|
|
293
|
+
report_json=$(echo "$report_json" | jq --arg v "fail" '.verdict = $v')
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
if [[ $json_only -eq 0 ]]; then
|
|
297
|
+
echo ""
|
|
298
|
+
total=$(echo "$report_json" | jq '.summary.total_findings // 0')
|
|
299
|
+
crit=$(echo "$report_json" | jq '.summary.critical // 0')
|
|
300
|
+
high=$(echo "$report_json" | jq '.summary.high // 0')
|
|
301
|
+
med=$(echo "$report_json" | jq '.summary.medium // 0')
|
|
302
|
+
|
|
303
|
+
echo -e " ${BOLD}──────────────────────────────────────${RESET}"
|
|
304
|
+
echo -e " Verdict: $(case $verdict in pass) echo -e \"${GREEN}${BOLD}PASS${RESET}\" ;; warn) echo -e \"${YELLOW}${BOLD}WARN${RESET}\" ;; fail) echo -e \"${RED}${BOLD}FAIL${RESET}\" ;; esac)"
|
|
305
|
+
echo -e " Findings: ${total} total (${RED}${crit} critical${RESET}, ${YELLOW}${high} high${RESET}, ${med} medium)"
|
|
306
|
+
echo -e " Time: $((elapsed_ms / 1000)).$((elapsed_ms % 1000))s"
|
|
307
|
+
report_id=$(echo "$report_json" | jq -r '.report_id // "unknown"')
|
|
308
|
+
echo -e " Report: ${report_id}"
|
|
309
|
+
if [[ ${#intel_errors[@]} -gt 0 ]]; then
|
|
310
|
+
echo -e " ${WARNMARK} ${YELLOW}Intel sources missing — results may be incomplete${RESET}"
|
|
311
|
+
fi
|
|
312
|
+
echo ""
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
# Write full JSON report to stdout if --json
|
|
316
|
+
if [[ $json_only -eq 1 ]]; then
|
|
317
|
+
echo "$report_json"
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
# Exit code: 0=pass, 1=warn, 2=fail
|
|
321
|
+
case "$verdict" in
|
|
322
|
+
pass) exit 0 ;;
|
|
323
|
+
warn) exit 1 ;;
|
|
324
|
+
fail) exit 2 ;;
|
|
325
|
+
*) exit 1 ;;
|
|
326
|
+
esac
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lowwattlabs/clawsec",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "ClawSec - Security Verification for ClawHub Skills",
|
|
5
|
+
"bin": {
|
|
6
|
+
"clawsec": "./bin/clawsec.js",
|
|
7
|
+
"clawsec-api": "./bin/clawsec-api.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"cli/",
|
|
12
|
+
"lib/",
|
|
13
|
+
"api/",
|
|
14
|
+
"requirements.txt",
|
|
15
|
+
"setup.sh"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node api/src/server.js",
|
|
19
|
+
"dev": "node --watch api/src/server.js",
|
|
20
|
+
"postinstall": "node bin/setup-venv.js"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"express": "^4.21.0",
|
|
24
|
+
"rate-limiter-flexible": "^5.0.0",
|
|
25
|
+
"uuid": "^10.0.0",
|
|
26
|
+
"cors": "^2.8.5"
|
|
27
|
+
},
|
|
28
|
+
"author": "Low Watt Labs",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/jchandler187/clawsec.git"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"security",
|
|
36
|
+
"audit",
|
|
37
|
+
"clawhub",
|
|
38
|
+
"skills",
|
|
39
|
+
"verification",
|
|
40
|
+
"threat-intel"
|
|
41
|
+
]
|
|
42
|
+
}
|