]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12717 Security Hotspots Page
authorJeremy Davis <jeremy.davis@sonarsource.com>
Tue, 3 Dec 2019 14:41:43 +0000 (15:41 +0100)
committerSonarTech <sonartech@sonarsource.com>
Mon, 13 Jan 2020 19:46:26 +0000 (20:46 +0100)
30 files changed:
server/sonar-web/public/images/hotspot-large.svg [new file with mode: 0644]
server/sonar-web/src/main/js/api/securityHotspots.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
server/sonar-web/src/main/js/app/styles/components/page.css
server/sonar-web/src/main/js/app/utils/startReactApp.tsx
server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/utils-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/FilterBar.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotCategory.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.css [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotListItem.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotCategory-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotListItem-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/styles.css [new file with mode: 0644]
server/sonar-web/src/main/js/apps/securityHotspots/utils.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts [new file with mode: 0644]
server/sonar-web/src/main/js/types/securityHotspots.ts [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/public/images/hotspot-large.svg b/server/sonar-web/public/images/hotspot-large.svg
new file mode 100644 (file)
index 0000000..edfcb68
--- /dev/null
@@ -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 (file)
index 0000000..f6a0ab8
--- /dev/null
@@ -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);
+}
index 6ad6d9f91a46a3e8905925974c380548df1df1c6..71258e7c207322a3e6ab9834de4e2e748cd622cd 100644 (file)
@@ -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()}
index 696bb7739f1077ee48b369291e67e132c980e549..1b30f2e863ce4abaad05ba0ab21d1073f8b42223 100644 (file)
@@ -75,6 +75,24 @@ exports[`should work for a branch 1`] = `
       issues.page
     </Link>
   </li>
+  <li>
+    <Link
+      activeClassName="active"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/security_hotspots",
+          "query": Object {
+            "branch": "release",
+            "id": "foo",
+          },
+        }
+      }
+    >
+      layout.security_hotspots
+    </Link>
+  </li>
   <li>
     <Link
       activeClassName="active"
@@ -172,6 +190,24 @@ exports[`should work for a branch 2`] = `
       issues.page
     </Link>
   </li>
+  <li>
+    <Link
+      activeClassName="active"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/security_hotspots",
+          "query": Object {
+            "branch": "release",
+            "id": "foo",
+          },
+        }
+      }
+    >
+      layout.security_hotspots
+    </Link>
+  </li>
   <li>
     <Link
       activeClassName="active"
@@ -267,6 +303,23 @@ exports[`should work for all qualifiers 1`] = `
       issues.page
     </Link>
   </li>
+  <li>
+    <Link
+      activeClassName="active"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/security_hotspots",
+          "query": Object {
+            "id": "foo",
+          },
+        }
+      }
+    >
+      layout.security_hotspots
+    </Link>
+  </li>
   <li>
     <Link
       activeClassName="active"
@@ -456,6 +509,23 @@ exports[`should work for all qualifiers 2`] = `
       issues.page
     </Link>
   </li>
+  <li>
+    <Link
+      activeClassName="active"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/security_hotspots",
+          "query": Object {
+            "id": "foo",
+          },
+        }
+      }
+    >
+      layout.security_hotspots
+    </Link>
+  </li>
   <li>
     <Link
       activeClassName="active"
@@ -577,6 +647,23 @@ exports[`should work for all qualifiers 3`] = `
       issues.page
     </Link>
   </li>
+  <li>
+    <Link
+      activeClassName="active"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/security_hotspots",
+          "query": Object {
+            "id": "foo",
+          },
+        }
+      }
+    >
+      layout.security_hotspots
+    </Link>
+  </li>
   <li>
     <Link
       activeClassName="active"
@@ -669,6 +756,23 @@ exports[`should work for all qualifiers 4`] = `
       issues.page
     </Link>
   </li>
