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/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 && }
+
+
+
+
+
+
+ >
+ )}
= ({ 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..06214850f
--- /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 { 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..3730d1be3
--- /dev/null
+++ b/client/src/app/pages/advisory-details/components/csaf-vulnerability-card.tsx
@@ -0,0 +1,191 @@
+/** 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
new file mode 100644
index 000000000..6d8588971
--- /dev/null
+++ b/client/src/app/pages/advisory-details/csaf-advisory-details.tsx
@@ -0,0 +1,143 @@
+/** CSAF-specific advisory details with 5-tab layout for VEX document visualization. */
+import React from "react";
+
+import {
+ EmptyState,
+ EmptyStateBody,
+ EmptyStateVariant,
+ PageSection,
+ Tab,
+ TabContent,
+ Tabs,
+ TabTitleText,
+} from "@patternfly/react-core";
+import CubesIcon from "@patternfly/react-icons/dist/esm/icons/cubes-icon";
+
+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;
+}
+
+/** Placeholder for tabs not yet implemented. */
+const ComingSoon: React.FC<{ label: string }> = ({ 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}
+ />
+
+
+
+
+
+
+
+
+ {csafDocument && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/client/src/app/pages/advisory-details/csaf-vulnerabilities.tsx b/client/src/app/pages/advisory-details/csaf-vulnerabilities.tsx
new file mode 100644
index 000000000..c9aaa4834
--- /dev/null
+++ b/client/src/app/pages/advisory-details/csaf-vulnerabilities.tsx
@@ -0,0 +1,94 @@
+/** CSAF Vulnerabilities tab rendering per-CVE cards sorted by severity. */
+import React from "react";
+
+import {
+ EmptyState,
+ EmptyStateBody,
+ EmptyStateVariant,
+ Flex,
+ FlexItem,
+} from "@patternfly/react-core";
+import CubesIcon from "@patternfly/react-icons/dist/esm/icons/cubes-icon";
+
+import type { CsafDocument, CsafVulnerability } from "@app/types/csaf";
+
+import { CsafVulnerabilityCard } from "./components/csaf-vulnerability-card";
+
+interface CsafVulnerabilitiesProps {
+ csafDocument: CsafDocument;
+}
+
+const SEVERITY_ORDER: Record = {
+ 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) => (
+
+
+
+ ))}
+
+ );
+};
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",