Aller au contenu principal

API des résultats

Chaque scanner capable de bloquer une fusion expose ses résultats via l’API REST. Lorsqu’un statut de commit indique « merge blocked », c’est ici que vous lisez les résultats précis qui en sont la cause, et ici que vous ignorez ceux que vous acceptez.

Ce sont les mêmes données que /<org>/_admin/code-security dans l’interface d’administration : l’interface est un client de ces endpoints.

Les segments de chemin ne correspondent pas aux noms de statut

C’est la seule chose à retenir par cœur. Le contexte du statut de commit et le segment de chemin de l’API diffèrent :

ScannerStatut de commit affiché sur la PRSegment de chemin de l’API
Analyse statiquefremforge/sastsast
Dépendancesfremforge/dep-scandeps
Images de conteneurfremforge/image-scanimages
Licencesfremforge/license-scanlicenses

Deviner le segment à partir du nom de statut renvoie 501 Not Implemented depuis la route fourre-tout de l’API, ce qui se lit comme « documenté mais pas encore livré ». Ce n’est pas le cas : vous avez la mauvaise URL.

URL de base

L’API du produit est servie sous /_app, et non à la racine du domaine (la racine est l’API propre à Forgejo) :

https://frem.sh/_app/api/v1/orgs/<org>/code-security/<scanner>/findings

Authentifiez-vous avec un jeton d’API d’organisation ou un PAT ; voir la référence de l’API pour les types de jetons.

Tous les résultats dans une seule liste

Pour lire d’un coup tous les résultats ouverts, quel que soit le scanner, utilisez la liste unique. C’est la liste de Sécurité › Résultats › Tous :

curl -sS \
  -H "Authorization: Bearer ${FREMFORGE_TOKEN}" \
  "https://frem.sh/_app/api/v1/orgs/acme/security/findings?severity=critical,high"
{
  "counts": { "code": 3, "secrets": 0, "dependencies": 41, "images": 12, "iac": 5, "licences": 2, "malware": 0 },
  "items": [
    {
      "type": "dependencies",
      "id": "0f6c…",
      "severity": "critical",
      "title": "GHSA-xxxx-xxxx-xxxx",
      "subject": "acme/api",
      "detail": "lodash@4.17.20",
      "pr_number": null,
      "known_exploited": true,
      "first_seen_at": "2026-10-02T08:14:03.120Z"
    }
  ],
  "next_cursor": "eyJr…"
}
  • type : une liste séparée par des virgules parmi code, secrets, dependencies, images, iac, licences et malware. Par défaut, tous. Les résultats d’infrastructure as code constituent ici leur propre type, bien que l’analyse qui les trouve soit l’analyse d’images.
  • severity : une liste séparée par des virgules parmi critical, high, medium et low.
  • repo : un dépôt (owner/name) ou une image.
  • limit : de 1 à 200, 50 par défaut. Pour la page suivante, passez comme cursor le next_cursor reçu. Il vaut null sur la dernière page.

L’ordre est celui de la console : exploitation connue d’abord, puis sévérité, puis ancienneté, de sorte que le résultat le plus proche de sa date limite de correction arrive en premier. Les sévérités sont rendues comparables entre scanners : une error SAST est high, un warning medium et tout le reste low ; une licence interdite est critical, une licence restreinte high et toute autre low ; les secrets et les malwares sont toujours critical. counts contient le nombre de résultats ouverts par type pour toute l’organisation, sans les filtres. Nécessite findings:read. Un type ou une sévérité inconnus renvoient 400.

  • branch : une branche de release protégée, par exemple release/1.x, pour lister les résultats de cette branche au lieu de ceux de la branche par défaut. Chaque ligne porte alors aussi branch et fix_by. Une branche qui n’a pas été analysée renvoie 404 unknown-branch. GET /orgs/{org}/security/release-branches liste les branches analysées.

Une ligne contient ce que la liste affiche. Pour le résultat complet (atteignabilité, triage, chemin de dépendance) et pour l’ignorer, utilisez l’endpoint propre au scanner ci-dessous avec l’id de la ligne.

Lister les résultats

Seuls les résultats ouverts, par défaut :

curl -sS \
  -H "Authorization: Bearer ${FREMFORGE_TOKEN}" \
  "https://frem.sh/_app/api/v1/orgs/acme/code-security/images/findings?limit=50"
{
  "findings": [
    {
      "id": "0f6c…",
      "registry_image": "acme/api@9f2c…",
      "cve_id": "CVE-2026-59950",
      "package": "mcp",
      "severity": "high",
      "fixed_version": "1.28.1",
      "dismissed_at": null,
      "created_at": "2026-07-25T06:33:41.940Z"
    }
  ],
  "has_more": false
}

