diff --git a/.github/actions/setup-python-poetry/action.yml b/.github/actions/setup-python-poetry/action.yml index cb17c050a2..62d01091d8 100644 --- a/.github/actions/setup-python-poetry/action.yml +++ b/.github/actions/setup-python-poetry/action.yml @@ -35,7 +35,9 @@ runs: shell: bash run: | python -m pip install --upgrade pip - pipx install poetry==${{ inputs.poetry-version }} + pipx install poetry==${INPUTS_POETRY_VERSION} + env: + INPUTS_POETRY_VERSION: ${{ inputs.poetry-version }} - name: Update poetry.lock with latest Prowler commit if: github.repository_owner == 'prowler-cloud' && github.repository != 'prowler-cloud/prowler' diff --git a/.github/actions/slack-notification/action.yml b/.github/actions/slack-notification/action.yml index 1427fed3e0..973779170d 100644 --- a/.github/actions/slack-notification/action.yml +++ b/.github/actions/slack-notification/action.yml @@ -26,16 +26,18 @@ runs: id: status shell: bash run: | - if [[ "${{ inputs.step-outcome }}" == "success" ]]; then + if [[ "${INPUTS_STEP_OUTCOME}" == "success" ]]; then echo "STATUS_TEXT=Completed" >> $GITHUB_ENV echo "STATUS_COLOR=#6aa84f" >> $GITHUB_ENV - elif [[ "${{ inputs.step-outcome }}" == "failure" ]]; then + elif [[ "${INPUTS_STEP_OUTCOME}" == "failure" ]]; then echo "STATUS_TEXT=Failed" >> $GITHUB_ENV echo "STATUS_COLOR=#fc3434" >> $GITHUB_ENV else # No outcome provided - pending/in progress state echo "STATUS_COLOR=#dbab09" >> $GITHUB_ENV fi + env: + INPUTS_STEP_OUTCOME: ${{ inputs.step-outcome }} - name: Send Slack notification (new message) if: inputs.update-ts == '' @@ -67,8 +69,11 @@ runs: id: slack-notification shell: bash run: | - if [[ "${{ inputs.update-ts }}" == "" ]]; then - echo "ts=${{ steps.slack-notification-post.outputs.ts }}" >> $GITHUB_OUTPUT + if [[ "${INPUTS_UPDATE_TS}" == "" ]]; then + echo "ts=${STEPS_SLACK_NOTIFICATION_POST_OUTPUTS_TS}" >> $GITHUB_OUTPUT else - echo "ts=${{ inputs.update-ts }}" >> $GITHUB_OUTPUT + echo "ts=${INPUTS_UPDATE_TS}" >> $GITHUB_OUTPUT fi + env: + INPUTS_UPDATE_TS: ${{ inputs.update-ts }} + STEPS_SLACK_NOTIFICATION_POST_OUTPUTS_TS: ${{ steps.slack-notification-post.outputs.ts }} diff --git a/.github/actions/trivy-scan/action.yml b/.github/actions/trivy-scan/action.yml index 5eca1266b0..b6db4499bc 100644 --- a/.github/actions/trivy-scan/action.yml +++ b/.github/actions/trivy-scan/action.yml @@ -54,7 +54,7 @@ runs: trivy-db-${{ runner.os }}- - name: Run Trivy vulnerability scan (JSON) - uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1 + uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # 0.34.1 with: image-ref: ${{ inputs.image-name }}:${{ inputs.image-tag }} format: 'json' @@ -66,7 +66,7 @@ runs: - name: Run Trivy vulnerability scan (SARIF) if: inputs.upload-sarif == 'true' && github.event_name == 'push' - uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1 + uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # 0.34.1 with: image-ref: ${{ inputs.image-name }}:${{ inputs.image-tag }} format: 'sarif' @@ -105,11 +105,14 @@ runs: echo "### 🔒 Container Security Scan" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "**Image:** \`${{ inputs.image-name }}:${{ inputs.image-tag }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Image:** \`${INPUTS_IMAGE_NAME}:${INPUTS_IMAGE_TAG}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "- 🔴 Critical: $CRITICAL" >> $GITHUB_STEP_SUMMARY echo "- 🟠 High: $HIGH" >> $GITHUB_STEP_SUMMARY echo "- **Total**: $TOTAL" >> $GITHUB_STEP_SUMMARY + env: + INPUTS_IMAGE_NAME: ${{ inputs.image-name }} + INPUTS_IMAGE_TAG: ${{ inputs.image-tag }} - name: Comment scan results on PR if: inputs.create-pr-comment == 'true' && github.event_name == 'pull_request' @@ -123,7 +126,7 @@ runs: const comment = require('./.github/scripts/trivy-pr-comment.js'); // Unique identifier to find our comment - const marker = ''; + const marker = ``; const body = marker + '\n' + comment; // Find existing comment @@ -159,6 +162,9 @@ runs: if: inputs.fail-on-critical == 'true' && steps.security-check.outputs.critical != '0' shell: bash run: | - echo "::error::Found ${{ steps.security-check.outputs.critical }} critical vulnerabilities" + echo "::error::Found ${STEPS_SECURITY_CHECK_OUTPUTS_CRITICAL} critical vulnerabilities" echo "::warning::Please update packages or use a different base image" exit 1 + + env: + STEPS_SECURITY_CHECK_OUTPUTS_CRITICAL: ${{ steps.security-check.outputs.critical }} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f4f12db90b..28eff02ff6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,6 +15,8 @@ updates: labels: - "dependencies" - "pip" + cooldown: + default-days: 7 # Dependabot Updates are temporary disabled - 2025/03/19 # - package-ecosystem: "pip" @@ -37,6 +39,8 @@ updates: labels: - "dependencies" - "github_actions" + cooldown: + default-days: 7 # Dependabot Updates are temporary disabled - 2025/03/19 # - package-ecosystem: "npm" @@ -59,6 +63,8 @@ updates: labels: - "dependencies" - "docker" + cooldown: + default-days: 7 # Dependabot Updates are temporary disabled - 2025/04/15 # v4.6 diff --git a/.github/workflows/api-bump-version.yml b/.github/workflows/api-bump-version.yml index 97cdd546ff..720defc22d 100644 --- a/.github/workflows/api-bump-version.yml +++ b/.github/workflows/api-bump-version.yml @@ -29,6 +29,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Get current API version id: get_api_version @@ -79,12 +81,14 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Calculate next API minor version run: | - MAJOR_VERSION=${{ needs.detect-release-type.outputs.major_version }} - MINOR_VERSION=${{ needs.detect-release-type.outputs.minor_version }} - CURRENT_API_VERSION="${{ needs.detect-release-type.outputs.current_api_version }}" + MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} + MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} + CURRENT_API_VERSION="${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_API_VERSION}" # API version follows Prowler minor + 1 # For Prowler 5.17.0 -> API 1.18.0 @@ -97,6 +101,10 @@ jobs: echo "Prowler release version: ${MAJOR_VERSION}.${MINOR_VERSION}.0" echo "Current API version: $CURRENT_API_VERSION" echo "Next API minor version (for master): $NEXT_API_VERSION" + env: + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_API_VERSION: ${{ needs.detect-release-type.outputs.current_api_version }} - name: Bump API versions in files for master run: | @@ -132,12 +140,13 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: v${{ needs.detect-release-type.outputs.major_version }}.${{ needs.detect-release-type.outputs.minor_version }} + persist-credentials: false - name: Calculate first API patch version run: | - MAJOR_VERSION=${{ needs.detect-release-type.outputs.major_version }} - MINOR_VERSION=${{ needs.detect-release-type.outputs.minor_version }} - CURRENT_API_VERSION="${{ needs.detect-release-type.outputs.current_api_version }}" + MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} + MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} + CURRENT_API_VERSION="${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_API_VERSION}" VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} # API version follows Prowler minor + 1 @@ -151,6 +160,10 @@ jobs: echo "Prowler release version: ${MAJOR_VERSION}.${MINOR_VERSION}.0" echo "First API patch version (for ${VERSION_BRANCH}): $FIRST_API_PATCH_VERSION" echo "Version branch: $VERSION_BRANCH" + env: + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_API_VERSION: ${{ needs.detect-release-type.outputs.current_api_version }} - name: Bump API versions in files for version branch run: | @@ -193,13 +206,15 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Calculate next API patch version run: | - MAJOR_VERSION=${{ needs.detect-release-type.outputs.major_version }} - MINOR_VERSION=${{ needs.detect-release-type.outputs.minor_version }} - PATCH_VERSION=${{ needs.detect-release-type.outputs.patch_version }} - CURRENT_API_VERSION="${{ needs.detect-release-type.outputs.current_api_version }}" + MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} + MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} + PATCH_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_PATCH_VERSION} + CURRENT_API_VERSION="${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_API_VERSION}" VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} # Extract current API patch to increment it @@ -222,6 +237,11 @@ jobs: echo "::error::Invalid API version format: $CURRENT_API_VERSION" exit 1 fi + env: + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_PATCH_VERSION: ${{ needs.detect-release-type.outputs.patch_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_API_VERSION: ${{ needs.detect-release-type.outputs.current_api_version }} - name: Bump API versions in files for version branch run: | diff --git a/.github/workflows/api-code-quality.yml b/.github/workflows/api-code-quality.yml index c5cc02a298..88f7d1bce0 100644 --- a/.github/workflows/api-code-quality.yml +++ b/.github/workflows/api-code-quality.yml @@ -34,6 +34,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for API changes id: check-changes diff --git a/.github/workflows/api-codeql.yml b/.github/workflows/api-codeql.yml index 45cc911050..cb8c6f05ec 100644 --- a/.github/workflows/api-codeql.yml +++ b/.github/workflows/api-codeql.yml @@ -43,6 +43,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Initialize CodeQL uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 diff --git a/.github/workflows/api-container-build-push.yml b/.github/workflows/api-container-build-push.yml index 17693785f7..e6c84fb124 100644 --- a/.github/workflows/api-container-build-push.yml +++ b/.github/workflows/api-container-build-push.yml @@ -58,6 +58,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Notify container push started id: slack-notification @@ -94,6 +96,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Login to DockerHub uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 @@ -138,18 +142,22 @@ jobs: run: | docker buildx imagetools create \ -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }} \ - -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }} \ - ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64 \ - ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64 + -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA} \ + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-amd64 \ + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-arm64 + env: + NEEDS_SETUP_OUTPUTS_SHORT_SHA: ${{ needs.setup.outputs.short-sha }} - name: Create and push manifests for release event if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' run: | docker buildx imagetools create \ - -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }} \ + -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${RELEASE_TAG} \ -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }} \ - ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64 \ - ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64 + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-amd64 \ + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-arm64 + env: + NEEDS_SETUP_OUTPUTS_SHORT_SHA: ${{ needs.setup.outputs.short-sha }} - name: Install regctl if: always() @@ -159,9 +167,11 @@ jobs: if: always() run: | echo "Cleaning up intermediate tags..." - regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64" || true - regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64" || true + regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-amd64" || true + regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-arm64" || true echo "Cleanup completed" + env: + NEEDS_SETUP_OUTPUTS_SHORT_SHA: ${{ needs.setup.outputs.short-sha }} notify-release-completed: if: always() && needs.notify-release-started.result == 'success' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch') @@ -171,15 +181,20 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Determine overall outcome id: outcome run: | - if [[ "${{ needs.container-build-push.result }}" == "success" && "${{ needs.create-manifest.result }}" == "success" ]]; then + if [[ "${NEEDS_CONTAINER_BUILD_PUSH_RESULT}" == "success" && "${NEEDS_CREATE_MANIFEST_RESULT}" == "success" ]]; then echo "outcome=success" >> $GITHUB_OUTPUT else echo "outcome=failure" >> $GITHUB_OUTPUT fi + env: + NEEDS_CONTAINER_BUILD_PUSH_RESULT: ${{ needs.container-build-push.result }} + NEEDS_CREATE_MANIFEST_RESULT: ${{ needs.create-manifest.result }} - name: Notify container push completed uses: ./.github/actions/slack-notification diff --git a/.github/workflows/api-container-checks.yml b/.github/workflows/api-container-checks.yml index e1bca8091c..b516a25bfd 100644 --- a/.github/workflows/api-container-checks.yml +++ b/.github/workflows/api-container-checks.yml @@ -29,6 +29,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check if Dockerfile changed id: dockerfile-changed @@ -64,6 +67,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for API changes id: check-changes diff --git a/.github/workflows/api-security.yml b/.github/workflows/api-security.yml index 04cb0ba7ff..5814af4549 100644 --- a/.github/workflows/api-security.yml +++ b/.github/workflows/api-security.yml @@ -34,6 +34,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for API changes id: check-changes diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index ec235fd33b..4f4e814463 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -43,7 +43,7 @@ jobs: services: postgres: - image: postgres + image: postgres:17@sha256:2cd82735a36356842d5eb1ef80db3ae8f1154172f0f653db48fde079b2a0b7f7 env: POSTGRES_HOST: ${{ env.POSTGRES_HOST }} POSTGRES_PORT: ${{ env.POSTGRES_PORT }} @@ -74,6 +74,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for API changes id: check-changes diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 974d919fc6..79e578ffae 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -1,6 +1,7 @@ name: 'Tools: Backport' on: + # zizmor: ignore[dangerous-triggers] - intentional: needs write access for backport PRs, no PR code checkout pull_request_target: branches: - 'master' diff --git a/.github/workflows/ci-zizmor.yml b/.github/workflows/ci-zizmor.yml new file mode 100644 index 0000000000..1ffb6b00fa --- /dev/null +++ b/.github/workflows/ci-zizmor.yml @@ -0,0 +1,44 @@ +name: 'CI: Zizmor' + +on: + push: + branches: + - 'master' + - 'v5.*' + paths: + - '.github/**' + pull_request: + branches: + - 'master' + - 'v5.*' + paths: + - '.github/**' + schedule: + - cron: '30 06 * * *' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + zizmor: + if: github.repository == 'prowler-cloud/prowler' + name: GitHub Actions Security Audit + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + security-events: write + contents: read + actions: read + + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0 + with: + token: ${{ github.token }} diff --git a/.github/workflows/create-backport-label.yml b/.github/workflows/create-backport-label.yml index b4308156c7..32bc3759fd 100644 --- a/.github/workflows/create-backport-label.yml +++ b/.github/workflows/create-backport-label.yml @@ -25,8 +25,9 @@ jobs: - name: Create backport label for minor releases env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_EVENT_RELEASE_TAG_NAME: ${{ github.event.release.tag_name }} run: | - RELEASE_TAG="${{ github.event.release.tag_name }}" + RELEASE_TAG="${GITHUB_EVENT_RELEASE_TAG_NAME}" if [ -z "$RELEASE_TAG" ]; then echo "Error: No release tag provided" diff --git a/.github/workflows/docs-bump-version.yml b/.github/workflows/docs-bump-version.yml index dde21b9c1e..899abd0f0e 100644 --- a/.github/workflows/docs-bump-version.yml +++ b/.github/workflows/docs-bump-version.yml @@ -29,6 +29,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Get current documentation version id: get_docs_version @@ -79,12 +81,14 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Calculate next minor version run: | - MAJOR_VERSION=${{ needs.detect-release-type.outputs.major_version }} - MINOR_VERSION=${{ needs.detect-release-type.outputs.minor_version }} - CURRENT_DOCS_VERSION="${{ needs.detect-release-type.outputs.current_docs_version }}" + MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} + MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} + CURRENT_DOCS_VERSION="${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_DOCS_VERSION}" NEXT_MINOR_VERSION=${MAJOR_VERSION}.$((MINOR_VERSION + 1)).0 echo "CURRENT_DOCS_VERSION=${CURRENT_DOCS_VERSION}" >> "${GITHUB_ENV}" @@ -93,6 +97,10 @@ jobs: echo "Current documentation version: $CURRENT_DOCS_VERSION" echo "Current release version: $PROWLER_VERSION" echo "Next minor version: $NEXT_MINOR_VERSION" + env: + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_DOCS_VERSION: ${{ needs.detect-release-type.outputs.current_docs_version }} - name: Bump versions in documentation for master run: | @@ -132,12 +140,13 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: v${{ needs.detect-release-type.outputs.major_version }}.${{ needs.detect-release-type.outputs.minor_version }} + persist-credentials: false - name: Calculate first patch version run: | - MAJOR_VERSION=${{ needs.detect-release-type.outputs.major_version }} - MINOR_VERSION=${{ needs.detect-release-type.outputs.minor_version }} - CURRENT_DOCS_VERSION="${{ needs.detect-release-type.outputs.current_docs_version }}" + MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} + MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} + CURRENT_DOCS_VERSION="${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_DOCS_VERSION}" FIRST_PATCH_VERSION=${MAJOR_VERSION}.${MINOR_VERSION}.1 VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} @@ -148,6 +157,10 @@ jobs: echo "First patch version: $FIRST_PATCH_VERSION" echo "Version branch: $VERSION_BRANCH" + env: + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_DOCS_VERSION: ${{ needs.detect-release-type.outputs.current_docs_version }} - name: Bump versions in documentation for version branch run: | @@ -193,13 +206,15 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Calculate next patch version run: | - MAJOR_VERSION=${{ needs.detect-release-type.outputs.major_version }} - MINOR_VERSION=${{ needs.detect-release-type.outputs.minor_version }} - PATCH_VERSION=${{ needs.detect-release-type.outputs.patch_version }} - CURRENT_DOCS_VERSION="${{ needs.detect-release-type.outputs.current_docs_version }}" + MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} + MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} + PATCH_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_PATCH_VERSION} + CURRENT_DOCS_VERSION="${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_DOCS_VERSION}" NEXT_PATCH_VERSION=${MAJOR_VERSION}.${MINOR_VERSION}.$((PATCH_VERSION + 1)) VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} @@ -212,6 +227,11 @@ jobs: echo "Current release version: $PROWLER_VERSION" echo "Next patch version: $NEXT_PATCH_VERSION" echo "Target branch: $VERSION_BRANCH" + env: + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_PATCH_VERSION: ${{ needs.detect-release-type.outputs.patch_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_CURRENT_DOCS_VERSION: ${{ needs.detect-release-type.outputs.current_docs_version }} - name: Bump versions in documentation for patch version run: | diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml index c525d8faa4..236765d069 100644 --- a/.github/workflows/find-secrets.yml +++ b/.github/workflows/find-secrets.yml @@ -26,6 +26,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 + persist-credentials: false - name: Scan for secrets with TruffleHog uses: trufflesecurity/trufflehog@ef6e76c3c4023279497fab4721ffa071a722fd05 # v3.92.4 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 1f44080229..ac181ce350 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,6 +1,7 @@ name: 'Tools: PR Labeler' on: + # zizmor: ignore[dangerous-triggers] - intentional: needs write access to apply labels, no PR code checkout pull_request_target: branches: - 'master' diff --git a/.github/workflows/mcp-container-build-push.yml b/.github/workflows/mcp-container-build-push.yml index 130ef90129..f5a677400b 100644 --- a/.github/workflows/mcp-container-build-push.yml +++ b/.github/workflows/mcp-container-build-push.yml @@ -57,6 +57,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Notify container push started id: slack-notification @@ -92,6 +94,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Login to DockerHub uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 @@ -144,30 +148,36 @@ jobs: run: | docker buildx imagetools create \ -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }} \ - -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }} \ - ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64 \ - ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64 + -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA} \ + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-amd64 \ + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-arm64 + env: + NEEDS_SETUP_OUTPUTS_SHORT_SHA: ${{ needs.setup.outputs.short-sha }} - name: Create and push manifests for release event if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' run: | docker buildx imagetools create \ - -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }} \ + -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${RELEASE_TAG} \ -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }} \ - ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64 \ - ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64 + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-amd64 \ + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-arm64 + env: + NEEDS_SETUP_OUTPUTS_SHORT_SHA: ${{ needs.setup.outputs.short-sha }} - name: Install regctl if: always() - uses: regclient/actions/regctl-installer@main + uses: regclient/actions/regctl-installer@da9319db8e44e8b062b3a147e1dfb2f574d41a03 # main - name: Cleanup intermediate architecture tags if: always() run: | echo "Cleaning up intermediate tags..." - regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64" || true - regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64" || true + regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-amd64" || true + regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-arm64" || true echo "Cleanup completed" + env: + NEEDS_SETUP_OUTPUTS_SHORT_SHA: ${{ needs.setup.outputs.short-sha }} notify-release-completed: if: always() && needs.notify-release-started.result == 'success' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch') @@ -177,15 +187,20 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Determine overall outcome id: outcome run: | - if [[ "${{ needs.container-build-push.result }}" == "success" && "${{ needs.create-manifest.result }}" == "success" ]]; then + if [[ "${NEEDS_CONTAINER_BUILD_PUSH_RESULT}" == "success" && "${NEEDS_CREATE_MANIFEST_RESULT}" == "success" ]]; then echo "outcome=success" >> $GITHUB_OUTPUT else echo "outcome=failure" >> $GITHUB_OUTPUT fi + env: + NEEDS_CONTAINER_BUILD_PUSH_RESULT: ${{ needs.container-build-push.result }} + NEEDS_CREATE_MANIFEST_RESULT: ${{ needs.create-manifest.result }} - name: Notify container push completed uses: ./.github/actions/slack-notification diff --git a/.github/workflows/mcp-container-checks.yml b/.github/workflows/mcp-container-checks.yml index 5ea0b6c465..bcd89fce09 100644 --- a/.github/workflows/mcp-container-checks.yml +++ b/.github/workflows/mcp-container-checks.yml @@ -29,6 +29,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check if Dockerfile changed id: dockerfile-changed @@ -63,6 +66,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for MCP changes id: check-changes diff --git a/.github/workflows/mcp-pypi-release.yml b/.github/workflows/mcp-pypi-release.yml index 7a3e5f5daa..31c22114ec 100644 --- a/.github/workflows/mcp-pypi-release.yml +++ b/.github/workflows/mcp-pypi-release.yml @@ -29,7 +29,7 @@ jobs: - name: Parse and validate version id: parse-version run: | - PROWLER_VERSION="${{ env.RELEASE_TAG }}" + PROWLER_VERSION="${RELEASE_TAG}" echo "version=${PROWLER_VERSION}" >> "${GITHUB_OUTPUT}" # Extract major version @@ -61,9 +61,13 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7 + with: + enable-cache: false - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 diff --git a/.github/workflows/pr-check-changelog.yml b/.github/workflows/pr-check-changelog.yml index cc22767552..e70bbcd713 100644 --- a/.github/workflows/pr-check-changelog.yml +++ b/.github/workflows/pr-check-changelog.yml @@ -32,6 +32,8 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Get changed files id: changed-files @@ -50,11 +52,11 @@ jobs: run: | missing_changelogs="" - if [[ "${{ steps.changed-files.outputs.any_changed }}" == "true" ]]; then + if [[ "${STEPS_CHANGED_FILES_OUTPUTS_ANY_CHANGED}" == "true" ]]; then # Check monitored folders for folder in $MONITORED_FOLDERS; do # Get files changed in this folder - changed_in_folder=$(echo "${{ steps.changed-files.outputs.all_changed_files }}" | tr ' ' '\n' | grep "^${folder}/" || true) + changed_in_folder=$(echo "${STEPS_CHANGED_FILES_OUTPUTS_ALL_CHANGED_FILES}" | tr ' ' '\n' | grep "^${folder}/" || true) if [ -n "$changed_in_folder" ]; then echo "Detected changes in ${folder}/" @@ -69,11 +71,11 @@ jobs: # Check root-level dependency files (poetry.lock, pyproject.toml) # These are associated with the prowler folder changelog - root_deps_changed=$(echo "${{ steps.changed-files.outputs.all_changed_files }}" | tr ' ' '\n' | grep -E "^(poetry\.lock|pyproject\.toml)$" || true) + root_deps_changed=$(echo "${STEPS_CHANGED_FILES_OUTPUTS_ALL_CHANGED_FILES}" | tr ' ' '\n' | grep -E "^(poetry\.lock|pyproject\.toml)$" || true) if [ -n "$root_deps_changed" ]; then echo "Detected changes in root dependency files: $root_deps_changed" # Check if prowler/CHANGELOG.md was already updated (might have been caught above) - prowler_changelog_updated=$(echo "${{ steps.changed-files.outputs.all_changed_files }}" | tr ' ' '\n' | grep "^prowler/CHANGELOG.md$" || true) + prowler_changelog_updated=$(echo "${STEPS_CHANGED_FILES_OUTPUTS_ALL_CHANGED_FILES}" | tr ' ' '\n' | grep "^prowler/CHANGELOG.md$" || true) if [ -z "$prowler_changelog_updated" ]; then # Only add if prowler wasn't already flagged if ! echo "$missing_changelogs" | grep -q "prowler"; then @@ -89,6 +91,9 @@ jobs: echo -e "${missing_changelogs}" echo "EOF" } >> $GITHUB_OUTPUT + env: + STEPS_CHANGED_FILES_OUTPUTS_ANY_CHANGED: ${{ steps.changed-files.outputs.any_changed }} + STEPS_CHANGED_FILES_OUTPUTS_ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} - name: Find existing changelog comment if: github.event.pull_request.head.repo.full_name == github.repository diff --git a/.github/workflows/pr-conflict-checker.yml b/.github/workflows/pr-conflict-checker.yml index c812f4ef19..37dc5cc462 100644 --- a/.github/workflows/pr-conflict-checker.yml +++ b/.github/workflows/pr-conflict-checker.yml @@ -1,6 +1,7 @@ name: 'Tools: PR Conflict Checker' on: + # zizmor: ignore[dangerous-triggers] - intentional: needs write access for conflict labels/comments, checkout uses PR head SHA for read-only grep pull_request_target: types: - 'opened' @@ -29,6 +30,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 + persist-credentials: false - name: Get changed files id: changed-files @@ -45,7 +47,7 @@ jobs: HAS_CONFLICTS=false # Check each changed file for conflict markers - for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + for file in ${STEPS_CHANGED_FILES_OUTPUTS_ALL_CHANGED_FILES}; do if [ -f "$file" ]; then echo "Checking file: $file" @@ -70,6 +72,8 @@ jobs: echo "has_conflicts=false" >> $GITHUB_OUTPUT echo "No conflict markers found in changed files" fi + env: + STEPS_CHANGED_FILES_OUTPUTS_ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} - name: Manage conflict label env: diff --git a/.github/workflows/pr-merged.yml b/.github/workflows/pr-merged.yml index 61970827f2..41856b0ede 100644 --- a/.github/workflows/pr-merged.yml +++ b/.github/workflows/pr-merged.yml @@ -1,6 +1,7 @@ name: 'Tools: PR Merged' on: + # zizmor: ignore[dangerous-triggers] - intentional: needs read access to merged PR metadata, no PR code checkout pull_request_target: branches: - 'master' @@ -25,8 +26,10 @@ jobs: - name: Calculate short commit SHA id: vars run: | - SHORT_SHA="${{ github.event.pull_request.merge_commit_sha }}" - echo "SHORT_SHA=${SHORT_SHA::7}" >> $GITHUB_ENV + SHORT_SHA="${GITHUB_EVENT_PULL_REQUEST_MERGE_COMMIT_SHA}" + echo "short_sha=${SHORT_SHA::7}" >> $GITHUB_OUTPUT + env: + GITHUB_EVENT_PULL_REQUEST_MERGE_COMMIT_SHA: ${{ github.event.pull_request.merge_commit_sha }} - name: Trigger Cloud repository pull request uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1 @@ -37,7 +40,7 @@ jobs: client-payload: | { "PROWLER_COMMIT_SHA": "${{ github.event.pull_request.merge_commit_sha }}", - "PROWLER_COMMIT_SHORT_SHA": "${{ env.SHORT_SHA }}", + "PROWLER_COMMIT_SHORT_SHA": "${{ steps.vars.outputs.short_sha }}", "PROWLER_PR_NUMBER": "${{ github.event.pull_request.number }}", "PROWLER_PR_TITLE": ${{ toJson(github.event.pull_request.title) }}, "PROWLER_PR_LABELS": ${{ toJson(github.event.pull_request.labels.*.name) }}, diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 815ade1dec..be2b624baf 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -31,6 +31,7 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} + persist-credentials: false - name: Set up Python uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 diff --git a/.github/workflows/sdk-bump-version.yml b/.github/workflows/sdk-bump-version.yml index 9f25065689..28f4b47ef5 100644 --- a/.github/workflows/sdk-bump-version.yml +++ b/.github/workflows/sdk-bump-version.yml @@ -68,17 +68,22 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Calculate next minor version run: | - MAJOR_VERSION=${{ needs.detect-release-type.outputs.major_version }} - MINOR_VERSION=${{ needs.detect-release-type.outputs.minor_version }} + MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} + MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} NEXT_MINOR_VERSION=${MAJOR_VERSION}.$((MINOR_VERSION + 1)).0 echo "NEXT_MINOR_VERSION=${NEXT_MINOR_VERSION}" >> "${GITHUB_ENV}" echo "Current version: $PROWLER_VERSION" echo "Next minor version: $NEXT_MINOR_VERSION" + env: + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} - name: Bump versions in files for master run: | @@ -113,11 +118,12 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: v${{ needs.detect-release-type.outputs.major_version }}.${{ needs.detect-release-type.outputs.minor_version }} + persist-credentials: false - name: Calculate first patch version run: | - MAJOR_VERSION=${{ needs.detect-release-type.outputs.major_version }} - MINOR_VERSION=${{ needs.detect-release-type.outputs.minor_version }} + MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} + MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} FIRST_PATCH_VERSION=${MAJOR_VERSION}.${MINOR_VERSION}.1 VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} @@ -127,6 +133,9 @@ jobs: echo "First patch version: $FIRST_PATCH_VERSION" echo "Version branch: $VERSION_BRANCH" + env: + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} - name: Bump versions in files for version branch run: | @@ -168,12 +177,14 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Calculate next patch version run: | - MAJOR_VERSION=${{ needs.detect-release-type.outputs.major_version }} - MINOR_VERSION=${{ needs.detect-release-type.outputs.minor_version }} - PATCH_VERSION=${{ needs.detect-release-type.outputs.patch_version }} + MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} + MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} + PATCH_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_PATCH_VERSION} NEXT_PATCH_VERSION=${MAJOR_VERSION}.${MINOR_VERSION}.$((PATCH_VERSION + 1)) VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} @@ -184,6 +195,10 @@ jobs: echo "Current version: $PROWLER_VERSION" echo "Next patch version: $NEXT_PATCH_VERSION" echo "Target branch: $VERSION_BRANCH" + env: + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_PATCH_VERSION: ${{ needs.detect-release-type.outputs.patch_version }} - name: Bump versions in files for version branch run: | diff --git a/.github/workflows/sdk-check-duplicate-test-names.yml b/.github/workflows/sdk-check-duplicate-test-names.yml index 45bd01bc41..1f47c06ea9 100644 --- a/.github/workflows/sdk-check-duplicate-test-names.yml +++ b/.github/workflows/sdk-check-duplicate-test-names.yml @@ -21,6 +21,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Check for duplicate test names across providers run: | diff --git a/.github/workflows/sdk-code-quality.yml b/.github/workflows/sdk-code-quality.yml index 004ca0b7eb..f7066682a3 100644 --- a/.github/workflows/sdk-code-quality.yml +++ b/.github/workflows/sdk-code-quality.yml @@ -32,6 +32,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for SDK changes id: check-changes diff --git a/.github/workflows/sdk-codeql.yml b/.github/workflows/sdk-codeql.yml index a45f633567..138e943879 100644 --- a/.github/workflows/sdk-codeql.yml +++ b/.github/workflows/sdk-codeql.yml @@ -50,6 +50,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Initialize CodeQL uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 diff --git a/.github/workflows/sdk-container-build-push.yml b/.github/workflows/sdk-container-build-push.yml index d510ec8568..b0c00da53e 100644 --- a/.github/workflows/sdk-container-build-push.yml +++ b/.github/workflows/sdk-container-build-push.yml @@ -62,6 +62,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 @@ -116,6 +118,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Notify container push started id: slack-notification @@ -152,6 +156,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Login to DockerHub uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 @@ -214,36 +220,44 @@ jobs: if: github.event_name == 'push' run: | docker buildx imagetools create \ - -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }} \ - -t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }} \ - -t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }} \ - ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-amd64 \ - ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-arm64 + -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_LATEST_TAG} \ + -t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_LATEST_TAG} \ + -t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_LATEST_TAG} \ + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_LATEST_TAG}-amd64 \ + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_LATEST_TAG}-arm64 + env: + NEEDS_SETUP_OUTPUTS_LATEST_TAG: ${{ needs.setup.outputs.latest_tag }} - name: Create and push manifests for release event if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' run: | docker buildx imagetools create \ - -t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.setup.outputs.prowler_version }} \ - -t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.setup.outputs.stable_tag }} \ - -t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.setup.outputs.prowler_version }} \ - -t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.setup.outputs.stable_tag }} \ - -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.prowler_version }} \ - -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.stable_tag }} \ - ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-amd64 \ - ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-arm64 + -t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${NEEDS_SETUP_OUTPUTS_PROWLER_VERSION} \ + -t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${NEEDS_SETUP_OUTPUTS_STABLE_TAG} \ + -t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${NEEDS_SETUP_OUTPUTS_PROWLER_VERSION} \ + -t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${NEEDS_SETUP_OUTPUTS_STABLE_TAG} \ + -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_PROWLER_VERSION} \ + -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_STABLE_TAG} \ + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_LATEST_TAG}-amd64 \ + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_LATEST_TAG}-arm64 + env: + NEEDS_SETUP_OUTPUTS_PROWLER_VERSION: ${{ needs.setup.outputs.prowler_version }} + NEEDS_SETUP_OUTPUTS_STABLE_TAG: ${{ needs.setup.outputs.stable_tag }} + NEEDS_SETUP_OUTPUTS_LATEST_TAG: ${{ needs.setup.outputs.latest_tag }} - name: Install regctl if: always() - uses: regclient/actions/regctl-installer@main + uses: regclient/actions/regctl-installer@da9319db8e44e8b062b3a147e1dfb2f574d41a03 # main - name: Cleanup intermediate architecture tags if: always() run: | echo "Cleaning up intermediate tags..." - regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-amd64" || true - regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-arm64" || true + regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_LATEST_TAG}-amd64" || true + regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_LATEST_TAG}-arm64" || true echo "Cleanup completed" + env: + NEEDS_SETUP_OUTPUTS_LATEST_TAG: ${{ needs.setup.outputs.latest_tag }} notify-release-completed: if: always() && needs.notify-release-started.result == 'success' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch') @@ -253,15 +267,20 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Determine overall outcome id: outcome run: | - if [[ "${{ needs.container-build-push.result }}" == "success" && "${{ needs.create-manifest.result }}" == "success" ]]; then + if [[ "${NEEDS_CONTAINER_BUILD_PUSH_RESULT}" == "success" && "${NEEDS_CREATE_MANIFEST_RESULT}" == "success" ]]; then echo "outcome=success" >> $GITHUB_OUTPUT else echo "outcome=failure" >> $GITHUB_OUTPUT fi + env: + NEEDS_CONTAINER_BUILD_PUSH_RESULT: ${{ needs.container-build-push.result }} + NEEDS_CREATE_MANIFEST_RESULT: ${{ needs.create-manifest.result }} - name: Notify container push completed uses: ./.github/actions/slack-notification diff --git a/.github/workflows/sdk-container-checks.yml b/.github/workflows/sdk-container-checks.yml index 7a0323c216..bbcc93c1b8 100644 --- a/.github/workflows/sdk-container-checks.yml +++ b/.github/workflows/sdk-container-checks.yml @@ -28,6 +28,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check if Dockerfile changed id: dockerfile-changed @@ -63,6 +66,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for SDK changes id: check-changes diff --git a/.github/workflows/sdk-pypi-release.yml b/.github/workflows/sdk-pypi-release.yml index 68a986d37a..369bf582fb 100644 --- a/.github/workflows/sdk-pypi-release.yml +++ b/.github/workflows/sdk-pypi-release.yml @@ -28,7 +28,7 @@ jobs: - name: Parse and validate version id: parse-version run: | - PROWLER_VERSION="${{ env.RELEASE_TAG }}" + PROWLER_VERSION="${RELEASE_TAG}" echo "version=${PROWLER_VERSION}" >> "${GITHUB_OUTPUT}" # Extract major version @@ -60,6 +60,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Install Poetry run: pipx install poetry==2.1.1 @@ -68,7 +70,6 @@ jobs: uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ env.PYTHON_VERSION }} - cache: 'poetry' - name: Build Prowler package run: poetry build @@ -92,6 +93,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Install Poetry run: pipx install poetry==2.1.1 @@ -100,7 +103,6 @@ jobs: uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ env.PYTHON_VERSION }} - cache: 'poetry' - name: Install toml package run: pip install toml diff --git a/.github/workflows/sdk-refresh-aws-services-regions.yml b/.github/workflows/sdk-refresh-aws-services-regions.yml index 25839f2631..49cf139138 100644 --- a/.github/workflows/sdk-refresh-aws-services-regions.yml +++ b/.github/workflows/sdk-refresh-aws-services-regions.yml @@ -28,6 +28,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: 'master' + persist-credentials: false - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 @@ -82,9 +83,14 @@ jobs: - name: PR creation result run: | - if [[ "${{ steps.create-pr.outputs.pull-request-number }}" ]]; then - echo "✓ Pull request #${{ steps.create-pr.outputs.pull-request-number }} created successfully" - echo "URL: ${{ steps.create-pr.outputs.pull-request-url }}" + if [[ "${STEPS_CREATE_PR_OUTPUTS_PULL_REQUEST_NUMBER}" ]]; then + echo "✓ Pull request #${STEPS_CREATE_PR_OUTPUTS_PULL_REQUEST_NUMBER} created successfully" + echo "URL: ${STEPS_CREATE_PR_OUTPUTS_PULL_REQUEST_URL}" else echo "✓ No changes detected - AWS regions are up to date" fi + + env: + STEPS_CREATE_PR_OUTPUTS_PULL_REQUEST_NUMBER: ${{ steps.create-pr.outputs.pull-request-number }} + + STEPS_CREATE_PR_OUTPUTS_PULL_REQUEST_URL: ${{ steps.create-pr.outputs.pull-request-url }} diff --git a/.github/workflows/sdk-refresh-oci-regions.yml b/.github/workflows/sdk-refresh-oci-regions.yml index 35cbf37828..5686491cb2 100644 --- a/.github/workflows/sdk-refresh-oci-regions.yml +++ b/.github/workflows/sdk-refresh-oci-regions.yml @@ -26,6 +26,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: 'master' + persist-credentials: false - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 @@ -85,9 +86,14 @@ jobs: - name: PR creation result run: | - if [[ "${{ steps.create-pr.outputs.pull-request-number }}" ]]; then - echo "✓ Pull request #${{ steps.create-pr.outputs.pull-request-number }} created successfully" - echo "URL: ${{ steps.create-pr.outputs.pull-request-url }}" + if [[ "${STEPS_CREATE_PR_OUTPUTS_PULL_REQUEST_NUMBER}" ]]; then + echo "✓ Pull request #${STEPS_CREATE_PR_OUTPUTS_PULL_REQUEST_NUMBER} created successfully" + echo "URL: ${STEPS_CREATE_PR_OUTPUTS_PULL_REQUEST_URL}" else echo "✓ No changes detected - OCI regions are up to date" fi + + env: + STEPS_CREATE_PR_OUTPUTS_PULL_REQUEST_NUMBER: ${{ steps.create-pr.outputs.pull-request-number }} + + STEPS_CREATE_PR_OUTPUTS_PULL_REQUEST_URL: ${{ steps.create-pr.outputs.pull-request-url }} diff --git a/.github/workflows/sdk-security.yml b/.github/workflows/sdk-security.yml index 01f94d842a..cabdeb50ac 100644 --- a/.github/workflows/sdk-security.yml +++ b/.github/workflows/sdk-security.yml @@ -25,12 +25,15 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for SDK changes id: check-changes uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1 with: - files: + files: ./** .github/workflows/sdk-security.yml files_ignore: | diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index 6800b71896..48da5cc97f 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -32,6 +32,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for SDK changes id: check-changes @@ -119,7 +122,7 @@ jobs: "wafv2": ["cognito", "elbv2"], } - changed_raw = """${{ steps.changed-aws.outputs.all_changed_files }}""" + changed_raw = """${STEPS_CHANGED_AWS_OUTPUTS_ALL_CHANGED_FILES}""" # all_changed_files is space-separated, not newline-separated # Strip leading "./" if present for consistent path handling changed_files = [Path(f.lstrip("./")) for f in changed_raw.split() if f] @@ -174,20 +177,25 @@ jobs: else: print("AWS service test paths: none detected") PY + env: + STEPS_CHANGED_AWS_OUTPUTS_ALL_CHANGED_FILES: ${{ steps.changed-aws.outputs.all_changed_files }} - name: Run AWS tests if: steps.changed-aws.outputs.any_changed == 'true' run: | - echo "AWS run_all=${{ steps.aws-services.outputs.run_all }}" - echo "AWS service_paths='${{ steps.aws-services.outputs.service_paths }}'" + echo "AWS run_all=${STEPS_AWS_SERVICES_OUTPUTS_RUN_ALL}" + echo "AWS service_paths='${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS}'" - if [ "${{ steps.aws-services.outputs.run_all }}" = "true" ]; then + if [ "${STEPS_AWS_SERVICES_OUTPUTS_RUN_ALL}" = "true" ]; then poetry run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml tests/providers/aws - elif [ -z "${{ steps.aws-services.outputs.service_paths }}" ]; then + elif [ -z "${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS}" ]; then echo "No AWS service paths detected; skipping AWS tests." else - poetry run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml ${{ steps.aws-services.outputs.service_paths }} + poetry run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml ${STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS} fi + env: + STEPS_AWS_SERVICES_OUTPUTS_RUN_ALL: ${{ steps.aws-services.outputs.run_all }} + STEPS_AWS_SERVICES_OUTPUTS_SERVICE_PATHS: ${{ steps.aws-services.outputs.service_paths }} - name: Upload AWS coverage to Codecov if: steps.changed-aws.outputs.any_changed == 'true' diff --git a/.github/workflows/test-impact-analysis.yml b/.github/workflows/test-impact-analysis.yml index 7f750ede2b..8fc3224e5f 100644 --- a/.github/workflows/test-impact-analysis.yml +++ b/.github/workflows/test-impact-analysis.yml @@ -49,6 +49,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Get changed files id: changed-files @@ -66,47 +69,60 @@ jobs: id: impact run: | echo "Changed files:" - echo "${{ steps.changed-files.outputs.all_changed_files }}" | tr ' ' '\n' + echo "${STEPS_CHANGED_FILES_OUTPUTS_ALL_CHANGED_FILES}" | tr ' ' '\n' echo "" - python .github/scripts/test-impact.py ${{ steps.changed-files.outputs.all_changed_files }} + python .github/scripts/test-impact.py ${STEPS_CHANGED_FILES_OUTPUTS_ALL_CHANGED_FILES} + env: + STEPS_CHANGED_FILES_OUTPUTS_ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} - name: Set convenience flags id: set-flags run: | - if [[ -n "${{ steps.impact.outputs.sdk-tests }}" ]]; then + if [[ -n "${STEPS_IMPACT_OUTPUTS_SDK_TESTS}" ]]; then echo "has-sdk-tests=true" >> $GITHUB_OUTPUT else echo "has-sdk-tests=false" >> $GITHUB_OUTPUT fi - - if [[ -n "${{ steps.impact.outputs.api-tests }}" ]]; then + + if [[ -n "${STEPS_IMPACT_OUTPUTS_API_TESTS}" ]]; then echo "has-api-tests=true" >> $GITHUB_OUTPUT else echo "has-api-tests=false" >> $GITHUB_OUTPUT fi - - if [[ -n "${{ steps.impact.outputs.ui-e2e }}" ]]; then + + if [[ -n "${STEPS_IMPACT_OUTPUTS_UI_E2E}" ]]; then echo "has-ui-e2e=true" >> $GITHUB_OUTPUT else echo "has-ui-e2e=false" >> $GITHUB_OUTPUT fi + env: + STEPS_IMPACT_OUTPUTS_SDK_TESTS: ${{ steps.impact.outputs.sdk-tests }} + STEPS_IMPACT_OUTPUTS_API_TESTS: ${{ steps.impact.outputs.api-tests }} + STEPS_IMPACT_OUTPUTS_UI_E2E: ${{ steps.impact.outputs.ui-e2e }} - name: Summary run: | echo "## Test Impact Analysis" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - - if [[ "${{ steps.impact.outputs.run-all }}" == "true" ]]; then + + if [[ "${STEPS_IMPACT_OUTPUTS_RUN_ALL}" == "true" ]]; then echo "🚨 **Critical path changed - running ALL tests**" >> $GITHUB_STEP_SUMMARY else echo "### Affected Modules" >> $GITHUB_STEP_SUMMARY - echo "\`${{ steps.impact.outputs.modules }}\`" >> $GITHUB_STEP_SUMMARY + echo "\`${STEPS_IMPACT_OUTPUTS_MODULES}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - + echo "### Tests to Run" >> $GITHUB_STEP_SUMMARY echo "| Category | Paths |" >> $GITHUB_STEP_SUMMARY echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| SDK Tests | \`${{ steps.impact.outputs.sdk-tests || 'none' }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| API Tests | \`${{ steps.impact.outputs.api-tests || 'none' }}\` |" >> $GITHUB_STEP_SUMMARY - echo "| UI E2E | \`${{ steps.impact.outputs.ui-e2e || 'none' }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SDK Tests | \`${STEPS_IMPACT_OUTPUTS_SDK_TESTS:-none}\` |" >> $GITHUB_STEP_SUMMARY + echo "| API Tests | \`${STEPS_IMPACT_OUTPUTS_API_TESTS:-none}\` |" >> $GITHUB_STEP_SUMMARY + echo "| UI E2E | \`${STEPS_IMPACT_OUTPUTS_UI_E2E:-none}\` |" >> $GITHUB_STEP_SUMMARY fi + + env: + STEPS_IMPACT_OUTPUTS_RUN_ALL: ${{ steps.impact.outputs.run-all }} + STEPS_IMPACT_OUTPUTS_SDK_TESTS: ${{ steps.impact.outputs.sdk-tests }} + STEPS_IMPACT_OUTPUTS_API_TESTS: ${{ steps.impact.outputs.api-tests }} + STEPS_IMPACT_OUTPUTS_UI_E2E: ${{ steps.impact.outputs.ui-e2e }} + STEPS_IMPACT_OUTPUTS_MODULES: ${{ steps.impact.outputs.modules }} diff --git a/.github/workflows/ui-bump-version.yml b/.github/workflows/ui-bump-version.yml index 7c576eed55..f91fb5db9b 100644 --- a/.github/workflows/ui-bump-version.yml +++ b/.github/workflows/ui-bump-version.yml @@ -68,17 +68,22 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Calculate next minor version run: | - MAJOR_VERSION=${{ needs.detect-release-type.outputs.major_version }} - MINOR_VERSION=${{ needs.detect-release-type.outputs.minor_version }} + MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} + MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} NEXT_MINOR_VERSION=${MAJOR_VERSION}.$((MINOR_VERSION + 1)).0 echo "NEXT_MINOR_VERSION=${NEXT_MINOR_VERSION}" >> "${GITHUB_ENV}" echo "Current version: $PROWLER_VERSION" echo "Next minor version: $NEXT_MINOR_VERSION" + env: + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} - name: Bump UI version in .env for master run: | @@ -115,11 +120,12 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: v${{ needs.detect-release-type.outputs.major_version }}.${{ needs.detect-release-type.outputs.minor_version }} + persist-credentials: false - name: Calculate first patch version run: | - MAJOR_VERSION=${{ needs.detect-release-type.outputs.major_version }} - MINOR_VERSION=${{ needs.detect-release-type.outputs.minor_version }} + MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} + MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} FIRST_PATCH_VERSION=${MAJOR_VERSION}.${MINOR_VERSION}.1 VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} @@ -129,6 +135,9 @@ jobs: echo "First patch version: $FIRST_PATCH_VERSION" echo "Version branch: $VERSION_BRANCH" + env: + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} - name: Bump UI version in .env for version branch run: | @@ -172,12 +181,14 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Calculate next patch version run: | - MAJOR_VERSION=${{ needs.detect-release-type.outputs.major_version }} - MINOR_VERSION=${{ needs.detect-release-type.outputs.minor_version }} - PATCH_VERSION=${{ needs.detect-release-type.outputs.patch_version }} + MAJOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION} + MINOR_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION} + PATCH_VERSION=${NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_PATCH_VERSION} NEXT_PATCH_VERSION=${MAJOR_VERSION}.${MINOR_VERSION}.$((PATCH_VERSION + 1)) VERSION_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} @@ -188,6 +199,10 @@ jobs: echo "Current version: $PROWLER_VERSION" echo "Next patch version: $NEXT_PATCH_VERSION" echo "Target branch: $VERSION_BRANCH" + env: + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MAJOR_VERSION: ${{ needs.detect-release-type.outputs.major_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_MINOR_VERSION: ${{ needs.detect-release-type.outputs.minor_version }} + NEEDS_DETECT_RELEASE_TYPE_OUTPUTS_PATCH_VERSION: ${{ needs.detect-release-type.outputs.patch_version }} - name: Bump UI version in .env for version branch run: | diff --git a/.github/workflows/ui-codeql.yml b/.github/workflows/ui-codeql.yml index 581e798189..fafa430baf 100644 --- a/.github/workflows/ui-codeql.yml +++ b/.github/workflows/ui-codeql.yml @@ -46,6 +46,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Initialize CodeQL uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 diff --git a/.github/workflows/ui-container-build-push.yml b/.github/workflows/ui-container-build-push.yml index 6f8c232185..81d4aa69cb 100644 --- a/.github/workflows/ui-container-build-push.yml +++ b/.github/workflows/ui-container-build-push.yml @@ -60,6 +60,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Notify container push started id: slack-notification @@ -96,6 +98,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Login to DockerHub uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 @@ -143,30 +147,36 @@ jobs: run: | docker buildx imagetools create \ -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }} \ - -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }} \ - ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64 \ - ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64 + -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA} \ + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-amd64 \ + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-arm64 + env: + NEEDS_SETUP_OUTPUTS_SHORT_SHA: ${{ needs.setup.outputs.short-sha }} - name: Create and push manifests for release event if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' run: | docker buildx imagetools create \ - -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }} \ + -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${RELEASE_TAG} \ -t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }} \ - ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64 \ - ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64 + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-amd64 \ + ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-arm64 + env: + NEEDS_SETUP_OUTPUTS_SHORT_SHA: ${{ needs.setup.outputs.short-sha }} - name: Install regctl if: always() - uses: regclient/actions/regctl-installer@main + uses: regclient/actions/regctl-installer@da9319db8e44e8b062b3a147e1dfb2f574d41a03 # main - name: Cleanup intermediate architecture tags if: always() run: | echo "Cleaning up intermediate tags..." - regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-amd64" || true - regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64" || true + regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-amd64" || true + regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${NEEDS_SETUP_OUTPUTS_SHORT_SHA}-arm64" || true echo "Cleanup completed" + env: + NEEDS_SETUP_OUTPUTS_SHORT_SHA: ${{ needs.setup.outputs.short-sha }} notify-release-completed: if: always() && needs.notify-release-started.result == 'success' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch') @@ -176,15 +186,20 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Determine overall outcome id: outcome run: | - if [[ "${{ needs.container-build-push.result }}" == "success" && "${{ needs.create-manifest.result }}" == "success" ]]; then + if [[ "${NEEDS_CONTAINER_BUILD_PUSH_RESULT}" == "success" && "${NEEDS_CREATE_MANIFEST_RESULT}" == "success" ]]; then echo "outcome=success" >> $GITHUB_OUTPUT else echo "outcome=failure" >> $GITHUB_OUTPUT fi + env: + NEEDS_CONTAINER_BUILD_PUSH_RESULT: ${{ needs.container-build-push.result }} + NEEDS_CREATE_MANIFEST_RESULT: ${{ needs.create-manifest.result }} - name: Notify container push completed uses: ./.github/actions/slack-notification diff --git a/.github/workflows/ui-container-checks.yml b/.github/workflows/ui-container-checks.yml index 4c027f1cb2..d986c14b1b 100644 --- a/.github/workflows/ui-container-checks.yml +++ b/.github/workflows/ui-container-checks.yml @@ -29,6 +29,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check if Dockerfile changed id: dockerfile-changed @@ -64,6 +67,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for UI changes id: check-changes diff --git a/.github/workflows/ui-e2e-tests-v2.yml b/.github/workflows/ui-e2e-tests-v2.yml index 9936737320..0e8a146b26 100644 --- a/.github/workflows/ui-e2e-tests-v2.yml +++ b/.github/workflows/ui-e2e-tests-v2.yml @@ -15,6 +15,9 @@ on: - 'ui/**' - 'api/**' # API changes can affect UI E2E +permissions: + contents: read + jobs: # First, analyze which tests need to run impact-analysis: @@ -76,20 +79,24 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Show test scope run: | echo "## E2E Test Scope" >> $GITHUB_STEP_SUMMARY - if [[ "${{ env.RUN_ALL_TESTS }}" == "true" ]]; then + if [[ "${RUN_ALL_TESTS}" == "true" ]]; then echo "Running **ALL** E2E tests (critical path changed)" >> $GITHUB_STEP_SUMMARY else - echo "Running tests matching: \`${{ env.E2E_TEST_PATHS }}\`" >> $GITHUB_STEP_SUMMARY + echo "Running tests matching: \`${E2E_TEST_PATHS}\`" >> $GITHUB_STEP_SUMMARY fi echo "" - echo "Affected modules: \`${{ needs.impact-analysis.outputs.modules }}\`" >> $GITHUB_STEP_SUMMARY + echo "Affected modules: \`${NEEDS_IMPACT_ANALYSIS_OUTPUTS_MODULES}\`" >> $GITHUB_STEP_SUMMARY + env: + NEEDS_IMPACT_ANALYSIS_OUTPUTS_MODULES: ${{ needs.impact-analysis.outputs.modules }} - name: Create k8s Kind Cluster - uses: helm/kind-action@v1 + uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1 with: cluster_name: kind @@ -150,7 +157,7 @@ jobs: node-version: '24.13.0' - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 with: version: 10 run_install: false @@ -195,14 +202,14 @@ jobs: - name: Run E2E tests working-directory: ./ui run: | - if [[ "${{ env.RUN_ALL_TESTS }}" == "true" ]]; then + if [[ "${RUN_ALL_TESTS}" == "true" ]]; then echo "Running ALL E2E tests..." pnpm run test:e2e else - echo "Running targeted E2E tests: ${{ env.E2E_TEST_PATHS }}" + echo "Running targeted E2E tests: ${E2E_TEST_PATHS}" # Convert glob patterns to playwright test paths # e.g., "ui/tests/providers/**" -> "tests/providers" - TEST_PATHS="${{ env.E2E_TEST_PATHS }}" + TEST_PATHS="${E2E_TEST_PATHS}" # Remove ui/ prefix and convert ** to empty (playwright handles recursion) TEST_PATHS=$(echo "$TEST_PATHS" | sed 's|ui/||g' | sed 's|\*\*||g' | tr ' ' '\n' | sort -u) # Drop auth setup helpers (not runnable test suites) @@ -244,6 +251,8 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "No UI E2E tests needed for this change." >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "Affected modules: \`${{ needs.impact-analysis.outputs.modules }}\`" >> $GITHUB_STEP_SUMMARY + echo "Affected modules: \`${NEEDS_IMPACT_ANALYSIS_OUTPUTS_MODULES}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "To run all tests, modify a file in a critical path (e.g., \`ui/lib/**\`)." >> $GITHUB_STEP_SUMMARY + env: + NEEDS_IMPACT_ANALYSIS_OUTPUTS_MODULES: ${{ needs.impact-analysis.outputs.modules }} diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index cc1a318277..1476d38c19 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -31,6 +31,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + # zizmor: ignore[artipacked] + persist-credentials: true # Required by tj-actions/changed-files to fetch PR branch - name: Check for UI changes id: check-changes @@ -81,7 +84,7 @@ jobs: - name: Setup pnpm if: steps.check-changes.outputs.any_changed == 'true' - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 with: version: 10 run_install: false @@ -122,10 +125,12 @@ jobs: if: steps.check-changes.outputs.any_changed == 'true' && steps.critical-changes.outputs.any_changed != 'true' && steps.changed-source.outputs.all_changed_files != '' run: | echo "Running tests related to changed files:" - echo "${{ steps.changed-source.outputs.all_changed_files }}" + echo "${STEPS_CHANGED_SOURCE_OUTPUTS_ALL_CHANGED_FILES}" # Convert space-separated to vitest related format (remove ui/ prefix for relative paths) - CHANGED_FILES=$(echo "${{ steps.changed-source.outputs.all_changed_files }}" | tr ' ' '\n' | sed 's|^ui/||' | tr '\n' ' ') + CHANGED_FILES=$(echo "${STEPS_CHANGED_SOURCE_OUTPUTS_ALL_CHANGED_FILES}" | tr ' ' '\n' | sed 's|^ui/||' | tr '\n' ' ') pnpm exec vitest related $CHANGED_FILES --run + env: + STEPS_CHANGED_SOURCE_OUTPUTS_ALL_CHANGED_FILES: ${{ steps.changed-source.outputs.all_changed_files }} - name: Run unit tests (test files only changed) if: steps.check-changes.outputs.any_changed == 'true' && steps.critical-changes.outputs.any_changed != 'true' && steps.changed-source.outputs.all_changed_files == '' diff --git a/README.md b/README.md index 9e5be11c53..3d1589f6cd 100644 --- a/README.md +++ b/README.md @@ -148,21 +148,17 @@ Prowler App offers flexible installation methods tailored to various environment **Commands** ``` console -curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/docker-compose.yml -curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/.env +VERSION=$(curl -s https://api.github.com/repos/prowler-cloud/prowler/releases/latest | jq -r .tag_name) +curl -sLO "https://raw.githubusercontent.com/prowler-cloud/prowler/refs/tags/${VERSION}/docker-compose.yml" +# Environment variables can be customized in the .env file. Using default values in production environments is not recommended. +curl -sLO "https://raw.githubusercontent.com/prowler-cloud/prowler/refs/tags/${VERSION}/.env" docker compose up -d ``` -> Containers are built for `linux/amd64`. +> [!WARNING] +> 🔒 For a secure setup, the API auto-generates a unique key pair, `DJANGO_TOKEN_SIGNING_KEY` and `DJANGO_TOKEN_VERIFYING_KEY`, and stores it in `~/.config/prowler-api` (non-container) or the bound Docker volume in `_data/api` (container). Never commit or reuse static/default keys. To rotate keys, delete the stored key files and restart the API. -### Configuring Your Workstation for Prowler App - -If your workstation's architecture is incompatible, you can resolve this by: - -- **Setting the environment variable**: `DOCKER_DEFAULT_PLATFORM=linux/amd64` -- **Using the following flag in your Docker command**: `--platform linux/amd64` - -> Once configured, access the Prowler App at http://localhost:3000. Sign up using your email and password to get started. +Once configured, access the Prowler App at http://localhost:3000. Sign up using your email and password to get started. ### Common Issues with Docker Pull Installation diff --git a/docs/docs.json b/docs/docs.json index c89ac2c3c0..8f18a85834 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -117,6 +117,13 @@ "user-guide/tutorials/prowler-app-jira-integration" ] }, + { + "group": "AWS Organizations", + "expanded": true, + "pages": [ + "user-guide/tutorials/prowler-cloud-aws-organizations" + ] + }, { "group": "Lighthouse AI", "pages": [ diff --git a/docs/getting-started/installation/prowler-app.mdx b/docs/getting-started/installation/prowler-app.mdx index 8b5382f156..a4eb883438 100644 --- a/docs/getting-started/installation/prowler-app.mdx +++ b/docs/getting-started/installation/prowler-app.mdx @@ -23,9 +23,15 @@ Refer to the [Prowler App Tutorial](/user-guide/tutorials/prowler-app) for detai ```bash VERSION=$(curl -s https://api.github.com/repos/prowler-cloud/prowler/releases/latest | jq -r .tag_name) curl -sLO "https://raw.githubusercontent.com/prowler-cloud/prowler/refs/tags/${VERSION}/docker-compose.yml" + # Environment variables can be customized in the .env file. Using default values in production environments is not recommended. curl -sLO "https://raw.githubusercontent.com/prowler-cloud/prowler/refs/tags/${VERSION}/.env" docker compose up -d ``` + + + For a secure setup, the API auto-generates a unique key pair, `DJANGO_TOKEN_SIGNING_KEY` and `DJANGO_TOKEN_VERIFYING_KEY`, and stores it in `~/.config/prowler-api` (non-container) or the bound Docker volume in `_data/api` (container). Never commit or reuse static/default keys. To rotate keys, delete the stored key files and restart the API. + + _Requirements_: diff --git a/docs/images/organizations/authentication-details.png b/docs/images/organizations/authentication-details.png new file mode 100644 index 0000000000..2b4ae782cf Binary files /dev/null and b/docs/images/organizations/authentication-details.png differ diff --git a/docs/images/organizations/aws-console-org-id.png b/docs/images/organizations/aws-console-org-id.png new file mode 100644 index 0000000000..9e4c726f84 Binary files /dev/null and b/docs/images/organizations/aws-console-org-id.png differ diff --git a/docs/images/organizations/cloud-providers-add.png b/docs/images/organizations/cloud-providers-add.png new file mode 100644 index 0000000000..21d0fadff3 Binary files /dev/null and b/docs/images/organizations/cloud-providers-add.png differ diff --git a/docs/images/organizations/connection-failures-skip.png b/docs/images/organizations/connection-failures-skip.png new file mode 100644 index 0000000000..387f868b24 Binary files /dev/null and b/docs/images/organizations/connection-failures-skip.png differ diff --git a/docs/images/organizations/launch-scan.png b/docs/images/organizations/launch-scan.png new file mode 100644 index 0000000000..564c7674a1 Binary files /dev/null and b/docs/images/organizations/launch-scan.png differ diff --git a/docs/images/organizations/onboarding-flow.png b/docs/images/organizations/onboarding-flow.png new file mode 100644 index 0000000000..8a11df38fd Binary files /dev/null and b/docs/images/organizations/onboarding-flow.png differ diff --git a/docs/images/organizations/onboarding-flow.svg b/docs/images/organizations/onboarding-flow.svg new file mode 100644 index 0000000000..15036e9765 --- /dev/null +++ b/docs/images/organizations/onboarding-flow.svg @@ -0,0 +1,71 @@ + + + + + + + + + Onboarding Flow + + + + + 1 + Create Management + Account Role + + Manually in IAM + Allows Prowler to + discover your org + structure + + + + + + + + + + + + + 2 + Deploy StackSet + + In AWS Console + Creates ProwlerScan + role in every + member account + + + + + + + + 3 + Run the Wizard + + In Prowler Cloud + Discovers accounts, + tests connections + + + + + + + + 4 + Launch Scans + + Automatic + Scans run on all + connected accounts + on your schedule + + + Steps 1 and 2 are done once in AWS | Steps 3 and 4 are done in Prowler Cloud + diff --git a/docs/images/organizations/organization-details-form.png b/docs/images/organizations/organization-details-form.png new file mode 100644 index 0000000000..41b574f4c3 Binary files /dev/null and b/docs/images/organizations/organization-details-form.png differ diff --git a/docs/images/organizations/role-arn-field.png b/docs/images/organizations/role-arn-field.png new file mode 100644 index 0000000000..3f6832bfa4 Binary files /dev/null and b/docs/images/organizations/role-arn-field.png differ diff --git a/docs/images/organizations/select-aws-provider.png b/docs/images/organizations/select-aws-provider.png new file mode 100644 index 0000000000..7b47b4b400 Binary files /dev/null and b/docs/images/organizations/select-aws-provider.png differ diff --git a/docs/images/organizations/select-organizations-method.png b/docs/images/organizations/select-organizations-method.png new file mode 100644 index 0000000000..f4c4aa7c8f Binary files /dev/null and b/docs/images/organizations/select-organizations-method.png differ diff --git a/docs/images/organizations/test-connections-button.png b/docs/images/organizations/test-connections-button.png new file mode 100644 index 0000000000..bcf8fb682e Binary files /dev/null and b/docs/images/organizations/test-connections-button.png differ diff --git a/docs/images/organizations/test-connections.png b/docs/images/organizations/test-connections.png new file mode 100644 index 0000000000..ccda58c8fb Binary files /dev/null and b/docs/images/organizations/test-connections.png differ diff --git a/docs/images/organizations/tree-view-accounts.png b/docs/images/organizations/tree-view-accounts.png new file mode 100644 index 0000000000..33a550e4f3 Binary files /dev/null and b/docs/images/organizations/tree-view-accounts.png differ diff --git a/docs/images/organizations/two-roles-architecture.png b/docs/images/organizations/two-roles-architecture.png new file mode 100644 index 0000000000..5205174264 Binary files /dev/null and b/docs/images/organizations/two-roles-architecture.png differ diff --git a/docs/images/organizations/two-roles-architecture.svg b/docs/images/organizations/two-roles-architecture.svg new file mode 100644 index 0000000000..4005466a63 --- /dev/null +++ b/docs/images/organizations/two-roles-architecture.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + Two Roles Architecture + + + + + + + Management Account + + + + + Management Role + + + Purpose: + Discover Organization structure + scan management account + + + Permissions: + SecurityAudit (AWS managed policy) + ViewOnlyAccess (AWS managed policy) + Additional read-only (inline policy) + organizations:DescribeAccount + organizations:DescribeOrganization + organizations:ListAccounts + organizations:ListAccountsForParent + organizations:ListOrganizationalUnitsForParent + organizations:ListRoots + organizations:ListTagsForResource + + + + Deploy: MANUALLY in IAM Console + + + + Prowler Cloud + + + + + + + + + + + Member Accounts + + + + + ProwlerScan Role (per account) + + + Purpose: + Security scanning of each account + + + Permissions: + SecurityAudit (AWS managed policy) + ViewOnlyAccess (AWS managed policy) + Additional read-only (inline policy) + + + Scope: + Read-only access across all AWS services + No write or modify permissions + + + + Deploy: via CloudFormation StackSet + + + Prowler discovers + your org structure + Prowler scans each + account for findings + diff --git a/docs/reo.js b/docs/reo.js new file mode 100644 index 0000000000..dfc026aa44 --- /dev/null +++ b/docs/reo.js @@ -0,0 +1,2 @@ +// Reo tracking beacon +!function(){var e,t,n;e="1fca1c3c1571b22",t=function(){Reo.init({clientID:"1fca1c3c1571b22"})},(n=document.createElement("script")).src="https://static.reo.dev/"+e+"/reo.js",n.defer=!0,n.onload=t,document.head.appendChild(n)}(); diff --git a/docs/user-guide/providers/aws/organizations.mdx b/docs/user-guide/providers/aws/organizations.mdx index 54be28c171..bbdedef50b 100644 --- a/docs/user-guide/providers/aws/organizations.mdx +++ b/docs/user-guide/providers/aws/organizations.mdx @@ -2,6 +2,12 @@ title: 'AWS Organizations in Prowler' --- + +**Using Prowler Cloud?** You can onboard your entire AWS Organization through the UI with automatic account discovery, OU-aware tree selection, and bulk connection testing — no scripts or YAML files required. + +See [AWS Organizations in Prowler Cloud](/user-guide/tutorials/prowler-cloud-aws-organizations) for the full walkthrough. + + Prowler can integrate with AWS Organizations to manage the visibility and onboarding of accounts centrally. When trusted access is enabled with the Organization, Prowler can discover accounts as they are created and even automate deployment of the Prowler Scan IAM Role. diff --git a/docs/user-guide/tutorials/aws-organizations-bulk-provisioning.mdx b/docs/user-guide/tutorials/aws-organizations-bulk-provisioning.mdx index e19d416d32..88a084dd17 100644 --- a/docs/user-guide/tutorials/aws-organizations-bulk-provisioning.mdx +++ b/docs/user-guide/tutorials/aws-organizations-bulk-provisioning.mdx @@ -7,9 +7,11 @@ Prowler offers an automated tool to discover and provision all AWS accounts with The tool, `aws_org_generator.py`‎, complements the [Bulk Provider Provisioning](./bulk-provider-provisioning) tool and is available in the Prowler repository at: [util/prowler-bulk-provisioning](https://github.com/prowler-cloud/prowler/tree/master/util/prowler-bulk-provisioning) -Native support for bulk provisioning AWS Organizations and similar multi-account structures directly in the Prowler UI/API is on the official roadmap. +**Native AWS Organizations support is now available in Prowler Cloud.** You can onboard all accounts via the UI wizard — with automatic discovery, hierarchical tree selection, connection testing, and bulk scan launch — without any scripts or YAML files. -Track progress and vote for this feature at: [Bulk Provisioning in the UI/API for AWS Organizations](https://roadmap.prowler.com/p/builk-provisioning-in-the-uiapi-for-aws-organizations-and-alike) +See [AWS Organizations in Prowler Cloud](/user-guide/tutorials/prowler-cloud-aws-organizations). + +The CLI-based tool below remains useful for self-hosted Prowler App and advanced automation scenarios. {/* TODO: Add screenshot of the tool in action */} diff --git a/docs/user-guide/tutorials/prowler-cloud-aws-organizations.mdx b/docs/user-guide/tutorials/prowler-cloud-aws-organizations.mdx new file mode 100644 index 0000000000..fba2d98f6c --- /dev/null +++ b/docs/user-guide/tutorials/prowler-cloud-aws-organizations.mdx @@ -0,0 +1,545 @@ +--- +title: 'AWS Organizations in Prowler Cloud' +description: 'Onboard all AWS accounts in your Organization through a single guided wizard' +--- + +import { VersionBadge } from "/snippets/version-badge.mdx" + + + +Prowler Cloud enables you to onboard all AWS accounts in your Organization through a single guided wizard. Instead of connecting accounts one by one, you can discover every account in your AWS Organization, select the ones you want to monitor, test connectivity, and launch scans — all from the Prowler Cloud UI. + + +This feature is **exclusively available in Prowler Cloud**. For CLI-based multi-account scanning, see [AWS Organizations in Prowler CLI](/user-guide/providers/aws/organizations). + + +## Overview + +### Individual Accounts vs Organizations + +| Approach | Best for | How it works | +|----------|----------|--------------| +| **Individual accounts** | A few AWS accounts | Connect each account one by one with its own IAM role. | +| **AWS Organizations** | 10+ accounts, or any org-managed environment | Connect once to your management account, discover all member accounts automatically, and scan them in bulk. | + +### How it works + +Before using the AWS Organizations wizard, you need to deploy **two IAM roles** in your AWS environment. The onboarding follows this sequence: + + + Onboarding flow: 1. Create Management Account Role, 2. Deploy StackSet, 3. Run the Wizard, 4. Launch Scans + + +## Key Concepts + +### What is an External ID? + +An **External ID** is a security token that Prowler generates unique to your tenant. When Prowler assumes the IAM role in your AWS account, it presents this External ID to prove its identity. + +This prevents the [confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html) — a scenario where an unauthorized party could trick AWS into granting access to your account. By requiring the External ID, only your specific Prowler tenant can assume the role. + +You don't need to create the External ID yourself — Prowler generates it automatically and displays it in the wizard for you to copy. + +### Two Roles Architecture + +Prowler requires **two separate IAM roles** deployed in different places, each with a distinct purpose: + +| Role | Where it lives | What it does | How to deploy it | +|------|---------------|--------------|------------------| +| **ProwlerScan** (management account) | Your management (root) account only | Discovers the Organization structure **and** scans the management account. Has additional Organizations discovery permissions. | **Manually** in the IAM Console ([Step 1](#step-1-create-the-management-account-role)). Cannot be deployed via StackSet. | +| **ProwlerScan** (member accounts) | Every member account | Scans the account for security findings. | Via **CloudFormation StackSet** ([Step 2](#step-2-deploy-the-cloudformation-stackset)). Automated across all accounts. | + + + Two Roles Architecture: ProwlerScan in management account (discovery + scanning) and ProwlerScan in member accounts (scanning only) + + + +**Same name, different permissions.** Both roles are named `ProwlerScan` — Prowler expects a consistent role name across all accounts. The management account role has the same scanning permissions as member accounts, plus additional Organizations discovery permissions (see [Step 1](#step-1-create-the-management-account-role) for the full list). + + +### What is a CloudFormation StackSet? + +A [CloudFormation StackSet](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html) lets you deploy the same CloudFormation template across multiple AWS accounts in a single operation. Prowler uses a StackSet to deploy the **ProwlerScan** IAM role into every member account of your organization, so you don't have to create the role manually in each account. + +## Prerequisites + +### Prowler Cloud Account + +You need an active [Prowler Cloud](https://cloud.prowler.com) account. Each AWS account you connect will count as a provider in your subscription. See [Billing Impact](#billing-impact) for details. + +### AWS Organization Enabled + +Your AWS environment must have [AWS Organizations](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_introduction.html) enabled. You will need access to the **management account** (or a delegated administrator account) to provide the Organization ID and IAM Role ARN. + +## Step 1: Create the Management Account Role + +The first role you need to create is the **management account role**. This role allows Prowler to discover your Organization structure — listing accounts, OUs, and hierarchy. + + +**This role must be created manually.** Organizational CloudFormation StackSets do not deploy to the management account itself — this is an AWS limitation, not a Prowler one. StackSets with service-managed permissions only target member accounts. Similarly, the Prowler Quick Create link only deploys the role to member accounts. + + + +**The role must be named `ProwlerScan`** — the same name as the role deployed to member accounts via StackSet. Prowler expects a consistent role name across all accounts in the Organization. If you use a different name, connection tests and scans will fail for the management account. + + +### Create the IAM Role + +1. Sign in to the [AWS IAM Console](https://console.aws.amazon.com/iam/) in your **management account**. + +2. Go to **Roles > Create role** and select **Custom trust policy**. + +3. Paste the following trust policy. This allows Prowler Cloud to assume the role using your tenant's External ID (you will get this from the Prowler wizard in [Step 3](#step-3-start-the-organization-wizard)): + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::232136659152:root" + }, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "" + }, + "StringLike": { + "aws:PrincipalArn": "arn:aws:iam::232136659152:role/prowler*" + } + } + } + ] +} +``` + +Replace `` with the External ID shown in the Prowler wizard. + +4. Attach the following AWS managed policies: + - **SecurityAudit** + - **ViewOnlyAccess** + + This allows Prowler to also scan the management account for security findings, just like any other account. + +5. Create an additional inline policy with the following permissions. These are specific to the management account and allow Prowler to discover your Organization structure: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ProwlerOrganizationDiscovery", + "Effect": "Allow", + "Action": [ + "organizations:DescribeAccount", + "organizations:DescribeOrganization", + "organizations:ListAccounts", + "organizations:ListAccountsForParent", + "organizations:ListOrganizationalUnitsForParent", + "organizations:ListRoots", + "organizations:ListTagsForResource" + ], + "Resource": "*" + }, + { + "Sid": "ProwlerStackSetManagement", + "Effect": "Allow", + "Action": [ + "organizations:RegisterDelegatedAdministrator", + "iam:CreateServiceLinkedRole" + ], + "Resource": "*" + } + ] +} +``` + + +You can optionally restrict the `Resource` field to your specific Organization ARN (e.g., `arn:aws:organizations::123456789012:organization/o-abc123def4`) instead of `"*"` to minimize the blast radius. + + +6. Name the role **`ProwlerScan`** and click **Create role**. Take note of the **Role ARN** — you will need it in the Prowler wizard. + +The ARN follows this format: `arn:aws:iam:::role/ProwlerScan` + + +The role **must** be named `ProwlerScan`. Do not use a different name. + + + +If you just created the role, it may take up to **60 seconds** for AWS to propagate it. If you get an error in the Prowler wizard, wait a moment and try again. + + +## Step 2: Deploy the CloudFormation StackSet + +After creating the management account role, the next step is to deploy the **ProwlerScan** role to your member accounts using a CloudFormation StackSet. This is the recommended method for consistent, scalable deployment across your entire organization. + +The StackSet uses **service-managed permissions**, which means AWS Organizations handles the cross-account deployment automatically — you don't need to create execution roles manually in each account. The StackSet deploys the ProwlerScan IAM role in every target member account, enabling Prowler to assume that role for cross-account scanning. + + +**Trusted access required:** CloudFormation StackSets must have trusted access enabled in your management account. Verify this in the AWS Console under **AWS Organizations > Settings > Trusted access for AWS CloudFormation StackSets**. + + +### Option A: Using the Prowler Quick Create Link (Recommended) + +The Prowler wizard provides a one-click link that opens the AWS Console with everything pre-configured. + + +**[Open Quick Create in AWS Console →](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/quickcreate?templateURL=https%3A%2F%2Fprowler-cloud-public.s3.eu-west-1.amazonaws.com%2Fpermissions%2Ftemplates%2Faws%2Fcloudformation%2Fprowler-scan-role.yml&stackName=Prowler)** + +Opens the CloudFormation Console with the Prowler scan role template and parameters pre-configured. You can also find this link in the Prowler wizard during [Step 4: Authentication](#step-4-authenticate-with-your-management-account). + + +1. Review the pre-filled parameters: + - **Template URL**: Points to the official [Prowler scan role template](https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml) hosted on Prowler's public S3 bucket. + - **ExternalId**: Pre-filled with your tenant's External ID when clicking the link from the Prowler Cloud wizard. If you open this link directly, you will need to enter the External ID manually. + + {/* TODO: screenshot of AWS Console Quick Create page showing pre-filled parameters */} + +2. Under **Deployment targets**, select: + - **Deploy to organization** to deploy to all accounts, or + - **Deploy to organizational units (OUs)** and specify the OU IDs you want to cover. + +3. Review the settings and click **Create StackSet**. AWS will begin deploying the ProwlerScan role to every target account. + +### Option B: Manual StackSet Deployment + +If you prefer full control over the deployment: + +1. Open the [AWS CloudFormation Console](https://console.aws.amazon.com/cloudformation/) in your management account. +2. Go to **StackSets > Create StackSet**. +3. Choose **Service-managed permissions**. +4. Use this template URL: + ``` + https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml + ``` +5. Set the **ExternalId** parameter to the External ID shown in the Prowler wizard. +6. Choose your deployment targets (entire organization or specific OUs). +7. Select the AWS regions where you want the role deployed. +8. Click **Create StackSet**. + +### Verify StackSet Deployment + +After deploying, verify that all stack instances completed successfully: + +1. In the CloudFormation Console, go to **StackSets** and select your Prowler StackSet. +2. Click the **Stack instances** tab. +3. Confirm that all instances show **Status: CURRENT** and **Stack status: CREATE_COMPLETE**. + +Deployment typically takes **2–5 minutes** for medium-sized organizations. Large organizations (500+ accounts) may take longer. + + +**Prefer Terraform?** You can deploy the ProwlerScan role using Terraform instead. See the [StackSets deployment guide](/user-guide/providers/aws/organizations#deploying-prowler-iam-roles-across-aws-organizations) for the Terraform module. + + +### Key Considerations + +- **Service-managed permissions**: Always select **Service-managed permissions** when creating the StackSet. This lets AWS Organizations manage the deployment automatically across current and future member accounts. +- **Least privilege**: The ProwlerScan role deployed by the StackSet uses `SecurityAudit` and `ViewOnlyAccess` — AWS managed policies that grant read-only access — plus a small set of additional read-only permissions for services not covered by those policies. See the [CloudFormation template](https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml) for the full list. Prowler does not make any changes to your accounts. +- **New accounts**: When you add new accounts to your AWS Organization, the StackSet automatically deploys the ProwlerScan role to them if you targeted the organization root or the relevant OU. Combined with Prowler's 6-hour automatic sync, new accounts are onboarded end-to-end without manual intervention. +- **Management account**: Organizational StackSets **do not deploy to the management account itself**. If you want to scan the management account, you need to create the ProwlerScan role there separately using a regular CloudFormation Stack. + +## Step 3: Start the Organization Wizard + +Now that both roles are deployed — the management account role (Step 1) and the ProwlerScan role in member accounts (Step 2) — you can start the Prowler wizard. + +### Open the Wizard + +1. Navigate to **Cloud Providers** and click **Add Cloud Provider**. + + + Cloud Providers page showing the Add Cloud Provider button + + +2. Select **Amazon Web Services** as the provider. + + + Provider selection modal with Amazon Web Services highlighted + + +3. Choose **Add Multiple Accounts With AWS Organizations**. + + + Method selector showing Add Multiple Accounts With AWS Organizations option highlighted + + +### Enter Organization Details + +- **Organization ID**: Your AWS Organization identifier, found in the [AWS Organizations Console](https://console.aws.amazon.com/organizations/). It follows the format `o-` followed by 10–32 lowercase alphanumeric characters (e.g., `o-abc123def4`). You can find it in the left sidebar of the AWS Organizations console: + + + AWS Organizations Console showing the Organization ID in the left sidebar + +- **Name** (optional): A display name for the organization. If left blank, Prowler uses the name stored in AWS. + + + Organization Details form with Organization ID and Name fields + + +Click **Next** to proceed to the authentication phase. + +## Step 4: Authenticate with Your Management Account + +### Copy the External ID + +The wizard displays a **Prowler External ID** — auto-generated and unique to your tenant. Click the copy icon to copy it. If you haven't already configured the trust policy on your management account role ([Step 1](#step-1-create-the-management-account-role)), do so now using this External ID. + + + Authentication Details form showing External ID, Role ARN field, and StackSet confirmation checkbox + + +### Enter the Role ARN + +Paste the **Role ARN** of the management account role you created in [Step 1](#step-1-create-the-management-account-role) into the **Role ARN** field. + +The ARN follows this format: +``` +arn:aws:iam:::role/ +``` + +For example: `arn:aws:iam::123456789012:role/ProwlerScan` + + + Role ARN field in the Authentication Details form + + +### Confirm and Discover + +1. Check the box: **"The StackSet has been successfully deployed in AWS"**. +2. Click **Authenticate**. + +Here's what happens behind the scenes: +- Prowler creates the organization resource and stores your credentials securely. +- An asynchronous discovery is triggered to query your AWS Organization structure. +- You will see a **"Gathering AWS Accounts..."** spinner — this typically takes **30 seconds to 2 minutes** depending on your organization size. + +{/* TODO: screenshot of the Authentication Details form with the spinner */} + +## Step 5: Select Accounts to Scan + +### Understanding the Tree View + +Once discovery completes, the wizard displays a **hierarchical tree view** of your Organization: + + + Hierarchical tree view showing OUs and accounts with selection checkboxes + + +- The tree supports up to **5 levels of nesting** (Root > OUs > Sub-OUs > Accounts). +- **Selecting an OU** automatically selects all accounts within it. +- **Individual overrides**: deselect specific accounts even if the parent OU is selected. +- The header shows **"X of Y accounts selected"** to track your selection. + +### Account Statuses + +Only **ACTIVE** accounts can be selected for scanning: + +| Status | Selectable? | Description | +|--------|-------------|-------------| +| **ACTIVE** | Yes | Account is active and operational. | +| **SUSPENDED** | No | Account is suspended by AWS. | +| **PENDING_CLOSURE** | No | Account is being closed. | +| **CLOSED** | No | Account has been closed. | + + +**Your existing data is safe.** If an AWS account is already connected to Prowler as an individual provider, it will appear in the tree with a checkmark indicator. + +When you proceed: +- The existing provider is **linked** to the organization — it is **not** duplicated. +- All your **historical scan data and findings are preserved** — nothing is overwritten. +- There is **no additional billing** — the existing provider is reused. + +This is completely safe. You are simply associating the account with the organization for easier management. + + +### Custom Aliases + +You can edit the display name for each account before connecting. This alias is only used in Prowler — it does not affect your AWS account name. + +### Blocked Accounts + +Some accounts may appear as **blocked** (grayed out, not selectable). This happens when: +- The account is **already linked to a different organization** in Prowler (`linked_to_other_organization`). + +Hover over the blocked account to see the specific reason. + +{/* TODO: screenshot of the tree view with account selection, showing active, already-connected, and blocked accounts */} + +## Step 6: Test Connections + +### How Connection Testing Works + +Click **Test Connections** to verify that Prowler can assume the **ProwlerScan** role in each selected member account. + + + Connection testing in progress with spinners on each account + + +- Each account shows a real-time status indicator: + - **Spinner** — test in progress + - **Green checkmark (✓)** — connection successful + - **Red icon (✗)** — connection failed (hover to see the error) + +### All Tests Pass + +If every account connects successfully, you automatically advance to the next step. + +### Some Tests Fail + +An error banner appears: **"There was a problem connecting to some accounts."** + +You have two options: + +**a) Fix and retry:** +1. Go to the AWS Console and verify the StackSet deployed to the failing accounts. +2. Check that the External ID in the StackSet matches the one shown in Prowler. +3. Return to Prowler and click **Test Connections** — only the **failed accounts are re-tested** (smart retry). Accounts that already passed are not tested again. + + + Test Connections button + + +**b) Skip and continue:** +Click **Skip Connection Validation** to proceed with only the accounts that connected successfully. The failed accounts will not be scanned. + + + Connection test results showing failed accounts with error banner and Skip Connection Validation button + + + +**Skip Connection Validation** is only available when at least one account connected successfully. + + +### All Tests Fail + +If **no accounts** connected successfully, you cannot proceed: + +> *"No accounts connected successfully. Fix the connection errors and retry before launching scans."* + +You must fix the underlying connection issues before continuing. See [Updating Credentials](#updating-credentials) below. + +### Updating Credentials + +If connection tests fail, here's how to fix common issues: + +1. Open the [CloudFormation Console](https://console.aws.amazon.com/cloudformation/) and check that your StackSet instances show **CREATE_COMPLETE** for the failing accounts. If not, update the StackSet to include the missing OUs. +2. Compare the **ExternalId** parameter in your StackSet with the External ID displayed in the Prowler wizard. They must match exactly. +3. After fixing the issue in AWS, return to Prowler and click **Test Connections**. Only the previously failed accounts will be re-tested. + +## Step 7: Launch Scans + +### Choose Scan Schedule + +| Schedule Option | Description | +|-----------------|-------------| +| **Scan Daily (every 24 hours)** | Creates a recurring daily scan for all connected accounts (default). | +| **Run a single scan (no recurring schedule)** | Launches a one-time scan. | + +### Launch + +Click **Launch scan**. A toast notification confirms: *"Scan Launched — Daily scan scheduled for X accounts"* with a link to the Scans page. You will be redirected to the **Providers** page. + +Scans are only launched for accounts that are accessible (passed connection testing) and were selected. + + + Launch Scan step showing Accounts Connected confirmation, scan schedule selector, and Launch scan button + + +### What Happens Next + +- Scans appear in the **Scans** page as they start and complete. +- Results populate the **Overview** and **Findings** pages. +- Prowler runs an **automatic sync every 6 hours** to detect new accounts added to your Organization or accounts that have been removed. New accounts are onboarded automatically based on the parent OU configuration. + +{/* TODO: screenshot of the Launch Scan step */} + +## Billing Impact + +Each AWS account you connect through the Organizations wizard counts as one **provider** in your Prowler Cloud subscription. + +- **Already-connected accounts**: if an account was already linked as a provider, adding it to the organization does **not** incur additional billing. The existing provider is reused. +- **Large organizations**: connecting a 500-account organization will result in up to 500 providers on your subscription. Review your plan limits before proceeding. +- **Deleted providers**: if you later remove an account, the deleted provider no longer counts toward your subscription. + +For pricing details, see [Prowler Cloud Pricing](/getting-started/products/prowler-cloud-pricing). + +## Troubleshooting + +### Invalid AWS Organization ID + +*"Must be a valid AWS Organization ID"* + +- Verify the Organization ID format: `o-` followed by 10–32 lowercase alphanumeric characters (e.g., `o-abc123def4`) +- Copy it directly from the [AWS Organizations Console](https://console.aws.amazon.com/organizations/) to avoid typos + +### Invalid IAM Role ARN + +*"Must be a valid IAM Role ARN"* + +- Verify the ARN format: `arn:aws:iam::<12-digit-account-id>:role/` +- Copy the ARN directly from the [IAM Console](https://console.aws.amazon.com/iam/) in your management account + +### Authentication Failed + +*"Authentication failed. Please verify the StackSet deployment and Role ARN"* + +- Verify the management account role exists and was created in [Step 1](#step-1-create-the-management-account-role) +- Confirm the trust policy includes the correct External ID from the wizard +- Check the role has all Organizations discovery permissions listed in [Step 1](#step-1-create-the-management-account-role) +- Double-check the Role ARN format and account ID for typos + +### Authentication Timed Out + +*"Authentication timed out"* + +- Retry the authentication step — the second attempt often succeeds +- Check for AWS API rate limiting on the Organizations service +- For very large organizations (500+ accounts), allow extra time for discovery + +### Connection Test Fails for All Accounts + +No accounts pass the connection test. + +- Verify the CloudFormation StackSet was deployed — complete [Step 2](#step-2-deploy-the-cloudformation-stackset) and wait for stack instances to reach **CREATE_COMPLETE** +- Check that the **ExternalId** parameter in the StackSet matches the External ID shown in the Prowler wizard +- If your accounts use IP-based IAM policies, allow [Prowler Cloud public IPs](/user-guide/tutorials/prowler-cloud-public-ips) + +### Connection Test Fails for Some Accounts + +Some accounts show a red icon while others pass. + +- Expand the StackSet deployment to include the OUs containing the failing accounts +- Suspended accounts cannot be scanned — deselect them and proceed +- Ensure the [STS regional endpoint](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html) is enabled in the account's region +- After fixing, click **Test Connections** — only the failed accounts will be re-tested + +### No Accounts Connected Successfully + +*"No accounts connected successfully. Fix the connection errors and retry before launching scans."* + +- Hover over the red icon on each account to see the specific error +- Fix the underlying issues using the guidance above +- Click **Test Connections** to retry + +### Failed to Apply Discovery + +*"Failed to apply discovery"* + +- Check the `blocked_reasons` field for any blocked accounts +- Retry the operation +- If the error persists, contact [Prowler Support](mailto:support@prowler.com) + +## What's Next + + + + Full guide to using Prowler Cloud features. + + + CLI-based Organizations scanning and StackSet deployment with Terraform. + + + Script-based bulk provisioning for advanced automation. + + diff --git a/permissions/templates/cloudformation/prowler-scan-role.yml b/permissions/templates/cloudformation/prowler-scan-role.yml index 74f026757d..395fed6424 100644 --- a/permissions/templates/cloudformation/prowler-scan-role.yml +++ b/permissions/templates/cloudformation/prowler-scan-role.yml @@ -36,6 +36,15 @@ Parameters: The IAM principal type and name that will be allowed to assume the role created, leave an * for all the IAM principals in your AWS account. If you are deploying this template to be used in Prowler Cloud please do not edit this. Type: String Default: role/prowler* + EnableOrganizations: + Description: | + Enable AWS Organizations discovery permissions. Set to true only when deploying this role in the management account. + This adds read-only Organizations permissions (e.g. ListAccounts, DescribeOrganization) and StackSet management permissions. + Type: String + Default: false + AllowedValues: + - true + - false EnableS3Integration: Description: | Enable S3 integration for storing Prowler scan reports. @@ -56,6 +65,7 @@ Parameters: Default: "" Conditions: + OrganizationsEnabled: !Equals [!Ref EnableOrganizations, true] S3IntegrationEnabled: !Equals [!Ref EnableS3Integration, true] @@ -140,6 +150,30 @@ Resources: Resource: - "arn:*:apigateway:*::/restapis/*" - "arn:*:apigateway:*::/apis/*" + - !If + - OrganizationsEnabled + - PolicyName: ProwlerOrganizations + PolicyDocument: + Version: "2012-10-17" + Statement: + - Sid: AllowOrganizationsReadOnly + Effect: Allow + Action: + - "organizations:DescribeAccount" + - "organizations:DescribeOrganization" + - "organizations:ListAccounts" + - "organizations:ListAccountsForParent" + - "organizations:ListOrganizationalUnitsForParent" + - "organizations:ListRoots" + - "organizations:ListTagsForResource" + Resource: "*" + - Sid: AllowStackSetManagement + Effect: Allow + Action: + - "organizations:RegisterDelegatedAdministrator" + - "iam:CreateServiceLinkedRole" + Resource: "*" + - !Ref AWS::NoValue - !If - S3IntegrationEnabled - PolicyName: S3Integration @@ -191,6 +225,7 @@ Metadata: - ExternalId - AccountId - IAMPrincipal + - EnableOrganizations - EnableS3Integration - Label: default: Optional diff --git a/permissions/templates/terraform/main.tf b/permissions/templates/terraform/main.tf index 83d8f6867a..1f1306c4ce 100644 --- a/permissions/templates/terraform/main.tf +++ b/permissions/templates/terraform/main.tf @@ -67,6 +67,45 @@ resource "aws_iam_role_policy_attachment" "prowler_scan_viewonly_policy_attachme policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/job-function/ViewOnlyAccess" } +# Organizations Policy (management account only) +################################### +data "aws_iam_policy_document" "prowler_organizations_policy" { + count = var.enable_organizations ? 1 : 0 + + statement { + sid = "AllowOrganizationsReadOnly" + effect = "Allow" + actions = [ + "organizations:DescribeAccount", + "organizations:DescribeOrganization", + "organizations:ListAccounts", + "organizations:ListAccountsForParent", + "organizations:ListOrganizationalUnitsForParent", + "organizations:ListRoots", + "organizations:ListTagsForResource", + ] + resources = ["*"] + } + + statement { + sid = "AllowStackSetManagement" + effect = "Allow" + actions = [ + "organizations:RegisterDelegatedAdministrator", + "iam:CreateServiceLinkedRole", + ] + resources = ["*"] + } +} + +resource "aws_iam_role_policy" "prowler_organizations_policy" { + count = var.enable_organizations ? 1 : 0 + + name = "ProwlerOrganizations" + role = aws_iam_role.prowler_scan.name + policy = data.aws_iam_policy_document.prowler_organizations_policy[0].json +} + # S3 Integration Module ################################### module "s3_integration" { diff --git a/permissions/templates/terraform/variables.tf b/permissions/templates/terraform/variables.tf index 453691fb23..832171b62a 100644 --- a/permissions/templates/terraform/variables.tf +++ b/permissions/templates/terraform/variables.tf @@ -27,6 +27,12 @@ variable "iam_principal" { default = "role/prowler*" } +variable "enable_organizations" { + type = bool + description = "Enable AWS Organizations discovery permissions. Set to true only when deploying this role in the management account." + default = false +} + variable "enable_s3_integration" { type = bool description = "Enable S3 integration for storing Prowler scan reports." diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 48b755a193..6d1184fd39 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -68,6 +68,7 @@ All notable changes to the **Prowler SDK** are documented in this file. ### 🔐 Security - Bumped `py-ocsf-models` to 0.8.1 and `cryptography` to 44.0.3 [(#10059)](https://github.com/prowler-cloud/prowler/pull/10059) +- Harden GitHub Actions workflows against expression injection, add `persist-credentials: false` to checkout steps, and configure dependabot cooldown [(#10200)](https://github.com/prowler-cloud/prowler/pull/10200) --- diff --git a/ui/app/(prowler)/providers/page.test.ts b/ui/app/(prowler)/providers/page.test.ts new file mode 100644 index 0000000000..8ede1bc614 --- /dev/null +++ b/ui/app/(prowler)/providers/page.test.ts @@ -0,0 +1,15 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { describe, expect, it } from "vitest"; + +describe("providers page", () => { + it("does not use unstable Date.now keys for the providers DataTable", () => { + const currentDir = path.dirname(fileURLToPath(import.meta.url)); + const pagePath = path.join(currentDir, "page.tsx"); + const source = readFileSync(pagePath, "utf8"); + + expect(source).not.toContain("key={`providers-${Date.now()}`}"); + }); +}); diff --git a/ui/app/(prowler)/providers/page.tsx b/ui/app/(prowler)/providers/page.tsx index 6e52449c04..0f7cce564d 100644 --- a/ui/app/(prowler)/providers/page.tsx +++ b/ui/app/(prowler)/providers/page.tsx @@ -104,7 +104,6 @@ const ProvidersTable = async ({
({ - pushMock: vi.fn(), -})); - -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - push: pushMock, - }), -})); - vi.mock("next-auth/react", () => ({ useSession: () => ({ data: null, @@ -30,9 +20,9 @@ vi.mock("next-auth/react", () => ({ describe("useProviderWizardController", () => { beforeEach(() => { + vi.useRealTimers(); sessionStorage.clear(); localStorage.clear(); - pushMock.mockReset(); useProviderWizardStore.getState().reset(); useOrgSetupStore.getState().reset(); }); @@ -131,7 +121,7 @@ describe("useProviderWizardController", () => { expect(onOpenChange).not.toHaveBeenCalled(); }); - it("closes and navigates when launch footer action is triggered", () => { + it("does not override launch footer config in the controller", () => { // Given const onOpenChange = vi.fn(); const { result } = renderHook(() => @@ -146,15 +136,10 @@ describe("useProviderWizardController", () => { result.current.setCurrentStep(PROVIDER_WIZARD_STEP.LAUNCH); }); - const { resolvedFooterConfig } = result.current; - act(() => { - resolvedFooterConfig.onAction?.(); - }); - // Then - expect(pushMock).toHaveBeenCalledWith("/scans"); - expect(onOpenChange).toHaveBeenCalledWith(false); - expect(result.current.currentStep).toBe(PROVIDER_WIZARD_STEP.CONNECT); + expect(result.current.resolvedFooterConfig.showAction).toBe(false); + expect(result.current.resolvedFooterConfig.showBack).toBe(false); + expect(onOpenChange).not.toHaveBeenCalled(); }); it("does not reset organizations step when org store updates while modal is open", () => { @@ -188,4 +173,69 @@ describe("useProviderWizardController", () => { expect(result.current.wizardVariant).toBe("organizations"); expect(result.current.orgCurrentStep).toBe(ORG_WIZARD_STEP.VALIDATE); }); + + it("does not rehydrate wizard state when initial data changes while modal remains open", async () => { + // Given + const onOpenChange = vi.fn(); + const { result, rerender } = renderHook( + ({ + open, + initialData, + }: { + open: boolean; + initialData?: { + providerId: string; + providerType: "gcp"; + providerUid: string; + providerAlias: string; + secretId: string | null; + mode: "add" | "update"; + }; + }) => + useProviderWizardController({ + open, + onOpenChange, + initialData, + }), + { + initialProps: { + open: true, + initialData: { + providerId: "provider-1", + providerType: "gcp", + providerUid: "project-123", + providerAlias: "gcp-main", + secretId: null, + mode: PROVIDER_WIZARD_MODE.ADD, + }, + }, + }, + ); + + await waitFor(() => { + expect(result.current.currentStep).toBe(PROVIDER_WIZARD_STEP.CREDENTIALS); + }); + + act(() => { + useProviderWizardStore.getState().setVia("service-account"); + result.current.setCurrentStep(PROVIDER_WIZARD_STEP.TEST); + }); + + // When: provider data refreshes while modal is still open + rerender({ + open: true, + initialData: { + providerId: "provider-1", + providerType: "gcp", + providerUid: "project-123", + providerAlias: "gcp-main", + secretId: "secret-1", + mode: PROVIDER_WIZARD_MODE.UPDATE, + }, + }); + + // Then: keep user progress in the current flow + expect(result.current.currentStep).toBe(PROVIDER_WIZARD_STEP.TEST); + expect(useProviderWizardStore.getState().via).toBe("service-account"); + }); }); diff --git a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts index 9e0f6b4636..5222486785 100644 --- a/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts +++ b/ui/components/providers/wizard/hooks/use-provider-wizard-controller.ts @@ -1,7 +1,6 @@ "use client"; -import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { DOCS_URLS, getProviderHelpText } from "@/lib/external-urls"; import { useOrgSetupStore } from "@/store/organizations/store"; @@ -63,7 +62,7 @@ export function useProviderWizardController({ const initialSecretId = initialData?.secretId ?? null; const initialVia = initialData?.via ?? null; const initialMode = initialData?.mode ?? null; - const router = useRouter(); + const hasHydratedForCurrentOpenRef = useRef(false); const [wizardVariant, setWizardVariant] = useState( WIZARD_VARIANT.PROVIDER, ); @@ -95,9 +94,15 @@ export function useProviderWizardController({ useEffect(() => { if (!open) { + hasHydratedForCurrentOpenRef.current = false; return; } + if (hasHydratedForCurrentOpenRef.current) { + return; + } + hasHydratedForCurrentOpenRef.current = true; + if (initialProviderId && initialProviderType && initialProviderUid) { setWizardVariant(WIZARD_VARIANT.PROVIDER); setProvider({ @@ -198,25 +203,7 @@ export function useProviderWizardController({ const docsLink = isProviderFlow ? getProviderHelpText(providerTypeHint ?? providerType ?? "").link : DOCS_URLS.AWS_ORGANIZATIONS; - const resolvedFooterConfig: WizardFooterConfig = - isProviderFlow && currentStep === PROVIDER_WIZARD_STEP.LAUNCH - ? { - showBack: true, - backLabel: "Back", - onBack: () => setCurrentStep(PROVIDER_WIZARD_STEP.TEST), - showSecondaryAction: false, - secondaryActionLabel: "", - secondaryActionVariant: "outline", - secondaryActionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON, - showAction: true, - actionLabel: "Go to scans", - actionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON, - onAction: () => { - handleClose(); - router.push("/scans"); - }, - } - : footerConfig; + const resolvedFooterConfig: WizardFooterConfig = footerConfig; const modalTitle = getProviderWizardModalTitle(mode); return { diff --git a/ui/components/providers/wizard/provider-wizard-modal.tsx b/ui/components/providers/wizard/provider-wizard-modal.tsx index c59a3082bb..2becce7161 100644 --- a/ui/components/providers/wizard/provider-wizard-modal.tsx +++ b/ui/components/providers/wizard/provider-wizard-modal.tsx @@ -13,7 +13,10 @@ import { ORG_SETUP_PHASE, ORG_WIZARD_STEP } from "@/types/organizations"; import { PROVIDER_WIZARD_STEP } from "@/types/provider-wizard"; import { useProviderWizardController } from "./hooks/use-provider-wizard-controller"; -import { getOrganizationsStepperOffset } from "./provider-wizard-modal.utils"; +import { + getOrganizationsStepperOffset, + getProviderWizardDocsDestination, +} from "./provider-wizard-modal.utils"; import { ConnectStep } from "./steps/connect-step"; import { CredentialsStep } from "./steps/credentials-step"; import { WIZARD_FOOTER_ACTION_TYPE } from "./steps/footer-controls"; @@ -62,6 +65,7 @@ export function ProviderWizardModal({ enabled: open, refreshToken: scrollHintRefreshToken, }); + const docsDestination = getProviderWizardDocsDestination(docsLink); return ( - Prowler Docs + {`Prowler Docs (${docsDestination})`}
@@ -136,7 +140,11 @@ export function ProviderWizardModal({ )} {isProviderFlow && currentStep === PROVIDER_WIZARD_STEP.LAUNCH && ( - + setCurrentStep(PROVIDER_WIZARD_STEP.TEST)} + onClose={handleClose} + onFooterChange={setFooterConfig} + /> )} {!isProviderFlow && orgCurrentStep === ORG_WIZARD_STEP.SETUP && ( diff --git a/ui/components/providers/wizard/provider-wizard-modal.utils.test.ts b/ui/components/providers/wizard/provider-wizard-modal.utils.test.ts index 8875c5ea2a..758d4d957e 100644 --- a/ui/components/providers/wizard/provider-wizard-modal.utils.test.ts +++ b/ui/components/providers/wizard/provider-wizard-modal.utils.test.ts @@ -5,6 +5,7 @@ import { PROVIDER_WIZARD_MODE } from "@/types/provider-wizard"; import { getOrganizationsStepperOffset, + getProviderWizardDocsDestination, getProviderWizardModalTitle, } from "./provider-wizard-modal.utils"; @@ -50,3 +51,21 @@ describe("getProviderWizardModalTitle", () => { expect(title).toBe("Update Provider Credentials"); }); }); + +describe("getProviderWizardDocsDestination", () => { + it("returns a compact provider label for short provider docs links", () => { + const destination = getProviderWizardDocsDestination( + "https://goto.prowler.com/provider-aws", + ); + + expect(destination).toBe("aws"); + }); + + it("returns a compact destination label for long docs links", () => { + const destination = getProviderWizardDocsDestination( + "https://docs.prowler.com/user-guide/tutorials/prowler-cloud-aws-organizations", + ); + + expect(destination).toBe("aws-organizations"); + }); +}); diff --git a/ui/components/providers/wizard/provider-wizard-modal.utils.ts b/ui/components/providers/wizard/provider-wizard-modal.utils.ts index de62adb50d..414fe5cfd6 100644 --- a/ui/components/providers/wizard/provider-wizard-modal.utils.ts +++ b/ui/components/providers/wizard/provider-wizard-modal.utils.ts @@ -27,3 +27,21 @@ export function getProviderWizardModalTitle(mode: ProviderWizardMode) { return "Adding A Cloud Provider"; } + +export function getProviderWizardDocsDestination(docsLink: string) { + try { + const parsed = new URL(docsLink); + const pathSegments = parsed.pathname + .split("/") + .filter((segment) => segment.length > 0); + const lastSegment = pathSegments.at(-1); + + if (!lastSegment) { + return parsed.hostname; + } + + return lastSegment.replace(/^provider-/, "").replace(/^prowler-cloud-/, ""); + } catch { + return docsLink; + } +} diff --git a/ui/components/providers/wizard/steps/launch-step.test.tsx b/ui/components/providers/wizard/steps/launch-step.test.tsx new file mode 100644 index 0000000000..62a9e15d13 --- /dev/null +++ b/ui/components/providers/wizard/steps/launch-step.test.tsx @@ -0,0 +1,85 @@ +import { act, render, waitFor } from "@testing-library/react"; +import type { ComponentProps } from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { useProviderWizardStore } from "@/store/provider-wizard/store"; + +import { LaunchStep } from "./launch-step"; + +const { scheduleDailyMock, scanOnDemandMock, toastMock } = vi.hoisted(() => ({ + scheduleDailyMock: vi.fn(), + scanOnDemandMock: vi.fn(), + toastMock: vi.fn(), +})); + +vi.mock("@/actions/scans", () => ({ + scheduleDaily: scheduleDailyMock, + scanOnDemand: scanOnDemandMock, +})); + +vi.mock("@/components/ui", () => ({ + ToastAction: ({ children, ...props }: ComponentProps<"button">) => ( + + ), + useToast: () => ({ + toast: toastMock, + }), +})); + +describe("LaunchStep", () => { + beforeEach(() => { + sessionStorage.clear(); + localStorage.clear(); + scheduleDailyMock.mockReset(); + scanOnDemandMock.mockReset(); + toastMock.mockReset(); + useProviderWizardStore.getState().reset(); + }); + + it("launches a daily scan and shows toast", async () => { + // Given + const onClose = vi.fn(); + const onFooterChange = vi.fn(); + useProviderWizardStore.setState({ + providerId: "provider-1", + providerType: "gcp", + providerUid: "project-123", + mode: "add", + }); + + scheduleDailyMock.mockResolvedValue({ data: { id: "scan-1" } }); + + render( + , + ); + + await waitFor(() => { + expect(onFooterChange).toHaveBeenCalled(); + }); + + // When + const initialFooterConfig = onFooterChange.mock.calls.at(-1)?.[0]; + await act(async () => { + initialFooterConfig.onAction?.(); + }); + + // Then + await waitFor(() => { + expect(scheduleDailyMock).toHaveBeenCalledTimes(1); + }); + + const sentFormData = scheduleDailyMock.mock.calls[0]?.[0] as FormData; + expect(sentFormData.get("providerId")).toBe("provider-1"); + expect(onClose).toHaveBeenCalledTimes(1); + expect(scanOnDemandMock).not.toHaveBeenCalled(); + expect(toastMock).toHaveBeenCalledWith( + expect.objectContaining({ + title: "Scan Launched", + }), + ); + }); +}); diff --git a/ui/components/providers/wizard/steps/launch-step.tsx b/ui/components/providers/wizard/steps/launch-step.tsx index 73f5bed03f..c56ca167f7 100644 --- a/ui/components/providers/wizard/steps/launch-step.tsx +++ b/ui/components/providers/wizard/steps/launch-step.tsx @@ -1,15 +1,162 @@ "use client"; -import { CheckCircle2 } from "lucide-react"; +import Link from "next/link"; +import { useEffect, useRef, useState } from "react"; + +import { scanOnDemand, scheduleDaily } from "@/actions/scans"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/shadcn/select/select"; +import { TreeSpinner } from "@/components/shadcn/tree-view/tree-spinner"; +import { TreeStatusIcon } from "@/components/shadcn/tree-view/tree-status-icon"; +import { ToastAction, useToast } from "@/components/ui"; +import { useProviderWizardStore } from "@/store/provider-wizard/store"; +import { TREE_ITEM_STATUS } from "@/types/tree"; + +import { + WIZARD_FOOTER_ACTION_TYPE, + WizardFooterConfig, +} from "./footer-controls"; + +const SCAN_SCHEDULE = { + DAILY: "daily", + SINGLE: "single", +} as const; + +type ScanScheduleOption = (typeof SCAN_SCHEDULE)[keyof typeof SCAN_SCHEDULE]; + +interface LaunchStepProps { + onBack: () => void; + onClose: () => void; + onFooterChange: (config: WizardFooterConfig) => void; +} + +export function LaunchStep({ + onBack, + onClose, + onFooterChange, +}: LaunchStepProps) { + const { toast } = useToast(); + const { providerId } = useProviderWizardStore(); + const [isLaunching, setIsLaunching] = useState(false); + const [scheduleOption, setScheduleOption] = useState( + SCAN_SCHEDULE.DAILY, + ); + const launchActionRef = useRef<() => void>(() => {}); + + const handleLaunchScan = async () => { + if (!providerId) { + return; + } + + setIsLaunching(true); + const formData = new FormData(); + formData.set("providerId", providerId); + const result = + scheduleOption === SCAN_SCHEDULE.DAILY + ? await scheduleDaily(formData) + : await scanOnDemand(formData); + + if (result?.error) { + setIsLaunching(false); + toast({ + variant: "destructive", + title: "Unable to launch scan", + description: String(result.error), + }); + return; + } + + setIsLaunching(false); + onClose(); + toast({ + title: "Scan Launched", + description: + scheduleOption === SCAN_SCHEDULE.DAILY + ? "Daily scan scheduled successfully." + : "Single scan launched successfully.", + action: ( + + Go to scans + + ), + }); + }; + + launchActionRef.current = () => { + void handleLaunchScan(); + }; + + useEffect(() => { + onFooterChange({ + showBack: true, + backLabel: "Back", + backDisabled: isLaunching, + onBack, + showAction: true, + actionLabel: isLaunching ? "Launching scans..." : "Launch scan", + actionDisabled: isLaunching || !providerId, + actionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON, + onAction: () => { + launchActionRef.current(); + }, + }); + }, [isLaunching, onBack, onFooterChange, providerId]); + + if (isLaunching) { + return ( +
+
+ +

Launching scans...

+
+
+ ); + } -export function LaunchStep() { return ( -
- -

Provider connected successfully

-

- Continue with the action button to go to scans. +

+
+ +

Connection validated!

+
+ +

+ Choose how you want to launch scans for this provider.

+ + {!providerId && ( +

+ Provider data is missing. Go back and test the connection again. +

+ )} + +
+

Scan schedule

+ +
); } diff --git a/ui/components/providers/wizard/steps/test-connection-step.test.tsx b/ui/components/providers/wizard/steps/test-connection-step.test.tsx index cd4d2d725f..75d564aa54 100644 --- a/ui/components/providers/wizard/steps/test-connection-step.test.tsx +++ b/ui/components/providers/wizard/steps/test-connection-step.test.tsx @@ -1,4 +1,5 @@ import { render, screen, waitFor } from "@testing-library/react"; +import { useEffect } from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { useProviderWizardStore } from "@/store/provider-wizard/store"; @@ -6,8 +7,9 @@ import { PROVIDER_WIZARD_MODE } from "@/types/provider-wizard"; import { TestConnectionStep } from "./test-connection-step"; -const { getProviderMock } = vi.hoisted(() => ({ +const { getProviderMock, loadingFromFormMock } = vi.hoisted(() => ({ getProviderMock: vi.fn(), + loadingFromFormMock: { current: false }, })); vi.mock("@/actions/providers", () => ({ @@ -15,7 +17,19 @@ vi.mock("@/actions/providers", () => ({ })); vi.mock("../../workflow/forms/test-connection-form", () => ({ - TestConnectionForm: () =>
, + TestConnectionForm: ({ + onLoadingChange, + }: { + onLoadingChange?: (isLoading: boolean) => void; + }) => { + useEffect(() => { + if (loadingFromFormMock.current) { + onLoadingChange?.(true); + } + }, [onLoadingChange]); + + return
; + }, })); describe("TestConnectionStep", () => { @@ -23,6 +37,7 @@ describe("TestConnectionStep", () => { sessionStorage.clear(); localStorage.clear(); getProviderMock.mockReset(); + loadingFromFormMock.current = false; useProviderWizardStore.getState().reset(); }); @@ -64,4 +79,54 @@ describe("TestConnectionStep", () => { }); expect(useProviderWizardStore.getState().secretId).toBe("secret-1"); }); + + it("updates footer action label to checking while connection test is in progress", async () => { + // Given + loadingFromFormMock.current = true; + useProviderWizardStore.setState({ + providerId: "provider-1", + providerType: "gcp", + mode: PROVIDER_WIZARD_MODE.ADD, + }); + getProviderMock.mockResolvedValue({ + data: { + id: "provider-1", + attributes: { + uid: "project-123", + provider: "gcp", + alias: "Main", + connection: { connected: false, last_checked_at: null }, + scanner_args: {}, + }, + relationships: { + secret: { data: { type: "provider-secrets", id: "secret-1" } }, + }, + }, + }); + const onFooterChange = vi.fn(); + + // When + render( + , + ); + + // Then + await waitFor(() => { + expect(onFooterChange).toHaveBeenCalled(); + }); + + await waitFor(() => { + const footerConfigs = onFooterChange.mock.calls.map((call) => call[0]); + const hasCheckingState = footerConfigs.some( + (config) => + config.actionLabel === "Checking connection..." && + config.actionDisabled === true, + ); + expect(hasCheckingState).toBe(true); + }); + }); }); diff --git a/ui/components/providers/wizard/steps/test-connection-step.tsx b/ui/components/providers/wizard/steps/test-connection-step.tsx index 4c87e98d81..0032771848 100644 --- a/ui/components/providers/wizard/steps/test-connection-step.tsx +++ b/ui/components/providers/wizard/steps/test-connection-step.tsx @@ -92,10 +92,9 @@ export function TestConnectionStep({ backDisabled: isFormLoading, onBack: onResetCredentials, showAction: canSubmit, - actionLabel: - mode === PROVIDER_WIZARD_MODE.UPDATE - ? "Check connection" - : "Launch scan", + actionLabel: isFormLoading + ? "Checking connection..." + : "Check connection", actionDisabled: isFormLoading, actionType: WIZARD_FOOTER_ACTION_TYPE.SUBMIT, actionFormId: formId, diff --git a/ui/components/providers/workflow/forms/test-connection-form.tsx b/ui/components/providers/workflow/forms/test-connection-form.tsx index d6ae5dd1e8..ccdf0afd32 100644 --- a/ui/components/providers/workflow/forms/test-connection-form.tsx +++ b/ui/components/providers/workflow/forms/test-connection-form.tsx @@ -1,6 +1,5 @@ "use client"; -import { Checkbox } from "@heroui/checkbox"; import { zodResolver } from "@hookform/resolvers/zod"; import { Icon } from "@iconify/react"; import { Loader2 } from "lucide-react"; @@ -14,9 +13,8 @@ import { checkConnectionProvider, deleteCredentials, } from "@/actions/providers"; -import { scanOnDemand, scheduleDaily } from "@/actions/scans"; import { getTask } from "@/actions/task/tasks"; -import { CheckIcon, RocketIcon } from "@/components/icons"; +import { CheckIcon } from "@/components/icons"; import { Button } from "@/components/shadcn"; import { useToast } from "@/components/ui"; import { Form } from "@/components/ui/form"; @@ -83,7 +81,6 @@ export const TestConnectionForm = ({ error: string | null; } | null>(null); const [isResettingCredentials, setIsResettingCredentials] = useState(false); - const [isRedirecting, setIsRedirecting] = useState(false); const formSchema = testConnectionFormSchema; @@ -91,7 +88,6 @@ export const TestConnectionForm = ({ resolver: zodResolver(formSchema), defaultValues: { providerId, - runOnce: false, }, }); @@ -149,44 +145,12 @@ export const TestConnectionForm = ({ } if (connected && !isUpdated) { - try { - // Check if the runOnce checkbox is checked - const runOnce = form.watch("runOnce"); - - let data; - - if (runOnce) { - data = await scanOnDemand(formData); - } else { - data = await scheduleDaily(formData); - } - - if (data.error) { - setApiErrorMessage(data.error); - form.setError("providerId", { - type: "server", - message: data.error, - }); - toast({ - variant: "destructive", - title: "Oops! Something went wrong", - description: data.error, - }); - } else { - if (onSuccess) { - onSuccess(); - return; - } - - setIsRedirecting(true); - router.push("/scans"); - } - } catch (_error) { - form.setError("providerId", { - type: "server", - message: "An unexpected error occurred. Please try again.", - }); + if (onSuccess) { + onSuccess(); + return; } + + return router.push("/providers"); } else { setConnectionStatus({ connected: false, @@ -235,25 +199,6 @@ export const TestConnectionForm = ({ } }; - if (isRedirecting) { - return ( -
-
-
-
-
-
-

- Scan initiated successfully -

-

- Redirecting to scans job details... -

-
-
- ); - } - return (
-
- {!isUpdated - ? "Check connection and launch scan" - : "Check connection"} -
+
Check connection

{!isUpdated - ? "After a successful connection, a scan will automatically run every 24 hours. To run a single scan instead, select the checkbox below." + ? "After a successful connection, continue to the launch step to configure and start your scan." : "A successful connection will redirect you to the providers page."}

@@ -309,20 +250,6 @@ export const TestConnectionForm = ({ providerUID={providerData.data.attributes.uid} /> - {!isUpdated && !connectionStatus?.error && ( - - Run a single scan (no recurring schedule). - - )} - {isUpdated && !connectionStatus?.error && (

Check the new credentials and test the connection. @@ -372,13 +299,13 @@ export const TestConnectionForm = ({ {isLoading ? ( ) : ( - !isUpdated && + )} {isLoading - ? "Loading" + ? "Checking" : isUpdated ? "Check connection" - : "Launch scan"} + : "Continue"} )}

diff --git a/ui/tests/providers/providers-page.ts b/ui/tests/providers/providers-page.ts index b4948657b5..117590dc33 100644 --- a/ui/tests/providers/providers-page.ts +++ b/ui/tests/providers/providers-page.ts @@ -756,6 +756,9 @@ export class ProvidersPage extends BasePage { }); if (await button.isVisible().catch(() => false)) { await button.click(); + if (actionName === "Check connection") { + await this.handleCheckConnectionCompletion(); + } if (actionName === "Launch scan") { await this.handleLaunchScanCompletion(); } @@ -768,9 +771,9 @@ export class ProvidersPage extends BasePage { ); } - private async handleLaunchScanCompletion(): Promise { - const goToScansButton = this.page.getByRole("button", { - name: "Go to scans", + private async handleCheckConnectionCompletion(): Promise { + const launchScanButton = this.page.getByRole("button", { + name: "Launch scan", exact: true, }); const connectionError = this.page.locator( @@ -779,9 +782,9 @@ export class ProvidersPage extends BasePage { try { await Promise.race([ - this.page.waitForURL(/\/scans/, { timeout: 20000 }), - goToScansButton.waitFor({ state: "visible", timeout: 20000 }), - connectionError.waitFor({ state: "visible", timeout: 20000 }), + launchScanButton.waitFor({ state: "visible", timeout: 30000 }), + this.wizardModal.waitFor({ state: "hidden", timeout: 30000 }), + connectionError.waitFor({ state: "visible", timeout: 30000 }), ]); } catch { // Continue and inspect visible state below. @@ -794,14 +797,47 @@ export class ProvidersPage extends BasePage { ); } - if (this.page.url().includes("/scans")) { - return; + if (await launchScanButton.isVisible().catch(() => false)) { + await launchScanButton.click(); + await this.handleLaunchScanCompletion(); + } + } + + private async handleLaunchScanCompletion(): Promise { + const connectionError = this.page.locator( + "div.border-border-error p.text-text-error-primary", + ); + const launchErrorToast = this.page.getByRole("alert").filter({ + hasText: /Unable to launch scan/i, + }); + + try { + await Promise.race([ + this.wizardModal.waitFor({ state: "hidden", timeout: 30000 }), + connectionError.waitFor({ state: "visible", timeout: 30000 }), + launchErrorToast.waitFor({ state: "visible", timeout: 30000 }), + ]); + } catch { + // Continue and inspect visible state below. } - if (await goToScansButton.isVisible().catch(() => false)) { - await goToScansButton.click(); - await this.page.waitForURL(/\/scans/, { timeout: 30000 }); + if (await connectionError.isVisible().catch(() => false)) { + const errorText = await connectionError.textContent(); + throw new Error( + `Test connection failed with error: ${errorText?.trim() || "Unknown error"}`, + ); } + + if (await launchErrorToast.isVisible().catch(() => false)) { + const errorText = await launchErrorToast.textContent(); + throw new Error( + `Launch scan failed with error: ${errorText?.trim() || "Unknown error"}`, + ); + } + + await expect(this.wizardModal).not.toBeVisible({ timeout: 30000 }); + await this.page.waitForURL(/\/providers/, { timeout: 30000 }); + await expect(this.providersTable).toBeVisible({ timeout: 30000 }); } async selectCredentialsType(type: AWSCredentialType): Promise { diff --git a/ui/tests/scans/scans-page.ts b/ui/tests/scans/scans-page.ts index 6f0052873d..1d968bafa7 100644 --- a/ui/tests/scans/scans-page.ts +++ b/ui/tests/scans/scans-page.ts @@ -38,6 +38,9 @@ export class ScansPage extends BasePage { async verifyPageLoaded(): Promise { // Verify the scans page is loaded + if (!this.page.url().includes("/scans")) { + await this.goto(); + } await expect(this.page).toHaveTitle(/Prowler/); await expect(this.scanTable).toBeVisible();