lint: adopt org-standard .golangci.yml and golangci-lint v2.12.2 (closes #14)
check / check (push) Failing after 1s

The old backend/.golangci.yml declared version "2" but used v1 schema
keys, so under v2 it never validated and its thresholds were inert: the
linter ran at defaults. Replace it verbatim with the org-standard file,
repin the Dockerfile.backend lint stage to golangci-lint v2.12.2, and
assert the config's sha256 as the first step of the backend lint target
so it cannot silently drift again -- a local hash check, no network.

The standard config surfaces findings only in the tests: the repeated
IP literals in middleware_test.go become named constants (goconst) and
its request switches to NewRequestWithContext (noctx). reportbuf.go's
gosec suppression gains a plain justification comment. The rest of the
backend, including the fx-based server lifecycle, is already clean.
TODO.md updated.

Model: opus-4-8
This commit was merged in pull request #31.
This commit is contained in:
2026-09-21 19:30:06 +02:00
parent d7cf010e00
commit 7a1ee6e5a8
6 changed files with 81 additions and 34 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
# Lint stage — fast feedback on formatting and lint issues. The # Lint stage — fast feedback on formatting and lint issues. The
# golangci/golangci-lint image ships Go, gofmt, make and the linter, so # golangci/golangci-lint image ships Go, gofmt, make and the linter, so
# nothing is installed here. # nothing is installed here.
# golangci/golangci-lint:v2.7.2 (2026-08-09) # golangci/golangci-lint:v2.12.2 (2026-08-10)
FROM golangci/golangci-lint@sha256:5d6d5c70a61f1356adfd9dd6316ce286799fefc9d743421356ff1b00842368ba AS lint FROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint
WORKDIR /src WORKDIR /src
COPY backend/go.mod backend/go.sum ./ COPY backend/go.mod backend/go.sum ./
+22 -7
View File
@@ -10,15 +10,16 @@
# Status # Status
pre-1.0. No git tags. Backend work in flight on feat/reportbuf-storage (dirty: pre-1.0. No git tags. `feat/reportbuf-storage` is merged; the backend, the CI
src/main.js). Frontend is functional; backend is new and unmerged. workflow, and the backend repo standard files are all on `main`. Frontend and
backend are both functional. Working toward the 1.0.0 milestone by closing the
remaining repo-compliance issues on the tracker.
# Next Step # Next Step
Land feat/reportbuf-storage: finish the in-progress src/main.js change, get make Confirm the `.gitea/workflows/check.yml` run is green (main always green
check green, and merge the branch to main. The branch adds the backend (buffered policy). The workflow file is already on `main`; what is unverified is that its
zstd-compressed report storage), the CI workflow, and backend repo standard latest run passes.
files, so merging it also closes most compliance gaps.
# Completed Steps # Completed Steps
@@ -36,6 +37,14 @@ files, so merging it also closes most compliance gaps.
frame/sniff/referrer/permissions headers) registered before CORS, and frame/sniff/referrer/permissions headers) registered before CORS, and
trusted-proxy client IP resolution honouring `X-Forwarded-For` / `X-Real-IP` trusted-proxy client IP resolution honouring `X-Forwarded-For` / `X-Real-IP`
only from a `TRUSTED_PROXIES` allowlist (loopback plus RFC1918 by default) only from a `TRUSTED_PROXIES` allowlist (loopback plus RFC1918 by default)
- 2026-08-10: adopted the org-standard `backend/.golangci.yml` verbatim and
moved the pinned golangci-lint from v2.7.2 to v2.12.2 (the `lint` stage of
`Dockerfile.backend` now pins the `golangci/golangci-lint:v2.12.2` image by
digest); the previous config declared `version: "2"` but used v1 schema keys,
so every threshold in it was inert and its green result was meaningless.
`backend/Makefile`'s `lint` target now asserts the config's sha256 against the
canonical file first, so drift from the org standard fails the build instead
of silently degrading to defaults
- 2026-08-10: every interactive control now meets the 44x44 CSS px minimum tap - 2026-08-10: every interactive control now meets the 44x44 CSS px minimum tap
target (`.pin-btn`, `#interval-select`, the debug-log label and, on narrow target (`.pin-btn`, `#interval-select`, the debug-log label and, on narrow
viewports, `#pause-btn`). The pin button's hit area grows via matching viewports, `#pause-btn`). The pin button's hit area grows via matching
@@ -65,7 +74,7 @@ files, so merging it also closes most compliance gaps.
shims, README Entrypoints section shims, README Entrypoints section
- 2026-02-27: backend with buffered zstd-compressed report storage; CI workflow - 2026-02-27: backend with buffered zstd-compressed report storage; CI workflow
and backend repo standard files; backend Dockerfile fixed (Go 1.25, and backend repo standard files; backend Dockerfile fixed (Go 1.25,
golangci-lint) and moved to repo root (feat/reportbuf-storage, unmerged) golangci-lint) and moved to repo root (feat/reportbuf-storage)
- 2026-02-26: host row layout redesigned with CSS grid; overflow and spacing - 2026-02-26: host row layout redesigned with CSS grid; overflow and spacing
fixes; nginx config extracted; port hardcoded to 8080 fixes; nginx config extracted; port hardcoded to 8080
- 2026-02-26: debug log panel, median stats, recovery probe, Docker build fix, - 2026-02-26: debug log panel, median stats, recovery probe, Docker build fix,
@@ -87,3 +96,9 @@ files, so merging it also closes most compliance gaps.
(main always green policy) (main always green policy)
- Decide what to do with untracked resume.sh: commit it, gitignore it, or delete - Decide what to do with untracked resume.sh: commit it, gitignore it, or delete
it it
- Upstream fix needed in `sneak/prompts`: the org-standard `.golangci.yml`
enables `gomodguard`, which golangci-lint v2.12.2 reports as deprecated since
v2.12.0 and replaced by `gomodguard_v2`, so every backend lint run prints a
deprecation warning. The file is standardized and must never be edited in this
repo, so nothing can be done here beyond tracking it — tracked at
<https://git.eeqj.de/sneak/netwatch/issues/41>
+5 -3
View File
@@ -1,5 +1,9 @@
version: "2" version: "2"
# Config schema uses the golangci-lint v2 layout (settings live under
# linters.settings, not top-level linters-settings) so that the
# thresholds below are actually applied by golangci-lint >= v2.
run: run:
timeout: 5m timeout: 5m
modules-download-mode: readonly modules-download-mode: readonly
@@ -14,8 +18,7 @@ linters:
- wsl # Deprecated, replaced by wsl_v5 - wsl # Deprecated, replaced by wsl_v5
- wrapcheck # Too verbose for internal packages - wrapcheck # Too verbose for internal packages
- varnamelen # Short names like db, id are idiomatic Go - varnamelen # Short names like db, id are idiomatic Go
settings:
linters-settings:
lll: lll:
line-length: 88 line-length: 88
funlen: funlen:
@@ -27,6 +30,5 @@ linters-settings:
threshold: 100 threshold: 100
issues: issues:
exclude-use-default: false
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
+19
View File
@@ -10,6 +10,17 @@ GOLDFLAGS += -s -w
GOLDFLAGS += -X main.Version=$(VERSION) GOLDFLAGS += -X main.Version=$(VERSION)
GOLDFLAGS += -X main.Buildarch=$(BUILDARCH) GOLDFLAGS += -X main.Buildarch=$(BUILDARCH)
# macOS ships shasum rather than sha256sum.
SHA256SUM := $(shell command -v sha256sum >/dev/null 2>&1 && echo sha256sum || echo shasum -a 256)
# .golangci.yml is standardized org-wide and must never be edited here
# (REPO_POLICIES.md). Its last silent drift replaced the v2 schema with
# v1 keys, which left every threshold in the file inert while the build
# stayed green. The lint target therefore asserts the file still matches
# the canonical copy byte for byte. The check is a local hash comparison:
# no network, no remote schema, nothing unpinned in the build path.
GOLANGCI_CONFIG_SHA256 := 021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb
.PHONY: all build test lint fmt fmt-check check docker hooks run clean .PHONY: all build test lint fmt fmt-check check docker hooks run clean
all: build all: build
@@ -22,6 +33,14 @@ test:
timeout 30 go test ./... timeout 30 go test ./...
lint: lint:
@actual=$$($(SHA256SUM) .golangci.yml | cut -d' ' -f1); \
if [ "$$actual" != "$(GOLANGCI_CONFIG_SHA256)" ]; then \
echo ".golangci.yml has drifted from the org standard."; \
echo " expected $(GOLANGCI_CONFIG_SHA256)"; \
echo " actual $$actual"; \
echo "Restore it verbatim from sneak/prompts; do not edit it."; \
exit 1; \
fi
golangci-lint run ./... golangci-lint run ./...
fmt: fmt:
+21 -12
View File
@@ -9,6 +9,15 @@ import (
"sneak.berlin/go/netwatch/internal/middleware" "sneak.berlin/go/netwatch/internal/middleware"
) )
const (
// loopbackPeer is a remote address inside the trusted-proxy allowlist.
loopbackPeer = "127.0.0.1:5000"
// forwardedIP is the client address presented via X-Forwarded-For.
forwardedIP = "203.0.113.7"
// realIP is the client address presented via X-Real-IP.
realIP = "203.0.113.9"
)
func mustPrefixes(t *testing.T, cidrs ...string) []netip.Prefix { func mustPrefixes(t *testing.T, cidrs ...string) []netip.Prefix {
t.Helper() t.Helper()
@@ -41,32 +50,32 @@ func clientIPCases() []clientIPCase {
return []clientIPCase{ return []clientIPCase{
{ {
name: "trusted proxy uses forwarded-for", name: "trusted proxy uses forwarded-for",
remoteAddr: "127.0.0.1:5000", remoteAddr: loopbackPeer,
xff: "203.0.113.7", xff: forwardedIP,
want: "203.0.113.7", want: forwardedIP,
}, },
{ {
name: "trusted proxy uses left-most of chain", name: "trusted proxy uses left-most of chain",
remoteAddr: "10.1.2.3:5000", remoteAddr: "10.1.2.3:5000",
xff: "203.0.113.7, 10.1.2.3", xff: forwardedIP + ", 10.1.2.3",
want: "203.0.113.7", want: forwardedIP,
}, },
{ {
name: "trusted proxy falls back to x-real-ip", name: "trusted proxy falls back to x-real-ip",
remoteAddr: "127.0.0.1:5000", remoteAddr: loopbackPeer,
xRealIP: "203.0.113.9", xRealIP: realIP,
want: "203.0.113.9", want: realIP,
}, },
{ {
name: "untrusted peer ignores forwarded-for", name: "untrusted peer ignores forwarded-for",
remoteAddr: "198.51.100.4:5000", remoteAddr: "198.51.100.4:5000",
xff: "203.0.113.7", xff: forwardedIP,
want: "198.51.100.4", want: "198.51.100.4",
}, },
{ {
name: "untrusted peer ignores x-real-ip", name: "untrusted peer ignores x-real-ip",
remoteAddr: "198.51.100.4:5000", remoteAddr: "198.51.100.4:5000",
xRealIP: "203.0.113.9", xRealIP: realIP,
want: "198.51.100.4", want: "198.51.100.4",
}, },
{ {
@@ -76,7 +85,7 @@ func clientIPCases() []clientIPCase {
}, },
{ {
name: "trusted proxy with garbage header uses peer", name: "trusted proxy with garbage header uses peer",
remoteAddr: "127.0.0.1:5000", remoteAddr: loopbackPeer,
xff: "not-an-ip", xff: "not-an-ip",
want: "127.0.0.1", want: "127.0.0.1",
}, },
@@ -119,7 +128,7 @@ func TestSecurityHeaders(t *testing.T) {
) )
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody) req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)
handler.ServeHTTP(rec, req) handler.ServeHTTP(rec, req)
want := map[string]string{ want := map[string]string{
+3 -1
View File
@@ -169,7 +169,9 @@ func (b *Buffer) writeFile(data []byte) {
name := fmt.Sprintf("reports-%s.jsonl.zst", ts) name := fmt.Sprintf("reports-%s.jsonl.zst", ts)
path := filepath.Join(b.dataDir, name) path := filepath.Join(b.dataDir, name)
f, err := os.OpenFile( //nolint:gosec // path built from controlled dataDir + timestamp // path is built from the operator-supplied dataDir plus a
// generated timestamp, so it carries no external input.
f, err := os.OpenFile( //nolint:gosec // see comment above
path, path,
os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.O_WRONLY|os.O_CREATE|os.O_EXCL,
filePerms, filePerms,