Metadata-Version: 2.4
Name: polyglot-bug-hunter-x
Version: 1.0.1
Summary: Multimodal AI agent that sees, hears and reads a website while hunting bugs
Author: PolyglotBugHunter-X contributors
License-Expression: MIT
Project-URL: Homepage, https://huggingface.co/spaces/Kicaulah/polyglot-bughunter-x-static
Project-URL: Model, https://huggingface.co/Kicaulah/polyglot-bughunter-x
Project-URL: Dataset, https://huggingface.co/datasets/Kicaulah/polyglot-bug-patterns
Project-URL: Repository, https://github.com/skamy64-ux/polyglot-bughunter-x
Keywords: security,web-security,bug-bounty,multimodal,agent,xss,sqli,csrf,idor,ssrf,cvss,prompt-injection,i18n
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Classifier: Natural Language :: English
Classifier: Natural Language :: Chinese (Simplified)
Classifier: Natural Language :: Japanese
Classifier: Natural Language :: Korean
Classifier: Natural Language :: Arabic
Classifier: Natural Language :: Russian
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: space
Requires-Dist: gradio<7,>=4.44; extra == "space"
Provides-Extra: duckdb
Requires-Dist: duckdb>=1.0; extra == "duckdb"
Provides-Extra: http
Requires-Dist: requests>=2.32; extra == "http"
Provides-Extra: image
Requires-Dist: pillow>=10.0; extra == "image"
Provides-Extra: audio
Requires-Dist: faster-whisper>=1.0; extra == "audio"
Provides-Extra: browser
Requires-Dist: playwright>=1.45; extra == "browser"
Provides-Extra: agent
Requires-Dist: langchain>=0.3; extra == "agent"
Requires-Dist: llama-index>=0.12; extra == "agent"
Provides-Extra: all
Requires-Dist: duckdb>=1.0; extra == "all"
Requires-Dist: requests>=2.32; extra == "all"
Requires-Dist: pillow>=10.0; extra == "all"
Requires-Dist: playwright>=1.45; extra == "all"
Requires-Dist: faster-whisper>=1.0; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Dynamic: license-file

<div align="center">

![demo](assets/demo.gif)

# 🕷️🔥 PolyglotBugHunter-X


### Bro, imagine an AI that can **see**, **hear**, and **read** a website while hunting bugs at the same time. Yeah, that's PolyglotBugHunter-X.

