Skip to content

Releases: 0xSteph/pentest-ai

ptai 1.1.0 — verification coverage + resilience

Choose a tag to compare

@0xSteph 0xSteph released this 29 Jun 05:41
1658a0c

ptai 1.1.0 — verification coverage + resilience

Roughly doubles the vulnerability classes ptai oracle-verifies, and fixes the bug that made a scan of a live vulnerable app report 0 verified.

Added

  • Ten new verification oracles (each with a control that must fail on a safe target): trusted-header bypass, JWT alg:none, host-header poisoning, XXE, type confusion, stored XSS, sequential IDOR, mass-assignment, non-blind SSRF, SQLi login-bypass. 14 distinct classes now verify.
  • Path-traversal oracle (ptai demo → 4 findings, all 4 oracle-VERIFIED).

Fixed

  • Scope safety: active tools (sqlmap/dalfox) are host-locked to the engagement target; no more attacking third-party URLs scraped from a target's content.
  • Verify resilience: the verify phase waits for a wobbled target to recover instead of failing every oracle against it. Took OWASP Juice Shop from 0 → 12 oracle-verified in a scan.

Validation

  • Juice Shop scoped scan: 12 verified. Multi-vuln honeypot: 23 verified across 14 classes, 100% precision, 0 false positives. Reproducible (honeypot + clean-app gates in the repo).

ptai 1.0.0 — the Trust Spine

Choose a tag to compare

@0xSteph 0xSteph released this 25 Jun 17:30
13738f1

[1.0.0] - 2026-06-25

The Trust Spine release. ptai now proves what it finds: a finding earns the VERIFIED badge only when a machine oracle re-runs the exploit and reproduces it N out of N times. No oracle, no badge, and no verdict from an LLM assertion.

Added

  • Verifier + receipt contract. A verified verdict must name the machine oracle that earned it, enforced in code.
  • Recipe-driven oracles: unescaped reflection, open redirect, IDOR/BOLA differential, error signature, MCP exposure, SQLi boolean and boolean-blind, SSTI (including error-based), sensitive data exposure, and HTTP request smuggling (CL.TE), plus an out-of-band OAST callback oracle for blind SSRF/XXE over a self-hosted loopback collaborator.
  • Portable proof capsules and ptai replay, plus multi-hop chain capsules, so a finding's proof travels and replays without trusting ptai.
  • Prove-or-kill gating: third-party scanner output (nuclei, nikto, zap) is held back until an oracle re-proves it.
  • Verified-only SARIF export (ptai export --sarif) with a frozen, versioned export-properties contract.
  • REST path-parameter injection in the SQLi/XSS/SSTI fuzzers, so injection in /rest/products/<id> style routes is caught, not just query parameters.
  • Experimental CL.TE request-smuggling discovery probe, oracle-gated.
  • CI gate (--fail-on verified) with a composite action, and a bundled ptai demo.

Changed

  • Honest verdicts. An oracle miss now reads as candidate (could not re-prove), never refuted; refuted is reserved for an oracle that can truly disprove a vulnerability.
  • Impact-honest severity: a bare out-of-band callback proves existence, not impact, so it is rated medium until impact is reproduced.

Verified

  • 100% precision with zero false positives on the honeypot benchmark, and field-validated against live OWASP Juice Shop: a real broken-object-level-authorization bug verified end to end with a replayable proof capsule.

v0.17.2

Choose a tag to compare

@0xSteph 0xSteph released this 09 Jun 12:19

Patch release: tool-installer fixes from user feedback on issue #12.

Fixed

  • No more duplicate installs. Tool detection now also checks ~/go/bin, ~/.cargo/bin, and ~/.local/bin, not just bare PATH. Go- and cargo-installed tools were being seen as missing and reinstalled on every run.
  • Cargo and manual install methods now work. feroxbuster, findomain, rustscan (cargo) and evil-winrm (gem) previously fell through to "Unsupported install method" and never installed.

