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.1.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.1literal ["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.1",
"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.1") | .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.1, 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