diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2022-03-15 11:04:15 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-03-17 20:03:09 +0000 |
commit | 83e4a98dc6c44840d1953b8ac76e52bdd4a81dd5 (patch) | |
tree | f8a6da24987c1d64392c0c678e6938d2321aea35 | |
parent | 4cd2466ed408d5664c88cc05ba6fd8a99f59d055 (diff) | |
download | sonarqube-83e4a98dc6c44840d1953b8ac76e52bdd4a81dd5.tar.gz sonarqube-83e4a98dc6c44840d1953b8ac76e52bdd4a81dd5.zip |
SONAR-16130,SONAR-16129 Add UI facet for OWASP 2021 Standards
27 files changed, 415 insertions, 80 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts new file mode 100644 index 00000000000..6c3ef1cd3bd --- /dev/null +++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts @@ -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)); + } +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx index 53a3f6da600..a9a5d6604c9 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx @@ -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), diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx index c08e02cc29e..3b7578e05e3 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/App-test.tsx.snap index 4d1707ff0da..cb55971f54b 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/App-test.tsx.snap @@ -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, diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap index 7a490ced59c..809ba918cf2 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap @@ -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} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/query.ts b/server/sonar-web/src/main/js/apps/coding-rules/query.ts index bd10f04faf9..ddbe9a742bd 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/query.ts +++ b/server/sonar-web/src/main/js/apps/coding-rules/query.ts @@ -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 { diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx new file mode 100644 index 00000000000..c703fd77313 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx @@ -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); +} diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx index 848b4da99fe..92db8e0acc8 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx @@ -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); }; diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx index 610b1b8e19f..0e4ed2e7a22 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx @@ -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, diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesApp-test.tsx.snap index 073c704f38a..5af18892050 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesApp-test.tsx.snap @@ -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, diff --git a/server/sonar-web/src/main/js/apps/issues/redirects.ts b/server/sonar-web/src/main/js/apps/issues/redirects.ts deleted file mode 100644 index 28dc59a5e6b..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/redirects.ts +++ /dev/null @@ -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 - }); - } -} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx index e15af101eac..dc69ae3909a 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx index e0eb8d46d25..6f1debef576 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx index 2893c038f93..a2b9e7cd062 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx @@ -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: { diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap index a38caf5b816..935c38c00b7 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap @@ -92,6 +92,18 @@ exports[`should render sub-facets 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" > <FacetHeader diff --git a/server/sonar-web/src/main/js/apps/issues/utils.ts b/server/sonar-web/src/main/js/apps/issues/utils.ts index 7b478ec8559..4ce712a7825 100644 --- a/server/sonar-web/src/main/js/apps/issues/utils.ts +++ b/server/sonar-web/src/main/js/apps/issues/utils.ts @@ -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', diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx index 50fa7a08089..f8a2a7dbe5f 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx @@ -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]: {} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap index 1762e1a557e..2ab1904234a 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap @@ -58,6 +58,7 @@ exports[`should render correctly 1`] = ` Object { "cwe": Object {}, "owaspTop10": Object {}, + "owaspTop10-2021": Object {}, "sansTop25": Object {}, "sonarsourceSecurity": Object {}, } diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap index fa7b43b13fa..6012cd9ffa7 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap @@ -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", diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx index a74de8f1277..3629750ec3f 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx @@ -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: {} }} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts b/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts index ca95c095990..b0275f40751 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts +++ b/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts @@ -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 diff --git a/server/sonar-web/src/main/js/helpers/__tests__/security-standard-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/security-standard-test.ts index 38e50e00c32..1d9c3de6667 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/security-standard-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/security-standard-test.ts @@ -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: { diff --git a/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts b/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts index d07d65d739b..d7fb2e6df26 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts @@ -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' diff --git a/server/sonar-web/src/main/js/helpers/security-standard.ts b/server/sonar-web/src/main/js/helpers/security-standard.ts index 5e17ac8d56e..7a8b78a8ac8 100644 --- a/server/sonar-web/src/main/js/helpers/security-standard.ts +++ b/server/sonar-web/src/main/js/helpers/security-standard.ts @@ -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( diff --git a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx index c1a3ec926e3..92025a107c2 100644 --- a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx +++ b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/types/security.ts b/server/sonar-web/src/main/js/types/security.ts index aef87359d70..6bec38d0265 100644 --- a/server/sonar-web/src/main/js/types/security.ts +++ b/server/sonar-web/src/main/js/types/security.ts @@ -20,6 +20,7 @@ import { Dict } from './types'; export enum SecurityStandard { + OWASP_TOP10_2021 = 'owaspTop10-2021', OWASP_TOP10 = 'owaspTop10', SANS_TOP25 = 'sansTop25', SONARSOURCE = 'sonarsourceSecurity', diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 97f508d25c6..52f3b72c70c 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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 |