← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~enriqueesanchz/launchpad:add-soss-export-mapping into launchpad:master

 

Enrique Sánchez has proposed merging ~enriqueesanchz/launchpad:add-soss-export-mapping into launchpad:master.

Commit message:
Add SOSSExport

Update SOSS cves ordering for exact match between import and export.
Add support for status_explanation in SOSSImport.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~enriqueesanchz/launchpad/+git/launchpad/+merge/491712
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~enriqueesanchz/launchpad:add-soss-export-mapping into launchpad:master.
diff --git a/lib/lp/bugs/scripts/soss/models.py b/lib/lp/bugs/scripts/soss/models.py
index c557812..f8fe1a9 100644
--- a/lib/lp/bugs/scripts/soss/models.py
+++ b/lib/lp/bugs/scripts/soss/models.py
@@ -7,6 +7,7 @@ from enum import Enum
 from typing import Any, Dict, List, Optional
 
 import yaml
+from packaging.version import Version
 
 __all__ = [
     "SOSSRecord",
@@ -53,6 +54,14 @@ class SOSSRecord:
         def __str__(self) -> str:
             return self.value
 
+        def __lt__(self, other) -> bool:
+            try:
+                self_ver = self.value.split(":")[-1].split("/")[0]
+                other_ver = self.value.split(":")[-1].split("/")[0]
+                return Version(self_ver) < Version(other_ver)
+            except Exception:
+                return self.value < other.value
+
     @dataclass
     class CVSS:
         source: str
@@ -95,6 +104,12 @@ class SOSSRecord:
                 "Note": self.note,
             }
 
+        def __lt__(self, other: "SOSSRecord.Package") -> bool:
+            if self.name == other.name:
+                return self.channel < other.channel
+
+            return self.name <= other.name
+
     references: List[str]
     notes: List[str]
     priority: PriorityEnum
