From 17f181b40ea8d509401980dfaa14d30f39580a72 Mon Sep 17 00:00:00 2001 From: Ryan Sawhill Aroha Date: Fri, 18 Nov 2016 03:52:27 -0500 Subject: [PATCH 1/3] split data api out into rhsda.py #18; logging #31 --- rhsda.py | 779 ++++++++++++++++++++++++++++++++++++++++++++++++++++ rhsecapi.py | 722 ++++++------------------------------------------ 2 files changed, 864 insertions(+), 637 deletions(-) create mode 100644 rhsda.py diff --git a/rhsda.py b/rhsda.py new file mode 100644 index 0000000..f4dd045 --- /dev/null +++ b/rhsda.py @@ -0,0 +1,779 @@ +#!/usr/bin/python2 +# -*- coding: utf-8 -*- +#------------------------------------------------------------------------------- +# Copyright 2016 Ryan Sawhill Aroha and rhsecapi contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +#------------------------------------------------------------------------------- + +# Modules from standard library +from __future__ import print_function +import requests +import logging +import sys +import re +import textwrap, fcntl, termios, struct +import json +import signal +import copy_reg +import types +import multiprocessing.dummy as multiprocessing +from argparse import Namespace + + +# Logging +logging.addLevelName(25, 'NOTICE') +consolehandler = logging.StreamHandler() +consolehandler.setLevel('DEBUG') +consolehandler.setFormatter(logging.Formatter("[%(levelname)-7s] %(name)s: %(message)s")) +logger = logging.getLogger('rhsda') +logger.setLevel('NOTICE') +logger.addHandler(consolehandler) + + +# Establish cveFields namespace +cveFields = Namespace() +# All supported API-provided CVE fields +cveFields.all = [ + 'threat_severity', + 'public_date', + 'iava', + 'cwe', + 'cvss', + 'cvss3', + 'bugzilla', + 'acknowledgement', + 'details', + 'statement', + 'mitigation', + 'upstream_fix', + 'references', + 'affected_release', + 'package_state', + ] +# The few text-heavy fields +cveFields.not_most = [ + 'acknowledgement', + 'details', + 'statement', + 'mitigation', + 'references', + ] +# All fields except the above +cveFields.most = list(cveFields.all) +for f in cveFields.not_most: + cveFields.most.remove(f) +# Simple set of most important fields +cveFields.base = [ + 'threat_severity', + 'public_date', + 'bugzilla', + 'affected_release', + 'package_state', + ] +# Aliases to make life easier +cveFields.aliases = { + 'severity': 'threat_severity', + 'date': 'public_date', + 'fixed_releases': 'affected_release', + 'fixed': 'affected_release', + 'releases': 'affected_release', + 'fix_states': 'package_state', + 'states': 'package_state', + } +# Printable mapping of aliases +cveFields.aliases_printable = [ + "threat_severity → severity", + "public_date → date", + "affected_release → fixed_releases or fixed or releases", + "package_state → fix_states or states", + ] +# A list of all fields + all aliases +cveFields.all_plus_aliases = list(cveFields.all) +cveFields.all_plus_aliases.extend([k for k in cveFields.aliases]) + + +# Regex to match a CVE id string +cve_regex_string = 'CVE-[0-9]{4}-[0-9]{4,}' +cve_regex = re.compile(cve_regex_string, re.IGNORECASE) + + +# The following function & copy_reg.pickle() call make it possible for pickle to serialize class functions +# This is critical to allow multiprocessing.Pool.map_async() to work as desired +# See: http://stackoverflow.com/a/19861595 +def _reduce_method(m): + if m.__self__ is None: + return getattr, (m.__class__, m.__func__.__name__) + else: + return getattr, (m.__self__, m.__func__.__name__) + +copy_reg.pickle(types.MethodType, _reduce_method) + + +# Set default number of worker threads +if multiprocessing.cpu_count() <= 2: + numThreadsDefault = 4 +else: + numThreadsDefault = multiprocessing.cpu_count() * 2 + + +def jprint(jsoninput, printOutput=True): + """Pretty-print jsoninput.""" + j = json.dumps(jsoninput, sort_keys=True, indent=2) + "\n" + if printOutput: + print(j) + else: + return j + + +def extract_cves_from_input(obj): + """Use case-insensitive regex to extract CVE ids from input object. + + *obj* can be a list, a file, or a string. + + A list of CVEs is returned. + """ + # Array to store found CVEs + found = [] + if obj == sys.stdin: + noun = "stdin" + else: + noun = "input" + if isinstance(obj, str): + obj = obj.splitlines() + for line in obj: + # Iterate over each line adding the returned list to our found list + found.extend(cve_regex.findall(line)) + if found: + matchCount = len(found) + # Converting to a set removes duplicates + found = list(set(found)) + uniqueCount = len(found) + logger.log(25, "Found {0} CVEs in {1}; {2} duplicates removed".format(uniqueCount, noun, matchCount-uniqueCount)) + return [x.upper() for x in found] + else: + logger.log(25, "No CVEs (matching regex: '{0}') found in {1}".format(cve_regex_string, noun)) + + +class ApiClient: + """Portable object to interface with the Red Hat Security Data API. + + https://access.redhat.com/documentation/en/red-hat-security-data-api/ + """ + + def __init__(self, logLevel='notice'): + self.cfg = Namespace() + self.cfg.apiUrl = 'https://access.redhat.com/labs/securitydataapi' + logger.setLevel(logLevel.upper()) + + def _get_terminal_width(self): + h, w, hp, wp = struct.unpack('HHHH', fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))) + return w + + def __validate_data_type(self, dT): + dataTypes = ['cvrf', 'cve', 'oval'] + if dT not in dataTypes: + raise ValueError("Invalid data type ('{0}') requested; should be one of: {1}".format(dT, ", ".join(dataTypes))) + + def __validate_out_format(self, oF): + outFormats = ['json', 'xml'] + if oF not in outFormats: + raise ValueError("Invalid outFormat type ('{0}') requested; should be one of: {1}".format(oF, ", ".join(outFormats))) + + def __get(self, url, params={}): + url = self.cfg.apiUrl + url + u = "" + if params: + for k in params: + if params[k]: + u += "&{0}={1}".format(k, params[k]) + u = u.replace("&", "?", 1) + logger.info("Getting '{0}{1}' ...".format(url, u)) + try: + r = requests.get(url, params=params) + except requests.exceptions.ConnectionError as e: + logger.error(e) + raise + except requests.exceptions.RequestException as e: + logger.error(e) + raise + logger.debug("Return status: '{0}'; Content-Type: '{1}'".format(r.status_code, r.headers['Content-Type'])) + r.raise_for_status() + if 'application/xml' in r.headers['Content-Type']: + return r.content + else: + return r.json() + + def _find(self, dataType, params, outFormat): + self.__validate_data_type(dataType) + self.__validate_out_format(outFormat) + url = '/{0}.{1}'.format(dataType, outFormat) + if isinstance(params, dict): + result = self.__get(url, params) + elif params: + result = self.__get(url + '?' + params) + else: + result = self.__get(url) + if isinstance(result, list): + logger.log(25, "{0} {1}s found with search query".format(len(result), dataType.upper())) + return result + + def _retrieve(self, dataType, query, outFormat): + self.__validate_data_type(dataType) + self.__validate_out_format(outFormat) + url = '/{0}/{1}.{2}'.format(dataType, query, outFormat) + return self.__get(url) + + def find_cvrfs(self, params=None, outFormat='json', + before=None, after=None, bug=None, cve=None, severity=None, package=None, + page=None, per_page=None): + """Find CVRF documents by recent or attributes. + + Provides an index to recent CVRF documents with a summary of their contents, + when no parameters are passed. Returns a convenience object as the response with + minimal attributes. + + With *outFormat* of "json", returns JSON object. + With *outFormat* of "xml", returns unformatted XML as string. + If *params* dict is passed, additional parameters are ignored. + """ + if not params: + params = { + 'before': before, + 'after': after, + 'bug': bug, + 'cve': cve, + 'severity': severity, + 'package': package, + 'page': page, + 'per_page': per_page, + } + return self._find('cvrf', params, outFormat) + + def find_cves(self, params=None, outFormat='json', + before=None, after=None, bug=None, advisory=None, severity=None, + package=None, cwe=None, cvss_score=None, cvss3_score=None, + page=None, per_page=None): + """Find CVEs by recent or attributes. + + Provides an index to recent CVEs when no parameters are passed. Returns a + convenience object as response with minimal attributes. + + With *outFormat* of "json", returns JSON object. + With *outFormat* of "xml", returns unformatted XML as string. + If *params* dict is passed, additional parameters are ignored. + """ + if not params: + params = { + 'before': before, + 'after': after, + 'bug': bug, + 'advisory': advisory, + 'severity': severity, + 'package': package, + 'cwe': cwe, + 'cvss_score': cvss_score, + 'cvss3_score': cvss3_score, + 'page': page, + 'per_page': per_page, + } + return self._find('cve', params, outFormat) + + def find_ovals(self, params=None, outFormat='json', + before=None, after=None, bug=None, cve=None, severity=None, + page=None, per_page=None): + """Find OVAL definitions by recent or attributes. + + Provides an index to recent OVAL definitions with a summary of their contents, + when no parameters are passed. Returns a convenience object as the response with + minimal attributes. + + With *outFormat* of "json", returns JSON object. + With *outFormat* of "xml", returns unformatted XML as string. + If *params* dict is passed, additional parameters are ignored. + """ + if not params: + params = { + 'before': before, + 'after': after, + 'bug': bug, + 'cve': cve, + 'severity': severity, + 'page': page, + 'per_page': per_page, + } + return self._find('oval', params, outFormat) + + def get_cvrf(self, rhsa, outFormat='json'): + """Retrieve CVRF details for an RHSA.""" + return self._retrieve('cvrf', rhsa, outFormat) + + def get_cvrf_oval(self, rhsa, outFormat='json'): + """Retrieve CVRF-OVAL details for an RHSA.""" + return self._retrieve('cvrf', '{0}/oval'.format(rhsa), outFormat) + + def get_cve(self, cve, outFormat='json'): + """Retrieve full details of a CVE.""" + return self._retrieve('cve', cve, outFormat) + + def get_oval(self, rhsa, outFormat='json'): + """Retrieve OVAL details for an RHSA.""" + return self._retrieve('oval', rhsa, outFormat) + + def __stripjoin(self, input, oneLineEach=False): + """Strip whitespace from input or input list.""" + text = "" + if isinstance(input, list): + for i in input: + text += i.encode('utf-8').strip() + if oneLineEach: + text += "\n" + else: + text += " " + else: + text = input.encode('utf-8').strip() + if oneLineEach: + text = "\n" + text + text = re.sub(r"\n+", "\n ", text) + else: + text = re.sub(r"\n+", " ", text) + if self.wrapper: + text = "\n" + "\n".join(self.wrapper.wrap(text)) + return text + + def __check_field(self, field, jsoninput): + """Return True if field is desired and exists in jsoninput.""" + if field in self.cfg.desiredFields and jsoninput.has_key(field): + return True + return False + + def _parse_cve_to_plaintext(self, cve): + """Generate a plaintext representation of a CVE. + + This is designed with only one argument in order to allow being used as a worker + with multiprocessing.Pool.map_async(). + + Various printing operations in this method are conditional upon (or are tweaked + by) the values in the self.cfg namespace as set in parent meth self.mget_cves(). + """ + # Output array: + out = [] + try: + # Store json + J = self.get_cve(cve) + except requests.exceptions.HTTPError as e: + # CVE not in RH CVE DB + logger.info(e) + if self.cfg.product or self.cfg.onlyCount or self.cfg.outFormat == 'json': + return False, "" + else: + out.append("{0}\n Not present in Red Hat CVE database".format(cve)) + if cve.startswith("CVE-"): + out.append(" Try https://cve.mitre.org/cgi-bin/cvename.cgi?name={0}".format(cve)) + out.append("") + return False, "\n".join(out) + # If json output requested + if self.cfg.outFormat.startswith('json'): + return True, J + # CVE ID + name = "" + if cve != J['name']: + name = " [{0}]".format(J['name']) + u = "" + if self.cfg.urls: + u = " (https://access.redhat.com/security/cve/{0})".format(cve) + out.append("{0}{1}{2}".format(cve, name, u)) + # SEVERITY + if self.__check_field('threat_severity', J): + u = "" + if self.cfg.urls: + u = " (https://access.redhat.com/security/updates/classification)" + out.append(" SEVERITY: {0} Impact{1}".format(J['threat_severity'], u)) + # PUBLIC_DATE + if self.__check_field('public_date', J): + out.append(" DATE: {0}".format(J['public_date'].split("T")[0])) + # IAVA + if self.__check_field('iava', J): + out.append(" IAVA: {0}".format(J['iava'])) + # CWE ID + if self.__check_field('cwe', J): + out.append(" CWE: {0}".format(J['cwe'])) + if self.cfg.urls: + cwes = re.findall("CWE-[0-9]+", J['cwe']) + if len(cwes) == 1: + out[-1] += " (http://cwe.mitre.org/data/definitions/{0}.html)".format(cwes[0].lstrip("CWE-")) + else: + for c in cwes: + out.append(" (http://cwe.mitre.org/data/definitions/{0}.html)".format(c.lstrip("CWE-"))) + # CVSS2 + if self.__check_field('cvss', J): + vector = J['cvss']['cvss_scoring_vector'] + if self.cfg.urls: + vector = "http://nvd.nist.gov/cvss.cfm?version=2&vector={0}".format(vector) + out.append(" CVSS: {0} ({1})".format(J['cvss']['cvss_base_score'], vector)) + # CVSS3 + if self.__check_field('cvss3', J): + vector = J['cvss3']['cvss3_scoring_vector'] + if self.cfg.urls: + vector = "https://www.first.org/cvss/calculator/3.0#{0}".format(vector) + out.append(" CVSS3: {0} ({1})".format(J['cvss3']['cvss3_base_score'], vector)) + # BUGZILLA + if 'bugzilla' in self.cfg.desiredFields: + if J.has_key('bugzilla'): + if self.cfg.urls: + bug = J['bugzilla']['url'] + else: + bug = J['bugzilla']['id'] + out.append(" BUGZILLA: {0}".format(bug)) + else: + out.append(" BUGZILLA: No Bugzilla data") + out.append(" Too new or too old? See: https://bugzilla.redhat.com/show_bug.cgi?id=CVE_legacy") + # ACKNOWLEDGEMENT + if self.__check_field('acknowledgement', J): + out.append(" ACKNOWLEDGEMENT: {0}".format(self.__stripjoin(J['acknowledgement']))) + # DETAILS + if self.__check_field('details', J): + out.append(" DETAILS: {0}".format(self.__stripjoin(J['details']))) + # STATEMENT + if self.__check_field('statement', J): + out.append(" STATEMENT: {0}".format(self.__stripjoin(J['statement']))) + # MITIGATION + if self.__check_field('mitigation', J): + out.append(" MITIGATION: {0}".format(self.__stripjoin(J['mitigation']))) + # UPSTREAM FIX + if self.__check_field('upstream_fix', J): + out.append(" UPSTREAM_FIX: {0}".format(J['upstream_fix'])) + # REFERENCES + if self.__check_field('references', J): + out.append(" REFERENCES:{0}".format(self.__stripjoin(J['references'], oneLineEach=True))) + # AFFECTED RELEASE + foundProduct_affected_release = False + if self.__check_field('affected_release', J): + if self.cfg.product: + out.append(" FIXED_RELEASES matching '{0}':".format(self.cfg.product)) + else: + out.append(" FIXED_RELEASES:") + affected_release = J['affected_release'] + if isinstance(affected_release, dict): + # When there's only one, it doesn't show up in a list + affected_release = [affected_release] + for release in affected_release: + if self.cfg.product: + if self.regex_product.search(release['product_name']) or self.regex_product.search(release['cpe']): + foundProduct_affected_release = True + else: + # If product doesn't match spotlight, go to next + continue + pkg = "" + if release.has_key('package'): + pkg = " [{0}]".format(release['package']) + advisory = release['advisory'] + if self.cfg.urls: + advisory = "https://access.redhat.com/errata/{0}".format(advisory) + out.append(" {0}{1}: {2}".format(release['product_name'], pkg, advisory)) + if self.cfg.product and not foundProduct_affected_release: + # If nothing found, remove the "FIXED_RELEASES" heading + out.pop() + # PACKAGE STATE + foundProduct_package_state = False + if self.__check_field('package_state', J): + if self.cfg.product: + out.append(" FIX_STATES matching '{0}':".format(self.cfg.product)) + else: + out.append(" FIX_STATES:") + package_state = J['package_state'] + if isinstance(package_state, dict): + # When there's only one, it doesn't show up in a list + package_state = [package_state] + for state in package_state: + if self.cfg.product: + if self.regex_product.search(state['product_name']) or self.regex_product.search(state['cpe']): + foundProduct_package_state = True + else: + # If product doesn't match spotlight, go to next + continue + pkg = "" + if state.has_key('package_name'): + pkg = " [{0}]".format(state['package_name']) + out.append(" {0}: {1}{2}".format(state['fix_state'], state['product_name'], pkg)) + if self.cfg.product and not foundProduct_package_state: + # If nothing found, remove the "FIX_STATES" heading + out.pop() + # If searching for product and not found return no output + if self.cfg.product and not (foundProduct_affected_release or foundProduct_package_state): + logger.info("Hiding {0} due to negative product match".format(cve)) + return None, "" + # Return no output if only counting + if self.cfg.onlyCount: + return True, "" + # Add one final newline to the end + out.append("") + return True, "\n".join(out) + + def _set_cve_plaintext_fields(self, desiredFields): + logger.debug("Requested fields string: '{0}'".format(desiredFields)) + if not desiredFields: + # Start with all fields if none given + desiredFields = ','.join(cveFields.all) + # Lower case + desiredFields = desiredFields.lower() + if desiredFields == 'all': + desiredFields = ','.join(cveFields.all) + elif desiredFields == 'most': + desiredFields = ','.join(cveFields.most) + elif desiredFields == 'base': + desiredFields = ','.join(cveFields.base) + # Save starting fields to temporary "fields" list; create postProcessed list + if desiredFields.startswith('+'): + fields = desiredFields[1:].split(',') + postProcessedFields = list(cveFields.base) + elif desiredFields.startswith('^'): + fields = desiredFields[1:].split(',') + postProcessedFields = list(cveFields.all) + else: + fields = desiredFields.split(',') + postProcessedFields = [] + # Iterate over list + for f in fields: + # Skip unknown fields + if f not in cveFields.all_plus_aliases: + logger.warning("Field '{0}' is not a known field; valid fields:\n{2}".format(f, ", ".join(cveFields.all_plus_aliases))) + continue + # Look-up aliases + if f not in cveFields.all: + f = cveFields.aliases[f] + # If using ^/+, remove/add field from/to defaults + if desiredFields.startswith('^') and f in postProcessedFields: + postProcessedFields.remove(f) + elif desiredFields.startswith('+'): + postProcessedFields.append(f) + # Otherwise, add to postprocessed list + else: + postProcessedFields.append(f) + logger.debug("Enabled fields: '{0}'".format(", ".join(postProcessedFields))) + self.cfg.desiredFields = postProcessedFields + + def _set_cve_plaintext_product(self, product): + self.cfg.product = product + if product: + self.regex_product = re.compile(product, re.IGNORECASE) + else: + self.regex_product = None + + def _set_cve_plaintext_width(self, wrapWidth): + if wrapWidth == 1: + if sys.stdin.isatty(): + wrapWidth = self._get_terminal_width() - 2 + else: + logger.warning("Stdin redirection suppresses term-width auto-detection; setting WIDTH to 70") + wrapWidth = 70 + if wrapWidth: + self.wrapper = textwrap.TextWrapper(width=wrapWidth, initial_indent=" ", subsequent_indent=" ", replace_whitespace=False) + else: + self.wrapper = 0 + + def mget_cves(self, cves, numThreads=0, onlyCount=False, outFormat='plaintext', + urls=False, fields='ALL', wrapWidth=70, product=None, timeout=300): + """Use multi-threading to lookup a list of CVEs and return text output. + + *cves*: A list of CVE ids or a str obj from which to regex CVE ids + *numThreads*: Number of concurrent worker threads; 0 == CPUs*2 + *onlyCount*: Whether to exit after simply logging number of valid/invalid CVEs + *outFormat*: Control output format ("plaintext", "json", or "jsonpretty") + *urls*: Whether to add extra URLs to certain fields + *fields*: Customize which fields are displayed by passing comma-sep string + *wrapWidth*: Width for long fields; 1 auto-detects based on terminal size + *product*: Restrict display of CVEs based on product-matching regex + *timeout*: Total ammount of time to wait for all CVEs to be retrieved + + ON *CVES*: + + If *cves* is a list, each item in the list will be retrieved as a CVE. + If *cves* is a string or file object, it will be regex-parsed line by line and + all CVE ids will be extracted into a list. + In all cases, character-case is irrelevant. + + ON *OUTFORMAT*: + + Setting to "plaintext" returns str object containing formatted output. + Setting to "json" returns list object (i.e., original JSON) + Setting to "jsonpretty" returns str object containing prettified JSON + + ON *FIELDS*: + + librhsecapi.cveFields.all is a list obj of supported fields, i.e.: + threat_severity, public_date, iava, cwe, cvss, cvss3, bugzilla, + acknowledgement, details, statement, mitigation, upstream_fix, references, + affected_release, package_state + + librhsecapi.cveFields.most is a list obj that excludes text-heavy fields, like: + acknowledgement, details, statement, mitigation, references + + librhsecapi.cveFields.base is a list obj of the most important fields, i.e.: + threat_severity, public_date, bugzilla, affected_release, package_state + + There is a group-alias for each of these, so you can do: + fields="ALL" + fields="MOST" + fields="BASE" + + Also note that some friendly aliases are supported, e.g.: + threat_severity → severity + public_date → date + affected_release → fixed_releases or fixed or releases + package_state → fix_states or states + + Note that the *fields* string can be prepended with "+" or "^" to signify + adding to cveFields.base or removing from cveFields.all, e.g.: + fields="+cvss,cwe,statement" + fields="^releases,mitigation" + + Finally: *fields* is case-insensitive. + """ + if outFormat not in ['plaintext', 'json', 'jsonpretty']: + raise ValueError("Invalid outFormat ('{0}') requested; should be one of: 'plaintext', 'json', 'jsonpretty'".format(outFormat)) + if isinstance(cves, str) or isinstance(cves, file): + cves = extract_cves_from_input(cves) + elif isinstance(cves, list): + cves = [x.upper() for x in cves] + else: + raise ValueError("Invalid 'cves=' argument input; must be list, string, or file obj") + # Configure threads + if not numThreads: + numThreads = numThreadsDefault + # Lower threads for small work-loads + if numThreads > len(cves): + numThreads = len(cves) + logger.info("Using {0} worker threads".format(numThreads)) + # Set cfg directives for our worker + self.cfg.onlyCount = onlyCount + self.cfg.urls = urls + self.cfg.outFormat = outFormat + self._set_cve_plaintext_width(wrapWidth) + self._set_cve_plaintext_fields(fields) + self._set_cve_plaintext_product(product) + # Disable sigint before starting process pool + original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) + pool = multiprocessing.Pool(processes=numThreads) + # Re-enable receipt of sigint + signal.signal(signal.SIGINT, original_sigint_handler) + # Allow cancelling with Ctrl-c + try: + p = pool.map_async(self._parse_cve_to_plaintext, cves) + # Need to specify timeout; see: http://stackoverflow.com/a/35134329 + results = p.get(timeout=timeout) + except KeyboardInterrupt: + logger.error("\nReceived KeyboardInterrupt; terminating worker threads") + pool.terminate() + return + else: + pool.close() + pool.join() + successValues, cveOutput = zip(*results) + n_total = len(cves) + n_hidden = successValues.count(None) + n_valid = successValues.count(True) + n_invalid = successValues.count(False) + logger.log(25, "Valid Red Hat CVE results retrieved: {0} of {1}".format(n_valid + n_hidden, n_total)) + if product: + logger.log(25, "Results matching spotlight-product option: {0} of {1}".format(n_valid, n_total)) + if n_invalid: + logger.log(25, "Invalid CVE queries: {0} of {1}".format(n_invalid, n_total)) + if onlyCount: + return + if outFormat == 'plaintext': + return "\n".join(cveOutput) + elif outFormat == 'json': + return cveOutput + elif outFormat == 'jsonpretty': + return jprint(cveOutput, False) + + def cve_search_query(self, params, outFormat='list'): + """Perform a CVE search query. + + ON *OUTFORMAT*: + + Setting to "list" returns list of found CVE ids. + Setting to "plaintext" returns str object containing new-line separated CVE ids. + Setting to "json" returns list object containing original JSON. + Setting to "jsonpretty" returns str object containing prettified JSON. + """ + if outFormat not in ['list', 'plaintext', 'json', 'jsonpretty']: + raise ValueError("Invalid outFormat ('{0}') requested; should be one of: 'list', 'plaintext', 'json', 'jsonpretty'".format(outFormat)) + result = self.find_cves(params) + if outFormat == 'json': + return result + if outFormat == 'jsonpretty': + return jprint(result, False) + cves = [] + for i in result: + cves.append(i['CVE']) + if outFormat == 'list': + return cves + if outFormat == 'plaintext': + return "\n".join(cves) + + def _err_print_support_urls(self, msg=None): + """Print error + support urls.""" + if msg: + logger.error(msg) + print("For help, open an issue at http://github.com/ryran/rhsecapi\n" + "Or post a comment at https://access.redhat.com/discussions/2713931\n", file=sys.stderr) + + def _iavm_query(self, url): + """Get IAVA json from IAVM Mapper App.""" + logger.info("Getting '{0}' ...".format(url)) + try: + r = requests.get(url, auth=()) + except requests.exceptions.ConnectionError as e: + self._err_print_support_urls(e) + return [] + except requests.exceptions.RequestException as e: + self._err_print_support_urls(e) + return [] + except requests.exceptions.HTTPError as e: + self._err_print_support_urls(e) + return [] + try: + result = r.json() + except: + logger.error("Login error; unable to get IAVA info") + print("IAVA→CVE mapping data is not provided by the public RH Security Data API.\n" + "Instead, this uses the IAVM Mapper App (access.redhat.com/labs/iavmmapper).\n\n" + "Access to this data requires RH Customer Portal credentials be provided.\n" + "Create a ~/.netrc with the following contents:\n\n" + "machine access.redhat.com\n" + " login YOUR-CUSTOMER-PORTAL-LOGIN\n" + " password YOUR_PASSWORD_HERE\n", + file=sys.stderr) + self._err_print_support_urls() + return [] + return result + + + def get_iava(self, iavaId): + """Validate IAVA number and return json.""" + url = 'https://access.redhat.com/labs/iavmmapper/api/iava/' + result = self._iavm_query(url) + if iavaId not in result: + logger.error("IAVM Mapper (https://access.redhat.com/labs/iavmmapper) has no knowledge of '{0}'".format(iavaId)) + self._err_print_support_urls() + return [] + url += '{0}'.format(iavaId) + result = self._iavm_query(url) + logger.log(25, "{0} CVEs found with search".format(len(result['IAVM']['CVEs']['CVENumber']))) + return result + + +if __name__ == "__main__": + a = ApiClient('info') + # print(a.mget_cves(sys.stdin), end="") + print(a.cve_search_query('per_page=5', outFormat='json')) \ No newline at end of file diff --git a/rhsecapi.py b/rhsecapi.py index df16f64..9604921 100755 --- a/rhsecapi.py +++ b/rhsecapi.py @@ -18,12 +18,10 @@ # Modules from standard library from __future__ import print_function import argparse -from sys import exit, stderr, stdin -import requests, json, re -import textwrap, fcntl, termios, struct -import multiprocessing.dummy as multiprocessing -import copy_reg -import types +import requests +import sys +import logging +import rhsda # Optional module try: @@ -33,161 +31,24 @@ print("Missing optional python module: argcomplete\n" "Install it to enable bash auto-magic tab-completion:\n" " yum/dnf install python-pip; pip install argcomplete\n" - " activate-global-python-argcomplete; (Then restart shell)\n", file=stderr) + " activate-global-python-argcomplete; (Then restart shell)\n", file=sys.stderr) haveArgcomplete = False # Globals prog = 'rhsecapi' vers = {} -vers['version'] = '0.9.1' -vers['date'] = '2016/11/10' -# Set default number of worker threads to use -if multiprocessing.cpu_count() < 4: - defaultThreads = 4 -else: - defaultThreads = multiprocessing.cpu_count() * 2 -# All supported API-provided CVE fields -allFields = [ - 'threat_severity', - 'public_date', - 'iava', - 'cwe', - 'cvss', - 'cvss3', - 'bugzilla', - 'acknowledgement', - 'details', - 'statement', - 'mitigation', - 'upstream_fix', - 'references', - 'affected_release', - 'package_state', - ] -# All supported fields minus the few text-heavy ones -mostFields = list(allFields) -notMostFields = [ - 'acknowledgement', - 'details', - 'statement', - 'mitigation', - 'references', - ] -for f in notMostFields: - mostFields.remove(f) -# Simple set of default fields -defaultFields = [ - 'threat_severity', - 'public_date', - 'bugzilla', - 'affected_release', - 'package_state', - ] -# Aliases to make life easier -friendlyFieldAliases = { - 'severity': 'threat_severity', - 'date': 'public_date', - 'fixed_releases': 'affected_release', - 'fixed': 'affected_release', - 'releases': 'affected_release', - 'fix_states': 'package_state', - 'states': 'package_state', - } -friendlyFieldAliases_printable = [ - "threat_severity → severity", - "public_date → date", - "affected_release → fixed_releases or fixed or releases", - "package_state → fix_states or states", - ] -allFieldsPlusAliases = list(allFields) -allFieldsPlusAliases.extend([k for k in friendlyFieldAliases]) +vers['version'] = '1.0.0_rc1' +vers['date'] = '2016/18/10' -def _reduce_method(m): - if m.__self__ is None: - return getattr, (m.__class__, m.__func__.__name__) - else: - return getattr, (m.__self__, m.__func__.__name__) - - -# Make it possible for pickle to serialize class functions -copy_reg.pickle(types.MethodType, _reduce_method) - - -def err_print_support_urls(msg=None): - """Print error + support urls.""" - if msg: - print(msg, file=stderr) - print("For help, open an issue at http://github.com/ryran/rhsecapi\n" - "Or post a comment at https://access.redhat.com/discussions/2713931\n", file=stderr) - - -class RedHatSecDataApiClient: - """Portable object to interface with the Red Hat Security Data API. - - https://access.redhat.com/documentation/en/red-hat-security-data-api/ - - Requires: - requests - sys - """ - def __init__(self, progressToStderr=False, apiurl='https://access.redhat.com/labs/securitydataapi'): - self.apiurl = apiurl - self.progressToStderr = progressToStderr - - def __validate_data_type(self, dt): - dataTypes = ['cvrf', 'cve', 'oval'] - if dt not in dataTypes: - raise ValueError("Invalid data type ('{0}') requested; should be one of: {1}".format(dt, ", ".join(dataTypes))) - - def __get(self, url, params={}): - url = self.apiurl + url - u = "" - if params: - for k in params: - if params[k]: - u += "&{0}={1}".format(k, params[k]) - u = u.replace("&", "?", 1) - if self.progressToStderr: - print("Getting '{0}{1}' ...".format(url, u), file=stderr) - r = requests.get(url, params=params) - r.raise_for_status() - return r.url, r.json() - - def _search(self, dataType, params=None): - self.__validate_data_type(dataType) - url = '/{0}.json'.format(dataType) - if isinstance(params, dict): - return self.__get(url, params) - elif params: - url += '?{0}'.format(params) - return self.__get(url) - - def _retrieve(self, dataType, query): - self.__validate_data_type(dataType) - url = '/{0}/{1}.json'.format(dataType, query) - return self.__get(url) - - def search_cvrf(self, params=None): - return self._search('cvrf', params) - - def search_cve(self, params=None): - return self._search('cve', params) - - def search_oval(self, params=None): - return self._search('oval', params) - - def get_cvrf(self, rhsa): - return self._retrieve('cvrf', rhsa) - - def get_cvrf_oval(self, rhsa): - return self._retrieve('cvrf', '{0}/oval'.format(rhsa)) - - def get_cve(self, cve): - return self._retrieve('cve', cve) - - def get_oval(self, rhsa): - return self._retrieve('oval', rhsa) +# Logging +logging.addLevelName(25, 'NOTICE') +consolehandler = logging.StreamHandler() +consolehandler.setLevel('DEBUG') +consolehandler.setFormatter(logging.Formatter("[%(levelname)-7s] %(name)s: %(message)s")) +logger = logging.getLogger('rhsecapi') +logger.setLevel('NOTICE') +logger.addHandler(consolehandler) def fpaste_it(inputdata, lang='text', author=None, password=None, private='no', expire=28, project=None, url='http://paste.fedoraproject.org'): @@ -217,9 +78,9 @@ def fpaste_it(inputdata, lang='text', author=None, password=None, private='no', p = urlencode(params) pasteSizeKiB = len(p)/1024.0 if pasteSizeKiB >= 512: - raise ValueError("Fedora Pastebin client: WARN: paste size ({0:.1f} KiB) too large (max size: 512 KiB)".format(pasteSizeKiB)) + raise ValueError("Fedora Pastebin client WARN: paste size ({0:.1f} KiB) too large (max size: 512 KiB)".format(pasteSizeKiB)) # Print status, then connect - print("Fedora Pastebin client: INFO: Uploading {0:.1f} KiB...".format(pasteSizeKiB), file=stderr) + logger.log(25, "Fedora Pastebin client uploading {0:.1f} KiB...".format(pasteSizeKiB)) r = requests.post(url, params) r.raise_for_status() try: @@ -230,22 +91,22 @@ def fpaste_it(inputdata, lang='text', author=None, password=None, private='no', tmp = NamedTemporaryFile(delete=False) print(r.content, file=tmp) tmp.flush() - raise ValueError("Fedora Pastebin client: ERROR: Didn't receive expected JSON response (saved to '{0}' for debugging)".format(tmp.name)) + raise ValueError("Fedora Pastebin client ERROR: Didn't receive expected JSON response (saved to '{0}' for debugging)".format(tmp.name)) # Error keys adapted from Jason Farrell's fpaste if j.has_key('error'): err = j['error'] if err == 'err_spamguard_php': - raise ValueError("Fedora Pastebin server: ERROR: Poster's IP rejected as malicious") + raise ValueError("Fedora Pastebin server ERROR: Poster's IP rejected as malicious") elif err == 'err_spamguard_noflood': - raise ValueError("Fedora Pastebin server: ERROR: Poster's IP rejected as trying to flood") + raise ValueError("Fedora Pastebin server ERROR: Poster's IP rejected as trying to flood") elif err == 'err_spamguard_stealth': - raise ValueError("Fedora Pastebin server: ERROR: Paste input triggered spam filter") + raise ValueError("Fedora Pastebin server ERROR: Paste input triggered spam filter") elif err == 'err_spamguard_ipban': - raise ValueError("Fedora Pastebin server: ERROR: Poster's IP rejected as permanently banned") + raise ValueError("Fedora Pastebin server ERROR: Poster's IP rejected as permanently banned") elif err == 'err_author_numeric': - raise ValueError("Fedora Pastebin server: ERROR: Poster's author should be alphanumeric") + raise ValueError("Fedora Pastebin server ERROR: Poster's author should be alphanumeric") else: - raise ValueError("Fedora Pastebin server: ERROR: '{0}'".format(err)) + raise ValueError("Fedora Pastebin server ERROR: '{0}'".format(err)) # Put together URL with optional hash if requested pasteUrl = '{0}/{1}'.format(url, j['result']['id']) if 'yes' in private and j['result'].has_key('hash'): @@ -253,15 +114,6 @@ def fpaste_it(inputdata, lang='text', author=None, password=None, private='no', return pasteUrl -def jprint(jsoninput, printOutput=True): - """Pretty-print jsoninput.""" - j = json.dumps(jsoninput, sort_keys=True, indent=2) - if printOutput: - print(j) - else: - return j - - class CustomFormatter(argparse.RawDescriptionHelpFormatter): """This custom formatter eliminates the duplicate metavar in help lines.""" def _format_action_invocation(self, action): @@ -298,9 +150,6 @@ def parse_args(): add_help=False, epilog=epilog, formatter_class=fmt) - # Regex to match a CVE id string - cve_regex_str = 'CVE-[0-9]{4}-[0-9]{4,}' - re_cve = re.compile(cve_regex_str, re.IGNORECASE) # New group g_listByAttr = p.add_argument_group( 'FIND CVES BY ATTRIBUTE') @@ -360,22 +209,22 @@ def parse_args(): help="Extract CVEs them from search query (as initiated by at least one of the --q-xxx options)") g_getCve.add_argument( '-0', '--extract-stdin', action='store_true', - help="Extract CVEs from stdin (CVEs will be matched by case-insensitive regex '{0}' and duplicates will be discarded); note that terminal width auto-detection is not possible in this mode and WIDTH defaults to '70' (but can be overridden with '--width')".format(cve_regex_str)) + help="Extract CVEs from stdin (CVEs will be matched by case-insensitive regex '{0}' and duplicates will be discarded); note that terminal width auto-detection is not possible in this mode and WIDTH defaults to '70' (but can be overridden with '--width')".format(rhsda.cve_regex_string)) # New group g_cveDisplay = p.add_argument_group( 'CVE DISPLAY OPTIONS') g_cveDisplay0 = g_cveDisplay.add_mutually_exclusive_group() g_cveDisplay0.add_argument( - '-f', '--fields', metavar="FIELDS", default=','.join(defaultFields), - help="Customize field display via comma-separated case-insensitive list (default: {0}); see --all-fields option for full list of official API-provided fields; shorter field aliases: {1}; optionally prepend FIELDS with plus (+) sign to add fields to the default (e.g., '-f +iava,cvss3') or a caret (^) to remove fields from the default (e.g., '-f ^bugzilla,severity')".format(", ".join(defaultFields), ", ".join(friendlyFieldAliases_printable))) + '-f', '--fields', metavar="FIELDS", default='BASE', + help="Customize field display via comma-separated case-insensitive list (default: {0}); see --all-fields option for full list of official API-provided fields; shorter field aliases: {1}; optionally prepend FIELDS with plus (+) sign to add fields to the default (e.g., '-f +iava,cvss3') or a caret (^) to remove fields from all-fields (e.g., '-f ^mitigation,severity')".format(", ".join(rhsda.cveFields.base), ", ".join(rhsda.cveFields.aliases_printable))) g_cveDisplay0.add_argument( '-a', '--all-fields', dest='fields', action='store_const', - const=','.join(allFields), - help="Display all supported fields (currently: {0})".format(", ".join(allFields))) + const='ALL', + help="Display all supported fields (currently: {0})".format(", ".join(rhsda.cveFields.all))) g_cveDisplay0.add_argument( '-m', '--most-fields', dest='fields', action='store_const', - const=','.join(mostFields), - help="Display all fields mentioned above except the heavy-text ones -- (excludes: {0})".format(", ".join(notMostFields))) + const='MOST', + help="Display all fields mentioned above except the heavy-text ones -- (excludes: {0})".format(", ".join(rhsda.cveFields.not_most))) g_cveDisplay.add_argument( '--spotlight', dest='spotlightedProduct', metavar="PRODUCT", help="Spotlight a particular PRODUCT via case-insensitive regex; this hides CVEs where 'FIXED_RELEASES' or 'FIX_STATES' don't have an item with 'cpe' (e.g. 'cpe:/o:redhat:enterprise_linux:7') or 'product_name' (e.g. 'Red Hat Enterprise Linux 7') matching PRODUCT; this also hides all items in 'FIXED_RELEASES' & 'FIX_STATES' that don't match PRODUCT") @@ -390,16 +239,16 @@ def parse_args(): 'GENERAL OPTIONS') g_general.add_argument( '-w', '--wrap', metavar="WIDTH", dest='wrapWidth', nargs='?', default=1, const=70, type=int, - help="Change wrap-width of long fields (acknowledgement, details, statement, mitigation) in non-json output (default: wrapping WIDTH equivalent to TERMWIDTH-2 unless using '--pastebin' where default WIDTH is '168'; specify '0' to disable wrapping; WIDTH defaults to '70' if option is used but WIDTH is omitted)") + help="Change wrap-width of long fields (acknowledgement, details, statement, mitigation, references) in non-json output (default: wrapping WIDTH equivalent to TERMWIDTH-2 unless using '--pastebin' where default WIDTH is '168'; specify '0' to disable wrapping; WIDTH defaults to '70' if option is used but WIDTH is omitted)") g_general.add_argument( '-c', '--count', action='store_true', help="Exit after printing CVE counts") g_general.add_argument( - '-v', '--verbose', action='store_true', - help="Print API urls & other debugging info to stderr") + '-l', '--loglevel', choices=['debug','info','notice','warning'], default='notice', + help="Configure logging level threshold; lower from the default of 'notice' to see extra details printed to stderr") g_general.add_argument( - '-t', '--threads', metavar="THREDS", type=int, default=defaultThreads, - help="Set number of concurrent worker threads to allow when making CVE queries (default on this system: {0})".format(defaultThreads)) + '-t', '--threads', metavar="THREDS", type=int, default=rhsda.numThreadsDefault, + help="Set number of concurrent worker threads to allow when making CVE queries (default on this system: {0})".format(rhsda.numThreadsDefault)) g_general.add_argument( '-p', '--pastebin', action='store_true', help="Send output to Fedora Project Pastebin (paste.fedoraproject.org) and print only URL to stdout") @@ -407,10 +256,6 @@ def parse_args(): '--dryrun', action='store_true', help="Skip CVE retrieval; this option only makes sense in concert with --extract-stdin, for the purpose of quickly getting a printable list of CVE ids from stdin") # g_general.add_argument( - # '--p-lang', metavar="LANG", default='text', - # choices=['ABAP', 'Actionscript', 'ADA', 'Apache Log', 'AppleScript', 'APT sources.list', 'ASM (m68k)', 'ASM (pic16)', 'ASM (x86)', 'ASM (z80)', 'ASP', 'AutoIT', 'Backus-Naur form', 'Bash', 'Basic4GL', 'BlitzBasic', 'Brainfuck', 'C', 'C for Macs', 'C#', 'C++', 'C++ (with QT)', 'CAD DCL', 'CadLisp', 'CFDG', 'CIL / MSIL', 'COBOL', 'ColdFusion', 'CSS', 'D', 'Delphi', 'Diff File Format', 'DIV', 'DOS', 'DOT language', 'Eiffel', 'Fortran', "FourJ's Genero", 'FreeBasic', 'GetText', 'glSlang', 'GML', 'gnuplot', 'Groovy', 'Haskell', 'HQ9+', 'HTML', 'INI (Config Files)', 'Inno', 'INTERCAL', 'IO', 'Java', 'Java 5', 'Javascript', 'KiXtart', 'KLone C & C++', 'LaTeX', 'Lisp', 'LOLcode', 'LotusScript', 'LScript', 'Lua', 'Make', 'mIRC', 'MXML', 'MySQL', 'NSIS', 'Objective C', 'OCaml', 'OpenOffice BASIC', 'Oracle 8 & 11 SQL', 'Pascal', 'Perl', 'PHP', 'Pixel Bender', 'PL/SQL', 'POV-Ray', 'PowerShell', 'Progress (OpenEdge ABL)', 'Prolog', 'ProvideX', 'Python', 'Q(uick)BASIC', 'robots.txt', 'Ruby', 'Ruby on Rails', 'SAS', 'Scala', 'Scheme', 'Scilab', 'SDLBasic', 'Smalltalk', 'Smarty', 'SQL', 'T-SQL', 'TCL', 'thinBasic', 'TypoScript', 'Uno IDL', 'VB.NET', 'Verilog', 'VHDL', 'VIM Script', 'Visual BASIC', 'Visual Fox Pro', 'Visual Prolog', 'Whitespace', 'Winbatch', 'Windows Registry Files', 'X++', 'XML', 'Xorg.conf'], - # help="Set the development language for the paste (default: 'text')") - # g_general.add_argument( # '-A', '--p-author', metavar="NAME", default=prog, # help="Set alphanumeric paste author (default: '{0}')".format(prog)) # g_general.add_argument( @@ -442,7 +287,7 @@ def parse_args(): p.print_help(file=tmp) tmp.flush() call(['less', tmp.name]) - exit() + sys.exit() # Add search params to dict o.searchParams = { 'before': o.q_before, @@ -467,498 +312,103 @@ def parse_args(): else: o.doSearch = True if o.q_iava and o.doSearch: - print("{0}: The --q-iava option is not compatible with other --q-xxx options; it can only be used alone".format(prog), file=stderr) - exit(1) - if o.extract_stdin and not stdin.isatty(): - found = [] - for line in stdin: - # Iterate over each line of stdin adding the returned list to our found list - found.extend(re_cve.findall(line)) - if found: - matchCount = len(found) - # Converting to a set removes duplicates - found = list(set(found)) - uniqueCount = len(found) - print("{0}: Found {1} CVEs in stdin; {2} duplicates removed\n".format(prog, uniqueCount, matchCount-uniqueCount), file=stderr) - o.cves.extend(found) - else: - print("{0}: No CVEs (matching regex: '{1}') found in stdin\n".format(prog, cve_regex_str), file=stderr) - # Convert all CVE ids to uppercase - o.cves = [x.upper() for x in o.cves] + logger.error("The --q-iava option is incompatible with other --q-xxx options; it can only be used alone") + sys.exit(1) + if o.extract_stdin and not sys.stdin.isatty(): + found = rhsda.extract_cves_from_input(sys.stdin) + o.cves.extend(found) # If only one CVE (common use-case), let's validate its format - if len(o.cves) == 1 and not re_cve.match(o.cves[0]): - print("{0}: Invalid CVE format; expected: 'CVE-YYYY-XXXX'\n".format(prog), file=stderr) + if len(o.cves) == 1 and not rhsda.cve_regex.match(o.cves[0]): + logger.error("Invalid CVE format '{0}'; expected: 'CVE-YYYY-XXXX'".format(o.cves[0])) o.showUsage = True # If no search (--q-xxx) and no CVEs mentioned - if not (o.doSearch or o.cves or o.q_iava): - print("{0}: Must specify CVEs to retrieve or search to perform (one of the --q-xxx opts)\n".format(prog), file=stderr) + if not o.showUsage and not (o.doSearch or o.cves or o.q_iava): + logger.error("Must specify a search to perform (one of the --q-xxx opts) or CVEs to retrieve") o.showUsage = True if o.showUsage: p.print_usage() print("\nRun {0} --help for full help page\n\n{1}".format(prog, epilog)) - exit() - if o.fields: - o.fields = o.fields.lower() - # Let's validate our fields; start by saving to a list - if o.fields.startswith('+') or o.fields.startswith('^'): - fields = o.fields[1:].split(',') - else: - fields = o.fields.split(',') - postProcessedFields = [] - for f in fields: - # If a field isn't known, exit - if f not in allFieldsPlusAliases: - print("{0}: Field '{1}' is not a supported field; valid fields:\n" - "{2}".format(prog, f, ", ".join(allFieldsPlusAliases)), file=stderr) - exit(1) - if f not in allFields: - f = friendlyFieldAliases[f] - # If using '--fields -xxx' format, remove - if o.fields.startswith('^') and f in defaultFields: - defaultFields.remove(f) - # If using '--fields +xxx' format, add - elif o.fields.startswith('+'): - defaultFields.append(f) - # Otherwise - else: - postProcessedFields.append(f) - # If we added/removed, we need to reset o.fields to new value - if o.fields.startswith('+') or o.fields.startswith('^'): - o.fields = ','.join(defaultFields) - else: - o.fields = ','.join(postProcessedFields) + sys.exit() # If autowrap and using pastebin, set good width if o.wrapWidth == 1 and o.pastebin: o.wrapWidth = 168 + if o.json: + o.outFormat = 'jsonpretty' + else: + o.outFormat = 'plaintext' + logger.setLevel(o.loglevel.upper()) return o -class RHSecApiParse: - """Parse and print results returned from RedHatSecDataApiClient. - - Requires: - RedHatSecDataApiClient - json - sys - requests - re - - Conditional: - textwrap - fcntl - termios - struct - """ - - - def __init__(self, - fields='threat_severity,public_date,bugzilla,affected_release,package_state', - printUrls=False, rawOutput=False, onlyCount=False, verbose=False, wrapWidth=1, - spotlightedProduct=None): - """Initialize class settings.""" - self.rhsda = RedHatSecDataApiClient(verbose) - if len(fields): - self.desiredFields = fields.split(",") - else: - self.desiredFields = [] - self.printUrls = printUrls - self.rawOutput = rawOutput - self.output = "" - self.onlyCount = onlyCount - self.cveCount = 0 - if wrapWidth == 1: - if stdin.isatty(): - wrapWidth = self.get_terminal_width() - 2 - else: - print("{0}: Unable to auto-detect terminal width due to stdin redirection; setting WIDTH to 70".format(prog), file=stderr) - wrapWidth = 70 - if wrapWidth: - self.w = textwrap.TextWrapper(width=wrapWidth, initial_indent=" ", subsequent_indent=" ", replace_whitespace=False) - else: - self.w = 0 - self.spotlightedProduct = spotlightedProduct - - def get_terminal_width(self): - h, w, hp, wp = struct.unpack('HHHH', fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))) - return w - - def search_query(self, params): - """Perform a CVE search query based on params.""" - try: - url, result = self.rhsda.search_cve(params) - except requests.exceptions.ConnectionError as e: - print("{0}: {1}".format(prog, e), file=stderr) - err_print_support_urls() - exit(1) - except requests.exceptions.HTTPError as e: - print("{0}: {1}".format(prog, e), file=stderr) - err_print_support_urls() - exit(1) - except requests.exceptions.RequestException as e: - print("{0}: {1}".format(prog, e), file=stderr) - err_print_support_urls() - exit(1) - print("CVEs found: {0}".format(len(result)), file=stderr) - if not self.onlyCount: - print(file=stderr) - return result - - def _check_field(self, field, jsoninput): - """Return True if field is desired and exists in jsoninput.""" - if field in self.desiredFields and jsoninput.has_key(field): - return True - return False - - def _stripjoin(self, input, oneLineEach=False): - """Strip whitespace from input or input list.""" - text = "" - if isinstance(input, list): - for i in input: - text += i.encode('utf-8').strip() - if oneLineEach: - text += "\n" - else: - text += " " - else: - text = input.encode('utf-8').strip() - if oneLineEach: - text = "\n" + text - text = re.sub(r"\n+", "\n ", text) - else: - text = re.sub(r"\n+", " ", text) - if self.w: - text = "\n" + "\n".join(self.w.wrap(text)) - return text - - def print_cve(self, cve): - """Print CVE data.""" - # Convenience: - spotlightedProduct = self.spotlightedProduct - # Output array: - out = [] - try: - requrl, j = self.rhsda.get_cve(cve) - except requests.exceptions.ConnectionError as e: - print("{0}: {1}".format(prog, e), file=stderr) - err_print_support_urls() - exit(1) - except requests.exceptions.HTTPError as e: - print("{0}: {1}".format(prog, e), file=stderr) - if not self.onlyCount: - out.append("{0}\n Not present in Red Hat CVE database\n".format(cve)) - if cve.startswith("CVE-"): - out.append(" Try https://cve.mitre.org/cgi-bin/cvename.cgi?name={0}\n\n".format(cve)) - if spotlightedProduct: - return "", False - else: - return "".join(out), False - except requests.exceptions.RequestException as e: - print("{0}: {1}".format(prog, e), file=stderr) - err_print_support_urls() - exit(1) - - # CVE name always printed - name = "" - if cve != j['name']: - name = " [{0}]".format(j['name']) - url = "" - if self.printUrls: - url = " (https://access.redhat.com/security/cve/{0})".format(cve) - out.append("{0}{1}{2}\n".format(cve, name, url)) - - # If --fields='' was used, done - if not self.desiredFields: - return "".join(out), True - - if self._check_field('threat_severity', j): - url = "" - if self.printUrls: - url = " (https://access.redhat.com/security/updates/classification)" - out.append(" SEVERITY: {0} Impact{1}\n".format(j['threat_severity'], url)) - - if self._check_field('public_date', j): - out.append(" DATE: {0}\n".format(j['public_date'].split("T")[0])) - - if self._check_field('iava', j): - out.append(" IAVA: {0}\n".format(j['iava'])) - - if self._check_field('cwe', j): - out.append(" CWE: {0}".format(j['cwe'])) - if self.printUrls: - cwes = re.findall("CWE-[0-9]+", j['cwe']) - if len(cwes) == 1: - out.append(" (http://cwe.mitre.org/data/definitions/{0}.html)\n".format(cwes[0].lstrip("CWE-"))) - else: - out.append("\n") - for c in cwes: - out.append(" http://cwe.mitre.org/data/definitions/{0}.html\n".format(c.lstrip("CWE-"))) - else: - out.append("\n") - - if self._check_field('cvss', j): - cvss_scoring_vector = j['cvss']['cvss_scoring_vector'] - if self.printUrls: - cvss_scoring_vector = "http://nvd.nist.gov/cvss.cfm?version=2&vector={0}".format(cvss_scoring_vector) - out.append(" CVSS: {0} ({1})\n".format(j['cvss']['cvss_base_score'], cvss_scoring_vector)) - - if self._check_field('cvss3', j): - cvss3_scoring_vector = j['cvss3']['cvss3_scoring_vector'] - if self.printUrls: - cvss3_scoring_vector = "https://www.first.org/cvss/calculator/3.0#{0}".format(cvss3_scoring_vector) - out.append(" CVSS3: {0} ({1})\n".format(j['cvss3']['cvss3_base_score'], cvss3_scoring_vector)) - - if 'bugzilla' in self.desiredFields: - if j.has_key('bugzilla'): - if self.printUrls: - bug = j['bugzilla']['url'] - else: - bug = j['bugzilla']['id'] - out.append(" BUGZILLA: {0}\n".format(bug)) - else: - out.append(" BUGZILLA: No Bugzilla data\n") - out.append(" Too new or too old? See: https://bugzilla.redhat.com/show_bug.cgi?id=CVE_legacy\n") - - if self._check_field('acknowledgement', j): - out.append(" ACKNOWLEDGEMENT: {0}\n".format(self._stripjoin(j['acknowledgement']))) - - if self._check_field('details', j): - out.append(" DETAILS: {0}\n".format(self._stripjoin(j['details']))) - - if self._check_field('statement', j): - out.append(" STATEMENT: {0}\n".format(self._stripjoin(j['statement']))) - - if self._check_field('mitigation', j): - out.append(" MITIGATION: {0}\n".format(self._stripjoin(j['mitigation']))) - - if self._check_field('upstream_fix', j): - out.append(" UPSTREAM_FIX: {0}\n".format(j['upstream_fix'])) - - if self._check_field('references', j): - out.append(" REFERENCES:{0}\n".format(self._stripjoin(j['references'], oneLineEach=True))) - - # Setup product-spotlighting - if spotlightedProduct: - re_prod = re.compile(spotlightedProduct, re.IGNORECASE) - affected_release_foundSpotlightedProduct = False - package_state_foundSpotlightedProduct = False - - if self._check_field('affected_release', j): - if spotlightedProduct: - out.append(" FIXED_RELEASES matching '{0}':\n".format(spotlightedProduct)) - else: - out.append(" FIXED_RELEASES:\n") - affected_release = j['affected_release'] - if isinstance(affected_release, dict): - # When there's only one, it doesn't show up in a list - affected_release = [affected_release] - for release in affected_release: - if spotlightedProduct: - if re_prod.search(release['product_name']) or re_prod.search(release['cpe']): - affected_release_foundSpotlightedProduct = True - else: - # If product doesn't match spotlight, go to next - continue - package = "" - if release.has_key('package'): - package = " [{0}]".format(release['package']) - advisory = release['advisory'] - if self.printUrls: - advisory = "https://access.redhat.com/errata/{0}".format(advisory) - out.append(" {0}{1}: {2}\n".format(release['product_name'], package, advisory)) - if spotlightedProduct and not affected_release_foundSpotlightedProduct: - # If nothing found, remove the "FIXED_RELEASES" heading - out.pop() - - if self._check_field('package_state', j): - if spotlightedProduct: - out.append(" FIX_STATES matching '{0}':\n".format(spotlightedProduct)) - else: - out.append(" FIX_STATES:\n") - package_state = j['package_state'] - if isinstance(package_state, dict): - # When there's only one, it doesn't show up in a list - package_state = [package_state] - for state in package_state: - if spotlightedProduct: - if re_prod.search(state['product_name']) or re_prod.search(state['cpe']): - package_state_foundSpotlightedProduct = True - else: - # If product doesn't match spotlight, go to next - continue - package_name = "" - if state.has_key('package_name'): - package_name = " [{0}]".format(state['package_name']) - out.append(" {2}: {0}{1}\n".format(state['product_name'], package_name, state['fix_state'])) - if spotlightedProduct and not package_state_foundSpotlightedProduct: - # If nothing found, remove the "FIX_STATES" heading - out.pop() - - if spotlightedProduct and not (affected_release_foundSpotlightedProduct or package_state_foundSpotlightedProduct): - return "", None - - # For the sake of efficiency, the following two checks should be at the beginning - # However, being down here allows --count & --json to respect --spotlight-product - if self.onlyCount: - return "", True - if self.rawOutput: - out = jprint(j, False) + "\n" - return out, True - - # Add one final newline to the end - out.append("\n") - return "".join(out), True - - -def iavm_query(url, progressToStderr=False): - """Get IAVA json from IAVM Mapper App.""" - if progressToStderr: - print("Getting '{0}' ...".format(url), file=stderr) - try: - r = requests.get(url, auth=()) - except requests.exceptions.ConnectionError as e: - print("{0}: {1}".format(prog, e), file=stderr) - err_print_support_urls() - exit(1) - except requests.exceptions.HTTPError as e: - print("{0}: {1}".format(prog, e), file=stderr) - err_print_support_urls() - exit(1) - except requests.exceptions.RequestException as e: - print("{0}: {1}".format(prog, e), file=stderr) - err_print_support_urls() - exit(1) - try: - result = r.json() - except: - print("{0}: Login error; unable to get IAVA info\n\n" - "IAVA->CVE mapping data is not provided by the public RH Security Data API.\n" - "Instead, this uses the IAVM Mapper App (access.redhat.com/labs/iavmmapper).\n\n" - "Access to this data requires RH Customer Portal credentials be provided.\n" - "Create a ~/.netrc with the following contents:\n\n" - "machine access.redhat.com\n" - " login YOUR-CUSTOMER-PORTAL-LOGIN\n" - " password YOUR_PASSWORD_HERE\n".format(prog), - file=stderr) - err_print_support_urls() - exit(1) - return result - - -def get_iava(iavaId, progressToStderr=False, onlyCount=False): - """Validate IAVA number and return json.""" - url = 'https://access.redhat.com/labs/iavmmapper/api/iava/' - result = iavm_query(url, progressToStderr=progressToStderr) - if iavaId not in result: - print("{0}: IAVM Mapper (https://access.redhat.com/labs/iavmmapper) has no knowledge of '{1}'\n".format(prog, iavaId), file=stderr) - err_print_support_urls() - exit(1) - url += '{0}'.format(iavaId) - result = iavm_query(url, progressToStderr=progressToStderr) - try: - print("CVEs found: {0}".format(len(result['IAVM']['CVEs']['CVENumber'])), file=stderr) - except: - err_print_support_urls() - raise - if not onlyCount: - print(file=stderr) - return result - - def main(opts): - a = RHSecApiParse(opts.fields, opts.printUrls, opts.json, opts.count, opts.verbose, opts.wrapWidth, opts.spotlightedProduct) + apiclient = rhsda.ApiClient(opts.loglevel) searchOutput = [] iavaOutput = [] - cveOutput = [] + cveOutput = "" if opts.doSearch: - result = a.search_query(opts.searchParams) + result = apiclient.cve_search_query(opts.searchParams, 'json') if opts.extract_search: - if result: - for i in result: - opts.cves.append(i['CVE']) + for cve in result: + opts.cves.append(cve['CVE']) elif not opts.count: if opts.json: - searchOutput.append(jprint(result, False) + "\n") + searchOutput.append(rhsda.jprint(result, False)) else: for cve in result: searchOutput.append(cve['CVE'] + "\n") if not opts.pastebin: - print("".join(searchOutput)) + print(file=sys.stderr) + print("".join(searchOutput), end="") elif opts.q_iava: - result = get_iava(opts.q_iava, opts.verbose, opts.count) + result = apiclient.get_iava(opts.q_iava) + if not result: + sys.exit(1) if opts.extract_search: - if result: - opts.cves.extend(result['IAVM']['CVEs']['CVENumber']) + opts.cves.extend(result['IAVM']['CVEs']['CVENumber']) elif not opts.count: if opts.json: - iavaOutput.append(jprint(result, False) + "\n") + iavaOutput.append(rhsda.jprint(result, False)) else: for cve in result['IAVM']['CVEs']['CVENumber']: iavaOutput.append(cve + "\n") if not opts.pastebin: - print("".join(iavaOutput)) + print(file=sys.stderr) + print("".join(iavaOutput), end="") if opts.dryrun and opts.cves: - print("Skipping CVE retrieval due to --dryrun\n" - "Number of CVEs that would have been retrieved: {0}".format(len(opts.cves)), file=stderr) + logger.notice("Skipping CVE retrieval due to --dryrun; would have retrieved: {0}".format(len(opts.cves))) cveOutput = " ".join(opts.cves) + "\n" elif opts.cves: - if opts.threads > len(opts.cves): - opts.threads = len(opts.cves) - if opts.verbose: - print("DEBUG THREADS: '{0}'".format(opts.threads), file=stderr) - if searchOutput: - searchOutput.append("\n") - if iavaOutput: - iavaOutput.append("\n") - # Disable sigint before starting process pool - import signal - original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) - pool = multiprocessing.Pool(processes=opts.threads) - # Re-enable receipt of sigint - signal.signal(signal.SIGINT, original_sigint_handler) - try: - p = pool.map_async(a.print_cve, opts.cves) - # Need to specify timeout; see: http://stackoverflow.com/a/35134329 - results = p.get(300) - except KeyboardInterrupt: - print("\n{0}: Received KeyboardInterrupt; terminating http request-worker threads".format(prog), file=stderr) - pool.terminate() - exit() - else: - pool.close() - pool.join() - cveOutput, successValues = zip(*results) - total = len(opts.cves) - hidden = successValues.count(None) - valid = successValues.count(True) - invalid = successValues.count(False) - print("Valid Red Hat CVE results retrieved: {0} of {1}".format(valid + hidden, total), file=stderr) - if hidden: - print("Results matching spotlight-product option: {0} of {1}".format(valid, total), file=stderr) - if invalid: - print("Invalid CVE queries: {0} of {1}".format(invalid, total), file=stderr) + if searchOutput or iavaOutput: + print(file=sys.stderr) + cveOutput = apiclient.mget_cves(cves=opts.cves, + numThreads=opts.threads, + onlyCount=opts.count, + outFormat=opts.outFormat, + urls=opts.printUrls, + fields=opts.fields, + wrapWidth=opts.wrapWidth, + product=opts.spotlightedProduct) + if not opts.count: + print(file=sys.stderr) if opts.count: return - if opts.verbose and opts.cves: - print("DEBUG FIELDS: '{0}'".format(opts.fields), file=stderr) - print(file=stderr) if opts.pastebin: opts.p_lang = 'text' if opts.json or not opts.cves: opts.p_lang = 'Python' - data = "".join(searchOutput) + "".join(iavaOutput) + "".join(cveOutput) + data = "".join(searchOutput) + "".join(iavaOutput) + cveOutput try: response = fpaste_it(inputdata=data, author=prog, lang=opts.p_lang, expire=opts.pexpire) except ValueError as e: - print(e, file=stderr) - print("{0}: Submitting to pastebin failed; print results to stdout instead? [y]".format(prog), file=stderr) + print(e, file=sys.stderr) + print("{0}: Submitting to pastebin failed; print results to stdout instead? [y]".format(prog), file=sys.stderr) answer = raw_input("> ") if "y" in answer or len(answer) == 0: print(data, end="") else: print(response) elif opts.cves: - print("".join(cveOutput), end="") - + print(cveOutput, end="") if __name__ == "__main__": @@ -967,6 +417,4 @@ def main(opts): main(opts) except KeyboardInterrupt: print("\n{0}: Received KeyboardInterrupt; exiting".format(prog)) - exit() -else: - a = RedHatSecDataApiClient(True) + sys.exit() From 31cb34ad4d9697d4078d4847ba67aa08e15ca86d Mon Sep 17 00:00:00 2001 From: Ryan Sawhill Aroha Date: Fri, 18 Nov 2016 12:29:10 -0500 Subject: [PATCH 2/3] update readme; rename --spotlight to --product #41 --- README.md | 736 ++++++++++++++++++++++++++++++++++++---------------- rhsda.py | 31 ++- rhsecapi.py | 8 +- 3 files changed, 533 insertions(+), 242 deletions(-) diff --git a/README.md b/README.md index 8eaa7b9..62078ce 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,8 @@ Specify as many CVEs on cmdline as needed; certain details are printed to stderr ``` $ rhsecapi CVE-2013-4113 CVE-2014-3669 CVE-2004-0230 CVE-2015-4642 -rhsecapi: 404 Client Error: Not Found for url: https://access.redhat.com/labs/securitydataapi/cve/CVE-2015-4642.json -Valid Red Hat CVE results retrieved: 3 of 4 -Invalid CVE queries: 1 of 4 +[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 3 of 4 +[NOTICE ] rhsda: Invalid CVE queries: 1 of 4 CVE-2013-4113 SEVERITY: Critical Impact @@ -69,17 +68,15 @@ CVE-2004-0230 CVE-2015-4642 Not present in Red Hat CVE database Try https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-4642 - ``` -A `--spotlight` option allows spotlighting a particular product via a case-insenstive regex, e.g., here's the same exact command above spotlighting EUS products: +A `--product` option allows spotlighting a particular product via a case-insenstive regex, e.g., here's the same exact command above spotlighting EUS products: ``` -$ rhsecapi CVE-2013-4113 CVE-2014-3669 CVE-2004-0230 CVE-2015-4642 --spotlight eus -rhsecapi: 404 Client Error: Not Found for url: https://access.redhat.com/labs/securitydataapi/cve/CVE-2015-4642.json -Valid Red Hat CVE results retrieved: 3 of 4 -Results matching spotlight-product option: 2 of 4 -Invalid CVE queries: 1 of 4 +$ rhsecapi CVE-2013-4113 CVE-2014-3669 CVE-2004-0230 CVE-2015-4642 --product eus +[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 3 of 4 +[NOTICE ] rhsda: Results matching spotlight-product option: 2 of 4 +[NOTICE ] rhsda: Invalid CVE queries: 1 of 4 CVE-2013-4113 SEVERITY: Critical Impact @@ -102,7 +99,7 @@ CVE-2014-3669 A `--urls` or `-u` option adds URLS ``` -$ rhsecapi CVE-2013-4113 CVE-2014-3669 CVE-2004-0230 CVE-2015-4642 --spotlight eus --urls 2>/dev/null +$ rhsecapi CVE-2013-4113 CVE-2014-3669 CVE-2004-0230 CVE-2015-4642 --product eus --urls 2>/dev/null CVE-2013-4113 (https://access.redhat.com/security/cve/CVE-2013-4113) SEVERITY: Critical Impact (https://access.redhat.com/security/updates/classification) DATE: 2013-07-11 @@ -127,30 +124,26 @@ First example: pasting newline-separated CVEs with shell heredoc redirection ``` $ rhsecapi --extract-stdin --count < CVE-2016-5630 -> CVE-2016-5631 -> CVE-2016-5632 -> CVE-2016-5633 -> CVE-2016-5634 -> CVE-2016-5635 -> EOF -rhsecapi: Found 6 CVEs in stdin; 0 duplicates removed - -rhsecapi: Unable to auto-detect terminal width due to stdin redirection; setting WIDTH to 70 -Valid Red Hat CVE results retrieved: 6 of 6 +CVE-2016-5630 +CVE-2016-5631 +CVE-2016-5632 +CVE-2016-5633 +CVE-2016-5634 +CVE-2016-5635 +EOF +[NOTICE ] rhsda: Found 6 CVEs in stdin; 0 duplicates removed +[WARNING] rhsda: Stdin redirection suppresses term-width auto-detection; setting WIDTH to 70 +[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 6 of 6 ``` Second example: piping in file(s) with `cat|` or file redirection (`< somefile`) ``` $ cat scan-results.csv | rhsecapi -0 -c -rhsecapi: Found 150 CVEs in stdin; 698 duplicates removed - -rhsecapi: Unable to auto-detect terminal width due to stdin redirection; setting WIDTH to 70 -rhsecapi: 404 Client Error: Not Found for url: https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-3197.json -rhsecapi: 404 Client Error: Not Found for url: https://access.redhat.com/labs/securitydataapi/cve/CVE-2015-4642.json -Valid Red Hat CVE results retrieved: 148 of 150 -Invalid CVE queries: 2 of 150 +[NOTICE ] rhsda: Found 150 CVEs in stdin; 698 duplicates removed +[WARNING] rhsda: Stdin redirection suppresses term-width auto-detection; setting WIDTH to 70 +[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 148 of 150 +[NOTICE ] rhsda: Invalid CVE queries: 2 of 150 ``` The CVE retrieval process is multi-threaded; with CPUcount < 4, it defaults to 4 threads; with CPUcount > 4, it defaults to `CPUcount * 2` @@ -164,26 +157,27 @@ $ rhsecapi --help | grep -A1 threads making CVE queries (default on this system: 8) $ time rhsecapi --q-empty --q-pagesize 48 --extract-search >/dev/null -CVEs found: 48 - -Valid Red Hat CVE results retrieved: 48 of 48 +[NOTICE ] rhsda: 48 CVEs found with search query +[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 48 of 48 -real 0m3.197s -user 0m0.613s -sys 0m0.077s +real 0m3.872s +user 0m0.825s +sys 0m0.055s ``` ## Installation -- **Option 1: Download python script directly from github and run it** - 1. Download very latest (potentially bleeding-edge & broken) version: `curl -LO https://raw.githubusercontent.com/ryran/rhsecapi/master/rhsecapi.py` - 1. Add execute bit: `chmod +x rhsecapi.py` - 1. Execute: `./rhsecapi.py` - -- **Option 2 for RHEL6, RHEL7, Fedora: Install rsaw's yum repo and then rhsecapi rpm** +- **Option 1 for RHEL6, RHEL7, Fedora: Install rsaw's yum repo and then rhsecapi rpm** 1. If you don't already have rsaw's yum repo due to xsos or upvm or something else, set it up with the following command: `yum install http://people.redhat.com/rsawhill/rpms/latest-rsawaroha-release.rpm` 1. Install rhsecapi: `yum install rhsecapi` 1. Execute: `rhsecapi` + +- **Option 2: Download latest release from github and run it** + 1. Go to [Releases](https://github.com/ryran/rhsecapi/releases) + 1. Download an extract a release + 1. Optional: `mkdir -p ~/bin; ln -sv /PATH/TO/rhsecapi.py ~/bin/rhsecapi` + 1. Execute: `rhsecapi` + ## Abbreviated usage @@ -194,14 +188,15 @@ usage: rhsecapi [--q-before YEAR-MM-DD] [--q-after YEAR-MM-DD] [--q-bug BZID] [--q-cwe CWEID] [--q-cvss SCORE] [--q-cvss3 SCORE] [--q-empty] [--q-pagesize PAGESZ] [--q-pagenum PAGENUM] [--q-raw RAWQUERY] [--q-iava IAVA] [-s] [-0] [-f FIELDS | -a | -m] - [--spotlight PRODUCT] [-j] [-u] [-w [WIDTH]] [-c] [-v] - [-t THREDS] [-p] [--dryrun] [-E [DAYS]] [-h] [--help] + [--product PRODUCT] [-j] [-u] [-w [WIDTH]] [-c] + [-l {debug,info,notice,warning}] [-t THREDS] [-p] [--dryrun] + [-E [DAYS]] [-h] [--help] [CVE [CVE ...]] - + Run rhsecapi --help for full help page VERSION: - rhsecapi v0.9.1 last mod 2016/11/10 + rhsecapi v1.0.0_rc2 last mod 2016/18/10 See to report bugs or RFEs ``` @@ -209,12 +204,12 @@ VERSION: ``` $ rhsecapi -- ---all-fields --help --q-after --q-empty --q-severity ---count --json --q-before --q-iava --spotlight ---dryrun --most-fields --q-bug --q-package --threads ---extract-search --pastebin --q-cvss --q-pagenum --urls ---extract-stdin --pexpire --q-cvss3 --q-pagesize --verbose ---fields --q-advisory --q-cwe --q-raw --wrap +--all-fields --help --product --q-cvss3 --q-pagesize +--count --json --q-advisory --q-cwe --q-raw +--dryrun --loglevel --q-after --q-empty --q-severity +--extract-search --most-fields --q-before --q-iava --threads +--extract-stdin --pastebin --q-bug --q-package --urls +--fields --pexpire --q-cvss --q-pagenum --wrap ``` ## Field display @@ -222,11 +217,10 @@ $ rhsecapi -- Add some fields to the defaults with `--fields +field[,field]...` and note that arguments to `--fields` are handled in a case-insensitive way ``` -$ rhsecapi CVE-2016-6302 -v --fields +CWE,cvss3 -DEBUG THREADS: '1' -Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-6302.json' ... -Valid Red Hat CVE results retrieved: 1 of 1 -DEBUG FIELDS: 'threat_severity,public_date,bugzilla,affected_release,package_state,cwe,cvss3' +$ rhsecapi CVE-2016-6302 --fields +CWE,cvss3 --loglevel info +[INFO ] rhsda: Using 1 worker threads +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-6302.json' ... +[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 1 of 1 CVE-2016-6302 SEVERITY: Moderate Impact @@ -250,29 +244,37 @@ CVE-2016-6302 Not affected: Red Hat Enterprise Linux 7 [openssl098e] ``` -Remove some fields from the defaults with `--fields ^field[,field]...` +Remove some fields from the list of all fields with `--fields ^field[,field]...` ``` -$ rhsecapi CVE-2016-6302 -vf ^FIXED_reLEASES,fIx_sTaTes -DEBUG THREADS: '1' -Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-6302.json' ... -Valid Red Hat CVE results retrieved: 1 of 1 -DEBUG FIELDS: 'threat_severity,public_date,bugzilla' +$ rhsecapi CVE-2016-6302 -f ^FIXED_reLEASES,fIx_sTaTes,DETAILS -l info +[INFO ] rhsda: Using 1 worker threads +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-6302.json' ... +[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 1 of 1 CVE-2016-6302 SEVERITY: Moderate Impact DATE: 2016-08-23 + IAVA: 2016-A-0262 + CWE: CWE-190->CWE-125 + CVSS: 4.3 (AV:N/AC:M/Au:N/C:N/I:N/A:P) + CVSS3: 5.9 (CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H) BUGZILLA: 1369855 + UPSTREAM_FIX: openssl 1.0.1u, openssl 1.0.2i + REFERENCES: + https://www.openssl.org/news/secadv/20160922.txt ``` Note that there are also two presets: `--all-fields` and `--most-fields` ``` -$ rhsecapi CVE-2016-6302 -v --most-fields 2>&1 | grep FIELDS -DEBUG FIELDS: 'threat_severity,public_date,iava,cwe,cvss,cvss3,bugzilla,upstream_fix,affected_release,package_state' +$ rhsecapi CVE-2016-6302 --loglevel debug --most-fields 2>&1 | grep fields +[DEBUG ] rhsda: Requested fields string: 'MOST' +[DEBUG ] rhsda: Enabled fields: 'threat_severity, public_date, iava, cwe, cvss, cvss3, bugzilla, upstream_fix, affected_release, package_state' -$ rhsecapi CVE-2016-6302 -v --all-fields 2>&1 | grep FIELDS -DEBUG FIELDS: 'threat_severity,public_date,iava,cwe,cvss,cvss3,bugzilla,acknowledgement,details,statement,mitigation,upstream_fix,references,affected_release,package_state' +$ rhsecapi CVE-2016-6302 --loglevel debug --all-fields 2>&1 | grep fields +[DEBUG ] rhsda: Requested fields string: 'ALL' +[DEBUG ] rhsda: Enabled fields: 'threat_severity, public_date, iava, cwe, cvss, cvss3, bugzilla, acknowledgement, details, statement, mitigation, upstream_fix, references, affected_release, package_state' ``` ## Find CVEs @@ -281,74 +283,55 @@ The `--q-xxx` options can be combined to craft a search, listing CVEs via a sing ### Empty search: list CVEs by public-date ``` -$ rhsecapi --verbose --q-empty -Getting 'https://access.redhat.com/labs/securitydataapi/cve.json' ... -CVEs found: 1000 + $ rhsecapi --loglevel info --q-empty +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve.json' ... +[NOTICE ] rhsda: 1000 CVEs found with search query -CVE-2016-8634 -CVE-2016-7035 -CVE-2016-8615 -CVE-2016-8625 -CVE-2016-8619 -CVE-2016-8624 -CVE-2016-8623 +CVE-2016-9401 +CVE-2016-9372 +CVE-2016-9066 +CVE-2016-9064 +CVE-2016-8635 +CVE-2016-9374 ... (output truncated for brevity of this README) ``` ``` -$ rhsecapi --verbose --q-empty --q-pagesize 5 --q-pagenum 3 -Getting 'https://access.redhat.com/labs/securitydataapi/cve.json?per_page=5&page=3' ... -CVEs found: 5 +$ rhsecapi -l info --q-empty --q-pagesize 4 --q-pagenum 3 +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve.json?per_page=5&page=3' ... +[NOTICE ] rhsda: 4 CVEs found with search query -CVE-2016-8617 -CVE-2016-8618 -CVE-2016-8621 -CVE-2016-8864 -CVE-2016-9013 +CVE-2016-5297 +CVE-2016-9376 +CVE-2016-5290 +CVE-2016-5291 ``` ``` -$ rhsecapi --q-empty --q-pagesize 1 --extract-search --all-fields --wrap -CVEs found: 1 - -Valid Red Hat CVE results retrieved: 1 of 1 +$ rhsecapi --q-empty --q-pagesize 1 --extract-search --all-fields +[NOTICE ] rhsda: 1 CVEs found with search query +[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 1 of 1 -CVE-2016-8632 - SEVERITY: Moderate Impact - DATE: 2016-11-07 - CWE: 119 - CVSS: 6.8 (AV:L/AC:L/Au:S/C:C/I:C/A:C) - BUGZILLA: 1390832 - ACKNOWLEDGEMENT: - Red Hat would like to thank Qian Zhang from MarvelTeam of Qihoo 360 - for reporting this issue. +CVE-2016-9401 + SEVERITY: Low Impact + DATE: 2016-11-17 + CWE: CWE-416 + CVSS: 3.3 (AV:L/AC:M/Au:N/C:P/I:P/A:N) + CVSS3: 4.4 (CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N) + BUGZILLA: 1396383 DETAILS: - ** RESERVED ** This candidate has been reserved by an organization - or individual that will use it when announcing a new security - problem. When the candidate has been publicized, the details for - this candidate will be provided. A flaw was found in the TIPC - networking subsystem which could allow for memory corruption and - possible privilege escalation. The flaw involves a system with an - unusually low MTU (60) on networking devices configured as bearers - for the TIPC protocol. An attacker could create a packet which will - overwrite memory outside of allocated space and allow for privilege - escalation. - STATEMENT: - This issue is rated as important. The affected code is not enabled - on Red Hat Enterprise Linux 6 and 7 or MRG-2 kernels. The commit - introducing the comment was not included in Red Hat Enterprise - Linux 5. + Details pending FIX_STATES: - Not affected: Red Hat Enterprise Linux 5 [kernel] - Not affected: Red Hat Enterprise Linux 6 [kernel] - Not affected: Red Hat Enterprise Linux 7 [kernel] + New: Red Hat Enterprise Linux 5 [bash] + New: Red Hat Enterprise Linux 6 [bash] + New: Red Hat Enterprise Linux 7 [bash] ``` ### Find by attributes ``` $ rhsecapi --q-package rhev-hypervisor6 --q-after 2014-10-01 -CVEs found: 6 +[NOTICE ] rhsda: 6 CVEs found with search query CVE-2015-3456 CVE-2015-0235 @@ -360,12 +343,12 @@ CVE-2014-3567 ``` $ rhsecapi --q-package rhev-hypervisor6 --q-after 2014-10-01 --count -CVEs found: 6 +[NOTICE ] rhsda: 6 CVEs found with search query ``` ``` $ rhsecapi --q-package rhev-hypervisor6 --q-after 2014-12-01 --q-severity critical --json -CVEs found: 1 +[NOTICE ] rhsda: 1 CVEs found with search query [ { @@ -401,14 +384,13 @@ CVEs found: 1 ``` ``` -$ rhsecapi -v --q-package rhev-hypervisor6 --q-after 2014-12-01 --q-severity critical --extract-search --spotlight hypervisor -Getting 'https://access.redhat.com/labs/securitydataapi/cve.json?after=2014-12-01&severity=critical&package=rhev-hypervisor6' ... -CVEs found: 1 - -DEBUG THREADS: '1' -Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2015-0235.json' ... -Valid Red Hat CVE results retrieved: 1 of 1 -DEBUG FIELDS: 'threat_severity,public_date,bugzilla,affected_release,package_state' +$ rhsecapi -v --q-package rhev-hypervisor6 --q-after 2014-12-01 --q-severity critical --extract-search --product hypervisor +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve.json?after=2014-12-01&severity=critical&package=rhev-hypervisor6' ... +[NOTICE ] rhsda: 1 CVEs found with search query +[INFO ] rhsda: Using 1 worker threads +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2015-0235.json' ... +[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 1 of 1 +[NOTICE ] rhsda: Results matching spotlight-product option: 1 of 1 CVE-2015-0235 SEVERITY: Critical Impact @@ -423,11 +405,11 @@ CVE-2015-0235 ### Find CVEs by IAVA ``` -$ rhsecapi --verbose --q-iava invalid -Getting 'https://access.redhat.com/labs/iavmmapper/api/iava/' ... -rhsecapi: Login error; unable to get IAVA info +$ rhsecapi --loglevel info --q-iava not-a-real-iava +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/iavmmapper/api/iava/' ... +[ERROR ] rhsda: Login error; unable to get IAVA info -IAVA->CVE mapping data is not provided by the public RH Security Data API. +IAVA→CVE mapping data is not provided by the public RH Security Data API. Instead, this uses the IAVM Mapper App (access.redhat.com/labs/iavmmapper). Access to this data requires RH Customer Portal credentials be provided. @@ -442,17 +424,19 @@ Or post a comment at https://access.redhat.com/discussions/2713931 $ vim ~/.netrc -$ rhsecapi --verbose --q-iava invalid -Getting 'https://access.redhat.com/labs/iavmmapper/api/iava/' ... -rhsecapi: IAVM Mapper (https://access.redhat.com/labs/iavmmapper) has no knowledge of 'invalid' +$ rhsecapi --loglevel info --q-iava not-a-real-iava +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/iavmmapper/api/iava/' ... +[ERROR ] rhsda: IAVM Mapper (https://access.redhat.com/labs/iavmmapper) has no knowledge of 'not-a-real-iava' For help, open an issue at http://github.com/ryran/rhsecapi Or post a comment at https://access.redhat.com/discussions/2713931 ``` ``` -$ rhsecapi --q-iava 2016-A-0287 -CVEs found: 4 +$ rhsecapi --loglevel info --q-iava 2016-A-0287 +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/iavmmapper/api/iava/' ... +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/iavmmapper/api/iava/2016-A-0287' ... +[NOTICE ] rhsda: 4 CVEs found with search CVE-2015-7940 CVE-2016-2107 @@ -461,8 +445,7 @@ CVE-2016-5604 ``` ``` -$ rhsecapi --q-iava 2016-A-0287 --json -CVEs found: 4 +$ rhsecapi --q-iava 2016-A-0287 --json --loglevel warning { "IAVM": { @@ -484,21 +467,18 @@ CVEs found: 4 ``` ``` -$ rhsecapi --q-iava 2016-A-0287 --extract-search --count -CVEs found: 4 -rhsecapi: 404 Client Error: Not Found for url: https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-5604.json -Valid Red Hat CVE results retrieved: 3 of 4 -Invalid CVE queries: 1 of 4 +$ rhsecapi --q-iava 2016-A-0287 --extract-search --count +[NOTICE ] rhsda: 4 CVEs found with search +[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 3 of 4 +[NOTICE ] rhsda: Invalid CVE queries: 1 of 4 ``` ``` -$ rhsecapi --q-iava 2016-A-0287 --extract-search --spotlight linux.6 -CVEs found: 4 - -rhsecapi: 404 Client Error: Not Found for url: https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-5604.json -Valid Red Hat CVE results retrieved: 3 of 4 -Results matching spotlight-product option: 2 of 4 -Invalid CVE queries: 1 of 4 +$ rhsecapi --q-iava 2016-A-0287 --extract-search --product linux.6 +[NOTICE ] rhsda: 4 CVEs found with search +[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 3 of 4 +[NOTICE ] rhsda: Results matching spotlight-product option: 2 of 4 +[NOTICE ] rhsda: Invalid CVE queries: 1 of 4 CVE-2016-2107 SEVERITY: Moderate Impact @@ -525,8 +505,9 @@ usage: rhsecapi [--q-before YEAR-MM-DD] [--q-after YEAR-MM-DD] [--q-bug BZID] [--q-cwe CWEID] [--q-cvss SCORE] [--q-cvss3 SCORE] [--q-empty] [--q-pagesize PAGESZ] [--q-pagenum PAGENUM] [--q-raw RAWQUERY] [--q-iava IAVA] [-s] [-0] [-f FIELDS | -a | -m] - [--spotlight PRODUCT] [-j] [-u] [-w [WIDTH]] [-c] [-v] - [-t THREDS] [-p] [--dryrun] [-E [DAYS]] [-h] [--help] + [--product PRODUCT] [-j] [-u] [-w [WIDTH]] [-c] + [-l {debug,info,notice,warning}] [-t THREDS] [-p] [--dryrun] + [-E [DAYS]] [-h] [--help] [CVE [CVE ...]] Make queries against the Red Hat Security Data API @@ -592,8 +573,8 @@ CVE DISPLAY OPTIONS: releases, package_state → fix_states or states; optionally prepend FIELDS with plus (+) sign to add fields to the default (e.g., '-f +iava,cvss3') or a - caret (^) to remove fields from the default (e.g., '-f - ^bugzilla,severity') + caret (^) to remove fields from all-fields (e.g., '-f + ^mitigation,severity') -a, --all-fields Display all supported fields (currently: threat_severity, public_date, iava, cwe, cvss, cvss3, bugzilla, acknowledgement, details, statement, @@ -602,7 +583,7 @@ CVE DISPLAY OPTIONS: -m, --most-fields Display all fields mentioned above except the heavy- text ones -- (excludes: acknowledgement, details, statement, mitigation, references) - --spotlight PRODUCT Spotlight a particular PRODUCT via case-insensitive + --product PRODUCT Spotlight a particular PRODUCT via case-insensitive regex; this hides CVEs where 'FIXED_RELEASES' or 'FIX_STATES' don't have an item with 'cpe' (e.g. 'cpe:/o:redhat:enterprise_linux:7') or 'product_name' @@ -614,13 +595,17 @@ CVE DISPLAY OPTIONS: GENERAL OPTIONS: -w, --wrap [WIDTH] Change wrap-width of long fields (acknowledgement, - details, statement, mitigation) in non-json output - (default: wrapping WIDTH equivalent to TERMWIDTH-2 - unless using '--pastebin' where default WIDTH is - '168'; specify '0' to disable wrapping; WIDTH defaults - to '70' if option is used but WIDTH is omitted) + details, statement, mitigation, references) in non- + json output (default: wrapping WIDTH equivalent to + TERMWIDTH-2 unless using '--pastebin' where default + WIDTH is '168'; specify '0' to disable wrapping; WIDTH + defaults to '70' if option is used but WIDTH is + omitted) -c, --count Exit after printing CVE counts - -v, --verbose Print API urls & other debugging info to stderr + -l, --loglevel {debug,info,notice,warning} + Configure logging level threshold; lower from the + default of 'notice' to see extra details printed to + stderr -t, --threads THREDS Set number of concurrent worker threads to allow when making CVE queries (default on this system: 8) -p, --pastebin Send output to Fedora Project Pastebin @@ -636,79 +621,374 @@ GENERAL OPTIONS: --help Show this help message and exit VERSION: - rhsecapi v0.9.1 last mod 2016/11/10 + rhsecapi v1.0.0_rc2 last mod 2016/18/10 See to report bugs or RFEs ``` -## Testing from python shell +## Working with `rhsda` library, e.g., in a web app ``` $ python ->>> import rhsecapi as r ->>> help(r.a) -Help on instance of RedHatSecDataApiClient in module rhsecapi: - -class RedHatSecDataApiClient - | Portable object to interface with the Red Hat Security Data API. - | - | https://access.redhat.com/documentation/en/red-hat-security-data-api/ - | - | Requires: - | requests - | sys - | - | Methods defined here: - | - | __init__(self, progressToStderr=False, apiurl='https://access.redhat.com/labs/securitydataapi') - | - | get_cve(self, cve) - | - | get_cvrf(self, rhsa) - | - | get_cvrf_oval(self, rhsa) - | - | get_oval(self, rhsa) - | - | search_cve(self, params=None) - | - | search_cvrf(self, params=None) - | - | search_oval(self, params=None) +Python 2.7.10 (default, Jun 20 2016, 14:45:40) +[GCC 5.3.1 20160406 (Red Hat 5.3.1-6)] on linux2 +Type "help", "copyright", "credits" or "license" for more information. +>>> import rhsda +>>> help(rhsda) +Help on module rhsda: + +NAME + rhsda + +FILE + /usr/share/rhsecapi/rhsda.py + +DESCRIPTION + # -*- coding: utf-8 -*- + #------------------------------------------------------------------------------- + # Copyright 2016 Ryan Sawhill Aroha and rhsecapi contributors + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU General Public License as published by + # the Free Software Foundation, either version 3 of the License, or + # (at your option) any later version. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + # General Public License for more details. + #------------------------------------------------------------------------------- + +CLASSES + ApiClient + + class ApiClient + | Portable object to interface with the Red Hat Security Data API. + | + | https://access.redhat.com/documentation/en/red-hat-security-data-api/ + | + | Methods defined here: + | + | __init__(self, logLevel='notice') + | + | cve_search_query(self, params, outFormat='list') + | Perform a CVE search query. + | + | ON *OUTFORMAT*: + | + | Setting to "list" returns list of found CVE ids. + | Setting to "plaintext" returns str object containing new-line separated CVE ids. + | Setting to "json" returns list object containing original JSON. + | Setting to "jsonpretty" returns str object containing prettified JSON. + | + | find_cves(self, params=None, outFormat='json', before=None, after=None, bug=None, advisory=None, severity=None, package=None, cwe=None, cvss_score=None, cvss3_score=None, page=None, per_page=None) + | Find CVEs by recent or attributes. + | + | Provides an index to recent CVEs when no parameters are passed. Returns a + | convenience object as response with minimal attributes. + | + | With *outFormat* of "json", returns JSON object. + | With *outFormat* of "xml", returns unformatted XML as string. + | If *params* dict is passed, additional parameters are ignored. + | + | find_cvrfs(self, params=None, outFormat='json', before=None, after=None, bug=None, cve=None, severity=None, package=None, page=None, per_page=None) + | Find CVRF documents by recent or attributes. + | + | Provides an index to recent CVRF documents with a summary of their contents, + | when no parameters are passed. Returns a convenience object as the response with + | minimal attributes. + | + | With *outFormat* of "json", returns JSON object. + | With *outFormat* of "xml", returns unformatted XML as string. + | If *params* dict is passed, additional parameters are ignored. + | + | find_ovals(self, params=None, outFormat='json', before=None, after=None, bug=None, cve=None, severity=None, page=None, per_page=None) + | Find OVAL definitions by recent or attributes. + | + | Provides an index to recent OVAL definitions with a summary of their contents, + | when no parameters are passed. Returns a convenience object as the response with + | minimal attributes. + | + | With *outFormat* of "json", returns JSON object. + | With *outFormat* of "xml", returns unformatted XML as string. + | If *params* dict is passed, additional parameters are ignored. + | + | get_cve(self, cve, outFormat='json') + | Retrieve full details of a CVE. + | + | get_cvrf(self, rhsa, outFormat='json') + | Retrieve CVRF details for an RHSA. + | + | get_cvrf_oval(self, rhsa, outFormat='json') + | Retrieve CVRF-OVAL details for an RHSA. + | + | get_iava(self, iavaId) + | Validate IAVA number and return json. + | + | get_oval(self, rhsa, outFormat='json') + | Retrieve OVAL details for an RHSA. + | + | mget_cves(self, cves, numThreads=0, onlyCount=False, outFormat='plaintext', urls=False, fields='ALL', wrapWidth=70, product=None, timeout=300) + | Use multi-threading to lookup a list of CVEs and return text output. + | + | *cves*: A list of CVE ids or a str obj from which to regex CVE ids + | *numThreads*: Number of concurrent worker threads; 0 == CPUs*2 + | *onlyCount*: Whether to exit after simply logging number of valid/invalid CVEs + | *outFormat*: Control output format ("plaintext", "json", or "jsonpretty") + | *urls*: Whether to add extra URLs to certain fields + | *fields*: Customize which fields are displayed by passing comma-sep string + | *wrapWidth*: Width for long fields; 1 auto-detects based on terminal size + | *product*: Restrict display of CVEs based on product-matching regex + | *timeout*: Total ammount of time to wait for all CVEs to be retrieved + | + | ON *CVES*: + | + | If *cves* is a list, each item in the list will be retrieved as a CVE. + | If *cves* is a string or file object, it will be regex-parsed line by line and + | all CVE ids will be extracted into a list. + | In all cases, character-case is irrelevant. + | + | ON *OUTFORMAT*: + | + | Setting to "plaintext" returns str object containing formatted output. + | Setting to "json" returns list object (i.e., original JSON) + | Setting to "jsonpretty" returns str object containing prettified JSON + | + | ON *FIELDS*: + | + | librhsecapi.cveFields.all is a list obj of supported fields, i.e.: + | threat_severity, public_date, iava, cwe, cvss, cvss3, bugzilla, + | acknowledgement, details, statement, mitigation, upstream_fix, references, + | affected_release, package_state + | + | librhsecapi.cveFields.most is a list obj that excludes text-heavy fields, like: + | acknowledgement, details, statement, mitigation, references + | + | librhsecapi.cveFields.base is a list obj of the most important fields, i.e.: + | threat_severity, public_date, bugzilla, affected_release, package_state + | + | There is a group-alias for each of these, so you can do: + | fields="ALL" + | fields="MOST" + | fields="BASE" + | + | Also note that some friendly aliases are supported, e.g.: + | threat_severity → severity + | public_date → date + | affected_release → fixed_releases or fixed or releases + | package_state → fix_states or states + | + | Note that the *fields* string can be prepended with "+" or "^" to signify + | adding to cveFields.base or removing from cveFields.all, e.g.: + | fields="+cvss,cwe,statement" + | fields="^releases,mitigation" + | + | Finally: *fields* is case-insensitive. + +FUNCTIONS + extract_cves_from_input(obj) + Use case-insensitive regex to extract CVE ids from input object. + + *obj* can be a list, a file, or a string. + + A list of CVEs is returned. + + jprint(jsoninput, printOutput=True) + Pretty-print jsoninput. + +DATA + consolehandler = + cveFields = Namespace(aliases={'severity': 'threat_severity'...tails',... + cve_regex = <_sre.SRE_Pattern object> + cve_regex_string = 'CVE-[0-9]{4}-[0-9]{4,}' + logger = + numThreadsDefault = 8 + print_function = _Feature((2, 6, 0, 'alpha', 2), (3, 0, 0, 'alpha', 0)... + (END) ->>> r.a.search_oval("cve=CVE-2016-5387") -Getting 'https://access.redhat.com/labs/securitydataapi/oval.json?cve=CVE-2016-5387' ... -('https://access.redhat.com/labs/securitydataapi/oval.json?cve=CVE-2016-5387', [{u'severity': u'important', u'bugzillas': [u'1353755'], u'resource_url': u'https://access.redhat.com/labs/securitydataapi/oval/RHSA-2016:1421.json', u'released_on': u'2016-07-18T04:00:00+00:00', u'RHSA': u'RHSA-2016:1421', u'CVEs': [u'CVE-2016-5387']}, {u'severity': u'important', u'bugzillas': [u'1347648', u'1353269', u'1353755'], u'resource_url': u'https://access.redhat.com/labs/securitydataapi/oval/RHSA-2016:1422.json', u'released_on': u'2016-07-18T04:00:00+00:00', u'RHSA': u'RHSA-2016:1422', u'CVEs': [u'CVE-2016-5387']}]) ->>> r.jprint(r.a.search_oval("cve=CVE-2016-5387")) -Getting 'https://access.redhat.com/labs/securitydataapi/oval.json?cve=CVE-2016-5387' ... -[ - "https://access.redhat.com/labs/securitydataapi/oval.json?cve=CVE-2016-5387", - [ - { - "CVEs": [ - "CVE-2016-5387" - ], - "RHSA": "RHSA-2016:1421", - "bugzillas": [ - "1353755" - ], - "released_on": "2016-07-18T04:00:00+00:00", - "resource_url": "https://access.redhat.com/labs/securitydataapi/oval/RHSA-2016:1421.json", - "severity": "important" - }, - { - "CVEs": [ - "CVE-2016-5387" - ], - "RHSA": "RHSA-2016:1422", - "bugzillas": [ - "1347648", - "1353269", - "1353755" - ], - "released_on": "2016-07-18T04:00:00+00:00", - "resource_url": "https://access.redhat.com/labs/securitydataapi/oval/RHSA-2016:1422.json", - "severity": "important" - } - ] -] + +>>> a = rhsda.ApiClient() +>>> txt = a.mget_cves("CVE-2016-5387 CVE-2016-5392") +[NOTICE ] rhsda: Found 2 CVEs in input; 0 duplicates removed +[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 2 of 2 +>>> print(txt) +CVE-2016-5392 + SEVERITY: Important Impact + DATE: 2016-07-14 + CWE: CWE-20 + CVSS: 6.8 (AV:N/AC:L/Au:S/C:C/I:N/A:N) + CVSS3: 6.5 (CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N) + BUGZILLA: 1356195 + ACKNOWLEDGEMENT: + This issue was discovered by Yanping Zhang (Red Hat). + DETAILS: + The API server in Kubernetes, as used in Red Hat OpenShift + Enterprise 3.2, in a multi tenant environment allows remote + authenticated users with knowledge of other project names to obtain + sensitive project and user information via vectors related to the + watch-cache list. The Kubernetes API server contains a watch cache + that speeds up performance. Due to an input validation error + OpenShift Enterprise may return data for other users and projects + when queried by a user. An attacker with knowledge of other project + names could use this vulnerability to view their information. + FIXED_RELEASES: + Red Hat OpenShift Enterprise 3.2 [atomic-openshift-3.2.1.7-1.git.0.2702170.el7]: RHSA-2016:1427 + FIX_STATES: + Affected: Red Hat OpenShift Enterprise 3 [Security] + +CVE-2016-5387 + SEVERITY: Important Impact + DATE: 2016-07-18 + IAVA: 2016-B-0160 + CWE: CWE-20 + CVSS: 5.0 (AV:N/AC:L/Au:N/C:N/I:P/A:N) + CVSS3: 5.0 (CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:L/A:N) + BUGZILLA: 1353755 + ACKNOWLEDGEMENT: + Red Hat would like to thank Scott Geary (VendHQ) for reporting this + issue. + DETAILS: + The Apache HTTP Server through 2.4.23 follows RFC 3875 section + 4.1.18 and therefore does not protect applications from the + presence of untrusted client data in the HTTP_PROXY environment + variable, which might allow remote attackers to redirect an + application's outbound HTTP traffic to an arbitrary proxy server + via a crafted Proxy header in an HTTP request, aka an "httpoxy" + issue. NOTE: the vendor states "This mitigation has been assigned + the identifier CVE-2016-5387"; in other words, this is not a CVE ID + for a vulnerability. It was discovered that httpd used the value + of the Proxy header from HTTP requests to initialize the HTTP_PROXY + environment variable for CGI scripts, which in turn was incorrectly + used by certain HTTP client implementations to configure the proxy + for outgoing HTTP requests. A remote attacker could possibly use + this flaw to redirect HTTP requests performed by a CGI script to an + attacker-controlled proxy via a malicious HTTP request. + UPSTREAM_FIX: httpd 2.4.24, httpd 2.2.32 + REFERENCES: + https://access.redhat.com/security/vulnerabilities/httpoxy + https://httpoxy.org/ + https://www.apache.org/security/asf-httpoxy-response.txt + FIXED_RELEASES: + Red Hat Enterprise Linux 5 [httpd-2.2.3-92.el5_11]: RHSA-2016:1421 + Red Hat Enterprise Linux 6 [httpd-2.2.15-54.el6_8]: RHSA-2016:1421 + Red Hat Enterprise Linux 7 [httpd-2.4.6-40.el7_2.4]: RHSA-2016:1422 + Red Hat JBoss Core Services 1: RHSA-2016:1625 + Red Hat JBoss Core Services on RHEL 6 Server [jbcs-httpd24-httpd-2.4.6-77.SP1.jbcs.el6]: RHSA-2016:1851 + Red Hat JBoss Core Services on RHEL 7 Server [jbcs-httpd24-httpd-2.4.6-77.SP1.jbcs.el7]: RHSA-2016:1851 + Red Hat JBoss Enterprise Web Server 2 for RHEL 6 Server [httpd-2.2.26-54.ep6.el6]: RHSA-2016:1649 + Red Hat JBoss Enterprise Web Server 2 for RHEL 7 Server [httpd22-2.2.26-56.ep6.el7]: RHSA-2016:1648 + Red Hat JBoss Web Server 2.1: RHSA-2016:1650 + Red Hat JBoss Web Server 3.0: RHSA-2016:1624 + Red Hat JBoss Web Server 3.0 for RHEL 6: RHSA-2016:1636 + Red Hat JBoss Web Server 3.0 for RHEL 7: RHSA-2016:1635 + Red Hat Software Collections for Red Hat Enterprise Linux Server (v. 6) [httpd24-httpd-2.4.18-11.el6]: RHSA-2016:1420 + Red Hat Software Collections for Red Hat Enterprise Linux Server (v. 7) [httpd24-httpd-2.4.18-11.el7]: RHSA-2016:1420 + FIX_STATES: + Affected: Red Hat JBoss EAP 6 [httpd22] + Not affected: Red Hat JBoss EAP 7 [httpd22] + Will not fix: Red Hat JBoss EWS 1 [httpd] + +>>> a = rhsda.ApiClient('info') +>>> print(a.mget_cves("CVE-2016-5387 CVE-2016-5392 CVE-2016-2379 CVE-2016-5773", fields='BASE', product='web.server.3')) +[NOTICE ] rhsda: Found 4 CVEs in input; 0 duplicates removed +[INFO ] rhsda: Using 4 worker threads +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-5392.json' ... +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-5773.json' ... +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-5387.json' ... +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-2379.json' ... +[INFO ] rhsda: Hiding CVE-2016-5392 due to negative product match +[INFO ] rhsda: Hiding CVE-2016-2379 due to negative product match +[INFO ] rhsda: Hiding CVE-2016-5773 due to negative product match +[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 4 of 4 +[NOTICE ] rhsda: Results matching spotlight-product option: 1 of 4 +CVE-2016-5387 + SEVERITY: Important Impact + DATE: 2016-07-18 + BUGZILLA: 1353755 + FIXED_RELEASES matching 'web.server.3': + Red Hat JBoss Web Server 3.0: RHSA-2016:1624 + Red Hat JBoss Web Server 3.0 for RHEL 6: RHSA-2016:1636 + Red Hat JBoss Web Server 3.0 for RHEL 7: RHSA-2016:1635 + +>>> print(a.get_cve("CVE-2016-5773", outFormat='xml')) +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-5773.xml' ... + + +Copyright © 2012 Red Hat, Inc. All rights reserved. + + Moderate + 2016-06-23T00:00:00 + +CVE-2016-5773 php: ZipArchive class Use After Free Vulnerability in PHP's GC algorithm and unserialize + + + 5.1 + AV:N/AC:H/Au:N/C:P/I:P/A:P + + + 5.6 + CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L + + CWE-416 +
+php_zip.c in the zip extension in PHP before 5.5.37, 5.6.x before 5.6.23, and 7.x before 7.0.8 improperly interacts with the unserialize implementation and garbage collection, which allows remote attackers to execute arbitrary code or cause a denial of service (use-after-free and application crash) via crafted serialized data containing a ZipArchive object. +
+ + Red Hat Software Collections for Red Hat Enterprise Linux Server (v. 6) + 2016-11-15T00:00:00 + RHSA-2016:2750 + rh-php56-php-5.6.25-1.el6 + + + Red Hat Software Collections for Red Hat Enterprise Linux Server (v. 7) + 2016-11-15T00:00:00 + RHSA-2016:2750 + rh-php56-php-5.6.25-1.el7 + + + Red Hat Enterprise Linux 5 + Not affected + php + + + Red Hat Enterprise Linux 5 + Will not fix + php53 + + + Red Hat Enterprise Linux 6 + Will not fix + php + + + Red Hat Enterprise Linux 7 + Will not fix + php + + php 5.5.37, php 5.6.23 +
+ +>>> json = a.find_cves(after='2015-01-01', before='2015-02-01') +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve.json?after=2015-01-01&before=2015-02-01' ... +[NOTICE ] rhsda: 232 CVEs found with search query +>>> json = a.find_cves(params={'after':'2015-01-01', 'before':'2015-02-01'}) +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve.json?after=2015-01-01&before=2015-02-01' ... +[NOTICE ] rhsda: 232 CVEs found with search query + +>>> json = a.find_cvrfs(after='2015-01-01', before='2015-02-01') +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cvrf.json?after=2015-01-01&before=2015-02-01' ... +[NOTICE ] rhsda: 50 CVRFs found with search query + +>>> json = a.find_ovals(after='2015-01-01', before='2015-02-01') +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/oval.json?after=2015-01-01&before=2015-02-01' ... +[NOTICE ] rhsda: 20 OVALs found with search query + +>>> txt = a.cve_search_query({'after':'2015-01-01', 'before':'2015-02-01', 'per_page':5}, outFormat='plaintext') +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve.json?per_page=5&after=2015-01-01&before=2015-02-01' ... +[NOTICE ] rhsda: 5 CVEs found with search query +>>> print(txt) +CVE-2014-0141 +CVE-2015-1563 +CVE-2015-8779 +CVE-2014-9749 +CVE-2015-0210 ``` diff --git a/rhsda.py b/rhsda.py index f4dd045..333d14a 100644 --- a/rhsda.py +++ b/rhsda.py @@ -71,6 +71,7 @@ cveFields.most = list(cveFields.all) for f in cveFields.not_most: cveFields.most.remove(f) +del(f) # Simple set of most important fields cveFields.base = [ 'threat_severity', @@ -99,6 +100,7 @@ # A list of all fields + all aliases cveFields.all_plus_aliases = list(cveFields.all) cveFields.all_plus_aliases.extend([k for k in cveFields.aliases]) +del(k) # Regex to match a CVE id string @@ -372,7 +374,7 @@ def _parse_cve_to_plaintext(self, cve): except requests.exceptions.HTTPError as e: # CVE not in RH CVE DB logger.info(e) - if self.cfg.product or self.cfg.onlyCount or self.cfg.outFormat == 'json': + if self.cfg.product or self.cfg.onlyCount or self.cfg.outFormat.startswith('json'): return False, "" else: out.append("{0}\n Not present in Red Hat CVE database".format(cve)) @@ -690,6 +692,13 @@ def mget_cves(self, cves, numThreads=0, onlyCount=False, outFormat='plaintext', if onlyCount: return if outFormat == 'plaintext': + # Remove all blank entries (created when spotlight-product hides a CVE) + cveOutput = list(cveOutput) + while 1: + try: + cveOutput.remove("") + except ValueError: + break return "\n".join(cveOutput) elif outFormat == 'json': return cveOutput @@ -725,8 +734,8 @@ def _err_print_support_urls(self, msg=None): """Print error + support urls.""" if msg: logger.error(msg) - print("For help, open an issue at http://github.com/ryran/rhsecapi\n" - "Or post a comment at https://access.redhat.com/discussions/2713931\n", file=sys.stderr) + print("\nFor help, open an issue at http://github.com/ryran/rhsecapi\n" + "Or post a comment at https://access.redhat.com/discussions/2713931", file=sys.stderr) def _iavm_query(self, url): """Get IAVA json from IAVM Mapper App.""" @@ -746,13 +755,13 @@ def _iavm_query(self, url): result = r.json() except: logger.error("Login error; unable to get IAVA info") - print("IAVA→CVE mapping data is not provided by the public RH Security Data API.\n" + print("\nIAVA→CVE mapping data is not provided by the public RH Security Data API.\n" "Instead, this uses the IAVM Mapper App (access.redhat.com/labs/iavmmapper).\n\n" "Access to this data requires RH Customer Portal credentials be provided.\n" "Create a ~/.netrc with the following contents:\n\n" "machine access.redhat.com\n" " login YOUR-CUSTOMER-PORTAL-LOGIN\n" - " password YOUR_PASSWORD_HERE\n", + " password YOUR_PASSWORD_HERE", file=sys.stderr) self._err_print_support_urls() return [] @@ -763,9 +772,12 @@ def get_iava(self, iavaId): """Validate IAVA number and return json.""" url = 'https://access.redhat.com/labs/iavmmapper/api/iava/' result = self._iavm_query(url) - if iavaId not in result: - logger.error("IAVM Mapper (https://access.redhat.com/labs/iavmmapper) has no knowledge of '{0}'".format(iavaId)) - self._err_print_support_urls() + if result: + if iavaId not in result: + logger.error("IAVM Mapper (https://access.redhat.com/labs/iavmmapper) has no knowledge of '{0}'".format(iavaId)) + self._err_print_support_urls() + return [] + else: return [] url += '{0}'.format(iavaId) result = self._iavm_query(url) @@ -775,5 +787,4 @@ def get_iava(self, iavaId): if __name__ == "__main__": a = ApiClient('info') - # print(a.mget_cves(sys.stdin), end="") - print(a.cve_search_query('per_page=5', outFormat='json')) \ No newline at end of file + print(a.mget_cves(sys.stdin), end="") diff --git a/rhsecapi.py b/rhsecapi.py index 9604921..9bd39fa 100755 --- a/rhsecapi.py +++ b/rhsecapi.py @@ -37,7 +37,7 @@ # Globals prog = 'rhsecapi' vers = {} -vers['version'] = '1.0.0_rc1' +vers['version'] = '1.0.0_rc2' vers['date'] = '2016/18/10' @@ -226,7 +226,7 @@ def parse_args(): const='MOST', help="Display all fields mentioned above except the heavy-text ones -- (excludes: {0})".format(", ".join(rhsda.cveFields.not_most))) g_cveDisplay.add_argument( - '--spotlight', dest='spotlightedProduct', metavar="PRODUCT", + '--product', help="Spotlight a particular PRODUCT via case-insensitive regex; this hides CVEs where 'FIXED_RELEASES' or 'FIX_STATES' don't have an item with 'cpe' (e.g. 'cpe:/o:redhat:enterprise_linux:7') or 'product_name' (e.g. 'Red Hat Enterprise Linux 7') matching PRODUCT; this also hides all items in 'FIXED_RELEASES' & 'FIX_STATES' that don't match PRODUCT") g_cveDisplay.add_argument( '-j', '--json', action='store_true', @@ -387,7 +387,7 @@ def main(opts): urls=opts.printUrls, fields=opts.fields, wrapWidth=opts.wrapWidth, - product=opts.spotlightedProduct) + product=opts.product) if not opts.count: print(file=sys.stderr) if opts.count: @@ -401,7 +401,7 @@ def main(opts): response = fpaste_it(inputdata=data, author=prog, lang=opts.p_lang, expire=opts.pexpire) except ValueError as e: print(e, file=sys.stderr) - print("{0}: Submitting to pastebin failed; print results to stdout instead? [y]".format(prog), file=sys.stderr) + logger.error("Submitting to pastebin failed; print results to stdout instead? [y]") answer = raw_input("> ") if "y" in answer or len(answer) == 0: print(data, end="") From 294a03b8341cb6acd3cf94543014ab0f42d7aca7 Mon Sep 17 00:00:00 2001 From: Ryan Sawhill Aroha Date: Fri, 18 Nov 2016 12:55:33 -0500 Subject: [PATCH 3/3] update readme --- README.md | 261 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 183 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 62078ce..6c0296d 100644 --- a/README.md +++ b/README.md @@ -625,13 +625,75 @@ VERSION: See to report bugs or RFEs ``` -## Working with `rhsda` library, e.g., in a web app +## Working with backend rhsda library + +The `rhsda` library does all the work of interfacing with the API. If run directly, it tries to find CVEs on stdin. + +``` +$ echo CVE-2016-9401 CVE-2016-9372 CVE-2016-8635 | python rhsda.py +[NOTICE ] rhsda: Found 3 CVEs in stdin; 0 duplicates removed +[INFO ] rhsda: Using 3 worker threads +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-9401.json' ... +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-8635.json' ... +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-9372.json' ... +[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 3 of 3 +CVE-2016-9401 + SEVERITY: Low Impact + DATE: 2016-11-17 + CWE: CWE-416 + CVSS: 3.3 (AV:L/AC:M/Au:N/C:P/I:P/A:N) + CVSS3: 4.4 (CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N) + BUGZILLA: 1396383 + DETAILS: + Details pending + FIX_STATES: + New: Red Hat Enterprise Linux 5 [bash] + New: Red Hat Enterprise Linux 6 [bash] + New: Red Hat Enterprise Linux 7 [bash] + +CVE-2016-8635 + SEVERITY: Moderate Impact + DATE: 2016-11-16 + CVSS: 4.3 (AV:N/AC:M/Au:N/C:P/I:N/A:N) + CVSS3: 5.3 (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N) + BUGZILLA: 1391818 + ACKNOWLEDGEMENT: + This issue was discovered by Hubert Kario (Red Hat). + DETAILS: + ** RESERVED ** This candidate has been reserved by an organization + or individual that will use it when announcing a new security + problem. When the candidate has been publicized, the details for + this candidate will be provided. It was found that Diffie Hellman + Client key exchange handling in NSS was vulnerable to small + subgroup confinement attack. An attacker could use this flaw to + recover private keys by confining the client DH key to small + subgroup of the desired group. + FIXED_RELEASES: + Red Hat Enterprise Linux 5 [nss-3.21.3-2.el5_11]: RHSA-2016:2779 + Red Hat Enterprise Linux 6 [nss-3.21.3-2.el6_8]: RHSA-2016:2779 + Red Hat Enterprise Linux 7 [nss-3.21.3-2.el7_3]: RHSA-2016:2779 + +CVE-2016-9372 + SEVERITY: Moderate Impact + DATE: 2016-11-16 + CVSS: 4.3 (AV:N/AC:M/Au:N/C:N/I:N/A:P) + CVSS3: 5.9 (CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H) + BUGZILLA: 1396409 + DETAILS: + Details pending + UPSTREAM_FIX: wireshark 2.2.2 + REFERENCES: + https://www.wireshark.org/security/wnpa-sec-2016-58.html + FIX_STATES: + Will not fix: Red Hat Enterprise Linux 5 [wireshark] + Will not fix: Red Hat Enterprise Linux 6 [wireshark] + Will not fix: Red Hat Enterprise Linux 7 [wireshark] +``` + +To plug it into, e.g., a web-app, check the help ``` $ python -Python 2.7.10 (default, Jun 20 2016, 14:45:40) -[GCC 5.3.1 20160406 (Red Hat 5.3.1-6)] on linux2 -Type "help", "copyright", "credits" or "license" for more information. >>> import rhsda >>> help(rhsda) Help on module rhsda: @@ -805,10 +867,92 @@ DATA print_function = _Feature((2, 6, 0, 'alpha', 2), (3, 0, 0, 'alpha', 0)... (END) +``` + +As can be seen above, an `rhsda.ApiClient` class does most of the work. Simple methods for all operations laid out in the upstream documentation are available, allowing receipt of plain json/xml. +``` >>> a = rhsda.ApiClient() + +>>> json = a.find_cves(after='2015-01-01', before='2015-02-01') +[NOTICE ] rhsda: 232 CVEs found with search query + +>>> json = a.find_cves(params={'after':'2015-01-01', 'before':'2015-02-01'}) +[NOTICE ] rhsda: 232 CVEs found with search query + +>>> json = a.find_cvrfs(after='2015-01-01', before='2015-02-01') +[NOTICE ] rhsda: 50 CVRFs found with search query + +>>> json = a.find_ovals(after='2015-01-01', before='2015-02-01') +[NOTICE ] rhsda: 20 OVALs found with search query + +>>> print(a.get_cve("CVE-2016-5773", outFormat='xml')) + + +Copyright © 2012 Red Hat, Inc. All rights reserved. + + Moderate + 2016-06-23T00:00:00 + +CVE-2016-5773 php: ZipArchive class Use After Free Vulnerability in PHP's GC algorithm and unserialize + + + 5.1 + AV:N/AC:H/Au:N/C:P/I:P/A:P + + + 5.6 + CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L + + CWE-416 +
+php_zip.c in the zip extension in PHP before 5.5.37, 5.6.x before 5.6.23, and 7.x before 7.0.8 improperly interacts with the unserialize implementation and garbage collection, which allows remote attackers to execute arbitrary code or cause a denial of service (use-after-free and application crash) via crafted serialized data containing a ZipArchive object. +
+ + Red Hat Software Collections for Red Hat Enterprise Linux Server (v. 6) + 2016-11-15T00:00:00 + RHSA-2016:2750 + rh-php56-php-5.6.25-1.el6 + + + Red Hat Software Collections for Red Hat Enterprise Linux Server (v. 7) + 2016-11-15T00:00:00 + RHSA-2016:2750 + rh-php56-php-5.6.25-1.el7 + + + Red Hat Enterprise Linux 5 + Not affected + php + + + Red Hat Enterprise Linux 5 + Will not fix + php53 + + + Red Hat Enterprise Linux 6 + Will not fix + php + + + Red Hat Enterprise Linux 7 + Will not fix + php + + php 5.5.37, php 5.6.23 +
+``` + +Also available: multi-threaded CVE retrieval (with default conversion to pretty-formatted plaintext) via `mget_cves()` method. Defaults to showing all fields. + +``` +>>> a = rhsda.ApiClient('info') # (This increases the console loglevel [stderr]) >>> txt = a.mget_cves("CVE-2016-5387 CVE-2016-5392") [NOTICE ] rhsda: Found 2 CVEs in input; 0 duplicates removed +[INFO ] rhsda: Using 2 worker threads +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-5392.json' ... +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-5387.json' ... [NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 2 of 2 >>> print(txt) CVE-2016-5392 @@ -886,15 +1030,45 @@ CVE-2016-5387 Affected: Red Hat JBoss EAP 6 [httpd22] Not affected: Red Hat JBoss EAP 7 [httpd22] Will not fix: Red Hat JBoss EWS 1 [httpd] +``` + +The `mget_cves()` method's `cves=` argument (the 1st kwarg) regex-finds CVEs in an input string: +``` +>>> s = "Hello thar we need CVE-2016-5387 fixed as well as CVE-2016-5392(worst).\nAnd not to mention CVE-2016-2379,CVE-2016-1000219please." >>> a = rhsda.ApiClient('info') ->>> print(a.mget_cves("CVE-2016-5387 CVE-2016-5392 CVE-2016-2379 CVE-2016-5773", fields='BASE', product='web.server.3')) +>>> json = a.mget_cves(s, outFormat='json') [NOTICE ] rhsda: Found 4 CVEs in input; 0 duplicates removed [INFO ] rhsda: Using 4 worker threads [INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-5392.json' ... -[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-5773.json' ... +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-1000219.json' ... +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-5387.json' ... +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-2379.json' ... +[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 4 of 4 +``` + +... or a file: + +``` +>>> a = rhsda.ApiClient() +>>> with open('scan-results.csv') as f: +... txt = a.mget_cves(f) +... +[NOTICE ] rhsda: Found 150 CVEs in input; 698 duplicates removed +[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 148 of 150 +[NOTICE ] rhsda: Invalid CVE queries: 2 of 150 +``` + +Also of course a list is fine: + +``` +>>> L = ['CVE-2016-5387', 'CVE-2016-5392', 'CVE-2016-2379', 'CVE-2016-5773'] +>>> print(a.mget_cves(L, fields='BASE', product='web.server.3')) +[INFO ] rhsda: Using 4 worker threads [INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-5387.json' ... +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-5392.json' ... [INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-2379.json' ... +[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-5773.json' ... [INFO ] rhsda: Hiding CVE-2016-5392 due to negative product match [INFO ] rhsda: Hiding CVE-2016-2379 due to negative product match [INFO ] rhsda: Hiding CVE-2016-5773 due to negative product match @@ -908,80 +1082,11 @@ CVE-2016-5387 Red Hat JBoss Web Server 3.0: RHSA-2016:1624 Red Hat JBoss Web Server 3.0 for RHEL 6: RHSA-2016:1636 Red Hat JBoss Web Server 3.0 for RHEL 7: RHSA-2016:1635 +``` ->>> print(a.get_cve("CVE-2016-5773", outFormat='xml')) -[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve/CVE-2016-5773.xml' ... - - -Copyright © 2012 Red Hat, Inc. All rights reserved. - - Moderate - 2016-06-23T00:00:00 - -CVE-2016-5773 php: ZipArchive class Use After Free Vulnerability in PHP's GC algorithm and unserialize - - - 5.1 - AV:N/AC:H/Au:N/C:P/I:P/A:P - - - 5.6 - CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L - - CWE-416 -
-php_zip.c in the zip extension in PHP before 5.5.37, 5.6.x before 5.6.23, and 7.x before 7.0.8 improperly interacts with the unserialize implementation and garbage collection, which allows remote attackers to execute arbitrary code or cause a denial of service (use-after-free and application crash) via crafted serialized data containing a ZipArchive object. -
- - Red Hat Software Collections for Red Hat Enterprise Linux Server (v. 6) - 2016-11-15T00:00:00 - RHSA-2016:2750 - rh-php56-php-5.6.25-1.el6 - - - Red Hat Software Collections for Red Hat Enterprise Linux Server (v. 7) - 2016-11-15T00:00:00 - RHSA-2016:2750 - rh-php56-php-5.6.25-1.el7 - - - Red Hat Enterprise Linux 5 - Not affected - php - - - Red Hat Enterprise Linux 5 - Will not fix - php53 - - - Red Hat Enterprise Linux 6 - Will not fix - php - - - Red Hat Enterprise Linux 7 - Will not fix - php - - php 5.5.37, php 5.6.23 -
- ->>> json = a.find_cves(after='2015-01-01', before='2015-02-01') -[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve.json?after=2015-01-01&before=2015-02-01' ... -[NOTICE ] rhsda: 232 CVEs found with search query ->>> json = a.find_cves(params={'after':'2015-01-01', 'before':'2015-02-01'}) -[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve.json?after=2015-01-01&before=2015-02-01' ... -[NOTICE ] rhsda: 232 CVEs found with search query - ->>> json = a.find_cvrfs(after='2015-01-01', before='2015-02-01') -[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cvrf.json?after=2015-01-01&before=2015-02-01' ... -[NOTICE ] rhsda: 50 CVRFs found with search query - ->>> json = a.find_ovals(after='2015-01-01', before='2015-02-01') -[INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/oval.json?after=2015-01-01&before=2015-02-01' ... -[NOTICE ] rhsda: 20 OVALs found with search query +There's also a convenience `cve_search_query()` method but that might go away. +``` >>> txt = a.cve_search_query({'after':'2015-01-01', 'before':'2015-02-01', 'per_page':5}, outFormat='plaintext') [INFO ] rhsda: Getting 'https://access.redhat.com/labs/securitydataapi/cve.json?per_page=5&after=2015-01-01&before=2015-02-01' ... [NOTICE ] rhsda: 5 CVEs found with search query