Cisco IOS Software and Cisco Unified Communications Manager contain a vulnerability that could allow an unauthenticated, remote attacker to cause a denial of service (DoS) condition.
\n
The vulnerability is due to improper processing of malformed packets by the affected software. An unauthenticated, remote attacker could exploit this vulnerability by sending malicious network requests to the targeted system. If successful, the attacker could cause the device to become unresponsive, resulting in a DoS condition.
\n
Cisco confirmed this vulnerability and released software updates.
\n \n \n
To exploit the vulnerability, an attacker must send malicious SIP packets to affected systems. Most environments restrict external connections using SIP, likely requiring an attacker to have access to internal networks prior to an attack. In addition, in environments that separate voice and data networks, attackers may have no access to networks that service voice traffic and allow the transmission of SIP packets, further increasing the difficulty of an exploit.
\n
Cisco indicates through the CVSS score that functional exploit code exists; however, the code is not known to be publicly available.
Cisco IOS Software and Cisco Unified Communications Manager contain a vulnerability that could allow an unauthenticated, remote attacker to cause a denial of service (DoS) condition.
\n
The vulnerability is due to improper processing of malformed packets by the affected software. An unauthenticated, remote attacker could exploit this vulnerability by sending malicious network requests to the targeted system. If successful, the attacker could cause the device to become unresponsive, resulting in a DoS condition.
\n
Cisco confirmed this vulnerability and released software updates.
\n \n \n
To exploit the vulnerability, an attacker must send malicious SIP packets to affected systems. Most environments restrict external connections using SIP, likely requiring an attacker to have access to internal networks prior to an attack. In addition, in environments that separate voice and data networks, attackers may have no access to networks that service voice traffic and allow the transmission of SIP packets, further increasing the difficulty of an exploit.
\n
Cisco indicates through the CVSS score that functional exploit code exists; however, the code is not known to be publicly available.
"
112 | }""" % test_advisory_id
113 |
114 |
115 | def mocked_get_requests(*args, **kwargs):
116 | """Mocks requests get method from QueryClient"""
117 |
118 | url = "{base_url}/{path}".format(
119 | base_url=API_URL, path=args[0])
120 | return mocked_requests_lib_get(url=url).json()
121 |
122 |
123 | def mocked_requests_lib_get(*args, **kwargs):
124 | """Mocks library requests.get method"""
125 |
126 | class MockResponse():
127 | def __init__(self, status_code, json_response):
128 | self.status_code = status_code
129 | self.json_response = json_response
130 |
131 | def json(self):
132 | return json.loads(self.json_response)
133 |
134 | def raise_for_status(self):
135 | if self.status_code == 404:
136 | raise Exception("Mock 404 Not Found Exception")
137 | elif self.status_code == 406:
138 | raise Exception("Mock 406 HttpError Exception")
139 |
140 | if API_URL in kwargs["url"]:
141 | if "advisory" in kwargs["url"]:
142 | return MockResponse(200, response_advisory_id)
143 | elif any(x in kwargs["url"] for x in
144 | ("cve", "severity", "year", "all")):
145 | return MockResponse(200, response_generic)
146 | else:
147 | return MockResponse(404, response_not_found)
148 | else:
149 | return MockResponse(406, response_error)
150 |
151 |
152 | @pytest.mark.skip(reason='Out of sync and requires / triggers token usage as'
153 | ' well as api access')
154 | class OpenVulnQueryClientTestCvrf(unittest.TestCase):
155 | """Unit Test for all function in OpenVulnQueryClient"""
156 |
157 | def setUp(self):
158 | self.open_vuln_client = query_client.OpenVulnQueryClient(
159 | config.CLIENT_ID,
160 | config.CLIENT_SECRET)
161 | self.adv_format = "cvrf"
162 |
163 | @mock.patch("query_client.OpenVulnQueryClient.get_request",
164 | side_effect=mocked_get_requests)
165 | def test_get_by_cve(self, mock_get_requests):
166 | """Checks if get_by_cve function calls request args with correct arguments"""
167 | exp_args = "cvrf/cve/%s" % test_cve
168 | response = self.open_vuln_client.get_by_cve(self.adv_format, test_cve)
169 | mock_get_requests.assert_called_with(exp_args)
170 |
171 | @mock.patch("query_client.OpenVulnQueryClient.get_request",
172 | side_effect=mocked_get_requests)
173 | def test_get_by_advisory(self, mock_get_requests):
174 | """Checks if get_by_advisory function calls request args with correct arguments"""
175 | exp_args = "cvrf/advisory/%s" % test_advisory_id
176 | response = self.open_vuln_client.get_by_advisory(self.adv_format,
177 | test_advisory_id)
178 | mock_get_requests.assert_called_with(exp_args)
179 |
180 | @mock.patch("query_client.OpenVulnQueryClient.get_request",
181 | side_effect=mocked_get_requests)
182 | def test_get_by_year(self, mock_get_requests):
183 | """Checks if get_by_year function calls request args with correct arguments"""
184 | exp_args = "cvrf/year/%s" % test_year
185 | response = self.open_vuln_client.get_by_year(self.adv_format,
186 | year=test_year)
187 | mock_get_requests.assert_called_with(exp_args)
188 |
189 | @mock.patch("query_client.OpenVulnQueryClient.get_request",
190 | side_effect=mocked_get_requests)
191 | def test_get_by_severity(self, mock_get_requests):
192 | """Checks if get_by_severity function calls request args with correct arguments"""
193 | exp_args = "cvrf/severity/%s" % test_severity
194 | response = self.open_vuln_client.get_by_severity(self.adv_format,
195 | severity=test_severity)
196 | mock_get_requests.assert_called_with(exp_args)
197 |
198 | @mock.patch("query_client.OpenVulnQueryClient.get_request",
199 | side_effect=mocked_get_requests)
200 | def test_get_by_all(self, mock_get_requests):
201 | """Checks if get_by_all function calls request args with correct arguments"""
202 | exp_args = "cvrf/all"
203 | response = self.open_vuln_client.get_by_all(self.adv_format, "all")
204 | mock_get_requests.assert_called_with(exp_args)
205 |
206 | @mock.patch("requests.get", side_effect=mocked_requests_lib_get)
207 | def test_get_requests(self, mock_get):
208 | """Checks if _get_requests function returns correct output"""
209 | test_path = "cvrf/cve/%s" % test_cve
210 | response = self.open_vuln_client.get_request(test_path)
211 | self.assertDictEqual(json.loads(response_generic), response)
212 |
213 | @mock.patch("requests.get", side_effect=mocked_requests_lib_get)
214 | def test_get_requests_invalid_path(self, mock_get):
215 | """Checks if get_by_cve function raises exception for bad url path"""
216 | invalid_test_path = "cvrf/cvoo/%s" % test_cve
217 | self.assertRaises(Exception, self.open_vuln_client.get_request,
218 | invalid_test_path)
219 |
220 | def test_advisory_list(self):
221 | advisories = json.loads(response_generic)["advisories"]
222 | advs = self.open_vuln_client.advisory_list(advisories, self.adv_format)
223 | self.assertIsInstance(advs[0], advisory.CVRF,
224 | "This object is not instance of Advisory")
225 |
226 |
227 | if __name__ == "__main__":
228 | unittest.main()
229 |
--------------------------------------------------------------------------------
/sbom_examples/example-openVulnQuery1_31.spdx:
--------------------------------------------------------------------------------
1 | ## Document Header
2 | SPDXVersion: SPDX-2.1
3 | DataLicense: CC0-1.0
4 | SPDXID: SPDXRef-DOCUMENT
5 | DocumentName: openVulnQuery1_31
6 | DocumentNamespace: https://www.cisco.com/spdxdocs
7 | Creator: Person: Omar Santos
8 | Created: 2021-06-10T20:34:00Z
9 | CreatorComment: DRAFT - DEMO ONLY - SBOM of openVulnQuery - a python-based module(s) to query the Cisco PSIRT openVuln API. The Cisco Product Security Incident Response Team (PSIRT) openVuln API is a RESTful API that allows customers to obtain Cisco Security Vulnerability information in different machine-consumable formats. APIs are important for customers because they allow their technical staff and programmers to build tools that help them do their job more effectively (in this case, to keep up with security vulnerability information). More information about the API can be found at: https://developer.cisco.com/psirt THIS DOCUMENT IS PROVIDED ON AN "AS IS" BASIS AND DOES NOT IMPLY ANY KIND OF GUARANTEE OR WARRANTY, INCLUDING THE WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE. YOUR USE OF THE INFORMATION ON THE DOCUMENT OR MATERIALS LINKED FROM THE DOCUMENT IS AT YOUR OWN RISK. CISCO RESERVES THE RIGHT TO CHANGE OR UPDATE THIS DOCUMENT AT ANY TIME.
10 | ## Packages
11 | ## 2.4 Primary Component (described by the SBOM)
12 | PackageName: openVulnQuery
13 | SPDXID: SPDXRef-openVulnQuery
14 | PackageComment: PURL is pkg:supplier/Cisco/openVulnQuery@1.31
15 | ExternalRef: PACKAGE-MANAGER purl pkg:supplier/Cisco/openVulnQuery@1.31
16 | PackageVersion: 1.31
17 | PackageSupplier: Organization: Cisco
18 | Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-openVulnQuery
19 | Relationship: SPDXRef-openVulnQuery CONTAINS NONE
20 | PackageDownloadLocation: https://github.com/CiscoPSIRT/openVulnQuery
21 | FilesAnalyzed: true
22 | PackageLicenseConcluded: NOASSERTION
23 | PackageLicenseDeclared: NOASSERTION
24 | PackageCopyrightText: NOASSERTION
25 | PackageFileName: openVulnQuery
26 | PackageHomePage: https://github.com/CiscoPSIRT/openVulnQuery
27 | ExtractedText: Copyright (c) 2021, Cisco Systems, Inc.
28 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
29 | files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
30 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished
31 | to do so, subject to the following conditions:
32 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
33 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
34 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
35 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
36 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37 | ## 2.4 All-Levels Components
38 | ##
39 | PackageName: argparse
40 | SPDXID: SPDXRef-argparse
41 | PackageComment: PURL is pkg:supplier/Python%20Software%20Foundation/argparse@1.4.0
42 | ExternalRef: PACKAGE-MANAGER purl pkg:supplier/Python%20Software%20Foundation/argparse@1.4.0
43 | PackageVersion: 1.4.0
44 | PackageSupplier: Organization: Python Software Foundation
45 | Relationship: SPDXRef-openVulnQuery CONTAINS SPDXRef-argparse
46 | Relationship: SPDXRef-argparse CONTAINS NOASSERTION
47 | PackageDownloadLocation: https://pypi.org/project/argparse/
48 | FilesAnalyzed: true
49 | PackageLicenseConcluded: NOASSERTION
50 | PackageLicenseDeclared: NOASSERTION
51 | PackageCopyrightText: argparse is (c) 2006-2009 Steven J. Bethard .
52 | PackageFileName: argparse
53 | PackageHomePage: https://docs.python.org/3/library/argparse.html#module-argparse
54 | ExtractedText: argparse is (c) 2006-2009 Steven J. Bethard .
55 | The argparse module was contributed to Python as of Python 2.7 and thus
56 | was licensed under the Python license. Same license applies to all files in
57 | the argparse package project.
58 | For details about the Python License, please see doc/Python-License.txt.
59 | History
60 | -------
61 | Before (and including) argparse 1.1, the argparse package was licensed under
62 | Apache License v2.0.
63 | After argparse 1.1, all project files from the argparse project were deleted
64 | due to license compatibility issues between Apache License 2.0 and GNU GPL v2.
65 | The project repository then had a clean start with some files taken from
66 | Python 2.7.1, so definitely all files are under Python License now.
67 | ## 2.4 All-Levels Components
68 | ##
69 | PackageName: requests
70 | SPDXID: SPDXRef-requests
71 | PackageComment: PURL is pkg:supplier/Python%20Software%20Foundation/requests@2.25.1
72 | ExternalRef: PACKAGE-MANAGER purl pkg:supplier/Python%20Software%20Foundation/requests@2.25.1
73 | PackageVersion: 2.25.1
74 | PackageSupplier: Organization: Python Software Foundation
75 | Relationship: SPDXRef-openVulnQuery CONTAINS SPDXRef-requests
76 | Relationship: SPDXRef-requests CONTAINS NOASSERTION
77 | PackageDownloadLocation: https://pypi.org/project/requests/
78 | FilesAnalyzed: false
79 | PackageLicenseConcluded: Apache License
80 | PackageLicenseDeclared: Apache License
81 | PackageCopyrightText: Apache License
82 | PackageFileName: requests
83 | PackageHomePage: https://github.com/psf/requests
84 | ExtractedText:
85 | Apache License
86 | Version 2.0, January 2004
87 | http://www.apache.org/licenses/
88 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
89 | 1. Definitions.
90 | "License" shall mean the terms and conditions for use, reproduction,
91 | and distribution as defined by Sections 1 through 9 of this document.
92 | "Licensor" shall mean the copyright owner or entity authorized by
93 | the copyright owner that is granting the License.
94 | "Legal Entity" shall mean the union of the acting entity and all
95 | other entities that control, are controlled by, or are under common
96 | control with that entity. For the purposes of this definition,
97 | "control" means (i) the power, direct or indirect, to cause the
98 | direction or management of such entity, whether by contract or
99 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
100 | outstanding shares, or (iii) beneficial ownership of such entity.
101 | "You" (or "Your") shall mean an individual or Legal Entity
102 | exercising permissions granted by this License.
103 | "Source" form shall mean the preferred form for making modifications,
104 | including but not limited to software source code, documentation
105 | source, and configuration files.
106 | "Object" form shall mean any form resulting from mechanical
107 | transformation or translation of a Source form, including but
108 | not limited to compiled object code, generated documentation,
109 | and conversions to other media types.
110 | "Work" shall mean the work of authorship, whether in Source or
111 | Object form, made available under the License, as indicated by a
112 | copyright notice that is included in or attached to the work
113 | (an example is provided in the Appendix below).
114 | "Derivative Works" shall mean any work, whether in Source or Object
115 | form, that is based on (or derived from) the Work and for which the
116 | editorial revisions, annotations, elaborations, or other modifications
117 | represent, as a whole, an original work of authorship. For the purposes
118 | of this License, Derivative Works shall not include works that remain
119 | separable from, or merely link (or bind by name) to the interfaces of,
120 | the Work and Derivative Works thereof.
121 | "Contribution" shall mean any work of authorship, including
122 | the original version of the Work and any modifications or additions
123 | to that Work or Derivative Works thereof, that is intentionally
124 | submitted to Licensor for inclusion in the Work by the copyright owner
125 | or by an individual or Legal Entity authorized to submit on behalf of
126 | the copyright owner. For the purposes of this definition, "submitted"
127 | means any form of electronic, verbal, or written communication sent
128 | to the Licensor or its representatives, including but not limited to
129 | communication on electronic mailing lists, source code control systems,
130 | and issue tracking systems that are managed by, or on behalf of, the
131 | Licensor for the purpose of discussing and improving the Work, but
132 | excluding communication that is conspicuously marked or otherwise
133 | designated in writing by the copyright owner as "Not a Contribution."
134 | "Contributor" shall mean Licensor and any individual or Legal Entity
135 | on behalf of whom a Contribution has been received by Licensor and
136 | subsequently incorporated within the Work.
137 | 2. Grant of Copyright License. Subject to the terms and conditions of
138 | this License, each Contributor hereby grants to You a perpetual,
139 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
140 | copyright license to reproduce, prepare Derivative Works of,
141 | publicly display, publicly perform, sublicense, and distribute the
142 | Work and such Derivative Works in Source or Object form.
143 | 3. Grant of Patent License. Subject to the terms and conditions of
144 | this License, each Contributor hereby grants to You a perpetual,
145 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
146 | (except as stated in this section) patent license to make, have made,
147 | use, offer to sell, sell, import, and otherwise transfer the Work,
148 | where such license applies only to those patent claims licensable
149 | by such Contributor that are necessarily infringed by their
150 | Contribution(s) alone or by combination of their Contribution(s)
151 | with the Work to which such Contribution(s) was submitted. If You
152 | institute patent litigation against any entity (including a
153 | cross-claim or counterclaim in a lawsuit) alleging that the Work
154 | or a Contribution incorporated within the Work constitutes direct
155 | or contributory patent infringement, then any patent licenses
156 | granted to You under this License for that Work shall terminate
157 | as of the date such litigation is filed.
158 | 4. Redistribution. You may reproduce and distribute copies of the
159 | Work or Derivative Works thereof in any medium, with or without
160 | modifications, and in Source or Object form, provided that You
161 | meet the following conditions:
162 | (a) You must give any other recipients of the Work or
163 | Derivative Works a copy of this License; and
164 | (b) You must cause any modified files to carry prominent notices
165 | stating that You changed the files; and
166 | (c) You must retain, in the Source form of any Derivative Works
167 | that You distribute, all copyright, patent, trademark, and
168 | attribution notices from the Source form of the Work,
169 | excluding those notices that do not pertain to any part of
170 | the Derivative Works; and
171 | (d) If the Work includes a "NOTICE" text file as part of its
172 | distribution, then any Derivative Works that You distribute must
173 | include a readable copy of the attribution notices contained
174 | within such NOTICE file, excluding those notices that do not
175 | pertain to any part of the Derivative Works, in at least one
176 | of the following places: within a NOTICE text file distributed
177 | as part of the Derivative Works; within the Source form or
178 | documentation, if provided along with the Derivative Works; or,
179 | within a display generated by the Derivative Works, if and
180 | wherever such third-party notices normally appear. The contents
181 | of the NOTICE file are for informational purposes only and
182 | do not modify the License. You may add Your own attribution
183 | notices within Derivative Works that You distribute, alongside
184 | or as an addendum to the NOTICE text from the Work, provided
185 | that such additional attribution notices cannot be construed
186 | as modifying the License.
187 | You may add Your own copyright statement to Your modifications and
188 | may provide additional or different license terms and conditions
189 | for use, reproduction, or distribution of Your modifications, or
190 | for any such Derivative Works as a whole, provided Your use,
191 | reproduction, and distribution of the Work otherwise complies with
192 | the conditions stated in this License.
193 | 5. Submission of Contributions. Unless You explicitly state otherwise,
194 | any Contribution intentionally submitted for inclusion in the Work
195 | by You to the Licensor shall be under the terms and conditions of
196 | this License, without any additional terms or conditions.
197 | Notwithstanding the above, nothing herein shall supersede or modify
198 | the terms of any separate license agreement you may have executed
199 | with Licensor regarding such Contributions.
200 | 6. Trademarks. This License does not grant permission to use the trade
201 | names, trademarks, service marks, or product names of the Licensor,
202 | except as required for reasonable and customary use in describing the
203 | origin of the Work and reproducing the content of the NOTICE file.
204 | 7. Disclaimer of Warranty. Unless required by applicable law or
205 | agreed to in writing, Licensor provides the Work (and each
206 | Contributor provides its Contributions) on an "AS IS" BASIS,
207 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
208 | implied, including, without limitation, any warranties or conditions
209 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
210 | PARTICULAR PURPOSE. You are solely responsible for determining the
211 | appropriateness of using or redistributing the Work and assume any
212 | risks associated with Your exercise of permissions under this License.
213 | 8. Limitation of Liability. In no event and under no legal theory,
214 | whether in tort (including negligence), contract, or otherwise,
215 | unless required by applicable law (such as deliberate and grossly
216 | negligent acts) or agreed to in writing, shall any Contributor be
217 | liable to You for damages, including any direct, indirect, special,
218 | incidental, or consequential damages of any character arising as a
219 | result of this License or out of the use or inability to use the
220 | Work (including but not limited to damages for loss of goodwill,
221 | work stoppage, computer failure or malfunction, or any and all
222 | other commercial damages or losses), even if such Contributor
223 | has been advised of the possibility of such damages.
224 | 9. Accepting Warranty or Additional Liability. While redistributing
225 | the Work or Derivative Works thereof, You may choose to offer,
226 | and charge a fee for, acceptance of support, warranty, indemnity,
227 | or other liability obligations and/or rights consistent with this
228 | License. However, in accepting such obligations, You may act only
229 | on Your own behalf and on Your sole responsibility, not on behalf
230 | of any other Contributor, and only if You agree to indemnify,
231 | defend, and hold each Contributor harmless for any liability
232 | incurred by, or claims asserted against, such Contributor by reason
233 | of your accepting any such warranty or additional liability.
234 |
--------------------------------------------------------------------------------
/openVulnQuery/_library/cli_api.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import datetime as dt
3 | import json
4 | import os
5 |
6 | from . import config
7 | from . import constants
8 |
9 |
10 | # Validator function required before referencing:
11 | def valid_date(date_text):
12 | date_parser_format = '%Y-%m-%d'
13 | try:
14 | start_date, end_date = date_text.split(':')
15 | start_date_obj = dt.datetime.strptime(start_date, date_parser_format)
16 | end_date_obj = dt.datetime.strptime(end_date, date_parser_format)
17 | if start_date_obj > end_date_obj:
18 | raise argparse.ArgumentTypeError(
19 | 'StartDate(%s) should me smaller than EndDate(%s)' % (
20 | start_date, end_date))
21 | momentarily = dt.datetime.now()
22 | if start_date_obj > momentarily or end_date_obj > momentarily:
23 | raise argparse.ArgumentTypeError('Invalid date %s' % date_text)
24 | return start_date, end_date
25 | except ValueError:
26 | raise argparse.ArgumentTypeError(
27 | '%s is not a valid date format. Enter date in'
28 | ' YYYY-MM-DD:YYYY-MM-DD format' % date_text)
29 |
30 |
31 | # CLI_API_ASPECT = ( # Code is data is code is data is code is ...
32 | # {
33 | # 'action': 'an_argparse_action',
34 | # 'choices': 'argparse_choices',
35 | # 'const': 'an_argparse_const',
36 | # 'dest': 'an_argparse_dest',
37 | # 'help': ('an_argparse_help'),
38 | # 'metavar': 'an_argparse_metavar',
39 | # 'nargs': 'an_argparse_nargs',
40 | # 'tokens': ('-a', '--abstract'),
41 | # 'type': 'an_argparse_type',
42 | # },
43 | # ) # Above structures can be fed into argparse parser construction.
44 |
45 | '''
46 | CLI_API_ADVISORY_FORMAT = (
47 | {
48 | 'action': 'store_const',
49 | 'const': constants.CVRF_ADVISORY_FORMAT_TOKEN,
50 | 'dest': 'advisory_format',
51 | 'help': (
52 | 'Selects from cvrf advisories, required except for ios and ios_xe'
53 | ' query'),
54 | 'tokens': ('--cvrf',),
55 | },
56 | {
57 | 'action': 'store_const',
58 | 'const': constants.OVAL_ADVISORY_FORMAT_TOKEN,
59 | 'dest': 'advisory_format',
60 | 'help': (
61 | 'Selects from oval advisories, required except for ios and ios_xe'
62 | ' query'),
63 | 'tokens': ('--oval',),
64 | },
65 | )
66 | '''
67 |
68 | CLI_API_API_RESOURCE = (
69 | {
70 | 'action': 'store_const',
71 | 'const': ('all', 'all'),
72 | 'dest': 'api_resource',
73 | 'help': 'Retrieves all advisories',
74 | 'tokens': ('--all',),
75 | },
76 | {
77 | 'dest': 'api_resource',
78 | 'help': 'Retrieve advisories by advisory id',
79 | 'metavar': '',
80 | 'tokens': ('--advisory',),
81 | 'type': (lambda x: ('advisory', x)),
82 | },
83 | {
84 | 'dest': 'api_resource',
85 | 'help': 'Retrieve advisories by cve id',
86 | 'metavar': '',
87 | 'tokens': ('--cve',),
88 | 'type': (lambda x: ('cve', x)),
89 | },
90 | {
91 | 'dest': 'api_resource',
92 | 'help': 'Retrieve advisories by Cisco Bug id',
93 | 'metavar': '',
94 | 'tokens': ('--bugid',),
95 | 'type': (lambda x: ('bugid', x)),
96 | },
97 | {
98 | 'dest': 'api_resource',
99 | 'help': 'Retrieves latest (number) advisories',
100 | 'metavar': 'number',
101 | 'tokens': ('--latest',),
102 | 'type': (lambda x: ('latest', x)),
103 | },
104 | {
105 | 'dest': 'api_resource',
106 | 'help': (
107 | 'Retrieve advisories by severity (low, medium, high, critical)'),
108 | 'metavar': '[critical, high, medium, low]',
109 | 'tokens': ('--severity',),
110 | 'type': (lambda x: ('severity', x)),
111 | },
112 | {
113 | 'dest': 'api_resource',
114 | 'help': 'Retrieve advisories by year',
115 | 'metavar': 'year',
116 | 'tokens': ('--year',),
117 | 'type': (lambda x: ('year', x)),
118 | },
119 | {
120 | 'dest': 'api_resource',
121 | 'help': 'Retrieve advisories by product names',
122 | 'metavar': 'product_name',
123 | 'tokens': ('--product',),
124 | 'type': (lambda x: ('product', x)),
125 | },
126 | {
127 | 'dest': 'api_resource',
128 | 'help': (
129 | 'Retrieve advisories affecting user inputted ios_xe version. '
130 | 'Only one version at a time is allowed.'),
131 | 'metavar': 'iosxe_version',
132 | 'tokens': ('--ios_xe',),
133 | 'type': (lambda x: ('ios_xe', x)),
134 | },
135 | {
136 | 'dest': 'api_resource',
137 | 'help': (
138 | 'Retrieve advisories affecting user inputted ios version. '
139 | 'Only one version at a time is allowed.'),
140 | 'metavar': 'ios_version',
141 | 'tokens': ('--ios',),
142 | 'type': (lambda x: ('ios', x)),
143 | },
144 | {
145 | 'dest': 'api_resource',
146 | 'help': (
147 | 'Retrieve advisories affecting user inputted NX-OS (in standalone mode) version. '
148 | 'Only one version at a time is allowed.'),
149 | 'metavar': 'nxos_version',
150 | 'tokens': ('--nxos',),
151 | 'type': (lambda x: ('nxos', x)),
152 | },
153 | {
154 | 'dest': 'api_resource',
155 | 'help': (
156 | 'Retrieve advisories affecting user inputted NX-OS (in ACI mode) version. '
157 | 'Only one version at a time is allowed.'),
158 | 'metavar': 'aci_version',
159 | 'tokens': ('--aci',),
160 | 'type': (lambda x: ('aci', x)),
161 | },
162 | {
163 | 'dest': 'api_resource',
164 | 'help': (
165 | 'Retrieve advisories affecting user inputted ASA version. '
166 | 'Only one version at a time is allowed.'),
167 | 'metavar': 'asa_version',
168 | 'tokens': ('--asa',),
169 | 'type': (lambda x: ('asa', x)),
170 | },
171 | {
172 | 'dest': 'api_resource',
173 | 'help': (
174 | 'Retrieve advisories affecting user inputted FMC version. '
175 | 'Only one version at a time is allowed.'),
176 | 'metavar': 'fmc_version',
177 | 'tokens': ('--fmc',),
178 | 'type': (lambda x: ('fmc', x)),
179 | },
180 | {
181 | 'dest': 'api_resource',
182 | 'help': (
183 | 'Retrieve advisories affecting user inputted FTD version. '
184 | 'Only one version at a time is allowed.'),
185 | 'metavar': 'ftd_version',
186 | 'tokens': ('--ftd',),
187 | 'type': (lambda x: ('ftd', x)),
188 | },
189 | {
190 | 'dest': 'api_resource',
191 | 'help': (
192 | 'Retrieve advisories affecting user inputted FXOS version. '
193 | 'Only one version at a time is allowed.'),
194 | 'metavar': 'fxos_version',
195 | 'tokens': ('--fxos',),
196 | 'type': (lambda x: ('fxos', x)),
197 | },
198 | {
199 | 'dest': 'api_resource',
200 | 'help': (
201 | 'Retrieve version information regarding the different Network Operating Systems. '
202 | 'Only one Network Operating System at a time is allowed.'),
203 | 'metavar': 'OS_version',
204 | 'tokens': ('--OS',),
205 | 'type': (lambda x: ('OS', x)),
206 | },
207 | {
208 | 'dest': 'api_resource',
209 | 'help': (
210 | 'Retrieve platform alias information regarding the different Network Operating Systems. '
211 | 'Only one Network Operating System at a time is allowed.'),
212 | 'metavar': 'platform',
213 | 'tokens': ('--platform',),
214 | 'type': (lambda x: ('platform', x)),
215 | },
216 | )
217 |
218 | CLI_API_OUTPUT_FORMAT = (
219 | {
220 | 'dest': 'output_format',
221 | 'help': 'Output to CSV with file path',
222 | 'metavar': 'filepath',
223 | 'tokens': ('--csv',),
224 | 'type': (lambda x: (constants.CSV_OUTPUT_FORMAT_TOKEN, x)),
225 | },
226 | {
227 | 'dest': 'output_format',
228 | 'help': 'Output to JSON with file path',
229 | 'metavar': 'filepath',
230 | 'tokens': ('--json',),
231 | 'type': (lambda x: (constants.JSON_OUTPUT_FORMAT_TOKEN, x)),
232 | },
233 | )
234 |
235 | CLI_API_ADDITIONAL_FILTERS = (
236 | {
237 | 'dest': 'first_published',
238 | 'help': (
239 | 'Filter advisories based on first_published date'
240 | ' YYYY-MM-DD:YYYY-MM-DD USAGE: followed by severity or all'),
241 | 'metavar': 'YYYY-MM-DD:YYYY-MM-DD',
242 | 'tokens': ('--first_published',),
243 | 'type': valid_date,
244 | },
245 | {
246 | 'dest': 'last_published',
247 | 'help': (
248 | 'Filter advisories based on last_published date'
249 | ' YYYY-MM-DD:YYYY-MM-DD USAGE: followed by severity or all'),
250 | 'metavar': 'YYYY-MM-DD:YYYY-MM-DD',
251 | 'tokens': ('--last_published', '--last_updated'),
252 | 'type': valid_date,
253 | },
254 | )
255 |
256 | CLI_API_PARSER_GENERIC = (
257 | {
258 | 'action': 'store_true',
259 | 'dest': 'count', # TODO made destination explicit, verify that OK
260 | 'help': 'Count of any field or fields',
261 | 'tokens': ('-c', '--count'), # TODO reversed order, verify that OK
262 | },
263 | {
264 | 'choices': constants.API_LABELS + constants.IPS_SIGNATURES,
265 | 'dest': 'fields',
266 | 'help': ('Separate fields by spaces to return advisory information.'
267 | ' Allowed values are: %s' % ', '.join(constants.API_LABELS)),
268 | 'metavar': '',
269 | 'nargs': '+',
270 | 'tokens': ('-f', '--fields'), # TODO reversed order, verify that OK
271 | },
272 | {
273 | 'dest': 'user_agent',
274 | 'help': 'Announced User-Agent headar value (towards service)',
275 | 'metavar': 'string',
276 | 'tokens': ('--user-agent',),
277 | },
278 | {
279 | 'choices': constants.SUPPORTED_PLATFORMS_ALIAS_NAME_ASA + constants.SUPPORTED_PLATFORMS_ALIAS_NAME_FTD +
280 | constants.SUPPORTED_PLATFORMS_ALIAS_NAME_FXOS + constants.SUPPORTED_PLATFORMS_ALIAS_NAME_NXOS,
281 | 'dest': 'platformAlias',
282 | 'help': ('Single platform alias. '
283 | ' Supported only for: %s' % ', '.join(constants.SUPPORTED_PLATFORMS_ALIAS)),
284 | 'metavar': '',
285 | 'nargs': '+',
286 | 'tokens': ('-pa', '--platformAlias'),
287 | },
288 | )
289 |
290 | CLI_API_CONFIG = (
291 | {
292 | 'dest': 'json_config_path',
293 | 'help': ('Path to JSON file with config (otherwise fallback to'
294 | ' environment variables CLIENT_ID and CLIENT_SECRET, or'
295 | ' config.py variables, or fail)'),
296 | 'metavar': 'filepath',
297 | 'tokens': ('--config',),
298 | },
299 | )
300 |
301 |
302 | def add_options_to_parser(option_parser, options_add_map):
303 | """Centralized default option provider for parser (dialect optparse).
304 |
305 | :param option_parser: An instance of argparse.ArgumentParser (for now)
306 | which will be enriched (cf. option_add_map) and returned.
307 | :param options_add_map: A sequence of dicts with the latter providing per
308 | option at least:
309 | - 'tokens' the seq of strings providing the option,
310 | - 'dest' providing the target variable/member string name, and
311 | - 'help' having a string value.
312 | :return the parser object (Note: Mutates object anyhow => for DRY 'nuff)
313 | """
314 |
315 | if not isinstance(option_parser, argparse.ArgumentParser):
316 | if not getattr(option_parser, '__module__', None) == argparse.__name__:
317 | # Danse to avoid refering argparse._MutuallyExclusiveGroup ...
318 | raise NotImplementedError(
319 | "Please provide an argparse.ArgumentParser instance or an"
320 | " object generated by argparse.ArgumentParser().add_mutually_"
321 | "exclusive_group(), received %s instead"
322 | "" % (type(option_parser),))
323 |
324 | for options in options_add_map:
325 | tokens = options['tokens']
326 | option_cfg = {k: v for k, v in options.items() if k != 'tokens'}
327 | option_parser.add_argument(*tokens, **option_cfg)
328 | return option_parser
329 |
330 |
331 | def parser_factory():
332 | """Knit CLI API together and produce an argparse based parser."""
333 | p = argparse.ArgumentParser(
334 | prog='openVulnQuery',
335 | description='Cisco OpenVuln API Command Line Interface')
336 | p.set_defaults(output_format=(constants.JSON_OUTPUT_FORMAT_TOKEN, None))
337 |
338 |
339 | add_options_to_parser(
340 | p.add_mutually_exclusive_group(required=True), CLI_API_API_RESOURCE)
341 |
342 | add_options_to_parser(
343 | p.add_mutually_exclusive_group(), CLI_API_OUTPUT_FORMAT)
344 |
345 | add_options_to_parser(
346 | p.add_mutually_exclusive_group(), CLI_API_ADDITIONAL_FILTERS)
347 |
348 | add_options_to_parser(p, CLI_API_PARSER_GENERIC)
349 |
350 | add_options_to_parser(
351 | p.add_mutually_exclusive_group(required=False), CLI_API_CONFIG)
352 |
353 | return p
354 |
355 |
356 | def process_command_line(string_list=None):
357 | """Interpret parameters given in command line."""
358 |
359 | parser = parser_factory()
360 |
361 | args = parser.parse_args(args=string_list)
362 |
363 | if args.api_resource[0] not in constants.ALLOWS_FILTER:
364 | if args.first_published or args.last_published:
365 | parser.error(
366 | 'Only {} based filter can have additional first_published or'
367 | ' last_published filter'.format(constants.ALLOWS_FILTER))
368 |
369 | if args.api_resource[0] not in constants.SUPPORTED_PLATFORMS_ALIAS:
370 | if args.platformAlias:
371 | parser.error(
372 | 'Only {} based filter can have additional platformAlias filter'
373 | .format(constants.SUPPORTED_PLATFORMS_ALIAS))
374 |
375 | if args.api_resource[0] in constants.NON_ADVISORY_QUERY:
376 | if args.count or args.fields:
377 | parser.error(
378 | '{} do not support fields or count options'.format(constants.NON_ADVISORY_QUERY))
379 |
380 | if args.api_resource[0] == "OS":
381 | if args.api_resource[1] not in constants.SUPPORTED_PLATFORMS_VERSION:
382 | parser.error(
383 | 'Only network operating system types for OS type query are: {}'.format(constants.SUPPORTED_PLATFORMS_VERSION))
384 | elif args.api_resource[0] == "platform":
385 | if args.api_resource[1] not in constants.SUPPORTED_PLATFORMS_ALIAS:
386 | parser.error(
387 | 'Only network operating system types for platform type query are: {}'.format(constants.SUPPORTED_PLATFORMS_ALIAS))
388 |
389 | if not args.json_config_path:
390 | # Try next environment variables are set, then config.py, or fail:
391 | keys_required = ('CLIENT_ID', 'CLIENT_SECRET')
392 | env_config = {k: os.getenv(k, None) for k in keys_required}
393 |
394 | if all([v for v in env_config.values()]): # OK, take env values:
395 | for key in keys_required:
396 | setattr(config, key, env_config[key])
397 | elif all([getattr(config, k, None) for k in keys_required]):
398 | pass # Fallback to the credentials in config.py (non-empty!)
399 | else:
400 | parser.error(
401 | ' --conf ? Missing configuration file (credentials)')
402 | else:
403 | if not os.path.isfile(args.json_config_path):
404 | parser.error(
405 | 'Configuration file not found at %s' % args.json_config_path)
406 | else:
407 | parsed_config = json.load(open(args.json_config_path))
408 | keys_required = ('CLIENT_ID', 'CLIENT_SECRET')
409 | for key in keys_required:
410 | setattr(config, key, parsed_config[key])
411 | keys_optional = ('REQUEST_TOKEN_URL', 'API_URL')
412 | for key in keys_optional:
413 | if key in parsed_config:
414 | setattr(config, key, parsed_config[key])
415 |
416 | return args
417 |
--------------------------------------------------------------------------------
/tests/test_query_client.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import mock
3 | import requests
4 | import json
5 | from openVulnQuery import query_client
6 | from openVulnQuery import constants
7 | from openVulnQuery import config
8 | from openVulnQuery import advisory
9 |
10 | NA = constants.NA_INDICATOR
11 | IPS_SIG = constants.IPS_SIGNATURE_LABEL
12 | mock_advisory_title = "Mock Advisory Title"
13 | mock_response = {
14 | 'advisoryId': "Cisco-SA-20111107-CVE-2011-0941",
15 | 'sir': "Medium",
16 | 'firstPublished': "2011-11-07T21:36:55+0000",
17 | 'lastUpdated': "2011-11-07T21:36:55+0000",
18 | 'cves': ["CVE-2011-0941", NA],
19 | 'cvrfUrl': (
20 | "http://tools.cisco.com/security/center/contentxml/"
21 | "CiscoSecurityAdvisory/Cisco-SA-20111107-CVE-2011-0941/cvrf/"
22 | "Cisco-SA-20111107-CVE-2011-0941_cvrf.xml"),
23 | 'bugIDs': "BUGISidf",
24 | 'cvssBaseScore': "7.0",
25 | 'advisoryTitle': "{}".format(mock_advisory_title),
26 | 'publicationUrl': "https://tools.cisco.com/mockurl",
27 | 'cwe': NA,
28 | 'productNames': ["product_name_1", "product_name_2"],
29 | 'summary': "This is summary",
30 | 'ipsSignatures': NA,
31 | }
32 |
33 | SAMPLE_CVE = "CVE-2011-0941"
34 | SAMPLE_PRODUCT = "Cisco Unified Communications Manager (CallManager)"
35 | response_sample_cve = """\
36 | {
37 | "advisories": [
38 | {
39 | "cvrfUrl": "https://tools.cisco.com/security/center/contentxml/\
40 | CiscoSecurityAdvisory/Cisco-SA-20111107-$CVE$/cvrf/Cisco-SA-20111107\
41 | -$CVE$_cvrf.xml",
42 | "bugIDs": [
43 | "CSCtj09179"
44 | ],
45 | "advisoryTitle": "Cisco IOS Software and Cisco Unified Communications\
46 | Manager Session Initiation Protocol Packet Processing Memory Leak\
47 | Vulnerability",
48 | "sir": "Medium",
49 | "firstPublished": "2011-11-07T16:36:55-0600",
50 | "lastUpdated": "2011-11-07T16:36:55-0600",
51 | "publicationUrl": "http://tools.cisco.com/security/center/content/\
52 | CiscoSecurityAdvisory/Cisco-SA-20111107-%(cve)s",
53 | "cvssBaseScore": "7.8",
54 | "ipsSignatures": [
55 | "NA"
56 | ],
57 | "productNames": [
58 | "Cisco Unified Communications Manager (CallManager)",
59 | "Cisco IOS Software Releases 12.4 T",
60 | "Cisco IOS Software Release 12.4(2)T",
61 | "Cisco IOS Software Release 12.4(4)T",
62 | "Cisco IOS Software Release 12.4(6)T",
63 | "Cisco IOS Software Release 12.4(9)T",
64 | "Cisco IOS Software Release 12.4(11)T",
65 | "Cisco IOS Software Release 12.4(15)T",
66 | "Cisco IOS Software Release 12.4(20)T",
67 | "Cisco IOS Software Release 12.4(22)T",
68 | "Cisco Unified Communications Manager Version 7.1",
69 | "Cisco IOS Software Release 12.4(24)T",
70 | "Cisco IOS 15.1M&T",
71 | "Cisco IOS Software Release 15.1(1)T",
72 | "Cisco Unified Communications Manager Version 8.0",
73 | "Cisco IOS Software Release 15.1(2)T",
74 | "Cisco Unified Communications Manager Version 8.5",
75 | "Cisco IOS 15.1S",
76 | "Cisco IOS Software Release 15.1(3)T",
77 | "Cisco IOS Software Release 15.1(4)M",
78 | "Cisco IOS Software Release 15.1(1)S",
79 | "Cisco IOS Software Release 15.1(2)S",
80 | "Cisco IOS Software Release 15.1(3)S"
81 | ],
82 | "advisoryId": "Cisco-SA-20111107-$CVE$",
83 | "summary": "
Cisco IOS Software and Cisco Unified Communications\
84 | Manager contain a vulnerability that could allow an unauthenticated,\
85 | remote attacker to cause a denial of service (DoS) condition.
86 |
The vulnerability is due to improper processing of malformed packets by\
87 | the affected software. An unauthenticated, remote attacker could\
88 | exploit this vulnerability by sending malicious network requests to the\
89 | targeted system. If successful, the attacker could cause the device\
90 | to become unresponsive, resulting in a DoS condition.
91 |
Cisco confirmed this vulnerability and released software updates.
92 |
93 |
94 |
To exploit the vulnerability, an attacker must send malicious SIP packets\
95 | to affected systems. Most environments restrict external connections\
96 | using SIP, likely requiring an attacker to have access to internal networks\
97 | prior to an attack. In addition, in environments that separate voice\
98 | and data networks, attackers may have no access to networks that service\
99 | voice traffic and allow the transmission of SIP packets, further increasing\
100 | the difficulty of an exploit.
101 |
Cisco indicates through the CVSS score\
102 | that functional exploit code exists; however, the code is not known to be\
103 | publicly available.
",
104 | "cwe": [
105 | "CWE-399"
106 | ],
107 | "cves": [
108 | "$CVE$"
109 | ]
110 | }
111 | ]
112 | }
113 | """.replace('\\', '').replace('\n', '')
114 | API_RESPONSE_CVE_OK = json.loads(
115 | response_sample_cve.replace('$CVE$', SAMPLE_CVE))
116 | CVES_EXPECTED = API_RESPONSE_CVE_OK['advisories'][0]['cves']
117 |
118 | CLIENT_ID = 'BadCodedBadCodedBadCoded'
119 | CLIENT_SECRET = 'DeadFaceDeadFaceDeadFace'
120 | REQUEST_TOKEN_URL = config.REQUEST_TOKEN_URL
121 | API_URL = config.API_URL
122 |
123 | OK_STATUS = 200
124 |
125 | NOT_FOUND_STATUS = 404
126 | NOT_FOUND_REASON = 'Not Found'
127 |
128 | NOT_FOUND_TOK_URL = "https://cloudsso.cisco.com/as/token.oauth2.404"
129 | NOT_FOUND_TOK_URL_FULL = (
130 | "{}?client_secret={}&client_id={}"
131 | "".format(NOT_FOUND_TOK_URL, CLIENT_SECRET, CLIENT_ID))
132 | TOK_HTTP_ERROR_MSG = '{} Client Error: {} for url: {}'.format(
133 | NOT_FOUND_STATUS, NOT_FOUND_REASON, NOT_FOUND_TOK_URL_FULL)
134 |
135 | OAUTH2_RESPONSE_BOGUS_BUT_OK = {
136 | "access_token": "FeedFaceBadCodedDeadBeadFeed",
137 | "token_type": "Bearer",
138 | "expires_in": 3599
139 | }
140 |
141 | NOT_FOUND_API_URL = "https://api.cisco.com/security/advisories.404"
142 | NOT_FOUND_API_URL_FULL = (
143 | "{}?client_secret={}&client_id={}"
144 | "".format(NOT_FOUND_API_URL, CLIENT_SECRET, CLIENT_ID))
145 | API_HTTP_ERROR_MSG = '{} Client Error: {} for url: {}'.format(
146 | NOT_FOUND_STATUS, NOT_FOUND_REASON, NOT_FOUND_API_URL_FULL)
147 |
148 | API_RESPONSE_BOGUS_BUT_OK = mock_response
149 |
150 |
151 | def mocked_req_post(*args, **kwargs):
152 | class MockResponse:
153 | def __init__(self, url=None, json_in=None, status_code=None):
154 | self.url = url
155 | self.json_in = json_in
156 | self.status_code = status_code
157 |
158 | def json(self):
159 | return self.json_in
160 |
161 | def raise_for_status(self):
162 | if self.status_code != OK_STATUS:
163 | raise requests.exceptions.HTTPError(TOK_HTTP_ERROR_MSG)
164 | url = args[0]
165 | if url == REQUEST_TOKEN_URL:
166 | return MockResponse(url, OAUTH2_RESPONSE_BOGUS_BUT_OK, 200)
167 | return MockResponse(url, None, NOT_FOUND_STATUS).raise_for_status()
168 |
169 |
170 | def mocked_req_get(*args, **kwargs):
171 | class MockResponse:
172 | def __init__(self, url=None, json_in=None, status_code=None):
173 | self.url = url
174 | self.json_in = json_in
175 | self.status_code = status_code
176 |
177 | def json(self):
178 | return self.json_in
179 |
180 | def raise_for_status(self):
181 | if self.status_code != OK_STATUS:
182 | raise requests.exceptions.HTTPError(API_HTTP_ERROR_MSG)
183 | url = kwargs['url']
184 | if url.startswith('{}/cvrf/all'.format(API_URL)):
185 | return MockResponse(url, API_RESPONSE_CVE_OK, 200)
186 | elif url.startswith('{}/cvrf/cve'.format(API_URL)):
187 | return MockResponse(url, API_RESPONSE_CVE_OK, 200)
188 | elif url.startswith('{}/cvrf/product'.format(API_URL)):
189 | return MockResponse(url, API_RESPONSE_CVE_OK, 200)
190 | elif url.startswith('{}/cvrf/severity'.format(API_URL)):
191 | return MockResponse(url, API_RESPONSE_CVE_OK, 200)
192 | elif url.startswith('{}/cvrf/year'.format(API_URL)):
193 | return MockResponse(url, API_RESPONSE_CVE_OK, 200)
194 | elif url.startswith(API_URL):
195 | return MockResponse(url, API_RESPONSE_BOGUS_BUT_OK, 200)
196 | return MockResponse(url, None, NOT_FOUND_STATUS).raise_for_status()
197 |
198 |
199 | class QueryClientTest(unittest.TestCase):
200 | def test_query_client_unchanged_adv_tokens(self):
201 | self.assertEquals(query_client.ADV_TOKENS,
202 | constants.ADVISORY_FORMAT_TOKENS)
203 |
204 | def test_query_client_unchanged_temporal_filter_keys(self):
205 | self.assertTrue(len(query_client.TEMPORAL_FILTER_KEYS) == 2)
206 |
207 | def test_query_client_ensure_adv_format_token_succeeds(self):
208 | self.assertTrue(query_client.ensure_adv_format_token(''))
209 |
210 | def test_query_client_filter_succeeds(self):
211 | self.assertTrue(query_client.Filter())
212 |
213 | def test_query_client_temporal_filter_succeeds(self):
214 | self.assertTrue(query_client.TemporalFilter('', *('',) * 2))
215 |
216 | def test_query_client_first_published_succeeds(self):
217 | self.assertTrue(query_client.FirstPublished(*('',) * 2))
218 |
219 | def test_query_client_last_updated_succeeds(self):
220 | self.assertTrue(query_client.LastUpdated(*('',) * 2))
221 |
222 | @mock.patch('openVulnQuery.authorization.requests.post',
223 | side_effect=mocked_req_post)
224 | def test_client_smoke_init_succeeds_mocked(self, mock_post):
225 | client = query_client.OpenVulnQueryClient(
226 | CLIENT_ID, CLIENT_SECRET, REQUEST_TOKEN_URL, user_agent='foo')
227 | self.assertIsInstance(client, query_client.OpenVulnQueryClient)
228 | self.assertEqual(
229 | client.auth_token, OAUTH2_RESPONSE_BOGUS_BUT_OK['access_token'])
230 | self.assertEqual(
231 | client.auth_url, REQUEST_TOKEN_URL)
232 |
233 | @mock.patch('openVulnQuery.query_client.requests.get',
234 | side_effect=mocked_req_get)
235 | @mock.patch('openVulnQuery.authorization.requests.post',
236 | side_effect=mocked_req_post)
237 | def test_client_smoke_fails_non_topic_mocked(self, mock_post, mock_get):
238 | client = query_client.OpenVulnQueryClient(
239 | CLIENT_ID, CLIENT_SECRET, REQUEST_TOKEN_URL, user_agent='foo')
240 | self.assertRaises(
241 | KeyError,
242 | client.get_by,
243 | 'let_this_topic_be_non_existing',
244 | None,
245 | None,
246 | **{})
247 |
248 | @mock.patch('openVulnQuery.query_client.requests.get',
249 | side_effect=mocked_req_get)
250 | @mock.patch('openVulnQuery.authorization.requests.post',
251 | side_effect=mocked_req_post)
252 | def test_client_cvrf_succeeds_advisory_mocked(self, mock_post, mock_get):
253 | client = query_client.OpenVulnQueryClient(
254 | CLIENT_ID, CLIENT_SECRET, REQUEST_TOKEN_URL, user_agent='foo')
255 | advisories_as_cvrf = client.get_by(
256 | 'advisory',
257 | format=constants.CVRF_ADVISORY_FORMAT_TOKEN,
258 | aspect='non_existing_aspect_on_server',
259 | **{'a_filter': None})
260 | self.assertTrue(len(advisories_as_cvrf) == 1)
261 | cvrf_first = advisories_as_cvrf[0]
262 | self.assertIsInstance(cvrf_first, advisory.CVRF)
263 | self.assertEqual(cvrf_first.advisory_id, mock_response['advisoryId'])
264 |
265 | @mock.patch('openVulnQuery.query_client.requests.get',
266 | side_effect=mocked_req_get)
267 | @mock.patch('openVulnQuery.authorization.requests.post',
268 | side_effect=mocked_req_post)
269 | def test_client_cvrf_succeeds_cve_mocked(self, mock_post, mock_get):
270 | client = query_client.OpenVulnQueryClient(
271 | CLIENT_ID, CLIENT_SECRET, REQUEST_TOKEN_URL, user_agent='foo')
272 | advisories_as_cvrf = client.get_by(
273 | 'cve',
274 | format=constants.CVRF_ADVISORY_FORMAT_TOKEN,
275 | aspect=SAMPLE_CVE,
276 | **{'a_filter': None})
277 | self.assertTrue(len(advisories_as_cvrf) == 1)
278 | cvrf_first = advisories_as_cvrf[0]
279 | self.assertIsInstance(cvrf_first, advisory.CVRF)
280 | self.assertEqual(cvrf_first.cves, CVES_EXPECTED)
281 |
282 | @mock.patch('openVulnQuery.query_client.requests.get',
283 | side_effect=mocked_req_get)
284 | @mock.patch('openVulnQuery.authorization.requests.post',
285 | side_effect=mocked_req_post)
286 | def test_client_cvrf_succeeds_product_mocked(self, mock_post, mock_get):
287 | client = query_client.OpenVulnQueryClient(
288 | CLIENT_ID, CLIENT_SECRET, REQUEST_TOKEN_URL, user_agent='foo')
289 | advisories_as_cvrf = client.get_by(
290 | 'product',
291 | format=constants.CVRF_ADVISORY_FORMAT_TOKEN,
292 | aspect=SAMPLE_PRODUCT,
293 | **{'a_filter': None})
294 | self.assertTrue(len(advisories_as_cvrf) == 1)
295 | cvrf_first = advisories_as_cvrf[0]
296 | self.assertIsInstance(cvrf_first, advisory.CVRF)
297 | self.assertIn(SAMPLE_PRODUCT, cvrf_first.product_names)
298 |
299 | @mock.patch('openVulnQuery.query_client.requests.get',
300 | side_effect=mocked_req_get)
301 | @mock.patch('openVulnQuery.authorization.requests.post',
302 | side_effect=mocked_req_post)
303 | def test_client_cvrf_succeeds_year_mocked(self, mock_post, mock_get):
304 | client = query_client.OpenVulnQueryClient(
305 | CLIENT_ID, CLIENT_SECRET, REQUEST_TOKEN_URL, user_agent='foo')
306 | sample_year = 2017
307 | advisories_as_cvrf = client.get_by(
308 | 'year',
309 | format=constants.CVRF_ADVISORY_FORMAT_TOKEN,
310 | aspect=sample_year,
311 | **{'a_filter': None})
312 | self.assertTrue(len(advisories_as_cvrf) == 1)
313 | cvrf_first = advisories_as_cvrf[0]
314 | self.assertIsInstance(cvrf_first, advisory.CVRF)
315 |
316 | @mock.patch('openVulnQuery.query_client.requests.get',
317 | side_effect=mocked_req_get)
318 | @mock.patch('openVulnQuery.authorization.requests.post',
319 | side_effect=mocked_req_post)
320 | def test_client_cvrf_succeeds_severity_mocked(self, mock_post, mock_get):
321 | client = query_client.OpenVulnQueryClient(
322 | CLIENT_ID, CLIENT_SECRET, REQUEST_TOKEN_URL, user_agent='foo')
323 | sample_severity = 'high'
324 | advisories_as_cvrf = client.get_by(
325 | 'severity',
326 | format=constants.CVRF_ADVISORY_FORMAT_TOKEN,
327 | aspect=sample_severity,
328 | a_filter=query_client.Filter()
329 | )
330 | self.assertTrue(len(advisories_as_cvrf) == 1)
331 | cvrf_first = advisories_as_cvrf[0]
332 | self.assertIsInstance(cvrf_first, advisory.CVRF)
333 |
334 | @mock.patch('openVulnQuery.query_client.requests.get',
335 | side_effect=mocked_req_get)
336 | @mock.patch('openVulnQuery.authorization.requests.post',
337 | side_effect=mocked_req_post)
338 | def test_client_cvrf_all_succeeds_mocked(self, mock_post, mock_get):
339 | client = query_client.OpenVulnQueryClient(
340 | CLIENT_ID, CLIENT_SECRET, REQUEST_TOKEN_URL, user_agent='foo')
341 | sample_all = 'all'
342 | advisories_as_cvrf = client.get_by(
343 | 'all',
344 | format=constants.CVRF_ADVISORY_FORMAT_TOKEN,
345 | aspect=sample_all,
346 | a_filter=query_client.Filter()
347 | )
348 | self.assertTrue(len(advisories_as_cvrf) == 1)
349 | cvrf_first = advisories_as_cvrf[0]
350 | self.assertIsInstance(cvrf_first, advisory.CVRF)
351 |
352 | @mock.patch('openVulnQuery.authorization.requests.post',
353 | side_effect=mocked_req_post)
354 | def test_client_smoke_not_found_tok_url_raises_mocked(self, mock_post):
355 | self.assertRaises(
356 | requests.exceptions.HTTPError,
357 | query_client.OpenVulnQueryClient,
358 | CLIENT_ID, CLIENT_SECRET, NOT_FOUND_TOK_URL, user_agent='foo')
359 |
360 | # @mock.patch('openVulnQuery.authorization.requests.post',
361 | # side_effect=mocked_req_post)
362 | # def test_authorization_smoke_raises_details_mocked(self, mock_post):
363 | # with self.assertRaises(requests.exceptions.HTTPError) as e:
364 | # query_client.get_oauth_token(
365 | # CLIENT_ID, CLIENT_SECRET, NOT_FOUND_TOK_URL)
366 | # self.assertEqual(str(e.exception), TOK_HTTP_ERROR_MSG)
367 |
--------------------------------------------------------------------------------
/openVulnQuery/_library/query_client.py:
--------------------------------------------------------------------------------
1 | import datetime as dt
2 | import json
3 | import logging
4 | import os
5 | import uuid
6 |
7 | import requests
8 |
9 | from . import advisory
10 | from . import authorization
11 | from . import config
12 | from . import constants
13 | from . import rest_api
14 |
15 | ADV_TOKENS = constants.ADVISORY_FORMAT_TOKENS
16 |
17 | TEMPORAL_FILTER_KEYS = ('startDate', 'endDate')
18 | PUBLISHED_FIRST = 'firstpublished'
19 | PUBLISHED_LAST = 'lastpublished'
20 | PLATFORMALIAS = 'platformAlias'
21 |
22 | TEMPORAL_PUBLICATION_ASPECTS = (PUBLISHED_FIRST, PUBLISHED_LAST)
23 |
24 | DEBUG_API_USAGE = os.getenv('CISCO_OPEN_VULN_API_DEBUG', None)
25 | DEBUG_API_PATH = os.getenv('CISCO_OPEN_VULN_API_PATH', None)
26 | DEBUG_TIME_STAMP_FORMAT = "%Y%m%dT%H%M%S.%f"
27 |
28 | def ensure_adv_format_token(adv_format):
29 | return adv_format if adv_format in ADV_TOKENS else ADV_TOKENS[-1]
30 |
31 |
32 | class Filter(object):
33 | def __init__(self, path='', params=None):
34 | self.path = path
35 | self.params = params
36 |
37 |
38 | class TemporalFilter(object):
39 | def __init__(self, path, *args):
40 | self.path = path # Better be in TEMPORAL_PUBLICATION_ASPECTS ...
41 | self.params = dict(zip(TEMPORAL_FILTER_KEYS, args))
42 |
43 | class OptionalParameters(object):
44 | def __init__(self, parameter_name='', *args):
45 | self.parameter_name = parameter_name
46 | self.parameter_value = args
47 |
48 |
49 | class FirstPublished(TemporalFilter):
50 | def __init__(self, *args):
51 | super(FirstPublished, self).__init__(PUBLISHED_FIRST, *args)
52 |
53 |
54 | class LastUpdated(TemporalFilter):
55 | def __init__(self, *args):
56 | super(LastUpdated, self).__init__(PUBLISHED_LAST, *args)
57 |
58 |
59 | class PlatformAlias(OptionalParameters):
60 | def __init__(self, *args):
61 | super(PlatformAlias, self).__init__(PLATFORMALIAS, *args)
62 |
63 | class OpenVulnQueryClient(object):
64 | """Client sends get request for advisory information from OpenVuln API.
65 |
66 | :var auth_token: OAuth2 Token for API authorization.
67 | :var headers: Headers containing OAuth2 Token and data type for
68 | request.
69 | """
70 |
71 | def __init__(self, client_id, client_secret, auth_url=None,
72 | user_agent='TestApp'):
73 | """
74 | :param client_id: Client application Id as retrieved from API provider
75 | :param client_secret: Client secret as retrieved from API provider
76 | :param auth_url: POST URL to request auth token response (default
77 | from config)
78 | :param user_agent: Communicates the name of the app per request.
79 |
80 | """
81 | logging.basicConfig(level=logging.WARNING)
82 | self.logger = logging.getLogger(__name__)
83 | self.auth_url = auth_url if auth_url else config.REQUEST_TOKEN_URL
84 | self.auth_token = authorization.get_oauth_token(
85 | client_id, client_secret, request_token_url=self.auth_url)
86 | self.headers = rest_api.rest_with_auth_headers(
87 | self.auth_token, user_agent)
88 |
89 | def get_by_all(self, adv_format, all_adv, a_filter=None):
90 | """Return all the advisories using requested advisory format"""
91 | req_cfg = {
92 | 'filter': a_filter.path,
93 | }
94 | req_path = "all/{filter}".format(**req_cfg)
95 | advisories = self.get_request(req_path, a_filter.params)
96 | return self.advisory_list(advisories['advisories'], adv_format)
97 |
98 | def get_by_cve(self, adv_format, cve_id, a_filter=None):
99 | """Return the advisory using requested cve id"""
100 | req_cfg = {
101 | 'cve_id': cve_id,
102 | }
103 | req_path = "cve/{cve_id}".format(**req_cfg)
104 | advisories = self.get_request(req_path)
105 | return self.advisory_list(advisories['advisories'], adv_format)
106 |
107 | def get_by_bugid(self, adv_format, bug_id, a_filter=None):
108 | """Return the advisory using requested cve id"""
109 | req_cfg = {
110 | 'bug_id': bug_id,
111 | }
112 | req_path = "bugid/{bug_id}".format(**req_cfg)
113 | advisories = self.get_request(req_path)
114 | return self.advisory_list(advisories['advisories'], adv_format)
115 |
116 | def get_by_advisory(self, adv_format, an_advisory, a_filter=None):
117 | """Return the advisory using requested advisory id"""
118 | req_cfg = {
119 | 'advisory': an_advisory,
120 | }
121 | req_path = "advisory/{advisory}".format(**req_cfg)
122 | advisories = self.get_request(req_path)
123 | return self.advisory_list(advisories['advisories'], adv_format)
124 |
125 | def get_by_severity(self, adv_format, severity, a_filter=None):
126 | """Return the advisories using requested severity"""
127 | req_cfg = {
128 | 'severity': severity,
129 | 'filter': Filter().path if a_filter is None else a_filter.path,
130 | }
131 | req_path = ("severity/{severity}/{filter}"
132 | "".format(**req_cfg))
133 | advisories = self.get_request(req_path, params=a_filter.params)
134 | return self.advisory_list(advisories['advisories'], adv_format)
135 |
136 | def get_by_year(self, adv_format, year, a_filter=None):
137 | """Return the advisories using requested year"""
138 | req_cfg = {
139 | 'year': year,
140 | }
141 | req_path = "year/{year}".format(**req_cfg)
142 | advisories = self.get_request(req_path)
143 | return self.advisory_list(advisories['advisories'], adv_format)
144 |
145 | def get_by_latest(self, adv_format, latest, a_filter=None):
146 | """Return the advisories using requested latest"""
147 | req_cfg = {
148 | 'latest': latest,
149 | }
150 | req_path = "latest/{latest}".format(**req_cfg)
151 | advisories = self.get_request(req_path)
152 | return self.advisory_list(advisories['advisories'], adv_format)
153 |
154 | def get_by_product(self, adv_format, product_name, a_filter=None):
155 | """Return advisories by product name"""
156 |
157 | '''
158 | TODO: It was discovered that the endpoint url in the documentation
159 | is incorrect. get_by_product should work AFTER the endpoint url path
160 | is properly edited to match the documentation; that is, to /security/advisories/product
161 | instead of the old /cvrf /oval urls. This will be done in December 2018.
162 | '''
163 | req_path = "product"
164 | advisories = self.get_request(
165 | req_path, params={'product': product_name})
166 | return self.advisory_list(advisories['advisories'], adv_format)
167 |
168 | def get_by_ios_xe(self, adv_format, ios_version, a_filter=None):
169 | """Return advisories by Cisco IOS advisories version"""
170 | req_path = "OSType/iosxe"
171 | try:
172 | advisories = self.get_request(
173 | req_path,
174 | params={'version': ios_version})
175 | return self.advisory_list(advisories['advisories'], adv_format)
176 | except requests.exceptions.HTTPError as e:
177 | raise requests.exceptions.HTTPError(
178 | e.response.status_code, e.response.text)
179 |
180 | def get_by_ios(self, adv_format, ios_version, a_filter=None):
181 | """Return advisories by Cisco IOS advisories version"""
182 | req_path = "OSType/ios"
183 | try:
184 | advisories = self.get_request(
185 | req_path,
186 | params={'version': ios_version})
187 | return self.advisory_list(advisories['advisories'], adv_format)
188 | except requests.exceptions.HTTPError as e:
189 | raise requests.exceptions.HTTPError(
190 | e.response.status_code, e.response.text)
191 |
192 | def get_by_nxos(self, adv_format, nxos_version, a_filter=None):
193 | """Return advisories by Cisco NX-OS (standalone mode) advisories version"""
194 | req_path = "OSType/nxos"
195 | try:
196 | req_cfg = {
197 | 'version': nxos_version,
198 | 'platformAlias': '' if a_filter is None else a_filter.parameter_value,
199 | }
200 | advisories = self.get_request(req_path, req_cfg)
201 | return self.advisory_list(advisories['advisories'], adv_format)
202 | except requests.exceptions.HTTPError as e:
203 | raise requests.exceptions.HTTPError(
204 | e.response.status_code, e.response.text)
205 |
206 | def get_by_aci(self, adv_format, aci_version, a_filter=None):
207 | """Return advisories by Cisco NX-OS (in ACI mode) advisories version"""
208 | req_path = "OSType/aci"
209 | try:
210 | advisories = self.get_request(
211 | req_path,
212 | params={'version': aci_version})
213 | return self.advisory_list(advisories['advisories'], adv_format)
214 | except requests.exceptions.HTTPError as e:
215 | raise requests.exceptions.HTTPError(
216 | e.response.status_code, e.response.text)
217 |
218 |
219 | def get_by_asa(self, adv_format, asa_version, a_filter=None):
220 | """Return advisories by Cisco ASA advisories version"""
221 | req_path = "OSType/asa"
222 | try:
223 | req_cfg = {
224 | 'version': asa_version,
225 | 'platformAlias': '' if a_filter is None else a_filter.parameter_value,
226 | }
227 | advisories = self.get_request(req_path, req_cfg)
228 | return self.advisory_list(advisories['advisories'], adv_format)
229 | except requests.exceptions.HTTPError as e:
230 | raise requests.exceptions.HTTPError(
231 | e.response.status_code, e.response.text)
232 |
233 | def get_by_fmc(self, adv_format, fmc_version, a_filter=None):
234 | """Return advisories by Cisco FMC advisories version"""
235 | req_path = "OSType/fmc"
236 | try:
237 | advisories = self.get_request(
238 | req_path,
239 | params={'version': fmc_version})
240 | return self.advisory_list(advisories['advisories'], adv_format)
241 | except requests.exceptions.HTTPError as e:
242 | raise requests.exceptions.HTTPError(
243 | e.response.status_code, e.response.text)
244 |
245 | def get_by_ftd(self, adv_format, ftd_version, a_filter=None):
246 | """Return advisories by Cisco FTD advisories version"""
247 | req_path = "OSType/ftd"
248 | try:
249 | req_cfg = {
250 | 'version': ftd_version,
251 | 'platformAlias': '' if a_filter is None else a_filter.parameter_value,
252 | }
253 | advisories = self.get_request(req_path, req_cfg)
254 | return self.advisory_list(advisories['advisories'], adv_format)
255 | except requests.exceptions.HTTPError as e:
256 | raise requests.exceptions.HTTPError(
257 | e.response.status_code, e.response.text)
258 |
259 | def get_by_fxos(self, adv_format, fxos_version, a_filter=None):
260 | """Return advisories by Cisco FXOS advisories version"""
261 | req_path = "OSType/fxos"
262 | try:
263 | req_cfg = {
264 | 'version': fxos_version,
265 | 'platformAlias': '' if a_filter is None else a_filter.parameter_value,
266 | }
267 | advisories = self.get_request(req_path, req_cfg)
268 | return self.advisory_list(advisories['advisories'], adv_format)
269 | except requests.exceptions.HTTPError as e:
270 | raise requests.exceptions.HTTPError(
271 | e.response.status_code, e.response.text)
272 |
273 | def get_by_os(self, adv_format, os_type, a_filter=None):
274 | """Return version information regarding the different Network Operating Systems."""
275 | req_path = "OS_version/OS_data"
276 | try:
277 | NOS_Data = self.get_request(
278 | req_path,
279 | params={'OSType': os_type})
280 | #Data is already a list of nost_type objects
281 | return NOS_Data
282 | except requests.exceptions.HTTPError as e:
283 | raise requests.exceptions.HTTPError(
284 | e.response.status_code, e.response.text)
285 |
286 | def get_by_platform(self, adv_format, os_type, a_filter=None):
287 | """Return platform information regarding the different Network Operating Systems."""
288 | req_path = "platforms"
289 | try:
290 | platforms = self.get_request(
291 | req_path,
292 | params={'OSType': os_type})
293 | #Data is already a list of platformAlias objects
294 | return platforms
295 | except requests.exceptions.HTTPError as e:
296 | raise requests.exceptions.HTTPError(
297 | e.response.status_code, e.response.text)
298 |
299 | def get_by(self, topic, format, aspect, **kwargs):
300 | """Cartesian product ternary paths biased REST dispatcher."""
301 | trampoline = { # key: function; required and [optional] parameters
302 | 'all': self.get_by_all, # format, all_adv, a_filter
303 | 'cve': self.get_by_cve, # format, cve, [a_filter]
304 | 'bugid': self.get_by_bugid, # format, bugid, [a_filter]
305 | 'advisory': self.get_by_advisory, # format, an_advisory,[a_filter]
306 | 'severity': self.get_by_severity, # format, severity, [a_filter]
307 | 'year': self.get_by_year, # format, year, [a_filter]
308 | 'latest': self.get_by_latest, # format, latest, [a_filter]
309 | 'product': self.get_by_product, # format, product_name, [a_filter]
310 | 'ios_xe': self.get_by_ios_xe, # 'ios', ios_version, [a_filter]
311 | 'ios': self.get_by_ios, # 'ios', ios_version, [a_filter]
312 | 'nxos': self.get_by_nxos, # 'ios', nxos_version, [a_filter]
313 | 'aci': self.get_by_aci, # 'ios', aci_version, [a_filter]
314 | 'asa': self.get_by_asa, # 'ios', asa_version, [a_filter]
315 | 'fmc': self.get_by_fmc, # 'ios', fmc_version, [a_filter]
316 | 'ftd': self.get_by_ftd, # 'ios', ftd_version, [a_filter]
317 | 'fxos': self.get_by_fxos, # 'ios', fxos_version, [a_filter]
318 | 'OS': self.get_by_os, # format, OS_Type, 'none'
319 | 'platform': self.get_by_platform, # format, OS_Type, 'none'
320 | }
321 | if topic not in trampoline:
322 | raise KeyError(
323 | "REST API 'topic' ({}) not (yet) supported.".format(topic))
324 |
325 | return trampoline[topic](format, aspect, **kwargs)
326 |
327 | def get_request(self, path, params=None):
328 | """Send get request to OpenVuln API utilizing headers.
329 |
330 | :param path: OpenVuln API path.
331 | :param params: url parameters
332 | :return JSON of requested arguments for advisory information.
333 | :raise HTTPError for anything other than a 200 response.
334 | """
335 | self.logger.info("Sending Get Request %s", path)
336 | req_cfg = {'base_url': config.API_URL, 'path': path}
337 | req_url = "{base_url}/{path}".format(**req_cfg)
338 | request_data = {
339 | 'url': req_url,
340 | 'headers': self.headers,
341 | 'params': params,
342 | }
343 | request_id = request_snapshot(request_data)
344 | r = requests.get(**request_data)
345 | r.raise_for_status()
346 | if request_id:
347 | response_snapshots(r.json(), request_id)
348 | return r.json()
349 |
350 | def advisory_list(self, advisories, adv_format):
351 | """Converts json into a list of advisory objects.
352 |
353 | :param advisories: A list of dictionaries describing advisories.
354 | :param adv_format: The target format in default format or
355 | something that evaluates to False (TODO HACK A DID ACK ?) for ios.
356 | :return list of advisory instances
357 | """
358 | adv_format = ensure_adv_format_token(adv_format)
359 | return [advisory.advisory_factory(adv, adv_format, self.logger)
360 | for adv in advisories]
361 |
362 |
363 | def snapshot_timestamp():
364 | """Generate timestamp in format DEBUG_TIME_STAMP_FORMAT."""
365 | return dt.datetime.now().strftime(DEBUG_TIME_STAMP_FORMAT)
366 |
367 |
368 | def snapshot_name(kind, correlating_id, time_stamp=None):
369 | """Generate a snapshot name for kind and correlating id (by request).
370 | :var kind: A string that will be lower cased and postfixed (before
371 | extension) to the filename.
372 | :var correlating_id: A string id to correlate multiple snapshots.
373 | :var time_stamp: A string rep of a time stamp or None
374 | :return A filename.
375 | """
376 | if time_stamp is None:
377 | time_stamp = snapshot_timestamp()
378 | return ('ts-{}_id-{}_snapshot-of-{}.json'
379 | ''.format(time_stamp, correlating_id, kind.lower()))
380 |
381 |
382 | def request_snapshot(data):
383 | """If env has CISCO_OPEN_VULN_API_DEBUG set (and evaluates to True)
384 | dump the data from the request to an existing folder as set by either env
385 | variable CISCO_OPEN_VULN_API_PATH or default taking the current folder.
386 |
387 | :var data: Request data as dict.
388 | :return unique request id, to ease matching to response snapshots or None
389 | if no debugging requested.
390 | """
391 | if not DEBUG_API_USAGE:
392 | return None
393 | request_id = str(uuid.uuid4())
394 | file_path = snapshot_name('request', request_id)
395 | if DEBUG_API_PATH:
396 | file_path = os.path.join(DEBUG_API_PATH, file_path)
397 | try:
398 | with open(file_path, 'w') as f:
399 | json.dump(data, f, encoding='utf-8')
400 | except (OSError, ValueError):
401 | pass # Best effort snapshots ;-)
402 |
403 | return request_id
404 |
405 |
406 | def response_snapshots(data, request_id):
407 | """If env has CISCO_OPEN_VULN_API_DEBUG set (and evaluates to True)
408 | dump the data from the response to an existing folder as set by either env
409 | variable CISCO_OPEN_VULN_API_PATH or default taking the current folder.
410 |
411 | :var data: Repsonse data as received from requests json method (no json!).
412 | :var unique request id, to ease matching to request snapshot.
413 | """
414 | if not DEBUG_API_USAGE:
415 | return None
416 |
417 | time_stamp = snapshot_timestamp()
418 | file_path = snapshot_name('response-raw', request_id, time_stamp)
419 | if DEBUG_API_PATH:
420 | file_path = os.path.join(DEBUG_API_PATH, file_path)
421 | try:
422 | with open(file_path, 'w') as f:
423 | json.dump(data, f, encoding='utf-8')
424 | except (OSError, ValueError):
425 | pass # Best effort snapshots ;-)
426 |
427 | file_path = snapshot_name('response-formatted', request_id, time_stamp)
428 | if DEBUG_API_PATH:
429 | file_path = os.path.join(DEBUG_API_PATH, file_path)
430 | try:
431 | with open(file_path, 'w') as f:
432 | json.dump(data, f, indent=4, encoding='utf-8')
433 | except (OSError, ValueError):
434 | pass # ditto (cf. above)
435 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # openVulnQuery
2 |
3 | A python-based module(s) to query the Cisco PSIRT openVuln API. openVulnQuery is supported in Python version 3.x.
4 |
5 | The Cisco Product Security Incident Response Team (PSIRT) openVuln API is a RESTful API that allows customers to obtain Cisco Security Vulnerability information in different machine-consumable formats. APIs are important for customers because they allow their technical staff and programmers to build tools that help them do their job more effectively (in this case, to keep up with security vulnerability information). More information about the API can be found at:
6 |
7 | ## PIP Installation
8 |
9 | You can easily install openVulnQuery using [pip](https://pypi.org/project/pip/):
10 |
11 | ```
12 | pip3 install openVulnQuery
13 | ```
14 |
15 | Alternatively, depending on your environment, you may need to specify the latest version (1.31), as demonstrated below:
16 |
17 | ```
18 | python3 -m pip install openVulnQuery==1.31
19 | ```
20 |
21 | If you are experiencing any difficulty installing openVulnQuery. Here is the link to [common installation issues solutions]().
22 |
23 | Requirements
24 |
25 | - Tested on Python 3.7 and 3.9.2
26 | - `argparse >= 1.4.0`
27 | - `requests >= 2.10.0`
28 |
29 | ## Config File
30 |
31 | Obtain client ID and Secret:
32 |
33 | 1. Visit
34 | 2. Sign In
35 | 3. Select My Applications Tab
36 | 4. Register a New Application by:
37 |
38 | - Enter an application name
39 | - Enter a description of your application.
40 | - Application Type field is Service.
41 | - Grant Type is Client Credentials.
42 | - Under Select APIs choose Cisco PSIRT openVuln API
43 | - Agree to the terms and service and click Register
44 |
45 | 5. The openVuln API rate limits are shown in the https://apiconsole.cisco.com/apps/mykeys
46 | 6. Note the value of "Client ID" (a string like e.g. 'abc12abcd13abcdefabcde1a')
47 | 7. Note the value of "Client Secret" (a string like e.g. '1a2abcDEfaBcDefAbcDeFA3b')
48 | 8. Provide the credentials to the application at runtime via two preferred alternativev ways:
49 |
50 | - Either export two matching environment variables (below the syntax for bash and assuming the values are as in steps 6\. and 7.):
51 |
52 | ```
53 | >> export CLIENT_ID="abc12abcd13abcdefabcde1a"
54 | >> export CLIENT_SECRET="1a2abcDEfaBcDefAbcDeFA3b"
55 | ```
56 |
57 | - Or create a valid JSON file (e.g. `credentials.json`) with these personal credentials similar to the below given (assuming the values are as in steps 6\. and 7.):
58 |
59 | ```
60 | {
61 | "CLIENT_ID": "abc12abcd13abcdefabcde1a",
62 | "CLIENT_SECRET": "1a2abcDEfaBcDefAbcDeFA3b"
63 | }
64 | ```
65 |
66 | 9. Do not distribute the credentials file resulting from previous step
67 |
68 | **Notes**:
69 |
70 | - The resulting OAuth2 Token will be automatically generated on every call to the API.
71 |
72 | ## Run OpenVulnQuery in the Terminal
73 |
74 | - If installed with pip run the program by typing
75 |
76 | ```
77 | >> openVulnQuery --config PathToCredentialsFile --Advisory Type --API Filters --Parsing Fields --Output Format -Count
78 | ```
79 |
80 | - Or cd into the directory with the main.py file and run using
81 |
82 | ```
83 | >> python main.py --config PathToCredentialsFile --Advisory Type --API Filters --Parsing Fields --Output Format -Count
84 | ```
85 |
86 | Notes:
87 |
88 | -- Used for whole word commands, - Used for single character commands
89 |
90 | ## Configuration (Optional)
91 |
92 | ```
93 | --config FILE
94 | Path to JSON file with credentials (as in above step 8)
95 | A sample has been provided in the same folder as main.py:
96 | sample:configuration.json
97 | The configuration will be tried first from config file,
98 | next from environemnt variables CLIENT_ID and CLIENT_SECRET,
99 | last from config.py variable values, or fail.
100 | ```
101 |
102 | ## API Filters (Required)
103 |
104 | ```
105 | --all
106 | Returns all advisories
107 | Example:
108 | >> openVulnQuery --all
109 |
110 |
111 | --advisory
112 | Search by specific advisory id
113 | Example:
114 | >> openVulnQuery --advisory cisco-sa-20110201-webex
115 |
116 | --bugid
117 | Search by specific Cisco Bug id
118 | Example:
119 | >> openVulnQuery --bugid CSCwb92675
120 |
121 | --cve
122 | Search by specific cve id
123 | Example:
124 | >> openVulnQuery --cve CVE-2010-3043
125 |
126 | --latest
127 | Search by the last number of advisories published
128 | Example:
129 | >> openVulnQuery --latest 10
130 |
131 | Note: the latest option is limited to 100 maximum queries
132 |
133 | --severity
134 | Search by severity (low, medium, high, critical)
135 | Examples:
136 | >> openVulnQuery --severity critical
137 | >> openVulnQuery --severity high
138 | >> openVulnQuery --severity medium
139 | >> openVulnQuery --severity low
140 |
141 | --year
142 | Search by the year (1995 to present)
143 | Example:
144 | >> openVulnQuery --year 2016
145 |
146 | --product
147 | Search by the product name
148 | Example:
149 | >> openVulnQuery --product Cisco
150 |
151 | --ios
152 | Cisco Software Checker has been integrated with openVulnAPI.
153 | Search by IOS version
154 | Examples:
155 | >> openVulnQuery --ios 15.6\(2\)SP (*use \ to escape bracket in ios version)
156 | >> openVulnQuery --ios 15.6(\2\)SP
157 |
158 |
159 | --ios_xe
160 | Cisco Software Checker has been integrated with openVulnAPI.
161 | Search by Cisco IOS XE Software version.
162 | Example:
163 | >> openVulnQuery --ios_xe 3.16.1S
164 |
165 | --nxos
166 | Cisco Software Checker has been integrated with openVulnAPI.
167 | Search by Cisco NX-OS (standalone mode) Software version.
168 | Example:
169 | >> openVulnQuery --nxos 8.3(1)
170 |
171 | --aci
172 | Cisco Software Checker has been integrated with openVulnAPI.
173 | Search by Cisco NX-OS (ACI mode) Software version.
174 | Example:
175 | >> openVulnQuery --aci 11.0(2j)
176 |
177 | --asa
178 | Cisco Software Checker has been integrated with openVulnAPI.
179 | Search by Cisco ASA Software version.
180 | Example:
181 | >> openVulnQuery --asa 9.18.1
182 |
183 | --fmc
184 | Cisco Software Checker has been integrated with openVulnAPI.
185 | Search by Cisco FMC Software version.
186 | Example:
187 | >> openVulnQuery --fmc 7.0.1
188 |
189 | --ftd
190 | Cisco Software Checker has been integrated with openVulnAPI.
191 | Search by Cisco FTD Software version.
192 | Example:
193 | >> openVulnQuery --ftd 7.0.1
194 |
195 | --fxos
196 | Cisco Software Checker has been integrated with openVulnAPI.
197 | Search by Cisco FXOS Software version.
198 | Example:
199 | >> openVulnQuery --fxos 2.6.1.131
200 |
201 | --OS
202 | To obtain version information regarding the different Network Operating Systems.
203 | Examples:
204 | >> openVulnQuery --OS asa
205 | >> openVulnQuery --OS ios
206 |
207 | --platform
208 | To obtain platform alias information regarding the different Network Operating Systems.
209 | Examples:
210 | >> openVulnQuery --platform asa
211 | >> openVulnQuery --platform nxos
212 | ```
213 |
214 | **NOTE**: Cisco reserves the right to remove End-of-Support releases from the Cisco Software Checker (subsequently reflected in this API).
215 |
216 |
217 | ## Client Application (Optional)
218 |
219 | ```
220 | --user-agent APPLICATION
221 | Name of application to be sent as User-Agent header value in the request.
222 | Default is TestApp.
223 | ```
224 |
225 | ## Parsing Fields (Optional)
226 |
227 | Notes:
228 |
229 | If no fields are passed in the default API fields will be returned
230 |
231 | Any field that has no information will return with with the field name and NA
232 |
233 | ### Available Fields
234 |
235 | - advisory_id
236 | - sir
237 | - first_published
238 | - last_updated
239 | - cves
240 | - bug_ids
241 | - cvss_base_score
242 | - advisory_title
243 | - publication_url
244 | - cwe
245 | - product_names
246 | - summary
247 | - vuln_title
248 | - cvrf_url
249 | - csafUrl
250 |
251 | **NOTE**: [CSAF](https://csaf.io) is a specification for structured machine-readable vulnerability-related advisories and further refine those standards over time. CSAF is the new name and replacement for the Common Vulnerability Reporting Framework (CVRF). Cisco will support CVRF until December 31, 2023. More information at: https://csaf.io
252 |
253 | ```
254 | -f or --fields
255 |
256 | API Fields
257 | Examples:
258 | openVulnQuery --config PathToCredentialsFile --any API filter -f or --fields list of fields separated by space
259 | >> openVulnQuery --config PathToCredentialsFile --all -f sir cves cvrf_url
260 | >> openVulnQuery --config PathToCredentialsFile --severity critical -f last_updated cves
261 |
262 | CVRF XML Fields
263 | Examples:
264 | openVulnQuery --config PathToCredentialsFile --any API filter -f or --fields list of fields separated by space
265 | >> openVulnQuery --config PathToCredentialsFile --all -f bug_ids vuln_title product_names
266 | >> openVulnQuery --config PathToCredentialsFile --severity critical -f bug_ids summary
267 |
268 | Combination
269 | Examples:
270 | openVulnQuery --config PathToCredentialsFile --any API filter -f or --fields list of fields separated by space
271 | >> openVulnQuery --config PathToCredentialsFile --all -f sir bug_ids cves vuln_title
272 | >> openVulnQuery --config PathToCredentialsFile --year 2011 -f cves cvrf_url bug_ids summary product_names
273 | ```
274 |
275 | ### Additional Filters
276 |
277 | User can be more specific on filtering advisories when searching all advisories or by severity. They can filter based on last updated and first published dates providing start and end date as a search range. Dates should be entered in YYYY-MM-DD format.
278 |
279 | ```
280 | >> # export CLIENT_ID and CLIENT_SECRET or write to config.py ... then:
281 | >> openVulnQuery --severity high --last_updated 2016-01-02:2016-04-02 --json filename.json
282 | >> openVulnQuery --all --last_updated 2016-01-02:2016-07-02
283 | >> openVulnQuery --severity critical --first_published 2015-01-02:2015-01-04
284 | ```
285 |
286 | ## Output Format (Optional)
287 |
288 | ```
289 | Default
290 | Table style printed to screen
291 | Example:
292 | >> openVulnQuery --config PathToCredentialsFile --year 2016
293 |
294 | --json file path
295 | Returns json in a file in the specified path
296 | Example:
297 | >> openVulnQuery --config PathToCredentialsFile --year 2016 --json /Users/bkorabik/Documents/2016_cvrf.json
298 |
299 | --csv file path
300 | Creates a CSV file in the specified path
301 | Example:
302 | >> openVulnQuery --config PathToCredentialsFile --year 2016 --csv /Users/bkorabik/Documents/2016_cvrf.csv
303 | ```
304 |
305 | ## Count (Optional)
306 |
307 | Returns the count of fields entered with -f or --fields. If no fields are entered the base API fields are counted and displayed
308 |
309 | ```
310 | -c
311 |
312 | Examples:
313 | >> openVulnQuery --config PathToCredentialsFile --year 2016 -c
314 | >> # export CLIENT_ID and CLIENT_SECRET or write to config.py ... then:
315 | >> openVulnQuery --severity low -f sir cves bug_ids -c
316 | ```
317 |
318 | ## Developers
319 |
320 | - Update the config.py file with client id and secret
321 | - Directly interact with query_client.py to query the Open Vuln API
322 | - query_client.py returns Advisory Object
323 | - advisory.py module has Advisory object a abstract class
324 | - This abstraction hides the implementation details and the data source used to populate the data type. The data members of security advisories are populated from API results.
325 |
326 | ## Disclosures:
327 |
328 | No support for filtering based on --API fields, you can't use --year 2016 and --severity high
329 |
330 | Filtering with Grep:
331 |
332 | ```
333 | Finding the Number of CVRF Advisories with a "Critical" sir in 2013
334 | >> openVulnQuery --config PathToCredentialsFile --year 2013 -f sir | grep -c "Critical"
335 | >> openVulnQuery --config PathToCredentialsFile --severity critical -f first_published | grep -c "2013"
336 | ```
337 |
338 | If more than one API filter is entered, the last filter will be used for the API call.
339 |
340 | You can alternatively use the date range functionality, as shown below:
341 |
342 | ```
343 | >> openVulnQuery --config PathToCredentialsFile --severity critical --first_published 2017-01-02:2017-10-01
344 | ```
345 |
346 | ## Run OpenVulnQuery as a Library
347 |
348 | After you install openVulnQuery package, you can use the query_client module to make API-call which returns advisory objects. For each query to the API, you can pick the advisory format.
349 |
350 | ```
351 | >> from openVulnQuery import query_client
352 | >> query_client = query_client.OpenVulnQueryClient(client_id="", client_secret="")
353 | >> advisories = query_client.get_by_year(year=2010, adv_format='default')
354 | >> advisories = query_client.get_by_ios_xe('ios', '3.16.1S')
355 | ```
356 |
357 | If you want to use the additional date filters based on first published and last updated date. You can pass the appropriate class
358 |
359 | ```
360 | >> advisories = query_client.get_by_severity(adv_format='cvrf', severity='high', FirstPublished(2016-01-01, 2016-02-02))
361 | ```
362 |
363 | ### Debugging Requests and Responses
364 |
365 | If the run time environment has the variable `CISCO_OPEN_VULN_API_DEBUG` set (and the value evaluates to True) the data forming every request as well as raw and formatted variants of successful responses (`HTTP 200/OK`) will be written to files in JSON format.
366 |
367 | The file names follow the pattern: `ts-{ts}_id-{id}_snapshot-of-{kind}.json`, where:
368 |
369 | - `{ts}` receives a date time stamp as ruled by the module variable `DEBUG_TIME_STAMP_FORMAT` (default `%Y%m%dT%H%M%S.%f`) and noted in local time,
370 | - `{id}` is a string holding a UUID4 generated for the request and useful to correlate request and response data files
371 | - `{kind}` is one of three strings speaking for themselves:
372 |
373 | - `request`
374 | - `response-raw`
375 | - `response-formated`
376 |
377 | The files will be written either to the current folder, or to a path stored in the environment variable `CISCO_OPEN_VULN_API_PATH` (if it is set).
378 |
379 | _Note_: The folder at that later path is expected to exist and be writeable by the user. Please note also, that Filesystem and JSON serialization errors are ignored.
380 |
381 | Here are the information stored in advisory object.
382 |
383 | ### Advisory
384 |
385 | ```
386 | * advisory_id
387 | * sir
388 | * first_published
389 | * last_updated
390 | * cves
391 | * bug_ids
392 | * cvss_base_score
393 | * advisory_title
394 | * publication_url
395 | * cwe
396 | * product_names
397 | * summary
398 | ```
399 |
400 | ### CVRF (inherits Advisory Abstract Class)
401 |
402 | ```
403 | * cvrf_url
404 | * vuln_title
405 | ```
406 |
407 |
408 |
409 | After you install openVulnQuery package, you can use the query_client module to make API-call which returns advisory objects. For each query to the API, you can pick advisory format.
410 |
411 | ```
412 | >> from openVulnQuery import query_client
413 | >> query_client = query_client.OpenVulnQueryClient(client_id='', client_secret='')
414 | >> advisories = query_client.get_by_year(year=2010, adv_format='default')
415 | ```
416 |
417 | Here are the information stored in advisory object.
418 |
419 | ### Advisory (Abstract Base Class)
420 |
421 | ```
422 | * advisory_id
423 | * sir
424 | * first_published
425 | * last_updated
426 | * cves
427 | * bug_ids
428 | * cvss_base_score
429 | * advisory_title
430 | * publication_url
431 | * cwe
432 | * product_names
433 | * summary
434 | ```
435 |
436 | ### CVRF
437 |
438 | ```
439 | * cvrf_url
440 | ```
441 |
442 | ### AdvisoryIOS
443 |
444 | ```
445 | * ios_release
446 | * first_fixed
447 | * cvrf_url
448 | ```
449 |
450 | ### Running the tests
451 |
452 | To run the tests in the tests folder, the additional required `mock` module should be installed inside the `venv`with the usual:
453 |
454 | ```
455 | pip3 install mock pytest
456 | ```
457 |
458 | There are unit tests in `tests/` and some sample like system level test (`tests/test_query_client_cvrf.py`) skipped in below sample runs, as it contacting the real API.
459 |
460 | Sample run (expecting `pytest` has been installed e.g. via `pip3 install pytest`):
461 |
462 | ```
463 | $ cd /www/github.com/CiscoPSIRT/openVulnAPI/openVulnQuery
464 |
465 | $ pytest
466 | =========================================================================================================== test session starts ============================================================================================================
467 | platform darwin -- pytest-3.1.2, py-1.4.34, pluggy-0.4.0
468 | rootdir: /www/github.com/CiscoPSIRT/openVulnAPI/openVulnQuery, inifile:
469 | plugins: cov-2.5.1
470 | collected 159 items
471 |
472 | tests/test_advisory.py ......................
473 | tests/test_authorization.py ...
474 | tests/test_cli_api.py ..............................................
475 | tests/test_config.py ....
476 | tests/test_constants.py ...........
477 | tests/test_main.py ...........................s......
478 | tests/test_query_client.py ................
479 | tests/test_query_client_cvrf.py ssssssss
480 | tests/test_utils.py ...............
481 |
482 | ================================================================================================== 150 passed, 9 skipped in 1.16 seconds ===================================================================================================
483 | ```
484 |
485 | Including coverage info (requires `pip install pytest-cov` which includes `pip install coverage` ):
486 |
487 | ```
488 | $ pytest --cov=openVulnQuery --cov-report=term-missing --cov-report=html
489 | =========================================================================================================== test session starts ============================================================================================================
490 | platform darwin -- pytest-3.1.2, py-1.4.34, pluggy-0.4.0
491 | rootdir: /www/github.com/CiscoPSIRT/openVulnAPI/openVulnQuery, inifile:
492 | plugins: cov-2.5.1
493 | collected 159 items
494 |
495 | tests/test_advisory.py ......................
496 | tests/test_authorization.py ...
497 | tests/test_cli_api.py ..............................................
498 | tests/test_config.py ....
499 | tests/test_constants.py ...........
500 | tests/test_main.py ...........................s......
501 | tests/test_query_client.py ................
502 | tests/test_query_client_cvrf.py ssssssss
503 | tests/test_utils.py ...............
504 |
505 | ---------- coverage: platform darwin, python 2.7.13-final-0 ----------
506 | Name Stmts Miss Cover Missing
507 | --------------------------------------------------------------
508 | openVulnQuery/__init__.py 0 0 100%
509 | openVulnQuery/advisory.py 90 1 99% 59
510 | openVulnQuery/authorization.py 6 0 100%
511 | openVulnQuery/cli_api.py 75 4 95% 294-297, 311
512 | openVulnQuery/config.py 4 0 100%
513 | openVulnQuery/constants.py 11 0 100%
514 | openVulnQuery/main.py 38 6 84% 57, 60-65, 70
515 | openVulnQuery/query_client.py 100 16 84% 128-134, 148-155, 160-167
516 | openVulnQuery/rest_api.py 3 0 100%
517 | openVulnQuery/utils.py 76 12 84% 109, 118-129
518 | --------------------------------------------------------------
519 | TOTAL 403 39 90%
520 | Coverage HTML written to dir htmlcov
521 |
522 |
523 | ================================================================================================== 150 passed, 9 skipped in 1.60 seconds ===================================================================================================
524 | ```
525 |
--------------------------------------------------------------------------------