Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SARIF output support #573

Merged
merged 6 commits into from
Oct 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow rule implementation classes to generate violation objects without needing to run audit. This is useful for unit testing and other locations

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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was moved to a class method in Violation

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