Açıklama Yok

case_assets_routes.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  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 csv
  19. from datetime import datetime
  20. import marshmallow
  21. from flask import Blueprint
  22. from flask import request
  23. from flask_login import current_user
  24. from app import db
  25. from app.blueprints.rest.case_comments import case_comment_update
  26. from app.blueprints.rest.endpoints import endpoint_deprecated
  27. from app.business.assets import assets_delete
  28. from app.business.assets import assets_create
  29. from app.business.assets import assets_get_detailed
  30. from app.business.assets import assets_get
  31. from app.business.assets import assets_update
  32. from app.business.errors import BusinessProcessingError
  33. from app.datamgmt.case.case_assets_db import get_raw_assets
  34. from app.datamgmt.case.case_assets_db import add_comment_to_asset
  35. from app.datamgmt.case.case_assets_db import create_asset
  36. from app.datamgmt.case.case_assets_db import delete_asset_comment
  37. from app.datamgmt.case.case_assets_db import get_asset
  38. from app.datamgmt.case.case_assets_db import get_asset_type_id
  39. from app.datamgmt.case.case_assets_db import get_assets
  40. from app.datamgmt.case.case_assets_db import get_assets_ioc_links
  41. from app.datamgmt.case.case_assets_db import get_case_asset_comment
  42. from app.datamgmt.case.case_assets_db import get_case_asset_comments
  43. from app.datamgmt.case.case_assets_db import get_similar_assets
  44. from app.datamgmt.case.case_db import get_case_client_id
  45. from app.datamgmt.manage.manage_attribute_db import get_default_custom_attributes
  46. from app.datamgmt.manage.manage_users_db import get_user_cases_fast
  47. from app.datamgmt.states import get_assets_state
  48. from app.iris_engine.access_control.utils import ac_fast_check_current_user_has_case_access
  49. from app.iris_engine.module_handler.module_handler import call_modules_hook
  50. from app.iris_engine.utils.tracker import track_activity
  51. from app.models.models import AnalysisStatus
  52. from app.models.authorization import CaseAccessLevel
  53. from app.schema.marshables import CaseAssetsSchema
  54. from app.schema.marshables import CommentSchema
  55. from app.blueprints.access_controls import ac_requires_case_identifier
  56. from app.blueprints.access_controls import ac_api_requires
  57. from app.blueprints.responses import response_error
  58. from app.blueprints.responses import response_success
  59. from app.blueprints.access_controls import ac_api_return_access_denied
  60. case_assets_rest_blueprint = Blueprint('case_assets_rest', __name__)
  61. @case_assets_rest_blueprint.route('/case/assets/filter', methods=['GET'])
  62. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  63. def case_filter_assets(caseid):
  64. """
  65. Returns the list of assets from the case.
  66. :return: A JSON object containing the assets of the case, enhanced with assets seen on other cases.
  67. """
  68. # Get all assets objects from the case and the customer id
  69. ret = {}
  70. assets = CaseAssetsSchema().dump(get_raw_assets(caseid), many=True)
  71. customer_id = get_case_client_id(caseid)
  72. ioc_links_req = get_assets_ioc_links(caseid)
  73. cache_ioc_link = {}
  74. for ioc in ioc_links_req:
  75. if ioc.asset_id not in cache_ioc_link:
  76. cache_ioc_link[ioc.asset_id] = [ioc._asdict()]
  77. else:
  78. cache_ioc_link[ioc.asset_id].append(ioc._asdict())
  79. cases_access = get_user_cases_fast(current_user.id)
  80. for a in assets:
  81. a['ioc_links'] = cache_ioc_link.get(a['asset_id'])
  82. if len(assets) < 300:
  83. # Find similar assets from other cases with the same customer
  84. a['link'] = list(get_similar_assets(
  85. a['asset_name'], a['asset_type_id'], caseid, customer_id, cases_access))
  86. else:
  87. a['link'] = []
  88. ret['assets'] = assets
  89. ret['state'] = get_assets_state(caseid)
  90. return response_success("", data=ret)
  91. @case_assets_rest_blueprint.route('/case/assets/list', methods=['GET'])
  92. @endpoint_deprecated('GET', '/api/v2/cases/{case_identifier}/assets')
  93. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  94. def case_list_assets(caseid):
  95. """
  96. Returns the list of assets from the case.
  97. :return: A JSON object containing the assets of the case, enhanced with assets seen on other cases.
  98. """
  99. # Get all assets objects from the case and the customer id
  100. assets = get_assets(caseid)
  101. customer_id = get_case_client_id(caseid)
  102. ret = {'assets': []}
  103. ioc_links_req = get_assets_ioc_links(caseid)
  104. cache_ioc_link = {}
  105. for ioc in ioc_links_req:
  106. if ioc.asset_id not in cache_ioc_link:
  107. cache_ioc_link[ioc.asset_id] = [ioc._asdict()]
  108. else:
  109. cache_ioc_link[ioc.asset_id].append(ioc._asdict())
  110. cases_access = get_user_cases_fast(current_user.id)
  111. for asset in assets:
  112. asset = asset._asdict()
  113. if len(assets) < 300:
  114. # Find similar assets from other cases with the same customer
  115. asset['link'] = list(get_similar_assets(
  116. asset['asset_name'], asset['asset_type_id'], caseid, customer_id, cases_access))
  117. else:
  118. asset['link'] = []
  119. asset['ioc_links'] = cache_ioc_link.get(asset['asset_id'])
  120. ret['assets'].append(asset)
  121. ret['state'] = get_assets_state(caseid)
  122. return response_success("", data=ret)
  123. @case_assets_rest_blueprint.route('/case/assets/state', methods=['GET'])
  124. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  125. @ac_api_requires()
  126. def case_assets_state(caseid):
  127. os = get_assets_state(caseid)
  128. if os:
  129. return response_success(data=os)
  130. return response_error('No assets state for this case.')
  131. @case_assets_rest_blueprint.route('/case/assets/add', methods=['POST'])
  132. @endpoint_deprecated('POST', '/api/v2/cases/<int:case_identifier>/assets')
  133. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  134. @ac_api_requires()
  135. def deprecated_add_asset(caseid):
  136. asset_schema = CaseAssetsSchema()
  137. try:
  138. msg, asset = assets_create(caseid, request.get_json())
  139. return response_success(msg, asset_schema.dump(asset))
  140. except BusinessProcessingError as e:
  141. return response_error(e.get_message(), e.get_data())
  142. @case_assets_rest_blueprint.route('/case/assets/upload', methods=['POST'])
  143. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  144. @ac_api_requires()
  145. def case_upload_ioc(caseid):
  146. try:
  147. # validate before saving
  148. add_asset_schema = CaseAssetsSchema()
  149. jsdata = request.get_json()
  150. # get IOC list from request
  151. csv_lines = jsdata["CSVData"].splitlines() # unavoidable since the file is passed as a string
  152. headers = "asset_name,asset_type_name,asset_description,asset_ip,asset_domain,asset_tags"
  153. if csv_lines[0].lower() != headers:
  154. csv_lines.insert(0, headers)
  155. # convert list of strings into CSV
  156. csv_data = csv.DictReader(csv_lines, delimiter=',')
  157. ret = []
  158. errors = []
  159. analysis_status = AnalysisStatus.query.filter(AnalysisStatus.name == 'Unspecified').first()
  160. analysis_status_id = analysis_status.id
  161. index = 0
  162. for row in csv_data:
  163. missing_field = False
  164. for e in headers.split(','):
  165. if row.get(e) is None:
  166. errors.append(f"{e} is missing for row {index}")
  167. missing_field = True
  168. continue
  169. if missing_field:
  170. continue
  171. # Asset name must not be empty
  172. if not row.get("asset_name"):
  173. errors.append(f"Empty asset name for row {index}")
  174. track_activity("Attempted to upload an empty asset name")
  175. index += 1
  176. continue
  177. if row.get("asset_tags"):
  178. row["asset_tags"] = row.get("asset_tags").replace("|", ",") # Reformat Tags
  179. if not row.get('asset_type_name'):
  180. errors.append(f"Empty asset type for row {index}")
  181. track_activity("Attempted to upload an empty asset type")
  182. index += 1
  183. continue
  184. type_id = get_asset_type_id(row['asset_type_name'].lower())
  185. if not type_id:
  186. errors.append(f"{row.get('asset_name')} (invalid asset type: {row.get('asset_type_name')}) for row {index}")
  187. track_activity(f"Attempted to upload unrecognized asset type \"{row.get('asset_type_name')}\"")
  188. index += 1
  189. continue
  190. row['asset_type_id'] = type_id.asset_id
  191. row.pop('asset_type_name', None)
  192. row['analysis_status_id'] = analysis_status_id
  193. request_data = call_modules_hook('on_preload_asset_create', data=row, caseid=caseid)
  194. add_asset_schema.is_unique_for_cid(caseid, request_data)
  195. asset_sc = add_asset_schema.load(request_data)
  196. asset_sc.custom_attributes = get_default_custom_attributes('asset')
  197. asset = create_asset(asset=asset_sc,
  198. caseid=caseid,
  199. user_id=current_user.id
  200. )
  201. asset = call_modules_hook('on_postload_asset_create', data=asset, caseid=caseid)
  202. if not asset:
  203. errors.append('Unable to add asset for internal reason')
  204. index += 1
  205. continue
  206. ret.append(request_data)
  207. track_activity(f"added asset {asset.asset_name}", caseid=caseid)
  208. index += 1
  209. if len(errors) == 0:
  210. msg = "Successfully imported data."
  211. else:
  212. msg = "Data is imported but we got errors with the following rows:\n- " + "\n- ".join(errors)
  213. return response_success(msg=msg, data=ret)
  214. except marshmallow.exceptions.ValidationError as e:
  215. return response_error(msg='Data error', data=e.messages)
  216. @case_assets_rest_blueprint.route('/case/assets/<int:cur_id>', methods=['GET'])
  217. @endpoint_deprecated('GET', '/api/v2/cases/<int:case_identifier>/assets/<int:cur_id>')
  218. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  219. @ac_api_requires()
  220. def deprecated_asset_view(cur_id, caseid):
  221. try:
  222. asset = assets_get_detailed(cur_id)
  223. return response_success(msg='Asset added', data=asset)
  224. except BusinessProcessingError as e:
  225. return response_error(e.get_message())
  226. @case_assets_rest_blueprint.route('/case/assets/update/<int:cur_id>', methods=['POST'])
  227. @endpoint_deprecated('PUT', '/api/v2/cases/<int:case_identifier>/assets/<int:identifier>')
  228. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  229. @ac_api_requires()
  230. def asset_update(cur_id, caseid):
  231. try:
  232. asset = get_asset(cur_id)
  233. if not asset:
  234. return response_error("Invalid asset ID for this case")
  235. result = assets_update(asset, request.get_json())
  236. schema = CaseAssetsSchema()
  237. return response_success(f'Updated asset {result.asset_name}', schema.dump(result))
  238. except BusinessProcessingError as e:
  239. return response_error(e.get_message(), data=e.get_data())
  240. @case_assets_rest_blueprint.route('/case/assets/delete/<int:cur_id>', methods=['POST'])
  241. @endpoint_deprecated('DELETE', '/api/v2/cases/<int:case_identifier>/assets/<int:cur_id>')
  242. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  243. @ac_api_requires()
  244. def deprecated_asset_delete(cur_id, caseid):
  245. try:
  246. asset = assets_get(cur_id)
  247. if not ac_fast_check_current_user_has_case_access(asset.case_id, [CaseAccessLevel.full_access]):
  248. return ac_api_return_access_denied(caseid=asset.case_id)
  249. assets_delete(asset)
  250. return response_success('Deleted')
  251. except BusinessProcessingError as _:
  252. return response_error('Invalid asset ID for this case')
  253. @case_assets_rest_blueprint.route('/case/assets/<int:cur_id>/comments/list', methods=['GET'])
  254. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  255. @ac_api_requires()
  256. def case_comment_asset_list(cur_id, caseid):
  257. asset_comments = get_case_asset_comments(cur_id)
  258. if asset_comments is None:
  259. return response_error('Invalid asset ID')
  260. return response_success(data=CommentSchema(many=True).dump(asset_comments))
  261. @case_assets_rest_blueprint.route('/case/assets/<int:cur_id>/comments/add', methods=['POST'])
  262. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  263. @ac_api_requires()
  264. def case_comment_asset_add(cur_id, caseid):
  265. try:
  266. asset = get_asset(cur_id)
  267. if not asset:
  268. return response_error('Invalid asset ID')
  269. comment_schema = CommentSchema()
  270. comment = comment_schema.load(request.get_json())
  271. comment.comment_case_id = caseid
  272. comment.comment_user_id = current_user.id
  273. comment.comment_date = datetime.now()
  274. comment.comment_update_date = datetime.now()
  275. db.session.add(comment)
  276. db.session.commit()
  277. add_comment_to_asset(asset.asset_id, comment.comment_id)
  278. db.session.commit()
  279. hook_data = {
  280. "comment": comment_schema.dump(comment),
  281. "asset": CaseAssetsSchema().dump(asset)
  282. }
  283. call_modules_hook('on_postload_asset_commented', data=hook_data, caseid=caseid)
  284. track_activity(f"asset \"{asset.asset_name}\" commented", caseid=caseid)
  285. return response_success("Asset commented", data=comment_schema.dump(comment))
  286. except marshmallow.exceptions.ValidationError as e:
  287. return response_error(msg="Data error", data=e.normalized_messages())
  288. @case_assets_rest_blueprint.route('/case/assets/<int:cur_id>/comments/<int:com_id>', methods=['GET'])
  289. @ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
  290. @ac_api_requires()
  291. def case_comment_asset_get(cur_id, com_id, caseid):
  292. comment = get_case_asset_comment(cur_id, com_id)
  293. if not comment:
  294. return response_error("Invalid comment ID")
  295. return response_success(data=comment._asdict())
  296. @case_assets_rest_blueprint.route('/case/assets/<int:cur_id>/comments/<int:com_id>/edit', methods=['POST'])
  297. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  298. @ac_api_requires()
  299. def case_comment_asset_edit(cur_id, com_id, caseid):
  300. return case_comment_update(com_id, 'assets', caseid)
  301. @case_assets_rest_blueprint.route('/case/assets/<int:cur_id>/comments/<int:com_id>/delete', methods=['POST'])
  302. @ac_requires_case_identifier(CaseAccessLevel.full_access)
  303. @ac_api_requires()
  304. def case_comment_asset_delete(cur_id, com_id, caseid):
  305. success, msg = delete_asset_comment(cur_id, com_id, caseid)
  306. if not success:
  307. return response_error(msg)
  308. call_modules_hook('on_postload_asset_comment_delete', data=com_id, caseid=caseid)
  309. track_activity(f"comment {com_id} on asset {cur_id} deleted", caseid=caseid)
  310. return response_success(msg)