Skip to content
This repository has been archived by the owner on Jul 1, 2022. It is now read-only.

Commit

Permalink
Merge pull request #12 from mitre/csv_attestation
Browse files Browse the repository at this point in the history
  • Loading branch information
rx294 authored Apr 13, 2021
2 parents a439344 + 616cf32 commit d93cbde
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 57 deletions.
2 changes: 2 additions & 0 deletions inspec-reporter-json-hdf.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Gem::Specification.new do |spec|
spec.required_ruby_version = '~> 2.5'

spec.add_runtime_dependency 'git-lite-version-bump', '~> 0.17', '>= 0.17.3'
spec.add_runtime_dependency 'roo', '~> 2.8.3'

spec.add_development_dependency 'bundler'
spec.add_development_dependency 'bundler-audit'
spec.add_development_dependency 'codeclimate-test-reporter'
Expand Down
93 changes: 59 additions & 34 deletions lib/inspec-reporter-json-hdf/reporter.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@

require 'inspec/plugin/v2'
require 'json'
require 'roo'

VALID_FREQUENCY = %w[annually semiannually quarterly monthly every2weeks weekly every3days daily].freeze

VALID_STATUSES = %w[passed failed].freeze

DATE_FORMAT = '%Y-%m-%d'.freeze

SUPPORTED_INCLUDE_TYPES = %w[csv xlsx].freeze

module InspecPlugins::HdfReporter
# Reporter Plugin Class
class Reporter < Inspec.plugin(2, :reporter)
Expand Down Expand Up @@ -41,40 +44,24 @@ def report
private

def apply_attestation(results, attestation)
if results.empty?
results = [{
"code_desc": 'Manually verified Status provided through attestation',
"run_time": 0.0,
"start_time": DateTime.now.to_s,
"status": attestation['status'],
"message": attestation_message(attestation)
}]
else
results.each do |result|
result[:message] = 'Automated test returned as passed.' if result[:status].eql?('passed')
result[:message] = result[:skip_message] if result[:status].eql?('skipped')

result[:status] = attestation['status']
result[:message] = result[:message] + attestation_message(attestation)

if result[:backtrace]
result[:message] = result[:message] + "\nbacktrace: #{result[:backtrace]}"
result[:backtrace] = nil
end
end
end
results
results << {
"code_desc": 'Manually verified Status provided through attestation',
"run_time": 0.0,
"start_time": DateTime.now.to_s,
"status": attestation['status'],
"message": attestation_message(attestation)
}
end

def attestation_message(attestation)
"
Attestation:
Status: #{attestation['status']}
Explanation: #{attestation['explanation']}
Updated: #{attestation['updated']}
Updated By: #{attestation['updated_by']}
Frequency: #{attestation['frequency']}
"
[
'Attestation:',
"Status: #{attestation['status']}",
"Explanation: #{attestation['explanation']}",
"Updated: #{attestation['updated']}",
"Updated By: #{attestation['updated_by']}",
"Frequency: #{attestation['frequency']}",
].join("\n")
end

def attestation_expired?(date, frequency)
Expand All @@ -88,9 +75,9 @@ def advanced_date(date, frequency)
when 'annually'
parsed_date.next_year(1)
when 'semiannually'
parsed_date.next_year(0.5)
parsed_date.next_month(6)
when 'quarterly'
parsed_date.next_year(0.25)
parsed_date.next_month(3)
when 'monthly'
parsed_date.next_month(1)
when 'every2weeks'
Expand Down Expand Up @@ -121,9 +108,47 @@ def valid_status?(status)
status.is_a?(String) && VALID_STATUSES.include?(status.downcase)
end

def parse_include_file(include_file)
if File.exist?(include_file['path'])
if SUPPORTED_INCLUDE_TYPES.include?(include_file['type'])
sheet = Roo::Spreadsheet.open(include_file['path'], extension: include_file['type'].to_sym ).sheet(0)

attestations = sheet.parse(control_id: "Control_ID",
explanation: "Explanation",
frequency: "Frequency",
status: "Status",
updated: "Updated",
updated_by: "Updated_By",
clean:true
)
# Following is required to convert Datetime field returned by xlsx parser to string
attestations.map do |h|
h[:updated] = h[:updated].to_s
end
else
puts "Warning: Invalid `include-attestations-file` type provided. Supported types: #{SUPPORTED_INCLUDE_TYPES.to_s}"
end
else
puts "Warning: Include Attestation File provided '#{include_file['path']}' not found."
end
attestations || []
end

