Project Genesis

Signed-off-by: mharb <mharb@noreply.localhost>
This commit is contained in:
2026-02-22 05:29:23 +00:00
commit b8745e093b
27 changed files with 1576 additions and 0 deletions

11
lambda/config.py Normal file
View File

@@ -0,0 +1,11 @@
"""Configuration constants for image processor"""
ALLOWED_FORMATS = {'JPEG', 'JPG', 'PNG', 'WEBP'}
MAX_DIMENSION = 4096
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
RESIZE_TARGET = (1024, 1024)
THUMBNAIL_TARGET = (200, 200)
DYNAMODB_TTL_DAYS = 90
DYNAMODB_TTL_SECONDS = DYNAMODB_TTL_DAYS * 24 * 60 * 60 # 7776000

74
lambda/image_processor.py Normal file
View File

@@ -0,0 +1,74 @@
"""Image processing operations"""
import io
import hashlib
from datetime import datetime
from PIL import Image
from config import (
ALLOWED_FORMATS, MAX_DIMENSION, MAX_FILE_SIZE,
RESIZE_TARGET, THUMBNAIL_TARGET
)
def validate_image(img_data: bytes) -> tuple[Image.Image, str]:
"""Validate and open image data"""
if len(img_data) > MAX_FILE_SIZE:
raise ValueError(f'File too large: {len(img_data)} bytes')
img_hash = hashlib.sha256(img_data).hexdigest()
img = Image.open(io.BytesIO(img_data))
if img.format not in ALLOWED_FORMATS:
raise ValueError(f'Invalid format: {img.format}')
if img.width * img.height > MAX_DIMENSION ** 2:
raise ValueError(f'Image too large: {img.width}x{img.height}')
return img, img_hash
def determine_processing(filename: str) -> tuple[tuple[int, int], str]:
"""Determine processing type based on filename"""
fn = filename.lower()
if '_thumb' in fn:
return THUMBNAIL_TARGET, 'resize_200x200'
elif '_grayscale' in fn:
return None, 'grayscale'
else:
return RESIZE_TARGET, 'resize_1024x1024'
def process_image(img: Image.Image, target: tuple[int, int], ptype: str) -> Image.Image:
"""Apply image processing transformations"""
if ptype == 'grayscale':
return img.convert('L')
elif target:
img.thumbnail(target, Image.Resampling.LANCZOS)
return img
def save_image(img: Image.Image, original_format: str) -> tuple[bytes, str]:
"""Save processed image to bytes"""
output = io.BytesIO()
fmt = original_format or 'JPEG'
img.save(output, format=fmt, quality=85)
return output.getvalue(), f'image/{fmt.lower()}'
def get_processed_key(original_key: str) -> str:
"""Generate processed object key"""
return original_key.replace('uploads/', 'processed/')
def build_result(original_key: str, processed_key: str, orig_size: tuple,
img: Image.Image, ptype: str, img_hash: str) -> dict:
"""Build processing result dictionary"""
return {
'status': 'success',
'original_size': orig_size,
'processed_size': img.size,
'processing_type': ptype,
'processed_key': processed_key,
'timestamp': datetime.utcnow().isoformat(),
'hash': img_hash
}

52
lambda/lambda_function.py Normal file
View File

@@ -0,0 +1,52 @@
"""AWS Lambda Image Processor - Security Hardened"""
import os
from image_processor import (
validate_image, determine_processing, process_image,
save_image, get_processed_key, build_result
)
from storage import write_metadata, upload_processed, get_object
from notifications import send_notification
BUCKET = os.environ.get('S3_BUCKET', '')
TABLE = os.environ['DYNAMODB_TABLE']
TOPIC = os.environ['SNS_TOPIC_ARN']
ENV = os.environ.get('ENVIRONMENT', 'prod')
def lambda_handler(event: dict, context) -> dict:
"""Main Lambda handler for image processing"""
for r in event.get('Records', []):
bucket = r['s3']['bucket']['name']
key = r['s3']['object']['key']
if not key.startswith('uploads/'):
continue
try:
filename = os.path.basename(key)
# Get and validate image
img_data, size = get_object(bucket, key)
img, img_hash = validate_image(img_data)
# Process image
target, ptype = determine_processing(filename)
img = process_image(img, target, ptype)
# Save and upload
output_data, content_type = save_image(img, img.format)
processed_key = get_processed_key(key)
upload_processed(bucket, processed_key, output_data, content_type,
{'original_hash': img_hash, 'processed_by': 'image-processor'})
# Build result and store metadata
result = build_result(key, processed_key, img.size, img, ptype, img_hash)
write_metadata(filename, os.path.basename(processed_key), result)
send_notification(filename, result, 'success')
except Exception as e:
send_notification(key, {'error': str(e)}, 'error')
raise
return {'statusCode': 200}

24
lambda/notifications.py Normal file
View File

@@ -0,0 +1,24 @@
"""Notification operations for SNS"""
import os
import json
from datetime import datetime
import boto3
sns = boto3.client('sns')
TOPIC = os.environ['SNS_TOPIC_ARN']
ENV = os.environ.get('ENVIRONMENT', 'prod')
def send_notification(filename: str, result: dict, status: str) -> None:
"""Send SNS notification"""
sns.publish(
TopicArn=TOPIC,
Subject=f'Image Processing {status.title()}',
Message=json.dumps({
'filename': filename,
'status': status,
'details': result,
'timestamp': datetime.utcnow().isoformat(),
'environment': ENV
}, indent=2)
)

1
lambda/requirements.txt Normal file
View File

@@ -0,0 +1 @@
Pillow==10.4.0

46
lambda/storage.py Normal file
View File

@@ -0,0 +1,46 @@
"""Storage operations for DynamoDB and S3"""
import os
import time
import boto3
from config import DYNAMODB_TTL_SECONDS
dynamodb = boto3.resource('dynamodb')
s3 = boto3.client('s3')
TABLE = os.environ['DYNAMODB_TABLE']
ENV = os.environ.get('ENVIRONMENT', 'prod')
def write_metadata(filename: str, processed_filename: str, result: dict) -> None:
"""Write processing metadata to DynamoDB"""
dynamodb.Table(TABLE).put_item(Item={
'filename': filename,
'processed_filename': processed_filename,
'timestamp': result['timestamp'],
'processing_type': result['processing_type'],
'status': result['status'],
'original_size': str(result['original_size']),
'processed_size': str(result['processed_size']),
'hash': result.get('hash', ''),
'ttl': int(time.time()) + DYNAMODB_TTL_SECONDS,
'environment': ENV
})
def upload_processed(bucket: str, key: str, data: bytes, content_type: str,
metadata: dict) -> None:
"""Upload processed image to S3"""
s3.put_object(
Bucket=bucket,
Key=key,
Body=data,
ContentType=content_type,
Metadata=metadata
)
def get_object(bucket: str, key: str) -> tuple[bytes, int]:
"""Get object from S3"""
obj = s3.get_object(Bucket=bucket, Key=key)
return obj['Body'].read(), obj['ContentLength']