summaryrefslogtreecommitdiffstats
path: root/bin
diff options
context:
space:
mode:
authorNeil Williams <codehelp@debian.org>2022-02-03 11:03:29 +0000
committerNeil Williams <codehelp@debian.org>2022-02-03 11:03:29 +0000
commit38fc7543c6e8fc4a2d15540fd63b837218361e8f (patch)
tree8f4674852856f5758e3409328d8b42452a761fbb /bin
parentce5b21c8e3e46da28d2a42a852b465fbeec4d056 (diff)
parent7bbb17a2475a187baea9a437a987d4ea38a7d5f7 (diff)
Merge branch 'grabcvefix' into 'master'
grab-cve-in-fix #1001451 See merge request security-tracker-team/security-tracker!100
Diffstat (limited to 'bin')
-rwxr-xr-xbin/grab-cve-in-fix414
-rwxr-xr-xbin/merge-cve-files49
-rwxr-xr-xbin/update-vuln369
3 files changed, 831 insertions, 1 deletions
diff --git a/bin/grab-cve-in-fix b/bin/grab-cve-in-fix
new file mode 100755
index 0000000000..98ea9cd476
--- /dev/null
+++ b/bin/grab-cve-in-fix
@@ -0,0 +1,414 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""
+grab-cve-in-fix - #1001451
+
+- queries the latest version of source:<package_name> in unstable
+- extracts all mentioned CVE IDs from the change
+- creates a correctly formatted CVE snippet with the recorded fixes that
+ can be reviewed and merged into the main data/CVE/list
+"""
+
+#
+# Copyright 2021-2022 Neil Williams <codehelp@debian.org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1301, USA.
+#
+
+# pylint: disable=too-few-public-methods,line-too-long,too-many-instance-attributes,too-many-branches
+
+# Examples:
+# --archive https://lists.debian.org/debian-devel-changes/2021/12/msg01280.html
+# --tracker https://tracker.debian.org/news/1285227/accepted-freerdp2-241dfsg1-1-source-into-unstable/
+
+import argparse
+import os
+import glob
+import logging
+import re
+import sys
+import requests
+
+# depends on python3-apt
+import apt_pkg
+
+# depends on python3-debian
+from debian.deb822 import Changes
+
+import setup_paths # noqa # pylint: disable=unused-import
+from sectracker.parsers import (
+ sourcepackages,
+ FlagAnnotation,
+ StringAnnotation,
+ PackageAnnotation,
+ Bug,
+ cvelist,
+ writecvelist,
+)
+
+
+class ParseChanges:
+ """Base for parsing DEB822 content into a CVE list"""
+
+ def __init__(self, url):
+ self.url = url
+ self.source_package = None
+ self.cves = []
+ self.bugs = {}
+ self.parsed = []
+ self.unstable_version = None
+ self.tracker_base = "https://security-tracker.debian.org/tracker/source-package/"
+ self.logger = logging.getLogger("grab-cve-in-fix")
+ self.logger.setLevel(logging.DEBUG)
+ # console logging
+ ch_log = logging.StreamHandler()
+ ch_log.setLevel(logging.DEBUG)
+ formatter = logging.Formatter("%(name)s - %(levelname)s - %(message)s")
+ ch_log.setFormatter(formatter)
+ self.logger.addHandler(ch_log)
+ apt_pkg.init_system() # pylint: disable=c-extension-no-member
+
+ def _read_cvelist(self):
+ os.chdir(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
+ data, _ = cvelist("data/CVE/list") # pylint: disable=no-value-for-parameter
+ for cve in self.cves:
+ for bug in data:
+ if bug.header.name == cve:
+ self.bugs[cve] = bug
+ package_checks = {}
+ cve_notes = {}
+ for cve, bug in self.bugs.items():
+ self.logger.info("%s: %s", bug.header.name, bug.header.description)
+ for line in bug.annotations:
+ if isinstance(line, PackageAnnotation):
+ package_checks.setdefault(cve, [])
+ package_checks[cve].append(line.package)
+ if isinstance(line, StringAnnotation) or isinstance(line, FlagAnnotation):
+ cve_notes.setdefault(cve, [])
+ cve_notes[cve].append(line.type)
+ if cve not in package_checks:
+ self.logger.error("CVE %s is not attributed to a Debian package: %s", cve, cve_notes.get(cve, ""))
+ elif self.source_package not in package_checks[cve]:
+ self.logger.warning(
+ "%s is listed against %s, not %s", cve, list(set(package_checks[cve])), self.source_package
+ )
+ if not self.cves:
+ self.logger.warning(
+ "no CVEs found in the changes output " "for %s %s",
+ self.source_package,
+ self.unstable_version,
+ )
+
+ def parse(self):
+ """Parser-specific code to pick out the DEB822 content"""
+ raise NotImplementedError
+
+ def _read_changes(self):
+ if not self.parsed:
+ return
+ rel = Changes(self.parsed)
+ changes = rel.get("Changes")
+ if not changes:
+ self.logger.error("%s %s\n", rel, self.parsed)
+ return
+ self.source_package = rel.get("Source")
+ self.unstable_version = rel.get("Version")
+ match = None
+ for log in changes.splitlines():
+ match = re.findall(r"(CVE-[0-9]{4}-[0-9]+)", log)
+ if match:
+ self.cves += match
+
+ def add_unstable_version(self):
+ """
+ Writes out a CVE file snippet with the filename:
+ ./<src_package>.list
+ Fails if the file already exists.
+
+ Prints error if any of the listed CVEs are not found
+ for the specified source_package.
+
+ If a new version is set, the fixed version for the CVE will
+ be updated to that version. Uses python3-apt to only update
+ if the version is declared, by apt, to be newer.
+
+ A typo in the CVE ID *may* cause a CVE to be declared as
+ fixed in the wrong source package. This is complicated by
+ the need to allow for embedded copies and removed packages.
+ """
+ modified = []
+ cve_file = f"{self.source_package}.list"
+ cves = sorted(set(self.cves))
+ cves.reverse()
+ for cve in cves:
+ if cve not in self.bugs:
+ self.logger.error(
+ "%s was not found in the Security Tracker CVE list! Check %s%s - "
+ "possible typo in the package changelog? Check the list of CVEs "
+ "in the security tracker and use this script again, in offline mode."
+ " ./bin grab-cve-in-fix --src %s --cves corrected-cve",
+ cve,
+ self.tracker_base,
+ self.source_package,
+ self.source_package,
+ )
+ continue
+ for line in self.bugs[cve].annotations:
+ if not isinstance(line, PackageAnnotation):
+ continue # skip notes etc.
+ if line.release: # only update unstable
+ continue
+ if line.package != self.source_package:
+ self.logger.info(
+ "Ignoring %s annotation for %s",
+ cve,
+ line.package,
+ )
+ continue # allow for removed, old or alternate pkg names
+ if line.version:
+ vcompare = apt_pkg.version_compare( # pylint: disable=c-extension-no-member
+ line.version, self.unstable_version
+ )
+ if vcompare < 0:
+ self.logger.info("Updating %s to %s", line.version, self.unstable_version)
+ mod_line = line._replace(version=self.unstable_version)
+ index = self.bugs[cve].annotations.index(line)
+ bug_list = list(self.bugs[cve].annotations)
+ bug_list[index] = mod_line
+ mod_bug = Bug(self.bugs[cve].file, self.bugs[cve].header, tuple(bug_list))
+ modified.append(mod_bug)
+ elif vcompare > 0:
+ self.logger.error(
+ "%s is listed as fixed in %s which is newer than %s",
+ cve,
+ line.version,
+ self.unstable_version,
+ )
+ else:
+ self.logger.info(
+ "%s already has annotation for - %s %s",
+ cve,
+ self.source_package,
+ line.version,
+ )
+ else:
+ mod_line = line._replace(version=self.unstable_version)
+ index = self.bugs[cve].annotations.index(line)
+ bug_list = list(self.bugs[cve].annotations)
+ bug_list[index] = mod_line
+ mod_bug = Bug(self.bugs[cve].file, self.bugs[cve].header, tuple(bug_list))
+ modified.append(mod_bug)
+ if not modified:
+ return 0
+ if os.path.exists(cve_file):
+ self.logger.critical("%s already exists", cve_file)
+ return -1
+ for cve in modified:
+ self.logger.info(
+ "Writing to ./%s with update for %s - %s %s",
+ cve_file,
+ cve.header.name,
+ self.source_package,
+ self.unstable_version,
+ )
+ with open(cve_file, "a") as snippet:
+ writecvelist(modified, snippet)
+ return 0
+
+
+class ParseSources(ParseChanges):
+ """Read latest version in unstable from updated local Sources files"""
+
+ def parse(self):
+ """
+ Support to pick up unstable_version from the local packages cache.
+
+ Also supports explicitly setting the version for times when
+ the package has received an unrelated update in unstable.
+ """
+ if self.unstable_version:
+ self.logger.info("Using forced version: %s", self.unstable_version)
+ self._read_cvelist()
+ self.add_unstable_version()
+ return 0
+
+ self.logger.info("Retrieving data from local packages data...")
+ if not self.source_package or not self.cves:
+ self.logger.error("for offline use, specify both --src and --cves options")
+ return 1
+ # self.url contains pkgdir which needs to contain Sources files
+ os.chdir(self.url)
+ for srcs_file in glob.glob("sid*Sources"):
+ srcs = sourcepackages(srcs_file) # pylint: disable=no-value-for-parameter
+ if srcs.get(self.source_package):
+ self.unstable_version = srcs[self.source_package].version
+ # src package is only listed in one Sources file
+ break
+ self._read_cvelist()
+ self.add_unstable_version()
+ return 0
+
+
+class ParseTrackerAccepted(ParseChanges):
+ """
+ Download and parse Accepted tracker NEWS
+
+ e.g. https://tracker.debian.org/news/1285227/accepted-freerdp2-241dfsg1-1-source-into-unstable/
+ """
+
+ MARKER = '<div class="email-news-body">'
+
+ def parse(self):
+ self.logger.info("Retrieving data from distro-tracker...")
+ req = requests.get(self.url)
+ if req.status_code != requests.codes.ok: # pylint: disable=no-member
+ return 2
+ self.parsed = []
+ for line in req.text.splitlines():
+ if not self.parsed and not line.startswith(self.MARKER):
+ continue
+ if self.MARKER in line:
+ line = line.replace(self.MARKER, "")
+ if "<pre>" in line:
+ line = line.replace("<pre>", "")
+ if line.startswith("\t"):
+ line = line.replace("\t", "")
+ self.parsed.append(line)
+ if line.startswith("</pre>"):
+ break
+ self._read_changes()
+ self._read_cvelist()
+ self.add_unstable_version()
+ return 0
+
+
+class ParseDDChanges(ParseChanges):
+ """
+ Download and parse an email in the debian-devel-changes archive
+
+ e.g. https://lists.debian.org/debian-devel-changes/2021/12/msg01280.html
+ """
+
+ def parse(self):
+ self.logger.info("Retrieving data from debian-devel-changes archive...")
+ req = requests.get(self.url)
+ if req.status_code != requests.codes.ok: # pylint: disable=no-member
+ return 3
+ for line in req.text.splitlines():
+ if not self.parsed and not line.startswith("<pre>"):
+ continue
+ pars = line.replace("<pre>", "")
+ self.parsed.append(pars)
+ if line.startswith("</pre>"):
+ break
+ self._read_changes()
+ self._read_cvelist()
+ self.add_unstable_version()
+ return 0
+
+
+class ParseDDStdIn(ParseChanges):
+ """
+ Parse an email originating from debian-devel-changes passed
+ on STDIN
+ """
+
+ MARKER = "-----BEGIN PGP SIGNED MESSAGE-----"
+
+ def parse(self):
+ self.logger.info("Retrieving data STDIN ...")
+ content = sys.stdin.read()
+ for line in content.splitlines():
+ if not self.parsed and not line.startswith(self.MARKER):
+ continue
+ self.parsed.append(line)
+ if not self.parsed:
+ self.logger.warning("Unable to find PGP marker - unsigned content?")
+ return 1
+ self._read_changes()
+ self._read_cvelist()
+ self.add_unstable_version()
+ return 0
+
+
+def main():
+ """
+ 1: Provide an option to parse the email from debian-devel-changes
+ 2: Provide an option to lookup the information using tracker.d.o
+ 3: Provide an option to read an email from debian-devel-changes on stdin
+ 4: Fallback to lookup the information in the local apt-cache
+ data populated by 'make update-packages'
+ data/packages/sid__main_Sources
+ data/packages/sid__contrib_Sources
+ data/packages/sid__non-free_Sources
+ """
+ parser = argparse.ArgumentParser(
+ description="Grab CVE data from a package upload for manual review",
+ usage="%(prog)s [-h] [[--input] | [--archive URL] | [--tracker TRACKER]] | "
+ "[[--src SRC] & [--cves [CVES ...]]]",
+ epilog="Data is written to a new <source_package>.list " "file which can be used with './bin/merge-cve-files'",
+ )
+ online = parser.add_argument_group(
+ "Online - query one of distro-tracker or " "debian-devel-changes mail archive or debian-devel-changes email"
+ )
+ online.add_argument(
+ "--input",
+ action="store_true",
+ help="Read from a debian-devel-changes email on STDIN",
+ )
+ online.add_argument(
+ "--archive",
+ help="URL of debian-devel-changes " "announcement in the list archive",
+ )
+ online.add_argument(
+ "--tracker",
+ help="URL of tracker.debian.org 'Accepted NEWS' page for unstable",
+ )
+ offline = parser.add_argument_group(
+ "Offline - run 'make update-packages' first & specify source package and CVE list"
+ )
+ offline.add_argument("--src", help="Source package name to look up version in local packages files")
+ offline.add_argument(
+ "--force-version",
+ help="Explicitly set the fixed version, in case sid has moved ahead.",
+ )
+ offline.add_argument("--cves", nargs="*", help="CVE ID tag with version from local packages files")
+ args = parser.parse_args()
+ if args.input:
+ data = ParseDDStdIn(args.input)
+ return data.parse()
+ if args.archive:
+ data = ParseDDChanges(args.archive)
+ return data.parse()
+ if args.tracker:
+ data = ParseTrackerAccepted(args.tracker)
+ return data.parse()
+ pkg_dir = os.path.join(".", "data", "packages")
+ if os.path.exists(pkg_dir):
+ data = ParseSources(pkg_dir)
+ data.source_package = args.src
+ data.cves = args.cves
+ if args.force_version:
+ data.unstable_version = args.force_version
+ return data.parse()
+ logger = logging.getLogger("grab-cve-in-fix")
+ logger.error("Unable to parse package data!")
+ return -1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/bin/merge-cve-files b/bin/merge-cve-files
index a26e38ab68..55f487e2d5 100755
--- a/bin/merge-cve-files
+++ b/bin/merge-cve-files
@@ -4,13 +4,53 @@
# the main one.
#
# Copyright © 2020 Emilio Pozuelo Monfort <pochu@debian.org>
+# Copyright (c) 2021-2022 Neil Williams <codehelp@debian.org>
import os.path
import sys
import setup_paths # noqa
from debian_support import internRelease
-from sectracker.parsers import cvelist, writecvelist, PackageAnnotation, FlagAnnotation, XrefAnnotation
+from sectracker.parsers import (
+ Bug,
+ cvelist,
+ writecvelist,
+ PackageAnnotation,
+ FlagAnnotation,
+ StringAnnotation,
+ XrefAnnotation
+)
+
+def merge_notes(bug, notes):
+ """
+ Special support for StringAnnotations.
+
+ notes is a dict containing a list of string annotations for
+ each CVE in the file being merged. Pick out the string annotations
+ for this bug, ignore if already exist, append if new.
+ """
+ new_notes = []
+ cve = bug.header.name
+ merge_list = notes.get(cve) # list of notes to merge
+ if not merge_list:
+ # nothing to merge
+ return bug
+ tagged_notes = [note.description for note in merge_list]
+ bug_notes = [ann.description for ann in bug.annotations if isinstance(ann, StringAnnotation)]
+ # get the list items in tagged_notes which are not in bug_notes
+ new_strings = list(set(tagged_notes) - set(bug_notes))
+ if not new_strings:
+ return bug
+ for new_ann in merge_list:
+ if new_ann.description in new_strings:
+ new_notes.append(new_ann)
+ bug_list = list(bug.annotations)
+ bug_list.extend(new_notes)
+ mod_bug = Bug(
+ bug.file, bug.header, tuple(bug_list)
+ )
+ return mod_bug
+
def merge_annotations(annotations, new_annotation):
if not isinstance(new_annotation, PackageAnnotation):
@@ -86,11 +126,18 @@ extra_data = parse_list(extra_list)
for extra_bug in extra_data:
bug = next(bug for bug in data if bug.header.name == extra_bug.header.name)
+ notes = {}
new_annotations = bug.annotations
for extra_annotation in extra_bug.annotations:
+ if isinstance(extra_annotation, StringAnnotation):
+ cve = f"{extra_bug.header.name}"
+ note_tag = notes.setdefault(cve, [])
+ note_tag.append(extra_annotation)
+ continue
new_annotations = merge_annotations(new_annotations, extra_annotation)
bug = bug._replace(annotations=new_annotations)
+ bug = merge_notes(bug, notes)
data = [bug if bug.header.name == old_bug.header.name else old_bug for old_bug in data]
with open(main_list, 'w') as f:
diff --git a/bin/update-vuln b/bin/update-vuln
new file mode 100755
index 0000000000..f6f93f2e46
--- /dev/null
+++ b/bin/update-vuln
@@ -0,0 +1,369 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+ update-vuln - #1001453
+
+ - mark a given released suite (stable/oldstable/LTS) as <not-affected>
+ for a specific CVE ID
+ - add a bug number to an existing CVE entry
+ - add a NOTE: entry to an existing CVE
+
+Only make one change to one CVE at a time. Review and merge that
+change and delete the merged file before updating the same CVE.
+
+The workflow would be:
+./bin/update-vuln --cve CVE-YYYY-NNNNN ...
+# on exit zero:
+./bin/merge-cve-files ./CVE-YYYY-NNNNN.list
+# review change to data/CVE/list
+git diff data/CVE/list
+rm ./CVE-YYYY-NNNNN.list
+# .. repeat
+git add data/CVE/list
+git commit
+
+"""
+# Copyright 2021-2022 Neil Williams <codehelp@debian.org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1301, USA.
+
+import os
+import argparse
+import bisect
+import logging
+import sys
+
+import setup_paths # noqa # pylint: disable=unused-import
+from sectracker.parsers import (
+ PackageAnnotation,
+ PackageBugAnnotation,
+ StringAnnotation,
+ Bug,
+ cvelist,
+ writecvelist,
+)
+
+# pylint: disable=line-too-long
+
+
+class ParseUpdates:
+ """
+ Update a CVE with requested changes and produce a file for
+ manual review and use with merge-cve-files.
+ """
+
+ def __init__(self):
+ self.cves = []
+ self.bugs = {}
+ self.marker = "aaaaaaaaaaaaa" # replacement for NoneType to always sort first
+ self.logger = logging.getLogger("update-vuln")
+ self.logger.setLevel(logging.DEBUG)
+ # console logging
+ ch_log = logging.StreamHandler()
+ ch_log.setLevel(logging.DEBUG)
+ formatter = logging.Formatter("%(name)s - %(levelname)s - %(message)s")
+ ch_log.setFormatter(formatter)
+ self.logger.addHandler(ch_log)
+
+ def _read_cvelist(self):
+ """Build a list of Bug items for the CVE from data/CVE/list"""
+ os.chdir(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
+ data, _ = cvelist("data/CVE/list") # pylint: disable=no-value-for-parameter
+ for cve in self.cves:
+ for bug in data:
+ if bug.header.name == cve:
+ self.bugs[cve] = bug
+
+ def _add_annotation_to_cve(self, cve, annotation):
+ """
+ Adds an annotation to a CVE entry.
+
+ StringAnnotation - appended to the end
+ PackageAnnotation - inserted in alphabetical order by release
+
+ Accounts for PackageAnnotation.release == None for unstable.
+ """
+ if isinstance(annotation, PackageAnnotation):
+ store = {ann.release: ann for ann in self.bugs[cve].annotations if isinstance(ann, PackageAnnotation)}
+ store[annotation.release] = annotation
+ # this is needed despite python3 >= 3.7 having ordered dicts
+ # because using the dict.keys() would need a copy of that list anyway.
+ existing = [ann.release for ann in self.bugs[cve].annotations if isinstance(ann, PackageAnnotation)]
+ if None in existing:
+ # release == None for unstable
+ index = existing.index(None)
+ existing[index] = self.marker
+ insertion = annotation.release if annotation.release else self.marker
+
+ # bisect cannot work with NoneType
+ bisect.insort(existing, insertion)
+
+ if self.marker in existing:
+ index = existing.index(self.marker)
+ existing[index] = None
+
+ bug_list = []
+ for item in existing:
+ bug_list.append(store[item])
+
+ elif isinstance(annotation, StringAnnotation):
+ bug_list = list(self.bugs[cve].annotations)
+ bug_list.append(annotation)
+ else:
+ raise ValueError(f"Unsupported annotation type: {type(annotation)}")
+
+ return Bug(self.bugs[cve].file, self.bugs[cve].header, tuple(bug_list))
+
+ def _replace_annotation_on_line(self, cve, line, mod_line):
+ index = self.bugs[cve].annotations.index(line)
+ bug_list = list(self.bugs[cve].annotations)
+ bug_list[index] = mod_line
+ return Bug(self.bugs[cve].file, self.bugs[cve].header, tuple(bug_list))
+
+ def write_modified(self, modified, cve_file):
+ """
+ Write out a CVE snippet for review and merge
+
+ Fails if the file already exists.
+ """
+ if not modified:
+ return 0
+ if not isinstance(modified, list):
+ return 0
+ if os.path.exists(cve_file):
+ self.logger.critical(
+ "%s already exists - merge the update and remove the file first.",
+ cve_file,
+ )
+ return -1
+ for cve in modified:
+ self.logger.info("Writing to ./%s with update for %s", cve_file, cve.header.name)
+ with open(cve_file, "a") as snippet:
+ writecvelist(modified, snippet)
+ return 0
+
+ def mark_not_affected(self, suite, src, description):
+ """
+ Writes out a CVE file snippet with the filename:
+ ./<cve>.list
+ Fails if the file already exists.
+ """
+ release = suite
+ if suite in ("unstable", "sid"):
+ # special handling for unstable
+ suite = None
+ release = "unstable"
+ modified = []
+ cve = self.cves[0]
+ cve_file = f"{cve}.list"
+ existing = [line.release for line in self.bugs[cve].annotations if isinstance(line, PackageAnnotation)]
+ if suite not in existing:
+ # line type release package kind version description flags
+ line = PackageAnnotation(0, "package", suite, src, "not-affected", None, description, [])
+ mod_bug = self._add_annotation_to_cve(cve, line)
+ modified.append(mod_bug)
+ for line in self.bugs[cve].annotations:
+ if not isinstance(line, PackageAnnotation):
+ continue # skip notes etc.
+ if line.release != suite:
+ continue
+ if line.package != src:
+ continue
+ # need to define the allowed changes
+ # if fixed, version would need to be undone too.
+ if line.kind == "not-affected":
+ self.logger.info("Nothing to do for %s in %s.", cve, suite)
+ return
+ mod_line = line._replace(kind="not-affected")
+ self.logger.info("Modified %s for %s in %s to <not-affected>", cve, src, release)
+ if mod_line.version:
+ self.logger.info("Removing version %s", line.version)
+ ver_line = mod_line
+ mod_line = ver_line._replace(version=None)
+ if description:
+ self.logger.info("Replacing description %s", line.description)
+ desc_line = mod_line
+ mod_line = desc_line._replace(description=description)
+ elif mod_line.description:
+ self.logger.info("Removing description %s", line.description)
+ desc_line = mod_line
+ mod_line = desc_line._replace(description=None)
+ # removing a bug annotation is not covered, yet.
+ mod_bug = self._replace_annotation_on_line(cve, line, mod_line)
+ modified.append(mod_bug)
+ self.write_modified(modified, cve_file)
+
+ def add_note(self, note):
+ """
+ Writes out a CVE file snippet with the filename:
+ ./<cve>.list
+ Fails if the file already exists.
+ """
+ # use _add_annotation_to_cve to add the note
+ modified = []
+ cve = self.cves[0]
+ cve_file = f"{cve}.list"
+ existing = [note.description for note in self.bugs[cve].annotations if isinstance(note, StringAnnotation)]
+ if note in existing:
+ self.logger.info("Note already exists, ignoring")
+ return
+ new_note = StringAnnotation(line=0, type="NOTE", description=note)
+ mod_bug = self._add_annotation_to_cve(cve, new_note)
+ modified.append(mod_bug)
+ self.write_modified(modified, cve_file)
+
+ def add_bug_number(self, bug, itp=False): # pylint: disable=too-many-locals
+ """
+ Writes out a CVE file snippet with the filename:
+ ./<cve>.list
+ Fails if the file already exists.
+ """
+ # bugs only apply to unstable (or itp)
+ modified = []
+ cve = self.cves[0]
+ cve_file = f"{cve}.list"
+ existing = [
+ pkg.flags
+ for pkg in self.bugs[cve].annotations
+ if isinstance(pkg, PackageAnnotation)
+ if not pkg.release and pkg.kind != "removed"
+ ]
+ bugs = [bug for sublist in existing for bug in sublist]
+ if bugs:
+ self.logger.warning("%s already has a bug annotation for unstable: %s", cve, bugs[0].bug)
+ return -1
+ pkgs = [
+ pkg
+ for pkg in self.bugs[cve].annotations
+ if isinstance(pkg, PackageAnnotation)
+ if not pkg.release and pkg.kind != "removed"
+ ]
+ if itp:
+ # no useful entry will exist in pkgs
+ new_flags = [PackageBugAnnotation(bug)]
+ new_pkg = PackageAnnotation(
+ 0,
+ "package",
+ None,
+ itp,
+ "itp",
+ None,
+ None,
+ new_flags,
+ )
+ others = []
+ else:
+ if not pkgs:
+ self.logger.error("%s does not have a package annotation.", cve)
+ return -1
+ old_pkg = pkgs[0]
+ if itp and old_pkg.kind == "fixed":
+ self.logger.error("%s is already marked as <fixed> but --itp flag was set.", cve)
+ return -3
+ new_flags = [PackageBugAnnotation(bug)]
+ new_pkg = PackageAnnotation(
+ old_pkg.line,
+ old_pkg.type,
+ old_pkg.release,
+ old_pkg.package,
+ old_pkg.kind,
+ old_pkg.version,
+ old_pkg.description,
+ new_flags,
+ )
+ bug_list = list(self.bugs[cve].annotations)
+ others = [pkg for pkg in bug_list if pkg.line != old_pkg.line]
+ bug_list = list(self.bugs[cve].annotations)
+ # may need to retain the original order.
+ new_list = [new_pkg] + others
+ mod_bug = Bug(self.bugs[cve].file, self.bugs[cve].header, tuple(new_list))
+ modified.append(mod_bug)
+ self.write_modified(modified, cve_file)
+ return 0
+
+ def load_cve(self, cve):
+ """Load all data for the specified CVE"""
+ self.logger.info("Loading data for %s...", cve)
+ self.cves.append(cve)
+ self._read_cvelist()
+
+
+def main():
+ """
+ This script does NOT reparse the output file - create, review and
+ merge ONE update at a time.
+ (For some operations, check-new-issues may be more suitable).
+
+ For example, --bug 100 --itp intended_pkg_name
+ then, merge-cve-list, then:
+ --note "URL:"
+ """
+ parser = argparse.ArgumentParser(
+ description="Make a single update to specified CVE data as not-affected, add bug number or add a note",
+ usage="%(prog)s [-h] --cve CVE [--src SRC --suite SUITE "
+ "[--description DESCRIPTION]] | [[--number NUMBER] [--itp SRC]] | [--note NOTE]",
+ epilog="Data is written to a new <cve_number>.list "
+ "file which can be used with './bin/merge-cve-files'. "
+ "Make sure the output file is merged and removed before "
+ "updating the same CVE again.",
+ )
+
+ required = parser.add_argument_group("Required arguments")
+ required.add_argument("--cve", required=True, help="The CVE ID to update")
+
+ affected = parser.add_argument_group(
+ "Marking a CVE as not-affected - must use --src and --suite "
+ "Optionally add a description or omit to remove the current description"
+ )
+ # needs to specify the src_package as well as suite to cope with removed etc.
+ affected.add_argument("--src", help="Source package name in SUITE")
+ affected.add_argument("--suite", default="unstable", help="Mark the CVE as <not-affected> in SUITE")
+ affected.add_argument(
+ "--description",
+ help="Optional description of why the SRC is unaffected in SUITE",
+ )
+
+ buggy = parser.add_argument_group("Add a bug number to the CVE")
+ buggy.add_argument("--number", help="Debian BTS bug number")
+ buggy.add_argument(
+ "--itp",
+ metavar="SRC",
+ help="Mark as an ITP bug for the specified source package name",
+ )
+
+ notes = parser.add_argument_group("Add a NOTE: entry to the CVE")
+ notes.add_argument("--note", help="Content of the NOTE: entry to add to the CVE")
+
+ args = parser.parse_args()
+ parser = ParseUpdates()
+ parser.load_cve(args.cve)
+
+ logger = logging.getLogger("update-vuln")
+ if not parser.bugs:
+ logger.critical("Unable to parse CVE ID %s", args.cve)
+ return -1
+ if args.src and args.suite:
+ parser.mark_not_affected(args.suite, args.src, args.description)
+ if args.note:
+ parser.add_note(args.note)
+ if args.number:
+ # to set itp properly, the source package name also needs to be set.
+ parser.add_bug_number(args.number, args.itp)
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())

© 2014-2024 Faster IT GmbH | imprint | privacy policy