Architectuurdiagram
Security Informer bestaat uit vijf primaire lagen: externe bronnen, workflow-orchestratie, kernopslag, management interfaces en notificatiekanalen.
╔═══════════════════════════════════════════════════════════════════════╗
║ SECURITY INFORMER STACK ║
╚═══════════════════════════════════════════════════════════════════════╝
┌─────────────────────── EXTERNE BRONNEN ───────────────────────────┐
│ VulnCheck NVD++ │ CISA KEV │ EPSS │ ENISA EUVD │ NVD NIST │
│ RSS: NCSC-NL, BleepingComputer, Krebs, THN, Dark Reading │
└───────────────────────────┬───────────────────────────────────────┘
│ HTTPS/REST/RSS
┌─────────▼──────────┐
│ n8n │ uw eigen n8n instantie (extern)
│ Workflow Engine │ 7 workflows, cron-gestuurd
└────────┬───────────┘
│
┌──────────────────┼──────────────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ PostgreSQL │ │ Claude AI │ │ Telegram │
│ (central) │◄──►│ Anthropic │ │ Bot │
└──────┬──────┘ └─────────────┘ └─────────────┘
│
┌─────┴──────────────────────────┐
│ │
┌───▼──────┐ ┌─────────┐ ┌────────▼────────┐
│ NocoDB │ │ Grafana │ │ Admin Portaal │
│ (assets)│ │ (stats) │ │ (Flask/Python) │
└──────────┘ └─────────┘ └─────────────────┘
│ │ │
│ uw eigen uw eigen
uw eigen Grafana- Admin-
NocoDB- domein domein
domein
│
┌────▼─────────────────────────────────────────┐
│ TRAEFIK │
│ TLS-terminatie, routing, Let's Encrypt │
└──────────────────────────────────────────────┘
│
Internet (HTTPS only)
Dataflow: van CVE naar notificatie
Stap-voor-stap hoe een nieuwe CVE het systeem doorloopt van bronpublicatie tot Telegram-alert.
CVE publicatie bij bron
Een CVE wordt gepubliceerd in VulnCheck NVD++ (of CISA KEV, EPSS, ENISA EUVD). Bij CISA KEV wordt de CVE ook gemarkeerd als Known Exploited.
n8n fetch-workflow
De CVE fetch-workflow (SI - CVE Feeds) draait elke 2 uur en combineert
alle vier CVE-bronnen in één workflow: VulnCheck NVD++, CISA KEV, EPSS en ENISA EUVD.
n8n haalt nieuwe CVE's op via HTTP en filtert op publicatiedatum.
Opslag in PostgreSQL
Elke CVE wordt opgeslagen in de tabel cve_raw met alle metadata:
CVE-ID, beschrijving, CVSS-score, publicatiedatum, KEV-status en EPSS-score.
Duplicaten worden genegeerd via ON CONFLICT DO NOTHING.
AI matching-workflow
Elk uur verwerkt de SI - AI Analyse workflow een batch van 50 CVE's die nog
niet gematcht zijn (ai_analysed = false). Het stuurt CVE + asset database
naar Claude AI voor relevantieanalyse.
Relevantiescore berekening
Op basis van de Claude AI-respons en de objectieve CVE-metadata wordt de
relevantiescore berekend en opgeslagen in cve_matches.
Asset-suggesties gaan naar asset_suggestions.
Notificatieregel evaluatie
De notificatieworkflow controleert elke 15 minuten of er matches zijn die voldoen aan een actieve notificatieregel (tijdvenster, minimum scores, KEV-status). Dit wordt gedaan via een PostgreSQL CTE-query die tijdvensters evalueert.
Telegram alert / e-mail rapport
Relevante matches worden als Telegram-bericht verstuurd met: CVE-ID, beschrijving, CVSS, relevantiescore, betrokken assets en aanbevolen actie. Voor e-mail worden matches gebundeld in een gestructureerd rapport.
Databaseschema
PostgreSQL 16 database. Alle tabellen met primaire kolombeschrijvingen.
Kerntabellen
Tabel: cve_raw | ||
|---|---|---|
| Kolom | Type | Beschrijving |
id | SERIAL PK | Interne identificatie |
cve_id | VARCHAR(20) UNIQUE | CVE-identifier (bijv. CVE-2024-12345) |
description | TEXT | Engelse beschrijving van de kwetsbaarheid |
cvss_v3_score | NUMERIC(3,1) | CVSS v3 base score (0.0 – 10.0) |
cvss_v3_vector | TEXT | CVSS v3 vector string |
epss_score | NUMERIC(5,4) | EPSS score (0.0000 – 1.0000) |
kev_status | BOOLEAN | True als CVE in CISA KEV staat |
published_at | TIMESTAMPTZ | Publicatiedatum CVE |
source | VARCHAR(20) | Primaire bron: vulncheck, cisa_kev, enisa, nvd |
ai_analysed | BOOLEAN | False = nog niet verwerkt door AI |
affected_cpes | TEXT | CPE-strings als tekst (geen JSONB) |
affected_products | TEXT | Getroffen producten in leesbare vorm |
created_at | TIMESTAMPTZ | Opslaagtijdstip in dit systeem |
Tabel: assets | ||
|---|---|---|
| Kolom | Type | Beschrijving |
id | SERIAL PK | Interne identificatie |
vendor | VARCHAR(100) | Fabrikant of leverancier |
product | VARCHAR(200) | Productnaam |
version | VARCHAR(50) | Versie of versierange |
cpe | TEXT | CPE 2.3 identifier voor exacte matching |
criticality | ENUM | low / medium / high / critical |
internet_facing | BOOLEAN | Publiek bereikbaar? |
subsidiary_id | INT FK | Koppeling aan subsidiaries.id |
notes | TEXT | Vrij tekstveld voor context |
active | BOOLEAN | False = uitgefaseerd, niet meer matchen |
created_at | TIMESTAMPTZ | Aanmaaktijdstip |
Tabel: cve_matches | ||
|---|---|---|
| Kolom | Type | Beschrijving |
id | SERIAL PK | Interne identificatie |
cve_id | INT FK | Koppeling aan cve_raw.id |
asset_id | INT FK | Koppeling aan assets.id |
relevance_score | NUMERIC(3,1) | Berekende relevantiescore (1.0 – 10.0) |
ai_reasoning | TEXT | Claude AI motivatie voor de match |
match_type | ENUM | direct / indirect / advisory |
priority | ENUM | critical / high / medium / low |
telegram_sent | BOOLEAN | True = Telegram-alert al verstuurd |
email_sent | BOOLEAN | True = opgenomen in e-mailrapport |
created_at | TIMESTAMPTZ | Tijdstip van match |
Tabel: notification_rules | ||
|---|---|---|
| Kolom | Type | Beschrijving |
id | SERIAL PK | Interne identificatie |
name | VARCHAR(100) | Beschrijvende naam van de regel |
active | BOOLEAN | False = tijdelijk uitgeschakeld |
days_of_week | INTEGER[] | Array van ISO weekdagnummers (1=ma, 7=zo) |
time_start | TIME | Begin tijdvenster (lokale tijd) |
time_end | TIME | Einde tijdvenster |
channel | VARCHAR(20) | telegram of email |
min_priority_score | NUMERIC(3,1) | Minimale priority score (1–10) |
min_relevance_score | NUMERIC(3,1) | Minimale relevance score (1–10) |
kev_always_send | BOOLEAN | KEV-alerts altijd sturen |
Tabel: component_health | ||
|---|---|---|
| Kolom | Type | Beschrijving |
id | SERIAL PK | Interne identificatie |
component_name | VARCHAR(50) UNIQUE | Componentnaam (bijv. vulncheck, cisa_kev, telegram) |
status | VARCHAR(20) | healthy / degraded / error / unknown |
last_activity_at | TIMESTAMPTZ | Laatste succesvolle activiteit |
last_tested_at | TIMESTAMPTZ | Laatste health check tijdstip |
last_test_result | VARCHAR(20) | passed / failed / skipped |
last_error | TEXT | Laatste foutmelding |
consecutive_failures | INTEGER | Aantal opeenvolgende mislukte checks |
n8n Workflows
Security Informer bestaat uit zeven n8n workflows. Elke workflow heeft een specifieke verantwoordelijkheid en draait op een eigen schema.
| # | Naam | Schema | Functie |
|---|---|---|---|
| 01 | SI - CVE Feeds |
Elke 2 uur | Haalt CVE's op van VulnCheck NVD++, CISA KEV, EPSS en ENISA EUVD; slaat op in PostgreSQL, markeert KEV-status en EPSS-scores |
| 02 | SI - RSS Feeds |
Elke 4 uur | Fetch 5 RSS-feeds (NCSC-NL, BleepingComputer, Krebs, THN, NCSC-UK); slaat artikelen op in news_items |
| 03 | SI - AI Analyse |
Elk uur (50 CVEs/batch) | Verwerkt ongeanalyseerde CVE's via Claude AI, berekent relevantiescore (1–10), schrijft matches en asset-suggesties |
| 04 | SI - Telegram Alerts |
Elke 15 minuten | Evalueert notificatieregels op tijdvenster en drempelwaardes, stuurt Telegram-alerts met inline keyboards voor asset-suggesties |
| 05 | SI - Email Rapport |
Maandag 08:00 | Verstuurt wekelijks e-mailrapport met overzicht van CVE-matches, gerankt op relevantiescore |
| 06 | SI - Telegram Callback |
Webhook (realtime) | Verwerkt inline keyboard-reacties vanuit Telegram: goedkeuren of snoozen van asset-suggesties |
| 07 | SI - Health Check |
Elk uur | Controleert status van alle componenten (CVE-bronnen, Telegram, SMTP, n8n); schrijft resultaten naar component_health |
Notificatie-engine
De notificatie-engine evalueert welke CVE-asset matches een notificatie moeten triggeren op basis van de actieve notificatieregels.
CTE-query (vereenvoudigd)
-- Bepaal welke matches nu genotificeerd moeten worden
WITH current_time_rules AS (
-- Selecteer regels die actief zijn op dit moment
SELECT *
FROM notification_rules
WHERE active = true
AND EXTRACT(ISODOW FROM NOW()) = ANY(days_of_week)
AND time_start <= LOCALTIME
AND time_end > LOCALTIME
),
pending_matches AS (
-- Alle matches die nog niet genotificeerd zijn
SELECT
cam.*,
c.cve_id, c.description, c.cvss_v3_score,
c.is_kev, c.epss_score,
a.vendor, a.product, a.criticality, a.internet_facing
FROM cve_asset_matches cam
JOIN cves c ON c.id = cam.cve_id
JOIN assets a ON a.id = cam.asset_id
WHERE cam.notified_at IS NULL
)
SELECT pm.*
FROM pending_matches pm
JOIN current_time_rules ctr ON (
-- Voldoet aan drempelwaardes
pm.relevance_score >= ctr.min_relevance_score
OR
-- KEV override: altijd sturen ongeacht tijdvenster
(pm.is_kev = true AND ctr.kev_always_send = true)
)
ORDER BY pm.relevance_score DESC;
Health Check Systeem
Elke CVE-bron en systeemcomponent heeft een gezondheidsregistratie in de
sources tabel. De health check-logica is zo ontworpen dat alleen
getest wordt wanneer dat zinvol is.
Test-beslissingslogica
-- Een component wordt ALLEEN getest als:
SELECT *
FROM component_health
WHERE
-- Nooit eerder getest, of
last_tested_at IS NULL
OR (
-- Laatste test is meer dan 1 uur geleden, EN
last_tested_at < NOW() - INTERVAL '1 hour'
AND
-- Er was geen activiteit in de laatste 15 minuten
(last_activity_at IS NULL
OR last_activity_at < NOW() - INTERVAL '15 minutes')
);
Dit voorkomt onnodige API-calls wanneer een bron recent actief was. Een bron die 5 minuten geleden data heeft gefetcht, wordt niet opnieuw getest — dat zou zowel nutteloos als potentieel rate-limiting zijn.
Beveiligingsoverwegingen
Geen publieke PostgreSQL poort
PostgreSQL heeft geen externe poortmapping in compose.yaml.
Communicatie gaat uitsluitend via het interne Docker-netwerk. Directe
databasetoegang van buitenaf is structureel onmogelijk.
API keys als n8n credentials
Alle API keys (VulnCheck, Anthropic, Telegram) worden opgeslagen als versleutelde n8n credentials. Workflow JSON-bestanden bevatten nooit raw API keys — alleen credential ID-referenties.
Traefik TLS-terminatie
Alle publieke endpoints (NocoDB, Admin, Grafana) zijn uitsluitend bereikbaar via HTTPS. Traefik regelt automatische certificaatverlenging via Let's Encrypt. HTTP-toegang wordt omgeleid.
Minimale databaserechten
Elke service gebruikt een eigen PostgreSQL-gebruiker met minimale rechten: NocoDB heeft alleen SELECT/INSERT/UPDATE op zijn tabellen, Grafana heeft uitsluitend SELECT-rechten op statistiektabellen.
.env-bestand bevat alle secrets en mag nooit
worden gecommit naar git. Zorg dat .env in .gitignore staat.
Gebruik voor productieopstelling een secrets manager (bijv. Docker Secrets, HashiCorp Vault).
Backup & Restore
Wat u minimaal moet backuppen voor een volledig herstel.
Backup
# PostgreSQL dump (bevat alle CVE's, assets, regels, matches)
docker compose exec postgres pg_dump \
-U si_app security_informer \
| gzip > backup_$(date +%Y%m%d_%H%M).sql.gz
# .env bestand (bevat alle secrets)
cp .env /veilige/locatie/.env.backup
# n8n workflow JSONs (voor reproduceerbare deployment)
ls workflows/*.json
# Exporteer ook de huidige workflow-configuratie via n8n API:
curl -s "${N8N_BASE_URL}/api/v1/workflows" \
-H "X-N8N-API-KEY: ${N8N_API_KEY}" | jq . > workflows_export.json
Restore
# Herstel PostgreSQL uit backup gunzip -c backup_20260315_1200.sql.gz | \ docker compose exec -T postgres psql -U si_app security_informer # Herstart services docker compose restart
Resource vereisten
| Component | RAM (min) | RAM (aanbevolen) | CPU | Opslag |
|---|---|---|---|---|
| PostgreSQL | 128 MB | 256 MB | 0.5 core | 1–5 GB (groeit met CVE-data) |
| NocoDB | 128 MB | 256 MB | 0.25 core | 50 MB |
| Admin Portaal | 64 MB | 128 MB | 0.1 core | 100 MB |
| Grafana | 128 MB | 256 MB | 0.25 core | 200 MB |
| n8n (extern) | 256 MB | 512 MB | 0.5 core | 500 MB |
| Totaal | 704 MB | 1.4 GB | 1.6 cores | ~2–7 GB |
Een VPS met 2 GB RAM en 2 CPU cores is ruim voldoende voor een productie-installatie. Opslag groeit voornamelijk door CVE-data (NVD bevat >250.000 CVE's; dagelijks ~100–300 nieuwe).
Omgevingsvariabelen — volledig overzicht
| Variabele | Beschrijving | Verplicht | Standaard |
|---|---|---|---|
POSTGRES_DB | Databasenaam | Nee | security_informer |
POSTGRES_USER | Hoofd DB gebruiker | Nee | si_app |
POSTGRES_PASSWORD | Hoofd DB wachtwoord | Ja | — |
POSTGRES_NOCODB_USER | NocoDB DB gebruiker | Nee | si_nocodb |
POSTGRES_NOCODB_PASSWORD | NocoDB DB wachtwoord | Ja | — |
POSTGRES_GRAFANA_PASSWORD | Grafana DB wachtwoord | Ja | — |
NC_AUTH_JWT_SECRET | NocoDB JWT signing secret | Ja | — |
NC_ADMIN_EMAIL | NocoDB admin e-mail | Ja | — |
NC_ADMIN_PASSWORD | NocoDB admin wachtwoord | Ja | — |
ADMIN_SECRET_KEY | Flask session secret key | Ja | — |
ADMIN_USERNAME | Admin portaal gebruikersnaam | Nee | admin |
ADMIN_PASSWORD | Admin portaal wachtwoord | Ja | — |
N8N_BASE_URL | n8n instantie URL | Ja | — |
N8N_API_KEY | n8n API key | Ja | — |
VULNCHECK_API_KEY | VulnCheck Bearer token | Ja | — |
NVD_API_KEY | NVD NIST API key (hogere rate limit) | Nee | — |
TELEGRAM_BOT_TOKEN | Telegram bot token | Ja | — |
TELEGRAM_CHAT_ID | Telegram chat/groep ID | Ja | — |
SMTP_HOST | SMTP server hostname | Nee | — |
SMTP_PORT | SMTP poort | Nee | 587 |
SMTP_USER | SMTP gebruiker | Nee | — |
SMTP_PASSWORD | SMTP wachtwoord | Nee | — |
REPORT_TO_EMAIL | Ontvanger e-mailrapporten | Nee | — |
NOCODB_DOMAIN | Domein voor NocoDB (Traefik) | Ja | — |
ADMIN_DOMAIN | Domein voor Admin Portaal (Traefik) | Ja | — |
GRAFANA_DOMAIN | Domein voor Grafana (Traefik) | Ja | — |
GF_SECURITY_ADMIN_PASSWORD | Grafana admin wachtwoord | Ja | — |
N8N_CREDENTIAL_ANTHROPIC_ID | n8n credential ID voor Anthropic | Na stap 6 | — |
N8N_CREDENTIAL_TELEGRAM_ID | n8n credential ID voor Telegram | Na stap 6 | — |
N8N_CREDENTIAL_VULNCHECK_ID | n8n credential ID voor VulnCheck | Na stap 6 | — |