-
Create a class that ends with the name Rule. This is a convention that must be observed in order for cfn-nag to load the rule. Additionally, derive this class from
BaseRule
:require 'cfn-nag/violation' require_relative 'base' class IamManagedPolicyNotActionRule < BaseRule
-
Define methods that describe some of the bookkeeping for the rule, like whether it is a WARNING/FAILING_VIOLATION, its unique identifier among rules, and error text shown when it matches:
def rule_text 'IAM managed policy should not allow Allow+NotAction' end def rule_type Violation::WARNING end def rule_id 'W17' end
-
Define the
audit_impl
method to do the actual work of the analysis. This method should return an array of logical resource identifiers from the CloudFormation template:def audit_impl(cfn_model) violating_policies = cfn_model.resources_by_type('AWS::IAM::ManagedPolicy').select do |policy| !policy.policy_document.allows_not_action.empty? end violating_policies.map { |policy| policy.logical_resource_id } end
-
The cfn_model object passed into the
audit_impl
method is where a majority of the improvement lies. When a CloudFormation document is parsed, it is mapped into a collection of objects that mirror the resource types. The call toresources_by_type
will return objects for each resource with that type (http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html).There are a few important details to mapping:
-
Any top-level attribute in the Properties block of a Resource will be an attribute of the returned object.
-
Attributes/properties are returned in camelCase.
-
For example, if dealing with an object type of 'AWS::RDS::DBCluster', and needing to write logic against the 'StorageEncrypted' property, then an example
audit_impl
might look like this:def audit_impl(cfn_model) violating_rdscluster = cfn_model.resources_by_type('AWS::RDS::DBCluster').select do |cluster| cluster.storageEncrypted.nil? || cluster.storageEncrypted.to_s.downcase == 'false' end violating_rdscluster.map { |cluster| cluster.logical_resource_id } end
-
-
cfn-model provides special handling for a subset of objects whereby properties may be transformed into something simpler and/or objects may be linked together.
-
If an object doesn't have special handling (consult cfn-model for the list of objects with special handling) then only the top-level attributes in Properties are mapped to object properties. This means that any second-level objects are accessible as Hash. For example, in
AWS::WAF::WebACL
, the DefaultAction property is mapped to a Hash with the keyType
-
The raw underlying CloudFormation template is available as a Hash from cfn_model for any special processing that mapping would interfere with.
A developer can write pretty much any rule against the mapped objects or even the raw objects, but the purpose of cfn-model's special handling of certain objects is to simplify rule writing. In CloudFormation there are a number of places where fields can be Array or Hash (like ingress). cfn-model typically collapses fields like this into Arrays so that enumerators can be used. Two other examples of note are:
- linking SecurityGroup with related SecurityGroupIngress and SecurityGroupEgress
- policyDocument fields are fairly complex, so they are mapped to a PolicyDocument with some query and convenience methods rules can use versus having to implement themselves (potentially in multiple places)
For a "synthetic" field like this, one computed by cfn-model versus being mapped from the underlying document, the original mapped field still holds its original content, while a new field is added with the computed value.
-
There are two general methods for placing custom rules:
- Placement into the
custom_rules
directory that is installed with the cfn-nag gem- Choosing this method requires the least amount of changes to your code relative to the examples seen on this page.
- If you need to know where the directory is on your filesystem, you can run
gem contents cfn-nag | grep custom_rules
. - Rules will automatically be included in subsequent executions of
cfn_nag_scan
. This can be verified by runningcfn_nag_rules
.
- Placement into a custom directory of your choice
- Choosing this method gives you slightly more flexiblity around saving rules wherever you'd like them to be, but requires a couple small tweaks to normal execution:
- You'll need to modify the import of the BaseRule class from
require_relative 'base'
torequire 'cfn-nag/custom_rules/base'
. - When executing the scan of a template, you'll need to use the
-r
or--rules-directory
switch to specify your own custom rules directory. - An example execution might look like this:
cfn_nag_scan -r ./example_rules my-cfn-template.yaml
Testing your rule is important. The tests for the custom_rules
included with
cfn_nag
are present in the spec/custom_rules
directory; the templates tested
are found in the subdirectories of test_templates/
sorted by template type.
Taking the Neptune database storage encryption rule test
as an example, note the test methods in which a 'rule pass' indicates an empty
list of logical resource IDs, while the failures have a list containing
the logical resource ID defined by the tested resource. The provided templates
defined in spec/test_templates/json/neptune/
include each possible failure
case in addition to success. Writing the test for your custom rule should
generally follow the same method.
After you have written your test, you can run the test suite locally via
rspec
. Although several valid methods exist for doing so, the following
is recommended:
- Use a Ruby virtual environment.
- Using a named gemset, perform
bundle install
to install dependencies. - From the root directory of the project,
rspec
executes the test suite.
For any generic rules you want to share with the community, submit a PR of the rule to lib/custom_rules
.
Code should be linted using Rubocop. Please be sure to use a unique
rule_id
and write the rule according section 1 of the above "Where to Place Rule Files" area.