Skip to content

Commit b2e7525

Browse files
committed
fix: exclude automated parser logs from CSAF not_affected impact statement
1 parent b9b8171 commit b2e7525

5 files changed

Lines changed: 115 additions & 7 deletions

File tree

backend/application/core/services/assessment.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@
3131
calculate_risk_acceptance_expiry_date,
3232
)
3333
from application.core.services.security_gate import check_security_gate
34-
from application.core.types import Assessment_Status, Status
34+
from application.core.types import (
35+
Assessment_Status,
36+
Observation_Log_Comment,
37+
Status,
38+
)
3539
from application.issue_tracker.services.issue_tracker import (
3640
push_observation_to_issue_tracker,
3741
)
@@ -503,7 +507,7 @@ def set_propagated_assessment_for_new_observation(observation: Observation) -> N
503507
)
504508
.exclude(observation__branch=observation.branch)
505509
.exclude(severity="", status="")
506-
.exclude(comment="Set by parser")
510+
.exclude(comment=Observation_Log_Comment.COMMENT_SET_BY_PARSER)
507511
.select_related("observation__branch")
508512
.order_by("observation__branch", "-created")
509513
)

backend/application/core/types.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,20 @@ class Assessment_Status:
9292
]
9393

9494

95+
class Observation_Log_Comment:
96+
COMMENT_SET_BY_PARSER = "Set by parser"
97+
COMMENT_UPDATED_BY_PARSER = "Updated by parser"
98+
COMMENT_NOT_FOUND_IN_LATEST_SCAN = "Observation not found in latest scan"
99+
100+
# Comments written automatically during import. They carry no human assessment and must not be
101+
# used as a VEX justification or impact statement.
102+
AUTOMATED_COMMENTS = [
103+
COMMENT_SET_BY_PARSER,
104+
COMMENT_UPDATED_BY_PARSER,
105+
COMMENT_NOT_FOUND_IN_LATEST_SCAN,
106+
]
107+
108+
95109
class VEX_Justification:
96110
JUSTIFICATION_COMPONENT_NOT_PRESENT = "component_not_present"
97111
JUSTIFICATION_VULNERABLE_CODE_NOT_PRESENT = "vulnerable_code_not_present"

