diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..95c7a62 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +# https://editorconfig.org — consistent formatting across editors. +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.py] +indent_size = 4 + +[*.{sh,gn}] +indent_size = 2 + +[Makefile] +indent_style = tab + +# Patches are verbatim diffs — do not let an editor rewrite their whitespace. +[*.patch] +trim_trailing_whitespace = false +insert_final_newline = false + +[*.{cmd,ps1}] +end_of_line = crlf diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0dca420 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,39 @@ +# Normalize line endings so a Windows checkout can't corrupt the patch set. +# `git apply` on patches/*.patch is whitespace-sensitive; CRLF creep silently breaks it. + +# Default: normalize text to LF in the repo, let git pick the working-tree ending. +* text=auto + +# These MUST stay LF everywhere (scripts run under sh; patches are applied verbatim). +*.patch text eol=lf +*.sh text eol=lf +*.py text eol=lf +*.js text eol=lf +*.mjs text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.json text eol=lf +*.gn text eol=lf +*.conf text eol=lf +tilion text eol=lf +series text eol=lf +Makefile text eol=lf + +# Windows-only helpers keep CRLF. +*.cmd text eol=crlf +*.ps1 text eol=crlf + +# Binary assets — never touch. +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.ttf binary +*.otf binary +*.woff binary +*.woff2 binary +*.dat binary +*.bin binary +*.tar.gz binary +*.zip binary diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..28bd393 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,47 @@ +name: Bug report +description: Something in Fortress or the SDKs is broken (not a detection vector) +title: "bug: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + For a page that *fingerprints* Fortress, use the "Detection vector" template instead. + For a security issue (crash, sandbox escape, host leak), do not file here — see SECURITY.md. + - type: textarea + id: what + attributes: + label: What happened + description: What you did, what you expected, and what actually happened. + validations: + required: true + - type: textarea + id: repro + attributes: + label: Steps to reproduce + placeholder: | + 1. docker run -p 9222:9222 tilion/fortress:latest + 2. connect_over_cdp("http://localhost:9222") + 3. ... + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Docker tag, pip/npm version, or bundle + Chromium version. + validations: + required: true + - type: input + id: platform + attributes: + label: Platform + placeholder: "macOS 14 arm64 / Ubuntu 24.04 x64 / Windows 11" + validations: + required: true + - type: textarea + id: logs + attributes: + label: Logs + description: Any relevant output. + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..4ebf425 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/tiliondev/fortress/security/advisories/new + about: Report a crash, sandbox escape, or host leak privately (see SECURITY.md) — not a public issue. + - name: Question or usage help + url: https://github.com/tiliondev/fortress/discussions + about: Ask how to set Fortress up or drive it — see AGENTS.md first. diff --git a/.github/ISSUE_TEMPLATE/detection_vector.yml b/.github/ISSUE_TEMPLATE/detection_vector.yml new file mode 100644 index 0000000..a2927cc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/detection_vector.yml @@ -0,0 +1,50 @@ +name: Detection vector +description: A page or script that tells Fortress apart from real Chrome +title: "detection: " +labels: ["detection"] +body: + - type: markdown + attributes: + value: | + A reproducible detector is the single most valuable thing you can send. Before filing, + please sanity-check it is a **fingerprint** issue and not an **IP** one: most "it got + blocked" reports are the datacenter IP being flagged before any page script runs. Re-run + through a residential/mobile proxy first — if it clears, the fingerprint was fine. + - type: textarea + id: repro + attributes: + label: Reproduction + description: A URL, or a self-contained HTML/JS snippet, that separates Fortress from real Chrome. + placeholder: | + https://example-detector.test + — or — + + validations: + required: true + - type: textarea + id: observed + attributes: + label: Observed vs expected + description: What Fortress shows, and what stock Chrome shows (values or screenshots). + validations: + required: true + - type: input + id: launch + attributes: + label: How you launched Fortress + description: Docker tag, pip/npm version, or bundle + the exact flags. + placeholder: "docker run tilion/fortress:latest / pip tilion-fortress 151.0.7908.0.post2" + validations: + required: true + - type: input + id: env + attributes: + label: Host OS and egress + placeholder: "Ubuntu 24.04, residential proxy" + - type: checkboxes + id: confirm + attributes: + label: Confirm + options: + - label: I verified this reproduces on a non-datacenter IP (or IP is not the factor here). + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..5db2717 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ + + +## What this changes + + + +## Type + +- [ ] Fingerprint patch (touches `patches/`) +- [ ] SDK / tooling / packaging +- [ ] Docs +- [ ] CI / infra + +## Checklist + +- [ ] `python tools/check_patches.py` passes +- [ ] `python -m pytest sdk/python/tests -q` passes (if SDK touched) +- [ ] If this touches `patches/`: it is **one file per patch**, added to `patches/series`, and uses + only the `uxr-` switch prefix (no brand strings baked into the binary) +- [ ] Any limitation or partial fix is written down (no oversold "undetectable" claims) +- [ ] For a surface change: before/after value on Fortress vs stock Chrome is in the description diff --git a/.gitignore b/.gitignore index 7542b36..0e35105 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,21 @@ dist/ .vscode/ .idea/ *.swp + +# Python packaging build artifacts (generated by build/setuptools) +*.egg-info/ +*.egg + +# test / lint caches +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.tox/ + +# local virtualenvs +.venv/ +venv/ + +# OS cruft +.DS_Store +Thumbs.db diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..75ba2f8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +# https://pre-commit.com — run the same cheap gates as CI before every commit. +# pip install pre-commit && pre-commit install +minimum_pre_commit_version: "3.0.0" + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + exclude: ^patches/ # patches are verbatim diffs + - id: end-of-file-fixer + exclude: ^patches/ + - id: check-yaml + - id: check-json + - id: check-merge-conflict + - id: mixed-line-ending + args: [--fix=lf] + exclude: '\.(cmd|ps1)$' + + - repo: local + hooks: + - id: check-patches + name: patch-set integrity linter + entry: python tools/check_patches.py + language: system + pass_filenames: false + files: ^patches/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..660e87a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,82 @@ +# Contributing to Fortress + +Fortress is a stealth Chromium engine that corrects fingerprint surfaces in the browser's **C++**, +then exposes raw CDP on `http://localhost:9222` as a drop-in for Playwright and Puppeteer. This +guide covers how to report a detection and how to get a change merged. + +## The single most valuable contribution + +**A page that reliably flags Fortress.** A minimal, reproducible detector — a URL or short script +that separates Fortress from real Chrome — is worth more than any feature. Open an issue with the +**Detection vector** template. + +Before filing, sanity-check it is a **fingerprint** issue and not an **IP** one: roughly 90% of +"it got blocked" reports are the datacenter IP getting flagged before any page script runs. Re-run +through a residential or mobile proxy first — if it clears, the fingerprint was fine. See rule 4 in +[AGENTS.md](AGENTS.md). + +## Two house rules + +1. **Every claim ships with a way to reproduce it.** A patch that changes a surface comes with the + command or test page that shows the before/after. +2. **Every limitation is written down.** If a patch is partial, say so in the patch header and the + docs. The word *undetectable* stays out of this project — we correct specific, named surfaces. + +## How the patch set is organized + +Fortress is a set of source patches applied to a pinned Chromium checkout (`CHROMIUM_VERSION`), not +a runtime library. + +- **`patches/`** — one patch per file, numbered, **single-surface**. `0002`/`0003` are the + `base::UxrConfig` singleton every override reads from; the rest each touch one place. +- **`patches/series`** — the apply order. **A patch not listed here is silently skipped** by + `build/apply-patches.sh`, so always add your patch to `series`. +- **`build/apply-patches.sh`** applies the series onto a Chromium `src/`. +- **`tools/gauntlet.py`** — the live detection harness (CreepJS / Sannysoft / BrowserScan). + +Full build instructions: [docs/BUILD_NATIVE.md](docs/BUILD_NATIVE.md). Expect a multi-hour first +compile; incremental rebuilds after a one-line patch are minutes. + +### The de-branded switch prefix — do not rename it + +Runtime overrides are exposed as `--uxr-*` flags read through `base::UxrConfig`. That prefix is +intentional and **must stay `uxr`** — a neutral token so the binary carries no product string a +detector could match. A new surface means a new `--uxr-` flag; never a `--fortress-*` / +`--tilion-*` flag, and never a brand string literal baked into the binary. + +## Before you open a PR — run the checks + +CI runs these on every PR; run them locally first (`make check`): + +```bash +python tools/check_patches.py # patch-set integrity (series, numbering, single-surface, uxr-only) +python -m pytest sdk/python/tests -q +``` + +Optionally install the git hooks so they run automatically: + +```bash +pip install pre-commit && pre-commit install +``` + +## Submitting a change + +1. **Open an issue first** for anything beyond a typo, so we can agree on the surface and approach. +2. **Branch** from `main`, focused on one surface / one fix. +3. **One patch per file, single-surface**, and add it to `patches/series`. +4. **Verify** with `tools/gauntlet.py`; paste the before/after into the PR. +5. **Rebase, don't merge** — `git fetch && git rebase origin/main` before pushing. The patch set is + rebased monthly onto new Chromium; a linear history keeps that sane. + +Docs, examples, the gauntlet, packaging, and the SDKs do **not** require a Chromium build — a great +place to start. + +## Security + +A page that *fingerprints* Fortress is not a security issue — file it in the open. A crash, sandbox +escape, or host leak **is** — report it privately per [SECURITY.md](SECURITY.md). + +## Licensing + +Fortress is BSD-3-Clause (a Chromium derivative — see [LICENSE](LICENSE) and [NOTICE](NOTICE)). By +contributing, you agree your contribution is licensed under the same terms. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b64eeec --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +# Fortress developer tasks. These wrap the scripts that already live in the repo; +# nothing here compiles Chromium (see docs/BUILD_NATIVE.md for that). +.DEFAULT_GOAL := help +PYTHON ?= python3 +BUNDLE ?= dist/tilion-fortress + +.PHONY: help lint test check gauntlet apply bundle clean + +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ + | awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}' + +lint: ## Run the patch-set integrity linter + $(PYTHON) tools/check_patches.py + +test: ## Run the Python SDK unit tests + $(PYTHON) -m pytest sdk/python/tests -q + +check: lint test ## Lint + test (what CI gates on) + +gauntlet: ## Run the live detection gauntlet against a bundle (BUNDLE=/path/to/tilion-fortress) + $(PYTHON) tools/gauntlet.py --bundle $(BUNDLE) + +apply: ## Apply the patch series onto a Chromium checkout (SRC=/path/to/chromium/src) + @test -n "$(SRC)" || { echo "usage: make apply SRC=/path/to/chromium/src"; exit 2; } + build/apply-patches.sh $(SRC) + +bundle: ## Assemble the portable bundle (SRC= FONTS= DEST=) + @test -n "$(SRC)" && test -n "$(DEST)" || { echo "usage: make bundle SRC= FONTS=fonts DEST=dist"; exit 2; } + packaging/build-bundle.sh $(SRC) $(FONTS) $(DEST) + +clean: ## Remove local build/test caches + rm -rf .pytest_cache **/__pycache__ sdk/python/*.egg-info dist/*.tar.gz dist/SHA256SUMS diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..09e7c69 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,34 @@ +# Security Policy + +## What counts as what + +Fortress is a stealth Chromium engine, so please route two very different things to two +different places: + +- **A detection vector** — a page or script that fingerprints Fortress and tells it apart from + real Chrome — is **not** a security vulnerability. It is exactly what the project wants, and it + belongs in a **public issue** (use the "Detection vector" template). The more reproducible, the + better. +- **A vulnerability in Fortress itself** — a way to crash the binary, escape the sandbox, leak the + host (files, environment, real IP outside the configured proxy), or compromise a machine running + it — is a **security issue**. Report it **privately** (below), not in a public issue. + +## Reporting a vulnerability + +Please use **GitHub's private vulnerability reporting**: the **Security** tab of this repository → +**Report a vulnerability**. That keeps the report private to the maintainers while it is triaged. + +Include, as far as you can: + +- affected version (Docker tag, `pip`/`npm` version, or bundle + Chromium version from `CHROMIUM_VERSION`), +- the platform, and +- a minimal reproduction and the impact. + +We aim to acknowledge a report within a few days and to keep you updated as we work on a fix. +Please give us a reasonable window to release a fix before disclosing publicly. + +## Supported versions + +Fortress tracks stable Chromium and is released from the tip of `main`. Security fixes land on the +**latest** release; older tagged releases are not maintained. Always verify a download against the +release `SHA256SUMS` (the `pip`/`npm` SDKs do this automatically). diff --git a/sdk/python/tilion_fortress.egg-info/PKG-INFO b/sdk/python/tilion_fortress.egg-info/PKG-INFO deleted file mode 100644 index 936f193..0000000 --- a/sdk/python/tilion_fortress.egg-info/PKG-INFO +++ /dev/null @@ -1,91 +0,0 @@ -Metadata-Version: 2.4 -Name: tilion-fortress -Version: 151.0.7908.0.post1 -Summary: Install and drive the Fortress stealth Chromium engine. Prebuilt binary, no source. -Author: arham766 -License: BSD-3-Clause -Project-URL: Homepage, https://github.com/tiliondev/fortress -Project-URL: Source, https://github.com/tiliondev/fortress -Keywords: chromium,stealth,browser,automation,anti-bot,fortress,tilion -Requires-Python: >=3.8 -Description-Content-Type: text/markdown - -

