Transfert vers un SIEM
fremforge écrit chaque action d’administration qui modifie un état dans un journal d’audit propre à chaque tenant, avec une garantie d’intégrité par chaînage de hachages (voir Intégrité de la chaîne d’audit). Les mêmes événements peuvent être diffusés vers votre SIEM dès leur écriture, si bien que vos règles de détection les voient en quelques secondes plutôt qu’au prochain export CSV.
Bientôt disponible : le même flux transportera aussi un événement par changement de cycle de vie d’un résultat de sécurité : un résultat de vulnérabilité, de licence, SAST, de secret, de malware ou de politique de paquets qui est ouvert, rouvert, ignoré ou résolu. Voir Événements de résultats.
Cette page décrit la forme du transfert, le contrat sur le fil pour la vérification, et la manière dont les SIEM courants l’ingèrent.
Comment fonctionne la livraison
audit-emit écrit la ligne chaînée et, dans la même transaction de base de données, met l’événement en file pour chaque point de terminaison activé dont les préfixes d’action correspondent. Un worker de livraison envoie la file de chaque point de terminaison dans l’ordre, un ou plusieurs événements par requête selon le format du point de terminaison. Un événement n’est retiré de la file que lorsque votre récepteur répond 2xx.
Les paramètres se trouvent sous Administration de l’organisation → Intégration → Transfert SIEM (/<org>/_admin/siem), et les mêmes paramètres sont disponibles dans l’API REST (/_app/api/v1/orgs/<org>/siem/endpoints).
La page Transfert SIEM comporte le formulaire Ajouter un point de terminaison et la liste des points de terminaison : nom, URL (identifiants masqués), statut, dernière livraison et nombre d’événements en attente. Le nom de chaque point de terminaison ouvre sa propre page (/<org>/_admin/siem/<endpoint id>), avec une carte par paramètre :
- Vue d’ensemble : statut, raison du dernier échec, filtre de préfixes d’action, Désactiver/Activer et Envoyer un test.
- En-tête d’authentification : le définir, le renouveler ou le supprimer.
- Signature : ce que couvre le HMAC.
- Livraison : événements en attente, le plus ancien, la prochaine tentative, totaux livrés et abandonnés, et les paramètres de format et de regroupement en lots.
- Rejouer : renvoyer des événements passés.
- Supprimer le point de terminaison : saisissez le nom du point de terminaison pour confirmer.
Configurer un point de terminaison
- Allez sous Administration de l’organisation → Transfert SIEM → Ajouter un point de terminaison.
- Saisissez un Nom (texte libre, unique dans l’organisation) et l’URL de votre récepteur :
https://…, ousyslog+tls://host:portpour le syslog sur TLS. Les adresseshttp://,localhost, RFC1918 et de métadonnées sont refusées ; utilisez un récepteur joignable publiquement. - Choisissez le Format (voir Formats et regroupement en lots). Les étapes suivantes se trouvent sous Options avancées et peuvent aussi être modifiées plus tard sur la page du point de terminaison.
- Restreignez éventuellement la liste d’autorisation des préfixes d’action (par exemple
forgejo.push, billing.). Vide signifie tout transférer. Chaque préfixe est comparé avecaction.startsWith(prefix). - Activez éventuellement Inclure le journal d’accès au registre pour recevoir aussi un événement par requête au registre de paquets ou de conteneurs (
registry.access.*). C’est désactivé par défaut ; voir Journal d’accès au registre. Les décisions de pull des contrôles d’images,registry.pull.blockedetregistry.pull.would_block, sont des événements d’audit, pas des événements d’accès : elles sont donc transférées sans cette option, à raison d’une par digest d’image, contrôle et décision par heure ; voir Contrôles sur les pulls de conteneurs. Bientôt disponible : Inclure les événements de résultats, coché par défaut, fera recevoir aussi au point de terminaison un événement par changement de cycle de vie d’un résultat de sécurité (security.finding.*), à partir du moment de sa création. Tant que cette fonctionnalité n’est pas livrée, le formulaire ne l’affiche pas. Voir Événements de résultats. - Définissez éventuellement un En-tête d’authentification : un nom d’en-tête et une valeur secrète, envoyés à chaque requête (par exemple
AuthorizationetSplunk <token>). Voir Authentification. - Choisissez ce que couvre la signature (l’horodatage et le corps par défaut, ou le corps seul).
- Réglez la taille des lots, la taille des requêtes et l’attente.
- Cliquez sur Créer le point de terminaison. Le secret HMAC partagé est affiché une seule fois sur l’écran suivant ; copiez-le dans la configuration de votre récepteur. fremforge ne l’affiche plus jamais. Si vous le perdez, supprimez le point de terminaison et ajoutez-le à nouveau.
Le point de terminaison est actif immédiatement ; la confirmation renvoie vers sa page. Envoyer un test envoie un événement synthétique fremforge.siem.test dans le format du point de terminaison, avec ses en-têtes et sa signature, et affiche le statut de la réponse et les 200 premiers octets de la réponse du récepteur.
Identifiants dans l’URL. Certains collecteurs reçoivent leur identifiant dans l’URL (?token=, ?key=&secret=, ou user:password@). fremforge stocke l’URL telle que saisie, chiffrée, et l’affiche partout ailleurs (page d’administration, API, résultat du test, journal d’audit) avec chaque valeur de requête et les informations d’utilisateur remplacées par ***. Seule la livraison utilise l’URL complète.
Authentification
Deux mécanismes, utilisables ensemble :
- En-tête d’authentification. Un en-tête par point de terminaison, avec le nom et la valeur de votre choix. La valeur est chiffrée au repos et en écriture seule : aucune page, réponse d’API, ligne de journal ni événement d’audit ne la contient. Le journal d’audit enregistre le nom de l’en-tête et le moment où il a été défini. Pour le renouveler, enregistrez une nouvelle valeur ; l’ancienne cesse d’être envoyée immédiatement, ajoutez donc d’abord la nouvelle clé à votre collecteur. Les noms d’en-têtes qui casseraient la requête sont refusés :
Host,Content-Length,Content-Type,Content-Encoding,Transfer-Encoding,Connection,Keep-Alive,Upgrade,TE,Trailer,Expect,Accept-Encoding,User-Agent,Cookie,Proxy-*etX-Fremforge-*. Les valeurs sont en ASCII imprimable, jusqu’à 4096 caractères. - Signature HMAC. Chaque requête porte
X-Fremforge-Signature: sha256=<hex>, calculée avec le secret HMAC du point de terminaison. Par défaut, elle couvre<timestamp>.<body>, afin que votre récepteur puisse rejeter une requête ancienne d’après son horodatage. Avec La signature couvre : Corps seul (entrée HTTP Elastic), elle ne couvre que le corps brut, ce que vérifient les récepteurs dotés d’une vérification intégrée du corps (l’entrée HTTP endpoint d’Elastic). Un récepteur en mode corps seul ne peut pas rejeter une requête rejouée d’après son âge ; dédupliquez plutôt sur l’idde l’événement.
Contrat sur le fil
Chaque livraison HTTPS est un POST :
| En-tête | Valeur |
|---|---|
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 | Secondes Unix (chaîne) au moment où fremforge a construit la requête |
X-Fremforge-Signature | sha256=<hex> sur <timestamp>.<body>, ou sur <body> en mode corps seul |
Idempotency-Key | L’id de l’événement pour un événement unique ; pour un lot, un SHA-256 des identifiants des événements du lot dans l’ordre |
X-Fremforge-Event-Count | Nombre d’événements dans le corps |
| votre en-tête d’authentification | Lorsqu’il est défini |
Chaque événement a cette forme (clés de premier niveau dans cet ordre ; clés fields.* triées par ordre alphabétique afin que les octets soient stables) :
{
"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 est l’identifiant de l’événement d’audit. Il est identique à chaque nouvelle tentative et à chaque rejeu de cet événement : utilisez-le pour dédupliquer.
actor est un nom d’utilisateur Forgejo seul pour une action d’une personne (dans la console web, et pour de nombreux appels d’API, l’utilisateur qui a créé le jeton), oidc:idp:<sub> pour une personne qui s’est connectée via votre IdP et agit via un jeton échangé, token:<id> pour les autres appels d’API, operator:<email> pour une action du personnel de fremverk, et d’autres formes <kind>:<id> pour les composants système.
hash_chain.this_hash est le même hachage que celui ancré dans OBS Object Lock, afin que votre SIEM puisse prouver qu’un événement est antérieur à un ancrage connu.
Un événement d’accès au registre (uniquement sur un point de terminaison avec include_access_log) a la même forme, avec action valant registry.access.<action> et "hash_chain": null : les événements d’accès ne font pas partie de la chaîne de hachage de l’audit. Ses fields sont l’événement tel que le renvoie l’API du journal d’accès.
Un événement de résultat (bientôt disponible ; uniquement sur un point de terminaison avec include_finding_events) aura aussi cette forme, avec action valant security.finding.<transition> et "hash_chain": null.
Formats et regroupement en lots
| Format | Corps | Événements par requête |
|---|---|---|
json (par défaut) | Un objet événement | 1 |
ndjson | Un objet événement par ligne, chaque ligne se terminant par \n | Jusqu’à max events |
json_array | Un tableau JSON d’objets événements | Jusqu’à max events |
cef | Une ligne CEF par événement, chacune se terminant par \n | Jusqu’à max events |
Pour les formats en lots :
- Événements max. par requête : de 1 à 1000 (100 lorsque vous passez à un format en lots sans choisir).
- Taille max. de requête : de 1 Kio à 4 Mio (1 Mio par défaut). Un lot est clos avant de la dépasser ; un événement isolé est toujours envoyé.
- Attente max. pour un lot : de 0 à 60 000 ms (0 par défaut). Combien de temps un lot partiellement rempli attend d’autres événements avant d’être envoyé.
Les événements sont livrés dans l’ordre où ils ont été écrits.
Correspondance des champs CEF
CEF:0|fremverk|fremforge|1|<action>|<action>|<severity>|<extension>
| CEF | Valeur |
|---|---|
| Signature ID, Name | action |
| Severity | 9 lorsque hash_chain.this_hash vaut chain-broken ; 7 lorsque action contient fail, denied, deny, blocked, violation, malware, secret, breach ou tamper ; 5 lorsqu’elle contient delete, revoke, disable, remove, bypass, suspend ou dismiss ; sinon 3 |
rt | created_at, en millisecondes depuis l’epoch |
externalId | id (la clé de déduplication) |
suser | actor |
act | action |
cat | action jusqu’au premier . |
cs1Label=tenantId, cs1 | tenant_id |
cs2Label=fields, cs2 | fields en JSON |
cs3Label=hashChainThis, cs3 | hash_chain.this_hash |
cs4Label=hashChainPrev, cs4 | hash_chain.prev_hash, vide lorsqu’il n’y en a pas |
dvchost | frem.sh |
Les champs d’en-tête échappent \ et | ; les valeurs d’extension échappent \, = et les sauts de ligne (\n, \r). Une valeur issue d’un événement d’audit ne peut donc pas commencer un nouveau champ ni une nouvelle ligne.
Événements de résultats
Bientôt disponible. Les événements de résultats ne sont pas encore disponibles. Cette section les décrit avant leur sortie ; d’ici là, la console et l’API ne proposent ni
include_finding_eventsni l’instantané, et aucun événementsecurity.finding.*n’est envoyé. Le changelog annoncera la sortie.
Un point de terminaison avec Inclure les événements de résultats activé (include_finding_events dans l’API) reçoit un événement chaque fois qu’un résultat de sécurité change d’état. Chaque type de résultat que signale fremforge est couvert :
alert_category | Résultat | Source |
|---|---|---|
dependency_vulnerability | Une dépendance vulnérable sur la branche par défaut | Analyse des dépendances |
image_vulnerability | Une vulnérabilité ou une mauvaise configuration dans une image de conteneur de votre registre (images passant par le proxy comprises) ou dans une analyse de dépôt | Analyse des images |
licence | Une dépendance sous licence forbidden ou restricted | Analyse des licences |
sast | Un résultat d’analyse statique sur la branche par défaut | SAST |
secret | Un secret committé (jamais le secret lui-même) | Analyse des secrets |
malware | Un paquet malveillant (OSV MAL-*), ou un envoi refusé par l’analyse antivirus (ClamAV) | Analyses des dépendances et du registre, analyse des envois |
package_policy | Un fichier du registre de paquets que la politique de paquets bloque ou signale | Politique de paquets |
Les résultats des analyses de pull request ne sont pas inclus : ils vont et viennent avec chaque pull request. C’est sur la branche par défaut que le cycle de vie d’un résultat est suivi. Les événements de politique de paquets ne sont envoyés que pour les organisations où les résultats de politique de paquets sont visibles.
Catalogue des événements
action | Quand |
|---|---|
security.finding.opened | Le résultat est détecté pour la première fois. |
security.finding.reopened | Le résultat est de nouveau détecté après avoir été résolu (transition_reason reappeared), ou sa dérogation a été retirée avant son expiration (undismissed). |
security.finding.dismissed | Quelqu’un a ignoré le résultat (risque accepté, faux positif), une liste d’autorisation le couvre, ou un résultat antivirus a fait l’objet d’une dérogation. actor est l’utilisateur qui l’a ignoré, ou system:<kind>. |
security.finding.dismissal_expired | Une dérogation limitée dans le temps a expiré et le résultat est de nouveau ouvert. Cela inclut une exception arrivée à expiration. |
security.finding.exception_revoked | Une exception approuvée qui ignorait le résultat a été révoquée, et le résultat est de nouveau ouvert. transition_reason est le déclencheur : manual (quelqu’un l’a révoquée), severity_raised, new_vulnerability ou approver_left. actor est la personne qui l’a révoquée, ou system:exception-revocation. |
security.finding.dismissal_revoked | fremforge a révoqué une dérogation qui reposait sur un verdict d’atteignabilité non atteignable, parce qu’une analyse ultérieure le contredit, et le résultat est de nouveau ouvert. transition_reason vaut reachability_changed, finding.revocation indique pourquoi, et actor vaut system:exception-revocation. (Avant le 2026-10-12, cet événement était envoyé sous dismissal_expired.) Une exception révoquée donne exception_revoked avec la même raison. |
security.finding.resolved | Le résultat n’est plus détecté : corrigé, retiré, remplacé par une analyse plus récente qui ne le signale plus, ou fichier de paquet supprimé. |
security.finding.known_exploited | Une vulnérabilité de dépendance ou d’image ouverte ou ignorée a été listée comme exploitée dans la nature par l’EU Vulnerability Database de l’ENISA ou le catalogue KEV de la CISA. Envoyé une fois par inscription. transition_reason vaut listed (la liste l’a ajoutée depuis), ou new (le résultat était déjà listé lors de sa première détection ; il est envoyé avec son opened, dans n’importe quel ordre). known_exploited nomme les CVE et les listes, et actor vaut system:known-exploited-feed. Un résultat ignoré est aussi signalé : le risque sous lequel il a été accepté a changé. |
security.finding.snapshot | Un par résultat ouvert, envoyé une fois lorsque vous demandez un instantané. |
Les changements sont pris en compte en une minute environ. Un résultat garde une seule identité d’une analyse à l’autre : une réanalyse qui signale de nouveau le même résultat n’envoie rien ; fields.original_alert_id est le même sur chaque événement concernant un résultat.
Ce qui est envoyé, et à partir de quand
- Uniquement les changements à partir de l’activation. Un point de terminaison reçoit des événements pour les changements qui surviennent après qu’il a commencé à les recevoir (
finding_events_sincedans l’API). Activer les événements de résultats n’envoie pas vos résultats existants. - Instantané des résultats ouverts. Pour charger ce qui est ouvert actuellement, utilisez Envoyer l’instantané des résultats ouverts sur la page du point de terminaison, ou
POST /_app/api/v1/orgs/<org>/siem/endpoints/<id>/finding-snapshot. Cela met en file un événementsecurity.finding.snapshotpar résultat ouvert, pour ce point de terminaison uniquement. Il ne peut être pris qu’une fois par point de terminaison ; une seconde demande reçoit409. - Par défaut. Un point de terminaison créé après la sortie inclut les événements de résultats, sauf si vous décochez la case (ou envoyez
"include_finding_events": false). Les points de terminaison existant avant la sortie restent tels quels, sans événements de résultats, afin qu’un récepteur conçu pour les événements d’audit ne commence pas à recevoir sans prévenir une nouvelle famille d’événements ; activez-les point de terminaison par point de terminaison sur sa page ou avecPATCH … {"include_finding_events": true}. - Les préfixes d’action s’appliquent comme pour les événements d’audit. Un point de terminaison avec des préfixes ne reçoit les événements de résultats que si l’un d’eux correspond, par exemple
security.finding.. - La livraison, le regroupement en lots, les nouvelles tentatives, le rejeu, le HMAC, l’en-tête d’authentification et la conservation des événements non livrés sont les mêmes que pour les événements d’audit. L’
idde l’événement est le même à chaque nouvelle tentative et à chaque rejeu : dédupliquez sur lui.
Champs
fields d’un événement de résultat :
| Champ | Valeur | Cortex XDM |
|---|---|---|
alert_name | Titre court, par exemple CVE-2024-3094 in xz-utils (frem.sh/acme/api@sha256:…) | xdm.alert.name |
alert_category | Le type de résultat (tableau ci-dessus) | xdm.alert.category |
alert_severity | critical, high, medium, low ou informational | xdm.alert.severity |
alert_description | Résumé de l’avis ou message de la règle, au plus 500 caractères, ou null | xdm.alert.description |
original_alert_id | Identifiant stable du résultat (32 caractères hexadécimaux), le même sur chaque événement concernant un résultat | xdm.alert.original_alert_id |
org | Slug de l’organisation | |
finding.type, finding.source | Type de résultat, et l’analyseur qui l’a signalé | |
finding.state, finding.previous_state | open, dismissed ou resolved ; l’état précédent (null lors de la première détection) | |
finding.transition, finding.transition_reason | La transition (comme dans action) et pourquoi : 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 | Quand le résultat a été vu pour la première fois ; quand il est entré dans son état actuel | |
finding.key | L’identité que le résultat garde d’une analyse à l’autre, par exemple dep:acme/api:GHSA-…:npm:lodash@4.17.20 | |
finding.row_id | L’identifiant actuel du résultat dans l’API des résultats ; il change lorsqu’une réanalyse remplace la ligne | |
finding.console_url | Où le résultat est affiché dans la console | |
finding.dismissal | {by, reason, expires_at, approved_by, exception_id} sur un événement dismissed, sinon null. approved_by et exception_id sont renseignés lorsqu’une exception approuvée a ignoré le résultat | |
finding.exception | {id, status, approved_by, revoked_by, revoked_reason, revoked_detail} de l’exception qui a pris fin, sur exception_revoked et sur un dismissal_expired qui a mis fin à une exception ; sinon null | |
finding.revocation | {reason, detail, revoked_verdict, revoked_at} sur dismissal_revoked : le déclencheur (reachability_changed), ce qui a changé, le verdict ultérieur qui a contredit non atteignable, et quand ; sinon null | |
cvss | {score, vector} lorsque l’avis en a un, sinon null | |
identifiers | Listes d’identifiants d’avis cve, ghsa, mal et other ; rule_id (règles SAST, de secrets et de mauvaise configuration d’image, règles de politique de paquets) ; licence ; signature (ClamAV) | |
component | Le composant concerné : purl, name, version, ecosystem, fixed_version, sha256. Null pour le SAST et les secrets. Pour une vulnérabilité d’image, le paquet dans l’image a un nom mais pas de purl ; le purl de l’image se trouve dans location.image.purl. | |
location | kind (repository, image, package ou upload), repository, path, line, commit_sha ; image {reference, purl, proxied, tags}, avec reference sous la forme publique frem.sh/<org>/<image>@sha256:<digest> ; package {type, owner, name, version, file, sha256} ; upload_kind | xdm.target.resource.name (reference, repository ou package) |
policy | Événements de politique de paquets uniquement : {verdict, reasons} | |
known_exploited | Sur chaque événement : {cve_ids, sources} lorsque le résultat est une vulnérabilité de dépendance ou d’image listée comme exploitée dans la nature (sources : euvd pour ENISA EUVD, cisa_kev pour CISA KEV), sinon null. Les identifiants CVE sont ceux du résultat ou les alias de son avis. |
Exemple (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
}Les événements de résultats ne font pas partie de la chaîne de hachage de l’audit. La modification du paramètre d’événements de résultats d’un point de terminaison, et une demande d’instantané, sont des événements d’audit (siem.endpoint.finding_events.update, siem.endpoint.finding_snapshot).
Événements de résultats en CEF
Un événement de résultat utilise la correspondance CEF avec ces différences :
| CEF | Valeur |
|---|---|
| Name | alert_name (Signature ID reste l’action) |
| Severity | Tant que le résultat est ouvert (opened, reopened, dismissal_expired, exception_revoked, dismissal_revoked, snapshot) : 10 critique, 8 élevée, 5 moyenne, 3 faible, 1 informative. 1 pour dismissed et resolved, afin qu’une bonne nouvelle ne déclenche pas d’alerte comme l’a fait le résultat. known_exploited vaut au moins 8, quelle que soit la gravité du résultat |
cat | alert_category (le type de résultat) |
msg | alert_name |
outcome | finding.state |
reason | finding.transition_reason |
start | finding.first_seen_at, en millisecondes depuis l’epoch |
cs5Label=findingId, cs5 | original_alert_id |
cs6Label=purl, cs6 | component.purl, lorsqu’il y en a un |
flexString1Label=identifiers, flexString1 | Les identifiants CVE, GHSA, MAL et autres, l’identifiant de règle, la licence et la signature, séparés par des virgules |
flexString2Label=location, flexString2 | La référence de l’image, le dépôt ou le paquet |
cfp1Label=cvssScore, cfp1 | cvss.score, lorsqu’il y en a un |
filePath | location.path |
fileHash | component.sha256 (résultats antivirus, fichiers de paquets) |
cs3 et cs4 sont vides, comme pour les événements d’accès au registre. En syslog, la sévérité RFC 5424 suit la sévérité CEF comme pour les événements d’audit : un résultat critique est envoyé en 2 (critical), un résultat résolu en 6 (informational).
Syslog sur TLS
Une URL de point de terminaison de la forme syslog+tls://host[:port] (port 6514 par défaut) livre en syslog au lieu de HTTPS :
- Transport : TCP avec TLS 1.2 ou ultérieur, avec un découpage par comptage d’octets (RFC 5425). fremforge vérifie le certificat du récepteur par rapport à l’ensemble des AC publiques, pour le nom d’hôte de l’URL : le récepteur a donc besoin d’un certificat reconnu publiquement. Utilisez le port 6514 ou 443 ; les autres ports ne sont pas joignables depuis fremforge.
- Message : un message RFC 5424 par événement,
<PRI>1 TIMESTAMP frem.sh fremforge - MSGID - MSG. PRI vaut facility 13 (log audit) × 8 + severity : 2 pour la sévérité CEF 9, 4 pour 7, 5 pour 5, sinon 6. MSGID est l’action(jusqu’à 32 caractères). MSG est une marque d’ordre d’octets UTF-8 suivie de la ligne CEF (formatcef) ou de l’événement JSON (tout autre format). - Regroupement en lots : jusqu’à max events messages dans la limite de max request size partagent une même connexion.
- Pas d’accusé de réception. Syslog n’a pas de réponse au niveau applicatif. Un lot compte comme livré lorsque chaque message a été écrit et que votre récepteur a fermé la connexion proprement. Une connexion refusée, un échec TLS, ou un récepteur qui ferme avant que le lot soit écrit font échouer le lot, qui est retenté en entier. Un récepteur qui accepte la connexion puis jette ce qu’il lit semble avoir reçu les événements. Dédupliquez sur
externalId(CEF) ouid(JSON). - L’en-tête d’authentification et la signature HMAC n’existent qu’en HTTPS. Restreignez votre écouteur à fremforge avec TLS et, si nécessaire, une liste d’autorisation d’adresses sources.
Vérifier la signature
Le HMAC est calculé sur les octets exacts du corps, dans l’ordre où ils arrivent. N’analysez pas et ne resérialisez pas le corps avant la vérification.
Récepteur Node.js (horodatage et corps) :
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');
}
}Récepteur Python (horodatage et corps) :
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')En mode corps seul, retirez l’horodatage : HMAC-SHA256 sur raw_body.
Gestion des échecs, nouvelles tentatives et rejeu
- Chaque requête a un délai d’expiration de 10 secondes.
- Une réponse autre que
2xx, un délai dépassé ou une erreur de connexion laisse les événements en file. Le point de terminaison espace ses tentatives : 5 secondes, doublées à chaque échec consécutif, 10 minutes au plus, avec une variation aléatoire de ±20 %. Lorsque votre récepteur répond de nouveau, la file se vide dans l’ordre. - Après 5 échecs consécutifs, le point de terminaison affiche
failingavec la raison du dernier échec. La livraison continue d’être retentée ; il n’y a rien à basculer. Désactiver un point de terminaison empêche que de nouveaux événements soient mis en file pour lui ; le réactiver reprend immédiatement la livraison. - Une réponse
413divise le lot par deux. Un événement isolé que votre récepteur refuse encore comme trop volumineux est abandonné et compté. - Les événements non livrés sont conservés pendant la durée de conservation de l’audit de votre organisation (Administration de l’organisation → Politique d’authentification ; 90 jours par défaut), et au plus 200 000 par point de terminaison. Les plus anciens sont abandonnés et comptés.
- La carte Livraison sur la page du point de terminaison (et le bloc
deliveryde l’API,dropped_total) affiche le nombre d’événements en attente, le plus ancien, la prochaine tentative, et les totaux livrés et abandonnés (affichés comme « écartés » dans certaines langues). Un événement est abandonné, et n’est jamais livré à ce point de terminaison sauf si vous le rejouez, uniquement dans ces cas : votre récepteur a répondu413à cet événement seul ; il était encore en attente lorsqu’il est devenu plus ancien que votre durée de conservation de l’audit ; le point de terminaison avait déjà 200 000 événements en attente, auquel cas les plus anciens partent en premier ; ou l’événement d’audit lui-même n’existe plus, ce qui ne se produit pas pendant la période de conservation. Rien d’autre n’abandonne un événement : un récepteur en échec ou désactivé ne fait que mettre les événements en attente. - Rejouer remet en file vos événements d’audit à partir d’un instant donné (et, une fois les événements de résultats disponibles, sur un point de terminaison qui les inclut, ses événements de résultats de la même fenêtre, jamais plus anciens que le moment où le point de terminaison a commencé à les recevoir) : Replay from (UTC) sur la page du point de terminaison, ou
POST /_app/api/v1/orgs/<org>/siem/endpoints/<id>/replayavec{"since": "<ISO 8601>", "until": "<ISO 8601, optional>"}.sincepeut remonter au plus à votre durée de conservation de l’audit. Une requête met en file au plus 100 000 événements et renvoienext_sincelorsqu’il en reste. Les événements encore en attente ne sont pas mis en file deux fois ; les événements rejoués gardent leurid.
La livraison est au moins une fois : une nouvelle tentative après une réponse perdue, ou un rejeu, renvoie un événement. Dédupliquez sur id.
SIEM courants
| SIEM | Ingère en HTTPS via | Paramètres fremforge | Directement depuis fremforge ? |
|---|---|---|---|
| Cortex XSIAM | HTTP log collector | En-tête d’authentification Authorization: <API key>, format json | Oui |
| Splunk | HTTP Event Collector, /services/collector/raw | En-tête d’authentification Authorization: Splunk <token>, format ndjson | Oui |
| Microsoft Sentinel | Azure Monitor Logs Ingestion API | Nécessite un jeton Entra ID de courte durée | Non, via un relais |
| Elastic | Entrée HTTP endpoint d’Elastic Agent / Filebeat | Signature sur le corps seul, éventuellement un en-tête secret | Oui |
| IBM QRadar | Source de logs HTTP Receiver, ou source de logs TLS Syslog | Format cef | Oui, si l’écouteur est joignable publiquement |
| Google SecOps | Flux webhook | URL avec ?key=&secret=, format ndjson | Oui |
Cortex XSIAM
Ingère du JSON tiers via un HTTP log collector (Settings → Data Sources & Integrations → + Add New → HTTP). Choisissez Log Format: JSON ; le Vendor et le Product que vous saisissez nomment le dataset <vendor>_<product>_raw. L’URL du collecteur a la forme https://api-<tenant external URL>/logs/v1/event et reçoit sa clé d’API dans Authorization (documentation Palo Alto).
Configuration : URL = l’URL du collecteur ; en-tête d’authentification Authorization = la clé d’API du collecteur ; format json.
Exemples de règles d’analyse et de modèle de données pour fremforge_audit_raw
Règle d’analyse (Settings → Configurations → Data Management → Parsing Rules), afin que _time soit l’heure de l’événement fremforge plutôt que l’heure d’arrivée. created_at a toujours trois chiffres décimaux et un 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);Règle de modèle de données (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;Les deux supposent qu’XSIAM stocke les clés JSON de premier niveau comme colonnes. Si votre dataset n’affiche que _raw_log, lisez les valeurs avec json_extract_scalar(_raw_log, "$.created_at") et ainsi de suite.
Règle de modèle de données pour les événements de résultats
Les événements de résultats (bientôt disponibles) arrivent dans le même dataset. Leurs champs d’alerte portent les noms de ceux d’XDM, si bien qu’ils correspondent un à un. Lorsque vous l’utilisez à côté de la règle d’audit ci-dessus, ajoutez | filter not (action ~= "^security\.finding\.") à la règle d’audit afin que chaque événement ne soit modélisé qu’une fois :
[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;Le HTTP log collector ingère ces événements comme des enregistrements de logs. Pour déclencher une alerte XSIAM à partir d’eux, ajoutez une règle de corrélation sur le dataset, par exemple sur action = "security.finding.opened" et alert_severity critical ou high, et groupez par original_alert_id afin que les événements ultérieurs concernant le même résultat (dismissed, resolved) s’y rattachent.
Splunk
Ingère via le HTTP Event Collector. /services/collector/raw prend le corps tel quel ; définissez sourcetype=_json dans l’URL pour que chaque ligne soit un événement. Le jeton va dans Authorization: Splunk <token>. Avec l’accusé de réception de l’indexeur activé, le point de terminaison raw nécessite aussi un GUID de canal ; ajoutez-le à l’URL sous la forme channel=<GUID> (documentation Splunk).
Configuration : URL https://<hec-host>:8088/services/collector/raw?sourcetype=_json ; en-tête d’authentification Authorization = Splunk <token> ; format ndjson.
Microsoft Sentinel
Ingère du JSON personnalisé via la Logs Ingestion API d’Azure Monitor, dans une table personnalisée _CL définie par une règle de collecte de données (DCR). La requête est envoyée à {endpoint}/dataCollectionRules/{dcrImmutableId}/streams/{streamName}?api-version=2023-01-01 avec un jeton bearer Entra ID issu du flux client-credentials (portée https://monitor.azure.com/.default), et le corps doit être un tableau JSON (documentation Microsoft). L’ancienne HTTP Data Collector API a atteint sa fin de support le 14 septembre 2026 ; ne construisez rien dessus.
Configuration : le jeton expire au bout d’environ une heure, si bien qu’un en-tête d’authentification statique ne peut pas le transporter. Placez le relais entre les deux : il récupère et met en cache le jeton et transfère le corps. Utilisez le format json_array, afin que le corps soit déjà le tableau qu’attend l’API. Associez created_at à TimeGenerated dans la transformation de la DCR.
Elastic
Ingère via l’entrée HTTP endpoint d’Elastic Agent (l’intégration Custom HTTP Endpoint Logs) ou de Filebeat. Elle écoute sur son propre port (8000 par défaut), peut terminer le TLS et peut vérifier un HMAC sur le corps (documentation Elastic).
Configuration : la signature couvre le corps seul ; dans l’entrée, définissez hmac.header: X-Fremforge-Signature, hmac.key: <the endpoint's HMAC secret>, hmac.type: sha256 et hmac.prefix: "sha256=". Pour utiliser aussi l’option d’en-tête secret de l’entrée, définissez le même nom et la même valeur d’en-tête que l’en-tête d’authentification du point de terminaison. Format json, ou ndjson pour les lots. Indexez actor, action et tenant_id en tant que keyword.
IBM QRadar
Ingère les corps HTTP POST via une source de logs utilisant le protocole HTTP Receiver. QRadar exécute un serveur HTTP ou HTTPS sur un Event Collector (port 12469 par défaut) ; le HTTPS nécessite un certificat émis par une AC, et le TLS mutuel est facultatif (documentation IBM). Les lignes CEF sont analysées par le Universal CEF DSM de QRadar.
Configuration : URL = l’URL HTTPS publique du récepteur, ou syslog+tls://<collector>:6514 pour une source de logs TLS Syslog ; format cef. fremforge ne peut pas présenter de certificat client : laissez le TLS mutuel désactivé. Lorsque l’Event Collector n’est pas joignable depuis Internet, un reverse proxy ou un relais syslog à votre périphérie lui transmet les événements.
Google SecOps (Chronicle)
Ingère via un flux webhook (Settings → Feeds → Add new, type de source Webhook, puis choisissez un type de logs). Les requêtes sont envoyées au point de terminaison ...:importPushLogs du flux avec une clé d’API et le secret du flux, en en-têtes X-goog-api-key et X-Webhook-Access-Key, ou sous la forme ?key=<API key>&secret=<secret>. Plusieurs objets JSON séparés par des sauts de ligne peuvent partager une requête, jusqu’à 4 Mo (documentation Google). Il n’existe pas d’analyseur fremforge prêt à l’emploi ; il vous faut un type de logs et un analyseur pour ces événements.
Configuration : URL = le point de terminaison du flux avec ?key=<API key>&secret=<secret> (stocké chiffré et affiché avec ***) ; format ndjson ; taille max. de requête au plus 4096 Kio.
Un relais qui vérifie la signature
Sentinel, et tout récepteur qui a besoin de plus d’un en-tête ou d’un jeton de courte durée, passent par un petit service : vérifier la signature fremforge, puis transférer le corps inchangé avec l’identifiant du SIEM. Cette version n’utilise que la bibliothèque standard de Python ; définissez TARGET_URL et TARGET_AUTH pour votre SIEM. Pour Sentinel, ajoutez la récupération du jeton ; avec le format json_array, le corps est déjà le tableau attendu. MAX_BODY correspond à la taille de requête par défaut de 1 Mio. Le relais transfère avec l’identifiant un événement correctement signé, répond 401 à une mauvaise signature ou à un horodatage vieux de plus de cinq minutes, et 502 lorsque la cible est injoignable.
"""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()Exécutez-le derrière votre terminaison TLS habituelle : fremforge ne livre qu’à des URL https:// publiques. Gardez la réponse du relais sous 10 secondes, et renvoyez une réponse autre que 2xx lorsque le SIEM a refusé l’événement. Un relais qui répond toujours 200 perd des événements : fremforge considère une réponse 2xx comme une livraison et retire l’événement de la file.
Webhook générique
Tout point de terminaison HTTPS qui renvoie 2xx une fois qu’il a accepté la requête. Vérifiez avec les extraits ci-dessus. Cela convient aux SIEM auto-hébergés (Wazuh, Graylog, …) et aux points d’ingestion HTTP tels que Logflare ou Datadog.
Santé côté opérateur
Les opérateurs de fremverk voient l’état de chaque point de terminaison sur la page de santé SIEM du portail opérateur. Une alarme se déclenche lorsque l’événement non livré le plus ancien d’un point de terminaison a plus de 24 heures, et nous contactons alors les propriétaires de votre organisation. Configurez aussi une alerte dans votre SIEM sur l’absence d’événements fremforge.
Ce qui ne passe pas sur le fil
- Syslog sur UDP ou TCP en clair. Le syslog passe uniquement par TLS.
- Certificats clients (TLS mutuel) vers votre récepteur.
- Plus d’un en-tête personnalisé par point de terminaison.
- Diffusion entre tenants. Chaque point de terminaison est lié à un seul tenant. Il n’existe aucun miroir côté opérateur ; les événements de votre organisation ne quittent jamais la chaîne propre au tenant.
Voir aussi
- Intégrité de la chaîne d’audit : ce que contiennent réellement les événements, et comment vérifier la chaîne de bout en bout.
- Webhooks : la même sémantique de format sur le fil pour les événements de dépôt et d’organisation, signés et livrés de la même manière.
- Modèle de sécurité des runners hébergés : d’où proviennent, dans la chaîne, les événements émis par les runners.