-
Notifications
You must be signed in to change notification settings - Fork 809
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #562 from 0xdabbad00/auditor
auditor added
- Loading branch information
Showing
21 changed files
with
3,071 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.git | ||
account-data | ||
docs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# CDK asset staging directory | ||
.cdk.staging | ||
cdk.out |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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":"{}"}]' | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"app": "node bin/cloudmapperauditor.js" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } |
Oops, something went wrong.