Skip to content

Commit 8675647

Browse files
authored
Merge branch 'bugfix' into report-css-fix
2 parents 6b26aae + 4decd88 commit 8675647

File tree

17 files changed

+1079
-26
lines changed

17 files changed

+1079
-26
lines changed

dojo/api_v2/permissions.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,13 @@ def has_permission(self, request, view):
473473
# Raise an explicit drf exception here
474474
raise ValidationError(e)
475475
if engagement := converted_dict.get("engagement"):
476-
# existing engagement, nothing special to check
476+
# Validate the resolved engagement's parent chain matches any provided identifiers
477+
if (product := converted_dict.get("product")) and engagement.product_id != product.id:
478+
msg = "The provided identifiers are inconsistent — the engagement does not belong to the specified product."
479+
raise ValidationError(msg)
480+
if (engagement_name := converted_dict.get("engagement_name")) and engagement.name != engagement_name:
481+
msg = "The provided identifiers are inconsistent — the engagement name does not match the specified engagement."
482+
raise ValidationError(msg)
477483
return user_has_permission(
478484
request.user, engagement, Permissions.Import_Scan_Result,
479485
)
@@ -764,6 +770,11 @@ def has_permission(self, request, view):
764770
try:
765771
converted_dict = auto_create.convert_querydict_to_dict(request.data)
766772
auto_create.process_import_meta_data_from_dict(converted_dict)
773+
# engagement is not a declared field on ReImportScanSerializer and will be
774+
# stripped during validation — don't use it in the permission check either,
775+
# so the permission check resolves targets the same way execution does
776+
converted_dict.pop("engagement", None)
777+
converted_dict.pop("engagement_id", None)
767778
# Get an existing product
768779
converted_dict["product_type"] = auto_create.get_target_product_type_if_exists(**converted_dict)
769780
converted_dict["product"] = auto_create.get_target_product_if_exists(**converted_dict)
@@ -774,7 +785,20 @@ def has_permission(self, request, view):
774785
raise ValidationError(e)
775786

776787
if test := converted_dict.get("test"):
777-
# existing test, nothing special to check
788+
# Validate the resolved test's parent chain matches any provided identifiers
789+
if (product := converted_dict.get("product")) and test.engagement.product_id != product.id:
790+
msg = "The provided identifiers are inconsistent — the test does not belong to the specified product."
791+
raise ValidationError(msg)
792+
if (engagement := converted_dict.get("engagement")) and test.engagement_id != engagement.id:
793+
msg = "The provided identifiers are inconsistent — the test does not belong to the specified engagement."
794+
raise ValidationError(msg)
795+
# Also validate by name when the objects were not resolved (e.g. names that match no existing record)
796+
if not converted_dict.get("product") and (product_name := converted_dict.get("product_name")) and test.engagement.product.name != product_name:
797+
msg = "The provided identifiers are inconsistent — the test does not belong to the specified product."
798+
raise ValidationError(msg)
799+
if not converted_dict.get("engagement") and (engagement_name := converted_dict.get("engagement_name")) and test.engagement.name != engagement_name:
800+
msg = "The provided identifiers are inconsistent — the test does not belong to the specified engagement."
801+
raise ValidationError(msg)
778802
return user_has_permission(
779803
request.user, test, Permissions.Import_Scan_Result,
780804
)
@@ -1181,7 +1205,10 @@ def check_auto_create_permission(
11811205
raise ValidationError(msg)
11821206

11831207
if engagement:
1184-
# existing engagement, nothing special to check
1208+
# Validate the resolved engagement's parent chain matches any provided names
1209+
if product is not None and engagement.product_id != product.id:
1210+
msg = "The provided identifiers are inconsistent — the engagement does not belong to the specified product."
1211+
raise ValidationError(msg)
11851212
return user_has_permission(
11861213
user, engagement, Permissions.Import_Scan_Result,
11871214
)

dojo/api_v2/serializers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,18 @@ def validate(self, data):
11231123
if data.get("target_start") > data.get("target_end"):
11241124
msg = "Your target start date exceeds your target end date"
11251125
raise serializers.ValidationError(msg)
1126+
if (
1127+
self.instance is not None
1128+
and "product" in data
1129+
and data.get("product") != self.instance.product
1130+
and not user_has_permission(
1131+
self.context["request"].user,
1132+
data.get("product"),
1133+
Permissions.Engagement_Edit,
1134+
)
1135+
):
1136+
msg = "You are not permitted to edit engagements in the destination product"
1137+
raise PermissionDenied(msg)
11261138
return data
11271139

11281140
def build_relational_field(self, field_name, relation_info):

dojo/engagement/views.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,12 @@ def edit_engagement(request, eid):
286286
if form.is_valid():
287287
# first save engagement details
288288
new_status = form.cleaned_data.get("status")
289+
if form.cleaned_data.get("product") != engagement.product:
290+
user_has_permission_or_403(
291+
request.user,
292+
form.cleaned_data.get("product"),
293+
Permissions.Engagement_Edit,
294+
)
289295
engagement.product = form.cleaned_data.get("product")
290296
engagement = form.save(commit=False)
291297
if (new_status in {"Cancelled", "Completed"}):
@@ -1442,9 +1448,10 @@ def view_edit_risk_acceptance(request, eid, raid, *, edit_mode=False):
14421448
"Since you are not the note's author, it was not deleted.",
14431449
extra_tags="alert-danger")
14441450

