Pencheff

AI security

AI agents

Tool-calling scan agent for testing LLM apps, chatbots, and agentic workflows.

ScopeAI Security

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

AI agent swarm — parallel multi-agent scanning

/features/swarm

Pencheff's swarm mode replaces the legacy single-agent loop as the default execution path for every scan. Instead of one agent iterating through tool calls sequentially, the orchestrator fans out work across 17 specialised agents arranged in three phases. Recon runs first and produces a frozen snapshot of the attack surface; 12 breaker agents then attack the target in parallel from that snapshot; 6 synthesis agents then process the merged findings in parallel to produce chains, compliance mappings, impact proofs, PoCs, screenshots, and admin access evidence.

The net effect is substantially deeper coverage in roughly the same wallclock time as the legacy loop, plus structured operator output that maps every finding through the full exploit chain → compliance → reproducibility path.

The swarm is opt-out: set SWARM_ENABLED=false to instantly revert every scan to the legacy single-agent path with no other changes required.

Pipeline shape

┌─────────────────────────────────────────────────────────────┐
│  Phase 1: ReconAgent (1 agent)                              │
│  Map attack surface → frozen ReconSnapshot                  │
└────────────────────────┬────────────────────────────────────┘
                         │ snapshot (read-only)
         ┌───────────────▼───────────────────────────────────┐
         │  Phase 2: Breakers (12 agents, fully parallel)    │
         │                                                    │
         │  InjectionAgent   ClientSideAgent   AuthAgent      │
         │  AuthzAgent       APIAgent          InfraAgent     │
         │  CloudAgent       LLMRedTeamAgent   SupplyChainAgent│
         │  K8sAgent   ActiveDirectoryAgent   MobileAppAgent  │
         └───────────────┬───────────────────────────────────┘
                         │ merge findings from all breakers
         ┌───────────────▼───────────────────────────────────┐
         │  Phase 3: Synthesis (6 agents, fully parallel)    │
         │                                                    │
         │  ChainAgent         ComplianceAgent                │
         │  ProofOfImpactAgent PayloadCraftingAgent           │
         │  EvidenceCaptureAgent AdminAccessAgent             │
         └───────────────────────────────────────────────────┘

The 19 agents

AgentPhaseMandateTools
ReconAgent1Map attack surface; produce frozen snapshotrecon_passive, recon_active, recon_api_discovery, scan_waf, authenticated_crawl
InjectionAgent2SQLi/NoSQLi/XXE/SSTI/cmdi + path traversal + file uploadscan_injection, scan_file_handling, oast_*, test_endpoint
ClientSideAgent2Reflected/DOM XSS, CSRF, open redirect, CORSscan_client_side, scan_dom_xss, test_endpoint
AuthAgent2Login weakness, JWT, OAuth, MFA bypassscan_auth, scan_oauth, scan_mfa_bypass, test_endpoint
AuthzAgent2IDOR, vertical/horizontal privesc — quiet-quits when no credentials suppliedscan_authz, test_endpoint
APIAgent2API/GraphQL flaws, websocket, business logicscan_api, scan_websocket, scan_business_logic, test_endpoint
InfraAgent2TLS/headers, HTTP smuggling, CRLF, subdomain takeoverscan_infrastructure, scan_advanced, scan_subdomain_takeover, run_security_tool
CloudAgent2Cloud misconfig, IAM, public blobs, blind SSRF callbacksscan_cloud, oast_*, test_endpoint
LLMRedTeamAgent2Prompt injection, jailbreak, system-prompt extraction on AI/LLM endpointsscan_llm_red_team, test_endpoint
SupplyChainAgent2Exposed dependency manifests, outdated client-side librariesrun_security_tool, test_endpoint
K8sAgent2Kubernetes control-plane exposure, RBAC misconfig, exposed metricsrun_security_tool, test_endpoint
ActiveDirectoryAgent2Active Directory attack paths: BloodHound relationship graph, Certipy ESC1–ESC8 cert template abuse, CrackMapExec SMB enum, Impacket secretsdumpscan_active_directory, test_endpoint
MobileAppAgent2Android/iOS static analysis: MobSF enrichment, manifest exported-component check, secrets sweep across decompiled outputscan_mobile_app
ChainAgent3Multi-step attack chains; blast-radius scoring; cross-system chain detectionexploit_chain_suggest, test_chain, test_endpoint
ComplianceAgent3Map findings to PCI-DSS/HIPAA/SOC2/GDPR controls (read-only)get_findings
ProofOfImpactAgent3Schema-only impact assessment via sqlmap (--dbs/--tables/--columns/--count only). No row data extracted.run_security_tool, test_endpoint
PayloadCraftingAgent3Generate curl + Python requests PoCs per finding (read-only synthesis)get_findings
EvidenceCaptureAgent3Playwright screenshot per verified high/critical finding with PII redactioncapture_evidence
AdminAccessAgent3Per-finding gated; when verified admin access exists, drives Playwright into the admin panel read-only: front-page screenshot, ≤ 5 menu links enumerated, then immediate logout. No state-changing tools in registry.playwright_navigate (GET-only), playwright_screenshot, playwright_enumerate_links, playwright_logout