diff --git a/lib/lp/bugs/scripts/soss/sossexport.py b/lib/lp/bugs/scripts/soss/sossexport.py
new file mode 100644
index 0000000..aae719f
--- /dev/null
+++ b/lib/lp/bugs/scripts/soss/sossexport.py
@@ -0,0 +1,211 @@
+#  Copyright 2025 Canonical Ltd.  This software is licensed under the
+#  GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""A SOSS (SOSS CVE Tracker) bug exporter"""
+import logging
+from collections import defaultdict
+from datetime import datetime
+from typing import Dict, List, Optional
+
+from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
+
+from lp.app.enums import InformationType
+from lp.app.errors import NotFoundError
+from lp.bugs.interfaces.cve import ICveSet
+from lp.bugs.model.bug import Bug as BugModel
+from lp.bugs.model.bugtask import BugTask
+from lp.bugs.model.cve import Cve as CveModel
+from lp.bugs.model.vulnerability import Vulnerability
+from lp.bugs.scripts.soss.models import SOSSRecord
+from lp.bugs.scripts.soss.sossimport import (
+    DISTRIBUTION_NAME,
+    PACKAGE_STATUS_MAP,
+    PACKAGE_TYPE_MAP,
+    PRIORITY_ENUM_MAP,
+)
+from lp.registry.interfaces.distribution import IDistributionSet
+from lp.registry.model.distribution import Distribution
+
+__all__ = [
+    "SOSSExporter",
+]
+
+logger = logging.getLogger(__name__)
+
+# Constants moved to module level with proper naming
+PRIORITY_ENUM_MAP_REVERSE = {v: k for k, v in PRIORITY_ENUM_MAP.items()}
+
+PACKAGE_TYPE_MAP_REVERSE = {v: k for k, v in PACKAGE_TYPE_MAP.items()}
+
+PACKAGE_STATUS_MAP_REVERSE = {v: k for k, v in PACKAGE_STATUS_MAP.items()}
+
+
+class SOSSExporter:
+    """
+    SOSSExporter is used to export Launchpad Vulnerability data to SOSS CVE
+    files.
+    """
+
+    def __init__(
+        self,
+        information_type: InformationType = InformationType.PRIVATESECURITY,
+        dry_run: bool = False,
+    ) -> None:
+        self.dry_run = dry_run
+        self.cve_set = getUtility(ICveSet)
+        self.soss = getUtility(IDistributionSet).getByName(DISTRIBUTION_NAME)
+
+        if self.soss is None:
+            logger.error("[SOSSExporter] SOSS distribution not found")
+            raise NotFoundError("SOSS distribution not found")
+
+    def _get_packages(
+        self, bugtasks: List[BugTask]
+    ) -> Dict[SOSSRecord.PackageTypeEnum, SOSSRecord.Package]:
+        """Get a dict of SOSSRecord.PackageTypeEnum: SOSSRecord.Package from a
+        bugtask list.
+        """
+        packages = defaultdict(list)
+        for bugtask in bugtasks:
+            pkg = SOSSRecord.Package(
+                name=bugtask.target.name,
+                channel=SOSSRecord.Channel(
+                    value="/".join(s for s in bugtask.channel if s is not None)
+                ),
+                repositories=bugtask.metadata.get("repositories"),
+                status=SOSSRecord.PackageStatusEnum(
+                    PACKAGE_STATUS_MAP_REVERSE[bugtask.status]
+                ),
+                note=bugtask.status_explanation or "",
+            )
+            packages[PACKAGE_TYPE_MAP_REVERSE[bugtask.packagetype]].append(pkg)
+
+        ordered_packages = {
+            k: sorted(packages[k])
+            for k in PACKAGE_TYPE_MAP_REVERSE.values()
+            if packages[k]
+        }
+
+        return ordered_packages
+
+    def _get_cvss(self, cvss: Dict) -> List[SOSSRecord.CVSS]:
+        """Get a list of SOSSRecord.CVSS from a cvss dict"""
+        cvss_list = []
+        for authority in cvss.keys():
+            for c in cvss[authority]:
+                cvss_list.append(
+                    SOSSRecord.CVSS(
+                        c.get("source"),
+                        c.get("vector"),
+                        c.get("baseScore"),
+                        c.get("baseSeverity"),
+                    )
+                )
+        return cvss_list
+
+    def to_record(self, cve_sequence: str) -> SOSSRecord:
+        """Return a SOSSRecord exporting Launchpad data for the specified
+        cve_sequence.
+        """
+        lp_cve = self._get_launchpad_cve(cve_sequence)
+        if lp_cve is None:
+            logger.error(f"[SOSSExporter] {cve_sequence} not found")
+            return None
+
+        bug = self._find_existing_bug(cve_sequence, lp_cve, self.soss)
+        if bug is None:
+            logger.error(f"[SOSSExporter] No bug found for {cve_sequence}")
+            return None
+
+        vulnerability = self._find_existing_vulnerability(lp_cve, self.soss)
+        if vulnerability is None:
+            logger.error(
+                f"[SOSSExporter] No vulnerability found for {cve_sequence}"
+            )
+            return None
+
+        # Export bug
+        desc_parts = bug.description.rsplit("\n\nReferences:\n")
+        references = desc_parts[1].split("\n") if len(desc_parts) > 1 else []
+
+        # Export bug.bugtasks
+        packages = self._get_packages(bug.bugtasks)
+        assigned_to = (
+            bug.bugtasks[0].assignee.name if bug.bugtasks[0].assignee else ""
+        )
+
+        # Export vulnerability
+        description = vulnerability.description
+        public_date = vulnerability.date_made_public
+        notes = vulnerability.notes.split("\n") if vulnerability.notes else []
+        priority = SOSSRecord.PriorityEnum(
+            PRIORITY_ENUM_MAP_REVERSE[vulnerability.importance]
+        )
+        priority_reason = vulnerability.importance_explanation
+
+        # Export vulnerability.cvss
+        cvss = self._get_cvss(vulnerability.cvss)
+
+        candidate = f"CVE-{lp_cve.sequence}"
+
+        return SOSSRecord(
+            references=references,
+            notes=notes,
+            priority=priority,
+            priority_reason=priority_reason,
+            assigned_to=assigned_to,
+            packages=packages,
+            candidate=candidate,
+            description=description,
+            cvss=cvss,
+            public_date=self._normalize_date_without_timezone(public_date),
+        )
+
+    def _find_existing_bug(
+        self,
+        cve_sequence: str,
+        lp_cve: CveModel,
+        distribution: Distribution,
+    ) -> Optional[BugModel]:
+        """Find existing bug for the given CVE."""
+        for vulnerability in lp_cve.vulnerabilities:
+            if vulnerability.distribution == distribution:
+                bugs = vulnerability.bugs
+                if len(bugs) > 1:
+                    raise ValueError(
+                        "Multiple existing bugs found for CVE ",
+                        cve_sequence,
+                    )
+                if bugs:
+                    return bugs[0]
+        return None
+
+    def _find_existing_vulnerability(
+        self, lp_cve: CveModel, distribution: Distribution
+    ) -> Optional[Vulnerability]:
+        """Find existing vulnerability for the current distribution"""
+        if not lp_cve:
+            return None
+
+        vulnerability = next(
+            (
+                v
+                for v in lp_cve.vulnerabilities
+                if v.distribution == distribution
+            ),
+            None,
+        )
+        return vulnerability
+
+    def _get_launchpad_cve(self, cve_sequence: str) -> Optional[CveModel]:
+        """Get CVE from Launchpad."""
+        return removeSecurityProxy(self.cve_set[cve_sequence])
+
+    def _normalize_date_without_timezone(
+        self, date_obj: datetime
+    ) -> Optional[datetime]:
+        """Normalize date to no timezone if needed."""
+        if date_obj and date_obj.tzinfo is not None:
+            return date_obj.replace(tzinfo=None)
+        return date_obj
diff --git a/lib/lp/bugs/scripts/soss/sossimport.py b/lib/lp/bugs/scripts/soss/sossimport.py
index 773caf4..e0026e1 100644
--- a/lib/lp/bugs/scripts/soss/sossimport.py
+++ b/lib/lp/bugs/scripts/soss/sossimport.py
@@ -13,6 +13,7 @@ from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
 from lp.app.enums import InformationType
