Skip to main content

Prowler integration

Prowler is the most popular open-source multi-cloud security posture (CSPM) scanner — it audits AWS, GCP, Azure, Kubernetes, and Microsoft 365 against CIS, NIST 800-53, ISO 27001, HIPAA, GDPR, PCI-DSS, and roughly twenty other compliance frameworks. CI/lock wraps prowler, captures its real argv and exit code, hashes the report file it writes, and parses the findings into a structured prowler/v0.1 predicate ready for Rego policy.

Tool URLhttps://github.com/prowler-cloud/prowler
LicenseApache-2.0
Categorymulti-cloud CSPM / compliance auditor
Rookery attestor used todayprowler (native — this page)

Validated invocation​

CI/lock invokes prowler directly as the wrapped command — no bash -c "cp …" shim. The real tool is what CI/lock executes, traces, and records. The validated invocation against AWS:

cilock run --step prowler-scan \
--signer-file-key-path key.pem \
--outfile attestation.json \
--attestations prowler,environment,git \
--enable-archivista=false \
-- prowler aws --services iam -M json -o output -F prowler-iam -z

This is the exact line validated end-to-end in tool-prowler-ocsf — don't paraphrase it.

Prowler exits with code 3 when failures are found. The -z / --ignore-exit-code-3 flag keeps command-run/v0.2.exitcode == 0 so the postproduct stage runs cleanly even when the scan finds problems — the findings themselves are still recorded inside the prowler/v0.1 predicate, so downstream policy still sees them. Drop -z if you want the runner to abort on any non-zero from the scanner.

AWS authentication is read from the surrounding shell. For SSO-backed profiles run aws sso login --profile <profile> first; for keys export AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY; for instance roles nothing extra is needed.

What gets captured​

A single run with --attestations prowler,environment,git produces these predicate types in the signed envelope:

Predicate typeSource
https://aflock.ai/attestations/environment/v0.1host OS, kernel, env vars (sensitive ones obfuscated)
https://aflock.ai/attestations/git/v0.1commit hash, branch, tags, dirty status, parents
https://aflock.ai/attestations/material/v0.3Merkle root over the working directory before prowler runs
https://aflock.ai/attestations/command-run/v0.2literal ["prowler","aws",…] argv, exit code, ptrace
https://aflock.ai/attestations/product/v0.3Merkle root over output/prowler-iam.json as a real product file
https://aflock.ai/attestations/prowler/v0.1the parsed Prowler report — account, provider, totalChecks/passCount/failCount, severity rollup, failedChecks list (checkId, severity, resourceArn, statusExtended), report digest

The prowler/v0.1 predicate is what distinguishes this flow from a generic sarif ingestion: Rego policy can branch on summary.bySeverity.critical.fail directly without re-parsing SARIF runs/results.

Why this shape​

Antipattern (older docs, see 28-prowler/)Correct shape (this example)
cilock run ... -- bash -c "cp prowler.json prowler-out.json" after running prowler outside CI/lockcilock run ... -- prowler aws --services iam -M json -o output -F prowler-iam -z
command-run.cmd records ["bash","-c","cp …"] — CI/lock "ran" cpcommand-run.cmd records ["prowler","aws",…] — the literal scanner argv is in the envelope
The ptrace spy traces cp, not prowlerThe ptrace spy traces prowler's syscalls because CI/lock is its direct parent
Product is a copy of a file prowler wrote elsewhereProduct is the file prowler wrote inside the wrapped step

The older 28-prowler/ example needed the cp because earlier rookery versions couldn't trace a long-running scanner under ptrace reliably; the v0.3 ptrace spy doesn't have that limitation, so direct invocation is the right shape going forward.

Validate it locally​

After running the invocation above:

# All six predicate types should be present.
jq -r '.payload' attestation.json | base64 -d \
| jq '[.predicate.attestations[].type] | sort'

Expected output:

[
"https://aflock.ai/attestations/command-run/v0.2",
"https://aflock.ai/attestations/environment/v0.1",
"https://aflock.ai/attestations/git/v0.1",
"https://aflock.ai/attestations/material/v0.3",
"https://aflock.ai/attestations/product/v0.3",
"https://aflock.ai/attestations/prowler/v0.1"
]

Confirm command-run.cmd carries the literal Prowler argv (proof the cp antipattern is gone):

jq -r '.payload' attestation.json | base64 -d \
| jq '.predicate.attestations[] | select(.type=="https://aflock.ai/attestations/command-run/v0.2") | .attestation.cmd'
# ["prowler","aws","--services","iam","-M","json","-o","output","-F","prowler-iam","-z"]

