Adversarial QA agent for web apps — powered by LLMs and browser automation.
qabot is a CLI tool that uses an AI agent to automatically test any web application. It navigates your app through a real browser, runs test cases you define in plain English, then goes adversarial — trying XSS, auth bypass, broken flows, and edge cases to find bugs before your users do.
It works with any web app — just point it at a URL.
qabot runs three sequential testing phases, each powered by an independent LLM-driven browser agent:
┌─────────────────────────────────────────────────────────────────────┐
│ qabot test <URL> │
└──────────────────────────────┬──────────────────────────────────────┘
│
▼
┌──────────────────────────┐
│ Phase 1: Discovery │
│ │
│ • Navigates all links │
│ • Maps routes & forms │
│ • Identifies auth pages │
│ • Caches results (TTL) │
│ │
│ Output: SiteMap │
└────────────┬─────────────┘
│
▼
┌───────────────────────────┐
│ Phase 2: Directed Tests │
│ │
│ • Runs user-defined tests│
│ • Fresh browser per test │
│ • Step-by-step execution │
│ • PASS/FAIL verdicts │
│ │
│ Output: TestResult[] │
└────────────┬──────────────┘
│
▼
┌────────────────────────────┐
│ Phase 3: Adversarial │
│ │
│ • XSS & injection attacks │
│ • Boundary testing │
│ • Auth bypass attempts │
│ • Flow breaking │
│ │
│ Output: Finding[] │
└────────────┬───────────────┘
│
▼
┌────────────────────────────┐
│ Report Generation │
│ │
│ • Collects all results │
│ • Renders HTML report │
│ • Opens in browser │
│ │
│ Output: qabot-report/ │
└────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ CLI (cli.py) │
│ click commands: test, explore │
└──────────────────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ Orchestrator (orchestrator.py) │
│ LLM selection · browser profile · phase wiring │
└───────┬──────────────────────┬───────────────────────┬──────────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────────┐ ┌───────────────────────┐
│ Discovery │ │ Directed │ │ Adversarial │
│ discovery.py │ │ directed.py │ │ adversarial.py │
│ │ │ │ │ │
│ Prompt: │ │ Prompt: │ │ Prompt: │
│ discovery.md │ │ directed.md │ │ adversarial.md │
│ │ │ │ │ │
│ → SiteMap │ │ → TestResult[] │ │ → Finding[] │
└───────┬───────┘ └─────────┬─────────┘ └───────────┬───────────┘
│ │ │
└────────────────────┼────────────────────────┘
│
▼
┌──────────────────────────────┐
│ ResultCollector │
│ collector.py │
│ │
│ Gathers all phase outputs │
│ → FullReport │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ Report Generator │
│ generator.py │
│ │
│ Jinja2 → template.html │
│ → qabot-report/index.html │
└──────────────────────────────┘
Shared data models (models.py) are the only interface between components:
| Model | Purpose |
|---|---|
SiteMap |
Discovered routes, forms, auth-gated pages |
TestResult |
Verdict (PASS/FAIL), steps taken, severity, reproduction steps |
AdversarialFinding |
Bug title, severity, description, reproduction steps |
FullReport |
Combines all results; computes pass/fail counts and severity checks |
Prerequisites: Python 3.11+, uv
pip install git+https://github.com/ShivaniNR/qabot.gitThen use it from anywhere:
qabot explore http://your-app.com --describe "My app"
qabot test http://your-app.com --config qabot.yaml --adversarialgit clone https://github.com/ShivaniNR/qabot.git
cd qabot
uv syncThen prefix commands with uv run:
uv run qabot explore http://your-app.com --describe "My app"Create a .env file (see .env.example):
ANTHROPIC_API_KEY=your-key-here
At least one LLM API key is required. Supported providers:
| Provider | Env Variable | Default Model |
|---|---|---|
| Anthropic | ANTHROPIC_API_KEY |
Claude Sonnet 4 |
| OpenAI | OPENAI_API_KEY |
GPT-4.1 Mini |
GOOGLE_API_KEY |
Gemini 2.0 Flash |
uv run qabot explore http://your-app.com --describe "E-commerce app with products, cart, and checkout"uv run qabot test http://your-app.com --config qabot.yaml --adversarialRun directed tests and optionally adversarial testing against a target URL.
| Flag | Type | Default | Description |
|---|---|---|---|
--config, -c |
PATH | — | Path to qabot.yaml config file |
--llm |
STRING | auto-detect | LLM model name (e.g., claude-sonnet-4-20250514, gpt-4.1-mini, gemini-2.0-flash) |
--adversarial / --no-adversarial |
FLAG | --no-adversarial |
Enable adversarial testing after directed tests |
--max-steps |
INT | 50 | Maximum browser steps for adversarial mode |
--rediscover |
FLAG | off | Force re-run discovery, ignore cache |
--ci |
FLAG | off | CI mode: no browser popup, text summary to stdout |
--fail-on |
CHOICE | critical |
Exit code 1 if bugs at or above this severity (critical, major, minor) |
--json-output |
FLAG | off | Output full results as JSON to stdout |
Run adversarial-only exploration (no config file needed).
| Flag | Type | Default | Description |
|---|---|---|---|
--llm |
STRING | auto-detect | LLM model name |
--describe, -d |
STRING | — | Brief description of the app (helps the agent understand context) |
--max-steps |
INT | 50 | Maximum browser steps |
--focus, -f |
STRING (multiple) | — | Areas to prioritize (e.g., --focus auth --focus forms --focus checkout) |
--auth |
KEY=VALUE (multiple) | — | Login credentials (e.g., --auth email=test@test.com --auth password=secret) |
--login-url |
STRING | /login |
Login page path |
--rediscover |
FLAG | off | Force re-run discovery |
--ci |
FLAG | off | CI mode |
--fail-on |
CHOICE | critical |
Severity threshold for exit code |
--json-output |
FLAG | off | JSON output |
Create a qabot.yaml to define test cases and settings:
url: http://127.0.0.1:8000
description: "E-commerce app with products, cart, checkout, signup, and login"
# Authentication (optional)
auth:
login_url: /login
credentials:
email: test@example.com
password: secret123
# Directed test cases — plain English instructions
tests:
- name: "Signup with valid data"
flow: "Go to /signup, enter name 'Alice', email 'alice@test.com', password 'pass123', click Sign Up, verify welcome message"
- name: "Empty signup submission"
flow: "Go to /signup, click Sign Up without filling any fields, verify that validation errors appear"
- name: "Add product to cart"
flow: "Go to /products, add Wireless Headphones with quantity 1, verify cart shows the item with correct price $49.99"
- name: "Checkout with empty cart"
flow: "Go to /checkout directly without adding items, verify it shows an error or redirects to cart"
# Adversarial settings
adversarial:
enabled: true
focus: ["forms", "cart", "checkout"]
max_steps: 20
# Discovery cache settings
discovery:
cache: true
max_age: "1h" # supports: s, m, h, dqabot categorizes all findings by severity:
| Severity | Examples |
|---|---|
| Critical | XSS vulnerabilities, SQL injection, auth bypass, 500 errors, data corruption, negative prices |
| Major | Features broken under edge cases, missing validation causing wrong behavior, broken error handling |
| Minor | Cosmetic issues, missing validation messages, poor UX for unusual inputs |
The --fail-on flag controls which severity triggers a non-zero exit code (useful for CI gates).
uv run qabot test http://staging.your-app.com \
--config qabot.yaml \
--adversarial \
--ci \
--fail-on major--ci— suppresses browser popup, prints text summary to stdout--fail-on major— exits with code 1 if any major or critical bugs found--json-output— machine-readable JSON output for pipeline parsing
qabot generates a self-contained HTML report at qabot-report/index.html with:
- Pass/fail summary for directed tests
- Adversarial findings with severity and reproduction steps
- Discovered sitemap (routes, forms, auth-gated pages)
The report is a single HTML file with inline CSS — no external assets, works offline, shareable as one file.
A sample app with 5 intentional bugs is included for testing:
| Bug | Location |
|---|---|
| XSS vulnerability | Signup name field renders unsanitized HTML |
| No form validation | Signup accepts completely empty fields |
| Negative quantities | Cart allows negative item quantities |
| Empty cart checkout | Can place an order with nothing in cart |
| No 404 page | Unknown routes return a blank 200 |
# Terminal 1 — run the buggy sample app
uv run uvicorn tests.sample_app:app --port 8000
# Terminal 2 — unleash qabot on it
uv run qabot test http://127.0.0.1:8000 --config tests/qabot.yaml --adversarialDuring adversarial testing, qabot attempts:
- Input attacks — empty fields, long strings, XSS payloads (
<script>alert('xss')</script>), SQL injection ('; DROP TABLE users; --), negative numbers, unicode/emoji - Flow breaking — skipping checkout steps, accessing auth pages without login, double-submitting forms, navigating away mid-flow
- Boundary testing — max/min values, non-existent pages (404 handling), URL parameter manipulation
- Auth & authorization — accessing other users' data via ID manipulation, admin pages without auth, default credentials
| Component | Technology |
|---|---|
| Runtime | Python 3.11+ |
| Browser automation | browser-use (Playwright + LLM) |
| CLI | Click |
| Config & models | Pydantic + PyYAML |
| Report rendering | Jinja2 |
| LLM providers | Anthropic (Claude), OpenAI (GPT-4), Google (Gemini) |
| Package manager | uv |
MIT