diff options
author | Neil Williams <codehelp@debian.org> | 2022-02-03 11:03:29 +0000 |
---|---|---|
committer | Neil Williams <codehelp@debian.org> | 2022-02-03 11:03:29 +0000 |
commit | 38fc7543c6e8fc4a2d15540fd63b837218361e8f (patch) | |
tree | 8f4674852856f5758e3409328d8b42452a761fbb /bin | |
parent | ce5b21c8e3e46da28d2a42a852b465fbeec4d056 (diff) | |
parent | 7bbb17a2475a187baea9a437a987d4ea38a7d5f7 (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-x | bin/grab-cve-in-fix | 414 | ||||
-rwxr-xr-x | bin/merge-cve-files | 49 | ||||
-rwxr-xr-x | bin/update-vuln | 369 |
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()) |