Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,29 @@ jobs:
go-version-file: pkg/go.mod
cache-dependency-path: '**/go.sum'

- name: Secret scan (gitleaks)
# The gitleaks CLI, not gitleaks-action (which needs an org licence).
# The release tarball is pinned by version and sha256 (from the
# release's gitleaks_<version>_checksums.txt), so a moved tag or a
# tampered asset fails the step. Scans the checked-out tree with the
# repo's .gitleaks.toml; any finding exits 1. The config self-test then
# proves a secret planted beside an allowlisted fixture is still caught.
if: always() && steps.checkout.outcome == 'success'
env:
GITLEAKS_VERSION: '8.30.1'
GITLEAKS_SHA256: '551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb'
run: |
set -euo pipefail
archive="${RUNNER_TEMP}/gitleaks.tar.gz"
curl -fsSL -o "${archive}" \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
echo "${GITLEAKS_SHA256} ${archive}" | sha256sum -c -
mkdir "${RUNNER_TEMP}/gitleaks"
tar -xzf "${archive}" -C "${RUNNER_TEMP}/gitleaks" gitleaks
"${RUNNER_TEMP}/gitleaks/gitleaks" dir . \
--config .gitleaks.toml --redact --no-banner --exit-code 1
GITLEAKS="${RUNNER_TEMP}/gitleaks/gitleaks" bash scripts/test-gitleaks-config.sh

- name: Run govulncheck CVE scanner (all modules)
# Independent scanners still run after another scanner fails.
if: always() && steps.checkout.outcome == 'success' && steps.setup_go.outcome == 'success'
Expand Down
23 changes: 23 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# gitleaks configuration for the CI "Secret scan (gitleaks)" step.
# Extends the default ruleset; every allowlist below is a regex anchored with
# ^...$ so it matches only the exact fixture secret, never a longer token that
# merely contains it. Do NOT add `paths`, `condition`, `stopwords`, `commits`,
# `targetRules` or `regexTarget`: in gitleaks 8.30.1 `paths` with
# condition = "AND" skips the whole file, so a real secret planted next to a
# fixture goes undetected. scripts/test-gitleaks-config.sh enforces this.
title = "cloud-commitments-go gitleaks config"

[extend]
useDefault = true

[[allowlists]]
description = "git-secrets 1.3.0 release commit SHA pinned in pre-commit.yml"
regexes = ['''^ad82d68ee924906a0401dfd48de5057731a9bc84$''']

[[allowlists]]
description = "synthetic bearer token fixture in purchaseguard tests"
regexes = ['''^tok-0123456789abcdef$''']

