Building Diffender: A Serverless AI Security Reviewer for GitHub Pull Requests with Amazon Bedrock

Home » BLOG » Building Diffender: A Serverless AI Security Reviewer for GitHub Pull Requests with Amazon Bedrock

Building Diffender: A Serverless AI Security Reviewer for GitHub Pull Requests with Amazon Bedrock

A pull request can pass its tests, satisfy its acceptance criteria, and still expose a production credential in one line of code. Diffender is an Amazon Bedrock pull request reviewer built to identify that kind of risk before the change reaches the main branch.

However, human review remains essential, and its depth can vary with workload, time pressure, and security experience. Meanwhile, deterministic scanners provide valuable coverage, although their findings are not always presented in a form that developers can immediately understand and act on.

Diffender adds another layer to that review process. It is a serverless GitHub bot that receives pull request events, retrieves the changed code, asks Claude Haiku 4.5 on Amazon Bedrock to review the diff for security issues, and posts the findings directly on the pull request.

The name combines two ideas: diff, the code changed in a pull request, and defender, the security reviewer examining those changes.

What Diffender Does

When a developer opens, reopens, or updates a pull request, GitHub sends an HTTP webhook to Diffender. Amazon API Gateway receives the request and invokes an AWS Lambda function.

The function then:

  1. Verifies that GitHub signed the request.
  2. Reads the pull request details from the webhook payload.
  3. Retrieves the pull request diff through the GitHub API.
  4. Sends the diff to Claude Haiku 4.5 through Amazon Bedrock.
  5. Receives a structured security review.
  6. Posts the review as a comment on the pull request.

Diffender security findings posted as a comment on a GitHub pull request

This is deliberately more than a model invocation wrapped in a Lambda function. The project includes webhook authentication, encrypted secret storage, infrastructure as code, restricted permissions, structured prompt design, external API integration, logging, and deployment automation.

How the AI Pull Request Security Reviewer Works

Diffender operates as a serverless code review bot built around an event-driven workflow. When a pull request changes, GitHub sends a signed webhook to Amazon API Gateway. In turn, API Gateway invokes AWS Lambda.

The function verifies the request, retrieves the pull request diff through the GitHub API, sends the changed code to Claude Haiku 4.5 through Amazon Bedrock, and posts the generated security review back to the pull request. In addition, Systems Manager Parameter Store protects the required secrets, while CloudWatch Logs records execution and error details.

Diffender architecture using GitHub, API Gateway, Lambda, Parameter Store, Amazon Bedrock, IAM, and CloudWatch Logs

For a broader introduction to event-driven compute, see the AWS Lambda cheat sheet.

AWS Services Behind the Review Workflow

Diffender uses a small set of managed AWS services, with each service handling a specific part of the pull request review process.

1. Amazon API Gateway

Amazon API Gateway exposes the public HTTPS endpoint that receives webhook events from GitHub. Because it integrates directly with Lambda, Diffender does not need a separate web server.

However, invalid traffic can still reach the endpoint before the Lambda function rejects it through signature validation.

2. AWS Lambda

AWS Lambda runs the main Diffender workflow. It verifies the webhook, retrieves the pull request diff, invokes Amazon Bedrock, and posts the review back to GitHub.

Since pull request activity occurs only when developers open or update code changes, event-driven compute fits the workload better than a continuously running server. Currently, the complete review must finish within the function’s configured timeout.

3. Amazon Bedrock

Amazon Bedrock gives Diffender managed access to Claude Haiku 4.5 without requiring separate model-hosting infrastructure. The Lambda function sends the pull request diff to the model and receives a structured security review in return.

However, language-model output remains probabilistic, so the review may miss issues or include false positives.

4. AWS Systems Manager Parameter Store

AWS Systems Manager Parameter Store holds the GitHub token and webhook secret as encrypted SecureString parameters. As a result, Diffender keeps sensitive values outside the source code and SAM template.

The current version does not rotate these values automatically, so a production deployment would need a separate rotation process.

5. AWS Identity and Access Management

AWS Identity and Access Management controls the AWS actions available to the Lambda function. The execution role grants access to Parameter Store, AWS KMS, and Amazon Bedrock for the review workflow.

Nevertheless, a production version could narrow the Amazon Bedrock and AWS KMS resource scopes further.

6. Amazon CloudWatch Logs

Amazon CloudWatch Logs records Lambda execution details and application errors. During development, these logs helped trace Amazon Bedrock invocation failures, GitHub API permission errors, and unexpected exceptions.

In addition, a production deployment should set an appropriate retention period and avoid writing sensitive values to the logs.

7. AWS Serverless Application Model

Finally, AWS Serverless Application Model, or AWS SAM, defines and deploys the Lambda function, API Gateway route, IAM policies, environment variables, and stack outputs.

Keeping these resources in one template makes the infrastructure reproducible and easier to review alongside the application code. The deployment identity still needs permission to create and update the required AWS resources.

API Gateway exposes the HTTPS endpoint that invokes Lambda, while the SAM Api event creates the integration through infrastructure as code.

Using Amazon Bedrock for Security Analysis

Diffender needs a model that can read source-code changes, identify potential security problems, and explain each finding in a format that works inside a GitHub pull request. The project does not require custom model training, dedicated inference servers, or GPU infrastructure.

Amazon Bedrock provides managed access to foundation models through AWS APIs. Diffender invokes Claude Haiku 4.5 through the AWS SDK, while the Lambda execution role controls access to the model.

Readers who are new to the service can review the Amazon Bedrock overview before continuing with the model configuration.

Diffender uses Claude Haiku 4.5 because this workload benefits from fast, focused analysis rather than long-form reasoning. In addition, the Lambda function uses the Amazon Bedrock Converse API, which provides a message-based interface for sending the system prompt, pull request diff, and inference settings to the model.

Part 2: Prepare Claude Haiku 4.5 in Amazon Bedrock

Current Amazon Bedrock documentation states that AWS enables foundation-model access by default. However, first-time users of Anthropic models may still need to submit use-case details before invocation.

To complete this step, select an Anthropic model in the Bedrock model catalog or playground.

In the AWS Management Console:

  1. Open Amazon Bedrock in us-east-1.
  2. Open the model catalog or model playground.
  3. Select Claude Haiku 4.5.
  4. Submit the Anthropic use-case details if prompted.
  5. Run a short test message.

