Browse Source

Add security hotspots page (#478)

tags/7.5
Pascal Mugnier 5 years ago
parent
commit
645ffc5c8a
19 changed files with 2306 additions and 51 deletions
  1. 1
    0
      server/sonar-docs/src/tooltips/security-reports/cwe.md
  2. 31
    0
      server/sonar-web/src/main/js/api/security-reports.ts
  3. 41
    0
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
  4. 350
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
  5. 5
    0
      server/sonar-web/src/main/js/app/styles/init/tables.css
  6. 11
    0
      server/sonar-web/src/main/js/app/types.ts
  7. 4
    0
      server/sonar-web/src/main/js/app/utils/startReactApp.js
  8. 22
    45
      server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
  9. 158
    0
      server/sonar-web/src/main/js/apps/securityReports/components/App.tsx
  10. 209
    0
      server/sonar-web/src/main/js/apps/securityReports/components/VulnerabilityList.tsx
  11. 155
    0
      server/sonar-web/src/main/js/apps/securityReports/components/__tests__/App-test.tsx
  12. 82
    0
      server/sonar-web/src/main/js/apps/securityReports/components/__tests__/VulnerabilityList-test.tsx
  13. 394
    0
      server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/App-test.tsx.snap
  14. 744
    0
      server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/VulnerabilityList-test.tsx.snap
  15. 26
    0
      server/sonar-web/src/main/js/apps/securityReports/style.css
  16. 51
    0
      server/sonar-web/src/main/js/apps/securityReports/utils.ts
  17. 1
    1
      server/sonar-web/src/main/js/components/controls/Checkbox.tsx
  18. 4
    4
      server/sonar-web/src/main/js/helpers/standards.json
  19. 17
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 0
server/sonar-docs/src/tooltips/security-reports/cwe.md View File

@@ -0,0 +1 @@
CWE™ is a community-developed list of common software security weaknesses. It serves as a common language, a measuring stick for software security tools, and as a baseline for weakness identification, mitigation, and prevention efforts.

+ 31
- 0
server/sonar-web/src/main/js/api/security-reports.ts View File

@@ -0,0 +1,31 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { SecurityHotspot } from '../app/types';
import { getJSON } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';

export function getSecurityHotspots(data: {
project: string;
standard: 'owaspTop10' | 'sansTop25' | 'cwe';
includeDistribution?: boolean;
branch?: string;
}): Promise<{ categories: Array<SecurityHotspot> }> {
return getJSON('/api/security_reports/show', data).catch(throwGlobalError);
}

+ 41
- 0
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx View File

@@ -169,6 +169,46 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
);
}

renderSecurityReportsLink() {
return (
<ul className="menu">
<li>
<Link
activeClassName="active"
to={{ pathname: '/project/security_reports/owasp_top_10', query: this.getQuery() }}>
{translate('security_reports.owaspTop10.page')}
</Link>
</li>
<li>
<Link
activeClassName="active"
to={{ pathname: '/project/security_reports/sans_top_25', query: this.getQuery() }}>
{translate('security_reports.sansTop25.page')}
</Link>
</li>
</ul>
);
}

renderSecurityReports() {
const isActive = location.pathname.startsWith('/project/security_reports');
return (
<Dropdown overlay={this.renderSecurityReportsLink()} tagName="li">
{({ onToggleClick, open }) => (
<a
aria-expanded={String(open)}
aria-haspopup="true"
className={classNames('dropdown-toggle', { active: isActive || open })}
href="#"
onClick={onToggleClick}>
{translate('layout.security_reports')}
<DropdownIcon className="little-spacer-left" />
</a>
)}
</Dropdown>
);
}

renderAdministration() {
const { branchLike } = this.props;

@@ -450,6 +490,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
<NavBarTabs>
{this.renderDashboardLink()}
{this.renderIssuesLink()}
{this.renderSecurityReports()}
{this.renderComponentMeasuresLink()}
{this.renderCodeLink()}
{this.renderActivityLink()}

+ 350
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap View File

@@ -38,6 +38,49 @@ exports[`should work for all qualifiers 1`] = `
issues.page
</Link>
</li>
<Dropdown
overlay={
<ul
className="menu"
>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/security_reports/owasp_top_10",
"query": Object {
"id": "foo",
},
}
}
>
security_reports.owaspTop10.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/security_reports/sans_top_25",
"query": Object {
"id": "foo",
},
}
}
>
security_reports.sansTop25.page
</Link>
</li>
</ul>
}
tagName="li"
/>
<li>
<Link
activeClassName="active"
@@ -208,6 +251,49 @@ exports[`should work for all qualifiers 2`] = `
issues.page
</Link>
</li>
<Dropdown
overlay={
<ul
className="menu"
>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/security_reports/owasp_top_10",
"query": Object {
"id": "foo",
},
}
}
>
security_reports.owaspTop10.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/security_reports/sans_top_25",
"query": Object {
"id": "foo",
},
}
}
>
security_reports.sansTop25.page
</Link>
</li>
</ul>
}
tagName="li"
/>
<li>
<Link
activeClassName="active"
@@ -327,6 +413,49 @@ exports[`should work for all qualifiers 3`] = `
issues.page
</Link>
</li>
<Dropdown
overlay={
<ul
className="menu"
>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/security_reports/owasp_top_10",
"query": Object {
"id": "foo",
},
}
}
>
security_reports.owaspTop10.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/security_reports/sans_top_25",
"query": Object {
"id": "foo",
},
}
}
>
security_reports.sansTop25.page
</Link>
</li>
</ul>
}
tagName="li"
/>
<li>
<Link
activeClassName="active"
@@ -446,6 +575,49 @@ exports[`should work for all qualifiers 4`] = `
issues.page
</Link>
</li>
<Dropdown
overlay={
<ul
className="menu"
>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/security_reports/owasp_top_10",
"query": Object {
"id": "foo",
},
}
}
>
security_reports.owaspTop10.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/security_reports/sans_top_25",
"query": Object {
"id": "foo",
},
}
}
>
security_reports.sansTop25.page
</Link>
</li>
</ul>
}
tagName="li"
/>
<li>
<Link
activeClassName="active"
@@ -538,6 +710,49 @@ exports[`should work for all qualifiers 5`] = `
issues.page
</Link>
</li>
<Dropdown
overlay={
<ul
className="menu"
>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/security_reports/owasp_top_10",
"query": Object {
"id": "foo",
},
}
}
>
security_reports.owaspTop10.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/security_reports/sans_top_25",
"query": Object {
"id": "foo",
},
}
}
>
security_reports.sansTop25.page
</Link>
</li>
</ul>
}
tagName="li"
/>
<li>
<Link
activeClassName="active"
@@ -659,6 +874,51 @@ exports[`should work for long-living branches 1`] = `
issues.page
</Link>
</li>
<Dropdown
overlay={
<ul
className="menu"
>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/security_reports/owasp_top_10",
"query": Object {
"branch": "release",
"id": "foo",
},
}
}
>
security_reports.owaspTop10.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/security_reports/sans_top_25",
"query": Object {
"branch": "release",
"id": "foo",
},
}
}
>
security_reports.sansTop25.page
</Link>
</li>
</ul>
}
tagName="li"
/>
<li>
<Link
activeClassName="active"
@@ -756,6 +1016,51 @@ exports[`should work for long-living branches 2`] = `
issues.page
</Link>
</li>
<Dropdown
overlay={
<ul
className="menu"
>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/security_reports/owasp_top_10",
"query": Object {
"branch": "release",
"id": "foo",
},
}
}
>
security_reports.owaspTop10.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/security_reports/sans_top_25",
"query": Object {
"branch": "release",
"id": "foo",
},
}
}
>
security_reports.sansTop25.page
</Link>
</li>
</ul>
}
tagName="li"
/>
<li>
<Link
activeClassName="active"
@@ -835,6 +1140,51 @@ exports[`should work for short-living branches 1`] = `
issues.page
</Link>
</li>
<Dropdown
overlay={
<ul
className="menu"
>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/security_reports/owasp_top_10",
"query": Object {
"branch": "feature",
"id": "foo",
},
}
}
>
security_reports.owaspTop10.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/security_reports/sans_top_25",
"query": Object {
"branch": "feature",
"id": "foo",
},
}
}
>
security_reports.sansTop25.page
</Link>
</li>
</ul>
}
tagName="li"
/>
<li>
<Link
activeClassName="active"

