Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

![cve_click.png](./docs/images/cve_click.png)

you will navigate to the CVE Details page where you can find details about a specific CVE.

![cve_details_page.png](./docs/images/cve_details_page.png)

### Reports Page

On this page, you will find a table containing all reports.
Expand Down Expand Up @@ -82,11 +92,10 @@ You will be able to sort, filter, and organize the reports to quickly locate spe

![report](./docs/images/report.png)


### 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.

![download_button](./docs/images/download_button.png)

![download_open](./docs/images/download_open.png)
![download_open](./docs/images/download_open.png)
Binary file added docs/images/cve_click.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/cve_details_page.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
67 changes: 67 additions & 0 deletions openspec/specs/cve-details-page/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 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 a route to display CVE details for a specific CVE ID.

#### Scenario: Navigate to CVE Details page
- **WHEN** a user navigates to `/reports/cve/:cveId` where `:cveId` is a CVE identifier (e.g., "cve-2024-0987")
- **THEN** the CVE Details page displays with the CVE ID from the route parameter

#### Scenario: Breadcrumb navigation displays full path
- **WHEN** a user navigates to the CVE Details page from a repository report page
- **THEN** a breadcrumb navigation is displayed showing: Reports > SbomName/CVE > Cve xxxx-xxxx > CVE Details
- **AND** breadcrumb items are clickable links that navigate to their respective pages, except the last "CVE Details" item which is non-clickable

### 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 in the format "Severity (Score)" (e.g., "High (8.2)") where severity is determined from the CVSS score using the shared `getCvssSeverityIconAndColor` utility function from `CvssBanner` component (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
Original file line number Diff line number Diff line change
Expand Up @@ -648,4 +648,3 @@ private Bson buildQueryFilter(Map<String, String> queryFilter) {


}

3 changes: 2 additions & 1 deletion src/main/webui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/main/webui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions src/main/webui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,6 +26,7 @@ const App: React.FC = () => {
path="/reports/component/:cveId/:reportId"
element={<RepositoryReportPage />}
/>
<Route path="/reports/cve/:cveId" element={<CveDetailsPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>
Expand Down
41 changes: 41 additions & 0 deletions src/main/webui/src/components/CveDescriptionCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import ReactMarkdown from "react-markdown";
import { Content } from "@patternfly/react-core";
import type { CveMetadata } from "../hooks/useCveDetails";
import NotAvailable from "./NotAvailable";

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<CveDescriptionCardProps> = ({
metadata,
}) => {
const description = metadata?.description;
const descriptionSource = metadata?.descriptionSource;

if (!description) {
return <NotAvailable />;
}

// Render markdown if source is GHSA, otherwise render as plain text
if (descriptionSource === "ghsa") {
return (
<Content>
<ReactMarkdown>{description}</ReactMarkdown>
</Content>
);
}

return (
<Content>
{description}
</Content>
);
};

export default CveDescriptionCard;
53 changes: 53 additions & 0 deletions src/main/webui/src/components/CveDetailsPageSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { PageSection, Grid, GridItem, Skeleton } from "@patternfly/react-core";
import SkeletonCard from "./SkeletonCard";

/**
* Skeleton loading state for the CVE Details page
* Matches the structure of CveDetailsPage content
*/
const CveDetailsPageSkeleton: React.FC = () => {
return (
<>
<PageSection>
<Grid hasGutter>
<GridItem>
<Skeleton width="15%" screenreaderText="Loading breadcrumb" />
</GridItem>
<GridItem>
<Skeleton width="30%" screenreaderText="Loading title" />
</GridItem>
</Grid>
</PageSection>
<PageSection>
<Grid hasGutter>
<GridItem span={6}>
<SkeletonCard
widths={["40%", "60%", "45%"]}
screenreaderText="Loading details card"
/>
</GridItem>
<GridItem span={6}>
<SkeletonCard
widths={["35%", "50%", "45%"]}
screenreaderText="Loading metadata card"
/>
</GridItem>
<GridItem span={6}>
<SkeletonCard
widths={["50%", "65%", "55%"]}
screenreaderText="Loading vulnerable packages card"
/>
</GridItem>
<GridItem span={6}>
<SkeletonCard
widths={["45%", "60%", "50%"]}
screenreaderText="Loading references card"
/>
</GridItem>
</Grid>
</PageSection>
</>
);
};

export default CveDetailsPageSkeleton;
109 changes: 109 additions & 0 deletions src/main/webui/src/components/CveMetadataCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
DescriptionList,
DescriptionListGroup,
DescriptionListTerm,
DescriptionListDescription,
Flex,
FlexItem,
} from "@patternfly/react-core";
import type { CveMetadata } from "../hooks/useCveDetails";
import { getCvssSeverityIconAndColor } 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<CveMetadataCardProps> = ({ metadata }) => {
// Format CVSS score with icon using shared utility from CvssBanner
// Uses PatternFly CSS variables for icon colors
const cvssDisplay =
metadata?.cvssScore !== undefined
? (() => {
const { severity, Icon, color } = getCvssSeverityIconAndColor(
metadata.cvssScore
);
return (
<Flex spaceItems={{ default: "spaceItemsXs" }}>
{Icon && (
<FlexItem>
<Icon color={color} />
</FlexItem>
)}
<FlexItem>
<span>
{severity} ({metadata.cvssScore})
</span>
</FlexItem>
</Flex>
);
})()
: null;

// Format EPSS score (multiply by 100 and add %)
const epssDisplay =
metadata?.epssPercentage !== undefined
? `${(metadata.epssPercentage * 100).toFixed(3)}%`
: null;

return (
<DescriptionList>
<DescriptionListGroup>
<DescriptionListTerm>CVSS Score</DescriptionListTerm>
<DescriptionListDescription>
{cvssDisplay || <NotAvailable />}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>EPSS Score</DescriptionListTerm>
<DescriptionListDescription>
{epssDisplay || <NotAvailable />}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>CWE</DescriptionListTerm>
<DescriptionListDescription>
{metadata?.cwe || <NotAvailable />}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>Published</DescriptionListTerm>
<DescriptionListDescription>
{metadata?.publishedAt ? (
<FormattedTimestamp date={metadata.publishedAt} />
) : (
<NotAvailable />
)}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>Updated</DescriptionListTerm>
<DescriptionListDescription>
{metadata?.updatedAt ? (
<FormattedTimestamp date={metadata.updatedAt} />
) : (
<NotAvailable />
)}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>Credits</DescriptionListTerm>
<DescriptionListDescription>
{metadata?.credits ? (
<a href={metadata.credits.htmlUrl} target="_blank" rel="noreferrer">
{metadata.credits.login}
</a>
) : (
<NotAvailable />
)}
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
);
};

export default CveMetadataCard;
32 changes: 32 additions & 0 deletions src/main/webui/src/components/CveReferencesCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { List, ListItem } from "@patternfly/react-core";
import type { CveMetadata } from "../hooks/useCveDetails";
import NotAvailable from "./NotAvailable";

interface CveReferencesCardProps {
metadata: CveMetadata | null;
}

/**
* Component to display CVE references as a list of clickable links
*/
const CveReferencesCard: React.FC<CveReferencesCardProps> = ({ metadata }) => {
const references = metadata?.references;

if (!references || references.length === 0) {
return <NotAvailable />;
}

return (
<List>
{references.map((reference, index) => (
<ListItem key={index}>
<a href={reference} target="_blank" rel="noreferrer">
{reference}
</a>
</ListItem>
))}
</List>
);
};

export default CveReferencesCard;
Loading