Skip to content
Closed
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
72 changes: 47 additions & 25 deletions .github/workflows/on-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ on:
paths:
- ".github/workflows/on-pr.yaml"
- ".gitops/**"
- "Makefile"
- "apis/**"
- "examples/**"
- "tests/**"
- "functions/**"
- "scripts/**"
- "upbound.yaml"

permissions:
Expand All @@ -27,31 +29,51 @@ permissions:

jobs:
validate:
uses: unbounded-tech/workflows-crossplane/.github/workflows/validate.yaml@v3.0.0
with:
examples: |
[
{ "example": "examples/crossplanestacks/minimal.yaml", "api_path": "apis/crossplanestacks" },
{ "example": "examples/crossplanestacks/standard.yaml", "api_path": "apis/crossplanestacks" },
{ "example": "examples/crossplanestacks/full.yaml", "api_path": "apis/crossplanestacks" },
{ "example": "examples/awsproviderstacks/minimal.yaml", "api_path": "apis/awsproviderstacks" },
{ "example": "examples/awsproviderstacks/full.yaml", "api_path": "apis/awsproviderstacks" },
{ "example": "examples/functionsstacks/minimal.yaml", "api_path": "apis/functionsstacks" },
{ "example": "examples/functionsstacks/full.yaml", "api_path": "apis/functionsstacks" },
{ "example": "examples/githubproviderstacks/minimal.yaml", "api_path": "apis/githubproviderstacks" },
{ "example": "examples/githubproviderstacks/full.yaml", "api_path": "apis/githubproviderstacks" },
{ "example": "examples/helmproviderstacks/minimal.yaml", "api_path": "apis/helmproviderstacks" },
{ "example": "examples/helmproviderstacks/full.yaml", "api_path": "apis/helmproviderstacks" },
{ "example": "examples/kubernetesproviderstacks/minimal.yaml", "api_path": "apis/kubernetesproviderstacks" },
{ "example": "examples/kubernetesproviderstacks/full.yaml", "api_path": "apis/kubernetesproviderstacks" },
{ "example": "examples/listmonkproviderstacks/minimal.yaml", "api_path": "apis/listmonkproviderstacks" },
{ "example": "examples/listmonkproviderstacks/full.yaml", "api_path": "apis/listmonkproviderstacks" },
{ "example": "examples/openpanelproviderstacks/minimal.yaml", "api_path": "apis/openpanelproviderstacks" },
{ "example": "examples/openpanelproviderstacks/full.yaml", "api_path": "apis/openpanelproviderstacks" },
{ "example": "examples/zitadelproviderstacks/minimal.yaml", "api_path": "apis/zitadelproviderstacks" },
{ "example": "examples/zitadelproviderstacks/full.yaml", "api_path": "apis/zitadelproviderstacks" }
]
error_on_missing_schemas: true
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
Comment on lines +34 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the workflow file
ls -la .github/workflows/on-pr.yaml || true

# Print relevant sections with line numbers
nl -ba .github/workflows/on-pr.yaml | sed -n '1,140p'

Repository: hops-ops/aws-crossplane-stack

Length of output: 212


🏁 Script executed:

#!/bin/bash
set -euo pipefail

nl -ba .github/workflows/on-pr.yaml | sed -n '1,160p'

Repository: hops-ops/aws-crossplane-stack

Length of output: 116


🏁 Script executed:

#!/bin/bash
set -euo pipefail

nl -ba .github/workflows/on-pr.yaml | sed -n '1,220p'

Repository: hops-ops/aws-crossplane-stack

Length of output: 116


🏁 Script executed:

#!/bin/bash
set -euo pipefail

nl -ba .github/workflows/on-pr.yaml | sed -n '1,220p'

Repository: hops-ops/aws-crossplane-stack

Length of output: 116


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- on-pr.yaml (lines 1-120) ----"
cat -n .github/workflows/on-pr.yaml | sed -n '1,120p'

echo "---- on-pr.yaml (lines 120-220) ----"
cat -n .github/workflows/on-pr.yaml | sed -n '120,220p' || true

Repository: hops-ops/aws-crossplane-stack

Length of output: 3931


🌐 Web query:

GitHub actions/checkout persist-credentials default value

