Browse Source

SONAR-12718 Create the hotspot details section

tags/8.2.0.32929
Philippe Perrin 4 years ago
parent
commit
31dc599a7a
29 changed files with 1184 additions and 73 deletions
  1. 5
    1
      server/sonar-web/src/main/js/api/security-hotspots.ts
  2. 4
    0
      server/sonar-web/src/main/js/app/styles/init/misc.css
  3. 3
    3
      server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx
  4. 8
    3
      server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx
  5. 4
    4
      server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx
  6. 18
    4
      server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx
  7. 222
    14
      server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap
  8. 11
    11
      server/sonar-web/src/main/js/apps/securityHotspots/__tests__/utils-test.ts
  9. 1
    1
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotCategory.tsx
  10. 2
    2
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx
  11. 1
    1
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotListItem.tsx
  12. 50
    7
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewer.tsx
  13. 69
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerRenderer.tsx
  14. 76
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerTabs.tsx
  15. 3
    3
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotCategory-test.tsx
  16. 7
    7
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx
  17. 3
    3
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotListItem-test.tsx
  18. 56
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewer-test.tsx
  19. 44
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerRenderer-test.tsx
  20. 68
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerTabs-test.tsx
  21. 32
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
  22. 278
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
  23. 131
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap
  24. 1
    0
      server/sonar-web/src/main/js/apps/securityHotspots/styles.css
  25. 3
    6
      server/sonar-web/src/main/js/apps/securityHotspots/utils.ts
  26. 48
    2
      server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts
  27. 27
    1
      server/sonar-web/src/main/js/types/security-hotspots.ts
  28. 2
    0
      server/sonar-web/src/main/js/types/types.d.ts
  29. 7
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

server/sonar-web/src/main/js/api/securityHotspots.ts → server/sonar-web/src/main/js/api/security-hotspots.ts View File

@@ -19,7 +19,7 @@
*/
import { getJSON } from 'sonar-ui-common/helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
import { HotspotSearchResponse } from '../types/securityHotspots';
import { DetailedHotspot, HotspotSearchResponse } from '../types/security-hotspots';