1445-
if "remove_finding" in request.POST:
1451+
if edit_mode and "remove_finding" in request.POST:
14461452
finding = get_object_or_404(
1447-
Finding, pk=request.POST["remove_finding_id"])
1453+
risk_acceptance.accepted_findings,
1454+
pk=request.POST["remove_finding_id"])
14481455

14491456
ra_helper.remove_finding_from_risk_acceptance(request.user, risk_acceptance, finding)
14501457

dojo/importers/auto_create_context.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ def get_target_engagement_if_exists(
181181
"""
182182
if engagement := get_object_or_none(Engagement, pk=engagement_id):
183183
logger.debug("Using existing engagement by id: %s", engagement_id)
184+
if product is not None and engagement.product_id != product.id:
185+
msg = "The provided identifiers are inconsistent — the engagement does not belong to the specified product."
186+
raise ValueError(msg)
184187
return engagement
185188
# if there's no product, then for sure there's no engagement either
186189
if product is None:
@@ -203,6 +206,9 @@ def get_target_test_if_exists(
203206
"""
204207
if test := get_object_or_none(Test, pk=test_id):
205208
logger.debug("Using existing Test by id: %s", test_id)
209+
if engagement is not None and test.engagement_id != engagement.id:
210+
msg = "The provided identifiers are inconsistent — the test does not belong to the specified engagement."
211+
raise ValueError(msg)
206212
return test
207213
# If the engagement is not supplied, we cannot do anything
208214
if not engagement:

dojo/risk_acceptance/api.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
from abc import ABC, abstractmethod
22
from typing import NamedTuple
33

4+
from django.core.exceptions import PermissionDenied
45
from django.db.models import QuerySet
56
from django.utils import timezone
67
from drf_spectacular.utils import extend_schema
78
from rest_framework import serializers, status
89
from rest_framework.decorators import action
9-
from rest_framework.permissions import IsAdminUser
10+
from rest_framework.permissions import IsAuthenticated
1011
from rest_framework.response import Response
1112

13+
from dojo.api_v2.permissions import UserHasRiskAcceptanceRelatedObjectPermission
1214
from dojo.api_v2.serializers import RiskAcceptanceSerializer
1315
from dojo.authorization.roles_permissions import Permissions
1416
from dojo.engagement.queries import get_authorized_engagements
15-
from dojo.models import Risk_Acceptance, User, Vulnerability_Id
17+
from dojo.models import Engagement, Risk_Acceptance, User, Vulnerability_Id
1618

1719
AcceptedRisk = NamedTuple("AcceptedRisk", (("vulnerability_id", str), ("justification", str), ("accepted_by", str)))
1820

@@ -40,10 +42,13 @@ def risk_application_model_class(self):
4042
request=AcceptedRiskSerializer(many=True),
4143
responses={status.HTTP_201_CREATED: RiskAcceptanceSerializer(many=True)},
4244
)
43-
@action(methods=["post"], detail=True, permission_classes=[IsAdminUser], serializer_class=AcceptedRiskSerializer,
44-
filter_backends=[], pagination_class=None)
45+
@action(methods=["post"], detail=True, permission_classes=[IsAuthenticated, UserHasRiskAcceptanceRelatedObjectPermission],
46+
serializer_class=AcceptedRiskSerializer, filter_backends=[], pagination_class=None)
4547
def accept_risks(self, request, pk=None):
4648
model = self.get_object()
49+
product = model.product if isinstance(model, Engagement) else model.engagement.product
50+
if not product.enable_full_risk_acceptance:
51+
raise PermissionDenied
4752
serializer = AcceptedRiskSerializer(data=request.data, many=True)
4853
if serializer.is_valid():
4954
accepted_risks = serializer.save()
@@ -63,7 +68,7 @@ class AcceptedFindingsMixin(ABC):
6368
request=AcceptedRiskSerializer(many=True),
6469
responses={status.HTTP_201_CREATED: RiskAcceptanceSerializer(many=True)},
6570
)
66-
@action(methods=["post"], detail=False, permission_classes=[IsAdminUser], serializer_class=AcceptedRiskSerializer)
71+
@action(methods=["post"], detail=False, permission_classes=[IsAuthenticated], serializer_class=AcceptedRiskSerializer)
6772
def accept_risks(self, request):
6873
serializer = AcceptedRiskSerializer(data=request.data, many=True)
6974
if serializer.is_valid():
@@ -72,7 +77,9 @@ def accept_risks(self, request):
7277
return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST)
7378
owner = request.user
7479
accepted_result = []
75-
for engagement in get_authorized_engagements(Permissions.Engagement_View):
80+
for engagement in get_authorized_engagements(Permissions.Risk_Acceptance):
81+
if not engagement.product.enable_full_risk_acceptance:
82+
continue
7683
base_findings = engagement.unaccepted_open_findings
7784
accepted = _accept_risks(accepted_risks, base_findings, owner)
7885
engagement.accept_risks(accepted)