def collect_attestations
plugin_config = Inspec::Config.cached.fetch_plugin_config('inspec-reporter-json-hdf')
attestations = plugin_config['attestations'] || []
attestations = []

# Parse Attestations from include file.
attestations = parse_include_file(plugin_config['include-attestations-file']) if plugin_config['include-attestations-file']

attestations.map!{ |x| x.transform_keys(&:to_s) }

# Merge inline Attestations from config file and `include file` with precedence to inline definitions.
attestations = (plugin_config['attestations'] || []) + attestations
attestations.uniq! {|e| e['control_id'] }

# Remove Attestations records without status provided.
attestations.reject! { |x| x['status'].eql?("") || x['status'].nil? }

if attestations.empty?
puts 'Warning: Attestations not provided; HDF will be generated without attestations.'
Expand Down
29 changes: 29 additions & 0 deletions templates/attestations.config.template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"plugins": {
"inspec-reporter-json-hdf": {
"include-attestations-file": {
"path": "./attestations.xlsx",
"type": "xlsx"
},
"attestations": [
{
"control_id": "test-control-1",
"explanation": "The routine review through interview/examination attests to this control passing",
"frequency": "annually",
"status": "passed",
"updated": "2021-04-06",
"updated_by": "John Doe, ISSO"
},
{
"control_id": "test-control-2",
"explanation": "The routine review through interview/examination attests to this control passing",
"frequency": "semiannually",
"status": "passed",
"updated": "2021-04-06",
"updated_by": "John Doe, ISSO"
}
]
}
},
"version": "1.2"
}
Binary file added templates/attestations.template.xlsx
Binary file not shown.
51 changes: 29 additions & 22 deletions test/functional/inspec_plugin_functional_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,104 +4,111 @@ class InspecPluginFunctionalTest < Minitest::Test
def test_with_a_single_pass_non_expired_attestation
hdf_json = JSON.parse(File.read('test_hdf.json'))
assert_equal('passed', hdf_json['profiles'][0]['controls'][0]['results'][0]['status'])
assert_match(%r(Automated test returned as passed.\n Attestation:\n Status: passed\n Explanation: Non-expired Status passed\n), hdf_json['profiles'][0]['controls'][0]['results'][0]['message'])
assert_equal('passed', hdf_json['profiles'][0]['controls'][0]['results'][1]['status'])

message = hdf_json['profiles'][0]['controls'][0]['results'][1]['message']
assert_match(%r(Attestation:), message)
assert_match(%r(Status: passed), message)
assert_match(%r(Explanation: Non-expired Status passed), message)
assert_match(%r(Updated By: John Doe, ISSO), message)
assert_match(%r(Frequency: annually), message)
refute_nil(hdf_json['profiles'][0]['controls'][0]['attestation'])
end


def test_with_a_multiple_pass_non_expired_attestation
hdf_json = JSON.parse(File.read('test_hdf.json'))
assert_equal('passed', hdf_json['profiles'][0]['controls'][1]['results'][0]['status'])
assert_equal('passed', hdf_json['profiles'][0]['controls'][1]['results'][1]['status'])
assert_match(%r(Automated test returned as passed.\n Attestation:\n Status: passed\n Explanation: Non-expired Status passed\n), hdf_json['profiles'][0]['controls'][1]['results'][0]['message'])
assert_match(%r(Automated test returned as passed.\n Attestation:\n Status: passed\n Explanation: Non-expired Status passed\n), hdf_json['profiles'][0]['controls'][1]['results'][1]['message'])
assert_equal('passed', hdf_json['profiles'][0]['controls'][1]['results'][2]['status'])
refute_nil(hdf_json['profiles'][0]['controls'][1]['attestation'])
end

def test_with_a_single_fail_non_expired_attestation
hdf_json = JSON.parse(File.read('test_hdf.json'))
assert_equal('passed', hdf_json['profiles'][0]['controls'][2]['results'][0]['status'])
assert_match(%r(\nexpected false\n got true\n\n Attestation:\n Status: passed\n Explanation: Non-expired Status passed\n), hdf_json['profiles'][0]['controls'][2]['results'][0]['message'])
assert_equal('failed', hdf_json['profiles'][0]['controls'][2]['results'][0]['status'])
assert_equal('passed', hdf_json['profiles'][0]['controls'][2]['results'][1]['status'])
refute_nil(hdf_json['profiles'][0]['controls'][2]['attestation'])
end

