Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(HIPAA Security): CloudWatch checks #339

Merged
merged 2 commits into from
Sep 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions RULES.md

Large diffs are not rendered by default.

46 changes: 38 additions & 8 deletions src/HIPAA-Security/hipaa-security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import {
hipaaSecurityCloudTrailEncryptionEnabled,
hipaaSecurityCloudTrailLogFileValidationEnabled,
} from './rules/cloudtrail';

import {
hipaaSecurityCloudWatchAlarmAction,
hipaaSecurityCloudWatchLogGroupEncrypted,
} from './rules/cloudwatch';
import {
hipaaSecurityEC2InstanceDetailedMonitoringEnabled,
hipaaSecurityEC2InstancesInVPC,
Expand All @@ -39,7 +42,7 @@ export class HIPAASecurityChecks extends NagPack {
this.checkAPIGW(node, ignores);
this.checkAutoScaling(node, ignores);
this.checkCloudTrail(node, ignores);
// this.checkCloudWatch(node, ignores);
this.checkCloudWatch(node, ignores);
// this.checkCodeBuild(node, ignores);
// this.checkDMS(node, ignores);
// this.checkDynamoDB(node, ignores);
Expand Down Expand Up @@ -222,12 +225,39 @@ export class HIPAASecurityChecks extends NagPack {
}
}

// /**
// * Check CloudWatch Resources
// * @param node the IConstruct to evaluate
// * @param ignores list of ignores for the resource
// */
// private checkCloudWatch(node: CfnResource, ignores: any): void {}
/**
* Check CloudWatch Resources
* @param node the IConstruct to evaluate
* @param ignores list of ignores for the resource
*/
private checkCloudWatch(node: CfnResource, ignores: any): void {
if (
!this.ignoreRule(ignores, 'HIPAA.Security-CloudWatchAlarmAction') &&
!hipaaSecurityCloudWatchAlarmAction(node)
) {
const ruleId = 'HIPAA.Security-CloudWatchAlarmAction';
const info =
'The CloudWatch alarm does not have at least one alarm action, one INSUFFICIENT_DATA action, or one OK action enabled - (Control ID: 164.312(b)).';
const explanation =
'Amazon CloudWatch alarms alert when a metric breaches the threshold for a specified number of evaluation periods. The alarm performs one or more actions based on the value of the metric or expression relative to a threshold over a number of time periods.';
Annotations.of(node).addError(
this.createMessage(ruleId, info, explanation)
);
}
if (
!this.ignoreRule(ignores, 'HIPAA.Security-CloudWatchLogGroupEncrypted') &&
!hipaaSecurityCloudWatchLogGroupEncrypted(node)
) {
const ruleId = 'HIPAA.Security-CloudWatchLogGroupEncrypted';
const info =
'The CloudWatch Log Group is not encrypted with an AWS KMS key - (Control IDs: 164.312(a)(2)(iv), 164.312(e)(2)(ii)).';
const explanation =
'To help protect sensitive data at rest, ensure encryption is enabled for your Amazon CloudWatch Log Groups.';
Annotations.of(node).addError(
this.createMessage(ruleId, info, explanation)
);
}
}

// /**
// * Check CodeBuild Resources
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

import { CfnAlarm } from '@aws-cdk/aws-cloudwatch';
import { IConstruct, Stack } from '@aws-cdk/core';

/**
* CloudWatch alarms have at least one alarm action, one INSUFFICIENT_DATA action, or one OK action enabled - (Control ID: 164.312(b))
* @param node the CfnResource to check
*/
export default function (node: IConstruct): boolean {
if (node instanceof CfnAlarm) {
const actionsEnabled = Stack.of(node).resolve(node.actionsEnabled);
if (actionsEnabled === false) {
return false;
}
// Actions can be an array with a token that then resolves to an empty array or undefined
const alarmActions = Stack.of(node).resolve(node.alarmActions);
const insufficientDataActions = Stack.of(node).resolve(
node.insufficientDataActions
);
const okActions = Stack.of(node).resolve(node.okActions);
const totalAlarmActions = alarmActions ? alarmActions.length : 0;
const totalInsufficientDataActions = insufficientDataActions
? insufficientDataActions.length
: 0;
const totalOkActions = okActions ? okActions.length : 0;
const totalActions =
totalAlarmActions + totalInsufficientDataActions + totalOkActions;
if (totalActions == 0) {
return false;
}
}
return true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

import { CfnLogGroup } from '@aws-cdk/aws-logs';
import { IConstruct } from '@aws-cdk/core';

/**
* CloudWatch Log Groups are encrypted with customer managed keys - (Control IDs: 164.312(a)(2)(iv), 164.312(e)(2)(ii))
* @param node the CfnResource to check
*/
export default function (node: IConstruct): boolean {
if (node instanceof CfnLogGroup) {
if (node.kmsKeyId == undefined) {
return false;
}
}
return true;
}
2 changes: 2 additions & 0 deletions src/HIPAA-Security/rules/cloudwatch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
export { default as hipaaSecurityCloudWatchAlarmAction } from './hipaaSecurityCloudWatchAlarmAction';
export { default as hipaaSecurityCloudWatchLogGroupEncrypted } from './hipaaSecurityCloudWatchLogGroupEncrypted';
111 changes: 111 additions & 0 deletions test/HIPAA-Security/HIPAA-Security-CloudWatch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
import { SynthUtils } from '@aws-cdk/assert';
import { Alarm, Metric } from '@aws-cdk/aws-cloudwatch';
import { Ec2Action, Ec2InstanceAction } from '@aws-cdk/aws-cloudwatch-actions';
import { Key } from '@aws-cdk/aws-kms';
import { LogGroup } from '@aws-cdk/aws-logs';
import { Aspects, Stack } from '@aws-cdk/core';
import { HIPAASecurityChecks } from '../../src';

describe('Amazon CloudWatch', () => {
test('HIPAA.Security-CloudWatchAlarmAction: CloudWatch alarms have at least one alarm action, one INSUFFICIENT_DATA action, or one OK action enabled', () => {
const nonCompliant = new Stack();
Aspects.of(nonCompliant).add(new HIPAASecurityChecks());
new Alarm(nonCompliant, 'rAlarm', {
metric: new Metric({
namespace: 'MyNamespace',
metricName: 'MyMetric',
}),
threshold: 100,
evaluationPeriods: 2,
}).addAlarmAction();
const messages = SynthUtils.synthesize(nonCompliant).messages;
expect(messages).toContainEqual(
expect.objectContaining({
entry: expect.objectContaining({
data: expect.stringContaining(
'HIPAA.Security-CloudWatchAlarmAction:'
),
}),
})
);

const nonCompliant2 = new Stack();
Aspects.of(nonCompliant2).add(new HIPAASecurityChecks());
new Alarm(nonCompliant2, 'rAlarm', {
metric: new Metric({
namespace: 'MyNamespace',
metricName: 'MyMetric',
}),
threshold: 100,
evaluationPeriods: 2,
actionsEnabled: false,
}).addOkAction(new Ec2Action(Ec2InstanceAction.REBOOT));
const messages2 = SynthUtils.synthesize(nonCompliant2).messages;
expect(messages2).toContainEqual(
expect.objectContaining({
entry: expect.objectContaining({
data: expect.stringContaining(
'HIPAA.Security-CloudWatchAlarmAction:'
),
}),
})
);

const activeCompliant = new Stack();
Aspects.of(activeCompliant).add(new HIPAASecurityChecks());
new Alarm(activeCompliant, 'rAlarm', {
metric: new Metric({
namespace: 'MyNamespace',
metricName: 'MyMetric',
}),
threshold: 100,
evaluationPeriods: 2,
}).addOkAction(new Ec2Action(Ec2InstanceAction.REBOOT));
const messages3 = SynthUtils.synthesize(activeCompliant).messages;
expect(messages3).not.toContainEqual(
expect.objectContaining({
entry: expect.objectContaining({
data: expect.stringContaining(
'HIPAA.Security-CloudWatchAlarmAction:'
),
}),
})
);
});

test('HIPAA.Security-CloudWatchLogGroupEncrypted: CloudWatch Log Groups are encrypted with customer managed keys', () => {
const nonCompliant = new Stack();
Aspects.of(nonCompliant).add(new HIPAASecurityChecks());
new LogGroup(nonCompliant, 'rLogGroup');
const messages = SynthUtils.synthesize(nonCompliant).messages;
expect(messages).toContainEqual(
expect.objectContaining({
entry: expect.objectContaining({
data: expect.stringContaining(
'HIPAA.Security-CloudWatchLogGroupEncrypted:'
),
}),
})
);

const activeCompliant = new Stack();
Aspects.of(activeCompliant).add(new HIPAASecurityChecks());
new LogGroup(activeCompliant, 'rLogGroup', {
encryptionKey: new Key(activeCompliant, 'rLogsKey'),
});
const messages2 = SynthUtils.synthesize(activeCompliant).messages;
expect(messages2).not.toContainEqual(
expect.objectContaining({
entry: expect.objectContaining({
data: expect.stringContaining(
'HIPAA.Security-CloudWatchLogGroupEncrypted:'
),
}),
})
);
});
});