# Catching over-permissive IAM in the pull request, not in production

Over-permissive IAM is one of the most common AWS misconfigurations: `"Action": "*"`, `iam:PassRole` granted on every resource, a trust policy with `"Principal": "*"`. The problem is usually found during an audit, *after* the policy has already been live for weeks.

I wanted that check to happen where the change is made: in the pull request. So I built **Cloud Policy Guard**, a Policy-as-Code security gate for Terraform.

## Scan the plan, not the account

The tool reads the JSON form of a Terraform plan, which describes exactly what Terraform is about to create:

```bash
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
cloud-policy-guard scan --terraform-plan plan.json
```

Because it works on the plan file, it runs **fully offline** and needs no AWS credentials. Nothing has to be deployed, and no secrets have to be handed to the CI job.

## The rules

| ID | Detects | Severity |
| --- | --- | --- |
| LP001 | Wildcard action, `Action: "*"` | Critical |
| LP002 | Wildcard resource, `Resource: "*"` (context-aware) | High / Medium |
| LP003 | Administrative permissions (`*` on `*`, or admin managed policies) | Critical |
| LP004 | Sensitive IAM actions such as `iam:PassRole`, `iam:PutRolePolicy` | High |
| LP005 | Service-level wildcards such as `s3:*`, `iam:*`, `secretsmanager:*` | High / Medium |
| LP006 | Broad trust policies, `Principal: "*"` or insecure cross-account trust | Critical |

The rules are context-aware: they evaluate the combination of actions and resources to set the severity, instead of flagging every wildcard the same way.

## Turning findings into a pass/fail decision

Each finding adds to a deterministic risk score: Critical = 10, High = 7, Medium = 4, Low = 1. A pipeline fails if any finding reaches the `fail_on` severity, or if the total score passes `max_risk_score`:

```yaml
# .cpg.yaml
fail_on: critical       # fail if any finding is critical or worse
max_risk_score: 50      # fail if the total risk score exceeds 50
exclude_rules:
  - LP005               # rules can be disabled per repo
format: text            # text for humans, json for pipelines
```

The exit code does the rest: **0 for PASS, 1 for FAIL**. Any CI system (GitHub Actions, GitLab CI, Jenkins) blocks the merge without extra integration work.

## In a GitHub Actions workflow

```yaml
- name: Terraform Plan
  run: |
    terraform init
    terraform plan -out=tfplan
    terraform show -json tfplan > plan.json

- name: Scan IAM least privilege
  # exit code 1 fails the job and blocks the PR
  run: cloud-policy-guard scan --terraform-plan plan.json
```

## Testing it honestly

The repo ships with a deliberately vulnerable Terraform example and a secure one. The vulnerable plan must FAIL and the secure plan must PASS, and a pytest suite keeps the rules from regressing. The tool is also containerised with Docker, so it behaves the same on my laptop and in CI.

## What I took away

*   Shifting left only works if the check is fast, offline and gives a clear yes/no.
    
*   A severity model plus a score threshold lets teams adopt a gate gradually instead of all at once.
    
*   Two outputs matter: coloured text that developers will actually read, and JSON that pipelines can parse.
