inspec_tools 2.0.5 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +21 -13
- data/Rakefile +82 -8
- data/lib/data/cis_to_nist_critical_controls +0 -0
- data/lib/data/cis_to_nist_mapping +0 -0
- data/lib/happy_mapper_tools/benchmark.rb +83 -0
- data/lib/happy_mapper_tools/stig_attributes.rb +3 -1
- data/lib/inspec_tools/csv.rb +42 -39
- data/lib/inspec_tools/generate_map.rb +35 -0
- data/lib/inspec_tools/inspec.rb +19 -91
- data/lib/inspec_tools/pdf.rb +2 -13
- data/lib/inspec_tools/plugin_cli.rb +22 -53
- data/lib/inspec_tools/summary.rb +108 -76
- data/lib/inspec_tools/xccdf.rb +1 -0
- data/lib/inspec_tools/xlsx_tool.rb +4 -16
- data/lib/utilities/cci_xml.rb +13 -0
- data/lib/utilities/cis_to_nist.rb +11 -0
- data/lib/utilities/inspec_util.rb +12 -76
- data/lib/utilities/mapping_validator.rb +10 -0
- data/lib/utilities/xccdf/from_inspec.rb +89 -0
- data/lib/utilities/xccdf/to_xccdf.rb +388 -0
- data/lib/utilities/xccdf/xccdf_score.rb +116 -0
- metadata +45 -40
- data/CHANGELOG.md +0 -724
- data/lib/data/NIST_Map_02052020_CIS_Controls_Version_7.1_Implementation_Groups_1.2.xlsx +0 -0
- data/lib/data/NIST_Map_09212017B_CSC-CIS_Critical_Security_Controls_VER_6.1_Excel_9.1.2016.xlsx +0 -0
- data/lib/utilities/extract_nist_cis_mapping.rb +0 -57
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            module InspecTools
         | 
| 2 | 
            +
              class GenerateMap
         | 
| 3 | 
            +
                attr_accessor :text
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize(text = nil)
         | 
| 6 | 
            +
                  @text = text.nil? ? default_text : text
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def generate_example(file)
         | 
| 10 | 
            +
                  File.write(file, @text)
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                private
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def default_text
         | 
| 16 | 
            +
                  <<~YML
         | 
| 17 | 
            +
                  # Setting csv_header to true will skip the csv file header
         | 
| 18 | 
            +
                  skip_csv_header: true
         | 
| 19 | 
            +
                  width   : 80
         | 
| 20 | 
            +
             | 
| 21 | 
            +
             | 
| 22 | 
            +
                  control.id: 0
         | 
| 23 | 
            +
                  control.title: 15
         | 
| 24 | 
            +
                  control.desc: 16
         | 
| 25 | 
            +
                  control.tags:
         | 
| 26 | 
            +
                    severity: 1
         | 
| 27 | 
            +
                    rid: 8
         | 
| 28 | 
            +
                    stig_id: 3
         | 
| 29 | 
            +
                    cci: 2
         | 
| 30 | 
            +
                    check: 12
         | 
| 31 | 
            +
                    fix: 10
         | 
| 32 | 
            +
                  YML
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
    
        data/lib/inspec_tools/inspec.rb
    CHANGED
    
    | @@ -9,17 +9,14 @@ require_relative '../happy_mapper_tools/stig_checklist' | |
| 9 9 | 
             
            require_relative '../happy_mapper_tools/benchmark'
         | 
| 10 10 | 
             
            require_relative '../utilities/inspec_util'
         | 
| 11 11 | 
             
            require_relative 'csv'
         | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
            # rubocop:disable Metrics/AbcSize
         | 
| 15 | 
            -
            # rubocop:disable Metrics/BlockLength
         | 
| 16 | 
            -
            # rubocop:disable Style/GuardClause
         | 
| 12 | 
            +
            require_relative '../utilities/xccdf/from_inspec'
         | 
| 13 | 
            +
            require_relative '../utilities/xccdf/to_xccdf'
         | 
| 17 14 |  | 
| 18 15 | 
             
            module InspecTools
         | 
| 19 | 
            -
              class Inspec
         | 