backend/application/import_observations/services/import_observations.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@
3636
calculate_risk_acceptance_expiry_date,
3737
)
3838
from application.core.services.security_gate import check_security_gate
39-
from application.core.types import Assessment_Status, Status
39+
from application.core.types import (
40+
Assessment_Status,
41+
Observation_Log_Comment,
42+
Status,
43+
)
4044
from application.epss.services.cvss_bt import apply_exploit_information
4145
from application.epss.services.epss import apply_epss
4246
from application.import_observations.exceptions import ParserError
@@ -695,7 +699,7 @@ def _process_current_observation(
695699
observation=observation_before,
696700
severity=severity,
697701
status=status,
698-
comment="Updated by parser",
702+
comment=Observation_Log_Comment.COMMENT_UPDATED_BY_PARSER,
699703
vex_justification="",
700704
vex_remediations=None,
701705
assessment_status=Assessment_Status.ASSESSMENT_STATUS_AUTO_APPROVED,
@@ -745,7 +749,7 @@ def _process_new_observation(imported_observation: Observation, settings: Settin
745749
observation=imported_observation,
746750
severity=imported_observation.current_severity,
747751
status=imported_observation.current_status,
748-
comment="Set by parser",
752+
comment=Observation_Log_Comment.COMMENT_SET_BY_PARSER,
749753
vex_justification="",
750754
vex_remediations=None,
751755
assessment_status=Assessment_Status.ASSESSMENT_STATUS_AUTO_APPROVED,
@@ -805,7 +809,7 @@ def _resolve_unimported_observations(
805809
observation=observation,
806810
severity="",
807811
status=observation.current_status,
808-
comment="Observation not found in latest scan",
812+
comment=Observation_Log_Comment.COMMENT_NOT_FOUND_IN_LATEST_SCAN,
809813
vex_justification="",
810814
vex_remediations=None,
811815
assessment_status=Assessment_Status.ASSESSMENT_STATUS_AUTO_APPROVED,

backend/application/vex/services/csaf_generator_vulnerability.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Optional
22

33
from application.core.models import Observation, Observation_Log
4+
from application.core.types import Observation_Log_Comment
45
from application.vex.services.csaf_generator_helpers import (
56
get_product_or_relationship_id,
67
get_vulnerability_ecosystem,
@@ -131,9 +132,11 @@ def set_flag_or_threat(vulnerability: CSAFVulnerability, observation: Observatio
131132
# "current"/"modifying" observation log helpers are unreliable here: automated log entries
132133
# (for example from rule changes) carry no status but still pass those filters and would then
133134
# provide a misleading justification and comment. Filtering on the current status skips those
134-
# entries.
135+
# entries. Automated parser comments (e.g. "Updated by parser") do carry a status, so they are
136+
# excluded explicitly to keep their meaningless text out of the impact statement.
135137
assessment_log = (
136138
Observation_Log.objects.filter(observation=observation, status=observation.current_status)
139+
.exclude(comment__in=Observation_Log_Comment.AUTOMATED_COMMENTS)
137140
.order_by("-created")
138141
.first()
139142
)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from django.core.management import call_command
2+
from django.test import TestCase
3+
from django.utils import timezone
4+
5+
from application.access_control.models import User
6+
from application.core.models import Observation, Observation_Log, Product
7+
from application.core.types import (
8+
Assessment_Status,
9+
Observation_Log_Comment,
10+
Severity,
11+
Status,
12+
VEX_Justification,
13+
)
14+
from application.import_observations.models import Parser
15+
from application.vex.services.csaf_generator_vulnerability import (
16+
create_vulnerability,
17+
set_flag_or_threat,
18+
)
19+
20+
21+
class TestSetFlagOrThreat(TestCase):
22+
def setUp(self):
23+
call_command("loaddata", "unittests/fixtures/vex_fixtures.json")
24+
self.product = Product.objects.get(pk=1)
25+
self.parser = Parser.objects.get(pk=1)
26+
self.user = User.objects.get(pk=1)
27+
28+
def _create_observation(self, current_vex_justification=""):
29+
return Observation.objects.create(
30+
product=self.product,
31+
parser=self.parser,
32+
title="CVE-2026-0001",
33+
current_severity=Severity.SEVERITY_HIGH,
34+
numerical_severity=Severity.NUMERICAL_SEVERITIES[Severity.SEVERITY_HIGH],
35+
# assessment_status is the manual assessment; current_status is derived from it on save.
36+
assessment_status=Status.STATUS_NOT_AFFECTED,
37+
current_status=Status.STATUS_NOT_AFFECTED,
38+
current_vex_justification=current_vex_justification,
39+
import_last_seen=timezone.now(),
40+
)
41+
42+
def _create_observation_log(self, observation, comment, vex_justification=""):
43+
return Observation_Log.objects.create(
44+
observation=observation,
45+
user=self.user,
46+
status=Status.STATUS_NOT_AFFECTED,
47+
comment=comment,
48+
vex_justification=vex_justification,
49+
assessment_status=Assessment_Status.ASSESSMENT_STATUS_AUTO_APPROVED,
50+
)
51+
52+
def test_human_assessment_is_preferred_over_automated_parser_log(self):
53+
observation = self._create_observation()
54+
self._create_observation_log(
55+
observation,
56+
comment="Vulnerable function is never called in this build.",
57+
vex_justification=VEX_Justification.JUSTIFICATION_VULNERABLE_CODE_NOT_PRESENT,
58+
)
59+
# The automated parser log is created last, so it is the newest entry with a matching status.
60+
self._create_observation_log(observation, comment=Observation_Log_Comment.COMMENT_UPDATED_BY_PARSER)
61+
62+
vulnerability = create_vulnerability("CVE-2026-0001")
63+
set_flag_or_threat(vulnerability, observation)
64+
65+
self.assertEqual(1, len(vulnerability.threats))
66+
self.assertEqual(
67+
"Vulnerable function is never called in this build.",
68+
vulnerability.threats[0].details,
69+
)
70+
self.assertEqual(1, len(vulnerability.flags))
71+
72+
def test_no_impact_statement_when_only_automated_parser_logs_exist(self):
73+
observation = self._create_observation()
74+
self._create_observation_log(observation, comment=Observation_Log_Comment.COMMENT_UPDATED_BY_PARSER)
75+
self._create_observation_log(observation, comment=Observation_Log_Comment.COMMENT_NOT_FOUND_IN_LATEST_SCAN)
76+
77+
vulnerability = create_vulnerability("CVE-2026-0001")
78+
set_flag_or_threat(vulnerability, observation)
79+
80+
# Neither a flag nor a threat is emitted, so validation fails and surfaces the missing
81+
# assessment instead of publishing a meaningless "Updated by parser" impact statement.
82+
self.assertEqual(0, len(vulnerability.threats))
83+
self.assertEqual(0, len(vulnerability.flags))

0 commit comments

Comments
 (0)