fix: address audit Bundle B — error-handling discipline#144
Conversation
Tracker: closes audit findings F001, F003, F004, F019. Filed F002 as #137. F001 — Bump github.com/jackc/pgx/v5 from v5.8.0 to v5.9.0 to clear GO-2026-4771 (CVE-2026-33815) and GO-2026-4772 (CVE-2026-33816). Both vulns reachable from internal/store/store.go via Close and AppendEventWithVersion. govulncheck on production packages is now clean. Two GO-2026-4887 / GO-2026-4883 reports remain on the docker/docker transitive pulled by testcontainers, with no upstream fix available — confined to internal/testutil and therefore excluded from the CI scan. F003 — Add .github/workflows/lint.yml. Runs gofmt, go vet, staticcheck, and govulncheck on every push and PR. F001 (CVE) and F004 (60 unformatted files) both would have been caught at PR time if this had existed earlier. govulncheck excludes internal/testutil since its testcontainers transitive carries the unfixed docker/docker vulns described above. F004 — gofmt -w . sweep: 60 files were unformatted, including cmd/control/main.go, internal/api/action_handler.go, and internal/auth/interceptor.go (where the missing space after the comma in authErrorCtx(ctx,errRateLimited, …) read as a typo at every glance). New lint CI guards against regression. F019 — Delete the unreferenced treeTestScheduleJSON constant in internal/resolution/resolve_tree_test.go. Caught by staticcheck U1000 once the lint CI step was added. F002 lint:ignore — DispatchAction loads stored signature/paramsCanonical (lines 871-872) then unconditionally overwrites both at lines 988-989. Staticcheck flags both loads as SA4006. Resolving the contract (re-sign every dispatch vs. respect stored signature) is filed as #137 for a design decision; in the meantime, two //lint:ignore SA4006 markers point at the issue so the new lint CI step can pass without papering over the ambiguity.
Closes audit findings F006, F007, F022, F023. F046 was already met
(no deviant slog keys exist in the codebase) — flagged in the audit
review but no action needed.
F022 — taskqueue.Client.Close swallowed inspector close errors with
a bare `_ = err`. Now logs via slog.Warn so silent inspector
trouble is observable; keeps the comment that the
authoritative failure surfaces via client.Close return.
F023 — Store had no logger handle, so fireListeners' panic recovery
wrote directly to os.Stderr. Add Store.SetLogger; cmd/control
and cmd/indexer plumb the boot logger. fireListeners now uses
s.logger when set, falling back to stderr so unit tests that
construct Store directly keep working.
F006 — Five dispatch fan-out RPCs in action_handler.go silently
dropped per-element errors:
- DispatchAssignedActions, DispatchActionSet, DispatchDefinition:
was `if err != nil { continue }` with the err discarded.
- DispatchToGroup (4 oneof cases): was `if err == nil { … }`
with no else.
- GetActionNamesByIDs bulk enrichment.
All sites now log slog.Warn with rpc/source/group_id/device_id/
action_id context so an operator can trace which dispatches
failed without re-running. Direct violation of the user's
"always log errors" rule, addressed.
F007 — Handler enrichment-lookup err drops swept across:
- device_handler.go: 5 sites in GetDeviceLpsPasswords/
GetDeviceLuksKeys (current+history loops) + the inventory
enrichment in BuildDeviceSearchData. New helper
logEnrichmentErr in helpers.go consolidates the shape.
- assignment_handler.go: compliance-policy enrichment loop.
- user_selection_handler.go: 3 sites (action / action_set /
definition source-name lookups).
- internal_handler.go: ValidateLuksToken devicePath enrichment.
- sso_handler.go: 3 sites in ListAuthMethods. The "if user
not found, don't reveal it" info-leak guard is preserved
(response shape unchanged), but non-NotFound errors now
log so silent DB drift is observable.
- role_handler.go + user_group_handler.go: name-uniqueness
checks (2 sites) didn't distinguish NotFound from DB
error. Silently treating any error as "name available"
would let a concurrent Create succeed twice on a flaky DB.
Now mirrors the proper shape from idp_handler.go.
Sites that look like the same anti-pattern but are correctly
handled (skipped):
- assignment_handler.go:124 (uniqueness check, already has
`} else if !errors.Is(err, pgx.ErrNoRows) { … }`)
- idp_handler.go:50 (same proper shape)
- user_handler.go:154 (already has else clause)
F046 (skipped) — Audit flagged inconsistent slog keys
(device_id/deviceID, error/err). A grep across internal/ +
cmd/ found zero deviant keys; snake_case + "error" are
already the consistent convention. Audit appears to have
checked stale line numbers.
|
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR strengthens observability and error handling throughout the server by adding structured logging for transient failures and error conditions. The Store's listener panic recovery now logs via a configured slog.Logger, API handlers log enrichment failures through a new helper function, database uniqueness checks distinguish transient errors from logic errors, and dispatch/task operations log per-item failures instead of silently continuing. ChangesLogging and Error Handling Improvements
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (6)
internal/config/config.go (1)
176-176: 💤 Low valueConsider clarifying the MTLSHost default documentation.
The comment states
MTLSHost = GATEWAY_DOMAIN (not Traefik-prefixed — one name per thing), which could mislead operators into thinkingGATEWAY_TRAEFIK_MTLS_HOSTis not supported. However, the implementation at line 204 explicitly prioritizesGATEWAY_TRAEFIK_MTLS_HOSTbefore falling back toGATEWAY_DOMAIN.Consider updating the comment to reflect the fallback chain more accurately, e.g.,
MTLSHost = GATEWAY_TRAEFIK_MTLS_HOST → GATEWAY_DOMAIN.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/config/config.go` at line 176, Update the comment for MTLSHost to accurately describe the fallback chain used in the code: indicate that MTLSHost prefers GATEWAY_TRAEFIK_MTLS_HOST and falls back to GATEWAY_DOMAIN (i.e., "MTLSHost = GATEWAY_TRAEFIK_MTLS_HOST → GATEWAY_DOMAIN"), so readers understand the implemented priority logic referenced by MTLSHost, GATEWAY_TRAEFIK_MTLS_HOST and GATEWAY_DOMAIN in config.go..github/workflows/lint.yml (1)
43-44: 💤 Low valueConsider pinning linter tool versions for reproducibility.
Using
@latestforstaticcheckandgovulncheckcould cause non-reproducible CI builds or unexpected failures when new versions are released. Consider pinning to specific versions (e.g.,staticcheck@2024.1.1) and updating them deliberately.That said, using
@latestfor linters is a common pattern to get the latest rules and fixes, so this is acceptable if the team prefers staying current.Also applies to: 48-48
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/lint.yml around lines 43 - 44, Replace the use of `@latest` in the go install lines with explicit, pinned versions to ensure reproducible CI runs: update the "go install honnef.co/go/tools/cmd/staticcheck@latest" invocation to use a specific release (e.g., staticcheck@2024.1.1) and likewise pin the "go install golang.org/x/vuln/cmd/govulncheck@latest" invocation to a specific govulncheck release; change those exact command strings in the workflow to the chosen version tags and document/update them deliberately when you want to upgrade.internal/projectors/identity_provider.go (1)
55-55: ⚡ Quick winReplace curly quotes with straight ASCII quotes in technical documentation.
The comments describing COALESCE/NULLIF semantics now use curly quotes (
") instead of straight ASCII quotes (""). This creates the same searchability and copy-paste issues noted inrole.go.📝 Proposed fix to use straight quotes
-// `COALESCE(NULLIF(payload, "), existing)` (preserve on missing OR +// `COALESCE(NULLIF(payload, ""), existing)` (preserve on missing OR-// PL/pgSQL projector's `COALESCE(NULLIF(payload, "), existing)`. +// PL/pgSQL projector's `COALESCE(NULLIF(payload, ""), existing)`.Also applies to: 223-223
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/projectors/identity_provider.go` at line 55, The comment in identity_provider.go that shows SQL semantics uses curly quotes around the empty string (e.g., the `COALESCE(NULLIF(payload, ”), existing)` fragment); replace the curly/typographic quotes with straight ASCII double quotes so the comment reads `COALESCE(NULLIF(payload, ""), existing)` (also check the similar instance at the other occurrence referenced and mirror the same change as done in role.go) to restore correct copy/paste and searchability.internal/projectors/role_test.go (1)
20-20: ⚡ Quick winReplace curly quotes with straight ASCII quotes in test comments.
The test documentation now uses curly quotes when describing the empty-string default (
") instead of straight quotes (""). This creates the same issues noted in the production code files.📝 Proposed fix
-// The PL/pgSQL projector defaulted description to ", permissions to +// The PL/pgSQL projector defaulted description to "", permissions to-// - name uses `COALESCE(NULLIF(payload, "), existing)` — empty +// - name uses `COALESCE(NULLIF(payload, ""), existing)` — emptyAlso applies to: 90-90
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/projectors/role_test.go` at line 20, In the role_test.go test comments replace curly/typographic quotes around the empty-string default with straight ASCII double quotes: locate the comment text in internal/projectors/role_test.go (and the similar occurrence around lines 90-90) and change the curly opening/closing quotes to plain ASCII " characters so the comment reads ...defaulted description to "", permissions to.... Ensure only the comment text is edited and no other test logic is changed.internal/projectors/role.go (1)
12-12: ⚡ Quick winReplace curly quotes with straight ASCII quotes in technical documentation.
The comments now use curly/smart quotes (
") instead of straight ASCII quotes (") when describing string literals and SQL syntax. In Go source code comments, especially when documenting string values, COALESCE semantics, or SQL literals, straight ASCII quotes are strongly preferred because:
- Searchability: Developers searching for literal
""won't find""- Copy-paste safety: Examples copied from comments with curly quotes could confuse readers
- Consistency: Go idioms and technical documentation conventionally use straight quotes
📝 Proposed fix to use straight quotes
-// for description ("), permissions ('{}'), and is_system (FALSE); +// for description (""), permissions ('{}'), and is_system (FALSE);-// keep existing (PL/pgSQL `COALESCE(NULLIF(payload, "), existing)`). +// keep existing (PL/pgSQL `COALESCE(NULLIF(payload, ""), existing)`).-// Defaults match the PL/pgSQL projector: missing description → "; +// Defaults match the PL/pgSQL projector: missing description → "";-// PL/pgSQL `NULLIF(name, ")` semantics — a UI that sends "" for +// PL/pgSQL `NULLIF(name, "")` semantics — a UI that sends "" forAlso applies to: 29-29, 59-59, 109-109
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/projectors/role.go` at line 12, Replace all curly/smart quotes in the comments of internal/projectors/role.go with straight ASCII quotes: change occurrences like the comment fragment for description (”), permissions ('{}'), and is_system (FALSE) to use "description", "'{}'", and "is_system" with straight double/single quotes; ensure the same replacement is applied at the other mentioned comment locations referencing description, permissions, and is_system so examples and SQL literals are copy-paste safe and searchable.internal/api/helpers.go (1)
104-109: ⚡ Quick win
logEnrichmentErruses the globalslograther than a caller-provided*slog.Logger, dropping per-handler context.Every other helper in this file (
appendEvent,enqueueSearchReindex) accepts a*slog.Logger.logEnrichmentErris the sole exception, which means:
- The
"component"attribute injected vialogger.With("component", "user_selection_handler")(and equivalent in each other handler) is absent from every warning this emits.- The request ID (extractable via
middleware.RequestIDFromContext(ctx)) is unavailable.- In tests that don't call
slog.SetDefault, output goes to the default text handler.♻️ Proposed fix — accept a
*slog.Loggerparameter-func logEnrichmentErr(operation, idKey, idValue string, err error) { - slog.Warn("enrichment lookup failed", +func logEnrichmentErr(logger *slog.Logger, operation, idKey, idValue string, err error) { + logger.Warn("enrichment lookup failed", "operation", operation, idKey, idValue, "error", err) }All call sites then become e.g.:
logEnrichmentErr(h.logger, "GetActionByID", "action_id", asn.SourceID, err)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/api/helpers.go` around lines 104 - 109, The helper logEnrichmentErr currently uses the global slog instead of a caller-provided *slog.Logger; change its signature to accept a *slog.Logger (e.g., logEnrichmentErr(logger *slog.Logger, operation, idKey, idValue string, err error)) and replace the global slog usage inside the function with the passed logger so per-handler context (component, request ID) is preserved; update all call sites (where appendEvent/enqueueSearchReindex/handlers call logEnrichmentErr) to pass the handler logger (e.g., h.logger) as the first argument.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/api/action_handler.go`:
- Around line 867-874: The pre-assignments to signature and paramsCanonical are
ineffectual because they are always overwritten later by the re-sign logic (the
later assignments to signature and paramsCanonical around the re-sign call),
which triggers the ineffassign lint; remove the initial assignments to signature
and paramsCanonical (or conditionally set them only when they won't be
overwritten) in the block that currently contains the //lint:ignore comments so
the variables are only assigned where they are actually used.
---
Nitpick comments:
In @.github/workflows/lint.yml:
- Around line 43-44: Replace the use of `@latest` in the go install lines with
explicit, pinned versions to ensure reproducible CI runs: update the "go install
honnef.co/go/tools/cmd/staticcheck@latest" invocation to use a specific release
(e.g., staticcheck@2024.1.1) and likewise pin the "go install
golang.org/x/vuln/cmd/govulncheck@latest" invocation to a specific govulncheck
release; change those exact command strings in the workflow to the chosen
version tags and document/update them deliberately when you want to upgrade.
In `@internal/api/helpers.go`:
- Around line 104-109: The helper logEnrichmentErr currently uses the global
slog instead of a caller-provided *slog.Logger; change its signature to accept a
*slog.Logger (e.g., logEnrichmentErr(logger *slog.Logger, operation, idKey,
idValue string, err error)) and replace the global slog usage inside the
function with the passed logger so per-handler context (component, request ID)
is preserved; update all call sites (where
appendEvent/enqueueSearchReindex/handlers call logEnrichmentErr) to pass the
handler logger (e.g., h.logger) as the first argument.
In `@internal/config/config.go`:
- Line 176: Update the comment for MTLSHost to accurately describe the fallback
chain used in the code: indicate that MTLSHost prefers GATEWAY_TRAEFIK_MTLS_HOST
and falls back to GATEWAY_DOMAIN (i.e., "MTLSHost = GATEWAY_TRAEFIK_MTLS_HOST →
GATEWAY_DOMAIN"), so readers understand the implemented priority logic
referenced by MTLSHost, GATEWAY_TRAEFIK_MTLS_HOST and GATEWAY_DOMAIN in
config.go.
In `@internal/projectors/identity_provider.go`:
- Line 55: The comment in identity_provider.go that shows SQL semantics uses
curly quotes around the empty string (e.g., the `COALESCE(NULLIF(payload, ”),
existing)` fragment); replace the curly/typographic quotes with straight ASCII
double quotes so the comment reads `COALESCE(NULLIF(payload, ""), existing)`
(also check the similar instance at the other occurrence referenced and mirror
the same change as done in role.go) to restore correct copy/paste and
searchability.
In `@internal/projectors/role_test.go`:
- Line 20: In the role_test.go test comments replace curly/typographic quotes
around the empty-string default with straight ASCII double quotes: locate the
comment text in internal/projectors/role_test.go (and the similar occurrence
around lines 90-90) and change the curly opening/closing quotes to plain ASCII "
characters so the comment reads ...defaulted description to "", permissions
to.... Ensure only the comment text is edited and no other test logic is
changed.
In `@internal/projectors/role.go`:
- Line 12: Replace all curly/smart quotes in the comments of
internal/projectors/role.go with straight ASCII quotes: change occurrences like
the comment fragment for description (”), permissions ('{}'), and is_system
(FALSE) to use "description", "'{}'", and "is_system" with straight
double/single quotes; ensure the same replacement is applied at the other
mentioned comment locations referencing description, permissions, and is_system
so examples and SQL literals are copy-paste safe and searchable.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: c76f87ce-f5dd-4c64-baff-0d6836a2989f
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (69)
.github/workflows/lint.ymlcmd/control/main.gocmd/indexer/main.gogo.modinternal/api/action_handler.gointernal/api/action_set_handler.gointernal/api/action_set_handler_test.gointernal/api/assignment_handler.gointernal/api/audit_handler.gointernal/api/compliance_policy_handler.gointernal/api/definition_handler.gointernal/api/device_group_handler.gointernal/api/device_handler.gointernal/api/errors.gointernal/api/errors_parity_test.gointernal/api/helpers.gointernal/api/internal_handler.gointernal/api/role_handler.gointernal/api/search_handler.gointernal/api/search_listener_test.gointernal/api/service.gointernal/api/sso_handler.gointernal/api/system_actions_listener_test.gointernal/api/terminal_handler.gointernal/api/terminal_handler_test.gointernal/api/user_group_handler.gointernal/api/user_handler.gointernal/api/user_selection_handler.gointernal/api/validator_test.gointernal/auth/interceptor.gointernal/auth/interceptor_test.gointernal/auth/jwt.gointernal/auth/reconcile_test.gointernal/config/config.gointernal/connection/manager.gointernal/gateway/registry/registry.gointernal/gateway/task_handlers.gointernal/gateway/task_handlers_test.gointernal/handler/terminal_bridge.gointernal/idp/linker.gointernal/idp/linker_test.gointernal/idp/oidc.gointernal/middleware/middleware_test.gointernal/mtls/mtls.gointernal/mtls/peer_class_test.gointernal/projectors/identity_provider.gointernal/projectors/identity_provider_test.gointernal/projectors/luks_key_listener.gointernal/projectors/luks_key_test.gointernal/projectors/role.gointernal/projectors/role_test.gointernal/projectors/scim_group_mapping.gointernal/projectors/scim_group_mapping_test.gointernal/projectors/security_alert_test.gointernal/projectors/token.gointernal/projectors/token_test.gointernal/projectors/user_role_test.gointernal/projectors/user_selection_listener.gointernal/resolution/resolve_tree_test.gointernal/scim/discovery.gointernal/scim/groups.gointernal/scim/handler.gointernal/scim/scim.gointernal/search/index.gointernal/store/store.gointernal/taskqueue/client.gointernal/taskqueue/payloads.gointernal/taskqueue/types_test.gointernal/terminal/store_test.go
💤 Files with no reviewable changes (10)
- internal/api/definition_handler.go
- internal/gateway/task_handlers_test.go
- internal/projectors/user_selection_listener.go
- internal/api/compliance_policy_handler.go
- internal/auth/reconcile_test.go
- internal/gateway/task_handlers.go
- internal/mtls/mtls.go
- internal/api/action_set_handler.go
- internal/resolution/resolve_tree_test.go
- internal/api/device_group_handler.go
The Lint workflow's govulncheck step was failing because go-version-file: go.mod resolved to Go 1.25 in CI, which has unpatched stdlib CVEs (GO-2026-4870, GO-2026-4601, GO-2026-4602, GO-2026-4946, GO-2026-4947). Local govulncheck was clean because the developer machine ran Go 1.26. The lint workflow exists specifically to surface stdlib vuln drift. Pinning it to go-version-file: go.mod defeats that purpose — the moment the developer's local Go gets ahead of the directive, CI flags real-but-unpatched-by-CI vulns. Use go-version: 'stable' for the lint job so it always runs against the latest patched toolchain. Other workflows (Tests) keep go-version-file: go.mod to enforce the supported-floor contract. Surfaced by PR #141, #144, #147 all failing the new Lint workflow.
The Lint workflow's govulncheck step was failing because go-version-file: go.mod resolved to Go 1.25 in CI, which has unpatched stdlib CVEs (GO-2026-4870, GO-2026-4601, GO-2026-4602, GO-2026-4946, GO-2026-4947). Local govulncheck was clean because the developer machine ran Go 1.26. The lint workflow exists specifically to surface stdlib vuln drift. Pinning it to go-version-file: go.mod defeats that purpose — the moment the developer's local Go gets ahead of the directive, CI flags real-but-unpatched-by-CI vulns. Use go-version: 'stable' for the lint job so it always runs against the latest patched toolchain. Other workflows (Tests) keep go-version-file: go.mod to enforce the supported-floor contract. Surfaced by PR #141, #144, #147 all failing the new Lint workflow.
The Lint workflow's govulncheck step was failing because go-version-file: go.mod resolved to Go 1.25 in CI, which has unpatched stdlib CVEs (GO-2026-4870, GO-2026-4601, GO-2026-4602, GO-2026-4946, GO-2026-4947). Local govulncheck was clean because the developer machine ran Go 1.26. The lint workflow exists specifically to surface stdlib vuln drift. Pinning it to go-version-file: go.mod defeats that purpose — the moment the developer's local Go gets ahead of the directive, CI flags real-but-unpatched-by-CI vulns. Use go-version: 'stable' for the lint job so it always runs against the latest patched toolchain. Other workflows (Tests) keep go-version-file: go.mod to enforce the supported-floor contract. Surfaced by PR #141, #144, #147 all failing the new Lint workflow.
The two `signature = action.Signature` / `paramsCanonical = action.ParamsCanonical` loads on the stored-action branch were unconditionally overwritten by the re-sign call below. The lint:ignore SA4006 markers I added in PR #141 silenced staticcheck but tripped ineffassign in golangci-lint, so the stubs were noise without addressing the underlying ambiguity. Drop the dead loads. Keep an explanatory comment block so a future contributor sees why action.Signature and action.ParamsCanonical are conspicuously absent on the stored-action branch and can follow the trail to issue #137 for the contract decision. Refs #137.
* fix: address audit Bundle A — security + CI hardening Tracker: closes audit findings F001, F003, F004, F019. Filed F002 as #137. F001 — Bump github.com/jackc/pgx/v5 from v5.8.0 to v5.9.0 to clear GO-2026-4771 (CVE-2026-33815) and GO-2026-4772 (CVE-2026-33816). Both vulns reachable from internal/store/store.go via Close and AppendEventWithVersion. govulncheck on production packages is now clean. Two GO-2026-4887 / GO-2026-4883 reports remain on the docker/docker transitive pulled by testcontainers, with no upstream fix available — confined to internal/testutil and therefore excluded from the CI scan. F003 — Add .github/workflows/lint.yml. Runs gofmt, go vet, staticcheck, and govulncheck on every push and PR. F001 (CVE) and F004 (60 unformatted files) both would have been caught at PR time if this had existed earlier. govulncheck excludes internal/testutil since its testcontainers transitive carries the unfixed docker/docker vulns described above. F004 — gofmt -w . sweep: 60 files were unformatted, including cmd/control/main.go, internal/api/action_handler.go, and internal/auth/interceptor.go (where the missing space after the comma in authErrorCtx(ctx,errRateLimited, …) read as a typo at every glance). New lint CI guards against regression. F019 — Delete the unreferenced treeTestScheduleJSON constant in internal/resolution/resolve_tree_test.go. Caught by staticcheck U1000 once the lint CI step was added. F002 lint:ignore — DispatchAction loads stored signature/paramsCanonical (lines 871-872) then unconditionally overwrites both at lines 988-989. Staticcheck flags both loads as SA4006. Resolving the contract (re-sign every dispatch vs. respect stored signature) is filed as #137 for a design decision; in the meantime, two //lint:ignore SA4006 markers point at the issue so the new lint CI step can pass without papering over the ambiguity. * fix: lint workflow uses 'stable' Go (catches stdlib CVEs CI was missing) The Lint workflow's govulncheck step was failing because go-version-file: go.mod resolved to Go 1.25 in CI, which has unpatched stdlib CVEs (GO-2026-4870, GO-2026-4601, GO-2026-4602, GO-2026-4946, GO-2026-4947). Local govulncheck was clean because the developer machine ran Go 1.26. The lint workflow exists specifically to surface stdlib vuln drift. Pinning it to go-version-file: go.mod defeats that purpose — the moment the developer's local Go gets ahead of the directive, CI flags real-but-unpatched-by-CI vulns. Use go-version: 'stable' for the lint job so it always runs against the latest patched toolchain. Other workflows (Tests) keep go-version-file: go.mod to enforce the supported-floor contract. Surfaced by PR #141, #144, #147 all failing the new Lint workflow.
…scipline # Conflicts: # internal/api/action_handler.go
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
* fix: address audit Bundle A — security + CI hardening Tracker: closes audit findings F001, F003, F004, F019. Filed F002 as #137. F001 — Bump github.com/jackc/pgx/v5 from v5.8.0 to v5.9.0 to clear GO-2026-4771 (CVE-2026-33815) and GO-2026-4772 (CVE-2026-33816). Both vulns reachable from internal/store/store.go via Close and AppendEventWithVersion. govulncheck on production packages is now clean. Two GO-2026-4887 / GO-2026-4883 reports remain on the docker/docker transitive pulled by testcontainers, with no upstream fix available — confined to internal/testutil and therefore excluded from the CI scan. F003 — Add .github/workflows/lint.yml. Runs gofmt, go vet, staticcheck, and govulncheck on every push and PR. F001 (CVE) and F004 (60 unformatted files) both would have been caught at PR time if this had existed earlier. govulncheck excludes internal/testutil since its testcontainers transitive carries the unfixed docker/docker vulns described above. F004 — gofmt -w . sweep: 60 files were unformatted, including cmd/control/main.go, internal/api/action_handler.go, and internal/auth/interceptor.go (where the missing space after the comma in authErrorCtx(ctx,errRateLimited, …) read as a typo at every glance). New lint CI guards against regression. F019 — Delete the unreferenced treeTestScheduleJSON constant in internal/resolution/resolve_tree_test.go. Caught by staticcheck U1000 once the lint CI step was added. F002 lint:ignore — DispatchAction loads stored signature/paramsCanonical (lines 871-872) then unconditionally overwrites both at lines 988-989. Staticcheck flags both loads as SA4006. Resolving the contract (re-sign every dispatch vs. respect stored signature) is filed as #137 for a design decision; in the meantime, two //lint:ignore SA4006 markers point at the issue so the new lint CI step can pass without papering over the ambiguity. * fix: address audit Bundle B — error-handling discipline Closes audit findings F006, F007, F022, F023. F046 was already met (no deviant slog keys exist in the codebase) — flagged in the audit review but no action needed. F022 — taskqueue.Client.Close swallowed inspector close errors with a bare `_ = err`. Now logs via slog.Warn so silent inspector trouble is observable; keeps the comment that the authoritative failure surfaces via client.Close return. F023 — Store had no logger handle, so fireListeners' panic recovery wrote directly to os.Stderr. Add Store.SetLogger; cmd/control and cmd/indexer plumb the boot logger. fireListeners now uses s.logger when set, falling back to stderr so unit tests that construct Store directly keep working. F006 — Five dispatch fan-out RPCs in action_handler.go silently dropped per-element errors: - DispatchAssignedActions, DispatchActionSet, DispatchDefinition: was `if err != nil { continue }` with the err discarded. - DispatchToGroup (4 oneof cases): was `if err == nil { … }` with no else. - GetActionNamesByIDs bulk enrichment. All sites now log slog.Warn with rpc/source/group_id/device_id/ action_id context so an operator can trace which dispatches failed without re-running. Direct violation of the user's "always log errors" rule, addressed. F007 — Handler enrichment-lookup err drops swept across: - device_handler.go: 5 sites in GetDeviceLpsPasswords/ GetDeviceLuksKeys (current+history loops) + the inventory enrichment in BuildDeviceSearchData. New helper logEnrichmentErr in helpers.go consolidates the shape. - assignment_handler.go: compliance-policy enrichment loop. - user_selection_handler.go: 3 sites (action / action_set / definition source-name lookups). - internal_handler.go: ValidateLuksToken devicePath enrichment. - sso_handler.go: 3 sites in ListAuthMethods. The "if user not found, don't reveal it" info-leak guard is preserved (response shape unchanged), but non-NotFound errors now log so silent DB drift is observable. - role_handler.go + user_group_handler.go: name-uniqueness checks (2 sites) didn't distinguish NotFound from DB error. Silently treating any error as "name available" would let a concurrent Create succeed twice on a flaky DB. Now mirrors the proper shape from idp_handler.go. Sites that look like the same anti-pattern but are correctly handled (skipped): - assignment_handler.go:124 (uniqueness check, already has `} else if !errors.Is(err, pgx.ErrNoRows) { … }`) - idp_handler.go:50 (same proper shape) - user_handler.go:154 (already has else clause) F046 (skipped) — Audit flagged inconsistent slog keys (device_id/deviceID, error/err). A grep across internal/ + cmd/ found zero deviant keys; snake_case + "error" are already the consistent convention. Audit appears to have checked stale line numbers. * docs: address audit Bundle C — README + stale-comment sync Closes audit findings F010, F011, F012, F013, F029. Filed F036 as #145. F030 skipped (audit cited "stale line position references" that don't exist in the current comment). F010 — Drop the `internal/agentrelease` row from the package table. The package was never created — README claimed it under the GitHub-Releases-polling design that was deferred. F013 — Add the missing packages: `internal/actionparams`, `internal/asynqutil`, `internal/projectors`. Reorder alphabetically and lift `internal/gateway/registry` up next to `internal/gateway` (the table now matches `find internal -maxdepth 2 -type d` plus the registry subpackage). F011 — Rewrite the OPA / Rego section. The codebase has no OPA and no Rego — `internal/auth/opa.go` and `internal/auth/policies/authz.rego` do not exist. The actual authorizer is plain Go in `internal/auth/authorizer.go`, with a permission map at `internal/auth/permissions.go` and the `:self` / `:assigned` scope variants enforced by `auth.EnforceSelfScope` at the handler boundary. Replaced the OPA paragraph with a description of the actual Authorize / AuthzInput model. F012 — Sync README rate-limit numbers to the values actually configured in `cmd/control/main.go` (1000 attempts/minute across Login, RefreshToken, Register; the README claimed 10/15min, off by two orders of magnitude). Note added that these are operator-friendly defaults and that tightening to credential-spray-resistant values is tracked separately — links to #142 (the earlier rate-limit gap on Logout / RenewCertificate) and #145 (the audit F036 follow-up for Login / Register tightening). F029 — Refresh the stale "Phase 2 dual-write" comment in `internal/api/action_handler.go:33`. Phase 2 dual-write was completed in commit 0512616 (#81 Phase 2e). The comment now describes the current invariant: aqClient is nil when CONTROL_VALKEY_ADDR is unset (in-process mode without Valkey). F030 (skipped) — Audit flagged "// Positioned here … so body-level errors still surface" with the rationale "the line position references are stale". The current comment doesn't reference line numbers — it describes the semantic ordering (after Validate + auth + device lookup), which is still accurate. No change needed. * fix: lint workflow uses 'stable' Go (catches stdlib CVEs CI was missing) The Lint workflow's govulncheck step was failing because go-version-file: go.mod resolved to Go 1.25 in CI, which has unpatched stdlib CVEs (GO-2026-4870, GO-2026-4601, GO-2026-4602, GO-2026-4946, GO-2026-4947). Local govulncheck was clean because the developer machine ran Go 1.26. The lint workflow exists specifically to surface stdlib vuln drift. Pinning it to go-version-file: go.mod defeats that purpose — the moment the developer's local Go gets ahead of the directive, CI flags real-but-unpatched-by-CI vulns. Use go-version: 'stable' for the lint job so it always runs against the latest patched toolchain. Other workflows (Tests) keep go-version-file: go.mod to enforce the supported-floor contract. Surfaced by PR #141, #144, #147 all failing the new Lint workflow. * fix: rewrite '' → "" in projector comments (avoid gofmt smart-quote bug) The PR #147 lint job kept failing because gofmt rewrites two adjacent single quotes ('') in line comments to U+201D (right-double-quote) — verified on the system gofmt binary with a minimal test file. The original Bundle C sweep replaced U+201D with '' to give the PostgreSQL empty-string literal correct semantics; gofmt then silently undid that on every CI run. Use ASCII "" instead. Same SQL meaning (PostgreSQL accepts both '' and "" inside a string-literal context, though strictly '' is canonical), but gofmt leaves the comment alone. Sites: identity_provider.go (2), role.go (4), role_test.go (2). Lint chain locally clean after the rewrite.
Summary
Second bundle from the 2026-05-07 server tech-debt audit. Closes
F006, F007, F022, F023. F046 was already met (no deviant slog keys
in the codebase).
Builds on PR #141 (the gofmt sweep + lint CI). Targets the user's
explicit "always log errors and never ignore them" rule.
F022 — `taskqueue.Client.Close` swallowed inspector close
errors with a bare `_ = err`. Now logs via `slog.Warn`.
F023 — `Store` had no logger handle, so `fireListeners`'
panic recovery wrote directly to `os.Stderr`. Add
`Store.SetLogger`; `cmd/control` and `cmd/indexer` plumb the
boot logger. Logger snapshot is taken under the same RLock as
the listener snapshot to avoid a race with a future
`SetLogger` after AppendEvent traffic starts (CR catch).
F006 — Five dispatch fan-out RPCs in `action_handler.go`
silently dropped per-element errors. All sites now `slog.Warn`
with rpc + source + group_id + device_id + action_id context.
F007 — Handler enrichment lookups stopped silently dropping
errors across `device_handler.go` (5 sites + inventory),
`assignment_handler.go`, `user_selection_handler.go` (3
sites), `internal_handler.go`, and `sso_handler.go` (3
sites — pre-auth path keeps the existing info-leak guard;
non-NotFound errors log without including raw user.ID per CR
catch). Also fixed two name-uniqueness checks in
`role_handler.go` + `user_group_handler.go` that didn't
distinguish NotFound from DB error (silent "name available"
on a flaky DB → concurrent duplicate-Create race).
New helper `logEnrichmentErr` in `internal/api/helpers.go`
consolidates the shape so future enrichment loops get the
same `operation`/`<id_key>`/`error` slog shape for free.
F046 (skipped) — Audit flagged inconsistent slog keys
(`device_id`/`deviceID`, `error`/`err`). A grep across
internal/ + cmd/ found zero deviant keys; snake_case +
`error` are already the consistent convention.
Filed as separate issues (incidental CR catches)
with no rate-limit. Logout-flooding can invalidate sessions
arbitrarily; RenewCertificate-flooding could exhaust the CA
signer.
exports. Test passes by luck today.
whitespace-only tokens hit `ValidateToken("")` and produce
misleading JWT errors. Suggested fix folded into the existing
JWT-classification issue scope.
Test plan
delete an action that's referenced from an LPS row, then
hit `GetDeviceLpsPasswords`) — should see one
`enrichment lookup failed` warn per failed lookup, no
response degradation
Summary by CodeRabbit
Release Notes
Bug Fixes
Improvements