From ab1ab9b374db3ff9c9878446eae5a29993acb30c Mon Sep 17 00:00:00 2001 From: Joey Mussalli Date: Mon, 24 Aug 2026 15:55:45 -0400 Subject: [PATCH] feat(scan): add REQUIRE_SIGNED to forbid unsigned rules trustabl verifies rules against an embedded trust keyring by default and exits 2 rather than running unverified rules. It also keeps an unsigned git path, and `--require-signed` exists specifically to forbid the fallback to it in CI. This plugin never offered that flag, while exposing two inputs that reach the unsigned path: RULES_REPO, exported unconditionally as TRUSTABL_RULES_REPO, and RULES_REF, which pins a git ref. Neither is wrong to use -- pinning a ref is a reasonable way to get reproducible scans -- but a security gate should be able to state the requirement rather than inherit whatever the default resolves to, and until now there was no way to say "never accept unsigned rules". docs/EVALUATION.md already tells readers that rules are "fetched at scan time from a signed channel". That is true of the tool's default, but nothing in this plugin held it to that, so the guarantee the documentation offers was not one a user could enforce. - REQUIRE_SIGNED=true adds --require-signed. Default false, so behaviour is unchanged for everyone who does not opt in. - Matched case-insensitively. Silently ignoring REQUIRE_SIGNED=TRUE would be a fail-open on the single input whose purpose is to tighten the gate. STRICT has the same case-sensitivity and is deliberately left alone: changing it is a behaviour change for existing users and belongs in its own PR. - Documented in a new "Rule integrity" section of the README, alongside what RULES_REPO and RULES_REF each mean for integrity, because those two are the reason the flag is worth having. Verified: the flag reaches BASE_ARGS for true/TRUE/True and is absent for unset/false/yes; STRICT's existing true/false/TRUE behaviour is byte-identical before and after. bash -n passes. No scan was run -- this changes which arguments are constructed, not what the scanner does with them. --- README.md | 20 ++++++++++++++++++++ scan/trustabl-scan.sh | 11 ++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d8c5d00..f2db708 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,23 @@ marketplaces. Distribution is **copy-paste**: vendor the `scan/` directory plus the relevant wrapper into your repo. A native CodePipeline action provider is an AWS Partner integration; an AWS Marketplace listing is a separate product (container/SaaS) motion. + +## Rule integrity + +`trustabl` verifies rules against an embedded trust keyring by default and +refuses to run unverified rules, exiting 2 on a verification failure. It also +has an unsigned git path, and this plugin exposes two inputs that reach it: + +| Var | Default | Effect on integrity | +|---|---|---| +| `RULES_REPO` | _(default)_ | Exported as `TRUSTABL_RULES_REPO` — points the scan at a different rules source. | +| `RULES_REF` | _(default)_ | Pins a git ref rather than taking the current signed bundle. | +| `REQUIRE_SIGNED` | `false` | `true` passes `--require-signed`, which forbids the unsigned fallback. | + +Neither of the first two is wrong to use — pinning a ref is a reasonable way to +get reproducible scans. What matters is that a gate should be explicit about it. +Set `REQUIRE_SIGNED=true` and the scan fails rather than quietly falling back to +unsigned rules; leave it unset and the behaviour is exactly as before. + +A scanner is only as trustworthy as the rules it runs, so it is worth deciding +this deliberately rather than inheriting a default. diff --git a/scan/trustabl-scan.sh b/scan/trustabl-scan.sh index caa6314..bd46fae 100644 --- a/scan/trustabl-scan.sh +++ b/scan/trustabl-scan.sh @@ -11,7 +11,7 @@ # GitLab-only bits (gl-sast report, CI_* env) are swapped for AWS equivalents. # # Inputs are environment variables (all optional; sensible defaults): -# TARGET VERSION DETECTORS STRICT RULES_REF RULES_REPO +# TARGET VERSION DETECTORS STRICT RULES_REF RULES_REPO REQUIRE_SIGNED # SARIF_FILE JSON_FILE RISK_SCORE_THRESHOLD SEVERITY_THRESHOLD # BRANCH GITHUB_TOKEN DEBUG @@ -132,6 +132,15 @@ BASE_ARGS=(scan "$TARGET") [ -n "$DETECTORS" ] && BASE_ARGS+=(--detectors "$DETECTORS") [ "$STRICT" = "true" ] && BASE_ARGS+=(--strict) [ -n "$RULES_REF" ] && BASE_ARGS+=(--rules-ref "$RULES_REF") +# trustabl verifies rules against an embedded keyring by default and exits 2 on +# a verification failure, but it also has an unsigned git path. This plugin +# hands users two knobs onto that path -- RULES_REPO (exported above as +# TRUSTABL_RULES_REPO) and RULES_REF -- and offered no way to say "never accept +# unsigned rules", which is exactly the guarantee a CI gate wants. +# Matched case-insensitively on purpose: silently ignoring REQUIRE_SIGNED=TRUE +# would be a fail-open on the one input whose job is to tighten the gate. +REQ_SIGNED="${REQUIRE_SIGNED:-false}" +[ "${REQ_SIGNED,,}" = "true" ] && BASE_ARGS+=(--require-signed) # Run 1: SARIF (file emit). trustabl "${BASE_ARGS[@]}" --format sarif > "$SARIF_FILE"