|
| 1 | +import ast |
1 | 2 | import logging |
2 | 3 | import uuid |
| 4 | +from pathlib import Path |
3 | 5 | from unittest.mock import patch |
4 | 6 |
|
5 | 7 | from django.core.exceptions import ValidationError |
@@ -933,3 +935,62 @@ def test_change_vulnerability_ids_on_reimport(self): |
933 | 935 | vuln_ids = list(Vulnerability_Id.objects.filter(finding=finding).values_list("vulnerability_id", flat=True)) |
934 | 936 | self.assertEqual(set(new_vulnerability_ids), set(vuln_ids)) |
935 | 937 | finding.delete() |
| 938 | + |
| 939 | + |
| 940 | +class TestParserPolicy(DojoTestCase): |
| 941 | + @staticmethod |
| 942 | + def _iter_parser_files(tools_root): |
| 943 | + yield from tools_root.rglob("parser.py") |
| 944 | + |
| 945 | + @staticmethod |
| 946 | + def _finding_service_offenders(repo_root, parser_file): |
| 947 | + try: |
| 948 | + source = parser_file.read_text(encoding="utf-8") |
| 949 | + tree = ast.parse(source) |
| 950 | + except (OSError, SyntaxError): |
| 951 | + return [] |
| 952 | + |
| 953 | + offenders = [] |
| 954 | + for node in ast.walk(tree): |
| 955 | + if not isinstance(node, ast.Call): |
| 956 | + continue |
| 957 | + |
| 958 | + func = node.func |
| 959 | + is_finding_call = ( |
| 960 | + isinstance(func, ast.Name) and func.id == "Finding" |
| 961 | + ) or ( |
| 962 | + isinstance(func, ast.Attribute) and func.attr == "Finding" |
| 963 | + ) |
| 964 | + |
| 965 | + if not is_finding_call: |
| 966 | + continue |
| 967 | + |
| 968 | + has_service_kwarg = any(keyword.arg == "service" for keyword in node.keywords if keyword.arg) |
| 969 | + if has_service_kwarg: |
| 970 | + rel_path = parser_file.relative_to(repo_root) |
| 971 | + offenders.append(f"{rel_path}:{node.lineno}") |
| 972 | + |
| 973 | + return offenders |
| 974 | + |
| 975 | + def test_parsers_must_not_set_service_on_finding_directly(self): |
| 976 | + """ |
| 977 | + Policy test: parser implementations must not set `Finding.service` directly. |
| 978 | +
|
| 979 | + Rationale: |
| 980 | + - `service` should be controlled via import/reimport options and not parser-specific mapping. |
| 981 | + - Direct parser assignment leads to inconsistent close-old-findings and dedupe behavior. |
| 982 | + """ |
| 983 | + repo_root = Path(__file__).resolve().parents[1] |
| 984 | + tools_root = repo_root / "dojo" / "tools" |
| 985 | + |
| 986 | + offenders = [] |
| 987 | + |
| 988 | + for parser_file in self._iter_parser_files(tools_root): |
| 989 | + offenders.extend(self._finding_service_offenders(repo_root, parser_file)) |
| 990 | + |
| 991 | + self.assertEqual( |
| 992 | + [], |
| 993 | + offenders, |
| 994 | + "Parser must not set Finding.service directly. Offenders:\n" |
| 995 | + + "\n".join(offenders), |
| 996 | + ) |
0 commit comments