Skip to content

Commit

Permalink
SARIF output support (#573)
Browse files Browse the repository at this point in the history
* SARIF output support
- Pass rule definitions to result formatters
- Add names to rule definitions/violations

* Resolve todo

* Update location details to use relative paths to srcroot

* Provide a default line number

* Migrate to a version.rb file for tracking version internally

* Remove ENV version override
Update version spec to reflect the version.rb values
  • Loading branch information
Kevin Formsma authored Oct 25, 2021
1 parent 351682e commit 67ae380
Show file tree
Hide file tree
Showing 48 changed files with 455 additions and 367 deletions.
2 changes: 1 addition & 1 deletion bin/cfn_nag_rules
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require 'cfn-nag'
require 'rubygems/specification'

opts = Optimist.options do
version Gem::Specification.find_by_name('cfn-nag').version
version CfnNagVersion::VERSION

opt :rule_directory, 'Extra rule directories', type: :io,
required: false,
Expand Down
4 changes: 3 additions & 1 deletion cfn-nag.gemspec
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# frozen_string_literal: true

require_relative 'lib/cfn-nag/version'

Gem::Specification.new do |s|
s.name = 'cfn-nag'
s.license = 'MIT'
s.version = ENV['GEM_VERSION'] || '0.0.0'
s.version = CfnNagVersion::VERSION
s.bindir = 'bin'
s.executables = %w[cfn_nag cfn_nag_rules cfn_nag_scan spcm_scan]
s.authors = ['Eric Kascic']
Expand Down
8 changes: 7 additions & 1 deletion lib/cfn-nag/base_rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,16 @@ def audit(cfn_model)
logical_resource_ids = audit_impl(cfn_model)
return if logical_resource_ids.empty?

violation(logical_resource_ids)
end

def violation(logical_resource_ids, line_numbers = nil)
Violation.new(id: rule_id,
name: self.class.name,
type: rule_type,
message: rule_text,
logical_resource_ids: logical_resource_ids)
logical_resource_ids: logical_resource_ids,
line_numbers: line_numbers)
end
end
end
18 changes: 7 additions & 11 deletions lib/cfn-nag/cfn_nag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require_relative 'result_view/simple_stdout_results'
require_relative 'result_view/colored_stdout_results'
require_relative 'result_view/json_results'
require_relative 'result_view/sarif_results'
require 'cfn-model'

# Top-level CfnNag class for running profiles
Expand Down Expand Up @@ -96,10 +97,10 @@ def audit(cloudformation_string:, parameter_values_string: nil, condition_values
violations = filter_violations_by_deny_list_and_profile(violations)
violations = mark_line_numbers(violations, cfn_model)
rescue RuleRepoException, Psych::SyntaxError, ParserError => fatal_error
violations << fatal_violation(fatal_error.to_s)
violations << Violation.fatal_violation(fatal_error.to_s)
rescue JSON::ParserError => json_parameters_error
error = "JSON Parameter values parse error: #{json_parameters_error}"
violations << fatal_violation(error)
violations << Violation.fatal_violation(error)
end

violations = prune_fatal_violations(violations) if @config.ignore_fatal
Expand All @@ -112,7 +113,7 @@ def prune_fatal_violations(violations)

def render_results(aggregate_results:,
output_format:)
results_renderer(output_format).new.render(aggregate_results)
results_renderer(output_format).new.render(aggregate_results, @config.custom_rule_loader.rule_definitions)
end

private
Expand Down Expand Up @@ -141,7 +142,7 @@ def filter_violations_by_deny_list_and_profile(violations)
violations: violations
)
rescue StandardError => deny_list_or_profile_parse_error
violations << fatal_violation(deny_list_or_profile_parse_error.to_s)
violations << Violation.fatal_violation(deny_list_or_profile_parse_error.to_s)
violations
end

Expand All @@ -152,17 +153,12 @@ def audit_result(violations)
}
end

def fatal_violation(message)
Violation.new(id: 'FATAL',
type: Violation::FAILING_VIOLATION,
message: message)
end

def results_renderer(output_format)
registry = {
'colortxt' => ColoredStdoutResults,
'txt' => SimpleStdoutResults,
'json' => JsonResults
'json' => JsonResults,
'sarif' => SarifResults
}
registry[output_format]
end
Expand Down
4 changes: 2 additions & 2 deletions lib/cfn-nag/cfn_nag_executor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ def scan_file(cfn_nag, fail_on_warnings)
end

