Skip to content

Commit a949154

Browse files
authored
Merge branch 'dev' into feature/even_quicker_verify
2 parents 7ce5496 + ad53d73 commit a949154

13 files changed

Lines changed: 471 additions & 39 deletions

File tree

components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"metismenu": "~3.0.7",
3434
"moment": "^2.30.1",
3535
"morris.js": "morrisjs/morris.js",
36-
"pdfmake": "^0.3.3",
36+
"pdfmake": "^0.3.4",
3737
"startbootstrap-sb-admin-2": "1.0.7"
3838
},
3939
"engines": {

components/yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -385,10 +385,10 @@ pdfkit@^0.17.2:
385385
linebreak "^1.1.0"
386386
png-js "^1.0.0"
387387

388-
pdfmake@^0.3.3:
389-
version "0.3.3"
390-
resolved "https://registry.yarnpkg.com/pdfmake/-/pdfmake-0.3.3.tgz#2705e8fabff4bf52a4a7b7ae9d93caee1b200cb7"
391-
integrity sha512-jSnF8rVLkbLLX37bnXWRFhEDO48quE7OIg7lgWBa6ihAbpCxASaBLWFOXNxSDeLBNt92304SBwpYcPkJnIArlA==
388+
pdfmake@^0.3.4:
389+
version "0.3.4"
390+
resolved "https://registry.yarnpkg.com/pdfmake/-/pdfmake-0.3.4.tgz#3448ca1434396275dce8f49202e761fefad781eb"
391+
integrity sha512-zbGBox6pgNeGdG7tlLVBbQJlYIlTHtXo5q8+dNhCb2O0Q2+Nc5bcpsgNzbzqfzlcJ0gX9f+ZBv1z4FuJjUHwVA==
392392
dependencies:
393393
linebreak "^1.1.0"
394394
pdfkit "^0.17.2"

docs/content/supported_tools/parsers/file/generic.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Generic Findings Import can be used to import any report in CSV or JSON format.
3131
- known_exploited: Indicator if the finding is listed in Known Exploited List. Must be TRUE, or FALSE
3232
- ransomware_used: Indicator if the finding is used in Ransomware. Must be TRUE, or FALSE
3333
- fix_available: Indicator if fix available for the finding. Must be TRUE, or FALSE
34+
- fix_version: Version where fix is available. String value.
3435
- kev_date: Date the finding was added to Known Exploited Vulnerabilities list in mm/dd/yyyy format or ISO format.
3536

3637
The CSV expects a header row with the names of the attributes.
@@ -94,6 +95,7 @@ The list of supported fields in JSON format:
9495
- known_exploited: Bool
9596
- ransomware_used: Bool
9697
- fix_available: Bool
98+
- fix_version: String
9799

98100
### Example JSON
99101

@@ -114,6 +116,7 @@ The list of supported fields in JSON format:
114116
"known_exploited": true,
115117
"ransomware_used": true,
116118
"fix_available": true,
119+
"fix_version": "0.0.00",
117120
"kev_date": "2024-05-01",
118121
"file_path": "src/first.cpp",
119122
"line": 13,

dojo/filters.py

Lines changed: 110 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,6 +2020,33 @@ def filter_mitigated_on(self, queryset, name, value):
20202020
return queryset.filter(mitigated=value)
20212021

20222022

2023+
def get_finding_group_queryset_for_context(pid=None, eid=None, tid=None):
2024+
"""
2025+
Helper function to build finding group queryset based on context hierarchy.
2026+
Context priority: test > engagement > product > global
2027+
2028+
Args:
2029+
pid: Product ID (least specific)
2030+
eid: Engagement ID
2031+
tid: Test ID (most specific)
2032+
2033+
Returns:
2034+
QuerySet of Finding_Group filtered by context
2035+
2036+
"""
2037+
if tid is not None:
2038+
# Most specific: filter by test
2039+
return Finding_Group.objects.filter(test_id=tid).only("id", "name")
2040+
if eid is not None:
2041+
# Filter by engagement's tests
2042+
return Finding_Group.objects.filter(test__engagement_id=eid).only("id", "name")
2043+
if pid is not None:
2044+
# Filter by product's tests
2045+
return Finding_Group.objects.filter(test__engagement__product_id=pid).only("id", "name")
2046+
# Global: return all (authorization will be applied separately)
2047+
return Finding_Group.objects.all().only("id", "name")
2048+
2049+
20232050
class FindingFilterWithoutObjectLookups(FindingFilterHelper, FindingTagStringFilter):
20242051
test__engagement__product__prod_type = NumberFilter(widget=HiddenInput())
20252052
test__engagement__product = NumberFilter(widget=HiddenInput())
@@ -2111,20 +2138,45 @@ class Meta:
21112138
def __init__(self, *args, **kwargs):
21122139
self.user = None
21132140
self.pid = None
2141+
self.eid = None
2142+
self.tid = None
21142143
if "user" in kwargs:
21152144
self.user = kwargs.pop("user")
21162145

21172146
if "pid" in kwargs:
21182147
self.pid = kwargs.pop("pid")
2148+
if "eid" in kwargs:
2149+
self.eid = kwargs.pop("eid")
2150+
if "tid" in kwargs:
2151+
self.tid = kwargs.pop("tid")
21192152
super().__init__(*args, **kwargs)
21202153
# Set some date fields
21212154
self.set_date_fields(*args, **kwargs)
2122-
# Don't show the product filter on the product finding view
2123-
if self.pid:
2124-
del self.form.fields["test__engagement__product__name"]
2125-
del self.form.fields["test__engagement__product__name_contains"]
2126-
del self.form.fields["test__engagement__product__prod_type__name"]
2127-
del self.form.fields["test__engagement__product__prod_type__name_contains"]
2155+
# Don't show the product/engagement/test filter fields when in specific context
2156+
if self.tid or self.eid or self.pid:
2157+
if "test__engagement__product__name" in self.form.fields:
2158+
del self.form.fields["test__engagement__product__name"]
2159+
if "test__engagement__product__name_contains" in self.form.fields:
2160+
del self.form.fields["test__engagement__product__name_contains"]
2161+
if "test__engagement__product__prod_type__name" in self.form.fields:
2162+
del self.form.fields["test__engagement__product__prod_type__name"]
2163+
if "test__engagement__product__prod_type__name_contains" in self.form.fields:
2164+
del self.form.fields["test__engagement__product__prod_type__name_contains"]
2165+
# Also hide engagement and test fields if in test or engagement context
2166+
if self.tid:
2167+
if "test__engagement__name" in self.form.fields:
2168+
del self.form.fields["test__engagement__name"]
2169+
if "test__engagement__name_contains" in self.form.fields:
2170+
del self.form.fields["test__engagement__name_contains"]
2171+
if "test__name" in self.form.fields:
2172+
del self.form.fields["test__name"]
2173+
if "test__name_contains" in self.form.fields:
2174+
del self.form.fields["test__name_contains"]
2175+
elif self.eid:
2176+
if "test__engagement__name" in self.form.fields:
2177+
del self.form.fields["test__engagement__name"]
2178+
if "test__engagement__name_contains" in self.form.fields:
2179+
del self.form.fields["test__engagement__name_contains"]
21282180

21292181

21302182
class FindingFilter(FindingFilterHelper, FindingTagFilter):
@@ -2163,38 +2215,79 @@ class Meta:
21632215
def __init__(self, *args, **kwargs):
21642216
self.user = None
21652217
self.pid = None
2218+
self.eid = None
2219+
self.tid = None
21662220
if "user" in kwargs:
21672221
self.user = kwargs.pop("user")
21682222

21692223
if "pid" in kwargs:
21702224
self.pid = kwargs.pop("pid")
2225+
if "eid" in kwargs:
2226+
self.eid = kwargs.pop("eid")
2227+
if "tid" in kwargs:
2228+
self.tid = kwargs.pop("tid")
21712229
super().__init__(*args, **kwargs)
21722230
# Set some date fields
21732231
self.set_date_fields(*args, **kwargs)
21742232
# Don't show the product filter on the product finding view
21752233
self.set_related_object_fields(*args, **kwargs)
21762234

21772235
def set_related_object_fields(self, *args: list, **kwargs: dict):
2178-
finding_group_query = Finding_Group.objects.all()
2179-
if self.pid is not None:
2180-
del self.form.fields["test__engagement__product"]
2181-
del self.form.fields["test__engagement__product__prod_type"]
2236+
# Use helper to get contextual finding group queryset
2237+
finding_group_query = get_finding_group_queryset_for_context(
2238+
pid=self.pid,
2239+
eid=self.eid,
2240+
tid=self.tid,
2241+
)
2242+
2243+
# Filter by most specific context: test > engagement > product
2244+
if self.tid is not None:
2245+
# Test context: filter finding groups by test
2246+
if "test__engagement__product" in self.form.fields:
2247+
del self.form.fields["test__engagement__product"]
2248+
if "test__engagement__product__prod_type" in self.form.fields:
2249+
del self.form.fields["test__engagement__product__prod_type"]
2250+
if "test__engagement" in self.form.fields:
2251+
del self.form.fields["test__engagement"]
2252+
if "test" in self.form.fields:
2253+
del self.form.fields["test"]
2254+
elif self.eid is not None:
2255+
# Engagement context: filter finding groups by engagement
2256+
if "test__engagement__product" in self.form.fields:
2257+
del self.form.fields["test__engagement__product"]
2258+
if "test__engagement__product__prod_type" in self.form.fields:
2259+
del self.form.fields["test__engagement__product__prod_type"]
2260+
if "test__engagement" in self.form.fields:
2261+
del self.form.fields["test__engagement"]
2262+
# Filter tests by engagement - get_authorized_tests doesn't support engagement param
2263+
engagement = Engagement.objects.filter(id=self.eid).select_related("product").first()
2264+
if engagement:
2265+
self.form.fields["test"].queryset = get_authorized_tests(Permissions.Test_View, product=engagement.product).filter(engagement_id=self.eid).prefetch_related("test_type")
2266+
elif self.pid is not None:
2267+
# Product context: filter finding groups by product
2268+
if "test__engagement__product" in self.form.fields:
2269+
del self.form.fields["test__engagement__product"]
2270+
if "test__engagement__product__prod_type" in self.form.fields:
2271+
del self.form.fields["test__engagement__product__prod_type"]
21822272
# TODO: add authorized check to be sure
2183-
self.form.fields["test__engagement"].queryset = Engagement.objects.filter(
2184-
product_id=self.pid,
2185-
).all()
2186-
self.form.fields["test"].queryset = get_authorized_tests(Permissions.Test_View, product=self.pid).prefetch_related("test_type")
2187-
finding_group_query = Finding_Group.objects.filter(test__engagement__product_id=self.pid)
2273+
if "test__engagement" in self.form.fields:
2274+
self.form.fields["test__engagement"].queryset = Engagement.objects.filter(
2275+
product_id=self.pid,
2276+
).all()
2277+
if "test" in self.form.fields:
2278+
self.form.fields["test"].queryset = get_authorized_tests(Permissions.Test_View, product=self.pid).prefetch_related("test_type")
21882279
else:
2280+
# Global context: show all authorized finding groups
21892281
self.form.fields[
21902282
"test__engagement__product__prod_type"].queryset = get_authorized_product_types(Permissions.Product_Type_View)
21912283
self.form.fields["test__engagement"].queryset = get_authorized_engagements(Permissions.Engagement_View)
2192-
del self.form.fields["test"]
2284+
if "test" in self.form.fields:
2285+
del self.form.fields["test"]
21932286

21942287
if self.form.fields.get("test__engagement__product"):
21952288
self.form.fields["test__engagement__product"].queryset = get_authorized_products(Permissions.Product_View)
21962289
if self.form.fields.get("finding_group", None):
2197-
self.form.fields["finding_group"].queryset = get_authorized_finding_groups_for_queryset(Permissions.Finding_Group_View, finding_group_query)
2290+
self.form.fields["finding_group"].queryset = get_authorized_finding_groups_for_queryset(Permissions.Finding_Group_View, finding_group_query, user=self.user)
21982291
self.form.fields["reporter"].queryset = get_authorized_users(Permissions.Finding_View)
21992292
self.form.fields["reviewers"].queryset = self.form.fields["reporter"].queryset
22002293

dojo/finding/views.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,8 @@ def filter_findings_by_form(self, request: HttpRequest, findings: QuerySet[Findi
267267
kwargs = {
268268
"user": request.user,
269269
"pid": self.get_product_id(),
270+
"eid": self.get_engagement_id(),
271+
"tid": self.get_test_id(),
270272
}
271273

272274
filter_string_matching = get_system_setting("filter_string_matching", False)
@@ -360,10 +362,11 @@ def add_breadcrumbs(self, request: HttpRequest, context: dict):
360362

361363
return request, context
362364

363-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
364-
# Store the product and engagement ids
365+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
366+
# Store the product, engagement, and test ids
365367
self.product_id = product_id
366368
self.engagement_id = engagement_id
369+
self.test_id = test_id
367370
# Get the initial context
368371
request, context = self.get_initial_context(request)
369372
# Get the filtered findings
@@ -386,46 +389,46 @@ def get(self, request: HttpRequest, product_id: int | None = None, engagement_id
386389

387390

388391
class ListOpenFindings(ListFindings):
389-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
392+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
390393
self.filter_name = "Open"
391-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
394+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
392395

393396

394397
class ListVerifiedFindings(ListFindings):
395-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
398+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
396399
self.filter_name = "Verified"
397-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
400+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
398401

399402

400403
class ListOutOfScopeFindings(ListFindings):
401-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
404+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
402405
self.filter_name = "Out of Scope"
403-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
406+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
404407

405408

406409
class ListFalsePositiveFindings(ListFindings):
407-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
410+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
408411
self.filter_name = "False Positive"
409-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
412+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
410413

411414

412415
class ListInactiveFindings(ListFindings):
413-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
416+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
414417
self.filter_name = "Inactive"
415-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
418+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
416419

417420

418421
class ListAcceptedFindings(ListFindings):
419-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
422+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
420423
self.filter_name = "Accepted"
421-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
424+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
422425

423426

424427
class ListClosedFindings(ListFindings):
425-
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None):
428+
def get(self, request: HttpRequest, product_id: int | None = None, engagement_id: int | None = None, test_id: int | None = None):
426429
self.filter_name = "Closed"
427430
self.order_by = "-mitigated"
428-
return super().get(request, product_id=product_id, engagement_id=engagement_id)
431+
return super().get(request, product_id=product_id, engagement_id=engagement_id, test_id=test_id)
429432

430433

431434
class ViewFinding(View):

dojo/templates/dojo/view_finding.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,6 +1317,23 @@ <h4>Credential
13171317
$("img#to_replace").attr('src', $(thumbnails[index + 1]).data('imageurl'));
13181318
$("#caption_to_replace").html($(thumbnails[index + 1]).data('caption'));
13191319
}
1320+
$('div#bulk_edit_menu').addClass('hidden');
1321+
}
1322+
});
1323+
1324+
$('input#select_all_mitigated').on('click', function (e) {
1325+
var checkbox_values = $("input[type=checkbox][name^='select_mitigated']");
1326+
if ($(this).is(":checked")) {
1327+
for (var i = 0; i < checkbox_values.length; i++) {
1328+
$(checkbox_values[i]).prop('checked', true)
1329+
}
1330+
$('div#bulk_edit_menu').removeClass('hidden');
1331+
}
1332+
else {
1333+
for (var i = 0; i < checkbox_values.length; i++) {
1334+
$(checkbox_values[i]).prop('checked', false)
1335+
}
1336+
$('div#bulk_edit_menu').addClass('hidden');
13201337
}
13211338
});
13221339
});