Paramètres de requête, pour les quatre scanners :

  • limit : de 1 à 200, 50 par défaut.
  • offset : paginez avec offset += limit jusqu’à ce que has_more vaille false.
  • include_dismissed : mettez true pour voir aussi les résultats ignorés.

Les champs de la réponse diffèrent d’un scanner à l’autre, parce que les résultats diffèrent : images renvoie cve_id / package / fixed_version, deps ajoute ecosystem / manifest_path / current_version et, pour les résultats de la branche par défaut, via / dependency_path (la dépendance directe qui introduit le paquet, voir chemins de dépendance), licenses renvoie license_id / category, et sast renvoie rule_id / path / line. La spécification OpenAPI contient le schéma exact de chacun.

Deux champs supplémentaires proviennent de l’atteignabilité et du triage : les résultats deps et images portent un objet reachability et un objet sla (l’échéance en vigueur, avec le multiplicateur optionnel appliqué), et les résultats sast portent un objet triage (emplacement, chemin de propagation, historique des rejets et appartenance ou non au groupe de moindre confiance). deps et images acceptent aussi ?reachability= pour filtrer par verdict. Les trois acceptent ?sort=risk pour lister dans l’ordre de triage : pour deps et images, le verdict d’atteignabilité, puis la sévérité, puis l’exploitation connue, puis l’EPSS ; pour sast, les signaux de triage (le groupe de moindre confiance en dernier, puis la sévérité, puis une trace de propagation avant une correspondance inconnue ou purement par motif, puis les plus anciens d’abord). La pagination avec offset est stable dans les deux ordres. L’ordre par défaut est du plus récent au plus ancien, les égalités étant départagées par id.

Les résultats deps et images portent aussi known_exploited : null, ou { "cve_ids": [...], "sources": [...] } lorsque la vulnérabilité est répertoriée comme exploitée dans la nature par l’EU Vulnerability Database de l’ENISA (euvd) ou par le catalogue KEV de la CISA (cisa_kev). Un résultat de dépendance est rapproché via les alias CVE de son avis de sécurité, un résultat d’image par son identifiant CVE ; les deux listes sont actualisées quotidiennement. Ajoutez ?known_exploited=true pour ne lister que ces résultats, ou ?known_exploited=false pour les autres ; toute autre valeur renvoie 400. Les listes Dépendances et Images de conteneur de la console offrent le même filtre, Exploitation connue uniquement, et les Correctifs de sécurité ouvrent en premier leurs pull requests de correction.

Migration nécessaire

Un résultat deps dont fixed_version est nul porte needs_migration lorsque son avis de sécurité n’est corrigé que sous un autre paquet, par exemple un nouveau chemin de module Go ou une nouvelle version majeure (github.com/docker/docker vers github.com/moby/moby/v2) ou un paquet renommé (npm request vers @cypress/request). Sinon, il vaut null :

"fixed_version": null,
"needs_migration": { "package": "github.com/moby/moby/v2", "fixed_version": "2.0.0-beta.8" }

Aucune pull request de correction n’est proposée pour un tel résultat ; voir Migration nécessaire.

Quand un résultat se ferme

Les tables de résultats sont en insertion seule : chaque analyse écrit un nouvel ensemble de lignes sous une nouvelle exécution d’analyse, et rien n’est supprimé lorsque vous corrigez le problème sous-jacent. Deux choses ferment un résultat.

Remplacé automatiquement. Une tâche horaire résout les résultats dont l’exécution d’analyse n’est plus la plus récente pour leur cible, en les marquant dismissed_by: auto-resolve. La « cible » est l’identité naturelle pour chaque scanner :

scanneridentité de la cible
sastdépôt + chemin du fichier
deps, licensesdépôt + chemin du manifeste
imagesréférence de l’image

La règle effective est donc « si l’analyse la plus récente de cette cible ne l’a pas émis à nouveau, il a disparu », la même sémantique qu’utilisent OSV et Trivy. Corrigez-le, laissez une analyse ultérieure s’exécuter, et il disparaît dans l’heure. Notez que le déclencheur est une analyse plus récente de la même cible, et non la fusion elle-même : si rien ne réanalyse cette cible, la ligne reste ouverte indéfiniment.

Il y a là une propriété de sûreté délibérée : seule la dernière exécution d’analyse remplace les précédentes. Une analyse mal configurée qui n’émet rien pour une cible n’efface pas les résultats de cette cible, si bien qu’un scanner défaillant ne peut pas vider votre tableau de bord en silence.

Ignoré explicitement. Vous l’ignorez, avec une raison. Les rejets manuels sont préservés par la passe d’auto-résolution, votre raison n’est donc jamais écrasée.

