Skip to content

Commit c603fae

Browse files
authored
Merge pull request #160 from emmanvg/files-endpoint-fix
Fixes for API Endpoint (#159)
2 parents dedc48c + 20f8788 commit c603fae

2 files changed

Lines changed: 67 additions & 21 deletions

File tree

tests/test_api.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ def test_edit_notes(self, mock_handler):
283283

284284
args, kwargs = mock_handler.edit_note.call_args_list[0]
285285
self.assertEqual(args[0], '114d70ba7d04c76d8c217c970f99682025c89b1a6ffe91eb9045653b4b954eb9')
286-
self.assertEqual(args[1], '1')
286+
self.assertEqual(args[1], 1)
287287
self.assertEqual(args[2], 'bar')
288288

289289
@mock.patch('api.handler')
@@ -292,4 +292,46 @@ def test_remove_notes(self, mock_handler):
292292

293293
args, kwargs = mock_handler.delete_note.call_args_list[0]
294294
self.assertEqual(args[0], '114d70ba7d04c76d8c217c970f99682025c89b1a6ffe91eb9045653b4b954eb9')
295-
self.assertEqual(args[1], '1')
295+
self.assertEqual(args[1], 1)
296+
297+
298+
class TestSHA256DownloadSampleCase(APITestCase):
299+
def setUp(self):
300+
super(self.__class__, self).setUp()
301+
# populate the DB w/ a task
302+
post_file(self.app)
303+
self.sql_db.update_task(
304+
task_id=1,
305+
task_status='Complete',
306+
)
307+
308+
@mock.patch('api.db')
309+
@mock.patch('api.handler')
310+
def test_malformed_request(self, mock_handler, mock_db):
311+
resp = self.app.get('/api/v1/files/..\opt\multiscanner\web_config.ini')
312+
313+
self.assertEqual(resp.status_code, api.HTTP_BAD_REQUEST)
314+
315+
@mock.patch('api.db')
316+
@mock.patch('api.handler')
317+
def test_other_hash(self, mock_handler, mock_db):
318+
# using MD5 instead of SHA256
319+
resp = self.app.get('/api/v1/files/96b47da202ddba8d7a6b91fecbf89a41')
320+
321+
self.assertEqual(resp.status_code, api.HTTP_BAD_REQUEST)
322+
323+
@mock.patch('api.db')
324+
@mock.patch('api.handler')
325+
def test_file_download_raw(self, mock_handler, mock_db):
326+
expected_response = b'my file contents'
327+
resp = self.app.get('/api/v1/files/114d70ba7d04c76d8c217c970f99682025c89b1a6ffe91eb9045653b4b954eb9?raw=t')
328+
329+
self.assertEqual(resp.status_code, api.HTTP_OK)
330+
self.assertEqual(resp.get_data(), expected_response)
331+
332+
@mock.patch('api.db')
333+
@mock.patch('api.handler')
334+
def test_file_not_found(self, mock_handler, mock_db):
335+
resp = self.app.get('/api/v1/files/26d11f0ea5cc77a59b6e47deee859440f26d2d14440beb712dbac8550d35ef1f?raw=t')
336+
337+
self.assertEqual(resp.status_code, api.HTTP_NOT_FOUND)

utils/api.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import multiprocessing
4848
import os
4949
import queue
50+
import re
5051
import shutil
5152
import subprocess
5253
import sys
@@ -56,7 +57,7 @@
5657

5758
import rarfile
5859
import requests
59-
from flask import Flask, abort, jsonify, make_response, request
60+
from flask import Flask, abort, jsonify, make_response, request, safe_join
6061
from flask.json import JSONEncoder
6162
from flask_cors import CORS
6263
from jinja2 import Markup
@@ -541,7 +542,7 @@ def create_task():
541542
)
542543

543544

544-
@app.route('/api/v1/tasks/<task_id>/report', methods=['GET'])
545+
@app.route('/api/v1/tasks/<int:task_id>/report', methods=['GET'])
545546
def get_report(task_id):
546547
'''
547548
Return a JSON dictionary corresponding
@@ -572,7 +573,7 @@ def _pre_process(report_dict={}):
572573
executed on report_dict.
573574
'''
574575

575-
# pop unecessary keys
576+
# pop unnecessary keys
576577
if report_dict.get('Report', {}).get('ssdeep', {}):
577578
for k in ['chunksize', 'chunk', 'double_chunk']:
578579
try:
@@ -628,7 +629,7 @@ def _linkify(s, url, new_tab=True):
628629
s=s)
629630

630631

631-
@app.route('/api/v1/tasks/<task_id>/file', methods=['GET'])
632+
@app.route('/api/v1/tasks/<int:task_id>/file', methods=['GET'])
632633
def files_get_task(task_id):
633634
# try to get report dict
634635
report_dict, success = get_report_dict(task_id)
@@ -645,7 +646,7 @@ def files_get_task(task_id):
645646
return jsonify({'Error': 'sha256 not in report!'})
646647

647648

648-
@app.route('/api/v1/tasks/<task_id>/maec', methods=['GET'])
649+
@app.route('/api/v1/tasks/<int:task_id>/maec', methods=['GET'])
649650
def get_maec_report(task_id):
650651
# try to get report dict
651652
report_dict, success = get_report_dict(task_id)
@@ -695,7 +696,7 @@ def taglist():
695696
return jsonify({'Tags': response})
696697

697698

