soc-collector: run-anywhere scanner that pushes reports to SOC Center
Build and push soc-collector / build (push) Successful in 31s
Build and push soc-collector / build (push) Successful in 31s
A single Docker image, configured entirely by env vars (SOC_URL + SOC_TOKEN + targets). Runs once and exits; schedule with cron. Four self-skipping sources: - surface nmap open-port/service scan (SOC_TARGETS) - cve Trivy HIGH/CRITICAL scan of running images (Docker socket) - tls cert-expiry / weak-TLS / missing-headers (SOC_DOMAINS) - config 0.0.0.0 binds, --privileged, rw docker-socket mounts (Docker socket) Stable slugs per source so SOC Center versions/diffs/alerts. Pure stdlib; image adds nmap + trivy. MIT, intended as a public Ethica repo.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"""TLS & web-exposure source: cert expiry, weak TLS, missing security headers per domain."""
|
||||
import datetime
|
||||
import http.client
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
from soc import finding
|
||||
|
||||
SECURITY_HEADERS = ["strict-transport-security", "content-security-policy",
|
||||
"x-content-type-options", "x-frame-options"]
|
||||
|
||||
|
||||
def _peer(host, port=443):
|
||||
ctx = ssl.create_default_context()
|
||||
with socket.create_connection((host, port), timeout=15) as sock:
|
||||
with ctx.wrap_socket(sock, server_hostname=host) as ss:
|
||||
return ss.getpeercert(), ss.version()
|
||||
|
||||
|
||||
def run(domains):
|
||||
if not domains:
|
||||
return None
|
||||
findings, good = [], []
|
||||
|
||||
for host in domains:
|
||||
host = host.strip()
|
||||
if not host:
|
||||
continue
|
||||
try:
|
||||
cert, ver = _peer(host)
|
||||
except Exception as e:
|
||||
findings.append(finding(
|
||||
f"tls-conn-{host}", "med", f"TLS connection to {host} failed", host=host,
|
||||
evidence=str(e), impact=["the site may be down or its TLS misconfigured"],
|
||||
fix=["check the service and its certificate"]))
|
||||
continue
|
||||
|
||||
not_after = cert.get("notAfter")
|
||||
try:
|
||||
exp = datetime.datetime.strptime(not_after, "%b %d %H:%M:%S %Y %Z")
|
||||
days = (exp - datetime.datetime.utcnow()).days
|
||||
sev = "crit" if days < 7 else "high" if days < 21 else "med" if days < 40 else None
|
||||
if sev:
|
||||
findings.append(finding(
|
||||
f"tls-exp-{host}", sev, f"Certificate for {host} expires in {days} days",
|
||||
host=host, evidence=f"notAfter={not_after}",
|
||||
impact=["an outage the moment it expires"],
|
||||
fix=["renew it / verify auto-renewal (e.g. Traefik 'le' resolver)"]))
|
||||
else:
|
||||
good.append(f"{host}: certificate valid for {days} more days")
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if ver in ("TLSv1", "TLSv1.1"):
|
||||
findings.append(finding(
|
||||
f"tls-weak-{host}", "med", f"{host} negotiated deprecated {ver}", host=host,
|
||||
evidence=ver, impact=["deprecated, downgradeable TLS"],
|
||||
fix=["disable TLS < 1.2 at the edge"]))
|
||||
|
||||
try:
|
||||
conn = http.client.HTTPSConnection(host, timeout=15)
|
||||
conn.request("HEAD", "/")
|
||||
resp = conn.getresponse()
|
||||
present = {k.lower() for k, _ in resp.getheaders()}
|
||||
conn.close()
|
||||
missing = [h for h in SECURITY_HEADERS if h not in present]
|
||||
if missing:
|
||||
findings.append(finding(
|
||||
f"hdr-{host}", "low", f"{host} is missing {len(missing)} security header(s)",
|
||||
host=host, evidence=", ".join(missing),
|
||||
impact=["weaker browser-side protections"],
|
||||
fix=["add at the edge/app: " + ", ".join(missing)]))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"scope": ", ".join(domains), "findings": findings, "good": good}
|
||||
Reference in New Issue
Block a user