aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/security-hotspots
diff options
context:
space:
mode:
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>2022-02-16 10:41:39 +0100
committersonartech <sonartech@sonarsource.com>2022-02-25 20:02:54 +0000
commite5474111c8f3e985cfa751d8cd86f1950aaf8e4d (patch)
tree5164d6200ea226175def9007343bd1948507c40a /server/sonar-web/src/main/js/apps/security-hotspots
parent9620694f92f2525837ce822d69f1296bf003ae69 (diff)
downloadsonarqube-e5474111c8f3e985cfa751d8cd86f1950aaf8e4d.tar.gz
sonarqube-e5474111c8f3e985cfa751d8cd86f1950aaf8e4d.zip
SONAR-16007 Showing secondary locations in hotspot list box
Diffstat (limited to 'server/sonar-web/src/main/js/apps/security-hotspots')
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts44
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css4
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx36
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap42
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/utils.ts52
9 files changed, 168 insertions, 18 deletions
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
index 969217f5617..f90fc16bf4b 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
@@ -74,7 +74,8 @@ interface State {
loading: boolean;
loadingMeasure: boolean;
loadingMore: boolean;
- selectedHotspot: RawHotspot | undefined;
+ selectedHotspot?: RawHotspot;
+ selectedHotspotLocation?: number;
standards: Standards;
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
index 715ce19cbff..43f521eb9e4 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
@@ -60,7 +60,8 @@ export interface SecurityHotspotsAppRendererProps {
onShowAllHotspots: () => void;
onSwitchStatusFilter: (option: HotspotStatusFilter) => void;
onUpdateHotspot: (hotspotKey: string) => Promise<void>;
- selectedHotspot: RawHotspot | undefined;
+ selectedHotspot?: RawHotspot;
+ selectedHotspotLocation?: number;
securityCategories: StandardSecurityCategories;
standards: Standards;
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
index d8155826feb..12856d774dd 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
@@ -24,12 +24,14 @@ import {
HotspotStatus,
HotspotStatusFilter,
HotspotStatusOption,
+ RawHotspot,
ReviewHistoryType,
RiskExposure
} from '../../../types/security-hotspots';
-import { IssueChangelog } from '../../../types/types';
+import { FlowLocation, IssueChangelog } from '../../../types/types';
import {
getHotspotReviewHistory,
+ getLocations,
getStatusAndResolutionFromStatusOption,
getStatusFilterFromStatusOption,
getStatusOptionFromStatusAndResolution,
@@ -277,3 +279,43 @@ describe('getStatusFilterFromStatusOption', () => {
);
});
});
+
+describe('getLocations', () => {
+ it('should return the correct value', () => {
+ const location1: FlowLocation = {
+ component: 'foo',
+ msg: 'Do not use foo',
+ textRange: { startLine: 7, endLine: 7, startOffset: 5, endOffset: 8 }
+ };
+
+ const location2: FlowLocation = {
+ component: 'foo2',
+ msg: 'Do not use foo2',
+ textRange: { startLine: 7, endLine: 7, startOffset: 5, endOffset: 8 }
+ };
+
+ let rawFlows: RawHotspot['flows'] = [
+ {
+ locations: [location1]
+ }
+ ];
+ expect(getLocations(rawFlows, undefined)).toEqual([location1]);
+
+ rawFlows = [
+ {
+ locations: [location1, location2]
+ }
+ ];
+ expect(getLocations(rawFlows, undefined)).toEqual([location2, location1]);
+
+ rawFlows = [
+ {
+ locations: [location1, location2]
+ },
+ {
+ locations: []
+ }
+ ];
+ expect(getLocations(rawFlows, 0)).toEqual([location2, location1]);
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx
index 4d2f505c4f2..fa176838b89 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx
@@ -31,6 +31,7 @@ export interface HotspotCategoryProps {
onHotspotClick: (hotspot: RawHotspot) => void;
onToggleExpand?: (categoryKey: string, value: boolean) => void;
selectedHotspot: RawHotspot;
+ selectedHotspotLocation?: number;
title: string;
isLastAndIncomplete: boolean;
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css
index 482112477b2..8fa66008174 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.css
@@ -96,3 +96,7 @@
.hotspot-risk-badge.LOW {
background-color: var(--yellow);
}
+
+.hotspot-box-filename {
+ direction: rtl;
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx
index 8c78d98ab5e..afa5194f180 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotList.tsx
@@ -39,6 +39,7 @@ interface Props {
onLoadMore: () => void;
securityCategories: StandardSecurityCategories;
selectedHotspot: RawHotspot;
+ selectedHotspotLocation?: number;
statusFilter: HotspotStatusFilter;
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
index ed241c168dc..7865080337c 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotListItem.tsx
@@ -19,9 +19,11 @@
*/
import classNames from 'classnames';
import * as React from 'react';
-import { translate } from '../../../helpers/l10n';
+import QualifierIcon from '../../../components/icons/QualifierIcon';
+import { ComponentQualifier } from '../../../types/component';
+import { getFilePath, getLocations } from '../utils';
+import LocationsList from '../../../components/locations/LocationsList';
import { RawHotspot } from '../../../types/security-hotspots';
-import { getStatusOptionFromStatusAndResolution } from '../utils';
export interface HotspotListItemProps {
hotspot: RawHotspot;
@@ -31,16 +33,36 @@ export interface HotspotListItemProps {
export default function HotspotListItem(props: HotspotListItemProps) {
const { hotspot, selected } = props;
+ const locations = getLocations(hotspot.flows, undefined);
+ const path = getFilePath(hotspot.component, hotspot.project);
+
return (
<a
className={classNames('hotspot-item', { highlight: selected })}
href="#"
onClick={() => !selected && props.onClick(hotspot)}>
- <div className="little-spacer-left">{hotspot.message}</div>
- <div className="badge spacer-top">
- {translate(
- 'hotspots.status_option',
- getStatusOptionFromStatusAndResolution(hotspot.status, hotspot.resolution)
+ <div className="little-spacer-left text-bold">{hotspot.message}</div>
+ <div className="display-flex-center">
+ <QualifierIcon qualifier={ComponentQualifier.File} />
+ <div
+ className="little-spacer-left hotspot-box-filename text-ellipsis big-spacer-top big-spacer-bottom"
+ title={path}>
+ {path}
+ </div>
+ </div>
+ <div className="spacer-top">
+ {selected && (
+ <LocationsList
+ locations={locations}
+ isCrossFile={false} // Currently we are not supporting cross file for security hotspot
+ uniqueKey={hotspot.key}
+ onLocationSelect={() => {
+ /* noop */
+ }}
+ scroll={() => {
+ /* noop */
+ }}
+ />
)}
</div>
</a>
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
index b959dae2d97..0d0f163550e 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
@@ -7,15 +7,26 @@ exports[`should render correctly 1`] = `
onClick={[Function]}
>
<div
- className="little-spacer-left"
+ className="little-spacer-left text-bold"
>
'3' is a magic number.
</div>
<div
- className="badge spacer-top"
+ className="display-flex-center"
>
- hotspots.status_option.TO_REVIEW
+ <QualifierIcon
+ qualifier="FIL"
+ />
+ <div
+ className="little-spacer-left hotspot-box-filename text-ellipsis big-spacer-top big-spacer-bottom"
+ title="com.github.kevinsawicki.http.HttpRequest"
+ >
+ com.github.kevinsawicki.http.HttpRequest
+ </div>
</div>
+ <div
+ className="spacer-top"
+ />
</a>
`;
@@ -26,14 +37,33 @@ exports[`should render correctly 2`] = `
onClick={[Function]}
>
<div
- className="little-spacer-left"
+ className="little-spacer-left text-bold"
>
'3' is a magic number.
</div>
<div
- className="badge spacer-top"
+ className="display-flex-center"
+ >
+ <QualifierIcon
+ qualifier="FIL"
+ />
+ <div
+ className="little-spacer-left hotspot-box-filename text-ellipsis big-spacer-top big-spacer-bottom"
+ title="com.github.kevinsawicki.http.HttpRequest"
+ >
+ com.github.kevinsawicki.http.HttpRequest
+ </div>
+ </div>
+ <div
+ className="spacer-top"
>
- hotspots.status_option.TO_REVIEW
+ <LocationsList
+ isCrossFile={false}
+ locations={Array []}
+ onLocationSelect={[Function]}
+ scroll={[Function]}
+ uniqueKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+ />
</div>
</a>
`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts b/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
index b50a669065c..cdcdd8cae45 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
@@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { groupBy, sortBy } from 'lodash';
+import { flatten, groupBy, sortBy } from 'lodash';
import {
renderCWECategory,
renderOwaspTop10Category,
@@ -36,7 +36,12 @@ import {
ReviewHistoryType,
RiskExposure
} from '../../types/security-hotspots';
-import { Dict, SourceViewerFile, StandardSecurityCategories } from '../../types/types';
+import {
+ Dict,
+ FlowLocation,
+ SourceViewerFile,
+ StandardSecurityCategories
+} from '../../types/types';
export const RISK_EXPOSURE_LEVELS = [RiskExposure.HIGH, RiskExposure.MEDIUM, RiskExposure.LOW];
export const SECURITY_STANDARDS = [
@@ -190,3 +195,46 @@ const STATUS_OPTION_TO_STATUS_FILTER = {
export function getStatusFilterFromStatusOption(statusOption: HotspotStatusOption) {
return STATUS_OPTION_TO_STATUS_FILTER[statusOption];
}
+
+function getSecondaryLocations(flows: RawHotspot['flows']) {
+ const parsedFlows: FlowLocation[][] = (flows || [])
+ .filter(flow => flow.locations !== undefined)
+ .map(flow => flow.locations!.filter(location => location.textRange != null))
+ .map(flow =>
+ flow.map(location => {
+ return { ...location };
+ })
+ );
+
+ const onlySecondaryLocations = parsedFlows.every(flow => flow.length === 1);
+
+ return onlySecondaryLocations
+ ? { secondaryLocations: orderLocations(flatten(parsedFlows)), flows: [] }
+ : { secondaryLocations: [], flows: parsedFlows.map(reverseLocations) };
+}
+
+export function getLocations(rawFlows: RawHotspot['flows'], selectedFlowIndex: number | undefined) {
+ const { flows, secondaryLocations } = getSecondaryLocations(rawFlows);
+ if (selectedFlowIndex !== undefined) {
+ return flows[selectedFlowIndex] || [];
+ }
+ return flows.length > 0 ? flows[0] : secondaryLocations;
+}
+
+function orderLocations(locations: FlowLocation[]) {
+ return sortBy(
+ locations,
+ location => location.textRange && location.textRange.startLine,
+ location => location.textRange && location.textRange.startOffset
+ );
+}
+
+function reverseLocations(locations: FlowLocation[]): FlowLocation[] {
+ const x = [...locations];
+ x.reverse();
+ return x;
+}
+
+export function getFilePath(component: string, project: string) {
+ return component.replace(project, '').replace(':', '');
+}