+  <li>
+    <Link
+      activeClassName="active"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/security_hotspots",
+          "query": Object {
+            "id": "foo",
+          },
+        }
+      }
+    >
+      layout.security_hotspots
+    </Link>
+  </li>
   <li>
     <Link
       activeClassName="active"
index 01555387dd5a1c9d8f82de45ea19b9fb6eeca527..c00695e5bea4a066f31d72f30d8c2b89423ba165 100644 (file)
   max-width: 980px;
 }
 
+.no-footer-page #footer {
+  display: none;
+}
+
 .page-footer-menu-item {
   display: inline-block;
 }
index 49d05fbc8ae44414532c97888fb4e4faf076d74a..305bc7d75e0a27035ea6032d20998844590a903c 100644 (file)
@@ -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 (file)
index 0000000..9f889e1
--- /dev/null
@@ -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 (file)
index 0000000..60dcbb1
--- /dev/null
@@ -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 (file)
index 0000000..3cec33f
--- /dev/null
@@ -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 (file)
index 0000000..f3a3f83
--- /dev/null
@@ -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 (file)
index 0000000..4f83ea7
--- /dev/null
@@ -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 (file)
index 0000000..67d90c3
--- /dev/null
@@ -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 (file)
index 0000000..1221e82
--- /dev/null
@@ -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 (file)
index 0000000..0927843
--- /dev/null
@@ -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 (file)
index 0000000..ee9109c
--- /dev/null
@@ -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 (file)
index 0000000..e8f6972
--- /dev/null
@@ -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 (file)
index 0000000..a33c3b8
--- /dev/null
@@ -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 (file)
index 0000000..549c8e3
--- /dev/null
@@ -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 (file)
index 0000000..c4e8c91
--- /dev/null
@@ -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 (file)
index 0000000..f1d3312
--- /dev/null
@@ -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 (file)
index 0000000..b48e336
--- /dev/null
@@ -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 (file)
index 0000000..e9a7b7d
--- /dev/null
@@ -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 (file)
index 0000000..3f07388
--- /dev/null
@@ -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 (file)
index 0000000..68c4b07
--- /dev/null
@@ -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 (file)
index 0000000..457deef
--- /dev/null
@@ -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 (file)
index 0000000..b76b508
--- /dev/null
@@ -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 (file)
index 0000000..147588d
--- /dev/null
@@ -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 (file)
index 0000000..f1c4be4
--- /dev/null
@@ -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 (file)
index 0000000..dc95ad9
--- /dev/null
@@ -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;
+}
index cd5465cece5a74aeb2673750583eca9b2ff1c22d..34eacaf3ec6d065c051f52a278bba77de1bd1576 100644 (file)
@@ -473,6 +473,7 @@ layout.login=Log in
 layout.logout=Log out
 layout.measures=Measures
 layout.settings=Administration
+layout.security_hotspots=Security Hotspots
 layout.security_reports=Security Reports
 layout.sonar.slogan=Continuous Code Quality
 
@@ -633,6 +634,24 @@ sessions.email_already_exists.4=Your email address will be erased from the first
 sessions.email_already_exists.5=You will no longer receive email notifications from this account.
 sessions.email_already_exists.6=Issues won't be automatically assigned to this account anymore.
 
+#------------------------------------------------------------------------------
+#
+# HOTSPOTS
+#
+#------------------------------------------------------------------------------
+
+risk_exposure.HIGH=High
+risk_exposure.MEDIUM=Medium
+risk_exposure.LOW=Low
+
+hotspots.page=Security Hotspots
+hotspots.no_hotspots.title=There are no Security Hotspots to review
+hotspots.no_hotspots.description=Next time you analyse a piece of code that contains a potential security risk, it will show up here.
+hotspots.learn_more=Learn more about Security Hotspots
+hotspots.list_title.TO_REVIEW={0} Security Hotspots to review
+hotspots.list_title.REVIEWED={0} reviewed Security Hotspots 
+hotspots.risk_exposure=Review priority:
+
 #------------------------------------------------------------------------------
 #
 # ISSUES