+ 5
- 0
server/sonar-web/src/main/js/app/styles/init/tables.css View File

@@ -134,6 +134,11 @@ table.data.condensed > tbody > tr > td {
padding-bottom: 5px;
}

table.data tr.subheader th {
font-size: var(--smallFontSize);
border-bottom: none;
}

table.data.no-outer-padding > thead > tr > th:first-child {
padding-left: 0;
}

+ 11
- 0
server/sonar-web/src/main/js/app/types.ts View File

@@ -213,6 +213,17 @@ export function isSameHomePage(a: HomePage, b: HomePage) {
);
}

export interface SecurityHotspot {
category?: string;
cwe?: string;
distribution?: Array<SecurityHotspot>;
openSecurityHotspots: number;
toReviewSecurityHotspots: number;
vulnerabilities: number;
vulnerabilityRating?: number;
wontFixSecurityHotspots: number;
}

export interface Issue {
actions?: string[];
assignee?: string;

+ 4
- 0
server/sonar-web/src/main/js/app/utils/startReactApp.js View File

@@ -198,6 +198,10 @@ const startReactApp = (lang, currentUser, appState) => {
)}
/>
<Route path="project/issues" component={Issues} />
<Route
path="project/security_reports/:type"
component={lazyLoad(() => import('../../apps/securityReports/components/App'))}
/>
<Route path="project/quality_gate" childRoutes={projectQualityGateRoutes} />
<Route
path="project/quality_profiles"

+ 22
- 45
server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx View File

@@ -26,6 +26,12 @@ import { translate } from '../../../helpers/l10n';
import FacetItemsList from '../../../components/facet/FacetItemsList';
import FacetItem from '../../../components/facet/FacetItem';
import Select from '../../../components/controls/Select';
import {
renderOwaspTop10Category,
renderSansTop25Category,
renderCWECategory,
Standards
} from '../../securityReports/utils';

