diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2019-12-03 15:41:43 +0100 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2020-01-13 20:46:26 +0100 |
commit | b8d394da901488b5493e95c0bf98d0ff1139de09 (patch) | |
tree | 09db6cb2fadc61401a8dbd061a08b50dc2730d26 /server | |
parent | 751fd7ffdb00dcf77e6fa982e6d3428dd7a6c564 (diff) | |
download | sonarqube-b8d394da901488b5493e95c0bf98d0ff1139de09.tar.gz sonarqube-b8d394da901488b5493e95c0bf98d0ff1139de09.zip |
SONAR-12717 Security Hotspots Page
Diffstat (limited to 'server')
29 files changed, 1845 insertions, 0 deletions
diff --git a/server/sonar-web/public/images/hotspot-large.svg b/server/sonar-web/public/images/hotspot-large.svg new file mode 100644 index 00000000000..edfcb684a6c --- /dev/null +++ b/server/sonar-web/public/images/hotspot-large.svg @@ -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> diff --git a/server/sonar-web/src/main/js/api/securityHotspots.ts b/server/sonar-web/src/main/js/api/securityHotspots.ts new file mode 100644 index 00000000000..f6a0ab8a64c --- /dev/null +++ b/server/sonar-web/src/main/js/api/securityHotspots.ts @@ -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); +} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx index 6ad6d9f91a4..71258e7c207 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx @@ -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()} diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap index 696bb7739f1..1b30f2e863c 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap @@ -82,6 +82,24 @@ exports[`should work for a branch 1`] = ` style={Object {}} to={ Object { + "pathname": "/security_hotspots", + "query": Object { + "branch": "release", + "id": "foo", + }, + } + } + > + layout.security_hotspots + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { "pathname": "/component_measures", "query": Object { "branch": "release", @@ -179,6 +197,24 @@ exports[`should work for a branch 2`] = ` style={Object {}} to={ Object { + "pathname": "/security_hotspots", + "query": Object { + "branch": "release", + "id": "foo", + }, + } + } + > + layout.security_hotspots + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { "pathname": "/component_measures", "query": Object { "branch": "release", @@ -274,6 +310,23 @@ exports[`should work for all qualifiers 1`] = ` style={Object {}} to={ Object { + "pathname": "/security_hotspots", + "query": Object { + "id": "foo", + }, + } + } + > + layout.security_hotspots + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { "pathname": "/component_measures", "query": Object { "id": "foo", @@ -463,6 +516,23 @@ exports[`should work for all qualifiers 2`] = ` style={Object {}} to={ Object { + "pathname": "/security_hotspots", + "query": Object { + "id": "foo", + }, + } + } + > + layout.security_hotspots + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { "pathname": "/component_measures", "query": Object { "id": "foo", @@ -584,6 +654,23 @@ exports[`should work for all qualifiers 3`] = ` style={Object {}} to={ Object { + "pathname": "/security_hotspots", + "query": Object { + "id": "foo", + }, + } + } + > + layout.security_hotspots + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { "pathname": "/component_measures", "query": Object { "id": "foo", @@ -676,6 +763,23 @@ exports[`should work for all qualifiers 4`] = ` style={Object {}} to={ Object { + "pathname": "/security_hotspots", + "query": Object { + "id": "foo", + }, + } + } + > + layout.security_hotspots + </Link> + </li> + <li> + <Link + activeClassName="active" + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { "pathname": "/component_measures", "query": Object { "id": "foo", diff --git a/server/sonar-web/src/main/js/app/styles/components/page.css b/server/sonar-web/src/main/js/app/styles/components/page.css index 01555387dd5..c00695e5bea 100644 --- a/server/sonar-web/src/main/js/app/styles/components/page.css +++ b/server/sonar-web/src/main/js/app/styles/components/page.css @@ -161,6 +161,10 @@ max-width: 980px; } +.no-footer-page #footer { + display: none; +} + .page-footer-menu-item { display: inline-block; } diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx index 49d05fbc8ae..305bc7d75e0 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx new file mode 100644 index 00000000000..9f889e176d7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx @@ -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} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx new file mode 100644 index 00000000000..60dcbb1644b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx new file mode 100644 index 00000000000..3cec33f41d7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx @@ -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} /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx new file mode 100644 index 00000000000..f3a3f832a20 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx @@ -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} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap new file mode 100644 index 00000000000..4f83ea76cd3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap @@ -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 {}} +/> +`; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap new file mode 100644 index 00000000000..67d90c34884 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/utils-test.ts new file mode 100644 index 00000000000..1221e827d3f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/__tests__/utils-test.ts @@ -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' + }); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/FilterBar.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/FilterBar.tsx new file mode 100644 index 00000000000..0927843ed84 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/FilterBar.tsx @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotCategory.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotCategory.tsx new file mode 100644 index 00000000000..ee9109cee46 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotCategory.tsx @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.css b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.css new file mode 100644 index 00000000000..e8f6972ca43 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.css @@ -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); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx new file mode 100644 index 00000000000..a33c3b84c35 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx @@ -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> + </> + ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotListItem.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotListItem.tsx new file mode 100644 index 00000000000..549c8e35bba --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotListItem.tsx @@ -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); diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewer.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewer.tsx new file mode 100644 index 00000000000..c4e8c911f98 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewer.tsx @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotCategory-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotCategory-test.tsx new file mode 100644 index 00000000000..f1d3312579c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotCategory-test.tsx @@ -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} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx new file mode 100644 index 00000000000..b48e336cd19 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx @@ -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} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotListItem-test.tsx b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotListItem-test.tsx new file mode 100644 index 00000000000..e9a7b7d0082 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotListItem-test.tsx @@ -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} /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap new file mode 100644 index 00000000000..3f07388c5ad --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap new file mode 100644 index 00000000000..68c4b07fa9b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap new file mode 100644 index 00000000000..457deefac03 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/styles.css b/server/sonar-web/src/main/js/apps/securityHotspots/styles.css new file mode 100644 index 00000000000..b76b508053d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/styles.css @@ -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; +} diff --git a/server/sonar-web/src/main/js/apps/securityHotspots/utils.ts b/server/sonar-web/src/main/js/apps/securityHotspots/utils.ts new file mode 100644 index 00000000000..147588deaa0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/securityHotspots/utils.ts @@ -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; +} diff --git a/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts b/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts new file mode 100644 index 00000000000..f1c4be442c9 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts @@ -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 + }; +} diff --git a/server/sonar-web/src/main/js/types/securityHotspots.ts b/server/sonar-web/src/main/js/types/securityHotspots.ts new file mode 100644 index 00000000000..dc95ad98d43 --- /dev/null +++ b/server/sonar-web/src/main/js/types/securityHotspots.ts @@ -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; +} |