import datetime, json, os, ssl, sys, time, urllib.error, urllib.parse, urllib.request DEFAULT_BASE = "https://crt.sh" def _cfg(): return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) def _inputs(): return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) def _ctx(cfg): if cfg.get("insecure") in (True, "true", "True", 1, "1"): c = ssl.create_default_context() c.check_hostname = False c.verify_mode = ssl.CERT_NONE return c return None def _get(url, headers, cfg, timeout): req = urllib.request.Request(url, headers=dict({"Accept": "application/json", "User-Agent": "riposte-soar"}, **headers)) with urllib.request.urlopen(req, timeout=timeout, context=_ctx(cfg)) as r: return json.loads(r.read() or b"[]") def _crtsh(cfg): base = (str(cfg.get("base_url") or "").strip() or DEFAULT_BASE).rstrip("/") # Riposte stops the script at 60 s: two tries at most, Cert Spotter keeps the rest. last = "" for attempt in range(2): try: return len(_get(base + "/?" + urllib.parse.urlencode({"q": "crt.sh", "output": "json", "exclude": "expired"}), {}, cfg, 20)), "" except urllib.error.HTTPError as e: last = "HTTP " + str(e.code) if e.code not in (502, 503, 504): break except Exception as e: last = str(getattr(e, "reason", e)) if attempt == 0: time.sleep(3) return None, last def _certspotter(cfg): key = str(cfg.get("certspotter_api_key") or "").strip() base = (str(cfg.get("certspotter_url") or "").strip() or "https://api.certspotter.com").rstrip("/") try: _get(base + "/v1/issuances?domain=crt.sh", {"Authorization": "Bearer " + key} if key else {}, cfg, 15) return True, "" except urllib.error.HTTPError as e: return False, "HTTP " + str(e.code) + (" (quota reached)" if e.code == 429 else "") except Exception as e: return False, str(getattr(e, "reason", e)) def _run(fn): try: print(json.dumps(fn(_cfg(), _inputs()))) except urllib.error.HTTPError as e: print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")[:2000]})) sys.exit(1) except Exception as e: print(json.dumps({"error": str(e)})) sys.exit(1) def main(cfg, inputs): count, crt_err = _crtsh(cfg) if count is not None: return {"ok": True, "crtsh": "ok", "certificates": count} if cfg.get("certspotter_fallback") in (False, "false", "False", 0, "0"): raise Exception("crt.sh unavailable (" + crt_err + ") and the Cert Spotter fallback is off") ok, cs_err = _certspotter(cfg) if not ok: raise Exception("crt.sh unavailable (" + crt_err + ") and Cert Spotter unreachable (" + cs_err + ")") # crt.sh being down is common and the fallback answers: the instance works. return {"ok": True, "crtsh": "unavailable (" + crt_err + ")", "certspotter": "ok", "note": "crt.sh is down right now; searches will be answered by Cert Spotter"} _run(main)