@@ -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); | |||
} |
@@ -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; | |||
} |
@@ -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; | |||
} | |||
@@ -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> | |||
)} |
@@ -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 | |||
}); |
@@ -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> = {}) { |
@@ -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> | |||
`; |
@@ -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', |
@@ -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 { |
@@ -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; | |||
} | |||
@@ -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; |
@@ -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} | |||
/> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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) }} | |||
/> | |||
</> | |||
); | |||
} |
@@ -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'); | |||
@@ -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 |
@@ -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} /> | |||
); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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} />); | |||
} |
@@ -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", | |||
}, | |||
} | |||
} | |||
/> | |||
`; |
@@ -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} | |||
/> | |||
`; |
@@ -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> | |||
`; |
@@ -48,4 +48,5 @@ | |||
#security_hotspots .main { | |||
flex: 1 0 70%; | |||
overflow-y: auto; | |||
background-color: white; | |||
} |
@@ -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; | |||
} |
@@ -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 | |||
}; | |||
} |
@@ -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 { |
@@ -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 }>; | |||
}; |
@@ -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 |