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:
| Scanner | Commit status you see on the PR | API path segment |
|---|---|---|
| Static analysis | fremforge/sast | sast |
| Dependencies | fremforge/dep-scan | deps |
| Container images | fremforge/image-scan | images |
| Licenses | fremforge/license-scan | licenses |
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>/findingsAuthenticate 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 withoffset += limituntilhas_moreisfalse.include_dismissed— settrueto 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:
| scanner | target identity |
|---|---|
sast | repo + file path |
deps, licenses | repo + manifest path |
images | image 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
- Accepting findings in-repo — the other way to accept a finding: a Trivy ignore file committed alongside the code.
- Image scanning — how images get scanned and what blocks a merge.
- License scanning — policy categories and allowlisting.
- API reference — authentication, tokens, and the full OpenAPI spec.