Skip to content

Commit

Permalink
Merge pull request #562 from 0xdabbad00/auditor
Browse files Browse the repository at this point in the history
auditor added
  • Loading branch information
0xdabbad00 authored Sep 30, 2019
2 parents 15e7246 + d291a4e commit 77bf0c5
Show file tree
Hide file tree
Showing 21 changed files with 3,071 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.git
account-data
docs
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.vscode
config.json
config/
.DS_Store
Expand All @@ -15,3 +16,9 @@ data/
private_commands/
output/
.vscode/
Pipfile.lock
auditor/node_modules
auditor/.cdk.staging
auditor/cdk.out
auditor/.env
auditor/resources/cloudmapper
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ python cloudmapper.py webserver --public

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

# Running CloudMapper regularly to audit your environment
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).


# Alternatives
For network diagrams, you may want to try https://github.com/lyft/cartography or https://github.com/anaynayak/aws-security-viz
Expand Down
3 changes: 3 additions & 0 deletions auditor/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# CDK asset staging directory
.cdk.staging
cdk.out
45 changes: 45 additions & 0 deletions auditor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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.

<img src="https://raw.githubusercontent.com/duo-labs/cloudmapper/master/docs/images/nightly_scanner_diagram.png" width=100% alt="Diagram">


# Setup
- Clone the required projects and install the necessary modules for CDK deployment:
```
git clone https://github.com/duo-labs/cloudmapper.git
cd cloudmapper/auditor
# Clone CloudMapper again into the auditor (weird, I know, but the only way to keep this all one repo)
git clone https://github.com/duo-labs/cloudmapper.git resources/cloudmapper
npm install
```

- Create an S3 bucket in your account, we'll call `MYCOMPANY-cloudmapper`
- Get a webhook to write to your slack channel and create the Secrets Manager secret `cloudmapper-slack-webhook` with it as follows:
```
aws secretsmanager create-secret --name cloudmapper-slack-webhook --secret-string '{"webhook":"https://hooks.slack.com/services/XXX/YYY/ZZZ"}'
```
- Create an SNS for alarms to go to if errors are encountered.
- Create roles in your other accounts with `SecurityAudit` and `ViewOnlyAccess` privileges and IAM trust policies that allow this account to assume them.
- Edit the files in `s3_bucket_files` and copy them to your S3 bucket.
- `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`.
- `config.json`: CloudMapper config file for specifying the accounts.
- `audit_config_override.yaml`: CloudMapper config file for muting audit findings.
- `run_cloudmapper.sh`: script for executing CloudMapper and should be unchanged.
- `cdk_app.yaml`: config for the CDK, only used during deploy.
- Deploy this CDK app:
```
cdk deploy
```

# Daily use
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.

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.

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.

## Kicking off a manual scan
To kick off a manual scan, without needing to wait until the scheduled time, run:
```
aws events put-events --entries '[{"Source":"cloudmapper","DetailType":"start","Detail":"{}"}]'
```
9 changes: 9 additions & 0 deletions auditor/bin/cloudmapperauditor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env node

// @ts-ignore: Cannot find declaration file
require('source-map-support/register');
const cdk = require('@aws-cdk/core');
const { CloudmapperauditorStack } = require('../lib/cloudmapperauditor-stack');

const app = new cdk.App();
new CloudmapperauditorStack(app, 'CloudmapperauditorStack');
3 changes: 3 additions & 0 deletions auditor/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"app": "node bin/cloudmapperauditor.js"
}
205 changes: 205 additions & 0 deletions auditor/lib/cloudmapperauditor-stack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/**
* Deploys the CloudMapper audit app.
* Usage: cdk deploy -c s3_bucket=MYCOMPANY-cloudmapper -c sns_topic=email
*/

const cdk = require('@aws-cdk/core');
const ecs = require('@aws-cdk/aws-ecs');
const ecsPatterns = require('@aws-cdk/aws-ecs-patterns');
const ec2 = require('@aws-cdk/aws-ec2');
const logs = require('@aws-cdk/aws-logs');
const iam = require('@aws-cdk/aws-iam');
const events = require('@aws-cdk/aws-events');
const targets = require('@aws-cdk/aws-events-targets');
const cloudwatch = require('@aws-cdk/aws-cloudwatch');
const cloudwatch_actions = require('@aws-cdk/aws-cloudwatch-actions');
const sns = require('@aws-cdk/aws-sns');
const sns_subscription = require('@aws-cdk/aws-sns-subscriptions');
const lambda = require('@aws-cdk/aws-lambda');

// Import libraries to read a config file
const yaml = require('js-yaml');
const fs = require('fs');

