Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions stixify/web/migrations/0025_alter_file_created.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.2.12 on 2026-07-28 14:40

import django.utils.timezone
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("stixify_core", "0024_file_admiralty_information_credibility_and_more"),
]

operations = [
migrations.AlterField(
model_name="file",
name="created",
field=models.DateTimeField(default=django.utils.timezone.now),
),
]
3 changes: 2 additions & 1 deletion stixify/web/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import txt2stix, txt2stix.extractions
from django.core.exceptions import ValidationError
from datetime import UTC, datetime, timezone
from django.utils import timezone as dj_timezone
from django.core.files.uploadedfile import InMemoryUploadedFile
import stix2
from file2txt.parsers.core import BaseParser
Expand Down Expand Up @@ -76,7 +77,7 @@ class CommonSTIXProps(models.Model):

labels = ArrayField(base_field=models.CharField(max_length=256), default=list, help_text="These will be added to the `labels` property of the STIX Report object generated")
identity = models.JSONField(default=default_identity, validators=[validate_identity], help_text="""This is a full STIX Identity JSON. e.g. `{"type":"identity","spec_version":"2.1","id":"identity--b1ae1a15-6f4b-431e-b990-1b9678f35e15","name":"Dummy Identity"}`. If no value is passed, [the Stixify identity object will be used](https://raw.githubusercontent.com/muchdogesec/stix4doge/refs/heads/main/objects/identity/stixify.json).""")
created = models.DateTimeField(auto_now_add=True)
created = models.DateTimeField(default=dj_timezone.now)
modified = models.DateTimeField(auto_now=True)

class Meta:
Expand Down
1 change: 1 addition & 0 deletions stixify/web/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class FileSerializer(serializers.ModelSerializer):
max_value=100,
help_text="A value between `0`-`100`. This value is determined by the content check module of the profile used to process the file. If a confidence value is set on the File object, that value will be used instead.",
)
created = serializers.DateTimeField(required=False, help_text="Set the `created` time of the STIX Report object generated for this File. If not passed, the time the File was uploaded will be used. This cannot be changed once the File has been created.")

class Meta:
model = File
Expand Down
6 changes: 3 additions & 3 deletions stixify/web/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ def partial_update(self, request, *args, **kwargs):
)
serializer.is_valid(raise_exception=True)
serializer.save()
ReportView.update_report(file_obj.report_id, serializer.validated_data)
ReportView.update_report(file_obj.report_id, serializer.validated_data, file_obj.modified)
return Response(FileSerializer(file_obj, context={"request": request}).data)

