--- /dev/null
+<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>
--- /dev/null
+/*
+ * 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);
+}
);
}
+ 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;
<NavBarTabs>
{this.renderDashboardLink()}
{this.renderIssuesLink()}
+ {this.renderSecurityHotspotsLink()}
{this.renderSecurityReports()}
{this.renderComponentMeasuresLink()}
{this.renderCodeLink()}
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"
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"
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"
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"
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"
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"
max-width: 980px;
}
+.no-footer-page #footer {
+ display: none;
+}
+
.page-footer-menu-item {
display: inline-block;
}
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';
)}
/>
<Route path="project/issues" component={Issues} />
+ <Route
+ path="security_hotspots"
+ component={lazyLoadComponent(() =>
+ import('../../apps/securityHotspots/SecurityHotspotsApp')
+ )}
+ />
<RouteWithChildRoutes
path="project/quality_gate"
childRoutes={projectQualityGateRoutes}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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} />
+ );
+}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<SecurityHotspotsAppRenderer
+ hotspots={Array []}
+ loading={true}
+ onHotspotClick={[Function]}
+ securityCategories={Object {}}
+/>
+`;
--- /dev/null
+// 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>
+`;
--- /dev/null
+/*
+ * 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'
+ });
+ });
+});
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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>
+ </>
+ );
+}
--- /dev/null
+/*
+ * 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);
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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} />
+ );
+}
--- /dev/null
+// 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>
+`;
--- /dev/null
+// 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>
+`;
--- /dev/null
+// 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>
+`;
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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
+ };
+}
--- /dev/null
+/*
+ * 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;
+}
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
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