class CloudmapperauditorStack extends cdk.Stack {
/**
*
* @param {cdk.Construct} scope
* @param {string} id
* @param {cdk.StackProps=} props
*/
constructor(scope, id, props) {
super(scope, id, props);

// Load config file
var config = yaml.safeLoad(fs.readFileSync('./s3_bucket_files/cdk_app.yaml', 'utf8'));

if (config['s3_bucket'] == 'MYCOMPANY-cloudmapper') {
console.log("You must configure the CDK app by editing ./s3_bucket_files/cdk_app.yaml");
process.exit(1);
}

// Create VPC to run everything in, but without a NAT gateway.
// We want to run in a public subnet, but the CDK creates a private subnet
// by default, which results in the use of a NAT gateway, which costs $30/mo.
// To avoid that unnecessary charge, we have to create the VPC in a complicated
// way.
// This trick was figured out by jeshan in https://github.com/aws/aws-cdk/issues/1305#issuecomment-525474540
// Normally, the CDK does not allow this because the private subnets have to have
// a route out, and you can't get rid of the private subnets.
// So the trick is to remove the routes out.
// The private subnets remain, but are not usable and have no costs.
const vpc = new ec2.Vpc(this, 'CloudMapperVpc', {
maxAzs: 2,
natGateways: 0
});

// Create a condition that will always fail.
// We will use this in a moment to remove the routes.
var exclude_condition = new cdk.CfnCondition(this,
'exclude-default-route-subnet',
{
// Checks if true == false, so this always fails
expression: cdk.Fn.conditionEquals(true, false)
}
);

// For the private subnets, add a CloudFormation condition to the routes
// to cause them to not be created.
for (var subnet of vpc.privateSubnets) {
for (var child of subnet.node.children) {
if (child.constructor.name==="CfnRoute") {
child.cfnOptions.condition = exclude_condition
}
}
}

// Define the ECS task
const cluster = new ecs.Cluster(this, 'Cluster', { vpc });

const taskDefinition = new ecs.FargateTaskDefinition(this, 'taskDefinition', {});

taskDefinition.addContainer('cloudmapper-container', {
image: ecs.ContainerImage.fromAsset('./resources'),
memoryLimitMiB: 512,
cpu: 256,
environment: {
S3_BUCKET: config['s3_bucket']
},
logging: new ecs.AwsLogDriver({
streamPrefix: 'cloudmapper',
logRetention: logs.RetentionDays.TWO_WEEKS
})
});

// Grant the ability to assume the IAM role in any account
taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({
resources: ["arn:aws:iam::*:role/"+config['iam_role']],
actions: ['sts:AssumeRole']
}));

// Grant the ability to read and write the files from the S3 bucket
taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({
resources: ["arn:aws:s3:::"+config['s3_bucket']],
actions: ['s3:ListBucket']
}));
taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({
resources: ["arn:aws:s3:::"+config['s3_bucket']+"/*"],
actions: ['s3:GetObject','s3:PutObject', 's3:DeleteObject']
}));

// Grant the ability to record the stdout to CloudWatch Logs
taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({
resources: ["*"],
actions: ['logs:*']
}));

// Grant the ability to record error and success metrics
taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({
// This IAM privilege has no paths or conditions
resources: ["*"],
actions: ['cloudwatch:PutMetricData']
}));

// Grant the ability to read from Secrets Manager
taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({
// This IAM privilege has no paths or conditions
resources: ["*"],
actions: ['secretsmanager:GetSecretValue'],
conditions: {'ForAnyValue:StringLike':{'secretsmanager:SecretId': '*cloudmapper-slack-webhook*'}}
}));

// Create rule to trigger this be run every 24 hours
new events.Rule(this, "scheduled_run", {
ruleName: "cloudmapper_scheduler",
// Run at 2am EST (6am UTC) every night
schedule: events.Schedule.expression("cron(0 6 * * ? *)"),
description: "Starts the CloudMapper auditing task every night",
targets: [new targets.EcsTask({
cluster: cluster,
taskDefinition: taskDefinition,
subnetSelection: {subnetType: ec2.SubnetType.PUBLIC}
})]
});

// Create rule to trigger this manually
new events.Rule(this, "manual_run", {
ruleName: "cloudmapper_manual_run",
eventPattern: {source: ['cloudmapper']},
description: "Allows CloudMapper auditing to be manually started",
targets: [new targets.EcsTask({
cluster: cluster,
taskDefinition: taskDefinition,
subnetSelection: {subnetType: ec2.SubnetType.PUBLIC}
})]
});

// Create alarm for any errors
const error_alarm = new cloudwatch.Alarm(this, "error_alarm", {
metric: new cloudwatch.Metric({
namespace: 'cloudmapper',
metricName: "errors",
statistic: "Sum"
}),
threshold: 0,
evaluationPeriods: 1,
datapointsToAlarm: 1,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
alarmDescription: "Detect errors",
alarmName: "cloudmapper_errors"
});

// Create SNS for alarms to be sent to
const sns_topic = new sns.Topic(this, 'cloudmapper_alarm', {
displayName: 'cloudmapper_alarm'
});

// Connect the alarm to the SNS
error_alarm.addAlarmAction(new cloudwatch_actions.SnsAction(sns_topic));

// Create Lambda to forward alarms
const alarm_forwarder = new lambda.Function(this, "alarm_forwarder", {
runtime: lambda.Runtime.PYTHON_3_7,
code: lambda.Code.asset("resources/alarm_forwarder"),
handler: "main.handler",
description: "Forwards alarms from the local SNS to another",
logRetention: logs.RetentionDays.TWO_WEEKS,
timeout: cdk.Duration.seconds(30),
memorySize: 128,
environment: {
"ALARM_SNS": config['alarm_sns_arn']
},
});

// Add priv to publish the events so the alarms can be forwarded
alarm_forwarder.addToRolePolicy(new iam.PolicyStatement({
resources: [config['alarm_sns_arn']],
actions: ['sns:Publish']
}));

// Connect the SNS to the Lambda
sns_topic.addSubscription(new sns_subscription.LambdaSubscription(alarm_forwarder));
}
}

module.exports = { CloudmapperauditorStack }
Loading

0 comments on commit 77bf0c5

Please sign in to comment.