Pencheff

Code security

Findings register

Finding lifecycle, severity, verification, and comments.

ScopeFeatured

Jump into setup guides, feature references, reporting conventions, API documentation, methodology pages, and workflow-specific playbooks.

OutputUnified evidence

Findings, reports, dashboards, exports, integrations, and retests all read from the same normalized record.

MethodDeterministic first

Pencheff favors repeatable checks, then uses AI for triage, enrichment, orchestration, and remediation where it adds signal.

From the Pencheff docs

Findings API

/reference/findings

GET /scans/{scan_id}/findings

Return every finding for a scan. Filter with query params:

  • ?severity=critical
  • ?category=injection
  • ?owasp_category=A03
  • ?verified_only=true
  • ?include_suppressed=true
  • ?sort=risk_score (default; use cvss_score or created_at)

GET /findings/{id}

Fetch a single finding with full evidence, comments, assignments, tags.

PATCH /findings/{id}

Update status. Valid fields:

{
  "verification_status": "true_positive" | "false_positive" | "true_negative" | "false_negative",
  "suppressed": true,
  "suppress_reason": "accepted_risk" | "wont_fix" | "false_positive" | "duplicate" | "out_of_scope",
  "suppress_notes": "string",
  "resolved_at": "2026-04-21T…Z",
  "sla_days": 7
}

Collaboration

  • POST /findings/{id}/comments — add a comment
  • GET /findings/{id}/comments — list comments
  • POST /findings/{id}/assign{"assignee_user_id": "..."}
  • POST /findings/{id}/tags{"tag": "p0-fix"}
  • DELETE /findings/{id}/tags/{tag} — remove a tag

Prioritisation fields

Every Finding includes the unified prioritisation surface:

FieldTypeSource
risk_scorefloat (0–100)computed at insert from CVSS × EPSS × KEV × SSVC × reachability
ssvc_decisionstringone of act, attend, track_star, track
reachabilitystringone of exploited, reachable, present, unknown — see Reachability classifier
epssfloat (0–1) | nullEPSS feed; populated for SCA findings
kevboolCISA KEV catalog membership

Sort the list endpoint by risk_score:

GET /scans/{scan_id}/findings?sort=risk_score   (default)

The unified, cross-table queue lives at /unified-findings.

POST /findings/{id}/propose_fix

Generates a draft FixProposal for the finding. SCA findings get a deterministic version-bump diff; SAST/DAST findings synthesise a unified diff via the operator-configured patch-synthesis backend. See Auto-fix PRs.

The route accepts kind{sast, dast}; SCA findings ride the dast kind and Pencheff detects the SCA payload from evidence and routes internally.

POST /findings/{id}/triage

Pro tier. Triage 2.0 — exploitability walkthrough returning { walkthrough, blast_radius, exploit_scenario, fix_outline, confidence }. Cached on finding.ai_triage; pass ?force=true to regenerate. See Triage 2.0.

From the Pencheff docs

Unified findings stream

/features/findings-stream

A single sortable, filterable queue across SAST, DAST, SCA, IaC, and secrets — no more "click into a scan, then its findings" friction when you just want to know "what should I fix first?"

Lives at /findings in the dashboard.

What it does

  • Pulls from both the findings table (DAST + SCA from live scans) and repo_findings table (SAST + SCA + secrets + IaC from repo scans).
  • Projects them to a common shape and merges.
  • Sorts by Pencheff's unified risk_score (CVSS × EPSS × KEV × SSVC × reachability) — NULL last, then severity, then created_at.
  • Paginates with stable order across pages.

Filters

The dashboard's filter chips translate one-to-one to query params:

GET /unified-findings
  ?source=sast        # also: dast, sca, iac, secret (multi-select)
  &severity=critical  # critical | high | medium | low | info
  &reachability=exploited
  &include_suppressed=false
  &target_id=<uuid>   # restrict to one target
  &limit=50&offset=0

Server-side filters fire before the merge, so paginated results stay consistent.

Source mapping

Pencheff projects each underlying row to a single source label so filters work uniformly:

sourceFrom findings rows where…From repo_findings rows where…
dastcategory != "components"
scacategory == "components"scannerosv, ghsa, pip-audit, npm-audit
sastscannersemgrep, bandit, gosec, brakeman, eslint, treesitter:*, ruff. (Legacy codeql rows from pre-v0.7 scans still classify as SAST.)
secretscannergitleaks, detect-secrets
iacscannertrivy_iac, checkov

Drill-through

Each row in the list is a hyperlink. The unified item carries enough metadata for the dashboard to deep-link:

  • findings-table rows → /findings/<id> → redirector that resolves the scan_id and forwards to the scan-scoped detail page.
  • repo_findings-table rows → /repos/... (the existing repo finding detail page).

Risk-first by default

The default sort is descending risk_score. A KEV-listed CVSS 6.0 beats an unexploited CVSS 8.5 — that's what Snyk gets wrong by sorting on CVSS alone. See EPSS, KEV, SSVC for the full priority formula.

What's tested

cd apps/api && uv run pytest tests/test_unified_findings.py

12 unit tests cover the projection logic + sort key (risk_score desc, severity tiebreaker, recency tiebreaker) + ecosystem-aware source mapping.

Related

Keep exploring Resources.