Kaynağa Gözat

SONAR-11973 Create a new SonarSource facet under the Standards facet group

tags/7.8
Wouter Admiraal 5 yıl önce
ebeveyn
işleme
f8a998749f

+ 27
- 24
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx Dosyayı Görüntüle

@@ -143,6 +143,33 @@ export default class Sidebar extends React.PureComponent<Props> {
stats={facets.statuses}
statuses={query.statuses}
/>
<StandardFacet
cwe={query.cwe}
cweOpen={!!openFacets.cwe}
cweStats={facets.cwe}
fetchingCwe={this.props.loadingFacets.cwe === true}
fetchingOwaspTop10={this.props.loadingFacets.owaspTop10 === true}
fetchingSansTop25={this.props.loadingFacets.sansTop25 === true}
fetchingSonarSourceSecurity={this.props.loadingFacets.sonarsourceSecurity === true}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={
openFacets[STANDARDS] === undefined
? query.types.includes('SECURITY_HOTSPOT') || query.types.includes('VULNERABILITY')
: openFacets[STANDARDS]
}
owaspTop10={query.owaspTop10}
owaspTop10Open={!!openFacets.owaspTop10}
owaspTop10Stats={facets.owaspTop10}
query={query}
sansTop25={query.sansTop25}
sansTop25Open={!!openFacets.sansTop25}
sansTop25Stats={facets.sansTop25}
sonarsourceSecurity={query.sonarsourceSecurity}
sonarsourceSecurityOpen={!!openFacets.sonarsourceSecurity}
sonarsourceSecurityStats={facets.sonarsourceSecurity}
/>
<CreationDateFacet
component={component}
createdAfter={query.createdAfter}
@@ -180,30 +207,6 @@ export default class Sidebar extends React.PureComponent<Props> {
rules={query.rules}
stats={facets.rules}
/>
<StandardFacet
cwe={query.cwe}
cweOpen={!!openFacets.cwe}
cweStats={facets.cwe}
fetchingCwe={this.props.loadingFacets.cwe === true}
fetchingOwaspTop10={this.props.loadingFacets.owaspTop10 === true}
fetchingSansTop25={this.props.loadingFacets.sansTop25 === true}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets[STANDARDS]}
owaspTop10={query.owaspTop10}
owaspTop10Open={!!openFacets.owaspTop10}
owaspTop10Stats={facets.owaspTop10}
query={query}
sansTop25={query.sansTop25}
sansTop25Open={!!openFacets.sansTop25}
sansTop25Stats={facets.sansTop25}
sonarsourceSecurity={
[
/* TODO */
]
}
/>
<TagFacet
component={component}
fetching={this.props.loadingFacets.tags === true}

+ 72
- 18
server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx Dosyayı Görüntüle

@@ -28,20 +28,23 @@ import FacetItem from '../../../components/facet/FacetItem';
import {
renderOwaspTop10Category,
renderSansTop25Category,
renderCWECategory
renderCWECategory,
renderSonarSourceSecurityCategory
} from '../../securityReports/utils';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import ListStyleFacet from '../../../components/facet/ListStyleFacet';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';
import { highlightTerm } from '../../../helpers/search';
import ListStyleFacet from '../../../components/facet/ListStyleFacet';
import { getStandards } from '../../../helpers/security-standard';

export interface Props {
interface Props {
cwe: string[];
cweOpen: boolean;
cweStats: T.Dict<number> | undefined;
fetchingCwe: boolean;
fetchingOwaspTop10: boolean;
fetchingSansTop25: boolean;
fetchingCwe: boolean;
fetchingSonarSourceSecurity: boolean;
loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>;
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
@@ -54,6 +57,8 @@ export interface Props {
sansTop25Open: boolean;
sansTop25Stats: T.Dict<number> | undefined;
sonarsourceSecurity: string[];
sonarsourceSecurityOpen: boolean;
sonarsourceSecurityStats: T.Dict<number> | undefined;
}

interface State {
@@ -61,7 +66,7 @@ interface State {
standards: T.Standards;
}

type StatsProp = 'owaspTop10Stats' | 'cweStats' | 'sansTop25Stats';
type StatsProp = 'owaspTop10Stats' | 'cweStats' | 'sansTop25Stats' | 'sonarsourceSecurityStats';
type ValuesProp = T.StandardType;

export default class StandardFacet extends React.PureComponent<Props, State> {
@@ -80,7 +85,8 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
this.props.open ||
this.props.owaspTop10.length > 0 ||
this.props.cwe.length > 0 ||
this.props.sansTop25.length > 0
this.props.sansTop25.length > 0 ||
this.props.sonarsourceSecurity.length > 0
) {
this.loadStandards();
}
@@ -97,20 +103,21 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
}

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

getValues = () => {
return [
...this.props.sonarsourceSecurity.map(item =>
renderSonarSourceSecurityCategory(this.state.standards, item, true)
),
...this.props.owaspTop10.map(item =>
renderOwaspTop10Category(this.state.standards, item, true)
),
@@ -133,8 +140,18 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
this.props.onToggle('sansTop25');
};

handleSonarSourceSecurityHeaderClick = () => {
this.props.onToggle('sonarsourceSecurity');
};

handleClear = () => {
this.props.onChange({ [this.property]: [], owaspTop10: [], sansTop25: [], cwe: [] });
this.props.onChange({
[this.property]: [],
owaspTop10: [],
sansTop25: [],
cwe: [],
sonarsourceSecurity: []
});
};

handleItemClick = (prop: ValuesProp, itemValue: string, multiple: boolean) => {
@@ -159,6 +176,10 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
this.handleItemClick('sansTop25', itemValue, multiple);
};

handleSonarSourceSecurityItemClick = (itemValue: string, multiple: boolean) => {
this.handleItemClick('sonarsourceSecurity', itemValue, multiple);
};

handleCWESearch = (query: string) => {
return Promise.resolve({
results: Object.keys(this.state.standards.cwe).filter(cwe =>
@@ -170,7 +191,10 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
};

loadCWESearchResultCount = (categories: string[]) => {
return this.props.loadSearchResultCount('cwe', { cwe: categories });
const { loadSearchResultCount } = this.props;
return loadSearchResultCount
? loadSearchResultCount('cwe', { cwe: categories })
: Promise.resolve({});
};

renderList = (
@@ -258,9 +282,39 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
return this.renderHint('sansTop25Stats', 'sansTop25');
}

renderSonarSourceSecurityList() {
return this.renderList(
'sonarsourceSecurityStats',
'sonarsourceSecurity',
renderSonarSourceSecurityCategory,
this.handleSonarSourceSecurityItemClick
);
}

renderSonarSourceSecurityHint() {
return this.renderHint('sonarsourceSecurityStats', 'sonarsourceSecurity');
}

renderSubFacets() {
return (
<>
<FacetBox className="is-inner" property="sonarsourceSecurity">
<FacetHeader
name={translate('issues.facet.sonarsourceSecurity')}
onClick={this.handleSonarSourceSecurityHeaderClick}
open={this.props.sonarsourceSecurityOpen}
values={this.props.sonarsourceSecurity.map(item =>
renderSonarSourceSecurityCategory(this.state.standards, item)
)}
/>
<DeferredSpinner loading={this.props.fetchingSonarSourceSecurity} />
{this.props.sonarsourceSecurityOpen && (
<>
{this.renderSonarSourceSecurityList()}
{this.renderSonarSourceSecurityHint()}
</>
)}
</FacetBox>
<FacetBox className="is-inner" property="owaspTop10">
<FacetHeader
name={translate('issues.facet.owaspTop10')}

+ 67
- 10
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx Dosyayı Görüntüle

@@ -19,12 +19,48 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import StandardFacet, { Props } from '../StandardFacet';
import StandardFacet from '../StandardFacet';
import { click } from '../../../../helpers/testUtils';
import { Query } from '../../utils';
import { getStandards } from '../../../../helpers/security-standard';

jest.mock('../../../../helpers/security-standard', () => ({
getStandards: jest.fn().mockResolvedValue({
owaspTop10: {
a1: {
title: 'Injection'
},
a2: {
title: 'Broken Authentication'
}
},
sansTop25: {
'insecure-interaction': {
title: 'Insecure Interaction Between Components'
}
},
cwe: {
unknown: {
title: 'No CWE associated'
},
'1004': {
title: "Sensitive Cookie Without 'HttpOnly' Flag"
}
},
sonarsourceSecurity: {
'sql-injection': {
title: 'SQL Injection'
},
'command-injection': {
title: 'Command Injection'
}
}
})
}));

it('should render closed', () => {
expect(shallowRender()).toMatchSnapshot();
expect(getStandards).not.toBeCalled();
});

it('should toggle standards facet', () => {
@@ -38,7 +74,13 @@ it('should clear standards facet', () => {
const onChange = jest.fn();
const wrapper = shallowRender({ onChange });
wrapper.children('FacetHeader').prop<Function>('onClear')();
expect(onChange).toBeCalledWith({ cwe: [], owaspTop10: [], sansTop25: [], standards: [] });
expect(onChange).toBeCalledWith({
cwe: [],
owaspTop10: [],
sansTop25: [],
sonarsourceSecurity: [],
standards: []
});
});

it('should render sub-facets', () => {
@@ -53,9 +95,13 @@ it('should render sub-facets', () => {
owaspTop10Stats: { a1: 15, a3: 5 },
sansTop25: ['risky-resource'],
sansTop25Open: true,
sansTop25Stats: { foo: 12, 'risky-resource': 10 }
sansTop25Stats: { foo: 12, 'risky-resource': 10 },
sonarsourceSecurity: ['sql-injection'],
sonarsourceSecurityOpen: true,
sonarsourceSecurityStats: { 'sql-injection': 12 }
})
).toMatchSnapshot();
expect(getStandards).toBeCalled();
});

it('should render empty sub-facet', () => {
@@ -79,12 +125,16 @@ it('should select items', () => {
owaspTop10Stats: { a1: 15, a3: 5 },
sansTop25: ['risky-resource'],
sansTop25Open: true,
sansTop25Stats: { foo: 12, 'risky-resource': 10 }
sansTop25Stats: { foo: 12, 'risky-resource': 10 },
sonarsourceSecurity: ['command-injection'],
sonarsourceSecurityOpen: true,
sonarsourceSecurityStats: { 'sql-injection': 10 }
});

selectAndCheck('owaspTop10', 'a1');
selectAndCheck('owaspTop10', 'a1', true, ['a1', 'a3']);
selectAndCheck('sansTop25', 'foo');
selectAndCheck('sonarsourceSecurity', 'sql-injection');

function selectAndCheck(facet: string, value: string, multiple = false, expectedValue = [value]) {
wrapper
@@ -102,6 +152,8 @@ it('should toggle sub-facets', () => {
expect(onToggle).lastCalledWith('owaspTop10');
click(wrapper.find('FacetBox[property="sansTop25"]').children('FacetHeader'));
expect(onToggle).lastCalledWith('sansTop25');
click(wrapper.find('FacetBox[property="sonarsourceSecurity"]').children('FacetHeader'));
expect(onToggle).lastCalledWith('sonarsourceSecurity');
});

it('should display correct selection', () => {
@@ -109,9 +161,11 @@ it('should display correct selection', () => {
open: true,
owaspTop10: ['a1', 'a3', 'unknown'],
sansTop25: ['risky-resource', 'foo'],
cwe: ['42', '1111', 'unknown']
cwe: ['42', '1111', 'unknown'],
sonarsourceSecurity: ['sql-injection']
});
checkValues('standards', [
'SONAR SQL Injection',
'OWASP A1 - a1 title',
'OWASP A3',
'Not OWAPS',
@@ -123,6 +177,7 @@ it('should display correct selection', () => {
]);
checkValues('owaspTop10', ['A1 - a1 title', 'A3', 'Not OWAPS']);
checkValues('sansTop25', ['Risky Resource Management', 'foo']);
checkValues('sonarsourceSecurity', ['SQL Injection']);

function checkValues(property: string, values: string[]) {
expect(
@@ -134,7 +189,7 @@ it('should display correct selection', () => {
}
});

function shallowRender(props: Partial<Props> = {}) {
function shallowRender(props: Partial<StandardFacet['props']> = {}) {
const wrapper = shallow(
<StandardFacet
cwe={[]}
@@ -143,6 +198,7 @@ function shallowRender(props: Partial<Props> = {}) {
fetchingCwe={false}
fetchingOwaspTop10={false}
fetchingSansTop25={false}
fetchingSonarSourceSecurity={false}
loadSearchResultCount={jest.fn()}
onChange={jest.fn()}
onToggle={jest.fn()}
@@ -155,16 +211,17 @@ function shallowRender(props: Partial<Props> = {}) {
sansTop25Open={false}
sansTop25Stats={{}}
sonarsourceSecurity={[]}
sonarsourceSecurityOpen={false}
sonarsourceSecurityStats={{}}
{...props}
/>,
// disable loading of standards.json
{ disableLifecycleMethods: true }
/>
);
wrapper.setState({
standards: {
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' } }
cwe: { 42: { title: 'cwe-42 title' }, unknown: { title: 'Unknown CWE' } },
sonarsourceSecurity: { 'sql-injection': { title: 'SQL Injection' } }
}
});
return wrapper;

+ 6
- 6
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.tsx.snap Dosyayı Görüntüle

@@ -6,10 +6,10 @@ Array [
"SeverityFacet",
"ResolutionFacet",
"StatusFacet",
"StandardFacet",
"InjectIntl(CreationDateFacet)",
"Connect(LanguageFacet)",
"RuleFacet",
"StandardFacet",
"TagFacet",
"ProjectFacet",
"DirectoryFacet",
@@ -24,10 +24,10 @@ Array [
"SeverityFacet",
"ResolutionFacet",
"StatusFacet",
"StandardFacet",
"InjectIntl(CreationDateFacet)",
"Connect(LanguageFacet)",
"RuleFacet",
"StandardFacet",
"TagFacet",
"FileFacet",
"AssigneeFacet",
@@ -41,10 +41,10 @@ Array [
"SeverityFacet",
"ResolutionFacet",
"StatusFacet",
"StandardFacet",
"InjectIntl(CreationDateFacet)",
"Connect(LanguageFacet)",
"RuleFacet",
"StandardFacet",
"TagFacet",
"ProjectFacet",
"AssigneeFacet",
@@ -58,10 +58,10 @@ Array [
"SeverityFacet",
"ResolutionFacet",
"StatusFacet",
"StandardFacet",
"InjectIntl(CreationDateFacet)",
"Connect(LanguageFacet)",
"RuleFacet",
"StandardFacet",
"TagFacet",
"DirectoryFacet",
"FileFacet",
@@ -76,10 +76,10 @@ Array [
"SeverityFacet",
"ResolutionFacet",
"StatusFacet",
"StandardFacet",
"InjectIntl(CreationDateFacet)",
"Connect(LanguageFacet)",
"RuleFacet",
"StandardFacet",
"TagFacet",
"DirectoryFacet",
"FileFacet",
@@ -94,10 +94,10 @@ Array [
"SeverityFacet",
"ResolutionFacet",
"StatusFacet",
"StandardFacet",
"InjectIntl(CreationDateFacet)",
"Connect(LanguageFacet)",
"RuleFacet",
"StandardFacet",
"TagFacet",
"ProjectFacet",
"AuthorFacet",

+ 38
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap Dosyayı Görüntüle

@@ -52,12 +52,50 @@ exports[`should render sub-facets 1`] = `
open={true}
values={
Array [
"SONAR SQL Injection",
"OWASP A3",
"SANS Risky Resource Management",
"CWE-42 - cwe-42 title",
]
}
/>
<FacetBox
className="is-inner"
property="sonarsourceSecurity"
>
<FacetHeader
name="issues.facet.sonarsourceSecurity"
onClick={[Function]}
open={true}
values={
Array [
"SQL Injection",
]
}
/>
<DeferredSpinner
loading={false}
timeout={100}
/>
<FacetItemsList>
<FacetItem
active={true}
disabled={false}
halfWidth={false}
key="sql-injection"
loading={false}
name="SQL Injection"
onClick={[Function]}
stat="12"
tooltip="SQL Injection"
value="sql-injection"
/>
</FacetItemsList>
<MultipleSelectionHint
options={1}
values={1}
/>
</FacetBox>
<FacetBox
className="is-inner"
property="owaspTop10"

+ 3
- 0
server/sonar-web/src/main/js/apps/issues/utils.ts Dosyayı Görüntüle

@@ -58,6 +58,7 @@ export interface Query {
sansTop25: string[];
severities: string[];
sinceLeakPeriod: boolean;
sonarsourceSecurity: string[];
sort: string;
statuses: string[];
tags: string[];
@@ -93,6 +94,7 @@ export function parseQuery(query: RawQuery): Query {
sansTop25: parseAsArray(query.sansTop25, parseAsString),
severities: parseAsArray(query.severities, parseAsString),
sinceLeakPeriod: parseAsBoolean(query.sinceLeakPeriod, false),
sonarsourceSecurity: parseAsArray(query.sonarsourceSecurity, parseAsString),
sort: parseAsSort(query.s),
statuses: parseAsArray(query.statuses, parseAsString),
tags: parseAsArray(query.tags, parseAsString),
@@ -130,6 +132,7 @@ export function serializeQuery(query: Query): RawQuery {
sansTop25: serializeStringArray(query.sansTop25),
severities: serializeStringArray(query.severities),
sinceLeakPeriod: query.sinceLeakPeriod ? 'true' : undefined,
sonarsourceSecurity: serializeStringArray(query.sonarsourceSecurity),
statuses: serializeStringArray(query.statuses),
tags: serializeStringArray(query.tags),
types: serializeStringArray(query.types)

+ 1
- 1
server/sonar-web/src/main/js/apps/securityReports/utils.ts Dosyayı Görüntüle

@@ -52,7 +52,7 @@ export function renderSansTop25Category(
return addPrefix(record ? record.title : category, 'SANS', withPrefix);
}

export function renderSonarSourceCategory(
export function renderSonarSourceSecurityCategory(
standards: T.Standards,
category: string,
withPrefix = false

+ 22
- 0
server/sonar-web/src/main/js/helpers/security-standard.ts Dosyayı Görüntüle

@@ -0,0 +1,22 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 function getStandards(): Promise<T.Standards> {
return import('./standards.json').then(x => x.default);
}

+ 2
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties Dosyayı Görüntüle

@@ -743,6 +743,7 @@ issues.facet.mode.effort=Effort
issues.facet.standards=Standard
issues.facet.owaspTop10=OWASP Top 10
issues.facet.sansTop25=SANS Top 25
issues.facet.sonarsourceSecurity=SonarSource
issues.facet.cwe=CWE

#------------------------------------------------------------------------------
@@ -2092,7 +2093,7 @@ organizations_permissions.provisioning.desc=Ability to initialize a project so i
security_reports.more_rules=Additional security-related rules are available but not active in your profiles.
security_reports.owaspTop10.page=OWASP Top 10
security_reports.sansTop25.page=SANS Top 25
security_reports.sonarsourceSecurity.page=SonarSource Security
security_reports.sonarsourceSecurity.page=SonarSource
security_reports.owaspTop10.description=Track Vulnerabilities and Security Hotspots corresponding to OWASP Top 10 standard.
security_reports.sansTop25.description=Track Vulnerabilities and Security Hotspots corresponding to SANS Top 25 standard (25 CWE items in three categories).
security_reports.sonarsourceSecurity.description=Track Vulnerabilities and Security Hotspots corresponding to SonarSource Security categories.

Loading…
İptal
Kaydet