constructor()

in source/resources/lib/compliance.ts [74:437]


  constructor(scope: Construct, id: string, props: NestedStackProps) {
    super(scope, id, props);
    const stack = Stack.of(this);
    this.account = stack.account; // Returns the AWS::AccountId for this stack (or the literal value if known)
    this.region = stack.region; // Returns the AWS::Region for this stack (or the literal value if known)

    //=============================================================================================
    // Parameters
    //=============================================================================================
    const uuid = new CfnParameter(this, "UUID", {
      description: "UUID for primary stack deployment",
      type: "String",
    });

    const metricsQueue = new CfnParameter(this, "MetricsQueue", {
      description: "Metrics queue for solution anonymous metrics",
      type: "String",
    });

    //=============================================================================================
    // Metadata
    //=============================================================================================
    this.templateOptions.metadata = {
      "AWS::CloudFormation::Interface": {
        ParameterGroups: [
          {
            Label: { default: "Shared Resource Configurations" },
            Parameters: [uuid.logicalId, metricsQueue.logicalId],
          },
        ],
        ParameterLabels: {
          [metricsQueue.logicalId]: {
            default: "Metric Queue",
          },
          [uuid.logicalId]: {
            default: "UUID",
          },
        },
      },
    };
    this.templateOptions.description = `(${manifest.solution.primarySolutionId}-cr) - The AWS CloudFormation template for deployment of the ${manifest.solution.name} compliance reporter resources. Version ${manifest.solution.solutionVersion}`;
    this.templateOptions.templateFormatVersion =
      manifest.solution.templateVersion;

    //=============================================================================================
    // Map
    //=============================================================================================
    const map = new CfnMapping(this, "PolicyStackMap", {
      mapping: {
        Metric: {
          SendAnonymousMetric: manifest.solution.sendMetric,
        },
        Solution: {
          SolutionId: manifest.solution.primarySolutionId,
          SolutionVersion: manifest.solution.solutionVersion,
        },
      },
    });

    //=============================================================================================
    // Resources
    //=============================================================================================
    /**
     * @description S3 bucket for access logs
     * @type {Bucket}
     */
    const accessLogsBucket: Bucket = new Bucket(this, "AccessLogsBucket", {
      encryption: BucketEncryption.S3_MANAGED,
      versioned: true,
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
      accessControl: BucketAccessControl.LOG_DELIVERY_WRITE,
      lifecycleRules: [
        {
          transitions: [
            {
              storageClass: StorageClass.INFREQUENT_ACCESS,
              transitionAfter: Duration.days(30),
            },
            {
              storageClass: StorageClass.GLACIER,
              transitionAfter: Duration.days(90),
            },
          ],
          expiration: Duration.days(365 * 2), // expire after 2 years
        },
      ],
    });

    /**
     * @description bucket to collect compliance reports
     * @type {Bucket}
     */
    const reportBucket: Bucket = new Bucket(this, "ComplianceReportBucket", {
      versioned: true,
      encryption: BucketEncryption.S3_MANAGED,
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
      serverAccessLogsBucket: accessLogsBucket,
    });

    /**
     * @description SNS topic for triggering compliance generator microservice
     * @type {Topic}
     */
    const fmsTopic: Topic = new Topic(this, "Topic", {
      displayName: "FMS compliance report generator subscription topic",
      topicName: "FMS_Compliance_Generator_Topic",
      masterKey: Alias.fromAliasName(this, "SNSKey", "alias/aws/sns"),
    });

    /**
     * @description SNS topic policy to enforce only encrypted connections over HTTPS,
     * adding aws:SecureTransport in conditions
     * @type {TopicPolicy}
     */
    new TopicPolicy(this, "TopicPolicy", {
      topics: [fmsTopic],
      policyDocument: new PolicyDocument({
        statements: [
          new PolicyStatement({
            sid: "AllowPublishThroughSSLOnly",
            actions: ["sns:Publish"],
            effect: Effect.DENY,
            resources: [fmsTopic.topicArn],
            conditions: {
              ["Bool"]: {
                "aws:SecureTransport": "false",
              },
            },

            principals: [new AnyPrincipal()],
          }),
        ],
      }),
    });

    /**
     * @description dead letter queue for lambda
     * @type {Queue}
     */
    const dlq: Queue = new Queue(this, `DLQ`, {
      encryption: QueueEncryption.KMS_MANAGED,
    });

    /**
     * @description SQS queue policy to enforce only encrypted connections over HTTPS,
     * adding aws:SecureTransport in conditions
     * @type {QueuePolicy}
     */
    const queuePolicy: QueuePolicy = new QueuePolicy(this, "QueuePolicy", {
      queues: [dlq],
    });
    queuePolicy.document.addStatements(
      new PolicyStatement({
        sid: "AllowPublishThroughSSLOnly",
        actions: ["sqs:*"],
        effect: Effect.DENY,
        resources: [],
        conditions: {
          ["Bool"]: {
            "aws:SecureTransport": "false",
          },
        },
        principals: [new AnyPrincipal()],
      })
    );

    /**
     * @description lambda function to generate compliance reports
     * @type {Function}
     */
    const complianceGenerator: Function = new Function(
      this,
      "ComplianceGenerator",
      {
        description: `${manifest.solution.primarySolutionId} - Function to generate compliance reports for FMS policies`,
        runtime: Runtime.NODEJS_14_X,
        deadLetterQueue: dlq,
        code: Code.fromAsset(
          `${path.dirname(
            __dirname
          )}/../services/complianceGenerator/dist/complianceGenerator.zip`
        ),
        handler: "index.handler",
        memorySize: 256,
        reservedConcurrentExecutions: 200,
        environment: {
          FMS_REPORT_BUCKET: reportBucket.bucketName, // bucket to upload compliance reports
          EXCLUDED_POLICIES: "NOP", // policies to exclude from compliance reporting
          FMS_TOPIC_ARN: fmsTopic.topicArn, // sns topic arn
          FMS_TOPIC_REGION: this.region, // deployment region
          SEND_METRIC: map.findInMap("Metric", "SendAnonymousMetric"),
          LOG_LEVEL: LOG_LEVEL.INFO, //change as needed
          SOLUTION_ID: map.findInMap("Solution", "SolutionId"),
          SOLUTION_VERSION: map.findInMap("Solution", "SolutionVersion"),
          MAX_ATTEMPTS: "" + 10, // retry attempts for SDKs, increase if you see throttling errors
          UUID: uuid.valueAsString,
          METRICS_QUEUE: `https://sqs.${this.region}.amazonaws.com/${this.account}/${metricsQueue.valueAsString}`,
          CUSTOM_SDK_USER_AGENT: `AwsSolution/${map.findInMap(
            "Solution",
            "SolutionId"
          )}/${map.findInMap("Solution", "SolutionVersion")}`,
        },
        timeout: Duration.seconds(300),
      }
    );
    complianceGenerator.addEventSource(new SnsEventSource(fmsTopic));

    /**
     * @description Events Rule for compliance generator
     * @type {Rule}
     */
    new Rule(this, "ComplianceGeneratorRule", {
      ruleName: "FMS-Compliance-Generator",
      schedule: Schedule.rate(Duration.days(1)),
      targets: [new LambdaFunction(complianceGenerator)],
    });

    /**
     * @description iam permissions for the compliance generator lambda function
     * @type {Policy}
     */
    const cgPolicy: Policy = new Policy(this, "ComplianceGeneratorPolicy", {
      policyName: "FMS-ComplianceGeneratorPolicy",
      roles: [complianceGenerator.role!],
    });

    /**
     * @description iam policy statement with FMS actions for compliance generator lambda
     * @type {PolicyStatement}
     */
    const cgPS0: PolicyStatement = new PolicyStatement({
      effect: Effect.ALLOW,
      sid: "FMSRead",
      actions: [
        "fms:ListComplianceStatus",
        "fms:GetComplianceDetail",
        "fms:ListPolicies",
      ],
      resources: ["*"], // fms read actions, to be performed on multiple policies
    });
    cgPolicy.addStatements(cgPS0);

    /**
     * @description iam policy statement with EC2 actions for compliance generator lambda
     * @type {PolicyStatement}
     */
    const cgPS1: PolicyStatement = new PolicyStatement({
      effect: Effect.ALLOW,
      sid: "EC2Read",
      actions: ["ec2:DescribeRegions"],
      resources: ["*"], // resource level permission not valid for this iam action
    });
    cgPolicy.addStatements(cgPS1);

    /**
     * @description iam policy statement with SNS actions for compliance generator lambda
     * @type {PolicyStatement}
     */
    const cgPS2: PolicyStatement = new PolicyStatement({
      effect: Effect.ALLOW,
      sid: "SNSWrite",
      actions: ["sns:Publish"],
      resources: [fmsTopic.topicArn],
    });
    cgPolicy.addStatements(cgPS2);

    /**
     * @description iam policy statement with S3 actions for compliance generator lambda
     * @type {PolicyStatement}
     */
    const cgPS3: PolicyStatement = new PolicyStatement({
      effect: Effect.ALLOW,
      sid: "S3Write",
      actions: ["s3:PutObject"],
      resources: [reportBucket.bucketArn, `${reportBucket.bucketArn}/*`],
    });
    cgPolicy.addStatements(cgPS3);

    /**
     * @description iam policy statement with SQS actions for compliance generator lambda
     * @type {PolicyStatement}
     */
    const cgPS4: PolicyStatement = new PolicyStatement({
      effect: Effect.ALLOW,
      sid: "SQSWrite",
      actions: ["sqs:SendMessage"],
      resources: [
        `arn:aws:sqs:${this.region}:${this.account}:${metricsQueue.valueAsString}`,
      ],
    });
    cgPolicy.addStatements(cgPS4);

    //=============================================================================================
    // cfn_nag suppress rules
    //=============================================================================================
    const cfn_nag_w89 = [
      {
        id: "W89",
        reason:
          "Not a valid use case for Lambda functions to be deployed inside a VPC",
      },
    ];

    const cg = complianceGenerator.node.findChild("Resource") as CfnFunction;
    cg.cfnOptions.metadata = {
      cfn_nag: {
        rules_to_suppress: [
          {
            id: "W58",
            reason:
              "CloudWatch logs write permissions added with managed role AWSLambdaBasicExecutionRole",
          },
          ...cfn_nag_w89,
        ],
      },
    };

    const ab = accessLogsBucket.node.defaultChild as CfnResource;
    ab.cfnOptions.metadata = {
      cfn_nag: {
        rules_to_suppress: [
          {
            id: "W35",
            reason: "access logging disabled, its a logging bucket",
          },
          {
            id: "W51",
            reason: "permission given for log delivery",
          },
        ],
      },
    };

    (reportBucket.node.defaultChild as CfnResource).cfnOptions.metadata = {
      cfn_nag: {
        rules_to_suppress: [
          {
            id: "W51",
            reason: "permission given to lambda to put compliance reports",
          },
        ],
      },
    };

    (cgPolicy.node.findChild("Resource") as CfnPolicy).cfnOptions.metadata = {
      cfn_nag: {
        rules_to_suppress: [
          {
            id: "W12",
            reason:
              "Resource * is required for IAM Read actions (fms:ListComplianceStatus,fms:GetComplianceDetail,fms:ListPolicies) to be performed on multiple FMS policies in different regions",
          },
        ],
      },
    };

    //=============================================================================================
    // Output
    //=============================================================================================
    new CfnOutput(this, "Report Bucket", {
      description: "Bucket with compliance reports",
      value: `s3://${reportBucket.bucketName}`,
    });
  }