Bastion / Documentation

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
VariableEffect
BASTION_VERSIONRelease tag to install, for example v0.1.0. Default: latest.
BASTION_INSTALL_DIRTarget directory.
BASTION_REQUIRE_SIGNATURE=1Fail if cosign is missing instead of skipping the signature check.
BASTION_DOWNLOAD_URLA 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.

FlagDescriptionDefault
--color stringcolour output: auto, always, never"auto"
--db stringdirectory with extra advisory JSON files (overrides built-ins with the same id)
--format stringoutput format: text, json or sarif (scan and trace)"text"
--glob stringextra glob of PHP binaries to inspect (repeatable), e.g. --glob '/srv/*/bin/php'
--image stringalso scan this container image (repeatable); read with docker save, never run
--imagesalso scan every local container image (reads each image once; can take a while)
--jsonprint machine-readable JSON instead of the terminal view
--no-containersskip Docker/Podman containers
--no-execnever execute anything (no php -v, no docker exec); rely on static analysis only
--sysroot stringscan 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.

FlagDescriptionDefault
--color stringcolour output: auto, always, never"auto"
--db stringdirectory with extra advisory JSON files (overrides built-ins with the same id)
--format stringoutput format: text, json or sarif (scan and trace)"text"
--jsonprint machine-readable JSON instead of the terminal view
--log stringextra log file or glob to analyse (repeatable)
--max-file-size intread at most this many bytes per file4194304
--no-autoonly scan directories given as arguments (no web root discovery)
--no-logsskip web server / PHP log analysis
--no-persistenceskip cron, systemd, SSH key and account checks
--since durationtime window for log analysis and the change timeline (e.g. 72h, 720h)720h0m0s
--sysroot stringinspect 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.

FlagDescriptionDefault
--color stringcolour output: auto, always, never"auto"
--db stringdirectory with extra advisory JSON files (overrides built-ins with the same id)
--format stringoutput format: text, json or sarif (scan and trace)"text"
--glob stringextra glob of PHP binaries to inspect (repeatable), e.g. --glob '/srv/*/bin/php'
--image stringalso scan this container image (repeatable); read with docker save, never run
--imagesalso scan every local container image (reads each image once; can take a while)
--jsonprint machine-readable JSON instead of the terminal view
--no-containersskip Docker/Podman containers
--no-execnever execute anything (no php -v, no docker exec); rely on static analysis only
--no-logsskip log analysis
--no-traceonly include the vulnerability scan
--o stringoutput filebastion-report-<host>-<date>.html
--scan stringbuild the report from a saved bastion scan --json file instead of scanning
--since durationtrace time window720h0m0s
--sysroot stringscan a mounted filesystem (image, backup, chroot) instead of the live host
--trace stringbuild 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)'
FlagDescriptionDefault
--color stringcolour output: auto, always, never"auto"
--db stringdirectory with extra advisory JSON files (overrides built-ins with the same id)
--format stringoutput format: text, json or sarif (scan and trace)"text"
--jsonprint machine-readable JSON instead of the terminal view
--os stringdistribution as id:version (e.g. ubuntu:24.04) when the pasted text does not say

Output formats and exit codes

FormatDescription
--format textTerminal view, the default. Colour follows --color, NO_COLOR and whether output is a terminal.
--format jsonVersioned documents: bastion.scan/v1 and bastion.trace/v1. --json is a shorthand.
--format sarifSARIF 2.1.0 with security-severity, for GitHub code scanning and other dashboards. Fixed builds are omitted.
Exit codeMeaning
0No affected builds, no indicators.
1A vulnerable or likely vulnerable build, or high-severity indicators.
2Some builds could not be verified, or only medium indicators.
64, 70Usage 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

fixedProven: 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.
vulnerableProven: 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 vulnerableNo 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.
unknownNot 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.

FieldMeaning
branchesFixed upstream release per branch; branches without one have no release.
vendor_fixesExact fixed package versions. A release without a dot ("9") matches every minor release; branch limits the entry to one upstream branch.
vendor_policiesVendors whose security database was checked and records no backport below the upstream fix.
build_date_fixesVendor rebuilds that kept the version number and are identified by compile date.
changelog_markersStrings that, found in an installed package's changelog, prove a backport.
fingerprintsByte strings that only patched binaries contain.
config_checksphp.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>.