6 views
An AI review bot with a classic Personal Access Token (repo scope) and bedrock:* on the account is not a productivity win — it is a credential that can push to main, exfiltrate private repos, and run up a five-figure model bill. Production AI code review bot IAM is boring on purpose: short-lived GitHub App installation tokens, IAM roles that can invoke one model and write logs, and a pipeline that only ever sees the PR diff + allowlisted paths.
⚡ TL;DR: Treat AI code review bot IAM as a security product: run the bot as a GitHub App (not a user PAT). Exchange the App JWT for an installation token inside the job; never store that token in env files or DynamoDB. Give the AWS task roleRelated hardening: IAM for AI agents, Lambda coding agent sandboxes, and Bedrock prompt caching for static rubrics.bedrock:InvokeModelon specific model ARNs,secretsmanager:GetSecretValueon one secret, and CloudWatch put-metric/log — nothing else. Fetch only the PR files you need; redact secrets before the prompt; post reviews via the Checks/PR review API. Illustrative budget: review job wall clock 30–180s.
AI code review bot IAM: threat model in one page
| Asset | Abuse if bot is over-privileged | Control |
|---|---|---|
| GitHub org repos | Force-push, exfil, workflow poisoning | GitHub App with contents:read, pull_requests:write only |
| Bedrock | Cost bomb / data to wrong model | IAM resource-level model ARNs + SCPs |
| Secrets | Cloud foothold | One secret ARN; no wildcard secretsmanager:* |
| Prompt content | Leaked API keys inside diffs | Pre-prompt secret scanning + Guardrails |
| CI identity | Lateral movement in AWS | Separate deploy role ≠ review role |
✅ App installation token minted per workflow run, expires in ~1 hour.
Pipeline shape
text
pull_request opened/synchronize
→ GitHub Actions (OIDC) assumes AwsCodeReviewRole
→ Job reads App private key from Secrets Manager (or GH encrypted secret → still mint install token)
→ Fetch PR diff via GitHub API (installation token)
→ Redact + truncate
→ Bedrock Converse with cached review rubric
→ POST PR review comments
→ Emit cost + latency metrics
OIDC to AWS (no long-lived AKIA keys in Actions)
yaml
# .github/workflows/ai-review.yml
name: ai-code-review
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
id-token: write # OIDC
contents: read
pull-requests: write
jobs:
review:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
.
fetch-depth: 0
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/AwsCodeReviewRole
aws-region: us-east-1
- name: Run reviewer
run: node scripts/ai-review.mjs
env:
GH_APP_SECRET_ARN: arn:aws:secretsmanager:us-east-1:123456789012:secret:github/code-review-app
BEDROCK_MODEL_ID: anthropic.claude-3-5-sonnet-...
MAX_DIFF_BYTES: "120000"
hcl
# terraform — trust GitHub OIDC for this repo only
resource "aws_iam_role" "code_review" {
name = "AwsCodeReviewRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = aws_iam_openid_connect_provider.github.arn }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:my-org/my-repo:*"
}
}
}]
})
}
resource "aws_iam_role_policy" "code_review" {
name = "code-review-least-privilege"
role = aws_iam_role.code_review.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "InvokeOneModel"
Effect = "Allow"
Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"]
Resource = [
"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-*"
]
},
{
Sid = "ReadAppSecret"
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
Resource = [aws_secretsmanager_secret.gh_app.arn]
},
{
Sid = "Logs"
Effect = "Allow"
Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
Resource = ["arn:aws:logs:us-east-1:123456789012:log-group:/ai/code-review:*"]
}
]
})
}
Mint the GitHub token in-process
javascript
GitHub App permissions to prefer:
// scripts/ai-review.mjs (illustrative)
import { createSign } from "crypto";
import {
SecretsManagerClient,
GetSecretValueCommand,
} from "@aws-sdk/client-secrets-manager";
import {
BedrockRuntimeClient,
ConverseCommand,
} from "@aws-sdk/client-bedrock-runtime";
const sm = new SecretsManagerClient({});
const br = new BedrockRuntimeClient({});
const secret = JSON.parse(
(
await sm.send(
new GetSecretValueCommand({ SecretId: process.env.GH_APP_SECRET_ARN })
)
).SecretString
);
const installationToken = await mintInstallationToken(secret);
// ✅ use for GitHub API only; ❌ never console.log(installationToken)
// ✅ never pass token into Bedrock messages
const diff = await fetchPrDiff(installationToken, {
owner: process.env.GITHUB_REPOSITORY_OWNER,
repo: process.env.GITHUB_REPOSITORY.split("/")[1],
pull_number: Number(process.env.PR_NUMBER),
});
const safeDiff = redactSecrets(truncate(diff, Number(process.env.MAX_DIFF_BYTES)));
const review = await runBedrockReview(safeDiff);
await postReview(installationToken, review);
async function mintInstallationToken(secret) {
const now = Math.floor(Date.now() / 1000);
const payload = { iat: now - 60, exp: now + 540, iss: String(secret.app_id) };
const header = Buffer.from(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString("base64url");
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
const signer = createSign("RSA-SHA256");
signer.update(`${header}.${body}`);
const sig = signer.sign(secret.private_key, "base64url");
const appJwt = `${header}.${body}.${sig}`;
const res = await fetch(
`https://api.github.com/app/installations/${secret.installation_id}/access_tokens`,
{
method: "POST",
headers: {
Authorization: `Bearer ${appJwt}`,
Accept: "application/vnd.github+json",
},
}
);
if (!res.ok) throw new Error(`token_mint_failed:${res.status}`);
const json = await res.json();
return json.token;
}
- ✅
Pull requests: Read & write(to post reviews) - ✅
Contents: Read(to read files if you must) - ✅
Checks: Write(optional status) - ❌
Administration,Actions: Write,Secrets,Workflows— not needed for review comments
Redact before the model sees the diff
Diffs routinely contain.env additions, service keys, and customer identifiers. Strip before Bedrock.
javascript
Pair with Bedrock Guardrails on the invoke path for PII filters if reviews may include customer data from fixtures.
function redactSecrets(text) {
return text
// illustrative patterns — extend with your secret scanner
.replace(/(?<=AKIA)[0-9A-Z]{16}/g, "********")
.replace(/(?<=ghp_)[A-Za-z0-9]{20,}/g, "********")
.replace(/(?<=xox[baprs]-)[A-Za-z0-9-]{10,}/g, "********")
.replace(/(?<=Bearer\s+)[A-Za-z0-9._\-+/=]{20,}/gi, "********")
.replace(/(?<=password\s*[:=]\s*)\S+/gi, "********");
}
function truncate(text, maxBytes) {
const buf = Buffer.from(text, "utf8");
if (buf.length <= maxBytes) return text;
// ✅ keep the head of the diff; ❌ don’t silently drop security-sensitive files without noting truncation
return buf.subarray(0, maxBytes).toString("utf8") + "\n\n[TRUNCATED]";
}
Bedrock call with a cached rubric
javascript
Illustrative cost control: cache the rubric (hundreds–thousands of tokens) across hundreds of PRs/day; bill the dynamic diff at full input rates only. Cap const RUBRIC = `You are a senior code reviewer. Focus on:
- correctness bugs, authz flaws, injection, unsafe deserialization
- missing tests for changed control flow
- Do NOT nitpick formatting.
Return JSON: { "summary": string, "findings": [{"path":string,"line":number,"severity":"high"|"medium"|"low","note":string}] }`;
async function runBedrockReview(diff) {
const out = await br.send(
new ConverseCommand({
modelId: process.env.BEDROCK_MODEL_ID,
system: [
{ text: RUBRIC },
{ cachePoint: { type: "default" } }, // ✅ static rubric cached across PRs
],
messages: [
{
role: "user",
content: [{ text: `Review this diff:\n\n${diff}` }],
},
],
inferenceConfig: { maxTokens: 2048, temperature: 0 },
})
);
const text = out.output.message.content.map((c) => c.text || "").join("");
return JSON.parse(text);
}
MAX_DIFF_BYTES so a 20MB generated-file PR cannot become a surprise invoice.
Posting reviews without over-sharing
javascript
async function postReview(token, review) {
// Map findings to GitHub pull request review comments API
// ✅ Only comment on lines in the diff hunk
// ❌ Don’t request changes for “style” — use severity gates
const event =
review.findings.some((f) => f.severity === "high") ? "REQUEST_CHANGES" : "COMMENT";
await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${pull}/reviews`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github+json",
},
body: JSON.stringify({
body: review.summary,
event,
comments: review.findings
.filter((f) => f.severity !== "low")
.slice(0, 20) // ✅ spam cap
.map((f) => ({
path: f.path,
line: f.line,
body: `**${f.severity}**: ${f.note}`,
})),
}),
});
}
Closing checklist
✅ Dos – ✅ GitHub App + per-run installation tokens; OIDC into a review-only IAM role – ✅ Resource-scopedbedrock:InvokeModel on known model ARNs
– ✅ Redact secrets and cap diff size before prompting
– ✅ Cache the static review rubric; log token usage per PR
– ✅ Separate the deploy/terraform role from the review role
❌ Don’ts
– ❌ Don’t put classic repo-scoped PATs in org secrets for bots
– ❌ Don’t grant the review role s3:*, iam:*, or bedrock:* wildcards
– ❌ Don’t send the GitHub token (or AWS creds) into the model context
– ❌ Don’t let the bot approve/merge its own PRs
– ❌ Don’t skip truncation — generated lockfiles and vendor trees will wreck cost and quality
Related reading
- Terraform for Developers: Infrastructure as Code From Zero to Production AWS
- AWS Lambda Best Practices: Write Functions That Scale and Never Time Out
- Hyper-Scale Serverless API Platform on AWS
- LLM Coding Agents on AWS: Safe Tool Sandboxes with Lambda (companion post)
- Bedrock Prompt Caching and Batch Inference (companion post)
- Amazon Bedrock Agents: Tool Use, Memory, and Production Guardrails (companion post)
- AI coding agent tool schemas
- ApplyGuardrail for coding agent tool I/O
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.

Pingback: Bedrock Guardrails: Block Prompt Injection Inside Internal Dev Tools - CheatCoders
Pingback: Amazon Q Developer: Custom Context Packs for Private Monorepos - CheatCoders
Pingback: AI-Written Terraform: Plan Diff Gates Before Any Apply Runs - CheatCoders