Bläddra i källkod

SONAR-16130,SONAR-16129 Add UI facet for OWASP 2021 Standards

tags/9.4.0.54424
Mathieu Suen 2 år sedan
förälder
incheckning
83e4a98dc6
27 ändrade filer med 415 tillägg och 80 borttagningar
  1. 91
    0
      server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
  2. 5
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
  3. 4
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx
  4. 7
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/App-test.tsx.snap
  5. 2
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap
  6. 5
    2
      server/sonar-web/src/main/js/apps/coding-rules/query.ts
  7. 59
    0
      server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
  8. 6
    2
      server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
  9. 2
    0
      server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx
  10. 2
    0
      server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesApp-test.tsx.snap
  11. 0
    55
      server/sonar-web/src/main/js/apps/issues/redirects.ts
  12. 4
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
  13. 83
    10
      server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
  14. 10
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx
  15. 12
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap
  16. 3
    0
      server/sonar-web/src/main/js/apps/issues/utils.ts
  17. 1
    0
      server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
  18. 1
    0
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap
  19. 22
    0
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap
  20. 4
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx
  21. 3
    0
      server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
  22. 25
    0
      server/sonar-web/src/main/js/helpers/__tests__/security-standard-test.ts
  23. 11
    0
      server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts
  24. 19
    3
      server/sonar-web/src/main/js/helpers/security-standard.ts
  25. 31
    7
      server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
  26. 1
    0
      server/sonar-web/src/main/js/types/security.ts
  27. 2
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 91
- 0
server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts Visa fil

@@ -0,0 +1,91 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 { cloneDeep } from 'lodash';
import { RequestData } from '../../helpers/request';
import { getStandards } from '../../helpers/security-standard';
import { mockPaging } from '../../helpers/testMocks';
import { RawFacet, RawIssuesResponse, ReferencedComponent } from '../../types/issues';
import { Standards } from '../../types/security';
import { searchIssues } from '../issues';

function mockReferenceComponent(override?: Partial<ReferencedComponent>) {
return {
key: 'component1',
name: 'Component1',
uuid: 'id1',
...override
};
}

export default class IssueServiceMock {
isAdmin = false;
standards?: Standards;

constructor() {
(searchIssues as jest.Mock).mockImplementation(this.listHandler);
}

reset() {
this.setIsAdmin(false);
}

async getStandards(): Promise<Standards> {
if (this.standards) {
return this.standards;
}
this.standards = await getStandards();
return this.standards;
}

owasp2021FacetList(): RawFacet {
return {
property: 'owaspTop10-2021',
values: [{ val: 'a1', count: 0 }]
};
}

setIsAdmin(isAdmin: boolean) {
this.isAdmin = isAdmin;
}

listHandler = (query: RequestData): Promise<RawIssuesResponse> => {
const facets = query.facets.split(',').map((name: string) => {
if (name === 'owaspTop10-2021') {
return this.owasp2021FacetList();
}
return {
property: name,
values: []
};
});
return this.reply({
components: [mockReferenceComponent()],
effortTotal: 199629,
facets,
issues: [],
languages: [],
paging: mockPaging()
});
};

reply<T>(response: T): Promise<T> {
return Promise.resolve(cloneDeep(response));
}
}

+ 5
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx Visa fil

