Zum Hauptinhalt springen

SIEM-Weiterleitung

fremforge schreibt jede zustandsändernde Admin-Aktion in ein Audit-Log pro Tenant mit Integritätsgarantie über eine Hash-Kette (siehe Integrität der Audit-Kette). Dieselben Ereignisse lassen sich an dein SIEM weiterleiten, sobald sie geschrieben sind, sodass deine Erkennungsregeln sie nach Sekunden sehen und nicht erst beim nächsten CSV-Export.

Demnächst: Derselbe Stream wird auch ein Ereignis pro Änderung im Lebenszyklus eines Sicherheitsbefunds übertragen: ein Schwachstellen-, Lizenz-, SAST-, Secret-, Malware- oder Paketrichtlinien-Befund, der geöffnet, wieder geöffnet, verworfen oder erledigt wird. Siehe Befund-Ereignisse.

Diese Seite beschreibt den Aufbau der Weiterleitung, den Übertragungsvertrag für die Verifizierung und wie verbreitete SIEMs die Ereignisse aufnehmen.

So funktioniert die Zustellung

audit-emit schreibt die verkettete Zeile und reiht das Ereignis in derselben Datenbanktransaktion für jeden aktivierten Endpunkt ein, dessen Aktionspräfixe passen. Ein Zustellungs-Worker sendet die Warteschlange jedes Endpunkts in Reihenfolge, je nach Format des Endpunkts ein oder mehrere Ereignisse pro Anfrage. Ein Ereignis wird erst aus der Warteschlange entfernt, wenn dein Empfänger mit 2xx antwortet.

Die Einstellungen findest du unter Org-Admin → Integration → SIEM-Weiterleitung (/<org>/_admin/siem), und dieselben Einstellungen gibt es in der REST-API (/_app/api/v1/orgs/<org>/siem/endpoints).

Die Seite SIEM-Weiterleitung enthält das Formular Endpunkt hinzufügen und die Liste der Endpunkte: Name, URL (Zugangsdaten maskiert), Status, letzte Zustellung und die Anzahl wartender Ereignisse. Der Name jedes Endpunkts öffnet seine eigene Seite (/<org>/_admin/siem/<endpoint id>), mit einer Karte pro Einstellung:

  • Übersicht: Status, der letzte Fehlergrund, der Filter für Aktionspräfixe, Deaktivieren/Aktivieren und Test senden.
  • Auth-Header: setzen, rotieren oder entfernen.
  • Signatur: was der HMAC abdeckt.
  • Zustellung: wartende Ereignisse, das älteste davon, der nächste Versuch, Summen der zugestellten und verworfenen Ereignisse sowie die Einstellungen für Format und Bündelung.
  • Erneut senden: vergangene Ereignisse noch einmal senden.
  • Endpunkt löschen: Gib zur Bestätigung den Namen des Endpunkts ein.

Einen Endpunkt konfigurieren

  1. Gehe zu Org-Admin → SIEM-Weiterleitung → Endpunkt hinzufügen.
  2. Gib einen Namen ein (freier Text, eindeutig innerhalb der Organisation) und die URL deines Empfängers: https://… oder syslog+tls://host:port für Syslog über TLS. http://, localhost, RFC1918- und Metadaten-Adressen werden abgelehnt; verwende einen öffentlich erreichbaren Empfänger.
  3. Wähle das Format (siehe Formate und Bündelung). Die übrigen Schritte liegen unter Erweiterte Optionen und lassen sich auch später auf der Seite des Endpunkts ändern.
  4. Schränke optional die Positivliste der Aktionspräfixe ein (z. B. forgejo.push, billing.). Leer bedeutet, dass alles weitergeleitet wird. Jedes Präfix wird mit action.startsWith(prefix) abgeglichen.
  5. Aktiviere optional Registry-Zugriffslog einbeziehen, um zusätzlich ein Ereignis pro Anfrage an die Paket- oder Container-Registry zu erhalten (registry.access.*). Standardmäßig ist das aus; siehe Zugriffsprotokoll der Registry. Die Pull-Entscheidungen der Image-Gates, registry.pull.blocked und registry.pull.would_block, sind Audit-Ereignisse, keine Zugriffsereignisse. Sie werden daher ohne diese Option weitergeleitet, eines pro Image-Digest, Gate und Entscheidung pro Stunde; siehe Container-Pull-Gates. Demnächst: Befund-Ereignisse einbeziehen, standardmäßig angehakt, wird dafür sorgen, dass der Endpunkt ab dem Moment seiner Erstellung zusätzlich ein Ereignis pro Änderung im Lebenszyklus eines Sicherheitsbefunds erhält (security.finding.*). Bis zur Veröffentlichung zeigt das Formular die Option nicht. Siehe Befund-Ereignisse.
  6. Setze optional einen Auth-Header: einen Header-Namen und einen geheimen Wert, die bei jeder Anfrage gesendet werden (zum Beispiel Authorization und Splunk <token>). Siehe Authentifizierung.
  7. Wähle, was die Signatur umfasst (standardmäßig Zeitstempel und Body, oder nur den Body).
  8. Lege Bündelgröße, Anfragegröße und Wartezeit fest.
  9. Klicke auf Endpunkt anlegen. Das gemeinsame HMAC-Secret wird auf dem nächsten Bildschirm einmal angezeigt; kopiere es in die Konfiguration deines Empfängers. fremforge zeigt es nie wieder an. Verlierst du es, lösche den Endpunkt und lege ihn neu an.

