GitHub Action for Detection-as-Code (DaC) powered by RSigma: lint, validate, fields-drift, backtest, and ATT&CK coverage in one CI gate.
This composite action turns a Sigma rule repository into a pull-request gate. It installs a verified rsigma release, runs the detection-as-code checks, annotates findings on the diff, keeps a single sticky summary comment up to date, and uploads the backtest and ATT&CK Navigator reports as artifacts.
name: Detection-as-Code
on:
pull_request:
permissions:
contents: read
pull-requests: write # the sticky summary comment
jobs:
rsigma:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # required for the merge-base fields-drift diff
- uses: timescale/rsigma-action@v1
with:
rules: rules/The minimal run lints, validates, and diffs rule fields. Set corpus to add backtesting and coverage: "true" to add ATT&CK coverage.
- uses: timescale/rsigma-action@v1
with:
version: v0.17.0 # pin a release; the default `latest` resolves once then caches
rules: rules/
pipelines: |
ecs_windows
lint-fail-level: warning
sources: |
sources/threat-intel.yml
corpus: tests/corpus
expectations: tests/expectations.yml
unexpected: fail
coverage: "true"
coverage-atomics: "true" # `true` uses the upstream Atomic Red Team index
coverage-baseline: "true" # `true` uses the SigmaHQ baseline heatmap
coverage-targets: tests/targets.txt
fail-on-gaps: "true"The action wraps shipped rsigma subcommands. Each step is independent and gated by an input toggle; a non-zero rsigma exit is recorded and surfaced by a final gate step, so the comment and artifacts always publish before the run fails.
- Lint:
rsigma rule lint <rules> --fail-level <level> --output-format json. Findings become PR annotations generated from the stable JSON envelope (not text-scraping problem matchers), so each annotation carries the lint rule name as its title. - Validate:
rsigma rule validate <rules> --resolve-sources [--source ...] [-p ...]. Proves rules parse, compile, and that dynamic source references resolve. - Fields drift:
rule fieldshas no native diff, so the action runs it on HEAD and on the PR merge-base (in a temporary git worktree) and diffs the field-name sets. Non-blocking; it surfaces coverage drift in the comment. Runs on pull requests only. - Backtest:
rsigma rule backtest --rules <rules> --corpus <corpus> [--expectations ...] [--unexpected ...] [-p ...] --junit backtest.xml --report backtest.json. Runs only whencorpusis set. The JUnit XML and JSON report upload as artifacts. - Coverage:
rsigma rule coverage --rules <rules> --navigator layer.json [--targets ...] [--baseline ...] [--atomics ...] [--fail-on-gaps]. The Navigator layer (format 4.5) uploads as an artifact, ready to load into the ATT&CK Navigator.
Exit codes follow the rsigma house scheme: 1 is findings (fails the gate), 2 is a rule error, and 3 is a config error. A 2/3 gets a distinct fail-fast annotation, so a broken expectations or rule file is never reported as a rule regression.
| Input | Default | Description |
|---|---|---|
version |
latest |
rsigma release to install (e.g. v0.17.0). latest resolves the newest release once, then pins it for the cache key. Pinning a tag is recommended. |
rules |
rules/ |
Rule file or directory, passed to every step. |
pipelines |
unset | Pipeline names or paths (one per line), forwarded as repeated -p. |
working-directory |
. |
Repo-relative root for all paths. |
lint |
true |
Run the lint step. |
validate |
true |
Run the validate step. |
fields-diff |
true |
Run the merge-base fields-drift diff (pull requests only). |
lint-fail-level |
warning |
Forwarded to rule lint --fail-level (error/warning/info). The rsigma default is error; this action is deliberately stricter. |
sources |
unset | Dynamic-source files or directories (one per line) for --resolve-sources --source. |
corpus |
unset | Backtest corpus file or directory (one per line). The backtest step runs only when set. |
expectations |
unset | Expectations YAML, forwarded to rule backtest --expectations. |
unexpected |
warn |
Forwarded to rule backtest --unexpected (fail/warn/ignore). |
coverage |
false |
Run the ATT&CK coverage step. |
coverage-targets |
unset | Target technique list, forwarded to rule coverage --targets. |
coverage-baseline |
unset | Baseline Navigator layer. true selects the upstream SigmaHQ default; otherwise a path or URL. |
coverage-atomics |
unset | Atomic Red Team index. true selects the upstream default; otherwise a path, URL, or atomics/ directory. |
fail-on-gaps |
false |
Forwarded to rule coverage --fail-on-gaps. |
annotations |
true |
Emit workflow-command annotations on the PR diff. |
pr-comment |
true |
Maintain the sticky summary comment on pull requests. |
github-token |
${{ github.token }} |
Token used to download the release, verify its attestation, and upsert the comment. |
| Output | Description |
|---|---|
rsigma-version |
The resolved rsigma version that ran. |
lint-errors |
Lint error count from the JSON envelope summary. |
lint-warnings |
Lint warning count from the JSON envelope summary. |
backtest-passed |
true when the backtest ran and all expectations passed, false otherwise. |
junit-path |
Path to the backtest JUnit XML report, if produced. |
report-path |
Path to the backtest JSON report, if produced. |
navigator-path |
Path to the coverage ATT&CK Navigator layer, if produced. |
fields-added |
Number of rule fields added relative to the merge-base. |
fields-removed |
Number of rule fields removed relative to the merge-base. |
The action needs contents: read to check out the rules and download the public rsigma release. The sticky PR comment additionally needs pull-requests: write. If you do not want the comment, set pr-comment: "false" and contents: read is enough.
permissions:
contents: read
pull-requests: writeThe install step downloads the rsigma-<target> archive from the timescale/rsigma releases and verifies it twice before putting it on PATH: the SHA256SUMS manifest entry must match, and the SLSA build-provenance attestation must verify against timescale/rsigma with gh attestation verify. There is no insecure fallback. The unpacked binary is cached per version and target, so steady-state runs skip the download and verification.
- The fields-drift diff needs git history for the merge-base, so check out with
fetch-depth: 0. - The action supports the six rsigma release targets: Linux, macOS, and Windows on amd64 and arm64.
- Minimum rsigma version:
v0.17.0(the release whererule backtestandrule coverageshipped). Older versions work for lint, validate, and fields-drift only.
For hardened consumers, pin the action by full commit SHA:
- uses: timescale/rsigma-action@<full-sha> # v1.0.0The floating v1 major tag is moved to each v1.x.y release. The version input that selects the rsigma engine is independent of the action version, and pinning a concrete version (rather than latest) keeps runs reproducible.
bash tests/run-golden.shruns the golden tests for the annotation and comment renderers (pure functions over JSON fixtures, no rsigma binary needed). Regenerate withbash tests/update-golden.shafter an intentional formatting change.shellcheck -x scripts/*.sh tests/*.shmust pass..github/workflows/e2e.ymlruns the action againstfixtures/on the ubuntu/macos/windows matrix.zizmor --pedanticmust pass on the workflows andaction.yml.
MIT. See LICENSE.