Support updateinfo output

This commit is contained in:
Mustafa Gezen 2023-02-03 00:27:32 +01:00
parent 15c4ca576d
commit ea2c37f66a
Signed by untrusted user who does not match committer: mustafa
GPG Key ID: DCDF010D946438C1
7 changed files with 289 additions and 5 deletions

View File

@ -13,6 +13,10 @@ from common.logger import Logger
NVRA_RE = re.compile( NVRA_RE = re.compile(
r"^(\S+)-([\w~%.+]+)-(\w+(?:\.[\w~%+]+)+?)(?:\.(\w+))?(?:\.rpm)?$" r"^(\S+)-([\w~%.+]+)-(\w+(?:\.[\w~%+]+)+?)(?:\.(\w+))?(?:\.rpm)?$"
) )
NEVRA_RE = re.compile(
r"^(\S+)-(\d):([\w~%.+]+)-(\w+(?:\.[\w~%+]+)+?)(?:\.(\w+))?(?:\.rpm)?$"
)
EPOCH_RE = re.compile(r"(\d+):")
DIST_RE = re.compile(r"(\.el\d(?:_\d|))") DIST_RE = re.compile(r"(\.el\d(?:_\d|))")
MODULE_DIST_RE = re.compile(r"\.module.+$") MODULE_DIST_RE = re.compile(r"\.module.+$")

View File

