[Git][security-tracker-team/security-tracker][master] 9 commits: grab-cve-in-fix #1001451

Neil Williams (@codehelp) codehelp at debian.org
Thu Feb 3 11:03:37 GMT 2022



Neil Williams pushed to branch master at Debian Security Tracker / security-tracker


Commits:
a5504038 by Neil Williams at 2022-01-27T09:08:15+00:00
grab-cve-in-fix #1001451

Add a tool to ease processing of new uploads which fix CVEs

- - - - -
2d0ec6a3 by Neil Williams at 2022-01-27T09:08:15+00:00
Add initial update-vuln script

- - - - -
6782f362 by Neil Williams at 2022-01-27T09:08:15+00:00
Add support for --input accepting email text on STDIN

- - - - -
be8bed3a by Neil Williams at 2022-01-27T09:08:15+00:00
Add support for merging NOTE: StringAnnotations

- - - - -
7e554b13 by Neil Williams at 2022-01-27T09:08:15+00:00
Add remaining support and switch to using logging

Add support to add a bug number.
Add warnings in --help that each update must be merged before
the same CVE can be updated again.

- - - - -
099786ad by Neil Williams at 2022-01-27T09:08:15+00:00
Update grab-cve-in-fix for known examples

Support catching errors in the d.changelog
Add support for forcing a specific version

Fix typo in new support in bin/merge-cve-files

Update support in update-vuln to insert new
PackageAnnotations in specific order.

- - - - -
5233129c by Neil Williams at 2022-01-27T09:08:15+00:00
Pylint updates

Extend linelength to 120 in black.

- - - - -
7bbb17a2 by Neil Williams at 2022-01-27T09:08:16+00:00
Improve error handling in grab-cve-in-fix

Catch and report on possible typos in changes entries to better support
maintainers pre-checking the d.changelog entries before upload - as long
as the .changes file is signed.

- - - - -
38fc7543 by Neil Williams at 2022-02-03T11:03:29+00:00
Merge branch 'grabcvefix' into 'master'

grab-cve-in-fix #1001451

See merge request security-tracker-team/security-tracker!100
- - - - -


3 changed files:

- + bin/grab-cve-in-fix
- bin/merge-cve-files
- + bin/update-vuln


Changes:

=====================================
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 at 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())


=====================================
bin/merge-cve-files
=====================================
@@ -4,13 +4,53 @@
 # the main one.
 #
 # Copyright © 2020 Emilio Pozuelo Monfort <pochu at debian.org>
+# Copyright (c) 2021-2022 Neil Williams <codehelp at 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:


=====================================
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 at 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())



View it on GitLab: https://salsa.debian.org/security-tracker-team/security-tracker/-/compare/ce5b21c8e3e46da28d2a42a852b465fbeec4d056...38fc7543c6e8fc4a2d15540fd63b837218361e8f

-- 
View it on GitLab: https://salsa.debian.org/security-tracker-team/security-tracker/-/compare/ce5b21c8e3e46da28d2a42a852b465fbeec4d056...38fc7543c6e8fc4a2d15540fd63b837218361e8f
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-security-tracker-commits/attachments/20220203/ff72bc6f/attachment-0001.htm>


More information about the debian-security-tracker-commits mailing list