Update: March 5, 2020
Please see Custom Rule Distribution Enhancements for cfn_nag for our latest best practices for cfn_nag custom rules.

Stelligent cfn_nag is an open source command-line tool that performs static analysis of AWS CloudFormation templates. The tool runs as a part of your pre-flight checks in your automated delivery pipeline and can be used to prevent a CloudFormation update from occurring that would put you in a compromised state. The core gem provides over 50 custom rules at the time of writing this blog post. These rules cover a wide range of AWS resources and are geared towards keeping your AWS account and resources secure.

The typical open source contribution model allows the community to propose additions to the core gem. This tends to be the most desirable outcome. Chances are that if you find something useful that someone else would find it useful too. However, there are instances where custom rules have company or project specific logic that may not make sense to put into the cfn_nag core gem. To accomplish this we recommend wrapping the cfn_nag core gem with a wrapper gem that contains your custom rules.

This article will walk you through the process necessary to create a wrapper gem. We have published an example wrapper gem which is a great starting point.

Adding custom rules with a gem wrapper

The following file structure is the bare minimum required for a cfn_nag wrapper gem. In this example the name of the gem is cfn-nag-custom-rules-example and it provides one custom rule called ExampleCustomRule. You will execute cfn_nag (or cfn_nag_scan) with your wrapper’s executable bin/cfn_nag_custom.

.
|- Gemfile
|- bin
|    \- cfn_nag_custom
|- cfn-nag-custom-rules-example.gemspec
|- lib
|    \- cfn-nag-custom-rules-example.rb
\- rules
     \- ExampleCustomRule.rb

The first of the important files is the Gemfile which is boilerplate.

Gemfile:

# frozen_string_literal: true

source 'https://rubygems.org'

gemspec

After that is the executable ruby script wrapper used to load up your custom rules on top of the core rules. It will pass through all arguments to the underlying cfn_nag (or cfn_nag_scan) command as you see fit.

bin/cfn_nag_custom:

#!/usr/bin/env ruby

args = *ARGV
path = Gem.loaded_specs['cfn-nag-custom-rules-example'].full_gem_path
command = "cfn_nag -r #{path}/lib/rules #{args.join(" ")}"
system(command)

Up next is the gemspec. There is nothing to note here outside of requiring the core gem as a dependency. Feel free to pin it any way you would like but we would recommend not always grabbing the latest version.

cfn-nag-custom-rules-example.gemspec:

# frozen_string_literal: true

Gem::Specification.new do |s|
  s.name          = 'cfn-nag-custom-rules-example'
  s.license       = 'MIT'
  s.version       = '0.0.1'
  s.bindir        = 'bin'
  s.executables   = %w[cfn_nag_custom]
  s.authors       = ['Eric Kascic']
  s.summary       = 'Example CFN Nag Wrapper'
  s.description   = 'Wrapper to show how to define custom rules with cfn_nag'
  s.homepage      = 'https://github.com/stelligent/cfn_nag'
  s.files         = Dir.glob('lib/**/*.rb')

  s.require_paths << 'lib' s.required_ruby_version = '>= 2.2'

  s.add_development_dependency('rspec', '~> 3.4')
  s.add_development_dependency('rubocop')

  s.add_runtime_dependency('cfn-nag', '>= 0.3.73')
end

The lib/cfn-nag-custom-rules-example.rb is just a blank ruby file required as part of how gems work and are loaded. Finally, we have our example custom rule. Any file in lib/rules that ends in Rule.rb will be loaded as a custom rule in cfn_nag. The example rule here enforces all S3 buckets should be named ‘foo.’ Please note that custom rules have a rule id that starts with C for custom rule. Rule types can be one of the following.

  • Violation::FAILING_VIOLATION – Will result in a failure
  • Violation::WARNING – Informational message. Only causes a failure if –file_on_warnings is set to true

lib/rules/ExampleCustomRule.rb:

# frozen_string_literal: true

require 'cfn-nag/custom_rules/base'

class ExampleCustomRule < BaseRule
  def rule_text
    'S3 buckets should always be named "foo"'
  end

  def rule_type
    Violation::FAILING_VIOLATION
  end

  def rule_id
    'C1' # Custom Rule #1
  end

  def audit_impl(cfn_model)
    resources = cfn_model.resources_by_type('AWS::S3::Bucket')

    violating_buckets = resources.select do |bucket|
      bucket.bucketName != 'foo'
    end

    violating_buckets.map(&:logical_resource_id)
  end
end

At this point, you can build, install, and execute your custom rules.

gem build cfn-nag-custom-rules-example.gemspec
gem install cfn-nag-custom-rules-example-0.0.1.gem
cfn_nag_custom buckets_with_insecure_acl.json

This results in:

{
  "failure_count": 3,
  "violations": [
    {
      "id": "C1",
      "type": "FAIL",
      "message": "S3 buckets should always be named \"foo\"",
      "logical_resource_ids": [
        "S3BucketRead",
        "S3BucketReadWrite"
      ]
    },
    {
      "id": "W31",
      "type": "WARN",
      "message": "S3 Bucket likely should not have a public read acl",
      "logical_resource_ids": [
        "S3BucketRead"
      ]
    },
    {
      "id": "F14",
      "type": "FAIL",
      "message": "S3 Bucket should not have a public read-write acl",
      "logical_resource_ids": [
        "S3BucketReadWrite"
      ]
    }
  ]
}

As you can see, it evaluated core cfn_nag rules as well as your custom rule.

Additional Resources

Stelligent Amazon Pollycast
Voiced by Amazon Polly