Pencheff

Risk, reporting, and compliance

Engagement profiles

Quick, Standard, Deep, Red-Team, AI-Only, Compliance, CI, and Continuous modes.

ScopeOperational Core

Run web, API, code, dependency, cloud, AI, and internal-network assessments from one queue with unified findings, evidence, remediation, and audit output.

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

Scans API

/reference/scans

POST /scans

Trigger a new scan.

POST /scans
{
  "target_id": "...",
  "profile": "standard",
  "consent_payload": {
    "authorization_text": "I am authorised to test example.com as of 2026-05-06 and I accept the disclosed actions.",
    "acknowledged": true
  }
}

profile is one of quick, standard, or deep. Older specialised names (engage, compliance, api-only, cicd, sca, iac, supply-chain, network-va, hackme, continuous, compliance-full) are still accepted and coerced to the matching tier at the runner — see Picking a profile for the fold-in table.

consent_payload is required when swarm mode is active (default). The API returns 422 Unprocessable Entity if the field is absent, if authorization_text is shorter than 50 characters, or if acknowledged is not true. The payload is persisted on Scan.consent_payload (JSONB) and included in every audit export. Shape (Pydantic schema ConsentPayload):

type ConsentPayload = {
  authorization_text: string;  // >= 50 characters
  acknowledged: boolean;       // must be true
};

GET /scans

List scans for the current org (paginated).

GET /scans/{id}

Fetch a single scan — includes summary counts, grade, progress.

GET /scans/{id}/progress (SSE)

Server-Sent Events stream of live progress ticks during an active scan. Events:

  • stage_start: recon_passive
  • stage_done: recon_passive: 47 endpoints
  • finding: HIGH SQLi on /api/users
  • finished

GET /scans/{id}/findings

Returns every finding for the scan with CVSS, EPSS, KEV, risk_score, compliance mapping.

GET /scans/{id}/linked-repos

Returns the repositories attached to this URL scan's target. Source-code findings (Semgrep · Bandit · gosec · Brakeman · ESLint · OSV · secret-scan) live on each repo's own assessment page (/repos/{repository_id}) — the URL scan deliberately does not mix SAST findings into its own results list. Use this endpoint to render a "Linked repositories" sidebar with deep-links.

type LinkedRepo = {
  repository_id: string;
  full_name: string;
  provider: string | null;
  scan_url: string;   // e.g. "/repos/abc-123"
};

Returns [] for scans whose target has no attached repos. Available on URL targets only.

GET /scans/{a}/compare/{b}

Compare two completed scans of the same workspace and return a structured diff. Particularly useful for LLM red-team scans where you want to gate a PR on safety regressions, or A/B different model versions on the same suite.

GET /scans/3a35.../compare/9359...
Authorization: Bearer <token>

Response:

type ScanCompare = {
  baseline: { name: string; summary: RedTeamSummary };
  candidate: { name: string; summary: RedTeamSummary };
  regressions: Finding[];     // candidate-only
  fixes: Finding[];           // baseline-only
  common_failures: Finding[]; // present in both
  counts: { regressions: number; fixes: number; common_failures: number };
  keys: { regressions: string[]; fixes: string[]; common_failures: string[] };
  scan_a: { id: string; profile: string; grade: string | null;
            score: number | null; created_at: string };
  scan_b: { id: string; profile: string; grade: string | null;
            score: number | null; created_at: string };
};

The dedup key is endpoint|parameter|technique|title, so re-running the identical suite against an unchanged target produces zero regressions. The web UI exposes the same diff at /scans/compare?a=…&b=… with a JUnit-XML download for the regressions list — wire it into CI to fail builds on new findings.

POST /scans/{id}/share (LLM only)

Issues a Fernet-encrypted token granting public read access to the scan's LLM-flavored report. Available only when the underlying target is kind: "llm".

POST /scans/3a35.../share?ttl_seconds=604800
Authorization: Bearer <token>

Response:

{
  "token": "gAAAAA...",
  "expires_in": 604800,
  "url_path": "/share/llm/gAAAAA..."
}

The companion public route GET /share/llm/{token} renders the scan as HTML (default), Markdown, CSV, or JSON depending on the ?download= query param. Token expiry is the only revocation — let it expire to revoke. PII is redacted in evidence snippets before rendering, regardless of the public/private path.

GET /scans/{id}/llm-traces

Returns the full LLM call trace for a scan. Each row corresponds to one chat-completions call made by a swarm agent during the scan.

Authentication required. Returns [] for scans run with SWARM_ENABLED=false.

type ScanLLMTrace = {
  id: string;
  scan_id: string;
  agent: string;             // e.g. "InjectionAgent", "ChainAgent"
  turn: number;              // agent conversation turn number
  request_messages: object[];  // full messages array sent to the LLM
  response: object;          // raw LLM response including tool-call blocks
  input_tokens: number;
  output_tokens: number;
  cache_read_tokens: number;
  reasoning: string | null;  // thinking/reasoning block if present
  created_at: string;
};

GET /scans/{id}/evidence/{finding_id}.png

Returns the Playwright evidence screenshot captured by EvidenceCaptureAgent for the specified finding. PII is redacted before storage.

Authentication required. Returns 404 Not Found if no screenshot exists for that finding (e.g. the finding was below high severity, the agent did not run, or the scan used the legacy single-agent path).

