Legion Runner
Version updated for https://github.com/Wraith-security/Legion_runner to version v1.0.43.
- This action is used across all versions by 8 repositories.
Action Type
This is a Node action using Node version 24.
Go to the GitHub Marketplace to find the latest changes.
Action Summary
Legion Runner is an open-source security tool that hardens GitHub Actions against supply chain attacks by monitoring outbound connections, blocking unauthorized destinations, and identifying processes behind them. It provides a comprehensive defense mechanism that runs on pure Node built-ins without dependencies and offers features like egress policy control, process attribution, and file integrity checks.
What’s Changed
Added
- Curated egress presets (
allowed-presets): opt-in per-ecosystem allowlists (npm, yarn, pnpm, pip, pypi, cargo, rust, go, maven, gradle, nuget, apt, debian, docker) so block mode “just works” for common toolchains without hand-listing endpoints. e.g.allowed-presets: "cargo, apt". Unit-tested. - Download integrity verification: the action verifies the
legionr-bpf/legionr-fimrelease binaries against a.sha256sidecar before running them (the release now attaches the checksums), and fails closed โ an unverified/corrupted/tampered download is rejected and the action degrades instead of executing it. learned-baselineinput (defaulttrue): in block mode, also allow destinations previously learned into the Actions cache. Setfalseto enforce ONLY the explicit allowlist (inline + policy-file + GitHub) with no cache read/write โ used by the enforce self-test for deterministic deny.- File-integrity / tamper detection (Rust
legionr-fimagent): snapshots high-value tamper targets at job start (credential/config files,.gitconfig + hooks, and checked-out source) and diffs them at job end, surfacing anything overwritten, deleted, or chmod’d in the summary. Only sha256 hashes are stored โ never contents. New inputsfile-integrity(auto|off) andfim-extra-paths.file-integrity: autodownloads the agent from the latest release (plain stable Rust, no eBPF toolchain) and degrades to a silent skip if unavailable. Logic lives inlegionr-core::fim(unit-tested); the binary is a release asset likelegionr-bpf, built + attached byrelease.yml. - Package repositories roll-up (
๐ฆ): the summary now classifies named outbound destinations into their ecosystem/registry (npm, PyPI, crates.io, apt, Docker, Go, NuGet, Maven, Gradle, RubyGems, Alpine, GitHub) and shows a Package repositories reached table โ registry, ecosystem, connections, and the process that reached each. Supply-chain risk hides in which registries a build talks to, so we surface them directly instead of leaving you to read IPs. Bare IPs that never got a forward name get a coarse CDN/provider hint (Fastly/Cloudflare/GitHub) via CIDR match โ honest about ambiguity (a shared CDN can’t name a registry). Logic inaction/repos.js, fully unit-tested. - Combined cross-job egress report (one summary for the whole run): GitHub
has no run-level summary, so each job emits its captured egress as a JSON
artifact (
node action/report.js emit) and a finalegress-reportjob merges them into a SINGLE table โ which job + process reached what โ with a package repositories roll-up and a per-job diagnostics block (render). Wired into CI;renderis pure and unit-tested. Pairs withjob-summary: falseso the run shows one combined summary instead of one table per job. job-summaryinput (defaulttrue): setfalseto keep monitoring and enforcement fully active but suppress the connections table in the job summary. Useful when many jobs in one workflow each run the action and you only want the table once (our own CI uses it so a run shows one table, not one per job).- Secure diagnostics line in the summary: reports which resolution path
actually fired (
forwarder on/off ยท captured DNS records N ยท getaddrinfo route โฆ ยท named X/Y destinations) so a run that comes back as bare IPs is triagable. Secure by construction โ only booleans, counts, and a fixed enum; never the upstream resolver IP, file paths, captured hostnames, or env values.
Fixed
- Block mode no longer hangs the runner at teardown.
applyEgressBlockinstalled a default-denyLEGION_EGRESSchain inOUTPUTand nothing ever removed it, so the runner’s own completion call (to rotating GitHub-backend IPs not in the static seed) was dropped and the job spun until timeout.post()now tears the firewall down (removeEgressBlock). - Runner hang from leaked daemons. The post step left privileged background
processes alive โ eBPF agent, DNS forwarder โ and the
/procmonitor could wedge in a blockingsssubprocess. The monitor now reads/proc/net/tcpdirectly (no subprocess); daemons are reliably reaped. - No more spurious “could not resolve” annotations. Allowlist entries that
are wildcard parents with no A record of their own (e.g.
blob.core.windows.net,actions.githubusercontent.com) used to emit one CI warning annotation each on every run. They are benign: the action skips them, and their subdomains are still observed via PTR / DNS capture (and opened just-in-time in block mode). They are now collected into a single plain-text log line instead. - Docs/labels: the eBPF mechanism is a tracepoint on
sys_enter_connect(not a “kprobe on tcp_connect”); the sampler is/proc-only (the “ss” fallback was removed). Corrected the runtime log line, summary label, and README. - Outbound connections showed as bare IPs when systemd-resolved owns
getaddrinfo. Thensswitchreroute didn’t always stick, so package-repo lookups bypassed the capture forwarder and were never named. The forwarder now targets the real upstream (systemd-resolved’s actual servers, not the127.0.0.53stub), and when the bypass is detected but the nsswitch reroute fails, Legion redirects systemd-resolved itself at the forwarder via aresolved.conf.ddrop-in โ verify-or-revert and restored on teardown, so it never breaks the job’s DNS. - IPv4-mapped IPv6 destinations rendered as long expanded addresses
(
0000:0000:0000:0000:0000:ffff:HHHH:HHHH) in the summary. The/procsampler emitted the expanded form whilenormalizeIponly collapsed the compressed::ffff:form. Both now collapse to dotted IPv4 (and the v4/v6 tables dedupe). (Shipped in v1.0.35.) - Removed dead
action/baseline.js; pinnedrelease.ymlcheckout to v6.
Reliability
- Tests for the paths that kept breaking: the firewall rule builders
(
egressBlockRules/egressUnblockRulesโ order, DNS-allow, DROP-last, OUTPUT-jump-removed-first), the checksum parser, the curated presets, and a full-stack PR gate that runs block + DNS-capture + eBPF and asserts the job finalizes (catches any teardown-hang regression), plus the package-repo classifier (host-suffix + CIDR matching). Action test count 19 โ 37.
Changed
- Removed em-dashes from the job-summary output (headers, the unresolved-host note, the enforce hint, and empty-cell placeholders) for plainer rendering.
- Name more destinations: route glibc
getaddrinfo(curl/apt/cargo/git) through the DNS-capture forwarder via annsswitch.confreroute, so hosts resolved by systemd-resolved (which ignoresresolv.conf) are now captured and named โ not justresolv.conf/c-ares callers. Health-checked and restored on teardown. (A connection to a hard-coded IP with no PTR still shows the IP โ there is no name to resolve.) - More accurate “unresolved destination” note in the summary (a name may have been resolved outside the capture path, vs. a genuine raw-IP connection).
- We dogfood our own action. Every real-work job in this repo (CI, release,
eBPF agent, FIM self-test) now runs Legion Runner as its first step (
@v1, audit) โ our CI is hardened by the product it ships. - Docs-only PRs skip the build/test matrix (and the release it gates) via
paths-ignore; adocs-passthroughworkflow reports the same check names green so required checks stay satisfied and README edits stay mergeable without burning CI minutes. - Our CI now captures names. The dogfooded harden steps run with
dns-capture: true(stillauditโ monitor, don’t firewall), so job summaries show real destination and package-repository names instead of bare IPs, and the capture path is exercised on every run. Safe now that@v1(>= 1.0.35) carries the teardown + systemd-resolved fixes.