in source/resources/lib/policy.ts [73:485]
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 table = new CfnParameter(this, "PolicyTable", {
description: "DynamoDB table for policy metadata",
type: "String",
});
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",
});
const policyIdentifier = new CfnParameter(this, "PolicyIdentifier", {
description: "A unique string identifier for the policies",
type: "String",
});
//=============================================================================================
// Metadata
//=============================================================================================
this.templateOptions.metadata = {
"AWS::CloudFormation::Interface": {
ParameterGroups: [
{
Label: { default: "Policy Configuration" },
Parameters: [policyIdentifier.logicalId],
},
{
Label: { default: "Shared Resource Configurations" },
Parameters: [
table.logicalId,
uuid.logicalId,
metricsQueue.logicalId,
],
},
],
ParameterLabels: {
[table.logicalId]: {
default: "Policy Table",
},
[metricsQueue.logicalId]: {
default: "Metric Queue",
},
[uuid.logicalId]: {
default: "UUID",
},
[policyIdentifier.logicalId]: {
default: "Policy Identifier",
},
},
},
};
this.templateOptions.description = `(${manifest.solution.primarySolutionId}-po) - The AWS CloudFormation template for deployment of the ${manifest.solution.name}. 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 - ssm parameter for org units
* @type {StringListParameter}
*/
const ou: StringListParameter = new StringListParameter(this, "FMSOUs", {
description: "FMS parameter store for OUs",
stringListValue: ["NOP"],
parameterName: `/FMS/${policyIdentifier.valueAsString}/OUs`,
simpleName: false,
});
/**
* @description ssm parameter for tags
* @type {StringParameter}
*/
const tags: StringParameter = new StringParameter(this, "FMSTags", {
description: "fms parameter for fms tags",
parameterName: `/FMS/${policyIdentifier.valueAsString}/Tags`,
stringValue: "NOP",
simpleName: false,
});
/**
* @description ssm parameter for regions
* @type {StringListParameter}
*/
const regions: StringListParameter = new StringListParameter(
this,
"FMSRegions",
{
description: "fms parameter for fms regions",
parameterName: `/FMS/${policyIdentifier.valueAsString}/Regions`,
stringListValue: ["NOP"],
simpleName: false,
}
);
/**
* @description S3 bucket for access logs
* @type {Bucket}
*/
const accessLogsBucket: Bucket = new Bucket(this, "AccessLogsBucket", {
versioned: true,
encryption: BucketEncryption.S3_MANAGED,
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 S3 bucket with default policy manifest
* @type {Bucket}
*/
const policyBucket: Bucket = new Bucket(this, "ManifestBucket", {
versioned: true,
encryption: BucketEncryption.S3_MANAGED,
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
serverAccessLogsBucket: accessLogsBucket,
});
/**
* @description following snippet can be used to source policy manifest from local file
* @link https://docs.aws.amazon.com/cdk/api/latest/docs/aws-s3-deployment-readme.html
* @example
```
new BucketDeployment(this, "CopyManifest", {
sources: [
Source.asset(`${path.dirname(__dirname)}/lib`, {
exclude: ["**", "!policy_manifest.json"],
}),
],
destinationBucket: policyBucket,
prune: true,
});
```
*/
new AwsCustomResource(this, "CopyManifest", {
onCreate: {
service: "S3",
action: "copyObject",
parameters: {
Bucket: policyBucket.bucketName,
CopySource: `${manifest.solution.policyBucket}/${manifest.solution.name}/${manifest.solution.solutionVersion}/policy_manifest.json`,
Key: "policy_manifest.json",
},
physicalResourceId: PhysicalResourceId.of(Date.now().toString()),
},
policy: AwsCustomResourcePolicy.fromStatements([
new PolicyStatement({
effect: Effect.ALLOW,
sid: "S3Get",
actions: ["s3:GetObject"],
resources: [`arn:aws:s3:::${manifest.solution.policyBucket}/*`],
}),
new PolicyStatement({
effect: Effect.ALLOW,
sid: "S3Put",
actions: ["s3:PutObject"],
resources: [`${policyBucket.bucketArn}/*`],
}),
]),
});
/**
* @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 create FMS security policy
* @type {Function}
*/
const policyManager: Function = new Function(this, "PolicyManager", {
description: `${map.findInMap(
"Solution",
"SolutionId"
)} - Function to create/update/delete FMS security policies`,
runtime: Runtime.NODEJS_14_X,
code: Code.fromAsset(
`${path.dirname(
__dirname
)}/../services/policyManager/dist/policyManager.zip`
),
handler: "index.handler",
deadLetterQueue: dlq,
retryAttempts: 0,
maxEventAge: Duration.minutes(15),
deadLetterQueueEnabled: true,
memorySize: 512,
environment: {
FMS_OU: ou.parameterName,
FMS_TAG: tags.parameterName,
FMS_REGION: regions.parameterName,
FMS_TABLE: table.valueAsString,
POLICY_MANIFEST: `${policyBucket.bucketName}|policy_manifest.json`, // manifest file to be used for policy configuration
POLICY_IDENTIFIER: policyIdentifier.valueAsString,
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.minutes(15),
});
/**
* @description Events rule to Lambda construct pattern
* @example `
* {
"source": [
"aws.ssm"
],
"detail-type": [
"Parameter Store Change"
],
"resources": [
"arn:aws:ssm:<region>:<account-id>:parameter<parameter-name>",
"arn:aws:ssm:<region>:<account-id>:parameter<parameter-name-2>",
]
}
*/
new EventsRuleToLambda(this, "EventsRuleLambda", {
existingLambdaObj: policyManager,
eventRuleProps: {
ruleName: `FMSPolicyRule-${policyIdentifier.valueAsString}`,
eventPattern: {
source: ["aws.ssm"],
detailType: ["Parameter Store Change"],
resources: [
`${ou.parameterArn}`,
`${tags.parameterArn}`,
`${regions.parameterArn}`,
],
},
},
});
/**
* @description log group for policy manager lambda function
* @type {LogGroup}
*/
const lg: LogGroup = new LogGroup(this, "PolicyMangerLogGroup", {
logGroupName: `/aws/lambda/${policyManager.functionName}`,
removalPolicy: RemovalPolicy.DESTROY,
retention: RetentionDays.ONE_WEEK,
});
/**
* @description iam permissions for the policy manager lambda function
* @type {IAMConstruct}
*/
new IAMConstruct(this, "LambdaIAM", {
policyTable: table.valueAsString,
sqs: dlq.queueArn,
logGroup: lg.logGroupArn,
role: policyManager.role!,
accountId: this.account,
region: this.region,
metricsQueue: metricsQueue.valueAsString,
regionParamArn: regions.parameterArn,
ouParamArn: ou.parameterArn,
tagParamArn: tags.parameterArn,
s3Bucket: policyBucket,
});
//=============================================================================================
// cfn_nag suppress rules
//=============================================================================================
const cfn_nag_w58_w89_w92 = [
{
id: "W58",
reason:
"CloudWatch logs write permissions added with managed role AWSLambdaBasicExecutionRole",
},
{
id: "W89",
reason:
"Not a valid use case for Lambda functions to be deployed inside a VPC",
},
{
id: "W92",
reason: "Lambda ReservedConcurrentExecutions not needed",
},
];
(lg.node.findChild("Resource") as CfnResource).cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: "W84",
reason:
"Using service default encryption https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/data-protection.html",
},
],
},
};
(
policyManager.node.findChild("Resource") as CfnFunction
).cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [...cfn_nag_w58_w89_w92],
},
};
(accessLogsBucket.node.defaultChild as CfnResource).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",
},
],
},
};
(policyBucket.node.defaultChild as CfnResource).cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [
{
id: "W51",
reason: "permission given to lambda to get policy manifest",
},
],
},
};
//=============================================================================================
// Output
//=============================================================================================
new CfnOutput(this, "Policy Manifest Bucket", {
description: "S3 Bucket with policy manifest file",
value: `s3://${policyBucket.bucketName}`,
});
}