diff --git a/.gitignore b/.gitignore index 72364f9..f77a5d4 100644 --- a/.gitignore +++ b/.gitignore @@ -79,6 +79,7 @@ celerybeat-schedule .env # virtualenv +.venv/ venv/ ENV/ @@ -87,3 +88,6 @@ ENV/ # Rope project settings .ropeproject + +# +.vscode/launch.json diff --git a/.tito/packages/.readme b/.tito/packages/.readme new file mode 100644 index 0000000..b9411e2 --- /dev/null +++ b/.tito/packages/.readme @@ -0,0 +1,3 @@ +the .tito/packages directory contains metadata files +named after their packages. Each file has the latest tagged +version and the project's relative directory. diff --git a/.tito/packages/rhsecapi b/.tito/packages/rhsecapi new file mode 100644 index 0000000..4fc0073 --- /dev/null +++ b/.tito/packages/rhsecapi @@ -0,0 +1 @@ +1.0.6-1 ./ diff --git a/.tito/tito.props b/.tito/tito.props new file mode 100644 index 0000000..9f6fd2b --- /dev/null +++ b/.tito/tito.props @@ -0,0 +1,6 @@ +[buildconfig] +builder = tito.builder.Builder +tagger = tito.tagger.VersionTagger +changelog_do_not_remove_cherrypick = 0 +changelog_format = %s (%ae) + diff --git a/README.md b/README.md index 693f401..1da4a01 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # rhsecapi -`rhsecapi` makes it easy to interface with the [Red Hat Security Data API](https://access.redhat.com/documentation/en/red-hat-security-data-api/) -- even from [behind a proxy](https://github.com/ryran/rhsecapi/issues/29). From the rpm description: +`rhsecapi` makes it easy to interface with the [Red Hat Security Data API](https://access.redhat.com/documentation/en/red-hat-security-data-api/) -- even from [behind a proxy](https://github.com/RedHatOfficial/rhsecapi/issues/29). From the rpm description: -> **Leverage Red Hat's Security Data API to find CVEs by various attributes (date, severity, scores, package, IAVA, etc). Retrieve customizable details about found CVEs or about specific CVE ids input on cmdline. Parse arbitrary stdin for CVE ids and generate a customized report, optionally sending it straight to pastebin. Searches are done via a single instantaneous http request and CVE retrieval is parallelized, utilizing multiple threads at once. Python requests is used for all remote communication, so proxy support is baked right in. BASH intelligent tab-completion is supported via optional Python argcomplete module. Python2 tested on RHEL6, RHEL7, & Fedora but since it doesn't integrate with RHN/RHSM/yum/Satellite, it can be used on any internet-connected machine. Feedback, feature requests, and code contributions welcome.** +> **Leverage Red Hat's Security Data API to find CVEs by various attributes (date, severity, scores, package, etc). Retrieve customizable details about found CVEs or about specific CVE ids input on cmdline. Parse arbitrary stdin for CVE ids and generate a customized report, optionally sending it straight to pastebin. Searches are done via a single instantaneous http request and CVE retrieval is parallelized, utilizing multiple threads at once. Python requests is used for all remote communication, so proxy support is baked right in. BASH intelligent tab-completion is supported via optional Python argcomplete module. Python2 tested on RHEL6, RHEL7, & Fedora but since it doesn't integrate with RHN/RHSM/yum/Satellite, it can be used on any internet-connected machine. Feedback, feature requests, and code contributions welcome.** If you don't have a GitHub account but do have a Red Hat Portal login, go here: [New cmdline tool using Red Hat's new Security Data API: rhsecapi](https://access.redhat.com/discussions/2713931). @@ -15,7 +15,6 @@ If you don't have a GitHub account but do have a Red Hat Portal login, go here: - [Find CVEs](#find-cves) - [Empty search: list CVEs by public-date](#empty-search-list-cves-by-public-date) - [Find CVEs by attributes](#find-cves-by-attributes) -- [Working with IAVAs](#working-with-iavas) - [Advanced: find unresolved CVEs for a specific package in a specific product](#advanced-find-unresolved-cves-for-a-specific-package-in-a-specific-product) - [Full help page](#full-help-page) - [Working with backend rhsda library](#working-with-backend-rhsda-library) @@ -174,7 +173,7 @@ sys 0m0.055s 1. Execute: `rhsecapi` - **Option 2: Download latest release from github and run it** - 1. Go to [Releases](https://github.com/ryran/rhsecapi/releases) + 1. Go to [Releases](https://github.com/RedHatOfficial/rhsecapi/releases) 1. Download and extract the latest release 1. Optional: `mkdir -p ~/bin; ln -sv /PATH/TO/rhsecapi.py ~/bin/rhsecapi` 1. Execute: `rhsecapi` @@ -198,7 +197,7 @@ Run rhsecapi --help for full help page VERSION: rhsecapi v1.0.0_rc10 last mod 2017/01/05 - See to report bugs or RFEs + See to report bugs or RFEs ``` ## BASH intelligent tab-completion @@ -211,7 +210,7 @@ $ rhsecapi --[TabTab] --extract-cves --pastebin --q-cvss --q-product --fields --pexpire --q-cvss3 --q-raw --help --product --q-cwe --q-severity ---iava --q-advisory --q-empty --stdin +--q-advisory --q-empty --stdin ``` ## Field display @@ -273,11 +272,11 @@ Note that there are also two presets: `--all-fields` and `--most-fields` ``` $ 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' +[DEBUG ] rhsda: Enabled fields: 'threat_severity, public_date, cwe, cvss, cvss3, bugzilla, upstream_fix, 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' +[DEBUG ] rhsda: Enabled fields: 'threat_severity, public_date, cwe, cvss, cvss3, bugzilla, acknowledgement, details, statement, mitigation, upstream_fix, references, affected_release, package_state' ``` ## Find CVEs @@ -379,68 +378,6 @@ CVE-2015-0235 RHEV Hypervisor for RHEL-6: [rhev-hypervisor6-6.6-20150123.1.el6ev] via RHSA-2015:0126 (2015-02-04) ``` - -### Working with IAVAs - -IAVAs can be retrieved instantly ... - -``` -$ rhsecapi --iava 2016-A-0287 -i 2016-A-0309 --urls -[NOTICE ] rhsda: Valid Red Hat IAVA results retrieved: 2 of 2 -[NOTICE ] rhsda: Number of CVEs mapped from retrieved IAVAs: 5 - -2016-A-0287 (https://access.redhat.com/labs/securitydataapi/iava?number=2016-A-0287) - TITLE : Multiple Vulnerabilities in Oracle Enterprise Manager - SEVERITY : CAT I - ID : 140611 - CVES : - CVE-2015-7940 (https://access.redhat.com/security/cve/CVE-2015-7940) - CVE-2016-2107 (https://access.redhat.com/security/cve/CVE-2016-2107) - CVE-2016-4979 (https://access.redhat.com/security/cve/CVE-2016-4979) - CVE-2016-5604 (https://access.redhat.com/security/cve/CVE-2016-5604) - -2016-A-0309 (https://access.redhat.com/labs/securitydataapi/iava?number=2016-A-0309) - TITLE : ISC BIND Remote Denial of Service Vulnerability - SEVERITY : CAT I - ID : 140634 - CVES : - CVE-2016-8864 (https://access.redhat.com/security/cve/CVE-2016-8864) -``` - -Each of the mapped CVEs can be looked up by simply adding the `-x`/`--extract-cves` option. (For brevity, the following example also uses `--product`.) - -``` -$ rhsecapi --iava 2016-A-0287 -i 2016-A-0309 --urls --extract-cves --product 'linux 6' -[NOTICE ] rhsda: Valid Red Hat IAVA results retrieved: 2 of 2 -[NOTICE ] rhsda: Number of CVEs mapped from retrieved IAVAs: 5 -[NOTICE ] rhsda: Valid Red Hat CVE results retrieved: 4 of 5 -[NOTICE ] rhsda: Results matching spotlight-product option: 3 of 5 - -CVE-2016-8864 (https://access.redhat.com/security/cve/CVE-2016-8864) - SEVERITY : Important Impact (https://access.redhat.com/security/updates/classification) - DATE : 2016-11-01 - BUGZILLA : https://bugzilla.redhat.com/show_bug.cgi?id=1389652 - FIXED_RELEASES matching 'linux 6' : - Red Hat Enterprise Linux 6: [bind-32:9.8.2-0.47.rc1.el6_8.3] via https://access.redhat.com/errata/RHSA-2016:2141 (2016-11-02) - -CVE-2016-2107 (https://access.redhat.com/security/cve/CVE-2016-2107) - SEVERITY : Moderate Impact (https://access.redhat.com/security/updates/classification) - DATE : 2016-05-03 - BUGZILLA : https://bugzilla.redhat.com/show_bug.cgi?id=1331426 - FIXED_RELEASES matching 'linux 6' : - Red Hat Enterprise Linux 6: [openssl-1.0.1e-48.el6_8.1] via https://access.redhat.com/errata/RHSA-2016:0996 (2016-05-10) - FIX_STATES matching 'linux 6' : - Not affected: Red Hat Enterprise Linux 6 [openssl098e] - -CVE-2016-4979 (https://access.redhat.com/security/cve/CVE-2016-4979) - SEVERITY : Moderate Impact (https://access.redhat.com/security/updates/classification) - DATE : 2016-07-05 - BUGZILLA : https://bugzilla.redhat.com/show_bug.cgi?id=1352476 - FIX_STATES matching 'linux 6' : - Not affected: Red Hat Enterprise Linux 6 [httpd] -``` - - ## Advanced: find unresolved CVEs for a specific package in a specific product - **Question:** @@ -532,9 +469,9 @@ usage: rhsecapi [--q-before YYYY-MM-DD] [--q-after YYYY-MM-DD] [--q-bug BZID] [--q-product PRODUCT] [--q-package PKG] [--q-cwe CWEID] [--q-cvss SCORE] [--q-cvss3 SCORE] [--q-empty] [--q-pagesize PAGESZ] [--q-pagenum PAGENUM] [--q-raw RAWQUERY] - [-i YYYY-?-NNNN] [-x] [-0] [-f FIELDS | -a | -m] [-p PRODUCT] - [-j] [-u] [-w [WIDTH]] [-c] [-l {debug,info,notice,warning}] - [-t THREDS] [-P] [-E [DAYS]] [--dryrun] [-h] [--help] + [-x] [-0] [-f FIELDS | -a | -m] [-p PRODUCT] [-j] [-u] + [-w [WIDTH]] [-c] [-l {debug,info,notice,warning}] [-t THREDS] + [-P] [-E [DAYS]] [--dryrun] [-h] [--help] [CVE-YYYY-NNNN [CVE-YYYY-NNNN ...]] Make queries against the Red Hat Security Data API @@ -552,7 +489,7 @@ FIND CVES BY ATTRIBUTE: --q-severity IMPACT Narrow down results by severity rating (specify one of 'low', 'moderate', 'important', or 'critical') --q-product PRODUCT Narrow down results by product name via case- - insensitive regex (e.g.: 'linux 7' or openstack + insensitive regex (e.g.: 'linux 7' or 'openstack platform [89]'); the API checks this against the 'FIXED_RELEASES' field so will only match CVEs where PRODUCT matches the 'product_name' of some released @@ -575,20 +512,13 @@ FIND CVES BY ATTRIBUTE: --q-raw b=y'); this allows passing arbitrary params (e.g. something new that is unknown to rhsecapi) -RETRIEVE SPECIFIC IAVAS: - -i, --iava YYYY-?-NNNN - Retrieve notice details for an IAVA number; specify - option multiple times to retrieve multiple IAVAs at - once (use below --extract-cves option to lookup mapped - CVEs) - RETRIEVE SPECIFIC CVES: CVE-YYYY-NNNN Retrieve a CVE or list of CVEs (e.g.: 'CVE-2016-5387'); note that case-insensitive regex- matching is done -- extra characters & duplicate CVEs will be discarded -x, --extract-cves Extract CVEs from search query (as initiated by at - least one of the --q-xxx options or the --iava option) + least one of the --q-xxx options) -0, --stdin Extract CVEs from stdin (CVEs will be matched by case- insensitive regex 'CVE-[0-9]{4}-[0-9]{4,}' and duplicates will be discarded); note that terminal @@ -606,11 +536,11 @@ CVE DISPLAY OPTIONS: date, affected_release → fixed_releases or fixed or 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 all-fields (e.g., '-f + fields to the default (e.g., '-f +cvss3') or a 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, + threat_severity, public_date, cwe, cvss, cvss3, bugzilla, acknowledgement, details, statement, mitigation, upstream_fix, references, affected_release, package_state) @@ -642,7 +572,7 @@ GENERAL OPTIONS: 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) + making CVE queries (default on this system: 48) -P, --pastebin Send output to Fedora Project Pastebin (paste.fedoraproject.org) and print only URL to stdout -E, --pexpire [DAYS] Set time in days after which paste will be deleted @@ -656,8 +586,8 @@ GENERAL OPTIONS: --help Show this help message and exit VERSION: - rhsecapi v1.0.0_rc10 last mod 2017/01/05 - See to report bugs or RFEs + rhsecapi v1.0.1 last mod 2017/06/27 + See to report bugs or RFEs ``` @@ -784,17 +714,6 @@ CLASSES | With *outFormat* of "xml", returns unformatted XML as string. | If *params* dict is passed, additional parameters are ignored. | - | find_iavas(self, params=None, outFormat='json', number=None, severity=None, page=None, per_page=None) - | Find IAVA notices by recent or attributes. - | - | Provides an index to recent IAVA notices when no parameters are passed. - | Each list item is a convenience object with minimal attributes. - | Use parameters to narrow down results. - | - | 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. | @@ -815,9 +734,6 @@ CLASSES | get_cvrf_oval(self, rhsa, outFormat='json') | Retrieve CVRF-OVAL details for an RHSA. | - | get_iava(self, iava, outFormat='json') - | Retrieve notice details for an IAVA. - | | get_oval(self, rhsa, outFormat='json') | Retrieve OVAL details for an RHSA. | @@ -850,7 +766,7 @@ CLASSES | ON *FIELDS*: | | librhsecapi.cveFields.all is a list obj of supported fields, i.e.: - | threat_severity, public_date, iava, cwe, cvss, cvss3, bugzilla, + | threat_severity, public_date, cwe, cvss, cvss3, bugzilla, | acknowledgement, details, statement, mitigation, upstream_fix, references, | affected_release, package_state | @@ -877,23 +793,6 @@ CLASSES | fields="^releases,mitigation" | | Finally: *fields* is case-insensitive. - | - | mget_iavas(self, iavas, numThreads=0, onlyCount=False, outFormat='plaintext', urls=False, timeout=300) - | Use multi-threading to lookup a list of IAVAs and return text output. - | - | *iavas*: A list of IAVA 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 ("list", "plaintext", "json", or "jsonpretty") - | *urls*: Whether to add extra URLs to certain fields - | *timeout*: Total ammount of time to wait for all CVEs to be retrieved - | - | ON *OUTFORMAT*: - | - | Setting to "list" returns list object containing ONLY CVE ids. - | 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 FUNCTIONS extract_cves_from_input(obj, descriptiveNoun=None) diff --git a/rhsecapi.py b/bin/rhsecapi similarity index 87% rename from rhsecapi.py rename to bin/rhsecapi index e396a28..b973ad8 100755 --- a/rhsecapi.py +++ b/bin/rhsecapi @@ -1,4 +1,4 @@ -#!/usr/bin/python2 +#!/usr/bin/env python # -*- coding: utf-8 -*- # PYTHON_ARGCOMPLETE_OK #------------------------------------------------------------------------------- @@ -46,8 +46,8 @@ # Globals prog = 'rhsecapi' vers = {} -vers['version'] = '1.0.0_rc10' -vers['date'] = '2017/01/05' +vers['version'] = '1.0.5' +vers['date'] = '2024/04/03' # Logging @@ -151,7 +151,7 @@ def parse_args(): epilog = ( "VERSION:\n" " {0}\n" - " See to report bugs or RFEs").format(version) + " See to report bugs or RFEs").format(version) fmt = lambda prog: CustomFormatter(prog) p = argparse.ArgumentParser( prog=prog, @@ -179,7 +179,7 @@ def parse_args(): help="Narrow down results by severity rating (specify one of 'low', 'moderate', 'important', or 'critical')") g_listByAttr.add_argument( '--q-product', metavar="PRODUCT", - help="Narrow down results by product name via case-insensitive regex (e.g.: 'linux 7' or openstack platform [89]'); the API checks this against the 'FIXED_RELEASES' field so will only match CVEs where PRODUCT matches the 'product_name' of some released errata") + help="Narrow down results by product name via case-insensitive regex (e.g.: 'linux 7' or 'openstack platform [89]'); the API checks this against the 'FIXED_RELEASES' field so will only match CVEs where PRODUCT matches the 'product_name' of some released errata") g_listByAttr.add_argument( '--q-package', metavar="PKG", help="Narrow down results by package name (e.g.: 'samba' or 'thunderbird')") @@ -205,12 +205,6 @@ def parse_args(): '--q-raw', metavar="RAWQUERY", action='append', help="Narrow down results by RAWQUERY (e.g.: '--q-raw a=x --q-raw b=y'); this allows passing arbitrary params (e.g. something new that is unknown to {0})".format(prog)) # New group - g_listByIava = p.add_argument_group( - 'RETRIEVE SPECIFIC IAVAS') - g_listByIava.add_argument( - '-i', '--iava', dest='iavas', metavar='YYYY-?-NNNN', action='append', - help="Retrieve notice details for an IAVA number; specify option multiple times to retrieve multiple IAVAs at once (use below --extract-cves option to lookup mapped CVEs)") - # New group g_getCve = p.add_argument_group( 'RETRIEVE SPECIFIC CVES') g_getCve.add_argument( @@ -218,7 +212,7 @@ def parse_args(): help="Retrieve a CVE or list of CVEs (e.g.: 'CVE-2016-5387'); note that case-insensitive regex-matching is done -- extra characters & duplicate CVEs will be discarded") g_getCve.add_argument( '-x', '--extract-cves', action='store_true', - help="Extract CVEs from search query (as initiated by at least one of the --q-xxx options or the --iava option)") + help="Extract CVEs from search query (as initiated by at least one of the --q-xxx options)") g_getCve.add_argument( '-0', '--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(rhsda.cve_regex_string)) @@ -228,7 +222,7 @@ def parse_args(): g_cveDisplay0 = g_cveDisplay.add_mutually_exclusive_group() g_cveDisplay0.add_argument( '-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))) + 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 +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='ALL', @@ -256,7 +250,7 @@ def parse_args(): '-c', '--count', action='store_true', help="Exit after printing CVE counts") g_general.add_argument( - '-l', '--loglevel', choices=['debug','info','notice','warning'], default='notice', + '-l', '--loglevel', choices=['debug','info','notice','warning'], default='warning', 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=rhsda.numThreadsDefault, @@ -281,12 +275,7 @@ def parse_args(): argcomplete.autocomplete(p) o = p.parse_args() if o.showHelp: - from tempfile import NamedTemporaryFile - from subprocess import call - tmp = NamedTemporaryFile(prefix='{0}-help-'.format(prog), suffix='.txt') - p.print_help(file=tmp) - tmp.flush() - call(['less', tmp.name]) + p.print_help() sys.exit() # Add search params to dict o.searchParams = { @@ -312,9 +301,6 @@ def parse_args(): o.doSearch = False else: o.doSearch = True - if o.iavas: - print("{0}: error: --q-xxx options not allowed in concert with -i/--iava".format(prog), file=sys.stderr) - sys.exit(1) if o.cves or o.stdin: print("{0}: error: --q-xxx options not allowed in concert with CVE args".format(prog), file=sys.stderr) sys.exit(1) @@ -326,8 +312,8 @@ def parse_args(): found = rhsda.extract_cves_from_input(sys.stdin) o.cves.extend(found) # If no search (--q-xxx) and no CVEs mentioned - if not o.showUsage and not (o.doSearch or o.cves or o.iavas): - logger.error("Must specify CVEs/IAVAs to retrieve or a search to perform (--q-xxx opts)") + if not o.showUsage and not (o.doSearch or o.cves): + logger.error("Must specify CVEs to retrieve or a search to perform (--q-xxx opts)") o.showUsage = True if o.showUsage: p.print_usage() @@ -346,11 +332,12 @@ def parse_args(): def main(opts): apiclient = rhsda.ApiClient(opts.loglevel) + from os import environ - if environ.has_key('RHSDA_URL') and environ['RHSDA_URL'].startswith('http'): + if environ.get('RHSDA_URL', '').startswith('http'): apiclient.cfg.apiUrl = environ['RHSDA_URL'] + searchOutput = "" - iavaOutput = "" cveOutput = "" if opts.doSearch: if opts.extract_cves: @@ -366,18 +353,6 @@ def main(opts): if not opts.pastebin: print(file=sys.stderr) print(searchOutput, end="") - if opts.iavas: - logger.debug("IAVAs: {0}".format(opts.iavas)) - if opts.extract_cves: - result = apiclient.mget_iavas(iavas=opts.iavas, numThreads=opts.threads, onlyCount=opts.count, outFormat='list') - opts.cves.extend(result) - elif opts.count: - result = apiclient.mget_iavas(iavas=opts.iavas, numThreads=opts.threads, onlyCount=opts.count) - else: - iavaOutput = apiclient.mget_iavas(iavas=opts.iavas, numThreads=opts.threads, outFormat=opts.outFormat, urls=opts.printUrls) - if not opts.pastebin: - print(file=sys.stderr) - print(iavaOutput, end="") if opts.cves: originalCount = len(opts.cves) # Converting to a set removes duplicates @@ -389,8 +364,6 @@ def main(opts): logger.log(25, "Skipping CVE retrieval due to --dryrun; would have retrieved: {0}".format(len(opts.cves))) cveOutput = " ".join(opts.cves) + "\n" else: - if 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.product) if opts.count: return @@ -398,7 +371,7 @@ def main(opts): opts.p_lang = 'text' if opts.json: opts.p_lang = 'Python' - data = searchOutput + iavaOutput + cveOutput + data = searchOutput + cveOutput try: response = fpaste_it(inputdata=data, author=prog, lang=opts.p_lang, expire=opts.pexpire) except ValueError as e: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests diff --git a/rhsda.py b/rhsda/__init__.py similarity index 80% rename from rhsda.py rename to rhsda/__init__.py index 5064857..aadc8d7 100644 --- a/rhsda.py +++ b/rhsda/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/python2 # -*- coding: utf-8 -*- #------------------------------------------------------------------------------- # Copyright 2016, 2017 @@ -24,11 +23,15 @@ import textwrap, fcntl, termios, struct import json import signal -import copy_reg import types -import multiprocessing.dummy as multiprocessing +import multiprocessing as multiprocessing from argparse import Namespace +# Check to see if we are running Python 2 or 3 +try: + import copy_reg +except: + import copyreg as copy_reg # Logging logging.addLevelName(25, 'NOTICE') @@ -36,7 +39,7 @@ consolehandler.setLevel('DEBUG') consolehandler.setFormatter(logging.Formatter("[%(levelname)-7s] %(name)s: %(message)s")) logger = logging.getLogger('rhsda') -logger.setLevel('NOTICE') +logger.setLevel('WARNING') logger.addHandler(consolehandler) @@ -46,7 +49,6 @@ cveFields.all = [ 'threat_severity', 'public_date', - 'iava', 'cwe', 'cvss', 'cvss3', @@ -72,7 +74,6 @@ 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', @@ -101,7 +102,6 @@ # 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 @@ -122,10 +122,15 @@ def _reduce_method(m): # Set default number of worker threads -if multiprocessing.cpu_count() <= 2: +try: + hardwareCPUCount = multiprocessing.cpu_count() +except: + hardwareCPUCount = multiprocessing.dummy.cpu_count() + +if hardwareCPUCount <= 2: numThreadsDefault = 4 else: - numThreadsDefault = multiprocessing.cpu_count() * 2 + numThreadsDefault = hardwareCPUCount * 2 def jprint(jsoninput): @@ -183,7 +188,7 @@ def _get_terminal_width(self): return w def __validate_data_type(self, dT): - dataTypes = ['cvrf', 'cve', 'oval', 'iava'] + 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))) @@ -321,28 +326,6 @@ def find_ovals(self, params=None, outFormat='json', } return self._find('oval', params, outFormat) - def find_iavas(self, params=None, outFormat='json', - number=None, severity=None, - page=None, per_page=None): - """Find IAVA notices by recent or attributes. - - Provides an index to recent IAVA notices when no parameters are passed. - Each list item is a convenience object with minimal attributes. - Use parameters to narrow down results. - - 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 = { - 'number': number, - 'severity': severity, - 'page': page, - 'per_page': per_page, - } - return self._find('iava', params, outFormat) - def get_cvrf(self, rhsa, outFormat='json'): """Retrieve CVRF details for an RHSA.""" return self._retrieve('cvrf', rhsa, outFormat) @@ -359,10 +342,6 @@ def get_oval(self, rhsa, outFormat='json'): """Retrieve OVAL details for an RHSA.""" return self._retrieve('oval', rhsa, outFormat) - def get_iava(self, iava, outFormat='json'): - """Retrieve notice details for an IAVA.""" - return self._retrieve('iava', iava, outFormat) - def __stripjoin(self, input, oneLineEach=False): """Strip whitespace from input or input list.""" text = "" @@ -384,7 +363,7 @@ def __stripjoin(self, input, oneLineEach=False): 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): + if field in self.cfg.desiredFields and field in jsoninput: return True return False @@ -433,9 +412,6 @@ def _get_and_parse_cve(self, cve): # 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'])) @@ -460,7 +436,7 @@ def _get_and_parse_cve(self, cve): 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 'bugzilla' in J: if self.cfg.urls: bug = J['bugzilla']['url'] else: @@ -506,7 +482,7 @@ def _get_and_parse_cve(self, cve): # If product doesn't match spotlight, go to next continue pkg = "" - if release.has_key('package'): + if 'package' in release: pkg = " [{0}]".format(release['package']) advisory = release['advisory'] if self.cfg.urls: @@ -534,7 +510,7 @@ def _get_and_parse_cve(self, cve): # If product doesn't match spotlight, go to next continue pkg = "" - if state.has_key('package_name'): + if 'package_name' in state: 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: @@ -551,60 +527,6 @@ def _get_and_parse_cve(self, cve): out.append("") return True, "\n".join(out) - def _get_and_parse_iava(self, iava): - """Generate a plaintext representation of an IAVA. - - 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_iavas(). - """ - # Output array: - out = [] - try: - # Store json - J = self.get_iava(iava) - except requests.exceptions.HTTPError as e: - # IAVA not in RH IAVA DB - logger.info(e) - if self.cfg.onlyCount or self.cfg.outFormat in ['list', 'json', 'jsonpretty']: - return False, "", 0 - else: - return False, "{0}\n Not present in Red Hat IAVA database\n".format(iava), 0 - numCves = len(J['cvelist']) - # If json output requested - if self.cfg.outFormat.startswith('json'): - return True, J, numCves - # If CVE list output - elif self.cfg.outFormat == 'list': - return True, J['cvelist'], numCves - # If onlyCount requested - elif self.cfg.onlyCount: - return True, "", numCves - # IAVA NUMBER - u = "" - if self.cfg.urls: - u = " ({0}/iava?number={1})".format(self.cfg.apiUrl, iava) - out.append("{0}{1}".format(iava, u)) - # TITLE - out.append(" TITLE : {0}".format(J['title'])) - # SEVERITY - out.append(" SEVERITY : {0}".format(J['severity'])) - # ID - out.append(" ID : {0}".format(J['id'])) - # CVELIST - if J['cvelist']: - out.append(" CVES :") - for cve in J['cvelist']: - u = "" - if self.cfg.urls: - u = " (https://access.redhat.com/security/cve/{0})".format(cve) - out.append(" {0}{1}".format(cve, u)) - # Add one final newline to the end - out.append("") - return True, "\n".join(out), numCves - def _set_cve_plaintext_fields(self, desiredFields): logger.debug("Requested fields string: '{0}'".format(desiredFields)) if not desiredFields: @@ -698,7 +620,7 @@ def mget_cves(self, cves, numThreads=0, onlyCount=False, outFormat='plaintext', ON *FIELDS*: librhsecapi.cveFields.all is a list obj of supported fields, i.e.: - threat_severity, public_date, iava, cwe, cvss, cvss3, bugzilla, + threat_severity, public_date, cwe, cvss, cvss3, bugzilla, acknowledgement, details, statement, mitigation, upstream_fix, references, affected_release, package_state @@ -728,10 +650,20 @@ def mget_cves(self, cves, numThreads=0, onlyCount=False, outFormat='plaintext', """ 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 not isinstance(cves, list): - raise ValueError("Invalid 'cves=' argument input; must be list, string, or file obj") + # This is necessary as Python3 doesn't have "file" types + try: + if isinstance(cves, (str, file)): + cves = extract_cves_from_input(cves) + elif not isinstance(cves, list): + raise ValueError("Invalid 'cves=' argument input; must be list, string, or file obj") + except: + from io import IOBase + + if isinstance(cves, (str, IOBase)): + cves = extract_cves_from_input(cves) + elif not isinstance(cves, list): + raise ValueError("Invalid 'cves=' argument input; must be list, string, or file obj") + if not len(cves): if outFormat in ['plaintext', 'jsonpretty']: return "" @@ -791,77 +723,6 @@ def mget_cves(self, cves, numThreads=0, onlyCount=False, outFormat='plaintext', elif outFormat == 'jsonpretty': return jprint(cveOutput) - def mget_iavas(self, iavas, numThreads=0, onlyCount=False, outFormat='plaintext', - urls=False, timeout=300): - """Use multi-threading to lookup a list of IAVAs and return text output. - - *iavas*: A list of IAVA 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 ("list", "plaintext", "json", or "jsonpretty") - *urls*: Whether to add extra URLs to certain fields - *timeout*: Total ammount of time to wait for all CVEs to be retrieved - - ON *OUTFORMAT*: - - Setting to "list" returns list object containing ONLY CVE ids. - 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 - """ - if outFormat not in ['list', 'plaintext', 'json', 'jsonpretty']: - raise ValueError("Invalid outFormat ('{0}') requested; should be one of: 'list', 'plaintext', 'json', 'jsonpretty'".format(outFormat)) - if not isinstance(iavas, list): - raise ValueError("Invalid 'iavas=' argument input; must be list obj") - # Configure threads - if not numThreads: - numThreads = numThreadsDefault - # Lower threads for small work-loads - if numThreads > len(iavas): - numThreads = len(iavas) - logger.info("Using {0} worker threads".format(numThreads)) - # Set cfg directives for our worker - self.cfg.onlyCount = onlyCount - self.cfg.outFormat = outFormat - self.cfg.urls = urls - # 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._get_and_parse_iava, iavas) - # Need to specify timeout; see: http://stackoverflow.com/a/35134329 - results = p.get(timeout=timeout) - except KeyboardInterrupt: - logger.error("Received KeyboardInterrupt; terminating worker threads") - pool.terminate() - raise - else: - pool.close() - pool.join() - successValues, iavaOutput, numCves = zip(*results) - n_total = len(iavas) - n_hidden = successValues.count(None) - n_valid = successValues.count(True) - logger.log(25, "Valid Red Hat IAVA results retrieved: {0} of {1}".format(n_valid + n_hidden, n_total)) - if sum(numCves): - logger.log(25, "Number of CVEs mapped from retrieved IAVAs: {0}".format(sum(numCves))) - if outFormat == 'list': - cves = [] - for cvelist in iavaOutput: - cves.extend(cvelist) - return cves - elif onlyCount: - return - if outFormat == 'plaintext': - return "\n".join(iavaOutput) - elif outFormat == 'json': - return iavaOutput - elif outFormat == 'jsonpretty': - return jprint(iavaOutput) - def cve_search_query(self, params, outFormat='list', urls=False): """Perform a CVE search query. @@ -889,30 +750,32 @@ def cve_search_query(self, params, outFormat='list', urls=False): rows.append(["CVE ID", "PUB DATE", "BUGZILLA", "SEVERITY", "CVSS2", "CVSS3", "RHSAS", "PKGS"]) for i in result: date = "" - if i.has_key('public_date'): + if 'public_date' in i and i['public_date'] is not None: date = i['public_date'].split("T")[0] bz = "" if urls: cve = "https://access.redhat.com/security/cve/{0}".format(i['CVE']) - if i.has_key('bugzilla'): + if 'bugzilla' in i and i['bugzilla'] is not None: bz = "https://bugzilla.redhat.com/show_bug.cgi?id={0}".format(i['bugzilla']) else: cve = i['CVE'] - if i.has_key('bugzilla'): + if 'bugzilla' in i and i['bugzilla'] is not None: bz = i['bugzilla'] - severity = i['severity'] - rhsas = "" - if i.has_key('advisories'): - rhsas = "{0: >2}".format(len(i['advisories'])) - pkgs = "" - if i.has_key('affected_packages'): - pkgs = "{0: >2}".format(len(i['affected_packages'])) + severity = "" + if 'severity' in i and i['severity'] is not None: + severity = i['severity'] cvss2 = "" - if i.has_key('cvss_score'): + if 'cvss_score' in i and i['cvss_score'] is not None: cvss2 = str(i['cvss_score']) cvss3 = "" - if i.has_key('cvss3_score'): + if 'cvss3_score' in i and i['cvss3_score'] is not None: cvss3 = str(i['cvss3_score']) + rhsas = "" + if 'advisories' in i and i['advisories'] is not None: + rhsas = "{0: >2}".format(len(i['advisories'])) + pkgs = "" + if 'affected_packages' in i and i['affected_packages'] is not None: + pkgs = "{0: >2}".format(len(i['affected_packages'])) line = [cve, date, bz, severity, cvss2, cvss3, rhsas, pkgs] rows.append(line) return self._columnize(rows, sep=" ") diff --git a/rhsecapi.spec b/rhsecapi.spec new file mode 100644 index 0000000..ecc5cf8 --- /dev/null +++ b/rhsecapi.spec @@ -0,0 +1,137 @@ +# Disable the debuginfo build operation +%global debug_package %{nil} + +%if 0%{?fedora} +%bcond_without python3 +%else +%if 0%{?rhel} >= 8 +%bcond_without python3 +%else +%bcond_with python3 +%endif +%endif + +Name: rhsecapi +Version: 1.0.6 +Release: 1%{?dist} +Summary: Provides a simple interface for the Red Hat Security Data API + +License: GPL +URL: https://github.com/RedHatOfficial/rhsecapi +Source: %{name}-%{version}.tar.gz + +%if %{with python3} +BuildRequires: python3-devel python3-setuptools +Requires: python3-requests python3-%{name} +%else +BuildRequires: python-devel python-setuptools +Requires: python-argparse python-requests python-%{name} +%endif + +%description +Leverage Red Hat's Security Data API to find CVEs by various attributes +(date, severity, scores, package, IAVA, etc). Retrieve customizable details +about found CVEs or about specific CVE ids input on cmdline. Parse +arbitrary stdin for CVE ids and generate a customized report, optionally +sending it straight to pastebin. Searches are done via a single +instantaneous http request and CVE retrieval is parallelized, utilizing +multiple threads at once. Python requests is used for all remote +communication, so proxy support is baked right in. BASH intelligent +tab-completion is supported via optional Python argcomplete module. Python2 +tested on RHEL6, RHEL7, & Fedora and Python3 on Fedora but since it doesnt +integrate with RHN/RHSM/yum/Satellite, it can be used on any +internet-connected machine. Feedback, feature requests, and code +contributions welcome. + +%if %{with python3} +%package -n python3-%{name} +Summary: Provides a simple interface for the Red Hat Security Data API + +%description -n python3-%{name} +Leverage Red Hat's Security Data API to find CVEs by various attributes +(date, severity, scores, package, IAVA, etc). Retrieve customizable details +about found CVEs or about specific CVE ids input on cmdline. Parse +arbitrary stdin for CVE ids and generate a customized report, optionally +sending it straight to pastebin. Searches are done via a single +instantaneous http request and CVE retrieval is parallelized, utilizing +multiple threads at once. Python requests is used for all remote +communication, so proxy support is baked right in. BASH intelligent +tab-completion is supported via optional Python argcomplete module. Python2 +tested on RHEL6, RHEL7, & Fedora and Python3 on Fedora but since it doesnt +integrate with RHN/RHSM/yum/Satellite, it can be used on any +internet-connected machine. Feedback, feature requests, and code +contributions welcome. +%else +%package -n python2-%{name} +Summary: Provides a simple interface for the Red Hat Security Data API + +%description -n python2-%{name} +Leverage Red Hat's Security Data API to find CVEs by various attributes +(date, severity, scores, package, IAVA, etc). Retrieve customizable details +about found CVEs or about specific CVE ids input on cmdline. Parse +arbitrary stdin for CVE ids and generate a customized report, optionally +sending it straight to pastebin. Searches are done via a single +instantaneous http request and CVE retrieval is parallelized, utilizing +multiple threads at once. Python requests is used for all remote +communication, so proxy support is baked right in. BASH intelligent +tab-completion is supported via optional Python argcomplete module. Python2 +tested on RHEL6, RHEL7, & Fedora and Python3 on Fedora but since it doesnt +integrate with RHN/RHSM/yum/Satellite, it can be used on any +internet-connected machine. Feedback, feature requests, and code +contributions welcome. +%endif + +%prep +%autosetup -c +%if %{with python3} +cp -a %{name}-%{version} python3 +%else +cp -a %{name}-%{version} python2 +%endif + +%build +rm -rf $RPM_BUILD_ROOT +mkdir $RPM_BUILD_ROOT + +%if %{with python3} +pushd python3 +# Remove CFLAGS=... for noarch packages (unneeded) +CFLAGS="$RPM_OPT_FLAGS" %{__python3} setup.py build +popd +%else +pushd python2 +# Remove CFLAGS=... for noarch packages (unneeded) +CFLAGS="$RPM_OPT_FLAGS" %{__python} setup.py build +popd +%endif + +%install +rm -rf $RPM_BUILD_ROOT + +%if %{with python3} +pushd python3 +%{__python3} setup.py install -O1 --root $RPM_BUILD_ROOT/ +popd +%else +pushd python2 +%{__python} setup.py install -O1 --root $RPM_BUILD_ROOT/ +popd +%endif + +%files +%{_bindir}/* +# For noarch packages: sitelib +%if %{with python3} +%files -n python3-%{name} +%{_bindir}/* +# For noarch packages: sitelib +%{python3_sitelib}/* +%else +%files -n python2-%{name} +%{python_sitelib}/* +%endif + +%changelog +* Wed Apr 03 2024 Kyle Walker 1.0.6-1 +- new package built with tito + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e3a6b9e --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +import os +from setuptools import setup, find_packages + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + +LONG_DESCRIPTION = """ +Leverage Red Hat's Security Data API to find CVEs by various attributes +(date, severity, scores, package, IAVA, etc). Retrieve customizable details +about found CVEs or about specific CVE ids input on cmdline. Parse +arbitrary stdin for CVE ids and generate a customized report, optionally +sending it straight to pastebin. Searches are done via a single +instantaneous http request and CVE retrieval is parallelized, utilizing +multiple threads at once. Python requests is used for all remote +communication, so proxy support is baked right in. BASH intelligent +tab-completion is supported via optional Python argcomplete module. Python2 +tested on RHEL6, RHEL7, & Fedora and Python3 on Fedora but since it doesnt +integrate with RHN/RHSM/yum/Satellite, it can be used on any +internet-connected machine. Feedback, feature requests, and code +contributions welcome. +""" +setup( + name = 'rhsecapi', + version = '1.0.6', + author = 'Ryan Sawhill Aroha', + author_email = 'rsaw@redhat.com', + description = 'Provides a simple interface for the Red Hat Security Data API', + license = 'GPL', + packages = find_packages(), + + scripts = ['bin/rhsecapi'], + install_requires = [ + 'requests', + ], + + long_description=LONG_DESCRIPTION +) + +