Step 0 — Load the policy, fresh

This is not a ledger to resume — it's live policy, loaded fresh every run:
bash
cat .kortix/memory/compliance-policy.md 2>/dev/null || echo "(no policy file yet — using the defaults below)"
If no policy file exists yet, use these starting defaults and propose creating .kortix/memory/compliance-policy.md from them on this run:
  • Buckets — none public unless explicitly allow-listed by name.
  • Tags — every resource carries owner and environment.
  • IAM roles — no role holds *:* or an unscoped iam:PassRole.

Step 0b — Editing the policy on request

If a human asked to add or change a rule in the policy itself (not run a sweep), that's the whole job for this run: read .kortix/memory/compliance-policy.md (Step 0 above), edit it to reflect the requested rule, and confirm the change back to the human. No AWS calls, no findings, no change request — the policy file itself is the human-authored input the rest of this skill checks against.

Step 1 — Pull AWS resource state (read-only)

S3 buckets and IAM roles are global, account-wide resourceslist-buckets and list-roles return the same full set no matter which region you ask from. List and evaluate each one exactly once per sweep, outside any region loop:
bash
for bucket in $(aws s3api list-buckets --query 'Buckets[].Name' --output text); do
  bucket_region=$(aws s3api get-bucket-location --bucket "$bucket" --query 'LocationConstraint' --output text)
  [ "$bucket_region" = "None" ] && bucket_region="us-east-1"
  aws s3api get-bucket-policy-status --bucket "$bucket" --region "$bucket_region" 2>/dev/null
  aws s3api get-bucket-tagging        --bucket "$bucket" --region "$bucket_region" 2>/dev/null
done
for role in $(aws iam list-roles --query 'Roles[].RoleName' --output text); do
  aws iam list-attached-role-policies --role-name "$role"
  aws iam list-role-policies          --role-name "$role"
done
Every call here is a read (list-*, get-*). The credentials are brokered server-side and read-only — you use them, you never see the raw key.
{{aws_regions}} is reserved for resources that are genuinely regional. Today's policy (Step 0) covers only buckets and roles, so nothing loops over {{aws_regions}} in this version of the sweep. If the policy is ever extended to a regional resource (e.g. VPCs or security groups), scope only that new check to $(echo "{{aws_regions}}" | tr ',' ' ') — never re-loop the global bucket/role checks above.

Step 2 — Check against policy

For each resource, evaluate against the policy loaded in Step 0. Each resource is listed exactly once (Step 1), so file at most one finding per resource id — never one per region pass:
CheckViolation conditionFinding type
Bucket exposurePolicyStatus.IsPublic = true and the bucket isn't allow-listedNewly public bucket
TaggingMissing a required tag key from the policyUntagged resource
IAM scopeRole holds *:* or an unscoped iam:PassRoleOver-broad role

Step 3 — Cross-reference the audit logs

For every violation from Step 2, find when it happened and who did it. Which region(s) to query depends on the resource type:
  • Bucket finding → query only the bucket's own region, resolved in Step 1 ($bucket_region).
  • Role finding → IAM is a global service with no owning region, so check each region in {{aws_regions}} in turn and stop at the first hit.
bash
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=ResourceName,AttributeValue="<resource-id>" \
  --max-results 10 --region <region-per-rule-above>
Attach the event time, actor, and source IP (if present) to the finding. If nothing turns up in any queried region within the CloudTrail retention window, note the finding as "origin unknown — predates available audit history" rather than guessing.

Step 4 — Draft the proposed remediation (never apply it)

  • Bucket → draft the tightened bucket policy / block-public-access config.
  • Tags → draft the missing tag set from the policy.
  • Role → draft the narrower policy (drop the offending statement or scope the resource/action).
Open one change request per finding via project.cr.open (or one per resource group when several findings share a fix). Title it with the resource and violation, e.g. compliance: newly-public bucket acme-uploads. The CR body is the diagnosis from Step 3 (what, when, who) plus the proposed fix. A human reviews and applies it — you stop at opening the CR.

Step 5 — Post the summary

Post to {{alert_channel}}: one line per finding — resource, violation, when/who changed it, and a link to its proposed CR. If nothing drifted, post one line saying so; don't skip the post just because the sweep is clean.
Compliance sweep — Kortix Marketplace | Kortix