贡献解析器 (Open Source)

所有命令均假设您位于 django-DefectDojo 克隆仓库的根目录下。

前提条件

  • 您已经 fork 了 https://github.com/DefectDojo/django-DefectDojo 并克隆到本地。
  • 检出(checkout)dev 分支,并确保已同步到最新更改。
  • 建议您为开发创建一个专用分支,例如 git checkout -b parser-name

使用 docker compose 部署方式最为简便,因为它具备 uWSGI 的热重载能力。 将您的环境设置为使用 dev 环境:

$ docker/setEnv.sh dev

更多详情请参阅 DOCKER.md

Docker 镜像

您需要在本地构建 docker 镜像,并最终传入本地用户的 uid,以便能够写入镜像内容(这对数据库迁移文件很有用)。假设您用户的 uid1000,则:

$ docker compose build --build-arg uid=1000

您需要修改哪些文件?

文件用途
dojo/tools/<parser_dir>/__init__.py用于类初始化的空文件
dojo/tools/<parser_dir>/parser.py核心内容。您在此处编写实际的解析器。类名必须是不含下划线的 Python 模块名加上 Parser。**示例:**当 Python 模块名为 dependency_check 时,类名应为 DependencyCheckParser
unittests/scans/<parser_dir>/{many_vulns,no_vuln,one_vuln}.json包含有意义数据的示例文件,用于单元测试。这是最小集合。
unittests/tools/test_<parser_name>_parser.py该解析器的单元测试。
dojo/settings/settings.dist.py如果您想使用基于现代哈希码的去重算法
docs/content/supported_tools/<file/api>/<parser_file>.md文档,说明所需的文件格式类型以及如何获取该文件

工厂契约

解析器通过工厂模式动态加载。要使您的解析器被正确加载并正常工作,您需要实现该契约。

  1. 您的解析器必须位于模块 dojo.tools 的子模块中
    • 例如:dojo.tools.my_tool.parser 模块
  2. 您的解析器必须是该子模块中的一个类。
    • 例如:dojo.tools.my_tool.parser.MyToolParser
  3. 该类的名称必须是不含下划线的 Python 模块名,并加上 Parser 后缀。
    • 例如:dojo.tools.my_tool.parser.MyToolParser
  4. 该类必须具有空构造函数,或不含构造函数
  5. 该类必须实现 4 个方法:
    1. def get_scan_types(self) 该函数返回您的解析器支持的所有 scan_type 的列表。这些标识符在内部使用。您的解析器可以支持多个 scan_type。例如,某些解析器使用不同的标识符来修改解析器的行为(聚合、过滤等)
    2. def get_label_for_scan_types(self, scan_type): 该函数返回一个字符串,用于在 UI 中提供部分文本(短标签)
    3. def get_description_for_scan_types(self, scan_type): 该函数返回一个字符串,用于在 UI 中提供部分文本(长描述)
    4. def get_findings(self, file, test) 该函数返回发现项(findings)列表
  6. 如果您的解析器有多个 scan_type(用于详细模式),您必须实现 def set_mode(self, mode) 方法
  7. 解析器实例会在针对该 scan_type 执行的所有导入操作中被重复使用,因此请勿在类级别存储任何数据

示例:


class MyToolParser(object):
    def get_scan_types(self):
        return ["My Tool Scan", "My Tool Scan detailed"]

    def get_label_for_scan_types(self, scan_type):
        if scan_type == "My Tool Scan":
            return "My Tool XML Scan aggregated by ..."
        else:
            return "My Tool XML Scan"

    def get_description_for_scan_types(self, scan_type):
        return "Aggregates findings per cwe, title, description, file_path. SonarQube output file can be imported in HTML format. Generate with https://github.com/soprasteria/sonar-report version >= 1.1.0"

    def requires_file(self, scan_type):
        return False

    # mode:
    # None (default): aggregates vulnerabilites per sink filename (legacy behavior)
    # 'detailed' : No aggregation
    mode = None

    def set_mode(self, mode):
        self.mode = mode

    def get_findings(self, file, test):
        <...>

API 解析器

DefectDojo 目前拥有数量有限的 API 解析器。虽然我们不会移除这些连接器,但添加 API 连接器一直存在问题,出于可支持性方面的考虑,我们目前无法接受社区提交的新 API 解析器/连接器。要维持高质量的 API 连接器,必须拥有该工具的许可证,而获得许可证需要与作者或供应商建立合作关系。我们即将宣布一项新计划,以帮助解决这一问题,并将 API 连接器引入 DefectDojo。

模板生成器

使用模板解析器可以快速生成所需的文件。开始之前,您需要安装 cookiecutter

$ pip install cookiecutter

然后从 django-DefectDojo 的根目录生成您的扫描器解析器:

$ cookiecutter https://github.com/DefectDojo/cookiecutter-scanner-parser

阅读更多信息,了解模板配置变量。

需要注意的事项

以下是一些注意事项,可使解析器在常见情况和边缘情况下都更加健壮。

不要手动解析 URL

我们使用 2 个模块来处理端点:

  • hyperlink
  • dojo.models,其中有一个专门的类用于处理围绕 URL 创建端点 Endpoint 的相关逻辑。

所有现有解析器都使用相同的代码来解析 URL 并创建端点。 使用 Endpoint.from_uri() 是创建端点的最佳方式。 如果您确实需要解析 URL,请使用 hyperlink 模块。

良好示例:

    if "url" in item:
        endpoint = Endpoint.from_uri(item["url"])
        finding.unsaved_endpoints = [endpoint]

非常糟糕的示例:

    u = urlparse(item["url"])
    endpoint = Endpoint(host=u.host)
    finding.unsaved_endpoints = [endpoint]

使用合适的库来解析信息

各种文件格式都是通过库来处理的。为了保持 DefectDojo 的精简,同时不扩大攻击面,请将所使用的库数量保持在最少,并以其他解析器为参考。

使用 defusedXML 而非 lxml

由于 xml 默认是一种不安全的格式,从各种 xml 输出中解析出的信息必须以安全的方式进行解析。经过评估,我们确定今后在解析器中解析 xml 文件时将使用 defusedXML 库,因为该库被评定为更安全。因此,我们只会接受使用 defusedxml 库的 PR。

并非所有属性都是必填的

解析器可能包含许多字段,其中许多可能是可选的。 如果没有相应的数据,最好不设置该属性,而不是填入 NANo data 等值……

请查看 dojo.models.Finding

源报告中可能缺少数据

对于那些您不能完全确定一定会出现在上传文件中的字段,请务必加入检查,以避免潜在的 KeyError 错误(例如字段不存在)。这类错误会转化为 500 错误,观感不佳。

良好示例:

   if "mykey" in data:
       finding.cwe = data["mykey"]
   finding.cwe = data.get("mykey", 123)
   some_list = data.get("key_of_the_list") or []

最后一个示例可以防范 key_of_the_list 存在但值为 null 的情况。

解析 CVSS 向量

数据中可能包含 CVSS 向量或分数。Defect Dojo 使用 RedHat Security 提供的 cvss 模块。 还有一个辅助方法,可用于验证该向量并从中提取基础分数和严重程度。

    from dojo.utils import parse_cvss_data

    cvss_vector = <get CVSS3 or CVSS4 vector from the report>
    cvss_data = parse_cvss_data(cvss_vector)
    if cvss_data:
        finding.severity = cvss_data["severity"]
        finding.cvssv3 = cvss_data["cvssv3"]
        finding.cvssv4 = cvss_data["cvssv4"]
        # we don't set any score fields as those will be overwritten by Defect Dojo

并非所有值都必须使用,因为扫描报告通常会提供自己的 severity 值。 有时也会提供 cvss_score。Defect Dojo 不会覆盖任何 cvss3_scorecvss4_score。 如果未设置分数,Defect Dojo 将使用 cvss 库来计算分数。 响应中还包含检测到的 CVSS 向量主版本号,位于 cvss_data["major_version"] 中。

如果您需要更多手动处理,可以直接解析 CVSS 向量。

使用示例:

    import cvss.parser
    from cvss import CVSS2, CVSS3, CVSS4

    # TEMPORARY: Use Defect Dojo implementation of `parse_cvss_from_text` white waiting for https://github.com/RedHatProductSecurity/cvss/pull/75 to be released
    vectors = cvss.parser.parse_cvss_from_text("CVSS:3.0/S:C/C:H/I:H/A:N/AV:P/AC:H/PR:H/UI:R/E:H/RL:O/RC:R/CR:H/IR:X/AR:X/MAC:H/MPR:X/MUI:X/MC:L/MA:X")
        if len(vectors) > 0 and type(vectors[0]) is CVSS3:
            print(vectors[0].severities())  # this is the 3 severities

            cvssv3 = vectors[0].clean_vector()
            severity = vectors[0].severities()[0]
            vectors[0].compute_base_score()
            cvssv3_score = vectors[0].scores()[0]
            finding.severity = severity
            finding.cvssv3_score = cvssv3_score

请勿采用以下这种做法:

    def get_severity(self, cvss, cvss_version="2.0"):
        cvss = float(cvss)
        cvss_version = float(cvss_version[:1])
        # If CVSS Version 3 and above
        if cvss_version >= 3:
            if cvss > 0 and cvss < 4:
                return "Low"
            elif cvss >= 4 and cvss < 7:
                return "Medium"
            elif cvss >= 7 and cvss < 9:
                return "High"
            elif cvss >= 9:
                return "Critical"
            else:
                return "Informational"
        # If CVSS Version prior to 3
        else:
            if cvss > 0 and cvss < 4:
                return "Low"
            elif cvss >= 4 and cvss < 7:
                return "Medium"
            elif cvss >= 7 and cvss <= 10:
                return "High"
            else:
                return "Informational"

去重算法

默认情况下,新的解析器使用"旧版(legacy)“去重算法,该算法记录在关于去重

请在适用的情况下使用预定义的去重算法。在哈希码配置中使用 unique_id_from_toolvuln_id_from_tool 字段时,重要的是这些值对于该发现项必须是唯一的,并且在后续扫描中长期保持不变。如果无法满足这一点,这些值仍然可以在发现项模型中设置,只是不用于去重。 这些值必须直接来自报告本身,而不能是解析器内部计算得出的值。

