npm package: @iamujjwalsinha/cdk-infra-guardian
Description: Projen component that generates GitHub Actions workflows for CDK infrastructure diff, security scanning, cost estimation, and destructive change detection on pull requests.
Author: Ujjwal Sinha
Latest version: 0.0.0

# @iamujjwalsinha/cdk-infra-guardian

[![npm version](https://img.shields.io/npm/v/@iamujjwalsinha/cdk-infra-guardian.svg?style=flat-square)](https://www.npmjs.com/package/@iamujjwalsinha/cdk-infra-guardian)
[![Build Status](https://img.shields.io/github/actions/workflow/status/iamujjwalsinha/cdk-infra-guardian/build.yml?style=flat-square)](https://github.com/iamujjwalsinha/cdk-infra-guardian/actions)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue?style=flat-square)](LICENSE)

> A Projen component that automatically generates GitHub Actions workflows to run `cdk diff` on pull requests — posting formatted PR comments with infrastructure diffs, security findings, cost estimates, and destructive-change warnings.

---

## Why This Exists

Running `cdk diff` on every pull request is essential for safe infrastructure delivery, but wiring up the workflow, parsing CDK's raw output, posting readable PR comments, integrating cdk-nag, running Infracost, and blocking dangerous deploys requires hundreds of lines of glue code. In large monorepos with 50+ stacks, that glue code multiplies into unmanageable boilerplate.

`cdk-infra-guardian` packages all of this as a single **Projen component** — one line in your `.projenrc.ts` generates production-grade GitHub Actions workflows for every stack, complete with security scanning, cost estimation, and destructive-change detection.

---

## Features

- **Automatic workflow generation** — one `.github/workflows/<stack>-cdk-diff.yml` per CDK stack
- **Formatted PR comments** — colour-coded summaries of added, modified, and removed resources
- **Destructive change detection** — blocks merges when critical resources (RDS, DynamoDB, S3, KMS, EKS, ECS…) would be deleted
- **cdk-nag integration** — parses nag output and posts security findings with severity levels
- **Infracost integration** — shows monthly cost delta per resource and total impact
- **Find-or-update comments** — one comment per stack, always current, never duplicated
- **Concurrency control** — cancels stale runs on new pushes via GitHub Actions concurrency groups
- **OIDC authentication** — uses `aws-actions/configure-aws-credentials@v4` with role assumption
- **Fully idempotent** — re-running `npx projen` regenerates identical workflows
- **Zero runtime dependencies** in the generated workflows — everything runs via `@actions/github-script`

---

## Quick Start

### 1. Install

```bash
npm install --save-dev @iamujjwalsinha/cdk-infra-guardian
```

### 2. Configure in `.projenrc.ts`

```typescript
import { awscdk } from 'projen';
import { CdkInfraGuardian } from '@iamujjwalsinha/cdk-infra-guardian';

const project = new awscdk.AwsCdkTypeScriptApp({
  name: 'my-infrastructure',
  cdkVersion: '2.130.0',
  defaultReleaseBranch: 'main',
});

new CdkInfraGuardian(project, {
  stacks: ['ApiStack', 'DatabaseStack', 'NetworkStack'],
  awsRegion: 'us-east-1',
  failOnDestructiveChanges: true,
  enableSecurityScan: true,
  enableCostEstimation: false,
});

project.synth();
```

### 3. Synthesize

```bash
npx projen
```

This generates `.github/workflows/apistack-cdk-diff.yml`, `databasestack-cdk-diff.yml`, and `networkstack-cdk-diff.yml`.

### 4. Add the AWS secret

In your repository settings, add `AWS_ROLE_ARN` as a secret pointing to an IAM role with read-only permissions to run `cdk diff`.

---

## Configuration Reference

All fields in `CdkInfraGuardianOptions` are optional with sensible defaults.

| Field | Type | Default | Description |
|---|---|---|---|
| `stacks` | `string[]` | `[]` | CDK stack names to generate workflows for. |
| `cdkApp` | `string` | `'bin/app.ts'` | CDK app entry point passed to `cdk synth`. |
| `cdkOut` | `string` | `'cdk.out'` | CDK cloud assembly output directory. |
| `failOnDestructiveChanges` | `boolean` | `true` | Fail the CI job when destructive changes are detected. |
| `enableCostEstimation` | `boolean` | `false` | Run Infracost CLI and post cost delta in the PR comment. |
| `enableSecurityScan` | `boolean` | `true` | Parse cdk-nag output and post security findings. |
| `runsOn` | `string` | `'ubuntu-latest'` | GitHub Actions runner image label. |
| `nodeVersion` | `string` | `'20'` | Node.js version for `actions/setup-node`. |
| `awsRegion` | `string` | `'us-east-1'` | AWS region for credential configuration. |
| `cdkContext` | `Record<string, string>` | `{}` | CDK context key-value pairs injected via `--context`. |
| `environmentVariables` | `Record<string, string>` | `{}` | Additional environment variables for the workflow. |
| `installCommand` | `string` | `'npm ci'` | Command to install Node.js dependencies. |
| `autoBootstrap` | `boolean` | `false` | Run `cdk bootstrap` before the diff step. |
| `maxParallelStacks` | `number` | `4` | Maximum number of stacks to diff in parallel (1–20). |
| `commentTag` | `string` | `'cdk-infra-guardian'` | Prefix for the hidden PR comment upsert marker. |
| `triggerPaths` | `string[]` | `['src/**','lib/**','bin/**','cdk.json']` | Glob patterns that trigger the workflow on PRs. |
| `permissions` | `Record<string, string>` | `{}` | Override default job permissions. |

---

## Generated Workflow Example

```yaml
# ~~ Generated by cdk-infra-guardian. To modify, edit your .projenrc.ts ~~

name: "CDK Diff — ApiStack"

on:
  pull_request:
    branches: [main]
    paths:
      - "src/**"
      - "lib/**"
      - "bin/**"
      - "cdk.json"

permissions:
  contents: read
  pull-requests: write
  id-token: write

concurrency:
  group: cdk-diff-${{ github.head_ref }}-apistack
  cancel-in-progress: true

jobs:
  diff:
    name: "Infrastructure Diff — ApiStack"
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: us-east-1

      - name: CDK Synth
        run: npx cdk synth ApiStack --quiet

      - name: CDK Diff
        id: diff
        run: |
          set +e
          DIFF_OUTPUT=$(npx cdk diff ApiStack --change-set --no-color 2>&1)
          DIFF_EXIT_CODE=$?
          echo "exit_code=$DIFF_EXIT_CODE" >> $GITHUB_OUTPUT
          echo "$DIFF_OUTPUT" > /tmp/cdk-diff-output.txt
          if [ $DIFF_EXIT_CODE -eq 2 ]; then
            echo "::error::CDK diff failed with exit code 2"
            exit 1
          fi

      - name: Analyze and Comment
        if: always() && steps.diff.outcome != 'cancelled'
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            # (inline analysis script)
```

---

## PR Comment Preview

When `cdk diff` detects changes, the component posts a comment like this:

```markdown
<!-- cdk-infra-guardian::ApiStack -->
## 🏗️ CDK Infrastructure Diff — `ApiStack`

### Summary
| | Count |
|---|---|
| 🟢 Added | 2 |
| 🟡 Modified | 1 |
| 🔴 Removed | 0 |

### Resource Changes
<details>
<summary>View 3 changes</summary>

🟢 `AWS::Lambda::Function` **ApiHandler**
🟢 `AWS::IAM::Role` **ApiHandlerRole**
🟡 `AWS::DynamoDB::Table` **UserTable**

</details>

### 🔒 Security Scan ❌
| Severity | Count |
|---|---|
| 🔴 HIGH | 1 |
| 🟡 MEDIUM | 2 |
| 🟢 LOW | 0 |

### 💰 Cost Impact
| Resource | Action | Monthly Δ |
|---|---|---|
| RDS db.t3.medium | 🟢 | +45.00 USD/mo |
| NAT Gateway | 🔴 | -32.00 USD/mo |
| **Total** | | **+13.00 USD/mo** |

---
<sub>Generated by cdk-infra-guardian • 2026-03-14T12:00:00.000Z</sub>
```

---

## Architecture

```
┌─────────────────────────────────────────────────────┐
│               CdkInfraGuardian (Projen Component)   │
│                                                     │
│  ┌──────────────┐  ┌──────────────────────────────┐ │
│  │ WorkflowGen  │──│ .github/workflows/*.yml      │ │
│  └──────┬───────┘  └──────────────────────────────┘ │
│         │                                           │
│  ┌──────▼───────┐                                   │
│  │ CDK Construct│  (optional L2 construct wrapper)  │
│  └──────┬───────┘                                   │
│         │                                           │
│  ┌──────▼───────┐  ┌──────────┐  ┌──────────────┐  │
│  │ DiffParser   │──│ Security │──│ CostEstimator│  │
│  └──────┬───────┘  │ Analyzer │  └──────┬───────┘  │
│         │          └──────────┘         │           │
│  ┌──────▼───────────────────────────────▼────────┐  │
│  │          GitHubCommentManager                 │  │
│  │  (find-or-update, markdown assembly)          │  │
│  └───────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────┘
```

**Data flow at PR time:**
1. `cdk diff --change-set --no-color` → raw stdout
2. `DiffParser.parse(stdout)` → `DiffResult[]`
3. `SecurityAnalyzer.scan(diffResults)` → `SecurityFinding[]`
4. `CostEstimator.estimate(diffResults)` → `CostReport | null`
5. `GitHubCommentManager.render(...)` → markdown string
6. `GitHubCommentManager.upsert(markdown)` → GitHub API call

---

## Multi-Stack / Monorepo Usage

For large monorepos with many stacks, configure `maxParallelStacks` to control concurrency:

```typescript
new CdkInfraGuardian(project, {
  stacks: [
    'VpcStack',
    'DatabaseStack',
    'AuthStack',
    'ApiStack',
    'FrontendStack',
    'MonitoringStack',
  ],
  maxParallelStacks: 6,
  triggerPaths: ['infrastructure/**', 'shared/**', 'cdk.json'],
});
```

Each stack gets its own workflow file with an independent concurrency group, so:
- Different stacks run in parallel.
- A new push cancels only the stale run for the same stack.
- PR comments are per-stack and never overwrite each other.

---

## Security Scanning

`cdk-infra-guardian` automatically scans the CDK cloud assembly for cdk-nag findings after synthesis. Place your `NagPack` in your CDK app:

```typescript
import { App } from 'aws-cdk-lib';
import { AwsSolutionsChecks } from 'cdk-nag';
import { Aspects } from 'constructs';

const app = new App();
Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));
```

When the workflow runs, the component:
1. Looks for `*.nag.json` files in the `cdk.out` directory.
2. Maps rule IDs to severity: `AwsSolutions-S1/IAM5/KMS5` → HIGH, other `AwsSolutions-*` → MEDIUM, `HIPAA-`/`NIST-`/`PCI-` prefixes → HIGH.
3. Posts findings in the PR comment with a pass/fail indicator.

Configure with `enableSecurityScan: false` to disable.

---

## Cost Estimation

When `enableCostEstimation: true`, the workflow runs `infracost diff` against the synthesized cloud assembly and posts a monthly cost delta table in the PR comment.

**Prerequisites:**
- Install the [Infracost CLI](https://www.infracost.io/docs/) on your runner, or add it to your workflow.
- Set `INFRACOST_API_KEY` as a repository secret.

The component degrades gracefully — if `infracost` is not in `$PATH`, cost estimation is skipped silently and no error is raised.

---

## Destructive Change Detection

The following resource types are classified as critical. Any `REMOVE` action on these resources will:
1. Appear in the **🚨 Destructive Changes** section of the PR comment.
2. Fail the CI job when `failOnDestructiveChanges: true` (the default).

| Resource Type |
|---|
| `AWS::RDS::DBInstance` |
| `AWS::RDS::DBCluster` |
| `AWS::DynamoDB::Table` |
| `AWS::DynamoDB::GlobalTable` |
| `AWS::S3::Bucket` |
| `AWS::KMS::Key` |
| `AWS::KMS::Alias` |
| `AWS::CloudFront::Distribution` |
| `AWS::ElastiCache::ReplicationGroup` |
| `AWS::EFS::FileSystem` |
| `AWS::Cognito::UserPool` |
| `AWS::ECS::Cluster` |
| `AWS::EKS::Cluster` |
| `AWS::Neptune::DBCluster` |
| `AWS::Redshift::Cluster` |
| `AWS::OpenSearchService::Domain` |
| `AWS::DocDB::DBCluster` |

To override or extend the list, use the exported `CRITICAL_RESOURCE_TYPES` set.

---

## Contributing

Contributions are welcome! Please follow the standard GitHub fork-and-PR workflow.

1. Fork the repository.
2. Create a feature branch: `git checkout -b feat/my-feature`.
3. Make your changes, adding tests for new behaviour.
4. Run `npm test` to verify all tests pass.
5. Run `npx projen` after changing `.projenrc.ts`.
6. Submit a pull request with a clear description of the change.

**Code quality requirements:**
- No `any` types — use `unknown` and narrow explicitly.
- Named exports only — no default exports.
- Every public function must have a JSDoc `@example`.
- All tests must use descriptive `it('should ...')` names.

---

## License

Apache-2.0 — see [LICENSE](LICENSE).