Byg et cybersikkerhedstrussel-informationsfeed til SaaS
Et feed til cybersikkerhedstrusselsintelligens er en kontinuerlig, filtreret strøm af sikkerhedssignaler – brud, CVE'er, udnyttede sårbarheder og leverandørhændelser – normaliseret til en enkelt kanal, scoret for relevans for din stak og dirigeret til de personer, der kan handle på det. De fleste artikler beder dig om at abonnere på 12 kommercielle feeds. For en lille eller mellemstor SaaS er det 12× støjen – og en regning på $20k–$100k/år – for et problem, du kan løse med en velbygget aggregator til under $50/måned.
Dette er til SaaS ingeniør- og sikkerhedsteams, der ønsker signal, ikke støj: alarmer om brud i dine afhængigheder (Postgres, Stripe, Cloudflare, Auth0), CVE'er i biblioteker, du leverer, og hændelser hos dine kunders brands. (Oplysning: APITube er nyheds-API, der bruges i eksemplet og sponsoren af denne artikel. Mønsteret virker med NewsAPI, GNews, NewsCatcher; CISA KEV og NVD CVE-feedet er gratis.)
Vigtigste pointer:
- 80 20-stacken er CISA KEV + NVD CVE feed + en struktureret nyheds-API. Du behøver ikke MISP, du behøver ikke et SIEM-modul, du behøver ikke et kommercielt feed til $30k/år.
- Score hvert signal langs fire akser — KEV match, CVSS-alvorlighed, leverandør-i-din-stack, aktualitet. Alt under tærsklen går til et digest, ikke en side.
- Filtrer efter dine afhængigheder. Et brud hos Stripe betyder noget; et brud hos et CRM, du ikke bruger, er støj.
- Ruter efter alvorlighed: P0 → PagerDuty, P1 → Slack
#sec-alerts, P2+ → daglig digest-e-mail. - En fungerende aggregator er ~150 linjer Python + en lille SQLite-butik. Samlet månedlig omkostning: ~$30 (nyheds-API-niveau) + ~$5 (Hetzner-boks).
Problemet
Et typisk SaaS-team har det samme trussels-intelligens-problem i tre varianter:
- Ingeniørarbejde: "Er det Postgres CVE, der lige er kommet i nyhederne i vores mindre version?"
- Kundesucces: "En af vores top 50 kunder er i nyhederne for et brud — skal vi kontakte dem?"
- Overholdelse: "Skete der noget med vores underprocessorer i dag, som jeg skal logge?"
Hvad der tilbydes:
- Abonner på 12 gratis feeds + RSS dem ind i Feedly. Du får firehose-volumen støj, ingen alvorlighedsscore, ingen filtrering efter stack, ingen routing.
- Køb en kommerciel threat-intel-platform ($10k–100k/år — Recorded Future, Mandiant, Anomali). Overkill for en 30-ingeniør SaaS, og relevanssignalet er stadig generisk.
- Byg det selv. Denne artikel gennemgår den vej.
Gabet i den offentlige diskussion er, at ingen skriver denne tredje mulighed ned. Top-10 SERP-resultater er leverandørlistelister ("12 gratis feeds at følge") og SEO-optimerede "hvad er et TI-feed"-sider, hvoraf ingen kommer tæt på en fungerende arkitektur.
Løsningsoversigt
Du bygger en lille daemon, der gør fem ting på en 30-minutters cron:
- Henter friske elementer fra tre kilder: en nyheds-API (følelse + enheder + emner), CISA Known Exploited Vulnerabilities (KEV) kataloget og NVD CVE feedet.
- Normaliserer hvert element til et fælles
ThreatSignal-skema. - Dedupes mod et SQLite lager af sete ID'er.
- Scorer hvert signal mod din stack og en relevans-tærskel.
- Ruter efter alvorlighed: PagerDuty for P0, Slack for P1, ugentlig digest for P2.
I modsætning til en kommerciel platform, der sælger dig et generisk feed plus dashboards, du aldrig vil åbne, viser denne aggregator kun det, der skærer sig med din leverandørliste og din kundeliste — hvilket betyder, at signal-til-støj-forholdet er indstillet af din konfigurationsfil, ikke af en leverandørs idé om "vigtigt."
Arkitektur
┌────────────────┐
│ CISA KEV JSON │──┐
└────────────────┘ │
┌────────────────┐ │
│ NVD CVE feed │──┤
└────────────────┘ │
┌────────────────┐ │ ┌──────────────┐ ┌──────────┐
│ APITube news │──┼────▶│ Normalizer │───▶│ SQLite │
│ topic:cyber │ │ │ + Dedup │ │ (seen) │
└────────────────┘ │ └──────┬───────┘ └──────────┘
│ │
│ ▼
│ ┌──────────────┐
│ │ Scorer │
│ │ KEV+CVSS+ │
│ │ stack+recent │
│ └──────┬───────┘
│ │
│ ▼
│ ┌──────────────┐
│ │ Router │
│ └──────┬───────┘
│ │
│ ┌─────┴─────┬───────────────┐
│ ▼ ▼ ▼
│ PagerDuty Slack Daily digest
│ (P0) (P1) (P2 / weekly)
│
cron 30m
Tre datakilder, én normalisator, ét persistent deduplikeringslager, én scorer, tre outputs.
Implementering
Stack-konfiguration (den del, der gør den SaaS-specifik)
# stack.yml
vendors:
critical: # P0 if breach
- postgres
- postgresql
- stripe
- cloudflare
- auth0
- okta
important: # P1 if breach
- sendgrid
- twilio
- datadog
- segment
customers: # match by entity name (case-insensitive)
- "Acme Corp"
- "Globex"
- "Initech"
libraries: # CVE matching by package name
- django
- psycopg
- requests
- jwt
keywords: # plain-text fallback for news scan
- "supply chain attack"
- "credential stuffing"
Denne fil er hele årsagen til, at din aggregator slår et kommercielt feed til SaaS-brug — den ved, hvad du holder af.
Kilder
# sources.py
import os, requests
from datetime import datetime, timezone
def fetch_kev() -> list[dict]:
r = requests.get("https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json", timeout=15)
r.raise_for_status()
return r.json().get("vulnerabilities", [])
def fetch_cves(modified_since: str) -> list[dict]:
r = requests.get("https://services.nvd.nist.gov/rest/json/cves/2.0",
params={"lastModStartDate": modified_since,
"lastModEndDate": datetime.now(timezone.utc).isoformat()},
timeout=20)
r.raise_for_status()
return r.json().get("vulnerabilities", [])
def fetch_news_cyber() -> list[dict]:
r = requests.get("https://api.apitube.io/v1/news/everything",
params={"topic.id": "cybersecurity",
"language.code": "en",
"per_page": 50,
"sort.by": "published_at",
"sort.order": "desc"},
headers={"X-API-Key": os.environ["APITUBE_API_KEY"]},
timeout=15)
r.raise_for_status()
return r.json().get("results", [])
Nyheds-API-kaldet er nøgleingrediensen. APITube returnerer hver artikel med entities (personer, organisationer, brands), topics og sentiment allerede vedhæftet – så du kan matche den returnerede entities[].name mod din kritiske stak i stedet for selv at køre enheds-ekstraktion – og indsnævre på serversiden med de typede filtre (organization.name, brand.name, person.name) eller en numerisk entity.id.
Normalisator
# normalize.py
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal
Kind = Literal["news", "kev", "cve"]
@dataclass
class ThreatSignal:
id: str # stable dedup key
kind: Kind
title: str
url: str
summary: str
published_at: datetime
entities: list[str] = field(default_factory=list)
cvss: float | None = None
cve_ids: list[str] = field(default_factory=list)
raw: dict = field(default_factory=dict)
Tre små adapterfunktioner (from_news, from_kev, from_cve) returnerer hver en ThreatSignal. At holde formen ensartet er det, der lader én scorer håndtere alle tre kilder.
Dedup
# dedup.py
import sqlite3
db = sqlite3.connect("seen.db")
db.execute("CREATE TABLE IF NOT EXISTS seen (id TEXT PRIMARY KEY, ts INTEGER)")
def is_new(id_: str) -> bool:
return db.execute("SELECT 1 FROM seen WHERE id=?", (id_,)).fetchone() is None
def mark(id_: str) -> None:
db.execute("INSERT OR IGNORE INTO seen VALUES (?, strftime('%s','now'))", (id_,))
db.commit()
For nyhedskilden er id artikel-URL. For KEV er det CVE ID. For CVE er det <cve_id>:<lastModified>, så en gen-scoret CVE udløser præcis én gang.
Scoren
# score.py
import yaml
from datetime import datetime, timezone
cfg = yaml.safe_load(open("stack.yml"))
CRIT = {v.lower() for v in cfg["vendors"]["critical"]}
IMP = {v.lower() for v in cfg["vendors"]["important"]}
CUST = {c.lower() for c in cfg["vendors"]["customers"]}
LIBS = {l.lower() for l in cfg["libraries"]}
def severity(sig) -> int:
"""Return 0=ignore, 1=digest, 2=slack, 3=pagerduty"""
text = (sig.title + " " + sig.summary).lower()
ents = {e.lower() for e in sig.entities}
if sig.kind == "kev":
affected = (sig.raw.get("vendorProject", "") + " " + sig.raw.get("product", "")).lower()
if any(v in affected for v in CRIT): return 3 # critical KEV → PagerDuty
if any(v in affected for v in IMP): return 2
return 1
if sig.kind == "cve":
cvss = sig.cvss or 0
prod = " ".join(sig.entities).lower()
if any(lib in prod for lib in LIBS) and cvss >= 7.0: return 3
if cvss >= 9.0: return 2
if cvss >= 7.0: return 1
return 0
if sig.kind == "news":
age_hours = (datetime.now(timezone.utc) - sig.published_at).total_seconds() / 3600
recency = max(0, 1 - age_hours / 48) # 0..1
crit_hit = bool(ents & CRIT) or any(v in text for v in CRIT)
cust_hit = bool(ents & CUST)
if "breach" in text and crit_hit: return 3
if cust_hit and "breach" in text: return 3
if crit_hit and recency > 0.3: return 2
if cust_hit: return 2
if any(v in text for v in cfg["keywords"]): return 1
return 0
Dette er bevidst meningsdannende. KEV-match mod en kritisk udbyder går til PagerDuty, fordi hvis CISA markerede det som aktivt udnyttet, og du leverer den udbyder, har du en beslutning, der skal træffes samme dag. En høj-CVSS CVE i et bibliotek, du ikke afhænger af, returnerer 0 — irrelevans er halvdelen af værdien af et TI-feed.
Ruter
# route.py
import os, requests, json
def to_pagerduty(sig):
requests.post("https://events.pagerduty.com/v2/enqueue", timeout=10, json={
"routing_key": os.environ["PAGERDUTY_KEY"],
"event_action": "trigger",
"payload": {
"summary": f"[SEC-P0] {sig.title}",
"severity": "critical",
"source": "ti-feed",
"custom_details": {"url": sig.url, "kind": sig.kind, "cves": sig.cve_ids},
},
})
def to_slack(sig, channel="#sec-alerts"):
requests.post(os.environ["SLACK_WEBHOOK_URL"], timeout=10, json={
"channel": channel,
"text": f"*[{sig.kind.upper()}] {sig.title}*\n{sig.summary}\n<{sig.url}|source>"
+ (f"\nCVEs: {', '.join(sig.cve_ids)}" if sig.cve_ids else ""),
})
def to_digest(sig):
# append to a daily file, send at 09:00 UTC by separate cron
with open("digest.ndjson", "a") as f:
f.write(json.dumps({"title": sig.title, "url": sig.url, "kind": sig.kind}) + "\n")
30-minutters cron-løkke
# run.py
from sources import fetch_kev, fetch_cves, fetch_news_cyber
from normalize import from_news, from_kev, from_cve
from dedup import is_new, mark
from score import severity
from route import to_pagerduty, to_slack, to_digest
def run():
sigs = [from_news(a) for a in fetch_news_cyber()] \
+ [from_kev(k) for k in fetch_kev()] \
+ [from_cve(c) for c in fetch_cves_recent()]
for sig in sigs:
if not is_new(sig.id): continue
sev = severity(sig)
if sev == 3: to_pagerduty(sig)
elif sev == 2: to_slack(sig)
elif sev == 1: to_digest(sig)
mark(sig.id)
Det er det hele — omkring 150 linjer kode fordelt på fem filer plus en konfiguration.
Resultater / hvad du faktisk får
Efter to uger med at køre dette på et rigtigt 30-ingeniør SaaS-team (stikprøvestørrelse på én — dine resultater kan variere):
| Kanal | Volumen | Signal-til-støj (subjektivt) |
|---|---|---|
| PagerDuty (P0) | ~1 side / måned | Hver side var handlingsorienteret |
Slack #sec-alerts (P1) | 4–6 / dag | ~70% læst, ~20% udløste en diskussion |
| Daglig oversigt (P2) | 30–60 / dag | Skimmet på 2 minutter én gang om dagen |
PagerDuty-volumenet er det, der gør systemet levedygtigt. Hvis du bliver vækket for ikke-handlingsbar støj, vil du mute kanalen inden for en uge, og systemet fejler. Den strenge scoring (KEV + kritisk-leverandør match, eller brud + kunde-navn match) er den del, der skal finjusteres af dig, ikke af en leverandør.
Omkostning
| Opsætning | Tilbagevendende omkostning | Afvejning |
|---|---|---|
| Denne aggregator (nyheds-API + KEV + NVD + cron + Slack/PagerDuty) | ~$30–50 / md | Vedligehold ~150 LOC + en konfigurationsfil |
| Abonnér på 12 gratis feeds via Feedly Pro+ | $96/år | Manuel triage, ingen PagerDuty-sti |
| MISP selv-hostet | $0 + driftsomkostninger | Kraftfuld, men tung; designet til IOC-deling, ikke SaaS-triage |
| Recorded Future / Mandiant / Anomali | $30k–100k+ / år | Generelt enterprise-feed, dashboards du ikke vil bruge, salgscyklus |
Over ~200 ingeniører eller i en reguleret branche (bank, sundhedsvæsen), er kommerciel TI berettiget. Under det vinder build-muligheden på relevans og omkostninger samtidigt.
Ofte stillede spørgsmål
Hvad er et trussels-efterretningsfeed?
Et trussels-intelligens-feed er en kontinuerlig strøm af sikkerhedssignaler – sårbarhedsmeddelelser, brudsrapporter, udnyttede CVE'er og indikatorer for kompromittering – leveret i et struktureret format, så et sikkerhedsteam kan opdage, prioritere og reagere på trusler. Feedets værdi afhænger helt af, hvor godt det filtrerer for relevans for din specifikke stak, kunder og leverandører; en generisk firehose har næsten nul signal-til-støj for en lille SaaS.
Hvordan bygger jeg et trussels-intelligensfeed?
For at bygge et trussels-informationsfeed, kombiner tre gratis eller billige kilder — CISA KEV kataloget (gratis), NVD CVE feedet (gratis) og en struktureret nyheds-API (~$30/måned) — til en normalisator, der producerer et ensartet ThreatSignal-skema, deduplikerer mod et SQLite-lager, scorer hvert signal mod din leverandør- og kundeliste, og ruter efter alvorlighed til PagerDuty, Slack eller en daglig digest. Hele pipelinen er ca. 150 linjer Python plus en YAML-konfiguration af dine afhængigheder.
Hvilke kilder skal jeg bruge til trusselsintelligens?
Minimum levedygtige kilder til et SaaS trusselsinformationsfeed er CISA KEV (aktivt udnyttede sårbarheder, gratis), NVD CVE feedet (alle opdagede CVE'er med CVSS-scorer, gratis) og en nyheds-API med sentiment- og enhedskendskab til dækning af brud og leverandør-incidents. Valgfrie sekundære kilder inkluderer MITRE ATT&CK, AlienVault OTX og Abuse.ch – tilføj dem kun, efter du har filtreret de første tre godt, ellers stabler du brandslanger.
Hvordan overvåger jeg cybertrusler, der er relevante for min SaaS?
For at overvåge cybertrusler relevante for din SaaS, vedligehold en konfigurationsfil, der angiver dine kritiske leverandører (Postgres, Stripe, Cloudflare, Auth0 osv.), vigtige leverandører, topkunder efter navn og leverede biblioteker. Score hvert indkommende signal mod denne liste — KEV match mod en kritisk leverandør eller omtale af brud plus kundenavn bør udløse en alarm; alt andet rutes til Slack eller en daglig digest. Relevans kommer fra konfigurationen, ikke fra kilden.


