name: Prowler Security Scan description: Run Prowler cloud security scanner using the official Docker image branding: icon: cloud color: green inputs: provider: description: Cloud provider to scan (e.g. aws, azure, gcp, github, kubernetes, iac). See https://docs.prowler.com for supported providers. required: true image-tag: description: > Docker image tag for prowlercloud/prowler. Default is "stable" (latest release). Available tags: "stable" (latest release), "latest" (master branch, not stable), "" (pinned release version). See all tags at https://hub.docker.com/r/prowlercloud/prowler/tags required: false default: stable output-formats: description: Output format(s) for scan results (e.g. "json-ocsf", "sarif json-ocsf") required: false default: json-ocsf push-to-cloud: description: Push scan findings to Prowler Cloud. Requires the PROWLER_CLOUD_API_KEY environment variable. See https://docs.prowler.com/user-guide/tutorials/prowler-app-import-findings#using-the-cli required: false default: "false" flags: description: 'Additional CLI flags passed to the Prowler scan (e.g. "--severity critical high --compliance cis_aws"). Values containing spaces can be quoted, e.g. "--resource-tag ''Environment=My Server''".' required: false default: "" extra-env: description: > Space-, newline-, or comma-separated list of host environment variable NAMES to forward to the Prowler container (e.g. "AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN" for AWS, "GITHUB_PERSONAL_ACCESS_TOKEN" for GitHub, "CLOUDFLARE_API_TOKEN" for Cloudflare). List names only; set the values via `env:` at the workflow or job level (typically from `secrets.*`). See the README for per-provider examples. required: false default: "" upload-sarif: description: 'Upload SARIF results to GitHub Code Scanning (requires "sarif" in output-formats and both `security-events: write` and `actions: read` permissions)' required: false default: "false" sarif-file: description: Path to the SARIF file to upload (auto-detected from output/ if not set) required: false default: "" sarif-category: description: Category for the SARIF upload (used to distinguish multiple analyses) required: false default: prowler fail-on-findings: description: Fail the workflow step when Prowler detects findings (exit code 3). By default the action tolerates findings and succeeds. required: false default: "false" runs: using: composite steps: - name: Validate inputs shell: bash env: INPUT_IMAGE_TAG: ${{ inputs.image-tag }} INPUT_UPLOAD_SARIF: ${{ inputs.upload-sarif }} INPUT_OUTPUT_FORMATS: ${{ inputs.output-formats }} run: | # Validate image tag format (alphanumeric, dots, hyphens, underscores only) if [[ ! "$INPUT_IMAGE_TAG" =~ ^[a-zA-Z0-9._-]+$ ]]; then echo "::error::Invalid image-tag '${INPUT_IMAGE_TAG}'. Must contain only alphanumeric characters, dots, hyphens, and underscores." exit 1 fi # Warn if upload-sarif is enabled but sarif not in output-formats if [ "$INPUT_UPLOAD_SARIF" = "true" ]; then if [[ ! "$INPUT_OUTPUT_FORMATS" =~ (^|[[:space:]])sarif($|[[:space:]]) ]]; then echo "::warning::upload-sarif is enabled but 'sarif' is not included in output-formats ('${INPUT_OUTPUT_FORMATS}'). SARIF upload will fail unless you add 'sarif' to output-formats." fi fi - name: Run Prowler scan shell: bash env: INPUT_PROVIDER: ${{ inputs.provider }} INPUT_IMAGE_TAG: ${{ inputs.image-tag }} INPUT_OUTPUT_FORMATS: ${{ inputs.output-formats }} INPUT_PUSH_TO_CLOUD: ${{ inputs.push-to-cloud }} INPUT_FLAGS: ${{ inputs.flags }} INPUT_EXTRA_ENV: ${{ inputs.extra-env }} INPUT_FAIL_ON_FINDINGS: ${{ inputs.fail-on-findings }} run: | set -e # Parse space-separated inputs with shlex so values with spaces can be quoted # (e.g. `--resource-tag 'Environment=My Server'`). mapfile -t OUTPUT_FORMATS < <(python3 -c 'import shlex, os; [print(t) for t in shlex.split(os.environ.get("INPUT_OUTPUT_FORMATS", ""))]') mapfile -t EXTRA_FLAGS < <(python3 -c 'import shlex, os; [print(t) for t in shlex.split(os.environ.get("INPUT_FLAGS", ""))]') mapfile -t EXTRA_ENV_NAMES < <(python3 -c 'import shlex, os; [print(t) for t in shlex.split(os.environ.get("INPUT_EXTRA_ENV", "").replace(",", " "))]') env_args=() for var in "${EXTRA_ENV_NAMES[@]}"; do [ -z "$var" ] && continue if [[ ! "$var" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then echo "::error::Invalid env var name '${var}' in extra-env. Names must match ^[A-Za-z_][A-Za-z0-9_]*$." exit 1 fi env_args+=("-e" "$var") done push_args=() if [ "$INPUT_PUSH_TO_CLOUD" = "true" ]; then push_args=("--push-to-cloud") env_args+=("-e" "PROWLER_CLOUD_API_KEY") fi mkdir -p "$GITHUB_WORKSPACE/output" chmod 777 "$GITHUB_WORKSPACE/output" set +e docker run --rm \ "${env_args[@]}" \ -v "$GITHUB_WORKSPACE:/home/prowler/workspace" \ -v "$GITHUB_WORKSPACE/output:/home/prowler/workspace/output" \ -w /home/prowler/workspace \ "prowlercloud/prowler:${INPUT_IMAGE_TAG}" \ "$INPUT_PROVIDER" \ --output-formats "${OUTPUT_FORMATS[@]}" \ "${push_args[@]}" \ "${EXTRA_FLAGS[@]}" exit_code=$? set -e # Exit code 3 = findings detected if [ "$exit_code" -eq 3 ] && [ "$INPUT_FAIL_ON_FINDINGS" != "true" ]; then echo "::notice::Prowler detected findings (exit code 3). Set fail-on-findings to 'true' to fail the workflow on findings." exit 0 fi exit $exit_code - name: Upload scan results if: always() uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: prowler-${{ inputs.provider }} path: output/ retention-days: 30 if-no-files-found: warn - name: Find SARIF file if: always() && inputs.upload-sarif == 'true' id: find-sarif shell: bash env: INPUT_SARIF_FILE: ${{ inputs.sarif-file }} run: | if [ -n "$INPUT_SARIF_FILE" ]; then echo "sarif_path=$INPUT_SARIF_FILE" >> "$GITHUB_OUTPUT" else sarif_file=$(find output/ -name '*.sarif' -type f | head -1) if [ -z "$sarif_file" ]; then echo "::warning::No .sarif file found in output/. Ensure 'sarif' is included in output-formats." echo "sarif_path=" >> "$GITHUB_OUTPUT" else echo "sarif_path=$sarif_file" >> "$GITHUB_OUTPUT" fi fi - name: Upload SARIF to GitHub Code Scanning if: always() && inputs.upload-sarif == 'true' && steps.find-sarif.outputs.sarif_path != '' uses: github/codeql-action/upload-sarif@d4b3ca9fa7f69d38bfcd667bdc45bc373d16277e # v4 with: sarif_file: ${{ steps.find-sarif.outputs.sarif_path }} category: ${{ inputs.sarif-category }} - name: Write scan summary if: always() shell: bash env: INPUT_PROVIDER: ${{ inputs.provider }} INPUT_UPLOAD_SARIF: ${{ inputs.upload-sarif }} INPUT_PUSH_TO_CLOUD: ${{ inputs.push-to-cloud }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} REPO_URL: ${{ github.server_url }}/${{ github.repository }} BRANCH: ${{ github.head_ref || github.ref_name }} GH_TOKEN: ${{ github.token }} run: | set +e # Build a link to the scan step in the workflow logs. Requires `actions: read` # on the caller's GITHUB_TOKEN; silently skips the link if unavailable. scan_step_url="" if [ -n "${GH_TOKEN:-}" ] && command -v gh >/dev/null 2>&1; then job_info=$(gh api \ "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT:-1}/jobs" \ --jq ".jobs[] | select(.runner_name == \"${RUNNER_NAME:-}\")" 2>/dev/null) if [ -n "$job_info" ]; then job_id=$(jq -r '.id // empty' <<<"$job_info") step_number=$(jq -r '[.steps[]? | select((.name // "") | test("Run Prowler scan"; "i")) | .number] | first // empty' <<<"$job_info") if [ -z "$step_number" ]; then step_number=$(jq -r '[.steps[]? | select(.status == "in_progress") | .number] | first // empty' <<<"$job_info") fi if [ -n "$job_id" ] && [ -n "$step_number" ]; then scan_step_url="${REPO_URL}/actions/runs/${GITHUB_RUN_ID}/job/${job_id}#step:${step_number}:1" elif [ -n "$job_id" ]; then scan_step_url="${REPO_URL}/actions/runs/${GITHUB_RUN_ID}/job/${job_id}" fi fi fi # Map provider code to a properly-cased display name. case "$INPUT_PROVIDER" in alibabacloud) provider_name="Alibaba Cloud" ;; aws) provider_name="AWS" ;; azure) provider_name="Azure" ;; cloudflare) provider_name="Cloudflare" ;; gcp) provider_name="GCP" ;; github) provider_name="GitHub" ;; googleworkspace) provider_name="Google Workspace" ;; iac) provider_name="IaC" ;; image) provider_name="Container Image" ;; kubernetes) provider_name="Kubernetes" ;; llm) provider_name="LLM" ;; m365) provider_name="Microsoft 365" ;; mongodbatlas) provider_name="MongoDB Atlas" ;; nhn) provider_name="NHN" ;; openstack) provider_name="OpenStack" ;; oraclecloud) provider_name="Oracle Cloud" ;; vercel) provider_name="Vercel" ;; *) provider_name="${INPUT_PROVIDER^}" ;; esac ocsf_file=$(find output/ -name '*.ocsf.json' -type f 2>/dev/null | head -1) { echo "## Prowler ${provider_name} Scan Summary" echo "" counts="" if [ -n "$ocsf_file" ] && [ -s "$ocsf_file" ]; then counts=$(jq -r '[ length, ([.[] | select(.status_code == "FAIL")] | length), ([.[] | select(.status_code == "PASS")] | length), ([.[] | select(.status_code == "MUTED")] | length), ([.[] | select(.status_code == "FAIL" and .severity == "Critical")] | length), ([.[] | select(.status_code == "FAIL" and .severity == "High")] | length), ([.[] | select(.status_code == "FAIL" and .severity == "Medium")] | length), ([.[] | select(.status_code == "FAIL" and .severity == "Low")] | length), ([.[] | select(.status_code == "FAIL" and .severity == "Informational")] | length) ] | @tsv' "$ocsf_file" 2>/dev/null) fi if [ -n "$counts" ]; then read -r total fail pass muted critical high medium low info <<<"$counts" line="**${fail:-0} failing** · ${pass:-0} passing" [ "${muted:-0}" -gt 0 ] && line="${line} · ${muted} muted" echo "${line} — ${total:-0} checks total" echo "" echo "| Severity | Failing |" echo "|----------|---------|" echo "| ‼️ Critical | ${critical:-0} |" echo "| 🔴 High | ${high:-0} |" echo "| 🟠 Medium | ${medium:-0} |" echo "| 🔵 Low | ${low:-0} |" echo "| ⚪ Informational | ${info:-0} |" echo "" else echo "_No findings report was produced. Check the scan logs above._" echo "" fi if [ -n "$scan_step_url" ]; then echo "**Scan logs:** [view in workflow run](${scan_step_url})" echo "" fi echo "**Get the full report:** [\`prowler-${INPUT_PROVIDER}\` artifact](${RUN_URL}#artifacts)" if [ "$INPUT_UPLOAD_SARIF" = "true" ] && [ -n "$BRANCH" ]; then encoded_branch=$(jq -nr --arg b "$BRANCH" '$b|@uri') echo "" echo "**See results in GitHub Code Security:** [open alerts on \`${BRANCH}\`](${REPO_URL}/security/code-scanning?query=is%3Aopen+branch%3A${encoded_branch})" fi if [ "$INPUT_PUSH_TO_CLOUD" != "true" ]; then echo "" echo "---" echo "" echo "### Scale ${provider_name} security with Prowler Cloud ☁️" echo "" echo "Send this scan's findings to **[Prowler Cloud](https://cloud.prowler.com)** and get:" echo "" echo "- **Unified findings** across every cloud, SaaS provider (M365, Google Workspace, GitHub, MongoDB Atlas), IaC repo, Kubernetes cluster, and container image" echo "- **Posture over time** with alerts, and notifications" echo "- **Prowler Lighthouse AI**: agentic assistant that triages findings, explains root cause and helps with remediation" echo "- **50+ Compliance frameworks** mapped automatically" echo "- **Enterprise-ready platform**: SOC 2 Type 2, SSO/SAML, AWS Security Hub, S3 and Jira integrations" echo "" echo "**Get started in 3 steps:**" echo "1. Create an account at [cloud.prowler.com](https://cloud.prowler.com)" echo "2. Generate a Prowler Cloud API key ([docs](https://docs.prowler.com/user-guide/tutorials/prowler-app-import-findings#using-the-cli))" echo "3. Add \`PROWLER_CLOUD_API_KEY\` to your GitHub secrets and set \`push-to-cloud: true\` on this action" echo "" echo "See [prowler.com/pricing](https://prowler.com/pricing) for plan details." fi } >> "$GITHUB_STEP_SUMMARY"