Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion .github/workflows/pr-linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ jobs:
echo "pr_sha=$(cat pr/pr_sha)" >> "$GITHUB_OUTPUT"

validate-pr:
# Necessary to have sufficient permissions to write to the PR
permissions:
contents: read
pull-requests: write
Expand All @@ -68,9 +67,13 @@ jobs:
checks: read
runs-on: ubuntu-latest
needs: download-if-workflow-run

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ needs.download-if-workflow-run.outputs.pr_sha }}

- name: Install & Build prlint
run: yarn install --frozen-lockfile && cd tools/@aws-cdk/prlint && yarn build+test
Expand All @@ -83,3 +86,46 @@ jobs:
PR_SHA: ${{ needs.download-if-workflow-run.outputs.pr_sha }}
LINTER_LOGIN: ${{ vars.LINTER_LOGIN }}
REPO_ROOT: ${{ github.workspace }}

# ---------- Your Custom cfn-guard Integration ----------
- 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: Run cfn-guard if templates changed
if: steps.filter_files.outputs.files_changed == 'true'
uses: ./tools/@aws-cdk/cfn-guard-custom-rules-tool
with:
data_directory: './changed_templates'
rule_file_path: './tools/@aws-cdk/cfn-guard-custom-rules-tool/rules/trust_scope_rules.guard'
show_summary: 'fail'
output_format: 'single-line-summary'

5 changes: 5 additions & 0 deletions tools/@aws-cdk/cfn-guard-custom-rules-tool/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*.js
*.js.map
*.d.ts
dist
node_modules
27 changes: 27 additions & 0 deletions tools/@aws-cdk/cfn-guard-custom-rules-tool/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: 'cfn-guard-custom-rules-tool'
description: 'CFN Guard for custom or granular guard rules'
author: QuantumNeuralCoder

inputs:
data_directory:
description: "Path to CloudFormation templates"
required: true
rule_set_url:
description: "URL to a single .guard file on GitHub"
required: true
show_summary:
description: "cfn-guard summary output. Options are all, pass, fail, skip or none"
required: false
default: "fail"
output_format:
description: "cfn-guard output format. Options: json, yaml, single-line-summary"
required: false
default: "single-line-summary"

runs:
using: node20
main: dist/index.js

branding:
icon: shield
color: red
116 changes: 116 additions & 0 deletions tools/@aws-cdk/cfn-guard-custom-rules-tool/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions tools/@aws-cdk/cfn-guard-custom-rules-tool/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "cfn-guard-custom-rules-tool",
"version": "1.0.0",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"prepare": "npm run build"
},
"dependencies": {
"@actions/core": "^1.10.0",
"@actions/exec": "^1.1.1"
},
"devDependencies": {
"@types/node": "^22.14.0",
"typescript": "^5.2.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Trust Scope Security Rules
# This rule file checks for overly broad trust scopes in IAM resources

# Rule to check for overly permissive IAM role trust policies
rule iam_role_trust_policy_not_overly_permissive {
AWS::IAM::Role {
Properties exists
Properties is_struct

Properties.AssumeRolePolicyDocument exists
Properties.AssumeRolePolicyDocument is_struct

Properties.AssumeRolePolicyDocument {
Statement exists
Statement is_list

# For each statement in the policy
Statement[*] {
# Check if Principal is overly permissive
when Principal exists {
# Check if Principal is a string (direct "*" case)
when Principal is_string {
Principal != "*"
}

# Check if AWS principal exists
when Principal.AWS exists {
# Check if AWS is a string
when Principal.AWS is_string {
Principal.AWS != "*"
Principal.AWS != /(?i):root/
}
}
}
}
}
}
}

61 changes: 61 additions & 0 deletions tools/@aws-cdk/cfn-guard-custom-rules-tool/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import * as fs from 'fs';
import * as path from 'path';
import * as https from 'https';
import * as os from 'os';

async function downloadFile(url: string, destination: string): Promise<void> {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(destination);
https.get(url, (res) => {
if (res.statusCode !== 200) {
reject(new Error(`Failed to get '${url}' (${res.statusCode})`));
return;
}
res.pipe(file);
file.on('finish', () => {
file.close(); // Close is synchronous, no need to wait
resolve();
});
}).on('error', (err) => {
fs.unlink(destination, () => reject(err));
});
});
}

async function run(): Promise<void> {
try {
const dataDir = core.getInput('data_directory');
const ruleSetPath = core.getInput('rule_set_path'); // optional
const ruleSetUrl = core.getInput('rule_set_url'); // optional
const showSummary = core.getInput('show_summary') || 'fail';
const outputFormat = core.getInput('output_format') || 'single-line-summary';

if (!ruleSetPath && !ruleSetUrl) {
throw new Error("Either 'rule_set_path' or 'rule_set_url' input must be provided.");
}

let rulePathToUse = ruleSetPath;

if (!rulePathToUse && ruleSetUrl) {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rules-'));
rulePathToUse = path.join(tempDir, 'rules.guard');
await downloadFile(ruleSetUrl, rulePathToUse);
}

core.info(`Running cfn-guard with rule set: ${rulePathToUse}`);

await exec.exec('cfn-guard', [
'validate',
'--data', dataDir,
'--rules', rulePathToUse,
'--show-summary', showSummary,
'--output-format', outputFormat
]);
} catch (error) {
core.setFailed((error as Error).message);
}
}

run();
Loading
Loading