Hi, thanks for the review. Addressed the comments :) > and we might want to re-use them
Well, It seems that the sec team does not have a standard for this types of fields so it makes sense to only use this for now inside the `SOSSRecord` class. If in the future (I hope so) we can reuse it, we will change it. > That being said, I really like how tidy this looks - it represents exactly a > SOSSRecord with all that's nested within it, and I don't need to go and find > the definition anything anywhere else. So IMO there is no need to > overcomplicate. Agree :) Diff comments: > diff --git a/lib/lp/bugs/scripts/soss/sossrecord.py > b/lib/lp/bugs/scripts/soss/sossrecord.py > new file mode 100644 > index 0000000..466606b > --- /dev/null > +++ b/lib/lp/bugs/scripts/soss/sossrecord.py done :) > @@ -0,0 +1,139 @@ > +# Copyright 2025 Canonical Ltd. This software is licensed under the > +# GNU Affero General Public License version 3 (see the file LICENSE). > +import re > +from dataclasses import dataclass > +from datetime import datetime > +from enum import Enum > +from typing import Any, Dict, List, Optional > + > +import yaml good catch, thanks! <3 > + > +# From `soss-cve-tracker/git-hooks/check-cve-syntax` > +VALID_CHANNEL_REGEX = re.compile(r"^(focal|jammy|noble):[^/]+/stable$") > + > + > +@dataclass > +class SOSSRecord: > + > + class PriorityEnum(Enum): > + NEEDS_TRIAGE = "Needs-triage" > + NEGLIGIBLE = "Negligible" > + LOW = "Low" > + MEDIUM = "Medium" > + HIGH = "High" > + CRITICAL = "Critical" > + > + class PackageStatusEnum(Enum): > + IGNORED = "ignored" > + NEEDS_TRIAGE = "needs-triage" > + RELEASED = "released" > + NOT_AFFECTED = "not-affected" > + DEFERRED = "deferred" > + NEEDED = "needed" > + > + class PackageTypeEnum(Enum): > + CONDA = "conda" > + PYTHON = "python" > + UNPACKAGED = "unpackaged" > + MAVEN = "maven" > + RUST = "rust" > + > + @dataclass > + class Channel: > + value: str > + > + def __post_init__(self): > + if not VALID_CHANNEL_REGEX.match(self.value): > + raise ValueError(f"Invalid channel format: {self.value}") > + > + @dataclass > + class CVSS: > + source: str > + vector: str > + base_score: float > + base_severity: float > + > + BASE_SEVERITIES = {"LOW", "MEDIUM", "HIGH", "CRITICAL"} > + > + def __post_init__(self): > + if not (0.0 <= self.base_score <= 10.0): > + raise ValueError(f"Invalid base score: {self.base_score}") > + if self.base_severity not in self.BASE_SEVERITIES: > + raise ValueError( > + f"Invalid base severity: {self.base_severity}" > + ) > + > + @dataclass > + class Package: > + name: str > + channel: "SOSSRecord.Channel" > + repositories: List[str] > + status: "SOSSRecord.PackageStatusEnum" > + note: str > + > + references: List[str] > + notes: List[str] > + priority: PriorityEnum > + priority_reason: str > + assigned_to: str > + packages: Dict[PackageTypeEnum, List[Package]] > + candidate: Optional[str] = None > + description: Optional[str] = None > + cvss: Optional[List[CVSS]] = None > + public_date: Optional[datetime] = None > + > + @classmethod > + def from_yaml(cls, yaml_str: str) -> "SOSSRecord": > + raw: Dict[str, Any] = yaml.safe_load(yaml_str) > + return cls.from_dict(raw) > + > + @classmethod > + def from_dict(cls, raw: Dict[str, Any]) -> "SOSSRecord": > + priority = SOSSRecord.PriorityEnum(raw["Priority"]) As I saw in the cves: - Candidate, Description, CVSS and PublicDate are not mandatory. - References, Notes can be empty using [], but they are always shown. - Priority-Reason, Assigned-To can be empty using "", but they are always shown. - Other fields are mandatory. As I have ongoing conversations with the stakeholders, there can be little changes on this. I'll put a comment as valuable info. Optional fields are also shown using typing `Optional[]`. > + > + packages = {} > + for enum_key, pkgs in raw.get("Packages", {}).items(): > + package_type = SOSSRecord.PackageTypeEnum(enum_key.lower()) > + package_list = [ > + SOSSRecord.Package( > + name=package["Name"], > + channel=SOSSRecord.Channel(package["Channel"]), > + repositories=package["Repositories"], > + status=SOSSRecord.PackageStatusEnum( > + package["Status"].lower() > + ), > + note=package["Note"], > + ) > + for package in pkgs > + ] > + packages[package_type] = package_list > + > + cvss_list = [ > + SOSSRecord.CVSS( > + cvss["source"], > + cvss["vector"], > + cvss["baseScore"], > + cvss["baseSeverity"], > + ) > + for cvss in raw.get("CVSS", []) > + ] > + > + public_date_str = raw.get("PublicDate") > + public_date = ( > + datetime.fromisoformat(public_date_str) > + if public_date_str > + else None > + ) > + > + return cls( > + references=raw.get("References", []), > + notes=raw.get("Notes", []), > + priority=priority, > + priority_reason=raw.get("Priority-Reason", ""), > + assigned_to=raw.get("Assigned-To", ""), > + packages=packages, > + candidate=raw.get("Candidate"), > + description=raw.get("Description"), > + cvss=cvss_list, > + public_date=public_date, > + ) -- https://code.launchpad.net/~enriqueesanchz/launchpad/+git/launchpad/+merge/487018 Your team Launchpad code reviewers is requested to review the proposed merge of ~enriqueesanchz/launchpad:soss-import-parsing into launchpad:master. _______________________________________________ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : launchpad-reviewers@lists.launchpad.net Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp