diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 0d8c2eb..2346552 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,69 +1,11 @@ #!/bin/sh -# Blocks commits that carry AWS credentials. -# -# examples/network/providerconfig.yaml holds a ProviderConfig and its secret -# together so automated testing can apply both at once. That means the file is -# tracked with placeholders and is meant to be edited locally — one `git add .` -# away from publishing real credentials. +# Refuses to commit AWS credentials. # # Enable with: # # git config core.hooksPath .githooks # -# Checks staged content, not the working tree, since that is what a commit -# records. `git commit --no-verify` bypasses it. -# -# Deliberately no `set -e`: this script manages its own exit status, and an -# AND-OR list returning non-zero must not abort the scan early. -set -u - -fail=0 -note() { printf '%s\n' "$*" >&2; } - -staged=$(git diff --cached --name-only --diff-filter=ACM) -[ -n "$staged" ] || exit 0 - -# 1. The credentials file must still hold its placeholders when committed. -guarded='examples/network/providerconfig.yaml' -if printf '%s\n' "$staged" | grep -qx "$guarded"; then - if ! git show ":$guarded" 2>/dev/null | grep -q 'REPLACE_ME'; then - note "BLOCKED: $guarded is staged with its REPLACE_ME placeholders removed." - note "" - note " Real credentials must never be committed. To keep your local edit" - note " out of git for good:" - note "" - note " git restore --staged $guarded" - note " git update-index --skip-worktree $guarded" - note "" - fail=1 - fi -fi - -# 2. Belt and braces: an AWS key pasted into any staged file, not just that one. -# grep -a so a binary blob is scanned rather than skipped. This file is -# exempt: it necessarily contains the patterns it searches for. -offenders=$( - printf '%s\n' "$staged" | while IFS= read -r f; do - [ -n "$f" ] || continue - if [ "$f" = '.githooks/pre-commit' ]; then continue; fi - blob=$(git show ":$f" 2>/dev/null) || continue - if printf '%s' "$blob" | grep -aEq '(AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}'; then - printf '%s\taccess key ID\n' "$f" - fi - if printf '%s' "$blob" | grep -aEq 'aws_secret_access_key[[:space:]]*=[[:space:]]*[A-Za-z0-9/+=]{40}'; then - printf '%s\tsecret access key\n' "$f" - fi - done -) - -if [ -n "$offenders" ]; then - printf '%s\n' "$offenders" | while IFS="$(printf '\t')" read -r f kind; do - note "BLOCKED: $f looks like it contains an AWS $kind." - done - fail=1 -fi - -if [ "$fail" -ne 0 ]; then - note "Commit aborted. Use --no-verify only if you are certain." -fi -exit "$fail" +# The check itself lives in hack/check-credentials.sh, which CI runs over the +# whole tree, so the hook and CI cannot drift apart. `git commit --no-verify` +# bypasses this. +exec "$(git rev-parse --show-toplevel)/hack/check-credentials.sh" --staged diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 494e500..f516a9a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -49,6 +49,19 @@ jobs: echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "Computed version: ${VERSION}" + # examples/network/providerconfig.yaml is tracked with placeholders and meant + # to be edited locally, so real credentials are one `git add .` away from + # being published. The pre-commit hook in .githooks/ catches that, but hooks + # are opt-in per clone — this is the check that cannot be skipped. + credentials: + runs-on: *default-runner + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Check for AWS credentials + run: hack/check-credentials.sh + # TEMPORARY: build the crossplane CLI from source, because TypeScript # embedded functions are not in a released CLI yet. Building it once and # sharing the binary keeps the jobs below from each pulling the CLI's whole @@ -60,7 +73,10 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: stevendborrelli/cli - ref: project-typescript-support + # Pinned to a commit rather than the branch: the branch is force-pushed + # as the PR is revised, which would silently change what a release + # builds. Bump this when picking up new CLI changes. + ref: 0d19745dc682ca02085f59bc8d9dcf5c37538421 # Take the Go version from the CLI's own go.mod so this keeps working as # the CLI moves forward. diff --git a/README.md b/README.md index 771524f..21b900c 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ kind: Configuration metadata: name: configuration-aws-network spec: - package: xpkg.upbound.io/upbound/configuration-aws-network-ts:v0.2.0 + package: xpkg.upbound.io/upbound/configuration-aws-network-ts:v0.3.0 ``` Verify the package is healthy. If not, run `kubectl describe configuration.pkg configuration-aws-network`. @@ -47,7 +47,7 @@ Verify the package is healthy. If not, run `kubectl describe configuration.pkg c ```sh $ kubectl get configuration.pkg configuration-aws-network NAME INSTALLED HEALTHY PACKAGE AGE -configuration-aws-network True True xpkg.upbound.io/upbound/configuration-aws-network-ts:v0.2.0 18m +configuration-aws-network True True xpkg.upbound.io/upbound/configuration-aws-network-ts:v0.3.0 18m ``` ### Configuring AWS Authentication @@ -326,16 +326,19 @@ so that automated testing can bring both up with a single `--extra-resources` ar > file up again later — say to change the placeholders themselves — reverse it with > `--no-skip-worktree`. -There is also a pre-commit hook in [.githooks/](.githooks/) that refuses any commit staging -this file without its placeholders, or staging an AWS access key or secret key in any other -file. It is worth enabling once per clone: +[hack/check-credentials.sh](hack/check-credentials.sh) refuses this file without its +placeholders, or an AWS access key or secret key in any other file. It runs in two places: -```bash -git config core.hooksPath .githooks -``` +- **CI**, over every tracked file, in the `credentials` job. This one cannot be skipped, and is + what actually keeps credentials off `main`. +- **A pre-commit hook**, over staged content, so you find out before the push rather than after. + Hooks are not installed by cloning, so enable it once per clone: + + ```bash + git config core.hooksPath .githooks + ``` -Hooks are not installed by cloning, so this is opt-in — it protects you, not the repository. -`git commit --no-verify` bypasses it. + `git commit --no-verify` bypasses the hook — but not CI. Once it is up, apply the example as normal: @@ -372,7 +375,7 @@ crossplane project build it references: ```bash -crossplane project push --tag v0.2.0 +crossplane project push --tag v0.3.0 ``` Both run in [CI](.github/workflows/ci.yaml) on every push. diff --git a/functions/network/package-lock.json b/functions/network/package-lock.json index 60e5811..5d4da5a 100644 --- a/functions/network/package-lock.json +++ b/functions/network/package-lock.json @@ -1,12 +1,12 @@ { "name": "configuration-aws-network-ts-function", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "configuration-aws-network-ts-function", - "version": "0.2.0", + "version": "0.3.0", "license": "Apache-2.0", "dependencies": { "@crossplane-org/function-sdk-typescript": "^0.7.0", diff --git a/functions/network/package.json b/functions/network/package.json index 9a1dd8d..22f4836 100644 --- a/functions/network/package.json +++ b/functions/network/package.json @@ -1,6 +1,6 @@ { "name": "configuration-aws-network-ts-function", - "version": "0.2.0", + "version": "0.3.0", "description": "Composes an AWS network (VPC, subnets, route tables, gateway, security groups).", "license": "Apache-2.0", "type": "module", diff --git a/hack/check-credentials.sh b/hack/check-credentials.sh new file mode 100755 index 0000000..1a4c141 --- /dev/null +++ b/hack/check-credentials.sh @@ -0,0 +1,84 @@ +#!/bin/sh +# Refuses AWS credentials in this repository. +# +# examples/network/providerconfig.yaml holds a ProviderConfig and its secret +# together so automated testing can apply both at once. That means it is +# tracked with placeholders and meant to be edited locally, which puts real +# credentials one `git add .` away from being published. +# +# Two modes, so the pre-commit hook and CI share one definition of what a +# credential looks like: +# +# check-credentials.sh scan tracked files on disk (CI) +# check-credentials.sh --staged scan staged blobs (pre-commit) +# +# Deliberately no `set -e`: this script manages its own exit status, and an +# AND-OR list returning non-zero must not abort the scan early. +set -u + +mode=worktree +[ "${1:-}" = '--staged' ] && mode=staged + +guarded='examples/network/providerconfig.yaml' +# This file necessarily contains the patterns it searches for. +self='hack/check-credentials.sh' + +fail=0 +note() { printf '%s\n' "$*" >&2; } + +if [ "$mode" = staged ]; then + files=$(git diff --cached --name-only --diff-filter=ACM) + read_file() { git show ":$1" 2>/dev/null; } +else + files=$(git ls-files) + read_file() { cat "$1" 2>/dev/null; } +fi + +[ -n "$files" ] || exit 0 + +# 1. The credentials file must still hold its placeholders. +if printf '%s\n' "$files" | grep -qx "$guarded"; then + if ! read_file "$guarded" | grep -q 'REPLACE_ME'; then + note "BLOCKED: $guarded no longer has its REPLACE_ME placeholders." + note "" + note " Real credentials must never be committed. To keep a local edit" + note " out of git for good:" + note "" + note " git restore --staged $guarded" + note " git update-index --skip-worktree $guarded" + note "" + fail=1 + fi +fi + +# 2. An AWS key in any file, not just that one. grep -a so a binary blob is +# scanned rather than skipped. +offenders=$( + printf '%s\n' "$files" | while IFS= read -r f; do + [ -n "$f" ] || continue + if [ "$f" = "$self" ]; then continue; fi + blob=$(read_file "$f") || continue + if printf '%s' "$blob" | grep -aEq '(AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}'; then + printf '%s\taccess key ID\n' "$f" + fi + if printf '%s' "$blob" | grep -aEq 'aws_secret_access_key[[:space:]]*=[[:space:]]*[A-Za-z0-9/+=]{40}'; then + printf '%s\tsecret access key\n' "$f" + fi + done +) + +if [ -n "$offenders" ]; then + printf '%s\n' "$offenders" | while IFS="$(printf '\t')" read -r f kind; do + note "BLOCKED: $f looks like it contains an AWS $kind." + done + fail=1 +fi + +if [ "$fail" -ne 0 ]; then + if [ "$mode" = staged ]; then + note "Commit aborted. Use --no-verify only if you are certain." + else + note "Credential check failed." + fi +fi +exit "$fail"