Skip to content

Commit c858eda

Browse files
committed
Merge branch 'dev' into security/remove-pickle
2 parents 7eac1d6 + 47b993c commit c858eda

5 files changed

Lines changed: 381 additions & 143 deletions

File tree

dojo/tools/api_sonarqube/importer.py

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import re
33
import textwrap
44

5+
import bleach
56
import html2text
7+
import markdown
68
from django.conf import settings
79
from django.core.exceptions import ValidationError
810
from lxml import etree
@@ -16,6 +18,44 @@
1618

1719

1820
class SonarQubeApiImporter:
21+
ALLOWED_RULE_DESCRIPTION_TAGS = [
22+
"a",
23+
"b",
24+
"blockquote",
25+
"br",
26+
"code",
27+
"em",
28+
"h1",
29+
"h2",
30+
"h3",
31+
"h4",
32+
"h5",
33+
"h6",
34+
"i",
35+
"li",
36+
"ol",
37+
"p",
38+
"pre",
39+
"strong",
40+
"table",
41+
"tbody",
42+
"td",
43+
"th",
44+
"thead",
45+
"tr",
46+
"ul",
47+
]
48+
ALLOWED_RULE_DESCRIPTION_ATTRIBUTES = {
49+
"a": [
50+
"href",
51+
"title",
52+
],
53+
}
54+
ALLOWED_RULE_DESCRIPTION_PROTOCOLS = [
55+
"http",
56+
"https",
57+
"mailto",
58+
]
1959