Claude Haiku 4.5 security test in the Amazon Bedrock model playground

Next, verify programmatic invocation with the AWS CLI:

 aws bedrock-runtime converse

--model-id "anthropic.claude-haiku-4-5-20251001-v1:0"

--messages '[{"role":"user","content":[{"text":"Find the security issue: password = "admin123""}]}]'

--region us-east-1

The response should contain a model-generated message and token-usage metadata.

The final application uses the following US geographical inference profile:

 us.anthropic.claude-haiku-4-5-20251001-v1:0 

AWS documents this identifier as a supported geo-inference ID for Claude Haiku 4.5.

Part 3: Create the GitHub Repository and Token

Create a public GitHub repository named Diffender with:

  • A README.md
  • An MIT license
  • A Python .gitignore

Diffender GitHub repository with README, MIT license, and Python gitignore

 

Next, create a fine-grained personal access token restricted to the Diffender repository.

Grant these repository permissions:

  • Pull requests: Read and write
  • Issues: Read and write
  • Contents: Read-only

The current implementation retrieves the pull request through the pull requests endpoint and posts the final message through the issue-comments endpoint.

That distinction is easy to miss. GitHub pull requests are also issues internally, so creating a general conversation comment on a pull request uses:

POST /repos/{owner}/{repo}/issues/{issue_number}/comments

GitHub requires pull request or contents read access to retrieve the pull request. In addition, GitHub evaluates fine-grained token permissions for each REST endpoint.

Copy the token beginning with:

github_pat_

Do not add it to the repository, a local configuration file, or the SAM template.

Part 4: Create the Project Structure

Clone the repository and create the application directories:

git clone <a class="decorated-link" href="https://github.com/chescaasignacion/Diffender.git" target="_new" rel="noopener" data-start="11514" data-end="11563">https://github.com/chescaasignacion/Diffender.git</a>
cd Diffender
mkdir -p src test-samples
touch src/requirements.txt

The project structure is:

Diffender/
├── src/
│ ├── app.py
│ └── requirements.txt
├── test-samples/
│ └── insecure_example.py
├── template.yaml
├── README.md
├── LICENSE
└── .gitignore

AWS SAM Template

Create template.yaml:

[yaml] AWSTemplateFormatVersion: ‘2010-09-09’
Transform: AWS::Serverless-2016-10-31
Description: Diffender – an AI security code reviewer for GitHub PRs, powered by Amazon Bedrock.

Parameters:
BedrockModelId:
Type: String
Default: us.anthropic.claude-haiku-4-5-20251001-v1:0

Globals:
Function:
Timeout: 60
MemorySize: 256
Runtime: python3.12

