@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,103 @@
1
+ #!/usr/bin/env python3
2
+ """ClawSec Intel Manifest Manager
3
+
4
+ Manages {INTEL_DIR}/manifest.json — tracks source sync status.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import sys
10
+ from datetime import datetime, timezone
11
+
12
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'common'))
13
+ from config import INTEL_DIR
14
+
15
+ MANIFEST_PATH = os.path.join(INTEL_DIR, "manifest.json")
16
+
17
+ def load_manifest():
18
+ """Load manifest, create empty if missing."""
19
+ if os.path.exists(MANIFEST_PATH):
20
+ with open(MANIFEST_PATH, 'r') as f:
21
+ try:
22
+ return json.load(f)
23
+ except json.JSONDecodeError:
24
+ pass
25
+ return {
26
+ "version": "2.0",
27
+ "sources": [],
28
+ "updated_at": None
29
+ }
30
+
31
+ def save_manifest(manifest):
32
+ """Atomic write manifest."""
33
+ manifest["updated_at"] = datetime.now(timezone.utc).isoformat()
34
+ tmp = MANIFEST_PATH + ".new"
35
+ with open(tmp, 'w') as f:
36
+ json.dump(manifest, f, indent=2, sort_keys=False)
37
+ os.rename(tmp, MANIFEST_PATH)
38
+
39
+ def update_source(name, url="", record_count=0, status="success", error_msg=""):
40
+ """Update or add a source entry in the manifest."""
41
+ manifest = load_manifest()
42
+ found = False
43
+ for src in manifest["sources"]:
44
+ if src["name"] == name:
45
+ src["url"] = url
46
+ src["last_sync"] = datetime.now(timezone.utc).isoformat()
47
+ src["record_count"] = record_count
48
+ src["status"] = status
49
+ if error_msg:
50
+ src["error"] = error_msg
51
+ elif "error" in src:
52
+ del src["error"]
53
+ found = True
54
+ break
55
+ if not found:
56
+ entry = {
57
+ "name": name,
58
+ "url": url,
59
+ "last_sync": datetime.now(timezone.utc).isoformat(),
60
+ "record_count": record_count,
61
+ "status": status
62
+ }
63
+ if error_msg:
64
+ entry["error"] = error_msg
65
+ manifest["sources"].append(entry)
66
+ save_manifest(manifest)
67
+
68
+ def get_source(name):
69
+ """Get a source entry by name."""
70
+ manifest = load_manifest()
71
+ for src in manifest["sources"]:
72
+ if src["name"] == name:
73
+ return src
74
+ return None
75
+
76
+ def get_status():
77
+ """Return manifest summary for status display."""
78
+ return load_manifest()
79
+
80
+ def main():
81
+ if len(sys.argv) < 2:
82
+ print("Usage: manifest.py <update|status> [args...]")
83
+ sys.exit(1)
84
+
85
+ cmd = sys.argv[1]
86
+ if cmd == "update":
87
+ if len(sys.argv) < 4:
88
+ print("Usage: manifest.py update <name> <record_count> [status] [error_msg]")
89
+ sys.exit(1)
90
+ name = sys.argv[2]
91
+ count = int(sys.argv[3])
92
+ status = sys.argv[4] if len(sys.argv) > 4 else "success"
93
+ error = sys.argv[5] if len(sys.argv) > 5 else ""
94
+ update_source(name, record_count=count, status=status, error_msg=error)
95
+ elif cmd == "status":
96
+ manifest = get_status()
97
+ print(json.dumps(manifest, indent=2))
98
+ else:
99
+ print(f"Unknown command: {cmd}")
100
+ sys.exit(1)
101
+
102
+ if __name__ == "__main__":
103
+ main()
@@ -0,0 +1,24 @@
1
+ # ⚡ Low Watt Labs
2
+ # CISA KEV sync
3
+ set -euo pipefail
4
+
5
+ source "$(dirname "$0")/../../common/config.sh"
6
+ source "$(dirname "$0")/../../common/colors.sh"
7
+ source "$(dirname "$0")/../../common/log.sh"
8
+ source "$(dirname "$0")/../../common/utils.sh"
9
+
10
+ INTEL_DIR="${CLAWSEC_INTEL_DIR}"
11
+ TARGET="${INTEL_DIR}/cisa-kev/known_exploited_vulnerabilities.json"
12
+ URL="https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
13
+ MANIFEST_PY="$(dirname "$0")/../manifest.py"
14
+
15
+ log_info "Syncing CISA KEV..."
16
+
17
+ if safe_download "$URL" "$TARGET" "jq empty"; then
18
+ count=$(jq '.vulnerabilities | length' "$TARGET" 2>/dev/null || echo 0)
19
+ python3 "$MANIFEST_PY" update cisa-kev "$count" success
20
+ echo -e "${CHECKMARK} CISA KEV: ${count} vulnerabilities"
21
+ else
22
+ python3 "$MANIFEST_PY" update cisa-kev 0 failed "download failed"
23
+ echo -e "${CROSSMARK} CISA KEV: download failed (using stale cache)"
24
+ fi
@@ -0,0 +1,34 @@
1
+ # ⚡ Low Watt Labs
2
+ # EPSS sync
3
+ set -euo pipefail
4
+
5
+ source "$(dirname "$0")/../../common/config.sh"
6
+ source "$(dirname "$0")/../../common/colors.sh"
7
+ source "$(dirname "$0")/../../common/log.sh"
8
+ source "$(dirname "$0")/../../common/utils.sh"
9
+
10
+ INTEL_DIR="${CLAWSEC_INTEL_DIR}"
11
+ TARGET="${INTEL_DIR}/epss/epss_scores-current.csv"
12
+ URL="https://epss.cyentia.com/epss_scores-current.csv.gz"
13
+ MANIFEST_PY="$(dirname "$0")/../manifest.py"
14
+
15
+ log_info "Syncing EPSS..."
16
+
17
+ gz_tmp=$(mktemp "/tmp/epss.XXXXXX.csv.gz")
18
+ if curl -fsSL --max-time 120 --retry 3 --retry-delay 5 "$URL" -o "$gz_tmp"; then
19
+ csv_tmp=$(mktemp "${TARGET}.XXXXXX.new")
20
+ if gunzip -c "$gz_tmp" > "$csv_tmp"; then
21
+ count=$(($(wc -l < "$csv_tmp") - 1)) # minus header
22
+ mv -f "$csv_tmp" "$TARGET"
23
+ python3 "$MANIFEST_PY" update epss "$count" success
24
+ echo -e "${CHECKMARK} EPSS: ${count} CVE scores"
25
+ else
26
+ rm -f "$csv_tmp"
27
+ python3 "$MANIFEST_PY" update epss 0 failed "gunzip failed"
28
+ echo -e "${CROSSMARK} EPSS: decompression failed"
29
+ fi
30
+ else
31
+ python3 "$MANIFEST_PY" update epss 0 failed "download failed"
32
+ echo -e "${CROSSMARK} EPSS: download failed"
33
+ fi
34
+ rm -f "$gz_tmp"
@@ -0,0 +1,27 @@
1
+ # ⚡ Low Watt Labs
2
+ # Feodo Tracker sync (abuse.ch)
3
+ set -euo pipefail
4
+
5
+ source "$(dirname "$0")/../../common/config.sh"
6
+ source "$(dirname "$0")/../../common/colors.sh"
7
+ source "$(dirname "$0")/../../common/log.sh"
8
+ source "$(dirname "$0")/../../common/utils.sh"
9
+
10
+ INTEL_DIR="${CLAWSEC_INTEL_DIR}"
11
+ TARGET="${INTEL_DIR}/feodo/c2_ips.csv"
12
+ URL="https://feodotracker.abuse.ch/downloads/ipblocklist.csv"
13
+ MANIFEST_PY="$(dirname "$0")/../manifest.py"
14
+
15
+ log_info "Syncing Feodo Tracker..."
16
+
17
+ tmp=$(mktemp "${TARGET}.XXXXXX.new")
18
+ if curl -fsSL --max-time 60 --retry 3 --retry-delay 5 "$URL" -o "$tmp"; then
19
+ count=$(grep -cv '^#' "$tmp" 2>/dev/null || echo 0)
20
+ mv -f "$tmp" "$TARGET"
21
+ python3 "$MANIFEST_PY" update feodo "$count" success
22
+ echo -e "${CHECKMARK} Feodo Tracker: ${count} C2 IPs"
23
+ else
24
+ rm -f "$tmp"
25
+ python3 "$MANIFEST_PY" update feodo 0 failed "download failed"
26
+ echo -e "${CROSSMARK} Feodo Tracker: download failed"
27
+ fi
@@ -0,0 +1,22 @@
1
+ # ⚡ Low Watt Labs
2
+ set -euo pipefail
3
+ source "$(dirname "$0")/../../common/config.sh"
4
+ source "$(dirname "$0")/../../common/colors.sh"
5
+ source "$(dirname "$0")/../../common/log.sh"
6
+ source "$(dirname "$0")/../../common/utils.sh"
7
+
8
+ INTEL_DIR="${CLAWSEC_INTEL_DIR}"
9
+ TARGET_CSV="${INTEL_DIR}/malwarebazaar/recent_hashes.csv"
10
+ MANIFEST_PY="$(dirname "$0")/../manifest.py"
11
+ CSV_URL="https://bazaar.abuse.ch/export/csv/recent/"
12
+
13
+ log_info "Syncing MalwareBazaar..."
14
+ if safe_download "$CSV_URL" "$TARGET_CSV"; then
15
+ count=$(($(wc -l < "$TARGET_CSV") - 8))
16
+ [[ $count -lt 0 ]] && count=0
17
+ python3 "$MANIFEST_PY" update malwarebazaar "$count" success
18
+ echo -e "${CHECKMARK} MalwareBazaar: ${count} samples"
19
+ else
20
+ python3 "$MANIFEST_PY" update malwarebazaar 0 failed "download failed"
21
+ echo -e "${CROSSMARK} MalwareBazaar: download failed"
22
+ fi
@@ -0,0 +1,101 @@
1
+ # ⚡ Low Watt Labs
2
+ # OSV sync - npm and PyPI ecosystems
3
+ # Downloads OSV advisories and builds a consolidated index for fast lookups
4
+ set -euo pipefail
5
+
6
+ # Run at low priority so this never starves other processes
7
+ renice -n 15 $$ >/dev/null 2>&1 || true
8
+ ionice -c 3 -p $$ 2>/dev/null || true
9
+
10
+ source "$(dirname "$0")/../../common/config.sh"
11
+ source "$(dirname "$0")/../../common/colors.sh"
12
+ source "$(dirname "$0")/../../common/log.sh"
13
+ source "$(dirname "$0")/../../common/utils.sh"
14
+
15
+ INTEL_DIR="${CLAWSEC_INTEL_DIR}"
16
+ OSV_DIR="${INTEL_DIR}/osv"
17
+ MANIFEST_PY="$(dirname "$0")/../manifest.py"
18
+ URL_BASE="https://osv-vulnerabilities.storage.googleapis.com"
19
+
20
+ ECOSYSTEMS=("npm" "PyPI")
21
+ total=0
22
+ any_fail=0
23
+
24
+ for eco in "${ECOSYSTEMS[@]}"; do
25
+ log_info "Syncing OSV $eco..."
26
+ eco_dir="${OSV_DIR}/${eco}"
27
+ mkdir -p "$eco_dir"
28
+
29
+ zip_url="${URL_BASE}/${eco}/all.zip"
30
+ zip_tmp=$(mktemp "/tmp/osv-${eco}.XXXXXX.zip")
31
+
32
+ if curl -fsSL --max-time 300 --retry 3 --retry-delay 5 "$zip_url" -o "$zip_tmp"; then
33
+ extract_tmp=$(mktemp -d "/tmp/osv-${eco}-extract.XXXXXX")
34
+ if unzip -q -o "$zip_tmp" -d "$extract_tmp" 2>/dev/null; then
35
+ # Clean old JSON files and broken symlinks before replacing
36
+ find "$eco_dir" -maxdepth 1 -name '*.json' -delete 2>/dev/null || true
37
+ find "$eco_dir" -maxdepth 1 -type l ! -exec test -e {} \; -delete 2>/dev/null || true
38
+ find "$extract_tmp" -maxdepth 1 -name '*.json' -exec mv -t "$eco_dir" {} +
39
+ count=$(find "$eco_dir" -maxdepth 1 -name '*.json' | wc -l)
40
+ total=$((total + count))
41
+ log_info "OSV $eco: $count advisories"
42
+ else
43
+ any_fail=1
44
+ log_warn "OSV $eco: extraction failed"
45
+ fi
46
+ rm -rf "$extract_tmp"
47
+ else
48
+ any_fail=1
49
+ log_warn "OSV $eco: download failed"
50
+ fi
51
+ rm -f "$zip_tmp"
52
+ done
53
+
54
+ # Build consolidated index: package_name -> list of advisory filenames
55
+ log_info "Building OSV package index..."
56
+ for eco in "${ECOSYSTEMS[@]}"; do
57
+ eco_dir="${OSV_DIR}/${eco}"
58
+ index_file="${eco_dir}/index.json"
59
+
60
+ if [[ -d "$eco_dir" ]]; then
61
+ # Build index using Python for speed
62
+ python3 -c "
63
+ import json, os, sys
64
+ eco_dir = sys.argv[1]
65
+ index = {}
66
+ for fname in os.listdir(eco_dir):
67
+ if not fname.endswith('.json') or fname == 'index.json':
68
+ continue
69
+ fpath = os.path.join(eco_dir, fname)
70
+ try:
71
+ with open(fpath) as f:
72
+ adv = json.load(f)
73
+ for affected in adv.get('affected', []):
74
+ pkg = affected.get('package', {})
75
+ name = pkg.get('name', '')
76
+ if name:
77
+ key = name.lower()
78
+ if key not in index:
79
+ index[key] = []
80
+ index[key].append(fname)
81
+ except (json.JSONDecodeError, KeyError, OSError):
82
+ continue
83
+ # Write index atomically
84
+ tmp = index_file + '.new'
85
+ with open(tmp, 'w') as f:
86
+ json.dump(index, f)
87
+ os.rename(tmp, index_file)
88
+ " "$eco_dir"
89
+ index_count=$(python3 -c "import json; print(len(json.load(open('$index_file'))))" 2>/dev/null || echo "?")
90
+ log_info "OSV $eco index: $index_count packages"
91
+ fi
92
+ done
93
+
94
+ status="success"
95
+ [[ $any_fail -eq 1 ]] && status="partial"
96
+ python3 "$MANIFEST_PY" update osv "$total" "$status"
97
+ if [[ $any_fail -eq 0 ]]; then
98
+ echo -e "${CHECKMARK} OSV: ${total} advisories (npm + PyPI, indexed)"
99
+ else
100
+ echo -e "${WARNMARK} OSV: partial sync — ${total} advisories"
101
+ fi
@@ -0,0 +1,28 @@
1
+ # ⚡ Low Watt Labs
2
+ set -euo pipefail
3
+ source "$(dirname "$0")/../../common/config.sh"
4
+ source "$(dirname "$0")/../../common/colors.sh"
5
+ source "$(dirname "$0")/../../common/log.sh"
6
+ source "$(dirname "$0")/../../common/utils.sh"
7
+
8
+ INTEL_DIR="${CLAWSEC_INTEL_DIR}"
9
+ REPO_DIR="${INTEL_DIR}/semgrep-rules/repo"
10
+ MANIFEST_PY="$(dirname "$0")/../manifest.py"
11
+
12
+ log_info "Syncing Semgrep rules (returntocorp/semgrep-rules)..."
13
+
14
+ if [[ -d "$REPO_DIR/.git" ]]; then
15
+ git -C "$REPO_DIR" pull --quiet 2>/dev/null && status="success" || status="failed"
16
+ else
17
+ rm -rf "$REPO_DIR"
18
+ git clone --depth 1 https://github.com/returntocorp/semgrep-rules.git "$REPO_DIR" 2>/dev/null && status="success" || status="failed"
19
+ fi
20
+
21
+ if [[ "$status" == "success" ]]; then
22
+ count=$(find "$REPO_DIR" -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null | wc -l)
23
+ python3 "$MANIFEST_PY" update semgrep-rules "$count" success
24
+ echo -e "${CHECKMARK} Semgrep rules: ${count} rule files"
25
+ else
26
+ python3 "$MANIFEST_PY" update semgrep-rules 0 failed "git pull/clone failed"
27
+ echo -e "${CROSSMARK} Semgrep rules: git sync failed"
28
+ fi
@@ -0,0 +1,28 @@
1
+ # ⚡ Low Watt Labs
2
+ # ThreatFox sync (abuse.ch)
3
+ set -euo pipefail
4
+
5
+ source "$(dirname "$0")/../../common/config.sh"
6
+ source "$(dirname "$0")/../../common/colors.sh"
7
+ source "$(dirname "$0")/../../common/log.sh"
8
+ source "$(dirname "$0")/../../common/utils.sh"
9
+
10
+ INTEL_DIR="${CLAWSEC_INTEL_DIR}"
11
+ TARGET="${INTEL_DIR}/threatfox/iocs.csv"
12
+ URL="https://threatfox.abuse.ch/export/csv/recent/"
13
+ MANIFEST_PY="$(dirname "$0")/../manifest.py"
14
+
15
+ log_info "Syncing ThreatFox..."
16
+
17
+ tmp=$(mktemp "${TARGET}.XXXXXX.new")
18
+ if curl -fsSL --max-time 120 --retry 3 --retry-delay 5 "$URL" -o "$tmp"; then
19
+ # Count actual data lines (skip comment lines starting with #)
20
+ count=$(grep -cv '^#' "$tmp" 2>/dev/null || echo 0)
21
+ mv -f "$tmp" "$TARGET"
22
+ python3 "$MANIFEST_PY" update threatfox "$count" success
23
+ echo -e "${CHECKMARK} ThreatFox: ${count} IOCs"
24
+ else
25
+ rm -f "$tmp"
26
+ python3 "$MANIFEST_PY" update threatfox 0 failed "download failed"
27
+ echo -e "${CROSSMARK} ThreatFox: download failed"
28
+ fi
@@ -0,0 +1,42 @@
1
+ # ⚡ Low Watt Labs
2
+ set -euo pipefail
3
+ source "$(dirname "$0")/../../common/config.sh"
4
+ source "$(dirname "$0")/../../common/colors.sh"
5
+ source "$(dirname "$0")/../../common/log.sh"
6
+ source "$(dirname "$0")/../../common/utils.sh"
7
+
8
+ INTEL_DIR="${CLAWSEC_INTEL_DIR}"
9
+ TARGET="${INTEL_DIR}/urlhaus/urls.csv"
10
+ URL="https://urlhaus.abuse.ch/downloads/csv/"
11
+ MANIFEST_PY="$(dirname "$0")/../manifest.py"
12
+ # The URLhaus ZIP contains a file named "csv.txt" — we extract it
13
+ # to a stable path so ioc-match.py always knows where to find it.
14
+ URLHAUS_INNER_FILE="csv.txt"
15
+
16
+ log_info "Syncing URLhaus..."
17
+ mkdir -p "${INTEL_DIR}/urlhaus"
18
+ zip_tmp=$(mktemp "/tmp/urlhaus.XXXXXX.zip")
19
+ if curl -fsSL --max-time 120 --retry 3 --retry-delay 5 "$URL" -o "$zip_tmp"; then
20
+ csv_tmp=$(mktemp "${TARGET}.XXXXXX.new")
21
+ if unzip -p "$zip_tmp" "$URLHAUS_INNER_FILE" > "$csv_tmp" 2>/dev/null; then
22
+ count=$(($(wc -l < "$csv_tmp") - 1)) # minus header
23
+ # Validate extracted CSV is text, not raw ZIP
24
+ if file "$csv_tmp" | grep -qi "zip\|archive"; then
25
+ rm -f "$csv_tmp"
26
+ python3 "$MANIFEST_PY" update urlhaus 0 failed "extracted file is still a ZIP"
27
+ echo -e "${CROSSMARK} URLhaus: extracted file is still a ZIP (sync may have failed)"
28
+ else
29
+ mv -f "$csv_tmp" "$TARGET"
30
+ fi
31
+ python3 "$MANIFEST_PY" update urlhaus "$count" success
32
+ echo -e "${CHECKMARK} URLhaus: ${count} malicious URLs"
33
+ else
34
+ rm -f "$csv_tmp"
35
+ python3 "$MANIFEST_PY" update urlhaus 0 failed "unzip failed"
36
+ echo -e "${CROSSMARK} URLhaus: decompression failed"
37
+ fi
38
+ else
39
+ python3 "$MANIFEST_PY" update urlhaus 0 failed "download failed"
40
+ echo -e "${CROSSMARK} URLhaus: download failed"
41
+ fi
42
+ rm -f "$zip_tmp"
@@ -0,0 +1,38 @@
1
+ # ⚡ Low Watt Labs
2
+ # YARA rules sync (Neo23x0/signature-base)
3
+ set -euo pipefail
4
+
5
+ source "$(dirname "$0")/../../common/config.sh"
6
+ source "$(dirname "$0")/../../common/colors.sh"
7
+ source "$(dirname "$0")/../../common/log.sh"
8
+ source "$(dirname "$0")/../../common/utils.sh"
9
+
10
+ INTEL_DIR="${CLAWSEC_INTEL_DIR}"
11
+ REPO_DIR="${INTEL_DIR}/yara-rules/repo"
12
+ MANIFEST_PY="$(dirname "$0")/../manifest.py"
13
+
14
+ log_info "Syncing YARA rules (Neo23x0/signature-base)..."
15
+
16
+ if [[ -d "$REPO_DIR/.git" ]]; then
17
+ if git -C "$REPO_DIR" pull --quiet 2>/dev/null; then
18
+ status="success"
19
+ else
20
+ status="failed"
21
+ fi
22
+ else
23
+ rm -rf "$REPO_DIR"
24
+ if git clone --depth 1 https://github.com/Neo23x0/signature-base.git "$REPO_DIR" 2>/dev/null; then
25
+ status="success"
26
+ else
27
+ status="failed"
28
+ fi
29
+ fi
30
+
31
+ if [[ "$status" == "success" ]]; then
32
+ count=$(find "$REPO_DIR/yara" -name '*.yar' -o -name '*.yara' 2>/dev/null | wc -l)
33
+ python3 "$MANIFEST_PY" update yara-rules "$count" success
34
+ echo -e "${CHECKMARK} YARA rules: ${count} rule files"
35
+ else
36
+ python3 "$MANIFEST_PY" update yara-rules 0 failed "git pull/clone failed"
37
+ echo -e "${CROSSMARK} YARA rules: git sync failed"
38
+ fi
@@ -0,0 +1,96 @@
1
+ # ⚡ Low Watt Labs — ClawSec Intel Sync
2
+ # ClawSec v2 - Intel Sync Orchestrator
3
+ # Runs all intel source sync jobs, gracefully handling failures
4
+ set -euo pipefail
5
+
6
+ VERSION="2.0.0"
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ SOURCES_DIR="${SCRIPT_DIR}/sources"
9
+
10
+ source "${SCRIPT_DIR}/../common/config.sh"
11
+ source "${SCRIPT_DIR}/../common/colors.sh"
12
+ source "${SCRIPT_DIR}/../common/log.sh"
13
+
14
+ usage() {
15
+ echo "ClawSec v${VERSION} — Intel Sync"
16
+ echo ""
17
+ echo "Usage: sync.sh [OPTIONS] [SOURCE...]"
18
+ echo ""
19
+ echo "Options:"
20
+ echo " --all Sync all sources (default)"
21
+ echo " --list List available sources"
22
+ echo " --status Show current cache status"
23
+ echo " --json Output status as JSON"
24
+ echo " --help Show this help"
25
+ echo ""
26
+ echo "Sources: cisa-kev, osv, epss, malwarebazaar, urlhaus,"
27
+ echo " threatfox, feodo, yara-rules, semgrep-rules"
28
+ exit 0
29
+ }
30
+
31
+ ALL_SOURCES=(cisa-kev osv epss malwarebazaar urlhaus threatfox feodo yara-rules semgrep-rules)
32
+ requested_sources=()
33
+ json_output=0
34
+
35
+ while [[ $# -gt 0 ]]; do
36
+ case "$1" in
37
+ --all) shift ;;
38
+ --list)
39
+ echo "Available intel sources:"
40
+ for s in "${ALL_SOURCES[@]}"; do echo " - $s"; done
41
+ exit 0 ;;
42
+ --status|--status-only)
43
+ python3 "${SCRIPT_DIR}/manifest.py" status
44
+ exit 0 ;;
45
+ --json)
46
+ json_output=1
47
+ shift ;;
48
+ --help|-h)
49
+ usage ;;
50
+ -*)
51
+ echo "Unknown option: $1" >&2
52
+ exit 1 ;;
53
+ *)
54
+ requested_sources+=("$1")
55
+ shift ;;
56
+ esac
57
+ done
58
+
59
+ # Default: all sources
60
+ [[ ${#requested_sources[@]} -eq 0 ]] && requested_sources=("${ALL_SOURCES[@]}")
61
+
62
+ echo -e "${BOLD}╔══════════════════════════════════════╗${RESET}"
63
+ echo -e "${BOLD}║ ClawSec v${VERSION} — Intel Sync ║${RESET}"
64
+ echo -e "${BOLD}╚══════════════════════════════════════╝${RESET}"
65
+ echo ""
66
+ echo -e " Syncing ${BOLD}${#requested_sources[@]}${RESET} source(s): ${CYAN}${requested_sources[*]}${RESET}"
67
+ echo ""
68
+
69
+ start_time=$(date +%s)
70
+ fail_count=0
71
+
72
+ for src in "${requested_sources[@]}"; do
73
+ script="${SOURCES_DIR}/${src}.sh"
74
+ if [[ -x "$script" ]]; then
75
+ if ! "$script"; then
76
+ fail_count=$((fail_count + 1))
77
+ fi
78
+ else
79
+ echo -e "${WARNMARK} Unknown source: $src (no script at $script)"
80
+ fail_count=$((fail_count + 1))
81
+ fi
82
+ done
83
+
84
+ end_time=$(date +%s)
85
+ elapsed=$((end_time - start_time))
86
+
87
+ echo ""
88
+ if [[ $fail_count -eq 0 ]]; then
89
+ echo -e "${CHECKMARK} Sync complete in ${elapsed}s — all sources fresh"
90
+ else
91
+ echo -e "${WARNMARK} Sync complete in ${elapsed}s — ${fail_count} source(s) had issues (stale data preserved)"
92
+ fi
93
+
94
+ if [[ $json_output -eq 1 ]]; then
95
+ python3 "${SCRIPT_DIR}/manifest.py" status
96
+ fi