Skip to content

Commit be4ad42

Browse files
authored
Merge pull request #1376 from LeanerCloud/ci/pre-commit-gosec-hook
ci(pre-commit): pinned gosec hook on changed packages (closes #1374)
2 parents 8240455 + 566a509 commit be4ad42

2 files changed

Lines changed: 174 additions & 19 deletions

File tree

‎.pre-commit-config.yaml‎

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -108,26 +108,19 @@ repos:
108108
types: [file]
109109

110110
- id: gosec
111-
name: Go security scanner
112-
# Exclusion rationale:
113-
# G101: False positives on variable names containing "password/secret/token"
114-
# G104: Unchecked errors — covered by go vet and golangci-lint errcheck
115-
# G115: Integer overflow — false positives on safe conversions (e.g. int to int32)
116-
# G117: Use of unsafe pointer arithmetic — pre-existing in vendor/generated code
117-
# G118: net/http serve without timeout — pre-existing; timeouts set at handler level
118-
# G122: Use of unsafe operations — pre-existing in low-level helpers
119-
# G204: Subprocess launched with variable — CLI tool needs dynamic commands
120-
# G301: Directory created with permissions > 0750 — acceptable for dev tooling
121-
# G304: File path from variable — CLI tool reads user-specified file paths
122-
# G402: TLS MinVersion not set — handled by cloud SDK defaults
123-
# G505: Import of crypto/sha1 — not used for security, only checksums
124-
# G702: TLS InsecureSkipVerify — pre-existing in test helpers only
125-
# G703: Errors unhandled in defer — pre-existing; deferred close errors logged separately
126-
# G705: Errors unhandled in goroutine — pre-existing pattern
127-
# G706: Errors ignored — pre-existing; covered by go vet errcheck
128-
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 ./...'
111+
name: Go security scanner (per-module, per-changed-package)
112+
# Scans only the Go packages that contain staged files, resolved to
113+
# their owning module (root, pkg/, providers/aws, providers/azure,
114+
# providers/gcp). Fast: never whole-repo, always per-changed-package.
115+
#
116+
# gosec v2.28.0 is auto-installed to
117+
# ~/.cache/pre-commit-gosec/v2.28.0/gosec on first use.
118+
#
119+
# Exclusion rationale and flag list live in scripts/gosec-hook.sh.
120+
# Keep in sync with the exclude= flags there.
121+
entry: bash scripts/gosec-hook.sh
129122
language: system
130-
pass_filenames: false
123+
pass_filenames: true
131124
files: \.go$
132125

133126
- id: trivy-config

‎scripts/gosec-hook.sh‎

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#!/usr/bin/env bash
2+
# scripts/gosec-hook.sh
3+
#
4+
# Pre-commit gosec hook: runs gosec on only the Go packages that contain staged
5+
# files, resolved to their owning module. Fast by design: never scans the whole
6+
# repo; each commit triggers at most one gosec invocation per affected module.
7+
#
8+
# Version pin: gosec v2.28.0 (matches the CI pin in ci.yml, bumped by #1384).
9+
# Installed on first use to ~/.cache/pre-commit-gosec/v2.28.0/gosec; never
10+
# modifies the system-wide gosec binary.
11+
#
12+
# Called by pre-commit with pass_filenames: true and files: \.go$.
13+
# Exits 0 when no staged .go files survive filtering (deleted / testdata).
14+
# Exits 1 on any gosec finding; exits 2 on setup failure.
15+
#
16+
# Exclusion rationale (kept in sync with .pre-commit-config.yaml):
17+
# G101 - variable names containing password/secret/token -> false positives
18+
# G104 - unchecked errors -> covered by errcheck
19+
# G115 - integer overflow -> safe conversions flagged
20+
# G117 - unsafe pointer arithmetic -> vendor/generated code
21+
# G118 - net/http serve without timeout -> timeouts set at handler level
22+
# G122 - unsafe operations -> low-level helpers, pre-existing
23+
# G204 - subprocess with variable -> CLI tool needs dynamic commands
24+
# G301 - dir permissions > 0750 -> acceptable for dev tooling
25+
# G304 - file path from variable -> CLI reads user-specified paths
26+
# G402 - TLS MinVersion not set -> handled by cloud SDK defaults
27+
# G505 - import of crypto/sha1 -> checksums, not security primitives
28+
# G702 - TLS InsecureSkipVerify -> test helpers only, pre-existing
29+
# G703 - unhandled defer error -> deferred close errors logged separately
30+
# G705 - unhandled goroutine error -> pre-existing pattern
31+
# G706 - ignored errors -> pre-existing; covered by go vet errcheck
32+
33+
set -euo pipefail
34+
35+
GOSEC_VERSION="2.28.0"
36+
GOSEC_BIN="${HOME}/.cache/pre-commit-gosec/v${GOSEC_VERSION}/gosec"
37+
38+
GOSEC_EXCLUDE="G101,G104,G115,G117,G118,G122,G204,G301,G304,G402,G505,G702,G703,G705,G706"
39+
40+
# Module roots in longest-prefix order (so "providers/azure" is checked before
41+
# a hypothetical "providers" root). Must mirror the per-module loop in
42+
# .github/workflows/ci.yml (root, pkg, providers/{aws,azure,gcp}, tests/e2e)
43+
# so a changed file under tests/e2e is scanned from within its own module.
44+
MODULE_DIRS="tests/e2e providers/azure providers/aws providers/gcp pkg"
45+
46+
# ---- helpers ----------------------------------------------------------------
47+
48+
ensure_gosec() {
49+
local need_install=0
50+
if [[ -x "$GOSEC_BIN" ]]; then
51+
# gosec built via `go install` embeds "dev" in -h regardless of tag;
52+
# read the real module version from the binary's build info instead.
53+
local installed_ver
54+
installed_ver=$(go version -m "$GOSEC_BIN" 2>/dev/null \
55+
| awk '$1=="mod" && $2~/gosec/{print $3}')
56+
# installed_ver is e.g. "v2.28.0"; compare against "v$GOSEC_VERSION".
57+
if [[ "$installed_ver" != "v${GOSEC_VERSION}" ]]; then
58+
echo "pre-commit/gosec: cached binary is ${installed_ver:-unknown}, need v${GOSEC_VERSION}; reinstalling" >&2
59+
need_install=1
60+
fi
61+
else
62+
need_install=1
63+
fi
64+
65+
if [[ $need_install -eq 1 ]]; then
66+
echo "pre-commit/gosec: installing gosec@v${GOSEC_VERSION} -> $(dirname "$GOSEC_BIN")" >&2
67+
mkdir -p "$(dirname "$GOSEC_BIN")"
68+
GOBIN="$(dirname "$GOSEC_BIN")" go install \
69+
"github.com/securego/gosec/v2/cmd/gosec@v${GOSEC_VERSION}" || {
70+
echo "pre-commit/gosec: install failed (is Go on PATH?)" >&2
71+
exit 2
72+
}
73+
fi
74+
}
75+
76+
# Print the module root (relative to repo root) that owns a given relative file
77+
# path, or empty string when the file belongs to the root module.
78+
module_for() {
79+
local f="$1" mod
80+
for mod in $MODULE_DIRS; do
81+
case "$f" in
82+
"$mod"/*) printf '%s' "$mod"; return ;;
83+
esac
84+
done
85+
printf ''
86+
}
87+
88+
# ---- main -------------------------------------------------------------------
89+
90+
[[ $# -eq 0 ]] && exit 0
91+
92+
REPO_ROOT="$(git rev-parse --show-toplevel)"
93+
94+
# Filter: skip deleted files and files under testdata/.
95+
live_files=()
96+
for f in "$@"; do
97+
[[ -f "$f" ]] || continue
98+
case "$f" in
99+
*/testdata/*) continue ;;
100+
testdata/*) continue ;;
101+
esac
102+
live_files+=("$f")
103+
done
104+
105+
[[ ${#live_files[@]} -eq 0 ]] && exit 0
106+
107+
ensure_gosec
108+
109+
# Build "module|package" pairs from the live file list.
110+
# Package path is relative to the owning module root, in ./pkg notation.
111+
pairs_raw=()
112+
for f in "${live_files[@]}"; do
113+
mod=$(module_for "$f")
114+
pkg_dir=$(dirname "$f")
115+
116+
if [[ -n "$mod" ]]; then
117+
# Strip the module prefix (plus the separating slash).
118+
rel="${pkg_dir:$((${#mod}+1))}"
119+
[[ -z "$rel" ]] && rel="." # file sits directly in the module root
120+
else
121+
rel="$pkg_dir" # root module; pkg_dir is already relative
122+
fi
123+
124+
# Normalise to go-tool notation.
125+
if [[ "$rel" == "." ]]; then
126+
pkg="."
127+
else
128+
pkg="./$rel"
129+
fi
130+
131+
pairs_raw+=("${mod}|${pkg}")
132+
done
133+
134+
# Deduplicate.
135+
pairs_sorted=$(printf '%s\n' "${pairs_raw[@]}" | sort -u)
136+
137+
# Enumerate unique module roots.
138+
mods=$(printf '%s\n' "$pairs_sorted" | cut -d'|' -f1 | sort -u)
139+
140+
fail=0
141+
while IFS= read -r mod; do
142+
pkgs=$(printf '%s\n' "$pairs_sorted" \
143+
| awk -F'|' -v m="$mod" '$1==m{print $2}' \
144+
| tr '\n' ' ')
145+
mod_dir="${REPO_ROOT}${mod:+/${mod}}"
146+
147+
echo "gosec [${mod:-.}]: scanning package(s): $pkgs" >&2
148+
149+
# pkgs intentionally unquoted: space-separated package paths, no glob chars.
150+
# shellcheck disable=SC2086
151+
if ! (cd "$mod_dir" && "$GOSEC_BIN" \
152+
-quiet \
153+
-exclude-dir=.legacy \
154+
-exclude-dir=.dev-notes \
155+
-exclude-dir=vendor \
156+
"-exclude=${GOSEC_EXCLUDE}" \
157+
$pkgs); then
158+
fail=1
159+
fi
160+
done <<< "$mods"
161+
162+
exit $fail

0 commit comments

Comments
 (0)