+from lp.app.errors import NotFoundError
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.bugs.enums import VulnerabilityStatus
 from lp.bugs.interfaces.bug import CreateBugParams, IBugSet
@@ -40,6 +41,10 @@ from lp.testing import person_logged_in
 
 __all__ = [
     "SOSSImporter",
+    "PRIORITY_ENUM_MAP",
+    "PACKAGE_TYPE_MAP",
+    "PACKAGE_STATUS_MAP",
+    "DISTRIBUTION_NAME",
 ]
 
 logger = logging.getLogger(__name__)
@@ -55,11 +60,11 @@ PRIORITY_ENUM_MAP = {
 }
 
 PACKAGE_TYPE_MAP = {
-    SOSSRecord.PackageTypeEnum.UNPACKAGED: ExternalPackageType.GENERIC,
-    SOSSRecord.PackageTypeEnum.PYTHON: ExternalPackageType.PYTHON,
-    SOSSRecord.PackageTypeEnum.MAVEN: ExternalPackageType.MAVEN,
     SOSSRecord.PackageTypeEnum.CONDA: ExternalPackageType.CONDA,
+    SOSSRecord.PackageTypeEnum.MAVEN: ExternalPackageType.MAVEN,
+    SOSSRecord.PackageTypeEnum.PYTHON: ExternalPackageType.PYTHON,
     SOSSRecord.PackageTypeEnum.RUST: ExternalPackageType.CARGO,
+    SOSSRecord.PackageTypeEnum.UNPACKAGED: ExternalPackageType.GENERIC,
 }
 
 PACKAGE_STATUS_MAP = {
@@ -97,7 +102,7 @@ class SOSSImporter:
 
         if self.soss is None:
             logger.error("[SOSSImporter] SOSS distribution not found")
-            raise Exception("SOSS distribution not found")
+            raise NotFoundError("SOSS distribution not found")
 
     def import_cve_from_file(
         self, cve_path: str
@@ -131,7 +136,7 @@ class SOSSImporter:
         else:
             bug = self._update_bug(bug, soss_record, lp_cve)
 
-        vulnerability = self._find_existing_vulnerability(bug, self.soss)
+        vulnerability = self._find_existing_vulnerability(lp_cve, self.soss)
         if not vulnerability:
             vulnerability = self._create_vulnerability(
                 bug, soss_record, lp_cve, self.soss
@@ -306,14 +311,18 @@ class SOSSImporter:
         return None
 
     def _find_existing_vulnerability(
-        self, bug: BugModel, distribution: Distribution
+        self, lp_cve: CveModel, distribution: Distribution
     ) -> Optional[Vulnerability]:
         """Find existing vulnerability for the current distribution"""
-        if not bug:
+        if not lp_cve:
             return None
 
         vulnerability = next(
-            (v for v in bug.vulnerabilities if v.distribution == distribution),
+            (
+                v
+                for v in lp_cve.vulnerabilities
+                if v.distribution == distribution
+            ),
             None,
         )
         return vulnerability
@@ -351,7 +360,7 @@ class SOSSImporter:
                 )
 
                 if target not in bugtask_by_target:
-                    self.bugtask_set.createTask(
+                    bugtask = self.bugtask_set.createTask(
                         bug,
                         self.bug_importer,
                         target,
@@ -377,6 +386,8 @@ class SOSSImporter:
                     bugtask.transitionToAssignee(assignee, validate=False)
                     bugtask.metadata = metadata
 
+                bugtask.status_explanation = package.note
+
         # Remove bugtasks that were deleted from the record
         for bugtask in bugtask_by_target.values():
             bugtask.destroySelf()
diff --git a/lib/lp/bugs/scripts/soss/tests/sampledata/CVE-2005-1544 b/lib/lp/bugs/scripts/soss/tests/sampledata/CVE-2005-1544
index 8788e47..15536ad 100644
--- a/lib/lp/bugs/scripts/soss/tests/sampledata/CVE-2005-1544
+++ b/lib/lp/bugs/scripts/soss/tests/sampledata/CVE-2005-1544
@@ -31,3 +31,4 @@ Packages:
     - soss-src-stable-local
     Status: ignored
     Note: ''
+Candidate: CVE-2005-1544
diff --git a/lib/lp/bugs/scripts/soss/tests/sampledata/CVE-2011-5000 b/lib/lp/bugs/scripts/soss/tests/sampledata/CVE-2011-5000
index e92427b..76ce151 100644
--- a/lib/lp/bugs/scripts/soss/tests/sampledata/CVE-2011-5000
+++ b/lib/lp/bugs/scripts/soss/tests/sampledata/CVE-2011-5000
@@ -12,3 +12,4 @@ Packages:
     - soss-conda-stable-local
     Status: ignored
     Note: ''
+Candidate: CVE-2011-5000
diff --git a/lib/lp/bugs/scripts/soss/tests/sampledata/CVE-2021-21300 b/lib/lp/bugs/scripts/soss/tests/sampledata/CVE-2021-21300
index 22ae13d..3997a96 100644
--- a/lib/lp/bugs/scripts/soss/tests/sampledata/CVE-2021-21300
+++ b/lib/lp/bugs/scripts/soss/tests/sampledata/CVE-2021-21300
@@ -11,3 +11,4 @@ Packages:
     - soss-conda-candidate-local
     Status: ignored
     Note: was ignored
+Candidate: CVE-2021-21300
diff --git a/lib/lp/bugs/scripts/soss/tests/sampledata/CVE-2025-1979 b/lib/lp/bugs/scripts/soss/tests/sampledata/CVE-2025-1979
index 5d90588..a562efa 100644
--- a/lib/lp/bugs/scripts/soss/tests/sampledata/CVE-2025-1979
+++ b/lib/lp/bugs/scripts/soss/tests/sampledata/CVE-2025-1979
@@ -16,39 +16,32 @@ Priority-Reason: 'Unrealistic exploitation scenario. Logs are stored locally and
   in this priority assignment. '
 Assigned-To: janitor
 Packages:
-  unpackaged:
+  conda:
+  - Name: ray
+    Channel: jammy:1.17.0/stable
+    Repositories:
+    - nvidia-pb3-python-stable-local
+    Status: not-affected
+    Note: 2.22.0+soss.1
+  maven:
   - Name: vllm
     Channel: noble:0.7.3/stable
     Repositories:
     - soss-src-stable-local
-    Status: needed
+    Status: needs-triage
     Note: ''
   python:
-  - Name: ray
-    Channel: jammy:2.22.0/stable
-    Repositories:
-    - nvidia-pb3-python-stable-local
-    Status: released
-    Note: 2.22.0+soss.1
   - Name: pyyaml
     Channel: jammy:2.22.0/stable
     Repositories:
     - nvidia-pb3-python-stable-local
     Status: not-affected
     Note: ''
-  maven:
-  - Name: vllm
-    Channel: noble:0.7.3/stable
-    Repositories:
-    - soss-src-stable-local
-    Status: needs-triage
-    Note: ''
-  conda:
   - Name: ray
-    Channel: jammy:1.17.0/stable
+    Channel: jammy:2.22.0/stable
     Repositories:
     - nvidia-pb3-python-stable-local
-    Status: not-affected
+    Status: released
     Note: 2.22.0+soss.1
   rust:
   - Name: ray
@@ -57,6 +50,13 @@ Packages:
     - nvidia-pb3-python-stable-local
     Status: deferred
     Note: 2.22.0+soss.1
+  unpackaged:
+  - Name: vllm
+    Channel: noble:0.7.3/stable
+    Repositories:
+    - soss-src-stable-local
+    Status: needed
+    Note: ''
 Candidate: CVE-2025-1979
 Description: "Versions of the package ray before 2.43.0 are vulnerable to Insertion\
   \ of Sensitive Information into Log File where the redis password is being logged\
diff --git a/lib/lp/bugs/scripts/soss/tests/test_sossexport.py b/lib/lp/bugs/scripts/soss/tests/test_sossexport.py
new file mode 100644
index 0000000..c7f315f
--- /dev/null
+++ b/lib/lp/bugs/scripts/soss/tests/test_sossexport.py
@@ -0,0 +1,108 @@
+from pathlib import Path
+
+import transaction
+from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
+
+from lp.app.enums import InformationType
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.bugs.interfaces.cve import ICveSet
+from lp.bugs.scripts.soss import SOSSRecord
+from lp.bugs.scripts.soss.sossexport import SOSSExporter
+from lp.bugs.scripts.soss.sossimport import SOSSImporter
+from lp.testing import TestCaseWithFactory, person_logged_in
+from lp.testing.layers import LaunchpadZopelessLayer
+
+
+class TestSOSSExporter(TestCaseWithFactory):
+
+    layer = LaunchpadZopelessLayer
+
+    def setUp(self):
+        super().setUp()
+        self.sampledata = Path(__file__).parent / "sampledata"
+        self.factory.makePerson(name="octagalland")
+        self.soss = self.factory.makeDistribution(
+            name="soss",
+            displayname="SOSS",
+        )
+        transaction.commit()
+
+        self.soss_importer = SOSSImporter()
+        self.soss_exporter = SOSSExporter()
+
+        self.cve_set = getUtility(ICveSet)
+        self.bug_importer = getUtility(ILaunchpadCelebrities).bug_importer
+
+    def test_get_packages(self):
+        """Test get SOSSRecord.Package list from bugtasks."""
+        for file in self.sampledata.iterdir():
+            cve_sequence = file.name.lstrip("CVE-")
+            if not self.cve_set[cve_sequence]:
+                self.factory.makeCVE(sequence=cve_sequence)
+
+            with open(file) as f:
+                soss_record = SOSSRecord.from_yaml(f)
+
+            bug, _ = self.soss_importer.import_cve_from_file(file)
+
+            naked_bug = removeSecurityProxy(bug)
+            packages = self.soss_exporter._get_packages(naked_bug.bugtasks)
+            self.assertEqual(soss_record.packages, packages)
+
+    def test_get_cvss(self):
+        """Test get SOSSRecord.CVSS list from vulnerability.cvss."""
+        for file in self.sampledata.iterdir():
+            cve_sequence = file.name.lstrip("CVE-")
+            if not self.cve_set[cve_sequence]:
+                self.factory.makeCVE(sequence=cve_sequence)
+
+            with open(file) as f:
+                soss_record = SOSSRecord.from_yaml(f)
+
+            _, vulnerability = self.soss_importer.import_cve_from_file(file)
+            naked_vulnerability = removeSecurityProxy(vulnerability)
+            cvss = self.soss_exporter._get_cvss(naked_vulnerability.cvss)
+
+            self.assertEqual(soss_record.cvss, cvss)
+
+    def test_to_record(self):
+        """Test that imported and exported SOSSRecords match."""
+        soss_importer = SOSSImporter(
+            information_type=InformationType.PRIVATESECURITY
+        )
+
+        for file in self.sampledata.iterdir():
+            cve_sequence = file.name.lstrip("CVE-")
+            if not self.cve_set[cve_sequence]:
+                self.factory.makeCVE(sequence=cve_sequence)
+
+            with open(file) as f:
+                soss_record = SOSSRecord.from_yaml(f)
+
+            bug, vulnerability = soss_importer.import_cve_from_file(file)
+
+            with person_logged_in(self.bug_importer):
+                exported = self.soss_exporter.to_record(file.name)
+
+            self.assertEqual(soss_record, exported)
+
+    def test_import_export(self):
+        """Integration test that checks that cve files imported and exported
+        match."""
+        soss_importer = SOSSImporter(
+            information_type=InformationType.PRIVATESECURITY
+        )
+
+        for file in self.sampledata.iterdir():
+            cve_sequence = file.name.lstrip("CVE-")
+            if not self.cve_set[cve_sequence]:
+                self.factory.makeCVE(sequence=cve_sequence)
+
+            bug, vulnerability = soss_importer.import_cve_from_file(file)
+
+            with person_logged_in(self.bug_importer):
+                exported = self.soss_exporter.to_record(file.name)
+
+            with open(file) as f:
+                self.assertEqual(f.read(), exported.to_yaml())
diff --git a/lib/lp/bugs/scripts/soss/tests/test_sossimport.py b/lib/lp/bugs/scripts/soss/tests/test_sossimport.py
index d577e6a..a79e5b2 100644
--- a/lib/lp/bugs/scripts/soss/tests/test_sossimport.py
+++ b/lib/lp/bugs/scripts/soss/tests/test_sossimport.py
@@ -76,24 +76,27 @@ class TestSOSSImporter(TestCaseWithFactory):
                     channel=("jammy:2.22.0", "stable"),
                 ),
                 BugTaskStatus.INVALID,
+                "",
                 {"repositories": ["nvidia-pb3-python-stable-local"]},
             ),
             (
                 self.soss.getExternalPackage(
                     name=ray,
-                    packagetype=ExternalPackageType.PYTHON,
-                    channel=("jammy:2.22.0", "stable"),
+                    packagetype=ExternalPackageType.CONDA,
+                    channel=("jammy:1.17.0", "stable"),
                 ),
-                BugTaskStatus.FIXRELEASED,
+                BugTaskStatus.INVALID,
+                "2.22.0+soss.1",
                 {"repositories": ["nvidia-pb3-python-stable-local"]},
             ),
             (
                 self.soss.getExternalPackage(
                     name=ray,
-                    packagetype=ExternalPackageType.CONDA,
-                    channel=("jammy:1.17.0", "stable"),
+                    packagetype=ExternalPackageType.PYTHON,
+                    channel=("jammy:2.22.0", "stable"),
                 ),
-                BugTaskStatus.INVALID,
+                BugTaskStatus.FIXRELEASED,
+                "2.22.0+soss.1",
                 {"repositories": ["nvidia-pb3-python-stable-local"]},
             ),
             (
@@ -103,24 +106,27 @@ class TestSOSSImporter(TestCaseWithFactory):
                     channel=("focal:0.27.0", "stable"),
                 ),
                 BugTaskStatus.DEFERRED,
+                "2.22.0+soss.1",
                 {"repositories": ["nvidia-pb3-python-stable-local"]},
             ),
             (
                 self.soss.getExternalPackage(
                     name=vllm,
-                    packagetype=ExternalPackageType.GENERIC,
+                    packagetype=ExternalPackageType.MAVEN,
                     channel=("noble:0.7.3", "stable"),
                 ),
-                BugTaskStatus.NEW,
+                BugTaskStatus.UNKNOWN,
+                "",
                 {"repositories": ["soss-src-stable-local"]},
             ),
             (
                 self.soss.getExternalPackage(
                     name=vllm,
-                    packagetype=ExternalPackageType.MAVEN,
+                    packagetype=ExternalPackageType.GENERIC,
                     channel=("noble:0.7.3", "stable"),
                 ),
-                BugTaskStatus.UNKNOWN,
+                BugTaskStatus.NEW,
+                "",
                 {"repositories": ["soss-src-stable-local"]},
             ),
         ]
