Skip to content

Commit 2419b1c

Browse files
author
whitekernel
committed
[ADD] Mising v2 DS file
1 parent 3efae01 commit 2419b1c

1 file changed

Lines changed: 159 additions & 0 deletions

File tree

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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+
19+
import base64
20+
import marshmallow.exceptions
21+
from flask import Blueprint
22+
from flask import request
23+
from flask import send_file
24+
from pathlib import Path
25+
26+
from app.blueprints.access_controls import ac_api_requires
27+
from app.blueprints.access_controls import ac_fast_check_current_user_has_case_access
28+
from app.blueprints.access_controls import ac_api_return_access_denied
29+
from app.blueprints.rest.endpoints import response_api_created
30+
from app.blueprints.rest.endpoints import response_api_error
31+
from app.blueprints.rest.endpoints import response_api_not_found
32+
from app.business.cases import cases_exists
33+
from app.datamgmt.datastore.datastore_db import datastore_get_file
34+
from app.datamgmt.datastore.datastore_db import datastore_get_interactive_path_node
35+
from app.datamgmt.datastore.datastore_db import datastore_get_local_file_path
36+
from app.iris_engine.utils.tracker import track_activity
37+
from app.models.authorization import CaseAccessLevel
38+
from app.schema.marshables import DSFileSchema
39+
40+
41+
class DatastoreOperations:
42+
"""Case-scoped datastore file operations used by the rich-text editor
43+
(inline image uploads embedded in notes and case summaries).
44+
45+
Only the two endpoints the editor actually needs are exposed here:
46+
the interactive upload and the file view. The broader tree / folder
47+
management still lives on the legacy `datastore_rest_blueprint` until
48+
that UI is migrated too.
49+
"""
50+
51+
@staticmethod
52+
def _check_case_access(case_identifier, access_levels):
53+
if not cases_exists(case_identifier):
54+
return response_api_not_found()
55+
56+
if not ac_fast_check_current_user_has_case_access(case_identifier, access_levels):
57+
return ac_api_return_access_denied(caseid=case_identifier)
58+
59+
return None
60+
61+
def add_interactive(self, case_identifier):
62+
access_error = self._check_case_access(case_identifier, [CaseAccessLevel.full_access])
63+
if access_error:
64+
return access_error
65+
66+
dsp = datastore_get_interactive_path_node(case_identifier)
67+
if not dsp:
68+
return response_api_error('Invalid path node for this case')
69+
70+
dsf_schema = DSFileSchema()
71+
try:
72+
js_data = request.get_json() or {}
73+
74+
try:
75+
file_content = base64.b64decode(js_data.get('file_content'))
76+
filename = js_data.get('file_original_name')
77+
except Exception as e:
78+
return response_api_error(str(e))
79+
80+
if not filename:
81+
return response_api_error('Data error', data={'file_original_name': ['Missing filename']})
82+
83+
dsf_sc, existed = dsf_schema.ds_store_file_b64(filename, file_content, dsp, case_identifier)
84+
85+
track_activity(
86+
f'File "{dsf_sc.file_original_name}" added to DS',
87+
caseid=case_identifier
88+
)
89+
90+
# URL is returned relative to /api/v2 so the frontend can embed it
91+
# directly as an <img src>. It's a REST path the browser can GET
92+
# without any further rewriting — `cid` is part of the path.
93+
return response_api_created({
94+
'existed': existed,
95+
'file_url': f'/api/v2/cases/{case_identifier}/datastore/files/{dsf_sc.file_id}',
96+
**dsf_schema.dump(dsf_sc)
97+
})
98+
99+
except marshmallow.exceptions.ValidationError as e:
100+
return response_api_error('Data error', data=e.messages)
101+
102+
def view(self, case_identifier, identifier):
103+
access_error = self._check_case_access(
104+
case_identifier,
105+
[CaseAccessLevel.read_only, CaseAccessLevel.full_access]
106+
)
107+
if access_error:
108+
return access_error
109+
110+
# Sanity-check that the file actually belongs to this case before
111+
# resolving its on-disk location. `datastore_get_local_file_path`
112+
# already filters by case id, but this keeps 404s consistent with
113+
# the rest of the v2 case routes.
114+
if not datastore_get_file(identifier, case_identifier):
115+
return response_api_not_found()
116+
117+
has_error, dsf = datastore_get_local_file_path(identifier, case_identifier)
118+
if has_error:
119+
return response_api_error('Unable to get requested file ID', data=dsf)
120+
121+
if dsf.file_is_ioc or dsf.file_password:
122+
destination_name = dsf.file_original_name + '.zip'
123+
else:
124+
destination_name = dsf.file_original_name
125+
126+
if not Path(dsf.file_local_name).is_file():
127+
return response_api_error(
128+
f'File {dsf.file_local_name} does not exist on the server. '
129+
f'Update or delete virtual entry'
130+
)
131+
132+
resp = send_file(dsf.file_local_name, as_attachment=False, download_name=destination_name)
133+
134+
track_activity(
135+
f'File "{destination_name}" downloaded',
136+
caseid=case_identifier,
137+
display_in_ui=False
138+
)
139+
return resp
140+
141+
142+
datastore_operations = DatastoreOperations()
143+
case_datastore_blueprint = Blueprint(
144+
'case_datastore_rest_v2',
145+
__name__,
146+
url_prefix='/<int:case_identifier>/datastore'
147+
)
148+
149+
150+
@case_datastore_blueprint.post('/files/interactive')
151+
@ac_api_requires()
152+
def datastore_add_interactive_file(case_identifier):
153+
return datastore_operations.add_interactive(case_identifier)
154+
155+
156+
@case_datastore_blueprint.get('/files/<int:identifier>')
157+
@ac_api_requires()
158+
def datastore_view_file(case_identifier, identifier):
159+
return datastore_operations.view(case_identifier, identifier)

0 commit comments

Comments
 (0)