diff --git a/README.md b/README.md
index 571a005e..5c9bc2ad 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,16 @@ After submitting the request, you will be redirected to the Report page. Once th
**Note:** There is a configurable pool of concurrent requests. Any request that is submitted when the pool is full will be queued. If after a certain time a callback response is not received, the report will be _expired_ (failed).
+### CVE Details Page
+
+By clicking on the CVE link:
+
+
+
+you will navigate to the CVE Details page where you can find details about a specific CVE.
+
+
+
### Reports Page
On this page, you will find a table containing all reports.
@@ -82,11 +92,10 @@ You will be able to sort, filter, and organize the reports to quickly locate spe

-
### Download Feature
A blue **Download** button is available on the repository report page, providing access to download either the VEX (Vulnerability Exploitability eXchange) data or the complete report as JSON files. The VEX option is only available when the component is in a vulnerable status and is automatically disabled otherwise.

-
\ No newline at end of file
+
diff --git a/docs/images/cve_click.png b/docs/images/cve_click.png
new file mode 100644
index 00000000..04b49eca
Binary files /dev/null and b/docs/images/cve_click.png differ
diff --git a/docs/images/cve_details_page.png b/docs/images/cve_details_page.png
new file mode 100644
index 00000000..0f204dd1
Binary files /dev/null and b/docs/images/cve_details_page.png differ
diff --git a/openspec/specs/cve-details-page/spec.md b/openspec/specs/cve-details-page/spec.md
new file mode 100644
index 00000000..55ddb7ec
--- /dev/null
+++ b/openspec/specs/cve-details-page/spec.md
@@ -0,0 +1,100 @@
+# cve-details-page Specification
+
+## Purpose
+
+The CVE Details page displays comprehensive information about a specific CVE (Common Vulnerabilities and Exposures) identifier, including description, metadata, vulnerable packages, and references. Users can access this page by clicking on a CVE link from the repository report page.
+
+## Requirements
+
+### Requirement: CVE Details Page Route
+
+The application SHALL provide routes to display CVE details for a specific CVE ID with support for proper breadcrumb navigation and efficient data fetching.
+
+#### Scenario: Navigate to CVE Details page with product and report
+
+- **WHEN** a user navigates to `/reports/product/cve/:productId/:cveId/:reportId` where:
+ - `:productId` is a product identifier
+ - `:cveId` is a CVE identifier (e.g., "cve-2024-0987")
+ - `:reportId` is a report identifier
+- **THEN** the CVE Details page displays with the CVE ID from the route parameter
+- **AND** the page fetches data directly from the specified report (more efficient than filtering)
+
+#### Scenario: Navigate to CVE Details page with product only
+
+- **WHEN** a user navigates to `/reports/product/cve/:productId/:cveId` where:
+ - `:productId` is a product identifier
+ - `:cveId` is a CVE identifier (e.g., "cve-2024-0987")
+- **THEN** the CVE Details page displays with the CVE ID from the route parameter
+- **AND** the page fetches data by filtering reports by CVE ID
+
+#### Scenario: Navigate to CVE Details page for component route
+
+- **WHEN** a user navigates to `/reports/component/cve/:productId/:cveId/:reportId` where:
+ - `:productId` is a product identifier (for component routes)
+ - `:cveId` is a CVE identifier (e.g., "cve-2024-0987")
+ - `:reportId` is a report identifier
+- **THEN** the CVE Details page displays with the CVE ID from the route parameter
+- **AND** the page fetches data directly from the specified report
+
+#### Scenario: Breadcrumb navigation displays full path
+
+- **WHEN** a user navigates to the CVE Details page with route parameters
+- **THEN** a breadcrumb navigation is displayed showing: Reports > ProductId/CVE > Report > CVE Details
+- **AND** breadcrumb items are clickable links that navigate to their respective pages, except the last "CVE Details" item which is non-clickable
+- **AND** the breadcrumb path is preserved even after page refresh (no dependency on location state)
+
+### Requirement: CVE Details Page Content Structure
+
+The CVE Details page SHALL display content in a structured layout with four cards arranged in a 2x2 grid layout.
+
+#### Scenario: Page displays four cards in grid layout
+
+- **WHEN** a user views the CVE Details page
+- **THEN** the page displays four cards organized in a 2x2 grid layout using PatternFly `Grid` and `GridItem` components:
+ - Top left: "Description" card
+ - Top right: "Metadata" card
+ - Bottom left: "Vulnerable Packages" card
+ - Bottom right: "References" card
+- **AND** the Description and Metadata cards have matching heights using flexbox layout (`height: 100%` and `flex: 1` on CardBody)
+
+#### Scenario: Description card
+
+- **WHEN** a user views the CVE Details page with CVE data loaded
+- **THEN** the Description card displays description field
+- **AND** the description text is extracted from `info.intel[0].nvd.cve_description` if available and not empty
+- **AND** if `nvd.cve_description` is empty or missing, the description is extracted from `info.intel[0].ghsa.description` as a fallback
+- **AND** if the description source is `ghsa.description`, the markdown text is rendered as formatted HTML using a markdown rendering library (e.g., `react-markdown`)
+- **AND** if the description source is `nvd.cve_description`, the plain text is displayed as-is
+- **AND** if no description is available from either source, the card displays "Not Available" using the `NotAvailable` component
+
+#### Scenario: Metadata card
+
+- **WHEN** a user views the CVE Details page with CVE data loaded
+- **THEN** the Metadata card displays CVE metadata using PatternFly `DescriptionList` component with the following fields:
+ - **CVSS Score**: Displays CVSS severity and score with priority icon using the `CvssBanner` component, which shows severity, icon, and score in the format "Severity Score" (e.g., "High 8.2") where severity is determined from the CVSS score (None, Low, Medium, High, Critical), and the appropriate severity icon is displayed next to the text
+ - **EPSS Score**: Displays EPSS percentage in the format "X.XXX%" (e.g., "0.025%") calculated by multiplying `epss.percentage` by 100
+ - **CWE**: Displays CWE identifier as a string in the format "CWE-XXX" (e.g., "CWE-22") from `ghsa.cwes[0].cwe_id`
+ - **Published**: Displays published date using `FormattedTimestamp` component from `ghsa.published_at` field
+ - **Updated**: Displays updated date using `FormattedTimestamp` component from `ghsa.updated_at` field
+ - **Credits**: Displays credits as a clickable link where the link text is `credits[0].user.login` and the link URL is `credits[0].user.html_url`, opening in a new tab
+- **AND** missing or null data fields display "Not Available" using the `NotAvailable` component
+
+#### Scenario: Vulnerable Packages card
+
+- **WHEN** a user views the CVE Details page with CVE data loaded
+- **THEN** the Vulnerable Packages card displays a list of vulnerable packages from `info.intel[0].ghsa.vulnerabilities` array
+- **AND** each vulnerable package displays the following information using PatternFly `DescriptionList` component:
+ - **Package Name**: Displays `package.name` in bold font (using PatternFly `Title` component with `strong` tag)
+ - **Ecosystem**: Displays `package.ecosystem` (e.g., "npm", "pypi", "maven") as a field label and value pair
+ - **Vulnerable Version**: Displays `vulnerable_version_range` (e.g., "< 7.5.7") as a field label and value pair
+ - **First patched Version**: Displays `first_patched_version` (e.g., "7.5.7") as a field label and value pair
+- **AND** if no vulnerable packages are available or the vulnerabilities array is empty, the card displays "Not Available" using the `NotAvailable` component
+- **AND** the card supports displaying 0, 1, or more vulnerable packages
+
+#### Scenario: References card
+
+- **WHEN** a user views the CVE Details page with CVE data loaded
+- **THEN** the References card displays a list of reference URLs from `info.intel[0].ghsa.references` array
+- **AND** each reference URL is displayed as a clickable link using PatternFly `List` and `ListItem` components
+- **AND** each reference link opens in a new tab when clicked (using `target="_blank"` and `rel="noreferrer"`)
+- **AND** if no references are available or the references array is empty, the card displays "Not Available" using the `NotAvailable` component
diff --git a/src/main/webui/package-lock.json b/src/main/webui/package-lock.json
index 9b0f175c..e47436af 100644
--- a/src/main/webui/package-lock.json
+++ b/src/main/webui/package-lock.json
@@ -16,6 +16,7 @@
"lodash": "^4.17.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-markdown": "^9.0.1",
"react-router": "^7.1.1",
"react-router-dom": "^7.1.1",
"victory": "^37.3.6"
@@ -5058,4 +5059,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/main/webui/package.json b/src/main/webui/package.json
index 0c6aef04..0573c335 100644
--- a/src/main/webui/package.json
+++ b/src/main/webui/package.json
@@ -23,6 +23,7 @@
"lodash": "^4.17.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-markdown": "^9.0.1",
"react-router": "^7.1.1",
"react-router-dom": "^7.1.1",
"victory": "^37.3.6"
diff --git a/src/main/webui/src/App.tsx b/src/main/webui/src/App.tsx
index 4dba58a0..db958ff6 100644
--- a/src/main/webui/src/App.tsx
+++ b/src/main/webui/src/App.tsx
@@ -5,6 +5,7 @@ import HomePage from "./pages/HomePage";
import ReportsPage from "./pages/ReportsPage";
import ReportPage from "./pages/ReportPage";
import RepositoryReportPage from "./pages/RepositoryReportPage";
+import CveDetailsPage from "./pages/CveDetailsPage";
/**
* App component - provides router context and defines all application routes
@@ -20,11 +21,26 @@ const App: React.FC = () => {
path="/reports/product/:productId/:cveId/:reportId"
element={}
/>
- } />
+ }
+ />
}
/>
+ }
+ />
+ }
+ />
+ }
+ />
} />
diff --git a/src/main/webui/src/components/CveDescriptionCard.tsx b/src/main/webui/src/components/CveDescriptionCard.tsx
new file mode 100644
index 00000000..9b252019
--- /dev/null
+++ b/src/main/webui/src/components/CveDescriptionCard.tsx
@@ -0,0 +1,45 @@
+import ReactMarkdown from "react-markdown";
+import { Content, EmptyState, EmptyStateBody } from "@patternfly/react-core";
+import { SearchIcon } from "@patternfly/react-icons";
+import type { CveMetadata } from "../hooks/useCveDetails";
+
+interface CveDescriptionCardProps {
+ metadata: CveMetadata | null;
+}
+
+/**
+ * Component to display CVE description with markdown rendering support
+ * Content from PatternFly automatically applies styling to standard HTML elements
+ * (h1-h6, p, ul, ol, blockquote) and overrides the base CSS reset.
+ */
+const CveDescriptionCard: React.FC = ({
+ metadata,
+}) => {
+ const description = metadata?.description;
+ const descriptionSource = metadata?.descriptionSource;
+
+ if (!description) {
+ return (
+
+ No description available for this CVE.
+
+ );
+ }
+
+ // Render markdown if source is GHSA, otherwise render as plain text
+ if (descriptionSource === "ghsa") {
+ return (
+
+ {description}
+
+ );
+ }
+
+ return {description};
+};
+
+export default CveDescriptionCard;
diff --git a/src/main/webui/src/components/CveMetadataCard.tsx b/src/main/webui/src/components/CveMetadataCard.tsx
new file mode 100644
index 00000000..f947c01e
--- /dev/null
+++ b/src/main/webui/src/components/CveMetadataCard.tsx
@@ -0,0 +1,90 @@
+import {
+ DescriptionList,
+ DescriptionListGroup,
+ DescriptionListTerm,
+ DescriptionListDescription,
+} from "@patternfly/react-core";
+import type { CveMetadata } from "../hooks/useCveDetails";
+import CvssBanner from "./CvssBanner";
+import FormattedTimestamp from "./FormattedTimestamp";
+import NotAvailable from "./NotAvailable";
+
+interface CveMetadataCardProps {
+ metadata: CveMetadata | null;
+}
+
+/**
+ * Component to display CVE metadata in a DescriptionList format
+ */
+const CveMetadataCard: React.FC = ({ metadata }) => {
+ // Convert numeric CVSS score to Cvss object format for CvssBanner component
+ const cvssDisplay =
+ metadata?.cvssScore !== undefined
+ ? {
+ score: metadata.cvssScore.toString(),
+ }
+ : null;
+
+ // Format EPSS score (multiply by 100 and add %)
+ const epssDisplay =
+ metadata?.epssPercentage !== undefined
+ ? `${(metadata.epssPercentage * 100).toFixed(3)}%`
+ : null;
+
+ return (
+
+
+ CVSS Score
+
+ {cvssDisplay ? : }
+
+
+
+ EPSS Score
+
+ {epssDisplay || }
+
+
+
+ CWE
+
+ {metadata?.cwe || }
+
+
+
+ Published
+
+ {metadata?.publishedAt ? (
+
+ ) : (
+
+ )}
+
+
+
+ Updated
+
+ {metadata?.updatedAt ? (
+
+ ) : (
+
+ )}
+
+
+
+ Credits
+
+ {metadata?.credits ? (
+
+ {metadata.credits.login}
+
+ ) : (
+
+ )}
+
+
+
+ );
+};
+
+export default CveMetadataCard;
diff --git a/src/main/webui/src/components/CveReferencesCard.tsx b/src/main/webui/src/components/CveReferencesCard.tsx
new file mode 100644
index 00000000..d64c008e
--- /dev/null
+++ b/src/main/webui/src/components/CveReferencesCard.tsx
@@ -0,0 +1,45 @@
+import {
+ List,
+ ListItem,
+ EmptyState,
+ EmptyStateBody,
+} from "@patternfly/react-core";
+import { SearchIcon } from "@patternfly/react-icons";
+import type { CveMetadata } from "../hooks/useCveDetails";
+
+interface CveReferencesCardProps {
+ metadata: CveMetadata | null;
+}
+
+/**
+ * Component to display CVE references as a list of clickable links
+ */
+const CveReferencesCard: React.FC = ({ metadata }) => {
+ const references = metadata?.references;
+
+ if (!references || references.length === 0) {
+ return (
+
+ No references available for this CVE.
+
+ );
+ }
+
+ return (
+
+ {references.map((reference, index) => (
+
+
+ {reference}
+
+
+ ))}
+
+ );
+};
+
+export default CveReferencesCard;
diff --git a/src/main/webui/src/components/CveStatus.tsx b/src/main/webui/src/components/CveStatus.tsx
index 0b5262cb..4bf1451f 100644
--- a/src/main/webui/src/components/CveStatus.tsx
+++ b/src/main/webui/src/components/CveStatus.tsx
@@ -7,7 +7,7 @@ interface CveStatusProps {
/**
* Component to display CVE status based on justification
*/
-const CveStatus: React.FC = ({ status }) => {
+const CveStatus: React.FC = ({ status }) => {
const getColor = (
status: string
diff --git a/src/main/webui/src/components/CveVulnerablePackagesCard.tsx b/src/main/webui/src/components/CveVulnerablePackagesCard.tsx
new file mode 100644
index 00000000..76638221
--- /dev/null
+++ b/src/main/webui/src/components/CveVulnerablePackagesCard.tsx
@@ -0,0 +1,65 @@
+import {
+ DescriptionList,
+ DescriptionListGroup,
+ DescriptionListTerm,
+ DescriptionListDescription,
+ Title,
+} from "@patternfly/react-core";
+import type { CveMetadata } from "../hooks/useCveDetails";
+import NotAvailable from "./NotAvailable";
+
+interface CveVulnerablePackagesCardProps {
+ metadata: CveMetadata | null;
+}
+
+/**
+ * Component to display CVE vulnerable packages with their details
+ */
+const CveVulnerablePackagesCard: React.FC = ({
+ metadata,
+}) => {
+ const vulnerablePackages = metadata?.vulnerablePackages;
+
+ if (!vulnerablePackages || vulnerablePackages.length === 0) {
+ return ;
+ }
+
+ return (
+ <>
+ {vulnerablePackages.map((pkg, index) => (
+