Skip to content

Commit 22545c6

Browse files
Maffoochclaude
andauthored
fix(importers): stop doubling the (scan_type) suffix in dynamic Test Type names (#15149)
* fix(importers): stop doubling the (scan_type) suffix in dynamic Test Type names Generic Findings Import (and other dynamic-test-type parsers like SARIF) built the Test Type name by unconditionally appending " Scan ({scan_type})" to the report's `type` field. When `type` already carried the " ({scan_type})" suffix, the suffix was doubled, e.g. "Prisma Cloud (Generic Findings Import) Scan (Generic Findings Import)". Extract the name resolution into resolve_dynamic_test_type_name(), which is now idempotent: a `type` already ending in " ({scan_type})" is used verbatim. The intentional "{type} Scan ({scan_type})" format is unchanged for all other cases (Tool1 -> "Tool1 Scan (Generic Findings Import)"), preserving SARIF and existing behavior. To avoid breaking existing data, the reimport validation accepts either the new idempotent name or the legacy (pre-patch) name via legacy_dynamic_test_type_name(), so reimports into tests whose test_type was created with the old doubled name keep working without a mismatch error. Also corrects the Generic Findings Import docs, which described a "{Test Name} (Generic Findings Import)" format the code never produced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(importers): restore ' Scan'-suffix guard to stop re-doubling dynamic Test Type names The idempotency rewrite dropped the legacy guard that returned the scan type as-is when '{type} Scan' already equals the scan_type. That re-introduced the exact doubling the PR removes for dynamic parsers whose scan_type ends in ' Scan' (Horusec, AWS Security Hub, Rusty Hog variants), e.g. 'Horusec Scan' became 'Horusec Scan (Horusec Scan)' on fresh import. Restore the guard and add a regression test importing a Horusec scan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fa72db5 commit 22545c6

8 files changed

Lines changed: 276 additions & 34 deletions

File tree

docs/content/asset_modelling/OS_hierarchy/OS__asset_hierarchy.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ Examples include:
159159
**Report-Defined Test Type Naming Rules:**
160160
- If the report's `type` field equals the scan type → uses scan type directly (e.g., "Generic Findings Import")
161161
- If the report's `type` field differs → creates "{type} Scan ({scan_type})" format (e.g., "Tool1 Scan (Generic Findings Import)")
162+
- If the report's `type` field already ends with the " ({scan_type})" suffix → uses it verbatim, so the suffix is never doubled (e.g., "Tool1 (Generic Findings Import)" stays "Tool1 (Generic Findings Import)")
162163
- If no `type` field is provided → uses scan type directly
163164

164165
**Important Considerations:**

docs/content/asset_modelling/engagements_tests/OS__tests.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ While each method differs primarily in how scan data is parsed and ingested, the
3737

3838
When no native parser exists for a given tool, **Generic Findings Import** allows you to import findings using a standardized JSON or CSV schema, regardless of the original source.
3939

40-
DefectDojo parses the provided data, creates a new Test (or imports into an existing one), and attaches the Findings. A corresponding Test Type is also created in the format “{Test Name} (Generic Findings Import).”
40+
DefectDojo parses the provided data, creates a new Test (or imports into an existing one), and attaches the Findings. A corresponding Test Type is also created based on the report's optional `type` field: when `type` is omitted (or equals the scan type) the Test Type is “Generic Findings Import”; when `type` is provided it becomes “{type} Scan (Generic Findings Import)” (a `type` that already ends with the “(Generic Findings Import)” suffix is used verbatim).
4141

4242
| | **Native Parsers** | **Generic Findings Import** |
4343
|----------|---------------|------------------------|

docs/content/asset_modelling/engagements_tests/PRO__tests.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ While each method differs primarily in how scan data is parsed and ingested, the
3535

3636
When no native parser exists for a given tool, [**Generic Findings Import**](/supported_tools/parsers/generic_findings_import) allows you to import findings using a standardized JSON or CSV schema, regardless of the original source.
3737

38-
DefectDojo parses the provided data, creates a new Test (or imports into an existing one), and attaches the Findings. A corresponding Test Type is also created in the format “`{Test Name}` (Generic Findings Import).”
38+
DefectDojo parses the provided data, creates a new Test (or imports into an existing one), and attaches the Findings. A corresponding Test Type is also created based on the report's optional `type` field: when `type` is omitted (or equals the scan type) the Test Type is “Generic Findings Import”; when `type` is provided it becomes “`{type}` Scan (Generic Findings Import)” (a `type` that already ends with the “(Generic Findings Import)” suffix is used verbatim).
3939

4040
#### Universal Parser
4141

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ Example:
226226
}
227227
```
228228

229+
The resulting Test Type name is derived from the `type` field: when `type` is omitted (or equals the scan type) the Test Type is `Generic Findings Import`; when `type` is provided it becomes `{type} Scan (Generic Findings Import)` (for the example above, `My custom Test type Scan (Generic Findings Import)`). A `type` that already ends with the `(Generic Findings Import)` suffix is used verbatim, so the suffix is never doubled.
230+
229231
### Sample Scan Data
230232

231233
Sample Generic Findings Import scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/generic).

docs/content/supported_tools/parsers/generic_findings_import.md

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,28 @@ weight: 2
66

77
Open-source and Pro users can use Generic Findings Import as a method to ingest JSON or CSV files into DefectDojo which are not already in the supported Tools list.
88

9-
Using Generic Findings Import will create a new Test Type in your DefectDojo instance called "`{The Name Of Your Test}` (Generic Findings Import)". For example, this JSON content will result in a Test Type called "Example Report (Generic Findings Import)":
10-
11-
```
12-
{
13-
"name": "Example Report",
14-
"findings": []
15-
}
16-
```
9+
Using Generic Findings Import creates a Test Type in your DefectDojo instance based on the optional `type` field in the report. The naming rules are:
10+
11+
- If no `type` field is provided (or it equals the scan type), the Test Type is simply **"Generic Findings Import"**. For example, this JSON content results in the Test Type "Generic Findings Import":
12+
13+
```
14+
{
15+
"name": "Example Report",
16+
"findings": []
17+
}
18+
```
19+
20+
- If a `type` field is provided and differs from the scan type, the Test Type is **"`{type}` Scan (Generic Findings Import)"**. For example, `"type": "Tool1"` results in the Test Type "Tool1 Scan (Generic Findings Import)":
21+
22+
```
23+
{
24+
"name": "Example Report",
25+
"type": "Tool1",
26+
"findings": []
27+
}
28+
```
29+
30+
- If the `type` field already ends with the "(Generic Findings Import)" suffix, it is used verbatim (the suffix is never doubled). For example, `"type": "Tool1 (Generic Findings Import)"` results in the Test Type "Tool1 (Generic Findings Import)".
1731

1832
DefectDojo Pro users can also consider using the [Universal Parser](../universal_parser), a tool which allows for highly customizable JSON, XML and CSV imports.
1933

dojo/importers/base_importer.py

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -282,33 +282,18 @@ def consolidate_dynamic_tests(self, tests: list[Test]) -> list[Finding]:
282282
# only if they are different. This is to support meta format like SARIF
283283
# so a report that have the label 'CodeScanner' will be changed to 'CodeScanner Scan (SARIF)'
284284
test_raw = tests[0]
285-
test_type_name = self.scan_type
286285
# Create a new test if it has not already been created
287286
if not self.test:
288-
# Determine if we should use a custom test type name
289-
if test_raw.type:
290-
# If test_raw.type equals scan_type, use scan_type directly
291-
if test_raw.type == self.scan_type:
292-
test_type_name = self.scan_type
293-
else:
294-
test_type_name = f"{tests[0].type} Scan"
295-
if test_type_name != self.scan_type:
296-
test_type_name = f"{test_type_name} ({self.scan_type})"
297-
self.test = self.create_test(test_type_name)
287+
# Resolve the Test_Type name from the report's type (idempotent: a type that already
288+
# carries the " (scan_type)" suffix is used verbatim rather than doubled)
289+
self.test = self.create_test(self.resolve_dynamic_test_type_name(test_raw.type))
298290
else:
299-
# During reimport, validate that the test_type matches
300-
# Calculate the expected test_type_name from the incoming report
301-
expected_test_type_name = self.scan_type
302-
if test_raw.type:
303-
# If test_raw.type equals scan_type, use scan_type directly
304-
if test_raw.type == self.scan_type:
305-
expected_test_type_name = self.scan_type
306-
else:
307-
expected_test_type_name = f"{test_raw.type} Scan"
308-
if expected_test_type_name != self.scan_type:
309-
expected_test_type_name = f"{expected_test_type_name} ({self.scan_type})"
310-
# Compare with existing test's test_type name
311-
if self.test.test_type.name != expected_test_type_name:
291+
# During reimport, validate that the test_type matches the incoming report.
292+
# Accept either the current (idempotent) name or the legacy name the pre-patch code
293+
# produced, so reimports into tests created before the doubling fix keep working.
294+
expected_test_type_name = self.resolve_dynamic_test_type_name(test_raw.type)
295+
legacy_test_type_name = self.legacy_dynamic_test_type_name(test_raw.type)
296+
if self.test.test_type.name not in {expected_test_type_name, legacy_test_type_name}:
312297
msg = (
313298
f"Test type mismatch: Test {self.test.id} has test_type '{self.test.test_type.name}', "
314299
f"but the report contains test_type '{expected_test_type_name}'. "
@@ -653,6 +638,42 @@ def update_test_progress(
653638
self.test.percent_complete = percentage_value
654639
self.test.save()
655640

641+
def resolve_dynamic_test_type_name(self, raw_type: str | None) -> str:
642+
"""
643+
Compute the Test_Type name for a dynamic-test-type report (e.g. Generic, SARIF).
644+
645+
- No type, or type already equals the scan type -> use the scan type as-is.
646+
- Type already carries the scan-type suffix (e.g. "Prisma Cloud (Generic Findings
647+
Import)") -> use it verbatim. This keeps the composition idempotent and prevents
648+
doubled names like "X (scan_type) Scan (scan_type)".
649+
- Type plus a " Scan" suffix already equals the scan type (e.g. type "Horusec" with
650+
scan_type "Horusec Scan") -> use the scan type as-is. This preserves the behavior of
651+
dynamic parsers whose scan_type already ends in " Scan" (Horusec, AWS Security Hub,
652+
Rusty Hog, ...) so their Test_Type name is not doubled into "Horusec Scan (Horusec Scan)".
653+
- Otherwise -> the intentional "{type} Scan ({scan_type})" format (also used by SARIF,
654+
so a report labeled 'CodeScanner' becomes 'CodeScanner Scan (SARIF)').
655+
"""
656+
if not raw_type or raw_type == self.scan_type:
657+
return self.scan_type
658+
if raw_type.endswith(f" ({self.scan_type})"):
659+
return raw_type
660+
if f"{raw_type} Scan" == self.scan_type:
661+
return self.scan_type
662+
return f"{raw_type} Scan ({self.scan_type})"
663+
664+
def legacy_dynamic_test_type_name(self, raw_type: str | None) -> str:
665+
"""
666+
Reproduce the name the pre-idempotency code produced. Used only for the reimport
667+
compatibility check so reimports into existing (pre-patch) tests whose test_type
668+
already has a doubled suffix keep working instead of raising a mismatch error.
669+
"""
670+
if not raw_type or raw_type == self.scan_type:
671+
return self.scan_type
672+
name = f"{raw_type} Scan"
673+
if name != self.scan_type:
674+
name = f"{name} ({self.scan_type})"
675+
return name
676+
656677
def get_or_create_test_type(
657678
self,
658679
test_type_name: str,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "Prisma Cloud",
3+
"type": "Prisma Cloud (Generic Findings Import)",
4+
"findings": [
5+
{
6+
"title": "Test Finding Prisma",
7+
"description": "This is a test finding whose type already carries the scan-type suffix",
8+
"severity": "High",
9+
"active": true,
10+
"verified": true
11+
}
12+
]
13+
}

0 commit comments

Comments
 (0)