698-
@app.route('/api/v1/tasks/<task_id>/tags', methods=['POST', 'DELETE'])
699+
@app.route('/api/v1/tasks/<int:task_id>/tags', methods=['POST', 'DELETE'])
699700
def tags(task_id):
700701
'''
701702
Add/Remove the specified tag to the specified task.
@@ -719,7 +720,7 @@ def tags(task_id):
719720
return jsonify({'Message': 'Tag Removed'})
720721

721722

722-
@app.route('/api/v1/tasks/<task_id>/notes', methods=['GET'])
723+
@app.route('/api/v1/tasks/<int:task_id>/notes', methods=['GET'])
723724
def get_notes(task_id):
724725
'''
725726
Get one or more analyst notes/comments associated with the specified task.
@@ -749,7 +750,7 @@ def get_notes(task_id):
749750
return jsonify(response)
750751

751752

752-
@app.route('/api/v1/tasks/<task_id>/notes', methods=['POST'])
753+
@app.route('/api/v1/tasks/<int:task_id>/notes', methods=['POST'])
753754
def add_note(task_id):
754755
'''
755756
Add an analyst note/comment to the specified task.
@@ -764,7 +765,7 @@ def add_note(task_id):
764765
return jsonify(response)
765766

766767

767-
@app.route('/api/v1/tasks/<task_id>/notes/<note_id>', methods=['PUT', 'DELETE'])
768+
@app.route('/api/v1/tasks/<int:task_id>/notes/<int:note_id>', methods=['PUT', 'DELETE'])
768769
def edit_note(task_id, note_id):
769770
'''
770771
Modify/remove the specified analyst note/comment.
@@ -784,7 +785,7 @@ def edit_note(task_id, note_id):
784785
return jsonify(response)
785786

786787

787-
@app.route('/api/v1/files/<sha256>', methods=['GET'])
788+
@app.route('/api/v1/files/<string:sha256>', methods=['GET'])
788789
# get raw file - /api/v1/files/get/<sha256>?raw=true
789790
def files_get_sha256(sha256):
790791
'''
@@ -793,18 +794,21 @@ def files_get_sha256(sha256):
793794
# is there a robust way to just get this as a bool?
794795
raw = request.args.get('raw', default='False', type=str)
795796

796-
return files_get_sha256_helper(sha256, raw)
797+
if re.match(r'^[a-fA-F0-9]{64}$', sha256):
798+
return files_get_sha256_helper(sha256, raw)
799+
else:
800+
return abort(HTTP_BAD_REQUEST)
797801

798802

799803
def files_get_sha256_helper(sha256, raw=None):
800804
'''
801805
Returns binary from storage. Defaults to password protected zipfile.
802806
'''
803-
file_path = os.path.join(api_config['api']['upload_folder'], sha256)
807+
file_path = safe_join(api_config['api']['upload_folder'], sha256)
804808
if not os.path.exists(file_path):
805809
abort(HTTP_NOT_FOUND)
806810

807-
with open(file_path, "rb") as fh:
811+
with open(file_path, 'rb') as fh:
808812
fh_content = fh.read()
809813

810814
raw = raw[0].lower()
@@ -816,13 +820,13 @@ def files_get_sha256_helper(sha256, raw=None):
816820
else:
817821
# ref: https://github.com/crits/crits/crits/core/data_tools.py#L122
818822
rawname = sha256 + '.bin'
819-
with open(os.path.join('/tmp/', rawname), 'wb') as raw_fh:
823+
with open(safe_join('/tmp/', rawname), 'wb') as raw_fh:
820824
raw_fh.write(fh_content)
821825

822826
zipname = sha256 + '.zip'
823827
args = ['/usr/bin/zip', '-j',
824-
os.path.join('/tmp', zipname),
825-
os.path.join('/tmp', rawname),
828+
safe_join('/tmp', zipname),
829+
safe_join('/tmp', rawname),
826830
'-P', 'infected']
827831
proc = subprocess.Popen(args)
828832
wait_seconds = 30
@@ -836,7 +840,7 @@ def files_get_sha256_helper(sha256, raw=None):
836840
proc.terminate()
837841
return make_response(jsonify({'Error': 'Process timed out'}))
838842
else:
839-
with open(os.path.join('/tmp', zipname), 'rb') as zip_fh:
843+
with open(safe_join('/tmp', zipname), 'rb') as zip_fh:
840844
zip_data = zip_fh.read()
841845
if len(zip_data) == 0:
842846
return make_response(jsonify({'Error': 'Zip file empty'}))
@@ -881,7 +885,7 @@ def run_ssdeep_group():
881885
HTTP_BAD_REQUEST)
882886

883887

884-
@app.route('/api/v1/tasks/<task_id>/pdf', methods=['GET'])
888+
@app.route('/api/v1/tasks/<int:task_id>/pdf', methods=['GET'])
885889
def get_pdf_report(task_id):
886890
'''
887891
Generates a PDF version of a JSON report.
@@ -898,7 +902,7 @@ def get_pdf_report(task_id):
898902
return response
899903

900904

901-
@app.route('/api/v1/tasks/<task_id>/stix2', methods=['GET'])
905+
@app.route('/api/v1/tasks/<int:task_id>/stix2', methods=['GET'])
902906
def get_stix2_bundle_from_report(task_id):
903907
'''
904908
Generates a STIX2 Bundle with indicators generated of a JSON report.

0 commit comments

Comments
 (0)