Skip to content

Commit b5303ad

Browse files
committed
Gate the release on a security job, and sign what it publishes
Renders the tui-kit templates that grew the security gate. CI now runs govulncheck and gitleaks — over the working tree and over the whole history — and the release job waits on them, so a v* tag that fails either check produces no release. A release now carries a CycloneDX SBOM per archive, a keyless cosign signature over checksums.txt as a Sigstore bundle, and build provenance for every archive, package and the checksum file. tui-kit's README says how to check them. gosec joins the lint bar; the one finding it raised is a fixture read in a test, annotated on the line. Adds the Scorecard workflow, which publishes this repository's supply-chain score weekly, and the badge that shows it.
1 parent 89f3380 commit b5303ad

7 files changed

Lines changed: 231 additions & 6 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Generated from tui-kit/templates/ci.yml. It runs the same checks
2-
# `make check` runs locally, cross-compiles for both release targets, and on a
3-
# `v*` tag lets GoReleaser publish the binaries.
2+
# `make check` runs locally, cross-compiles for both release targets, scans
3+
# for known vulnerabilities and committed secrets, and on a `v*` tag lets
4+
# GoReleaser publish the binaries — signed, with an SBOM and build provenance
5+
# beside them. A tag that fails any of those jobs produces no release.
46
name: CI
57

68
on:
@@ -146,13 +148,73 @@ jobs:
146148
python3 "$kit/tools/check-nfpm.py" --manifest tool.json \
147149
--config .goreleaser.yaml
148150
151+
security:
152+
name: vulnerabilities and secrets
153+
runs-on: ubuntu-latest
154+
steps:
155+
- uses: actions/checkout@v7
156+
with:
157+
# gitleaks reads the whole history: a secret that was committed
158+
# once stays reachable long after the commit that removed it.
159+
fetch-depth: 0
160+
161+
- uses: actions/setup-go@v7
162+
with:
163+
# go.mod is the single place the family's Go version lives, so a
164+
# bump there moves CI with it.
165+
go-version-file: go.mod
166+
check-latest: true
167+
168+
# Reports only the vulnerabilities the build actually reaches, so a CVE
169+
# in an unused corner of a dependency does not stop a release while a
170+
# reachable one does.
171+
- name: govulncheck
172+
run: |
173+
go install golang.org/x/vuln/cmd/govulncheck@latest
174+
govulncheck ./...
175+
176+
# The official gitleaks action requires a paid license for repositories
177+
# owned by an organization, and this one is. The released binary is the
178+
# same scanner without that condition; it is pinned to a version and its
179+
# checksum is verified against the release's own checksums file before
180+
# it is allowed to run.
181+
# It is unpacked outside the checkout, into the runner's temp
182+
# directory: the working-tree scan below would otherwise read the
183+
# scanner's own binary as if it were part of the repository.
184+
- name: install gitleaks
185+
env:
186+
GITLEAKS_VERSION: "8.30.1"
187+
run: |
188+
base="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}"
189+
asset="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
190+
cd "$RUNNER_TEMP"
191+
curl -fsSL --retry 3 -o "$asset" "$base/$asset"
192+
curl -fsSL --retry 3 -o checksums.txt \
193+
"$base/gitleaks_${GITLEAKS_VERSION}_checksums.txt"
194+
grep " ${asset}$" checksums.txt | sha256sum -c -
195+
tar xzf "$asset" gitleaks
196+
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
197+
198+
- name: gitleaks (history)
199+
run: gitleaks git --no-banner --redact --config .gitleaks.toml
200+
201+
# The working tree as well as the history: a file that is present but
202+
# not committed yet is the one worth catching early.
203+
- name: gitleaks (working tree)
204+
run: gitleaks dir --no-banner --redact --config .gitleaks.toml
205+
149206
release:
150207
name: release
151208
if: startsWith(github.ref, 'refs/tags/v')
152-
needs: [check, cross-build, manifest]
209+
needs: [check, cross-build, manifest, security]
153210
runs-on: ubuntu-latest
154211
permissions:
212+
# contents to attach the artifacts, id-token for the keyless signing
213+
# identity, attestations to record the provenance GitHub then serves to
214+
# `gh attestation verify`.
155215
contents: write
216+
id-token: write
217+
attestations: write
156218
steps:
157219
- uses: actions/checkout@v7
158220
with:
@@ -165,9 +227,27 @@ jobs:
165227
go-version-file: go.mod
166228
check-latest: true
167229

