7336b17f79
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.
77 lines
3.0 KiB
Python
77 lines
3.0 KiB
Python
"""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}
|