diff --git a/CODESTYLE.md b/CODESTYLE.md index 921d4b434..4e56890be 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -61,6 +61,7 @@ New files should be prefixed by the following license header, where `${current_y This allows any code maintainer to immediately spot which code elements can be freely modified without having to worry about the external context. Note: private elements are only called within the modules in which they are defined. +* Function names should be prefixed by the module name they belong to. Example: `iocs_create` instead of `create` ## Javascript coding rules diff --git a/architecture.md b/architecture.md new file mode 100644 index 000000000..d92d60adf --- /dev/null +++ b/architecture.md @@ -0,0 +1,55 @@ +# Iris Architecture + +The IRIS coarse-grained architecture can be understood by looking at the docker-compose.yml file. The main elements are: + +* db: postgresql database to store all application data +* app: backend application +* worker: most module hooks are processed by the worker +* rabbitmq: message broker between the app and worker +* nginx: the front server to serve static files and dispatch requests to app + +## Code organisation + +This section explains how the code is organized within the major namespaces. +They reflect the layered architecture of the IRIS backend: + +* blueprints +* business +* datamgmt + +The IRIS backend is a Flask application. + +### blueprints + +This is the public API of the `app`. It contains all the endpoints: REST, GraphQL, Flask templates (pages and modals). +The requests payloads are converted to business objects from `models` and passed down to calls into the business layer. + +Forbidden imports in this layer: + +* `from app.datamgmt`, as everything should go through the business layer first +* `from sqlalchemy` + +### business + +This is where processing happens. The methods should exclusively manipulate business objects from the `models` namespace. + +Forbidden imports in this layer: + +* `from app import db`, as the business layer should not take case of persistence details but rather delegate to the + `datamgmt` layer + +### datamgmt + +This layer handles persistence. It should be the only layer with knowledge of the database engine. + +Forbidden imports in this layer: + +* `from app.business`, as the business layer should call the persistence layer (not the other way around) + +### models + +The description of all objects handled by IRIS `business` layer and persisted through `datamgt`. + +### alembic + +This namespace takes care of the database migration. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 8be1806c0..8df0e079b 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -42,6 +42,8 @@ services: image: iriswebapp_app:v2.4.7 ports: - "127.0.0.1:8000:8000" + volumes: + - ./source/app:/iriswebapp/app worker: extends: @@ -51,6 +53,8 @@ services: context: . dockerfile: docker/webApp/Dockerfile image: iriswebapp_app:v2.4.7 + volumes: + - ./source/app:/iriswebapp/app nginx: extends: diff --git a/source/app/blueprints/case/case_ioc_routes.py b/source/app/blueprints/case/case_ioc_routes.py index 51834f464..0fb690caa 100644 --- a/source/app/blueprints/case/case_ioc_routes.py +++ b/source/app/blueprints/case/case_ioc_routes.py @@ -16,7 +16,6 @@ # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# IMPORTS ------------------------------------------------ from datetime import datetime import csv @@ -61,9 +60,9 @@ 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.iocs import update -from app.business.iocs import delete +from app.business.iocs import iocs_create +from app.business.iocs import iocs_update +from app.business.iocs import iocs_delete from app.business.errors import BusinessProcessingError case_ioc_blueprint = Blueprint( @@ -73,7 +72,6 @@ ) -# CONTENT ------------------------------------------------ @case_ioc_blueprint.route('/case/ioc', methods=['GET']) @ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access) def case_ioc(caseid, url_redir): @@ -130,7 +128,7 @@ def case_add_ioc(caseid): ioc_schema = IocSchema() try: - ioc, msg = create(request.get_json(), caseid) + ioc, msg = iocs_create(request.get_json(), caseid) return response_success(msg, data=ioc_schema.dump(ioc)) except BusinessProcessingError as e: return response_error(e.get_message(), data=e.get_data()) @@ -249,7 +247,7 @@ def case_add_ioc_modal(caseid): def case_delete_ioc(cur_id, caseid): try: - msg = delete(cur_id, caseid) + msg = iocs_delete(cur_id, caseid) return response_success(msg=msg) except BusinessProcessingError as e: @@ -297,7 +295,7 @@ def case_update_ioc(cur_id, caseid): ioc_schema = IocSchema() try: - ioc, msg = update(cur_id, request.get_json(), caseid) + ioc, msg = iocs_update(cur_id, request.get_json(), caseid) return response_success(msg, data=ioc_schema.dump(ioc)) except BusinessProcessingError as e: return response_error(e.get_message(), data=e.get_data()) diff --git a/source/app/blueprints/case/case_notes_routes.py b/source/app/blueprints/case/case_notes_routes.py index 29cfdc488..c2cb3de76 100644 --- a/source/app/blueprints/case/case_notes_routes.py +++ b/source/app/blueprints/case/case_notes_routes.py @@ -17,9 +17,8 @@ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import marshmallow -# IMPORTS ------------------------------------------------ from datetime import datetime -from flask import Blueprint, jsonify +from flask import Blueprint from flask import redirect from flask import render_template from flask import request @@ -31,12 +30,18 @@ from app import db, socket_io, app from app.blueprints.case.case_comments import case_comment_update -from app.business.errors import BusinessProcessingError, UnhandledBusinessError -from app.business.notes import update, create, list_note_revisions, get_note_revision, delete_note_revision +from app.business.errors import BusinessProcessingError +from app.business.notes import notes_update +from app.business.notes import notes_create +from app.business.notes import notes_list_revisions +from app.business.notes import notes_get_revision +from app.business.notes import notes_delete_revision from app.datamgmt.case.case_db import case_get_desc_crc from app.datamgmt.case.case_db import get_case -from app.datamgmt.case.case_notes_db import add_comment_to_note, get_directories_with_note_count, get_directory, \ - delete_directory +from app.datamgmt.case.case_notes_db import add_comment_to_note +from app.datamgmt.case.case_notes_db import get_directories_with_note_count +from app.datamgmt.case.case_notes_db import get_directory +from app.datamgmt.case.case_notes_db import delete_directory from app.datamgmt.case.case_notes_db import delete_note from app.datamgmt.case.case_notes_db import delete_note_comment from app.datamgmt.case.case_notes_db import get_case_note_comment @@ -47,10 +52,13 @@ from app.iris_engine.utils.tracker import track_activity from app.models import Notes from app.models.authorization import CaseAccessLevel -from app.schema.marshables import CaseNoteDirectorySchema, CaseNoteRevisionSchema +from app.schema.marshables import CaseNoteDirectorySchema +from app.schema.marshables import CaseNoteRevisionSchema from app.schema.marshables import CaseNoteSchema from app.schema.marshables import CommentSchema -from app.util import ac_api_case_requires, ac_socket_requires, endpoint_deprecated, add_obj_history_entry +from app.util import ac_api_case_requires +from app.util import ac_socket_requires +from app.util import endpoint_deprecated from app.util import ac_case_requires from app.util import response_error from app.util import response_success @@ -61,7 +69,6 @@ template_folder='templates') -# CONTENT ------------------------------------------------ @case_notes_blueprint.route('/case/notes', methods=['GET']) @ac_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access) def case_notes(caseid, url_redir): @@ -158,9 +165,7 @@ def case_note_save(cur_id, caseid): try: - note = update(identifier=cur_id, - request_json=request.get_json(), - case_identifier=caseid) + note = notes_update(identifier=cur_id, request_json=request.get_json(), case_identifier=caseid) return response_success(f"Note ID {cur_id} saved", data=addnote_schema.dump(note)) @@ -175,8 +180,7 @@ def case_note_list_history(cur_id, caseid): try: - note_version = list_note_revisions(identifier=cur_id, - case_identifier=caseid) + note_version = notes_list_revisions(identifier=cur_id, case_identifier=caseid) return response_success(f"ok", data=note_version_sc.dump(note_version)) @@ -191,9 +195,9 @@ def case_note_revision(cur_id, revision_id, caseid): try: - note_version = get_note_revision(identifier=cur_id, - revision_number=revision_id, - case_identifier=caseid) + note_version = notes_get_revision(identifier=cur_id, + revision_number=revision_id, + case_identifier=caseid) return response_success(f"ok", data=note_version_sc.dump(note_version)) @@ -207,9 +211,9 @@ def case_note_revision_delete(cur_id, revision_id, caseid): try: - delete_note_revision(identifier=cur_id, - revision_number=revision_id, - case_identifier=caseid) + notes_delete_revision(identifier=cur_id, + revision_number=revision_id, + case_identifier=caseid) return response_success(f"Revision {revision_id} of note {cur_id} deleted") @@ -224,8 +228,7 @@ def case_note_add(caseid): try: - note = create(request_json=request.get_json(), - case_identifier=caseid) + note = notes_create(request_json=request.get_json(), case_identifier=caseid) return response_success(f"Note ID {note.note_id} created", data=addnote_schema.dump(note)) @@ -525,7 +528,7 @@ def socket_ping_note(data): @socket_io.on('pong-note') @ac_socket_requires(CaseAccessLevel.full_access) -def socket_ping_note(data): +def socket_pong_note(data): emit('pong-note', {"user": current_user.name, "note_id": data['note_id']}, room=data['channel']) diff --git a/source/app/blueprints/case/case_routes.py b/source/app/blueprints/case/case_routes.py index bc96748a5..07ff1cd80 100644 --- a/source/app/blueprints/case/case_routes.py +++ b/source/app/blueprints/case/case_routes.py @@ -18,7 +18,6 @@ import binascii import marshmallow -# IMPORTS ------------------------------------------------ import traceback from flask import Blueprint from flask import redirect @@ -54,19 +53,23 @@ from app.datamgmt.manage.manage_users_db import get_user from app.datamgmt.manage.manage_users_db import get_users_list_restricted_from_case from app.datamgmt.manage.manage_users_db import set_user_case_access -from app.datamgmt.reporter.report_db import export_case_json +from app.business.cases import cases_export_to_json from app.forms import PipelinesCaseForm -from app.iris_engine.access_control.utils import ac_get_all_access_level, ac_fast_check_current_user_has_case_access, \ - ac_fast_check_user_has_case_access +from app.iris_engine.access_control.utils import ac_get_all_access_level +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_case_access_for_users from app.iris_engine.module_handler.module_handler import list_available_pipelines from app.iris_engine.utils.tracker import track_activity -from app.models import CaseStatus, ReviewStatusList +from app.models import CaseStatus +from app.models import ReviewStatusList from app.models import UserActivity from app.models.authorization import CaseAccessLevel from app.models.authorization import User -from app.schema.marshables import TaskLogSchema, CaseSchema, CaseDetailsSchema -from app.util import ac_api_case_requires, add_obj_history_entry +from app.schema.marshables import TaskLogSchema +from app.schema.marshables import CaseSchema +from app.schema.marshables import CaseDetailsSchema +from app.util import ac_api_case_requires +from app.util import add_obj_history_entry from app.util import ac_case_requires from app.util import ac_socket_requires from app.util import response_error @@ -168,7 +171,7 @@ def socket_summary_onsave(data): @socket_io.on('clear_buffer') @ac_socket_requires(CaseAccessLevel.full_access) -def socket_summary_onchange(message): +def socket_summary_on_clear_buffer(message): emit('clear_buffer', message) @@ -240,7 +243,7 @@ def activity_fetch(caseid): @case_blueprint.route("/case/export", methods=['GET']) @ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access) def export_case(caseid): - return response_success('', data=export_case_json(caseid)) + return response_success('', data=cases_export_to_json(caseid)) @case_blueprint.route("/case/meta", methods=['GET']) diff --git a/source/app/blueprints/graphql/cases.py b/source/app/blueprints/graphql/cases.py index 27667306f..bceb05572 100644 --- a/source/app/blueprints/graphql/cases.py +++ b/source/app/blueprints/graphql/cases.py @@ -27,11 +27,16 @@ from graphene import Float from graphene import String -from app.business.iocs import build_filter_case_ioc_query from app.models.cases import Cases -from app.business.cases import create -from app.business.cases import delete -from app.business.cases import update +from app.models.authorization import Permissions +from app.models.authorization import CaseAccessLevel + +from app.business.iocs import iocs_build_filter_query +from app.business.cases import cases_create +from app.business.cases import cases_delete +from app.business.cases import cases_update +from app.business.permissions import permissions_check_current_user_has_some_permission +from app.business.permissions import permissions_check_current_user_has_some_case_access from app.blueprints.graphql.iocs import IOCConnection @@ -48,10 +53,10 @@ class Meta: @staticmethod def resolve_iocs(root, info, ioc_id=None, ioc_uuid=None, ioc_value=None, ioc_type_id=None, ioc_description=None, ioc_tlp_id=None, ioc_tags=None, ioc_misp=None, user_id=None, Linked_cases=None, **kwargs): - return build_filter_case_ioc_query(ioc_id=ioc_id, ioc_uuid=ioc_uuid, ioc_value=ioc_value, - ioc_type_id=ioc_type_id, ioc_description=ioc_description, - ioc_tlp_id=ioc_tlp_id, ioc_tags=ioc_tags, ioc_misp=ioc_misp, - user_id=user_id, linked_cases=Linked_cases) + return iocs_build_filter_query(ioc_id=ioc_id, ioc_uuid=ioc_uuid, ioc_value=ioc_value, + ioc_type_id=ioc_type_id, ioc_description=ioc_description, + ioc_tlp_id=ioc_tlp_id, ioc_tags=ioc_tags, ioc_misp=ioc_misp, + user_id=user_id, linked_cases=Linked_cases) class CaseConnection(Connection): @@ -89,7 +94,7 @@ def mutate(root, info, name, description, client_id, soc_id=None, classification request['case_soc_id'] = soc_id if classification_id: request['classification_id'] = classification_id - case, _ = create(request) + case, _ = cases_create(request) return CaseCreate(case=case) @@ -102,7 +107,9 @@ class Arguments: @staticmethod def mutate(root, info, case_id): - delete(case_id) + permissions_check_current_user_has_some_permission([Permissions.standard_user]) + permissions_check_current_user_has_some_case_access(case_id, [CaseAccessLevel.full_access]) + cases_delete(case_id) class CaseUpdate(Mutation): @@ -150,5 +157,8 @@ def mutate(root, info, case_id, name=None, soc_id=None, classification_id=None, request['case_tags'] = tags if review_status_id: request['review_status_id'] = review_status_id - case, _ = update(case_id, request) + permissions_check_current_user_has_some_permission([Permissions.standard_user]) + permissions_check_current_user_has_some_case_access(case_id, [CaseAccessLevel.full_access]) + + case, _ = cases_update(case_id, request) return CaseUpdate(case=case) diff --git a/source/app/blueprints/graphql/graphql_route.py b/source/app/blueprints/graphql/graphql_route.py index 42e0ca987..dff808a67 100644 --- a/source/app/blueprints/graphql/graphql_route.py +++ b/source/app/blueprints/graphql/graphql_route.py @@ -37,18 +37,22 @@ from app.util import is_user_authenticated from app.util import response_error +from app.models.authorization import CaseAccessLevel + from app.blueprints.graphql.cases import CaseObject from app.blueprints.graphql.iocs import IOCObject from app.blueprints.graphql.iocs import IOCCreate from app.blueprints.graphql.iocs import IOCUpdate from app.blueprints.graphql.iocs import IOCDelete -from app.business.cases import get_case_by_identifier -from app.business.iocs import get_ioc_by_identifier from app.blueprints.graphql.cases import CaseCreate from app.blueprints.graphql.cases import CaseDelete from app.blueprints.graphql.cases import CaseUpdate from app.blueprints.graphql.cases import CaseConnection +from app.business.cases import cases_get_by_identifier +from app.business.iocs import iocs_get_by_identifier +from app.business.permissions import permissions_check_current_user_has_some_case_access + class Query(ObjectType): """This is the IRIS GraphQL queries documentation!""" @@ -69,11 +73,14 @@ def resolve_cases(root, info, classification_id=None, client_id=None, state_id=N @staticmethod def resolve_case(root, info, case_id): - return get_case_by_identifier(case_id) + permissions_check_current_user_has_some_case_access(case_id, + [CaseAccessLevel.read_only, CaseAccessLevel.full_access]) + + return cases_get_by_identifier(case_id) @staticmethod def resolve_ioc(root, info, ioc_id): - return get_ioc_by_identifier(ioc_id) + return iocs_get_by_identifier(ioc_id) class Mutation(ObjectType): diff --git a/source/app/blueprints/graphql/iocs.py b/source/app/blueprints/graphql/iocs.py index c5128d347..8c3163086 100644 --- a/source/app/blueprints/graphql/iocs.py +++ b/source/app/blueprints/graphql/iocs.py @@ -24,12 +24,12 @@ from graphene import Float from graphene import String -from app.business.permissions import check_current_user_has_some_case_access_stricter +from app.business.permissions import permissions_check_current_user_has_some_case_access_stricter from app.models.authorization import CaseAccessLevel from app.models.models import Ioc -from app.business.iocs import create -from app.business.iocs import update -from app.business.iocs import delete +from app.business.iocs import iocs_create +from app.business.iocs import iocs_update +from app.business.iocs import iocs_delete from graphene.relay import Connection @@ -74,9 +74,9 @@ def mutate(root, info, case_id, type_id, tlp_id, value, description=None, tags=N 'ioc_description': description, 'ioc_tags': tags } - check_current_user_has_some_case_access_stricter([CaseAccessLevel.full_access]) + permissions_check_current_user_has_some_case_access_stricter([CaseAccessLevel.full_access]) - ioc, _ = create(request, case_id) + ioc, _ = iocs_create(request, case_id) return IOCCreate(ioc=ioc) @@ -101,7 +101,7 @@ class Arguments: @staticmethod def mutate(root, info, ioc_id, case_id, type_id=None, tlp_id=None, value=None, description=None, tags=None, ioc_misp=None, user_id=None, ioc_enrichment=None, modification_history=None): - check_current_user_has_some_case_access_stricter([CaseAccessLevel.full_access]) + permissions_check_current_user_has_some_case_access_stricter([CaseAccessLevel.full_access]) request = {} if type_id: @@ -122,7 +122,7 @@ def mutate(root, info, ioc_id, case_id, type_id=None, tlp_id=None, value=None, d request['ioc_enrichment'] = ioc_enrichment if modification_history: request['modification_history'] = modification_history - ioc, _ = update(ioc_id, request, case_id) + ioc, _ = iocs_update(ioc_id, request, case_id) return IOCCreate(ioc=ioc) @@ -136,7 +136,7 @@ class Arguments: @staticmethod def mutate(root, info, ioc_id, case_id): - check_current_user_has_some_case_access_stricter([CaseAccessLevel.full_access]) + permissions_check_current_user_has_some_case_access_stricter([CaseAccessLevel.full_access]) - message = delete(ioc_id, case_id) + message = iocs_delete(ioc_id, case_id) return IOCDelete(message=message) diff --git a/source/app/blueprints/manage/manage_access_control.py b/source/app/blueprints/manage/manage_access_control.py index 45e937b6e..8d5e704e4 100644 --- a/source/app/blueprints/manage/manage_access_control.py +++ b/source/app/blueprints/manage/manage_access_control.py @@ -20,7 +20,7 @@ from flask_wtf import FlaskForm from werkzeug.utils import redirect -from app.business.users import _reset_user_mfa +from app.business.users import users_reset_mfa from app.iris_engine.access_control.utils import ac_recompute_all_users_effective_ac from app.iris_engine.access_control.utils import ac_recompute_effective_ac from app.iris_engine.access_control.utils import ac_trace_effective_user_permissions @@ -70,7 +70,7 @@ def manage_ac_compute_effective_ac(cur_id): @ac_api_requires(Permissions.server_administrator) def manage_ac_reset_mfa(cur_id): - _reset_user_mfa(cur_id) + users_reset_mfa(cur_id) return response_success('Updated') diff --git a/source/app/blueprints/manage/manage_cases_routes.py b/source/app/blueprints/manage/manage_cases_routes.py index 72878618a..7bd69ea14 100644 --- a/source/app/blueprints/manage/manage_cases_routes.py +++ b/source/app/blueprints/manage/manage_cases_routes.py @@ -31,6 +31,17 @@ from werkzeug.utils import secure_filename from app import db + +from app.models.authorization import CaseAccessLevel +from app.models.authorization import Permissions + +from app.business.permissions import permissions_check_current_user_has_some_case_access +from app.business.cases import cases_delete +from app.business.cases import cases_update +from app.business.cases import cases_create +from app.business.errors import BusinessProcessingError +from app.business.errors import PermissionDeniedError + from app.datamgmt.alerts.alerts_db import get_alert_status_by_name from app.datamgmt.case.case_db import get_case from app.datamgmt.client.client_db import get_client_list @@ -56,8 +67,6 @@ 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.authorization import CaseAccessLevel -from app.models.authorization import Permissions from app.schema.marshables import CaseSchema from app.schema.marshables import CaseDetailsSchema from app.util import add_obj_history_entry @@ -67,18 +76,12 @@ 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.cases import update -from app.business.cases import create -from app.business.errors import BusinessProcessingError -from app.business.errors import PermissionDeniedError manage_cases_blueprint = Blueprint('manage_case', __name__, template_folder='templates') -# CONTENT ------------------------------------------------ @manage_cases_blueprint.route('/manage/cases', methods=['GET']) @ac_requires(Permissions.standard_user, no_cid_required=True) def manage_index_cases(caseid, url_redir): @@ -232,12 +235,15 @@ def manage_case_filter() -> Response: @ac_api_requires(Permissions.standard_user) def api_delete_case(cur_id): try: - delete(cur_id) + permissions_check_current_user_has_some_case_access(cur_id, [CaseAccessLevel.full_access]) + except PermissionDeniedError: + return ac_api_return_access_denied(caseid=cur_id) + + try: + cases_delete(cur_id) 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) @manage_cases_blueprint.route('/manage/cases/reopen/', methods=['POST']) @@ -348,7 +354,7 @@ def api_add_case(): case_schema = CaseSchema() try: - case, msg = create(request.get_json()) + case, msg = cases_create(request.get_json()) return response_success(msg, data=case_schema.dump(case)) except BusinessProcessingError as e: return response_error(e.get_message(), data=e.get_data()) @@ -365,9 +371,11 @@ def api_list_case(): @manage_cases_blueprint.route('/manage/cases/update/', methods=['POST']) @ac_api_requires(Permissions.standard_user) def update_case_info(cur_id): + permissions_check_current_user_has_some_case_access(cur_id, [CaseAccessLevel.full_access]) + case_schema = CaseSchema() try: - case, msg = update(cur_id, request.get_json()) + case, msg = cases_update(cur_id, request.get_json()) return response_success(msg, data=case_schema.dump(case)) except BusinessProcessingError as e: return response_error(e.get_message(), data=e.get_data()) diff --git a/source/app/blueprints/reports/reports_route.py b/source/app/blueprints/reports/reports_route.py index c9c100515..71f5aeba7 100644 --- a/source/app/blueprints/reports/reports_route.py +++ b/source/app/blueprints/reports/reports_route.py @@ -27,7 +27,12 @@ from app.iris_engine.reporter.reporter import IrisMakeDocReport from app.iris_engine.reporter.reporter import IrisMakeMdReport from app.iris_engine.utils.tracker import track_activity + from app.models import CaseTemplateReport +from app.models.authorization import CaseAccessLevel + +from app.business.permissions import permissions_check_current_user_has_some_case_access_stricter + from app.util import FileRemover from app.util import ac_api_requires from app.util import ac_requires_case_identifier @@ -42,6 +47,10 @@ @ac_api_requires() @ac_requires_case_identifier() def download_case_activity(report_id, caseid): + # TODO should we move this up + # and replace by annotation @ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)? + permissions_check_current_user_has_some_case_access_stricter( + [CaseAccessLevel.read_only, CaseAccessLevel.full_access]) call_modules_hook('on_preload_activities_report_create', data=report_id, caseid=caseid) if report_id: @@ -56,7 +65,7 @@ def download_case_activity(report_id, caseid): # Get file extension _, report_format = os.path.splitext(report.internal_reference) - + # Depending on the template format, the generation process is different if report_format == ".docx": mreport = IrisMakeDocReport(tmp_dir, report_id, caseid, safe_mode) @@ -87,7 +96,11 @@ def download_case_activity(report_id, caseid): @reports_blueprint.route("/case/report/generate-investigation/", methods=['GET']) @ac_api_requires() @ac_requires_case_identifier() -def _gen_report(report_id, caseid): +def generate_report(report_id, caseid): + # TODO should we move this up + # and replace by annotation @ac_api_case_requires(CaseAccessLevel.read_only, CaseAccessLevel.full_access)? + permissions_check_current_user_has_some_case_access_stricter( + [CaseAccessLevel.read_only, CaseAccessLevel.full_access]) safe_mode = False @@ -101,7 +114,7 @@ def _gen_report(report_id, caseid): safe_mode = True _, report_format = os.path.splitext(report.internal_reference) - + if report_format == ".md" or report_format == ".html": mreport = IrisMakeMdReport(tmp_dir, report_id, caseid, safe_mode) fpath, logs = mreport.generate_md_report(doc_type="Investigation") diff --git a/source/app/business/cases.py b/source/app/business/cases.py index fe41d1386..73241e427 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. +import datetime import logging as log import traceback @@ -23,17 +24,17 @@ from marshmallow.exceptions import ValidationError -from app.schema.marshables import CaseSchema - from app import app from app import db from app.util import add_obj_history_entry +from app.schema.marshables import CaseSchema -from app.models.authorization import CaseAccessLevel -from app.models.authorization import Permissions from app.models import ReviewStatusList +from app.business.errors import BusinessProcessingError +from app.business.iocs import iocs_exports_to_json + from app.iris_engine.module_handler.module_handler import call_modules_hook from app.iris_engine.utils.tracker import track_activity from app.iris_engine.access_control.utils import ac_set_new_case_access @@ -51,16 +52,14 @@ from app.datamgmt.manage.manage_cases_db import map_alert_resolution_to_case_status from app.datamgmt.manage.manage_cases_db import close_case from app.datamgmt.case.case_db import get_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 get_case_by_identifier(case_identifier): - check_current_user_has_some_case_access(case_identifier, [CaseAccessLevel.read_only, CaseAccessLevel.full_access]) - - return get_case(case_identifier) +from app.datamgmt.reporter.report_db import export_caseinfo_json +from app.datamgmt.reporter.report_db import process_md_images_links_for_report +from app.datamgmt.reporter.report_db import export_case_evidences_json +from app.datamgmt.reporter.report_db import export_case_tm_json +from app.datamgmt.reporter.report_db import export_case_assets_json +from app.datamgmt.reporter.report_db import export_case_tasks_json +from app.datamgmt.reporter.report_db import export_case_comments_json +from app.datamgmt.reporter.report_db import export_case_notes_json def _load(request_data, **kwargs): @@ -71,7 +70,11 @@ def _load(request_data, **kwargs): raise BusinessProcessingError('Data error', e.messages) -def create(request_json): +def cases_get_by_identifier(case_identifier): + return get_case(case_identifier) + + +def cases_create(request_json): try: # TODO remove caseid doesn't seems to be useful for call_modules_hook => remove argument request_data = call_modules_hook('on_preload_case_create', request_json, None) @@ -120,10 +123,7 @@ def create(request_json): raise BusinessProcessingError('Error creating case - check server logs') -def delete(case_identifier): - check_current_user_has_some_permission([Permissions.standard_user]) - check_current_user_has_some_case_access(case_identifier, [CaseAccessLevel.full_access]) - +def cases_delete(case_identifier): if case_identifier == 1: track_activity(f'tried to delete case {case_identifier}, but case is the primary case', caseid=case_identifier, ctx_less=True) @@ -143,10 +143,7 @@ def delete(case_identifier): raise BusinessProcessingError('Cannot delete the case. Please check server logs for additional informations') -def update(case_identifier, request_data): - check_current_user_has_some_permission([Permissions.standard_user]) - check_current_user_has_some_case_access(case_identifier, [CaseAccessLevel.full_access]) - +def cases_update(case_identifier, request_data): case_i = get_case(case_identifier) if not case_i: raise BusinessProcessingError('Case not found') @@ -231,3 +228,55 @@ def update(case_identifier, request_data): log.error(e.__str__()) log.error(traceback.format_exc()) raise BusinessProcessingError('Error updating case - check server logs') + + +def cases_export_to_json(case_id): + """ + Fully export a case a JSON + """ + export = {} + case = export_caseinfo_json(case_id) + + if not case: + export['errors'] = ["Invalid case number"] + return export + + case['description'] = process_md_images_links_for_report(case['description']) + + export['case'] = case + export['evidences'] = export_case_evidences_json(case_id) + export['timeline'] = export_case_tm_json(case_id) + export['iocs'] = iocs_exports_to_json(case_id) + export['assets'] = export_case_assets_json(case_id) + export['tasks'] = export_case_tasks_json(case_id) + export['comments'] = export_case_comments_json(case_id) + export['notes'] = export_case_notes_json(case_id) + export['export_date'] = datetime.datetime.utcnow() + + return export + + +def cases_export_to_report_json(case_id): + """ + Fully export of a case for report generation + """ + export = {} + case = export_caseinfo_json(case_id) + + if not case: + export['errors'] = ["Invalid case number"] + return export + + case['description'] = process_md_images_links_for_report(case['description']) + + export['case'] = case + export['evidences'] = export_case_evidences_json(case_id) + export['timeline'] = export_case_tm_json(case_id) + export['iocs'] = iocs_exports_to_json(case_id) + export['assets'] = export_case_assets_json(case_id) + export['tasks'] = export_case_tasks_json(case_id) + export['notes'] = export_case_notes_json(case_id) + export['comments'] = export_case_comments_json(case_id) + export['export_date'] = datetime.datetime.utcnow() + + return export diff --git a/source/app/business/iocs.py b/source/app/business/iocs.py index 09884a026..bae103b5b 100644 --- a/source/app/business/iocs.py +++ b/source/app/business/iocs.py @@ -20,8 +20,8 @@ from marshmallow.exceptions import ValidationError from app import db -from app.models import Ioc, IocLink -from app.models.authorization import CaseAccessLevel +from app.models import Ioc +from app.models import IocLink 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 @@ -32,15 +32,9 @@ 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 from app.datamgmt.case.case_iocs_db import get_ioc -def get_ioc_by_identifier(ioc_identifier): - - return get_ioc(ioc_identifier) - - def _load(request_data): try: add_ioc_schema = IocSchema() @@ -49,7 +43,11 @@ def _load(request_data): raise BusinessProcessingError('Data error', e.messages) -def create(request_json, case_identifier): +def iocs_get_by_identifier(ioc_identifier): + return get_ioc(ioc_identifier) + + +def iocs_create(request_json, case_identifier): # 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 @@ -81,7 +79,7 @@ def create(request_json, case_identifier): # TODO most probably this method should not require a case_identifier... Since the IOC gets modified for all cases... -def update(identifier, request_json, case_identifier): +def iocs_update(identifier, request_json, case_identifier): try: ioc = get_ioc(identifier, caseid=case_identifier) @@ -120,7 +118,7 @@ def update(identifier, request_json, case_identifier): raise BusinessProcessingError('Unexpected error server-side', e) -def delete(identifier, case_identifier): +def iocs_delete(identifier, case_identifier): call_modules_hook('on_preload_ioc_delete', data=identifier, caseid=case_identifier) ioc = get_ioc(identifier, case_identifier) @@ -138,23 +136,24 @@ def delete(identifier, case_identifier): return f'IOC {identifier} deleted' -def get_iocs(case_identifier): - check_current_user_has_some_case_access_stricter([CaseAccessLevel.read_only, CaseAccessLevel.full_access]) +def iocs_exports_to_json(case_id): + iocs = get_iocs_by_case(case_id) + + iocs_serialized = IocSchema().dump(iocs, many=True) - return get_iocs_by_case(case_identifier) + return iocs_serialized -def build_filter_case_ioc_query(ioc_id: int = None, - ioc_uuid: str = None, - ioc_value: str = None, - ioc_type_id: int = None, - ioc_description: str = None, - ioc_tlp_id: int = None, - ioc_tags: str = None, - ioc_misp: str = None, - user_id: float = None, - linked_cases: float = None - ): +def iocs_build_filter_query(ioc_id: int = None, + ioc_uuid: str = None, + ioc_value: str = None, + ioc_type_id: int = None, + ioc_description: str = None, + ioc_tlp_id: int = None, + ioc_tags: str = None, + ioc_misp: str = None, + user_id: float = None, + linked_cases: float = None): """ Get a list of iocs from the database, filtered by the given parameters """ diff --git a/source/app/business/notes.py b/source/app/business/notes.py index df4985708..96117d433 100644 --- a/source/app/business/notes.py +++ b/source/app/business/notes.py @@ -19,14 +19,14 @@ from flask_login import current_user from marshmallow import ValidationError -from app import db, app +from app import db +from app import app from app.business.errors import BusinessProcessingError, UnhandledBusinessError -from app.business.permissions import check_current_user_has_some_case_access_stricter from app.datamgmt.case.case_notes_db import get_note from app.iris_engine.module_handler.module_handler import call_modules_hook from app.iris_engine.utils.tracker import track_activity from app.models import NoteRevisions -from app.models.authorization import CaseAccessLevel, User +from app.models.authorization import User from app.schema.marshables import CaseNoteSchema from app.util import add_obj_history_entry @@ -40,14 +40,13 @@ def _load(request_data, note_schema=None): raise BusinessProcessingError('Data error', e.messages) -def create(request_json, case_identifier): +def notes_create(request_json, case_identifier): """ Create a note. :param request_json: The request data. :param case_identifier: The case identifier. """ - try: request_data = call_modules_hook('on_preload_note_create', data=request_json, caseid=case_identifier) note_schema = CaseNoteSchema() @@ -87,7 +86,7 @@ def create(request_json, case_identifier): raise BusinessProcessingError('Unexpected error server-side', e) -def update(identifier: int = None, request_json: dict = None, case_identifier: int = None): +def notes_update(identifier: int = None, request_json: dict = None, case_identifier: int = None): """ Update a note by its identifier. @@ -95,8 +94,6 @@ def update(identifier: int = None, request_json: dict = None, case_identifier: i :param request_json: The request data. :param case_identifier: The case identifier. """ - check_current_user_has_some_case_access_stricter([CaseAccessLevel.full_access]) - try: addnote_schema = CaseNoteSchema() @@ -152,7 +149,7 @@ def update(identifier: int = None, request_json: dict = None, case_identifier: i raise UnhandledBusinessError('Unexpected error server-side', str(e)) -def list_note_revisions(identifier: int = None, case_identifier: int = None): +def notes_list_revisions(identifier: int = None, case_identifier: int = None): """ List the revisions of a note by its identifier. @@ -187,7 +184,7 @@ def list_note_revisions(identifier: int = None, case_identifier: int = None): raise UnhandledBusinessError('Unexpected error server-side', str(e)) -def get_note_revision(identifier: int = None, revision_number: int = None, case_identifier: int = None): +def notes_get_revision(identifier: int = None, revision_number: int = None, case_identifier: int = None): """ Get a note revision by its identifier and revision number. @@ -216,7 +213,7 @@ def get_note_revision(identifier: int = None, revision_number: int = None, case_ raise UnhandledBusinessError('Unexpected error server-side', str(e)) -def delete_note_revision(identifier: int = None, revision_number: int = None, case_identifier: int = None): +def notes_delete_revision(identifier: int = None, revision_number: int = None, case_identifier: int = None): """ Delete a note revision by its identifier and revision number. @@ -224,8 +221,6 @@ def delete_note_revision(identifier: int = None, revision_number: int = None, ca :param revision_number: The revision number. :param case_identifier: The case identifier. """ - check_current_user_has_some_case_access_stricter([CaseAccessLevel.full_access]) - try: note = get_note(identifier, caseid=case_identifier) if not note: @@ -248,4 +243,4 @@ def delete_note_revision(identifier: int = None, revision_number: int = None, ca raise BusinessProcessingError('Data error', e.messages) except Exception as e: - raise UnhandledBusinessError('Unexpected error server-side', str(e)) \ No newline at end of file + raise UnhandledBusinessError('Unexpected error server-side', str(e)) diff --git a/source/app/business/permissions.py b/source/app/business/permissions.py index e3793940e..d79d449c8 100644 --- a/source/app/business/permissions.py +++ b/source/app/business/permissions.py @@ -38,7 +38,7 @@ def _deny_permission(): # When moving down permission checks from the REST layer into the business layer, # this method is used to replace manual calls to ac_fast_check_current_user_has_case_access -def check_current_user_has_some_case_access(case_identifier, access_levels): +def permissions_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): _deny_permission() @@ -47,7 +47,7 @@ def check_current_user_has_some_case_access(case_identifier, access_levels): # This one comes from ac_api_case_requires, whereas the other one comes from the way api_delete_case was written... # When moving down permission checks from the REST layer into the business layer, # this method is used to replace annotation ac_api_case_requires -def check_current_user_has_some_case_access_stricter(access_levels): +def permissions_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? @@ -60,7 +60,7 @@ def check_current_user_has_some_case_access_stricter(access_levels): # When moving down permission checks from the REST layer into the business layer, # this method is used to replace annotation ac_api_requires -def check_current_user_has_some_permission(permissions): +def permissions_check_current_user_has_some_permission(permissions): if 'permissions' not in session: session['permissions'] = ac_get_effective_permissions_of_user(current_user) diff --git a/source/app/business/users.py b/source/app/business/users.py index a917c6cde..e089c7f9d 100644 --- a/source/app/business/users.py +++ b/source/app/business/users.py @@ -20,7 +20,7 @@ from app.datamgmt.manage.manage_users_db import get_user, get_active_user -def _reset_user_mfa(user_id: int = None): +def users_reset_mfa(user_id: int = None): """ Resets a user MFA by setting to none its MFA token """ diff --git a/source/app/datamgmt/datastore/datastore_db.py b/source/app/datamgmt/datastore/datastore_db.py index 53c663d0b..e95e33979 100644 --- a/source/app/datamgmt/datastore/datastore_db.py +++ b/source/app/datamgmt/datastore/datastore_db.py @@ -54,6 +54,7 @@ def datastore_get_root(cid): return dsp_root + def ds_list_tree(cid): dsp_root = datastore_get_root(cid) diff --git a/source/app/datamgmt/reporter/report_db.py b/source/app/datamgmt/reporter/report_db.py index 51336cbf0..3b8806b23 100644 --- a/source/app/datamgmt/reporter/report_db.py +++ b/source/app/datamgmt/reporter/report_db.py @@ -20,20 +20,19 @@ from sqlalchemy import desc -from app.business.iocs import get_iocs -from app.datamgmt.case.case_notes_db import get_notes_from_group, get_case_note_comments -from app.datamgmt.case.case_tasks_db import get_tasks_with_assignees -from app.models import AnalysisStatus, CompromiseStatus, TaskAssignee, NotesGroupLink +from app.datamgmt.case.case_notes_db import get_notes_from_group +from app.datamgmt.case.case_notes_db import get_case_note_comments +from app.models import AnalysisStatus +from app.models import CompromiseStatus +from app.models import TaskAssignee from app.models import AssetsType from app.models import CaseAssets from app.models import CaseEventsAssets from app.models import CaseEventsIoc from app.models import CaseReceivedFile -from app.models import CaseStatus from app.models import CaseTasks from app.models import Cases from app.models import CasesEvent -from app.models import Client from app.models import Comments from app.models import EventCategory from app.models import Ioc @@ -45,59 +44,9 @@ from app.models import TaskStatus from app.models import Tlp from app.models.authorization import User -from app.schema.marshables import CaseDetailsSchema, CommentSchema, CaseNoteSchema, IocSchema - - -def export_case_json(case_id): - """ - Fully export a case a JSON - """ - export = {} - case = export_caseinfo_json(case_id) - - if not case: - export['errors'] = ["Invalid case number"] - return export - - case['description'] = process_md_images_links_for_report(case['description']) - - export['case'] = case - export['evidences'] = export_case_evidences_json(case_id) - export['timeline'] = export_case_tm_json(case_id) - export['iocs'] = export_case_iocs_json(case_id) - export['assets'] = export_case_assets_json(case_id) - export['tasks'] = export_case_tasks_json(case_id) - export['comments'] = export_case_comments_json(case_id) - export['notes'] = export_case_notes_json(case_id) - export['export_date'] = datetime.datetime.utcnow() - - return export - - -def export_case_json_for_report(case_id): - """ - Fully export of a case for report generation - """ - export = {} - case = export_caseinfo_json(case_id) - - if not case: - export['errors'] = ["Invalid case number"] - return export - - case['description'] = process_md_images_links_for_report(case['description']) - - export['case'] = case - export['evidences'] = export_case_evidences_json(case_id) - export['timeline'] = export_case_tm_json(case_id) - export['iocs'] = export_case_iocs_json(case_id) - export['assets'] = export_case_assets_json(case_id) - export['tasks'] = export_case_tasks_json(case_id) - export['notes'] = export_case_notes_json(case_id) - export['comments'] = export_case_comments_json(case_id) - export['export_date'] = datetime.datetime.utcnow() - - return export +from app.schema.marshables import CaseDetailsSchema +from app.schema.marshables import CommentSchema +from app.schema.marshables import CaseNoteSchema def export_case_json_extended(case_id): @@ -334,14 +283,6 @@ def export_case_tm_json(case_id): return tim -def export_case_iocs_json(case_id): - iocs = get_iocs(case_id) - - iocs_serialized = IocSchema().dump(iocs, many=True) - - return iocs_serialized - - def export_case_tasks_json(case_id): res = CaseTasks.query.with_entities( CaseTasks.task_title, diff --git a/source/app/iris_engine/reporter/reporter.py b/source/app/iris_engine/reporter/reporter.py index bcc791249..5aa6e1870 100644 --- a/source/app/iris_engine/reporter/reporter.py +++ b/source/app/iris_engine/reporter/reporter.py @@ -18,30 +18,22 @@ # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# IMPORTS ------------------------------------------------ - -# VARS --------------------------------------------------- - -# CONTENT ------------------------------------------------ import logging as log import os from datetime import datetime - -import jinja2 -from jinja2.sandbox import SandboxedEnvironment - -from app.datamgmt.reporter.report_db import export_case_json_for_report -from app.iris_engine.utils.common import IrisJinjaEnv from docx_generator.docx_generator import DocxGenerator from docx_generator.exceptions import rendering_error from flask_login import current_user from sqlalchemy import desc from app import app +from app.business.cases import cases_export_to_report_json +from app.business.cases import cases_export_to_json + from app.datamgmt.activities.activities_db import get_auto_activities from app.datamgmt.activities.activities_db import get_manual_activities from app.datamgmt.case.case_db import case_get_desc_crc -from app.datamgmt.reporter.report_db import export_case_json + from app.models import AssetsType from app.models import CaseAssets from app.models import CaseEventsAssets @@ -51,7 +43,9 @@ from app.models import Ioc from app.models import IocAssetLink from app.models import IocLink + from app.iris_engine.reporter.ImageHandler import ImageHandler +from app.iris_engine.utils.common import IrisJinjaEnv LOG_FORMAT = '%(asctime)s :: %(levelname)s :: %(module)s :: %(funcName)s :: %(message)s' log.basicConfig(level=log.INFO, format=LOG_FORMAT) @@ -115,7 +109,7 @@ def _get_case_info(self): Retrieve information of the case :return: """ - case_info = export_case_json(self._caseid) + case_info = cases_export_to_json(self._caseid) # Get customer, user and case title case_info['doc_id'] = IrisReportMaker.get_docid() @@ -361,7 +355,7 @@ def _get_case_info(self): Retrieve information of the case :return: """ - case_info = export_case_json_for_report(self._caseid) + case_info = cases_export_to_report_json(self._caseid) # Get customer, user and case title case_info['doc_id'] = IrisMakeDocReport.get_docid() diff --git a/tests/tests.py b/tests/tests.py index 942f8b341..826d580e9 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -546,7 +546,7 @@ def test_graphql_delete_case_should_not_fail(self): self._subject.execute_graphql_query(payload2) payload = { 'query': f''' mutation {{ - caseUpdate(caseId: {case_identifier}, name: "test_delete_case") {{ + caseUpdate(caseId: {case_identifier}, name: "test_delete_case") {{ case {{ name }} }} }}'''