@@ -165,9 +171,14 @@ class TestSOSSImporter(TestCaseWithFactory):
     ):
         self.assertEqual(len(bugtasks), len(bugtask_reference))
 
-        for i, (target, status, metadata) in enumerate(bugtask_reference):
+        for i, (target, status, status_explanation, metadata) in enumerate(
+            bugtask_reference
+        ):
             self.assertEqual(bugtasks[i].target, target)
             self.assertEqual(bugtasks[i].status, status)
+            self.assertEqual(
+                bugtasks[i].status_explanation, status_explanation
+            )
             self.assertEqual(bugtasks[i].importance, importance)
             self.assertEqual(bugtasks[i].assignee, assignee)
             self.assertEqual(bugtasks[i].metadata, metadata)
@@ -356,6 +367,7 @@ class TestSOSSImporter(TestCaseWithFactory):
                 channel=("noble:4.23.1", "stable"),
             ),
             BugTaskStatus.DEFERRED,
+            "test note",
             {"repositories": ["test-repo"]},
         )
 
@@ -407,13 +419,13 @@ class TestSOSSImporter(TestCaseWithFactory):
             ],
             SOSSRecord.PackageTypeEnum.UNPACKAGED,
         )
-        self.assertEqual(generic_pkg, self.bugtask_reference[4][0])
+        self.assertEqual(generic_pkg, self.bugtask_reference[5][0])
 
         maven_pkg = soss_importer._get_or_create_external_package(
             self.soss_record.packages[SOSSRecord.PackageTypeEnum.MAVEN][0],
             SOSSRecord.PackageTypeEnum.MAVEN,
         )
