From 779fa0829e9f1244f46316ae45e15002e04d642c Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 8 Mar 2024 13:45:13 +0100 Subject: [PATCH 01/29] [ADD] First steps to introduce graphql --- source/app/blueprints/graphql/__init__.py | 0 .../app/blueprints/graphql/graphql_route.py | 87 +++++++++++++++++++ source/app/views.py | 3 + source/requirements.txt | 3 + 4 files changed, 93 insertions(+) create mode 100644 source/app/blueprints/graphql/__init__.py create mode 100644 source/app/blueprints/graphql/graphql_route.py diff --git a/source/app/blueprints/graphql/__init__.py b/source/app/blueprints/graphql/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py new file mode 100644 index 000000000..b839bf0cc --- /dev/null +++ b/source/app/blueprints/graphql/graphql_route.py @@ -0,0 +1,87 @@ +# IRIS Source Code +# Copyright (C) ${current_year} - DFIR-IRIS +# contact@dfir-iris.org +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +from functools import wraps +from flask import request +from flask_wtf import FlaskForm +from flask import Blueprint +from graphql_server.flask import GraphQLView +from graphene import ObjectType, String, Schema + +from app.util import is_user_authenticated +from app.util import response_error + + +class Query(ObjectType): + """Query documentation""" + + hello = String(first_name=String(default_value='stranger'), description='Field documentation') + goodbye = String() + + def resolve_hello(root, info, first_name): + return f'Hello {first_name}!' + + def resolve_goodbye(root, info): + return 'See ya!' + + +# util.ac_api_requires does not seem to work => it leads to error: +# TypeError: dispatch_request() got an unexpected keyword argument 'caseid' +# so I rewrote a simpler decorator... +# Maybe, no decorator is needed (since graphql needs only one endpoint) => try to write code directly +def ac_graphql_requires(): + def inner_wrap(f): + @wraps(f) + def wrap(*args, **kwargs): + if request.method == 'POST': + cookie_session = request.cookies.get('session') + if cookie_session: + form = FlaskForm() + if not form.validate(): + return response_error('Invalid CSRF token') + elif request.is_json: + request.json.pop('csrf_token') + + if not is_user_authenticated(request): + return response_error('Authentication required', status=401) + + return f(*args, **kwargs) + return wrap + return inner_wrap + + +schema = Schema(query=Query) +graphql_view = GraphQLView.as_view('graphql', schema=schema) + +graphql_blueprint = Blueprint('graphql', __name__) + + +@graphql_blueprint.route('/graphql', methods=['POST']) +@ac_graphql_requires() +def process_graphql_request(*args, **kwargs): + return graphql_view(*args, **kwargs) + +# TODO add first unit tests: test request is rejected with wrong token, test request is successful +# TODO try to rewrite this as another blueprint and group it with the other blueprints +# TODO how to handle permissions? +# TODO link with the database: graphene-sqlalchemy +# TODO I am unsure about the code organization (directories) +# curl --insecure -X POST -H "Content-Type: application/json" -d '{ "query": "{ hello(firstName: \"friendly\") }" }' https://127.0.0.1/graphql +#app.add_url_rule('/graphql', view_func=GraphQLView.as_view('graphql', schema=schema)) +#API_KEY=B8BA5D730210B50F41C06941582D7965D57319D5685440587F98DFDC45A01594 +#curl --insecure -X POST --header 'Authorization: Bearer '${API_KEY} --header 'Content-Type: application/json' -d '{ "query": "{ hello(firstName: \"friendly\") }" }' https://127.0.0.1/graphql diff --git a/source/app/views.py b/source/app/views.py index bfaf836e9..6bf1a0225 100644 --- a/source/app/views.py +++ b/source/app/views.py @@ -30,6 +30,7 @@ from app.blueprints.case.case_routes import case_blueprint from app.blueprints.context.context import ctx_blueprint # Blueprints +from app.blueprints.graphql.graphql_route import graphql_blueprint from app.blueprints.dashboard.dashboard_routes import dashboard_blueprint from app.blueprints.datastore.datastore_routes import datastore_blueprint from app.blueprints.demo_landing.demo_landing import demo_blueprint @@ -67,6 +68,8 @@ from app.models.authorization import User from app.post_init import run_post_init + +app.register_blueprint(graphql_blueprint) app.register_blueprint(dashboard_blueprint) app.register_blueprint(overview_blueprint) app.register_blueprint(login_blueprint) diff --git a/source/requirements.txt b/source/requirements.txt index b80f631c1..83a15151c 100644 --- a/source/requirements.txt +++ b/source/requirements.txt @@ -32,6 +32,9 @@ PyJWT==2.4.0 cryptography>=39.0.1 ldap3==2.9.1 pyintelowl>=4.4.0 +graphene==3.3 +# unfortunately we are relying on a beta version here. I hope a definitive version gets released soon +graphql-server[flask]==3.0.0b7 dependencies/docx_generator-0.8.0-py3-none-any.whl dependencies/iris_interface-1.2.0-py3-none-any.whl From 1959ca628de5602363ab687aa4769b1ee43fcc61 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 8 Mar 2024 14:42:13 +0100 Subject: [PATCH 02/29] [CLEAN] Put graphql blueprint initialization code in a function --- .../app/blueprints/graphql/graphql_route.py | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index b839bf0cc..61ba07483 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -44,37 +44,37 @@ def resolve_goodbye(root, info): # TypeError: dispatch_request() got an unexpected keyword argument 'caseid' # so I rewrote a simpler decorator... # Maybe, no decorator is needed (since graphql needs only one endpoint) => try to write code directly -def ac_graphql_requires(): - def inner_wrap(f): - @wraps(f) - def wrap(*args, **kwargs): - if request.method == 'POST': - cookie_session = request.cookies.get('session') - if cookie_session: - form = FlaskForm() - if not form.validate(): - return response_error('Invalid CSRF token') - elif request.is_json: - request.json.pop('csrf_token') +def ac_graphql_requires(f): + @wraps(f) + def wrap(*args, **kwargs): + if request.method == 'POST': + cookie_session = request.cookies.get('session') + if cookie_session: + form = FlaskForm() + if not form.validate(): + return response_error('Invalid CSRF token') + elif request.is_json: + request.json.pop('csrf_token') - if not is_user_authenticated(request): - return response_error('Authentication required', status=401) + if not is_user_authenticated(request): + return response_error('Authentication required', status=401) - return f(*args, **kwargs) - return wrap - return inner_wrap + return f(*args, **kwargs) + return wrap -schema = Schema(query=Query) -graphql_view = GraphQLView.as_view('graphql', schema=schema) +def _create_blueprint(): + schema = Schema(query=Query) + graphql_view = GraphQLView.as_view('graphql', schema=schema) + graphql_view_with_authentication = ac_graphql_requires(graphql_view) -graphql_blueprint = Blueprint('graphql', __name__) + blueprint = Blueprint('graphql', __name__) + blueprint.add_url_rule('/graphql', view_func=graphql_view_with_authentication, methods=['POST']) + return blueprint -@graphql_blueprint.route('/graphql', methods=['POST']) -@ac_graphql_requires() -def process_graphql_request(*args, **kwargs): - return graphql_view(*args, **kwargs) + +graphql_blueprint = _create_blueprint() # TODO add first unit tests: test request is rejected with wrong token, test request is successful # TODO try to rewrite this as another blueprint and group it with the other blueprints @@ -84,4 +84,4 @@ def process_graphql_request(*args, **kwargs): # curl --insecure -X POST -H "Content-Type: application/json" -d '{ "query": "{ hello(firstName: \"friendly\") }" }' https://127.0.0.1/graphql #app.add_url_rule('/graphql', view_func=GraphQLView.as_view('graphql', schema=schema)) #API_KEY=B8BA5D730210B50F41C06941582D7965D57319D5685440587F98DFDC45A01594 -#curl --insecure -X POST --header 'Authorization: Bearer '${API_KEY} --header 'Content-Type: application/json' -d '{ "query": "{ hello(firstName: \"friendly\") }" }' https://127.0.0.1/graphql +#curl --insecure -X POST --header 'Authorization: Bearer '${API_KEY} --header 'Content-Type: application/json' -d '{ "query": "{ hello(firstName: \"friendly\") }" }' https://127.0.0.1/graphql \ No newline at end of file From 77ac06a1be9a369bcd4a1df3230cece251148f1c Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 8 Mar 2024 15:03:52 +0100 Subject: [PATCH 03/29] [ADD] First tests of the graphql API (pretty raw for the time being) --- .../app/blueprints/graphql/graphql_route.py | 7 +----- tests/tests.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index 61ba07483..eec76f5b2 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -76,12 +76,7 @@ def _create_blueprint(): graphql_blueprint = _create_blueprint() -# TODO add first unit tests: test request is rejected with wrong token, test request is successful -# TODO try to rewrite this as another blueprint and group it with the other blueprints # TODO how to handle permissions? # TODO link with the database: graphene-sqlalchemy # TODO I am unsure about the code organization (directories) -# curl --insecure -X POST -H "Content-Type: application/json" -d '{ "query": "{ hello(firstName: \"friendly\") }" }' https://127.0.0.1/graphql -#app.add_url_rule('/graphql', view_func=GraphQLView.as_view('graphql', schema=schema)) -#API_KEY=B8BA5D730210B50F41C06941582D7965D57319D5685440587F98DFDC45A01594 -#curl --insecure -X POST --header 'Authorization: Bearer '${API_KEY} --header 'Content-Type: application/json' -d '{ "query": "{ hello(firstName: \"friendly\") }" }' https://127.0.0.1/graphql \ No newline at end of file +# TODO define the graphql API and establish the most important needs diff --git a/tests/tests.py b/tests/tests.py index f88b1f7a0..9d8cbeb6a 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -18,6 +18,8 @@ from unittest import TestCase from iris import Iris +import requests +from iris import _API_KEY, _API_URL class Tests(TestCase): @@ -54,3 +56,25 @@ def test_update_case_should_not_require_case_name_issue_358(self): case_identifier = response['data']['case_id'] response = self._subject.update_case(case_identifier, {'case_tags': 'test,example'}) self.assertEqual('success', response['status']) + + # TODO rewrite this test in a nicer way (too low level) + # TODO use gql + def test_graphql_endpoint_should_not_fail(self): + url = _API_URL + '/graphql' + _headers = {'Authorization': f'Bearer {_API_KEY}', 'Content-Type': 'application/json'} + payload = { + 'query': '{ hello(firstName: "Paul") }' + } + response = requests.post(_API_URL + '/graphql', headers=_headers, json=payload) + body = response.json() + self.assertEqual('Hello Paul!', body['data']['hello']) + + # TODO rewrite this test in a nicer way (too low level) + def test_graphql_endpoint_should_reject_requests_with_wrong_authentication_token(self): + url = _API_URL + '/graphql' + _headers = {'Authorization': f'Bearer 0000000000000000000000000000000000000000000000000000000000000000', 'Content-Type': 'application/json'} + payload = { + 'query': '{ hello(firstName: "friendly") }' + } + response = requests.post(url, headers=_headers, json=payload) + self.assertEqual(401, response.status_code) From c58cf0173f2e8e107c4b5b3a83df3f2ced070b32 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:29:43 +0100 Subject: [PATCH 04/29] [IMP] use removed small duplication in test --- tests/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests.py b/tests/tests.py index 9d8cbeb6a..f025ea175 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -65,7 +65,7 @@ def test_graphql_endpoint_should_not_fail(self): payload = { 'query': '{ hello(firstName: "Paul") }' } - response = requests.post(_API_URL + '/graphql', headers=_headers, json=payload) + response = requests.post(url, headers=_headers, json=payload) body = response.json() self.assertEqual('Hello Paul!', body['data']['hello']) From cc8ad079b1610c11542523ad66731dfb9ace5307 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 15 Mar 2024 11:33:24 +0100 Subject: [PATCH 05/29] [IMP] prefixed local method with underscore --- source/app/blueprints/graphql/graphql_route.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index eec76f5b2..83f77d9fc 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -44,7 +44,7 @@ def resolve_goodbye(root, info): # TypeError: dispatch_request() got an unexpected keyword argument 'caseid' # so I rewrote a simpler decorator... # Maybe, no decorator is needed (since graphql needs only one endpoint) => try to write code directly -def ac_graphql_requires(f): +def _ac_graphql_requires(f): @wraps(f) def wrap(*args, **kwargs): if request.method == 'POST': @@ -66,7 +66,7 @@ def wrap(*args, **kwargs): def _create_blueprint(): schema = Schema(query=Query) graphql_view = GraphQLView.as_view('graphql', schema=schema) - graphql_view_with_authentication = ac_graphql_requires(graphql_view) + graphql_view_with_authentication = _ac_graphql_requires(graphql_view) blueprint = Blueprint('graphql', __name__) blueprint.add_url_rule('/graphql', view_func=graphql_view_with_authentication, methods=['POST']) From 7e4ef404ba71f399f4ab1bf72d23f8ce6bf804bf Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 15 Mar 2024 14:02:06 +0100 Subject: [PATCH 06/29] [IMP] Improved test readability --- tests/graphql_api.py | 29 +++++++++++++++++++++++++++++ tests/iris.py | 10 ++++++++-- tests/tests.py | 17 +++++------------ 3 files changed, 42 insertions(+), 14 deletions(-) create mode 100644 tests/graphql_api.py diff --git a/tests/graphql_api.py b/tests/graphql_api.py new file mode 100644 index 000000000..98dc5864b --- /dev/null +++ b/tests/graphql_api.py @@ -0,0 +1,29 @@ +# IRIS Source Code +# Copyright (C) 2023 - DFIR-IRIS +# contact@dfir-iris.org +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import requests + + +class GraphQLApi: + + def __init__(self, url, api_key): + self._url = url + self._headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'} + + def execute(self, payload): + return requests.post(self._url, headers=self._headers, json=payload) diff --git a/tests/iris.py b/tests/iris.py index 2bc2f9da6..7f415bff6 100644 --- a/tests/iris.py +++ b/tests/iris.py @@ -21,9 +21,10 @@ import time from docker_compose import DockerCompose from rest_api import RestApi +from graphql_api import GraphQLApi from server_timeout_error import ServerTimeoutError -_API_URL = 'http://127.0.0.1:8000' +API_URL = 'http://127.0.0.1:8000' _API_KEY = 'B8BA5D730210B50F41C06941582D7965D57319D5685440587F98DFDC45A01594' _IRIS_PATH = Path('..') _TEST_DATA_PATH = Path('./data') @@ -33,7 +34,8 @@ class Iris: def __init__(self): self._docker_compose = DockerCompose(_IRIS_PATH) - self._api = RestApi(_API_URL, _API_KEY) + self._api = RestApi(API_URL, _API_KEY) + self._graphql_api = GraphQLApi(API_URL + '/graphql', _API_KEY) def _wait(self, condition, attempts, sleep_duration=1): count = 0 @@ -89,3 +91,7 @@ def update_case(self, case_identifier, data): def get_cases(self): return self._api.get('/manage/cases/list') + + def execute_graphql_query(self, payload): + response = self._graphql_api.execute(payload) + return response.json() diff --git a/tests/tests.py b/tests/tests.py index f025ea175..6ddf80bb8 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -18,8 +18,8 @@ from unittest import TestCase from iris import Iris -import requests -from iris import _API_KEY, _API_URL +from iris import API_URL +from graphql_api import GraphQLApi class Tests(TestCase): @@ -57,24 +57,17 @@ def test_update_case_should_not_require_case_name_issue_358(self): response = self._subject.update_case(case_identifier, {'case_tags': 'test,example'}) self.assertEqual('success', response['status']) - # TODO rewrite this test in a nicer way (too low level) - # TODO use gql def test_graphql_endpoint_should_not_fail(self): - url = _API_URL + '/graphql' - _headers = {'Authorization': f'Bearer {_API_KEY}', 'Content-Type': 'application/json'} payload = { 'query': '{ hello(firstName: "Paul") }' } - response = requests.post(url, headers=_headers, json=payload) - body = response.json() + body = self._subject.execute_graphql_query(payload) self.assertEqual('Hello Paul!', body['data']['hello']) - # TODO rewrite this test in a nicer way (too low level) def test_graphql_endpoint_should_reject_requests_with_wrong_authentication_token(self): - url = _API_URL + '/graphql' - _headers = {'Authorization': f'Bearer 0000000000000000000000000000000000000000000000000000000000000000', 'Content-Type': 'application/json'} + graphql_api = GraphQLApi(API_URL + '/graphql', 64*'0') payload = { 'query': '{ hello(firstName: "friendly") }' } - response = requests.post(url, headers=_headers, json=payload) + response = graphql_api.execute(payload) self.assertEqual(401, response.status_code) From 0afe528a478c3b15b9cf2e52af7885cade4d6b85 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 15 Mar 2024 14:10:53 +0100 Subject: [PATCH 07/29] [IMP] Removed now obsolete comment --- source/app/blueprints/graphql/graphql_route.py | 1 - 1 file changed, 1 deletion(-) diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index 83f77d9fc..7bb01416d 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -43,7 +43,6 @@ def resolve_goodbye(root, info): # util.ac_api_requires does not seem to work => it leads to error: # TypeError: dispatch_request() got an unexpected keyword argument 'caseid' # so I rewrote a simpler decorator... -# Maybe, no decorator is needed (since graphql needs only one endpoint) => try to write code directly def _ac_graphql_requires(f): @wraps(f) def wrap(*args, **kwargs): From 770f320933c7982da6f25917460ed44afd9435ff Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 15 Mar 2024 14:54:57 +0100 Subject: [PATCH 08/29] [ADD] Started implementation of query cases, which should return the list of cases visible by the user --- source/app/blueprints/graphql/graphql_route.py | 12 +++++++++++- source/requirements.txt | 1 + tests/tests.py | 17 ++++++++++++----- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index 7bb01416d..c01eccc15 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -21,18 +21,28 @@ from flask_wtf import FlaskForm from flask import Blueprint from graphql_server.flask import GraphQLView -from graphene import ObjectType, String, Schema +from graphene import ObjectType, String, Schema, List +from graphene_sqlalchemy import SQLAlchemyObjectType +from app.models.cases import Cases from app.util import is_user_authenticated from app.util import response_error +class CaseObject(SQLAlchemyObjectType): + class Meta: + model = Cases + + class Query(ObjectType): """Query documentation""" hello = String(first_name=String(default_value='stranger'), description='Field documentation') goodbye = String() + # starting with the conversion of '/manage/cases/list' + cases = List(lambda: CaseObject, description='author documentation') + def resolve_hello(root, info, first_name): return f'Hello {first_name}!' diff --git a/source/requirements.txt b/source/requirements.txt index 83a15151c..772a01057 100644 --- a/source/requirements.txt +++ b/source/requirements.txt @@ -35,6 +35,7 @@ pyintelowl>=4.4.0 graphene==3.3 # unfortunately we are relying on a beta version here. I hope a definitive version gets released soon graphql-server[flask]==3.0.0b7 +graphene-sqlalchemy==3.0.0rc1 dependencies/docx_generator-0.8.0-py3-none-any.whl dependencies/iris_interface-1.2.0-py3-none-any.whl diff --git a/tests/tests.py b/tests/tests.py index 6ddf80bb8..dbb1049c2 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -57,6 +57,14 @@ def test_update_case_should_not_require_case_name_issue_358(self): response = self._subject.update_case(case_identifier, {'case_tags': 'test,example'}) self.assertEqual('success', response['status']) + def test_graphql_endpoint_should_reject_requests_with_wrong_authentication_token(self): + graphql_api = GraphQLApi(API_URL + '/graphql', 64*'0') + payload = { + 'query': '{ hello(firstName: "friendly") }' + } + response = graphql_api.execute(payload) + self.assertEqual(401, response.status_code) + def test_graphql_endpoint_should_not_fail(self): payload = { 'query': '{ hello(firstName: "Paul") }' @@ -64,10 +72,9 @@ def test_graphql_endpoint_should_not_fail(self): body = self._subject.execute_graphql_query(payload) self.assertEqual('Hello Paul!', body['data']['hello']) - def test_graphql_endpoint_should_reject_requests_with_wrong_authentication_token(self): - graphql_api = GraphQLApi(API_URL + '/graphql', 64*'0') + def test_graphql_cases_should_contain_the_initial_case(self): payload = { - 'query': '{ hello(firstName: "friendly") }' + 'query': '{ cases { name } }' } - response = graphql_api.execute(payload) - self.assertEqual(401, response.status_code) + body = self._subject.execute_graphql_query(payload) + # TODO should check the list contains an element with name "#1 - Initial Demo" From 3a4201200e41260afb9b19c6ee4eb89c1b316577 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Mon, 18 Mar 2024 15:57:05 +0100 Subject: [PATCH 09/29] [ADD] Get cases by the current user identifier --- source/app/blueprints/graphql/graphql_route.py | 10 ++++++++++ tests/docker_compose.py | 8 +++++--- tests/tests.py | 5 ++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index c01eccc15..087f6f803 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -20,6 +20,8 @@ from flask import request from flask_wtf import FlaskForm from flask import Blueprint +from flask_login import current_user + from graphql_server.flask import GraphQLView from graphene import ObjectType, String, Schema, List from graphene_sqlalchemy import SQLAlchemyObjectType @@ -27,6 +29,7 @@ from app.models.cases import Cases from app.util import is_user_authenticated from app.util import response_error +from app.datamgmt.manage.manage_cases_db import get_filtered_cases class CaseObject(SQLAlchemyObjectType): @@ -43,6 +46,13 @@ class Query(ObjectType): # starting with the conversion of '/manage/cases/list' cases = List(lambda: CaseObject, description='author documentation') + def resolve_cases(self, info): + # TODO missing permissions + # TODO add all parameters to filter + # TODO parameter current_user_id should be mandatory on get_filtered_cases + filtered_cases = get_filtered_cases(current_user_id=current_user.id) + return filtered_cases.items + def resolve_hello(root, info, first_name): return f'Hello {first_name}!' diff --git a/tests/docker_compose.py b/tests/docker_compose.py index dd6223f07..c30d2def1 100644 --- a/tests/docker_compose.py +++ b/tests/docker_compose.py @@ -18,6 +18,8 @@ import subprocess +_DOCKER_COMPOSE = ['docker', 'compose'] + class DockerCompose: @@ -25,10 +27,10 @@ def __init__(self, docker_compose_path): self._docker_compose_path = docker_compose_path def start(self): - subprocess.check_call(['docker', 'compose', 'up', '--detach'], cwd=self._docker_compose_path) + subprocess.check_call(_DOCKER_COMPOSE + ['up', '--detach'], cwd=self._docker_compose_path) def extract_all_logs(self): - return subprocess.check_output(['docker', 'compose', 'logs', '--no-color'], cwd=self._docker_compose_path, universal_newlines=True) + return subprocess.check_output(_DOCKER_COMPOSE + ['logs', '--no-color'], cwd=self._docker_compose_path, universal_newlines=True) def stop(self): - subprocess.check_call(['docker', 'compose', 'down', '--volumes'], cwd=self._docker_compose_path) + subprocess.check_call(_DOCKER_COMPOSE + ['down', '--volumes'], cwd=self._docker_compose_path) diff --git a/tests/tests.py b/tests/tests.py index dbb1049c2..68b96ddce 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -77,4 +77,7 @@ def test_graphql_cases_should_contain_the_initial_case(self): 'query': '{ cases { name } }' } body = self._subject.execute_graphql_query(payload) - # TODO should check the list contains an element with name "#1 - Initial Demo" + case_names = [] + for case in body['data']['cases']: + case_names.append(case['name']) + self.assertIn('#1 - Initial Demo', case_names) From f18e87226cfd3218b9b621f16f7c68787ced7ff3 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Tue, 19 Mar 2024 11:22:17 +0100 Subject: [PATCH 10/29] [IMP] Updated comment --- source/app/blueprints/graphql/graphql_route.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index 087f6f803..540c6f7cc 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -43,7 +43,7 @@ class Query(ObjectType): hello = String(first_name=String(default_value='stranger'), description='Field documentation') goodbye = String() - # starting with the conversion of '/manage/cases/list' + # starting with the conversion of '/manage/cases/filter' cases = List(lambda: CaseObject, description='author documentation') def resolve_cases(self, info): From 13e5694ad8d6296ab685a352e350f9b2299ea12d Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:43:08 +0100 Subject: [PATCH 11/29] [IMP] Moved graphql definition of CaseObject in a dedicated file --- source/app/blueprints/graphql/cases.py | 26 +++++++++++++++++++ .../app/blueprints/graphql/graphql_route.py | 24 ++++------------- 2 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 source/app/blueprints/graphql/cases.py diff --git a/source/app/blueprints/graphql/cases.py b/source/app/blueprints/graphql/cases.py new file mode 100644 index 000000000..dc0c2e075 --- /dev/null +++ b/source/app/blueprints/graphql/cases.py @@ -0,0 +1,26 @@ +# IRIS Source Code +# Copyright (C) 2024 - DFIR-IRIS +# contact@dfir-iris.org +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +from graphene_sqlalchemy import SQLAlchemyObjectType + +from app.models.cases import Cases + + +class CaseObject(SQLAlchemyObjectType): + class Meta: + model = Cases diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index 540c6f7cc..f3a947f7c 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -1,5 +1,5 @@ # IRIS Source Code -# Copyright (C) ${current_year} - DFIR-IRIS +# Copyright (C) 2024 - DFIR-IRIS # contact@dfir-iris.org # # This program is free software; you can redistribute it and/or @@ -24,17 +24,11 @@ from graphql_server.flask import GraphQLView from graphene import ObjectType, String, Schema, List -from graphene_sqlalchemy import SQLAlchemyObjectType -from app.models.cases import Cases from app.util import is_user_authenticated from app.util import response_error from app.datamgmt.manage.manage_cases_db import get_filtered_cases - - -class CaseObject(SQLAlchemyObjectType): - class Meta: - model = Cases +from app.blueprints.graphql.cases import CaseObject class Query(ObjectType): @@ -47,11 +41,9 @@ class Query(ObjectType): cases = List(lambda: CaseObject, description='author documentation') def resolve_cases(self, info): - # TODO missing permissions # TODO add all parameters to filter # TODO parameter current_user_id should be mandatory on get_filtered_cases - filtered_cases = get_filtered_cases(current_user_id=current_user.id) - return filtered_cases.items + return get_filtered_cases(current_user_id=current_user.id) def resolve_hello(root, info, first_name): return f'Hello {first_name}!' @@ -60,10 +52,7 @@ def resolve_goodbye(root, info): return 'See ya!' -# util.ac_api_requires does not seem to work => it leads to error: -# TypeError: dispatch_request() got an unexpected keyword argument 'caseid' -# so I rewrote a simpler decorator... -def _ac_graphql_requires(f): +def _check_authentication_wrapper(f): @wraps(f) def wrap(*args, **kwargs): if request.method == 'POST': @@ -85,7 +74,7 @@ def wrap(*args, **kwargs): def _create_blueprint(): schema = Schema(query=Query) graphql_view = GraphQLView.as_view('graphql', schema=schema) - graphql_view_with_authentication = _ac_graphql_requires(graphql_view) + graphql_view_with_authentication = _check_authentication_wrapper(graphql_view) blueprint = Blueprint('graphql', __name__) blueprint.add_url_rule('/graphql', view_func=graphql_view_with_authentication, methods=['POST']) @@ -95,7 +84,4 @@ def _create_blueprint(): graphql_blueprint = _create_blueprint() -# TODO how to handle permissions? -# TODO link with the database: graphene-sqlalchemy # TODO I am unsure about the code organization (directories) -# TODO define the graphql API and establish the most important needs From 922720aba1a4c26a52833d6b1c274b8995d6f1f3 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:13:53 +0100 Subject: [PATCH 12/29] [IMP] made parameter current_user_id of get_filtered_cases mandatory --- source/app/blueprints/graphql/graphql_route.py | 3 +-- source/app/blueprints/manage/manage_cases_routes.py | 2 +- source/app/datamgmt/manage/manage_cases_db.py | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index f3a947f7c..f01a356cb 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -42,8 +42,7 @@ class Query(ObjectType): def resolve_cases(self, info): # TODO add all parameters to filter - # TODO parameter current_user_id should be mandatory on get_filtered_cases - return get_filtered_cases(current_user_id=current_user.id) + return get_filtered_cases(current_user.id) def resolve_hello(root, info, first_name): return f'Hello {first_name}!' diff --git a/source/app/blueprints/manage/manage_cases_routes.py b/source/app/blueprints/manage/manage_cases_routes.py index e14dd9a1e..f0510f7cd 100644 --- a/source/app/blueprints/manage/manage_cases_routes.py +++ b/source/app/blueprints/manage/manage_cases_routes.py @@ -201,6 +201,7 @@ def manage_case_filter(caseid) -> Response: draw = 1 filtered_cases = get_filtered_cases( + current_user.id, case_ids=case_ids_str, case_customer_id=case_customer_id, case_name=case_name, @@ -216,7 +217,6 @@ def manage_case_filter(caseid) -> Response: search_value=search_value, page=page, per_page=per_page, - current_user_id=current_user.id, sort_by=order_by, sort_dir=sort_dir ) diff --git a/source/app/datamgmt/manage/manage_cases_db.py b/source/app/datamgmt/manage/manage_cases_db.py index a0716baed..87c0c1f03 100644 --- a/source/app/datamgmt/manage/manage_cases_db.py +++ b/source/app/datamgmt/manage/manage_cases_db.py @@ -381,7 +381,8 @@ def delete_case(case_id): return True -def get_filtered_cases(start_open_date: str = None, +def get_filtered_cases(current_user_id, + start_open_date: str = None, end_open_date: str = None, case_customer_id: int = None, case_ids: list = None, @@ -395,7 +396,6 @@ def get_filtered_cases(start_open_date: str = None, case_soc_id: str = None, per_page: int = None, page: int = None, - current_user_id = None, search_value=None, sort_by=None, sort_dir='asc' From e4a749b3342e9189ed903f7624d24e3223fc1225 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:18:06 +0100 Subject: [PATCH 13/29] [DEL] Removed now unnecessary test hello and goodbye graphql queries --- source/app/blueprints/graphql/graphql_route.py | 13 ++----------- tests/tests.py | 9 +-------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index f01a356cb..d6674a249 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -32,24 +32,15 @@ class Query(ObjectType): - """Query documentation""" - - hello = String(first_name=String(default_value='stranger'), description='Field documentation') - goodbye = String() + """This is the IRIS GraphQL queries documentation!""" # starting with the conversion of '/manage/cases/filter' - cases = List(lambda: CaseObject, description='author documentation') + cases = List(lambda: CaseObject, description='Retrieves cases') def resolve_cases(self, info): # TODO add all parameters to filter return get_filtered_cases(current_user.id) - def resolve_hello(root, info, first_name): - return f'Hello {first_name}!' - - def resolve_goodbye(root, info): - return 'See ya!' - def _check_authentication_wrapper(f): @wraps(f) diff --git a/tests/tests.py b/tests/tests.py index 68b96ddce..fecadaaca 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -60,18 +60,11 @@ def test_update_case_should_not_require_case_name_issue_358(self): def test_graphql_endpoint_should_reject_requests_with_wrong_authentication_token(self): graphql_api = GraphQLApi(API_URL + '/graphql', 64*'0') payload = { - 'query': '{ hello(firstName: "friendly") }' + 'query': '{ cases { name } }' } response = graphql_api.execute(payload) self.assertEqual(401, response.status_code) - def test_graphql_endpoint_should_not_fail(self): - payload = { - 'query': '{ hello(firstName: "Paul") }' - } - body = self._subject.execute_graphql_query(payload) - self.assertEqual('Hello Paul!', body['data']['hello']) - def test_graphql_cases_should_contain_the_initial_case(self): payload = { 'query': '{ cases { name } }' From b5d4d8504dcceccda838bf8957ed2df4dc345e01 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:19:07 +0100 Subject: [PATCH 14/29] [DEL] Removed unused import --- source/app/blueprints/graphql/graphql_route.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index d6674a249..f66b4b27e 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -23,7 +23,7 @@ from flask_login import current_user from graphql_server.flask import GraphQLView -from graphene import ObjectType, String, Schema, List +from graphene import ObjectType, Schema, List from app.util import is_user_authenticated from app.util import response_error From 9aed83b5227790af4e4054f034724e8849f7aa5a Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Tue, 19 Mar 2024 15:42:57 +0100 Subject: [PATCH 15/29] [IMP] Starting to separate business layer from REST layer. The business layer will be common to REST and GraphQL and include permission checks --- .../blueprints/manage/manage_cases_routes.py | 12 ++++------ source/app/business/__init__.py | 0 source/app/business/cases.py | 23 +++++++++++++++++++ 3 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 source/app/business/__init__.py create mode 100644 source/app/business/cases.py diff --git a/source/app/blueprints/manage/manage_cases_routes.py b/source/app/blueprints/manage/manage_cases_routes.py index f0510f7cd..4fbdd4ede 100644 --- a/source/app/blueprints/manage/manage_cases_routes.py +++ b/source/app/blueprints/manage/manage_cases_routes.py @@ -49,17 +49,14 @@ from app.datamgmt.manage.manage_case_templates_db import get_case_templates_list, case_template_pre_modifier, \ case_template_post_modifier from app.datamgmt.manage.manage_cases_db import close_case, map_alert_resolution_to_case_status, get_filtered_cases -from app.datamgmt.manage.manage_cases_db import delete_case from app.datamgmt.manage.manage_cases_db import get_case_details_rt from app.datamgmt.manage.manage_cases_db import get_case_protagonists from app.datamgmt.manage.manage_cases_db import list_cases_dict from app.datamgmt.manage.manage_cases_db import reopen_case from app.datamgmt.manage.manage_common import get_severities_list -from app.datamgmt.manage.manage_users_db import get_user_organisations from app.forms import AddCaseForm from app.iris_engine.access_control.utils import ac_fast_check_current_user_has_case_access, \ ac_current_user_has_permission -from app.iris_engine.access_control.utils import ac_fast_check_user_has_case_access from app.iris_engine.access_control.utils import ac_set_new_case_access from app.iris_engine.module_handler.module_handler import call_modules_hook from app.iris_engine.module_handler.module_handler import configure_module_on_init @@ -67,18 +64,17 @@ from app.iris_engine.tasker.tasks import task_case_update from app.iris_engine.utils.common import build_upload_path from app.iris_engine.utils.tracker import track_activity -from app.models.alerts import AlertStatus from app.models.authorization import CaseAccessLevel from app.models.authorization import Permissions -from app.models.models import Client, ReviewStatusList +from app.models.models import ReviewStatusList from app.schema.marshables import CaseSchema, CaseDetailsSchema -from app.util import ac_api_case_requires, add_obj_history_entry +from app.util import add_obj_history_entry from app.util import ac_api_requires from app.util import ac_api_return_access_denied -from app.util import ac_case_requires from app.util import ac_requires from app.util import response_error from app.util import response_success +from app.business.cases import delete manage_cases_blueprint = Blueprint('manage_case', __name__, @@ -250,7 +246,7 @@ def api_delete_case(cur_id, caseid): else: try: call_modules_hook('on_preload_case_delete', data=cur_id, caseid=caseid) - if delete_case(case_id=cur_id): + if delete(cur_id): call_modules_hook('on_postload_case_delete', data=cur_id, caseid=caseid) diff --git a/source/app/business/__init__.py b/source/app/business/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/source/app/business/cases.py b/source/app/business/cases.py new file mode 100644 index 000000000..df810d9e3 --- /dev/null +++ b/source/app/business/cases.py @@ -0,0 +1,23 @@ +# IRIS Source Code +# Copyright (C) 2024 - DFIR-IRIS +# contact@dfir-iris.org +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +from app.datamgmt.manage.manage_cases_db import delete_case + + +def delete(identifier): + return delete_case(identifier) From 8afb5239cfd56c8676a7f91d799c878a6f911146 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Tue, 19 Mar 2024 16:04:29 +0100 Subject: [PATCH 16/29] [IMP] Moved some more code down into the business layer --- .../blueprints/manage/manage_cases_routes.py | 18 +++++----------- .../app/business/business_processing_error.py | 21 +++++++++++++++++++ source/app/business/cases.py | 17 +++++++++++++-- 3 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 source/app/business/business_processing_error.py diff --git a/source/app/blueprints/manage/manage_cases_routes.py b/source/app/blueprints/manage/manage_cases_routes.py index 4fbdd4ede..9d0d0368f 100644 --- a/source/app/blueprints/manage/manage_cases_routes.py +++ b/source/app/blueprints/manage/manage_cases_routes.py @@ -75,6 +75,7 @@ from app.util import response_error from app.util import response_success from app.business.cases import delete +from app.business.business_processing_error import BusinessProcessingError manage_cases_blueprint = Blueprint('manage_case', __name__, @@ -245,20 +246,11 @@ def api_delete_case(cur_id, caseid): else: try: - call_modules_hook('on_preload_case_delete', data=cur_id, caseid=caseid) - if delete(cur_id): - - call_modules_hook('on_postload_case_delete', data=cur_id, caseid=caseid) - - track_activity("case {} deleted successfully".format(cur_id), ctx_less=True) + try: + delete(cur_id, caseid) return response_success("Case successfully deleted") - - else: - track_activity("tried to delete case {}, but it doesn't exist".format(cur_id), - caseid=caseid, ctx_less=True) - - return response_error("Tried to delete a non-existing case") - + except BusinessProcessingError as e: + return response_error(str(e)) except Exception as e: app.app.logger.exception(e) return response_error("Cannot delete the case. Please check server logs for additional informations") diff --git a/source/app/business/business_processing_error.py b/source/app/business/business_processing_error.py new file mode 100644 index 000000000..94304d494 --- /dev/null +++ b/source/app/business/business_processing_error.py @@ -0,0 +1,21 @@ +# IRIS Source Code +# Copyright (C) 2024 - DFIR-IRIS +# contact@dfir-iris.org +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +class BusinessProcessingError(Exception): + pass diff --git a/source/app/business/cases.py b/source/app/business/cases.py index df810d9e3..ec8bc3f25 100644 --- a/source/app/business/cases.py +++ b/source/app/business/cases.py @@ -16,8 +16,21 @@ # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +from app.iris_engine.module_handler.module_handler import call_modules_hook +from app.iris_engine.utils.tracker import track_activity from app.datamgmt.manage.manage_cases_db import delete_case +from app.business.business_processing_error import BusinessProcessingError + + +def delete(identifier, context_case_identifier): + call_modules_hook('on_preload_case_delete', data=identifier, caseid=context_case_identifier) + if not delete_case(identifier): + track_activity(f'tried to delete case {identifier}, but it doesn\'t exist', + caseid=context_case_identifier, ctx_less=True) + raise BusinessProcessingError('Tried to delete a non-existing case') + call_modules_hook('on_postload_case_delete', data=identifier, caseid=context_case_identifier) + track_activity(f'case {identifier} deleted successfully', ctx_less=True) + + -def delete(identifier): - return delete_case(identifier) From b8d6268f1b4e7a6a5b37a154f857126dbd4cb6b9 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Tue, 19 Mar 2024 16:11:27 +0100 Subject: [PATCH 17/29] [IMP] Moved some more code down into the business layer --- .../blueprints/manage/manage_cases_routes.py | 22 ++++-------------- source/app/business/cases.py | 23 ++++++++++++------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/source/app/blueprints/manage/manage_cases_routes.py b/source/app/blueprints/manage/manage_cases_routes.py index 9d0d0368f..0006b9cba 100644 --- a/source/app/blueprints/manage/manage_cases_routes.py +++ b/source/app/blueprints/manage/manage_cases_routes.py @@ -33,7 +33,6 @@ from werkzeug import Response from werkzeug.utils import secure_filename -import app from app import db from app.datamgmt.alerts.alerts_db import get_alert_status_by_name from app.datamgmt.case.case_db import get_case, get_review_id_from_name @@ -238,22 +237,11 @@ def api_delete_case(cur_id, caseid): if not ac_fast_check_current_user_has_case_access(cur_id, [CaseAccessLevel.full_access]): return ac_api_return_access_denied(caseid=cur_id) - if cur_id == 1: - track_activity("tried to delete case {}, but case is the primary case".format(cur_id), - caseid=caseid, ctx_less=True) - - return response_error("Cannot delete a primary case to keep consistency") - - else: - try: - try: - delete(cur_id, caseid) - return response_success("Case successfully deleted") - except BusinessProcessingError as e: - return response_error(str(e)) - except Exception as e: - app.app.logger.exception(e) - return response_error("Cannot delete the case. Please check server logs for additional informations") + try: + delete(cur_id, caseid) + return response_success('Case successfully deleted') + except BusinessProcessingError as e: + return response_error(str(e)) @manage_cases_blueprint.route('/manage/cases/reopen/', methods=['POST']) diff --git a/source/app/business/cases.py b/source/app/business/cases.py index ec8bc3f25..c68903480 100644 --- a/source/app/business/cases.py +++ b/source/app/business/cases.py @@ -16,6 +16,7 @@ # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +from app import app from app.iris_engine.module_handler.module_handler import call_modules_hook from app.iris_engine.utils.tracker import track_activity from app.datamgmt.manage.manage_cases_db import delete_case @@ -23,14 +24,20 @@ def delete(identifier, context_case_identifier): - call_modules_hook('on_preload_case_delete', data=identifier, caseid=context_case_identifier) - if not delete_case(identifier): - track_activity(f'tried to delete case {identifier}, but it doesn\'t exist', + if identifier == 1: + track_activity(f'tried to delete case {identifier}, but case is the primary case', caseid=context_case_identifier, ctx_less=True) - raise BusinessProcessingError('Tried to delete a non-existing case') - call_modules_hook('on_postload_case_delete', data=identifier, caseid=context_case_identifier) - track_activity(f'case {identifier} deleted successfully', ctx_less=True) - - + raise BusinessProcessingError('Cannot delete a primary case to keep consistency') + try: + call_modules_hook('on_preload_case_delete', data=identifier, caseid=context_case_identifier) + if not delete_case(identifier): + track_activity(f'tried to delete case {identifier}, but it doesn\'t exist', + caseid=context_case_identifier, ctx_less=True) + raise BusinessProcessingError('Tried to delete a non-existing case') + call_modules_hook('on_postload_case_delete', data=identifier, caseid=context_case_identifier) + track_activity(f'case {identifier} deleted successfully', ctx_less=True) + except Exception as e: + app.logger.exception(e) + raise BusinessProcessingError("Cannot delete the case. Please check server logs for additional informations") From 95386f738020492e10d0de044523b992bce6d83b Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Tue, 19 Mar 2024 16:18:08 +0100 Subject: [PATCH 18/29] [IMP] Moved a part of permissions checks down into the business layer for /manage/cases/delete --- source/app/blueprints/manage/manage_cases_routes.py | 12 ++++++------ source/app/business/cases.py | 10 ++++++++-- .../{business_processing_error.py => errors.py} | 4 ++++ 3 files changed, 18 insertions(+), 8 deletions(-) rename source/app/business/{business_processing_error.py => errors.py} (94%) diff --git a/source/app/blueprints/manage/manage_cases_routes.py b/source/app/blueprints/manage/manage_cases_routes.py index 0006b9cba..374aaa032 100644 --- a/source/app/blueprints/manage/manage_cases_routes.py +++ b/source/app/blueprints/manage/manage_cases_routes.py @@ -54,8 +54,8 @@ from app.datamgmt.manage.manage_cases_db import reopen_case from app.datamgmt.manage.manage_common import get_severities_list from app.forms import AddCaseForm -from app.iris_engine.access_control.utils import ac_fast_check_current_user_has_case_access, \ - ac_current_user_has_permission +from app.iris_engine.access_control.utils import ac_fast_check_current_user_has_case_access +from app.iris_engine.access_control.utils import ac_current_user_has_permission from app.iris_engine.access_control.utils import ac_set_new_case_access from app.iris_engine.module_handler.module_handler import call_modules_hook from app.iris_engine.module_handler.module_handler import configure_module_on_init @@ -74,7 +74,8 @@ from app.util import response_error from app.util import response_success from app.business.cases import delete -from app.business.business_processing_error import BusinessProcessingError +from app.business.errors import BusinessProcessingError +from app.business.errors import PermissionDenied manage_cases_blueprint = Blueprint('manage_case', __name__, @@ -234,14 +235,13 @@ def manage_case_filter(caseid) -> Response: @manage_cases_blueprint.route('/manage/cases/delete/', methods=['POST']) @ac_api_requires(Permissions.standard_user, no_cid_required=True) def api_delete_case(cur_id, caseid): - if not ac_fast_check_current_user_has_case_access(cur_id, [CaseAccessLevel.full_access]): - return ac_api_return_access_denied(caseid=cur_id) - try: delete(cur_id, caseid) return response_success('Case successfully deleted') except BusinessProcessingError as e: return response_error(str(e)) + except PermissionDenied: + return ac_api_return_access_denied(caseid=cur_id) @manage_cases_blueprint.route('/manage/cases/reopen/', methods=['POST']) diff --git a/source/app/business/cases.py b/source/app/business/cases.py index c68903480..7f203bf4e 100644 --- a/source/app/business/cases.py +++ b/source/app/business/cases.py @@ -17,13 +17,19 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from app import app +from app.models.authorization import CaseAccessLevel +from app.iris_engine.access_control.utils import ac_fast_check_current_user_has_case_access from app.iris_engine.module_handler.module_handler import call_modules_hook from app.iris_engine.utils.tracker import track_activity from app.datamgmt.manage.manage_cases_db import delete_case -from app.business.business_processing_error import BusinessProcessingError +from app.business.errors import BusinessProcessingError +from app.business.errors import PermissionDenied def delete(identifier, context_case_identifier): + if not ac_fast_check_current_user_has_case_access(identifier, [CaseAccessLevel.full_access]): + raise PermissionDenied() + if identifier == 1: track_activity(f'tried to delete case {identifier}, but case is the primary case', caseid=context_case_identifier, ctx_less=True) @@ -40,4 +46,4 @@ def delete(identifier, context_case_identifier): track_activity(f'case {identifier} deleted successfully', ctx_less=True) except Exception as e: app.logger.exception(e) - raise BusinessProcessingError("Cannot delete the case. Please check server logs for additional informations") + raise BusinessProcessingError('Cannot delete the case. Please check server logs for additional informations') diff --git a/source/app/business/business_processing_error.py b/source/app/business/errors.py similarity index 94% rename from source/app/business/business_processing_error.py rename to source/app/business/errors.py index 94304d494..f1c03576c 100644 --- a/source/app/business/business_processing_error.py +++ b/source/app/business/errors.py @@ -19,3 +19,7 @@ class BusinessProcessingError(Exception): pass + + +class PermissionDenied(Exception): + pass From fab8d0d527a6aecd80a493eda98dc41e2ff5e823 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Wed, 20 Mar 2024 09:11:21 +0100 Subject: [PATCH 19/29] [IMP] Renamed exception PermissionDenied into PermissionDeniedError --- source/app/blueprints/manage/manage_cases_routes.py | 4 ++-- source/app/business/cases.py | 4 ++-- source/app/business/errors.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/source/app/blueprints/manage/manage_cases_routes.py b/source/app/blueprints/manage/manage_cases_routes.py index 374aaa032..53944ab43 100644 --- a/source/app/blueprints/manage/manage_cases_routes.py +++ b/source/app/blueprints/manage/manage_cases_routes.py @@ -75,7 +75,7 @@ from app.util import response_success from app.business.cases import delete from app.business.errors import BusinessProcessingError -from app.business.errors import PermissionDenied +from app.business.errors import PermissionDeniedError manage_cases_blueprint = Blueprint('manage_case', __name__, @@ -240,7 +240,7 @@ def api_delete_case(cur_id, caseid): return response_success('Case successfully deleted') except BusinessProcessingError as e: return response_error(str(e)) - except PermissionDenied: + except PermissionDeniedError: return ac_api_return_access_denied(caseid=cur_id) diff --git a/source/app/business/cases.py b/source/app/business/cases.py index 7f203bf4e..1d8383253 100644 --- a/source/app/business/cases.py +++ b/source/app/business/cases.py @@ -23,12 +23,12 @@ from app.iris_engine.utils.tracker import track_activity from app.datamgmt.manage.manage_cases_db import delete_case from app.business.errors import BusinessProcessingError -from app.business.errors import PermissionDenied +from app.business.errors import PermissionDeniedError def delete(identifier, context_case_identifier): if not ac_fast_check_current_user_has_case_access(identifier, [CaseAccessLevel.full_access]): - raise PermissionDenied() + raise PermissionDeniedError() if identifier == 1: track_activity(f'tried to delete case {identifier}, but case is the primary case', diff --git a/source/app/business/errors.py b/source/app/business/errors.py index f1c03576c..711d63f0c 100644 --- a/source/app/business/errors.py +++ b/source/app/business/errors.py @@ -21,5 +21,5 @@ class BusinessProcessingError(Exception): pass -class PermissionDenied(Exception): +class PermissionDeniedError(Exception): pass From 3e0f500ff4b1757f251f6786b7ed72922f8902d9 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Wed, 20 Mar 2024 09:23:45 +0100 Subject: [PATCH 20/29] [IMP] Introduced permissions file to group code related to the business layer permission checks --- source/app/business/cases.py | 22 ++++++++++------------ source/app/business/permissions.py | 25 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 source/app/business/permissions.py diff --git a/source/app/business/cases.py b/source/app/business/cases.py index 1d8383253..cb9dfdd79 100644 --- a/source/app/business/cases.py +++ b/source/app/business/cases.py @@ -18,32 +18,30 @@ from app import app from app.models.authorization import CaseAccessLevel -from app.iris_engine.access_control.utils import ac_fast_check_current_user_has_case_access from app.iris_engine.module_handler.module_handler import call_modules_hook from app.iris_engine.utils.tracker import track_activity from app.datamgmt.manage.manage_cases_db import delete_case from app.business.errors import BusinessProcessingError -from app.business.errors import PermissionDeniedError +from app.business.permissions import check_current_user_has_some_case_access -def delete(identifier, context_case_identifier): - if not ac_fast_check_current_user_has_case_access(identifier, [CaseAccessLevel.full_access]): - raise PermissionDeniedError() +def delete(case_identifier, context_case_identifier): + check_current_user_has_some_case_access(case_identifier, [CaseAccessLevel.full_access]) - if identifier == 1: - track_activity(f'tried to delete case {identifier}, but case is the primary case', + if case_identifier == 1: + track_activity(f'tried to delete case {case_identifier}, but case is the primary case', caseid=context_case_identifier, ctx_less=True) raise BusinessProcessingError('Cannot delete a primary case to keep consistency') try: - call_modules_hook('on_preload_case_delete', data=identifier, caseid=context_case_identifier) - if not delete_case(identifier): - track_activity(f'tried to delete case {identifier}, but it doesn\'t exist', + call_modules_hook('on_preload_case_delete', data=case_identifier, caseid=context_case_identifier) + if not delete_case(case_identifier): + track_activity(f'tried to delete case {case_identifier}, but it doesn\'t exist', caseid=context_case_identifier, ctx_less=True) raise BusinessProcessingError('Tried to delete a non-existing case') - call_modules_hook('on_postload_case_delete', data=identifier, caseid=context_case_identifier) - track_activity(f'case {identifier} deleted successfully', ctx_less=True) + call_modules_hook('on_postload_case_delete', data=case_identifier, caseid=context_case_identifier) + track_activity(f'case {case_identifier} deleted successfully', ctx_less=True) except Exception as e: app.logger.exception(e) raise BusinessProcessingError('Cannot delete the case. Please check server logs for additional informations') diff --git a/source/app/business/permissions.py b/source/app/business/permissions.py new file mode 100644 index 000000000..29efad29b --- /dev/null +++ b/source/app/business/permissions.py @@ -0,0 +1,25 @@ +# IRIS Source Code +# Copyright (C) 2024 - DFIR-IRIS +# contact@dfir-iris.org +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +from app.iris_engine.access_control.utils import ac_fast_check_current_user_has_case_access +from app.business.errors import PermissionDeniedError + + +def check_current_user_has_some_case_access(case_identifier, access_levels): + if not ac_fast_check_current_user_has_case_access(case_identifier, access_levels): + raise PermissionDeniedError() From be82f2cb50b65f70872d87b0a779bf8f83256ed3 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Wed, 20 Mar 2024 09:46:26 +0100 Subject: [PATCH 21/29] [IMP] Introduced permission check in the business layer --- source/app/business/cases.py | 3 +++ source/app/business/permissions.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/source/app/business/cases.py b/source/app/business/cases.py index cb9dfdd79..9e5f0a230 100644 --- a/source/app/business/cases.py +++ b/source/app/business/cases.py @@ -18,14 +18,17 @@ from app import app from app.models.authorization import CaseAccessLevel +from app.models.authorization import Permissions from app.iris_engine.module_handler.module_handler import call_modules_hook from app.iris_engine.utils.tracker import track_activity from app.datamgmt.manage.manage_cases_db import delete_case from app.business.errors import BusinessProcessingError from app.business.permissions import check_current_user_has_some_case_access +from app.business.permissions import check_current_user_has_some_permission def delete(case_identifier, context_case_identifier): + check_current_user_has_some_permission([Permissions.standard_user]) check_current_user_has_some_case_access(case_identifier, [CaseAccessLevel.full_access]) if case_identifier == 1: diff --git a/source/app/business/permissions.py b/source/app/business/permissions.py index 29efad29b..4ac199329 100644 --- a/source/app/business/permissions.py +++ b/source/app/business/permissions.py @@ -16,6 +16,11 @@ # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +from flask import session +from flask_login import current_user + +from app.models.authorization import Permissions +from app.iris_engine.access_control.utils import ac_get_effective_permissions_of_user from app.iris_engine.access_control.utils import ac_fast_check_current_user_has_case_access from app.business.errors import PermissionDeniedError @@ -23,3 +28,14 @@ def check_current_user_has_some_case_access(case_identifier, access_levels): if not ac_fast_check_current_user_has_case_access(case_identifier, access_levels): raise PermissionDeniedError() + + +def check_current_user_has_some_permission(permissions): + if 'permissions' not in session: + session['permissions'] = ac_get_effective_permissions_of_user(current_user) + + for permission in permissions: + if session['permissions'] & permission.value: + return + + raise PermissionDeniedError() From 447e236bac98a0b47f32bc3baccb814922c5d450 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Wed, 20 Mar 2024 13:26:28 +0100 Subject: [PATCH 22/29] [IMP] Changed self to root as the first argument of a resolver in graphene is the parent object (the root) --- source/app/blueprints/graphql/graphql_route.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index f66b4b27e..02fc1d643 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -37,7 +37,7 @@ class Query(ObjectType): # starting with the conversion of '/manage/cases/filter' cases = List(lambda: CaseObject, description='Retrieves cases') - def resolve_cases(self, info): + def resolve_cases(root, info): # TODO add all parameters to filter return get_filtered_cases(current_user.id) From a73f9e5e7eb30365c4fe1ed4a0920100a0231c2b Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Wed, 20 Mar 2024 15:44:04 +0100 Subject: [PATCH 23/29] [IMP] Seems lambda is not necessary --- source/app/blueprints/graphql/graphql_route.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index 02fc1d643..6346231f9 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -35,7 +35,7 @@ class Query(ObjectType): """This is the IRIS GraphQL queries documentation!""" # starting with the conversion of '/manage/cases/filter' - cases = List(lambda: CaseObject, description='Retrieves cases') + cases = List(CaseObject, description='Retrieves cases') def resolve_cases(root, info): # TODO add all parameters to filter From f2b0e2dfac804d3dd63ea978fef569fd5d65ec0f Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 22 Mar 2024 08:29:45 +0100 Subject: [PATCH 24/29] [IMP] One import per line --- source/app/blueprints/graphql/graphql_route.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index 6346231f9..84d9cb8a0 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -23,7 +23,9 @@ from flask_login import current_user from graphql_server.flask import GraphQLView -from graphene import ObjectType, Schema, List +from graphene import ObjectType +from graphene import Schema +from graphene import List from app.util import is_user_authenticated from app.util import response_error From 1cc334901b5a8e921341db4ea9758b189048ce8d Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 22 Mar 2024 09:18:26 +0100 Subject: [PATCH 25/29] [ADD] global identifier to CaseObject --- source/app/blueprints/graphql/cases.py | 2 ++ tests/tests.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/source/app/blueprints/graphql/cases.py b/source/app/blueprints/graphql/cases.py index dc0c2e075..d7d4a7064 100644 --- a/source/app/blueprints/graphql/cases.py +++ b/source/app/blueprints/graphql/cases.py @@ -17,6 +17,7 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from graphene_sqlalchemy import SQLAlchemyObjectType +from graphene.relay import Node from app.models.cases import Cases @@ -24,3 +25,4 @@ class CaseObject(SQLAlchemyObjectType): class Meta: model = Cases + interfaces = [Node] diff --git a/tests/tests.py b/tests/tests.py index fecadaaca..e1d2cadc6 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -20,6 +20,7 @@ from iris import Iris from iris import API_URL from graphql_api import GraphQLApi +from base64 import b64encode class Tests(TestCase): @@ -74,3 +75,14 @@ def test_graphql_cases_should_contain_the_initial_case(self): for case in body['data']['cases']: case_names.append(case['name']) self.assertIn('#1 - Initial Demo', case_names) + + def test_graphql_cases_should_have_a_global_identifier(self): + payload = { + 'query': '{ cases { id name } }' + } + body = self._subject.execute_graphql_query(payload) + first_case = None + for case in body['data']['cases']: + if case['name'] == '#1 - Initial Demo': + first_case = case + self.assertEqual(b64encode(b'CaseObject:1').decode(), first_case['id']) From c69457ae8bf599f5e97f80dd6b85ad9c5c3c0a23 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:44:01 +0100 Subject: [PATCH 26/29] [IMP] use annotation @staticmethod, better for the IDE --- .../app/blueprints/graphql/graphql_route.py | 1 + source/app/blueprints/graphql/iocs.py | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 source/app/blueprints/graphql/iocs.py diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index 84d9cb8a0..ce126991c 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -39,6 +39,7 @@ class Query(ObjectType): # starting with the conversion of '/manage/cases/filter' cases = List(CaseObject, description='Retrieves cases') + @staticmethod def resolve_cases(root, info): # TODO add all parameters to filter return get_filtered_cases(current_user.id) diff --git a/source/app/blueprints/graphql/iocs.py b/source/app/blueprints/graphql/iocs.py new file mode 100644 index 000000000..dc95c5b63 --- /dev/null +++ b/source/app/blueprints/graphql/iocs.py @@ -0,0 +1,51 @@ +# IRIS Source Code +# Copyright (C) 2024 - DFIR-IRIS +# contact@dfir-iris.org +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +from graphene_sqlalchemy import SQLAlchemyObjectType +from graphene import Mutation +from graphene import ID +from graphene import NonNull + +from app.models.models import Ioc + + +class IocObject(SQLAlchemyObjectType): + class Meta: + model = Ioc + + +class AddIoc(Mutation): + + class Arguments: + # note: I prefer NonNull rather than the syntax required=True + caseId: NonNull(ID) + #typeId: 1 + #tlpId: 1 + #value: "8.8.8.8" + #description: "some description" + #tags: + + @staticmethod + def mutate(root, info, title, description, year, username): + author = Author.query.filter_by(username=username).first() + book = Book(title=title, description=description, year=year) + if author is not None: + book.author = author + db.session.add(book) + db.session.commit() + return AddBook(book=book) \ No newline at end of file From 89e375480587943431824f652acb669e61a8a854 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 22 Mar 2024 14:05:08 +0100 Subject: [PATCH 27/29] [IMP] Removed unused import --- source/app/business/permissions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/source/app/business/permissions.py b/source/app/business/permissions.py index 4ac199329..b7fe5f42a 100644 --- a/source/app/business/permissions.py +++ b/source/app/business/permissions.py @@ -19,7 +19,6 @@ from flask import session from flask_login import current_user -from app.models.authorization import Permissions from app.iris_engine.access_control.utils import ac_get_effective_permissions_of_user from app.iris_engine.access_control.utils import ac_fast_check_current_user_has_case_access from app.business.errors import PermissionDeniedError From c29e2474a3c8c557c3ca2116f0a40472086cdabd Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:06:57 +0100 Subject: [PATCH 28/29] [IMP] Moved code to create an ioc down into the business layer --- source/app/blueprints/case/case_ioc_routes.py | 41 +++-------- .../blueprints/manage/manage_cases_routes.py | 2 +- source/app/business/errors.py | 11 ++- source/app/business/iocs.py | 70 +++++++++++++++++++ source/app/business/permissions.py | 15 ++++ 5 files changed, 104 insertions(+), 35 deletions(-) create mode 100644 source/app/business/iocs.py diff --git a/source/app/blueprints/case/case_ioc_routes.py b/source/app/blueprints/case/case_ioc_routes.py index d2ee4cb7f..58318ce7d 100644 --- a/source/app/blueprints/case/case_ioc_routes.py +++ b/source/app/blueprints/case/case_ioc_routes.py @@ -64,6 +64,8 @@ from app.util import ac_case_requires from app.util import response_error from app.util import response_success +from app.business.iocs import create +from app.business.errors import BusinessProcessingError case_ioc_blueprint = Blueprint( 'case_ioc', @@ -126,40 +128,13 @@ def case_ioc_state(caseid): @case_ioc_blueprint.route('/case/ioc/add', methods=['POST']) @ac_api_case_requires(CaseAccessLevel.full_access) def case_add_ioc(caseid): - try: - # validate before saving - add_ioc_schema = IocSchema() - - request_data = call_modules_hook('on_preload_ioc_create', data=request.get_json(), caseid=caseid) - - ioc = add_ioc_schema.load(request_data) - - if not check_ioc_type_id(type_id=ioc.ioc_type_id): - return response_error("Not a valid IOC type") - - ioc, existed = add_ioc(ioc=ioc, - user_id=current_user.id, - caseid=caseid - ) - link_existed = add_ioc_link(ioc.ioc_id, caseid) - - if link_existed: - return response_success("IOC already exists and linked to this case", data=add_ioc_schema.dump(ioc)) + add_ioc_schema = IocSchema() - if not link_existed: - ioc = call_modules_hook('on_postload_ioc_create', data=ioc, caseid=caseid) - - if ioc: - track_activity("added ioc \"{}\"".format(ioc.ioc_value), caseid=caseid) - - msg = "IOC already existed in DB. Updated with info on DB." if existed else "IOC added" - - return response_success(msg=msg, data=add_ioc_schema.dump(ioc)) - - return response_error("Unable to create IOC for internal reasons") - - except marshmallow.exceptions.ValidationError as e: - return response_error(msg="Data error", data=e.messages, status=400) + try: + ioc, msg = create(request.get_json(), caseid) + return response_success(msg, data=add_ioc_schema.dump(ioc)) + except BusinessProcessingError as e: + return response_error(e.get_message(), data=e.get_data()) @case_ioc_blueprint.route('/case/ioc/upload', methods=['POST']) diff --git a/source/app/blueprints/manage/manage_cases_routes.py b/source/app/blueprints/manage/manage_cases_routes.py index 53944ab43..c94c3e398 100644 --- a/source/app/blueprints/manage/manage_cases_routes.py +++ b/source/app/blueprints/manage/manage_cases_routes.py @@ -239,7 +239,7 @@ def api_delete_case(cur_id, caseid): delete(cur_id, caseid) return response_success('Case successfully deleted') except BusinessProcessingError as e: - return response_error(str(e)) + return response_error(e.get_message()) except PermissionDeniedError: return ac_api_return_access_denied(caseid=cur_id) diff --git a/source/app/business/errors.py b/source/app/business/errors.py index 711d63f0c..15efb8158 100644 --- a/source/app/business/errors.py +++ b/source/app/business/errors.py @@ -18,7 +18,16 @@ class BusinessProcessingError(Exception): - pass + + def __init__(self, message, data=None): + self._message = message + self._data = data + + def get_message(self): + return self._message + + def get_data(self): + return self._data class PermissionDeniedError(Exception): diff --git a/source/app/business/iocs.py b/source/app/business/iocs.py new file mode 100644 index 000000000..98d763482 --- /dev/null +++ b/source/app/business/iocs.py @@ -0,0 +1,70 @@ +# IRIS Source Code +# Copyright (C) 2024 - DFIR-IRIS +# contact@dfir-iris.org +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +from flask_login import current_user +from marshmallow.exceptions import ValidationError + +from app.models.authorization import CaseAccessLevel +from app.datamgmt.case.case_iocs_db import add_ioc +from app.datamgmt.case.case_iocs_db import add_ioc_link +from app.datamgmt.case.case_iocs_db import check_ioc_type_id +from app.schema.marshables import IocSchema +from app.iris_engine.module_handler.module_handler import call_modules_hook +from app.iris_engine.utils.tracker import track_activity +from app.business.errors import BusinessProcessingError +from app.business.permissions import check_current_user_has_some_case_access_stricter + + +def _load(request_data): + try: + add_ioc_schema = IocSchema() + return add_ioc_schema.load(request_data) + except ValidationError as e: + raise BusinessProcessingError('Data error', e.messages) + + +def create(request_json, case_identifier): + check_current_user_has_some_case_access_stricter([CaseAccessLevel.full_access]) + + # TODO ideally schema validation should be done before, outside the business logic in the REST API + # for that the hook should be called after schema validation + request_data = call_modules_hook('on_preload_ioc_create', data=request_json, caseid=case_identifier) + ioc = _load(request_data) + + if not check_ioc_type_id(type_id=ioc.ioc_type_id): + raise BusinessProcessingError('Not a valid IOC type') + + ioc, existed = add_ioc(ioc=ioc, user_id=current_user.id, caseid=case_identifier) + + link_existed = add_ioc_link(ioc.ioc_id, case_identifier) + + if link_existed: + # note: I am no big fan of returning tuples. + # It is a code smell some type is missing, or the code is badly designed. + return ioc, 'IOC already exists and linked to this case' + + if not link_existed: + ioc = call_modules_hook('on_postload_ioc_create', data=ioc, caseid=case_identifier) + + if ioc: + track_activity(f'added ioc "{ioc.ioc_value}"', caseid=case_identifier) + + msg = "IOC already existed in DB. Updated with info on DB." if existed else "IOC added" + return ioc, msg + + raise BusinessProcessingError('Unable to create IOC for internal reasons') diff --git a/source/app/business/permissions.py b/source/app/business/permissions.py index b7fe5f42a..651ab2f71 100644 --- a/source/app/business/permissions.py +++ b/source/app/business/permissions.py @@ -18,7 +18,9 @@ from flask import session from flask_login import current_user +from flask import request +from app.util import get_case_access from app.iris_engine.access_control.utils import ac_get_effective_permissions_of_user from app.iris_engine.access_control.utils import ac_fast_check_current_user_has_case_access from app.business.errors import PermissionDeniedError @@ -29,6 +31,19 @@ def check_current_user_has_some_case_access(case_identifier, access_levels): raise PermissionDeniedError() +# TODO: really this and the previous method should be merged. +# This one comes from ac_api_case_requires, whereas the other one comes from the way api_delete_case was written... +def check_current_user_has_some_case_access_stricter(access_levels): + redir, caseid, has_access = get_case_access(request, access_levels, from_api=True) + + # TODO: do we really want to keep the details of the errors, when permission is denied => more work, more complex code? + if not caseid or redir: + raise PermissionDeniedError() + + if not has_access: + raise PermissionDeniedError() + + def check_current_user_has_some_permission(permissions): if 'permissions' not in session: session['permissions'] = ac_get_effective_permissions_of_user(current_user) From 6f558eb553f13720b075f6a325c8b966fd29d3b6 Mon Sep 17 00:00:00 2001 From: c8y3 <25362953+c8y3@users.noreply.github.com> Date: Fri, 22 Mar 2024 16:32:10 +0100 Subject: [PATCH 29/29] [ADD] First mutation to add an ioc --- .../app/blueprints/graphql/graphql_route.py | 7 +++- source/app/blueprints/graphql/iocs.py | 39 ++++++++++++------- tests/tests.py | 29 ++++++++++++-- 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index ce126991c..98bf03366 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -31,6 +31,7 @@ from app.util import response_error from app.datamgmt.manage.manage_cases_db import get_filtered_cases from app.blueprints.graphql.cases import CaseObject +from app.blueprints.graphql.iocs import AddIoc class Query(ObjectType): @@ -45,6 +46,10 @@ def resolve_cases(root, info): return get_filtered_cases(current_user.id) +class Mutation(ObjectType): + create_ioc = AddIoc.Field() + + def _check_authentication_wrapper(f): @wraps(f) def wrap(*args, **kwargs): @@ -65,7 +70,7 @@ def wrap(*args, **kwargs): def _create_blueprint(): - schema = Schema(query=Query) + schema = Schema(query=Query, mutation=Mutation) graphql_view = GraphQLView.as_view('graphql', schema=schema) graphql_view_with_authentication = _check_authentication_wrapper(graphql_view) diff --git a/source/app/blueprints/graphql/iocs.py b/source/app/blueprints/graphql/iocs.py index dc95c5b63..4f1aeb9cd 100644 --- a/source/app/blueprints/graphql/iocs.py +++ b/source/app/blueprints/graphql/iocs.py @@ -17,11 +17,14 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from graphene_sqlalchemy import SQLAlchemyObjectType +from graphene import Field from graphene import Mutation -from graphene import ID from graphene import NonNull +from graphene import Int +from graphene import String from app.models.models import Ioc +from app.business.iocs import create class IocObject(SQLAlchemyObjectType): @@ -32,20 +35,26 @@ class Meta: class AddIoc(Mutation): class Arguments: + # TODO: it seems really too difficult to work with IDs. + # I don't understand why graphql_relay.from_global_id does not seem to work... # note: I prefer NonNull rather than the syntax required=True - caseId: NonNull(ID) - #typeId: 1 - #tlpId: 1 - #value: "8.8.8.8" - #description: "some description" - #tags: + # TODO: Integers in graphql are only 32 bits. => will this be a problem? Should we use either float or string? + case_id = NonNull(Int) + type_id = NonNull(Int) + tlp_id = NonNull(Int) + value = NonNull(String) + # TODO add these non mandatory arguments + #description = + #tags = + + ioc = Field(IocObject) @staticmethod - def mutate(root, info, title, description, year, username): - author = Author.query.filter_by(username=username).first() - book = Book(title=title, description=description, year=year) - if author is not None: - book.author = author - db.session.add(book) - db.session.commit() - return AddBook(book=book) \ No newline at end of file + def mutate(root, info, case_id, type_id, tlp_id, value): + request = { + 'ioc_type_id': type_id, + 'ioc_tlp_id': tlp_id, + 'ioc_value': value + } + ioc, _ = create(request, case_id) + return AddIoc(ioc=ioc) diff --git a/tests/tests.py b/tests/tests.py index e1d2cadc6..66e6ed971 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -81,8 +81,31 @@ def test_graphql_cases_should_have_a_global_identifier(self): 'query': '{ cases { id name } }' } body = self._subject.execute_graphql_query(payload) - first_case = None + first_case = self._get_first_case(body) + self.assertEqual(b64encode(b'CaseObject:1').decode(), first_case['id']) + + def test_graphql_create_ioc_should_not_fail(self): + payload = { + 'query': f'''mutation {{ + createIoc(caseId: 1, typeId: 1, tlpId: 1, value: "8.8.8.8", + description: "some description", tags: "") {{ + ioc {{ iocValue }} + }} + }}''' + } + payload = { + 'query': f'''mutation {{ + createIoc(caseId: 1, typeId: 1, tlpId: 1, value: "8.8.8.8") {{ + ioc {{ iocValue }} + }} + }}''' + } + body = self._subject.execute_graphql_query(payload) + self.assertNotIn('errors', body) + + def _get_first_case(self, body): for case in body['data']['cases']: if case['name'] == '#1 - Initial Demo': - first_case = case - self.assertEqual(b64encode(b'CaseObject:1').decode(), first_case['id']) + return case + +# TODO: should maybe try to use gql \ No newline at end of file