2060
"""
2161
This class imports from SonarQube (SQ) all open/confirmed SQ issues related to the project related to the test as
@@ -153,13 +193,13 @@ def import_issues(self, test):
153193
except KeyError:
154194
sonarqube_permalink = "No permalink \n"
155195

156-
# custom (user defined) SQ rules may not have 'htmlDesc'
157-
if "htmlDesc" in rule:
196+
rule_details = self.get_rule_details(rule)
197+
if rule_details:
158198
description = self.clean_rule_description_html(
159-
rule["htmlDesc"],
199+
rule_details,
160200
)
161-
cwe = self.clean_cwe(rule["htmlDesc"])
162-
references = sonarqube_permalink + self.get_references(rule["htmlDesc"])
201+
cwe = self.clean_cwe(rule_details)
202+
references = sonarqube_permalink + self.get_references(rule_details)
163203
else:
164204
description = ""
165205
cwe = None
@@ -338,8 +378,10 @@ def import_hotspots(self, test):
338378

339379
@staticmethod
340380
def clean_rule_description_html(raw_html):
381+
if not raw_html:
382+
return ""
341383
search = re.search(
342-
r"^(.*?)(?:(<h2>See</h2>)|(<b>References</b>))",
384+
r"^(.*?)(?:(<h2>See</h2>)|(<h2>References</h2>)|(<b>References</b>))",
343385
raw_html,
344386
re.DOTALL,
345387
)
@@ -356,6 +398,36 @@ def clean_cwe(raw_html):
356398
return int(search.group(1))
357399
return None
358400

401+
@staticmethod
402+
def get_rule_details(rule):
403+
if html_desc := rule.get("htmlDesc"):
404+
return SonarQubeApiImporter.sanitize_rule_details(html_desc)
405+
if not (md_desc := rule.get("mdDesc")):
406+
return ""
407+
# SonarQube 2025.x can return markdown-only rule descriptions, including
408+
# inline HTML that should still be treated as markdown content.
409+
return SonarQubeApiImporter.sanitize_rule_details(
410+
markdown.markdown(md_desc, extensions=["extra"]),
411+
)
412+
413+
@staticmethod
414+
def sanitize_rule_details(description):
415+
if not description:
416+
return ""
417+
sanitized_description = re.sub(
418+
r"<(script|style)\b[^>]*>.*?</\1>",
419+
"",
420+
description,
421+
flags=re.DOTALL | re.IGNORECASE,
422+
)
423+
return bleach.clean(
424+
sanitized_description,
425+
tags=SonarQubeApiImporter.ALLOWED_RULE_DESCRIPTION_TAGS,
426+
attributes=SonarQubeApiImporter.ALLOWED_RULE_DESCRIPTION_ATTRIBUTES,
427+
protocols=SonarQubeApiImporter.ALLOWED_RULE_DESCRIPTION_PROTOCOLS,
428+
strip=True,
429+
)
430+
359431
@staticmethod
360432
def convert_sonar_severity(sonar_severity):
361433
sev = sonar_severity.lower()
@@ -382,6 +454,8 @@ def convert_scanner_confidence(sonar_scanner_confidence):
382454

383455
@staticmethod
384456
def get_references(vuln_details):
457+
if not vuln_details:
458+
return ""
385459
parser = etree.HTMLParser()
386460
details = etree.fromstring(vuln_details, parser)
387461

dojo/utils.py

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1794,35 +1794,18 @@ def async_delete_task(model_label, pk, **kwargs):
17941794

17951795
# Step 2: Prepare duplicate clusters (must happen before any deletion)
17961796
# When CASCADE_DELETE=True, reconfigure_duplicate_cluster skips reconfiguration —
1797-
# we handle that below by expanding scope to include outside duplicates.
1797+
# and deletes any outside scope duplicates to avoid FK violations during chunked deletion.
17981798
prepare_duplicates_for_delete(obj)
17991799

1800-
# Step 3: Delete outside-scope duplicates first — these point to findings
1801-
# in the main scope via duplicate_finding FK, so they must be removed before
1802-
# the originals to avoid FK violations during chunked deletion.
1803-
scope_ids = finding_qs.values_list("id", flat=True)
1804-
outside_dupes_qs = (
1805-
Finding.objects.filter(duplicate_finding_id__in=scope_ids)
1806-
.exclude(id__in=scope_ids)
1807-
)
1800+
# Step 3: Delete the main scope findings
18081801
chunk_size = get_setting("ASYNC_OBEJECT_DELETE_CHUNK_SIZE")
1809-
outside_count = outside_dupes_qs.count()
1810-
if outside_count:
1811-
logger.info("ASYNC_DELETE: Deleting %d outside-scope duplicates first", outside_count)
1812-
bulk_delete_findings(
1813-
outside_dupes_qs,
1814-
chunk_size=chunk_size,
1815-
cascade_root=cascade_root,
1816-
)
1817-
1818-
# Step 4: Delete the main scope findings
18191802
bulk_delete_findings(
18201803
finding_qs,
18211804
chunk_size=chunk_size,
18221805
cascade_root=cascade_root,
18231806
)
18241807

1825-
# Step 5: Delete all remaining related objects (Tests, Engagements,
1808+
# Step 4: Delete all remaining related objects (Tests, Engagements,
18261809
# Endpoints, etc.) via SQL cascade. Findings are already gone, so
18271810
# skip_relations={Finding} avoids walking empty relations.
18281811
# Single transaction is fine here — the heavy relations (Findings,
@@ -1831,12 +1814,12 @@ def async_delete_task(model_label, pk, **kwargs):
18311814
with transaction.atomic():
18321815
cascade_delete_related_objects(type(obj), pk_query, skip_relations={Finding})
18331816

1834-
# Step 6: Delete the top-level object via ORM to fire Django signals
1817+
# Step 5: Delete the top-level object via ORM to fire Django signals
18351818
# (post_delete notifications, pghistory audit, Pro signals).
18361819
# All children are already gone so this is a single-row DELETE.
18371820
obj.delete()
18381821

1839-
# Step 7: Recalculate product grade once (Engagement/Test deletes only). Skip when the
1822+
# Step 6: Recalculate product grade once (Engagement/Test deletes only). Skip when the
18401823
# deleted object is the Product itself — it is removed in step 6 and grading is pointless.
18411824
# For Product TYpe deletiongs we don't have a product instance, so this never fires.
18421825
if product and not isinstance(obj, Product):
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"key": "typescript:S1854",
3+
"repo": "typescript",
4+
"name": "Dead stores should be removed",
5+
"createdAt": "2018-01-17T10:11:21-0500",
6+
"mdDesc": "A dead store happens when a local variable is assigned a value that is not read by any subsequent instruction. Calculating or retrieving a value only to then overwrite it or throw it away, could indicate a serious error in the code. Even if it's not an error, it is at best a waste of resources. Therefore all calculated values should be used.\n\n## Noncompliant Code Example\n\ni = a + b; // Noncompliant; calculation result not used before value is overwritten\ni = compute();\n\n## Compliant Solution\n\ni = a + b;\ni += compute();\n\n## Exceptions\n\nThis rule ignores initializations to -1, 0, 1, `null`, `true`, `false`, `\"\"`, `[]` and `{}`.\n\n## See\n\n- [MITRE, CWE-563](http://cwe.mitre.org/data/definitions/563.html) - Assignment to Variable without Use ('Unused Variable')\n- [CERT, MSC13-C.](https://www.securecoding.cert.org/confluence/x/QYA5) - Detect and remove unused values\n- [CERT, MSC56-J.](https://www.securecoding.cert.org/confluence/x/uQCSBg) - Detect and remove superfluous code and values\n",
7+
"severity": "MAJOR",
8+
"status": "READY",
9+
"isTemplate": false,
10+
"tags": [],
11+
"sysTags": [
12+
"cert",
13+
"cwe",
14+
"unused"
15+
],
16+
"lang": "ts",
17+
"langName": "TypeScript",
18+
"params": [],
19+
"defaultDebtRemFnType": "CONSTANT_ISSUE",
20+
"defaultDebtRemFnOffset": "15min",
21+
"debtOverloaded": false,
22+
"debtRemFnType": "CONSTANT_ISSUE",
23+
"debtRemFnOffset": "15min",
24+
"defaultRemFnType": "CONSTANT_ISSUE",
25+
"defaultRemFnBaseEffort": "15min",
26+
"remFnType": "CONSTANT_ISSUE",
27+
"remFnBaseEffort": "15min",
28+
"remFnOverloaded": false,
29+
"scope": "MAIN",
30+
"isExternal": false,
31+
"type": "CODE_SMELL"
32+
}

0 commit comments

Comments
 (0)