diff --git a/.github/workflows/examples/ci.yaml b/.github/workflows/examples/ci.yaml index 89193eb1..d6927b64 100644 --- a/.github/workflows/examples/ci.yaml +++ b/.github/workflows/examples/ci.yaml @@ -5,32 +5,35 @@ name: CI on: - workflow_dispatch: # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: # Allows you to run this workflow manually from the Actions tab workflow_call: # Allows this workflow to be called from another workflow - pull_request: # When a pull request event occurs + pull_request: # When a pull request event occurs -permissions: # Sets permissions of the GITHUB_TOKEN - checks: write # Permits an action to create a check run - contents: read # For actions to fetch code and list commits - packages: read # For actions to fetch packages - id-token: write # Required to fetch an OpenID Connect (OIDC) token - pull-requests: write # Permits an action to add a label to a pull request +permissions: {} # Sets default permissions of the GITHUB_TOKEN jobs: version: name: Calculate version - uses: ./.github/workflows/examples/example-references/_version.yml # Path to an existing github action + permissions: + contents: read # For actions/checkout to fetch code + uses: ./.github/workflows/examples/example-references/_version.yml # Path to an existing github action test: name: Run test + permissions: + checks: write + contents: read + pull-requests: write uses: ./.github/workflows/examples/example-references/_test.yml - with: # Parameters specific to this action that need to be defined in order for the step to be completed + with: # Parameters specific to this action that need to be defined in order for the step to be completed project-name: Billing.Test project-path: ./test/Billing.Test build: name: Run build - needs: # This job will not run until test and version jobs are complete + permissions: + contents: read + needs: # This job will not run until test and version jobs are complete - test - version uses: ./.github/workflows/examples/example-references/_build.yml @@ -41,6 +44,9 @@ jobs: build-push-docker: name: Build Docker image + permissions: + contents: read + id-token: write needs: - test - version diff --git a/.github/workflows/examples/example.yaml b/.github/workflows/examples/example.yaml index ba4be031..2ebd87c5 100644 --- a/.github/workflows/examples/example.yaml +++ b/.github/workflows/examples/example.yaml @@ -7,129 +7,120 @@ name: Build permissions: # Sets permissions of the GITHUB_TOKEN (Can be set at the workflow level or job level) - contents: read - # More info: https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#permissions + contents: read + # More info: https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#permissions on: # Describes when to run the workflow - # https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows + # https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows - workflow_dispatch: # When triggered manually + workflow_dispatch: # When triggered manually - push: # On push to the following branches. Temporarily add a development branch to prompt workflow runs for troubleshooting - branches: ["main", "rc", "hotfix-rc"] - paths-ignore: # Updates to these directories or files will not trigger a workflow run - - ".github/workflows/**" + push: # On push to the following branches. Temporarily add a development branch to prompt workflow runs for troubleshooting + branches: ["main", "rc", "hotfix-rc"] + paths-ignore: # Updates to these directories or files will not trigger a workflow run + - ".github/workflows/**" - # Pull_request_target: #We strongly discourage using this unless absolutely necessary as it requires access to certain Github secrets. - # If using this, include the .github/workflows/check-run.yml job and target only the main branch - # More info at https://github.blog/news-insights/product-news/github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks + # Pull_request_target: #We strongly discourage using this unless absolutely necessary as it requires access to certain Github secrets. + # If using this, include the .github/workflows/check-run.yml job and target only the main branch + # More info at https://github.blog/news-insights/product-news/github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks - pull_request: # When a pull request event occurs - types: - [ - opened, - synchronize, - unlabeled, - labeled, - unlabeled, - reopened, - edited, - ] - branches: ["main"] # Branches where a pull request will trigger the workflow + pull_request: # When a pull request event occurs + types: + [opened, synchronize, unlabeled, labeled, unlabeled, reopened, edited] + branches: ["main"] # Branches where a pull request will trigger the workflow - release: # Runs your workflow when release activity in your repository occurs - types: [published, created] + release: # Runs your workflow when release activity in your repository occurs + types: [published, created] - merge_group: # Runs required status checks on merge groups created by merge queue - types: [checks_requested] + merge_group: # Runs required status checks on merge groups created by merge queue + types: [checks_requested] - repository_dispatch: # Runs when a webook event triggers a workflow from outside of github - types: [contentful-publish] # Optional, limit repository dispatch events to those in a specified list + repository_dispatch: # Runs when a webook event triggers a workflow from outside of github + types: [contentful-publish] # Optional, limit repository dispatch events to those in a specified list - workflow_call: # Workflow can be called by another workflow + workflow_call: # Workflow can be called by another workflow env: # Environment variables set for this step but not accessible by all workflows, steps or jobs. - _AZ_REGISTRY: "ACMEprod.azurecr.io" - INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}" + _AZ_REGISTRY: "ACMEprod.azurecr.io" + INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}" jobs: # A workflow run is made up of one or more jobs that can run sequentially or in parallel - first-job: - name: First Job Name - uses: ./.github/workflows/examples/example-references/_version.yml # Path to an existing github action - if: github.event.pull_request.draft == false # prevent part of a job from running on a draft PR - secrets: inherit # When called by another workflow, pass all the calling workflow's secrets to the called workflow - # "secrets" is only available for a reusable workflow call with "uses" - strategy: # Create multiple job runs for each of a set of variables - fail-fast: false # If true, cancel entire run if any job in the matrix fails - matrix: # Matrix of variables used to define multiple job runs - include: - - project_name: Admin - base_path: ./src - node: true # Enables steps with if: ${{ matrix.node }} - - # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token - permissions: # Sets permissions of the GITHUB_TOKEN - security-events: write # Allow actions to upload results to Github - id-token: write # Required to fetch an OpenID Connect (OIDC) token - contents: read # For actions/checkout to fetch code - deployments: write # Permits an action to create a new deployment - issues: write # Permits an action to create a new issue - checks: write # Permits an action to create a check run - actions: write # Permits an action to cancel a workflow run - packages: read # Permits an action to access packages on GitHub Packages - pull-requests: write # Permits an action to add a label to a pull request - - # steps: when a reusable workflow is called with "uses", "steps" is not available - second-job: - name: Second Job Name - runs-on: ubuntu-22.04 # The type of runner that the job will run on, not available if "uses" is used - permissions: - contents: read - id-token: write # Required to fetch an OpenID Connect (OIDC) token - defaults: - run: # Set the default shell and working directory - shell: bash - working-directory: "home/WorkingDirectory" - - needs: - - first-job # This job will wait until first-job completes - # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/setting-a-default-shell-and-working-directory - steps: - # Using Azure go obtain secrets from Azure Key Vault - - name: Log in to Azure - uses: bitwarden/gh-actions/azure-login@main - with: - subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - tenant_id: ${{ secrets.AZURE_TENANT_ID }} - client_id: ${{ secrets.AZURE_CLIENT_ID }} - - # Obtain the Key Vault secrets and use them later via GitHub outputs - - name: Get Azure Key Vault secrets - id: get-kv-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: gh-REPOSITORY_NAME_EXAMPLE # The name of the Azure Key Vault created for this repossitory - secrets: "SECRETS-OR-CREDENTIALS,ANOTHER-SECRET" # Comma-separated list of secrets to retrieve from Azure Key Vault - - # Logout to remove access to Azure Key Vault secrets - - name: Log out from Azure - uses: bitwarden/gh-actions/azure-logout@main - - - name: Descriptive step name - # NOT RECOMMENDED if: always() # run even if previous steps failed or the workflow is canceled, this can cause a workflow run to hang indefinitely - if: failure() # run when any previous step of a job fails - # if: '!cancelled()' # run even if previous steps failed - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 Always pin a public action version to a full git SHA, followed by the version number in a comment. Version pins are insecure and can introduce vulnerabilities into workflows. - with: # Parameters specific to this action that need to be defined in order for the step to be completed - fetch-depth: 0 # Full git history for actions that rely on whether a change has occurred - ref: ${{ github.event.pull_request.head.sha }} - creds: ${{ steps.get-kv-secrets.outputs.SECRETS-OR-CREDENTIALS }} # Use the secrets retrieved from Azure Key Vault in the previous step - - name: Another descriptive step name - # Run a script instead of an existing github action - run: | - whoami - dotnet --info - node --version - npm --version - echo "GitHub ref: $GITHUB_REF" - echo "GitHub event: $GITHUB_EVENT" + first-job: + name: First Job Name + uses: ./.github/workflows/examples/example-references/_version.yml # Path to an existing github action + if: github.event.pull_request.draft == false # prevent part of a job from running on a draft PR + strategy: # Create multiple job runs for each of a set of variables + fail-fast: false # If true, cancel entire run if any job in the matrix fails + matrix: # Matrix of variables used to define multiple job runs + include: + - project_name: Admin + base_path: ./src + node: true # Enables steps with if: ${{ matrix.node }} + + # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token + permissions: # Sets permissions of the GITHUB_TOKEN + security-events: write # Allow actions to upload results to Github + id-token: write # Required to fetch an OpenID Connect (OIDC) token + contents: read # For actions/checkout to fetch code + deployments: write # Permits an action to create a new deployment + issues: write # Permits an action to create a new issue + checks: write # Permits an action to create a check run + actions: write # Permits an action to cancel a workflow run + packages: read # Permits an action to access packages on GitHub Packages + pull-requests: write # Permits an action to add a label to a pull request + + # steps: when a reusable workflow is called with "uses", "steps" is not available + second-job: + name: Second Job Name + runs-on: ubuntu-22.04 # The type of runner that the job will run on, not available if "uses" is used + permissions: + contents: read + id-token: write # Required to fetch an OpenID Connect (OIDC) token + defaults: + run: # Set the default shell and working directory + shell: bash + working-directory: "home/WorkingDirectory" + + needs: + - first-job # This job will wait until first-job completes + # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/setting-a-default-shell-and-working-directory + steps: + # Using Azure go obtain secrets from Azure Key Vault + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + + # Obtain the Key Vault secrets and use them later via GitHub outputs + - name: Get Azure Key Vault secrets + id: get-kv-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: gh-REPOSITORY_NAME_EXAMPLE # The name of the Azure Key Vault created for this repossitory + secrets: "SECRETS-OR-CREDENTIALS,ANOTHER-SECRET" # Comma-separated list of secrets to retrieve from Azure Key Vault + + # Logout to remove access to Azure Key Vault secrets + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + + - name: Descriptive step name + # NOT RECOMMENDED if: always() # run even if previous steps failed or the workflow is canceled, this can cause a workflow run to hang indefinitely + if: failure() # run when any previous step of a job fails + # if: '!cancelled()' # run even if previous steps failed + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 Always pin a public action version to a full git SHA, followed by the version number in a comment. Version pins are insecure and can introduce vulnerabilities into workflows. + with: # Parameters specific to this action that need to be defined in order for the step to be completed + persist-credentials: false # Do not persist the token used to fetch the repository, more secure + fetch-depth: 0 # Full git history for actions that rely on whether a change has occurred + ref: ${{ github.event.pull_request.head.sha }} + creds: ${{ steps.get-kv-secrets.outputs.SECRETS-OR-CREDENTIALS }} # Use the secrets retrieved from Azure Key Vault in the previous step + - name: Another descriptive step name + # Run a script instead of an existing github action + run: | + whoami + dotnet --info + node --version + npm --version + echo "GitHub ref: $GITHUB_REF" + echo "GitHub event: $GITHUB_EVENT" diff --git a/.github/workflows/examples/pull_request_target.yml b/.github/workflows/examples/pull_request_target.yml index 430a4834..5283c479 100644 --- a/.github/workflows/examples/pull_request_target.yml +++ b/.github/workflows/examples/pull_request_target.yml @@ -7,11 +7,11 @@ name: Build Thing on PR Target permissions: - checks: read + checks: read contents: read on: - pull_request_target: + pull_request_target: # zizmor: ignore[dangerous-triggers] types: [opened, synchronize, reopened] branches: - main @@ -30,4 +30,3 @@ jobs: needs: check-run if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} uses: ./.github/workflows/examples/ci.yaml - secrets: inherit \ No newline at end of file diff --git a/.github/workflows/examples/scan.yaml b/.github/workflows/examples/scan.yaml index 6c9aa418..36d89d17 100644 --- a/.github/workflows/examples/scan.yaml +++ b/.github/workflows/examples/scan.yaml @@ -8,162 +8,165 @@ name: Scan on: - # Controls when the workflow will run - - # Can use other triggers such as multiple events, activity types and fiters: - # https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#on - workflow_dispatch: # When triggered manually - - push: - # On push to the following branches. Temporarily add a development - # branch to prompt workflow runs for troubleshooting - branches: - - "main" - - "rc" - - "hotfix-rc" - pull_request_target: - # When a pull request event occurs. Default is opened or reopened unless - # otherwise specified, as below: - types: [opened, synchronize] # Options include labeled, unlabeled, reopened - branches: "main" + # Controls when the workflow will run + + # Can use other triggers such as multiple events, activity types and fiters: + # https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#on + workflow_dispatch: # When triggered manually + + push: + # On push to the following branches. Temporarily add a development + # branch to prompt workflow runs for troubleshooting + branches: + - "main" + - "rc" + - "hotfix-rc" + pull_request_target: # zizmor: ignore[dangerous-triggers] + # When a pull request event occurs. Default is opened or reopened unless + # otherwise specified, as below: + types: [opened, synchronize] # Options include labeled, unlabeled, reopened + branches: "main" permissions: {} # A workflow run is made up of one or more jobs that can run sequentially or in # parallel jobs: - # This workflow contains the jobs "check-run", "sast", and "quality" - # This job is relatively simple and just imports a previously written action - # to be used in this workflow - check-run: # You set this value with the name of the job you're describing - name: Check PR run # Human readable descriptor - # location and branch of bitwarden-owned action being used - uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main - permissions: - contents: read - - sast: - # A more complex job that has multiple actions as steps described below - name: SAST scan - runs-on: ubuntu-22.04 # The type of runner that the job will run on - needs: check-run # This job will wait until check-run completes - permissions: # Sets permissions of the GITHUB_TOKEN - contents: read # For actions/checkout to fetch code - pull-requests: write # For github actions to upload feedback to PR - # For github/codeql-action/upload-sarif to upload SARIF results - security-events: write - id-token: write # For bitwarden/gh-actions/azure-login to get an ID token - - # Steps represent a sequence of tasks executed as part of the job - steps: - - name: Check out repo - # Always pin a public action version to a full git SHA. - # Version pins are insecure and can introduce vulnerabilities - # into workflows. - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - # Parameters specific to this action that need to be defined - # in order for the step to be completed - ref: ${{ github.event.pull_request.head.sha }} - - - name: Log in to Azure - uses: bitwarden/gh-actions/azure-login@main - with: - subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - tenant_id: ${{ secrets.AZURE_TENANT_ID }} - client_id: ${{ secrets.AZURE_CLIENT_ID }} - - - name: Get Azure Key Vault secrets - id: get-kv-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: gh-org-bitwarden - secrets: "CHECKMARX-TENANT,CHECKMARX-CLIENT-ID,CHECKMARX-SECRET" - - - name: Log out from Azure - uses: bitwarden/gh-actions/azure-logout@main - - - name: Scan with Checkmarx - if: github.event.pull_request.draft == false # Prevent step from running on draft PR - uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36 - # Environment variables set for this step but not accessible by all - # workflows, steps or jobs - env: - INCREMENTAL: - "${{ contains(github.event_name, 'pull_request') \ - && '--sast-incremental' || '' }}" - with: - project_name: ${{ github.repository }} - cx_tenant: ${{ steps.get-kv-secrets.outputs.CHECKMARX-TENANT }} - base_uri: https://ast.checkmarx.net/ - cx_client_id: ${{ steps.get-kv-secrets.outputs.CHECKMARX-CLIENT-ID }} - cx_client_secret: ${{ steps.get-kv-secrets.outputs.CHECKMARX-SECRET }} - additional_params: | - --report-format sarif \ - --filter \ - "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT"\ - --output-path . ${{ env.INCREMENTAL }} - - - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 - with: - sarif_file: cx_result.sarif - sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }} - ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} - - quality: - name: Quality scan - runs-on: ubuntu-22.04 - needs: check-run - permissions: - contents: read - pull-requests: write - id-token: write - - steps: - # Set up whatever resources your environment will need - # to run workflows on your code - - name: Set up JDK 17 - uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 - with: - java-version: 17 - distribution: "zulu" - # This step checks out a copy of your repository - - name: Set up .NET - uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 - # Install a tool without a Github Action - - name: Install SonarCloud scanner - run: dotnet tool install dotnet-sonarscanner -g - - - name: Log in to Azure - uses: bitwarden/gh-actions/azure-login@main - with: - subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - tenant_id: ${{ secrets.AZURE_TENANT_ID }} - client_id: ${{ secrets.AZURE_CLIENT_ID }} - - - name: Get Azure Key Vault secrets - id: get-kv-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: gh-org-bitwarden - secrets: "SONAR-TOKEN" - - - name: Log out from Azure - uses: bitwarden/gh-actions/azure-logout@main - - - name: Scan with SonarCloud - env: - SONAR_TOKEN: ${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Additional scripts to run outside of a Github Action - run: | - dotnet-sonarscanner begin /k:" \ - ${{ github.repository_owner }}_${{ github.event.repository.name }}" \ - /d:sonar.test.inclusions=test/,bitwarden_license/test/ \ - /d:sonar.exclusions=test/,bitwarden_license/test/ \ - /o:"${{ github.repository_owner }}" \ - /d:sonar.token="${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }}" \ - /d:sonar.host.url="https://sonarcloud.io" - dotnet build - dotnet-sonarscanner end /d:sonar.token="${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }}" + # This workflow contains the jobs "check-run", "sast", and "quality" + # This job is relatively simple and just imports a previously written action + # to be used in this workflow + check-run: # You set this value with the name of the job you're describing + name: Check PR run # Human readable descriptor + # location and branch of bitwarden-owned action being used + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + permissions: + contents: read + + sast: + # A more complex job that has multiple actions as steps described below + name: SAST scan + runs-on: ubuntu-22.04 # The type of runner that the job will run on + needs: check-run # This job will wait until check-run completes + permissions: # Sets permissions of the GITHUB_TOKEN + contents: read # For actions/checkout to fetch code + pull-requests: write # For github actions to upload feedback to PR + # For github/codeql-action/upload-sarif to upload SARIF results + security-events: write + id-token: write # For bitwarden/gh-actions/azure-login to get an ID token + + # Steps represent a sequence of tasks executed as part of the job + steps: + - name: Check out repo + # Always pin a public action version to a full git SHA. + # Version pins are insecure and can introduce vulnerabilities + # into workflows. + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # Parameters specific to this action that need to be defined + # in order for the step to be completed + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false # We don't need to push code + + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + + - name: Get Azure Key Vault secrets + id: get-kv-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: gh-org-bitwarden + secrets: "CHECKMARX-TENANT,CHECKMARX-CLIENT-ID,CHECKMARX-SECRET" + + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + + - name: Scan with Checkmarx + if: github.event.pull_request.draft == false # Prevent step from running on draft PR + uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36 + # Environment variables set for this step but not accessible by all + # workflows, steps or jobs + env: + INCREMENTAL: "${{ contains(github.event_name, 'pull_request') \ + && '--sast-incremental' || '' }}" + with: + project_name: ${{ github.repository }} + cx_tenant: ${{ steps.get-kv-secrets.outputs.CHECKMARX-TENANT }} + base_uri: https://ast.checkmarx.net/ + cx_client_id: ${{ steps.get-kv-secrets.outputs.CHECKMARX-CLIENT-ID }} + cx_client_secret: ${{ steps.get-kv-secrets.outputs.CHECKMARX-SECRET }} + additional_params: | + --report-format sarif \ + --filter \ + "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT"\ + --output-path . ${{ env.INCREMENTAL }} + + - name: Upload Checkmarx results to GitHub + uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + with: + sarif_file: cx_result.sarif + sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }} + ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} + + quality: + name: Quality scan + runs-on: ubuntu-22.04 + needs: check-run + permissions: + contents: read + pull-requests: write + id-token: write + + steps: + # Set up whatever resources your environment will need + # to run workflows on your code + - name: Set up JDK 17 + uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0 + with: + java-version: 17 + distribution: "zulu" + # This step checks out a copy of your repository + - name: Set up .NET + uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 + # Install a tool without a Github Action + - name: Install SonarCloud scanner + run: dotnet tool install dotnet-sonarscanner -g + + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + + - name: Get Azure Key Vault secrets + id: get-kv-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: gh-org-bitwarden + secrets: "SONAR-TOKEN" + + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + + - name: Scan with SonarCloud + env: + SONAR_TOKEN: ${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + _REPOSITORY_OWNER: ${{ github.repository_owner }} + _REPOSITORY_NAME: ${{ github.event.repository.name }} + + # Additional scripts to run outside of a Github Action + run: | + dotnet-sonarscanner begin /k:" \ + ${_REPOSITORY_OWNER}_${_REPOSITORY_NAME}" \ + /d:sonar.test.inclusions=test/,bitwarden_license/test/ \ + /d:sonar.exclusions=test/,bitwarden_license/test/ \ + /o:"$_REPOSITORY_OWNER" \ + /d:sonar.token="$SONAR_TOKEN" \ + /d:sonar.host.url="https://sonarcloud.io" + dotnet build + dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN" diff --git a/.gitignore b/.gitignore index 181dcd9c..4b2f297b 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ flake.* .venv/ .pytest_cache .pytype + +download-actionlint.bash +actionlint diff --git a/Taskfile.yml b/Taskfile.yml index 0e47f49b..eca75570 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,6 +1,6 @@ # https://taskfile.dev -version: '3' +version: "3" tasks: fmt: @@ -69,6 +69,16 @@ tasks: cmds: - pipenv run bwwl actions --output test.json update + zizmor: + desc: "Run zizmor directly on GitHub workflows" + cmds: + - zizmor {{.CLI_ARGS | default ".github/workflows"}} + + zizmor:install: + desc: "Install zizmor via pip" + cmds: + - pip install zizmor + dist: silent: true cmds: diff --git a/settings.yaml b/settings.yaml index c65e0c7b..d60cda40 100644 --- a/settings.yaml +++ b/settings.yaml @@ -21,10 +21,14 @@ enabled_rules: level: error - id: bitwarden_workflow_linter.rules.check_blocked_domains.RuleCheckBlockedDomains level: warning + # - id: bitwarden_workflow_linter.rules.run_zizmor.RunZizmor + # level: error approved_actions_path: default_actions.json default_branch: main # List of domains that should trigger an error if found in workflows blocked_domains: - - ghrc.io + - ghrc.io +# Optional: URL to shared base zizmor configuration file +zizmor_config_url: https://raw.githubusercontent.com/bitwarden/workflow-linter/refs/heads/main/zizmor.yml diff --git a/src/bitwarden_workflow_linter/default_settings.yaml b/src/bitwarden_workflow_linter/default_settings.yaml index c65e0c7b..60aac0da 100644 --- a/src/bitwarden_workflow_linter/default_settings.yaml +++ b/src/bitwarden_workflow_linter/default_settings.yaml @@ -21,10 +21,14 @@ enabled_rules: level: error - id: bitwarden_workflow_linter.rules.check_blocked_domains.RuleCheckBlockedDomains level: warning + - id: bitwarden_workflow_linter.rules.run_zizmor.RunZizmor + level: warning approved_actions_path: default_actions.json default_branch: main # List of domains that should trigger an error if found in workflows blocked_domains: - - ghrc.io + - ghrc.io +# Optional: URL to zizmor configuration file +# zizmor_config_url: https://raw.githubusercontent.com/bitwarden/workflow-linter/refs/heads/main/zizmor.yml diff --git a/src/bitwarden_workflow_linter/rules/run_zizmor.py b/src/bitwarden_workflow_linter/rules/run_zizmor.py new file mode 100644 index 00000000..e3483d3e --- /dev/null +++ b/src/bitwarden_workflow_linter/rules/run_zizmor.py @@ -0,0 +1,145 @@ +"""A Rule to run zizmor on workflows.""" + +from typing import Optional, Tuple +import subprocess +import platform +import urllib.request +import tempfile +import os + +from ..rule import Rule +from ..models.workflow import Workflow +from ..utils import LintLevels, Settings + + +def install_zizmor(platform_system: str, version: str) -> Tuple[bool, str]: + """Install zizmor via pip.""" + error = f"An error occurred when installing Zizmor on {platform_system}" + try: + subprocess.run( + ["pip", "install", f"zizmor=={version}"], check=True, capture_output=True + ) + return True, "" + except (FileNotFoundError, subprocess.CalledProcessError): + return False, f"{error} : check pip installation" + + +def check_zizmor_path(platform_system: str, version: str) -> Tuple[bool, str]: + """Check if zizmor is in the system's PATH.""" + try: + result = subprocess.run( + ["zizmor", "--version"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + text=True, + ) + installed_version = result.stdout.strip().split()[-1] if result.stdout else "" + if version in installed_version: + return True, "" + else: + return install_zizmor(platform_system, version) + except subprocess.CalledProcessError: + return ( + False, + ( + "Failed to install zizmor, please check your pip " + "installation or manually install it" + ), + ) + except FileNotFoundError: + return install_zizmor(platform_system, version) + + +def download_config_file(config_url: str) -> Optional[str]: + """Download zizmor config file from remote URL.""" + if not config_url: + return None + + try: + with urllib.request.urlopen(config_url) as response: + config_content = response.read() + + # Create temporary file for config + with tempfile.NamedTemporaryFile( + mode="w", suffix=".yml", delete=False + ) as temp_file: + temp_file.write(config_content.decode("utf-8")) + return temp_file.name + except (urllib.error.URLError, IOError): + return None + + +class RunZizmor(Rule): + """Rule to run zizmor as part of workflow linter.""" + + def __init__( + self, + settings: Optional[Settings] = None, + lint_level: Optional[LintLevels] = LintLevels.WARNING, + ) -> None: + self.message = "Zizmor must pass without errors" + self.on_fail = lint_level + self.compatibility = [Workflow] + self.settings = settings + + def fn(self, obj: Workflow) -> Tuple[bool, str]: + if not obj or not obj.filename: + raise AttributeError( + "Running zizmor without a filename is not currently supported" + ) + + if not self.settings.zizmor_version: + raise KeyError("The 'zizmor_version' is missing in the configuration file.") + + # Check if zizmor is already installed + installed, error = check_zizmor_path( + platform.system(), self.settings.zizmor_version + ) + if not installed: + return False, error + + # Build zizmor command + cmd = ["zizmor", "--format", "plain"] + + # Add config file if specified + config_file = None + if self.settings.zizmor_config_url: + config_file = download_config_file(self.settings.zizmor_config_url) + if config_file: + cmd.extend(["--config", config_file]) + + # Add the workflow file + cmd.append(obj.filename) + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=False, + ) + + # Clean up temporary config file if created + if config_file: + try: + os.unlink(config_file) + except OSError: + pass + + # zizmor returns 0 for success, non-zero for findings or errors + if result.returncode == 0: + return True, "" + else: + # Return the findings/errors from zizmor + output = result.stdout if result.stdout else result.stderr + return False, output + + except (subprocess.CalledProcessError, FileNotFoundError, OSError) as e: + # Clean up temporary config file if created + if config_file: + try: + os.unlink(config_file) + except OSError: + pass + return False, f"Error running zizmor: {str(e)}" diff --git a/src/bitwarden_workflow_linter/utils.py b/src/bitwarden_workflow_linter/utils.py index 935084d3..41148a1d 100644 --- a/src/bitwarden_workflow_linter/utils.py +++ b/src/bitwarden_workflow_linter/utils.py @@ -113,6 +113,8 @@ class Settings: enabled_rules: list[dict[str, str]] approved_actions: dict[str, Action] actionlint_version: str + zizmor_version: str + zizmor_config_url: Optional[str] default_branch: Optional[str] blocked_domains: Optional[list[str]] @@ -121,6 +123,8 @@ def __init__( enabled_rules: Optional[list[dict[str, str]]] = None, approved_actions: Optional[dict[str, dict[str, str]]] = None, actionlint_version: Optional[str] = None, + zizmor_version: Optional[str] = None, + zizmor_config_url: Optional[str] = None, default_branch: Optional[str] = None, blocked_domains: Optional[list[str]] = None, ) -> None: @@ -139,15 +143,27 @@ def __init__( if approved_actions is None: approved_actions = {} - + if actionlint_version is None: actionlint_version = "" + if zizmor_version is None: + zizmor_version = "" + self.actionlint_version = actionlint_version + self.zizmor_version = zizmor_version + self.zizmor_config_url = zizmor_config_url self.enabled_rules = enabled_rules - self.approved_actions = { - name: Action(**action) for name, action in approved_actions.items() - } + # Handle both dict[str, dict] and dict[str, Action] + # for approved_actions for testing help + self.approved_actions = {} + for name, action in approved_actions.items(): + if isinstance(action, Action): + # Already an Action object, use it directly + self.approved_actions[name] = action + else: + # Dictionary, create Action object + self.approved_actions[name] = Action(**action) self.default_branch = default_branch self.blocked_domains = blocked_domains or [] @@ -170,6 +186,15 @@ def factory() -> SettingsFromFactory: version_data = yaml.load(version_file) actionlint_version = version_data["actionlint_version"] + # load zizmor version + with ( + importlib.resources.files("bitwarden_workflow_linter") + .joinpath("zizmor_version.yaml") + .open("r", encoding="utf-8") as version_file + ): + version_data = yaml.load(version_file) + zizmor_version = version_data["zizmor_version"] + # load override settings settings_filename = "settings.yaml" local_settings = None @@ -197,12 +222,14 @@ def factory() -> SettingsFromFactory: default_branch = settings.get("default_branch") if default_branch is None or len(default_branch) == 0: - raise Exception("The default_branch is not set in the default_settings.yaml file") + raise Exception("The default_branch is not set in the default_settings.yaml file") return Settings( enabled_rules=settings["enabled_rules"], approved_actions=settings["approved_actions"], actionlint_version=actionlint_version, + zizmor_version=zizmor_version, + zizmor_config_url=settings.get("zizmor_config_url"), default_branch=default_branch, blocked_domains=settings.get("blocked_domains", []), ) diff --git a/src/bitwarden_workflow_linter/zizmor_version.yaml b/src/bitwarden_workflow_linter/zizmor_version.yaml new file mode 100644 index 00000000..d9028505 --- /dev/null +++ b/src/bitwarden_workflow_linter/zizmor_version.yaml @@ -0,0 +1 @@ +zizmor_version: "1.14.2" diff --git a/tests/rules/test_run_zizmor.py b/tests/rules/test_run_zizmor.py new file mode 100644 index 00000000..ecb798b7 --- /dev/null +++ b/tests/rules/test_run_zizmor.py @@ -0,0 +1,308 @@ +"""Test src/bitwarden_workflow_linter/rules/run_zizmor.""" + +import pytest +import subprocess +import tempfile +import urllib.request +import os + +import src.bitwarden_workflow_linter.rules.run_zizmor as zizmor_module +from src.bitwarden_workflow_linter.utils import Settings +from src.bitwarden_workflow_linter.load import WorkflowBuilder +from src.bitwarden_workflow_linter.rules.run_zizmor import ( + RunZizmor, + install_zizmor, + check_zizmor_path, + download_config_file, +) + +settings = Settings.factory() + + +@pytest.fixture(name="rule") +def fixture_rule(): + return RunZizmor(settings) + + +def test_rule_on_correct_workflow(rule): + """Test zizmor rule on a correct workflow.""" + rule.settings = settings + correct_workflow = WorkflowBuilder.build("tests/fixtures/test_workflow.yaml") + + # Mock successful zizmor execution + def mock_check_zizmor_path(*args, **kwargs): + return True, "" + + def mock_run(*args, **kwargs): + return subprocess.CompletedProcess(args, 0, stdout="") + + original_check = zizmor_module.check_zizmor_path + original_run = subprocess.run + + try: + zizmor_module.check_zizmor_path = mock_check_zizmor_path + subprocess.run = mock_run + + result, _ = rule.fn(correct_workflow) + assert result is True + finally: + zizmor_module.check_zizmor_path = original_check + subprocess.run = original_run + + +def test_rule_on_workflow_with_findings(rule): + """Test zizmor rule on a workflow that has findings.""" + rule.settings = settings + workflow = WorkflowBuilder.build("tests/fixtures/test_workflow.yaml") + + # Mock zizmor execution with findings + def mock_check_zizmor_path(*args, **kwargs): + return True, "" + + def mock_run(*args, **kwargs): + return subprocess.CompletedProcess(args, 1, + stdout="warning: insecure action found") + + original_check = zizmor_module.check_zizmor_path + original_run = subprocess.run + + try: + zizmor_module.check_zizmor_path = mock_check_zizmor_path + subprocess.run = mock_run + + result, error = rule.fn(workflow) + assert result is False + assert "warning: insecure action found" in error + finally: + zizmor_module.check_zizmor_path = original_check + subprocess.run = original_run + + +def test_install_zizmor(monkeypatch): + """Test installing zizmor.""" + def mock_run(*args, **kwargs): + return subprocess.CompletedProcess(args, 0) + + monkeypatch.setattr(subprocess, "run", mock_run) + result, _ = install_zizmor("Error", settings.zizmor_version) + assert result is True + + +def test_failed_install_zizmor(monkeypatch): + """Test failed zizmor installation.""" + def mock_run(*args, **kwargs): + raise subprocess.CalledProcessError(1, "cmd") + + monkeypatch.setattr(subprocess, "run", mock_run) + result, error = install_zizmor("Error", settings.zizmor_version) + assert result is False + assert "check pip installation" in error + + +def test_install_zizmor_linux(monkeypatch): + """Test zizmor installation on Linux.""" + def mock_run(*args, **kwargs): + return subprocess.CompletedProcess(args, 0) + + monkeypatch.setattr(subprocess, "run", mock_run) + result, _ = install_zizmor("Linux", settings.zizmor_version) + assert result is True + + +def test_install_zizmor_darwin(monkeypatch): + """Test zizmor installation on Darwin/macOS.""" + def mock_run(*args, **kwargs): + return subprocess.CompletedProcess(args, 0) + + monkeypatch.setattr(subprocess, "run", mock_run) + result, _ = install_zizmor("Darwin", settings.zizmor_version) + assert result is True + + +def test_install_zizmor_windows(monkeypatch): + """Test zizmor installation on Windows.""" + def mock_run(*args, **kwargs): + return subprocess.CompletedProcess(args, 0) + + monkeypatch.setattr(subprocess, "run", mock_run) + result, _ = install_zizmor("Windows", settings.zizmor_version) + assert result is True + + +def test_check_zizmor_path_installed(monkeypatch): + """Test checking zizmor when it's already installed.""" + def mock_run(*args, **kwargs): + return subprocess.CompletedProcess( + args, 0, stdout=f"zizmor {settings.zizmor_version}" + ) + + monkeypatch.setattr(subprocess, "run", mock_run) + result, _ = check_zizmor_path("Linux", settings.zizmor_version) + assert result is True + + +def test_check_zizmor_path_not_installed(monkeypatch): + """Test checking zizmor when it's not installed.""" + def mock_run(*args, **kwargs): + raise FileNotFoundError + + monkeypatch.setattr(subprocess, "run", mock_run) + result, _ = check_zizmor_path("Linux", settings.zizmor_version) + assert result is False + + +def test_check_zizmor_path_wrong_version(monkeypatch): + """Test checking zizmor when wrong version is installed.""" + def mock_run(*args, **kwargs): + return subprocess.CompletedProcess(args, 0, stdout="zizmor 0.1.0") + + def mock_install(*args, **kwargs): + return True, "" + + monkeypatch.setattr(subprocess, "run", mock_run) + monkeypatch.setattr( + "src.bitwarden_workflow_linter.rules.run_zizmor.install_zizmor", + mock_install + ) + result, _ = check_zizmor_path("Linux", settings.zizmor_version) + assert result is True + + +def test_download_config_file_success(monkeypatch): + """Test downloading config file successfully.""" + mock_config = "# zizmor config\nrules = []" + + class MockResponse: + def read(self): + return mock_config.encode("utf-8") + + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + def mock_urlopen(url): + return MockResponse() + + original_urlopen = urllib.request.urlopen + + try: + urllib.request.urlopen = mock_urlopen + config_path = download_config_file("https://example.com/config.yaml") + + assert config_path is not None + + # Verify the file was created with correct content + with open(config_path, "r") as f: + content = f.read() + assert mock_config in content + + # Clean up + os.unlink(config_path) + finally: + urllib.request.urlopen = original_urlopen + +def test_download_config_file_failure(): + """Test downloading config file failure.""" + config_path = download_config_file("https://invalid-url") + assert config_path is None + + +def test_download_config_file_empty_url(): + """Test downloading config file with empty URL.""" + config_path = download_config_file("") + assert config_path is None + + +def test_rule_with_config_url(rule): + """Test zizmor rule with config URL.""" + # Create a temporary settings object with config URL + temp_settings = Settings( + enabled_rules=settings.enabled_rules, + approved_actions=settings.approved_actions, + actionlint_version=settings.actionlint_version, + zizmor_version=settings.zizmor_version, + zizmor_config_url="https://example.com/zizmor.yml", + default_branch=settings.default_branch, + blocked_domains=settings.blocked_domains + ) + rule.settings = temp_settings + + workflow = WorkflowBuilder.build("tests/fixtures/test_workflow.yaml") + + # Mock successful operations + def mock_check_zizmor_path(*args, **kwargs): + return True, "" + + def mock_download_config_file(url): + temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".yml", delete=False) + temp_file.write("# mock config") + temp_file.close() + return temp_file.name + + def mock_run(*args, **kwargs): + return subprocess.CompletedProcess(args, 0, stdout="") + + original_check = zizmor_module.check_zizmor_path + original_download = zizmor_module.download_config_file + original_run = subprocess.run + + try: + zizmor_module.check_zizmor_path = mock_check_zizmor_path + zizmor_module.download_config_file = mock_download_config_file + subprocess.run = mock_run + + result, _ = rule.fn(workflow) + assert result is True + finally: + zizmor_module.check_zizmor_path = original_check + zizmor_module.download_config_file = original_download + subprocess.run = original_run + + +def test_rule_without_filename(rule): + """Test zizmor rule with workflow without filename.""" + rule.settings = settings + workflow = WorkflowBuilder.build("tests/fixtures/test_workflow.yaml") + workflow.filename = None + + with pytest.raises(AttributeError, match="Running zizmor without a filename"): + rule.fn(workflow) + + +def test_rule_without_zizmor_version(): + """Test zizmor rule without zizmor version in settings.""" + temp_settings = Settings( + enabled_rules=settings.enabled_rules, + approved_actions=settings.approved_actions, + actionlint_version=settings.actionlint_version, + zizmor_version="", # Empty version + default_branch=settings.default_branch, + blocked_domains=settings.blocked_domains + ) + rule = RunZizmor(temp_settings) + workflow = WorkflowBuilder.build("tests/fixtures/test_workflow.yaml") + + with pytest.raises(KeyError, match="zizmor_version"): + rule.fn(workflow) + + +def test_rule_zizmor_not_installed(rule): + """Test zizmor rule when zizmor is not installed.""" + rule.settings = settings + workflow = WorkflowBuilder.build("tests/fixtures/test_workflow.yaml") + + def mock_check_zizmor_path(*args, **kwargs): + return False, "zizmor not found" + + original_check = zizmor_module.check_zizmor_path + + try: + zizmor_module.check_zizmor_path = mock_check_zizmor_path + + result, error = rule.fn(workflow) + assert result is False + assert "zizmor not found" in error + finally: + zizmor_module.check_zizmor_path = original_check diff --git a/zizmor.yml b/zizmor.yml new file mode 100644 index 00000000..11a81c8f --- /dev/null +++ b/zizmor.yml @@ -0,0 +1,7 @@ +rules: + unpinned-uses: + config: + policies: + bitwarden/gh-actions/*: ref-pin + dangerous-triggers: + disable: true