Skip to content

ci: pin gitleaks, allowlist pre-submodule test history, and add an aggregate gate - #474

Merged
BitHighlander merged 2 commits into
release/7.14.2from
fix/secret-scan-historical-tests-and-pin
Aug 17, 2026
Merged

ci: pin gitleaks, allowlist pre-submodule test history, and add an aggregate gate#474
BitHighlander merged 2 commits into
release/7.14.2from
fix/secret-scan-historical-tests-and-pin

Conversation

@BitHighlander

@BitHighlander BitHighlander commented Aug 17, 2026

Copy link
Copy Markdown
Owner

CI hardening in two independently revertable commits. Together they make the pipeline deterministic and structurally incapable of hiding a required-job failure.


Commit 1 — 52a78b525 scanner determinism

secret-scan went red across the entire repository with no change to it, taking the whole build and test graph down.

Run 32075116969 on #472: failed in 9s, everything downstream SKIPPED. Reproduced locally with CI's exact command — 12 findings, none from this release:

file findings era
tests/test_ecies.py 6 2014, inherited from Trezor
tests/test_msg_eos_signtx.py 4 2018
tests/test_msg_ethereum_erc20_signtx_exchange.py 2 2018

git rev-list 1af2ffe7de..HEAD intersected with the findings: 0.

The flagged lines are published BIP32 test vectors — WIF keys printed beside their own address and pubkey as documentation:

# address: 1CK7SJdcb8z9HuvVft3D91HLpLC6KSsGb
# pubkey:  0338d78612e990f2eea0c426b5e48a8db70b9d7ed66282b3b26511e0b1c75515a6
# privkey: L5X3rf5hJfRt9ZjQzFopvSBGkpnSotn4jKGLL6ECJxcuT2JgGh65

Root cause: GITLEAKS_VERSION resolved from releases/latest. A newer generic-api-key rule started flagging historical fixtures. Nothing here changed.

Two fixes. Allowlist ^tests/ — that path does not exist in the working tree (the python suite lived at the repo root before becoming deps/python-keepkey), but CI scans with fetch-depth: 0. Identical situation and remedy to the existing ^deps/ exemption, and scoped by path rather than regex because the findings are real key material by format. And pin gitleaks to 8.30.0.

The cost is written into the config rather than left implicit: a genuine secret under tests/ would not be caught, which is acceptable only while the directory doesn't exist — if it returns, the exemption goes in the same commit.

Negative-controlled both directions:

planted result
fresh EC private key under lib/firmware/ detected (leaks found: 1)
same key under tests/ ignored (path-scoped as intended)

gitleaks detect --source . --verbose --redactno leaks found, exit 0.