Phase 1: Recon

The ReconAgent runs first and exclusively. It calls recon_passive (DNS, HTTP headers, technology fingerprinting, Shodan metadata if configured), recon_active (path enumeration, port probe on the primary host), and recon_api_discovery (OpenAPI/GraphQL schema fetch, common API prefixes). It also runs scan_waf to detect WAF presence and authenticated_crawl when credentials are supplied.

The result is a frozen ReconSnapshot: a serialised record of discovered endpoints, technology stack fingerprints, WAF type, and discovered API specs. All Phase 2 agents receive an identical read-only copy of this snapshot — they cannot extend it or communicate with each other.

Graceful degradation: if any individual recon call returns a transient error (network timeout, tool error), the agent retries once with a 5 s backoff. If the retry also fails, the snapshot is emitted with the successfully-collected endpoints and a partial=true flag. The orchestrator proceeds — Phase 2 agents see the partial snapshot and work on what exists. Only a fully empty snapshot (zero endpoints) triggers the catastrophic fallback (see below).

Phase 2: Breakers (parallel fan-out)

Once the ReconSnapshot is sealed, the orchestrator spawns all 12 breaker agents simultaneously via asyncio.gather. Each agent receives:

  • A fresh, isolated pencheff session seeded with the read-only snapshot (so every breaker starts from the same known state).
  • Its own per-agent tool registry — each agent is granted only the tools it needs, reducing the chance of accidental cross-domain tool calls.
  • A turn budget drawn from the SWARM_TURNS_* env-var family (see Configuration).

Notable behaviours:

  • AuthzAgent quiet-quit: if the scan has no credentials, AuthzAgent emits a single informational note ("no credentials — skipping authz scan") and exits cleanly. Its absence is non-fatal.
  • Per-breaker retry: each breaker retries its first failing tool call once with a 10 s backoff. After that, partial findings are committed and the agent exits — a breaker crash does not bring down the swarm.
  • Partial-failure tolerance: the orchestrator waits for all 12 breakers, collects results from those that succeeded, and logs a warning for those that failed. The merge step proceeds on whatever findings exist.

After all breakers finish, their findings are de-duplicated by a deterministic key (endpoint|parameter|technique|title) and merged into a single findings list that Phase 3 agents consume.

Phase 3: Synthesis (6 agents in parallel)

All six synthesis agents start simultaneously once the merged findings list is available. Each reads the merged findings (and nothing else) — none of them probe the target again except to capture a specific screenshot or run a --count-only sqlmap call.

Each agent writes its output into a named section of the operator summary. Failure of any one synthesis agent is non-fatal: the other five still deliver their sections, and the failed section is noted as "unavailable" in the report rather than crashing the whole scan.

Operator-visible output sections

The final operator summary stitches the Phase 3 agent outputs into a structured document in this order:

  1. Lead paragraph (from ChainAgent) — the top attack chain with its blast-radius score and any cross-system chain it detected.
  2. ## Compliance mapping (from ComplianceAgent) — which PCI-DSS, HIPAA, SOC 2, and GDPR controls are affected.
  3. ## Proof of Impact (from ProofOfImpactAgent) — schema-level evidence: database names, table names, column names, row counts. No customer data is extracted.
  4. ## Reproducible PoCs (from PayloadCraftingAgent) — one curl command and one Python requests snippet per verified high/critical finding.
  5. ## Evidence Screenshots (from EvidenceCaptureAgent) — inline PNG thumbnails of every verified high/critical finding with PII redacted.
  6. ## Admin Panel Access (Verified) (from AdminAccessAgent) — a screenshot of the admin panel front page plus up to 5 enumerated menu links. This section is only present when verified admin access was confirmed by a Phase 2 breaker.

Catastrophic fallback

If the ReconAgent produces a snapshot with zero endpoints, or if all 12 Phase 2 breakers fail, the orchestrator falls back automatically to the legacy single-agent loop (agent_runner.run_agent). The scan continues — no operator action required. A swarm_fallback: true flag is set on the scan record and visible in the scan-detail API response and the UI banner.

Killswitch: setting SWARM_ENABLED=false in the API environment immediately disables swarm mode for all new scans. In-flight scans are unaffected. This is the fastest path to reverting to the legacy path if an unexpected issue arises.

Cost and performance

Typical numbers for a deep scan against a medium-complexity target:

MetricTypical value
Wallclock time~33 minutes
Total input tokens~411 K
Total output tokens~86 K
Total LLM calls~109 calls

Per-tier turn budgets (controlled by SWARM_TURNS_* env vars):

TierReconAgentEach breakerEach synthesis agent
quick8126
standard152510
deep255020

Configuration

All swarm behaviour is driven by environment variables on the API container. Field naming follows apps/api/pencheff_api/config.py.

VariableDefaultDescription
SWARM_ENABLEDtrueMaster on/off switch. false reverts every scan to the legacy single-agent loop.
SWARM_TURNS_RECON_QUICK8Turn budget for ReconAgent on quick profile.
SWARM_TURNS_RECON_STANDARD15Turn budget for ReconAgent on standard profile.
SWARM_TURNS_RECON_DEEP25Turn budget for ReconAgent on deep profile.
SWARM_TURNS_BREAKER_QUICK12Turn budget per Phase 2 breaker on quick.
SWARM_TURNS_BREAKER_STANDARD25Turn budget per Phase 2 breaker on standard.
SWARM_TURNS_BREAKER_DEEP50Turn budget per Phase 2 breaker on deep.
SWARM_TURNS_SYNTHESIS_QUICK6Turn budget per Phase 3 synthesis agent on quick.
SWARM_TURNS_SYNTHESIS_STANDARD10Turn budget per Phase 3 synthesis agent on standard.
SWARM_TURNS_SYNTHESIS_DEEP20Turn budget per Phase 3 synthesis agent on deep.
SWARM_TURNS_CHAIN_QUICK6Override for ChainAgent specifically on quick (defaults to synthesis budget if unset).
SWARM_TURNS_CHAIN_DEEP30Override for ChainAgent on deep.
SWARM_BREAKER_RETRY_ATTEMPTS1How many times a failing breaker tool call is retried.
SWARM_BREAKER_RETRY_BACKOFF_SEC10Seconds to wait between retry attempts.

Consent screen

Because the swarm calls significantly more external endpoints and can demonstrate real proof-of-impact (schema enumeration, admin access), Pencheff requires explicit operator consent before any scan is created.

The scan-creation form (and the POST /scans API body) now includes a consent_payload block. The operator must:

  1. Review the disclosed-actions catalogue for the agent classes they are enabling. Each agent class lists exactly what it will probe and what data it may read.
  2. Paste or type an authorization statement of at least 50 characters (typically: "I am authorised to test [target] as of [date] and I accept the disclosed actions above.").
  3. Tick the "I confirm" checkbox.

The consent_payload is persisted on Scan.consent_payload (JSONB) and is included in every audit export. The API rejects POST /scans if consent_payload is absent or if the authorization text is shorter than 50 characters.

Note: The consent model described here covers the current non-destructive swarm only. Agents that mutate target state, extract row data, or impact availability require a separate expanded consent flow documented in docs/superpowers/specs/2026-05-06-destructive-agents-blueprint.md (repo path, not a published docs route) and are not enabled in any current release.

LLM trace persistence

Every LLM call made by every swarm agent is recorded in the scan_llm_traces database table. Each row stores:

  • agent — which agent made the call (InjectionAgent, ChainAgent, etc.)
  • turn — the agent's conversation turn number at the time of the call
  • request_messages — the full messages array sent to the LLM (JSONB)
  • response — the raw response (JSONB), including tool-call blocks
  • input_tokens, output_tokens, cache_read_tokens — token counts
  • reasoning — the reasoning/thinking block if the model returned one

Traces are accessible via GET /scans/{id}/llm-traces (auth required). They are also summarised inline in the scan assessment log:

[InjectionAgent] LLM turn=3 in=1234t out=567t cached=800t · calls=[scan_injection]
[ChainAgent] LLM turn=1 in=3421t out=912t cached=2800t · calls=[exploit_chain_suggest,test_chain]

Evidence screenshots

When EvidenceCaptureAgent runs, it opens a Playwright browser context, navigates to the vulnerable URL with the session's auth cookies, and captures a full-page PNG. PII is redacted before the PNG is stored.

Screenshots are stored at ~/.pencheff/evidence/<scan_id>/<finding_id>.png inside the API worker container.

They are served via GET /scans/{id}/evidence/{finding_id}.png (auth required). A 404 is returned if no screenshot exists for that finding.

The Evidence Screenshots section of the operator summary embeds each PNG inline via a signed URL that expires after 24 hours.

What we DON'T do