Resources:
DiffenderFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: diffender
Handler: app.lambda_handler
CodeUri: src/
Environment:
Variables:
BEDROCK_MODEL_ID: !Ref BedrockModelId
GITHUB_TOKEN_PARAM: /diffender/github-token
WEBHOOK_SECRET_PARAM: /diffender/webhook-secret
Policies:
– Version: ‘2012-10-17’
Statement:
– Effect: Allow
Action:
– ssm:GetParameter
Resource:
– !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/diffender/*
– Version: ‘2012-10-17’
Statement:
– Effect: Allow
Action:
– kms:Decrypt
Resource: ‘
Condition:
StringEquals:
kms:ViaService: !Sub ssm.${AWS::Region}.amazonaws.com
– Version: ‘2012-10-17’
Statement:
– Effect: Allow
Action:
– bedrock:InvokeModel
Resource: ‘

Events:
Webhook:
Type: Api
Properties:
Path: /webhook
Method: post

Outputs:
WebhookUrl:
Value: !Sub https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/webhook[/yaml]

The template defines one Lambda function and one API Gateway REST API route. It also creates the Lambda execution role from inline policy statements.

The function can perform three categories of AWS action:

  • Retrieve the Parameter Store values
  • Decrypt encrypted parameters through AWS KMS
  • Invoke Amazon Bedrock

The Amazon Bedrock and AWS KMS statements use wildcard resource values in this tutorial. Therefore, a production version should evaluate whether those resources can be narrowed based on the selected AWS KMS key, model, and inference-profile ARNs.

Lambda Handler

The complete Lambda handler is available in the <a href=”https://github.com/chescaasignacion/Diffender”>Diffender GitHub repository</a>. The following sections focus on the code that controls secret retrieval, signature validation, Amazon Bedrock invocation, and GitHub integration.

Retrieve Secrets from Parameter Store

ssm = boto3.client("ssm")
_cache = {}

def _get_param(name):
if name not in _cache:
_cache[name] = ssm.get_parameter(
Name=name,
WithDecryption=True,
)["Parameter"]["Value"]
&amp;amp;lt;div class="relative w-full mt-4 mb-1"&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;lt;div class="contents"&amp;amp;gt;&amp;amp;lt;div class="relative"&amp;amp;gt;&amp;amp;lt;div class="h-full min-h-0 min-w-0"&amp;amp;gt;&amp;amp;lt;div class="h-full min-h-0 min-w-0"&amp;amp;gt;&amp;amp;lt;div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl"&amp;amp;gt;&amp;amp;lt;div class="h-full w-full border-radius-3xl bg-(--code-block-surface) corner-superellipse/1.1 overflow-clip rounded-3xl [--code-block-surface:var(--bg-elevated-secondary)] dark:[--code-block-surface:var(--composer-surface-primary)] lxnfua_clipPathFallback"&amp;amp;gt;&amp;amp;lt;div class="pointer-events-none absolute end-1.5 top-1 z-2 md:end-2 md:top-1"&amp;amp;gt;&amp;amp;amp;nbsp;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div class="relative"&amp;amp;gt;&amp;amp;lt;div class="pe-11 pt-3"&amp;amp;gt;&amp;amp;lt;div class="relative z-0 flex max-w-full"&amp;amp;gt;&amp;amp;lt;div id="code-block-viewer" class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼd ͼr" dir="ltr"&amp;amp;gt;&amp;amp;lt;div class="cm-scroller"&amp;amp;gt;&amp;amp;lt;pre class="cm-content q9tKkq_readonly m-0"&amp;amp;gt;&amp;amp;lt;code&amp;amp;gt;return _cache[name]&amp;amp;lt;/code&amp;amp;gt;&amp;amp;lt;/pre&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;amp;nbsp;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;

The function retrieves each parameter only once per warm Lambda execution environment. Later invocations can reuse the cached value.

Validate the GitHub Signature

def verify_signature(raw_body, signature_header):
if not signature_header:
return False
&amp;amp;lt;div class="relative w-full mt-4 mb-1"&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;lt;div class="contents"&amp;amp;gt;&amp;amp;lt;div class="relative"&amp;amp;gt;&amp;amp;lt;div class="h-full min-h-0 min-w-0"&amp;amp;gt;&amp;amp;lt;div class="h-full min-h-0 min-w-0"&amp;amp;gt;&amp;amp;lt;div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl"&amp;amp;gt;&amp;amp;lt;div class="h-full w-full border-radius-3xl bg-(--code-block-surface) corner-superellipse/1.1 overflow-clip rounded-3xl [--code-block-surface:var(--bg-elevated-secondary)] dark:[--code-block-surface:var(--composer-surface-primary)] lxnfua_clipPathFallback"&amp;amp;gt;&amp;amp;lt;div class="pointer-events-none absolute end-1.5 top-1 z-2 md:end-2 md:top-1"&amp;amp;gt;&amp;amp;amp;nbsp;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div class="relative"&amp;amp;gt;&amp;amp;lt;div class="pe-11 pt-3"&amp;amp;gt;&amp;amp;lt;div class="relative z-0 flex max-w-full"&amp;amp;gt;&amp;amp;lt;div id="code-block-viewer" class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼd ͼr" dir="ltr"&amp;amp;gt;&amp;amp;lt;div class="cm-scroller"&amp;amp;gt;&amp;amp;lt;pre class="cm-content q9tKkq_readonly m-0"&amp;amp;gt;&amp;amp;lt;code&amp;amp;gt;secret = _get_param(WEBHOOK_SECRET_PARAM).encode()

expected = (
    "sha256="
    + hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
)

return hmac.compare_digest(expected, signature_header)&amp;amp;lt;/code&amp;amp;gt;&amp;amp;lt;/pre&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;amp;nbsp;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;

This function recalculates the HMAC-SHA-256 signature from the raw request body and compares it with the value supplied by GitHub.

Retrieve the Pull Request Diff

def get_pr_diff(owner, repo, number):
url = (
f"&amp;amp;lt;a class="decorated-link" href="https://api.github.com/repos/" target="_new" rel="noopener" data-start="15541" data-end="15570"&amp;amp;gt;https://api.github.com/repos/&amp;amp;lt;/a&amp;amp;gt;"
f"{owner}/{repo}/pulls/{number}"
)
&amp;amp;lt;div class="relative w-full mt-4 mb-1"&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;lt;div class="contents"&amp;amp;gt;&amp;amp;lt;div class="relative"&amp;amp;gt;&amp;amp;lt;div class="h-full min-h-0 min-w-0"&amp;amp;gt;&amp;amp;lt;div class="h-full min-h-0 min-w-0"&amp;amp;gt;&amp;amp;lt;div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl"&amp;amp;gt;&amp;amp;lt;div class="h-full w-full border-radius-3xl bg-(--code-block-surface) corner-superellipse/1.1 overflow-clip rounded-3xl [--code-block-surface:var(--bg-elevated-secondary)] dark:[--code-block-surface:var(--composer-surface-primary)] lxnfua_clipPathFallback"&amp;amp;gt;&amp;amp;lt;div class="pointer-events-none absolute end-1.5 top-1 z-2 md:end-2 md:top-1"&amp;amp;gt;&amp;amp;amp;nbsp;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div class="relative"&amp;amp;gt;&amp;amp;lt;div class="pe-11 pt-3"&amp;amp;gt;&amp;amp;lt;div class="relative z-0 flex max-w-full"&amp;amp;gt;&amp;amp;lt;div id="code-block-viewer" class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼd ͼr" dir="ltr"&amp;amp;gt;&amp;amp;lt;div class="cm-scroller"&amp;amp;gt;&amp;amp;lt;pre class="cm-content q9tKkq_readonly m-0"&amp;amp;gt;&amp;amp;lt;code&amp;amp;gt;return _github_request(
    "GET",
    url,
    accept="application/vnd.github.v3.diff",
)&amp;amp;lt;/code&amp;amp;gt;&amp;amp;lt;/pre&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;amp;nbsp;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;

The GitHub API returns the code changes as a unified diff, which contains the lines added, removed, or modified by the pull request.

Invoke Claude Through Amazon Bedrock

def review_with_bedrock(diff_text):
max_chars = 60000
truncated = diff_text[:max_chars]
&amp;amp;lt;div class="relative w-full mt-4 mb-1"&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;lt;div class="contents"&amp;amp;gt;&amp;amp;lt;div class="relative"&amp;amp;gt;&amp;amp;lt;div class="h-full min-h-0 min-w-0"&amp;amp;gt;&amp;amp;lt;div class="h-full min-h-0 min-w-0"&amp;amp;gt;&amp;amp;lt;div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl"&amp;amp;gt;&amp;amp;lt;div class="h-full w-full border-radius-3xl bg-(--code-block-surface) corner-superellipse/1.1 overflow-clip rounded-3xl [--code-block-surface:var(--bg-elevated-secondary)] dark:[--code-block-surface:var(--composer-surface-primary)] lxnfua_clipPathFallback"&amp;amp;gt;&amp;amp;lt;div class="pointer-events-none absolute end-1.5 top-1 z-2 md:end-2 md:top-1"&amp;amp;gt;&amp;amp;amp;nbsp;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div class="relative"&amp;amp;gt;&amp;amp;lt;div class="pe-11 pt-3"&amp;amp;gt;&amp;amp;lt;div class="relative z-0 flex max-w-full"&amp;amp;gt;&amp;amp;lt;div id="code-block-viewer" class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼd ͼr" dir="ltr"&amp;amp;gt;&amp;amp;lt;div class="cm-scroller"&amp;amp;gt;&amp;amp;lt;pre class="cm-content q9tKkq_readonly m-0"&amp;amp;gt;&amp;amp;lt;code&amp;amp;gt;note = (
    ""
    if len(diff_text) &amp;amp;amp;lt;= max_chars
    else "\n\n"
)

user_message = (
    "Review this pull request diff:\n\n"
    f"```diff\n{truncated}{note}\n```"
)

response = bedrock.converse(
    modelId=BEDROCK_MODEL_ID,
    system=[{"text": SYSTEM_PROMPT}],
    messages=[
        {
            "role": "user",
            "content": [{"text": user_message}],
        }
    ],
    inferenceConfig={
        "maxTokens": 1500,
        "temperature": 0.2,
    },
)

return response["output"]["message"]["content"][0]["text"]&amp;amp;lt;/code&amp;amp;gt;&amp;amp;lt;/pre&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;amp;nbsp;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;

The function limits the diff to 60,000 characters before sending it to the model. It also caps the response at 1,500 tokens.

Post the Review to GitHub

def post_pr_comment(owner, repo, number, body):
url = (
f"&amp;amp;lt;a class="decorated-link" href="https://api.github.com/repos/" target="_new" rel="noopener" data-start="16917" data-end="16946"&amp;amp;gt;https://api.github.com/repos/&amp;amp;lt;/a&amp;amp;gt;"
f"{owner}/{repo}/issues/{number}/comments"
)
&amp;amp;lt;div class="relative w-full mt-4 mb-1"&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;lt;div class="contents"&amp;amp;gt;&amp;amp;lt;div class="relative"&amp;amp;gt;&amp;amp;lt;div class="h-full min-h-0 min-w-0"&amp;amp;gt;&amp;amp;lt;div class="h-full min-h-0 min-w-0"&amp;amp;gt;&amp;amp;lt;div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl"&amp;amp;gt;&amp;amp;lt;div class="h-full w-full border-radius-3xl bg-(--code-block-surface) corner-superellipse/1.1 overflow-clip rounded-3xl [--code-block-surface:var(--bg-elevated-secondary)] dark:[--code-block-surface:var(--composer-surface-primary)] lxnfua_clipPathFallback"&amp;amp;gt;&amp;amp;lt;div class="pointer-events-none absolute end-1.5 top-1 z-2 md:end-2 md:top-1"&amp;amp;gt;&amp;amp;amp;nbsp;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div class="relative"&amp;amp;gt;&amp;amp;lt;div class="pe-11 pt-3"&amp;amp;gt;&amp;amp;lt;div class="relative z-0 flex max-w-full"&amp;amp;gt;&amp;amp;lt;div id="code-block-viewer" class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼd ͼr" dir="ltr"&amp;amp;gt;&amp;amp;lt;div class="cm-scroller"&amp;amp;gt;&amp;amp;lt;pre class="cm-content q9tKkq_readonly m-0"&amp;amp;gt;&amp;amp;lt;code&amp;amp;gt;return _github_request(
    "POST",
    url,
    data={"body": body},
)&amp;amp;lt;/code&amp;amp;gt;&amp;amp;lt;/pre&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;amp;nbsp;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;

GitHub uses the Issues Comments API for general pull request conversation comments.

Requirements File

The tutorial does not install an external application package. The Python Lambda runtime includes the AWS SDK for Python, so src/requirements.txt can remain empty for this build.

For a controlled production release, package and pin the required SDK version so the application does not depend on whichever SDK version the runtime includes.

&amp;amp;lt;h1 data-section-id="1n0k789" data-start="17567" data-end="17612"&amp;amp;gt;Intentionally empty for the tutorial build.&amp;amp;lt;/h1&amp;amp;gt;

Deliberately Insecure Test File

Create test-samples/insecure_example.py:

"""Intentionally insecure sample.

Open a pull request with this file to see Diffender in action.
"""

import hashlib
import sqlite3
import subprocess

AWS_SECRET_ACCESS_KEY = (
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
)

def get_user(username):
"""Contains an intentional SQL-injection vulnerability."""
&amp;amp;lt;div class="relative w-full mt-4 mb-1"&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;lt;div class="contents"&amp;amp;gt;&amp;amp;lt;div class="relative"&amp;amp;gt;&amp;amp;lt;div class="h-full min-h-0 min-w-0"&amp;amp;gt;&amp;amp;lt;div class="h-full min-h-0 min-w-0"&amp;amp;gt;&amp;amp;lt;div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl"&amp;amp;gt;&amp;amp;lt;div class="h-full w-full border-radius-3xl bg-(--code-block-surface) corner-superellipse/1.1 overflow-clip rounded-3xl [--code-block-surface:var(--bg-elevated-secondary)] dark:[--code-block-surface:var(--composer-surface-primary)] lxnfua_clipPathFallback"&amp;amp;gt;&amp;amp;lt;div class="pointer-events-none absolute end-1.5 top-1 z-2 md:end-2 md:top-1"&amp;amp;gt;&amp;amp;amp;nbsp;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div class="relative"&amp;amp;gt;&amp;amp;lt;div class="pe-11 pt-3"&amp;amp;gt;&amp;amp;lt;div class="relative z-0 flex max-w-full"&amp;amp;gt;&amp;amp;lt;div id="code-block-viewer" class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼd ͼr" dir="ltr"&amp;amp;gt;&amp;amp;lt;div class="cm-scroller"&amp;amp;gt;&amp;amp;lt;pre class="cm-content q9tKkq_readonly m-0"&amp;amp;gt;&amp;amp;lt;code&amp;amp;gt;return sqlite3.connect("app.db").execute(
    "SELECT * FROM users WHERE name = '%s'" % username
).fetchall()&amp;amp;lt;/code&amp;amp;gt;&amp;amp;lt;/pre&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;amp;nbsp;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;
def ping(host):
"""Contains an intentional command-injection vulnerability."""
&amp;amp;lt;div class="relative w-full mt-4 mb-1"&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;lt;div class="contents"&amp;amp;gt;&amp;amp;lt;div class="relative"&amp;amp;gt;&amp;amp;lt;div class="h-full min-h-0 min-w-0"&amp;amp;gt;&amp;amp;lt;div class="h-full min-h-0 min-w-0"&amp;amp;gt;&amp;amp;lt;div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl"&amp;amp;gt;&amp;amp;lt;div class="h-full w-full border-radius-3xl bg-(--code-block-surface) corner-superellipse/1.1 overflow-clip rounded-3xl [--code-block-surface:var(--bg-elevated-secondary)] dark:[--code-block-surface:var(--composer-surface-primary)] lxnfua_clipPathFallback"&amp;amp;gt;&amp;amp;lt;div class="pointer-events-none absolute end-1.5 top-1 z-2 md:end-2 md:top-1"&amp;amp;gt;&amp;amp;amp;nbsp;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div class="relative"&amp;amp;gt;&amp;amp;lt;div class="pe-11 pt-3"&amp;amp;gt;&amp;amp;lt;div class="relative z-0 flex max-w-full"&amp;amp;gt;&amp;amp;lt;div id="code-block-viewer" class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼd ͼr" dir="ltr"&amp;amp;gt;&amp;amp;lt;div class="cm-scroller"&amp;amp;gt;&amp;amp;lt;pre class="cm-content q9tKkq_readonly m-0"&amp;amp;gt;&amp;amp;lt;code&amp;amp;gt;return subprocess.check_output(
    "ping -c 1 " + host,
    shell=True,
)&amp;amp;lt;/code&amp;amp;gt;&amp;amp;lt;/pre&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;amp;nbsp;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;
def hash_password(password):
"""Contains intentionally weak password hashing."""
&amp;amp;lt;div class="relative w-full mt-4 mb-1"&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;lt;div class="contents"&amp;amp;gt;&amp;amp;lt;div class="relative"&amp;amp;gt;&amp;amp;lt;div class="h-full min-h-0 min-w-0"&amp;amp;gt;&amp;amp;lt;div class="h-full min-h-0 min-w-0"&amp;amp;gt;&amp;amp;lt;div class="border border-token-border-light border-radius-3xl corner-superellipse/1.1 rounded-3xl"&amp;amp;gt;&amp;amp;lt;div class="h-full w-full border-radius-3xl bg-(--code-block-surface) corner-superellipse/1.1 overflow-clip rounded-3xl [--code-block-surface:var(--bg-elevated-secondary)] dark:[--code-block-surface:var(--composer-surface-primary)] lxnfua_clipPathFallback"&amp;amp;gt;&amp;amp;lt;div class="pointer-events-none absolute end-1.5 top-1 z-2 md:end-2 md:top-1"&amp;amp;gt;&amp;amp;amp;nbsp;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div class="relative"&amp;amp;gt;&amp;amp;lt;div class="pe-11 pt-3"&amp;amp;gt;&amp;amp;lt;div class="relative z-0 flex max-w-full"&amp;amp;gt;&amp;amp;lt;div id="code-block-viewer" class="q9tKkq_viewer cm-editor z-10 light:cm-light dark:cm-light flex h-full w-full flex-col items-stretch ͼd ͼr" dir="ltr"&amp;amp;gt;&amp;amp;lt;div class="cm-scroller"&amp;amp;gt;&amp;amp;lt;pre class="cm-content q9tKkq_readonly m-0"&amp;amp;gt;&amp;amp;lt;code&amp;amp;gt;return hashlib.md5(password.encode()).hexdigest()&amp;amp;lt;/code&amp;amp;gt;&amp;amp;lt;/pre&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;lt;div class=""&amp;amp;gt;&amp;amp;amp;nbsp;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;

This intentionally unsafe file belongs only in an isolated demonstration repository. Therefore, never use it in an actual application.

Part 5: Build and Deploy with AWS SAM

AWS SAM keeps the application code and infrastructure definition in the same project. Readers who need a refresher on SAM templates, resources, and deployment commands can review the AWS SAM cheat sheet.

Build the application in a Python 3.12-compatible container:

sam build --use-container

The sam build command prepares the source and infrastructure definition for deployment. In addition, using a container reduces differences between the local machine and the Lambda build environment.

Next, deploy the stack:

sam deploy --guided

Use the following guided responses:

Stack Name: Diffender
AWS Region: us-east-1
Parameter BedrockModelId: accept the default
Confirm changes before deploy: Y
Allow SAM CLI IAM role creation: Y
Disable rollback: N
DiffenderFunction Webhook may not have authorization defined: Y
Save arguments to configuration file: Y
SAM configuration file: samconfig.toml
SAM configuration environment: default

During deployment, SAM displays an authorization warning because the endpoint does not use an API Gateway authorizer. That is intentional because GitHub cannot authenticate through IAM or Amazon Cognito before sending the webhook.

Instead, Diffender verifies each request through application-level HMAC validation.

After deployment, copy the WebhookUrl from the CloudFormation outputs:

&amp;amp;lt;a class="decorated-link cursor-pointer" target="_new" rel="noopener" data-start="20120" data-end="20188"&amp;amp;gt;https://YOUR_API_ID.execute-api.us-east-1.amazonaws.com/Prod/webhook&amp;amp;lt;/a&amp;amp;gt;

AWS SAM deployment output showing the Diffender stack and API Gateway webhook URL

Finally, commit samconfig.toml only after confirming that it contains no secrets. The file normally stores deployment preferences and parameter values, but secrets should remain in Parameter Store.

Part 6: Store Secrets in Parameter Store

Store the GitHub token as a SecureString parameter:

aws ssm put-parameter &amp;lt;br data-start="20712" data-end="20714"&amp;gt;--name "/diffender/github-token" &amp;lt;br data-start="20749" data-end="20751"&amp;gt;--type SecureString &amp;lt;br data-start="20773" data-end="20775"&amp;gt;--value "YOUR_GITHUB_TOKEN" &amp;lt;br data-start="20805" data-end="20807"&amp;gt;--region us-east-1

Next, generate and store a random webhook secret:

aws ssm put-parameter &amp;lt;br data-start="20917" data-end="20919"&amp;gt;--name "/diffender/webhook-secret" &amp;lt;br data-start="20956" data-end="20958"&amp;gt;--type SecureString &amp;lt;br data-start="20980" data-end="20982"&amp;gt;--value "$(openssl rand -hex 20)" &amp;lt;br data-start="21018" data-end="21020"&amp;gt;--region us-east-1

Retrieve the webhook secret once so it can be added to GitHub:

aws ssm get-parameter &amp;lt;br data-start="21143" data-end="21145"&amp;gt;--name "/diffender/webhook-secret" &amp;lt;br data-start="21182" data-end="21184"&amp;gt;--with-decryption &amp;lt;br data-start="21204" data-end="21206"&amp;gt;--region us-east-1 &amp;lt;br data-start="21227" data-end="21229"&amp;gt;--query "Parameter.Value" &amp;lt;br data-start="21257" data-end="21259"&amp;gt;--output text

Parameter Store supports encrypted SecureString parameters backed by AWS KMS. The Lambda function requests decryption only when it retrieves the values.

GitHub webhook recent delivery showing a successful ping and HTTP 200 response

Show only the parameter names in the screenshot. Never display the decrypted values.

After the first retrieval, the function caches both parameters in the Lambda execution environment. As a result, a warm invocation can reuse the values and avoid making two additional Parameter Store calls.

However, a warm environment may continue using a cached value after a token or webhook secret changes. Therefore, a production rotation process should account for that behavior.

Part 7: Configure the GitHub Webhook

In the Diffender repository:

  1. Open Settings.
  2. Select Webhooks.
  3. Choose Add webhook.
  4. Paste the API Gateway output into Payload URL.
  5. Select application/json as the content type.
  6. Paste the webhook secret.
  7. Select Let me select individual events.
  8. Enable Pull requests.
  9. Save the webhook.

After the repository creates the webhook, GitHub sends a ping event. Diffender handles this event before attempting to parse a pull request:

if github_event == "ping":
return {"statusCode": 200, "body": "pong"}

Next, open the webhook delivery record and confirm that GitHub received HTTP status 200.

GitHub webhook recent delivery showing a successful ping and HTTP 200 response

The webhook must use the exact API Gateway URL, including /Prod/webhook, and the secret must match the value stored in Parameter Store. In addition, the application/json content type ensures that Lambda receives the request body in the format expected by the signature-validation and JSON-parsing logic.

A successful ping confirms that GitHub can reach the endpoint and that the basic webhook configuration works. If the delivery fails, review the response in Recent Deliveries, correct the URL or secret, and select Redeliver.

Why a Public Endpoint Is Acceptable Here

GitHub must be able to reach the webhook over HTTPS, so the API Gateway endpoint remains publicly accessible. However, Diffender does not trust a request simply because it reaches that endpoint.

When the repository uses a webhook secret, GitHub adds an X-Hub-Signature-256 header containing an HMAC-SHA-256 signature of the request body. Diffender recalculates the signature using the same secret stored in Parameter Store:

expected = (
"sha256="
+ hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
)

The function then compares the calculated signature with the value sent by GitHub:

hmac.compare_digest(expected, signature_header)

Using hmac.compare_digest helps reduce timing differences that can occur with a normal string comparison. Diffender rejects requests with missing or invalid signatures before it processes the pull request.

As a result, Diffender confirms that the sender knows the shared webhook secret. However, this control does not prevent unwanted traffic from reaching API Gateway. Therefore, a production deployment may also use throttling, AWS WAF, CloudWatch alarms, and cost monitoring.

Part 8: Test the AI GitHub Code Reviewer

Create a separate feature branch so the intentionally insecure sample does not affect the repository’s default branch:

git checkout -b test/diffender-security-review

Next, add the sample file, commit the change, and push the branch to GitHub:

git add test-samples/insecure_example.py
git commit -m "Add insecure sample for Diffender test"
git push -u origin test/diffender-security-review

Open a pull request from test/diffender-security-review into the default branch. GitHub then sends a pull request webhook with the opened action to the API Gateway endpoint.

After validating the webhook, Diffender retrieves the pull request diff, which contains the lines added or changed in the branch. The Lambda function then sends this code to Claude Haiku 4.5 through Amazon Bedrock for security analysis.

The sample intentionally includes several issues that should trigger findings:

  • A hardcoded AWS-style secret
  • SQL injection caused by unsafe string formatting
  • Command injection caused by shell=True
  • The use of MD5 for password hashing

At this point, GitHub push protection may detect the AWS-style key and block the push.

Stripe API Key

Although this may interrupt the demonstration, the behavior is expected and shows defense in depth. GitHub secret scanning focuses on exposed credentials, while Diffender reviews the rest of the code for issues such as injection vulnerabilities and weak cryptography.

If push protection blocks the test, use an obviously fake secret pattern or follow GitHub’s safe testing guidance. Never bypass push protection for a real credential.

After Diffender processes the webhook, it posts a review comment containing an overall risk level, a findings table, and recommended fixes.

Diffender pull request review showing security findings and recommended fixes

How the Lambda Function Processes Code Changes

The Lambda handler processes each webhook in a controlled sequence. This order allows Diffender to reject invalid requests early and avoid unnecessary GitHub API calls or model invocations.

Validate the Incoming Request

First, the function reads the raw API Gateway request body and converts all HTTP header names to lowercase. This step allows the handler to locate GitHub headers consistently.

Next, Diffender validates the X-Hub-Signature-256 header by using the webhook secret from Parameter Store. If the request contains a missing or invalid signature, the function immediately returns an HTTP 401 response.

The handler then checks the GitHub event type. It responds to ping events with HTTP 200, ignores events unrelated to pull requests, and processes only the opened, reopened, and synchronize actions.

Retrieve and Analyze the Code Changes

After Diffender accepts the event, the handler reads the repository owner, repository name, and pull request number from the payload.

Next, the function calls the GitHub Pull Requests API and retrieves the diff containing the lines added or modified in the branch. It limits the diff to 60,000 characters to control model input size, response time, and cost.

The function then sends the prepared diff and security-review instructions to Claude Haiku 4.5 through the Amazon Bedrock Converse API.

Publish the Review and Handle Errors

After Claude returns the analysis, Diffender adds a short attribution footer and posts the findings through the GitHub Issues Comments API.

If a GitHub API request fails, the function records the status code and response body in CloudWatch Logs. Likewise, it logs unexpected exceptions before returning an appropriate HTTP status code to GitHub.

The order matters because Diffender validates each request before it reads the pull request details, calls GitHub, or invokes Amazon Bedrock. As a result, an unauthenticated request cannot trigger a security review or consume model resources.

Designing the Prompt for a Useful Security Review

The prompt does more than tell Claude to inspect a pull request. It sets the boundaries of the review, limits unsupported assumptions, and gives the response a format that works well inside GitHub.

Keep the Review Focused

TD for Business

Diffender looks specifically for security issues rather than acting as a general code reviewer. The prompt makes that boundary clear:

Focus ONLY on security issues

This instruction keeps the response centered on problems such as exposed credentials, injection vulnerabilities, weak cryptography, unsafe permissions, and sensitive-data exposure.

As a result, the review leaves out comments about naming, formatting, or code style that could distract from the actual security findings.

Base Findings on the Diff

The prompt also tells the model:

Only report REAL issues you can see in the diff; do not invent problems.

A pull request diff rarely contains the full application context. Without this instruction, the model might make assumptions about how a function works or whether a value contains sensitive information.

Therefore, the wording encourages Claude to connect each finding to code that appears in the pull request. It does not remove the possibility of false positives, but it makes the expected standard clear.

Make the Review Easy to Read

Diffender asks Claude to return an overall risk level, a findings table, and a collapsible section for each issue. Each finding should also explain why the issue matters and suggest a possible fix.

This format works well in a pull request because developers can scan the summary quickly and open the details when needed. Moreover, the consistent structure could later support GitHub Checks, labels, or merge rules.

A future version could request JSON instead of Markdown, validate the response against a schema, and render the GitHub comment separately.

Keep the Output Consistent

The Amazon Bedrock request uses a low temperature:

"temperature": 0.2

For this type of review, consistency provides more value than creative variation. Therefore, the lower value helps reduce unnecessary differences when Diffender analyzes similar code changes.

However, a language model still generates the response, so results may change slightly between runs or after a model update.

Put a Limit on Large Pull Requests

Diffender sends no more than 60,000 characters from the pull request diff:

max_chars = 60000
truncated = diff_text[:max_chars]

This limit prevents a very large pull request from causing unexpected latency or token usage. Diffender measures the limit in characters rather than model tokens, so it serves as a practical safeguard rather than an exact token boundary.

However, the character limit may cut a long diff in the middle of a file. A more complete version could split the diff by file, review each section separately, combine repeated findings, and identify any files that it did not analyze.

Security Review of Diffender Itself

A security review tool should meet the same standard it applies to other code. Therefore, Diffender separates webhook validation, credential storage, model access, and review output into distinct controls.

Key safeguards include:

  • Diffender verifies each webhook request with HMAC-SHA-256.
  • The function compares signatures with hmac.compare_digest.
  • Systems Manager Parameter Store stores the GitHub token and webhook secret as encrypted SecureString parameters.
  • The fine-grained GitHub token grants access only to the Diffender repository.
  • The Lambda execution role grants only the AWS actions required by the workflow, although the resource scopes can be tightened further.
  • Diffender sends Claude only the pull request diff.
  • Claude cannot run commands, modify files, or merge code.
  • Diffender posts the findings as comments for human review.
  • The Lambda function writes errors to CloudWatch Logs for troubleshooting.

Together, these controls keep the model in an advisory role while GitHub and AWS permissions enforce access and execution boundaries.

What Would Change for Production

Although Diffender works well as a portfolio project, an organization-wide deployment would require several improvements.

  • Replace the personal access token with a GitHub App. GitHub Apps provide installation-scoped access and short-lived tokens, making them better suited to multiple repositories and teams.
  • Move review processing behind Amazon SQS. The webhook could validate the request, enqueue the event, and return immediately. A separate Lambda function could then perform the review.
  • Add idempotency with DynamoDB. Recording GitHub delivery IDs or commit SHAs would prevent webhook retries from creating duplicate comments.
  • Publish or update a single GitHub Check. This approach would avoid adding a new comment whenever a pull request receives another commit. In addition, it could later support merge controls.
  • Validate structured model output. Requesting JSON and validating it against a schema would provide more control over severity levels, required fields, and Markdown rendering.
  • Treat pull request content as untrusted input. The application should clearly separate the diff from the system instructions so comments or strings inside the code cannot redefine the model’s task.

Finally, a production deployment should configure API throttling, CloudWatch log retention, alarms, cost monitoring, and a process for rotating credentials.

Cost Considerations

Diffender uses consumption-based AWS services, so costs should remain low for a repository with occasional pull request activity.

AWS charges for Lambda and API Gateway according to usage, while standard Parameter Store parameters normally add no separate storage charge. Amazon Bedrock will likely account for the main variable cost because pricing depends on the input and output tokens processed by Claude Haiku 4.5.

To keep each review predictable, Diffender limits the pull request diff to 60,000 characters and caps the model response at 1,500 tokens. However, actual costs will still vary based on pull request size, model pricing, log volume, and AWS account eligibility.

Note: Check the current AWS pricing pages before publishing a fixed per-review estimate.

Current Limitations

Diffender supports security review rather than replacing it. Static analysis, dependency scanning, secret detection, penetration testing, and human judgment still provide coverage that a language model cannot guarantee.

More importantly, the model reviews only the code included in the pull request diff. It does not see the full call graph, deployment environment, runtime behavior, or surrounding business logic.

As a result, it may miss a vulnerability that depends on context outside the change. Conversely, it may flag code that appears risky in the diff but is safe within the wider application.

The current version also has several practical limits:

  • Diffender truncates pull request diffs after 60,000 characters.
  • Claude returns findings as Markdown rather than schema-validated data.
  • Diffender processes each review synchronously, which can delay the webhook response.
  • GitHub webhook retries may create duplicate comments.
  • Each pull request update can generate another review comment.
  • Findings do not currently affect the pull request’s merge status.
  • The current implementation uses a repository-scoped personal access token.
  • Diffender does not correlate findings with dependency databases, scanner results, or runtime exposure.
  • Adversarial text inside a diff may attempt to influence the model.
  • The project does not yet include an evaluation dataset that measures false positives and missed findings.

Diffender is most useful alongside deterministic tools such as Semgrep, Checkov, tfsec, dependency scanners, and GitHub secret scanning. These tools identify known rule violations, while Diffender adds context by explaining why a finding matters and how developers could address it.

Another example of AI-assisted security review is covered in Improving Application Security with AWS Security Agent.

Clean Up the Resources

When testing is complete, remove the AWS and GitHub resources that are no longer needed.

First, delete the AWS SAM stack:

sam delete &amp;lt;br data-start="37526" data-end="37528"&amp;gt;--stack-name Diffender &amp;lt;br data-start="37553" data-end="37555"&amp;gt;--region us-east-1

Because the setup created the GitHub token and webhook secret manually, deleting the CloudFormation stack does not remove them. Therefore, delete both parameters separately:

aws ssm delete-parameters &amp;lt;br data-start="37793" data-end="37795"&amp;gt;--names &amp;lt;br data-start="37805" data-end="37807"&amp;gt;"/diffender/github-token" &amp;lt;br data-start="37837" data-end="37839"&amp;gt;"/diffender/webhook-secret" &amp;lt;br data-start="37871" data-end="37873"&amp;gt;--region us-east-1

Finally, delete the repository webhook and revoke the fine-grained GitHub personal access token if they will no longer be used.

After cleanup, confirm that CloudFormation deleted the stack successfully. In addition, check for resources outside the stack, particularly retained CloudWatch log groups, and remove them when they are no longer required.

What Diffender Demonstrates

Diffender brings together several skills that are difficult to show through certification study alone. The project covers the design of an event-driven AWS workload, secure webhook integration, request-signature validation, encrypted secret storage, restricted IAM permissions, infrastructure as code, and troubleshooting through CloudWatch Logs.

In addition, it demonstrates how a Lambda function can coordinate multiple services and external APIs. Diffender retrieves pull request data from GitHub, invokes Claude Haiku 4.5 through an Amazon Bedrock inference profile, and returns the result through GitHub’s Issues Comments API.

The most important part of the project is not simply that a language model reviews code. Instead, the model operates inside a controlled workflow with authenticated requests, limited context, restricted permissions, visible output, and human oversight.

The design also makes its current tradeoffs clear and provides a practical path from a working prototype to a more production-ready system.

Lessons from Building Diffender

Diffender began as a practical attempt to give every pull request an immediate security-focused review without adding another server to maintain.

The current version completes that workflow. It verifies GitHub webhooks, retrieves the changed code, sends the diff to Claude through Amazon Bedrock, and posts the findings back to the pull request.

However, building the project also highlighted the less visible parts of the system that matter just as much as the model. These include permissions, secret handling, API behavior, error handling, and operational limits.

There is still work to do before Diffender would be ready for a wider production rollout. GitHub App authentication, asynchronous processing, idempotency, and structured model output are the clearest next steps. These limitations are part of the project and provide a practical direction for future improvements.

Diffender is not intended to replace security engineers or established scanning tools. Instead, it shows how AI can support an existing review process while remaining behind clear security boundaries, controlled permissions, and human oversight.

The most useful outcome was not simply getting a model to comment on a pull request. It was learning what had to be built around the model for that comment to become a meaningful part of the review process.

Repository:

https://github.com/chescaasignacion/Diffender

References

Amazon Bedrock

Amazon Bedrock Converse API Reference

https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html

Using the Converse API

https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html

Claude Haiku 4.5 Model Card

https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-haiku-4-5.html

Request Access to Amazon Bedrock Models

https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html

Simplified Model Access in Amazon Bedrock

https://aws.amazon.com/blogs/security/simplified-amazon-bedrock-model-access/

Using an Inference Profile for Model Invocation

https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-use.html

Cross-Region Inference in Amazon Bedrock

https://docs.aws.amazon.com/bedrock/latest/userguide/cross-region-inference.html

Supported Regions and Models for Inference Profiles

https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html

Amazon Bedrock Pricing

https://aws.amazon.com/bedrock/pricing/

AWS Serverless and Security Services

Amazon API Gateway Integration with AWS Lambda

https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html

AWS Lambda Execution Roles

https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html

AWS Lambda Pricing

https://aws.amazon.com/lambda/pricing/

Amazon API Gateway Pricing

https://aws.amazon.com/api-gateway/pricing/

AWS Systems Manager Parameter Store

https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html

AWS KMS Encryption for Parameter Store SecureString Parameters

https://docs.aws.amazon.com/systems-manager/latest/userguide/secure-string-parameter-kms-encryption.html

Creating Parameter Store Parameters with the AWS CLI

https://docs.aws.amazon.com/systems-manager/latest/userguide/param-create-cli.html

AWS Serverless Application Model Developer Guide

https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html

Building Applications with AWS SAM

https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-build.html

Deploying Applications with AWS SAM

https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-deploying.html

Viewing AWS SAM Application Logs

https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-logs.html

AWS IAM Security Best Practices

https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html

GitHub

Validating Webhook Deliveries

https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries

Webhook Events and Payloads

https://docs.github.com/en/webhooks/webhook-events-and-payloads

GitHub REST API for Pull Requests

https://docs.github.com/en/rest/pulls/pulls

GitHub REST API for Issue Comments

https://docs.github.com/en/rest/issues/comments

Permissions Required for Fine-Grained Personal Access Tokens

https://docs.github.com/en/rest/authentication/permissions-required-for-fine-grained-personal-access-tokens

Managing Fine-Grained Personal Access Tokens

https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens

Push Protection for Repositories and Organizations

https://docs.github.com/en/code-security/secret-scanning/introduction/about-push-protection

 

 

 

 

Tutorials Dojo portal

Turn Your Team Into Cloud-Ready Professionals Today

Tutorials Dojo for Business

Learn AWS with our PlayCloud Hands-On Labs

$2.99 AWS and Azure Exam Study Guide eBooks

tutorials dojo study guide eBook

New Claude Certified Architect Foundations CCA-F

Claude Certified Architect Foundations CCA-F

Learn GCP By Doing! Try Our GCP PlayCloud

Learn Azure with our Azure PlayCloud

FREE AI and AWS Digital Courses

FREE AWS, Azure, GCP Practice Test Samplers

SAA-C03 Exam Guide SAA-C03 examtopics AWS Certified Solutions Architect Associate

Subscribe to our YouTube Channel

Tutorials Dojo YouTube Channel

Follow Us On Linkedin

Written by: Franchesca Asignacion

Franchesca Asignacion is a Cloud Business Development Manager at Tutorials Dojo. In addition to her business role, she works on the technical side of cloud and AI technologies, particularly in DevSecMLOps. She also produces educational video courses and writes technical articles focused on cloud computing and artificial intelligence, helping learners understand modern technologies and their practical applications.

AWS, Azure, and GCP Certifications are consistently among the top-paying IT certifications in the world, considering that most companies have now shifted to the cloud. Earn over $150,000 per year with an AWS, Azure, or GCP certification!

Follow us on LinkedIn, YouTube, Facebook, or join our Slack study group. More importantly, answer as many practice exams as you can to help increase your chances of passing your certification exams on your first try!

View Our AWS, Azure, and GCP Exam Reviewers Check out our FREE courses

Our Community

~98%
passing rate
Around 95-98% of our students pass the AWS Certification exams after training with our courses.
200k+
students
Over 200k enrollees choose Tutorials Dojo in preparing for their AWS Certification exams.
~4.8
ratings
Our courses are highly rated by our enrollees from all over the world.

What our students say about us?