v0.17.1 - close issue #12 silent-exit + hang + zero-findings

Choose a tag to compare

@0xSteph 0xSteph released this 27 May 16:11

Closes issue #12. Three user-facing failure modes fixed across two reporters (poeylizn on DeepSeek-via-OpenAI-compat, as8ASd3 on Ollama with no PENTEST_AI_LLM_PROVIDER).

Headline: ptai start now produces findings on a vulnerable target regardless of which LLM you have, and always exits cleanly.

Live-verified end-to-end with Ollama qwen2.5-coder:7b against the local honeypot: 63 findings (13 critical, 6 high) emerged via the safety-net escalation path, clean exit in 193 s.

What's in 0.17.1 vs 0.17.0: 0.17.1 is the same production code as 0.17.0 plus a CI test fix (0.17.0's new matrix test had two assertion bugs that blocked PyPI publish). The actual user-facing fixes are unchanged.

See CHANGELOG.md for the full surface.

Install / upgrade:

```
pipx upgrade ptai

or

pip install --upgrade ptai
```

If ptai start still surprises you, run ptai doctor for a single-command diagnostic of your provider config.

v0.16.2 — PENTEST_AI_MODEL across all providers + provider docs (issue #12 follow-up)

Choose a tag to compare

@0xSteph 0xSteph released this 26 May 16:09

Issue #12 follow-up. A 0.16.1 user (poeylizn) was pointing ptai at DeepSeek deployed in the cloud and still hit a failure because the OpenAI provider hardcoded gpt-4o regardless of what their endpoint actually served. Two changes plus a new docs page close the loop.

Fixed

  • PENTEST_AI_MODEL env var now honoured across all four providers (engine/llm/factory.py). Previously only the LiteLLM path read it; the openai / anthropic / ollama paths used hardcoded defaults. A user with OPENAI_BASE_URL=https://api.deepseek.com/v1 and a DeepSeek key got a 404 because the factory still asked for gpt-4o. Now setting PENTEST_AI_MODEL=deepseek-chat (or any model your endpoint actually serves) routes through cleanly. Same env var works for pinning Claude / Llama / Qwen variants on the other paths. Closes the silent-mismatch class for the OpenAI-compatible-endpoint case.

Added

  • docs/llm-providers.md — concrete configuration recipes for every supported path: Anthropic, OpenAI, OpenAI-compatible third-parties (DeepSeek cloud, Groq, Together AI, local llama.cpp/vLLM/LM Studio), Ollama, and LiteLLM (Azure, Bedrock, Vertex, OpenRouter). Each recipe is a copy-pasteable env block. Also covers troubleshooting (gpt-4o 404s, Ollama hangs, 401s, LiteLLM ImportErrors) and the --no-llm escape hatch. Linked from the README's Path 3 section.

v0.16.1 — Real multi-provider CLI agent-mode (issue #12)

Choose a tag to compare

@0xSteph 0xSteph released this 26 May 14:53

Patch release closing the silent-exit bug in CLI agent-mode that issue #12 reporters kept hitting. The 0.16.0 release had an earlier attempt at this fix (read the canonical OLLAMA_HOST env var instead of OLLAMA_BASE_URL); that change was correct but solved an adjacent bug, not the one users were experiencing.

Fixed

  • CLI agent-mode now actually honors PENTEST_AI_LLM_PROVIDER (cli/main.py, engine/agents/anthropic_agent.py, engine/llm/factory.py). The previous ptai start flow hardcoded AsyncAnthropic() regardless of which provider the user configured. Users who set OPENAI_API_KEY or OLLAMA_HOST (or both) saw agent_mode: NNN action handlers registered, then a spinner, then a silent exit — because the first Anthropic call failed under the spinner and the agent's exception handler returned a clean finish action. The CLI now constructs the LLM client via engine.llm.factory.create_llm_client(), which routes correctly through every supported provider:

    # OpenAI
    export OPENAI_API_KEY=sk-...
    ptai start http://target
    
    # Local Ollama (configurable via OLLAMA_HOST)
    export PENTEST_AI_LLM_PROVIDER=ollama
    ptai start http://target
    
    # No LLM, deterministic tools only
    ptai start http://target --no-llm

    Three sub-changes:

    • cli/main.py replaces AsyncAnthropic() with create_llm_client().
    • engine/agents/anthropic_agent.py duck-types the client. If it exposes .complete() (the unified LLMClient from the factory), use that; otherwise fall back to the legacy client.messages.create() path so existing test fixtures keep working.
    • engine/llm/factory.py auto-detects provider from whichever API key is set when PENTEST_AI_LLM_PROVIDER isn't explicit. ANTHROPIC_API_KEY set → anthropic, OPENAI_API_KEY set → openai, neither set → openai default. Closes the "I set OPENAI_API_KEY in good faith, why is it routing to Anthropic" foot-gun.

    Verified end-to-end against a live local Ollama running qwen2.5-coder:7b: factory routes to OllamaProvider, the unified LLMClient.complete() round-trips successfully, AnthropicAgent.decide_next_action returns a real Action instead of the silent finish-on-failure. Closes the silent-exit class for the issue #12 reporters.

v0.16.0 — Evidence bundle, real stealth, OOB callbacks, plugin foundation

Choose a tag to compare

@0xSteph 0xSteph released this 26 May 04:59

Headline release closing 3 of 4 deal-breakers from the pentester-perspective audit shipped on 2026-05-24. Every finding now carries cryptographically-hashed proof, blind-vulnerability classes are detectable via OOB callbacks, and intensity=stealth actually changes traffic shape. Plus the foundations for the Caido / Burp / ZAP plugins coming in subsequent releases.

39 commits since 0.15.3. Smoke verified against the TaskFlow honeypot: 8 findings persisted, 8/8 carry evidence_artifacts, 406/406 on-disk artifacts produce valid curl reproducers, SARIF webRequest/webResponse populated, REST /v1/health + /v1/findings return 200, CLI agent-mode parity confirmed.

Added: plugin-client surfaces (REST adapter + auth hardening)

Foundation for the pentest-ai-extensions repo's Caido / Burp / ZAP plugins.

  • MCP auth: per-install token file + Host: allowlist (mcp_server/auth_local.py). ~/.pentest-ai/mcp-token (0600, fresh secrets.token_urlsafe(32) on first start) gates every SSE/HTTP request via Authorization: Bearer …. Host header allowlist (127.0.0.1 / localhost / ::1 plus the configured bind host) closes the CVE-2025-49596-style DNS-rebinding pivot from a same-host browser. Stdio transport stays auth-less (caller already has process access). ptai mcp --no-auth opts out on trusted hosts; --token-file <path> overrides the location.
  • REST adapter at /v1/* (mcp_server/rest.py). Four routes — GET /v1/health, GET /v1/findings, POST /v1/http_request, GET /v1/evidence — delegate to the existing @mcp.tool() functions via FastMCP's custom_route() decorator. Reuses LocalAuthMiddleware for identical Bearer-token + Host-header enforcement. Lets JVM/JS HTTP clients consume ptai over plain REST instead of SSE+JSON-RPC; foundation for the proxy plugins.
  • get_findings gains url= + since= filters (engine/findings_db.py, mcp_server/server.py). url= is a case-insensitive LIKE match against the target column so a proxy plugin can scope the Findings tab to the URL the user is currently inspecting. since=<iso-ts> lets the tab poll incrementally without re-downloading. Both default to None and combine cleanly with existing severity/status filters.
  • health() MCP tool (mcp_server/server.py). Liveness probe for plugin status indicators. Returns {status, version, timestamp, uptime_seconds, active_engagements} with zero side effects; never raises (degrades to active_engagements=0 when the DB is unreachable).

Added: CLI agent-mode parity for evidence + proxy passthrough

  • ptai start agent-mode path (engine/agents/handlers/registry_bridge.py) now populates session._ptai_extras with engagement_id + an EvidenceCollector (rooted at $PENTEST_EVIDENCE_DIR) + an optional proxy from PTAI_UPSTREAM_PROXY, then drains the pending-evidence buffer onto every emitted finding via _attach_pending_evidence_to_findings(session, findings). Closes the carry-forward from Phase 1: every CLI-driven probe now carries evidence_artifacts the same way MCP-driven probes do. Intensity-derived stealth knobs (UA rotation, jitter) on the CLI path deferred — WorkingMemory doesn't carry intensity today; small refactor follows when needed.

Added: out-of-band collaborator (Interactsh / OAST) integration

Phase 4 of the pentester-first roadmap closes audit deal-breaker #2 — blind-vulnerability classes (blind SSRF, blind SQLi, blind XXE, blind stored XSS, SSTI, Log4Shell) are now detectable.

  • engine/oob/ package — async Interactsh client. Generates RSA-2048 keypair per engagement, POST /register to the configured server, polls GET /poll, decrypts each interaction RSA-OAEP-SHA256-wrapped AES-CTR-256 (IV = first 16 bytes). Wire format verified against github.com/projectdiscovery/interactsh. Defaults to https://oast.fun, accepts any self-hosted server via --oast-server.
  • pending_oob table (engine/findings_db.py) — parks finding templates at probe-fire time; persists across MCP restarts so late-arriving callbacks don't lose their trail. Indexed by engagement, status, and payload subdomain for O(1) interaction → probe lookup.
  • Curated payload library (engine/oob/payloads.py) — per-vuln-class payload templates with {OAST} placeholders: blind SSRF (HTTP/gopher/dict/ldap), blind SQLi per DBMS (MySQL/Postgres/MSSQL/Oracle), blind XXE (two-stage DTD + SVG), blind RCE (curl/wget/nslookup/dig + Windows variants + base64-wrapped WAF bypass), blind stored XSS (5 shapes), SSTI (Jinja2/Twig/Freemarker), Log4Shell (jndi:ldap/dns/rmi + ${lower:} bypass).
  • poll_oob MCP tool — the LLM driving ptai over MCP calls this after firing OOB-enabled probes. Polls the collaborator up to timeout=60 seconds (capped at 300, configurable), matches arriving interactions back to their pending rows via find_pending_oob_by_full_id, materializes the finding with the interaction record (timestamp, source IP, raw bytes) as an on-disk evidence artifact via the Phase-1 collector, flips the pending row to matched. Stale rows past their expires_at get bulk-moved to expired.
  • Probe wirings — SSRF (web.ssrf_cloud_metadata), blind SQLi (web.sqli_fuzz), XXE (web.xxe_upload), stored XSS (web.stored_xss) all fire OAST payloads when engagement_id is present in session extras. Bounded fan-out: one OOB-fire per discovered endpoint/path so request volume stays sane. Blind-RCE wiring deferred — no general command-injection probe exists today; the payloads ship for when one lands.
  • CLI flagsptai start --oast-server URL / --oast-token T / --no-oast. Sets PTAI_OAST_SERVER / PTAI_OAST_TOKEN / PTAI_NO_OAST=1 for the engagement.
  • Privacy disclosure in README under Responsible Use — encrypted-payload + server-side-metadata model, when to self-host (paid engagements / programs forbidding third-party collaborator infra), how to disable entirely.

End-to-end verified by a test (tests/test_oob_end_to_end.py) that stands up a mock Interactsh server on a loopback port, exercises register_oob_probe → mock-/register → pending_oob → mock-/poll-with-real-encryption → poll_oob → materialized finding with on-disk OOB evidence artifact carrying the interaction's source IP. 346 / 346 across the full Phase 1 + Phase 2 + Phase 4 regression sweep.

Added: real intensity=stealth implementation

The intensity=stealth knob had been advertised on the engagement schema since 0.15.0 but only changed rate-limit behavior. Now it actually changes traffic shape — closing the credibility-gap deal-breaker from the pentester audit.

  • Curated UA pool with per-call rotation (engine/probes/primitives.py). 7 modern UAs (Chrome / Firefox / Safari across Windows / Mac / Linux / Android / iOS) selected at random per HTTP call when stealth is on. WAF / scanner fingerprinters can't pin a single engine.
  • Per-request jitter (engine/probes/primitives.py). asyncio.sleep of random.uniform(min_ms, max_ms) / 1000 before every outbound call when stealth is on (default window 250–1500 ms). Concurrent probe waves stop bursting the target.
  • Upstream proxy passthrough (engine/probes/primitives.py + mcp_server/probes.py). Every HTTP call honors extras["proxy"], populated from the PTAI_UPSTREAM_PROXY env var or the new --upstream-proxy CLI flag. Lets pentesters route ptai through Burp / Caido for live inspection at any intensity — ptai start http://target --upstream-proxy http://127.0.0.1:8080.
  • Stealth knobs wired into MCP run_probe (mcp_server/probes.py). When the engagement intensity is stealth, run_probe populates session extras with ua_rotation=True, jitter_ms=(250, 1500). http_request always honors PTAI_UPSTREAM_PROXY.

Active on the MCP path today. CLI agent-mode + legacy orchestrator wiring still tracked separately.

Added: per-finding evidence bundle

The headline change. Every finding now ships with cryptographically-hashed proof of the HTTP exchange that produced it — closing the "AI slop" reproducibility deal-breaker that's caused multiple bug-bounty programs to deprioritize LLM-driven reports through 2025-2026.

  • evidence_artifacts column on findings table (engine/findings_db.py). JSON list of {artifact_id, type, sha256, filename, method, url, status_code} summaries pointing at the on-disk exchanges. Migration via the existing _add_column_if_missing() pattern with a '[]' default so old rows read as an empty list.
  • HTTP capture in the primitives chokepoint (engine/probes/primitives.py). Every http_get / http_post_json / http_post_form / http_put_json call through the primitives layer now persists its request + response to disk and appends a summary to a per-session pending buffer when an EvidenceCollector is attached via _ptai_extras. Existing probes get evidence for free with zero probe-side changes.
  • MCP run_probe collector wiring + orchestrator auto-attach (mcp_server/probes.py). Constructs an EvidenceCollector rooted at $PENTEST_EVIDENCE_DIR/<engagement_id>/, attaches it plus engagement_id to the probe's session, then after the probe returns its findings drains the pending buffer and auto-attaches the artifact summaries to every emitted finding that didn't set its own evidence_artifacts.
  • MCP http_request capture parity (mcp_server/probes.py). The LLM's raw-HTTP escape hatch now captures its exchange and returns the artifact summary in the response dict under evidence_artifacts.
  • SARIF v2.1.0 DAST webRequest / webResponse (engine/sarif.py). When a finding carries an http_request artifact, the SARIF result populates the DAST extension fields. GitHub Code Scanning's Security tab now renders the captured exchange inline with each finding.
  • request_to_curl() utility (engine/evidence.py). POSIX-shell-safe curl one-liner builder; used by the report rendere...
Read more

v0.15.3 — default cap $2 → $10 + docs

Choose a tag to compare

@0xSteph 0xSteph released this 20 May 12:22

Default-budget bump and documentation. The standalone agent loop's spending cap defaults to $10 USD (up from $2), which fits a normal Sonnet 4.6 web-app engagement end to end without the cap firing mid-recon.

Changed

  • DEFAULT_PRICE_LIMIT_USD raised from $2 to $10 (engine/llm/cost.py). The previous $2 default was pre-launch hardening but aborted real Sonnet engagements mid-engagement; $10 finishes most Sonnet sweeps while still catching runaway agent loops. Override via PTAI_PRICE_LIMIT=N; =0 is still unlimited; resume after a cap-abort with ptai resume <engagement_id> (engagement state is preserved at aborted_cost_limit status). Applies only to the standalone agent loop (ptai start) — MCP-driven engagements continue to use the AI client's own billing.

Docs

  • New "Spending cap (Path 3 only)" section in README under the standalone-CLI install path. Documents the env-var override, the unlimited escape hatch, and the resume-after-cap workflow. Explicitly clarifies that Paths 1 and 2 (MCP-driven via Claude Code, Cursor, etc.) don't use this cap.

Install

pip install ptai==0.15.3

PyPI: https://pypi.org/project/ptai/0.15.3/

Full notes in CHANGELOG.md.

v0.15.2 — MCP auth-cache + bearer CLI + reap

Choose a tag to compare

@0xSteph 0xSteph released this 20 May 05:47

Pre-launch patch + small feature batch. The headline is the MCP-side auth-cache fix that makes http_request and run_probe inherit the engagement's authentication automatically, so an LLM driving ptai over MCP doesn't have to re-thread auth_profile on every tool call.

Fixed

  • MCP http_request / run_probe inherit engagement auth automatically (mcp_server/auth_cache.py, mcp_server/server.py). A process-local cache keyed by engagement_id stores the auth captured during start_engagement; subsequent tool calls without an explicit auth_profile argument resolve from the cache. Validated end-to-end against Juice Shop v19.2.1: start_engagement(auth_profile="juice_shop") populates the cache, then http_request(engagement_id=..., url="/rest/basket/1") returns the admin basket with no further auth wiring.

Added

  • ptai start --auth-profile <name> accepts bearer-flow profiles (cli/main.py). Previously the CLI rejected anything other than form_post; now bearer_static and bearer_dynamic both work and propagate the token through every specialist agent.
  • ptai reap --older-than <duration> [--force] [--dry-run] (cli/main.py, engine/findings_db.py:FindingsDB.reap_engagements). Bulk-delete old engagements with cascade safety — skips engagements that still have findings unless --force is passed.

Internal

  • Post-publish PyPI smoke test (.github/workflows/release.yml). New smoke job waits for the wheel to propagate, installs it in a fresh venv, verifies ptai --version matches the tag, imports key modules, and runs an MCP initialize handshake. Catches missing-file wheels and broken entry points before users hit them.
  • Test hygiene: silenced async-mock warnings in file-upload tests, registered the pytest-timeout marker.
  • Reverted a brief regression in the scorer's title-match branch (73e0bd4 reverts 25d041c) that dropped the Juice Shop catch rate ~73% → ~32% on realistic data. Unit tests passed; the realistic benchmark caught it. Adding a corpus-benchmark CI gate is on deck.

Install

pip install ptai==0.15.2

PyPI: https://pypi.org/project/ptai/0.15.2/

Full notes in CHANGELOG.md.

v0.15.1 — MCP bearer-flow auth fix

Choose a tag to compare

@0xSteph 0xSteph released this 20 May 05:47

Patch follow-up to 0.15.0 closing a silent auth failure on the headline flow: bearer profiles.

Fixed

  • MCP bearer-flow auth profile resolution (mcp_server/auth.py). Profiles declaring flow: bearer with password_source: env silently fell back to form-post: resolve_auth_profile_to_dict ignored prof.flow and returned a form_post dict for any password-source profile. Downstream POSTed form-encoded credentials to JSON-only endpoints like Juice Shop /rest/user/login and the login silently failed with auth_profile '<name>' could not be resolved or login failed. Adds a bearer_dynamic branch on both sides of the resolver. Two integration tests cover the dict shape and the async POST→JWT path. Validated end-to-end against Juice Shop v19.2.1 (8 phases completed, 100 findings, same 43/68 catch set as the 0.15.0 deterministic baseline).

Install

pip install ptai==0.15.1

PyPI: https://pypi.org/project/ptai/0.15.1/

Full notes in CHANGELOG.md.