Install, run and read the results
Reference for the bastion command line. Flag tables on this page are generated from the binary's own help output.
Install
One static binary for Linux and macOS, amd64 and arm64. The script picks the build for your platform from the latest release, checks its SHA-256 against SHA256SUMS and, if cosign is installed, verifies the Sigstore signature of that file. It installs to /usr/local/bin (or ~/.local/bin without root) and changes nothing else.
curl -fsSL https://larptard.github.io/bastion/install.sh | sh
| Variable | Effect |
|---|---|
| BASTION_VERSION | Release tag to install, for example v0.1.0. Default: latest. |
| BASTION_INSTALL_DIR | Target directory. |
| BASTION_REQUIRE_SIGNATURE=1 | Fail if cosign is missing instead of skipping the signature check. |
| BASTION_DOWNLOAD_URL | A mirror with the same release files. Checksum and signature checks still apply. |
Verify by hand
VERSION=v0.1.0 BASE=https://github.com/larptard/bastion/releases/download/$VERSION curl -fsSLO $BASE/bastion_linux_amd64.tar.gz -O $BASE/SHA256SUMS -O $BASE/SHA256SUMS.sigstore.json sha256sum --ignore-missing -c SHA256SUMS cosign verify-blob --bundle SHA256SUMS.sigstore.json \ --certificate-identity-regexp '^https://github.com/larptard/bastion/.github/workflows/release.yml@refs/tags/v' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com SHA256SUMS gh attestation verify bastion_linux_amd64.tar.gz --repo larptard/bastion # build provenance
From source
go install github.com/larptard/bastion/cmd/bastion@latest # Go 1.24+ # or git clone https://github.com/larptard/bastion && cd bastion && make build
Quick start
sudo bastion scan # every PHP build: patched or not sudo bastion scan --images # also every local container image sudo bastion trace # signs of compromise in web roots, persistence, logs sudo bastion report # both, as one offline HTML file php -v | bastion check # assess pasted output, no root needed
Run as root for full coverage. Nothing is modified; remediation steps are printed for you to apply.
bastion scan
Finds PHP builds in the standard locations of distributions and hosting panels, in $PATH, in running processes (php-fpm, php-cgi, lsphp, FrankenPHP, Apache with mod_php, including processes still running a replaced binary), in Docker and Podman containers and other mount namespaces, and, on request, in container images. For each build it reads the version and build date from the binary, looks up the owning dpkg, rpm or apk package and its changelog, evaluates every advisory and, for builds that are not fixed, audits the interim mitigation settings in php.ini and PHP-FPM pools.
| Flag | Description | Default |
|---|---|---|
| --color string | colour output: auto, always, never | "auto" |
| --db string | directory with extra advisory JSON files (overrides built-ins with the same id) | |
| --format string | output format: text, json or sarif (scan and trace) | "text" |
| --glob string | extra glob of PHP binaries to inspect (repeatable), e.g. --glob '/srv/*/bin/php' | |
| --image string | also scan this container image (repeatable); read with docker save, never run | |
| --images | also scan every local container image (reads each image once; can take a while) | |
| --json | print machine-readable JSON instead of the terminal view | |
| --no-containers | skip Docker/Podman containers | |
| --no-exec | never execute anything (no php -v, no docker exec); rely on static analysis only | |
| --sysroot string | scan a mounted filesystem (image, backup, chroot) instead of the live host |
bastion trace [dir…]
Discovers web roots from nginx, Apache, LiteSpeed and Caddy configuration and the default paths of common panels, plus any directories you pass. It reports webshell signatures, PHP in uploads and images, .user.ini and .htaccess abuse, suspicious cron, systemd and shell start-up entries, /etc/ld.so.preload, extra UID 0 accounts, SSH keys for web users, executables in temp directories, global auto_prepend_file, and log indicators, then builds a timeline. The rule catalogue lists every check.
| Flag | Description | Default |
|---|---|---|
| --color string | colour output: auto, always, never | "auto" |
| --db string | directory with extra advisory JSON files (overrides built-ins with the same id) | |
| --format string | output format: text, json or sarif (scan and trace) | "text" |
| --json | print machine-readable JSON instead of the terminal view | |
| --log string | extra log file or glob to analyse (repeatable) | |
| --max-file-size int | read at most this many bytes per file | 4194304 |
| --no-auto | only scan directories given as arguments (no web root discovery) | |
| --no-logs | skip web server / PHP log analysis | |
| --no-persistence | skip cron, systemd, SSH key and account checks | |
| --since duration | time window for log analysis and the change timeline (e.g. 72h, 720h) | 720h0m0s |
| --sysroot string | inspect a mounted filesystem (image, backup) instead of the live host |
bastion report
Runs scan and trace and writes bastion-report-<host>-<time>.html: one file with inline styles, icons and data, no external requests, printable. It contains host details, so share it deliberately. Use --scan and --trace to build a report from saved JSON, for example collected from several servers. Sample report.
| Flag | Description | Default |
|---|---|---|
| --color string | colour output: auto, always, never | "auto" |
| --db string | directory with extra advisory JSON files (overrides built-ins with the same id) | |
| --format string | output format: text, json or sarif (scan and trace) | "text" |
| --glob string | extra glob of PHP binaries to inspect (repeatable), e.g. --glob '/srv/*/bin/php' | |
| --image string | also scan this container image (repeatable); read with docker save, never run | |
| --images | also scan every local container image (reads each image once; can take a while) | |
| --json | print machine-readable JSON instead of the terminal view | |
| --no-containers | skip Docker/Podman containers | |
| --no-exec | never execute anything (no php -v, no docker exec); rely on static analysis only | |
| --no-logs | skip log analysis | |
| --no-trace | only include the vulnerability scan | |
| --o string | output file | bastion-report-<host>-<date>.html |
| --scan string | build the report from a saved bastion scan --json file instead of scanning | |
| --since duration | trace time window | 720h0m0s |
| --sysroot string | scan a mounted filesystem (image, backup, chroot) instead of the live host | |
| --trace string | build the report from a saved bastion trace --json file instead of tracing |
bastion check
Assesses text instead of the machine: the output of php -v, optionally with dpkg-query -W 'php*', rpm -qa 'php*', apk info -v or /etc/os-release. It uses the same engine as the online check, and recognises Plesk builds from /opt/plesk/php/ paths.
ssh web-01 'php -v; dpkg-query -W "php*"' | bastion check bastion check --os ubuntu:24.04 'PHP 8.3.6 (cli)'
| Flag | Description | Default |
|---|---|---|
| --color string | colour output: auto, always, never | "auto" |
| --db string | directory with extra advisory JSON files (overrides built-ins with the same id) | |
| --format string | output format: text, json or sarif (scan and trace) | "text" |
| --json | print machine-readable JSON instead of the terminal view | |
| --os string | distribution as id:version (e.g. ubuntu:24.04) when the pasted text does not say |
Output formats and exit codes
| Format | Description |
|---|---|
| --format text | Terminal view, the default. Colour follows --color, NO_COLOR and whether output is a terminal. |
| --format json | Versioned documents: bastion.scan/v1 and bastion.trace/v1. --json is a shorthand. |
| --format sarif | SARIF 2.1.0 with security-severity, for GitHub code scanning and other dashboards. Fixed builds are omitted. |
| Exit code | Meaning |
|---|---|
| 0 | No affected builds, no indicators. |
| 1 | A vulnerable or likely vulnerable build, or high-severity indicators. |
| 2 | Some builds could not be verified, or only medium indicators. |
| 64, 70 | Usage error, runtime error. |
sudo bastion scan --json | jq -r '.instances[] | select(.assessments[0].status != "fixed") | "\(.target) \(.path) \(.assessments[0].status)"' sudo bastion trace --format sarif > trace.sarif
Statuses
| fixed | Proven: upstream version at or above the fix, a vendor package at or above the vendor's fixed version, a dated vendor rebuild, a changelog naming the CVE, or code only the fix contains. |
| vulnerable | Proven: below the upstream fix with no package manager involved, a vendor package older than the vendor's fixed version, or a vendor build dated before its fixed rebuild. |
| likely vulnerable | No evidence of a fix where evidence would be expected: an end-of-life branch with only a source backport, a package whose vendor database or changelog is silent, or a process still running a replaced binary. |
| unknown | Not enough information to decide either way. |
Advisory data
Each vulnerability is one JSON document in internal/advisory/data/, validated on load. Add a file and rebuild, or point --db at a directory to add or override advisories at run time. bastion advisories prints what is loaded.
| Field | Meaning |
|---|---|
| branches | Fixed upstream release per branch; branches without one have no release. |
| vendor_fixes | Exact fixed package versions. A release without a dot ("9") matches every minor release; branch limits the entry to one upstream branch. |
| vendor_policies | Vendors whose security database was checked and records no backport below the upstream fix. |
| build_date_fixes | Vendor rebuilds that kept the version number and are identified by compile date. |
| changelog_markers | Strings that, found in an installed package's changelog, prove a backport. |
| fingerprints | Byte strings that only patched binaries contain. |
| config_checks | php.ini directives audited for unfixed builds. safe is off, an exact value, or a size bound like <=32M. |
{
"schema": "bastion.advisory/v1",
"id": "EXAMPLE-2027",
"name": "Example",
"product": "php",
"published": "2027-01-01",
"severity": "high",
"cvss": { "version": "4.0", "score": 8.1, "vector": "CVSS:4.0/…" },
"aliases": ["CVE-2027-0001"],
"vulnerabilities": [{ "cve": "CVE-2027-0001", "ghsa": "", "title": { "en": "…" }, "url": "https://…" }],
"summary": { "en": "…" },
"branches": [
{ "branch": "8.5", "fixed": "8.5.20", "status": "supported" },
{ "branch": "8.1", "status": "eol" }
],
"older_branches": "affected",
"newer_branches": "fixed",
"vendor_fixes": [
{ "distro": "ubuntu", "release": "24.04", "package": "php8.3", "fixed": "8.3.6-0ubuntu0.24.04.20", "format": "dpkg", "advisory": "USN-…", "url": "https://…" },
{ "distro": "rhel", "release": "9", "branch": "8.2", "package": "php", "fixed": "8.2.40-1.module+el9…", "format": "rpm", "advisory": "RHSA-…", "url": "https://…" }
],
"vendor_policies": [{ "distro": "alpine", "source": "Alpine secdb", "checked": "2027-01-02", "url": "https://…" }],
"build_date_fixes": [{ "environment": "Plesk", "branches": ["8.1"], "fixed_on": "2027-01-01", "source": "…", "url": "https://…" }],
"changelog_markers": ["CVE-2027-0001"],
"fingerprints": [],
"mitigations": [{ "en": "…" }],
"config_checks": [{ "setting": "memory_limit", "default": "128M", "safe": "-1", "text": { "en": "…" } }],
"references": [{ "title": "…", "url": "https://…" }]
}
Webshell and obfuscation signatures follow the same approach in internal/trace/rules/content.json.
FAQ
Does Bastion change anything on the server?
No. It opens files and reads /proc. It never writes outside the report file you ask for, never restarts services, never deletes or quarantines. When a binary cannot be read statically it may run php -v, and when a container filesystem is not readable it may use docker exec; --no-exec turns both off. Image scanning uses docker save, which creates no container.
Does it send data anywhere?
No. scan, trace, check and report make no network connections; the Docker and Podman CLIs only talk to the local daemon. The vulnerability data is compiled in. The website's online check also runs in your browser.
Why does it need root?
To see other users' processes (php-fpm workers, Apache), container filesystems under /proc/<pid>/root, crontabs and web server logs. Without root it still runs and lists what it could not see.
Why is a build reported as unknown?
Bastion could not prove it either way. Typically this is a distribution package from a vendor without fixed-version data whose changelog is not installed. Ask the vendor whether CVE-2026-109618 and CVE-2026-109619 are fixed in that package.
I built PHP 8.1 from the official backport branch. Why likely vulnerable?
The backport branches keep the version string (8.1.34) and add no marker that can be read from the binary, so a patched build is indistinguishable from an unpatched one. Bastion reports what it can prove.
If trace finds nothing, is the server clean?
Not necessarily. Signatures miss novel backdoors, and an attacker with root can hide from any on-host tool. A clean trace lowers the odds; it is not proof. If you suspect a breach, image the disk and investigate offline: --sysroot works on a mounted image.
What about false positives?
File signatures were tuned against about 33,000 PHP files from WordPress, Drupal, phpMyAdmin, Laravel, Symfony, phpseclib and PHPMailer with no high or medium matches; CI repeats the check on every change. Weak signals are only reported when several appear in one file. Each finding names its rule and shows the matching line.
Containers, images, Kubernetes, LXC?
Running Docker and Podman containers are read through /proc/<pid>/root. Any other process with its own root filesystem (Kubernetes pods via containerd or CRI-O, LXC, chroots) is found in /proc and scanned the same way. Images are scanned with --image REF or --images.
Windows?
scan and trace run on Linux and macOS. For Windows, use the online check with php -v.
How current is the vulnerability data?
It is compiled into each release and reflects official sources at that time. PHP2Shell data comes from php.net, the GitHub advisories, the Ubuntu and Debian trackers, Red Hat errata, Alpine's secdb and Plesk's advisory, as of 22 October 2026. Newer data can be loaded without upgrading with --db <dir>.
