Skip to content

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 a golangci-lint linter alongside the standard vet and lint passes.
  • Secret scanning with gitleaks, so a credential committed by accident fails the build.
  • Dependency vulnerability scanning with govulncheck against 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.