Skip to content

Commit ef849b2

Browse files
ci(security): add Semgrep SAST gate (#485)
Adds a required semgrep job (p/golang, p/typescript, p/react, p/owasp-top-ten; blocking on ERROR severity only) wired into ci-complete's needs, replicating seed #1737. CodeQL is advisory and skipped on private repos, so Semgrep becomes the required SAST signal; corrects the ci-complete comments, which still claimed CodeQL/ license-check were required contexts (branch protection actually requires only CI Complete + Lint PR Title/Body). No ERROR findings exist on current main. Annotates the one known WARNING-severity false positive (HIBP SHA-1 k-anonymity API contract in internal/auth/hibp.go) with a nosemgrep justification ahead of a future WARNING ratchet.
1 parent 9c18fcb commit ef849b2

2 files changed

Lines changed: 55 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,51 @@ jobs:
517517
with:
518518
sarif_file: trivy-results.sarif
519519

520+
# ===========================================================================
521+
# Semgrep SAST — required gate (Phase 4, replaces CodeQL as the required
522+
# SAST signal since CodeQL is skipped entirely on private repos)
523+
# ===========================================================================
524+
# Blocking on ERROR severity only for now — a deliberate first step,
525+
# mirroring seed #1737. WARNING/MEDIUM findings are visible in the job log
526+
# but do not fail the build yet; ratchet to a stricter --severity once the
527+
# current WARNING backlog is triaged in a follow-up. One known false
528+
# positive is already annotated inline with `// nosemgrep: <rule-id> --
529+
# <reason>` (mirroring the existing `#nosec` justifications) so the
530+
# ratchet won't need to re-litigate it: internal/auth/hibp.go (SHA-1 is
531+
# the HIBP k-anonymity API contract).
532+
#
533+
# Runs unconditionally (no path filter) — same rationale as `security`
534+
# above: SAST coverage on every PR is non-negotiable, and Semgrep is fast
535+
# enough (~1-2 min for this ruleset) that gating it behind path filters
536+
# isn't worth the added complexity.
537+
semgrep:
538+
name: Semgrep SAST
539+
runs-on: ubuntu-latest
540+
timeout-minutes: 10
541+
permissions:
542+
contents: read
543+
steps:
544+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
545+
546+
- name: Set up Python
547+
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
548+
with:
549+
python-version: "3.13"
550+
551+
- name: Install Semgrep (pinned)
552+
run: pip install "semgrep==1.168.0"
553+
554+
- name: Run Semgrep (blocking on ERROR severity)
555+
run: |
556+
semgrep scan \
557+
--config p/golang \
558+
--config p/typescript \
559+
--config p/react \
560+
--config p/owasp-top-ten \
561+
--severity ERROR \
562+
--error \
563+
--metrics=off
564+
520565
# ============================================================================
521566
# Quality
522567
# ============================================================================
@@ -948,10 +993,14 @@ jobs:
948993
# Every required-status-check on `main` is consolidated into this single
949994
# job. It fails if any upstream job actually failed; it passes if every
950995
# upstream either succeeded or was intentionally skipped via the path
951-
# filter. Branch protection should require ONLY "CI Complete" plus the
952-
# security-critical checks from other workflows (CodeQL, license-check) —
953-
# not every individual job — so docs-only PRs merge cleanly when
954-
# backend/frontend/c are all skipped.
996+
# filter. Branch protection requires ONLY "CI Complete" (plus "Lint PR
997+
# Title" / "Lint PR Body" from their own workflows) rather than
998+
# enumerating every individual job — so a docs-only PR can merge cleanly
999+
# when backend/frontend/c are all skipped. CodeQL and license-check are
1000+
# NOT required contexts: CodeQL is advisory (and skips entirely on
1001+
# private repos) and license-check reports without blocking. The
1002+
# `semgrep` job in this file is the required SAST gate (blocking on
1003+
# ERROR severity).
9551004
ci-complete:
9561005
name: CI Complete
9571006
runs-on: ubuntu-latest
@@ -969,6 +1018,7 @@ jobs:
9691018
- c-lint
9701019
- dataplane-safety
9711020
- security
1021+
- semgrep
9721022
- quality
9731023
- i18n
9741024
- docs

internal/auth/hibp.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ func CheckPasswordBreached(ctx context.Context, password string) (bool, int, err
113113
// full hash never leaves this function, and the API's protocol is
114114
// fixed to SHA-1; a "stronger" hash would simply not match HIBP's
115115
// corpus. See https://haveibeenpwned.com/API/v3#PwnedPasswords.
116+
// nosemgrep: go.lang.security.audit.crypto.use_of_weak_crypto.use-of-sha1 -- HIBP k-anonymity API mandates SHA-1, same rationale as the #nolint
116117
hash := sha1.Sum([]byte(password)) //nolint:gosec // HIBP requires SHA-1.
117118
hex := strings.ToUpper(hex.EncodeToString(hash[:]))
118119
if len(hex) != hibpPrefixLen+hibpSuffixLen {

0 commit comments

Comments
 (0)