Commit 2 — 4e8d74667 the aggregate gate (closes #471)

A skipped job is not a red check. When a Stage-1 gate fails, GitHub marks the downstream graph skipped, the summary shows green ticks on whatever finished, and the absence of signal is indistinguishable from success.

Three occurrences on this release line: gitleaks on develop, lint-format timing out inside its apt.llvm.org install (#471), and gitleaks again (commit 1 above).

ci-gate needs every required job, runs if: always(), and fails unless each reports exactly success. failure, cancelled and skipped are all not-success — for a required job, they are. publish-emulator is excluded as workflow_dispatch-only.

Verified against the shapes that actually occurred:

scenario result
all success PASS
one skipped FAIL
one cancelled FAIL
one failure FAIL
python-integration-tests cancelled — today's branch FAIL
lint-format cancelled + 2 skipped — the #471 run FAIL

Point branch protection at CI gate rather than the individual jobs. It is the only check whose green means the entire graph ran and passed.


Consequence, stated up front

release/7.14.2 will report RED after this merges, because python-integration-tests is killed at its 30-minute timeout on every run (#466). That has been the branch's true state all along — this PR only stops it being hidden. #466 becomes the unambiguous next blocker to a genuinely green graph.

This PR's own ci-gate will therefore also be red, for exactly that reason. That is the gate working, not failing.

secret-scan went red across the whole repository with no change to it. The
install step resolved GITLEAKS_VERSION from releases/latest, and a newer
generic-api-key rule began flagging published BIP32 test vectors in history:
12 findings across tests/test_ecies.py (2014, inherited from Trezor),
tests/test_msg_eos_signtx.py and tests/test_msg_ethereum_erc20_signtx_exchange.py
(2018). Zero of them are in any commit in 1af2ffe..HEAD.

The flagged lines are WIF private keys printed next to their own address and
public key as documentation. They are upstream test fixtures, published for a
decade, and secure nothing.

Two changes, because the failure had two causes.

Allowlist ^tests/. That path does not exist in the working tree -- the python
suite lived at the repository root before it became deps/python-keepkey -- but
CI scans with fetch-depth 0, so gitleaks still reads those commits. Same
situation and same remedy as the existing ^deps/ exemption. Scoped by path, not
by regex: the findings are real key material by format, so a pattern exemption
would suppress genuine secrets elsewhere in the tree.

The cost is recorded in the config rather than left implicit: a real secret
committed under tests/ would not be caught, which is acceptable only while the
directory does not exist. Negative-controlled both ways -- a freshly generated
EC private key planted under lib/firmware/ is still detected, the same key
under tests/ is ignored.

Pin gitleaks to 8.30.0. Tracking releases/latest means an upstream ruleset
change can turn this gate red with no change here, and because every build job
declares `needs: [.., secret-scan]`, the entire build and test graph is SKIPPED
rather than failed -- silence that looks identical to success. That is the
third instance of this class on this release (see #471 for lint-format, and the
earlier gitleaks breakage). Partially addresses #424.

Verified: gitleaks detect --source . --verbose --redact reports "no leaks
found" and exits 0.

Refs #424 #471
… jobs

Every build and test job declares
`needs: [lint-format, static-analysis, check-submodules, secret-scan]`. When one
of those gate jobs fails, GitHub marks the whole downstream graph SKIPPED rather
than failed -- and a skipped job is not a red check. The run summary shows green
ticks on whatever finished, which reads as healthy unless someone opens the job
list and notices that the ARM build, the unit tests and both python suites
produced nothing.

That has happened three times on this release line: gitleaks failing on develop,
lint-format timing out inside its apt.llvm.org install (#471), and gitleaks again
after an unpinned upstream bump (the commit before this one). Each time the
absence of signal was indistinguishable from success.

ci-gate needs every required job, runs `if: always()` so it executes even when
they skip, and fails unless each reports exactly `success`. failure, cancelled
and skipped are all not-success, because for a required job they are.
publish-emulator is deliberately excluded: it is workflow_dispatch-only and is
legitimately skipped on every push and pull_request.

Point branch protection at this job rather than the individual ones. It is the
only check whose green means "the entire graph ran and passed".

Verified against the two shapes that actually occurred, plus the obvious ones:

  all success                       -> PASS
  one skipped                       -> FAIL
  one cancelled                     -> FAIL
  one failure                       -> FAIL
  python-integration-tests cancelled-> FAIL (today's release/7.14.2)
  lint-format cancelled + 2 skipped -> FAIL (the #471 run)

Consequence, stated up front: release/7.14.2 will now report RED until #466 is
fixed, because python-integration-tests is killed at its 30-minute timeout on
every run. That is the honest state of the branch and has been all along; this
job only stops it being hidden.

Closes #471
@BitHighlander BitHighlander changed the title ci(secret-scan): allowlist pre-submodule test history, and pin gitleaks ci: pin gitleaks, allowlist pre-submodule test history, and add an aggregate gate Aug 17, 2026
@BitHighlander
BitHighlander merged commit fc19dda into release/7.14.2 Aug 17, 2026
8 of 9 checks passed
@BitHighlander
BitHighlander deleted the fix/secret-scan-historical-tests-and-pin branch August 17, 2026 22:57
BitHighlander added a commit that referenced this pull request Aug 17, 2026
…riant

The integration suite is red, the quickest route to green is to stop showing a
screen, and the commit that does it looks like a test fix. Writing the rule down
is the cheapest defence against that, because the pressure arrives disguised as
housekeeping and the result would look like progress.

Tests adapt to newly required disclosure. Firmware disclosure is never removed to
satisfy a stale test. If a test fails because the device now shows more, the test
is wrong. The extra THORChain screens disclose affiliate-fee bytes that were
previously signed invisibly.

Also replaces the known-red section, which was wrong in two ways. It said the
integration stall was pre-existing at 4406197 -- a controlled run (same test
files, only firmware varying) showed 1af2ffe passes and head hangs, so this
release introduced it. And it could not enumerate the failures at all, because
the job was killed before producing JUnit XML.

With #477 the suite completes in 134s and the list is finite: 22 failed, 362
passed, 47 skipped, every failure a stale pinned expectation and none a firmware
defect. Recorded as a table with an owner per row, since that is now the concrete
work list for the repin.

Adds the two measurement traps that cost real time here: do not run the suite
sequentially against one emulator, and treat the PINNED submodule commit as the
test source of truth rather than the working tree -- reading master's copy of
test__sign_transformERC20 produced a confident and wrong "this breaks nothing".

Refs #466 #474 #477
BitHighlander added a commit that referenced this pull request Aug 20, 2026
The emulator test script detected the firmware version with:

    FW_VERSION=$(sed -n '/^project/,/)/p' CMakeLists.txt \
                 | grep -oP '\d+\.\d+\.\d+' || echo "7.14.0")

`grep -oP` is a GNU extension. This container's grep is BusyBox, which has no
-P, so that command ALWAYS failed and the fallback silently supplied 7.14.0 --
on the 7.14.2 release branch. CI has been reporting
"Detected FW_VERSION=7.14.0 from CMakeLists.txt" while CMakeLists says 7.14.2.

Everything downstream keys off that number. SECTIONS entries are selected by
ver_ge(fw_version, min_fw), so at 7.14.0 every test gated to 7.14.1 or later was
excluded from BOTH the screenshot filter and validate_junit. That is the root
cause of the gap #487 set out to fix: the suites this release changed captured
no screens because the report system did not believe those tests existed yet.

It also means the per-test screenshot audit added in #487 passed vacuously --
with no 7.14.2 sections active, nothing declared screens, so nothing could be
missing. The audit was correct; it was being asked about the wrong release.

Two changes:
  - read VERSION with sed alone, portable to BusyBox
  - FAIL if it cannot be read, rather than defaulting. A wrong version here is
    invisible and silently narrows what CI checks -- exactly the failure mode
    ci-gate (#474) and the screenshot audit were added to prevent.

Same class as those two: the check ran, went green, and was measuring something
other than what its name implied.
BitHighlander added a commit that referenced this pull request Aug 21, 2026
…at all

The alpha<-develop merge left .gitleaks.toml with BOTH allowlist forms — alpha's
three `[[allowlists]]` and develop's one `[allowlist]`. gitleaks refuses that
outright:

    FTL Failed to load config
        error="[allowlist] is deprecated, it cannot be used alongside [[allowlists]]"

secret-scan is a stage-1 gate, so its failure SKIPPED the entire build graph:
build-emulator, build-arm-firmware, unit-tests, python-integration-tests,
python-dylib-tests and generate-test-report all reported "skipped", and the run
looked like one red job rather than a release with no evidence behind it. This
is the silent-skip class #474 added the aggregate `CI gate` for, and that gate
did its job — it failed and named all seven.

Fix is presentational only: develop's `[allowlist]` becomes a fourth
`[[allowlists]]` entry. No path, regex or rule changed, and neither side's
exemptions are dropped — alpha's three are narrow (path AND regex, for the AES
test vectors, ed25519 type names and the rc21 doc pin) and develop's is broad
path scoping (U2F attestation artifacts, deps/, docs/, tests/) with its
rationale intact.

Verified locally with gitleaks 8.30.0, CI's exact command:

    gitleaks detect --source . --verbose --redact
    -> 7586 commits scanned, no leaks found, exit 0

And the negative control develop's own comment claims, re-run because a merge
is exactly when an exemption silently widens — a fresh prime256v1 key planted in
two places, JSON report:

    private-key -> lib/firmware/NEGCTRL_planted.pem     CAUGHT
    (same key under deps/)                              exempt, as documented

Neither gate in tools/ could have caught this. merge_symbol_gate.py reads C
symbols; merge_direction_gate.py only flags a file taken verbatim from one side,
and this file was genuinely merged — the union was syntactically valid TOML and
semantically invalid to the tool that consumes it. Config files merged from both
sides need the consuming tool run against them, not a diff read.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant