Selaa lähdekoodia

SONAR-12717 Security Hotspots Page

tags/8.2.0.32929
Jeremy Davis 4 vuotta sitten
vanhempi
commit
b8d394da90
30 muutettua tiedostoa jossa 1864 lisäystä ja 0 poistoa
  1. 1
    0
      server/sonar-web/public/images/hotspot-large.svg
  2. 30
    0
      server/sonar-web/src/main/js/api/securityHotspots.ts
  3. 13
    0
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
  4. 104
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
  5. 4
    0
      server/sonar-web/src/main/js/app/styles/components/page.css
  6. 7
    0
      server/sonar-web/src/main/js/app/utils/startReactApp.tsx
  7. 111
    0
      server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx
  8. 97
    0
      server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx
  9. 79
    0
      server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx
  10. 47
    0
      server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx
  11. 10
    0
      server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap
  12. 34
    0
      server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap
  13. 144
    0
      server/sonar-web/src/main/js/apps/securityHotspots/__tests__/utils-test.ts
  14. 30
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/FilterBar.tsx
  15. 79
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotCategory.tsx
  16. 104
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.css
  17. 84
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx
  18. 44
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotListItem.tsx
  19. 30
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewer.tsx
  20. 56
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotCategory-test.tsx
  21. 63
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx
  22. 44
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotListItem-test.tsx
  23. 166
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap
  24. 223
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap
  25. 39
    0
      server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap
  26. 51
    0
      server/sonar-web/src/main/js/apps/securityHotspots/styles.css
  27. 64
    0
      server/sonar-web/src/main/js/apps/securityHotspots/utils.ts
  28. 39
    0
      server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts
  29. 48
    0
      server/sonar-web/src/main/js/types/securityHotspots.ts
  30. 19
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 0
server/sonar-web/public/images/hotspot-large.svg Näytä tiedosto

@@ -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>

+ 30
- 0
server/sonar-web/src/main/js/api/securityHotspots.ts Näytä tiedosto

@@ -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);
}

+ 13
- 0
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx Näytä tiedosto

@@ -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()}

+ 104
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap Näytä tiedosto

@@ -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"

+ 4
- 0
server/sonar-web/src/main/js/app/styles/components/page.css Näytä tiedosto

@@ -161,6 +161,10 @@
max-width: 980px;
}

.no-footer-page #footer {
display: none;
}

.page-footer-menu-item {
display: inline-block;
}

+ 7
- 0
server/sonar-web/src/main/js/app/utils/startReactApp.tsx Näytä tiedosto

@@ -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}

+ 111
- 0
server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsApp.tsx Näytä tiedosto

@@ -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}
/>
);
}
}

+ 97
- 0
server/sonar-web/src/main/js/apps/securityHotspots/SecurityHotspotsAppRenderer.tsx Näytä tiedosto

@@ -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>
);
}

+ 79
- 0
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsApp-test.tsx Näytä tiedosto

@@ -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} />
);
}

+ 47
- 0
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx Näytä tiedosto

@@ -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}
/>
);
}

+ 10
- 0
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap Näytä tiedosto

@@ -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 {}}
/>
`;

+ 34
- 0
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap Näytä tiedosto

@@ -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>
`;

+ 144
- 0
server/sonar-web/src/main/js/apps/securityHotspots/__tests__/utils-test.ts Näytä tiedosto

@@ -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'
});
});
});

+ 30
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/FilterBar.tsx Näytä tiedosto

@@ -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>
);
}

+ 79
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotCategory.tsx Näytä tiedosto

@@ -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>
);
}

+ 104
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.css Näytä tiedosto

@@ -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);
}

+ 84
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotList.tsx Näytä tiedosto

@@ -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>
</>
);
}

+ 44
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotListItem.tsx Näytä tiedosto

@@ -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);

+ 30
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotViewer.tsx Näytä tiedosto

@@ -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>
);
}

+ 56
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotCategory-test.tsx Näytä tiedosto

@@ -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}
/>
);
}

+ 63
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotList-test.tsx Näytä tiedosto

@@ -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}
/>
);
}

+ 44
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotListItem-test.tsx Näytä tiedosto

@@ -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} />
);
}

+ 166
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotCategory-test.tsx.snap Näytä tiedosto

@@ -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>
`;

+ 223
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotList-test.tsx.snap Näytä tiedosto

@@ -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>
`;

+ 39
- 0
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotListItem-test.tsx.snap Näytä tiedosto

@@ -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>
`;

+ 51
- 0
server/sonar-web/src/main/js/apps/securityHotspots/styles.css Näytä tiedosto

@@ -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;
}

+ 64
- 0
server/sonar-web/src/main/js/apps/securityHotspots/utils.ts Näytä tiedosto

@@ -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;
}

+ 39
- 0
server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts Näytä tiedosto

@@ -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
};
}

+ 48
- 0
server/sonar-web/src/main/js/types/securityHotspots.ts Näytä tiedosto

@@ -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;
}

+ 19
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Näytä tiedosto

@@ -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

Loading…
Peruuta
Tallenna