@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,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
+ }
@@ -0,0 +1,6 @@
1
+ # ClawSec v2 Python dependencies
2
+ requests>=2.31.0
3
+ yara-python>=4.5.0
4
+ rich>=13.7.0
5
+ semgrep>=1.50.0
6
+ packaging>=23.0