Skip to content

sec(ci): unprotect only the RDS instance each state owns before destroy #2875

sec(ci): unprotect only the RDS instance each state owns before destroy

sec(ci): unprotect only the RDS instance each state owns before destroy #2875

Workflow file for this run

name: pre-commit
on:
pull_request:
branches: [main]
push:
branches: [main]
permissions:
contents: read
jobs:
pre-commit:
name: Run pre-commit hooks
runs-on: ubuntu-latest
# 35 minutes accommodates the 3-attempt retry wrapper on the
# `Run pre-commit` step below (3 attempts * 10 min per-attempt
# timeout + 2 * 90 s retry waits = 33 min worst case) plus a
# small margin for setup/install steps. Without the bump, the
# job-level cap killed any retry attempt before it could start,
# making the retry policy ineffective (CR finding on #697).
timeout-minutes: 35
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.13"
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
# Read from go.mod rather than a hardcoded string, matching
# aws_sanity / azure_sanity / database-migration. One fewer place the
# Go version has to be bumped by hand (issue #1833).
go-version-file: go.mod
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24"
# Cache the npm download cache keyed on the frontend lockfile.
# Saves ~30-40s per run vs an uncached `npm ci`. Same pattern
# already used in frontend-build-sentinel.yml.
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Set up Terraform
# Required by the terraform_fmt + terraform_validate pre-commit hooks.
# terraform_validate calls `terraform init` per module, which the
# action wraps with HTTP-cached provider downloads.
#
# Pin must satisfy `required_version = ">= 1.10.0"` declared by every
# `terraform/environments/*/main.tf` — pinning to a sub-1.10 version
# makes init abort before validate even runs. Action major matches
# `.github/workflows/ci.yml` so both workflows resolve to the same
# Terraform binary; otherwise a behavioural drift between the two
# could pass one and fail the other.
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with:
terraform_version: "1.10.5"
terraform_wrapper: false
- name: Install tflint
# Pinned to a release tag (not master) so a malicious or accidental
# change to install_linux.sh on master can't silently land on this
# CI runner. `curl -fsSL` makes transport errors fail loudly
# instead of writing an HTML error page to stdin and feeding it
# to bash.
env:
TFLINT_VERSION: v0.55.0
run: |
set -euo pipefail
curl -fsSL -o /tmp/tflint-install.sh \
"https://raw.githubusercontent.com/terraform-linters/tflint/${TFLINT_VERSION}/install_linux.sh"
bash /tmp/tflint-install.sh
# Cache the tflint ruleset plugins (aws/azurerm/google) that
# `tflint --init` downloads from the GitHub Releases API. Without
# this cache EVERY run re-downloads all three plugins and is exposed
# to transient GitHub release-API 503s — a sustained 503 outrode the
# GITHUB_TOKEN auth + 3-attempt pre-commit retry below and reddened
# this job repo-wide (blocking every PR). Keyed on .tflint.hcl so a
# plugin-version bump re-downloads; restore-keys seeds a warm start.
- name: Cache tflint plugins
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.tflint.d/plugins
key: tflint-plugins-${{ runner.os }}-${{ hashFiles('.tflint.hcl') }}
restore-keys: |
tflint-plugins-${{ runner.os }}-
# Pre-populate the plugin cache with a dedicated, authenticated,
# retried `tflint --init` BEFORE pre-commit runs. On a cache hit this
# is a fast no-op (tflint skips download when the pinned plugin
# versions are already present — no API call, so immune to the 503).
# On a cache miss (version bump / cold cache) the retry loop rides
# out transient release-API 503s at the init level instead of
# re-running every hook via the coarse outer retry. GITHUB_TOKEN
# raises the release-API limit above the 60/hr anonymous ceiling.
- name: Initialize tflint plugins
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -uo pipefail
for attempt in 1 2 3 4 5 6; do
if tflint --init --config="${GITHUB_WORKSPACE}/.tflint.hcl"; then
echo "tflint --init succeeded (attempt ${attempt})"
exit 0
fi
wait=$((attempt * 20))
echo "tflint --init failed (attempt ${attempt}/6); retrying in ${wait}s..." >&2
sleep "${wait}"
done
echo "tflint --init failed after 6 attempts — GitHub release API likely unavailable" >&2
exit 1
# Cache the installed tool binaries (gosec, gocyclo). Keyed on the
# pinned version strings so a tool-version bump still triggers a
# fresh install. Both binaries land in ~/go/bin which setup-go@v6
# already adds to PATH. Restored BEFORE the install steps so the
# `if: cache-hit != 'true'` guards below can short-circuit them on
# cache-hit runs (the `go install` invocations cost ~3-5s each
# even when the module cache is warm; skipping them on cache-hit
# is worth the extra `if`).
- name: Cache Go-installed tools (gosec, gocyclo)
id: cache-go-tools
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/go/bin
key: go-tools-${{ runner.os }}-gosec-v2.28.0-gocyclo-v0.6.0
- name: Install gosec
if: steps.cache-go-tools.outputs.cache-hit != 'true'
# Pinned to the same version ci.yml's `securego/gosec` Action uses,
# so an upstream gosec release with rule changes can't silently
# downgrade the gate between the two workflows.
run: go install github.com/securego/gosec/v2/cmd/gosec@v2.28.0
- name: Install gocyclo
if: steps.cache-go-tools.outputs.cache-hit != 'true'
# Pinned to match ci.yml — security tool installs must not use
# @latest; that's exactly the supply-chain weakness this PR is
# closing for Dockerfile FROMs.
run: go install github.com/fzipp/gocyclo/cmd/gocyclo@v0.6.0
- name: Install Trivy
# Pinned to v0.69.3 (the latest release with published GitHub-release
# tarballs as of writing). Tags exist for v0.58 onwards but several
# mid-range releases skipped publishing assets to the Releases page;
# the install.sh script fetches via GitHub Releases, so picking one
# of those tags makes install bail silently after detecting the
# version. v0.69.3 ships the standard `trivy_<ver>_Linux-64bit.tar.gz`
# asset.
#
# The installer itself is fetched from the same pinned release tag
# (not the mutable `main` branch) and downloaded to a file before
# execution, matching the tflint step above: a malicious or
# accidental change to install.sh on main can't silently execute
# on this CI runner, and `curl -fsSL` makes transport errors fail
# loudly instead of piping an HTML error page into sh.
env:
TRIVY_VERSION: v0.69.3
run: |
set -euo pipefail
curl -fsSL -o /tmp/trivy-install.sh \
"https://raw.githubusercontent.com/aquasecurity/trivy/${TRIVY_VERSION}/contrib/install.sh"
sh /tmp/trivy-install.sh -b /usr/local/bin "${TRIVY_VERSION}"
- name: Install git-secrets
# Pinned to a release tag rather than master HEAD. After install
# we register the AWS pattern set and ASSERT at least one pattern
# was registered — without the assert, a registration failure
# produces a patternless scanner that exits 0 unconditionally,
# leaving the gate silently downgraded.
run: |
set -euo pipefail
git clone --depth 1 --branch 1.3.0 https://github.com/awslabs/git-secrets.git /tmp/git-secrets
sudo make -C /tmp/git-secrets install
git secrets --register-aws --global
git secrets --list --global | grep -q '.' || {
echo "git-secrets registration produced no patterns — gate would be silently disabled"
exit 1
}
# Note: the local `hadolint` hook in .pre-commit-config.yaml (search for
# `id: hadolint`; no line number, because this comment has already gone
# stale twice as that file shifted) runs ghcr.io/hadolint/hadolint pinned
# by digest. The version lives in that entry and is the single source of
# truth; do not restate it here, or this comment rots again. We do NOT
# install a host binary here — it would be dead code (never invoked by
# the hook) AND a supply-chain hole (latest tag, no checksum). Note:
# an earlier version of this comment claimed the hook already pinned
# the image to v2.14.0 via the hook repo's `rev:` -- it did not; that
# `rev:` only pins hadolint's *hook definition*, whose upstream entry
# (`ghcr.io/hadolint/hadolint hadolint`) has no image tag and floats to
# `:latest`. See the digest pin in .pre-commit-config.yaml for the fix.
# If a future change switches the hook to a host binary, install a
# pinned + sha256-verified binary here.
- name: Install pre-commit
run: pip install 'pre-commit==4.0.1'
# Cache pre-commit's per-hook environments (Go, Python, Node, etc.
# virtualenvs it builds on first run). Keyed on the hook config
# because pre-commit will rebuild any env whose pinned rev changes.
# Saves ~30-60s per cache-hit run; safe because pre-commit verifies
# env integrity on use.
- name: Cache pre-commit environments
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
restore-keys: |
pre-commit-${{ runner.os }}-
# Cache the Go build cache so `go vet`, `gosec`, and any other
# Go-compiling hooks reuse compiled object files instead of
# rebuilding from source. setup-go@v6 caches ~/go/pkg/mod
# (modules) but NOT ~/.cache/go-build (compiled output) — this
# step covers the latter. Keyed on go.sum so a dep upgrade still
# invalidates the cache and gets clean builds.
- name: Cache Go build cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/go-build
key: go-build-${{ runner.os }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
go-build-${{ runner.os }}-
- name: Install frontend deps
run: |
if [ -f frontend/package-lock.json ]; then
cd frontend && npm ci
fi
- name: Run pre-commit
# SKIP=terraform_validate: that hook calls `terraform init` per
# module, which creates `.terraform.lock.hcl` files. Those are
# gitignored, so on a fresh CI checkout they don't exist and the
# init step "modifies files", which pre-commit reports as a
# failure. Local pre-commit runs work because lock files persist
# between invocations. terraform_fmt and terraform_tflint still
# run and catch the syntax/style issues that terraform_validate
# would catch; the deeper schema validation runs in
# `terraform plan` during deploy workflows.
#
# GITHUB_TOKEN is passed so terraform_tflint's `tflint --init`
# step authenticates against the GitHub API (5000/hr per-token)
# when it downloads ruleset plugin releases. Without the token,
# tflint goes anonymous and hits the 60/hr per-IP limit shared
# across every workflow on the runner's NAT IP, which trips
# intermittently when PRs land in the same hour (issue #564).
#
# nick-fields/retry wraps the run with up to 3 attempts and a
# 90-second wait so transient flakes (GitHub Releases blips,
# tflint plugin download timeouts, etc.) do not require a
# manual rerun. The GITHUB_TOKEN fix above is the primary fix;
# the retry wrapper is the cheap defense-in-depth for the
# residual flakes that token alone cannot eliminate.
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
env:
# SKIP the `gosec` pre-commit hook in CI: it is designed to scan
# only the changed packages of a local commit, but `pre-commit run
# --all-files` (this CI job) feeds it EVERY .go file across all six
# modules at once, and gosec's whole-repo analysis exhausts the
# runner's memory — the job dies with "The runner has received a
# shutdown signal" at this step on every run. gosec is NOT dropped:
# the dedicated `Security Scanning` job in ci.yml runs gosec v2.28.0
# per-module (SARIF) as the authoritative gate, so this only removes
# the duplicate that OOMs CI — the same dedup rationale as the
# already-skipped `terraform_validate` (covered by Validate Terraform).
# Local developers still get the fast per-changed-package gosec hook.
SKIP: terraform_validate,gosec
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 90
command: pre-commit run --all-files