mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-10 21:24:56 +03:00
606 lines
26 KiB
Python
606 lines
26 KiB
Python
#!/usr/bin/env python3
|
|
# For authorized security assessments and defensive monitoring only
|
|
"""Serverless Function Injection Detection Agent - Scans Lambda functions for injection vulnerabilities, layer hijacking, and IAM escalation paths."""
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import zipfile
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
try:
|
|
import boto3
|
|
from botocore.exceptions import ClientError
|
|
except ImportError:
|
|
print("ERROR: boto3 required. Install with: pip install boto3")
|
|
sys.exit(1)
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Dangerous function patterns by runtime
|
|
INJECTION_PATTERNS = {
|
|
"python": [
|
|
{"pattern": r"\beval\s*\(", "sink": "eval()", "severity": "critical", "cwe": "CWE-95"},
|
|
{"pattern": r"\bexec\s*\(", "sink": "exec()", "severity": "critical", "cwe": "CWE-95"},
|
|
{"pattern": r"\bos\.system\s*\(", "sink": "os.system()", "severity": "critical", "cwe": "CWE-78"},
|
|
{"pattern": r"\bos\.popen\s*\(", "sink": "os.popen()", "severity": "critical", "cwe": "CWE-78"},
|
|
{"pattern": r"\bsubprocess\.call\s*\(.*shell\s*=\s*True", "sink": "subprocess.call(shell=True)", "severity": "critical", "cwe": "CWE-78"},
|
|
{"pattern": r"\bsubprocess\.run\s*\(.*shell\s*=\s*True", "sink": "subprocess.run(shell=True)", "severity": "critical", "cwe": "CWE-78"},
|
|
{"pattern": r"\bsubprocess\.Popen\s*\(.*shell\s*=\s*True", "sink": "subprocess.Popen(shell=True)", "severity": "critical", "cwe": "CWE-78"},
|
|
{"pattern": r"\bpickle\.loads\s*\(", "sink": "pickle.loads()", "severity": "high", "cwe": "CWE-502"},
|
|
{"pattern": r"\byaml\.load\s*\((?!.*Loader\s*=\s*yaml\.SafeLoader)", "sink": "yaml.load() without SafeLoader", "severity": "high", "cwe": "CWE-502"},
|
|
{"pattern": r"\bjinja2\.Template\s*\(.*event", "sink": "jinja2.Template() with event data", "severity": "high", "cwe": "CWE-1336"},
|
|
{"pattern": r"\b__import__\s*\(", "sink": "__import__()", "severity": "high", "cwe": "CWE-95"},
|
|
{"pattern": r"f['\"].*\{.*event.*\}.*['\"].*\.execute\(", "sink": "SQL via f-string with event data", "severity": "critical", "cwe": "CWE-89"},
|
|
{"pattern": r"['\"].*%s.*['\"].*%.*event", "sink": "SQL via string formatting with event data", "severity": "critical", "cwe": "CWE-89"},
|
|
],
|
|
"nodejs": [
|
|
{"pattern": r"\beval\s*\(", "sink": "eval()", "severity": "critical", "cwe": "CWE-95"},
|
|
{"pattern": r"\bnew\s+Function\s*\(", "sink": "new Function()", "severity": "critical", "cwe": "CWE-95"},
|
|
{"pattern": r"\bchild_process\.exec\s*\(", "sink": "child_process.exec()", "severity": "critical", "cwe": "CWE-78"},
|
|
{"pattern": r"\bchild_process\.execSync\s*\(", "sink": "child_process.execSync()", "severity": "critical", "cwe": "CWE-78"},
|
|
{"pattern": r"\bexecSync\s*\(", "sink": "execSync()", "severity": "critical", "cwe": "CWE-78"},
|
|
{"pattern": r"\bexec\s*\((?!ute)", "sink": "exec()", "severity": "high", "cwe": "CWE-78"},
|
|
{"pattern": r"\bvm\.runInNewContext\s*\(", "sink": "vm.runInNewContext()", "severity": "critical", "cwe": "CWE-95"},
|
|
{"pattern": r"\bvm\.runInThisContext\s*\(", "sink": "vm.runInThisContext()", "severity": "critical", "cwe": "CWE-95"},
|
|
{"pattern": r"\brequire\s*\(\s*['\"]child_process['\"]\s*\)", "sink": "require('child_process')", "severity": "medium", "cwe": "CWE-78"},
|
|
{"pattern": r"`.*\$\{.*event.*\}`.*exec", "sink": "Template literal command injection", "severity": "critical", "cwe": "CWE-78"},
|
|
],
|
|
}
|
|
|
|
EVENT_DATA_ACCESSORS = [
|
|
r"event\s*\[",
|
|
r"event\s*\.",
|
|
r"event\.get\s*\(",
|
|
r"event\[.Records.\]",
|
|
r"event\.body",
|
|
r"event\.headers",
|
|
r"event\.queryStringParameters",
|
|
r"event\.pathParameters",
|
|
r"event\.requestContext",
|
|
]
|
|
|
|
|
|
def detect_runtime_family(runtime):
|
|
"""Map Lambda runtime to language family."""
|
|
if not runtime:
|
|
return "unknown"
|
|
runtime_lower = runtime.lower()
|
|
if "python" in runtime_lower:
|
|
return "python"
|
|
if "node" in runtime_lower:
|
|
return "nodejs"
|
|
if "java" in runtime_lower:
|
|
return "java"
|
|
if "go" in runtime_lower:
|
|
return "go"
|
|
if "ruby" in runtime_lower:
|
|
return "ruby"
|
|
if "dotnet" in runtime_lower:
|
|
return "dotnet"
|
|
return "unknown"
|
|
|
|
|
|
def enumerate_functions(lambda_client):
|
|
"""Enumerate all Lambda functions with their configurations."""
|
|
functions = []
|
|
paginator = lambda_client.get_paginator("list_functions")
|
|
for page in paginator.paginate():
|
|
for func in page["Functions"]:
|
|
func_info = {
|
|
"function_name": func["FunctionName"],
|
|
"function_arn": func["FunctionArn"],
|
|
"runtime": func.get("Runtime", "container"),
|
|
"runtime_family": detect_runtime_family(func.get("Runtime")),
|
|
"handler": func.get("Handler"),
|
|
"role": func["Role"],
|
|
"memory_size": func.get("MemorySize"),
|
|
"timeout": func.get("Timeout"),
|
|
"last_modified": func.get("LastModified"),
|
|
"layers": [l["Arn"] for l in func.get("Layers", [])],
|
|
"environment_variables": list(func.get("Environment", {}).get("Variables", {}).keys()),
|
|
"has_function_url": False,
|
|
"has_secrets_in_env": False,
|
|
}
|
|
|
|
# Check for secrets in environment variable names
|
|
secret_patterns = ["KEY", "SECRET", "PASSWORD", "TOKEN", "CREDENTIAL", "API_KEY", "PRIVATE"]
|
|
for var_name in func_info["environment_variables"]:
|
|
if any(pat in var_name.upper() for pat in secret_patterns):
|
|
func_info["has_secrets_in_env"] = True
|
|
break
|
|
|
|
# Check for function URL
|
|
try:
|
|
url_config = lambda_client.get_function_url_config(FunctionName=func["FunctionName"])
|
|
func_info["has_function_url"] = True
|
|
func_info["function_url_auth"] = url_config.get("AuthType", "UNKNOWN")
|
|
except ClientError:
|
|
pass
|
|
|
|
functions.append(func_info)
|
|
|
|
logger.info("Enumerated %d Lambda functions", len(functions))
|
|
return functions
|
|
|
|
|
|
def get_event_source_mappings(lambda_client):
|
|
"""Get all event source mappings to identify injection entry points."""
|
|
mappings = []
|
|
paginator = lambda_client.get_paginator("list_event_source_mappings")
|
|
for page in paginator.paginate():
|
|
for mapping in page["EventSourceMappings"]:
|
|
source_arn = mapping.get("EventSourceArn", "")
|
|
source_type = "unknown"
|
|
if ":sqs:" in source_arn:
|
|
source_type = "SQS"
|
|
elif ":dynamodb:" in source_arn:
|
|
source_type = "DynamoDB Stream"
|
|
elif ":kinesis:" in source_arn:
|
|
source_type = "Kinesis Stream"
|
|
elif ":kafka" in source_arn:
|
|
source_type = "Kafka"
|
|
elif ":mq:" in source_arn:
|
|
source_type = "MQ"
|
|
|
|
mappings.append({
|
|
"function_arn": mapping.get("FunctionArn"),
|
|
"event_source_arn": source_arn,
|
|
"source_type": source_type,
|
|
"state": mapping.get("State"),
|
|
"batch_size": mapping.get("BatchSize"),
|
|
})
|
|
|
|
logger.info("Found %d event source mappings", len(mappings))
|
|
return mappings
|
|
|
|
|
|
def download_and_scan_function(lambda_client, function_name, runtime_family, work_dir):
|
|
"""Download function code and scan for injection patterns."""
|
|
findings = []
|
|
try:
|
|
response = lambda_client.get_function(FunctionName=function_name)
|
|
code_location = response["Code"]["Location"]
|
|
|
|
import urllib.request
|
|
zip_path = os.path.join(work_dir, f"{function_name}.zip")
|
|
req = urllib.request.Request(code_location)
|
|
with urllib.request.urlopen(req, timeout=60) as resp, open(zip_path, "wb") as out:
|
|
out.write(resp.read())
|
|
|
|
extract_dir = os.path.join(work_dir, function_name)
|
|
os.makedirs(extract_dir, exist_ok=True)
|
|
|
|
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
zf.extractall(extract_dir)
|
|
|
|
# Determine file extensions to scan
|
|
extensions = {
|
|
"python": [".py"],
|
|
"nodejs": [".js", ".mjs", ".ts"],
|
|
"java": [".java"],
|
|
"go": [".go"],
|
|
"ruby": [".rb"],
|
|
}
|
|
target_exts = extensions.get(runtime_family, [".py", ".js"])
|
|
|
|
patterns = INJECTION_PATTERNS.get(runtime_family, [])
|
|
|
|
for root, dirs, files in os.walk(extract_dir):
|
|
# Skip node_modules and vendor directories
|
|
dirs[:] = [d for d in dirs if d not in ("node_modules", "vendor", "__pycache__", ".git")]
|
|
|
|
for filename in files:
|
|
if not any(filename.endswith(ext) for ext in target_exts):
|
|
continue
|
|
|
|
filepath = os.path.join(root, filename)
|
|
relative_path = os.path.relpath(filepath, extract_dir)
|
|
|
|
try:
|
|
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
|
|
lines = f.readlines()
|
|
except Exception:
|
|
continue
|
|
|
|
for line_num, line in enumerate(lines, 1):
|
|
for pattern_info in patterns:
|
|
if re.search(pattern_info["pattern"], line):
|
|
# Check if event data flows into this sink
|
|
context_start = max(0, line_num - 10)
|
|
context_lines = lines[context_start:line_num]
|
|
context_text = "".join(context_lines)
|
|
|
|
event_data_involved = any(
|
|
re.search(accessor, context_text)
|
|
for accessor in EVENT_DATA_ACCESSORS
|
|
)
|
|
|
|
findings.append({
|
|
"function_name": function_name,
|
|
"file": relative_path,
|
|
"line": line_num,
|
|
"code": line.strip()[:200],
|
|
"sink": pattern_info["sink"],
|
|
"severity": pattern_info["severity"],
|
|
"cwe": pattern_info["cwe"],
|
|
"event_data_flow": event_data_involved,
|
|
"confidence": "high" if event_data_involved else "medium",
|
|
})
|
|
|
|
except ClientError as e:
|
|
logger.warning("Cannot download %s: %s", function_name, e)
|
|
except Exception as e:
|
|
logger.warning("Error scanning %s: %s", function_name, e)
|
|
|
|
return findings
|
|
|
|
|
|
def audit_layers(lambda_client, functions):
|
|
"""Audit Lambda layers for security issues."""
|
|
findings = []
|
|
layer_accounts = {}
|
|
account_id = None
|
|
|
|
for func in functions:
|
|
for layer_arn in func.get("layers", []):
|
|
# Extract account ID from layer ARN
|
|
parts = layer_arn.split(":")
|
|
if len(parts) >= 5:
|
|
layer_account = parts[4]
|
|
if account_id is None:
|
|
# Get our own account ID from function ARN
|
|
func_parts = func["function_arn"].split(":")
|
|
if len(func_parts) >= 5:
|
|
account_id = func_parts[4]
|
|
|
|
if layer_account != account_id and account_id:
|
|
findings.append({
|
|
"type": "external_layer",
|
|
"function_name": func["function_name"],
|
|
"layer_arn": layer_arn,
|
|
"layer_account": layer_account,
|
|
"severity": "high",
|
|
"description": f"Function uses layer from external account {layer_account}",
|
|
})
|
|
|
|
layer_accounts.setdefault(layer_arn, []).append(func["function_name"])
|
|
|
|
# Check for layers used by many functions (high-impact if compromised)
|
|
for layer_arn, func_names in layer_accounts.items():
|
|
if len(func_names) >= 5:
|
|
findings.append({
|
|
"type": "high_impact_layer",
|
|
"layer_arn": layer_arn,
|
|
"affected_functions": func_names,
|
|
"severity": "medium",
|
|
"description": f"Layer is shared across {len(func_names)} functions - compromise would be high impact",
|
|
})
|
|
|
|
return findings
|
|
|
|
|
|
def detect_privilege_escalation_paths(iam_client, functions):
|
|
"""Identify Lambda functions with overprivileged execution roles."""
|
|
findings = []
|
|
checked_roles = {}
|
|
|
|
dangerous_actions = [
|
|
"iam:PassRole", "iam:CreateUser", "iam:CreateRole", "iam:AttachRolePolicy",
|
|
"iam:AttachUserPolicy", "iam:PutRolePolicy", "iam:PutUserPolicy",
|
|
"iam:CreateAccessKey", "iam:UpdateAssumeRolePolicy",
|
|
"lambda:UpdateFunctionCode", "lambda:UpdateFunctionConfiguration",
|
|
"lambda:CreateFunction", "lambda:InvokeFunction",
|
|
"sts:AssumeRole",
|
|
]
|
|
|
|
for func in functions:
|
|
role_arn = func["role"]
|
|
role_name = role_arn.split("/")[-1]
|
|
|
|
if role_name in checked_roles:
|
|
role_findings = checked_roles[role_name]
|
|
else:
|
|
role_findings = {"dangerous_permissions": [], "has_wildcard_resource": False, "has_admin": False}
|
|
|
|
try:
|
|
# Check attached policies
|
|
attached = iam_client.list_attached_role_policies(RoleName=role_name)
|
|
for policy in attached["AttachedPolicies"]:
|
|
if policy["PolicyName"] in ("AdministratorAccess", "PowerUserAccess"):
|
|
role_findings["has_admin"] = True
|
|
|
|
try:
|
|
policy_info = iam_client.get_policy(PolicyArn=policy["PolicyArn"])
|
|
version_id = policy_info["Policy"]["DefaultVersionId"]
|
|
policy_doc = iam_client.get_policy_version(
|
|
PolicyArn=policy["PolicyArn"], VersionId=version_id
|
|
)
|
|
for stmt in policy_doc["PolicyVersion"]["Document"].get("Statement", []):
|
|
if stmt.get("Effect") != "Allow":
|
|
continue
|
|
actions = stmt.get("Action", [])
|
|
if isinstance(actions, str):
|
|
actions = [actions]
|
|
resources = stmt.get("Resource", [])
|
|
if isinstance(resources, str):
|
|
resources = [resources]
|
|
|
|
if "*" in actions:
|
|
role_findings["has_admin"] = True
|
|
if "*" in resources:
|
|
role_findings["has_wildcard_resource"] = True
|
|
|
|
for action in actions:
|
|
if action in dangerous_actions or action == "*":
|
|
role_findings["dangerous_permissions"].append(action)
|
|
except ClientError:
|
|
continue
|
|
|
|
# Check inline policies
|
|
inline = iam_client.list_role_policies(RoleName=role_name)
|
|
for policy_name in inline["PolicyNames"]:
|
|
try:
|
|
policy_doc = iam_client.get_role_policy(
|
|
RoleName=role_name, PolicyName=policy_name
|
|
)
|
|
for stmt in policy_doc["PolicyDocument"].get("Statement", []):
|
|
if stmt.get("Effect") != "Allow":
|
|
continue
|
|
actions = stmt.get("Action", [])
|
|
if isinstance(actions, str):
|
|
actions = [actions]
|
|
for action in actions:
|
|
if action in dangerous_actions or action == "*":
|
|
role_findings["dangerous_permissions"].append(action)
|
|
except ClientError:
|
|
continue
|
|
|
|
except ClientError as e:
|
|
logger.warning("Cannot audit role %s: %s", role_name, e)
|
|
|
|
checked_roles[role_name] = role_findings
|
|
|
|
if role_findings["has_admin"]:
|
|
findings.append({
|
|
"type": "admin_execution_role",
|
|
"function_name": func["function_name"],
|
|
"role": role_name,
|
|
"severity": "critical",
|
|
"description": "Function has administrative execution role - any code modification grants full account access",
|
|
})
|
|
elif role_findings["dangerous_permissions"]:
|
|
findings.append({
|
|
"type": "dangerous_permissions",
|
|
"function_name": func["function_name"],
|
|
"role": role_name,
|
|
"permissions": list(set(role_findings["dangerous_permissions"])),
|
|
"severity": "high",
|
|
"description": f"Execution role has dangerous permissions: {', '.join(set(role_findings['dangerous_permissions']))}",
|
|
})
|
|
|
|
return findings
|
|
|
|
|
|
def check_cloudtrail_for_modifications(cloudtrail_client, days_back=7):
|
|
"""Search CloudTrail for suspicious Lambda modifications."""
|
|
findings = []
|
|
end_time = datetime.now(timezone.utc)
|
|
start_time = end_time - timedelta(days=days_back)
|
|
|
|
suspicious_events = [
|
|
"UpdateFunctionCode20150331v2",
|
|
"UpdateFunctionConfiguration20150331v2",
|
|
"PublishLayerVersion20181031",
|
|
"AddLayerVersionPermission20181031",
|
|
"CreateFunction20150331",
|
|
]
|
|
|
|
for event_name in suspicious_events:
|
|
try:
|
|
response = cloudtrail_client.lookup_events(
|
|
LookupAttributes=[
|
|
{"AttributeKey": "EventName", "AttributeValue": event_name}
|
|
],
|
|
StartTime=start_time,
|
|
EndTime=end_time,
|
|
MaxResults=50,
|
|
)
|
|
for event in response.get("Events", []):
|
|
ct_event = json.loads(event.get("CloudTrailEvent", "{}"))
|
|
req_params = ct_event.get("requestParameters", {})
|
|
|
|
finding = {
|
|
"event_name": event_name,
|
|
"time": event["EventTime"].isoformat(),
|
|
"user": event.get("Username"),
|
|
"source_ip": ct_event.get("sourceIPAddress"),
|
|
"user_agent": ct_event.get("userAgent", "")[:100],
|
|
"function_name": req_params.get("functionName"),
|
|
"suspicious": False,
|
|
"indicators": [],
|
|
}
|
|
|
|
# Flag suspicious patterns
|
|
user_agent = ct_event.get("userAgent", "")
|
|
if "console.amazonaws.com" not in user_agent and "cloudformation" not in user_agent.lower():
|
|
if "UpdateFunctionCode" in event_name:
|
|
finding["suspicious"] = True
|
|
finding["indicators"].append("Function code updated outside console/CloudFormation")
|
|
|
|
# Check for role changes
|
|
if "role" in req_params and "UpdateFunctionConfiguration" in event_name:
|
|
finding["suspicious"] = True
|
|
finding["indicators"].append(f"Execution role changed to: {req_params['role']}")
|
|
|
|
# Check for layer additions
|
|
if "layers" in req_params and "UpdateFunctionConfiguration" in event_name:
|
|
finding["suspicious"] = True
|
|
finding["indicators"].append(f"Layers modified: {req_params['layers']}")
|
|
|
|
# Off-hours modification
|
|
event_hour = event["EventTime"].hour
|
|
if event_hour < 6 or event_hour > 22:
|
|
finding["indicators"].append(f"Modification at unusual hour: {event_hour}:00 UTC")
|
|
|
|
findings.append(finding)
|
|
|
|
except ClientError as e:
|
|
logger.warning("CloudTrail query failed for %s: %s", event_name, e)
|
|
|
|
return findings
|
|
|
|
|
|
def check_function_url_security(lambda_client, functions):
|
|
"""Check Lambda function URLs for insecure authentication."""
|
|
findings = []
|
|
for func in functions:
|
|
if func.get("has_function_url") and func.get("function_url_auth") == "NONE":
|
|
findings.append({
|
|
"type": "unauthenticated_function_url",
|
|
"function_name": func["function_name"],
|
|
"severity": "high",
|
|
"description": "Function URL has AuthType=NONE - publicly accessible without authentication",
|
|
})
|
|
return findings
|
|
|
|
|
|
def generate_report(functions, event_sources, injection_findings, layer_findings,
|
|
escalation_findings, cloudtrail_findings, url_findings):
|
|
"""Generate comprehensive serverless injection detection report."""
|
|
|
|
all_findings = []
|
|
for f in injection_findings:
|
|
f["category"] = "code_injection"
|
|
all_findings.append(f)
|
|
for f in layer_findings:
|
|
f["category"] = "layer_security"
|
|
all_findings.append(f)
|
|
for f in escalation_findings:
|
|
f["category"] = "privilege_escalation"
|
|
all_findings.append(f)
|
|
for f in cloudtrail_findings:
|
|
if f.get("suspicious"):
|
|
f["category"] = "suspicious_modification"
|
|
f["severity"] = "high"
|
|
all_findings.append(f)
|
|
for f in url_findings:
|
|
f["category"] = "function_url"
|
|
all_findings.append(f)
|
|
|
|
critical = [f for f in all_findings if f.get("severity") == "critical"]
|
|
high = [f for f in all_findings if f.get("severity") == "high"]
|
|
|
|
report = {
|
|
"report_type": "Serverless Function Injection Assessment",
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"summary": {
|
|
"functions_analyzed": len(functions),
|
|
"event_source_mappings": len(event_sources),
|
|
"total_findings": len(all_findings),
|
|
"critical_findings": len(critical),
|
|
"high_findings": len(high),
|
|
"injection_sinks_found": len(injection_findings),
|
|
"layer_issues": len(layer_findings),
|
|
"escalation_paths": len(escalation_findings),
|
|
"suspicious_modifications": len([f for f in cloudtrail_findings if f.get("suspicious")]),
|
|
},
|
|
"findings": all_findings,
|
|
"functions": functions,
|
|
"event_source_mappings": event_sources,
|
|
"cloudtrail_events": cloudtrail_findings,
|
|
}
|
|
|
|
return report
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Serverless Function Injection Detection Agent")
|
|
parser.add_argument("--region", default="us-east-1", help="AWS region")
|
|
parser.add_argument("--functions", nargs="+", help="Specific function names to scan (default: all)")
|
|
parser.add_argument("--scan-code", action="store_true", help="Download and scan function code for injection sinks")
|
|
parser.add_argument("--cloudtrail-days", type=int, default=7, help="Days of CloudTrail history to search")
|
|
parser.add_argument("--output", default="serverless_injection_report.json", help="Output report file")
|
|
args = parser.parse_args()
|
|
|
|
session = boto3.Session(region_name=args.region)
|
|
lambda_client = session.client("lambda")
|
|
iam_client = session.client("iam")
|
|
cloudtrail_client = session.client("cloudtrail")
|
|
|
|
logger.info("Starting serverless function injection detection in %s", args.region)
|
|
|
|
# Step 1: Enumerate functions
|
|
all_functions = enumerate_functions(lambda_client)
|
|
if args.functions:
|
|
all_functions = [f for f in all_functions if f["function_name"] in args.functions]
|
|
|
|
# Step 2: Get event source mappings
|
|
event_sources = get_event_source_mappings(lambda_client)
|
|
|
|
# Step 3: Scan code for injection patterns
|
|
injection_findings = []
|
|
if args.scan_code:
|
|
work_dir = tempfile.mkdtemp(prefix="lambda_scan_")
|
|
try:
|
|
for func in all_functions:
|
|
if func["runtime_family"] in INJECTION_PATTERNS:
|
|
logger.info("Scanning %s (%s)", func["function_name"], func["runtime"])
|
|
findings = download_and_scan_function(
|
|
lambda_client, func["function_name"],
|
|
func["runtime_family"], work_dir
|
|
)
|
|
injection_findings.extend(findings)
|
|
finally:
|
|
shutil.rmtree(work_dir, ignore_errors=True)
|
|
|
|
# Step 4: Audit layers
|
|
layer_findings = audit_layers(lambda_client, all_functions)
|
|
|
|
# Step 5: Detect privilege escalation paths
|
|
escalation_findings = detect_privilege_escalation_paths(iam_client, all_functions)
|
|
|
|
# Step 6: Check CloudTrail for suspicious modifications
|
|
cloudtrail_findings = check_cloudtrail_for_modifications(cloudtrail_client, args.cloudtrail_days)
|
|
|
|
# Step 7: Check function URL security
|
|
url_findings = check_function_url_security(lambda_client, all_functions)
|
|
|
|
# Generate report
|
|
report = generate_report(
|
|
all_functions, event_sources, injection_findings, layer_findings,
|
|
escalation_findings, cloudtrail_findings, url_findings
|
|
)
|
|
|
|
with open(args.output, "w") as f:
|
|
json.dump(report, f, indent=2, default=str)
|
|
logger.info("Report saved to %s", args.output)
|
|
|
|
summary = report["summary"]
|
|
logger.info(
|
|
"Assessment complete: %d functions, %d findings (%d critical, %d high)",
|
|
summary["functions_analyzed"],
|
|
summary["total_findings"],
|
|
summary["critical_findings"],
|
|
summary["high_findings"],
|
|
)
|
|
|
|
if summary["critical_findings"] > 0:
|
|
logger.warning("CRITICAL FINDINGS DETECTED:")
|
|
for f in report["findings"]:
|
|
if f.get("severity") == "critical":
|
|
logger.warning(" [%s] %s: %s", f.get("category", ""), f.get("function_name", ""), f.get("sink", f.get("description", "")))
|
|
|
|
return 0 if summary["critical_findings"] == 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|