Der Endpunkt ist sofort aktiv; die Bestätigung verlinkt auf seine Seite. Test senden sendet ein synthetisches Ereignis fremforge.siem.test im Format des Endpunkts, mit seinen Headern und seiner Signatur, und zeigt den Antwortstatus und die ersten 200 Bytes der Antwort des Empfängers.

Zugangsdaten in der URL. Manche Collectoren erwarten ihre Zugangsdaten in der URL (?token=, ?key=&secret= oder user:password@). fremforge speichert die URL wie eingegeben verschlüsselt und zeigt sie überall sonst (Admin-Seite, API, Testergebnis, Audit-Log) mit *** anstelle jedes Query-Werts und der Benutzerinformation. Nur die Zustellung verwendet die vollständige URL.

Authentifizierung

Zwei Mechanismen, die sich kombinieren lassen:

  • Auth-Header. Ein Header pro Endpunkt, Name und Wert nach deiner Wahl. Der Wert wird verschlüsselt gespeichert und kann nur geschrieben werden: Keine Seite, keine API-Antwort, keine Logzeile und kein Audit-Ereignis enthält ihn. Das Audit-Log erfasst den Header-Namen und wann er gesetzt wurde. Zum Rotieren speicherst du einen neuen Wert; der alte wird sofort nicht mehr gesendet, also hinterlege den neuen Schlüssel zuerst in deinem Collector. Header-Namen, die die Anfrage kaputt machen würden, werden abgelehnt: Host, Content-Length, Content-Type, Content-Encoding, Transfer-Encoding, Connection, Keep-Alive, Upgrade, TE, Trailer, Expect, Accept-Encoding, User-Agent, Cookie, Proxy-* und X-Fremforge-*. Werte bestehen aus druckbarem ASCII, bis zu 4096 Zeichen.
  • HMAC-Signatur. Jede Anfrage trägt X-Fremforge-Signature: sha256=<hex>, mit dem HMAC-Secret des Endpunkts als Schlüssel. Standardmäßig deckt sie <timestamp>.<body> ab, sodass dein Empfänger eine alte Anfrage anhand ihres Zeitstempels ablehnen kann. Mit Signatur umfasst: nur Body deckt sie allein den rohen Body ab. Das prüfen Empfänger mit eingebauter Body-Verifizierung (der HTTP-Endpoint-Input von Elastic). Ein Empfänger mit reiner Body-Signatur kann eine wiederholte Anfrage nicht anhand ihres Alters ablehnen; dedupliziere stattdessen über die id des Ereignisses.

Übertragungsvertrag

Jede HTTPS-Zustellung ist ein POST:

HeaderWert
Content-Typeapplication/json (json, json_array), application/x-ndjson (ndjson), text/plain; charset=utf-8 (cef)
User-Agentfremforge-siem-forwarder/1
X-Fremforge-TimestampUnix-Sekunden (String), zu denen fremforge die Anfrage erstellt hat
X-Fremforge-Signaturesha256=<hex> über <timestamp>.<body> oder im Modus nur Body über <body>
Idempotency-KeyDie id des Ereignisses bei einem einzelnen Ereignis; bei einem Bündel ein SHA-256 über die IDs der Ereignisse des Bündels in Reihenfolge
X-Fremforge-Event-CountAnzahl der Ereignisse im Body
dein Auth-HeaderWenn gesetzt

Jedes Ereignis hat diese Form (Schlüssel der obersten Ebene in dieser Reihenfolge; die Schlüssel von fields.* alphabetisch sortiert, damit die Bytes stabil sind):