dojo/templates/dojo/snippets/endpoints.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,13 @@ <h6>Mitigated Endpoints / Systems ({{ finding.mitigated_endpoint_count }})</h6>
7272
{% if V3_FEATURE_LOCATIONS %}
7373
<td style="word-break: break-word">{{ endpoint.location }}{% if endpoint.is_broken %} <span data-toggle="tooltip" title="Endpoint is broken. Check documentation to look for fix process" >&#128681;</span>{% endif %}</td>
7474
<td>{{ endpoint.status }}</td>
75-
<td>{{ endpoint.auditor|safe }}</td>
75+
<td>{{ endpoint.auditor }}</td>
7676
<td>{{ endpoint.audit_time|date }}</td>
7777
{% else %}
78-
{% comment %} TODO: Delete this after the move to Locations {% endcomment %}
78+
{% comment %} TODO: Delete this after the move to Locations {% endcomment %}
7979
<td style="word-break: break-word">{{ endpoint }}{% if endpoint.endpoint.is_broken %} <span data-toggle="tooltip" title="Endpoint is broken. Check documentation to look for fix process" >&#128681;</span>{% endif %}</td>
8080
<td>{{ endpoint.status }}</td>
81-
<td>{{ endpoint.mitigated_by|safe }}</td>
81+
<td>{{ endpoint.mitigated_by }}</td>
8282
<td>{{ endpoint.mitigated_time|date }}</td>
8383
{% endif %}
8484
</tr>
@@ -256,7 +256,7 @@ <h4>Mitigated Endpoints / Systems ({{ finding.mitigated_endpoint_count }})
256256
{% include "dojo/snippets/tags.html" with tags=endpoint.location.tags.all %}
257257
</td>
258258
<td>{{ endpoint.get_status_display }}</td>
259-
<td>{{ endpoint.auditor|safe }}</td>
259+
<td>{{ endpoint.auditor }}</td>
260260
<td>{{ endpoint.audit_time|date }}</td>
261261
{% else %}
262262
{% comment %} TODO: Delete this after the move to Locations {% endcomment %}
@@ -265,7 +265,7 @@ <h4>Mitigated Endpoints / Systems ({{ finding.mitigated_endpoint_count }})
265265
{% include "dojo/snippets/tags.html" with tags=endpoint.endpoint.tags.all %}
266266
</td>
267267
<td>{{ endpoint.status }}</td>
268-
<td>{{ endpoint.mitigated_by|safe }}</td>
268+
<td>{{ endpoint.mitigated_by }}</td>
269269
<td>{{ endpoint.mitigated_time|date }}</td>
270270
{% endif %}
271271
</tr>