export interface Props {
cwe: string[];
@@ -43,12 +49,6 @@ export interface Props {
sansTop25Stats: { [x: string]: number } | undefined;
}

interface Standards {
owaspTop10: { [x: string]: { title: string } };
sansTop25: { [x: string]: { title: string } };
cwe: { [x: string]: { title: string } };
}

interface State {
standards: Standards;
}
@@ -90,9 +90,9 @@ export default class StandardFacet extends React.PureComponent<Props, State> {

getValues = () => {
return [
...this.props.owaspTop10.map(this.renderOwaspTop10Category),
...this.props.sansTop25.map(this.renderSansTop25Category),
...this.props.cwe.map(this.renderCWECategory)
...this.props.owaspTop10.map(item => renderOwaspTop10Category(this.state.standards, item)),
...this.props.sansTop25.map(item => renderSansTop25Category(this.state.standards, item)),
...this.props.cwe.map(item => renderCWECategory(this.state.standards, item))
];
};

@@ -150,37 +150,10 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
this.handleItemClick('cwe', value, true);
};

renderOwaspTop10Category = (category: string) => {
const record = this.state.standards.owaspTop10[category];
if (!record) {
return category.toUpperCase();
} else if (category === 'unknown') {
return record.title;
} else {
return `${category.toUpperCase()} - ${record.title}`;
}
};

renderCWECategory = (category: string) => {
const record = this.state.standards.cwe[category];
if (!record) {
return `CWE-${category}`;
} else if (category === 'unknown') {
return record.title;
} else {
return `CWE-${category} - ${record.title}`;
}
};

renderSansTop25Category = (category: string) => {
const record = this.state.standards.sansTop25[category];
return record ? record.title : category;
};

renderList = (
statsProp: 'owaspTop10Stats' | 'cweStats' | 'sansTop25Stats',
valuesProp: 'owaspTop10' | 'cwe' | 'sansTop25',
renderName: (category: string) => string,
renderName: (standards: Standards, category: string) => string,
onClick: (x: string, multiple?: boolean) => void
) => {
const stats = this.props[statsProp];
@@ -211,7 +184,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
active={values.includes(category)}
key={category}
loading={this.props.loading}
name={renderName(category)}
name={renderName(this.state.standards, category)}
onClick={onClick}
stat={formatFacetStat(getStat(category))}
tooltip={values.length === 1 && !values.includes(category)}
@@ -226,18 +199,18 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
return this.renderList(
'owaspTop10Stats',
'owaspTop10',
this.renderOwaspTop10Category,
renderOwaspTop10Category,
this.handleOwaspTop10ItemClick
);
}

renderCWEList() {
return this.renderList('cweStats', 'cwe', this.renderCWECategory, this.handleCWEItemClick);
return this.renderList('cweStats', 'cwe', renderCWECategory, this.handleCWEItemClick);
}

renderCWESearch() {
const options = Object.keys(this.state.standards.cwe).map(cwe => ({
label: this.renderCWECategory(cwe),
label: renderCWECategory(this.state.standards, cwe),
value: cwe
}));
return (
@@ -259,7 +232,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
return this.renderList(
'sansTop25Stats',
'sansTop25',
this.renderSansTop25Category,
renderSansTop25Category,
this.handleSansTop25ItemClick
);
}
@@ -272,7 +245,9 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
name={translate('issues.facet.owaspTop10')}
onClick={this.handleOwaspTop10HeaderClick}
open={this.props.owaspTop10Open}
values={this.props.owaspTop10.map(this.renderOwaspTop10Category)}
values={this.props.owaspTop10.map(item =>
renderOwaspTop10Category(this.state.standards, item)
)}
/>
{this.props.owaspTop10Open && this.renderOwaspTop10List()}
</FacetBox>
@@ -281,7 +256,9 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
name={translate('issues.facet.sansTop25')}
onClick={this.handleSansTop25HeaderClick}
open={this.props.sansTop25Open}
values={this.props.sansTop25.map(this.renderSansTop25Category)}
values={this.props.sansTop25.map(item =>
renderSansTop25Category(this.state.standards, item)
)}
/>
{this.props.sansTop25Open && this.renderSansTop25List()}
</FacetBox>
@@ -290,7 +267,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
name={translate('issues.facet.cwe')}
onClick={this.handleCWEHeaderClick}
open={this.props.cweOpen}
values={this.props.cwe.map(this.renderCWECategory)}
values={this.props.cwe.map(item => renderCWECategory(this.state.standards, item))}
/>
{this.props.cweOpen && this.renderCWEList()}
{this.props.cweOpen && this.renderCWESearch()}

+ 158
- 0
server/sonar-web/src/main/js/apps/securityReports/components/App.tsx View File

@@ -0,0 +1,158 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as PropTypes from 'prop-types';
import Helmet from 'react-helmet';
import VulnerabilityList from './VulnerabilityList';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
import { translate } from '../../../helpers/l10n';
import { Component, BranchLike, SecurityHotspot } from '../../../app/types';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import Checkbox from '../../../components/controls/Checkbox';
import { RawQuery } from '../../../helpers/query';
import NotFound from '../../../app/components/NotFound';
import '../style.css';
import { getSecurityHotspots } from '../../../api/security-reports';
import { isLongLivingBranch } from '../../../helpers/branches';
import DocTooltip from '../../../components/docs/DocTooltip';

interface Props {
branchLike?: BranchLike;
component: Component;
location: { pathname: string; query: RawQuery };
params: { type: string };
}

interface State {
loading: boolean;
findings: Array<SecurityHotspot>;
hasVulnerabilities: boolean;
type: 'owaspTop10' | 'sansTop25' | 'cwe';
showCWE: boolean;
}

export default class App extends React.PureComponent<Props, State> {
mounted = false;

static contextTypes = {
router: PropTypes.object.isRequired
};

constructor(props: Props) {
super(props);
this.state = {
loading: false,
findings: [],
hasVulnerabilities: false,
type: props.params.type === 'owasp_top_10' ? 'owaspTop10' : 'sansTop25',
showCWE: props.location.query.showCWE === 'true'
};
}

componentDidMount() {
this.mounted = true;
this.fetchSecurityHotspots();
}

componentWillReceiveProps(newProps: Props) {
if (newProps.location.pathname !== this.props.location.pathname) {
const showCWE = newProps.location.query.showCWE === 'true';
const type = newProps.params.type === 'owasp_top_10' ? 'owaspTop10' : 'sansTop25';
this.setState({ type, showCWE }, this.fetchSecurityHotspots);
}
}

componentWillUnmount() {
this.mounted = false;
}

fetchSecurityHotspots = () => {
const { branchLike, component } = this.props;
this.setState({ loading: true });
getSecurityHotspots({
project: component.key,
standard: this.state.type,
includeDistribution: this.state.showCWE,
branch: isLongLivingBranch(branchLike) ? branchLike.name : undefined
})
.then(results => {
if (this.mounted) {
const hasVulnerabilities = results.categories.some(item => item.vulnerabilities > 0);
this.setState({ hasVulnerabilities, findings: results.categories, loading: false });
}
})
.catch(() => {
if (this.mounted) {
this.setState({ loading: false });
}
});
};

handleCheck = (checked: boolean) => {
const { router } = this.context;
router.push({
pathname: this.props.location.pathname,
query: { id: this.props.component.key, showCWE: checked }
});
this.setState({ showCWE: checked }, this.fetchSecurityHotspots);
};

render() {
const { branchLike, component, params } = this.props;
const { loading, findings, showCWE, type } = this.state;
if (params.type !== 'owasp_top_10' && params.type !== 'sans_top_25') {
return <NotFound withContainer={false} />;
}
return (
<div className="page page-limited" id="security-reports">
<Suggestions suggestions="security_reports" />
<Helmet title={translate('security_reports', type, 'page')} />
<header className="page-header">
<h1 className="page-title">{translate('security_reports', type, 'page')}</h1>
<div className="page-description">
{translate('security_reports', type, 'description')}
</div>
</header>
<div className="display-inline-flex-center">
<Checkbox
checked={showCWE}
className="spacer-left spacer-right vertical-middle"
disabled={!this.state.hasVulnerabilities}
id={'showCWE'}
onCheck={this.handleCheck}>
<label className="little-spacer-left" htmlFor={'showCWE'}>
{translate('security_reports.cwe.show')}
<DocTooltip className="spacer-left" doc="security-reports/cwe" />
</label>
</Checkbox>
</div>
<DeferredSpinner loading={loading}>
<VulnerabilityList
branchLike={branchLike}
component={component}
findings={findings}
showCWE={showCWE}
type={type}
/>
</DeferredSpinner>
</div>
);
}
}

+ 209
- 0
server/sonar-web/src/main/js/apps/securityReports/components/VulnerabilityList.tsx View File

@@ -0,0 +1,209 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { Link } from 'react-router';
import { translate } from '../../../helpers/l10n';
import { SecurityHotspot, Component, BranchLike } from '../../../app/types';
import Rating from '../../../components/ui/Rating';
import { getComponentIssuesUrl } from '../../../helpers/urls';
import { getBranchLikeQuery } from '../../../helpers/branches';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import VulnerabilityIcon from '../../../components/icons-components/VulnerabilityIcon';
import SecurityHotspotIcon from '../../../components/icons-components/SecurityHotspotIcon';
import {
renderOwaspTop10Category,
renderSansTop25Category,
renderCWECategory,
Standards
} from '../utils';

interface Props {
branchLike?: BranchLike;
component: Component;
findings: Array<SecurityHotspot>;
showCWE: boolean;
type: 'owaspTop10' | 'sansTop25' | 'cwe';
}

interface State {
standards: Standards;
}

export default class VulnerabilityList extends React.PureComponent<Props, State> {
mounted = false;
state: State = { standards: { owaspTop10: {}, sansTop25: {}, cwe: {} } };

componentDidMount() {
this.mounted = true;
this.loadStandards();
}

componentWillUnmount() {
this.mounted = false;
}

loadStandards = () => {
import('../../../helpers/standards.json')
.then(x => x.default)
.then(
({ owaspTop10, sansTop25, cwe }: Standards) => {
if (this.mounted) {
this.setState({ standards: { owaspTop10, sansTop25, cwe } });
}
},
() => {}
);
};

getName(finding: SecurityHotspot, type: 'owaspTop10' | 'sansTop25' | 'cwe') {
const category = finding.category || finding.cwe || 'unknown';
const renderers = {
owaspTop10: renderOwaspTop10Category,
sansTop25: renderSansTop25Category,
cwe: renderCWECategory
};
return (
<>
{renderers[type](this.state.standards, category)}
{this.state.standards[type][category] &&
this.state.standards[type][category].description && (
<HelpTooltip
className="spacer-left"
overlay={this.state.standards[type][category].description}
/>
)}
</>
);
}

renderFinding(finding: SecurityHotspot, isCWE?: boolean): React.ReactFragment {
const { branchLike, component, type } = this.props;
const params: { [name: string]: string | undefined } = {
...getBranchLikeQuery(branchLike),
types: 'SECURITY_HOTSPOT'
};
params[type] = finding.category || finding.cwe;

const subFindings =
this.props.showCWE && finding.distribution
? finding.distribution.map(f => this.renderFinding(f, true))
: null;

return (
<React.Fragment key={finding.category || finding.cwe}>
<tr>
{isCWE && <td />}
<td className="nowrap" colSpan={isCWE ? 1 : 2}>
<div className="display-inline-flex-center">
{this.getName(finding, isCWE ? 'cwe' : type)}
</div>
</td>
<td>
<div className="display-inline-flex-center">
<Link
to={getComponentIssuesUrl(component.key, { ...params, types: 'VULNERABILITY' })}>
{finding.vulnerabilities}
</Link>
<Link
className="link-no-underline spacer-left"
to={getComponentIssuesUrl(component.key, { ...params, types: 'VULNERABILITY' })}>
<Rating value={finding.vulnerabilityRating || 1} />
</Link>
</div>
</td>
<td>
<Link
className="spacer-right"
to={getComponentIssuesUrl(component.key, {
...params,
types: 'SECURITY_HOTSPOT',
resolved: 'false',
statuses: 'OPEN'
})}>
{finding.openSecurityHotspots}
</Link>
</td>
<td>
<Link
className="spacer-right"
to={getComponentIssuesUrl(component.key, {
...params,
types: 'SECURITY_HOTSPOT',
resolutions: 'FIXED'
})}>
{finding.toReviewSecurityHotspots}
</Link>
</td>
<td>
<Link
className="spacer-right"
to={getComponentIssuesUrl(component.key, {
...params,
types: 'SECURITY_HOTSPOT',
resolutions: 'WONTFIX'
})}>
{finding.wontFixSecurityHotspots}
</Link>
</td>
</tr>
{subFindings}
</React.Fragment>
);
}

render() {
return (
<div className="boxed-group boxed-group-inner spacer-top">
<table className="data zebra">
<thead>
<tr>
<th className="security-category-column" colSpan={2}>
{translate('security_reports.list.categories')}
</th>
<th className="security-result-column">
<div className="display-inline-flex-center">
<VulnerabilityIcon className="spacer-right" />{' '}
{translate('security_reports.list.vulnerabilities')}
</div>
</th>
<th colSpan={3}>
<div className="display-inline-flex-center">
<SecurityHotspotIcon className="spacer-right" />{' '}
{translate('security_reports.list.hotspots')}
</div>
</th>
</tr>
<tr className="subheader">
<th colSpan={3} />
<th className="security-result-column">{translate('security_reports.line.open')}</th>
<th className="security-result-column">
{translate('security_reports.line.in_review')}
</th>
<th className="security-result-column">
{translate('security_reports.line.wont_fix')}
</th>
</tr>
</thead>
<tbody>{this.props.findings.map(finding => this.renderFinding(finding))}</tbody>
</table>
</div>
);
}
}

+ 155
- 0
server/sonar-web/src/main/js/apps/securityReports/components/__tests__/App-test.tsx View File

@@ -0,0 +1,155 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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.
*/
/* eslint-disable import/first, import/order */
jest.mock('../../../../api/security-reports', () => ({
getSecurityHotspots: jest.fn(() => {
const distribution: any = [
{
cwe: '477',
vulnerabilities: 1,
vulnerabiliyRating: 1,
toReviewSecurityHotspots: 2,
openSecurityHotspots: 10,
wontFixSecurityHotspots: 0
},
{
cwe: '396',
vulnerabilities: 2,
vulnerabiliyRating: 2,
toReviewSecurityHotspots: 2,
openSecurityHotspots: 10,
wontFixSecurityHotspots: 0
}
];
return Promise.resolve({
categories: [
{
category: 'a1',
vulnerabilities: 2,
vulnerabiliyRating: 5,
toReviewSecurityHotspots: 2,
openSecurityHotspots: 10,
wontFixSecurityHotspots: 0,
distribution
},
{
category: 'a2',
vulnerabilities: 3,
vulnerabiliyRating: 3,
toReviewSecurityHotspots: 8,
openSecurityHotspots: 100,
wontFixSecurityHotspots: 10
}
]
});
})
}));

import * as React from 'react';
import { shallow } from 'enzyme';
import { Component } from '../../../../app/types';
import App from '../App';
import { waitAndUpdate } from '../../../../helpers/testUtils';

const getSecurityHotspots = require('../../../../api/security-reports')
.getSecurityHotspots as jest.Mock<any>;

const component = { key: 'foo', name: 'Foo', qualifier: 'TRK' } as Component;
const context = { router: { push: jest.fn() } };
const location = { pathname: 'foo', query: {} };
const locationWithCWE = { pathname: 'foo', query: { showCWE: 'true' } };
const owaspParams = { type: 'owasp_top_10' };
const sansParams = { type: 'sans_top_25' };
const wrongParams = { type: 'foo' };

beforeEach(() => {
getSecurityHotspots.mockClear();
});

it('renders error on wrong type parameters', () => {
const wrapper = shallow(<App component={component} location={location} params={wrongParams} />, {
context
});
expect(wrapper).toMatchSnapshot();
});

it('renders owaspTop10', () => {
const wrapper = shallow(<App component={component} location={location} params={owaspParams} />, {
context
});
expect(getSecurityHotspots).toBeCalledWith({
project: 'foo',
standard: 'owaspTop10',
includeDistribution: false,
branch: undefined
});
expect(wrapper).toMatchSnapshot();
});

it('renders with cwe', () => {
const wrapper = shallow(
<App component={component} location={locationWithCWE} params={owaspParams} />,
{ context }
);
expect(getSecurityHotspots).toBeCalledWith({
project: 'foo',
standard: 'owaspTop10',
includeDistribution: true,
branch: undefined
});
expect(wrapper).toMatchSnapshot();
});

it('handle checkbox for cwe display', async () => {
const wrapper = shallow(<App component={component} location={location} params={owaspParams} />, {
context
});
expect(getSecurityHotspots).toBeCalledWith({
project: 'foo',
standard: 'owaspTop10',
includeDistribution: false,
branch: undefined
});
expect(wrapper).toMatchSnapshot();

wrapper.find('Checkbox').prop<Function>('onCheck')(true);
await waitAndUpdate(wrapper);

expect(getSecurityHotspots).toBeCalledWith({
project: 'foo',
standard: 'owaspTop10',
includeDistribution: true,
branch: undefined
});
expect(wrapper).toMatchSnapshot();
});

it('renders sansTop25', () => {
const wrapper = shallow(<App component={component} location={location} params={sansParams} />, {
context
});
expect(getSecurityHotspots).toBeCalledWith({
project: 'foo',
standard: 'sansTop25',
includeDistribution: false,
branch: undefined
});
expect(wrapper).toMatchSnapshot();
});

+ 82
- 0
server/sonar-web/src/main/js/apps/securityReports/components/__tests__/VulnerabilityList-test.tsx View File

@@ -0,0 +1,82 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
import VulnerabilityList from '../VulnerabilityList';
import { Component } from '../../../../app/types';

jest.mock('../../../../helpers/standards.json', () => ({
default: {
owaspTop10: { a1: { title: 'a1 title' }, unknown: { title: 'Not OWAPS' } },
sansTop25: { 'risky-resource': { title: 'Risky Resource Management' } },
cwe: { 42: { title: 'cwe-42 title' }, unknown: { title: 'Unknown CWE' } }
}
}));

const component = { key: 'foo', name: 'Foo', qualifier: 'TRK' } as Component;
const findings = [
{
category: 'a1',
vulnerabilities: 2,
vulnerabilityRating: 5,
toReviewSecurityHotspots: 2,
openSecurityHotspots: 10,
wontFixSecurityHotspots: 0,
distribution: [
{
cwe: '42',
vulnerabilities: 1,
vulnerabilityRating: 1,
toReviewSecurityHotspots: 2,
openSecurityHotspots: 10,
wontFixSecurityHotspots: 0
}
]
},
{
category: 'unknown',
vulnerabilities: 3,
vulnerabilityRating: 3,
toReviewSecurityHotspots: 8,
openSecurityHotspots: 100,
wontFixSecurityHotspots: 10
}
];

it('renders', () => {
const wrapper = shallow(
<VulnerabilityList
component={component}
findings={findings}
showCWE={false}
type="owaspTop10"
/>
);
expect(wrapper.find('tr').length).toBe(4);
expect(wrapper).toMatchSnapshot();
});

it('renders with cwe', () => {
const wrapper = shallow(
<VulnerabilityList component={component} findings={findings} showCWE={true} type="owaspTop10" />
);
expect(wrapper.find('tr').length).toBe(5);
expect(wrapper).toMatchSnapshot();
});

+ 394
- 0
server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/App-test.tsx.snap View File

@@ -0,0 +1,394 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`handle checkbox for cwe display 1`] = `
<div
className="page page-limited"
id="security-reports"
>
<Suggestions
suggestions="security_reports"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="security_reports.owaspTop10.page"
/>
<header
className="page-header"
>
<h1
className="page-title"
>
security_reports.owaspTop10.page
</h1>
<div
className="page-description"
>
security_reports.owaspTop10.description
</div>
</header>
<div
className="display-inline-flex-center"
>
<Checkbox
checked={false}
className="spacer-left spacer-right vertical-middle"
disabled={true}
id="showCWE"
onCheck={[Function]}
thirdState={false}
>
<label
className="little-spacer-left"
htmlFor="showCWE"
>
security_reports.cwe.show
<DocTooltip
className="spacer-left"
doc="security-reports/cwe"
/>
</label>
</Checkbox>
</div>
<DeferredSpinner
loading={true}
timeout={100}
>
<VulnerabilityList
component={
Object {
"key": "foo",
"name": "Foo",
"qualifier": "TRK",
}
}
findings={Array []}
showCWE={false}
type="owaspTop10"
/>
</DeferredSpinner>
</div>
`;

exports[`handle checkbox for cwe display 2`] = `
<div
className="page page-limited"
id="security-reports"
>
<Suggestions
suggestions="security_reports"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="security_reports.owaspTop10.page"
/>
<header
className="page-header"
>
<h1
className="page-title"
>
security_reports.owaspTop10.page
</h1>
<div
className="page-description"
>
security_reports.owaspTop10.description
</div>
</header>
<div
className="display-inline-flex-center"
>
<Checkbox
checked={true}
className="spacer-left spacer-right vertical-middle"
disabled={false}
id="showCWE"
onCheck={[Function]}
thirdState={false}
>
<label
className="little-spacer-left"
htmlFor="showCWE"
>
security_reports.cwe.show
<DocTooltip
className="spacer-left"
doc="security-reports/cwe"
/>
</label>
</Checkbox>
</div>
<DeferredSpinner
loading={false}
timeout={100}
>
<VulnerabilityList
component={
Object {
"key": "foo",
"name": "Foo",
"qualifier": "TRK",
}
}
findings={
Array [
Object {
"category": "a1",
"distribution": Array [
Object {
"cwe": "477",
"openSecurityHotspots": 10,
"toReviewSecurityHotspots": 2,
"vulnerabilities": 1,
"vulnerabiliyRating": 1,
"wontFixSecurityHotspots": 0,
},
Object {
"cwe": "396",
"openSecurityHotspots": 10,
"toReviewSecurityHotspots": 2,
"vulnerabilities": 2,
"vulnerabiliyRating": 2,
"wontFixSecurityHotspots": 0,
},
],
"openSecurityHotspots": 10,
"toReviewSecurityHotspots": 2,
"vulnerabilities": 2,
"vulnerabiliyRating": 5,
"wontFixSecurityHotspots": 0,
},
Object {
"category": "a2",
"openSecurityHotspots": 100,
"toReviewSecurityHotspots": 8,
"vulnerabilities": 3,
"vulnerabiliyRating": 3,
"wontFixSecurityHotspots": 10,
},
]
}
showCWE={true}
type="owaspTop10"
/>
</DeferredSpinner>
</div>
`;

exports[`renders error on wrong type parameters 1`] = `
<NotFound
withContainer={false}
/>
`;

exports[`renders owaspTop10 1`] = `
<div
className="page page-limited"
id="security-reports"
>
<Suggestions
suggestions="security_reports"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="security_reports.owaspTop10.page"
/>
<header
className="page-header"
>
<h1
className="page-title"
>
security_reports.owaspTop10.page
</h1>
<div
className="page-description"
>
security_reports.owaspTop10.description
</div>
</header>
<div
className="display-inline-flex-center"
>
<Checkbox
checked={false}
className="spacer-left spacer-right vertical-middle"
disabled={true}
id="showCWE"
onCheck={[Function]}
thirdState={false}
>
<label
className="little-spacer-left"
htmlFor="showCWE"
>
security_reports.cwe.show
<DocTooltip
className="spacer-left"
doc="security-reports/cwe"
/>
</label>
</Checkbox>
</div>
<DeferredSpinner
loading={true}
timeout={100}
>
<VulnerabilityList
component={
Object {
"key": "foo",
"name": "Foo",
"qualifier": "TRK",
}
}
findings={Array []}
showCWE={false}
type="owaspTop10"
/>
</DeferredSpinner>
</div>
`;

exports[`renders sansTop25 1`] = `
<div
className="page page-limited"
id="security-reports"
>
<Suggestions
suggestions="security_reports"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="security_reports.sansTop25.page"
/>
<header
className="page-header"
>
<h1
className="page-title"
>
security_reports.sansTop25.page
</h1>
<div
className="page-description"
>
security_reports.sansTop25.description
</div>
</header>
<div
className="display-inline-flex-center"
>
<Checkbox
checked={false}
className="spacer-left spacer-right vertical-middle"
disabled={true}
id="showCWE"
onCheck={[Function]}
thirdState={false}
>
<label
className="little-spacer-left"
htmlFor="showCWE"
>
security_reports.cwe.show
<DocTooltip
className="spacer-left"
doc="security-reports/cwe"
/>
</label>
</Checkbox>
</div>
<DeferredSpinner
loading={true}
timeout={100}
>
<VulnerabilityList
component={
Object {
"key": "foo",
"name": "Foo",
"qualifier": "TRK",
}
}
findings={Array []}
showCWE={false}
type="sansTop25"
/>
</DeferredSpinner>
</div>
`;

exports[`renders with cwe 1`] = `
<div
className="page page-limited"
id="security-reports"
>
<Suggestions
suggestions="security_reports"
/>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="security_reports.owaspTop10.page"
/>
<header
className="page-header"
>
<h1
className="page-title"
>
security_reports.owaspTop10.page
</h1>
<div
className="page-description"
>
security_reports.owaspTop10.description
</div>
</header>
<div
className="display-inline-flex-center"
>
<Checkbox
checked={true}
className="spacer-left spacer-right vertical-middle"
disabled={true}
id="showCWE"
onCheck={[Function]}
thirdState={false}
>
<label
className="little-spacer-left"
htmlFor="showCWE"
>
security_reports.cwe.show
<DocTooltip
className="spacer-left"
doc="security-reports/cwe"
/>
</label>
</Checkbox>
</div>
<DeferredSpinner
loading={true}
timeout={100}
>
<VulnerabilityList
component={
Object {
"key": "foo",
"name": "Foo",
"qualifier": "TRK",
}
}
findings={Array []}
showCWE={true}
type="owaspTop10"
/>
</DeferredSpinner>
</div>
`;

+ 744
- 0
server/sonar-web/src/main/js/apps/securityReports/components/__tests__/__snapshots__/VulnerabilityList-test.tsx.snap View File

@@ -0,0 +1,744 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<div
className="boxed-group boxed-group-inner spacer-top"
>
<table
className="data zebra"
>
<thead>
<tr>
<th
className="security-category-column"
colSpan={2}
>
security_reports.list.categories
</th>
<th
className="security-result-column"
>
<div
className="display-inline-flex-center"
>
<VulnerabilityIcon
className="spacer-right"
/>
security_reports.list.vulnerabilities
</div>
</th>
<th
colSpan={3}
>
<div
className="display-inline-flex-center"
>
<SecurityHotspotIcon
className="spacer-right"
/>
security_reports.list.hotspots
</div>
</th>
</tr>
<tr
className="subheader"
>
<th
colSpan={3}
/>
<th
className="security-result-column"
>
security_reports.line.open
</th>
<th
className="security-result-column"
>
security_reports.line.in_review
</th>
<th
className="security-result-column"
>
security_reports.line.wont_fix
</th>
</tr>
</thead>
<tbody>
<React.Fragment
key="a1"
>
<tr>
<td
className="nowrap"
colSpan={2}
>
<div
className="display-inline-flex-center"
>
<React.Fragment>
A1
</React.Fragment>
</div>
</td>
<td>
<div
className="display-inline-flex-center"
>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "a1",
"types": "VULNERABILITY",
},
}
}
>
2
</Link>
<Link
className="link-no-underline spacer-left"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "a1",
"types": "VULNERABILITY",
},
}
}
>
<Rating
value={5}
/>
</Link>
</div>
</td>
<td>
<Link
className="spacer-right"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "a1",
"resolved": "false",
"statuses": "OPEN",
"types": "SECURITY_HOTSPOT",
},
}
}
>
10
</Link>
</td>
<td>
<Link
className="spacer-right"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "a1",
"resolutions": "FIXED",
"types": "SECURITY_HOTSPOT",
},
}
}
>
2
</Link>
</td>
<td>
<Link
className="spacer-right"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "a1",
"resolutions": "WONTFIX",
"types": "SECURITY_HOTSPOT",
},
}
}
>
0
</Link>
</td>
</tr>
</React.Fragment>
<React.Fragment
key="unknown"
>
<tr>
<td
className="nowrap"
colSpan={2}
>
<div
className="display-inline-flex-center"
>
<React.Fragment>
UNKNOWN
</React.Fragment>
</div>
</td>
<td>
<div
className="display-inline-flex-center"
>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "unknown",
"types": "VULNERABILITY",
},
}
}
>
3
</Link>
<Link
className="link-no-underline spacer-left"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "unknown",
"types": "VULNERABILITY",
},
}
}
>
<Rating
value={3}
/>
</Link>
</div>
</td>
<td>
<Link
className="spacer-right"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "unknown",
"resolved": "false",
"statuses": "OPEN",
"types": "SECURITY_HOTSPOT",
},
}
}
>
100
</Link>
</td>
<td>
<Link
className="spacer-right"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "unknown",
"resolutions": "FIXED",
"types": "SECURITY_HOTSPOT",
},
}
}
>
8
</Link>
</td>
<td>
<Link
className="spacer-right"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "unknown",
"resolutions": "WONTFIX",
"types": "SECURITY_HOTSPOT",
},
}
}
>
10
</Link>
</td>
</tr>
</React.Fragment>
</tbody>
</table>
</div>
`;

exports[`renders with cwe 1`] = `
<div
className="boxed-group boxed-group-inner spacer-top"
>
<table
className="data zebra"
>
<thead>
<tr>
<th
className="security-category-column"
colSpan={2}
>
security_reports.list.categories
</th>
<th
className="security-result-column"
>
<div
className="display-inline-flex-center"
>
<VulnerabilityIcon
className="spacer-right"
/>
security_reports.list.vulnerabilities
</div>
</th>
<th
colSpan={3}
>
<div
className="display-inline-flex-center"
>
<SecurityHotspotIcon
className="spacer-right"
/>
security_reports.list.hotspots
</div>
</th>
</tr>
<tr
className="subheader"
>
<th
colSpan={3}
/>
<th
className="security-result-column"
>
security_reports.line.open
</th>
<th
className="security-result-column"
>
security_reports.line.in_review
</th>
<th
className="security-result-column"
>
security_reports.line.wont_fix
</th>
</tr>
</thead>
<tbody>
<React.Fragment
key="a1"
>
<tr>
<td
className="nowrap"
colSpan={2}
>
<div
className="display-inline-flex-center"
>
<React.Fragment>
A1
</React.Fragment>
</div>
</td>
<td>
<div
className="display-inline-flex-center"
>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "a1",
"types": "VULNERABILITY",
},
}
}
>
2
</Link>
<Link
className="link-no-underline spacer-left"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "a1",
"types": "VULNERABILITY",
},
}
}
>
<Rating
value={5}
/>
</Link>
</div>
</td>
<td>
<Link
className="spacer-right"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "a1",
"resolved": "false",
"statuses": "OPEN",
"types": "SECURITY_HOTSPOT",
},
}
}
>
10
</Link>
</td>
<td>
<Link
className="spacer-right"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "a1",
"resolutions": "FIXED",
"types": "SECURITY_HOTSPOT",
},
}
}
>
2
</Link>
</td>
<td>
<Link
className="spacer-right"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "a1",
"resolutions": "WONTFIX",
"types": "SECURITY_HOTSPOT",
},
}
}
>
0
</Link>
</td>
</tr>
<React.Fragment
key="42"
>
<tr>
<td />
<td
className="nowrap"
colSpan={1}
>
<div
className="display-inline-flex-center"
>
<React.Fragment>
CWE-42
</React.Fragment>
</div>
</td>
<td>
<div
className="display-inline-flex-center"
>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "42",
"types": "VULNERABILITY",
},
}
}
>
1
</Link>
<Link
className="link-no-underline spacer-left"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "42",
"types": "VULNERABILITY",
},
}
}
>
<Rating
value={1}
/>
</Link>
</div>
</td>
<td>
<Link
className="spacer-right"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "42",
"resolved": "false",
"statuses": "OPEN",
"types": "SECURITY_HOTSPOT",
},
}
}
>
10
</Link>
</td>
<td>
<Link
className="spacer-right"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "42",
"resolutions": "FIXED",
"types": "SECURITY_HOTSPOT",
},
}
}
>
2
</Link>
</td>
<td>
<Link
className="spacer-right"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "42",
"resolutions": "WONTFIX",
"types": "SECURITY_HOTSPOT",
},
}
}
>
0
</Link>
</td>
</tr>
</React.Fragment>
</React.Fragment>
<React.Fragment
key="unknown"
>
<tr>
<td
className="nowrap"
colSpan={2}
>
<div
className="display-inline-flex-center"
>
<React.Fragment>
UNKNOWN
</React.Fragment>
</div>
</td>
<td>
<div
className="display-inline-flex-center"
>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "unknown",
"types": "VULNERABILITY",
},
}
}
>
3
</Link>
<Link
className="link-no-underline spacer-left"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "unknown",
"types": "VULNERABILITY",
},
}
}
>
<Rating
value={3}
/>
</Link>
</div>
</td>
<td>
<Link
className="spacer-right"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "unknown",
"resolved": "false",
"statuses": "OPEN",
"types": "SECURITY_HOTSPOT",
},
}
}
>
100
</Link>
</td>
<td>
<Link
className="spacer-right"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "unknown",
"resolutions": "FIXED",
"types": "SECURITY_HOTSPOT",
},
}
}
>
8
</Link>
</td>
<td>
<Link
className="spacer-right"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/issues",
"query": Object {
"id": "foo",
"owaspTop10": "unknown",
"resolutions": "WONTFIX",
"types": "SECURITY_HOTSPOT",
},
}
}
>
10
</Link>
</td>
</tr>
</React.Fragment>
</tbody>
</table>
</div>
`;

+ 26
- 0
server/sonar-web/src/main/js/apps/securityReports/style.css View File

@@ -0,0 +1,26 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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-category-column {
width: 52%;
}

.security-result-column {
width: 12%;
}

+ 51
- 0
server/sonar-web/src/main/js/apps/securityReports/utils.ts View File

@@ -0,0 +1,51 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 interface Standards {
owaspTop10: { [x: string]: { title: string; description?: string } };
sansTop25: { [x: string]: { title: string; description?: string } };
cwe: { [x: string]: { title: string; description?: string } };
}

export function renderOwaspTop10Category(standards: Standards, category: string): string {
const record = standards.owaspTop10[category];
if (!record) {
return category.toUpperCase();
} else if (category === 'unknown') {
return record.title;
} else {
return `${category.toUpperCase()} - ${record.title}`;
}
}

export function renderCWECategory(standards: Standards, category: string): string {
const record = standards.cwe[category];
if (!record) {
return `CWE-${category}`;
} else if (category === 'unknown') {
return record.title;
} else {
return `CWE-${category} - ${record.title}`;
}
}

export function renderSansTop25Category(standards: Standards, category: string): string {
const record = standards.sansTop25[category];
return record ? record.title : category;
}

+ 1
- 1
server/sonar-web/src/main/js/components/controls/Checkbox.tsx View File

@@ -56,7 +56,7 @@ export default class Checkbox extends React.PureComponent<Props> {
return (
<a
className={classNames('link-checkbox', this.props.className, {
'text-muted': this.props.disabled,
note: this.props.disabled,
disabled: this.props.disabled
})}
href="#"

+ 4
- 4
server/sonar-web/src/main/js/helpers/standards.json View File

@@ -3,12 +3,12 @@
"a1": {
"title": "Injection",
"description":
"Injection flaws, such as SQL, NoSQL, OS, and LDAP injection, occur when untrusted data is sent to an interpreter as part of a command or query. The attackers hostile data can trick the interpreter into executing unintended commands or accessing data without proper authorization."
"Injection flaws, such as SQL, NoSQL, OS, and LDAP injection, occur when untrusted data is sent to an interpreter as part of a command or query. The attacker’s hostile data can trick the interpreter into executing unintended commands or accessing data without proper authorization."
},
"a2": {
"title": "Broken Authentication",
"description":
"Application functions related to authentication and session management are often implemented incorrectly, allowing attackers to compromise passwords, keys, or session tokens, or to exploit other implementation flaws to assume other users identities temporarily or permanently."
"Application functions related to authentication and session management are often implemented incorrectly, allowing attackers to compromise passwords, keys, or session tokens, or to exploit other implementation flaws to assume other users’ identities temporarily or permanently."
},
"a3": {
"title": "Sensitive Data Exposure",
@@ -23,7 +23,7 @@
"a5": {
"title": "Broken Access Control",
"description":
"Restrictions on what authenticated users are allowed to do are often not properly enforced. Attackers can exploit these flaws to access unauthorized functionality and/or data, such as access other users' accounts, view sensitive files, modify other users data, change access rights, etc."
"Restrictions on what authenticated users are allowed to do are often not properly enforced. Attackers can exploit these flaws to access unauthorized functionality and/or data, such as access other users' accounts, view sensitive files, modify other users’ data, change access rights, etc."
},
"a6": {
"title": "Security Misconfiguration",
@@ -33,7 +33,7 @@
"a7": {
"title": "Cross-Site Scripting (XSS)",
"description":
"XSS flaws occur whenever an application includes untrusted data in a new web page without proper validation or escaping, or updates an existing web page with user-supplied data using a browser API that can create HTML or JavaScript. XSS allows attackers to execute scripts in the victims browser which can hijack user sessions, deface web sites, or redirect the user to malicious sites."
"XSS flaws occur whenever an application includes untrusted data in a new web page without proper validation or escaping, or updates an existing web page with user-supplied data using a browser API that can create HTML or JavaScript. XSS allows attackers to execute scripts in the victim’s browser which can hijack user sessions, deface web sites, or redirect the user to malicious sites."
},
"a8": {
"title": "Insecure Deserialization",

+ 17
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -442,6 +442,7 @@ layout.login=Log in
layout.logout=Log out
layout.measures=Measures
layout.settings=Administration
layout.security_reports=Security Reports
layout.sonar.slogan=Continuous Code Quality

sidebar.projects=Projects
@@ -2012,7 +2013,22 @@ organizations_permissions.scan.desc=Ability to get all settings required to perf
organizations_permissions.provisioning=Create Projects
organizations_permissions.provisioning.desc=Ability to initialize a project so its settings can be configured before the first analysis.


#------------------------------------------------------------------------------
#
# SECURITY REPORTS PAGE
#
#------------------------------------------------------------------------------
security_reports.owaspTop10.page=OWASP Top 10
security_reports.sansTop25.page=SANS Top 25
security_reports.owaspTop10.description=Track Vulnerabilities and Security Hotspots conforming to OWASP Top 10 standard.
security_reports.sansTop25.description=Track Vulnerabilities and Security Hotspots conforming to SANS Top 25 standard.
security_reports.list.categories=Categories
security_reports.list.vulnerabilities=Vulnerabilities
security_reports.list.hotspots=Security Hotspots
security_reports.line.open=Open
security_reports.line.wont_fix=Won't Fix
security_reports.line.in_review=In Review
security_reports.cwe.show=Show CWE distribution

#------------------------------------------------------------------------------
#

Loading…
Cancel
Save