#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ grab-cve-in-fix - #1001451 - queries the latest version of source: 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 Neil Williams # # 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 # 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-debian from debian.deb822 import Changes import setup_paths # noqa # pylint: disable=unused-import from sectracker.parsers import ( sourcepackages, 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.logger = logging.getLogger("grab-cve-in-fix") self.logger.setLevel(logging.DEBUG) # console logging ch = logging.StreamHandler() ch.setLevel(logging.DEBUG) formatter = logging.Formatter("%(name)s - %(levelname)s - %(message)s") ch.setFormatter(formatter) self.logger.addHandler(ch) def _read_cvelist(self): os.chdir(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) data, _ = cvelist("data/CVE/list") for cve in self.cves: for bug in data: if bug.header.name == cve: self.bugs[cve] = bug 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: ./.list Fails if the file already exists. """ modified = [] cve_file = f"{self.source_package}.list" cves = sorted(set(self.cves)) cves.reverse() for cve in cves: 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: continue # allow for removed, old or alternate pkg names if line.version: 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 if os.path.exists(cve_file): self.logger.critical("%s already exists", cve_file) return -1 mods = [] 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) class ParseSources(ParseChanges): """Read latest version in unstable from updated local Sources files""" def parse(self): 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) 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 = '