230+
# Both are called by GoReleaser itself: syft for the `sboms` section,
231+
# cosign for the `signs` one. They have to be on PATH before it runs.
232+
- uses: anchore/sbom-action/download-syft@v0
233+
- uses: sigstore/cosign-installer@v4
234+
168235
- uses: goreleaser/goreleaser-action@v7
169236
with:
170237
version: "~> v2"
171238
args: release --clean
172239
env:
173240
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
241+
242+
# Provenance: a signed statement of which workflow, at which commit,
243+
# produced each file. `gh attestation verify <file> -R tui-tools/tui-firewall`
244+
# checks a download against it without any key to distribute.
245+
- name: attest build provenance
246+
uses: actions/attest-build-provenance@v4
247+
with:
248+
subject-path: |
249+
dist/*.tar.gz
250+
dist/*.deb
251+
dist/*.rpm
252+
dist/*.pkg.tar.zst
253+
dist/checksums.txt

‎.github/workflows/scorecard.yml‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Generated from tui-kit/templates/scorecard.yml.
2+
#
3+
# Scorecard grades the repository's own supply-chain posture — branch
4+
# protection, pinned actions, signed releases, whether a security policy
5+
# exists — from the outside, the same way a person deciding whether to trust
6+
# the tool would. Publishing the result is the point: the score is public and
7+
# the badge links to the run that produced it, so the claim is checkable
8+
# rather than asserted.
9+
name: Scorecard
10+
11+
on:
12+
branch_protection_rule:
13+
schedule:
14+
# Weekly. The score moves with GitHub settings and with dependencies, not
15+
# only with commits, so it has to run on a clock as well as on a push.
16+
- cron: "27 5 * * 1"
17+
push:
18+
branches: [main]
19+
20+
permissions: read-all
21+
22+
jobs:
23+
analysis:
24+
name: analyze
25+
runs-on: ubuntu-latest
26+
permissions:
27+
# Upload the findings to the repository's code scanning view.
28+
security-events: write
29+
# Publish the result to the public OpenSSF API, which is what lets the
30+
# badge and deps.dev read it. Keyless, via the workflow's OIDC token.
31+
id-token: write
32+
contents: read
33+
actions: read
34+
35+
steps:
36+
- uses: actions/checkout@v7
37+
with:
38+
persist-credentials: false
39+
40+
- name: run analysis
41+
uses: ossf/scorecard-action@v2
42+
with:
43+
results_file: results.sarif
44+
results_format: sarif
45+
publish_results: true
46+
47+
# Kept for a while as a plain file too: code scanning only retains the
48+
# latest, and a regression is easier to read against the previous run.
49+
- uses: actions/upload-artifact@v7
50+
with:
51+
name: scorecard-results
52+
path: results.sarif
53+
retention-days: 5
54+
55+
- uses: github/codeql-action/upload-sarif@v4
56+
with:
57+
sarif_file: results.sarif

‎.gitleaks.toml‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Secret scanning configuration for the tui-tools family.
2+
#
3+
# Copy to .gitleaks.toml at the root of the tool's repository, unchanged. CI
4+
# runs it twice: over the whole git history, because a secret that was
5+
# committed once stays reachable long after the commit that removed it, and
6+
# over the working tree, so a file that is not committed yet is caught first.
7+
[extend]
8+
# The upstream rule set is the baseline; this file only adds to it.
9+
useDefault = true
10+
11+
# Not a secret, but it leaks whose machine built the artifact and how that
12+
# machine is laid out. It arrives the same way every time: a fixture, a log or
13+
# a terminal capture taken on a real workstation and committed verbatim.
14+
# Anything captured that way has to be scrubbed to a neutral path before it
15+
# lands in a public repository — /home/user, /root, or the placeholder the
16+
# surrounding document already uses.
17+
#
18+
# The username is the capture group rather than the whole path, because the
19+
# upstream allowlist drops any finding containing a common word and "home"
20+
# is one of them. Matching on the name also makes the allowlist below read
21+
# for what it is: the names that belong to a machine image, not a person.
22+
[[rules]]
23+
id = "developer-home-path"
24+
description = "Absolute home directory path from a developer machine"
25+
regex = '''(?:/home/|/Users/)([A-Za-z][A-Za-z0-9_-]*)/'''
26+
secretGroup = 1
27+
tags = ["privacy", "path"]
28+
29+
[rules.allowlist]
30+
regexTarget = "secret"
31+
regexes = [
32+
'''^(user|username|you|me|example|root|runner|ubuntu|debian|fedora|arch|vagrant|linuxbrew|Shared)$''',
33+
]
34+
35+
# Build output. CI scans a fresh checkout, where none of this exists, but a
36+
# developer running the same command locally would otherwise be shown their
37+
# own compiled caches instead of a real finding.
38+
[allowlist]
39+
description = "Generated build output, never committed"
40+
paths = [
41+
'''(^|/)(bin|dist|__pycache__)/''',
42+
]

‎.golangci.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ linters:
1111
enable:
1212
- errcheck
1313
- govet
14+
# The tools run privileged commands on someone else's machine, so the
15+
# security linter is part of the bar rather than an occasional audit.
16+
# The one finding the family expects is G204 inside internal/<backend>/,
17+
# where an argv built from user input is the whole point; that line
18+
# carries a //nolint saying so.
19+
- gosec
1420
- ineffassign
1521
- staticcheck
1622
- unused

‎.goreleaser.yaml‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
# git tag v0.1.0 && git push origin v0.1.0
66
#
77
# CI runs `goreleaser release` on a v* tag and attaches static linux/amd64 and
8-
# linux/arm64 archives to the GitHub release. Generated from the template in
9-
# tui-kit/templates/.goreleaser.yaml.
8+
# linux/arm64 archives to the GitHub release, each with its SBOM, plus a
9+
# checksums.txt carrying a keyless cosign signature. Generated from the
10+
# template in tui-kit/templates/.goreleaser.yaml.
1011
version: 2
1112

1213
project_name: tui-firewall
@@ -99,6 +100,43 @@ nfpms:
99100
checksum:
100101
name_template: checksums.txt
101102

103+
# One SBOM per archive, in CycloneDX JSON, generated by syft from the archive
104+
# itself rather than from the source tree, so it describes what was actually
105+
# shipped. The release job installs syft before calling GoReleaser.
106+
sboms:
107+
- id: archive
108+
artifacts: archive
109+
documents:
110+
- "${artifact}.cyclonedx.json"
111+
cmd: syft
112+
args:
113+
- "$artifact"
114+
- --output
115+
- cyclonedx-json=$document
116+
117+
# Keyless signing: cosign gets its identity from the workflow's OIDC token,
118+
# so there is no signing key to hold, rotate or leak, and the certificate
119+
# names the workflow and the tag that produced the file.
120+
#
121+
# Only checksums.txt is signed. It already names every archive and every
122+
# package with its digest, so one signature covers the whole release: verify
123+
# the file, then `sha256sum -c` whatever you downloaded against it.
124+
#
125+
# The output is a Sigstore bundle — certificate, signature and transparency
126+
# log entry in one file — which is what `cosign verify-blob --bundle` reads.
127+
signs:
128+
- id: checksums
129+
cmd: cosign
130+
args:
131+
- sign-blob
132+
- --yes
133+
- --bundle
134+
- "${signature}"
135+
- "${artifact}"
136+
artifacts: checksum
137+
signature: "${artifact}.sigstore.json"
138+
output: true
139+
102140
snapshot:
103141
version_template: "{{ incpatch .Version }}-next"
104142

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<img src="assets/logo.png" alt="tui-firewall" width="240">
22

3+
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/tui-tools/tui-firewall/badge)](https://scorecard.dev/viewer/?uri=github.com/tui-tools/tui-firewall)
4+
35
A terminal UI for the Linux firewall — **ufw** and **firewalld** — that shows
46
the rules you actually have and **previews the exact command line of every
57
change before running it**.

‎internal/firewalld/parse_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626
// from an assumption into a test.
2727
func fixture(t *testing.T, name string) string {
2828
t.Helper()
29-
data, err := os.ReadFile(filepath.Join("testdata", name))
29+
data, err := os.ReadFile(filepath.Join("testdata", name)) //nolint:gosec // the name is a literal in the test above, and testdata is in the repository
3030
if err != nil {
3131
t.Fatalf("read fixture: %v", err)
3232
}

0 commit comments

Comments
 (0)