@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,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
|