Development-lifecycle security¶
The controls that protect a PEART release run continuously during development, not only at release
time. The guiding invariant is that make check equals CI: the same gates a maintainer runs locally
are the gates that must pass on every push to main, so a commit that would fail CI fails on the
developer's machine first.
The gates on every commit¶
make check runs, and CI enforces:
- Static analysis for security with
gosec, wired in as agolangci-lintlinter alongside the standard vet and lint passes. - Secret scanning with
gitleaks, so a credential committed by accident fails the build. - Dependency vulnerability scanning with
govulncheckagainst the full dependency tree. - A JavaScript syntax gate that parses every script with a pure-Go ES-module parser, catching a broken script before it ships (no Node in the pipeline).
- Line-length and formatting checks, and an i18n consistency check across the locale files.
Testing spans unit tests, table-driven tests for the tricky logic, integration tests, and browser
tests. One browser test is worth naming: it loads the real page shell in a real browser and fails if
the Content-Security-Policy the server sends is actually violated in the console, not merely absent
from the page source, so the CSP that the whole front-end security rests on cannot silently regress.
The project's own code carries zero occurrences of innerHTML, unsafeHTML or unsafeStatic, a
property that is measured, not assumed.
Build and distribution¶
Binaries are plain cross-compiled Go with no CGO (ADR 0004), built with -trimpath so no build-machine
path leaks into the artifact. Database schema changes are forward-only migrations, reviewable one by
one.
Software bill of materials¶
Every release publishes a CycloneDX SBOM. It is produced by cyclonedx-gomod for the Go module graph,
then a second step folds in the front-end packages vendored under web/vendor/ (Lit and a QR-code
generator) as CycloneDX components too, so the published SBOM lists a vendored npm package the same way
it lists a Go module. The bill of materials is therefore complete across both language ecosystems, not
just the Go side.
Signed, attested releases¶
Releases are signed keyless with cosign, using the release workflow's own short-lived OpenID Connect token rather than any long-lived private key the project would have to protect or rotate. The signatures are published to the public Sigstore transparency log.
Signed at release: the checksum manifest, the SBOM, and the container image (signed by its content digest, not by a movable tag). Anyone can verify the checksum manifest against the release workflow's identity:
cosign verify-blob \
--certificate-identity-regexp 'https://github.com/mentesan/peart/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--signature checksums.txt.sig --certificate checksums.txt.pem \
checksums.txt
and the image by digest:
cosign verify \
--certificate-identity-regexp 'https://github.com/mentesan/peart/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/mentesan/peart@<digest>
The provenance of the vendored front-end is currently asserted by web/vendor/MANIFEST.md; verifying
each vendored file by hash against the upstream package in CI is a planned hardening.