{
  "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 ist die ID des Audit-Ereignisses. Sie ist bei jeder Wiederholung und jedem erneuten Senden dieses Ereignisses gleich: Dedupliziere darüber.

actor ist ein reiner Forgejo-Benutzername bei einer Aktion einer Person (in der Web-Konsole und bei vielen API-Aufrufen der Benutzer, der das Token erzeugt hat), oidc:idp:<sub> bei einer Person, die sich über deinen IdP angemeldet hat und über ein eingetauschtes Token handelt, token:<id> bei anderen API-Aufrufen, operator:<email> bei einer Aktion von fremverk-Mitarbeitenden und andere Formen <kind>:<id> bei Systemkomponenten.

hash_chain.this_hash ist derselbe Hash, der in OBS Object Lock verankert wird, sodass dein SIEM nachweisen kann, dass ein Ereignis älter ist als ein bekannter Anker.

Ein Registry-Zugriffsereignis (nur bei einem Endpunkt mit include_access_log) hat dieselbe Form, mit action registry.access.<action> und "hash_chain": null: Zugriffsereignisse sind nicht Teil der Audit-Hash-Kette. Seine fields sind das Ereignis, wie die API des Zugriffsprotokolls es liefert.

Ein Befund-Ereignis (demnächst; nur bei einem Endpunkt mit include_finding_events) wird ebenfalls diese Form haben, mit action security.finding.<transition> und "hash_chain": null.

Formate und Bündelung

FormatBodyEreignisse pro Anfrage
json (Standard)Ein Ereignisobjekt1
ndjsonEin Ereignisobjekt pro Zeile, jede Zeile endet mit \nBis zu max. Ereignisse
json_arrayEin JSON-Array aus EreignisobjektenBis zu max. Ereignisse
cefEine CEF-Zeile pro Ereignis, jede endet mit \nBis zu max. Ereignisse

Für die gebündelten Formate:

  • Max. Ereignisse pro Anfrage: 1 bis 1000 (100, wenn du zu einem gebündelten Format wechselst, ohne etwas zu wählen).
  • Max. Anfragegröße: 1 KiB bis 4 MiB (Standard 1 MiB). Ein Bündel wird geschlossen, bevor es sie überschreiten würde; ein einzelnes Ereignis wird immer gesendet.
  • Max. Wartezeit: 0 bis 60 000 ms (Standard 0). Wie lange ein teilweise gefülltes Bündel auf weitere Ereignisse wartet, bevor es gesendet wird.

Ereignisse werden in der Reihenfolge zugestellt, in der sie geschrieben wurden.

CEF-Feldzuordnung

CEF:0|fremverk|fremforge|1|<action>|<action>|<severity>|<extension>

CEFWert
Signature ID, Nameaction
Severity9, wenn hash_chain.this_hash chain-broken ist; 7, wenn action fail, denied, deny, blocked, violation, malware, secret, breach oder tamper enthält; 5, wenn es delete, revoke, disable, remove, bypass, suspend oder dismiss enthält; sonst 3
rtcreated_at, Millisekunden seit der Epoche
externalIdid (der Schlüssel zur Deduplizierung)
suseractor
actaction
cataction bis zum ersten .
cs1Label=tenantId, cs1tenant_id
cs2Label=fields, cs2fields als JSON
cs3Label=hashChainThis, cs3hash_chain.this_hash
cs4Label=hashChainPrev, cs4hash_chain.prev_hash, leer, wenn es keinen gibt
dvchostfrem.sh

Header-Felder maskieren \ und |; Extension-Werte maskieren \, = und Zeilenumbrüche (\n, \r). Ein Wert aus einem Audit-Ereignis kann daher kein neues Feld und keine neue Zeile beginnen.

Befund-Ereignisse

Demnächst. Befund-Ereignisse sind noch nicht verfügbar. Dieser Abschnitt beschreibt sie vor ihrer Veröffentlichung; bis dahin bieten Konsole und API weder include_finding_events noch die Momentaufnahme an, und es wird kein Ereignis security.finding.* gesendet. Das Changelog kündigt die Veröffentlichung an.

Ein Endpunkt mit aktiviertem Befund-Ereignisse einbeziehen (include_finding_events in der API) erhält jedes Mal ein Ereignis, wenn ein Sicherheitsbefund seinen Zustand ändert. Jeder Befundtyp, den fremforge meldet, ist abgedeckt:

alert_categoryBefundQuelle
dependency_vulnerabilityEine verwundbare Abhängigkeit auf dem Standard-BranchAbhängigkeits-Scan
image_vulnerabilityEine Schwachstelle oder Fehlkonfiguration in einem Container-Image in deiner Registry (einschließlich über den Proxy bezogener Images) oder in einem Repository-ScanImage-Scan
licenceEine Abhängigkeit unter einer Lizenz mit forbidden oder restrictedLizenz-Scan
sastEin Befund der statischen Analyse auf dem Standard-BranchSAST
secretEin committetes Secret (nie das Secret selbst)Secret-Scan
malwareEin schädliches Paket (OSV MAL-*) oder ein vom Virenscan (ClamAV) abgelehnter UploadAbhängigkeits- und Registry-Scans, Upload-Scan
package_policyEine Datei in der Paket-Registry, die die Paketrichtlinie blockiert oder bei der sie warntPaketrichtlinie

Befunde aus Pull-Request-Scans sind nicht enthalten: Sie kommen und gehen mit jedem Pull Request. Der Lebenszyklus eines Befunds wird auf dem Standard-Branch verfolgt. Ereignisse zu Paketrichtlinien werden nur für Organisationen gesendet, in denen Ergebnisse der Paketrichtlinie sichtbar sind.

Ereigniskatalog

actionWann
security.finding.openedDer Befund wird zum ersten Mal erkannt.
security.finding.reopenedDer Befund wird erneut erkannt, nachdem er erledigt war (transition_reason reappeared), oder seine Verwerfung wurde vor ihrem Ablauf zurückgenommen (undismissed).
security.finding.dismissedJemand hat den Befund verworfen (Risiko akzeptiert, Fehlalarm), eine Positivliste deckt ihn ab, oder ein Antivirus-Befund wurde übersteuert. actor ist der Benutzer, der ihn verworfen hat, oder system:<kind>.
security.finding.dismissal_expiredEine befristete Verwerfung ist abgelaufen, und der Befund ist wieder offen. Dazu gehört auch eine Ausnahme, die ihr Ablaufdatum erreicht hat.
security.finding.exception_revokedEine genehmigte Ausnahme, die den Befund verworfen hatte, wurde widerrufen, und der Befund ist wieder offen. transition_reason ist der Auslöser: manual (jemand hat sie widerrufen), severity_raised, new_vulnerability oder approver_left. actor ist die Person, die sie widerrufen hat, oder system:exception-revocation.
security.finding.dismissal_revokedfremforge hat eine Verwerfung widerrufen, die sich auf ein Erreichbarkeitsurteil nicht erreichbar stützte, weil ein späterer Scan ihm widerspricht, und der Befund ist wieder offen. transition_reason ist reachability_changed, finding.revocation nennt den Grund, und actor ist system:exception-revocation. (Vor dem 2026-10-12 wurde das als dismissal_expired gesendet.) Eine widerrufene Ausnahme ist exception_revoked mit demselben Grund.
security.finding.resolvedDer Befund wird nicht mehr erkannt: behoben, entfernt, abgelöst durch einen neueren Scan, der ihn nicht mehr meldet, oder die Paketdatei wurde gelöscht.
security.finding.known_exploitedEine offene oder verworfene Schwachstelle in einer Abhängigkeit oder einem Image wurde in der EU Vulnerability Database der ENISA oder im KEV-Katalog der CISA als in freier Wildbahn ausgenutzt gelistet. Wird einmal pro Listung gesendet. transition_reason ist listed (die Liste hat ihn inzwischen aufgenommen) oder new (der Befund war beim ersten Auftreten bereits gelistet; das Ereignis wird zusammen mit seinem opened gesendet, in beliebiger Reihenfolge). known_exploited nennt die CVEs und die Listen, und actor ist system:known-exploited-feed. Auch ein verworfener Befund wird gemeldet: Das Risiko, unter dem er akzeptiert wurde, hat sich geändert.
security.finding.snapshotEines pro offenem Befund, einmal gesendet, wenn du eine Momentaufnahme anforderst.

Änderungen werden innerhalb von etwa einer Minute erfasst. Ein Befund behält über Rescans hinweg eine Identität, daher sendet ein Rescan, der denselben Befund erneut meldet, nichts; fields.original_alert_id ist bei jedem Ereignis zu einem Befund gleich.

Was gesendet wird und ab wann

  • Nur Änderungen ab dem Opt-in. Ein Endpunkt erhält Ereignisse zu Änderungen, die geschehen, nachdem er begonnen hat, sie zu empfangen (finding_events_since in der API). Das Einschalten der Befund-Ereignisse sendet deine bestehenden Befunde nicht.
  • Momentaufnahme offener Befunde. Um zu laden, was jetzt offen ist, verwende Momentaufnahme offener Befunde senden auf der Seite des Endpunkts oder POST /_app/api/v1/orgs/<org>/siem/endpoints/<id>/finding-snapshot. Das reiht ein Ereignis security.finding.snapshot pro offenem Befund ein, nur für diesen Endpunkt. Sie kann einmal pro Endpunkt erstellt werden; eine zweite Anfrage wird mit 409 beantwortet.
  • Standard. Ein Endpunkt, der nach der Veröffentlichung angelegt wird, enthält Befund-Ereignisse, sofern du das Häkchen nicht entfernst (oder "include_finding_events": false sendest). Endpunkte, die vor der Veröffentlichung existieren, bleiben, wie sie waren, ohne Befund-Ereignisse, damit ein für Audit-Ereignisse gebauter Empfänger nicht unangekündigt eine neue Ereignisfamilie erhält; schalte sie pro Endpunkt auf seiner Seite oder mit PATCH … {"include_finding_events": true} ein.
  • Aktionspräfixe gelten wie für Audit-Ereignisse. Ein Endpunkt mit Präfixen erhält Befund-Ereignisse nur, wenn eines davon passt, zum Beispiel security.finding..
  • Zustellung, Bündelung, Wiederholung, erneutes Senden, HMAC, der Auth-Header und die Aufbewahrung nicht zugestellter Ereignisse funktionieren wie bei Audit-Ereignissen. Die id des Ereignisses ist bei jeder Wiederholung und jedem erneuten Senden gleich: Dedupliziere darüber.

Felder

fields eines Befund-Ereignisses:

FeldWertCortex XDM
alert_nameKurzer Titel, z. B. CVE-2024-3094 in xz-utils (frem.sh/acme/api@sha256:…)xdm.alert.name
alert_categoryDer Befundtyp (Tabelle oben)xdm.alert.category
alert_severitycritical, high, medium, low oder informationalxdm.alert.severity
alert_descriptionZusammenfassung der Advisory oder Meldung der Regel, höchstens 500 Zeichen, oder nullxdm.alert.description
original_alert_idStabile Befund-ID (32 Hex-Zeichen), bei jedem Ereignis zu einem Befund gleichxdm.alert.original_alert_id
orgSlug der Organisation
finding.type, finding.sourceBefundtyp und der Scanner, der ihn gemeldet hat
finding.state, finding.previous_stateopen, dismissed oder resolved; der vorherige Zustand (null beim ersten Auftreten)
finding.transition, finding.transition_reasonDer Übergang (wie in action) und warum: 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_atWann der Befund zum ersten Mal gesehen wurde; wann er in seinen aktuellen Zustand gewechselt ist
finding.keyDie Identität, die der Befund über Rescans hinweg behält, z. B. dep:acme/api:GHSA-…:npm:lodash@4.17.20
finding.row_idDie aktuelle ID des Befunds in der Befund-API; sie ändert sich, wenn ein Rescan die Zeile ersetzt
finding.console_urlWo der Befund in der Konsole angezeigt wird
finding.dismissal{by, reason, expires_at, approved_by, exception_id} bei einem Ereignis dismissed, sonst null. approved_by und exception_id sind gesetzt, wenn eine genehmigte Ausnahme den Befund verworfen hat
finding.exception{id, status, approved_by, revoked_by, revoked_reason, revoked_detail} der beendeten Ausnahme, bei exception_revoked und bei einem dismissal_expired, das eine Ausnahme beendet hat; sonst null
finding.revocation{reason, detail, revoked_verdict, revoked_at} bei dismissal_revoked: der Auslöser (reachability_changed), was sich geändert hat, das spätere Urteil, das nicht erreichbar widersprochen hat, und wann; sonst null
cvss{score, vector}, wenn die Advisory einen hat, sonst null
identifiersListen der Advisory-IDs cve, ghsa, mal und other; rule_id (Regeln für SAST, Secrets und Image-Fehlkonfigurationen, Regeln der Paketrichtlinie); licence; signature (ClamAV)
componentDie betroffene Komponente: purl, name, version, ecosystem, fixed_version, sha256. Null bei SAST und Secrets. Bei einer Image-Schwachstelle hat das Paket im Image einen Namen, aber keine purl; die purl des Images steht in location.image.purl.
locationkind (repository, image, package oder upload), repository, path, line, commit_sha; image {reference, purl, proxied, tags}, mit reference in der öffentlichen Form frem.sh/<org>/<image>@sha256:<digest>; package {type, owner, name, version, file, sha256}; upload_kindxdm.target.resource.name (Referenz, Repository oder Paket)
policyNur Ereignisse der Paketrichtlinie: {verdict, reasons}
known_exploitedBei jedem Ereignis: {cve_ids, sources}, wenn der Befund eine als in freier Wildbahn ausgenutzt gelistete Schwachstelle in einer Abhängigkeit oder einem Image ist (sources: euvd für ENISA EUVD, cisa_kev für CISA KEV), sonst null. Die CVE-IDs sind die des Befunds selbst oder die Aliasse seiner Advisory.

Beispiel (Format 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
}

Befund-Ereignisse sind nicht Teil der Audit-Hash-Kette. Die Änderung der Einstellung für Befund-Ereignisse eines Endpunkts und eine angeforderte Momentaufnahme sind Audit-Ereignisse (siem.endpoint.finding_events.update, siem.endpoint.finding_snapshot).

Befund-Ereignisse in CEF

Ein Befund-Ereignis verwendet die CEF-Zuordnung mit diesen Unterschieden:

CEFWert
Namealert_name (Signature ID bleibt die action)
SeveritySolange der Befund offen ist (opened, reopened, dismissal_expired, exception_revoked, dismissal_revoked, snapshot): 10 kritisch, 8 hoch, 5 mittel, 3 niedrig, 1 informativ. 1 bei dismissed und resolved, damit gute Nachrichten keinen Alarm auslösen wie zuvor der Befund. known_exploited ist mindestens 8, unabhängig vom Schweregrad des Befunds
catalert_category (der Befundtyp)
msgalert_name
outcomefinding.state
reasonfinding.transition_reason
startfinding.first_seen_at, Millisekunden seit der Epoche
cs5Label=findingId, cs5original_alert_id
cs6Label=purl, cs6component.purl, wenn es eine gibt
flexString1Label=identifiers, flexString1Die CVE-, GHSA-, MAL- und anderen IDs, Regel-ID, Lizenz und Signatur, durch Kommas getrennt
flexString2Label=location, flexString2Die Image-Referenz, das Repository oder das Paket
cfp1Label=cvssScore, cfp1cvss.score, wenn es einen gibt
filePathlocation.path
fileHashcomponent.sha256 (Antivirus-Befunde, Paketdateien)

cs3 und cs4 sind leer, wie bei Registry-Zugriffsereignissen. Über Syslog folgt der Schweregrad nach RFC 5424 dem CEF-Schweregrad wie bei Audit-Ereignissen: Ein kritischer Befund wird mit 2 (critical) gesendet, ein erledigter mit 6 (informational).

Syslog über TLS

Eine Endpunkt-URL der Form syslog+tls://host[:port] (Standardport 6514) stellt über Syslog statt über HTTPS zu:

  • Transport: TCP mit TLS 1.2 oder neuer, gerahmt durch Octet Counting (RFC 5425). fremforge prüft das Zertifikat des Empfängers gegen die öffentlichen CAs, für den Hostnamen in der URL, daher braucht der Empfänger ein öffentlich vertrauenswürdiges Zertifikat. Verwende Port 6514 oder 443; andere Ports sind von fremforge aus nicht erreichbar.
  • Nachricht: eine Nachricht nach RFC 5424 pro Ereignis, <PRI>1 TIMESTAMP frem.sh fremforge - MSGID - MSG. PRI ist Facility 13 (log audit) × 8 + Severity: 2 bei CEF-Schweregrad 9, 4 bei 7, 5 bei 5, sonst 6. MSGID ist die action (bis zu 32 Zeichen). MSG ist eine UTF-8-Byte-Order-Mark, gefolgt von der CEF-Zeile (Format cef) oder dem JSON-Ereignis (jedes andere Format).
  • Bündelung: bis zu max. Ereignisse Nachrichten innerhalb der max. Anfragegröße teilen sich eine Verbindung.
  • Keine Bestätigung. Syslog hat keine Antwort auf Anwendungsebene. Ein Bündel gilt als zugestellt, wenn jede Nachricht geschrieben wurde und dein Empfänger die Verbindung sauber geschlossen hat. Eine abgelehnte Verbindung, ein TLS-Fehler oder ein Empfänger, der schließt, bevor das Bündel geschrieben ist, lässt das Bündel scheitern, und es wird als Ganzes wiederholt. Ein Empfänger, der die Verbindung annimmt und dann verwirft, was er liest, sieht wie zugestellt aus. Dedupliziere über externalId (CEF) oder id (JSON).
  • Auth-Header und HMAC-Signatur gibt es nur bei HTTPS. Beschränke deinen Listener mit TLS auf fremforge und, falls nötig, mit einer Positivliste für Quelladressen.

Die Signatur verifizieren

Der HMAC wird über die exakten Bytes des Bodys in der Reihenfolge ihres Eintreffens berechnet. Parse und serialisiere den Body vor der Verifizierung nicht neu.

Empfänger in Node.js (Zeitstempel und 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');
  }
}

