├── tests ├── __init__.py ├── test_config.py ├── test_authorization.py ├── test_constants.py ├── test_utils.py ├── test_advisory.py ├── test_main.py ├── test_cli_api.py ├── test_query_client_cvrf.py └── test_query_client.py ├── openVulnQuery ├── _library │ ├── __init__.py │ ├── sample_credentials.json │ ├── _compatibility.py │ ├── rest_api.py │ ├── authorization.py │ ├── config.py │ ├── constants.py │ ├── main.py │ ├── utils.py │ ├── advisory.py │ ├── cli_api.py │ └── query_client.py ├── main.py └── __init__.py ├── .gitignore ├── requirements ├── prod.txt ├── common.txt └── dev.txt ├── Dockerfile ├── sbom_examples ├── example-openVulnQuery1_31-swid.xml ├── CycloneDX-openVulnQuery1-31-10-6-2021-21-59.json ├── example-openVulnQuery1_31-cyclonedx.xml └── example-openVulnQuery1_31.spdx ├── setup.py ├── LICENSE ├── .github └── workflows │ ├── python-package.yml │ ├── python-publish.yml │ └── generator-generic-ossf-slsa3-publish.yml ├── InstallationIssueSolutions.md ├── SECURITY.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openVulnQuery/_library/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | openVulnQuery/.DS_Store 3 | -------------------------------------------------------------------------------- /requirements/prod.txt: -------------------------------------------------------------------------------- 1 | -r common.txt 2 | -------------------------------------------------------------------------------- /requirements/common.txt: -------------------------------------------------------------------------------- 1 | requests==2.20.0 2 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | -r common.txt 2 | 3 | mock 4 | pytest 5 | pytest-cov 6 | -------------------------------------------------------------------------------- /openVulnQuery/main.py: -------------------------------------------------------------------------------- 1 | from ._library.main import main 2 | 3 | if __name__ == '__main__': 4 | main() 5 | -------------------------------------------------------------------------------- /openVulnQuery/_library/sample_credentials.json: -------------------------------------------------------------------------------- 1 | { 2 | "CLIENT_ID": "BadCodedBadCodedBadCoded", 3 | "CLIENT_SECRET": "DeadFaceDeadFaceDeadFace" 4 | } -------------------------------------------------------------------------------- /openVulnQuery/_library/_compatibility.py: -------------------------------------------------------------------------------- 1 | if str == bytes: 2 | string_or_bytes = basestring 3 | else: 4 | string_or_bytes = (str, bytes, bytearray) 5 | 6 | def is_unicode_or_bytes(argument): 7 | return isinstance(argument, string_or_bytes) 8 | -------------------------------------------------------------------------------- /openVulnQuery/__init__.py: -------------------------------------------------------------------------------- 1 | from ._library import advisory 2 | from ._library import authorization 3 | from ._library import cli_api 4 | from ._library import config 5 | from ._library import constants 6 | from ._library import query_client 7 | from ._library import rest_api 8 | from ._library import utils 9 | from ._library import main 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This is a Dockerfile to create a container running Python 3.7 containing the 2 | # openVulnQuery module. The openVulnQuery module is a client for the Cisco 3 | # openVuln API. 4 | # 5 | # Author: Omar Santos, os@cisco.com 6 | 7 | 8 | FROM python:3.7.10-alpine as builder 9 | 10 | WORKDIR /build 11 | COPY . . 12 | RUN python3 setup.py bdist_wheel 13 | 14 | 15 | FROM python:3.7.10-alpine 16 | 17 | COPY --from=builder /build/dist/*.whl /whl/ 18 | RUN python3 -m pip --no-cache-dir install /whl/*.whl \ 19 | && rm -rf /whl 20 | 21 | CMD ["/usr/local/bin/python3"] 22 | -------------------------------------------------------------------------------- /openVulnQuery/_library/rest_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | For latest OpenAPI specification file for this API consult: 3 | https://github.com/CiscoPSIRT/openVulnAPI/blob/master/swagger/openVulnAPIOAS_3_0_3.yaml 4 | 5 | The OpenVulnQuery client implements all the same endpoints but is currently missing some optional parameters. 6 | """ 7 | 8 | MIME_TYPE = 'application/json' 9 | 10 | 11 | def rest_with_auth_headers(auth_token, user_agent): 12 | """Construct per session for sending with all GET requests to API.""" 13 | return { 14 | 'Authorization': 'Bearer {}'.format(auth_token), 15 | 'Accept': MIME_TYPE, 16 | 'User-Agent': user_agent, 17 | } 18 | -------------------------------------------------------------------------------- /sbom_examples/example-openVulnQuery1_31-swid.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 11 | 13 | 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setup(name='OpenVulnQuery', 7 | version='1.34', 8 | description='A python-based module(s) to query the Cisco PSIRT openVuln API.', 9 | long_description=long_description, 10 | long_description_content_type="text/markdown", 11 | url='https://github.com/CiscoPSIRT/openVulnQuery', 12 | author=' Omar Santos', 13 | author_email='os@cisco.com', 14 | license='The MIT License (MIT)', 15 | packages=find_packages(exclude=["tests"]), 16 | entry_points={ 17 | 'console_scripts': 18 | ['openVulnQuery=openVulnQuery.main:main'] 19 | }, 20 | install_requires=[ 21 | 'argparse>=1.4.0', 22 | 'requests>=2.10.0' 23 | ], 24 | zip_safe=False,) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025, Cisco Systems, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation 4 | files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, 5 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished 6 | to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 11 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 12 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 13 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: openVulnQuery Package 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: [3.8, 3.9.16, 3.10.11, 3.11.3] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install flake8 pytest mock pytest pytest-cov requests 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | - name: Test with pytest 32 | run: | 33 | pytest 34 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | """Note: Suggested as pre-commit hook, to block credentials being published.""" 2 | 3 | import os 4 | import unittest 5 | 6 | from openVulnQuery import config 7 | 8 | CLIENT_ID = '' 9 | CLIENT_ID_DUMMY = 'BadCodedBadCodedBadCoded' 10 | CLIENT_SECRET_DUMMY = 'DeadFaceDeadFaceDeadFace' 11 | CLIENT_SECRET = '' 12 | REQUEST_TOKEN_URL = "https://cloudsso.cisco.com/as/token.oauth2" 13 | API_URL = "https://api.cisco.com/security/advisories/v2" 14 | 15 | 16 | class ConfigTest(unittest.TestCase): 17 | def test_config_api_reference_ok(self): 18 | self.assertEqual(config.API_URL, API_URL) 19 | 20 | def test_config_token_reference_ok(self): 21 | self.assertEqual(config.REQUEST_TOKEN_URL, REQUEST_TOKEN_URL) 22 | 23 | def test_config_client_id_empty(self): 24 | self.assertIn(config.CLIENT_ID, 25 | (CLIENT_ID, CLIENT_ID_DUMMY, os.getenv('CLIENT_ID', ''))) 26 | 27 | def test_config_client_secret_empty(self): 28 | self.assertIn( 29 | CLIENT_SECRET, 30 | (CLIENT_SECRET, CLIENT_SECRET_DUMMY, 31 | os.getenv('CLIENT_SECRET', ''))) 32 | -------------------------------------------------------------------------------- /openVulnQuery/_library/authorization.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from . import config 4 | 5 | 6 | def get_oauth_token(client_id, client_secret, request_token_url=None): 7 | """Get OAuth2 token from api based on client id and secret. 8 | 9 | :param client_id: Client id stored in config file. 10 | :param client_secret: Client secret stored in config file. 11 | :param request_token_url: the POST URL to request a token response 12 | 13 | :return The valid access token to pass to api in header. 14 | :raise requests exhibits anything other than a 200 response. 15 | 16 | """ 17 | r = requests.post( 18 | request_token_url if request_token_url else config.REQUEST_TOKEN_URL, 19 | params={'client_id': client_id, 'client_secret': client_secret}, 20 | data={'grant_type': 'client_credentials'} 21 | ) 22 | # Added check to see if migrated to new API structure. 23 | if r.status_code == 400: 24 | print("Ensure you have updated your code as per https://raw.githubusercontent.com/api-at-cisco/Images/master/Whats_New_Doc.pdf") 25 | exit() 26 | r.raise_for_status() 27 | return r.json()['access_token'] 28 | -------------------------------------------------------------------------------- /openVulnQuery/_library/config.py: -------------------------------------------------------------------------------- 1 | # There are four prerequisites needed before a user can initiate the API service calls and obtain access to the underlying Cisco security vulnerability information. 2 | # - Sign-in with your CCO ID 3 | # - Register a client application to create a “unique client identifier” that will identify your client application to the Cisco Token services. Registration creates the client credentials along with name assignment, description, and subscribes the client application to one or more of the OAuth v2.0 grant types requested for their client application. 4 | # - Get Access Tokens - utilize Cisco's Token services to acquire an OAuth v2.0 access-token(s). 5 | # - Make API Calls 6 | 7 | # Enter your client ID and client secret below. 8 | 9 | CLIENT_ID = "" 10 | CLIENT_SECRET = "" 11 | 12 | #The following two URLs have changed. See https://raw.githubusercontent.com/api-at-cisco/Images/master/Whats_New_Doc.pdf 13 | #REQUEST_TOKEN_URL = "https://cloudsso.cisco.com/as/token.oauth2" 14 | #API_URL = "https://api.cisco.com/security/advisories/v2" 15 | 16 | REQUEST_TOKEN_URL = "https://id.cisco.com/oauth2/default/v1/token" 17 | API_URL = "https://apix.cisco.com/security/advisories/v2" 18 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /InstallationIssueSolutions.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting openVulnQuery Installation Issues # 2 | 3 | This page is dedicated to resolving common installation issue encountered during installation of openVulnQuery. 4 | 5 | #### Mac OS X/ Linux #### 6 | - ##### Permission Denied on installation as mentioned in https://communities.cisco.com/thread/71785 ###### 7 | 8 | While installing a Python package, It is recommended to do so in python virtual environment. This keeps all the dependencies seperate 9 | from global site-packages. `virtualenv` is a tool to create this isolated python environment. 10 | 11 | ```sh 12 | $ pip install virtualenv 13 | ``` 14 | Now, go to your project folder 15 | 16 | ```sh 17 | $ cd project_folder 18 | $ virtualenv venv 19 | ``` 20 | Here venv is name of your virtual environment 21 | To activate the virtual environment 22 | 23 | ```sh 24 | $ source venv/bin/activate 25 | ``` 26 | 27 | you will see (venv) added on your terminal. 28 | This means your virtual environment is active and 29 | whatever you install using pip will be contained in that virtual environment without being installed into your local machine. 30 | 31 | 32 | ```sh 33 | $ pip install openVulnQuery 34 | ``` 35 | 36 | You can find more information in this [guide to python page](http://docs.python-guide.org/en/latest/dev/virtualenvs/). -------------------------------------------------------------------------------- /sbom_examples/CycloneDX-openVulnQuery1-31-10-6-2021-21-59.json: -------------------------------------------------------------------------------- 1 | { 2 | "bomFormat": "CycloneDX", 3 | "specVersion": "1.2", 4 | "serialNumber": "urn:uuid:3240804f-7eba-172b-cad8-2c2dd1e3bcc9", 5 | "version": 1, 6 | "metadata": { 7 | "timestamp": "2021-06-11T00:34:00Z", 8 | "authors": [ 9 | { 10 | "name": " Omar Santos" 11 | } 12 | ], 13 | "component": { 14 | "type": "device", 15 | "bom-ref": "55b70252-2b42-cea8-f67d-0ba91c66f906", 16 | "name": "openVulnQuery", 17 | "purl": "pkg:supplier/%20Cisco/openVulnQuery@1.31", 18 | "supplier": { 19 | "name": " Cisco" 20 | }, 21 | "version": "1.31" 22 | }, 23 | "manufacture": { 24 | "name": " Cisco" 25 | } 26 | }, 27 | "components": [ 28 | { 29 | "type": "library", 30 | "bom-ref": "4f12be9f-a189-6ecb-fccf-9ac386118f2c", 31 | "name": "argparse", 32 | "purl": "pkg:supplier/%20Python%20Software%20Foundation/argparse@1.4.0", 33 | "publisher": " Python Software Foundation", 34 | "version": "1.4.0" 35 | }, 36 | { 37 | "type": "library", 38 | "bom-ref": "ac1deb94-99cc-526c-f609-c49ad8c1fe10", 39 | "name": "requests", 40 | "purl": "pkg:supplier/%20Python%20Software%20Foundation/requests@2.25.1", 41 | "publisher": " Python Software Foundation", 42 | "version": "2.25.1" 43 | } 44 | ], 45 | "dependencies": [ 46 | { 47 | "ref": "55b70252-2b42-cea8-f67d-0ba91c66f906", 48 | "dependsOn": [ 49 | "4f12be9f-a189-6ecb-fccf-9ac386118f2c", 50 | "ac1deb94-99cc-526c-f609-c49ad8c1fe10" 51 | ] 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To learn about Cisco security vulnerability disclosure policies and publications, access the [Cisco Security Vulnerability Policy](https://tools.cisco.com/security/center/resources/security_vulnerability_policy.html). This document also contains instructions for obtaining fixed software and receiving security vulnerability information from Cisco. 4 | 5 | All Cisco Security Advisories are published at: https://www.cisco.com/go/psirt 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Individuals or organizations that are experiencing a product security issue are strongly encouraged to contact the Cisco PSIRT. Cisco welcomes reports from independent researchers, industry organizations, vendors, customers, and other sources concerned with product or network security. The minimal data needed for reporting a security issue is a description of the potential vulnerability. 10 | 11 | Please contact the Cisco PSIRT using one of the following methods. 12 | 13 | ### Emergency Support 14 | - Phone +1 877 228 7302 (toll-free within North America) 15 | - +1 408 525 6532 (International direct-dial) 16 | - Hours 24 hours a day, 7 days a week 17 | 18 | ## Nonemergency Support 19 | - Email psirt@cisco.com 20 | - Hours Support requests that are received via email are typically acknowledged within 48 hours. Ongoing status on reported issues will be determined as needed. 21 | 22 | Cisco encourages the encryption of sensitive information that is sent to Cisco in email messages. The Cisco PSIRT supports encrypted messages via PGP/GNU Privacy Guard (GPG). The Cisco PSIRT public key is available at the following link: https://tools.cisco.com/security/center/resources/security_vulnerability_policy.html#cpsir 23 | -------------------------------------------------------------------------------- /sbom_examples/example-openVulnQuery1_31-cyclonedx.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 2021-06-10T20:34:00Z 6 | 7 | 8 | Omar Santos 9 | 10 | 11 | 12 | 13 | openVulnQuery 14 | 1.31 15 | pkg:supplier/Cisco/openVulnQuery@1.31 16 | 17 | 18 | Cisco 19 | 20 | 21 | Cisco 22 | 23 | 24 | 25 | 26 | 27 | Cisco 28 | 29 | 30 | 31 | 32 | 33 | Python Software Foundation 34 | argparse 35 | 1.4.0 36 | pkg:supplier/Python%20Software%20Foundation/argparse@1.4.0 37 | 38 | 39 | Python Software Foundation 40 | requests 41 | 2.25.1 42 | pkg:supplier/Python%20Software%20Foundation/requests@2.25.1 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /openVulnQuery/_library/constants.py: -------------------------------------------------------------------------------- 1 | IPS_SIGNATURE_LABEL = 'ips_signatures' 2 | PLATFORMS_LABEL = 'platforms' 3 | 4 | API_LABELS = ( 5 | 'advisory_id', 6 | 'advisory_title', 7 | 'bug_ids', 8 | 'cves', 9 | 'cvrfUrl', 10 | 'csafUrl', 11 | 'cvss_base_score', 12 | 'cwe', 13 | 'first_fixed', 14 | 'first_published', 15 | 'ios_release', 16 | IPS_SIGNATURE_LABEL, 17 | PLATFORMS_LABEL, 18 | 'last_updated', 19 | 'product_names', 20 | 'publication_url', 21 | 'sir', 22 | 'summary', 23 | ) 24 | 25 | IPS_SIGNATURES = ( 26 | 'legacy_ips_id', 27 | 'legacy_ips_url', 28 | 'release_version', 29 | 'software_version', 30 | ) 31 | 32 | PLATFORMS = ( 33 | 'id', 34 | 'name', 35 | 'firstFixes', 36 | 'vulnerabilityState', 37 | ) 38 | 39 | ALLOWS_FILTER = ( 40 | 'all', 41 | 'severity', 42 | ) 43 | 44 | ALLOWS_OPTIONAL = ( 45 | 'platformAlias', 46 | ) 47 | 48 | NON_ADVISORY_QUERY = ( 49 | 'OS', 50 | 'platform', 51 | ) 52 | 53 | SUPPORTED_PLATFORMS_VERSION = ( 54 | 'aci', 55 | 'asa', 56 | 'ios', 57 | 'iosxe', 58 | 'ftd', 59 | 'fmc', 60 | 'fxos', 61 | 'nxos', 62 | ) 63 | 64 | SUPPORTED_PLATFORMS_ALIAS = ( 65 | 'asa', 66 | 'ftd', 67 | 'fxos', 68 | 'nxos', 69 | ) 70 | 71 | SUPPORTED_PLATFORMS_ALIAS_NAME_ASA = ( 72 | 'ISA3000', 73 | 'ASAV', 74 | 'ASA5500X', 75 | 'ASASM', 76 | 'FPR1000', 77 | 'FPR2100', 78 | 'FPR4100', 79 | 'FPR9000', 80 | 'FWL3100', 81 | ) 82 | 83 | SUPPORTED_PLATFORMS_ALIAS_NAME_FTD = ( 84 | 'ISA3000', 85 | 'ASA5500', 86 | 'FPR1000', 87 | 'FPR2100', 88 | 'FPR4100', 89 | 'FPR9000', 90 | 'FPRNGFW', 91 | 'FWL3100', 92 | ) 93 | 94 | SUPPORTED_PLATFORMS_ALIAS_NAME_FXOS = ( 95 | 'FPR4100', 96 | 'FPR9000', 97 | ) 98 | 99 | SUPPORTED_PLATFORMS_ALIAS_NAME_NXOS = ( 100 | 'MDS9000', 101 | 'NEXUS1000V', 102 | 'NEXUS3000', 103 | 'NEXUS5000', 104 | 'NEXUS6000', 105 | 'NEXUS7000', 106 | 'NEXUS9000', 107 | ) 108 | 109 | NA_INDICATOR = 'NA' 110 | 111 | JSON_OUTPUT_FORMAT_TOKEN = 'json' 112 | CSV_OUTPUT_FORMAT_TOKEN = 'csv' 113 | 114 | DEFAULT_ADVISORY_FORMAT_TOKEN = 'default' 115 | IOS_ADVISORY_FORMAT_TOKEN = 'ios' 116 | IOSXE_ADVISORY_FORMAT_TOKEN = 'ios_xe' 117 | 118 | ADVISORY_FORMAT_TOKENS = ( 119 | DEFAULT_ADVISORY_FORMAT_TOKEN, 120 | IOS_ADVISORY_FORMAT_TOKEN, 121 | ) 122 | -------------------------------------------------------------------------------- /.github/workflows/generator-generic-ossf-slsa3-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow lets you generate SLSA provenance file for your project. 7 | # The generation satisfies level 3 for the provenance requirements - see https://slsa.dev/spec/v0.1/requirements 8 | # The project is an initiative of the OpenSSF (openssf.org) and is developed at 9 | # https://github.com/slsa-framework/slsa-github-generator. 10 | # The provenance file can be verified using https://github.com/slsa-framework/slsa-verifier. 11 | # For more information about SLSA and how it improves the supply-chain, visit slsa.dev. 12 | 13 | name: SLSA generic generator 14 | on: 15 | workflow_dispatch: 16 | release: 17 | types: [created] 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | outputs: 23 | digests: ${{ steps.hash.outputs.digests }} 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | # ======================================================== 29 | # 30 | # Step 1: Build your artifacts. 31 | # 32 | # ======================================================== 33 | - name: Build artifacts 34 | run: | 35 | # These are some amazing artifacts. 36 | echo "artifact1" > artifact1 37 | echo "artifact2" > artifact2 38 | 39 | # ======================================================== 40 | # 41 | # Step 2: Add a step to generate the provenance subjects 42 | # as shown below. Update the sha256 sum arguments 43 | # to include all binaries that you generate 44 | # provenance for. 45 | # 46 | # ======================================================== 47 | - name: Generate subject for provenance 48 | id: hash 49 | run: | 50 | set -euo pipefail 51 | 52 | # List the artifacts the provenance will refer to. 53 | files=$(ls artifact*) 54 | # Generate the subjects (base64 encoded). 55 | echo "hashes=$(sha256sum $files | base64 -w0)" >> "${GITHUB_OUTPUT}" 56 | 57 | provenance: 58 | needs: [build] 59 | permissions: 60 | actions: read # To read the workflow path. 61 | id-token: write # To sign the provenance. 62 | contents: write # To add assets to a release. 63 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.4.0 64 | with: 65 | base64-subjects: "${{ needs.build.outputs.digests }}" 66 | upload-assets: true # Optional: Upload to a new release 67 | -------------------------------------------------------------------------------- /tests/test_authorization.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import mock 4 | import requests 5 | from openVulnQuery import authorization 6 | from openVulnQuery import config 7 | 8 | CLIENT_ID = 'BadCodedBadCodedBadCoded' 9 | CLIENT_SECRET = 'DeadFaceDeadFaceDeadFace' 10 | REQUEST_TOKEN_URL = config.REQUEST_TOKEN_URL 11 | API_URL = config.API_URL 12 | 13 | BAD_REQUEST_TOKEN_URL = "https://example.com/as/token.oauth2" 14 | NOT_FOUND_REQUEST_TOKEN_URL = "https://cloudsso.cisco.com/as/token.oauth2.404" 15 | NOT_FOUND_STATUS = 404 16 | NOT_FOUND_REASON = 'Not Found' 17 | NOT_FOUND_URL_FULL = ( 18 | "{}?client_secret={}&client_id={}" 19 | "".format(NOT_FOUND_REQUEST_TOKEN_URL, CLIENT_SECRET, CLIENT_ID)) 20 | HTTP_ERROR_MSG = '{} Client Error: {} for url: {}'.format( 21 | NOT_FOUND_STATUS, NOT_FOUND_REASON, NOT_FOUND_URL_FULL) 22 | 23 | OK_STATUS = 200 24 | OAUTH2_RESPONSE_BOGUS_BUT_OK = { 25 | "access_token": "FeedFaceBadCodedDeadBeadFeed", 26 | "token_type": "Bearer", 27 | "expires_in": 3599 28 | } 29 | 30 | 31 | def mocked_req_post(*args, **kwargs): 32 | class MockResponse: 33 | def __init__(self, url=None, json_in=None, status_code=None): 34 | self.url = url 35 | self.json_in = json_in 36 | self.status_code = status_code 37 | 38 | def json(self): 39 | return self.json_in 40 | 41 | def raise_for_status(self): 42 | if self.status_code != OK_STATUS: 43 | raise requests.exceptions.HTTPError(HTTP_ERROR_MSG) 44 | url = args[0] 45 | if url == REQUEST_TOKEN_URL: 46 | return MockResponse(url, OAUTH2_RESPONSE_BOGUS_BUT_OK, 200) 47 | return MockResponse(url, None, NOT_FOUND_STATUS).raise_for_status() 48 | 49 | 50 | class AuthorizationTest(unittest.TestCase): 51 | @mock.patch('openVulnQuery.authorization.requests.post', 52 | side_effect=mocked_req_post) 53 | def test_authorization_smoke_succeeds_mocked(self, mock_post): 54 | data = authorization.get_oauth_token( 55 | CLIENT_ID, CLIENT_SECRET, REQUEST_TOKEN_URL) 56 | self.assertEqual(data, OAUTH2_RESPONSE_BOGUS_BUT_OK['access_token']) 57 | 58 | @mock.patch('openVulnQuery.authorization.requests.post', 59 | side_effect=mocked_req_post) 60 | def test_authorization_smoke_raises_mocked(self, mock_post): 61 | self.assertRaises( 62 | requests.exceptions.HTTPError, 63 | authorization.get_oauth_token, 64 | CLIENT_ID, CLIENT_SECRET, NOT_FOUND_REQUEST_TOKEN_URL) 65 | 66 | @mock.patch('openVulnQuery.authorization.requests.post', 67 | side_effect=mocked_req_post) 68 | def test_authorization_smoke_raises_details_mocked(self, mock_post): 69 | with self.assertRaises(requests.exceptions.HTTPError) as e: 70 | authorization.get_oauth_token( 71 | CLIENT_ID, CLIENT_SECRET, NOT_FOUND_REQUEST_TOKEN_URL) 72 | self.assertEqual(str(e.exception), HTTP_ERROR_MSG) 73 | -------------------------------------------------------------------------------- /tests/test_constants.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openVulnQuery import constants 4 | 5 | IPS_SIGNATURE_LABEL = 'ips_signatures' 6 | 7 | API_LABELS = ( 8 | 'advisory_id', 9 | 'advisory_title', 10 | 'bug_ids', 11 | 'cves', 12 | 'cvrf_url', 13 | 'cvss_base_score', 14 | 'cwe', 15 | 'first_fixed', 16 | 'first_published', 17 | 'ios_release', 18 | IPS_SIGNATURE_LABEL, 19 | 'last_updated', 20 | 'oval_url', 21 | 'product_names', 22 | 'publication_url', 23 | 'sir', 24 | 'summary', 25 | ) 26 | 27 | IPS_SIGNATURES = ( 28 | 'legacy_ips_id', 29 | 'legacy_ips_url', 30 | 'release_version', 31 | 'software_version', 32 | ) 33 | 34 | ALLOWS_FILTER = ( 35 | 'all', 36 | 'severity', 37 | ) 38 | 39 | NA_INDICATOR = 'NA' 40 | 41 | JSON_OUTPUT_FORMAT_TOKEN = 'json' 42 | CSV_OUTPUT_FORMAT_TOKEN = 'csv' 43 | 44 | CVRF_ADVISORY_FORMAT_TOKEN = 'cvrf' 45 | OVAL_ADVISORY_FORMAT_TOKEN = 'oval' 46 | IOS_ADVISORY_FORMAT_TOKEN = 'ios' 47 | 48 | ADVISORY_FORMAT_TOKENS = ( 49 | CVRF_ADVISORY_FORMAT_TOKEN, 50 | OVAL_ADVISORY_FORMAT_TOKEN, 51 | IOS_ADVISORY_FORMAT_TOKEN, 52 | ) 53 | 54 | 55 | class ConstantsTest(unittest.TestCase): 56 | def test_constants_unchanged_na_indicator(self): 57 | self.assertEqual(constants.NA_INDICATOR, NA_INDICATOR) 58 | 59 | def test_constants_filters_available(self): 60 | self.assertEqual(constants.ALLOWS_FILTER, ALLOWS_FILTER) 61 | 62 | def test_constants_unchanged_advisory_tokens(self): 63 | self.assertEqual( 64 | constants.ADVISORY_FORMAT_TOKENS, ADVISORY_FORMAT_TOKENS) 65 | 66 | def test_constants_unchanged_json_format_token(self): 67 | self.assertEqual( 68 | constants.JSON_OUTPUT_FORMAT_TOKEN, JSON_OUTPUT_FORMAT_TOKEN) 69 | 70 | def test_constants_unchanged_csv_format_token(self): 71 | self.assertEqual( 72 | constants.CSV_OUTPUT_FORMAT_TOKEN, CSV_OUTPUT_FORMAT_TOKEN) 73 | 74 | def test_constants_unchanged_cvrf_advisory_format_token(self): 75 | self.assertEqual( 76 | constants.CVRF_ADVISORY_FORMAT_TOKEN, CVRF_ADVISORY_FORMAT_TOKEN) 77 | 78 | def test_constants_unchanged_oval_advisory_format_token(self): 79 | self.assertEqual( 80 | constants.OVAL_ADVISORY_FORMAT_TOKEN, OVAL_ADVISORY_FORMAT_TOKEN) 81 | 82 | def test_constants_unchanged_ios_advisory_format_token(self): 83 | self.assertEqual( 84 | constants.IOS_ADVISORY_FORMAT_TOKEN, IOS_ADVISORY_FORMAT_TOKEN) 85 | 86 | def test_constants_unchanged_ips_signature_label(self): 87 | self.assertEqual(constants.IPS_SIGNATURE_LABEL, IPS_SIGNATURE_LABEL) 88 | 89 | def test_constants_api_labels_is_non_empty_tuple(self): 90 | self.assertTrue(isinstance(constants.API_LABELS, tuple)) 91 | self.assertTrue(constants.API_LABELS) 92 | 93 | def test_constants_unique_api_labels(self): 94 | api_labels = sorted(constants.API_LABELS) 95 | api_labels_unique = sorted(set(api_labels)) 96 | self.assertEqual(api_labels, api_labels_unique) 97 | -------------------------------------------------------------------------------- /openVulnQuery/_library/main.py: -------------------------------------------------------------------------------- 1 | from . import cli_api 2 | from . import config 3 | from . import constants 4 | from . import query_client 5 | from . import utils 6 | 7 | 8 | def filter_or_aggregate(advisories, fields=None, count=None): 9 | """Post process depending on requested fields and count aggregation.""" 10 | if fields: 11 | if count: 12 | return [utils.count_fields(advisories, fields)] 13 | else: 14 | return utils.filter_advisories(advisories, fields) 15 | else: 16 | return utils.filter_advisories(advisories, constants.API_LABELS) 17 | 18 | 19 | def filter_config(resource, first_pub_pair=None, last_pub_pair=None, platformAlias=None): 20 | """Provide rule conforming filter config from request of filters and API 21 | resource(s) requested.""" 22 | 23 | if resource in constants.ALLOWS_FILTER: 24 | # Process eventual filter parameters: 25 | if first_pub_pair: 26 | a_filter = query_client.TemporalFilter( 27 | query_client.PUBLISHED_FIRST, *first_pub_pair) 28 | elif last_pub_pair: 29 | a_filter = query_client.TemporalFilter( 30 | query_client.PUBLISHED_LAST, *last_pub_pair) 31 | else: 32 | a_filter = query_client.Filter() 33 | elif resource in constants.SUPPORTED_PLATFORMS_ALIAS: 34 | if platformAlias: 35 | a_filter = query_client.OptionalParameters( 36 | query_client.PLATFORMALIAS, *platformAlias) 37 | else: # Default is 'empty' filter 38 | a_filter = None 39 | else: 40 | a_filter = None 41 | 42 | return {'a_filter': a_filter} 43 | 44 | def main(string_list=None): 45 | args = cli_api.process_command_line(string_list) 46 | api_resource_key, api_resource_value = args.api_resource 47 | 48 | topic = api_resource_key 49 | 50 | if api_resource_key == constants.IOS_ADVISORY_FORMAT_TOKEN: 51 | adv_format = constants.IOS_ADVISORY_FORMAT_TOKEN 52 | elif api_resource_key == constants.IOSXE_ADVISORY_FORMAT_TOKEN: 53 | adv_format = constants.IOS_ADVISORY_FORMAT_TOKEN 54 | else: 55 | adv_format = constants.DEFAULT_ADVISORY_FORMAT_TOKEN 56 | 57 | f_cfg = filter_config( 58 | api_resource_key, args.first_published, args.last_published, args.platformAlias) 59 | 60 | client_cfg = { 61 | 'client_id': config.CLIENT_ID, 62 | 'client_secret': config.CLIENT_SECRET 63 | } 64 | if args.user_agent: 65 | client_cfg['user_agent'] = args.user_agent 66 | 67 | client = query_client.OpenVulnQueryClient(**client_cfg) 68 | 69 | if api_resource_key == 'OS': 70 | # Retrieve version information regarding the different Network Operating Systems. 71 | # No filtering supported 72 | OS_versions = client.get_by(topic, adv_format, api_resource_value, **f_cfg) 73 | output_format, file_path = args.output_format 74 | 75 | with utils.get_output_filehandle(file_path) as f: 76 | utils.output(OS_versions, output_format, f) 77 | 78 | elif api_resource_key == 'platform': 79 | # Retrieve platform alias information regarding the different Network Operating Systems. 80 | # No filtering supported 81 | platform_aliases = client.get_by(topic, adv_format, api_resource_value, **f_cfg) 82 | output_format, file_path = args.output_format 83 | 84 | with utils.get_output_filehandle(file_path) as f: 85 | utils.output(platform_aliases, output_format, f) 86 | 87 | else: 88 | advisories = client.get_by(topic, adv_format, api_resource_value, **f_cfg) 89 | 90 | output_format, file_path = args.output_format 91 | 92 | with utils.get_output_filehandle(file_path) as f: 93 | utils.output(filter_or_aggregate(advisories, args.fields, args.count), 94 | output_format, f) 95 | -------------------------------------------------------------------------------- /openVulnQuery/_library/utils.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import json 3 | import sys 4 | 5 | from . import constants 6 | from ._compatibility import is_unicode_or_bytes 7 | 8 | TAB = u'\t' 9 | IPS_SIG = constants.IPS_SIGNATURE_LABEL 10 | PLATFORMS_TAG = constants.PLATFORMS_LABEL 11 | 12 | def filter_advisories(advisories, fields): 13 | """Filter the advisories per some criteria ... 14 | 15 | :param advisories: An iterable with advisory entries 16 | :param fields: The requested fields (TODO reverse documentation?) 17 | :return list of advisories passing filter criteria 18 | """ 19 | is_nested_ips_signature_field = any( 20 | field in constants.IPS_SIGNATURES for field in fields) 21 | 22 | is_nested_platforms_field = any( 23 | field in constants.PLATFORMS for field in fields) 24 | 25 | filter_fields = list(fields) 26 | 27 | if is_nested_ips_signature_field: 28 | filter_fields.append(IPS_SIG) 29 | elif IPS_SIG in fields: # and not is_nested_ips_signature_field: 30 | filter_fields.extend(constants.IPS_SIGNATURES) 31 | 32 | if is_nested_platforms_field: 33 | filter_fields.append(PLATFORMS_TAG) 34 | elif PLATFORMS_TAG in fields: 35 | filter_fields.extend(constants.PLATFORMS) 36 | 37 | return [adv.filter(*filter_fields) for adv in advisories] 38 | 39 | 40 | def count_fields(advisories, fields): 41 | """Counter dict from fields over all advisories. """ 42 | counts = dict.fromkeys(fields, 0) 43 | for adv in advisories: 44 | for field in fields: 45 | if hasattr(adv, field): 46 | counts[field] += get_count(getattr(adv, field)) 47 | return counts 48 | 49 | 50 | def get_count(advisory_field): 51 | """Count of the number of valid items in a particular advisory field.""" 52 | 53 | count = 0 54 | if is_unicode_or_bytes(advisory_field): 55 | if advisory_field != constants.NA_INDICATOR: 56 | count = 1 57 | else: 58 | for item in advisory_field: 59 | if item != constants.NA_INDICATOR: 60 | count += 1 61 | return count 62 | 63 | 64 | def output(advisories, output_format, file_handle): 65 | """Write data in CSV or JSON to file_handle . 66 | 67 | :param advisories: List of advisory objects. 68 | :param output_format: Either set as csv or json or use default stdout. 69 | :param file_handle: The path to put the csv or json file with a file name 70 | or stdout if no path or filename. 71 | """ 72 | if output_format == constants.JSON_OUTPUT_FORMAT_TOKEN: 73 | _to_json(advisories, file_handle) 74 | elif output_format == constants.CSV_OUTPUT_FORMAT_TOKEN: 75 | _to_csv(advisories, file_handle, delimiter=",") 76 | 77 | 78 | def _to_json(advisory_list, file_handle): 79 | """Write json to a file""" 80 | file_handle.write(json.dumps(advisory_list, sort_keys=True, indent=4)) 81 | 82 | 83 | def _to_csv(advisory_list, file_handle, delimiter): 84 | """Write csv to a file, with option to specify a delimiter""" 85 | 86 | flattened_advisory_list = flatten_list(advisory_list) 87 | header = _get_headers(flattened_advisory_list) 88 | if IPS_SIG in header: 89 | header.remove(IPS_SIG) 90 | 91 | w = csv.DictWriter(file_handle, header, delimiter=delimiter, 92 | extrasaction='ignore', restval=constants.NA_INDICATOR) 93 | w.writeheader() 94 | for advisory in flattened_advisory_list: 95 | w.writerow(advisory) 96 | 97 | 98 | def flatten_list(advisory_seq): 99 | """Flatten the structures returned as list.""" 100 | return [_flatten_datastructure(adv) for adv in advisory_seq] 101 | 102 | 103 | def _get_headers(advisories): 104 | """Return list of unique headers.""" 105 | return list(set(key for adv in advisories for key in adv.keys())) 106 | 107 | 108 | def _flatten_datastructure(field_list): 109 | final_dict = {} 110 | for k, v in field_list.items(): 111 | if isinstance(v, list): 112 | if v and isinstance(v[0], dict): 113 | final_dict.update(_reduce_list_dict(k, v)) 114 | else: 115 | final_dict[k] = TAB.join(v) 116 | else: 117 | final_dict[k] = v.encode('utf-8').strip() if v else None 118 | return final_dict 119 | 120 | 121 | def _reduce_list_dict(node, vals): 122 | keys = vals[0].keys() 123 | flattened_dict = {"%s_%s" % (node, k): "" for k in keys} 124 | for v in vals: 125 | for key in keys: 126 | separator = "" 127 | flattened_key = "%s_%s" % (node, key) 128 | if flattened_dict[flattened_key] != "": 129 | separator = "\t" 130 | upd_val = ( 131 | "%s%s%s" % (flattened_dict[flattened_key], separator, v[key])) 132 | flattened_dict[flattened_key] = upd_val 133 | return flattened_dict 134 | 135 | 136 | def get_output_filehandle(file_path=None): 137 | """File handle if file_path given else returns stdout handle""" 138 | return open(file_path, "w") if file_path else sys.stdout 139 | -------------------------------------------------------------------------------- /openVulnQuery/_library/advisory.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta 2 | 3 | from . import constants 4 | from ._compatibility import is_unicode_or_bytes 5 | 6 | NA = constants.NA_INDICATOR 7 | IPS_SIG = constants.IPS_SIGNATURE_LABEL 8 | PLATFORMS_TAG = constants.PLATFORMS_LABEL 9 | 10 | ADVISORIES_COMMONS_MAP = { 11 | 'advisory_id': "advisoryId", 12 | 'advisory_title': "advisoryTitle", 13 | 'bug_ids': "bugIDs", 14 | 'cves': "cves", 15 | 'cvss_base_score': "cvssBaseScore", 16 | 'cwe': "cwe", 17 | 'first_published': "firstPublished", 18 | 'last_updated': "lastUpdated", 19 | 'product_names': "productNames", 20 | 'publication_url': "publicationUrl", 21 | 'cvrfUrl': "cvrfUrl", 22 | 'csafUrl': "csafUrl", 23 | 'sir': "sir", 24 | 'summary': "summary", 25 | } 26 | 27 | IPS_SIG_MAP = { 28 | IPS_SIG: 'ipsSignatures', 29 | } 30 | 31 | IOS_ADD_ONS_MAP = { 32 | 'first_fixed': 'firstFixed', 33 | 'ios_release': 'iosRelease', 34 | } 35 | 36 | PLATFORMS_MAP = { 37 | PLATFORMS_TAG: 'platforms', 38 | } 39 | 40 | class Filterable(object): 41 | """Filterable mixin class""" 42 | 43 | def filter(self, *args): 44 | filtered_dict = {} 45 | for arg in args: 46 | if hasattr(self, arg): 47 | attr = getattr(self, arg) 48 | if (isinstance(attr, list) and 49 | attr and 50 | isinstance(attr[0], Filterable)): 51 | filtered_dict[arg] = [a.filter(*args) for a in attr] 52 | else: 53 | filtered_dict[arg] = attr 54 | return filtered_dict 55 | 56 | 57 | class Advisory(Filterable): 58 | """ 59 | This class abstracts advisory object 60 | """ 61 | 62 | __metaclass__ = ABCMeta 63 | 64 | def __init__(self, advisory_id, sir, first_published, last_updated, cves, 65 | bug_ids, cvss_base_score, advisory_title, publication_url, 66 | cwe, cvrfUrl, csafUrl, product_names, summary): 67 | self.advisory_id = advisory_id 68 | self.sir = sir 69 | self.first_published = first_published 70 | self.last_updated = last_updated 71 | self.cves = cves 72 | self.bug_ids = bug_ids 73 | self.cvss_base_score = cvss_base_score 74 | self.advisory_title = advisory_title 75 | self.publication_url = publication_url 76 | self.cwe = cwe 77 | self.cvrfUrl = cvrfUrl 78 | self.csafUrl = csafUrl 79 | self.product_names = product_names 80 | self.summary = summary 81 | 82 | class AdvisoryDefault(Advisory): 83 | """Default object inherits from Advisory""" 84 | 85 | def __init__(self, *args, **kwargs): 86 | self.ips_signatures = [] 87 | if IPS_SIG in kwargs: 88 | self.ips_signatures = [ 89 | IPSSignature(**kw) if not is_unicode_or_bytes(kw) else NA 90 | for kw in kwargs.pop(IPS_SIG)] 91 | 92 | self.platforms = [] 93 | if 'platforms' in kwargs: 94 | self.platforms = [ 95 | platformsList(**kw) if not is_unicode_or_bytes(kw) else NA 96 | for kw in kwargs.pop('platforms')] 97 | 98 | super(AdvisoryDefault, self).__init__(*args, **kwargs) 99 | 100 | class AdvisoryIOS(Advisory): 101 | """Advisory Object with additional information on IOS/IOSXE version """ 102 | 103 | def __init__(self, *args, **kwargs): 104 | self.ips_signatures = [] 105 | if IPS_SIG in kwargs: 106 | self.ips_signatures = [ 107 | IPSSignature(**kw) if not is_unicode_or_bytes(kw) else NA 108 | for kw in kwargs.pop(IPS_SIG)] 109 | 110 | self.first_fixed = kwargs.pop('first_fixed', None) 111 | self.ios_release = kwargs.pop('ios_release', None) 112 | super(AdvisoryIOS, self).__init__(*args, **kwargs) 113 | 114 | class IPSSignature(Filterable): 115 | def __init__(self, legacyIpsId, releaseVersion, softwareVersion, 116 | legacyIpsUrl): 117 | self.legacy_ips_id = legacyIpsId 118 | self.release_version = releaseVersion 119 | self.software_version = softwareVersion 120 | self.legacy_ips_url = legacyIpsUrl 121 | 122 | class platformsList(Filterable): 123 | def __init__(self, id, name, firstFixes, vulnerabilityState): 124 | self.id = id 125 | self.name = name 126 | self.firstFixes = firstFixes 127 | self.vulnerabilityState = vulnerabilityState 128 | 129 | def advisory_format_factory_map(): 130 | """Map the advisory format tokens to callable instantiators.""" 131 | return dict(zip( 132 | constants.ADVISORY_FORMAT_TOKENS, (AdvisoryDefault, AdvisoryIOS))) 133 | 134 | def advisory_factory(adv_data, adv_format, logger): 135 | """Converts json into a list of advisory objects. 136 | :param adv_data: A dictionary describing an advisory. 137 | :param adv_format: The target format in ('default', 'ios') 138 | :param logger: A logger (for now expecting to be ready to log) 139 | :returns advisory instance according to adv_format 140 | """ 141 | 142 | adv_map = {} # Initial fill from shared common model key map: 143 | for k, v in ADVISORIES_COMMONS_MAP.items(): 144 | adv_map[k] = adv_data[v] 145 | 146 | for k, v in IPS_SIG_MAP.items(): 147 | adv_map[k] = adv_data[v] 148 | 149 | for k, v in PLATFORMS_MAP.items(): 150 | if v in adv_data: 151 | adv_map[k] = adv_data[v] 152 | 153 | if adv_format == constants.IOS_ADVISORY_FORMAT_TOKEN: 154 | for k, v in IOS_ADD_ONS_MAP.items(): 155 | if v in adv_data: 156 | adv_map[k] = adv_data[v] 157 | 158 | an_adv = advisory_format_factory_map()[adv_format](**adv_map) 159 | logger.debug( 160 | "{} Advisory {} Created".format(adv_format, an_adv.advisory_id)) 161 | 162 | return an_adv 163 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import mock 3 | import sys 4 | import json 5 | import pytest 6 | from openVulnQuery import constants 7 | from openVulnQuery import utils 8 | from openVulnQuery import advisory 9 | 10 | NA = constants.NA_INDICATOR 11 | CSV_OUTPUT_FORMAT_TOKEN = constants.CSV_OUTPUT_FORMAT_TOKEN 12 | JSON_OUTPUT_FORMAT_TOKEN = constants.JSON_OUTPUT_FORMAT_TOKEN 13 | 14 | mock_advisory_title = "Mock Advisory Title" 15 | adv_cfg = { 16 | 'advisory_id': "Cisco-SA-20111107-CVE-2011-0941", 17 | 'sir': "Medium", 18 | 'first_published': "2011-11-07T21:36:55+0000", 19 | 'last_updated': "2011-11-07T21:36:55+0000", 20 | 'cves': ["CVE-2011-0941", NA], 21 | 'cvrf_url': ( 22 | "http://tools.cisco.com/security/center/contentxml/" 23 | "CiscoSecurityAdvisory/Cisco-SA-20111107-CVE-2011-0941/cvrf/" 24 | "Cisco-SA-20111107-CVE-2011-0941_cvrf.xml"), 25 | 'bug_ids': "BUGISidf", 26 | 'cvss_base_score': "7.0", 27 | 'advisory_title': "{}".format(mock_advisory_title), 28 | 'publication_url': "https://tools.cisco.com/mockurl", 29 | 'cwe': NA, 30 | 'product_names': ["product_name_1", "product_name_2"], 31 | 'summary': "This is summary" 32 | } 33 | mock_advisory = advisory.CVRF(**adv_cfg) 34 | mock_advisories = [mock_advisory] 35 | 36 | 37 | class UtilsTest(unittest.TestCase): 38 | 39 | def test_filter_advisories_general_input(self): 40 | fields = ["advisory_title", "sir", "bug_ids"] 41 | expected_output = [ 42 | {"advisory_title": "Mock Advisory Title", 43 | "sir": "Medium", 44 | "bug_ids": "BUGISidf"}] 45 | output = utils.filter_advisories(mock_advisories, fields) 46 | self.assertListEqual(output, expected_output) 47 | 48 | def test_filter_advisories_empty_fields(self): 49 | fields = [] 50 | expected_output = [{}] 51 | self.assertListEqual( 52 | utils.filter_advisories(mock_advisories, fields), expected_output) 53 | 54 | def test_filter_advisories_invalid_fields(self): 55 | fields = ["advisory_title", "v_score"] 56 | expected_output = [{'advisory_title': '%s' % mock_advisory_title}] 57 | output = utils.filter_advisories(mock_advisories, fields) 58 | self.assertIsInstance(output, list) 59 | self.assertDictEqual(output[0], expected_output[0]) 60 | 61 | def test_filter_advisories_ips_sig_fields(self): 62 | fields = ["advisory_title", constants.IPS_SIGNATURE_LABEL] 63 | expected_output = [ 64 | { 65 | 'advisory_title': '%s' % mock_advisory_title, 66 | constants.IPS_SIGNATURE_LABEL: [], 67 | } 68 | ] 69 | output = utils.filter_advisories(mock_advisories, fields) 70 | self.assertIsInstance(output, list) 71 | self.assertDictEqual(output[0], expected_output[0]) 72 | 73 | def test_filter_advisories_ips_sig_fields_nested(self): 74 | fields = ["advisory_title"] + list(constants.IPS_SIGNATURES) 75 | expected_output = [ 76 | { 77 | 'advisory_title': '%s' % mock_advisory_title, 78 | constants.IPS_SIGNATURE_LABEL: [], 79 | } 80 | ] 81 | output = utils.filter_advisories(mock_advisories, fields) 82 | self.assertIsInstance(output, list) 83 | self.assertDictEqual(output[0], expected_output[0]) 84 | 85 | def test_count_fields_valid_input(self): 86 | fields = ["bug_ids", "advisory_title"] 87 | expected_output = {'bug_ids': 1, 'advisory_title': 1} 88 | output = utils.count_fields(mock_advisories, fields) 89 | self.assertDictEqual(output, expected_output) 90 | 91 | def test_count_fields_invalid_input(self): 92 | fields = ["bug_ids", "v_score"] 93 | expected_output = {'bug_ids': 1, 'v_score': 0} 94 | output = utils.count_fields(mock_advisories, fields) 95 | self.assertDictEqual(output, expected_output) 96 | 97 | def test_get_count_valid(self): 98 | self.assertEqual(utils.get_count( 99 | getattr(mock_advisory, "advisory_title")), 1) 100 | self.assertEqual(utils.get_count( 101 | getattr(mock_advisory, "product_names")), 2) 102 | 103 | def test_get_count_fields_with_NA(self): 104 | self.assertEqual(utils.get_count(getattr(mock_advisory, "cwe")), 0) 105 | self.assertEqual(utils.get_count(getattr(mock_advisory, "cves")), 1) 106 | 107 | def test_flatten_list_void_pair(self): 108 | naive_void = [{}, {}] 109 | self.assertListEqual(utils.flatten_list(naive_void), naive_void) 110 | 111 | def test_flatten_list_distinct_pair(self): 112 | naive_distinct = [{'foo': 'bar'}, {'baz': ''}] 113 | naive_distinct_side_effected = [{'foo': b'bar'}, {'baz': None}] 114 | self.assertListEqual( 115 | utils.flatten_list(naive_distinct), naive_distinct_side_effected) 116 | 117 | # def test_flatten_list_distinct_nested_pair(self): 118 | # naive_distinct = [{'foo': 'bar'}, {'baz': {'yes': 'no'}}] 119 | # naive_distinct_side_effected = [{'foo': 'bar'}, {'baz': None}] 120 | # self.assertListEqual( 121 | # utils.flatten_list(naive_distinct), naive_distinct_side_effected) 122 | # 123 | # def test_private_reduce_list_dict(self): 124 | # k = 'no' 125 | # v = [{'no': {'foo': 'bar', 'baz': 'yes'}}, 126 | # {'no': {'foo': 'bar', 'baz': 'yes'}}, 127 | # {'yes': {'foo': 'bar', 'baz': 'maybe'}}] 128 | # whatever = [] 129 | # self.assertListEqual( 130 | # utils._reduce_list_dict(k, v), whatever) 131 | 132 | def test_private_get_headers_distinct(self): 133 | naive_distinct = [{'foo': 'bar'}, {'baz': ''}] 134 | expected = ['baz', 'foo'] 135 | self.assertListEqual( 136 | sorted(utils._get_headers(naive_distinct)), expected) 137 | 138 | @mock.patch("openVulnQuery.utils.open", create=True) 139 | def test_get_output_filehandle(self, mock_open): 140 | mock_open.side_effect = [ 141 | mock.mock_open(read_data='name').return_value, 142 | mock.mock_open(read_data='empty').return_value, 143 | ] 144 | 145 | self.assertTrue(utils.get_output_filehandle('foo')) 146 | mock_open.assert_called_once_with('foo', 'w') 147 | mock_open.reset_mock() 148 | 149 | self.assertEqual(sys.stdout, utils.get_output_filehandle(None)) 150 | mock_open.assert_not_called() 151 | 152 | @mock.patch("openVulnQuery.utils.open", create=True) 153 | def test_output_trial(self, mock_open): 154 | filtered_advisories = utils.filter_advisories( 155 | mock_advisories, constants.API_LABELS) 156 | s_rep = json.dumps(filtered_advisories) 157 | mock_open.side_effect = [ 158 | mock.mock_open(read_data=s_rep).return_value, 159 | ] 160 | self.assertIsNone(utils.output( 161 | filtered_advisories, JSON_OUTPUT_FORMAT_TOKEN, mock_open)) 162 | 163 | @mock.patch("openVulnQuery.utils.open", create=True) 164 | def test_output_csv_succeeds(self, mock_open): 165 | s_empty = ',\n,' 166 | mock_open.side_effect = [ 167 | mock.mock_open(read_data=s_empty).return_value, 168 | ] 169 | filtered_advisories = utils.filter_advisories( 170 | mock_advisories, constants.API_LABELS) 171 | self.assertIsNone(utils.output( 172 | filtered_advisories, CSV_OUTPUT_FORMAT_TOKEN, mock_open)) -------------------------------------------------------------------------------- /tests/test_advisory.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from openVulnQuery import advisory 3 | from openVulnQuery import constants 4 | 5 | NA = constants.NA_INDICATOR 6 | IPS_SIG = constants.IPS_SIGNATURE_LABEL 7 | mock_advisory_title = "Mock Advisory Title" 8 | adv_cfg = { 9 | 'advisory_id': "Cisco-SA-20111107-CVE-2011-0941", 10 | 'sir': "Medium", 11 | 'first_published': "2011-11-07T21:36:55+0000", 12 | 'last_updated': "2011-11-07T21:36:55+0000", 13 | 'cves': ["CVE-2011-0941", NA], 14 | 'cvrf_url': ( 15 | "http://tools.cisco.com/security/center/contentxml/" 16 | "CiscoSecurityAdvisory/Cisco-SA-20111107-CVE-2011-0941/cvrf/" 17 | "Cisco-SA-20111107-CVE-2011-0941_cvrf.xml"), 18 | 'bug_ids': "BUGISidf", 19 | 'cvss_base_score': "7.0", 20 | 'advisory_title': "{}".format(mock_advisory_title), 21 | 'publication_url': "https://tools.cisco.com/mockurl", 22 | 'cwe': NA, 23 | 'product_names': ["product_name_1", "product_name_2"], 24 | 'summary': "This is summary" 25 | } 26 | mock_advisory = advisory.CVRF(**adv_cfg) 27 | mock_advisories = [mock_advisory] 28 | 29 | 30 | class MockLogger(object): 31 | def debug(self, *args, **kwargs): 32 | pass 33 | 34 | 35 | class AdvisoryTest(unittest.TestCase): 36 | def test_advisory_unchanged_na(self): 37 | self.assertEquals(advisory.NA, NA) 38 | 39 | def test_advisory_ips_sig_map_key_unchanged(self): 40 | self.assertTrue(IPS_SIG in advisory.IPS_SIG_MAP) 41 | 42 | def test_advisory_filterable_succeeds(self): 43 | self.assertTrue(advisory.Filterable()) 44 | 45 | def test_advisory_advisory_succeeds(self): 46 | adv_map = {} 47 | for k, v in advisory.ADVISORIES_COMMONS_MAP.items(): 48 | adv_map[k] = NA 49 | self.assertTrue(advisory.Advisory(**adv_map)) 50 | 51 | def test_advisory_cvrf_succeeds(self): 52 | adv_map = {} 53 | for k, v in advisory.ADVISORIES_COMMONS_MAP.items(): 54 | adv_map[k] = NA 55 | self.assertTrue(advisory.CVRF(**adv_map)) 56 | 57 | def test_advisory_oval_succeeds(self): 58 | adv_map = {} 59 | for k, v in advisory.ADVISORIES_COMMONS_MAP.items(): 60 | adv_map[k] = NA 61 | self.assertTrue(advisory.OVAL(**adv_map)) 62 | 63 | def test_advisory_advisoryios_succeeds(self): 64 | adv_map = {} 65 | for k, v in advisory.ADVISORIES_COMMONS_MAP.items(): 66 | adv_map[k] = NA 67 | self.assertTrue(advisory.AdvisoryIOS(**adv_map)) 68 | 69 | def test_advisory_ipssignature_succeeds(self): 70 | self.assertTrue(advisory.IPSSignature(*('',) * 4)) 71 | 72 | def test_advisory_cvrf_with_ips_sig_succeeds(self): 73 | adv_map = {IPS_SIG: ''} 74 | for k, v in advisory.ADVISORIES_COMMONS_MAP.items(): 75 | adv_map[k] = NA 76 | self.assertTrue(advisory.CVRF(**adv_map)) 77 | 78 | def test_advisory_oval_with_ips_sig_succeeds(self): 79 | adv_map = {IPS_SIG: ''} 80 | for k, v in advisory.ADVISORIES_COMMONS_MAP.items(): 81 | adv_map[k] = NA 82 | self.assertTrue(advisory.OVAL(**adv_map)) 83 | 84 | def test_advisory_format_factory_map_succeeds(self): 85 | self.assertTrue(advisory.advisory_format_factory_map()) 86 | 87 | def test_advisory_unchanged_format_factory_map(self): 88 | frozen = dict( 89 | zip(constants.ADVISORY_FORMAT_TOKENS, 90 | (advisory.CVRF, advisory.OVAL, advisory.AdvisoryIOS))) 91 | self.assertDictEqual(advisory.advisory_format_factory_map(), frozen) 92 | 93 | def test_advisory_advisory_factory_cvrf_wrong_format(self): 94 | adv_map = {} 95 | for k, v in advisory.ADVISORIES_COMMONS_MAP.items(): 96 | adv_map[v] = k 97 | self.assertRaises(ValueError, advisory.advisory_factory, 98 | adv_map, 'this_is_an_unknown_format_token', None) 99 | 100 | def test_advisory_advisory_factory_cvrf_missing_key_cvrf_url(self): 101 | adv_map = {} 102 | for k, v in advisory.ADVISORIES_COMMONS_MAP.items(): 103 | adv_map[v] = k 104 | self.assertRaises(KeyError, advisory.advisory_factory, 105 | adv_map, constants.CVRF_ADVISORY_FORMAT_TOKEN, None) 106 | 107 | def test_advisory_advisory_factory_oval_missing_key_oval_url(self): 108 | adv_map = {} 109 | for k, v in advisory.ADVISORIES_COMMONS_MAP.items(): 110 | adv_map[v] = k 111 | self.assertRaises(KeyError, advisory.advisory_factory, 112 | adv_map, constants.OVAL_ADVISORY_FORMAT_TOKEN, None) 113 | 114 | def test_advisory_advisory_factory_advisoryios_missing_key_oval_url(self): 115 | adv_map = {} 116 | for k, v in advisory.ADVISORIES_COMMONS_MAP.items(): 117 | adv_map[v] = k 118 | self.assertRaises(KeyError, advisory.advisory_factory, 119 | adv_map, constants.IOS_ADVISORY_FORMAT_TOKEN, None) 120 | 121 | def test_advisory_advisory_factory_cvrf_with_key_cvrf_url_miss_ips(self): 122 | adv_map = {advisory.CVRF_URL_MAP[advisory.CVRF_URL_TOKEN]: ''} 123 | for k, v in advisory.ADVISORIES_COMMONS_MAP.items(): 124 | adv_map[v] = k 125 | self.assertRaises(KeyError, advisory.advisory_factory, 126 | adv_map, constants.CVRF_ADVISORY_FORMAT_TOKEN, None) 127 | 128 | def test_advisory_advisory_factory_oval_with_key_oval_url_miss_ips(self): 129 | adv_map = {constants.OVAL_ADVISORY_FORMAT_TOKEN: ''} 130 | for k, v in advisory.ADVISORIES_COMMONS_MAP.items(): 131 | adv_map[v] = k 132 | self.assertRaises(KeyError, advisory.advisory_factory, 133 | adv_map, constants.OVAL_ADVISORY_FORMAT_TOKEN, None) 134 | 135 | def test_advisory_advisory_factory_oval_with_key_oval_miss_ips(self): 136 | adv_map = {advisory.OVAL_URL_MAP[advisory.OVAL_URL_TOKEN]: ''} 137 | for k, v in advisory.ADVISORIES_COMMONS_MAP.items(): 138 | adv_map[v] = k 139 | self.assertRaises(KeyError, advisory.advisory_factory, 140 | adv_map, constants.OVAL_ADVISORY_FORMAT_TOKEN, None) 141 | 142 | def test_advisory_advisory_factory_cvrf_with_key_cvrf_url_and_ips(self): 143 | adv_map = { 144 | advisory.CVRF_URL_MAP[advisory.CVRF_URL_TOKEN]: '', 145 | advisory.IPS_SIG_MAP[advisory.IPS_SIG]: '', 146 | } 147 | for k, v in advisory.ADVISORIES_COMMONS_MAP.items(): 148 | adv_map[v] = k 149 | self.assertTrue(advisory.advisory_factory( 150 | adv_map, 151 | constants.CVRF_ADVISORY_FORMAT_TOKEN, 152 | MockLogger())) 153 | 154 | def test_advisory_advisory_factory_oval_with_key_oval_url_and_ips(self): 155 | adv_map = { 156 | advisory.OVAL_URL_MAP[advisory.OVAL_URL_TOKEN]: '', 157 | advisory.IPS_SIG_MAP[advisory.IPS_SIG]: '', 158 | } 159 | for k, v in advisory.ADVISORIES_COMMONS_MAP.items(): 160 | adv_map[v] = k 161 | self.assertTrue(advisory.advisory_factory( 162 | adv_map, 163 | constants.OVAL_ADVISORY_FORMAT_TOKEN, 164 | MockLogger())) 165 | 166 | def test_advisory_advisory_factory_advisoryios_with_key_oval_url_and_rel( 167 | self): 168 | adv_map = { 169 | advisory.OVAL_URL_MAP[advisory.OVAL_URL_TOKEN]: '', 170 | advisory.IOS_ADD_ONS_MAP['first_fixed']: '', 171 | advisory.IOS_ADD_ONS_MAP['ios_release']: '', 172 | } 173 | for k, v in advisory.ADVISORIES_COMMONS_MAP.items(): 174 | adv_map[v] = k 175 | self.assertTrue(advisory.advisory_factory( 176 | adv_map, 177 | constants.IOS_ADVISORY_FORMAT_TOKEN, 178 | MockLogger())) 179 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openVulnQuery import advisory 4 | from openVulnQuery import config 5 | from openVulnQuery import constants 6 | from openVulnQuery import main 7 | 8 | DATE_SEP_TOKEN = ':' 9 | BAD_REQUEST_TOKEN_URL = "https://example.com/as/token.oauth2" 10 | REQUEST_TOKEN_URL = config.REQUEST_TOKEN_URL 11 | 12 | NA = constants.NA_INDICATOR 13 | CVRF_TOKEN = constants.CVRF_ADVISORY_FORMAT_TOKEN 14 | OVAL_TOKEN = constants.OVAL_ADVISORY_FORMAT_TOKEN 15 | IOS_TOKEN = constants.IOS_ADVISORY_FORMAT_TOKEN 16 | 17 | mock_advisory_title = "Mock Advisory Title" 18 | adv_cfg = { 19 | 'advisory_id': "Cisco-SA-20111107-CVE-2011-0941", 20 | 'sir': "Medium", 21 | 'first_published': "2011-11-07T21:36:55+0000", 22 | 'last_updated': "2011-11-07T21:36:55+0000", 23 | 'cves': ["CVE-2011-0941", NA], 24 | 'cvrf_url': ( 25 | "http://tools.cisco.com/security/center/contentxml/" 26 | "CiscoSecurityAdvisory/Cisco-SA-20111107-CVE-2011-0941/cvrf/" 27 | "Cisco-SA-20111107-CVE-2011-0941_cvrf.xml"), 28 | 'bug_ids': "BUGISidf", 29 | 'cvss_base_score': "7.0", 30 | 'advisory_title': "{}".format(mock_advisory_title), 31 | 'publication_url': "https://tools.cisco.com/mockurl", 32 | 'cwe': NA, 33 | 'product_names': ["product_name_1", "product_name_2"], 34 | 'summary': "This is summary" 35 | } 36 | mock_advisory = advisory.CVRF(**adv_cfg) 37 | mock_advisories = [mock_advisory] 38 | 39 | 40 | class MainTest(unittest.TestCase): 41 | def test_main_smoke_user_error(self): 42 | with self.assertRaises(SystemExit) as e: 43 | main.main() 44 | self.assertEqual(e.exception.code, 2) 45 | 46 | def test_main_smoke_help(self): 47 | with self.assertRaises(SystemExit) as e: 48 | main.main(['--help']) 49 | self.assertEqual(e.exception.code, 0) 50 | 51 | def test_main_smoke_help_short(self): 52 | with self.assertRaises(SystemExit) as e: 53 | main.main(['-h']) 54 | self.assertEqual(e.exception.code, 0) 55 | 56 | def test_main_missing_adv_format(self): 57 | string_list = '--severity critical'.split() 58 | with self.assertRaises(SystemExit) as e: 59 | main.main(string_list) 60 | self.assertEqual(e.exception.code, 2) 61 | 62 | def test_main_unknown_adv_format(self): 63 | string_list = '--unknown'.split() 64 | with self.assertRaises(SystemExit) as e: 65 | main.main(string_list) 66 | self.assertEqual(e.exception.code, 2) 67 | 68 | def test_main_filter_forbidden_by_api(self): 69 | se = '2017-06-20', '2017-06-21' 70 | s_e = DATE_SEP_TOKEN.join(se) 71 | string_list = ( 72 | '--cvrf --product foo --first_published {}'.format(s_e).split()) 73 | with self.assertRaises(SystemExit) as e: 74 | main.main(string_list) 75 | self.assertEqual(e.exception.code, 2) 76 | 77 | def test_main_filter_date_format_end_missing(self): 78 | se = '2017-06-20', '' 79 | s_e = DATE_SEP_TOKEN.join(se) 80 | string_list = ( 81 | '--cvrf --product foo --first_published {}'.format(s_e).split()) 82 | with self.assertRaises(SystemExit) as e: 83 | main.main(string_list) 84 | self.assertEqual(e.exception.code, 2) 85 | 86 | def test_main_filter_date_format_sep_bad(self): 87 | se = '2017-06-20', '2017-06-21' 88 | s_e = 'P'.join(se) 89 | string_list = ( 90 | '--cvrf --product foo --first_published {}'.format(s_e).split()) 91 | with self.assertRaises(SystemExit) as e: 92 | main.main(string_list) 93 | self.assertEqual(e.exception.code, 2) 94 | 95 | def test_main_filter_date_format_end_bad(self): 96 | se = '2017-06-20', '2017-06-00' 97 | s_e = DATE_SEP_TOKEN.join(se) 98 | string_list = ( 99 | '--cvrf --product foo --first_published {}'.format(s_e).split()) 100 | with self.assertRaises(SystemExit) as e: 101 | main.main(string_list) 102 | self.assertEqual(e.exception.code, 2) 103 | 104 | def test_main_ios_severity_fails(self): 105 | string_list = '--ios \'15.5(2)T1\' --severity ontologicalyoff'.split() 106 | with self.assertRaises(SystemExit) as e: 107 | main.main(string_list) 108 | self.assertEqual(e.exception.code, 2) 109 | 110 | def test_main_ios_product_fails(self): 111 | string_list = '--ios \'15.5(2)T1\' --product ontologicalyoff'.split() 112 | with self.assertRaises(SystemExit) as e: 113 | main.main(string_list) 114 | self.assertEqual(e.exception.code, 2) 115 | 116 | def test_main_ios_cve_fails(self): 117 | string_list = '--ios \'15.5(2)T1\' --cve CVE-2017'.split() 118 | with self.assertRaises(SystemExit) as e: 119 | main.main(string_list) 120 | self.assertEqual(e.exception.code, 2) 121 | 122 | def test_main_ios_latest_fails(self): 123 | string_list = '--ios \'15.5(2)T1\' --latest 42'.split() 124 | with self.assertRaises(SystemExit) as e: 125 | main.main(string_list) 126 | self.assertEqual(e.exception.code, 2) 127 | 128 | def test_main_ios_advisory_fails(self): 129 | string_list = '--ios \'15.5(2)T1\' --advisory ontologicalyoff'.split() 130 | with self.assertRaises(SystemExit) as e: 131 | main.main(string_list) 132 | self.assertEqual(e.exception.code, 2) 133 | 134 | def test_main_ios_all_fails(self): 135 | string_list = '--ios \'15.5(2)T1\' --all'.split() 136 | with self.assertRaises(SystemExit) as e: 137 | main.main(string_list) 138 | self.assertEqual(e.exception.code, 2) 139 | 140 | def test_main_ios_year_fails(self): 141 | string_list = '--ios \'15.5(2)T1\' --year 2017'.split() 142 | with self.assertRaises(SystemExit) as e: 143 | main.main(string_list) 144 | self.assertEqual(e.exception.code, 2) 145 | 146 | def test_main_ios_unknown_out_format_fails(self): 147 | string_list = '--ios \'15.5(2)T1\' --yaml so-what.yaml'.split() 148 | with self.assertRaises(SystemExit) as e: 149 | main.main(string_list) 150 | self.assertEqual(e.exception.code, 2) 151 | 152 | def test_main_cvrf_version_fails(self): 153 | string_list = '--cvrf --version 15.5'.split() 154 | with self.assertRaises(SystemExit) as e: 155 | main.main(string_list) 156 | self.assertEqual(e.exception.code, 2) 157 | 158 | def test_main_cvrf_all_fails_wrong_token_url(self): 159 | string_list = '--cvrf --all'.split() 160 | config.REQUEST_TOKEN_URL = BAD_REQUEST_TOKEN_URL 161 | self.assertRaises(Exception, main.main, string_list) 162 | 163 | def test_main_advisory_format_from_call_cvrf(self): 164 | self.assertEqual( 165 | main.advisory_format_from_call(CVRF_TOKEN), CVRF_TOKEN) 166 | 167 | def test_main_advisory_format_from_call_oval(self): 168 | self.assertEqual( 169 | main.advisory_format_from_call(OVAL_TOKEN), OVAL_TOKEN) 170 | 171 | def test_main_advisory_format_from_call_none(self): 172 | self.assertEqual( 173 | main.advisory_format_from_call(None), IOS_TOKEN) 174 | 175 | def test_main_advisory_format_from_call_false(self): 176 | self.assertEqual( 177 | main.advisory_format_from_call(False), IOS_TOKEN) 178 | 179 | def test_main_advisory_format_from_call_true(self): 180 | self.assertEqual( 181 | main.advisory_format_from_call(True), IOS_TOKEN) 182 | 183 | def test_main_advisory_format_from_call_empty(self): 184 | self.assertEqual( 185 | main.advisory_format_from_call(''), IOS_TOKEN) 186 | 187 | def test_main_filter_or_aggregate_succeeds(self): 188 | self.assertTrue(main.filter_or_aggregate(mock_advisories, None, None)) 189 | 190 | def test_main_filter_or_aggregate_fields_succeeds(self): 191 | self.assertTrue(main.filter_or_aggregate(mock_advisories, ['sir'], '')) 192 | 193 | def test_main_filter_or_aggregate_fields_count_succeeds(self): 194 | self.assertTrue(main.filter_or_aggregate(mock_advisories, ['sir'], 42)) 195 | 196 | def test_main_filter_config_succeeds(self): 197 | self.assertTrue(main.filter_config('no_resource_matches', None, None)) 198 | 199 | def test_main_filter_config_all_succeeds(self): 200 | self.assertTrue(main.filter_config( 201 | constants.ALLOWS_FILTER[0], '', '')) 202 | 203 | def test_main_filter_config_severity_succeeds(self): 204 | self.assertTrue(main.filter_config( 205 | constants.ALLOWS_FILTER[-1], '', '')) 206 | 207 | def test_main_filter_config_all_first_succeeds(self): 208 | se = '2017-06-20', '2017-06-21' 209 | self.assertTrue(main.filter_config( 210 | constants.ALLOWS_FILTER[0], se, '')) 211 | 212 | def test_main_filter_config_all_last_succeeds(self): 213 | se = '2017-06-20', '2017-06-21' 214 | self.assertTrue(main.filter_config( 215 | constants.ALLOWS_FILTER[0], '', se)) 216 | -------------------------------------------------------------------------------- /tests/test_cli_api.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import unittest 3 | import os 4 | import inspect 5 | 6 | from openVulnQuery import config 7 | from openVulnQuery import cli_api 8 | 9 | DATE_PARSER_FORMAT = '%Y-%m-%d' 10 | DATE_SEP_TOKEN = ':' 11 | SAMPLE_CLIENT_ID = 'BadCodedBadCodedBadCoded' 12 | SAMPLE_CLIENT_SECRET = 'DeadFaceDeadFaceDeadFace' 13 | 14 | class CliApiTest(unittest.TestCase): 15 | def test_cli_api_unchanged_date_sep_token(self): 16 | se = '2017-06-20', '2017-06-21' 17 | s_e = DATE_SEP_TOKEN.join(se) 18 | self.assertTupleEqual( 19 | cli_api.valid_date(s_e), se) 20 | 21 | def test_cli_api_unchanged_date_parser_format(self): 22 | se = '1999-12-31', '2000-02-28' 23 | s_e = DATE_SEP_TOKEN.join(se) 24 | self.assertTupleEqual( 25 | cli_api.valid_date(s_e), se) 26 | 27 | def test_cli_api_date_date_sep_token_nok(self): 28 | se = '2017-06-20', '2017-06-21' 29 | s_e = ' '.join(se) 30 | with self.assertRaises(argparse.ArgumentTypeError): 31 | cli_api.valid_date(s_e) 32 | 33 | def test_cli_api_date_parser_format_nok(self): 34 | se = '19991231', '20000228' 35 | s_e = DATE_SEP_TOKEN.join(se) 36 | self.assertRaises(argparse.ArgumentTypeError, cli_api.valid_date, s_e) 37 | 38 | def test_cli_api_date_parser_start_after_end(self): 39 | se = '2000-02-28', '1999-12-31' 40 | s_e = DATE_SEP_TOKEN.join(se) 41 | self.assertRaises(argparse.ArgumentTypeError, cli_api.valid_date, s_e) 42 | 43 | def test_cli_api_date_parser_start_after_now(self): 44 | se = '9998-12-31', '9999-02-28' 45 | s_e = DATE_SEP_TOKEN.join(se) 46 | self.assertRaises(argparse.ArgumentTypeError, cli_api.valid_date, s_e) 47 | 48 | def test_cli_api_date_parser_end_after_now(self): 49 | se = '1999-12-31', '9999-02-28' 50 | s_e = DATE_SEP_TOKEN.join(se) 51 | self.assertRaises(argparse.ArgumentTypeError, cli_api.valid_date, s_e) 52 | 53 | def test_add_options_to_parser_alien_target(self): 54 | self.assertRaises( 55 | NotImplementedError, cli_api.add_options_to_parser, '', '') 56 | 57 | def test_parser_factory_dynamic_succeeds(self): 58 | self.assertTrue(cli_api.parser_factory()) 59 | 60 | def test_parser_factory_dynamic_yields_expected_parser_type(self): 61 | self.assertTrue( 62 | isinstance(cli_api.parser_factory(), argparse.ArgumentParser)) 63 | 64 | def test_cli_api_process_command_line_help(self): 65 | with self.assertRaises(SystemExit) as e: 66 | cli_api.process_command_line(['--help']) 67 | self.assertEqual(e.exception.code, 0) 68 | 69 | def test_cli_api_process_command_line_help_short(self): 70 | with self.assertRaises(SystemExit) as e: 71 | cli_api.process_command_line(['-h']) 72 | self.assertEqual(e.exception.code, 0) 73 | 74 | def test_cli_api_process_command_line_missing_adv_format(self): 75 | string_list = '--severity critical'.split() 76 | with self.assertRaises(SystemExit) as e: 77 | cli_api.process_command_line(string_list) 78 | self.assertEqual(e.exception.code, 2) 79 | 80 | def test_cli_api_process_command_line_filter_forbidden_by_api(self): 81 | se = '2017-06-20', '2017-06-21' 82 | s_e = DATE_SEP_TOKEN.join(se) 83 | string_list = ( 84 | '--cvrf --product foo --first_published {}'.format(s_e).split()) 85 | with self.assertRaises(SystemExit) as e: 86 | cli_api.process_command_line(string_list) 87 | self.assertEqual(e.exception.code, 2) 88 | 89 | def test_cli_api_process_command_line_filter_date_format_end_missing(self): 90 | se = '2017-06-20', '' 91 | s_e = DATE_SEP_TOKEN.join(se) 92 | string_list = ( 93 | '--cvrf --product foo --first_published {}'.format(s_e).split()) 94 | with self.assertRaises(SystemExit) as e: 95 | cli_api.process_command_line(string_list) 96 | self.assertEqual(e.exception.code, 2) 97 | 98 | def test_cli_api_process_command_line_filter_date_format_sep_bad(self): 99 | se = '2017-06-20', '2017-06-21' 100 | s_e = 'P'.join(se) 101 | string_list = ( 102 | '--cvrf --product foo --first_published {}'.format(s_e).split()) 103 | with self.assertRaises(SystemExit) as e: 104 | cli_api.process_command_line(string_list) 105 | self.assertEqual(e.exception.code, 2) 106 | 107 | def test_cli_api_process_command_line_filter_date_format_end_bad(self): 108 | se = '2017-06-20', '2017-06-00' 109 | s_e = DATE_SEP_TOKEN.join(se) 110 | string_list = ( 111 | '--cvrf --product foo --first_published {}'.format(s_e).split()) 112 | with self.assertRaises(SystemExit) as e: 113 | cli_api.process_command_line(string_list) 114 | self.assertEqual(e.exception.code, 2) 115 | 116 | # CVRF format as precondition value 117 | def test_cli_api_process_command_line_cvrf_all_succeeds(self): 118 | string_list = '--cvrf --all'.split() 119 | self.assertTrue(cli_api.process_command_line(string_list)) 120 | 121 | def test_cli_api_process_command_line_cvrf_severity_succeeds(self): 122 | string_list = '--cvrf --severity critical'.split() 123 | self.assertTrue(cli_api.process_command_line(string_list)) 124 | 125 | def test_cli_api_process_command_line_cvrf_year_succeeds(self): 126 | string_list = '--cvrf --year 2017'.split() 127 | self.assertTrue(cli_api.process_command_line(string_list)) 128 | 129 | def test_cli_api_process_command_line_cvrf_product_succeeds(self): 130 | string_list = '--cvrf --product foo'.split() 131 | self.assertTrue(cli_api.process_command_line(string_list)) 132 | 133 | def test_cli_api_process_command_line_cvrf_all_json_stdout_succeeds(self): 134 | string_list = '--cvrf --all --json \'\''.split() 135 | self.assertTrue(cli_api.process_command_line(string_list)) 136 | 137 | def test_cli_api_process_command_line_cvrf_all_json_file_succeeds(self): 138 | string_list = '--cvrf --all --json foo.json'.split() 139 | self.assertTrue(cli_api.process_command_line(string_list)) 140 | 141 | def test_cli_api_process_command_line_cvrf_all_csv_stdout_succeeds(self): 142 | string_list = '--cvrf --all --csv \'\''.split() 143 | self.assertTrue(cli_api.process_command_line(string_list)) 144 | 145 | def test_cli_api_process_command_line_cvrf_all_csv_file_succeeds(self): 146 | string_list = '--cvrf --all --csv foo.csv'.split() 147 | self.assertTrue(cli_api.process_command_line(string_list)) 148 | 149 | # OVAL format as precondition value 150 | def test_cli_api_process_command_line_oval_all_succeeds(self): 151 | string_list = '--oval --all'.split() 152 | self.assertTrue(cli_api.process_command_line(string_list)) 153 | 154 | def test_cli_api_process_command_line_oval_severity_succeeds(self): 155 | string_list = '--oval --severity critical'.split() 156 | self.assertTrue(cli_api.process_command_line(string_list)) 157 | 158 | def test_cli_api_process_command_line_oval_year_succeeds(self): 159 | string_list = '--oval --year 2017'.split() 160 | self.assertTrue(cli_api.process_command_line(string_list)) 161 | 162 | def test_cli_api_process_command_line_oval_product_succeeds(self): 163 | string_list = '--oval --product foo'.split() 164 | self.assertTrue(cli_api.process_command_line(string_list)) 165 | 166 | def test_cli_api_process_command_line_oval_all_json_stdout_succeeds(self): 167 | string_list = '--oval --all --json \'\''.split() 168 | self.assertTrue(cli_api.process_command_line(string_list)) 169 | 170 | def test_cli_api_process_command_line_oval_all_json_file_succeeds(self): 171 | string_list = '--oval --all --json foo.json'.split() 172 | self.assertTrue(cli_api.process_command_line(string_list)) 173 | 174 | def test_cli_api_process_command_line_oval_all_csv_stdout_succeeds(self): 175 | string_list = '--oval --all --csv \'\''.split() 176 | self.assertTrue(cli_api.process_command_line(string_list)) 177 | 178 | def test_cli_api_process_command_line_oval_all_csv_file_succeeds(self): 179 | string_list = '--oval --all --csv foo.csv'.split() 180 | self.assertTrue(cli_api.process_command_line(string_list)) 181 | 182 | # IOS format as precondition value 183 | # ios ... 184 | def test_cli_api_process_command_line_ios_all_succeeds(self): 185 | string_list = '--ios \'15.5(2)T1\''.split() 186 | self.assertTrue(cli_api.process_command_line(string_list)) 187 | 188 | def test_cli_api_process_command_line_ios_all_json_stdout_succeeds(self): 189 | string_list = '--ios \'15.5(2)T1\' --json \'\''.split() 190 | self.assertTrue(cli_api.process_command_line(string_list)) 191 | 192 | def test_cli_api_process_command_line_ios_all_json_file_succeeds(self): 193 | string_list = '--ios \'15.5(2)T1\' --json foo.json'.split() 194 | self.assertTrue(cli_api.process_command_line(string_list)) 195 | 196 | def test_cli_api_process_command_line_ios_all_csv_stdout_succeeds(self): 197 | string_list = '--ios \'15.5(2)T1\' --csv \'\''.split() 198 | self.assertTrue(cli_api.process_command_line(string_list)) 199 | 200 | def test_cli_api_process_command_line_ios_all_csv_file_succeeds(self): 201 | string_list = '--ios \'15.5(2)T1\' --csv foo.csv'.split() 202 | self.assertTrue(cli_api.process_command_line(string_list)) 203 | 204 | # ios_xe ... 205 | def test_cli_api_process_command_line_ios_xe_all_succeeds(self): 206 | string_list = '--ios_xe \'3.16.1aS\''.split() 207 | self.assertTrue(cli_api.process_command_line(string_list)) 208 | 209 | def test_cli_api_process_command_line_ios_xe_all_json_stdout_succeeds(self): 210 | string_list = '--ios_xe \'3.16.1aS\' --json \'\''.split() 211 | self.assertTrue(cli_api.process_command_line(string_list)) 212 | 213 | def test_cli_api_process_command_line_ios_xe_all_json_file_succeeds(self): 214 | string_list = '--ios_xe \'3.16.1aS\' --json foo.json'.split() 215 | self.assertTrue(cli_api.process_command_line(string_list)) 216 | 217 | def test_cli_api_process_command_line_ios_xe_all_csv_stdout_succeeds(self): 218 | string_list = '--ios_xe \'3.16.1aS\' --csv \'\''.split() 219 | self.assertTrue(cli_api.process_command_line(string_list)) 220 | 221 | def test_cli_api_process_command_line_ios_xe_all_csv_file_succeeds(self): 222 | string_list = '--ios_xe \'3.16.1aS\' --csv foo.csv'.split() 223 | self.assertTrue(cli_api.process_command_line(string_list)) 224 | 225 | # Config aspects 226 | def test_cli_api_process_command_line_cvrf_all_config_file_not_exist(self): 227 | string_list = '--cvrf --all --config NOT_PRESENT_PLEASE'.split() 228 | with self.assertRaises(SystemExit) as e: 229 | cli_api.process_command_line(string_list) 230 | self.assertEqual(e.exception.code, 2) 231 | 232 | def test_cli_api_process_command_line_cvrf_all_config_sample_file(self): 233 | cfg_name = 'sample_credentials.json' 234 | cfg_dir = os.path.dirname(os.path.abspath( 235 | inspect.getsourcefile(cli_api))) 236 | cfg_path = os.path.join(cfg_dir, cfg_name) 237 | string_list = '--cvrf --all --config {}'.format(cfg_path).split() 238 | args = cli_api.process_command_line(string_list) 239 | self.assertEqual(config.CLIENT_ID, SAMPLE_CLIENT_ID) 240 | self.assertEqual(config.CLIENT_SECRET, SAMPLE_CLIENT_SECRET) 241 | 242 | def test_cli_api_process_command_line_cvrf_all_config_wrong_file(self): 243 | cfg_name = '__init__.py' 244 | cfg_dir = os.path.dirname(os.path.abspath( 245 | inspect.getsourcefile(cli_api))) 246 | cfg_path = os.path.join(cfg_dir, cfg_name) 247 | string_list = '--cvrf --all --config {}'.format(cfg_path).split() 248 | self.assertRaises( 249 | ValueError, cli_api.process_command_line, string_list) 250 | -------------------------------------------------------------------------------- /tests/test_query_client_cvrf.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | import pytest 4 | 5 | import mock 6 | from openVulnQuery import advisory 7 | from openVulnQuery import config 8 | from openVulnQuery import query_client 9 | 10 | API_URL = "https://api.cisco.com/security/advisories" 11 | test_advisory_id = "Cisco-SA-20111107-CVE-2011-0941" 12 | test_cve = "CVE-2011-0941" 13 | test_severity = "Medium" 14 | test_year = "2011" 15 | response_not_found = '{"error" : "Not Found"}' 16 | response_error = "{'error' : 'Invalid Url'}" 17 | 18 | response_generic = """{ 19 | "advisories": [ 20 | { 21 | "advisoryId": "Cisco-SA-20111107-CVE-2011-0941", 22 | "sir": "Medium", 23 | "firstPublished": "2011-11-07T21:36:55+0000", 24 | "lastUpdated": "2011-11-07T21:36:55+0000", 25 | "cves": [ 26 | "%s" 27 | ], 28 | "cvrfUrl": "http://tools.cisco.com/security/center/contentxml/CiscoSecurityAdvisory/Cisco-SA-20111107-CVE-2011-0941/cvrf/Cisco-SA-20111107-CVE-2011-0941_cvrf.xml", 29 | "bugIDs": [ 30 | "CSCtj09179" 31 | ], 32 | "cvssBaseScore": "7.8", 33 | "advisoryTitle": "Cisco IOS Software and Cisco Unified Communications Manager Session Initiation Protocol Packet Processing Memory Leak Vulnerability", 34 | "publicationUrl": "http://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/Cisco-SA-20111107-CVE-2011-0941", 35 | "cwe": [ 36 | "CWE-399" 37 | ], 38 | "productNames": [ 39 | "Cisco Unified Communications Manager (CallManager)", 40 | "Cisco IOS Software Releases 12.4 T", 41 | "Cisco IOS Software Release 12.4(2)T", 42 | "Cisco IOS Software Release 12.4(4)T", 43 | "Cisco IOS Software Release 12.4(6)T", 44 | "Cisco IOS Software Release 12.4(9)T", 45 | "Cisco IOS Software Release 12.4(11)T", 46 | "Cisco IOS Software Release 12.4(15)T", 47 | "Cisco IOS Software Release 12.4(20)T", 48 | "Cisco IOS Software Release 12.4(22)T", 49 | "Cisco Unified Communications Manager Version 7.1", 50 | "Cisco IOS Software Release 12.4(24)T", 51 | "Cisco IOS 15.1M&T", 52 | "Cisco IOS Software Release 15.1(1)T", 53 | "Cisco Unified Communications Manager Version 8.0", 54 | "Cisco IOS Software Release 15.1(2)T", 55 | "Cisco Unified Communications Manager Version 8.5", 56 | "Cisco IOS 15.1S", 57 | "Cisco IOS Software Release 15.1(3)T", 58 | "Cisco IOS Software Release 15.1(4)M", 59 | "Cisco IOS Software Release 15.1(1)S", 60 | "Cisco IOS Software Release 15.1(2)S", 61 | "Cisco IOS Software Release 15.1(3)S" 62 | ], 63 | "summary": "

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.

