|
18 | 18 | CSAFNote, |
19 | 19 | CSAFProductStatus, |
20 | 20 | CSAFReference, |
| 21 | + CSAFThreat, |
21 | 22 | CSAFVulnerability, |
22 | 23 | ) |
23 | 24 |
|
@@ -124,38 +125,48 @@ def set_flag_or_threat(vulnerability: CSAFVulnerability, observation: Observatio |
124 | 125 | if vex_status == CSAF_Status.CSAF_STATUS_NOT_AFFECTED: |
125 | 126 | product_or_relationship_id = get_product_or_relationship_id(observation) |
126 | 127 | observation_log = get_current_modifying_observation_log(observation) |
127 | | - if observation_log and observation_log.vex_justification: |
| 128 | + # Prefer the observation's authoritative current justification and only fall back to the latest |
| 129 | + # modifying observation log when it is empty. The log's vex_justification can be empty even |
| 130 | + # though a justification was assessed, because empty log entries (for example the ones created |
| 131 | + # when a rego rule is removed) count as "modifying" and shadow the real assessment. That would |
| 132 | + # leave the product in known_not_affected without any impact statement (CSAF mandatory test |
| 133 | + # 6.1.27.9). |
| 134 | + justification = observation.current_vex_justification |
| 135 | + if not justification and observation_log: |
| 136 | + justification = observation_log.vex_justification |
| 137 | + csaf_label = map_vex_justification_to_csaf_openvex_justification(justification) |
| 138 | + if csaf_label: |
128 | 139 | found = False |
129 | 140 | for flag in vulnerability.flags: |
130 | | - if flag.label == map_vex_justification_to_csaf_openvex_justification(observation_log.vex_justification): |
| 141 | + if flag.label == csaf_label: |
131 | 142 | if product_or_relationship_id not in flag.product_ids: |
132 | 143 | flag.product_ids.append(product_or_relationship_id) |
133 | 144 | found = True |
134 | 145 | break |
135 | 146 | if not found: |
136 | 147 | csaf_flag = CSAFFlag( |
137 | | - label=map_vex_justification_to_csaf_openvex_justification(observation_log.vex_justification), |
| 148 | + label=csaf_label, |
138 | 149 | product_ids=[product_or_relationship_id], |
139 | 150 | ) |
140 | 151 | vulnerability.flags.append(csaf_flag) |
141 | | - |
142 | | - |
143 | | -# else: |
144 | | -# category = "impact" |
145 | | -# details = ( |
146 | | -# observation_log.comment if observation_log and observation_log.comment else "No justification available" |
147 | | -# ) |
148 | | -# found = False |
149 | | -# for threat in vulnerability.threats: |
150 | | -# if threat.category == category and threat.details == details: |
151 | | -# if product_or_relationship_id not in threat.product_ids: |
152 | | -# threat.product_ids.append(product_or_relationship_id) |
153 | | -# found = True |
154 | | -# break |
155 | | -# if not found: |
156 | | -# threat = CSAFThreat( |
157 | | -# category=category, |
158 | | -# details=details, |
159 | | -# product_ids=[product_or_relationship_id], |
160 | | -# ) |
161 | | -# vulnerability.threats.append(threat) |
| 152 | + elif observation_log and observation_log.comment: |
| 153 | + # CSAF mandatory test 6.1.27.9 requires an impact statement for every product in |
| 154 | + # known_not_affected. When no machine-readable vex_justification is set, fall back to |
| 155 | + # the assessment comment as a free-text impact threat. Without a comment we emit nothing |
| 156 | + # and let validation fail, so the missing justification is caught instead of masked. |
| 157 | + category = "impact" |
| 158 | + details = observation_log.comment |
| 159 | + found = False |
| 160 | + for threat in vulnerability.threats: |
| 161 | + if threat.category == category and threat.details == details: |
| 162 | + if product_or_relationship_id not in threat.product_ids: |
| 163 | + threat.product_ids.append(product_or_relationship_id) |
| 164 | + found = True |
| 165 | + break |
| 166 | + if not found: |
| 167 | + threat = CSAFThreat( |
| 168 | + category=category, |
| 169 | + details=details, |
| 170 | + product_ids=[product_or_relationship_id], |
| 171 | + ) |
| 172 | + vulnerability.threats.append(threat) |
0 commit comments