Нема описа

case_notes_routes.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  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 marshmallow
  19. from datetime import datetime
  20. from flask import Blueprint
  21. from flask import request
  22. from flask_login import current_user
  23. from sqlalchemy import or_
  24. from sqlalchemy import and_
  25. from app import db
  26. from app import app
  27. from app.blueprints.rest.case_comments import case_comment_update
  28. from app.business.errors import BusinessProcessingError
  29. from app.business.notes import notes_create
  30. from app.business.notes import notes_list_revisions
  31. from app.business.notes import notes_get_revision
  32. from app.business.notes import notes_delete_revision
  33. from app.business.notes import notes_update
  34. from app.datamgmt.case.case_db import get_case
  35. from app.datamgmt.case.case_notes_db import add_comment_to_note
  36. from app.datamgmt.case.case_notes_db import get_directories_with_note_count
  37. from app.datamgmt.case.case_notes_db import get_directory
  38. from app.datamgmt.case.case_notes_db import delete_directory
  39. from app.datamgmt.case.case_notes_db import delete_note
  40. from app.datamgmt.case.case_notes_db import delete_note_comment
  41. from app.datamgmt.case.case_notes_db import get_case_note_comment
  42. from app.datamgmt.case.case_notes_db import get_case_note_comments
  43. from app.datamgmt.case.case_notes_db import get_note
  44. from app.datamgmt.states import get_notes_state
  45. from app.iris_engine.module_handler.module_handler import call_modules_hook
  46. from app.iris_engine.utils.tracker import track_activity
  47. from app.models.models import Notes
  48. from app.models.authorization import CaseAccessLevel
  49. from app.schema.marshables import CaseNoteDirectorySchema
  50. from app.schema.marshables import CaseNoteRevisionSchema
  51. from app.schema.marshables import CaseNoteSchema
  52. from app.schema.marshables import CommentSchema
  53. from app.blueprints.access_controls import ac_requires_case_identifier
  54. from app.blueprints.access_controls import ac_api_requires
  55. from app.blueprints.rest.endpoints import endpoint_removed
  56. from app.blueprints.responses import response_error
  57. from app.blueprints.responses import response_success
  58. case_notes_rest_blueprint = Blueprint('case_notes_rest', __name__)
  59. @case_notes_rest_blueprint.route('/case/notes/<int:cur_id>', methods=['GET'])
  60. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  61. @ac_api_requires()
  62. def case_note_detail(cur_id, caseid):
  63. """
  64. Returns a note and its comments
  65. ---
  66. tags:
  67. - Case Notes
  68. parameters:
  69. - name: cur_id
  70. in: path
  71. description: Note ID
  72. type: integer
  73. required: true
  74. - name: caseid
  75. in: path
  76. description: Case ID
  77. type: integer
  78. required: true
  79. responses:
  80. 200:
  81. description: Note and its comments
  82. schema:
  83. $ref: '#/definitions/CaseNoteSchema'
  84. 400:
  85. description: Data error
  86. """
  87. try:
  88. note = get_note(cur_id, caseid=caseid)
  89. if not note:
  90. return response_error(msg="Invalid note ID")
  91. note_comments = get_case_note_comments(cur_id)
  92. note_schema = CaseNoteSchema()
  93. comments_schema = CommentSchema(many=True)
  94. note = note_schema.dump(note)
  95. note['comments'] = comments_schema.dump(note_comments)
  96. return response_success(data=note)
  97. except marshmallow.exceptions.ValidationError as e:
  98. return response_error(msg="Data error", data=e.messages)
  99. @case_notes_rest_blueprint.route('/case/notes/delete/<int:cur_id>', methods=['POST'])
  100. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  101. @ac_api_requires()
  102. def case_note_delete(cur_id, caseid):
  103. call_modules_hook('on_preload_note_delete', data=cur_id, caseid=caseid)
  104. note = get_note(cur_id, caseid)
  105. if not note:
  106. return response_error("Invalid note ID for this case")
  107. try:
  108. delete_note(cur_id, caseid)
  109. except Exception as e:
  110. return response_error("Unable to remove note", data=e.__traceback__)
  111. call_modules_hook('on_postload_note_delete', data=cur_id, caseid=caseid)
  112. track_activity(f"deleted note \"{note.note_title}\"", caseid=caseid)
  113. return response_success(f"Note deleted {cur_id}")
  114. @case_notes_rest_blueprint.route('/case/notes/update/<int:cur_id>', methods=['POST'])
  115. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  116. @ac_api_requires()
  117. def case_note_save(cur_id, caseid):
  118. addnote_schema = CaseNoteSchema()
  119. try:
  120. note = notes_update(identifier=cur_id, request_json=request.get_json(), case_identifier=caseid)
  121. return response_success(f"Note ID {cur_id} saved", data=addnote_schema.dump(note))
  122. except BusinessProcessingError as e:
  123. return response_error(e.get_message(), data=e.get_data())
  124. @case_notes_rest_blueprint.route('/case/notes/<int:cur_id>/revisions/list', methods=['GET'])
  125. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  126. @ac_api_requires()
  127. def case_note_list_history(cur_id, caseid):
  128. note_version_sc = CaseNoteRevisionSchema(many=True)
  129. try:
  130. note_version = notes_list_revisions(identifier=cur_id, case_identifier=caseid)
  131. return response_success("ok", data=note_version_sc.dump(note_version))
  132. except BusinessProcessingError as e:
  133. return response_error(e.get_message(), data=e.get_data())
  134. @case_notes_rest_blueprint.route('/case/notes/<int:cur_id>/revisions/<int:revision_id>', methods=['GET'])
  135. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  136. @ac_api_requires()
  137. def case_note_revision(cur_id, revision_id, caseid):
  138. note_version_sc = CaseNoteRevisionSchema()
  139. try:
  140. note_version = notes_get_revision(identifier=cur_id,
  141. revision_number=revision_id,
  142. case_identifier=caseid)
  143. return response_success("ok", data=note_version_sc.dump(note_version))
  144. except BusinessProcessingError as e:
  145. return response_error(e.get_message(), data=e.get_data())
  146. @case_notes_rest_blueprint.route('/case/notes/<int:cur_id>/revisions/<int:revision_id>/delete', methods=['POST'])
  147. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  148. @ac_api_requires()
  149. def case_note_revision_delete(cur_id, revision_id, caseid):
  150. try:
  151. notes_delete_revision(identifier=cur_id,
  152. revision_number=revision_id,
  153. case_identifier=caseid)
  154. return response_success(f"Revision {revision_id} of note {cur_id} deleted")
  155. except BusinessProcessingError as e:
  156. return response_error(e.get_message(), data=e.get_data())
  157. @case_notes_rest_blueprint.route('/case/notes/add', methods=['POST'])
  158. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  159. @ac_api_requires()
  160. def case_note_add(caseid):
  161. addnote_schema = CaseNoteSchema()
  162. try:
  163. note = notes_create(request_json=request.get_json(), case_identifier=caseid)
  164. return response_success(f"Note ID {note.note_id} created", data=addnote_schema.dump(note))
  165. except BusinessProcessingError as e:
  166. return response_error(e.get_message(), data=e.get_data())
  167. @case_notes_rest_blueprint.route('/case/notes/directories/add', methods=['POST'])
  168. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  169. @ac_api_requires()
  170. def case_directory_add(caseid):
  171. try:
  172. directory_schema = CaseNoteDirectorySchema()
  173. request_data = request.get_json()
  174. if request_data.get('parent_id') is not None:
  175. directory_schema.verify_parent_id(request_data['parent_id'],
  176. case_id=caseid)
  177. request_data.pop('id', None)
  178. request_data['case_id'] = caseid
  179. new_directory = directory_schema.load(request_data)
  180. db.session.add(new_directory)
  181. db.session.commit()
  182. track_activity(f"added directory \"{new_directory.name}\"", caseid=caseid)
  183. return response_success('Directory added', data=directory_schema.dump(new_directory))
  184. except marshmallow.exceptions.ValidationError as e:
  185. return response_error(msg="Data error", data=e.messages)
  186. @case_notes_rest_blueprint.route('/case/notes/directories/update/<dir_id>', methods=['POST'])
  187. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  188. @ac_api_requires()
  189. def case_directory_update(dir_id, caseid):
  190. try:
  191. directory = get_directory(dir_id, caseid)
  192. if not directory:
  193. return response_error(msg="Invalid directory ID")
  194. directory_schema = CaseNoteDirectorySchema()
  195. request_data = request.get_json()
  196. request_data['case_id'] = caseid
  197. if request_data.get('parent_id') is not None:
  198. directory_schema.verify_parent_id(request_data['parent_id'],
  199. case_id=caseid,
  200. current_id=dir_id)
  201. new_directory = directory_schema.load(request_data, instance=directory, partial=True)
  202. db.session.commit()
  203. track_activity(f"modified directory \"{new_directory.name}\"", caseid=caseid)
  204. return response_success('Directory modified', data=directory_schema.dump(new_directory))
  205. except marshmallow.exceptions.ValidationError as e:
  206. return response_error(msg="Data error", data=e.messages)
  207. except Exception as e:
  208. app.logger.exception(f"Failed to update directory: {e}")
  209. return response_error(msg="Internal error", status=500)
  210. @case_notes_rest_blueprint.route('/case/notes/directories/delete/<dir_id>', methods=['POST'])
  211. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  212. @ac_api_requires()
  213. def case_directory_delete(dir_id, caseid):
  214. try:
  215. directory = get_directory(dir_id, caseid)
  216. if not directory:
  217. return response_error(msg="Invalid directory ID")
  218. # Proceed to delete directory, but remove all associated notes and subdirectories recursively
  219. has_succeed = delete_directory(directory, caseid)
  220. if has_succeed:
  221. track_activity(f"deleted directory \"{directory.name}\"", caseid=caseid)
  222. return response_success('Directory deleted')
  223. return response_error('Unable to delete directory')
  224. except marshmallow.exceptions.ValidationError as e:
  225. return response_error(msg="Data error", data=e.messages)
  226. @case_notes_rest_blueprint.route('/case/notes/groups/list', methods=['GET'])
  227. @endpoint_removed('Use /case/notes/directories/filter', 'v2.4.0')
  228. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  229. @ac_api_requires()
  230. def case_load_notes_groups(caseid):
  231. pass
  232. @case_notes_rest_blueprint.route('/case/notes/state', methods=['GET'])
  233. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  234. @ac_api_requires()
  235. def case_notes_state(caseid):
  236. os = get_notes_state(caseid=caseid)
  237. if os:
  238. return response_success(data=os)
  239. return response_error('No notes state for this case.')
  240. @case_notes_rest_blueprint.route('/case/notes/search', methods=['GET', 'POST'])
  241. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  242. @ac_api_requires()
  243. def case_search_notes(caseid):
  244. search_input = request.args.get('search_input')
  245. notes = Notes.query.filter(
  246. and_(Notes.note_case_id == caseid,
  247. or_(Notes.note_title.ilike(f'%{search_input}%'),
  248. Notes.note_content.ilike(f'%{search_input}%')))
  249. ).all()
  250. note_schema = CaseNoteSchema(many=True)
  251. serialized_notes = note_schema.dump(notes)
  252. return response_success(data=serialized_notes)
  253. @case_notes_rest_blueprint.route('/case/notes/groups/add', methods=['POST'])
  254. @endpoint_removed('Use /case/notes/directories/add', 'v2.4.0')
  255. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  256. @ac_api_requires()
  257. def case_add_notes_groups(caseid):
  258. pass
  259. @case_notes_rest_blueprint.route('/case/notes/groups/delete/<int:cur_id>', methods=['POST'])
  260. @endpoint_removed('Use /case/notes/directories/delete/<ID>', 'v2.4.0')
  261. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  262. @ac_api_requires()
  263. def case_delete_notes_groups(cur_id, caseid):
  264. pass
  265. @case_notes_rest_blueprint.route('/case/notes/groups/<int:cur_id>', methods=['GET'])
  266. @endpoint_removed('Use /case/notes/directories/<ID>', 'v2.4.0')
  267. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  268. @ac_api_requires()
  269. def case_get_notes_group(cur_id, caseid):
  270. pass
  271. @case_notes_rest_blueprint.route('/case/notes/directories/filter', methods=['GET'])
  272. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  273. @ac_api_requires()
  274. def case_filter_notes_directories(caseid):
  275. if not get_case(caseid=caseid):
  276. return response_error("Invalid case ID")
  277. directories = get_directories_with_note_count(caseid)
  278. return response_success("", data=directories)
  279. @case_notes_rest_blueprint.route('/case/notes/groups/update/<int:cur_id>', methods=['POST'])
  280. @endpoint_removed('Use /case/notes/directories/update/<ID>', 'v2.4.0')
  281. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  282. @ac_api_requires()
  283. def case_edit_notes_groups(cur_id, caseid):
  284. pass
  285. @case_notes_rest_blueprint.route('/case/notes/<int:cur_id>/comments/list', methods=['GET'])
  286. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  287. @ac_api_requires()
  288. def case_comment_note_list(cur_id, caseid):
  289. note_comments = get_case_note_comments(cur_id)
  290. if note_comments is None:
  291. return response_error('Invalid note ID')
  292. return response_success(data=CommentSchema(many=True).dump(note_comments))
  293. @case_notes_rest_blueprint.route('/case/notes/<int:cur_id>/comments/add', methods=['POST'])
  294. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  295. @ac_api_requires()
  296. def case_comment_note_add(cur_id, caseid):
  297. try:
  298. note = get_note(cur_id, caseid=caseid)
  299. if not note:
  300. return response_error('Invalid note ID')
  301. comment_schema = CommentSchema()
  302. comment = comment_schema.load(request.get_json())
  303. comment.comment_case_id = caseid
  304. comment.comment_user_id = current_user.id
  305. comment.comment_date = datetime.now()
  306. comment.comment_update_date = datetime.now()
  307. db.session.add(comment)
  308. db.session.commit()
  309. add_comment_to_note(note.note_id, comment.comment_id)
  310. db.session.commit()
  311. hook_data = {
  312. "comment": comment_schema.dump(comment),
  313. "note": CaseNoteSchema().dump(note)
  314. }
  315. call_modules_hook('on_postload_note_commented', data=hook_data, caseid=caseid)
  316. track_activity("note \"{}\" commented".format(note.note_title), caseid=caseid)
  317. return response_success("Note commented", data=comment_schema.dump(comment))
  318. except marshmallow.exceptions.ValidationError as e:
  319. return response_error(msg="Data error", data=e.normalized_messages())
  320. @case_notes_rest_blueprint.route('/case/notes/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
  321. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  322. @ac_api_requires()
  323. def case_comment_note_get(cur_id, com_id, caseid):
  324. comment = get_case_note_comment(cur_id, com_id)
  325. if not comment:
  326. return response_error("Invalid comment ID")
  327. return response_success(data=comment._asdict())
  328. @case_notes_rest_blueprint.route('/case/notes/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
  329. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  330. @ac_api_requires()
  331. def case_comment_note_edit(cur_id, com_id, caseid):
  332. return case_comment_update(com_id, 'notes', caseid)
  333. @case_notes_rest_blueprint.route('/case/notes/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
  334. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  335. @ac_api_requires()
  336. def case_comment_note_delete(cur_id, com_id, caseid):
  337. success, msg = delete_note_comment(cur_id, com_id)
  338. if not success:
  339. return response_error(msg)
  340. call_modules_hook('on_postload_note_comment_delete', data=com_id, caseid=caseid)
  341. track_activity(f"comment {com_id} on note {cur_id} deleted", caseid=caseid)
  342. return response_success(msg)