diff --git a/.github/workflows/security-guardian.yml b/.github/workflows/security-guardian.yml index 64262b4f0ba36..f2c0136604955 100644 --- a/.github/workflows/security-guardian.yml +++ b/.github/workflows/security-guardian.yml @@ -9,42 +9,9 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - fetch-depth: 0 # Fetches full history + fetch-depth: 0 # Required to enable full git diff - - name: Get list of changed .template.json files - id: filter_files - run: | - echo "Getting changed CloudFormation templates..." - mkdir -p changed_templates - - git fetch origin main --depth=1 - - base_sha="${{ github.event.pull_request.base.sha }}" - head_sha="${{ github.event.pull_request.head.sha }}" - if [[ -z "$base_sha" ]]; then base_sha=$(git merge-base origin/main HEAD); fi - if [[ -z "$head_sha" ]]; then head_sha=HEAD; fi - - git diff --name-status "$base_sha" "$head_sha" \ - | grep -E '^(A|M)\s+.*\.template\.json$' \ - | awk '{print $2}' > changed_files.txt || true - - while IFS= read -r file; do - if [ -f "$file" ]; then - safe_name=$(echo "$file" | sed 's|/|_|g') - cp "$file" "changed_templates/$safe_name" - else - echo "::warning::Changed file not found in workspace: $file" - fi - done < changed_files.txt - - if [ -s changed_files.txt ]; then - echo "files_changed=true" >> $GITHUB_OUTPUT - else - echo "files_changed=false" >> $GITHUB_OUTPUT - fi - - name: Install cfn-guard - if: steps.filter_files.outputs.files_changed == 'true' run: | mkdir -p $HOME/.local/bin curl -L -o cfn-guard.tar.gz https://github.com/aws-cloudformation/cloudformation-guard/releases/latest/download/cfn-guard-v3-x86_64-ubuntu-latest.tar.gz @@ -52,16 +19,15 @@ jobs: mv cfn-guard-v3-*/cfn-guard $HOME/.local/bin/cfn-guard chmod +x $HOME/.local/bin/cfn-guard echo "$HOME/.local/bin" >> $GITHUB_PATH - + - name: Install & Build security-guardian - if: steps.filter_files.outputs.files_changed == 'true' run: yarn install --frozen-lockfile && cd tools/@aws-cdk/security-guardian && yarn build - - name: Run cfn-guard if templates changed - if: steps.filter_files.outputs.files_changed == 'true' + - name: Run Security Guardian uses: ./tools/@aws-cdk/security-guardian with: - data_directory: './changed_templates' - rule_set_path: './tools/@aws-cdk/security-guardian/rules/trust_scope_rules.guard' + base_sha: ${{ github.event.pull_request.base.sha }} + head_sha: ${{ github.event.pull_request.head.sha }} + rule_set_path: './tools/@aws-cdk/security-guardian/rules' show_summary: 'fail' output_format: 'single-line-summary' diff --git a/tools/@aws-cdk/security-guardian/README.md b/tools/@aws-cdk/security-guardian/README.md index 4adc690bde988..ccc0eea93b677 100644 --- a/tools/@aws-cdk/security-guardian/README.md +++ b/tools/@aws-cdk/security-guardian/README.md @@ -1,41 +1,41 @@ # Security Guardian -A GitHub Action tool designed to - - detect broadly scoped inline policies in `*.template.json` files in incoming PRs and validate changed AWS CloudFormation templates against custom [cfn-guard](https://github.com/aws-cloudformation/cloudformation-guard) rules. Supports local paths for custom rule sets. - - [in future] detect broadly scoped CFN intrinsic statements +A GitHub Action and CLI tool that helps detect broadly scoped IAM principals in CloudFormation templates by: + +- Validating **changed** `*.template.json` files in pull requests using custom [cfn-guard v3](https://github.com/aws-cloudformation/cloudformation-guard) rules. +- Detecting **broadly scoped IAM principals** using CloudFormation **intrinsic functions** (e.g., `Fn::Join` with `:root`). --- ## Features -- Validates only changed `*.template.json` files in PRs -- Supports `cfn-guard v3` -- Accepts rules from a local file or remote URL -- Outputs validation results in summary format + Validates **only changed** templates in a PR + Supports **cfn-guard v3** with rule sets + Scans for **broad IAM principals using intrinsics** + Runs locally and in GitHub Actions + Outputs human-readable and machine-parsable summaries --- -## Inputs - -| Name | Description | Required | Default | -|------------------|-------------------------------------------------------------------|----------|-----------------------| -| `data_directory` | Directory containing templates to validate | Yes | | -| `rule_file_path` | Local path to the rules file | Yes | | -| `show_summary` | Whether to show summary output (`fail`, `warn`, `none`) | No | `fail` | -| `output_format` | Output format (`single-line-summary`, `json`, etc.) | No | `single-line-summary` | +## Inputs (GitHub Action) -> `data_directory` and `rule_file_path` must be set. +| Name | Description | Required | Default | +|------------------|------------------------------------------------------|----------|-----------------------| +| `rule_set_path` | Local path to the cfn-guard rules file | Yes | | +| `show_summary` | Show summary (`fail`, `warn`, or `none`) | No | `fail` | +| `output_format` | Output format (`single-line-summary`, `json`, etc.) | No | `single-line-summary` | +| `base_sha` | Commit SHA to compare against | No | `origin/main` | +| `head_sha` | The commit SHA for the head (current) branch or PR | No | `HEAD` | --- -## Usage +## Usage (GitHub Action) ```yaml -- name: Run CFN Guard +- name: Run Security Guardian uses: ./tools/@aws-cdk/security-guardian with: - data_directory: './changed_templates' - rule_set_path: './tools/@aws-cdk/security-guardian/rules/trust_scope_rules.guard' + rule_set_path: './tools/@aws-cdk/security-guardian/rules' show_summary: 'fail' output_format: 'single-line-summary' ``` @@ -44,28 +44,52 @@ A GitHub Action tool designed to ## Local Development -### 1. Build +### 1. Install Dependencies ```bash -npm install -npm run build +cd tools/@aws-cdk/security-guardian && yarn install ``` ### 2. Run Locally +The tool automatically detects changed templates and validates them. + ```bash -node dist/index.js \ - --data_directory=./changed_templates \ - --rule_file_path=./rules.guard \ - --output_format=single-line-summary \ - --show_summary=fail +yarn security-guardian ``` +> You can override defaults using: +> - `--base_sha=origin/main` +> - `--output_format=json` +> - `--show_summary=warn` + --- -## Acknowledgments +## Output + +In addition to validation results from `cfn-guard`, the tool logs detailed findings from the intrinsic scan (if applicable), such as: -Built on top of [cfn-guard](https://github.com/aws-cloudformation/cloudformation-guard) and [GitHub Actions Toolkit](https://github.com/actions/toolkit). +``` +detailed_output File: changed_templates/example.template.json +{ + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + ["arn:", { "Ref": "AWS::Partition" }, ":iam::", { "Ref": "AWS::AccountId" }, ":root"] + ] + } + }, + "Resource": "*" +} +``` --- -Happy Guarding! +## Acknowledgments + +Built with care on top of [cfn-guard](https://github.com/aws-cloudformation/cloudformation-guard) and the [GitHub Actions Toolkit](https://github.com/actions/toolkit). + +--- +Happy Guarding! diff --git a/tools/@aws-cdk/security-guardian/package-lock.json b/tools/@aws-cdk/security-guardian/package-lock.json index a37a578ff2db0..4dd4f71010839 100644 --- a/tools/@aws-cdk/security-guardian/package-lock.json +++ b/tools/@aws-cdk/security-guardian/package-lock.json @@ -12,8 +12,9 @@ "@actions/exec": "^1.1.1" }, "devDependencies": { - "@types/node": "^22.14.0", - "typescript": "^5.2.2" + "@types/node": "^22.14.1", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" } }, "node_modules/@actions/core": { @@ -51,6 +52,19 @@ "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", "license": "MIT" }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -60,16 +74,173 @@ "node": ">=14" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "22.14.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", - "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", @@ -111,6 +282,23 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } } } } diff --git a/tools/@aws-cdk/security-guardian/package.json b/tools/@aws-cdk/security-guardian/package.json index 350e84e92c96d..64180932070e4 100644 --- a/tools/@aws-cdk/security-guardian/package.json +++ b/tools/@aws-cdk/security-guardian/package.json @@ -4,14 +4,16 @@ "main": "dist/index.js", "scripts": { "build": "tsc", - "prepare": "npm run build" + "prepare": "npm run build", + "security-guardian": "ts-node src/index.ts --rule_set_path=./rules" }, "dependencies": { "@actions/core": "^1.10.0", "@actions/exec": "^1.1.1" }, "devDependencies": { - "@types/node": "^22.14.0", - "typescript": "^5.2.2" + "@types/node": "^22.14.1", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" } } diff --git a/tools/@aws-cdk/security-guardian/src/check-intrinsics.ts b/tools/@aws-cdk/security-guardian/src/check-intrinsics.ts new file mode 100644 index 0000000000000..f832674fbc0b1 --- /dev/null +++ b/tools/@aws-cdk/security-guardian/src/check-intrinsics.ts @@ -0,0 +1,111 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as core from '@actions/core'; + +export async function runScan(dataDir: string) { + let issuesFound = 0; + let matches: Array<{ filePath: string, statements: any[] }> = []; + let totalFiles = 0; + + function isRootPrincipal(statement: any): boolean { + if (typeof statement !== 'object' || statement == null) return false; + if (statement.Effect !== 'Allow') return false; + + const principal = statement.Principal; + if (typeof principal !== 'object' || principal == null) return false; + + const awsPrincipal = principal.AWS; + if (typeof awsPrincipal === 'object' && awsPrincipal['Fn::Join']) { + const joinArgs = awsPrincipal['Fn::Join']; + if (Array.isArray(joinArgs) && joinArgs.length === 2) { + const parts = joinArgs[1]; + if (Array.isArray(parts)) { + const joined = parts.map(p => typeof p === 'string' ? p : '').join(''); + return joined.endsWith(':root'); + } + } + } + + return false; + } + + function findMatchingStatements(obj: any): any[] { + const results: any[] = []; + + if (Array.isArray(obj)) { + for (const item of obj) { + results.push(...findMatchingStatements(item)); + } + } else if (typeof obj === 'object' && obj !== null) { + for (const [key, value] of Object.entries(obj)) { + if (key === 'Statement') { + const stmts = Array.isArray(value) ? value : [value]; + for (const stmt of stmts) { + if (isRootPrincipal(stmt)) { + results.push(stmt); + } + } + } else { + results.push(...findMatchingStatements(value)); + } + } + } + + return results; + } + + function walkDir(dir: string, fileCallback: (filePath: string) => void) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + walkDir(fullPath, fileCallback); + } else if (entry.isFile() && entry.name.endsWith('.json')) { + fileCallback(fullPath); + } + } + } + + core.info(`Scanning JSON files in: ${dataDir}`); + + walkDir(dataDir, filePath => { + totalFiles++; + core.info(`Processing: ${filePath}`); + let data; + + try { + const content = fs.readFileSync(filePath, 'utf8'); + data = JSON.parse(content); + } catch (err) { + core.warning(`Skipping ${filePath}: ${(err as Error).message}`); + return; + } + + const found = findMatchingStatements(data); + if (found.length > 0) { + core.info(`Match found in: ${filePath} (statements: ${found.length})`); + matches.push({ filePath, statements: found }); + issuesFound += found.length; + } + }); + + // Build human-readable detailed output + let detailedOutput = ''; + for (const match of matches) { + detailedOutput += `File: ${match.filePath}\n`; + for (const stmt of match.statements) { + detailedOutput += `${JSON.stringify(stmt, null, 2)} |n| `; + } + detailedOutput += '='.repeat(60) + '\n'; + } + + // Set the output for GitHub Actions + core.info(`detailed_output ${detailedOutput.trim()}`); + + core.info('\n Scan complete!'); + core.info(` Files scanned : ${totalFiles}`); + core.info(` Matches found : ${matches.length}`); + + return issuesFound; +} diff --git a/tools/@aws-cdk/security-guardian/src/get-changed-files.ts b/tools/@aws-cdk/security-guardian/src/get-changed-files.ts new file mode 100644 index 0000000000000..f9b8e38f1106c --- /dev/null +++ b/tools/@aws-cdk/security-guardian/src/get-changed-files.ts @@ -0,0 +1,39 @@ +import * as core from '@actions/core'; +import * as exec from '@actions/exec'; +import * as fs from 'fs'; +import * as path from 'path'; + +export async function detectChangedTemplates(baseSha: string, headSha: string, outputDir: string): Promise { + core.info(`Detecting changed .template.json files from ${baseSha} to ${headSha}`); + + let stdout = ''; + await exec.exec('git', ['diff', '--name-status', `${baseSha}`, `${headSha}`], { + listeners: { + stdout: (data: Buffer) => { + stdout += data.toString(); + } + } + }); + + const changed = stdout + .split('\n') + .filter(line => /^(A|M)\s+.*\.template\.json$/.test(line)) + .map(line => line.trim().split(/\s+/)[1]); + + if (changed.length === 0) return false; + + for (const file of changed) { + const repoRoot = await exec.getExecOutput('git', ['rev-parse', '--show-toplevel']); + const fullPath = path.join(repoRoot.stdout.trim(), file); + console.log('fullpath:', fullPath); + if (fs.existsSync(fullPath)) { + const safeName = file.replace(/\//g, '_'); + fs.copyFileSync(fullPath, path.join(outputDir, safeName)); + core.info(`Copied: ${file}`); + } else { + core.warning(`Changed file not found: ${file}`); + } + } + + return true; +} diff --git a/tools/@aws-cdk/security-guardian/src/index.ts b/tools/@aws-cdk/security-guardian/src/index.ts index 820abd3f7eeb3..6105c6c3b339c 100644 --- a/tools/@aws-cdk/security-guardian/src/index.ts +++ b/tools/@aws-cdk/security-guardian/src/index.ts @@ -1,30 +1,117 @@ import * as core from '@actions/core'; import * as exec from '@actions/exec'; +import { detectChangedTemplates } from './get-changed-files'; +import { runScan } from './check-intrinsics'; +import * as fs from 'fs'; +import * as path from 'path'; + +function setupWorkingDir(dir: string) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + core.info(`Created working directory: ${dir}`); + } else { + const files = fs.readdirSync(dir); + for (const file of files) { + fs.unlinkSync(path.join(dir, file)); + } + core.info(`Cleaned working directory: ${dir}`); + } +} + +function cleanupWorkingDir(dir: string) { + try { + if (fs.existsSync(dir)) { + const files = fs.readdirSync(dir); + for (const file of files) { + fs.unlinkSync(path.join(dir, file)); + } + fs.rmdirSync(dir); + core.info(`Removed working directory: ${dir}`); + } + } catch (err) { + core.warning(`Cleanup failed for ${dir}: ${(err as Error).message}`); + } +} + +function getInput(name: string, options?: { required?: boolean, default?: string }): string { + // GitHub Actions input + const actionInput = core.getInput(name); + if (actionInput) return actionInput; + + // Local CLI arg + const cliArg = process.argv.find(arg => arg.startsWith(`--${name}=`)); + if (cliArg) return cliArg.split('=')[1]; + + // Default fallback + if (options?.default !== undefined) return options.default; + + if (options?.required) { + throw new Error(`Fatal error: Input '${name}' must be provided.`); + } + + return ''; +} async function run(): Promise { + const errors: string[] = []; + let workingDir: string = './changed_templates'; + try { - const dataDir = core.getInput('data_directory'); - const ruleSetPath = core.getInput('rule_set_path'); - const showSummary = core.getInput('show_summary') || 'fail'; - const outputFormat = core.getInput('output_format') || 'single-line-summary'; + const ruleSetPath = getInput('rule_set_path'); + const baseSha = getInput('base_sha', { default: 'origin/main' }); + const headSha = getInput('head_sha', { default: 'HEAD' }); + const outputFormat = getInput('output_format', { default: 'single-line-summary' }); + const showSummary = getInput('show_summary', { default: 'fail' }); + setupWorkingDir(workingDir); + + const filesChanged = await detectChangedTemplates(baseSha, headSha, workingDir); + if (!filesChanged) { + core.info('No template files changed. Skipping validation.'); + return; + } + + if (!ruleSetPath) throw new Error("Input 'rule_set_path' must be provided."); - if (!ruleSetPath) { - throw new Error(" 'rule_set_path' input must be provided."); + core.info(`Running cfn-guard with rule set: ${ruleSetPath}`); + try { + await exec.exec('cfn-guard', [ + 'validate', + '--data', workingDir, + '--rules', ruleSetPath, + '--show-summary', showSummary, + '--output-format', outputFormat + ]); + } catch (err) { + const message = `cfn-guard validation failed: ${(err as Error).message}`; + core.warning(message); + errors.push(message); } - let rulePathToUse = ruleSetPath; + core.info(`Running scanner for intrinsics`); + try { + const issuesFound = await runScan(workingDir); + if (issuesFound > 0) { + const msg = `Intrinsic scan found ${issuesFound} issue(s).`; + core.warning(msg); + core.setOutput('issues_found', issuesFound); + errors.push(msg); + } + } catch (err) { + const message = `Intrinsic scan failed: ${(err as Error).message}`; + core.warning(message); + errors.push(message); + } - core.info(`Running cfn-guard with rule set: ${rulePathToUse}`); + if (errors.length > 0) { + core.setFailed(`Action completed with issues: ${errors}`); + } else { + core.info('All validations passed.'); + } - await exec.exec('cfn-guard', [ - 'validate', - '--data', dataDir, - '--rules', rulePathToUse, - '--show-summary', showSummary, - '--output-format', outputFormat - ]); - } catch (error) { - core.setFailed((error as Error).message); + } catch (fatal) { + core.setFailed(`Fatal error: ${(fatal as Error).message}`); + } finally { + cleanupWorkingDir(workingDir); } } diff --git a/tools/@aws-cdk/security-guardian/test/templates/CMCMK-Stack.template.json b/tools/@aws-cdk/security-guardian/test/templates/CMCMK-Stack.template.json index 3adb8822a448f..eed2f7244155d 100644 --- a/tools/@aws-cdk/security-guardian/test/templates/CMCMK-Stack.template.json +++ b/tools/@aws-cdk/security-guardian/test/templates/CMCMK-Stack.template.json @@ -1,6 +1,6 @@ { "Resources": { - "LambdaExecutionRoleD5C26073": { + "LambdaExecutionRoleD5C26073": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { diff --git a/tools/@aws-cdk/security-guardian/test/test.sh b/tools/@aws-cdk/security-guardian/test/test.sh new file mode 100755 index 0000000000000..af0da9b1a1b7e --- /dev/null +++ b/tools/@aws-cdk/security-guardian/test/test.sh @@ -0,0 +1,3 @@ +INPUT_DATA_DIRECTORY=./test/templates \ +INPUT_RULE_SET_PATH=./rules/trust_scope_rules.guard \ +npx ts-node src/index.ts