├── .github └── FUNDING.yml ├── .gitignore ├── .jshintignore ├── .jshintrc ├── LICENSE ├── Makefile ├── README.md ├── check ├── iamInlinePolicy.js ├── iamManagedPolicy.js ├── iamPolicy.js ├── logicalID.js ├── resourceType.js └── securityGroupInbound.js ├── checks.json ├── cli.js ├── index.js ├── lib └── privateIpRange.js ├── package.json └── test ├── .jshintrc ├── iamInlinePolicy.js ├── iamManagedPolicy.js ├── iamPolicy.js ├── logicalID.js ├── privateIpRange.js ├── resourceType.js ├── securityGroupInbound.js ├── template.js └── templates ├── template0.json └── template1.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: widdix 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr" : 500, 3 | "bitwise" : true, 4 | "camelcase" : false, 5 | "curly" : true, 6 | "eqeqeq" : true, 7 | "forin" : true, 8 | "immed" : false, 9 | "indent" : 4, 10 | "latedef" : true, 11 | "newcap" : false, 12 | "noarg" : true, 13 | "noempty" : true, 14 | "nonew" : false, 15 | "plusplus" : false, 16 | "quotmark" : false, 17 | "undef" : true, 18 | "unused" : false, 19 | "strict" : true, 20 | "trailing" : true, 21 | "maxparams" : false, 22 | "maxdepth" : false, 23 | "maxstatements" : false, 24 | "maxcomplexity" : false, 25 | "maxlen" : false, 26 | "asi" : false, 27 | "boss" : false, 28 | "debug" : false, 29 | "eqnull" : false, 30 | "es5" : false, 31 | "esnext" : false, 32 | "moz" : false, 33 | "evil" : false, 34 | "expr" : false, 35 | "funcscope" : false, 36 | "globalstrict" : false, 37 | "iterator" : false, 38 | "lastsemic" : false, 39 | "laxbreak" : false, 40 | "laxcomma" : false, 41 | "loopfunc" : false, 42 | "multistr" : false, 43 | "proto" : false, 44 | "scripturl" : false, 45 | "shadow" : false, 46 | "sub" : false, 47 | "supernew" : false, 48 | "validthis" : false, 49 | "browser" : false, 50 | "couch" : false, 51 | "devel" : false, 52 | "dojo" : false, 53 | "jquery" : false, 54 | "mootools" : false, 55 | "node" : true, 56 | "nonstandard" : false, 57 | "prototypejs" : false, 58 | "rhino" : false, 59 | "worker" : false, 60 | "wsh" : false, 61 | "yui" : false, 62 | "globals" : {} 63 | } 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 widdix GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: test 2 | 3 | jshint: 4 | @echo "jshint" 5 | @find . -name "*.js" -print0 | xargs -0 ./node_modules/.bin/jshint 6 | 7 | circular: 8 | @echo "circular" 9 | @./node_modules/.bin/madge --circular --format amd --exclude "madge|source-map" . 10 | 11 | coverage: 12 | @echo "coverage" 13 | @./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha test/* 14 | @open coverage/lcov-report/index.html 15 | @echo 16 | 17 | mocha: 18 | @echo "mocha" 19 | @./node_modules/.bin/mocha test/*.js 20 | @echo 21 | 22 | test: jshint mocha circular 23 | @echo "test" 24 | @echo 25 | 26 | outdated: 27 | @echo "outdated modules?" 28 | @./node_modules/.bin/npmedge 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/widdix/aws-cf-checker.png)](http://travis-ci.org/widdix/aws-cf-checker) 2 | [![NPM version](https://badge.fury.io/js/aws-cf-checker.png)](http://badge.fury.io/js/aws-cf-checker) 3 | [![NPM dependencies](https://david-dm.org/widdix/aws-cf-checker.png)](https://david-dm.org/widdix/aws-cf-checker) 4 | 5 | # AWS CloudFormation Checker 6 | 7 | Checks can guarantee high security, reliability and conformity of your CloudFormation templates. We provide a set of default checks that you can use to validate your templates. 8 | 9 | ## CLI usage 10 | 11 | install the module globally 12 | 13 | ``` 14 | npm install aws-cf-checker -g 15 | ``` 16 | 17 | reading template from file 18 | 19 | ``` 20 | cf-checker --templateFile ./path/to/template.json 21 | 22 | cf-checker --templateFile ./path/to/template.json --checksFile ./path/to/checks.json 23 | ``` 24 | 25 | reading template from stdin 26 | 27 | ``` 28 | cat ./path/to/template.json | cf-checker 29 | 30 | cat ./path/to/template.json | cf-checker --checksFile ./path/to/checks.json 31 | ``` 32 | 33 | as long as the exit code is `0` your template is fine 34 | 35 | ## Programatic usage 36 | 37 | install the module locally 38 | 39 | ``` 40 | npm install aws-cf-checker 41 | ``` 42 | 43 | reading template from file 44 | 45 | ```javascript 46 | var checker = require("aws-cf-checker") 47 | 48 | checker.checkFile("./path/to/template.json", {"logicalID": {}}, function(err, findings) { 49 | if (err) { 50 | throw err; 51 | } else { 52 | if (findings.length > 0) { 53 | console.error("findings", findings); 54 | } else { 55 | console.log("no findings"); 56 | } 57 | } 58 | }); 59 | ``` 60 | 61 | using a template object 62 | 63 | ```javascript 64 | var checker = require("aws-cf-checker") 65 | 66 | var template = { 67 | "AWSTemplateFormatVersion": "2010-09-09", 68 | "Description": "minimal template" 69 | }; 70 | checker.checkTemplate(template, {"logicalID": {}}, function(err, findings) { 71 | if (err) { 72 | throw err; 73 | } else { 74 | if (findings.length > 0) { 75 | console.error("findings", findings); 76 | } else { 77 | console.log("no findings"); 78 | } 79 | } 80 | }); 81 | ``` 82 | 83 | as long as the `findings` array is empty your template is fine 84 | 85 | ## Checks 86 | 87 | Checks are configured with a JSON file. Have a look at our [default checks](https://github.com/widdix/aws-cf-checker/blob/master/checks.json). 88 | 89 | ### logicalID 90 | 91 | Checks logical ids of your template. 92 | 93 | Options: (Object) 94 | 95 | * `case`: (Enum["pascal", "camel"] default: "pascal") 96 | 97 | ### resourceType 98 | 99 | Checks if the resource types are allowed in the template. Wildcard * is supported. 100 | 101 | By default, nothing is allowed (implicit deny). If you deny something it overrides what you allowed (explicit deny). 102 | 103 | Options: (Object) 104 | 105 | * `deny`: (Array[String]) (whitelist, wildcard * can be used) 106 | * `allow`: (Array[String]) (blacklist, wildcard * can be used) 107 | 108 | ### securityGroupInbound 109 | 110 | Checks that only security groups attached to: 111 | 112 | * AWS::ElasticLoadBalancing::LoadBalancer (external) 113 | 114 | allow traffic from public IP addresses. 115 | 116 | Security groups attached to: 117 | 118 | * AWS::ElasticLoadBalancing::LoadBalancer (internal) 119 | * AWS::AutoScaling::LaunchConfiguration 120 | * AWS::EC2::NetworkInterface 121 | * AWS::EC2::Instance 122 | * AWS::EC2::SpotFleet 123 | * AWS::RDS::DBInstance 124 | * AWS::RDS::DBCluster 125 | * AWS::Redshift::Cluster 126 | * AWS::ElastiCache::CacheCluster 127 | * AWS::ElastiCache::ReplicationGroup 128 | * AWS::EFS::MountTarget 129 | * AWS::OpsWorks::Layer 130 | 131 | should only allow inbound traffic from other security groups or private ip addresses. 132 | 133 | Assumes that your account only supports the [EC2 platform](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-supported-platforms.html) EC2-VPC. 134 | 135 | Options: (Object) 136 | 137 | none 138 | 139 | ### iamInlinePolicy 140 | 141 | Checks IAM Users, Groups and Roles for inline policies. 142 | 143 | Options: (Boolean) 144 | 145 | `true` := inline policies are allowed 146 | `false` := inline policies are denied 147 | 148 | ### iamPolicy 149 | 150 | Checks allowed actions and resources of IAM policy statements. Wildcard * is supported. 151 | 152 | A statement with NotAction is a finding. 153 | A statement with Effect != Allow is skipped. 154 | 155 | By default, nothing is allowed (implicit deny). If you deny something it overrides what you allowed (explicit deny). 156 | 157 | Options: (Object) 158 | 159 | * `allow`: (Array[Object]) List of allowed actions & resources (whitelist) 160 | * `action`: (String | Array[String]) IAM action (wildcard * can be used) 161 | * `resource`: (String | Array[String]) IAM resource (wildcard * can be used) 162 | * `deny`: (Array[Object]) List of denied actions & resources (blacklist) 163 | * `action`: (String | Array[String]) IAM action (wildcard * can be used) 164 | * `resource`: (String | Array[String]) IAM resource (wildcard * can be used) 165 | 166 | ### iamManagedPolicy 167 | 168 | Checks IAM Users, Groups and Roles for managed policy attachments. Wildcard * is supported. 169 | 170 | Options: (Object) 171 | 172 | * `allow`: (Array[String]) List of allowed ARNs (whitelist, wildcard * can be used) 173 | * `deny`: (Array[String]) List of denied ARNs (blacklist, wildcard * can be used) 174 | -------------------------------------------------------------------------------- /check/iamInlinePolicy.js: -------------------------------------------------------------------------------- 1 | /* 2 | Checks IAM Users, Groups and Roles for inline policies. 3 | 4 | Options: (Boolean) 5 | 6 | `true` := inline policies are allowed 7 | `false` := inline policies are denied 8 | */ 9 | "use strict"; 10 | 11 | var _ = require("lodash"); 12 | 13 | function filterPartResource(object) { 14 | return object.Part === "Resource"; 15 | } 16 | 17 | function filterTypeIamEntity(object) { 18 | return object.Type === "AWS::IAM::Group" || object.Type === "AWS::IAM::Role" || object.Type === "AWS::IAM::User" || object.Type === "AWS::IAM::Policy"; 19 | } 20 | 21 | exports.check = function(objects, options, cb) { 22 | var findings = []; 23 | function checker(object) { 24 | if (options === false) { 25 | if (object.Type === "AWS::IAM::Policy" || object.Properties.Policies !== undefined) { 26 | findings.push({ 27 | logicalID: object.LogicalId, 28 | message: "Inline Policy not allowed" 29 | }); 30 | } 31 | } 32 | } 33 | _.chain(objects) 34 | .filter(filterPartResource) 35 | .filter(filterTypeIamEntity) 36 | .each(checker) 37 | .value(); 38 | cb(null, findings); 39 | }; 40 | -------------------------------------------------------------------------------- /check/iamManagedPolicy.js: -------------------------------------------------------------------------------- 1 | /* 2 | Checks IAM Users, Groups and Roles for managed policy attachments. Wildcard * is supported. 3 | 4 | Options: (Object) 5 | 6 | * `allow`: (Array[String]) List of allowed ARNs (whitelist, wildcard * can be used) 7 | * `deny`: (Array[String]) List of denied ARNs (blacklist, wildcard * can be used) 8 | */ 9 | "use strict"; 10 | 11 | var _ = require("lodash"); 12 | var wildstring = require("wildstring"); 13 | 14 | function filterPartResource(object) { 15 | return object.Part === "Resource"; 16 | } 17 | 18 | function filterTypeIamEntity(object) { 19 | return object.Type === "AWS::IAM::Group" || object.Type === "AWS::IAM::Role" || object.Type === "AWS::IAM::User"; 20 | } 21 | 22 | function extractManagedPolicyARNs(object) { 23 | return object.Properties.ManagedPolicyArns; 24 | } 25 | 26 | exports.check = function(objects, options, cb) { 27 | var findings = []; 28 | function checker(object) { 29 | var managedPolicyARNs = extractManagedPolicyARNs(object); 30 | _.each(managedPolicyARNs, function(managedPolicyARN) { 31 | if (options.allow !== undefined && _.some(options.allow, function(allow) { 32 | return wildstring.match(allow, managedPolicyARN); 33 | }) === false) { 34 | findings.push({ 35 | logicalID: object.LogicalId, 36 | message: "ManagedPolicyARN " + managedPolicyARN + " not allowed" 37 | }); 38 | } 39 | if (options.deny !== undefined && _.some(options.deny, function(deny) { 40 | return wildstring.match(deny, managedPolicyARN); 41 | }) === true) { 42 | findings.push({ 43 | logicalID: object.LogicalId, 44 | message: "ManagedPolicyARN " + managedPolicyARN + " denied" 45 | }); 46 | } 47 | }); 48 | } 49 | _.chain(objects) 50 | .filter(filterPartResource) 51 | .filter(filterTypeIamEntity) 52 | .each(checker) 53 | .value(); 54 | cb(null, findings); 55 | }; 56 | -------------------------------------------------------------------------------- /check/iamPolicy.js: -------------------------------------------------------------------------------- 1 | /* 2 | Checks allowed actions and resources of IAM policy statements. Wildcard * is supported. 3 | 4 | A statement with NotAction is a finding. 5 | A statement with Effect != Allow is skipped. 6 | 7 | By default, nothing is allowed (implicit deny). If you deny something it overrides what you allowed (explicit deny). 8 | 9 | Options: (Object) 10 | 11 | * `allow`: (Array[Object]) List of allowed actions & resources (whitelist) 12 | * `action`: (String | Array[String]) IAM action (wildcard * can be used) 13 | * `resource`: (String | Array[String]) IAM resource (wildcard * can be used) 14 | * `deny`: (Array[Object]) List of denied actions & resources (blacklist) 15 | * `action`: (String | Array[String]) IAM action (wildcard * can be used) 16 | * `resource`: (String | Array[String]) IAM resource (wildcard * can be used) 17 | */ 18 | "use strict"; 19 | 20 | var _ = require("lodash"); 21 | var wildstring = require("wildstring"); 22 | 23 | function filterPartResource(object) { 24 | return object.Part === "Resource"; 25 | } 26 | 27 | function filterTypeIamEntity(object) { 28 | return object.Type === "AWS::IAM::Group" || object.Type === "AWS::IAM::Role" || object.Type === "AWS::IAM::User" || object.Type === "AWS::IAM::Policy" || "AWS::IAM::ManagedPolicy"; 29 | } 30 | 31 | function filterEffectAllow(statement) { 32 | return statement.Effect === "Allow"; 33 | } 34 | 35 | function extractNotActions(statements) { 36 | return _.chain(statements) 37 | .filter(function(statement) { 38 | return statement.NotAction !== undefined; 39 | }) 40 | .map("NotAction") 41 | .flatten() 42 | .value(); 43 | } 44 | 45 | function extractNotResources(statements) { 46 | return _.chain(statements) 47 | .filter(function(statement) { 48 | return statement.NotResource !== undefined; 49 | }) 50 | .map("NotResource") 51 | .flatten() 52 | .value(); 53 | } 54 | 55 | function extractStatements(object) { 56 | if (object.Type === "AWS::IAM::Policy" || object.Type === "AWS::IAM::ManagedPolicy") { 57 | return object.Properties.PolicyDocument.Statement; 58 | } else { 59 | return _.chain(object.Properties.Policies) 60 | .map(function(policy) { 61 | return policy.PolicyDocument.Statement; 62 | }) 63 | .flatten() 64 | .value(); 65 | } 66 | } 67 | 68 | function toWildcard(input) { 69 | if (input === undefined) { 70 | return "*"; 71 | } else { 72 | return input; 73 | } 74 | } 75 | 76 | function toArray(input) { 77 | if (Array.isArray(input) === false) { 78 | return [input]; 79 | } else { 80 | return input; 81 | } 82 | } 83 | 84 | function cross(action, resource) { 85 | var res = []; 86 | _.each(toArray(toWildcard(action)), function(a) { 87 | _.each(toArray(toWildcard(resource)), function(r) { 88 | res.push({"action": a, "resource": r}); 89 | }); 90 | }); 91 | return res; 92 | } 93 | exports.cross = cross; 94 | 95 | function extractAllowedActionResourcePairs(statements) { 96 | return _.chain(statements) 97 | .filter(filterEffectAllow) 98 | .map(function(statement) { 99 | return cross(statement.Action, statement.Resource); 100 | }) 101 | .flatten() 102 | .value(); 103 | } 104 | 105 | exports.check = function(objects, options, cb) { 106 | var findings = []; 107 | function checker(object) { 108 | var statements = extractStatements(object); 109 | var notActions = extractNotActions(statements); 110 | var notResources = extractNotResources(statements); 111 | var allowedActionResourcePairs = extractAllowedActionResourcePairs(statements); 112 | if (notActions.length > 0) { 113 | _.each(notActions, function(action) { 114 | findings.push({ 115 | logicalID: object.LogicalId, 116 | message: "NotAction " + action + " is not allowed" 117 | }); 118 | }); 119 | } else if (notResources.length > 0) { 120 | _.each(notResources, function(resource) { 121 | findings.push({ 122 | logicalID: object.LogicalId, 123 | message: "NotResource " + resource + " is not allowed" 124 | }); 125 | }); 126 | } else { 127 | _.each(allowedActionResourcePairs, function(pair) { 128 | if (_.some(options.allow, function(allow) { 129 | return _.some(toArray(toWildcard(allow.action)), function(allowAction) { 130 | return wildstring.match(allowAction, pair.action); 131 | }) && _.some(toArray(toWildcard(allow.resource)), function(allowResource) { 132 | return wildstring.match(allowResource, pair.resource); 133 | }); 134 | }) === false) { 135 | findings.push({ 136 | logicalID: object.LogicalId, 137 | message: "Action & Resource " + pair.action + " & " + pair.resource + " not allowed" 138 | }); 139 | } 140 | if (_.some(options.deny, function(deny) { 141 | return _.some(toArray(toWildcard(deny.action)), function(denyAction) { 142 | return wildstring.match(denyAction, pair.action); 143 | }) && _.some(toArray(toWildcard(deny.resource)), function(denyResource) { 144 | return wildstring.match(denyResource, pair.resource); 145 | }); 146 | }) === true) { 147 | findings.push({ 148 | logicalID: object.LogicalId, 149 | message: "Action & Resource " + pair.action + " & " + pair.resource + " denied" 150 | }); 151 | } 152 | }); 153 | } 154 | } 155 | _.chain(objects) 156 | .filter(filterPartResource) 157 | .filter(filterTypeIamEntity) 158 | .each(checker) 159 | .value(); 160 | cb(null, findings); 161 | }; 162 | -------------------------------------------------------------------------------- /check/logicalID.js: -------------------------------------------------------------------------------- 1 | /* 2 | Checks logical ids of your template. 3 | 4 | Options: (Object) 5 | 6 | * `case`: (Enum["pascal", "camel"] default: "pascal") 7 | */ 8 | "use strict"; 9 | 10 | var _ = require("lodash"); 11 | 12 | var CASES = { 13 | "camel": /^([a-z0-9]+)([A-Z][a-z0-9]+)*/, 14 | "pascal": /^([A-Z][a-z0-9]*)+/ 15 | }; 16 | 17 | exports.check = function(objects, options, cb) { 18 | var findings = []; 19 | var c = options["case"] || "pascal"; 20 | var regex = CASES[c]; 21 | function checker(object) { 22 | if (regex.test(object.LogicalId) === false) { 23 | findings.push({ 24 | logicalID: object.LogicalId, 25 | message: "Logical ID does not match " + c + "case" 26 | }); 27 | } 28 | } 29 | _.each(objects, checker); 30 | cb(null, findings); 31 | }; 32 | -------------------------------------------------------------------------------- /check/resourceType.js: -------------------------------------------------------------------------------- 1 | /* 2 | Checks if the resource types are allowed in the template. Wildcard * is supported. 3 | 4 | By default, nothing is allowed (implicit deny). If you deny something it overrides what you allowed (explicit deny). 5 | 6 | Options: (Object) 7 | 8 | * `deny`: (Array[String]) (whitelist, wildcard * can be used) 9 | * `allow`: (Array[String]) (blacklist, wildcard * can be used) 10 | */ 11 | "use strict"; 12 | 13 | var _ = require("lodash"); 14 | var wildstring = require("wildstring"); 15 | 16 | function filterPartResource(object) { 17 | return object.Part === "Resource"; 18 | } 19 | 20 | exports.check = function(objects, options, cb) { 21 | var findings = []; 22 | function checker(object) { 23 | if (_.some(options.allow, function(allow) { 24 | return wildstring.match(allow, object.Type); 25 | }) === false) { 26 | findings.push({ 27 | logicalID: object.LogicalId, 28 | message: "Resource Type " + object.Type + " not allowed" 29 | }); 30 | } 31 | if (_.some(options.deny, function(deny) { 32 | return wildstring.match(deny, object.Type); 33 | }) === true) { 34 | findings.push({ 35 | logicalID: object.LogicalId, 36 | message: "Resource Type " + object.Type + " denied" 37 | }); 38 | } 39 | } 40 | _.chain(objects) 41 | .filter(filterPartResource) 42 | .each(checker) 43 | .value(); 44 | cb(null, findings); 45 | }; 46 | -------------------------------------------------------------------------------- /check/securityGroupInbound.js: -------------------------------------------------------------------------------- 1 | /* 2 | Checks that only security groups attached to: 3 | 4 | * AWS::ElasticLoadBalancing::LoadBalancer (external) 5 | 6 | allow traffic from public IP addresses. 7 | 8 | Security groups attached to: 9 | 10 | * AWS::ElasticLoadBalancing::LoadBalancer (internal) 11 | * AWS::AutoScaling::LaunchConfiguration 12 | * AWS::EC2::NetworkInterface 13 | * AWS::EC2::Instance 14 | * AWS::EC2::SpotFleet 15 | * AWS::RDS::DBInstance 16 | * AWS::RDS::DBCluster 17 | * AWS::Redshift::Cluster 18 | * AWS::ElastiCache::CacheCluster 19 | * AWS::ElastiCache::ReplicationGroup 20 | * AWS::EFS::MountTarget 21 | * AWS::OpsWorks::Layer 22 | 23 | should only allow inbound traffic from other security groups or private ip addresses. 24 | 25 | Assumes that your account only supports the [EC2 platform](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-supported-platforms.html) EC2-VPC. 26 | 27 | Options: (Object) 28 | 29 | none 30 | */ 31 | 32 | // TODO what about port ranges? I think we should not allow them 33 | // TODO it should be possible to allow or deny ports in the options 34 | "use strict"; 35 | 36 | var _ = require("lodash"); 37 | var privateIpRange = require("../lib/privateIpRange.js"); 38 | 39 | function filterPartResource(object) { 40 | return object.Part === "Resource"; 41 | } 42 | 43 | function filterTypeSecurityGroup(object) { 44 | return object.Type === "AWS::EC2::SecurityGroup"; 45 | } 46 | 47 | function filterTypeSecurityGroupIngress(object) { 48 | return object.Type === "AWS::EC2::SecurityGroupIngress"; 49 | } 50 | 51 | function mapRef(entry) { 52 | return entry.Ref; 53 | } 54 | 55 | function normalizeSecurityGroupAttachmentIds(propertyName) { 56 | return function(object) { 57 | if (object.Properties[propertyName] === undefined) { 58 | throw new Error("can not find property " + propertyName + " in " + object.LogicalId); 59 | } 60 | return _.map(object.Properties[propertyName], mapRef); 61 | }; 62 | } 63 | 64 | function alwaysPrivate(object) { 65 | return false; 66 | } 67 | 68 | // defines what types can be attached to a security group 69 | var SECURITY_GROUP_ATTACHMENT_DEFINITION = { 70 | "AWS::ElasticLoadBalancing::LoadBalancer": { 71 | "normalizationFun": normalizeSecurityGroupAttachmentIds("SecurityGroups"), 72 | "isPublicFun": function(object) { 73 | if(object.Properties.Scheme === "internal") { 74 | return false; 75 | } 76 | return true; 77 | } 78 | }, 79 | "AWS::AutoScaling::LaunchConfiguration": { 80 | "normalizationFun": normalizeSecurityGroupAttachmentIds("SecurityGroups"), 81 | "isPublicFun": alwaysPrivate 82 | }, 83 | "AWS::RDS::DBInstance": { 84 | "normalizationFun": normalizeSecurityGroupAttachmentIds("VPCSecurityGroups"), 85 | "isPublicFun": alwaysPrivate 86 | }, 87 | "AWS::RDS::DBCluster": { 88 | "normalizationFun": normalizeSecurityGroupAttachmentIds("VpcSecurityGroupIds"), 89 | "isPublicFun": alwaysPrivate 90 | }, 91 | "AWS::Redshift::Cluster": { 92 | "normalizationFun": normalizeSecurityGroupAttachmentIds("VpcSecurityGroupIds"), 93 | "isPublicFun": alwaysPrivate 94 | }, 95 | "AWS::ElastiCache::CacheCluster": { 96 | "normalizationFun": normalizeSecurityGroupAttachmentIds("VpcSecurityGroupIds"), 97 | "isPublicFun": alwaysPrivate 98 | }, 99 | "AWS::ElastiCache::ReplicationGroup": { 100 | "normalizationFun": normalizeSecurityGroupAttachmentIds("SecurityGroupIds"), 101 | "isPublicFun": alwaysPrivate 102 | }, 103 | "AWS::EC2::Instance": { 104 | "normalizationFun": function(object) { 105 | if (object.Properties.NetworkInterfaces) { 106 | return _.chain(object.Properties.NetworkInterfaces) 107 | .map("GroupSet") 108 | .flatten() 109 | .map(mapRef) 110 | .value(); 111 | } else { 112 | return normalizeSecurityGroupAttachmentIds("SecurityGroupIds")(object); 113 | } 114 | }, 115 | "isPublicFun": alwaysPrivate // TODO is the assumption that a standalone EC2 instance should be never accessible from the outside valid? 116 | }, 117 | "AWS::EFS::MountTarget": { 118 | "normalizationFun": normalizeSecurityGroupAttachmentIds("SecurityGroups"), 119 | "isPublicFun": alwaysPrivate 120 | }, 121 | "AWS::EC2::SpotFleet": { 122 | "normalizationFun": function(object) { 123 | return _.chain(object.Properties.SpotFleetRequestConfigData.LaunchSpecifications) 124 | .map("SecurityGroups") 125 | .flatten() 126 | .map(mapRef) 127 | .value(); 128 | }, 129 | "isPublicFun": alwaysPrivate 130 | }, 131 | "AWS::OpsWorks::Layer": { 132 | "normalizationFun": normalizeSecurityGroupAttachmentIds("CustomSecurityGroupIds"), 133 | "isPublicFun": alwaysPrivate 134 | }, 135 | "AWS::EC2::NetworkInterface": { 136 | "normalizationFun": normalizeSecurityGroupAttachmentIds("GroupSet"), 137 | "isPublicFun": alwaysPrivate // TODO is the assumption that a standalone ENI should be never accessible from the outside valid? 138 | } 139 | }; 140 | 141 | function findSecurityGroupAttachments(objects, securityGroupObject) { 142 | return _.chain(objects) 143 | .filter(filterPartResource) 144 | .filter(function(object) { 145 | var definition = SECURITY_GROUP_ATTACHMENT_DEFINITION[object.Type]; 146 | return definition !== undefined; 147 | }) 148 | .map(function(object) { 149 | var definition = SECURITY_GROUP_ATTACHMENT_DEFINITION[object.Type]; 150 | object.AttachedSecurityGroupLogicalIds = definition.normalizationFun(object); 151 | return object; 152 | }) 153 | .filter(function(object) { 154 | return object.AttachedSecurityGroupLogicalIds.indexOf(securityGroupObject.LogicalId) !== -1; 155 | }) 156 | .value(); 157 | } 158 | 159 | function extractIngressRules(objects, securityGroupObject) { 160 | return _.chain(objects) 161 | .filter(filterPartResource) 162 | .filter(filterTypeSecurityGroupIngress) 163 | .filter(function(ingressObject) { 164 | if (ingressObject.Properties.GroupId.Ref === undefined) { 165 | return false; 166 | } 167 | return ingressObject.Properties.GroupId.Ref === securityGroupObject.LogicalId; 168 | }) 169 | .value(); 170 | } 171 | 172 | function hasPublicAttachments(attachments) { 173 | return _.chain(attachments) 174 | .find(function(attachment) { 175 | var definition = SECURITY_GROUP_ATTACHMENT_DEFINITION[attachment.Type]; 176 | return definition.isPublicFun(attachment); 177 | }) 178 | .value() !== undefined; 179 | } 180 | 181 | function hasPrivateAttachments(attachments) { 182 | return _.chain(attachments) 183 | .find(function(attachment) { 184 | var definition = SECURITY_GROUP_ATTACHMENT_DEFINITION[attachment.Type]; 185 | return !definition.isPublicFun(attachment); 186 | }) 187 | .value() !== undefined; 188 | } 189 | 190 | function hasPublicRules(rules) { 191 | return _.chain(rules) 192 | .find(function(rule) { 193 | if (rule.CidrIp !== undefined) { 194 | return !privateIpRange(rule.CidrIp); 195 | } 196 | return false; 197 | }) 198 | .value() !== undefined; 199 | } 200 | 201 | function hasPrivateRules(rules) { 202 | return _.chain(rules) 203 | .find(function(rule) { 204 | if (rule.SourceSecurityGroupId !== undefined) { 205 | return true; 206 | } 207 | return privateIpRange(rule.CidrIp); 208 | }) 209 | .value() !== undefined; 210 | } 211 | 212 | exports.check = function(objects, options, cb) { 213 | var findings = []; 214 | function checker(object) { 215 | var rules = extractIngressRules(objects, object); 216 | rules = rules.concat(object.Properties.SecurityGroupIngress); 217 | var attachments = findSecurityGroupAttachments(objects, object); 218 | if (hasPublicAttachments(attachments) && hasPublicRules(rules)) { 219 | return; 220 | } else if (hasPublicAttachments(attachments) && hasPrivateRules(rules)) { 221 | return; 222 | } else if (hasPrivateAttachments(attachments) && hasPublicRules(rules)) { 223 | findings.push({ 224 | logicalID: object.LogicalId, 225 | message: "public inbound rules for private attachments found" 226 | }); 227 | } else if (hasPrivateAttachments(attachments) && hasPrivateRules(rules)) { 228 | return; 229 | } else { 230 | throw new Error("unexpected combination"); 231 | } 232 | } 233 | _.chain(objects) 234 | .filter(filterPartResource) 235 | .filter(filterTypeSecurityGroup) 236 | .each(checker) 237 | .value(); 238 | cb(null, findings); 239 | }; 240 | -------------------------------------------------------------------------------- /checks.json: -------------------------------------------------------------------------------- 1 | { 2 | "logicalID": { 3 | "case": "pascal" 4 | }, 5 | "securityGroupInbound": { 6 | } 7 | } -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | var checker = require("./index.js"); 5 | var fs = require("fs"); 6 | var argv = require("minimist")(process.argv.slice(2)); 7 | 8 | function checkCallback(err, findings) { 9 | if (err) { 10 | console.error(err.message); 11 | process.exit(2); 12 | } else { 13 | if (findings.length > 0) { 14 | findings.forEach(console.dir); 15 | process.exit(1); 16 | } else { 17 | process.exit(0); 18 | } 19 | } 20 | } 21 | 22 | function checkFile(file, options) { 23 | checker.checkFile(file, options, checkCallback); 24 | } 25 | 26 | function checkJSON(json, options) { 27 | checker.checkTemplate(JSON.parse(json), options, checkCallback); 28 | } 29 | 30 | var checks = require("./checks.json"); 31 | if (argv.checksFile) { 32 | var json = fs.readFileSync(argv.checksFile, {"encoding": "utf8"}); 33 | checks = JSON.parse(json); 34 | } 35 | 36 | if (argv.templateFile) { 37 | checkFile(argv.templateFile, checks); 38 | } else { 39 | var data = ""; 40 | process.stdin.resume(); 41 | process.stdin.setEncoding("utf8"); 42 | process.stdin.on("data", function(chunk) { 43 | data += chunk; 44 | }); 45 | process.stdin.on("end", function() { 46 | checkJSON(data, checks); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"); 4 | var _ = require("lodash"); 5 | var async = require("neo-async"); 6 | 7 | function loadJSONFile(filename, cb) { 8 | fs.readFile(filename, {"encoding": "utf8"}, cb); 9 | } 10 | 11 | function parseJSON(json, cb) { 12 | cb(null, JSON.parse(json)); 13 | } 14 | 15 | function clone(obj) { 16 | return JSON.parse(JSON.stringify(obj)); 17 | } 18 | 19 | function mapTemplate(template, cb) { 20 | var objects = []; 21 | function mapper(part) { 22 | return function(object, logicalId) { 23 | var res = clone(object); 24 | res.LogicalId = logicalId; 25 | res.Part = part; 26 | return res; 27 | }; 28 | } 29 | objects = objects.concat(_.map(template.Parameters, mapper("Parameter"))); 30 | objects = objects.concat(_.map(template.Mappings, mapper("Mapping"))); 31 | objects = objects.concat(_.map(template.Conditions, mapper("Condition"))); 32 | objects = objects.concat(_.map(template.Resources, mapper("Resource"))); 33 | objects = objects.concat(_.map(template.Outputs, mapper("Output"))); 34 | cb(null, objects); 35 | } 36 | 37 | function runChecks(objects, checks, cb) { 38 | async.map(Object.keys(checks), function(check, cb) { 39 | var required; 40 | try { 41 | required = require("./check/" + check + ".js"); 42 | } catch (err) { 43 | cb(err); 44 | return; 45 | } 46 | required.check(objects, checks[check], function(err, findings) { 47 | if (err) { 48 | cb(err); 49 | } else { 50 | cb(null, findings); 51 | } 52 | }); 53 | }, function(err, nestedFindings) { 54 | if (err) { 55 | cb(err); 56 | } else { 57 | cb(null, _.flatten(nestedFindings)); 58 | } 59 | }); 60 | } 61 | 62 | function checkTemplate(template, checks, cb) { 63 | mapTemplate(template, function(err, objects) { 64 | if (err) { 65 | cb(err); 66 | } else { 67 | runChecks(objects, checks, cb); 68 | } 69 | }); 70 | } 71 | 72 | exports.checkTemplate = checkTemplate; 73 | 74 | exports.checkFile = function(filename, checks, cb) { 75 | loadJSONFile(filename, function(err, json) { 76 | if (err) { 77 | cb(err); 78 | } else { 79 | parseJSON(json, function(err, template) { 80 | if (err) { 81 | cb(err); 82 | } else { 83 | checkTemplate(template, checks, cb); 84 | } 85 | }); 86 | } 87 | }); 88 | }; 89 | -------------------------------------------------------------------------------- /lib/privateIpRange.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | var Netmask = require("netmask").Netmask; 5 | 6 | var PRIVATE_IP_RANGES = [ 7 | new Netmask("10.0.0.0/8"), 8 | new Netmask("172.16.0.0/12"), 9 | new Netmask("192.168.0.0/16") 10 | ]; 11 | 12 | module.exports = function(rangeOrAddress) { 13 | var block; 14 | if (rangeOrAddress.indexOf("/") !== -1) { 15 | block = new Netmask(rangeOrAddress); 16 | } else { 17 | block = rangeOrAddress; 18 | } 19 | return _.find(PRIVATE_IP_RANGES, function(range) { 20 | return range.contains(block); 21 | }) !== undefined; 22 | }; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-cf-checker", 3 | "version": "1.0.0", 4 | "description": "Checks AWS CloudFormation templates for security, reliability and conformity", 5 | "keywords": [ 6 | "aws", 7 | "cloudformation", 8 | "cfn", 9 | "cf" 10 | ], 11 | "main": "index.js", 12 | "scripts": { 13 | "test": "make test" 14 | }, 15 | "author": "Michael Wittig ", 16 | "contributors": [ 17 | "Andreas Wittig " 18 | ], 19 | "license": "MIT", 20 | "dependencies": { 21 | "lodash": "4.13.1", 22 | "minimist": "1.2.0", 23 | "neo-async": "1.8.2", 24 | "netmask": "1.0.6", 25 | "wildstring": "1.0.8" 26 | }, 27 | "devDependencies": { 28 | "assert-plus": "1.0.0", 29 | "mocha": "2.5.3", 30 | "jshint": "2.9.2", 31 | "madge": "0.5.3", 32 | "npmedge": "0.2.2", 33 | "istanbul": "0.4.3" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/widdix/aws-cf-checker.git" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/widdix/aws-cf-checker/issues" 41 | }, 42 | "engines": { 43 | "node": ">=0.10" 44 | }, 45 | "bin": { 46 | "cf-checker": "./cli.js" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr" : 500, 3 | "bitwise" : true, 4 | "camelcase" : false, 5 | "curly" : true, 6 | "eqeqeq" : true, 7 | "forin" : true, 8 | "immed" : false, 9 | "indent" : 4, 10 | "latedef" : true, 11 | "newcap" : false, 12 | "noarg" : true, 13 | "noempty" : true, 14 | "nonew" : false, 15 | "plusplus" : false, 16 | "quotmark" : false, 17 | "undef" : true, 18 | "unused" : false, 19 | "strict" : true, 20 | "trailing" : true, 21 | "maxparams" : false, 22 | "maxdepth" : false, 23 | "maxstatements" : false, 24 | "maxcomplexity" : false, 25 | "maxlen" : false, 26 | "asi" : false, 27 | "boss" : false, 28 | "debug" : false, 29 | "eqnull" : false, 30 | "es5" : false, 31 | "esnext" : false, 32 | "moz" : false, 33 | "evil" : false, 34 | "expr" : false, 35 | "funcscope" : false, 36 | "globalstrict" : false, 37 | "iterator" : false, 38 | "lastsemic" : false, 39 | "laxbreak" : false, 40 | "laxcomma" : false, 41 | "loopfunc" : false, 42 | "multistr" : false, 43 | "proto" : false, 44 | "scripturl" : false, 45 | "shadow" : false, 46 | "sub" : false, 47 | "supernew" : false, 48 | "validthis" : false, 49 | "browser" : false, 50 | "couch" : false, 51 | "devel" : false, 52 | "dojo" : false, 53 | "jquery" : false, 54 | "mootools" : false, 55 | "node" : true, 56 | "nonstandard" : false, 57 | "prototypejs" : false, 58 | "rhino" : false, 59 | "worker" : false, 60 | "wsh" : false, 61 | "yui" : false, 62 | "globals" : {}, 63 | "predef" : ["describe", "it", "before", "after"] 64 | } 65 | -------------------------------------------------------------------------------- /test/iamInlinePolicy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var checker = require("../index.js"); 4 | var assert = require("assert-plus"); 5 | 6 | function test(template, options, expectedFindings, done) { 7 | checker.checkTemplate(template, options, function(err, findings) { 8 | if (err) { 9 | throw err; 10 | } else { 11 | if (findings.length !== expectedFindings) { 12 | console.log(findings); 13 | } 14 | assert.equal(findings.length, expectedFindings, "findings"); 15 | done(); 16 | } 17 | }); 18 | } 19 | 20 | describe("iamInlinePolicy", function() { 21 | it("empty", function(done) { 22 | test({ 23 | 24 | }, {"iamInlinePolicy": true}, 0, done); 25 | }); 26 | describe("Policy", function() { 27 | it("with not allowed inline policy", function(done) { 28 | test({ 29 | "Resources": { 30 | "Test": { 31 | "Type": "AWS::IAM::Policy", 32 | "Properties": { 33 | "PolicyDocument": { 34 | "Version": "2012-10-17", 35 | "Statement": [{ 36 | "Effect": "Allow", 37 | "Action": [ 38 | "s3:GetObject", 39 | "s3:ListBucket" 40 | ], 41 | "Resource": "*" 42 | }] 43 | } 44 | } 45 | } 46 | } 47 | }, {"iamInlinePolicy": false}, 1, done); 48 | }); 49 | it("with allowed inline policy", function(done) { 50 | test({ 51 | "Resources": { 52 | "Test": { 53 | "Type": "AWS::IAM::Policy", 54 | "Properties": { 55 | "PolicyDocument": { 56 | "Version": "2012-10-17", 57 | "Statement": [{ 58 | "Effect": "Allow", 59 | "Action": [ 60 | "s3:GetObject", 61 | "s3:ListBucket" 62 | ], 63 | "Resource": "*" 64 | }] 65 | } 66 | } 67 | } 68 | } 69 | }, {"iamInlinePolicy": true}, 0, done); 70 | }); 71 | it("with default", function(done) { 72 | test({ 73 | "Resources": { 74 | "Test": { 75 | "Type": "AWS::IAM::Policy", 76 | "Properties": { 77 | "PolicyDocument": { 78 | "Version": "2012-10-17", 79 | "Statement": [{ 80 | "Effect": "Allow", 81 | "Action": [ 82 | "s3:GetObject", 83 | "s3:ListBucket" 84 | ], 85 | "Resource": "*" 86 | }] 87 | } 88 | } 89 | } 90 | } 91 | }, {"iamInlinePolicy": true}, 0, done); 92 | }); 93 | }); 94 | describe("User", function() { 95 | it("with not allowed inline policy", function(done) { 96 | test({ 97 | "Resources": { 98 | "Test": { 99 | "Type": "AWS::IAM::User", 100 | "Properties": { 101 | "Policies": [{ 102 | "PolicyName": "s3", 103 | "PolicyDocument": { 104 | "Version": "2012-10-17", 105 | "Statement": [{ 106 | "Effect": "Allow", 107 | "Action": [ 108 | "s3:GetObject", 109 | "s3:ListBucket" 110 | ], 111 | "Resource": "*" 112 | }] 113 | } 114 | }] 115 | } 116 | } 117 | } 118 | }, {"iamInlinePolicy": false}, 1, done); 119 | }); 120 | it("with allowed inline policy", function(done) { 121 | test({ 122 | "Resources": { 123 | "Test": { 124 | "Type": "AWS::IAM::User", 125 | "Properties": { 126 | "Policies": [{ 127 | "PolicyName": "s3", 128 | "PolicyDocument": { 129 | "Version": "2012-10-17", 130 | "Statement": [{ 131 | "Effect": "Allow", 132 | "Action": [ 133 | "s3:GetObject", 134 | "s3:ListBucket" 135 | ], 136 | "Resource": "*" 137 | }] 138 | } 139 | }] 140 | } 141 | } 142 | } 143 | }, {"iamInlinePolicy": true}, 0, done); 144 | }); 145 | }); 146 | describe("Group", function() { 147 | it("with not allowed inline policy", function(done) { 148 | test({ 149 | "Resources": { 150 | "Test": { 151 | "Type": "AWS::IAM::Group", 152 | "Properties": { 153 | "Policies": [{ 154 | "PolicyName": "s3", 155 | "PolicyDocument": { 156 | "Version": "2012-10-17", 157 | "Statement": [{ 158 | "Effect": "Allow", 159 | "Action": [ 160 | "s3:GetObject", 161 | "s3:ListBucket" 162 | ], 163 | "Resource": "*" 164 | }] 165 | } 166 | }] 167 | } 168 | } 169 | } 170 | }, {"iamInlinePolicy": false}, 1, done); 171 | }); 172 | it("with allowed inline policy", function(done) { 173 | test({ 174 | "Resources": { 175 | "Test": { 176 | "Type": "AWS::IAM::Group", 177 | "Properties": { 178 | "Policies": [{ 179 | "PolicyName": "s3", 180 | "PolicyDocument": { 181 | "Version": "2012-10-17", 182 | "Statement": [{ 183 | "Effect": "Allow", 184 | "Action": [ 185 | "s3:GetObject", 186 | "s3:ListBucket" 187 | ], 188 | "Resource": "*" 189 | }] 190 | } 191 | }] 192 | } 193 | } 194 | } 195 | }, {"iamInlinePolicy": true}, 0, done); 196 | }); 197 | }); 198 | describe("Role", function() { 199 | it("with not allowed inline policy", function(done) { 200 | test({ 201 | "Resources": { 202 | "Test": { 203 | "Type": "AWS::IAM::Role", 204 | "Properties": { 205 | "AssumeRolePolicyDocument": { 206 | "Version": "2012-10-17", 207 | "Statement": [{ 208 | "Effect": "Allow", 209 | "Principal": { 210 | "Service": ["ec2.amazonaws.com"] 211 | }, 212 | "Action": ["sts:AssumeRole"] 213 | }] 214 | }, 215 | "Policies": [{ 216 | "PolicyName": "s3", 217 | "PolicyDocument": { 218 | "Version": "2012-10-17", 219 | "Statement": [{ 220 | "Effect": "Allow", 221 | "Action": [ 222 | "s3:GetObject", 223 | "s3:ListBucket" 224 | ], 225 | "Resource": "*" 226 | }] 227 | } 228 | }] 229 | } 230 | } 231 | } 232 | }, {"iamInlinePolicy": false}, 1, done); 233 | }); 234 | it("with allowed inline policy", function(done) { 235 | test({ 236 | "Resources": { 237 | "Test": { 238 | "Type": "AWS::IAM::Role", 239 | "Properties": { 240 | "AssumeRolePolicyDocument": { 241 | "Version": "2012-10-17", 242 | "Statement": [{ 243 | "Effect": "Allow", 244 | "Principal": { 245 | "Service": ["ec2.amazonaws.com"] 246 | }, 247 | "Action": ["sts:AssumeRole"] 248 | }] 249 | }, 250 | "Policies": [{ 251 | "PolicyName": "s3", 252 | "PolicyDocument": { 253 | "Version": "2012-10-17", 254 | "Statement": [{ 255 | "Effect": "Allow", 256 | "Action": [ 257 | "s3:GetObject", 258 | "s3:ListBucket" 259 | ], 260 | "Resource": "*" 261 | }] 262 | } 263 | }] 264 | } 265 | } 266 | } 267 | }, {"iamInlinePolicy": true}, 0, done); 268 | }); 269 | }); 270 | }); 271 | -------------------------------------------------------------------------------- /test/iamManagedPolicy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var checker = require("../index.js"); 4 | var assert = require("assert-plus"); 5 | 6 | function test(template, options, expectedFindings, done) { 7 | checker.checkTemplate(template, options, function(err, findings) { 8 | if (err) { 9 | throw err; 10 | } else { 11 | if (findings.length !== expectedFindings) { 12 | console.log(findings); 13 | } 14 | assert.equal(findings.length, expectedFindings, "findings"); 15 | done(); 16 | } 17 | }); 18 | } 19 | 20 | describe("iamManagedPolicy", function() { 21 | it("empty", function(done) { 22 | test({ 23 | 24 | }, {"iamManagedPolicy": {}}, 0, done); 25 | }); 26 | describe("Role", function() { 27 | describe("allow", function() { 28 | it("nothing attached, allow [*]", function(done) { 29 | test({ 30 | "Resources": { 31 | "Test": { 32 | "Type": "AWS::IAM::Role", 33 | "Properties": { 34 | } 35 | } 36 | } 37 | }, {"iamManagedPolicy": {allow: ["*"]}}, 0, done); 38 | }); 39 | it("empty attached, allow [*]", function(done) { 40 | test({ 41 | "Resources": { 42 | "Test": { 43 | "Type": "AWS::IAM::Role", 44 | "Properties": { 45 | "ManagedPolicyArns": [] 46 | } 47 | } 48 | } 49 | }, {"iamManagedPolicy": {allow: ["*"]}}, 0, done); 50 | }); 51 | it("one policy attached, allow [*]", function(done) { 52 | test({ 53 | "Resources": { 54 | "Test": { 55 | "Type": "AWS::IAM::Role", 56 | "Properties": { 57 | "ManagedPolicyArns": ["arn:aws:iam::aws:policy/AdministratorAccess"] 58 | } 59 | } 60 | } 61 | }, {"iamManagedPolicy": {allow: ["*"]}}, 0, done); 62 | }); 63 | it("one policy attached, allow []", function(done) { 64 | test({ 65 | "Resources": { 66 | "Test": { 67 | "Type": "AWS::IAM::Role", 68 | "Properties": { 69 | "ManagedPolicyArns": ["arn:aws:iam::aws:policy/AdministratorAccess"] 70 | } 71 | } 72 | } 73 | }, {"iamManagedPolicy": {allow: []}}, 1, done); 74 | }); 75 | it("two policies attached, allow [*]", function(done) { 76 | test({ 77 | "Resources": { 78 | "Test": { 79 | "Type": "AWS::IAM::Role", 80 | "Properties": { 81 | "ManagedPolicyArns": [ 82 | "arn:aws:iam::aws:policy/AdministratorAccess", 83 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 84 | ] 85 | } 86 | } 87 | } 88 | }, {"iamManagedPolicy": {allow: ["*"]}}, 0, done); 89 | }); 90 | it("two policies attached, allow only one by ARN", function(done) { 91 | test({ 92 | "Resources": { 93 | "Test": { 94 | "Type": "AWS::IAM::Role", 95 | "Properties": { 96 | "ManagedPolicyArns": [ 97 | "arn:aws:iam::aws:policy/AdministratorAccess", 98 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 99 | ] 100 | } 101 | } 102 | } 103 | }, {"iamManagedPolicy": {allow: ["arn:aws:iam::aws:policy/AdministratorAccess"]}}, 1, done); 104 | }); 105 | it("two policies attached, allow only one by wildcard", function(done) { 106 | test({ 107 | "Resources": { 108 | "Test": { 109 | "Type": "AWS::IAM::Role", 110 | "Properties": { 111 | "ManagedPolicyArns": [ 112 | "arn:aws:iam::aws:policy/AdministratorAccess", 113 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 114 | ] 115 | } 116 | } 117 | } 118 | }, {"iamManagedPolicy": {allow: ["arn:aws:iam::aws:policy/*ReadOnlyAccess"]}}, 1, done); 119 | }); 120 | it("two ReadOnly policies attached, allow both by wildcard", function(done) { 121 | test({ 122 | "Resources": { 123 | "Test": { 124 | "Type": "AWS::IAM::Role", 125 | "Properties": { 126 | "ManagedPolicyArns": [ 127 | "arn:aws:iam::aws:policy/AmazonGlacierReadOnlyAccess", 128 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 129 | ] 130 | } 131 | } 132 | } 133 | }, {"iamManagedPolicy": {allow: ["arn:aws:iam::aws:policy/*ReadOnlyAccess"]}}, 0, done); 134 | }); 135 | }); 136 | describe("deny", function() { 137 | it("nothing attached, deny [*]", function(done) { 138 | test({ 139 | "Resources": { 140 | "Test": { 141 | "Type": "AWS::IAM::Role", 142 | "Properties": { 143 | } 144 | } 145 | } 146 | }, {"iamManagedPolicy": {deny: ["*"]}}, 0, done); 147 | }); 148 | it("empty attached, deny [*]", function(done) { 149 | test({ 150 | "Resources": { 151 | "Test": { 152 | "Type": "AWS::IAM::Role", 153 | "Properties": { 154 | "ManagedPolicyArns": [] 155 | } 156 | } 157 | } 158 | }, {"iamManagedPolicy": {deny: ["*"]}}, 0, done); 159 | }); 160 | it("one policy attached, deny []", function(done) { 161 | test({ 162 | "Resources": { 163 | "Test": { 164 | "Type": "AWS::IAM::Role", 165 | "Properties": { 166 | "ManagedPolicyArns": ["arn:aws:iam::aws:policy/AdministratorAccess"] 167 | } 168 | } 169 | } 170 | }, {"iamManagedPolicy": {deny: []}}, 0, done); 171 | }); 172 | it("one policy attached, deny [*]", function(done) { 173 | test({ 174 | "Resources": { 175 | "Test": { 176 | "Type": "AWS::IAM::Role", 177 | "Properties": { 178 | "ManagedPolicyArns": ["arn:aws:iam::aws:policy/AdministratorAccess"] 179 | } 180 | } 181 | } 182 | }, {"iamManagedPolicy": {deny: ["*"]}}, 1, done); 183 | }); 184 | it("two policies attached, deny [*]", function(done) { 185 | test({ 186 | "Resources": { 187 | "Test": { 188 | "Type": "AWS::IAM::Role", 189 | "Properties": { 190 | "ManagedPolicyArns": [ 191 | "arn:aws:iam::aws:policy/AdministratorAccess", 192 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 193 | ] 194 | } 195 | } 196 | } 197 | }, {"iamManagedPolicy": {deny: ["*"]}}, 2, done); 198 | }); 199 | it("two policies attached, deny only one by ARN", function(done) { 200 | test({ 201 | "Resources": { 202 | "Test": { 203 | "Type": "AWS::IAM::Role", 204 | "Properties": { 205 | "ManagedPolicyArns": [ 206 | "arn:aws:iam::aws:policy/AdministratorAccess", 207 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 208 | ] 209 | } 210 | } 211 | } 212 | }, {"iamManagedPolicy": {deny: ["arn:aws:iam::aws:policy/AdministratorAccess"]}}, 1, done); 213 | }); 214 | it("two policies attached, deny only one by wildcard", function(done) { 215 | test({ 216 | "Resources": { 217 | "Test": { 218 | "Type": "AWS::IAM::Role", 219 | "Properties": { 220 | "ManagedPolicyArns": [ 221 | "arn:aws:iam::aws:policy/AdministratorAccess", 222 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 223 | ] 224 | } 225 | } 226 | } 227 | }, {"iamManagedPolicy": {deny: ["arn:aws:iam::aws:policy/*ReadOnlyAccess"]}}, 1, done); 228 | }); 229 | it("two ReadOnly policies attached, deny both by wildcard", function(done) { 230 | test({ 231 | "Resources": { 232 | "Test": { 233 | "Type": "AWS::IAM::Role", 234 | "Properties": { 235 | "ManagedPolicyArns": [ 236 | "arn:aws:iam::aws:policy/AmazonGlacierReadOnlyAccess", 237 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 238 | ] 239 | } 240 | } 241 | } 242 | }, {"iamManagedPolicy": {deny: ["arn:aws:iam::aws:policy/*ReadOnlyAccess"]}}, 2, done); 243 | }); 244 | }); 245 | }); 246 | describe("User", function() { 247 | describe("allow", function() { 248 | it("nothing attached, allow [*]", function(done) { 249 | test({ 250 | "Resources": { 251 | "Test": { 252 | "Type": "AWS::IAM::User", 253 | "Properties": { 254 | } 255 | } 256 | } 257 | }, {"iamManagedPolicy": {allow: ["*"]}}, 0, done); 258 | }); 259 | it("empty attached, allow [*]", function(done) { 260 | test({ 261 | "Resources": { 262 | "Test": { 263 | "Type": "AWS::IAM::User", 264 | "Properties": { 265 | "ManagedPolicyArns": [] 266 | } 267 | } 268 | } 269 | }, {"iamManagedPolicy": {allow: ["*"]}}, 0, done); 270 | }); 271 | it("one policy attached, allow [*]", function(done) { 272 | test({ 273 | "Resources": { 274 | "Test": { 275 | "Type": "AWS::IAM::User", 276 | "Properties": { 277 | "ManagedPolicyArns": ["arn:aws:iam::aws:policy/AdministratorAccess"] 278 | } 279 | } 280 | } 281 | }, {"iamManagedPolicy": {allow: ["*"]}}, 0, done); 282 | }); 283 | it("two policies attached, allow [*]", function(done) { 284 | test({ 285 | "Resources": { 286 | "Test": { 287 | "Type": "AWS::IAM::User", 288 | "Properties": { 289 | "ManagedPolicyArns": [ 290 | "arn:aws:iam::aws:policy/AdministratorAccess", 291 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 292 | ] 293 | } 294 | } 295 | } 296 | }, {"iamManagedPolicy": {allow: ["*"]}}, 0, done); 297 | }); 298 | it("two policies attached, allow only one by ARN", function(done) { 299 | test({ 300 | "Resources": { 301 | "Test": { 302 | "Type": "AWS::IAM::User", 303 | "Properties": { 304 | "ManagedPolicyArns": [ 305 | "arn:aws:iam::aws:policy/AdministratorAccess", 306 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 307 | ] 308 | } 309 | } 310 | } 311 | }, {"iamManagedPolicy": {allow: ["arn:aws:iam::aws:policy/AdministratorAccess"]}}, 1, done); 312 | }); 313 | it("two policies attached, allow only one by wildcard", function(done) { 314 | test({ 315 | "Resources": { 316 | "Test": { 317 | "Type": "AWS::IAM::User", 318 | "Properties": { 319 | "ManagedPolicyArns": [ 320 | "arn:aws:iam::aws:policy/AdministratorAccess", 321 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 322 | ] 323 | } 324 | } 325 | } 326 | }, {"iamManagedPolicy": {allow: ["arn:aws:iam::aws:policy/*ReadOnlyAccess"]}}, 1, done); 327 | }); 328 | it("two ReadOnly policies attached, allow both by wildcard", function(done) { 329 | test({ 330 | "Resources": { 331 | "Test": { 332 | "Type": "AWS::IAM::User", 333 | "Properties": { 334 | "ManagedPolicyArns": [ 335 | "arn:aws:iam::aws:policy/AmazonGlacierReadOnlyAccess", 336 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 337 | ] 338 | } 339 | } 340 | } 341 | }, {"iamManagedPolicy": {allow: ["arn:aws:iam::aws:policy/*ReadOnlyAccess"]}}, 0, done); 342 | }); 343 | }); 344 | describe("deny", function() { 345 | it("nothing attached, deny [*]", function(done) { 346 | test({ 347 | "Resources": { 348 | "Test": { 349 | "Type": "AWS::IAM::User", 350 | "Properties": { 351 | } 352 | } 353 | } 354 | }, {"iamManagedPolicy": {deny: ["*"]}}, 0, done); 355 | }); 356 | it("empty attached, deny [*]", function(done) { 357 | test({ 358 | "Resources": { 359 | "Test": { 360 | "Type": "AWS::IAM::User", 361 | "Properties": { 362 | "ManagedPolicyArns": [] 363 | } 364 | } 365 | } 366 | }, {"iamManagedPolicy": {deny: ["*"]}}, 0, done); 367 | }); 368 | it("one policy attached, deny [*]", function(done) { 369 | test({ 370 | "Resources": { 371 | "Test": { 372 | "Type": "AWS::IAM::User", 373 | "Properties": { 374 | "ManagedPolicyArns": ["arn:aws:iam::aws:policy/AdministratorAccess"] 375 | } 376 | } 377 | } 378 | }, {"iamManagedPolicy": {deny: ["*"]}}, 1, done); 379 | }); 380 | it("two policies attached, deny [*]", function(done) { 381 | test({ 382 | "Resources": { 383 | "Test": { 384 | "Type": "AWS::IAM::User", 385 | "Properties": { 386 | "ManagedPolicyArns": [ 387 | "arn:aws:iam::aws:policy/AdministratorAccess", 388 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 389 | ] 390 | } 391 | } 392 | } 393 | }, {"iamManagedPolicy": {deny: ["*"]}}, 2, done); 394 | }); 395 | it("two policies attached, deny only one by ARN", function(done) { 396 | test({ 397 | "Resources": { 398 | "Test": { 399 | "Type": "AWS::IAM::User", 400 | "Properties": { 401 | "ManagedPolicyArns": [ 402 | "arn:aws:iam::aws:policy/AdministratorAccess", 403 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 404 | ] 405 | } 406 | } 407 | } 408 | }, {"iamManagedPolicy": {deny: ["arn:aws:iam::aws:policy/AdministratorAccess"]}}, 1, done); 409 | }); 410 | it("two policies attached, deny only one by wildcard", function(done) { 411 | test({ 412 | "Resources": { 413 | "Test": { 414 | "Type": "AWS::IAM::User", 415 | "Properties": { 416 | "ManagedPolicyArns": [ 417 | "arn:aws:iam::aws:policy/AdministratorAccess", 418 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 419 | ] 420 | } 421 | } 422 | } 423 | }, {"iamManagedPolicy": {deny: ["arn:aws:iam::aws:policy/*ReadOnlyAccess"]}}, 1, done); 424 | }); 425 | it("two ReadOnly policies attached, deny both by wildcard", function(done) { 426 | test({ 427 | "Resources": { 428 | "Test": { 429 | "Type": "AWS::IAM::User", 430 | "Properties": { 431 | "ManagedPolicyArns": [ 432 | "arn:aws:iam::aws:policy/AmazonGlacierReadOnlyAccess", 433 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 434 | ] 435 | } 436 | } 437 | } 438 | }, {"iamManagedPolicy": {deny: ["arn:aws:iam::aws:policy/*ReadOnlyAccess"]}}, 2, done); 439 | }); 440 | }); 441 | }); 442 | describe("Group", function() { 443 | describe("allow", function() { 444 | it("nothing attached, allow [*]", function(done) { 445 | test({ 446 | "Resources": { 447 | "Test": { 448 | "Type": "AWS::IAM::Group", 449 | "Properties": { 450 | } 451 | } 452 | } 453 | }, {"iamManagedPolicy": {allow: ["*"]}}, 0, done); 454 | }); 455 | it("empty attached, allow [*]", function(done) { 456 | test({ 457 | "Resources": { 458 | "Test": { 459 | "Type": "AWS::IAM::Group", 460 | "Properties": { 461 | "ManagedPolicyArns": [] 462 | } 463 | } 464 | } 465 | }, {"iamManagedPolicy": {allow: ["*"]}}, 0, done); 466 | }); 467 | it("one policy attached, allow [*]", function(done) { 468 | test({ 469 | "Resources": { 470 | "Test": { 471 | "Type": "AWS::IAM::Group", 472 | "Properties": { 473 | "ManagedPolicyArns": ["arn:aws:iam::aws:policy/AdministratorAccess"] 474 | } 475 | } 476 | } 477 | }, {"iamManagedPolicy": {allow: ["*"]}}, 0, done); 478 | }); 479 | it("two policies attached, allow [*]", function(done) { 480 | test({ 481 | "Resources": { 482 | "Test": { 483 | "Type": "AWS::IAM::Group", 484 | "Properties": { 485 | "ManagedPolicyArns": [ 486 | "arn:aws:iam::aws:policy/AdministratorAccess", 487 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 488 | ] 489 | } 490 | } 491 | } 492 | }, {"iamManagedPolicy": {allow: ["*"]}}, 0, done); 493 | }); 494 | it("two policies attached, allow only one by ARN", function(done) { 495 | test({ 496 | "Resources": { 497 | "Test": { 498 | "Type": "AWS::IAM::Group", 499 | "Properties": { 500 | "ManagedPolicyArns": [ 501 | "arn:aws:iam::aws:policy/AdministratorAccess", 502 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 503 | ] 504 | } 505 | } 506 | } 507 | }, {"iamManagedPolicy": {allow: ["arn:aws:iam::aws:policy/AdministratorAccess"]}}, 1, done); 508 | }); 509 | it("two policies attached, allow only one by wildcard", function(done) { 510 | test({ 511 | "Resources": { 512 | "Test": { 513 | "Type": "AWS::IAM::Group", 514 | "Properties": { 515 | "ManagedPolicyArns": [ 516 | "arn:aws:iam::aws:policy/AdministratorAccess", 517 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 518 | ] 519 | } 520 | } 521 | } 522 | }, {"iamManagedPolicy": {allow: ["arn:aws:iam::aws:policy/*ReadOnlyAccess"]}}, 1, done); 523 | }); 524 | it("two ReadOnly policies attached, allow both by wildcard", function(done) { 525 | test({ 526 | "Resources": { 527 | "Test": { 528 | "Type": "AWS::IAM::Group", 529 | "Properties": { 530 | "ManagedPolicyArns": [ 531 | "arn:aws:iam::aws:policy/AmazonGlacierReadOnlyAccess", 532 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 533 | ] 534 | } 535 | } 536 | } 537 | }, {"iamManagedPolicy": {allow: ["arn:aws:iam::aws:policy/*ReadOnlyAccess"]}}, 0, done); 538 | }); 539 | }); 540 | describe("deny", function() { 541 | it("nothing attached, deny [*]", function(done) { 542 | test({ 543 | "Resources": { 544 | "Test": { 545 | "Type": "AWS::IAM::Group", 546 | "Properties": { 547 | } 548 | } 549 | } 550 | }, {"iamManagedPolicy": {deny: ["*"]}}, 0, done); 551 | }); 552 | it("empty attached, deny [*]", function(done) { 553 | test({ 554 | "Resources": { 555 | "Test": { 556 | "Type": "AWS::IAM::Group", 557 | "Properties": { 558 | "ManagedPolicyArns": [] 559 | } 560 | } 561 | } 562 | }, {"iamManagedPolicy": {deny: ["*"]}}, 0, done); 563 | }); 564 | it("one policy attached, deny [*]", function(done) { 565 | test({ 566 | "Resources": { 567 | "Test": { 568 | "Type": "AWS::IAM::Group", 569 | "Properties": { 570 | "ManagedPolicyArns": ["arn:aws:iam::aws:policy/AdministratorAccess"] 571 | } 572 | } 573 | } 574 | }, {"iamManagedPolicy": {deny: ["*"]}}, 1, done); 575 | }); 576 | it("two policies attached, deny [*]", function(done) { 577 | test({ 578 | "Resources": { 579 | "Test": { 580 | "Type": "AWS::IAM::Group", 581 | "Properties": { 582 | "ManagedPolicyArns": [ 583 | "arn:aws:iam::aws:policy/AdministratorAccess", 584 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 585 | ] 586 | } 587 | } 588 | } 589 | }, {"iamManagedPolicy": {deny: ["*"]}}, 2, done); 590 | }); 591 | it("two policies attached, deny only one by ARN", function(done) { 592 | test({ 593 | "Resources": { 594 | "Test": { 595 | "Type": "AWS::IAM::Group", 596 | "Properties": { 597 | "ManagedPolicyArns": [ 598 | "arn:aws:iam::aws:policy/AdministratorAccess", 599 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 600 | ] 601 | } 602 | } 603 | } 604 | }, {"iamManagedPolicy": {deny: ["arn:aws:iam::aws:policy/AdministratorAccess"]}}, 1, done); 605 | }); 606 | it("two policies attached, deny only one by wildcard", function(done) { 607 | test({ 608 | "Resources": { 609 | "Test": { 610 | "Type": "AWS::IAM::Group", 611 | "Properties": { 612 | "ManagedPolicyArns": [ 613 | "arn:aws:iam::aws:policy/AdministratorAccess", 614 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 615 | ] 616 | } 617 | } 618 | } 619 | }, {"iamManagedPolicy": {deny: ["arn:aws:iam::aws:policy/*ReadOnlyAccess"]}}, 1, done); 620 | }); 621 | it("two ReadOnly policies attached, deny both by wildcard", function(done) { 622 | test({ 623 | "Resources": { 624 | "Test": { 625 | "Type": "AWS::IAM::Group", 626 | "Properties": { 627 | "ManagedPolicyArns": [ 628 | "arn:aws:iam::aws:policy/AmazonGlacierReadOnlyAccess", 629 | "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess" 630 | ] 631 | } 632 | } 633 | } 634 | }, {"iamManagedPolicy": {deny: ["arn:aws:iam::aws:policy/*ReadOnlyAccess"]}}, 2, done); 635 | }); 636 | }); 637 | }); 638 | }); 639 | -------------------------------------------------------------------------------- /test/iamPolicy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var checker = require("../index.js"); 4 | var assert = require("assert-plus"); 5 | var iamPolicy = require("../check/iamPolicy.js"); 6 | 7 | function test(template, options, expectedFindings, done) { 8 | checker.checkTemplate(template, options, function(err, findings) { 9 | if (err) { 10 | throw err; 11 | } else { 12 | if (findings.length !== expectedFindings) { 13 | console.log("findings", findings); 14 | } 15 | assert.equal(findings.length, expectedFindings, "findings"); 16 | done(); 17 | } 18 | }); 19 | } 20 | 21 | function suite(templateJSON, done) { 22 | function wrap(policyDocument) { 23 | return JSON.parse(templateJSON.replace("\"$PolicyDocument\"", JSON.stringify(policyDocument))); 24 | } 25 | describe("Resource", function() { 26 | it("NotResource is not allowed", function(done) { 27 | test(wrap({ 28 | "Version": "2012-10-17", 29 | "Statement": [{ 30 | "Effect": "Allow", 31 | "Action": [ 32 | "s3:PutObject" 33 | ], 34 | "NotResource": "arn:aws:s3:::name-of-bucket" 35 | }] 36 | }), {"iamPolicy": {"allow": [{"action": "*", "resource": "arn:aws:s3:::*"}]}}, 1, done); 37 | }); 38 | it("string in template", function(done) { 39 | test(wrap({ 40 | "Version": "2012-10-17", 41 | "Statement": [{ 42 | "Effect": "Allow", 43 | "Action": [ 44 | "s3:PutObject" 45 | ], 46 | "Resource": "arn:aws:s3:::name-of-bucket" 47 | }] 48 | }), {"iamPolicy": {"allow": [{"action": "*", "resource": "arn:aws:s3:::*"}]}}, 0, done); 49 | }); 50 | it("array in template", function(done) { 51 | test(wrap({ 52 | "Version": "2012-10-17", 53 | "Statement": [{ 54 | "Effect": "Allow", 55 | "Action": [ 56 | "s3:PutObject" 57 | ], 58 | "Resource": [ 59 | "arn:aws:s3:::name-of-bucket" 60 | ] 61 | }] 62 | }), {"iamPolicy": {"allow": [{"action": "*", "resource": "arn:aws:s3:::*"}]}}, 0, done); 63 | }); 64 | it("string in action", function(done) { 65 | test(wrap({ 66 | "Version": "2012-10-17", 67 | "Statement": [{ 68 | "Effect": "Allow", 69 | "Action": "s3:PutObject", 70 | "Resource": "arn:aws:s3:::name-of-bucket" 71 | }] 72 | }), {"iamPolicy": {"allow": [{"action": "s3:PutObject", "resource": "*"}]}}, 0, done); 73 | }); 74 | it("array in action", function(done) { 75 | test(wrap({ 76 | "Version": "2012-10-17", 77 | "Statement": [{ 78 | "Effect": "Allow", 79 | "Action": "s3:PutObject", 80 | "Resource": "arn:aws:s3:::name-of-bucket" 81 | }] 82 | }), {"iamPolicy": {"allow": [{"action": ["s3:PutObject", "s3:DeleteObject"], "resource": "*"}]}}, 0, done); 83 | }); 84 | it("string in resource", function(done) { 85 | test(wrap({ 86 | "Version": "2012-10-17", 87 | "Statement": [{ 88 | "Effect": "Allow", 89 | "Action": "s3:PutObject", 90 | "Resource": "arn:aws:s3:::name-of-bucket" 91 | }] 92 | }), {"iamPolicy": {"allow": [{"action": "*", "resource": "arn:aws:s3:::*"}]}}, 0, done); 93 | }); 94 | it("array in resource", function(done) { 95 | test(wrap({ 96 | "Version": "2012-10-17", 97 | "Statement": [{ 98 | "Effect": "Allow", 99 | "Action": "s3:PutObject", 100 | "Resource": "arn:aws:s3:::name-of-bucket" 101 | }] 102 | }), {"iamPolicy": {"allow": [{"action": "*", "resource": ["arn:aws:s3:::*", "arn:aws:ec2:*:*"]}]}}, 0, done); 103 | }); 104 | describe("allow", function() { 105 | it("allow specific s3 bucket", function(done) { 106 | test(wrap({ 107 | "Version": "2012-10-17", 108 | "Statement": [{ 109 | "Effect": "Allow", 110 | "Action": [ 111 | "s3:PutObject" 112 | ], 113 | "Resource": "arn:aws:s3:::name-of-bucket" 114 | }] 115 | }), {"iamPolicy": {"allow": [{"action": "s3:*", "resource": "arn:aws:s3:::*"}]}}, 0, done); 116 | }); 117 | }); 118 | describe("explicit deny", function() { 119 | it("deny all s3 buckets", function(done) { 120 | test(wrap({ 121 | "Version": "2012-10-17", 122 | "Statement": [{ 123 | "Effect": "Allow", 124 | "Action": [ 125 | "s3:PutObject" 126 | ], 127 | "Resource": "arn:aws:s3:::name-of-bucket" 128 | }] 129 | }), {"iamPolicy": {"allow": ["*"], "deny": [{"action": "s3:*", "resource": "arn:aws:s3:::*"}]}}, 1, done); 130 | }); 131 | }); 132 | }); 133 | describe("Action", function() { 134 | it("NotAction is not allowed", function(done) { 135 | test(wrap({ 136 | "Version": "2012-10-17", 137 | "Statement": [{ 138 | "Effect": "Allow", 139 | "NotAction": [ 140 | "s3:PutObject" 141 | ], 142 | "Resource": "*" 143 | }] 144 | }), {"iamPolicy": {"allow": [{"action": "s3:PutObject", "resource": "*"}]}}, 1, done); 145 | }); 146 | it("string", function(done) { 147 | test(wrap({ 148 | "Version": "2012-10-17", 149 | "Statement": [{ 150 | "Effect": "Allow", 151 | "Action": "s3:PutObject", 152 | "Resource": "*" 153 | }] 154 | }), {"iamPolicy": {"allow": [{"action": "s3:PutObject", "resource": "*"}]}}, 0, done); 155 | }); 156 | it("array", function(done) { 157 | test(wrap({ 158 | "Version": "2012-10-17", 159 | "Statement": [{ 160 | "Effect": "Allow", 161 | "Action": [ 162 | "s3:PutObject" 163 | ], 164 | "Resource": "*" 165 | }] 166 | }), {"iamPolicy": {"allow": [{"action": "s3:PutObject", "resource": "*"}]}}, 0, done); 167 | }); 168 | describe("allow", function() { 169 | it("wildcard", function(done) { 170 | test(wrap({ 171 | "Version": "2012-10-17", 172 | "Statement": [{ 173 | "Effect": "Allow", 174 | "Action": [ 175 | "s3:PutObject" 176 | ], 177 | "Resource": "*" 178 | }] 179 | }), {"iamPolicy": {"allow": [{"action": "s3:*", "resource": "*"}]}}, 0, done); 180 | }); 181 | it("not allowed action in one statement with one action", function(done) { 182 | test(wrap({ 183 | "Version": "2012-10-17", 184 | "Statement": [{ 185 | "Effect": "Allow", 186 | "Action": [ 187 | "s3:PutObject" 188 | ], 189 | "Resource": "*" 190 | }] 191 | }), {"iamPolicy": {"allow": [{"action": "s3:GetObject", "resource": "*"}]}}, 1, done); 192 | }); 193 | it("allowed action in one statement with one action", function(done) { 194 | test(wrap({ 195 | "Version": "2012-10-17", 196 | "Statement": [{ 197 | "Effect": "Allow", 198 | "Action": [ 199 | "s3:GetObject" 200 | ], 201 | "Resource": "*" 202 | }] 203 | }), {"iamPolicy": {"allow": [{"action": "s3:GetObject", "resource": "*"}]}}, 0, done); 204 | }); 205 | it("allowed - but not used - action in one statement with one action", function(done) { 206 | test(wrap({ 207 | "Version": "2012-10-17", 208 | "Statement": [{ 209 | "Effect": "Allow", 210 | "Action": [ 211 | "s3:GetObject" 212 | ], 213 | "Resource": "*" 214 | }] 215 | }), {"iamPolicy": {"allow": [ 216 | {"action": "s3:GetObject", "resource": "*"}, 217 | {"action": "s3:PutObject", "resource": "*"} 218 | ]}}, 0, done); 219 | }); 220 | it("allowed actions in one statement with multiple actions", function(done) { 221 | test(wrap({ 222 | "Version": "2012-10-17", 223 | "Statement": [{ 224 | "Effect": "Allow", 225 | "Action": [ 226 | "s3:GetObject", 227 | "s3:PutObject" 228 | ], 229 | "Resource": "*" 230 | }] 231 | }), {"iamPolicy": {"allow": [ 232 | {"action": "s3:GetObject", "resource": "*"}, 233 | {"action": "s3:PutObject", "resource": "*"} 234 | ]}}, 0, done); 235 | }); 236 | it("allowed actions in multiple statements with one action", function(done) { 237 | test(wrap({ 238 | "Version": "2012-10-17", 239 | "Statement": [{ 240 | "Effect": "Allow", 241 | "Action": [ 242 | "s3:GetObject" 243 | ], 244 | "Resource": "*" 245 | }, { 246 | "Effect": "Allow", 247 | "Action": [ 248 | "s3:PutObject" 249 | ], 250 | "Resource": "*" 251 | }] 252 | }), {"iamPolicy": {"allow": [ 253 | {"action": "s3:GetObject", "resource": "*"}, 254 | {"action": "s3:PutObject", "resource": "*"} 255 | ]}}, 0, done); 256 | }); 257 | it("ignore Effect = Deny", function(done) { 258 | test(wrap({ 259 | "Version": "2012-10-17", 260 | "Statement": [{ 261 | "Effect": "Deny", 262 | "Action": [ 263 | "s3:GetObject" 264 | ], 265 | "Resource": "*" 266 | }] 267 | }), {"iamPolicy": {"allow": []}}, 0, done); 268 | }); 269 | }); 270 | describe("explicit deny", function() { 271 | it("empty", function(done) { 272 | test({ 273 | "Resources": { 274 | } 275 | }, {"iamPolicy": {}}, 0, done); 276 | }); 277 | it("nothing allowed", function(done) { 278 | test(wrap({ 279 | "Version": "2012-10-17", 280 | "Statement": [{ 281 | "Effect": "Allow", 282 | "Action": [ 283 | "s3:PutObject" 284 | ], 285 | "Resource": "*" 286 | }] 287 | }), {"iamPolicy": {}}, 1, done); 288 | }); 289 | it("allow wildcard does not match", function(done) { 290 | test(wrap({ 291 | "Version": "2012-10-17", 292 | "Statement": [{ 293 | "Effect": "Allow", 294 | "Action": [ 295 | "s3:PutObject" 296 | ], 297 | "Resource": "*" 298 | }] 299 | }), {"iamPolicy": {"allow": [{"action": "ec2:*", "resource": "*"}]}}, 1, done); 300 | }); 301 | it("allow wildcard does match", function(done) { 302 | test(wrap({ 303 | "Version": "2012-10-17", 304 | "Statement": [{ 305 | "Effect": "Allow", 306 | "Action": [ 307 | "s3:PutObject" 308 | ], 309 | "Resource": "*" 310 | }] 311 | }), {"iamPolicy": {"allow": [{"action": "s3:*", "resource": "*"}]}}, 0, done); 312 | }); 313 | }); 314 | describe("explicit deny", function() { 315 | it("wildcard", function(done) { 316 | test(wrap({ 317 | "Version": "2012-10-17", 318 | "Statement": [{ 319 | "Effect": "Allow", 320 | "Action": [ 321 | "s3:PutObject" 322 | ], 323 | "Resource": "*" 324 | }] 325 | }), {"iamPolicy": {"allow": ["*"], "deny": [{"action": "s3:*", "resource": "*"}]}}, 1, done); 326 | }); 327 | it("denied action in one statement with one action", function(done) { 328 | test(wrap({ 329 | "Version": "2012-10-17", 330 | "Statement": [{ 331 | "Effect": "Allow", 332 | "Action": [ 333 | "s3:GetObject" 334 | ], 335 | "Resource": "*" 336 | }] 337 | }), {"iamPolicy": {"allow": ["*"], "deny": [{"action": "s3:GetObject", "resource": "*"}]}}, 1, done); 338 | }); 339 | it("not denied action in one statement with one action", function(done) { 340 | test(wrap({ 341 | "Version": "2012-10-17", 342 | "Statement": [{ 343 | "Effect": "Allow", 344 | "Action": [ 345 | "s3:PutObject" 346 | ], 347 | "Resource": "*" 348 | }] 349 | }), {"iamPolicy": {"allow": ["*"], "deny": [{"action": "s3:GetObject", "resource": "*"}]}}, 0, done); 350 | }); 351 | it("ignore Effect := Deny", function(done) { 352 | test(wrap({ 353 | "Version": "2012-10-17", 354 | "Statement": [{ 355 | "Effect": "Deny", 356 | "Action": [ 357 | "s3:GetObject" 358 | ], 359 | "Resource": "*" 360 | }] 361 | }), {"iamPolicy": {"allow": ["*"], "deny": []}}, 0, done); 362 | }); 363 | }); 364 | }); 365 | } 366 | 367 | describe("iamPolicy", function() { 368 | it("empty", function(done) { 369 | test({}, {"iamPolicy": true}, 0, done); 370 | }); 371 | describe("cross", function() { 372 | it("undefined with undefined", function() { 373 | var pairs = iamPolicy.cross(undefined, undefined); 374 | assert.deepEqual(pairs, [{"action": "*", "resource": "*"}]); 375 | }); 376 | it("undefined with string", function() { 377 | var pairs = iamPolicy.cross(undefined, "r1"); 378 | assert.deepEqual(pairs, [{"action": "*", "resource": "r1"}]); 379 | }); 380 | it("string with undefined", function() { 381 | var pairs = iamPolicy.cross("a1", undefined); 382 | assert.deepEqual(pairs, [{"action": "a1", "resource": "*"}]); 383 | }); 384 | it("string with string", function() { 385 | var pairs = iamPolicy.cross("a1", "r1"); 386 | assert.deepEqual(pairs, [{"action": "a1", "resource": "r1"}]); 387 | }); 388 | it("string with strings", function() { 389 | var pairs = iamPolicy.cross("a1", ["r1", "r2"]); 390 | assert.deepEqual(pairs, [{"action": "a1", "resource": "r1"}, {"action": "a1", "resource": "r2"}]); 391 | }); 392 | it("strings with string", function() { 393 | var pairs = iamPolicy.cross(["a1", "a2"], "r1"); 394 | assert.deepEqual(pairs, [{"action": "a1", "resource": "r1"}, {"action": "a2", "resource": "r1"}]); 395 | }); 396 | it("strings with strings", function() { 397 | var pairs = iamPolicy.cross(["a1", "a2"], ["r1", "r2"]); 398 | assert.deepEqual(pairs, [{"action": "a1", "resource": "r1"}, {"action": "a1", "resource": "r2"}, {"action": "a2", "resource": "r1"}, {"action": "a2", "resource": "r2"}]); 399 | }); 400 | }); 401 | describe("ManagedPolicy", function(done) { 402 | suite(JSON.stringify({ 403 | "Resources": { 404 | "ManagedPolicy": { 405 | "Type": "AWS::IAM::ManagedPolicy", 406 | "Properties": { 407 | "PolicyDocument": "$PolicyDocument" 408 | } 409 | } 410 | } 411 | }), done); 412 | }); 413 | describe("Policy", function(done) { 414 | suite(JSON.stringify({ 415 | "Resources": { 416 | "ManagedPolicy": { 417 | "Type": "AWS::IAM::Policy", 418 | "Properties": { 419 | "PolicyDocument": "$PolicyDocument" 420 | } 421 | } 422 | } 423 | }), done); 424 | }); 425 | describe("User", function(done) { 426 | suite(JSON.stringify({ 427 | "Resources": { 428 | "User": { 429 | "Type": "AWS::IAM::User", 430 | "Properties": { 431 | "Policies": [{ 432 | "PolicyName": "test", 433 | "PolicyDocument": "$PolicyDocument" 434 | }] 435 | } 436 | } 437 | } 438 | }), done); 439 | }); 440 | describe("Group", function(done) { 441 | suite(JSON.stringify({ 442 | "Resources": { 443 | "Group": { 444 | "Type": "AWS::IAM::Group", 445 | "Properties": { 446 | "Policies": [{ 447 | "PolicyName": "test", 448 | "PolicyDocument": "$PolicyDocument" 449 | }] 450 | } 451 | } 452 | } 453 | }), done); 454 | }); 455 | describe("Role", function(done) { 456 | suite(JSON.stringify({ 457 | "Resources": { 458 | "Role": { 459 | "Type": "AWS::IAM::Role", 460 | "Properties": { 461 | "Policies": [{ 462 | "PolicyName": "s3", 463 | "PolicyDocument": "$PolicyDocument" 464 | }] 465 | } 466 | } 467 | } 468 | }), done); 469 | }); 470 | }); 471 | -------------------------------------------------------------------------------- /test/logicalID.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var checker = require("../index.js"); 4 | var assert = require("assert-plus"); 5 | 6 | function test(template, options, expectedFindings, done) { 7 | checker.checkTemplate(template, options, function(err, findings) { 8 | if (err) { 9 | throw err; 10 | } else { 11 | assert.equal(findings.length, expectedFindings, "findings"); 12 | done(); 13 | } 14 | }); 15 | } 16 | 17 | function testDefault(template, expectedFindings, done) { 18 | test(template, {"logicalID": true}, expectedFindings, done); 19 | } 20 | 21 | function testPascal(template, expectedFindings, done) { 22 | test(template, {"logicalID": {"case": "pascal"}}, expectedFindings, done); 23 | } 24 | 25 | function testCamel(template, expectedFindings, done) { 26 | test(template, {"logicalID": {"case": "camel"}}, expectedFindings, done); 27 | } 28 | 29 | describe("logicalID", function() { 30 | describe("default case", function() { 31 | var testMethod = testDefault; 32 | it("empty", function(done) { 33 | testMethod({ 34 | }, 0, done); 35 | }); 36 | it("parts", function(done) { 37 | testMethod({ 38 | "Parameters": { 39 | "res1": {} 40 | }, 41 | "Mappings": { 42 | "res2": {} 43 | }, 44 | "Conditions": { 45 | "res3": {} 46 | }, 47 | "Resources": { 48 | "res4": {} 49 | }, 50 | "Outputs": { 51 | "res5": {} 52 | } 53 | }, 5, done); 54 | }); 55 | it("empty string", function(done) { 56 | testMethod({ 57 | "Resources": { 58 | "": { 59 | } 60 | } 61 | }, 1, done); 62 | }); 63 | it("lower case", function(done) { 64 | testMethod({ 65 | "Resources": { 66 | "res": { 67 | } 68 | } 69 | }, 1, done); 70 | }); 71 | it("one char", function(done) { 72 | testMethod({ 73 | "Resources": { 74 | "R": { 75 | } 76 | } 77 | }, 0, done); 78 | }); 79 | it("only upper case", function(done) { 80 | testMethod({ 81 | "Resources": { 82 | "ABC": { 83 | } 84 | } 85 | }, 0, done); 86 | }); 87 | it("valid1", function(done) { 88 | testMethod({ 89 | "Resources": { 90 | "ValidPascal1Case": { 91 | } 92 | } 93 | }, 0, done); 94 | }); 95 | it("valid2", function(done) { 96 | testMethod({ 97 | "Resources": { 98 | "Valid": { 99 | } 100 | } 101 | }, 0, done); 102 | }); 103 | it("valid3", function(done) { 104 | testMethod({ 105 | "Resources": { 106 | "Valid1": { 107 | } 108 | } 109 | }, 0, done); 110 | }); 111 | }); 112 | describe("pascal case", function() { 113 | var testMethod = testPascal; 114 | it("empty", function(done) { 115 | testMethod({ 116 | }, 0, done); 117 | }); 118 | it("parts", function(done) { 119 | testMethod({ 120 | "Parameters": { 121 | "res1": {} 122 | }, 123 | "Mappings": { 124 | "res2": {} 125 | }, 126 | "Conditions": { 127 | "res3": {} 128 | }, 129 | "Resources": { 130 | "res4": {} 131 | }, 132 | "Outputs": { 133 | "res5": {} 134 | } 135 | }, 5, done); 136 | }); 137 | it("empty string", function(done) { 138 | testMethod({ 139 | "Resources": { 140 | "": { 141 | } 142 | } 143 | }, 1, done); 144 | }); 145 | it("lower case", function(done) { 146 | testMethod({ 147 | "Resources": { 148 | "res": { 149 | } 150 | } 151 | }, 1, done); 152 | }); 153 | it("one char", function(done) { 154 | testMethod({ 155 | "Resources": { 156 | "R": { 157 | } 158 | } 159 | }, 0, done); 160 | }); 161 | it("only upper case", function(done) { 162 | testMethod({ 163 | "Resources": { 164 | "ABC": { 165 | } 166 | } 167 | }, 0, done); 168 | }); 169 | it("valid1", function(done) { 170 | testMethod({ 171 | "Resources": { 172 | "ValidPascal1Case": { 173 | } 174 | } 175 | }, 0, done); 176 | }); 177 | it("valid2", function(done) { 178 | testMethod({ 179 | "Resources": { 180 | "Valid": { 181 | } 182 | } 183 | }, 0, done); 184 | }); 185 | it("valid3", function(done) { 186 | testMethod({ 187 | "Resources": { 188 | "Valid1": { 189 | } 190 | } 191 | }, 0, done); 192 | }); 193 | }); 194 | describe("camel case", function() { 195 | var testMethod = testCamel; 196 | it("empty", function(done) { 197 | testMethod({ 198 | }, 0, done); 199 | }); 200 | it("parts", function(done) { 201 | testMethod({ 202 | "Parameters": { 203 | "Res1": {} 204 | }, 205 | "Mappings": { 206 | "Res2": {} 207 | }, 208 | "Conditions": { 209 | "Res3": {} 210 | }, 211 | "Resources": { 212 | "Res4": {} 213 | }, 214 | "Outputs": { 215 | "Res5": {} 216 | } 217 | }, 5, done); 218 | }); 219 | it("empty string", function(done) { 220 | testMethod({ 221 | "Resources": { 222 | "": { 223 | } 224 | } 225 | }, 1, done); 226 | }); 227 | it("upper case", function(done) { 228 | testMethod({ 229 | "Resources": { 230 | "Res": { 231 | } 232 | } 233 | }, 1, done); 234 | }); 235 | it("one char", function(done) { 236 | testMethod({ 237 | "Resources": { 238 | "r": { 239 | } 240 | } 241 | }, 0, done); 242 | }); 243 | it("only upper case", function(done) { 244 | testMethod({ 245 | "Resources": { 246 | "ABC": { 247 | } 248 | } 249 | }, 1, done); 250 | }); 251 | it("valid1", function(done) { 252 | testMethod({ 253 | "Resources": { 254 | "validCamel1Case": { 255 | } 256 | } 257 | }, 0, done); 258 | }); 259 | it("valid2", function(done) { 260 | testMethod({ 261 | "Resources": { 262 | "valid": { 263 | } 264 | } 265 | }, 0, done); 266 | }); 267 | it("valid3", function(done) { 268 | testMethod({ 269 | "Resources": { 270 | "valid1": { 271 | } 272 | } 273 | }, 0, done); 274 | }); 275 | }); 276 | }); 277 | -------------------------------------------------------------------------------- /test/privateIpRange.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var privateIpRange = require("../lib/privateIpRange.js"); 4 | var assert = require("assert-plus"); 5 | 6 | describe("privateIpRange", function() { 7 | describe("10.0.0.0/8", function() { 8 | it("full", function() { 9 | assert.equal(privateIpRange("10.0.0.0/8"), true); 10 | }); 11 | it("range inside", function() { 12 | assert.equal(privateIpRange("10.0.0.0/16"), true); 13 | }); 14 | it("ip inside", function() { 15 | assert.equal(privateIpRange("10.0.0.10"), true); 16 | }); 17 | }); 18 | describe("172.16.0.0/12", function() { 19 | it("full", function() { 20 | assert.equal(privateIpRange("172.16.0.0/12"), true); 21 | }); 22 | it("range inside", function() { 23 | assert.equal(privateIpRange("172.16.0.0/24"), true); 24 | }); 25 | it("ip inside", function() { 26 | assert.equal(privateIpRange("172.16.0.10"), true); 27 | }); 28 | }); 29 | describe("192.168.0.0/16", function() { 30 | it("full", function() { 31 | assert.equal(privateIpRange("192.168.0.0/16"), true); 32 | }); 33 | it("range inside", function() { 34 | assert.equal(privateIpRange("192.168.0.0/24"), true); 35 | }); 36 | it("ip inside", function() { 37 | assert.equal(privateIpRange("192.168.0.10"), true); 38 | }); 39 | }); 40 | describe("public", function() { 41 | it("range", function() { 42 | assert.equal(privateIpRange("52.95.60.0/24"), false); 43 | }); 44 | it("ip outside", function() { 45 | assert.equal(privateIpRange("88.128.80.117"), false); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/resourceType.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var checker = require("../index.js"); 4 | var assert = require("assert-plus"); 5 | // TODO test wildcard 6 | function test(template, options, expectedFindings, done) { 7 | checker.checkTemplate(template, options, function(err, findings) { 8 | if (err) { 9 | throw err; 10 | } else { 11 | assert.equal(findings.length, expectedFindings, "findings"); 12 | done(); 13 | } 14 | }); 15 | } 16 | 17 | describe("resourceType", function() { 18 | describe("implicit deny", function() { 19 | it("empty", function(done) { 20 | test({ 21 | "Resources": { 22 | } 23 | }, { 24 | "resourceType": { 25 | } 26 | }, 0, done); 27 | }); 28 | it("nothing allowed", function(done) { 29 | test({ 30 | "Resources": { 31 | "VPC": { 32 | "Type": "AWS::EC2::InternetGateway", 33 | "Properties": { 34 | } 35 | } 36 | } 37 | }, { 38 | "resourceType": { 39 | } 40 | }, 1, done); 41 | }); 42 | it("allow wildcard does not match", function(done) { 43 | test({ 44 | "Resources": { 45 | "VPC": { 46 | "Type": "AWS::EC2::InternetGateway", 47 | "Properties": { 48 | } 49 | } 50 | } 51 | }, { 52 | "resourceType": { 53 | "allow": ["AWS::IAM::*"] 54 | } 55 | }, 1, done); 56 | }); 57 | it("allow wildcard does match", function(done) { 58 | test({ 59 | "Resources": { 60 | "VPC": { 61 | "Type": "AWS::EC2::InternetGateway", 62 | "Properties": { 63 | } 64 | } 65 | } 66 | }, { 67 | "resourceType": { 68 | "allow": ["AWS::EC2::*"] 69 | } 70 | }, 0, done); 71 | }); 72 | }); 73 | describe("explicit deny", function() { 74 | it("empty", function(done) { 75 | test({ 76 | "Resources": { 77 | } 78 | }, { 79 | "resourceType": { 80 | "allow": ["*"], 81 | "deny": ["AWS::EC2::VPC"] 82 | } 83 | }, 0, done); 84 | }); 85 | it("no hit", function(done) { 86 | test({ 87 | "Resources": { 88 | "VPC": { 89 | "Type": "AWS::EC2::InternetGateway", 90 | "Properties": { 91 | } 92 | } 93 | } 94 | }, { 95 | "resourceType": { 96 | "allow": ["*"], 97 | "deny": ["AWS::EC2::VPC"] 98 | } 99 | }, 0, done); 100 | }); 101 | it("hit", function(done) { 102 | test({ 103 | "Resources": { 104 | "VPC": { 105 | "Type": "AWS::EC2::VPC", 106 | "Properties": { 107 | } 108 | } 109 | } 110 | }, { 111 | "resourceType": { 112 | "allow": ["*"], 113 | "deny": ["AWS::EC2::VPC"] 114 | } 115 | }, 1, done); 116 | }); 117 | it("no hit by wildcard", function(done) { 118 | test({ 119 | "Resources": { 120 | "VPC": { 121 | "Type": "AWS::EC2::InternetGateway", 122 | "Properties": { 123 | } 124 | } 125 | } 126 | }, { 127 | "resourceType": { 128 | "allow": ["*"], 129 | "deny": ["AWS::IAM::*"] 130 | } 131 | }, 0, done); 132 | }); 133 | it("hit by wildcard", function(done) { 134 | test({ 135 | "Resources": { 136 | "VPC": { 137 | "Type": "AWS::EC2::VPC", 138 | "Properties": { 139 | } 140 | } 141 | } 142 | }, { 143 | "resourceType": { 144 | "allow": ["*"], 145 | "deny": ["*"] 146 | } 147 | }, 1, done); 148 | }); 149 | }); 150 | describe("allow", function() { 151 | it("empty", function(done) { 152 | test({ 153 | "Resources": { 154 | } 155 | }, { 156 | "resourceType": { 157 | "allow": ["AWS::EC2::VPC"] 158 | } 159 | }, 0, done); 160 | }); 161 | it("hit", function(done) { 162 | test({ 163 | "Resources": { 164 | "VPC": { 165 | "Type": "AWS::EC2::InternetGateway", 166 | "Properties": { 167 | } 168 | } 169 | } 170 | }, { 171 | "resourceType": { 172 | "allow": ["AWS::EC2::VPC"] 173 | } 174 | }, 1, done); 175 | }); 176 | it("no hit", function(done) { 177 | test({ 178 | "Resources": { 179 | "VPC": { 180 | "Type": "AWS::EC2::VPC", 181 | "Properties": { 182 | } 183 | } 184 | } 185 | }, { 186 | "resourceType": { 187 | "allow": ["AWS::EC2::VPC"] 188 | } 189 | }, 0, done); 190 | }); 191 | it("hit by wildcard", function(done) { 192 | test({ 193 | "Resources": { 194 | "VPC": { 195 | "Type": "AWS::EC2::InternetGateway", 196 | "Properties": { 197 | } 198 | } 199 | } 200 | }, { 201 | "resourceType": { 202 | "allow": ["AWS::IAM::*"] 203 | } 204 | }, 1, done); 205 | }); 206 | it("no hit by wildcard", function(done) { 207 | test({ 208 | "Resources": { 209 | "VPC": { 210 | "Type": "AWS::EC2::VPC", 211 | "Properties": { 212 | } 213 | } 214 | } 215 | }, { 216 | "resourceType": { 217 | "allow": ["*"] 218 | } 219 | }, 0, done); 220 | }); 221 | }); 222 | }); 223 | -------------------------------------------------------------------------------- /test/securityGroupInbound.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var checker = require("../index.js"); 4 | var assert = require("assert-plus"); 5 | 6 | function test(template, options, expectedFindings, done) { 7 | checker.checkTemplate(template, options, function(err, findings) { 8 | if (err) { 9 | throw err; 10 | } else { 11 | assert.equal(findings.length, expectedFindings, "findings"); 12 | done(); 13 | } 14 | }); 15 | } 16 | 17 | describe("securityGroupInbound", function() { 18 | it("empty", function(done) { 19 | test({ 20 | 21 | }, {"securityGroupInbound": true}, 0, done); 22 | }); 23 | it("secure AutoScaling + LoadBalancer setup", function(done) { 24 | test({ 25 | "Resources": { 26 | "SGServer": { 27 | "Type": "AWS::EC2::SecurityGroup", 28 | "Properties": { 29 | "SecurityGroupIngress": [{ 30 | "FromPort": 80, 31 | "ToPort": 80, 32 | "IpProtocol": "tcp", 33 | "SourceSecurityGroupId": {"Ref": "SGLoadBalancer"} 34 | }] 35 | } 36 | }, 37 | "SGLoadBalancer": { 38 | "Type": "AWS::EC2::SecurityGroup", 39 | "Properties": { 40 | "SecurityGroupIngress": [{ 41 | "FromPort": 80, 42 | "ToPort": 80, 43 | "IpProtocol": "tcp", 44 | "CidrIp": "0.0.0.0/0" 45 | }] 46 | } 47 | }, 48 | "LoadBalancer": { 49 | "Type": "AWS::ElasticLoadBalancing::LoadBalancer", 50 | "Properties": { 51 | "SecurityGroups": [{"Ref": "SGLoadBalancer"}] 52 | } 53 | }, 54 | "AutoScalingGroup": { 55 | "Type": "AWS::AutoScaling::AutoScalingGroup", 56 | "Properties": { 57 | "LaunchConfigurationName": {"Ref": "LaunchConfiguration"}, 58 | "LoadBalancerNames": [{"Ref": "LoadBalancer"}] 59 | } 60 | }, 61 | "LaunchConfiguration": { 62 | "Type": "AWS::AutoScaling::LaunchConfiguration", 63 | "Properties": { 64 | "SecurityGroups": [{"Ref": "SGServer"}] 65 | } 66 | } 67 | } 68 | }, {"securityGroupInbound": true}, 0, done); 69 | }); 70 | it("secure AutoScaling + LoadBalancer + RDS instance setup", function(done) { 71 | test({ 72 | "Resources": { 73 | "SGDatabase": { 74 | "Type": "AWS::EC2::SecurityGroup", 75 | "Properties": { 76 | "SecurityGroupIngress": [{ 77 | "FromPort": 3306, 78 | "ToPort": 3306, 79 | "IpProtocol": "tcp", 80 | "SourceSecurityGroupId": {"Ref": "SGServer"} 81 | }] 82 | } 83 | }, 84 | "SGServer": { 85 | "Type": "AWS::EC2::SecurityGroup", 86 | "Properties": { 87 | "SecurityGroupIngress": [{ 88 | "FromPort": 80, 89 | "ToPort": 80, 90 | "IpProtocol": "tcp", 91 | "SourceSecurityGroupId": {"Ref": "SGLoadBalancer"} 92 | }] 93 | } 94 | }, 95 | "SGLoadBalancer": { 96 | "Type": "AWS::EC2::SecurityGroup", 97 | "Properties": { 98 | "SecurityGroupIngress": [{ 99 | "FromPort": 80, 100 | "ToPort": 80, 101 | "IpProtocol": "tcp", 102 | "CidrIp": "0.0.0.0/0" 103 | }] 104 | } 105 | }, 106 | "LoadBalancer": { 107 | "Type": "AWS::ElasticLoadBalancing::LoadBalancer", 108 | "Properties": { 109 | "SecurityGroups": [{"Ref": "SGLoadBalancer"}] 110 | } 111 | }, 112 | "AutoScalingGroup": { 113 | "Type": "AWS::AutoScaling::AutoScalingGroup", 114 | "Properties": { 115 | "LaunchConfigurationName": {"Ref": "LaunchConfiguration"}, 116 | "LoadBalancerNames": [{"Ref": "LoadBalancer"}] 117 | } 118 | }, 119 | "LaunchConfiguration": { 120 | "Type": "AWS::AutoScaling::LaunchConfiguration", 121 | "Properties": { 122 | "SecurityGroups": [{"Ref": "SGServer"}] 123 | } 124 | }, 125 | "Database": { 126 | "Type": "AWS::RDS::DBInstance", 127 | "Properties": { 128 | "VPCSecurityGroups": [{"Ref": "SGDatabase"}] 129 | } 130 | } 131 | } 132 | }, {"securityGroupInbound": true}, 0, done); 133 | }); 134 | it("insecure AutoScaling + LoadBalancer setup", function(done) { 135 | test({ 136 | "Resources": { 137 | "SGServer": { 138 | "Type": "AWS::EC2::SecurityGroup", 139 | "Properties": { 140 | "SecurityGroupIngress": [{ 141 | "FromPort": 80, 142 | "ToPort": 80, 143 | "IpProtocol": "tcp", 144 | "CidrIp": "0.0.0.0/0" 145 | }] 146 | } 147 | }, 148 | "SGLoadBalancer": { 149 | "Type": "AWS::EC2::SecurityGroup", 150 | "Properties": { 151 | "SecurityGroupIngress": [{ 152 | "FromPort": 80, 153 | "ToPort": 80, 154 | "IpProtocol": "tcp", 155 | "CidrIp": "0.0.0.0/0" 156 | }] 157 | } 158 | }, 159 | "LoadBalancer": { 160 | "Type": "AWS::ElasticLoadBalancing::LoadBalancer", 161 | "Properties": { 162 | "SecurityGroups": [{"Ref": "SGLoadBalancer"}] 163 | } 164 | }, 165 | "AutoScalingGroup": { 166 | "Type": "AWS::AutoScaling::AutoScalingGroup", 167 | "Properties": { 168 | "LaunchConfigurationName": {"Ref": "LaunchConfiguration"}, 169 | "LoadBalancerNames": [{"Ref": "LoadBalancer"}] 170 | } 171 | }, 172 | "LaunchConfiguration": { 173 | "Type": "AWS::AutoScaling::LaunchConfiguration", 174 | "Properties": { 175 | "SecurityGroups": [{"Ref": "SGServer"}] 176 | } 177 | } 178 | } 179 | }, {"securityGroupInbound": true}, 1, done); 180 | }); 181 | it("secure internal LoadBalancer setup", function(done) { 182 | test({ 183 | "Resources": { 184 | "SGLoadBalancer": { 185 | "Type": "AWS::EC2::SecurityGroup", 186 | "Properties": { 187 | "SecurityGroupIngress": [{ 188 | "FromPort": 80, 189 | "ToPort": 80, 190 | "IpProtocol": "tcp", 191 | "CidrIp": "10.0.0.0/16" 192 | }] 193 | } 194 | }, 195 | "LoadBalancer": { 196 | "Type": "AWS::ElasticLoadBalancing::LoadBalancer", 197 | "Properties": { 198 | "SecurityGroups": [{"Ref": "SGLoadBalancer"}], 199 | "Scheme": "internal" 200 | } 201 | } 202 | } 203 | }, {"securityGroupInbound": true}, 0, done); 204 | }); 205 | it("insecure internal LoadBalancer setup", function(done) { 206 | test({ 207 | "Resources": { 208 | "SGLoadBalancer": { 209 | "Type": "AWS::EC2::SecurityGroup", 210 | "Properties": { 211 | "SecurityGroupIngress": [{ 212 | "FromPort": 80, 213 | "ToPort": 80, 214 | "IpProtocol": "tcp", 215 | "CidrIp": "0.0.0.0/0" 216 | }] 217 | } 218 | }, 219 | "LoadBalancer": { 220 | "Type": "AWS::ElasticLoadBalancing::LoadBalancer", 221 | "Properties": { 222 | "SecurityGroups": [{"Ref": "SGLoadBalancer"}], 223 | "Scheme": "internal" 224 | } 225 | } 226 | } 227 | }, {"securityGroupInbound": true}, 1, done); 228 | }); 229 | it("secure RDS instance setup", function(done) { 230 | test({ 231 | "Resources": { 232 | "SGDatabase": { 233 | "Type": "AWS::EC2::SecurityGroup", 234 | "Properties": { 235 | "SecurityGroupIngress": [{ 236 | "FromPort": 3306, 237 | "ToPort": 3306, 238 | "IpProtocol": "tcp", 239 | "CidrIp": "10.0.0.0/16" 240 | }] 241 | } 242 | }, 243 | "Database": { 244 | "Type": "AWS::RDS::DBInstance", 245 | "Properties": { 246 | "VPCSecurityGroups": [{"Ref": "SGDatabase"}] 247 | } 248 | } 249 | } 250 | }, {"securityGroupInbound": true}, 0, done); 251 | }); 252 | it("insecure RDS instance setup", function(done) { 253 | test({ 254 | "Resources": { 255 | "SGDatabase": { 256 | "Type": "AWS::EC2::SecurityGroup", 257 | "Properties": { 258 | "SecurityGroupIngress": [{ 259 | "FromPort": 3306, 260 | "ToPort": 3306, 261 | "IpProtocol": "tcp", 262 | "CidrIp": "0.0.0.0/0" 263 | }] 264 | } 265 | }, 266 | "Database": { 267 | "Type": "AWS::RDS::DBInstance", 268 | "Properties": { 269 | "VPCSecurityGroups": [{"Ref": "SGDatabase"}] 270 | } 271 | } 272 | } 273 | }, {"securityGroupInbound": true}, 1, done); 274 | }); 275 | it("secure RDS instance setup with external security group", function(done) { 276 | test({ 277 | "Parameters": { 278 | "SGDatabase": { 279 | "Type": "AWS::EC2::SecurityGroup::Id", 280 | } 281 | }, 282 | "Resources": { 283 | "Database": { 284 | "Type": "AWS::RDS::DBInstance", 285 | "Properties": { 286 | "VPCSecurityGroups": [{"Ref": "SGDatabase"}] 287 | } 288 | } 289 | } 290 | }, {"securityGroupInbound": true}, 0, done); 291 | }); 292 | it("secure RDS instance setup with external security group as source", function(done) { 293 | test({ 294 | "Parameters": { 295 | "SGDatabaseClient": { 296 | "Type": "AWS::EC2::SecurityGroup::Id", 297 | } 298 | }, 299 | "Resources": { 300 | "SGDatabase": { 301 | "Type": "AWS::EC2::SecurityGroup", 302 | "Properties": { 303 | "SecurityGroupIngress": [{ 304 | "FromPort": 3306, 305 | "ToPort": 3306, 306 | "IpProtocol": "tcp", 307 | "SourceSecurityGroupId": {"Ref": "SGDatabaseClient"} 308 | }] 309 | } 310 | }, 311 | "Database": { 312 | "Type": "AWS::RDS::DBInstance", 313 | "Properties": { 314 | "VPCSecurityGroups": [{"Ref": "SGDatabase"}] 315 | } 316 | } 317 | } 318 | }, {"securityGroupInbound": true}, 0, done); 319 | }); 320 | }); 321 | -------------------------------------------------------------------------------- /test/template.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var checker = require("../index.js"); 4 | var assert = require("assert-plus"); 5 | 6 | describe("templates", function() { 7 | describe("missing check", function() { 8 | it("check", function(done) { 9 | checker.checkFile("./test/templates/template1.json", { 10 | "missing": true 11 | }, function(err, findings) { 12 | if (err) { 13 | done(); 14 | } else { 15 | assert.fail(); 16 | } 17 | }); 18 | }); 19 | }); 20 | describe("template0", function() { 21 | it("check", function(done) { 22 | checker.checkFile("./test/templates/template0.json", { 23 | "logicalID": true, 24 | "securityGroupInbound": true 25 | }, function(err, findings) { 26 | if (err) { 27 | throw err; 28 | } else { 29 | assert.equal(findings.length, 0, "findings"); 30 | done(); 31 | } 32 | }); 33 | }); 34 | }); 35 | describe("template1", function() { 36 | it("check", function(done) { 37 | checker.checkFile("./test/templates/template1.json", { 38 | "logicalID": true, 39 | "securityGroupInbound": true 40 | }, function(err, findings) { 41 | if (err) { 42 | throw err; 43 | } else { 44 | assert.equal(findings.length, 0, "findings"); 45 | done(); 46 | } 47 | }); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/templates/template0.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "0", 4 | "Parameters": { 5 | }, 6 | "Mappings": { 7 | }, 8 | "Conditions": { 9 | }, 10 | "Resources": { 11 | }, 12 | "Outputs": { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/templates/template1.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "1", 4 | "Parameters": { 5 | "KeyName": { 6 | "Description": "Key Pair name", 7 | "Type": "AWS::EC2::KeyPair::KeyName", 8 | "Default": "mykey" 9 | } 10 | }, 11 | "Mappings": { 12 | "EC2RegionMap": { 13 | "ap-northeast-1": {"AmazonLinuxAMIHVMEBSBacked64bit": "ami-cbf90ecb"}, 14 | "ap-southeast-1": {"AmazonLinuxAMIHVMEBSBacked64bit": "ami-68d8e93a"}, 15 | "ap-southeast-2": {"AmazonLinuxAMIHVMEBSBacked64bit": "ami-fd9cecc7"}, 16 | "eu-central-1": {"AmazonLinuxAMIHVMEBSBacked64bit": "ami-a8221fb5"}, 17 | "eu-west-1": {"AmazonLinuxAMIHVMEBSBacked64bit": "ami-a10897d6"}, 18 | "sa-east-1": {"AmazonLinuxAMIHVMEBSBacked64bit": "ami-b52890a8"}, 19 | "us-east-1": {"AmazonLinuxAMIHVMEBSBacked64bit": "ami-1ecae776"}, 20 | "us-west-1": {"AmazonLinuxAMIHVMEBSBacked64bit": "ami-d114f295"}, 21 | "us-west-2": {"AmazonLinuxAMIHVMEBSBacked64bit": "ami-e7527ed7"} 22 | } 23 | }, 24 | "Resources": { 25 | "VPC": { 26 | "Type": "AWS::EC2::VPC", 27 | "Properties": { 28 | "CidrBlock": "172.31.0.0/16", 29 | "EnableDnsHostnames": "true" 30 | } 31 | }, 32 | "InternetGateway": { 33 | "Type": "AWS::EC2::InternetGateway", 34 | "Properties": { 35 | } 36 | }, 37 | "VPCGatewayAttachment": { 38 | "Type": "AWS::EC2::VPCGatewayAttachment", 39 | "Properties": { 40 | "VpcId": {"Ref": "VPC"}, 41 | "InternetGatewayId": {"Ref": "InternetGateway"} 42 | } 43 | }, 44 | "SubnetA": { 45 | "Type": "AWS::EC2::Subnet", 46 | "Properties": { 47 | "AvailabilityZone": {"Fn::Select": ["0", {"Fn::GetAZs": ""}]}, 48 | "CidrBlock": "172.31.38.0/24", 49 | "VpcId": {"Ref": "VPC"} 50 | } 51 | }, 52 | "SubnetB": { 53 | "Type": "AWS::EC2::Subnet", 54 | "Properties": { 55 | "AvailabilityZone": {"Fn::Select": ["1", {"Fn::GetAZs": ""}]}, 56 | "CidrBlock": "172.31.37.0/24", 57 | "VpcId": {"Ref": "VPC"} 58 | } 59 | }, 60 | "RouteTable": { 61 | "Type": "AWS::EC2::RouteTable", 62 | "Properties": { 63 | "VpcId": {"Ref": "VPC"} 64 | } 65 | }, 66 | "RouteTableAssociationA": { 67 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 68 | "Properties": { 69 | "SubnetId": {"Ref": "SubnetA"}, 70 | "RouteTableId": {"Ref": "RouteTable"} 71 | } 72 | }, 73 | "RouteTableAssociationB": { 74 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 75 | "Properties": { 76 | "SubnetId": {"Ref": "SubnetB"}, 77 | "RouteTableId": {"Ref": "RouteTable"} 78 | } 79 | }, 80 | "RoutePublicNATToInternet": { 81 | "Type": "AWS::EC2::Route", 82 | "Properties": { 83 | "RouteTableId": {"Ref": "RouteTable"}, 84 | "DestinationCidrBlock": "0.0.0.0/0", 85 | "GatewayId": {"Ref": "InternetGateway"} 86 | }, 87 | "DependsOn": "VPCGatewayAttachment" 88 | }, 89 | "NetworkAcl": { 90 | "Type": "AWS::EC2::NetworkAcl", 91 | "Properties": { 92 | "VpcId": {"Ref": "VPC"} 93 | } 94 | }, 95 | "SubnetNetworkAclAssociationA": { 96 | "Type": "AWS::EC2::SubnetNetworkAclAssociation", 97 | "Properties": { 98 | "SubnetId": {"Ref": "SubnetA"}, 99 | "NetworkAclId": {"Ref": "NetworkAcl"} 100 | } 101 | }, 102 | "SubnetNetworkAclAssociationB": { 103 | "Type": "AWS::EC2::SubnetNetworkAclAssociation", 104 | "Properties": { 105 | "SubnetId": {"Ref": "SubnetB"}, 106 | "NetworkAclId": {"Ref": "NetworkAcl"} 107 | } 108 | }, 109 | "NetworkAclEntryIngress": { 110 | "Type": "AWS::EC2::NetworkAclEntry", 111 | "Properties": { 112 | "NetworkAclId": {"Ref": "NetworkAcl"}, 113 | "RuleNumber": "100", 114 | "Protocol": "-1", 115 | "RuleAction": "allow", 116 | "Egress": "false", 117 | "CidrBlock": "0.0.0.0/0" 118 | } 119 | }, 120 | "NetworkAclEntryEgress": { 121 | "Type": "AWS::EC2::NetworkAclEntry", 122 | "Properties": { 123 | "NetworkAclId": {"Ref": "NetworkAcl"}, 124 | "RuleNumber": "100", 125 | "Protocol": "-1", 126 | "RuleAction": "allow", 127 | "Egress": "true", 128 | "CidrBlock": "0.0.0.0/0" 129 | } 130 | }, 131 | "LoadBalancer": { 132 | "Type": "AWS::ElasticLoadBalancing::LoadBalancer", 133 | "Properties": { 134 | "Subnets": [{"Ref": "SubnetA"}, {"Ref": "SubnetB"}], 135 | "LoadBalancerName": "awsinaction-elb", 136 | "Listeners": [{ 137 | "InstancePort": "80", 138 | "InstanceProtocol": "HTTP", 139 | "LoadBalancerPort": "80", 140 | "Protocol": "HTTP" 141 | }], 142 | "HealthCheck": { 143 | "HealthyThreshold": "2", 144 | "Interval": "5", 145 | "Target": "TCP:80", 146 | "Timeout": "3", 147 | "UnhealthyThreshold": "2" 148 | }, 149 | "SecurityGroups": [{"Ref": "LoadBalancerSecurityGroup"}], 150 | "Scheme": "internet-facing" 151 | } 152 | }, 153 | "LoadBalancerSecurityGroup": { 154 | "Type": "AWS::EC2::SecurityGroup", 155 | "Properties": { 156 | "GroupDescription": "awsinaction-elb-sg", 157 | "VpcId": {"Ref": "VPC"}, 158 | "SecurityGroupIngress": [{ 159 | "CidrIp": "0.0.0.0/0", 160 | "FromPort": 80, 161 | "IpProtocol": "tcp", 162 | "ToPort": 80 163 | }] 164 | } 165 | }, 166 | "WebServerSecurityGroup": { 167 | "Type": "AWS::EC2::SecurityGroup", 168 | "Properties": { 169 | "GroupDescription": "awsinaction-sg", 170 | "VpcId": {"Ref": "VPC"}, 171 | "SecurityGroupIngress": [{ 172 | "FromPort": 80, 173 | "IpProtocol": "tcp", 174 | "SourceSecurityGroupId": {"Ref": "LoadBalancerSecurityGroup"}, 175 | "ToPort": 80 176 | }] 177 | } 178 | }, 179 | "DatabaseSecurityGroup": { 180 | "Type": "AWS::EC2::SecurityGroup", 181 | "Properties": { 182 | "GroupDescription": "awsinaction-db-sg", 183 | "VpcId": {"Ref": "VPC"}, 184 | "SecurityGroupIngress": [{ 185 | "IpProtocol": "tcp", 186 | "FromPort": "3306", 187 | "ToPort": "3306", 188 | "SourceSecurityGroupId": {"Ref": "WebServerSecurityGroup"} 189 | }] 190 | } 191 | }, 192 | "Database": { 193 | "Type": "AWS::RDS::DBInstance", 194 | "Properties": { 195 | "AllocatedStorage": "5", 196 | "BackupRetentionPeriod": "0", 197 | "DBInstanceClass": "db.t2.micro", 198 | "DBInstanceIdentifier": "awsinaction-db", 199 | "DBName": "wordpress", 200 | "Engine": "MySQL", 201 | "MasterUsername": "wordpress", 202 | "MasterUserPassword": "wordpress", 203 | "VPCSecurityGroups": [{"Ref": "DatabaseSecurityGroup"}], 204 | "DBSubnetGroupName": {"Ref": "DBSubnetGroup"} 205 | } 206 | }, 207 | "DBSubnetGroup" : { 208 | "Type" : "AWS::RDS::DBSubnetGroup", 209 | "Properties" : { 210 | "DBSubnetGroupDescription" : "DB subnet group", 211 | "SubnetIds": [{"Ref": "SubnetA"}, {"Ref": "SubnetB"}] 212 | } 213 | }, 214 | "LaunchConfiguration": { 215 | "Type": "AWS::AutoScaling::LaunchConfiguration", 216 | "Metadata": { 217 | "AWS::CloudFormation::Init": { 218 | "config": { 219 | "packages": { 220 | "yum": { 221 | "php": [], 222 | "php-mysql": [], 223 | "mysql": [], 224 | "httpd": [] 225 | } 226 | }, 227 | "sources": { 228 | "/var/www/html": "https://wordpress.org/wordpress-4.2.4.tar.gz" 229 | }, 230 | "files": { 231 | "/tmp/config": { 232 | "content": {"Fn::Join": ["", [ 233 | "#!/bin/bash -ex\n", 234 | "cp /var/www/html/wordpress/wp-config-sample.php /var/www/html/wordpress/wp-config.php\n", 235 | "sed -i \"s/'database_name_here'/'wordpress'/g\" wp-config.php\n", 236 | "sed -i \"s/'username_here'/'wordpress'/g\" wp-config.php\n", 237 | "sed -i \"s/'password_here'/'wordpress'/g\" wp-config.php\n", 238 | "sed -i \"s/'localhost'/'", {"Fn::GetAtt": ["Database", "Endpoint.Address"]}, "'/g\" wp-config.php\n", 239 | "chmod -R 777 wp-content/ \n" 240 | ]]}, 241 | "mode": "000500", 242 | "owner": "root", 243 | "group": "root" 244 | } 245 | }, 246 | "commands": { 247 | "01_config": { 248 | "command": "/tmp/config", 249 | "cwd": "/var/www/html/wordpress" 250 | } 251 | }, 252 | "services": { 253 | "sysvinit": { 254 | "httpd": { 255 | "enabled": "true", 256 | "ensureRunning": "true" 257 | } 258 | } 259 | } 260 | } 261 | } 262 | }, 263 | "Properties": { 264 | "EbsOptimized": false, 265 | "ImageId": {"Fn::FindInMap": ["EC2RegionMap", {"Ref": "AWS::Region"}, "AmazonLinuxAMIHVMEBSBacked64bit"]}, 266 | "InstanceType": "t2.micro", 267 | "SecurityGroups": [{"Ref": "WebServerSecurityGroup"}], 268 | "KeyName": {"Ref": "KeyName"}, 269 | "AssociatePublicIpAddress": true, 270 | "UserData": {"Fn::Base64": {"Fn::Join": ["", [ 271 | "#!/bin/bash -ex\n", 272 | "yum update -y aws-cfn-bootstrap\n", 273 | "/opt/aws/bin/cfn-init -v --stack ", {"Ref": "AWS::StackName"}, " --resource LaunchConfiguration --region ", {"Ref": "AWS::Region"}, "\n", 274 | "/opt/aws/bin/cfn-signal -e $? --stack ", {"Ref": "AWS::StackName"}, " --resource AutoScalingGroup --region ", {"Ref": "AWS::Region"}, "\n" 275 | ]]}} 276 | } 277 | }, 278 | "AutoScalingGroup": { 279 | "Type": "AWS::AutoScaling::AutoScalingGroup", 280 | "Properties": { 281 | "LoadBalancerNames": [{"Ref": "LoadBalancer"}], 282 | "LaunchConfigurationName": {"Ref": "LaunchConfiguration"}, 283 | "MinSize": "2", 284 | "MaxSize": "2", 285 | "DesiredCapacity": "2", 286 | "VPCZoneIdentifier": [{"Ref": "SubnetA"}, {"Ref": "SubnetB"}] 287 | }, 288 | "CreationPolicy": { 289 | "ResourceSignal": { 290 | "Timeout": "PT10M" 291 | } 292 | } 293 | } 294 | }, 295 | "Outputs": { 296 | "URL": { 297 | "Value": {"Fn::Join": ["", ["http://", {"Fn::GetAtt": ["LoadBalancer", "DNSName"]}, "/wordpress"]]}, 298 | "Description": "Wordpress URL" 299 | } 300 | } 301 | } 302 | --------------------------------------------------------------------------------