-        self.assertEqual(maven_pkg, self.bugtask_reference[5][0])
+        self.assertEqual(maven_pkg, self.bugtask_reference[4][0])
 
     def test_prepare_cvss_data(self):
         """Test prepare the cvss json"""
diff --git a/lib/lp/bugs/scripts/soss/tests/test_sossrecord.py b/lib/lp/bugs/scripts/soss/tests/test_sossrecord.py
index fc3377c..a1cff07 100644
--- a/lib/lp/bugs/scripts/soss/tests/test_sossrecord.py
+++ b/lib/lp/bugs/scripts/soss/tests/test_sossrecord.py
@@ -47,48 +47,39 @@ class TestSOSSRecord(TestCase):
             ),
             assigned_to="janitor",
             packages={
-                SOSSRecord.PackageTypeEnum.UNPACKAGED: [
+                SOSSRecord.PackageTypeEnum.CONDA: [
+                    SOSSRecord.Package(
+                        name="ray",
+                        channel=SOSSRecord.Channel("jammy:1.17.0/stable"),
+                        repositories=["nvidia-pb3-python-stable-local"],
+                        status=SOSSRecord.PackageStatusEnum.NOT_AFFECTED,
+                        note="2.22.0+soss.1",
+                    )
+                ],
+                SOSSRecord.PackageTypeEnum.MAVEN: [
                     SOSSRecord.Package(
                         name="vllm",
                         channel=SOSSRecord.Channel("noble:0.7.3/stable"),
                         repositories=["soss-src-stable-local"],
-                        status=SOSSRecord.PackageStatusEnum.NEEDED,
+                        status=SOSSRecord.PackageStatusEnum.NEEDS_TRIAGE,
                         note="",
                     )
                 ],
                 SOSSRecord.PackageTypeEnum.PYTHON: [
                     SOSSRecord.Package(
-                        name="ray",
-                        channel=SOSSRecord.Channel("jammy:2.22.0/stable"),
-                        repositories=["nvidia-pb3-python-stable-local"],
-                        status=SOSSRecord.PackageStatusEnum.RELEASED,
-                        note="2.22.0+soss.1",
-                    ),
-                    SOSSRecord.Package(
                         name="pyyaml",
                         channel=SOSSRecord.Channel("jammy:2.22.0/stable"),
                         repositories=["nvidia-pb3-python-stable-local"],
                         status=SOSSRecord.PackageStatusEnum.NOT_AFFECTED,
                         note="",
                     ),
-                ],
-                SOSSRecord.PackageTypeEnum.MAVEN: [
-                    SOSSRecord.Package(
-                        name="vllm",
-                        channel=SOSSRecord.Channel("noble:0.7.3/stable"),
-                        repositories=["soss-src-stable-local"],
-                        status=SOSSRecord.PackageStatusEnum.NEEDS_TRIAGE,
-                        note="",
-                    )
-                ],
-                SOSSRecord.PackageTypeEnum.CONDA: [
                     SOSSRecord.Package(
                         name="ray",
-                        channel=SOSSRecord.Channel("jammy:1.17.0/stable"),
+                        channel=SOSSRecord.Channel("jammy:2.22.0/stable"),
                         repositories=["nvidia-pb3-python-stable-local"],
-                        status=SOSSRecord.PackageStatusEnum.NOT_AFFECTED,
+                        status=SOSSRecord.PackageStatusEnum.RELEASED,
                         note="2.22.0+soss.1",
-                    )
+                    ),
                 ],
                 SOSSRecord.PackageTypeEnum.RUST: [
                     SOSSRecord.Package(
@@ -99,6 +90,15 @@ class TestSOSSRecord(TestCase):
                         note="2.22.0+soss.1",
                     )
                 ],
+                SOSSRecord.PackageTypeEnum.UNPACKAGED: [
+                    SOSSRecord.Package(
+                        name="vllm",
+                        channel=SOSSRecord.Channel("noble:0.7.3/stable"),
+                        repositories=["soss-src-stable-local"],
+                        status=SOSSRecord.PackageStatusEnum.NEEDED,
+                        note="",
+                    )
+                ],
             },
             candidate="CVE-2025-1979",
             description=(
@@ -159,48 +159,39 @@ class TestSOSSRecord(TestCase):
             ),
             "Assigned-To": "janitor",
             "Packages": {
-                "unpackaged": [
+                "conda": [
+                    {
+                        "Name": "ray",
+                        "Channel": "jammy:1.17.0/stable",
+                        "Repositories": ["nvidia-pb3-python-stable-local"],
+                        "Status": "not-affected",
+                        "Note": "2.22.0+soss.1",
+                    }
+                ],
+                "maven": [
                     {
                         "Name": "vllm",
                         "Channel": "noble:0.7.3/stable",
                         "Repositories": ["soss-src-stable-local"],
-                        "Status": "needed",
+                        "Status": "needs-triage",
                         "Note": "",
                     }
                 ],
                 "python": [
                     {
-                        "Name": "ray",
-                        "Channel": "jammy:2.22.0/stable",
-                        "Repositories": ["nvidia-pb3-python-stable-local"],
-                        "Status": "released",
-                        "Note": "2.22.0+soss.1",
-                    },
-                    {
                         "Name": "pyyaml",
                         "Channel": "jammy:2.22.0/stable",
                         "Repositories": ["nvidia-pb3-python-stable-local"],
                         "Status": "not-affected",
                         "Note": "",
                     },
-                ],
-                "maven": [
-                    {
-                        "Name": "vllm",
-                        "Channel": "noble:0.7.3/stable",
-                        "Repositories": ["soss-src-stable-local"],
-                        "Status": "needs-triage",
-                        "Note": "",
-                    }
-                ],
-                "conda": [
                     {
                         "Name": "ray",
-                        "Channel": "jammy:1.17.0/stable",
+                        "Channel": "jammy:2.22.0/stable",
                         "Repositories": ["nvidia-pb3-python-stable-local"],
-                        "Status": "not-affected",
+                        "Status": "released",
                         "Note": "2.22.0+soss.1",
-                    }
+                    },
                 ],
                 "rust": [
                     {
@@ -211,6 +202,15 @@ class TestSOSSRecord(TestCase):
                         "Note": "2.22.0+soss.1",
                     }
                 ],
+                "unpackaged": [
+                    {
+                        "Name": "vllm",
+                        "Channel": "noble:0.7.3/stable",
+                        "Repositories": ["soss-src-stable-local"],
+                        "Status": "needed",
+                        "Note": "",
+                    }
+                ],
             },
             "Candidate": "CVE-2025-1979",
             "Description": (