主机及主机组管理、hotpatch插件、定时下载安全公告、定时漏洞扫描、数据矫正
This commit is contained in:
parent
9c9b6162b0
commit
c6f1d0e390
@ -1,252 +0,0 @@
|
||||
From ff5a842960179f8399434cfd36caeed23bb5c218 Mon Sep 17 00:00:00 2001
|
||||
From: young <954906362@qq.com>
|
||||
Date: Wed, 14 Dec 2022 21:36:46 +0800
|
||||
Subject: [PATCH 1/3] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=8A=E4=BC=A0?=
|
||||
=?UTF-8?q?=E6=96=87=E4=BB=B6=E5=92=8C=E6=8E=A5=E5=8F=A3=E4=B8=8D=E4=B8=80?=
|
||||
=?UTF-8?q?=E8=87=B4=E4=BD=86=E6=B2=A1=E6=8A=A5=E9=94=99=E7=9A=84=E9=97=AE?=
|
||||
=?UTF-8?q?=E9=A2=98?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.../cve_handler/manager/parse_advisory.py | 9 ++++++---
|
||||
.../cve_handler/manager/parse_unaffected.py | 8 ++++++++
|
||||
apollo/handler/cve_handler/view.py | 18 ++++++++++++++----
|
||||
3 files changed, 28 insertions(+), 7 deletions(-)
|
||||
|
||||
diff --git a/apollo/handler/cve_handler/manager/parse_advisory.py b/apollo/handler/cve_handler/manager/parse_advisory.py
|
||||
index 773f746..4848ea3 100644
|
||||
--- a/apollo/handler/cve_handler/manager/parse_advisory.py
|
||||
+++ b/apollo/handler/cve_handler/manager/parse_advisory.py
|
||||
@@ -15,12 +15,12 @@ Time:
|
||||
Author:
|
||||
Description: parse security advisory xml file, insert into database
|
||||
"""
|
||||
+from collections import defaultdict
|
||||
from xml.etree import cElementTree as ET
|
||||
from xml.etree.ElementTree import ParseError
|
||||
-from collections import defaultdict
|
||||
|
||||
-from vulcanus.log.log import LOGGER
|
||||
from apollo.function.customize_exception import ParseAdvisoryError
|
||||
+from vulcanus.log.log import LOGGER
|
||||
|
||||
__all__ = ["parse_security_advisory"]
|
||||
|
||||
@@ -99,7 +99,10 @@ def parse_cvrf_dict(cvrf_dict):
|
||||
ParseXmlError
|
||||
"""
|
||||
# affected package of this security advisory. joined with ',' if have multiple packages
|
||||
- cvrf_note = cvrf_dict["cvrfdoc"]["DocumentNotes"]["Note"]
|
||||
+ cve_document_notes = cvrf_dict["cvrfdoc"].get("DocumentNotes", "")
|
||||
+ if not cve_document_notes:
|
||||
+ return [], [], []
|
||||
+ cvrf_note = cve_document_notes["Note"]
|
||||
affected_pkgs = ""
|
||||
for info in cvrf_note:
|
||||
if info["Title"] == "Affected Component":
|
||||
diff --git a/apollo/handler/cve_handler/manager/parse_unaffected.py b/apollo/handler/cve_handler/manager/parse_unaffected.py
|
||||
index 9b4ae03..7212a5c 100644
|
||||
--- a/apollo/handler/cve_handler/manager/parse_unaffected.py
|
||||
+++ b/apollo/handler/cve_handler/manager/parse_unaffected.py
|
||||
@@ -76,7 +76,13 @@ def parse_cvrf_dict(cvrf_dict):
|
||||
Raises:
|
||||
ParseXmlError
|
||||
"""
|
||||
+ cvrf_note = cvrf_dict["cvrfdoc"].get("DocumentNotes", "")
|
||||
+ if cvrf_note:
|
||||
+ return [], [], []
|
||||
+
|
||||
cve_info_list = cvrf_dict["cvrfdoc"]["Vulnerability"]
|
||||
+ if isinstance(cve_info_list, dict):
|
||||
+ cve_info_list = [cve_info_list]
|
||||
cve_table_rows = []
|
||||
cve_pkg_rows = []
|
||||
doc_list = []
|
||||
@@ -87,6 +93,8 @@ def parse_cvrf_dict(cvrf_dict):
|
||||
remediation = cve_info["Remediations"]["Remediation"]
|
||||
if isinstance(remediation, list):
|
||||
remediation = remediation[0]
|
||||
+ if remediation["Type"] != "Unaffected":
|
||||
+ continue
|
||||
cvss_score = cve_info["CVSSScoreSets"]["ScoreSet"]["BaseScore"]
|
||||
severity = parse_cve_severity(cvss_score)
|
||||
cve_row = {
|
||||
diff --git a/apollo/handler/cve_handler/view.py b/apollo/handler/cve_handler/view.py
|
||||
index 4bfde0f..f90bd8e 100644
|
||||
--- a/apollo/handler/cve_handler/view.py
|
||||
+++ b/apollo/handler/cve_handler/view.py
|
||||
@@ -229,8 +229,10 @@ class VulUploadAdvisory(BaseResponse):
|
||||
def _save_single_advisory(proxy, file_path):
|
||||
file_name = os.path.basename(file_path)
|
||||
try:
|
||||
- cve_rows, cve_pkg_rows, cve_pkg_docs = parse_security_advisory(
|
||||
- file_path)
|
||||
+ cve_rows, cve_pkg_rows, cve_pkg_docs = parse_security_advisory(file_path)
|
||||
+ if cve_rows == [] and cve_pkg_rows == [] and cve_pkg_docs == []:
|
||||
+ os.remove(file_path)
|
||||
+ return WRONG_FILE_FORMAT
|
||||
os.remove(file_path)
|
||||
except (KeyError, ParseAdvisoryError) as error:
|
||||
os.remove(file_path)
|
||||
@@ -264,8 +266,10 @@ class VulUploadAdvisory(BaseResponse):
|
||||
for file_path in file_path_list:
|
||||
file_name = os.path.basename(file_path)
|
||||
try:
|
||||
- cve_rows, cve_pkg_rows, cve_pkg_docs = parse_security_advisory(
|
||||
- file_path)
|
||||
+ cve_rows, cve_pkg_rows, cve_pkg_docs = parse_security_advisory(file_path)
|
||||
+ if cve_rows == [] and cve_pkg_rows == [] and cve_pkg_docs == []:
|
||||
+ shutil.rmtree(folder_path)
|
||||
+ return WRONG_FILE_FORMAT
|
||||
except (KeyError, ParseAdvisoryError) as error:
|
||||
fail_list.append(file_name)
|
||||
LOGGER.error(
|
||||
@@ -356,6 +360,9 @@ class VulUploadUnaffected(BaseResponse):
|
||||
file_name = os.path.basename(file_path)
|
||||
try:
|
||||
cve_rows, cve_pkg_rows, doc_list = parse_unaffected_cve(file_path)
|
||||
+ if cve_rows == [] and cve_pkg_rows == [] and doc_list == []:
|
||||
+ os.remove(file_path)
|
||||
+ return WRONG_FILE_FORMAT
|
||||
os.remove(file_path)
|
||||
except (KeyError, ParseAdvisoryError) as error:
|
||||
os.remove(file_path)
|
||||
@@ -387,6 +394,9 @@ class VulUploadUnaffected(BaseResponse):
|
||||
file_name = os.path.basename(file_path)
|
||||
try:
|
||||
cve_rows, cve_pkg_rows, doc_list = parse_unaffected_cve(file_path)
|
||||
+ if cve_rows == [] and cve_pkg_rows == [] and doc_list == []:
|
||||
+ shutil.rmtree(folder_path)
|
||||
+ return WRONG_FILE_FORMAT
|
||||
except (KeyError, ParseAdvisoryError) as error:
|
||||
fail_list.append(file_name)
|
||||
LOGGER.error("Some error occurred when parsing unaffected cve advisory '%s'." % file_name)
|
||||
--
|
||||
Gitee
|
||||
|
||||
|
||||
From 574bbe874c9f87f7e2fff223fb48da047be8b83c Mon Sep 17 00:00:00 2001
|
||||
From: young <954906362@qq.com>
|
||||
Date: Thu, 15 Dec 2022 10:03:58 +0800
|
||||
Subject: [PATCH 2/3] =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8E=E6=8E=A5?=
|
||||
=?UTF-8?q?=E5=8F=A3=E4=B8=8D=E4=B8=80=E8=87=B4=E7=9A=84=E9=94=99=E8=AF=AF?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
apollo/handler/cve_handler/view.py | 6 ++++++
|
||||
1 file changed, 6 insertions(+)
|
||||
|
||||
diff --git a/apollo/handler/cve_handler/view.py b/apollo/handler/cve_handler/view.py
|
||||
index f90bd8e..cea00eb 100644
|
||||
--- a/apollo/handler/cve_handler/view.py
|
||||
+++ b/apollo/handler/cve_handler/view.py
|
||||
@@ -265,6 +265,9 @@ class VulUploadAdvisory(BaseResponse):
|
||||
fail_list = []
|
||||
for file_path in file_path_list:
|
||||
file_name = os.path.basename(file_path)
|
||||
+ suffix = file_name.split('.')[-1]
|
||||
+ if suffix != "xml":
|
||||
+ return WRONG_FILE_FORMAT
|
||||
try:
|
||||
cve_rows, cve_pkg_rows, cve_pkg_docs = parse_security_advisory(file_path)
|
||||
if cve_rows == [] and cve_pkg_rows == [] and cve_pkg_docs == []:
|
||||
@@ -392,6 +395,9 @@ class VulUploadUnaffected(BaseResponse):
|
||||
fail_list = []
|
||||
for file_path in file_path_list:
|
||||
file_name = os.path.basename(file_path)
|
||||
+ suffix = file_name.split('.')[-1]
|
||||
+ if suffix != "xml":
|
||||
+ return WRONG_FILE_FORMAT
|
||||
try:
|
||||
cve_rows, cve_pkg_rows, doc_list = parse_unaffected_cve(file_path)
|
||||
if cve_rows == [] and cve_pkg_rows == [] and doc_list == []:
|
||||
--
|
||||
Gitee
|
||||
|
||||
|
||||
From b5e456ab33a323c8156a024c64b5a2193883347d Mon Sep 17 00:00:00 2001
|
||||
From: young <954906362@qq.com>
|
||||
Date: Thu, 15 Dec 2022 16:40:08 +0800
|
||||
Subject: [PATCH 3/3] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BB=A3=E7=A0=81?=
|
||||
=?UTF-8?q?=E6=A3=80=E8=A7=86=E9=97=AE=E9=A2=98?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
.../handler/cve_handler/manager/parse_unaffected.py | 2 --
|
||||
apollo/handler/cve_handler/view.py | 13 +++++++------
|
||||
2 files changed, 7 insertions(+), 8 deletions(-)
|
||||
|
||||
diff --git a/apollo/handler/cve_handler/manager/parse_unaffected.py b/apollo/handler/cve_handler/manager/parse_unaffected.py
|
||||
index 7212a5c..6338cd1 100644
|
||||
--- a/apollo/handler/cve_handler/manager/parse_unaffected.py
|
||||
+++ b/apollo/handler/cve_handler/manager/parse_unaffected.py
|
||||
@@ -93,8 +93,6 @@ def parse_cvrf_dict(cvrf_dict):
|
||||
remediation = cve_info["Remediations"]["Remediation"]
|
||||
if isinstance(remediation, list):
|
||||
remediation = remediation[0]
|
||||
- if remediation["Type"] != "Unaffected":
|
||||
- continue
|
||||
cvss_score = cve_info["CVSSScoreSets"]["ScoreSet"]["BaseScore"]
|
||||
severity = parse_cve_severity(cvss_score)
|
||||
cve_row = {
|
||||
diff --git a/apollo/handler/cve_handler/view.py b/apollo/handler/cve_handler/view.py
|
||||
index cea00eb..8c161a3 100644
|
||||
--- a/apollo/handler/cve_handler/view.py
|
||||
+++ b/apollo/handler/cve_handler/view.py
|
||||
@@ -230,10 +230,9 @@ class VulUploadAdvisory(BaseResponse):
|
||||
file_name = os.path.basename(file_path)
|
||||
try:
|
||||
cve_rows, cve_pkg_rows, cve_pkg_docs = parse_security_advisory(file_path)
|
||||
- if cve_rows == [] and cve_pkg_rows == [] and cve_pkg_docs == []:
|
||||
- os.remove(file_path)
|
||||
- return WRONG_FILE_FORMAT
|
||||
os.remove(file_path)
|
||||
+ if not all([cve_rows, cve_pkg_rows, cve_pkg_docs]):
|
||||
+ return WRONG_FILE_FORMAT
|
||||
except (KeyError, ParseAdvisoryError) as error:
|
||||
os.remove(file_path)
|
||||
LOGGER.error(
|
||||
@@ -267,10 +266,11 @@ class VulUploadAdvisory(BaseResponse):
|
||||
file_name = os.path.basename(file_path)
|
||||
suffix = file_name.split('.')[-1]
|
||||
if suffix != "xml":
|
||||
+ shutil.rmtree(folder_path)
|
||||
return WRONG_FILE_FORMAT
|
||||
try:
|
||||
cve_rows, cve_pkg_rows, cve_pkg_docs = parse_security_advisory(file_path)
|
||||
- if cve_rows == [] and cve_pkg_rows == [] and cve_pkg_docs == []:
|
||||
+ if not all([cve_rows, cve_pkg_rows, cve_pkg_docs]):
|
||||
shutil.rmtree(folder_path)
|
||||
return WRONG_FILE_FORMAT
|
||||
except (KeyError, ParseAdvisoryError) as error:
|
||||
@@ -363,7 +363,7 @@ class VulUploadUnaffected(BaseResponse):
|
||||
file_name = os.path.basename(file_path)
|
||||
try:
|
||||
cve_rows, cve_pkg_rows, doc_list = parse_unaffected_cve(file_path)
|
||||
- if cve_rows == [] and cve_pkg_rows == [] and doc_list == []:
|
||||
+ if not all([cve_rows, cve_pkg_rows, doc_list]):
|
||||
os.remove(file_path)
|
||||
return WRONG_FILE_FORMAT
|
||||
os.remove(file_path)
|
||||
@@ -397,10 +397,11 @@ class VulUploadUnaffected(BaseResponse):
|
||||
file_name = os.path.basename(file_path)
|
||||
suffix = file_name.split('.')[-1]
|
||||
if suffix != "xml":
|
||||
+ shutil.rmtree(folder_path)
|
||||
return WRONG_FILE_FORMAT
|
||||
try:
|
||||
cve_rows, cve_pkg_rows, doc_list = parse_unaffected_cve(file_path)
|
||||
- if cve_rows == [] and cve_pkg_rows == [] and doc_list == []:
|
||||
+ if not all([cve_rows, cve_pkg_rows, doc_list]):
|
||||
shutil.rmtree(folder_path)
|
||||
return WRONG_FILE_FORMAT
|
||||
except (KeyError, ParseAdvisoryError) as error:
|
||||
--
|
||||
Gitee
|
||||
|
||||
@ -1,749 +0,0 @@
|
||||
From b316b4ec37fdca20c314b9755a81416c1f10a68f Mon Sep 17 00:00:00 2001
|
||||
From: wang-guangge <wangguangge@huawei.com>
|
||||
Date: Fri, 24 Mar 2023 22:56:26 +0800
|
||||
Subject: [PATCH] add dnf hot patch list plugin
|
||||
|
||||
---
|
||||
hotpatch/baseclass.py | 192 +++++++++++++++++++
|
||||
hotpatch/hotpatch.py | 201 ++++++++++++++++++++
|
||||
hotpatch/hotpatch_updateinfo.py | 321 ++++++++++++++++++++++++++++++++
|
||||
3 files changed, 714 insertions(+)
|
||||
create mode 100644 hotpatch/baseclass.py
|
||||
create mode 100644 hotpatch/hotpatch.py
|
||||
create mode 100644 hotpatch/hotpatch_updateinfo.py
|
||||
|
||||
diff --git a/hotpatch/baseclass.py b/hotpatch/baseclass.py
|
||||
new file mode 100644
|
||||
index 0000000..6dd1330
|
||||
--- /dev/null
|
||||
+++ b/hotpatch/baseclass.py
|
||||
@@ -0,0 +1,192 @@
|
||||
+class Hotpatch(object):
|
||||
+ __slots__ = ['_name', '_version', '_cves',
|
||||
+ '_advisory', '_arch', '_filename', '_state']
|
||||
+
|
||||
+ def __init__(self,
|
||||
+ name,
|
||||
+ version,
|
||||
+ arch,
|
||||
+ filename,
|
||||
+ release=''):
|
||||
+ """
|
||||
+ name: str
|
||||
+ version: str
|
||||
+ arch: str
|
||||
+ filename: str
|
||||
+ release: str
|
||||
+ """
|
||||
+ self._name = name
|
||||
+ self._version = version
|
||||
+ self._arch = arch
|
||||
+ self._filename = filename
|
||||
+ self._cves = []
|
||||
+ self._advisory = None
|
||||
+ self._state = ''
|
||||
+
|
||||
+ @property
|
||||
+ def state(self):
|
||||
+ return self._state
|
||||
+
|
||||
+ @state.setter
|
||||
+ def state(self, value):
|
||||
+ self._state = value
|
||||
+
|
||||
+ @property
|
||||
+ def name(self):
|
||||
+ return self._name
|
||||
+
|
||||
+ @property
|
||||
+ def version(self):
|
||||
+ return self._version
|
||||
+
|
||||
+ @property
|
||||
+ def src_pkg_nevre(self):
|
||||
+ src_pkg = self.name[self.name.index('-')+1:self.name.rindex('-')]
|
||||
+ release_pos = src_pkg.rindex('-')
|
||||
+ version_pos = src_pkg.rindex('-', 0, release_pos)
|
||||
+ src_pkg_name, src_pkg_version, src_pkg_release = src_pkg[
|
||||
+ 0:version_pos], src_pkg[version_pos+1:release_pos], src_pkg[release_pos+1:]
|
||||
+ return src_pkg_name, src_pkg_version, src_pkg_release
|
||||
+
|
||||
+ @property
|
||||
+ def nevra(self):
|
||||
+ """
|
||||
+ nevra: name-version-release.arch
|
||||
+ """
|
||||
+ return self.filename[0:self.filename.rindex('.')]
|
||||
+
|
||||
+ @property
|
||||
+ def hotpatch_name(self):
|
||||
+ hotpatch_name = self.name[self.name.rindex('-')+1:]
|
||||
+ return hotpatch_name
|
||||
+
|
||||
+ @property
|
||||
+ def syscare_name(self):
|
||||
+ src_pkg = '%s-%s-%s' % (self.src_pkg_nevre)
|
||||
+ return '%s/%s' % (src_pkg, self.hotpatch_name)
|
||||
+
|
||||
+ @property
|
||||
+ def cves(self):
|
||||
+ return self._cves
|
||||
+
|
||||
+ @cves.setter
|
||||
+ def cves(self, cves):
|
||||
+ self._cves = cves
|
||||
+
|
||||
+ @property
|
||||
+ def advisory(self):
|
||||
+ return self._advisory
|
||||
+
|
||||
+ @advisory.setter
|
||||
+ def advisory(self, advisory):
|
||||
+ self._advisory = advisory
|
||||
+
|
||||
+ @property
|
||||
+ def arch(self):
|
||||
+ return self._arch
|
||||
+
|
||||
+ @property
|
||||
+ def filename(self):
|
||||
+ return self._filename
|
||||
+
|
||||
+
|
||||
+class Cve(object):
|
||||
+ __slots__ = ['_cve_id', '_hotpatch']
|
||||
+
|
||||
+ def __init__(self,
|
||||
+ id,
|
||||
+ href='',
|
||||
+ title='',
|
||||
+ type='cve'):
|
||||
+ """
|
||||
+ id: str
|
||||
+ href: str
|
||||
+ title: str
|
||||
+ type: str
|
||||
+ """
|
||||
+ self._cve_id = id
|
||||
+ self._hotpatch = None
|
||||
+
|
||||
+ @property
|
||||
+ def hotpatch(self):
|
||||
+ return self._hotpatch
|
||||
+
|
||||
+ @hotpatch.setter
|
||||
+ def hotpatch(self, hotpatch: Hotpatch):
|
||||
+ self._hotpatch = hotpatch
|
||||
+
|
||||
+ @property
|
||||
+ def cve_id(self):
|
||||
+ return self._cve_id
|
||||
+
|
||||
+
|
||||
+class Advisory(object):
|
||||
+ __slots__ = ['_id', '_type', '_title', '_severity',
|
||||
+ '_description', '_updated', '_hotpatches', '_cves']
|
||||
+
|
||||
+ def __init__(self,
|
||||
+ id,
|
||||
+ type,
|
||||
+ title,
|
||||
+ severity,
|
||||
+ description,
|
||||
+ updated="1970-01-01 08:00:00",
|
||||
+ release="",
|
||||
+ issued=""):
|
||||
+ """
|
||||
+ id: str
|
||||
+ type: str
|
||||
+ title: str
|
||||
+ severity: str
|
||||
+ description: str
|
||||
+ updated: str
|
||||
+ release: str
|
||||
+ issued: str
|
||||
+ """
|
||||
+ self._id = id
|
||||
+ self._type = type
|
||||
+ self._title = title
|
||||
+ self._severity = severity
|
||||
+ self._description = description
|
||||
+ self._updated = updated
|
||||
+ self._cves = {}
|
||||
+ self._hotpatches = []
|
||||
+
|
||||
+ @property
|
||||
+ def id(self):
|
||||
+ return self._id
|
||||
+
|
||||
+ @property
|
||||
+ def type(self):
|
||||
+ return self._type
|
||||
+
|
||||
+ @property
|
||||
+ def title(self):
|
||||
+ return self._title
|
||||
+
|
||||
+ @property
|
||||
+ def severity(self):
|
||||
+ return self._severity
|
||||
+
|
||||
+ @property
|
||||
+ def description(self):
|
||||
+ return self._description
|
||||
+
|
||||
+ @property
|
||||
+ def updated(self):
|
||||
+ return self._updated
|
||||
+
|
||||
+ @property
|
||||
+ def cves(self):
|
||||
+ return self._cves
|
||||
+
|
||||
+ @cves.setter
|
||||
+ def cves(self, advisory_cves):
|
||||
+ self._cves = advisory_cves
|
||||
+
|
||||
+ @property
|
||||
+ def hotpatches(self):
|
||||
+ return self._hotpatches
|
||||
+
|
||||
+ def add_hotpatch(self, hotpatch: Hotpatch):
|
||||
+ self._hotpatches.append(hotpatch)
|
||||
diff --git a/hotpatch/hotpatch.py b/hotpatch/hotpatch.py
|
||||
new file mode 100644
|
||||
index 0000000..80cc69b
|
||||
--- /dev/null
|
||||
+++ b/hotpatch/hotpatch.py
|
||||
@@ -0,0 +1,201 @@
|
||||
+import dnf
|
||||
+from dnf.i18n import _
|
||||
+from dnf.cli.commands.updateinfo import UpdateInfoCommand
|
||||
+import hawkey
|
||||
+from .hotpatch_updateinfo import HotpatchUpdateInfo
|
||||
+
|
||||
+
|
||||
+class Versions:
|
||||
+ """
|
||||
+ Version number processing
|
||||
+ """
|
||||
+
|
||||
+ separator = (".", "-")
|
||||
+ _connector = "&"
|
||||
+
|
||||
+ def _order(self, version, separator=None):
|
||||
+ """
|
||||
+ Version of the cutting
|
||||
+ Args:
|
||||
+ version: version
|
||||
+ separator: separator
|
||||
+
|
||||
+ Returns:
|
||||
+
|
||||
+ """
|
||||
+ if not separator:
|
||||
+ separator = self._connector
|
||||
+ return tuple([int(v) for v in version.split(separator) if v.isdigit()])
|
||||
+
|
||||
+ def lgt(self, version, compare_version):
|
||||
+ """
|
||||
+ Returns true if the size of the compared version is greater
|
||||
+ than that of the compared version, or false otherwise
|
||||
+
|
||||
+ """
|
||||
+ for separator in self.separator:
|
||||
+ version = self._connector.join(
|
||||
+ [v for v in version.split(separator)])
|
||||
+ compare_version = self._connector.join(
|
||||
+ [v for v in compare_version.split(separator)]
|
||||
+ )
|
||||
+ version = self._order(version)
|
||||
+ compare_version = self._order(compare_version)
|
||||
+ return version >= compare_version
|
||||
+
|
||||
+
|
||||
+@dnf.plugin.register_command
|
||||
+class HotpatchCommand(dnf.cli.Command):
|
||||
+ aliases = ['hotpatch']
|
||||
+ summary = _('show hotpatch info')
|
||||
+
|
||||
+ def __init__(self, cli):
|
||||
+ """
|
||||
+ Initialize the command
|
||||
+ """
|
||||
+ super(HotpatchCommand, self).__init__(cli)
|
||||
+
|
||||
+ @staticmethod
|
||||
+ def set_argparser(parser):
|
||||
+ output_format = parser.add_mutually_exclusive_group()
|
||||
+ output_format.add_argument("--list", dest='_spec_action', const='list',
|
||||
+ action='store_const',
|
||||
+ help=_('show list of cves'))
|
||||
+
|
||||
+ def configure(self):
|
||||
+ demands = self.cli.demands
|
||||
+ demands.sack_activation = True
|
||||
+ demands.available_repos = True
|
||||
+
|
||||
+ self.filter_cves = self.opts.cves if self.opts.cves else None
|
||||
+
|
||||
+ def run(self):
|
||||
+ self.hp_hawkey = HotpatchUpdateInfo(self.cli.base, self.cli)
|
||||
+
|
||||
+ if self.opts._spec_action == 'list':
|
||||
+ self.display()
|
||||
+
|
||||
+ def get_mapping_nevra_cve(self) -> dict:
|
||||
+ """
|
||||
+ Get cve nevra mapping based on the UpdateInfoCommand of 'dnf updateinfo list cves'
|
||||
+
|
||||
+ Returns:
|
||||
+ {
|
||||
+ (nevra, advisory.updated):
|
||||
+ cve_id: {
|
||||
+ (advisory.type, advisory.severity),
|
||||
+ ...
|
||||
+ }
|
||||
+ ...
|
||||
+ }
|
||||
+ """
|
||||
+ # configure UpdateInfoCommand with 'dnf updateinfo list cves'
|
||||
+ updateinfo = UpdateInfoCommand(self.cli)
|
||||
+ updateinfo.opts = self.opts
|
||||
+
|
||||
+ updateinfo.opts.spec_action = 'list'
|
||||
+ updateinfo.opts.with_cve = True
|
||||
+ updateinfo.opts.spec = '*'
|
||||
+ updateinfo.opts._advisory_types = set()
|
||||
+ updateinfo.opts.availability = 'available'
|
||||
+ self.updateinfo = updateinfo
|
||||
+
|
||||
+ apkg_adv_insts = updateinfo.available_apkg_adv_insts(
|
||||
+ updateinfo.opts.spec)
|
||||
+
|
||||
+ mapping_nevra_cve = dict()
|
||||
+ for apkg, advisory, _ in apkg_adv_insts:
|
||||
+ nevra = (apkg.name, apkg.evr, apkg.arch)
|
||||
+ for ref in advisory.references:
|
||||
+ if ref.type != hawkey.REFERENCE_CVE:
|
||||
+ continue
|
||||
+ mapping_nevra_cve.setdefault((nevra, advisory.updated), dict())[
|
||||
+ ref.id] = (advisory.type, advisory.severity)
|
||||
+
|
||||
+ return mapping_nevra_cve
|
||||
+
|
||||
+ def _filter_and_format_list_output(self, echo_lines: list, fixed_cve_id: set, fixed_coldpatches: set):
|
||||
+ """
|
||||
+ Only show specified cve information that have not been fixed, and format output
|
||||
+ """
|
||||
+ def is_patch_fixed(coldpatch, fixed_coldpatches):
|
||||
+ """
|
||||
+ Check whether the coldpatch is fixed
|
||||
+ """
|
||||
+ for fixed_coldpatch in fixed_coldpatches:
|
||||
+ pkg_name, pkg_evr, _ = coldpatch
|
||||
+ fixed_pkg_name, fixed_pkg_evr, _ = fixed_coldpatch
|
||||
+ if pkg_name != fixed_pkg_name:
|
||||
+ continue
|
||||
+ version = Versions()
|
||||
+ if version.lgt(fixed_pkg_evr, pkg_evr):
|
||||
+ return True
|
||||
+ return False
|
||||
+
|
||||
+ idw = tiw = ciw = 0
|
||||
+ format_lines = set()
|
||||
+ for echo_line in echo_lines:
|
||||
+ cve_id, type, coldpatch, hotpatch = echo_line[0], echo_line[1], echo_line[2], echo_line[3]
|
||||
+ if self.filter_cves is not None and cve_id not in self.filter_cves:
|
||||
+ continue
|
||||
+ if cve_id in fixed_cve_id:
|
||||
+ continue
|
||||
+ if not isinstance(coldpatch, str):
|
||||
+ if is_patch_fixed(coldpatch, fixed_coldpatches):
|
||||
+ continue
|
||||
+ else:
|
||||
+ pkg_name, pkg_evr, pkg_arch = coldpatch
|
||||
+ coldpatch = '%s-%s.%s' % (pkg_name, pkg_evr, pkg_arch)
|
||||
+
|
||||
+ idw = max(idw, len(cve_id))
|
||||
+ tiw = max(tiw, len(type))
|
||||
+ ciw = max(ciw, len(coldpatch))
|
||||
+ format_lines.add((cve_id, type, coldpatch, hotpatch))
|
||||
+ for format_line in sorted(format_lines, key=lambda x: x[2]):
|
||||
+ print('%-*s %-*s %-*s %s' %
|
||||
+ (idw, format_line[0], tiw, format_line[1], ciw, format_line[2], format_line[3]))
|
||||
+
|
||||
+ def display(self):
|
||||
+ """
|
||||
+ Append hotpatch information according to the output of 'dnf updateinfo list cves'
|
||||
+
|
||||
+ echo lines:
|
||||
+ [
|
||||
+ [cve_id, type, coldpatch, hotpatch]
|
||||
+ ]
|
||||
+ """
|
||||
+
|
||||
+ def type2label(updateinfo, typ, sev):
|
||||
+ if typ == hawkey.ADVISORY_SECURITY:
|
||||
+ return updateinfo.SECURITY2LABEL.get(sev, _('Unknown/Sec.'))
|
||||
+ else:
|
||||
+ return updateinfo.TYPE2LABEL.get(typ, _('unknown'))
|
||||
+
|
||||
+ mapping_nevra_cve = self.get_mapping_nevra_cve()
|
||||
+ echo_lines = []
|
||||
+ fixed_cve_id = set()
|
||||
+ fixed_coldpatches = set()
|
||||
+ iterated_cve_id = set()
|
||||
+ for ((nevra), aupdated), id2type in sorted(mapping_nevra_cve.items(), key=lambda x: x[0]):
|
||||
+ pkg_name, pkg_evr, pkg_arch = nevra
|
||||
+ for cve_id, atypesev in id2type.items():
|
||||
+ iterated_cve_id.add(cve_id)
|
||||
+ label = type2label(self.updateinfo, *atypesev)
|
||||
+ echo_line = [cve_id, label, nevra, '-']
|
||||
+ if cve_id in self.hp_hawkey.hotpatch_cves:
|
||||
+ hotpatch = self.hp_hawkey.hotpatch_cves[cve_id].hotpatch
|
||||
+ if hotpatch is not None and hotpatch.src_pkg_nevre[0] == pkg_name:
|
||||
+ if hotpatch.state == self.hp_hawkey.INSTALLED:
|
||||
+ # record the fixed cves
|
||||
+ for cve_id in hotpatch.cves:
|
||||
+ fixed_cve_id.add(cve_id)
|
||||
+ # record the fixed coldpatch to filter the cves of the corresponding coldpatch with the lower version
|
||||
+ fixed_coldpatches.add((nevra))
|
||||
+ continue
|
||||
+ elif hotpatch.state == self.hp_hawkey.INSTALLABLE:
|
||||
+ echo_line[3] = hotpatch.nevra
|
||||
+
|
||||
+ echo_lines.append(echo_line)
|
||||
+
|
||||
+ self._filter_and_format_list_output(
|
||||
+ echo_lines, fixed_cve_id, fixed_coldpatches)
|
||||
diff --git a/hotpatch/hotpatch_updateinfo.py b/hotpatch/hotpatch_updateinfo.py
|
||||
new file mode 100644
|
||||
index 0000000..4e0b702
|
||||
--- /dev/null
|
||||
+++ b/hotpatch/hotpatch_updateinfo.py
|
||||
@@ -0,0 +1,321 @@
|
||||
+from .baseclass import Hotpatch, Cve, Advisory
|
||||
+from .syscare import Syscare
|
||||
+import os
|
||||
+from typing import Optional
|
||||
+import gzip
|
||||
+import xml.etree.ElementTree as ET
|
||||
+import datetime
|
||||
+
|
||||
+
|
||||
+class HotpatchUpdateInfo(object):
|
||||
+ """
|
||||
+ Hotpatch relevant updateinfo processing
|
||||
+ """
|
||||
+
|
||||
+ UNINSTALLABLE = 0
|
||||
+ INSTALLED = 1
|
||||
+ INSTALLABLE = 2
|
||||
+
|
||||
+ def __init__(self, base, cli):
|
||||
+ self.base = base
|
||||
+ self.cli = cli
|
||||
+ # dict {advisory_id: Advisory}
|
||||
+ self._hotpatch_advisories = {}
|
||||
+ # dict {cve_id: Cve}
|
||||
+ self._hotpatch_cves = {}
|
||||
+ # list [{'Uuid': uuid, 'Name':name, 'Status': status}]
|
||||
+ self._hotpatch_status = []
|
||||
+
|
||||
+ self.init_hotpatch_info()
|
||||
+
|
||||
+ def init_hotpatch_info(self):
|
||||
+ """
|
||||
+ Initialize hotpatch information
|
||||
+ """
|
||||
+ self._get_installed_pkgs()
|
||||
+ self._parse_and_store_hotpatch_info_from_updateinfo()
|
||||
+ self._init_hotpatch_status_from_syscare()
|
||||
+ self._init_hotpatch_state()
|
||||
+
|
||||
+ @property
|
||||
+ def hotpatch_cves(self):
|
||||
+ return self._hotpatch_cves
|
||||
+
|
||||
+ @property
|
||||
+ def hotpatch_status(self):
|
||||
+ return self._hotpatch_status
|
||||
+
|
||||
+ def _get_installed_pkgs(self):
|
||||
+ """
|
||||
+ Get installed packages by setting the hawkey
|
||||
+ """
|
||||
+ sack = self.base.sack
|
||||
+ # the latest installed packages
|
||||
+ q = sack.query().installed().latest(1)
|
||||
+ # plus packages of the running kernel
|
||||
+ kernel_q = sack.query().filterm(empty=True)
|
||||
+ kernel = sack.get_running_kernel()
|
||||
+ if kernel:
|
||||
+ kernel_q = kernel_q.union(
|
||||
+ sack.query().filterm(sourcerpm=kernel.sourcerpm))
|
||||
+ q = q.union(kernel_q.installed())
|
||||
+ q = q.apply()
|
||||
+
|
||||
+ self._inst_pkgs_query = q
|
||||
+
|
||||
+ def _parse_and_store_hotpatch_info_from_updateinfo(self):
|
||||
+ """
|
||||
+ Initialize hotpatch information from repos
|
||||
+ """
|
||||
+ # get xxx-hotpatch.xml.gz file paths by traversing the system_cachedir(/var/cache/dnf)
|
||||
+ system_cachedir = self.cli.base.conf.system_cachedir
|
||||
+ all_repos = self.cli.base.repos
|
||||
+ map_repo_updateinfoxml = {}
|
||||
+
|
||||
+ for file in os.listdir(system_cachedir):
|
||||
+ file_path = os.path.join(system_cachedir, file)
|
||||
+ if os.path.isdir(file_path):
|
||||
+ repodata_path = os.path.join(file_path, "repodata")
|
||||
+ if not os.path.isdir(repodata_path):
|
||||
+ continue
|
||||
+
|
||||
+ for xml_file in os.listdir(repodata_path):
|
||||
+ # the hotpatch relevant updateinfo is recorded in xxx-hotpatch.xml.gz
|
||||
+ if "hotpatch" in xml_file:
|
||||
+ repo_name = file.split("-")[0]
|
||||
+ cache_updateinfo_xml_path = os.path.join(
|
||||
+ repodata_path, xml_file)
|
||||
+ map_repo_updateinfoxml[repo_name] = cache_updateinfo_xml_path
|
||||
+
|
||||
+ # only hotpatch relevant updateinfo from enabled repos are parsed and stored
|
||||
+ for repo in all_repos.iter_enabled():
|
||||
+ repo_id = repo.id
|
||||
+ if repo_id in map_repo_updateinfoxml:
|
||||
+ updateinfoxml_path = map_repo_updateinfoxml[repo_id]
|
||||
+ self._parse_and_store_from_xml(updateinfoxml_path)
|
||||
+
|
||||
+ def _parse_pkglist(self, pkglist):
|
||||
+ """
|
||||
+ Parse the pkglist information, filter the hotpatches with different arches
|
||||
+ """
|
||||
+ hotpatches = []
|
||||
+ hot_patch_collection = pkglist.find('collection')
|
||||
+ arches = self.base.sack.list_arches()
|
||||
+ if not hot_patch_collection:
|
||||
+ return hotpatches
|
||||
+ for package in hot_patch_collection.iter('package'):
|
||||
+ hotpatch = {key: value for key, value in package.items()}
|
||||
+ if hotpatch['arch'] not in arches:
|
||||
+ continue
|
||||
+ hotpatch['filename'] = package.find('filename').text
|
||||
+ hotpatches.append(hotpatch)
|
||||
+ return hotpatches
|
||||
+
|
||||
+ def _parse_references(self, reference):
|
||||
+ """
|
||||
+ Parse the reference information, check whether the 'id' is missing
|
||||
+ """
|
||||
+ cves = []
|
||||
+ for ref in reference:
|
||||
+ cve = {key: value for key, value in ref.items()}
|
||||
+ if 'id' not in cve:
|
||||
+ continue
|
||||
+ cves.append(cve)
|
||||
+ return cves
|
||||
+
|
||||
+ def _verify_date_str_lawyer(self, datetime_str: str) -> str:
|
||||
+ """
|
||||
+ Check whether the 'datetime' field is legal, if not return default value
|
||||
+ """
|
||||
+ if datetime_str.isdigit() and len(datetime_str) == 10:
|
||||
+ datetime_str = int(datetime_str)
|
||||
+ datetime_str = datetime.datetime.fromtimestamp(
|
||||
+ datetime_str).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ try:
|
||||
+ datetime.datetime.strptime(datetime_str, '%Y-%m-%d %H:%M:%S')
|
||||
+ return datetime_str
|
||||
+ except ValueError:
|
||||
+ return "1970-01-01 08:00:00"
|
||||
+
|
||||
+ def _parse_advisory(self, update):
|
||||
+ """
|
||||
+ Parse the advisory information: check whether the 'datetime' field is legal, parse the 'references'
|
||||
+ field and the 'pkglist' field, save 'type' information
|
||||
+ """
|
||||
+ advisory = {}
|
||||
+ for node in update:
|
||||
+ if node.tag == 'datetime':
|
||||
+ advisory[node.tag] = self._verify_date_str_lawyer(
|
||||
+ update.find(node.tag).text)
|
||||
+ elif node.tag == 'references':
|
||||
+ advisory[node.tag] = self._parse_references(node)
|
||||
+ elif node.tag == 'pkglist':
|
||||
+ advisory['hotpatches'] = self._parse_pkglist(node)
|
||||
+ else:
|
||||
+ advisory[node.tag] = update.find(node.tag).text
|
||||
+ advisory['type'] = update.get('type')
|
||||
+ return advisory
|
||||
+
|
||||
+ def _store_advisory_info(self, advisory_kwargs: dict()):
|
||||
+ """
|
||||
+ Instantiate Cve, Hotpatch and Advisory object according to the advisory kwargs
|
||||
+ """
|
||||
+ advisory_references = advisory_kwargs.pop('references')
|
||||
+ advisory_hotpatches = advisory_kwargs.pop('hotpatches')
|
||||
+ advisory = Advisory(**advisory_kwargs)
|
||||
+ advisory_cves = {}
|
||||
+ for cve_kwargs in advisory_references:
|
||||
+ cve = Cve(**cve_kwargs)
|
||||
+ self._hotpatch_cves[cve.cve_id] = cve
|
||||
+ advisory_cves[cve.cve_id] = cve
|
||||
+ advisory.cves = advisory_cves
|
||||
+
|
||||
+ for hotpatch_kwargs in advisory_hotpatches:
|
||||
+ hotpatch = Hotpatch(**hotpatch_kwargs)
|
||||
+ hotpatch.advisory = advisory
|
||||
+ hotpatch.cves = advisory_cves.keys()
|
||||
+
|
||||
+ advisory.add_hotpatch(hotpatch)
|
||||
+
|
||||
+ for cve in advisory_cves.values():
|
||||
+ cve.hotpatch = hotpatch
|
||||
+
|
||||
+ self._hotpatch_advisories.setdefault(
|
||||
+ advisory_kwargs['id'], list()).append(advisory)
|
||||
+
|
||||
+ def _init_hotpatch_state(self):
|
||||
+ """
|
||||
+ Initialize the hotpatch state
|
||||
+
|
||||
+ each hotpatch has three states:
|
||||
+ 1. UNINSTALLABLE: can not be installed due to the source package version mismatch
|
||||
+ 2. INSTALLED: has been installed and actived in syscare
|
||||
+ 3. INSTALLABLE: can be installed
|
||||
+
|
||||
+ """
|
||||
+ for advisories in self._hotpatch_advisories.values():
|
||||
+ for advisory in advisories:
|
||||
+ for hotpatch in advisory.hotpatches:
|
||||
+ src_pkg_name, src_pkg_version, src_pkg_release = hotpatch.src_pkg_nevre
|
||||
+ inst_pkgs = self._inst_pkgs_query.filter(name=src_pkg_name)
|
||||
+ hotpatch.state = self.UNINSTALLABLE
|
||||
+ # check whether the relevant source package is installed on this machine
|
||||
+ if not inst_pkgs:
|
||||
+ continue
|
||||
+ for inst_pkg in inst_pkgs:
|
||||
+ inst_pkg_vere = '%s-%s' % (inst_pkg.version,
|
||||
+ inst_pkg.release)
|
||||
+ hp_vere = '%s-%s' % (src_pkg_version, src_pkg_release)
|
||||
+ if hp_vere != inst_pkg_vere:
|
||||
+ continue
|
||||
+ elif self._get_hotpatch_status_in_syscare(hotpatch) == 'ACTIVED':
|
||||
+ hotpatch.state = self.INSTALLED
|
||||
+ else:
|
||||
+ hotpatch.state = self.INSTALLABLE
|
||||
+
|
||||
+ def _parse_and_store_from_xml(self, updateinfoxml):
|
||||
+ """
|
||||
+ Parse and store hotpatch update information from xxx-hotpatch.xml.gz
|
||||
+
|
||||
+ xxx-hotpatch.xml.gz e.g.
|
||||
+
|
||||
+ <?xml version="1.0" encoding="UTF-8"?>
|
||||
+ <updates>
|
||||
+ <update from="openeuler.org" type="security" status="stable">
|
||||
+ <id>openEuler-SA-2022-1</id>
|
||||
+ <title>An update for mariadb is now available for openEuler-22.03-LTS</title>
|
||||
+ <severity>Important</severity>
|
||||
+ <release>openEuler</release>
|
||||
+ <issued date="2022-04-16"></issued>
|
||||
+ <references>
|
||||
+ <reference href="https://nvd.nist.gov/vuln/detail/CVE-2021-46658" id="CVE-2021-1" title="CVE-2021-1" type="cve"></reference>
|
||||
+ </references>
|
||||
+ <description>patch-redis-6.2.5-1-HP001.(CVE-2022-24048)</description>
|
||||
+ <pkglist>
|
||||
+ <collection>
|
||||
+ <name>openEuler</name>
|
||||
+ <package arch="aarch64" name="patch-redis-6.2.5-1-HP001" release="0" version="1">
|
||||
+ <filename>patch-redis-6.2.5-1-HP001-0-1.aarch64.rpm</filename>
|
||||
+ </package>
|
||||
+ <package arch="x86_64" name="patch-redis-6.2.5-1-HP001" release="0" version="1">
|
||||
+ <filename>patch-redis-6.2.5-1-HP001-0-1.x86_64.rpm</filename>
|
||||
+ </package>
|
||||
+ <collection>
|
||||
+ </pkglist>
|
||||
+ </update>
|
||||
+ ...
|
||||
+ </updates>
|
||||
+ """
|
||||
+ content = gzip.open(updateinfoxml)
|
||||
+ tree = ET.parse(content)
|
||||
+ root = tree.getroot()
|
||||
+ for update in root.iter('update'):
|
||||
+ advisory = self._parse_advisory(update)
|
||||
+ self._store_advisory_info(advisory)
|
||||
+
|
||||
+ def _init_hotpatch_status_from_syscare(self):
|
||||
+ """
|
||||
+ Initialize hotpatch status from syscare
|
||||
+ """
|
||||
+ self._hotpatch_status = Syscare().list()
|
||||
+
|
||||
+ self._hotpatch_state = {}
|
||||
+ for hotpatch_info in self._hotpatch_status:
|
||||
+ self._hotpatch_state[hotpatch_info['Name']
|
||||
+ ] = hotpatch_info['Status']
|
||||
+
|
||||
+ def _get_hotpatch_status_in_syscare(self, hotpatch: Hotpatch) -> str:
|
||||
+ """
|
||||
+ Get hotpatch status in syscare
|
||||
+ """
|
||||
+ if hotpatch.syscare_name not in self._hotpatch_state:
|
||||
+ return ''
|
||||
+ return self._hotpatch_state[hotpatch.syscare_name]
|
||||
+
|
||||
+ def get_hotpatches_from_cve(self, cves: Optional[list[str]] = []) -> dict():
|
||||
+ """
|
||||
+ Get hotpatches from specified cve
|
||||
+
|
||||
+ Args:
|
||||
+ cves: [cve_id_1, cve_id_2]
|
||||
+
|
||||
+ Returns:
|
||||
+ {
|
||||
+ cve_id_1: [hotpatch1],
|
||||
+ cve_id_2: []
|
||||
+ }
|
||||
+ """
|
||||
+ mapping_cve_hotpatches = dict()
|
||||
+ for cve_id in cves:
|
||||
+ mapping_cve_hotpatches[cve_id] = []
|
||||
+ if cve_id not in self.hotpatch_cves:
|
||||
+ continue
|
||||
+ hotpatch = self.hotpatch_cves[cve_id].hotpatch
|
||||
+ if hotpatch is not None and hotpatch.state == self.INSTALLABLE:
|
||||
+ mapping_cve_hotpatches[cve_id].append(hotpatch.nevra)
|
||||
+ return mapping_cve_hotpatches
|
||||
+
|
||||
+ def get_hotpatches_from_advisories(self, advisories: Optional[list[str]] = []) -> dict():
|
||||
+ """
|
||||
+ Get hotpatches from specified advisories
|
||||
+
|
||||
+ Args:
|
||||
+ advisories: [advisory_id_1, advisory_id_2]
|
||||
+
|
||||
+ Return:
|
||||
+ {
|
||||
+ advisory_id_1: [hotpatch1],
|
||||
+ advisory_id_2: []
|
||||
+ }
|
||||
+ """
|
||||
+ mapping_advisory_hotpatches = dict()
|
||||
+ for advisory_id in advisories:
|
||||
+ mapping_advisory_hotpatches[advisory_id] = []
|
||||
+ if advisory_id not in self._hotpatch_advisories:
|
||||
+ continue
|
||||
+ for advisory in self._hotpatch_advisories[advisory_id]:
|
||||
+ for hotpatch in advisory.hotpatches:
|
||||
+ if hotpatch.state == self.INSTALLABLE:
|
||||
+ mapping_advisory_hotpatches[advisory_id].append(
|
||||
+ hotpatch.nevra)
|
||||
+ return mapping_advisory_hotpatches
|
||||
--
|
||||
2.33.0
|
||||
|
||||
@ -1,399 +0,0 @@
|
||||
From 20c09ffb55a630a4259b7c87cd88da2ce6f28a07 Mon Sep 17 00:00:00 2001
|
||||
From: zhu-yuncheng <zhuyuncheng@huawei.com>
|
||||
Date: Sat, 25 Mar 2023 14:43:20 +0800
|
||||
Subject: [PATCH] add dnf hot upgrade plugin
|
||||
|
||||
---
|
||||
hotpatch/hot-upgrade.py | 275 ++++++++++++++++++++++++++++++++++++++++
|
||||
hotpatch/syscare.py | 96 ++++++++++++++
|
||||
2 files changed, 371 insertions(+)
|
||||
create mode 100644 hotpatch/hot-upgrade.py
|
||||
create mode 100644 hotpatch/syscare.py
|
||||
|
||||
diff --git a/hotpatch/hot-upgrade.py b/hotpatch/hot-upgrade.py
|
||||
new file mode 100644
|
||||
index 0000000..7a4c3c6
|
||||
--- /dev/null
|
||||
+++ b/hotpatch/hot-upgrade.py
|
||||
@@ -0,0 +1,275 @@
|
||||
+# supplies the dnf 'diff' command.
|
||||
+#
|
||||
+# Copyright (C) 2018 Red Hat, Inc.
|
||||
+# Written by Pavel Raiskup <praiskup@redhat.com>.
|
||||
+#
|
||||
+# This copyrighted material is made available to anyone wishing to use,
|
||||
+# modify, copy, or redistribute it subject to the terms and conditions of
|
||||
+# the GNU General Public License v.2, or (at your option) any later version.
|
||||
+# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
+# ANY WARRANTY expressed or implied, including the implied warranties 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. Any Red Hat trademarks that are incorporated in the
|
||||
+# source code or documentation are not subject to the GNU General Public
|
||||
+# License and may only be used or replicated with the express permission of
|
||||
+# Red Hat, Inc.
|
||||
+
|
||||
+from __future__ import print_function
|
||||
+
|
||||
+import dnf.base
|
||||
+import dnf.exceptions
|
||||
+import hawkey
|
||||
+from dnf.cli import commands
|
||||
+from dnf.cli.option_parser import OptionParser
|
||||
+from dnf.cli.output import Output
|
||||
+from dnfpluginscore import _, logger
|
||||
+
|
||||
+from .syscare import Syscare
|
||||
+from .hotpatch_updateinfo import HotpatchUpdateInfo
|
||||
+
|
||||
+
|
||||
+@dnf.plugin.register_command
|
||||
+class HotupgradeCommand(dnf.cli.Command):
|
||||
+ aliases = ("hotupgrade",)
|
||||
+ summary = "Hot upgrade package using hot patch."
|
||||
+ usage = ""
|
||||
+ syscare = Syscare()
|
||||
+ hp_list = []
|
||||
+
|
||||
+ @staticmethod
|
||||
+ def set_argparser(parser):
|
||||
+ parser.add_argument('packages', nargs='*', help=_('Package to upgrade'),
|
||||
+ action=OptionParser.ParseSpecGroupFileCallback,
|
||||
+ metavar=_('PACKAGE'))
|
||||
+
|
||||
+ def configure(self):
|
||||
+ """Verify that conditions are met so that this command can run.
|
||||
+ These include that there are enabled repositories with gpg
|
||||
+ keys, and that this command is being run by the root user.
|
||||
+ """
|
||||
+ demands = self.cli.demands
|
||||
+ demands.sack_activation = True
|
||||
+ demands.available_repos = True
|
||||
+ demands.resolving = True
|
||||
+ demands.root_user = True
|
||||
+
|
||||
+ commands._checkGPGKey(self.base, self.cli)
|
||||
+ if not self.opts.filenames:
|
||||
+ commands._checkEnabledRepo(self.base)
|
||||
+
|
||||
+ def run(self):
|
||||
+ if self.opts.pkg_specs:
|
||||
+ self.hp_list = self.opts.pkg_specs
|
||||
+ elif self.opts.cves or self.opts.advisory:
|
||||
+ cve_pkgs = self.get_hotpatch_based_on_cve(self.opts.cves)
|
||||
+ advisory_pkgs = self.get_hotpatch_based_on_advisory(self.opts.advisory)
|
||||
+ self.hp_list = cve_pkgs + advisory_pkgs
|
||||
+ else:
|
||||
+ raise dnf.exceptions.Error(_('No qualified rpm package name or cve/advisory id.'))
|
||||
+
|
||||
+ hp_target_map = self._get_available_hotpatches(self.hp_list)
|
||||
+ if not hp_target_map:
|
||||
+ raise dnf.exceptions.Error(_('No hot patches marked for install.'))
|
||||
+
|
||||
+ target_patch_map = self._get_applied_old_patch(list(hp_target_map.values()))
|
||||
+ if target_patch_map:
|
||||
+ self._remove_hot_patches(target_patch_map)
|
||||
+ else:
|
||||
+ self.syscare.save()
|
||||
+ success = self._install_hot_patch(list(hp_target_map.keys()))
|
||||
+ if not success:
|
||||
+ output, status = self.syscare.restore()
|
||||
+ if status:
|
||||
+ raise dnf.exceptions.Error(_('Roll back failed.'))
|
||||
+ raise dnf.exceptions.Error(_("Roll back succeed."))
|
||||
+ return
|
||||
+
|
||||
+ def run_transaction(self) -> None:
|
||||
+ """
|
||||
+ apply hot patches
|
||||
+ Returns:
|
||||
+ None
|
||||
+ """
|
||||
+ logger.info(_('Applying hot patch'))
|
||||
+ if not self.base.transaction:
|
||||
+ for hp in self.hp_list:
|
||||
+ self._apply_hp(hp)
|
||||
+ return
|
||||
+
|
||||
+ for ts_item in self.base.transaction:
|
||||
+ if ts_item.action not in dnf.transaction.FORWARD_ACTIONS:
|
||||
+ continue
|
||||
+ self._apply_hp(str(ts_item.pkg))
|
||||
+
|
||||
+ def _apply_hp(self, hp_full_name):
|
||||
+ pkg_info = self._parse_hp_name(hp_full_name)
|
||||
+ hp_full_name = "-".join([pkg_info["name"], pkg_info["version"], pkg_info["release"]]) \
|
||||
+ + '/' + pkg_info["hp_name"]
|
||||
+ output, status = self.syscare.apply(hp_full_name)
|
||||
+ if status:
|
||||
+ logger.info(_('Apply hot patch failed: %s.'), hp_full_name)
|
||||
+ else:
|
||||
+ logger.info(_('Apply hot patch succeed: %s.'), hp_full_name)
|
||||
+
|
||||
+ def _get_available_hotpatches(self, pkg_specs: list) -> dict:
|
||||
+ """
|
||||
+ check two conditions:
|
||||
+ 1. the hot patch rpm package exists in repositories
|
||||
+ 2. the hot patch's target package with specific version and release already installed
|
||||
+ Args:
|
||||
+ pkg_specs: full names of hot patches' rpm packages
|
||||
+
|
||||
+ Returns:
|
||||
+ dict: key is available hot patches' full name, value is target package's name-version-release
|
||||
+ """
|
||||
+ hp_target_map = {}
|
||||
+ installed_packages = self.base.sack.query().installed()
|
||||
+ for pkg_spec in set(pkg_specs):
|
||||
+ query = self.base.sack.query()
|
||||
+ # check the package exist in repo or not
|
||||
+ subj = dnf.subject.Subject(pkg_spec)
|
||||
+ parsed_nevras = subj.get_nevra_possibilities(forms=[hawkey.FORM_NEVRA])
|
||||
+ if len(parsed_nevras) != 1:
|
||||
+ logger.info(_('Cannot parse NEVRA for package "{nevra}"').format(nevra=pkg_spec))
|
||||
+ continue
|
||||
+
|
||||
+ parsed_nevra = parsed_nevras[0]
|
||||
+ available_hp = query.available().filter(name=parsed_nevra.name, version=parsed_nevra.version,
|
||||
+ release=parsed_nevra.release, arch=parsed_nevra.arch)
|
||||
+ if not available_hp:
|
||||
+ logger.info(_('No match for argument: %s'), self.base.output.term.bold(pkg_spec))
|
||||
+ continue
|
||||
+
|
||||
+ # check the hot patch's target package installed or not
|
||||
+ pkg_info = self._parse_hp_name(pkg_spec)
|
||||
+ installed_pkg = installed_packages.filter(name=pkg_info["name"],
|
||||
+ version=pkg_info["version"],
|
||||
+ release=pkg_info["release"]).run()
|
||||
+ if not installed_pkg:
|
||||
+ logger.info(_("The hot patch's target package is not installed: %s"),
|
||||
+ self.base.output.term.bold(pkg_spec))
|
||||
+ continue
|
||||
+
|
||||
+ if len(installed_pkg) != 1:
|
||||
+ logger.info(_("The hot patch '%s' has multiple target packages, please check."),
|
||||
+ self.base.output.term.bold(pkg_spec))
|
||||
+ continue
|
||||
+ target = "-".join([pkg_info["name"], pkg_info["version"], pkg_info["release"]])
|
||||
+ hp_target_map[pkg_spec] = target
|
||||
+ return hp_target_map
|
||||
+
|
||||
+ def _get_applied_old_patch(self, targets: list):
|
||||
+ """
|
||||
+ get targets' applied hot patches
|
||||
+ Args:
|
||||
+ targets: target RPMs' name-version-release. e.g. redis-1.0-1
|
||||
+
|
||||
+ Returns:
|
||||
+ dict: targets' applied hot patches. e.g. {'redis-1.0-1': 'redis-1.0-1/HP001'}
|
||||
+ """
|
||||
+ target_patch_map = {}
|
||||
+ hps_info = Syscare.list()
|
||||
+ for hp_info in hps_info:
|
||||
+ target, hp_name = hp_info["Name"].split('/')
|
||||
+ if target in targets and hp_info["Status"] != "NOT-APPLIED":
|
||||
+ logger.info(_("The target package '%s' has a hotpatch '%s' applied"),
|
||||
+ self.base.output.term.bold(target),
|
||||
+ self.base.output.term.bold(hp_name))
|
||||
+ target_patch_map[target] = hp_info["Name"]
|
||||
+ return target_patch_map
|
||||
+
|
||||
+ def _remove_hot_patches(self, target_patch_map: dict) -> None:
|
||||
+ output = Output(self.base, dnf.conf.Conf())
|
||||
+ logger.info(_("Gonna remove these hot patches: %s"), list(target_patch_map.values()))
|
||||
+ remove_flag = output.userconfirm()
|
||||
+ if not remove_flag:
|
||||
+ raise dnf.exceptions.Error(_('Operation aborted.'))
|
||||
+
|
||||
+ self.syscare.save()
|
||||
+ for target, hp_name in target_patch_map.items():
|
||||
+ logger.info(_("Remove hot patch %s."), hp_name)
|
||||
+ output, status = self.syscare.remove(hp_name)
|
||||
+ if status:
|
||||
+ logger.info(_("Remove hot patch '%s' failed, roll back to original status."),
|
||||
+ self.base.output.term.bold(hp_name))
|
||||
+ output, status = self.syscare.restore()
|
||||
+ if status:
|
||||
+ raise dnf.exceptions.Error(_('Roll back failed.'))
|
||||
+ raise dnf.exceptions.Error(_('Roll back succeed.'))
|
||||
+
|
||||
+ @staticmethod
|
||||
+ def _parse_hp_name(hp_filename: str) -> dict:
|
||||
+ """
|
||||
+ parse hot patch's name, get target rpm's name, version, release and hp's name.
|
||||
+ Args:
|
||||
+ hp_filename: hot patch's name, in the format of
|
||||
+ 'patch-{pkg_name}-{pkg_version}-{pkg_release}-{patchname}-{patch_version}-{patch_release}.rpm'
|
||||
+ e.g. patch-kernel-5.10.0-60.66.0.91.oe2203-HP001-1-1.x86_64.rpm
|
||||
+ pkg_name may have '-' in it, patch name cannot have '-'.
|
||||
+ Returns:
|
||||
+ dict: rpm info. {"name": "", "version": "", "release": "", "hp_name": ""}
|
||||
+ """
|
||||
+ splitted_hp_filename = hp_filename.split('-')
|
||||
+ try:
|
||||
+ rpm_info = {"release": splitted_hp_filename[-4], "version": splitted_hp_filename[-5],
|
||||
+ "name": "-".join(splitted_hp_filename[1:-5]), "hp_name": splitted_hp_filename[-3]}
|
||||
+ except IndexError as e:
|
||||
+ raise dnf.exceptions.Error(_('Parse hot patch name failed. Please insert correct hot patch name.'))
|
||||
+ return rpm_info
|
||||
+
|
||||
+ def _install_hot_patch(self, pkg_specs: list) -> bool:
|
||||
+ """
|
||||
+ install hot patches
|
||||
+ Args:
|
||||
+ pkg_specs: hot patches' full name
|
||||
+
|
||||
+ Returns:
|
||||
+ bool
|
||||
+ """
|
||||
+ success = True
|
||||
+ for pkg_spec in pkg_specs:
|
||||
+ try:
|
||||
+ self.base.install(pkg_spec)
|
||||
+ except dnf.exceptions.MarkingError as e:
|
||||
+ logger.info(_('No match for argument: %s.'),
|
||||
+ self.base.output.term.bold(pkg_spec))
|
||||
+ success = False
|
||||
+ return success
|
||||
+
|
||||
+ def get_hotpatch_based_on_cve(self, cves: list) -> list:
|
||||
+ """
|
||||
+ Get the hot patches corresponding to CVEs
|
||||
+ Args:
|
||||
+ cves: cve id list
|
||||
+
|
||||
+ Returns:
|
||||
+ list: list of hot patches full name. e.g.["tmp2-tss-3.1.0-3.oe2203sp1"]
|
||||
+ """
|
||||
+ updateinfo = HotpatchUpdateInfo(self.cli.base, self.cli)
|
||||
+ hp_list = []
|
||||
+ cve_hp_dict = updateinfo.get_hotpatches_from_cve(cves)
|
||||
+ for cve, hp in cve_hp_dict.items():
|
||||
+ if not hp:
|
||||
+ logger.info(_("The cve doesn't exist: %s"), cve)
|
||||
+ continue
|
||||
+ hp_list += hp
|
||||
+ return list(set(hp_list))
|
||||
+
|
||||
+ def get_hotpatch_based_on_advisory(self, advisories: list) -> list:
|
||||
+ """
|
||||
+ Get the hot patches corresponding to advisories
|
||||
+ Args:
|
||||
+ advisories: advisory id list
|
||||
+
|
||||
+ Returns:
|
||||
+ list: list of hot patches full name. e.g.["tmp2-tss-3.1.0-3.oe2203sp1"]
|
||||
+ """
|
||||
+ updateinfo = HotpatchUpdateInfo(self.cli.base, self.cli)
|
||||
+ hp_list = []
|
||||
+ advisory_hp_dict = updateinfo.get_hotpatches_from_advisories(advisories)
|
||||
+ for hp in advisory_hp_dict.values():
|
||||
+ hp_list += hp
|
||||
+ return list(set(hp_list))
|
||||
diff --git a/hotpatch/syscare.py b/hotpatch/syscare.py
|
||||
new file mode 100644
|
||||
index 0000000..b24b07e
|
||||
--- /dev/null
|
||||
+++ b/hotpatch/syscare.py
|
||||
@@ -0,0 +1,96 @@
|
||||
+import subprocess
|
||||
+from typing import List
|
||||
+
|
||||
+
|
||||
+SUCCEED = 0
|
||||
+FAIL = 255
|
||||
+
|
||||
+def cmd_output(cmd):
|
||||
+ try:
|
||||
+ result = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
+ result.wait()
|
||||
+ return result.stdout.read().decode('utf-8'), result.returncode
|
||||
+ except Exception as e:
|
||||
+ print("error: ", e)
|
||||
+ return str(e), FAIL
|
||||
+
|
||||
+class Syscare:
|
||||
+ @classmethod
|
||||
+ def list(cls, condition=None) -> List[dict]:
|
||||
+ """
|
||||
+ Target Name Status
|
||||
+ redis-6.2.5-1.oe2203 CVE-2021-23675 ACTIVED
|
||||
+ kernel-5.10.0-60.80.0.104.oe2203 modify-proc-version ACTIVED
|
||||
+ """
|
||||
+ cmd = ["syscare", "list"]
|
||||
+ list_output, return_code = cmd_output(cmd)
|
||||
+ if return_code != SUCCEED:
|
||||
+ return []
|
||||
+
|
||||
+ content = list_output.split('\n')
|
||||
+ if len(content) <= 2:
|
||||
+ return []
|
||||
+
|
||||
+ header = content[0].split()
|
||||
+ result = []
|
||||
+ for item in content[1:-1]:
|
||||
+ tmp = dict(zip(header, item.split()))
|
||||
+ if not condition or cls.judge(tmp, condition):
|
||||
+ result.append(tmp)
|
||||
+ return result
|
||||
+
|
||||
+ @staticmethod
|
||||
+ def judge(content: dict, condition: dict):
|
||||
+ for key, value in condition.items():
|
||||
+ if content.get(key) != value:
|
||||
+ return False
|
||||
+ return True
|
||||
+
|
||||
+ @staticmethod
|
||||
+ def status(patch_name: str):
|
||||
+ cmd = ["syscare", "status", patch_name]
|
||||
+ output, return_code = cmd_output(cmd)
|
||||
+
|
||||
+ return output, return_code
|
||||
+
|
||||
+ @staticmethod
|
||||
+ def active(patch_name: str):
|
||||
+ cmd = ["syscare", "active", patch_name]
|
||||
+ output, return_code = cmd_output(cmd)
|
||||
+
|
||||
+ return output, return_code
|
||||
+
|
||||
+ @staticmethod
|
||||
+ def deactive(patch_name: str):
|
||||
+ cmd = ["syscare", "deactive", patch_name]
|
||||
+ output, return_code = cmd_output(cmd)
|
||||
+
|
||||
+ return output, return_code
|
||||
+
|
||||
+ @staticmethod
|
||||
+ def remove(patch_name: str):
|
||||
+ cmd = ["syscare", "remove", patch_name]
|
||||
+ output, return_code = cmd_output(cmd)
|
||||
+
|
||||
+ return output, return_code
|
||||
+
|
||||
+ @staticmethod
|
||||
+ def apply(patch_name: str):
|
||||
+ cmd = ["syscare", "apply", patch_name]
|
||||
+ output, return_code = cmd_output(cmd)
|
||||
+
|
||||
+ return output, return_code
|
||||
+
|
||||
+ @staticmethod
|
||||
+ def save():
|
||||
+ cmd = ["syscare", "save"]
|
||||
+ output, return_code = cmd_output(cmd)
|
||||
+
|
||||
+ return output, return_code
|
||||
+
|
||||
+ @staticmethod
|
||||
+ def restore():
|
||||
+ cmd = ["syscare", "restore"]
|
||||
+ output, return_code = cmd_output(cmd)
|
||||
+
|
||||
+ return output, return_code
|
||||
\ No newline at end of file
|
||||
--
|
||||
2.30.0
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
From a1ccf84bf28323ce613e0c1d1819c31d0d6cd13b Mon Sep 17 00:00:00 2001
|
||||
From: zhu-yuncheng <zhuyuncheng@huawei.com>
|
||||
Date: Mon, 27 Mar 2023 23:45:18 +0800
|
||||
Subject: [PATCH] better hotupgrade command output
|
||||
|
||||
---
|
||||
hotpatch/hot-upgrade.py | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/hotpatch/hot-upgrade.py b/hotpatch/hot-upgrade.py
|
||||
index 7a4c3c6..06f32ad 100644
|
||||
--- a/hotpatch/hot-upgrade.py
|
||||
+++ b/hotpatch/hot-upgrade.py
|
||||
@@ -253,7 +253,7 @@ class HotupgradeCommand(dnf.cli.Command):
|
||||
cve_hp_dict = updateinfo.get_hotpatches_from_cve(cves)
|
||||
for cve, hp in cve_hp_dict.items():
|
||||
if not hp:
|
||||
- logger.info(_("The cve doesn't exist: %s"), cve)
|
||||
+ logger.info(_("The cve's hot patch doesn't exist: %s"), cve)
|
||||
continue
|
||||
hp_list += hp
|
||||
return list(set(hp_list))
|
||||
--
|
||||
2.30.0
|
||||
|
||||
Binary file not shown.
BIN
aops-apollo-v1.2.0.tar.gz
Normal file
BIN
aops-apollo-v1.2.0.tar.gz
Normal file
Binary file not shown.
@ -1,20 +1,17 @@
|
||||
Name: aops-apollo
|
||||
Version: v1.1.2
|
||||
Release: 7
|
||||
Version: v1.2.0
|
||||
Release: 1
|
||||
Summary: Cve management service, monitor machine vulnerabilities and provide fix functions.
|
||||
License: MulanPSL2
|
||||
URL: https://gitee.com/openeuler/%{name}
|
||||
Source0: %{name}-%{version}.tar.gz
|
||||
Patch0001: 0001-fix-partial-succeed-bug.patch
|
||||
Patch0002: 0002-add-dnf-hot-patch-list-plugin.patch
|
||||
Patch0003: 0003-add-dnf-hot-upgrade-plugin.patch
|
||||
Patch0004: 0004-better-hotupgrade-command-output.patch
|
||||
|
||||
BuildRequires: python3-setuptools
|
||||
Requires: aops-vulcanus >= v1.0.0
|
||||
Requires: aops-vulcanus >= v1.2.0
|
||||
Requires: python3-elasticsearch python3-flask-restful python3-marshmallow >= 3.13.0
|
||||
Requires: python3-sqlalchemy python3-PyMySQL python3-Flask-APScheduler >= 1.11.0
|
||||
Requires: python3-PyYAML python3-flask
|
||||
Requires: python3-retrying python3-lxml
|
||||
Provides: aops-apollo
|
||||
|
||||
|
||||
@ -29,7 +26,8 @@ Requires: python3-hawkey python3-dnf syscare
|
||||
dnf hotpatch plugin, it's about hotpatch query and fix
|
||||
|
||||
%prep
|
||||
%autosetup -n %{name}-%{version} -p1
|
||||
%autosetup -n %{name}-%{version}
|
||||
|
||||
|
||||
# build for aops-apollo
|
||||
%py3_build
|
||||
@ -41,10 +39,10 @@ dnf hotpatch plugin, it's about hotpatch query and fix
|
||||
#install for aops-dnf-plugin
|
||||
cp -r hotpatch %{buildroot}/%{python3_sitelib}/dnf-plugins/
|
||||
|
||||
|
||||
%files
|
||||
%doc README.*
|
||||
%attr(0644,root,root) %{_sysconfdir}/aops/apollo.ini
|
||||
%attr(0644,root,root) %{_sysconfdir}/aops/apollo_crontab.ini
|
||||
%attr(0755,root,root) %{_bindir}/aops-apollo
|
||||
%attr(0755,root,root) /usr/lib/systemd/system/aops-apollo.service
|
||||
%{python3_sitelib}/aops_apollo*.egg-info
|
||||
@ -53,25 +51,18 @@ cp -r hotpatch %{buildroot}/%{python3_sitelib}/dnf-plugins/
|
||||
%files -n dnf-hotpatch-plugin
|
||||
%{python3_sitelib}/dnf-plugins/*
|
||||
|
||||
|
||||
%changelog
|
||||
* Mon Mar 27 2023 zhu-yuncheng<zhuyuncheng@huawei.com> - v1.1.2-7
|
||||
- better hotupgrade command output when cve exists but hot patch doesn't
|
||||
|
||||
* Sat Mar 25 2023 wangguangge<wangguangge@huawei.com> - v1.1.2-6
|
||||
- fix baseclass.py bug and add syscare require in spec
|
||||
|
||||
* Sat Mar 25 2023 zhu-yuncheng<zhuyuncheng@huawei.com> - v1.1.2-5
|
||||
- add dnf hot upgrade plugin
|
||||
|
||||
* Fri Mar 24 2023 wangguangge<wangguangge@huawei.com> - v1.1.2-4
|
||||
* Mon Apr 17 2023 gongzhengtang<gong_zhengtang@163.com> - v1.2.0-1
|
||||
- add updated security advisory at regular time
|
||||
- add execute the CVE scan command at regular time
|
||||
- add correct abnormal data at regular time
|
||||
- add dnf hotpatch list plugin
|
||||
|
||||
* Tue Dec 27 2022 wenxin<shusheng.wen@outlook.com> - v1.1.2-3
|
||||
- modify version for vulcanus
|
||||
|
||||
* Thu Dec 15 2022 ptyang<1475324955@qq.com> - v1.1.2-2
|
||||
- fix "PARTIAL_SUCCEED" bug
|
||||
- fix "PARTIAL_SUCCEED" bug
|
||||
|
||||
* Wed Dec 07 2022 wenxin<shusheng.wen@outlook.com> - v1.1.2-1
|
||||
- modify status code for upload security advisories;fix cve query error
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user