From 6f4d5fcab1e2bbbd48449d80032f034ecea5356c Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Sat, 16 May 2026 16:17:30 +0400 Subject: [PATCH 1/6] fix(deps): bump Go to 1.25.10 + go-billy/v5 to 5.9.0 (Scorecard Vulnerabilities) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 8 of the vulnerabilities the Scorecard `Vulnerabilities` check flagged (currently scoring 5/10). After this PR + Scorecard rescan, that check should jump close to ✅. ## Reachable vulnerabilities (govulncheck-confirmed) — fixed by Go 1.25.10 | Advisory | Module / path | Severity | |---|---|---| | GO-2026-4986 | `net/mail` consumeComment — quadratic concat | DoS | | GO-2026-4977 | `net/mail` consumePhrase — quadratic concat | DoS | | GO-2026-4982 | `html/template` meta-content URL escaping bypass | XSS | | GO-2026-4980 | `html/template` escaper bypass | XSS | | GO-2026-4971 | `net` Dial / LookupPort NUL-byte panic on Windows | DoS | | GO-2026-4918 | `net/http` HTTP/2 SETTINGS_MAX_FRAME_SIZE infinite loop | DoS | All reachable via call paths govulncheck traced — e.g., `pulumi.init` → `mail.ParseAddress`; `mcp.Start` → `http.Server.Serve` → `template.Execute`; multiple `http.Client.*` paths through gcp / cloudflare / mongo / aws code. Fix: bump `go` directive in go.mod from 1.25.9 to 1.25.10. Per HARDENING.md tracker convention (`feedback_sc_no_toolchain`): bump the `go` directive only, never add a `toolchain` line. ## Scorecard-reported (not reachable) — partly fixed | Advisory | Module | Status | |---|---|---| | GHSA-m3xc-h892-ggx6 | `go-git/go-billy/v5 < 5.9.0` | ✅ fixed (5.8.0 → 5.9.0) | | GHSA-qw64-3x98-g7q2 | `go-git/go-billy/v5 < 5.9.0` | ✅ fixed (same bump) | | GHSA-389r-gv7p-r3rp | `go-git/go-git/v6` alpha | ❌ FALSE POSITIVE — we use v5 (5.18.0) | | GO-2022-0635 | `aws-sdk-go/service/s3/s3crypto` | ❌ FALSE POSITIVE — we import aws-sdk-go v1 but NOT the s3crypto subpackage | | GO-2022-0646 | `aws-sdk-go/service/s3/s3crypto` | ❌ FALSE POSITIVE — same as above | govulncheck agrees on the three false positives (`Found 2 vulnerabilities in modules you require, but your code doesn't appear to call these`). Closing the false-positive flags in Scorecard requires either: (a) Migrating the 3 .go files using aws-sdk-go v1 (cloudtrail-related code in pkg/clouds/{pulumi/,}aws/) to aws-sdk-go-v2 — cleaner, separate PR. The v1 advisories have NO upstream fix; they're architectural (AWS deprecated s3crypto v1 in favor of v3). (b) Document via OpenVEX (Phase 2 deferred — Phase 4-and-after). ## go.sum churn `go mod tidy` also bumped two indirect deps (`cyphar/filepath-securejoin v0.4.1 → v0.6.1` and `golang.org/x/exp` to 2026-04-10). Standard upstream-dep refresh as a side effect of bumping go-billy. ## Validation - `go build ./...` — clean - `go vet ./...` — clean (no output) - `go test -short ./pkg/security/...` — all packages PASS (29 tests; bump didn't break the HMAC integrity cache work from PR #254) - `govulncheck ./...` — **0 reachable vulnerabilities** (was 6) Part of the [OpenSSF score climb plan](https://github.com/simple-container-com/api/blob/main/HARDENING.md) documented in HARDENING.md Phase 8. Signed-off-by: Dmitrii Creed --- go.mod | 9 ++++----- go.sum | 12 ++++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index c9bf944c..bfc24efb 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/simple-container-com/api -go 1.25.9 +go 1.25.10 require ( cloud.google.com/go/storage v1.49.0 @@ -17,7 +17,7 @@ require ( github.com/disgoorg/disgo v0.18.5 github.com/fatih/color v1.18.0 github.com/go-delve/delve v1.26.3 - github.com/go-git/go-billy/v5 v5.8.0 + github.com/go-git/go-billy/v5 v5.9.0 github.com/go-git/go-git/v5 v5.18.0 github.com/golangci/golangci-lint v1.64.8 github.com/google/uuid v1.6.0 @@ -167,7 +167,7 @@ require ( github.com/cosiner/argv v0.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect - github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/daixiang0/gci v0.13.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deckarep/golang-set/v2 v2.5.0 // indirect @@ -445,7 +445,7 @@ require ( gocloud.dev v0.37.0 // indirect gocloud.dev/secrets/hashivault v0.37.0 // indirect golang.org/x/arch v0.11.0 // indirect - golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect + golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.53.0 // indirect @@ -453,7 +453,6 @@ require ( golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.44.0 // indirect - golang.org/x/tools/go/expect v0.1.1-deprecated // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect diff --git a/go.sum b/go.sum index c1009139..e3069dc4 100644 --- a/go.sum +++ b/go.sum @@ -307,8 +307,8 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= -github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= -github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c= github.com/daixiang0/gci v0.13.5/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -396,8 +396,8 @@ github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8b github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= -github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= +github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= +github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM= @@ -1232,8 +1232,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= -golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4= From e0299ca2b05c1c6912b4a3943b2ca309101f5cbd Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Sat, 16 May 2026 16:54:22 +0400 Subject: [PATCH 2/6] =?UTF-8?q?fix(deps):=20SCA=20pass=20round-2=20?= =?UTF-8?q?=E2=80=94=20go-git=205.19.0=20+=20Caddy=202.11.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comprehensive SCA pass on top of the Go 1.25.10 + go-billy 5.9.0 work in this PR's first commit. Identifies + fixes additional vulnerable deps that the first triage missed. ## go-git/v5 5.18.0 → 5.19.0 CVE-2026-45022 (HIGH) — go-git's improper parsing of specially crafted objects may lead to inconsistent interpretation compared to upstream Git. Trivy fs flagged this; my earlier triage missed it because Scorecard's flag pointed at the v6-alpha advisory and I incorrectly classified the v5 sibling as a false positive too. Same upstream advisory, separate v5 advisory: GHSA-389r-gv7p-r3rp (v6) and CVE-2026-45022 (v5). Fix is in 5.19.0. ## Caddy 2.11.2 → 2.11.3 (caddy.Dockerfile) Caddy 2.11.2 image scan revealed 18 CVEs (2 CRITICAL, 9 HIGH) all in the binary's vendored deps. Caddy 2.11.3 released after our Phase 1 lock; it bumps: - go-jose/v4 4.1.3 → 4.1.4 (CVE-2026-34986 HIGH) - otel + otel/sdk 1.42→1.43 (CVE-2026-29181, CVE-2026-39883 HIGH) - smallstep/certificates 0.30.0-rc3 → 0.30.0 (CVE-2026-30836 CRITICAL) - Plus Caddy core fixes: fastcgi non-PHP execution bug, admin-socket auth-bypass via array-index normalization + path-prefix matching. Source: https://github.com/caddyserver/caddy/releases/tag/v2.11.3 Updated all three sites (builder FROM + final FROM + xcaddy build arg) per the in-file note. New digests resolved via Docker Hub registry API on 2026-05-16. ## Net source-side state after this commit - trivy fs: 0 vulnerabilities (was 1 HIGH = CVE-2026-45022, now fixed) - govulncheck: 0 reachable; 2 unreachable in modules (the documented aws-sdk-go v1 s3crypto false positives) ## Image-side state (verify post-rebuild) Each prod image at v2026.5.14: kubectl 8 (5H/3M) — all upstream kubectl-binary stdlib@1.26.2; no SC action; track upstream rebuild caddy 18 (2C/9H/6M/1L) — should drop to ~6 after rebuild with Caddy 2.11.3 (this PR) github-actions 27 (17H/10M) — 7 fixed by Go 1.25.10 + go-git/go-billy bumps (this PR); remaining 20 are bundled pulumi/gcloud binaries @ 1.26.2 (upstream) cloud-helpers 17 (9H/8M) — glibc 2.34-231.amzn2023.0.4 NOW patched (Phase 1 deferred status closes); rebuild auto-picks via dnf upgrade. Plus stdlib fixed by Go 1.25.10. ## Dependabot reconciliation | PR | What | Verdict | |---|---|---| | #162 | go-git/v5 5.13.1 → 5.16.5 | SUPERSEDED — we're at 5.19.0 now | | #237 | pulumi-command/sdk 0.9.2 → 1.2.1 | LET STAND | | #242 | alpine 3.21 → 3.23 (docker-minor-and-patch group) | LET STAND — fixes Alpine OS-pkg CVEs in kubectl/github-actions images | | #243 | caddy digest bump (still 2.11.2) | SUPERSEDED — this PR bumps to 2.11.3 | | #244 | alpine/kubectl base digest bump | LET STAND | | #245-247 | mkdocs deps | LET STAND | | #248-251 | GitHub Actions bumps | LET STAND | | #252 | gomod-minor-and-patch group (26 deps) | PARTIAL SUPERSEDE — go-billy/go-git/go-jose/otel/grpc bumps from this PR. Dependabot will auto-rebase #252 on top with the remaining 22 non-security minor/patch bumps. | | #233 | reecetech/version-increment | LET STAND | ## Validation - `go build ./...` clean - `go vet ./...` clean - `go test -short ./pkg/security/...` — all 8 packages PASS - `govulncheck ./...` — 0 reachable - `trivy fs` — 0 findings (any severity) Refs HARDENING.md Phase 8 Scorecard climb plan. Signed-off-by: Dmitrii Creed --- caddy.Dockerfile | 12 +++++++----- go.mod | 5 +++-- go.sum | 10 ++++++---- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/caddy.Dockerfile b/caddy.Dockerfile index ec63aec2..001cbae6 100644 --- a/caddy.Dockerfile +++ b/caddy.Dockerfile @@ -1,16 +1,18 @@ -# Caddy 2.11.2: clears Go-stdlib CVEs in 2.8.4's binary + CVE-2026-27586. -# Bumping requires editing all three "2.11.2" sites below (two FROMs + xcaddy). +# Caddy 2.11.3: closes vendored-dep CVEs in 2.11.2's binary (go-jose v4, +# otel, smallstep/certificates) plus Caddy core fastcgi + admin-socket +# auth-bypass fixes — see https://github.com/caddyserver/caddy/releases/tag/v2.11.3. +# Bumping requires editing all three "2.11.x" sites below (two FROMs + xcaddy). # Refresh: docker buildx imagetools inspect caddy:X.Y.Z[-builder] -FROM caddy:2.11.2-builder@sha256:10ed0251c5cd1dbb4db0b71ad43121147961a51adfec35febce2c93ea25c24f4 AS builder +FROM caddy:2.11.3-builder@sha256:14f5b3ecb208d45a37bc26435a8c0c29181de98115358b4b863c6ec5801116a5 AS builder RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \ --mount=type=cache,target=/root/.cache,sharing=locked \ - xcaddy build "v2.11.2" \ + xcaddy build "v2.11.3" \ --with github.com/grafana/certmagic-gcs@v0.1.7 \ && caddy version -FROM caddy:2.11.2@sha256:25cdc846626b62d05f6b633b9b40c2c9f6ef89b515dc76133cefd920f7dbe562 +FROM caddy:2.11.3@sha256:3739ea4f0c877259a693d932693cf8f3408e9a9497c004f031b0e830e93e1546 RUN apk update && apk upgrade --no-cache && rm -rf /var/cache/apk/* diff --git a/go.mod b/go.mod index bfc24efb..aaaac3df 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/fatih/color v1.18.0 github.com/go-delve/delve v1.26.3 github.com/go-git/go-billy/v5 v5.9.0 - github.com/go-git/go-git/v5 v5.18.0 + github.com/go-git/go-git/v5 v5.19.0 github.com/golangci/golangci-lint v1.64.8 github.com/google/uuid v1.6.0 github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef @@ -285,6 +285,7 @@ require ( github.com/kisielk/errcheck v1.9.0 // indirect github.com/kkHAIKE/contextcheck v1.1.6 // indirect github.com/klauspost/compress v1.17.6 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kulti/thelper v0.6.3 // indirect github.com/kunwardeep/paralleltest v1.0.10 // indirect github.com/kylelemons/godebug v1.1.0 // indirect @@ -340,7 +341,7 @@ require ( github.com/pgavlin/fx v0.1.6 // indirect github.com/pgavlin/goldmark v1.1.33-0.20200616210433-b5eb04559386 // indirect github.com/pgavlin/text v0.0.0-20240821195002-b51d0990e284 // indirect - github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/term v1.2.0-beta.2 // indirect github.com/pkoukk/tiktoken-go v0.1.6 // indirect diff --git a/go.sum b/go.sum index e3069dc4..30961a69 100644 --- a/go.sum +++ b/go.sum @@ -400,8 +400,8 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM= -github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= +github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc= +github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -710,6 +710,8 @@ github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/tt github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -881,8 +883,8 @@ github.com/pgavlin/text v0.0.0-20240821195002-b51d0990e284 h1:qpLdAFg3kyV/mEsuMP github.com/pgavlin/text v0.0.0-20240821195002-b51d0990e284/go.mod h1:fk4+YyTLi0Ap0CsL1HA70/tAs6evqw3hbPGdR8rD/3E= github.com/philippgille/chromem-go v0.7.0 h1:4jfvfyKymjKNfGxBUhHUcj1kp7B17NL/I1P+vGh1RvY= github.com/philippgille/chromem-go v0.7.0/go.mod h1:hTd+wGEm/fFPQl7ilfCwQXkgEUxceYh86iIdoKMolPo= -github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= -github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= From a15d3aed687a7498fa145bbdece5ff62c42eb4f0 Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Sat, 16 May 2026 18:01:36 +0400 Subject: [PATCH 3/6] feat(security): add 23 custom Semgrep rules + opt into curated registry packs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the SAST coverage gaps surfaced by the parallel-agent audit and the codex+gemini review. Adds .semgrep/rules/ consumer-rules wired through the existing simple-container-com/actions semgrep workflow, plus registry-packs opt-in for community-maintained coverage. ## New rules under .semgrep/rules/ **sigstore.yml** (6 rules) — supply-chain core for Phase 2 attestation: - gha-cosign-verify-without-identity-flag — `cosign verify*` missing `--certificate-identity[-regexp]` - gha-cosign-verify-without-oidc-issuer-flag — missing `--certificate-oidc-issuer` - gha-cosign-verify-insecure-flags — `--insecure-ignore-tlog` / `--insecure-ignore-sct` (Rekor bypass) - gha-gh-attestation-missing-scope — `gh attestation verify` without `--repo` or `--owner` - gha-attest-build-provenance-mutable-subject — `subject-name` with `:latest` / `:staging` without `subject-digest` - gha-oidc-id-token-on-untrusted-trigger — `id-token: write` reachable from `pull_request_target` / `workflow_run` **gha-extras.yml** (4 rules) — workflow-misuse patterns: - gha-workflow-level-secret-env — `env: KEY: ${{ secrets.* }}` at file scope (leaks to every step) - gha-upload-artifact-secrets-leak — artifact path matches `.env|.pem|.key|secrets/` - gha-pull-request-target-implicit-base-ref — `pull_request_target` checkout without explicit `ref:` - gha-github-token-in-github-env — `GITHUB_TOKEN` written to `$GITHUB_ENV` (cross-step persistence) **pulumi-iac.yml** (5 rules) — IaC patterns beyond AWS-RDS: - go-pulumi-k8s-privileged-workload — Privileged/HostNetwork/HostPID/ HostPath - go-pulumi-gcp-storage-public-access — allUsers / allAuthenticatedUsers / UniformBucketLevelAccess: false - go-pulumi-gcp-compute-default-sa-cloud-platform — full-project SA + cloud-platform scope - go-pulumi-aws-sg-open-ingress — 0.0.0.0/0 ingress - go-pulumi-iam-wildcard-policy — `*:*` IAM grants **go-canon.yml** (8 rules) — gosec / dgryski-semgrep-go misses CodeQL security-extended doesn't cover: - go-hmac-not-constant-time — `bytes.Equal` on MAC values - go-cipher-deterministic-nonce — zero / constant nonce - go-yaml-unmarshal-into-interface — typed-struct hygiene - go-jwt-parse-unverified — skipping signature - go-jwt-none-algorithm — `alg: none` - go-http-client-no-timeout — missing `Timeout` - go-tls-min-version-below-1-2 — TLS 1.0/1.1 - go-exec-command-inherits-environ-default — leaks parent env ## Workflow config (.github/workflows/semgrep.yml) - consumer-rules: '.semgrep/rules' - registry-packs: 'p/ci,p/secrets,p/golang,p/gosec' Adds ~300 curated registry rules on top of SC's 30 custom rules + the existing simple-container-com/actions shared bundle. ## FP triage rounds | Round | Total | Fixes | |---|---|---| | 1 | 81 | initial draft — generic regex with single-line lookahead | | 2 | 63 | extended lookahead window to span `\` continuations | | 3 | 43 | excluded markdown + skipped comment lines | | 4 | invalid | tried metavariable-pattern with negation-in-Or | | 5 | 36 | switched to YAML structural matching with metavariable-pattern + pattern-not-regex | Net findings after polish: 0 ERROR-level, 36 WARNING-level. The 36 WARNINGs are real instances of `yaml.Unmarshal into interface{}` and `exec.Command` without explicit `Env` set — defense-in-depth tech debt the team can address incrementally. Action's `fail-on-severity: ERROR` default means they don't block CI. ## Follow-up — promote to shared ruleset Once these rules survive ~14 days on this PR's CI without further tuning, promote to simple-container-com/actions/semgrep-scan/rules/ in a separate PR. Drop the `consumer-rules:` line from this workflow config at the same time. Refs HARDENING.md Phase 8 score-climb plan (Scorecard SAST + Tier A → S). Signed-off-by: Dmitrii Creed --- .github/workflows/semgrep.yml | 13 +++ .semgrep/rules/gha-extras.yml | 65 +++++++++++++ .semgrep/rules/go-canon.yml | 167 ++++++++++++++++++++++++++++++++++ .semgrep/rules/pulumi-iac.yml | 92 +++++++++++++++++++ .semgrep/rules/sigstore.yml | 148 ++++++++++++++++++++++++++++++ 5 files changed, 485 insertions(+) create mode 100644 .semgrep/rules/gha-extras.yml create mode 100644 .semgrep/rules/go-canon.yml create mode 100644 .semgrep/rules/pulumi-iac.yml create mode 100644 .semgrep/rules/sigstore.yml diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 468f6beb..bc9572ff 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -12,3 +12,16 @@ jobs: uses: simple-container-com/actions/.github/workflows/semgrep.yml@main permissions: contents: read + with: + # Consumer-rules: SC-api-specific Semgrep rules at .semgrep/rules/. + # Categories: sigstore (cosign/gh-attestation/attest-build-provenance + # supply-chain hardening), gha-extras (workflow-misuse), pulumi-iac + # (K8s + GCP + AWS-extra), go-canon (gosec G-series + dgryski/ + # semgrep-go misses CodeQL security-extended doesn't cover). + # After 14 days clean here, promote to the shared SC ruleset in + # simple-container-com/actions/semgrep-scan/rules/. + consumer-rules: '.semgrep/rules' + # Curated registry packs covering CI/secrets + Go + gosec G-series. + # Pinned at the action level via Semgrep image digest in + # semgrep-scan/action.yml; pack content is fetched at runtime. + registry-packs: 'p/ci,p/secrets,p/golang,p/gosec' diff --git a/.semgrep/rules/gha-extras.yml b/.semgrep/rules/gha-extras.yml new file mode 100644 index 00000000..5373a3fa --- /dev/null +++ b/.semgrep/rules/gha-extras.yml @@ -0,0 +1,65 @@ +# GitHub Actions extras — workflow-misuse patterns that aren't in the +# OWASP CICD canon but are real attack surface for SC's threat model +# (parses customer secrets, runs in customer CI with elevated perms). + +rules: + - id: gha-workflow-level-secret-env + message: >- + Workflow-level `env:` block defines a variable with `secrets.*`. + That value leaks to EVERY step in EVERY job — including any + third-party action call inside the workflow. Scope the secret + to the specific job or step that needs it. + severity: ERROR + languages: [yaml] + paths: + include: [".github/workflows/*.yml", ".github/workflows/*.yaml"] + pattern-regex: '(?ms)\Aname:[^\n]*\n[\s\S]*?\non:[^\n]*\n[\s\S]*?\nenv:\s*\n(?:[ \t]+[^\n]+\n)*?[ \t]+[A-Z_][A-Z0-9_]*:\s*\$\{\{\s*secrets\.' + + - id: gha-upload-artifact-secrets-leak + message: >- + `actions/upload-artifact` path matches a credential / secret file + pattern (`.env`, `*.pem`, `*.key`, `secrets/**`). Artifacts are + retained for ≤ 90 days and downloadable by anyone with repo read + access — never publish credentials this way. + severity: ERROR + languages: [yaml] + paths: + include: [".github/workflows/*.yml", ".github/workflows/*.yaml"] + patterns: + - pattern-regex: 'uses:\s*actions/upload-artifact@' + - pattern-either: + - pattern-regex: 'path:\s*[''"]?[^\n]*\.env' + - pattern-regex: 'path:\s*[''"]?[^\n]*\.pem' + - pattern-regex: 'path:\s*[''"]?[^\n]*\.key' + - pattern-regex: 'path:\s*[''"]?[^\n]*secrets/' + + - id: gha-pull-request-target-implicit-base-ref + message: >- + `pull_request_target` defaults `actions/checkout` to the base + branch — but any step run after `checkout` with `ref:` set to a + `github.event.pull_request.head.*` value runs FORK code with the + target's elevated permissions (the canonical PPE vector). Pin + checkout explicitly to a trusted ref or remove `pull_request_target`. + severity: ERROR + languages: [yaml] + paths: + include: [".github/workflows/*.yml", ".github/workflows/*.yaml"] + patterns: + - pattern-regex: 'on:\s*\n\s*pull_request_target' + - pattern-regex: 'uses:\s*actions/checkout@' + - pattern-not-regex: 'uses:\s*actions/checkout@[^\n]*\n(?:\s+[^\n]+\n)*\s+ref:\s*\$\{\{\s*github\.event\.workflow_run' + + - id: gha-github-token-in-github-env + message: >- + Writing `GITHUB_TOKEN` (or a derived alias) to `$GITHUB_ENV` + persists the token in the runner's environment for the remainder + of the job — every subsequent step, including third-party actions + run via `uses:`, can read it. Pass tokens via per-step `env:` + mappings instead. + severity: ERROR + languages: [generic] + paths: + include: [".github/workflows/*.yml", ".github/workflows/*.yaml"] + pattern-either: + - pattern-regex: 'echo\s+["'']?[A-Z_]*TOKEN[A-Z_]*=\$\{?\{?\s*secrets\.GITHUB_TOKEN' + - pattern-regex: 'echo\s+["'']?[A-Z_]*TOKEN[A-Z_]*=[^\n]+>>\s*\$GITHUB_ENV' diff --git a/.semgrep/rules/go-canon.yml b/.semgrep/rules/go-canon.yml new file mode 100644 index 00000000..cb3df827 --- /dev/null +++ b/.semgrep/rules/go-canon.yml @@ -0,0 +1,167 @@ +# Go security canon — gosec G-series + dgryski/semgrep-go patterns +# that CodeQL's `security-extended` Go pack misses. Focused on the +# crypto + parsing surfaces in SC (secrets cache, attestation predicate +# parsing, OIDC JWT validation, downloaded-binary hardening). + +rules: + - id: go-hmac-not-constant-time + message: >- + HMAC / MAC comparison uses `bytes.Equal` or a non-constant-time + operator. The byte-comparison primitive leaks timing information + that lets an attacker reconstruct the expected MAC byte-by-byte. + Use `hmac.Equal` (or `crypto/subtle.ConstantTimeCompare`). + severity: ERROR + languages: [go] + paths: + include: ["**/*.go"] + exclude: ["**/*_test.go"] + patterns: + - pattern-either: + - pattern: bytes.Equal($A, $B) + - metavariable-regex: + metavariable: $A + regex: '(mac|hmac|HMAC|MAC|signature|sig|tag)' + + - id: go-cipher-deterministic-nonce + message: >- + AEAD cipher seal/open is invoked with a zero / constant nonce. The + whole point of an AEAD is per-message nonce uniqueness; reusing a + nonce under the same key catastrophically breaks confidentiality + and integrity (especially for GCM / ChaCha20-Poly1305). Use + `crypto/rand.Read(nonce[:])` per message. + severity: ERROR + languages: [go] + paths: + include: ["**/*.go"] + exclude: ["**/*_test.go"] + pattern-either: + - pattern: | + $NONCE := make([]byte, $AEAD.NonceSize()) + ... + $AEAD.Seal($DST, $NONCE, $PLAINTEXT, $AAD) + - pattern: | + $NONCE := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + ... + $AEAD.Seal(...) + + - id: go-yaml-unmarshal-into-interface + message: >- + `yaml.Unmarshal` into `interface{}` or `map[string]interface{}` + from untrusted input expands into Go runtime objects controlled by + the attacker — billion-laughs DoS, type-confusion later in the + pipeline, and (for `yaml.v2`) deserialization gadgets. Always + unmarshal into a TYPED struct. + severity: WARNING + languages: [go] + paths: + include: ["**/*.go"] + exclude: ["**/*_test.go"] + pattern-either: + - pattern: | + var $V interface{} + ... + yaml.Unmarshal($DATA, &$V) + - pattern: | + var $V map[string]interface{} + ... + yaml.Unmarshal($DATA, &$V) + + - id: go-jwt-parse-unverified + message: >- + `jwt.ParseUnverified` skips signature validation entirely. Even + "just reading claims" requires verification because the claims + themselves are attacker-controlled until the signature is checked. + Use `jwt.Parse(..., keyfunc)` + `WithValidMethods` + audience and + issuer checks. + severity: ERROR + languages: [go] + paths: + include: ["**/*.go"] + exclude: ["**/*_test.go"] + pattern-either: + - pattern: jwt.ParseUnverified(...) + - pattern: $JWT.ParseUnverified(...) + + - id: go-jwt-none-algorithm + message: >- + JWT verification accepts the `none` algorithm. A `none`-algorithm + JWT has no signature; any caller can claim any identity. Restrict + with `WithValidMethods([]string{"RS256", "ES256", ...})` or the + v5+ `WithValidAlg` option. + severity: ERROR + languages: [go] + paths: + include: ["**/*.go"] + exclude: ["**/*_test.go"] + pattern-either: + - pattern-regex: 'jwt\.SigningMethod(?:HS|RS|ES|PS)?None' + - pattern-regex: '["'']alg["'']\s*:\s*["'']none["'']' + + - id: go-http-client-no-timeout + message: >- + `&http.Client{}` literal does not set `Timeout`. Default is "no + timeout" — a malicious or slow upstream hangs the request + indefinitely, exhausting goroutines/file descriptors (slowloris + class DoS). Set an explicit `Timeout: 30 * time.Second` (or a + reasonable bound for the caller's use case). + severity: WARNING + languages: [go] + paths: + include: ["**/*.go"] + exclude: ["**/*_test.go"] + patterns: + - pattern: '&http.Client{...}' + - pattern-not: '&http.Client{..., Timeout: ..., ...}' + + - id: go-tls-min-version-below-1-2 + message: >- + `tls.Config{MinVersion: ...}` below `tls.VersionTLS12` allows + vulnerable TLS 1.0 / 1.1 handshakes. Modern stacks should pin + `MinVersion: tls.VersionTLS13` (or 1.2 as the floor). + severity: ERROR + languages: [go] + paths: + include: ["**/*.go"] + exclude: ["**/*_test.go"] + pattern-either: + - pattern: "tls.Config{..., MinVersion: tls.VersionTLS10, ...}" + - pattern: "tls.Config{..., MinVersion: tls.VersionTLS11, ...}" + - pattern: "tls.Config{..., MinVersion: 0x0301, ...}" + - pattern: "tls.Config{..., MinVersion: 0x0302, ...}" + + - id: go-exec-command-inherits-environ-default + message: >- + `exec.Command(...).Run()` or `.Output()` without setting `cmd.Env` + inherits the parent process environment — in a CI runner that + includes every secret injected as an env var. Pass an explicit + filtered `cmd.Env = []string{...}`. The standard pattern is + `cmd.Env = append(os.Environ()[:0:0], "PATH=" + os.Getenv("PATH"))`. + severity: WARNING + languages: [go] + paths: + include: ["**/*.go"] + exclude: ["**/*_test.go"] + patterns: + - pattern-either: + - pattern: | + $CMD := exec.Command($ARG, ...) + ... + $CMD.Run() + - pattern: | + $CMD := exec.Command($ARG, ...) + ... + $CMD.Output() + - pattern: | + $CMD := exec.CommandContext($CTX, $ARG, ...) + ... + $CMD.Run() + - pattern-not: | + $CMD := exec.Command(...) + ... + $CMD.Env = ... + ... + - pattern-not: | + $CMD := exec.CommandContext(...) + ... + $CMD.Env = ... + ... diff --git a/.semgrep/rules/pulumi-iac.yml b/.semgrep/rules/pulumi-iac.yml new file mode 100644 index 00000000..dcecabe0 --- /dev/null +++ b/.semgrep/rules/pulumi-iac.yml @@ -0,0 +1,92 @@ +# Pulumi-Go IaC hardening — SC provisions parent/client stacks on AWS, +# GCP, and Kubernetes. The existing SC ruleset only covers AWS RDS +# encryption. These rules expand to GCP + Kubernetes baselines. +# Ports of Checkov / Kubesec patterns to Pulumi-Go's API surface. + +rules: + - id: go-pulumi-k8s-privileged-workload + message: >- + Pulumi-Kubernetes Pod/Container is provisioned with a privilege- + escalation knob (`Privileged: true`, `HostNetwork: true`, + `HostPID: true`, or a hostPath volume mount). On a multi-tenant + cluster this allows breakout to the node. Drop the privilege OR + document the workload as cluster-singleton with an explicit + threat-model justification. + severity: ERROR + languages: [go] + paths: + include: ["**/*.go"] + pattern-either: + - pattern: | + $ARGS = corev1.SecurityContextArgs{..., Privileged: pulumi.Bool(true), ...} + - pattern: | + $ARGS = corev1.PodSpecArgs{..., HostNetwork: pulumi.Bool(true), ...} + - pattern: | + $ARGS = corev1.PodSpecArgs{..., HostPID: pulumi.Bool(true), ...} + - pattern: | + $V = corev1.VolumeArgs{..., HostPath: $HP, ...} + + - id: go-pulumi-gcp-storage-public-access + message: >- + GCP Storage Bucket / BucketIAMMember allows public-internet read + (`allUsers` or `allAuthenticatedUsers` member) OR disables + Uniform Bucket-Level Access. Even if the bucket holds non-sensitive + data the public-ACL surface is a CIS GCP audit failure (CKV_GCP_28, + CKV_GCP_29). Use a private bucket + signed URLs for public access. + severity: ERROR + languages: [go] + paths: + include: ["**/*.go"] + pattern-either: + - pattern: | + $M = storage.BucketIAMMemberArgs{..., Member: pulumi.String("allUsers"), ...} + - pattern: | + $M = storage.BucketIAMMemberArgs{..., Member: pulumi.String("allAuthenticatedUsers"), ...} + - pattern: | + $B = storage.BucketArgs{..., UniformBucketLevelAccess: pulumi.Bool(false), ...} + + - id: go-pulumi-gcp-compute-default-sa-cloud-platform + message: >- + GCE VM uses the default Compute Engine service account paired with + the broad `cloud-platform` OAuth scope. An attacker with RCE on + the VM can call ANY GCP API as the default SA — full project + takeover. Bind the VM to a least-privilege custom service account + and use narrow scopes (or `cloud-platform` only on the custom SA). + severity: ERROR + languages: [go] + paths: + include: ["**/*.go"] + patterns: + - pattern: | + $A = compute.InstanceArgs{..., ServiceAccount: $SA, ...} + - pattern-regex: 'compute\.InstanceArgs\{[\s\S]{0,400}ServiceAccount:[\s\S]{0,200}Scopes:[\s\S]{0,200}cloud-platform' + + - id: go-pulumi-aws-sg-open-ingress + message: >- + AWS SecurityGroup or SecurityGroupRule with ingress `CidrBlocks` + containing `0.0.0.0/0` (any source). For non-HTTPS / non-HTTP + protocols this is a CIS AWS Foundation control failure (CKV_AWS_24 + / CKV_AWS_260). Restrict to the source CIDR the workload actually + needs. + severity: ERROR + languages: [go] + paths: + include: ["**/*.go"] + pattern-either: + - pattern: | + $A = ec2.SecurityGroupArgs{..., Ingress: $I, ...} + - pattern-regex: 'CidrBlocks:[\s]*pulumi\.StringArray\{[^\}]*"0\.0\.0\.0/0"' + + - id: go-pulumi-iam-wildcard-policy + message: >- + IAM policy statement with `Action: "*"` and/or `Resource: "*"`. + Grants god-mode access — never appropriate outside narrowly-scoped + break-glass automation. Document or split into least-privilege + grants. + severity: ERROR + languages: [go] + paths: + include: ["**/*.go"] + pattern-either: + - pattern-regex: '"Action":\s*"\*"' + - pattern-regex: '"Resource":\s*"\*"[^\}]*"Effect":\s*"Allow"' diff --git a/.semgrep/rules/sigstore.yml b/.semgrep/rules/sigstore.yml new file mode 100644 index 00000000..a69b9844 --- /dev/null +++ b/.semgrep/rules/sigstore.yml @@ -0,0 +1,148 @@ +# Sigstore / cosign / attest-build-provenance hardening — supply-chain core. +# These rules guard the verification surface that SC's Phase 2 attestation +# pipeline (PR #257) ships. Most are regex-anchored to the exact CLI flags +# the documented consumer verification flow uses. + +rules: + - id: gha-cosign-verify-without-identity-flag + message: >- + `cosign verify[-blob|-attestation]` invocation is missing + `--certificate-identity` (or `--certificate-identity-regexp`). + Without identity binding the call accepts any keyless signature + from any workflow in the Sigstore transparency log — defeats + trust-root separation. See docs/SECURITY.md "Identity-regex contract". + severity: ERROR + languages: [yaml] + paths: + include: + - ".github/workflows/*.yml" + - ".github/workflows/*.yaml" + - ".github/actions/**/action.yml" + - ".github/actions/**/action.yaml" + # Match a `run: |` block that contains `cosign verify*` AND lacks + # `--certificate-identity` anywhere in the block body. + patterns: + - pattern: | + run: | + $BODY + - metavariable-regex: + metavariable: $BODY + regex: '\bcosign\s+verify(?:-blob|-attestation)?\b' + - metavariable-pattern: + metavariable: $BODY + patterns: + - pattern-regex: '.' + - pattern-not-regex: '--certificate-identity' + + - id: gha-cosign-verify-without-oidc-issuer-flag + message: >- + `cosign verify[-blob|-attestation]` invocation is missing + `--certificate-oidc-issuer`. Required to bind the keyless signer + to a specific OIDC provider (almost always + `https://token.actions.githubusercontent.com` for SC). + severity: ERROR + languages: [yaml] + paths: + include: + - ".github/workflows/*.yml" + - ".github/workflows/*.yaml" + - ".github/actions/**/action.yml" + - ".github/actions/**/action.yaml" + patterns: + - pattern: | + run: | + $BODY + - metavariable-regex: + metavariable: $BODY + regex: '\bcosign\s+verify(?:-blob|-attestation)?\b' + - metavariable-pattern: + metavariable: $BODY + patterns: + - pattern-regex: '.' + - pattern-not-regex: '--certificate-oidc-issuer' + + - id: gha-cosign-verify-insecure-flags + message: >- + `cosign verify*` invoked with transparency-log bypass flags + (`--insecure-ignore-tlog`, `--insecure-ignore-sct`). These flags + DISABLE the Rekor inclusion proof that backs keyless verification — + a signature would verify even if it was never published to the + transparency log. Never use in CI or in documented consumer commands. + severity: ERROR + languages: [generic] + paths: + include: ["**/*.yml", "**/*.yaml", "**/*.sh", "**/*.md"] + pattern-either: + - pattern-regex: 'cosign\s+verify\S*[^\n]*--insecure-ignore-tlog' + - pattern-regex: 'cosign\s+verify\S*[^\n]*--insecure-ignore-sct' + + - id: gha-gh-attestation-missing-scope + message: >- + `gh attestation verify` requires `--owner ` OR `--repo + /` to bind the attestation lookup to a trusted scope. + Add `--cert-identity-regex` + `--cert-oidc-issuer` to match the + strict identity contract documented in docs/SECURITY.md. + severity: ERROR + languages: [yaml] + paths: + include: + - ".github/workflows/*.yml" + - ".github/workflows/*.yaml" + - ".github/actions/**/action.yml" + - ".github/actions/**/action.yaml" + # Whole-`run:`-block matching — find `gh attestation verify` and require + # `--repo` or `--owner` in the same block. + patterns: + - pattern: | + run: | + $BODY + - metavariable-regex: + metavariable: $BODY + regex: '\bgh\s+attestation\s+verify\b' + - metavariable-pattern: + metavariable: $BODY + patterns: + - pattern-regex: '.' + - pattern-not-regex: '--(repo|owner)\s' + + - id: gha-attest-build-provenance-mutable-subject + message: >- + `actions/attest-build-provenance` must publish provenance bound to + an immutable subject. Using `subject-name` with a mutable tag + (`:latest`, `:staging`) without a corresponding `subject-digest` + yields a provenance that "verifies" against whatever the tag + currently points to — defeats the SLSA Build L3 non-falsifiability + property. Always pass `subject-digest:`. + severity: ERROR + languages: [yaml] + paths: + include: [".github/workflows/*.yml", ".github/workflows/*.yaml"] + pattern-either: + - pattern: | + uses: actions/attest-build-provenance@... + with: + ... + subject-name: "$REPO:latest" + - pattern: | + uses: actions/attest-build-provenance@... + with: + ... + subject-name: "$REPO:staging" + + - id: gha-oidc-id-token-on-untrusted-trigger + message: >- + A job with `permissions: id-token: write` is reachable from an + untrusted-input trigger (`pull_request_target`, `workflow_run`, or + `pull_request` without explicit gating). OIDC token-grant from a + fork PR can mint a Sigstore identity that the SC trust-root regex + accepts. Either remove `id-token: write` from the job or split the + workflow so signing jobs run only on trusted refs. + severity: ERROR + languages: [yaml] + paths: + include: [".github/workflows/*.yml", ".github/workflows/*.yaml"] + patterns: + - pattern-either: + - pattern-regex: 'on:\s*\n\s*pull_request_target' + - pattern-regex: 'on:\s*\n\s*workflow_run' + - pattern-regex: 'id-token:\s*write' From 1725bed7ce7fd0eaa833c38a67d8ca591baee794 Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Sat, 16 May 2026 18:31:58 +0400 Subject: [PATCH 4/6] fix(sast): drop yaml-unmarshal rule, exclude trusted CLIs from exec-Command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Option A from the triage on round-5 results (36 WARNING findings, all real instances but mostly intentional by design): 1. Dropped `go-yaml-unmarshal-into-interface` entirely (21 findings). SC's untyped-unmarshal sites are intentional — generic YAML inspection tooling (`obfuscateYAMLCredentials`, `configdiff/ resolver`, `validation/validator`, `chat/commands_project`'s AI analysis path) needs `interface{}` because the result isn't a security decision input. Taint-aware coverage for the cases that DO matter (untrusted-source flow → unmarshal) lives in the `p/golang` registry pack opted into via .github/workflows/ semgrep.yml. Note retained in go-canon.yml explaining the drop. 2. Tightened `go-exec-command-inherits-environ-default` with: - File-level excludes for pkg/security/{sbom,scan,tools}/** (intentional design surface for external-scanner invocation), pkg/assistant/chat/input.go and pkg/assistant/cicd/utils.go + pkg/assistant/analysis/git_analyzer.go (terminal-control + git read-only invocations that need env propagation). - Metavariable exclusion of trusted-CLI first-argument literals: git, stty, gh, cosign, syft, trivy, grype, kubectl, docker, pulumi, sc, welder. These all require env (PATH, HOME, GIT_*, GH_TOKEN, TERM) to function correctly; flagging them produces noise without signal. Round-7 result: 0 findings across all 22 remaining custom rules. The rules now catch REGRESSIONS (e.g., a new exec.Command invocation of an untrusted binary without explicit Env) without drowning the signal on intentional usage. Refs HARDENING.md Phase 8 score-climb plan. Signed-off-by: Dmitrii Creed --- .semgrep/rules/go-canon.yml | 55 ++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/.semgrep/rules/go-canon.yml b/.semgrep/rules/go-canon.yml index cb3df827..b4a4b8cd 100644 --- a/.semgrep/rules/go-canon.yml +++ b/.semgrep/rules/go-canon.yml @@ -44,27 +44,13 @@ rules: ... $AEAD.Seal(...) - - id: go-yaml-unmarshal-into-interface - message: >- - `yaml.Unmarshal` into `interface{}` or `map[string]interface{}` - from untrusted input expands into Go runtime objects controlled by - the attacker — billion-laughs DoS, type-confusion later in the - pipeline, and (for `yaml.v2`) deserialization gadgets. Always - unmarshal into a TYPED struct. - severity: WARNING - languages: [go] - paths: - include: ["**/*.go"] - exclude: ["**/*_test.go"] - pattern-either: - - pattern: | - var $V interface{} - ... - yaml.Unmarshal($DATA, &$V) - - pattern: | - var $V map[string]interface{} - ... - yaml.Unmarshal($DATA, &$V) + # Note: a `go-yaml-unmarshal-into-interface` rule was drafted and ran in + # round 5 (21 findings) but was DROPPED in round 6 after triage: SC's + # untyped-unmarshal sites are intentional (generic YAML inspection for + # diff/obfuscation/AI-analysis tooling, where the result isn't used for + # security decisions). Taint-aware coverage for the cases that DO matter + # (untrusted-source flow → unmarshal) lives in the `p/golang` registry + # pack opted into via .github/workflows/semgrep.yml. - id: go-jwt-parse-unverified message: >- @@ -140,7 +126,23 @@ rules: languages: [go] paths: include: ["**/*.go"] - exclude: ["**/*_test.go"] + exclude: + - "**/*_test.go" + # pkg/security/{sbom,scan,tools} is the SC binary's intentional + # design surface for invoking external scanners (syft, trivy, + # grype) and installing toolchain — env inheritance is required + # so the scanners pick up PATH / HOME / GOPATH. These are not + # security-decision call sites. + - "pkg/security/sbom/**/*.go" + - "pkg/security/scan/**/*.go" + - "pkg/security/tools/**/*.go" + # Terminal control invocations of stty have no env-relevant + # impact and should not be flagged. + - "pkg/assistant/chat/input.go" + # git-status / git-remote read-only invocations need GIT_* + # env propagation to honor user gitconfig. + - "pkg/assistant/analysis/git_analyzer.go" + - "pkg/assistant/cicd/utils.go" patterns: - pattern-either: - pattern: | @@ -165,3 +167,12 @@ rules: ... $CMD.Env = ... ... + # Skip trusted-CLI invocations: these require env propagation by + # design (git needs HOME/GIT_*; stty needs TERM; gh needs GH_TOKEN; + # cosign/syft/trivy/grype need PATH). Threat-model is about + # untrusted-binary invocation, not all exec.Command usage. + - metavariable-pattern: + metavariable: $ARG + patterns: + - pattern-regex: '.' + - pattern-not-regex: '"(git|stty|gh|cosign|syft|trivy|grype|kubectl|docker|pulumi|sc|welder)"' From 02678b4583c135573f0cbd2df2f017d7867c0b29 Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Sat, 16 May 2026 19:04:48 +0400 Subject: [PATCH 5/6] =?UTF-8?q?fix(sast):=20drop=20p/secrets=20registry=20?= =?UTF-8?q?pack=20=E2=80=94=20TruffleHog=20covers=20it=20without=20FPs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `p/secrets` pack flagged 26 ERROR-severity false positives in docs/ examples, testdata SSH keys, and example .sc/cfg files — all documented example values, not real secrets. TruffleHog already scans for secrets on this repo (security-scan.yml) and is tuned with the `secret-scan-extra-excludes` list for these exact paths. The shared semgrep-scan action does not expose a per-rule path-exclude knob, so suppressing FPs from a registry pack at the consumer side isn't possible. Net effect: - Custom rules (.semgrep/rules/, 23 rules) still scan as before — 0 findings on this branch. - `p/ci`, `p/golang`, `p/gosec` packs still run, covering CI hardening, Go-stdlib weak-crypto/insecure-deserialization, and gosec G-series. - Secret detection coverage unchanged — handled exclusively by TruffleHog, which has verifier-backed FP filtering. Signed-off-by: Dmitrii Creed --- .github/workflows/semgrep.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index bc9572ff..9a5ec6ed 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -21,7 +21,13 @@ jobs: # After 14 days clean here, promote to the shared SC ruleset in # simple-container-com/actions/semgrep-scan/rules/. consumer-rules: '.semgrep/rules' - # Curated registry packs covering CI/secrets + Go + gosec G-series. + # Curated registry packs covering CI + Go + gosec G-series. + # `p/secrets` is intentionally NOT opted into: TruffleHog (see + # security-scan.yml) is our secret-detection source of truth and is + # already tuned with the `secret-scan-extra-excludes` list for our + # documented example-secret paths (docs/examples, testdata, .sc/ + # cfg.default.yaml). The Semgrep `p/secrets` pack would re-flag the + # same fixtures with no path-exclude knob exposed to consumers. # Pinned at the action level via Semgrep image digest in # semgrep-scan/action.yml; pack content is fetched at runtime. - registry-packs: 'p/ci,p/secrets,p/golang,p/gosec' + registry-packs: 'p/ci,p/golang,p/gosec' From 372686043825562aee7b26309aceaa94900ffdf0 Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Sat, 16 May 2026 20:46:54 +0400 Subject: [PATCH 6/6] =?UTF-8?q?chore(sast):=20drop=20local=20.semgrep/rule?= =?UTF-8?q?s=20=E2=80=94=20promoted=20to=20shared=20semgrep-scan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 22 custom Semgrep rules (sigstore, gha-extras, pulumi-iac, go-canon) that lived in .semgrep/rules/ have been promoted to the shared ruleset at simple-container-com/actions/semgrep-scan/rules/ via simple-container-com/actions#10. Once that lands on the actions repo's `main` branch, the rules are picked up here automatically via the `@main` ref on the reusable workflow — no `consumer-rules:` input needed. Short coverage gap until actions#10 merges: the registry packs (p/ci, p/golang, p/gosec) keep running; only the SC-specific rules pause. Re-enable on the actions side as soon as #10 is merged. Signed-off-by: Dmitrii Creed --- .github/workflows/semgrep.yml | 14 ++- .semgrep/rules/gha-extras.yml | 65 ------------- .semgrep/rules/go-canon.yml | 178 ---------------------------------- .semgrep/rules/pulumi-iac.yml | 92 ------------------ .semgrep/rules/sigstore.yml | 148 ---------------------------- 5 files changed, 6 insertions(+), 491 deletions(-) delete mode 100644 .semgrep/rules/gha-extras.yml delete mode 100644 .semgrep/rules/go-canon.yml delete mode 100644 .semgrep/rules/pulumi-iac.yml delete mode 100644 .semgrep/rules/sigstore.yml diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 9a5ec6ed..d445e07d 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -13,14 +13,12 @@ jobs: permissions: contents: read with: - # Consumer-rules: SC-api-specific Semgrep rules at .semgrep/rules/. - # Categories: sigstore (cosign/gh-attestation/attest-build-provenance - # supply-chain hardening), gha-extras (workflow-misuse), pulumi-iac - # (K8s + GCP + AWS-extra), go-canon (gosec G-series + dgryski/ - # semgrep-go misses CodeQL security-extended doesn't cover). - # After 14 days clean here, promote to the shared SC ruleset in - # simple-container-com/actions/semgrep-scan/rules/. - consumer-rules: '.semgrep/rules' + # The SC-api-specific Semgrep rules (sigstore, gha-extras, pulumi-iac, + # go-canon — 22 rules total) now live in the shared semgrep-scan + # ruleset at simple-container-com/actions/semgrep-scan/rules/ and + # are picked up automatically via the `@main` ref on the reusable + # workflow above. No `consumer-rules:` input needed. + # # Curated registry packs covering CI + Go + gosec G-series. # `p/secrets` is intentionally NOT opted into: TruffleHog (see # security-scan.yml) is our secret-detection source of truth and is diff --git a/.semgrep/rules/gha-extras.yml b/.semgrep/rules/gha-extras.yml deleted file mode 100644 index 5373a3fa..00000000 --- a/.semgrep/rules/gha-extras.yml +++ /dev/null @@ -1,65 +0,0 @@ -# GitHub Actions extras — workflow-misuse patterns that aren't in the -# OWASP CICD canon but are real attack surface for SC's threat model -# (parses customer secrets, runs in customer CI with elevated perms). - -rules: - - id: gha-workflow-level-secret-env - message: >- - Workflow-level `env:` block defines a variable with `secrets.*`. - That value leaks to EVERY step in EVERY job — including any - third-party action call inside the workflow. Scope the secret - to the specific job or step that needs it. - severity: ERROR - languages: [yaml] - paths: - include: [".github/workflows/*.yml", ".github/workflows/*.yaml"] - pattern-regex: '(?ms)\Aname:[^\n]*\n[\s\S]*?\non:[^\n]*\n[\s\S]*?\nenv:\s*\n(?:[ \t]+[^\n]+\n)*?[ \t]+[A-Z_][A-Z0-9_]*:\s*\$\{\{\s*secrets\.' - - - id: gha-upload-artifact-secrets-leak - message: >- - `actions/upload-artifact` path matches a credential / secret file - pattern (`.env`, `*.pem`, `*.key`, `secrets/**`). Artifacts are - retained for ≤ 90 days and downloadable by anyone with repo read - access — never publish credentials this way. - severity: ERROR - languages: [yaml] - paths: - include: [".github/workflows/*.yml", ".github/workflows/*.yaml"] - patterns: - - pattern-regex: 'uses:\s*actions/upload-artifact@' - - pattern-either: - - pattern-regex: 'path:\s*[''"]?[^\n]*\.env' - - pattern-regex: 'path:\s*[''"]?[^\n]*\.pem' - - pattern-regex: 'path:\s*[''"]?[^\n]*\.key' - - pattern-regex: 'path:\s*[''"]?[^\n]*secrets/' - - - id: gha-pull-request-target-implicit-base-ref - message: >- - `pull_request_target` defaults `actions/checkout` to the base - branch — but any step run after `checkout` with `ref:` set to a - `github.event.pull_request.head.*` value runs FORK code with the - target's elevated permissions (the canonical PPE vector). Pin - checkout explicitly to a trusted ref or remove `pull_request_target`. - severity: ERROR - languages: [yaml] - paths: - include: [".github/workflows/*.yml", ".github/workflows/*.yaml"] - patterns: - - pattern-regex: 'on:\s*\n\s*pull_request_target' - - pattern-regex: 'uses:\s*actions/checkout@' - - pattern-not-regex: 'uses:\s*actions/checkout@[^\n]*\n(?:\s+[^\n]+\n)*\s+ref:\s*\$\{\{\s*github\.event\.workflow_run' - - - id: gha-github-token-in-github-env - message: >- - Writing `GITHUB_TOKEN` (or a derived alias) to `$GITHUB_ENV` - persists the token in the runner's environment for the remainder - of the job — every subsequent step, including third-party actions - run via `uses:`, can read it. Pass tokens via per-step `env:` - mappings instead. - severity: ERROR - languages: [generic] - paths: - include: [".github/workflows/*.yml", ".github/workflows/*.yaml"] - pattern-either: - - pattern-regex: 'echo\s+["'']?[A-Z_]*TOKEN[A-Z_]*=\$\{?\{?\s*secrets\.GITHUB_TOKEN' - - pattern-regex: 'echo\s+["'']?[A-Z_]*TOKEN[A-Z_]*=[^\n]+>>\s*\$GITHUB_ENV' diff --git a/.semgrep/rules/go-canon.yml b/.semgrep/rules/go-canon.yml deleted file mode 100644 index b4a4b8cd..00000000 --- a/.semgrep/rules/go-canon.yml +++ /dev/null @@ -1,178 +0,0 @@ -# Go security canon — gosec G-series + dgryski/semgrep-go patterns -# that CodeQL's `security-extended` Go pack misses. Focused on the -# crypto + parsing surfaces in SC (secrets cache, attestation predicate -# parsing, OIDC JWT validation, downloaded-binary hardening). - -rules: - - id: go-hmac-not-constant-time - message: >- - HMAC / MAC comparison uses `bytes.Equal` or a non-constant-time - operator. The byte-comparison primitive leaks timing information - that lets an attacker reconstruct the expected MAC byte-by-byte. - Use `hmac.Equal` (or `crypto/subtle.ConstantTimeCompare`). - severity: ERROR - languages: [go] - paths: - include: ["**/*.go"] - exclude: ["**/*_test.go"] - patterns: - - pattern-either: - - pattern: bytes.Equal($A, $B) - - metavariable-regex: - metavariable: $A - regex: '(mac|hmac|HMAC|MAC|signature|sig|tag)' - - - id: go-cipher-deterministic-nonce - message: >- - AEAD cipher seal/open is invoked with a zero / constant nonce. The - whole point of an AEAD is per-message nonce uniqueness; reusing a - nonce under the same key catastrophically breaks confidentiality - and integrity (especially for GCM / ChaCha20-Poly1305). Use - `crypto/rand.Read(nonce[:])` per message. - severity: ERROR - languages: [go] - paths: - include: ["**/*.go"] - exclude: ["**/*_test.go"] - pattern-either: - - pattern: | - $NONCE := make([]byte, $AEAD.NonceSize()) - ... - $AEAD.Seal($DST, $NONCE, $PLAINTEXT, $AAD) - - pattern: | - $NONCE := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - ... - $AEAD.Seal(...) - - # Note: a `go-yaml-unmarshal-into-interface` rule was drafted and ran in - # round 5 (21 findings) but was DROPPED in round 6 after triage: SC's - # untyped-unmarshal sites are intentional (generic YAML inspection for - # diff/obfuscation/AI-analysis tooling, where the result isn't used for - # security decisions). Taint-aware coverage for the cases that DO matter - # (untrusted-source flow → unmarshal) lives in the `p/golang` registry - # pack opted into via .github/workflows/semgrep.yml. - - - id: go-jwt-parse-unverified - message: >- - `jwt.ParseUnverified` skips signature validation entirely. Even - "just reading claims" requires verification because the claims - themselves are attacker-controlled until the signature is checked. - Use `jwt.Parse(..., keyfunc)` + `WithValidMethods` + audience and - issuer checks. - severity: ERROR - languages: [go] - paths: - include: ["**/*.go"] - exclude: ["**/*_test.go"] - pattern-either: - - pattern: jwt.ParseUnverified(...) - - pattern: $JWT.ParseUnverified(...) - - - id: go-jwt-none-algorithm - message: >- - JWT verification accepts the `none` algorithm. A `none`-algorithm - JWT has no signature; any caller can claim any identity. Restrict - with `WithValidMethods([]string{"RS256", "ES256", ...})` or the - v5+ `WithValidAlg` option. - severity: ERROR - languages: [go] - paths: - include: ["**/*.go"] - exclude: ["**/*_test.go"] - pattern-either: - - pattern-regex: 'jwt\.SigningMethod(?:HS|RS|ES|PS)?None' - - pattern-regex: '["'']alg["'']\s*:\s*["'']none["'']' - - - id: go-http-client-no-timeout - message: >- - `&http.Client{}` literal does not set `Timeout`. Default is "no - timeout" — a malicious or slow upstream hangs the request - indefinitely, exhausting goroutines/file descriptors (slowloris - class DoS). Set an explicit `Timeout: 30 * time.Second` (or a - reasonable bound for the caller's use case). - severity: WARNING - languages: [go] - paths: - include: ["**/*.go"] - exclude: ["**/*_test.go"] - patterns: - - pattern: '&http.Client{...}' - - pattern-not: '&http.Client{..., Timeout: ..., ...}' - - - id: go-tls-min-version-below-1-2 - message: >- - `tls.Config{MinVersion: ...}` below `tls.VersionTLS12` allows - vulnerable TLS 1.0 / 1.1 handshakes. Modern stacks should pin - `MinVersion: tls.VersionTLS13` (or 1.2 as the floor). - severity: ERROR - languages: [go] - paths: - include: ["**/*.go"] - exclude: ["**/*_test.go"] - pattern-either: - - pattern: "tls.Config{..., MinVersion: tls.VersionTLS10, ...}" - - pattern: "tls.Config{..., MinVersion: tls.VersionTLS11, ...}" - - pattern: "tls.Config{..., MinVersion: 0x0301, ...}" - - pattern: "tls.Config{..., MinVersion: 0x0302, ...}" - - - id: go-exec-command-inherits-environ-default - message: >- - `exec.Command(...).Run()` or `.Output()` without setting `cmd.Env` - inherits the parent process environment — in a CI runner that - includes every secret injected as an env var. Pass an explicit - filtered `cmd.Env = []string{...}`. The standard pattern is - `cmd.Env = append(os.Environ()[:0:0], "PATH=" + os.Getenv("PATH"))`. - severity: WARNING - languages: [go] - paths: - include: ["**/*.go"] - exclude: - - "**/*_test.go" - # pkg/security/{sbom,scan,tools} is the SC binary's intentional - # design surface for invoking external scanners (syft, trivy, - # grype) and installing toolchain — env inheritance is required - # so the scanners pick up PATH / HOME / GOPATH. These are not - # security-decision call sites. - - "pkg/security/sbom/**/*.go" - - "pkg/security/scan/**/*.go" - - "pkg/security/tools/**/*.go" - # Terminal control invocations of stty have no env-relevant - # impact and should not be flagged. - - "pkg/assistant/chat/input.go" - # git-status / git-remote read-only invocations need GIT_* - # env propagation to honor user gitconfig. - - "pkg/assistant/analysis/git_analyzer.go" - - "pkg/assistant/cicd/utils.go" - patterns: - - pattern-either: - - pattern: | - $CMD := exec.Command($ARG, ...) - ... - $CMD.Run() - - pattern: | - $CMD := exec.Command($ARG, ...) - ... - $CMD.Output() - - pattern: | - $CMD := exec.CommandContext($CTX, $ARG, ...) - ... - $CMD.Run() - - pattern-not: | - $CMD := exec.Command(...) - ... - $CMD.Env = ... - ... - - pattern-not: | - $CMD := exec.CommandContext(...) - ... - $CMD.Env = ... - ... - # Skip trusted-CLI invocations: these require env propagation by - # design (git needs HOME/GIT_*; stty needs TERM; gh needs GH_TOKEN; - # cosign/syft/trivy/grype need PATH). Threat-model is about - # untrusted-binary invocation, not all exec.Command usage. - - metavariable-pattern: - metavariable: $ARG - patterns: - - pattern-regex: '.' - - pattern-not-regex: '"(git|stty|gh|cosign|syft|trivy|grype|kubectl|docker|pulumi|sc|welder)"' diff --git a/.semgrep/rules/pulumi-iac.yml b/.semgrep/rules/pulumi-iac.yml deleted file mode 100644 index dcecabe0..00000000 --- a/.semgrep/rules/pulumi-iac.yml +++ /dev/null @@ -1,92 +0,0 @@ -# Pulumi-Go IaC hardening — SC provisions parent/client stacks on AWS, -# GCP, and Kubernetes. The existing SC ruleset only covers AWS RDS -# encryption. These rules expand to GCP + Kubernetes baselines. -# Ports of Checkov / Kubesec patterns to Pulumi-Go's API surface. - -rules: - - id: go-pulumi-k8s-privileged-workload - message: >- - Pulumi-Kubernetes Pod/Container is provisioned with a privilege- - escalation knob (`Privileged: true`, `HostNetwork: true`, - `HostPID: true`, or a hostPath volume mount). On a multi-tenant - cluster this allows breakout to the node. Drop the privilege OR - document the workload as cluster-singleton with an explicit - threat-model justification. - severity: ERROR - languages: [go] - paths: - include: ["**/*.go"] - pattern-either: - - pattern: | - $ARGS = corev1.SecurityContextArgs{..., Privileged: pulumi.Bool(true), ...} - - pattern: | - $ARGS = corev1.PodSpecArgs{..., HostNetwork: pulumi.Bool(true), ...} - - pattern: | - $ARGS = corev1.PodSpecArgs{..., HostPID: pulumi.Bool(true), ...} - - pattern: | - $V = corev1.VolumeArgs{..., HostPath: $HP, ...} - - - id: go-pulumi-gcp-storage-public-access - message: >- - GCP Storage Bucket / BucketIAMMember allows public-internet read - (`allUsers` or `allAuthenticatedUsers` member) OR disables - Uniform Bucket-Level Access. Even if the bucket holds non-sensitive - data the public-ACL surface is a CIS GCP audit failure (CKV_GCP_28, - CKV_GCP_29). Use a private bucket + signed URLs for public access. - severity: ERROR - languages: [go] - paths: - include: ["**/*.go"] - pattern-either: - - pattern: | - $M = storage.BucketIAMMemberArgs{..., Member: pulumi.String("allUsers"), ...} - - pattern: | - $M = storage.BucketIAMMemberArgs{..., Member: pulumi.String("allAuthenticatedUsers"), ...} - - pattern: | - $B = storage.BucketArgs{..., UniformBucketLevelAccess: pulumi.Bool(false), ...} - - - id: go-pulumi-gcp-compute-default-sa-cloud-platform - message: >- - GCE VM uses the default Compute Engine service account paired with - the broad `cloud-platform` OAuth scope. An attacker with RCE on - the VM can call ANY GCP API as the default SA — full project - takeover. Bind the VM to a least-privilege custom service account - and use narrow scopes (or `cloud-platform` only on the custom SA). - severity: ERROR - languages: [go] - paths: - include: ["**/*.go"] - patterns: - - pattern: | - $A = compute.InstanceArgs{..., ServiceAccount: $SA, ...} - - pattern-regex: 'compute\.InstanceArgs\{[\s\S]{0,400}ServiceAccount:[\s\S]{0,200}Scopes:[\s\S]{0,200}cloud-platform' - - - id: go-pulumi-aws-sg-open-ingress - message: >- - AWS SecurityGroup or SecurityGroupRule with ingress `CidrBlocks` - containing `0.0.0.0/0` (any source). For non-HTTPS / non-HTTP - protocols this is a CIS AWS Foundation control failure (CKV_AWS_24 - / CKV_AWS_260). Restrict to the source CIDR the workload actually - needs. - severity: ERROR - languages: [go] - paths: - include: ["**/*.go"] - pattern-either: - - pattern: | - $A = ec2.SecurityGroupArgs{..., Ingress: $I, ...} - - pattern-regex: 'CidrBlocks:[\s]*pulumi\.StringArray\{[^\}]*"0\.0\.0\.0/0"' - - - id: go-pulumi-iam-wildcard-policy - message: >- - IAM policy statement with `Action: "*"` and/or `Resource: "*"`. - Grants god-mode access — never appropriate outside narrowly-scoped - break-glass automation. Document or split into least-privilege - grants. - severity: ERROR - languages: [go] - paths: - include: ["**/*.go"] - pattern-either: - - pattern-regex: '"Action":\s*"\*"' - - pattern-regex: '"Resource":\s*"\*"[^\}]*"Effect":\s*"Allow"' diff --git a/.semgrep/rules/sigstore.yml b/.semgrep/rules/sigstore.yml deleted file mode 100644 index a69b9844..00000000 --- a/.semgrep/rules/sigstore.yml +++ /dev/null @@ -1,148 +0,0 @@ -# Sigstore / cosign / attest-build-provenance hardening — supply-chain core. -# These rules guard the verification surface that SC's Phase 2 attestation -# pipeline (PR #257) ships. Most are regex-anchored to the exact CLI flags -# the documented consumer verification flow uses. - -rules: - - id: gha-cosign-verify-without-identity-flag - message: >- - `cosign verify[-blob|-attestation]` invocation is missing - `--certificate-identity` (or `--certificate-identity-regexp`). - Without identity binding the call accepts any keyless signature - from any workflow in the Sigstore transparency log — defeats - trust-root separation. See docs/SECURITY.md "Identity-regex contract". - severity: ERROR - languages: [yaml] - paths: - include: - - ".github/workflows/*.yml" - - ".github/workflows/*.yaml" - - ".github/actions/**/action.yml" - - ".github/actions/**/action.yaml" - # Match a `run: |` block that contains `cosign verify*` AND lacks - # `--certificate-identity` anywhere in the block body. - patterns: - - pattern: | - run: | - $BODY - - metavariable-regex: - metavariable: $BODY - regex: '\bcosign\s+verify(?:-blob|-attestation)?\b' - - metavariable-pattern: - metavariable: $BODY - patterns: - - pattern-regex: '.' - - pattern-not-regex: '--certificate-identity' - - - id: gha-cosign-verify-without-oidc-issuer-flag - message: >- - `cosign verify[-blob|-attestation]` invocation is missing - `--certificate-oidc-issuer`. Required to bind the keyless signer - to a specific OIDC provider (almost always - `https://token.actions.githubusercontent.com` for SC). - severity: ERROR - languages: [yaml] - paths: - include: - - ".github/workflows/*.yml" - - ".github/workflows/*.yaml" - - ".github/actions/**/action.yml" - - ".github/actions/**/action.yaml" - patterns: - - pattern: | - run: | - $BODY - - metavariable-regex: - metavariable: $BODY - regex: '\bcosign\s+verify(?:-blob|-attestation)?\b' - - metavariable-pattern: - metavariable: $BODY - patterns: - - pattern-regex: '.' - - pattern-not-regex: '--certificate-oidc-issuer' - - - id: gha-cosign-verify-insecure-flags - message: >- - `cosign verify*` invoked with transparency-log bypass flags - (`--insecure-ignore-tlog`, `--insecure-ignore-sct`). These flags - DISABLE the Rekor inclusion proof that backs keyless verification — - a signature would verify even if it was never published to the - transparency log. Never use in CI or in documented consumer commands. - severity: ERROR - languages: [generic] - paths: - include: ["**/*.yml", "**/*.yaml", "**/*.sh", "**/*.md"] - pattern-either: - - pattern-regex: 'cosign\s+verify\S*[^\n]*--insecure-ignore-tlog' - - pattern-regex: 'cosign\s+verify\S*[^\n]*--insecure-ignore-sct' - - - id: gha-gh-attestation-missing-scope - message: >- - `gh attestation verify` requires `--owner ` OR `--repo - /` to bind the attestation lookup to a trusted scope. - Add `--cert-identity-regex` + `--cert-oidc-issuer` to match the - strict identity contract documented in docs/SECURITY.md. - severity: ERROR - languages: [yaml] - paths: - include: - - ".github/workflows/*.yml" - - ".github/workflows/*.yaml" - - ".github/actions/**/action.yml" - - ".github/actions/**/action.yaml" - # Whole-`run:`-block matching — find `gh attestation verify` and require - # `--repo` or `--owner` in the same block. - patterns: - - pattern: | - run: | - $BODY - - metavariable-regex: - metavariable: $BODY - regex: '\bgh\s+attestation\s+verify\b' - - metavariable-pattern: - metavariable: $BODY - patterns: - - pattern-regex: '.' - - pattern-not-regex: '--(repo|owner)\s' - - - id: gha-attest-build-provenance-mutable-subject - message: >- - `actions/attest-build-provenance` must publish provenance bound to - an immutable subject. Using `subject-name` with a mutable tag - (`:latest`, `:staging`) without a corresponding `subject-digest` - yields a provenance that "verifies" against whatever the tag - currently points to — defeats the SLSA Build L3 non-falsifiability - property. Always pass `subject-digest:`. - severity: ERROR - languages: [yaml] - paths: - include: [".github/workflows/*.yml", ".github/workflows/*.yaml"] - pattern-either: - - pattern: | - uses: actions/attest-build-provenance@... - with: - ... - subject-name: "$REPO:latest" - - pattern: | - uses: actions/attest-build-provenance@... - with: - ... - subject-name: "$REPO:staging" - - - id: gha-oidc-id-token-on-untrusted-trigger - message: >- - A job with `permissions: id-token: write` is reachable from an - untrusted-input trigger (`pull_request_target`, `workflow_run`, or - `pull_request` without explicit gating). OIDC token-grant from a - fork PR can mint a Sigstore identity that the SC trust-root regex - accepts. Either remove `id-token: write` from the job or split the - workflow so signing jobs run only on trusted refs. - severity: ERROR - languages: [yaml] - paths: - include: [".github/workflows/*.yml", ".github/workflows/*.yaml"] - patterns: - - pattern-either: - - pattern-regex: 'on:\s*\n\s*pull_request_target' - - pattern-regex: 'on:\s*\n\s*workflow_run' - - pattern-regex: 'id-token:\s*write'