diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c09f092..fae7bab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [ ubuntu-16.04, ubuntu-latest, macos-latest, windows-latest ] + platform: [ ubuntu-latest, macos-latest, windows-latest ] ruby: [ 2.7 ] runs-on: ${{ matrix.platform }} @@ -57,7 +57,8 @@ jobs: inspec_tools inspec2ckl -j examples/sample_json/rhel-simp.json -o inspec2ckl_test_1.ckl inspec_tools inspec2ckl -j test/data/inspec_1.json -o test/data/inspec2ckl_test_2.ckl inspec_tools inspec2xccdf -j examples/sample_json/rhel-simp.json -a lib/data/attributes.yml -o inspec2xccdf_test.xml - inspec_tools inspec2xccdf -j examples/sample_json/rhel-simp.json -a examples/inspec2xccdf/xccdf_compliant_attribute.json -m examples/inspec2xccdf/metadata.json -o inspec2xccdf_11.xml + inspec_tools inspec2xccdf -j examples/sample_json/rhel-simp.json -a examples/inspec2xccdf/xccdf_compliant_attribute.json -m examples/inspec2xccdf/metadata.json -o inspec2xccdf_11_1.xml + inspec_tools inspec2xccdf -j examples/sample_json/inspec-v4.28.0.json -a examples/inspec2xccdf/xccdf_compliant_attribute.json -o inspec2xccdf_11_2.xml inspec_tools compliance -j examples/sample_json/single_control_results.json -f examples/sample_yaml/threshold.yaml env: CHEF_LICENSE: "accept" @@ -72,20 +73,26 @@ jobs: env: CHEF_LICENSE: "accept" - name: Validate Output CKL with xmllint - if: matrix.platform == 'ubuntu-latest' || matrix.platform == 'ubuntu-16.04' + if: matrix.platform == 'ubuntu-latest' uses: ChristophWurst/xmllint-action@v1 with: xml-file: inspec2ckl_test_1.ckl xml-schema-file: test/schemas/U_Checklist_Schema_V2-3.xsd - name: Validate Output CKL with xmllint - if: matrix.platform == 'ubuntu-latest' || matrix.platform == 'ubuntu-16.04' + if: matrix.platform == 'ubuntu-latest' uses: ChristophWurst/xmllint-action@v1 with: xml-file: test/data/inspec2ckl_test_2.ckl xml-schema-file: test/schemas/U_Checklist_Schema_V2-3.xsd - - name: Validate XCCDF 1.1 with xmllint - if: matrix.platform == 'ubuntu-latest' || matrix.platform == 'ubuntu-16.04' + - name: Validate XCCDF 1.1 with xmllint (Inspec Version < 4.28) + if: matrix.platform == 'ubuntu-latest' uses: ChristophWurst/xmllint-action@v1 with: - xml-file: inspec2xccdf_11.xml + xml-file: inspec2xccdf_11_1.xml + xml-schema-file: test/schemas/xccdf_114/xccdf-1.1.4.xsd + - name: Validate XCCDF 1.1 with xmllint (Inspec Version > 4.28) + if: matrix.platform == 'ubuntu-latest' + uses: ChristophWurst/xmllint-action@v1 + with: + xml-file: inspec2xccdf_11_2.xml xml-schema-file: test/schemas/xccdf_114/xccdf-1.1.4.xsd diff --git a/.gitignore b/.gitignore index d19c35a..3f536f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ *.gem *.rbc +*.swp .bundle .config .idea .yardoc +.rake_tasks~ _yardoc Gemfile.lock coverage diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 25c0ed4..9dfff15 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2021-05-27 14:19:48 UTC using RuboCop version 1.14.0. +# on 2021-05-11 19:56:27 UTC using RuboCop version 1.14.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -13,7 +13,7 @@ Lint/FloatComparison: - 'lib/utilities/inspec_util.rb' - 'lib/utilities/xccdf/xccdf_score.rb' -# Offense count: 41 +# Offense count: 42 # Configuration parameters: IgnoredMethods, CountRepeatedAttributes. Metrics/AbcSize: Max: 182 @@ -21,22 +21,22 @@ Metrics/AbcSize: # Offense count: 9 # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 296 + Max: 550 -# Offense count: 18 +# Offense count: 17 # Configuration parameters: IgnoredMethods. Metrics/CyclomaticComplexity: Max: 30 -# Offense count: 49 +# Offense count: 66 # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. Metrics/MethodLength: - Max: 46 + Max: 44 -# Offense count: 15 +# Offense count: 14 # Configuration parameters: IgnoredMethods. Metrics/PerceivedComplexity: - Max: 31 + Max: 30 # Offense count: 2 # Configuration parameters: AllowedNames. diff --git a/examples/sample_json/inspec-v4.28.0.json b/examples/sample_json/inspec-v4.28.0.json new file mode 100644 index 0000000..1177982 --- /dev/null +++ b/examples/sample_json/inspec-v4.28.0.json @@ -0,0 +1,158 @@ +{ + "name": "example_id", + "title": "bTitle", + "maintainer": "The Authors", + "copyright": "The Authors", + "copyright_email": "you@example.com", + "license": "Apache-2.0", + "summary": "bDescription", + "version": "0.1.0", + "supports": [], + "controls": [ + { + "title": "Ensure configuration is set in situations", + "desc": "Identify the threat actor and threat vector.\n \n Describe the mitigation.\n \n Note the external dependencies of the configuration.", + "descriptions": { + "default": "Identify the threat actor and threat vector.\n \n Describe the mitigation.\n \n Note the external dependencies of the configuration.", + "rationale": "", + "check": "Describe preconditions for conducting the check.\n \nList each step of the check.\n\nIdentify mitigating factors.\n\nDefine success or failure conditions.", + "fix": "Describe preconditions for changing the configuration.\n\nList each step of applying the configuration.\n\nIdentify risks to confidentialty, integrity, or availability associated with applying the configuration." + }, + "impact": 0.5, + "refs": [], + "tags": { + "severity": "low", + "gtitle": "SRG-APP-000220-ZZZ-567890", + "gid": "gid_unused", + "rid": "r1_rule", + "stig_id": "stig_id_unused", + "fix_id": "fix_id_unused", + "cci": [ + "CCI-001499", + "CCI-000197" + ], + "legacy": [ + "V-72845", + "SV-87497" + ], + "nist": [ + "CM-5 (6)", + "IA-5 (1) (c)" + ] + }, + "code": "control 'X-123456' do\n title 'Ensure configuration is set in situations'\n desc \"Identify the threat actor and threat vector.\n \n Describe the mitigation.\n \n Note the external dependencies of the configuration.\n \"\n desc 'rationale', ''\n desc 'check', \"Describe preconditions for conducting the check.\n \nList each step of the check.\n\nIdentify mitigating factors.\n\nDefine success or failure conditions.\n\"\n desc 'fix', \"\n Describe preconditions for changing the configuration.\n\n List each step of applying the configuration.\n\n Identify risks to confidentialty, integrity, or availability associated with applying the configuration.\n \"\n impact 0.5\n tag severity: 'low'\n tag gtitle: 'SRG-APP-000220-ZZZ-567890'\n tag gid: 'gid_unused'\n tag rid: 'r1_rule'\n tag stig_id: 'stig_id_unused'\n tag fix_id: 'fix_id_unused'\n tag cci: ['CCI-001499', 'CCI-000197']\n tag legacy: ['V-72845', 'SV-87497']\n tag nist: ['CM-5 (6)', 'IA-5 (1) (c)']\nend\n", + "source_location": { + "ref": "./controls/g1Identifier.rb", + "line": 3 + }, + "id": "X-123456" + }, + { + "title": "Ensure a log metric filter and alarm exist for AWS Config\nconfiguration changes", + "desc": "Real-time monitoring of API calls can be achieved by directing\nCloudTrail Logs to CloudWatch Logs and establishing corresponding metric\nfilters and alarms. It is recommended that a metric filter and alarm be\nestablished for detecting changes to CloudTrail's configurations.", + "descriptions": { + "default": "Real-time monitoring of API calls can be achieved by directing\nCloudTrail Logs to CloudWatch Logs and establishing corresponding metric\nfilters and alarms. It is recommended that a metric filter and alarm be\nestablished for detecting changes to CloudTrail's configurations.", + "rationale": "", + "check": "N/A", + "fix": "ft2FixText" + }, + "impact": 0.5, + "refs": [], + "tags": { + "severity": "medium", + "gtitle": "g2Title", + "gid": "g2Identifier", + "rid": "r2_rule", + "stig_id": "r2Version", + "fix_id": "f2Identifier", + "cci": [ + "CCI-001495", + "CCI-000196" + ], + "legacy": [ + "identVLegacy3", + "identVLegacy4" + ], + "nist": [ + "AU-9", + "IA-5 (1) (c)" + ] + }, + "code": "control 'g2Identifier' do\n title \"Ensure a log metric filter and alarm exist for AWS Config\nconfiguration changes\"\n desc \"Real-time monitoring of API calls can be achieved by directing\nCloudTrail Logs to CloudWatch Logs and establishing corresponding metric\nfilters and alarms. It is recommended that a metric filter and alarm be\nestablished for detecting changes to CloudTrail's configurations.\"\n desc 'rationale', ''\n desc 'check', 'N/A'\n desc 'fix', 'ft2FixText'\n impact 0.5\n tag severity: 'medium'\n tag gtitle: 'g2Title'\n tag gid: 'g2Identifier'\n tag rid: 'r2_rule'\n tag stig_id: 'r2Version'\n tag fix_id: 'f2Identifier'\n tag cci: ['CCI-001495', 'CCI-000196']\n tag legacy: ['identVLegacy3', 'identVLegacy4']\n tag nist: ['AU-9', 'IA-5 (1) (c)']\nend\n", + "source_location": { + "ref": "./controls/g2Identifier.rb", + "line": 3 + }, + "id": "g2Identifier" + }, + { + "title": "Ensure a log metric filter and alarm exist for AWS Config\nconfiguration changes", + "desc": "Real-time monitoring of API calls can be achieved by directing\nCloudTrail Logs to CloudWatch Logs and establishing corresponding metric\nfilters and alarms. It is recommended that a metric filter and alarm be\nestablished for detecting changes to CloudTrail's configurations.", + "descriptions": { + "default": "Real-time monitoring of API calls can be achieved by directing\nCloudTrail Logs to CloudWatch Logs and establishing corresponding metric\nfilters and alarms. It is recommended that a metric filter and alarm be\nestablished for detecting changes to CloudTrail's configurations.", + "rationale": "", + "check": "N/A", + "fix": "ft3FixText" + }, + "impact": 0.5, + "refs": [], + "tags": { + "severity": "medium", + "gtitle": "g3Title", + "gid": "g3Identifier", + "rid": "r3_rule", + "stig_id": "r3Version", + "fix_id": "f3Identifier", + "cci": [ + "CCI-001495", + "CCI-000196" + ], + "legacy": [ + "identVLegacy5", + "identVLegacy6" + ], + "nist": [ + "AU-9", + "IA-5 (1) (c)" + ] + }, + "code": "control 'g3Identifier' do\n title \"Ensure a log metric filter and alarm exist for AWS Config\nconfiguration changes\"\n desc \"Real-time monitoring of API calls can be achieved by directing\nCloudTrail Logs to CloudWatch Logs and establishing corresponding metric\nfilters and alarms. It is recommended that a metric filter and alarm be\nestablished for detecting changes to CloudTrail's configurations.\"\n desc 'rationale', ''\n desc 'check', 'N/A'\n desc 'fix', 'ft3FixText'\n impact 0.5\n tag severity: 'medium'\n tag gtitle: 'g3Title'\n tag gid: 'g3Identifier'\n tag rid: 'r3_rule'\n tag stig_id: 'r3Version'\n tag fix_id: 'f3Identifier'\n tag cci: ['CCI-001495', 'CCI-000196']\n tag legacy: ['identVLegacy5', 'identVLegacy6']\n tag nist: ['AU-9', 'IA-5 (1) (c)']\nend\n", + "source_location": { + "ref": "./controls/g3Identifier.rb", + "line": 3 + }, + "id": "g3Identifier" + } + ], + "groups": [ + { + "title": null, + "controls": [ + "X-123456" + ], + "id": "controls/g1Identifier.rb" + }, + { + "title": null, + "controls": [ + "g2Identifier" + ], + "id": "controls/g2Identifier.rb" + }, + { + "title": null, + "controls": [ + "g3Identifier" + ], + "id": "controls/g3Identifier.rb" + } + ], + "inputs": [], + "sha256": "4e73883fc2f0d7c85e953346717c149539978c2780bc52c99276e3d6d6fe0567", + "status_message": "", + "status": "loaded", + "generator": { + "name": "inspec", + "version": "4.28.0" + } +} diff --git a/lib/inspec_tools/inspec.rb b/lib/inspec_tools/inspec.rb index f27e5fe..0ffa3f8 100644 --- a/lib/inspec_tools/inspec.rb +++ b/lib/inspec_tools/inspec.rb @@ -9,11 +9,12 @@ require_relative '../happy_mapper_tools/benchmark' require_relative '../utilities/inspec_util' require_relative 'csv' -require_relative '../utilities/xccdf/from_inspec' -require_relative '../utilities/xccdf/to_xccdf' +require_relative '../utilities/xccdf/xccdf_score' module InspecTools class Inspec + DATA_NOT_FOUND_MESSAGE = 'N/A'.freeze + def initialize(inspec_json, metadata = {}) @json = JSON.parse(inspec_json) @metadata = metadata @@ -38,10 +39,11 @@ def to_ckl(title = nil, date = nil, cklist = nil) # @param attributes [Hash] Optional input attributes # @return [String] XML formatted String def to_xccdf(attributes, verbose = false) - data = Utils::FromInspec.new.parse_data_for_xccdf(@json) + data = parse_data_for_xccdf(@json) @verbose = verbose + @benchmark = HappyMapperTools::Benchmark::Benchmark.new - Utils::ToXCCDF.new(attributes || {}, data).to_xml(@metadata) + to_xml(@metadata, attributes, data) end #### @@ -80,6 +82,478 @@ def find_topmost_profile_name(index, parent_name = nil) find_topmost_profile_name(index + 1, parent_name) end + # Build entire XML document and produce final output + # @param metadata [Hash] Data representing a system under scan + def to_xml(metadata, attributes, data) + attributes = {} if attributes.nil? + build_benchmark_header(attributes) + build_groups(attributes, data) + # Only populate results if a target is defined so that conformant XML is produced. + @benchmark.testresult = build_test_results(metadata, data) if metadata['fqdn'] + @benchmark.to_xml + end + + def build_benchmark_header(attributes) + @benchmark.title = attributes['benchmark.title'] + @benchmark.id = attributes['benchmark.id'] + @benchmark.description = attributes['benchmark.description'] + @benchmark.version = attributes['benchmark.version'] + @benchmark.xmlns = 'http://checklists.nist.gov/xccdf/1.1' + + @benchmark.status = HappyMapperTools::Benchmark::Status.new + @benchmark.status.status = attributes['benchmark.status'] + @benchmark.status.date = attributes['benchmark.status.date'] + + if attributes['benchmark.notice.id'] + @benchmark.notice = HappyMapperTools::Benchmark::Notice.new + @benchmark.notice.id = attributes['benchmark.notice.id'] + end + + if attributes['benchmark.plaintext'] || attributes['benchmark.plaintext.id'] + @benchmark.plaintext = HappyMapperTools::Benchmark::Plaintext.new + @benchmark.plaintext.plaintext = attributes['benchmark.plaintext'] + @benchmark.plaintext.id = attributes['benchmark.plaintext.id'] + end + + @benchmark.reference = HappyMapperTools::Benchmark::ReferenceBenchmark.new + @benchmark.reference.href = attributes['reference.href'] + @benchmark.reference.dc_publisher = attributes['reference.dc.publisher'] + @benchmark.reference.dc_source = attributes['reference.dc.source'] + end + + # Translate join of Inspec results and input attributes to XCCDF Groups + def build_groups(attributes, data) + group_array = [] + data['controls'].each do |control| + group = HappyMapperTools::Benchmark::Group.new + group.id = control['id'] + group.title = control['gtitle'] + group.description = "#{control['gdescription']}" if control['gdescription'] + + group.rule = HappyMapperTools::Benchmark::Rule.new + group.rule.id = control['rid'] + group.rule.severity = control['severity'] + group.rule.weight = control['rweight'] + group.rule.version = control['rversion'] + group.rule.title = control['title'].tr("\n", ' ') if control['title'] + group.rule.description = "#{control['desc']}false#{control['rationale']}" + + if ['reference.dc.publisher', 'reference.dc.title', 'reference.dc.subject', 'reference.dc.type', 'reference.dc.identifier'].any? { |a| attributes.key?(a) } + group.rule.reference = build_rule_reference(attributes) + end + + group.rule.ident = build_rule_idents(control['cci']) if control['cci'] + group.rule.ident += build_rule_idents(control['legacy']) if control['legacy'] + + group.rule.fixtext = HappyMapperTools::Benchmark::Fixtext.new + group.rule.fixtext.fixref = control['fix_id'] + group.rule.fixtext.fixtext = control['fix'] + + group.rule.fix = build_rule_fix(control['fix_id']) if control['fix_id'] + + group.rule.check = HappyMapperTools::Benchmark::Check.new + group.rule.check.system = control['checkref'] + + # content_ref is optional for schema compliance + if attributes['content_ref.name'] || attributes['content_ref.href'] + group.rule.check.content_ref = HappyMapperTools::Benchmark::ContentRef.new + group.rule.check.content_ref.name = attributes['content_ref.name'] + group.rule.check.content_ref.href = attributes['content_ref.href'] + end + + group.rule.check.content = control['check'] + + group_array << group + end + @benchmark.group = group_array + end + + # Construct a Benchmark Testresult from Inspec data. This must be called after all XML processing has occurred for profiles + # and groups. + # @param metadata [Hash] + # @return [TestResult] + def build_test_results(metadata, data) + test_result = HappyMapperTools::Benchmark::TestResult.new + test_result.version = @benchmark.version + test_result = populate_remark(test_result, data) + test_result = populate_target_facts(test_result, metadata) + test_result = populate_identity(test_result, metadata) + test_result = populate_results(test_result, data) + populate_score(test_result, @benchmark.group) + end + + # Contruct a Rule / RuleResult fix element with the provided id. + def build_rule_fix(fix_id) + HappyMapperTools::Benchmark::Fix.new.tap { |f| f.id = fix_id } + end + + # Construct rule identifiers for rule + # @param idents [Array] + def build_rule_idents(idents) + raise "#{idents} is not an Array type." unless idents.is_a?(Array) + + # Each rule identifier is a different element + idents.map do |identifier| + HappyMapperTools::Benchmark::Ident.new identifier + end + end + + # Contruct a Rule reference element + def build_rule_reference(attributes) + reference = HappyMapperTools::Benchmark::ReferenceGroup.new + reference.dc_publisher = attributes['reference.dc.publisher'] + reference.dc_title = attributes['reference.dc.title'] + reference.dc_subject = attributes['reference.dc.subject'] + reference.dc_type = attributes['reference.dc.type'] + reference.dc_identifier = attributes['reference.dc.identifier'] + reference + end + + # Create a remark with contextual information about the Inspec version and profiles used + # @param result [HappyMapperTools::Benchmark::TestResult] + def populate_remark(result, data) + result.remark = "Results created using Inspec version #{data['inspec_version']}." + result.remark += "\n#{data['profiles'].map { |p| "Profile: #{p['name']} Version: #{p['version']}" }.join("\n")}" if data['profiles'] + result + end + + # Create all target specific information. + # @param result [HappyMapperTools::Benchmark::TestResult] + # @param metadata [Hash] + def populate_target_facts(result, metadata) + result.target = metadata['fqdn'] + result.target_address = metadata['ip'] if metadata['ip'] + + all_facts = [] + + if metadata['mac'] + fact = HappyMapperTools::Benchmark::Fact.new + fact.name = 'urn:xccdf:fact:asset:identifier:mac' + fact.type = 'string' + fact.fact = metadata['mac'] + all_facts << fact + end + + if metadata['ip'] + fact = HappyMapperTools::Benchmark::Fact.new + fact.name = 'urn:xccdf:fact:asset:identifier:ipv4' + fact.type = 'string' + fact.fact = metadata['ip'] + all_facts << fact + end + + return result unless all_facts.size.nonzero? + + facts = HappyMapperTools::Benchmark::TargetFact.new + facts.fact = all_facts + result.target_facts = facts + result + end + + # Add information about the the account and organization executing the tests. + def populate_identity(test_result, metadata) + if metadata['identity'] + test_result.identity = HappyMapperTools::Benchmark::IdentityType.new + test_result.identity.authenticated = true + test_result.identity.identity = metadata['identity']['identity'] + test_result.identity.privileged = metadata['identity']['privileged'] + end + + test_result.organization = metadata['organization'] if metadata['organization'] + test_result + end + + # Build out the TestResult given all the control and result data. + def populate_results(test_result, data) + # NOTE: id is not an XCCDF 1.2 compliant identifier and will need to be updated when that support is added. + test_result.id = 'result_1' + test_result.starttime = run_start_time(data) + test_result.endtime = run_end_time(data) + + # Build out individual results + all_rule_result = [] + + data['controls'].each do |control| + next if control['results'].nil? || control['results'].empty? + + control_results = + control['results'].map do |result| + populate_rule_result(control, result, xccdf_status(result['status'], control['impact'])) + end + + # Consolidate results into single rule result do to lack of multiple=true attribute on Rule. + # 1. Select the unified result status + selected_status = control_results.reduce(control_results.first.result) { |f_status, rule_result| xccdf_and_result(f_status, rule_result.result) } + + # 2. Only choose results with that status + # 3. Combine those results + all_rule_result << combine_results(control_results.select { |r| r.result == selected_status }) + end + + test_result.rule_result = all_rule_result + test_result + end + + # Return the earliest time of execution. + def run_start_time(data) + start_times = + data['controls'].map do |control| + next if control['results'].nil? + + control['results'].map { |result| DateTime.parse(result['start_time']) } + end + start_times.flatten.min + end + + # Return the latest time of execution accounting for Inspec duration. + def run_end_time(data) + end_times = + data['controls'].map do |control| + next if control['results'].nil? + + control['results'].map { |result| end_time(result['start_time'], result['run_time']) } + end + end_times.flatten.max + end + + # Create rule-result from the control and Inspec result information + def populate_rule_result(control, result, result_status) + rule_result = HappyMapperTools::Benchmark::RuleResultType.new + + rule_result.idref = control['rid'] + rule_result.severity = control['severity'] + rule_result.time = end_time(result['start_time'], result['run_time']) + rule_result.weight = control['rweight'] + + rule_result.result = result_status + rule_result.message = result_message(result, result_status) if result_message(result, result_status) + rule_result.instance = result['code_desc'] + + rule_result.ident = build_rule_idents(control['cci']) if control['cci'] + rule_result.ident += build_rule_idents(control['legacy']) if control['legacy'] + + # Fix information is only necessary when there are failed tests + rule_result.fix = build_rule_fix(control['fix_id']) if control['fix_id'] && result_status == 'fail' + + rule_result.check = HappyMapperTools::Benchmark::Check.new + rule_result.check.system = control['checkref'] + rule_result.check.content = result['code_desc'] + rule_result + end + + # Map the Inspec result status to appropriate XCCDF test result status. + # XCCDF options include: pass, fail, error, unknown, notapplicable, notchecked, notselected, informational, fixed + # + # @param inspec_status [String] The reported Inspec status from an individual test + # @param impact [String] A value of 0.0 - 1.0 + # @return A valid Inspec status. + def xccdf_status(inspec_status, impact) + # Currently, there is no good way to map an Inspec result status to one of XCCDF status unknown or notselected. + case inspec_status + when 'failed' + 'fail' + when 'passed' + 'pass' + when 'skipped' + if impact.to_f.zero? + 'notapplicable' + else + 'notchecked' + end + else + # In the event Inspec adds a new unaccounted for status, mapping to XCCDF unknown. + 'unknown' + end + end + + # When more than one result occurs for a rule and the specification does not declare multiple, the result must be combined. + # This determines the appropriate result to be selected when there are two to compare. + # @param one [String] A rule-result status + # @param two [String] A rule-result status + # @return The result of the AND operation. + def xccdf_and_result(one, two) + # From XCCDF specification truth table + # P = pass + # F = fail + # U = unknown + # E = error + # N = notapplicable + # K = notchecked + # S = notselected + # I = informational + + case one + when 'pass' + %w{fail unknown}.any? { |s| s == two } ? two : one + when 'fail' + one + when 'unknown' + two == 'fail' ? two : one + when 'notapplicable' + %w{pass fail unknown}.any? { |s| s == two } ? two : one + when 'notchecked' + %w{pass fail unknown notapplicable}.any? { |s| s == two } ? two : one + end + end + + # Combines rule results with the same result into a single rule result. + def combine_results(rule_results) + return rule_results.first if rule_results.size == 1 + + # Can combine, result, idents (duplicate, take first instance), instance - combine into an array removing duplicates + # check.content - Only one value allowed, combine by joining with line feed. Prior to, make sure all values are unique. + + rule_result = HappyMapperTools::Benchmark::RuleResultType.new + rule_result.idref = rule_results.first.idref + rule_result.severity = rule_results.first.severity + # Take latest time + rule_result.time = rule_results.reduce(rule_results.first.time) { |time, r| time > r.time ? time : r.time } + rule_result.weight = rule_results.first.weight + + rule_result.result = rule_results.first.result + rule_result.message = rule_results.reduce([]) { |messages, r| r.message ? messages.push(r.message) : messages } + rule_result.instance = rule_results.reduce([]) { |instances, r| r.instance ? instances.push(r.instance) : instances }.join("\n") + + rule_result.ident = rule_results.first.ident + rule_result.fix = rule_results.first.fix + + if rule_results.first.check + rule_result.check = HappyMapperTools::Benchmark::Check.new + rule_result.check.system = rule_results.first.check.system + rule_result.check.content = rule_results.map { |r| r.check.content }.join("\n") + end + + rule_result + end + + # Calculate an end time given a start time and second duration + def end_time(start, duration) + DateTime.parse(start) + (duration / (24*60*60)) + end + + # Builds the message information for rule results + # @param result [Hash] A single Inspec result + # @param xccdf_status [String] the xccdf calculated result status for the provided result + def result_message(result, xccdf_status) + return unless result['message'] || result['skip_message'] + + message = HappyMapperTools::Benchmark::MessageType.new + # Including the code of the check and the resulting message if there is one. + message.message = "#{result['code_desc'] ? "#{result['code_desc']}\n\n" : ''}#{result['message'] || result['skip_message']}" + message.severity = result_message_severity(xccdf_status) + message + end + + # All rule-result messages require a defined severity. This determines a value to use based upon the result XCCDF status. + def result_message_severity(xccdf_status) + case xccdf_status + when 'fail' + 'error' + when 'notapplicable' + 'warning' + else + 'info' + end + end + + # Convert raw Inspec result json into format acceptable for XCCDF transformation. + def parse_data_for_xccdf(json) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + data = {} + + controls = [] + if json['profiles'].nil? + controls = json['controls'] + elsif json['profiles'].length == 1 + controls = json['profiles'].last['controls'] + else + json['profiles'].each do |profile| + controls.concat(profile['controls']) + end + end + c_data = {} + + controls.each do |control| + c_id = control['id'].to_sym + c_data[c_id] = {} + c_data[c_id]['id'] = control['id'] || DATA_NOT_FOUND_MESSAGE + c_data[c_id]['title'] = control['title'] if control['title'] # Optional attribute + c_data[c_id]['desc'] = control['desc'] || DATA_NOT_FOUND_MESSAGE + c_data[c_id]['severity'] = control['tags']['severity'] || 'unknown' + c_data[c_id]['gid'] = control['tags']['gid'] || control['id'] + c_data[c_id]['gtitle'] = control['tags']['gtitle'] if control['tags']['gtitle'] # Optional attribute + c_data[c_id]['gdescription'] = control['tags']['gdescription'] if control['tags']['gdescription'] # Optional attribute + c_data[c_id]['rid'] = control['tags']['rid'] || "r_#{c_data[c_id]['gid']}" + c_data[c_id]['rversion'] = control['tags']['rversion'] if control['tags']['rversion'] # Optional attribute + c_data[c_id]['rweight'] = control['tags']['rweight'] if control['tags']['rweight'] # Optional attribute where N/A is not schema compliant + c_data[c_id]['stig_id'] = control['tags']['stig_id'] || DATA_NOT_FOUND_MESSAGE + c_data[c_id]['cci'] = control['tags']['cci'] if control['tags']['cci'] # Optional attribute + c_data[c_id]['legacy'] = control['tags']['legacy'] if control['tags']['legacy'] # Optional attribute + c_data[c_id]['nist'] = control['tags']['nist'] || ['unmapped'] + + # new (post-2020) inspec output places check, fix, and rationale fields in a descriptions block + if control['descriptions'].is_a?(Hash) && control['descriptions'].key?('check') && control['descriptions'].key?('fix') && control['descriptions'].key?('rationale') + c_data[c_id]['check'] = control['descriptions']['check'] || DATA_NOT_FOUND_MESSAGE + c_data[c_id]['fix'] = control['descriptions']['fix'] || DATA_NOT_FOUND_MESSAGE + c_data[c_id]['rationale'] = control['descriptions']['rationale'] || DATA_NOT_FOUND_MESSAGE + else + c_data[c_id]['check'] = control['tags']['check'] || DATA_NOT_FOUND_MESSAGE + c_data[c_id]['fix'] = control['tags']['fix'] || DATA_NOT_FOUND_MESSAGE + c_data[c_id]['rationale'] = control['tags']['rationale'] || DATA_NOT_FOUND_MESSAGE + end + c_data[c_id]['checkref'] = control['tags']['checkref'] || DATA_NOT_FOUND_MESSAGE + c_data[c_id]['fix_id'] = control['tags']['fix_id'] if control['tags']['fix_id'] # Optional attribute where N/A is not schema compliant + c_data[c_id]['cis_family'] = control['tags']['cis_family'] || DATA_NOT_FOUND_MESSAGE + c_data[c_id]['cis_rid'] = control['tags']['cis_rid'] || DATA_NOT_FOUND_MESSAGE + c_data[c_id]['cis_level'] = control['tags']['cis_level'] || DATA_NOT_FOUND_MESSAGE + c_data[c_id]['impact'] = control['impact'].to_s || DATA_NOT_FOUND_MESSAGE + c_data[c_id]['code'] = control['code'].to_s || DATA_NOT_FOUND_MESSAGE + c_data[c_id]['results'] = parse_results_for_xccdf(control['results']) if control['results'] + end + + data['controls'] = c_data.values + data['profiles'] = parse_profiles_for_xccdf(json['profiles']) + data['status'] = 'success' + # If generator exists this is a more up-to-date inspec.json so look for version in the new location, else old location + data['inspec_version'] = json['generator'].nil? ? json['version'] : json['generator']['version'] + data + end + + # Set scores for all 4 required/recommended scoring systems. + def populate_score(test_result, groups) + score = Utils::XCCDFScore.new(groups, test_result.rule_result) + test_result.score = [score.default_score, score.flat_score, score.flat_unweighted_score, score.absolute_score] + test_result + end + + # Convert profile information for result processing + # @param profiles [Array[Hash]] - The profiles section of the JSON output + def parse_profiles_for_xccdf(profiles) + return [] unless profiles + + profiles.map do |profile| + data = {} + data['name'] = profile['name'] + data['version'] = profile['version'] + data + end + end + + # Convert the test result data to a parseable Hash for downstream processing + # @param results [Array[Hash]] - The results section of the JSON output + def parse_results_for_xccdf(results) + results.map do |result| + data = {} + data['status'] = result['status'] + data['code_desc'] = result['code_desc'] + data['run_time'] = result['run_time'] + data['start_time'] = result['start_time'] + data['resource'] = result['resource'] + data['message'] = result['message'] + data['skip_message'] = result['skip_message'] + data + end + end + ### # This method converts an inspec json to an array of arrays # diff --git a/lib/utilities/xccdf/from_inspec.rb b/lib/utilities/xccdf/from_inspec.rb deleted file mode 100644 index e64f5ef..0000000 --- a/lib/utilities/xccdf/from_inspec.rb +++ /dev/null @@ -1,90 +0,0 @@ -module Utils - # Data transformation from Inspec result output into usable data for XCCDF conversions. - class FromInspec - DATA_NOT_FOUND_MESSAGE = 'N/A'.freeze - - # Convert raw Inspec result json into format acceptable for XCCDF transformation. - def parse_data_for_xccdf(json) - data = {} - - controls = [] - if json['profiles'].nil? - controls = json['controls'] - elsif json['profiles'].length == 1 - controls = json['profiles'].last['controls'] - else - json['profiles'].each do |profile| - controls.concat(profile['controls']) - end - end - c_data = {} - - controls.each do |control| - c_id = control['id'].to_sym - c_data[c_id] = {} - c_data[c_id]['id'] = control['id'] || DATA_NOT_FOUND_MESSAGE - c_data[c_id]['title'] = control['title'] if control['title'] # Optional attribute - c_data[c_id]['desc'] = control['desc'] || DATA_NOT_FOUND_MESSAGE - c_data[c_id]['severity'] = control['tags']['severity'] || 'unknown' - c_data[c_id]['gid'] = control['tags']['gid'] || control['id'] - c_data[c_id]['gtitle'] = control['tags']['gtitle'] if control['tags']['gtitle'] # Optional attribute - c_data[c_id]['gdescription'] = control['tags']['gdescription'] if control['tags']['gdescription'] # Optional attribute - c_data[c_id]['rid'] = control['tags']['rid'] || "r_#{c_data[c_id]['gid']}" - c_data[c_id]['rversion'] = control['tags']['rversion'] if control['tags']['rversion'] # Optional attribute - c_data[c_id]['rweight'] = control['tags']['rweight'] if control['tags']['rweight'] # Optional attribute where N/A is not schema compliant - c_data[c_id]['stig_id'] = control['tags']['stig_id'] || DATA_NOT_FOUND_MESSAGE - c_data[c_id]['cci'] = control['tags']['cci'] if control['tags']['cci'] # Optional attribute - c_data[c_id]['legacy'] = control['tags']['legacy'] if control['tags']['legacy'] # Optional attribute - c_data[c_id]['nist'] = control['tags']['nist'] || ['unmapped'] - c_data[c_id]['check'] = control['tags']['check'] || DATA_NOT_FOUND_MESSAGE - c_data[c_id]['checkref'] = control['tags']['checkref'] || DATA_NOT_FOUND_MESSAGE - c_data[c_id]['fix'] = control['tags']['fix'] || DATA_NOT_FOUND_MESSAGE - c_data[c_id]['fix_id'] = control['tags']['fix_id'] if control['tags']['fix_id'] # Optional attribute where N/A is not schema compliant - c_data[c_id]['rationale'] = control['tags']['rationale'] || DATA_NOT_FOUND_MESSAGE - c_data[c_id]['cis_family'] = control['tags']['cis_family'] || DATA_NOT_FOUND_MESSAGE - c_data[c_id]['cis_rid'] = control['tags']['cis_rid'] || DATA_NOT_FOUND_MESSAGE - c_data[c_id]['cis_level'] = control['tags']['cis_level'] || DATA_NOT_FOUND_MESSAGE - c_data[c_id]['impact'] = control['impact'].to_s || DATA_NOT_FOUND_MESSAGE - c_data[c_id]['code'] = control['code'].to_s || DATA_NOT_FOUND_MESSAGE - c_data[c_id]['results'] = parse_results_for_xccdf(control['results']) if control['results'] - end - - data['controls'] = c_data.values - data['profiles'] = parse_profiles_for_xccdf(json['profiles']) - data['status'] = 'success' - data['inspec_version'] = json['version'] - data - end - - private - - # Convert profile information for result processing - # @param profiles [Array[Hash]] - The profiles section of the JSON output - def parse_profiles_for_xccdf(profiles) - return [] unless profiles - - profiles.map do |profile| - data = {} - data['name'] = profile['name'] - data['version'] = profile['version'] - data - end - end - - # Convert the test result data to a parseable Hash for downstream processing - # @param results [Array[Hash]] - The results section of the JSON output - def parse_results_for_xccdf(results) - results.map do |result| - data = {} - data['status'] = result['status'] - data['code_desc'] = result['code_desc'] - data['run_time'] = result['run_time'] - data['start_time'] = result['start_time'] - data['resource'] = result['resource'] - data['message'] = result['message'] - data['skip_message'] = result['skip_message'] - data - end - end - end -end diff --git a/lib/utilities/xccdf/to_xccdf.rb b/lib/utilities/xccdf/to_xccdf.rb deleted file mode 100644 index c4f17d3..0000000 --- a/lib/utilities/xccdf/to_xccdf.rb +++ /dev/null @@ -1,387 +0,0 @@ -require_relative 'xccdf_score' - -module Utils - # Data conversions for Inspec output into XCCDF format. - class ToXCCDF - # @param attribute [Hash] XCCDF supplemental attributes - # @param data [Hash] Converted Inspec output data - def initialize(attribute, data) - @attribute = attribute - @data = data - @benchmark = HappyMapperTools::Benchmark::Benchmark.new - end - - # Build entire XML document and produce final output - # @param metadata [Hash] Data representing a system under scan - def to_xml(metadata) - build_benchmark_header - build_groups - # Only populate results if a target is defined so that conformant XML is produced. - @benchmark.testresult = build_test_results(metadata) if metadata['fqdn'] - @benchmark.to_xml - end - - private - - # Sets top level XCCDF Benchmark attributes - def build_benchmark_header - @benchmark.title = @attribute['benchmark.title'] - @benchmark.id = @attribute['benchmark.id'] - @benchmark.description = @attribute['benchmark.description'] - @benchmark.version = @attribute['benchmark.version'] - @benchmark.xmlns = 'http://checklists.nist.gov/xccdf/1.1' - - @benchmark.status = HappyMapperTools::Benchmark::Status.new - @benchmark.status.status = @attribute['benchmark.status'] - @benchmark.status.date = @attribute['benchmark.status.date'] - - if @attribute['benchmark.notice.id'] - @benchmark.notice = HappyMapperTools::Benchmark::Notice.new - @benchmark.notice.id = @attribute['benchmark.notice.id'] - end - - if @attribute['benchmark.plaintext'] || @attribute['benchmark.plaintext.id'] - @benchmark.plaintext = HappyMapperTools::Benchmark::Plaintext.new - @benchmark.plaintext.plaintext = @attribute['benchmark.plaintext'] - @benchmark.plaintext.id = @attribute['benchmark.plaintext.id'] - end - - @benchmark.reference = HappyMapperTools::Benchmark::ReferenceBenchmark.new - @benchmark.reference.href = @attribute['reference.href'] - @benchmark.reference.dc_publisher = @attribute['reference.dc.publisher'] - @benchmark.reference.dc_source = @attribute['reference.dc.source'] - end - - # Translate join of Inspec results and input attributes to XCCDF Groups - def build_groups - group_array = [] - @data['controls'].each do |control| - group = HappyMapperTools::Benchmark::Group.new - group.id = control['id'] - group.title = control['gtitle'] - group.description = "#{control['gdescription']}" if control['gdescription'] - - group.rule = HappyMapperTools::Benchmark::Rule.new - group.rule.id = control['rid'] - group.rule.severity = control['severity'] - group.rule.weight = control['rweight'] - group.rule.version = control['rversion'] - group.rule.title = control['title'].tr("\n", ' ') if control['title'] - group.rule.description = "#{control['desc']}false" - - if ['reference.dc.publisher', 'reference.dc.title', 'reference.dc.subject', 'reference.dc.type', 'reference.dc.identifier'].any? { |a| @attribute.key?(a) } - group.rule.reference = build_rule_reference - end - - group.rule.ident = build_rule_idents(control['cci']) if control['cci'] - group.rule.ident += build_rule_idents(control['legacy']) if control['legacy'] - - group.rule.fixtext = HappyMapperTools::Benchmark::Fixtext.new - group.rule.fixtext.fixref = control['fix_id'] - group.rule.fixtext.fixtext = control['fix'] - - group.rule.fix = build_rule_fix(control['fix_id']) if control['fix_id'] - - group.rule.check = HappyMapperTools::Benchmark::Check.new - group.rule.check.system = control['checkref'] - - # content_ref is optional for schema compliance - if @attribute['content_ref.name'] || @attribute['content_ref.href'] - group.rule.check.content_ref = HappyMapperTools::Benchmark::ContentRef.new - group.rule.check.content_ref.name = @attribute['content_ref.name'] - group.rule.check.content_ref.href = @attribute['content_ref.href'] - end - - group.rule.check.content = control['check'] - - group_array << group - end - @benchmark.group = group_array - end - - # Construct a Benchmark Testresult from Inspec data. This must be called after all XML processing has occurred for profiles - # and groups. - # @param metadata [Hash] - # @return [TestResult] - def build_test_results(metadata) - test_result = HappyMapperTools::Benchmark::TestResult.new - test_result.version = @benchmark.version - populate_remark(test_result) - populate_target_facts(test_result, metadata) - populate_identity(test_result, metadata) - populate_results(test_result) - populate_score(test_result, @benchmark.group) - - test_result - end - - # Contruct a Rule / RuleResult fix element with the provided id. - def build_rule_fix(fix_id) - HappyMapperTools::Benchmark::Fix.new.tap { |f| f.id = fix_id } - end - - # Construct rule identifiers for rule - # @param idents [Array] - def build_rule_idents(idents) - raise "#{idents} is not an Array type." unless idents.is_a?(Array) - - # Each rule identifier is a different element - idents.map do |identifier| - HappyMapperTools::Benchmark::Ident.new identifier - end - end - - # Contruct a Rule reference element - def build_rule_reference - reference = HappyMapperTools::Benchmark::ReferenceGroup.new - reference.dc_publisher = @attribute['reference.dc.publisher'] - reference.dc_title = @attribute['reference.dc.title'] - reference.dc_subject = @attribute['reference.dc.subject'] - reference.dc_type = @attribute['reference.dc.type'] - reference.dc_identifier = @attribute['reference.dc.identifier'] - reference - end - - # Create a remark with contextual information about the Inspec version and profiles used - # @param result [HappyMapperTools::Benchmark::TestResult] - def populate_remark(result) - result.remark = "Results created using Inspec version #{@data['inspec_version']}.\n#{@data['profiles'].map { |p| "Profile: #{p['name']} Version: #{p['version']}" }.join("\n")}" - end - - # Create all target specific information. - # @param result [HappyMapperTools::Benchmark::TestResult] - # @param metadata [Hash] - def populate_target_facts(result, metadata) - result.target = metadata['fqdn'] - result.target_address = metadata['ip'] if metadata['ip'] - - all_facts = [] - - if metadata['mac'] - fact = HappyMapperTools::Benchmark::Fact.new - fact.name = 'urn:xccdf:fact:asset:identifier:mac' - fact.type = 'string' - fact.fact = metadata['mac'] - all_facts << fact - end - - if metadata['ip'] - fact = HappyMapperTools::Benchmark::Fact.new - fact.name = 'urn:xccdf:fact:asset:identifier:ipv4' - fact.type = 'string' - fact.fact = metadata['ip'] - all_facts << fact - end - - return unless all_facts.size.nonzero? - - facts = HappyMapperTools::Benchmark::TargetFact.new - facts.fact = all_facts - result.target_facts = facts - end - - # Build out the TestResult given all the control and result data. - def populate_results(test_result) - # NOTE: id is not an XCCDF 1.2 compliant identifier and will need to be updated when that support is added. - test_result.id = 'result_1' - test_result.starttime = run_start_time - test_result.endtime = run_end_time - - # Build out individual results - all_rule_result = [] - - @data['controls'].each do |control| - next if control['results'].empty? - - control_results = - control['results'].map do |result| - populate_rule_result(control, result, xccdf_status(result['status'], control['impact'])) - end - - # Consolidate results into single rule result do to lack of multiple=true attribute on Rule. - # 1. Select the unified result status - selected_status = control_results.reduce(control_results.first.result) { |f_status, rule_result| xccdf_and_result(f_status, rule_result.result) } - - # 2. Only choose results with that status - # 3. Combine those results - all_rule_result << combine_results(control_results.select { |r| r.result == selected_status }) - end - - test_result.rule_result = all_rule_result - test_result - end - - # Create rule-result from the control and Inspec result information - def populate_rule_result(control, result, result_status) - rule_result = HappyMapperTools::Benchmark::RuleResultType.new - - rule_result.idref = control['rid'] - rule_result.severity = control['severity'] - rule_result.time = end_time(result['start_time'], result['run_time']) - rule_result.weight = control['rweight'] - - rule_result.result = result_status - rule_result.message = result_message(result, result_status) if result_message(result, result_status) - rule_result.instance = result['code_desc'] - - rule_result.ident = build_rule_idents(control['cci']) if control['cci'] - rule_result.ident += build_rule_idents(control['legacy']) if control['legacy'] - - # Fix information is only necessary when there are failed tests - rule_result.fix = build_rule_fix(control['fix_id']) if control['fix_id'] && result_status == 'fail' - - rule_result.check = HappyMapperTools::Benchmark::Check.new - rule_result.check.system = control['checkref'] - rule_result.check.content = result['code_desc'] - rule_result - end - - # Combines rule results with the same result into a single rule result. - def combine_results(rule_results) - return rule_results.first if rule_results.size == 1 - - # Can combine, result, idents (duplicate, take first instance), instance - combine into an array removing duplicates - # check.content - Only one value allowed, combine by joining with line feed. Prior to, make sure all values are unique. - - rule_result = HappyMapperTools::Benchmark::RuleResultType.new - rule_result.idref = rule_results.first.idref - rule_result.severity = rule_results.first.severity - # Take latest time - rule_result.time = rule_results.reduce(rule_results.first.time) { |time, r| time > r.time ? time : r.time } - rule_result.weight = rule_results.first.weight - - rule_result.result = rule_results.first.result - rule_result.message = rule_results.reduce([]) { |messages, r| r.message ? messages.push(r.message) : messages } - rule_result.instance = rule_results.reduce([]) { |instances, r| r.instance ? instances.push(r.instance) : instances }.join("\n") - - rule_result.ident = rule_results.first.ident - rule_result.fix = rule_results.first.fix - - if rule_results.first.check - rule_result.check = HappyMapperTools::Benchmark::Check.new - rule_result.check.system = rule_results.first.check.system - rule_result.check.content = rule_results.map { |r| r.check.content }.join("\n") - end - - rule_result - end - - # Add information about the the account and organization executing the tests. - def populate_identity(test_result, metadata) - if metadata['identity'] - test_result.identity = HappyMapperTools::Benchmark::IdentityType.new - test_result.identity.authenticated = true - test_result.identity.identity = metadata['identity']['identity'] - test_result.identity.privileged = metadata['identity']['privileged'] - end - - test_result.organization = metadata['organization'] if metadata['organization'] - end - - # Return the earliest time of execution. - def run_start_time - @data['controls'].map { |control| control['results'].map { |result| DateTime.parse(result['start_time']) } }.flatten.min - end - - # Return the latest time of execution accounting for Inspec duration. - def run_end_time - end_times = - @data['controls'].map do |control| - control['results'].map { |result| end_time(result['start_time'], result['run_time']) } - end - - end_times.flatten.max - end - - # Calculate an end time given a start time and second duration - def end_time(start, duration) - DateTime.parse(start) + (duration / (24*60*60)) - end - - # Map the Inspec result status to appropriate XCCDF test result status. - # XCCDF options include: pass, fail, error, unknown, notapplicable, notchecked, notselected, informational, fixed - # - # @param inspec_status [String] The reported Inspec status from an individual test - # @param impact [String] A value of 0.0 - 1.0 - # @return A valid Inspec status. - def xccdf_status(inspec_status, impact) - # Currently, there is no good way to map an Inspec result status to one of XCCDF status unknown or notselected. - case inspec_status - when 'failed' - 'fail' - when 'passed' - 'pass' - when 'skipped' - if impact.to_f.zero? - 'notapplicable' - else - 'notchecked' - end - else - # In the event Inspec adds a new unaccounted for status, mapping to XCCDF unknown. - 'unknown' - end - end - - # When more than one result occurs for a rule and the specification does not declare multiple, the result must be combined. - # This determines the appropriate result to be selected when there are two to compare. - # @param one [String] A rule-result status - # @param two [String] A rule-result status - # @return The result of the AND operation. - def xccdf_and_result(one, two) - # From XCCDF specification truth table - # P = pass - # F = fail - # U = unknown - # E = error - # N = notapplicable - # K = notchecked - # S = notselected - # I = informational - - case one - when 'pass' - %w{fail unknown}.any? { |s| s == two } ? two : one - when 'fail' - one - when 'unknown' - two == 'fail' ? two : one - when 'notapplicable' - %w{pass fail unknown}.any? { |s| s == two } ? two : one - when 'notchecked' - %w{pass fail unknown notapplicable}.any? { |s| s == two } ? two : one - end - end - - # Builds the message information for rule results - # @param result [Hash] A single Inspec result - # @param xccdf_status [String] the xccdf calculated result status for the provided result - def result_message(result, xccdf_status) - return unless result['message'] || result['skip_message'] - - message = HappyMapperTools::Benchmark::MessageType.new - # Including the code of the check and the resulting message if there is one. - message.message = "#{result['code_desc'] ? "#{result['code_desc']}\n\n" : ''}#{result['message'] || result['skip_message']}" - message.severity = result_message_severity(xccdf_status) - message - end - - # All rule-result messages require a defined severity. This determines a value to use based upon the result XCCDF status. - def result_message_severity(xccdf_status) - case xccdf_status - when 'fail' - 'error' - when 'notapplicable' - 'warning' - else - 'info' - end - end - - # Set scores for all 4 required/recommended scoring systems. - def populate_score(test_result, groups) - score = Utils::XCCDFScore.new(groups, test_result.rule_result) - test_result.score = [score.default_score, score.flat_score, score.flat_unweighted_score, score.absolute_score] - end - end -end diff --git a/test/unit/inspec_tools/inspec_test.rb b/test/unit/inspec_tools/inspec_test.rb index fef6e11..3821fa5 100644 --- a/test/unit/inspec_tools/inspec_test.rb +++ b/test/unit/inspec_tools/inspec_test.rb @@ -1,4 +1,5 @@ require_relative '../test_helper' +require 'json' class InspecTest < Minitest::Test def test_that_xccdf_exists @@ -51,4 +52,567 @@ def test_inspec_to_csv_profile_json csv = inspec_tools.to_csv assert(csv) end + + def test_to_xccdf_single_control + profile = File.read('./examples/sample_json/single_control_profile.json') + json = JSON.parse(profile) + inspec_converter = InspecTools::Inspec.new(profile) + subject = inspec_converter.send(:parse_data_for_xccdf, json) + assert_equal('Users must re-authenticate for privilege escalation.', subject['controls'][0]['title']) + assert_equal('F-78301r2_fix', subject['controls'][0]['fix_id']) + assert_match(/Verify the operating system requires users to reauthenticate/, subject['controls'][0]['check']) + assert_match(/Configure the operating system to require users to reauthenticate/, subject['controls'][0]['fix']) + end + + def test_to_xccdf_no_value_when_no_cci + json = { + 'profiles' => [ + { 'controls' => [ + { + 'id' => '1', + 'tags' => {}, + 'descriptions' => {} + }, + ] }, + ] + } + inspec_converter = InspecTools::Inspec.new(json.to_json) + subject = inspec_converter.send(:parse_data_for_xccdf, json) + refute subject['controls'].first.key?('cci') + end + + def test_to_xccdf_no_value_when_no_fix + json = { + 'profiles' => [ + { 'controls' => [ + { + 'id' => '1', + 'tags' => {}, + 'descriptions' => {} + }, + ] }, + ] + } + inspec_converter = InspecTools::Inspec.new(json.to_json) + subject = inspec_converter.send(:parse_data_for_xccdf, json) + assert subject['controls'].first.key?('fix') + end + + def test_to_xccdf_no_value_when_no_fix_id + json = { + 'profiles' => [ + { 'controls' => [ + { + 'id' => '1', + 'tags' => {}, + 'descriptions' => {} + }, + ] }, + ] + } + inspec_converter = InspecTools::Inspec.new(json.to_json) + subject = inspec_converter.send(:parse_data_for_xccdf, json) + refute subject['controls'].first.key?('fix_id') + end + + def test_to_xccdf_no_value_when_no_gdescription + json = { + 'profiles' => [ + { 'controls' => [ + { + 'id' => '1', + 'tags' => {}, + 'descriptions' => {} + }, + ] }, + ] + } + inspec_converter = InspecTools::Inspec.new(json.to_json) + subject = inspec_converter.send(:parse_data_for_xccdf, json) + refute subject['controls'].first.key?('gdescription') + end + + def test_to_xccdf_no_value_when_no_gtitle + json = { + 'profiles' => [ + { 'controls' => [ + { + 'id' => '1', + 'tags' => {}, + 'descriptions' => {} + }, + ] }, + ] + } + inspec_converter = InspecTools::Inspec.new(json.to_json) + subject = inspec_converter.send(:parse_data_for_xccdf, json) + refute subject['controls'].first.key?('gtitle') + end + + def test_to_xccdf_no_gid_defaults_to_control_id + json = { + 'profiles' => [ + { 'controls' => [ + { + 'id' => '1', + 'tags' => {}, + 'descriptions' => {} + }, + ] }, + ] + } + inspec_converter = InspecTools::Inspec.new(json.to_json) + subject = inspec_converter.send(:parse_data_for_xccdf, json) + assert_equal json['profiles'].first['controls'].first['id'], subject['controls'].first['gid'] + end + + def test__to_xccdf_no_rid_default_rid_value + json = { + 'profiles' => [ + { 'controls' => [ + { + 'id' => '1', + 'tags' => { 'gid' => 'g_id_1' }, + 'descriptions' => {} + }, + ] }, + ] + } + inspec_converter = InspecTools::Inspec.new(json.to_json) + subject = inspec_converter.send(:parse_data_for_xccdf, json) + assert_equal "r_#{json['profiles'].first['controls'].first['tags']['gid']}", subject['controls'].first['rid'] + end + + def test_to_xccdf_no_value_when_no_rversion + json = { + 'profiles' => [ + { 'controls' => [ + { + 'id' => '1', + 'tags' => {}, + 'descriptions' => {} + }, + ] }, + ] + } + inspec_converter = InspecTools::Inspec.new(json.to_json) + subject = inspec_converter.send(:parse_data_for_xccdf, json) + refute subject['controls'].first.key?('rversion') + end + + def test_to_xccdf_no_value_when_no_rweight + json = { + 'profiles' => [ + { 'controls' => [ + { + 'id' => '1', + 'tags' => {}, + 'descriptions' => {} + }, + ] }, + ] + } + inspec_converter = InspecTools::Inspec.new(json.to_json) + subject = inspec_converter.send(:parse_data_for_xccdf, json) + refute subject['controls'].first.key?('rweight') + end + + def test_to_xccdf_default_value_when_no_severity + json = { + 'profiles' => [ + { 'controls' => [ + { + 'id' => '1', + 'tags' => {}, + 'descriptions' => {} + }, + ] }, + ] + } + inspec_converter = InspecTools::Inspec.new(json.to_json) + subject = inspec_converter.send(:parse_data_for_xccdf, json) + assert_equal 'unknown', subject['controls'].first['severity'] + end + + def test_to_xccdf_no_value_when_no_title + json = { + 'profiles' => [ + { 'controls' => [ + { + 'id' => '1', + 'tags' => {}, + 'descriptions' => {} + }, + ] }, + ] + } + inspec_converter = InspecTools::Inspec.new(json.to_json) + subject = inspec_converter.send(:parse_data_for_xccdf, json) + refute subject['controls'].first.key?('title') + end + + def test_to_xccdf_run_end_time + # Minimum JSON to create a converter object. + json = { + 'profiles' => [ + { 'controls' => [ + { + 'id' => '1', + 'tags' => {}, + 'descriptions' => {} + }, + ] }, + ] + } + inspec_converter = InspecTools::Inspec.new(json.to_json) + control = { 'controls' => [ + { + 'results' => [ + { + 'run_time' => 0.000101, + 'start_time' => '2019-10-17T08:00:02-04:00' + }, + ] + }, + { + 'results' => [ + { + 'run_time' => 2.426861, + 'start_time' => '2019-10-17T08:00:04-04:00' + }, + { + 'run_time' => 2.0e-06, + 'start_time' => '2019-10-17T08:00:02-04:00' + }, + + ] + }, + ] } + assert_equal '2019-10-17T08:00:06-04:00', inspec_converter.send(:run_end_time, control).to_s + end + + def test_to_xccdf_run_start_time + # Minimum JSON to create a converter object. + json = { + 'profiles' => [ + { 'controls' => [ + { + 'id' => '1', + 'tags' => {}, + 'descriptions' => {} + }, + ] }, + ] + } + inspec_converter = InspecTools::Inspec.new(json.to_json) + control = { 'controls' => [ + { + 'results' => [ + { + 'run_time' => 0.000101, + 'start_time' => '2019-10-17T08:00:02-04:00' + }, + ] + }, + { + 'results' => [ + { + 'run_time' => 2.426861, + 'start_time' => '2019-10-17T08:00:04-04:00' + }, + { + 'run_time' => 2.0e-06, + 'start_time' => '2019-10-17T08:00:02-04:00' + }, + + ] + }, + ] } + assert_equal '2019-10-17T08:00:02-04:00', inspec_converter.send(:run_start_time, control).to_s + end + + def test_to_xccdf_build_benchmark_header + json = { 'controls' => [ + { + 'results' => [ + { + 'run_time' => 0.000101, + 'start_time' => '2019-10-17T08:00:02-04:00' + }, + ] + }, + { + 'results' => [ + { + 'run_time' => 2.426861, + 'start_time' => '2019-10-17T08:00:04-04:00' + }, + { + 'run_time' => 2.0e-06, + 'start_time' => '2019-10-17T08:00:02-04:00' + }, + + ] + }, + ] } + inspec_converter = InspecTools::Inspec.new(json.to_json) + # Do not create something to parse, just set the data + inspec_converter.instance_variable_set(:@benchmark, HappyMapperTools::Benchmark::Benchmark.new) + inspec_converter.send(:build_benchmark_header, {}) + # when attribute benchmark.plaintext and benchmark.plaintext.id are not provided does not include notice on the benchmark + # when attribute benchmark.notice.id is not provided does not include notice on the benchmark + assert_nil inspec_converter.instance_variable_get(:@benchmark).notice + assert_nil inspec_converter.instance_variable_get(:@benchmark).plaintext + end + + def test_to_xccdf_build_groups_content_ref + # when attribute content_ref.name and content_ref.href are not provided + # it does not include Group::Rule::check::check-content-ref on the benchmark + json = { 'controls' => [ + { + 'id' => '1', + 'desc' => 'A description' + }, + ] } + inspec_converter = InspecTools::Inspec.new(json.to_json) + inspec_converter.instance_variable_set(:@benchmark, HappyMapperTools::Benchmark::Benchmark.new) + inspec_converter.send(:build_groups, {}, json) + assert_nil inspec_converter.instance_variable_get(:@benchmark).group.first.rule.check.content_ref + end + + def test_to_xccdf_build_groups_ident + # when tag cci is not provided it does not include Group::Rule::ident on the benchmark + json = { 'controls' => [ + { + 'id' => '1', + 'desc' => 'A description' + }, + ] } + inspec_converter = InspecTools::Inspec.new(json.to_json) + inspec_converter.instance_variable_set(:@benchmark, HappyMapperTools::Benchmark::Benchmark.new) + inspec_converter.send(:build_groups, {}, json) + assert_nil inspec_converter.instance_variable_get(:@benchmark).group.first.rule.ident + end + + def test_to_xccdf_build_groups_fix + # when tag fixref is not provided it does not include Group::Rule::fix on the benchmark + json = { 'controls' => [ + { + 'id' => '1', + 'desc' => 'A description' + }, + ] } + inspec_converter = InspecTools::Inspec.new(json.to_json) + inspec_converter.instance_variable_set(:@benchmark, HappyMapperTools::Benchmark::Benchmark.new) + inspec_converter.send(:build_groups, {}, json) + assert_nil inspec_converter.instance_variable_get(:@benchmark).group.first.rule.fix + end + + def test_to_xccdf_build_groups_gdescription_not_provided + # when tag gdescription is not provided it does not include Group::description on the benchmark + json = { 'controls' => [ + { + 'id' => '1', + 'desc' => 'A description' + }, + ] } + inspec_converter = InspecTools::Inspec.new(json.to_json) + inspec_converter.instance_variable_set(:@benchmark, HappyMapperTools::Benchmark::Benchmark.new) + inspec_converter.send(:build_groups, {}, json) + assert_nil inspec_converter.instance_variable_get(:@benchmark).group.first.description + end + + def test_to_xccdf_build_groups_gdescription_provided + # when tag gdescription is provided wraps the data in XML tags + json = { 'controls' => [ + { + 'id' => '1', + 'gdescription' => 'A test description', + 'desc' => 'A description' + }, + ] } + inspec_converter = InspecTools::Inspec.new(json.to_json) + inspec_converter.instance_variable_set(:@benchmark, HappyMapperTools::Benchmark::Benchmark.new) + inspec_converter.send(:build_groups, {}, json) + assert_equal "#{json['controls'].first['gdescription']}", inspec_converter.instance_variable_get(:@benchmark).group.first.description + end + + def test_to_xccdf_build_groups_reference + # when no reference attributes are provided it does not include Group::Rule::fix on the benchmark + json = { 'controls' => [ + { + 'id' => '1', + 'desc' => 'A description' + }, + ] } + inspec_converter = InspecTools::Inspec.new(json.to_json) + inspec_converter.instance_variable_set(:@benchmark, HappyMapperTools::Benchmark::Benchmark.new) + inspec_converter.send(:build_groups, {}, json) + assert_nil inspec_converter.instance_variable_get(:@benchmark).group.first.rule.reference + end + + def test_populate_target_facts_no_facts + # Some json to just create the inspectools::inspec object + json = { 'controls' => [ + { + 'id' => '1', + 'desc' => 'A description' + }, + ] } + metadata = {} + inspec_converter = InspecTools::Inspec.new(json.to_json) + results_benchmark = inspec_converter.send(:populate_target_facts, HappyMapperTools::Benchmark::TestResult.new, metadata) + assert_nil results_benchmark.target_facts + end + + def test_populate_target_facts_mac_address + # Some json to just create the inspectools::inspec object + json = { 'controls' => [ + { + 'id' => '1', + 'desc' => 'A description' + }, + ] } + metadata = { 'mac' => '00:11:00' } + inspec_converter = InspecTools::Inspec.new(json.to_json) + results_benchmark = inspec_converter.send(:populate_target_facts, HappyMapperTools::Benchmark::TestResult.new, metadata) + assert_equal '00:11:00', results_benchmark.target_facts.fact.first.fact + end + + def test_populate_target_facts_ip_address + # Some json to just create the inspectools::inspec object + json = { 'controls' => [ + { + 'id' => '1', + 'desc' => 'A description' + }, + ] } + metadata = { 'ip' => '192.168.0.1' } + inspec_converter = InspecTools::Inspec.new(json.to_json) + results_benchmark = inspec_converter.send(:populate_target_facts, HappyMapperTools::Benchmark::TestResult.new, metadata) + assert_equal '192.168.0.1', results_benchmark.target_facts.fact.first.fact + end + + def test_populate_target_facts_target_address + # Some json to just create the inspectools::inspec object + json = { 'controls' => [ + { + 'id' => '1', + 'desc' => 'A description' + }, + ] } + metadata = { 'ip' => '192.168.0.1' } + inspec_converter = InspecTools::Inspec.new(json.to_json) + results_benchmark = inspec_converter.send(:populate_target_facts, HappyMapperTools::Benchmark::TestResult.new, metadata) + assert_equal '192.168.0.1', results_benchmark.target_address + end + + def test_populate_target_facts_fqdn + # Some json to just create the inspectools::inspec object + json = { 'controls' => [ + { + 'id' => '1', + 'desc' => 'A description' + }, + ] } + metadata = { 'fqdn' => 'some.host.local' } + inspec_converter = InspecTools::Inspec.new(json.to_json) + results_benchmark = inspec_converter.send(:populate_target_facts, HappyMapperTools::Benchmark::TestResult.new, metadata) + assert_equal 'some.host.local', results_benchmark.target + end + + def test_populate_results_multiple_results + # when there is more than one test result for a control it consolidates the values into a single rule-result value + json = { 'controls' => [ + { + 'results' => [ + { + 'run_time' => 2.426861, + 'start_time' => '2019-10-17T08:00:04-04:00', + 'status' => 'failed', + 'code_desc' => 'File 1 should exist', + 'cci' => %w{ident_1 ident_2}, + 'message' => 'Can\'t find file: 1' + }, + { + 'run_time' => 2.0e-06, + 'start_time' => '2019-10-17T08:00:02-04:00', + 'status' => 'failed', + 'code_desc' => 'File 2 should exist', + 'cci' => %w{ident_1 ident_2}, + 'message' => 'Can\'t find file: 2' + }, + { + 'run_time' => 2.0e-06, + 'start_time' => '2019-10-17T08:00:02-04:00', + 'status' => 'passed' + }, + ] + }, + ] } + inspec_converter = InspecTools::Inspec.new(json.to_json) + results_benchmark = inspec_converter.send(:populate_results, HappyMapperTools::Benchmark::TestResult.new, json) + assert_equal 1, results_benchmark.rule_result.size + assert_equal 'fail', results_benchmark.rule_result.first.result + end + + def test_result_message_no_information + # Some json to just create the inspectools::inspec object + json = { 'controls' => [ + { + 'id' => '1', + 'desc' => 'A description' + }, + ] } + inspec_converter = InspecTools::Inspec.new(json.to_json) + assert_nil inspec_converter.send(:result_message, {}, 'pass') + end + + def test_result_message_information + # Some json to just create the inspectools::inspec object + json = { 'controls' => [ + { + 'id' => '1', + 'desc' => 'A description' + }, + ] } + result = { + 'status' => 'failed', + 'code_desc' => 'System Package firewalld should be installed', + 'message' => 'expected that `System Package firewalld` is installed' + } + inspec_converter = InspecTools::Inspec.new(json.to_json) + assert_equal "System Package firewalld should be installed\n\nexpected that `System Package firewalld` is installed", inspec_converter.send(:result_message, result, 'fail').message + assert_equal 'error', inspec_converter.send(:result_message, result, 'fail').severity + end + + def test_populate_identity_identity + # Some json to just create the inspectools::inspec object + json = { 'controls' => [ + { + 'id' => '1', + 'desc' => 'A description' + }, + ] } + metadata = { 'identity' => { 'identity' => 'some_user', 'privileged' => true } } + inspec_converter = InspecTools::Inspec.new(json.to_json) + results_benchmark = inspec_converter.send(:populate_identity, HappyMapperTools::Benchmark::TestResult.new, metadata) + assert results_benchmark.identity.authenticated + assert_equal 'some_user', results_benchmark.identity.identity + assert results_benchmark.identity.privileged + end + + def test_populate_identity_organization + # Some json to just create the inspectools::inspec object + json = { 'controls' => [ + { + 'id' => '1', + 'desc' => 'A description' + }, + ] } + metadata = { 'organization' => 'MITRE Corporation' } + inspec_converter = InspecTools::Inspec.new(json.to_json) + results_benchmark = inspec_converter.send(:populate_identity, HappyMapperTools::Benchmark::TestResult.new, metadata) + assert_equal 'MITRE Corporation', results_benchmark.organization + end end diff --git a/test/unit/utils/xccdf/from_inspec_test.rb b/test/unit/utils/xccdf/from_inspec_test.rb deleted file mode 100644 index eaf9340..0000000 --- a/test/unit/utils/xccdf/from_inspec_test.rb +++ /dev/null @@ -1,116 +0,0 @@ -require_relative '../../test_helper' -require_relative '../../../../lib/utilities/xccdf/from_inspec' - -describe Utils::FromInspec do - let(:dci) { Utils::FromInspec.new } - - describe '#parse_data_for_xccdf' do - let(:subject) { dci.parse_data_for_xccdf(json) } - let(:json) do - { - 'profiles' => [{ 'controls' => controls }] - } - end - let(:controls) do - [ - { - 'id' => '1', - 'tags' => {} - }, - ] - end - - describe 'when parsing the single control profile' do - let(:json) { JSON.parse(File.read('./examples/sample_json/single_control_profile.json')) } - - it 'parses as expected' do - assert_equal('Users must re-authenticate for privilege escalation.', subject['controls'][0]['title']) - assert_equal('F-78301r2_fix', subject['controls'][0]['fix_id']) - end - end - - describe 'when there is no cci' do - it 'does not set a value' do - refute subject['controls'].first.key?('cci') - end - end - - describe 'when there is no fix' do - it 'does not set a value' do - assert subject['controls'].first.key?('fix') - end - end - - describe 'when there is no fix_id' do - it 'does not set a value' do - refute subject['controls'].first.key?('fix_id') - end - end - - describe 'when there is no gdescription' do - it 'does not set a value' do - refute subject['controls'].first.key?('gdescription') - end - end - - describe 'when there is no gid' do - let(:controls) do - [ - { - 'id' => '1', - 'tags' => {} - }, - ] - end - - it 'defaults a value that is control id' do - assert_equal controls.first['id'], subject['controls'].first['gid'] - end - end - - describe 'when there is no gtitle' do - it 'does not set a value' do - refute subject['controls'].first.key?('gtitle') - end - end - - describe 'when there is no rid' do - let(:controls) do - [ - { - 'id' => '1', - 'tags' => { 'gid' => 'g_id_1' } - }, - ] - end - - it 'defaults a value that is r_ + the gid value' do - assert_equal "r_#{controls.first['tags']['gid']}", subject['controls'].first['rid'] - end - end - - describe 'when there is no rversion' do - it 'does not set a value' do - refute subject['controls'].first.key?('rversion') - end - end - - describe 'when there is no rweight' do - it 'does not set a value' do - refute subject['controls'].first.key?('rweight') - end - end - - describe 'when there is no severity' do - it 'defaults to the value unknown' do - assert_equal 'unknown', subject['controls'].first['severity'] - end - end - - describe 'when there is no title' do - it 'does not set a value' do - refute subject['controls'].first.key?('title') - end - end - end -end diff --git a/test/unit/utils/xccdf/to_xccdf_test.rb b/test/unit/utils/xccdf/to_xccdf_test.rb deleted file mode 100644 index d18b89e..0000000 --- a/test/unit/utils/xccdf/to_xccdf_test.rb +++ /dev/null @@ -1,288 +0,0 @@ -require_relative '../../test_helper' -require_relative '../../../../lib/utilities/xccdf/to_xccdf' -require_relative '../../../../lib/happy_mapper_tools/benchmark' - -describe Utils::ToXCCDF do - let(:dci) { Utils::ToXCCDF.new(attributes, inspec_data) } - let(:attributes) { {} } - let(:inspec_data) do - { 'controls' => controls } - end - - let(:controls) do - [ - { - 'results' => [ - { - 'run_time' => 0.000101, - 'start_time' => '2019-10-17T08:00:02-04:00' - }, - ] - }, - { - 'results' => [ - { - 'run_time' => 2.426861, - 'start_time' => '2019-10-17T08:00:04-04:00' - }, - { - 'run_time' => 2.0e-06, - 'start_time' => '2019-10-17T08:00:02-04:00' - }, - - ] - }, - ] - end - - describe '#run_end_time' do - it 'returns the latest time of all results' do - assert_equal '2019-10-17T08:00:06-04:00', dci.send(:run_end_time).to_s - end - end - - describe '#run_start_time' do - it 'returns the earliest start time of all results' do - assert_equal '2019-10-17T08:00:02-04:00', dci.send(:run_start_time).to_s - end - end - - describe '#build_benchmark_header' do - let(:subject) { dci.send(:build_benchmark_header) } - let(:attributes) { {} } - - describe 'when attribute benchmark.notice.id is not provided' do - it 'does not include notice on the benchmark' do - subject - benchmark = dci.instance_variable_get(:@benchmark) - assert_nil benchmark.notice - end - end - - describe 'when attribute benchmark.plaintext and benchmark.plaintext.id are not provided' do - it 'does not include notice on the benchmark' do - subject - benchmark = dci.instance_variable_get(:@benchmark) - assert_nil benchmark.plaintext - end - end - end - - describe '#build_groups' do - let(:subject) { dci.send(:build_groups) } - let(:attributes) { {} } - let(:controls) do - [ - { - 'id' => '1', - 'desc' => 'A description' - }, - ] - end - - describe 'when attribute content_ref.name and content_ref.href are not provided' do - it 'does not include Group::Rule::check::check-content-ref on the benchmark' do - subject - benchmark = dci.instance_variable_get(:@benchmark) - assert_nil benchmark.group.first.rule.check.content_ref - end - end - - describe 'when tag cci is not provided' do - it 'does not include Group::Rule::ident on the benchmark' do - subject - benchmark = dci.instance_variable_get(:@benchmark) - assert_nil benchmark.group.first.rule.ident - end - end - - describe 'when tag fixref is not provided' do - it 'does not include Group::Rule::fix on the benchmark' do - subject - benchmark = dci.instance_variable_get(:@benchmark) - assert_nil benchmark.group.first.rule.fix - end - end - - describe 'when tag gdescription is not provided' do - it 'does not include Group::description on the benchmark' do - subject - benchmark = dci.instance_variable_get(:@benchmark) - assert_nil benchmark.group.first.description - end - end - - describe 'when tag gdescription is provided' do - let(:controls) do - [ - { - 'id' => '1', - 'gdescription' => 'A test description', - 'desc' => 'A description' - }, - ] - end - - it 'wraps the data in XML tags' do - subject - benchmark = dci.instance_variable_get(:@benchmark) - assert_equal "#{controls.first['gdescription']}", benchmark.group.first.description - end - end - - describe 'when no reference attributes are provided' do - it 'does not include Group::Rule::fix on the benchmark' do - subject - benchmark = dci.instance_variable_get(:@benchmark) - assert_nil benchmark.group.first.rule.reference - end - end - end - - describe '#populate_target_facts' do - let(:subject) { dci.send(:populate_target_facts, benchmark_test_result, metadata) } - let(:benchmark_test_result) { HappyMapperTools::Benchmark::TestResult.new } - let(:metadata) { {} } - - describe 'when no facts are provided' do - it 'does not create the target_facts element' do - subject - assert_nil benchmark_test_result.target_facts - end - end - - describe 'when a mac address is defined' do - let(:metadata) { { 'mac' => '00:11:00' } } - - it 'sets a fact' do - subject - assert_equal '00:11:00', benchmark_test_result.target_facts.fact.first.fact - end - end - - describe 'when a ipv4 address is defined' do - let(:metadata) { { 'ip' => '192.168.0.1' } } - - it 'sets a fact' do - subject - assert_equal '192.168.0.1', benchmark_test_result.target_facts.fact.first.fact - end - end - - describe 'when a target address is defined' do - let(:metadata) { { 'ip' => '192.168.0.1' } } - - it 'sets a target' do - subject - assert_equal '192.168.0.1', benchmark_test_result.target_address - end - end - - describe 'when a target is defined' do - let(:metadata) { { 'fqdn' => 'some.host.local' } } - - it 'sets a target' do - subject - assert_equal 'some.host.local', benchmark_test_result.target - end - end - end - - describe '#populate_results' do - let(:subject) { dci.send(:populate_results, benchmark_test_result) } - let(:benchmark_test_result) { HappyMapperTools::Benchmark::TestResult.new } - describe 'when there is more than one test result for a control' do - let(:controls) do - [ - { - 'results' => [ - { - 'run_time' => 2.426861, - 'start_time' => '2019-10-17T08:00:04-04:00', - 'status' => 'failed', - 'code_desc' => 'File 1 should exist', - 'cci' => %w{ident_1 ident_2}, - 'message' => 'Can\'t find file: 1' - }, - { - 'run_time' => 2.0e-06, - 'start_time' => '2019-10-17T08:00:02-04:00', - 'status' => 'failed', - 'code_desc' => 'File 2 should exist', - 'cci' => %w{ident_1 ident_2}, - 'message' => 'Can\'t find file: 2' - }, - { - 'run_time' => 2.0e-06, - 'start_time' => '2019-10-17T08:00:02-04:00', - 'status' => 'passed' - }, - ] - }, - ] - end - - it 'consolidates the values into a single rule-result value' do - subject - assert_equal 1, benchmark_test_result.rule_result.size - assert_equal 'fail', benchmark_test_result.rule_result.first.result - end - end - end - - describe '#result_message' do - let(:subject) { dci.send(:result_message, result, xccdf_status) } - let(:xccdf_status) { 'pass' } - - describe 'when there is no message information' do - let(:result) { {} } - - it 'returns nil' do - assert_nil subject - end - end - - describe 'when there is message information' do - let(:xccdf_status) { 'fail' } - let(:result) do - { - 'status' => 'failed', - 'code_desc' => 'System Package firewalld should be installed', - 'message' => 'expected that `System Package firewalld` is installed' - } - end - - it 'returns a message' do - message = subject - assert_equal "System Package firewalld should be installed\n\nexpected that `System Package firewalld` is installed", message.message - assert_equal 'error', message.severity - end - end - end - - describe '#populate_identity' do - let(:subject) { dci.send(:populate_identity, benchmark_test_result, metadata) } - let(:benchmark_test_result) { HappyMapperTools::Benchmark::TestResult.new } - let(:metadata) { {} } - - describe 'when test_result.identity.identity is provided' do - let(:metadata) { { 'identity' => { 'identity' => 'some_user', 'privileged' => true } } } - - it 'sets the identity information' do - subject - assert benchmark_test_result.identity.authenticated - assert_equal 'some_user', benchmark_test_result.identity.identity - assert benchmark_test_result.identity.privileged - end - end - - describe 'when test_result.organization is provided' do - let(:metadata) { { 'organization' => 'MITRE Corporation' } } - - it 'sets the organization' do - subject - assert_equal 'MITRE Corporation', benchmark_test_result.organization - end - end - end -end