Class: HybridPlatformsConductor::HpcPlugins::Test::Vulnerabilities

Inherits:
TestOnlyRemoteNode show all
Defined in:
lib/hybrid_platforms_conductor/hpc_plugins/test/vulnerabilities.rb

Overview

Test that the node has not known vulnerabilities. Check this by using OVAL files published by vendors. For example, RedHat publishes them here: www.redhat.com/security/data/oval/v2/RHEL7/ This test uses an oval.json file stored in the OS images folder, having the following structure:

  • urls (Array<String>): List of URLs pointing to OVAL files [default: []]

    Each URL can be directly an XML file, either raw or compressed with .gz or .bz2.
    
  • repo_urls (Array<String>): List of URLs pointing to repositories of OVAL files [default: []]

    The last HTML link of each repo URL is followed until an OVAL file is found.
    Each final OVAL URL can be directly an XML file, either raw or compressed with .gz or .bz2.
    This is useful to follow repository links, such as jFrog or web servers serving common file systems structure storing several versions of the OVAL file.
    
  • reported_severities (Array<String> or nil): List of severities to report, if any (use Unknown when the severity is not known), or nil for all [default: nil]

Constant Summary collapse

KNOWN_COMPRESSIONS =

Known compression methods, per file extension, and their corresponding uncompress bash script

{
  bz2: {
    cmd: proc { |file| "if [ ! -f \"#{File.basename(file, '.bz2')}\" ] ; then bunzip2 \"#{file}\" ; fi" },
    packages: ['bzip2']
  },
  gz: {
    cmd: proc { |file| "if [ ! -f \"#{File.basename(file, '.gz')}\" ] ; then gunzip \"#{file}\" ; fi" },
    packages: ['gzip']
  }
}

Constants included from LoggerHelpers

LoggerHelpers::LEVELS_MODIFIERS, LoggerHelpers::LEVELS_TO_STDERR

Instance Attribute Summary

Attributes inherited from Test

#errors, #expected_failure, #name, #node, #platform

Instance Method Summary collapse

Methods inherited from TestOnlyRemoteNode

only_on_nodes

Methods inherited from Test

#assert_equal, #assert_match, #error, #executed, #executed?, #initialize, only_on_nodes, only_on_platforms, #to_s

Methods inherited from Plugin

extend_config_dsl_with, #initialize, valid?

Methods included from LoggerHelpers

#err, #init_loggers, #log_component=, #log_debug?, #log_level=, #out, #section, #set_loggers_format, #stderr_device, #stderr_device=, #stderr_displayed?, #stdout_device, #stdout_device=, #stdout_displayed?, #stdouts_to_s, #with_progress_bar

Constructor Details

This class inherits a constructor from HybridPlatformsConductor::Test

Instance Method Details

#test_on_nodeObject

Check my_test_plugin.rb.sample documentation for signature details.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/hybrid_platforms_conductor/hpc_plugins/test/vulnerabilities.rb', line 36

