From 3f0446f1905b03192ab868374dd1efcb6abe04d7 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Thu, 16 Jul 2026 16:55:28 +0300 Subject: [PATCH 1/3] ci(pre-commit): pinned gosec hook on changed packages (closes #1374) Replace the whole-repo gosec invocation with a per-module, per-changed-package script (scripts/gosec-hook.sh) that: - Pins gosec v2.26.1 (matches securego/gosec SHA in ci.yml); auto-installs to ~/.cache/pre-commit-gosec/v2.26.1/gosec on first use via go install, detected via go version -m so the "dev" string from go install does not fool the version check. - Accepts staged .go file paths from pre-commit (pass_filenames: true), resolves each to its owning Go module (root / pkg / providers/aws / providers/azure / providers/gcp), then runs gosec once per affected module scanning only the packages that contain changed files. - Skips deleted files and testdata/ paths cleanly. - Same -exclude= rule list as the previous hook; keeps local and CI pre-commit verdicts aligned. - Exits nonzero on any finding; exits 0 when no live .go files are staged. Tested: clean pass on cmd/ + providers/aws/ in ~1.4 s; deliberate G501 (crypto/md5 import) correctly fails with exit 1; clean pass restored after removing the scratch file. --- .pre-commit-config.yaml | 31 +++----- scripts/gosec-hook.sh | 160 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+), 19 deletions(-) create mode 100755 scripts/gosec-hook.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 285e98ba5..4c5b83dfb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -108,26 +108,19 @@ repos: types: [file] - id: gosec - name: Go security scanner - # Exclusion rationale: - # G101: False positives on variable names containing "password/secret/token" - # G104: Unchecked errors — covered by go vet and golangci-lint errcheck - # G115: Integer overflow — false positives on safe conversions (e.g. int to int32) - # G117: Use of unsafe pointer arithmetic — pre-existing in vendor/generated code - # G118: net/http serve without timeout — pre-existing; timeouts set at handler level - # G122: Use of unsafe operations — pre-existing in low-level helpers - # G204: Subprocess launched with variable — CLI tool needs dynamic commands - # G301: Directory created with permissions > 0750 — acceptable for dev tooling - # G304: File path from variable — CLI tool reads user-specified file paths - # G402: TLS MinVersion not set — handled by cloud SDK defaults - # G505: Import of crypto/sha1 — not used for security, only checksums - # G702: TLS InsecureSkipVerify — pre-existing in test helpers only - # G703: Errors unhandled in defer — pre-existing; deferred close errors logged separately - # G705: Errors unhandled in goroutine — pre-existing pattern - # G706: Errors ignored — pre-existing; covered by go vet errcheck - entry: bash -c 'gosec -quiet -exclude-dir=.legacy -exclude-dir=.dev-notes -exclude-dir=vendor -exclude=G101,G104,G115,G117,G118,G122,G204,G301,G304,G402,G505,G702,G703,G705,G706 ./...' + name: Go security scanner (per-module, per-changed-package) + # Scans only the Go packages that contain staged files, resolved to + # their owning module (root, pkg/, providers/aws, providers/azure, + # providers/gcp). Fast: never whole-repo, always per-changed-package. + # + # gosec v2.26.1 is auto-installed to + # ~/.cache/pre-commit-gosec/v2.26.1/gosec on first use. + # + # Exclusion rationale and flag list live in scripts/gosec-hook.sh. + # Keep in sync with the exclude= flags there. + entry: bash scripts/gosec-hook.sh language: system - pass_filenames: false + pass_filenames: true files: \.go$ - id: trivy-config diff --git a/scripts/gosec-hook.sh b/scripts/gosec-hook.sh new file mode 100755 index 000000000..c2c542510 --- /dev/null +++ b/scripts/gosec-hook.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +# scripts/gosec-hook.sh +# +# Pre-commit gosec hook: runs gosec on only the Go packages that contain staged +# files, resolved to their owning module. Fast by design: never scans the whole +# repo; each commit triggers at most one gosec invocation per affected module. +# +# Version pin: gosec v2.26.1 (matches the securego/gosec SHA pin in ci.yml). +# Installed on first use to ~/.cache/pre-commit-gosec/v2.26.1/gosec; never +# modifies the system-wide gosec binary. +# +# Called by pre-commit with pass_filenames: true and files: \.go$. +# Exits 0 when no staged .go files survive filtering (deleted / testdata). +# Exits 1 on any gosec finding; exits 2 on setup failure. +# +# Exclusion rationale (kept in sync with .pre-commit-config.yaml): +# G101 - variable names containing password/secret/token -> false positives +# G104 - unchecked errors -> covered by errcheck +# G115 - integer overflow -> safe conversions flagged +# G117 - unsafe pointer arithmetic -> vendor/generated code +# G118 - net/http serve without timeout -> timeouts set at handler level +# G122 - unsafe operations -> low-level helpers, pre-existing +# G204 - subprocess with variable -> CLI tool needs dynamic commands +# G301 - dir permissions > 0750 -> acceptable for dev tooling +# G304 - file path from variable -> CLI reads user-specified paths +# G402 - TLS MinVersion not set -> handled by cloud SDK defaults +# G505 - import of crypto/sha1 -> checksums, not security primitives +# G702 - TLS InsecureSkipVerify -> test helpers only, pre-existing +# G703 - unhandled defer error -> deferred close errors logged separately +# G705 - unhandled goroutine error -> pre-existing pattern +# G706 - ignored errors -> pre-existing; covered by go vet errcheck + +set -euo pipefail + +GOSEC_VERSION="2.26.1" +GOSEC_BIN="${HOME}/.cache/pre-commit-gosec/v${GOSEC_VERSION}/gosec" + +GOSEC_EXCLUDE="G101,G104,G115,G117,G118,G122,G204,G301,G304,G402,G505,G702,G703,G705,G706" + +# Module roots in longest-prefix order (so "providers/azure" is checked before +# a hypothetical "providers" root). +MODULE_DIRS="providers/azure providers/aws providers/gcp pkg" + +# ---- helpers ---------------------------------------------------------------- + +ensure_gosec() { + local need_install=0 + if [[ -x "$GOSEC_BIN" ]]; then + # gosec built via `go install` embeds "dev" in -h regardless of tag; + # read the real module version from the binary's build info instead. + local installed_ver + installed_ver=$(go version -m "$GOSEC_BIN" 2>/dev/null \ + | awk '$1=="mod" && $2~/gosec/{print $3}') + # installed_ver is e.g. "v2.26.1"; compare against "v$GOSEC_VERSION". + if [[ "$installed_ver" != "v${GOSEC_VERSION}" ]]; then + echo "pre-commit/gosec: cached binary is ${installed_ver:-unknown}, need v${GOSEC_VERSION}; reinstalling" >&2 + need_install=1 + fi + else + need_install=1 + fi + + if [[ $need_install -eq 1 ]]; then + echo "pre-commit/gosec: installing gosec@v${GOSEC_VERSION} -> $(dirname "$GOSEC_BIN")" >&2 + mkdir -p "$(dirname "$GOSEC_BIN")" + GOBIN="$(dirname "$GOSEC_BIN")" go install \ + "github.com/securego/gosec/v2/cmd/gosec@v${GOSEC_VERSION}" || { + echo "pre-commit/gosec: install failed (is Go on PATH?)" >&2 + exit 2 + } + fi +} + +# Print the module root (relative to repo root) that owns a given relative file +# path, or empty string when the file belongs to the root module. +module_for() { + local f="$1" mod + for mod in $MODULE_DIRS; do + case "$f" in + "$mod"/*) printf '%s' "$mod"; return ;; + esac + done + printf '' +} + +# ---- main ------------------------------------------------------------------- + +[[ $# -eq 0 ]] && exit 0 + +REPO_ROOT="$(git rev-parse --show-toplevel)" + +# Filter: skip deleted files and files under testdata/. +live_files=() +for f in "$@"; do + [[ -f "$f" ]] || continue + case "$f" in + */testdata/*) continue ;; + testdata/*) continue ;; + esac + live_files+=("$f") +done + +[[ ${#live_files[@]} -eq 0 ]] && exit 0 + +ensure_gosec + +# Build "module|package" pairs from the live file list. +# Package path is relative to the owning module root, in ./pkg notation. +pairs_raw=() +for f in "${live_files[@]}"; do + mod=$(module_for "$f") + pkg_dir=$(dirname "$f") + + if [[ -n "$mod" ]]; then + # Strip the module prefix (plus the separating slash). + rel="${pkg_dir:$((${#mod}+1))}" + [[ -z "$rel" ]] && rel="." # file sits directly in the module root + else + rel="$pkg_dir" # root module; pkg_dir is already relative + fi + + # Normalise to go-tool notation. + if [[ "$rel" == "." ]]; then + pkg="." + else + pkg="./$rel" + fi + + pairs_raw+=("${mod}|${pkg}") +done + +# Deduplicate. +pairs_sorted=$(printf '%s\n' "${pairs_raw[@]}" | sort -u) + +# Enumerate unique module roots. +mods=$(printf '%s\n' "$pairs_sorted" | cut -d'|' -f1 | sort -u) + +fail=0 +while IFS= read -r mod; do + pkgs=$(printf '%s\n' "$pairs_sorted" \ + | awk -F'|' -v m="$mod" '$1==m{print $2}' \ + | tr '\n' ' ') + mod_dir="${REPO_ROOT}${mod:+/${mod}}" + + echo "gosec [${mod:-.}]: scanning package(s): $pkgs" >&2 + + # pkgs intentionally unquoted: space-separated package paths, no glob chars. + # shellcheck disable=SC2086 + if ! (cd "$mod_dir" && "$GOSEC_BIN" \ + -quiet \ + -exclude-dir=.legacy \ + -exclude-dir=.dev-notes \ + -exclude-dir=vendor \ + "-exclude=${GOSEC_EXCLUDE}" \ + $pkgs); then + fail=1 + fi +done <<< "$mods" + +exit $fail From aa67dc6b7110f4bf5612c00568c604999b55c5ec Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Thu, 16 Jul 2026 19:55:56 +0300 Subject: [PATCH 2/3] ci(pre-commit): bump gosec pin to v2.28.0 to match CI (#1384) PR #1384 bumps the CI Security Scanning gosec pin to v2.28.0; align the pre-commit hook so local and CI verdicts stay in agreement. Updates the GOSEC_VERSION pin in scripts/gosec-hook.sh and the doc mentions in the hook config comment. Re-verified with v2.28.0: auto-install to the versioned cache dir works; clean pass on cmd/ + pkg/ + providers/aws in ~1.6 s warm; deliberate G501 (crypto/md5) scratch file fails with exit 1; clean pass restored after removal. --- .pre-commit-config.yaml | 4 ++-- scripts/gosec-hook.sh | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4c5b83dfb..43ab8887f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -113,8 +113,8 @@ repos: # their owning module (root, pkg/, providers/aws, providers/azure, # providers/gcp). Fast: never whole-repo, always per-changed-package. # - # gosec v2.26.1 is auto-installed to - # ~/.cache/pre-commit-gosec/v2.26.1/gosec on first use. + # gosec v2.28.0 is auto-installed to + # ~/.cache/pre-commit-gosec/v2.28.0/gosec on first use. # # Exclusion rationale and flag list live in scripts/gosec-hook.sh. # Keep in sync with the exclude= flags there. diff --git a/scripts/gosec-hook.sh b/scripts/gosec-hook.sh index c2c542510..c0029d4b2 100755 --- a/scripts/gosec-hook.sh +++ b/scripts/gosec-hook.sh @@ -5,8 +5,8 @@ # files, resolved to their owning module. Fast by design: never scans the whole # repo; each commit triggers at most one gosec invocation per affected module. # -# Version pin: gosec v2.26.1 (matches the securego/gosec SHA pin in ci.yml). -# Installed on first use to ~/.cache/pre-commit-gosec/v2.26.1/gosec; never +# Version pin: gosec v2.28.0 (matches the CI pin in ci.yml, bumped by #1384). +# Installed on first use to ~/.cache/pre-commit-gosec/v2.28.0/gosec; never # modifies the system-wide gosec binary. # # Called by pre-commit with pass_filenames: true and files: \.go$. @@ -32,7 +32,7 @@ set -euo pipefail -GOSEC_VERSION="2.26.1" +GOSEC_VERSION="2.28.0" GOSEC_BIN="${HOME}/.cache/pre-commit-gosec/v${GOSEC_VERSION}/gosec" GOSEC_EXCLUDE="G101,G104,G115,G117,G118,G122,G204,G301,G304,G402,G505,G702,G703,G705,G706" @@ -51,7 +51,7 @@ ensure_gosec() { local installed_ver installed_ver=$(go version -m "$GOSEC_BIN" 2>/dev/null \ | awk '$1=="mod" && $2~/gosec/{print $3}') - # installed_ver is e.g. "v2.26.1"; compare against "v$GOSEC_VERSION". + # installed_ver is e.g. "v2.28.0"; compare against "v$GOSEC_VERSION". if [[ "$installed_ver" != "v${GOSEC_VERSION}" ]]; then echo "pre-commit/gosec: cached binary is ${installed_ver:-unknown}, need v${GOSEC_VERSION}; reinstalling" >&2 need_install=1 From aabd9570638b35b87320da0150935ae0323bc9f3 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Thu, 16 Jul 2026 21:08:13 +0300 Subject: [PATCH 3/3] ci(pre-commit): register tests/e2e module in gosec hook; fix sp.tf EOF Address the Major CR finding on #1376 and the red pre-commit CI job. - scripts/gosec-hook.sh: add tests/e2e to MODULE_DIRS. It is a standalone Go module (tests/e2e/go.mod) but the hook only knew providers/* and pkg, so staged files there were scanned from the repo root as ./tests/e2e, breaking module/package resolution. Now mirrors the per-module loop in ci.yml (root, pkg, providers/{aws,azure,gcp}, tests/e2e). - terraform/environments/azure/ci-cd-permissions/sp.tf: strip the extra trailing blank line so the file ends with a single newline. The end-of-file-fixer pre-commit hook runs --all-files in CI and flagged this pre-existing whitespace on every PR. The pre-commit gocyclo failure is pre-existing repo-wide debt (four AWS service-client functions at complexity 11, below golangci's threshold of 15 but above the hook's -over 10). Not touched by this PR; tracked in the follow-up issue. --- scripts/gosec-hook.sh | 6 ++++-- terraform/environments/azure/ci-cd-permissions/sp.tf | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/gosec-hook.sh b/scripts/gosec-hook.sh index c0029d4b2..d6ba76709 100755 --- a/scripts/gosec-hook.sh +++ b/scripts/gosec-hook.sh @@ -38,8 +38,10 @@ GOSEC_BIN="${HOME}/.cache/pre-commit-gosec/v${GOSEC_VERSION}/gosec" GOSEC_EXCLUDE="G101,G104,G115,G117,G118,G122,G204,G301,G304,G402,G505,G702,G703,G705,G706" # Module roots in longest-prefix order (so "providers/azure" is checked before -# a hypothetical "providers" root). -MODULE_DIRS="providers/azure providers/aws providers/gcp pkg" +# a hypothetical "providers" root). Must mirror the per-module loop in +# .github/workflows/ci.yml (root, pkg, providers/{aws,azure,gcp}, tests/e2e) +# so a changed file under tests/e2e is scanned from within its own module. +MODULE_DIRS="tests/e2e providers/azure providers/aws providers/gcp pkg" # ---- helpers ---------------------------------------------------------------- diff --git a/terraform/environments/azure/ci-cd-permissions/sp.tf b/terraform/environments/azure/ci-cd-permissions/sp.tf index 025d79b29..aabba937e 100644 --- a/terraform/environments/azure/ci-cd-permissions/sp.tf +++ b/terraform/environments/azure/ci-cd-permissions/sp.tf @@ -47,4 +47,3 @@ resource "azuread_application_federated_identity_credential" "github_pr" { issuer = "https://token.actions.githubusercontent.com" subject = "repo:${var.github_repo}:pull_request" } -