[[allowlists]]
description = "synthetic hex token fixture in the Azure idempotency marker tests"
regexes = ['''^4f1c2b0e9d8a7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b$''']
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
- **`provider`:** `ProviderConfig` is renamed to `Config` (#39). `ProviderConfig` stays as a deprecated type alias, so existing consumers keep compiling; migrate to `provider.Config`. The provider modules move over once their pinned `pkg` version includes the rename.
- **`provider`:** `ProviderConfig` is renamed to `Config` (#39). `ProviderConfig` stays as a type alias, so existing consumers keep compiling; migrate to `provider.Config`. The provider modules move over once their pinned `pkg` version includes the rename.
- **`httpclient`:** dials to the RFC 8215 local-use NAT64 prefix `64:ff9b:1::/48` are refused when any RFC 6052 layout (/48, /56, /64, /96) embeds a metadata IPv4 address; Teredo `2001::/32` stays allowed (#238).
- **CI:** the security-scan job runs a pinned gitleaks CLI (v8.30.1, sha256-verified) over the checkout with `.gitleaks.toml` and fails on any finding (#101).
- **`common.HoursPerMonth`:** exported 730-hour average month (`HoursPerYear / 12`) next to `HoursPerYear`; the remaining bare 730 and 8760 literals in the aws and gcp providers use the shared constants (#109).
- **`gcp`:** region-lookup permission errors are now classified by type only (googleapi 403 or gRPC `PermissionDenied`) through the shared `IsPermissionError`. gRPC `PermissionDenied` is now skipped with a warning like a 403, and an untyped error whose text merely contains "403" and "permission" now propagates instead of being skipped (#6).
- **BREAKING `gcp`: stored contexts removed (#6).** `computeengine.NewClient`
Expand Down
74 changes: 74 additions & 0 deletions scripts/test-gitleaks-config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/bin/bash
# Self-test for .gitleaks.toml. Allowlisted fixture literals are derived from
# the config itself (each `regexes` entry, ^ and $ stripped) and located in the
# tracked tree. Checks that:
# - the config uses no `paths`, `condition`, `stopwords`, `commits`,
# `targetRules` or `regexTarget` key (gitleaks 8.30.1 skips a whole file
# when `paths` is combined with condition = "AND"), and that injecting one
# is detected;
# - each literal scans clean on its own;
# - the literal with a prefix or suffix, or beside a PAT-shaped string, is caught;
# - the real tree is clean.
# Usage: GITLEAKS=/path/to/gitleaks bash scripts/test-gitleaks-config.sh
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CONFIG="$REPO_ROOT/.gitleaks.toml"
GITLEAKS="${GITLEAKS:-gitleaks}"
command -v "$GITLEAKS" >/dev/null || { echo "gitleaks not found (set GITLEAKS)" >&2; exit 1; }

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

# Assembled at runtime so this file itself scans clean.
PAT="ghp""_aB3dE5gH7jK9mN1pQ3sT5vW7yZ9bC1dE3fG5"

pass=0 fail=0
check() { # label expected_rc actual_rc
if [ "$3" -eq "$2" ]; then pass=$((pass + 1)); else fail=$((fail + 1)); echo "FAIL: $1 (expected rc $2, got $3)" >&2; fi
}

scan() { # dir; prints nothing, returns gitleaks rc
local rc=0
"$GITLEAKS" dir "$1" --config "$CONFIG" --no-banner --redact --exit-code 1 >/dev/null 2>&1 || rc=$?
echo "$rc"
}

# 0 when the config is free of the forbidden keys, 1 otherwise.
forbidden_keys() {
if grep -qE '^[[:space:]]*(paths|condition|stopwords|commits|targetRules|regexTarget)[[:space:]]*=' "$1"; then return 1; fi
}
rc=0; forbidden_keys "$CONFIG" || rc=$?
check "config has no paths/condition/stopwords/commits/targetRules/regexTarget" 0 "$rc"
for key in 'paths = ['"'''"'unrelated\.txt'"'''"']' 'condition = "AND"' 'stopwords = ["x"]' 'commits = ["abc"]' 'targetRules = ["generic-api-key"]' 'regexTarget = "line"'; do
{ cat "$CONFIG"; printf '\n[[allowlists]]\n%s\n' "$key"; } >"$tmp/injected.toml"
rc=0; forbidden_keys "$tmp/injected.toml" || rc=$?
check "injected '$key' is detected" 1 "$rc"
done

# Literals from the config: regexes = ['''^literal$'''].
literals=$(sed -nE "s/^regexes = \['''\^(.*)\\$'''\]\$/\1/p" "$CONFIG")
[ -n "$literals" ] || { echo "no anchored regexes found in $CONFIG" >&2; exit 1; }
unanchored=$(grep -E "^regexes" "$CONFIG" | grep -vE "\['''\^.*\\$'''\]\$" || true)
[ -z "$unanchored" ] || { echo "FAIL: regexes not anchored with ^...\$:" >&2; echo "$unanchored" >&2; fail=$((fail + 1)); }

i=0
while IFS= read -r literal; do
i=$((i + 1))
path=$(git -C "$REPO_ROOT" grep -lF -e "$literal" -- . ':!.gitleaks.toml' ':!scripts/test-gitleaks-config.sh' | head -1)
[ -n "$path" ] || { echo "FAIL: allowlisted literal #$i not found in the tree" >&2; fail=$((fail + 1)); continue; }
d="$tmp/c$i"
mkdir -p "$d/$(dirname "$path")"
printf 'const token = "%s"\n' "$literal" >"$d/$path"
check "fixture literal in $path is allowed" 0 "$(scan "$d")"
printf 'const token = "%sx9"\n' "$literal" >"$d/$path"
check "fixture literal plus a suffix in $path is caught" 1 "$(scan "$d")"
printf 'const token = "%s"\n' "$literal" >"$d/$path"
printf 'const real = "%s"\n' "$PAT" >>"$d/$path"
check "PAT-shaped string beside the fixture in $path is caught" 1 "$(scan "$d")"
done <<<"$literals"

check "repository tree is clean" 0 "$(scan "$REPO_ROOT")"

echo "$pass PASS, $fail FAIL"
[ "$fail" -eq 0 ]
Loading