def test_on_node
  # Get the image name for this node
  image = @nodes_handler.get_image_of(@node).to_sym
  # Find if we have such an image registered
  if @config.known_os_images.include?(image)
    oval_file = "#{@config.os_image_dir(image)}/oval.json"
    if File.exist?(oval_file)
      oval_info = JSON.parse(File.read(oval_file))
      # Get all URLs
      urls = oval_info['urls'] || []
      urls.concat(
        (oval_info['repo_urls'] || []).map do |artifactory_url|
          # Follow the last link recursively until we find a .xml or compressed file
          current_url = artifactory_url
          loop do
            current_url = "#{current_url}#{current_url.end_with?('/') ? '' : '/'}#{Nokogiri::HTML.parse(URI.open(current_url)).css('a').last['href']}"
            break if current_url.end_with?('.xml') || KNOWN_COMPRESSIONS.keys.any? { |file_ext| current_url.end_with?(".#{file_ext}") }
            log_debug "Follow last link to #{current_url}"
          end
          current_url
        end
      )
      # TODO: Access the user correctly when the user notion will be moved out of the ssh connector
      sudo = @deployer.instance_variable_get(:@actions_executor).connector(:ssh).ssh_user == 'root' ? '' : "#{@nodes_handler.sudo_on(@node)} "
      Hash[urls.map do |url|
        # 1. Get the OVAL file on the node to be tested (uncompress it if needed)
        # 2. Make sure oscap is installed
        # 3. Generate the report for this OVAL file using oscap
        # 4. Get back the report here to analyze it
        local_oval_file = File.basename(url)
        uncompress_cmds = []
        packages_to_install = []
        KNOWN_COMPRESSIONS.each do |file_ext, compress_info|
          file_ending = ".#{file_ext}"
          if local_oval_file.end_with?(file_ending)
            uncompress_cmds << compress_info[:cmd].call(local_oval_file)
            packages_to_install.concat(compress_info[:packages])
            local_oval_file = File.basename(local_oval_file, file_ending)
          end
        end
        cmds = <<~EOS
          set -e
          #{
            case image
            when :centos_7
              "#{sudo}yum install -y wget openscap-scanner #{packages_to_install.join(' ')}"
            when :debian_9
              "#{sudo}apt install -y wget libopenscap8 #{packages_to_install.join(' ')}"
            when :debian_10
              # On Debian 10 we have to compile it from sources, as the packaged official version has core dumps.
              # cf https://www.mail-archive.com/[email protected]/msg1688223.html
              # TODO: Remove this Debian 10 specificity when the official libopenscap8 will be corrected
              <<~EOS2
                if [ ! -x "$(command -v oscap)" ] || [ "$(oscap --version | head -n 1 | awk '{print $6}')" != "1.3.4" ]; then
                  rm -rf openscap
                  git clone --recurse-submodules https://github.com/OpenSCAP/openscap.git
                  cd openscap
                  #{sudo}apt install -y cmake libdbus-1-dev libdbus-glib-1-dev libcurl4-openssl-dev libgcrypt20-dev libselinux1-dev libxslt1-dev libgconf2-dev libacl1-dev libblkid-dev libcap-dev libxml2-dev libldap2-dev libpcre3-dev python-dev swig libxml-parser-perl libxml-xpath-perl libperl-dev libbz2-dev librpm-dev g++ libapt-pkg-dev libyaml-dev
                  cd build
                  cmake ../
                  make
                  #{sudo}make install
                fi
                #{sudo}apt install -y wget #{packages_to_install.join(' ')}
              EOS2
            else
              raise "Non supported image: #{image}. Please adapt this test's code."
            end
          }
          rm -rf hpc_vulnerabilities_test
          mkdir -p hpc_vulnerabilities_test
          cd hpc_vulnerabilities_test
          wget -N #{url}
          #{uncompress_cmds.join("\n")}
          #{sudo}oscap oval eval --skip-valid --results "#{local_oval_file}.results.xml" "#{local_oval_file}"
          echo "===== RESULTS ====="
          cat "#{local_oval_file}.results.xml"
          cd ..
        EOS
        [
          cmds,
          {
            validator: proc do |stdout|
              idx_results = stdout.index('===== RESULTS =====')
              if idx_results.nil?
                error 'No results given by the oscap run', stdout.join("\n")
              else
                results = Nokogiri::XML(stdout[idx_results + 1..-1].join("\n"))
                results.remove_namespaces!
                oval_definitions = results.css('oval_results oval_definitions definitions definition')
                results.css('results system definitions definition').each do |definition_xml|
                  if definition_xml['result'] == 'true'
                    # Just found an OVAL item to be patched.
                    definition_id = definition_xml['definition_id']
                    oval_definition = oval_definitions.find { |el| el['id'] == definition_id }
                    # We don't forcefully want to report all missing patches. Only the most important ones.
                    severity = oval_definition.css('metadata advisory severity').text
                    severity = 'Unknown' if severity.empty?
                    if !oval_info.key?('reported_severities') || oval_info['reported_severities'].include?(severity)
                      # Only consider the first line of the description, as sometimes it's very long
                      error "Non-patched #{severity} vulnerability found: #{oval_definition.css('metadata title').text} - #{oval_definition.css('metadata description').text.split("\n").first}"
                    end
                  end
                end
              end
            end,
            # Increase timeout in case we have to install a lot of dependencies (like for Debian 10)
            timeout: 240
          }
        ]
      end]
    else
      error "No OVAL file defined for image #{image} at #{oval_file}"
      {}
    end
  else
    error "Unknown OS image #{image} defined for node #{@node}"
    {}
  end
end