AWSTemplateFormatVersion: "2010-09-09"
Description: Provision Products
Resources:
  FunctionRole:
    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/AmazonSSMReadOnlyAccess
        - arn:aws:iam::aws:policy/service-role/AWSConfigRulesExecutionRole
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  Permission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !Ref Function
      Principal: "config.amazonaws.com"
      SourceAccount: !Ref AWS::AccountId

  Function:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Runtime: nodejs8.10
      Timeout: 10
      Role: !GetAtt FunctionRole.Arn
      Code:
        ZipFile: |
          const assert = require("assert")
          const { ConfigService, SSM } = require("aws-sdk")

          async function ItShouldHaveUniqueAccounts(configuration) {
              const { outputs } = configuration
              if (!outputs) {
                  throw new Error("Could not read stack outputs");
              }

              const DevelopmentAccountOutput = 
                  outputs.find(o => o.outputKey === "DevelopmentAccountParameter");

              const ProductionAccountOutput =
                  outputs.find(o => o.outputKey === "ProductionAccountParameter");

              assert(DevelopmentAccountOutput !== undefined,
                  "Development account output is not defined");

              assert(ProductionAccountOutput !== undefined,
                  "Production account output is not defined");

              const result = await Promise.all([
                  new SSM().getParameter({
                      Name: DevelopmentAccountOutput.outputValue
                  }).promise(),
                  new SSM().getParameter({
                      Name: ProductionAccountOutput.outputValue
                  }).promise()
              ]);

              const ids = result.map(r => r.Parameter.Value);
              assert(new Set(ids).size === ids.length,
                  `Account IDs should be unique for development and production: ${ids}`);
          }

          // Compliance result is a convenience class for sending results back to AWS Config
          export class ComplianceResult {
              constructor(configService) {
                this.configService = configService;
              }

              async Response(type, resultToken, resources, annotation) {
                  const date = new Date()
                  const evaluations = resources.map(r => ({
                      Annotation: annotation,
                      ComplianceResourceId: r.resourceId,
                      ComplianceResourceType: r.resourceType,
                      ComplianceType: type,
                      OrderingTimestamp: date
                  }))

                  return await this.configService.putEvaluations({
                      Evaluations: evaluations,
                      ResultToken: resultToken
                  }).promise()
              }

              async NotApplicable(resultToken, resources, annotation) {
                  return await this.Response("NOT_APPLICABLE", resultToken, resources, annotation)
              }

              async Compliant(resultToken, resources, annotation) {
                  return await this.Response("COMPLIANT", resultToken, resources, annotation)
              }

              async NonCompliant(resultToken, resources, annotation) {
                  return await this.Response("NON_COMPLIANT", resultToken, resources, annotation)
              }
          }

          exports.handler = async (event, context, callback) => {
              const input = JSON.parse(event.invokingEvent);
              const { configuration, resourceType } = input.configurationItem;
              
              const config = new ConfigService();
              const compliance = new ComplianceResult(config);

              if (resourceType != "AWS::CloudFormation::Stack") {
                  await compliance.NotApplicable(event.resultToken);
                  throw new Error("Rule was not triggered on an AWS::CloudFormation::Stack");
              }

              const ppTag = configuration.tags.find(({ key }) => key === "aws:servicecatalog:provisionedProductArn")
              if (!ppTag) {
                  await compliance.NotApplicable(event.resultToken);
                  throw new Error("No provisioned product aws system tag on stack");
              }

              const ppId = ppTag.value.split("/").pop();
              
              const resources = [{ resourceId: ppId, resourceType: "AWS::ServiceCatalog::CloudFormationProvisionedProduct" }]
              try {
                  await ItShouldHaveUniqueAccounts(configuration);
                  return await compliance.Compliant(event.resultToken, resources);
              } catch (err) {
                  return await compliance.NonCompliant(event.resultToken, resources, err.message);
              }
          }
Outputs:
  FunctionArn:
    Description: SSM parameter containing development account ID
    Value: !GetAtt Function.Arn