Skip to content

Commit 4342f8e

Browse files
authored
add file resubmission feature (#133)
1 parent daa8cf1 commit 4342f8e

File tree

14 files changed

+1467
-709
lines changed

14 files changed

+1467
-709
lines changed

app/poetry.lock

Lines changed: 781 additions & 657 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ requests = "^2.32.3"
2828
sqlalchemy = "^1.4.54"
2929
waitress = "^3.0.2"
3030
jinja2 = "^3.1.6"
31+
boto3 = "^1.40.2"
3132

3233
[tool.poetry.dev-dependencies]
3334

app/strelka_ui/blueprints/strelka.py

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111
from sqlalchemy.orm import joinedload, defer
1212

1313
from strelka_ui.database import db
14-
from strelka_ui.models import FileSubmission, User
14+
from strelka_ui.models import FileSubmission, User, get_request_id
1515
from strelka_ui.services.auth import auth_required
1616
from strelka_ui.services.files import (
1717
decrypt_file,
1818
check_file_size,
1919
convert_bytesio_to_filestorage,
2020
)
2121
from strelka_ui.services.strelka import get_db_status, get_frontend_status, submit_data
22+
from strelka_ui.services.s3 import upload_file, calculate_expires_at, is_s3_enabled, download_file, is_file_expired
2223
from strelka_ui.services.virustotal import (
2324
get_virustotal_positives,
2425
create_vt_zip_and_download,
@@ -339,6 +340,21 @@ def get_mimetype_priority(mime_list):
339340
# Get Strelka Submission IoCs
340341
# Store a set of IoCs in IoCs field
341342

343+
# Handle S3 upload if enabled
344+
s3_key = None
345+
s3_expires_at = None
346+
if is_s3_enabled():
347+
try:
348+
# Create temporary submission ID for S3 key generation
349+
temp_submission_id = get_request_id(response[0])
350+
success, s3_key, error_msg = upload_file(file, temp_submission_id)
351+
if success:
352+
s3_expires_at = calculate_expires_at()
353+
logging.info(f"Successfully uploaded file to S3: {s3_key}")
354+
else:
355+
logging.warning(f"Failed to upload file to S3: {error_msg}")
356+
except Exception as e:
357+
logging.error(f"Unexpected error during S3 upload: {e}")
342358

343359
# Create a new submission object and add it to the database.
344360
new_submission = FileSubmission(
@@ -350,7 +366,9 @@ def get_mimetype_priority(mime_list):
350366
request.headers.get("User-Agent"),
351367
user.id,
352368
submitted_description,
353-
submitted_at
369+
submitted_at,
370+
s3_key,
371+
s3_expires_at
354372
)
355373

356374
db.session.add(new_submission)
@@ -708,6 +726,105 @@ def check_vt_api_key():
708726
return jsonify({"apiKeyAvailable": api_key_exists}), 200
709727

710728

729+
@strelka.route("/resubmit/<submission_id>", methods=["POST"])
730+
@auth_required
731+
def resubmit_file(user: User, submission_id: str) -> Tuple[Response, int]:
732+
"""
733+
Resubmit a file from S3 storage for analysis.
734+
735+
Args:
736+
user: User object representing the authenticated user.
737+
submission_id: ID of the original submission to resubmit.
738+
739+
Returns:
740+
If successful, returns the new submission details and a 200 status code.
741+
If unsuccessful, returns an error message and appropriate status code.
742+
"""
743+
if not is_s3_enabled():
744+
return jsonify({
745+
"error": "File resubmission is not enabled",
746+
"details": "S3 storage is not configured or feature is disabled"
747+
}), 400
748+
749+
try:
750+
# Find the original submission
751+
original_submission = db.session.query(FileSubmission).filter_by(
752+
file_id=submission_id
753+
).first()
754+
755+
if not original_submission:
756+
return jsonify({
757+
"error": "Original submission not found",
758+
"details": f"Submission {submission_id} does not exist"
759+
}), 404
760+
761+
# Check if file has S3 key
762+
if not original_submission.s3_key:
763+
return jsonify({
764+
"error": "File not available for resubmission",
765+
"details": "Original file was not stored in S3"
766+
}), 400
767+
768+
# Check if file has expired
769+
if original_submission.s3_expires_at and is_file_expired(original_submission.s3_expires_at):
770+
return jsonify({
771+
"error": "File has expired",
772+
"details": "The original file has been automatically deleted due to retention policy"
773+
}), 410
774+
775+
# Download file from S3
776+
success, file_storage, error_msg = download_file(original_submission.s3_key)
777+
if not success:
778+
return jsonify({
779+
"error": "Failed to retrieve file from storage",
780+
"details": error_msg
781+
}), 500
782+
783+
new_description = f"Resubmission of /submissions/{original_submission.file_id}"
784+
785+
# Use the existing submit_to_strelka function with resubmission type
786+
# We need to temporarily modify the file object to include the new description
787+
class ResubmissionFile:
788+
def __init__(self, file_storage, description):
789+
self.filename = file_storage.filename
790+
self.stream = file_storage.stream
791+
self.content_type = file_storage.content_type
792+
self.description = description
793+
794+
def seek(self, *args, **kwargs):
795+
return self.stream.seek(*args, **kwargs)
796+
797+
def read(self, *args, **kwargs):
798+
return self.stream.read(*args, **kwargs)
799+
800+
resubmission_file = ResubmissionFile(file_storage, new_description)
801+
802+
# Call the existing submit_to_strelka function
803+
response = submit_to_strelka(
804+
resubmission_file,
805+
user,
806+
"", # No submitted_hash for resubmission
807+
new_description,
808+
"resubmission" # Mark as resubmission type
809+
)
810+
811+
# Add original submission ID to the response
812+
if isinstance(response, tuple) and len(response) == 2:
813+
response_data, status_code = response
814+
if status_code == 200 and hasattr(response_data, 'get_json'):
815+
json_data = response_data.get_json()
816+
json_data["original_submission_id"] = submission_id
817+
return jsonify(json_data), status_code
818+
819+
return response
820+
821+
except Exception as e:
822+
logging.error("Error during file resubmission: %s", e)
823+
return jsonify({
824+
"error": "File resubmission failed",
825+
"details": str(e)
826+
}), 500
827+
711828
def submissions_to_json(submission: FileSubmission) -> Dict[str, any]:
712829
"""
713830
Converts the given submission to a dictionary representation that can be

app/strelka_ui/example.env

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,17 @@ export VIRUSTOTAL_API_LIMIT=
2929

3030
# Default Submission Exclusions
3131
export DEFAULT_EXCLUDED_SUBMITTERS=["ExcludeUser"]
32+
33+
# S3 Configuration for File Resubmission
34+
export ENABLE_FILE_RESUBMISSION=false
35+
export S3_BUCKET_NAME=
36+
export S3_REGION=us-east-1
37+
export S3_ACCESS_KEY_ID=
38+
export S3_SECRET_ACCESS_KEY=
39+
# make sure S3_FILE_RETENTION_DAYS value matches the bucket policy
40+
export S3_FILE_RETENTION_DAYS=7
41+
export S3_ENDPOINT_URL=
42+
# boto3 >=1.36.0 enables checksum validation by default, but this feature supported by some self-hosted solutions.
43+
# To disable this feature, set the following environment variables:
44+
export AWS_REQUEST_CHECKSUM_CALCULATION=when_required
45+
export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""add_s3_fields_for_resubmission
2+
3+
Revision ID: 1b23372700aa
4+
Revises: 3671ee09c427
5+
Create Date: 2025-08-04 16:20:29.846533
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '1b23372700aa'
14+
down_revision = '3671ee09c427'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
with op.batch_alter_table('file_submission', schema=None) as batch_op:
22+
batch_op.add_column(sa.Column('s3_key', sa.String(), nullable=True))
23+
batch_op.add_column(sa.Column('s3_expires_at', sa.DateTime(), nullable=True))
24+
# ### end Alembic commands ###
25+
26+
27+
def downgrade():
28+
# ### commands auto generated by Alembic - please adjust! ###
29+
with op.batch_alter_table('file_submission', schema=None) as batch_op:
30+
batch_op.drop_column('s3_expires_at')
31+
batch_op.drop_column('s3_key')
32+
# ### end Alembic commands ###

app/strelka_ui/models.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ class FileSubmission(db.Model):
3030
highest_vt_count (int): The highest number of VirusTotal hits for a file.
3131
highest_vt_sha256 (str): The SHA256 hash of the file with the highest number of VirusTotal hits.
3232
mime_type (str): The MIME type of the file.
33+
s3_key (str): S3 object key for stored file (for resubmission).
34+
s3_expires_at (datetime.datetime): When the S3 file expires and gets deleted.
3335
"""
3436

3537
__tablename__ = "file_submission"
@@ -69,6 +71,10 @@ class FileSubmission(db.Model):
6971
)
7072
processed_at: datetime.datetime = db.Column(db.DateTime())
7173

74+
# S3 Storage Metadata (for file resubmission)
75+
s3_key: str = db.Column(db.String(), nullable=True)
76+
s3_expires_at: datetime.datetime = db.Column(db.DateTime(), nullable=True)
77+
7278
def __init__(
7379
self,
7480
file_name: str,
@@ -80,7 +86,8 @@ def __init__(
8086
submitted_by_user_id: int,
8187
submitted_description: str,
8288
submitted_at: datetime.datetime,
83-
89+
s3_key: str = None,
90+
s3_expires_at: datetime.datetime = None,
8491
):
8592
self.file_id = get_request_id(strelka_response[0]) # submitted_file
8693
self.file_name = file_name
@@ -107,6 +114,10 @@ def __init__(
107114
self.hashes = get_hashes(strelka_response[0])
108115
self.insights = get_all_insights(strelka_response)
109116
self.iocs = get_all_iocs(strelka_response)
117+
118+
# S3 Storage fields
119+
self.s3_key = s3_key
120+
self.s3_expires_at = s3_expires_at
110121

111122

112123
def __repr__(self):

0 commit comments

Comments
 (0)