💡 Result:

The default value for the persist-credentials input in the GitHub actions/checkout action is true [1][2][3]. When this option is enabled, the action configures the authentication token or SSH key with the local git configuration, allowing your workflow scripts to run authenticated git commands [1][4][3]. The action automatically removes these credentials during post-job cleanup [1][4]. To opt out of this behavior, you can explicitly set persist-credentials to false in your workflow file [1][5].

Citations:


🌐 Web query:

GitHub actions/checkout persist-credentials default value

💡 Result:

The default value of the GitHub Actions checkout step input persist-credentials is true.[1][2] In the actions/checkout action.yml, persist-credentials is defined with default: true.[2] The README also documents that the token/credentials are persisted locally unless you set persist-credentials: false to opt out.[1][3].

Citations:


🌐 Web query:

GitHub actions/checkout persist-credentials default value

💡 Result:

For GitHub Actions actions/checkout, the input persist-credentials default value is true. [1][2][3] This means that if you don’t set persist-credentials explicitly, the checkout action will configure the token/SSH key in the local git config (enabling authenticated git commands), and the token is removed during post-job cleanup. [2][4][1]

Citations:


Pin GitHub Actions to commit SHAs and disable persisted checkout credentials

  • .github/workflows/on-pr.yaml uses mutable action refs (actions/checkout@v6, actions/cache@v5, docker/login-action@v3.7.0, upbound/action-up-project@v1.1.2); pin each to an exact commit SHA (applies to checkout: 35, cache: 38, docker login: 64, build: 71).
  • Checkout currently leaves persist-credentials at its default (true), so the workflow token is configured in git; set persist-credentials: false to reduce token exposure since later steps execute code from the checked-out repo.
  • Crossplane CLI install executes a remote script from crossplane/crossplane@master via curl ... | sh; pin the script to a commit SHA (or use a release artifact) to avoid executing an unpinned source.
Suggested hardening
       - name: Checkout
-        uses: actions/checkout@v6
+        uses: actions/checkout@<pinned-commit-sha>
+        with:
+          persist-credentials: false
...
       - name: Cache crossplane
-        uses: actions/cache@v5
+        uses: actions/cache@<pinned-commit-sha>
...
       - name: Docker login to GHCR
-        uses: docker/login-action@v3.7.0
+        uses: docker/login-action@<pinned-commit-sha>
...
       - name: Build with up
-        uses: upbound/action-up-project@v1.1.2
+        uses: upbound/action-up-project@<pinned-commit-sha>
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 34-35: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 35-35: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/on-pr.yaml around lines 34 - 35, Update the workflow to
pin mutable GitHub Action refs to exact commit SHAs for actions/checkout,
actions/cache, docker/login-action, and upbound/action-up-project (replace
v6/v5/v3.7.0/v1.1.2 with their corresponding commit SHAs) and set
persist-credentials: false on the actions/checkout step to prevent the workflow
token from being left in the checked-out repo; also replace the unpinned
Crossplane CLI install (the curl | sh command referencing
crossplane/crossplane@master) with a pinned commit SHA or use an official
release artifact/verified installer so the executed script is tied to a specific
commit.

Source: Linters/SAST tools


- name: Cache crossplane
uses: actions/cache@v5
with:
path: |
~/.crossplane/cache
~/.up/cache
~/.up/build-cache
key: ${{ runner.os }}-crossplane-${{ hashFiles('**/upbound.yaml','**/definition.yaml') }}-

- name: Install up
env:
UP_VERSION: v0.46.0
run: |
set -euo pipefail
curl --fail --location --retry 5 --retry-all-errors --retry-delay 5 \
"https://cli.upbound.io/stable/${UP_VERSION}/bin/linux_amd64/up" \
--output up
chmod +x up
sudo mv up /usr/local/bin/up
up version

- name: Install Crossplane CLI
run: |
curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh \
| XP_VERSION=v2.0.2 sh

Comment on lines +58 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the referenced workflow section
FILE=".github/workflows/on-pr.yaml"

echo "== File: $FILE =="
if [ -f "$FILE" ]; then
  nl -ba "$FILE" | sed -n '40,90p'
else
  echo "Missing: $FILE"
  exit 1
fi

# Search for crossplane install usage elsewhere in workflows
echo
echo "== Search: crossplane install usage =="
rg -n "crossplane.*install|install\\.sh|XP_VERSION|raw\\.githubusercontent\\.com/crossplane/crossplane" .github/workflows || true

# Search for any other pinned version usage (v2.0.2 etc.) in repo
echo
echo "== Search: XP_VERSION pin =="
rg -n "XP_VERSION|v2\.0\.2|crossplane.*v[0-9]+\.[0-9]+\.[0-9]+" .github/workflows || true

Repository: hops-ops/aws-crossplane-stack

Length of output: 175


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/on-pr.yaml"

echo "== File: $FILE =="
if [ -f "$FILE" ]; then
  awk 'NR>=40 && NR<=90 {printf "%d\t%s\n", NR, $0}' "$FILE"
else
  echo "Missing: $FILE"
  exit 1
fi

echo
echo "== Search: crossplane install usage =="
rg -n "crossplane.*install|install\\.sh|XP_VERSION|raw\\.githubusercontent\\.com/crossplane/crossplane" .github/workflows || true

echo
echo "== Search: XP_VERSION pin =="
rg -n "XP_VERSION|v2\\.0\\.2|crossplane.*v[0-9]+\\.[0-9]+\\.[0-9]+" .github/workflows || true

Repository: hops-ops/aws-crossplane-stack

Length of output: 3131


🌐 Web query:

Crossplane v2.0.2 install.sh raw.githubusercontent.com crossplane/crossplane v2.0.2 install.sh

💡 Result:

To install the Crossplane CLI version v2.0.2, you can use the official Crossplane installation script with an environment variable to specify the version [1][2]. The installation script is located at: https://raw.githubusercontent.com/crossplane/crossplane/main/install.sh Run the following command in your terminal to download and install the v2.0.2 binary: curl -sL "https://raw.githubusercontent.com/crossplane/crossplane/main/install.sh" | XP_VERSION=v2.0.2 sh After running this command, the script will download the binary, which you can then move to your local path (e.g., /usr/local/bin) to use the crossplane CLI [3][4]. Important Installation Notes: 1. Crossplane CLI vs. Crossplane Control Plane: The install script described above is for the Crossplane CLI (command-line tool) [1][2]. The Crossplane control plane itself is installed into a Kubernetes cluster using Helm [5][6]. 2. Version Compatibility: For versions prior to v2.3.0, the installation script identifies the binary as "crank" and renames it to "crossplane" during the installation process [4][7]. 3. Manual Installation: If you prefer not to use the shell script, you can manually download the binary from the official release repository [1][2]. For v2.0.2, you would look for the binary at https://releases.crossplane.io/stable/v2.0.2/bin/ [1][4].

Citations:


🌐 Web query:

Crossplane CLI install.sh security pin commit recommended curl | sh

💡 Result:

Crossplane’s recommended one-liner to install the Crossplane CLI is: curl -sL "https://raw.githubusercontent.com/crossplane/crossplane/main/install.sh" | sh [1] Security/pinning notes (based on the install script and docs): 1) The command above downloads whatever install.sh exists at the referenced URL and then executes it, so it is not “pinned” to a specific commit of the script. The script itself supports pinning the CLI version you install, via XP_VERSION and XP_CHANNEL environment variables (these choose which artifact to download) [1][2]. 2) The install.sh script downloads a platform-specific CLI binary using curl with flags that fail on HTTP errors and follow redirects (curl -sfL...). It does not, in the portions shown, include an additional integrity check such as verifying a checksum before executing the result [3]. 3) Crossplane’s docs also provide a manual alternative that avoids running the downloaded shell script: you can download the CLI binary directly from the releases repository for your channel/version [1]. Recommended safer patterns you can use: A) Pin the CLI artifact version (still runs unpinned install.sh): XP_VERSION=v1.20.0 curl -sL "https://raw.githubusercontent.com/crossplane/crossplane/main/install.sh" | sh [1] B) Pin the install script itself (unpinned CLI artifact risk remains unless you also pin XP_VERSION/XP_CHANNEL): - Prefer using a raw URL that targets a specific crossplane/crossplane commit hash for install.sh, rather than the moving /main/ (or /master/) path. The exact content of install.sh can be verified via the raw file at that commit [4]. C) Avoid executing install.sh (most “no-script” approach): - Manually download the CLI binary from the releases repository for your desired channel/version (docs call out that the release repo names the CLI binary “crank”) [1].

Citations:


🌐 Web query:

raw.githubusercontent.com/crossplane/crossplane/v2.0.2/install.sh

💡 Result:

The URL you provided (raw.githubusercontent.com/crossplane/crossplane/v2.0.2/install.sh) is an installation script for the Crossplane CLI binary (not the Crossplane Helm chart / controller itself). The script’s job is to download the appropriate CLI binary for your OS/CPU and place it as a local executable (typically into /usr/local/bin via the post-install instructions). It supports selecting release “channel” and “version” via XP_CHANNEL and XP_VERSION environment variables, and handles “older” versions that used the crank binary/artifacts vs newer versions that use crossplane/crossplane-cli artifacts. Key behaviors (from the script logic visible in the repository’s install.sh): 1) Default environment variables - XP_CHANNEL defaults to “stable”. - XP_VERSION defaults to “current”. - BIN defaults to “crossplane”. [1] 2) OS/architecture detection - The script uses uname -s and uname -m to build the OS and ARCH/OS_ARCH values used in the download URL. [1] 3) Artifact URL construction - It constructs a download URL of the form: https://<url_host>/<XP_CHANNEL>/<XP_VERSION>/<url_dir>/<OS_ARCH>/<url_file> [1] - For “compressed” downloads (COMPRESSED=true) it downloads a tarball (crossplane-cli.tar.gz); otherwise it downloads the binary directly. [1] 4) Compatibility logic for older CLI releases - If XP_VERSION is not “current”, it parses the major/minor version. - For versions earlier than 2.3 (major==2 and minor<3, or major<2), it switches to: - url_host = releases.crossplane.io - bundle_name = crank.tar.gz - BIN = crank Otherwise it uses: - url_host = cli.crossplane.io - bundle_name = crossplane-cli.tar.gz - BIN name “crossplane” [1] 5) Download, unpack, and rename - It downloads with curl -sfL and exits with an error if the requested version/channel artifacts are missing. [1] - If a tarball was downloaded, it unpacks it with tar xzf and then removes the sha256 + tarball. [1] - If the downloaded binary name is not “crossplane” (e.g., crank for older versions), it renames it to crossplane. [1] 6) Finalization - It chmod +x crossplane. [1] - It prints instructions to move it to /usr/local/bin and run crossplane --help. [1] How this relates to official documentation (current mechanism) - Crossplane’s CLI docs describe using an install script from the raw GitHub URL, e.g. curl -sL "https://raw.githubusercontent.com/crossplane/crossplane/main/install.sh" | sh, and explain that the script detects CPU architecture and downloads the latest stable release. [2] - The docs also describe XP_CHANNEL and XP_VERSION variables for selecting other CLI versions/branches, matching the same variables used inside install.sh. [2] Note on “today’s” date / version targeting - Your request specifies v2.0.2, but the web results I could retrieve in the first search round show the install.sh content from the repository’s master branch (URL ending in /crossplane/master/install.sh). The code patterns above are therefore grounded in that retrieved install.sh content; the same script template is what the docs refer to for installing any version by pinning XP_VERSION/XP_CHANNEL or by pointing at a release-specific install.sh URL. [1][2]

Citations:


🌐 Web query:

Does raw.githubusercontent.com/crossplane/crossplane/v2.0.2/install.sh exist? 404 or valid

💡 Result:

The URL raw.githubusercontent.com/crossplane/crossplane/v2.0.2/install.sh does not exist (it returns 404), while the install script does exist for other paths/tags (e.g., master has https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh). [1] What I found: 1) Crossplane’s official docs reference the installer at raw.githubusercontent.com/crossplane/crossplane/main/install.sh / install.sh from the repository’s main branch (not v2.0.2). [2] 2) I could locate the repository’s install.sh content on master (but not on the v2.0.2 tag path). [1] So for v2.0.2 specifically, expect 404 at that raw.githubusercontent.com path.

Citations:


🌐 Web query:

Crossplane CLI docs pin install.sh commit hash recommended

💡 Result:

There is no official recommendation to pin the Crossplane install.sh script to a specific commit hash, and the script is intended to be fetched from the main branch to ensure the most recent installation logic is used [1][2][3]. For production or reproducible environments where you require a consistent CLI version, the official documentation instead recommends pinning the version of the Crossplane CLI binary itself using the XP_VERSION environment variable [3][4]. To install a specific version, use: curl -sL "https://raw.githubusercontent.com/crossplane/crossplane/main/install.sh" | XP_VERSION=v1.20.0 sh This ensures that regardless of updates to the install.sh script, you receive the specific binary version desired [3][4]. While the script is currently maintained in the main branch of the Crossplane repository [1][2], security discussions have noted the risks of piping directly to shell and have proposed future improvements for integrity verification [5]. Always prefer fetching official release versions as documented in the Crossplane CLI reference [3][4].

Citations:


Avoid executing an unpinned Crossplane installer via curl | sh in on-pr workflow.
Line 60 downloads https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh (moving ref) and executes it via | ... sh; while XP_VERSION=v2.0.2 pins the downloaded CLI binary, the installer script itself remains mutable, weakening provenance/reproducibility.

  • Download install.sh to a file (with curl --fail --location --retry ...) and execute the saved script instead of piping to sh.
  • Pin install.sh to an immutable ref (commit SHA) rather than master (the .../v2.0.2/install.sh path returns 404).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/on-pr.yaml around lines 58 - 62, The workflow step
"Install Crossplane CLI" currently pipes the mutable installer at
.../master/install.sh into sh; instead download the installer to a file and
execute that saved file, and pin the installer URL to an immutable commit SHA
instead of master. Replace the curl | sh usage by using curl --fail --location
--retry (or similar) to fetch the installer to a local file (e.g., install.sh),
make it executable, and run it with XP_VERSION=v2.0.2 ./install.sh; update the
URL to point to the specific commit SHA for the installer script in the
crossplane repo to ensure reproducibility.

- name: Docker login to GHCR
uses: docker/login-action@v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}

- name: Build with up
uses: upbound/action-up-project@v1.1.2
with:
skip-login-check: true

- name: Validate all examples
run: scripts/ci-validate-all.sh

test:
uses: unbounded-tech/workflows-crossplane/.github/workflows/test.yaml@v3.0.0
Expand Down
72 changes: 47 additions & 25 deletions .github/workflows/on-push-main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ on:
- ".github/workflows/on-push-main.yaml"
- ".github/workflows/on-version-tagged.yaml"
- ".gitops/**"
- "Makefile"
- "apis/**"
- "examples/**"
- "functions/**"
- "scripts/**"
- "tests/**"
- "upbound.yaml"

Expand All @@ -23,31 +25,51 @@ permissions:

jobs:
validate:
uses: unbounded-tech/workflows-crossplane/.github/workflows/validate.yaml@v3.0.0
with:
examples: |
[
{ "example": "examples/crossplanestacks/minimal.yaml", "api_path": "apis/crossplanestacks" },
{ "example": "examples/crossplanestacks/standard.yaml", "api_path": "apis/crossplanestacks" },
{ "example": "examples/crossplanestacks/full.yaml", "api_path": "apis/crossplanestacks" },
{ "example": "examples/awsproviderstacks/minimal.yaml", "api_path": "apis/awsproviderstacks" },
{ "example": "examples/awsproviderstacks/full.yaml", "api_path": "apis/awsproviderstacks" },
{ "example": "examples/functionsstacks/minimal.yaml", "api_path": "apis/functionsstacks" },
{ "example": "examples/functionsstacks/full.yaml", "api_path": "apis/functionsstacks" },
{ "example": "examples/githubproviderstacks/minimal.yaml", "api_path": "apis/githubproviderstacks" },
{ "example": "examples/githubproviderstacks/full.yaml", "api_path": "apis/githubproviderstacks" },
{ "example": "examples/helmproviderstacks/minimal.yaml", "api_path": "apis/helmproviderstacks" },
{ "example": "examples/helmproviderstacks/full.yaml", "api_path": "apis/helmproviderstacks" },
{ "example": "examples/kubernetesproviderstacks/minimal.yaml", "api_path": "apis/kubernetesproviderstacks" },
{ "example": "examples/kubernetesproviderstacks/full.yaml", "api_path": "apis/kubernetesproviderstacks" },
{ "example": "examples/listmonkproviderstacks/minimal.yaml", "api_path": "apis/listmonkproviderstacks" },
{ "example": "examples/listmonkproviderstacks/full.yaml", "api_path": "apis/listmonkproviderstacks" },
{ "example": "examples/openpanelproviderstacks/minimal.yaml", "api_path": "apis/openpanelproviderstacks" },
{ "example": "examples/openpanelproviderstacks/full.yaml", "api_path": "apis/openpanelproviderstacks" },
{ "example": "examples/zitadelproviderstacks/minimal.yaml", "api_path": "apis/zitadelproviderstacks" },
{ "example": "examples/zitadelproviderstacks/full.yaml", "api_path": "apis/zitadelproviderstacks" }
]
error_on_missing_schemas: true
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
Comment on lines +30 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Inspect the workflow file around the referenced lines
FILE=".github/workflows/on-push-main.yaml"
echo "== File: $FILE =="
if [ -f "$FILE" ]; then
  nl -ba "$FILE" | sed -n '1,120p'
else
  echo "Missing: $FILE"
fi

# Also list other workflows that might share the same patterns (for comparison)
echo
echo "== Other workflows referencing this file (if any) =="
fd .github/workflows -t f | sort

Repository: hops-ops/aws-crossplane-stack

Length of output: 182


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE=".github/workflows/on-push-main.yaml"

echo "== File: $FILE =="
if [ -f "$FILE" ]; then
  # Print with line numbers without nl
  awk '{printf "%d\t%s\n", NR, $0}' "$FILE" | sed -n '1,140p'
else
  echo "Missing: $FILE"
fi

echo
echo "== Repo .github/workflows files =="
fd ".github/workflows" -t f | sort || true

Repository: hops-ops/aws-crossplane-stack

Length of output: 3891


🌐 Web query:

GitHub Actions checkout persist-credentials default value actions/checkout@v6

💡 Result:

For actions/checkout@v6, the persist-credentials input’s “default value” is effectively true (enabled) when you don’t set it in your workflow, because the action.yml defines persist-credentials with a default of an empty string (meaning use the action’s built-in default behavior) and the v6 README describes that credentials are persisted unless you opt out with persist-credentials: false [1][2]. What the action does: - The auth token is persisted in the local git config so that subsequent authenticated git commands work in the job; the token is removed during post-job cleanup. You can disable this by setting persist-credentials: false [2]. So if you write: - uses: actions/checkout@v6 (without a persist-credentials key), it behaves as “persist-credentials enabled” [1][2].

Citations:


Harden checkout credentials and pin third-party actions to immutable refs

  • on-push-main.yaml uses actions/checkout@v6 without persist-credentials: false, so checkout credentials are persisted by default on the main push path.
  • actions/checkout@v6 (line 31), actions/cache@v5 (line 34), docker/login-action@v3.7.0 (line 60), and upbound/action-up-project@v1.1.2 (line 67) are pinned only to version tags (not commit SHAs), leaving avoidable supply-chain risk.
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 30-31: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 31-31: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/on-push-main.yaml around lines 30 - 31, Update the
checkout step and third-party action pins to harden credentials and reduce
supply-chain risk: for the Checkout step (actions/checkout@v6) add
persist-credentials: false to prevent token persistence, and replace the tag
pins for actions/checkout, actions/cache@v5, docker/login-action@v3.7.0, and
upbound/action-up-project@v1.1.2 with their corresponding immutable commit SHAs
(pin each action to a specific rev) while preserving existing inputs and
behavior.

Source: Linters/SAST tools


- name: Cache crossplane
uses: actions/cache@v5
with:
path: |
~/.crossplane/cache
~/.up/cache
~/.up/build-cache
key: ${{ runner.os }}-crossplane-${{ hashFiles('**/upbound.yaml','**/definition.yaml') }}-

- name: Install up
env:
UP_VERSION: v0.46.0
run: |
set -euo pipefail
curl --fail --location --retry 5 --retry-all-errors --retry-delay 5 \
"https://cli.upbound.io/stable/${UP_VERSION}/bin/linux_amd64/up" \
--output up
chmod +x up
sudo mv up /usr/local/bin/up
up version

- name: Install Crossplane CLI
run: |
curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh \
| XP_VERSION=v2.0.2 sh
Comment on lines +54 to +57

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the referenced workflow lines
sed -n '1,120p' .github/workflows/on-push-main.yaml | nl -ba | sed -n '40,80p'

# Search for other uses of the Crossplane install script URL (master or otherwise)
rg -n "crossplane/crossplane/(master|main)/install\.sh|XP_VERSION|Install Crossplane CLI|install\.sh \\\"" -S .github/workflows . || true

# Search for any other Crossplane installer/provenance handling
rg -n "crossplane.*install\.sh|XP_VERSION=" -S .github/workflows . || true

Repository: hops-ops/aws-crossplane-stack

Length of output: 116


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Workflow snippet around lines 50-70 ==="
# Use sed with explicit range; no line-number printer needed
sed -n '45,75p' .github/workflows/on-push-main.yaml

echo
echo "=== Search for Crossplane installer URL patterns ==="
rg -n "crossplane/crossplane/.*/install\.sh|XP_VERSION=|Install Crossplane CLI|XP_VERSION=" .github/workflows -S || true

echo
echo "=== Search repo for 'install.sh' invocations ==="
rg -n "install\.sh" . -S || true

Repository: hops-ops/aws-crossplane-stack

Length of output: 1748


Pin the Crossplane installer source instead of using the moving master branch

The workflow downloads https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh and pipes it directly to sh, making CI depend on a moving branch even though XP_VERSION=v2.0.2 is set. This same pattern exists in both .github/workflows/on-push-main.yaml and .github/workflows/on-pr.yaml.

      - name: Install Crossplane CLI
        run: |
          curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh \
            | XP_VERSION=v2.0.2 sh

Use a pinned commit/tag for the installer URL (and ideally verify provenance, e.g., checksum/signature) to keep branch-to-branch CI deterministic and safer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/on-push-main.yaml around lines 54 - 57, The "Install
Crossplane CLI" step currently curls the installer from the moving master
branch; update the run command in that step (and the identical step in the other
workflow) to fetch the installer from a pinned tag or commit URL (e.g., replace
.../master/install.sh with the specific release tag or commit SHA for
Crossplane), and optionally add a checksum or signature verification step after
download before piping to sh to ensure provenance.


- name: Docker login to GHCR
uses: docker/login-action@v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}

- name: Build with up
uses: upbound/action-up-project@v1.1.2
with:
skip-login-check: true

- name: Validate all examples
run: scripts/ci-validate-all.sh

test:
uses: unbounded-tech/workflows-crossplane/.github/workflows/test.yaml@v3.0.0
Expand Down
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,11 @@ validate\:all: generate-configuration
done

# Shorthand aliases
.PHONY: render validate generate-configuration
.PHONY: render validate generate-configuration print-examples
render: ; @$(MAKE) 'render:all'
validate: ; @$(MAKE) generate-configuration 'validate:all'
print-examples:
@printf '%s\n' $(EXAMPLES)

# Single example targets
render\:%:
Expand Down
155 changes: 155 additions & 0 deletions scripts/ci-validate-all.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#!/usr/bin/env bash
set -euo pipefail

crossplane_bin="${CROSSPLANE_BIN:-./crossplane}"
if [ ! -x "$crossplane_bin" ]; then
crossplane_bin="$(command -v crossplane)"
fi

configuration_from_upbound() {
local api_dir="$1"
local output_file="${api_dir}/configuration.yaml"

mkdir -p "$api_dir"

python3 - upbound.yaml "$output_file" <<'PY'
import re
import sys
import tempfile

try:
import yaml
except Exception:
target = f"{tempfile.gettempdir()}/aws-crossplane-stack-pyyaml"
sys.path.insert(0, target)
try:
import yaml
except Exception:
import subprocess
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "--quiet", "--target", target, "PyYAML"]
)
import yaml

in_file, out_file = sys.argv[1], sys.argv[2]

with open(in_file, "r", encoding="utf-8") as f:
project = yaml.safe_load(f) or {}

metadata = project.get("metadata") or {}
spec = project.get("spec") or {}

name = metadata.get("name")
if not name:
raise SystemExit("upbound.yaml is missing metadata.name")

maintainer = spec.get("maintainer", "")
if isinstance(maintainer, str):
maintainer = re.sub(r"\s*<[^>]+>\s*$", "", maintainer).strip()

annotations = {
"meta.crossplane.io/maintainer": maintainer,
"meta.crossplane.io/source": spec.get("source", ""),
"meta.crossplane.io/description": spec.get("description", ""),
}

depends_on = []
for dep in spec.get("dependsOn") or []:
if not isinstance(dep, dict):
continue

kind = str(dep.get("kind", "")).strip().lower()
package = dep.get("package")
version = dep.get("version")
if not package:
continue

if kind == "provider":
item = {"provider": package}
elif kind == "function":
item = {"function": package}
elif kind == "configuration":
item = {"configuration": package}
else:
continue

if version is not None:
item["version"] = version
depends_on.append(item)

output = {
"apiVersion": "meta.pkg.crossplane.io/v1alpha1",
"kind": "Configuration",
"metadata": {
"name": name,
"annotations": {k: v for k, v in annotations.items() if v},
},
"spec": {
"dependsOn": depends_on,
},
}

with open(out_file, "w", encoding="utf-8") as f:
yaml.safe_dump(output, f, sort_keys=False)

print(f"Wrote {out_file}")
PY
}

render_with_retry() {
local attempt=1
local max_attempts="${RENDER_RETRIES:-3}"
local retry_delay="${RENDER_RETRY_DELAY_SECONDS:-5}"
local status=0
local output

output="$(mktemp)"
while true; do
if up composition render "$@" >"$output"; then
cat "$output"
rm -f "$output"
return 0
fi

status=$?
rm -f "$output"
if [ "$attempt" -ge "$max_attempts" ]; then
return "$status"
fi
Comment on lines +108 to +118

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

render_with_retry can return the wrong status after a failed render.

Line 114 captures $? after the if compound, so the final failure path can return a non-render status and let validation proceed incorrectly. Capture the render exit code inside the else branch.

Suggested fix
 	while true; do
-		if up composition render "$@" >"$output"; then
+		if up composition render "$@" >"$output"; then
 			cat "$output"
 			rm -f "$output"
 			return 0
+		else
+			status=$?
 		fi
-
-		status=$?
 		rm -f "$output"
 		if [ "$attempt" -ge "$max_attempts" ]; then
 			return "$status"
 		fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ci-validate-all.sh` around lines 108 - 118, The render step in
render_with_retry captures $? after the entire if compound which can grab the
wrong exit code; move the capture of the render command's exit status
(status=$?) into the else branch immediately after the failed "up composition
render \"$@\" >\"$output\"" so you record the render exit code instead of the
if's exit code, then proceed to rm -f "$output" and the existing retry/return
logic using that status variable.


echo "Render attempt ${attempt}/${max_attempts} failed; retrying in ${retry_delay}s..." >&2
sleep "$retry_delay"
attempt=$((attempt + 1))
output="$(mktemp)"
done
}

while IFS= read -r entry; do
[ -n "$entry" ] || continue

example="${entry%%::*}"
observed="${entry#*::}"
api_dir="$(awk -F/ '{print "apis/" $2}' <<<"$example")"
composition="${api_dir}/composition.yaml"
definition="${api_dir}/definition.yaml"
render_args=(--xrd="$definition" "$composition" "$example")

if [ -n "$observed" ]; then
render_args+=(--observed-resources="$observed")
fi

echo "=== Validating ${example} (api=${api_dir}) ==="
configuration_from_upbound "$api_dir"

render_with_retry "${render_args[@]}" --quiet >/dev/null

"$crossplane_bin" beta validate "$example" "$api_dir"

render_with_retry \
"${render_args[@]}" \
--include-full-xr \
--quiet \
| "$crossplane_bin" beta validate "$api_dir" --error-on-missing-schemas -

echo ""
done < <(make --no-print-directory print-examples)
Loading