diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..98cea94 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +code/tests +code/observables.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4bcac02 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# macOS +.DS_Store + +# PyCharm +.idea/ + +# Python +__pycache__/ +venv/ + +# dotenv +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c946e9c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM alpine:3.14 +LABEL maintainer="Ian Redden " + +ENV PIP_IGNORE_INSTALLED 1 + +# install packages we need +RUN apk update && apk add --no-cache musl-dev openssl-dev gcc py3-configobj \ +supervisor libffi-dev uwsgi-python3 uwsgi-http jq syslog-ng uwsgi-syslog \ +py3-pip python3-dev + +# do the Python dependencies +ADD code /app +ADD code/Pipfile code/Pipfile.lock / +RUN set -ex && pip install --no-cache-dir --upgrade pipenv && \ + pipenv install --system +RUN chown -R uwsgi.uwsgi /etc/uwsgi + +# copy over scripts to init +ADD scripts / +RUN mv /uwsgi.ini /etc/uwsgi +RUN chmod +x /*.sh + +# entrypoint +ENTRYPOINT ["/entrypoint.sh"] +CMD ["/start.sh"] diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..e8ac0fd --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,3 @@ +@Library('softserve-jenkins-library@main') _ + +startPipeline() diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..12ef85e --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,22 @@ + +The MIT License (MIT) + +Copyright (c) 2021 Cisco SecureX + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 48cdce8..9c69655 100644 --- a/README.md +++ b/README.md @@ -1 +1,101 @@ -placeholder +[![Gitter Chat](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg)](https://gitter.im/CiscoSecurity/Threat-Response "Gitter Chat") + +# Docker Relay Template (Cisco Hosted) + +A Cisco SecureX Concrete Relay implementation using [CyberScan](https://www.cyberscan.io/) +as a third-party Cyber Threat Intelligence service provider. + +The Relay itself is just a simple application written in Python that can be +easily packaged and deployed in docker container. + +## Rationale + +- We need an application that will translate API requests from SecureX Threat Response to the third-party integration, and vice versa. +- We need an application that can be completely self-contained within a virtualized container using Docker. + +## Testing (Optional) + +Open the code folder in your terminal. +``` +cd code +``` + +If you want to test the application you have to install dependencies from the [Pipfile](code/Pipfile) file: +``` +pip install --no-cache-dir --upgrade pipenv && pipenv install --dev +``` + +You can perform two kinds of testing: + +- Run static code analysis checking for any semantic discrepancies and +[PEP 8](https://www.python.org/dev/peps/pep-0008/) compliance: + + `flake8 .` + +- Run the suite of unit tests and measure the code coverage: + + `coverage run --source api/ -m pytest --verbose tests/unit/ && coverage report` + +### Building the Docker Container +In order to build the application, we need to use a `Dockerfile`. + + 1. Open a terminal. Build the container image using the `docker build` command. + +``` +docker build -t tr-05-cyberscan . +``` + + 2. Once the container is built, and an image is successfully created, start your container using the `docker run` command and specify the name of the image we have just created. By default, the container will listen for HTTP requests using port 9090. + +``` +docker run -dp 9090:9090 --name tr-05-cyberscan tr-05-cyberscan +``` + + 3. Watch the container logs to ensure it starts correctly. + +``` +docker logs tr-05-cyberscan +``` + + 4. Once the container has started correctly, open your web browser to http://localhost:9090. You should see a response from the container. + +``` +curl http://localhost:9090 +``` + +## Implementation Details + +This application was developed and tested under Python version 3.9. + +### Implemented Relay Endpoints + +- `POST /health` + - Verifies the Authorization Bearer JWT and decodes it to restore the original + credentials. + - Authenticates to the underlying external service to check that the provided + credentials are valid and the service is available at the moment. + + +- `POST /observe/observables` + - Accepts a list of observables and filters out unsupported ones. + - Verifies the Authorization Bearer JWT and decodes it to restore the original credentials. + - Makes a series of requests to the underlying external service to query for some + cyber threat intelligence data on each supported observable. + - Maps the fetched data into appropriate CTIM entities. + - Returns a list per each of the following CTIM entities (if any extracted): + - `Sighting` + + +- `POST /refer/observables` + - Accepts a list of observables and filters out unsupported ones. + - Builds a search link per each supported observable to pivot back to the underlying external service and look up the observable there. + - Returns a list of those links. + + +- `POST /version` + - Returns the current version of the application + +### Supported Types of Observables + +- `ip` +- `domain` diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..dafdc61 --- /dev/null +++ b/build.sh @@ -0,0 +1,32 @@ +#! /usr/bin/env sh +echo " .:|:.:|:. " +echo " C I S C O " +echo " SecureX " +echo +echo " Development Dockerfile build script." +echo + +module_name="Docker relay" +image_name="tr-05-docker-relay" + +CONFIG_FILE=code/container_settings.json +if [ -f $CONFIG_FILE ]; then + echo + echo "The configuration file (container_settings.json) already exists." + echo + version=`jq -r .VERSION code/container_settings.json` +else + read -p 'Version: ' version + echo {\"VERSION\": \"$version\", \"NAME\": \"$module_name\"} > code/container_settings.json +fi + +echo " Integration Module: $module_name" +echo " Version: $version" +echo +echo "Starting build process ..." +echo +docker build -t "$image_name:$version" . + +echo +echo "Please ensure you update module_type.json with correct url." +echo diff --git a/code/Pipfile b/code/Pipfile new file mode 100644 index 0000000..906db61 --- /dev/null +++ b/code/Pipfile @@ -0,0 +1,19 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +cryptography = "==3.3.2" +Flask = "==2.0.1" +marshmallow = "==3.12.1" +requests = "==2.25.1" +PyJWT = "==2.1.0" + +[dev-packages] +flake8 = "==3.9.2" +coverage = "==5.5" +pytest = "==6.2.4" + +[requires] +python_version = "3.9" diff --git a/code/Pipfile.lock b/code/Pipfile.lock new file mode 100644 index 0000000..c0e3cb1 --- /dev/null +++ b/code/Pipfile.lock @@ -0,0 +1,395 @@ +{ + "_meta": { + "hash": { + "sha256": "fdd746951392809b96e62d08cd666bb3a10042655dedf07c39610f4c56f43326" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", + "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + ], + "version": "==2021.5.30" + }, + "cffi": { + "hashes": [ + "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d", + "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771", + "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872", + "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c", + "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc", + "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762", + "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202", + "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5", + "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548", + "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a", + "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f", + "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20", + "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218", + "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c", + "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e", + "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56", + "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224", + "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a", + "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2", + "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a", + "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819", + "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346", + "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b", + "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e", + "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534", + "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb", + "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0", + "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156", + "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd", + "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87", + "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc", + "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195", + "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33", + "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f", + "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d", + "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd", + "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728", + "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7", + "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca", + "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99", + "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf", + "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e", + "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c", + "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5", + "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69" + ], + "version": "==1.14.6" + }, + "chardet": { + "hashes": [ + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.0.0" + }, + "click": { + "hashes": [ + "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", + "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + ], + "markers": "python_version >= '3.6'", + "version": "==8.0.1" + }, + "cryptography": { + "hashes": [ + "sha256:0d7b69674b738068fa6ffade5c962ecd14969690585aaca0a1b1fc9058938a72", + "sha256:1bd0ccb0a1ed775cd7e2144fe46df9dc03eefd722bbcf587b3e0616ea4a81eff", + "sha256:3c284fc1e504e88e51c428db9c9274f2da9f73fdf5d7e13a36b8ecb039af6e6c", + "sha256:49570438e60f19243e7e0d504527dd5fe9b4b967b5a1ff21cc12b57602dd85d3", + "sha256:541dd758ad49b45920dda3b5b48c968f8b2533d8981bcdb43002798d8f7a89ed", + "sha256:5a60d3780149e13b7a6ff7ad6526b38846354d11a15e21068e57073e29e19bed", + "sha256:7951a966613c4211b6612b0352f5bf29989955ee592c4a885d8c7d0f830d0433", + "sha256:922f9602d67c15ade470c11d616f2b2364950602e370c76f0c94c94ae672742e", + "sha256:a0f0b96c572fc9f25c3f4ddbf4688b9b38c69836713fb255f4a2715d93cbaf44", + "sha256:a777c096a49d80f9d2979695b835b0f9c9edab73b59e4ceb51f19724dda887ed", + "sha256:a9a4ac9648d39ce71c2f63fe7dc6db144b9fa567ddfc48b9fde1b54483d26042", + "sha256:aa4969f24d536ae2268c902b2c3d62ab464b5a66bcb247630d208a79a8098e9b", + "sha256:c7390f9b2119b2b43160abb34f63277a638504ef8df99f11cb52c1fda66a2e6f", + "sha256:e18e6ab84dfb0ab997faf8cca25a86ff15dfea4027b986322026cc99e0a892da" + ], + "index": "pypi", + "version": "==3.3.2" + }, + "flask": { + "hashes": [ + "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55", + "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9" + ], + "index": "pypi", + "version": "==2.0.1" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, + "itsdangerous": { + "hashes": [ + "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", + "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" + ], + "markers": "python_version >= '3.6'", + "version": "==2.0.1" + }, + "jinja2": { + "hashes": [ + "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", + "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.1" + }, + "markupsafe": { + "hashes": [ + "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", + "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", + "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", + "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", + "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", + "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", + "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", + "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", + "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", + "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", + "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", + "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", + "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", + "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", + "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", + "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", + "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + ], + "markers": "python_version >= '3.6'", + "version": "==2.0.1" + }, + "marshmallow": { + "hashes": [ + "sha256:8050475b70470cc58f4441ee92375db611792ba39ca1ad41d39cad193ea9e040", + "sha256:b45cde981d1835145257b4a3c5cb7b80786dcf5f50dd2990749a50c16cb48e01" + ], + "index": "pypi", + "version": "==3.12.1" + }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.20" + }, + "pyjwt": { + "hashes": [ + "sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1", + "sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130" + ], + "index": "pypi", + "version": "==2.1.0" + }, + "requests": { + "hashes": [ + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + ], + "index": "pypi", + "version": "==2.25.1" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.16.0" + }, + "urllib3": { + "hashes": [ + "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", + "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.6" + }, + "werkzeug": { + "hashes": [ + "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42", + "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8" + ], + "markers": "python_version >= '3.6'", + "version": "==2.0.1" + } + }, + "develop": { + "attrs": { + "hashes": [ + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" + }, + "coverage": { + "hashes": [ + "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", + "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", + "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", + "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", + "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", + "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", + "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", + "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", + "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", + "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", + "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", + "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", + "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", + "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", + "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", + "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", + "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", + "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", + "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", + "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", + "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", + "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", + "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", + "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", + "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", + "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", + "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", + "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", + "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", + "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", + "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", + "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", + "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", + "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", + "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", + "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", + "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", + "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", + "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", + "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", + "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", + "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", + "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", + "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", + "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", + "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", + "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", + "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", + "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", + "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", + "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", + "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" + ], + "index": "pypi", + "version": "==5.5" + }, + "flake8": { + "hashes": [ + "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", + "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" + ], + "index": "pypi", + "version": "==3.9.2" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "packaging": { + "hashes": [ + "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", + "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" + ], + "markers": "python_version >= '3.6'", + "version": "==21.0" + }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.13.1" + }, + "py": { + "hashes": [ + "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.10.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", + "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.7.0" + }, + "pyflakes": { + "hashes": [ + "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", + "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.3.1" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==2.4.7" + }, + "pytest": { + "hashes": [ + "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", + "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" + ], + "index": "pypi", + "version": "==6.2.4" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==0.10.2" + } + } +} diff --git a/code/api/__init__.py b/code/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code/api/client.py b/code/api/client.py new file mode 100644 index 0000000..335d0ce --- /dev/null +++ b/code/api/client.py @@ -0,0 +1,109 @@ +from http import HTTPStatus + +import requests +from flask import current_app +from requests.exceptions import ConnectionError, SSLError, InvalidURL + +from api.errors import ( + CyberScanConnectionError, + AuthorizationError, + CyberScanSSLError, +) + +INVALID_CREDENTIALS = 'wrong api_key' + +REFER_PATH = { + 'ip': 'ip/{observable}', + 'domain': 'domain/{observable}' +} + + +class CyberScanClient: + def __init__(self, credentials): + self._credentials = credentials + self._headers = { + 'User-Agent': current_app.config['USER_AGENT'] + } + self._ctr_entities_limit = current_app.config['CTR_ENTITIES_LIMIT'] + + @property + def _url(self): + url = current_app.config['CYBERSCAN_API_ENDPOINT'] + return url.format(host=self._credentials.get('host').rstrip('/')) + + def _auth(self): + payload = { + 'key': self._credentials.get('api_key') + } + response = self._request('token', method='POST', payload=payload) + + self._headers['Authorization'] = 'Bearer ' \ + f'{response.get("access_token")}' + + def health(self): + self._auth() + + def _get_domain_ip(self, observable): + path = f'domain/{observable["value"]}' + response = self._request(path) + try: + ip = response.get('ip') + except AttributeError: + ip = '' + + return ip + + def get_vulnerabilities(self, observable): + self._auth() + ip = observable['value'] if observable['type'] == 'ip' \ + else self._get_domain_ip(observable) + vulnerabilities = [] + path = f'vulnerabilities/ip/{ip}' + response = self._request(path) + if response: + vulnerabilities = response.get('vulnerabilities') + if len(vulnerabilities) > self._ctr_entities_limit: + vulnerabilities = vulnerabilities[:self._ctr_entities_limit] + return vulnerabilities + + def refer(self, observables): + self._auth() + relay_output = [] + for observable in observables: + + path = REFER_PATH[observable.get('type')].format( + observable=observable.get('value') + ) + response = self._request(path) + + relay_output.append( + { + 'id': ('ref-cyberscan-search-' + f'{observable["type"].replace("_", "-")}' + f'-{observable["value"]}'), + 'title': f'Details for this {observable.get("type")}', + 'description': + f'Details for this {observable["type"]} ' + 'in the CyberScan', + 'url': response.get('details_page'), + 'categories': ['CyberScan'], + } + ) + + return relay_output + + def _request(self, path, method='GET', payload=None): + url = '/'.join([self._url, path.lstrip('/')]) + + try: + response = requests.request(method, url, json=payload, + headers=self._headers, timeout=10) + except SSLError as error: + raise CyberScanSSLError(error) + except (ConnectionError, InvalidURL): + raise CyberScanConnectionError(url) + + if response.ok: + return response.json() + elif response.status_code == HTTPStatus.FORBIDDEN: + raise AuthorizationError(INVALID_CREDENTIALS) diff --git a/code/api/enrich.py b/code/api/enrich.py new file mode 100644 index 0000000..75f6db1 --- /dev/null +++ b/code/api/enrich.py @@ -0,0 +1,43 @@ +from functools import partial + +from flask import Blueprint, g + +from api.client import CyberScanClient +from api.mapping import Sighting +from api.schemas import ObservableSchema +from api.utils import get_json, get_credentials, jsonify_data, jsonify_result + +enrich_api = Blueprint('enrich', __name__) + +get_observables = partial(get_json, schema=ObservableSchema(many=True)) + + +@enrich_api.route('/observe/observables', methods=['POST']) +def observe_observables(): + credentials = get_credentials() + observables = get_observables() + + g.sightings = [] + client = CyberScanClient(credentials) + + for observable in observables: + vulnerabilities = client.get_vulnerabilities(observable) + mapping = Sighting(observable) + + for vulnerability in vulnerabilities: + sighting = mapping.extract(vulnerability) + g.sightings.append(sighting) + + return jsonify_result() + + +@enrich_api.route('/refer/observables', methods=['POST']) +def refer_observables(): + credentials = get_credentials() + observables = get_observables() + + client = CyberScanClient(credentials) + + relay_output = client.refer(observables) + + return jsonify_data(relay_output) diff --git a/code/api/errors.py b/code/api/errors.py new file mode 100644 index 0000000..d6d7150 --- /dev/null +++ b/code/api/errors.py @@ -0,0 +1,63 @@ +AUTH_ERROR = 'authorization error' +INVALID_ARGUMENT = 'invalid argument' +UNKNOWN = 'unknown' +CONNECTION_ERROR = 'connection error' + + +class TRFormattedError(Exception): + def __init__(self, code, message, type_='fatal'): + super().__init__() + self.code = code or UNKNOWN + self.message = message or 'Something went wrong.' + self.type_ = type_ + + @property + def json(self): + return {'type': self.type_, + 'code': self.code, + 'message': self.message} + + +class AuthorizationError(TRFormattedError): + def __init__(self, message): + super().__init__( + AUTH_ERROR, + f'Authorization failed: {message}' + ) + + +class InvalidArgumentError(TRFormattedError): + def __init__(self, message): + super().__init__( + INVALID_ARGUMENT, + str(message) + ) + + +class WatchdogError(TRFormattedError): + def __init__(self): + super().__init__( + code='health check failed', + message='Invalid Health Check' + ) + + +class CyberScanConnectionError(TRFormattedError): + def __init__(self, url): + super().__init__( + CONNECTION_ERROR, + 'Unable to connect to CyberScan,' + f' validate the configured API endpoint: {url}' + ) + + +class CyberScanSSLError(TRFormattedError): + def __init__(self, error): + message = getattr( + error.args[0].reason.args[0], 'verify_message', '' + ) or error.args[0].reason.args[0].args[0] + + super().__init__( + UNKNOWN, + f'Unable to verify SSL certificate: {message}' + ) diff --git a/code/api/health.py b/code/api/health.py new file mode 100644 index 0000000..7982caf --- /dev/null +++ b/code/api/health.py @@ -0,0 +1,15 @@ +from flask import Blueprint + +from api.utils import get_credentials, jsonify_data +from api.client import CyberScanClient + +health_api = Blueprint('health', __name__) + + +@health_api.route('/health', methods=['POST']) +def health(): + credentials = get_credentials() + client = CyberScanClient(credentials) + client.health() + + return jsonify_data({'status': 'ok'}) diff --git a/code/api/mapping.py b/code/api/mapping.py new file mode 100644 index 0000000..34d087f --- /dev/null +++ b/code/api/mapping.py @@ -0,0 +1,77 @@ +from datetime import datetime +from uuid import uuid4 + + +SIGHTING = 'sighting' + +SOURCE = 'CyberScan' +CONFIDENCE = 'High' + +SIGHTING_DEFAULTS = { + 'count': 1, + 'internal': True, + 'confidence': CONFIDENCE, + 'type': SIGHTING, + 'source': SOURCE, + 'schema_version': '1.1.11', +} + + +class Sighting: + def __init__(self, observable): + self.observable = observable + + @staticmethod + def _transient_id(entity): + uuid = uuid4() + return f'transient:{entity}-{uuid}' + + @staticmethod + def _time_format(time): + return f'{time.isoformat(timespec="seconds")}Z' + + def _observed_time(self): + observed_time = self._time_format(datetime.utcnow()) + return { + 'start_time': observed_time + } + + @staticmethod + def _make_data_table(message): + data = { + 'columns': [], + 'rows': [[]] + } + + for key, value in message.items(): + data['columns'].append({'name': key, 'type': 'string'}) + data['rows'][0].append(str(value)) + + return data + + @staticmethod + def _short_description(vulnerability): + return f'Vulnerability {vulnerability.get("cve")} observed at ' \ + 'CyberScan' + + @staticmethod + def _description(vulnerability): + return vulnerability.get('description') + + @staticmethod + def _title(vulnerability): + return vulnerability.get('name') + + def extract(self, vulnerability): + sighting = { + 'id': self._transient_id(SIGHTING), + 'observed_time': self._observed_time(), + 'observables': [self.observable], + 'short_description': self._short_description(vulnerability), + 'description': self._description(vulnerability), + 'data': self._make_data_table(vulnerability), + 'title': self._title(vulnerability), + **SIGHTING_DEFAULTS, + } + + return sighting diff --git a/code/api/schemas.py b/code/api/schemas.py new file mode 100644 index 0000000..558812e --- /dev/null +++ b/code/api/schemas.py @@ -0,0 +1,57 @@ +from marshmallow import ValidationError, Schema, fields, INCLUDE + + +def validate_string(value): + if value == '': + raise ValidationError('Field may not be blank.') + + +class ObservableSchema(Schema): + type = fields.String( + validate=validate_string, + required=True, + ) + value = fields.String( + validate=validate_string, + required=True, + ) + + +class ActionFormParamsSchema(Schema): + action_id = fields.String( + data_key='action-id', + validate=validate_string, + required=True, + ) + observable_type = fields.String( + validate=validate_string, + required=True, + ) + observable_value = fields.String( + validate=validate_string, + required=True, + ) + + class Meta: + unknown = INCLUDE + + +class DashboardTileSchema(Schema): + tile_id = fields.String( + data_key='tile_id', + validate=validate_string, + required=True + ) + + +class DashboardTileDataSchema(Schema): + period = fields.String( + data_key='period', + validate=validate_string, + required=True + ) + tile_id = fields.String( + data_key='tile_id', + validate=validate_string, + required=True + ) diff --git a/code/api/utils.py b/code/api/utils.py new file mode 100644 index 0000000..d52c656 --- /dev/null +++ b/code/api/utils.py @@ -0,0 +1,174 @@ +import json +from json.decoder import JSONDecodeError + +import jwt +import requests +from flask import request, jsonify, current_app, g +from jwt import InvalidSignatureError, DecodeError, InvalidAudienceError +from requests.exceptions import ConnectionError, InvalidURL, HTTPError + +from api.errors import AuthorizationError, InvalidArgumentError + +NO_AUTH_HEADER = 'Authorization header is missing' +WRONG_AUTH_TYPE = 'Wrong authorization type' +WRONG_PAYLOAD_STRUCTURE = 'Wrong JWT payload structure' +WRONG_JWT_STRUCTURE = 'Wrong JWT structure' +WRONG_AUDIENCE = 'Wrong configuration-token-audience' +KID_NOT_FOUND = 'kid from JWT header not found in API response' +WRONG_KEY = ('Failed to decode JWT with provided key. ' + 'Make sure domain in custom_jwks_host ' + 'corresponds to your SecureX instance region.') +JWKS_HOST_MISSING = ('jwks_host is missing in JWT payload. Make sure ' + 'custom_jwks_host field is present in module_type') +WRONG_JWKS_HOST = ('Wrong jwks_host in JWT payload. Make sure domain follows ' + 'the visibility..cisco.com structure') + + +def get_public_key(jwks_host, token): + """ + Get public key by requesting it from specified jwks host. + + NOTE. This function is just an example of how one can read and check + anything before passing to an API endpoint, and thus it may be modified in + any way, replaced by another function, or even removed from the module. + """ + + expected_errors = ( + ConnectionError, + InvalidURL, + KeyError, + JSONDecodeError, + HTTPError + ) + try: + response = requests.get(f"https://{jwks_host}/.well-known/jwks") + response.raise_for_status() + jwks = response.json() + + public_keys = {} + for jwk in jwks['keys']: + kid = jwk['kid'] + public_keys[kid] = jwt.algorithms.RSAAlgorithm.from_jwk( + json.dumps(jwk) + ) + kid = jwt.get_unverified_header(token)['kid'] + return public_keys.get(kid) + + except expected_errors: + raise AuthorizationError(WRONG_JWKS_HOST) + + +def get_auth_token(): + """ + Parse and validate incoming request Authorization header. + + NOTE. This function is just an example of how one can read and check + anything before passing to an API endpoint, and thus it may be modified in + any way, replaced by another function, or even removed from the module. + """ + expected_errors = { + KeyError: NO_AUTH_HEADER, + AssertionError: WRONG_AUTH_TYPE + } + try: + scheme, token = request.headers['Authorization'].split() + assert scheme.lower() == 'bearer' + return token + except tuple(expected_errors) as error: + raise AuthorizationError(expected_errors[error.__class__]) + + +def get_credentials(): + """ + Get Authorization token and validate its signature + against the public key from /.well-known/jwks endpoint. + + NOTE. This function is just an example of how one can read and check + anything before passing to an API endpoint, and thus it may be modified in + any way, replaced by another function, or even removed from the module. + """ + + expected_errors = { + KeyError: JWKS_HOST_MISSING, + AssertionError: WRONG_PAYLOAD_STRUCTURE, + InvalidSignatureError: WRONG_KEY, + DecodeError: WRONG_JWT_STRUCTURE, + InvalidAudienceError: WRONG_AUDIENCE, + TypeError: KID_NOT_FOUND + } + token = get_auth_token() + try: + jwks_host = jwt.decode( + token, options={'verify_signature': False} + )['jwks_host'] + key = get_public_key(jwks_host, token) + aud = request.url_root + payload = jwt.decode( + token, key=key, algorithms=['RS256'], audience=[aud.rstrip('/')] + ) + + assert 'host' in payload + assert 'api_key' in payload + + set_ctr_entities_limit(payload) + + return payload + except tuple(expected_errors) as error: + message = expected_errors[error.__class__] + raise AuthorizationError(message) + + +def get_json(schema): + """ + Parse the incoming request's data as JSON. + Validate it against the specified schema. + + NOTE. This function is just an example of how one can read and check + anything before passing to an API endpoint, and thus it may be modified in + any way, replaced by another function, or even removed from the module. + """ + + data = request.get_json(force=True, silent=True, cache=False) + + message = schema.validate(data) + + if message: + raise InvalidArgumentError(message) + + return data + + +def jsonify_data(data): + return jsonify({'data': data}) + + +def jsonify_errors(data): + return jsonify({'errors': [data]}) + + +def set_ctr_entities_limit(payload): + try: + ctr_entities_limit = int(payload['CTR_ENTITIES_LIMIT']) + assert ctr_entities_limit > 0 + except (KeyError, ValueError, AssertionError): + ctr_entities_limit = current_app.config['CTR_DEFAULT_ENTITIES_LIMIT'] + current_app.config['CTR_ENTITIES_LIMIT'] = ctr_entities_limit + + +def format_docs(docs): + return {'count': len(docs), 'docs': docs} + + +def jsonify_result(): + result = {'data': {}} + + if g.get('sightings'): + result['data']['sightings'] = format_docs(g.sightings) + + if g.get('errors'): + result['errors'] = g.errors + + if not result.get('data'): + result.pop('data', None) + + return jsonify(result) diff --git a/code/api/version.py b/code/api/version.py new file mode 100644 index 0000000..35bc6db --- /dev/null +++ b/code/api/version.py @@ -0,0 +1,8 @@ +from flask import Blueprint, jsonify, current_app + +version_api = Blueprint('version', __name__) + + +@version_api.route('/version', methods=['POST']) +def version(): + return jsonify({'version': current_app.config['VERSION']}) diff --git a/code/api/watchdog.py b/code/api/watchdog.py new file mode 100644 index 0000000..eb482aa --- /dev/null +++ b/code/api/watchdog.py @@ -0,0 +1,15 @@ +from flask import request, Blueprint + +from api.utils import jsonify_data +from api.errors import WatchdogError + +watchdog_api = Blueprint('watchdog', __name__) + + +@watchdog_api.route('/watchdog', methods=['GET']) +def watchdog(): + try: + watchdog_key = request.headers['Health-Check'] + return jsonify_data(watchdog_key) + except KeyError: + raise WatchdogError diff --git a/code/app.py b/code/app.py new file mode 100644 index 0000000..4472800 --- /dev/null +++ b/code/app.py @@ -0,0 +1,46 @@ +import traceback + +from flask import Flask, jsonify + +from api.enrich import enrich_api +from api.health import health_api +from api.version import version_api +from api.watchdog import watchdog_api +from api.errors import TRFormattedError +from api.utils import jsonify_errors + +app = Flask(__name__) + +app.url_map.strict_slashes = False +app.config.from_object('config.Config') + +app.register_blueprint(enrich_api) +app.register_blueprint(health_api) +app.register_blueprint(version_api) +app.register_blueprint(watchdog_api) + + +@app.errorhandler(Exception) +def handle_error(exception): + code = getattr(exception, 'code', 500) + message = getattr(exception, 'description', 'Something went wrong.') + reason = '.'.join([ + exception.__class__.__module__, + exception.__class__.__name__, + ]) + + if code != 404: + app.logger.error(traceback.format_exc()) + + response = jsonify(code=code, message=message, reason=reason) + return response, code + + +@app.errorhandler(TRFormattedError) +def handle_tr_formatted_error(exception): + app.logger.error(traceback.format_exc()) + return jsonify_errors(exception.json) + + +if __name__ == '__main__': + app.run() diff --git a/code/config.py b/code/config.py new file mode 100644 index 0000000..6fadb14 --- /dev/null +++ b/code/config.py @@ -0,0 +1,13 @@ +import json + + +class Config: + settings = json.load(open('container_settings.json', 'r')) + VERSION = settings["VERSION"] + + USER_AGENT = ('SecureX Threat Response Integrations ' + '') + + CYBERSCAN_API_ENDPOINT = 'https://{host}/api/v1' + + CTR_DEFAULT_ENTITIES_LIMIT = 100 diff --git a/code/container_settings.json b/code/container_settings.json new file mode 100644 index 0000000..d78f202 --- /dev/null +++ b/code/container_settings.json @@ -0,0 +1,4 @@ +{ + "VERSION": "1.0.0", + "NAME": "CyberScan" +} diff --git a/code/tests/__init__.py b/code/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code/tests/unit/__init__.py b/code/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code/tests/unit/api/__init__.py b/code/tests/unit/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code/tests/unit/api/test_authorization.py b/code/tests/unit/api/test_authorization.py new file mode 100644 index 0000000..c692f36 --- /dev/null +++ b/code/tests/unit/api/test_authorization.py @@ -0,0 +1,174 @@ +from http import HTTPStatus +from unittest.mock import patch + +from pytest import fixture + +from api.errors import AUTH_ERROR +from api.utils import ( + WRONG_PAYLOAD_STRUCTURE, + WRONG_KEY, + WRONG_AUDIENCE, + KID_NOT_FOUND, + JWKS_HOST_MISSING +) +from tests.unit.api.utils import get_headers +from tests.unit.conftest import mock_api_response +from tests.unit.payloads_for_tests import ( + EXPECTED_RESPONSE_OF_JWKS_ENDPOINT, + RESPONSE_OF_JWKS_ENDPOINT_WITH_WRONG_KEY +) + + +def routes(): + yield '/health' + yield '/observe/observables' + yield '/refer/observables' + + +@fixture(scope='module', params=routes(), ids=lambda route: f'POST {route}') +def route(request): + return request.param + + +@fixture(scope='module') +def wrong_jwt_structure(): + return 'wrong_jwt_structure' + + +@fixture(scope='module') +def authorization_errors_expected_payload(route): + def _make_payload_message(message): + payload = { + 'errors': [{ + 'code': AUTH_ERROR, + 'message': f'Authorization failed: {message}', + 'type': 'fatal'}] + + } + return payload + + return _make_payload_message + + +def test_call_with_authorization_header_failure( + route, client, + authorization_errors_expected_payload +): + response = client.post(route) + + assert response.status_code == HTTPStatus.OK + assert response.json == authorization_errors_expected_payload( + 'Authorization header is missing' + ) + + +def test_call_with_wrong_authorization_type( + route, client, valid_jwt, + authorization_errors_expected_payload +): + response = client.post( + route, headers=get_headers(valid_jwt(), auth_type='wrong_type') + ) + + assert response.status_code == HTTPStatus.OK + assert response.json == authorization_errors_expected_payload( + 'Wrong authorization type' + ) + + +def test_call_with_wrong_jwt_structure( + route, client, wrong_jwt_structure, + authorization_errors_expected_payload +): + response = client.post(route, headers=get_headers(wrong_jwt_structure)) + + assert response.status_code == HTTPStatus.OK + assert response.json == authorization_errors_expected_payload( + 'Wrong JWT structure' + ) + + +@patch('requests.get') +def test_call_with_jwt_encoded_by_wrong_key( + mock_request, route, + client, valid_jwt, + authorization_errors_expected_payload +): + mock_request.return_value = \ + mock_api_response(payload=RESPONSE_OF_JWKS_ENDPOINT_WITH_WRONG_KEY) + response = client.post(route, headers=get_headers(valid_jwt())) + + assert response.status_code == HTTPStatus.OK + assert response.json == authorization_errors_expected_payload(WRONG_KEY) + + +@patch('requests.get') +def test_call_with_wrong_jwt_payload_structure( + mock_request, + route, client, valid_jwt, + authorization_errors_expected_payload +): + mock_request.return_value = \ + mock_api_response(payload=EXPECTED_RESPONSE_OF_JWKS_ENDPOINT) + response = \ + client.post(route, + headers=get_headers(valid_jwt(wrong_structure=True))) + + assert response.status_code == HTTPStatus.OK + assert response.json == authorization_errors_expected_payload( + WRONG_PAYLOAD_STRUCTURE + ) + + +@patch('requests.get') +def test_call_with_wrong_audience( + mock_request, route, client, valid_jwt, + authorization_errors_expected_payload +): + mock_request.return_value = \ + mock_api_response(payload=EXPECTED_RESPONSE_OF_JWKS_ENDPOINT) + + response = client.post( + route, + headers=get_headers(valid_jwt(aud='wrong_aud')) + ) + assert response.status_code == HTTPStatus.OK + assert response.json == authorization_errors_expected_payload( + WRONG_AUDIENCE + ) + + +@patch('requests.get') +def test_call_with_wrong_kid( + mock_request, route, client, valid_jwt, + authorization_errors_expected_payload +): + mock_request.return_value = \ + mock_api_response(payload=EXPECTED_RESPONSE_OF_JWKS_ENDPOINT) + + response = client.post( + route, + headers=get_headers(valid_jwt(kid='wrong_kid')) + ) + assert response.status_code == HTTPStatus.OK + assert response.json == authorization_errors_expected_payload( + KID_NOT_FOUND + ) + + +@patch('requests.get') +def test_call_with_missing_jwks_host( + mock_request, route, client, valid_jwt, + authorization_errors_expected_payload +): + mock_request.return_value = \ + mock_api_response(payload=EXPECTED_RESPONSE_OF_JWKS_ENDPOINT) + + response = client.post( + route, + headers=get_headers(valid_jwt(wrong_jwks_host=True)) + ) + assert response.status_code == HTTPStatus.OK + assert response.json == authorization_errors_expected_payload( + JWKS_HOST_MISSING + ) diff --git a/code/tests/unit/api/test_enrich.py b/code/tests/unit/api/test_enrich.py new file mode 100644 index 0000000..1b6db35 --- /dev/null +++ b/code/tests/unit/api/test_enrich.py @@ -0,0 +1,106 @@ +from http import HTTPStatus +from unittest.mock import patch + +from freezegun import freeze_time +from pytest import fixture +from requests.exceptions import SSLError + +from tests.unit.api.utils import get_headers +from tests.unit.conftest import mock_api_response +from tests.unit.payloads_for_tests import ( + EXPECTED_RESPONSE_OF_JWKS_ENDPOINT, + EXPECTED_RESPONSE_OF_SUCCESS_AUTH, + EXPECTED_RESPONSE_OF_GET_IP, + EXPECTED_RESPONSE_OF_GET_VULNERABILITIES, + EXPECTED_RELAY_RESPONSE, + EXPECTED_REFER_RESPONSE, +) + + +def routes(): + yield '/observe/observables' + yield '/refer/observables' + + +def responses(): + yield mock_api_response(payload=EXPECTED_RESPONSE_OF_SUCCESS_AUTH) + yield mock_api_response(payload=EXPECTED_RESPONSE_OF_GET_IP) + yield mock_api_response(payload=EXPECTED_RESPONSE_OF_GET_VULNERABILITIES) + + +def ids(): + yield 'c9826d98-35df-4b8b-a61f-e52313920c5a' + yield '8d518924-a3ac-4e3f-b0fd-4d017c219cf1' + yield 'ea815346-d9a8-4efb-9bf0-ed3a8ebabf65' + yield 'ca6a3495-0863-4789-83ab-039a57a5a84d' + yield 'f994a79c-6134-4334-86b3-b84165eb10a9' + yield '0d266e2b-1f5a-40ec-9b45-80824d1672bf' + yield '65b3711e-ed3a-46d6-adb3-78fe944ecf69' + + +@fixture(scope='module', params=routes(), ids=lambda route: f'POST {route}') +def route(request): + return request.param + + +@fixture(scope='module') +def invalid_json_value(): + return [{'type': 'ip', 'value': ''}] + + +@patch('requests.get') +def test_enrich_call_with_valid_jwt_but_invalid_json_value( + mock_request, + route, client, valid_jwt, invalid_json_value, + invalid_json_expected_payload +): + mock_request.return_value = \ + mock_api_response(payload=EXPECTED_RESPONSE_OF_JWKS_ENDPOINT) + response = client.post(route, + headers=get_headers(valid_jwt()), + json=invalid_json_value) + assert response.status_code == HTTPStatus.OK + assert response.json == invalid_json_expected_payload( + "{0: {'value': ['Field may not be blank.']}}" + ) + + +@fixture(scope='module') +def valid_json(): + return [{'type': 'domain', 'value': 'j-p.link'}] + + +@freeze_time("2022-07-12T09:38:46") +@patch('api.mapping.uuid4') +@patch('requests.request') +@patch('requests.get') +def test_enrich_call_success(mock_get, mock_request, mock_id, + route, client, valid_jwt, valid_json): + mock_get.return_value = \ + mock_api_response(payload=EXPECTED_RESPONSE_OF_JWKS_ENDPOINT) + mock_request.side_effect = responses() + mock_id.side_effect = ids() + response = client.post(route, headers=get_headers(valid_jwt()), + json=valid_json) + assert response.status_code == HTTPStatus.OK + if route == '/observe/observables': + assert response.json == EXPECTED_RELAY_RESPONSE + elif route == '/refer/observables': + assert response.json == EXPECTED_REFER_RESPONSE + + +@patch('requests.request') +@patch('requests.get') +def test_enrich_call_with_ssl_error(mock_get, mock_request, + mock_exception_for_ssl_error, + client, route, valid_jwt, valid_json, + ssl_error_expected_relay_response): + + mock_get.return_value = \ + mock_api_response(payload=EXPECTED_RESPONSE_OF_JWKS_ENDPOINT) + mock_request.side_effect = [SSLError(mock_exception_for_ssl_error)] + + response = client.post(route, headers=get_headers(valid_jwt()), + json=valid_json) + assert response.status_code == HTTPStatus.OK + assert response.json == ssl_error_expected_relay_response diff --git a/code/tests/unit/api/test_health.py b/code/tests/unit/api/test_health.py new file mode 100644 index 0000000..ff454cc --- /dev/null +++ b/code/tests/unit/api/test_health.py @@ -0,0 +1,33 @@ +from http import HTTPStatus +from unittest.mock import patch + +from pytest import fixture + +from tests.unit.api.utils import get_headers +from tests.unit.conftest import mock_api_response +from tests.unit.payloads_for_tests import ( + EXPECTED_RESPONSE_OF_JWKS_ENDPOINT, + EXPECTED_RESPONSE_OF_SUCCESS_AUTH, +) + + +def routes(): + yield '/health' + + +@fixture(scope='module', params=routes(), ids=lambda route: f'POST {route}') +def route(request): + return request.param + + +@patch('requests.request') +@patch('requests.get') +def test_health_call_success(mock_get, mock_request, + route, client, valid_jwt): + mock_get.return_value = \ + mock_api_response(payload=EXPECTED_RESPONSE_OF_JWKS_ENDPOINT) + mock_request.return_value = mock_api_response( + payload=EXPECTED_RESPONSE_OF_SUCCESS_AUTH) + response = client.post(route, headers=get_headers(valid_jwt())) + assert response.status_code == HTTPStatus.OK + assert response.json == {'data': {'status': 'ok'}} diff --git a/code/tests/unit/api/test_version.py b/code/tests/unit/api/test_version.py new file mode 100644 index 0000000..e85e680 --- /dev/null +++ b/code/tests/unit/api/test_version.py @@ -0,0 +1,25 @@ +from http import HTTPStatus + +from pytest import fixture + + +def routes(): + yield '/version' + + +@fixture(scope='module', params=routes(), ids=lambda route: f'POST {route}') +def route(request): + return request.param + + +@fixture(scope='module') +def version_expected_payload(client): + app = client.application + return {'version': app.config['VERSION']} + + +def test_version_call_success(route, client, version_expected_payload): + response = client.post(route) + + assert response.status_code == HTTPStatus.OK + assert response.get_json() == version_expected_payload diff --git a/code/tests/unit/api/test_watchdog.py b/code/tests/unit/api/test_watchdog.py new file mode 100644 index 0000000..3d62ce8 --- /dev/null +++ b/code/tests/unit/api/test_watchdog.py @@ -0,0 +1,38 @@ +from http import HTTPStatus + +from pytest import fixture + + +def routes(): + yield '/watchdog' + + +@fixture(scope='module', params=routes(), ids=lambda route: f'GET {route}') +def route(request): + return request.param + + +def test_watchdog_call_success(route, client): + response = client.get(route, headers={'Health-Check': 'test'}) + + expected_payload = {'data': 'test'} + + assert response.status_code == HTTPStatus.OK + assert response.get_json() == expected_payload + + +def test_watchdog_call_with_missing_header(route, client): + response = client.get(route) + + expected_payload = { + 'errors': [ + { + 'code': 'health check failed', + 'message': 'Invalid Health Check', + 'type': 'fatal' + } + ] + } + + assert response.status_code == HTTPStatus.OK + assert response.get_json() == expected_payload diff --git a/code/tests/unit/api/utils.py b/code/tests/unit/api/utils.py new file mode 100644 index 0000000..913e7b1 --- /dev/null +++ b/code/tests/unit/api/utils.py @@ -0,0 +1,2 @@ +def get_headers(jwt, auth_type='Bearer'): + return {'Authorization': f'{auth_type} {jwt}'} diff --git a/code/tests/unit/conftest.py b/code/tests/unit/conftest.py new file mode 100644 index 0000000..936f2a8 --- /dev/null +++ b/code/tests/unit/conftest.py @@ -0,0 +1,102 @@ +from http import HTTPStatus +from unittest.mock import MagicMock + +import jwt +from pytest import fixture + +from app import app +from api.errors import INVALID_ARGUMENT +from tests.unit.payloads_for_tests import PRIVATE_KEY + + +@fixture(scope='session') +def client(): + app.rsa_private_key = PRIVATE_KEY + + app.testing = True + + with app.test_client() as client: + yield client + + +@fixture(scope='session') +def valid_jwt(client): + def _make_jwt( + api_key='some_key', + host='host', + jwks_host='visibility.amp.cisco.com', + aud='http://localhost', + kid='02B1174234C29F8EFB69911438F597FF3FFEE6B7', + wrong_structure=False, + wrong_jwks_host=False + ): + payload = { + 'api_key': api_key, + 'jwks_host': jwks_host, + 'aud': aud, + 'host': host, + } + + if wrong_jwks_host: + payload.pop('jwks_host') + + if wrong_structure: + payload.pop('api_key') + + return jwt.encode( + payload, client.application.rsa_private_key, algorithm='RS256', + headers={ + 'kid': kid + } + ) + + return _make_jwt + + +@fixture(scope='module') +def invalid_json_expected_payload(): + def _make_message(message): + return { + 'errors': [{ + 'code': INVALID_ARGUMENT, + 'message': message, + 'type': 'fatal' + }] + } + + return _make_message + + +def mock_api_response(status_code=HTTPStatus.OK, payload=None): + mock_response = MagicMock() + + mock_response.status_code = status_code + mock_response.ok = status_code == HTTPStatus.OK + + mock_response.json = lambda: payload + + return mock_response + + +@fixture(scope='module') +def ssl_error_expected_relay_response(): + return { + 'errors': + [ + { + 'code': 'unknown', + 'message': + 'Unable to verify SSL certificate: ' + 'self signed certificate', + 'type': 'fatal' + } + ] + } + + +@fixture +def mock_exception_for_ssl_error(): + mock_response = MagicMock() + mock_response.reason.args.__getitem__().verify_message = 'self signed' \ + ' certificate' + return mock_response diff --git a/code/tests/unit/payloads_for_tests.py b/code/tests/unit/payloads_for_tests.py new file mode 100644 index 0000000..8e1ac50 --- /dev/null +++ b/code/tests/unit/payloads_for_tests.py @@ -0,0 +1,567 @@ +EXPECTED_RESPONSE_OF_JWKS_ENDPOINT = { + 'keys': [ + { + 'kty': 'RSA', + 'n': 'tSKfSeI0fukRIX38AHlKB1YPpX8PUYN2JdvfM-XjNmLfU1M74N0V' + 'mdzIX95sneQGO9kC2xMIE-AIlt52Yf_KgBZggAlS9Y0Vx8DsSL2H' + 'vOjguAdXir3vYLvAyyHin_mUisJOqccFKChHKjnk0uXy_38-1r17' + '_cYTp76brKpU1I4kM20M__dbvLBWjfzyw9ehufr74aVwr-0xJfsB' + 'Vr2oaQFww_XHGz69Q7yHK6DbxYO4w4q2sIfcC4pT8XTPHo4JZ2M7' + '33Ea8a7HxtZS563_mhhRZLU5aynQpwaVv2U--CL6EvGt8TlNZOke' + 'Rv8wz-Rt8B70jzoRpVK36rR-pHKlXhMGT619v82LneTdsqA25Wi2' + 'Ld_c0niuul24A6-aaj2u9SWbxA9LmVtFntvNbRaHXE1SLpLPoIp8' + 'uppGF02Nz2v3ld8gCnTTWfq_BQ80Qy8e0coRRABECZrjIMzHEg6M' + 'loRDy4na0pRQv61VogqRKDU2r3_VezFPQDb3ciYsZjWBr3HpNOkU' + 'jTrvLmFyOE9Q5R_qQGmc6BYtfk5rn7iIfXlkJAZHXhBy-ElBuiBM' + '-YSkFM7dH92sSIoZ05V4MP09Xcppx7kdwsJy72Sust9Hnd9B7V35' + 'YnVF6W791lVHnenhCJOziRmkH4xLLbPkaST2Ks3IHH7tVltM6NsR' + 'k3jNdVM', + 'e': 'AQAB', + 'alg': 'RS256', + 'kid': '02B1174234C29F8EFB69911438F597FF3FFEE6B7', + 'use': 'sig' + } + ] +} + +RESPONSE_OF_JWKS_ENDPOINT_WITH_WRONG_KEY = { + 'keys': [ + { + 'kty': 'RSA', + 'n': 'pSKfSeI0fukRIX38AHlKB1YPpX8PUYN2JdvfM-XjNmLfU1M74N0V' + 'mdzIX95sneQGO9kC2xMIE-AIlt52Yf_KgBZggAlS9Y0Vx8DsSL2H' + 'vOjguAdXir3vYLvAyyHin_mUisJOqccFKChHKjnk0uXy_38-1r17' + '_cYTp76brKpU1I4kM20M__dbvLBWjfzyw9ehufr74aVwr-0xJfsB' + 'Vr2oaQFww_XHGz69Q7yHK6DbxYO4w4q2sIfcC4pT8XTPHo4JZ2M7' + '33Ea8a7HxtZS563_mhhRZLU5aynQpwaVv2U--CL6EvGt8TlNZOke' + 'Rv8wz-Rt8B70jzoRpVK36rR-pHKlXhMGT619v82LneTdsqA25Wi2' + 'Ld_c0niuul24A6-aaj2u9SWbxA9LmVtFntvNbRaHXE1SLpLPoIp8' + 'uppGF02Nz2v3ld8gCnTTWfq_BQ80Qy8e0coRRABECZrjIMzHEg6M' + 'loRDy4na0pRQv61VogqRKDU2r3_VezFPQDb3ciYsZjWBr3HpNOkU' + 'jTrvLmFyOE9Q5R_qQGmc6BYtfk5rn7iIfXlkJAZHXhBy-ElBuiBM' + '-YSkFM7dH92sSIoZ05V4MP09Xcppx7kdwsJy72Sust9Hnd9B7V35' + 'YnVF6W791lVHnenhCJOziRmkH4xLLbPkaST2Ks3IHH7tVltM6NsR' + 'k3jNdVM', + 'e': 'AQAB', + 'alg': 'RS256', + 'kid': '02B1174234C29F8EFB69911438F597FF3FFEE6B7', + 'use': 'sig' + } + ] +} + +PRIVATE_KEY = '''-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY-----''' + +EXPECTED_RESPONSE_OF_SUCCESS_AUTH = { + 'access_token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhcGlfa2V5IjoiZXl' + 'KMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKSVV6STFOaUo5LmV5SmpkWE4' + 'wYjIxbGNpSTZJbU5wYzJOdklIUmxjM1FnWVdOamIzVnVkQ0lzSW1Gd2F' + 'WOXJaWGtpT2lKaGNHbGZhMlY1SWl3aWRYSnNJanB1ZFd4c0xDSmxlSEF' + 'pT2pFMk5qQXlNRGt4TXpoOS5VWmY4cWREaWhYQ3V1eEtvdEJHUlVkcXp' + 'CSHkxU1VkQUFLeHFtZG51bTVBIiwidXJsIjpudWxsLCJleHBpcmVzIjo' + 'xNjU3NjIzMTM4LjAwODE4ODV9.vY_wTF1C07XIBHCtdUg3MGY1umSboo' + 'hq4sBAA3UbPLA' +} + + +EXPECTED_RESPONSE_OF_GET_IP = { + 'domain': 'j-p.link', + 'ip': '116.203.177.93', + 'vulns': { + 'critical': 0, + 'high': 0, + 'medium': 7, + 'low': 0, + 'info': 30 + }, + 'cvss_total': 34.4, + 'open_ports': 5, + 'details_page': + 'https://www.cyberscan.io/vulnerabilities/j-p.link/116.203.177.93' +} + +EXPECTED_RESPONSE_OF_GET_VULNERABILITIES = { + 'ip': '116.203.177.93', + 'vulnerabilities': [ + { + 'name': 'OpenSSH ', + 'family': 'General', + 'description': '** DISPUTED ** scp in OpenSSH through 8.3p1 allow' + 's command injection in the scp.c toremote functio' + 'n, as demonstrated by backtick characters in the ' + 'destination argument. NOTE: the vendor reportedly' + ' has stated that they intentionally omit validati' + 'on of \"anomalous argument transfers\" because th' + 'at could \"stand a great chance of breaking exist' + 'ing workflows.\"', + 'severity': 'Medium', + 'port': 22, + 'protocol': 'tcp', + 'cve': 'CVE-2020-15778', + 'cvss': 6.8, + 'confidence': 30 + }, + { + 'name': 'Diffie-Hellman Ephemeral Key Exchange DoS Vulnerability ' + '(SSH, D(HE)ater)', + 'family': 'Denial of Service', + 'description': 'The remote SSH server is supporting Diffie-Hellma' + 'n ephemeral\n (DHE) Key Exchange (KEX) algorithm' + 's and thus could be prone to a denial of service ' + '(DoS)\n vulnerability.', + 'severity': 'Medium', + 'port': 22, + 'protocol': 'tcp', + 'cve': 'CVE-2002-20001', + 'cvss': 5, + 'confidence': 30 + }, + { + 'name': 'SSL/TLS: Known Untrusted / Dangerous Certificate Authori' + 'ty (CA) Detection', + 'family': 'SSL and TLS', + 'description': 'The service is using an SSL/TLS certificate from ' + 'a known\n untrusted and/or dangerous certificate' + ' authority (CA).', + 'severity': 'Medium', + 'port': 443, + 'protocol': 'tcp', + 'cve': '', + 'cvss': 5, + 'confidence': 99 + }, + { + 'name': 'OpenSSH 8.2 ', + 'family': 'General', + 'description': 'ssh-agent in OpenSSH before 8.5 has a double free' + ' that may be relevant in a few less-common scenar' + 'ios, such as unconstrained agent-socket access on' + ' a legacy operating system, or the forwarding of ' + 'an agent to an attacker-controlled host.', + 'severity': 'Medium', + 'port': 22, + 'protocol': 'tcp', + 'cve': 'CVE-2021-28041', + 'cvss': 4.6, + 'confidence': 30 + }, + { + 'name': 'OpenSSH 6.2 ', + 'family': 'Privilege escalation', + 'description': 'OpenSSH is prone to a privilege scalation vulnera' + 'bility in\n certain configurations.', + 'severity': 'Medium', + 'port': 22, + 'protocol': 'tcp', + 'cve': 'CVE-2021-41617', + 'cvss': 4.4, + 'confidence': 30 + }, + { + 'name': 'OpenSSH Information Disclosure Vulnerability ' + '(CVE-2016-20012)', + 'family': 'General', + 'description': 'OpenBSD OpenSSH is prone to an information disclo' + 'sure\n vulnerability.', + 'severity': 'Medium', + 'port': 22, + 'protocol': 'tcp', + 'cve': 'CVE-2016-20012', + 'cvss': 4.3, + 'confidence': 50 + }, + { + 'name': 'OpenBSD OpenSSH Information Disclosure Vulnerability ' + '(CVE-2020-14145)', + 'family': 'General', + 'description': 'The client side in OpenSSH 5.7 through 8.4 has an' + ' Observable Discrepancy leading to an information' + ' leak in the algorithm negotiation. This allows m' + 'an-in-the-middle attackers to target initial conn' + 'ection attempts (where no host key for the server' + ' has been cached by the client).', + 'severity': 'Medium', + 'port': 22, + 'protocol': 'tcp', + 'cve': 'CVE-2020-14145', + 'cvss': 4.3, + 'confidence': 30 + } + ] +} + +EXPECTED_RELAY_RESPONSE = { + 'data': + { + 'sightings': { + 'count': 7, + 'docs': [ + { + 'confidence': 'High', + 'count': 1, + 'data': { + 'columns': [ + {'name': 'name', 'type': 'string'}, + {'name': 'family', 'type': 'string'}, + {'name': 'description', 'type': 'string'}, + {'name': 'severity', 'type': 'string'}, + {'name': 'port', 'type': 'string'}, + {'name': 'protocol', 'type': 'string'}, + {'name': 'cve', 'type': 'string'}, + {'name': 'cvss', 'type': 'string'}, + {'name': 'confidence', 'type': 'string'}], + 'rows': [ + [ + 'OpenSSH ', 'General', + '** DISPUTED ** scp in OpenSSH through 8.' + '3p1 allows command injection in the scp.' + 'c toremote function, as demonstrated by ' + 'backtick characters in the destination a' + 'rgument. NOTE: the vendor reportedly has' + ' stated that they intentionally omit val' + 'idation of "anomalous argument transfers' + '" because that could "stand a great chan' + 'ce of breaking existing workflows."', + 'Medium', '22', 'tcp', 'CVE-2020-15778', + '6.8', '30' + ] + ] + }, + 'description': '** DISPUTED ** scp in OpenSSH through' + ' 8.3p1 allows command injection in th' + 'e scp.c toremote function, as demonst' + 'rated by backtick characters in the d' + 'estination argument. NOTE: the vendor' + ' reportedly has stated that they inte' + 'ntionally omit validation of "anomalo' + 'us argument transfers" because that c' + 'ould "stand a great chance of breakin' + 'g existing workflows."', + 'id': 'transient:sighting-c9826d98-35df-4b8b-a61f-e52' + '313920c5a', + 'internal': True, + 'observables': + [{'type': 'domain', 'value': 'j-p.link'}], + 'observed_time': + {'start_time': '2022-07-12T09:38:46Z'}, + 'schema_version': '1.1.11', + 'short_description': 'Vulnerability CVE-2020-15778 ob' + 'served at CyberScan', + 'source': 'CyberScan', 'title': 'OpenSSH ', + 'type': 'sighting' + }, + { + 'confidence': 'High', 'count': 1, + 'data': { + 'columns': [ + {'name': 'name', 'type': 'string'}, + {'name': 'family', 'type': 'string'}, + {'name': 'description', 'type': 'string'}, + {'name': 'severity', 'type': 'string'}, + {'name': 'port', 'type': 'string'}, + {'name': 'protocol', 'type': 'string'}, + {'name': 'cve', 'type': 'string'}, + {'name': 'cvss', 'type': 'string'}, + {'name': 'confidence', 'type': 'string'}], + 'rows': [[ + 'Diffie-Hellman Ephemeral Key Exchange DoS Vu' + 'lnerability (SSH, D(HE)ater)', + 'Denial of Service', + 'The remote SSH server is supporting Diffie-H' + 'ellman ephemeral\n (DHE) Key Exchange (KEX)' + ' algorithms and thus could be prone to a den' + 'ial of service (DoS)\n vulnerability.', + 'Medium', '22', 'tcp', 'CVE-2002-20001', '5', + '30']] + }, + 'description': 'The remote SSH server is supporting D' + 'iffie-Hellman ephemeral\n (DHE) Key ' + 'Exchange (KEX) algorithms and thus co' + 'uld be prone to a denial of service (' + 'DoS)\n vulnerability.', + 'id': 'transient:sighting-8d518924-a3ac-4e3f-b0fd-4d0' + '17c219cf1', + 'internal': True, + 'observables': + [{'type': 'domain', 'value': 'j-p.link'}], + 'observed_time': + {'start_time': '2022-07-12T09:38:46Z'}, + 'schema_version': '1.1.11', + 'short_description': 'Vulnerability CVE-2002-20001 ob' + 'served at CyberScan', + 'source': 'CyberScan', + 'title': 'Diffie-Hellman Ephemeral Key Exchange DoS V' + 'ulnerability (SSH, D(HE)ater)', + 'type': 'sighting' + }, + { + 'confidence': 'High', 'count': 1, + 'data': { + 'columns': [ + {'name': 'name', 'type': 'string'}, + {'name': 'family', 'type': 'string'}, + {'name': 'description', 'type': 'string'}, + {'name': 'severity', 'type': 'string'}, + {'name': 'port', 'type': 'string'}, + {'name': 'protocol', 'type': 'string'}, + {'name': 'cve', 'type': 'string'}, + {'name': 'cvss', 'type': 'string'}, + {'name': 'confidence', 'type': 'string'}], + 'rows': [[ + 'SSL/TLS: Known Untrusted / Dangerous Certifi' + 'cate Authority (CA) Detection', + 'SSL and TLS', + 'The service is using an SSL/TLS certificate' + ' from a known\n untrusted and/or dangerous' + ' certificate authority (CA).', + 'Medium', '443', 'tcp', '', '5', '99']] + }, + 'description': + 'The service is using an SSL/TLS certificate from' + ' a known\n untrusted and/or dangerous certifica' + 'te authority (CA).', + 'id': 'transient:sighting-ea815346-d9a8-4efb-9bf0-ed3' + 'a8ebabf65', + 'internal': True, + 'observables': + [{'type': 'domain', 'value': 'j-p.link'}], + 'observed_time': + {'start_time': '2022-07-12T09:38:46Z'}, + 'schema_version': '1.1.11', + 'short_description': + 'Vulnerability observed at CyberScan', + 'source': 'CyberScan', + 'title': 'SSL/TLS: Known Untrusted / Dangerous Certif' + 'icate Authority (CA) Detection', + 'type': 'sighting' + }, + { + 'confidence': 'High', 'count': 1, + 'data': { + 'columns': [ + {'name': 'name', 'type': 'string'}, + {'name': 'family', 'type': 'string'}, + {'name': 'description', 'type': 'string'}, + {'name': 'severity', 'type': 'string'}, + {'name': 'port', 'type': 'string'}, + {'name': 'protocol', 'type': 'string'}, + {'name': 'cve', 'type': 'string'}, + {'name': 'cvss', 'type': 'string'}, + {'name': 'confidence', 'type': 'string'}], + 'rows': [[ + 'OpenSSH 8.2 ', 'General', + 'ssh-agent in OpenSSH before 8.5 has a double' + ' free that may be relevant in a few less-com' + 'mon scenarios, such as unconstrained agent-s' + 'ocket access on a legacy operating system, o' + 'r the forwarding of an agent to an attacker-' + 'controlled host.', 'Medium', '22', 'tcp', + 'CVE-2021-28041', '4.6', '30']] + }, + 'description': 'ssh-agent in OpenSSH before 8.5 has a' + ' double free that may be relevant in ' + 'a few less-common scenarios, such as ' + 'unconstrained agent-socket access on ' + 'a legacy operating system, or the for' + 'warding of an agent to an attacker-co' + 'ntrolled host.', + 'id': 'transient:sighting-ca6a3495-0863-4789-83ab-039' + 'a57a5a84d', + 'internal': True, + 'observables': + [{'type': 'domain', 'value': 'j-p.link'}], + 'observed_time': + {'start_time': '2022-07-12T09:38:46Z'}, + 'schema_version': '1.1.11', + 'short_description': 'Vulnerability CVE-2021-28041 ob' + 'served at CyberScan', + 'source': 'CyberScan', 'title': 'OpenSSH 8.2 ', + 'type': 'sighting' + }, + { + 'confidence': 'High', 'count': 1, + 'data': { + 'columns': [ + {'name': 'name', 'type': 'string'}, + {'name': 'family', 'type': 'string'}, + {'name': 'description', 'type': 'string'}, + {'name': 'severity', 'type': 'string'}, + {'name': 'port', 'type': 'string'}, + {'name': 'protocol', 'type': 'string'}, + {'name': 'cve', 'type': 'string'}, + {'name': 'cvss', 'type': 'string'}, + {'name': 'confidence', 'type': 'string'}], + 'rows': [[ + 'OpenSSH 6.2 ', 'Privilege escalation', + 'OpenSSH is prone to a privilege scalation vu' + 'lnerability in\n certain configurations.', + 'Medium', '22', 'tcp', 'CVE-2021-41617', + '4.4', '30']] + }, + 'description': 'OpenSSH is prone to a privilege scala' + 'tion vulnerability in\n certain conf' + 'igurations.', + 'id': 'transient:sighting-f994a79c-6134-4334-86b3-b84' + '165eb10a9', + 'internal': True, + 'observables': + [{'type': 'domain', 'value': 'j-p.link'}], + 'observed_time': + {'start_time': '2022-07-12T09:38:46Z'}, + 'schema_version': '1.1.11', + 'short_description': 'Vulnerability CVE-2021-41617 ob' + 'served at CyberScan', + 'source': 'CyberScan', 'title': 'OpenSSH 6.2 ', + 'type': 'sighting' + }, + { + 'confidence': 'High', 'count': 1, + 'data': { + 'columns': [ + {'name': 'name', 'type': 'string'}, + {'name': 'family', 'type': 'string'}, + {'name': 'description', 'type': 'string'}, + {'name': 'severity', 'type': 'string'}, + {'name': 'port', 'type': 'string'}, + {'name': 'protocol', 'type': 'string'}, + {'name': 'cve', 'type': 'string'}, + {'name': 'cvss', 'type': 'string'}, + {'name': 'confidence', 'type': 'string'}], + 'rows': [[ + 'OpenSSH Information Disclosure Vulnerability' + ' (CVE-2016-20012)', 'General', + 'OpenBSD OpenSSH is prone to an information d' + 'isclosure\n vulnerability.', 'Medium', + '22', 'tcp', 'CVE-2016-20012', '4.3', '50']] + }, + 'description': 'OpenBSD OpenSSH is prone to an inform' + 'ation disclosure\n vulnerability.', + 'id': 'transient:sighting-0d266e2b-1f5a-40ec-9b45-808' + '24d1672bf', + 'internal': True, + 'observables': + [{'type': 'domain', 'value': 'j-p.link'}], + 'observed_time': + {'start_time': '2022-07-12T09:38:46Z'}, + 'schema_version': '1.1.11', + 'short_description': 'Vulnerability CVE-2016-20012 ob' + 'served at CyberScan', + 'source': 'CyberScan', + 'title': 'OpenSSH Information Disclosure Vulnerabilit' + 'y (CVE-2016-20012)', + 'type': 'sighting' + }, + { + 'confidence': 'High', 'count': 1, + 'data': { + 'columns': [ + {'name': 'name', 'type': 'string'}, + {'name': 'family', 'type': 'string'}, + {'name': 'description', 'type': 'string'}, + {'name': 'severity', 'type': 'string'}, + {'name': 'port', 'type': 'string'}, + {'name': 'protocol', 'type': 'string'}, + {'name': 'cve', 'type': 'string'}, + {'name': 'cvss', 'type': 'string'}, + {'name': 'confidence', 'type': 'string'}], + 'rows': [[ + 'OpenBSD OpenSSH Information Disclosure Vulne' + 'rability (CVE-2020-14145)', 'General', + 'The client side in OpenSSH 5.7 through 8.4 h' + 'as an Observable Discrepancy leading to an i' + 'nformation leak in the algorithm negotiation' + '. This allows man-in-the-middle attackers to' + ' target initial connection attempts (where n' + 'o host key for the server has been cached by' + ' the client).', 'Medium', '22', 'tcp', + 'CVE-2020-14145', '4.3', '30']] + }, + 'description': 'The client side in OpenSSH 5.7 throug' + 'h 8.4 has an Observable Discrepancy l' + 'eading to an information leak in the ' + 'algorithm negotiation. This allows ma' + 'n-in-the-middle attackers to target i' + 'nitial connection attempts (where no ' + 'host key for the server has been cach' + 'ed by the client).', + 'id': 'transient:sighting-65b3711e-ed3a-46d6-adb3-78f' + 'e944ecf69', + 'internal': True, + 'observables': + [{'type': 'domain', 'value': 'j-p.link'}], + 'observed_time': + {'start_time': '2022-07-12T09:38:46Z'}, + 'schema_version': '1.1.11', + 'short_description': 'Vulnerability CVE-2020-14145 ob' + 'served at CyberScan', + 'source': 'CyberScan', + 'title': 'OpenBSD OpenSSH Information Disclosure Vuln' + 'erability (CVE-2020-14145)', + 'type': 'sighting' + } + ] + } + } +} + +EXPECTED_REFER_RESPONSE = { + 'data': [ + {'categories': ['CyberScan'], + 'description': 'Details for this domain in the CyberScan', + 'id': 'ref-cyberscan-search-domain-j-p.link', + 'title': 'Details for this domain', + 'url': 'https://www.cyberscan.io/vulnerabilities' + '/j-p.link/116.203.177.93' + } + ], +} diff --git a/code/tests/unit/test_app.py b/code/tests/unit/test_app.py new file mode 100644 index 0000000..cc631a2 --- /dev/null +++ b/code/tests/unit/test_app.py @@ -0,0 +1,32 @@ +from collections import namedtuple +from http import HTTPStatus + +from pytest import fixture + + +Call = namedtuple('Call', ('method', 'route', 'expected_status_code')) + + +def calls(): + yield Call('POST', '/post', HTTPStatus.NOT_FOUND) + yield Call('GET', '/get', HTTPStatus.NOT_FOUND) + yield Call('PUT', '/put', HTTPStatus.NOT_FOUND) + yield Call('DELETE', '/delete', HTTPStatus.NOT_FOUND) + + yield Call('GET', '/version', HTTPStatus.METHOD_NOT_ALLOWED) + yield Call('GET', '/health', HTTPStatus.METHOD_NOT_ALLOWED) + yield Call('GET', '/observe/observables', HTTPStatus.METHOD_NOT_ALLOWED) + yield Call('GET', '/refer/observables', HTTPStatus.METHOD_NOT_ALLOWED) + yield Call('POST', '/watchdog', HTTPStatus.METHOD_NOT_ALLOWED) + + +@fixture(scope='module', + params=calls(), + ids=lambda call: f'{call.method} {call.route}') +def call(request): + return request.param + + +def test_non_relay_call_failure(call, client): + response = client.open(call.route, method=call.method) + assert response.status_code == call.expected_status_code diff --git a/module_type.json.sample b/module_type.json.sample new file mode 100644 index 0000000..bc6066a --- /dev/null +++ b/module_type.json.sample @@ -0,0 +1,62 @@ +{ + "title": "CyberScan", + "default_name": "CyberScan", + "short_description": "CyberScan is a vulnerability scanner, a penetration tool and an OSINT-scanner in one product.", + "description": "CyberScan is a portal designed for security measures, combining functions of vulnerability scanner, penetration tool and open source intelligence tool. In just a few steps you get a comprehensive vulnerability view of your systems.", + "tips": "When configuring CyberScan integration, you must first obtain an API key for your CyberScan account and then add the CyberScan integration module in SecureX.\n\n1. To obtain a CyberScan API key please reach out to your CyberScan team and let them know your interest in the SecureX integration. \n\n2. In SecureX, complete the **Add New CyberScan Integration Module** form:\n - **Integration Module Name** - Leave the default name or enter a name that is meaningful to you.\n - **API KEY** - Enter CyberScan API key\n - **CyberScan API key** - Enter host of your CyberScan api\n - **Entities Limit** - Specify the maximum number of sightings, indicators, and judgements in a single response, per requested observable (must be a positive value). We recommend that you enter a limit in the range of 50 to 100. The default is 100 entities.\n3. Click **Save** to complete the CyberScan integration module configuration.", + "external_references": [ + { + "label": "CyberScan", + "link": "https://www.cyberscan.io/" + } + ], + "configuration_spec": [ + { + "key": "custom_api_key", + "type": "api_key", + "label": "API KEY", + "tooltip": "CyberScan API KEY", + "required": true + }, + { + "key": "custom_host", + "type": "string", + "label": "CyberScan API Host", + "tooltip": "CyberScan API Host", + "required": true + }, + { + "key": "custom_CTR_ENTITIES_LIMIT", + "type": "integer", + "label": "Entities Limit", + "tooltip": "Restricts the maximum number of `Sightings`. Please note that the number over 100 might lead to data inconsistency.", + "required": false + } + ], + "capabilities": [ + { + "id": "health", + "description": "Healthcheck" + }, + { + "id": "observe", + "description": "Enrichments" + }, + { + "id": "refer", + "description": "Reference links" + } + ], + "properties": { + "supported-apis": [ + "health", + "observe/observables", + "refer/observables" + ], + "auth-type": "configuration-token", + "configuration-token-alg": "RS256", + "custom_jwks_host": "visibility.amp.cisco.com", + "url": "https://ciscohosted.url" + }, + "logo": "data:image/png;base64,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" +} diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100644 index 0000000..bccbded --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env sh +set -e + + +if [ -n "$ALPINEPYTHON" ] ; then + export PYTHONPATH=$PYTHONPATH:/usr/local/lib/$ALPINEPYTHON/site-packages:/usr/lib/$ALPINEPYTHON/site-packages +fi + +exec "$@" diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100644 index 0000000..b7df3c3 --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,13 @@ +#! /usr/bin/env sh +if [[ -z "${DEBUG}" ]]; then + echo "[start.sh] DEBUG MODE OFF" >> /var/log/messages +else + echo "[start.sh] DEBUG MODE ON" >> /var/log/messages + echo "[start.sh] ............." >> /var/log/messages + echo "[start.sh] Integration Module: " `jq -r .NAME /app/container_settings.json` >> /var/log/messages + echo "[start.sh] Version: " `jq -r .VERSION /app/container_settings.json` >> /var/log/messages + echo "[start.sh] Starting supervisord ..." >> /var/log/messages + echo "[start.sh] ............." >> /var/log/messages +fi +set -e +exec /usr/bin/supervisord -c /supervisord.ini diff --git a/scripts/supervisord.ini b/scripts/supervisord.ini new file mode 100644 index 0000000..f642bc8 --- /dev/null +++ b/scripts/supervisord.ini @@ -0,0 +1,18 @@ +[supervisord] +nodaemon=true +user=root + +[program:syslog-ng] +command=/usr/sbin/syslog-ng --foreground -f /syslog-ng.conf --no-caps +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr + +[program:uwsgi] +command=/usr/sbin/uwsgi /etc/uwsgi/uwsgi.ini +stdout_logfile=/var/log/messages +stdout_logfile_maxbytes=100000 +stderr_logfile=/var/log/messages +stderr_logfile_maxbytes=100000 +stdout_logfile_backups=0 +stderr_logfile_backups=0 diff --git a/scripts/syslog-ng.conf b/scripts/syslog-ng.conf new file mode 100644 index 0000000..4dd29bd --- /dev/null +++ b/scripts/syslog-ng.conf @@ -0,0 +1,12 @@ +@version:3.30 + +source s_src { file("/var/log/messages"); }; +destination d_stdout { pipe("/dev/stdout"); }; + +log { + source(s_src); + filter { + not message("ERROR in app") and not message("GET /watchdog") + }; + destination(d_stdout); +}; diff --git a/scripts/uwsgi.ini b/scripts/uwsgi.ini new file mode 100644 index 0000000..c5b4976 --- /dev/null +++ b/scripts/uwsgi.ini @@ -0,0 +1,14 @@ +[uwsgi] +http = :9090 +chdir = /app +module = app +callable = app +processes = 4 +threads = 2 +plugin = http,python3,syslog +master = true +gid = uwsgi +uid = uwsgi +log-x-forwarded-for = true +log-format = %(addr) - %(user) [%(ltime)] "%(method) %(uri) %(proto)" %(status) %(size) "%(referer)" "%(uagent)" +log-master = true