GET /scans/3a35.../evidence/f7b2c4....png
Authorization: Bearer <token>

Response: image/png binary. Cache the response client-side — screenshots do not change after the scan completes.

DELETE /scans/{id}

Cancel an in-flight scan or remove a finished one.

Scan object

type Scan = {
  id: string;
  target_id: string;
  status: "queued" | "running" | "completed" | "failed" | "cancelled";
  profile: string;
  progress_pct: number;      // 0-100
  current_stage: string | null;
  summary: {
    critical: number; high: number; medium: number; low: number; info: number;
    suppressed: number;
  } | null;
  grade: "A" | "B" | "C" | "D" | "F" | null;
  score: number | null;
  started_at: string | null;
  finished_at: string | null;
};

From the Pencheff docs

Core concepts

/getting-started/concepts

Session

Every scan runs inside a PentestSession identified by a 12-char hex ID. The session holds the target, discovered state (endpoints, subdomains, tech stack, open ports, WebSocket endpoints, OAuth endpoints, WAF info, exploit chains), credentials, a FindingsDB, and a full request log.

Sessions live in memory while the scan runs; the SaaS backend persists them to Postgres so the dashboard can keep rendering long after the agent disconnected.

Finding

A Finding captures a single vulnerability with:

  • severity (critical | high | medium | low | info) and cvss_score/cvss_vector
  • category (injection, auth, authz, components, misconfiguration, …)
  • owasp_category (A01…A10) — automatically maps into PCI-DSS, NIST 800-53, SOC 2, ISO 27001, HIPAA
  • verification_statusunverified | true_positive | false_positive | true_negative | false_negative
  • evidence — list of request/response pairs proving the vuln
  • remediation and references
  • epss, kev, and risk_score = cvss × (1 + epss) × (2 if kev else 1) — see EPSS & KEV enrichment
  • sla_days and due_date — computed from severity, breached hourly by the SaaS SLA monitor

Findings are deduplicated by (endpoint, parameter, category, title).

Verification status

StatusMeaning
unverifiedScanner flagged it, not yet confirmed
true_positiveProven exploitable via test_endpoint
false_positiveScanner error — tested and safe
true_negativeConfirmed absent
false_negativeMissed by scanner, found manually

An elite Pencheff report contains only findings verified as true_positive. The agent is instructed to never report unverified findings as confirmed.

Suppression

A finding can be suppressed with one of:

  • accepted_risk — known and accepted
  • wont_fix — acknowledged but not in remediation scope
  • false_positive — confirmed noise
  • duplicate — same vuln tracked elsewhere
  • out_of_scope — outside agreed test scope

Suppressed findings are excluded from reports by default but remain in the database with timestamp, reason, and notes.

Profile

A profile is a named preset that selects which modules to run, at what depth, with what crawl settings. See the first-scan guide for the 12 built-in profiles.

Policy

A ScanPolicy (apiVersion: pencheff/v1) is a YAML file that fully specifies a scan: targets, auth, modules, assertions, thresholds, reports, schedule.

apiVersion: pencheff/v1
kind: ScanPolicy
spec:
  targets: [{ url: https://example.com }]
  modules:
    - { name: scan_injection, depth: standard }
  assertions:
    - { id: no_critical, condition: "critical == 0" }
  thresholds: { fail_on: high }

Scan profile YAML is stored alongside each schedule.

Exploit chain

A chain is a multi-step narrative attack. Pencheff's exploit_chain_suggest tool proposes chains like:

  • SSRF → cloud metadata → IAM credential theft → S3 read
  • XSS → session theft → admin impersonation → database dump
  • IDOR → user enumeration → admin → privilege escalation
  • Open redirect → OAuth token theft → account takeover

test_chain then runs each step and records the evidence end-to-end.

Compliance mapping

Every finding carries an OWASP category; Pencheff's reporting/compliance.py auto-derives:

  • OWASP Top 10 2021
  • PCI-DSS 4.0 requirement numbers
  • NIST 800-53 Rev 5 control families
  • SOC 2 Trust Services Criteria
  • ISO 27001:2022 Annex A controls
  • HIPAA Security Rule safeguards

Compliance reports are pre-rendered in Word, CSV, JSON, and SPDX/CycloneDX (for SBOM).

Credentials

Credentials live in a CredentialStore — one session can carry multiple named sets (default, admin, readonly, …) for testing authorization boundaries. Every credential is wrapped in a MaskedSecret that masks itself in repr/str to prevent accidental leakage. The SaaS backend stores them Fernet-encrypted at rest.

What's next

FAQ

Common questions

What is an engagement profile in Pencheff?
An engagement profile is a pre-configured scan template that controls scope, depth, test modules, authentication settings, rate limits, and compliance mappings for a specific type of assessment — such as a quick surface scan, a deep API pentest, or a CI/CD gate check.
Can I create custom engagement profiles for my team?
Yes. You can define, save, and share custom profiles with your workspace. Profiles can specify which vulnerability classes to test, set exclusion rules for sensitive endpoints, and configure rate limits appropriate for your application's production environment.
What is the difference between Quick, Standard, and Deep profiles?
Quick (2–5 min) runs high-confidence, low-noise checks against the primary surface. Standard (10–25 min) adds broader injection coverage and access-control probes. Deep (30–90 min) enables full exploit chaining, authenticated session testing, and all OWASP Top 10 modules.

Related

Keep exploring Platform.