]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16130,SONAR-16129 Add UI facet for OWASP 2021 Standards
authorMathieu Suen <mathieu.suen@sonarsource.com>
Tue, 15 Mar 2022 10:04:15 +0000 (11:04 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 17 Mar 2022 20:03:09 +0000 (20:03 +0000)
27 files changed:
server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/FacetsList.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/App-test.tsx.snap
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/FacetsList-test.tsx.snap
server/sonar-web/src/main/js/apps/coding-rules/query.ts
server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesApp-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/redirects.ts [deleted file]
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/utils.ts
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsAppRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx
server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
server/sonar-web/src/main/js/helpers/__tests__/security-standard-test.ts
server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts
server/sonar-web/src/main/js/helpers/security-standard.ts
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
server/sonar-web/src/main/js/types/security.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

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 (file)
index 0000000..6c3ef1c
--- /dev/null
@@ -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));
+  }
+}
index 53a3f6da6000cc98dfdb745bfcb8f0008ffaa256..a9a5d6604c9ffd321acb42e17c9d7332ad13dbb9 100644 (file)
@@ -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),
index c08e02cc29e55252c1b2344c84195015c14cee32..3b7578e05e3c70dff03405403d84524f6beb3406 100644 (file)
@@ -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}
index 4d1707ff0da90ae249f5d5b593634e43d5914d68..cb55971f54be2b346dd4dc8c3cd19f5f7987c16c 100644 (file)
@@ -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,
index 7a490ced59c7e842e58a3f069e980225416f426d..809ba918cf29a20e7b85b757202ef1114a01df10 100644 (file)
@@ -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}
index bd10f04faf933479d28d141071cfe9c34fd30e35..ddbe9a742bd0f3cb95a4a458a171b6a6397fb863 100644 (file)
@@ -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 (file)
index 0000000..c703fd7
--- /dev/null
@@ -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);
+}
index 848b4da99fe08782f63874eedad856b82d811478..92db8e0acc80c083d1b74815717ed404a51a33ab 100644 (file)
@@ -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);
   };
 
index 610b1b8e19f4ad0eac4d745be2b1af7174ca1a00..0e4ed2e7a22f7d2b18cad049ff86e588bdf9cb7c 100644 (file)
@@ -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,
index 073c704f38a0b5e361dd56d6a361ea60da825fdc..5af18892050ad4fe84e19cca22419cf7c8a2f3e4 100644 (file)
@@ -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 (file)
index 28dc59a..0000000
+++ /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
-    });
-  }
-}
index e15af101eacf23456b71d884172ce13afec0283d..dc69ae3909af3add4c858c6e06f3eb6ae700fdcc 100644 (file)
@@ -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}
index e0eb8d46d25c1f00e7898c05c62c87becbd69acf..6f1debef57639b75603c98263deef9c9d9c0c953 100644 (file)
@@ -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}
index 2893c038f934cb9fc889a423538a7a1cc54e490a..a2b9e7cd062b9ebb555269412c8390ecedb0b892 100644 (file)
@@ -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: {
index a38caf5b8161216be229d6f3800e1ad8970787ce..935c38c00b723a64def5cdaa34ec5c9fc3f85dfc 100644 (file)
@@ -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"
index 7b478ec85597505b0f745f8607c4ca9d540e7599..4ce712a782592c886780e7a7ca290dcd1850809c 100644 (file)
@@ -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',
index 50fa7a08089c521ff5cff4d7a11c8cae297ab1e1..f8a2a7dbe5fb78873d712292df24305a47f2df8e 100644 (file)
@@ -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]: {}
index 1762e1a557e01ef077607160e94a4a54de76b2bf..2ab1904234a9fd30fca0320d66a3b606dd7f1273 100644 (file)
@@ -58,6 +58,7 @@ exports[`should render correctly 1`] = `
     Object {
       "cwe": Object {},
       "owaspTop10": Object {},
+      "owaspTop10-2021": Object {},
       "sansTop25": Object {},
       "sonarsourceSecurity": Object {},
     }
index fa7b43b13fa117a479b6147821606aa132a625b2..6012cd9ffa7410eafdab0ca66d92d8d78e65eb42 100644 (file)
@@ -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",
index a74de8f1277ff63e7771478a1df9679f66286040..3629750ec3ffd185054f5ba07303e349d2cea28e 100644 (file)
@@ -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: {}
       }}
index ca95c09599074f6e82c32c9378c943ce0759017d..b0275f40751ea4e3b75eccfa52b970eb3f41741a 100644 (file)
@@ -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
index 38e50e00c3208ae0663352b57c59fe6419c038ad..1d9c3de666796f04775c162b978958d8435745eb 100644 (file)
@@ -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: {
index d07d65d739b4607d72ea2cfefd5eb3128b78de2c..d7fb2e6df2670344fcbd1ef8aa2c755a5de6b30b 100644 (file)
@@ -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'
index 5e17ac8d56e84ee21a1c2cd3700742db0385e418..7a8b78a8ac810a4968aa20595ac2488c7b12e6e7 100644 (file)
@@ -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(
index c1a3ec926e3cb9e75fdcac9430f08eca7db37d0d..92025a107c21351e32b20df0e867546a0bfb9d6a 100644 (file)
@@ -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>
index aef87359d70f6ad86bed1f593ba301051b3dbca6..6bec38d0265cd305a9775134154570130b854f0e 100644 (file)
@@ -20,6 +20,7 @@
 import { Dict } from './types';
 
 export enum SecurityStandard {
+  OWASP_TOP10_2021 = 'owaspTop10-2021',
   OWASP_TOP10 = 'owaspTop10',
   SANS_TOP25 = 'sansTop25',
   SONARSOURCE = 'sonarsourceSecurity',
index 97f508d25c62db6bc0ac4fafa08bdbefce473823..52f3b72c70c49e08c30ed2c7a3e0f631b5d61a10 100644 (file)
@@ -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