- Fortress — stealth Chromium engine -

- -

tilion-fortress

- -

Drive the Fortress stealth Chromium engine with one line — no build, no Chromium source.

- -

- PyPI - Python - Docker Pulls - Stars - License -

- ---- - -**Stop getting blocked — without `puppeteer-stealth`.** JavaScript stealth patches self-reveal: a detector checks whether a getter is native code and catches the override. Fortress compiles the fingerprint correction into Chromium's **C++**, so a page inspecting itself sees stock Chrome. It clears **CreepJS**, **Sannysoft**, **BrowserScan**, and live **Cloudflare** as a normal Chrome install. - -## Install - -```bash -pip install tilion-fortress -``` - -On first use it downloads the prebuilt Fortress binary for your platform from the official GitHub Release (SHA-256 verified) and caches it. No Chromium source, no compilation. - -## Quick start - -```python -from tilion_fortress import Fortress -from playwright.sync_api import sync_playwright - -with Fortress() as f: # launches the stealth engine on a CDP endpoint - with sync_playwright() as p: - browser = p.chromium.connect_over_cdp(f.cdp_url) - page = browser.new_page() - page.goto("https://bot.sannysoft.com") - page.screenshot(path="all-green.png") -``` - -Keep your existing Playwright / Puppeteer / CDP code — just point it at `f.cdp_url`. Works the same under **browser-use**, **Crawl4AI**, **Stagehand**, and **LangChain**. - -## Verified against live detectors - -| Suite | Result | -|---|---| -| **CreepJS** | 0% headless · 0% stealth | -| **bot.sannysoft.com** | 0 failed · all green · WebGL = NVIDIA RTX 3060 / ANGLE D3D11 | -| **browserscan.net** | "No bots detected, could be a human" | -| **rebrowser bot-detector** | no `Runtime.enable` leak · `webdriver=false` | -| **Cloudflare Turnstile** | cleared a live challenge | - -## Custom persona - -The default persona is a coherent Windows identity. Override any surface: - -```python -Fortress( - persona={"timezone": "America/Chicago", "languages": "en-GB,en", - "hw_concurrency": 16, "webgl_renderer": "ANGLE (NVIDIA, RTX 3060 ...)"}, - extra_args=["--window-size=1280,800"], -) -``` - -## Platform support - -Linux x64 has a native prebuilt binary. On macOS / Windows the package transparently runs Fortress via the official Docker image (`arham766/fortress`) — Docker is the cross-OS vehicle until native Win/Mac builds ship. - -> **Still blocked?** ~90% of the time it's your **IP**, not your fingerprint — datacenter ranges are flagged before any page script runs. Route egress through a residential or mobile proxy and retry. - -## Links - -- **Source & docs:** https://github.com/tiliondev/fortress -- **Agent guide:** https://github.com/tiliondev/fortress/blob/main/AGENTS.md -- **Docker image:** https://hub.docker.com/r/arham766/fortress - -BSD-3-Clause · reproducible from source · monthly Chromium rebase · **Blink · V8 · BoringSSL** patched in-tree. diff --git a/sdk/python/tilion_fortress.egg-info/SOURCES.txt b/sdk/python/tilion_fortress.egg-info/SOURCES.txt deleted file mode 100644 index 1bfbbfc..0000000 --- a/sdk/python/tilion_fortress.egg-info/SOURCES.txt +++ /dev/null @@ -1,9 +0,0 @@ -README.md -pyproject.toml -tilion_fortress/__init__.py -tilion_fortress/__main__.py -tilion_fortress.egg-info/PKG-INFO -tilion_fortress.egg-info/SOURCES.txt -tilion_fortress.egg-info/dependency_links.txt -tilion_fortress.egg-info/entry_points.txt -tilion_fortress.egg-info/top_level.txt \ No newline at end of file diff --git a/sdk/python/tilion_fortress.egg-info/dependency_links.txt b/sdk/python/tilion_fortress.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/sdk/python/tilion_fortress.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/sdk/python/tilion_fortress.egg-info/entry_points.txt b/sdk/python/tilion_fortress.egg-info/entry_points.txt deleted file mode 100644 index fac1db7..0000000 --- a/sdk/python/tilion_fortress.egg-info/entry_points.txt +++ /dev/null @@ -1,2 +0,0 @@ -[console_scripts] -tilion-fortress = tilion_fortress.__main__:main diff --git a/sdk/python/tilion_fortress.egg-info/top_level.txt b/sdk/python/tilion_fortress.egg-info/top_level.txt deleted file mode 100644 index f6400c4..0000000 --- a/sdk/python/tilion_fortress.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -tilion_fortress diff --git a/tools/gauntlet.py b/tools/gauntlet.py index 805b72d..2751954 100755 --- a/tools/gauntlet.py +++ b/tools/gauntlet.py @@ -12,7 +12,7 @@ No third-party deps — raw CDP over a hand-rolled WebSocket so it runs anywhere Python 3 does. """ -import argparse, base64, json, os, socket, struct, subprocess, sys, time, http.client +import argparse, base64, json, os, shutil, socket, struct, subprocess, sys, tempfile, time, http.client def ws_eval(port, expr, timeout=20): @@ -68,16 +68,21 @@ def main(): ap.add_argument("--keep", action="store_true", help="leave the browser running") args = ap.parse_args() - launcher = os.path.join(args.bundle, "tilion") + launcher = os.path.join(args.bundle, "tilion.cmd" if os.name == "nt" else "tilion") if not os.path.exists(launcher): sys.exit(f"no launcher at {launcher}") + # Isolated, OS-appropriate temp dirs (not a hardcoded /tmp, which doesn't exist on Windows). + profile = tempfile.mkdtemp(prefix="fortress-gauntlet-") + home = tempfile.mkdtemp(prefix="fortress-gauntlet-home-") + cmd = [launcher, "--headless=new", "--no-sandbox", "--disable-gpu", + f"--remote-debugging-port={args.port}", + f"--user-data-dir={profile}", "about:blank"] + if os.name == "nt": + cmd = ["cmd", "/c", *cmd] # a .cmd launcher must run through the shell interpreter proc = subprocess.Popen( - [launcher, "--headless=new", "--no-sandbox", "--disable-gpu", - f"--remote-debugging-port={args.port}", - "--user-data-dir=/tmp/fortress-gauntlet", "about:blank"], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - env={**os.environ, "HOME": "/tmp/fortress-gauntlet-home"}) + cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + env={**os.environ, "HOME": home}) try: for _ in range(60): try: @@ -119,6 +124,12 @@ def main(): finally: if not args.keep: proc.terminate() + try: + proc.wait(timeout=10) + except Exception: + proc.kill() + shutil.rmtree(profile, ignore_errors=True) + shutil.rmtree(home, ignore_errors=True) if __name__ == "__main__":