From feec6318e60d52ba4f7c36f272e65b23b2925283 Mon Sep 17 00:00:00 2001 From: Bryan Ramos Date: Thu, 28 May 2026 14:01:33 -0400 Subject: [PATCH 1/4] feat(advisory): add CSAF 2.0 types, query hook, and ECharts dependency Define TypeScript interfaces for the full CSAF 2.0 VEX document structure (CsafDocument, CsafVulnerability, CsafProductTree, etc.) and add useFetchAdvisoryCsafById query hook that fetches raw CSAF JSON via the downloadAdvisory endpoint and parses the Blob into typed data. Also add echarts and echarts-for-react dependencies for upcoming tree visualization components. Implements TC-4618 Assisted-by: Claude Code --- client/package.json | 2 + client/src/app/queries/advisories.ts | 21 ++ client/src/app/types/csaf.ts | 290 +++++++++++++++++++++++++++ package-lock.json | 54 ++++- 4 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 client/src/app/types/csaf.ts diff --git a/client/package.json b/client/package.json index 5dbb09de7..8dc4695ca 100644 --- a/client/package.json +++ b/client/package.json @@ -30,6 +30,8 @@ "@tanstack/react-query-devtools": "^5.61.0", "axios": "^1.16.0", "dayjs": "^1.11.18", + "echarts": "^5.6.0", + "echarts-for-react": "^3.0.2", "file-saver": "^2.0.5", "oidc-client-ts": "^3.3.0", "packageurl-js": "^2.0.1", diff --git a/client/src/app/queries/advisories.ts b/client/src/app/queries/advisories.ts index a2bebe849..709487d89 100644 --- a/client/src/app/queries/advisories.ts +++ b/client/src/app/queries/advisories.ts @@ -18,6 +18,7 @@ import { listAdvisoryLabels, updateAdvisoryLabels, } from "@app/client"; +import type { CsafDocument } from "@app/types/csaf"; import { uploadAdvisory } from "@app/api/rest"; import { @@ -141,6 +142,26 @@ export const useFetchAdvisorySourceById = (id: string) => { }; }; +/** Fetches the raw CSAF JSON document and parses it into a typed CsafDocument. */ +export const useFetchAdvisoryCsafById = (id: string) => { + const { data, isLoading, error } = useQuery({ + queryKey: [AdvisoriesQueryKey, id, "csaf"], + queryFn: async () => { + const response = await downloadAdvisory({ client, path: { key: id } }); + const blob = response.data as Blob; + const text = await blob.text(); + return JSON.parse(text) as CsafDocument; + }, + enabled: !!id, + }); + + return { + csafDocument: data, + isFetching: isLoading, + fetchError: error as AxiosError | null, + }; +}; + export const useUploadAdvisory = () => { const queryClient = useQueryClient(); return useUpload({ diff --git a/client/src/app/types/csaf.ts b/client/src/app/types/csaf.ts new file mode 100644 index 000000000..d633ee1e0 --- /dev/null +++ b/client/src/app/types/csaf.ts @@ -0,0 +1,290 @@ +/** CSAF 2.0 VEX document type definitions per the OASIS CSAF JSON schema. */ + +/** Top-level CSAF document container. */ +export interface CsafDocument { + document: CsafDocumentMetadata; + product_tree?: CsafProductTree; + vulnerabilities?: CsafVulnerability[]; +} + +/** Document-level metadata section. */ +export interface CsafDocumentMetadata { + category: string; + csaf_version: string; + title: string; + publisher: CsafPublisher; + tracking: CsafTracking; + notes?: CsafNote[]; + references?: CsafReference[]; + distribution?: CsafDistribution; + aggregate_severity?: CsafAggregateSeverity; + lang?: string; + source_lang?: string; +} + +/** Document publisher information. */ +export interface CsafPublisher { + category: string; + name: string; + namespace: string; + contact_details?: string; + issuing_authority?: string; +} + +/** Document tracking metadata including revision history. */ +export interface CsafTracking { + current_release_date: string; + id: string; + initial_release_date: string; + revision_history: CsafRevision[]; + status: string; + version: string; + generator?: CsafGenerator; + aliases?: string[]; +} + +/** Document version revision entry. */ +export interface CsafRevision { + date: string; + number: string; + summary: string; +} + +/** Tool that generated the CSAF document. */ +export interface CsafGenerator { + engine: CsafGeneratorEngine; + date?: string; +} + +/** Generator engine identification. */ +export interface CsafGeneratorEngine { + name: string; + version?: string; +} + +/** Document-level note. */ +export interface CsafNote { + category: string; + text: string; + audience?: string; + title?: string; +} + +/** External reference link. */ +export interface CsafReference { + url: string; + summary?: string; + category?: string; +} + +/** TLP distribution information. */ +export interface CsafDistribution { + text?: string; + tlp?: CsafTlp; +} + +/** Traffic Light Protocol classification. */ +export interface CsafTlp { + label: string; + url?: string; +} + +/** Aggregate severity for the entire document. */ +export interface CsafAggregateSeverity { + text: string; + namespace?: string; +} + +/** Product tree describing the vendor/product/version hierarchy. */ +export interface CsafProductTree { + branches?: CsafBranch[]; + full_product_names?: CsafFullProductName[]; + relationships?: CsafRelationship[]; + product_groups?: CsafProductGroup[]; +} + +/** Recursive branch in the product tree hierarchy. */ +export interface CsafBranch { + category: string; + name: string; + branches?: CsafBranch[]; + product?: CsafFullProductName; +} + +/** A uniquely identifiable product with an ID and display name. */ +export interface CsafFullProductName { + name: string; + product_id: string; + product_identification_helper?: CsafProductIdentificationHelper; +} + +/** Helper data for identifying a product (CPE, PURL, etc.). */ +export interface CsafProductIdentificationHelper { + cpe?: string; + purl?: string; + hashes?: CsafHash[]; + model_numbers?: string[]; + serial_numbers?: string[]; + skus?: string[]; + x_generic_uris?: CsafGenericUri[]; +} + +/** Cryptographic hash for product identification. */ +export interface CsafHash { + file_hashes: CsafFileHash[]; + filename: string; +} + +/** Individual file hash entry. */ +export interface CsafFileHash { + algorithm: string; + value: string; +} + +/** Generic URI reference. */ +export interface CsafGenericUri { + namespace: string; + uri: string; +} + +/** Relationship between two products. */ +export interface CsafRelationship { + category: string; + full_product_name: CsafFullProductName; + product_reference: string; + relates_to_product_reference: string; +} + +/** Named group of product IDs. */ +export interface CsafProductGroup { + group_id: string; + product_ids: string[]; + summary?: string; +} + +/** Vulnerability entry in a CSAF document. */ +export interface CsafVulnerability { + cve?: string; + cwe?: CsafCwe; + title?: string; + notes?: CsafNote[]; + references?: CsafReference[]; + discovery_date?: string; + release_date?: string; + scores?: CsafScore[]; + product_status?: CsafProductStatus; + remediations?: CsafRemediation[]; + threats?: CsafThreat[]; + acknowledgments?: CsafAcknowledgment[]; + involvements?: CsafInvolvement[]; + ids?: CsafVulnerabilityId[]; +} + +/** CWE weakness classification. */ +export interface CsafCwe { + id: string; + name: string; +} + +/** CVSS scoring data for a set of products. */ +export interface CsafScore { + products: string[]; + cvss_v3?: CsafCvssV3; + cvss_v2?: CsafCvssV2; +} + +/** CVSS v3.x score details. */ +export interface CsafCvssV3 { + version: string; + vectorString: string; + baseScore: number; + baseSeverity: string; + attackVector?: string; + attackComplexity?: string; + privilegesRequired?: string; + userInteraction?: string; + scope?: string; + confidentialityImpact?: string; + integrityImpact?: string; + availabilityImpact?: string; + exploitCodeMaturity?: string; + remediationLevel?: string; + reportConfidence?: string; + temporalScore?: number; + temporalSeverity?: string; + environmentalScore?: number; + environmentalSeverity?: string; +} + +/** CVSS v2 score details. */ +export interface CsafCvssV2 { + version: string; + vectorString: string; + baseScore: number; + accessVector?: string; + accessComplexity?: string; + authentication?: string; + confidentialityImpact?: string; + integrityImpact?: string; + availabilityImpact?: string; +} + +/** Product status categories per vulnerability. */ +export interface CsafProductStatus { + fixed?: string[]; + known_affected?: string[]; + known_not_affected?: string[]; + first_affected?: string[]; + first_fixed?: string[]; + last_affected?: string[]; + recommended?: string[]; + under_investigation?: string[]; +} + +/** Remediation action for affected products. */ +export interface CsafRemediation { + category: string; + details: string; + product_ids?: string[]; + group_ids?: string[]; + url?: string; + date?: string; + entitlements?: string[]; + restart_required?: CsafRestartRequired; +} + +/** Restart requirement for a remediation. */ +export interface CsafRestartRequired { + category: string; + details?: string; +} + +/** Threat information for affected products. */ +export interface CsafThreat { + category: string; + details: string; + product_ids?: string[]; + group_ids?: string[]; + date?: string; +} + +/** Acknowledgment of contributors or reporters. */ +export interface CsafAcknowledgment { + names?: string[]; + organization?: string; + summary?: string; + urls?: string[]; +} + +/** Involvement of a party in vulnerability handling. */ +export interface CsafInvolvement { + party: string; + status: string; + summary?: string; +} + +/** Alternative vulnerability identifier. */ +export interface CsafVulnerabilityId { + system_name: string; + text: string; +} diff --git a/package-lock.json b/package-lock.json index f1d69cf59..d272e179a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,6 +70,8 @@ "@tanstack/react-query-devtools": "^5.61.0", "axios": "^1.16.0", "dayjs": "^1.11.18", + "echarts": "^5.6.0", + "echarts-for-react": "^3.0.2", "file-saver": "^2.0.5", "oidc-client-ts": "^3.3.0", "packageurl-js": "^2.0.1", @@ -6200,6 +6202,36 @@ "node": ">= 0.4" } }, + "node_modules/echarts": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.6.1" + } + }, + "node_modules/echarts-for-react": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.6.tgz", + "integrity": "sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "size-sensor": "^1.0.1" + }, + "peerDependencies": { + "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "react": "^15.0.0 || >=16.0.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6888,7 +6920,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-diff": { @@ -11458,6 +11489,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/size-sensor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.3.tgz", + "integrity": "sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==", + "license": "ISC" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -13606,6 +13643,21 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zrender": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "server": { "name": "@trustify-ui/server", "version": "0.1.0", From 4db37d1d6bcf4031a1bb6649f688de6286096523 Mon Sep 17 00:00:00 2001 From: Bryan Ramos Date: Thu, 28 May 2026 14:09:24 -0400 Subject: [PATCH 2/4] feat(advisory): add CSAF type detection and conditional 5-tab layout When advisory labels.type === "csaf", render a CSAF-specific 5-tab layout (Overview, Vulnerabilities, Product Tree, Relationship Tree, Source) instead of the default 2-tab Info/Vulnerabilities view. The CsafAdvisoryDetails component fetches the parsed CSAF document via useFetchAdvisoryCsafById and provides placeholder content for each tab that subsequent tasks will replace with real implementations. Implements TC-4619 Assisted-by: Claude Code --- .../advisory-details/advisory-details.tsx | 99 +++++++------ .../csaf-advisory-details.tsx | 139 ++++++++++++++++++ 2 files changed, 194 insertions(+), 44 deletions(-) create mode 100644 client/src/app/pages/advisory-details/csaf-advisory-details.tsx diff --git a/client/src/app/pages/advisory-details/advisory-details.tsx b/client/src/app/pages/advisory-details/advisory-details.tsx index 484f4781e..277a474ed 100644 --- a/client/src/app/pages/advisory-details/advisory-details.tsx +++ b/client/src/app/pages/advisory-details/advisory-details.tsx @@ -43,9 +43,11 @@ import { useFetchAdvisoryById, } from "@app/queries/advisories"; +import { DocumentMetadata } from "@app/components/DocumentMetadata"; + +import { CsafAdvisoryDetails } from "./csaf-advisory-details"; import { Overview } from "./overview"; import { VulnerabilitiesByAdvisory } from "./vulnerabilities-by-advisory"; -import { DocumentMetadata } from "@app/components/DocumentMetadata"; export const AdvisoryDetails: React.FC = () => { const navigate = useNavigate(); @@ -87,11 +89,13 @@ export const AdvisoryDetails: React.FC = () => { const { mutate: deleteAdvisory, isPending: isDeleting } = useDeleteAdvisoryMutation(onDeleteAdvisorySuccess, onDeleteAdvisoryError); - // Tabs + const isCsaf = advisory?.labels.type === "csaf"; + + // Tabs (default non-CSAF layout) const { propHelpers: { getTabsProps, getTabProps, getTabContentProps }, } = useTabControls({ - persistenceKeyPrefix: "ad", // ad="advisory details" + persistenceKeyPrefix: "ad", persistTo: "urlParams", tabKeys: ["info", "vulnerabilities"], }); @@ -177,47 +181,54 @@ export const AdvisoryDetails: React.FC = () => { - - - Info} - tabContentRef={infoTabRef} - /> - Vulnerabilities} - tabContentRef={vulnerabilitiesTabRef} - /> - - - - - - {advisory && } - - - - - - + + {isCsaf ? ( + + ) : ( + <> + + + Info} + tabContentRef={infoTabRef} + /> + Vulnerabilities} + tabContentRef={vulnerabilitiesTabRef} + /> + + + + + + {advisory && } + + + + + + + + )} = ({ label }) => ( + + This tab is under construction. + +); + +export const CsafAdvisoryDetails: React.FC = ({ + advisoryId, +}) => { + const { csafDocument, isFetching, fetchError } = + useFetchAdvisoryCsafById(advisoryId); + + const { + propHelpers: { getTabsProps, getTabProps, getTabContentProps }, + } = useTabControls({ + persistenceKeyPrefix: "cad", + persistTo: "urlParams", + tabKeys: [ + "overview", + "vulnerabilities", + "product-tree", + "relationship-tree", + "source", + ], + }); + + const overviewTabRef = React.createRef(); + const vulnerabilitiesTabRef = React.createRef(); + const productTreeTabRef = React.createRef(); + const relationshipTreeTabRef = React.createRef(); + const sourceTabRef = React.createRef(); + + return ( + <> + + + Overview} + tabContentRef={overviewTabRef} + /> + Vulnerabilities} + tabContentRef={vulnerabilitiesTabRef} + /> + Product Tree} + tabContentRef={productTreeTabRef} + /> + Relationship Tree} + tabContentRef={relationshipTreeTabRef} + /> + Source} + tabContentRef={sourceTabRef} + /> + + + + + + + + + + + + + + + + + + + + + + + ); +}; From 6452ecbb1c7059daec953f8421a6bdeeabba49ba Mon Sep 17 00:00:00 2001 From: Bryan Ramos Date: Thu, 28 May 2026 14:50:44 -0400 Subject: [PATCH 3/4] feat(advisory): implement CSAF Vulnerabilities tab with per-CVE cards Add CsafVulnerabilities tab component rendering per-CVE cards sorted by severity (critical first). Each card includes: - CVE ID linked to vulnerability details page - Severity shield with CVSS score - Expandable CVSS v3 breakdown (all metrics) - Expandable affected products list (first 5 + "+N more" toggle) - Expandable remediations grouped by category with linkified URLs - Expandable references as external links Product IDs resolved to display names via full_product_names. Graceful empty states for missing optional fields. Implements TC-4621 Assisted-by: Claude Code --- .../components/csaf-cvss-details.tsx | 51 +++++ .../components/csaf-remediations.tsx | 157 ++++++++++++++ .../components/csaf-vulnerability-card.tsx | 194 ++++++++++++++++++ .../csaf-advisory-details.tsx | 6 +- .../advisory-details/csaf-vulnerabilities.tsx | 94 +++++++++ 5 files changed, 501 insertions(+), 1 deletion(-) create mode 100644 client/src/app/pages/advisory-details/components/csaf-cvss-details.tsx create mode 100644 client/src/app/pages/advisory-details/components/csaf-remediations.tsx create mode 100644 client/src/app/pages/advisory-details/components/csaf-vulnerability-card.tsx create mode 100644 client/src/app/pages/advisory-details/csaf-vulnerabilities.tsx diff --git a/client/src/app/pages/advisory-details/components/csaf-cvss-details.tsx b/client/src/app/pages/advisory-details/components/csaf-cvss-details.tsx new file mode 100644 index 000000000..1f4a263d0 --- /dev/null +++ b/client/src/app/pages/advisory-details/components/csaf-cvss-details.tsx @@ -0,0 +1,51 @@ +/** Expandable CVSS v3 breakdown showing all scoring metrics. */ +import React from "react"; + +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + ExpandableSection, +} from "@patternfly/react-core"; + +import type { CsafCvssV3 } from "@app/types/csaf"; + +interface CsafCvssDetailsProps { + cvss: CsafCvssV3; +} + +export const CsafCvssDetails: React.FC = ({ cvss }) => { + const [isExpanded, setIsExpanded] = React.useState(false); + + const metrics: { label: string; value?: string }[] = [ + { label: "Attack Vector", value: cvss.attackVector }, + { label: "Attack Complexity", value: cvss.attackComplexity }, + { label: "Privileges Required", value: cvss.privilegesRequired }, + { label: "User Interaction", value: cvss.userInteraction }, + { label: "Scope", value: cvss.scope }, + { label: "Confidentiality Impact", value: cvss.confidentialityImpact }, + { label: "Integrity Impact", value: cvss.integrityImpact }, + { label: "Availability Impact", value: cvss.availabilityImpact }, + { label: "Vector String", value: cvss.vectorString }, + ]; + + return ( + setIsExpanded(expanded)} + isExpanded={isExpanded} + > + + {metrics + .filter((m) => m.value) + .map((m) => ( + + {m.label} + {m.value} + + ))} + + + ); +}; diff --git a/client/src/app/pages/advisory-details/components/csaf-remediations.tsx b/client/src/app/pages/advisory-details/components/csaf-remediations.tsx new file mode 100644 index 000000000..68bca150e --- /dev/null +++ b/client/src/app/pages/advisory-details/components/csaf-remediations.tsx @@ -0,0 +1,157 @@ +/** Remediations section grouped by category with expandable product lists. */ +import React from "react"; + +import { + Card, + CardBody, + CardTitle, + ExpandableSection, + Flex, + FlexItem, + Label, + List, + ListItem, +} from "@patternfly/react-core"; +import ExternalLinkAltIcon from "@patternfly/react-icons/dist/esm/icons/external-link-alt-icon"; + +import type { CsafFullProductName, CsafRemediation } from "@app/types/csaf"; + +interface CsafRemediationsProps { + remediations: CsafRemediation[]; + productNameMap: Map; +} + +/** Groups remediations by category. */ +function groupByCategory( + remediations: CsafRemediation[], +): Map { + const groups = new Map(); + for (const rem of remediations) { + const existing = groups.get(rem.category); + if (existing) { + existing.push(rem); + } else { + groups.set(rem.category, [rem]); + } + } + return groups; +} + +/** Renders text with URLs converted to links. */ +const LinkifiedText: React.FC<{ text: string }> = ({ text }) => { + const urlRegex = /(https?:\/\/[^\s)]+)/g; + const parts = text.split(urlRegex); + return ( + <> + {parts.map((part, i) => + urlRegex.test(part) ? ( + + {part} + + ) : ( + {part} + ), + )} + + ); +}; + +/** Single remediation card within a category group. */ +const RemediationCard: React.FC<{ + remediation: CsafRemediation; + productNameMap: Map; +}> = ({ remediation, productNameMap }) => { + const [showProducts, setShowProducts] = React.useState(false); + const productIds = remediation.product_ids || []; + + return ( + + + + + + + {remediation.url && ( + + + {remediation.url} + + + )} + {productIds.length > 0 && ( + + setShowProducts(expanded)} + isExpanded={showProducts} + > + + {productIds.map((pid) => ( + + {productNameMap.get(pid) || pid} + + ))} + + + + )} + + + + ); +}; + +export const CsafRemediations: React.FC = ({ + remediations, + productNameMap, +}) => { + const grouped = React.useMemo( + () => groupByCategory(remediations), + [remediations], + ); + + return ( + + + {Array.from(grouped.entries()).map(([category, rems]) => ( + + + + ( + {rems.length}) + + + + {rems.map((rem, i) => ( + + + + ))} + + + + + ))} + + + ); +}; diff --git a/client/src/app/pages/advisory-details/components/csaf-vulnerability-card.tsx b/client/src/app/pages/advisory-details/components/csaf-vulnerability-card.tsx new file mode 100644 index 000000000..3d382be49 --- /dev/null +++ b/client/src/app/pages/advisory-details/components/csaf-vulnerability-card.tsx @@ -0,0 +1,194 @@ +/** Individual CVE card with severity, CVSS breakdown, products, remediations, and references. */ +import React from "react"; +import { Link } from "react-router-dom"; + +import dayjs from "dayjs"; + +import { + Button, + Card, + CardBody, + CardHeader, + Content, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + ExpandableSection, + Flex, + FlexItem, + List, + ListItem, +} from "@patternfly/react-core"; +import ExternalLinkAltIcon from "@patternfly/react-icons/dist/esm/icons/external-link-alt-icon"; + +import { Paths } from "@app/Routes"; +import { SeverityShieldAndText } from "@app/components/SeverityShieldAndText"; +import type { CsafVulnerability } from "@app/types/csaf"; + +import { CsafCvssDetails } from "./csaf-cvss-details"; +import { CsafRemediations } from "./csaf-remediations"; + +interface CsafVulnerabilityCardProps { + vulnerability: CsafVulnerability; + productNameMap: Map; +} + +const INITIAL_PRODUCTS_SHOWN = 5; + +export const CsafVulnerabilityCard: React.FC = ({ + vulnerability, + productNameMap, +}) => { + const [showAllProducts, setShowAllProducts] = React.useState(false); + const cvss = vulnerability.scores?.[0]?.cvss_v3; + const affectedProducts = vulnerability.product_status?.known_affected || []; + const visibleProducts = showAllProducts + ? affectedProducts + : affectedProducts.slice(0, INITIAL_PRODUCTS_SHOWN); + const hiddenCount = affectedProducts.length - INITIAL_PRODUCTS_SHOWN; + + return ( + + + + {vulnerability.cve && ( + + + {vulnerability.cve} + + + )} + {cvss && ( + + + + )} + {vulnerability.title && ( + + {vulnerability.title} + + )} + + + + + {/* Metadata */} + + + {vulnerability.cwe && ( + + CWE + + {vulnerability.cwe.id} — {vulnerability.cwe.name} + + + )} + {vulnerability.discovery_date && ( + + Discovered + + {dayjs(vulnerability.discovery_date).format("YYYY-MM-DD")} + + + )} + {vulnerability.release_date && ( + + Released + + {dayjs(vulnerability.release_date).format("YYYY-MM-DD")} + + + )} + + + + {/* CVSS Details */} + {cvss && ( + + + + )} + + {/* Affected Products */} + {affectedProducts.length > 0 && ( + + + + {visibleProducts.map((pid) => ( + + {productNameMap.get(pid) || pid} + + ))} + + {hiddenCount > 0 && !showAllProducts && ( + + )} + + + )} + + {/* Remediations */} + {vulnerability.remediations && + vulnerability.remediations.length > 0 && ( + + + + )} + + {/* References */} + {vulnerability.references && vulnerability.references.length > 0 && ( + + + + {vulnerability.references.map((ref, i) => ( + + + {ref.summary || ref.url} + + + ))} + + + + )} + + + + ); +}; diff --git a/client/src/app/pages/advisory-details/csaf-advisory-details.tsx b/client/src/app/pages/advisory-details/csaf-advisory-details.tsx index c2c23cf41..6d8588971 100644 --- a/client/src/app/pages/advisory-details/csaf-advisory-details.tsx +++ b/client/src/app/pages/advisory-details/csaf-advisory-details.tsx @@ -17,6 +17,8 @@ import { LoadingWrapper } from "@app/components/LoadingWrapper"; import { useTabControls } from "@app/hooks/tab-controls"; import { useFetchAdvisoryCsafById } from "@app/queries/advisories"; +import { CsafVulnerabilities } from "./csaf-vulnerabilities"; + interface CsafAdvisoryDetailsProps { advisoryId: string; } @@ -109,7 +111,9 @@ export const CsafAdvisoryDetails: React.FC = ({ ref={vulnerabilitiesTabRef} aria-label="CSAF vulnerabilities" > - + {csafDocument && ( + + )} = { + CRITICAL: 0, + HIGH: 1, + MEDIUM: 2, + LOW: 3, + NONE: 4, +}; + +/** Sorts vulnerabilities by CVSS severity, critical first. */ +function sortBySeverity( + vulnerabilities: CsafVulnerability[], +): CsafVulnerability[] { + return [...vulnerabilities].sort((a, b) => { + const aSev = a.scores?.[0]?.cvss_v3?.baseSeverity?.toUpperCase() || "NONE"; + const bSev = b.scores?.[0]?.cvss_v3?.baseSeverity?.toUpperCase() || "NONE"; + return (SEVERITY_ORDER[aSev] ?? 5) - (SEVERITY_ORDER[bSev] ?? 5); + }); +} + +/** Builds product ID to display name map from full_product_names. */ +function buildProductNameMap(csafDocument: CsafDocument): Map { + const map = new Map(); + const products = csafDocument.product_tree?.full_product_names; + if (products) { + for (const p of products) { + map.set(p.product_id, p.name); + } + } + return map; +} + +export const CsafVulnerabilities: React.FC = ({ + csafDocument, +}) => { + const vulnerabilities = csafDocument.vulnerabilities; + + const sorted = React.useMemo( + () => (vulnerabilities ? sortBySeverity(vulnerabilities) : []), + [vulnerabilities], + ); + + const productNameMap = React.useMemo( + () => buildProductNameMap(csafDocument), + [csafDocument], + ); + + if (sorted.length === 0) { + return ( + + + This advisory does not contain vulnerability data. + + + ); + } + + return ( + + {sorted.map((vuln, i) => ( + + + + ))} + + ); +}; From 88f74e827e9a7181aa895cc0e783e301b95c0a4a Mon Sep 17 00:00:00 2001 From: Bryan Ramos Date: Thu, 28 May 2026 15:27:26 -0400 Subject: [PATCH 4/4] fix(advisory): fix frozen ExpandableSections, severity cast, and unused import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove isExpanded={false} from Affected Products, References, and Remediations ExpandableSections — PF6 treats this as controlled mode with no onToggle, permanently locking sections closed - Fix severity cast to use correct Severity union values (high/medium) instead of Red Hat labels (important/moderate) - Remove unused CsafFullProductName import from csaf-remediations.tsx Implements TC-4621 Assisted-by: Claude Code --- .../advisory-details/components/csaf-remediations.tsx | 4 ++-- .../components/csaf-vulnerability-card.tsx | 11 ++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/client/src/app/pages/advisory-details/components/csaf-remediations.tsx b/client/src/app/pages/advisory-details/components/csaf-remediations.tsx index 68bca150e..06214850f 100644 --- a/client/src/app/pages/advisory-details/components/csaf-remediations.tsx +++ b/client/src/app/pages/advisory-details/components/csaf-remediations.tsx @@ -14,7 +14,7 @@ import { } from "@patternfly/react-core"; import ExternalLinkAltIcon from "@patternfly/react-icons/dist/esm/icons/external-link-alt-icon"; -import type { CsafFullProductName, CsafRemediation } from "@app/types/csaf"; +import type { CsafRemediation } from "@app/types/csaf"; interface CsafRemediationsProps { remediations: CsafRemediation[]; @@ -124,7 +124,7 @@ export const CsafRemediations: React.FC = ({ ); return ( - + {Array.from(grouped.entries()).map(([category, rems]) => ( diff --git a/client/src/app/pages/advisory-details/components/csaf-vulnerability-card.tsx b/client/src/app/pages/advisory-details/components/csaf-vulnerability-card.tsx index 3d382be49..3730d1be3 100644 --- a/client/src/app/pages/advisory-details/components/csaf-vulnerability-card.tsx +++ b/client/src/app/pages/advisory-details/components/csaf-vulnerability-card.tsx @@ -74,8 +74,8 @@ export const CsafVulnerabilityCard: React.FC = ({ value={ cvss.baseSeverity.toLowerCase() as | "critical" - | "important" - | "moderate" + | "high" + | "medium" | "low" | "none" } @@ -132,10 +132,7 @@ export const CsafVulnerabilityCard: React.FC = ({ {/* Affected Products */} {affectedProducts.length > 0 && ( - + {visibleProducts.map((pid) => ( @@ -170,7 +167,7 @@ export const CsafVulnerabilityCard: React.FC = ({ {/* References */} {vulnerability.references && vulnerability.references.length > 0 && ( - + {vulnerability.references.map((ref, i) => (