dojo/test/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def get_findings(self, request: HttpRequest, test: Test):
122122
findings = Finding.objects.filter(test=test).order_by("numerical_severity")
123123
filter_string_matching = get_system_setting("filter_string_matching", False)
124124
finding_filter_class = FindingFilterWithoutObjectLookups if filter_string_matching else FindingFilter
125-
findings = finding_filter_class(request.GET, pid=test.engagement.product.id, queryset=findings)
125+
findings = finding_filter_class(request.GET, pid=test.engagement.product.id, eid=test.engagement.id, tid=test.id, queryset=findings)
126126
paged_findings = get_page_items_and_count(request, prefetch_for_findings(findings.qs), 25, prefix="findings")
127127
fix_available_count = findings.qs.filter(fix_available=True).count()
128128

dojo/tools/generic/csv_parser.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ def _get_findings_csv(self, filename):
103103
if "fix_available" in row:
104104
finding.fix_available = bool(row["fix_available"])
105105

106+
if "fix_version" in row:
107+
finding.fix_version = row["fix_version"]
108+
106109
# manage endpoints
107110
if row.get("Url"):
108111
if settings.V3_FEATURE_LOCATIONS:

dojo/tools/generic/json_parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def _get_test_json(self, data):
110110
"known_exploited",
111111
"ransomware_used",
112112
"fix_available",
113+
"fix_version",
113114
}.union(required)
114115
not_allowed = sorted(set(item).difference(allowed))
115116
if not_allowed:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Date,Title,CweId,Url,Severity,Description,Mitigation,Impact,References,Active,Verified,fix_available,fix_version
2+
01/15/2025,Test finding with fix_version,502,,High,Vulnerability with fix version available,Upgrade to 2.1.3,,,TRUE,FALSE,TRUE,2.1.3
3+
01/15/2025,Test finding without fix_version,611,,Medium,Vulnerability without fix version,,,,TRUE,FALSE,FALSE,

0 commit comments

Comments
 (0)