|
| 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