Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue415 graph ql first steps #444

Merged
merged 29 commits into from
Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
779fa08
[ADD] First steps to introduce graphql
c8y3 Mar 8, 2024
1959ca6
[CLEAN] Put graphql blueprint initialization code in a function
c8y3 Mar 8, 2024
77ac06a
[ADD] First tests of the graphql API (pretty raw for the time being)
c8y3 Mar 8, 2024
c58cf01
[IMP] use removed small duplication in test
c8y3 Mar 15, 2024
cc8ad07
[IMP] prefixed local method with underscore
c8y3 Mar 15, 2024
7e4ef40
[IMP] Improved test readability
c8y3 Mar 15, 2024
0afe528
[IMP] Removed now obsolete comment
c8y3 Mar 15, 2024
770f320
[ADD] Started implementation of query cases, which should return the …
c8y3 Mar 15, 2024
3a42012
[ADD] Get cases by the current user identifier
c8y3 Mar 18, 2024
f18e872
[IMP] Updated comment
c8y3 Mar 19, 2024
13e5694
[IMP] Moved graphql definition of CaseObject in a dedicated file
c8y3 Mar 19, 2024
922720a
[IMP] made parameter current_user_id of get_filtered_cases mandatory
c8y3 Mar 19, 2024
e4a749b
[DEL] Removed now unnecessary test hello and goodbye graphql queries
c8y3 Mar 19, 2024
b5d4d85
[DEL] Removed unused import
c8y3 Mar 19, 2024
9aed83b
[IMP] Starting to separate business layer from REST layer. The busine…
c8y3 Mar 19, 2024
8afb523
[IMP] Moved some more code down into the business layer
c8y3 Mar 19, 2024
b8d6268
[IMP] Moved some more code down into the business layer
c8y3 Mar 19, 2024
95386f7
[IMP] Moved a part of permissions checks down into the business layer…
c8y3 Mar 19, 2024
fab8d0d
[IMP] Renamed exception PermissionDenied into PermissionDeniedError
c8y3 Mar 20, 2024
3e0f500
[IMP] Introduced permissions file to group code related to the busine…
c8y3 Mar 20, 2024
be82f2c
[IMP] Introduced permission check in the business layer
c8y3 Mar 20, 2024
447e236
[IMP] Changed self to root as the first argument of a resolver in gra…
c8y3 Mar 20, 2024
a73f9e5
[IMP] Seems lambda is not necessary
c8y3 Mar 20, 2024
f2b0e2d
[IMP] One import per line
c8y3 Mar 22, 2024
1cc3349
[ADD] global identifier to CaseObject
c8y3 Mar 22, 2024
c69457a
[IMP] use annotation @staticmethod, better for the IDE
c8y3 Mar 22, 2024
89e3754
[IMP] Removed unused import
c8y3 Mar 22, 2024
c29e247
[IMP] Moved code to create an ioc down into the business layer
c8y3 Mar 22, 2024
6f558eb
[ADD] First mutation to add an ioc
c8y3 Mar 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 8 additions & 33 deletions source/app/blueprints/case/case_ioc_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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'])
Expand Down
Empty file.
28 changes: 28 additions & 0 deletions source/app/blueprints/graphql/cases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# IRIS Source Code
# Copyright (C) 2024 - DFIR-IRIS
# [email protected]
#
# 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.relay import Node

from app.models.cases import Cases


class CaseObject(SQLAlchemyObjectType):
class Meta:
model = Cases
interfaces = [Node]
85 changes: 85 additions & 0 deletions source/app/blueprints/graphql/graphql_route.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# IRIS Source Code
# Copyright (C) 2024 - DFIR-IRIS
# [email protected]
#
# 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 flask_login import current_user

from graphql_server.flask import GraphQLView
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 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):
"""This is the IRIS GraphQL queries documentation!"""

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


class Mutation(ObjectType):
create_ioc = AddIoc.Field()


def _check_authentication_wrapper(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


def _create_blueprint():
schema = Schema(query=Query, mutation=Mutation)
graphql_view = GraphQLView.as_view('graphql', schema=schema)
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'])

return blueprint


graphql_blueprint = _create_blueprint()

# TODO I am unsure about the code organization (directories)
60 changes: 60 additions & 0 deletions source/app/blueprints/graphql/iocs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# IRIS Source Code
# Copyright (C) 2024 - DFIR-IRIS
# [email protected]
#
# 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 Field
from graphene import Mutation
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):
class Meta:
model = Ioc


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
# 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, 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)
52 changes: 14 additions & 38 deletions source/app/blueprints/manage/manage_cases_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -49,36 +48,34 @@
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_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
from app.iris_engine.module_handler.module_handler import instantiate_module_from_name
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
from app.business.errors import BusinessProcessingError
from app.business.errors import PermissionDeniedError

manage_cases_blueprint = Blueprint('manage_case',
__name__,
Expand Down Expand Up @@ -201,6 +198,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,
Expand All @@ -216,7 +214,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
)
Expand All @@ -238,35 +235,14 @@ def manage_case_filter(caseid) -> Response:
@manage_cases_blueprint.route('/manage/cases/delete/<int:cur_id>', 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]):
try:
delete(cur_id, caseid)
return response_success('Case successfully deleted')
except BusinessProcessingError as e:
return response_error(e.get_message())
except PermissionDeniedError:
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:
call_modules_hook('on_preload_case_delete', data=cur_id, caseid=caseid)
if delete_case(case_id=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)
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 Exception as e:
app.app.logger.exception(e)
return response_error("Cannot delete the case. Please check server logs for additional informations")


@manage_cases_blueprint.route('/manage/cases/reopen/<int:cur_id>', methods=['POST'])
@ac_api_requires(Permissions.standard_user, no_cid_required=True)
Expand Down
Empty file added source/app/business/__init__.py
Empty file.
50 changes: 50 additions & 0 deletions source/app/business/cases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# IRIS Source Code
# Copyright (C) 2024 - DFIR-IRIS
# [email protected]
#
# 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 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:
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=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=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')
Loading
Loading