説明なし

case_routes.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. # IRIS Source Code
  2. # Copyright (C) 2024 - DFIR-IRIS
  3. # contact@dfir-iris.org
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU Lesser General Public
  7. # License as published by the Free Software Foundation; either
  8. # version 3 of the License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. # Lesser General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU Lesser General Public License
  16. # along with this program; if not, write to the Free Software Foundation,
  17. # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. import binascii
  19. import marshmallow
  20. import traceback
  21. from flask import Blueprint
  22. from flask import request
  23. from flask_login import current_user
  24. from sqlalchemy import and_
  25. from sqlalchemy import desc
  26. from app import app
  27. from app import db
  28. from app import socket_io
  29. from app.blueprints.rest.endpoints import endpoint_deprecated
  30. from app.business.cases import cases_exists
  31. from app.datamgmt.case.case_db import get_review_id_from_name
  32. from app.datamgmt.case.case_db import case_get_desc_crc
  33. from app.datamgmt.case.case_db import get_case
  34. from app.datamgmt.manage.manage_groups_db import add_case_access_to_group
  35. from app.datamgmt.manage.manage_groups_db import get_group_with_members
  36. from app.datamgmt.manage.manage_users_db import get_user
  37. from app.datamgmt.manage.manage_users_db import get_users_list_restricted_from_case
  38. from app.datamgmt.manage.manage_users_db import set_user_case_access
  39. from app.business.cases import cases_export_to_json
  40. from app.iris_engine.access_control.utils import ac_fast_check_user_has_case_access
  41. from app.iris_engine.access_control.utils import ac_set_case_access_for_users
  42. from app.iris_engine.utils.tracker import track_activity
  43. from app.models.models import CaseStatus
  44. from app.models.models import ReviewStatusList
  45. from app.models.models import UserActivity
  46. from app.models.authorization import CaseAccessLevel
  47. from app.models.authorization import User
  48. from app.schema.marshables import TaskLogSchema
  49. from app.schema.marshables import CaseSchema
  50. from app.schema.marshables import CaseDetailsSchema
  51. from app.blueprints.access_controls import ac_requires_case_identifier
  52. from app.blueprints.access_controls import ac_api_requires
  53. from app.util import add_obj_history_entry
  54. from app.blueprints.responses import response_error
  55. from app.blueprints.responses import response_success
  56. case_rest_blueprint = Blueprint('case_rest', __name__)
  57. log = app.logger
  58. @case_rest_blueprint.route('/case/exists', methods=['GET'])
  59. @endpoint_deprecated('GET', '/api/v2/cases/<int:identifier>')
  60. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  61. @ac_api_requires()
  62. def case_routes_exists(caseid):
  63. if cases_exists(caseid):
  64. return response_success('Case exists')
  65. return response_error('Case does not exist', 404)
  66. @case_rest_blueprint.route('/case/summary/update', methods=['POST'])
  67. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  68. @ac_api_requires()
  69. def desc_fetch(caseid):
  70. js_data = request.get_json()
  71. case = get_case(caseid)
  72. if not case:
  73. return response_error('Invalid case ID')
  74. case.description = js_data.get('case_description')
  75. crc = binascii.crc32(case.description.encode('utf-8'))
  76. db.session.commit()
  77. track_activity('updated summary', caseid)
  78. if not request.cookies.get('session'):
  79. # API call so we propagate the message to everyone
  80. data = {
  81. 'case_description': case.description,
  82. 'last_saved': current_user.user
  83. }
  84. socket_io.emit('save', data, to=f'case-{caseid}')
  85. return response_success('Summary updated', data=crc)
  86. @case_rest_blueprint.route('/case/summary/fetch', methods=['GET'])
  87. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  88. @ac_api_requires()
  89. def summary_fetch(caseid):
  90. desc_crc32, description = case_get_desc_crc(caseid)
  91. return response_success('Summary fetch', data={'case_description': description, 'crc32': desc_crc32})
  92. @case_rest_blueprint.route('/case/activities/list', methods=['GET'])
  93. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  94. @ac_api_requires()
  95. def activity_fetch(caseid):
  96. ua = UserActivity.query.with_entities(
  97. UserActivity.activity_date,
  98. User.name,
  99. UserActivity.activity_desc,
  100. UserActivity.is_from_api
  101. ).filter(and_(
  102. UserActivity.case_id == caseid,
  103. UserActivity.display_in_ui == True
  104. )).join(
  105. UserActivity.user
  106. ).order_by(
  107. desc(UserActivity.activity_date)
  108. ).limit(40).all()
  109. output = [a._asdict() for a in ua]
  110. return response_success('', data=output)
  111. @case_rest_blueprint.route('/case/export', methods=['GET'])
  112. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  113. @ac_api_requires()
  114. def export_case(caseid):
  115. return response_success('', data=cases_export_to_json(caseid))
  116. @case_rest_blueprint.route('/case/meta', methods=['GET'])
  117. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  118. @ac_api_requires()
  119. def meta_case(caseid):
  120. case_details = get_case(caseid)
  121. return response_success('', data=CaseDetailsSchema().dump(case_details))
  122. @case_rest_blueprint.route('/case/tasklog/add', methods=['POST'])
  123. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  124. @ac_api_requires()
  125. def case_add_tasklog(caseid):
  126. log_schema = TaskLogSchema()
  127. try:
  128. log_data = log_schema.load(request.get_json())
  129. ua = track_activity(log_data.get('log_content'), caseid, user_input=True)
  130. except marshmallow.exceptions.ValidationError as e:
  131. return response_error(msg='Data error', data=e.messages)
  132. return response_success('Log saved', data=ua)
  133. @case_rest_blueprint.route('/case/users/list', methods=['GET'])
  134. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  135. @ac_api_requires()
  136. def case_get_users(caseid):
  137. users = get_users_list_restricted_from_case(caseid)
  138. return response_success(data=users)
  139. @case_rest_blueprint.route('/case/access/set-group', methods=['POST'])
  140. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  141. @ac_api_requires()
  142. def group_cac_set_case(caseid):
  143. data = request.get_json()
  144. if not data:
  145. return response_error('Invalid request')
  146. if data.get('case_id') != caseid:
  147. return response_error('Inconsistent case ID')
  148. case = get_case(caseid)
  149. if not case:
  150. return response_error('Invalid case ID')
  151. group_id = data.get('group_id')
  152. access_level = data.get('access_level')
  153. group = get_group_with_members(group_id)
  154. try:
  155. success, logs = add_case_access_to_group(group, [data.get('case_id')], access_level)
  156. if success:
  157. success, logs = ac_set_case_access_for_users(group.group_members, caseid, access_level)
  158. except Exception as e:
  159. log.error('Error while setting case access for group: {}'.format(e))
  160. log.error(traceback.format_exc())
  161. return response_error(msg=str(e))
  162. if success:
  163. track_activity('case access set to {} for group {}'.format(data.get('access_level'), group_id), caseid)
  164. add_obj_history_entry(case, 'access changed to {} for group {}'.format(data.get('access_level'), group_id),
  165. commit=True)
  166. return response_success(msg=logs)
  167. return response_error(msg=logs)
  168. @case_rest_blueprint.route('/case/access/set-user', methods=['POST'])
  169. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  170. @ac_api_requires()
  171. def user_cac_set_case(caseid):
  172. data = request.get_json()
  173. if not data:
  174. return response_error('Invalid request')
  175. if data.get('user_id') == current_user.id:
  176. return response_error('I can\'t let you do that, Dave')
  177. user = get_user(data.get('user_id'))
  178. if not user:
  179. return response_error('Invalid user ID')
  180. if data.get('case_id') != caseid:
  181. return response_error('Inconsistent case ID')
  182. case = get_case(caseid)
  183. if not case:
  184. return response_error('Invalid case ID')
  185. try:
  186. success, logs = set_user_case_access(user.id, data.get('case_id'), data.get('access_level'))
  187. track_activity('case access set to {} for user {}'.format(data.get('access_level'), user.name), caseid)
  188. add_obj_history_entry(case, 'access changed to {} for user {}'.format(data.get('access_level'), user.name))
  189. db.session.commit()
  190. except Exception as e:
  191. log.error('Error while setting case access for user: {}'.format(e))
  192. log.error(traceback.format_exc())
  193. return response_error(msg=str(e))
  194. if success:
  195. return response_success(msg=logs)
  196. return response_error(msg=logs)
  197. @case_rest_blueprint.route('/case/update-status', methods=['POST'])
  198. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  199. @ac_api_requires()
  200. def case_update_status(caseid):
  201. case = get_case(caseid)
  202. if not case:
  203. return response_error('Invalid case ID')
  204. status = request.get_json().get('status_id')
  205. case_status = {item.value for item in CaseStatus}
  206. try:
  207. status = int(status)
  208. except ValueError:
  209. return response_error('Invalid status')
  210. except TypeError:
  211. return response_error('Invalid status. Expected int')
  212. if status not in case_status:
  213. return response_error('Invalid status')
  214. case.status_id = status
  215. add_obj_history_entry(case, f'status updated to {CaseStatus(status).name}')
  216. db.session.commit()
  217. return response_success('Case status updated', data=case.status_id)
  218. @case_rest_blueprint.route('/case/review/update', methods=['POST'])
  219. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  220. @ac_api_requires()
  221. def case_review(caseid):
  222. case = get_case(caseid)
  223. if not case:
  224. return response_error('Invalid case ID')
  225. action = request.get_json().get('action')
  226. reviewer_id = request.get_json().get('reviewer_id')
  227. if action == 'start':
  228. review_name = ReviewStatusList.review_in_progress
  229. elif action in ('cancel', 'request'):
  230. review_name = ReviewStatusList.pending_review
  231. elif action == 'no_review':
  232. review_name = ReviewStatusList.no_review_required
  233. elif action == 'to_review':
  234. review_name = ReviewStatusList.not_reviewed
  235. elif action == 'done':
  236. review_name = ReviewStatusList.reviewed
  237. else:
  238. return response_error('Invalid action')
  239. case.review_status_id = get_review_id_from_name(review_name)
  240. if reviewer_id:
  241. try:
  242. reviewer_id = int(reviewer_id)
  243. except ValueError:
  244. return response_error('Invalid reviewer ID')
  245. if not ac_fast_check_user_has_case_access(reviewer_id, caseid, [CaseAccessLevel.full_access]):
  246. return response_error('Invalid reviewer ID')
  247. case.reviewer_id = reviewer_id
  248. db.session.commit()
  249. add_obj_history_entry(case, f'review status updated to {review_name}')
  250. track_activity(f'review status updated to {review_name}', caseid)
  251. db.session.commit()
  252. return response_success('Case review updated', data=CaseSchema().dump(case))