Un rejet peut expirer. Donnez-lui un expires_at (via l’API) ou une date Expire le (dans les pages d’administration) et l’acceptation tombe d’elle-même : une tâche quotidienne rouvre le résultat une fois la date passée, consigne dans le journal d’audit qui l’avait ignoré et pourquoi, et, pour un résultat de sévérité élevée ou critique, envoie la même alerte de sécurité qu’un nouveau résultat. Une date signifie jusqu’à la fin de ce jour, à minuit heure de Europe/Copenhagen. Sans date, un rejet n’expire jamais : c’est le comportement par défaut et celui de tous les rejets antérieurs. L’expiration est reportée avec le rejet lors des analyses ultérieures, de sorte qu’une nouvelle analyse ne peut pas transformer une acceptation temporaire en acceptation permanente.

Ce que couvre le rejet d’un résultat d’analyse d’image. Un résultat d’image de conteneur ignoré le reste lors des analyses ultérieures du même dépôt ou de la même image, mais seulement pour la même CVE ou la même règle dans le même fichier (la target du résultat). Si la même règle apparaît dans un autre fichier, y compris un fichier ajouté plus tard, il s’agit d’un résultat distinct qui bloque de lui-même. Ignorer KSV-0014 sur un manifeste ne l’accepte donc pas pour le prochain Deployment que quelqu’un ajoutera.

En pratique : corrigez à la source et laissez l’analyse suivante remplacer le résultat. Ignorez-le lorsque vous acceptez le résultat, ou lorsqu’aucune autre analyse de cette cible n’est attendue, par exemple pour un résultat lié à une branche fusionnée depuis et qui n’est plus analysée.

Ignorer un résultat

reason est obligatoire, est stocké sur le résultat et est écrit dans le journal d’audit : rédigez-le pour la personne qui le lira dans six mois.

curl -sS -X POST \
  -H "Authorization: Bearer ${FREMFORGE_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Fixed at source in acme/api#412; stale PR-scoped row."}' \
  "https://frem.sh/_app/api/v1/orgs/acme/code-security/images/findings/0f6c…/dismiss"
{ "dismissed": true, "expires_at": null, "id": "0f6c…" }

Ajoutez expires_at pour rendre l’acceptation temporaire : une date ("2026-12-31", acceptée jusqu’à la fin de ce jour) ou un horodatage ISO 8601 avec fuseau. Elle doit être dans le futur et à moins de trois ans ; toute autre valeur renvoie 400 invalid-expires-at. La réponse renvoie l’instant enregistré :

{ "dismissed": true, "expires_at": "2026-12-31T23:00:00.000Z", "id": "0f6c…" }

Un 404 signifie qu’aucun résultat ouvert ne porte cet id : le plus souvent, il est déjà ignoré.

Pour en fermer plusieurs d’un coup, sast, deps et images acceptent jusqu’à 1000 ids par appel :

curl -sS -X POST \
  -H "Authorization: Bearer ${FREMFORGE_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"ids":["0f6c…","1a2b…"],"reason":"Accepted: vendored fixture, not shipped."}' \
  "https://frem.sh/_app/api/v1/orgs/acme/code-security/deps/findings/bulk-dismiss"

licenses ne permet que le rejet unitaire : il n’existe pas d’endpoint groupé pour ce scanner.

Lire n’est pas ingérer

Deux chemins d’apparence similaire remplissent des rôles différents :

  • /orgs/<org>/code-security/images/findings : ce que décrit cette page. Limité à l’organisation, lecture et rejet.
  • /_app/api/v1/image-scans/findings : l’endpoint d’ingestion auquel le runner du scanner envoie ses résultats par POST. Il ne sert pas à la lecture.

Paramètres

Le seuil de blocage de chaque scanner est aussi un appel d’API : PUT /orgs/<org>/code-security/<scanner>/settings. Notez que images bloque selon block_action, tandis que sast, deps et licenses bloquent selon block_severity ; le seuil des images est l’entrée image_scan de la politique d’analyse de l’organisation (PUT /orgs/<org>/policy, voir seuil de blocage) ; licenses accepte en plus allowlisted_license_ids.

Pour deps, il existe une politique optionnelle supplémentaire : PUT /orgs/<org>/security/dep-scan-unreachable-policy ({"enabled": true, "repos": [...]}, portée policy:write) empêche un résultat de pull request dont le verdict d’atteignabilité est non atteignable de bloquer fremforge/dep-scan. Elle est désactivée par défaut ; les résultats unknown, les malwares et les résultats à exploitation connue bloquent toujours, et un faux « non atteignable » laisse fusionner une vraie vulnérabilité. Voir Atteignabilité : ne pas bloquer sur les résultats non atteignables. Un résultat qu’elle a laissé passer porte block_waived: "unreachable" dans la liste ci-dessus.

Voir aussi