Skip to content

Pentest a web application

cxg pentest runs an AI-driven whitebox pipeline: it reads guardlink’s threat hypotheses about a codebase, drives an authenticated browser against the running app, and writes confirmed, refuted, and ambiguous findings to a report. This guide sets up the two inputs it needs, a findings.sarif and a live session, and launches it against a target you host yourself.

flowchart TB
    GL["guardlink findings.sarif,<br/>inline annotations, endpoints"] --> HY["hypotheses"]
    CB["your source code"] --> SEL
    AP["captured auth profiles"] --> PF["pre-flight:<br/>which identity is this cookie?"]
    PF --> SEL["pick a probe per<br/>vulnerability class"]
    HY --> SEL
    SEL --> RUN["run probes in parallel<br/>authenticated browser contexts"]
    RUN --> TR["triage every result"]
    TR --> REP["report.json"]
    TR --> LOG["audit.jsonl"]

    class REP,LOG emphasis
  • cxg installed. See Installation.
  • The Python orchestrator installed: cxg pentest install. Run it once; it copies the pipeline into ~/.cert-x-gen/pentest/ and checks its Python deps.
  • An AI CLI on your PATH: claude, codex, or gemini. The pipeline asks it to write probe templates. Without one, generation produces nothing.
  • A guardlink findings.sarif for the codebase. See feed guardlink findings into cxg. cxg reads it from <codebase>/whitebox/findings.sarif.

Any local app with a session works. The pipeline needs the app running (to probe) and its source on disk (for guardlink and for the AI to read). Point --target at the URL and --codebase at the source tree.

  1. Confirm the app answers, and that it gates something behind a session.

    Terminal window
    curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8002/

    A 302 to a login page unauthenticated, 200 with a cookie, is the shape the pre-flight check expects.

  2. Confirm the guardlink hypotheses are in place.

    Terminal window
    ls ./app/whitebox/findings.sarif

The pipeline acts as a logged-in user, so it needs a captured session. For CI and for a reproducible run, import a Playwright storage_state rather than capturing interactively. Keep it out of your home store by writing to a dedicated, 0700 directory.

  1. Import the session as a named profile.

    Terminal window
    mkdir -p ci-auth && chmod 700 ci-auth
    cxg pentest auth import --profile live --target http://127.0.0.1:8002 \
    --storage-state ./session.json --auth-dir ./ci-auth
    ✓ imported 1 cookies across 1 domains, 0 localStorage items, 0 sessionStorage items
    [auth] imported live (storage_state, 1 cookies)

    The profile files are written 0600. A later run that loads this profile cannot tell an imported session from an interactively captured one.

  2. Verify the session is alive before spending a whole run on it.

    Terminal window
    cxg pentest auth verify --profile live --auth-dir ./ci-auth
    ✓ session for 'live' is ALIVE against http://127.0.0.1:8002
    · landed at http://127.0.0.1:8002/ (no login redirect)

    A dead session prints ✗ session … expired and exits non-zero, which is your cue to re-capture.

Point it at the running app and its source, and give it a goal.

Terminal window
cxg pentest run \
--codebase ./app \
--target http://127.0.0.1:8002 \
--auth live --auth-dir ./ci-auth \
--ai --ai-provider claude \
--goal "verify the IDOR exposure on the profile endpoint" \
--attestation "Local test against a self-hosted demo, 2026-08-13"

The pre-flight is deterministic and worth reading in full:

[1] guardlink: 1 SARIF hypotheses, 0 inline, 0 endpoints
[scope] audit log → ~/.cert-x-gen/sessions/pentest-20260813-141227/audit.jsonl
[1b] inspecting 1 captured auth profiles…
· live role=user tier=20 email=demo@example.com id=1
· landed at http://127.0.0.1:8002/ (no login redirect)
[2] generating JS templates via claude (reads codebase)…

Step [1] is guardlink ingestion, with one SARIF hypothesis loaded from app/whitebox/findings.sarif. Step [1b] is the session pre-flight: it resolved the identity behind the cookie before spending any AI budget. Step [2] hands the ranked hypotheses to your AI CLI, which reads the codebase and writes probe templates.

flowchart TB
    H["a hypothesis, such as<br/>idor on /api/profile"] --> Q{"built-in probe<br/>for this class?"}
    Q -->|yes| R["run against the app<br/>as a logged-in user"]
    Q -->|"no, with --ai"| AI["AI writes one from your source,<br/>then it must compile and validate"]
    Q -->|"no, without --ai"| SK["skipped"]
    AI -->|passes| R
    AI -->|fails| SK

    class R emphasis

In a pipeline the exit code is the machine-readable verdict:

Exit Meaning
0 No confirmed findings, a clean run.
1 No templates were available to run (guardlink output missing or empty, or the ranker selected none).
2 Confirmed findings present.
3 The scan was hard-killed by a 5xx streak or a scope violation.
5 CI mode: an auth session was dead at pre-flight, so the run stopped before spending AI budget.

Exit 5 is what makes this safe to automate. See run cxg in CI for the non-interactive setup that relies on it.

Artifacts land in the session directory named in the [scope] line: report.json (the split of confirmed / mitigation-verified / ambiguous) and audit.jsonl (every HTTP request the run made). The audit log is the dispute-ready record of what was sent, which is why --attestation is stamped into its header.

flowchart TB
    R["a probe runs"] --> T{"deterministic triage"}
    T -->|confirmed| OK["confirmed_findings"]
    T -->|refuted| MV["mitigation_verifications"]
    T -->|"ambiguous,<br/>environment-bound"| AMB["ambiguous"]
    T -->|"ambiguous,<br/>payload-bound"| M["AI rewrites the probe<br/>to address the failure"]
    M -->|"up to 3 times"| R

    class OK,MV,AMB emphasis
  • Two identities. Cross-user IDOR and session-replay probes need at least two profiles. Import a second and pass --auth victim,attacker.
  • Confirm blind vulnerabilities out of band. For SSRF and blind injection, add OAST, and read detect blind vulnerabilities with OAST first, because the two OAST flags mean very different things.