def validate_options(opts)
unless opts[:output_format].nil? || %w[colortxt txt json].include?(opts[:output_format])
unless opts[:output_format].nil? || %w[colortxt txt json sarif].include?(opts[:output_format])
Optimist.die(:output_format,
'Must be colortxt, txt, or json')
'Must be colortxt, txt, json or sarif')
end

opts[:rule_arguments]&.each do |rule_argument|
Expand Down
7 changes: 4 additions & 3 deletions lib/cfn-nag/cli_options.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# frozen_string_literal: true

require 'optimist'
require_relative 'version'

# rubocop:disable Metrics/ClassLength
class Options
@custom_rule_exceptions_message = 'Isolate custom rule exceptions - just ' \
'emit the exception without stack trace ' \
'and keep chugging'

@version = Gem::Specification.find_by_name('cfn-nag').version
@version = CfnNagVersion::VERSION

def self.for(type)
case type
Expand Down Expand Up @@ -89,7 +90,7 @@ def self.file_options
required: false,
default: false
opt :output_format,
'Format of results: [txt, json, colortxt]',
'Format of results: [txt, json, colortxt, sarif]',
type: :string,
default: 'colortxt'
opt :rule_repository,
Expand Down Expand Up @@ -132,7 +133,7 @@ def self.scan_options
type: :string,
required: true
opt :output_format,
'Format of results: [txt, json, colortxt]',
'Format of results: [txt, json, colortxt, sarif]',
type: :string,
default: 'colortxt'
opt :debug,
Expand Down
8 changes: 7 additions & 1 deletion lib/cfn-nag/custom_rules/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ def audit(cfn_model)
logical_resource_ids = audit_impl(cfn_model)
return if logical_resource_ids.empty?

violation(logical_resource_ids)
end

def violation(logical_resource_ids, line_numbers = [])
Violation.new(id: rule_id,
name: self.class.name,
type: rule_type,
message: rule_text,
logical_resource_ids: logical_resource_ids)
logical_resource_ids: logical_resource_ids,
line_numbers: line_numbers)
end
end
2 changes: 1 addition & 1 deletion lib/cfn-nag/result_view/json_results.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
require 'json'

class JsonResults
def render(results)
def render(results, _rule_registry)
hashified_results = results.each do |result|
result[:file_results][:violations] = result[:file_results][:violations].map(&:to_h)
end
Expand Down
103 changes: 103 additions & 0 deletions lib/cfn-nag/result_view/sarif_results.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# frozen_string_literal: true

require 'json'
require 'pathname'

class SarifResults
def render(results, rule_registry)
sarif_results = []
results.each do |file|
# For each file in the results, review the violations
file[:file_results][:violations].each do |violation|
# For each violation, generate a sarif result for each logical resource id in the violation
violation.logical_resource_ids.each_with_index do |_logical_resource_id, index|
sarif_results << sarif_result(file_name: file[:filename], violation: violation, index: index)
end
end
end

sarif_report = {
version: '2.1.0',
'$schema': 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
runs: [
tool: {
driver: driver(rule_registry.rules)
},
results: sarif_results
]
}

puts JSON.pretty_generate(sarif_report)
end

# Generates a SARIF driver object, which describes the tool and the rules used
def driver(rules)
{
name: 'cfn_nag',
informationUri: 'https://github.com/stelligent/cfn_nag',
semanticVersion: CfnNagVersion::VERSION,
rules: rules.map do |rule_definition|
{
id: "CFN_NAG_#{rule_definition.id}",
name: rule_definition.name,
fullDescription: {
text: rule_definition.message
}
}
end
}
end

# Given a cfn_nag Violation object, and index, generates a SARIF result object for the finding
def sarif_result(file_name:, violation:, index:)
{
ruleId: "CFN_NAG_#{violation.id}",
level: sarif_level(violation.type),
message: {
text: violation.message
},
locations: [
{
physicalLocation: {
artifactLocation: {
uri: relative_path(file_name),
uriBaseId: '%SRCROOT%'
},
region: {
startLine: sarif_line_number(violation.line_numbers[index])
}
},
logicalLocations: [
{
name: violation.logical_resource_ids[index]
}
]
}
]
}
end