Empfänger in Python (Zeitstempel und 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')

Im Modus nur Body lässt du den Zeitstempel weg: HMAC-SHA256 über raw_body.

Fehlerbehandlung, Wiederholung und erneutes Senden

  • Jede Anfrage hat ein Timeout von 10 Sekunden.
  • Eine Antwort ohne 2xx, ein Timeout oder ein Verbindungsfehler lässt die Ereignisse in der Warteschlange. Der Endpunkt wartet zunehmend länger: 5 Sekunden, bei jedem weiteren Fehler in Folge verdoppelt, höchstens 10 Minuten, mit ±20 % Jitter. Antwortet dein Empfänger wieder, wird die Warteschlange in Reihenfolge abgearbeitet.
  • Nach 5 Fehlern in Folge zeigt der Endpunkt failing mit dem letzten Fehlergrund. Die Zustellung wird weiter versucht; nichts muss umgeschaltet werden. Das Deaktivieren eines Endpunkts verhindert, dass neue Ereignisse für ihn eingereiht werden; das erneute Aktivieren setzt die Zustellung sofort fort.
  • Ein 413 halbiert das Bündel. Ein einzelnes Ereignis, das dein Empfänger weiterhin als zu groß ablehnt, wird verworfen und gezählt.
  • Nicht zugestellte Ereignisse werden für die Audit-Aufbewahrungsdauer deiner Organisation aufbewahrt (Org-Admin → Auth-Richtlinie; standardmäßig 90 Tage), und höchstens 200 000 pro Endpunkt. Ältere werden verworfen und gezählt.
  • Die Karte Zustellung auf der Seite des Endpunkts (und der Block delivery der API, dropped_total) zeigt die Anzahl wartender Ereignisse, das älteste davon, den nächsten Versuch sowie die Summen der zugestellten und verworfenen Ereignisse (in manchen Sprachen als „discarded“ angezeigt). Ein Ereignis wird nur in diesen Fällen verworfen und dann nie an diesen Endpunkt zugestellt, es sei denn, du sendest es erneut: Dein Empfänger hat auf dieses Ereignis allein mit 413 geantwortet; es wartete noch, als es älter wurde als deine Audit-Aufbewahrung; der Endpunkt hatte bereits 200 000 wartende Ereignisse, dann gehen die ältesten zuerst; oder das Audit-Ereignis selbst existiert nicht mehr, was innerhalb der Aufbewahrungsdauer nicht vorkommt. Nichts anderes verwirft ein Ereignis: Ein fehlschlagender oder deaktivierter Empfänger lässt Ereignisse nur warten.
  • Erneut senden reiht deine Audit-Ereignisse ab einem Zeitpunkt noch einmal ein (und, sobald Befund-Ereignisse verfügbar sind, bei einem Endpunkt, der sie einbezieht, seine Befund-Ereignisse desselben Zeitfensters, nie älter als der Zeitpunkt, ab dem der Endpunkt sie empfängt): Erneut senden ab (UTC) auf der Seite des Endpunkts oder POST /_app/api/v1/orgs/<org>/siem/endpoints/<id>/replay mit {"since": "<ISO 8601>", "until": "<ISO 8601, optional>"}. since darf höchstens so weit zurückliegen wie deine Audit-Aufbewahrung. Eine Anfrage reiht höchstens 100 000 Ereignisse ein und liefert next_since, wenn noch weitere übrig sind. Noch wartende Ereignisse werden nicht doppelt eingereiht; erneut gesendete Ereignisse behalten ihre id.

Die Zustellung erfolgt mindestens einmal: Eine Wiederholung nach einer verlorenen Antwort oder ein erneutes Senden sendet ein Ereignis nochmals. Dedupliziere über id.

Verbreitete SIEMs

SIEMNimmt über HTTPS auf viaEinstellungen in fremforgeDirekt von fremforge?
Cortex XSIAMHTTP-Log-CollectorAuth-Header Authorization: <API key>, Format jsonJa
SplunkHTTP Event Collector, /services/collector/rawAuth-Header Authorization: Splunk <token>, Format ndjsonJa
Microsoft SentinelAzure Monitor Logs Ingestion APIBraucht ein kurzlebiges Entra-ID-TokenNein, über ein Relay
ElasticHTTP-Endpoint-Input von Elastic Agent / FilebeatSignatur umfasst nur Body, optional ein geheimer HeaderJa
IBM QRadarLogquelle HTTP Receiver oder eine Logquelle TLS SyslogFormat cefJa, wenn der Listener öffentlich erreichbar ist
Google SecOpsWebhook-FeedURL mit ?key=&secret=, Format ndjsonJa

Cortex XSIAM

Nimmt JSON von Drittanbietern über einen HTTP-Log-Collector auf (Settings → Data Sources & Integrations → + Add New → HTTP). Wähle Log Format: JSON; Vendor und Product, die du eingibst, benennen das Dataset <vendor>_<product>_raw. Die Collector-URL hat die Form https://api-<tenant external URL>/logs/v1/event und erwartet ihren API-Schlüssel in Authorization (Doku von Palo Alto).

Einrichtung: URL = die Collector-URL; Auth-Header Authorization = der API-Schlüssel des Collectors; Format json.

Beispiel für Parsing- und Datenmodellregeln für fremforge_audit_raw

Parsing-Regel (Settings → Configurations → Data Management → Parsing Rules), damit _time der Ereigniszeitpunkt von fremforge ist und nicht der Ankunftszeitpunkt. created_at hat immer drei Nachkommastellen und ein 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);

