mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-05-06 08:47:18 +00:00
308 lines
14 KiB
YAML
308 lines
14 KiB
YAML
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),
|
|
"<x.y.z>" (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"
|