# Line number defaults to 1 unless provided with valid number
def sarif_line_number(line_number)
line_number.nil? || line_number.to_i < 1 ? 1 : line_number.to_i
end

def sarif_level(violation_type)
case violation_type
when RuleDefinition::WARNING
'warning'
else
'error'
end
end

def relative_path(file_name)
file_pathname = Pathname.new(file_name)

if file_pathname.relative?
file_pathname.to_s
else
file_pathname.relative_path_from(Pathname.pwd).to_s
end
end
end
2 changes: 1 addition & 1 deletion lib/cfn-nag/result_view/stdout_results.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def print_warnings(violations)
puts "Warnings count: #{Violation.count_warnings(violations)}"
end

def render(results)
def render(results, _rule_definitions)
results.each do |result|
60.times { print '-' }
puts "\n#{result[:filename]}"
Expand Down
9 changes: 6 additions & 3 deletions lib/cfn-nag/rule_definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,30 @@ class RuleDefinition
WARNING = 'WARN'
FAILING_VIOLATION = 'FAIL'

attr_reader :id, :type, :message
attr_reader :id, :name, :type, :message

def initialize(id:,
name:,
type:,
message:)
@id = id
@name = name
@type = type
@message = message

[@id, @type, @message].each do |required|
[@id, @type, @name, @message].each do |required|
raise 'No parameters to Violation constructor can be nil' if required.nil?
end
end

def to_s
"#{@id} #{@type} #{@message}"
"#{@id} #{name} #{@type} #{@message}"
end

def to_h
{
id: @id,
name: @name,
type: @type,
message: @message
}
Expand Down
1 change: 1 addition & 0 deletions lib/cfn-nag/rule_registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def definition(rule_class)
if existing_def.nil?
rule_definition = RuleDefinition.new(
id: rule.rule_id,
name: rule_class.name,
type: rule.rule_type,
message: rule.rule_text
)
Expand Down
6 changes: 6 additions & 0 deletions lib/cfn-nag/version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

module CfnNagVersion
# This is managed at release time via scripts/publish.sh
VERSION = '0.0.0'
end
11 changes: 11 additions & 0 deletions lib/cfn-nag/violation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@
class Violation < RuleDefinition
attr_reader :logical_resource_ids, :line_numbers

# rubocop:disable Metrics/ParameterLists
def initialize(id:,
name:,
type:,
message:,
logical_resource_ids: [],
line_numbers: [])
super id: id,
name: name,
type: type,
message: message

@logical_resource_ids = logical_resource_ids
@line_numbers = line_numbers
end
# rubocop:enable Metrics/ParameterLists

def to_s
"#{super} #{@logical_resource_ids}"
Expand Down Expand Up @@ -57,6 +61,13 @@ def count_failures(violations)
end
end

def fatal_violation(message)
Violation.new(id: 'FATAL',
name: 'system',
type: Violation::FAILING_VIOLATION,
message: message)
end

private

def empty?(array)
Expand Down
4 changes: 2 additions & 2 deletions scripts/deploy_local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
echo "Installing cfn_nag from local source"
gem uninstall cfn-nag -x
brew gem uninstall cfn-nag
GEM_VERSION=0.0.01 gem build cfn-nag.gemspec
gem install cfn-nag-0.0.01.gem --no-document
gem build cfn-nag.gemspec
gem install cfn-nag-0.0.0.gem --no-document
2 changes: 1 addition & 1 deletion scripts/publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ else
new_version="${minor_version}.$((current_version+1))"
fi

sed -i.bak "s/0\.0\.0/${new_version}/g" cfn-nag.gemspec
sed -i.bak "s/0\.0\.0/${new_version}/g" lib/cfn-nag/version.rb

# publish rubygem to rubygems.org, https://rubygems.org/gems/cfn-nag
gem build cfn-nag.gemspec
Expand Down
2 changes: 1 addition & 1 deletion scripts/setup_and_run_end_to_end_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ download_and_scan_templates () {
fi
}

# Build and install gem locally, using version 0.0.01
# Build and install gem locally, using version 0.0.0
/bin/sh scripts/deploy_local.sh

# Install the two gems required to run end-to-end tests
Expand Down
Loading

0 comments on commit 67ae380

Please sign in to comment.