The current swarm is explicitly non-destructive:

  • No row data is extracted from databases — ProofOfImpactAgent uses sqlmap with --dbs/--tables/--columns/--count only.
  • No state-changing requests are issued — every breaker and synthesis agent operates read-only.
  • No availability degradation — no slow-loris, query-of-death, or resource exhaustion testing.
  • No out-of-scope lateral movement.

Capabilities that would change any of these properties require a separate expanded consent model, legal review, and additional infrastructure described in docs/superpowers/specs/2026-05-06-destructive-agents-blueprint.md (repo path). None of those capabilities are implemented in the current release.

From the Pencheff docs

Pencheff Sentry — runtime LLM guardrail

/features/sentry

Sentry is a runtime LLM guardrail that drops between your application and the model provider. It blocks prompt injection, PII / secret exfiltration, unsafe HTML in model output, and unbounded consumption as they happen — instead of catching them post-hoc on the next Pencheff red-team scan.

Same OWASP-LLM-Top-10 (2025) taxonomy as the offline scanner. Same detector library. Inline.

Modes

ModeWhat it isBest for
HTTP proxy sidecarA FastAPI service in front of an OpenAI-compatible upstreamDrop-in URL change for any OpenAI-compatible provider
LiteLLM pluginpre_call / post_call hooksStacks already running LiteLLM
MCP middlewareWraps the MCP tool-call pathLLM agents that call tools — blocks unsafe tool args inline

The Cloudflare Worker mode (edge deployment) is on the v0.8 roadmap.

Quick start

pip install pencheff-sentry

pencheff-sentry serve \
  --upstream https://api.openai.com/v1 \
  --port 4242 \
  --max-output-tokens 4000

Then change your application's OpenAI base URL from https://api.openai.com/v1 to http://localhost:4242. Sentry forwards allowed requests verbatim and blocks unsafe ones with a clean 403 sentry_blocked response that includes the OWASP-LLM category.

{
  "error": {
    "message": "Pencheff Sentry blocked: prompt injection (direct-override)",
    "type": "guardrail_block",
    "code": "sentry_blocked",
    "pencheff_sentry": {
      "category": "LLM01",
      "detector": "direct-override"
    }
  }
}

What it detects

OWASP LLMDetectorExamples
LLM01Prompt injectionignore previous instructions, pretend to be DAN, print your system prompt, encoded variants
LLM02PII / secretsSSN, credit card, email, phone, AWS access key, OpenAI sk-, GitHub PAT shapes
LLM05Unsafe output handling<script> / <iframe> / javascript: / inline event handlers in model response
LLM10Unbounded consumptionOutput token ceiling configurable via --max-output-tokens

The full pattern set lives in pencheff_sentry/core.py — pure Python, no I/O, easy to extend.

LiteLLM plugin

import litellm
from pencheff_sentry.litellm_plugin import register

register(litellm)

# Sentry now intercepts every litellm.completion() call.
response = litellm.completion(
    model="gpt-4",
    messages=[{"role": "user", "content": "..."}],
)

pre_call raises litellm.BadRequestError on a blocked prompt. The post_call hook mutates a blocked response into a safe refusal string and stamps response.pencheff_sentry = {blocked, category, detector, reason} so downstream code can distinguish a guardrail-driven refusal from a model-native refusal.

Audit log

Sentry never persists prompt or response bodies by default — auditors asking "did you log my customer's prompt?" get a clean answer. The opt-in audit log (--audit-log path.jsonl) records decisions only: verdict, detector, category, plus a SHA-256 hash of the prompt/response for correlation. Never the body itself.

{"ts":"2026-05-08T15:00:01Z","side":"prompt","verdict":"block","category":"LLM01","detector":"direct-override","reason":"prompt injection (direct-override)","prompt_hash":"a7c2..."}

Default judge

The default judge is IBM Granite Guardian (Apache-2.0). Llama Guard 3 is opt-in via PENCHEFF_LLAMA_GUARD_ENABLED=1 — it ships under the Llama Community License (≤700 M MAU + attribution required), and Pencheff surfaces the license notice in every JudgeResult.reason so downstream consumers can reproduce it.

See features/llm-redteam for the full judge ensemble.

Extending the detector chain

from pencheff_sentry.core import GuardrailConfig, evaluate_prompt

cfg = GuardrailConfig(
    extra_patterns=[
        # (regex, detector_name, owasp_category)
        (r"(?i)\binternal[- ]doc:[a-z0-9-]+\b", "internal-doc-leak", "LLM02"),
    ],
)
decision = evaluate_prompt(user_prompt, config=cfg)
if decision.verdict == "block":
    refuse(decision.reason)

Source

  • Package: pencheff-sentry on PyPI (separate from the main pencheff package).
  • Source tree: plugins/sentry/.
  • License: MIT.

Related

Keep exploring Platform.