#!/usr/bin/env python3 """soc-collector — scan an estate and push reports to SOC Center. Runs once and exits (schedule it with cron / a systemd timer / a CI job). Each enabled source becomes one report, re-ingested under a stable slug so SOC Center versions it, diffs each run, and alerts only on new findings. 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-drift), so the same image is safe to run anywhere. """ import os import sys import soc from sources import cve, config_drift, surface, tls 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) prefix = os.environ.get("SOC_SLUG_PREFIX", "").strip() 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") def slug(s): return f"{prefix}-{s}" if prefix else s plan = [] if "surface" in want: plan.append(("surface", "Attack surface (nmap)", slug("attack-surface"), lambda: surface.run(targets))) if "cve" in want: plan.append(("cve", "Container CVEs (Trivy)", slug("container-cves"), cve.run)) if "tls" in want: plan.append(("tls", "TLS & web exposure", slug("tls-exposure"), lambda: tls.run(domains))) if "config" in want: plan.append(("config", "Config drift (Docker)", slug("config-drift"), config_drift.run)) print(f"[soc-collector] target={url} sources={sorted(want)}") rc = 0 for key, title, rslug, fn in plan: print(f"[soc-collector] source: {key}") try: data = fn() except Exception as e: print(f" ! {key} errored: {e}") rc = 1 continue if data is None: print(f" - {key} skipped (no targets / no Docker socket)") continue n = len(data.get("findings", [])) status, resp = soc.post_report(url, token, rslug, title, data, visibility=visibility, tags=base_tags + [key]) if status in (200, 201): d = resp.get("diff", {}) print(f" ✓ {rslug}: {n} finding(s), {'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')}") else: print(f" ! {rslug}: HTTP {status} {resp}") rc = 1 sys.exit(rc) if __name__ == "__main__": main()