export function getSecurityHotspots(data: {
projectKey: string;
@@ -28,3 +28,7 @@ export function getSecurityHotspots(data: {
}): Promise<HotspotSearchResponse> {
return getJSON('/api/hotspots/search', data).catch(throwGlobalError);
}

export function getSecurityHotspotDetails(securityHotspotKey: string): Promise<DetailedHotspot> {
return getJSON('/api/hotspots/show', { hotspot: securityHotspotKey }).catch(throwGlobalError);
}

+ 4
- 0
server/sonar-web/src/main/js/app/styles/init/misc.css View File

@@ -132,6 +132,10 @@ th.hide-overflow {
margin-top: 4px !important;
}

.big-padded {
padding: calc(2 * var(--gridSize));
}

td.little-spacer-left {
padding-left: 4px !important;
}

+ 3
- 3
server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx View File

@@ -19,10 +19,10 @@
*/
import * as React from 'react';
import { addNoFooterPageClass, removeNoFooterPageClass } from 'sonar-ui-common/helpers/pages';
import { getSecurityHotspots } from '../../api/securityHotspots';
import { getSecurityHotspots } from '../../api/security-hotspots';
import { getStandards } from '../../helpers/security-standard';
import { BranchLike } from '../../types/branch-like';
import { RawHotspot } from '../../types/securityHotspots';
import { RawHotspot } from '../../types/security-hotspots';
import SecurityHotspotsAppRenderer from './SecurityHotspotsAppRenderer';
import './styles.css';
import { sortHotspots } from './utils';
@@ -37,7 +37,7 @@ interface Props {
interface State {
hotspots: RawHotspot[];
loading: boolean;
securityCategories: T.Dict<{ title: string; description?: string }>;
securityCategories: T.StandardSecurityCategories;
selectedHotspotKey: string | undefined;
}


+ 8
- 3
server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx View File

@@ -26,7 +26,7 @@ import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
import A11ySkipTarget from '../../app/components/a11y/A11ySkipTarget';
import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import ScreenPositionHelper from '../../components/common/ScreenPositionHelper';
import { RawHotspot } from '../../types/securityHotspots';
import { RawHotspot } from '../../types/security-hotspots';
import FilterBar from './components/FilterBar';
import HotspotList from './components/HotspotList';
import HotspotViewer from './components/HotspotViewer';
@@ -37,7 +37,7 @@ export interface SecurityHotspotsAppRendererProps {
loading: boolean;
onHotspotClick: (key: string) => void;
selectedHotspotKey?: string;
securityCategories: T.Dict<{ title: string; description?: string }>;
securityCategories: T.StandardSecurityCategories;
}

export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) {
@@ -84,7 +84,12 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
/>
</div>
<div className="main">
<HotspotViewer />
{selectedHotspotKey && (
<HotspotViewer
hotspotKey={selectedHotspotKey}
securityCategories={securityCategories}
/>
)}
</div>
</div>
)}

+ 4
- 4
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx View File

@@ -21,9 +21,9 @@ import { shallow } from 'enzyme';
import * as React from 'react';
import { addNoFooterPageClass } from 'sonar-ui-common/helpers/pages';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { getSecurityHotspots } from '../../../api/securityHotspots';
import { getSecurityHotspots } from '../../../api/security-hotspots';
import { mockMainBranch } from '../../../helpers/mocks/branch-like';
import { mockHotspot } from '../../../helpers/mocks/security-hotspots';
import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
import { getStandards } from '../../../helpers/security-standard';
import { mockComponent } from '../../../helpers/testMocks';
import SecurityHotspotsApp from '../SecurityHotspotsApp';
@@ -33,7 +33,7 @@ jest.mock('sonar-ui-common/helpers/pages', () => ({
removeNoFooterPageClass: jest.fn()
}));

jest.mock('../../../api/securityHotspots', () => ({
jest.mock('../../../api/security-hotspots', () => ({
getSecurityHotspots: jest.fn().mockResolvedValue({ hotspots: [], rules: [] })
}));

@@ -49,7 +49,7 @@ it('should load data correctly', async () => {
const sonarsourceSecurity = { cat1: { title: 'cat 1' } };
(getStandards as jest.Mock).mockResolvedValue({ sonarsourceSecurity });

const hotspots = [mockHotspot()];
const hotspots = [mockRawHotspot()];
(getSecurityHotspots as jest.Mock).mockResolvedValue({
hotspots
});

+ 18
- 4
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx View File

@@ -19,19 +19,33 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockHotspot } from '../../../helpers/mocks/security-hotspots';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
import SecurityHotspotsAppRenderer, {
SecurityHotspotsAppRendererProps
} from '../SecurityHotspotsAppRenderer';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
expect(
shallowRender()
.find(ScreenPositionHelper)
.dive()
).toMatchSnapshot();
});

it('should render correctly with hotspots', () => {
const hotspots = [mockHotspot({ key: 'h1' }), mockHotspot({ key: 'h2' })];
expect(shallowRender({ hotspots })).toMatchSnapshot();
expect(shallowRender({ hotspots, selectedHotspotKey: 'h2' })).toMatchSnapshot();
const hotspots = [mockRawHotspot({ key: 'h1' }), mockRawHotspot({ key: 'h2' })];
expect(
shallowRender({ hotspots })
.find(ScreenPositionHelper)
.dive()
).toMatchSnapshot();
expect(
shallowRender({ hotspots, selectedHotspotKey: 'h2' })
.find(ScreenPositionHelper)
.dive()
).toMatchSnapshot();
});

function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) {

+ 222
- 14
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap View File

@@ -11,24 +11,232 @@ exports[`should render correctly 1`] = `
</div>
`;

exports[`should render correctly 2`] = `
<div>
<div
className="wrapper"
style={
Object {
"top": 0,
}
}
>
<Suggestions
suggestions="security_hotspots"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="hotspots.page"
/>
<A11ySkipTarget
anchor="security_hotspots_main"
/>
<DeferredSpinner
className="huge-spacer-left big-spacer-top"
loading={false}
timeout={100}
>
<div
className="display-flex-column display-flex-center"
>
<img
alt="hotspots.page"
className="huge-spacer-top"
height={166}
src="/images/hotspot-large.svg"
/>
<h1
className="huge-spacer-top"
>
hotspots.no_hotspots.title
</h1>
<div
className="abs-width-400 text-center big-spacer-top"
>
hotspots.no_hotspots.description
</div>
<Link
className="big-spacer-top"
onlyActiveOnIndex={false}
style={Object {}}
target="_blank"
to={
Object {
"pathname": "/documentation/user-guide/security-hotspots/",
}
}
>
hotspots.learn_more
</Link>
</div>
</DeferredSpinner>
</div>
</div>
`;

exports[`should render correctly with hotspots 1`] = `
<div
id="security_hotspots"
>
<FilterBar />
<ScreenPositionHelper>
<Component />
</ScreenPositionHelper>
<div>
<div
className="wrapper"
style={
Object {
"top": 0,
}
}
>
<Suggestions
suggestions="security_hotspots"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="hotspots.page"
/>
<A11ySkipTarget
anchor="security_hotspots_main"
/>
<DeferredSpinner
className="huge-spacer-left big-spacer-top"
loading={false}
timeout={100}
>
<div
className="layout-page"
>
<div
className="sidebar"
>
<HotspotList
hotspots={
Array [
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h1",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": "FALSE-POSITIVE",
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
"status": "RESOLVED",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
},
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": "FALSE-POSITIVE",
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
"status": "RESOLVED",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
},
]
}
onHotspotClick={[MockFunction]}
securityCategories={Object {}}
/>
</div>
<div
className="main"
/>
</div>
</DeferredSpinner>
</div>
</div>
`;

exports[`should render correctly with hotspots 2`] = `
<div
id="security_hotspots"
>
<FilterBar />
<ScreenPositionHelper>
<Component />
</ScreenPositionHelper>
<div>
<div
className="wrapper"
style={
Object {
"top": 0,
}
}
>
<Suggestions
suggestions="security_hotspots"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="hotspots.page"
/>
<A11ySkipTarget
anchor="security_hotspots_main"
/>
<DeferredSpinner
className="huge-spacer-left big-spacer-top"
loading={false}
timeout={100}
>
<div
className="layout-page"
>
<div
className="sidebar"
>
<HotspotList
hotspots={
Array [
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h1",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": "FALSE-POSITIVE",
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
"status": "RESOLVED",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
},
Object {
"author": "Developer 1",
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
"creationDate": "2013-05-13T17:55:39+0200",
"key": "h2",
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
"resolution": "FALSE-POSITIVE",
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
"status": "RESOLVED",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
},
]
}
onHotspotClick={[MockFunction]}
securityCategories={Object {}}
selectedHotspotKey="h2"
/>
</div>
<div
className="main"
>
<HotspotViewer
hotspotKey="h2"
securityCategories={Object {}}
/>
</div>
</div>
</DeferredSpinner>
</div>
</div>
`;

+ 11
- 11
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/utils-test.ts View File

@@ -17,60 +17,60 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { mockHotspot } from '../../../helpers/mocks/security-hotspots';
import { RiskExposure } from '../../../types/securityHotspots';
import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
import { RiskExposure } from '../../../types/security-hotspots';
import { groupByCategory, mapRules, sortHotspots } from '../utils';

const hotspots = [
mockHotspot({
mockRawHotspot({
key: '3',
vulnerabilityProbability: RiskExposure.HIGH,
securityCategory: 'object-injection',
message: 'tfdh'
}),
mockHotspot({
mockRawHotspot({
key: '5',
vulnerabilityProbability: RiskExposure.MEDIUM,
securityCategory: 'xpath-injection',
message: 'asdf'
}),
mockHotspot({
mockRawHotspot({
key: '1',
vulnerabilityProbability: RiskExposure.HIGH,
securityCategory: 'dos',
message: 'a'
}),
mockHotspot({
mockRawHotspot({
key: '7',
vulnerabilityProbability: RiskExposure.LOW,
securityCategory: 'ssrf',
message: 'rrrr'
}),
mockHotspot({
mockRawHotspot({
key: '2',
vulnerabilityProbability: RiskExposure.HIGH,
securityCategory: 'dos',
message: 'b'
}),
mockHotspot({
mockRawHotspot({
key: '8',
vulnerabilityProbability: RiskExposure.LOW,
securityCategory: 'ssrf',
message: 'sssss'
}),
mockHotspot({
mockRawHotspot({
key: '4',
vulnerabilityProbability: RiskExposure.MEDIUM,
securityCategory: 'log-injection',
message: 'asdf'
}),
mockHotspot({
mockRawHotspot({
key: '9',
vulnerabilityProbability: RiskExposure.LOW,
securityCategory: 'xxe',
message: 'aaa'
}),
mockHotspot({
mockRawHotspot({
key: '6',
vulnerabilityProbability: RiskExposure.LOW,
securityCategory: 'xss',

+ 1
- 1
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotCategory.tsx View File

@@ -21,7 +21,7 @@ import * as classNames from 'classnames';
import * as React from 'react';
import ChevronDownIcon from 'sonar-ui-common/components/icons/ChevronDownIcon';
import ChevronUpIcon from 'sonar-ui-common/components/icons/ChevronUpIcon';
import { RawHotspot } from '../../../types/securityHotspots';
import { RawHotspot } from '../../../types/security-hotspots';
import HotspotListItem from './HotspotListItem';

export interface HotspotCategoryProps {

+ 2
- 2
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx View File

@@ -22,7 +22,7 @@ import { groupBy } from 'lodash';
import * as React from 'react';
import SecurityHotspotIcon from 'sonar-ui-common/components/icons/SecurityHotspotIcon';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { RawHotspot, RiskExposure } from '../../../types/securityHotspots';
import { RawHotspot, RiskExposure } from '../../../types/security-hotspots';
import { groupByCategory, RISK_EXPOSURE_LEVELS } from '../utils';
import HotspotCategory from './HotspotCategory';
import './HotspotList.css';
@@ -30,7 +30,7 @@ import './HotspotList.css';
export interface HotspotListProps {
hotspots: RawHotspot[];
onHotspotClick: (key: string) => void;
securityCategories: T.Dict<{ title: string; description?: string }>;
securityCategories: T.StandardSecurityCategories;
selectedHotspotKey: string | undefined;
}


+ 1
- 1
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotListItem.tsx View File

@@ -20,7 +20,7 @@
import * as classNames from 'classnames';
import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { RawHotspot } from '../../../types/securityHotspots';
import { RawHotspot } from '../../../types/security-hotspots';

export interface HotspotListItemProps {
hotspot: RawHotspot;

+ 50
- 7
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewer.tsx View File

@@ -17,14 +17,57 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import * as React from 'react';
import { getSecurityHotspotDetails } from '../../../api/security-hotspots';
import { DetailedHotspot } from '../../../types/security-hotspots';
import HotspotViewerRenderer from './HotspotViewerRenderer';

interface Props {
hotspotKey: string;
securityCategories: T.StandardSecurityCategories;
}

interface State {
hotspot?: DetailedHotspot;
loading: boolean;
}

export default class HotspotViewer extends React.PureComponent<Props, State> {
mounted = false;

componentWillMount() {
this.mounted = true;
this.fetchHotspot();
}

componentDidUpdate(prevProps: Props) {
if (prevProps.hotspotKey !== this.props.hotspotKey) {
this.fetchHotspot();
}
}

componentWillUnmount() {
this.mounted = false;
}

fetchHotspot() {
this.setState({ loading: true });
return getSecurityHotspotDetails(this.props.hotspotKey)
.then(hotspot => this.mounted && this.setState({ hotspot }))
.finally(() => this.mounted && this.setState({ loading: false }));
}

export interface Props {}
render() {
const { securityCategories } = this.props;
const { hotspot, loading } = this.state;

export default function HotspotViewer(props: Props) {
return (
<div {...props} className="hotspot-viewer">
Show hotspot details
</div>
);
return (
<HotspotViewerRenderer
hotspot={hotspot}
loading={loading}
securityCategories={securityCategories}
/>
);
}
}

+ 69
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerRenderer.tsx View File

@@ -0,0 +1,69 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 * as React from 'react';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { DetailedHotspot } from '../../../types/security-hotspots';
import HotspotViewerTabs from './HotspotViewerTabs';

export interface HotspotViewerRendererProps {
hotspot?: DetailedHotspot;
loading: boolean;
securityCategories: T.StandardSecurityCategories;
}

export default function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
const { hotspot, loading, securityCategories } = props;

return (
<DeferredSpinner loading={loading}>
{hotspot && (
<div className="big-padded">
<div className="big-spacer-bottom">
<h1>{hotspot.message}</h1>
<div className="text-muted">
<span>{translate('hotspot.category')}</span>
<span className="little-spacer-left">
{securityCategories[hotspot.rule.securityCategory].title}
</span>
</div>
</div>
<div className="huge-spacer-bottom">
<span>{translate('hotspot.status')}</span>
<span className="badge little-spacer-left">
{translate('issue.status', hotspot.status)}
</span>
{hotspot.assignee && hotspot.assignee.name && (
<>
<span className="huge-spacer-left">{translate('hotspot.assigned_to')}</span>
<strong className="little-spacer-left">
{hotspot.assignee.active
? hotspot.assignee.name
: translateWithParameters('user.x_deleted', hotspot.assignee.name)}
</strong>
</>
)}
</div>
<HotspotViewerTabs hotspot={hotspot} />
</div>
)}
</DeferredSpinner>
);
}

+ 76
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewerTabs.tsx View File

@@ -0,0 +1,76 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { sanitize } from 'dompurify';
import * as React from 'react';
import BoxedTabs from 'sonar-ui-common/components/controls/BoxedTabs';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { DetailedHotspot } from '../../../types/security-hotspots';

export interface HotspotViewerTabsProps {
hotspot: DetailedHotspot;
}

export enum Tabs {
RiskDescription = 'risk',
VulnerabilityDescription = 'vulnerability',
FixRecommendation = 'fix'
}

export default function HotspotViewerTabs(props: HotspotViewerTabsProps) {
const { hotspot } = props;
const [currentTab, setCurrentTab] = React.useState(Tabs.RiskDescription);

const tabs = {
[Tabs.RiskDescription]: {
title: translate('hotspot.tabs.risk_description'),
content: hotspot.rule.riskDescription || ''
},
[Tabs.VulnerabilityDescription]: {
title: translate('hotspot.tabs.vulnerability_description'),
content: hotspot.rule.vulnerabilityDescription || ''
},
[Tabs.FixRecommendation]: {
title: translate('hotspot.tabs.fix_recommendations'),
content: hotspot.rule.fixRecommendations || ''
}
};

const tabsToDisplay = Object.values(Tabs)
.filter(tab => Boolean(tabs[tab].content))
.map(tab => ({ key: tab, label: tabs[tab].title }));

if (tabsToDisplay.length === 0) {
return null;
}

if (!tabsToDisplay.find(tab => tab.key === currentTab)) {
setCurrentTab(tabsToDisplay[0].key);
}

return (
<>
<BoxedTabs onSelect={tab => setCurrentTab(tab)} selected={currentTab} tabs={tabsToDisplay} />
<div
className="boxed-group markdown big-padded"
dangerouslySetInnerHTML={{ __html: sanitize(tabs[currentTab].content) }}
/>
</>
);
}

+ 3
- 3
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotCategory-test.tsx View File

@@ -19,7 +19,7 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockRawHotspot } from '../../../../helpers/mocks/security-hotspots';
import HotspotCategory, { HotspotCategoryProps } from '../HotspotCategory';

it('should render correctly', () => {
@@ -27,12 +27,12 @@ it('should render correctly', () => {
});

it('should render correctly with hotspots', () => {
const hotspots = [mockHotspot({ key: 'h1' }), mockHotspot({ key: 'h2' })];
const hotspots = [mockRawHotspot({ key: 'h1' }), mockRawHotspot({ key: 'h2' })];
expect(shallowRender({ hotspots })).toMatchSnapshot();
});

it('should handle collapse and expand', () => {
const wrapper = shallowRender({ hotspots: [mockHotspot()] });
const wrapper = shallowRender({ hotspots: [mockRawHotspot()] });

wrapper.find('.hotspot-category-header').simulate('click');


+ 7
- 7
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx View File

@@ -19,8 +19,8 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
import { RiskExposure } from '../../../../types/securityHotspots';
import { mockRawHotspot } from '../../../../helpers/mocks/security-hotspots';
import { RiskExposure } from '../../../../types/security-hotspots';
import HotspotList, { HotspotListProps } from '../HotspotList';

it('should render correctly', () => {
@@ -29,19 +29,19 @@ it('should render correctly', () => {

it('should render correctly with hotspots', () => {
const hotspots = [
mockHotspot({ key: 'h1', securityCategory: 'cat2' }),
mockHotspot({ key: 'h2', securityCategory: 'cat1' }),
mockHotspot({
mockRawHotspot({ key: 'h1', securityCategory: 'cat2' }),
mockRawHotspot({ key: 'h2', securityCategory: 'cat1' }),
mockRawHotspot({
key: 'h3',
securityCategory: 'cat1',
vulnerabilityProbability: RiskExposure.MEDIUM
}),
mockHotspot({
mockRawHotspot({
key: 'h4',
securityCategory: 'cat1',
vulnerabilityProbability: RiskExposure.MEDIUM
}),
mockHotspot({
mockRawHotspot({
key: 'h5',
securityCategory: 'cat2',
vulnerabilityProbability: RiskExposure.MEDIUM

+ 3
- 3
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotListItem-test.tsx View File

@@ -19,7 +19,7 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockRawHotspot } from '../../../../helpers/mocks/security-hotspots';
import { HotspotListItem, HotspotListItemProps } from '../HotspotListItem';

it('should render correctly', () => {
@@ -28,7 +28,7 @@ it('should render correctly', () => {
});

it('should handle click', () => {
const hotspot = mockHotspot({ key: 'hotspotKey' });
const hotspot = mockRawHotspot({ key: 'hotspotKey' });
const onClick = jest.fn();
const wrapper = shallowRender({ hotspot, onClick });

@@ -39,6 +39,6 @@ it('should handle click', () => {

function shallowRender(props: Partial<HotspotListItemProps> = {}) {
return shallow(
<HotspotListItem hotspot={mockHotspot()} onClick={jest.fn()} selected={false} {...props} />
<HotspotListItem hotspot={mockRawHotspot()} onClick={jest.fn()} selected={false} {...props} />
);
}

+ 56
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewer-test.tsx View File

@@ -0,0 +1,56 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { getSecurityHotspotDetails } from '../../../../api/security-hotspots';
import HotspotViewer from '../HotspotViewer';

const hotspotKey = 'hotspot-key';

jest.mock('../../../../api/security-hotspots', () => ({
getSecurityHotspotDetails: jest.fn().mockResolvedValue({ id: `I am a detailled hotspot` })
}));

it('should render correctly', async () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();

await waitAndUpdate(wrapper);

expect(wrapper).toMatchSnapshot();
expect(getSecurityHotspotDetails).toHaveBeenCalledWith(hotspotKey);

const newHotspotKey = `new-${hotspotKey}`;
wrapper.setProps({ hotspotKey: newHotspotKey });

await waitAndUpdate(wrapper);
expect(getSecurityHotspotDetails).toHaveBeenCalledWith(newHotspotKey);
});

function shallowRender(props?: Partial<HotspotViewer['props']>) {
return shallow<HotspotViewer>(
<HotspotViewer
hotspotKey={hotspotKey}
securityCategories={{ cat1: { title: 'cat1' } }}
{...props}
/>
);
}

+ 44
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerRenderer-test.tsx View File

@@ -0,0 +1,44 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
import * as React from 'react';
import { mockDetailledHotspot } from '../../../../helpers/mocks/security-hotspots';
import { mockUser } from '../../../../helpers/testMocks';
import HotspotViewerRenderer, { HotspotViewerRendererProps } from '../HotspotViewerRenderer';

it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
expect(shallowRender({ hotspot: undefined })).toMatchSnapshot('no hotspot');
expect(
shallowRender({ hotspot: mockDetailledHotspot({ assignee: mockUser({ active: false }) }) })
).toMatchSnapshot('deleted assignee');
});

function shallowRender(props?: Partial<HotspotViewerRendererProps>) {
return shallow(
<HotspotViewerRenderer
hotspot={mockDetailledHotspot()}
loading={false}
securityCategories={{ 'sql-injection': { title: 'SQL injection' } }}
{...props}
/>
);
}

+ 68
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotViewerTabs-test.tsx View File

@@ -0,0 +1,68 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
import * as React from 'react';
import BoxedTabs from 'sonar-ui-common/components/controls/BoxedTabs';
import {
mockDetailledHotspot,
mockDetailledHotspotRule
} from '../../../../helpers/mocks/security-hotspots';
import HotspotViewerTabs, { HotspotViewerTabsProps, Tabs } from '../HotspotViewerTabs';

it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot('risk');

const onSelect = wrapper.find(BoxedTabs).prop('onSelect') as (tab: Tabs) => void;

if (!onSelect) {
fail('onSelect should be defined');
} else {
onSelect(Tabs.VulnerabilityDescription);
expect(wrapper).toMatchSnapshot('vulnerability');

onSelect(Tabs.FixRecommendation);
expect(wrapper).toMatchSnapshot('fix');
}

expect(
shallowRender({
hotspot: mockDetailledHotspot({
rule: mockDetailledHotspotRule({ riskDescription: undefined })
})
})
).toMatchSnapshot('empty tab');

expect(
shallowRender({
hotspot: mockDetailledHotspot({
rule: mockDetailledHotspotRule({
riskDescription: undefined,
fixRecommendations: undefined,
vulnerabilityDescription: undefined
})
})
})
).toMatchSnapshot('no tabs');
});

function shallowRender(props?: Partial<HotspotViewerTabsProps>) {
return shallow(<HotspotViewerTabs hotspot={mockDetailledHotspot()} {...props} />);
}

+ 32
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap View File

@@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<HotspotViewerRenderer
loading={true}
securityCategories={
Object {
"cat1": Object {
"title": "cat1",
},
}
}
/>
`;

exports[`should render correctly 2`] = `
<HotspotViewerRenderer
hotspot={
Object {
"id": "I am a detailled hotspot",
}
}
loading={false}
securityCategories={
Object {
"cat1": Object {
"title": "cat1",
},
}
}
/>
`;

+ 278
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap View File

@@ -0,0 +1,278 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<DeferredSpinner
loading={false}
timeout={100}
>
<div
className="big-padded"
>
<div
className="big-spacer-bottom"
>
<h1>
'3' is a magic number.
</h1>
<div
className="text-muted"
>
<span>
hotspot.category
</span>
<span
className="little-spacer-left"
>
SQL injection
</span>
</div>
</div>
<div
className="huge-spacer-bottom"
>
<span>
hotspot.status
</span>
<span
className="badge little-spacer-left"
>
issue.status.RESOLVED
</span>
<span
className="huge-spacer-left"
>
hotspot.assigned_to
</span>
<strong
className="little-spacer-left"
>
John Doe
</strong>
</div>
<HotspotViewerTabs
hotspot={
Object {
"assignee": Object {
"active": true,
"local": true,
"login": "john.doe",
"name": "John Doe",
},
"author": Object {
"active": true,
"local": true,
"login": "john.doe",
"name": "John Doe",
},
"component": Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "FIL",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
},
"resolution": "FALSE-POSITIVE",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "RESOLVED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
}
}
/>
</div>
</DeferredSpinner>
`;

exports[`should render correctly: deleted assignee 1`] = `
<DeferredSpinner
loading={false}
timeout={100}
>
<div
className="big-padded"
>
<div
className="big-spacer-bottom"
>
<h1>
'3' is a magic number.
</h1>
<div
className="text-muted"
>
<span>
hotspot.category
</span>
<span
className="little-spacer-left"
>
SQL injection
</span>
</div>
</div>
<div
className="huge-spacer-bottom"
>
<span>
hotspot.status
</span>
<span
className="badge little-spacer-left"
>
issue.status.RESOLVED
</span>
<span
className="huge-spacer-left"
>
hotspot.assigned_to
</span>
<strong
className="little-spacer-left"
>
user.x_deleted.John Doe
</strong>
</div>
<HotspotViewerTabs
hotspot={
Object {
"assignee": Object {
"active": false,
"local": true,
"login": "john.doe",
"name": "John Doe",
},
"author": Object {
"active": true,
"local": true,
"login": "john.doe",
"name": "John Doe",
},
"component": Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "FIL",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
},
"creationDate": "2013-05-13T17:55:41+0200",
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
"line": 142,
"message": "'3' is a magic number.",
"project": Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"organization": "foo",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
},
"resolution": "FALSE-POSITIVE",
"rule": Object {
"fixRecommendations": "<p>This a <strong>strong</strong> message about fixing !</p>",
"key": "squid:S2077",
"name": "That rule",
"riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>",
"securityCategory": "sql-injection",
"vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
"vulnerabilityProbability": "HIGH",
},
"status": "RESOLVED",
"textRange": Object {
"endLine": 142,
"endOffset": 83,
"startLine": 142,
"startOffset": 26,
},
"updateDate": "2013-05-13T17:55:42+0200",
}
}
/>
</div>
</DeferredSpinner>
`;

exports[`should render correctly: no hotspot 1`] = `
<DeferredSpinner
loading={false}
timeout={100}
/>
`;

+ 131
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap View File

@@ -0,0 +1,131 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: empty tab 1`] = `
<Fragment>
<BoxedTabs
onSelect={[Function]}
selected="vulnerability"
tabs={
Array [
Object {
"key": "vulnerability",
"label": "hotspot.tabs.vulnerability_description",
},
Object {
"key": "fix",
"label": "hotspot.tabs.fix_recommendations",
},
]
}
/>
<div
className="boxed-group markdown big-padded"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
}
}
/>
</Fragment>
`;

exports[`should render correctly: fix 1`] = `
<Fragment>
<BoxedTabs
onSelect={[Function]}
selected="fix"
tabs={
Array [
Object {
"key": "risk",
"label": "hotspot.tabs.risk_description",
},
Object {
"key": "vulnerability",
"label": "hotspot.tabs.vulnerability_description",
},
Object {
"key": "fix",
"label": "hotspot.tabs.fix_recommendations",
},
]
}
/>
<div
className="boxed-group markdown big-padded"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about fixing !</p>",
}
}
/>
</Fragment>
`;

exports[`should render correctly: no tabs 1`] = `""`;

exports[`should render correctly: risk 1`] = `
<Fragment>
<BoxedTabs
onSelect={[Function]}
selected="risk"
tabs={
Array [
Object {
"key": "risk",
"label": "hotspot.tabs.risk_description",
},
Object {
"key": "vulnerability",
"label": "hotspot.tabs.vulnerability_description",
},
Object {
"key": "fix",
"label": "hotspot.tabs.fix_recommendations",
},
]
}
/>
<div
className="boxed-group markdown big-padded"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about risk !</p>",
}
}
/>
</Fragment>
`;

exports[`should render correctly: vulnerability 1`] = `
<Fragment>
<BoxedTabs
onSelect={[Function]}
selected="vulnerability"
tabs={
Array [
Object {
"key": "risk",
"label": "hotspot.tabs.risk_description",
},
Object {
"key": "vulnerability",
"label": "hotspot.tabs.vulnerability_description",
},
Object {
"key": "fix",
"label": "hotspot.tabs.fix_recommendations",
},
]
}
/>
<div
className="boxed-group markdown big-padded"
dangerouslySetInnerHTML={
Object {
"__html": "<p>This a <strong>strong</strong> message about vulnerability !</p>",
}
}
/>
</Fragment>
`;

+ 1
- 0
server/sonar-web/src/main/js/apps/securityHotspots/styles.css View File

@@ -48,4 +48,5 @@
#security_hotspots .main {
flex: 1 0 70%;
overflow-y: auto;
background-color: white;
}

+ 3
- 6
server/sonar-web/src/main/js/apps/securityHotspots/utils.ts View File

@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { groupBy, sortBy } from 'lodash';
import { RawHotspot, RiskExposure } from '../../types/securityHotspots';
import { RawHotspot, RiskExposure } from '../../types/security-hotspots';

export const RISK_EXPOSURE_LEVELS = [RiskExposure.HIGH, RiskExposure.MEDIUM, RiskExposure.LOW];

@@ -31,7 +31,7 @@ export function mapRules(rules: Array<{ key: string; name: string }>): T.Dict<st

export function groupByCategory(
hotspots: RawHotspot[] = [],
securityCategories: T.Dict<{ title: string; description?: string }>
securityCategories: T.StandardSecurityCategories
) {
const groups = groupBy(hotspots, h => h.securityCategory);

@@ -56,9 +56,6 @@ export function sortHotspots(
]);
}

function getCategoryTitle(
key: string,
securityCategories: T.Dict<{ title: string; description?: string }>
) {
function getCategoryTitle(key: string, securityCategories: T.StandardSecurityCategories) {
return securityCategories[key] ? securityCategories[key].title : key;
}

+ 48
- 2
server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts View File

@@ -17,9 +17,16 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { RawHotspot, RiskExposure } from '../../types/securityHotspots';
import { ComponentQualifier } from '../../types/component';
import {
DetailedHotspot,
DetailedHotspotRule,
RawHotspot,
RiskExposure
} from '../../types/security-hotspots';
import { mockComponent, mockUser } from '../testMocks';

export function mockHotspot(overrides: Partial<RawHotspot> = {}): RawHotspot {
export function mockRawHotspot(overrides: Partial<RawHotspot> = {}): RawHotspot {
return {
key: '01fc972e-2a3c-433e-bcae-0bd7f88f5123',
component: 'com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest',
@@ -37,3 +44,42 @@ export function mockHotspot(overrides: Partial<RawHotspot> = {}): RawHotspot {
...overrides
};
}

export function mockDetailledHotspot(overrides?: Partial<DetailedHotspot>): DetailedHotspot {
return {
assignee: mockUser(),
author: mockUser(),
component: mockComponent({ qualifier: ComponentQualifier.File }),
creationDate: '2013-05-13T17:55:41+0200',
key: '01fc972e-2a3c-433e-bcae-0bd7f88f5123',
line: 142,
message: "'3' is a magic number.",
project: mockComponent({ qualifier: ComponentQualifier.Project }),
resolution: 'FALSE-POSITIVE',
rule: mockDetailledHotspotRule(),
status: 'RESOLVED',
textRange: {
startLine: 142,
endLine: 142,
startOffset: 26,
endOffset: 83
},
updateDate: '2013-05-13T17:55:42+0200',
...overrides
};
}

export function mockDetailledHotspotRule(
overrides?: Partial<DetailedHotspotRule>
): DetailedHotspotRule {
return {
key: 'squid:S2077',
name: 'That rule',
fixRecommendations: '<p>This a <strong>strong</strong> message about fixing !</p>',
riskDescription: '<p>This a <strong>strong</strong> message about risk !</p>',
vulnerabilityDescription: '<p>This a <strong>strong</strong> message about vulnerability !</p>',
vulnerabilityProbability: RiskExposure.HIGH,
securityCategory: 'sql-injection',
...overrides
};
}

server/sonar-web/src/main/js/types/securityHotspots.ts → server/sonar-web/src/main/js/types/security-hotspots.ts View File

@@ -35,10 +35,36 @@ export interface RawHotspot {
resolution: string;
rule: string;
securityCategory: string;
status: string;
subProject?: string;
updateDate: string;
vulnerabilityProbability: RiskExposure;
}

export interface DetailedHotspot {
assignee?: Pick<T.UserBase, 'active' | 'login' | 'name'>;
author?: Pick<T.UserBase, 'login'>;
component: T.Component;
creationDate: string;
key: string;
line?: number;
message: string;
project: T.Component;
resolution: string;
rule: DetailedHotspotRule;
status: string;
subProject?: string;
textRange: T.TextRange;
updateDate: string;
}

export interface DetailedHotspotRule {
fixRecommendations?: string;
key: string;
name: string;
riskDescription?: string;
securityCategory: string;
vulnerabilityDescription?: string;
vulnerabilityProbability: RiskExposure;
}

export interface HotspotSearchResponse {

+ 2
- 0
server/sonar-web/src/main/js/types/types.d.ts View File

@@ -849,6 +849,8 @@ declare namespace T {
uuid: string;
}

export type StandardSecurityCategories = T.Dict<{ title: string; description?: string }>;

export type Standards = {
[key in StandardType]: T.Dict<{ title: string; description?: string }>;
};

+ 7
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -652,6 +652,13 @@ hotspots.list_title.TO_REVIEW={0} Security Hotspots to review
hotspots.list_title.REVIEWED={0} reviewed Security Hotspots
hotspots.risk_exposure=Review priority:

hotspot.category=Category:
hotspot.status=Status:
hotspot.assigned_to=Assigned to:
hotspot.tabs.risk_description=What's the risk?
hotspot.tabs.vulnerability_description=Are you vulnerable?
hotspot.tabs.fix_recommendations=How can you fix it?

#------------------------------------------------------------------------------
#
# ISSUES

Loading…
Cancel
Save