Skip to content

Commit f2eb2ec

Browse files
committed
Generic Parser: Support Test Type Meta
1 parent d9b49c5 commit f2eb2ec

File tree

10 files changed

+340
-86
lines changed

10 files changed

+340
-86
lines changed

docs/content/en/connecting_your_tools/parsers/generic_findings_import.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ You can use Generic Findings Import as a method to ingest JSON or CSV files into
99
Files uploaded using Generic Findings Import must conform to the accepted format with respect to CSV column headers / JSON attributes.
1010

1111
These attributes are supported for CSV:
12+
1213
- Date: Date of the finding in mm/dd/yyyy format.
1314
- Title: Title of the finding
1415
- CweId: Cwe identifier, must be an integer value.
@@ -104,18 +105,32 @@ Example:
104105
}
105106
```
106107

107-
This parser supports an attribute `name` and `type` to be able to define `TestType`. Based on this, you can define custom `HASHCODE_FIELDS` or `DEDUPLICATION_ALGORITHM` in the settings.
108+
This parser supports some additional attributes to be able to define custom `TestTypes` as well as influencing some meta fields on the `Test`:
109+
110+
- `name`: The internal name of the tool you are using. This is primarily informational, and used for reading the report manually.
111+
- `type`: The name of the test type to create in DefectDojo with the suffix of `(Generic Findings Import)`. The suffix is an important identifier for future users attempting to identify the test type to supply when importing new reports. This value is very important when fetching the correct test type to import findings into, so be sure to keep the `type` consistent from import to import! As an example, a report submitted with a `type` of `Internal Company Tool` will produce a test type in DefectDojo with the title `Internal Company Tool (Generic Findings Import)`. With this newly created test type, you can define custom `HASHCODE_FIELDS` or `DEDUPLICATION_ALGORITHM` in the settings.
112+
- `version`: The version of the tool you are using. This is primarily informational, and is used for reading the report manually and tracking format changes from version to version.
113+
- `description`: A brief description of the test. This could be an explanation of what the tool is reporting, where the tools is maintained, who the point of contact is for the tool when issues arise, or anything in between.
114+
- `static_tool`: Dictates that tool used is running static analysis methods to discover vulnerabilities.
115+
- `dynamic_tool`: Dictates that tool used is running dynamic analysis methods to discover vulnerabilities.
116+
- `soc`: Dictates that tool is used for reporting alerts from a soc (Pro Edition Only).
108117

109118
Example:
110119

111120
```JSON
112121
{
113122
"name": "My wonderful report",
114123
"type": "My custom Test type",
124+
"version": "1.0.5",
125+
"description": "A unicorn tool that is capable of static analysis, dynamic analysis, and even capturing soc alerts!",
126+
"static_tool": true,
127+
"dynamic_tool": true,
128+
"soc": true,
115129
"findings": [
116130
]
117131
}
118132
```
119133

120134
### Sample Scan Data
135+
121136
Sample Generic Findings Import scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/generic).

dojo/importers/base_importer.py

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
)
3434
from dojo.notifications.helper import create_notification
3535
from dojo.tools.factory import get_parser
36+
from dojo.tools.parser_test import ParserTest
3637
from dojo.utils import max_safe
3738

3839
logger = logging.getLogger(__name__)
@@ -179,15 +180,36 @@ def parse_dynamic_test_type_tests(
179180
logger.warning(e)
180181
raise ValidationError(e)
181182

182-
def parse_dynamic_test_type_findings_from_tests(
183-
self,
184-
tests: list[Test],
185-
) -> list[Finding]:
186-
"""
187-
Currently we only support import one Test
188-
so for parser that support multiple tests (like SARIF)
189-
we aggregate all the findings into one uniq test
190-
"""
183+
def consolidate_dynamic_tests(self, tests: list[Test]) -> list[Finding]:
184+
parsed_findings = []
185+
# Make sure we have at least one test returned
186+
if len(tests) == 0:
187+
logger.info(f"No tests found in import for {self.scan_type}")
188+
self.test = None
189+
return parsed_findings
190+
# for now we only consider the first test in the list and artificially aggregate all findings of all tests
191+
# this is the same as the old behavior as current import/reimporter implementation doesn't handle the case
192+
# when there is more than 1 test
193+
#
194+
# we also aggregate the label of the Test_type to show the user the original self.scan_type
195+
# only if they are different. This is to support meta format like SARIF
196+
# so a report that have the label 'CodeScanner' will be changed to 'CodeScanner Scan (SARIF)'
197+
test_raw = tests[0]
198+
test_type_name = self.scan_type
199+
# Create a new test if it has not already been created
200+
if not self.test:
201+
# Determine if we should use a custom test type name
202+
if test_raw.type:
203+
test_type_name = f"{tests[0].type} Scan"
204+
if test_type_name != self.scan_type:
205+
test_type_name = f"{test_type_name} ({self.scan_type})"
206+
self.test = self.create_test(test_type_name)
207+
# This part change the name of the Test
208+
# we get it from the data of the parser
209+
# Update the test and test type with meta from the raw test
210+
self.update_test_from_internal_test(test_raw)
211+
self.update_test_type_from_internal_test(test_raw)
212+
# Aggregate all of the findings into a single place
191213
parsed_findings = []
192214
for test_raw in tests:
193215
parsed_findings.extend(test_raw.findings)
@@ -205,7 +227,7 @@ def parse_findings_dynamic_test_type(
205227
This version of this function is intended to be extended by children classes
206228
"""
207229
tests = self.parse_dynamic_test_type_tests(scan, parser)
208-
return self.parse_dynamic_test_type_findings_from_tests(tests)
230+
return self.consolidate_dynamic_tests(tests)
209231

210232
def parse_findings(
211233
self,
@@ -215,12 +237,12 @@ def parse_findings(
215237
"""
216238
Determine how to parse the findings based on the presence of the
217239
`get_tests` function on the parser object
218-
219-
This function will vary by importer, so it is marked as
220-
abstract with a prohibitive exception raised if the
221-
method is attempted to to be used by the BaseImporter class
222240
"""
223-
self.check_child_implementation_exception()
241+
# Attempt any preprocessing before generating findings
242+
scan = self.process_scan_file(scan)
243+
if hasattr(parser, "get_tests"):
244+
return self.parse_findings_dynamic_test_type(scan, parser)
245+
return self.parse_findings_static_test_type(scan, parser)
224246

225247
def sync_process_findings(
226248
self,
@@ -539,6 +561,22 @@ def get_or_create_test_type(
539561
test_type.save()
540562
return test_type
541563

564+
def update_test_from_internal_test(self, internal_test: ParserTest) -> None:
565+
if (name := getattr(internal_test, "name", None)) is not None:
566+
self.test.name = name
567+
if (description := getattr(internal_test, "description", None)) is not None:
568+
self.test.description = description
569+
if (version := getattr(internal_test, "version", None)) is not None:
570+
self.test.version = version
571+
self.test.save()
572+
573+
def update_test_type_from_internal_test(self, internal_test: ParserTest) -> None:
574+
if (static_tool := getattr(internal_test, "static_tool", None)) is not None:
575+
self.test.test_type.static_tool = static_tool
576+
if (dynamic_tool := getattr(internal_test, "dynamic_tool", None)) is not None:
577+
self.test.test_type.dynamic_tool = dynamic_tool
578+
self.test.test_type.save()
579+
542580
def verify_tool_configuration_from_test(self):
543581
"""
544582
Verify that the Tool_Configuration supplied along with the

dojo/importers/default_importer.py

Lines changed: 1 addition & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -320,21 +320,6 @@ def close_old_findings(
320320

321321
return old_findings
322322

323-
def parse_findings(
324-
self,
325-
scan: TemporaryUploadedFile,
326-
parser: Parser,
327-
) -> list[Finding]:
328-
"""
329-
Determine how to parse the findings based on the presence of the
330-
`get_tests` function on the parser object
331-
"""
332-
# Attempt any preprocessing before generating findings
333-
scan = self.process_scan_file(scan)
334-
if hasattr(parser, "get_tests"):
335-
return self.parse_findings_dynamic_test_type(scan, parser)
336-
return self.parse_findings_static_test_type(scan, parser)
337-
338323
def parse_findings_static_test_type(
339324
self,
340325
scan: TemporaryUploadedFile,
@@ -364,40 +349,7 @@ def parse_findings_dynamic_test_type(
364349
into a single test, and then renames the test is applicable
365350
"""
366351
logger.debug("IMPORT_SCAN parser v2: Create Test and parse findings")
367-
tests = self.parse_dynamic_test_type_tests(scan, parser)
368-
parsed_findings = []
369-
# Make sure we have at least one test returned
370-
if len(tests) == 0:
371-
logger.info(f"No tests found in import for {self.scan_type}")
372-
self.test = None
373-
return parsed_findings
374-
# for now we only consider the first test in the list and artificially aggregate all findings of all tests
375-
# this is the same as the old behavior as current import/reimporter implementation doesn't handle the case
376-
# when there is more than 1 test
377-
#
378-
# we also aggregate the label of the Test_type to show the user the original self.scan_type
379-
# only if they are different. This is to support meta format like SARIF
380-
# so a report that have the label 'CodeScanner' will be changed to 'CodeScanner Scan (SARIF)'
381-
test_type_name = self.scan_type
382-
# Determine if we should use a custom test type name
383-
if tests[0].type:
384-
test_type_name = f"{tests[0].type} Scan"
385-
if test_type_name != self.scan_type:
386-
test_type_name = f"{test_type_name} ({self.scan_type})"
387-
# Create a new test if it has not already been created
388-
if not self.test:
389-
self.test = self.create_test(test_type_name)
390-
# This part change the name of the Test
391-
# we get it from the data of the parser
392-
test_raw = tests[0]
393-
if test_raw.name:
394-
self.test.name = test_raw.name
395-
if test_raw.description:
396-
self.test.description = test_raw.description
397-
self.test.save()
398-
logger.debug("IMPORT_SCAN parser v2: Parse findings (aggregate)")
399-
# Aggregate all the findings and return them with the newly created test
400-
return self.parse_dynamic_test_type_findings_from_tests(tests)
352+
return super().parse_findings_dynamic_test_type(scan, parser)
401353

402354
def async_process_findings(
403355
self,

dojo/importers/default_reimporter.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -288,21 +288,6 @@ def close_old_findings(
288288

289289
return mitigated_findings
290290

291-
def parse_findings(
292-
self,
293-
scan: TemporaryUploadedFile,
294-
parser: Parser,
295-
) -> list[Finding]:
296-
"""
297-
Determine how to parse the findings based on the presence of the
298-
`get_tests` function on the parser object
299-
"""
300-
# Attempt any preprocessing before generating findings
301-
scan = self.process_scan_file(scan)
302-
if hasattr(parser, "get_tests"):
303-
return self.parse_findings_dynamic_test_type(scan, parser)
304-
return self.parse_findings_static_test_type(scan, parser)
305-
306291
def parse_findings_static_test_type(
307292
self,
308293
scan: TemporaryUploadedFile,

dojo/tools/generic/json_parser.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ def _get_test_json(self, data):
1515
name=data.get("name", self.ID),
1616
parser_type=data.get("type", self.ID),
1717
version=data.get("version"),
18+
description=data.get("description"),
19+
dynamic_tool=data.get("dynamic_tool"),
20+
static_tool=data.get("static_tool"),
21+
soc=data.get("soc"),
1822
)
1923
test_internal.findings = []
2024
for item in data.get("findings", []):

dojo/tools/parser_test.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,41 @@
1+
import importlib
2+
from contextlib import suppress
3+
4+
from django.conf import settings
5+
6+
17
class ParserTest:
2-
def __init__(self, name: str, parser_type: str, version: str):
3-
self.name = name
4-
self.type = parser_type
5-
self.version = version
6-
self.description = None
8+
def __init__(self, *args: list, **kwargs: dict):
9+
parser_test_class = OpenSourceParserTest
10+
with suppress(ModuleNotFoundError):
11+
if (
12+
class_path := getattr(settings, "PARSER_TEST_CLASS_PATH", None)
13+
) is not None:
14+
module_name, _separator, class_name = class_path.rpartition(".")
15+
module = importlib.import_module(module_name)
16+
parser_test_class = getattr(module, class_name)
17+
parser_test_class().apply(self, *args, **kwargs)
18+
19+
20+
class OpenSourceParserTest:
21+
def apply(
22+
self,
23+
instance: ParserTest,
24+
name: str,
25+
parser_type: str,
26+
version: str,
27+
*args: list,
28+
description: str | None,
29+
dynamic_tool: bool | None,
30+
static_tool: bool | None,
31+
**kwargs: dict,
32+
):
33+
instance.name = name
34+
instance.type = parser_type
35+
instance.version = version
36+
if description is not None:
37+
instance.description = description
38+
if dynamic_tool is not None:
39+
instance.dynamic_tool = dynamic_tool
40+
if static_tool is not None:
41+
instance.static_tool = static_tool

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,4 @@ vulners==2.3.6
7575
fontawesomefree==6.6.0
7676
PyYAML==6.0.2
7777
pyopenssl==25.0.0
78+
parameterized==0.9.0
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "Test 1",
3+
"type": "Tool 1",
4+
"version": "1.0.0",
5+
"description": "The contents of this report is from a tool that gathers vulnerabilities both statically and dynamically",
6+
"dynamic_tool": true,
7+
"static_tool": true,
8+
"findings": [
9+
{
10+
"title": "test title",
11+
"description": "Some very long description with\n\n some UTF-8 chars à qu'il est beau",
12+
"severity": "Medium"
13+
}
14+
]
15+
}

0 commit comments

Comments
 (0)