Skip to content

ci: add blocking SAST gate (gosec, govulncheck, semgrep)#197

Merged
Joey0538 merged 7 commits into
mainfrom
claude/setup-sast-compliance-y2ruq
May 19, 2026
Merged

ci: add blocking SAST gate (gosec, govulncheck, semgrep)#197
Joey0538 merged 7 commits into
mainfrom
claude/setup-sast-compliance-y2ruq

Conversation

@Joey0538
Copy link
Copy Markdown
Collaborator

@Joey0538 Joey0538 commented May 19, 2026

Summary

Sets up SAST compliance enforced in CI (not a bypassable pre-commit hook), gating on medium+ severity. Adds a sast job to ci.yml plus make tools / make sast so local and CI runs are byte-identical.

What this does

  • .github/workflows/ci.yml — new sast job, parallel with lint/test (needs: prewarm). Runs make tools then make sast; fails the run on any medium+ gosec finding, any reachable govulncheck vuln, or any semgrep WARNING/ERROR.
  • Makefilemake tools (pinned golangci-lint/gosec/govulncheck via go install, semgrep via pipx), make sast (runs all three, aggregates exit code), and make sast-gosec / sast-vuln / sast-semgrep. gosec scoped to shipped product code (-exclude-dir=tools, -tests=false, -exclude-generated).
  • Clean baseline — intentional/reviewed findings annotated with justified gosec-native #nosec comments so the gate catches only new issues:
    • pkg/oidc/oidc.go, pkg/searchengine/factory.go — the two opt-in InsecureSkipVerify transports (already //nolint:gosec for golangci; standalone gosec needs its own #nosec).
    • history-service/internal/cassrepo/walker.go — three G115 false positives (lossless int64↔uint64 framing round-trip + a uint16(len) already bounded by the guard above it).
  • CLAUDE.mdmake tools/make sast in the command table + a SAST guardrail note (documents that //nolint:gosec does not suppress standalone gosec).

Verification

  • make sast-gosec — clean (0 issues; Nosec: 2 confirms G402 suppressions work)
  • make lint — 0 issues (golangci still honors the existing //nolint:gosec)
  • ✅ Workflow YAML parses; make -n tools/make -n sast dry-run clean
  • ⚠️ govulncheck/semgrep not live-validated in the authoring sandbox (egress to vuln.go.dev blocked; container Python broken). Both run on GitHub-hosted runners — this PR's own CI run is the end-to-end verification.

All source edits are comment-only — zero behavior change.

Follow-up (needs a human)

To make this actually block merges, add sast as a required status check in branch-protection for main.

Test plan

  • CI sast job goes green on this PR (validates gosec + govulncheck + semgrep end-to-end on a real runner)
  • lint / test / test-integration unaffected
  • Locally: make tools && make sast passes on a machine with vuln.go.dev reachable

https://claude.ai/code/session_01ERurgA9KHk8wMUJi2dtYxf


Generated by Claude Code

Summary by CodeRabbit

  • Chores

    • Added pinned developer/security tooling and Make targets to install and run SAST locally.
    • Enabled SAST as a blocking CI gate to enforce medium+ policy and aggregate scan results.
    • Upgraded the Go toolchain across builds and pipelines to a newer patch release.
  • Documentation

    • Clarified SAST usage, suppression guidance, and local scan instructions.

Review Change Stack

Enforce SAST compliance in CI rather than via a bypassable pre-commit
hook. Adds a `sast` job to ci.yml (parallel with lint/test, fail on
medium+) plus `make tools`/`make sast` so local and CI runs are
byte-identical. Establishes a clean baseline by annotating intentional,
reviewed findings with justified gosec-native `#nosec` comments
(InsecureSkipVerify opt-ins; lossless serialization conversions) and
scoping the gosec scan to shipped product code (excludes dev/ops
utilities under tools/).

https://claude.ai/code/session_01ERurgA9KHk8wMUJi2dtYxf
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

📝 Walkthrough

Walkthrough

The PR introduces SAST infrastructure: pinned SAST tool installation and orchestration in the Makefile, a blocking sast CI job that installs and runs the scans, documentation updates for suppression format and local use, inline #nosec annotations in a few files, and a repo-wide Go toolchain version bump to 1.25.10 across go.mod, Docker builder images, and CI pipeline variables.

Changes

SAST Security Scanning

Layer / File(s) Summary
Makefile SAST tool configuration and targets
Makefile
Makefile defines pinned versions for gosec, govulncheck, semgrep, and golangci-lint, computed tool paths and scan flags, a tools target that installs them idempotently (Go tools via go install, semgrep via pipx), and sast orchestration targets that run individual scans while aggregating failures.
CI workflow SAST job
.github/workflows/ci.yml
New sast job in the GitHub Actions workflow checks out the repo, sets up Go, restores build caches, installs pinned SAST tools via make tools, adjusts PATH for semgrep, and runs make sast as a blocking gate.
Documentation and suppression directives
CLAUDE.md
CLAUDE.md documents the new make tools and make sast targets for local use, marks SAST as a blocking CI gate, specifies the gosec-native suppression format/placement, and updates Docker builder image guidance.
Inline security suppression annotations
history-service/internal/cassrepo/walker.go, pkg/oidc/oidc.go, pkg/searchengine/factory.go, pkg/roomkeysender/roomkeysender.go
Adds #nosec directives: G115 for lossless int64↔uint64 cursor encoding/decoding, G402 for opt-in TLS InsecureSkipVerify paths, and G117 to document room-key payload handling.
Go toolchain version updates
go.mod, **/deploy/Dockerfile, **/deploy/azure-pipelines.yml
Bumps the go directive to 1.25.10, updates multiple service Docker builder images to golang:1.25.10-alpine, and updates GO_VERSION variables in several Azure pipeline configs to 1.25.10.
  • Estimated code review effort: 🎯 4 (Complex) | ⏱️ ~45 minutes

  • Possibly related PRs:

    • hmchangw/chat#43: Overlaps on repo-wide Go version bump changes.
    • hmchangw/chat#33: Related to pkg/oidc TLS handling where a #nosec G402 suppression is introduced.
  • Suggested reviewers:

    • mliu33

"I'm a rabbit in the CI glade,
I pin the tools so scans are made,
I hop through docs and Docker tags,
And stash #nosec in tidy flags,
Secure builds bloom where my paws have played."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: adding a blocking SAST gate with three specific static analysis tools (gosec, govulncheck, semgrep) integrated into CI—this is accurate to the primary objective of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/setup-sast-compliance-y2ruq

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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 `@Makefile`:
- Around line 128-130: The Makefile currently lets `make tools` succeed when
semgrep cannot be installed because the `|| echo ...` branch returns success;
update the failing branch so it returns a non-zero exit code (for example
replace the `|| echo ...` fallback with a shell block that prints the message
and exits non‑zero) so the `@command -v pipx ... && pipx install ... || ...`
command will fail the target when pipx/installation is unavailable; modify the
line containing the pipx check/installation (the shell command using `command -v
pipx` and `pipx install --force semgrep==$(SEMGREP_VERSION)`) to use a failing
fallback (e.g., `|| { echo "..."; exit 1; }` or an equivalent that writes to
stderr and returns non‑zero).
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: badc66ae-1e4d-4559-8ea6-be52446edcd5

📥 Commits

Reviewing files that changed from the base of the PR and between 6b0259e and 1a36683.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • CLAUDE.md
  • Makefile
  • history-service/internal/cassrepo/walker.go
  • pkg/oidc/oidc.go
  • pkg/searchengine/factory.go

Comment thread Makefile Outdated
claude added 2 commits May 19, 2026 02:58
The sast job failed at `make tools`: gosec v2.21.4 pins
golang.org/x/tools@v0.25.0, which fails to compile under any Go 1.25.x
("invalid array length -delta * delta"). Bump GOSEC_VERSION to v2.26.1
(Go 1.25-compatible) and pin GOTOOLCHAIN for reproducible tool builds.
v2.26.1's new G117 rule flags the intentional RoomKeyEvent.PrivateKey
serialization in roomkeysender (room-key distribution to the authorized
account over its auth-gated per-user subject) — suppressed with a
justified gosec-native annotation, consistent with the clean baseline.

Also fail `make tools` (instead of silently passing) when semgrep
cannot be installed and is not already on PATH (CodeRabbit review).

https://claude.ai/code/session_01ERurgA9KHk8wMUJi2dtYxf
govulncheck (now running in the SAST gate) flagged reachable known
stdlib advisories in net / net/mail / html/template, fixed in the
May 2026 Go security release. Bump every build/CI pin to 1.25.10:
go.mod go directive, ci.yml GO_VERSION, all service Dockerfiles and
azure-pipelines.yml, and the SAST tool-build toolchain pin. Patches
the actually-shipped binaries, not just the scanner's view.

Validated: gosec builds + make lint + make sast-gosec all green
under go1.25.10.

https://claude.ai/code/session_01ERurgA9KHk8wMUJi2dtYxf
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
auth-service/deploy/Dockerfile (1)

9-12: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Run the runtime container as a non-root user.

The final stage defaults to root, which weakens container isolation if the process is compromised. Add a dedicated unprivileged user/group before ENTRYPOINT.

Suggested hardening diff
 FROM alpine:3.21
 RUN apk add --no-cache ca-certificates
+RUN addgroup -S app && adduser -S -G app app
 COPY --from=builder /auth-service /auth-service
+USER app
 ENTRYPOINT ["/auth-service"]
🤖 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 `@auth-service/deploy/Dockerfile` around lines 9 - 12, The Dockerfile's final
stage runs as root (default) which weakens isolation; update the final stage to
create an unprivileged user/group (e.g., addgroup/adduser or adduser -D), chown
the binary /auth-service to that user/group, switch to that user with the USER
instruction before ENTRYPOINT, and ensure the binary has executable permissions;
reference the Dockerfile's final stage, the /auth-service file and ENTRYPOINT to
locate where to add the group/user creation, chown and USER directives.
mock-user-service/deploy/Dockerfile (1)

13-19: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Run the runtime container as non-root.

Final stage has no USER, so the service runs as root by default. Please drop privileges in the runtime image.

Suggested patch
 FROM alpine:3.21
 
 RUN apk add --no-cache ca-certificates
+RUN addgroup -S app && adduser -S -G app app
 
 COPY --from=builder /mock-user-service /mock-user-service
 
+USER app
 ENTRYPOINT ["/mock-user-service"]
🤖 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 `@mock-user-service/deploy/Dockerfile` around lines 13 - 19, The final
Dockerfile stage runs the binary as root because no USER is set; create a
non-root user and group (e.g., app user), chown the copied binary
(/mock-user-service) to that user, and add a USER instruction before ENTRYPOINT
so the container drops privileges; update the Dockerfile final stage to create
the user, ensure the binary is owned by that user, and use USER <username> prior
to ENTRYPOINT ["/mock-user-service"] to run the service as non-root.
🤖 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.

Outside diff comments:
In `@auth-service/deploy/Dockerfile`:
- Around line 9-12: The Dockerfile's final stage runs as root (default) which
weakens isolation; update the final stage to create an unprivileged user/group
(e.g., addgroup/adduser or adduser -D), chown the binary /auth-service to that
user/group, switch to that user with the USER instruction before ENTRYPOINT, and
ensure the binary has executable permissions; reference the Dockerfile's final
stage, the /auth-service file and ENTRYPOINT to locate where to add the
group/user creation, chown and USER directives.

In `@mock-user-service/deploy/Dockerfile`:
- Around line 13-19: The final Dockerfile stage runs the binary as root because
no USER is set; create a non-root user and group (e.g., app user), chown the
copied binary (/mock-user-service) to that user, and add a USER instruction
before ENTRYPOINT so the container drops privileges; update the Dockerfile final
stage to create the user, ensure the binary is owned by that user, and use USER
<username> prior to ENTRYPOINT ["/mock-user-service"] to run the service as
non-root.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 741cf2b3-f9b2-4128-abfa-4411911014e4

📥 Commits

Reviewing files that changed from the base of the PR and between 1a36683 and 39ff90f.

📒 Files selected for processing (30)
  • .github/workflows/ci.yml
  • CLAUDE.md
  • Makefile
  • auth-service/deploy/Dockerfile
  • auth-service/deploy/azure-pipelines.yml
  • broadcast-worker/deploy/Dockerfile
  • broadcast-worker/deploy/azure-pipelines.yml
  • go.mod
  • history-service/deploy/Dockerfile
  • history-service/deploy/azure-pipelines.yml
  • inbox-worker/deploy/Dockerfile
  • inbox-worker/deploy/azure-pipelines.yml
  • message-gatekeeper/deploy/Dockerfile
  • message-gatekeeper/deploy/azure-pipelines.yml
  • message-worker/deploy/Dockerfile
  • message-worker/deploy/azure-pipelines.yml
  • mock-user-service/deploy/Dockerfile
  • mock-user-service/deploy/azure-pipelines.yml
  • notification-worker/deploy/Dockerfile
  • notification-worker/deploy/azure-pipelines.yml
  • pkg/roomkeysender/roomkeysender.go
  • room-service/deploy/Dockerfile
  • room-service/deploy/azure-pipelines.yml
  • room-worker/deploy/Dockerfile
  • room-worker/deploy/azure-pipelines.yml
  • search-service/deploy/Dockerfile
  • search-sync-worker/deploy/Dockerfile
  • search-sync-worker/deploy/azure-pipelines.yml
  • tools/loadgen/deploy/Dockerfile
  • tools/nats-debug/deploy/Dockerfile
✅ Files skipped from review due to trivial changes (13)
  • message-worker/deploy/azure-pipelines.yml
  • search-service/deploy/Dockerfile
  • tools/nats-debug/deploy/Dockerfile
  • pkg/roomkeysender/roomkeysender.go
  • history-service/deploy/azure-pipelines.yml
  • notification-worker/deploy/azure-pipelines.yml
  • message-gatekeeper/deploy/azure-pipelines.yml
  • inbox-worker/deploy/azure-pipelines.yml
  • auth-service/deploy/azure-pipelines.yml
  • search-sync-worker/deploy/azure-pipelines.yml
  • mock-user-service/deploy/azure-pipelines.yml
  • room-service/deploy/azure-pipelines.yml
  • room-worker/deploy/azure-pipelines.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • CLAUDE.md

claude added 4 commits May 19, 2026 05:51
The sast job failed with only "exit code 2" visible — the failing
scanner and its findings were not inspectable without signing in.
Tee `make sast` to a log and, reusing the repo's test-integration
pattern, write it to the step summary and surface the tail as an
::error:: annotation on failure. Add a per-scanner PASS/FAIL summary
line to the `sast` make target so the failing scanner is unambiguous
in the captured output.

https://claude.ai/code/session_01ERurgA9KHk8wMUJi2dtYxf
CodeRabbit flagged the alpine runtime stages running as root (CWE-250).
Mirror the existing search-service Dockerfile convention
(adduser -D -u 10001 app + USER app before ENTRYPOINT) across the
other 13 service/tool images. search-service already complied and is
left unchanged. Binaries are world-executable so no chown is needed;
only nats-debug exposes a port (8090, unprivileged).

https://claude.ai/code/session_01ERurgA9KHk8wMUJi2dtYxf
Diagnosis from the now self-reporting sast annotation:
gosec=PASS, govulncheck=PASS (Go 1.25.10 cleared the stdlib
advisories), semgrep=FAIL — but a Python crash, not a finding:
"ModuleNotFoundError: No module named 'pkg_resources'". semgrep
1.86.0 imports pkg_resources, which setuptools-less Python 3.12+
on ubuntu-latest no longer provides. Bump semgrep to 1.163.0 and
`pipx inject semgrep setuptools` so pkg_resources is importable
regardless of runner Python.

https://claude.ai/code/session_01ERurgA9KHk8wMUJi2dtYxf
semgrep (now running: gosec=PASS, govulncheck=PASS) flagged one real
blocking finding — missing-ssl-minversion at pkg/oidc/oidc.go: the
tls.Config set InsecureSkipVerify but no MinVersion. Add
MinVersion: tls.VersionTLS12, matching the existing
pkg/searchengine/factory.go convention. A genuine (minor) hardening
gap fixed properly rather than suppressed.

https://claude.ai/code/session_01ERurgA9KHk8wMUJi2dtYxf
Copy link
Copy Markdown
Owner

@hmchangw hmchangw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@Joey0538 Joey0538 merged commit b4bf819 into main May 19, 2026
24 checks passed
@hmchangw hmchangw deleted the claude/setup-sast-compliance-y2ruq branch May 19, 2026 06:37
Joey0538 pushed a commit that referenced this pull request May 19, 2026
Service containers run as non-root (uid 10001, since the runtime
hardening in #197) and bind-mount docker-local/backend.creds
read-only at /etc/nats/backend.creds. setup.sh wrote it 0600, so the
in-container user got "permission denied" on `make up`. Generate it
0644 instead. Safe only because this is a throwaway local-dev
credential created by this script; the .env file stays 0600 (read by
the compose CLI on the host, never mounted).
Joey0538 added a commit that referenced this pull request May 19, 2026
… frontend search contract (#201)

* fix(search): serve search.rooms from the ES spotlight index

search.rooms previously matched in ES then re-hydrated each room from
the Mongo `subscriptions` collection. The spotlight index already
carries roomId/roomName/roomType/siteId per (account, room), so the
Mongo hop was redundant — it added a cross-store dependency, a round
trip, and a consistency window for fields ES already has. Serve the
response directly from the spotlight hit; drop HydrateRooms and the
subscriptions collection (added solely for this — apps does its own
$lookup). Add SiteID to model.SearchRoom.

Frontend: correct the search.rooms / search.messages wire contract.
The payload field is `query` (was incorrectly sending `searchText`),
the rooms filter is `roomType` (was `scope`, and dropped the
server-rejected `app` value), responses are `{messages,total}` /
`{rooms}` (were `results`), and the room hit field is `name` (was
`roomName`). Drop phantom fields; keep siteId now that the backend
returns it. Update consumers, tests, and docs/client-api.md.

* chore: trim redundant //nolint:gosec justification

The `// #nosec G402 -- ...` line directly above already states the
justification; the trailing `//nolint:gosec // ...` restated it in
slightly different words. Both directives remain (standalone gosec
and golangci-lint are independent mechanisms) — only the duplicated
prose is removed. Addresses the pending simplify nit from #197.

* refactor(search): drop unreachable nil guard; rename stale test

simplify review: parseRooms always returns a non-nil slice (nil only
with a non-nil error, handled above), so the `if rooms == nil` guard
in searchRooms was dead defensive code — removed. Renamed
TestSearchRoomsResponseJSON_EmptySubscriptions → _EmptyRooms now that
the path no longer touches subscriptions.

* test/docs: address CodeRabbit review on #201

- docs/client-api.md (search.rooms): fix stale #5#6 error-envelope
  anchor (every other ref uses #6); the `internal` reason no longer
  says "ES or MongoDB" — this endpoint is ES-only now.
- SearchResultsPane.test.jsx: assert the full search.rooms wire
  payload {query,roomType,size}, not just query.
- integration_test.go: seed a room owned by another account and
  assert it does not leak. With Mongo hydration removed, the
  spotlight userAccount term filter is the sole access boundary —
  this guards that regression directly.

* ci: deepen base fetch in prewarm affected-detection

The "Detect affected integration targets" step did
`git fetch --depth=1 origin <base>` then a three-dot
`origin/<base>...HEAD` diff. Three-dot needs a merge base, but a
depth-1 base fetch has no history, so the step intermittently
failed with "no merge base" (exit 128), blocking every downstream
job. Drop --depth=1; checkout already uses fetch-depth: 0, so a
full base fetch resolves the merge base reliably.

* fix(local-dev): make backend.creds readable by non-root containers

Service containers run as non-root (uid 10001, since the runtime
hardening in #197) and bind-mount docker-local/backend.creds
read-only at /etc/nats/backend.creds. setup.sh wrote it 0600, so the
in-container user got "permission denied" on `make up`. Generate it
0644 instead. Safe only because this is a throwaway local-dev
credential created by this script; the .env file stays 0600 (read by
the compose CLI on the host, never mounted).

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants