diff --git a/.ci/tests/integration-oauth2/e2e.yaml b/.ci/tests/integration-oauth2/e2e.yaml index a0725a4ee..679a880cf 100644 --- a/.ci/tests/integration-oauth2/e2e.yaml +++ b/.ci/tests/integration-oauth2/e2e.yaml @@ -11,7 +11,13 @@ setup: - name: install helm command: | - curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + # Pin the executable too, not only the surrounding GitHub Action. + helm_dir=$(mktemp -d) + curl --fail --silent --show-error --location -o "$helm_dir/helm.tar.gz" https://get.helm.sh/helm-v3.22.1-linux-amd64.tar.gz + echo "896cf63684a42a723d61c980369d6e588330927deb13676ca0d85006dd6da0ea $helm_dir/helm.tar.gz" | sha256sum --check --strict + tar -xzf "$helm_dir/helm.tar.gz" -C "$helm_dir" + sudo install "$helm_dir/linux-amd64/helm" /usr/local/bin/helm + rm -rf "$helm_dir" - name: setup cert-manager command: | @@ -49,7 +55,9 @@ setup: - name: install pulsar cluster command: | rm -rf pulsar-charts/ - git clone --branch pulsar-operator-0.17.10 https://github.com/streamnative/charts.git pulsar-charts + git clone --no-checkout https://github.com/streamnative/charts.git pulsar-charts + # Verified pulsar-operator-0.17.10 commit; do not execute a movable tag. + git -C pulsar-charts checkout --detach 0f64202d0bd529dea5ab27bbe27ef6be84b2bac9 cd pulsar-charts/ ./scripts/pulsar/prepare_helm_release.sh -n default -k ${PULSAR_RELEASE_NAME} -c helm repo add grafana https://grafana.github.io/helm-charts diff --git a/.ci/tests/integration-oauth2/e2e_with_downloader.yaml b/.ci/tests/integration-oauth2/e2e_with_downloader.yaml index 2542c7891..c869f1e89 100644 --- a/.ci/tests/integration-oauth2/e2e_with_downloader.yaml +++ b/.ci/tests/integration-oauth2/e2e_with_downloader.yaml @@ -11,7 +11,13 @@ setup: - name: install helm command: | - curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + # Pin the executable too, not only the surrounding GitHub Action. + helm_dir=$(mktemp -d) + curl --fail --silent --show-error --location -o "$helm_dir/helm.tar.gz" https://get.helm.sh/helm-v3.22.1-linux-amd64.tar.gz + echo "896cf63684a42a723d61c980369d6e588330927deb13676ca0d85006dd6da0ea $helm_dir/helm.tar.gz" | sha256sum --check --strict + tar -xzf "$helm_dir/helm.tar.gz" -C "$helm_dir" + sudo install "$helm_dir/linux-amd64/helm" /usr/local/bin/helm + rm -rf "$helm_dir" - name: setup cert-manager command: | @@ -50,7 +56,9 @@ setup: - name: install pulsar cluster command: | rm -rf pulsar-charts/ - git clone --branch pulsar-operator-0.17.10 https://github.com/streamnative/charts.git pulsar-charts + git clone --no-checkout https://github.com/streamnative/charts.git pulsar-charts + # Verified pulsar-operator-0.17.10 commit; do not execute a movable tag. + git -C pulsar-charts checkout --detach 0f64202d0bd529dea5ab27bbe27ef6be84b2bac9 cd pulsar-charts/ ./scripts/pulsar/prepare_helm_release.sh -n default -k ${PULSAR_RELEASE_NAME} -c helm repo add grafana https://grafana.github.io/helm-charts diff --git a/.ci/tests/integration/e2e.yaml b/.ci/tests/integration/e2e.yaml index 0b7e81d65..fafb6c27c 100644 --- a/.ci/tests/integration/e2e.yaml +++ b/.ci/tests/integration/e2e.yaml @@ -11,7 +11,13 @@ setup: - name: install helm command: | - curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + # Pin the executable too, not only the surrounding GitHub Action. + helm_dir=$(mktemp -d) + curl --fail --silent --show-error --location -o "$helm_dir/helm.tar.gz" https://get.helm.sh/helm-v3.22.1-linux-amd64.tar.gz + echo "896cf63684a42a723d61c980369d6e588330927deb13676ca0d85006dd6da0ea $helm_dir/helm.tar.gz" | sha256sum --check --strict + tar -xzf "$helm_dir/helm.tar.gz" -C "$helm_dir" + sudo install "$helm_dir/linux-amd64/helm" /usr/local/bin/helm + rm -rf "$helm_dir" - name: setup cert-manager command: | @@ -53,7 +59,9 @@ setup: - name: install pulsar cluster command: | rm -rf pulsar-charts/ - git clone --branch pulsar-operator-0.17.10 https://github.com/streamnative/charts.git pulsar-charts + git clone --no-checkout https://github.com/streamnative/charts.git pulsar-charts + # Verified pulsar-operator-0.17.10 commit; do not execute a movable tag. + git -C pulsar-charts checkout --detach 0f64202d0bd529dea5ab27bbe27ef6be84b2bac9 cd pulsar-charts/ ./scripts/pulsar/prepare_helm_release.sh -n default -k ${PULSAR_RELEASE_NAME} -c helm repo add grafana https://grafana.github.io/helm-charts diff --git a/.ci/tests/integration/e2e_with_tls.yaml b/.ci/tests/integration/e2e_with_tls.yaml index 8f7b98be2..e1eb69277 100644 --- a/.ci/tests/integration/e2e_with_tls.yaml +++ b/.ci/tests/integration/e2e_with_tls.yaml @@ -11,7 +11,13 @@ setup: - name: install helm command: | - curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + # Pin the executable too, not only the surrounding GitHub Action. + helm_dir=$(mktemp -d) + curl --fail --silent --show-error --location -o "$helm_dir/helm.tar.gz" https://get.helm.sh/helm-v3.22.1-linux-amd64.tar.gz + echo "896cf63684a42a723d61c980369d6e588330927deb13676ca0d85006dd6da0ea $helm_dir/helm.tar.gz" | sha256sum --check --strict + tar -xzf "$helm_dir/helm.tar.gz" -C "$helm_dir" + sudo install "$helm_dir/linux-amd64/helm" /usr/local/bin/helm + rm -rf "$helm_dir" - name: setup cert-manager command: | @@ -45,7 +51,9 @@ setup: - name: install pulsar cluster command: | rm -rf pulsar-charts/ - git clone --branch pulsar-operator-0.17.10 https://github.com/streamnative/charts.git pulsar-charts + git clone --no-checkout https://github.com/streamnative/charts.git pulsar-charts + # Verified pulsar-operator-0.17.10 commit; do not execute a movable tag. + git -C pulsar-charts checkout --detach 0f64202d0bd529dea5ab27bbe27ef6be84b2bac9 cd pulsar-charts/ ./scripts/pulsar/prepare_helm_release.sh -n default -k ${PULSAR_RELEASE_NAME} -c helm repo add grafana https://grafana.github.io/helm-charts diff --git a/.github/WORKFLOW_SECURITY.md b/.github/WORKFLOW_SECURITY.md new file mode 100644 index 000000000..84fe80a88 --- /dev/null +++ b/.github/WORKFLOW_SECURITY.md @@ -0,0 +1,117 @@ +# GitHub Actions security boundaries + +Reviewed against `eced1f28126f7ea789522b1a7dd1a59331659f71` (2026-11-11). +The audit covers all 11 existing workflows, both local actions, their checkout, +credential, execution, token, cache and artifact paths, and directly invoked +integration configuration. This change adds a separate trusted cloud suite and +a workflow security regression check. It does not change repository/organization +settings, secrets, cloud IAM, or release registries. + +## Findings and changes + +| Existing workflow | Finding | Resulting boundary | +| --- | --- | --- | +| `test-integration-skywalking-e2e.yml` | `pull_request_target` selected PR head code and a PR-editable SSH action while Azure/GSA credentials were in job-wide environment. | Ordinary `pull_request`, normal merge-ref checkout, read-only token, no secrets. Basic and TLS cases remain. OAuth2/downloader move to `test-integration-cloud.yml`, accepting only official-repository `master` push/manual events with no arbitrary ref input. GSA is supplied only to the OAuth2 case. | +| `test-function-runner.yml` | Automatic SSH access, persisted checkout credential, implicit token permissions. | Read-only normal PR checkout, no persisted credential or SSH; runner-image tests remain. | +| `test-helm-charts.yml` | Automatic tmate session; implicit token permissions; old kubebuilder action downloaded unchecked executables. | Read-only PR job, no terminal, checksum-verified kubebuilder installation. Helm tests remain. | +| `olm-verify.yml` | Automatic tmate, implicit permissions, unchecked downloaded tools. | Read-only PR job, no terminal, checksum-verified tools. Local-registry OLM verification remains. | +| `project.yml` | Implicit permissions, obsolete checkout, unchecked tools and mutable VPA CRD. | Read-only PR job, non-persisted checkout, pinned tools/CRD. Unit/style/license/envtest checks remain. | +| `trivy.yml` | Requested issue/PR write access despite only building/scanning PR code. | `contents: read` only, no repository secrets, no persisted credentials or action-managed cache. | +| `trivy_scheduled_master.yml` | Also ran for PRs with issue/PR/security write permissions and PR-selected code. | Scheduled/manual official `master` scans only. Build job is read-only; same-run SARIF files are consumed by an isolated upload-only job with `security-events: write`. Native SARIF replaces the obsolete Docker-specific `/contrib/sarif.tpl` path. | +| `documentbot.yml` | Privileged target event used mutable third-party code, unnecessary checkout, and allowed arbitrary existing label names from PR checkboxes. | Metadata-only pinned `github-script`, no checkout/shell, only `pull-requests: write`, and exact `doc-required`, `no-need-doc`, `doc` allowlist. PR text is data, never script interpolation. | +| `release-note.yml` | PR trigger did not need publishing privileges, while the actual release-drafting operation had only read access. | Official `master` push only, no checkout or arbitrary build, job-scoped contents write / PR read. Automatic labeling disabled. | +| `release.yml` | Build code shared a runner with contents-write credentials; release ref only checked a prefix. | Strict version-tag syntax and reachability from current `master` validated before credential jobs. Build checks out the resolved immutable SHA with read-only token. Release-file upload has a separate write-only job consuming this run's artifacts without executing them. | +| `bundle-release.yml` | Arbitrary dispatch ref, early registry credentials, persisted cross-repo PAT, mutable remote helper and preflight image; auth JSON explicitly printed. | Dispatch workflow must be on official `master`; tag must resolve to a commit reachable from `master` before publishing jobs start. Immutable SHA checkout, checksum tools, pinned preflight digest, tag's own helper/template, no persisted checkout tokens. Cross-repo git authentication is scoped to the publishing step. Auth JSON is private temporary data, never printed, removed via trap; verbose trace disabled. | + +The deleted `ssh-access` composite action is no longer callable. The unused +`tune-runner-vm` action was inspected: it changes ephemeral runner settings but +has no current callers, credentials or external downloads, so it is unchanged. +All third-party workflow actions now use verified full commit SHAs. Direct +kubebuilder/operator-sdk/yq/Helm binary downloads are checked against recorded +upstream SHA-256 values before execution. The VPA CRD is commit-pinned. E2E Helm +installation no longer pipes mutable `main` code into a secret-bearing shell; +the Pulsar charts preparation script is checked out at its verified release +commit before execution. + +## Intentional behavior changes + +- Cloud OAuth2/downloader coverage runs after merge or an explicit dispatch on + `master`, never against an unmerged PR. No new environment is implicitly created + or assumed to have approvals. The workflow deliberately has no PR ref input. +- Normal fork PR approval and secret-withholding settings remain administrator + controls. Approval is not a reason to execute PR code with cloud credentials. +- PR concurrency is keyed by PR number, so unrelated PRs do not cancel each other. +- Automatic SSH/upterm/tmate debugging is removed, not merely actor-restricted. +- The documentation bot only synchronizes the three documentation labels. It no + longer auto-assigns authors, posts comments, manages `doc-info-missing`, or uses + the former `doc-added` bypass. +- Release inputs must be `vX.Y.Z` or `vX.Y.Z-prerelease` tags reachable from + `master`. Arbitrary branches/SHAs/PR refs and unmerged release branches are not + accepted. Historical releases must contain the required build/helper files. +- Build caches are disabled rather than shared across privileged and PR jobs. + This trades speed for a smaller trust surface. No `workflow_run`, cross-run + artifact download, OIDC permission, or self-hosted runner is introduced. + +## Residual controls and limitations + +1. **Protect the trust roots outside YAML.** Require review and CODEOWNERS on + `master`, workflow changes and release tags; limit who can push/dispatch, + publish releases, edit repository secrets, or approve environments. These + settings were not changed or assumed verified. A writer who can replace the + trusted workflow can remove its guards. New YAML does not retroactively alter + old tag workflows or runs already started under the old target workflow. +2. **Review/rotate incident credentials separately.** Removing a vulnerable path + does not invalidate previously disclosed cloud/registry/PAT credentials or + erase old logs. No credential value was read or copied into this change. +3. **Trusted code still executes with necessary credentials.** Cloud E2E needs + Azure/GSA values; publishing needs registry credentials and a cross-repo bot + token. Step scope reduces accidental exposure, not runner-level isolation. + Use narrowly scoped service identities and independently protected environments + if required. This change does not migrate cloud authentication to OIDC or + guarantee downstream tools redact all transformed/structured secrets. +4. **Supply-chain hardening is not complete dependency reproducibility.** Pinned + actions and checked binaries still rely on upstream code, hosted runner + images, package ecosystems, Docker base images, Helm charts and registries. + Chart-testing's pinned action installs transitive Python dependencies; the + pinned SkyWalking action builds its tool with Docker dependencies. Build/test + scripts elsewhere still fetch external tools or manifests. Digest-locking and + verifying all transitive dependencies is a separate broader build change. + Trivy vulnerability databases intentionally update. Continue reviewing pin + updates; a pinned old version is not automatically vulnerability-free. +5. **Artifacts are not executed.** Release/SARIF upload jobs only download named + artifacts from their own successful trusted build in the same run. They do + not accept PR artifacts, run IDs, or arbitrary download URLs. They still + depend on the trusted producer and the pinned upload/parser implementation. +6. **Current GitHub defenses are not ignored.** The then-current mutable + checkout v3 included a guard against unsafe fork head checkout on target + events. This can make the original fork workflow fail instead of execute; + it does not make target-event execution of same-repository PR code with + secrets an appropriate design. No claim is made that every fork could still + exploit the original workflow at audit time, that tmate was universally + public, or that fork caches could overwrite default-branch caches. + +## Validation + +Run from the repository root with Python 3.12, Node and Git available: + +```sh +python -m pip install --only-binary=:all: --require-hashes -r .github/scripts/workflow-security-requirements.txt +python .github/scripts/test_workflow_security.py +actionlint -shellcheck='' -pyflakes='' +git diff --check +``` + +The dependency hash is for the CPython 3.12 Linux x86_64 wheel used by CI. +The new workflow installs checksum-verified actionlint 1.7.7 and runs these checks. +Tests parse every workflow, reject sample unsafe mutations, verify secret/token +and upload boundaries, execute the actual tag-validation shell against isolated +local Git histories (including injection, missing and unmerged tags), exercise +the actual label JavaScript with hostile body data, and syntax-check shell blocks. +They are regression checks, not a substitute for review or branch protection. + +Local validation does **not** execute Kubernetes/Docker E2E, publish an image or +release, submit a Red Hat certification, expose credentials, or prove GitHub's +server-side event/permission behavior. Real Actions results must be checked for +the exact PR head. Trusted workflows should be verified by maintainers after +merge using approved test identities; no release workflow was dispatched as a +validation shortcut. diff --git a/.github/actions/ssh-access/action.yml b/.github/actions/ssh-access/action.yml deleted file mode 100644 index 07a363ddd..000000000 --- a/.github/actions/ssh-access/action.yml +++ /dev/null @@ -1,157 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# - -name: ssh access -description: Sets up SSH access to build VM with upterm -inputs: - action: - description: | - Action to perform: options are "start" and "wait" - "start" will install, configure and start upterm. - "wait" will wait until a connection is established to upterm and will continue to wait until the session is closed. - required: false - default: 'start' - limit-access-to-actor: - description: 'If only the public SSH keys of the user triggering the workflow should be authorized' - required: false - default: 'false' - limit-access-to-users: - description: 'If only the public SSH keys of the listed GitHub users should be authorized. Comma separate list of GitHub user names.' - required: false - default: '' - secure-access: - description: | - Set to false for allowing public access when limit-access-to-actor and limit-access-to-users are unset. - required: false - default: 'true' - timeout: - description: 'When action=wait, the timeout in seconds to wait for the user to connect' - required: false - default: '300' -runs: - using: composite - steps: - - run: | - if [[ "${{ inputs.action }}" == "start" ]]; then - echo "::group::Installing upterm & tmux" - if [[ "$OSTYPE" == "linux-gnu"* ]]; then - # install upterm - curl -sL https://github.com/owenthereal/upterm/releases/download/v0.20.0/upterm_linux_amd64.tar.gz | tar zxvf - -C /tmp upterm && sudo install /tmp/upterm /usr/local/bin/ && rm -rf /tmp/upterm - - # install tmux if it's not present - if ! command -v tmux &>/dev/null; then - sudo apt-get -y install tmux - fi - elif [[ "$OSTYPE" == "darwin"* ]]; then - brew install --cask owenthereal/upterm/upterm - # install tmux if it's not present - if ! command -v tmux &>/dev/null; then - brew install tmux - fi - else - echo "Unsupported $OSTYPE" - exit 0 - fi - echo '::endgroup::' - echo "::group::Configuring ssh and ssh keys" - # generate ssh key - mkdir -p ~/.ssh - chmod 0700 ~/.ssh - if [ ! -f ~/.ssh/id_rsa ]; then - ssh-keygen -q -t rsa -N "" -f ~/.ssh/id_rsa - fi - if [ ! -f ~/.ssh/id_ed25519 ]; then - ssh-keygen -q -t ed25519 -N "" -f ~/.ssh/id_ed25519 - fi - # configure ssh - echo -e "Host *\nStrictHostKeyChecking no\nCheckHostIP no\nTCPKeepAlive yes\nServerAliveInterval 30\nServerAliveCountMax 180\nVerifyHostKeyDNS yes\nUpdateHostKeys yes\nAddressFamily inet\n" > ~/.ssh/config - authorizedKeysParameter="" - authorizedKeysFile=${HOME}/.ssh/authorized_keys - if [[ "${{ inputs.secure-access }}" != "false" ]]; then - ssh-keygen -q -t ed25519 -N "$(echo $RANDOM | md5sum | awk '{ print $1 }')" -C "Prevent public access" -f /tmp/dummykey$$ - cat /tmp/dummykey$$.pub >> $authorizedKeysFile - rm /tmp/dummykey$$ /tmp/dummykey$$.pub - fi - limit_access_to_actor="${{ inputs.limit-access-to-actor }}" - if [[ "${limit_access_to_actor}" == "true" ]]; then - echo "Adding ${GITHUB_ACTOR} to allowed users (identified by ssh key registered in GitHub)" - curl -s https://github.com/${GITHUB_ACTOR}.keys >> $authorizedKeysFile - fi - limit_access_to_users="${{ inputs.limit-access-to-users }}" - for github_user in ${limit_access_to_users//,/ }; do - if [[ -n "${github_user}" ]]; then - echo "Adding ${github_user} to allowed users (identified by ssh key registered in GitHub)" - curl -s https://github.com/${github_user}.keys >> $authorizedKeysFile - fi - done - if [ -f $authorizedKeysFile ]; then - chmod 0600 $authorizedKeysFile - authorizedKeysParameter="--authorized-keys $authorizedKeysFile" - echo -e "Using $authorizedKeysFile\nContent:\n---------------------------" - cat $authorizedKeysFile - echo "---------------------------" - fi - echo '::endgroup::' - echo "::group::Starting terminal session and connecting to server" - echo 'set-option -ga update-environment " UPTERM_ADMIN_SOCKET"' >> ~/.tmux.conf - tmux new -d -s upterm-wrapper -x 132 -y 43 "upterm host --skip-host-key-check --accept ${authorizedKeysParameter} --force-command 'tmux attach -t upterm' -- tmux new -s upterm -x 132 -y 43" - sleep 1 - tmux set -t upterm-wrapper window-size largest - tmux set -t upterm window-size largest - echo '::endgroup::' - echo -e "\nSSH connection information" - for i in {1..10}; do - export UPTERM_ADMIN_SOCKET=$(find $HOME/.upterm $XDG_RUNTIME_DIR/upterm /run/user/$(id -u)/upterm -name "*.sock" 2>/dev/null | head -n 1) - if [ ! -S "$UPTERM_ADMIN_SOCKET" ]; then - echo "Waiting for upterm admin socket ..." - sleep 1 - else - echo "upterm admin socket available in $UPTERM_ADMIN_SOCKET" - break - fi - done - shopt -s nullglob - upterm session current || { - echo "Starting upterm failed." - cat $HOME/.upterm/upterm.log 2>/dev/null || true - exit 0 - } - elif [[ "${{ inputs.action }}" == "wait" ]]; then - # only wait if upterm was installed - if command -v upterm &>/dev/null; then - shopt -s nullglob - export UPTERM_ADMIN_SOCKET=$(find $HOME/.upterm $XDG_RUNTIME_DIR/upterm /run/user/$(id -u)/upterm -name "*.sock" 2>/dev/null | head -n 1) - echo "SSH connection information" - upterm session current || { - echo "upterm isn't running. Not waiting any longer." - exit 0 - } - timeout=${{ inputs.timeout }} - echo "Waiting $timeout seconds..." - sleep $timeout - echo "Keep waiting as long as there's a connected session" - while upterm session current|grep Connected &>/dev/null; do - sleep 30 - done - echo "No session is connected. Not waiting any longer." - else - echo "upterm isn't installed" - fi - fi - shell: bash diff --git a/.github/scripts/test_workflow_security.py b/.github/scripts/test_workflow_security.py new file mode 100644 index 000000000..f5e5c2860 --- /dev/null +++ b/.github/scripts/test_workflow_security.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +"""Regression checks for the Actions trust boundaries, without cloud credentials.""" + +import copy +import json +import os +from pathlib import Path +import re +import subprocess +import tempfile +import unittest + +import yaml + +ROOT = Path(__file__).resolve().parents[2] +WORKFLOWS = ROOT / '.github/workflows' + + +class WorkflowLoader(yaml.SafeLoader): + """Use YAML 1.2 booleans so GitHub's `on` key is not parsed as True.""" + + +WorkflowLoader.yaml_implicit_resolvers = { + key: [(tag, pattern) for tag, pattern in values + if tag != 'tag:yaml.org,2002:bool'] + for key, values in yaml.SafeLoader.yaml_implicit_resolvers.items() +} +WorkflowLoader.add_implicit_resolver( + 'tag:yaml.org,2002:bool', re.compile(r'^(?:true|false)$', re.I), list('tTfF')) + + +def load(name): + return yaml.load((WORKFLOWS / name).read_text(), Loader=WorkflowLoader) + + +def violations(workflow): + errors = [] + events = workflow.get('on', {}) + if not isinstance(events, dict): + events = dict.fromkeys(events if isinstance(events, list) else [events]) + permissions = workflow.get('permissions') + if not isinstance(permissions, dict): + errors.append('explicit permissions required') + for name, job in workflow['jobs'].items(): + effective = job.get('permissions', permissions) or {} + text = json.dumps(job) + if effective.get('id-token') == 'write': + errors.append('OIDC requires a separate security review') + if 'pull_request' in events: + if any(value == 'write' for value in effective.values()): + errors.append('PR job must not have write permissions') + if 'secrets.' in text: + errors.append('PR job must not reference secrets') + if 'pull_request_target' in events: + for step in job.get('steps', []): + if 'run' in step or step.get('uses', '').startswith(('actions/checkout@', './')): + errors.append('target event must be metadata only') + if '${{' in step.get('with', {}).get('script', ''): + errors.append('never interpolate event data into JavaScript') + for step in job.get('steps', []): + action = step.get('uses', '') + if action and not re.fullmatch(r'[^@]+@[0-9a-f]{40}', action): + errors.append('external actions require a commit pin') + if any(value in action.lower() for value in ('ssh-access', 'tmate', 'upterm')): + errors.append('automatic remote access is forbidden') + if action.startswith('actions/checkout@'): + if step.get('with', {}).get('persist-credentials') is not False: + errors.append('checkout must not persist credentials') + if action.startswith('actions/setup-go@') and step['with'].get('cache') is not False: + errors.append('Go cache must be explicitly disabled') + return errors + + +class WorkflowSecurity(unittest.TestCase): + def test_all_workflows_obey_baseline(self): + for path in sorted(WORKFLOWS.glob('*.yml')): + with self.subTest(workflow=path.name): + self.assertEqual([], violations(load(path.name))) + + def test_shell_syntax_and_e2e_installers(self): + for path in WORKFLOWS.glob('*.yml'): + for job in load(path.name)['jobs'].values(): + for step in job.get('steps', []): + if 'run' in step: + subprocess.run(['bash', '-n'], input=step['run'], text=True, check=True) + for path in (ROOT / '.ci/tests').glob('integration*/e2e*.yaml'): + config = yaml.load(path.read_text(), Loader=WorkflowLoader) + for step in config['setup']['steps']: + command = step.get('command', '') + subprocess.run(['bash', '-n'], input=command, text=True, check=True) + self.assertNotIn('get-helm-3 | bash', command) + if step['name'] == 'install helm': + self.assertIn('sha256sum --check --strict', command) + self.assertIn('helm-v3.22.1-linux-amd64.tar.gz', command) + if step['name'] == 'install pulsar cluster': + self.assertIn('checkout --detach 0f64202d0bd529dea5ab27bbe27ef6be84b2bac9', command) + + def test_corrupt_helm_download_never_executes(self): + with tempfile.TemporaryDirectory() as directory: + base = Path(directory) + binary = base / 'bin' + binary.mkdir() + curl = binary / 'curl' + curl.write_text('#!/bin/bash\nwhile (($#)); do\n' + ' if [[ "$1" == -o ]]; then printf corrupt > "$2"; exit 0; fi\n' + ' shift\ndone\nexit 1\n') + sudo = binary / 'sudo' + sudo.write_text('#!/bin/bash\ntouch "$INSTALL_MARKER"\n') + curl.chmod(0o755) + sudo.chmod(0o755) + for path in (ROOT / '.ci/tests').glob('integration*/e2e*.yaml'): + config = yaml.load(path.read_text(), Loader=WorkflowLoader) + command = next(s['command'] for s in config['setup']['steps'] + if s['name'] == 'install helm') + result = subprocess.run(['bash', '-ec', command], capture_output=True, text=True, + env={**os.environ, 'PATH': f"{binary}:{os.environ['PATH']}", + 'TMPDIR': directory, 'INSTALL_MARKER': str(base / 'installed')}) + self.assertNotEqual(0, result.returncode) + self.assertFalse((base / 'installed').exists()) + + def test_preflight_authfile_is_not_logged(self): + steps = load('bundle-release.yml')['jobs']['openshift']['steps'] + step = next(s for s in steps if s.get('name') == 'Trigger openshift preflight test') + self.assertIn('umask 077', step['run']) + self.assertIn("trap 'rm -f \"$AUTHFILE\"' EXIT", step['run']) + self.assertNotIn('cat $AUTHFILE', step['run']) + self.assertNotIn('PFLT_LOGLEVEL=trace', step['run']) + self.assertIn('quay.io/opdev/preflight@sha256:', step['run']) + + def test_policy_rejects_unsafe_mutations(self): + original = load('test-integration-skywalking-e2e.yml') + cases = [] + w = copy.deepcopy(original) + w['jobs']['integration']['env'] = {'TOKEN': '${{ secrets.CLOUD_TOKEN }}'} + cases.append(w) + w = copy.deepcopy(original) + w['permissions']['contents'] = 'write' + cases.append(w) + w = copy.deepcopy(original) + w['on'] = {'pull_request_target': None} + cases.append(w) + w = copy.deepcopy(original) + w['jobs']['integration']['steps'][0]['uses'] = 'owner/action@main' + cases.append(w) + for workflow in cases: + self.assertTrue(violations(workflow)) + + def test_pr_integration_preserves_secretless_cases(self): + workflow = load('test-integration-skywalking-e2e.yml') + self.assertEqual({'pull_request'}, set(workflow['on'])) + cases = workflow['jobs']['integration']['strategy']['matrix']['case'] + self.assertEqual({'.ci/tests/integration/e2e.yaml', + '.ci/tests/integration/e2e_with_tls.yaml'}, + {case['e2e'] for case in cases}) + self.assertIn('github.event.pull_request.number', workflow['concurrency']['group']) + + def test_cloud_integration_is_trusted_only(self): + workflow = load('test-integration-cloud.yml') + self.assertEqual({'push', 'workflow_dispatch'}, set(workflow['on'])) + self.assertEqual(['master'], workflow['on']['push']['branches']) + self.assertIsNone(workflow['on']['workflow_dispatch']) + job = workflow['jobs']['integration'] + for guard in ("github.repository == 'streamnative/function-mesh'", + "github.ref == 'refs/heads/master'", + "github.event_name == 'push'", + "github.event_name == 'workflow_dispatch'"): + self.assertIn(guard, job['if']) + self.assertNotIn('env', job) + secret_steps = [s for s in job['steps'] if 'secrets.' in json.dumps(s)] + self.assertEqual(1, len(secret_steps)) + self.assertTrue(secret_steps[0]['uses'].startswith('apache/skywalking-infra-e2e@')) + self.assertIn("matrix.case.name == 'integration test with oauth2'", + secret_steps[0]['env']['GSA_CREDENTIAL']) + for step in job['steps']: + if step.get('uses', '').startswith('actions/checkout@'): + self.assertNotIn('ref', step['with']) + self.assertNotIn('repository', step['with']) + + def test_write_jobs_never_execute_builds_or_artifacts(self): + for file in ('release.yml', 'trivy_scheduled_master.yml'): + workflow = load(file) + self.assertNotIn('pull_request', workflow['on']) + self.assertEqual({'contents': 'read'}, workflow['permissions']) + upload = workflow['jobs']['upload'] + self.assertEqual('build', upload['needs']) + for step in upload['steps']: + self.assertNotIn('run', step) + self.assertFalse(step['uses'].startswith(('actions/checkout@', './'))) + download = upload['steps'][0] + self.assertTrue(download['uses'].startswith('actions/download-artifact@')) + self.assertEqual({'name'}, set(download['with'])) + + def test_release_tag_checks_gate_credentials(self): + for name, jobs in [('release.yml', ['build']), + ('bundle-release.yml', ['operatorhub', 'openshift'])]: + workflow = load(name) + validate = workflow['jobs']['validate-tag'] + self.assertNotIn('secrets.', json.dumps(validate)) + self.assertIn("github.repository == 'streamnative/function-mesh'", validate['if']) + for job_name in jobs: + job = workflow['jobs'][job_name] + self.assertEqual('validate-tag', job['needs']) + checkout = next(s for s in job['steps'] + if s.get('uses', '').startswith('actions/checkout@')) + self.assertEqual('${{ needs.validate-tag.outputs.sha }}', checkout['with']['ref']) + self.assertIn("github.ref == 'refs/heads/master'", + load('bundle-release.yml')['jobs']['validate-tag']['if']) + + def test_tag_validation_with_local_git_history(self): + scripts = [load(name)['jobs']['validate-tag']['steps'][1]['run'] + for name in ('release.yml', 'bundle-release.yml')] + self.assertEqual(scripts[0], scripts[1]) + with tempfile.TemporaryDirectory() as directory: + base = Path(directory) + origin, checkout = base / 'origin', base / 'checkout' + def git(*args, cwd=origin): + return subprocess.check_output(['git', *args], cwd=cwd, stderr=subprocess.DEVNULL) + origin.mkdir() + git('init', '-b', 'master') + git('config', 'user.email', 'ci@example.invalid') + git('config', 'user.name', 'CI test') + git('commit', '--allow-empty', '-m', 'trusted') + git('tag', 'v1.2.3') + git('tag', '-a', 'v1.2.3-rc.1', '-m', 'annotated tag') + git('checkout', '-b', 'feature') + git('commit', '--allow-empty', '-m', 'unmerged') + git('tag', 'v9.9.9') + git('checkout', 'master') + git('clone', str(origin), str(checkout), cwd=base) + tags = {'v1.2.3': True, 'v1.2.3-rc.1': True, 'v9.9.9': False, + 'v0.0.0': False, 'master': False, 'refs/pull/831/head': False, + '--help': False, '$(touch injected)': False, 'v1.2.3\nevil': False} + for tag, expected in tags.items(): + with self.subTest(tag=tag): + output = base / 'output' + output.write_text('') + result = subprocess.run(['bash', '-e', '-c', scripts[0]], cwd=checkout, + env={**os.environ, 'TAG': tag, 'GITHUB_OUTPUT': str(output)}, + capture_output=True, text=True) + self.assertEqual(expected, result.returncode == 0, result.stderr) + self.assertEqual(expected, output.read_text().startswith('sha=')) + self.assertFalse((checkout / 'injected').exists()) + + def test_label_script_only_changes_documentation_labels(self): + script = load('documentbot.yml')['jobs']['labeling']['steps'][0]['with']['script'] + harness = ''' + const calls = []; + const context = {repo: {owner: 'test', repo: 'test'}, payload: {pull_request: + {number: 1, body: process.env.BODY, labels: [{name:'no-need-doc'}, {name:'approved'}]}}}; + const github = {rest: {issues: { + addLabels: async args => calls.push({add: args.labels}), + removeLabel: async args => calls.push({remove: args.name})}}}; + (async () => {SCRIPT})().then(() => console.log(JSON.stringify(calls))); + '''.replace('SCRIPT', script) + result = subprocess.check_output(['node', '-e', harness], text=True, env={**os.environ, + 'BODY': '- [x] `approved`\n- [x] `doc-required`\n- [ ] `no-need-doc`\n' + '- [x] `doc`\n${{ secrets.TOKEN }}\n$(touch malicious)'} ) + self.assertEqual([{'add': ['doc-required', 'doc']}, {'remove': 'no-need-doc'}], json.loads(result)) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/.github/scripts/workflow-security-requirements.txt b/.github/scripts/workflow-security-requirements.txt new file mode 100644 index 000000000..dcf7af30d --- /dev/null +++ b/.github/scripts/workflow-security-requirements.txt @@ -0,0 +1,2 @@ +# CPython 3.12 Linux x86_64 wheel; used by the ubuntu-latest security check. +PyYAML==6.0.3 --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc diff --git a/.github/workflows/bundle-release.yml b/.github/workflows/bundle-release.yml index 14f608616..a779f8b9d 100644 --- a/.github/workflows/bundle-release.yml +++ b/.github/workflows/bundle-release.yml @@ -17,14 +17,47 @@ on: default: true type: boolean +permissions: + contents: read + jobs: + validate-tag: + if: github.repository == 'streamnative/function-mesh' && github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + sha: ${{ steps.tag.outputs.sha }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: master + fetch-depth: 0 + persist-credentials: false + - name: Require a version tag reachable from master + id: tag + env: + TAG: ${{ inputs.tag }} + shell: bash + run: | + set -euo pipefail + if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$ ]]; then + echo 'Expected a version tag such as v0.30.0 or v0.30.0-rc.1' >&2 + exit 1 + fi + git fetch --no-tags origin "refs/tags/$TAG:refs/tags/$TAG" + sha=$(git rev-parse --verify "refs/tags/$TAG^{commit}") + git merge-base --is-ancestor "$sha" origin/master + printf 'sha=%s\n' "$sha" >> "$GITHUB_OUTPUT" + operatorhub: + needs: validate-tag name: publish to operatorhub.io runs-on: ubuntu-latest + timeout-minutes: 90 if: ${{ inputs.operatorhub }} steps: - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@v1.3.0 + uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 with: # this might remove tools that are actually needed, # if set to "true" but frees about 6 GB @@ -39,43 +72,49 @@ jobs: swap-storage: true - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: - ref: ${{ inputs.tag }} + persist-credentials: false + ref: ${{ needs.validate-tag.outputs.sha }} - name: Login to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Set up GO 1.26.10 - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: + cache: false go-version: 1.26.10 id: go - name: InstallKubebuilder run: | - curl -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.1/kubebuilder_2.3.1_linux_amd64.tar.gz | tar -xz -C /tmp/ + curl --fail --silent --show-error --location -o /tmp/kubebuilder.tar.gz https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.1/kubebuilder_2.3.1_linux_amd64.tar.gz + echo 'ff496970f209706763f2aba2bdcefc2de8d00085b3b972b5790117b59ea4ed10 /tmp/kubebuilder.tar.gz' | sha256sum --check --strict + tar -xzf /tmp/kubebuilder.tar.gz -C /tmp sudo mv /tmp/kubebuilder_2.3.1_linux_amd64 /usr/local/kubebuilder export PATH=$PATH:/usr/local/kubebuilder/bin - name: Install operator-sdk run: | RELEASE_VERSION=v1.29.0 - curl -LO "https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk_linux_amd64" + curl --fail --silent --show-error --location --remote-name "https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk_linux_amd64" + echo '90d88e8107bd87c7af1a49f64a10dfdf72bf110643842523c1ef123af221ee2a operator-sdk_linux_amd64' | sha256sum --check --strict chmod +x operator-sdk_linux_amd64 && sudo mkdir -p /usr/local/bin/ && sudo mv operator-sdk_linux_amd64 /usr/local/bin/operator-sdk - name: Set up yq run: | - sudo wget https://github.com/mikefarah/yq/releases/download/v4.30.4/yq_linux_amd64 -O /usr/bin/yq - sudo chmod +x /usr/bin/yq + curl --fail --silent --show-error --location -o /tmp/yq https://github.com/mikefarah/yq/releases/download/v4.30.4/yq_linux_amd64 + echo '30459aa144a26125a1b22c62760f9b3872123233a5658934f7bd9fe714d7864d /tmp/yq' | sha256sum --check --strict + sudo install /tmp/yq /usr/local/bin/yq yq --help - name: Get vertical-pod-autoscaler crd run: | - wget https://raw.githubusercontent.com/kubernetes/autoscaler/master/vertical-pod-autoscaler/deploy/vpa-v1-crd-gen.yaml -O config/crd/bases/vpa-v1-crd.yaml + wget https://raw.githubusercontent.com/kubernetes/autoscaler/0e493b62210509c14233575696100104ef1d9e76/vertical-pod-autoscaler/deploy/vpa-v1-crd-gen.yaml -O config/crd/bases/vpa-v1-crd.yaml - name: build release id: build_release @@ -89,7 +128,7 @@ jobs: make release - name: Login to Quay - uses: docker/login-action@v1 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: quay.io username: ${{ secrets.QUAY_DOCKER_USERNAME }} @@ -108,8 +147,9 @@ jobs: make redhat-certificated-image-build redhat-certificated-image-push redhat-certificated-bundle redhat-certificated-bundle-build redhat-certificated-bundle-push - name: Checkout streamnative community-operators - uses: actions/checkout@v3 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: + persist-credentials: false repository: streamnative/community-operators path: community-operators token: ${{ secrets.SNBOT_GITHUB_TOKEN }} @@ -119,6 +159,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.SNBOT_GITHUB_TOKEN }} VERSION: ${{ inputs.tag }} run: | + gh auth setup-git pushd community-operators git config --global user.email "streamnativebot@streamnative.io" git config --global user.name "streamnativebot" @@ -145,17 +186,18 @@ jobs: git commit -s -m "operator function-mesh ($VERSION)" git push --set-upstream origin $BRANCH - wget https://raw.githubusercontent.com/streamnative/function-mesh/master/.github/openshift/community-operators -O ../.github/community-operators - gh pr create --title "operator function-mesh ($VERSION)" -F ../.github/community-operators -R k8s-operatorhub/community-operators + gh pr create --title "operator function-mesh ($VERSION)" -F ../.github/openshift/community-operators -R k8s-operatorhub/community-operators popd openshift: + needs: validate-tag name: publish to openshift certificated runs-on: ubuntu-latest + timeout-minutes: 90 if: ${{ inputs.openshift }} steps: - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@v1.3.0 + uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 with: # this might remove tools that are actually needed, # if set to "true" but frees about 6 GB @@ -170,47 +212,49 @@ jobs: swap-storage: true - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: - ref: ${{ inputs.tag }} + persist-credentials: false + ref: ${{ needs.validate-tag.outputs.sha }} - name: Login to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Set up GO 1.26.10 - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: + cache: false go-version: 1.26.10 id: go - name: InstallKubebuilder run: | - curl -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.1/kubebuilder_2.3.1_linux_amd64.tar.gz | tar -xz -C /tmp/ + curl --fail --silent --show-error --location -o /tmp/kubebuilder.tar.gz https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.1/kubebuilder_2.3.1_linux_amd64.tar.gz + echo 'ff496970f209706763f2aba2bdcefc2de8d00085b3b972b5790117b59ea4ed10 /tmp/kubebuilder.tar.gz' | sha256sum --check --strict + tar -xzf /tmp/kubebuilder.tar.gz -C /tmp sudo mv /tmp/kubebuilder_2.3.1_linux_amd64 /usr/local/kubebuilder export PATH=$PATH:/usr/local/kubebuilder/bin - name: Install operator-sdk run: | RELEASE_VERSION=v1.29.0 - curl -LO "https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk_linux_amd64" + curl --fail --silent --show-error --location --remote-name "https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk_linux_amd64" + echo '90d88e8107bd87c7af1a49f64a10dfdf72bf110643842523c1ef123af221ee2a operator-sdk_linux_amd64' | sha256sum --check --strict chmod +x operator-sdk_linux_amd64 && sudo mkdir -p /usr/local/bin/ && sudo mv operator-sdk_linux_amd64 /usr/local/bin/operator-sdk - name: Set up yq run: | - sudo wget https://github.com/mikefarah/yq/releases/download/v4.30.4/yq_linux_amd64 -O /usr/bin/yq - sudo chmod +x /usr/bin/yq + curl --fail --silent --show-error --location -o /tmp/yq https://github.com/mikefarah/yq/releases/download/v4.30.4/yq_linux_amd64 + echo '30459aa144a26125a1b22c62760f9b3872123233a5658934f7bd9fe714d7864d /tmp/yq' | sha256sum --check --strict + sudo install /tmp/yq /usr/local/bin/yq yq --help - name: Get vertical-pod-autoscaler crd run: | - wget https://raw.githubusercontent.com/kubernetes/autoscaler/master/vertical-pod-autoscaler/deploy/vpa-v1-crd-gen.yaml -O config/crd/bases/vpa-v1-crd.yaml - - - name: Use the master hack scripts - run: | - wget https://raw.githubusercontent.com/streamnative/function-mesh/master/hack/publish-rhel.sh -O hack/publish-rhel.sh + wget https://raw.githubusercontent.com/kubernetes/autoscaler/0e493b62210509c14233575696100104ef1d9e76/vertical-pod-autoscaler/deploy/vpa-v1-crd-gen.yaml -O config/crd/bases/vpa-v1-crd.yaml - name: build release id: build_release @@ -223,14 +267,14 @@ jobs: make release - name: Login to Quay for RedHat container certification - uses: docker/login-action@v1 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: quay.io username: ${{ secrets.REDHAT_CERTIFICATED_CONTAINER_REGISTRY_USER }} password: ${{ secrets.REDHAT_CERTIFICATED_CONTAINER_REGISTRY_PASSWORD }} - name: Login to RedHat Connect registry - uses: docker/login-action@v1 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: https://registry.connect.redhat.com username: ${{ secrets.REDHAT_CONNECT_REGISTRY_USER }} @@ -258,12 +302,13 @@ jobs: REDHAT_CERTIFICATED_CONTAINER_AUTHFILE: ${{ secrets.REDHAT_CERTIFICATED_CONTAINER_AUTHFILE }} run: | echo $VERSION - export TARGET_DIR=/tmp/artifacts - export AUTHFILE=/tmp/authfile.json - mkdir $TARGET_DIR - echo $REDHAT_CERTIFICATED_CONTAINER_AUTHFILE > $AUTHFILE - cat $AUTHFILE - docker run -i --rm --security-opt=label=disable --env PFLT_LOGLEVEL=trace --env PFLT_ARTIFACTS=/artifacts --env PFLT_LOGFILE=/artifacts/preflight.log --env PFLT_CERTIFICATION_PROJECT_ID=$CONTAINER_IMAGE_CERTIFICATION_PROJECT_ID --env PFLT_PYXIS_API_TOKEN=$PFLT_PYXIS_API_TOKEN --env PFLT_DOCKERCONFIG=/temp-authfile.json -v $TARGET_DIR:/artifacts -v $AUTHFILE:/temp-authfile.json:ro quay.io/opdev/preflight:stable check container quay.io/redhat-isv-containers/$CONTAINER_IMAGE_CERTIFICATION_PROJECT_ID:$VERSION --submit + set -euo pipefail + umask 077 + TARGET_DIR=$(mktemp -d "$RUNNER_TEMP/preflight-artifacts.XXXXXX") + AUTHFILE=$(mktemp "$RUNNER_TEMP/preflight-auth.XXXXXX") + trap 'rm -f "$AUTHFILE"' EXIT + printf '%s' "$REDHAT_CERTIFICATED_CONTAINER_AUTHFILE" > "$AUTHFILE" + docker run -i --rm --security-opt=label=disable --env PFLT_LOGLEVEL=info --env PFLT_ARTIFACTS=/artifacts --env PFLT_LOGFILE=/artifacts/preflight.log --env PFLT_CERTIFICATION_PROJECT_ID=$CONTAINER_IMAGE_CERTIFICATION_PROJECT_ID --env PFLT_PYXIS_API_TOKEN --env PFLT_DOCKERCONFIG=/temp-authfile.json -v "$TARGET_DIR:/artifacts" -v "$AUTHFILE:/temp-authfile.json:ro" quay.io/opdev/preflight@sha256:cb198b3bb3b5fc8969f4af2e7bef22533f46eae185c586c7077cbc33612f0ba5 check container quay.io/redhat-isv-containers/$CONTAINER_IMAGE_CERTIFICATION_PROJECT_ID:$VERSION --submit sleep 60s - name: Wait for Scan to Complete @@ -314,8 +359,9 @@ jobs: make redhat-certificated-bundle redhat-certificated-bundle-build - name: Checkout streamnative certified-operators - uses: actions/checkout@v3 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: + persist-credentials: false repository: streamnative/certified-operators path: certified-operators token: ${{ secrets.SNBOT_GITHUB_TOKEN }} @@ -325,6 +371,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.SNBOT_GITHUB_TOKEN }} VERSION: ${{ inputs.tag }} run: | + gh auth setup-git pushd certified-operators git config --global user.email "streamnativebot@streamnative.io" git config --global user.name "streamnativebot" diff --git a/.github/workflows/documentbot.yml b/.github/workflows/documentbot.yml index 6a2262ab1..db7f12a26 100644 --- a/.github/workflows/documentbot.yml +++ b/.github/workflows/documentbot.yml @@ -21,31 +21,46 @@ name: Auto Labeling on: - pull_request_target : + pull_request_target: types: - opened - edited - labeled - -# A GitHub token created for a PR coming from a fork doesn't have -# 'admin' or 'write' permission (which is required to add labels) -# To avoid this issue, you can use the `scheduled` event and run -# this action on a certain interval.And check the label about the -# document. +# Metadata only: never check out or execute pull request code in this workflow. +permissions: {} jobs: labeling: if: ${{ github.repository == 'streamnative/function-mesh' }} permissions: - pull-requests: write + pull-requests: write runs-on: ubuntu-latest + timeout-minutes: 10 steps: - - uses: actions/checkout@v2 - - - uses: streamnative/github-workflow-libraries/doc-label-check@master + - name: Synchronize documentation labels only + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - label-pattern: '- \[(.*?)\] ?`(.+?)`' # matches '- [x] `label`' - + script: | + const allowed = new Set(['doc-required', 'no-need-doc', 'doc']); + const pr = context.payload.pull_request; + const body = pr.body || ''; + const requested = new Set(); + const pattern = /^- \[([xX ])\]\s*`(doc-required|no-need-doc|doc)`[ \t]*\r?$/gm; + for (const match of body.matchAll(pattern)) { + if (match[1].toLowerCase() === 'x') requested.add(match[2]); + } + const current = new Set(pr.labels.map(label => label.name)); + const target = {...context.repo, issue_number: pr.number}; + const add = [...requested].filter(label => !current.has(label)); + if (add.length) await github.rest.issues.addLabels({...target, labels: add}); + for (const name of allowed) { + if (current.has(name) && !requested.has(name)) { + try { + await github.rest.issues.removeLabel({...target, name}); + } catch (error) { + if (error.status !== 404) throw error; + } + } + } diff --git a/.github/workflows/olm-verify.yml b/.github/workflows/olm-verify.yml index 06232503d..16aa72960 100644 --- a/.github/workflows/olm-verify.yml +++ b/.github/workflows/olm-verify.yml @@ -11,13 +11,17 @@ on: - "LICENSE" - "mesh-worker-service/README.md" - "tools/README.md" +permissions: + contents: read + jobs: olm-verify: name: Verify OLM bundle runs-on: ubuntu-latest + timeout-minutes: 90 steps: - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@v1.3.0 + uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 with: # this might remove tools that are actually needed, # if set to "true" but frees about 6 GB @@ -32,29 +36,35 @@ jobs: swap-storage: true - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: Set up GO 1.26.10 - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: + cache: false go-version: 1.26.10 id: go - name: InstallKubebuilder run: | - curl -L -o kubebuilder "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v3.6.0/kubebuilder_linux_amd64" + curl --fail --silent --show-error --location -o kubebuilder "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v3.6.0/kubebuilder_linux_amd64" + echo 'd5a9f639cb98298817bcab32b6046eb45cb912b56217a75d729646512a436ff4 kubebuilder' | sha256sum --check --strict chmod +x kubebuilder && mv kubebuilder /usr/local/bin/ - name: Install operator-sdk run: | RELEASE_VERSION=v1.29.0 - curl -LO "https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk_linux_amd64" + curl --fail --silent --show-error --location --remote-name "https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk_linux_amd64" + echo '90d88e8107bd87c7af1a49f64a10dfdf72bf110643842523c1ef123af221ee2a operator-sdk_linux_amd64' | sha256sum --check --strict chmod +x operator-sdk_linux_amd64 && sudo mkdir -p /usr/local/bin/ && sudo mv operator-sdk_linux_amd64 /usr/local/bin/operator-sdk - name: Set up yq run: | - sudo wget https://github.com/mikefarah/yq/releases/download/v4.30.4/yq_linux_amd64 -O /usr/bin/yq - sudo chmod +x /usr/bin/yq + curl --fail --silent --show-error --location -o /tmp/yq https://github.com/mikefarah/yq/releases/download/v4.30.4/yq_linux_amd64 + echo '30459aa144a26125a1b22c62760f9b3872123233a5658934f7bd9fe714d7864d /tmp/yq' | sha256sum --check --strict + sudo install /tmp/yq /usr/local/bin/yq yq --help - name: Create kind cluster @@ -98,7 +108,3 @@ jobs: - name: Verify Operator health run: | kubectl wait deployment/function-mesh-controller-manager --for=condition=Available --timeout=600s - - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - if: failure() diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml index 357d58e27..ebb2a92b2 100644 --- a/.github/workflows/project.yml +++ b/.github/workflows/project.yml @@ -11,17 +11,21 @@ on: - "LICENSE" - "mesh-worker-service/README.md" - "tools/README.md" +permissions: + contents: read + jobs: build: name: unit-tests runs-on: ubuntu-latest + timeout-minutes: 90 strategy: fail-fast: false matrix: go-version: [1.26.10] steps: - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@v1.3.0 + uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 with: # this might remove tools that are actually needed, # if set to "true" but frees about 6 GB @@ -36,17 +40,21 @@ jobs: swap-storage: true - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: + cache: false go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory - uses: actions/checkout@v1 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: InstallKubebuilder run: | - curl -L -o kubebuilder "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v3.6.0/kubebuilder_linux_amd64" + curl --fail --silent --show-error --location -o kubebuilder "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v3.6.0/kubebuilder_linux_amd64" + echo 'd5a9f639cb98298817bcab32b6046eb45cb912b56217a75d729646512a436ff4 kubebuilder' | sha256sum --check --strict chmod +x kubebuilder && mv kubebuilder /usr/local/bin/ - name: InstallTool @@ -73,7 +81,7 @@ jobs: - name: Get vertical-pod-autoscaler crd run: | - wget https://raw.githubusercontent.com/kubernetes/autoscaler/master/vertical-pod-autoscaler/deploy/vpa-v1-crd-gen.yaml -O config/crd/bases/vpa-v1-crd.yaml + wget https://raw.githubusercontent.com/kubernetes/autoscaler/0e493b62210509c14233575696100104ef1d9e76/vertical-pod-autoscaler/deploy/vpa-v1-crd-gen.yaml -O config/crd/bases/vpa-v1-crd.yaml - name: Run unit test check run: make test diff --git a/.github/workflows/release-note.yml b/.github/workflows/release-note.yml index a04af81c8..25abe2441 100644 --- a/.github/workflows/release-note.yml +++ b/.github/workflows/release-note.yml @@ -2,39 +2,25 @@ name: Function-Mesh Release Notes on: push: - branches: - - master + branches: [master] paths-ignore: - 'docs/**' - 'README.md' - pull_request: - # Only following types are handled by the action, but one can default to all as well - types: [ opened, reopened, synchronize ] -permissions: - contents: read +permissions: {} jobs: update_release_draft: + if: github.repository == 'streamnative/function-mesh' + permissions: + contents: write + pull-requests: read runs-on: ubuntu-latest - + timeout-minutes: 10 steps: - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@v1.3.0 + - name: Update draft release notes + uses: release-drafter/release-drafter@6a93d829887aa2e0748befe2e808c66c0ec6e4c7 # v6 with: - # this might remove tools that are actually needed, - # if set to "true" but frees about 6 GB - tool-cache: false - # all of these default to true, but feel free to set to - # "false" if necessary for your workflow - android: true - dotnet: true - haskell: true - large-packages: true - docker-images: true - swap-storage: true - - - name: release note - uses: release-drafter/release-drafter@v5 + disable-autolabeler: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 12605a995..434403768 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,16 +5,45 @@ on: types: [created] permissions: - contents: write + contents: read jobs: - upload: - if: startsWith(github.ref, 'refs/tags/v') - name: Upload Release files + validate-tag: + if: github.repository == 'streamnative/function-mesh' && startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + sha: ${{ steps.tag.outputs.sha }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: master + fetch-depth: 0 + persist-credentials: false + - name: Require a version tag reachable from master + id: tag + env: + TAG: ${{ github.event.release.tag_name }} + shell: bash + run: | + set -euo pipefail + if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$ ]]; then + echo 'Expected a version tag such as v0.30.0 or v0.30.0-rc.1' >&2 + exit 1 + fi + git fetch --no-tags origin "refs/tags/$TAG:refs/tags/$TAG" + sha=$(git rev-parse --verify "refs/tags/$TAG^{commit}") + git merge-base --is-ancestor "$sha" origin/master + printf 'sha=%s\n' "$sha" >> "$GITHUB_OUTPUT" + + build: + needs: validate-tag + name: Build release files and images runs-on: ubuntu-latest + timeout-minutes: 90 steps: - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@v1.3.0 + uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 with: # this might remove tools that are actually needed, # if set to "true" but frees about 6 GB @@ -29,44 +58,52 @@ jobs: swap-storage: true - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + ref: ${{ needs.validate-tag.outputs.sha }} - name: Login to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - name: Set up GO 1.26.10 - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: + cache: false go-version: 1.26.10 id: go - name: InstallKubebuilder run: | - curl -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.1/kubebuilder_2.3.1_linux_amd64.tar.gz | tar -xz -C /tmp/ + curl --fail --silent --show-error --location -o /tmp/kubebuilder.tar.gz https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.1/kubebuilder_2.3.1_linux_amd64.tar.gz + echo 'ff496970f209706763f2aba2bdcefc2de8d00085b3b972b5790117b59ea4ed10 /tmp/kubebuilder.tar.gz' | sha256sum --check --strict + tar -xzf /tmp/kubebuilder.tar.gz -C /tmp sudo mv /tmp/kubebuilder_2.3.1_linux_amd64 /usr/local/kubebuilder export PATH=$PATH:/usr/local/kubebuilder/bin - name: Install operator-sdk run: | RELEASE_VERSION=v1.29.0 - curl -LO "https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk_linux_amd64" + curl --fail --silent --show-error --location --remote-name "https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk_linux_amd64" + echo '90d88e8107bd87c7af1a49f64a10dfdf72bf110643842523c1ef123af221ee2a operator-sdk_linux_amd64' | sha256sum --check --strict chmod +x operator-sdk_linux_amd64 && sudo mkdir -p /usr/local/bin/ && sudo mv operator-sdk_linux_amd64 /usr/local/bin/operator-sdk - name: Set up yq run: | - sudo wget https://github.com/mikefarah/yq/releases/download/v4.30.4/yq_linux_amd64 -O /usr/bin/yq - sudo chmod +x /usr/bin/yq + curl --fail --silent --show-error --location -o /tmp/yq https://github.com/mikefarah/yq/releases/download/v4.30.4/yq_linux_amd64 + echo '30459aa144a26125a1b22c62760f9b3872123233a5658934f7bd9fe714d7864d /tmp/yq' | sha256sum --check --strict + sudo install /tmp/yq /usr/local/bin/yq yq --help - name: Get vertical-pod-autoscaler crd run: | - wget https://raw.githubusercontent.com/kubernetes/autoscaler/master/vertical-pod-autoscaler/deploy/vpa-v1-crd-gen.yaml -O config/crd/bases/vpa-v1-crd.yaml + wget https://raw.githubusercontent.com/kubernetes/autoscaler/0e493b62210509c14233575696100104ef1d9e76/vertical-pod-autoscaler/deploy/vpa-v1-crd-gen.yaml -O config/crd/bases/vpa-v1-crd.yaml - name: build release id: build_release @@ -82,8 +119,31 @@ jobs: make docker-push make docker-build image-push bundle bundle-build bundle-push catalog-build catalog-push + - name: Preserve release files for the isolated upload job + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: release-files + path: | + manifests/crd.yaml + manifests/rbac.yaml + install.sh + if-no-files-found: error + retention-days: 1 + + upload: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + steps: + # Same trusted run only; files are uploaded, never executed. + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: release-files + - name: Upload crd.yaml to release - uses: svenstaro/upload-release-action@v2 + uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: manifests/crd.yaml @@ -92,7 +152,7 @@ jobs: overwrite: true - name: Upload rbac.yaml to release - uses: svenstaro/upload-release-action@v2 + uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: manifests/rbac.yaml @@ -101,7 +161,7 @@ jobs: overwrite: true - name: Upload install.sh to release - uses: svenstaro/upload-release-action@v2 + uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: install.sh diff --git a/.github/workflows/test-function-runner.yml b/.github/workflows/test-function-runner.yml index a701bae7c..38ac850b1 100644 --- a/.github/workflows/test-function-runner.yml +++ b/.github/workflows/test-function-runner.yml @@ -11,12 +11,16 @@ on: - 'LICENSE' - 'mesh-worker-service/README.md' - 'tools/README.md' +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest + timeout-minutes: 90 steps: - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@v1.3.0 + uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 with: # this might remove tools that are actually needed, # if set to "true" but frees about 6 GB @@ -39,18 +43,13 @@ jobs: sudo service docker start - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: + persist-credentials: false fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha }} - - - name: Setup ssh access to build runner VM - uses: ./.github/actions/ssh-access - with: - limit-access-to-actor: true - name: Deploy k8s cluster env - uses: nick-invision/retry@v2 + uses: nick-invision/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2 with: timeout_minutes: 60 max_attempts: 3 @@ -69,10 +68,3 @@ jobs: - name: Verify function runner run: | .ci/verify_function_runner.sh - - - name: Wait for ssh connection when build fails - uses: ./.github/actions/ssh-access - if: failure() - continue-on-error: true - with: - action: wait diff --git a/.github/workflows/test-helm-charts.yml b/.github/workflows/test-helm-charts.yml index 610b78c9b..50dd08d1a 100644 --- a/.github/workflows/test-helm-charts.yml +++ b/.github/workflows/test-helm-charts.yml @@ -30,12 +30,16 @@ on: - "LICENSE" - "mesh-worker-service/README.md" - "tools/README.md" +permissions: + contents: read + jobs: lint-test: runs-on: ubuntu-latest + timeout-minutes: 90 steps: - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@v1.3.0 + uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 with: # this might remove tools that are actually needed, # if set to "true" but frees about 6 GB @@ -50,29 +54,31 @@ jobs: swap-storage: true - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: Fetch history run: git fetch --prune --unshallow - name: Set up Helm - uses: azure/setup-helm@v1 + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 with: version: v3.8.1 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: 3.9 - name: Set up chart-testing - uses: helm/chart-testing-action@v2.3.1 + uses: helm/chart-testing-action@afea100a513515fbd68b0e72a7bb0ae34cb62aec # v2.3.1 - name: Run chart-testing (list-changed) id: list-changed run: | changed=$(ct list-changed --config .ci/ct.yaml) if [[ -n "$changed" ]]; then - echo "::set-output name=changed::true" + echo "changed=true" >> "$GITHUB_OUTPUT" fi - name: Run chart-testing (lint) @@ -85,28 +91,31 @@ jobs: - name: Set up GO 1.26.10 if: steps.list-changed.outputs.changed == 'true' - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: + cache: false go-version: 1.26.10 id: go - name: setup kubebuilder 3.6.0 if: steps.list-changed.outputs.changed == 'true' - uses: RyanSiu1995/kubebuilder-action@v1.2.1 - with: - version: 3.6.0 + run: | + curl --fail --silent --show-error --location -o /tmp/kubebuilder https://github.com/kubernetes-sigs/kubebuilder/releases/download/v3.6.0/kubebuilder_linux_amd64 + echo 'd5a9f639cb98298817bcab32b6046eb45cb912b56217a75d729646512a436ff4 /tmp/kubebuilder' | sha256sum --check --strict + sudo install /tmp/kubebuilder /usr/local/bin/kubebuilder - name: Install operator-sdk if: steps.list-changed.outputs.changed == 'true' run: | RELEASE_VERSION=v1.29.0 - curl -LO "https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk_linux_amd64" + curl --fail --silent --show-error --location --remote-name "https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk_linux_amd64" + echo '90d88e8107bd87c7af1a49f64a10dfdf72bf110643842523c1ef123af221ee2a operator-sdk_linux_amd64' | sha256sum --check --strict chmod +x operator-sdk_linux_amd64 && sudo mkdir -p /usr/local/bin/ && sudo mv operator-sdk_linux_amd64 /usr/local/bin/operator-sdk - name: Get vertical-pod-autoscaler crd if: steps.list-changed.outputs.changed == 'true' run: | - wget https://raw.githubusercontent.com/kubernetes/autoscaler/master/vertical-pod-autoscaler/deploy/vpa-v1-crd-gen.yaml -O config/crd/bases/vpa-v1-crd.yaml + wget https://raw.githubusercontent.com/kubernetes/autoscaler/0e493b62210509c14233575696100104ef1d9e76/vertical-pod-autoscaler/deploy/vpa-v1-crd-gen.yaml -O config/crd/bases/vpa-v1-crd.yaml helm repo add cowboysysop https://cowboysysop.github.io/charts/ helm repo update @@ -127,7 +136,3 @@ jobs: - name: Run chart-testing (install) run: ct install --config .ci/ct.yaml if: steps.list-changed.outputs.changed == 'true' - - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - if: failure() diff --git a/.github/workflows/test-integration-cloud.yml b/.github/workflows/test-integration-cloud.yml new file mode 100644 index 000000000..cec1d335c --- /dev/null +++ b/.github/workflows/test-integration-cloud.yml @@ -0,0 +1,77 @@ +name: Trusted branch - Cloud Integration Tests +# Never add pull_request, pull_request_target, workflow_run, or PR-ref inputs here. +on: + push: + branches: [master] + workflow_dispatch: + +# cancel old workflow if new one is triggered +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read +jobs: + integration: + if: github.repository == 'streamnative/function-mesh' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') + runs-on: ubuntu-latest + timeout-minutes: 80 + strategy: + matrix: + case: + - name: integration test with oauth2 + e2e: ".ci/tests/integration-oauth2/e2e.yaml" + - name: integration test with downloader + e2e: ".ci/tests/integration-oauth2/e2e_with_downloader.yaml" + steps: + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 + with: + # this might remove tools that are actually needed, + # if set to "true" but frees about 6 GB + tool-cache: true + # all of these default to true, but feel free to set to + # "false" if necessary for your workflow + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + + # the runner machine has a disk /dev/sdb1 which mounted to /mnt, and it has more free disk than /dev/sda1. + # we can use it to save docker's data to avoid bookie error due to lack of disk. + - name: change docker data dir + run: | + sudo service docker stop + echo '{ "exec-opts": ["native.cgroupdriver=cgroupfs"], "cgroup-parent": "/actions_job", "data-root": "/mnt/docker" }' | sudo tee /etc/docker/daemon.json + sudo service docker start + + - name: Report disk usage + run: df -h + + - name: Set up GO 1.26.10 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + cache: false + go-version: 1.26.10 + + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + fetch-depth: 0 + + # Only this step gets cloud credentials. Checkout is the trusted event SHA. + - uses: apache/skywalking-infra-e2e@82494d053f879c2e10bc6c1439c05673d50bb37b # v1.2.0 + env: + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + GSA_CREDENTIAL: ${{ matrix.case.name == 'integration test with oauth2' && secrets.GSA_CREDENTIAL || '' }} + with: + e2e-file: ${{matrix.case.e2e}} + + - name: Cleanup + if: ${{ failure() }} + run: e2e cleanup -c ${{matrix.case.e2e}} diff --git a/.github/workflows/test-integration-skywalking-e2e.yml b/.github/workflows/test-integration-skywalking-e2e.yml index ef6524322..ba6121aac 100644 --- a/.github/workflows/test-integration-skywalking-e2e.yml +++ b/.github/workflows/test-integration-skywalking-e2e.yml @@ -1,6 +1,6 @@ name: Precommit - Integration Tests on: - pull_request_target: + pull_request: branches: - '*' paths-ignore: @@ -14,17 +14,14 @@ on: # cancel old workflow if new one is triggered concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true -permissions: read-all +permissions: + contents: read jobs: integration: runs-on: ubuntu-latest - env: - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - GSA_CREDENTIAL: ${{ secrets.GSA_CREDENTIAL }} timeout-minutes: 80 strategy: matrix: @@ -33,13 +30,9 @@ jobs: e2e: ".ci/tests/integration/e2e.yaml" - name: integration test with tls e2e: ".ci/tests/integration/e2e_with_tls.yaml" - - name: integration test with oauth2 - e2e: ".ci/tests/integration-oauth2/e2e.yaml" - - name: integration test with downloader - e2e: ".ci/tests/integration-oauth2/e2e_with_downloader.yaml" steps: - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@v1.3.0 + uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 with: # this might remove tools that are actually needed, # if set to "true" but frees about 6 GB @@ -65,33 +58,21 @@ jobs: run: df -h - name: Set up GO 1.26.10 - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: + cache: false go-version: 1.26.10 - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: + persist-credentials: false fetch-depth: 0 - repository: ${{github.event.pull_request.head.repo.full_name}} - ref: ${{ github.event.pull_request.head.sha }} - - name: Setup ssh access to build runner VM - uses: ./.github/actions/ssh-access - with: - limit-access-to-actor: true - - - uses: apache/skywalking-infra-e2e@v1.2.0 + - uses: apache/skywalking-infra-e2e@82494d053f879c2e10bc6c1439c05673d50bb37b # v1.2.0 with: e2e-file: ${{matrix.case.e2e}} - - name: Wait for ssh connection when build fails - uses: ./.github/actions/ssh-access - if: failure() - continue-on-error: true - with: - action: wait - - name: Cleanup if: ${{ failure() }} run: e2e cleanup -c ${{matrix.case.e2e}} diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index feef7b3e4..ebdec774e 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -13,23 +13,20 @@ on: - "tools/README.md" permissions: - pull-requests: write - issues: write + contents: read jobs: build: name: Scan - permissions: - pull-requests: write - issues: write runs-on: ubuntu-latest + timeout-minutes: 90 env: # Fall back between official mirrors when a registry is rate-limited. TRIVY_DB_REPOSITORY: mirror.gcr.io/aquasec/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2 TRIVY_JAVA_DB_REPOSITORY: mirror.gcr.io/aquasec/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1 steps: - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@v1.3.0 + uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 with: # this might remove tools that are actually needed, # if set to "true" but frees about 6 GB @@ -52,34 +49,38 @@ jobs: sudo service docker start - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: + persist-credentials: false fetch-depth: 0 - repository: ${{github.event.pull_request.head.repo.full_name}} - ref: ${{ github.event.pull_request.head.sha }} - name: Set up GO 1.26.10 - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: + cache: false go-version: 1.26.10 id: go - name: InstallKubebuilder run: | - curl -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.1/kubebuilder_2.3.1_linux_amd64.tar.gz | tar -xz -C /tmp/ + curl --fail --silent --show-error --location -o /tmp/kubebuilder.tar.gz https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.1/kubebuilder_2.3.1_linux_amd64.tar.gz + echo 'ff496970f209706763f2aba2bdcefc2de8d00085b3b972b5790117b59ea4ed10 /tmp/kubebuilder.tar.gz' | sha256sum --check --strict + tar -xzf /tmp/kubebuilder.tar.gz -C /tmp sudo mv /tmp/kubebuilder_2.3.1_linux_amd64 /usr/local/kubebuilder export PATH=$PATH:/usr/local/kubebuilder/bin - name: Install operator-sdk run: | RELEASE_VERSION=v1.29.0 - curl -LO "https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk_linux_amd64" + curl --fail --silent --show-error --location --remote-name "https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk_linux_amd64" + echo '90d88e8107bd87c7af1a49f64a10dfdf72bf110643842523c1ef123af221ee2a operator-sdk_linux_amd64' | sha256sum --check --strict chmod +x operator-sdk_linux_amd64 && sudo mkdir -p /usr/local/bin/ && sudo mv operator-sdk_linux_amd64 /usr/local/bin/operator-sdk - name: Set up yq run: | - sudo wget https://github.com/mikefarah/yq/releases/download/v4.30.4/yq_linux_amd64 -O /usr/bin/yq - sudo chmod +x /usr/bin/yq + curl --fail --silent --show-error --location -o /tmp/yq https://github.com/mikefarah/yq/releases/download/v4.30.4/yq_linux_amd64 + echo '30459aa144a26125a1b22c62760f9b3872123233a5658934f7bd9fe714d7864d /tmp/yq' | sha256sum --check --strict + sudo install /tmp/yq /usr/local/bin/yq yq --help - name: Build operator image @@ -90,8 +91,9 @@ jobs: IMG=${image} make docker-build-skip-test - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@v0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: + cache: 'false' image-ref: "function-mesh-operator:latest" format: "table" exit-code: "1" @@ -102,48 +104,54 @@ jobs: - name: Run Trivy vulnerability scanner for java id: scan-java-runner - uses: aquasecurity/trivy-action@v0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: + cache: 'false' image-ref: "pulsar-functions-java-runner:latest" format: "table" exit-code: "0" - name: Run Trivy vulnerability scanner for python id: scan-python-runner - uses: aquasecurity/trivy-action@v0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: + cache: 'false' image-ref: "pulsar-functions-python-runner:latest" format: "table" exit-code: "0" - name: Run Trivy vulnerability scanner for go id: scan-go-runner - uses: aquasecurity/trivy-action@v0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: + cache: 'false' image-ref: "pulsar-functions-go-runner:latest" format: "table" exit-code: "0" - name: Run Trivy vulnerability scanner for java with pulsarctl id: scan-java-pulsarctl-runner - uses: aquasecurity/trivy-action@v0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: + cache: 'false' image-ref: "pulsar-functions-pulsarctl-java-runner:latest" format: "table" exit-code: "0" - name: Run Trivy vulnerability scanner for python with pulsarctl id: scan-python-pulsarctl-runner - uses: aquasecurity/trivy-action@v0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: + cache: 'false' image-ref: "pulsar-functions-pulsarctl-python-runner:latest" format: "table" exit-code: "0" - name: Run Trivy vulnerability scanner for go with pulsarctl id: scan-go-pulsarctl-runner - uses: aquasecurity/trivy-action@v0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: + cache: 'false' image-ref: "pulsar-functions-pulsarctl-go-runner:latest" format: "table" exit-code: "0" diff --git a/.github/workflows/trivy_scheduled_master.yml b/.github/workflows/trivy_scheduled_master.yml index 01fdc1270..7548ed099 100644 --- a/.github/workflows/trivy_scheduled_master.yml +++ b/.github/workflows/trivy_scheduled_master.yml @@ -1,46 +1,25 @@ name: Scheduled master branch scan security vulnerabilities on: - pull_request: - branches: - - "master" - paths-ignore: - - "docs/**" - - "README.md" - - "CHANGELOG.md" - - "PROJECT" - - "LICENSE" - - "mesh-worker-service/README.md" - - "tools/README.md" + workflow_dispatch: schedule: - cron: "0 0 * * *" permissions: - pull-requests: write - issues: write - # For Trivy uploads to Github Security tab - # required for all workflows - security-events: write - # only required for workflows in private repositories - actions: read contents: read jobs: build: name: Scan - permissions: - pull-requests: write - issues: write - security-events: write - actions: read - contents: read + if: github.repository == 'streamnative/function-mesh' && github.ref == 'refs/heads/master' runs-on: ubuntu-latest + timeout-minutes: 90 env: # Fall back between official mirrors when a registry is rate-limited. TRIVY_DB_REPOSITORY: mirror.gcr.io/aquasec/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2,public.ecr.aws/aquasecurity/trivy-db:2 TRIVY_JAVA_DB_REPOSITORY: mirror.gcr.io/aquasec/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1,public.ecr.aws/aquasecurity/trivy-java-db:1 steps: - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@v1.3.0 + uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 with: # this might remove tools that are actually needed, # if set to "true" but frees about 6 GB @@ -62,34 +41,38 @@ jobs: echo '{ "exec-opts": ["native.cgroupdriver=cgroupfs"], "cgroup-parent": "/actions_job", "data-root": "/mnt/docker" }' | sudo tee /etc/docker/daemon.json sudo service docker start - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: + persist-credentials: false fetch-depth: 0 - repository: ${{github.event.pull_request.head.repo.full_name}} - ref: ${{ github.event.pull_request.head.sha }} - name: Set up GO 1.26.10 - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: + cache: false go-version: 1.26.10 id: go - name: InstallKubebuilder run: | - curl -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.1/kubebuilder_2.3.1_linux_amd64.tar.gz | tar -xz -C /tmp/ + curl --fail --silent --show-error --location -o /tmp/kubebuilder.tar.gz https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.1/kubebuilder_2.3.1_linux_amd64.tar.gz + echo 'ff496970f209706763f2aba2bdcefc2de8d00085b3b972b5790117b59ea4ed10 /tmp/kubebuilder.tar.gz' | sha256sum --check --strict + tar -xzf /tmp/kubebuilder.tar.gz -C /tmp sudo mv /tmp/kubebuilder_2.3.1_linux_amd64 /usr/local/kubebuilder export PATH=$PATH:/usr/local/kubebuilder/bin - name: Install operator-sdk run: | RELEASE_VERSION=v1.14.0 - curl -LO "https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk_linux_amd64" + curl --fail --silent --show-error --location --remote-name "https://github.com/operator-framework/operator-sdk/releases/download/${RELEASE_VERSION}/operator-sdk_linux_amd64" + echo 'a2203454454293c5efe2083427a8f8b160a84dc06e6c2de97475043abf3680b7 operator-sdk_linux_amd64' | sha256sum --check --strict chmod +x operator-sdk_linux_amd64 && sudo mkdir -p /usr/local/bin/ && sudo mv operator-sdk_linux_amd64 /usr/local/bin/operator-sdk - name: Set up yq run: | - sudo wget https://github.com/mikefarah/yq/releases/download/v4.30.4/yq_linux_amd64 -O /usr/bin/yq - sudo chmod +x /usr/bin/yq + curl --fail --silent --show-error --location -o /tmp/yq https://github.com/mikefarah/yq/releases/download/v4.30.4/yq_linux_amd64 + echo '30459aa144a26125a1b22c62760f9b3872123233a5658934f7bd9fe714d7864d /tmp/yq' | sha256sum --check --strict + sudo install /tmp/yq /usr/local/bin/yq yq --help - name: Build operator image @@ -100,8 +83,9 @@ jobs: IMG=${image} make docker-build-skip-test - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@v0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: + cache: 'false' image-ref: "function-mesh-operator:latest" format: "table" exit-code: "1" @@ -112,84 +96,108 @@ jobs: - name: Run Trivy vulnerability scanner for java id: scan-java-runner - uses: aquasecurity/trivy-action@v0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: + cache: 'false' image-ref: "pulsar-functions-java-runner:latest" format: "table" exit-code: "0" - name: Run Trivy vulnerability scanner for python id: scan-python-runner - uses: aquasecurity/trivy-action@v0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: + cache: 'false' image-ref: "pulsar-functions-python-runner:latest" format: "table" exit-code: "0" - name: Run Trivy vulnerability scanner for go id: scan-go-runner - uses: aquasecurity/trivy-action@v0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: + cache: 'false' image-ref: "pulsar-functions-go-runner:latest" - format: "template" - template: "@/contrib/sarif.tpl" + format: "sarif" output: "trivy-results-go.sarif" severity: "LOW,MEDIUM,HIGH,CRITICAL" exit-code: "0" - name: Run Trivy vulnerability scanner for java with pulsarctl id: scan-java-pulsarctl-runner - uses: aquasecurity/trivy-action@v0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: + cache: 'false' image-ref: "pulsar-functions-pulsarctl-java-runner:latest" - format: "template" - template: "@/contrib/sarif.tpl" + format: "sarif" output: "trivy-results.sarif" severity: "LOW,MEDIUM,HIGH,CRITICAL" exit-code: "0" - name: Run Trivy vulnerability scanner for python with pulsarctl id: scan-python-pulsarctl-runner - uses: aquasecurity/trivy-action@v0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: + cache: 'false' image-ref: "pulsar-functions-pulsarctl-python-runner:latest" - format: "template" - template: "@/contrib/sarif.tpl" + format: "sarif" output: "trivy-results-python.sarif" severity: "LOW,MEDIUM,HIGH,CRITICAL" exit-code: "0" - name: Run Trivy vulnerability scanner for go with pulsarctl id: scan-go-pulsarctl-runner - uses: aquasecurity/trivy-action@v0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0 with: + cache: 'false' image-ref: "pulsar-functions-pulsarctl-go-runner:latest" - format: "template" + format: "sarif" exit-code: "0" - template: "@/contrib/sarif.tpl" output: "trivy-results-pulsarctl.sarif" severity: "LOW,MEDIUM,HIGH,CRITICAL" + - name: Preserve SARIF for the isolated upload job + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: trivy-sarif + path: trivy-results*.sarif + if-no-files-found: error + retention-days: 1 + + upload: + needs: build + if: github.repository == 'streamnative/function-mesh' && github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + security-events: write + steps: + # Same-run artifacts only. No checkout or execution of artifact contents. + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: trivy-sarif + - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@9418214df1f2c4b8128c770156c409013b07d6b9 # v3 with: sarif_file: "trivy-results.sarif" category: "Default" - name: Upload Trivy go scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@9418214df1f2c4b8128c770156c409013b07d6b9 # v3 with: sarif_file: "trivy-results-go.sarif" category: "Golang" - name: Upload Trivy pulsarctl scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@9418214df1f2c4b8128c770156c409013b07d6b9 # v3 with: sarif_file: "trivy-results-pulsarctl.sarif" category: "Pulsarctl" - name: Upload Trivy python scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@9418214df1f2c4b8128c770156c409013b07d6b9 # v3 with: sarif_file: "trivy-results-python.sarif" category: "Python" diff --git a/.github/workflows/workflow-security.yml b/.github/workflows/workflow-security.yml new file mode 100644 index 000000000..1b337fd38 --- /dev/null +++ b/.github/workflows/workflow-security.yml @@ -0,0 +1,38 @@ +name: Precommit - Workflow security + +on: + pull_request: + paths: + - '.github/**' + - '.ci/tests/integration*/**' + push: + branches: [master] + paths: + - '.github/**' + - '.ci/tests/integration*/**' + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: '3.12' + - name: Install pinned workflow test dependency + run: | + python -m pip install --only-binary=:all: --require-hashes -r .github/scripts/workflow-security-requirements.txt + - name: Validate GitHub Actions syntax and expressions + run: | + curl --fail --silent --show-error --location -o "$RUNNER_TEMP/actionlint.tar.gz" https://github.com/rhysd/actionlint/releases/download/v1.7.7/actionlint_1.7.7_linux_amd64.tar.gz + echo "023070a287cd8cccd71515fedc843f1985bf96c436b7effaecce67290e7e0757 $RUNNER_TEMP/actionlint.tar.gz" | sha256sum --check --strict + tar -xzf "$RUNNER_TEMP/actionlint.tar.gz" -C "$RUNNER_TEMP" actionlint + "$RUNNER_TEMP/actionlint" -shellcheck='' -pyflakes='' + - name: Test workflow trust boundaries + run: python .github/scripts/test_workflow_security.py