@@ -108,6 +108,11 @@ export class App extends React.PureComponent<Props, State> {
openFacets: {
languages: true,
owaspTop10: shouldOpenStandardsChildFacet({}, query, SecurityStandard.OWASP_TOP10),
'owaspTop10-2021': shouldOpenStandardsChildFacet(
{},
query,
SecurityStandard.OWASP_TOP10_2021
),
sansTop25: shouldOpenStandardsChildFacet({}, query, SecurityStandard.SANS_TOP25),
sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet({}, query),
standards: shouldOpenStandardsFacet({}, query),

+ 4
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx Visa fil

@@ -110,6 +110,7 @@ export default function FacetsList(props: FacetsListProps) {
cweStats={props.facets && props.facets.cwe}
fetchingCwe={false}
fetchingOwaspTop10={false}
fetchingOwaspTop10-2021={false}
fetchingSansTop25={false}
fetchingSonarSourceSecurity={false}
onChange={props.onFilterChange}
@@ -118,6 +119,9 @@ export default function FacetsList(props: FacetsListProps) {
owaspTop10={props.query.owaspTop10}
owaspTop10Open={!!props.openFacets.owaspTop10}
owaspTop10Stats={props.facets && props.facets.owaspTop10}
owaspTop10-2021={props.query['owaspTop10-2021']}
owaspTop10-2021Open={!!props.openFacets['owaspTop10-2021']}
owaspTop10-2021Stats={props.facets && props.facets['owaspTop10-2021']}
query={props.query}
sansTop25={props.query.sansTop25}
sansTop25Open={!!props.openFacets.sansTop25}

+ 7
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/App-test.tsx.snap Visa fil

@@ -16,6 +16,7 @@ exports[`renderBulkButton should show bulk change button when user has edit righ
"inheritance": undefined,
"languages": Array [],
"owaspTop10": Array [],
"owaspTop10-2021": Array [],
"profile": undefined,
"repositories": Array [],
"ruleKey": undefined,
@@ -80,6 +81,7 @@ exports[`renderBulkButton should show bulk change button when user has global ad
"inheritance": undefined,
"languages": Array [],
"owaspTop10": Array [],
"owaspTop10-2021": Array [],
"profile": undefined,
"repositories": Array [],
"ruleKey": undefined,
@@ -139,6 +141,7 @@ exports[`should render correctly: loaded (ScreenPositionHelper) 1`] = `
Object {
"languages": true,
"owaspTop10": false,
"owaspTop10-2021": false,
"sansTop25": false,
"sonarsourceSecurity": false,
"standards": false,
@@ -155,6 +158,7 @@ exports[`should render correctly: loaded (ScreenPositionHelper) 1`] = `
"inheritance": undefined,
"languages": Array [],
"owaspTop10": Array [],
"owaspTop10-2021": Array [],
"profile": undefined,
"repositories": Array [],
"ruleKey": undefined,
@@ -230,6 +234,7 @@ exports[`should render correctly: loaded 1`] = `
"inheritance": undefined,
"languages": Array [],
"owaspTop10": Array [],
"owaspTop10-2021": Array [],
"profile": undefined,
"repositories": Array [],
"ruleKey": undefined,
@@ -434,6 +439,7 @@ exports[`should render correctly: open rule (ScreenPositionHelper) 1`] = `
Object {
"languages": true,
"owaspTop10": false,
"owaspTop10-2021": false,
"sansTop25": false,
"sonarsourceSecurity": false,
"standards": false,
@@ -450,6 +456,7 @@ exports[`should render correctly: open rule (ScreenPositionHelper) 1`] = `
"inheritance": undefined,
"languages": Array [],
"owaspTop10": Array [],
"owaspTop10-2021": Array [],
"profile": undefined,
"repositories": Array [],
"ruleKey": undefined,

+ 2
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap Visa fil

@@ -38,11 +38,13 @@ exports[`should render correctly 1`] = `
cweOpen={false}
fetchingCwe={false}
fetchingOwaspTop10={false}
fetchingOwaspTop10-2021={false}
fetchingSansTop25={false}
fetchingSonarSourceSecurity={false}
onChange={[MockFunction]}
onToggle={[MockFunction]}
open={false}
owaspTop10-2021Open={false}
owaspTop10Open={false}
query={Object {}}
sansTop25Open={false}

+ 5
- 2
server/sonar-web/src/main/js/apps/coding-rules/query.ts Visa fil

@@ -41,6 +41,7 @@ export interface Query {
inheritance: RuleInheritance | undefined;
languages: string[];
owaspTop10: string[];
'owaspTop10-2021': string[];
profile: string | undefined;
repositories: string[];
ruleKey: string | undefined;
@@ -85,6 +86,7 @@ export function parseQuery(query: RawQuery): Query {
inheritance: parseAsInheritance(query.inheritance),
languages: parseAsArray(query.languages, parseAsString),
owaspTop10: parseAsArray(query.owaspTop10, parseAsString),
'owaspTop10-2021': parseAsArray(query['owaspTop10-2021'], parseAsString),
profile: parseAsOptionalString(query.qprofile),
repositories: parseAsArray(query.repositories, parseAsString),
ruleKey: parseAsOptionalString(query.rule_key),
@@ -110,6 +112,7 @@ export function serializeQuery(query: Query): RawQuery {
is_template: serializeOptionalBoolean(query.template),
languages: serializeStringArray(query.languages),
owaspTop10: serializeStringArray(query.owaspTop10),
'owaspTop10-2021': serializeStringArray(query['owaspTop10-2021']),
q: serializeString(query.searchQuery),
qprofile: serializeString(query.profile),
repositories: serializeStringArray(query.repositories),
@@ -133,6 +136,7 @@ export function shouldRequestFacet(facet: string): facet is FacetKey {
'cwe',
'languages',
'owaspTop10',
'owaspTop10-2021',
'repositories',
'sansTop25',
'severities',
@@ -164,9 +168,8 @@ export function hasRuleKey(query: RawQuery) {
function parseAsInheritance(value?: string): RuleInheritance | undefined {
if (value === 'INHERITED' || value === 'NONE' || value === 'OVERRIDES') {
return value;
} else {
return undefined;
}
return undefined;
}

function serializeInheritance(value: RuleInheritance | undefined): string | undefined {

+ 59
- 0
server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx Visa fil

@@ -0,0 +1,59 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock';
import { renderOwaspTop102021Category } from '../../../helpers/security-standard';
import { renderComponentApp } from '../../../helpers/testReactTestingUtils';
import AppContainer from '../components/AppContainer';

jest.mock('../../../api/issues');

let handler: IssuesServiceMock;

beforeAll(() => {
handler = new IssuesServiceMock();
});

afterEach(() => handler.reset());

jest.setTimeout(10_000);

it('should support OWASP Top 10 version 2021', async () => {
const user = userEvent.setup();
renderIssueApp();
await user.click(await screen.findByRole('link', { name: 'issues.facet.standards' }));
const owaspTop102021 = screen.getByRole('link', { name: 'issues.facet.owaspTop10_2021' });
expect(owaspTop102021).toBeInTheDocument();

await user.click(owaspTop102021);
await Promise.all(
handler.owasp2021FacetList().values.map(async ({ val }) => {
const standard = await handler.getStandards();
/* eslint-disable-next-line testing-library/render-result-naming-convention */
const linkName = renderOwaspTop102021Category(standard, val);
expect(await screen.findByRole('link', { name: linkName })).toBeInTheDocument();
})
);
});

function renderIssueApp() {
renderComponentApp('project/issues', AppContainer);
}

+ 6
- 2
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx Visa fil

@@ -153,6 +153,11 @@ export default class App extends React.PureComponent<Props, State> {
myIssues: areMyIssuesSelected(props.location.query),
openFacets: {
owaspTop10: shouldOpenStandardsChildFacet({}, query, SecurityStandard.OWASP_TOP10),
'owaspTop10-2021': shouldOpenStandardsChildFacet(
{},
query,
SecurityStandard.OWASP_TOP10_2021
),
sansTop25: shouldOpenStandardsChildFacet({}, query, SecurityStandard.SANS_TOP25),
severities: true,
sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet({}, query),
@@ -406,7 +411,7 @@ export default class App extends React.PureComponent<Props, State> {

const facets = requestFacets
? Object.keys(openFacets)
.filter(facet => facet !== STANDARDS)
.filter(facet => facet !== STANDARDS && openFacets[facet])
.join(',')
: undefined;

@@ -432,7 +437,6 @@ export default class App extends React.PureComponent<Props, State> {
if (myIssues) {
Object.assign(parameters, { assignees: '__me__' });
}

return this.props.fetchIssues(parameters);
};


+ 2
- 0
server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx Visa fil

@@ -363,6 +363,7 @@ it('should display the right facets open', () => {
}).state('openFacets')
).toEqual({
owaspTop10: false,
'owaspTop10-2021': false,
sansTop25: false,
severities: true,
standards: false,
@@ -375,6 +376,7 @@ it('should display the right facets open', () => {
}).state('openFacets')
).toEqual({
owaspTop10: true,
'owaspTop10-2021': false,
sansTop25: false,
severities: true,
standards: true,

+ 2
- 0
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesApp-test.tsx.snap Visa fil

@@ -99,6 +99,7 @@ exports[`should show warnning when not all projects are accessible 1`] = `
openFacets={
Object {
"owaspTop10": false,
"owaspTop10-2021": false,
"sansTop25": false,
"severities": true,
"sonarsourceSecurity": false,
@@ -121,6 +122,7 @@ exports[`should show warnning when not all projects are accessible 1`] = `
"issues": Array [],
"languages": Array [],
"owaspTop10": Array [],
"owaspTop10-2021": Array [],
"projects": Array [],
"resolutions": Array [],
"resolved": true,

+ 0
- 55
server/sonar-web/src/main/js/apps/issues/redirects.ts Visa fil

@@ -1,55 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 { Location } from '../../helpers/urls';
import { RawQuery } from '../../types/types';
import { areMyIssuesSelected, parseQuery, serializeQuery } from './utils';

function parseHash(hash: string) {
const query: RawQuery = {};
const parts = hash.split('|');
parts.forEach(part => {
const tokens = part.split('=');
if (tokens.length === 2) {
const property = decodeURIComponent(tokens[0]);
const value = decodeURIComponent(tokens[1]);
if (property === 'assigned_to_me' && value === 'true') {
query.myIssues = 'true';
} else {
query[property] = value;
}
}
});
return query;
}

export function onEnter(state: any, replace: (location: Location) => void) {
const { hash } = window.location;
if (hash.length > 1) {
const query = parseHash(hash.substr(1));
const normalizedQuery = {
...serializeQuery(parseQuery(query)),
myIssues: areMyIssuesSelected(query) ? 'true' : undefined
};
replace({
pathname: state.location.pathname,
query: normalizedQuery
});
}
}

+ 4
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx Visa fil

@@ -180,6 +180,7 @@ export class Sidebar extends React.PureComponent<Props> {
cweStats={facets.cwe}
fetchingCwe={this.props.loadingFacets.cwe === true}
fetchingOwaspTop10={this.props.loadingFacets.owaspTop10 === true}
fetchingOwaspTop10-2021={this.props.loadingFacets['owaspTop10-2021'] === true}
fetchingSansTop25={this.props.loadingFacets.sansTop25 === true}
fetchingSonarSourceSecurity={this.props.loadingFacets.sonarsourceSecurity === true}
loadSearchResultCount={this.props.loadSearchResultCount}
@@ -189,6 +190,9 @@ export class Sidebar extends React.PureComponent<Props> {
owaspTop10={query.owaspTop10}
owaspTop10Open={!!openFacets.owaspTop10}
owaspTop10Stats={facets.owaspTop10}
owaspTop10-2021={query['owaspTop10-2021']}
owaspTop10-2021Open={!!openFacets['owaspTop10-2021']}
owaspTop10-2021Stats={facets['owaspTop10-2021']}
query={query}
sansTop25={query.sansTop25}
sansTop25Open={!!openFacets.sansTop25}

+ 83
- 10
server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx Visa fil

@@ -30,6 +30,7 @@ import { highlightTerm } from '../../../helpers/search';
import {
getStandards,
renderCWECategory,
renderOwaspTop102021Category,
renderOwaspTop10Category,
renderSansTop25Category,
renderSonarSourceSecurityCategory
@@ -45,6 +46,7 @@ interface Props {
cweStats: Dict<number> | undefined;
fetchingCwe: boolean;
fetchingOwaspTop10: boolean;
'fetchingOwaspTop10-2021': boolean;
fetchingSansTop25: boolean;
fetchingSonarSourceSecurity: boolean;
loadSearchResultCount?: (property: string, changes: Partial<Query>) => Promise<Facet>;
@@ -54,6 +56,9 @@ interface Props {
owaspTop10: string[];
owaspTop10Open: boolean;
owaspTop10Stats: Dict<number> | undefined;
'owaspTop10-2021': string[];
'owaspTop10-2021Open': boolean;
'owaspTop10-2021Stats': Dict<number> | undefined;
query: Partial<Query>;
sansTop25: string[];
sansTop25Open: boolean;
@@ -67,14 +72,25 @@ interface State {
standards: Standards;
}

type StatsProp = 'owaspTop10Stats' | 'cweStats' | 'sansTop25Stats' | 'sonarsourceSecurityStats';
type StatsProp =
| 'owaspTop10-2021Stats'
| 'owaspTop10Stats'
| 'cweStats'
| 'sansTop25Stats'
| 'sonarsourceSecurityStats';
type ValuesProp = StandardType;

export default class StandardFacet extends React.PureComponent<Props, State> {
mounted = false;
property = STANDARDS;
state: State = {
standards: { owaspTop10: {}, sansTop25: {}, cwe: {}, sonarsourceSecurity: {} }
standards: {
owaspTop10: {},
'owaspTop10-2021': {},
sansTop25: {},
cwe: {},
sonarsourceSecurity: {}
}
};

componentDidMount() {
@@ -84,6 +100,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
if (
this.props.open ||
this.props.owaspTop10.length > 0 ||
this.props['owaspTop10-2021'].length > 0 ||
this.props.cwe.length > 0 ||
this.props.sansTop25.length > 0 ||
this.props.sonarsourceSecurity.length > 0
@@ -104,9 +121,23 @@ export default class StandardFacet extends React.PureComponent<Props, State> {

loadStandards = () => {
getStandards().then(
({ owaspTop10, sansTop25, cwe, sonarsourceSecurity }: Standards) => {
({
'owaspTop10-2021': owaspTop102021,
owaspTop10,
sansTop25,
cwe,
sonarsourceSecurity
}: Standards) => {
if (this.mounted) {
this.setState({ standards: { owaspTop10, sansTop25, cwe, sonarsourceSecurity } });
this.setState({
standards: {
'owaspTop10-2021': owaspTop102021,
owaspTop10,
sansTop25,
cwe,
sonarsourceSecurity
}
});
}
},
() => {}
@@ -121,6 +152,9 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
...this.props.owaspTop10.map(item =>
renderOwaspTop10Category(this.state.standards, item, true)
),
...this.props['owaspTop10-2021'].map(item =>
renderOwaspTop102021Category(this.state.standards, item, true)
),
...this.props.sansTop25.map(item =>
renderSansTop25Category(this.state.standards, item, true)
),
@@ -136,6 +170,10 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
this.props.onToggle('owaspTop10');
};

handleOwaspTop102021HeaderClick = () => {
this.props.onToggle('owaspTop10-2021');
};

handleSansTop25HeaderClick = () => {
this.props.onToggle('sansTop25');
};
@@ -148,6 +186,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
this.props.onChange({
[this.property]: [],
owaspTop10: [],
'owaspTop10-2021': [],
sansTop25: [],
cwe: [],
sonarsourceSecurity: []
@@ -172,6 +211,10 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
this.handleItemClick(SecurityStandard.OWASP_TOP10, itemValue, multiple);
};

handleOwaspTop102021ItemClick = (itemValue: string, multiple: boolean) => {
this.handleItemClick(SecurityStandard.OWASP_TOP10_2021, itemValue, multiple);
};

handleSansTop25ItemClick = (itemValue: string, multiple: boolean) => {
this.handleItemClick(SecurityStandard.SANS_TOP25, itemValue, multiple);
};
@@ -265,8 +308,13 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
);
}

renderOwaspTop10Hint() {
return this.renderHint('owaspTop10Stats', SecurityStandard.OWASP_TOP10);
renderOwaspTop102021List() {
return this.renderList(
'owaspTop10-2021Stats',
SecurityStandard.OWASP_TOP10_2021,
renderOwaspTop102021Category,
this.handleOwaspTop102021ItemClick
);
}

renderSansTop25List() {
@@ -278,10 +326,6 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
);
}

renderSansTop25Hint() {
return this.renderHint('sansTop25Stats', SecurityStandard.SANS_TOP25);
}

renderSonarSourceSecurityList() {
return this.renderList(
'sonarsourceSecurityStats',
@@ -291,6 +335,18 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
);
}

renderOwaspTop10Hint() {
return this.renderHint('owaspTop10Stats', SecurityStandard.OWASP_TOP10);
}

renderOwaspTop102021Hint() {
return this.renderHint('owaspTop10-2021Stats', SecurityStandard.OWASP_TOP10_2021);
}

renderSansTop25Hint() {
return this.renderHint('sansTop25Stats', SecurityStandard.SANS_TOP25);
}

renderSonarSourceSecurityHint() {
return this.renderHint('sonarsourceSecurityStats', SecurityStandard.SONARSOURCE);
}
@@ -315,6 +371,23 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
</>
)}
</FacetBox>
<FacetBox className="is-inner" property={SecurityStandard.OWASP_TOP10_2021}>
<FacetHeader
fetching={this.props['fetchingOwaspTop10-2021']}
name={translate('issues.facet.owaspTop10_2021')}
onClick={this.handleOwaspTop102021HeaderClick}
open={this.props['owaspTop10-2021Open']}
values={this.props['owaspTop10-2021'].map(item =>
renderOwaspTop102021Category(this.state.standards, item)
)}
/>
{this.props['owaspTop10-2021Open'] && (
<>
{this.renderOwaspTop102021List()}
{this.renderOwaspTop102021Hint()}
</>
)}
</FacetBox>
<FacetBox className="is-inner" property={SecurityStandard.OWASP_TOP10}>
<FacetHeader
fetching={this.props.fetchingOwaspTop10}

+ 10
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx Visa fil

@@ -78,6 +78,7 @@ it('should clear standards facet', () => {
expect(onChange).toBeCalledWith({
cwe: [],
owaspTop10: [],
'owaspTop10-2021': [],
sansTop25: [],
sonarsourceSecurity: [],
standards: []
@@ -161,6 +162,7 @@ it('should display correct selection', () => {
const wrapper = shallowRender({
open: true,
owaspTop10: ['a1', 'a3'],
'owaspTop10-2021': ['a1', 'a2'],
sansTop25: ['risky-resource', 'foo'],
cwe: ['42', '1111', 'unknown'],
sonarsourceSecurity: ['sql-injection', 'others']
@@ -170,6 +172,8 @@ it('should display correct selection', () => {
'Others',
'OWASP A1 - a1 title',
'OWASP A3',
'OWASP A1 - a1 title',
'OWASP A2',
'SANS Risky Resource Management',
'SANS foo',
'CWE-42 - cwe-42 title',
@@ -177,6 +181,7 @@ it('should display correct selection', () => {
'Unknown CWE'
]);
checkValues('owaspTop10', ['A1 - a1 title', 'A3']);
checkValues('owaspTop10-2021', ['A1 - a1 title', 'A2']);
checkValues('sansTop25', ['Risky Resource Management', 'foo']);
checkValues('sonarsourceSecurity', ['SQL Injection', 'Others']);

@@ -198,6 +203,7 @@ function shallowRender(props: Partial<StandardFacet['props']> = {}) {
cweStats={{}}
fetchingCwe={false}
fetchingOwaspTop10={false}
fetchingOwaspTop10-2021={false}
fetchingSansTop25={false}
fetchingSonarSourceSecurity={false}
loadSearchResultCount={jest.fn()}
@@ -207,6 +213,9 @@ function shallowRender(props: Partial<StandardFacet['props']> = {}) {
owaspTop10={[]}
owaspTop10Open={false}
owaspTop10Stats={{}}
owaspTop10-2021={[]}
owaspTop10-2021Open={false}
owaspTop10-2021Stats={{}}
query={{} as Query}
sansTop25={[]}
sansTop25Open={false}
@@ -220,6 +229,7 @@ function shallowRender(props: Partial<StandardFacet['props']> = {}) {
wrapper.setState({
standards: {
owaspTop10: { a1: { title: 'a1 title' } },
'owaspTop10-2021': { a1: { title: 'a1 title' } },
sansTop25: { 'risky-resource': { title: 'Risky Resource Management' } },
cwe: { 42: { title: 'cwe-42 title' }, unknown: { title: 'Unknown CWE' } },
sonarsourceSecurity: {

+ 12
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap Visa fil

@@ -90,6 +90,18 @@ exports[`should render sub-facets 1`] = `
values={1}
/>
</FacetBox>
<FacetBox
className="is-inner"
property="owaspTop10-2021"
>
<FacetHeader
fetching={false}
name="issues.facet.owaspTop10_2021"
onClick={[Function]}
open={false}
values={Array []}
/>
</FacetBox>
<FacetBox
className="is-inner"
property="owaspTop10"

+ 3
- 0
server/sonar-web/src/main/js/apps/issues/utils.ts Visa fil

@@ -53,6 +53,7 @@ export interface Query {
issues: string[];
languages: string[];
owaspTop10: string[];
'owaspTop10-2021': string[];
projects: string[];
resolutions: string[];
resolved: boolean;
@@ -95,6 +96,7 @@ export function parseQuery(query: RawQuery): Query {
issues: parseAsArray(query.issues, parseAsString),
languages: parseAsArray(query.languages, parseAsString),
owaspTop10: parseAsArray(query.owaspTop10, parseAsString),
'owaspTop10-2021': parseAsArray(query['owaspTop10-2021'], parseAsString),
projects: parseAsArray(query.projects, parseAsString),
resolutions: parseAsArray(query.resolutions, parseAsString),
resolved: parseAsBoolean(query.resolved),
@@ -132,6 +134,7 @@ export function serializeQuery(query: Query): RawQuery {
issues: serializeStringArray(query.issues),
languages: serializeStringArray(query.languages),
owaspTop10: serializeStringArray(query.owaspTop10),
'owaspTop10-2021': serializeStringArray(query['owaspTop10-2021']),
projects: serializeStringArray(query.projects),
resolutions: serializeStringArray(query.resolutions),
resolved: query.resolved ? undefined : 'false',

+ 1
- 0
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx Visa fil

@@ -98,6 +98,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
selectedHotspot: undefined,
standards: {
[SecurityStandard.OWASP_TOP10]: {},
[SecurityStandard.OWASP_TOP10_2021]: {},
[SecurityStandard.SANS_TOP25]: {},
[SecurityStandard.SONARSOURCE]: {},
[SecurityStandard.CWE]: {}

+ 1
- 0
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap Visa fil

@@ -58,6 +58,7 @@ exports[`should render correctly 1`] = `
Object {
"cwe": Object {},
"owaspTop10": Object {},
"owaspTop10-2021": Object {},
"sansTop25": Object {},
"sonarsourceSecurity": Object {},
}

+ 22
- 0
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap Visa fil

@@ -157,6 +157,17 @@ exports[`should render correctly when filtered by category or cwe: category 1`]
"title": "Sensitive Data Exposure",
},
},
"owaspTop10-2021": Object {
"a1": Object {
"title": "Injection",
},
"a2": Object {
"title": "Broken Authentication",
},
"a3": Object {
"title": "Sensitive Data Exposure",
},
},
"sansTop25": Object {
"insecure-interaction": Object {
"title": "Insecure Interaction Between Components",
@@ -278,6 +289,17 @@ exports[`should render correctly when filtered by category or cwe: cwe 1`] = `
"title": "Sensitive Data Exposure",
},
},
"owaspTop10-2021": Object {
"a1": Object {
"title": "Injection",
},
"a2": Object {
"title": "Broken Authentication",
},
"a3": Object {
"title": "Sensitive Data Exposure",
},
},
"sansTop25": Object {
"insecure-interaction": Object {
"title": "Insecure Interaction Between Components",

+ 4
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx Visa fil

@@ -73,6 +73,10 @@ function shallowRender(props: Partial<HotspotSimpleListProps> = {}) {
a1: { title: 'A1 - SQL Injection' },
a3: { title: 'A3 - Sensitive Data Exposure' }
},
'owaspTop10-2021': {
a1: { title: 'A1 - SQL Injection' },
a3: { title: 'A3 - Sensitive Data Exposure' }
},
sansTop25: {},
sonarsourceSecurity: {}
}}

+ 3
- 0
server/sonar-web/src/main/js/apps/security-hotspots/utils.ts Visa fil

@@ -20,6 +20,7 @@
import { flatten, groupBy, sortBy } from 'lodash';
import {
renderCWECategory,
renderOwaspTop102021Category,
renderOwaspTop10Category,
renderSansTop25Category,
renderSonarSourceSecurityCategory
@@ -47,12 +48,14 @@ export const RISK_EXPOSURE_LEVELS = [RiskExposure.HIGH, RiskExposure.MEDIUM, Ris
export const SECURITY_STANDARDS = [
SecurityStandard.SONARSOURCE,
SecurityStandard.OWASP_TOP10,
SecurityStandard.OWASP_TOP10_2021,
SecurityStandard.SANS_TOP25,
SecurityStandard.CWE
];

export const SECURITY_STANDARD_RENDERER = {
[SecurityStandard.OWASP_TOP10]: renderOwaspTop10Category,
[SecurityStandard.OWASP_TOP10_2021]: renderOwaspTop102021Category,
[SecurityStandard.SANS_TOP25]: renderSansTop25Category,
[SecurityStandard.SONARSOURCE]: renderSonarSourceSecurityCategory,
[SecurityStandard.CWE]: renderCWECategory

+ 25
- 0
server/sonar-web/src/main/js/helpers/__tests__/security-standard-test.ts Visa fil

@@ -20,6 +20,7 @@
import { Standards } from '../../types/security';
import {
renderCWECategory,
renderOwaspTop102021Category,
renderOwaspTop10Category,
renderSansTop25Category,
renderSonarSourceSecurityCategory
@@ -36,6 +37,7 @@ describe('renderCWECategory', () => {
}
},
owaspTop10: {},
'owaspTop10-2021': {},
sansTop25: {},
sonarsourceSecurity: {}
};
@@ -56,6 +58,7 @@ describe('renderOwaspTop10Category', () => {
title: 'Injection'
}
},
'owaspTop10-2021': {},
sansTop25: {},
sonarsourceSecurity: {}
};
@@ -67,10 +70,31 @@ describe('renderOwaspTop10Category', () => {
});
});

describe('renderOwaspTop102021Category', () => {
const standards: Standards = {
cwe: {},
owaspTop10: {},
'owaspTop10-2021': {
a1: {
title: 'Injection'
}
},
sansTop25: {},
sonarsourceSecurity: {}
};
it('should render owasp categories correctly', () => {
expect(renderOwaspTop102021Category(standards, 'a1')).toEqual('A1 - Injection');
expect(renderOwaspTop102021Category(standards, 'a1', true)).toEqual('OWASP A1 - Injection');
expect(renderOwaspTop102021Category(standards, 'a2')).toEqual('A2');
expect(renderOwaspTop102021Category(standards, 'a2', true)).toEqual('OWASP A2');
});
});

describe('renderSansTop25Category', () => {
const standards: Standards = {
cwe: {},
owaspTop10: {},
'owaspTop10-2021': {},
sansTop25: {
'insecure-interaction': {
title: 'Insecure Interaction Between Components'
@@ -94,6 +118,7 @@ describe('renderSonarSourceSecurityCategory', () => {
const standards: Standards = {
cwe: {},
owaspTop10: {},
'owaspTop10-2021': {},
sansTop25: {},
sonarsourceSecurity: {
xss: {

+ 11
- 0
server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts Visa fil

@@ -154,6 +154,17 @@ export function mockStandards(): Standards {
title: 'Sensitive Data Exposure'
}
},
'owaspTop10-2021': {
a1: {
title: 'Injection'
},
a2: {
title: 'Broken Authentication'
},
a3: {
title: 'Sensitive Data Exposure'
}
},
sansTop25: {
'insecure-interaction': {
title: 'Insecure Interaction Between Components'

+ 19
- 3
server/sonar-web/src/main/js/helpers/security-standard.ts Visa fil

@@ -39,12 +39,28 @@ export function renderOwaspTop10Category(
category: string,
withPrefix = false
): string {
const record = standards.owaspTop10[category];
return renderOwaspCategory('owaspTop10', standards, category, withPrefix);
}

export function renderOwaspTop102021Category(
standards: Standards,
category: string,
withPrefix = false
): string {
return renderOwaspCategory('owaspTop10-2021', standards, category, withPrefix);
}

function renderOwaspCategory(
type: 'owaspTop10' | 'owaspTop10-2021',
standards: Standards,
category: string,
withPrefix: boolean
) {
const record = standards[type][category];
if (!record) {
return addPrefix(category.toUpperCase(), 'OWASP', withPrefix);
} else {
return addPrefix(`${category.toUpperCase()} - ${record.title}`, 'OWASP', withPrefix);
}
return addPrefix(`${category.toUpperCase()} - ${record.title}`, 'OWASP', withPrefix);
}

export function renderSansTop25Category(

+ 31
- 7
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx Visa fil

@@ -23,29 +23,53 @@ import * as React from 'react';
import { HelmetProvider } from 'react-helmet-async';
import { IntlProvider } from 'react-intl';
import { Provider } from 'react-redux';
import { createMemoryHistory, RouteConfig, Router } from 'react-router';
import { createMemoryHistory, Route, RouteComponent, RouteConfig, Router } from 'react-router';
import { Store } from 'redux';
import AppStateContextProvider from '../app/components/app-state/AppStateContextProvider';
import CurrentUserContextProvider from '../app/components/current-user/CurrentUserContextProvider';
import { MetricsContext } from '../app/components/metrics/MetricsContext';
import getStore from '../app/utils/getStore';
import { RouteWithChildRoutes } from '../app/utils/startReactApp';
import { Store as State } from '../store/rootReducer';
import { AppState } from '../types/appstate';
import { Dict, Metric } from '../types/types';
import { CurrentUser } from '../types/users';
import { DEFAULT_METRICS } from './mocks/metrics';
import { mockAppState } from './testMocks';
import { mockAppState, mockCurrentUser } from './testMocks';

interface RenderContext {
metrics?: Dict<Metric>;
store?: Store<State, any>;
history?: History;
appState?: AppState;
currentUser?: CurrentUser;
}

export function renderComponentApp(
indexPath: string,
component: RouteComponent,
context: RenderContext = {}
): RenderResult {
return renderRoutedApp(<Route path={indexPath} component={component} />, indexPath, context);
}

export function renderApp(
indexPath: string,
routes: RouteConfig,
context: RenderContext
): RenderResult {
return renderRoutedApp(
<RouteWithChildRoutes path={indexPath} childRoutes={routes} />,
indexPath,
context
);
}

function renderRoutedApp(
children: React.ReactElement,
indexPath: string,
{
currentUser = mockCurrentUser(),
metrics = DEFAULT_METRICS,
store = getStore(),
appState = mockAppState(),
@@ -58,11 +82,11 @@ export function renderApp(
<IntlProvider defaultLocale="en" locale="en">
<MetricsContext.Provider value={metrics}>
<Provider store={store}>
<AppStateContextProvider appState={appState}>
<Router history={history}>
<RouteWithChildRoutes path={indexPath} childRoutes={routes} />
</Router>
</AppStateContextProvider>
<CurrentUserContextProvider currentUser={currentUser}>
<AppStateContextProvider appState={appState}>
<Router history={history}>{children}</Router>
</AppStateContextProvider>
</CurrentUserContextProvider>
</Provider>
</MetricsContext.Provider>
</IntlProvider>

+ 1
- 0
server/sonar-web/src/main/js/types/security.ts Visa fil

@@ -20,6 +20,7 @@
import { Dict } from './types';

export enum SecurityStandard {
OWASP_TOP10_2021 = 'owaspTop10-2021',
OWASP_TOP10 = 'owaspTop10',
SANS_TOP25 = 'sansTop25',
SONARSOURCE = 'sonarsourceSecurity',

+ 2
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties Visa fil

@@ -966,7 +966,8 @@ issues.facet.mode=Display Mode
issues.facet.mode.count=Issues
issues.facet.mode.effort=Effort
issues.facet.standards=Security Category
issues.facet.owaspTop10=OWASP Top 10
issues.facet.owaspTop10=OWASP Top 10 2017
issues.facet.owaspTop10_2021=OWASP Top 10 2021
issues.facet.sansTop25=SANS Top 25
issues.facet.sonarsourceSecurity=SonarSource
issues.facet.cwe=CWE

Laddar…
Avbryt
Spara