67eaf409b9
Build and push soc-collector / build (push) Successful in 6s
All enabled sources merge into a single report keyed by SOC_SLUG, criticals first. One server = one report; run per server with a distinct SOC_SLUG. Avoids the 4-reports-per-host explosion when scanning several servers. README updated.
91 lines
3.6 KiB
Python
91 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""soc-collector — scan an estate and push ONE consolidated report to SOC Center.
|
|
|
|
Each run produces a single report (all enabled sources merged), keyed by SOC_SLUG —
|
|
so one server = one report. Run it per server with a distinct SOC_SLUG and you get one
|
|
report each, not one-per-source-per-server. Re-running the same slug lets SOC Center
|
|
version it, diff each run, and alert only on new findings.
|
|
|
|
Runs once and exits (schedule with cron / a systemd timer). Configuration is entirely
|
|
via env vars — see the README. Required: SOC_URL, SOC_TOKEN. A source self-skips when
|
|
its inputs are absent (no SOC_TARGETS -> no nmap; no Docker socket -> no Trivy / config).
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
import soc
|
|
from sources import cve, config_drift, surface, tls
|
|
|
|
SEV_ORDER = {"crit": 0, "high": 1, "med": 2, "low": 3}
|
|
|
|
|
|
def _list(name):
|
|
return [x.strip() for x in os.environ.get(name, "").replace(";", ",").split(",") if x.strip()]
|
|
|
|
|
|
def main():
|
|
url = soc._env("SOC_URL", required=True)
|
|
token = soc._env("SOC_TOKEN", required=True)
|
|
visibility = os.environ.get("SOC_VISIBILITY", "private")
|
|
base_tags = _list("SOC_TAGS") or ["collector"]
|
|
want = set(_list("SOC_SOURCES") or ["surface", "cve", "tls", "config"])
|
|
targets, domains = _list("SOC_TARGETS"), _list("SOC_DOMAINS")
|
|
# One report per run; the slug names this server. Fall back to the old prefix var.
|
|
slug = (os.environ.get("SOC_SLUG") or os.environ.get("SOC_SLUG_PREFIX") or "soc-scan").strip()
|
|
title = os.environ.get("SOC_TITLE") or f"Security scan — {slug}"
|
|
|
|
runners = []
|
|
if "surface" in want:
|
|
runners.append(("surface", lambda: surface.run(targets)))
|
|
if "cve" in want:
|
|
runners.append(("cve", cve.run))
|
|
if "tls" in want:
|
|
runners.append(("tls", lambda: tls.run(domains)))
|
|
if "config" in want:
|
|
runners.append(("config", config_drift.run))
|
|
|
|
print(f"[soc-collector] target={url} slug={slug} sources={sorted(want)}")
|
|
findings, surface_rows, good, scope, ran = [], [], [], [], []
|
|
for key, fn in runners:
|
|
print(f"[soc-collector] source: {key}")
|
|
try:
|
|
data = fn()
|
|
except Exception as e:
|
|
print(f" ! {key} errored: {e}")
|
|
continue
|
|
if data is None:
|
|
print(f" - {key} skipped (no targets / no Docker socket)")
|
|
continue
|
|
ran.append(key)
|
|
n = data.get("findings", [])
|
|
findings += n
|
|
surface_rows += data.get("surface", [])
|
|
good += data.get("good", [])
|
|
if data.get("scope"):
|
|
scope.append(f"{key}: {data['scope']}")
|
|
print(f" ✓ {key}: {len(n)} finding(s)")
|
|
|
|
if not ran:
|
|
print("[soc-collector] no sources ran (need SOC_TARGETS / SOC_DOMAINS / a Docker socket)")
|
|
sys.exit(1)
|
|
|
|
findings.sort(key=lambda f: SEV_ORDER.get(f.get("sev"), 9)) # criticals first
|
|
data = {"scope": " · ".join(scope), "findings": findings,
|
|
"surface": surface_rows, "good": good}
|
|
|
|
status, resp = soc.post_report(url, token, slug, title, data,
|
|
visibility=visibility, tags=base_tags + ran)
|
|
if status in (200, 201):
|
|
d = resp.get("diff", {})
|
|
print(f"[soc-collector] ✓ {slug}: {len(findings)} finding(s) from {ran}, "
|
|
f"{'created' if resp.get('created') else 'updated'} "
|
|
f"(+{d.get('added', 0)}/-{d.get('removed', 0)}/~{d.get('changed', 0)}) "
|
|
f"alerted={resp.get('alerted')}")
|
|
sys.exit(0)
|
|
print(f"[soc-collector] ! {slug}: HTTP {status} {resp}")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|