AWSTemplateFormatVersion: '2010-09-09'
Description: 'CloudFormation template for S3 notification processor Lambda'

Parameters:
  S3BucketName:
    Type: String
    Description: Name of the S3 bucket where JSON notification files are stored

  WebServiceEndpoint:
    Type: String
    Description: Default endpoint for the web service

  LambdaTimeout:
    Type: Number
    Description: Timeout for the Lambda function in seconds
    Default: 30

  LambdaMemorySize:
    Type: Number
    Description: Memory allocation for the Lambda function in MB
    Default: 256

Resources:
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
      Policies:
        - PolicyName: S3ReadAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - 's3:GetObject'
                Resource: !Sub 'arn:aws:s3:::${S3BucketName}/*'

  NotificationProcessorFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: notification-processor
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Runtime: nodejs18.x
      Timeout: !Ref LambdaTimeout
      MemorySize: !Ref LambdaMemorySize
      Environment:
        Variables:
          DEFAULT_WEBSERVICE_ENDPOINT: !Ref WebServiceEndpoint
      Code:
        ZipFile: |
          const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
          const https = require('https');
          const http = require('http');
          const url = require('url');

          const s3Client = new S3Client();

          /**
          * Processes the notification data received from S3. The below documentation describes the expected format for a BP rule notification.
          * If this function is not overridden then the read json will be sent to the endpoint without any modifications.
          *
          * @param {Object} notificationData - The JSON data parsed from the S3 notification file
          * @param {Array} notificationData.providers - List of AWS account providers
          * @param {Object} notificationData.providers[] - Provider information
          * @param {string} notificationData.providers[].id - AWS account ID
          * @param {string} notificationData.providers[].name - Account name/description
          *
          * @param {Array} notificationData.reports - List of compliance/well-architected reports
          * @param {Object} notificationData.reports[] - Individual report
          * @param {string} notificationData.reports[].category - Report category (e.g., "Compliance & Availability")
          * @param {string} notificationData.reports[].product - Product or service area (e.g., "Well-Architected - Architecture")
          * @param {string} notificationData.reports[].id - Unique report identifier
          * @param {string} notificationData.reports[].rule - Name of the rule being checked
          * @param {string} notificationData.reports[].description - Detailed description of the rule
          * @param {number} notificationData.reports[].instant - Unix timestamp (milliseconds)
          * @param {string} notificationData.reports[].executionDateTimeUtc - ISO-formatted execution date/time
          * @param {Array<string>} notificationData.reports[].headers - Column headers for the results
          * @param {Array<string>} notificationData.reports[].certifications - Related compliance certifications
          *
          * @param {Object} notificationData.reports[].changes - Changes detected in compliance state
          * @param {Array} notificationData.reports[].changes.unchanged - Items that haven't changed since last check
          * @param {Array} notificationData.reports[].changes.removed - Items that were previously flagged but are now resolved
          * @param {Array} notificationData.reports[].changes.added - New items that have been flagged (if any)
          *
          * @param {Object} notificationData.reports[].changes.unchanged[] - Provider with unchanged items
          * @param {string} notificationData.reports[].changes.unchanged[].provider - AWS account ID
          * @param {Array} notificationData.reports[].changes.unchanged[].results - List of unchanged findings
          *
          * @param {Object} notificationData.reports[].changes.removed[] - Provider with removed/resolved items
          * @param {string} notificationData.reports[].changes.removed[].provider - AWS account ID
          * @param {Array} notificationData.reports[].changes.removed[].results - List of resolved findings
          *
          * @param {Object} notificationData.reports[].changes.[type].results[] - Individual finding
          * @param {string} notificationData.reports[].changes.[type].results[].identifier - Unique hash of the finding
          * @param {Array<string>} notificationData.reports[].changes.[type].results[].columns - Values corresponding to headers
          * @param {string} notificationData.reports[].changes.[type].results[].kind - Severity ("WARN", "ERROR", etc.)
          *
          * @param {Array} notificationData.reports[].conditions - Filtering conditions applied to the report
          * @param {Object} notificationData.reports[].conditions[] - Individual filter condition
          * @param {string} notificationData.reports[].conditions[].name - Name of the filter
          * @param {string} notificationData.reports[].conditions[].option - Field being filtered
          * @param {string} notificationData.reports[].conditions[].operation - Comparison operator
          * @param {string} notificationData.reports[].conditions[].value - Value to compare against
          *
          * @param {string} notificationData.reports[].kind - Overall report severity
          *
          * To override the default endpoint provided by cloud formation:
          * @param {Object} [notificationData.webserviceEndpoint] - Optional custom endpoint to send results to
          *
          * @returns {Object} The processed payload to be sent to the web service
          */
          function preparePayload(notificationData) {
              // Write your integration code here, for instance, filing a ticket
              
              return notificationData;
          }

          async function streamToString(stream) {
              return new Promise((resolve, reject) => {
                  const chunks = [];
                  stream.on('data', (chunk) => chunks.push(chunk));
                  stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
                  stream.on('error', reject);
              });
          }

          function callWebService(endpoint, payload) {
              return new Promise((resolve, reject) => {
                  const parsedUrl = url.parse(endpoint);
                  const options = {
                      hostname: parsedUrl.hostname,
                      port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
                      path: parsedUrl.path,
                      method: 'POST',
                      headers: {
                          'Content-Type': 'application/json',
                          'Content-Length': Buffer.byteLength(JSON.stringify(payload))
                      }
                  };

                  const httpModule = parsedUrl.protocol === 'https:' ? https : http;

                  const req = httpModule.request(options, (res) => {
                      let data = '';

                      res.on('data', (chunk) => {
                          data += chunk;
                      });

                      res.on('end', () => {
                          if (res.statusCode >= 200 && res.statusCode < 300) {
                              try {
                                  resolve(JSON.parse(data));
                              } catch (e) {
                                  resolve(data);
                              }
                          } else {
                              reject(new Error(`HTTP error ${res.statusCode}: ${data}`));
                          }
                      });
                  });

                  req.on('error', (error) => {
                      reject(error);
                  });

                  req.write(JSON.stringify(payload));
                  req.end();
              });
          }

          exports.handler = async (event) => {
              try {
                  const record = event.Records[0];
                  const bucket = record.s3.bucket.name;
                  const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));

                  console.log(`Processing file: s3://${bucket}/${key}`);

                  const getObjectCommand = new GetObjectCommand({
                      Bucket: bucket,
                      Key: key
                  });
                  const s3Response = await s3Client.send(getObjectCommand);
          
                  const bodyContents = await streamToString(s3Response.Body);
          
                  const notificationData = JSON.parse(bodyContents);

                  const webserviceEndpoint = notificationData.webserviceEndpoint || 
                                         process.env.DEFAULT_WEBSERVICE_ENDPOINT;

                  if (!webserviceEndpoint) {
                      throw new Error('No webservice endpoint specified');
                  }

                  const payload = preparePayload(notificationData);

                  const result = await callWebService(webserviceEndpoint, payload);

                  return {
                      statusCode: 200,
                      body: JSON.stringify({
                          message: 'Notification processed successfully',
                          webserviceResponse: result
                      })
                  };
              } catch (error) {
                  console.error('Error processing notification:', error);
                  throw error;
              }
          };

  LambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref NotificationProcessorFunction
      Principal: s3.amazonaws.com
      SourceAccount: !Ref 'AWS::AccountId'
      SourceArn: !Sub 'arn:aws:s3:::${S3BucketName}'

Outputs:
  LambdaFunctionName:
    Description: "Name of the Lambda function"
    Value: !Ref NotificationProcessorFunction

  LambdaFunctionARN:
    Description: "ARN of the Lambda function"
    Value: !GetAtt NotificationProcessorFunction.Arn