Inspect the parsed Prowler summary that Rego policy will gate on:

jq -r '.payload' attestation.json | base64 -d \
| jq '.predicate.attestations[] | select(.type=="https://aflock.ai/attestations/prowler/v0.1") | .attestation.summary | {totalChecks, passCount, failCount, bySeverity}'
# {
# "totalChecks": 93,
# "passCount": 74,
# "failCount": 19,
# "bySeverity": { "critical": {"pass":5,"fail":2}, "high": {"pass":55,"fail":9}, "medium": {"pass":13,"fail":7}, "low": {"pass":1,"fail":1} }
# }

Notes​

Service scoping. --services iam ran 36 IAM checks in ~11s; dropping the flag scans every supported service in the target account, which usually takes minutes. Use --services iam ec2 s3 to scope to multiple services.

Compliance frameworks. Instead of (or in addition to) service scoping, pass -c <framework> — e.g. -c cis_2.0_aws, -c nist_800_53_revision_5_aws, -c hipaa_aws, -c pci_3.2.1_aws. Run prowler aws --list-compliance for the full list. The prowler/v0.1 predicate carries every finding regardless of framework selection.

Output formats. Prowler 3.x emits its native JSON via -M json; Prowler 4+ recommends json-ocsf (OCSF v1.1) and json-asff (AWS Security Hub) as the modern formats. The rookery prowler attestor accepts all three — see fix(prowler): accept Prowler 4 OCSF + ASFF input formats.

AWS auth model. Prowler delegates to boto3, so any auth mechanism boto3 understands works: AWS_PROFILE, AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY, EC2 instance role, ECS task role, EKS IRSA, SSO (aws sso login --profile <profile> first). The environment/v0.1 attestor obfuscates any env var matching CI/lock's sensitive-key list — your AWS secret keys won't leak into the signed envelope as plaintext.

Other clouds. prowler gcp, prowler azure, prowler kubernetes, prowler m365. The CI/lock invocation shape is identical (swap aws for the provider), and the same prowler/v0.1 predicate format is emitted across providers.

FAQ​

Does CI/lock support Prowler?​

Yes. Wrap prowler aws --services <svc> -M json -o output -F <prefix> -z with cilock run --attestations prowler,environment,git. Prowler's report becomes a signed v0.3 attestation under https://aflock.ai/attestations/prowler/v0.1, the literal Prowler argv is captured in command-run/v0.2, and the report file is hashed into the v0.3 Merkle tree.

Which clouds does Prowler scan under CI/lock?​

All clouds Prowler itself supports: AWS, GCP, Azure, Kubernetes, and Microsoft 365. The CI/lock invocation is the same shape across providers — swap prowler aws for prowler gcp, prowler azure, prowler kubernetes, or prowler m365. Each provider emits the same prowler/v0.1 predicate format, so a single Rego policy can gate deploys against findings from any cloud.

How is the rookery prowler attestor different from the sarif attestor?​

The rookery prowler attestor parses Prowler's native JSON (or OCSF / ASFF in Prowler 4+) and produces a structured prowler/v0.1 predicate carrying account, provider, totalChecks/passCount/failCount, a severity rollup, and a typed failedChecks list with each finding's checkId, severity, resourceArn, and statusExtended. The generic sarif attestor would flatten that into a single results array. Rego over prowler/v0.1 gates on summary.bySeverity.critical.fail > 0 directly; the same gate over SARIF requires counting and filtering. Use prowler/v0.1 for compliance gates; use sarif/v0.1 if you want one uniform predicate across many scanners.

How do I gate deploys on Prowler findings?​

Write a Rego policy over the captured prowler/v0.1 predicate. The attestation.summary.bySeverity.<level>.fail counts give you per-severity gates (e.g. deny if summary.bySeverity.critical.fail > 0), and attestation.summary.failedChecks[].checkId lets you allowlist or denylist specific Prowler checks (e.g. allow iam_root_mfa_enabled failing on a sandbox account but block it on prod). The matching attestation.summary.totalChecks > 0 guard catches the silent-scan-failure case — see 28-prowler/policy/ for a worked multi-step verify recipe.

Why pass -z / --ignore-exit-code-3?​

Prowler exits 3 whenever any check fails — that's its default "I found something" signal. Without -z, a real-world scan returns non-zero from command-run, CI/lock treats the wrapped command as failed, and the postproduct stage (which is where the prowler/v0.1 attestor parses the report) doesn't run. -z keeps the exit code at 0 so the report is captured and parsed; the findings themselves are recorded inside the predicate, so downstream policy still sees every failure.

See also​