Datenmodellregel (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 setzen voraus, dass XSIAM die JSON-Schlüssel der obersten Ebene als Spalten speichert. Zeigt dein Dataset nur _raw_log, lies die Werte mit json_extract_scalar(_raw_log, "$.created_at") usw.

Datenmodellregel für Befund-Ereignisse

Befund-Ereignisse (demnächst) landen im selben Dataset. Ihre Alert-Felder sind nach denen von XDM benannt, sodass sie eins zu eins zugeordnet werden. Wenn du sie neben der Audit-Regel oben verwendest, ergänze die Audit-Regel um | filter not (action ~= "^security\.finding\."), damit jedes Ereignis nur einmal modelliert wird:

[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;

Der HTTP-Log-Collector nimmt diese Ereignisse als Log-Datensätze auf. Um daraus einen XSIAM-Alert auszulösen, lege eine Korrelationsregel auf dem Dataset an, zum Beispiel auf action = "security.finding.opened" und alert_severity critical oder high, und gruppiere nach original_alert_id, damit die späteren Ereignisse zum selben Befund (dismissed, resolved) ihm zugeordnet werden.

Splunk

Nimmt über den HTTP Event Collector auf. /services/collector/raw übernimmt den Body unverändert; setze sourcetype=_json in der URL, damit jede Zeile ein Ereignis ist. Das Token gehört in Authorization: Splunk <token>. Mit aktivierter Indexer-Bestätigung braucht der Raw-Endpunkt zusätzlich eine Kanal-GUID; gib sie in der URL als channel=<GUID> an (Doku von Splunk).

Einrichtung: URL https://<hec-host>:8088/services/collector/raw?sourcetype=_json; Auth-Header Authorization = Splunk <token>; Format ndjson.

Microsoft Sentinel

Nimmt eigenes JSON über die Logs Ingestion API von Azure Monitor auf, in eine eigene _CL-Tabelle, die durch eine Datensammlungsregel (DCR) definiert ist. Die Anfrage geht an {endpoint}/dataCollectionRules/{dcrImmutableId}/streams/{streamName}?api-version=2023-01-01, mit einem Entra-ID-Bearer-Token aus dem Client-Credentials-Flow (Scope https://monitor.azure.com/.default), und der Body muss ein JSON-Array sein (Doku von Microsoft). Die ältere HTTP Data Collector API hat am 14. September 2026 das Ende ihres Supports erreicht; baue nicht darauf auf.

Einrichtung: Das Token läuft nach etwa einer Stunde ab, daher kann ein statischer Auth-Header es nicht tragen. Schalte das Relay dazwischen: Es holt das Token, speichert es zwischen und leitet den Body weiter. Verwende das Format json_array, dann ist der Body bereits das Array, das die API erwartet. Ordne created_at in der DCR-Transformation TimeGenerated zu.

Elastic

Nimmt über den Input HTTP endpoint von Elastic Agent (die Integration Custom HTTP Endpoint Logs) oder Filebeat auf. Er lauscht auf einem eigenen Port (Standard 8000), kann TLS terminieren und einen HMAC über den Body prüfen (Doku von Elastic).

Einrichtung: Signatur umfasst nur Body; setze im Input hmac.header: X-Fremforge-Signature, hmac.key: <the endpoint's HMAC secret>, hmac.type: sha256 und hmac.prefix: "sha256=". Um zusätzlich die Option des Inputs für einen geheimen Header zu nutzen, setze denselben Header-Namen und Wert wie im Auth-Header des Endpunkts. Format json oder ndjson für Bündel. Indexiere actor, action und tenant_id als keyword.

IBM QRadar

Nimmt Bodys von HTTP-POSTs über eine Logquelle mit dem Protokoll HTTP Receiver auf. QRadar betreibt einen HTTP- oder HTTPS-Server auf einem Event Collector (Standardport 12469); HTTPS braucht ein von einer CA ausgestelltes Zertifikat, Mutual TLS ist optional (Doku von IBM). CEF-Zeilen werden vom Universal CEF DSM von QRadar geparst.

Einrichtung: URL = die öffentliche HTTPS-URL des Receivers oder syslog+tls://<collector>:6514 für eine Logquelle TLS Syslog; Format cef. fremforge kann kein Client-Zertifikat vorweisen, lass Mutual TLS also aus. Ist der Event Collector nicht aus dem Internet erreichbar, leitet ein Reverse Proxy oder Syslog-Relay an deinem Netzrand an ihn weiter.

Google SecOps (Chronicle)

Nimmt über einen Webhook-Feed auf (Settings → Feeds → Add new, Quelltyp Webhook, dann einen Log-Typ wählen). Anfragen gehen an den Endpunkt ...:importPushLogs des Feeds, mit einem API-Schlüssel und dem Secret des Feeds, als Header X-goog-api-key und X-Webhook-Access-Key oder als ?key=<API key>&secret=<secret>. Mehrere durch Zeilenumbrüche getrennte JSON-Objekte können sich eine Anfrage teilen, bis zu 4 MB (Doku von Google). Es gibt keinen vorgefertigten Parser für fremforge; du brauchst einen Log-Typ und einen Parser für diese Ereignisse.

Einrichtung: URL = der Feed-Endpunkt mit ?key=<API key>&secret=<secret> (verschlüsselt gespeichert und mit *** angezeigt); Format ndjson; max. Anfragegröße höchstens 4096 KiB.

Ein Relay, das die Signatur prüft

Sentinel und jeder Empfänger, der mehr als einen Header oder ein kurzlebiges Token braucht, verwenden einen kleinen Dienst: Er prüft die Signatur von fremforge und leitet den Body dann unverändert mit den Zugangsdaten des SIEM weiter. Diese Version nutzt nur die Standardbibliothek von Python; setze TARGET_URL und TARGET_AUTH für dein SIEM. Für Sentinel ergänzt du das Abrufen des Tokens; mit dem Format json_array ist der Body bereits das Array, das es erwartet. MAX_BODY entspricht der Standard-Anfragegröße von 1 MiB. Das Relay leitet ein korrekt signiertes Ereignis mit den Zugangsdaten weiter, antwortet auf eine falsche Signatur oder einen mehr als fünf Minuten alten Zeitstempel mit 401 und mit 502, wenn das Ziel nicht erreichbar ist.

"""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()

Betreibe es hinter deinem üblichen TLS-Terminator: fremforge stellt nur an öffentliche https://-URLs zu. Halte die Antwort des Relays unter 10 Sekunden und gib einen Status außerhalb von 2xx zurück, wenn das SIEM das Ereignis abgelehnt hat. Ein Relay, das immer mit 200 antwortet, verliert Ereignisse: fremforge wertet ein 2xx als zugestellt und entfernt das Ereignis aus der Warteschlange.

Generischer Webhook

Jeder HTTPS-Endpunkt, der 2xx zurückgibt, sobald er die Anfrage angenommen hat. Verifiziere mit den Snippets oben. Das passt für selbst betriebene SIEMs (Wazuh, Graylog, …) und HTTP-Annahmestellen wie Logflare oder Datadog.

Zustand auf Betreiberseite

Die Betreiber von fremverk sehen den Zustand jedes Endpunkts auf der SIEM-Zustandsseite des Betreiberportals. Ein Alarm wird ausgelöst, wenn das älteste nicht zugestellte Ereignis eines Endpunkts älter als 24 Stunden ist, und wir kontaktieren die Owner deiner Organisation. Lege in deinem SIEM zusätzlich einen Alarm für ausbleibende fremforge-Ereignisse an.

Was nicht übertragen wird

  • Syslog über UDP oder unverschlüsseltes TCP. Syslog gibt es nur mit TLS.
  • Client-Zertifikate (Mutual TLS) gegenüber deinem Empfänger.
  • Mehr als ein eigener Header pro Endpunkt.
  • Mandantenübergreifende Verteilung. Jeder Endpunkt ist an einen Tenant gebunden. Es gibt keinen Spiegel auf Betreiberseite; Ereignisse aus deiner Organisation verlassen nie die Kette pro Tenant.

Verwandte Themen

  • Integrität der Audit-Kette: was tatsächlich in den Ereignissen steht und wie du die Kette durchgängig verifizierst.
  • Webhooks: dieselbe Semantik der Übertragungsform für Repository- und Organisationsereignisse, auf dieselbe Weise signiert und zugestellt.
  • Modell der gehosteten Runner: woher von Runnern ausgelöste Ereignisse in der Kette stammen.