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
3 changes: 3 additions & 0 deletions dojo/api_v3/import_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ def _check_auto_permission(request: HttpRequest, payload: ImportForm) -> object
auto = AutoCreateContextManager()
context = {
"scan_type": payload.scan_type,
# The auto branch dispatches on this id, so the check has to resolve the target the same
# way execution does.
"engagement": payload.engagement,
"product_name": payload.asset_name,
"engagement_name": payload.engagement_name,
"product_type_name": payload.organization_name,
Expand Down
75 changes: 74 additions & 1 deletion unittests/api_v3/test_apiv3_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,25 @@
The consolidated ``POST /import`` (import/reimport/auto) must reproduce the v2 endpoints' DB state
for identical payloads, including ``close_old_findings``. Both paths run in the shared test
transaction so DB-state assertions are exact.

``TestApiV3ImportAuthz`` covers the other half: auto mode dispatches on the numeric ``engagement``,
so the permission check has to resolve that same target rather than the name fields alone.
"""
from __future__ import annotations

import datetime
from collections import Counter

from django.contrib.auth.models import Permission
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse

from dojo.models import Finding, Test
from dojo.models import Engagement, Finding, Product, Product_Type, Test, Test_Type, User

from .base import ApiV3TestCase

_ZAP = "ZAP Scan"
_GENERIC = "Generic Findings Import"


def _finding_multiset(test_id: int) -> Counter:
Expand Down Expand Up @@ -117,3 +124,69 @@ def test_auto_mode_creates_then_reuses(self):
)
self.assertEqual("reimport", reused["mode_resolved"])
self.assertEqual(first_test, reused["test"]["id"])


class TestApiV3ImportAuthz(ApiV3TestCase):

"""Auto mode must not import into an engagement the caller cannot import to."""

def setUp(self):
super().setUp()
product = Product.objects.create(
name="authz victim product",
prod_type=Product_Type.objects.create(name="authz victim org"),
description="victim",
)
self.engagement = Engagement.objects.create(
name="authz victim engagement", product=product,
target_start=datetime.date(2026, 1, 1), target_end=datetime.date(2026, 2, 1),
)
# Holds the self-service product-type add permission and nothing else, so the name-based
# auto-create check passes while the engagement stays out of reach.
self.outsider = User.objects.create(username="authz outsider", is_active=True)
self.outsider.user_permissions.add(Permission.objects.get(codename="add_product_type"))
self.outsider = User.objects.get(pk=self.outsider.pk)

def _post(self):
scan = SimpleUploadedFile(
"scan.json",
b'{"findings":[{"title":"injected","severity":"High","description":"x"}]}',
content_type="application/json",
)
return self.token_client(user=self.outsider).post(self.v3_url("import"), {
"scan_type": _GENERIC,
"mode": "auto",
"auto_create_context": "true",
"asset_name": "authz unused name",
"organization_name": "authz unused org",
"engagement_name": "authz unused engagement",
"engagement": self.engagement.id,
"file": scan,
}, format="multipart")

def test_auto_mode_rejects_unauthorized_engagement_id(self):
response = self._post()
self.assertEqual(403, response.status_code, response.content[:400])
self.assertFalse(Test.objects.filter(engagement=self.engagement).exists())

def test_auto_mode_rejects_unauthorized_engagement_id_with_existing_test(self):
# With a matching test already there, auto resolves to reimport, which also closes the
# engagement's active findings. Same denial.
test = Test.objects.create(
engagement=self.engagement,
test_type=Test_Type.objects.get_or_create(name=_GENERIC)[0],
scan_type=_GENERIC,
target_start=datetime.datetime(2026, 1, 1, tzinfo=datetime.UTC),
target_end=datetime.datetime(2026, 2, 1, tzinfo=datetime.UTC),
)
finding = Finding.objects.create(
test=test, title="existing", severity="High", description="x",
active=True, verified=False, reporter=self.admin,
)

response = self._post()

self.assertEqual(403, response.status_code, response.content[:400])
finding.refresh_from_db()
self.assertTrue(finding.active)
self.assertEqual(1, Finding.objects.filter(test__engagement=self.engagement).count())
Loading