This is the abridged developer documentation for BattleTest
# BattleTest: automated PR review and live security testing
> BattleTest reviews every GitHub pull request for CVEs, secrets, and AI-detected vulnerabilities, and runs autonomous live infrastructure security testing.
One agent, one evidence standard, two scopes. **PR security review is free and open to anyone**: it reviews every pull request and posts what it finds on the diff. **Live application testing** is the same agent at full scope against a running system, shipped only as a **BattleTest Enterprise** deployment inside a client’s own network. PR Security Review Every pull request gets a full security analysis (CVE detection, secret scanning, injection analysis, config hardening checks) posted as a GitHub comment within minutes. BattleTest Enterprise The agent crawls and probes a running application, discovering endpoints, testing for active vulnerabilities, and reporting what it confirmed and what it could not reach. Runs as a dedicated per-client deployment. ## What you get on every pull request [Section titled “What you get on every pull request”](#what-you-get-on-every-pull-request) When a developer opens a PR, BattleTest runs automatically and posts a review as a comment on the diff within 2–4 minutes: Screenshot to addA real BattleTest review comment on a GitHub pull request: the risk-score header followed by findings, each with severity, file and line, and fix guidance. No configuration. No CI pipeline changes. Nothing to remember to trigger. The two products share one finding record, so a vulnerability caught in a PR and later confirmed in a live scan is linked. A resolved issue that resurfaces is flagged as a regression. For the design behind this, read [About BattleTest](/docs/concepts/what-is-battletest). ## Where to start [Section titled “Where to start”](#where-to-start) [Get your first PR review](/docs/tutorials/quick-start) [Connect a repo and see a real security review in under 5 minutes.](/docs/tutorials/quick-start) [How live testing works](/docs/tutorials/live-testing) [What a run does, what it reports, and how a dedicated deployment is set up.](/docs/tutorials/live-testing) ## How the docs are organised [Section titled “How the docs are organised”](#how-the-docs-are-organised) The documentation has four sections: * **Tutorials**: step-by-step guides through a complete task. Start here if you’re new. * **Concepts**: how BattleTest works, why it’s designed this way, and how to interpret what it produces. * **How-to guides**: task-focused instructions for specific goals: configuring settings, managing findings, exporting reports. Assumes you know your way around the product. * **Reference**: technical specifications: finding fields, scan report structure, GitHub App permissions, limits, and notification events.
# Integrations and API
> BattleTest integrations: the GitHub status check for PR merge gating, finding export, and the upcoming REST API.
BattleTest integrates with your existing tooling through the GitHub status check and finding export today, with a REST API on the roadmap. ## GitHub status check [Section titled “GitHub status check”](#github-status-check) Every PR review creates a `BattleTest Security Review` check run on the PR. Use it as a required check in GitHub branch protection rules to block merges until the security review has completed. The check reports `success` when the risk score is below 40, `action_required` at 40 or higher (“request changes”), and `failure` only on an internal review error. On its own it blocks nothing; add it as a required status check to gate merges on risky PRs. See [How to require security review before merging](/docs/how-to/require-security-review) for setup. No CI config, no runner minutes, no GitHub Actions dependency. ## Finding export [Section titled “Finding export”](#finding-export) Scan reports and PR findings export as PDF for auditors or JSON for ticketing systems and security dashboards, per run or across every PR in a repository. See [How to export findings](/docs/how-to/export-findings). ## Public REST API [Section titled “Public REST API”](#public-rest-api) BattleTest runs on an internal versioned API (`/api/v1`) that the web app and integrations use today. A **public, documented, stable** REST API is not yet released. When it is, it will support: * Triggering PR and live scan runs programmatically * Querying findings, scan history, and risk score trends * Managing targets, repository connections, and settings * Bulk export of finding data To be notified when the public API is available, email .
# Understanding findings
> What a BattleTest finding contains, how CRITICAL through LOW severity is assigned, how to triage false positives, and how findings persist across scans.
A finding is BattleTest’s conclusion that a specific, locatable vulnerability exists at a specific place in your code or running application. This page explains how to read a finding, how to decide what to do with it, and how findings behave over time. ## What a finding contains [Section titled “What a finding contains”](#what-a-finding-contains) Every finding has the same core anatomy whether it came from a PR review or a live scan: **Title** names the vulnerability class plainly: “SQL Injection in /api/users”, “Exposed AWS secret in commit history”, “Missing CSRF protection on state-changing form”. The title is understandable without reading the full finding. **Evidence** is the specific thing that triggered the finding: the exact code pattern, the committed credential, the payload that elicited a confirming response, or the configuration state that was checked. Evidence is what distinguishes a finding from a guess. If the evidence doesn’t make sense given your code, the finding is likely a false positive. **Explanation** is why this is a risk in your specific context. BattleTest explains the data flow that makes this particular instance a problem: a SQL injection finding describes how the untrusted input reaches the database call. **Remediation** is what to change, with a code example in the language and framework used by the affected code. The remediation is written for the diff context, not as a generic “sanitise your inputs” instruction. **References** link to the CVE, CWE, or OWASP category that classifies this vulnerability type, for teams that track findings against a compliance framework. ## Severity levels [Section titled “Severity levels”](#severity-levels) Severity reflects the impact and exploitability of the finding in your context. | Severity | What it means | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **CRITICAL** | Directly exploitable in your specific context: data breach, account takeover, remote code execution, exposed production credential. Fix before this code ships to production. | | **HIGH** | Exploitable with moderate effort or when combined with another finding. Address in this PR or immediately after merge. | | **MEDIUM** | Exploitable under specific conditions: a particular attacker position, a specific sequence of operations, or a non-default configuration. Triage and schedule for the current sprint. | | **LOW** | Defense-in-depth gap, best-practice deviation, or informational finding with low direct impact. Track and address when convenient. | Severity is assessed against your actual code context. A reflected XSS in an admin panel behind authentication is lower severity than the same pattern in a public-facing login form. ## How findings from different sources differ [Section titled “How findings from different sources differ”](#how-findings-from-different-sources-differ) The **Source** field on a finding tells you how it was detected: * **CVE**: matched a new or updated dependency against vulnerability databases. Deterministic: the CVE either applies to this version or it doesn’t. Near zero false positives, but only covers known, catalogued vulnerabilities. * **Secret**: a credential, API key, or cryptographic key found in the commit history or current code. Check the evidence to confirm it’s a real credential and not a test fixture. * **Config**: a structural repository check failed (missing `SECURITY.md`, no lockfile committed, no Dependabot). These are observations that represent gaps in your security process, not exploitable vulnerabilities on their own. * **AI**: the AI agent identified a vulnerability through semantic analysis of the diff. These findings have evidence and context-specific explanations, but they’re probabilistic. Occasionally one will be a false positive. Read the explanation and judge whether the risk is real. * **Regression**: a finding semantically similar to one previously resolved on this repository or target. It keeps the underlying finding type and adds a penalty to the risk score because you’ve fixed this before. ## Confirmed vs Potential (live scans only) [Section titled “Confirmed vs Potential (live scans only)”](#confirmed-vs-potential-live-scans-only) In live application testing results, findings have a confidence level in addition to a severity: **Confirmed** means the agent verified the vulnerability with a working proof of concept: a payload that produced the confirming server response. These are real vulnerabilities. Act on them. **Potential** means the endpoint or pattern looked suspicious but couldn’t be verified without a higher permission tier, or without risking data corruption. These are worth investigating manually. Approving the next permission tier on a staging environment often promotes a Potential to Confirmed or rules it out. PR review findings don’t use this classification: with no live target to probe, the AI makes a judgment call about exploitability, reflected in severity instead. ## How to triage a finding [Section titled “How to triage a finding”](#how-to-triage-a-finding) For each finding, the decision flow is: 1. **Read the evidence.** Does it reflect a real pattern in your code, or does it look like a test fixture, a doc comment, or a false positive from the parser? 2. **Read the explanation.** Does the described data flow make sense? Is the input actually untrusted in your context? 3. **If real:** fix it. The remediation section has a working example. For findings in a PR, fix in that branch before merging. For live scan findings, deploy the fix and run a new scan to confirm resolution. 4. **If not real (false positive):** dismiss it with a reason. See [how to review, dismiss, and track findings](/docs/how-to/manage-findings). Dismissing a finding removes it from future risk scores for this code path or endpoint and excludes it from regression detection. ## How findings behave across scans [Section titled “How findings behave across scans”](#how-findings-behave-across-scans) Findings have a lifecycle that spans multiple scans. When a vulnerability is fixed and disappears from a subsequent scan, BattleTest classifies it as **Resolved** and adds it to the regression detection baseline. If the same vulnerability pattern reappears in a future scan (because the fix was reverted, because similar code was introduced elsewhere, or because a dependency re-introduced the same CVE) it’s classified as a **Regression** and weighted more heavily in the risk score. Dismissed findings don’t contribute to the regression baseline. A dismissed finding won’t trigger a regression alert if it reappears, because you’ve already judged it not worth tracking. The practical effect: your first few PR reviews surface a backlog of findings. As you address and dismiss them, subsequent reviews become more signal-dense, surfacing what’s genuinely new or regressed rather than restating the full history. ## When findings disagree with your own assessment [Section titled “When findings disagree with your own assessment”](#when-findings-disagree-with-your-own-assessment) BattleTest’s AI analysis is context-aware but not infallible. If a finding doesn’t reflect how your code actually works, dismiss it with a note explaining why. The dismissal is recorded in your audit trail and feeds back into the model for your repository. If you believe a finding is systematically wrong (the same false positive keeps appearing across PRs) contact with the finding IDs. Patterns in false positives usually indicate a codebase pattern that the model isn’t handling correctly.
# How BattleTest works
> How BattleTest reviews code changes, tests authorised live targets, validates findings, and tracks security issues over time.
BattleTest provides two complementary forms of security testing: pull request review for code changes and live application testing for running applications. Both produce findings in the same workspace, so your team can triage, remediate, and track recurring issues consistently. ## Pull request review [Section titled “Pull request review”](#pull-request-review) When an eligible pull request is opened or updated, BattleTest analyses the change for issues such as exposed secrets, vulnerable dependencies, unsafe configuration, and security-relevant code behaviour. Results appear in the BattleTest dashboard and the pull request’s `BattleTest Security Review` check. Findings include the affected location, severity, evidence, explanation, and remediation guidance. You can use the check as an informational signal or configure it as a required merge gate. See [How PR review works](/docs/concepts/how-pr-review-works) for review coverage and [Risk score](/docs/concepts/risk-score) for verdict behaviour. ## Live application testing [Section titled “Live application testing”](#live-application-testing) A live run evaluates an application or API you have registered and verified. The run begins by mapping the authorised surface, then performs testing allowed by the permission level you selected. Higher-risk actions stay subject to scope and approval controls. BattleTest validates candidate issues against observed evidence before reporting them. Reports distinguish confirmed findings from items that need manual verification and provide reproduction and remediation information where available. See [How live testing works](/docs/concepts/live-testing) for the testing phases and permission model before running a test. ## Shared finding history [Section titled “Shared finding history”](#shared-finding-history) PR reviews and live runs contribute to the same organisation-level finding history. BattleTest uses it to recognise related issues, highlight possible regressions, and show whether a risk appears in a code change, a deployed application, or both. Similarity and regression signals support triage. They are not proof that two findings have the same root cause. Review the underlying evidence before closing or combining issues. ## Safety and data boundaries [Section titled “Safety and data boundaries”](#safety-and-data-boundaries) * Repository access is limited to the GitHub installations and repositories you select. * Live testing requires ownership or explicit permission and must follow the [Acceptable Use Policy](https://battletest.dev/acceptable-use). * Testing intensity is constrained by the selected permission level, target scope, usage limits, and applicable approvals. * Findings can contain selected code or response evidence needed to explain an issue. See [Security & Data Handling](/docs/reference/security-and-data). ## Timing [Section titled “Timing”](#timing) Review and test duration varies with change size, target complexity, reachable surface, and the depth required to validate potential findings. The dashboard shows live status while a run is active. See [Limits & Quotas](/docs/reference/limits-and-quotas#scan-duration) for typical ranges and usage limits.
# How PR review works
> How BattleTest's automated PR review works: CVE scanning, secret detection, AI code analysis, and regression detection on every pull request.
When a pull request event arrives from GitHub, BattleTest runs several layers of analysis in sequence. Knowing what each layer checks, and what it can and cannot catch, helps you interpret findings accurately. ## Advanced vs basic reviews [Section titled “Advanced vs basic reviews”](#advanced-vs-basic-reviews) BattleTest runs one of two review modes depending on your current usage. **Advanced reviews** run the full pipeline: dependency CVE scanning, secret detection on the PR diff, configuration hardening checks, regression detection, and multi-turn AI code analysis. They produce proof-of-concept scripts for confirmed findings and post a detailed GitHub PR review with inline comments. **Basic reviews** run a fast single-pass scan covering secret detection and CVE scanning only. No AI analysis, no regression detection, no PoC generation. Basic reviews run automatically when your advanced limit is reached, so every PR gets at least partial coverage. The PR review includes a note indicating that the basic scan ran. Which type runs depends on repository visibility and how much you have already used: * **Public repo:** 20 advanced reviews/hour *per repository*, then basic fallback (20/hour per repository) * **Private repo:** 10 advanced reviews/month, then basic fallback (5/hour) See [Limits & Quotas](/docs/reference/limits-and-quotas) for the full figures. ## Dependency CVE scanning [Section titled “Dependency CVE scanning”](#dependency-cve-scanning) Every dependency added or updated in the PR diff is checked against the Open Source Vulnerability (OSV) database and NVD. The result is a list of CVEs affecting the specific versions in the PR, with CVSS scores and fixed-version guidance. This catches the most automatable and most commonly exploited vulnerability class: known-vulnerable third-party libraries that enter a codebase through routine upgrades. ## Secret scanning [Section titled “Secret scanning”](#secret-scanning) Secret scanning runs against the PR diff (the lines the pull request adds or changes) using gitleaks with 150+ rules. Any changed file that looks like a credential store (`.env`, key files, and similar) is additionally fetched and scanned in full, not just the diffed lines. PR review scans the change. Full-history secret sweeps run as part of the repo-posture battletest, not on every PR. The rule set covers cloud credentials (AWS, GCP, Azure), SaaS API keys (Stripe, SendGrid, Twilio, and hundreds of others), database connection strings, and cryptographic key formats. Findings include the file path and line number. If you receive a secret finding, rotate the credential immediately. Removing it from the current branch is not sufficient: it remains readable in the commit history of anyone who has already cloned the branch. See [how to respond to an exposed credential](/docs/how-to/exposed-secrets). ## Configuration hardening [Section titled “Configuration hardening”](#configuration-hardening) A set of structural checks against the repository itself: is there a `SECURITY.md`? Is Dependabot configured? Is there a `CODEOWNERS` file? Are lockfiles committed alongside package manifests? Does a CI configuration include security gates? These are organisational prerequisites for a healthy security posture, not code vulnerabilities. ## Regression detection [Section titled “Regression detection”](#regression-detection) New findings are compared against your repository’s finding history. If the same vulnerability class was previously resolved and has returned, it’s flagged as a regression and weighted more heavily in the risk score. This comparison makes consecutive reviews useful: each PR is assessed against what your team has already fixed. ## AI security analysis [Section titled “AI security analysis”](#ai-security-analysis) An AI agent analyses the complete diff in context: the changed lines, the functions they interact with, and the data flow from request input to storage and output. The agent produces a prioritised finding list with severity classification, code location, a plain-English explanation of why the finding is a problem, and a remediation example in the language and framework used by the PR. This layer runs after the others because it’s richer for it: a CVE in a new dependency informs the analysis of what risk surface has changed, and a detected secret is cross-referenced against the rest of the codebase. The AI analysis adds semantic understanding on top of the earlier layers. This layer only runs in advanced reviews. ## Why findings are advisory by default [Section titled “Why findings are advisory by default”](#why-findings-are-advisory-by-default) BattleTest posts findings as a PR comment and creates a check run. By default it does not block merges. A security tool that blocks deploys on every false positive quickly gets bypassed, so the goal is that every finding is visible and tracked, not that every finding requires resolution before a PR can merge. The check conclusion mirrors the review verdict: a risk score of 40 or higher reports `action_required` (“request changes”), anything lower reports `success`. On its own this surfaces a status and blocks nothing. Teams that want a hard gate add the check as a required status check in GitHub branch protection, which then blocks the PRs whose risk warrants changes while letting low-risk PRs through. See [how to require security review before merging](/docs/how-to/require-security-review).
# How live testing works
> How live application testing works: passive discovery, active vulnerability probing, proof-of-concept findings, and scan-over-scan regression checks.
Live application testing is an autonomous security assessment run against a domain you control. A PR review analyses code before it ships; a live run operates against your running application from the perspective of an external attacker. ## Why dynamic testing catches what code review doesn’t [Section titled “Why dynamic testing catches what code review doesn’t”](#why-dynamic-testing-catches-what-code-review-doesnt) Code review, however thorough, cannot observe the interaction between components at runtime. A vulnerability that depends on the specific configuration of a load balancer, the presence of a misconfigured header in a particular environment, or the combination of how your application processes a request and what your database does with the result only manifests in a running system. Dynamic testing also reveals your actual attack surface: what’s reachable from the internet, what endpoints exist that documentation doesn’t mention, what subdomains are exposed. Code review tells you what you built. Live testing tells you what’s exposed. ## The assessment phases [Section titled “The assessment phases”](#the-assessment-phases) A battletest runs in three sequential phases: **Discovery** maps the application: pages, API endpoints, forms, subdomains, and the technologies detected at each. This is passive reconnaissance (no active probing) and produces the inventory the next phase uses. **Targeted testing** probes the discovered endpoints with techniques you’ve approved. The security testing layer runs injection tests against identified input surfaces, directory enumeration against directory structures, port scanning against the open port profile, and a browser-based agent for JavaScript-rendered content and form analysis. Each technique is permission-gated. **Reporting** compiles confirmed findings into a report with severity, evidence (the payload or observation that confirmed the finding), reproduction steps, and remediation guidance. The report is available immediately in the dashboard and as a PDF or JSON export. ## The permission tier model [Section titled “The permission tier model”](#the-permission-tier-model) The permission tier model is the central design decision in live application testing. BattleTest never runs at a higher permission tier than you’ve explicitly approved. | Tier | What it does | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Read-only** | Passive observation: header analysis, SSL configuration, public endpoint mapping. Nothing is sent to the application except ordinary HTTP requests. | | **Safe probing** | Non-destructive probing for common misconfigurations. Still does not send attack payloads. | | **Active probing** | Active vulnerability testing: injection testing, directory enumeration, port scanning. Where most confirmed findings originate. | | **Exploit chain** | Full exploit chain execution against confirmed vulnerabilities. Reserved for environments specifically set up to receive it, such as a dedicated testing instance. | Active security testing against production without careful control is dangerous: it can affect real users, corrupt real data, and trigger alerts in your monitoring. The permission tiers let you advance the assessment at a pace you control, against targets you’ve prepared appropriately. Most production assessments stop at active probing. Exploit chain testing is typically used on staging environments. ## Regression detection across scans [Section titled “Regression detection across scans”](#regression-detection-across-scans) Each battletest is compared against the previous scan of the same target. Findings are classified as: * **New**: appeared this scan * **Persistent**: present in both scans * **Resolved**: was present, now absent * **Regression**: similar to a previously resolved finding The regression category gets the most attention: it indicates something your team spent time fixing has come back. Run timing matters. A run immediately after a deploy tells you whether the deploy introduced new issues. A run after the system has stabilised tells you the steady-state security posture. A deployment’s run cadence is agreed as part of the engagement.
# Risk score
> How BattleTest calculates the risk score, what the score bands mean, and how to use score trends to track security posture over time.
Every completed PR review and live run produces a risk score from **0 to 100** (e.g. `72 / 100`). The score summarises the security posture of a single PR or the current state of a live target. It’s designed to be glanceable. Understanding what goes into it helps you interpret it correctly. ## What the score measures [Section titled “What the score measures”](#what-the-score-measures) The risk score is a weighted aggregate of the findings in that scan. It’s a signal about how much risk the current scan introduces, relative to severity and your organisation’s history with the same code or target. It’s not a normalised percentage of vulnerabilities found, and it’s not a compliance score. Four factors drive the score: **Severity distribution** is the primary driver. Critical findings have exponential weight: a single confirmed Critical finding can push a previously clean score past 80. High findings have substantial weight; Medium and Low findings have diminishing contributions. One Critical finding demands immediate attention regardless of how many Low findings are also present. **Exploitability** modifies the base weight for CVE findings. If a CVE has a public exploit or a proof-of-concept available, the finding contributes more to the score than an unproven theoretical vulnerability with the same CVSS score. **Regression penalty** adds to the score for any finding classified as a regression: a vulnerability that your team previously resolved and that has returned. The penalty reflects the operational cost of fixing the same issue twice. A regression in a MEDIUM finding can contribute as much to the score as a new HIGH finding. **Finding count** has a diminishing effect. The second Critical finding adds less to the score than the first; ten Medium findings together add less than two Highs. This prevents a large number of Low findings from pushing the score into the Critical band when the underlying issues are minor. ## Score bands [Section titled “Score bands”](#score-bands) | Score | Band | What it means | | ------ | ----------------- | ---------------------------------------------------------------------------------------------------------- | | 0–20 | **Clean** | No significant findings. The diff or target looks clear. | | 21–40 | **Low risk** | Minor issues present, no directly exploitable path identified. Address when convenient. | | 41–60 | **Moderate risk** | Real findings worth acting on before the next release. Not immediately dangerous but shouldn’t accumulate. | | 61–80 | **High risk** | Critical and High findings require attention. Don’t ship this PR without addressing them. | | 81–100 | **Critical** | Stop. At least one finding represents an immediately exploitable risk. Fix before any deployment. | These bands match the [PR Findings reference](/docs/reference/pr-findings#risk-score). ## Reading the score for PR review [Section titled “Reading the score for PR review”](#reading-the-score-for-pr-review) A score near 0 on a PR that introduced a lot of new code is a positive signal: the diff looks clean from the analysis. A score near 0 on a one-line change is less informative (there wasn’t much to analyse). A score in the High or Critical band doesn’t always mean the PR should be rejected. Read the findings driving the score. If the Critical finding is a secret that was removed in a later commit (but still in branch history), the fix is to purge it from history, not to close the PR. If the High finding is a dependency CVE in a package that’s only used in tests, the risk profile differs from the same CVE in a production-facing library. Use the score to prioritise findings for review. ## Reading the score for live scans [Section titled “Reading the score for live scans”](#reading-the-score-for-live-scans) A live scan score above 60 with Confirmed findings means your running application has verified vulnerabilities. This is more urgent than the same score on a PR review, because the vulnerability is already in production. A score above 60 with only Potential findings means the agent detected suspicious patterns but couldn’t verify them at the current permission tier. Investigate manually, or approve the next permission tier on a staging environment to get Confirmed findings. ## Using score trends [Section titled “Using score trends”](#using-score-trends) The dashboard shows a score trend line per repository and per live target. This is often more useful than any individual score. **A falling trend** after a remediation sprint confirms the fixes are working. Export the trend as evidence for SOC 2 Type II (“security controls operated continuously and effectively”). **A rising trend** despite no new features suggests technical debt accumulation: old dependencies drifting toward known CVEs, configuration gaps not being closed. **A flat trend at a non-zero score** is a warning sign. If your score has sat at 55 for six weeks, the same findings are being generated repeatedly without being addressed. The baseline is stagnant. **Spikes on PR reviews** are expected when a large refactor or a new integration lands. A spike followed by a return to baseline suggests the PR introduced temporary risk that was mitigated. A spike that doesn’t return to baseline suggests a new vulnerability class entered the codebase and wasn’t resolved. ## What the score is not [Section titled “What the score is not”](#what-the-score-is-not) The risk score is not a compliance grade. A score of 0 means the automated analysis found nothing. It does not mean your application is secure against a determined attacker. BattleTest covers a wide surface but it’s not exhaustive. The score is one signal among several, not a security certification. The score also doesn’t directly reflect the count of findings. A scan with two Critical findings and no others scores higher than a scan with thirty Low findings. Volume of minor findings must not obscure the severity signal.
# About BattleTest
> BattleTest vs traditional pentest, vs SAST tools, and why continuous automated security testing catches more vulnerabilities than point-in-time engagements.
BattleTest is an AI-native security platform built around a simple observation: most application security tools are too expensive for early-stage teams, too slow to fit into a development workflow, or cover only one slice of the attack surface. ## Two products, one shared memory [Section titled “Two products, one shared memory”](#two-products-one-shared-memory) BattleTest has two complementary products that cover different parts of the vulnerability lifecycle: * **PR Review** catches vulnerabilities before they merge. Every pull request on your connected repositories gets an automated security review (CVE detection, secret scanning, injection analysis, and config hardening checks) posted as a comment on the PR within minutes. * **BattleTest Enterprise** finds vulnerabilities in your running infrastructure. An AI agent coordinates a crawl-and-probe security assessment against a domain you specify, discovering endpoints, testing for active vulnerabilities, and producing a findings report. Both products share a common finding record. When a vulnerability found in a PR review later appears in a live scan (or when something previously resolved resurfaces) BattleTest flags the regression. This connection across products makes the coverage coherent rather than additive. ## Why code review and live testing together [Section titled “Why code review and live testing together”](#why-code-review-and-live-testing-together) A vulnerability has a lifecycle. It starts as a code pattern: a raw SQL query, a missing auth check, a committed secret. That code pattern becomes a runtime behaviour once it’s deployed. A runtime behaviour becomes an exploitable vulnerability once an attacker finds it. Code-only tools (SAST, dependency scanners) catch the pattern early but miss the context of what the running application actually exposes. Live pentesting tools catch the runtime exposure but have no connection to the code that produced it and no insight into what’s been introduced recently. BattleTest operates at both points. The PR review catches the pattern before it ships; the live run confirms whether it’s exploitable in production; the shared memory connects the two, so a finding in code is linked to a related finding in the live scan. ## Who it’s for [Section titled “Who it’s for”](#who-its-for) BattleTest is designed for teams that want meaningful security coverage without the infrastructure of a dedicated security team: * Solo developers who want to know whether their code introduces obvious vulnerabilities before they push to production * Startups preparing for SOC 2, ISO 27001, or their first enterprise customer security questionnaire * Small engineering teams that want security checks integrated into their PR workflow rather than as a quarterly audit The design assumption is that your team knows how to build software but doesn’t have a dedicated AppSec engineer. BattleTest automates the class of checks that would otherwise be skipped because no one has time to do them manually. It does not replace expert security judgment on complex architectural decisions. ## BattleTest vs the alternatives [Section titled “BattleTest vs the alternatives”](#battletest-vs-the-alternatives) **vs. traditional pentest** A pentest engagement runs once or twice a year, takes weeks to schedule, and produces a report that’s already partially stale by the time you receive it. BattleTest runs on every PR and every deploy. For the class of vulnerabilities it covers (dependency CVEs, secrets, injection patterns, misconfiguration, surface exposure) continuous automated testing covers more of the year than two point-in-time engagements do. BattleTest replaces the pentest for the routine, automatable portion of security coverage. It does not replace a pentest for complex architectural reviews or threat-model-specific assessments. **vs. SAST tools (CodeQL, SonarQube, Semgrep)** Traditional static analysis tools work by pattern matching: a rule that looks for string concatenation into a SQL query, a rule for `eval()` on user input. They’re fast and deterministic but blind to context: they can’t trace a parameter across multiple function calls, understand the semantics of an ORM escape hatch, or recognise that a finding in a test file is not a production risk. BattleTest’s AI analysis reads the diff in context: the changed lines plus the functions they call, the data they receive, and the historical finding record for the same files. It produces ranked, explained findings with remediation code in your stack. The trade-off is that it’s probabilistic: it will occasionally flag something that turns out to be a false positive. The finding always includes its reasoning, so you can evaluate it quickly. BattleTest also does things SAST tools don’t: it scans PRs for exposed secrets (and sweeps full git history during a repo-posture battletest), checks live infrastructure for exploitable vulnerabilities, and connects the finding record across code and runtime so regressions are caught automatically. **vs. doing nothing** The median time from vulnerability introduction to discovery is 197 days (IBM Cost of a Data Breach, 2023). For solo developers and small teams, that number is typically higher: there’s no security review process to catch things as they ship. BattleTest cuts this to near-zero for the vulnerability classes it covers, on every PR, automatically. ## What BattleTest does not do [Section titled “What BattleTest does not do”](#what-battletest-does-not-do) Understanding the boundaries helps you use BattleTest as part of a layered security approach rather than treating it as a complete solution. BattleTest doesn’t replace a penetration test. A pentest is scoped to a specific threat model, conducted by humans who think adversarially, and produces a report tailored to your architecture. BattleTest automates broad coverage across a known vulnerability taxonomy. It will miss novel attack chains, business logic flaws, and vulnerabilities that depend on understanding how your users actually behave. BattleTest doesn’t replace a security engineer on your team. If your architecture has a fundamental design flaw (a trust boundary that’s incorrectly placed, a threat model that’s been invalidated by a product change) BattleTest won’t identify it. It operates at the level of individual code patterns, endpoint behaviours, and known vulnerability classes. BattleTest doesn’t block all merges with findings by default. Findings are advisory unless you configure GitHub branch protection to require the status check. A tool that blocks every PR on a false positive quickly gets bypassed, so the goal is that every finding is visible and tracked, not that every finding must be resolved before shipping. ## The shared memory in practice [Section titled “The shared memory in practice”](#the-shared-memory-in-practice) Consider a common scenario: your AI analysis flags a potential SQL injection in a PR. The developer isn’t sure it’s a real risk, since the ORM should be escaping correctly. They merge. Six weeks later, a live security test confirms the injection via active probing. The live scan finding links back to the PR review finding from six weeks ago. You can see exactly which commit introduced it, what the original finding said, and that a developer reviewed it and concluded the risk was low. That audit trail (from code pattern through to confirmed exploitable vulnerability) makes the shared memory useful for both remediation (you know exactly where to look) and compliance (you have a record of when the vulnerability was introduced and how it was discovered). The reverse case matters too: a vulnerability confirmed in a live scan that then disappears from the next scan (because you deployed a fix) is automatically classified as Resolved. If a similar pattern reappears in a future PR review, BattleTest flags it as a regression: you’ve seen this before, and it’s back.
# Add the Battletested! badge
> Show that your project uses BattleTest PR security reviews.
Add this badge to your repository’s README when your project uses BattleTest PR security reviews.  The badge identifies use of PR security reviews. It does not certify that a project is secure or that every change has been reviewed. ## Add it to your README [Section titled “Add it to your README”](#add-it-to-your-readme) Copy this Markdown into your README:
```markdown
[](https://battletest.dev/open-source?utm_source=readme&utm_medium=badge&utm_campaign=open_source)
```
Preview the README before committing. The image should show **Battletested! | PR security reviews**, and clicking it should open the open-source programme page. The descriptive image text also helps readers using assistive technology. The badge is optional. It does not change your review limits, grant sponsorship, or affect any sponsored access. You can remove it whenever you want. ## Start reviewing pull requests [Section titled “Start reviewing pull requests”](#start-reviewing-pull-requests) If you found BattleTest through a badge, [connect your GitHub repositories](/docs/how-to/connect-github) and follow [your first PR review](/docs/tutorials/quick-start). PR review is free within the [review limits](/docs/reference/limits-and-quotas). For a deployment you operate yourself, start with [self-hosting](/docs/how-to/configure-source-control). The badge does not configure reviews or supply model credentials.
# How to prepare a target for live application testing
> What an application needs in place before a BattleTest Enterprise engagement: authorisation, reachability, permission tiers, and self-serve signup for authenticated coverage.
## Prerequisites [Section titled “Prerequisites”](#prerequisites) * You must own the target or have permission to test it under your agreement. Enterprise does not require DNS ownership verification or a separate approval for each host. Any configured organisation scope restrictions still apply. * The application must be reachable from wherever the deployment runs. * **For authenticated coverage, the app must offer self-serve email/password signup.** Most of an application’s attack surface (IDOR, broken access control, privilege escalation) lives behind login, and the agent acquires its own test identities by registering real accounts. See below. ## Authenticated testing requires self-serve signup [Section titled “Authenticated testing requires self-serve signup”](#authenticated-testing-requires-self-serve-signup) To test everything behind login, BattleTest signs up its own disposable accounts (using throwaway mailboxes it controls), logs in, and drives the app as a real user, including creating a second identity to probe for cross-user access flaws (IDOR/BOLA/RBAC). The second identity is also how findings are confirmed: nothing is reported unless it was replayed against the running system and reproduced from a second, unrelated account. For this to work, the target must allow **self-serve registration with email and password** (or another credential BattleTest can create on demand). Caution **SSO-only apps get discovery-only coverage.** If the only way in is “Sign in with Google/GitHub” and there is no email/password signup, BattleTest will **not** automate a third-party login (it’s outside your authorisation and against those providers’ terms). It reports the app as SSO-only and tests only the unauthenticated surface. To get authenticated testing, expose a self-serve email/password signup: even a staging-only or feature-flagged one is enough. ## Scope restrictions and third-party hosting [Section titled “Scope restrictions and third-party hosting”](#scope-restrictions-and-third-party-hosting) Enterprise deployments can test public hosts, private IP addresses, and local aliases without DNS TXT records or CNAME approvals. The application must still be reachable from the deployment. Administrators may restrict targets in **Settings → Policy → Testing scope**. With no entries, registered targets are allowed. With entries, each target must match a listed domain or IP range. See [Configure testing scope](/docs/how-to/configure-testing-scope). For applications hosted on shared or third-party infrastructure, confirm that your permission to test covers the intended activity. Registration in BattleTest does not establish that permission. ## Environments and domains [Section titled “Environments and domains”](#environments-and-domains) A target is described as an **environment**: one application, and every hostname that belongs to it. `api.example.com` and `app.example.com` are one environment, not two. One battletest run covers the whole environment regardless of how many hostnames it contains. Group by application. Do not group by DNS. Splitting one application across two environments hides exactly the flaws worth finding: an agent that can move from the web app to the API in a single run is what surfaces a cross-service access-control break. Screenshot to addThe dashboard environment view: an environment with its hostnames, each showing its registration status. ## Permission tiers [Section titled “Permission tiers”](#permission-tiers) Each environment carries a permission tier that decides what the agent is allowed to do to it. The tier is a code-level control: offensive tooling is withheld at lower tiers rather than the agent being instructed not to use it. Raising a tier is an explicit, recorded approval, and steps that could change state pause for a human decision. A first assessment normally starts read-only, and the tier is raised once the discovered inventory has been reviewed. See [How live testing works](/docs/concepts/live-testing#the-permission-tier-model) for what each tier permits. ## What a run reports [Section titled “What a run reports”](#what-a-run-reports) A run reports what it confirmed and what it could **not** reach. Coverage you did not get is a fact about the assessment and appears in the report. Discovery output (the endpoint inventory the run built) is part of the record. For the shape of the report itself, see [Scan results](/docs/reference/scan-results).
# Bring your own key and storage
> Connect your own model provider to remove review limits, and your own S3-compatible bucket so finished reviews live in storage you control.
PR review is free, and the limits on it exist for one reason: inference is the marginal cost of a review, and those limits bound our spend on your behalf. Connect your own model provider and there is nothing left for a limit to protect. Reviews stop being capped and always run at full depth. Connect your own storage as well and the durable copy of your review history lives in a bucket you own. Both are free: in both cases the resource is yours. ## Bring your own model connection [Section titled “Bring your own model connection”](#bring-your-own-model-connection) The connection is provider-neutral. Choose the API format the host accepts (`OpenAI` or `Anthropic`), then use that format’s official endpoint or enter a custom endpoint. Custom endpoints cover hosted providers, gateways and reachable local inference servers. 1. Go to **Settings → Usage**. 2. Under **Bring your own**, choose the API format and official/custom endpoint. 3. Enter the model ID and, when the endpoint requires one, its API key. Leave the key blank for a keyless local endpoint. 4. Save. BattleTest runs a real one-token model request in the selected format before accepting it. Screenshot to addSettings → Usage → Bring your own: the model-connection form with API format, endpoint, model ID, and the last verification result. BattleTest resolves known model prices from its bundled aggregate snapshot. If the model is not present, enter input, cached-input and output prices per million tokens. For local inference those overrides can all be `0`. ### What changes [Section titled “What changes”](#what-changes) Every review runs at **advanced** depth, with no hourly or monthly cap. The public-repo hourly limit and the private-repo monthly allowance both stop applying: reviews are what cost us, and on your own key they cost us nothing. Bring-your-own infrastructure is configured separately from your plan. ### If the key stops working [Section titled “If the key stops working”](#if-the-key-stops-working) If your configured provider rejects a request, the review fails and displays the error. It does not fall back to BattleTest credentials. The last verification result is shown in Settings, including the reason for the most recent failure. ### How the key is stored [Section titled “How the key is stored”](#how-the-key-is-stored) Non-empty keys are encrypted at rest, decrypted per use, and never returned to a browser; the settings page shows only the last four characters. If the instance has no encryption key configured, saving a credential fails rather than writing it in cleartext. A deliberately blank key stores no secret. Remove it at any time from the same screen. Removing it puts the organisation back on our inference and back within the free limits. ## Bring your own storage [Section titled “Bring your own storage”](#bring-your-own-storage) Storage is the companion setting. It changes who holds the data rather than who pays for the compute. 1. Go to **Settings → Usage**. 2. Under **Bring your own**, enter your S3-compatible endpoint, region, bucket, access key ID and secret access key. 3. Save. BattleTest verifies the credentials before accepting them. Works with any S3-compatible storage: AWS S3, Cloudflare R2, MinIO, Ceph. ### What changes [Section titled “What changes”](#what-changes-1) When a review finishes, its record is written to your bucket and **our copy is deleted**. The durable form of your review history then exists only in storage you control. After the BattleTest copy expires, report requests read from your bucket using the configured credentials. ### Why verification writes, reads and deletes [Section titled “Why verification writes, reads and deletes”](#why-verification-writes-reads-and-deletes) The check performs all three operations rather than only asking whether the bucket exists. BattleTest performs all three in normal use: write at the end of a run, read when someone opens a report, delete to clean up and to let your own lifecycle rules work. A read-only key passes a bucket-exists check and then fails at the end of your first review, after the work is done and with the local copy already queued for deletion. So the check runs the real operations. The probe object it writes is deleted as part of the check. ## Using both together [Section titled “Using both together”](#using-both-together) They are independent and can be used separately, but together they are the configuration described in our [pricing](https://battletest.dev/pricing): reviews are unlimited because you pay your provider directly, and finished reports are stored in your bucket rather than ours. BattleTest retains review counts and organisation metadata. See [Security & Data Handling](/docs/reference/security-and-data) for retention details. ## API [Section titled “API”](#api) The same settings are available over the API: | Method | Path | Purpose | | -------- | --------------------------------------- | -------------------------------------------------- | | `GET` | `/api/v1/settings/byo?orgId=` | Current key and storage status for an organisation | | `POST` | `/api/v1/settings/byo/model-key` | Save or replace model credentials | | `DELETE` | `/api/v1/settings/byo/model-key?orgId=` | Remove model credentials | | `POST` | `/api/v1/settings/byo/storage` | Save or replace storage credentials | | `DELETE` | `/api/v1/settings/byo/storage?orgId=` | Remove storage credentials | Saving through either route runs the same verification as the settings screen. ## Related [Section titled “Related”](#related) * [Limits and quotas](/docs/reference/limits-and-quotas): what the limits are when you are on our inference * [Fair-use compute limit](/docs/reference/limits-and-quotas#fair-use-compute-limit): what happens as an organisation approaches it * [Security & Data Handling](/docs/reference/security-and-data): what is accessed and retained
# Resolve instance readiness gaps
> Find required services and coverage gaps and open their configuration.
1. Open **Settings → Deployment → Instance readiness**, or select **Review readiness** in the header banner. 2. Address **required** services first. A missing required service prevents its workflow from operating. 3. Review each **coverage gap**. Its impact explains the evidence or tests that are unavailable. 4. Configure optional capabilities that your organisation uses. 5. Follow the row’s action to its provider editor or configuration guide. Provider changes save at runtime; deployment-managed changes may require restarting the API and worker. A model, mail, or identity action opens its specific editor. The source-control action opens Integrations. License actions open the Deployment license section. ## Storage and vulnerability data [Section titled “Storage and vulnerability data”](#storage-and-vulnerability-data) Configure evidence storage and any required mirrors or offline vulnerability database through deployment configuration. The database and Redis connections must be available at startup. See [Self-hosted deployment configuration](/docs/how-to/enterprise-setup) and [Network and egress settings](/docs/reference/network-and-egress) for the settings, mounts, and restart requirements. The **External endpoints** list reports configured or default destinations. It is not an outbound request log and does not establish that network policy permits a destination. Verify policy and network access separately.
# Clean up after a live test
> What BattleTest changed on your target during a live test, and how to undo it. Read the cleanup notes, understand each item, and reconcile against your own records.
A live test exercises your real application, so it can create and change data on the target: test accounts, records, uploaded files, activated items. BattleTest never undoes any of this for you. Instead it gives you a cleanup list of everything it changed, with how to undo each item, so you decide what to remove. ## Where to find it [Section titled “Where to find it”](#where-to-find-it) Every finished live run has its cleanup list in two places: * **Cleanup notes**, the button next to **Written report** on the run. It opens a side panel with the full list. * The **written report** has the same list as a section you can print or export. Both cover the whole run, not one finding. ## What each item tells you [Section titled “What each item tells you”](#what-each-item-tells-you) Each entry describes one change and how to reverse it: * **What changed**, in plain terms, naming the specific thing (for example the survey title, the account, the record id). * **The exact time** it happened, so you can match it to your own logs. * **Who it acted as**, when BattleTest used a test account or token (for example a Telegram id or an email). This is never a password or token, only the identity. * **How to undo it**, written as a step you can act on. Items are marked reversible, manual, or auto-expiring. ## Export it [Section titled “Export it”](#export-it) From the written report you can download the cleanup list as: * A **checklist** inside the Markdown or printed report, with a checkbox per item. * A **JSON file** (`battletest-cleanup-.json`) you can feed to a ticketing system or a script. ## How complete it is [Section titled “How complete it is”](#how-complete-it-is) BattleTest records every change it makes to your target: * **Ordinary web requests** are captured automatically. Anything the test created, updated, deleted, or uploaded over HTTP is on the list. * **Direct data changes** (for example a row written through SQL injection, or a file written through command injection) are recorded by the agent when it makes them, so they appear on the same list. If a run used an attack that can change data directly but recorded no such change, the cleanup notes show a reminder to check your database and files yourself, in case something was changed in a way the list could not capture. ## Recommended order [Section titled “Recommended order”](#recommended-order) 1. Open **Cleanup notes** and read the whole list. 2. Reverse the plain creations first (test accounts, draft records, uploads). 3. For anything that was made live, check your own records for real activity before you remove it. 4. Note any manual items (for example a value that was changed and needs restoring from a backup). 5. If the reminder about direct data changes is shown, check your database and files.
# Manage extension access
> Grant and revoke the hosts a registered extension can contact.
Open **Settings → Policy → Extensions**. Extension grants apply to the deployment; you need testing scope management permission to change them. 1. Find the registered extension and read its requested hosts and capabilities. 2. Select only the requested hosts you intend to grant. 3. Select the extension’s save action and review the resulting allowed hosts. 4. Use **Revoke all** to withdraw its grants. A grant authorises that extension’s requested access. It does not authorise testing a target, replace DNS verification, or prove network connectivity. Configure target authorisation in [Testing scope](/docs/how-to/configure-testing-scope). Review the deployment’s [network and egress policy](/docs/reference/network-and-egress) before granting access to external services.
# Set up GitHub as source control
> Connect a GitHub App to a self-hosted BattleTest deployment.
## Before you start [Section titled “Before you start”](#before-you-start) You need an administrator account in BattleTest and permission to create and install a GitHub App. The connection applies to the deployment. Repository access also depends on the App installation and the BattleTest organisation associated with the GitHub account. ## Create the GitHub App [Section titled “Create the GitHub App”](#create-the-github-app) 1. Open **Settings → Integrations → Add source-control connection**. Choose **GitHub / GitHub Enterprise Server** and name the connection. 2. In GitHub, [register a GitHub App](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app). For GitHub Enterprise Server, create it on your server. 3. Copy the **Webhook URL** from the BattleTest dialog into the App. Enable webhooks and choose a webhook secret. Use the same secret in BattleTest. 4. Grant repository permissions: **Contents: read**, **Metadata: read**, **Pull requests: read and write**, **Checks: read and write**, and **Commit statuses: read and write**. Subscribe to **Pull request** events. 5. Generate an App private key and download the PEM file. Record the **App ID**. ## Save and test the connection [Section titled “Save and test the connection”](#save-and-test-the-connection) 1. Enter the GitHub host for Enterprise Server, or leave it blank for github.com. 2. Enter the App ID, private key, and webhook secret. Pasted secrets are encrypted before storage. Environment and mounted-file references are alternatives. 3. Select **Test connection**. This verifies App credentials and identity. It does not verify repository permissions or webhook delivery. 4. Select **Save connection**. OAuth client credentials are optional and separate from the App’s repository access. If your deployment configures GitHub sign-in, use its configured OAuth callback URL. ## Verify repository access and delivery [Section titled “Verify repository access and delivery”](#verify-repository-access-and-delivery) 1. Install the App on the GitHub account or organisation that owns the repositories. Select the repositories it should review. 2. Associate that GitHub installation with the intended BattleTest organisation. An App credential test alone does not grant your BattleTest account access to repositories. 3. Open **Change Reviews** and check the repository selector. A saved connection and accessible repositories are reported separately. 4. Open or update a pull request in an installed repository. Check **Advanced → Recent deliveries** in the GitHub App for the delivery response, then confirm the review appears in BattleTest. If the credentials pass but no repositories appear, check the installation, selected repositories, and organisation association. See [GitHub App reference](/docs/reference/github-app).
# Configure AI models
> Configure reasoning, supporting, embedding, and optional model connections.
## Choose a model slot [Section titled “Choose a model slot”](#choose-a-model-slot) Open **Settings → Providers**. Configure the model slots used by your deployment: * **Reasoning model**: primary security analysis. * **Fast model**: supporting tasks and extraction. * **Embeddings model**: repository memory and semantic recall. * **Vision model**: optional visual fallback. * **Exploit specialist model**: optional exploit construction and verification. ## Save a connection [Section titled “Save a connection”](#save-a-connection) 1. Select **Configure** or **Set up** on the slot. 2. Choose the adapter and API format. Enter its endpoint, model identifier, and credentials. Use a model identifier offered by that endpoint. 3. Select **Test connection** and review the prerequisite checks. 4. Select **Save connection**. Changes apply across the deployment. Use **Advanced settings** for pricing overrides, reasoning modes, and extra request JSON. These values must match the endpoint’s API. See [Chat model fields](/docs/reference/adapters/openai-chat), [Embedding fields](/docs/reference/adapters/openai-embeddings), and [Vision fields](/docs/reference/adapters/openai-vision). ## Understand effective configuration [Section titled “Understand effective configuration”](#understand-effective-configuration) An environment-configured connection is marked **Environment configuration**. Saving it creates a Settings override. Removing that override restores the environment configuration when one exists. Pasted credentials are encrypted; blank saved credential fields retain their current value. Editing a connection invalidates its current test result. After saving, check the row’s test state and timestamp.
# How to configure notifications
> Choose which BattleTest events notify you and where (email, browser push, or in-app), enable push, and install BattleTest as an app.
BattleTest can notify you per event, per channel. Configure everything at [Settings → Notifications](https://battletest.dev/settings?tab=notifications). For the full list of events and their default channels, see the [Notification Events reference](/docs/reference/notification-events). ## Channels [Section titled “Channels”](#channels) | Channel | Where it arrives | | ---------- | ------------------------------------------------------------------------- | | **Email** | Your account email address | | **Push** | Browser push notifications (requires granting permission in your browser) | | **In-app** | The notification bell in the dashboard | The GitHub PR comment is always posted regardless of these settings. It is the primary output of a PR review, not a notification channel. ## Turn a notification on or off [Section titled “Turn a notification on or off”](#turn-a-notification-on-or-off) Open the Notifications tab Go to [Settings → Notifications](https://battletest.dev/settings?tab=notifications). Events are grouped by category (Change Reviews, Live testing, Billing & Plan, Account & Security, Team & Organization, Integrations, Run Rooms), each with a master toggle. Toggle the channel for an event Each event has an email, push, and in-app toggle. Switch the channel on or off for that event. Screenshot to addSettings → Notifications: events grouped by category, each row with email, push, and in-app toggles and a per-category master toggle. Some security- and money-critical events (a password change, an ownership transfer, a failed payment on an account that still has billing records) keep **email locked on**; you can add channels but not remove email. These are marked in the [Notification Events reference](/docs/reference/notification-events). ## Enable browser push [Section titled “Enable browser push”](#enable-browser-push) Toggle push on for any event The first time you enable a push toggle, your browser asks for notification permission. Allow notifications Click **Allow**. If you previously blocked notifications for the BattleTest domain, re-enable them in your browser’s site settings first, then toggle push again. Push notifications carry a one-tap action where one helps (**Review & approve** on a paused battletest, **View finding** on a confirmed vulnerability) that deep-links straight to the right page. ## Install BattleTest as an app [Section titled “Install BattleTest as an app”](#install-battletest-as-an-app) BattleTest is a Progressive Web App, so you can install it for a dedicated window and reliable push notifications even when no browser tab is open. Open BattleTest in a supported browser Use Chrome, Edge, or another Chromium browser on desktop, or Safari / Chrome on mobile. Install On desktop, click the **install** icon in the address bar (or the browser menu → **Install BattleTest**). On iOS Safari, use **Share → Add to Home Screen**. Grant notification permission Open the installed app and enable a push toggle if you haven’t already, so alerts arrive even when the app is closed. ## See your notifications in the app [Section titled “See your notifications in the app”](#see-your-notifications-in-the-app) The **notification bell** in the dashboard header shows your in-app feed with an unread count. Open it to read recent notifications, jump to the related page from a notification’s action, or mark items as read (individually or all at once). ## Get finding data into your own systems [Section titled “Get finding data into your own systems”](#get-finding-data-into-your-own-systems) Per-channel notifications are for humans. To pull finding data into a spreadsheet, a ticket tracker, or your own reporting, export it instead. See [How to export findings](/docs/how-to/export-findings). ## Not receiving emails? [Section titled “Not receiving emails?”](#not-receiving-emails) Check your spam folder; the first email from `noreply@battletest.dev` is sometimes filtered. Add the address to your contacts. See [Troubleshooting](/docs/how-to/troubleshooting#im-not-receiving-battletest-email-notifications) if it persists.
# Set up email delivery
> Configure an SMTP relay for account and review notifications.
## Before you start [Section titled “Before you start”](#before-you-start) Obtain the relay hostname, port, authentication credentials, and an allowed sender address from your email administrator. Confirm the API can reach the relay. ## Configure the relay [Section titled “Configure the relay”](#configure-the-relay) 1. Open **Settings → Providers → Email transport → Configure**. 2. Enter **SMTP host** and **Port**. Use `587` for STARTTLS or `465` for implicit TLS. Use port `25` only when your internal relay requires it. 3. Set **Implicit TLS** to match the relay. Enter the username and password if authentication is required. 4. Enter **From address**. The relay must allow this sender. 5. Enter a recipient under **Send the test message to**, then select **Test connection**. This sends a test email and checks relay access, authentication, and message acceptance. Message acceptance does not confirm inbox delivery. 6. Select **Save connection**. The connection applies to the entire deployment. Pasted passwords are encrypted at rest. When you reopen the dialog, **saved · leave blank to keep** means the stored password is retained for testing and saving. To replace it, enter a new password. ## Choose notifications [Section titled “Choose notifications”](#choose-notifications) Open **Settings → Notifications** to select your account’s optional notification events and channels. Required account messages use the configured relay. See [Configure notification preferences](/docs/how-to/configure-notifications). A saved connection without a current test appears as **Untested**. If a test fails, check the error, sender address, relay access, and credentials. See [SMTP field reference](/docs/reference/adapters/smtp-mail).
# Set up source control
> Connect repository providers and verify review access.
## Choose a provider [Section titled “Choose a provider”](#choose-a-provider) Open **Settings → Integrations** and select **Add source-control connection**. Name the connection and choose the provider. Connections are shared across the deployment; repository access remains subject to organisation membership and provider permissions. * For a native GitHub App connection, follow [Set up GitHub as source control](/docs/how-to/configure-github-app). * For GitLab, use a token with the `api` scope and select the BattleTest organisation that receives its reviews. See [Connect GitLab](/docs/how-to/connect-gitlab). * For the universal gateway, configure the gateway URL, provider, endpoint, authentication method, and organisation. See [SCM gateway settings](/docs/reference/adapters/go-scm-gateway). ## Verify the connection [Section titled “Verify the connection”](#verify-the-connection) 1. Enter the provider credentials and any repository used for testing. 2. Select **Test connection** and read each result. The checks depend on the selected provider. 3. Select **Save connection**. 4. Configure the provider webhook using the URL and secret in the dialog. Native GitHub uses its GitHub webhook URL; gateway connections use their connection-specific callback. 5. Open **Change Reviews**. Confirm a repository is accessible, then open or update a source change to verify event delivery. A saved connection is configuration. A passing credential test, repository discovery, and webhook delivery verify different parts of the review workflow.
# Set up SSO
> Connect an OpenID Connect identity provider and verify sign-in.
## Register the application [Section titled “Register the application”](#register-the-application) 1. Open **Settings → Providers → Identity / SSO → Configure**. 2. Copy the displayed **Redirect URI** into an OpenID Connect application in your identity provider. The URL must match exactly. 3. Obtain the issuer URL, client ID, and client secret. Confirm the issuer’s discovery endpoint is reachable from the deployment. ## Configure and verify sign-in [Section titled “Configure and verify sign-in”](#configure-and-verify-sign-in) 1. Enter the issuer URL, client ID, and client secret. Pasted secrets are encrypted before storage; a blank saved secret retains the current value. 2. Keep local password sign-in available while verifying the connection. 3. Select **Test connection** to check discovery and provider configuration, then **Save connection**. 4. Use **Verify sign-in** from the dialog, or open `/auth/oidc/login` on your deployment. Sign in with a test identity and verify the account and organisation access. 5. Disable local password fallback only after sign-in succeeds and you have an administrator recovery procedure. Connection testing does not exercise a user’s full sign-in flow. This identity connection applies to the entire deployment. See [OIDC settings](/docs/reference/adapters/oidc-identity).
# Write finding suppression rules
> Create narrow organisation rules and preview their impact.
## Create a rule [Section titled “Create a rule”](#create-a-rule) Open **Settings → Policy → Finding suppression** and select the organisation. You need permission to remove findings. 1. Select **Write a rule**. 2. Enter one or more conditions. Class and category match exact identifiers. Title, description, and location match part of the text. Each condition must contain at least three characters. 3. Read the rule summary. **All filled conditions must match** for a finding to be suppressed. 4. Choose the severity ceiling. New rules default to **medium**; high and critical findings are excluded at this ceiling. 5. Choose the dismissal reason and enter an explanation of at least ten characters. 6. Read the impact preview against recent PR review findings. If no sample exists, the UI says that impact is unknown. 7. If you choose high, critical, or no ceiling, explicitly acknowledge the higher-severity suppression. 8. Select **Save rule**. These rules apply to future PR reviews for the organisation. They do not retroactively dismiss existing findings. Suppressed findings carry the rule, explanation, and author in the review’s dismissal record. ## Review a rule [Section titled “Review a rule”](#review-a-rule) Select **Disable** to stop applying a rule while keeping its record. Select **Enable** to resume it. Delete a rule only when you intend to remove its rule record. For temporary exceptions, record a review date in the reason and disable the rule when that date arrives. See [Manage findings](/docs/how-to/manage-findings) for individual finding actions.
# Configure testing scope
> Restrict an Enterprise organisation to selected domains and network ranges.
Enterprise deployments rely on the customer’s authorisation to test targets under their agreement. DNS verification and individual public-host approvals are not required. Register a target, then choose its testing level when starting a battletest. ## Restrict targets (optional) [Section titled “Restrict targets (optional)”](#restrict-targets-optional) With no scope entries, all registered targets in the organisation are allowed. Add entries to restrict testing to specific domains or network ranges. 1. Open **Settings → Policy → Testing scope** and select the organisation. 2. Select **Add entry**. 3. Choose **Domain** or **Network range**. 4. Enter a domain or CIDR range, add an optional note, and save. A domain entry covers that domain and its subdomains. A CIDR entry covers IP address targets inside that range. For a hostname or local alias, add its domain name. The target picker checks these restrictions before you continue. Launches and scheduled runs check them again. DNS verification does not override a configured restriction. ## Change restrictions [Section titled “Change restrictions”](#change-restrictions) Remove an entry to exclude its targets, unless another entry covers them. Removing the last entry removes the organisation’s scope restrictions. Scope management requires the organisation’s testing scope permission. Member permissions, licensing, testing levels, and human approval requirements still apply. ## Managed deployments [Section titled “Managed deployments”](#managed-deployments) Managed deployments retain their domain ownership checks. Follow [Add a target](/docs/how-to/adding-a-target) to publish the DNS verification record.
# Connect Bitbucket Cloud
> Authorize a Bitbucket account and choose repositories for pull request security reviews.
Connect Bitbucket Cloud from an existing BattleTest workspace. You need permission to connect integrations in that workspace and repository **Admin** access in Bitbucket. This integration supports Bitbucket Cloud. ## Connect your account [Section titled “Connect your account”](#connect-your-account) 1. Select the intended workspace and open **Settings → Integrations**. 2. Choose **Connect Bitbucket Cloud**. 3. Authorize BattleTest in Bitbucket and return to Settings. 4. Choose **Install BattleTest** and install the app in the Bitbucket workspace that owns your repositories. 5. Return to Settings and open **Repositories** beside the connected account. 6. Enable the repositories you want reviewed. App authorization can take a few minutes to arrive after installation. If enabling a repository asks you to install the app, wait briefly and retry. Bitbucket Forge apps require a shared workspace; personal workspaces are not supported. Enabling a repository creates a webhook with a verification secret. Only explicitly enabled repositories can start reviews through this connection. Password users must [verify their email](/docs/how-to/manage-account#verify-your-email) before connecting. If Settings says administrator setup is required, ask the instance administrator to [configure the OAuth consumer](/docs/self-hosting/configure-source-control). Managed users do not need to create their own consumer. ## Posting identity and permissions [Section titled “Posting identity and permissions”](#posting-identity-and-permissions) Comments, inline findings and build statuses use the **BattleTest app’s bot identity**. You do not create or manage a bot account. Your OAuth authorization is used for repository selection and webhook setup; publishing uses a separate app system token. The setup OAuth consumer needs account read, email, repository read, pull request read and webhook permissions. Email permission also supports BattleTest sign-in with a verified address. The installed Forge app separately needs repository and pull request read, user identity read and pull request write. Forge’s write scope includes comment publishing and broader pull request actions; BattleTest uses it for review comments and does not approve or merge changes. Repository code-write permission is not requested. Only enabled repositories in the app’s verified workspace can use its system token. Missing, expired or revoked app authorization stops reviews; BattleTest never falls back to posting as the authorizing user. ## Verify a review [Section titled “Verify a review”](#verify-a-review) Open a non-draft pull request targeting the repository’s default branch. Check for the BattleTest summary, inline comments and the **BattleTest Security Review** build status. Open the review in BattleTest’s **PR Reviews** page. Push a new commit to verify another review starts for the new revision. A skipped, cancelled or inconclusive review uses Bitbucket’s **Stopped** status. It is not a successful security review. Comments include findings that could not be placed inline. BattleTest does not rewrite the pull request description or approve or merge it. Bitbucket’s code search is unavailable through this review adapter. File reads, directory listings and diff analysis still use the repository API. ## Reconnect or disable [Section titled “Reconnect or disable”](#reconnect-or-disable) Use **Reconnect** after revoking provider access. Authorize the same Bitbucket account to preserve its selections. If the BattleTest app was uninstalled, install it again in the same workspace. Its scheduled authorization refresh also supports manual reviews when no recent webhook was received. Turn off a repository in **Repositories** to stop reviews, or choose **Disconnect** to disable the entire connection. If remote webhook cleanup fails, reviews remain disabled locally. Restore provider access and retry disconnect to finish cleanup.
# How to connect a GitHub repository
> Install the BattleTest GitHub App to get automated CVE, secret, and AI code review on every pull request, and manage which repositories are connected.
You connect repositories to a **workspace**, so you need one first. If you just signed up, BattleTest guides you through creating a workspace and then straight into the connect step below; continue from there. If you already have a workspace, start at step one. ## Install the GitHub App [Section titled “Install the GitHub App”](#install-the-github-app) Open the connect flow Go to [the dashboard](https://battletest.dev/dashboard) and click **Connect GitHub**. Choose account and repositories On the GitHub App installation page, select the account or organisation that owns your repositories. Under **Repository access**, choose **Only select repositories** and tick the repositories you want to connect. Complete installation Click **Install & Authorize**. Repositories appear in the BattleTest Repositories list with a *Connected* status. PR reviews begin immediately on the next pull request event. Screenshot to addThe GitHub App installation page: choosing the account and, under Repository access, 'Only select repositories' with repositories ticked. ## Add or remove repositories from an existing installation [Section titled “Add or remove repositories from an existing installation”](#add-or-remove-repositories-from-an-existing-installation) Open repository settings Go to [Dashboard → Repositories](https://battletest.dev/dashboard). Configure the App Click **Configure GitHub App**. Update access Under Repository access, add or remove repositories, then click **Save**. ## Connect repositories in a GitHub organisation [Section titled “Connect repositories in a GitHub organisation”](#connect-repositories-in-a-github-organisation) To install on an organisation you must have organisation **Owner** permissions, or the organisation must allow members to install Apps. If the install button is greyed out, ask an organisation owner to complete the installation. To install on all current and future repositories in an organisation, select **All repositories** on the installation page instead of selecting specific repositories. ## Uninstall the GitHub App [Section titled “Uninstall the GitHub App”](#uninstall-the-github-app) Uninstall from GitHub: **Settings → Applications → Installed GitHub Apps → BattleTest Security → Configure → Uninstall**. BattleTest loses repository access immediately. Historical findings are retained in your account until you delete them.
# Connect GitLab
> Authorize a GitLab account and choose projects for merge request security reviews.
Connect GitLab from an existing BattleTest workspace. You need permission to connect integrations in that workspace and **Maintainer** or **Owner** access to each GitLab project you enable. ## Connect your account [Section titled “Connect your account”](#connect-your-account) 1. Select the intended workspace and open **Settings → Integrations**. 2. Choose **Connect GitLab**. 3. Authorize BattleTest in GitLab and return to Settings. 4. Open **Repositories** beside the connected account. 5. Enable the projects you want reviewed. Enabling a project creates a project-scoped **BattleTest bot** service account and its merge request webhook. You do not need to create a bot account or supply a token. Only explicitly enabled projects can start reviews through this connection. Password users must [verify their email](/docs/how-to/manage-account#verify-your-email) before connecting. If Settings says administrator setup is required, ask the instance administrator to [configure the OAuth application](/docs/self-hosting/configure-source-control). Managed users do not need to create their own application. ## Posting identity [Section titled “Posting identity”](#posting-identity) Review notes, inline discussions and commit statuses use **BattleTest bot**, with a separate credential for each project. Your account authorizes project selection, bot setup and webhook management. Reviews never use your account’s credential. GitLab’s `api` OAuth scope gives the setup connection broad API access as the authorizing account. The bot has Developer access to its project and an encrypted token that BattleTest rotates automatically before expiry. Settings shows the project’s reviewer identity. Project service accounts require GitLab **18.11 or later** without feature flags. GitLab Free limits their number; if the instance cannot create one, enabling reviews reports the problem and does not fall back to posting as you. See [GitLab service accounts](https://docs.gitlab.com/user/profile/service_accounts/). ## Verify a review [Section titled “Verify a review”](#verify-a-review) Open a non-draft merge request targeting the project’s default branch, then check its BattleTest summary and commit status. Open the same review in **PR Reviews** in BattleTest. Push a new commit to check that another review starts for the new revision. Draft and closed merge requests cancel an in-flight review. Becoming ready starts a review, including when no new commit was pushed. Inline findings use GitLab diff discussions. Commit statuses do not carry the full finding report; read the summary for the evidence and review coverage. ## Reconnect or disable [Section titled “Reconnect or disable”](#reconnect-or-disable) Use **Reconnect** after revoking access or when authorization expires beyond recovery. Authorize the same GitLab account to preserve its project selections. If the bot credential was revoked or expired, disable and re-enable that project after reconnecting to restore its bot access. Turn off a project in **Repositories** to stop reviews, or choose **Disconnect** to disable all projects on that connection. BattleTest removes its webhooks and revokes its bot tokens while preserving review history. If GitLab is unavailable, review access is revoked locally and any remaining provider cleanup is reported. Restore provider access and retry disconnect to finish cleanup.
# Configure a self-hosted deployment
> Save application configuration with the setup wizard or manage it through your environment.
The enterprise image opens `/setup` until you finish setup. Signing in or installing a license does not complete the wizard automatically. Enter the first-run token printed in the API log, or sign in with an existing account. Review your configuration and select **Finish setup**. Application settings and provider connections are stored in Postgres. Pasted secrets are encrypted with the deployment’s `GITHUB_TOKEN_ENCRYPTION_KEY`, using the same storage as Settings → Providers. Keep that encryption key stable across the API and worker, restarts, and database restores. You do not need to copy the wizard’s settings into Compose or an environment file. Empty secret fields retain existing credentials. Provider changes and application settings apply at runtime; changes to storage or tracing clients are saved and require an API and worker restart. The wizard identifies these changes when saved. Restarting reads the saved values from Postgres. After completion, `/setup` redirects to Settings and the Setup entry disappears. The first-run token stops working. Provider connections remain editable in Settings → Providers. ## Environment configuration [Section titled “Environment configuration”](#environment-configuration) Environment variables remain an alternative for automated deployment. They supply defaults until a value is explicitly saved through setup; saved application values take precedence. Infrastructure needed before the application can start remains deployment-managed: database and Redis connections, the encryption key, mounted files, container networking, and shared sidecar credentials. To skip the wizard on a deployment that you have already configured, set this on the API:
```dotenv
BATTLETEST_SKIP_SETUP=true
```
The supplied enterprise Compose file forwards this variable. Its default is `false`. Removing it opens setup again only if setup has never been completed in the database. It does not grant a license or bypass authentication. Examples of optional application environment configuration:
```dotenv
LICENSE_FILE=/run/secrets/battletest-license
EGRESS_MODE=airgap
MAIL_PROVIDER=smtp
SMTP_HOST=smtp.internal.example
SMTP_PORT=587
SMTP_USER=battletest
SMTP_PASS=your-relay-password
SMTP_FROM=battletest@example.com
```
Supply environment secrets using your deployment platform’s secret management. Configure a private CA bundle with `NODE_EXTRA_CA_CERTS` and its mount on both API and worker. Configure `TURN_PUBLIC_IP` on coturn itself. These are shown as deployment-managed values in setup because saving an application setting cannot mount a host file or change another container’s networking.
# How to export findings for a security audit or SOC 2
> Export BattleTest scan reports and PR findings as PDF or JSON for SOC 2 evidence, security questionnaires, auditor review, and ticketing imports.
## Export a single scan report [Section titled “Export a single scan report”](#export-a-single-scan-report) Open the scan run Go to [Tests](https://battletest.dev/tests), select the environment, and click on the scan run you want to export. Export On the run detail page, click **Export**. Choose format Choose **PDF** for a formatted report suitable for sharing with auditors, or **JSON** for integration with ticketing systems or security dashboards. Screenshot to addThe Export control on a run detail page with the PDF / JSON format choice. ## Export PR review findings [Section titled “Export PR review findings”](#export-pr-review-findings) Open the PR run Go to the [Dashboard](https://battletest.dev/dashboard), select a repository, and click on a PR run to open its detail page. Export Click **Export** and select the format. To export findings across all PRs for a repository, go to Dashboard → the repository → **Export all findings**. This produces a JSON file of all finding records for that repository. ## What the PDF report includes [Section titled “What the PDF report includes”](#what-the-pdf-report-includes) * Scan date, target, and scope (permission tier) * Overall risk score * All findings with severity, location, description, and remediation * Attack surface summary (endpoints discovered, tests run) * Comparison with previous scan (new, resolved, persistent, regression) The PDF is formatted for sharing with external parties: customers, auditors, security questionnaire respondents. It does not include internal metadata (run IDs, database IDs). ## Evidence of continuous testing for SOC 2 [Section titled “Evidence of continuous testing for SOC 2”](#evidence-of-continuous-testing-for-soc-2) SOC 2 Type II requires evidence that security controls operated continuously over the audit period, not just at a point in time. To build your evidence package: * Export monthly JSON findings dumps from the Dashboard for each repository * The PR findings history shows security review was applied to every code change throughout the period If your auditor needs a specific report format or additional metadata, contact .
# How to respond to an exposed credential
> What to do after committing an API key, AWS secret, or database password to git: rotate the credential, remove it from code, and purge it from history.
Caution If you committed a credential and then removed it in a later commit, it is still exposed. Anyone who has cloned the repository can read it from git history. Rotation is mandatory. This guide covers what to do when BattleTest flags a committed secret, or when you discover one yourself. The steps apply to any credential: AWS access keys, API tokens, database passwords, private keys, or service account credentials. ## Step 1: Rotate the credential immediately [Section titled “Step 1: Rotate the credential immediately”](#step-1-rotate-the-credential-immediately) Before anything else, revoke the exposed credential and issue a new one. Do this now, before touching git history. If the key has been in a public or shared repository, assume it has already been read. | Credential type | Where to rotate | | ---------------------------- | --------------------------------------------------------------- | | AWS access key | IAM → Users → Security credentials → Make inactive → Create new | | GitHub personal access token | Settings → Developer settings → Personal access tokens → Revoke | | Stripe API key | Dashboard → Developers → API keys → Roll secret key | | SendGrid API key | Settings → API Keys → Revoke | | Database password | Your database admin panel or `ALTER USER` command | | Other SaaS API key | Check the service’s security or developer settings | After rotation, store the new credential in your deployment secrets manager (environment variables, Vault, AWS Secrets Manager, etc.). Do not put it in code. ## Step 2: Remove the credential from current code [Section titled “Step 2: Remove the credential from current code”](#step-2-remove-the-credential-from-current-code) If the credential is still in your codebase (not just in history), remove it: 1. Find all occurrences: `grep -r "AKIAIOSFODNN7EXAMPLE" .` (use your actual key prefix) 2. Replace with an environment variable reference: `process.env.AWS_ACCESS_KEY_ID` 3. Add the secret to your secrets manager or `.env` file (not committed) 4. Add `.env` to `.gitignore` if it isn’t already Commit the removal. ## Step 3: Purge the credential from git history [Section titled “Step 3: Purge the credential from git history”](#step-3-purge-the-credential-from-git-history) Removing a secret from current code doesn’t remove it from git history. You need to rewrite history. **Option A: `git filter-repo` (recommended)**
```bash
# Install
pip install git-filter-repo
# Remove the file that contained the secret
git filter-repo --path path/to/secret/file --invert-paths
# Or replace the secret string everywhere in history
git filter-repo --replace-text <(echo 'ACTUAL_SECRET_VALUE==>REDACTED')
```
**Option B: BFG Repo Cleaner (faster for large repos)**
```bash
# Download BFG from https://rtyley.github.io/bfg-repo-cleaner/
# Create a file listing secrets to remove
echo 'AKIAIOSFODNN7EXAMPLE' > secrets.txt
# Run BFG
java -jar bfg.jar --replace-text secrets.txt your-repo.git
# Clean up
cd your-repo
git reflog expire --expire=now --all && git gc --prune=now --aggressive
```
Both tools rewrite every commit in your history that contained the secret. ## Step 4: Force-push to the remote [Section titled “Step 4: Force-push to the remote”](#step-4-force-push-to-the-remote) After rewriting history locally, push the rewritten history to the remote:
```bash
git push --force --all
git push --force --tags
```
Caution Force-pushing rewrites the shared history for everyone who has cloned the repository. Anyone with a local clone will need to re-clone or run `git fetch --all && git reset --hard origin/main`. If this is an organisation repository with branch protection rules preventing force-push, you’ll need to temporarily disable protection rules or coordinate with an admin. ## Step 5: Notify anyone who has access to the repository [Section titled “Step 5: Notify anyone who has access to the repository”](#step-5-notify-anyone-who-has-access-to-the-repository) If the repository is or was public, or if it’s a shared private repository, assume the credential has been seen. Notify your team that they need to re-clone and that the old credential is revoked. If the key had write or admin access to a production system, check your audit logs for unexpected activity during the period the key was exposed. ## Step 6: Prevent recurrence [Section titled “Step 6: Prevent recurrence”](#step-6-prevent-recurrence) BattleTest scans every PR for secrets automatically, so a credential that lands in a pull request is flagged before it merges. To avoid future incidents: * **Use environment variables.** Never hardcode credentials, even in config files committed to a private repo. * **Add pre-commit hooks.** Tools like [`detect-secrets`](https://github.com/Yelp/detect-secrets) or [`gitleaks`](https://github.com/gitleaks/gitleaks) can prevent secrets from being committed in the first place. * **Connect BattleTest to your repositories.** Every PR gets scanned automatically. See [Get your first PR security review](/docs/tutorials/quick-start).
# Install or renew a deployment license
> Review the signed license and replace it on a self-hosted deployment.
1. Open **Settings → Deployment → Deployment license**. Review the license state and expiry or grace period. 2. Obtain the signed license for the deployment. 3. Follow the card’s installation instructions: supply the token through `LICENSE_KEY`, or mount a license file and set `LICENSE_FILE` to its path. 4. Keep the signing verification configuration consistent with the license issuer. Recreate the API and worker after changing environment configuration. Mounted-file changes are detected without rebuilding the image. 5. Return to the license card and confirm it is valid. Review **Instance readiness** for remaining prerequisites. Treat the license token as a deployment secret. Supply it through your platform’s secret management. Installing a license does not complete setup; finish first-run configuration separately. See [Self-hosted deployment configuration](/docs/how-to/enterprise-setup) for optional environment configuration and setup completion.
# How to review, dismiss, and track findings
> Triage BattleTest findings from PR reviews and live scans: dismiss false positives with a reason, mark issues resolved, and track open findings over time.
## Where findings appear [Section titled “Where findings appear”](#where-findings-appear) Findings from PR reviews appear in two places: as a comment on the GitHub pull request from `battletest[bot]`, and in the run detail page in the BattleTest dashboard (linked from the PR comment). Findings from live scans appear only in the dashboard. Both types share the same lifecycle: **Open → Dismissed** or **Resolved**. Screenshot to addThe run detail page in the dashboard: the findings list with severity, status, and the dismiss / resolve controls. ## Dismiss a false positive [Section titled “Dismiss a false positive”](#dismiss-a-false-positive) When a finding is not a real vulnerability (for example, a secret detected in a test fixture file, or an injection pattern in documentation), dismiss it so it doesn’t affect future risk scores. Open the finding Open the run detail page in the dashboard and click on the finding to expand it. Dismiss Click **Dismiss**. Select a reason Choose: *False positive*, *Accepted risk*, or *Not applicable*. Add a note (optional) Optionally add a note explaining the decision. Dismissed findings are excluded from the risk score on subsequent scans of the same code path or endpoint. They’re also excluded from regression detection: a dismissed finding will not trigger a regression alert if it reappears. ## Mark a finding as resolved [Section titled “Mark a finding as resolved”](#mark-a-finding-as-resolved) You don’t need to mark findings as resolved manually. BattleTest detects resolution automatically: if the finding no longer appears in the next scan of the same PR branch or target, it’s automatically classified as *Resolved*. For live scan findings, trigger a new scan after deploying the fix. In the comparison view, the fixed finding will appear under *Resolved* in the scan diff. Resolved findings contribute to the regression detection baseline. If the same vulnerability pattern reappears in a future scan, it’s flagged as a regression (not a new finding) and the risk score includes a regression penalty. ## View all open findings for a repository [Section titled “View all open findings for a repository”](#view-all-open-findings-for-a-repository) Select repository Go to [Dashboard → Repositories](https://battletest.dev/dashboard) and select a repository. Open Findings tab Click the **Findings** tab. This shows all open findings across all PR runs for that repository, sorted by severity. Filter by severity, source (CVE, Secret, AI, etc.), and status. ## View all open findings for a live target [Section titled “View all open findings for a live target”](#view-all-open-findings-for-a-live-target) Select environment Go to [Tests](https://battletest.dev/tests) and select the environment. Open Findings tab Click the **Findings** tab. This shows persistent and new findings across all scans of the target, with scan-over-scan comparison available on each finding. ## Export findings [Section titled “Export findings”](#export-findings) See [how to export findings for an audit or SOC 2](/docs/how-to/export-findings).
# How to configure out-of-band testing
> Configure the bundled callback catcher or self-hosted interactsh, including DNS and network requirements.
Some vulnerabilities never tell you they worked. Blind SSRF, XXE, out-of-band SQL injection and blind XSS all produce the same response as a target that is not vulnerable at all. The only way to observe them is to make the target call a server you control, and then watch that server. That server is the out-of-band (OAST) backend. Caution Without an OAST backend, blind-vulnerability tests are unavailable. This limitation is recorded in the event stream and signed export. ## Why this is not simply “on” [Section titled “Why this is not simply “on””](#why-this-is-not-simply-on) The out-of-band hostname is embedded **in the payload**. It is therefore your target (not the scanner) that makes the callback. The template scanner’s own default is a set of public servers operated by its vendor. Left alone, that means a machine inside your network resolves and connects to a third-party host, disclosing that the machine exists, that it is reachable, and that it processes attacker-supplied input. A packet capture of the scanner would show none of it, because the scanner is not the thing making the call. **BattleTest never uses that default in any mode.** The image overrides it. This page is about which backend you get instead. ## The three modes [Section titled “The three modes”](#the-three-modes) Set `OAST_MODE` in your deployment’s environment. | Mode | Agent blind-vuln tools | Template-scanner OAST templates | DNS you must own | | ------------------- | --------------------------- | ------------------------------- | --------------------------------- | | `catcher` (default) | Yes | No | Nothing, or one wildcard A record | | `interactsh` | Yes | Yes | A whole domain, delegated | | `off` | No: reported as unavailable | No | Nothing | ### `catcher`: the default [Section titled “catcher: the default”](#catcher-the-default) The deployment ships a small, self-hosted callback catcher (`oob-catcher`). It is receive-only, has no egress of its own, and speaks a simple register-and-poll contract that BattleTest’s own agents use directly. **What it detects.** Everything the agents probe for: blind SSRF, XXE, out-of-band SQL injection, blind XSS, DNS exfiltration (anywhere an agent constructs the payload itself). **What it does not.** The template scanner (nuclei) does not speak this protocol and cannot be pointed at it. Its OAST template set stays excluded. Use self-hosted interactsh to enable nuclei OAST templates. **What it costs you.** At most one DNS record: * A single A record plus `OOB_PUBLIC_URL` gives path-token correlation (`https://oob.example.com/c/`). Enough for payloads that take a URL. * A wildcard A record (`*.oast.example.com` → the catcher) additionally catches payloads that take a bare hostname rather than a URL, which is the common shape for XXE and for DNS-only exfiltration. Set `OOB_DOMAIN` to the wildcard’s base. The address must be reachable **by your targets**. Check it from a host outside your network. An internal-only catcher observes nothing, and the run still looks clean. ### `interactsh`: the full capability [Section titled “interactsh: the full capability”](#interactsh-the-full-capability) A self-hosted [interactsh](https://github.com/projectdiscovery/interactsh) server, run by you. The template scanner is pointed at it with `-iserver` and `-itoken`, which restores its entire OAST template set. Everything `catcher` does, plus the template scanner’s blind-vulnerability templates. It also captures SMTP and LDAP interactions, which the catcher does not. ### `off`: no out-of-band testing [Section titled “off: no out-of-band testing”](#off-no-out-of-band-testing) The template scanner runs with OAST disabled, and the agent’s out-of-band tools report themselves **unavailable** rather than returning an empty result that reads like a negative finding. The run emits a degradation notice naming the classes that went untested, and that notice is part of the signed export. Choose this deliberately. Use it when your deployment cannot expose any callback surface. ## What running interactsh actually requires [Section titled “What running interactsh actually requires”](#what-running-interactsh-actually-requires) Caution **interactsh cannot run on a delegated subdomain.** It runs its own authoritative DNS server for the domain you give it. You cannot hand it `oast.example.com` from your existing zone; you delegate the **nameservers** of a domain to it, which means that domain’s DNS stops being served by your provider and starts being served by this container. If your DNS is centrally managed, plan this with your network team before you start. Concretely, four things: **1. A domain you can repoint.** Not a subdomain: a registered domain whose nameserver records you can change at the registrar. In practice this is a domain bought for the purpose. Whatever else that domain was serving stops being served the moment you delegate it. **2. A host with a stable public IP,** reachable from your targets on ports 53 (TCP and UDP), 80, 443, 25, 587 and 389. **3. Those ports free on that host.** Port 80 and 443 rule out sharing with a reverse proxy; port 25 collides with BattleTest’s own test-mailbox catcher if you run one. In practice interactsh wants a machine of its own. **4. Ports that stay as they are.** The port is implied by the payload the target receives (`http://.` is port 80), so remapping on the host side breaks correlation silently, with no error to show for it. ### Delegating the domain [Section titled “Delegating the domain”](#delegating-the-domain) Create host (glue) records at your registrar Add two hostnames on the domain, `ns1` and `ns2`, both pointing at your server’s public IP address. Registrars call this “host records”, “glue records”, or “personal nameservers”; the wording varies, the operation does not. Both may point at the same IP. Two records exist because most registrars require two nameservers. Repoint the domain's nameservers In the domain’s DNS settings, choose to use your own nameservers and enter `ns1.YOUR-DOMAIN` and `ns2.YOUR-DOMAIN`. Delegation makes your interactsh server authoritative for the domain. DNS propagation can take up to 48 hours. Configure the deployment
```bash
OAST_MODE=interactsh
OAST_INTERACTSH_DOMAIN=your-domain.example
OAST_INTERACTSH_IP=203.0.113.10 # the server's PUBLIC IP
OAST_INTERACTSH_TOKEN=
OAST_INTERACTSH_URL=https://your-domain.example
```
`OAST_INTERACTSH_IP` is **not** optional. Left unset, interactsh tries to discover its own address by fetching a public IP-echo service and by opening a socket toward one of its vendor’s hosts: two undeclared outbound calls at startup, and a hard exit when both fail. Worse, inside a container the address it would settle on is the container’s, so every DNS answer would point your targets at a private address and no callback would ever arrive. `OAST_INTERACTSH_TOKEN` is **not** optional either. A server started without one accepts registration from anyone who finds it, which makes it an open relay running on your infrastructure under your domain. The service refuses to start without a token rather than defaulting to open. Start it
```bash
docker compose -f docker-compose.prod.yml --profile interactsh up -d interactsh
```
The service sits behind a compose profile so a deployment that has not done the delegation never starts a server that cannot work. Verify a callback actually lands From a host **outside** your network, not the deployment host:
```bash
dig +short test123.your-domain.example @ns1.your-domain.example
curl -sv http://test123.your-domain.example/ 2>&1 | head -20
```
The DNS query should resolve to your server’s public IP, and the HTTP request should reach it. If either fails, the delegation has not propagated or a firewall is in the way, and every blind-vulnerability result the deployment produces from here will be a false negative. ### Air-gapped and internal-CA deployments [Section titled “Air-gapped and internal-CA deployments”](#air-gapped-and-internal-ca-deployments) interactsh obtains a TLS certificate through ACME by default, which reaches Let’s Encrypt. On a network that cannot, supply your own:
```bash
OAST_INTERACTSH_ARGS=-skip-acme -cert /tls/tls.crt -privkey /tls/tls.key
OAST_INTERACTSH_CERT_PATH=/etc/ssl/internal/oast-wildcard.crt
OAST_INTERACTSH_KEY_PATH=/etc/ssl/internal/oast-wildcard.key
```
The certificate must be a **wildcard** for the domain. Correlation hostnames are randomly generated subdomains, so a single-name certificate makes every HTTPS and SMTPS interaction fail its handshake, which looks identical to a target that never called back. `-skip-acme` on its own, with no certificate supplied, disables TLS entirely. HTTP, DNS and SMTP interactions still work; HTTPS ones do not. Storage is on disk (`-disk`) so a redeploy does not discard interactions a run is still polling for. ## Network requirements [Section titled “Network requirements”](#network-requirements) Confirm these requirements before delegating the domain: * **The domain is single-purpose.** Buy one for this. It serves no mail, no web presence, and nothing else depends on its DNS. * **Callback destination.** Self-hosting keeps callback traffic on infrastructure you control. * **The server is receive-only.** It answers DNS and accepts connections. It initiates nothing outbound once `OAST_INTERACTSH_IP` is set. * **Without delegation.** The bundled catcher supports agent-driven tests. Nuclei OAST templates require interactsh. The report records which backend was used. ## What a run tells you [Section titled “What a run tells you”](#what-a-run-tells-you) If out-of-band testing is unavailable, the run emits a coverage notice before testing. The notice lists the vulnerability classes that will not be tested. That notice is recorded inside the signed manifest content, so the caveat cannot be separated from the report it qualifies. ## Related [Section titled “Related”](#related) [Network and egress](/docs/reference/network-and-egress) [Every host the deployment can contact, and how each is closed or pointed at your own infrastructure.](/docs/reference/network-and-egress) [Prepare a target](/docs/how-to/adding-a-target) [Authorisation, reachability and permission tiers before a live run.](/docs/how-to/adding-a-target)
# How to configure PR review
> Monitor BattleTest review usage, understand which branches get reviewed, and control what BattleTest posts to your pull requests.
PR review usage lives in [Settings → Usage](https://battletest.dev/settings?tab=usage), and what BattleTest posts to your PRs is controlled from [Settings → Integrations](https://battletest.dev/settings?tab=integrations). For the limit figures themselves, see [Limits & Quotas](/docs/reference/limits-and-quotas#pr-review-limits). ## Monitor your review usage [Section titled “Monitor your review usage”](#monitor-your-review-usage) Open the Usage tab Go to [Settings → Usage](https://battletest.dev/settings?tab=usage). Read the meters Each limit has a live meter showing current usage and when its window resets. Hourly windows reset on a rolling basis; monthly caps reset on the first of each UTC month. Reset times are shown in your local timezone. ## Which branches get reviewed [Section titled “Which branches get reviewed”](#which-branches-get-reviewed) BattleTest reviews pull requests that **target your repository’s default branch**. This is zero-config and applies automatically. A PR opened against any other base branch (for example, an intermediary `feature-A → feature-B` merge) is skipped so it doesn’t burn your review limits. To make the review a required merge gate on the default branch, add a GitHub branch protection rule. See [How to require security review before merging](/docs/how-to/require-security-review). ## What happens when you hit your limit [Section titled “What happens when you hit your limit”](#what-happens-when-you-hit-your-limit) When your advanced review limit is reached, BattleTest never drops the PR event; it falls back to a [basic scan](/docs/concepts/how-pr-review-works#advanced-vs-basic-reviews) so every PR still gets at least partial coverage. The PR comment notes that the basic scan ran. If both the advanced limit and the basic fallback limit are exhausted in the same window, BattleTest posts a non-blocking GitHub check run with the reset time; no PR event is ever silently dropped, and running out of quota never blocks a merge. ## Control what BattleTest posts to your PRs [Section titled “Control what BattleTest posts to your PRs”](#control-what-battletest-posts-to-your-prs) You can change how much BattleTest writes to each pull request from [Settings → Integrations](https://battletest.dev/settings?tab=integrations) → **PR review output**: * **Full** (default): inline comments on each finding plus a summary report. * **Minimal**: a one-line verdict with a link to the full report, no inline diff comments. * **None**: nothing is posted to the PR. The `BattleTest Security Review` check run and the report dashboard still update. ## Configure notifications [Section titled “Configure notifications”](#configure-notifications) See [How to configure notifications](/docs/how-to/configure-notifications). ## When you regularly hit a limit [Section titled “When you regularly hit a limit”](#when-you-regularly-hit-a-limit) Hitting the hourly or monthly advanced-review limit is not an error state. A basic scan runs instead, the pull request still gets coverage, and the PR comment says which mode ran. The limits and how each window resets are documented in [Limits & Quotas](/docs/reference/limits-and-quotas).
# How to require security review before merging
> Use GitHub branch protection rules to block pull requests from merging until a BattleTest security review has completed; no CI pipeline changes required.
BattleTest posts findings as a PR comment and sets a commit status check, but does not block merges by default. To make the security review a required gate: ## Add the required status check in GitHub [Section titled “Add the required status check in GitHub”](#add-the-required-status-check-in-github) Open branch protection rules In your GitHub repository, go to **Settings → Branches**. Edit the rule Under **Branch protection rules**, click **Edit** on your default branch rule (or **Add rule** if none exists). Enable required status checks Enable **Require status checks to pass before merging**. Add BattleTest check In the search field, type `BattleTest Security Review` and select it from the results. Save Click **Save changes**. Screenshot to addGitHub's merge box on a pull request with the 'BattleTest Security Review' check listed as a required status check. After this, a PR is blocked from merging when: * the review hasn’t completed yet, or * the review encountered an internal error (`failure`), or * the review completed with a **risk score of 40 or higher**: the “request changes” verdict (`action_required`). PRs whose risk score is below 40 (clean, or only a low-severity / likely false-positive finding) pass as `success` and merge normally. This means requiring the check gates the PRs that warrant changes without blocking on trivial findings. See [Risk score](/docs/concepts/risk-score#score-bands) for what drives the score. ## Adjust the blocking threshold [Section titled “Adjust the blocking threshold”](#adjust-the-blocking-threshold) The check blocks at risk score 40 and above, matching the verdict shown in the PR comment. A configurable per-repo threshold (for example, block only on CRITICAL findings) is on the roadmap; contact if you need a custom threshold today. If you want the review visible but never blocking, don’t add it as a required status check; the comment and check still appear on every PR. ## Which branches are reviewed [Section titled “Which branches are reviewed”](#which-branches-are-reviewed) BattleTest reviews pull requests that target your repository’s **default branch**. PRs opened against other base branches are skipped so they don’t burn your review limits. The required check is meaningful on merges into the default branch, which is where you want the merge gate. See [How to configure PR review](/docs/how-to/pr-settings#which-branches-get-reviewed). ## Re-trigger a review that didn’t run [Section titled “Re-trigger a review that didn’t run”](#re-trigger-a-review-that-didnt-run) If a PR was opened before the GitHub App was installed, or if the webhook delivery failed, the review won’t have run. Re-trigger it by: * Pushing any new commit to the PR branch, or * Closing and reopening the PR
# Troubleshooting
> Fix common BattleTest problems: missing PR review comments, stuck GitHub status checks, repositories not appearing, scans stuck Running, lost notifications.
## BattleTest PR review comment not appearing on GitHub pull request [Section titled “BattleTest PR review comment not appearing on GitHub pull request”](#battletest-pr-review-comment-not-appearing-on-github-pull-request) ## BattleTest Security Review check stuck on “pending” in GitHub [Section titled “BattleTest Security Review check stuck on “pending” in GitHub”](#battletest-security-review-check-stuck-on-pending-in-github) The check shows as pending either because the review is still running (expected for 2–4 minutes after the PR event) or because the webhook delivery failed before BattleTest received the event. Check **Settings → GitHub Apps → BattleTest Security → Recent Deliveries** in your repository. If no `pull_request` delivery is listed for the PR that’s stuck, the webhook didn’t fire; re-trigger it by pushing any new commit to the PR branch or closing and reopening the PR. ## GitHub App installed but no repositories appear in BattleTest [Section titled “GitHub App installed but no repositories appear in BattleTest”](#github-app-installed-but-no-repositories-appear-in-battletest) After installation, check the GitHub App configuration page: **Settings → GitHub Apps → BattleTest Security → Configure**. Under Repository access, confirm at least one repository is selected. The default GitHub App flow sometimes leaves the selection empty; this is a GitHub UI issue on initial install. ## A live scan failed or is stuck in “Running” for more than 30 minutes [Section titled “A live scan failed or is stuck in “Running” for more than 30 minutes”](#a-live-scan-failed-or-is-stuck-in-running-for-more-than-30-minutes) ## Secret was committed to git and BattleTest flagged it: what do I do? [Section titled “Secret was committed to git and BattleTest flagged it: what do I do?”](#secret-was-committed-to-git-and-battletest-flagged-it-what-do-i-do) See [How to respond to an exposed credential](/docs/how-to/exposed-secrets) for a step-by-step guide: rotating the credential, removing it from current code, purging it from git history, and preventing recurrence. ## BattleTest not scanning pull requests on feature branches [Section titled “BattleTest not scanning pull requests on feature branches”](#battletest-not-scanning-pull-requests-on-feature-branches) This is expected. BattleTest reviews pull requests that target your repository’s **default branch** only; PRs opened against other base branches (such as `feature-A → feature-B`) are skipped by design so they don’t burn your review limits. Reviews run on the PR that merges the work into your default branch, which is where the merge gate matters. See [which branches get reviewed](/docs/how-to/pr-settings#which-branches-get-reviewed). ## I’m not receiving BattleTest email notifications [Section titled “I’m not receiving BattleTest email notifications”](#im-not-receiving-battletest-email-notifications) Check [Settings → Notifications](https://battletest.dev/settings?tab=notifications) and confirm email is enabled. Check your spam folder; the first email from `noreply@battletest.dev` is sometimes filtered. Add the address to your contacts to prevent future filtering. ## GitHub App was uninstalled accidentally [Section titled “GitHub App was uninstalled accidentally”](#github-app-was-uninstalled-accidentally) Reinstall it from **Dashboard → Repositories → Connect GitHub**. Your findings history and settings are preserved; nothing is deleted on uninstall, only the GitHub access is revoked. ## Still stuck? [Section titled “Still stuck?”](#still-stuck) Email with your repository name or scan run ID. We’ll investigate directly and respond within one business day.
# GitHub / GitHub Enterprise Server
> A GitHub App connection to github.com or a self-hosted GHES instance.
A GitHub App connection to github.com or a self-hosted GHES instance. This provider fills the **scm** slot. Configure it in **Settings → Integrations**, or during first-run setup. ## Settings [Section titled “Settings”](#settings) | Key | Field | Type | Required | Notes | | --------------- | ------------------- | ------------------- | -------- | ------------------------------------------------------------------------------------------- | | `host` | GitHub host | text | No | GHES hostname, e.g. github.yourco.com. Blank = github.com. Derives api/web/uploads/graphql. | | `appId` | GitHub App ID | text | Yes | — | | `privateKey` | App private key | secret or reference | Yes | Paste the PEM private key (encrypted at rest), or use env:NAME / file:/path. | | `webhookSecret` | Webhook secret | secret or reference | No | HMAC secret for this instance’s inbound webhooks. | | `clientId` | OAuth client ID | text | No | — | | `clientSecret` | OAuth client secret | secret or reference | No | — | ## What the connection test checks [Section titled “What the connection test checks”](#what-the-connection-test-checks) Select Test connection to run a live test against the credentials you supplied. Saving validates and stores the configuration. Every check below is evaluated against the real service, not inferred from the configuration. | Check | Outcome | | ------------------------------------------------- | --------- | | Authenticate as the GitHub App (App-JWT accepted) | Must pass | | Resolve the App identity (GET /app) | Must pass | *Adapter id: `github-scm`.*
# GitLab / GitLab self-managed
> An access-token connection to gitlab.com or a self-managed GitLab. Merge requests get a summary note, inline discussions and a commit status. GitLab has no per-line annotations and no blocking review, so those degrade. See the SCM provider docs.
An access-token connection to gitlab.com or a self-managed GitLab. Merge requests get a summary note, inline discussions and a commit status. GitLab has no per-line annotations and no blocking review, so those degrade. See the SCM provider docs. This provider fills the **scm** slot. Configure it in **Settings → Integrations**, or during first-run setup. ## Settings [Section titled “Settings”](#settings) | Key | Field | Type | Required | Notes | | --------------- | -------------------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `host` | GitLab host | text | No | Self-managed hostname, e.g. gitlab.yourco.com. Blank = gitlab.com. | | `accessToken` | Access token | secret or reference | Yes | Group or project access token with api scope. Secret reference (env:NAME / file:/path). Reviews are posted as whichever user owns it. | | `orgId` | Organisation | text | Yes | Every merge request reviewed through this connection bills and attributes to this organisation. GitLab groups are not linked individually. | | `webhookSecret` | Webhook secret token | secret or reference | No | The token GitLab sends as X-Gitlab-Token on this instance’s inbound webhooks. | ## What the connection test checks [Section titled “What the connection test checks”](#what-the-connection-test-checks) Select Test connection to run a live test against the credentials you supplied. Saving validates and stores the configuration. Every check below is evaluated against the real service, not inferred from the configuration. | Check | Outcome | | ---------------------------------------------- | --------- | | Authenticate with the access token (GET /user) | Must pass | | Reach the v4 REST API on this host | Must pass | *Adapter id: `gitlab-scm`.*
# Universal SCM gateway (go-scm)
> One stateless gateway for GitHub, GitLab, Bitbucket Cloud, Bitbucket Data Center, Azure DevOps, Gitea, Gogs, Gitee, and Harness Code. BattleTest owns authentication; the enterprise supplies endpoint and secret references.
One stateless gateway for GitHub, GitLab, Bitbucket Cloud, Bitbucket Data Center, Azure DevOps, Gitea, Gogs, Gitee, and Harness Code. BattleTest owns authentication; the enterprise supplies endpoint and secret references. This provider fills the **scm** slot. Configure it in **Settings → Integrations**, or during first-run setup. ## Settings [Section titled “Settings”](#settings) | Key | Field | Type | Required | Notes | | ---------------- | -------------------------- | --------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | | `providerId` | SCM provider | one of: `github`, `gitlab`, `bitbucket`, `bitbucket_server`, `azure`, `gitea`, `gogs`, `gitee`, `harness` | Yes | Default: `github`. | | `endpoint` | API endpoint | URL | No | Leave blank for the provider’s official endpoint. Required for self-hosted providers and custom domains. | | `webBase` | Web endpoint | URL | No | Browser-facing base URL used for change links. Leave blank when it matches the official provider. | | `authMode` | Authentication | one of: `github_app`, `bearer`, `basic`, `private_token`, `header`, `none` | Yes | Default: `bearer`. | | `appId` | GitHub App ID | text | No | Shown when **authMode** is `github_app`. | | `installationId` | GitHub App installation ID | number | No | Shown when **authMode** is `github_app`. | | `username` | Username | text | No | Shown when **authMode** is `basic`. | | `credential` | Credential | secret or reference | No | Token, password, or GitHub App private-key reference (env:NAME / file:/path). Leave blank only for anonymous local SCM. | | `headerName` | Header name | text | No | Shown when **authMode** is `header`. | | `scheme` | Header scheme | text | No | Optional prefix such as Bearer. Shown when **authMode** is `header`. | | `owner` | Account / organisation | text | No | Used by Azure DevOps drivers. | | `project` | Project | text | No | Used by Azure DevOps and Harness. | | `account` | Harness account | text | No | — | | `organization` | Harness organisation | text | No | — | | `botLogin` | Integration bot login | text | No | Used only to identify and supersede this integration’s own reviews. GitHub Apps default to GITHUB_APP_SLUG\[bot]. | | `probeRepo` | Connection-test repository | text | No | Optional provider-native repository path, for example owner/repo. Required when testing anonymous access. | | `orgId` | BattleTest organisation | text | Yes | Runs received through this connection are attributed to this BattleTest organisation. | | `webhookSecret` | Webhook secret | secret or reference | No | — | ## What the connection test checks [Section titled “What the connection test checks”](#what-the-connection-test-checks) Select Test connection to run a live test against the credentials you supplied. Saving validates and stores the configuration. Every check below is evaluated against the real service, not inferred from the configuration. | Check | Outcome | | ----------------------------------------- | --------- | | Reach the internal SCM gateway | Must pass | | Authenticate to the selected SCM provider | Must pass | *Adapter id: `go-scm-gateway`.*
# OIDC single sign-on
> Any OpenID Connect provider: Okta, Entra ID, Keycloak, Auth0, Google Workspace, Ping. Configured by issuer URL; endpoints come from discovery.
Any OpenID Connect provider: Okta, Entra ID, Keycloak, Auth0, Google Workspace, Ping. Configured by issuer URL; endpoints come from discovery. This provider fills the **identity** slot. Configure it in **Settings → Providers**, or during first-run setup. ## Settings [Section titled “Settings”](#settings) | Key | Field | Type | Required | Notes | | ----------------- | --------------------------- | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------- | | `issuer` | Issuer URL | URL | Yes | Enter the issuer URL. BattleTest discovers its OpenID Connect endpoints automatically. | | `clientId` | Client ID | text | Yes | — | | `clientSecret` | Client secret | secret or reference | Yes | Paste the client secret to store it encrypted, or use an env:NAME or file:/path reference. | | `scopes` | Scopes | text | No | We need openid and email. Add more only if your provider requires them. Default: `openid email profile`. | | `redirectUri` | Redirect URI | URL | No | Derived from this instance’s web origin. Register this exact value with your provider. | | `allowLocalLogin` | Keep password login enabled | yes / no | No | Turn off once SSO works. Leaving it on during rollout means a broken IdP does not lock everyone out. Default: `true`. | ## What the connection test checks [Section titled “What the connection test checks”](#what-the-connection-test-checks) Select Test connection to run a live test against the credentials you supplied. Saving validates and stores the configuration. Every check below is evaluated against the real service, not inferred from the configuration. | Check | Outcome | | ----------------------------------------- | --------- | | Discovery document reachable | Must pass | | Authorization and token endpoints present | Must pass | | Signing keys published | Must pass | | Redirect URI matches this instance | Advisory | *Adapter id: `oidc-identity`.*
# Chat model endpoint
> Anthropic or OpenAI API format, hosted officially, by another provider, through a gateway, or locally.
Anthropic or OpenAI API format, hosted officially, by another provider, through a gateway, or locally. This provider fills the **model.reasoning**, **model.fast**, **model.exploit** slot. Configure it in **Settings → Providers**, or during first-run setup. ## Settings [Section titled “Settings”](#settings) | Key | Field | Type | Required | Notes | | ----------------------- | ---------------------------- | ----------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `apiFormat` | API format | one of: `openai`, `anthropic` | Yes | Choose the request/response shape the endpoint accepts. This is independent of who hosts it. Default: `openai`. | | `endpointMode` | Endpoint | one of: `official`, `custom` | Yes | Official uses api.openai.com or api.anthropic.com. Custom supports gateways, other providers, and local inference. Default: `official`. | | `baseUrl` | Custom base URL | URL | No | The API root, usually ending at /v1. HTTP is supported for private/local deployments. Shown when **endpointMode** is `custom`. | | `model` | Model ID | text | Yes | — | | `pricingModel` | Pricing model ID | text | No | Only needed when Model ID is a gateway alias. Enter the upstream billable model from the bundled aggregate, for example anthropic/claude-sonnet-5. | | `apiKey` | API key | secret or reference | No | Paste the key (encrypted at rest), or use a reference (env:NAME / file:/run/secrets/x). Leave blank for a keyless local endpoint. | | `reasoningStyle` | Thinking mode style | one of: `none`, `openai_effort`, `anthropic_thinking`, `qwen_enable_thinking` | No | How this endpoint expresses reasoning/thinking. none = the model has no toggle. Default: `none`. | | `defaultReasoning` | Default thinking level | one of: `off`, `low`, `medium`, `high` | No | Recommended: high for the reasoning slot, off for the fast slot. Default: `off`. | | `reasoningBudgetTokens` | Thinking token budget (high) | number | No | Budget for “high” effort on Anthropic-style thinking. Defaults to 24576. Shown when **reasoningStyle** is `anthropic_thinking`. | | `extraBody` | Extra request JSON | text | No | Optional JSON object merged into every request body (provider-specific params). | | `inputUsdPerM` | Input $ / 1M tokens | number | No | Optional. Leave all three blank to use the bundled pricing aggregate. | | `cachedInputUsdPerM` | Cached input $ / 1M tokens | number | No | Required with the other overrides. Use the input rate when the provider has no cache discount. | | `outputUsdPerM` | Output $ / 1M tokens | number | No | Required with the other overrides. Local inference can use 0 for all three. | ## What the connection test checks [Section titled “What the connection test checks”](#what-the-connection-test-checks) Select Test connection to run a live test against the credentials you supplied. Saving validates and stores the configuration. Every check below is evaluated against the real service, not inferred from the configuration. | Check | Outcome | | ------------------------------------ | --------- | | Endpoint reachable and authenticated | Must pass | | Serves POST /chat/completions | Must pass | *Adapter id: `openai-chat`.*
# OpenAI-compatible embeddings endpoint
> Any endpoint serving POST /embeddings, returning 768-dim vectors.
Any endpoint serving POST /embeddings, returning 768-dim vectors. This provider fills the **model.embeddings** slot. Configure it in **Settings → Providers**, or during first-run setup. ## Settings [Section titled “Settings”](#settings) | Key | Field | Type | Required | Notes | | --------- | -------- | ------------------- | -------- | --------------------------------------------------------------------------------------- | | `baseUrl` | Base URL | URL | Yes | OpenAI-compatible API root ending at /v1. | | `model` | Model ID | text | Yes | — | | `apiKey` | API key | secret or reference | No | Paste the key (encrypted at rest), or use a reference (env:NAME / file:/run/secrets/x). | ## What the connection test checks [Section titled “What the connection test checks”](#what-the-connection-test-checks) Select Test connection to run a live test against the credentials you supplied. Saving validates and stores the configuration. Every check below is evaluated against the real service, not inferred from the configuration. | Check | Outcome | | ------------------------------------ | --------- | | Endpoint reachable and authenticated | Must pass | | Returns 768-dimensional vectors | Must pass | *Adapter id: `openai-embeddings`.*
# Vision model endpoint
> A multimodal Anthropic- or OpenAI-format endpoint that accepts image input (CAPTCHA/canvas fallback).
A multimodal Anthropic- or OpenAI-format endpoint that accepts image input (CAPTCHA/canvas fallback). This provider fills the **model.vision** slot. Configure it in **Settings → Providers**, or during first-run setup. ## Settings [Section titled “Settings”](#settings) | Key | Field | Type | Required | Notes | | ----------------------- | ---------------------------- | ----------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `apiFormat` | API format | one of: `openai`, `anthropic` | Yes | Choose the request/response shape the endpoint accepts. This is independent of who hosts it. Default: `openai`. | | `endpointMode` | Endpoint | one of: `official`, `custom` | Yes | Official uses api.openai.com or api.anthropic.com. Custom supports gateways, other providers, and local inference. Default: `official`. | | `baseUrl` | Custom base URL | URL | No | The API root, usually ending at /v1. HTTP is supported for private/local deployments. Shown when **endpointMode** is `custom`. | | `model` | Model ID | text | Yes | — | | `pricingModel` | Pricing model ID | text | No | Only needed when Model ID is a gateway alias. Enter the upstream billable model from the bundled aggregate, for example anthropic/claude-sonnet-5. | | `apiKey` | API key | secret or reference | No | Paste the key (encrypted at rest), or use a reference (env:NAME / file:/run/secrets/x). Leave blank for a keyless local endpoint. | | `reasoningStyle` | Thinking mode style | one of: `none`, `openai_effort`, `anthropic_thinking`, `qwen_enable_thinking` | No | How this endpoint expresses reasoning/thinking. none = the model has no toggle. Default: `none`. | | `defaultReasoning` | Default thinking level | one of: `off`, `low`, `medium`, `high` | No | Recommended: high for the reasoning slot, off for the fast slot. Default: `off`. | | `reasoningBudgetTokens` | Thinking token budget (high) | number | No | Budget for “high” effort on Anthropic-style thinking. Defaults to 24576. Shown when **reasoningStyle** is `anthropic_thinking`. | | `extraBody` | Extra request JSON | text | No | Optional JSON object merged into every request body (provider-specific params). | | `inputUsdPerM` | Input $ / 1M tokens | number | No | Optional. Leave all three blank to use the bundled pricing aggregate. | | `cachedInputUsdPerM` | Cached input $ / 1M tokens | number | No | Required with the other overrides. Use the input rate when the provider has no cache discount. | | `outputUsdPerM` | Output $ / 1M tokens | number | No | Required with the other overrides. Local inference can use 0 for all three. | ## What the connection test checks [Section titled “What the connection test checks”](#what-the-connection-test-checks) Select Test connection to run a live test against the credentials you supplied. Saving validates and stores the configuration. Every check below is evaluated against the real service, not inferred from the configuration. | Check | Outcome | | ------------------------------------ | --------- | | Endpoint reachable and authenticated | Must pass | | Accepts image input and responds | Must pass | *Adapter id: `openai-vision`.*
# SMTP relay
> Send notifications through your configured SMTP relay.
Send notifications through your configured SMTP relay. This provider fills the **mail** slot. Configure it in **Settings → Providers**, or during first-run setup. ## Settings [Section titled “Settings”](#settings) | Key | Field | Type | Required | Notes | | --------------- | ------------------------ | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------- | | `host` | SMTP host | text | Yes | — | | `port` | Port | number | No | 587 for STARTTLS, 465 for implicit TLS, 25 for an internal relay that does not authenticate. Default: `587`. | | `secure` | Implicit TLS | yes / no | No | Defaults from the port: on for 465, STARTTLS otherwise. | | `user` | Username | text | No | Leave empty for an internal relay that accepts unauthenticated mail from this host. | | `pass` | Password | secret or reference | No | Paste the SMTP password (encrypted at rest), or use a reference: env:NAME / file:/path. | | `from` | From address | text | Yes | Must be an address your relay will accept as a sender, or everything is rejected after the connection succeeds. | | `testRecipient` | Send the test message to | text | No | Only used when you press Test. Nothing is sent here otherwise. | ## What the connection test checks [Section titled “What the connection test checks”](#what-the-connection-test-checks) Select Test connection to run a live test against the credentials you supplied. Saving validates and stores the configuration. Every check below is evaluated against the real service, not inferred from the configuration. | Check | Outcome | | ---------------------------------- | --------- | | Relay reachable and TLS negotiated | Must pass | | Credentials accepted | Must pass | | Test message accepted for delivery | Must pass | *Adapter id: `smtp-mail`.*
# BattleTest GitHub App reference
> Reference for the BattleTest GitHub App: install URL, status check name, subscribed webhook events with actions, and the permissions the app requests.
## App details [Section titled “App details”](#app-details) | Property | Value | | -------------- | --------------------------------------------- | | App name | BattleTest Security | | Install URL | `https://github.com/apps/battletest-security` | | Check run name | `BattleTest Security Review` | ## Subscribed webhook events [Section titled “Subscribed webhook events”](#subscribed-webhook-events) BattleTest subscribes to the following events. Each maps to behaviour in the webhook handler; if an event is not enabled in the App’s subscription settings, the corresponding behaviour will silently not fire. | Event | Actions used | What it drives | | ----------------------- | ------------------------------------------------------- | -------------------------------------------------------------------- | | **Pull request** | `opened`, `synchronize`, `reopened`, `ready_for_review` | Starts a review (drafts are skipped until `ready_for_review`) | | | `closed`, `converted_to_draft` | Cancels an in-flight review | | **Check run** | `rerequested` | Re-runs a review when you click **Re-run** | | **Check suite** | `rerequested` | Re-runs a review from the checks UI | | **Installation** | `created`, `deleted` | Links the installation on install; cleans up and emails on uninstall | | **Installation target** | `renamed` | Keeps the account/org login in sync after a rename | | **Repository** | `renamed` | Keeps scan history findable under the new repo name | ## Permissions [Section titled “Permissions”](#permissions) | Permission | Level | Purpose | | ------------- | ----- | -------------------------------------------------------------- | | Contents | Read | Read repository code and git history for security analysis | | Pull requests | Write | Post security review comments and dismiss stale reviews on PRs | | Checks | Write | Create and update the `BattleTest Security Review` check run | | Metadata | Read | Required by GitHub for all Apps | ## Data handling [Section titled “Data handling”](#data-handling) BattleTest processes relevant repository code and pull request content to perform reviews. It stores review records, findings, and the selected evidence needed to explain them; it does not create a general-purpose copy of your repository. See [Security & Data Handling](/docs/reference/security-and-data) for details. ## Status check behaviour [Section titled “Status check behaviour”](#status-check-behaviour) The check conclusion mirrors the review verdict, so it can act as an opt-in merge gate. | State | GitHub conclusion | When | Blocks merge? | | ------------------- | ----------------- | --------------------------------------------------------------------- | ------------------------------- | | **In progress** | — | Review is running | No | | **Success** | `success` | Review completed; risk score below 40 (clean or low-risk) | No | | **Action required** | `action_required` | Review completed; risk score 40 or higher (“request changes” verdict) | Only if set as a required check | | **Failure** | `failure` | An internal review error prevented completion | Only if set as a required check | By default nothing blocks a merge; `action_required` simply surfaces a “needs attention” state on risky PRs. A single LOW finding or likely false positive (risk below 40) passes as `success`. To turn the review into a hard gate, add it as a required status check in GitHub branch protection; then PRs whose risk warrants changes are blocked until resolved. See [How to require security review before merging](/docs/how-to/require-security-review) and [Risk score](/docs/concepts/risk-score#score-bands).
# Limits and quotas
> Canonical reference for BattleTest numeric limits: advanced and basic PR review caps by repository visibility, verified-domain ceiling, run duration, and the fair-use compute limit.
This page is the canonical source for every numeric limit in BattleTest. The how-to guides link here rather than restating these figures. PR review is free. There are no tiers to compare; the only thing that changes a review limit is whether the repository is public or private. ## PR review limits [Section titled “PR review limits”](#pr-review-limits) BattleTest runs two review types. **Advanced** reviews run the full agentic pipeline (AI code analysis, CVE scanning, regression detection). **Basic** reviews are a fast single-pass scan (secrets and CVEs only) that runs automatically as a fallback when the advanced limit is reached. See [How PR review works](/docs/concepts/how-pr-review-works#advanced-vs-basic-reviews) for the distinction. | Repository | Advanced reviews | Basic fallback | | ---------- | ------------------------------- | --------------------------- | | Public | 20 per hour, **per repository** | 20 per hour, per repository | | Private | 10 per month | 5 per hour | On a public repo the accuracy-critical verifier agent runs on the higher-capability model even though the rest of the pipeline does not: a false positive on a public PR comment costs more than the model does. Hourly limits use a rolling 60-minute window, not a clock-hour reset. Monthly caps reset on the first of each UTC month. When the monthly advanced cap is reached, reviews fall back to basic scans for the rest of the cycle so every PR still gets coverage. ## Live testing limits [Section titled “Live testing limits”](#live-testing-limits) Live application testing is not self-serve. It runs as a dedicated per-client deployment, and run allowances, cadence, and depth are set as part of that engagement rather than by a limit on this page. A standard account cannot start a live run; the API refuses it. Domain registration and DNS verification remain open to any account, capped at **50 active verified domains** as an anti-abuse ceiling. That ceiling exists to bound pathological registration, not to price anything. One battletest is **one run regardless of scope**. Depth is chosen per run (Discover, Probe, or Full Exploit) and multiple domains or assets covered inside one run are still one run. ## Scan duration [Section titled “Scan duration”](#scan-duration) A live security test runs until the agents have thoroughly covered the target; there is **no time budget you need to plan around**. Duration scales with how much there is to test, so a small hardened site finishes quickly while a large or issue-rich application takes longer (the agents chase every confirmed weakness deeper). Total AI work per run is bounded by your [fair-use compute limit](#fair-use-compute-limit), not a clock. A 90-minute wall-clock backstop exists as a safety net against a hung run, not as a coverage limit; runs are not designed to approach it, and one that does indicates a fault worth reporting rather than a target too large to finish. | Operation | Typical duration | | ----------------------------------- | ------------------------------------------ | | PR review (all four passes) | 2–4 minutes | | Discovery scan | 5–15 minutes (depends on application size) | | Live test (probe) | \~15–40 minutes for a typical application | | Live test (with exploit validation) | longer for large or exploit-rich targets | ## Fair-use compute limit [Section titled “Fair-use compute limit”](#fair-use-compute-limit) Underneath the countable limits above sits a second limit, measured in compute rather than in events: a **monthly fair-use compute limit** on the total AI work an organisation runs, across PR reviews and battletests combined. It exists because the countable limits alone do not bound cost. Reviews vary enormously in how much work they take, so a month that looks identical by count can differ by an order of magnitude in compute. It is sized well above normal usage, so most teams never see it. * It is measured across **all** AI usage for your organisation: PR reviews and battletests together. * It is a **soft** limit. Work already in progress always finishes. It only affects **new** work. * It resets each month. The limit you will meet day to day is the countable one above, not the fair-use limit. ### What makes a run use more compute [Section titled “What makes a run use more compute”](#what-makes-a-run-use-more-compute) Cost scales with how much there is to examine, not with a clock. A pull request uses more compute when the diff is large, when it touches security-relevant code paths that pull more of the surrounding codebase into context, or when confirmed findings send the analysis agent deeper. A battletest uses more when: * **The target is exploit-rich.** Every confirmed weakness opens new avenues: a recovered credential unlocks authenticated areas, an exposed endpoint reveals more endpoints. The agents follow each thread, so a target with many real issues is tested far more deeply than a hardened one. * **The site is large or dense.** More pages, forms, and API endpoints mean more surface to map and probe. Additional testing consumes more of the monthly compute allowance. ### What happens when you reach it [Section titled “What happens when you reach it”](#what-happens-when-you-reach-it) When your organisation reaches its monthly fair-use compute limit: 1. **Work already in progress finishes normally.** Nothing is interrupted mid-run. 2. **PR reviews keep running in a lighter-weight mode.** The pipeline drops to the faster model rather than stopping, so no pull request is left unreviewed. 3. **New battletests soft-stop** until the month resets. You will see a **Soft limit reached this month** note on **Settings → Usage**, and you will get a notification. The limit resets at the start of each month with no action needed. Reaching it regularly means BattleTest is doing more work than the free product is sized to absorb. If that is where you are, [get in touch](https://battletest.dev/contact) about a dedicated deployment.
# Network and egress
> Every host a BattleTest deployment can contact at runtime, how each one is disabled or pointed at your own infrastructure, and how to verify the list against your own packet capture.
This page answers one question: **what does this container talk to?** Every external call BattleTest can make is listed below with the mechanism that closes it. Run a packet capture on the deployment and compare. A connection not on this page is a defect; report it. ## The posture switch [Section titled “The posture switch”](#the-posture-switch) One deployment-wide setting governs outbound behaviour.
```bash
EGRESS_MODE=airgap # enterprise default — fail-closed, nothing leaves
EGRESS_MODE=proxied # outbound allowed, but only through your forward proxy
EGRESS_MODE=open # direct internet (managed SaaS)
```
**`airgap`**: Category A is hard-disabled. Category B is served from your mirror or an offline database and **fails closed**: it never silently falls back to a public host. Category C vendors require a key or DSN and have no hardcoded fallbacks. Category D is your own infrastructure. **`proxied`**: outbound is permitted but routed through your forward proxy. Because Bun’s `fetch` honours `HTTP(S)_PROXY` natively, this is a transport concern rather than a per-call change. One exception: **Category A stays gated exactly as in airgap.** Proxying a connection does not un-leak the target to the aggregator receiving it, so third-party reconnaissance still requires explicit opt-in. **`open`**: direct internet, with per-feature disclosure. The effective posture is surfaced in the setup wizard as **Completely Offline**, **Outbound via Proxy**, **Direct Outbound**, or **Hybrid** (an air-gapped control plane with a proxy for test traffic). ### Configuration [Section titled “Configuration”](#configuration) | Setting | Purpose | | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `EGRESS_MODE=airgap\|proxied\|open` | Global posture. Defaults to `airgap` in the enterprise image | | `EGRESS_PROXY_URL` | Forward proxy for `proxied` mode. Falls back to `HTTPS_PROXY`/`HTTP_PROXY` if set directly | | `NO_PROXY` | Proxy-bypass list, merged with internal defaults (localhost, sidecars, `.internal`) | | `BATTLETEST_PROXY_URL` | Separate proxy for test **target** traffic on the sidecars: an independent axis from control-plane egress | | `DNS_RESOLVER` | Optional. Lookups use the container’s own resolver by default; set only when there is no usable system resolver | | `DNS_SERVERS` | Optional. Pin lookups to specific internal servers instead of `/etc/resolv.conf` | | `VULN_DB_PATH` | Offline vulnerability database (OSV-format snapshot). Takes priority over feed URLs | | `OSV_BASE_URL`, `NVD_BASE_URL`, `GHSA_BASE_URL`, `OPENCVE_BASE_URL`, `CVEDB_BASE_URL` | Your mirror or self-hosted instance for each vulnerability feed | | `PACKAGE_MIRROR_` | Your internal registry mirror per ecosystem. Fails closed if unset in airgap | | `SENTRY_MODE=off\|battletest\|custom` | Error reporting. Opt-in, default off, redacted | | `OAST_MODE=catcher\|interactsh\|off` | Which out-of-band callback backend runs. Defaults to `catcher`, the bundled receive-only catcher. The vendor’s public servers are unreachable in all three; see [out-of-band testing](/docs/how-to/out-of-band-testing) | ## The principle [Section titled “The principle”](#the-principle) Every outbound call sorts into one of two kinds, and they get opposite treatment. **Data about you going out**: your domains sent to a certificate-transparency log, your DNS sent to a public resolver, your target IPs sent to an intelligence aggregator. These are leaks: they broadcast your internal attack surface to third parties. They are eliminated: off by default, enabled only with explicit per-target consent, and disclosed when used. **Reference data coming in**: vulnerability databases, CVE feeds, advisories. Genuinely needed, but the data lives outside, and *querying* it leaks your private dependency names and your CVE interests. These are served from your mirror or an offline snapshot. Both end in the same place: **zero required runtime egress.** Everything is off, served locally, or pointed at infrastructure you control. ## Category A: Data about you going out [Section titled “Category A: Data about you going out”](#category-a-data-about-you-going-out) Disabled by default in `airgap` and `proxied`. Each becomes available only when named in `RECON_ENABLE` *and* the target carries `external_recon_authorized`. Both gates must agree. | Host | Purpose | Enforcement outside `open` | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `crt.sh` | Certificate transparency | Off unless in `RECON_ENABLE` | | `api.certspotter.com` | Certificate transparency | Off unless in `RECON_ENABLE` | | `api.hackertarget.com` | Recon aggregation | Off unless in `RECON_ENABLE` | | `www.circl.lu` | Passive DNS | Off unless in `RECON_ENABLE` | | `rdap.org` | RDAP / WHOIS | Off unless in `RECON_ENABLE` | | `web.archive.org` | Wayback history | Off unless in `RECON_ENABLE` | | `api.github.com/search/code` | Public code-leak search | Off unless in `RECON_ENABLE`. The GitHub token is dual-purpose, so key presence alone is **not** treated as opt-in | | `ipinfo.io`, `ip-api.com` | Proxy exit-IP geolocation | Skipped unless `open` | | `web.archive.org`, `otx.alienvault.com`, `urlscan.io`, `commoncrawl.org` via `url-mining` | Passive endpoint discovery | Off unless in `RECON_ENABLE`. Queries these archives about your target; never touches the target itself | | `api.shodan.io`, `api.platform.censys.io`, `otx.alienvault.com`, `api.greynoise.io`, `api.criminalip.io`, `app.netlas.io`, `api.zoomeye.org`, `api.securitytrails.com` | Keyed OSINT | Key-gated. Supplying the key is itself the opt-in | | `subfinder` aggregators (sidecar) | Passive DNS / CT | Refused unless `open`, at both the request validator and the shell. Note the limits described under [what we have not proved](#where-the-enforcement-is-thinner-than-the-tables-imply) | ### One host we removed rather than gated [Section titled “One host we removed rather than gated”](#one-host-we-removed-rather-than-gated) `cloudflare-dns.com` was previously the default DNS-over-HTTPS resolver. **It is no longer contacted in any mode.** Lookups now use the container’s own resolver. The public DoH default was an artifact of the original Cloudflare Workers runtime, which cannot open a UDP socket, so it was deleted. `DNS_RESOLVER` remains as an opt-in override for a DoH endpoint **you** run. Your resolver’s own forwarding policy is your configuration to make. ## Category B: Reference data coming in [Section titled “Category B: Reference data coming in”](#category-b-reference-data-coming-in) Each feed resolves to your mirror if configured, the public host in `open` mode, and otherwise **null**: the caller skips the lookup and degrades to an empty result. It never falls back to the public host. | Host | Purpose | Override | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ---------------------------- | | `api.osv.dev` | OSV vulnerability data | `OSV_BASE_URL` | | `services.nvd.nist.gov` | NVD | `NVD_BASE_URL` | | `api.github.com/advisories` | GHSA | `GHSA_BASE_URL` | | `app.opencve.io` | OpenCVE | `OPENCVE_BASE_URL` | | `cvedb.shodan.io` | EPSS / KEV enrichment | `CVEDB_BASE_URL` | | `registry.npmjs.org`, `pypi.org`, `crates.io`, `proxy.golang.org`, `repo.packagist.org`, `rubygems.org`, `hex.pm`, `api.nuget.org`, `search.maven.org`, `pub.dev` | Package identity and typosquat checks | `PACKAGE_MIRROR_` | A transparent caching proxy (an Artifactory remote repository or a Nexus proxy) preserves the registry’s URL layout, so swapping the host is sufficient. ### The fully offline path [Section titled “The fully offline path”](#the-fully-offline-path) Point `VULN_DB_PATH` at an OSV-format snapshot directory and every vulnerability lookup (dependency scanning, CVE enrichment, the verifier’s oracle) is answered from disk. No mirror, no URLs, zero egress. It takes **priority** over the feed URLs when set. Refresh it by running the sync script against OSV or your own mirror, shipping the resulting folder in, and restarting. A completely offline deployment loses no vulnerability coverage this way. ## Category C: Vendor services [Section titled “Category C: Vendor services”](#category-c-vendor-services) Off by default, opt-in, or absent from the enterprise image. | Host | Purpose | Enforcement | | ---------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `*.ingest.*.sentry.io` | Error reporting | Opt-in via `SENTRY_MODE`, default off, redacted | | `cloud.langfuse.com` | LLM tracing | Key-gated. In airgap, disabled unless `LANGFUSE_BASE_URL` is your own instance | | `next-api.useplunk.com` | Lifecycle email | Hard no-op in airgap | | `api.pdtm.sh` | Version ping from the bundled ProjectDiscovery scanners | Disabled image-wide. Sends tool name, version, OS and Go version (no target data), but it is still an undeclared call, so it is switched off rather than documented | | FCM / Mozilla autopush | Web Push relay | Off in airgap unless explicitly enabled | | `proxy.webshare.io`, `api.capsolver.com`, `5sim.net` | Anti-blocking for authorised external testing | Off in airgap. Keyed | | `api.endpoints.huggingface.cloud` | Managed inference-endpoint lifecycle | Only when Hugging Face endpoints are configured. Unused if you run your own inference | | `analytics.drreamer.digital` | Product analytics on the **web interface** | Gated on `PUBLIC_ANALYTICS_ENABLED`, which a self-hosted instance leaves unset. The beacon, its connection hint, and the server-side proxy route are all behind that one flag; with it unset the proxy returns `404` | ### Licensing is offline [Section titled “Licensing is offline”](#licensing-is-offline) The enterprise edition is backed by an Ed25519-signed licence verified locally against a public key baked into the image. **Zero egress.** Installing a licence is what makes an instance the enterprise edition; its entitlements (tier, caps, feature flags, expiry and grace period) drive the product directly. A missing or forged licence falls back to a locked free floor. There is no billing integration and no payment egress — payment is handled out of band. It is forgery-proof, not tamper-proof: the inherent limit of client-side enforcement. ### The image is signed and ships an SBOM [Section titled “The image is signed and ships an SBOM”](#the-image-is-signed-and-ships-an-sbom) You can verify provenance and integrity offline, air-gapped, with no call home: key-based `cosign` with no transparency-log lookup, plus an SPDX/CycloneDX SBOM attached as an attestation. Docker does not check signatures at runtime, so this is a check you run deliberately. ## Category D: Your own infrastructure [Section titled “Category D: Your own infrastructure”](#category-d-your-own-infrastructure) | Destination | Notes | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Your SCM | `GITHUB_HOST` points every GitHub call at your GitHub Enterprise Server. Unset means github.com | | The test target | The system you authorised us to test | | Your SMTP relay | The **only** mail transport. `MAIL_PROVIDER` is `smtp` by default and there is no vendor alternative; the managed email provider was removed from the product entirely. With no `SMTP_HOST` set, the email channel is disabled and in-app and push notifications continue unaffected | | Your object storage | Any S3-compatible endpoint | | Your model endpoints | Chat, embeddings and vision all resolve through one seam | | Callback catcher | Receive-only, self-hosted. No egress. The default out-of-band backend | | Self-hosted interactsh (optional) | Receive-only, self-hosted, and off unless you start it. Restores the template scanner’s out-of-band coverage without any third-party callback host. Runs its own authoritative DNS, so it needs a **delegated domain**; see [out-of-band testing](/docs/how-to/out-of-band-testing). Give it your server’s public IP explicitly: left to discover its own address it makes two undeclared calls at startup and then exits when they fail | | Signalling broker and TURN server | Self-hosted. See below | ### Inference resolves through one seam [Section titled “Inference resolves through one seam”](#inference-resolves-through-one-seam) Every model transport (chat, embeddings and vision, including the compatibility layer that serves `env.AI.run`) resolves its endpoint and its credential **immediately before each request**, not once at startup. Configuring a model slot therefore routes all of them to your endpoint, including calls made before configuration finishes loading. In `airgap`, the endpoint is checked against an **allowlist**: a request proceeds only if the destination is private (loopback, RFC1918, CGNAT, unique-local, a single-label host) or is an endpoint you configured. Anything else throws at the point of the fetch, so a misconfigured instance fails its run instead of disclosing the prompt. Comparison is by parsed URL origin, so a lookalike host such as `api.deepinfra.com.example.net` does not pass. The credential is resolved alongside the endpoint. Pointing inference at your own server does not hand that server a third-party API key. This is asserted as a **property**: the test suite constructs the adapters the way the server does and fails if any of them can reach a public host under `EGRESS_MODE=airgap`, including a table of public providers and lookalike domains. ### The Room’s audio [Section titled “The Room’s audio”](#the-rooms-audio) The default configuration of the library we use would have made the Room’s audio a Category A leak, and the problem is not visible from a dependency list. **PeerJS ships public TURN servers in its default config.** A TURN server is not a broker: when two browsers cannot reach each other directly, media **relays through it**. An instance using the default would have relayed call audio through a third-party server. The packets are encrypted, so the relay cannot hear the call, but it sees every participant’s IP address, who called whom, when, and for how long. We pass an explicit ICE configuration and **refuse any TURN URL containing `peerjs.com`**, including one an operator supplies. Two tests fail if that changes. **There is no public STUN fallback either.** An unconfigured deployment resolves to no ICE servers at all: LAN calls work, everything else fails visibly. A public default would hand a third party the address of every browser that opened a room, so there is no default. **TURN credentials are minted per user and never shipped.** The API issues a short-lived HMAC credential after the room’s view check. The shared secret is derived from the instance secret on both sides: never transmitted, never in the browser bundle. **Relay is the default**, so media goes through your own TURN server even when a direct path exists. It costs bandwidth and buys one thing: participants stop learning each other’s IP addresses. The Room holds guests and external auditors, and neither party opted into revealing their address to the other. **The TURN server will not relay into private ranges.** RFC1918, link-local and loopback are denied; a TURN server that forwards into private space is an SSRF pivot into whatever network it sits in, and this one sits next to the scanners. Both services are containers in your deployment, so none of this requires egress: a call between two people on your network never leaves it. ## Build-time only [Section titled “Build-time only”](#build-time-only) Fetched when the image is built, never at runtime: Kali packages and pinned tool tarballs, vulnerability-scanner templates, and Playwright browsers. The template scanner’s out-of-band callback feature is disabled unless you configure a backend for it; see below. The catalogue and payload import scripts reach public sources at build time. They are not runtime paths. ## What is recorded in every export [Section titled “What is recorded in every export”](#what-is-recorded-in-every-export) Public model endpoints, live advisory feeds, and hosted callback catchers are supported. Their use must be disclosed in the deployment manifest. Each dependency is classified as `self`, `battletest`, or `external`. The readiness panel shows the classification, and the list of anything that ran off-instance is recorded **inside the signed manifest content**, so the disclosure cannot be separated from the report it qualifies. An empty list is a positive claim, and it is only ever empty when nothing left your network. Classification errs toward disclosure: a host we cannot prove is internal is reported as `external`. Over-disclosure starts a conversation; under-disclosure is a false claim in a signed document. ## What we have not proved [Section titled “What we have not proved”](#what-we-have-not-proved) This list is maintained by source audit plus packet capture on a live deployment. Some things are neither confirmed present nor confirmed absent, and we would rather say so. * **The crawler SDK has no telemetry to disable.** We audited the pinned version’s complete set of environment variables and found no telemetry, analytics or reporting entry, and the vendor’s own SDK is not a dependency of that image. No telemetry opt-out variable is required for this version. * **The sandbox layer** is not yet audited to the same standard. Treat it as unproven. * **A Cloudflare API token** is accepted as configuration, but no runtime call using it was found in the application source. * Confirmed **absent**: PostHog and OpenTelemetry exporters. Not merely unimported: no dependency and no source match. * **Turnstile** (`challenges.cloudflare.com`) is reached only when a Turnstile secret is set. Unset (the self-hosted default) skips the check before any request is made. * **The browser binary** in the crawler image is a third-party build. The launch flags disable its background networking, but that is the browser’s behaviour rather than something this deployment’s configuration controls. ### Where the enforcement is thinner than the tables imply [Section titled “Where the enforcement is thinner than the tables imply”](#where-the-enforcement-is-thinner-than-the-tables-imply) **The command tool in the security sidecar accepts arbitrary shell.** This is deliberate (it is an offensive testing tool and its agents need a shell) and it means the sidecar’s egress controls are layered rather than absolute: * Package installation is **refused** outside `open` mode. That layer is solid: installing reaches the network by definition, so refusing it cannot be worked around at that entry point. * Known out-of-band and passive-recon tool names are **refused on the shell path** as well as the task path. This check blocks known tool names. Arbitrary shell commands can bypass it, so it is not a security boundary. * The out-of-band callback client has been **removed from the image** entirely. It was never wired into any executor. **The enforceable control for this container is network-level.** Deny egress from the security-tools container in air-gapped operation and allow only what your deployment declares. Everything above is a supporting layer over that. **The vendor’s public out-of-band servers are never used, in any mode.** The template-based vulnerability scanner defaults to a set of callback servers operated by its vendor, and the hostname is embedded in the payload, so it is **your own target systems** that call out, a disclosure originating inside your network rather than from this container, and one a packet capture here would never show. That default is overridden at the image level rather than per call site, so it holds however the scanner is invoked, and the build fails if the override is not in place. What varies is which backend you get instead; see [out-of-band testing](/docs/how-to/out-of-band-testing) for the full decision: * **`OAST_MODE=catcher`** (the default): the bundled callback catcher, which is receive-only and self-hosted. It covers the agent-driven blind-vulnerability paths and needs at most one wildcard DNS record. It is **not** protocol-compatible with the template scanner, so that scanner’s out-of-band templates stay excluded. * **`OAST_MODE=interactsh`**: a self-hosted interactsh server that you run. The scanner is pointed at it and its full out-of-band template set is restored. This requires **delegating a domain’s nameservers** to that server, which is a real commitment rather than a configuration flag; the how-to page states what it costs before you agree to it. * **`OAST_MODE=off`**: no out-of-band testing anywhere. The agent’s callback tools report themselves unavailable rather than returning an empty result that reads like a negative finding. The trade-off: under `catcher` or `off`, blind vulnerability classes that only prove themselves via an outbound connection are not covered by the template scanner. A run reports what it could not reach, and that statement travels inside the signed export rather than beside it. An explicitly configured `-iserver` from a caller is treated as a declared choice and passed through untouched. A deployment with no configured server gets out-of-band testing **disabled**, never the vendor default. **Check the value your deployment actually receives.** The code defaults `EGRESS_MODE` to `airgap` when unset, but the published compose files set it explicitly for managed operation. Read the value in your own environment rather than assuming the code default applies. At boot and during normal application runtime, an empirical capture on a live deployment showed the only external connection was error reporting, which is now off by default. Test-time egress is enumerated statically above rather than observed dynamically. ## Reporting a discrepancy [Section titled “Reporting a discrepancy”](#reporting-a-discrepancy) If a packet capture shows a connection this page does not list, email ****. A missing entry is a defect in this document and we will correct it here. ## Extensions and outbound access [Section titled “Extensions and outbound access”](#extensions-and-outbound-access) An extension declares the hosts it needs. An operator grants some subset, once, at install. The runtime checks the grant on every request. The declaration is a ceiling and the grant is the floor: an extension can never be granted a host it did not declare, and declaring new hosts in a later version requires fresh consent rather than inheriting the old. Grants are stored per extension and host. Removing or disabling an extension drops its grants; a host another extension still holds a grant on is unaffected, because each holds its own. **Where this is enforced depends on your egress mode.** | Mode | Enforcement | | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `proxied` | **Real.** The forward proxy is set as a process environment variable before anything starts, so every outbound request routes through it regardless of what extension code does. The proxy is the chokepoint, outside the extension’s reach. | | `airgap` | Outbound is refused wholesale. Grants apply only to internal hosts reachable via `NO_PROXY`. | | `open` | **Advisory.** There is no chokepoint. The host check does not isolate extensions from the shared process and can be bypassed. | Run `proxied` in production, not `open`. Only under `proxied` is an extension’s declared boundary a boundary rather than a promise. Every request an extension makes through the runtime carries an `x-battletest-extension` header naming it. Alert on any request reaching your proxy **without** that header: it came from something that bypassed the runtime wrapper. Do not treat it as ordinary traffic. Extensions also declare how much they need the network at all, which lets a run distinguish a source your policy blocked from one that cannot work in your deployment: * **`none`**: reaches nothing off-host; runs in every mode. * **`required`**: cannot function without network. Under airgap this is reported as a capability mismatch, not a policy refusal: no grant would change it. * **`optional`**: supports execution with or without network access. * **`degraded`**: works without network and returns less; the run says what was missing.
# Notification events
> Complete reference for every BattleTest notification event: the seven categories, each event, and its default and locked delivery channels across email, push, and in-app.
BattleTest sends notifications on a fixed catalog of events. Each event belongs to one of seven categories and has a default set of channels it is delivered on. You can override the channels per event from [Settings → Notifications](https://battletest.dev/settings?tab=notifications); to change them, see [How to configure notifications](/docs/how-to/configure-notifications). ## Channels [Section titled “Channels”](#channels) | Channel | Delivery | | ---------- | ---------------------------------------------------------------- | | **Email** | Sent to your account email address from `noreply@battletest.dev` | | **Push** | Browser push (requires granting notification permission) | | **In-app** | The notification bell in the dashboard | In the tables below, ✓ marks a channel that is **on by default**. A ✓ 🔒 channel is **required**: it is always on and cannot be turned off, because the event is money- or security-critical and must not be silently missed. ## Change Reviews [Section titled “Change Reviews”](#change-reviews) Automated security reviews on your pull requests and merge requests. | Event | Fires when | Email | Push | In-app | | -------------------------------- | ------------------------------------------------------------------------------------------------- | ----- | ---- | ------ | | PR review completed | A pull-request security review finished | | | ✓ | | PR review failed | A review couldn’t complete and needs a retry | ✓ | | ✓ | | PR reviewed in basic mode | Your advanced-review limit was hit, so the PR got a basic scan instead of the full agentic review | ✓ | | ✓ | | PR not reviewed — limits reached | A PR was skipped because all review limits were exhausted | ✓ | | ✓ | | Approaching PR review limit | You’re near your monthly advanced-review limit | | | ✓ | ## Live testing [Section titled “Live testing”](#live-testing) Active security assessments against your domains. | Event | Fires when | Email | Push | In-app | | ---------------------------------- | ------------------------------------------------------------------------------------------------------ | ----- | ---- | ------ | | Battletest completed | A live security assessment finished | ✓ | | ✓ | | Battletest failed | A scan couldn’t complete | ✓ | | ✓ | | Vulnerability found | A critical or high-severity issue was confirmed | ✓ | ✓ | ✓ | | Security report ready | Your full assessment report is ready to view | ✓ | | ✓ | | Battletest needs your approval | A scan is paused waiting for you to approve an exploit-gated step | ✓ | ✓ | ✓ | | Approval granted — scan resumed | An exploit-gated step you approved has resumed | | | ✓ | | Battletest stopped at your request | You declined an exploit-gated step; the scan stopped with findings gathered so far | | | ✓ | | Approval rejected | An approval request was rejected | | | ✓ | | Monthly battletest limit reached | You’ve used all the battletest runs available to you this cycle | ✓ | | ✓ | | Approaching battletest limit | You’re near the monthly battletest run limit | | | ✓ | | Scheduled run skipped | A scheduled battletest didn’t run (a lost DNS verification, or the monthly battletest cap was reached) | ✓ | | ✓ | | Scheduled run couldn’t start | A scheduled battletest failed to launch | ✓ | | ✓ | ## Billing & Plan [Section titled “Billing & Plan”](#billing--plan) Payment, add-on, and self-hosted licence events. Most fire only for accounts with existing billing records; the licence events fire during a self-hosted deployment purchase. | Event | Fires when | Email | Push | In-app | | ---------------------- | ----------------------------------------------------------------------------------- | ----- | ---- | ------ | | Payment failed | We couldn’t charge your card | ✓ 🔒 | | ✓ | | Past due | An outstanding balance moved the account to past due | ✓ 🔒 | | ✓ | | Payment received | A payment went through | | | ✓ | | Plan changed | The plan on the account changed and new limits are in effect | ✓ 🔒 | | ✓ | | Change scheduled | A change or cancellation is scheduled for the end of the billing period | ✓ | | ✓ | | Cancelled | Billing was cancelled; access ends at the period end | ✓ 🔒 | | ✓ | | Refund issued | A refund was issued to your payment method | ✓ | | ✓ | | Card expiring | Your saved card is expiring | ✓ | | ✓ | | Payment method removed | The card on file was removed while billing was still active | ✓ | | ✓ | | Add-on added | An add-on was added to your subscription | ✓ | | ✓ | | Add-on removed | An add-on was removed | | | ✓ | | Licence request staged | A completed payment is waiting for a self-hosted licence to be signed and delivered | ✓ | | ✓ | | Licence key issued | Your self-hosted licence key was issued and sent to you | ✓ | | ✓ | ## Account & Security [Section titled “Account & Security”](#account--security) Sign-in, password, and account-security events. | Event | Fires when | Email | Push | In-app | | --------------------------------------- | ------------------------------------------------------------------- | ----- | ---- | ------ | | Password changed | Your account password was changed | ✓ 🔒 | | ✓ | | Email address changed | Your account email was changed (also sent to your previous address) | ✓ 🔒 | | ✓ | | New sign-in | A new sign-in from a new device or location | ✓ | | ✓ | | Welcome | A welcome message when you create your account | ✓ | | | | Email verified | Your email address was verified | | | ✓ | | Account deleted | Confirmation that your account was deleted | ✓ 🔒 | | | | API key created | A new API key was created on your account | ✓ | | ✓ | | API key revoked | An API key was revoked | ✓ | | ✓ | | Could not save a review to your storage | A bring-your-own-storage export failed | ✓ | | ✓ | ## Team & Organization [Section titled “Team & Organization”](#team--organization) Membership, roles, and org changes. | Event | Fires when | Email | Push | In-app | | ------------------------- | ----------------------------------------------------------- | ----- | ---- | ------ | | Your role changed | Your role in an organization was changed | ✓ | | ✓ | | Removed from organization | You were removed from an organization | ✓ | | ✓ | | Organization deleted | An organization you belonged to was deleted | ✓ | | ✓ | | Invitation received | You were invited to an organization | ✓ | | ✓ | | Invitation accepted | Someone you invited joined your organization | | | ✓ | | Ownership transfer | An organization ownership transfer was offered or completed | ✓ | | ✓ | ## Integrations [Section titled “Integrations”](#integrations) GitHub and domain verification. | Event | Fires when | Email | Push | In-app | | ---------------------------- | --------------------------------------------------------------- | ----- | ---- | ------ | | GitHub account linked | A GitHub account was linked to your BattleTest account | ✓ | | ✓ | | GitHub account unlinked | A GitHub account was disconnected | ✓ | | ✓ | | GitHub connected to org | A GitHub installation was connected to an organization | | | ✓ | | GitHub disconnected from org | A GitHub installation was removed from an organization | ✓ | | ✓ | | GitHub App suspended | The BattleTest GitHub App was suspended; reviews are paused | ✓ | | ✓ | | GitHub App uninstalled | The BattleTest GitHub App was removed from an account | ✓ | | ✓ | | Domain verified | A domain passed DNS ownership verification | | | ✓ | | Domain verification lost | A domain’s DNS verification record is missing; scans are paused | ✓ | | ✓ | ## Run Rooms [Section titled “Run Rooms”](#run-rooms) Access requests and invitations for the live run room. These are time-sensitive: a run is in flight while someone waits to be let in, so the two that block a person default to push as well. | Event | Fires when | Email | Push | In-app | | -------------------------------- | ---------------------------------------------------------------------- | ----- | ---- | ------ | | Someone asked to join a run room | A person with the link requested access to a run you can admit them to | ✓ | ✓ | ✓ | | You were added to a run room | Your request to join a run room was approved | ✓ | | ✓ | | Someone asked to steer a run | A watcher asked for permission to steer a live run | ✓ | ✓ | ✓ | | Your run access was upgraded | You were granted a higher level of access in a run room | ✓ | | ✓ | | You were invited to a run room | Someone invited you to watch or steer a run | ✓ | | ✓ | ## Related [Section titled “Related”](#related) * [How to configure notifications](/docs/how-to/configure-notifications): turn channels on or off, enable browser push, and install the app. * [How to export findings](/docs/how-to/export-findings): get finding data out of BattleTest for your own reporting.
# PR findings
> Reference for BattleTest PR findings: severity definitions, risk score bands and weights, finding record fields, and the Open to Resolved lifecycle.
## Severity levels [Section titled “Severity levels”](#severity-levels) | Severity | Definition | | ------------ | ---------------------------------------------------------------------------------------------------------------------------- | | **CRITICAL** | Exploitable vulnerability with high impact: data breach, account takeover, remote code execution. Immediate action required. | | **HIGH** | Significant vulnerability. Should be addressed in this PR or tracked for immediate remediation. | | **MEDIUM** | Vulnerability with limited exploitability or impact. Triage and schedule. | | **LOW** | Best-practice deviation or informational finding. Address when convenient. | ## Risk score [Section titled “Risk score”](#risk-score) The risk score (0–100) summarises the security posture of the PR. It is driven by finding severity and count, with regression findings carrying an additional penalty. | Component | Weight | | ------------------------------------------------------------- | ------------------------------ | | CRITICAL finding | Highest | | HIGH finding | High | | MEDIUM finding | Medium | | LOW finding | Low | | Regression penalty (similar to a previously resolved finding) | Significant additional penalty | | Score | Band | | ------ | ----------------------------------- | | 0–20 | Clean | | 21–40 | Low risk | | 41–60 | Moderate risk | | 61–80 | High risk | | 81–100 | Critical: fix before any deployment | See [Risk score](/docs/concepts/risk-score) for how these factors are weighted and how to use score trends over time. ## Finding fields [Section titled “Finding fields”](#finding-fields) | Field | Description | | --------------- | ------------------------------------------------------------------------------ | | **Title** | Short description of the vulnerability type | | **Severity** | CRITICAL / HIGH / MEDIUM / LOW | | **Location** | File path and line number(s) | | **Description** | What the vulnerability is and why it’s a problem | | **Evidence** | The code pattern or payload that triggered the finding | | **Remediation** | How to fix it, with a code example in the relevant language/framework | | **References** | CVE ID, OWASP category, or CWE number where applicable | | **Source** | How the finding was detected: `CVE`, `Secret`, `Config`, `Regression`, or `AI` | ## Finding lifecycle [Section titled “Finding lifecycle”](#finding-lifecycle) | State | Meaning | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | **Open** | Finding detected, not yet acted on | | **Dismissed** | Marked as a false positive or accepted risk. Does not count toward risk score in future scans. Does not trigger regression detection. | | **Resolved** | Finding no longer present in subsequent scans of the same code path. Contributes to the regression detection baseline. | ## PR review output [Section titled “PR review output”](#pr-review-output) Each completed review produces: * A GitHub PR comment from `battletest[bot]` * A `BattleTest Security Review` check run: `success` when the risk score is below 40, `action_required` at 40 or higher (“request changes”), and `failure` only on an internal review error * A run record in the BattleTest dashboard linked from the PR comment ## Dashboard run detail [Section titled “Dashboard run detail”](#dashboard-run-detail) The run detail page includes: * All findings with expanded reasoning * Historical comparison: how this PR compares to previous runs on the same repository * Export as JSON or PDF * Per-finding dismiss and status controls
# Scan results
> Reference for live scan reports: report structure, finding categories, Confirmed and Potential confidence levels, scan diffs, and export formats.
## Scan report structure [Section titled “Scan report structure”](#scan-report-structure) Each completed battletest produces a report with: * **Overall risk score** (0–100) * **Attack surface summary:** endpoints discovered, forms found, subdomains enumerated, ports scanned * **Findings:** sorted by severity with full detail * **Diff vs previous scan:** new, resolved, persistent, and regression findings ## Finding categories [Section titled “Finding categories”](#finding-categories) | Category | Examples | | -------------------- | ------------------------------------------------------------------------------ | | **Injection** | SQL injection, XSS, command injection, path traversal | | **Authentication** | Weak auth, session management issues, credential exposure | | **Misconfiguration** | Missing security headers, exposed admin interfaces, default credentials | | **Exposure** | Sensitive information in responses, directory listing, backup files accessible | | **Dependency** | Outdated server software or frameworks with known CVEs | | **SSL/TLS** | Certificate issues, weak ciphers, deprecated protocol versions | ## Confidence levels [Section titled “Confidence levels”](#confidence-levels) | Level | Meaning | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Confirmed** | The agent verified the vulnerability with a working proof of concept. Act immediately. | | **Potential** | The endpoint or pattern is suspicious but could not be confirmed without a higher permission tier. Review manually or approve the next tier. | | **Informational** | Not a vulnerability. Noted for awareness (e.g., overly verbose server headers). | ## Scan diff classification [Section titled “Scan diff classification”](#scan-diff-classification) | Classification | Meaning | | -------------- | ------------------------------------------------------------- | | **New** | Finding present in this scan, not in the previous scan | | **Persistent** | Finding present in both this scan and the previous scan | | **Resolved** | Finding present in the previous scan, absent in this scan | | **Regression** | Finding semantically similar to a previously resolved finding | ## Export formats [Section titled “Export formats”](#export-formats) | Format | Use | | -------- | ----------------------------------------------------------------------------------------------- | | **PDF** | Formatted report for sharing with customers, auditors, or security questionnaire responses | | **JSON** | Machine-readable findings for integration with ticketing systems, SIEMs, or security dashboards |
# Security and data handling
> What BattleTest accesses, what it retains, how AI-assisted analysis works, and the safeguards and providers involved.
This page describes BattleTest’s data-handling practices. It does not publish sensitive architecture, control configuration, or information that would reduce the security of the Service. Our [Privacy Policy](https://battletest.dev/privacy) controls how we handle personal data. ## Repository access [Section titled “Repository access”](#repository-access) BattleTest accesses only the repositories and permissions made available through the GitHub integration you install. Repository access can be changed or revoked from GitHub. | Data | Why it is used | Typical handling | | ------------------------------------ | -------------------------------------------------- | ----------------------------------------------------------------------------------- | | Repository and pull request metadata | Identify the repository, change, and review status | Stored with review records | | Relevant code changes and context | Perform the requested security review | Processed for analysis; not used to build a general-purpose copy of your repository | | Findings, evidence, and remediation | Present results and compare later changes | Stored in your BattleTest workspace | | Installation credentials | Access repositories you selected | Restricted and protected as security-sensitive data | Disconnecting GitHub stops future repository access. Existing findings and reports remain in your workspace until you delete them, delete the relevant workspace, or delete your account. ## Live security testing [Section titled “Live security testing”](#live-security-testing) For live application testing, BattleTest processes the targets, scope, permission level, test configuration, and any test credentials you provide. During a run, the Service may process requests and responses, discovered assets, screenshots, browser state, tool output, approvals, and evidence needed to generate findings. Use dedicated test accounts and non-production credentials wherever practical. Only test systems you own or are authorised to test, and follow the [Acceptable Use Policy](https://battletest.dev/acceptable-use). ## Records retained in your workspace [Section titled “Records retained in your workspace”](#records-retained-in-your-workspace) BattleTest generally stores: * account, organisation, membership, role, and preference records; * repository and target registrations; * review and test status, findings, evidence, reports, and remediation notes; * notification, approval, usage, and licence-entitlement records; and * security, reliability, and diagnostic events needed to operate the Service. Temporary execution data and credentials expire or are deleted according to operational retention periods. Some evidence may contain selected code or response content when it is necessary to explain or reproduce a finding. ## AI-assisted analysis [Section titled “AI-assisted analysis”](#ai-assisted-analysis) BattleTest uses AI-assisted systems to analyse relevant code changes, application responses, screenshots, and security evidence. Depending on the feature, the minimum content needed for the task may be sent to an AI service provider. Current providers include: | Provider | Purpose | | ------------ | ---------------------------------------------------- | | DeepInfra | Text-based security analysis and inference | | Anthropic | Visual analysis for supported live-testing workflows | | OpenAI | Embeddings and supported AI-assisted workflows | | Hugging Face | Embeddings and specialised model inference | Provider use may vary by feature and availability. AI output can be incomplete or wrong; review material findings before relying on them. BattleTest does not use customer content to train a public BattleTest model. ## Other service providers [Section titled “Other service providers”](#other-service-providers) The providers below apply to the **managed (SaaS) service** at battletest.dev. A **self-hosted deployment** reaches none of them by default: email, error reporting, and AI-observability are off or point at your own infrastructure, and the egress controls are described in [Network and egress](/docs/reference/network-and-egress). We use service providers only where needed to operate or support the managed Service. Core providers include: | Provider | Role | | ---------- | ------------------------------------------------------------------------ | | netcup | Application and database hosting in Nuremberg, Germany | | GitHub | Authentication and repository integration when connected by the customer | | Cloudflare | DNS, content delivery, bot protection, and object storage | | Plunk | Transactional and lifecycle email delivery | | Sentry | Error monitoring and service diagnostics | | Langfuse | AI workflow observability and diagnostics | | Webshare | Network routing for customer-authorised testing | | CapSolver | Challenge handling for customer-authorised testing | | 5SIM | Temporary test communications for customer-authorised testing | Security-intelligence workflows may query third-party sources using target indicators needed for the customer-authorised task. Two groups, both optional: **Keyed sources**: used only when you supply an API key: Shodan, Censys, AlienVault OTX, GreyNoise, Criminal IP, Netlas, ZoomEye, SecurityTrails. **Open sources**: no key required, and disabled unless the collector is explicitly enabled: crt.sh and Cert Spotter (certificate transparency), HackerTarget, CIRCL passive DNS, RDAP (registration data), the Internet Archive Wayback Machine, GitHub code search, and OpenCVE. Name resolution uses the container’s own resolver. No public DNS-over-HTTPS service is contacted in any mode; set `DNS_RESOLVER` only if you want lookups sent to a DoH endpoint you nominate. Self-hosted deployments running in air-gapped mode reach none of the above: every one of these collectors fails closed rather than falling back to a public endpoint. Provider use varies by feature, target, availability, and the permissions you grant. Optional testing providers receive only the task information needed for the feature you authorise. First-party product analytics are operated by BattleTest and are not used for third-party advertising; they are **off by default in self-hosted deployments** and are enabled only by setting `PUBLIC_ANALYTICS_ENABLED=1`. ## Security practices [Section titled “Security practices”](#security-practices) BattleTest uses layered administrative, organisational, and technical safeguards designed for the sensitivity of the data and the risks of a security-testing service. These include: * encrypted transport and protected storage; * role-based access and organisation boundaries; * restricted handling of credentials and other secret material; * request authentication, scoped integrations, and approval controls for higher-risk actions; * logging, monitoring, abuse prevention, and incident response processes; and * backup, recovery, dependency-management, and vulnerability-remediation practices. No internet service can guarantee absolute security. Keep your BattleTest account and connected providers secure, grant only required permissions, review organisation membership, and remove credentials or integrations that are no longer needed. ## Retention, export, and deletion [Section titled “Retention, export, and deletion”](#retention-export-and-deletion) Account and workspace records are generally retained while the relevant account or workspace is active. You can export supported reports and findings from the product. Account deletion removes associated customer data from active systems, subject to limited records retained for fraud prevention, security, legal compliance, dispute resolution, and backup rotation. Deleting an account may require transferring organisation ownership first. See the [Privacy Policy](https://battletest.dev/privacy) for details and privacy-request options. ## Security disclosures [Section titled “Security disclosures”](#security-disclosures) Report a suspected vulnerability in BattleTest to . Include enough information to reproduce the issue, avoid accessing other customers’ data, and do not disrupt the Service. We aim to acknowledge legitimate reports within two business days.
# Configure GitLab and Bitbucket integrations
> Set up GitLab and Bitbucket sign-in, repository authorization and BattleTest bot publishing.
Set `WEB_ORIGIN` to the public HTTPS dashboard origin. BattleTest derives its OAuth callbacks and repository webhook destinations from this value. Local HTTP is allowed only for `localhost` and `127.0.0.1`. Configure a stable `GITHUB_TOKEN_ENCRYPTION_KEY` containing 64 hexadecimal characters. The same key encrypts GitLab and Bitbucket access tokens, rotating refresh tokens and per-connection webhook secrets. Keep it with your backups. Do not change it on an existing deployment without migrating stored credentials. ## GitLab application [Section titled “GitLab application”](#gitlab-application) Create a server-side confidential application using [GitLab’s OAuth application instructions](https://docs.gitlab.com/integration/oauth_provider/). Select `api`, `openid`, `profile` and `email` scopes and register this redirect URI, replacing the origin with your own:
```text
https://battletest.dev/api/scm/oauth/gitlab/callback
```
Set these variables on the **API** service through its secret configuration:
```dotenv
GITLAB_URL=https://gitlab.com
GITLAB_OAUTH_CLIENT_ID=
GITLAB_OAUTH_CLIENT_SECRET=
```
`GITLAB_URL` can point to your self-managed GitLab HTTPS origin. It must have no path, query or credentials. The application must exist on that same instance. Changing the host requires reconnecting existing accounts. BattleTest uses the authorization code flow with PKCE. The connecting GitLab account needs Maintainer or Owner access to provision project service accounts and install webhooks. GitLab 18.11 or later supports project service accounts without feature flags, including Free tiers subject to service-account limits. When a maintainer enables a project, BattleTest creates a **BattleTest bot** service account scoped to that project, assigns Developer access and stores a separate encrypted API token. Review notes and statuses use this bot. Token rotation uses the bot’s own token; setup grants never publish reviews. You do not need a global GitLab bot account or bot token in server configuration. ## Bitbucket Cloud consumer [Section titled “Bitbucket Cloud consumer”](#bitbucket-cloud-consumer) Create an OAuth consumer in the intended Bitbucket workspace’s settings. Make it a private consumer and register this callback, replacing the origin with your own:
```text
https://battletest.dev/api/scm/oauth/bitbucket/callback
```
Enable these [OAuth consumer permissions](https://developer.atlassian.com/cloud/bitbucket/rest/intro/#bitbucket-oauth-20-scopes): | Consumer permission | OAuth scope | Used for | | ------------------------ | ------------- | ---------------------------------------------------- | | Account: Read | `account` | Identify the setup account and list its workspaces | | Email | `email` | Read verified email addresses for BattleTest sign-in | | Repositories: Read | `repository` | Discover and verify repository metadata | | Pull requests: Read | `pullrequest` | Access change metadata during setup | | Webhooks: Read and write | `webhook` | Install and remove repository webhooks | The consumer also supplies the identity and verified email for BattleTest sign-in. Bitbucket grants the permissions configured on its consumer for each authorization. These are setup permissions. BattleTest does not publish reviews using the authorizing account, even though the OAuth scope can permit commenting. Set the consumer’s key and secret on the **API** service:
```dotenv
BITBUCKET_OAUTH_CLIENT_ID=
BITBUCKET_OAUTH_CLIENT_SECRET=
```
The integration uses Bitbucket Cloud’s API. Bitbucket Data Center is not supported by this adapter. In a Bitbucket authorization URL, `client_id` is the consumer key. This URL authorizes an account; the Forge installation link below installs the BattleTest app. Start account connections from **Settings → Integrations** so BattleTest generates and validates the authorization state. A bare authorization link cannot complete a BattleTest connection. ## Bitbucket Forge app [Section titled “Bitbucket Forge app”](#bitbucket-forge-app) The scaffold is in the public [battletest/battletest repository](https://github.com/battletest/battletest/tree/main/integrations/bitbucket-forge). Register it as **BattleTest** in your Atlassian developer space. The supplied manifest sends lifecycle events, pull request events and a five-minute scheduled authorization refresh to your API using **app system tokens**, with user tokens disabled. 1. Use the Forge CLI to register the app and replace the placeholder `app.id` in its manifest. 2. Set the manifest’s remote `baseUrl` to your public dashboard origin. 3. Deploy to production, then enable sharing in the Atlassian developer console and copy the installation link. 4. Set these API variables to the exact app and environment IDs from Forge and the installation link: From the root of your `battletest/battletest` checkout, use Node.js 22 or 24 and run:
```sh
cd integrations/bitbucket-forge
bunx @forge/cli login
bun run register
bun run lint
bun run deploy
bunx @forge/cli environments list --json
bunx @forge/cli install --environment production --product Bitbucket
```
For the first installation, enter the shared workspace URL and approve the listed scopes. Use `bunx @forge/cli install --upgrade --environment production --product Bitbucket` after a deployment changes scopes or remote URLs on an existing installation. Check installations with `bunx @forge/cli install list --json`. Forge login uses an Atlassian API token. Keep it in the local keychain or the CLI’s environment configuration. It is separate from the Bitbucket OAuth consumer secret. See [Forge login](https://developer.atlassian.com/platform/forge/cli-reference/login/).
```dotenv
BITBUCKET_FORGE_APP_ID=ari:cloud:ecosystem::app/
BITBUCKET_FORGE_ENVIRONMENT_ID=ari:cloud:ecosystem::environment//
BITBUCKET_FORGE_INSTALL_URL=https://developer.atlassian.com/console/install/
```
Use the app UUID from `manifest.yml` and the production environment UUID returned by `environments list`. Keep the full `ari:cloud:ecosystem::environment/` value, including both UUIDs. The installation URL comes from **Distribution → Sharing** in the developer console; the distribution settings page and Bitbucket OAuth authorization URL are different links. The API verifies Atlassian’s JWT signature, audience, issuer, expiry, environment, endpoint module and workspace before storing a system token. Tokens remain opaque, encrypted at rest and usable locally for at most ten minutes from the invocation; scheduled refresh maintains authorization. Reviews still re-check enabled repository selection on every operation. Failed provider authentication is an error, not a reason to use the setup user’s grant. The Forge app requests `read:app-system-token`, `read:user:bitbucket`, `read:repository:bitbucket`, `read:pullrequest:bitbucket` and `write:pullrequest:bitbucket`. That write scope permits comments and broader PR actions. BattleTest uses comments and statuses, without approving or merging changes. The app does not request repository code-write scope. Maintainers install this app in a shared Bitbucket workspace through Settings’ installation link before enabling repositories. Personal workspaces do not support Forge apps. See [Forge Remote](https://developer.atlassian.com/platform/forge/remote/essentials/). ## Sign-in and linked accounts [Section titled “Sign-in and linked accounts”](#sign-in-and-linked-accounts) Set `AUTH_MODE=both` to offer password sign-in and all configured social providers, or `AUTH_MODE=oauth` for social sign-in alone. `local` keeps password sign-in alone; `github` keeps GitHub sign-in alone. GitLab and Bitbucket reuse the applications and callback URLs above. Sign-in appears when that provider’s client ID and secret are configured. Bitbucket sign-in does not require a Forge installation. Users can continue with a provider on the login or signup page, or link one in **Settings → Account → Linked accounts**. `OPEN_SIGNUP=false` blocks creation of new GitLab and Bitbucket accounts while allowing returning users to sign in. When a verified email already belongs to another BattleTest account, the user must sign in to that account and link the provider explicitly. Linked accounts provide sign-in access. Repository connections in **Settings → Integrations** separately authorize reviews for an organisation. Users must keep at least one enabled sign-in method when unlinking accounts. ## Routing and first connection [Section titled “Routing and first connection”](#routing-and-first-connection) Restart or redeploy the API after changing configuration. The web service must proxy these public endpoints to the API: * `/api/scm/oauth/gitlab/callback` * `/api/scm/oauth/bitbucket/callback` * `/webhook/source-control/` * `/webhook/forge/bitbucket` The shipped SvelteKit routes handle these paths. Keep them reachable through your reverse proxy. Repository connection callbacks require the same signed-in BattleTest user who started authorization. Sign-in callbacks validate browser state and create a BattleTest session; linking requires the account session that started the flow. Webhook delivery does not require a browser session. Open **Settings → Integrations**, connect an account and select repositories. Do not manually copy a connection webhook URL to another project’s webhook configuration. Reviews always use BattleTest automation identities. Instance administrators register the integrations once; repository maintainers authorize, install where required and select repositories without creating bot accounts. ## Verify the deployment [Section titled “Verify the deployment”](#verify-the-deployment) Use a disposable test repository with no sensitive code. Confirm OAuth returns to the intended workspace, enabling a repository installs one webhook, and a non-draft merge request or pull request targeting the default branch produces a review and status for the correct commit. Then push a new commit, change draft state, close the change and disconnect. Also confirm a disabled repository and an invalid webhook signature cannot start reviews. Verify the displayed author is BattleTest bot, revoke its credential or uninstall the app, and confirm no review is posted as the maintainer. Test GitLab bot rotation and Bitbucket scheduled refresh, including a manual review after an idle period, before relying on unattended operation.
# Run live application testing
> Start a battletest, choose targets and invitees, reuse a previous configuration, and review results.
Use **Battletests** in your Enterprise deployment to start testing and review past results. You need permission to run scans in the target’s organization. ## Start a battletest [Section titled “Start a battletest”](#start-a-battletest) Open **Battletests** and select **Start battletest**. 1. **Targets:** Choose an organization and select the hosts to test. Search by hostname or filter by environment. To register a missing host, select **Add target**. Registration is checked against any configured scope restrictions before you continue. Enterprise deployments do not require DNS verification. 2. **Testing level:** Choose **Discover**, **Probe**, or **Full exploit**. Full exploit requires an explicit authorization confirmation for the selected targets. 3. **People:** Search organization members by name, email, or username. Selected people receive viewer access and an invitation notification when testing starts. Select **Save as my default** to reuse that invitation list for future battletests in this organization. 4. **Review:** Check the targets, level, and people, then select **Start battletest**. Each selected target starts a separate battletest. If some targets cannot start, the dialog lists their errors and keeps only those targets selected for retry. The battletests that started remain in your history. [Prepare a target](/docs/how-to/adding-a-target) describes authorization, reachability, and the account requirements for authenticated coverage. ## Reuse a configuration [Section titled “Reuse a configuration”](#reuse-a-configuration) Select **Use configuration** beside a past battletest, or **Use a previous battletest** in the launch dialog. Review the loaded targets, testing level, and people before starting. Full exploit authorization must be confirmed again. Remove unavailable targets or people before continuing. Older battletests may not have a complete saved configuration. The dialog loads the recorded values and asks you to select any missing testing level. ## Find past battletests [Section titled “Find past battletests”](#find-past-battletests) Search by target or environment, then filter by status, testing level, environment, or date. Open a battletest to follow progress and inspect its findings. Completed and failed entries also provide a report link. ## What happens during testing [Section titled “What happens during testing”](#what-happens-during-testing) A run maps the application, tests within its selected level, and records findings and coverage. ### 1. Discovery [Section titled “1. Discovery”](#1-discovery) Discovery is passive. It sends ordinary HTTP requests to map what the application exposes, and nothing it does is distinguishable from normal traffic. It takes 5–15 minutes depending on application size, and it produces: * An **endpoint inventory**: pages, API routes, and forms the crawler found * A **technology fingerprint**: what stack the application appears to run * An **SSL/TLS assessment** and header analysis * Informational findings from the passive pass At the lowest permission tier this is where a run stops. There are no injection findings, no port scan results, and no active probes, because the tooling that would do those things is not available to the agent at that tier. ### 2. Active probing [Section titled “2. Active probing”](#2-active-probing) Raising the permission tier makes injection testing, directory enumeration, and port scanning available. This is an explicit, recorded approval, and steps that could change state pause for a human decision. A typical application takes 15–30 minutes at this stage. The [permission tier model](/docs/concepts/live-testing#the-permission-tier-model) describes what each tier permits. ### 3. Confirmation [Section titled “3. Confirmation”](#3-confirmation) Confirmation decides what ends up in the report. A finding is marked **Confirmed** only when the agent replayed it against the running system and reproduced it from a second, unrelated account. A confirmed finding carries: * The endpoint and parameter that were vulnerable * The exact payload that triggered it * The server response that confirmed it * Reproduction steps you can run yourself * Remediation guidance for your stack Findings marked **Potential** were detected but not confirmed. They are reported as unconfirmed, and the report says why confirmation did not happen. A run also reports what it could **not** reach. An SSO-only login wall, an unreachable subdomain, or a tier that withheld the tooling needed to prove something are all stated in the record. Coverage you did not get is a fact about the assessment. ### 4. Comparison [Section titled “4. Comparison”](#4-comparison) Each run is compared against the previous run of the same environment. Findings are classified **new**, **persistent**, **resolved**, or **regression**. Regressions get the most attention: they mean something the team already spent time fixing has come back. ## What’s next [Section titled “What’s next”](#whats-next) [Manage findings](/docs/how-to/manage-findings) [Dismiss false positives, mark resolved, and track open issues.](/docs/how-to/manage-findings) [Export for audits](/docs/how-to/export-findings) [Export findings as PDF or JSON for SOC 2 evidence.](/docs/how-to/export-findings) [Scan results reference](/docs/reference/scan-results) [What every field in a scan report means.](/docs/reference/scan-results) [Prepare a target](/docs/how-to/adding-a-target) [What an application needs in place before a run can cover it properly.](/docs/how-to/adding-a-target)
# Get Your First PR Security Review
> Connect a GitHub repository to BattleTest, open a pull request, and read your first automated security review with CVE, secret, and AI findings in 5 minutes.
In this tutorial you’ll set up a BattleTest workspace, connect a GitHub repository, and get a real automated security review posted on a pull request. By the end you’ll have seen what the tool does and be ready to use it on your own work. You need a GitHub account and at least one repository with an open (or easily openable) pull request. Sign up Go to [battletest.dev/login](https://battletest.dev/login) and click **Continue with GitHub**. GitHub will ask you to authorise BattleTest to read your profile and email. Click **Authorize**. BattleTest opens a short two-step setup: first you’ll name your workspace, then connect GitHub. Create your workspace A workspace is where your repositories, findings, team, and billing live. Enter a name (your own or your company’s is fine) and click **Create workspace**. You can rename it later, so don’t overthink it. Connect the GitHub App On the next step, click **Install GitHub App**. GitHub opens the App installation page. Choose the account or organisation that owns the repository you want to review. Under **Repository access**, select **Only select repositories** and pick one repository to start with. Click **Install & Authorize**. You’re returned to BattleTest and land on your dashboard. Your repository now appears with a green *Connected* badge. Open a pull request Go to your repository on GitHub and open a pull request: any change, however small. If one is already open, push any new commit to it. Within 30 seconds, a new check appears at the bottom of the PR page: *BattleTest Security Review — In progress*. Read the security review After 2–4 minutes, BattleTest posts a comment from **battletest\[bot]**. The comment opens with a **risk score** (0–100). Scores up to 20 are clean: the diff looks clear. Scores above 60 mean findings require attention before merging. [How risk scores are calculated →](/docs/concepts/risk-score) Below the score, findings are listed by severity: CRITICAL first, then HIGH, MEDIUM, LOW. Each finding shows: * The file and line number * What the vulnerability is, in plain English * The vulnerable code in context * A drop-in fix with a working code example * CWE and OWASP category references If the PR has no findings, the comment says so: that’s also useful signal. Not every finding requires immediate action. Read [Understanding findings](/docs/concepts/findings) to know how to triage them. View the full run in the dashboard Click **View full report →** at the bottom of the PR comment. This opens the run detail page where you can see expanded reasoning for each finding and compare this PR against others on the same repository. Every pull request on this repository is now reviewed automatically. No manual trigger, no YAML to write, no runner minutes, no configuration to maintain. Every PR that opens from this point forward gets a full security pass: dependency CVEs, exposed secrets, injection patterns, config gaps. Here’s what a typical finding looks like in the PR comment: Screenshot to addA single finding in the PR comment: severity, type, and file:line, followed by the explanation, the vulnerable code, the fix, and CWE/OWASP references. The finding tells you what’s wrong, where it is, why it’s a problem, and what to type to fix it, in your language and framework. ## What’s next [Section titled “What’s next”](#whats-next) [PR Findings reference](/docs/reference/pr-findings) [What each field in a finding means, how risk scores are calculated, and the finding lifecycle.](/docs/reference/pr-findings) [Run a live security test](/docs/tutorials/live-testing) [Test your running infrastructure, not just the code.](/docs/tutorials/live-testing) [Require review before merging](/docs/how-to/require-security-review) [Block PRs with the BattleTest status check.](/docs/how-to/require-security-review) [Add a live target](/docs/how-to/adding-a-target) [Add a domain for autonomous security assessment.](/docs/how-to/adding-a-target)
# The in-app AI assistant
> A read-only AI assistant built into the BattleTest app that answers questions about your findings, PR reviews, and usage, grounded in your own data and the documentation.
BattleTest ships with an AI assistant inside the app: the sparkle button in the bottom corner of any signed-in page. It answers questions about your account in plain language and links you to the right place to act. ## What it can do [Section titled “What it can do”](#what-it-can-do) The assistant is grounded in two things: this documentation, and **your own data** (read through a small set of read-only tools scoped to your signed-in account). It can: * **Explain your findings and PR reviews**: “why was this PR flagged?”, “what are my open critical findings?”, “summarise my last battletest.” * **Answer usage and limit questions**: “am I close to a review limit?”, “how many in-depth reviews have I used this month?”. * **Explain how the product works**: anything covered in these docs: risk scores, the finding lifecycle, PR review vs live application testing, notifications. * **Take you there**: when doing something is the answer, it offers a button that deep-links to the exact page (a specific finding, your notification settings, source-control settings). ## What it will not do [Section titled “What it will not do”](#what-it-will-not-do) * It reads targets, findings, battletest history, and change reviews in organisations where your role permits access, plus your legacy personal targets. It cannot read an organisation where you lack permission. * It **won’t invent specifics.** Exact numbers, thresholds, tiers, and limits come from the documentation or your account data. If something isn’t documented, it says so rather than guessing. * It isn’t a security scanner. It reports on battletests and PR reviews that have already run. It does not test your site itself. ## Privacy [Section titled “Privacy”](#privacy) Conversations are tied to your user account and are used only to answer your questions. The assistant’s data tools are read-only and scoped to your signed-in identity, so it can never surface another customer’s findings or account. See [Security & Data Handling](/docs/reference/security-and-data) for how BattleTest handles your data overall. ## Tips [Section titled “Tips”](#tips) * Ask about your **current situation** directly: “my usage this month”, “my open findings on `example.com`”, “my last three PR reviews.” The assistant looks these up rather than guessing. * If an answer needs an exact number that isn’t in the docs, the assistant tells you where in the app to find it. * Prefer to use your own AI tools (Claude, Cursor, ChatGPT)? You can connect the BattleTest documentation to them directly: see [Connect the docs to your AI tools](/docs/using-ai/docs-mcp). ## Documentation index [Section titled “Documentation index”](#documentation-index) Raw documentation ships with the deployment. The assistant can search it without an embedding model. For semantic search, configure an embedding model in **Settings → Providers → AI models**, then select **Build index** under **Documentation index**. The same action is available during setup. The build uses the configured model and may incur charges from that provider. The index is stored in the deployment database and survives restarts. **Ready** means the saved index matches the shipped documentation and configured model. If either changes, select **Rebuild index** when **Update available** appears. **Build failed** offers a retry. A failed rebuild preserves the previously stored vectors; the assistant uses raw documentation until an index matches the current configuration. Enterprise deployments do not build or rebuild the documentation index automatically on startup.
# Connect the docs to your AI tools
> Point Claude, Cursor, ChatGPT or any AI coding tool at the BattleTest documentation as llms.txt, so answers come from the current docs instead of a model's memory. Public and read-only.
The BattleTest documentation is published in the **llms.txt** format, so AI coding tools can read and cite it directly. Answers about BattleTest come from the current documentation instead of a model’s memory. ## The files [Section titled “The files”](#the-files) | File | What it is | | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | [`/docs/llms.txt`](/docs/llms.txt) | An index of the documentation: every page, with its title, description and URL. Small; a good starting point for a tool to decide what to read. | | [`/docs/llms-full.txt`](/docs/llms-full.txt) | The **entire documentation** as a single Markdown file. Give this to a tool when you want it grounded in everything at once. | | [`/docs/llms-small.txt`](/docs/llms-small.txt) | A trimmed full-text build for smaller context windows. | ## Use it [Section titled “Use it”](#use-it) **Claude, ChatGPT and other chat tools**: paste the URL `https://battletest.dev/docs/llms-full.txt` into the conversation, or attach it as context, then ask your question. Many clients fetch and read the file automatically. **Cursor, Windsurf and other editors**: add `https://battletest.dev/docs/llms.txt` as a documentation source (in Cursor: **Settings → Features → Docs → Add new doc**). The editor indexes it and your assistant cites BattleTest docs when relevant. **Your own agents**: fetch `https://battletest.dev/docs/llms-full.txt` and include it in the model’s context. It is plain Markdown with stable headings.