Naar de hoofdinhoud

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

  1. Ga naar Organisatiebeheer → SIEM-doorsturen → Een endpoint toevoegen.
  2. Vul een Naam in (vrije tekst, uniek binnen de organisatie) en de URL van je ontvanger: https://…, of syslog+tls://host:port voor syslog via TLS. http://, localhost, RFC1918-adressen en metadata-adressen worden geweigerd; gebruik een ontvanger die openbaar bereikbaar is.
  3. 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.
  4. Beperk eventueel de allowlist Prefixen voor gebeurtenisacties (bijvoorbeeld forgejo.push, billing.). Leeg betekent: alles doorsturen. Elke prefix wordt vergeleken met action.startsWith(prefix).
  5. 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.blocked en registry.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.
  6. Stel eventueel een Auth-header in: een headernaam en een geheime waarde die bij elk verzoek worden meegestuurd (bijvoorbeeld Authorization en Splunk <token>). Zie Authenticatie.
  7. Kies wat de Handtekening omvat (standaard tijdstempel en body, of alleen de body).
  8. Stel de bundelgrootte, de verzoekgrootte en de wachttijd in.
  9. 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-* en X-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 het id van de gebeurtenis.

Contract op de lijn

Elke aflevering via HTTPS is een POST:

HeaderWaarde
Content-Typeapplication/json (json, json_array), application/x-ndjson (ndjson), text/plain; charset=utf-8 (cef)
User-Agentfremforge-siem-forwarder/1
X-Fremforge-TimestampUnix-seconden (string) waarop fremforge het verzoek opbouwde
X-Fremforge-Signaturesha256=<hex> over <timestamp>.<body>, of over <body> als alleen de body wordt ondertekend
Idempotency-KeyHet 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-CountAantal gebeurtenissen in de body
je auth-headerAls 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

FormaatBodyGebeurtenissen per verzoek
json (standaard)Eén gebeurtenisobject1
ndjsonEén gebeurtenisobject per regel, elke regel eindigt op \nTot max. gebeurtenissen
json_arrayEen JSON-array van gebeurtenisobjectenTot max. gebeurtenissen
cefEén CEF-regel per gebeurtenis, elke regel eindigt op \nTot 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>

CEFWaarde
Signature ID, Nameaction
Severity9 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
rtcreated_at, in milliseconden sinds de epoch
externalIdid (de sleutel om te ontdubbelen)
suseractor
actaction
cataction tot de eerste .
cs1Label=tenantId, cs1tenant_id
cs2Label=fields, cs2fields als JSON
cs3Label=hashChainThis, cs3hash_chain.this_hash
cs4Label=hashChainPrev, cs4hash_chain.prev_hash, leeg als die er niet is
dvchostfrem.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_events en de momentopname niet aan, en wordt er geen gebeurtenis security.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_categoryBevindingBron
dependency_vulnerabilityEen kwetsbare afhankelijkheid op de standaardbranchScan van afhankelijkheden
image_vulnerabilityEen kwetsbaarheid of misconfiguratie in een containerimage in je registry (inclusief geproxyde images) of in een scan van een repositoryImagescan
licenceEen afhankelijkheid onder een licentie die forbidden of restricted isLicentiescan
sastEen bevinding van statische analyse op de standaardbranchSAST
secretEen gecommit secret (nooit het secret zelf)Secretscan
malwareEen kwaadaardig pakket (OSV MAL-*), of een upload die door de antivirusscan (ClamAV) is geweigerdScans van afhankelijkheden en de registry, scannen van uploads
package_policyEen bestand in de pakketregistry dat het pakketbeleid blokkeert of waarvoor het waarschuwtPakketbeleid

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