" 64 | } 65 | ] 66 | }""" % test_cve 67 | 68 | response_advisory_id = """{ 69 | "advisoryId": "%s", 70 | "sir": "Medium", 71 | "firstPublished": "2011-11-07T21:36:55+0000", 72 | "lastUpdated": "2011-11-07T21:36:55+0000", 73 | "cves": [ 74 | "CVE-2011-0941" 75 | ], 76 | "cvrfUrl": "http://tools.cisco.com/security/center/contentxml/CiscoSecurityAdvisory/Cisco-SA-20111107-CVE-2011-0941/cvrf/Cisco-SA-20111107-CVE-2011-0941_cvrf.xml", 77 | "bugIDs": [ 78 | "CSCtj09179" 79 | ], 80 | "cvssBaseScore": "7.8", 81 | "advisoryTitle": "Cisco IOS Software and Cisco Unified Communications Manager Session Initiation Protocol Packet Processing Memory Leak Vulnerability", 82 | "publicationUrl": "http://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/Cisco-SA-20111107-CVE-2011-0941", 83 | "cwe": [ 84 | "CWE-399" 85 | ], 86 | "productNames": [ 87 | "Cisco Unified Communications Manager (CallManager)", 88 | "Cisco IOS Software Releases 12.4 T", 89 | "Cisco IOS Software Release 12.4(2)T", 90 | "Cisco IOS Software Release 12.4(4)T", 91 | "Cisco IOS Software Release 12.4(6)T", 92 | "Cisco IOS Software Release 12.4(9)T", 93 | "Cisco IOS Software Release 12.4(11)T", 94 | "Cisco IOS Software Release 12.4(15)T", 95 | "Cisco IOS Software Release 12.4(20)T", 96 | "Cisco IOS Software Release 12.4(22)T", 97 | "Cisco Unified Communications Manager Version 7.1", 98 | "Cisco IOS Software Release 12.4(24)T", 99 | "Cisco IOS 15.1M&T", 100 | "Cisco IOS Software Release 15.1(1)T", 101 | "Cisco Unified Communications Manager Version 8.0", 102 | "Cisco IOS Software Release 15.1(2)T", 103 | "Cisco Unified Communications Manager Version 8.5", 104 | "Cisco IOS 15.1S", 105 | "Cisco IOS Software Release 15.1(3)T", 106 | "Cisco IOS Software Release 15.1(4)M", 107 | "Cisco IOS Software Release 15.1(1)S", 108 | "Cisco IOS Software Release 15.1(2)S", 109 | "Cisco IOS Software Release 15.1(3)S" 110 | ], 111 | "summary": "

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 | --------------------------------------------------------------------------------