@@ -0,0 +1 @@ | |||
<svg width="75" height="83" xmlns="http://www.w3.org/2000/svg"><path d="M74.03 13.28a5.89 5.89 0 00-3.96-4.52L39.02.18a6.3 6.3 0 00-2.96 0L5.01 8.76a5.54 5.54 0 00-3.96 4.52c-.48 3.35-4.38 33.09 6.74 48.84a53.22 53.22 0 0028.39 20.33c.45.07.9.07 1.36 0 .43.07.87.07 1.3 0A52.8 52.8 0 0067.3 62.12c10.94-15.75 7.16-45.49 6.74-48.84zM67 42a39.5 39.5 0 01-5.92 15.97A54.33 54.33 0 0138 75V42h29zM38 8v33H8.5a158.2 158.2 0 010-25.21L38 8z" fill="#236A97" fill-rule="nonzero"/></svg> |
@@ -0,0 +1,30 @@ | |||
/* | |||
* 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 { getJSON } from 'sonar-ui-common/helpers/request'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
import { HotspotSearchResponse } from '../types/securityHotspots'; | |||
export function getSecurityHotspots(data: { | |||
projectKey: string; | |||
p: number; | |||
ps: number; | |||
}): Promise<HotspotSearchResponse> { | |||
return getJSON('/api/hotspots/search', data).catch(throwGlobalError); | |||
} |
@@ -151,6 +151,18 @@ export class ComponentNavMenu extends React.PureComponent<Props> { | |||
); | |||
} | |||
renderSecurityHotspotsLink() { | |||
return ( | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
to={{ pathname: '/security_hotspots', query: this.getQuery() }}> | |||
{translate('layout.security_hotspots')} | |||
</Link> | |||
</li> | |||
); | |||
} | |||
renderSecurityReports() { | |||
const { branchLike, component } = this.props; | |||
const { extensions = [] } = component; | |||
@@ -488,6 +500,7 @@ export class ComponentNavMenu extends React.PureComponent<Props> { | |||
<NavBarTabs> | |||
{this.renderDashboardLink()} | |||
{this.renderIssuesLink()} | |||
{this.renderSecurityHotspotsLink()} | |||
{this.renderSecurityReports()} | |||
{this.renderComponentMeasuresLink()} | |||
{this.renderCodeLink()} |
@@ -75,6 +75,24 @@ exports[`should work for a branch 1`] = ` | |||
issues.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/security_hotspots", | |||
"query": Object { | |||
"branch": "release", | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
layout.security_hotspots | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -172,6 +190,24 @@ exports[`should work for a branch 2`] = ` | |||
issues.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/security_hotspots", | |||
"query": Object { | |||
"branch": "release", | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
layout.security_hotspots | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -267,6 +303,23 @@ exports[`should work for all qualifiers 1`] = ` | |||
issues.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/security_hotspots", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
layout.security_hotspots | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -456,6 +509,23 @@ exports[`should work for all qualifiers 2`] = ` | |||
issues.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/security_hotspots", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
layout.security_hotspots | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -577,6 +647,23 @@ exports[`should work for all qualifiers 3`] = ` | |||
issues.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/security_hotspots", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
layout.security_hotspots | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -669,6 +756,23 @@ exports[`should work for all qualifiers 4`] = ` | |||
issues.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/security_hotspots", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
layout.security_hotspots | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" |
@@ -161,6 +161,10 @@ | |||
max-width: 980px; | |||
} | |||
.no-footer-page #footer { | |||
display: none; | |||
} | |||
.page-footer-menu-item { | |||
display: inline-block; | |||
} |
@@ -26,6 +26,7 @@ import { IntlProvider } from 'react-intl'; | |||
import { Provider } from 'react-redux'; | |||
import { IndexRoute, Redirect, Route, RouteConfig, RouteProps, Router } from 'react-router'; | |||
import { lazyLoad } from 'sonar-ui-common/components/lazyLoad'; | |||
import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent'; | |||
import { ThemeProvider } from 'sonar-ui-common/components/theme'; | |||
import getHistory from 'sonar-ui-common/helpers/getHistory'; | |||
import aboutRoutes from '../../apps/about/routes'; | |||
@@ -234,6 +235,12 @@ export default function startReactApp( | |||
)} | |||
/> | |||
<Route path="project/issues" component={Issues} /> | |||
<Route | |||
path="security_hotspots" | |||
component={lazyLoadComponent(() => | |||
import('../../apps/securityHotspots/SecurityHotspotsApp') | |||
)} | |||
/> | |||
<RouteWithChildRoutes | |||
path="project/quality_gate" | |||
childRoutes={projectQualityGateRoutes} |
@@ -0,0 +1,111 @@ | |||
/* | |||
* 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 { addNoFooterPageClass, removeNoFooterPageClass } from 'sonar-ui-common/helpers/pages'; | |||
import { getSecurityHotspots } from '../../api/securityHotspots'; | |||
import { getStandards } from '../../helpers/security-standard'; | |||
import { BranchLike } from '../../types/branch-like'; | |||
import { RawHotspot } from '../../types/securityHotspots'; | |||
import SecurityHotspotsAppRenderer from './SecurityHotspotsAppRenderer'; | |||
import './styles.css'; | |||
import { sortHotspots } from './utils'; | |||
const PAGE_SIZE = 500; | |||
interface Props { | |||
branchLike?: BranchLike; | |||
component: T.Component; | |||
} | |||
interface State { | |||
hotspots: RawHotspot[]; | |||
loading: boolean; | |||
securityCategories: T.Dict<{ title: string; description?: string }>; | |||
selectedHotspotKey: string | undefined; | |||
} | |||
export default class SecurityHotspotsApp extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state = { | |||
loading: true, | |||
hotspots: [], | |||
securityCategories: {}, | |||
selectedHotspotKey: undefined | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
addNoFooterPageClass(); | |||
this.fetchInitialData(); | |||
} | |||
componentDidUpdate(previous: Props) { | |||
if (this.props.component.key !== previous.component.key) { | |||
this.fetchInitialData(); | |||
} | |||
} | |||
componentWillUnmount() { | |||
removeNoFooterPageClass(); | |||
this.mounted = false; | |||
} | |||
fetchInitialData() { | |||
return Promise.all([ | |||
getStandards(), | |||
getSecurityHotspots({ projectKey: this.props.component.key, p: 1, ps: PAGE_SIZE }) | |||
]) | |||
.then(([{ sonarsourceSecurity }, response]) => { | |||
if (!this.mounted) { | |||
return; | |||
} | |||
const hotspots = sortHotspots(response.hotspots, sonarsourceSecurity); | |||
this.setState({ | |||
hotspots, | |||
loading: false, | |||
securityCategories: sonarsourceSecurity, | |||
selectedHotspotKey: hotspots.length > 0 ? hotspots[0].key : undefined | |||
}); | |||
}) | |||
.catch(() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}); | |||
} | |||
handleHotspotClick = (key: string) => this.setState({ selectedHotspotKey: key }); | |||
render() { | |||
const { hotspots, loading, securityCategories, selectedHotspotKey } = this.state; | |||
return ( | |||
<SecurityHotspotsAppRenderer | |||
hotspots={hotspots} | |||
loading={loading} | |||
onHotspotClick={this.handleHotspotClick} | |||
securityCategories={securityCategories} | |||
selectedHotspotKey={selectedHotspotKey} | |||
/> | |||
); | |||
} | |||
} |
@@ -0,0 +1,97 @@ | |||
/* | |||
* 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 { Helmet } from 'react-helmet-async'; | |||
import { Link } from 'react-router'; | |||
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
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 FilterBar from './components/FilterBar'; | |||
import HotspotList from './components/HotspotList'; | |||
import HotspotViewer from './components/HotspotViewer'; | |||
import './styles.css'; | |||
export interface SecurityHotspotsAppRendererProps { | |||
hotspots: RawHotspot[]; | |||
loading: boolean; | |||
onHotspotClick: (key: string) => void; | |||
selectedHotspotKey?: string; | |||
securityCategories: T.Dict<{ title: string; description?: string }>; | |||
} | |||
export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) { | |||
const { hotspots, loading, securityCategories, selectedHotspotKey } = props; | |||
return ( | |||
<div id="security_hotspots"> | |||
<FilterBar /> | |||
<ScreenPositionHelper> | |||
{({ top }) => ( | |||
<div className="wrapper" style={{ top }}> | |||
<Suggestions suggestions="security_hotspots" /> | |||
<Helmet title={translate('hotspots.page')} /> | |||
<A11ySkipTarget anchor="security_hotspots_main" /> | |||
<DeferredSpinner className="huge-spacer-left big-spacer-top" loading={loading}> | |||
{hotspots.length === 0 ? ( | |||
<div className="display-flex-column display-flex-center"> | |||
<img | |||
alt={translate('hotspots.page')} | |||
className="huge-spacer-top" | |||
height={166} | |||
src={`${getBaseUrl()}/images/hotspot-large.svg`} | |||
/> | |||
<h1 className="huge-spacer-top">{translate('hotspots.no_hotspots.title')}</h1> | |||
<div className="abs-width-400 text-center big-spacer-top"> | |||
{translate('hotspots.no_hotspots.description')} | |||
</div> | |||
<Link | |||
className="big-spacer-top" | |||
target="_blank" | |||
to={{ pathname: '/documentation/user-guide/security-hotspots/' }}> | |||
{translate('hotspots.learn_more')} | |||
</Link> | |||
</div> | |||
) : ( | |||
<div className="layout-page"> | |||
<div className="sidebar"> | |||
<HotspotList | |||
hotspots={hotspots} | |||
onHotspotClick={props.onHotspotClick} | |||
securityCategories={securityCategories} | |||
selectedHotspotKey={selectedHotspotKey} | |||
/> | |||
</div> | |||
<div className="main"> | |||
<HotspotViewer /> | |||
</div> | |||
</div> | |||
)} | |||
</DeferredSpinner> | |||
</div> | |||
)} | |||
</ScreenPositionHelper> | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,79 @@ | |||
/* | |||
* 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 { addNoFooterPageClass } from 'sonar-ui-common/helpers/pages'; | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { getSecurityHotspots } from '../../../api/securityHotspots'; | |||
import { mockMainBranch } from '../../../helpers/mocks/branch-like'; | |||
import { mockHotspot } from '../../../helpers/mocks/security-hotspots'; | |||
import { getStandards } from '../../../helpers/security-standard'; | |||
import { mockComponent } from '../../../helpers/testMocks'; | |||
import SecurityHotspotsApp from '../SecurityHotspotsApp'; | |||
jest.mock('sonar-ui-common/helpers/pages', () => ({ | |||
addNoFooterPageClass: jest.fn(), | |||
removeNoFooterPageClass: jest.fn() | |||
})); | |||
jest.mock('../../../api/securityHotspots', () => ({ | |||
getSecurityHotspots: jest.fn().mockResolvedValue({ hotspots: [], rules: [] }) | |||
})); | |||
jest.mock('../../../helpers/security-standard', () => ({ | |||
getStandards: jest.fn() | |||
})); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should load data correctly', async () => { | |||
const sonarsourceSecurity = { cat1: { title: 'cat 1' } }; | |||
(getStandards as jest.Mock).mockResolvedValue({ sonarsourceSecurity }); | |||
const hotspots = [mockHotspot()]; | |||
(getSecurityHotspots as jest.Mock).mockResolvedValue({ | |||
hotspots | |||
}); | |||
const wrapper = shallowRender(); | |||
expect(wrapper.state().loading).toBe(true); | |||
expect(addNoFooterPageClass).toBeCalled(); | |||
expect(getStandards).toBeCalled(); | |||
expect(getSecurityHotspots).toBeCalled(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().loading).toBe(false); | |||
expect(wrapper.state().hotspots).toEqual(hotspots); | |||
expect(wrapper.state().selectedHotspotKey).toBe(hotspots[0].key); | |||
expect(wrapper.state().securityCategories).toBe(sonarsourceSecurity); | |||
expect(wrapper.state()); | |||
}); | |||
function shallowRender(props: Partial<SecurityHotspotsApp['props']> = {}) { | |||
return shallow<SecurityHotspotsApp>( | |||
<SecurityHotspotsApp branchLike={mockMainBranch()} component={mockComponent()} {...props} /> | |||
); | |||
} |
@@ -0,0 +1,47 @@ | |||
/* | |||
* 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 { mockHotspot } from '../../../helpers/mocks/security-hotspots'; | |||
import SecurityHotspotsAppRenderer, { | |||
SecurityHotspotsAppRendererProps | |||
} from '../SecurityHotspotsAppRenderer'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).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(); | |||
}); | |||
function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) { | |||
return shallow( | |||
<SecurityHotspotsAppRenderer | |||
hotspots={[]} | |||
loading={false} | |||
onHotspotClick={jest.fn()} | |||
securityCategories={{}} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,10 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<SecurityHotspotsAppRenderer | |||
hotspots={Array []} | |||
loading={true} | |||
onHotspotClick={[Function]} | |||
securityCategories={Object {}} | |||
/> | |||
`; |
@@ -0,0 +1,34 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
id="security_hotspots" | |||
> | |||
<FilterBar /> | |||
<ScreenPositionHelper> | |||
<Component /> | |||
</ScreenPositionHelper> | |||
</div> | |||
`; | |||
exports[`should render correctly with hotspots 1`] = ` | |||
<div | |||
id="security_hotspots" | |||
> | |||
<FilterBar /> | |||
<ScreenPositionHelper> | |||
<Component /> | |||
</ScreenPositionHelper> | |||
</div> | |||
`; | |||
exports[`should render correctly with hotspots 2`] = ` | |||
<div | |||
id="security_hotspots" | |||
> | |||
<FilterBar /> | |||
<ScreenPositionHelper> | |||
<Component /> | |||
</ScreenPositionHelper> | |||
</div> | |||
`; |
@@ -0,0 +1,144 @@ | |||
/* | |||
* 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 { mockHotspot } from '../../../helpers/mocks/security-hotspots'; | |||
import { RiskExposure } from '../../../types/securityHotspots'; | |||
import { groupByCategory, mapRules, sortHotspots } from '../utils'; | |||
const hotspots = [ | |||
mockHotspot({ | |||
key: '3', | |||
vulnerabilityProbability: RiskExposure.HIGH, | |||
securityCategory: 'object-injection', | |||
message: 'tfdh' | |||
}), | |||
mockHotspot({ | |||
key: '5', | |||
vulnerabilityProbability: RiskExposure.MEDIUM, | |||
securityCategory: 'xpath-injection', | |||
message: 'asdf' | |||
}), | |||
mockHotspot({ | |||
key: '1', | |||
vulnerabilityProbability: RiskExposure.HIGH, | |||
securityCategory: 'dos', | |||
message: 'a' | |||
}), | |||
mockHotspot({ | |||
key: '7', | |||
vulnerabilityProbability: RiskExposure.LOW, | |||
securityCategory: 'ssrf', | |||
message: 'rrrr' | |||
}), | |||
mockHotspot({ | |||
key: '2', | |||
vulnerabilityProbability: RiskExposure.HIGH, | |||
securityCategory: 'dos', | |||
message: 'b' | |||
}), | |||
mockHotspot({ | |||
key: '8', | |||
vulnerabilityProbability: RiskExposure.LOW, | |||
securityCategory: 'ssrf', | |||
message: 'sssss' | |||
}), | |||
mockHotspot({ | |||
key: '4', | |||
vulnerabilityProbability: RiskExposure.MEDIUM, | |||
securityCategory: 'log-injection', | |||
message: 'asdf' | |||
}), | |||
mockHotspot({ | |||
key: '9', | |||
vulnerabilityProbability: RiskExposure.LOW, | |||
securityCategory: 'xxe', | |||
message: 'aaa' | |||
}), | |||
mockHotspot({ | |||
key: '6', | |||
vulnerabilityProbability: RiskExposure.LOW, | |||
securityCategory: 'xss', | |||
message: 'zzz' | |||
}) | |||
]; | |||
const categories = { | |||
'object-injection': { | |||
title: 'Object Injection' | |||
}, | |||
'xpath-injection': { | |||
title: 'XPath Injection' | |||
}, | |||
'log-injection': { | |||
title: 'Log Injection' | |||
}, | |||
dos: { | |||
title: 'Denial of Service (DoS)' | |||
}, | |||
ssrf: { | |||
title: 'Server-Side Request Forgery (SSRF)' | |||
}, | |||
xxe: { | |||
title: 'XML External Entity (XXE)' | |||
}, | |||
xss: { | |||
title: 'Cross-Site Scripting (XSS)' | |||
} | |||
}; | |||
describe('sortHotspots', () => { | |||
it('should sort properly', () => { | |||
const result = sortHotspots(hotspots, categories); | |||
expect(result.map(h => h.key)).toEqual(['1', '2', '3', '4', '5', '6', '7', '8', '9']); | |||
}); | |||
}); | |||
describe('groupByCategory', () => { | |||
it('should group and sort properly', () => { | |||
const result = groupByCategory(hotspots, categories); | |||
expect(result).toHaveLength(7); | |||
expect(result.map(g => g.key)).toEqual([ | |||
'xss', | |||
'dos', | |||
'log-injection', | |||
'object-injection', | |||
'ssrf', | |||
'xxe', | |||
'xpath-injection' | |||
]); | |||
}); | |||
}); | |||
describe('mapRules', () => { | |||
it('should map names to keys', () => { | |||
const rules = [ | |||
{ key: 'a', name: 'A rule' }, | |||
{ key: 'b', name: 'B rule' }, | |||
{ key: 'c', name: 'C rule' } | |||
]; | |||
expect(mapRules(rules)).toEqual({ | |||
a: 'A rule', | |||
b: 'B rule', | |||
c: 'C rule' | |||
}); | |||
}); | |||
}); |
@@ -0,0 +1,30 @@ | |||
/* | |||
* 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'; | |||
export interface FilterBarProps {} | |||
export default function FilterBar(props: FilterBarProps) { | |||
return ( | |||
<div className="filter-bar display-flex-center"> | |||
<h3 {...props}>Filter</h3> | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,79 @@ | |||
/* | |||
* 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 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 HotspotListItem from './HotspotListItem'; | |||
export interface HotspotCategoryProps { | |||
category: { | |||
key: string; | |||
title: string; | |||
}; | |||
hotspots: RawHotspot[]; | |||
onHotspotClick: (key: string) => void; | |||
selectedHotspotKey: string | undefined; | |||
} | |||
export default function HotspotCategory(props: HotspotCategoryProps) { | |||
const { category, hotspots, selectedHotspotKey } = props; | |||
const [expanded, setExpanded] = React.useState(true); | |||
if (hotspots.length < 1) { | |||
return null; | |||
} | |||
const risk = hotspots[0].vulnerabilityProbability; | |||
return ( | |||
<div className={classNames('hotspot-category', risk)}> | |||
<a | |||
className="hotspot-category-header display-flex-space-between display-flex-center" | |||
href="#" | |||
onClick={() => setExpanded(!expanded)}> | |||
<strong className="flex-1">{category.title}</strong> | |||
<span> | |||
<span className="hotspot-counter">{hotspots.length}</span> | |||
{expanded ? ( | |||
<ChevronUpIcon className="big-spacer-left" /> | |||
) : ( | |||
<ChevronDownIcon className="big-spacer-left" /> | |||
)} | |||
</span> | |||
</a> | |||
{expanded && ( | |||
<ul> | |||
{hotspots.map(h => ( | |||
<li key={h.key}> | |||
<HotspotListItem | |||
hotspot={h} | |||
onClick={props.onHotspotClick} | |||
selected={h.key === selectedHotspotKey} | |||
/> | |||
</li> | |||
))} | |||
</ul> | |||
)} | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,104 @@ | |||
/* | |||
* 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. | |||
*/ | |||
.hotspot-list-header { | |||
padding: calc(2 * var(--gridSize)) var(--gridSize); | |||
} | |||
.hotspot-risk-header { | |||
padding: var(--gridSize); | |||
} | |||
.hotspot-category { | |||
background-color: white; | |||
border: 1px solid var(--barBorderColor); | |||
} | |||
.hotspot-category .hotspot-category-header { | |||
padding: calc(2 * var(--gridSize)) var(--gridSize); | |||
color: var(--baseFontColor); | |||
border-bottom: none; | |||
border-left: 4px solid; | |||
} | |||
.hotspot-category .hotspot-category-header:hover { | |||
color: var(--blue); | |||
} | |||
.hotspot-category.HIGH .hotspot-category-header { | |||
border-left-color: var(--red); | |||
} | |||
.hotspot-category.MEDIUM .hotspot-category-header { | |||
border-left-color: var(--orange); | |||
} | |||
.hotspot-category.LOW .hotspot-category-header { | |||
border-left-color: var(--yellow); | |||
} | |||
.hotspot-item { | |||
color: var(--baseFontColor); | |||
display: block; | |||
padding: var(--gridSize) calc(2 * var(--gridSize)); | |||
border: 1px solid transparent; | |||
border-top-color: var(--barBorderColor); | |||
transition: padding 0s, border 0s; | |||
} | |||
.hotspot-item:hover { | |||
background-color: var(--veryLightBlue); | |||
border: 1px dashed var(--blue); | |||
color: var(--baseFontColor); | |||
} | |||
.hotspot-item.highlight { | |||
background-color: var(--veryLightBlue); | |||
color: var(--baseFontColor); | |||
border: 1px solid var(--blue); | |||
cursor: unset; | |||
} | |||
.hotspot-counter { | |||
color: var(--baseFontColor); | |||
background-color: var(--gray94); | |||
border-radius: 50%; | |||
padding: calc(var(--gridSize) / 2) var(--gridSize); | |||
} | |||
.hotspot-risk-badge { | |||
color: white; | |||
text-transform: uppercase; | |||
display: inline-block; | |||
text-align: center; | |||
min-width: 48px; | |||
padding: 0 var(--gridSize); | |||
font-weight: bold; | |||
border-radius: 2px; | |||
} | |||
.hotspot-risk-badge.HIGH { | |||
background-color: var(--red); | |||
} | |||
.hotspot-risk-badge.MEDIUM { | |||
background-color: var(--orange); | |||
} | |||
.hotspot-risk-badge.LOW { | |||
background-color: var(--yellow); | |||
} |
@@ -0,0 +1,84 @@ | |||
/* | |||
* 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 classNames from 'classnames'; | |||
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 { groupByCategory, RISK_EXPOSURE_LEVELS } from '../utils'; | |||
import HotspotCategory from './HotspotCategory'; | |||
import './HotspotList.css'; | |||
export interface HotspotListProps { | |||
hotspots: RawHotspot[]; | |||
onHotspotClick: (key: string) => void; | |||
securityCategories: T.Dict<{ title: string; description?: string }>; | |||
selectedHotspotKey: string | undefined; | |||
} | |||
export default function HotspotList(props: HotspotListProps) { | |||
const { hotspots, securityCategories, selectedHotspotKey } = props; | |||
const groupedHotspots: Array<{ | |||
risk: RiskExposure; | |||
categories: Array<{ key: string; hotspots: RawHotspot[]; title: string }>; | |||
}> = React.useMemo(() => { | |||
const risks = groupBy(hotspots, h => h.vulnerabilityProbability); | |||
return RISK_EXPOSURE_LEVELS.map(risk => ({ | |||
risk, | |||
categories: groupByCategory(risks[risk], securityCategories) | |||
})).filter(risk => risk.categories.length > 0); | |||
}, [hotspots, securityCategories]); | |||
return ( | |||
<> | |||
<h1 className="hotspot-list-header bordered-bottom"> | |||
<SecurityHotspotIcon className="spacer-right" /> | |||
{translateWithParameters(`hotspots.list_title.TO_REVIEW`, hotspots.length)} | |||
</h1> | |||
<ul className="huge-spacer-bottom"> | |||
{groupedHotspots.map(riskGroup => ( | |||
<li className="big-spacer-bottom" key={riskGroup.risk}> | |||
<div className="hotspot-risk-header little-spacer-left"> | |||
<span>{translate('hotspots.risk_exposure')}</span> | |||
<div className={classNames('hotspot-risk-badge', 'spacer-left', riskGroup.risk)}> | |||
{translate('risk_exposure', riskGroup.risk)} | |||
</div> | |||
</div> | |||
<ul> | |||
{riskGroup.categories.map(cat => ( | |||
<li className="spacer-bottom" key={cat.key}> | |||
<HotspotCategory | |||
category={{ key: cat.key, title: cat.title }} | |||
hotspots={cat.hotspots} | |||
onHotspotClick={props.onHotspotClick} | |||
selectedHotspotKey={selectedHotspotKey} | |||
/> | |||
</li> | |||
))} | |||
</ul> | |||
</li> | |||
))} | |||
</ul> | |||
</> | |||
); | |||
} |
@@ -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 * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { RawHotspot } from '../../../types/securityHotspots'; | |||
export interface HotspotListItemProps { | |||
hotspot: RawHotspot; | |||
onClick: (key: string) => void; | |||
selected: boolean; | |||
} | |||
export function HotspotListItem(props: HotspotListItemProps) { | |||
const { hotspot, selected } = props; | |||
return ( | |||
<a | |||
className={classNames('hotspot-item', { highlight: selected })} | |||
href="#" | |||
onClick={() => !selected && props.onClick(hotspot.key)}> | |||
<div className="little-spacer-left">{hotspot.message}</div> | |||
<div className="badge spacer-top">{translate('issue.status', hotspot.status)}</div> | |||
</a> | |||
); | |||
} | |||
export default React.memo(HotspotListItem); |
@@ -0,0 +1,30 @@ | |||
/* | |||
* 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'; | |||
export interface Props {} | |||
export default function HotspotViewer(props: Props) { | |||
return ( | |||
<div {...props} className="hotspot-viewer"> | |||
Show hotspot details | |||
</div> | |||
); | |||
} |
@@ -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 { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import HotspotCategory, { HotspotCategoryProps } from '../HotspotCategory'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should render correctly with hotspots', () => { | |||
const hotspots = [mockHotspot({ key: 'h1' }), mockHotspot({ key: 'h2' })]; | |||
expect(shallowRender({ hotspots })).toMatchSnapshot(); | |||
}); | |||
it('should handle collapse and expand', () => { | |||
const wrapper = shallowRender({ hotspots: [mockHotspot()] }); | |||
wrapper.find('.hotspot-category-header').simulate('click'); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.find('.hotspot-category-header').simulate('click'); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<HotspotCategoryProps> = {}) { | |||
return shallow( | |||
<HotspotCategory | |||
category={{ key: 'class-injection', title: 'Class Injection' }} | |||
hotspots={[]} | |||
onHotspotClick={jest.fn()} | |||
selectedHotspotKey="" | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,63 @@ | |||
/* | |||
* 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 { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { RiskExposure } from '../../../../types/securityHotspots'; | |||
import HotspotList, { HotspotListProps } from '../HotspotList'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should render correctly with hotspots', () => { | |||
const hotspots = [ | |||
mockHotspot({ key: 'h1', securityCategory: 'cat2' }), | |||
mockHotspot({ key: 'h2', securityCategory: 'cat1' }), | |||
mockHotspot({ | |||
key: 'h3', | |||
securityCategory: 'cat1', | |||
vulnerabilityProbability: RiskExposure.MEDIUM | |||
}), | |||
mockHotspot({ | |||
key: 'h4', | |||
securityCategory: 'cat1', | |||
vulnerabilityProbability: RiskExposure.MEDIUM | |||
}), | |||
mockHotspot({ | |||
key: 'h5', | |||
securityCategory: 'cat2', | |||
vulnerabilityProbability: RiskExposure.MEDIUM | |||
}) | |||
]; | |||
expect(shallowRender({ hotspots })).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<HotspotListProps> = {}) { | |||
return shallow( | |||
<HotspotList | |||
hotspots={[]} | |||
onHotspotClick={jest.fn()} | |||
securityCategories={{}} | |||
selectedHotspotKey="h2" | |||
{...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 { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { HotspotListItem, HotspotListItemProps } from '../HotspotListItem'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ selected: true })).toMatchSnapshot(); | |||
}); | |||
it('should handle click', () => { | |||
const hotspot = mockHotspot({ key: 'hotspotKey' }); | |||
const onClick = jest.fn(); | |||
const wrapper = shallowRender({ hotspot, onClick }); | |||
wrapper.simulate('click'); | |||
expect(onClick).toBeCalledWith(hotspot.key); | |||
}); | |||
function shallowRender(props: Partial<HotspotListItemProps> = {}) { | |||
return shallow( | |||
<HotspotListItem hotspot={mockHotspot()} onClick={jest.fn()} selected={false} {...props} /> | |||
); | |||
} |
@@ -0,0 +1,166 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should handle collapse and expand 1`] = ` | |||
<div | |||
className="hotspot-category HIGH" | |||
> | |||
<a | |||
className="hotspot-category-header display-flex-space-between display-flex-center" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<strong | |||
className="flex-1" | |||
> | |||
Class Injection | |||
</strong> | |||
<span> | |||
<span | |||
className="hotspot-counter" | |||
> | |||
1 | |||
</span> | |||
<ChevronDownIcon | |||
className="big-spacer-left" | |||
/> | |||
</span> | |||
</a> | |||
</div> | |||
`; | |||
exports[`should handle collapse and expand 2`] = ` | |||
<div | |||
className="hotspot-category HIGH" | |||
> | |||
<a | |||
className="hotspot-category-header display-flex-space-between display-flex-center" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<strong | |||
className="flex-1" | |||
> | |||
Class Injection | |||
</strong> | |||
<span> | |||
<span | |||
className="hotspot-counter" | |||
> | |||
1 | |||
</span> | |||
<ChevronUpIcon | |||
className="big-spacer-left" | |||
/> | |||
</span> | |||
</a> | |||
<ul> | |||
<li | |||
key="01fc972e-2a3c-433e-bcae-0bd7f88f5123" | |||
> | |||
<Memo(HotspotListItem) | |||
hotspot={ | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123", | |||
"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", | |||
} | |||
} | |||
onClick={[MockFunction]} | |||
selected={false} | |||
/> | |||
</li> | |||
</ul> | |||
</div> | |||
`; | |||
exports[`should render correctly 1`] = `""`; | |||
exports[`should render correctly with hotspots 1`] = ` | |||
<div | |||
className="hotspot-category HIGH" | |||
> | |||
<a | |||
className="hotspot-category-header display-flex-space-between display-flex-center" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<strong | |||
className="flex-1" | |||
> | |||
Class Injection | |||
</strong> | |||
<span> | |||
<span | |||
className="hotspot-counter" | |||
> | |||
2 | |||
</span> | |||
<ChevronUpIcon | |||
className="big-spacer-left" | |||
/> | |||
</span> | |||
</a> | |||
<ul> | |||
<li | |||
key="h1" | |||
> | |||
<Memo(HotspotListItem) | |||
hotspot={ | |||
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", | |||
} | |||
} | |||
onClick={[MockFunction]} | |||
selected={false} | |||
/> | |||
</li> | |||
<li | |||
key="h2" | |||
> | |||
<Memo(HotspotListItem) | |||
hotspot={ | |||
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", | |||
} | |||
} | |||
onClick={[MockFunction]} | |||
selected={false} | |||
/> | |||
</li> | |||
</ul> | |||
</div> | |||
`; |
@@ -0,0 +1,223 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<Fragment> | |||
<h1 | |||
className="hotspot-list-header bordered-bottom" | |||
> | |||
<SecurityHotspotIcon | |||
className="spacer-right" | |||
/> | |||
hotspots.list_title.TO_REVIEW.0 | |||
</h1> | |||
<ul | |||
className="huge-spacer-bottom" | |||
/> | |||
</Fragment> | |||
`; | |||
exports[`should render correctly with hotspots 1`] = ` | |||
<Fragment> | |||
<h1 | |||
className="hotspot-list-header bordered-bottom" | |||
> | |||
<SecurityHotspotIcon | |||
className="spacer-right" | |||
/> | |||
hotspots.list_title.TO_REVIEW.5 | |||
</h1> | |||
<ul | |||
className="huge-spacer-bottom" | |||
> | |||
<li | |||
className="big-spacer-bottom" | |||
key="HIGH" | |||
> | |||
<div | |||
className="hotspot-risk-header little-spacer-left" | |||
> | |||
<span> | |||
hotspots.risk_exposure | |||
</span> | |||
<div | |||
className="hotspot-risk-badge spacer-left HIGH" | |||
> | |||
risk_exposure.HIGH | |||
</div> | |||
</div> | |||
<ul> | |||
<li | |||
className="spacer-bottom" | |||
key="cat1" | |||
> | |||
<HotspotCategory | |||
category={ | |||
Object { | |||
"key": "cat1", | |||
"title": "cat1", | |||
} | |||
} | |||
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": "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": "cat1", | |||
"status": "RESOLVED", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
}, | |||
] | |||
} | |||
onHotspotClick={[MockFunction]} | |||
selectedHotspotKey="h2" | |||
/> | |||
</li> | |||
<li | |||
className="spacer-bottom" | |||
key="cat2" | |||
> | |||
<HotspotCategory | |||
category={ | |||
Object { | |||
"key": "cat2", | |||
"title": "cat2", | |||
} | |||
} | |||
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": "cat2", | |||
"status": "RESOLVED", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "HIGH", | |||
}, | |||
] | |||
} | |||
onHotspotClick={[MockFunction]} | |||
selectedHotspotKey="h2" | |||
/> | |||
</li> | |||
</ul> | |||
</li> | |||
<li | |||
className="big-spacer-bottom" | |||
key="MEDIUM" | |||
> | |||
<div | |||
className="hotspot-risk-header little-spacer-left" | |||
> | |||
<span> | |||
hotspots.risk_exposure | |||
</span> | |||
<div | |||
className="hotspot-risk-badge spacer-left MEDIUM" | |||
> | |||
risk_exposure.MEDIUM | |||
</div> | |||
</div> | |||
<ul> | |||
<li | |||
className="spacer-bottom" | |||
key="cat1" | |||
> | |||
<HotspotCategory | |||
category={ | |||
Object { | |||
"key": "cat1", | |||
"title": "cat1", | |||
} | |||
} | |||
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": "h3", | |||
"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": "cat1", | |||
"status": "RESOLVED", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "MEDIUM", | |||
}, | |||
Object { | |||
"author": "Developer 1", | |||
"component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest", | |||
"creationDate": "2013-05-13T17:55:39+0200", | |||
"key": "h4", | |||
"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": "cat1", | |||
"status": "RESOLVED", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "MEDIUM", | |||
}, | |||
] | |||
} | |||
onHotspotClick={[MockFunction]} | |||
selectedHotspotKey="h2" | |||
/> | |||
</li> | |||
<li | |||
className="spacer-bottom" | |||
key="cat2" | |||
> | |||
<HotspotCategory | |||
category={ | |||
Object { | |||
"key": "cat2", | |||
"title": "cat2", | |||
} | |||
} | |||
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": "h5", | |||
"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": "cat2", | |||
"status": "RESOLVED", | |||
"updateDate": "2013-05-13T17:55:39+0200", | |||
"vulnerabilityProbability": "MEDIUM", | |||
}, | |||
] | |||
} | |||
onHotspotClick={[MockFunction]} | |||
selectedHotspotKey="h2" | |||
/> | |||
</li> | |||
</ul> | |||
</li> | |||
</ul> | |||
</Fragment> | |||
`; |
@@ -0,0 +1,39 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<a | |||
className="hotspot-item" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<div | |||
className="little-spacer-left" | |||
> | |||
'3' is a magic number. | |||
</div> | |||
<div | |||
className="badge spacer-top" | |||
> | |||
issue.status.RESOLVED | |||
</div> | |||
</a> | |||
`; | |||
exports[`should render correctly 2`] = ` | |||
<a | |||
className="hotspot-item highlight" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<div | |||
className="little-spacer-left" | |||
> | |||
'3' is a magic number. | |||
</div> | |||
<div | |||
className="badge spacer-top" | |||
> | |||
issue.status.RESOLVED | |||
</div> | |||
</a> | |||
`; |
@@ -0,0 +1,51 @@ | |||
/* | |||
* 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. | |||
*/ | |||
#security_hotspots .wrapper { | |||
position: fixed; | |||
/* top is defined programatically */ | |||
bottom: 0; | |||
width: 100%; | |||
} | |||
#security_hotspots .layout-page { | |||
margin: 0 auto; | |||
min-width: var(--minPageWidth); | |||
max-width: 1280px; | |||
height: 100%; | |||
} | |||
#security_hotspots .filter-bar { | |||
max-width: 1280px; | |||
margin: 0 auto; | |||
padding: var(--gridSize) 20px; | |||
border-bottom: 1px solid var(--barBorderColor); | |||
} | |||
#security_hotspots .sidebar { | |||
flex: 1 0 30%; | |||
border-right: 1px solid var(--barBorderColor); | |||
height: 100%; | |||
overflow-y: auto; | |||
} | |||
#security_hotspots .main { | |||
flex: 1 0 70%; | |||
overflow-y: auto; | |||
} |
@@ -0,0 +1,64 @@ | |||
/* | |||
* 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 { groupBy, sortBy } from 'lodash'; | |||
import { RawHotspot, RiskExposure } from '../../types/securityHotspots'; | |||
export const RISK_EXPOSURE_LEVELS = [RiskExposure.HIGH, RiskExposure.MEDIUM, RiskExposure.LOW]; | |||
export function mapRules(rules: Array<{ key: string; name: string }>): T.Dict<string> { | |||
return rules.reduce((ruleMap: T.Dict<string>, r) => { | |||
ruleMap[r.key] = r.name; | |||
return ruleMap; | |||
}, {}); | |||
} | |||
export function groupByCategory( | |||
hotspots: RawHotspot[] = [], | |||
securityCategories: T.Dict<{ title: string; description?: string }> | |||
) { | |||
const groups = groupBy(hotspots, h => h.securityCategory); | |||
return sortBy( | |||
Object.keys(groups).map(key => ({ | |||
key, | |||
title: getCategoryTitle(key, securityCategories), | |||
hotspots: groups[key] | |||
})), | |||
cat => cat.title | |||
); | |||
} | |||
export function sortHotspots( | |||
hotspots: RawHotspot[], | |||
securityCategories: T.Dict<{ title: string }> | |||
) { | |||
return sortBy(hotspots, [ | |||
h => RISK_EXPOSURE_LEVELS.indexOf(h.vulnerabilityProbability), | |||
h => getCategoryTitle(h.securityCategory, securityCategories), | |||
h => h.message | |||
]); | |||
} | |||
function getCategoryTitle( | |||
key: string, | |||
securityCategories: T.Dict<{ title: string; description?: string }> | |||
) { | |||
return securityCategories[key] ? securityCategories[key].title : key; | |||
} |
@@ -0,0 +1,39 @@ | |||
/* | |||
* 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 { RawHotspot, RiskExposure } from '../../types/securityHotspots'; | |||
export function mockHotspot(overrides: Partial<RawHotspot> = {}): RawHotspot { | |||
return { | |||
key: '01fc972e-2a3c-433e-bcae-0bd7f88f5123', | |||
component: 'com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest', | |||
project: 'com.github.kevinsawicki:http-request', | |||
rule: 'checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck', | |||
status: 'RESOLVED', | |||
resolution: 'FALSE-POSITIVE', | |||
securityCategory: 'command-injection', | |||
vulnerabilityProbability: RiskExposure.HIGH, | |||
message: "'3' is a magic number.", | |||
line: 81, | |||
author: 'Developer 1', | |||
creationDate: '2013-05-13T17:55:39+0200', | |||
updateDate: '2013-05-13T17:55:39+0200', | |||
...overrides | |||
}; | |||
} |
@@ -0,0 +1,48 @@ | |||
/* | |||
* 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. | |||
*/ | |||
export enum RiskExposure { | |||
LOW = 'LOW', | |||
MEDIUM = 'MEDIUM', | |||
HIGH = 'HIGH' | |||
} | |||
export interface RawHotspot { | |||
assignee?: string; | |||
author?: string; | |||
component: string; | |||
creationDate: string; | |||
key: string; | |||
line?: number; | |||
message: string; | |||
project: string; | |||
resolution: string; | |||
rule: string; | |||
securityCategory: string; | |||
updateDate: string; | |||
vulnerabilityProbability: RiskExposure; | |||
status: string; | |||
subProject?: string; | |||
} | |||
export interface HotspotSearchResponse { | |||
components?: { key: string; qualifier: string; name: string }[]; | |||
hotspots: RawHotspot[]; | |||
paging: T.Paging; | |||
} |
@@ -473,6 +473,7 @@ layout.login=Log in | |||
layout.logout=Log out | |||
layout.measures=Measures | |||
layout.settings=Administration | |||
layout.security_hotspots=Security Hotspots | |||
layout.security_reports=Security Reports | |||
layout.sonar.slogan=Continuous Code Quality | |||
@@ -633,6 +634,24 @@ sessions.email_already_exists.4=Your email address will be erased from the first | |||
sessions.email_already_exists.5=You will no longer receive email notifications from this account. | |||
sessions.email_already_exists.6=Issues won't be automatically assigned to this account anymore. | |||
#------------------------------------------------------------------------------ | |||
# | |||
# HOTSPOTS | |||
# | |||
#------------------------------------------------------------------------------ | |||
risk_exposure.HIGH=High | |||
risk_exposure.MEDIUM=Medium | |||
risk_exposure.LOW=Low | |||
hotspots.page=Security Hotspots | |||
hotspots.no_hotspots.title=There are no Security Hotspots to review | |||
hotspots.no_hotspots.description=Next time you analyse a piece of code that contains a potential security risk, it will show up here. | |||
hotspots.learn_more=Learn more about Security Hotspots | |||
hotspots.list_title.TO_REVIEW={0} Security Hotspots to review | |||
hotspots.list_title.REVIEWED={0} reviewed Security Hotspots | |||
hotspots.risk_exposure=Review priority: | |||
#------------------------------------------------------------------------------ | |||
# | |||
# ISSUES |