def test_with_a_multiple_fail_non_expired_attestation
hdf_json = JSON.parse(File.read('test_hdf.json'))
assert_equal('passed', hdf_json['profiles'][0]['controls'][3]['results'][0]['status'])
assert_equal('passed', hdf_json['profiles'][0]['controls'][3]['results'][1]['status'])
assert_match(%r(\nexpected false\n got true\n\n Attestation:\n Status: passed\n Explanation: Non-expired Status passed\n), hdf_json['profiles'][0]['controls'][3]['results'][0]['message'])
assert_match(%r(\nexpected true\n got false\n\n Attestation:\n Status: passed\n Explanation: Non-expired Status passed\n), hdf_json['profiles'][0]['controls'][3]['results'][1]['message'])
assert_equal('failed', hdf_json['profiles'][0]['controls'][3]['results'][0]['status'])
assert_equal('failed', hdf_json['profiles'][0]['controls'][3]['results'][1]['status'])
assert_equal('passed', hdf_json['profiles'][0]['controls'][3]['results'][2]['status'])
refute_nil(hdf_json['profiles'][0]['controls'][3]['attestation'])
end

def test_with_a_single_skip_non_expired_attestation
hdf_json = JSON.parse(File.read('test_hdf.json'))
assert_equal('passed', hdf_json['profiles'][0]['controls'][4]['results'][0]['status'])
assert_match(%r(Manual Test\n Attestation:\n Status: passed\n Explanation: Non-expired Status passed\n), hdf_json['profiles'][0]['controls'][4]['results'][0]['message'])
assert_equal('skipped', hdf_json['profiles'][0]['controls'][4]['results'][0]['status'])
assert_equal('passed', hdf_json['profiles'][0]['controls'][4]['results'][1]['status'])
refute_nil(hdf_json['profiles'][0]['controls'][4]['attestation'])
end
def test_with_a_multiple_skip_non_expired_attestation
hdf_json = JSON.parse(File.read('test_hdf.json'))
assert_equal('passed', hdf_json['profiles'][0]['controls'][5]['results'][0]['status'])
assert_equal('passed', hdf_json['profiles'][0]['controls'][5]['results'][1]['status'])
assert_match(%r(Manual Test2\n Attestation:\n Status: passed\n Explanation: Non-expired Status passed\n), hdf_json['profiles'][0]['controls'][5]['results'][0]['message'])
assert_match(%r(Manual Test2\n Attestation:\n Status: passed\n Explanation: Non-expired Status passed\n), hdf_json['profiles'][0]['controls'][5]['results'][1]['message'])
assert_equal('skipped', hdf_json['profiles'][0]['controls'][5]['results'][0]['status'])
assert_equal('skipped', hdf_json['profiles'][0]['controls'][5]['results'][1]['status'])
assert_equal('passed', hdf_json['profiles'][0]['controls'][5]['results'][2]['status'])
refute_nil(hdf_json['profiles'][0]['controls'][5]['attestation'])
end
def test_with_a_mixed_statuses_non_expired_attestation
hdf_json = JSON.parse(File.read('test_hdf.json'))
assert_equal('passed', hdf_json['profiles'][0]['controls'][6]['results'][0]['status'])
assert_equal('skipped', hdf_json['profiles'][0]['controls'][6]['results'][0]['status'])
assert_equal('passed', hdf_json['profiles'][0]['controls'][6]['results'][1]['status'])
assert_equal('passed', hdf_json['profiles'][0]['controls'][6]['results'][2]['status'])
assert_match(%r(Manual Test\n Attestation:\n Status: passed\n Explanation: Non-expired Status passed\n), hdf_json['profiles'][0]['controls'][6]['results'][0]['message'])
assert_match(%r(Automated test returned as passed.\n Attestation:\n Status: passed\n Explanation: Non-expired Status passed\n), hdf_json['profiles'][0]['controls'][6]['results'][1]['message'])
assert_match(%r(\nexpected false\n got true\n\n Attestation:\n Status: passed\n Explanation: Non-expired Status passed\n), hdf_json['profiles'][0]['controls'][6]['results'][2]['message'])
assert_equal('failed', hdf_json['profiles'][0]['controls'][6]['results'][2]['status'])
assert_equal('passed', hdf_json['profiles'][0]['controls'][6]['results'][3]['status'])
refute_nil(hdf_json['profiles'][0]['controls'][6]['attestation'])
end
def test_with_a_no_statuses_non_expired_attestation
hdf_json = JSON.parse(File.read('test_hdf.json'))
assert_equal('passed', hdf_json['profiles'][0]['controls'][7]['results'][0]['status'])
assert_match(%r(\n Attestation:\n Status: passed\n Explanation: Non-expired Status passed\n), hdf_json['profiles'][0]['controls'][7]['results'][0]['message'])
refute_nil(hdf_json['profiles'][0]['controls'][7]['attestation'])
end
def test_with_a_single_pass_expired_attestation
hdf_json = JSON.parse(File.read('test_hdf.json'))
assert_equal('passed', hdf_json['profiles'][0]['controls'][8]['results'][0]['status'])
assert_nil(hdf_json['profiles'][0]['controls'][8]['results'][1])
refute_nil(hdf_json['profiles'][0]['controls'][8]['attestation'])
end
def test_with_a_multiple_pass_expired_attestation
hdf_json = JSON.parse(File.read('test_hdf.json'))
assert_equal('passed', hdf_json['profiles'][0]['controls'][9]['results'][0]['status'])
assert_equal('passed', hdf_json['profiles'][0]['controls'][9]['results'][1]['status'])
assert_nil(hdf_json['profiles'][0]['controls'][9]['results'][2])
refute_nil(hdf_json['profiles'][0]['controls'][9]['attestation'])
end
def test_with_a_single_fail_expired_attestation
hdf_json = JSON.parse(File.read('test_hdf.json'))
assert_equal('failed', hdf_json['profiles'][0]['controls'][10]['results'][0]['status'])
assert_nil(hdf_json['profiles'][0]['controls'][10]['results'][1])
refute_nil(hdf_json['profiles'][0]['controls'][10]['attestation'])
end
def test_with_a_multiple_fail_expired_attestation
hdf_json = JSON.parse(File.read('test_hdf.json'))
assert_equal('failed', hdf_json['profiles'][0]['controls'][11]['results'][0]['status'])
assert_equal('failed', hdf_json['profiles'][0]['controls'][11]['results'][1]['status'])
assert_nil(hdf_json['profiles'][0]['controls'][11]['results'][2])
refute_nil(hdf_json['profiles'][0]['controls'][11]['attestation'])
end
def test_with_a_single_skip_expired_attestation
hdf_json = JSON.parse(File.read('test_hdf.json'))
assert_equal('skipped', hdf_json['profiles'][0]['controls'][12]['results'][0]['status'])
assert_nil(hdf_json['profiles'][0]['controls'][12]['results'][1])
refute_nil(hdf_json['profiles'][0]['controls'][12]['attestation'])
end
def test_with_a_multiple_skip_expired_attestation
hdf_json = JSON.parse(File.read('test_hdf.json'))
assert_equal('skipped', hdf_json['profiles'][0]['controls'][13]['results'][0]['status'])
assert_equal('skipped', hdf_json['profiles'][0]['controls'][13]['results'][1]['status'])
assert_nil(hdf_json['profiles'][0]['controls'][13]['results'][2])
refute_nil(hdf_json['profiles'][0]['controls'][12]['attestation'])
end
def test_with_a_mixed_statuses_expired_attestation
hdf_json = JSON.parse(File.read('test_hdf.json'))
assert_equal('skipped', hdf_json['profiles'][0]['controls'][14]['results'][0]['status'])
assert_equal('passed', hdf_json['profiles'][0]['controls'][14]['results'][1]['status'])
assert_equal('failed', hdf_json['profiles'][0]['controls'][14]['results'][2]['status'])
assert_nil(hdf_json['profiles'][0]['controls'][14]['results'][3])
refute_nil(hdf_json['profiles'][0]['controls'][14]['attestation'])
end
def test_with_a_no_statuses_expired_attestation
Expand Down
14 changes: 13 additions & 1 deletion test/generate_attestation_file.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'json'
require 'date'
require 'csv'


DATE_FORMAT = '%Y-%m-%d'.freeze
Expand Down Expand Up @@ -144,10 +145,21 @@
config_json = {
'plugins' => {
'inspec-reporter-json-hdf' => {
'attestations' => attestations
'include-attestations-file' => {
'type' => 'csv',
'path' => './attestations.csv'
},
'attestations' => attestations[0..4]
}
},
'version' => '1.2'
}

File.write('attestations.json', config_json.to_json)

CSV.open("attestations.csv", "wb") do |csv|
csv << ["Control_ID","Explanation","Frequency","Status","Updated","Updated_By"]
attestations[5..15].each do |hash|
csv << hash.values
end
end

0 comments on commit d93cbde

Please sign in to comment.