Doorsturen naar SIEM
fremforge schrijft elke beheeractie die iets verandert naar een auditlog per tenant, met een integriteitsgarantie via een hashketen (zie Integriteit van de auditketen). Dezelfde gebeurtenissen kunnen naar je SIEM worden doorgestuurd zodra ze zijn geschreven, zodat je detectieregels ze binnen seconden zien in plaats van bij de volgende CSV-export.
Binnenkort beschikbaar: dezelfde stroom zal ook één gebeurtenis bevatten per wijziging in de levenscyclus van een beveiligingsbevinding: een bevinding over een kwetsbaarheid, licentie, SAST, secret, malware of pakketbeleid die wordt geopend, heropend, genegeerd of opgelost. Zie Bevindinggebeurtenissen.
Deze pagina beschrijft hoe het doorsturen werkt, het contract op de lijn waarmee je kunt verifiëren, en hoe gangbare SIEM’s de gebeurtenissen inlezen.
Hoe de aflevering werkt
audit-emit schrijft de geketende rij en zet de gebeurtenis in dezelfde databasetransactie in de wachtrij van elk ingeschakeld endpoint waarvan de actieprefixen overeenkomen. Een afleverproces verstuurt de wachtrij van elk endpoint op volgorde, met één of meer gebeurtenissen per verzoek, afhankelijk van het formaat van het endpoint. Een gebeurtenis gaat pas uit de wachtrij als je ontvanger met 2xx antwoordt.
De instellingen staan onder Organisatiebeheer → Integratie → SIEM-doorsturen (/<org>/_admin/siem), en dezelfde instellingen zitten in de REST API (/_app/api/v1/orgs/<org>/siem/endpoints).
De pagina SIEM-doorsturen bevat het formulier Een endpoint toevoegen en de lijst met endpoints: naam, URL (inloggegevens gemaskeerd), status, laatste levering en het aantal wachtende gebeurtenissen. De naam van elk endpoint opent een eigen pagina (/<org>/_admin/siem/<endpoint id>), met één kaart per instelling:
- Overzicht: de status, de reden van de laatste fout, het filter op actieprefixen, Uitschakelen/Inschakelen en Test verzenden.
- Auth-header: instellen, roteren of verwijderen.
- Signatuur: wat de HMAC omvat.
- Levering: wachtende gebeurtenissen, de oudste daarvan, de volgende poging, het totaal afgeleverd en verworpen, en de instellingen voor formaat en bundeling.
- Opnieuw versturen: eerdere gebeurtenissen nogmaals versturen.
- Endpoint verwijderen: typ de naam van het endpoint om te bevestigen.
Een endpoint configureren
- Ga naar Organisatiebeheer → SIEM-doorsturen → Een endpoint toevoegen.
- Vul een Naam in (vrije tekst, uniek binnen de organisatie) en de URL van je ontvanger:
https://…, ofsyslog+tls://host:portvoor syslog via TLS.http://,localhost, RFC1918-adressen en metadata-adressen worden geweigerd; gebruik een ontvanger die openbaar bereikbaar is. - Kies het Formaat (zie Formaten en bundeling). De overige stappen staan onder Geavanceerde opties en kun je later ook op de pagina van het endpoint wijzigen.
- Beperk eventueel de allowlist Prefixen voor gebeurtenisacties (bijvoorbeeld
forgejo.push, billing.). Leeg betekent: alles doorsturen. Elke prefix wordt vergeleken metaction.startsWith(prefix). - Zet eventueel Toegangslog van het register meesturen aan om ook één gebeurtenis te ontvangen per verzoek aan het pakket- of containerregister (
registry.access.*). Dit staat standaard uit; zie Toegangslog van de registry. De pullbeslissingen van de imagepoorten,registry.pull.blockedenregistry.pull.would_block, zijn auditgebeurtenissen en geen toegangsgebeurtenissen. Ze worden dus ook zonder deze optie doorgestuurd, één per imagedigest, poort en beslissing per uur; zie Poorten voor containerpulls. Binnenkort beschikbaar: Bevindinggebeurtenissen meesturen, standaard aangevinkt, laat het endpoint ook één gebeurtenis ontvangen per wijziging in de levenscyclus van een beveiligingsbevinding (security.finding.*), vanaf het moment dat het endpoint wordt aangemaakt. Tot dit beschikbaar is, toont het formulier de optie niet. Zie Bevindinggebeurtenissen. - Stel eventueel een Auth-header in: een headernaam en een geheime waarde die bij elk verzoek worden meegestuurd (bijvoorbeeld
AuthorizationenSplunk <token>). Zie Authenticatie. - Kies wat de Handtekening omvat (standaard tijdstempel en body, of alleen de body).
- Stel de bundelgrootte, de verzoekgrootte en de wachttijd in.
- Klik op Endpoint aanmaken. Het gedeelde HMAC-secret wordt op het volgende scherm één keer getoond; kopieer het naar de configuratie van je ontvanger. fremforge toont het nooit meer. Raak je het kwijt, verwijder het endpoint dan en voeg het opnieuw toe.
Het endpoint is direct actief; de bevestiging linkt naar de pagina ervan. Test verzenden verstuurt een synthetische gebeurtenis fremforge.siem.test in het formaat van het endpoint, met de headers en handtekening ervan, en toont de antwoordstatus en de eerste 200 bytes van het antwoord van de ontvanger.
Inloggegevens in de URL. Sommige collectors krijgen hun inloggegeven in de URL mee (?token=, ?key=&secret= of user:password@). fremforge slaat de URL zoals ingevoerd versleuteld op, en toont hem overal elders (beheerpagina, API, testresultaat, auditlog) met elke querywaarde en de gebruikersinformatie vervangen door ***. Alleen de aflevering gebruikt de volledige URL.
Authenticatie
Twee mechanismen, die je samen kunt gebruiken:
- Auth-header. Eén header per endpoint, met een naam en waarde naar keuze. De waarde wordt versleuteld opgeslagen en kan alleen worden geschreven: geen pagina, API-antwoord, logregel of auditgebeurtenis bevat haar. De auditlog legt de naam van de header vast en wanneer hij is ingesteld. Om te roteren sla je een nieuwe waarde op; de oude wordt direct niet meer meegestuurd, dus voeg de nieuwe sleutel eerst toe aan je collector. Headernamen die het verzoek zouden breken, worden geweigerd:
Host,Content-Length,Content-Type,Content-Encoding,Transfer-Encoding,Connection,Keep-Alive,Upgrade,TE,Trailer,Expect,Accept-Encoding,User-Agent,Cookie,Proxy-*enX-Fremforge-*. Waarden bestaan uit afdrukbare ASCII-tekens, tot 4096 tekens. - HMAC-handtekening. Elk verzoek bevat
X-Fremforge-Signature: sha256=<hex>, met het HMAC-secret van het endpoint als sleutel. Standaard omvat de handtekening<timestamp>.<body>, zodat je ontvanger een oud verzoek aan het tijdstempel kan herkennen en weigeren. Met Handtekening omvat: Alleen body omvat ze alleen de ruwe body, en dat is wat ontvangers met ingebouwde verificatie van de body (de HTTP-endpoint-input van Elastic) controleren. Een ontvanger die alleen de body controleert, kan een herhaald verzoek niet aan zijn leeftijd herkennen; ontdubbel in plaats daarvan op hetidvan de gebeurtenis.
Contract op de lijn
Elke aflevering via HTTPS is een POST:
| Header | Waarde |
|---|---|
Content-Type | application/json (json, json_array), application/x-ndjson (ndjson), text/plain; charset=utf-8 (cef) |
User-Agent | fremforge-siem-forwarder/1 |
X-Fremforge-Timestamp | Unix-seconden (string) waarop fremforge het verzoek opbouwde |
X-Fremforge-Signature | sha256=<hex> over <timestamp>.<body>, of over <body> als alleen de body wordt ondertekend |
Idempotency-Key | Het id van de gebeurtenis bij één gebeurtenis; bij een bundel een SHA-256 van de ids van de gebeurtenissen in de bundel, op volgorde |
X-Fremforge-Event-Count | Aantal gebeurtenissen in de body |
| je auth-header | Als die is ingesteld |
Elke gebeurtenis heeft deze vorm (sleutels op het hoogste niveau in deze volgorde; sleutels in fields.* alfabetisch gesorteerd, zodat de bytes stabiel zijn):
{
"id": "00000000-0000-0000-0000-000000000000",
"tenant_id": "11111111-1111-1111-1111-111111111111",
"actor": "alice",
"action": "policy.update.api",
"fields": { "...": "..." },
"created_at": "2026-05-15T13:42:11.123Z",
"hash_chain": {
"prev_hash": "ab12...",
"this_hash": "cd34..."
}
}id is het id van de auditgebeurtenis. Het is hetzelfde bij elke nieuwe poging en elke keer dat die gebeurtenis opnieuw wordt verstuurd: gebruik het om te ontdubbelen.
actor is een kale Forgejo-gebruikersnaam voor een actie van een persoon (in de webconsole, en bij veel API-aanroepen de gebruiker die het token heeft aangemaakt), oidc:idp:<sub> voor een persoon die via je IdP heeft ingelogd en via een ingewisseld token handelt, token:<id> voor andere API-aanroepen, operator:<email> voor een actie van een medewerker van fremverk, en andere vormen <kind>:<id> voor systeemcomponenten.
hash_chain.this_hash is dezelfde hash die in OBS Object Lock is verankerd, zodat je SIEM kan aantonen dat een gebeurtenis ouder is dan een bekend anker.
Een toegangsgebeurtenis van de registry (alleen op een endpoint met include_access_log) heeft dezelfde vorm, met action registry.access.<action> en "hash_chain": null: toegangsgebeurtenissen maken geen deel uit van de hashketen van de auditlog. De fields ervan zijn de gebeurtenis zoals de API van de toegangslog die teruggeeft.
Een bevindinggebeurtenis (binnenkort beschikbaar; alleen op een endpoint met include_finding_events) krijgt ook deze vorm, met action security.finding.<transition> en "hash_chain": null.
Formaten en bundeling
| Formaat | Body | Gebeurtenissen per verzoek |
|---|---|---|
json (standaard) | Eén gebeurtenisobject | 1 |
ndjson | Eén gebeurtenisobject per regel, elke regel eindigt op \n | Tot max. gebeurtenissen |
json_array | Een JSON-array van gebeurtenisobjecten | Tot max. gebeurtenissen |
cef | Eén CEF-regel per gebeurtenis, elke regel eindigt op \n | Tot max. gebeurtenissen |
Voor de gebundelde formaten:
- Max. gebeurtenissen per verzoek: 1 tot 1000 (100 als je naar een gebundeld formaat overstapt zonder te kiezen).
- Max. grootte per verzoek: 1 KiB tot 4 MiB (standaard 1 MiB). Een bundel wordt gesloten voordat hij die grootte zou overschrijden; een enkele gebeurtenis wordt altijd verstuurd.
- Max. wachttijd: 0 tot 60 000 ms (standaard 0). Hoe lang een gedeeltelijk gevulde bundel op meer gebeurtenissen wacht voordat hij wordt verstuurd.
Gebeurtenissen worden afgeleverd in de volgorde waarin ze zijn geschreven.
Veldtoewijzing in CEF
CEF:0|fremverk|fremforge|1|<action>|<action>|<severity>|<extension>
| CEF | Waarde |
|---|---|
| Signature ID, Name | action |
| Severity | 9 wanneer hash_chain.this_hash gelijk is aan chain-broken; 7 wanneer action fail, denied, deny, blocked, violation, malware, secret, breach of tamper bevat; 5 wanneer het delete, revoke, disable, remove, bypass, suspend of dismiss bevat; anders 3 |
rt | created_at, in milliseconden sinds de epoch |
externalId | id (de sleutel om te ontdubbelen) |
suser | actor |
act | action |
cat | action tot de eerste . |
cs1Label=tenantId, cs1 | tenant_id |
cs2Label=fields, cs2 | fields als JSON |
cs3Label=hashChainThis, cs3 | hash_chain.this_hash |
cs4Label=hashChainPrev, cs4 | hash_chain.prev_hash, leeg als die er niet is |
dvchost | frem.sh |
Headervelden escapen \ en |; waarden in de extensie escapen \, = en regeleinden (\n, \r). Een waarde uit een auditgebeurtenis kan dus geen nieuw veld en geen nieuwe regel beginnen.
Bevindinggebeurtenissen
Binnenkort beschikbaar. Bevindinggebeurtenissen zijn nog niet beschikbaar. Deze sectie beschrijft ze vooruitlopend op de release; tot dan bieden de console en de API
include_finding_eventsen de momentopname niet aan, en wordt er geen gebeurtenissecurity.finding.*verstuurd. De changelog kondigt de release aan.
Een endpoint waarop Bevindinggebeurtenissen meesturen aanstaat (include_finding_events in de API), ontvangt één gebeurtenis telkens wanneer een beveiligingsbevinding van toestand verandert. Elk type bevinding dat fremforge rapporteert, valt eronder:
alert_category | Bevinding | Bron |
|---|---|---|
dependency_vulnerability | Een kwetsbare afhankelijkheid op de standaardbranch | Scan van afhankelijkheden |
image_vulnerability | Een kwetsbaarheid of misconfiguratie in een containerimage in je registry (inclusief geproxyde images) of in een scan van een repository | Imagescan |
licence | Een afhankelijkheid onder een licentie die forbidden of restricted is | Licentiescan |
sast | Een bevinding van statische analyse op de standaardbranch | SAST |
secret | Een gecommit secret (nooit het secret zelf) | Secretscan |
malware | Een kwaadaardig pakket (OSV MAL-*), of een upload die door de antivirusscan (ClamAV) is geweigerd | Scans van afhankelijkheden en de registry, scannen van uploads |
package_policy | Een bestand in de pakketregistry dat het pakketbeleid blokkeert of waarvoor het waarschuwt | Pakketbeleid |
Bevindingen uit scans van pull requests vallen er niet onder: die komen en gaan met elke pull request. De levenscyclus van een bevinding wordt op de standaardbranch gevolgd. Gebeurtenissen over het pakketbeleid worden alleen verstuurd voor organisaties waar de resultaten van het pakketbeleid zichtbaar zijn.
Overzicht van gebeurtenissen
action | Wanneer |
|---|---|
security.finding.opened | De bevinding wordt voor het eerst gedetecteerd. |
security.finding.reopened | De bevinding wordt opnieuw gedetecteerd nadat ze was opgelost (transition_reason reappeared), of het negeren ervan werd ingetrokken voordat het verliep (undismissed). |
security.finding.dismissed | Iemand negeerde de bevinding (risico geaccepteerd, fout-positief), een allowlist dekt haar, of een antivirusbevinding werd overschreven. actor is de gebruiker die haar negeerde, of system:<kind>. |
security.finding.dismissal_expired | Het negeren met een vervaldatum is verlopen en de bevinding staat weer open. Dit geldt ook voor een uitzondering die haar vervaldatum bereikte. |
security.finding.exception_revoked | Een goedgekeurde uitzondering waardoor de bevinding werd genegeerd, is ingetrokken en de bevinding staat weer open. transition_reason is de aanleiding: manual (iemand trok haar in), severity_raised, new_vulnerability of approver_left. actor is de persoon die haar introk, of system:exception-revocation. |
security.finding.dismissal_revoked | fremforge trok het negeren in dat steunde op een bereikbaarheidsoordeel niet bereikbaar, omdat een latere scan dat tegenspreekt, en de bevinding staat weer open. transition_reason is reachability_changed, finding.revocation zegt waarom, en actor is system:exception-revocation. (Vóór 2026-10-12 werd dit verstuurd als dismissal_expired.) Een ingetrokken uitzondering is exception_revoked met dezelfde reden. |
security.finding.resolved | De bevinding wordt niet meer gedetecteerd: verholpen, verwijderd, vervangen door een nieuwere scan die haar niet meer rapporteert, of het pakketbestand is verwijderd. |
security.finding.known_exploited | Een openstaande of genegeerde kwetsbaarheid in een afhankelijkheid of image is als in het wild misbruikt opgenomen in de EU Vulnerability Database van ENISA of in de KEV-catalogus van CISA. Eén keer verstuurd per vermelding. transition_reason is listed (de lijst heeft haar sindsdien opgenomen) of new (de bevinding stond al op de lijst toen ze voor het eerst werd gezien; ze wordt samen met haar opened verstuurd, in willekeurige volgorde). known_exploited noemt de CVE’s en de lijsten, en actor is system:known-exploited-feed. Ook een genegeerde bevinding wordt gemeld: het risico waaronder ze werd geaccepteerd, is veranderd. |
security.finding.snapshot | Eén per openstaande bevinding, één keer verstuurd wanneer je een momentopname aanvraagt. |
Wijzigingen worden binnen ongeveer een minuut opgepikt. Een bevinding houdt over nieuwe scans heen één identiteit, dus een nieuwe scan die dezelfde bevinding opnieuw rapporteert, verstuurt niets; fields.original_alert_id is hetzelfde bij elke gebeurtenis over één bevinding.
Wat er wordt verstuurd, en vanaf wanneer
- Alleen wijzigingen vanaf het aanzetten. Een endpoint ontvangt gebeurtenissen voor wijzigingen die plaatsvinden nadat het ze is gaan ontvangen (
finding_events_sincein de API). Als je bevindinggebeurtenissen aanzet, worden je bestaande bevindingen niet verstuurd. - Momentopname van open bevindingen. Om te laden wat nu openstaat, gebruik je Momentopname van open bevindingen versturen op de pagina van het endpoint, of
POST /_app/api/v1/orgs/<org>/siem/endpoints/<id>/finding-snapshot. Dat zet één gebeurtenissecurity.finding.snapshotper openstaande bevinding in de wachtrij, alleen voor dat endpoint. Dit kan één keer per endpoint; een tweede verzoek krijgt409als antwoord. - Standaard. Een endpoint dat na de release wordt aangemaakt, stuurt bevindinggebeurtenissen mee, tenzij je het vakje uitvinkt (of
"include_finding_events": falsemeestuurt). Endpoints die al vóór de release bestaan, blijven zoals ze waren, zonder bevindinggebeurtenissen, zodat een ontvanger die voor auditgebeurtenissen is gebouwd niet onaangekondigd een nieuwe familie gebeurtenissen gaat ontvangen; zet ze per endpoint aan op de pagina ervan of metPATCH … {"include_finding_events": true}. - Actieprefixen gelden net als voor auditgebeurtenissen. Een endpoint met prefixen ontvangt bevindinggebeurtenissen alleen als een daarvan overeenkomt, bijvoorbeeld
security.finding.. - Aflevering, bundeling, nieuwe pogingen, opnieuw versturen, HMAC, de auth-header en het bewaren van niet-afgeleverde gebeurtenissen werken hetzelfde als voor auditgebeurtenissen. Het
idvan de gebeurtenis is hetzelfde bij elke nieuwe poging en bij opnieuw versturen: ontdubbel erop.
Velden
fields van een bevindinggebeurtenis:
| Veld | Waarde | Cortex XDM |
|---|---|---|
alert_name | Korte titel, bijvoorbeeld CVE-2024-3094 in xz-utils (frem.sh/acme/api@sha256:…) | xdm.alert.name |
alert_category | Het type bevinding (tabel hierboven) | xdm.alert.category |
alert_severity | critical, high, medium, low of informational | xdm.alert.severity |
alert_description | Samenvatting van de advisory of bericht van de regel, hooguit 500 tekens, of null | xdm.alert.description |
original_alert_id | Stabiel id van de bevinding (32 hexadecimale tekens), hetzelfde bij elke gebeurtenis over één bevinding | xdm.alert.original_alert_id |
org | Slug van de organisatie | |
finding.type, finding.source | Type bevinding, en de scanner die haar rapporteerde | |
finding.state, finding.previous_state | open, dismissed of resolved; de toestand daarvoor (null bij de eerste keer) | |
finding.transition, finding.transition_reason | De overgang (zoals in action) en waarom: new, reappeared, undismissed, dismissal_expired, manual, severity_raised, new_vulnerability, approver_left, reachability_changed, dismissed, new_dismissed, not_detected, superseded, no_longer_present, allowed, package_deleted, listed, snapshot | |
finding.first_seen_at, finding.state_changed_at | Wanneer de bevinding voor het eerst werd gezien; wanneer ze in haar huidige toestand kwam | |
finding.key | De identiteit die de bevinding over nieuwe scans heen houdt, bijvoorbeeld dep:acme/api:GHSA-…:npm:lodash@4.17.20 | |
finding.row_id | Het huidige id van de bevinding in de findings-API; het verandert wanneer een nieuwe scan de rij vervangt | |
finding.console_url | Waar de bevinding in de console wordt getoond | |
finding.dismissal | {by, reason, expires_at, approved_by, exception_id} bij een gebeurtenis dismissed, anders null. approved_by en exception_id zijn ingevuld wanneer een goedgekeurde uitzondering de bevinding negeerde | |
finding.exception | {id, status, approved_by, revoked_by, revoked_reason, revoked_detail} van de uitzondering die eindigde, bij exception_revoked en bij een dismissal_expired die een uitzondering beëindigde; anders null | |
finding.revocation | {reason, detail, revoked_verdict, revoked_at} bij dismissal_revoked: de aanleiding (reachability_changed), wat er veranderde, het latere oordeel dat niet bereikbaar tegensprak, en wanneer; anders null | |
cvss | {score, vector} als de advisory er een heeft, anders null | |
identifiers | Lijsten met advisory-ids cve, ghsa, mal en other; rule_id (regels voor SAST, secrets en misconfiguratie in images, regels van het pakketbeleid); licence; signature (ClamAV) | |
component | Het getroffen component: purl, name, version, ecosystem, fixed_version, sha256. Null voor SAST en secrets. Bij een kwetsbaarheid in een image heeft het pakket in het image een naam maar geen purl; de purl van het image staat in location.image.purl. | |
location | kind (repository, image, package of upload), repository, path, line, commit_sha; image {reference, purl, proxied, tags}, met reference in de openbare vorm frem.sh/<org>/<image>@sha256:<digest>; package {type, owner, name, version, file, sha256}; upload_kind | xdm.target.resource.name (reference, repository of package) |
policy | Alleen bij gebeurtenissen over het pakketbeleid: {verdict, reasons} | |
known_exploited | Bij elke gebeurtenis: {cve_ids, sources} wanneer de bevinding een kwetsbaarheid in een afhankelijkheid of image is die als in het wild misbruikt wordt vermeld (sources: euvd voor ENISA EUVD, cisa_kev voor CISA KEV), anders null. De CVE-ids zijn die van de bevinding zelf of de aliassen van haar advisory. |
Voorbeeld (formaat json):
{
"id": "6f0b1f7e-2c55-4c55-9a0e-6b8f8f7e1a10",
"tenant_id": "11111111-1111-1111-1111-111111111111",
"actor": "system:finding-lifecycle",
"action": "security.finding.opened",
"fields": {
"alert_category": "dependency_vulnerability",
"alert_description": "Command injection in lodash",
"alert_name": "GHSA-35jh-r3h4-6jhm in lodash 4.17.20",
"alert_severity": "high",
"component": { "purl": "pkg:npm/lodash@4.17.20", "name": "lodash", "version": "4.17.20", "ecosystem": "npm", "fixed_version": "4.17.21", "sha256": null },
"cvss": { "score": 7.2, "vector": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H" },
"finding": {
"id": "3c1d2a8e5b0f4e6a9d7c1b2a3e4f5a6b",
"key": "dep:acme/api:GHSA-35jh-r3h4-6jhm:npm:lodash@4.17.20",
"type": "dependency_vulnerability",
"source": "dep-scan",
"state": "open",
"previous_state": null,
"transition": "opened",
"transition_reason": "new",
"first_seen_at": "2026-10-08T09:12:40.120Z",
"state_changed_at": "2026-10-08T09:13:02.457Z",
"row_id": "0d6c7c43-2b1f-4f5e-8a3e-0b2f9f1c2d3e",
"console_url": "https://frem.sh/acme/_admin/code-security/deps",
"dismissal": null
},
"identifiers": { "cve": [], "ghsa": ["GHSA-35jh-r3h4-6jhm"], "mal": [], "other": [], "rule_id": null, "licence": null, "signature": null },
"known_exploited": null,
"location": { "kind": "repository", "repository": "acme/api", "path": "package-lock.json", "line": null, "commit_sha": null, "image": null, "package": null, "upload_kind": null },
"org": "acme",
"original_alert_id": "3c1d2a8e5b0f4e6a9d7c1b2a3e4f5a6b"
},
"created_at": "2026-10-08T09:13:02.457Z",
"hash_chain": null
}Bevindinggebeurtenissen maken geen deel uit van de hashketen van de auditlog. Het wijzigen van de instelling voor bevindinggebeurtenissen van een endpoint en het aanvragen van een momentopname zijn wel auditgebeurtenissen (siem.endpoint.finding_events.update, siem.endpoint.finding_snapshot).
Bevindinggebeurtenissen in CEF
Een bevindinggebeurtenis gebruikt de CEF-toewijzing met deze verschillen:
| CEF | Waarde |
|---|---|
| Name | alert_name (Signature ID blijft de action) |
| Severity | Zolang de bevinding openstaat (opened, reopened, dismissal_expired, exception_revoked, dismissal_revoked, snapshot): 10 critical, 8 high, 5 medium, 3 low, 1 informational. 1 voor dismissed en resolved, zodat goed nieuws geen alarm geeft zoals de bevinding dat deed. known_exploited is minstens 8, ongeacht de ernst van de bevinding |
cat | alert_category (het type bevinding) |
msg | alert_name |
outcome | finding.state |
reason | finding.transition_reason |
start | finding.first_seen_at, in milliseconden sinds de epoch |
cs5Label=findingId, cs5 | original_alert_id |
cs6Label=purl, cs6 | component.purl, als die er is |
flexString1Label=identifiers, flexString1 | De CVE-, GHSA-, MAL- en overige ids, het regel-id, de licentie en de signature, gescheiden door komma’s |
flexString2Label=location, flexString2 | De imagereferentie, de repository of het pakket |
cfp1Label=cvssScore, cfp1 | cvss.score, als die er is |
filePath | location.path |
fileHash | component.sha256 (antivirusbevindingen, pakketbestanden) |
cs3 en cs4 zijn leeg, net als bij toegangsgebeurtenissen van de registry. Via syslog volgt de ernst volgens RFC 5424 de CEF-ernst, net als bij auditgebeurtenissen: een kritieke bevinding wordt verstuurd met 2 (critical), een opgeloste met 6 (informational).
Syslog via TLS
Een endpoint-URL in de vorm syslog+tls://host[:port] (standaardpoort 6514) levert via syslog af in plaats van via HTTPS:
- Transport: TCP met TLS 1.2 of hoger, ingekaderd met octet counting (RFC 5425). fremforge controleert het certificaat van de ontvanger tegen de openbare set CA’s, voor de hostnaam in de URL, dus de ontvanger heeft een openbaar vertrouwd certificaat nodig. Gebruik poort 6514 of 443; andere poorten zijn vanuit fremforge niet bereikbaar.
- Bericht: één RFC 5424-bericht per gebeurtenis,
<PRI>1 TIMESTAMP frem.sh fremforge - MSGID - MSG. PRI is facility 13 (log audit) × 8 + ernst: 2 bij CEF-ernst 9, 4 bij 7, 5 bij 5, anders 6. MSGID is deaction(tot 32 tekens). MSG is een UTF-8-byte-order-mark gevolgd door de CEF-regel (formaatcef) of de JSON-gebeurtenis (elk ander formaat). - Bundeling: tot max. gebeurtenissen berichten binnen max. grootte per verzoek delen één verbinding.
- Geen bevestiging. Syslog kent geen antwoord op applicatieniveau. Een bundel telt als afgeleverd wanneer elk bericht is geschreven en je ontvanger de verbinding netjes heeft gesloten. Een geweigerde verbinding, een TLS-fout of een ontvanger die de verbinding sluit voordat de bundel is geschreven, laat de bundel mislukken, en die wordt dan in zijn geheel opnieuw geprobeerd. Een ontvanger die de verbinding accepteert en daarna weggooit wat hij leest, lijkt afgeleverd. Ontdubbel op
externalId(CEF) ofid(JSON). - De auth-header en de HMAC-handtekening bestaan alleen bij HTTPS. Beperk je listener tot fremforge met TLS en, als dat nodig is, een allowlist van bronadressen.
De handtekening verifiëren
De HMAC wordt berekend over precies de bytes van de body, in de volgorde waarin ze binnenkomen. Parse en serialiseer niet opnieuw voordat je verifieert.
Ontvanger in Node.js (tijdstempel en body):
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(req, body) {
const sig = req.headers['x-fremforge-signature'];
const ts = req.headers['x-fremforge-timestamp'];
if (!sig?.startsWith('sha256=') || !ts) throw new Error('missing headers');
// 5-minute replay window. Adjust to your tolerance.
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) throw new Error('stale');
const expected = createHmac('sha256', SHARED_SECRET)
.update(`${ts}.${body}`)
.digest('hex');
const got = sig.slice(7); // strip "sha256="
if (expected.length !== got.length) throw new Error('bad sig');
if (!timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(got, 'hex'))) {
throw new Error('bad sig');
}
}Ontvanger in Python (tijdstempel en body):
import hmac, hashlib, time
def verify(headers, raw_body: bytes) -> None:
sig = headers.get('X-Fremforge-Signature', '')
ts = headers.get('X-Fremforge-Timestamp', '')
if not sig.startswith('sha256=') or not ts:
raise ValueError('missing headers')
if abs(time.time() - int(ts)) > 300:
raise ValueError('stale')
expected = hmac.new(
SHARED_SECRET.encode(),
f'{ts}.'.encode() + raw_body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, sig[7:]):
raise ValueError('bad sig')Wordt alleen de body ondertekend, laat dan het tijdstempel weg: HMAC-SHA256 over raw_body.
Foutafhandeling, nieuwe pogingen en opnieuw versturen
- Elk verzoek heeft een time-out van 10 seconden.
- Een antwoord dat geen
2xxis, een time-out of een verbindingsfout laat de gebeurtenissen in de wachtrij staan. Het endpoint wacht dan steeds langer: 5 seconden, verdubbeld bij elke volgende fout, hooguit 10 minuten, met ±20 % jitter. Zodra je ontvanger weer antwoordt, loopt de wachtrij op volgorde leeg. - Na 5 fouten op rij toont het endpoint
failingmet de reden van de laatste fout. De aflevering blijft het opnieuw proberen; je hoeft niets om te zetten. Een endpoint uitschakelen voorkomt dat er nieuwe gebeurtenissen voor in de wachtrij komen; als je het weer inschakelt, gaat de aflevering direct verder. - Een
413halveert de bundel. Een enkele gebeurtenis die je ontvanger nog steeds als te groot weigert, wordt verworpen en geteld. - Niet-afgeleverde gebeurtenissen worden bewaard zolang de bewaartermijn van de auditlog van je organisatie loopt (Organisatiebeheer → Auth-beleid; standaard 90 dagen), en hooguit 200 000 per endpoint. Oudere worden verworpen en geteld.
- De kaart Levering op de pagina van het endpoint (en het blok
deliveryin de API,dropped_total) toont het aantal wachtende gebeurtenissen, de oudste daarvan, de volgende poging, en het totaal afgeleverd en verworpen. Een gebeurtenis wordt alleen in deze gevallen verworpen, en dan nooit meer aan dat endpoint afgeleverd tenzij je haar opnieuw verstuurt: je ontvanger antwoordde413op die gebeurtenis alleen; ze wachtte nog toen ze ouder werd dan je bewaartermijn van de auditlog; het endpoint had al 200 000 wachtende gebeurtenissen, en dan gaan de oudste eerst; of de auditgebeurtenis zelf bestaat niet meer, wat binnen de bewaartermijn niet gebeurt. Niets anders verwerpt een gebeurtenis: een falende of uitgeschakelde ontvanger laat gebeurtenissen alleen wachten. - Opnieuw versturen zet je auditgebeurtenissen vanaf een bepaald moment opnieuw in de wachtrij (en, zodra bevindinggebeurtenissen beschikbaar zijn, op een endpoint dat ze meestuurt ook de bevindinggebeurtenissen uit dezelfde periode, nooit ouder dan het moment waarop het endpoint ze begon te ontvangen): Opnieuw versturen vanaf (UTC) op de pagina van het endpoint, of
POST /_app/api/v1/orgs/<org>/siem/endpoints/<id>/replaymet{"since": "<ISO 8601>", "until": "<ISO 8601, optional>"}.sincemag hooguit je bewaartermijn van de auditlog terug liggen. Eén verzoek zet hooguit 100 000 gebeurtenissen in de wachtrij en geeftnext_sinceterug als er meer zijn. Gebeurtenissen die nog wachten, worden niet dubbel ingepland; opnieuw verstuurde gebeurtenissen houden hunid.
De aflevering gebeurt minstens één keer: een nieuwe poging na een verloren antwoord, of opnieuw versturen, verstuurt een gebeurtenis nogmaals. Ontdubbel op id.
Gangbare SIEM’s
| SIEM | Leest via HTTPS in met | Instellingen in fremforge | Rechtstreeks vanuit fremforge? |
|---|---|---|---|
| Cortex XSIAM | HTTP log collector | Auth-header Authorization: <API key>, formaat json | Ja |
| Splunk | HTTP Event Collector, /services/collector/raw | Auth-header Authorization: Splunk <token>, formaat ndjson | Ja |
| Microsoft Sentinel | Logs Ingestion API van Azure Monitor | Heeft een kortlevend token van Entra ID nodig | Nee, via een relay |
| Elastic | HTTP-endpoint-input van Elastic Agent / Filebeat | Handtekening alleen over de body, eventueel een geheime header | Ja |
| IBM QRadar | Logbron HTTP Receiver, of een logbron TLS Syslog | Formaat cef | Ja, als de listener openbaar bereikbaar is |
| Google SecOps | Webhookfeed | URL met ?key=&secret=, formaat ndjson | Ja |
Cortex XSIAM
Leest JSON van derden in via een HTTP log collector (Settings → Data Sources & Integrations → + Add New → HTTP). Kies Log Format: JSON; de Vendor en Product die je invult, bepalen de naam van de dataset <vendor>_<product>_raw. De URL van de collector heeft de vorm https://api-<tenant external URL>/logs/v1/event en verwacht de API-sleutel in Authorization (documentatie van Palo Alto).
Inrichting: URL = de URL van de collector; auth-header Authorization = de API-sleutel van de collector; formaat json.
Voorbeeld van parsing- en datamodelregels voor fremforge_audit_raw
Parsingregel (Settings → Configurations → Data Management → Parsing Rules), zodat _time het tijdstip van de gebeurtenis in fremforge is in plaats van het tijdstip van aankomst. created_at heeft altijd drie decimalen en een Z:
[INGEST:vendor="fremforge", product="audit", target_dataset="fremforge_audit_raw", no_hit=keep]
alter _time = parse_timestamp("%Y-%m-%dT%H:%M:%E3SZ", created_at);Datamodelregel (Settings → Configurations → Data Management → Data Model Rules):
[MODEL: dataset="fremforge_audit_raw"]
filter action != "fremforge.siem.test"
| alter
xdm.event.id = id,
xdm.event.type = "fremforge_audit",
xdm.event.original_event_type = action,
xdm.event.operation_sub_type = action,
xdm.event.description = to_json_string(fields),
xdm.observer.vendor = "fremforge",
xdm.observer.product = "fremforge",
xdm.observer.unique_identifier = tenant_id,
xdm.source.user.identifier = actor;Beide gaan ervan uit dat XSIAM de JSON-sleutels op het hoogste niveau als kolommen opslaat. Toont je dataset alleen _raw_log, lees de waarden dan met json_extract_scalar(_raw_log, "$.created_at") enzovoort.
Datamodelregel voor bevindinggebeurtenissen
Bevindinggebeurtenissen (binnenkort beschikbaar) komen in dezelfde dataset terecht. Hun alertvelden zijn naar die van XDM genoemd, dus ze zijn één op één toe te wijzen. Gebruik je deze regel naast de auditregel hierboven, voeg dan | filter not (action ~= "^security\.finding\.") toe aan de auditregel, zodat elke gebeurtenis één keer wordt gemodelleerd:
[MODEL: dataset="fremforge_audit_raw"]
filter action ~= "^security\.finding\."
| alter
xdm.event.id = id,
xdm.event.type = "fremforge_finding",
xdm.event.original_event_type = action,
xdm.event.outcome = json_extract_scalar(fields, "$.finding.state"),
xdm.event.outcome_reason = json_extract_scalar(fields, "$.finding.transition_reason"),
xdm.alert.name = json_extract_scalar(fields, "$.alert_name"),
xdm.alert.category = json_extract_scalar(fields, "$.alert_category"),
xdm.alert.severity = json_extract_scalar(fields, "$.alert_severity"),
xdm.alert.description = json_extract_scalar(fields, "$.alert_description"),
xdm.alert.original_alert_id = json_extract_scalar(fields, "$.original_alert_id"),
xdm.target.resource.name = coalesce(json_extract_scalar(fields, "$.location.image.reference"), json_extract_scalar(fields, "$.location.repository"), json_extract_scalar(fields, "$.location.package.name")),
xdm.observer.vendor = "fremforge",
xdm.observer.product = "fremforge",
xdm.observer.unique_identifier = tenant_id,
xdm.source.user.identifier = actor;De HTTP log collector leest deze gebeurtenissen in als logrecords. Om er een XSIAM-alert van te maken, voeg je een correlatieregel toe op de dataset, bijvoorbeeld op action = "security.finding.opened" en alert_severity critical of high, en groepeer je op original_alert_id, zodat de latere gebeurtenissen over dezelfde bevinding (dismissed, resolved) eraan worden gekoppeld.
Splunk
Leest in via de HTTP Event Collector. /services/collector/raw neemt de body zoals hij is; zet sourcetype=_json in de URL, zodat elke regel één gebeurtenis is. Het token gaat in Authorization: Splunk <token>. Staat indexer acknowledgement aan, dan heeft het raw-endpoint ook een kanaal-GUID nodig; zet die in de URL als channel=<GUID> (documentatie van Splunk).
Inrichting: URL https://<hec-host>:8088/services/collector/raw?sourcetype=_json; auth-header Authorization = Splunk <token>; formaat ndjson.
Microsoft Sentinel
Leest eigen JSON in via de Logs Ingestion API van Azure Monitor, in een eigen _CL-tabel die door een data collection rule (DCR) wordt gedefinieerd. Het verzoek gaat naar {endpoint}/dataCollectionRules/{dcrImmutableId}/streams/{streamName}?api-version=2023-01-01 met een bearer token van Entra ID uit de client-credentials-flow (scope https://monitor.azure.com/.default), en de body moet een JSON-array zijn (documentatie van Microsoft). De oudere HTTP Data Collector API wordt sinds 14 september 2026 niet meer ondersteund; bouw er niet op.
Inrichting: het token verloopt na ongeveer een uur, dus een vaste auth-header kan het niet bevatten. Zet de relay ertussen: die haalt het token op, bewaart het in een cache en stuurt de body door. Gebruik formaat json_array, zodat de body al de array is die de API verwacht. Wijs created_at toe aan TimeGenerated in de DCR-transformatie.
Elastic
Leest in via de HTTP endpoint-input van Elastic Agent (de integratie Custom HTTP Endpoint Logs) of Filebeat. Die luistert op een eigen poort (standaard 8000), kan TLS afhandelen en kan een HMAC over de body controleren (documentatie van Elastic).
Inrichting: handtekening alleen over de body; stel in de input hmac.header: X-Fremforge-Signature, hmac.key: <the endpoint's HMAC secret>, hmac.type: sha256 en hmac.prefix: "sha256=" in. Wil je ook de optie voor een geheime header van de input gebruiken, stel dan dezelfde headernaam en waarde in als de auth-header van het endpoint. Formaat json, of ndjson voor bundels. Indexeer actor, action en tenant_id als keyword.
IBM QRadar
Leest de body’s van HTTP POST-verzoeken in via een logbron met het protocol HTTP Receiver. QRadar draait een HTTP- of HTTPS-server op een Event Collector (standaardpoort 12469); HTTPS heeft een certificaat van een CA nodig, en mutual TLS is optioneel (documentatie van IBM). CEF-regels worden geparset door de Universal CEF DSM van QRadar.
Inrichting: URL = de openbare HTTPS-URL van de receiver, of syslog+tls://<collector>:6514 voor een logbron TLS Syslog; formaat cef. fremforge kan geen clientcertificaat aanbieden, dus laat mutual TLS uit. Is de Event Collector niet vanaf internet bereikbaar, dan stuurt een reverse proxy of syslog-relay aan de rand van je netwerk door.
Google SecOps (Chronicle)
Leest in via een webhookfeed (Settings → Feeds → Add new, brontype Webhook, kies dan een logtype). Verzoeken gaan naar het endpoint ...:importPushLogs van de feed, met een API-sleutel en het secret van de feed, als headers X-goog-api-key en X-Webhook-Access-Key, of als ?key=<API key>&secret=<secret>. Meerdere JSON-objecten, gescheiden door regeleinden, kunnen één verzoek delen, tot 4 MB (documentatie van Google). Er is geen kant-en-klare parser voor fremforge; je hebt een logtype en een parser voor deze gebeurtenissen nodig.
Inrichting: URL = het endpoint van de feed met ?key=<API key>&secret=<secret> (versleuteld opgeslagen en getoond met ***); formaat ndjson; max. grootte per verzoek hooguit 4096 KiB.
Een relay die de handtekening verifieert
Sentinel, en elke ontvanger die meer dan één header of een kortlevend token nodig heeft, gebruikt een kleine dienst: die verifieert de handtekening van fremforge en stuurt de body daarna ongewijzigd door met het inloggegeven van de SIEM. Deze versie gebruikt alleen de standaardbibliotheek van Python; stel TARGET_URL en TARGET_AUTH in voor je SIEM. Voeg voor Sentinel het ophalen van het token toe; met formaat json_array is de body al de array die Sentinel verwacht. MAX_BODY komt overeen met de standaard verzoekgrootte van 1 MiB. De relay stuurt een correct ondertekende gebeurtenis door met het inloggegeven, antwoordt 401 op een verkeerde handtekening of een tijdstempel dat meer dan vijf minuten oud is, en 502 wanneer het doel onbereikbaar is.
"""fremforge SIEM forwarder -> SIEM relay: verify the signature, forward with the SIEM's credential."""
import hashlib
import hmac
import os
import time
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
SECRET = os.environ["FREMFORGE_SIEM_SECRET"].encode()
TARGET_URL = os.environ["TARGET_URL"] # e.g. https://api-<tenant>/logs/v1/event
TARGET_AUTH = os.environ["TARGET_AUTH"] # e.g. "<XSIAM API key>" or "Splunk <HEC token>"
MAX_SKEW_S = 300
MAX_BODY = 1024 * 1024
class Relay(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length") or 0)
if length <= 0 or length > MAX_BODY:
return self.reply(413)
body = self.rfile.read(length)
sig = self.headers.get("X-Fremforge-Signature", "")
ts = self.headers.get("X-Fremforge-Timestamp", "")
if not sig.startswith("sha256=") or not ts.isdigit():
return self.reply(401)
if abs(time.time() - int(ts)) > MAX_SKEW_S:
return self.reply(401)
expected = hmac.new(SECRET, ts.encode() + b"." + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig[7:]):
return self.reply(401)
req = urllib.request.Request(
TARGET_URL,
data=body,
method="POST",
headers={"Authorization": TARGET_AUTH, "Content-Type": "application/json"},
)
try:
# fremforge gives up after 10 s; answer before that.
with urllib.request.urlopen(req, timeout=4) as res:
status = res.status
except urllib.error.HTTPError as e:
status = e.code
except OSError:
status = 0
# Pass failure back, so fremforge keeps the event and retries it.
self.reply(200 if 200 <= status < 300 else 502)
def reply(self, code):
self.send_response(code)
self.send_header("Content-Length", "0")
self.end_headers()
def log_message(self, fmt, *args):
pass
if __name__ == "__main__":
ThreadingHTTPServer(("0.0.0.0", int(os.environ.get("PORT", "8080"))), Relay).serve_forever()Draai de relay achter je gebruikelijke TLS-terminator: fremforge levert alleen af aan openbare https://-URL’s. Houd het antwoord van de relay onder de 10 seconden, en geef een antwoord dat geen 2xx is wanneer de SIEM de gebeurtenis weigerde. Een relay die altijd 200 antwoordt, verliest gebeurtenissen: fremforge beschouwt een 2xx als afgeleverd en haalt de gebeurtenis uit de wachtrij.
Generieke webhook
Elk HTTPS-endpoint dat 2xx teruggeeft zodra het het verzoek heeft geaccepteerd. Verifieer met de codevoorbeelden hierboven. Dit past bij zelf gehoste SIEM’s (Wazuh, Graylog, …) en HTTP-intakes zoals Logflare of Datadog.
Gezondheid aan de kant van de operator
Operators van fremverk zien de toestand van elk endpoint op de pagina SIEM-gezondheid van het operatorportaal. Er gaat een alarm af wanneer de oudste niet-afgeleverde gebeurtenis van een endpoint meer dan 24 uur oud is, en dan nemen we contact op met de eigenaren van je organisatie. Stel ook in je SIEM een alarm in op het uitblijven van gebeurtenissen van fremforge.
Wat niet over de lijn gaat
- Syslog via UDP of gewone TCP. Syslog gaat alleen via TLS.
- Clientcertificaten (mutual TLS) richting je ontvanger.
- Meer dan één eigen header per endpoint.
- Fan-out over tenants heen. Elk endpoint hoort bij één tenant. Er is geen spiegel aan de kant van de operator; gebeurtenissen van je organisatie verlaten nooit de keten van de tenant.
Gerelateerd
- Integriteit van de auditketen: wat er werkelijk in de gebeurtenissen staat, en hoe je de keten van begin tot eind verifieert.
- Webhooks: dezelfde semantiek op de lijn voor gebeurtenissen van repository’s en organisaties, op dezelfde manier ondertekend en afgeleverd.
- Beveiligingsmodel van de gehoste runners: waar de gebeurtenissen die runners uitzenden in de keten vandaan komen.