diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..98213f5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,45 @@ +version: 2 + +# Dependabot is what makes SHA-pinned actions maintainable. Pinning without it +# means the pins rot: an action stays frozen at whatever commit was current the +# day someone wrote it, security fixes included. +# +# This covers version updates. Dependabot *security* updates (the automatic PRs +# for a newly disclosed advisory) are a separate repository setting and are +# already enabled. +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + groups: + # golang.org/x/* move together and are almost always safe; one PR rather + # than five keeps the noise down without hiding anything. + golang-x: + patterns: + - golang.org/x/* + + - package-ecosystem: npm + directory: /frontend + schedule: + interval: weekly + open-pull-requests-limit: 5 + groups: + # The Svelte 3 / Vite 3 toolchain moves as a unit — svelte, svelte-check, + # svelte-preprocess and the Vite plugin are version-locked against each + # other, so separate PRs for them just fail each other's CI. + svelte-toolchain: + patterns: + - svelte + - svelte-* + - "@sveltejs/*" + - vite + + # The workflows pin actions by commit SHA, so this is the only thing that + # ever moves them. + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75e1df6..8718dfa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,23 +11,70 @@ on: permissions: contents: read +# Superseded pushes to the same pull request are pointless work. Pushes to main +# are never cancelled: every commit on the default branch gets its own verdict. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: - backend: - name: Go (${{ matrix.os }}) + # The full application build, on every platform we ship. This is deliberately + # `wails build` and not a bare `go build`: the CLI regenerates the JS bindings + # and builds the frontend before compiling, so this is the only check that + # covers the whole pipeline the release workflow runs. Without it, CI can be + # green while `wails build` is broken, and the breakage only surfaces when a + # tag is pushed — which is to say, when it is already a failed release. + build: + name: Build & test (${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - os: [ubuntu-24.04, windows-latest, macos-latest] + include: + - os: ubuntu-24.04 + platform: linux/amd64 + - os: windows-latest + platform: windows/amd64 + - os: macos-latest + platform: darwin/universal + steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: + # From go.mod, so the toolchain has exactly one source of truth. go-version-file: go.mod + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "20" + cache: npm + cache-dependency-path: frontend/package-lock.json + - name: Install Linux dependencies if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev pkg-config - - name: Create frontend/dist placeholder for go:embed - run: mkdir -p frontend/dist && touch frontend/dist/.gitkeep + + - name: Install Wails CLI + # Pinned, and kept in sync with the wails/v2 version in go.mod. The CLI + # generates the bridge bindings and drives the build, so letting it + # float means a future release can change the output — or drift from the + # library it generates against — with nothing in this repository + # changing. The release workflow pins the same version. + run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.15.0 + + - name: Build + shell: bash + run: | + TAGS="" + if [ "$RUNNER_OS" = "Linux" ]; then TAGS="-tags webkit2_41"; fi + wails build -platform ${{ matrix.platform }} $TAGS + - name: Vet & test shell: bash run: | @@ -36,19 +83,149 @@ jobs: go vet $TAGS ./... go test $TAGS -race -count=1 ./... - frontend: - name: Frontend (check + build) - runs-on: ubuntu-latest + quality: + name: Quality (lint, types, tidy) + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "20" cache: npm cache-dependency-path: frontend/package-lock.json - - run: npm ci + + - name: Install Linux dependencies + run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev pkg-config + + - name: Install frontend deps + run: npm ci working-directory: frontend - - run: npx svelte-check --tsconfig ./tsconfig.json --fail-on-warnings + + - name: Svelte type check + run: npx svelte-check --tsconfig ./tsconfig.json --fail-on-warnings working-directory: frontend - - run: npm run build + + # main.go carries `//go:embed all:frontend/dist`, and frontend/dist is + # gitignored, so anything that compiles the main package fails with + # "pattern all:frontend/dist: no matching files found" before it analyses a + # line. staticcheck stops outright; govulncheck is worse — it carries on + # and silently reports on the subpackages alone, so main would look clean + # because it was never looked at. A placeholder rather than a real frontend + # build: neither tool inspects the bytes the binary embeds, and the build + # job already compiles the real thing on all three platforms. + - name: Placeholder for the embedded frontend + run: | + mkdir -p frontend/dist + echo '' > frontend/dist/index.html + + - name: Staticcheck + # Catches what go vet does not: dead stores, misused stdlib contracts, + # simplifications that hide intent. + # + # Pinned, unlike govulncheck below. A linter that floats can fail a + # commit that changed nothing, because a newly added check fired — the + # finding may well be real, but it should arrive in its own deliberate + # bump, not as a surprise on someone else's pull request. Dependabot + # does not track versions inside a `run:` step, so this is bumped by + # hand. + run: | + go install honnef.co/go/tools/cmd/staticcheck@v0.8.1 + staticcheck -tags webkit2_41 ./... + + - name: go.mod is tidy + # A stale go.mod/go.sum means the dependency set that CI resolves is not + # the one recorded in the repo, so every other check here is verifying + # something slightly different from what a release builds. + run: | + go mod tidy + git diff --exit-code go.mod go.sum + + security: + name: Security audit + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "20" + cache: npm + cache-dependency-path: frontend/package-lock.json + + # main.go carries `//go:embed all:frontend/dist`, and frontend/dist is + # gitignored, so anything that compiles the main package fails with + # "pattern all:frontend/dist: no matching files found" before it analyses a + # line. staticcheck stops outright; govulncheck is worse — it carries on + # and silently reports on the subpackages alone, so main would look clean + # because it was never looked at. A placeholder rather than a real frontend + # build: neither tool inspects the bytes the binary embeds, and the build + # job already compiles the real thing on all three platforms. + - name: Placeholder for the embedded frontend + run: | + mkdir -p frontend/dist + echo '' > frontend/dist/index.html + + # govulncheck's analysis is only as complete as the packages it manages to + # load, and it does not fail when one of them does not build — it reports + # on the rest and prints a clean bill of health. On Linux the Wails + # packages need cgo against GTK/WebKit and the webkit2_41 tag, so without + # these the main package silently drops out of the scan. This build is the + # tripwire: it fails loudly if the toolchain cannot compile what the next + # step is supposed to analyse. + - name: Install Linux dependencies + run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev pkg-config + + - name: Confirm the tree builds before scanning it + run: go build -tags webkit2_41 ./... + + - name: Go vulnerability check + # Reports only vulnerabilities in code paths the binary actually calls, + # standard library included — which is how the Go 1.25 -> 1.26 toolchain + # bump was found to be a security fix and not just housekeeping. + # + # Deliberately @latest, unlike staticcheck: the point of this step is to + # know about advisories published since the last commit, and an old + # scanner reports an old world. A new finding here means a real new + # advisory, not a new opinion about existing code. + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + govulncheck -tags webkit2_41 ./... + + - name: npm audit (production dependencies) + # --omit=dev because the Vite/esbuild advisories affect the developer + # machine during `wails dev`, not the shipped binary; they are tracked + # separately and must not block a release. No `|| true` here: a swallowed + # exit code turns this step into decoration. + run: npm audit --omit=dev --audit-level=high working-directory: frontend + + # Blocks a pull request that introduces a dependency carrying a known + # advisory. govulncheck cannot do this: it reports on what is already merged. + dependency-review: + name: Dependency review + runs-on: ubuntu-24.04 + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: high diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..9f77d73 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,88 @@ +name: CodeQL + +# Static analysis of the code itself, which nothing else here does: go vet and +# staticcheck look for mistakes, govulncheck and npm audit look for known +# vulnerable dependencies, and none of them go looking for an exploitable +# pattern in code we wrote. The weekly run matters as much as the per-push one — +# a query added to CodeQL after a commit lands would otherwise never see it. + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: '17 4 * * 1' + +permissions: + contents: read + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-24.04 + permissions: + # Only this job may write findings; everything else stays read-only. + security-events: write + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + language: [go, javascript-typescript] + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Go + if: matrix.language == 'go' + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + + - name: Install Linux dependencies + if: matrix.language == 'go' + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev pkg-config + + - name: Initialize CodeQL + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + languages: ${{ matrix.language }} + queries: security-and-quality + + # main.go carries `//go:embed all:frontend/dist`, and frontend/dist is + # produced by the frontend build and gitignored — so a plain `go build` + # fails with "pattern all:frontend/dist: no matching files found" before a + # single query runs. + # + # A placeholder rather than a real frontend build: CodeQL analyses the Go + # code, not the bytes the binary embeds, so `npm ci && npm run build` would + # cost minutes to produce something no query looks at. The + # javascript-typescript leg analyses the frontend SOURCE, which is what + # matters there. + - name: Placeholder for the embedded frontend + if: matrix.language == 'go' + run: | + mkdir -p frontend/dist + echo '' > frontend/dist/index.html + + # The Go analysis needs a real build. Autobuild would not know about the + # webkit2_41 tag, and would analyse a subset of the tree without saying so. + - name: Build Go + if: matrix.language == 'go' + run: go build -tags webkit2_41 ./... + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + category: /language:${{ matrix.language }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8461403..9c121b9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,11 +29,13 @@ jobs: name: Windows (amd64) runs-on: windows-latest steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "20" cache: npm @@ -72,7 +74,7 @@ jobs: cp build/bin/SyslogStudio.exe SyslogStudio-windows-amd64.exe mv build/bin/*installer*.exe SyslogStudio-windows-amd64-setup.exe powershell -Command "Compress-Archive -Path build/bin/SyslogStudio.exe -DestinationPath SyslogStudio-windows-amd64.zip" - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: windows-amd64 path: | @@ -84,11 +86,13 @@ jobs: name: macOS (universal) runs-on: macos-latest steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "20" cache: npm @@ -122,7 +126,7 @@ jobs: ln -s /Applications dmg-staging/Applications hdiutil create -volname "SyslogStudio" -srcfolder dmg-staging -ov -format UDZO \ "SyslogStudio-macos-universal.dmg" - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: macos-universal path: | @@ -133,11 +137,13 @@ jobs: name: Linux (amd64) runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "20" cache: npm @@ -177,7 +183,7 @@ jobs: Depends: libgtk-3-0, libwebkit2gtk-4.1-0 EOF dpkg-deb --build pkg "SyslogStudio-linux-amd64.deb" - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: linux-amd64 path: | @@ -191,12 +197,18 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + # Build provenance: id-token to get the OIDC token that proves the + # attestation came from this workflow run, attestations to record it. + id-token: write + attestations: write steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: artifacts merge-multiple: true @@ -219,10 +231,31 @@ jobs: fi go run ./tools/updatersign sign artifacts/SyslogStudio-checksums.txt test -s artifacts/SyslogStudio-checksums.txt.sig + # Signed, verifiable provenance for every published asset: which workflow, + # which commit, which runner produced these exact bytes. It answers a + # question the Ed25519 manifest signature does not — that signature proves + # the checksums were signed by the holder of the release key, not that the + # binaries were built from this repository by this workflow. + # + # Anyone can check a download without trusting this repo's tooling: + # gh attestation verify SyslogStudio-windows-amd64.exe --repo Wasabules/SyslogStudio + - name: Attest build provenance + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: | + artifacts/SyslogStudio-windows-amd64.exe + artifacts/SyslogStudio-windows-amd64-setup.exe + artifacts/SyslogStudio-windows-amd64.zip + artifacts/SyslogStudio-macos-universal.dmg + artifacts/SyslogStudio-macos-universal.zip + artifacts/SyslogStudio-linux-amd64 + artifacts/SyslogStudio-linux-amd64.deb + artifacts/SyslogStudio-linux-amd64.tar.gz + - name: List release assets run: ls -lh artifacts/ - name: Create GitHub Release - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 with: generate_release_notes: true files: artifacts/* diff --git a/internal/syslog/framing_test.go b/internal/syslog/framing_test.go index c3a0ed9..eb56539 100644 --- a/internal/syslog/framing_test.go +++ b/internal/syslog/framing_test.go @@ -36,9 +36,9 @@ func eq(t *testing.T, got, want []string) { func TestFraming_OctetCounting_Single(t *testing.T) { msg := "<34>1 2003-10-11T22:14:15.003Z host app - - - hello" - input := "51 " + msg // note: length must match msg byte length - // Recompute the correct length rather than hardcoding. - input = itoa(len(msg)) + " " + msg + // The octet count must be the byte length of msg, so compute it rather + // than hardcoding a number that silently rots when msg is edited. + input := itoa(len(msg)) + " " + msg eq(t, scanAll(t, input), []string{msg}) }