# Copyright 2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from reconbf.lib.logger import logger import reconbf.lib.test_class as test_class from reconbf.lib.result import GroupTestResult from reconbf.lib.result import TestResult from reconbf.lib.result import Result import os import platform import string @test_class.explanation( """ Protection name: Reboot required Check: Verify if the system thinks a reboot is required Purpose: Some distributions will report whether a reboot is required due to recently installed packages. This usually means a new binary cannot be easily restarted: like the kernel or the init system. """) def reboot_required(): try: distro, _version, _name = platform.linux_distribution() except Exception: return TestResult(Result.SKIP, "Could not detect distribution") if distro in ('Ubuntu', 'Debian'): if os.path.isfile('/var/run/reboot-required'): try: with open('/var/run/reboot-required.pkgs', 'r') as f: packages = set(line.strip() for line in f.readlines()) except Exception: packages = None if packages: packages = ', '.join(sorted(packages)) msg = "Reboot is required to update: %s" % packages else: msg = "Reboot is required" return TestResult(Result.FAIL, msg) else: return TestResult(Result.PASS) else: return TestResult(Result.SKIP, "Unknown distribution") @test_class.explanation( """ Protection name: Running processes have all corresponding files Check: Checks that each process running on the system uses files which are present on the disk. Purpose: Usually every running process will have the corresponding executable file and library available all the time. Anything different is an uncommon situation. It can happen for example because: file was deleted on purpose to avoid detection, package has been upgraded but the process was not restarted (potentially still vulnerable), etc. """) def missing_process_binaries(): results = GroupTestResult() for pid in os.listdir('/proc'): if not pid.isdigit(): continue try: main_binary = os.readlink(os.path.join('/proc', pid, 'exe')) missing_main = False missing = set() links = os.listdir(os.path.join('/proc', pid, 'map_files')) for link in links: link_path = os.readlink(os.path.join('/proc', pid, 'map_files', link)) if link_path.endswith(' (deleted)'): if link_path == main_binary: missing_main = True else: link_path = link_path[:-10] # only check libraries, data files can go missing # without issues file_name = os.path.basename(link_path) if file_name.endswith('.so') or '.so.' in file_name: missing.add(link_path) if main_binary.endswith(' (deleted)'): main_binary = main_binary[:-10] process = "pid %s, %s" % (pid, main_binary) missing_list = [] if missing_main: missing_list.append('main binary') missing_list.extend(sorted(missing)) if missing_list: msg = "Missing: %s" % ', '.join(missing_list) results.add_result(process, TestResult(Result.FAIL, msg)) else: results.add_result(process, TestResult(Result.PASS)) except EnvironmentError: # this pid can disappear at any point, so on any read failure, # just continue with the next process continue return results def _parse_deb_repo_line(line): # this parses only the one-line format, because honestly, who uses deb822 # in their sources file... line = line.strip() # cut off the comments comment_pos = line.find('#') if comment_pos != -1: line = line[:comment_pos] # ignore empty lines if not line: return # everything's split by whitespace parts = line.split() pkg_type = parts.pop(0) while parts: if '=' in parts[0]: # just ignore the options... parts.pop(0) else: break if not parts: logger.warning('deb entry "%s" missing uri, ignoring', line) return uri = parts.pop(0) if not parts: logger.warning('deb entry "%s" missing suite, ignoring', line) return suite = parts.pop(0) components = parts return { 'type': pkg_type, 'uri': uri, 'suite': suite, 'components': components, } SOURCES_LIST_CHARS = set(string.ascii_letters + string.digits + '_-.') def _get_deb_repos(): repos = [] files = ['/etc/apt/sources.list'] try: for name in os.listdir('/etc/apt/sources.list.d'): if not name.endswith('.list'): continue if set(name).difference(SOURCES_LIST_CHARS): # unexpected characters in the name, ignored by apt by default continue files.append('/etc/apt/sources.list.d/' + name) except OSError: # if the directory cannot be read, it's ok to ignore it pass for name in files: try: with open(name, 'r') as f: for line in f: repo = _parse_deb_repo_line(line) if repo: repos.append(repo) except EnvironmentError: logger.warning('cannot read repo list "%s"', name) continue return repos @test_class.explanation( """ Protection name: Security updates in repo lists Check: Will the package manager look at security updates when a standard system update is triggered. Purpose: Many systems use a different repository for standard releases and for security updates. This is due to multiple reasons (security-only update streams, mirror delays, etc.), but means a system may seem to be updating ok even if no security fixes are pulled. Currently, this test supports Debian and Ubuntu systems only. """) def security_updates(): try: distro, _version, version_name = platform.linux_distribution() except Exception: return TestResult(Result.SKIP, "Could not detect distribution") if distro in ('Ubuntu', 'Debian'): repos = _get_deb_repos() security_suite = version_name + '-security' found_security = False for repo in repos: if repo['type'] != 'deb': continue if distro == 'Ubuntu' and repo['suite'] == security_suite: found_security = True break if (distro == 'Debian' and 'http://security.debian.org' in repo['uri']): found_security = True break if found_security: return TestResult(Result.PASS, "Security repo present") else: return TestResult(Result.FAIL, "Upstream security repo not configured") else: return TestResult(Result.SKIP, "Unknown distribution")