actionWanneer
security.finding.openedDe bevinding wordt voor het eerst gedetecteerd.
security.finding.reopenedDe bevinding wordt opnieuw gedetecteerd nadat ze was opgelost (transition_reason reappeared), of het negeren ervan werd ingetrokken voordat het verliep (undismissed).
security.finding.dismissedIemand 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_expiredHet 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_revokedEen 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_revokedfremforge 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.resolvedDe 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_exploitedEen 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.snapshotEé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_since in 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 gebeurtenis security.finding.snapshot per openstaande bevinding in de wachtrij, alleen voor dat endpoint. Dit kan één keer per endpoint; een tweede verzoek krijgt 409 als antwoord.
  • Standaard. Een endpoint dat na de release wordt aangemaakt, stuurt bevindinggebeurtenissen mee, tenzij je het vakje uitvinkt (of "include_finding_events": false meestuurt). 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 met PATCH … {"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 id van de gebeurtenis is hetzelfde bij elke nieuwe poging en bij opnieuw versturen: ontdubbel erop.

Velden

fields van een bevindinggebeurtenis:

VeldWaardeCortex XDM
alert_nameKorte titel, bijvoorbeeld CVE-2024-3094 in xz-utils (frem.sh/acme/api@sha256:…)xdm.alert.name
alert_categoryHet type bevinding (tabel hierboven)xdm.alert.category
alert_severitycritical, high, medium, low of informationalxdm.alert.severity
alert_descriptionSamenvatting van de advisory of bericht van de regel, hooguit 500 tekens, of nullxdm.alert.description
original_alert_idStabiel id van de bevinding (32 hexadecimale tekens), hetzelfde bij elke gebeurtenis over één bevindingxdm.alert.original_alert_id
orgSlug van de organisatie
finding.type, finding.sourceType bevinding, en de scanner die haar rapporteerde
finding.state, finding.previous_stateopen, dismissed of resolved; de toestand daarvoor (null bij de eerste keer)
finding.transition, finding.transition_reasonDe 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_atWanneer de bevinding voor het eerst werd gezien; wanneer ze in haar huidige toestand kwam
finding.keyDe identiteit die de bevinding over nieuwe scans heen houdt, bijvoorbeeld dep:acme/api:GHSA-…:npm:lodash@4.17.20
finding.row_idHet huidige id van de bevinding in de findings-API; het verandert wanneer een nieuwe scan de rij vervangt
finding.console_urlWaar 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
identifiersLijsten 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)
componentHet 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.
locationkind (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_kindxdm.target.resource.name (reference, repository of package)
policyAlleen bij gebeurtenissen over het pakketbeleid: {verdict, reasons}
known_exploitedBij 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:

CEFWaarde
Namealert_name (Signature ID blijft de action)
SeverityZolang 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
catalert_category (het type bevinding)
msgalert_name
outcomefinding.state
reasonfinding.transition_reason
startfinding.first_seen_at, in milliseconden sinds de epoch
cs5Label=findingId, cs5original_alert_id
cs6Label=purl, cs6component.purl, als die er is
flexString1Label=identifiers, flexString1De CVE-, GHSA-, MAL- en overige ids, het regel-id, de licentie en de signature, gescheiden door komma’s
flexString2Label=location, flexString2De imagereferentie, de repository of het pakket
cfp1Label=cvssScore, cfp1cvss.score, als die er is
filePathlocation.path
fileHashcomponent.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 de action (tot 32 tekens). MSG is een UTF-8-byte-order-mark gevolgd door de CEF-regel (formaat cef) 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) of id (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 2xx is, 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 failing met 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 413 halveert 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 delivery in 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 antwoordde 413 op 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>/replay met {"since": "<ISO 8601>", "until": "<ISO 8601, optional>"}. since mag hooguit je bewaartermijn van de auditlog terug liggen. Eén verzoek zet hooguit 100 000 gebeurtenissen in de wachtrij en geeft next_since terug als er meer zijn. Gebeurtenissen die nog wachten, worden niet dubbel ingepland; opnieuw verstuurde gebeurtenissen houden hun id.

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

SIEMLeest via HTTPS in metInstellingen in fremforgeRechtstreeks vanuit fremforge?
Cortex XSIAMHTTP log collectorAuth-header Authorization: <API key>, formaat jsonJa
SplunkHTTP Event Collector, /services/collector/rawAuth-header Authorization: Splunk <token>, formaat ndjsonJa
Microsoft SentinelLogs Ingestion API van Azure MonitorHeeft een kortlevend token van Entra ID nodigNee, via een relay
ElasticHTTP-endpoint-input van Elastic Agent / FilebeatHandtekening alleen over de body, eventueel een geheime headerJa
IBM QRadarLogbron HTTP Receiver, of een logbron TLS SyslogFormaat cefJa, als de listener openbaar bereikbaar is
Google SecOpsWebhookfeedURL met ?key=&secret=, formaat ndjsonJa

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