@ -10,6 +10,7 @@ py_library(
"routes/api_advisories.py", "routes/api_advisories.py",
"routes/api_compat.py", "routes/api_compat.py",
"routes/api_red_hat.py", "routes/api_red_hat.py",
"routes/api_updateinfo.py",
"routes/login.py", "routes/login.py",
"routes/logout.py", "routes/logout.py",
"routes/red_hat_advisories.py", "routes/red_hat_advisories.py",
@ -28,13 +29,16 @@ py_library(
deps = [ deps = [
"//apollo/db:db_lib", "//apollo/db:db_lib",
"//apollo/db/serialize:serialize_lib", "//apollo/db/serialize:serialize_lib",
"//apollo/rpmworker:rpmworker_lib",
"//common:common_lib", "//common:common_lib",
"@pypi_fastapi//:pkg", "@pypi_fastapi//:pkg",
"@pypi_fastapi_pagination//:pkg", "@pypi_fastapi_pagination//:pkg",
"@pypi_itsdangerous//:pkg", "@pypi_itsdangerous//:pkg",
"@pypi_jinja2//:pkg", "@pypi_jinja2//:pkg",
"@pypi_passlib//:pkg", "@pypi_passlib//:pkg",
"@pypi_pydantic//:pkg",
"@pypi_python_multipart//:pkg", "@pypi_python_multipart//:pkg",
"@pypi_python_slugify//:pkg",
"@pypi_rssgen//:pkg", "@pypi_rssgen//:pkg",
"@pypi_starlette//:pkg", "@pypi_starlette//:pkg",
"@pypi_tortoise_orm//:pkg", "@pypi_tortoise_orm//:pkg",

View File

@ -0,0 +1,261 @@
import datetime
from typing import Optional
from xml.etree import ElementTree as ET
from fastapi import APIRouter, Response
from slugify import slugify
from apollo.db import AdvisoryAffectedProduct
from apollo.server.settings import COMPANY_NAME, MANAGING_EDITOR, UI_URL, get_setting
from apollo.rpmworker.repomd import NEVRA_RE, NVRA_RE, EPOCH_RE
from common.fastapi import RenderErrorTemplateException
router = APIRouter(tags=["updateinfo"])
@router.get("/{product_name}/{repo}/updateinfo.xml")
async def get_updateinfo(
product_name: str,
repo: str,
req_arch: Optional[str] = None,
):
filters = {
"name": product_name,
"advisory__packages__repo_name": repo,
}
if req_arch:
filters["arch"] = req_arch
affected_products = await AdvisoryAffectedProduct.filter(
**filters
).prefetch_related(
"advisory",
"advisory__cves",
"advisory__fixes",
"advisory__packages",
"supported_product",
).all()
if not affected_products:
raise RenderErrorTemplateException("No advisories found", 404)
ui_url = await get_setting(UI_URL)
managing_editor = await get_setting(MANAGING_EDITOR)
company_name = await get_setting(COMPANY_NAME)
advisories = {}
for affected_product in affected_products:
advisory = affected_product.advisory
if advisory.name not in advisories:
advisories[advisory.name] = {
"advisory":
advisory,
"arch":
affected_product.arch,
"major_version":
affected_product.major_version,
"minor_version":
affected_product.minor_version,
"supported_product_name":
affected_product.supported_product.name,
}
tree = ET.Element("updates")
for _, adv in advisories.items():
advisory = adv["advisory"]
product_arch = adv["arch"]
major_version = adv["major_version"]
minor_version = adv["minor_version"]
supported_product_name = adv["supported_product_name"]
update = ET.SubElement(tree, "update")
# Set update attributes
update.set("from", managing_editor)
update.set("status", "final")
if advisory.kind == "Security":
update.set("type", "security")
elif advisory.kind == "Bug Fix":
update.set("type", "bugfix")
elif advisory.kind == "Enhancement":
update.set("type", "enhancement")
update.set("version", "2")
# Add id
ET.SubElement(update, "id").text = advisory.name
# Add title
ET.SubElement(update, "title").text = advisory.synopsis
# Add description
ET.SubElement(update, "description").text = advisory.description
# Add time
time_format = "%Y-%m-%d %H:%M:%S"
ET.SubElement(update, "issued"
).text = advisory.published_at.strftime(time_format)
ET.SubElement(update, "updated"
).text = advisory.updated_at.strftime(time_format)
# Add rights
now = datetime.datetime.utcnow()
ET.SubElement(
update, "rights"
).text = f"Copyright {now.year} {company_name}"
# Add release name
release_name = f"{supported_product_name} {major_version}"
if minor_version:
release_name += f".{minor_version}"
ET.SubElement(update, "release").text = release_name
# Add pushcount
ET.SubElement(update, "pushcount").text = "1"
# Add severity
ET.SubElement(update, "severity").text = advisory.severity
# Add summary
ET.SubElement(update, "summary").text = advisory.topic
# Add description
ET.SubElement(update, "description").text = advisory.description
# Add solution
ET.SubElement(update, "solution").text = ""
# Add references
references = ET.SubElement(update, "references")
for cve in advisory.cves:
reference = ET.SubElement(references, "reference")
reference.set(
"href",
f"https://cve.mitre.org/cgi-bin/cvename.cgi?name={cve.cve}",
)
reference.set("id", cve.cve)
reference.set("type", "cve")
reference.set("title", cve.cve)
for fix in advisory.fixes:
reference = ET.SubElement(references, "reference")
reference.set("href", fix.source)
reference.set("id", fix.ticket_id)
reference.set("type", "bugzilla")
reference.set("title", fix.description)
# Add UI self reference
reference = ET.SubElement(references, "reference")
reference.set("href", f"{ui_url}/{advisory.name}")
reference.set("id", advisory.name)
reference.set("type", "self")
reference.set("title", advisory.name)
# Add packages
packages = ET.SubElement(update, "pkglist")
# Create collection
collection = ET.SubElement(packages, "collection")
collection_short = slugify(f"{product_name}-{repo}-rpms")
collection.set("short", collection_short)
# Set short to name as well
ET.SubElement(collection, "name").text = collection_short
pkg_name_map = {}
for pkg in advisory.packages:
if pkg.package_name not in pkg_name_map:
pkg_name_map[pkg.package_name] = []
pkg_name_map[pkg.package_name].append(pkg)
pkg_src_rpm = {}
for top_pkg in advisory.packages:
if top_pkg.package_name not in pkg_src_rpm:
top_nvra_no_epoch = EPOCH_RE.sub("", top_pkg.nevra)
top_nvra = NVRA_RE.search(top_nvra_no_epoch)
top_arch = top_nvra.group(4)
for pkg in pkg_name_map[top_pkg.package_name]:
nvra_no_epoch = EPOCH_RE.sub("", pkg.nevra)
nvra = NVRA_RE.search(nvra_no_epoch)
if nvra:
name = nvra.group(1)
arch = nvra.group(4)
if pkg.package_name == name and top_arch == arch:
src_rpm = nvra_no_epoch
if not src_rpm.endswith(".rpm"):
src_rpm += ".rpm"
pkg_src_rpm[pkg.package_name] = src_rpm
# If we encounter modules, we need to add them to the collection later
modules = {}
for pkg in advisory.packages:
if pkg.nevra.endswith(".src.rpm"):
continue
name = pkg.package_name
epoch = "0"
if NEVRA_RE.match(pkg.nevra):
nevra = NEVRA_RE.search(pkg.nevra)
name = nevra.group(1)
epoch = nevra.group(2)
version = nevra.group(3)
release = nevra.group(4)
arch = nevra.group(5)
elif NVRA_RE.match(pkg.nevra):
nvra = NVRA_RE.search(pkg.nevra)
name = nvra.group(1)
version = nvra.group(2)
release = nvra.group(3)
arch = nvra.group(4)
else:
continue
if pkg.package_name not in pkg_src_rpm:
continue
package = ET.SubElement(collection, "package")
package.set("name", name)
package.set("arch", arch)
package.set("epoch", epoch)
package.set("version", version)
package.set("release", release)
package.set("src", pkg_src_rpm[pkg.package_name])
# Add filename element
ET.SubElement(package,
"filename").text = EPOCH_RE.sub("", pkg.nevra)
# Add checksum
ET.SubElement(
package, "sum", type=pkg.checksum_type
).text = pkg.checksum
# Check if module
if pkg.module_name:
modules[pkg.module_name] = {
"name": pkg.module_name,
"context": pkg.module_context,
"stream": pkg.module_stream,
"version": pkg.module_version,
"arch": product_arch,
}
# Add modules
for module in modules.values():
module_element = ET.Element("module")
module_element.set("name", module["name"])
module_element.set("stream", module["stream"])
module_element.set("version", module["version"])
module_element.set("context", module["context"])
module_element.set("arch", module["arch"])
collection.insert(1, module_element)
ET.indent(tree)
xml_str = ET.tostring(tree, encoding="unicode", method="xml")
return Response(content=xml_str, media_type="application/xml")

View File

@ -9,14 +9,15 @@ from fastapi.responses import JSONResponse
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
from fastapi_pagination import add_pagination from fastapi_pagination import add_pagination
from apollo.server.routes.advisories import router as advisories_router
from apollo.server.routes.statistics import router as statistics_router from apollo.server.routes.statistics import router as statistics_router
from apollo.server.routes.login import router as login_router from apollo.server.routes.login import router as login_router
from apollo.server.routes.logout import router as logout_router from apollo.server.routes.logout import router as logout_router
from apollo.server.routes.admin_index import router as admin_index_router from apollo.server.routes.admin_index import router as admin_index_router
from apollo.server.routes.api_advisories import router as api_advisories_router from apollo.server.routes.api_advisories import router as api_advisories_router
from apollo.server.routes.api_compat import router as api_compat_router from apollo.server.routes.api_updateinfo import router as api_updateinfo_router
from apollo.server.routes.api_red_hat import router as api_red_hat_router from apollo.server.routes.api_red_hat import router as api_red_hat_router
from apollo.server.routes.advisories import router as advisories_router from apollo.server.routes.api_compat import router as api_compat_router
from apollo.server.routes.red_hat_advisories import router as red_hat_advisories_router from apollo.server.routes.red_hat_advisories import router as red_hat_advisories_router
from apollo.server.settings import SECRET_KEY, SettingsMiddleware, get_setting from apollo.server.settings import SECRET_KEY, SettingsMiddleware, get_setting
from apollo.server.utils import admin_user_scheme, templates from apollo.server.utils import admin_user_scheme, templates
@ -49,8 +50,9 @@ app.include_router(
) )
app.include_router(red_hat_advisories_router, prefix="/red_hat") app.include_router(red_hat_advisories_router, prefix="/red_hat")
app.include_router(api_advisories_router, prefix="/api/v3/advisories") app.include_router(api_advisories_router, prefix="/api/v3/advisories")
app.include_router(api_compat_router, prefix="/v2/advisories") app.include_router(api_updateinfo_router, prefix="/api/v3/updateinfo")
app.include_router(api_red_hat_router, prefix="/api/v3/red_hat") app.include_router(api_red_hat_router, prefix="/api/v3/red_hat")
app.include_router(api_compat_router, prefix="/v2/advisories")
add_pagination(app) add_pagination(app)

View File

@ -1128,6 +1128,9 @@ manifest:
shellingham.posix.proc: shellingham shellingham.posix.proc: shellingham
shellingham.posix.ps: shellingham shellingham.posix.ps: shellingham
six: six six: six
slugify: python_slugify
slugify.slugify: python_slugify
slugify.special: python_slugify
sniffio: sniffio sniffio: sniffio
soupsieve: soupsieve soupsieve: soupsieve
soupsieve.css_match: soupsieve soupsieve.css_match: soupsieve
@ -1303,6 +1306,7 @@ manifest:
temporalio.worker.workflow_sandbox: temporalio temporalio.worker.workflow_sandbox: temporalio
temporalio.workflow: temporalio temporalio.workflow: temporalio
test_autoflake: autoflake test_autoflake: autoflake
text_unidecode: text_unidecode
toml: toml toml: toml
toml.decoder: toml toml.decoder: toml
toml.encoder: toml toml.encoder: toml
@ -1503,4 +1507,4 @@ manifest:
yarl: yarl yarl: yarl
pip_repository: pip_repository:
name: pypi name: pypi
integrity: 98955591e0f143193fb26aa58a3c5c9120c83f3270459709acd849f9613119ac integrity: 8d848d11c467949c981e296a23211f42a009566f0d2ab5fe11ceb59ade4cb74e

View File

@ -17,4 +17,5 @@ python-multipart==0.0.5
itsdangerous==2.1.2 itsdangerous==2.1.2
PyYAML==6.0 PyYAML==6.0
beautifulsoup4==4.11.2 beautifulsoup4==4.11.2
rssgen==0.9.0 rssgen==0.9.0
python-slugify==8.0.0

View File

@ -653,6 +653,10 @@ python-dateutil==2.8.2 \
python-multipart==0.0.5 \ python-multipart==0.0.5 \
--hash=sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43 --hash=sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43
# via -r ./requirements.txt # via -r ./requirements.txt
python-slugify==8.0.0 \
--hash=sha256:51f217508df20a6c166c7821683384b998560adcf8f19a6c2ca8b460528ccd9c \
--hash=sha256:f1da83f3c7ab839b3f84543470cd95bdb5a81f1a0b80fed502f78b7dca256062
# via -r ./requirements.txt
pytz==2022.7.1 \ pytz==2022.7.1 \
--hash=sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0 \ --hash=sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0 \
--hash=sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a --hash=sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a
@ -746,6 +750,10 @@ temporalio==1.0.0 \
--hash=sha256:7c82a875c3db9ab2c8492ddc01498dbb2636cad34cf8bc985a6f0f17bd627f99 \ --hash=sha256:7c82a875c3db9ab2c8492ddc01498dbb2636cad34cf8bc985a6f0f17bd627f99 \
--hash=sha256:b2454ef6b68335a554adca1e4f14831b5c3ea33ef8adb25742dd91652bd38a82 --hash=sha256:b2454ef6b68335a554adca1e4f14831b5c3ea33ef8adb25742dd91652bd38a82
# via -r ./requirements.txt # via -r ./requirements.txt
text-unidecode==1.3 \
--hash=sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8 \
--hash=sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93
# via python-slugify
toml==0.10.2 \ toml==0.10.2 \
--hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
--hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f