单元测试

每个解析器都必须有单元测试,至少要测试 0 个漏洞、1 个漏洞和多个漏洞的情况。您可以先参考其他解析器的做法。高质量的测试越多越好。

为发现项的各项属性添加检查非常重要。 例如:

        with self.subTest(i=0):
            finding = findings[0]
            self.assertEqual("test title", finding.title)
            self.assertEqual(True, finding.active)
            self.assertEqual(True, finding.verified)
            self.assertEqual(False, finding.duplicate)
            self.assertIn(finding.severity, Finding.SEVERITIES)
            self.assertEqual("CVE-2020-36234", finding.vulnerability_ids[0])
            self.assertEqual(261, finding.cwe)
            self.assertEqual("CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N", finding.cvssv3)
            self.assertIn("security", finding.tags)
            self.assertIn("network", finding.tags)
            self.assertEqual("3287f2d0-554f-491b-8516-3c349ead8ee5", finding.unique_id_from_tool)
            self.assertEqual("TEST1", finding.vuln_id_from_tool)

使用 with 打开示例文件

为确保文件句柄能够被正确关闭,请使用 with 语句模式来打开文件。 不要这样写:

    testfile = open("path_to_file.json")
    ...
    testfile.close()

而应这样写:

    with open("path_to_file.json") as testfile:
        ...

这样可以确保文件在 with 语句结束时被关闭,即使代码块中某处发生异常也是如此。

测试数据库

Django 使用一个名为 test_defectdojo 的独立测试数据库来运行单元测试。该数据库会自动创建,并初始化一组基本的测试数据。

运行您的测试

以下本地命令将启动您新解析器的单元测试

$ docker compose exec uwsgi bash -c 'python manage.py test unittests.tools.<your_unittest_py_file>.<main_class_name> -v2'

或者像这样:

$ ./run-unittest.sh --test-case unittests.tools.<your_unittest_py_file>.<main_class_name>

以 aqua 解析器为例:

$ docker compose exec uwsgi bash -c 'python manage.py test unittests.tools.test_aqua_parser.TestAquaParser -v2'

或者像这样:

$ ./run-unittest.sh --test-case unittests.tools.test_aqua_parser.TestAquaParser

如果您想运行所有解析器的单元测试,只需运行 $ docker-compose exec uwsgi bash -c 'python manage.py test -p "test_*_parser.py" -v2'

端点验证

某些类型的解析器会创建一个存在漏洞的端点列表(存储在 finding.unsaved_endpoints 中)。DefectDojo 要求以特定格式(遵循 RFC)存储端点。不符合该格式的端点仍可被存储,但会在 UI 中被标记为异常(红旗 🚩)。为确保您的解析器以正确格式存储端点,请在单元测试中对所有端点运行 .clean() 函数

findings = parser.get_findings(testfile, Test())
for finding in findings:
    for endpoint in finding.unsaved_endpoints:
        endpoint.clean()

测试 API 解析器

不仅要测试解析器,还应测试导入器(importer)。 unittest.mock 中的 patch 方法通常有助于模拟 API 响应。 强烈建议使用该方法。

可能涉及的其他文件

修改模型

如果您需要修改模型,例如增加数据库列的大小以容纳需要保存的更长字符串数据

  • dojo/models.py 中进行所需的修改

  • 通过运行以下命令在 dojo/db_migrations 中创建一个新的迁移文件,并将其包含在您的 PR 中

    $ docker compose exec uwsgi bash -c 'python manage.py makemigrations -v2'

接受不同类型的上传文件

如果您希望解析器能够接受新的文件类型,请查看 dojo/forms.py 中大约第 436 行(截至本文写作时)的位置,或找到出现字符串 attrs={"accept": 的 2 处位置(分别用于导入和重新导入)。

目前接受的格式:.xml、.csv、.nessus、.json、.html、.js、.zip。

不仅仅需要 parser.py

当然,没有什么能阻止您拥有比 parser.py 更多的文件。毕竟这是 Python :-)

Pull Request 示例

如果您想查看已经成为 DefectDojo 一部分的以往解析器,请访问 https://github.com/DefectDojo/django-DefectDojo/pulls?q=is%3Apr+sort%3Aupdated-desc+label%3A%22Import+Scans%22+is%3Aclosed

更新导入页面文档

请在 [docs/content/en/connecting_your_tools/parsers] 中添加一个新的 .md 文件,写明您新解析器的详细信息。请包含以下内容标题:

  • 可接受的文件类型——请说明如何从相关工具生成此类文件,因为某些工具具有多种生成方法或需要特定命令。
  • 一个示例单元测试代码块(如适用)。
  • 一个指向相关单元测试文件夹的链接,以便用户可以从文档中快速导航到该处。
  • 一个指向扫描器本身的链接(例如 GitHub 或供应商链接)

以下是一个已完成的解析器文档页面示例:https://github.com/DefectDojo/django-DefectDojo/blob/master/docs/content/supported_tools/file/acunetix.md