build: move to Go 1.26.10 and Vite 6.4.5 for October advisories

Go 1.26.10 (20 October) distrusts the compromised eMudhra intermediate
CAs in crypto/x509 (CVE-2026-111943). Binaries built with 1.26.9 still
accept chains through them, so every verified outbound TLS connection
Pulse makes (licence, updates, webhooks, AI providers) stays open to
interception by a holder of those keys. govulncheck does not flag a
trust-store change as a called symbol, so the scheduled scan stayed
green while the gap existed.

Advance the governed toolchain floor and every pin together: go.mod,
the toolchain installer and release-script guards, the source-built
container stages (manifest-list digests for golang:1.26.10-alpine and
the Debian dev container image), the root-pair diagnostic, and their
tests.

Vite 6.4.3 bundles a launch-editor with three advisories
(GHSA-587h-7wmj-h5pm, GHSA-r825-w8mm-5pxw, GHSA-698v-4r3x-3vcv) that
failed the daily advisory watch. 6.4.4 bundles the fixed launch-editor
and 6.4.5 adds the dev-server server.fs query check. Both reach only
the local dev server, not the built product. The lockfile changes only
the vite entry, and dependencySecurity.test.ts now pins the floor.
This commit is contained in:
rcourtman
2026-10-21 15:19:02 +01:00
parent 29bf5e8a47
commit 02e53bc777
23 changed files with 65 additions and 42 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.26.9@sha256:d7722066f0b60ceccb6c0643cbed1f5f9e15506ac237146c95333504d7805d89
FROM golang:1.26.10@sha256:3745f1aa11822297b79113f5e84ce2bffeb039734571c39855809cfe9e5d7be6
# Set bash as default shell for features
SHELL ["/bin/bash", "-c"]
+3 -3
View File
@@ -5,7 +5,7 @@ This dev container provides a complete, reproducible development environment for
## What's Included
### Development Tools
- **Go 1.26.9** - Backend development
- **Go 1.26.10** - Backend development
- **Node.js 24** - Frontend development
- **gopls v0.17.0** - Go language server
- **Delve** - Go debugger
@@ -225,7 +225,7 @@ Custom overrides: Create `.env.devcontainer` (gitignored)
## Resources
- **VM Specs**: 8GB RAM, 30GB disk, 2 CPU cores
- **Base Image**: `golang:1.26.9` (Debian-based)
- **Base Image**: `golang:1.26.10` (Debian-based)
- **Caches**: ~2-3GB for Go modules and build artifacts
## Tips & Tricks
@@ -247,7 +247,7 @@ MacBook (VS Code)
dev-containers VM (Proxmox)
↓ Docker
Dev Container
├── Go 1.26.9 + tools
├── Go 1.26.10 + tools
├── Node 24 + npm
├── Your code (/workspaces/pulse)
├── Hot reload watchers
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: '1.26.9'
go-version: '1.26.10'
cache: false
- name: Collect fixed exact-pair root evidence
run: python3 scripts/diagnose-root-pair.py --output root-pair-evidence
+1 -1
View File
@@ -26,7 +26,7 @@ RUN --mount=type=cache,id=pulse-npm-cache,target=/root/.npm \
# Build stage for Go backend
# Force amd64 platform - Go cross-compiles for all targets anyway,
# and this avoids slow QEMU emulation during multi-arch builds
FROM --platform=linux/amd64 golang:1.26.9-alpine@sha256:3082400e369fa24d5fc60bca20edab3f6d604e0c5a690ec66b295eff4dd87ade AS backend-builder
FROM --platform=linux/amd64 golang:1.26.10-alpine@sha256:9ce231b28998b32e116927c61aa5c406183061d4f3f2d61b292c88e7f6bdf7ca AS backend-builder
ARG BUILD_AGENT
ARG VERSION
+1 -1
View File
@@ -12,7 +12,7 @@ COPY SECURITY.md TERMS.md /app/
RUN --mount=type=cache,id=pulse-control-plane-npm-cache,target=/root/.npm \
npm run build
FROM --platform=$BUILDPLATFORM golang:1.26.9-alpine@sha256:3082400e369fa24d5fc60bca20edab3f6d604e0c5a690ec66b295eff4dd87ade AS builder
FROM --platform=$BUILDPLATFORM golang:1.26.10-alpine@sha256:9ce231b28998b32e116927c61aa5c406183061d4f3f2d61b292c88e7f6bdf7ca AS builder
ARG VERSION=dev
ARG BUILD_TIME=unknown
ARG GIT_COMMIT=unknown
+1 -1
View File
@@ -10,7 +10,7 @@ The source pair is fixed in `scripts/diagnose-root-pair.py`: baseline
`1120cce4d631bca5525cafc5b5620251998773f7` tree measured from ephemeral
source commit `4cdf250f474ad9cde6ffb121527f021b7387603f7`; metadata retains that original
evidence identity. Tree and HTTP source hashes must match. Both API test binaries
use Go 1.26.9, readonly modules and identical CI
use Go 1.26.10, readonly modules and identical CI
embed text; compilation finishes before either warm-up. Four available CPUs
are selected and inherited by children, GOMAXPROCS is four. After two 100ms
warm-ups and ten seconds settling, ten rounds alternate AB/BA with one 1s
@@ -5176,8 +5176,8 @@ governed release toolchain and immutable Go builder digest together, not to
suppress the scanner or produce release artifacts with an older patched-over
runtime.
The hosted control-plane source builder and integration mock builder are both
on the reviewed `golang:1.26.9-alpine` digest
`sha256:3082400e369fa24d5fc60bca20edab3f6d604e0c5a690ec66b295eff4dd87ade`.
on the reviewed `golang:1.26.10-alpine` digest
`sha256:9ce231b28998b32e116927c61aa5c406183061d4f3f2d61b292c88e7f6bdf7ca`.
`TestProviderMSPGoBuilderMatchesIntegrationMock` pins their exact agreement;
it does not pull the image, prove its registry contents, or qualify a deployed
control plane. The release publisher's prebuilt control-plane target is a
@@ -5233,14 +5233,19 @@ generated row-level inventory that does prove completeness, needs a declarative
record in the preparation snapshot: a marker in the body alone could be deleted to restore
legacy acceptance, and a frozen in-flight release keeps its own renderer.
As of 2026-10-11, the governed release floor is Go `1.26.9`. It supersedes
`1.26.8`, whose standard library is reachable through twelve vulnerable Pulse
call paths reported by `govulncheck` after the 2026-10-08 advisories, including
`net/http`, `crypto/tls`, `html/template`, `os`, and `net/textproto`. The
As of 2026-10-21, the governed release floor is Go `1.26.10`. It supersedes
`1.26.9`, whose `crypto/x509` still verifies chains through the compromised
eMudhra intermediate CAs that Go 1.26.10 distrusts (CVE-2026-111943, Go issue
82345). Pulse's certificate-verifying outbound TLS clients, such as licence,
update, webhook and AI-provider connections, verify chains with that package, so
an older compiler leaves an interception path open even though `govulncheck`
need not report a called symbol for a trust-store change. The 2026-10-11 floor
of `1.26.9` remains the minimum for the 2026-10-08 `net/http`, `crypto/tls`,
`html/template`, `os`, and `net/textproto` advisories, and the
`golang.org/x/net` floor is `v0.60.0` for the same advisories. Source-built
container stages pin the Docker Official Images manifest list for
`golang:1.26.9-alpine`,
`sha256:3082400e369fa24d5fc60bca20edab3f6d604e0c5a690ec66b295eff4dd87ade`;
`golang:1.26.10-alpine`,
`sha256:9ce231b28998b32e116927c61aa5c406183061d4f3f2d61b292c88e7f6bdf7ca`;
the checked-in toolchain files and release-script guards must reject an older
compiler so local, exact-candidate, provider control-plane, and container builds
cannot silently reintroduce the vulnerable runtime.
@@ -5255,6 +5260,12 @@ copies; a successful production build alone does not establish this property.
Vitest migration must preserve test assertions while using constructable
constructor mocks and explicit standalone mock-history cleanup. Dependency
updates must retain the required full-graph audit rather than suppressing it.
The same boundary keeps Vite on 6.x at or above `6.4.5`: 6.4.4 bundles the
patched `launch-editor` (GHSA-587h-7wmj-h5pm, GHSA-r825-w8mm-5pxw,
GHSA-698v-4r3x-3vcv) and 6.4.5 applies the dev-server `server.fs` check after
stripping the query. These reach only the local dev server, never the built
product, and the same proof file pins the manifest range and the single locked
copy.
The October deserialization/parsing advisory floors are Seroval `1.6.3`
(GHSA-p6vx-979v-rg4c and GHSA-jp82-f5mq-hwhp), postcss-selector-parser `7.1.6`
+4 -4
View File
@@ -38,7 +38,7 @@
"tailwindcss": "^4.3.3",
"typescript": "^5.3.0",
"typescript-eslint": "^8.71.1",
"vite": "^6.4.3",
"vite": "^6.4.5",
"vite-plugin-solid": "^2.11.14",
"vite-plugin-sri-gen": "^1.7.4",
"vitest": "^4.1.11"
@@ -5764,9 +5764,9 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
"integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
"version": "6.4.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.5.tgz",
"integrity": "sha512-4enwjFfi4IwtEh0w+jPzpLVOFQiPHmmzPxcHXQOyEur47o6u0jE573DmGpjA+7irXoMLYRZD5ZaFUEneS3abPg==",
"dev": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -91,7 +91,7 @@
"tailwindcss": "^4.3.3",
"typescript": "^5.3.0",
"typescript-eslint": "^8.71.1",
"vite": "^6.4.3",
"vite": "^6.4.5",
"vite-plugin-solid": "^2.11.14",
"vite-plugin-sri-gen": "^1.7.4",
"vitest": "^4.1.11"
@@ -125,6 +125,18 @@ describe('frontend dependency security floors', () => {
}
});
it('keeps Vite above the bundled launch-editor and dev-server fs floors', () => {
// GHSA-587h-7wmj-h5pm, GHSA-r825-w8mm-5pxw and GHSA-698v-4r3x-3vcv: Vite
// bundles launch-editor, fixed on 6.x in 6.4.4. 6.4.5 also applies the
// server.fs check after stripping the query.
expect(manifest.devDependencies.vite).toBe('^6.4.5');
const versions = lockedVersions('vite');
expect(versions).toHaveLength(1);
expect(versions[0]).not.toContain('-');
expect(parseVersion(versions[0])[0], 'vite must stay on the reviewed major').toBe(6);
expect(atLeast(versions[0], [6, 4, 5]), `vite ${versions[0]} is vulnerable`).toBe(true);
});
it('keeps every js-yaml copy above the empty-merge CPU exhaustion floor', () => {
// GHSA-2883-xcg3-v3hh: our override keeps every transitive copy on patched 4.x.
expect(manifest.overrides['js-yaml']).toBe('^4.3.2');
+1 -1
View File
@@ -2,7 +2,7 @@ module github.com/rcourtman/pulse-go-rewrite
go 1.26.0
toolchain go1.26.9
toolchain go1.26.10
require (
github.com/IGLOU-EU/go-wildcard/v2 v2.1.1
+1 -1
View File
@@ -1 +1 @@
go1.26.9
go1.26.10
+1 -1
View File
@@ -44,7 +44,7 @@ case "${OUTPUT_ROOT}" in
;;
esac
required_go="go1.26.9"
required_go="go1.26.10"
current_go="$(go env GOVERSION 2>/dev/null || true)"
if [[ "${current_go}" != "${required_go}" ]]; then
echo "Error: Go toolchain must be ${required_go} (got ${current_go:-unknown})." >&2
+1 -1
View File
@@ -20,7 +20,7 @@ if [ -x /usr/local/go/bin/go ]; then
fi
# Release artifacts must be built with the vetted toolchain to match security-gate evidence.
required_go="go1.26.9"
required_go="go1.26.10"
current_go="$(go env GOVERSION 2>/dev/null || true)"
if [[ "${PULSE_SKIP_GO_VERSION_CHECK:-false}" != "true" ]]; then
if [[ "${current_go}" != "${required_go}" ]]; then
@@ -35,7 +35,7 @@ if [[ -z "${PULSE_UPDATE_SIGNING_PUBLIC_KEY:-}" ]]; then
exit 3
fi
required_go="go1.26.9"
required_go="go1.26.10"
go_version="$(go env GOVERSION 2>/dev/null || true)"
if [[ "${go_version}" != "${required_go}" ]]; then
echo "Error: Go toolchain must be ${required_go} (got ${go_version:-unknown})." >&2
+2 -2
View File
@@ -71,8 +71,8 @@ def telemetry():
def execute(repo, output):
output.mkdir(parents=True, exist_ok=False) # never overwrite a previous attempt
os.environ.update(GOTOOLCHAIN='local', GOMAXPROCS='4', GOFLAGS='-mod=readonly', GOENV='off', GOAMD64='v1', CGO_ENABLED='1')
if run(['go', 'version']).strip() != 'go version go1.26.9 linux/amd64':
raise ValueError('requires exact hosted experiment toolchain go1.26.9 linux/amd64')
if run(['go', 'version']).strip() != 'go version go1.26.10 linux/amd64':
raise ValueError('requires exact hosted experiment toolchain go1.26.10 linux/amd64')
metadata = {
'purpose': 'diagnostic-only; no gate disposition or release candidate',
'rounds': ROUNDS, 'gomaxprocs': 4, 'benchtime': '1s',
+1 -1
View File
@@ -3,7 +3,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VERSION_FILE="${SCRIPT_DIR}/.go-version"
DEFAULT_VERSION="go1.26.9"
DEFAULT_VERSION="go1.26.10"
TARGET_ROOT="/opt/toolchains/go"
DOWNLOAD_ROOT="https://dl.google.com/go"
GOPATH_DIR="/var/lib/pulse/go"
@@ -1629,7 +1629,7 @@ func TestDockerBuildUsesCanonicalReleaseLdflags(t *testing.T) {
dockerfile := string(dockerfileBytes)
dockerRequired := []string{
"FROM --platform=linux/amd64 node:24-alpine@sha256:" + node24Amd64FrontendDigest + " AS frontend-builder",
`FROM --platform=linux/amd64 golang:1.26.9-alpine@sha256:`,
`FROM --platform=linux/amd64 golang:1.26.10-alpine@sha256:`,
`FROM backend-builder AS release-assets-builder`,
`AS agent_runtime`,
`AS pulse-runtime-foundation`,
@@ -1664,7 +1664,7 @@ func TestDockerBuildUsesCanonicalReleaseLdflags(t *testing.T) {
}
}
assertDigestPinnedDockerStage(t, dockerfile, `FROM --platform=linux/amd64 node:24-alpine@sha256:`, ` AS frontend-builder`)
assertDigestPinnedDockerStage(t, dockerfile, `FROM --platform=linux/amd64 golang:1.26.9-alpine@sha256:`, ` AS backend-builder`)
assertDigestPinnedDockerStage(t, dockerfile, `FROM --platform=linux/amd64 golang:1.26.10-alpine@sha256:`, ` AS backend-builder`)
assertDigestPinnedDockerStage(t, dockerfile, `FROM alpine:3.24@sha256:`, ` AS agent_runtime`)
assertDigestPinnedDockerStage(t, dockerfile, `FROM alpine:3.24@sha256:`, ` AS pulse-runtime-foundation`)
hostedStart := strings.Index(dockerfile, `FROM pulse-runtime-base AS hosted_runtime`)
@@ -1677,7 +1677,7 @@ func TestDockerBuildUsesCanonicalReleaseLdflags(t *testing.T) {
t.Fatalf("hosted_runtime target must not depend on installer rendering or embedded agent artifacts:\n%s", hostedStage)
}
if strings.Contains(dockerfile, `FROM --platform=linux/amd64 node:24-alpine AS frontend-builder`) ||
strings.Contains(dockerfile, `FROM --platform=linux/amd64 golang:1.26.9-alpine AS backend-builder`) ||
strings.Contains(dockerfile, `FROM --platform=linux/amd64 golang:1.26.10-alpine AS backend-builder`) ||
strings.Contains(dockerfile, `FROM alpine:3.24 AS agent_runtime`) ||
strings.Contains(dockerfile, `FROM alpine:3.24 AS pulse-runtime-base`) {
t.Fatal("Dockerfile base images must be pinned by immutable @sha256 digests")
@@ -13,7 +13,7 @@ func TestIntegrationContainersUseGovernedImmutableBases(t *testing.T) {
t.Fatalf("read mock GitHub Dockerfile: %v", err)
}
dockerfile := string(dockerfileBytes)
assertDigestPinnedDockerStage(t, dockerfile, `FROM golang:1.26.9-alpine@sha256:`, ` AS builder`)
assertDigestPinnedDockerStage(t, dockerfile, `FROM golang:1.26.10-alpine@sha256:`, ` AS builder`)
assertDigestPinnedDockerStage(t, dockerfile, `FROM alpine:3.24@sha256:`, ``)
composeBytes, err := os.ReadFile(repoFile("tests", "integration", "docker-compose.test.yml"))
@@ -15,7 +15,7 @@ func TestProviderMSPGoBuilderMatchesIntegrationMock(t *testing.T) {
// These builders use the same Go patch level and immutable image. The
// mock's build must not exercise a different compiler from the hosted
// control plane merely because Dependabot advanced one Dockerfile.
const expected = "golang:1.26.9-alpine@sha256:3082400e369fa24d5fc60bca20edab3f6d604e0c5a690ec66b295eff4dd87ade"
const expected = "golang:1.26.10-alpine@sha256:9ce231b28998b32e116927c61aa5c406183061d4f3f2d61b292c88e7f6bdf7ca"
pattern := regexp.MustCompile(`(?m)^FROM (?:--platform=\$BUILDPLATFORM )?(golang:[^[:space:]]+) AS builder$`)
for _, path := range [][]string{
{"deploy", "provider-msp", "Dockerfile.control-plane"},
@@ -328,7 +328,7 @@ func TestProviderMSPControlPlaneDockerfileBuildsReleaseLicenseBinary(t *testing.
"FROM --platform=linux/amd64 node:24-alpine@sha256:"+node24Amd64FrontendDigest+" AS frontend-builder",
"npm ci",
"npm run build",
"FROM --platform=$BUILDPLATFORM golang:1.26.9-alpine@sha256:",
"FROM --platform=$BUILDPLATFORM golang:1.26.10-alpine@sha256:",
"FROM alpine:3.24@sha256:",
"ARG PULSE_LICENSE_PUBLIC_KEY_SHA256",
"ARG TARGETOS",
@@ -355,7 +355,7 @@ func TestProviderMSPControlPlaneDockerfileBuildsReleaseLicenseBinary(t *testing.
"CGO_ENABLED=0 go build -o /pulse-control-plane ./cmd/pulse-control-plane",
)
assertDigestPinnedDockerStage(t, text, `FROM --platform=linux/amd64 node:24-alpine@sha256:`, ` AS frontend-builder`)
assertDigestPinnedDockerStage(t, text, `FROM --platform=$BUILDPLATFORM golang:1.26.9-alpine@sha256:`, ` AS builder`)
assertDigestPinnedDockerStage(t, text, `FROM --platform=$BUILDPLATFORM golang:1.26.10-alpine@sha256:`, ` AS builder`)
assertDigestPinnedDockerStage(t, text, `FROM alpine:3.24@sha256:`, ` AS control-plane-runtime-foundation`)
}
+3 -3
View File
@@ -414,9 +414,9 @@ test_go_release_toolchain_floor() {
module_toolchain="$(sed -n 's/^toolchain //p' "${ROOT_DIR}/go.mod")"
installed_toolchain="$(cd "${ROOT_DIR}" && go env GOVERSION)"
assert_contains "Go module uses the patched release toolchain" "${module_toolchain}" "go1.26.9"
assert_contains "dev toolchain installer uses the patched release toolchain" "$(<"${ROOT_DIR}/scripts/.go-version")" "go1.26.9"
assert_contains "active dev runtime resolves the patched release toolchain" "${installed_toolchain}" "go1.26.9"
assert_contains "Go module uses the patched release toolchain" "${module_toolchain}" "go1.26.10"
assert_contains "dev toolchain installer uses the patched release toolchain" "$(<"${ROOT_DIR}/scripts/.go-version")" "go1.26.10"
assert_contains "active dev runtime resolves the patched release toolchain" "${installed_toolchain}" "go1.26.10"
}
test_backend_race_suite_keeps_hosted_runner_timeout_headroom() {
+2 -2
View File
@@ -56,7 +56,7 @@ class DiagnosticTests(unittest.TestCase):
def fake_run(args, cwd=None):
if args == ['go', 'version']:
return 'go version go1.26.7 linux/amd64' if bad_go else 'go version go1.26.9 linux/amd64'
return 'go version go1.26.7 linux/amd64' if bad_go else 'go version go1.26.10 linux/amd64'
if args[:2] == ['git', 'rev-parse']:
return 'wrong' if bad_tree else diag.TREES[args[2].split('^')[0]]
if args[:3] == ['go', 'tool', 'nm']:
@@ -151,7 +151,7 @@ class DiagnosticTests(unittest.TestCase):
self.assertIn('[[ "$GITHUB_RUN_ATTEMPT" == 1 ]]', workflow)
self.assertIn('path: root-pair-evidence/', workflow)
self.assertIn('if: always()', workflow)
self.assertIn("go-version: '1.26.9'", workflow)
self.assertIn("go-version: '1.26.10'", workflow)
self.assertIn('timeout-minutes: 45', workflow)
@@ -1,4 +1,4 @@
FROM golang:1.26.9-alpine@sha256:3082400e369fa24d5fc60bca20edab3f6d604e0c5a690ec66b295eff4dd87ade AS builder
FROM golang:1.26.10-alpine@sha256:9ce231b28998b32e116927c61aa5c406183061d4f3f2d61b292c88e7f6bdf7ca AS builder
WORKDIR /build
COPY go.mod ./