| 20 | 
            -
                def initialize(inspec_json, metadata =  | 
| 16 | 
            +
              class Inspec # rubocop:disable Metrics/ClassLength
         | 
| 17 | 
            +
                def initialize(inspec_json, metadata = {})
         | 
| 21 18 | 
             
                  @json = JSON.parse(inspec_json.gsub(/\\+u0000/, ''))
         | 
| 22 | 
            -
                  @metadata =  | 
| 19 | 
            +
                  @metadata = metadata
         | 
| 23 20 | 
             
                end
         | 
| 24 21 |  | 
| 25 22 | 
             
                def to_ckl(title = nil, date = nil, cklist = nil)
         | 
| @@ -36,16 +33,15 @@ module InspecTools | |
| 36 33 | 
             
                  @checklist.to_xml.encode('UTF-8').gsub('<?xml version="1.0"?>', '<?xml version="1.0" encoding="UTF-8"?>').chomp
         | 
| 37 34 | 
             
                end
         | 
| 38 35 |  | 
| 36 | 
            +
                # Convert Inspec result data to XCCDF
         | 
| 37 | 
            +
                #
         | 
| 38 | 
            +
                # @param attributes [Hash] Optional input attributes
         | 
| 39 | 
            +
                # @return [String] XML formatted String
         | 
| 39 40 | 
             
                def to_xccdf(attributes, verbose = false)
         | 
| 40 | 
            -
                   | 
| 41 | 
            -
                  @attribute = attributes
         | 
| 42 | 
            -
                  @attribute = {} if @attribute.eql? false
         | 
| 41 | 
            +
                  data = Utils::FromInspec.new.parse_data_for_xccdf(@json)
         | 
| 43 42 | 
             
                  @verbose = verbose
         | 
| 44 | 
            -
             | 
| 45 | 
            -
                   | 
| 46 | 
            -
                  # populate_profiles @todo populate profiles; not implemented now because its use is deprecated
         | 
| 47 | 
            -
                  populate_groups
         | 
| 48 | 
            -
                  @benchmark.to_xml
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  Utils::ToXCCDF.new(attributes || {}, data).to_xml(@metadata)
         | 
| 49 45 | 
             
                end
         | 
| 50 46 |  | 
| 51 47 | 
             
                ####
         | 
| @@ -70,7 +66,7 @@ module InspecTools | |
| 70 66 | 
             
                #
         | 
| 71 67 | 
             
                # @param inspec_json : an inspec profile formatted as a json object
         | 
| 72 68 | 
             
                ###
         | 
| 73 | 
            -
                def inspec_json_to_array(inspec_json)
         | 
| 69 | 
            +
                def inspec_json_to_array(inspec_json) # rubocop:disable Metrics/CyclomaticComplexity
         | 
| 74 70 | 
             
                  data = []
         | 
| 75 71 | 
             
                  headers = {}
         | 
| 76 72 | 
             
                  inspec_json['controls'].each do |control|
         | 
| @@ -97,10 +93,11 @@ module InspecTools | |
| 97 93 | 
             
                      @data['controls'] << control
         | 
| 98 94 | 
             
                    end
         | 
| 99 95 | 
             
                  end
         | 
| 100 | 
            -
             | 
| 101 | 
            -
             | 
| 102 | 
            -
             | 
| 103 | 
            -
             | 
| 96 | 
            +
             | 
| 97 | 
            +
                  return unless json['profiles'].nil?
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                  json['controls'].each do |control|
         | 
| 100 | 
            +
                    @data['controls'] << control
         | 
| 104 101 | 
             
                  end
         | 
| 105 102 | 
             
                end
         | 
| 106 103 |  | 
| @@ -161,7 +158,7 @@ module InspecTools | |
| 161 158 | 
             
                  vuln
         | 
| 162 159 | 
             
                end
         | 
| 163 160 |  | 
| 164 | 
            -
                def generate_asset
         | 
| 161 | 
            +
                def generate_asset # rubocop:disable Metrics/AbcSize
         | 
| 165 162 | 
             
                  asset = HappyMapperTools::StigChecklist::Asset.new
         | 
| 166 163 | 
             
                  asset.role = !@metadata['role'].nil? ? @metadata['role'] : 'Workstation'
         | 
| 167 164 | 
             
                  asset.type = !@metadata['type'].nil? ? @metadata['type'] : 'Computing'
         | 
| @@ -223,75 +220,6 @@ module InspecTools | |
| 223 220 | 
             
                  ip
         | 
| 224 221 | 
             
                end
         | 
| 225 222 |  | 
| 226 | 
            -
                def populate_header
         | 
| 227 | 
            -
                  @benchmark.title = @attribute['benchmark.title']
         | 
| 228 | 
            -
                  @benchmark.id = @attribute['benchmark.id']
         | 
| 229 | 
            -
                  @benchmark.description = @attribute['benchmark.description']
         | 
| 230 | 
            -
                  @benchmark.version = @attribute['benchmark.version']
         | 
| 231 | 
            -
             | 
| 232 | 
            -
                  @benchmark.status = HappyMapperTools::Benchmark::Status.new
         | 
| 233 | 
            -
                  @benchmark.status.status = @attribute['benchmark.status']
         | 
| 234 | 
            -
                  @benchmark.status.date = @attribute['benchmark.status.date']
         | 
| 235 | 
            -
             | 
| 236 | 
            -
                  @benchmark.notice = HappyMapperTools::Benchmark::Notice.new
         | 
| 237 | 
            -
                  @benchmark.notice.id = @attribute['benchmark.notice.id']
         | 
| 238 | 
            -
             | 
| 239 | 
            -
                  @benchmark.plaintext = HappyMapperTools::Benchmark::Plaintext.new
         | 
| 240 | 
            -
                  @benchmark.plaintext.plaintext = @attribute['benchmark.plaintext']
         | 
| 241 | 
            -
                  @benchmark.plaintext.id = @attribute['benchmark.plaintext.id']
         | 
| 242 | 
            -
             | 
| 243 | 
            -
                  @benchmark.reference = HappyMapperTools::Benchmark::ReferenceBenchmark.new
         | 
| 244 | 
            -
                  @benchmark.reference.href = @attribute['reference.href']
         | 
| 245 | 
            -
                  @benchmark.reference.dc_publisher = @attribute['reference.dc.publisher']
         | 
| 246 | 
            -
                  @benchmark.reference.dc_source = @attribute['reference.dc.source']
         | 
| 247 | 
            -
                end
         | 
| 248 | 
            -
             | 
| 249 | 
            -
                def populate_groups
         | 
| 250 | 
            -
                  group_array = []
         | 
| 251 | 
            -
                  @data['controls'].each do |control|
         | 
| 252 | 
            -
                    group = HappyMapperTools::Benchmark::Group.new
         | 
| 253 | 
            -
                    group.id = control['id']
         | 
| 254 | 
            -
                    group.title = control['gtitle']
         | 
| 255 | 
            -
                    group.description = "<GroupDescription>#{control['gdescription']}</GroupDescription>"
         | 
| 256 | 
            -
             | 
| 257 | 
            -
                    group.rule = HappyMapperTools::Benchmark::Rule.new
         | 
| 258 | 
            -
                    group.rule.id = control['rid']
         | 
| 259 | 
            -
                    group.rule.severity = control['severity']
         | 
| 260 | 
            -
                    group.rule.weight = control['rweight']
         | 
| 261 | 
            -
                    group.rule.version = control['rversion']
         | 
| 262 | 
            -
                    group.rule.title = control['title'].tr("\n", ' ')
         | 
| 263 | 
            -
                    group.rule.description = "<VulnDiscussion>#{control['desc'].tr("\n", ' ')}</VulnDiscussion><FalsePositives></FalsePositives><FalseNegatives></FalseNegatives><Documentable>false</Documentable><Mitigations></Mitigations><SeverityOverrideGuidance></SeverityOverrideGuidance><PotentialImpacts></PotentialImpacts><ThirdPartyTools></ThirdPartyTools><MitigationControl></MitigationControl><Responsibility></Responsibility><IAControls></IAControls>"
         | 
| 264 | 
            -
             | 
| 265 | 
            -
                    group.rule.reference = HappyMapperTools::Benchmark::ReferenceGroup.new
         | 
| 266 | 
            -
                    group.rule.reference.dc_publisher = @attribute['reference.dc.publisher']
         | 
| 267 | 
            -
                    group.rule.reference.dc_title = @attribute['reference.dc.title']
         | 
| 268 | 
            -
                    group.rule.reference.dc_subject = @attribute['reference.dc.subject']
         | 
| 269 | 
            -
                    group.rule.reference.dc_type = @attribute['reference.dc.type']
         | 
| 270 | 
            -
                    group.rule.reference.dc_identifier = @attribute['reference.dc.identifier']
         | 
| 271 | 
            -
             | 
| 272 | 
            -
                    group.rule.ident = HappyMapperTools::Benchmark::Ident.new
         | 
| 273 | 
            -
                    group.rule.ident.system = 'https://public.cyber.mil/stigs/cci/'
         | 
| 274 | 
            -
                    group.rule.ident.ident = control['cci']
         | 
| 275 | 
            -
             | 
| 276 | 
            -
                    group.rule.fixtext = HappyMapperTools::Benchmark::Fixtext.new
         | 
| 277 | 
            -
                    group.rule.fixtext.fixref = control['fixref']
         | 
| 278 | 
            -
                    group.rule.fixtext.fixtext = control['fix']
         | 
| 279 | 
            -
             | 
| 280 | 
            -
                    group.rule.fix = HappyMapperTools::Benchmark::Fix.new
         | 
| 281 | 
            -
                    group.rule.fix.id = control['fixref']
         | 
| 282 | 
            -
             | 
| 283 | 
            -
                    group.rule.check = HappyMapperTools::Benchmark::Check.new
         | 
| 284 | 
            -
                    group.rule.check.system = control['checkref']
         | 
| 285 | 
            -
                    group.rule.check.content_ref = HappyMapperTools::Benchmark::ContentRef.new
         | 
| 286 | 
            -
                    group.rule.check.content_ref.name = @attribute['content_ref.name']
         | 
| 287 | 
            -
                    group.rule.check.content_ref.href = @attribute['content_ref.href']
         | 
| 288 | 
            -
                    group.rule.check.content = control['check']
         | 
| 289 | 
            -
             | 
| 290 | 
            -
                    group_array << group
         | 
| 291 | 
            -
                  end
         | 
| 292 | 
            -
                  @benchmark.group = group_array
         | 
| 293 | 
            -
                end
         | 
| 294 | 
            -
             | 
| 295 223 | 
             
                def generate_title(title, json, date)
         | 
| 296 224 | 
             
                  title ||= "Untitled - Checklist Created from Automated InSpec Results JSON; Profiles: #{json['profiles'].map { |x| x['name'] }.join(' | ')}"
         | 
| 297 225 | 
             
                  title + " Checklist Date: #{date || Date.today.to_s}"
         | 
    
        data/lib/inspec_tools/pdf.rb
    CHANGED
    
    | @@ -2,9 +2,9 @@ require 'digest' | |
| 2 2 |  | 
| 3 3 | 
             
            require_relative '../utilities/inspec_util'
         | 
| 4 4 | 
             
            require_relative '../utilities/extract_pdf_text'
         | 
| 5 | 
            -
            require_relative '../utilities/extract_nist_cis_mapping'
         | 
| 6 5 | 
             
            require_relative '../utilities/parser'
         | 
| 7 6 | 
             
            require_relative '../utilities/text_cleaner'
         | 
| 7 | 
            +
            require_relative '../utilities/cis_to_nist'
         | 
| 8 8 |  | 
| 9 9 | 
             
            # rubocop:disable Metrics/AbcSize
         | 
| 10 10 | 
             
            # rubocop:disable Metrics/PerceivedComplexity
         | 
| @@ -24,7 +24,7 @@ module InspecTools | |
| 24 24 | 
             
                  @controls = []
         | 
| 25 25 | 
             
                  @csv_handle = nil
         | 
| 26 26 | 
             
                  @cci_xml = nil
         | 
| 27 | 
            -
                  @nist_mapping =  | 
| 27 | 
            +
                  @nist_mapping = Utils::CisToNist.get_mapping('cis_to_nist_critical_controls')
         | 
| 28 28 | 
             
                  @pdf_text = ''
         | 
| 29 29 | 
             
                  @clean_text = ''
         | 
| 30 30 | 
             
                  @transformed_data = ''
         | 
| @@ -33,7 +33,6 @@ module InspecTools | |
| 33 33 | 
             
                  @title ||= extract_title
         | 
| 34 34 | 
             
                  clean_pdf_text
         | 
| 35 35 | 
             
                  transform_data
         | 
| 36 | 
            -
                  read_excl
         | 
| 37 36 | 
             
                  insert_json_metadata
         | 
| 38 37 | 
             
                  @profile['controls'] = parse_controls
         | 
| 39 38 | 
             
                  @profile['sha256'] = Digest::SHA256.hexdigest @profile.to_s
         | 
| @@ -122,15 +121,5 @@ module InspecTools | |
| 122 121 | 
             
                def write_clean_text
         | 
| 123 122 | 
             
                  File.write('debug_text', @clean_text)
         | 
| 124 123 | 
             
                end
         | 
| 125 | 
            -
             | 
| 126 | 
            -
                def read_excl
         | 
| 127 | 
            -
                  nist_map_path = File.join(File.dirname(__FILE__), '../data/NIST_Map_09212017B_CSC-CIS_Critical_Security_Controls_VER_6.1_Excel_9.1.2016.xlsx')
         | 
| 128 | 
            -
                  excel = Util::ExtractNistMappings.new(nist_map_path)
         | 
| 129 | 
            -
                  @nist_mapping = excel.full_excl
         | 
| 130 | 
            -
                rescue StandardError => e
         | 
| 131 | 
            -
                  puts "Exception: #{e.message}"
         | 
| 132 | 
            -
                  puts 'Existing...'
         | 
| 133 | 
            -
                  exit
         | 
| 134 | 
            -
                end
         | 
| 135 124 | 
             
              end
         | 
| 136 125 | 
             
            end
         | 
| @@ -1,4 +1,3 @@ | |
| 1 | 
            -
            require 'yaml'
         | 
| 2 1 | 
             
            require 'json'
         | 
| 3 2 | 
             
            require 'roo'
         | 
| 4 3 | 
             
            require_relative '../utilities/inspec_util'
         | 
| @@ -13,8 +12,8 @@ module InspecTools | |
| 13 12 | 
             
              autoload :CKL, 'inspec_tools/ckl'
         | 
| 14 13 | 
             
              autoload :Inspec, 'inspec_tools/inspec'
         | 
| 15 14 | 
             
              autoload :Summary, 'inspec_tools/summary'
         | 
| 16 | 
            -
              autoload :Threshold, 'inspec_tools/threshold'
         | 
| 17 15 | 
             
              autoload :XLSXTool, 'inspec_tools/xlsx_tool'
         | 
| 16 | 
            +
              autoload :GenerateMap, 'inspec_tools/generate_map'
         | 
| 18 17 | 
             
            end
         | 
| 19 18 |  | 
| 20 19 | 
             
            # rubocop:disable Style/GuardClause
         | 
| @@ -23,7 +22,7 @@ module InspecPlugins | |
| 23 22 | 
             
                class CliCommand < Inspec.plugin(2, :cli_command) # rubocop:disable Metrics/ClassLength
         | 
| 24 23 | 
             
                  POSSIBLE_LOG_LEVELS = %w{debug info warn error fatal}.freeze
         | 
| 25 24 |  | 
| 26 | 
            -
                  class_option :log_directory, type: :string, aliases: :l, desc: ' | 
| 25 | 
            +
                  class_option :log_directory, type: :string, aliases: :l, desc: 'Provide log location'
         | 
| 27 26 | 
             
                  class_option :log_level, type: :string, desc: "Set the logging level: #{POSSIBLE_LOG_LEVELS}"
         | 
| 28 27 |  | 
| 29 28 | 
             
                  subcommand_desc 'tools [COMMAND]', 'Runs inspec_tools commands through Inspec'
         | 
| @@ -54,12 +53,18 @@ module InspecPlugins | |
| 54 53 |  | 
| 55 54 | 
             
                  desc 'inspec2xccdf', 'inspec2xccdf translates an inspec profile and attributes files to an xccdf file'
         | 
| 56 55 | 
             
                  long_desc InspecTools::Help.text(:inspec2xccdf)
         | 
| 57 | 
            -
                  option :inspec_json, required: true, aliases: '-j'
         | 
| 58 | 
            -
             | 
| 59 | 
            -
                  option : | 
| 56 | 
            +
                  option :inspec_json, required: true, aliases: '-j',
         | 
| 57 | 
            +
                                       desc: 'path to InSpec JSON file created'
         | 
| 58 | 
            +
                  option :attributes,  required: true, aliases: '-a',
         | 
| 59 | 
            +
                                       desc: 'path to yml file that provides the required attributes for the XCCDF document. These attributes are parts of XCCDF document which do not fit into the InSpec schema.'
         | 
| 60 | 
            +
                  option :output, required: true, aliases: '-o',
         | 
| 61 | 
            +
                                  desc: 'name or path to create the XCCDF and title to give the XCCDF'
         | 
| 62 | 
            +
                  option :metadata, required: false, type: :string, aliases: '-m',
         | 
| 63 | 
            +
                                    desc: 'path to JSON file with additional host metadata for the XCCDF file'
         | 
| 60 64 | 
             
                  def inspec2xccdf
         | 
| 61 65 | 
             
                    json = File.read(options[:inspec_json])
         | 
| 62 | 
            -
                     | 
| 66 | 
            +
                    metadata = options[:metadata] ? JSON.parse(File.read(options[:metadata])) : {}
         | 
| 67 | 
            +
                    inspec_tool = InspecTools::Inspec.new(json, metadata)
         | 
| 63 68 | 
             
                    attr_hsh = YAML.load_file(options[:attributes])
         | 
| 64 69 | 
             
                    xccdf = inspec_tool.to_xccdf(attr_hsh)
         | 
| 65 70 | 
             
                    File.write(options[:output], xccdf)
         | 
| @@ -73,10 +78,11 @@ module InspecPlugins | |
| 73 78 | 
             
                  option :output, required: false, aliases: '-o', default: 'profile'
         | 
| 74 79 | 
             
                  option :format, required: false, aliases: '-f', enum: %w{ruby hash}, default: 'ruby'
         | 
| 75 80 | 
             
                  option :separate_files, required: false, type: :boolean, default: true, aliases: '-s'
         | 
| 81 | 
            +
                  option :control_name_prefix, required: false, type: :string, aliases: '-p'
         | 
| 76 82 | 
             
                  def csv2inspec
         | 
| 77 83 | 
             
                    csv = CSV.read(options[:csv], encoding: 'ISO8859-1')
         | 
| 78 84 | 
             
                    mapping = YAML.load_file(options[:mapping])
         | 
| 79 | 
            -
                    profile = InspecTools::CSVTool.new(csv, mapping, options[:csv].split('/')[-1].split('.')[0], options[:verbose]).to_inspec
         | 
| 85 | 
            +
                    profile = InspecTools::CSVTool.new(csv, mapping, options[:csv].split('/')[-1].split('.')[0], options[:verbose]).to_inspec(control_name_prefix: options[:control_name_prefix])
         | 
| 80 86 | 
             
                    Utils::InspecUtil.unpack_inspec_json(options[:output], profile, options[:separate_files], options[:format])
         | 
| 81 87 | 
             
                  end
         | 
| 82 88 |  | 
| @@ -136,26 +142,8 @@ module InspecPlugins | |
| 136 142 |  | 
| 137 143 | 
             
                  desc 'generate_map', 'Generates mapping template from CSV to Inspec Controls'
         | 
| 138 144 | 
             
                  def generate_map
         | 
| 139 | 
            -
                     | 
| 140 | 
            -
                     | 
| 141 | 
            -
                    skip_csv_header: true
         | 
| 142 | 
            -
                    width   : 80
         | 
| 143 | 
            -
             | 
| 144 | 
            -
             | 
| 145 | 
            -
                    control.id: 0
         | 
| 146 | 
            -
                    control.title: 15
         | 
| 147 | 
            -
                    control.desc: 16
         | 
| 148 | 
            -
                    control.tags:
         | 
| 149 | 
            -
                            severity: 1
         | 
| 150 | 
            -
                            rid: 8
         | 
| 151 | 
            -
                            stig_id: 3
         | 
| 152 | 
            -
                            cci: 2
         | 
| 153 | 
            -
                            check: 12
         | 
| 154 | 
            -
                            fix: 10
         | 
| 155 | 
            -
                    '
         | 
| 156 | 
            -
                    myfile = File.new('mapping.yml', 'w')
         | 
| 157 | 
            -
                    myfile.puts template
         | 
| 158 | 
            -
                    myfile.close
         | 
| 145 | 
            +
                    generator = InspecTools::GenerateMap.new
         | 
| 146 | 
            +
                    generator.generate_example('mapping.yml')
         | 
| 159 147 | 
             
                  end
         | 
| 160 148 |  | 
| 161 149 | 
             
                  desc 'generate_ckl_metadata', 'Generate metadata file that can be passed to inspec2ckl'
         | 
| @@ -200,26 +188,14 @@ module InspecPlugins | |
| 200 188 | 
             
                  desc 'summary', 'summary parses an inspec results json to create a summary json'
         | 
| 201 189 | 
             
                  long_desc InspecTools::Help.text(:summary)
         | 
| 202 190 | 
             
                  option :inspec_json, required: true, aliases: '-j'
         | 
| 203 | 
            -
                  option :verbose, type: :boolean, aliases: '-V'
         | 
| 204 191 | 
             
                  option :json_full, type: :boolean, required: false, aliases: '-f'
         | 
| 205 192 | 
             
                  option :json_counts, type: :boolean, required: false, aliases: '-k'
         | 
| 193 | 
            +
                  option :threshold_file, required: false, aliases: '-t'
         | 
| 194 | 
            +
                  option :threshold_inline, required: false, aliases: '-i'
         | 
| 206 195 |  | 
| 207 196 | 
             
                  def summary
         | 
| 208 | 
            -
                    summary = InspecTools::Summary.new( | 
| 209 | 
            -
             | 
| 210 | 
            -
                    unless options.include?('json_full') || options.include?('json_counts')
         | 
| 211 | 
            -
                      puts "\nOverall compliance: #{summary[:compliance]}%\n\n"
         | 
| 212 | 
            -
                      summary[:status].keys.each do |category|
         | 
| 213 | 
            -
                        puts category
         | 
| 214 | 
            -
                        summary[:status][category].keys.each do |impact|
         | 
| 215 | 
            -
                          puts "\t#{impact} : #{summary[:status][category][impact]}"
         | 
| 216 | 
            -
                        end
         | 
| 217 | 
            -
                      end
         | 
| 218 | 
            -
                    end
         | 
| 219 | 
            -
             | 
| 220 | 
            -
                    json_summary = summary.to_json
         | 
| 221 | 
            -
                    puts json_summary if options[:json_full]
         | 
| 222 | 
            -
                    puts summary[:status].to_json if options[:json_counts]
         | 
| 197 | 
            +
                    summary = InspecTools::Summary.new(options: options)
         | 
| 198 | 
            +
                    summary.output_summary
         | 
| 223 199 | 
             
                  end
         | 
| 224 200 |  | 
| 225 201 | 
             
                  desc 'compliance', 'compliance parses an inspec results json to check if the compliance level meets a specified threshold'
         | 
| @@ -227,17 +203,10 @@ module InspecPlugins | |
| 227 203 | 
             
                  option :inspec_json, required: true, aliases: '-j'
         | 
| 228 204 | 
             
                  option :threshold_file, required: false, aliases: '-f'
         | 
| 229 205 | 
             
                  option :threshold_inline, required: false, aliases: '-i'
         | 
| 230 | 
            -
                  option :verbose, type: :boolean, aliases: '-V'
         | 
| 231 206 |  | 
| 232 207 | 
             
                  def compliance
         | 
| 233 | 
            -
                     | 
| 234 | 
            -
             | 
| 235 | 
            -
                      exit(1)
         | 
| 236 | 
            -
                    end
         | 
| 237 | 
            -
                    threshold = YAML.load_file(options[:threshold_file]) unless options[:threshold_file].nil?
         | 
| 238 | 
            -
                    threshold = YAML.safe_load(options[:threshold_inline]) unless options[:threshold_inline].nil?
         | 
| 239 | 
            -
                    compliance = InspecTools::Summary.new(File.read(options[:inspec_json])).threshold(threshold)
         | 
| 240 | 
            -
                    compliance ? exit(0) : exit(1)
         | 
| 208 | 
            +
                    compliance = InspecTools::Summary.new(options: options)
         | 
| 209 | 
            +
                    compliance.results_meet_threshold? ? exit(0) : exit(1)
         | 
| 241 210 | 
             
                  end
         | 
| 242 211 | 
             
                end
         | 
| 243 212 | 
             
              end
         | 
    
        data/lib/inspec_tools/summary.rb
    CHANGED
    
    | @@ -2,8 +2,6 @@ require 'json' | |
| 2 2 | 
             
            require 'yaml'
         | 
| 3 3 | 
             
            require_relative '../utilities/inspec_util'
         | 
| 4 4 |  | 
| 5 | 
            -
            # rubocop:disable Metrics/AbcSize
         | 
| 6 | 
            -
             | 
| 7 5 | 
             
            # Impact Definitions
         | 
| 8 6 | 
             
            CRITICAL = 0.9
         | 
| 9 7 | 
             
            HIGH = 0.7
         | 
| @@ -16,48 +14,118 @@ TALLYS = %i(total critical high medium low).freeze | |
| 16 14 | 
             
            THRESHOLD_TEMPLATE = File.expand_path('../data/threshold.yaml', File.dirname(__FILE__))
         | 
| 17 15 |  | 
| 18 16 | 
             
            module InspecTools
         | 
| 17 | 
            +
              # rubocop:disable Metrics/ClassLength
         | 
| 19 18 | 
             
              class Summary
         | 
| 20 | 
            -
                 | 
| 21 | 
            -
             | 
| 19 | 
            +
                attr_reader :json
         | 
| 20 | 
            +
                attr_reader :json_full
         | 
| 21 | 
            +
                attr_reader :json_counts
         | 
| 22 | 
            +
                attr_reader :threshold_file
         | 
| 23 | 
            +
                attr_reader :threshold_inline
         | 
| 24 | 
            +
                attr_reader :summary
         | 
| 25 | 
            +
                attr_reader :threshold
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def initialize(**options)
         | 
| 28 | 
            +
                  options = options[:options]
         | 
| 29 | 
            +
                  @json = JSON.parse(File.read(options[:inspec_json]))
         | 
| 30 | 
            +
                  @json_full = false || options[:json_full]
         | 
| 31 | 
            +
                  @json_counts = false || options[:json_counts]
         | 
| 32 | 
            +
                  @threshold = parse_threshold(options[:threshold_inline], options[:threshold_file])
         | 
| 33 | 
            +
                  @threshold_provided = options[:threshold_inline] || options[:threshold_file]
         | 
| 34 | 
            +
                  @summary = compute_summary
         | 
| 22 35 | 
             
                end
         | 
| 23 36 |  | 
| 24 | 
            -
                def  | 
| 25 | 
            -
                  @ | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
                     | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 37 | 
            +
                def output_summary
         | 
| 38 | 
            +
                  unless @json_full || @json_counts
         | 
| 39 | 
            +
                    puts "\nThreshold compliance: #{@threshold['compliance.min']}%"
         | 
| 40 | 
            +
                    puts "\nOverall compliance: #{@summary[:compliance]}%\n\n"
         | 
| 41 | 
            +
                    @summary[:status].keys.each do |category|
         | 
| 42 | 
            +
                      puts category
         | 
| 43 | 
            +
                      @summary[:status][category].keys.each do |impact|
         | 
| 44 | 
            +
                        puts "\t#{impact} : #{@summary[:status][category][impact]}"
         | 
| 45 | 
            +
                      end
         | 
| 46 | 
            +
                    end
         | 
| 31 47 | 
             
                  end
         | 
| 32 | 
            -
             | 
| 33 | 
            -
                  @summary
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  puts @summary.to_json if @json_full
         | 
| 50 | 
            +
                  puts @summary[:status].to_json if @json_counts
         | 
| 34 51 | 
             
                end
         | 
| 35 52 |  | 
| 36 | 
            -
                def  | 
| 37 | 
            -
                   | 
| 38 | 
            -
             | 
| 39 | 
            -
                   | 
| 40 | 
            -
                   | 
| 53 | 
            +
                def results_meet_threshold?
         | 
| 54 | 
            +
                  raise 'Please provide threshold as a yaml file or inline yaml' unless @threshold_provided
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  compliance = true
         | 
| 57 | 
            +
                  failure = []
         | 
| 58 | 
            +
                  failure << check_max_compliance(@threshold['compliance.max'], @summary[:compliance], '', 'compliance')
         | 
| 59 | 
            +
                  failure << check_min_compliance(@threshold['compliance.min'], @summary[:compliance], '', 'compliance')
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  BUCKETS.each do |bucket|
         | 
| 62 | 
            +
                    TALLYS.each do |tally|
         | 
| 63 | 
            +
                      failure << check_min_compliance(@threshold["#{bucket}.#{tally}.min"], @summary[:status][bucket][tally], bucket, tally)
         | 
| 64 | 
            +
                      failure << check_max_compliance(@threshold["#{bucket}.#{tally}.max"], @summary[:status][bucket][tally], bucket, tally)
         | 
| 65 | 
            +
                    end
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  failure.reject!(&:nil?)
         | 
| 69 | 
            +
                  compliance = false if failure.length.positive?
         | 
| 70 | 
            +
                  output(compliance, failure)
         | 
| 71 | 
            +
                  compliance
         | 
| 41 72 | 
             
                end
         | 
| 42 73 |  | 
| 43 74 | 
             
                private
         | 
| 44 75 |  | 
| 76 | 
            +
                def check_min_compliance(min, data, bucket, tally)
         | 
| 77 | 
            +
                  expected_to_string(bucket, tally, 'min', min, data) if min != -1 and data < min
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                def check_max_compliance(max, data, bucket, tally)
         | 
| 81 | 
            +
                  expected_to_string(bucket, tally, 'max', max, data) if max != -1 and data > max
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                def output(passed_threshold, what_failed)
         | 
| 85 | 
            +
                  if passed_threshold
         | 
| 86 | 
            +
                    puts "Overall compliance threshold of #{@threshold['compliance.min']}\% met. Current compliance at #{@summary[:compliance]}\%"
         | 
| 87 | 
            +
                  else
         | 
| 88 | 
            +
                    puts 'Compliance threshold was not met: '
         | 
| 89 | 
            +
                    puts what_failed.join("\n")
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                def expected_to_string(bucket, tally, maxmin, value, got)
         | 
| 94 | 
            +
                  return "Expected #{bucket}.#{tally}.#{maxmin}:#{value} got:#{got}" unless bucket.empty? || bucket.nil?
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                  "Expected #{tally}.#{maxmin}:#{value}\% got:#{got}\%"
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                def parse_threshold(threshold_inline, threshold_file)
         | 
| 100 | 
            +
                  threshold = Utils::InspecUtil.to_dotted_hash(YAML.load_file(THRESHOLD_TEMPLATE))
         | 
| 101 | 
            +
                  threshold.merge!(Utils::InspecUtil.to_dotted_hash(YAML.load_file(threshold_file))) if threshold_file
         | 
| 102 | 
            +
                  threshold.merge!(Utils::InspecUtil.to_dotted_hash(YAML.safe_load(threshold_inline))) if threshold_inline
         | 
| 103 | 
            +
                  threshold
         | 
| 104 | 
            +
                end
         | 
| 105 | 
            +
             | 
| 45 106 | 
             
                def compute_summary
         | 
| 46 | 
            -
                   | 
| 47 | 
            -
             | 
| 48 | 
            -
                   | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
                   | 
| 55 | 
            -
                   | 
| 56 | 
            -
                   | 
| 57 | 
            -
                   | 
| 58 | 
            -
                   | 
| 59 | 
            -
             | 
| 60 | 
            -
                   | 
| 107 | 
            +
                  data = Utils::InspecUtil.parse_data_for_ckl(@json)
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                  data.keys.each do |control_id|
         | 
| 110 | 
            +
                    current_control = data[control_id]
         | 
| 111 | 
            +
                    current_control[:compliance_status] = Utils::InspecUtil.control_status(current_control, true)
         | 
| 112 | 
            +
                    current_control[:finding_details] = Utils::InspecUtil.control_finding_details(current_control, current_control[:compliance_status])
         | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                  summary = {}
         | 
| 116 | 
            +
                  summary[:buckets] = {}
         | 
| 117 | 
            +
                  summary[:buckets][:failed]    = select_by_status(data, 'Open')
         | 
| 118 | 
            +
                  summary[:buckets][:passed]    = select_by_status(data, 'NotAFinding')
         | 
| 119 | 
            +
                  summary[:buckets][:no_impact] = select_by_status(data, 'Not_Applicable')
         | 
| 120 | 
            +
                  summary[:buckets][:skipped]   = select_by_status(data, 'Not_Reviewed')
         | 
| 121 | 
            +
                  summary[:buckets][:error]     = select_by_status(data, 'Profile_Error')
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  summary[:status] = {}
         | 
| 124 | 
            +
                  %i(failed passed no_impact skipped error).each do |key|
         | 
| 125 | 
            +
                    summary[:status][key] = tally_by_impact(summary[:buckets][key])
         | 
| 126 | 
            +
                  end
         | 
| 127 | 
            +
                  summary[:compliance] = compute_compliance(summary)
         | 
| 128 | 
            +
                  summary
         | 
| 61 129 | 
             
                end
         | 
| 62 130 |  | 
| 63 131 | 
             
                def select_by_impact(controls, impact)
         | 
| @@ -78,49 +146,13 @@ module InspecTools | |
| 78 146 | 
             
                  tally
         | 
| 79 147 | 
             
                end
         | 
| 80 148 |  | 
| 81 | 
            -
                def compute_compliance
         | 
| 82 | 
            -
                  ( | 
| 83 | 
            -
                    ( | 
| 84 | 
            -
                      | 
| 85 | 
            -
                      | 
| 86 | 
            -
                      | 
| 87 | 
            -
                end
         | 
| 88 | 
            -
             | 
| 89 | 
            -
                def threshold_compliance
         | 
| 90 | 
            -
                  compliance = true
         | 
| 91 | 
            -
                  failure = []
         | 
| 92 | 
            -
                  max = @threshold['compliance.max']
         | 
| 93 | 
            -
                  min = @threshold['compliance.min']
         | 
| 94 | 
            -
                  if max != -1 and @summary[:compliance] > max
         | 
| 95 | 
            -
                    compliance = false
         | 
| 96 | 
            -
                    failure << "Expected compliance.max:#{max} got:#{@summary[:compliance]}"
         | 
| 97 | 
            -
                  end
         | 
| 98 | 
            -
                  if min != -1 and @summary[:compliance] < min
         | 
| 99 | 
            -
                    compliance = false
         | 
| 100 | 
            -
                    failure << "Expected compliance.min:#{min} got:#{@summary[:compliance]}"
         | 
| 101 | 
            -
                  end
         | 
| 102 | 
            -
                  status = @summary[:status]
         | 
| 103 | 
            -
                  BUCKETS.each do |bucket|
         | 
| 104 | 
            -
                    TALLYS.each do |tally|
         | 
| 105 | 
            -
                      max = @threshold["#{bucket}.#{tally}.max"]
         | 
| 106 | 
            -
                      min = @threshold["#{bucket}.#{tally}.min"]
         | 
| 107 | 
            -
                      if max != -1 and status[bucket][tally] > max
         | 
| 108 | 
            -
                        compliance = false
         | 
| 109 | 
            -
                        failure << "Expected #{bucket}.#{tally}.max:#{max} got:#{status[bucket][tally]}"
         | 
| 110 | 
            -
                      end
         | 
| 111 | 
            -
                      if min != -1 and status[bucket][tally] < min
         | 
| 112 | 
            -
                        compliance = false
         | 
| 113 | 
            -
                        failure << "Expected #{bucket}.#{tally}.min:#{min} got:#{status[bucket][tally]}"
         | 
| 114 | 
            -
                      end
         | 
| 115 | 
            -
                    end
         | 
| 116 | 
            -
                  end
         | 
| 117 | 
            -
                  puts failure.join("\n") unless compliance
         | 
| 118 | 
            -
                  puts 'Compliance threshold met' if compliance
         | 
| 119 | 
            -
                  compliance
         | 
| 120 | 
            -
                end
         | 
| 121 | 
            -
             | 
| 122 | 
            -
                def parse_threshold(new_threshold)
         | 
| 123 | 
            -
                  @threshold.merge!(new_threshold)
         | 
| 149 | 
            +
                def compute_compliance(summary)
         | 
| 150 | 
            +
                  (summary[:status][:passed][:total]*100.0/
         | 
| 151 | 
            +
                    (summary[:status][:passed][:total]+
         | 
| 152 | 
            +
                     summary[:status][:failed][:total]+
         | 
| 153 | 
            +
                     summary[:status][:skipped][:total]+
         | 
| 154 | 
            +
                     summary[:status][:error][:total])).floor
         | 
| 124 155 | 
             
                end
         | 
| 125 156 | 
             
              end
         | 
| 157 | 
            +
              # rubocop:enable Metrics/ClassLength
         | 
| 126 158 | 
             
            end
         |