From c3ef72a75659003fa04491b0efe14800c4f14763 Mon Sep 17 00:00:00 2001 From: Jan Dobes Date: Tue, 15 Sep 2026 11:06:05 +0200 Subject: [PATCH] feat(manager): affected packages API RHINENG-28514 --- common/feature_flags.py | 1 + common/peewee_model.py | 34 +++++++++++++ develfeatureflags.json | 14 +++++ manager.spec.yaml | 55 ++++++++++++++++++++ manager/system_handler.py | 104 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 208 insertions(+) diff --git a/common/feature_flags.py b/common/feature_flags.py index da7c3c5af..2ba455cee 100644 --- a/common/feature_flags.py +++ b/common/feature_flags.py @@ -22,6 +22,7 @@ OS_EXPOSURE_REPORT_FEATURE = "vulnerability.os_exposure_report" NEW_NOTIFICATIONS_FEATURE = "vulnerability.new_notifications" TIMING_LOG_FEATURE = "vulnerability.timing_log" +API_AFFECTED_PACKAGES_FEATURE = "vulnerability.api_affected_packages" class UnleashClientProxy: diff --git a/common/peewee_model.py b/common/peewee_model.py index 51eda3cd6..6ed1dbb38 100644 --- a/common/peewee_model.py +++ b/common/peewee_model.py @@ -494,6 +494,20 @@ class Meta: table_name = "package_name" +class Evr(BaseModel): + """evr table""" + + id = AutoField() + epoch = IntegerField(null=False) + version = TextField(null=False) + release = TextField(null=False) + + class Meta: + """evr table metadata""" + + table_name = "evr" + + class CPE(BaseModel): """cpe table""" @@ -539,6 +553,8 @@ class SystemVulnerablePackage(BaseModel): rh_account_id = ForeignKeyField(column_name="rh_account_id", model=RHAccount, field="id") system_id = ForeignKeyField(column_name="system_id", model=SystemPlatform, field="id") vulnerable_package_id = ForeignKeyField(column_name="vulnerable_package_id", model=VulnerablePackage, field="id") + affected_evr_id = ForeignKeyField(column_name="affected_evr_id", model=Evr, field="id", null=True) + affected_arch = TextField(null=True) first_reported = DateTimeField(null=False) class Meta: @@ -547,6 +563,24 @@ class Meta: table_name = "system_vulnerable_package" +class SystemVulnerabilitiesPackageDetail(BaseModel): + """system_vulnerabilities_pkg_detail table""" + + rh_account_id = ForeignKeyField(column_name="rh_account_id", model=RHAccount, field="id") + system_vulnerability_id = ForeignKeyField(column_name="system_vulnerability_id", model=SystemVulnerabilities, field="id") + package_name_id = ForeignKeyField(column_name="package_name_id", model=PackageName, field="id") + affected_evr_id = ForeignKeyField(column_name="affected_evr_id", model=Evr, field="id") + affected_arch = TextField(null=False) + fixed_evr_id = ForeignKeyField(column_name="fixed_evr_id", model=Evr, field="id", null=True) + fixed_arch = TextField(null=True) + + class Meta: + """system_vulnerabilities_pkg_detail table metadata""" + + table_name = "system_vulnerabilities_pkg_detail" + primary_key = False + + class SystemCveData(BaseModelRW): """system cve data""" diff --git a/develfeatureflags.json b/develfeatureflags.json index b7c00538b..45f72f2d0 100644 --- a/develfeatureflags.json +++ b/develfeatureflags.json @@ -56,6 +56,20 @@ ], "strategy": "default", "parameters": {} + }, + { + "name": "vulnerability.api_affected_packages", + "type": "release", + "enabled": true, + "stale": false, + "strategies": [ + { + "name": "default", + "parameters": {} + } + ], + "strategy": "default", + "parameters": {} } ] } diff --git a/manager.spec.yaml b/manager.spec.yaml index 9ed89dfbd..bf80453d6 100644 --- a/manager.spec.yaml +++ b/manager.spec.yaml @@ -634,6 +634,28 @@ paths: - $ref: '#/components/parameters/report' - $ref: '#/components/parameters/tags' + /systems/{inventory_id}/cves/{cve_id}: + get: + summary: Get affected and fixed packages for a system CVE + description: > + This endpoint lists package versions affected by a given CVE on a system, + including the fixed version when one is available. + operationId: manager.system_handler.GetSystemCvePackages.get + x-methodName: getAffectedPackagesBySystemCve + security: + - ApiKeyAuth: [] + - BasicAuth: [] + responses: + 200: + description: Affected and fixed package information. + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/SystemCvePackagesOut' + parameters: + - $ref: '#/components/parameters/inventory_id' + - $ref: '#/components/parameters/cve_id' + /systems/{inventory_id}/cves/ids: get: summary: Get a CVE identification number report for a system @@ -3336,6 +3358,39 @@ components: - links - meta + SystemCvePackagesOut: + type: object + properties: + data: + type: array + description: Packages affected by the CVE on the system. + items: + type: object + properties: + package_name: + type: string + description: Package name. + example: kernel + affected_evra: + type: string + description: Installed affected epoch, version, release, and architecture. + example: '0:1.0-1.x86_64' + fixed_evra: + type: string + description: Fixed epoch, version, release, and architecture, when available. + example: '0:1.0-2.x86_64' + nullable: true + required: + - package_name + - affected_evra + - fixed_evra + meta: + type: object + description: Response metadata. + required: + - data + - meta + SystemCvesIdsOut: type: object properties: diff --git a/manager/system_handler.py b/manager/system_handler.py index e7ae38713..8f7b76040 100644 --- a/manager/system_handler.py +++ b/manager/system_handler.py @@ -20,6 +20,8 @@ from common.config import Config from common.constants import HostType from common.constants import remediation +from common.feature_flags import API_AFFECTED_PACKAGES_FEATURE +from common.feature_flags import UNLEASH from common.logging import get_logger from common.peewee_conditions import system_is_active from common.peewee_conditions import system_is_vulnerable @@ -28,15 +30,19 @@ from common.peewee_model import CveAccountData from common.peewee_model import CveImpact from common.peewee_model import CveMetadata +from common.peewee_model import Evr from common.peewee_model import InsightsRule from common.peewee_model import OperatingSystem +from common.peewee_model import PackageName from common.peewee_model import RHAccount from common.peewee_model import SystemCveData from common.peewee_model import SystemGroupSet from common.peewee_model import SystemPlatform from common.peewee_model import SystemTagSet from common.peewee_model import SystemVulnerabilities +from common.peewee_model import SystemVulnerabilitiesPackageDetail from common.peewee_model import SystemVulnerablePackage +from common.peewee_model import VulnerablePackage from common.peewee_model import VulnerablePackageCVE from .base import CVE_SYNOPSIS_SORT @@ -807,6 +813,104 @@ class GetSystemsCvesIds(GetSystemsCves): _ids_only = True +class GetSystemCvePackages(GetRequest): + """GET to /v1/systems/{inventory_id}/cves/{cve_id}""" + + _endpoint_name = r"/v1/systems/{inventory_id}/cves/{cve_id}" + + @staticmethod + def _format_evra(evr, arch): + """Format separate epoch, version, release, and architecture columns as EVRA.""" + return fn.CONCAT(evr.epoch, ":", evr.version, "-", evr.release, ".", arch) + + @classmethod + @RBAC.need_permissions(RbacRoutePermissions.VULNERABILITY_RESULTS, allow_system_auth=True) + @RBAC.need_permissions_filter_value(RbacFilterRoutePermissions.REPORTABLE_ENDPOINTS) + @RBACv2.enforce_workspace_permission(RbacRoutePermissions.VULNERABILITY_RESULTS, allow_system_auth=True) + @RBACv2.enforce_workspace_permission_filter_value(RbacFilterRoutePermissions.REPORTABLE_ENDPOINTS) + def handle_get(cls, **kwargs): + """Return affected and fixed package details for one system-CVE pair.""" + if not UNLEASH.is_enabled(API_AFFECTED_PACKAGES_FEATURE): + return cls.format_exception("Access to this endpoint is not allowed.", 403) + + rh_account_id = get_account_data(context.context["user"]).id + fixed_evr = Evr.alias("fixed_evr") + fixed_query = ( + SystemVulnerabilitiesPackageDetail.select( + PackageName.name.alias("package_name"), + cls._format_evra(Evr, SystemVulnerabilitiesPackageDetail.affected_arch).alias("affected_evra"), + Case( + None, + ( + ( + (SystemVulnerabilitiesPackageDetail.fixed_evr_id.is_null(False)) + & (SystemVulnerabilitiesPackageDetail.fixed_arch.is_null(False)), + cls._format_evra(fixed_evr, SystemVulnerabilitiesPackageDetail.fixed_arch), + ), + ), + Value(None).cast("text"), + ).alias("fixed_evra"), + ) + .join( + SystemVulnerabilities, + on=(SystemVulnerabilitiesPackageDetail.system_vulnerability_id == SystemVulnerabilities.id), + ) + .join( + SystemPlatform, + on=(SystemVulnerabilities.system_id == SystemPlatform.id) & system_is_active(image=None, stale=None), + ) + .join(CveMetadata, on=(SystemVulnerabilities.cve_id == CveMetadata.id)) + .switch(SystemVulnerabilitiesPackageDetail) + .join(PackageName, on=(SystemVulnerabilitiesPackageDetail.package_name_id == PackageName.id)) + .switch(SystemVulnerabilitiesPackageDetail) + .join(Evr, on=(SystemVulnerabilitiesPackageDetail.affected_evr_id == Evr.id)) + .switch(SystemVulnerabilitiesPackageDetail) + .join( + fixed_evr, + JOIN.LEFT_OUTER, + on=(SystemVulnerabilitiesPackageDetail.fixed_evr_id == fixed_evr.id), + ) + .where(SystemVulnerabilitiesPackageDetail.rh_account_id == rh_account_id) + .where(SystemVulnerabilities.rh_account_id == rh_account_id) + .where(SystemPlatform.inventory_id == kwargs["inventory_id"]) + .where(CveMetadata.cve == kwargs["cve_id"]) + ) + + unfixed_query = ( + SystemVulnerablePackage.select( + PackageName.name.alias("package_name"), + cls._format_evra(Evr, SystemVulnerablePackage.affected_arch).alias("affected_evra"), + Value(None).cast("text").alias("fixed_evra"), + ) + .join( + SystemPlatform, + on=(SystemVulnerablePackage.system_id == SystemPlatform.id) & system_is_active(image=None, stale=None), + ) + .switch(SystemVulnerablePackage) + .join(VulnerablePackage, on=(SystemVulnerablePackage.vulnerable_package_id == VulnerablePackage.id)) + .join(VulnerablePackageCVE, on=(VulnerablePackage.id == VulnerablePackageCVE.vulnerable_package_id)) + .join(CveMetadata, on=(VulnerablePackageCVE.cve_id == CveMetadata.id)) + .switch(VulnerablePackage) + .join(PackageName, on=(VulnerablePackage.package_name_id == PackageName.id)) + .switch(SystemVulnerablePackage) + .join(Evr, on=(SystemVulnerablePackage.affected_evr_id == Evr.id)) + .where(SystemVulnerablePackage.rh_account_id == rh_account_id) + .where(SystemPlatform.inventory_id == kwargs["inventory_id"]) + .where(CveMetadata.cve == kwargs["cve_id"]) + .where(SystemVulnerablePackage.affected_evr_id.is_null(False)) + .where(SystemVulnerablePackage.affected_arch.is_null(False)) + ) + + if context.context["user"]["identity_type"] == "System": + system_cn = context.context["user"]["system_cn"] + fixed_query = fixed_query.where(SystemPlatform.owner_id == system_cn) + unfixed_query = unfixed_query.where(SystemPlatform.owner_id == system_cn) + + fixed_query = SystemGroupSet_join(fixed_query) + unfixed_query = SystemGroupSet_join(unfixed_query) + return {"data": list((fixed_query | unfixed_query).dicts()), "meta": {}} + + class GetSystemDetails(GetRequest): """GET to /systems/{inventory_id}"""