--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import axios from 'axios';
+import { Cve } from '../types/cves';
+
+const CVE_BASE_URL = '/api/v2/analysis/cves';
+
+export function getCve(cveId: string): Promise<Cve> {
+ return axios.get(`${CVE_BASE_URL}/${cveId}`);
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { cloneDeep } from 'lodash';
+import { mockCve } from '../../helpers/testMocks';
+import { Cve } from '../../types/cves';
+import { getCve } from '../cves';
+
+jest.mock('../../api/cves');
+
+export const DEFAULT_CVE_LIST = [
+ mockCve({ id: 'CVE-2021-12345' }),
+ mockCve({ id: 'CVE-2021-12346' }),
+];
+
+export default class CveServiceMock {
+ private cveList: Cve[];
+
+ constructor() {
+ this.cveList = cloneDeep(DEFAULT_CVE_LIST);
+ jest.mocked(getCve).mockImplementation(this.handleGetCve);
+ }
+
+ setCveList(cveList: Cve[]) {
+ this.cveList = cveList;
+ }
+
+ handleGetCve = (cveId: string) => {
+ const cve = this.cveList.find((cve) => cve.id === cveId);
+ if (!cve) {
+ return Promise.reject(new Error('Cve not found'));
+ }
+ return this.reply(cve);
+ };
+
+ reset = () => {
+ this.cveList = cloneDeep(DEFAULT_CVE_LIST);
+ };
+
+ reply<T>(response: T): Promise<T> {
+ return Promise.resolve(cloneDeep(response));
+ }
+}
return [
mockRawHotspot({ assignee: 'John Doe', key: 'test-1' }),
mockRawHotspot({ assignee: 'John Doe', key: 'test-2' }),
+ mockRawHotspot({ assignee: 'John Doe', key: 'test-cve', cveId: 'CVE-2021-12345' }),
];
};
message: "'2' is a magic number.",
codeVariants: ['variant 1', 'variant 2'],
}),
+ mockHotspot({
+ rule: mockHotspotRule({ key: 'rule2' }),
+ key: 'test-cve',
+ status: HotspotStatus.TO_REVIEW,
+ message: 'CVE on jackson',
+ cveId: 'CVE-2021-12345',
+ }),
];
this.canChangeStatus = true;
};
issueStatus: IssueStatus.Open,
ruleDescriptionContextKey: 'spring',
author: 'bob.marley@test.com',
+ cveId: 'CVE-2021-12345',
}),
snippets: keyBy(
[
import { byRole, byText } from '~sonar-aligned/helpers/testSelector';
import { ISSUE_101 } from '../../../api/mocks/data/ids';
import { TabKeys } from '../../../components/rules/RuleTabViewer';
-import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
+import { mockCurrentUser, mockCve, mockLoggedInUser } from '../../../helpers/testMocks';
import { Feature } from '../../../types/features';
import { RestUserDetailed } from '../../../types/users';
import {
branchHandler,
componentsHandler,
+ cveHandler,
issuesHandler,
renderIssueApp,
renderProjectIssuesApp,
beforeEach(() => {
issuesHandler.reset();
+ cveHandler.reset();
componentsHandler.reset();
branchHandler.reset();
usersHandler.reset();
expect(screen.getByRole('heading', { name: 'Defense-In-Depth', level: 3 })).toBeInTheDocument();
});
+ it('should render CVE details', async () => {
+ const user = userEvent.setup();
+ renderProjectIssuesApp('project/issues?issues=issue2&open=issue2&id=myproject');
+
+ await user.click(
+ await screen.findByRole('tab', { name: 'coding_rules.description_section.title.root_cause' }),
+ );
+
+ await user.click(screen.getByRole('radio', { name: 'coding_rules.description_context.other' }));
+
+ expect(await screen.findByRole('heading', { name: 'CVE-2021-12345' })).toBeInTheDocument();
+
+ const rows = byRole('row').getAll(ui.cveTable.get());
+ expect(rows).toHaveLength(4);
+ expect(byText('CWE-79, CWE-89').get(rows[0])).toBeInTheDocument();
+ expect(byText('rule.cve_details.epss_score.value.20.56').get(rows[1])).toBeInTheDocument();
+ expect(byText('0.3').get(rows[2])).toBeInTheDocument();
+ expect(byText('Oct 04, 2021').get(rows[3])).toBeInTheDocument();
+ });
+
+ it('should not render CVE CVSS and CWEs when not set', async () => {
+ const user = userEvent.setup();
+ cveHandler.setCveList([
+ mockCve({
+ cvssScore: undefined,
+ cwes: [],
+ }),
+ ]);
+ renderProjectIssuesApp('project/issues?issues=issue2&open=issue2&id=myproject');
+
+ await user.click(
+ await screen.findByRole('tab', { name: 'coding_rules.description_section.title.root_cause' }),
+ );
+
+ await user.click(
+ await screen.findByRole('radio', { name: 'coding_rules.description_context.other' }),
+ );
+
+ expect(await screen.findByRole('heading', { name: 'CVE-2021-12345' })).toBeInTheDocument();
+
+ const rows = byRole('row').getAll(ui.cveTable.get());
+ expect(rows).toHaveLength(2);
+ expect(byText('rule.cve_details.epss_score.value.20.56').get(rows[0])).toBeInTheDocument();
+ expect(byText('Oct 04, 2021').get(rows[1])).toBeInTheDocument();
+ });
+
it('should be able to change the issue status', async () => {
const user = userEvent.setup();
issuesHandler.setIsAdmin(true);
import { isPortfolioLike } from '~sonar-aligned/helpers/component';
import { ComponentQualifier } from '~sonar-aligned/types/component';
import { Location, RawQuery, Router } from '~sonar-aligned/types/router';
+import { getCve } from '../../../api/cves';
import { listIssues, searchIssues } from '../../../api/issues';
import { getRuleDetails } from '../../../api/rules';
import withComponentContext from '../../../app/components/componentContext/withComponentContext';
import { withBranchLikes } from '../../../queries/branch';
import { BranchLike } from '../../../types/branch-like';
import { isProject } from '../../../types/component';
+import { Cve } from '../../../types/cves';
import {
ASSIGNEE_ME,
Facet,
bulkChangeModal: boolean;
checkAll?: boolean;
checked: string[];
+ cve?: Cve;
effortTotal?: number;
facets: Dict<Facet>;
issues: Issue[];
.then((response) => response.rule)
.catch(() => undefined);
+ let cve: Cve | undefined;
+ if (typeof openIssue.cveId === 'string') {
+ cve = await getCve(openIssue.cveId);
+ }
+
if (this.mounted) {
- this.setState({ loadingRule: false, openRuleDetails });
+ this.setState({ loadingRule: false, openRuleDetails, cve });
}
}
}
renderPage() {
- const { openRuleDetails, checkAll, issues, loading, openIssue, paging, loadingRule } =
+ const { openRuleDetails, cve, checkAll, issues, loading, openIssue, paging, loadingRule } =
this.state;
return (
onIssueChange={this.handleIssueChange}
ruleDescriptionContextKey={openIssue.ruleDescriptionContextKey}
ruleDetails={openRuleDetails}
+ cve={cve}
selectedFlowIndex={this.state.selectedFlowIndex}
selectedLocationIndex={this.state.selectedLocationIndex}
/>
import { byPlaceholderText, byRole, byTestId, byText } from '~sonar-aligned/helpers/testSelector';
import BranchesServiceMock from '../../api/mocks/BranchesServiceMock';
import ComponentsServiceMock from '../../api/mocks/ComponentsServiceMock';
+import CveServiceMock from '../../api/mocks/CveServiceMock';
import FixIssueServiceMock from '../../api/mocks/FixIssueServiceMock';
import IssuesServiceMock from '../../api/mocks/IssuesServiceMock';
import SourcesServiceMock from '../../api/mocks/SourcesServiceMock';
export const usersHandler = new UsersServiceMock();
export const issuesHandler = new IssuesServiceMock(usersHandler);
+export const cveHandler = new CveServiceMock();
export const componentsHandler = new ComponentsServiceMock();
export const sourcesHandler = new SourcesServiceMock();
export const branchHandler = new BranchesServiceMock();
vulnerabilityIssueTypeFilter: byRole('checkbox', { name: 'issue.type.VULNERABILITY' }),
prioritizedRuleFilter: byRole('checkbox', { name: 'issues.facet.prioritized_rule' }),
+ cveTable: byRole('table', { name: 'rule.cve_details' }),
+
bulkChangeComment: byRole('textbox', { name: /issue_bulk_change.resolution_comment/ }),
clearAllFilters: byRole('button', { name: 'clear_all_filters' }),
<HotspotViewer
component={component}
hotspotKey={selectedHotspot.key}
+ cveId={selectedHotspot.cveId}
hotspotsReviewedMeasure={hotspotsReviewedMeasure}
onLocationClick={props.onLocationClick}
onSwitchStatusFilter={props.onSwitchStatusFilter}
import { MetricKey } from '~sonar-aligned/types/metrics';
import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock';
import CodingRulesServiceMock from '../../../api/mocks/CodingRulesServiceMock';
+import CveServiceMock from '../../../api/mocks/CveServiceMock';
import SecurityHotspotServiceMock from '../../../api/mocks/SecurityHotspotServiceMock';
import { getSecurityHotspots, setSecurityHotspotStatus } from '../../../api/security-hotspots';
import { getUsers } from '../../../api/users';
import { mockComponent } from '../../../helpers/mocks/component';
import { openHotspot, probeSonarLintServers } from '../../../helpers/sonarlint';
import { get, save } from '../../../helpers/storage';
-import { mockLoggedInUser } from '../../../helpers/testMocks';
+import { mockCve, mockLoggedInUser } from '../../../helpers/testMocks';
import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils';
import { ComponentContextShape } from '../../../types/component';
import SecurityHotspotsApp from '../SecurityHotspotsApp';
filterToReview: byRole('radio', { name: 'hotspot.filters.status.to_review' }),
fixContent: byText('This is how to fix'),
fixTab: byRole('tab', { name: /hotspots.tabs.fix_recommendations/ }),
+ cveTable: byRole('table', { name: 'rule.cve_details' }),
hotpostListTitle: byText('hotspots.list_title'),
hotspotCommentBox: byRole('textbox', { name: 'hotspots.comment.field' }),
hotspotStatus: byRole('heading', { name: 'status: hotspots.status_option.FIXED' }),
const originalScrollTo = window.scrollTo;
const hotspotsHandler = new SecurityHotspotServiceMock();
+const cveHandler = new CveServiceMock();
const rulesHandles = new CodingRulesServiceMock();
const branchHandler = new BranchesServiceMock();
afterEach(() => {
hotspotsHandler.reset();
+ cveHandler.reset();
rulesHandles.reset();
branchHandler.reset();
});
expect(await ui.reviewButton.findAll()).toHaveLength(2);
});
+
+ it('should render CVE details', async () => {
+ const user = userEvent.setup();
+
+ renderSecurityHotspotsApp(
+ 'security_hotspots?id=guillaume-peoch-sonarsource_benflix_AYGpXq2bd8qy4i0eO9ed&hotspots=test-cve',
+ );
+
+ await user.click(await ui.riskTab.find());
+ expect(await screen.findByRole('heading', { name: 'CVE-2021-12345' })).toBeInTheDocument();
+
+ const rows = byRole('row').getAll(ui.cveTable.get());
+ expect(rows).toHaveLength(4);
+ expect(byText('CWE-79, CWE-89').get(rows[0])).toBeInTheDocument();
+ expect(byText('rule.cve_details.epss_score.value.20.56').get(rows[1])).toBeInTheDocument();
+ expect(byText('0.3').get(rows[2])).toBeInTheDocument();
+ expect(byText('Oct 04, 2021').get(rows[3])).toBeInTheDocument();
+ });
+
+ it('should not render CVE CVSS and CWEs when not set', async () => {
+ const user = userEvent.setup();
+ cveHandler.setCveList([
+ mockCve({
+ cvssScore: undefined,
+ cwes: [],
+ }),
+ ]);
+
+ renderSecurityHotspotsApp(
+ 'security_hotspots?id=guillaume-peoch-sonarsource_benflix_AYGpXq2bd8qy4i0eO9ed&hotspots=test-cve',
+ );
+
+ await user.click(await ui.riskTab.find());
+ expect(await screen.findByRole('heading', { name: 'CVE-2021-12345' })).toBeInTheDocument();
+
+ const rows = byRole('row').getAll(ui.cveTable.get());
+ expect(rows).toHaveLength(2);
+ expect(byText('rule.cve_details.epss_score.value.20.56').get(rows[0])).toBeInTheDocument();
+ expect(byText('Oct 04, 2021').get(rows[1])).toBeInTheDocument();
+ });
});
describe('CRUD', () => {
};
render() {
- const { hotspot, selectedHotspotLocation } = this.props;
+ const { branchLike, component, hotspot, selectedHotspotLocation } = this.props;
const { highlightedSymbols, lastLine, loading, sourceLines, secondaryLocations } = this.state;
const locations = locationsByLine([hotspot]);
return (
<HotspotSnippetContainerRenderer
+ component={component}
+ branchLike={branchLike}
highlightedSymbols={highlightedSymbols}
hotspot={hotspot}
loading={loading}
*/
import { withTheme } from '@emotion/react';
import styled from '@emotion/styled';
-import { Spinner, themeColor } from 'design-system';
+import { Spinner } from '@sonarsource/echoes-react';
+import { FlagMessage, themeColor } from 'design-system';
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
+import { BranchLike } from '../../../types/branch-like';
import { Hotspot } from '../../../types/security-hotspots';
import {
+ Component,
ExpandDirection,
FlowLocation,
LinearIssueLocation,
} from '../../../types/types';
import SnippetViewer from '../../issues/crossComponentSourceViewer/SnippetViewer';
import HotspotPrimaryLocationBox from './HotspotPrimaryLocationBox';
+import HotspotSnippetHeader from './HotspotSnippetHeader';
export interface HotspotSnippetContainerRendererProps {
+ branchLike?: BranchLike;
+ component: Component;
highlightedSymbols: string[];
hotspot: Hotspot;
loading: boolean;
selectedHotspotLocation,
sourceLines,
sourceViewerFile,
+ component,
+ branchLike,
} = props;
const scrollableRef = React.useRef<HTMLDivElement>(null);
: undefined;
return (
- <>
- {!loading && sourceLines.length === 0 && (
- <p className="sw-my-4">{translate('hotspots.no_associated_lines')}</p>
+ <Spinner isLoading={loading}>
+ {sourceLines.length === 0 && (
+ <FlagMessage variant="info">{translate('hotspots.no_associated_lines')}</FlagMessage>
)}
- <SourceFileWrapper className="sw-box-border sw-w-full sw-rounded-1" ref={scrollableRef}>
- <Spinner className="sw-m-4" loading={loading} />
-
- {!loading && sourceLines.length > 0 && (
- <SnippetViewer
- component={sourceViewerFile}
- displayLineNumberOptions={false}
- displaySCM={false}
- expandBlock={(_i, direction) =>
- animateExpansion(scrollableRef, props.onExpandBlock, direction)
- }
- handleSymbolClick={props.onSymbolClick}
- highlightedLocationMessage={highlightedLocation}
- highlightedSymbols={highlightedSymbols}
- index={0}
- locations={secondaryLocations}
- locationsByLine={primaryLocations}
- onLocationSelect={props.onLocationSelect}
- renderAdditionalChildInLine={renderHotspotBoxInLine}
- renderDuplicationPopup={noop}
- snippet={sourceLines}
- hideLocationIndex={secondaryLocations.length !== 0}
- />
- )}
- </SourceFileWrapper>
- </>
+ {sourceLines.length > 0 && (
+ <>
+ <HotspotSnippetHeader hotspot={hotspot} component={component} branchLike={branchLike} />
+ <SourceFileWrapper className="sw-box-border sw-w-full sw-rounded-1" ref={scrollableRef}>
+ <SnippetViewer
+ component={sourceViewerFile}
+ displayLineNumberOptions={false}
+ displaySCM={false}
+ expandBlock={(_i, direction) =>
+ animateExpansion(scrollableRef, props.onExpandBlock, direction)
+ }
+ handleSymbolClick={props.onSymbolClick}
+ highlightedLocationMessage={highlightedLocation}
+ highlightedSymbols={highlightedSymbols}
+ index={0}
+ locations={secondaryLocations}
+ locationsByLine={primaryLocations}
+ onLocationSelect={props.onLocationSelect}
+ renderAdditionalChildInLine={renderHotspotBoxInLine}
+ renderDuplicationPopup={noop}
+ snippet={sourceLines}
+ hideLocationIndex={secondaryLocations.length !== 0}
+ />
+ </SourceFileWrapper>
+ </>
+ )}
+ </Spinner>
);
}
return (
<StyledHeader
- className={`sw-box-border sw-flex sw-gap-2 sw-justify-between -sw-mb-4 sw-mt-6 sw-px-4
+ className={`sw-box-border sw-flex sw-gap-2 sw-justify-between sw-mt-6 sw-px-4
sw-py-3`}
>
<Note className="sw-flex sw-flex-1 sw-flex-wrap sw-gap-2 sw-items-center sw-my-1/2">
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { getCve } from '../../../api/cves';
import { getRuleDetails } from '../../../api/rules';
import { getSecurityHotspotDetails } from '../../../api/security-hotspots';
import { get } from '../../../helpers/storage';
+import { Cve } from '../../../types/cves';
import { Standards } from '../../../types/security';
import {
Hotspot,
interface Props {
component: Component;
+ cveId?: string;
hotspotKey: string;
hotspotsReviewedMeasure?: string;
onLocationClick: (index: number) => void;
}
interface State {
+ cve?: Cve;
hotspot?: Hotspot;
lastStatusChangedTo?: HotspotStatusOption;
loading: boolean;
try {
const hotspot = await getSecurityHotspotDetails(this.props.hotspotKey);
const ruleDetails = await getRuleDetails({ key: hotspot.rule.key }).then((r) => r.rule);
+ let cve;
+ if (typeof this.props.cveId === 'string') {
+ cve = await getCve(this.props.cveId);
+ }
if (this.mounted) {
this.setState({
loading: false,
ruleLanguage: ruleDetails.lang,
ruleDescriptionSections: ruleDetails.descriptionSections,
+ cve,
});
}
} catch (error) {
hotspot,
ruleDescriptionSections,
ruleLanguage,
+ cve,
loading,
showStatusUpdateSuccessModal,
lastStatusChangedTo,
onUpdateHotspot={this.handleHotspotUpdate}
ruleDescriptionSections={ruleDescriptionSections}
ruleLanguage={ruleLanguage}
+ cve={cve}
selectedHotspotLocation={selectedHotspotLocation}
showStatusUpdateSuccessModal={showStatusUpdateSuccessModal}
standards={standards}
import { HotspotHeader } from './HotspotHeader';
import { Spinner } from 'design-system';
+import { Cve } from '../../../types/cves';
import { CurrentUser } from '../../../types/users';
import { RuleDescriptionSection } from '../../coding-rules/rule';
import HotspotReviewHistoryAndComments from './HotspotReviewHistoryAndComments';
export interface HotspotViewerRendererProps {
component: Component;
currentUser: CurrentUser;
+ cve?: Cve;
hotspot?: Hotspot;
hotspotsReviewedMeasure?: string;
lastStatusChangedTo?: HotspotStatusOption;
loading,
ruleDescriptionSections,
ruleLanguage,
+ cve,
selectedHotspotLocation,
showStatusUpdateSuccessModal,
standards,
onCommentUpdate={props.onUpdateHotspot}
/>
}
- branchLike={branchLike}
- component={component}
codeTabContent={
<HotspotSnippetContainer
branchLike={branchLike}
onUpdateHotspot={props.onUpdateHotspot}
ruleDescriptionSections={ruleDescriptionSections}
ruleLanguage={ruleLanguage}
+ cve={cve}
/>
</div>
)}
import { KeyboardKeys } from '../../../helpers/keycodes';
import { translate } from '../../../helpers/l10n';
import { useRefreshBranchStatus } from '../../../queries/branch';
-import { BranchLike } from '../../../types/branch-like';
+import { Cve } from '../../../types/cves';
import { Hotspot, HotspotStatusOption } from '../../../types/security-hotspots';
-import { Component } from '../../../types/types';
import { RuleDescriptionSection, RuleDescriptionSections } from '../../coding-rules/rule';
import useStickyDetection from '../hooks/useStickyDetection';
-import HotspotSnippetHeader from './HotspotSnippetHeader';
import StatusReviewButton from './status/StatusReviewButton';
interface Props {
activityTabContent: React.ReactNode;
- branchLike?: BranchLike;
codeTabContent: React.ReactNode;
- component: Component;
+ cve: Cve | undefined;
hotspot: Hotspot;
onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise<void>;
ruleDescriptionSections?: RuleDescriptionSection[];
hotspot,
ruleDescriptionSections,
ruleLanguage,
- component,
- branchLike,
+ cve,
} = props;
const refreshBranchStatus = useRefreshBranchStatus(component.key);
/>
{isSticky && <StatusReviewButton hotspot={hotspot} onStatusChange={handleStatusChange} />}
</div>
- {currentTab.value === TabKeys.Code && codeTabContent && (
- <HotspotSnippetHeader hotspot={hotspot} component={component} branchLike={branchLike} />
- )}
</StickyTabs>
<div
aria-labelledby={getTabId(currentTab.value)}
{currentTab.value === TabKeys.Code && codeTabContent}
{currentTab.value === TabKeys.RiskDescription && rootCauseDescriptionSections && (
- <RuleDescription language={ruleLanguage} sections={rootCauseDescriptionSections} />
+ <RuleDescription
+ language={ruleLanguage}
+ sections={rootCauseDescriptionSections}
+ cve={cve}
+ />
)}
{currentTab.value === TabKeys.VulnerabilityDescription &&
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { ContentCell, Table, TableRow } from 'design-system';
+import React from 'react';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { Cve } from '../../types/cves';
+import DateFormatter from '../intl/DateFormatter';
+
+type Props = {
+ cve: Cve;
+};
+
+export function CveDetails({ cve }: Readonly<Props>) {
+ const { id, description, cwes, cvssScore, epssScore, epssPercentile, publishedAt } = cve;
+ return (
+ <>
+ <h2>{id}</h2>
+ <p>{description}</p>
+ <Table columnCount={2} aria-label={translate('rule.cve_details')}>
+ {cwes.length > 0 && (
+ <TableRow>
+ <ContentCell>{translate('rule.cve_details.cwes')}</ContentCell>
+ <ContentCell>{cwes.join(', ')}</ContentCell>
+ </TableRow>
+ )}
+ <TableRow>
+ <ContentCell>{translate('rule.cve_details.epss_score')}</ContentCell>
+ <ContentCell>
+ {translateWithParameters(
+ 'rule.cve_details.epss_score.value',
+ Math.round(epssScore * 100),
+ Math.round(epssPercentile * 100),
+ )}
+ </ContentCell>
+ </TableRow>
+ {typeof cvssScore === 'number' && (
+ <TableRow>
+ <ContentCell>{translate('rule.cve_details.cvss_score')}</ContentCell>
+ <ContentCell>{cvssScore.toFixed(1)}</ContentCell>
+ </TableRow>
+ )}
+ <TableRow>
+ <ContentCell>{translate('rule.cve_details.published_date')}</ContentCell>
+ <ContentCell>
+ <DateFormatter date={publishedAt} />
+ </ContentCell>
+ </TableRow>
+ </Table>
+ </>
+ );
+}
import StyledHeader from '../../apps/issues/components/StyledHeader';
import { fillBranchLike } from '../../helpers/branch-like';
import { translate } from '../../helpers/l10n';
+import { Cve } from '../../types/cves';
import { Feature } from '../../types/features';
import { Issue, RuleDetails } from '../../types/types';
import { CurrentUser, NoticeType } from '../../types/users';
activityTabContent?: React.ReactNode;
codeTabContent?: React.ReactNode;
currentUser: CurrentUser;
+ cve?: Cve;
extendedDescription?: string;
hasFeature: (feature: string) => boolean;
issue: Issue;
ruleDescriptionContextKey,
extendedDescription,
activityTabContent,
+ cve,
issue,
suggestionTabContent,
hasFeature,
descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ??
descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]
).concat(descriptionSectionsByKey[RuleDescriptionSections.INTRODUCTION] ?? [])}
+ cve={cve}
/>
),
},
import { translate, translateWithParameters } from '../../helpers/l10n';
import { sanitizeString } from '../../helpers/sanitize';
import { isDefined } from '../../helpers/types';
+import { Cve as CveDetailsType } from '../../types/cves';
+import { CveDetails } from './CveDetails';
import OtherContextOption from './OtherContextOption';
const OTHERS_KEY = 'others';
interface Props {
className?: string;
+ cve?: CveDetailsType;
defaultContextKey?: string;
language?: string;
sections: RuleDescriptionSection[];
};
render() {
- const { className, language, sections } = this.props;
+ const { className, language, sections, cve } = this.props;
const { contexts, defaultContext, selectedContext } = this.state;
const introductionSection = sections?.find(
language={language}
/>
)}
-
{defaultContext && (
<FlagMessage variant="info" className="sw-mb-4">
{translateWithParameters(
)}
</FlagMessage>
)}
-
<div className="sw-mb-4">
<ToggleButton
label={translate('coding_rules.description_context.title')}
</h2>
)}
</div>
-
{selectedContext.key === OTHERS_KEY ? (
<OtherContextOption />
) : (
language={language}
/>
)}
+
+ {cve && <CveDetails cve={cve} />}
</StyledHtmlFormatter>
);
}
htmlAsString={sanitizeString(sections[0].content)}
language={language}
/>
+
+ {cve && <CveDetails cve={cve} />}
</StyledHtmlFormatter>
);
}
SoftwareQuality,
} from '../types/clean-code-taxonomy';
import { RuleRepository } from '../types/coding-rules';
+import { Cve } from '../types/cves';
import { EditionKey } from '../types/editions';
import {
IssueDeprecatedStatus,
};
}
+export function mockCve(overrides: Partial<Cve> = {}): Cve {
+ return {
+ id: 'CVE-2021-12345',
+ epssPercentile: 0.56051,
+ cvssScore: 0.31051,
+ description: 'description',
+ cwes: ['CWE-79', 'CWE-89'],
+ epssScore: 0.2,
+ lastModifiedAt: '2021-10-04T14:00:00Z',
+ publishedAt: '2021-10-04T14:00:00Z',
+ ...overrides,
+ };
+}
+
export function mockLocation(overrides: Partial<Location> = {}): Location {
return {
hash: '',
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+export type Cve = {
+ cvssScore?: number;
+ cwes: string[];
+ description: string;
+ epssPercentile: number;
+ epssScore: number;
+ id: string;
+ lastModifiedAt: string;
+ publishedAt: string;
+};
comments?: Comment[];
component: string;
creationDate: string;
+ cveId?: string;
flows?: Array<{
description?: string;
locations?: RawFlowLocation[];
author?: string;
component: string;
creationDate: string;
+ cveId?: string;
flows?: Array<{
locations?: Array<Omit<FlowLocation, 'componentName'>>;
}>;
comment: HotspotComment[];
component: HotspotComponent;
creationDate: string;
+ cveId?: string;
flows: { locations: FlowLocation[] }[];
key: string;
line?: number;
hotspots.status_option.SAFE=Safe
hotspots.status_option.SAFE.description=The code has been reviewed and does not pose a risk. It does not need to be modified.
hotspots.get_permalink=Get Permalink
-hotspots.no_associated_lines=Security Hotspot raised on the following file:
+hotspots.no_associated_lines=This Security Hotspot is not associated with any specific lines of code.
hotspots.congratulations=Congratulations!
hotspots.find_in_status_filter_x= You can find it again by setting the status filter to {status_label}.
hotspots.successful_status_change_to_x=The Security Hotspot was successfully changed to {0}.
rule.clean_code_attribute.TRUSTWORTHY=Trustworthy
rule.clean_code_attribute.TRUSTWORTHY.title=This is a responsibility rule, the code should be trustworthy.
-
+rule.cve_details=CVE details
+rule.cve_details.cwes=CWEs
+rule.cve_details.epss_score=EPSS Score
+rule.cve_details.epss_score.value={0}% ({1}th)
+rule.cve_details.cvss_score=CVSS Score
+rule.cve_details.published_date=Published Date
#------------------------------------------------------------------------------
#