From 77e571141ee9f49add61b7a398b2487201b372b6 Mon Sep 17 00:00:00 2001 From: Taras Maliuchenko Date: Wed, 10 Mar 2021 09:58:43 +0200 Subject: [PATCH 01/33] Create template for docker hosting Remove all mentions about AWS hosting --- .dockerignore | 2 + .gitignore | 12 ++ .travis.yml | 11 ++ Dockerfile | 19 +++ LICENSE.md | 22 +++ README.md | 85 +++++++++++ build.sh | 32 ++++ code/api/__init__.py | 0 code/api/dashboard.py | 25 +++ code/api/enrich.py | 29 ++++ code/api/errors.py | 41 +++++ code/api/health.py | 10 ++ code/api/respond.py | 24 +++ code/api/schemas.py | 57 +++++++ code/api/utils.py | 136 +++++++++++++++++ code/api/version.py | 8 + code/api/watchdog.py | 14 ++ code/app.py | 46 ++++++ code/config.py | 6 + code/container_settings.json | 1 + code/requirements.txt | 9 ++ code/tests/__init__.py | 0 code/tests/unit/__init__.py | 0 code/tests/unit/api/__init__.py | 0 code/tests/unit/api/test_authorization.py | 176 ++++++++++++++++++++++ code/tests/unit/api/test_dashboard.py | 107 +++++++++++++ code/tests/unit/api/test_enrich.py | 54 +++++++ code/tests/unit/api/test_health.py | 19 +++ code/tests/unit/api/test_respond.py | 106 +++++++++++++ code/tests/unit/api/test_version.py | 25 +++ code/tests/unit/api/test_watchdog.py | 20 +++ code/tests/unit/api/utils.py | 2 + code/tests/unit/conftest.py | 71 +++++++++ code/tests/unit/payloads_for_tests.py | 103 +++++++++++++ code/tests/unit/test_app.py | 38 +++++ module_type.json.sample | 18 +++ scripts/entrypoint.sh | 15 ++ scripts/start.sh | 13 ++ scripts/supervisord.ini | 18 +++ scripts/syslog-ng.conf | 12 ++ scripts/uwsgi.ini | 14 ++ 41 files changed, 1400 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Dockerfile create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 build.sh create mode 100644 code/api/__init__.py create mode 100644 code/api/dashboard.py create mode 100644 code/api/enrich.py create mode 100644 code/api/errors.py create mode 100644 code/api/health.py create mode 100644 code/api/respond.py create mode 100644 code/api/schemas.py create mode 100644 code/api/utils.py create mode 100644 code/api/version.py create mode 100644 code/api/watchdog.py create mode 100644 code/app.py create mode 100644 code/config.py create mode 100644 code/container_settings.json create mode 100644 code/requirements.txt create mode 100644 code/tests/__init__.py create mode 100644 code/tests/unit/__init__.py create mode 100644 code/tests/unit/api/__init__.py create mode 100644 code/tests/unit/api/test_authorization.py create mode 100644 code/tests/unit/api/test_dashboard.py create mode 100644 code/tests/unit/api/test_enrich.py create mode 100644 code/tests/unit/api/test_health.py create mode 100644 code/tests/unit/api/test_respond.py create mode 100644 code/tests/unit/api/test_version.py create mode 100644 code/tests/unit/api/test_watchdog.py create mode 100644 code/tests/unit/api/utils.py create mode 100644 code/tests/unit/conftest.py create mode 100644 code/tests/unit/payloads_for_tests.py create mode 100644 code/tests/unit/test_app.py create mode 100644 module_type.json.sample create mode 100644 scripts/entrypoint.sh create mode 100644 scripts/start.sh create mode 100644 scripts/supervisord.ini create mode 100644 scripts/syslog-ng.conf create mode 100644 scripts/uwsgi.ini 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/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c106c1a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +services: +- docker +jobs: + include: + - stage: build&test + script: + - docker build -t tr-05-module-name . + - docker run -d -p 9090:9090 --name tr-05-module-name tr-05-module-name + - while true; do if docker logs tr-05-module-name | grep "entered RUNNING state"; then + break; else sleep 1; fi done + - curl -X POST -sSLi http://localhost:9090 | grep '200 OK' diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e17c40e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM alpine:3.13 +LABEL maintainer="Ian Redden " + +# install packages we need +RUN apk update && apk add --no-cache musl-dev openssl-dev gcc python3 py3-configobj python3-dev supervisor git libffi-dev uwsgi-python3 uwsgi-http jq nano syslog-ng uwsgi-syslog py3-pip + +# do the Python dependencies +ADD code /app +RUN pip3 install -r /app/requirements.txt +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/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 new file mode 100644 index 0000000..6cb6967 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +[![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) + +Generic Docker Relay template not bound to any real third-party Cyber Threat +Intelligence service provider. + +**NOTE.** The template aims to show the general structure for future +implementations. It also provides a couple of utility functions that might be +handy. Keep in mind that the main idea here is to just give you a hint of a +possible approach rather than enforcing you to do everything exactly the same +way. + +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 [requirements.txt](requirements.txt) file: +``` +pip install --upgrade --requirement requirements.txt +``` + +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-module-name . +``` + + 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-module-name tr-05-module-name +``` + + 3. Watch the container logs to ensure it starts correctly. + +``` +docker logs tr-05-module-name +``` + + 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 + +**NOTE.** Remember that this application is just a template so here `N/A` means +that it has no implemented Relay endpoints and supported types of observables. +That will not be the case for real integrations with third-party services so +you may consider the following sections as some placeholders. + +### Implemented Relay Endpoints + +`N/A` + +### Supported Types of Observables + +`N/A` diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..2c0c300 --- /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="Module name" +image_name="tr-05-module-name" + +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/api/__init__.py b/code/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code/api/dashboard.py b/code/api/dashboard.py new file mode 100644 index 0000000..4f93871 --- /dev/null +++ b/code/api/dashboard.py @@ -0,0 +1,25 @@ +from flask import Blueprint +from api.utils import jsonify_data, get_jwt, get_json +from api.schemas import DashboardTileSchema, DashboardTileDataSchema + +dashboard_api = Blueprint('dashboard', __name__) + + +@dashboard_api.route('/tiles', methods=['POST']) +def tiles(): + _ = get_jwt() + return jsonify_data([]) + + +@dashboard_api.route('/tiles/tile', methods=['POST']) +def tile(): + _ = get_jwt() + _ = get_json(DashboardTileSchema()) + return jsonify_data({}) + + +@dashboard_api.route('/tiles/tile-data', methods=['POST']) +def tile_data(): + _ = get_jwt() + _ = get_json(DashboardTileDataSchema()) + return jsonify_data({}) diff --git a/code/api/enrich.py b/code/api/enrich.py new file mode 100644 index 0000000..a3d333b --- /dev/null +++ b/code/api/enrich.py @@ -0,0 +1,29 @@ +from flask import Blueprint +from functools import partial +from api.schemas import ObservableSchema +from api.utils import get_json, get_jwt, jsonify_data + +enrich_api = Blueprint('enrich', __name__) + +get_observables = partial(get_json, schema=ObservableSchema(many=True)) + + +@enrich_api.route('/deliberate/observables', methods=['POST']) +def deliberate_observables(): + _ = get_jwt() + _ = get_observables() + return jsonify_data({}) + + +@enrich_api.route('/observe/observables', methods=['POST']) +def observe_observables(): + _ = get_jwt() + _ = get_observables() + return jsonify_data({}) + + +@enrich_api.route('/refer/observables', methods=['POST']) +def refer_observables(): + _ = get_jwt() + _ = get_observables() + return jsonify_data([]) diff --git a/code/api/errors.py b/code/api/errors.py new file mode 100644 index 0000000..f12be5b --- /dev/null +++ b/code/api/errors.py @@ -0,0 +1,41 @@ +AUTH_ERROR = 'authorization error' +INVALID_ARGUMENT = 'invalid argument' +UNKNOWN = 'unknown' + + +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' + ) diff --git a/code/api/health.py b/code/api/health.py new file mode 100644 index 0000000..34427c1 --- /dev/null +++ b/code/api/health.py @@ -0,0 +1,10 @@ +from flask import Blueprint +from api.utils import get_jwt, jsonify_data + +health_api = Blueprint('health', __name__) + + +@health_api.route('/health', methods=['POST']) +def health(): + _ = get_jwt() + return jsonify_data({'status': 'ok'}) diff --git a/code/api/respond.py b/code/api/respond.py new file mode 100644 index 0000000..ed4768c --- /dev/null +++ b/code/api/respond.py @@ -0,0 +1,24 @@ + +from flask import Blueprint +from functools import partial +from api.utils import get_json, get_jwt, jsonify_data +from api.schemas import ObservableSchema, ActionFormParamsSchema + +respond_api = Blueprint('respond', __name__) + +get_observables = partial(get_json, schema=ObservableSchema(many=True)) +get_action_form_params = partial(get_json, schema=ActionFormParamsSchema()) + + +@respond_api.route('/respond/observables', methods=['POST']) +def respond_observables(): + _ = get_jwt() + _ = get_observables() + return jsonify_data([]) + + +@respond_api.route('/respond/trigger', methods=['POST']) +def respond_trigger(): + _ = get_jwt() + _ = get_action_form_params() + return jsonify_data({'status': 'success'}) 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..17aed1c --- /dev/null +++ b/code/api/utils.py @@ -0,0 +1,136 @@ +import jwt +import json +import requests + +from flask import request, jsonify +from requests.exceptions import ConnectionError, InvalidURL +from api.errors import AuthorizationError, InvalidArgumentError +from jwt import InvalidSignatureError, DecodeError, InvalidAudienceError + + +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: WRONG_JWKS_HOST, + InvalidURL: WRONG_JWKS_HOST, + } + try: + response = requests.get(f"https://{jwks_host}/.well-known/jwks") + 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 tuple(expected_errors) as error: + message = expected_errors[error.__class__] + raise AuthorizationError(message) + + +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_jwt(): + """ + 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: WRONG_PAYLOAD_STRUCTURE, + AssertionError: JWKS_HOST_MISSING, + 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} + ).get('jwks_host') + assert jwks_host + key = get_public_key(jwks_host, token) + aud = request.url_root + payload = jwt.decode( + token, key=key, algorithms=['RS256'], audience=[aud.rstrip('/')] + ) + return payload['key'] + 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]}) 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..1e517c8 --- /dev/null +++ b/code/api/watchdog.py @@ -0,0 +1,14 @@ +from api.utils import jsonify_data +from flask import request, Blueprint +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..0b1e43e --- /dev/null +++ b/code/app.py @@ -0,0 +1,46 @@ +from flask import Flask, jsonify + +from api.dashboard import dashboard_api +from api.enrich import enrich_api +from api.health import health_api +from api.respond import respond_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(dashboard_api) +app.register_blueprint(enrich_api) +app.register_blueprint(health_api) +app.register_blueprint(respond_api) +app.register_blueprint(version_api) +app.register_blueprint(watchdog_api) + + +@app.errorhandler(Exception) +def handle_error(exception): + app.logger.error(exception) + code = getattr(exception, 'code', 500) + message = getattr(exception, 'description', 'Something went wrong.') + reason = '.'.join([ + exception.__class__.__module__, + exception.__class__.__name__, + ]) + + response = jsonify(code=code, message=message, reason=reason) + return response, code + + +@app.errorhandler(TRFormattedError) +def handle_tr_formatted_error(exception): + app.logger.error(exception) + 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..d380803 --- /dev/null +++ b/code/config.py @@ -0,0 +1,6 @@ +import json + + +class Config: + settings = json.load(open('container_settings.json', 'r')) + VERSION = settings["VERSION"] diff --git a/code/container_settings.json b/code/container_settings.json new file mode 100644 index 0000000..2e874f8 --- /dev/null +++ b/code/container_settings.json @@ -0,0 +1 @@ +{"VERSION": "1.0.0", "NAME": "Module name"} diff --git a/code/requirements.txt b/code/requirements.txt new file mode 100644 index 0000000..ff4cdc3 --- /dev/null +++ b/code/requirements.txt @@ -0,0 +1,9 @@ +cryptography==3.3.2 +Flask==1.1.2 +marshmallow==3.8.0 +zappa==0.52.0 +requests==2.24.0 +pyjwt[crypto]==2.0.0 +flake8==3.8.3 +coverage==5.2.1 +pytest==6.0.1 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..afe7d74 --- /dev/null +++ b/code/tests/unit/api/test_authorization.py @@ -0,0 +1,176 @@ +from pytest import fixture +from http import HTTPStatus + +from .utils import get_headers +from unittest.mock import patch +from api.errors import AUTH_ERROR +from ..conftest import mock_api_response +from api.utils import ( + WRONG_PAYLOAD_STRUCTURE, + WRONG_KEY, + WRONG_AUDIENCE, + KID_NOT_FOUND, + JWKS_HOST_MISSING +) +from ..payloads_for_tests import ( + EXPECTED_RESPONSE_OF_JWKS_ENDPOINT, + RESPONSE_OF_JWKS_ENDPOINT_WITH_WRONG_KEY +) + + +def routes(): + yield '/health' + yield '/deliberate/observables' + yield '/observe/observables' + yield '/refer/observables' + yield '/respond/observables' + yield '/respond/trigger' + + +@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(jwks_host='')) + ) + assert response.status_code == HTTPStatus.OK + assert response.json == authorization_errors_expected_payload( + JWKS_HOST_MISSING + ) diff --git a/code/tests/unit/api/test_dashboard.py b/code/tests/unit/api/test_dashboard.py new file mode 100644 index 0000000..02f562a --- /dev/null +++ b/code/tests/unit/api/test_dashboard.py @@ -0,0 +1,107 @@ + + +from pytest import fixture +from http import HTTPStatus +from .utils import get_headers +from unittest.mock import patch +from collections import namedtuple +from api.errors import INVALID_ARGUMENT +from ..conftest import mock_api_response +from ..payloads_for_tests import EXPECTED_RESPONSE_OF_JWKS_ENDPOINT + +WrongCall = namedtuple('WrongCall', ('endpoint', 'payload', 'message')) + + +def wrong_calls(): + yield WrongCall( + '/tiles/tile', + {'tile-id': 'some_value'}, + "{'tile_id': ['Missing data for required field.'], " + "'tile-id': ['Unknown field.']}" + ) + yield WrongCall( + '/tiles/tile', + {'tile_id': ''}, + "{'tile_id': ['Field may not be blank.']}" + ) + yield WrongCall( + '/tiles/tile-data', + {'tile-id': 'some_value', 'period': 'some_period'}, + "{'tile_id': ['Missing data for required field.'], " + "'tile-id': ['Unknown field.']}" + ) + yield WrongCall( + '/tiles/tile-data', + {'tile_id': '', 'period': 'some_period'}, + "{'tile_id': ['Field may not be blank.']}" + ) + yield WrongCall( + '/tiles/tile-data', + {'tile_id': 'some_value', 'not_period': 'some_period'}, + "{'period': ['Missing data for required field.'], " + "'not_period': ['Unknown field.']}" + ) + yield WrongCall( + '/tiles/tile-data', + {'tile_id': 'some_value', 'period': ''}, + "{'period': ['Field may not be blank.']}" + ) + + +@fixture( + scope='module', + params=wrong_calls(), + ids=lambda wrong_payload: f'{wrong_payload.endpoint}, ' + f'{wrong_payload.payload}' +) +def wrong_call(request): + return request.param + + +@fixture(scope='module') +def invalid_argument_expected_payload(): + def _make_message(message): + return { + 'errors': [{ + 'code': INVALID_ARGUMENT, + 'message': message, + 'type': 'fatal' + }] + } + + return _make_message + + +@patch('requests.get') +def test_dashboard_call_with_wrong_payload(mock_request, + wrong_call, client, valid_jwt, + invalid_argument_expected_payload): + + mock_request.return_value = \ + mock_api_response(payload=EXPECTED_RESPONSE_OF_JWKS_ENDPOINT) + + response = client.post( + path=wrong_call.endpoint, + headers=get_headers(valid_jwt()), + json=wrong_call.payload + ) + assert response.status_code == HTTPStatus.OK + assert response.json == invalid_argument_expected_payload( + wrong_call.message + ) + + +def routes(): + yield '/tiles' + yield '/tiles/tile' + yield '/tiles/tile-data' + + +@fixture(scope='module', params=routes(), ids=lambda route: f'POST {route}') +def route(request): + return request.param + + +def test_dashboard_call_success(route, client, valid_jwt): + response = client.post(route, headers=get_headers(valid_jwt())) + assert response.status_code == HTTPStatus.OK diff --git a/code/tests/unit/api/test_enrich.py b/code/tests/unit/api/test_enrich.py new file mode 100644 index 0000000..9f59eec --- /dev/null +++ b/code/tests/unit/api/test_enrich.py @@ -0,0 +1,54 @@ +from pytest import fixture +from http import HTTPStatus +from .utils import get_headers +from unittest.mock import patch +from ..conftest import mock_api_response +from ..payloads_for_tests import EXPECTED_RESPONSE_OF_JWKS_ENDPOINT + + +def routes(): + yield '/deliberate/observables' + 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 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': 'cisco.com'}] + + +@patch('requests.get') +def test_enrich_call_success(mock_request, + route, client, valid_jwt, valid_json): + mock_request.return_value = \ + mock_api_response(payload=EXPECTED_RESPONSE_OF_JWKS_ENDPOINT) + response = client.post(route, headers=get_headers(valid_jwt()), + json=valid_json) + assert response.status_code == HTTPStatus.OK diff --git a/code/tests/unit/api/test_health.py b/code/tests/unit/api/test_health.py new file mode 100644 index 0000000..d4af751 --- /dev/null +++ b/code/tests/unit/api/test_health.py @@ -0,0 +1,19 @@ +from http import HTTPStatus + +from pytest import fixture + +from .utils import get_headers + + +def routes(): + yield '/health' + + +@fixture(scope='module', params=routes(), ids=lambda route: f'POST {route}') +def route(request): + return request.param + + +def test_health_call_success(route, client, valid_jwt): + response = client.post(route, headers=get_headers(valid_jwt())) + assert response.status_code == HTTPStatus.OK diff --git a/code/tests/unit/api/test_respond.py b/code/tests/unit/api/test_respond.py new file mode 100644 index 0000000..c2fad5e --- /dev/null +++ b/code/tests/unit/api/test_respond.py @@ -0,0 +1,106 @@ +from http import HTTPStatus +from unittest.mock import patch + +from pytest import fixture + +from .utils import get_headers +from ..conftest import mock_api_response +from ..payloads_for_tests import EXPECTED_RESPONSE_OF_JWKS_ENDPOINT + + +def routes(): + yield '/respond/observables' + yield '/respond/trigger' + + +@fixture(scope='module', params=routes(), ids=lambda route: f'POST {route}') +def route(request): + return request.param + + +@fixture() +def invalid_json_value(): + return [{'type': 'ip', 'value': ''}] + + +@fixture() +def invalid_json_action_id(): + return {'action_id': 'some_action_id', + 'observable_type': 'domain', + 'observable_value': 'cisco.com'} + + +@fixture() +def invalid_json_observable_value(): + return {'action-id': 'some_action_id', + 'observable_type': 'ip', + 'observable_value': ''} + + +@patch('requests.get') +def test_respond_call_with_valid_jwt_but_invalid_json_value( + mock_request, client, valid_jwt, invalid_json_value, + invalid_json_expected_payload, route='/respond/observables' +): + 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.']}}" + ) + + +@patch('requests.get') +def test_respond_call_with_valid_jwt_but_invalid_json_action_id( + mock_request, client, valid_jwt, invalid_json_action_id, + invalid_json_expected_payload, route='/respond/trigger' +): + 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_action_id) + assert response.status_code == HTTPStatus.OK + assert response.json == invalid_json_expected_payload( + "{'action-id': ['Missing data for required field.']}" + ) + + +@patch('requests.get') +def test_respond_call_with_valid_jwt_but_invalid_json_observable_value( + mock_request, client, valid_jwt, invalid_json_observable_value, + invalid_json_expected_payload, route='/respond/trigger' +): + 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_observable_value) + assert response.status_code == HTTPStatus.OK + assert response.json == invalid_json_expected_payload( + "{'observable_value': ['Field may not be blank.']}" + ) + + +@fixture(scope='module') +def valid_json(route): + if route.endswith('/observables'): + return [{'type': 'domain', 'value': 'cisco.com'}] + + if route.endswith('/trigger'): + return {'action-id': 'valid-action-id', + 'observable_type': 'domain', + 'observable_value': 'cisco.com'} + + +@patch('requests.get') +def test_respond_call_success(mock_request, + route, client, valid_jwt, valid_json): + mock_request.return_value = \ + mock_api_response(payload=EXPECTED_RESPONSE_OF_JWKS_ENDPOINT) + response = client.post(route, headers=get_headers(valid_jwt()), + json=valid_json) + assert response.status_code == HTTPStatus.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..694070c --- /dev/null +++ b/code/tests/unit/api/test_watchdog.py @@ -0,0 +1,20 @@ +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 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..3bcda7a --- /dev/null +++ b/code/tests/unit/conftest.py @@ -0,0 +1,71 @@ +import jwt + +from app import app +from pytest import fixture +from http import HTTPStatus +from unittest.mock import MagicMock +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( + key='some_key', + jwks_host='visibility.amp.cisco.com', + aud='http://localhost', + kid='02B1174234C29F8EFB69911438F597FF3FFEE6B7', + wrong_structure=False + ): + payload = { + 'key': key, + 'jwks_host': jwks_host, + 'aud': aud, + } + + if wrong_structure: + payload.pop('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 diff --git a/code/tests/unit/payloads_for_tests.py b/code/tests/unit/payloads_for_tests.py new file mode 100644 index 0000000..caa92b6 --- /dev/null +++ b/code/tests/unit/payloads_for_tests.py @@ -0,0 +1,103 @@ +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-----""" diff --git a/code/tests/unit/test_app.py b/code/tests/unit/test_app.py new file mode 100644 index 0000000..77cc7b8 --- /dev/null +++ b/code/tests/unit/test_app.py @@ -0,0 +1,38 @@ +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', '/deliberate/observables', HTTPStatus.METHOD_NOT_ALLOWED) + yield Call('GET', '/observe/observables', HTTPStatus.METHOD_NOT_ALLOWED) + yield Call('GET', '/refer/observables', HTTPStatus.METHOD_NOT_ALLOWED) + yield Call('GET', '/respond/observables', HTTPStatus.METHOD_NOT_ALLOWED) + yield Call('GET', '/respond/trigger', HTTPStatus.METHOD_NOT_ALLOWED) + yield Call('GET', '/tiles', HTTPStatus.METHOD_NOT_ALLOWED) + yield Call('GET', '/tiles/tile', HTTPStatus.METHOD_NOT_ALLOWED) + yield Call('GET', '/tiles/tile-data', 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..1fc29ed --- /dev/null +++ b/module_type.json.sample @@ -0,0 +1,18 @@ +{ + "title": "Module title", + "default_name": "Module default name", + "short_description": "Module short description", + "description": "Module description", + "tips": "Module tips", + "external_references": [], + "configuration_spec": [], + "capabilities": [], + "properties": { + "url": "https://ciscohosted.url", + "supported-apis": [], + "auth-type": "configuration-token", + "configuration-token-alg": "RS256", + "custom_jwks_host": "Custom jwks host" + }, + "logo": "logo link" +} diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100644 index 0000000..5d7524c --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env sh +set -e + +# Grab the repository +if [ -n "$GITREPO" ]; then + echo "rm -rf /app && git clone $GITREPO /app" + rm -rf /app && git clone $GITREPO /app +fi + + +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 From bf5478fbe5269d901f5fcd7c6599d7fd5d8100d4 Mon Sep 17 00:00:00 2001 From: Taras Maliuchenko Date: Fri, 12 Mar 2021 17:29:44 +0200 Subject: [PATCH 02/33] Delete zappa dependency --- code/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/code/requirements.txt b/code/requirements.txt index ff4cdc3..b64fe5a 100644 --- a/code/requirements.txt +++ b/code/requirements.txt @@ -1,7 +1,6 @@ cryptography==3.3.2 Flask==1.1.2 marshmallow==3.8.0 -zappa==0.52.0 requests==2.24.0 pyjwt[crypto]==2.0.0 flake8==3.8.3 From 5843b569c0a4912dd1324b1e2522a243b22a5fc0 Mon Sep 17 00:00:00 2001 From: Taras Maliuchenko Date: Fri, 12 Mar 2021 18:01:41 +0200 Subject: [PATCH 03/33] Minor changes in README file --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 80e963a..6cb6967 100644 --- a/README.md +++ b/README.md @@ -83,4 +83,3 @@ you may consider the following sections as some placeholders. ### Supported Types of Observables `N/A` ->>>>>>> CCTRI-2118 From 80fd2ec177a474782bbec48fb59fc2c10093097e Mon Sep 17 00:00:00 2001 From: Taras Maliuchenko Date: Tue, 16 Mar 2021 09:05:48 +0200 Subject: [PATCH 04/33] Replace tr-05-module-name by tr-05-docker-relay --- .travis.yml | 6 +++--- README.md | 6 +++--- build.sh | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index c106c1a..4ece34e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,8 @@ jobs: include: - stage: build&test script: - - docker build -t tr-05-module-name . - - docker run -d -p 9090:9090 --name tr-05-module-name tr-05-module-name - - while true; do if docker logs tr-05-module-name | grep "entered RUNNING state"; then + - docker build -t tr-05-docker-relay . + - docker run -d -p 9090:9090 --name tr-05-docker-relay tr-05-docker-relay + - while true; do if docker logs tr-05-docker-relay | grep "entered RUNNING state"; then break; else sleep 1; fi done - curl -X POST -sSLi http://localhost:9090 | grep '200 OK' diff --git a/README.md b/README.md index 6cb6967..ffc6636 100644 --- a/README.md +++ b/README.md @@ -48,19 +48,19 @@ 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-module-name . +docker build -t tr-05-docker-relay . ``` 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-module-name tr-05-module-name +docker run -dp 9090:9090 --name tr-05-docker-relay tr-05-docker-relay ``` 3. Watch the container logs to ensure it starts correctly. ``` -docker logs tr-05-module-name +docker logs tr-05-docker-relay ``` 4. Once the container has started correctly, open your web browser to http://localhost:9090. You should see a response from the container. diff --git a/build.sh b/build.sh index 2c0c300..dafdc61 100644 --- a/build.sh +++ b/build.sh @@ -6,8 +6,8 @@ echo echo " Development Dockerfile build script." echo -module_name="Module name" -image_name="tr-05-module-name" +module_name="Docker relay" +image_name="tr-05-docker-relay" CONFIG_FILE=code/container_settings.json if [ -f $CONFIG_FILE ]; then From bd0253abb8b291a4ae0935fc05b5db55d67fa9c8 Mon Sep 17 00:00:00 2001 From: Taras Maliuchenko Date: Tue, 16 Mar 2021 09:08:18 +0200 Subject: [PATCH 05/33] Add dashboard api to authorization tests --- code/tests/unit/api/test_authorization.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/tests/unit/api/test_authorization.py b/code/tests/unit/api/test_authorization.py index afe7d74..b117b4f 100644 --- a/code/tests/unit/api/test_authorization.py +++ b/code/tests/unit/api/test_authorization.py @@ -25,6 +25,9 @@ def routes(): yield '/refer/observables' yield '/respond/observables' yield '/respond/trigger' + yield '/tiles' + yield '/tiles/tile' + yield '/tiles/tile-data' @fixture(scope='module', params=routes(), ids=lambda route: f'POST {route}') From d7f022ed9178cad71de79ebdd71c1b4f093cc1bb Mon Sep 17 00:00:00 2001 From: Taras Maliuchenko Date: Tue, 16 Mar 2021 10:27:11 +0200 Subject: [PATCH 06/33] Make module type be more informative --- module_type.json.sample | 78 +++++++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/module_type.json.sample b/module_type.json.sample index 1fc29ed..237b39c 100644 --- a/module_type.json.sample +++ b/module_type.json.sample @@ -1,18 +1,66 @@ { - "title": "Module title", - "default_name": "Module default name", - "short_description": "Module short description", - "description": "Module description", - "tips": "Module tips", - "external_references": [], - "configuration_spec": [], - "capabilities": [], - "properties": { - "url": "https://ciscohosted.url", - "supported-apis": [], - "auth-type": "configuration-token", - "configuration-token-alg": "RS256", - "custom_jwks_host": "Custom jwks host" + "title": "Docker Relay", + "default_name": "Docker Relay", + "short_description": "Generic Docker Relay module that can be used when developing new integrations", + "description": "Generic Docker Relay module description", + "tips": ""When configuring this integration, you must first gather some information from your third-party account if needed and add the Docker Relay Module\n\n1. Complete the **Add New Docker Relay Module** form:\n - **Module Name** - Leave the default name or enter a name that is meaningful to you.\n - **API KEY** - Enter third party api key\n9. Click **Save** to complete the Docker Relay module configuration.", + "external_references": [ + { + "label": "Docker Relay Template", + "link": "https://github.com/CiscoSecurity/tr-05-docker-relay" + } + ], + "configuration_spec": [ + { + "key": "custom_API_KEY", + "type": "api_key", + "label": "API KEY", + "tooltip": "The module API KEY", + "required": true + } + ], + "capabilities": [ + { + "id": "health", + "description": "Healthcheck" }, - "logo": "logo link" + { + "id": "deliberate", + "description": "Deliberation" + }, + { + "id": "observe", + "description": "Enrichments" + }, + { + "id": "refer", + "description": "Reference links" + }, + { + "id": "respond", + "description": "Response actions" + }, + { + "id": "tiles", + "description": "Dashboard tiles" + } + ], + "properties": { + "supported-apis": [ + "health", + "observe/observables", + "deliberate/observables", + "refer/observables", + "respond/observables", + "respond/trigger", + "tiles", + "tiles/tile", + "tiles/tile-data" + ], + "auth-type": "configuration-token", + "configuration-token-alg": "RS256", + "custom_jwks_host": "visibility.amp.cisco.com", + "url": "https://ciscohosted.url" + }, + "logo": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCI+Cgk8c3R5bGU+CgkJdHNwYW4geyB3aGl0ZS1zcGFjZTpwcmUgfQoJCS5zaHAwIHsgZmlsbDogIzk1YTVhNiB9IAoJCS5zaHAxIHsgZmlsbDogIzdmOGM4ZCB9IAoJPC9zdHlsZT4KCTxnIGlkPSJMYXllciI+CgkJPHBhdGggaWQ9IkxheWVyIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsYXNzPSJzaHAwIiBkPSJNMTcuNTkgMTEuNjdMMjYuNDMgMTQuNThMMjcuMDcgMTVDMjguNSAxNC4xNyAyOS45OCAxMy4zMyAzMS40OSAxMi41TDMxLjIyIDExLjY3TDMyLjk3IDIuNUw0MS4wNyAwLjQyTDQ3LjI0IDcuNUw0Ny40MiA4LjMzQzQ5LjE1IDcuOTIgNTAuODUgNy45MiA1Mi41OCA4LjMzTDUyLjc2IDcuNUw1OC45MyAwLjQyTDY3LjAzIDIuNUw2OC43OCAxMS42N0w2OC41MSAxMi41QzcwLjAyIDEzLjMzIDcxLjUgMTQuMTcgNzIuOTMgMTVMNzMuNTcgMTQuNThMODIuNDEgMTEuNjdMODguMyAxNy41TDg1LjM1IDI2LjI1TDg0LjcxIDI3LjA4Qzg1LjY1IDI4LjMzIDg2LjU0IDMwIDg3LjI5IDMxLjI1TDg4LjEyIDMxLjI1TDk3LjIzIDMyLjkyTDk5LjQ0IDQwLjgzTDkyLjM1IDQ3LjA4TDkxLjYyIDQ3LjA4QzkxLjczIDQ5LjE3IDkxLjczIDUwLjgzIDkxLjYyIDUyLjVMOTIuMzUgNTIuNUw5OS40NCA1OC43NUw5Ny4yMyA2Ny4wOEw4OC4xMiA2OC43NUw4Ny4yOSA2OC4zM0M4Ni41NCA3MCA4NS42NSA3MS4yNSA4NC43MSA3Mi45Mkw4NS4zNSA3My4zM0w4OC4zIDgyLjA4TDgyLjQxIDg4LjMzTDczLjU3IDg1TDcyLjkzIDg0LjU4QzcxLjUgODUuNDIgNzAuMDIgODYuMjUgNjguNTEgODcuMDhMNjguNzggODcuOTJMNjcuMDMgOTcuMDhMNTguOTMgOTkuMTdMNTIuNzYgOTIuMDhMNTIuNTggOTEuNjdMNDcuNDIgOTEuNjdMNDcuMjQgOTIuMDhMNDEuMDcgOTkuMTdMMzIuOTcgOTcuMDhMMzEuMjIgODcuOTJMMzEuNDkgODcuMDhDMjkuOTggODYuMjUgMjguNSA4NS40MiAyNy4wNyA4NC41OEwyNi40MyA4NUwxNy41OSA4OC4zM0wxMS43IDgyLjA4TDE0LjY0IDczLjMzTDE1LjI5IDcyLjkyQzE0LjM0IDcxLjI1IDEzLjQ2IDcwIDEyLjcxIDY4LjMzTDExLjg4IDY4Ljc1TDIuNzcgNjcuMDhMMC41NiA1OC43NUw3LjY1IDUyLjVMOC4zOCA1Mi41QzguMjggNTAuODMgOC4yOCA0OS4xNyA4LjM4IDQ3LjA4TDcuNjUgNDcuMDhMMC41NiA0MC44M0wyLjc3IDMyLjkyTDExLjg4IDMxLjI1TDEyLjcxIDMxLjI1QzEzLjQ2IDMwIDE0LjM0IDI4LjMzIDE1LjI5IDI3LjA4TDE0LjY0IDI2LjI1TDExLjcgMTcuNUwxNy41OSAxMS42N0wxNy41OSAxMS42N1pNMjkuMzggMjkuMTdDMTcuOTkgNDAuNDIgMTcuOTkgNTkuMTcgMjkuMzggNzAuNDJDNDAuNzcgODEuNjcgNTkuMjMgODEuNjcgNzAuNjMgNzAuNDJDODIuMDEgNTkuMTcgODIuMDEgNDAuNDIgNzAuNjMgMjkuMTdDNTkuMjMgMTcuOTIgNDAuNzcgMTcuOTIgMjkuMzggMjkuMTdaIiAvPgoJCTxwYXRoIGlkPSJMYXllciIgZmlsbC1ydWxlPSJldmVub2RkIiBjbGFzcz0ic2hwMSIgZD0iTTI2LjQzIDI2LjI1QzM5LjQ1IDEzLjMzIDYwLjU1IDEzLjMzIDczLjU3IDI2LjI1Qzg2LjU5IDM5LjE3IDg2LjU5IDYwLjQyIDczLjU3IDczLjMzQzYwLjU1IDg2LjI1IDM5LjQ1IDg2LjI1IDI2LjQzIDczLjMzQzEzLjQxIDYwLjQyIDEzLjQxIDM5LjE3IDI2LjQzIDI2LjI1TDI2LjQzIDI2LjI1Wk00MS42MiAzNS40MkM0NS4zMSAzMy4zMyA0OS41MiAzMi41IDUzLjU5IDMzLjc1TDU5LjM5IDIyLjA4QzUwLjQ3IDE5LjE3IDQwLjQzIDIwLjgzIDMyLjYgMjYuNjdMNDEuNjIgMzUuNDJaTTI2LjcxIDMyLjVDMjAuODUgNDAuNDIgMTkuMzUgNTAuNDIgMjIuMzggNTkuMTdMMzMuNyA1My4zM0MzMi44IDQ5LjE3IDMzLjU1IDQ1IDM1LjczIDQxLjY3TDI2LjcxIDMyLjVaTTQ0LjExIDQzLjc1QzQwLjg1IDQ3LjA4IDQwLjg1IDUyLjUgNDQuMTEgNTUuODNDNDcuMzYgNTkuMTcgNTIuNjQgNTkuMTcgNTUuODkgNTUuODNDNTkuMTUgNTIuNSA1OS4xNSA0Ny4wOCA1NS44OSA0My43NUM1Mi42NCA0MC44MyA0Ny4zNiA0MC44MyA0NC4xMSA0My43NVpNNjYuNzYgMjYuMjVMNjAuOTYgMzcuNUM2MS4yMiAzNy41IDYxLjUzIDM3LjkyIDYxLjc4IDM3LjkyQzY0LjY2IDQwLjgzIDY2LjIzIDQ0LjU4IDY2LjU3IDQ4LjMzTDc5LjEgNTAuNDJDNzkuMTkgNDIuNSA3Ni40MSAzNSA3MC42MyAyOS4xN0M2OS40MiAyNy45MiA2OC4xIDI3LjA4IDY2Ljc2IDI2LjI1Wk0yOS4zOCA3MC40MkMzNS4xNiA3Ni4yNSA0Mi43OCA3OS4xNyA1MC4zNyA3OC43NUw0OC41MiA2Ni4yNUM0NC43NyA2Ni4yNSA0MS4wOSA2NC41OCAzOC4yMSA2MS42N0MzNy45NiA2MS4yNSAzNy44IDYxLjI1IDM3LjU3IDYwLjgzTDI2LjI1IDY2LjY3QzI3LjE5IDY3LjkyIDI4LjE4IDY5LjE3IDI5LjM4IDcwLjQyWk02MS43OCA2MS42N0M2MC4yNiA2My4zMyA1OC40OSA2NC4xNyA1Ni42MyA2NUw1OC41NiA3Ny45MkM2Mi45NyA3Ni4yNSA2Ny4xNCA3My43NSA3MC42MyA3MC40MkM3NC4xMSA2Ny4wOCA3Ni41NSA2Mi45MiA3Ny45IDU4LjMzTDY1LjI4IDU2LjY3QzY0LjQ4IDU4LjMzIDYzLjMxIDYwIDYxLjc4IDYxLjY3WiIgLz4KCTwvZz4KPC9zdmc+" } From 21a46a8533bd489e13f646b821cd6647fb9ad11d Mon Sep 17 00:00:00 2001 From: Taras Maliuchenko Date: Tue, 16 Mar 2021 10:50:00 +0200 Subject: [PATCH 07/33] Minor change in module type --- module_type.json.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module_type.json.sample b/module_type.json.sample index 237b39c..2e6ea76 100644 --- a/module_type.json.sample +++ b/module_type.json.sample @@ -3,7 +3,7 @@ "default_name": "Docker Relay", "short_description": "Generic Docker Relay module that can be used when developing new integrations", "description": "Generic Docker Relay module description", - "tips": ""When configuring this integration, you must first gather some information from your third-party account if needed and add the Docker Relay Module\n\n1. Complete the **Add New Docker Relay Module** form:\n - **Module Name** - Leave the default name or enter a name that is meaningful to you.\n - **API KEY** - Enter third party api key\n9. Click **Save** to complete the Docker Relay module configuration.", + "tips": When configuring this integration, you must first gather some information from your third-party account if needed and add the Docker Relay Module\n\n1. Complete the **Add New Docker Relay Module** form:\n - **Module Name** - Leave the default name or enter a name that is meaningful to you.\n - **API KEY** - Enter third party api key\n9. Click **Save** to complete the Docker Relay module configuration.", "external_references": [ { "label": "Docker Relay Template", From d6025dc4c5b1a59de8558e9c5bb90b6fce705bf8 Mon Sep 17 00:00:00 2001 From: Taras Maliuchenko Date: Wed, 24 Mar 2021 11:42:03 +0200 Subject: [PATCH 08/33] Add KeyError handling in get_public_key function --- code/api/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/code/api/utils.py b/code/api/utils.py index 17aed1c..7693761 100644 --- a/code/api/utils.py +++ b/code/api/utils.py @@ -35,6 +35,7 @@ def get_public_key(jwks_host, token): expected_errors = { ConnectionError: WRONG_JWKS_HOST, InvalidURL: WRONG_JWKS_HOST, + KeyError: WRONG_JWKS_HOST } try: response = requests.get(f"https://{jwks_host}/.well-known/jwks") From f2afd14c866752361470ecf8ffe2f17fe9b3b12d Mon Sep 17 00:00:00 2001 From: Lena Shynkarenko Date: Fri, 14 May 2021 14:45:54 +0300 Subject: [PATCH 09/33] [CCTRI-2184] Add 404 error handling --- code/api/utils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/code/api/utils.py b/code/api/utils.py index 7693761..dfa0b9f 100644 --- a/code/api/utils.py +++ b/code/api/utils.py @@ -1,11 +1,11 @@ -import jwt import json +from json.decoder import JSONDecodeError import requests - +import jwt from flask import request, jsonify from requests.exceptions import ConnectionError, InvalidURL -from api.errors import AuthorizationError, InvalidArgumentError from jwt import InvalidSignatureError, DecodeError, InvalidAudienceError +from api.errors import AuthorizationError, InvalidArgumentError NO_AUTH_HEADER = 'Authorization header is missing' @@ -35,7 +35,8 @@ def get_public_key(jwks_host, token): expected_errors = { ConnectionError: WRONG_JWKS_HOST, InvalidURL: WRONG_JWKS_HOST, - KeyError: WRONG_JWKS_HOST + KeyError: WRONG_JWKS_HOST, + JSONDecodeError: WRONG_JWKS_HOST } try: response = requests.get(f"https://{jwks_host}/.well-known/jwks") From a7c96525e1d7941817065c4111ab73275b77d8a0 Mon Sep 17 00:00:00 2001 From: YevhenLysen Date: Tue, 29 Jun 2021 13:46:40 +0300 Subject: [PATCH 10/33] Added Jenkinsfile --- Jenkinsfile | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Jenkinsfile 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() From 3db6a8607c60904b80e0d9c19d9b75918c0aad4b Mon Sep 17 00:00:00 2001 From: YuriiTsekhovyi Date: Thu, 24 Jun 2021 11:32:12 +0300 Subject: [PATCH 11/33] Add warning and 403 error handling --- code/api/utils.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/code/api/utils.py b/code/api/utils.py index dfa0b9f..c210412 100644 --- a/code/api/utils.py +++ b/code/api/utils.py @@ -1,12 +1,13 @@ import json from json.decoder import JSONDecodeError -import requests + import jwt +import requests from flask import request, jsonify -from requests.exceptions import ConnectionError, InvalidURL from jwt import InvalidSignatureError, DecodeError, InvalidAudienceError -from api.errors import AuthorizationError, InvalidArgumentError +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' @@ -32,14 +33,16 @@ def get_public_key(jwks_host, token): any way, replaced by another function, or even removed from the module. """ - expected_errors = { - ConnectionError: WRONG_JWKS_HOST, - InvalidURL: WRONG_JWKS_HOST, - KeyError: WRONG_JWKS_HOST, - JSONDecodeError: WRONG_JWKS_HOST - } + 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 = {} @@ -50,9 +53,9 @@ def get_public_key(jwks_host, token): ) kid = jwt.get_unverified_header(token)['kid'] return public_keys.get(kid) - except tuple(expected_errors) as error: - message = expected_errors[error.__class__] - raise AuthorizationError(message) + + except expected_errors: + raise AuthorizationError(WRONG_JWKS_HOST) def get_auth_token(): From a255fb202fa7ff8c2e9d8c88dd92c1799f01bd52 Mon Sep 17 00:00:00 2001 From: YuriiTsekhovyi Date: Thu, 24 Jun 2021 12:04:30 +0300 Subject: [PATCH 12/33] flask update --- code/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/requirements.txt b/code/requirements.txt index b64fe5a..60a5459 100644 --- a/code/requirements.txt +++ b/code/requirements.txt @@ -1,5 +1,5 @@ cryptography==3.3.2 -Flask==1.1.2 +Flask==2.0.1 marshmallow==3.8.0 requests==2.24.0 pyjwt[crypto]==2.0.0 From af5a570b60254bd248ab4b05dd8a409ec45cd0a1 Mon Sep 17 00:00:00 2001 From: YuriiTsekhovyi Date: Thu, 1 Jul 2021 15:34:14 +0300 Subject: [PATCH 13/33] jwks_host message fix --- code/api/utils.py | 7 +++---- code/tests/unit/api/test_authorization.py | 2 +- code/tests/unit/conftest.py | 6 +++++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/code/api/utils.py b/code/api/utils.py index c210412..9aadd5d 100644 --- a/code/api/utils.py +++ b/code/api/utils.py @@ -98,10 +98,9 @@ def get_jwt(): } token = get_auth_token() try: - jwks_host = jwt.decode( - token, options={'verify_signature': False} - ).get('jwks_host') - assert jwks_host + jwks_payload = jwt.decode(token, options={'verify_signature': False}) + assert 'jwks_host' in jwks_payload + jwks_host = jwks_payload.get('jwks_host') key = get_public_key(jwks_host, token) aud = request.url_root payload = jwt.decode( diff --git a/code/tests/unit/api/test_authorization.py b/code/tests/unit/api/test_authorization.py index b117b4f..5a11a60 100644 --- a/code/tests/unit/api/test_authorization.py +++ b/code/tests/unit/api/test_authorization.py @@ -171,7 +171,7 @@ def test_call_with_missing_jwks_host( response = client.post( route, - headers=get_headers(valid_jwt(jwks_host='')) + headers=get_headers(valid_jwt(wrong_jwks_host=True)) ) assert response.status_code == HTTPStatus.OK assert response.json == authorization_errors_expected_payload( diff --git a/code/tests/unit/conftest.py b/code/tests/unit/conftest.py index 3bcda7a..57dc857 100644 --- a/code/tests/unit/conftest.py +++ b/code/tests/unit/conftest.py @@ -25,7 +25,8 @@ def _make_jwt( jwks_host='visibility.amp.cisco.com', aud='http://localhost', kid='02B1174234C29F8EFB69911438F597FF3FFEE6B7', - wrong_structure=False + wrong_structure=False, + wrong_jwks_host=False ): payload = { 'key': key, @@ -33,6 +34,9 @@ def _make_jwt( 'aud': aud, } + if wrong_jwks_host: + payload.pop('jwks_host') + if wrong_structure: payload.pop('key') From 40a74f294a826438bbcbc9f8456e06b4cc870c09 Mon Sep 17 00:00:00 2001 From: YuriiTsekhovyi Date: Tue, 6 Jul 2021 11:02:44 +0300 Subject: [PATCH 14/33] [CCTRI-2667] - alpine&python version update --- Dockerfile | 6 ++++-- README.md | 2 ++ code/requirements.txt | 14 +++++++------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index e17c40e..ee61c0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,10 @@ -FROM alpine:3.13 +FROM alpine:3.14 LABEL maintainer="Ian Redden " # install packages we need -RUN apk update && apk add --no-cache musl-dev openssl-dev gcc python3 py3-configobj python3-dev supervisor git libffi-dev uwsgi-python3 uwsgi-http jq nano syslog-ng uwsgi-syslog py3-pip +RUN apk update && apk add --no-cache musl-dev openssl-dev gcc py3-configobj \ +supervisor git libffi-dev uwsgi-python3 uwsgi-http jq syslog-ng uwsgi-syslog \ +py3-pip python3-dev # do the Python dependencies ADD code /app diff --git a/README.md b/README.md index ffc6636..f6225a4 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,8 @@ curl http://localhost:9090 ## Implementation Details +This application was developed and tested under Python version 3.9. + **NOTE.** Remember that this application is just a template so here `N/A` means that it has no implemented Relay endpoints and supported types of observables. That will not be the case for real integrations with third-party services so diff --git a/code/requirements.txt b/code/requirements.txt index 60a5459..b90da97 100644 --- a/code/requirements.txt +++ b/code/requirements.txt @@ -1,8 +1,8 @@ -cryptography==3.3.2 Flask==2.0.1 -marshmallow==3.8.0 -requests==2.24.0 -pyjwt[crypto]==2.0.0 -flake8==3.8.3 -coverage==5.2.1 -pytest==6.0.1 +marshmallow==3.12.1 +requests==2.25.1 +cryptography==3.3.2 +pyjwt[crypto]==2.1.0 +flake8==3.9.2 +coverage==5.5 +pytest==6.2.4 From 00700c2b3ecd777e0dde33f6a48648c1df99d12d Mon Sep 17 00:00:00 2001 From: Taras Maliuchenko Date: Thu, 15 Jul 2021 14:13:56 +0300 Subject: [PATCH 15/33] Test jenkins notifications --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index e8ac0fd..f78cbe9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,3 +1,3 @@ -@Library('softserve-jenkins-library@main') _ +@Library('softserve-jenkins-library@CCTRI-2795') _ startPipeline() From d0385b33aef06f09587f56af2fd109788c184aa5 Mon Sep 17 00:00:00 2001 From: Taras Maliuchenko Date: Thu, 15 Jul 2021 14:45:08 +0300 Subject: [PATCH 16/33] Test jenkins notifications --- Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile b/Jenkinsfile index f78cbe9..add3aeb 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,3 +1,4 @@ @Library('softserve-jenkins-library@CCTRI-2795') _ + startPipeline() From 98c26ecf5e57def643270bff70ee971bdc887e40 Mon Sep 17 00:00:00 2001 From: Taras Maliuchenko Date: Thu, 15 Jul 2021 14:48:39 +0300 Subject: [PATCH 17/33] Test jenkins notifications --- Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile b/Jenkinsfile index add3aeb..5e8081f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,4 +1,5 @@ @Library('softserve-jenkins-library@CCTRI-2795') _ + startPipeline() From f76ba50ab5f5f4b4b040069044dbe0530f5a201c Mon Sep 17 00:00:00 2001 From: Taras Maliuchenko Date: Thu, 15 Jul 2021 14:52:59 +0300 Subject: [PATCH 18/33] Get back old logic --- Jenkinsfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 5e8081f..e8ac0fd 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,5 +1,3 @@ -@Library('softserve-jenkins-library@CCTRI-2795') _ - - +@Library('softserve-jenkins-library@main') _ startPipeline() From c12973a8ed40de5e28770241b4f643346ff59170 Mon Sep 17 00:00:00 2001 From: Maksym Storozhenko Date: Tue, 10 Aug 2021 11:08:03 +0300 Subject: [PATCH 19/33] [CCTRI-2962] move from pip to pipenv --- .travis.yml | 11 -- Dockerfile | 8 +- README.md | 4 +- code/Pipfile | 19 ++ code/Pipfile.lock | 395 ++++++++++++++++++++++++++++++++++++++++++ code/requirements.txt | 8 - scripts/entrypoint.sh | 6 - 7 files changed, 422 insertions(+), 29 deletions(-) delete mode 100644 .travis.yml create mode 100644 code/Pipfile create mode 100644 code/Pipfile.lock delete mode 100644 code/requirements.txt diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4ece34e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -services: -- docker -jobs: - include: - - stage: build&test - script: - - docker build -t tr-05-docker-relay . - - docker run -d -p 9090:9090 --name tr-05-docker-relay tr-05-docker-relay - - while true; do if docker logs tr-05-docker-relay | grep "entered RUNNING state"; then - break; else sleep 1; fi done - - curl -X POST -sSLi http://localhost:9090 | grep '200 OK' diff --git a/Dockerfile b/Dockerfile index ee61c0a..c946e9c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,18 @@ 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 git libffi-dev uwsgi-python3 uwsgi-http jq syslog-ng uwsgi-syslog \ +supervisor libffi-dev uwsgi-python3 uwsgi-http jq syslog-ng uwsgi-syslog \ py3-pip python3-dev # do the Python dependencies ADD code /app -RUN pip3 install -r /app/requirements.txt +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 diff --git a/README.md b/README.md index f6225a4..8503c5e 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,9 @@ Open the code folder in your terminal. cd code ``` -If you want to test the application you have to install dependencies from the [requirements.txt](requirements.txt) file: +If you want to test the application you have to install dependencies from the [Pipfile](code/Pipfile) file: ``` -pip install --upgrade --requirement requirements.txt +pip install --no-cache-dir --upgrade pipenv && pipenv install --dev ``` You can perform two kinds of testing: 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/requirements.txt b/code/requirements.txt deleted file mode 100644 index b90da97..0000000 --- a/code/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -Flask==2.0.1 -marshmallow==3.12.1 -requests==2.25.1 -cryptography==3.3.2 -pyjwt[crypto]==2.1.0 -flake8==3.9.2 -coverage==5.5 -pytest==6.2.4 diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index 5d7524c..bccbded 100644 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -1,12 +1,6 @@ #!/usr/bin/env sh set -e -# Grab the repository -if [ -n "$GITREPO" ]; then - echo "rm -rf /app && git clone $GITREPO /app" - rm -rf /app && git clone $GITREPO /app -fi - if [ -n "$ALPINEPYTHON" ] ; then export PYTHONPATH=$PYTHONPATH:/usr/local/lib/$ALPINEPYTHON/site-packages:/usr/lib/$ALPINEPYTHON/site-packages From 8cf9cced02eb338d06d80419b35e563156ac6c9f Mon Sep 17 00:00:00 2001 From: mstoro <78480384+mstoro@users.noreply.github.com> Date: Fri, 10 Dec 2021 11:15:43 +0200 Subject: [PATCH 20/33] [CCTRI-3443] add traceback to log file (#12) --- code/app.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/code/app.py b/code/app.py index 0b1e43e..6ac6cf6 100644 --- a/code/app.py +++ b/code/app.py @@ -1,3 +1,5 @@ +import traceback + from flask import Flask, jsonify from api.dashboard import dashboard_api @@ -24,7 +26,7 @@ @app.errorhandler(Exception) def handle_error(exception): - app.logger.error(exception) + app.logger.error(traceback.format_exc()) code = getattr(exception, 'code', 500) message = getattr(exception, 'description', 'Something went wrong.') reason = '.'.join([ @@ -38,7 +40,7 @@ def handle_error(exception): @app.errorhandler(TRFormattedError) def handle_tr_formatted_error(exception): - app.logger.error(exception) + app.logger.error(traceback.format_exc()) return jsonify_errors(exception.json) From ab9060fede96ac27411d2adb27216fb8abb477ee Mon Sep 17 00:00:00 2001 From: mstoro <78480384+mstoro@users.noreply.github.com> Date: Mon, 4 Apr 2022 18:24:09 +0300 Subject: [PATCH 21/33] [CCTRI-3505] remove traceback from logs in case of 404 error (#13) --- code/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/code/app.py b/code/app.py index 6ac6cf6..8102eca 100644 --- a/code/app.py +++ b/code/app.py @@ -26,7 +26,6 @@ @app.errorhandler(Exception) def handle_error(exception): - app.logger.error(traceback.format_exc()) code = getattr(exception, 'code', 500) message = getattr(exception, 'description', 'Something went wrong.') reason = '.'.join([ @@ -34,6 +33,9 @@ def handle_error(exception): exception.__class__.__name__, ]) + if code != 404: + app.logger.error(traceback.format_exc()) + response = jsonify(code=code, message=message, reason=reason) return response, code From 0243b8da882407ceaa7b30cd12a724f3b809c15f Mon Sep 17 00:00:00 2001 From: mstoro Date: Tue, 5 Jul 2022 11:27:03 +0300 Subject: [PATCH 22/33] [CCTRI-3540] update app skeleton --- README.md | 52 +++++++---- code/api/dashboard.py | 25 ----- code/api/enrich.py | 11 +-- code/api/health.py | 1 + code/api/respond.py | 24 ----- code/api/watchdog.py | 3 +- code/app.py | 4 - code/container_settings.json | 5 +- code/tests/unit/api/test_authorization.py | 17 ++-- code/tests/unit/api/test_dashboard.py | 107 ---------------------- code/tests/unit/api/test_enrich.py | 11 ++- code/tests/unit/api/test_health.py | 2 +- code/tests/unit/api/test_respond.py | 106 --------------------- code/tests/unit/api/test_watchdog.py | 1 + code/tests/unit/conftest.py | 7 +- code/tests/unit/test_app.py | 6 -- 16 files changed, 61 insertions(+), 321 deletions(-) delete mode 100644 code/api/dashboard.py delete mode 100644 code/api/respond.py delete mode 100644 code/tests/unit/api/test_dashboard.py delete mode 100644 code/tests/unit/api/test_respond.py diff --git a/README.md b/README.md index 8503c5e..9c69655 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,8 @@ # Docker Relay Template (Cisco Hosted) -Generic Docker Relay template not bound to any real third-party Cyber Threat -Intelligence service provider. - -**NOTE.** The template aims to show the general structure for future -implementations. It also provides a couple of utility functions that might be -handy. Keep in mind that the main idea here is to just give you a hint of a -possible approach rather than enforcing you to do everything exactly the same -way. +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. @@ -17,7 +11,7 @@ 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. +- We need an application that can be completely self-contained within a virtualized container using Docker. ## Testing (Optional) @@ -48,19 +42,19 @@ 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-docker-relay . +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-docker-relay tr-05-docker-relay +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-docker-relay +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. @@ -73,15 +67,35 @@ curl http://localhost:9090 This application was developed and tested under Python version 3.9. -**NOTE.** Remember that this application is just a template so here `N/A` means -that it has no implemented Relay endpoints and supported types of observables. -That will not be the case for real integrations with third-party services so -you may consider the following sections as some placeholders. - ### Implemented Relay Endpoints -`N/A` +- `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 -`N/A` +- `ip` +- `domain` diff --git a/code/api/dashboard.py b/code/api/dashboard.py deleted file mode 100644 index 4f93871..0000000 --- a/code/api/dashboard.py +++ /dev/null @@ -1,25 +0,0 @@ -from flask import Blueprint -from api.utils import jsonify_data, get_jwt, get_json -from api.schemas import DashboardTileSchema, DashboardTileDataSchema - -dashboard_api = Blueprint('dashboard', __name__) - - -@dashboard_api.route('/tiles', methods=['POST']) -def tiles(): - _ = get_jwt() - return jsonify_data([]) - - -@dashboard_api.route('/tiles/tile', methods=['POST']) -def tile(): - _ = get_jwt() - _ = get_json(DashboardTileSchema()) - return jsonify_data({}) - - -@dashboard_api.route('/tiles/tile-data', methods=['POST']) -def tile_data(): - _ = get_jwt() - _ = get_json(DashboardTileDataSchema()) - return jsonify_data({}) diff --git a/code/api/enrich.py b/code/api/enrich.py index a3d333b..ebe07f5 100644 --- a/code/api/enrich.py +++ b/code/api/enrich.py @@ -1,5 +1,7 @@ -from flask import Blueprint from functools import partial + +from flask import Blueprint + from api.schemas import ObservableSchema from api.utils import get_json, get_jwt, jsonify_data @@ -8,13 +10,6 @@ get_observables = partial(get_json, schema=ObservableSchema(many=True)) -@enrich_api.route('/deliberate/observables', methods=['POST']) -def deliberate_observables(): - _ = get_jwt() - _ = get_observables() - return jsonify_data({}) - - @enrich_api.route('/observe/observables', methods=['POST']) def observe_observables(): _ = get_jwt() diff --git a/code/api/health.py b/code/api/health.py index 34427c1..db72e28 100644 --- a/code/api/health.py +++ b/code/api/health.py @@ -1,4 +1,5 @@ from flask import Blueprint + from api.utils import get_jwt, jsonify_data health_api = Blueprint('health', __name__) diff --git a/code/api/respond.py b/code/api/respond.py deleted file mode 100644 index ed4768c..0000000 --- a/code/api/respond.py +++ /dev/null @@ -1,24 +0,0 @@ - -from flask import Blueprint -from functools import partial -from api.utils import get_json, get_jwt, jsonify_data -from api.schemas import ObservableSchema, ActionFormParamsSchema - -respond_api = Blueprint('respond', __name__) - -get_observables = partial(get_json, schema=ObservableSchema(many=True)) -get_action_form_params = partial(get_json, schema=ActionFormParamsSchema()) - - -@respond_api.route('/respond/observables', methods=['POST']) -def respond_observables(): - _ = get_jwt() - _ = get_observables() - return jsonify_data([]) - - -@respond_api.route('/respond/trigger', methods=['POST']) -def respond_trigger(): - _ = get_jwt() - _ = get_action_form_params() - return jsonify_data({'status': 'success'}) diff --git a/code/api/watchdog.py b/code/api/watchdog.py index 1e517c8..eb482aa 100644 --- a/code/api/watchdog.py +++ b/code/api/watchdog.py @@ -1,5 +1,6 @@ -from api.utils import jsonify_data from flask import request, Blueprint + +from api.utils import jsonify_data from api.errors import WatchdogError watchdog_api = Blueprint('watchdog', __name__) diff --git a/code/app.py b/code/app.py index 8102eca..4472800 100644 --- a/code/app.py +++ b/code/app.py @@ -2,10 +2,8 @@ from flask import Flask, jsonify -from api.dashboard import dashboard_api from api.enrich import enrich_api from api.health import health_api -from api.respond import respond_api from api.version import version_api from api.watchdog import watchdog_api from api.errors import TRFormattedError @@ -16,10 +14,8 @@ app.url_map.strict_slashes = False app.config.from_object('config.Config') -app.register_blueprint(dashboard_api) app.register_blueprint(enrich_api) app.register_blueprint(health_api) -app.register_blueprint(respond_api) app.register_blueprint(version_api) app.register_blueprint(watchdog_api) diff --git a/code/container_settings.json b/code/container_settings.json index 2e874f8..d78f202 100644 --- a/code/container_settings.json +++ b/code/container_settings.json @@ -1 +1,4 @@ -{"VERSION": "1.0.0", "NAME": "Module name"} +{ + "VERSION": "1.0.0", + "NAME": "CyberScan" +} diff --git a/code/tests/unit/api/test_authorization.py b/code/tests/unit/api/test_authorization.py index 5a11a60..c692f36 100644 --- a/code/tests/unit/api/test_authorization.py +++ b/code/tests/unit/api/test_authorization.py @@ -1,10 +1,9 @@ -from pytest import fixture from http import HTTPStatus - -from .utils import get_headers from unittest.mock import patch + +from pytest import fixture + from api.errors import AUTH_ERROR -from ..conftest import mock_api_response from api.utils import ( WRONG_PAYLOAD_STRUCTURE, WRONG_KEY, @@ -12,7 +11,9 @@ KID_NOT_FOUND, JWKS_HOST_MISSING ) -from ..payloads_for_tests import ( +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 ) @@ -20,14 +21,8 @@ def routes(): yield '/health' - yield '/deliberate/observables' yield '/observe/observables' yield '/refer/observables' - yield '/respond/observables' - yield '/respond/trigger' - yield '/tiles' - yield '/tiles/tile' - yield '/tiles/tile-data' @fixture(scope='module', params=routes(), ids=lambda route: f'POST {route}') diff --git a/code/tests/unit/api/test_dashboard.py b/code/tests/unit/api/test_dashboard.py deleted file mode 100644 index 02f562a..0000000 --- a/code/tests/unit/api/test_dashboard.py +++ /dev/null @@ -1,107 +0,0 @@ - - -from pytest import fixture -from http import HTTPStatus -from .utils import get_headers -from unittest.mock import patch -from collections import namedtuple -from api.errors import INVALID_ARGUMENT -from ..conftest import mock_api_response -from ..payloads_for_tests import EXPECTED_RESPONSE_OF_JWKS_ENDPOINT - -WrongCall = namedtuple('WrongCall', ('endpoint', 'payload', 'message')) - - -def wrong_calls(): - yield WrongCall( - '/tiles/tile', - {'tile-id': 'some_value'}, - "{'tile_id': ['Missing data for required field.'], " - "'tile-id': ['Unknown field.']}" - ) - yield WrongCall( - '/tiles/tile', - {'tile_id': ''}, - "{'tile_id': ['Field may not be blank.']}" - ) - yield WrongCall( - '/tiles/tile-data', - {'tile-id': 'some_value', 'period': 'some_period'}, - "{'tile_id': ['Missing data for required field.'], " - "'tile-id': ['Unknown field.']}" - ) - yield WrongCall( - '/tiles/tile-data', - {'tile_id': '', 'period': 'some_period'}, - "{'tile_id': ['Field may not be blank.']}" - ) - yield WrongCall( - '/tiles/tile-data', - {'tile_id': 'some_value', 'not_period': 'some_period'}, - "{'period': ['Missing data for required field.'], " - "'not_period': ['Unknown field.']}" - ) - yield WrongCall( - '/tiles/tile-data', - {'tile_id': 'some_value', 'period': ''}, - "{'period': ['Field may not be blank.']}" - ) - - -@fixture( - scope='module', - params=wrong_calls(), - ids=lambda wrong_payload: f'{wrong_payload.endpoint}, ' - f'{wrong_payload.payload}' -) -def wrong_call(request): - return request.param - - -@fixture(scope='module') -def invalid_argument_expected_payload(): - def _make_message(message): - return { - 'errors': [{ - 'code': INVALID_ARGUMENT, - 'message': message, - 'type': 'fatal' - }] - } - - return _make_message - - -@patch('requests.get') -def test_dashboard_call_with_wrong_payload(mock_request, - wrong_call, client, valid_jwt, - invalid_argument_expected_payload): - - mock_request.return_value = \ - mock_api_response(payload=EXPECTED_RESPONSE_OF_JWKS_ENDPOINT) - - response = client.post( - path=wrong_call.endpoint, - headers=get_headers(valid_jwt()), - json=wrong_call.payload - ) - assert response.status_code == HTTPStatus.OK - assert response.json == invalid_argument_expected_payload( - wrong_call.message - ) - - -def routes(): - yield '/tiles' - yield '/tiles/tile' - yield '/tiles/tile-data' - - -@fixture(scope='module', params=routes(), ids=lambda route: f'POST {route}') -def route(request): - return request.param - - -def test_dashboard_call_success(route, client, valid_jwt): - response = client.post(route, headers=get_headers(valid_jwt())) - assert response.status_code == HTTPStatus.OK diff --git a/code/tests/unit/api/test_enrich.py b/code/tests/unit/api/test_enrich.py index 9f59eec..e940e36 100644 --- a/code/tests/unit/api/test_enrich.py +++ b/code/tests/unit/api/test_enrich.py @@ -1,13 +1,14 @@ -from pytest import fixture from http import HTTPStatus -from .utils import get_headers from unittest.mock import patch -from ..conftest import mock_api_response -from ..payloads_for_tests import EXPECTED_RESPONSE_OF_JWKS_ENDPOINT + +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 def routes(): - yield '/deliberate/observables' yield '/observe/observables' yield '/refer/observables' diff --git a/code/tests/unit/api/test_health.py b/code/tests/unit/api/test_health.py index d4af751..32714f5 100644 --- a/code/tests/unit/api/test_health.py +++ b/code/tests/unit/api/test_health.py @@ -2,7 +2,7 @@ from pytest import fixture -from .utils import get_headers +from tests.unit.api.utils import get_headers def routes(): diff --git a/code/tests/unit/api/test_respond.py b/code/tests/unit/api/test_respond.py deleted file mode 100644 index c2fad5e..0000000 --- a/code/tests/unit/api/test_respond.py +++ /dev/null @@ -1,106 +0,0 @@ -from http import HTTPStatus -from unittest.mock import patch - -from pytest import fixture - -from .utils import get_headers -from ..conftest import mock_api_response -from ..payloads_for_tests import EXPECTED_RESPONSE_OF_JWKS_ENDPOINT - - -def routes(): - yield '/respond/observables' - yield '/respond/trigger' - - -@fixture(scope='module', params=routes(), ids=lambda route: f'POST {route}') -def route(request): - return request.param - - -@fixture() -def invalid_json_value(): - return [{'type': 'ip', 'value': ''}] - - -@fixture() -def invalid_json_action_id(): - return {'action_id': 'some_action_id', - 'observable_type': 'domain', - 'observable_value': 'cisco.com'} - - -@fixture() -def invalid_json_observable_value(): - return {'action-id': 'some_action_id', - 'observable_type': 'ip', - 'observable_value': ''} - - -@patch('requests.get') -def test_respond_call_with_valid_jwt_but_invalid_json_value( - mock_request, client, valid_jwt, invalid_json_value, - invalid_json_expected_payload, route='/respond/observables' -): - 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.']}}" - ) - - -@patch('requests.get') -def test_respond_call_with_valid_jwt_but_invalid_json_action_id( - mock_request, client, valid_jwt, invalid_json_action_id, - invalid_json_expected_payload, route='/respond/trigger' -): - 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_action_id) - assert response.status_code == HTTPStatus.OK - assert response.json == invalid_json_expected_payload( - "{'action-id': ['Missing data for required field.']}" - ) - - -@patch('requests.get') -def test_respond_call_with_valid_jwt_but_invalid_json_observable_value( - mock_request, client, valid_jwt, invalid_json_observable_value, - invalid_json_expected_payload, route='/respond/trigger' -): - 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_observable_value) - assert response.status_code == HTTPStatus.OK - assert response.json == invalid_json_expected_payload( - "{'observable_value': ['Field may not be blank.']}" - ) - - -@fixture(scope='module') -def valid_json(route): - if route.endswith('/observables'): - return [{'type': 'domain', 'value': 'cisco.com'}] - - if route.endswith('/trigger'): - return {'action-id': 'valid-action-id', - 'observable_type': 'domain', - 'observable_value': 'cisco.com'} - - -@patch('requests.get') -def test_respond_call_success(mock_request, - route, client, valid_jwt, valid_json): - mock_request.return_value = \ - mock_api_response(payload=EXPECTED_RESPONSE_OF_JWKS_ENDPOINT) - response = client.post(route, headers=get_headers(valid_jwt()), - json=valid_json) - assert response.status_code == HTTPStatus.OK diff --git a/code/tests/unit/api/test_watchdog.py b/code/tests/unit/api/test_watchdog.py index 694070c..e7b57cb 100644 --- a/code/tests/unit/api/test_watchdog.py +++ b/code/tests/unit/api/test_watchdog.py @@ -1,4 +1,5 @@ from http import HTTPStatus + from pytest import fixture diff --git a/code/tests/unit/conftest.py b/code/tests/unit/conftest.py index 57dc857..aa641e5 100644 --- a/code/tests/unit/conftest.py +++ b/code/tests/unit/conftest.py @@ -1,9 +1,10 @@ +from http import HTTPStatus +from unittest.mock import MagicMock + import jwt +from pytest import fixture from app import app -from pytest import fixture -from http import HTTPStatus -from unittest.mock import MagicMock from api.errors import INVALID_ARGUMENT from tests.unit.payloads_for_tests import PRIVATE_KEY diff --git a/code/tests/unit/test_app.py b/code/tests/unit/test_app.py index 77cc7b8..cc631a2 100644 --- a/code/tests/unit/test_app.py +++ b/code/tests/unit/test_app.py @@ -15,14 +15,8 @@ def calls(): yield Call('GET', '/version', HTTPStatus.METHOD_NOT_ALLOWED) yield Call('GET', '/health', HTTPStatus.METHOD_NOT_ALLOWED) - yield Call('GET', '/deliberate/observables', HTTPStatus.METHOD_NOT_ALLOWED) yield Call('GET', '/observe/observables', HTTPStatus.METHOD_NOT_ALLOWED) yield Call('GET', '/refer/observables', HTTPStatus.METHOD_NOT_ALLOWED) - yield Call('GET', '/respond/observables', HTTPStatus.METHOD_NOT_ALLOWED) - yield Call('GET', '/respond/trigger', HTTPStatus.METHOD_NOT_ALLOWED) - yield Call('GET', '/tiles', HTTPStatus.METHOD_NOT_ALLOWED) - yield Call('GET', '/tiles/tile', HTTPStatus.METHOD_NOT_ALLOWED) - yield Call('GET', '/tiles/tile-data', HTTPStatus.METHOD_NOT_ALLOWED) yield Call('POST', '/watchdog', HTTPStatus.METHOD_NOT_ALLOWED) From 3a5e2fca8a0482decec6406e22bdfdd507fd9b82 Mon Sep 17 00:00:00 2001 From: mstoro Date: Tue, 5 Jul 2022 18:16:04 +0300 Subject: [PATCH 23/33] [CCTRI-3541] implements health endpoint --- code/api/client.py | 41 +++++++++++++++++++++++++++++++++++++ code/api/enrich.py | 6 +++--- code/api/errors.py | 10 +++++++++ code/api/health.py | 8 ++++++-- code/api/utils.py | 31 ++++++++++++++++++++-------- code/config.py | 7 +++++++ code/tests/unit/conftest.py | 8 +++++--- 7 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 code/api/client.py diff --git a/code/api/client.py b/code/api/client.py new file mode 100644 index 0000000..e423adf --- /dev/null +++ b/code/api/client.py @@ -0,0 +1,41 @@ +import requests +from flask import current_app +from requests.exceptions import ConnectionError +from http import HTTPStatus + +from api.errors import CyberScanConnectionError, AuthorizationError + +INVALID_CREDENTIALS = 'wrong api_key' + + +class CyberScanClient: + def __init__(self, credentials): + self._credentials = credentials + self._headers = { + 'User-Agent': current_app.config['USER_AGENT'] + } + + @property + def _url(self): + url = current_app.config['CYBERSCAN_API_ENDPOINT'] + return url.format(host=self._credentials.get('host').rstrip('/')) + + def health(self): + payload = { + 'key': self._credentials.get('api_key') + } + return self._request('token', method='POST', payload=payload) + + 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) + except ConnectionError: + 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 index ebe07f5..0d16f93 100644 --- a/code/api/enrich.py +++ b/code/api/enrich.py @@ -3,7 +3,7 @@ from flask import Blueprint from api.schemas import ObservableSchema -from api.utils import get_json, get_jwt, jsonify_data +from api.utils import get_json, get_credentials, jsonify_data enrich_api = Blueprint('enrich', __name__) @@ -12,13 +12,13 @@ @enrich_api.route('/observe/observables', methods=['POST']) def observe_observables(): - _ = get_jwt() + _ = get_credentials() _ = get_observables() return jsonify_data({}) @enrich_api.route('/refer/observables', methods=['POST']) def refer_observables(): - _ = get_jwt() + _ = get_credentials() _ = get_observables() return jsonify_data([]) diff --git a/code/api/errors.py b/code/api/errors.py index f12be5b..9d6070d 100644 --- a/code/api/errors.py +++ b/code/api/errors.py @@ -1,6 +1,7 @@ AUTH_ERROR = 'authorization error' INVALID_ARGUMENT = 'invalid argument' UNKNOWN = 'unknown' +CONNECTION_ERROR = 'connection error' class TRFormattedError(Exception): @@ -39,3 +40,12 @@ def __init__(self): 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}' + ) diff --git a/code/api/health.py b/code/api/health.py index db72e28..7982caf 100644 --- a/code/api/health.py +++ b/code/api/health.py @@ -1,11 +1,15 @@ from flask import Blueprint -from api.utils import get_jwt, jsonify_data +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(): - _ = get_jwt() + credentials = get_credentials() + client = CyberScanClient(credentials) + client.health() + return jsonify_data({'status': 'ok'}) diff --git a/code/api/utils.py b/code/api/utils.py index 9aadd5d..bc449c3 100644 --- a/code/api/utils.py +++ b/code/api/utils.py @@ -3,7 +3,7 @@ import jwt import requests -from flask import request, jsonify +from flask import request, jsonify, current_app from jwt import InvalidSignatureError, DecodeError, InvalidAudienceError from requests.exceptions import ConnectionError, InvalidURL, HTTPError @@ -78,7 +78,7 @@ def get_auth_token(): raise AuthorizationError(expected_errors[error.__class__]) -def get_jwt(): +def get_credentials(): """ Get Authorization token and validate its signature against the public key from /.well-known/jwks endpoint. @@ -89,8 +89,8 @@ def get_jwt(): """ expected_errors = { - KeyError: WRONG_PAYLOAD_STRUCTURE, - AssertionError: JWKS_HOST_MISSING, + KeyError: JWKS_HOST_MISSING, + AssertionError: WRONG_PAYLOAD_STRUCTURE, InvalidSignatureError: WRONG_KEY, DecodeError: WRONG_JWT_STRUCTURE, InvalidAudienceError: WRONG_AUDIENCE, @@ -98,15 +98,21 @@ def get_jwt(): } token = get_auth_token() try: - jwks_payload = jwt.decode(token, options={'verify_signature': False}) - assert 'jwks_host' in jwks_payload - jwks_host = jwks_payload.get('jwks_host') + 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('/')] ) - return payload['key'] + + 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) @@ -138,3 +144,12 @@ def 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 diff --git a/code/config.py b/code/config.py index d380803..6fadb14 100644 --- a/code/config.py +++ b/code/config.py @@ -4,3 +4,10 @@ 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/tests/unit/conftest.py b/code/tests/unit/conftest.py index aa641e5..7a6b61b 100644 --- a/code/tests/unit/conftest.py +++ b/code/tests/unit/conftest.py @@ -22,7 +22,8 @@ def client(): @fixture(scope='session') def valid_jwt(client): def _make_jwt( - key='some_key', + api_key='some_key', + host='host', jwks_host='visibility.amp.cisco.com', aud='http://localhost', kid='02B1174234C29F8EFB69911438F597FF3FFEE6B7', @@ -30,16 +31,17 @@ def _make_jwt( wrong_jwks_host=False ): payload = { - 'key': key, + '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('key') + payload.pop('api_key') return jwt.encode( payload, client.application.rsa_private_key, algorithm='RS256', From 782d013481a72a685906745509266b2fe036551f Mon Sep 17 00:00:00 2001 From: mstoro Date: Wed, 6 Jul 2022 16:50:28 +0300 Subject: [PATCH 24/33] [CCTRI-3542] implement observe/observables endpoint --- code/api/client.py | 26 +++++++++++++-- code/api/enrich.py | 24 +++++++++++--- code/api/mapping.py | 77 +++++++++++++++++++++++++++++++++++++++++++++ code/api/utils.py | 21 ++++++++++++- 4 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 code/api/mapping.py diff --git a/code/api/client.py b/code/api/client.py index e423adf..1a104d3 100644 --- a/code/api/client.py +++ b/code/api/client.py @@ -20,11 +20,33 @@ def _url(self): url = current_app.config['CYBERSCAN_API_ENDPOINT'] return url.format(host=self._credentials.get('host').rstrip('/')) - def health(self): + def _auth(self): payload = { 'key': self._credentials.get('api_key') } - return self._request('token', method='POST', payload=payload) + 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) + + return response.get('ip') + + def get_vulnerabilities(self, observable): + self._auth() + ip = observable['value'] if observable['type'] == 'ip' \ + else self._get_domain_ip(observable) + path = f'vulnerabilities/ip/{ip}' + response = self._request(path) + print(response) + + return response.get('vulnerabilities') def _request(self, path, method='GET', payload=None): url = '/'.join([self._url, path.lstrip('/')]) diff --git a/code/api/enrich.py b/code/api/enrich.py index 0d16f93..e4433b3 100644 --- a/code/api/enrich.py +++ b/code/api/enrich.py @@ -1,9 +1,11 @@ from functools import partial -from flask import Blueprint +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 +from api.utils import get_json, get_credentials, jsonify_data, jsonify_result enrich_api = Blueprint('enrich', __name__) @@ -12,9 +14,21 @@ @enrich_api.route('/observe/observables', methods=['POST']) def observe_observables(): - _ = get_credentials() - _ = get_observables() - return jsonify_data({}) + 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']) diff --git a/code/api/mapping.py b/code/api/mapping.py new file mode 100644 index 0000000..8d5e955 --- /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('title') + + 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/utils.py b/code/api/utils.py index bc449c3..d52c656 100644 --- a/code/api/utils.py +++ b/code/api/utils.py @@ -3,7 +3,7 @@ import jwt import requests -from flask import request, jsonify, current_app +from flask import request, jsonify, current_app, g from jwt import InvalidSignatureError, DecodeError, InvalidAudienceError from requests.exceptions import ConnectionError, InvalidURL, HTTPError @@ -153,3 +153,22 @@ def set_ctr_entities_limit(payload): 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) From 7d597045b973f8f6acb19237186cbd44e2cc5d6a Mon Sep 17 00:00:00 2001 From: mstoro Date: Thu, 7 Jul 2022 14:49:47 +0300 Subject: [PATCH 25/33] [CCTRI-3537] implement refer/observables --- code/api/client.py | 31 +++++++++++++++++++++++++++++++ code/api/enrich.py | 11 ++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/code/api/client.py b/code/api/client.py index 1a104d3..f965b30 100644 --- a/code/api/client.py +++ b/code/api/client.py @@ -7,6 +7,11 @@ INVALID_CREDENTIALS = 'wrong api_key' +REFER_PATH = { + 'ip': 'ip/{observable}', + 'domain': 'domain/{observable}' +} + class CyberScanClient: def __init__(self, credentials): @@ -48,6 +53,32 @@ def get_vulnerabilities(self, observable): return response.get('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('/')]) diff --git a/code/api/enrich.py b/code/api/enrich.py index e4433b3..75f6db1 100644 --- a/code/api/enrich.py +++ b/code/api/enrich.py @@ -33,6 +33,11 @@ def observe_observables(): @enrich_api.route('/refer/observables', methods=['POST']) def refer_observables(): - _ = get_credentials() - _ = get_observables() - return jsonify_data([]) + credentials = get_credentials() + observables = get_observables() + + client = CyberScanClient(credentials) + + relay_output = client.refer(observables) + + return jsonify_data(relay_output) From 61f55981cd66d7c42d89f88163a2fac7de944894 Mon Sep 17 00:00:00 2001 From: mstoro Date: Tue, 12 Jul 2022 11:34:52 +0300 Subject: [PATCH 26/33] [CCTRI-3544] implement limits logic --- code/api/client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/code/api/client.py b/code/api/client.py index f965b30..06f8503 100644 --- a/code/api/client.py +++ b/code/api/client.py @@ -19,6 +19,7 @@ def __init__(self, credentials): self._headers = { 'User-Agent': current_app.config['USER_AGENT'] } + self._ctr_entities_limit = current_app.config['CTR_ENTITIES_LIMIT'] @property def _url(self): @@ -49,9 +50,11 @@ def get_vulnerabilities(self, observable): else self._get_domain_ip(observable) path = f'vulnerabilities/ip/{ip}' response = self._request(path) - print(response) + vulnerabilities = response.get('vulnerabilities') + if len(vulnerabilities) > self._ctr_entities_limit: + vulnerabilities = vulnerabilities[:self._ctr_entities_limit] - return response.get('vulnerabilities') + return vulnerabilities def refer(self, observables): self._auth() From 8a53d070b55db32257f718394275fc331d29c703 Mon Sep 17 00:00:00 2001 From: mstoro Date: Tue, 12 Jul 2022 18:15:24 +0300 Subject: [PATCH 27/33] [CCTRI-3545] add unit tests --- code/api/client.py | 13 +- code/api/errors.py | 12 + code/api/mapping.py | 2 +- code/tests/unit/api/test_enrich.py | 59 +++- code/tests/unit/api/test_health.py | 16 +- code/tests/unit/api/test_watchdog.py | 17 + code/tests/unit/conftest.py | 24 ++ code/tests/unit/payloads_for_tests.py | 468 +++++++++++++++++++++++++- 8 files changed, 600 insertions(+), 11 deletions(-) diff --git a/code/api/client.py b/code/api/client.py index 06f8503..281ae55 100644 --- a/code/api/client.py +++ b/code/api/client.py @@ -1,9 +1,14 @@ +from http import HTTPStatus + import requests from flask import current_app -from requests.exceptions import ConnectionError -from http import HTTPStatus +from requests.exceptions import ConnectionError, SSLError -from api.errors import CyberScanConnectionError, AuthorizationError +from api.errors import ( + CyberScanConnectionError, + AuthorizationError, + CyberScanSSLError, +) INVALID_CREDENTIALS = 'wrong api_key' @@ -88,6 +93,8 @@ def _request(self, path, method='GET', payload=None): try: response = requests.request(method, url, json=payload, headers=self._headers) + except SSLError as error: + raise CyberScanSSLError(error) except ConnectionError: raise CyberScanConnectionError(url) diff --git a/code/api/errors.py b/code/api/errors.py index 9d6070d..d6d7150 100644 --- a/code/api/errors.py +++ b/code/api/errors.py @@ -49,3 +49,15 @@ def __init__(self, url): '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/mapping.py b/code/api/mapping.py index 8d5e955..34d087f 100644 --- a/code/api/mapping.py +++ b/code/api/mapping.py @@ -60,7 +60,7 @@ def _description(vulnerability): @staticmethod def _title(vulnerability): - return vulnerability.get('title') + return vulnerability.get('name') def extract(self, vulnerability): sighting = { diff --git a/code/tests/unit/api/test_enrich.py b/code/tests/unit/api/test_enrich.py index e940e36..1b6db35 100644 --- a/code/tests/unit/api/test_enrich.py +++ b/code/tests/unit/api/test_enrich.py @@ -1,11 +1,20 @@ 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 +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(): @@ -13,6 +22,22 @@ def routes(): 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 @@ -42,14 +67,40 @@ def test_enrich_call_with_valid_jwt_but_invalid_json_value( @fixture(scope='module') def valid_json(): - return [{'type': 'domain', 'value': 'cisco.com'}] + 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_request, +def test_enrich_call_success(mock_get, mock_request, mock_id, route, client, valid_jwt, valid_json): - mock_request.return_value = \ + 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 index 32714f5..ff454cc 100644 --- a/code/tests/unit/api/test_health.py +++ b/code/tests/unit/api/test_health.py @@ -1,8 +1,14 @@ 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(): @@ -14,6 +20,14 @@ def route(request): return request.param -def test_health_call_success(route, client, valid_jwt): +@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_watchdog.py b/code/tests/unit/api/test_watchdog.py index e7b57cb..3d62ce8 100644 --- a/code/tests/unit/api/test_watchdog.py +++ b/code/tests/unit/api/test_watchdog.py @@ -19,3 +19,20 @@ def test_watchdog_call_success(route, client): 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/conftest.py b/code/tests/unit/conftest.py index 7a6b61b..936f2a8 100644 --- a/code/tests/unit/conftest.py +++ b/code/tests/unit/conftest.py @@ -76,3 +76,27 @@ def mock_api_response(status_code=HTTPStatus.OK, payload=None): 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 index caa92b6..8e1ac50 100644 --- a/code/tests/unit/payloads_for_tests.py +++ b/code/tests/unit/payloads_for_tests.py @@ -50,7 +50,7 @@ ] } -PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY----- +PRIVATE_KEY = '''-----BEGIN RSA PRIVATE KEY----- MIIJKwIBAAKCAgEAtSKfSeI0fukRIX38AHlKB1YPpX8PUYN2JdvfM+XjNmLfU1M7 4N0VmdzIX95sneQGO9kC2xMIE+AIlt52Yf/KgBZggAlS9Y0Vx8DsSL2HvOjguAdX ir3vYLvAyyHin/mUisJOqccFKChHKjnk0uXy/38+1r17/cYTp76brKpU1I4kM20M @@ -100,4 +100,468 @@ nKyI8B5gw4C0G0iL1dSsz2bR1O4GNOVfT3R6joZEXATFo/Kc2L0YAvApBNUYvY0k bjJ/JfTO5060SsWftf4iw3jrhSn9RwTTYdq/kErGFWvDGJn2MiuhMe2onNfVzIGR mdUxHwi1ulkspAn/fmY7f0hZpskDwcHyZmbKZuk+NU/FJ8IAcmvk9y7m25nSSc8= ------END RSA PRIVATE KEY-----""" +-----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' + } + ], +} From 012fbaa9045aa4e464f357cb93ff929afebb8ec2 Mon Sep 17 00:00:00 2001 From: mstoro Date: Tue, 19 Jul 2022 11:46:28 +0300 Subject: [PATCH 28/33] [CCTRI-3550] handle 500 error if host field is empty --- code/api/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/api/client.py b/code/api/client.py index 281ae55..7f1a16c 100644 --- a/code/api/client.py +++ b/code/api/client.py @@ -2,7 +2,7 @@ import requests from flask import current_app -from requests.exceptions import ConnectionError, SSLError +from requests.exceptions import ConnectionError, SSLError, InvalidURL from api.errors import ( CyberScanConnectionError, @@ -95,7 +95,7 @@ def _request(self, path, method='GET', payload=None): headers=self._headers) except SSLError as error: raise CyberScanSSLError(error) - except ConnectionError: + except (ConnectionError, InvalidURL): raise CyberScanConnectionError(url) if response.ok: From b9f07c53f463b0573f0a16d0fef05072b8e44ff5 Mon Sep 17 00:00:00 2001 From: mstoro Date: Tue, 19 Jul 2022 12:06:58 +0300 Subject: [PATCH 29/33] [CCTRI-3551] handle 500 error in case of special symbol in observable --- code/api/client.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/code/api/client.py b/code/api/client.py index 7f1a16c..0e050fb 100644 --- a/code/api/client.py +++ b/code/api/client.py @@ -46,18 +46,25 @@ def health(self): def _get_domain_ip(self, observable): path = f'domain/{observable["value"]}' response = self._request(path) + try: + ip = response.get('ip') + except AttributeError: + ip = '' - return response.get('ip') + return ip def get_vulnerabilities(self, observable): self._auth() ip = observable['value'] if observable['type'] == 'ip' \ else self._get_domain_ip(observable) - path = f'vulnerabilities/ip/{ip}' - response = self._request(path) - vulnerabilities = response.get('vulnerabilities') - if len(vulnerabilities) > self._ctr_entities_limit: - vulnerabilities = vulnerabilities[:self._ctr_entities_limit] + if ip: + path = f'vulnerabilities/ip/{ip}' + response = self._request(path) + vulnerabilities = response.get('vulnerabilities') + if len(vulnerabilities) > self._ctr_entities_limit: + vulnerabilities = vulnerabilities[:self._ctr_entities_limit] + else: + vulnerabilities = [] return vulnerabilities From e2ce24f2637e83049269907bef9dbfd8cb3378a7 Mon Sep 17 00:00:00 2001 From: mstoro Date: Tue, 19 Jul 2022 12:22:44 +0300 Subject: [PATCH 30/33] [CCTRI-3552] add timeout in case of wrong host or cyrillic symbols in host --- code/api/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/api/client.py b/code/api/client.py index 0e050fb..aed86fe 100644 --- a/code/api/client.py +++ b/code/api/client.py @@ -99,7 +99,7 @@ def _request(self, path, method='GET', payload=None): try: response = requests.request(method, url, json=payload, - headers=self._headers) + headers=self._headers, timeout=10) except SSLError as error: raise CyberScanSSLError(error) except (ConnectionError, InvalidURL): From bbc382752de995fa66af1bcad919e52b437ed553 Mon Sep 17 00:00:00 2001 From: mstoro Date: Tue, 19 Jul 2022 19:14:54 +0300 Subject: [PATCH 31/33] [CCTRI-3551] handle special symbols in body for ip type --- code/api/client.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/code/api/client.py b/code/api/client.py index aed86fe..335d0ce 100644 --- a/code/api/client.py +++ b/code/api/client.py @@ -57,15 +57,13 @@ def get_vulnerabilities(self, observable): self._auth() ip = observable['value'] if observable['type'] == 'ip' \ else self._get_domain_ip(observable) - if ip: - path = f'vulnerabilities/ip/{ip}' - response = self._request(path) + 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] - else: - vulnerabilities = [] - return vulnerabilities def refer(self, observables): From 5263920f76de52cce9e14ae86c235c04c65e785d Mon Sep 17 00:00:00 2001 From: mstoro Date: Thu, 21 Jul 2022 17:29:29 +0300 Subject: [PATCH 32/33] [CCTRI-3539] update module_type.json.sample --- module_type.json.sample | 54 +++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/module_type.json.sample b/module_type.json.sample index 2e6ea76..2a321c3 100644 --- a/module_type.json.sample +++ b/module_type.json.sample @@ -1,22 +1,36 @@ { - "title": "Docker Relay", - "default_name": "Docker Relay", - "short_description": "Generic Docker Relay module that can be used when developing new integrations", - "description": "Generic Docker Relay module description", - "tips": When configuring this integration, you must first gather some information from your third-party account if needed and add the Docker Relay Module\n\n1. Complete the **Add New Docker Relay Module** form:\n - **Module Name** - Leave the default name or enter a name that is meaningful to you.\n - **API KEY** - Enter third party api key\n9. Click **Save** to complete the Docker Relay module configuration.", + "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 CyberScant integration module configuration.", "external_references": [ { - "label": "Docker Relay Template", - "link": "https://github.com/CiscoSecurity/tr-05-docker-relay" + "label": "CyberScan", + "link": "https://www.cyberscan.io/" } ], "configuration_spec": [ { - "key": "custom_API_KEY", + "key": "custom_api_key", "type": "api_key", "label": "API KEY", - "tooltip": "The module 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": [ @@ -24,10 +38,6 @@ "id": "health", "description": "Healthcheck" }, - { - "id": "deliberate", - "description": "Deliberation" - }, { "id": "observe", "description": "Enrichments" @@ -35,32 +45,18 @@ { "id": "refer", "description": "Reference links" - }, - { - "id": "respond", - "description": "Response actions" - }, - { - "id": "tiles", - "description": "Dashboard tiles" } ], "properties": { "supported-apis": [ "health", "observe/observables", - "deliberate/observables", - "refer/observables", - "respond/observables", - "respond/trigger", - "tiles", - "tiles/tile", - "tiles/tile-data" + "refer/observables" ], "auth-type": "configuration-token", "configuration-token-alg": "RS256", "custom_jwks_host": "visibility.amp.cisco.com", "url": "https://ciscohosted.url" }, - "logo": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCI+Cgk8c3R5bGU+CgkJdHNwYW4geyB3aGl0ZS1zcGFjZTpwcmUgfQoJCS5zaHAwIHsgZmlsbDogIzk1YTVhNiB9IAoJCS5zaHAxIHsgZmlsbDogIzdmOGM4ZCB9IAoJPC9zdHlsZT4KCTxnIGlkPSJMYXllciI+CgkJPHBhdGggaWQ9IkxheWVyIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsYXNzPSJzaHAwIiBkPSJNMTcuNTkgMTEuNjdMMjYuNDMgMTQuNThMMjcuMDcgMTVDMjguNSAxNC4xNyAyOS45OCAxMy4zMyAzMS40OSAxMi41TDMxLjIyIDExLjY3TDMyLjk3IDIuNUw0MS4wNyAwLjQyTDQ3LjI0IDcuNUw0Ny40MiA4LjMzQzQ5LjE1IDcuOTIgNTAuODUgNy45MiA1Mi41OCA4LjMzTDUyLjc2IDcuNUw1OC45MyAwLjQyTDY3LjAzIDIuNUw2OC43OCAxMS42N0w2OC41MSAxMi41QzcwLjAyIDEzLjMzIDcxLjUgMTQuMTcgNzIuOTMgMTVMNzMuNTcgMTQuNThMODIuNDEgMTEuNjdMODguMyAxNy41TDg1LjM1IDI2LjI1TDg0LjcxIDI3LjA4Qzg1LjY1IDI4LjMzIDg2LjU0IDMwIDg3LjI5IDMxLjI1TDg4LjEyIDMxLjI1TDk3LjIzIDMyLjkyTDk5LjQ0IDQwLjgzTDkyLjM1IDQ3LjA4TDkxLjYyIDQ3LjA4QzkxLjczIDQ5LjE3IDkxLjczIDUwLjgzIDkxLjYyIDUyLjVMOTIuMzUgNTIuNUw5OS40NCA1OC43NUw5Ny4yMyA2Ny4wOEw4OC4xMiA2OC43NUw4Ny4yOSA2OC4zM0M4Ni41NCA3MCA4NS42NSA3MS4yNSA4NC43MSA3Mi45Mkw4NS4zNSA3My4zM0w4OC4zIDgyLjA4TDgyLjQxIDg4LjMzTDczLjU3IDg1TDcyLjkzIDg0LjU4QzcxLjUgODUuNDIgNzAuMDIgODYuMjUgNjguNTEgODcuMDhMNjguNzggODcuOTJMNjcuMDMgOTcuMDhMNTguOTMgOTkuMTdMNTIuNzYgOTIuMDhMNTIuNTggOTEuNjdMNDcuNDIgOTEuNjdMNDcuMjQgOTIuMDhMNDEuMDcgOTkuMTdMMzIuOTcgOTcuMDhMMzEuMjIgODcuOTJMMzEuNDkgODcuMDhDMjkuOTggODYuMjUgMjguNSA4NS40MiAyNy4wNyA4NC41OEwyNi40MyA4NUwxNy41OSA4OC4zM0wxMS43IDgyLjA4TDE0LjY0IDczLjMzTDE1LjI5IDcyLjkyQzE0LjM0IDcxLjI1IDEzLjQ2IDcwIDEyLjcxIDY4LjMzTDExLjg4IDY4Ljc1TDIuNzcgNjcuMDhMMC41NiA1OC43NUw3LjY1IDUyLjVMOC4zOCA1Mi41QzguMjggNTAuODMgOC4yOCA0OS4xNyA4LjM4IDQ3LjA4TDcuNjUgNDcuMDhMMC41NiA0MC44M0wyLjc3IDMyLjkyTDExLjg4IDMxLjI1TDEyLjcxIDMxLjI1QzEzLjQ2IDMwIDE0LjM0IDI4LjMzIDE1LjI5IDI3LjA4TDE0LjY0IDI2LjI1TDExLjcgMTcuNUwxNy41OSAxMS42N0wxNy41OSAxMS42N1pNMjkuMzggMjkuMTdDMTcuOTkgNDAuNDIgMTcuOTkgNTkuMTcgMjkuMzggNzAuNDJDNDAuNzcgODEuNjcgNTkuMjMgODEuNjcgNzAuNjMgNzAuNDJDODIuMDEgNTkuMTcgODIuMDEgNDAuNDIgNzAuNjMgMjkuMTdDNTkuMjMgMTcuOTIgNDAuNzcgMTcuOTIgMjkuMzggMjkuMTdaIiAvPgoJCTxwYXRoIGlkPSJMYXllciIgZmlsbC1ydWxlPSJldmVub2RkIiBjbGFzcz0ic2hwMSIgZD0iTTI2LjQzIDI2LjI1QzM5LjQ1IDEzLjMzIDYwLjU1IDEzLjMzIDczLjU3IDI2LjI1Qzg2LjU5IDM5LjE3IDg2LjU5IDYwLjQyIDczLjU3IDczLjMzQzYwLjU1IDg2LjI1IDM5LjQ1IDg2LjI1IDI2LjQzIDczLjMzQzEzLjQxIDYwLjQyIDEzLjQxIDM5LjE3IDI2LjQzIDI2LjI1TDI2LjQzIDI2LjI1Wk00MS42MiAzNS40MkM0NS4zMSAzMy4zMyA0OS41MiAzMi41IDUzLjU5IDMzLjc1TDU5LjM5IDIyLjA4QzUwLjQ3IDE5LjE3IDQwLjQzIDIwLjgzIDMyLjYgMjYuNjdMNDEuNjIgMzUuNDJaTTI2LjcxIDMyLjVDMjAuODUgNDAuNDIgMTkuMzUgNTAuNDIgMjIuMzggNTkuMTdMMzMuNyA1My4zM0MzMi44IDQ5LjE3IDMzLjU1IDQ1IDM1LjczIDQxLjY3TDI2LjcxIDMyLjVaTTQ0LjExIDQzLjc1QzQwLjg1IDQ3LjA4IDQwLjg1IDUyLjUgNDQuMTEgNTUuODNDNDcuMzYgNTkuMTcgNTIuNjQgNTkuMTcgNTUuODkgNTUuODNDNTkuMTUgNTIuNSA1OS4xNSA0Ny4wOCA1NS44OSA0My43NUM1Mi42NCA0MC44MyA0Ny4zNiA0MC44MyA0NC4xMSA0My43NVpNNjYuNzYgMjYuMjVMNjAuOTYgMzcuNUM2MS4yMiAzNy41IDYxLjUzIDM3LjkyIDYxLjc4IDM3LjkyQzY0LjY2IDQwLjgzIDY2LjIzIDQ0LjU4IDY2LjU3IDQ4LjMzTDc5LjEgNTAuNDJDNzkuMTkgNDIuNSA3Ni40MSAzNSA3MC42MyAyOS4xN0M2OS40MiAyNy45MiA2OC4xIDI3LjA4IDY2Ljc2IDI2LjI1Wk0yOS4zOCA3MC40MkMzNS4xNiA3Ni4yNSA0Mi43OCA3OS4xNyA1MC4zNyA3OC43NUw0OC41MiA2Ni4yNUM0NC43NyA2Ni4yNSA0MS4wOSA2NC41OCAzOC4yMSA2MS42N0MzNy45NiA2MS4yNSAzNy44IDYxLjI1IDM3LjU3IDYwLjgzTDI2LjI1IDY2LjY3QzI3LjE5IDY3LjkyIDI4LjE4IDY5LjE3IDI5LjM4IDcwLjQyWk02MS43OCA2MS42N0M2MC4yNiA2My4zMyA1OC40OSA2NC4xNyA1Ni42MyA2NUw1OC41NiA3Ny45MkM2Mi45NyA3Ni4yNSA2Ny4xNCA3My43NSA3MC42MyA3MC40MkM3NC4xMSA2Ny4wOCA3Ni41NSA2Mi45MiA3Ny45IDU4LjMzTDY1LjI4IDU2LjY3QzY0LjQ4IDU4LjMzIDYzLjMxIDYwIDYxLjc4IDYxLjY3WiIgLz4KCTwvZz4KPC9zdmc+" + "logo": "data:image/png;base64,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" } From 5733be8b9dd3daeee1cb814cd354818d93a1d6a1 Mon Sep 17 00:00:00 2001 From: mstoro <78480384+mstoro@users.noreply.github.com> Date: Thu, 21 Jul 2022 17:46:54 +0300 Subject: [PATCH 33/33] Update module_type.json.sample --- module_type.json.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module_type.json.sample b/module_type.json.sample index 2a321c3..bc6066a 100644 --- a/module_type.json.sample +++ b/module_type.json.sample @@ -3,7 +3,7 @@ "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 CyberScant integration module configuration.", + "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",