Skip to main content

Findings API

Every scanner that can block a merge exposes its findings over the REST API. When a commit status says “merge blocked”, this is where you read the specific findings behind it, and where you dismiss the ones you accept.

This is the same data as /<org>/_admin/code-security in the admin UI — the UI is a client of these endpoints.

The path segments do not match the status names

This is the one thing worth memorising. The commit-status context and the API path segment differ:

ScannerCommit status you see on the PRAPI path segment
Static analysisfremforge/sastsast
Dependenciesfremforge/dep-scandeps
Container imagesfremforge/image-scanimages
Licensesfremforge/license-scanlicenses

Guessing the segment from the status name returns 501 Not Implemented from the API’s catch-all, which reads as “documented but not yet shipped”. It is not — you have the wrong URL.

Base URL

The product API is served under /_app, not at the apex (the apex is Forgejo’s own API):

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

Authenticate with an org API token or a PAT — see the API reference for the token types.

List findings

Open findings only, by default:

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
}

Query parameters, all four scanners:

  • limit — 1–200, default 50.
  • offset — page with offset += limit until has_more is false.
  • include_dismissed — set true to see dismissed findings too.

The response fields differ per scanner, because the findings do: images returns cve_id / package / fixed_version, deps adds ecosystem / manifest_path / current_version, licenses returns license_id / category, and sast returns rule_id / path / line. The OpenAPI spec carries the exact schema for each.

When a finding closes

Findings tables are insert-only: every scan writes a fresh set of rows under a new scan run, and nothing is deleted when you fix the underlying problem. Two things close a finding.

Superseded automatically. An hourly job resolves findings whose scan run is no longer the latest for their target, marking them dismissed_by: auto-resolve. “Target” is the natural identity for each scanner:

scannertarget identity
sastrepo + file path
deps, licensesrepo + manifest path
imagesimage reference

So the effective rule is “if the most recent scan of that target didn’t re-emit it, it’s gone” — the same semantics OSV and Trivy use. Fix it, let a later scan run, and it clears within the hour. Note the trigger is a newer scan of the same target, not the merge itself: if nothing re-scans that target, the row stays open indefinitely.

There is a deliberate safety property here — only the latest scan run supersedes. A misconfigured scan that emits nothing for a target does not wipe that target’s findings, so a broken scanner cannot silently clear your dashboard.

Dismissed explicitly. You dismiss it, with a reason. Manual dismissals are preserved by the auto-resolve pass, so your reason is never overwritten.

In practice: fix at source and let the next scan supersede it. Dismiss when you are accepting the finding, or when no further scan of that target is expected — for example a finding tied to a branch that has since merged and is no longer scanned.

Dismiss a finding

reason is required, is stored on the finding, and is written to the audit log — write it for whoever reads it in six months.

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, "id": "0f6c…" }

A 404 means no open finding with that id — most often it is already dismissed.

To close many at once, sast, deps and images accept up to 1000 ids per call:

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 has single dismissal only — there is no bulk endpoint for it.

Reading is not ingesting

Two similar-looking paths do different jobs:

  • /orgs/<org>/code-security/images/findings — what this page describes. Org-scoped, read and dismiss.
  • /api/v1/image-scans/findings — the ingest endpoint the scanner runner POSTs results to. Not for reading.

Settings

Each scanner’s blocking threshold is also an API call — PUT /orgs/<org>/code-security/<scanner>/settings. Note that images gates on block_action while sast, deps and licenses gate on block_severity; licenses additionally takes allowlisted_license_ids.

See also