"""Lambda Handler for Datadog SCA GitHub Issue Creation. This Lambda creates GitHub issues for third-party dependency updates identified by Datadog SCA (Software Composition Analysis). """ import hashlib import hmac import json import os from typing import Any, Dict, Optional import boto3 import sentry_sdk from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration from config import ENVIRONMENT, LAMBDA_NAME, SENTRY_DSN, logger from src.github_issue_processor import ( GitHubIssueProcessor, GitHubIssueProcessorException, ) # Initialize Sentry for error tracking if SENTRY_DSN: sentry_sdk.init( dsn=SENTRY_DSN, environment=ENVIRONMENT, integrations=[AwsLambdaIntegration(timeout_warning=True)] ) # Initialize Secrets Manager client secrets_client = boto3.client("secretsmanager") def get_secret_value(secret_name: str) -> str: """Retrieve a secret value from AWS Secrets Manager. Args: secret_name: The name or ARN of the secret to retrieve Returns: str: The secret value Raises: Exception: If the secret cannot be retrieved """ try: response = secrets_client.get_secret_value(SecretId=secret_name) return response["SecretString"] except Exception as e: logger.error(f"Failed to retrieve secret {secret_name}: {str(e)}") raise def validate_jira_webhook_signature( payload: str, signature_header: Optional[str], secret: str ) -> bool: """Validate the HMAC signature from Jira webhook. Jira webhooks include an X-Hub-Signature header with format "method=signature" where method is the hash algorithm (e.g., sha256) and signature is the HMAC hex digest of the payload signed with the webhook's secret token. Reference: https://developer.atlassian.com/cloud/jira/platform/webhooks/ #secure-admin-webhooks Args: payload: The raw request body as a string signature_header: The X-Hub-Signature header value (e.g., "sha256=abc123...") secret: The webhook secret token configured in Jira Returns: bool: True if the signature is valid, False otherwise Example: >>> secret = "It's a Secret to Everybody" >>> payload = "Hello World!" >>> signature = ( ... "sha256=a4771c39fbe90f317c7824e83ddef3caae9cb3d976c214" ... "ace1f2937e133263c9" ... ) >>> validate_jira_webhook_signature(payload, signature, secret) True """ if not signature_header: logger.warning("No X-Hub-Signature header present") return False try: # Parse the signature header format: "method=signature" if "=" not in signature_header: logger.warning( f"Invalid signature header format: {signature_header}" ) return False method, given_signature = signature_header.split("=", 1) # Currently Jira uses SHA-256, but check method in case it changes if method != "sha256": logger.warning(f"Unsupported hash method: {method}") return False # Calculate the expected HMAC signature hash_object = hmac.new( secret.encode("utf-8"), msg=payload.encode("utf-8"), digestmod=hashlib.sha256, ) calculated_signature = hash_object.hexdigest() # Use constant-time comparison to prevent timing attacks is_valid = hmac.compare_digest(calculated_signature, given_signature) if not is_valid: logger.warning( "Signature validation failed. " f"Expected: sha256={calculated_signature}, " f"Received: {signature_header}" ) return is_valid except Exception as e: logger.error(f"Error validating webhook signature: {str(e)}") return False def handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: """Lambda entry point for creating GitHub issues from Jira webhooks. Expected event payload structure from SQS: { "Records": [ { "messageId": "...", "receiptHandle": "...", "body": "{...}", # JSON string of the Jira webhook payload "attributes": {...}, "messageAttributes": {...}, "md5OfBody": "...", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:...", "awsRegion": "us-east-1" } ] } Expected Jira webhook body structure (based on Jira webhook events): { "timestamp": 1606480436302, "webhookEvent": "jira:issue_updated", "issue_event_type_name": "issue_generic", "user": { "self": "https://jira.example.com/rest/api/2/user?accountId=...", "accountId": "...", "displayName": "John Doe", ... }, "issue": { "id": "10001", "self": "https://jira.example.com/rest/api/2/issue/10001", "key": "PROJ-123", "fields": { "summary": "Issue summary", "description": "Issue description", "issuetype": {"name": "Bug"}, "project": {"key": "PROJ", "name": "Project Name"}, "customfield_xxxxx": "custom field values", ... } } } Args: event: Lambda event payload containing SQS records context: Lambda context object containing runtime information Returns: dict: Response containing batch processing results { "statusCode": 200, "body": { "processed": 5, "failed": 0, "results": [...] } } Raises: GitHubIssueProcessorException: For any processing errors """ logger.info(f"Starting {LAMBDA_NAME}") logger.debug(f"Event payload: {json.dumps(event)}") results = [] failed_count = 0 processed_count = 0 try: # Process each SQS record records = event.get("Records", []) logger.info(f"Processing {len(records)} SQS messages") for record in records: issue_key = "Unknown" # Initialize to handle error cases try: # Extract the message body (this is the Jira webhook payload) raw_body = record.get("body", "") # Parse the Jira webhook data try: webhook_data = json.loads(raw_body) except json.JSONDecodeError as e: error_msg = f"Invalid JSON in SQS message body: {str(e)}" logger.error(error_msg) failed_count += 1 results.append({ "messageId": record.get("messageId"), "status": "error", "error": error_msg }) continue # Extract Jira webhook fields webhook_event = webhook_data.get("webhookEvent", "") timestamp = webhook_data.get("timestamp") issue_data = webhook_data.get("issue", {}) user_data = webhook_data.get("user", {}) # Log webhook event details logger.info( f"Received Jira webhook event: {webhook_event} " f"at timestamp {timestamp}" ) # Extract issue fields issue_key = issue_data.get("key", "") issue_id = issue_data.get("id", "") fields = issue_data.get("fields", {}) # Extract user information user_display_name = user_data.get("displayName", "Unknown User") logger.info( f"Processing issue {issue_key} (ID: {issue_id}) " f"triggered by {user_display_name}" ) # Validate required fields from custom field mapping # Assuming custom fields are used to pass GitHub-related data repository_name = fields.get("customfield_repository") package_name = fields.get("customfield_package") current_version = fields.get("customfield_current_version") fixed_version = fields.get("customfield_fixed_version") required_fields = { "repository_name": repository_name, "package_name": package_name, "current_version": current_version, "fixed_version": fixed_version } missing_fields = [ field_name for field_name, field_value in required_fields.items() if not field_value ] if missing_fields: error_msg = ( "Missing required custom fields: " f"{', '.join(missing_fields)}" ) logger.error(error_msg) failed_count += 1 results.append({ "messageId": record.get("messageId"), "jira_issue": issue_key, "status": "error", "error": error_msg }) continue # Extract optional fields vulnerability_id = fields.get( "customfield_vulnerability_id", "Unknown" ) severity = fields.get("customfield_severity", "MEDIUM") description = fields.get("description", "") summary = fields.get("summary", "") logger.info( f"Processing dependency update for {package_name} " f"({current_version} -> {fixed_version}) in {repository_name}" ) # Create and execute processor processor = GitHubIssueProcessor( repository_name=repository_name, jira_issue=issue_key, package_name=package_name, current_version=current_version, fixed_version=fixed_version, vulnerability_id=vulnerability_id, severity=severity, description=description or summary ) result = processor.create_issue() logger.info( "Successfully created GitHub issue " f"#{result['issue_number']} at {result['issue_url']}" ) processed_count += 1 results.append({ "messageId": record.get("messageId"), "status": "success", "issue_url": result["issue_url"], "issue_number": result["issue_number"], "repository": repository_name, "package": package_name, "jira_issue": issue_key, "jira_event": webhook_event, "triggered_by": user_display_name }) except GitHubIssueProcessorException as e: logger.error(f"GitHub issue processor error: {str(e)}") failed_count += 1 results.append({ "messageId": record.get("messageId"), "jira_issue": issue_key, "status": "error", "error": str(e) }) except Exception as e: error_msg = f"Unexpected error processing message: {str(e)}" logger.error(error_msg) failed_count += 1 results.append({ "messageId": record.get("messageId"), "status": "error", "error": error_msg }) logger.info( f"Batch processing complete: {processed_count} successful, " f"{failed_count} failed" ) return { "statusCode": 200, "body": json.dumps({ "processed": processed_count, "failed": failed_count, "results": results }) } except Exception as e: error_msg = f"Unexpected error processing SQS batch: {str(e)}" logger.error(error_msg) return { "statusCode": 500, "body": json.dumps({"error": "Internal server error"}) }