@extend_schema(
Expand Down Expand Up @@ -764,7 +764,7 @@ def validate_report_id(self, report_id: str):
return report_uuid

@classmethod
def update_report(cls, report_id, validated_data):
def update_report(cls, report_id, validated_data, modified=None):
report = cls.get_report(report_id).data
for k in ["name", "labels"]:
if k not in validated_data:
Expand All @@ -779,7 +779,7 @@ def update_report(cls, report_id, validated_data):
url=source,
)
)
report["modified"] = stix2_format_datetime(timezone.now())
report["modified"] = stix2_format_datetime(modified or timezone.now())
helper = ArangoDBHelper(settings.VIEW_NAME, None)
returned = helper.execute_query(
"""
Expand Down
17 changes: 9 additions & 8 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,16 @@ def stixifier_profile():
@pytest.fixture
def identity():
from dogesec_commons.identity.models import Identity

identity = Identity.objects.create(
identity, _ = Identity.objects.get_or_create(
id="identity--c5f27ca2-a580-4fee-9bb9-753e2b563a30",
created=timezone.now(),
modified=timezone.now(),
stix=dict(
name="dummy identity",
identity_class="individual",
created_by_ref="identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
defaults=dict(
created=timezone.now(),
modified=timezone.now(),
stix=dict(
name="dummy identity",
identity_class="individual",
created_by_ref="identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
),
),
)
yield identity
Expand Down
50 changes: 49 additions & 1 deletion tests/src/views/test_file_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,54 @@ def test_create(client, stixifier_profile, api_schema, identity):
api_schema['/api/v1/files/']['POST'].validate_response(Transport.get_st_response(resp))


@pytest.mark.django_db
def test_create_with_custom_created(client, stixifier_profile, api_schema, identity):
custom_created = "2020-05-17T12:30:00Z"
payload = dict(
file=SimpleUploadedFile(name="name.pdf", content=b"file content"),
profile_id=stixifier_profile.id,
identity_id=identity.id,
mode="md",
name="Upload test with custom created",
report_id="report--567681d6-2817-4d84-84fb-87b2f059b92e",
created=custom_created,
)
with (
patch(
"stixify.web.views.JobSerializer", side_effect=JobSerializer
) as mock_job_serializer_cls,
patch("stixify.web.views.new_task") as mock_new_task,
):
resp = client.post("/api/v1/files/", data=payload)
assert resp.status_code == 201, resp.content
file = models.File.objects.get(pk="567681d6-2817-4d84-84fb-87b2f059b92e")
assert file.created.isoformat() == "2020-05-17T12:30:00+00:00"
resp.wsgi_request.FILES.clear()
api_schema['/api/v1/files/']['POST'].validate_response(Transport.get_st_response(resp))


@pytest.mark.django_db
def test_patch_file_cannot_change_created(client, stixify_file, api_schema):
original_created = stixify_file.created
payload = {
"name": "Updated name only",
"created": "1999-01-01T00:00:00Z",
}
with patch("stixify.web.views.ReportView.update_report"):
resp = client.patch(
"/api/v1/files/dcbeb240-8dd6-4892-8e9e-7b6bda30e454/",
data=json.dumps(payload),
content_type="application/json",
)

assert resp.status_code == 200, resp.content
file_obj = models.File.objects.get(pk="dcbeb240-8dd6-4892-8e9e-7b6bda30e454")
assert file_obj.created == original_created, "created must not be changed via PATCH"
api_schema['/api/v1/files/{file_id}/']['PATCH'].validate_response(
Transport.get_st_response(resp)
)


@pytest.mark.django_db
def test_create_mhtml_pdf(client, stixifier_profile, api_schema, identity):
payload = dict(
Expand Down Expand Up @@ -206,7 +254,7 @@ def test_patch_file_metadata(client, stixify_file, api_schema):
assert file_obj.name == payload["name"]
assert file_obj.labels == payload["labels"]
assert file_obj.sources == payload["sources"]
mock_update_report.assert_called_once_with("report--dcbeb240-8dd6-4892-8e9e-7b6bda30e454", {'name': 'Updated file name', 'labels': ['threat-report', 'customer-facing'], 'sources': ['https://example.com/report', 'https://example.com/notes']})
mock_update_report.assert_called_once_with("report--dcbeb240-8dd6-4892-8e9e-7b6bda30e454", {'name': 'Updated file name', 'labels': ['threat-report', 'customer-facing'], 'sources': ['https://example.com/report', 'https://example.com/notes']}, file_obj.modified)
api_schema['/api/v1/files/{file_id}/']['PATCH'].validate_response(
Transport.get_st_response(resp)
)
Expand Down
23 changes: 19 additions & 4 deletions tests/src/views/test_report_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import typing
import uuid
from datetime import datetime, timezone as dt_timezone
from stix2.utils import format_datetime as stix2_format_datetime
from stixify.classifier.models import Cluster, DocumentEmbedding
from stixify.web import models
from stixify.web.serializers import FileSerializer, JobSerializer
Expand Down Expand Up @@ -120,8 +122,15 @@ def test_list(client, api_schema):
api_schema['/api/v1/reports/']['GET'].validate_response(Transport.get_st_response(resp))


@pytest.mark.parametrize(
'modified_time',
[
None,
datetime(2025, 1, 1, 12, 0, 0, tzinfo=dt_timezone.utc),
],
)
@pytest.mark.django_db
def test_update_report_updates_name_labels_and_sources(client, api_schema):
def test_update_report_updates_name_labels_and_sources(client, api_schema, modified_time):
report_id = "report--52d2146c-798a-440f-942f-6fe039fb8995"
original_resp = client.get(f"/api/v1/reports/{report_id}/")
assert original_resp.status_code == 200, original_resp.content
Expand All @@ -146,14 +155,20 @@ def test_update_report_updates_name_labels_and_sources(client, api_schema):
}

try:
assert ReportView.update_report(report_id, payload) is True
assert ReportView.update_report(report_id, payload, modified=modified_time) is True
time.sleep(1) # wait for it to update view

resp = client.get(f"/api/v1/reports/{report_id}/")
assert resp.status_code == 200, resp.content
assert resp.data["name"] == payload["name"]
assert resp.data["created"] == original_resp.data["created"], "Report created timestamp should not change"
assert resp.data["labels"] == payload["labels"]
assert resp.data["modified"] > original_resp.data["modified"], "Report modified timestamp should be updated"
if modified_time:
assert resp.data["modified"] == stix2_format_datetime(modified_time), (
"Report modified timestamp should match the passed-in modified value, not now()"
)
else:
assert resp.data["modified"] > original_resp.data["modified"], "Report modified timestamp should be updated"

sources = [
ref["url"]
Expand All @@ -167,7 +182,7 @@ def test_update_report_updates_name_labels_and_sources(client, api_schema):
)
finally:
ReportView.update_report(report_id, original_payload)

time.sleep(1) # wait for it to update view before other tests run

@pytest.mark.parametrize(
"report_id,expected_ids",
Expand Down
Loading