dojo/tools/govulncheck/parser.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@ def get_location(data, node):
4040
def get_version(data, node):
4141
return data["Requires"]["Modules"][str(node)]["Version"]
4242

43+
@staticmethod
44+
def get_fix_info(affected_ranges):
45+
for r in affected_ranges:
46+
for event in r.get("events", []):
47+
if "fixed" in event:
48+
return True, event["fixed"]
49+
return False, ""
50+
51+
@staticmethod
52+
def get_introduced_version(affected_ranges):
53+
for r in affected_ranges:
54+
for event in r.get("events", []):
55+
if "introduced" in event:
56+
return event["introduced"]
57+
return ""
58+
4359
def get_finding_trace_info(self, data, osv_id):
4460
# Browse the findings to look for matching OSV-id. If the OSV-id is matching, extract traces.
4561
trace_info_strs = []
@@ -202,8 +218,12 @@ def get_findings(self, scan_file, test):
202218
else:
203219
title = f"{osv_data['id']} - {affected_package['name']}"
204220

205-
affected_version = self.get_affected_version(data, osv_data["id"])
221+
fix_available, fix_version = self.get_fix_info(affected_ranges)
206222

223+
affected_version = (
224+
self.get_affected_version(data, osv_data["id"])
225+
or self.get_introduced_version(affected_ranges)
226+
)
207227
severity = elem["osv"].get("severity", SEVERITY)
208228

209229
d = {
@@ -215,6 +235,8 @@ def get_findings(self, scan_file, test):
215235
"description": description,
216236
"impact": impact,
217237
"references": references,
238+
"fix_available": fix_available,
239+
"fix_version": fix_version,
218240
"file_path": path,
219241
"url": db_specific_url,
220242
"unique_id_from_tool": osv_id,

dojo/tools/risk_recon/api.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import requests
21
from django.conf import settings
32

3+
from dojo.utils_ssrf import SSRFError, make_ssrf_safe_session, validate_url_for_ssrf
4+
45

56
class RiskReconAPI:
67
def __init__(self, api_key, endpoint, data):
@@ -26,7 +27,14 @@ def __init__(self, api_key, endpoint, data):
2627
raise Exception(msg)
2728
if self.url.endswith("/"):
2829
self.url = endpoint[:-1]
29-
self.session = requests.Session()
30+
31+
try:
32+
validate_url_for_ssrf(self.url)
33+
except SSRFError as exc:
34+
msg = f"Invalid Risk Recon API url: {exc}"
35+
raise Exception(msg) from exc
36+
37+
self.session = make_ssrf_safe_session()
3038
self.map_toes()
3139
self.get_findings()
3240

@@ -54,7 +62,7 @@ def map_toes(self):
5462
filters = comps.get(name)
5563
self.toe_map[toe_id] = filters or self.data
5664
else:
57-
msg = f"Unable to query Target of Evaluations due to {response.status_code} - {response.content}"
65+
msg = f"Unable to query Target of Evaluations due to {response.status_code}"
5866
raise Exception(msg) # TODO: when implementing ruff BLE001, please fix also TODO in unittests/test_risk_recon.py
5967

6068
def filter_finding(self, finding):
@@ -86,5 +94,5 @@ def get_findings(self):
8694
if not self.filter_finding(finding):
8795
self.findings.append(finding)
8896
else:
89-
msg = f"Unable to collect findings from toe: {toe} due to {response.status_code} - {response.content}"
97+
msg = f"Unable to collect findings from toe: {toe} due to {response.status_code}"
9098
raise Exception(msg)

0 commit comments

Comments
 (0)