AI-Powered AWS Security Scanner
Build a Lambda security scanner that uses Gemini AI to audit AWS for misconfigs.
Introduction
30 Second Summary
Every company running workloads in the cloud has the same fear: a misconfigured S3 bucket or an open security group quietly exposing sensitive data to the internet.
In this project, you will build an AI-powered security scanner that automatically detects AWS misconfigurations and uses Google Gemini to generate plain-English remediation recommendations mapped to NIST 800-53 controls.
What You'll Build
A fully automated AWS security scanning pipeline deployed entirely through Terraform, powered by a Python Lambda function and Google Gemini for AI-driven analysis.
By the end of this project, you'll have:
- A Python Lambda function that scans S3 buckets, IAM policies, and Security Groups for misconfigurations.
- Terraform configuration files (main.tf, variables.tf, outputs.tf) that deploy the complete infrastructure including IAM roles, EventBridge scheduling, and CloudWatch logging.
- AI-generated security reports with NIST 800-53 control mappings viewable in CloudWatch Logs.
- An auto-remediation capability that automatically fixes the security issues your scanner finds, turning a passive detector into an active security control.
Are there any prerequisites?
You need an AWS account with admin access, Terraform installed, Python installed, and the AWS CLI configured with active credentials. You will also need a Google account to get a free Gemini API key. The setup is covered in Step 1.
Set Up Your Tools and Get a Gemini API Key
Before you can build your security scanner, you need the tools that power it. Your scanner relies on three things: Terraform to deploy infrastructure, Python to write the scanning logic, and the Google Gemini API to generate AI-driven security recommendations. In this step, you will get your free Gemini API key, verify your local tools, and set up the project directory.
In This Step, Get Ready To:
- Get a free Gemini API key from Google AI Studio.
- Verify that Terraform, Python, and AWS CLI are installed and configured.
- Create the project directory structure and install the google-genai Python package.
Get Your Free Gemini API Key
Google Gemini offers a free API tier that requires no billing or credit card. Your scanner will send its findings to Gemini and receive plain-English security recommendations in return. To make API calls, you need an API key.
- Open Google AI Studio in your browser.
- Sign in with your Google account.
- Click Get API key in the left sidebar.
- Click Create API key and select any Google Cloud project (or let it create one for you).
- Copy the generated API key and save it somewhere secure, like a password manager or a local text file that you will not commit to version control.
Keep your API key safe
Your Gemini API key starts with AIza. Never commit it to a Git repository or share it publicly. You will pass it to Terraform as a variable later, not hardcode it into your source files.
Why Gemini instead of OpenAI or Claude?
Gemini's free tier gives you free input and output tokens with no billing setup required. OpenAI and Claude both require credit card registration for API access. This keeps the project at $0 cost.
Verify Your Local Tools
Your project depends on three tools working together: Terraform to deploy infrastructure, Python to write the Lambda function, and the AWS CLI to interact with your AWS account. Let's confirm each one is installed and working.
- Open a terminal window.
- Run the following command to check your Terraform installation.
terraform --version
- Run the following command to check your Python installation.
python --version
- Run the following command to verify your AWS CLI is configured with active credentials.
aws sts get-caller-identity
What does this command do?
The aws sts get-caller-identity command calls the AWS Security Token Service to confirm who you are authenticated as. It returns your AWS account ID, user ARN, and user ID. If this command fails, your AWS CLI credentials are not configured correctly.
I see version numbers and my AWS account info
All three tools are installed and your AWS credentials are active. You are ready to move on.
I see an error
Here are the most common issues:
- "command not found" for terraform or python means the tool is not installed or not in your system PATH. Reinstall and make sure to check the "Add to PATH" option.
- On some systems, Python is available as python3 instead of python. Try python3 --version if the first command fails.
- If aws sts get-caller-identity returns an error, run aws configure and enter your AWS access key ID, secret access key, and default region.
Still stuck?
Help me troubleshoot my tool installation.
Create the Project Directory Structure
Your project has two main parts: Terraform configuration files that define your infrastructure, and a Python Lambda function that does the actual scanning. Organizing these into separate directories keeps things clean.
- Run the following commands to create the project structure.
macOS
mkdir -p ai-security-scanner/lambda
cd ai-security-scanner
Windows
mkdir ai-security-scanner\lambda
cd ai-security-scanner
Your ai-security-scanner folder is the project root where your Terraform files will live. The lambda subdirectory will hold your Python scanner code and its dependencies.
- Install the google-genai Python package into the lambda directory.
pip install --target ./lambda google-genai
Why install into the lambda directory?
AWS Lambda includes boto3 in its Python runtime by default, but google-genai is not included. The --target flag installs the package into a specific folder instead of your global Python environment. Later, you will zip this folder together with your scanner code to create the Lambda deployment package.
- Verify the installation succeeded by checking that the google directory now exists inside your lambda folder.
macOS
ls lambda/
Windows
dir lambda\
You should see a google directory along with several other dependency folders. This confirms the google-genai SDK and its dependencies are ready to be bundled with your Lambda function.
Having trouble with pip install?
If you see a permissions error, try using pip3 instead of pip. If you get a "command not found" error, make sure Python is installed and added to your PATH. Help me fix my pip installation issue.
Your environment is ready. You have a Gemini API key, verified tools, and a project directory with the Gemini SDK installed. Next up, you will write the Python Lambda function that scans your AWS resources for security misconfigurations.
Write the Security Scanner Lambda Function
Your tools are installed and your API key is ready. Now it's time to write the actual scanner.
The core of this project is a Python Lambda function that inspects your AWS environment for common security misconfigurations. It checks three resource types: S3 buckets, IAM policies, and Security Groups. After collecting findings, the function sends them to Google Gemini for AI-powered analysis and NIST 800-53 control mapping.
In This Step, Get Ready To:
- Write scanning logic that audits S3 buckets, IAM policies, and Security Groups using boto3.
- Integrate Google Gemini to analyze findings and map them to NIST 800-53 controls.
- Package the Lambda function with its dependencies into a deployment ZIP file.
Build the S3, IAM, and Security Group Scanning Logic
Your scanner needs to check three categories of AWS resources. For each category, you will use boto3 to query the AWS API and look for specific misconfigurations. S3 buckets get checked for missing encryption and public access. IAM policies get checked for overly permissive wildcard actions. Security Groups get checked for rules that allow unrestricted inbound traffic from the entire internet.
- Navigate to your Lambda code directory inside your project folder.
- Create a file called lambda_function.py.
- Add the following imports and scanning functions to the file.
import json
import os
import boto3
from botocore.exceptions import ClientError
s3_client = boto3.client('s3')
iam_client = boto3.client('iam')
ec2_client = boto3.client('ec2')
def scan_s3_buckets():
"""Check all S3 buckets for encryption and public access issues."""
findings = []
buckets = s3_client.list_buckets()['Buckets']
for bucket in buckets:
bucket_name = bucket['Name']
# Check encryption status
try:
s3_client.get_bucket_encryption(Bucket=bucket_name)
except ClientError as e:
if e.response['Error']['Code'] == 'ServerSideEncryptionConfigurationNotFoundError':
findings.append({
'resource': f's3://{bucket_name}',
'issue': 'No default encryption configured',
'severity': 'HIGH'
})
# Check public access settings
try:
public_access = s3_client.get_public_access_block(Bucket=bucket_name)
config = public_access['PublicAccessBlockConfiguration']
if not all([
config.get('BlockPublicAcls', False),
config.get('BlockPublicPolicy', False),
config.get('IgnorePublicAcls', False),
config.get('RestrictPublicBuckets', False)
]):
findings.append({
'resource': f's3://{bucket_name}',
'issue': 'Public access block is not fully enabled',
'severity': 'HIGH'
})
except ClientError:
findings.append({
'resource': f's3://{bucket_name}',
'issue': 'No public access block configuration found',
'severity': 'CRITICAL'
})
return findings
def scan_iam_policies():
"""Check custom IAM policies for overly permissive wildcard actions."""
findings = []
policies = iam_client.list_policies(Scope='Local')['Policies']
for policy in policies:
version = iam_client.get_policy_version(
PolicyArn=policy['Arn'],
VersionId=policy['DefaultVersionId']
)
document = version['PolicyVersion']['Document']
# Handle both string and dict Statement formats
statements = document.get('Statement', [])
if isinstance(statements, dict):
statements = [statements]
for statement in statements:
actions = statement.get('Action', [])
if isinstance(actions, str):
actions = [actions]
resources = statement.get('Resource', [])
if isinstance(resources, str):
resources = [resources]
if '*' in actions or '*' in resources:
findings.append({
'resource': policy['Arn'],
'issue': f'Policy contains wildcard permissions: Actions={actions}, Resources={resources}',
'severity': 'HIGH'
})
return findings
def scan_security_groups():
"""Check Security Groups for rules allowing unrestricted inbound access."""
findings = []
sensitive_ports = [22, 3389, 3306, 5432, 1433]
security_groups = ec2_client.describe_security_groups()['SecurityGroups']
for sg in security_groups:
for rule in sg.get('IpPermissions', []):
for ip_range in rule.get('IpRanges', []):
if ip_range.get('CidrIp') == '0.0.0.0/0':
from_port = rule.get('FromPort', 0)
to_port = rule.get('ToPort', 65535)
# Check if any sensitive port falls within the rule's range
exposed_ports = [p for p in sensitive_ports if from_port <= p <= to_port]
if exposed_ports:
findings.append({
'resource': f'{sg["GroupId"]} ({sg.get("GroupName", "unnamed")})',
'issue': f'Allows unrestricted inbound access (0.0.0.0/0) on sensitive ports: {exposed_ports}',
'severity': 'CRITICAL'
})
return findings
What does this code do?
This code defines three scanning functions, one for each AWS resource type.
- scan_s3_buckets() lists every S3 bucket in your account and checks whether each one has default encryption enabled and a public access block configured.
- scan_iam_policies() retrieves all customer-managed IAM policies and reads their policy documents, flagging any that use wildcard * in actions or resources.
- scan_security_groups() examines every Security Group for inbound rules that allow traffic from 0.0.0.0/0 on sensitive ports like SSH (22) and RDP (3389).
Why these three resource types?
S3 misconfigurations, overly permissive IAM policies, and open Security Groups are among the most common causes of real-world cloud breaches. These three checks cover the NIST 800-53 control families SC (System and Communications Protection), AC (Access Control), and CM (Configuration Management).
Integrate Google Gemini for AI-Powered Analysis
Raw scan findings tell you what is wrong, but they don't tell you why it matters or how to fix it. This is where Gemini comes in. You will format the findings into a structured prompt and send them to the Gemini API. The prompt instructs Gemini to rate severity, recommend remediation steps, and map each finding to the relevant NIST 800-53 control family.
- Add the following Gemini integration and Lambda handler code to the bottom of lambda_function.py.
from google import genai
from google.genai import types
def analyze_with_gemini(findings):
"""Send scan findings to Gemini for AI-powered security analysis."""
if not findings:
return "No security findings to analyze. Your environment looks clean!"
gemini_api_key = os.environ.get('GEMINI_API_KEY')
if not gemini_api_key:
return "ERROR: GEMINI_API_KEY environment variable not set."
# Initialize the Gemini client
client = genai.Client(api_key=gemini_api_key)
# Format findings into a structured prompt
findings_text = json.dumps(findings, indent=2)
prompt = f"""You are an AWS cloud security expert. Analyze the following security scan findings
from an automated AWS environment audit.
For each finding:
1. Explain why this is a security risk in plain English.
2. Rate the severity (CRITICAL, HIGH, MEDIUM, LOW).
3. Provide specific remediation steps using AWS CLI or Console.
4. Map the finding to the relevant NIST 800-53 control family
(e.g., AC for Access Control, SC for System and Communications Protection,
CM for Configuration Management).
Findings:
{findings_text}
Provide your analysis in a clear, structured format."""
# Send to Gemini and get the response
response = client.models.generate_content(
model='gemini-2.0-flash',
contents=prompt
)
return response.text
def lambda_handler(event, context):
"""Main Lambda entry point. Runs all scans and sends findings to Gemini."""
print("Starting AWS Security Scan...")
# Run all three scans
all_findings = []
print("Scanning S3 buckets...")
s3_findings = scan_s3_buckets()
all_findings.extend(s3_findings)
print(f"Found {len(s3_findings)} S3 issues.")
print("Scanning IAM policies...")
iam_findings = scan_iam_policies()
all_findings.extend(iam_findings)
print(f"Found {len(iam_findings)} IAM issues.")
print("Scanning Security Groups...")
sg_findings = scan_security_groups()
all_findings.extend(sg_findings)
print(f"Found {len(sg_findings)} Security Group issues.")
print(f"\nTotal findings: {len(all_findings)}")
print(f"Raw findings:\n{json.dumps(all_findings, indent=2)}")
# Send findings to Gemini for AI analysis
print("\nSending findings to Gemini for analysis...")
ai_analysis = analyze_with_gemini(all_findings)
print(f"\n{'='*60}")
print("AI SECURITY ANALYSIS (NIST 800-53 Mapped)")
print(f"{'='*60}")
print(ai_analysis)
return {
'statusCode': 200,
'body': json.dumps({
'total_findings': len(all_findings),
'findings': all_findings,
'ai_analysis': ai_analysis
})
}
What does this code do?
The analyze_with_gemini() function takes the list of raw findings, converts them to JSON, and wraps them in a detailed prompt. The prompt tells Gemini to act as a cloud security expert and produce remediation steps mapped to NIST 800-53 control families.
The lambda_handler() function is the entry point that Lambda calls. It runs all three scans, collects findings, sends them to Gemini, and logs everything to CloudWatch via print() statements. Every print() call inside a Lambda function automatically appears in CloudWatch Logs.
What is NIST 800-53?
NIST 800-53 is a catalog of security and privacy controls published by the National Institute of Standards and Technology. Each control belongs to a family identified by a two-letter code. In this project, the most relevant families are AC (Access Control) for IAM findings, SC (System and Communications Protection) for encryption issues, and CM (Configuration Management) for Security Group misconfigurations.
Package the Lambda Deployment Artifact
Lambda functions run in a managed environment that includes boto3 by default, but not the google-genai package. You need to bundle that dependency alongside your code into a single ZIP file. Terraform will upload this ZIP when it creates the Lambda function.
- Create a requirements.txt file in your Lambda code directory with the following content.
google-genai
- Install the dependency into a local package directory and create the deployment ZIP.
macOS
- Run the following commands from your Lambda code directory.
pip install --target ./package google-genai
cd package
zip -r ../deployment.zip .
cd ..
zip deployment.zip lambda_function.py
What do these commands do?
The first command installs the google-genai package and all its dependencies into a local package folder instead of your system Python. The remaining commands zip those dependencies together with your lambda_function.py into a single deployment.zip file.
Windows
- Run the following commands from your Lambda code directory in PowerShell.
pip install --target .\package google-genai
Copy-Item lambda_function.py -Destination .\package\
Compress-Archive -Path .\package\* -DestinationPath deployment.zip -Force
What do these commands do?
The first command installs the google-genai package into a local package folder. The second copies your Lambda handler into the same folder. The third uses PowerShell's Compress-Archive to create deployment.zip from everything in the folder.
- Verify that deployment.zip was created in your Lambda code directory.
- Move or copy deployment.zip to your Terraform directory so Terraform can reference it during deployment.
Pro tip: keep boto3 out of the ZIP
You do not need to include boto3 in your deployment package. AWS Lambda's Python runtime already includes boto3 by default. Only third-party packages like google-genai need to be bundled.
Having trouble with the ZIP file?
Make sure lambda_function.py is at the root level of the ZIP, not nested inside a subfolder. Lambda looks for the handler file at the top level. You can verify by listing the ZIP contents and checking that lambda_function.py appears without a directory prefix. Help me troubleshoot my deployment ZIP.
Your scanner code is written and packaged. Next up, you will define the Terraform infrastructure that deploys this function to AWS along with its IAM role, EventBridge schedule, and CloudWatch logging.
Define the Terraform Infrastructure
Your scanner code is packaged and ready to deploy. Now you need the infrastructure to run it on. Terraform lets you define all of your AWS resources in configuration files, so you can create, update, and destroy your entire stack with a single command. In this step, you will write the Terraform configuration that deploys your Lambda function, IAM role, EventBridge schedule, and CloudWatch log group.
In This Step, Get Ready To:
- Create an IAM role with least-privilege permissions for the Lambda function.
- Define the Lambda function and an EventBridge schedule to automate scans.
- Set up variables and outputs to make the configuration reusable and informative.
Define the IAM Role and Policies
Every Lambda function needs an IAM role that defines what AWS services it can access. This is where the principle of least privilege comes in. Your scanner only needs to read S3, IAM, and EC2 resources, plus write logs to CloudWatch. It should not have permissions to do anything else.
- Open your main.tf file in your Terraform directory.
- Add the following Terraform configuration to define the AWS provider, the IAM role, and the IAM policy.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# IAM role that the Lambda function assumes when it runs
resource "aws_iam_role" "lambda_role" {
name = "${var.function_name}-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}
# Policy granting read-only access to S3, IAM, and EC2 (Security Groups)
# plus permission to write CloudWatch Logs
resource "aws_iam_role_policy" "lambda_policy" {
name = "${var.function_name}-policy"
role = aws_iam_role.lambda_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:ListAllMyBuckets",
"s3:GetBucketEncryption",
"s3:GetBucketPublicAccessBlock"
]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"iam:ListPolicies",
"iam:GetPolicyVersion"
]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"ec2:DescribeSecurityGroups"
]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "arn:aws:logs:*:*:*"
}
]
})
}
What Does This Configuration Do?
This creates two resources. The aws_iam_role defines a trust policy that says "only Lambda can assume this role." The aws_iam_role_policy defines exactly what the role can do. Notice each statement grants only the specific API actions the scanner needs. No wildcards, no extra permissions.
Why Inline Policies Instead of Managed Policies?
AWS provides managed policies like ReadOnlyAccess, but those grant far more permissions than your scanner needs. An inline policy lets you specify the exact actions required. This follows the NIST 800-53 AC (Access Control) principle of least privilege.
Define the Lambda Function and EventBridge Schedule
With the IAM role in place, you can define the Lambda function itself. You will also create an EventBridge rule that triggers the scanner on a schedule, turning it from a manual tool into an automated security control.
- Add the following resources to your main.tf file, below the IAM resources you just created.
# CloudWatch Log Group for the Lambda function's output
resource "aws_cloudwatch_log_group" "scanner_logs" {
name = "/aws/lambda/${var.function_name}"
retention_in_days = 14
}
# The Lambda function that runs your security scanner
resource "aws_lambda_function" "security_scanner" {
filename = "${path.module}/../lambda/deployment.zip"
function_name = var.function_name
role = aws_iam_role.lambda_role.arn
handler = "scanner.lambda_handler"
runtime = "python3.13"
timeout = 60
source_code_hash = filebase64sha256("${path.module}/../lambda/deployment.zip")
environment {
variables = {
GEMINI_API_KEY = var.gemini_api_key
}
}
depends_on = [aws_cloudwatch_log_group.scanner_logs]
}
# EventBridge rule that triggers the scanner every 12 hours
resource "aws_cloudwatch_event_rule" "scan_schedule" {
name = "${var.function_name}-schedule"
description = "Triggers the security scanner on a regular schedule"
schedule_expression = "rate(12 hours)"
}
# Connect the EventBridge rule to the Lambda function
resource "aws_cloudwatch_event_target" "scanner_target" {
rule = aws_cloudwatch_event_rule.scan_schedule.name
target_id = "SecurityScannerTarget"
arn = aws_lambda_function.security_scanner.arn
}
# Grant EventBridge permission to invoke the Lambda function
resource "aws_lambda_permission" "allow_eventbridge" {
statement_id = "AllowEventBridgeInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.security_scanner.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.scan_schedule.arn
}
What Does Each Resource Do?
- aws_cloudwatch_log_group creates a dedicated log group where your scanner's output will appear. The 14-day retention keeps costs down.
- aws_lambda_function deploys your ZIP package as a Lambda function with the python3.13 runtime and a 60-second timeout. The source_code_hash tells Terraform to redeploy whenever your ZIP file changes.
- aws_cloudwatch_event_rule creates a schedule that fires every 12 hours.
- aws_cloudwatch_event_target connects that schedule to your Lambda function.
- aws_lambda_permission grants EventBridge the right to invoke your function. Without this, the schedule would fire but Lambda would reject the invocation.
Pro tip: the Gemini API key
The Gemini API key is passed as an environment variable on the Lambda function. Terraform will prompt you for this value when you run terraform apply in the next step. For production use, you would store this in AWS Secrets Manager instead of an environment variable.
Define Variables and Outputs
Hardcoding values like the AWS region or function name directly into main.tf makes the configuration rigid. Terraform variables let you parameterize these values so the same configuration can be reused across environments. Outputs display key information after deployment, like the ARNs you will need for testing.
- Create a new file called variables.tf in your Terraform directory.
- Add the following variable definitions.
variable "aws_region" {
description = "AWS region to deploy resources"
type = string
default = "us-east-1"
}
variable "function_name" {
description = "Name of the Lambda function"
type = string
default = "ai-security-scanner"
}
variable "gemini_api_key" {
description = "Google Gemini API key for AI analysis"
type = string
sensitive = true
}
What Does sensitive = true Do?
Marking the gemini_api_key variable as sensitive = true tells Terraform to redact its value from terminal output and logs. The key is still stored in Terraform's state file, but it will not be displayed when you run terraform plan or terraform apply.
- Create another file called outputs.tf in the same directory.
- Add the following output definitions.
output "lambda_function_arn" {
description = "ARN of the security scanner Lambda function"
value = aws_lambda_function.security_scanner.arn
}
output "cloudwatch_log_group" {
description = "CloudWatch Log Group for scanner output"
value = aws_cloudwatch_log_group.scanner_logs.name
}
output "eventbridge_rule_arn" {
description = "ARN of the EventBridge schedule rule"
value = aws_cloudwatch_event_rule.scan_schedule.arn
}
Why These Three Outputs?
After Terraform deploys your stack, these outputs print the Lambda ARN (so you can invoke it), the CloudWatch log group name (so you can find your scan results), and the EventBridge rule ARN (so you can verify the schedule). You will use all three in the next step when you test the scanner.
Your entire infrastructure is now defined as code. In the next step, you will deploy it all with a single Terraform command and watch your scanner run against real AWS resources.
Deploy and Test the Scanner
Your Terraform configuration is complete and your Lambda code is packaged. Now it is time to deploy everything to AWS and watch your security scanner in action. In this step, you will use Terraform to provision all your resources, invoke the scanner manually, and then create a deliberate misconfiguration to prove the scanner catches real issues.
In This Step, Get Ready To:
- Deploy the entire security scanner stack with Terraform.
- Invoke the Lambda function and review AI-generated security recommendations in CloudWatch Logs.
- Create a deliberate misconfiguration and verify the scanner detects it with NIST 800-53 control mappings.
Initialize and Deploy With Terraform
Terraform uses a three-command workflow to deploy infrastructure. First, terraform init downloads the AWS provider plugin. Then, terraform plan shows you exactly what resources will be created. Finally, terraform apply actually provisions them in your AWS account.
- Open your terminal and navigate to your Terraform directory (the folder containing main.tf).
- Run the initialization command.
terraform init
What does this command do?
terraform init downloads the AWS provider plugin specified in your configuration. It also sets up the local .terraform directory where Terraform stores its state and provider binaries. You only need to run this once per project, or when you change providers.
You should see a message saying "Terraform has been successfully initialized" with a green checkmark.
- Run the plan command to preview what Terraform will create.
terraform plan
Terraform prompts you for your Gemini API key if you did not set it in a terraform.tfvars file. Paste your API key when prompted.
Pro tip: use a tfvars file
To avoid pasting your API key every time, create a file called terraform.tfvars in your Terraform directory with the line gemini_api_key = "your-api-key-here". Terraform reads this file automatically. Make sure to add *.tfvars to your .gitignore so you never commit secrets to version control.
The plan output shows you every resource Terraform will create. You should see your Lambda function, IAM role, IAM policy, EventBridge rule, EventBridge target, Lambda permission, and CloudWatch log group. Review the list to confirm everything looks correct.
- Deploy the infrastructure by running the apply command.
terraform apply
- Type yes when Terraform asks for confirmation.
Terraform creates all your resources and displays the outputs you defined. You should see your Lambda function ARN, CloudWatch log group name, and EventBridge schedule ARN printed in the terminal.
Seeing an error during apply?
Common issues include an invalid ZIP file path (check that your filename argument in main.tf points to the correct location of your deployment ZIP) or AWS credentials that have expired. Run aws sts get-caller-identity to verify your credentials are still active.
If you are stuck, help me debug my terraform apply error.
Trigger the Scanner Manually and Review Results
Your EventBridge schedule triggers the scanner automatically, but you do not want to wait for the next scheduled run. You can invoke the Lambda function directly using the AWS CLI to test it right now.
- Invoke the Lambda function using the AWS CLI.
aws lambda invoke \
--function-name [[FUNCTION_NAME="your-function-name"]] \
--log-type Tail \
output.json
What does this command do?
The aws lambda invoke command triggers your Lambda function on demand. The --log-type Tail flag returns the last 4 KB of execution logs directly in the response. The function's return value is saved to output.json.
The command returns a JSON response with a StatusCode of 200 if the function executed successfully. The scanner may take 15-30 seconds to complete because it scans your resources and waits for the Gemini API response.
- Open the AWS Console in your browser.
- Navigate to CloudWatch.
- Select Log groups in the left sidebar.
- Click on the log group for your Lambda function (it starts with /aws/lambda/).
- Click on the most recent log stream to view the scan results.
Inside the log stream, you should see your scanner's output. This includes the raw findings from each resource type (S3, IAM, Security Groups) followed by Gemini's AI-generated analysis with severity ratings, remediation recommendations, and NIST 800-53 control mappings.
What should I see?
The CloudWatch logs show your scanner's structured output. You will see scan results for each resource type, then Gemini's analysis. Gemini maps each finding to NIST 800-53 control families like AC (Access Control) for IAM issues, SC (System and Communications Protection) for encryption gaps, and CM (Configuration Management) for Security Group misconfigurations.
If your AWS account is relatively clean, the scanner may report few or no findings. That is expected. The next substep creates a deliberate misconfiguration to test the scanner properly.
Seeing a timeout or error in the logs?
If the function times out, your Lambda timeout may be too short. Verify it is set to 60 seconds in your Terraform configuration. If you see an error related to the Gemini API, check that your API key environment variable is set correctly on the Lambda function. You can verify this in the AWS Console under Lambda > your function > Configuration > Environment variables.
If you need help, help me debug my Lambda execution error.
Create a Deliberate Misconfiguration and Re-Scan
The best way to prove your scanner works is to give it something to find. You will create an S3 bucket without encryption so the scanner flags it as a security issue.
- Create a test S3 bucket using the AWS CLI.
aws s3api create-bucket \
--bucket [[TEST_BUCKET="your-unique-test-bucket-name"]] \
--region us-east-1
Why us-east-1?
S3 bucket names are globally unique. When creating a bucket in us-east-1, you do not need to specify a LocationConstraint. For any other region, you would need to add --create-bucket-configuration LocationConstraint=your-region. Use a unique name like security-test-yourname-2025.
This bucket is created without default encryption enabled. Your scanner should detect this as a finding and map it to the SC (System and Communications Protection) control family in NIST 800-53.
- Invoke the Lambda function again to re-scan your environment.
aws lambda invoke \
--function-name [[FUNCTION_NAME="your-function-name"]] \
--log-type Tail \
output.json
- Go back to CloudWatch Logs in the AWS Console.
- Click on the newest log stream in your Lambda's log group.
- Look for the finding related to your test bucket.
You should see your test bucket flagged for missing encryption. Gemini's analysis includes a severity rating, a remediation recommendation (enable SSE-S3 or SSE-KMS encryption), and a mapping to the NIST 800-53 SC control family. This is the complete pipeline working end-to-end: automated detection, AI analysis, and compliance mapping.
What is NIST 800-53 SC?
SC stands for System and Communications Protection. This control family covers encryption, boundary protection, and data integrity. An unencrypted S3 bucket violates SC-28 (Protection of Information at Rest), which requires organizations to protect the confidentiality and integrity of data stored on their systems.
Your AI-powered security scanner is fully operational. It detects real misconfigurations, generates intelligent remediation guidance, and maps findings to compliance frameworks. Up next, you will clean up your resources and explore an optional challenge to take this scanner even further.
Secret mission
Add Auto-Remediation to Your Security Scanner
Your scanner can detect misconfigurations. But what if it could fix them too? In this secret mission, you'll upgrade your scanner from a passive detection tool into an active security control that automatically remediates the issues it finds.
Before you go
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Lambda, EventBridge, and CloudWatch Logs are well within AWS Free Tier limits for this project's usage. The Gemini API free tier covers the API calls made during scanning. No ongoing charges apply unless the EventBridge schedule continues triggering scans frequently.
Resources you used:
- 1 Lambda function (security scanner).
- 1 IAM role with attached policies (Lambda execution role).
- 1 EventBridge rule and target (scheduled scan trigger).
- 1 CloudWatch Log Group (scan results and AI recommendations).
- Any test S3 buckets you created for the misconfiguration test.
- Google Gemini API key (stored as a Lambda environment variable).
Keep everything running
No action needed. Choose this if you want the scanner to keep running on its EventBridge schedule for continuous monitoring. AWS Free Tier covers minimal Lambda and EventBridge usage, so costs should remain at $0.
- Your Lambda function will continue to be invoked on the EventBridge schedule.
- CloudWatch Logs will accumulate scan results over time.
- Monitor your AWS billing dashboard periodically to confirm you stay within Free Tier limits.
Pause - I'll come back to this later
Stop the scheduled scans to prevent any usage, but keep all resources deployed so you can resume later.
- Open the Amazon EventBridge console and navigate to **Rules**.
- Select your scanner's EventBridge rule and click **Disable**.
- Your Lambda function, IAM role, and CloudWatch Log Group will remain in place but idle.
- To resume, re-enable the EventBridge rule from the console or redeploy with terraform apply.
Delete - I don't want to use this again
Remove all project resources completely and start fresh.
- Open your terminal and navigate to your Terraform project directory.
- Run terraform destroy and type yes when prompted. This removes the Lambda function, IAM role, EventBridge rule, and CloudWatch Log Group.
- Delete any test S3 buckets you created manually for the misconfiguration test. Open the S3 console, select each test bucket, empty it, then delete it.
- Optionally, revoke your Gemini API key in Google AI Studio if you no longer need it.
- Delete your local project folder if you no longer need the source code.
Mission accomplished
That's a Wrap!
Congratulations! You just built the exact kind of tool that AI Cloud Security Engineers build every day. You have a fully automated security scanner running in AWS Lambda, deployed entirely through Terraform, that detects cloud misconfigurations and generates AI-driven remediation guidance mapped to NIST 800-53 controls.
Here's what you learned:
- Written a Python Lambda function that scans S3 buckets, IAM policies, and Security Groups for real security misconfigurations using boto3.
- Integrated Google Gemini to generate AI-driven remediation recommendations mapped to NIST 800-53 control families like AC, SC, and CM.
- Deployed the entire stack using Terraform, including IAM roles, Lambda functions, EventBridge scheduling, and CloudWatch logging.
- Added auto-remediation to automatically fix security issues like unencrypted S3 buckets and open Security Group rules, turning a passive scanner into an active security control.
Ready to quiz yourself?