[![HF Space](https://img.shields.io/badge/🤗%20Space-try%20it-red)](https://huggingface.co/spaces/Kicaulah/polyglot-bughunter-x-static)
[![HF Model](https://img.shields.io/badge/🧠%20Model-yellow)](https://huggingface.co/Kicaulah/polyglot-bughunter-x)
[![HF Dataset](https://img.shields.io/badge/🗃️%20Dataset-blue)](https://huggingface.co/datasets/Kicaulah/polyglot-bug-patterns)
[![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github)](https://github.com/skamy64-ux/polyglot-bughunter-x)
[![License MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue)](https://www.python.org/downloads/)

</div>

---

## 🚀 Fire this up in one click

<div align="center">

### **[🎨 Try it live on the Space](https://huggingface.co/spaces/Kicaulah/polyglot-bughunter-x-static)**
#### no install · no account · no API key · works on a free CPU tier

</div>

Press **🚀 Scan Example** and it boots a deliberately vulnerable app on
`127.0.0.1`, attacks it across all three modalities, and renders a full report in
about five seconds: SQLi, IDOR, SSTI, XSS, SSRF, open redirect, weak cookies,
exposed `.env`, audio polyglots and a visual prompt-injection canary — each with a
**CVSS v3.1** score and evidence you can re-check.

Nothing leaves the container. Nobody gets an angry email. It always works.

To scan a target you actually own, use the CLI — a browser page cannot make
server requests, so real targets need a real process:

```bash
git clone https://github.com/skamy64-ux/polyglot-bughunter-x && cd polyglot-bughunter-x
./run.py scan https://staging.yourcompany.com --i-own-this
```

---

## 🤔 What is this?

A **multimodal agent for automated web security testing** — for systems you own or
have written permission to test.

| Modality | What it does |
|---|---|
| ⌨️ **Text** | Reflected XSS, SQLi (boolean/error/UNION differentials), command injection, SSTI, path traversal, SSRF, open redirect, NoSQL/LDAP, prompt injection, IDOR, CSRF, race conditions, DOM-XSS sinks |
| 🖼️ **Image** | Screenshot diffing with hotspot boxes, EXIF/GPS privacy audit, alt-text prompt injection, generated near-invisible-text canary |
| 🎙️ **Audio** | RIFF/HTML/ZIP polyglots, silence + decode-bomb checks, spectrogram injection, tone-encoded instructions, STT-pipeline prompt injection |
| 👁️ **Passive** | CSP/HSTS/nosniff/XFO, cookie flags, CORS reflection with credentials, mixed content, TLS, exposed `.env`/`.git`/`wp-config`/`/actuator/env`, fingerprinting, debug leaks |

**43 payloads across 8 bug classes — 24 of them polyglot.** A polyglot payload is
a single string that is simultaneously plausible HTML, JS, SQL *and* shell, so it
escapes whatever context your app dropped it into. That's the house style.

### Why not just call a transformer?

Because a scanner that hallucinates a finding is worse than no scanner.

Every verdict here comes from a **reproducible comparison** — a captured baseline
response versus an injected one — and every finding ships the exact request,
response snippet and signal that produced it. `{{7*7}}` coming back as `49` is
proof. "This looks exploitable" is labelled `needs-manual-confirm` and says so.

Scoring is real **CVSS v3.1** base vectors, cross-checked against RedHat's `cvss`
library over **5000 random vectors: 0 mismatches**.

## 🌍 Twelve languages

🇬🇧 EN · 🇨🇳 中文 · 🇯🇵 日本語 · 🇰🇷 한국어 · 🇮🇩 ID · 🇪🇸 ES · 🇸🇦 AR · 🇷🇺 RU ·
🇩🇪 DE · 🇫🇷 FR · 🇵🇹 PT · 🇮🇳 HI

160 keys each, 100% coverage, no blanks. Auto-detected from `navigator.language`;
a dropdown re-renders the entire UI *and* the report. Arabic gets proper RTL.

<details>
<summary>Translations live in <code>/locales/</code> — English is the master, the rest are translations</summary>

```
locales/
├── en.json   160 keys  ← MASTER
├── zh.json   160 keys  中文
├── ja.json   160 keys  日本語
├── ko.json   160 keys  한국어
├── id.json   160 keys  Bahasa Indonesia
├── es.json   160 keys  Español
├── ar.json   160 keys  العربية  (RTL)
├── ru.json   160 keys  Русский
├── de.json   160 keys  Deutsch
├── fr.json   160 keys  Français
├── pt.json   160 keys  Português
└── hi.json   160 keys  हिन्दी
```

Missing keys fall back to English rather than showing raw `snake_case` to a user.
</details>

## ⚡ Quick start

### Install

```bash
# from a release tag. works today, needs no account and no token.
pip install "polyglot-bug-hunter-x @ git+https://github.com/skamy64-ux/polyglot-bughunter-x@v1.0.1"

# from a clone
git clone https://github.com/skamy64-ux/polyglot-bughunter-x
cd polyglot-bughunter-x
pip install .
```

`pip install polyglot-bug-hunter-x` from PyPI is not available yet. The
publishing workflow is in place and every gate passes; the remaining step is a
one-time browser confirmation on pypi.org, which needs an account.

The tag is a tag on purpose. Installing from a branch means a notebook or a
script you published last month can start running different code without any
diff to review.

The scanning core is **stdlib-only**. `pip install` pulls in nothing. Every real
dependency is an extra, and all of them degrade gracefully:

```bash
pip install "polyglot-bug-hunter-x[http]"      # requests, nicer TLS handling
pip install "polyglot-bug-hunter-x[image]"     # pillow, EXIF and pixel analysis
pip install "polyglot-bug-hunter-x[browser]"   # playwright, screenshot proof
pip install "polyglot-bug-hunter-x[duckdb]"    # Parquet round-trip
pip install "polyglot-bug-hunter-x[all]"
pip install "polyglot-bug-hunter-x[space]"     # gradio, only for the 4-tab UI
```

### Nothing installed at all

```bash
git clone https://github.com/skamy64-ux/polyglot-bughunter-x
cd polyglot-bughunter-x
./run.py                # menu
./run.py demo           # boots its own vulnerable target on localhost, scans it
```

No venv, no `pip install`, no dependencies. `run.py` puts `src/` on the path
itself. Pick **4** from the menu to get the same UI as the live Static Space on
`http://127.0.0.1:8000`.

### As a command

```bash
pip install -e .        # installs the `pbhx` command, still zero dependencies

pbhx demo                          # offline scan + md/html/json/sarif report
pbhx scan https://staging.example --i-own-this --active
pbhx serve                         # the 4-tab Gradio UI on 127.0.0.1:7860
pbhx canary -o canary.png          # visual prompt-injection canary
pbhx audio -o suite/               # 8 adversarial audio files
pbhx payload "{{7*7}}"             # explain a payload against every detector
pbhx targets                       # hosts that exist to be scanned
pbhx capabilities                  # which optional extras are installed
```

Exit codes are meant for CI: `0` clean, `1` findings at or above `--fail-on`,
`2` refused or bad usage.

```bash
pbhx demo --fail-on high          # non-zero if the demo finds anything high+
pbhx scan "$URL" --i-own-this --json | jq '.findings[] | select(.severity=="critical")'
```

`--json` puts only JSON on stdout, so it pipes cleanly. The human report goes
to stderr alongside it.

### As a library

```python
from polyglot_bug_hunter import Hunter, ScanPolicy

# 1. the demo. boots its own vulnerable target on localhost. no auth needed.
report = Hunter.demo()
print(report.risk_score, report.counts())

# 2. your own infrastructure. the authorization tick is mandatory.
policy = ScanPolicy(
    authorization_confirmed=True,
    authorization_note="staging.mycompany.com, ticket SEC-1234",
    active_probing=True,
)
report = Hunter(policy, modes=["text", "image", "audio"], lang="auto").scan(
    "https://staging.mycompany.com"
)

for f in report.sorted_findings()[:5]:
    print(f.severity.value, f.cvss.score, f.title)
```

Run it locally:

```bash
python -m polyglot_bug_hunter.demo_target 8787   # poke the vulnerable app yourself
python tools/build_dataset.py                    # regenerate the HF dataset
python tools/build_space.py                      # assemble the Space folder
python -m pytest -q                              # the test suite
```

### Why the CLI exists

The Static Space on Hugging Face is a browser page, so it cannot scan a real
target: there is no server to make requests from. The demo, payload lab, canary
and audio suite all work there because they run entirely in the tab. Anything
that touches a real host needs the CLI, which is why it is the primary
interface rather than a wrapper around the Space.

## 🗂️ Project layout

```
polyglot-bughunter-x/
├── src/polyglot_bug_hunter/
│   ├── cli.py            the `pbhx` command: demo, scan, serve, payload, canary, audio
│   ├── safety.py         5 gates: auth, network range, scheme, payload, rate limit
│   ├── hunter.py         the orchestrator. one class, .scan(url) -> report
│   ├── demo_target.py    an intentionally vulnerable app we host ourselves
│   ├── models.py         Finding / Evidence / Asset / ScanReport + CVSS v3.1 math
│   ├── payloads.py       43 payloads, param-name routing, safety-classified
│   ├── net.py            stdlib HTTP client + `replace_param` (see below)
│   ├── htmlx.py          HTML parsing, link/param ranking, DOM-XSS sink audit
│   ├── config.py         ScanConfig / ModalityConfig
│   ├── i18n.py           12 locales, auto-detect, RTL
│   ├── scanner/
│   │   ├── text.py       differential injection analysis
│   │   ├── passive.py    headers, cookies, CORS, TLS, exposure
│   │   ├── image.py      PNG encode/decode, pixel diff, visual canary
│   │   ├── audio.py      wav synthesis, spectrogram attack, container audit
│   │   ├── access.py     IDOR walk
│   │   └── race.py       TOCTOU probes + CSRF assessment
│   ├── report/render.py  markdown, self-contained HTML, JSON, SARIF
│   └── storage/db.py     DuckDB or sqlite3, same API
├── hf_space/         → upload as a Space (4 tabs, 12 languages, CPU-only)
├── hf_model/         → upload as a Model (config.json + payload vocabulary)
├── hf_dataset/       → upload as a Dataset (JSONL + Parquet)
├── hf_static_space/  → upload as a Static Space (the free one, no PRO needed)
├── run.py            → one-command launcher, no install required
├── notebooks/hf_demo.ipynb
├── locales/          → 12 JSON locale files
├── tools/            → build_space.py, build_dataset.py, build_notebook.py
└── tests/
```

## 🛡️ The safety model

This tool fires requests at someone else's machine, so the *first* import in the
package is `safety.py` and nothing touches the network until a `ScanPolicy` says
yes.

| Gate | Behaviour |
|---|---|
| **Authorization** | `authorization_confirmed=False` → `AuthorizationError` before any byte moves |
| **Network range** | loopback, RFC1918, link-local (so cloud metadata at `169.254.169.254`), CGNAT, multicast, reserved → refused by default. DNS resolved by us, so DNS-rebinding can't sneak past |
| **Scheme** | `file://`, `gopher://`, `javascript:` → refused |
| **Payload** | `DROP TABLE`, `DELETE FROM`, `system(`, `sleep(`, `xp_cmdshell`, `rm -rf` → blocked at the boundary, for community payloads too |
| **Methods** | `POST/PUT/PATCH/DELETE` refused unless explicitly enabled |
| **Rate limit** | mandatory, 5–120 req/min, plus a hard total request budget |

Time-based blind SQLi is **deliberately not implemented** — `sleep()` is on the
blocklist, and a scanner that can hang a database is a scanner that can be used
as a DoS tool.

> ⚠️ **Only test systems you own or have explicit written permission to test.**
> Unauthorised scanning is illegal in most jurisdictions (CFAA 18 U.S.C. §1030,
> UK CMA 1990 s.1, id. UU ITEA Pasal 35-51). You are 100% responsible for every
> target you point this at.

## 📦 The Hugging Face family

| Repo | What it is |
|---|---|
| 🎨 [`polyglot-bughunter-x-static`](https://huggingface.co/spaces/Kicaulah/polyglot-bughunter-x-static) | **The Space.** Static, 4 tabs, 12 languages, 100% in-browser, free tier, no cold start. |
| 🐍 [`polyglot-bughunter-x`](https://huggingface.co/spaces/Kicaulah/polyglot-bughunter-x-static) | The full Gradio Space. Needs HF PRO (HF blocks new Gradio Spaces on the free tier). |
| 🧠 [`polyglot-bughunter-x`](https://huggingface.co/Kicaulah/polyglot-bughunter-x) | Model repo: `config.json`, 43-payload vocabulary, CVSS + scoring profile. |
| 🗃️ [`polyglot-bug-patterns`](https://huggingface.co/datasets/Kicaulah/polyglot-bug-patterns) | Dataset: payloads + real detector output, JSONL **and** Parquet. |
| 📓 [`hf_demo.ipynb`](notebooks/hf_demo.ipynb) | Runnable walkthrough. Works on HF Jupyter, Colab, Kaggle. |
| 💻 [GitHub](https://github.com/skamy64-ux/polyglot-bughunter-x) | Source, issues, PRs. |

### Kaggle

| Repo | What it is |
|---|---|
| 📊 [`polyglot-bug-patterns`](https://www.kaggle.com/datasets/simonmarc/polyglot-bug-patterns) | The dataset: 11 files, JSONL **and** Parquet, public. |
| 📓 [`polyglot-bug-patterns-demo`](https://www.kaggle.com/code/simonmarc/polyglot-bug-patterns-demo) | A Kernel that runs the whole detector end to end on Kaggle's free CPU. |

The Kernel inlines the package as a base64 wheel inside the notebook. That is
not a stylistic choice: Kaggle uploads the notebook and nothing else, so a
`pip install ./package` fails with `File './package' does not exist` and a
side-by-side `.whl` leaves `glob` empty. Rebuild and push with:

```bash
python tools/build_kaggle.py            # dataset
python tools/build_kaggle_kernel.py     # kernel (builds a wheel, inlines it)
kaggle datasets version -p kaggle_dataset
kaggle kernels push -p kaggle_kernel
```

Auth comes from `~/.kaggle/access_token` (one line, `chmod 600`) or
`~/.kaggle/kaggle.json` or `KAGGLE_API_TOKEN` — the CLI checks all three, so no
environment variable is needed once the file exists.

```python
from datasets import load_dataset
ds = load_dataset("Kicaulah/polyglot-bug-patterns", "payloads")
polyglots = ds["train"].filter(lambda r: r["polyglot"])
print(len(polyglots), "polyglot payloads")
```

## 🚀 Releasing

Five published surfaces, and a release that lands on four of them is worse than
no release script, because it looks done. One command runs the gate, rebuilds
every artifact, checks for drift, pushes, then re-reads every destination back:

```bash
python tools/release.py --all          # check, build, publish, verify
python tools/release.py --check        # the gate only, nothing uploaded
python tools/release.py --build        # rebuild every artifact
python tools/release.py --verify       # re-read all six URLs
python tools/release.py --publish --only kaggle-dataset,kernel
python tools/release.py --all --dry-run
```

| Guard | What it stops |
|---|---|
| The gate | Publishing on a red test suite. It runs ruff, pytest, JS/Python parity, app-level and browser checks. |
| Drift check | A version bump reaching four surfaces out of five. Compares the wheel shipped in the Kaggle dataset against the one inlined in the kernel, and both against `pyproject`. |
| Dirty-tree refusal | Uploading artifacts that do not match the commit. This is only meaningful because the build is reproducible. |
| Anonymous verify | A broken publish reading as a success. With a token loaded both hosts answer 200 for private repos, so the verifier uses no credential at all. |

Two things that verification taught, both of which report a healthy thing as
broken: **Kaggle does not implement `HEAD`** — it 404s every HEAD request
whether or not the repo exists — and it 404s any User-Agent that does not look
like a browser. GET with a browser agent is the only combination that tells the
truth.

## 🧱 Engineering notes

A few decisions worth knowing about, because they're the difference between a
tool and a demo:

* **The core is stdlib-only.** No requests, no bs4, no pillow, no numpy. A Space
  that needs a 400 MB wheel download loses every visitor to a cold start.
  Optional deps are adapters that return empty results instead of raising.
* **`replace_param()` rebuilds the URL.** Appending to a URL that already has the
  parameter produces `/x?id=1&id=2`, and most servers honour the *first* one — so
  every differential test silently comes back "clean". This one function is the
  difference between a working scanner and a scanner that finds nothing.
* **The PNG codec is hand-rolled** (`zlib` + `struct`). 120 lines to generate a
  canary with hidden pixels and to decode a screenshot for the pixel diff. No
  Pillow on the critical path.
* **Findings dedupe by scope.** A missing CSP is one finding with "affects 8
  pages", not eight identical rows. Injection findings key on path + parameter, so
  `?q=a` and `?q=b` are the same bug.
* **The demo target simulates, it doesn't execute.** It reproduces the four
  responses a real injectable app gives. Findings against it prove the *detector*.

## 📝 Changelog

### v1.0.0 — first flight 🚀
* 4-tab Gradio Space, 12 languages, 100% CPU, one-click offline demo.
* 43 payloads / 8 classes, 24 polyglot, param-name routing.
* CVSS v3.1 validated against an independent implementation (5000 vectors, 0 mismatches).
* Markdown / HTML / JSON / SARIF reports, DuckDB-or-sqlite history.
* `findings`, `scans`, `class_index` dataset configs in JSONL + Parquet.
* 5-gate safety model, destructive-payload blocklist, mandatory rate limiting.

<sub>MIT licensed · 🕷️ PolyglotBugHunter-X · authorized security testing only</sub>
