launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #28619
[Merge] ~andrey-fedoseev/launchpad:uct-import into launchpad:master
Andrey Fedoseev has proposed merging ~andrey-fedoseev/launchpad:uct-import into launchpad:master.
Commit message:
Add `uct-import` script
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~andrey-fedoseev/launchpad/+git/launchpad/+merge/425142
IMPORTANT: you can find some TODO items in the diff, please review them. While the script is functional, some things may need adjustment.
Add `uct-import` script
The script is meant to import CVE entries from `ubuntu-cve-tracker` to
Launchpad database as bugs/bug tasks/vulnerabilities
The script parses the CVE files using the code copied from
`cve_lib` in `ubuntu-cve-tracker`, and transforms the data to
structured `CVE` objects.
The `CVE` object is then imported as a `Bug`, with `BugTask` created for:
- Every package listed in the CVE
- Every combination of package/distroseries listed in the CVE
`Vulnerability` instance is created for every distribution affected by the CVE
The `Bug` and the `Vulnerability`s are linked to the corresponding `Cve`
object which is supposed to be present in LP database
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~andrey-fedoseev/launchpad:uct-import into launchpad:master.
diff --git a/lib/lp/bugs/scripts/tests/sampledata/CVE-2022-23222 b/lib/lp/bugs/scripts/tests/sampledata/CVE-2022-23222
new file mode 100644
index 0000000..6d665be
--- /dev/null
+++ b/lib/lp/bugs/scripts/tests/sampledata/CVE-2022-23222
@@ -0,0 +1,44 @@
+PublicDateAtUSN: 2022-01-14 08:15:00 UTC
+Candidate: CVE-2022-23222
+PublicDate: 2022-01-14 08:15:00 UTC
+References:
+ https://ubuntu.com/security/notices/USN-5368-1
+Description:
+ kernel/bpf/verifier.c in the Linux kernel through 5.15.14 allows local
+ users to gain privileges because of the availability of pointer arithmetic
+ via certain *_OR_NULL pointer types.
+Ubuntu-Description:
+ It was discovered that the BPF verifier in the Linux kernel did not
+ properly restrict pointer types in certain situations. A local attacker
+ could use this to cause a denial of service (system crash) or possibly
+ execute arbitrary code.
+Notes:
+ sbeattie> Ubuntu 21.10 / 5.13+ kernels disable unprivileged BPF by default.
+ sbeattie> kernels 5.8 and older are not affected, priority high is for
+ 5.10 and 5.11 based kernels only
+Mitigation:
+ seth-arnold> set kernel.unprivileged_bpf_disabled to 1
+Bugs:
+ https://github.com/mm2/Little-CMS/issues/29
+ https://github.com/mm2/Little-CMS/issues/30
+ https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=745471
+Priority: critical
+Discovered-by: tr3e wang
+Assigned-to:
+CVSS:
+ nvd: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H [7.8 HIGH]
+
+Patches_linux:
+ break-fix: 457f44363a8894135c85b7a9afd2bd8196db24ab c25b2ae136039ffa820c26138ed4a5e5f3ab3841|local-CVE-2022-23222-fix
+upstream_linux: released (5.17~rc1)
+impish_linux: released (5.13.0-37.42)
+devel_linux: not-affected (5.15.0-25.25)
+Priority_linux_impish: medium
+Priority_linux_devel: medium
+Tags_linux: not-ue
+
+Patches_linux-hwe:
+upstream_linux-hwe: released (5.17~rc1)
+impish_linux-hwe: DNE
+devel_linux-hwe: DNE
+Priority_linux-hwe: high
diff --git a/lib/lp/bugs/scripts/tests/test_uctimport.py b/lib/lp/bugs/scripts/tests/test_uctimport.py
new file mode 100644
index 0000000..743eea9
--- /dev/null
+++ b/lib/lp/bugs/scripts/tests/test_uctimport.py
@@ -0,0 +1,357 @@
+# Copyright 2022 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+import datetime
+from pathlib import Path
+
+from dateutil.tz import tzutc
+from zope.component import getUtility
+
+from lp.app.enums import InformationType
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.bugs.enums import VulnerabilityStatus
+from lp.bugs.interfaces.bugtask import (
+ BugTaskImportance,
+ BugTaskStatus,
+ )
+from lp.bugs.scripts.uctimport import (
+ CVE,
+ DistroSeriesPackageStatus,
+ Note,
+ Package,
+ PackageStatus,
+ Patch,
+ Priority,
+ UCTImporter,
+ )
+from lp.registry.interfaces.series import SeriesStatus
+from lp.testing import TestCaseWithFactory
+from lp.testing.layers import ZopelessDatabaseLayer
+
+
+class TestUCTImporter(TestCaseWithFactory):
+
+ layer = ZopelessDatabaseLayer
+
+ def setUp(self, *args, **kwargs):
+ super().setUp(*args, **kwargs)
+ self.importer = UCTImporter()
+
+ def test_load_cve_from_file(self):
+ cve_path = Path(__file__).parent / "sampledata" / "CVE-2022-23222"
+ cve = self.importer.load_cve_from_file(cve_path)
+ self.assertEqual(
+ cve,
+ CVE(
+ assigned_to="",
+ bugs=[
+ "https://github.com/mm2/Little-CMS/issues/29",
+ "https://github.com/mm2/Little-CMS/issues/30",
+ "https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=745471",
+ ],
+ cvss=[
+ {
+ "source": "nvd",
+ "vector": (
+ "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"
+ ),
+ "baseScore": "7.8",
+ "baseSeverity": "HIGH",
+ }
+ ],
+ candidate="CVE-2022-23222",
+ date_made_public=datetime.datetime(
+ 2022, 1, 14, 8, 15, tzinfo=tzutc()
+ ),
+ description=(
+ "kernel/bpf/verifier.c in the Linux kernel through "
+ "5.15.14 allows local\nusers to gain privileges because "
+ "of the availability of pointer arithmetic\nvia certain "
+ "*_OR_NULL pointer types."
+ ),
+ discovered_by="tr3e wang",
+ mitigation=(
+ "seth-arnold> set kernel.unprivileged_bpf_disabled to 1"
+ ),
+ notes=[
+ Note(
+ author="sbeattie",
+ text=(
+ "Ubuntu 21.10 / 5.13+ kernels disable "
+ "unprivileged BPF by default.\nkernels 5.8 and "
+ "older are not affected, priority high is "
+ "for\n5.10 and 5.11 based kernels only"
+ ),
+ ),
+ ],
+ priority=Priority.CRITICAL,
+ references=[
+ "https://ubuntu.com/security/notices/USN-5368-1"
+ ],
+ ubuntu_description=(
+ "It was discovered that the BPF verifier in the Linux "
+ "kernel did not\nproperly restrict pointer types in "
+ "certain situations. A local attacker\ncould use this to "
+ "cause a denial of service (system crash) or possibly\n"
+ "execute arbitrary code."
+ ),
+ packages=[
+ Package(
+ name="linux",
+ statuses=[
+ DistroSeriesPackageStatus(
+ distroseries="upstream",
+ status=PackageStatus.RELEASED,
+ reason="5.17~rc1",
+ priority=None,
+ ),
+ DistroSeriesPackageStatus(
+ distroseries="impish",
+ status=PackageStatus.RELEASED,
+ reason="5.13.0-37.42",
+ priority=Priority.MEDIUM,
+ ),
+ DistroSeriesPackageStatus(
+ distroseries="devel",
+ status=PackageStatus.NOT_AFFECTED,
+ reason="5.15.0-25.25",
+ priority=Priority.MEDIUM,
+ ),
+ ],
+ priority=None,
+ tags={"not-ue"},
+ patches=[
+ Patch(
+ patch_type="break-fix",
+ entry=(
+ "457f44363a8894135c85b7a9afd2bd8196db24ab "
+ "c25b2ae136039ffa820c26138ed4a5e5f3ab3841|"
+ "local-CVE-2022-23222-fix"
+ ),
+ )
+ ],
+ ),
+ Package(
+ name="linux-hwe",
+ statuses=[
+ DistroSeriesPackageStatus(
+ distroseries="upstream",
+ status=PackageStatus.RELEASED,
+ reason="5.17~rc1",
+ priority=None,
+ ),
+ DistroSeriesPackageStatus(
+ distroseries="impish",
+ status=PackageStatus.DOES_NOT_EXIST,
+ reason="",
+ priority=None,
+ ),
+ DistroSeriesPackageStatus(
+ distroseries="devel",
+ status=PackageStatus.DOES_NOT_EXIST,
+ reason="",
+ priority=None,
+ ),
+ ],
+ priority=Priority.HIGH,
+ tags=set(),
+ patches=[],
+ ),
+ ],
+ ),
+ )
+
+ def test_create_bug(self):
+ celebrities = getUtility(ILaunchpadCelebrities)
+ ubuntu = celebrities.ubuntu
+ owner = celebrities.bug_importer
+ supported_series = self.factory.makeDistroSeries(
+ distribution=ubuntu, status=SeriesStatus.SUPPORTED
+ )
+ current_series = self.factory.makeDistroSeries(
+ distribution=ubuntu, status=SeriesStatus.CURRENT
+ )
+ devel_series = self.factory.makeDistroSeries(
+ distribution=ubuntu, status=SeriesStatus.DEVELOPMENT
+ )
+ dsp1 = self.factory.makeDistributionSourcePackage(distribution=ubuntu)
+ dsp2 = self.factory.makeDistributionSourcePackage(distribution=ubuntu)
+ lp_cve = self.factory.makeCVE("2022-23222")
+
+ for package in (dsp1, dsp2):
+ for series in (supported_series, current_series, devel_series):
+ self.factory.makeSourcePackagePublishingHistory(
+ distroseries=series,
+ sourcepackagerelease=self.factory.makeSourcePackageRelease(
+ distroseries=series,
+ sourcepackagename=package.sourcepackagename,
+ ),
+ )
+
+ now = datetime.datetime.now(tzutc())
+ cve = CVE(
+ assigned_to="",
+ bugs=[
+ "https://github.com/mm2/Little-CMS/issues/29",
+ "https://github.com/mm2/Little-CMS/issues/30",
+ "https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=745471",
+ ],
+ cvss=[],
+ candidate="CVE-2022-23222",
+ date_made_public=now,
+ description="description",
+ discovered_by="tr3e wang",
+ mitigation="mitigation",
+ notes=[Note(author="author", text="text")],
+ priority=Priority.MEDIUM,
+ references=[
+ "https://ubuntu.com/security/notices/USN-5368-1"
+ ],
+ ubuntu_description="ubuntu-description",
+ packages=[
+ Package(
+ name=dsp1.sourcepackagename.name,
+ statuses=[
+ DistroSeriesPackageStatus(
+ distroseries=supported_series.name,
+ status=PackageStatus.RELEASED,
+ reason="released",
+ priority=Priority.HIGH,
+ ),
+ DistroSeriesPackageStatus(
+ distroseries=current_series.name,
+ status=PackageStatus.DOES_NOT_EXIST,
+ reason="does not exist",
+ priority=None,
+ ),
+ ],
+ priority=Priority.LOW,
+ patches=[],
+ tags=set(),
+ ),
+ Package(
+ name=dsp2.sourcepackagename.name,
+ statuses=[
+ DistroSeriesPackageStatus(
+ distroseries=supported_series.name,
+ status=PackageStatus.NOT_AFFECTED,
+ reason="not affected",
+ priority=Priority.LOW,
+ ),
+ DistroSeriesPackageStatus(
+ distroseries=current_series.name,
+ status=PackageStatus.IGNORED,
+ reason="ignored",
+ priority=None,
+ ),
+ DistroSeriesPackageStatus(
+ distroseries="devel",
+ status=PackageStatus.NEEDS_TRIAGE,
+ reason="needs triage",
+ priority=None,
+ ),
+ ],
+ priority=None,
+ patches=[],
+ tags=set(),
+ ),
+ ],
+ )
+ bug, vulnerabilities = self.importer.create_bug(cve, lp_cve)
+
+ self.assertEqual(bug.title, "CVE-2022-23222")
+ self.assertEqual(bug.description, "ubuntu-description")
+ self.assertEqual(bug.owner, owner)
+ self.assertEqual(bug.information_type, InformationType.PRIVATESECURITY)
+
+ messages = list(bug.messages)
+ self.assertEqual(len(messages), 5)
+
+ message = messages.pop(0)
+ self.assertEqual(message.owner, owner)
+ self.assertEqual(message.text_contents, "description")
+
+ for external_bug_url in cve.bugs:
+ message = messages.pop(0)
+ self.assertEqual(message.text_contents, external_bug_url)
+
+ for reference in cve.references:
+ message = messages.pop(0)
+ self.assertEqual(message.text_contents, reference)
+
+ bug_tasks = bug.bugtasks
+ # 7 bug tasks are supposed to be created:
+ # 2 for distro packages
+ # 5 for combinations of distroseries/package:
+ # 2 for the first package (2 distro series)
+ # 3 for the second package (3 distro series)
+ self.assertEqual(len(bug_tasks), 7)
+
+ bug_tasks_by_target = {
+ (t.distribution, t.distroseries, t.sourcepackagename): t
+ for t in bug_tasks
+ }
+ t = bug_tasks_by_target.pop((ubuntu, None, dsp1.sourcepackagename))
+ self.assertEqual(t.importance, BugTaskImportance.LOW)
+ self.assertEqual(t.status, BugTaskStatus.NEW)
+ self.assertEqual(t.status_explanation, None)
+
+ t = bug_tasks_by_target.pop((ubuntu, None, dsp2.sourcepackagename))
+ self.assertEqual(t.importance, BugTaskImportance.MEDIUM)
+ self.assertEqual(t.status, BugTaskStatus.UNKNOWN)
+ self.assertEqual(t.status_explanation, None)
+
+ t = bug_tasks_by_target.pop(
+ (None, supported_series, dsp1.sourcepackagename)
+ )
+ self.assertEqual(t.importance, BugTaskImportance.HIGH)
+ self.assertEqual(t.status, BugTaskStatus.FIXRELEASED)
+ self.assertEqual(t.status_explanation, "released")
+
+ t = bug_tasks_by_target.pop(
+ (None, current_series, dsp1.sourcepackagename)
+ )
+ self.assertEqual(t.importance, BugTaskImportance.LOW)
+ self.assertEqual(t.status, BugTaskStatus.DOESNOTEXIST)
+ self.assertEqual(t.status_explanation, "does not exist")
+
+ t = bug_tasks_by_target.pop(
+ (None, supported_series, dsp2.sourcepackagename)
+ )
+ self.assertEqual(t.importance, BugTaskImportance.LOW)
+ self.assertEqual(t.status, BugTaskStatus.INVALID)
+ self.assertEqual(t.status_explanation, "not affected")
+
+ t = bug_tasks_by_target.pop(
+ (None, current_series, dsp2.sourcepackagename)
+ )
+ self.assertEqual(t.importance, BugTaskImportance.MEDIUM)
+ self.assertEqual(t.status, BugTaskStatus.WONTFIX)
+ self.assertEqual(t.status_explanation, "ignored")
+
+ t = bug_tasks_by_target.pop(
+ (None, devel_series, dsp2.sourcepackagename)
+ )
+ self.assertEqual(t.importance, BugTaskImportance.MEDIUM)
+ self.assertEqual(t.status, BugTaskStatus.UNKNOWN)
+ self.assertEqual(t.status_explanation, "needs triage")
+
+ self.assertEqual(bug.cves, [lp_cve])
+
+ self.assertEqual(len(vulnerabilities), 1)
+
+ vulnerability = vulnerabilities[0]
+ self.assertEqual(vulnerability.distribution, ubuntu)
+ self.assertEqual(vulnerability.creator, owner)
+ self.assertEqual(vulnerability.cve, lp_cve)
+ self.assertEqual(
+ vulnerability.status, VulnerabilityStatus.NEEDS_TRIAGE
+ )
+ self.assertEqual(vulnerability.description, "description")
+ self.assertEqual(vulnerability.notes, "author> text")
+ self.assertEqual(vulnerability.mitigation, "mitigation")
+ self.assertEqual(vulnerability.importance, BugTaskImportance.MEDIUM)
+ self.assertEqual(
+ vulnerability.information_type, InformationType.PRIVATESECURITY
+ )
+ self.assertEqual(vulnerability.date_made_public, now)
+ self.assertEqual(vulnerability.bugs, [bug])
diff --git a/lib/lp/bugs/scripts/uctimport.py b/lib/lp/bugs/scripts/uctimport.py
new file mode 100644
index 0000000..09f8421
--- /dev/null
+++ b/lib/lp/bugs/scripts/uctimport.py
@@ -0,0 +1,2246 @@
+# Copyright 2022 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""A UCT (Ubuntu CVE Tracker) bug importer
+
+This code can import CVE summaries stored in UCT repository to bugs in
+Launchpad.
+
+For each entry in UCT we:
+
+1. Create a Bug instance
+2. Create a Vulnerability instance and link it to the bug (multiple
+ Vulnerabilities may be created if the CVE entry covers multiple
+ distributions)
+3. Create a Bug Task for each package/distro-series in the CVE entry
+4. Update the statuses of Bug Tasks based on the information in the CVE entry
+"""
+import codecs
+from datetime import datetime
+from enum import Enum
+import glob
+import logging
+import math
+import os
+from pathlib import Path
+import re
+import sys
+from typing import (
+ Any,
+ Dict,
+ List,
+ NamedTuple,
+ Optional,
+ Set,
+ Tuple,
+ )
+
+import dateutil.parser
+import yaml
+from zope.component import getUtility
+
+from lp.app.enums import InformationType
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.bugs.enums import VulnerabilityStatus
+from lp.bugs.interfaces.bug import (
+ CreateBugParams,
+ IBugSet,
+ )
+from lp.bugs.interfaces.bugtask import (
+ BugTaskImportance,
+ BugTaskStatus,
+ IBugTaskSet,
+ )
+from lp.bugs.interfaces.cve import ICveSet
+from lp.bugs.interfaces.vulnerability import IVulnerabilitySet
+from lp.bugs.model.bug import Bug as BugModel
+from lp.bugs.model.cve import Cve as CveModel
+from lp.bugs.model.vulnerability import Vulnerability
+from lp.registry.interfaces.distroseries import IDistroSeriesSet
+from lp.registry.interfaces.series import SeriesStatus
+from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
+from lp.registry.model.distribution import Distribution
+from lp.registry.model.distributionsourcepackage import (
+ DistributionSourcePackage,
+ )
+from lp.registry.model.distroseries import DistroSeries
+from lp.registry.model.sourcepackagename import SourcePackageName
+from lp.services.messages.interfaces.message import IMessageSet
+
+
+DEFAULT_LOGGER = logging.getLogger("lp.bugs.scripts.import")
+
+
+class Priority(Enum):
+ CRITICAL = "critical"
+ HIGH = "high"
+ MEDIUM = "medium"
+ LOW = "low"
+ UNTRIAGED = "untriaged"
+ NEGLIGIBLE = "negligible"
+
+
+class PackageStatus(Enum):
+ IGNORED = "ignored"
+ NEEDS_TRIAGE = "needs-triage"
+ DOES_NOT_EXIST = "DNE"
+ RELEASED = "released"
+ NOT_AFFECTED = "not-affected"
+ DEFERRED = "deferred"
+ NEEDED = "needed"
+ PENDING = "pending"
+
+
+DistroSeriesPackageStatus = NamedTuple(
+ "DistroSeriesPackageStatus",
+ [
+ ("distroseries", str),
+ ("status", PackageStatus),
+ ("reason", str),
+ ("priority", Optional[Priority]),
+ ],
+)
+
+
+Patch = NamedTuple(
+ "Patch",
+ [
+ ("patch_type", str),
+ ("entry", str),
+ ],
+)
+
+
+Package = NamedTuple(
+ "Package",
+ [
+ ("name", str),
+ ("statuses", List[DistroSeriesPackageStatus]),
+ ("priority", Optional[Priority]),
+ ("tags", Set[str]),
+ ("patches", List[Patch]),
+ ],
+)
+
+Note = NamedTuple(
+ "Note",
+ [
+ ("author", str),
+ ("text", str),
+ ],
+)
+
+
+CVE = NamedTuple(
+ "CVE",
+ [
+ ("assigned_to", str),
+ ("bugs", List[str]),
+ ("cvss", List[Dict[str, Any]]),
+ ("candidate", str),
+ ("date_made_public", Optional[datetime]),
+ ("description", str),
+ ("discovered_by", str),
+ ("mitigation", str),
+ ("notes", List[Note]),
+ ("priority", Priority),
+ ("references", List[str]),
+ ("ubuntu_description", str),
+ ("packages", List[Package]),
+ ],
+)
+
+PRIORITY_MAP = {
+ Priority.CRITICAL: BugTaskImportance.CRITICAL,
+ Priority.HIGH: BugTaskImportance.HIGH,
+ Priority.MEDIUM: BugTaskImportance.MEDIUM,
+ Priority.LOW: BugTaskImportance.LOW,
+ Priority.UNTRIAGED: BugTaskImportance.UNDECIDED, # TODO: confirm this
+ Priority.NEGLIGIBLE: BugTaskImportance.WISHLIST, # TODO: confirm this
+}
+
+STATUS_MAP = {
+ PackageStatus.IGNORED: BugTaskStatus.WONTFIX,
+ PackageStatus.NEEDS_TRIAGE: BugTaskStatus.UNKNOWN,
+ PackageStatus.DOES_NOT_EXIST: BugTaskStatus.DOESNOTEXIST,
+ PackageStatus.RELEASED: BugTaskStatus.FIXRELEASED,
+ PackageStatus.NOT_AFFECTED: BugTaskStatus.INVALID,
+ # PackageStatus.DEFERRED: ..., # TODO: fix this
+ PackageStatus.NEEDED: BugTaskStatus.NEW,
+ PackageStatus.PENDING: BugTaskStatus.FIXCOMMITTED,
+}
+
+
+def format_cve_notes(notes: List[Note]) -> str:
+ return "\n".join(
+ "{author}> {text}".format(author=note.author, text=note.text)
+ for note in notes
+ )
+
+
+class UCTImporter:
+ def __init__(self, logger: Optional[logging.Logger] = None) -> None:
+ self.logger = logger or DEFAULT_LOGGER
+
+ def import_cve_from_file(self, cve_path: Path) -> None:
+ cve = self.load_cve_from_file(cve_path)
+ self.import_cve(cve)
+
+ @staticmethod
+ def load_cve_from_file(cve_path: Path) -> CVE:
+ """
+ Load a `CVE` instance from data contained in `cve_path`.
+
+ The file is parsed to a dictionary using the code copied from
+ `cve_lib` in `ubuntu-cve-tracker`.
+
+ A `CVE` instance is created from that dictionary, applying some data
+ transformations along the way.
+ """
+
+ cve_data = load_cve(str(cve_path)) # type: Dict[str, Any]
+
+ packages = [] # type: List[Package]
+ tags = cve_data.pop("tags") # type: Dict[str, Set[str]]
+ patches = cve_data.pop(
+ "patches"
+ ) # type: Dict[str, List[Tuple[str, str]]]
+ for package, statuses_dict in cve_data.pop("pkgs").items():
+ statuses = [] # type: List[DistroSeriesPackageStatus]
+ for distroseries, (status, reason) in statuses_dict.items():
+ distroseries_priority = cve_data.pop(
+ "Priority_{package}_{distroseries}".format(
+ package=package,
+ distroseries=distroseries,
+ ),
+ None,
+ )
+ statuses.append(
+ DistroSeriesPackageStatus(
+ distroseries=distroseries,
+ status=PackageStatus(status),
+ reason=reason,
+ priority=(
+ Priority(distroseries_priority)
+ if distroseries_priority
+ else None
+ ),
+ )
+ )
+ package_priority = cve_data.pop(
+ "Priority_{package}".format(package=package), None
+ )
+ packages.append(
+ Package(
+ name=package,
+ statuses=statuses,
+ priority=(
+ Priority(package_priority)
+ if package_priority
+ else None
+ ),
+ tags=tags.pop(package, set()),
+ patches=[
+ Patch(patch_type=patch_type, entry=entry)
+ for patch_type, entry in patches.pop(package, [])
+ ],
+ )
+ )
+
+ crd = cve_data.pop("CRD", None)
+ if crd == "unknown":
+ crd = None
+ public_date = cve_data.pop("PublicDate", None)
+ if public_date == "unknown":
+ public_date = None
+ public_date_at_USN = cve_data.pop("PublicDateAtUSN", None)
+ if public_date_at_USN == "unknown":
+ public_date_at_USN = None
+
+ date_made_public = crd or public_date or public_date_at_USN
+
+ cve = CVE(
+ assigned_to=cve_data.pop("Assigned-to").strip(),
+ bugs=cve_data.pop("Bugs").strip().split("\n"),
+ cvss=cve_data.pop("CVSS"),
+ candidate=cve_data.pop("Candidate").strip(),
+ date_made_public=dateutil.parser.parse(date_made_public)
+ if date_made_public
+ else None,
+ description=cve_data.pop("Description").strip(),
+ discovered_by=cve_data.pop("Discovered-by").strip(),
+ mitigation=cve_data.pop("Mitigation", "").strip(),
+ notes=[
+ Note(author=author, text=text)
+ for author, text in cve_data.pop("Notes")
+ ],
+ priority=Priority(cve_data.pop("Priority")),
+ references=cve_data.pop("References").strip().split("\n"),
+ ubuntu_description=cve_data.pop("Ubuntu-Description").strip(),
+ packages=packages,
+ )
+
+ # make sure all fields are consumed
+ if cve_data:
+ raise AssertionError(
+ "not all fields are consumed: {}".format(cve_data)
+ )
+
+ return cve
+
+ def import_cve(self, cve: CVE) -> None:
+ lp_cve: CveModel = getUtility(ICveSet)[cve.candidate]
+ if lp_cve is None:
+ self.logger.warning(
+ "Could not find the CVE in LP: %s", cve.candidate
+ )
+ return
+ self.create_bug(cve, lp_cve)
+
+ def create_bug(
+ self, cve: CVE, lp_cve: CveModel
+ ) -> Tuple[Optional[BugModel], List[Vulnerability]]:
+
+ self.logger.debug("creating bug...")
+
+ affected_packages = [] # type: List[DistributionSourcePackage]
+ affected_distro_series = [] # type: List[DistroSeries]
+ affected_distributions = set() # type: Set[Distribution]
+ importances = {}
+ statuses_with_explanations = {}
+
+ for cve_package in cve.packages:
+ source_package_name = self.get_source_package_name(
+ cve_package.name
+ )
+ if not source_package_name:
+ continue
+
+ package_priority = cve_package.priority or cve.priority
+ importances[source_package_name] = (
+ PRIORITY_MAP[package_priority] if package_priority else None
+ )
+
+ for cve_package_status in cve_package.statuses:
+ distro_series = self.get_distro_series(
+ cve_package_status.distroseries
+ )
+ if not distro_series:
+ continue
+
+ if distro_series not in affected_distro_series:
+ affected_distro_series.append(distro_series)
+
+ affected_distributions.add(distro_series.distribution)
+
+ distro_package = DistributionSourcePackage(
+ distribution=distro_series.distribution,
+ sourcepackagename=source_package_name,
+ )
+ if distro_package not in affected_packages:
+ affected_packages.append(distro_package)
+
+ distro_series_package_priority = (
+ cve_package_status.priority or package_priority
+ )
+ key = (source_package_name, distro_series)
+ importances[key] = (
+ PRIORITY_MAP[distro_series_package_priority]
+ if distro_series_package_priority
+ else None
+ )
+ statuses_with_explanations[key] = (
+ STATUS_MAP[cve_package_status.status],
+ cve_package_status.reason,
+ )
+
+ if not affected_packages:
+ self.logger.warning("Could not find any affected packages")
+ return None, []
+
+ distro_package = affected_packages.pop(0)
+ affected_distributions = {distro_package.distribution}
+
+ # Create the bug
+ # TODO: confirm this. Should this be something like `team_security`?
+ owner = getUtility(ILaunchpadCelebrities).bug_importer
+
+ # TODO: is this correct?
+ if cve.date_made_public:
+ information_type = InformationType.PRIVATESECURITY
+ else:
+ information_type = InformationType.EMBARGOED
+
+ bug = getUtility(IBugSet).createBug(
+ CreateBugParams(
+ description=cve.ubuntu_description,
+ title=cve.candidate,
+ information_type=information_type,
+ owner=owner,
+ msg=getUtility(IMessageSet).fromText(
+ "", cve.description, owner=owner
+ ),
+ target=distro_package,
+ importance=importances[distro_package.sourcepackagename],
+ )
+ ) # type: BugModel
+
+ # Add links to external bug trackers
+ for external_bug_url in cve.bugs:
+ bug.newMessage(owner=owner, content=external_bug_url)
+
+ # Add references
+ for reference in cve.references:
+ bug.newMessage(owner=owner, content=reference)
+
+ # TODO: shall we store discovered_by?
+ # TODO: shall we store assigned_to? (this looks like a LP username)
+ # TODO: shall we store cvss?
+
+ self.logger.info("Created bug with ID: %s", bug.id)
+
+ # Create bug tasks for distribution packages
+ bug_task_set = getUtility(IBugTaskSet)
+ for distro_package in affected_packages:
+ bug_task_set.createTask(
+ bug,
+ owner,
+ distro_package,
+ importance=importances[distro_package.sourcepackagename],
+ )
+
+ # Create bug tasks for distro series by adding nominations
+ # This may create some extra bug tasks which we will delete later
+ for distro_series in affected_distro_series:
+ nomination = bug.addNomination(owner, distro_series)
+ nomination.approve(owner)
+
+ # Set importance and status on distro series bug tasks
+ # If the bug task's package/series isn't listed in the
+ # CVE entry - delete it
+ for bug_task in bug.bugtasks:
+ distro_series = bug_task.distroseries
+ if not distro_series:
+ continue
+ source_package_name = bug_task.sourcepackagename
+ key = (source_package_name, distro_series)
+ if key not in importances:
+ # This combination of package/series is not present in the CVE
+ # Delete it
+ bug_task.delete(owner)
+ continue
+ bug_task.importance = importances[key]
+ status, status_explanation = statuses_with_explanations[key]
+ bug_task.transitionToStatus(status, owner)
+ bug_task.status_explanation = status_explanation
+
+ # Link the bug to CVE
+ bug.linkCVE(lp_cve, owner)
+
+ # Create the Vulnerabilities
+ vulnerabilities = []
+ for distribution in affected_distributions:
+ vulnerabilities.append(
+ self.create_vulnerability(bug, cve, lp_cve, distribution)
+ )
+
+ return bug, vulnerabilities
+
+ def get_source_package_name(
+ self, package_name: str
+ ) -> Optional[SourcePackageName]:
+ spn = getUtility(ISourcePackageNameSet).queryByName(package_name)
+ if not spn:
+ self.logger.warning("Could not find package: %s", package_name)
+ return spn
+
+ def get_devel_series(
+ self, distribution: Distribution
+ ) -> Optional[DistroSeries]:
+ for series in distribution.series:
+ if series.status == SeriesStatus.FROZEN:
+ return series
+ for series in distribution.series:
+ if series.status == SeriesStatus.DEVELOPMENT:
+ return series
+
+ def get_distro_series(
+ self, distro_series_name: str
+ ) -> Optional[DistroSeries]:
+ if "/" in distro_series_name:
+ series_name, distro_name = distro_series_name.split("/", 1)
+ if distro_name == "esm":
+ # TODO: ESM needs special handling
+ pass
+ return
+ else:
+ series_name = distro_series_name
+ distribution = getUtility(ILaunchpadCelebrities).ubuntu
+ if series_name == "devel":
+ distro_series = self.get_devel_series(distribution)
+ else:
+ distro_series = getUtility(IDistroSeriesSet).queryByName(
+ distribution, series_name
+ )
+ if not distro_series:
+ self.logger.warning(
+ "Could not find the distro series: %s", distro_series_name
+ )
+ return distro_series
+
+ def create_vulnerability(
+ self,
+ bug: BugModel,
+ cve: CVE,
+ lp_cve: CveModel,
+ distribution: Distribution,
+ ) -> Vulnerability:
+ # TODO: is this correct?
+ if cve.date_made_public:
+ information_type = InformationType.PRIVATESECURITY
+ else:
+ information_type = InformationType.EMBARGOED
+
+ vulnerability = getUtility(IVulnerabilitySet).new(
+ distribution=distribution,
+ creator=bug.owner,
+ cve=lp_cve,
+ status=VulnerabilityStatus.NEEDS_TRIAGE,
+ description=cve.description,
+ notes=format_cve_notes(cve.notes),
+ mitigation=cve.mitigation,
+ importance=PRIORITY_MAP[cve.priority],
+ information_type=information_type,
+ date_made_public=cve.date_made_public,
+ ) # type: Vulnerability
+
+ # TODO: is this correct?
+ vulnerability.linkBug(bug, bug.owner)
+
+ self.logger.info("Create vulnerability with ID: %s", vulnerability)
+
+ return vulnerability
+
+
+############################################################################
+# The code below is copied from `cve_lib` module from `ubuntu-cve-tracker` #
+############################################################################
+
+
+def set_cve_dir(path):
+ """Return a path with CVEs in it. Specifically:
+ - if 'path' has CVEs in it, return path
+ - if 'path' is a relative directory with no CVEs, see if UCT is defined
+ and if so, see if 'UCT/path' has CVEs in it and return path
+ """
+ p = path
+ found = False
+ if len(glob.glob("%s/CVE-*" % path)) > 0:
+ found = True
+ elif not path.startswith("/") and "UCT" in os.environ:
+ tmp = os.path.join(os.environ["UCT"], path)
+ if len(glob.glob("%s/CVE-*" % tmp)) > 0:
+ found = True
+ p = tmp
+ # print("INFO: using '%s'" % p, file=sys.stderr)
+
+ if not found:
+ print(
+ "WARN: could not find CVEs in '%s' (or relative to UCT)" % path,
+ file=sys.stderr,
+ )
+ return p
+
+
+if "UCT" in os.environ:
+ active_dir = set_cve_dir(os.environ["UCT"] + "/active")
+ retired_dir = set_cve_dir(os.environ["UCT"] + "/retired")
+ ignored_dir = set_cve_dir(os.environ["UCT"] + "/ignored")
+ embargoed_dir = os.environ["UCT"] + "/embargoed"
+ meta_dir = os.path.join(os.environ["UCT"], "meta_lists")
+ subprojects_dir = os.environ["UCT"] + "/subprojects"
+else:
+ active_dir = set_cve_dir("active")
+ retired_dir = set_cve_dir("retired")
+ ignored_dir = set_cve_dir("ignored")
+ embargoed_dir = "embargoed" # Intentionally not using set_cve_dir()
+ meta_dir = os.path.join(
+ os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
+ "meta_lists",
+ )
+ subprojects_dir = "subprojects"
+
+PRODUCT_UBUNTU = "ubuntu"
+
+# common to all scripts
+# these get populated by the contents of subprojects defined below
+all_releases = []
+eol_releases = []
+external_releases = []
+releases = []
+devel_release = ""
+
+# known subprojects which are supported by cve_lib - in general each
+# subproject is defined by the combination of a product and series as
+# <product/series>.
+#
+# For each subproject, it is either internal (ie is part of this static
+# dict) or external (found dynamically at runtime by
+# load_external_subprojects()).
+#
+# eol specifies whether the subproject is now end-of-life. packages
+# specifies list of files containing the names of supported packages for the
+# subproject. alias defines an alternate preferred name for the subproject
+# (this is often used to support historical names for projects etc).
+subprojects = {
+ "stable-phone-overlay/vivid": {
+ "eol": True,
+ "packages": ["vivid-stable-phone-overlay-supported.txt"],
+ "name": "Ubuntu Touch 15.04",
+ "alias": "vivid/stable-phone-overlay",
+ },
+ "ubuntu-core/vivid": {
+ "eol": True,
+ "packages": ["vivid-ubuntu-core-supported.txt"],
+ "name": "Ubuntu Core 15.04",
+ "alias": "vivid/ubuntu-core",
+ },
+ "esm/precise": {
+ "eol": True,
+ "packages": ["precise-esm-supported.txt"],
+ "name": "Ubuntu 12.04 ESM",
+ "codename": "Precise Pangolin",
+ "alias": "precise/esm",
+ "ppa": "ubuntu-esm/esm",
+ "parent": "ubuntu/precise",
+ "description": (
+ "Available with UA Infra or UA Desktop: "
+ "https://ubuntu.com/advantage"
+ ),
+ "stamp": 1493521200,
+ },
+ "esm/trusty": {
+ "eol": False,
+ "packages": ["trusty-esm-supported.txt"],
+ "name": "Ubuntu 14.04 ESM",
+ "codename": "Trusty Tahr",
+ "alias": "trusty/esm",
+ "ppa": "ubuntu-esm/esm-infra-security",
+ "parent": "ubuntu/trusty",
+ "description": (
+ "Available with UA Infra or UA Desktop: "
+ "https://ubuntu.com/advantage"
+ ),
+ "stamp": 1556593200,
+ },
+ "esm-infra/xenial": {
+ "eol": False,
+ "components": ["main", "restricted"],
+ "packages": ["esm-infra-xenial-supported.txt"],
+ "name": "Ubuntu 16.04 ESM",
+ "codename": "Xenial Xerus",
+ "ppa": "ubuntu-esm/esm-infra-security",
+ "parent": "ubuntu/xenial",
+ "description": (
+ "Available with UA Infra or UA Desktop: "
+ "https://ubuntu.com/advantage"
+ ),
+ "stamp": 1618963200,
+ },
+ "fips/xenial": {
+ "eol": False,
+ "packages": ["fips-xenial-supported.txt"],
+ "name": "Ubuntu 16.04 FIPS Certified",
+ "codename": "Xenial Xerus",
+ "ppa": "ubuntu-advantage/fips",
+ "parent": "ubuntu/xenial",
+ "description": "Available with UA ... https://ubuntu.com/advantage",
+ },
+ "fips/bionic": {
+ "eol": False,
+ "packages": ["fips-bionic-supported.txt"],
+ "name": "Ubuntu 18.04 FIPS Certified",
+ "codename": "Bionic Beaver",
+ "ppa": "ubuntu-advantage/fips",
+ "parent": "ubuntu/bionic",
+ "description": "Available with UA ... https://ubuntu.com/advantage",
+ },
+ "fips/focal": {
+ "eol": False,
+ "packages": ["fips-focal-supported.txt"],
+ "name": "Ubuntu 20.04 FIPS Certified",
+ "codename": "Focal Fossa",
+ "ppa": "ubuntu-advantage/fips",
+ "parent": "ubuntu/bionic",
+ "description": "Available with UA ... https://ubuntu.com/advantage",
+ },
+ "fips-updates/xenial": {
+ "eol": False,
+ "packages": ["fips-updates-xenial-supported.txt"],
+ "name": "Ubuntu 16.04 FIPS Compliant",
+ "codename": "Xenial Xerus",
+ "ppa": "ubuntu-advantage/fips-updates",
+ "parent": "ubuntu/xenial",
+ "description": "Available with UA ... https://ubuntu.com/advantage",
+ },
+ "fips-updates/bionic": {
+ "eol": False,
+ "packages": ["fips-updates-bionic-supported.txt"],
+ "name": "Ubuntu 18.04 FIPS Compliant",
+ "codename": "Bionic Beaver",
+ "ppa": "ubuntu-advantage/fips-updates",
+ "parent": "ubuntu/bionic",
+ "description": "Available with UA ... https://ubuntu.com/advantage",
+ },
+ "fips-updates/focal": {
+ "eol": False,
+ "packages": ["fips-updates-focal-supported.txt"],
+ "name": "Ubuntu 20.04 FIPS Compliant",
+ "codename": "Focal Fossa",
+ "ppa": "ubuntu-advantage/fips-updates",
+ "parent": "ubuntu/bionic",
+ "description": "Available with UA ... https://ubuntu.com/advantage",
+ },
+ "ubuntu/warty": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 4.10",
+ "codename": "Warty Warthog",
+ "alias": "warty",
+ "description": "Interim Release",
+ "stamp": 1098748800,
+ },
+ "ubuntu/hoary": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 5.04",
+ "codename": "Hoary Hedgehog",
+ "alias": "hoary",
+ "description": "Interim Release",
+ "stamp": 1112918400,
+ },
+ "ubuntu/breezy": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 5.10",
+ "codename": "Breezy Badger",
+ "alias": "breezy",
+ "description": "Interim Release",
+ "stamp": 1129075200,
+ },
+ "ubuntu/dapper": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 6.06 LTS",
+ "codename": "Dapper Drake",
+ "alias": "dapper",
+ "description": "Long Term Support",
+ "stamp": 1149120000,
+ },
+ "ubuntu/edgy": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 6.10",
+ "codename": "Edgy Eft",
+ "alias": "edgy",
+ "description": "Interim Release",
+ "stamp": 1161864000,
+ },
+ "ubuntu/feisty": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 7.04",
+ "codename": "Feisty Fawn",
+ "alias": "feisty",
+ "description": "Interim Release",
+ "stamp": 1176984000,
+ },
+ "ubuntu/gutsy": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 7.10",
+ "codename": "Gutsy Gibbon",
+ "alias": "gutsy",
+ "description": "Interim Release",
+ "stamp": 1192708800,
+ },
+ "ubuntu/hardy": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 8.04 LTS",
+ "codename": "Hardy Heron",
+ "alias": "hardy",
+ "description": "Long Term Support",
+ "stamp": 1209038400,
+ },
+ "ubuntu/intrepid": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 8.10",
+ "codename": "Intrepid Ibex",
+ "alias": "intrepid",
+ "description": "Interim Release",
+ "stamp": 1225368000,
+ },
+ "ubuntu/jaunty": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 9.04",
+ "codename": "Jaunty Jackalope",
+ "alias": "jaunty",
+ "description": "Interim Release",
+ "stamp": 1240488000,
+ },
+ "ubuntu/karmic": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 9.10",
+ "codename": "Karmic Koala",
+ "alias": "karmic",
+ "description": "Interim Release",
+ "stamp": 1256817600,
+ },
+ "ubuntu/lucid": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 10.04 LTS",
+ "codename": "Lucid Lynx",
+ "alias": "lucid",
+ "description": "Long Term Support",
+ "stamp": 1272565800,
+ },
+ "ubuntu/maverick": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 10.10",
+ "codename": "Maverick Meerkat",
+ "alias": "maverick",
+ "description": "Interim Release",
+ "stamp": 1286706600,
+ },
+ "ubuntu/natty": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 11.04",
+ "codename": "Natty Narwhal",
+ "alias": "natty",
+ "description": "Interim Release",
+ "stamp": 1303822800,
+ },
+ "ubuntu/oneiric": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 11.10",
+ "codename": "Oneiric Ocelot",
+ "alias": "oneiric",
+ "description": "Interim Release",
+ "stamp": 1318446000,
+ },
+ "ubuntu/precise": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 12.04 LTS",
+ "codename": "Precise Pangolin",
+ "alias": "precise",
+ "description": "Long Term Support",
+ "stamp": 1335423600,
+ },
+ "ubuntu/quantal": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 12.10",
+ "codename": "Quantal Quetzal",
+ "alias": "quantal",
+ "description": "Interim Release",
+ "stamp": 1350547200,
+ },
+ "ubuntu/raring": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 13.04",
+ "codename": "Raring Ringtail",
+ "alias": "raring",
+ "description": "Interim Release",
+ "stamp": 1366891200,
+ },
+ "ubuntu/saucy": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 13.10",
+ "codename": "Saucy Salamander",
+ "alias": "saucy",
+ "description": "Interim Release",
+ "stamp": 1381993200,
+ },
+ "ubuntu/trusty": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 14.04 LTS",
+ "codename": "Trusty Tahr",
+ "alias": "trusty",
+ "description": "Long Term Support",
+ "stamp": 1397826000,
+ },
+ "ubuntu/utopic": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 14.10",
+ "codename": "Utopic Unicorn",
+ "alias": "utopic",
+ "description": "Interim Release",
+ "stamp": 1414083600,
+ },
+ "ubuntu/vivid": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 15.04",
+ "codename": "Vivid Vervet",
+ "alias": "vivid",
+ "description": "Interim Release",
+ "stamp": 1429027200,
+ },
+ "ubuntu/wily": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 15.10",
+ "codename": "Wily Werewolf",
+ "alias": "wily",
+ "description": "Interim Release",
+ "stamp": 1445518800,
+ },
+ "ubuntu/xenial": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 16.04 LTS",
+ "codename": "Xenial Xerus",
+ "alias": "xenial",
+ "description": "Long Term Support",
+ "stamp": 1461279600,
+ },
+ "ubuntu/yakkety": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 16.10",
+ "codename": "Yakkety Yak",
+ "alias": "yakkety",
+ "description": "Interim Release",
+ "stamp": 1476518400,
+ },
+ "ubuntu/zesty": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 17.04",
+ "codename": "Zesty Zapus",
+ "alias": "zesty",
+ "description": "Interim Release",
+ "stamp": 1492153200,
+ },
+ "ubuntu/artful": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 17.10",
+ "codename": "Artful Aardvark",
+ "alias": "artful",
+ "description": "Interim Release",
+ "stamp": 1508418000,
+ },
+ "ubuntu/bionic": {
+ "eol": False,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 18.04 LTS",
+ "codename": "Bionic Beaver",
+ "alias": "bionic",
+ "description": "Long Term Support",
+ "stamp": 1524870000,
+ },
+ "ubuntu/cosmic": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 18.10",
+ "codename": "Cosmic Cuttlefish",
+ "alias": "cosmic",
+ "description": "Interim Release",
+ "stamp": 1540040400,
+ },
+ "ubuntu/disco": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 19.04",
+ "codename": "Disco Dingo",
+ "alias": "disco",
+ "description": "Interim Release",
+ "stamp": 1555581600,
+ },
+ "ubuntu/eoan": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 19.10",
+ "codename": "Eoan Ermine",
+ "alias": "eoan",
+ "description": "Interim Release",
+ "stamp": 1571234400,
+ },
+ "ubuntu/focal": {
+ "eol": False,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 20.04 LTS",
+ "codename": "Focal Fossa",
+ "alias": "focal",
+ "description": "Long Term Support",
+ "stamp": 1587567600,
+ },
+ "ubuntu/groovy": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 20.10",
+ "codename": "Groovy Gorilla",
+ "alias": "groovy",
+ "description": "Interim Release",
+ "stamp": 1603288800,
+ },
+ "ubuntu/hirsute": {
+ "eol": True,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 21.04",
+ "codename": "Hirsute Hippo",
+ "alias": "hirsute",
+ "description": "Interim Release",
+ "stamp": 1619049600,
+ },
+ "ubuntu/impish": {
+ "eol": False,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 21.10",
+ "codename": "Impish Indri",
+ "alias": "impish",
+ "description": "Interim Release",
+ "stamp": 1634220000,
+ },
+ "ubuntu/jammy": {
+ "eol": False,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 22.04 LTS",
+ "codename": "Jammy Jellyfish",
+ "alias": "jammy",
+ "description": "Long Term Support",
+ "stamp": 1650693600,
+ },
+ "ubuntu/kinetic": {
+ "eol": False,
+ "components": [
+ "main",
+ "restricted",
+ "universe",
+ "multiverse",
+ "partner",
+ ],
+ "name": "Ubuntu 22.10",
+ "codename": "Kinetic Kudu",
+ "alias": "kinetic",
+ "devel": True, # there can be only one ⚔
+ "description": "Interim Release",
+ },
+ "snap": {
+ "eol": False,
+ "packages": ["snap-supported.txt"],
+ },
+}
+
+
+def product_series(rel):
+ """Return the product,series tuple for rel."""
+ series = ""
+ parts = rel.split("/", 1)
+ product = parts[0]
+ if len(parts) == 2:
+ series = parts[1]
+ return product, series
+
+
+# get the subproject details for rel along with
+# it's canonical name, product and series
+def get_subproject_details(rel):
+ """Return the product,series,details tuple for rel."""
+ canon, product, series, details = None, None, None, None
+ try:
+ details = subprojects[rel]
+ product, series = product_series(rel)
+ canon = product + "/" + series
+ except (ValueError, KeyError):
+ # look for alias
+ for r in subprojects:
+ try:
+ if subprojects[r]["alias"] == rel:
+ product, series = product_series(r)
+ details = subprojects[r]
+ canon = product + "/" + series
+ break
+ except KeyError:
+ pass
+ if details is not None:
+ break
+ return canon, product, series, details
+
+
+def release_alias(rel):
+ """Return the alias for rel or just rel if no alias is defined."""
+ alias = rel
+ _, _, _, details = get_subproject_details(rel)
+ try:
+ alias = details["alias"]
+ except (KeyError, TypeError):
+ pass
+ return alias
+
+
+def release_parent(rel):
+ """Return the parent for rel or None if no parent is defined."""
+ parent = None
+ _, _, _, details = get_subproject_details(rel)
+ try:
+ parent = release_alias(details["parent"])
+ except (KeyError, TypeError):
+ pass
+ return parent
+
+
+def get_external_subproject_cve_dir(subproject):
+ """Get the directory where CVE files are stored for the subproject.
+
+ Get the directory where CVE files are stored for a subproject. In
+ general this is within the higher level project directory, not within
+ the specific subdirectory for the particular series that defines this
+ subproject.
+
+ """
+ rel, product, _, _ = get_subproject_details(subproject)
+ if rel not in external_releases:
+ raise ValueError("%s is not an external subproject" % rel)
+ # CVEs live in the product dir
+ return os.path.join(subprojects_dir, product)
+
+
+def get_external_subproject_dir(subproject):
+ """Get the directory for the given external subproject."""
+ rel, _, _, _ = get_subproject_details(subproject)
+ if rel not in external_releases:
+ raise ValueError("%s is not an external subproject" % rel)
+ return os.path.join(subprojects_dir, rel)
+
+
+def read_external_subproject_config(subproject):
+ """Read and return the configuration for the given subproject."""
+ sp_dir = get_external_subproject_dir(subproject)
+ config_yaml = os.path.join(sp_dir, "config.yaml")
+ with open(config_yaml) as cfg:
+ return yaml.safe_load(cfg)
+
+
+def find_files_recursive(path, name):
+ """Return a list of all files under path with name."""
+ matches = []
+ for root, _, files in os.walk(path, followlinks=True):
+ for f in files:
+ if f == name:
+ filepath = os.path.join(root, f)
+ matches.append(filepath)
+ return matches
+
+
+def find_external_subproject_cves(cve):
+ """
+ Return the list of external subproject CVE snippets for the given CVE.
+ """
+ cves = []
+ for rel in external_releases:
+ # fallback to the series specific subdir rather than just the
+ # top-level project directory even though this is preferred
+ for d in [
+ get_external_subproject_cve_dir(rel),
+ get_external_subproject_dir(rel),
+ ]:
+ path = os.path.join(d, cve)
+ if os.path.exists(path):
+ cves.append(path)
+ return cves
+
+
+def load_external_subprojects():
+ """Search for and load subprojects into the global subprojects dict.
+
+ Search for and load subprojects into the global subprojects dict.
+
+ A subproject is defined as a directory which resides within
+ subprojects_dir and contains a supported.txt file. It can also contain
+ a project.yml file which specifies configuration directives for the
+ project as well as snippet CVE files. By convention, a subproject is
+ usually defined as the combination of a product and series, ie:
+
+ esm-apps/focal
+
+ as such in this case there would expect to be within subprojects_dir a
+ directory called esm-apps/ and within that a subdirectory called
+ focal/. Inside this focal/ subdirectory a supported.txt file would list
+ the packages which are supported by the esm-apps/focal subproject. By
+ convention, snippet CVE files should reside within the esm-apps/
+ project directory rather than the esm-apps/focal/ subdirectory to avoid
+ unnecessary fragmentation across different subproject series.
+
+ """
+ for supported_txt in find_files_recursive(
+ subprojects_dir, "supported.txt"
+ ):
+ # rel name is the path component between subprojects/ and
+ # /supported.txt
+ rel = supported_txt[
+ len(subprojects_dir) + 1:-len("supported.txt") - 1
+ ]
+ external_releases.append(rel)
+ subprojects.setdefault(rel, {"packages": [], "eol": False})
+ # an external subproject can append to an internal one
+ subprojects[rel]["packages"].append(supported_txt)
+ try:
+ # use config to populate other parts of the
+ # subproject settings
+ config = read_external_subproject_config(rel)
+ subprojects[rel].setdefault("ppa", config["ppa"])
+ subprojects[rel].setdefault("name", config["name"])
+ subprojects[rel].setdefault("description", config["description"])
+ subprojects[rel].setdefault("parent", config["parent"])
+ except Exception:
+ pass
+
+
+load_external_subprojects()
+
+for release in subprojects:
+ details = subprojects[release]
+ rel = release_alias(release)
+ # prefer the alias name
+ all_releases.append(rel)
+ if details["eol"]:
+ eol_releases.append(rel)
+ if "devel" in details and details["devel"]:
+ if devel_release != "":
+ raise ValueError("there can be only one ⚔ devel")
+ devel_release = rel
+ # ubuntu specific releases
+ product, series = product_series(release)
+ if product == PRODUCT_UBUNTU:
+ releases.append(rel)
+
+
+VALID_TAGS = {
+ "universe-binary": (
+ "Binaries built from this source package are in universe and so are "
+ "supported by the community. For more details see "
+ "https://wiki.ubuntu.com/SecurityTeam/FAQ#Official_Support"
+ ),
+ "not-ue": (
+ "This package is not directly supported by the Ubuntu Security Team"
+ ),
+ "apparmor": (
+ "This vulnerability is mitigated in part by an AppArmor profile. "
+ "For more details see "
+ "https://wiki.ubuntu.com/Security/Features#apparmor"
+ ),
+ "stack-protector": (
+ "This vulnerability is mitigated in part by the use of gcc's stack "
+ "protector in Ubuntu. For more details see "
+ "https://wiki.ubuntu.com/Security/Features#stack-protector"
+ ),
+ "fortify-source": (
+ "This vulnerability is mitigated in part by the use of "
+ "-D_FORTIFY_SOURCE=2 in Ubuntu. For more details see "
+ "https://wiki.ubuntu.com/Security/Features#fortify-source"
+ ),
+ "symlink-restriction": (
+ "This vulnerability is mitigated in part by the use of symlink "
+ "restrictions in Ubuntu. For more details see "
+ "https://wiki.ubuntu.com/Security/Features#symlink"
+ ),
+ "hardlink-restriction": (
+ "This vulnerability is mitigated in part by the use of hardlink "
+ "restrictions in Ubuntu. For more details see "
+ "https://wiki.ubuntu.com/Security/Features#hardlink"
+ ),
+ "heap-protector": (
+ "This vulnerability is mitigated in part by the use of GNU C Library "
+ "heap protector in Ubuntu. For more details see "
+ "https://wiki.ubuntu.com/Security/Features#heap-protector"
+ ),
+ "pie": (
+ "This vulnerability is mitigated in part by the use of Position "
+ "Independent Executables in Ubuntu. For more details see "
+ "https://wiki.ubuntu.com/Security/Features#pie"
+ ),
+}
+
+# Possible CVE priorities
+PRIORITIES = ["negligible", "low", "medium", "high", "critical"]
+
+NOTE_RE = re.compile(r"^\s+([A-Za-z0-9-]+)([>|]) *(.*)$")
+
+EXIT_FAIL = 1
+EXIT_OKAY = 0
+
+# New CVE file format for release package field is:
+# <product>[/<where or who>]_SOFTWARE[/<modifier>]: <status> [(<when>)]
+# <product> is the Canonical product or supporting technology (eg, ‘esm-apps’
+# or ‘snap’). ‘ubuntu’ is the implied product when ‘<product>/’ is omitted
+# from the ‘<product>[/<where or who>]’ tuple (ie, where we might use
+# ‘ubuntu/bionic_DEBSRCPKG’ for consistency, we continue to use
+# ‘bionic_DEBSRCPKG’)
+# <where or who> indicates where the software lives or in the case of snaps or
+# other technologies with a concept of publishers, who the publisher is
+# SOFTWARE is the name of the software as dictated by the product (eg, the deb
+# source package, the name of the snap or the name of the software project
+# <modifier> is an optional key for grouping collections of packages (eg,
+# ‘melodic’ for the ROS Melodic release or ‘rocky’ for the OpenStack Rocky
+# release)
+# <status> indicates the statuses as defined in UCT (eg, needs-triage, needed,
+# pending, released, etc)
+# <when> indicates ‘when’ the software will be/was fixed when used with the
+# ‘pending’ or ‘released’ status (eg, the source package version, snap
+# revision, etc)
+# e.g.: esm-apps/xenial_jackson-databind: released (2.4.2-3ubuntu0.1~esm2)
+# e.g.: git/github.com/gogo/protobuf_gogoprotobuf: needs-triage
+# This method should keep supporting existing current format:
+# e.g.: bionic_jackson-databind: needs-triage
+def parse_cve_release_package_field(
+ cve, field, data, value, code, msg, linenum
+):
+ package = ""
+ release = ""
+ state = ""
+ details = ""
+ try:
+ release, package = field.split("_", 1)
+ except ValueError:
+ msg += "%s: %d: bad field with '_': '%s'\n" % (cve, linenum, field)
+ code = EXIT_FAIL
+ return False, package, release, state, details, code, msg
+
+ try:
+ info = value.split(" ", 1)
+ except ValueError:
+ msg += "%s: %d: missing state for '%s': '%s'\n" % (
+ cve,
+ linenum,
+ field,
+ value,
+ )
+ code = EXIT_FAIL
+ return False, package, release, state, details, code, msg
+
+ state = info[0]
+ if state == "":
+ state = "needs-triage"
+
+ if len(info) < 2:
+ details = ""
+ else:
+ details = info[1].strip()
+
+ if details.startswith("["):
+ msg += "%s: %d: %s has details that starts with a bracket: '%s'\n" % (
+ cve,
+ linenum,
+ field,
+ details,
+ )
+ code = EXIT_FAIL
+ return False, package, release, state, details, code, msg
+
+ if details.startswith("("):
+ details = details[1:]
+ if details.endswith(")"):
+ details = details[:-1]
+
+ # Work-around for old-style of only recording released versions
+ if details == "" and state[0] in ("0123456789"):
+ details = state
+ state = "released"
+
+ valid_states = [
+ "needs-triage",
+ "needed",
+ "active",
+ "pending",
+ "released",
+ "deferred",
+ "DNE",
+ "ignored",
+ "not-affected",
+ ]
+ if state not in valid_states:
+ msg += (
+ "%s: %d: %s has unknown state: '%s' (valid states are: %s)\n"
+ % (
+ cve,
+ linenum,
+ field,
+ state,
+ " ".join(valid_states),
+ )
+ )
+ code = EXIT_FAIL
+ return False, package, release, state, details, code, msg
+
+ # Verify "released" kernels have version details
+ # if state == 'released' and package in kernel_srcs and details == '':
+ # msg += "%s: %s_%s has state '%s' but lacks version note\n" % (
+ # cve, package, release, state
+ # )
+ # code = EXIT_FAIL
+
+ # Verify "active" states have an Assignee
+ if state == "active" and data["Assigned-to"].strip() == "":
+ msg += "%s: %d: %s has state '%s' but lacks 'Assigned-to'\n" % (
+ cve,
+ linenum,
+ field,
+ state,
+ )
+ code = EXIT_FAIL
+ return False, package, release, state, details, code, msg
+
+ return True, package, release, state, details, code, msg
+
+
+class NotesParser:
+ def __init__(self):
+ self.notes = list()
+ self.user = None
+ self.separator = None
+ self.note = None
+
+ def parse_line(self, cve, line, linenum, code):
+ msg = ""
+ m = NOTE_RE.match(line)
+ if m is not None:
+ new_user = m.group(1)
+ new_sep = m.group(2)
+ new_note = m.group(3)
+ else:
+ # follow up comments should have 2 space indent and
+ # an author
+ if self.user is None:
+ msg += "%s: %d: Note entry with no author: '%s'\n" % (
+ cve,
+ linenum,
+ line[1:],
+ )
+ code = EXIT_FAIL
+ if not line.startswith(" "):
+ msg += (
+ "%s: %d: Note continuations should be indented by "
+ "2 spaces: '%s'.\n" % (cve, linenum, line)
+ )
+ code = EXIT_FAIL
+ new_user = self.user
+ new_sep = self.separator
+ new_note = line.strip()
+ if self.user and self.separator and self.note:
+ # if is different user, start a new note
+ if new_user != self.user:
+ self.notes.append((self.user, self.note))
+ self.user = new_user
+ self.note = new_note
+ self.separator = new_sep
+ elif new_sep != self.separator:
+ # finish this note and start a new one since this has new
+ # semantics
+ self.notes.append((self.user, self.note))
+ self.separator = new_sep
+ self.note = new_note
+ else:
+ if self.separator == "|":
+ self.note = self.note + " " + new_note
+ else:
+ assert self.separator == ">"
+ self.note = self.note + "\n" + new_note
+ else:
+ # this is the first note
+ self.user = new_user
+ self.separator = new_sep
+ self.note = new_note
+ return code, msg
+
+ def finalize(self):
+ if self.user is not None and self.note is not None:
+ # add last Note
+ self.notes.append((self.user, self.note))
+ self.user = None
+ self.note = None
+ notes = self.notes
+ self.user = None
+ self.separator = None
+ self.notes = None
+ return notes
+
+
+def load_cve(cve, strict=False, srcmap=None):
+ """Loads a given CVE into:
+ dict( fields...
+ 'pkgs' -> dict( pkg -> dict( release -> (state, details) ) )
+ )
+ """
+
+ msg = ""
+ code = EXIT_OKAY
+ required_fields = [
+ "Candidate",
+ "PublicDate",
+ "References",
+ "Description",
+ "Ubuntu-Description",
+ "Notes",
+ "Bugs",
+ "Priority",
+ "Discovered-by",
+ "Assigned-to",
+ "CVSS",
+ ]
+ extra_fields = ["CRD", "PublicDateAtUSN", "Mitigation"]
+
+ data = dict()
+ # maps entries in data to their source line - if didn't supply one
+ # create a local one to simplify the code
+ if srcmap is None:
+ srcmap = dict()
+ srcmap.setdefault("pkgs", dict())
+ srcmap.setdefault("tags", dict())
+ data.setdefault("tags", dict())
+ srcmap.setdefault("patches", dict())
+ data.setdefault("patches", dict())
+ affected = dict()
+ lastfield = ""
+ fields_seen = []
+ if not os.path.exists(cve):
+ raise ValueError("File does not exist: '%s'" % (cve))
+ linenum = 0
+ notes_parser = NotesParser()
+ cvss_entries = []
+
+ cve_file = codecs.open(cve, encoding="utf-8")
+
+ for line in cve_file.readlines():
+ line = line.rstrip()
+ linenum += 1
+
+ # Ignore blank/commented lines
+ if len(line) == 0 or line.startswith("#"):
+ continue
+ if line.startswith(" "):
+ try:
+ # parse Notes properly
+ if lastfield == "Notes":
+ code, newmsg = notes_parser.parse_line(
+ cve, line, linenum, code
+ )
+ if code != EXIT_OKAY:
+ msg += newmsg
+ elif "Patches_" in lastfield:
+ try:
+ _, pkg = lastfield.split("_", 1)
+ patch_type, entry = line.split(":", 1)
+ patch_type = patch_type.strip()
+ entry = entry.strip()
+ data["patches"][pkg].append((patch_type, entry))
+ srcmap["patches"][pkg].append((cve, linenum))
+ except Exception as e:
+ msg += (
+ "%s: %d: Failed to parse '%s' entry %s: %s\n"
+ % (
+ cve,
+ linenum,
+ lastfield,
+ line,
+ e,
+ )
+ )
+ code = EXIT_FAIL
+ elif lastfield == "CVSS":
+ try:
+ cvss = dict()
+ result = re.search(
+ r" (.+)\: (\S+)( \[(.*) (.*)\])?", line
+ )
+ if result is None:
+ continue
+ cvss["source"] = result.group(1)
+ cvss["vector"] = result.group(2)
+ entry = parse_cvss(cvss["vector"])
+ if entry is None:
+ raise RuntimeError(
+ "Failed to parse_cvss() without raising "
+ "an exception."
+ )
+ if result.group(3):
+ cvss["baseScore"] = result.group(4)
+ cvss["baseSeverity"] = result.group(5)
+
+ cvss_entries.append(cvss)
+ # CVSS in srcmap will be a tuple since this is the
+ # line where the CVSS block starts - so convert it
+ # to a dict first if needed
+ if type(srcmap["CVSS"]) is tuple:
+ srcmap["CVSS"] = dict()
+ srcmap["CVSS"].setdefault(
+ cvss["source"], (cve, linenum)
+ )
+ except Exception as e:
+ msg += "%s: %d: Failed to parse CVSS: %s\n" % (
+ cve,
+ linenum,
+ e,
+ )
+ code = EXIT_FAIL
+ else:
+ data[lastfield] += "\n%s" % (line[1:])
+ except KeyError as e:
+ msg += "%s: %d: bad line '%s' (%s)\n" % (cve, linenum, line, e)
+ code = EXIT_FAIL
+ continue
+
+ try:
+ field, value = line.split(":", 1)
+ except ValueError as e:
+ msg += "%s: %d: bad line '%s' (%s)\n" % (cve, linenum, line, e)
+ code = EXIT_FAIL
+ continue
+
+ lastfield = field = field.strip()
+ if field in fields_seen:
+ msg += "%s: %d: repeated field '%s'\n" % (cve, linenum, field)
+ code = EXIT_FAIL
+ else:
+ fields_seen.append(field)
+ value = value.strip()
+ if field == "Candidate":
+ data.setdefault(field, value)
+ srcmap.setdefault(field, (cve, linenum))
+ if (
+ value != ""
+ and not value.startswith("CVE-")
+ and not value.startswith("UEM-")
+ and not value.startswith("EMB-")
+ ):
+ msg += (
+ "%s: %d: unknown Candidate '%s' "
+ "(must be /(CVE|UEM|EMB)-/)\n"
+ % (
+ cve,
+ linenum,
+ value,
+ )
+ )
+ code = EXIT_FAIL
+ elif "Priority" in field:
+ # For now, throw away comments on Priority fields
+ if " " in value:
+ value = value.split()[0]
+ if "Priority_" in field:
+ try:
+ _, pkg = field.split("_", 1)
+ except ValueError:
+ msg += "%s: %d: bad field with 'Priority_': '%s'\n" % (
+ cve,
+ linenum,
+ field,
+ )
+ code = EXIT_FAIL
+ continue
+ data.setdefault(field, value)
+ srcmap.setdefault(field, (cve, linenum))
+ if value not in ["untriaged", "not-for-us"] + PRIORITIES:
+ msg += "%s: %d: unknown Priority '%s'\n" % (
+ cve,
+ linenum,
+ value,
+ )
+ code = EXIT_FAIL
+ elif "Patches_" in field:
+ try:
+ _, pkg = field.split("_", 1)
+ except ValueError:
+ msg += "%s: %d: bad field with 'Patches_': '%s'\n" % (
+ cve,
+ linenum,
+ field,
+ )
+ code = EXIT_FAIL
+ continue
+ # value should be empty
+ if len(value) > 0:
+ msg += "%s: %d: '%s' field should have no value\n" % (
+ cve,
+ linenum,
+ field,
+ )
+ code = EXIT_FAIL
+ continue
+ data["patches"].setdefault(pkg, list())
+ srcmap["patches"].setdefault(pkg, list())
+ elif "Tags_" in field:
+ """These are processed into the "tags" hash"""
+ try:
+ _, pkg = field.split("_", 1)
+ except ValueError:
+ msg += "%s: %d: bad field with 'Tags_': '%s'\n" % (
+ cve,
+ linenum,
+ field,
+ )
+ code = EXIT_FAIL
+ continue
+ data["tags"].setdefault(pkg, set())
+ srcmap["tags"].setdefault(pkg, (cve, linenum))
+ for word in value.strip().split(" "):
+ if word not in VALID_TAGS:
+ msg += "%s: %d: invalid tag '%s': '%s'\n" % (
+ cve,
+ linenum,
+ word,
+ field,
+ )
+ code = EXIT_FAIL
+ continue
+ data["tags"][pkg].add(word)
+ elif "_" in field:
+ (
+ success,
+ pkg,
+ rel,
+ state,
+ details,
+ code,
+ msg,
+ ) = parse_cve_release_package_field(
+ cve, field, data, value, code, msg, linenum
+ )
+ if not success:
+ assert code == EXIT_FAIL
+ continue
+ canon, _, _, _ = get_subproject_details(rel)
+ if canon is None and rel not in ["upstream", "devel"]:
+ msg += "%s: %d: unknown entry '%s'\n" % (cve, linenum, rel)
+ code = EXIT_FAIL
+ continue
+ affected.setdefault(pkg, dict())
+ if rel in affected[pkg]:
+ msg += (
+ "%s: %d: duplicate entry for '%s': original at line %d\n"
+ % (
+ cve,
+ linenum,
+ rel,
+ srcmap["pkgs"][pkg][rel][1],
+ )
+ )
+ code = EXIT_FAIL
+ continue
+ affected[pkg].setdefault(rel, [state, details])
+ srcmap["pkgs"].setdefault(pkg, dict())
+ srcmap["pkgs"][pkg].setdefault(rel, (cve, linenum))
+ elif field not in required_fields + extra_fields:
+ msg += "%s: %d: unknown field '%s'\n" % (cve, linenum, field)
+ code = EXIT_FAIL
+ else:
+ data.setdefault(field, value)
+ srcmap.setdefault(field, (cve, linenum))
+
+ cve_file.close()
+
+ data["Notes"] = notes_parser.finalize()
+ data["CVSS"] = cvss_entries
+
+ # Check for required fields
+ for field in required_fields:
+ nonempty = ["Candidate"]
+ if strict:
+ nonempty += ["PublicDate"]
+ # boilerplate files are special and can (should?) be empty
+ if "boilerplate" in cve:
+ nonempty = []
+
+ if field not in data or field not in fields_seen:
+ msg += "%s: %d: missing field '%s'\n" % (cve, linenum, field)
+ code = EXIT_FAIL
+ elif field in nonempty and data[field].strip() == "":
+ msg += "%s: %d: required field '%s' is empty\n" % (
+ cve,
+ linenum,
+ field,
+ )
+ code = EXIT_FAIL
+
+ # Fill in defaults for missing fields
+ if "Priority" not in data:
+ data.setdefault("Priority", "untriaged")
+ srcmap.setdefault("Priority", (cve, 1))
+ # Perform override fields
+ if "PublicDateAtUSN" in data:
+ data["PublicDate"] = data["PublicDateAtUSN"]
+ srcmap["PublicDate"] = srcmap["PublicDateAtUSN"]
+ if (
+ "CRD" in data
+ and data["CRD"].strip() != ""
+ and data["PublicDate"] != data["CRD"]
+ ):
+ if cve.startswith("embargoed"):
+ print(
+ "%s: %d: adjusting PublicDate to use CRD: %s"
+ % (cve, linenum, data["CRD"]),
+ file=sys.stderr,
+ )
+ data["PublicDate"] = data["CRD"]
+ srcmap["PublicDate"] = srcmap["CRD"]
+
+ # entries need an upstream entry if any entries are from the internal
+ # list of subprojects
+ for pkg in affected:
+ needs_upstream = False
+ for rel in affected[pkg]:
+ if rel not in external_releases:
+ needs_upstream = True
+ if needs_upstream and "upstream" not in affected[pkg]:
+ msg += "%s: %d: missing upstream '%s'\n" % (cve, linenum, pkg)
+ code = EXIT_FAIL
+
+ data["pkgs"] = affected
+
+ code, msg = load_external_subproject_cve_data(cve, data, srcmap, code, msg)
+
+ if code != EXIT_OKAY:
+ raise ValueError(msg.strip())
+ return data
+
+
+def amend_external_subproject_pkg(cve, data, srcmap, amendments, code, msg):
+ linenum = 0
+ for line in amendments.splitlines():
+ linenum += 1
+ if len(line) == 0 or line.startswith("#") or line.startswith(" "):
+ continue
+ try:
+ field, value = line.split(":", 1)
+ field = field.strip()
+ value = value.strip()
+ except ValueError as e:
+ msg += "%s: bad line '%s' (%s)\n" % (cve, line, e)
+ code = EXIT_FAIL
+ return code, msg
+
+ if "_" in field:
+ (
+ success,
+ pkg,
+ release,
+ state,
+ details,
+ code,
+ msg,
+ ) = parse_cve_release_package_field(
+ cve, field, data, value, code, msg, linenum
+ )
+ if not success:
+ return code, msg
+
+ data.setdefault("pkgs", dict())
+ data["pkgs"].setdefault(pkg, dict())
+ srcmap["pkgs"].setdefault(pkg, dict())
+ # override existing release info if it exists
+ data["pkgs"][pkg][release] = [state, details]
+ srcmap["pkgs"][pkg][release] = (cve, linenum)
+
+ return code, msg
+
+
+def load_external_subproject_cve_data(cve, data, srcmap, code, msg):
+ cve_id = os.path.basename(cve)
+ for f in find_external_subproject_cves(cve_id):
+ with codecs.open(f, "r", encoding="utf-8") as fp:
+ amendments = fp.read()
+ fp.close()
+ code, msg = amend_external_subproject_pkg(
+ f, data, srcmap, amendments, code, msg
+ )
+
+ return code, msg
+
+
+def parse_cvss(cvss):
+ # parse a CVSS string into components suitable for MITRE / NVD JSON
+ # format - assumes only the Base metric group from
+ # https://www.first.org/cvss/specification-document since this is
+ # mandatory - also validates by raising exceptions on errors
+ metrics = {
+ "attackVector": {
+ "abbrev": "AV",
+ "values": {
+ "NETWORK": 0.85,
+ "ADJACENT": 0.62,
+ "LOCAL": 0.55,
+ "PHYSICAL": 0.2,
+ },
+ },
+ "attackComplexity": {
+ "abbrev": "AC",
+ "values": {"LOW": 0.77, "HIGH": 0.44},
+ },
+ "privilegesRequired": {
+ "abbrev": "PR",
+ "values": {
+ "NONE": 0.85,
+ # [ scope unchanged, changed ]
+ "LOW": [0.62, 0.68], # depends on scope
+ "HIGH": [0.27, 0.5],
+ }, # depends on scope
+ },
+ "userInteraction": {
+ "abbrev": "UI",
+ "values": {"NONE": 0.85, "REQUIRED": 0.62},
+ },
+ "scope": {"abbrev": "S", "values": {"UNCHANGED", "CHANGED"}},
+ "confidentialityImpact": {
+ "abbrev": "C",
+ "values": {"HIGH": 0.56, "LOW": 0.22, "NONE": 0},
+ },
+ "integrityImpact": {
+ "abbrev": "I",
+ "values": {"HIGH": 0.56, "LOW": 0.22, "NONE": 0},
+ },
+ "availabilityImpact": {
+ "abbrev": "A",
+ "values": {"HIGH": 0.56, "LOW": 0.22, "NONE": 0},
+ },
+ }
+ severities = {
+ "NONE": 0.0,
+ "LOW": 3.9,
+ "MEDIUM": 6.9,
+ "HIGH": 8.9,
+ "CRITICAL": 10.0,
+ }
+ js = None
+ # coerce cvss into a string
+ cvss = str(cvss)
+ for c in cvss.split("/"):
+ elements = c.split(":")
+ if len(elements) != 2:
+ raise ValueError("Invalid CVSS element '%s'" % c)
+ valid = False
+ metric = elements[0]
+ value = elements[1]
+ if metric == "CVSS":
+ if value == "3.0" or value == "3.1":
+ js = {"baseMetricV3": {"cvssV3": {"version": value}}}
+ valid = True
+ else:
+ raise ValueError(
+ "Unable to process CVSS version '%s' (we only support 3.x)"
+ % value
+ )
+ else:
+ for m in metrics.keys():
+ if metrics[m]["abbrev"] == metric:
+ for val in metrics[m]["values"]:
+ if val[0:1] == value:
+ js["baseMetricV3"]["cvssV3"][m] = val
+ valid = True
+ if not valid:
+ raise ValueError("Invalid CVSS elements '%s:%s'" % (metric, value))
+ for m in metrics.keys():
+ if m not in js["baseMetricV3"]["cvssV3"]:
+ raise ValueError("Missing required CVSS base element %s" % m)
+ # add vectorString
+ js["baseMetricV3"]["cvssV3"]["vectorString"] = cvss
+
+ # now calculate CVSS scores
+ iss = 1 - (
+ (
+ 1
+ - metrics["confidentialityImpact"]["values"][
+ js["baseMetricV3"]["cvssV3"]["confidentialityImpact"]
+ ]
+ )
+ * (
+ 1
+ - metrics["integrityImpact"]["values"][
+ js["baseMetricV3"]["cvssV3"]["integrityImpact"]
+ ]
+ )
+ * (
+ 1
+ - metrics["availabilityImpact"]["values"][
+ js["baseMetricV3"]["cvssV3"]["availabilityImpact"]
+ ]
+ )
+ )
+ if js["baseMetricV3"]["cvssV3"]["scope"] == "UNCHANGED":
+ impact = 6.42 * iss
+ else:
+ impact = 7.52 * (iss - 0.029) - 3.25 * pow(iss - 0.02, 15)
+ attackVector = metrics["attackVector"]["values"][
+ js["baseMetricV3"]["cvssV3"]["attackVector"]
+ ]
+ attackComplexity = metrics["attackComplexity"]["values"][
+ js["baseMetricV3"]["cvssV3"]["attackComplexity"]
+ ]
+ privilegesRequired = metrics["privilegesRequired"]["values"][
+ js["baseMetricV3"]["cvssV3"]["privilegesRequired"]
+ ]
+ # privilegesRequires could be a list if is LOW or HIGH (and then the
+ # value depends on whether the scope is unchanged or not)
+ if isinstance(privilegesRequired, list):
+ if js["baseMetricV3"]["cvssV3"]["scope"] == "UNCHANGED":
+ privilegesRequired = privilegesRequired[0]
+ else:
+ privilegesRequired = privilegesRequired[1]
+ userInteraction = metrics["userInteraction"]["values"][
+ js["baseMetricV3"]["cvssV3"]["userInteraction"]
+ ]
+ exploitability = (
+ 8.22
+ * attackVector
+ * attackComplexity
+ * privilegesRequired
+ * userInteraction
+ )
+ if impact <= 0:
+ base_score = 0
+ elif js["baseMetricV3"]["cvssV3"]["scope"] == "UNCHANGED":
+ # use ceil and * 10 / 10 to get rounded up to nearest 10th decimal
+ # (where rounded-up is say 0.01 -> 0.1)
+ base_score = math.ceil(min(impact + exploitability, 10) * 10) / 10
+ else:
+ base_score = (
+ math.ceil(min(1.08 * (impact + exploitability), 10) * 10) / 10
+ )
+ js["baseMetricV3"]["cvssV3"]["baseScore"] = base_score
+ for severity in severities.keys():
+ if base_score <= severities[severity]:
+ js["baseMetricV3"]["cvssV3"]["baseSeverity"] = severity
+ break
+ # these use normal rounding to 1 decimal place
+ js["baseMetricV3"]["exploitabilityScore"] = round(exploitability * 10) / 10
+ js["baseMetricV3"]["impactScore"] = round(impact * 10) / 10
+ return js
diff --git a/scripts/uct-import.py b/scripts/uct-import.py
new file mode 100755
index 0000000..867aada
--- /dev/null
+++ b/scripts/uct-import.py
@@ -0,0 +1,33 @@
+#!/usr/bin/python3 -S
+#
+# Copyright 2022 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+import _pythonpath # noqa: F401
+
+import logging
+from pathlib import Path
+
+from lp.bugs.scripts.uctimport import UCTImporter
+from lp.services.scripts.base import LaunchpadScript
+
+
+class UCTImportScript(LaunchpadScript):
+
+ description = (
+ "Import bugs into Launchpad from CVE entries in ubuntu-cve-tracker."
+ )
+ loglevel = logging.INFO
+
+ def main(self):
+ if len(self.args) != 1:
+ self.parser.error('Please specify a CVE file to import')
+
+ importer = UCTImporter()
+
+ cve_path = Path(self.args[0])
+ importer.import_cve_from_file(cve_path)
+
+
+if __name__ == '__main__':
+ script = UCTImportScript('lp.services.scripts.uctimport')
+ script.run()
Follow ups