Skip to content

Commit 77bf0c5

Browse files
authored
Merge pull request #562 from 0xdabbad00/auditor
auditor added
2 parents 15e7246 + d291a4e commit 77bf0c5

21 files changed

+3071
-0
lines changed

.dockerignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.git
2+
account-data
3+
docs

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.vscode
12
config.json
23
config/
34
.DS_Store
@@ -15,3 +16,9 @@ data/
1516
private_commands/
1617
output/
1718
.vscode/
19+
Pipfile.lock
20+
auditor/node_modules
21+
auditor/.cdk.staging
22+
auditor/cdk.out
23+
auditor/.env
24+
auditor/resources/cloudmapper

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ python cloudmapper.py webserver --public
169169

170170
You should then be able to view the report by visiting http://127.0.0.1:8000/account-data/report.html
171171

172+
# Running CloudMapper regularly to audit your environment
173+
A CDK app for deploying CloudMapper via Fargate so that it runs nightly, sends audit findings as alerts to a Slack channel, and generating a report that is saved on S3, is described [here](auditor/README.md).
174+
172175

173176
# Alternatives
174177
For network diagrams, you may want to try https://github.com/lyft/cartography or https://github.com/anaynayak/aws-security-viz

auditor/.npmignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# CDK asset staging directory
2+
.cdk.staging
3+
cdk.out

auditor/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
The purpose of this project is to run CloudMapper's collection and audit capabilities nightly, across multiple accounts, sending any audit findings to a Slack channel and keeping a copy of the collected metadata in an S3 bucket.
2+
3+
<img src="https://raw.githubusercontent.com/duo-labs/cloudmapper/master/docs/images/nightly_scanner_diagram.png" width=100% alt="Diagram">
4+
5+
6+
# Setup
7+
- Clone the required projects and install the necessary modules for CDK deployment:
8+
```
9+
git clone https://github.com/duo-labs/cloudmapper.git
10+
cd cloudmapper/auditor
11+
# Clone CloudMapper again into the auditor (weird, I know, but the only way to keep this all one repo)
12+
git clone https://github.com/duo-labs/cloudmapper.git resources/cloudmapper
13+
npm install
14+
```
15+
16+
- Create an S3 bucket in your account, we'll call `MYCOMPANY-cloudmapper`
17+
- Get a webhook to write to your slack channel and create the Secrets Manager secret `cloudmapper-slack-webhook` with it as follows:
18+
```
19+
aws secretsmanager create-secret --name cloudmapper-slack-webhook --secret-string '{"webhook":"https://hooks.slack.com/services/XXX/YYY/ZZZ"}'
20+
```
21+
- Create an SNS for alarms to go to if errors are encountered.
22+
- Create roles in your other accounts with `SecurityAudit` and `ViewOnlyAccess` privileges and IAM trust policies that allow this account to assume them.
23+
- Edit the files in `s3_bucket_files` and copy them to your S3 bucket.
24+
- `config`: This is the containers `~/.aws/config` that will be used to assume roles in other accounts. These must be named CloudMapper. Note that the `credential_source` is set to `EcsContainer`.
25+
- `config.json`: CloudMapper config file for specifying the accounts.
26+
- `audit_config_override.yaml`: CloudMapper config file for muting audit findings.
27+
- `run_cloudmapper.sh`: script for executing CloudMapper and should be unchanged.
28+
- `cdk_app.yaml`: config for the CDK, only used during deploy.
29+
- Deploy this CDK app:
30+
```
31+
cdk deploy
32+
```
33+
34+
# Daily use
35+
Before setting this up to run against an account, you should manually run CloudMapper's audit or report command on the account to determine which findings should be fixed in the account, or muted. This is done to avoid having your Slack channel flooded with 100 findings. If you are not fixing or muting issues, the value of this tool will quickly deteriorate. It does not keep track of issues it previously alerted you about, so it will repeatedly alert on the same problems if action is not taken. The expectation is you should be receiving a handful or less of alerts each day (ideally zero). If that is not the case, this tool is not being used as intended and you will not get value out of it.
36+
37+
To mute issues, you should modify `audit_config_override.yaml` in the S3 bucket. To test your changes, you can download the `account-data` from the S3 bucket and run CloudMapper's `audit` command to ensure the filtering works as intended.
38+
39+
To add new accounts, you should first manually run CloudMapper's audit and fix/mute issues as needed. After that, add the account to the `config.json` and `config` files in the S3 bucket, along with setting up the necessary trust relationship.
40+
41+
## Kicking off a manual scan
42+
To kick off a manual scan, without needing to wait until the scheduled time, run:
43+
```
44+
aws events put-events --entries '[{"Source":"cloudmapper","DetailType":"start","Detail":"{}"}]'
45+
```

auditor/bin/cloudmapperauditor.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env node
2+
3+
// @ts-ignore: Cannot find declaration file
4+
require('source-map-support/register');
5+
const cdk = require('@aws-cdk/core');
6+
const { CloudmapperauditorStack } = require('../lib/cloudmapperauditor-stack');
7+
8+
const app = new cdk.App();
9+
new CloudmapperauditorStack(app, 'CloudmapperauditorStack');

auditor/cdk.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"app": "node bin/cloudmapperauditor.js"
3+
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/**
2+
* Deploys the CloudMapper audit app.
3+
* Usage: cdk deploy -c s3_bucket=MYCOMPANY-cloudmapper -c sns_topic=email
4+
*/
5+
6+
const cdk = require('@aws-cdk/core');
7+
const ecs = require('@aws-cdk/aws-ecs');
8+
const ecsPatterns = require('@aws-cdk/aws-ecs-patterns');
9+
const ec2 = require('@aws-cdk/aws-ec2');
10+
const logs = require('@aws-cdk/aws-logs');
11+
const iam = require('@aws-cdk/aws-iam');
12+
const events = require('@aws-cdk/aws-events');
13+
const targets = require('@aws-cdk/aws-events-targets');
14+
const cloudwatch = require('@aws-cdk/aws-cloudwatch');
15+
const cloudwatch_actions = require('@aws-cdk/aws-cloudwatch-actions');
16+
const sns = require('@aws-cdk/aws-sns');
17+
const sns_subscription = require('@aws-cdk/aws-sns-subscriptions');
18+
const lambda = require('@aws-cdk/aws-lambda');
19+
20+
// Import libraries to read a config file
21+
const yaml = require('js-yaml');
22+
const fs = require('fs');
23+
24+
class CloudmapperauditorStack extends cdk.Stack {
25+
/**
26+
*
27+
* @param {cdk.Construct} scope
28+
* @param {string} id
29+
* @param {cdk.StackProps=} props
30+
*/
31+
constructor(scope, id, props) {
32+
super(scope, id, props);
33+
34+
// Load config file
35+
var config = yaml.safeLoad(fs.readFileSync('./s3_bucket_files/cdk_app.yaml', 'utf8'));
36+
37+
if (config['s3_bucket'] == 'MYCOMPANY-cloudmapper') {
38+
console.log("You must configure the CDK app by editing ./s3_bucket_files/cdk_app.yaml");
39+
process.exit(1);
40+
}
41+
42+
// Create VPC to run everything in, but without a NAT gateway.
43+
// We want to run in a public subnet, but the CDK creates a private subnet
44+
// by default, which results in the use of a NAT gateway, which costs $30/mo.
45+
// To avoid that unnecessary charge, we have to create the VPC in a complicated
46+
// way.
47+
// This trick was figured out by jeshan in https://github.com/aws/aws-cdk/issues/1305#issuecomment-525474540
48+
// Normally, the CDK does not allow this because the private subnets have to have
49+
// a route out, and you can't get rid of the private subnets.
50+
// So the trick is to remove the routes out.
51+
// The private subnets remain, but are not usable and have no costs.
52+
const vpc = new ec2.Vpc(this, 'CloudMapperVpc', {
53+
maxAzs: 2,
54+
natGateways: 0
55+
});
56+
57+
// Create a condition that will always fail.
58+
// We will use this in a moment to remove the routes.
59+
var exclude_condition = new cdk.CfnCondition(this,
60+
'exclude-default-route-subnet',
61+
{
62+
// Checks if true == false, so this always fails
63+
expression: cdk.Fn.conditionEquals(true, false)
64+
}
65+
);
66+
67+
// For the private subnets, add a CloudFormation condition to the routes
68+
// to cause them to not be created.
69+
for (var subnet of vpc.privateSubnets) {
70+
for (var child of subnet.node.children) {
71+
if (child.constructor.name==="CfnRoute") {
72+
child.cfnOptions.condition = exclude_condition
73+
}
74+
}
75+
}
76+
77+
// Define the ECS task
78+
const cluster = new ecs.Cluster(this, 'Cluster', { vpc });
79+
80+
const taskDefinition = new ecs.FargateTaskDefinition(this, 'taskDefinition', {});
81+
82+
taskDefinition.addContainer('cloudmapper-container', {
83+
image: ecs.ContainerImage.fromAsset('./resources'),
84+
memoryLimitMiB: 512,
85+
cpu: 256,
86+
environment: {
87+
S3_BUCKET: config['s3_bucket']
88+
},
89+
logging: new ecs.AwsLogDriver({
90+
streamPrefix: 'cloudmapper',
91+
logRetention: logs.RetentionDays.TWO_WEEKS
92+
})
93+
});
94+
95+
// Grant the ability to assume the IAM role in any account
96+
taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({
97+
resources: ["arn:aws:iam::*:role/"+config['iam_role']],
98+
actions: ['sts:AssumeRole']
99+
}));
100+
101+
// Grant the ability to read and write the files from the S3 bucket
102+
taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({
103+
resources: ["arn:aws:s3:::"+config['s3_bucket']],
104+
actions: ['s3:ListBucket']
105+
}));
106+
taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({
107+
resources: ["arn:aws:s3:::"+config['s3_bucket']+"/*"],
108+
actions: ['s3:GetObject','s3:PutObject', 's3:DeleteObject']
109+
}));
110+
111+
// Grant the ability to record the stdout to CloudWatch Logs
112+
taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({
113+
resources: ["*"],
114+
actions: ['logs:*']
115+
}));
116+
117+
// Grant the ability to record error and success metrics
118+
taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({
119+
// This IAM privilege has no paths or conditions
120+
resources: ["*"],
121+
actions: ['cloudwatch:PutMetricData']
122+
}));
123+
124+
// Grant the ability to read from Secrets Manager
125+
taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({
126+
// This IAM privilege has no paths or conditions
127+
resources: ["*"],
128+
actions: ['secretsmanager:GetSecretValue'],
129+
conditions: {'ForAnyValue:StringLike':{'secretsmanager:SecretId': '*cloudmapper-slack-webhook*'}}
130+
}));
131+
132+
// Create rule to trigger this be run every 24 hours
133+
new events.Rule(this, "scheduled_run", {
134+
ruleName: "cloudmapper_scheduler",
135+
// Run at 2am EST (6am UTC) every night
136+
schedule: events.Schedule.expression("cron(0 6 * * ? *)"),
137+
description: "Starts the CloudMapper auditing task every night",
138+
targets: [new targets.EcsTask({
139+
cluster: cluster,
140+
taskDefinition: taskDefinition,
141+
subnetSelection: {subnetType: ec2.SubnetType.PUBLIC}
142+
})]
143+
});
144+
145+
// Create rule to trigger this manually
146+
new events.Rule(this, "manual_run", {
147+
ruleName: "cloudmapper_manual_run",
148+
eventPattern: {source: ['cloudmapper']},
149+
description: "Allows CloudMapper auditing to be manually started",
150+
targets: [new targets.EcsTask({
151+
cluster: cluster,
152+
taskDefinition: taskDefinition,
153+
subnetSelection: {subnetType: ec2.SubnetType.PUBLIC}
154+
})]
155+
});
156+
157+
// Create alarm for any errors
158+
const error_alarm = new cloudwatch.Alarm(this, "error_alarm", {
159+
metric: new cloudwatch.Metric({
160+
namespace: 'cloudmapper',
161+
metricName: "errors",
162+
statistic: "Sum"
163+
}),
164+
threshold: 0,
165+
evaluationPeriods: 1,
166+
datapointsToAlarm: 1,
167+
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
168+
alarmDescription: "Detect errors",
169+
alarmName: "cloudmapper_errors"
170+
});
171+
172+
// Create SNS for alarms to be sent to
173+
const sns_topic = new sns.Topic(this, 'cloudmapper_alarm', {
174+
displayName: 'cloudmapper_alarm'
175+
});
176+
177+
// Connect the alarm to the SNS
178+
error_alarm.addAlarmAction(new cloudwatch_actions.SnsAction(sns_topic));
179+
180+
// Create Lambda to forward alarms
181+
const alarm_forwarder = new lambda.Function(this, "alarm_forwarder", {
182+
runtime: lambda.Runtime.PYTHON_3_7,
183+
code: lambda.Code.asset("resources/alarm_forwarder"),
184+
handler: "main.handler",
185+
description: "Forwards alarms from the local SNS to another",
186+
logRetention: logs.RetentionDays.TWO_WEEKS,
187+
timeout: cdk.Duration.seconds(30),
188+
memorySize: 128,
189+
environment: {
190+
"ALARM_SNS": config['alarm_sns_arn']
191+
},
192+
});
193+
194+
// Add priv to publish the events so the alarms can be forwarded
195+
alarm_forwarder.addToRolePolicy(new iam.PolicyStatement({
196+
resources: [config['alarm_sns_arn']],
197+
actions: ['sns:Publish']
198+
}));
199+
200+
// Connect the SNS to the Lambda
201+
sns_topic.addSubscription(new sns_subscription.LambdaSubscription(alarm_forwarder));
202+
}
203+
}
204+
205+
module.exports = { CloudmapperauditorStack }

0 commit comments

Comments
 (0)