]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13566 Display hotspots of a specific category
authorJeremy Davis <jeremy.davis@sonarsource.com>
Mon, 5 Oct 2020 16:05:41 +0000 (18:05 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 7 Oct 2020 20:07:44 +0000 (20:07 +0000)
24 files changed:
server/sonar-web/src/main/js/api/security-hotspots.ts
server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/issues/components/App.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
server/sonar-web/src/main/js/apps/issues/utils.ts
server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.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/components/HotspotCategory.tsx
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/security-hotspots/styles.css
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/urls.ts
server/sonar-web/src/main/js/types/security.ts [new file with mode: 0644]
server/sonar-web/src/main/js/types/types.d.ts

index f6ccad30b570e492762ca31878dbec4d0ff14686..1dc1feb2fea41295c82b70b44f669898a3c7af41 100644 (file)
@@ -30,6 +30,8 @@ import {
   HotspotStatus
 } from '../types/security-hotspots';
 
+const HOTSPOTS_SEARCH_URL = '/api/hotspots/search';
+
 export function assignSecurityHotspot(
   hotspotKey: string,
   data: HotspotAssignRequest
@@ -76,7 +78,7 @@ export function getSecurityHotspots(
     sinceLeakPeriod?: boolean;
   } & BranchParameters
 ): Promise<HotspotSearchResponse> {
-  return getJSON('/api/hotspots/search', data).catch(throwGlobalError);
+  return getJSON(HOTSPOTS_SEARCH_URL, data).catch(throwGlobalError);
 }
 
 export function getSecurityHotspotList(
@@ -85,7 +87,7 @@ export function getSecurityHotspotList(
     projectKey: string;
   } & BranchParameters
 ): Promise<HotspotSearchResponse> {
-  return getJSON('/api/hotspots/search', { ...data, hotspots: hotspotKeys.join() }).catch(
+  return getJSON(HOTSPOTS_SEARCH_URL, { ...data, hotspots: hotspotKeys.join() }).catch(
     throwGlobalError
   );
 }
index 967f142878e93e9af596093d395d96fd12b842d1..78c1e827e91e17befd03f9f127eff2c32be53fa1 100644 (file)
@@ -50,6 +50,7 @@ import {
   getMyOrganizations,
   Store
 } from '../../../store/rootReducer';
+import { SecurityStandard } from '../../../types/security';
 import {
   shouldOpenSonarSourceSecurityFacet,
   shouldOpenStandardsChildFacet,
@@ -121,8 +122,8 @@ export class App extends React.PureComponent<Props, State> {
       loading: true,
       openFacets: {
         languages: true,
-        owaspTop10: shouldOpenStandardsChildFacet({}, query, 'owaspTop10'),
-        sansTop25: shouldOpenStandardsChildFacet({}, query, 'sansTop25'),
+        owaspTop10: shouldOpenStandardsChildFacet({}, query, SecurityStandard.OWASP_TOP10),
+        sansTop25: shouldOpenStandardsChildFacet({}, query, SecurityStandard.SANS_TOP25),
         sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet({}, query),
         standards: shouldOpenStandardsFacet({}, query),
         types: true
index a88f0031c2387fc8cd3467a5a32074544a3d51d6..5d49a357089203871d3f1932a48144da1ff3b33a 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { scrollToElement } from 'sonar-ui-common/helpers/scrolling';
+import { SecurityStandard } from '../../../types/security';
 import {
   scrollToIssue,
   shouldOpenSonarSourceSecurityFacet,
@@ -80,37 +81,57 @@ describe('shouldOpenStandardsFacet', () => {
 
 describe('shouldOpenStandardsChildFacet', () => {
   it('should open standard child facet', () => {
-    expect(shouldOpenStandardsChildFacet({ owaspTop10: true }, {}, 'owaspTop10')).toBe(true);
-    expect(shouldOpenStandardsChildFacet({ sansTop25: true }, {}, 'sansTop25')).toBe(true);
     expect(
-      shouldOpenStandardsChildFacet({ sansTop25: true }, { owaspTop10: ['A1'] }, 'owaspTop10')
+      shouldOpenStandardsChildFacet({ owaspTop10: true }, {}, SecurityStandard.OWASP_TOP10)
     ).toBe(true);
     expect(
-      shouldOpenStandardsChildFacet({ owaspTop10: false }, { owaspTop10: ['A1'] }, 'owaspTop10')
+      shouldOpenStandardsChildFacet({ sansTop25: true }, {}, SecurityStandard.SANS_TOP25)
     ).toBe(true);
     expect(
-      shouldOpenStandardsChildFacet({}, { sansTop25: ['insecure-interactions'] }, 'sansTop25')
+      shouldOpenStandardsChildFacet(
+        { sansTop25: true },
+        { owaspTop10: ['A1'] },
+        SecurityStandard.OWASP_TOP10
+      )
+    ).toBe(true);
+    expect(
+      shouldOpenStandardsChildFacet(
+        { owaspTop10: false },
+        { owaspTop10: ['A1'] },
+        SecurityStandard.OWASP_TOP10
+      )
+    ).toBe(true);
+    expect(
+      shouldOpenStandardsChildFacet(
+        {},
+        { sansTop25: ['insecure-interactions'] },
+        SecurityStandard.SANS_TOP25
+      )
     ).toBe(true);
     expect(
       shouldOpenStandardsChildFacet(
         {},
         { sansTop25: ['insecure-interactions'], sonarsourceSecurity: ['sql-injection'] },
-        'sonarsourceSecurity'
+        SecurityStandard.SONARSOURCE
       )
     ).toBe(true);
   });
 
   it('should NOT open standard child facet', () => {
-    expect(shouldOpenStandardsChildFacet({ standards: true }, {}, 'owaspTop10')).toBe(false);
-    expect(shouldOpenStandardsChildFacet({ sansTop25: true }, {}, 'owaspTop10')).toBe(false);
-    expect(shouldOpenStandardsChildFacet({}, { types: ['VULNERABILITY'] }, 'sansTop25')).toBe(
-      false
-    );
+    expect(
+      shouldOpenStandardsChildFacet({ standards: true }, {}, SecurityStandard.OWASP_TOP10)
+    ).toBe(false);
+    expect(
+      shouldOpenStandardsChildFacet({ sansTop25: true }, {}, SecurityStandard.OWASP_TOP10)
+    ).toBe(false);
+    expect(
+      shouldOpenStandardsChildFacet({}, { types: ['VULNERABILITY'] }, SecurityStandard.SANS_TOP25)
+    ).toBe(false);
     expect(
       shouldOpenStandardsChildFacet(
         {},
         { sansTop25: ['insecure-interactions'], sonarsourceSecurity: ['sql-injection'] },
-        'owaspTop10'
+        SecurityStandard.OWASP_TOP10
       )
     ).toBe(false);
   });
index a86834fbc1d983535532e4fb69ae72694824976b..0c8163a83614209791ae8944776847aa628ee597 100644 (file)
@@ -50,6 +50,7 @@ import {
 } from '../../../helpers/branch-like';
 import { isSonarCloud } from '../../../helpers/system';
 import { BranchLike } from '../../../types/branch-like';
+import { SecurityStandard } from '../../../types/security';
 import * as actions from '../actions';
 import ConciseIssuesList from '../conciseIssuesList/ConciseIssuesList';
 import ConciseIssuesListHeader from '../conciseIssuesList/ConciseIssuesListHeader';
@@ -156,8 +157,8 @@ export default class App extends React.PureComponent<Props, State> {
       locationsNavigator: false,
       myIssues: areMyIssuesSelected(props.location.query),
       openFacets: {
-        owaspTop10: shouldOpenStandardsChildFacet({}, query, 'owaspTop10'),
-        sansTop25: shouldOpenStandardsChildFacet({}, query, 'sansTop25'),
+        owaspTop10: shouldOpenStandardsChildFacet({}, query, SecurityStandard.OWASP_TOP10),
+        sansTop25: shouldOpenStandardsChildFacet({}, query, SecurityStandard.SANS_TOP25),
         severities: true,
         sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet({}, query),
         standards: shouldOpenStandardsFacet({}, query),
index f43048c2a426f905cd3ac0e5e7379b6cf7d41cfa..541989aabf190787c84933c3e1ca6d8d2df7f6ad 100644 (file)
@@ -34,6 +34,7 @@ import {
   renderSansTop25Category,
   renderSonarSourceSecurityCategory
 } from '../../../helpers/security-standard';
+import { SecurityStandard, Standards, StandardType } from '../../../types/security';
 import { Facet, formatFacetStat, Query, STANDARDS } from '../utils';
 
 interface Props {
@@ -61,11 +62,11 @@ interface Props {
 }
 
 interface State {
-  standards: T.Standards;
+  standards: Standards;
 }
 
 type StatsProp = 'owaspTop10Stats' | 'cweStats' | 'sansTop25Stats' | 'sonarsourceSecurityStats';
-type ValuesProp = T.StandardType;
+type ValuesProp = StandardType;
 
 export default class StandardFacet extends React.PureComponent<Props, State> {
   mounted = false;
@@ -101,7 +102,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
 
   loadStandards = () => {
     getStandards().then(
-      ({ owaspTop10, sansTop25, cwe, sonarsourceSecurity }: T.Standards) => {
+      ({ owaspTop10, sansTop25, cwe, sonarsourceSecurity }: Standards) => {
         if (this.mounted) {
           this.setState({ standards: { owaspTop10, sansTop25, cwe, sonarsourceSecurity } });
         }
@@ -166,15 +167,15 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
   };
 
   handleOwaspTop10ItemClick = (itemValue: string, multiple: boolean) => {
-    this.handleItemClick('owaspTop10', itemValue, multiple);
+    this.handleItemClick(SecurityStandard.OWASP_TOP10, itemValue, multiple);
   };
 
   handleSansTop25ItemClick = (itemValue: string, multiple: boolean) => {
-    this.handleItemClick('sansTop25', itemValue, multiple);
+    this.handleItemClick(SecurityStandard.SANS_TOP25, itemValue, multiple);
   };
 
   handleSonarSourceSecurityItemClick = (itemValue: string, multiple: boolean) => {
-    this.handleItemClick('sonarsourceSecurity', itemValue, multiple);
+    this.handleItemClick(SecurityStandard.SONARSOURCE, itemValue, multiple);
   };
 
   handleCWESearch = (query: string) => {
@@ -197,7 +198,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
   renderList = (
     statsProp: StatsProp,
     valuesProp: ValuesProp,
-    renderName: (standards: T.Standards, category: string) => string,
+    renderName: (standards: Standards, category: string) => string,
     onClick: (x: string, multiple?: boolean) => void
   ) => {
     const stats = this.props[statsProp];
@@ -214,8 +215,8 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
     stats: any,
     values: string[],
     categories: string[],
-    renderName: (standards: T.Standards, category: string) => React.ReactNode,
-    renderTooltip: (standards: T.Standards, category: string) => string,
+    renderName: (standards: Standards, category: string) => React.ReactNode,
+    renderTooltip: (standards: Standards, category: string) => string,
     onClick: (x: string, multiple?: boolean) => void
   ) => {
     if (!categories.length) {
@@ -256,46 +257,46 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
   renderOwaspTop10List() {
     return this.renderList(
       'owaspTop10Stats',
-      'owaspTop10',
+      SecurityStandard.OWASP_TOP10,
       renderOwaspTop10Category,
       this.handleOwaspTop10ItemClick
     );
   }
 
   renderOwaspTop10Hint() {
-    return this.renderHint('owaspTop10Stats', 'owaspTop10');
+    return this.renderHint('owaspTop10Stats', SecurityStandard.OWASP_TOP10);
   }
 
   renderSansTop25List() {
     return this.renderList(
       'sansTop25Stats',
-      'sansTop25',
+      SecurityStandard.SANS_TOP25,
       renderSansTop25Category,
       this.handleSansTop25ItemClick
     );
   }
 
   renderSansTop25Hint() {
-    return this.renderHint('sansTop25Stats', 'sansTop25');
+    return this.renderHint('sansTop25Stats', SecurityStandard.SANS_TOP25);
   }
 
   renderSonarSourceSecurityList() {
     return this.renderList(
       'sonarsourceSecurityStats',
-      'sonarsourceSecurity',
+      SecurityStandard.SONARSOURCE,
       renderSonarSourceSecurityCategory,
       this.handleSonarSourceSecurityItemClick
     );
   }
 
   renderSonarSourceSecurityHint() {
-    return this.renderHint('sonarsourceSecurityStats', 'sonarsourceSecurity');
+    return this.renderHint('sonarsourceSecurityStats', SecurityStandard.SONARSOURCE);
   }
 
   renderSubFacets() {
     return (
       <>
-        <FacetBox className="is-inner" property="sonarsourceSecurity">
+        <FacetBox className="is-inner" property={SecurityStandard.SONARSOURCE}>
           <FacetHeader
             fetching={this.props.fetchingSonarSourceSecurity}
             name={translate('issues.facet.sonarsourceSecurity')}
@@ -312,7 +313,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
             </>
           )}
         </FacetBox>
-        <FacetBox className="is-inner" property="owaspTop10">
+        <FacetBox className="is-inner" property={SecurityStandard.OWASP_TOP10}>
           <FacetHeader
             fetching={this.props.fetchingOwaspTop10}
             name={translate('issues.facet.owaspTop10')}
@@ -329,7 +330,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
             </>
           )}
         </FacetBox>
-        <FacetBox className="is-inner" property="sansTop25">
+        <FacetBox className="is-inner" property={SecurityStandard.SANS_TOP25}>
           <FacetHeader
             fetching={this.props.fetchingSansTop25}
             name={translate('issues.facet.sansTop25')}
@@ -358,7 +359,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
           onSearch={this.handleCWESearch}
           onToggle={this.props.onToggle}
           open={this.props.cweOpen}
-          property="cwe"
+          property={SecurityStandard.CWE}
           query={omit(this.props.query, 'cwe')}
           renderFacetItem={item => renderCWECategory(this.state.standards, item)}
           renderSearchResult={(item, query) =>
index 8dc99ee6329ebe63dfaebb32d53b62fbea33c4a9..8e6807d394b57cc49f14693983b4c7504d06c005 100644 (file)
@@ -33,6 +33,7 @@ import { scrollToElement } from 'sonar-ui-common/helpers/scrolling';
 import { get, save } from 'sonar-ui-common/helpers/storage';
 import { searchMembers } from '../../api/organizations';
 import { searchUsers } from '../../api/users';
+import { SecurityStandard, StandardType } from '../../types/security';
 
 export interface Query {
   assigned: boolean;
@@ -65,11 +66,11 @@ export interface Query {
 }
 
 export const STANDARDS = 'standards';
-export const STANDARD_TYPES: T.StandardType[] = [
-  'owaspTop10',
-  'sansTop25',
-  'cwe',
-  'sonarsourceSecurity'
+export const STANDARD_TYPES: StandardType[] = [
+  SecurityStandard.OWASP_TOP10,
+  SecurityStandard.SANS_TOP25,
+  SecurityStandard.CWE,
+  SecurityStandard.SONARSOURCE
 ];
 
 // allow sorting by CREATION_DATE only
@@ -288,13 +289,13 @@ export function shouldOpenStandardsFacet(
 export function shouldOpenStandardsChildFacet(
   openFacets: T.Dict<boolean>,
   query: Partial<Query>,
-  standardType: T.StandardType
+  standardType: SecurityStandard
 ): boolean {
   const filter = query[standardType];
   return (
     openFacets[STANDARDS] !== false &&
     (openFacets[standardType] ||
-      (standardType !== 'cwe' && filter !== undefined && filter.length > 0))
+      (standardType !== SecurityStandard.CWE && filter !== undefined && filter.length > 0))
   );
 }
 
@@ -304,7 +305,7 @@ export function shouldOpenSonarSourceSecurityFacet(
 ): boolean {
   // Open it by default if the parent is open, and no other standard is open.
   return (
-    shouldOpenStandardsChildFacet(openFacets, query, 'sonarsourceSecurity') ||
+    shouldOpenStandardsChildFacet(openFacets, query, SecurityStandard.SONARSOURCE) ||
     (shouldOpenStandardsFacet(openFacets, query) && !isOneStandardChildFacetOpen(openFacets, query))
   );
 }
index 7a16e77ae4e9c56bb60b4f089ba2184547d7d273..aa95bf48b7d0e20e1bbe8ee547aa686872ed2da4 100644 (file)
@@ -124,7 +124,6 @@ exports[`should render correctly for hotspots 1`] = `
         "query": Object {
           "assignedToMe": undefined,
           "branch": undefined,
-          "category": undefined,
           "hotspots": undefined,
           "id": "my-project",
           "pullRequest": "1001",
@@ -158,7 +157,6 @@ exports[`should render correctly for hotspots 2`] = `
         "query": Object {
           "assignedToMe": undefined,
           "branch": undefined,
-          "category": undefined,
           "hotspots": undefined,
           "id": "my-project",
           "pullRequest": "1001",
index 7f49f682cc8d02ee5436729e61a558278d46bfd1..e96fea1196f240130995a19fc69cfc55f5bcc864 100644 (file)
@@ -31,6 +31,7 @@ import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../helpe
 import { getStandards } from '../../helpers/security-standard';
 import { isLoggedIn } from '../../helpers/users';
 import { BranchLike } from '../../types/branch-like';
+import { SecurityStandard, Standards } from '../../types/security';
 import {
   HotspotFilters,
   HotspotResolution,
@@ -40,6 +41,7 @@ import {
 } from '../../types/security-hotspots';
 import SecurityHotspotsAppRenderer from './SecurityHotspotsAppRenderer';
 import './styles.css';
+import { SECURITY_STANDARDS } from './utils';
 
 const HOTSPOT_KEYMASTER_SCOPE = 'hotspots-list';
 const PAGE_SIZE = 500;
@@ -53,6 +55,8 @@ interface Props {
 }
 
 interface State {
+  filterByCategory?: { standard: SecurityStandard; category: string };
+  filters: HotspotFilters;
   hotspotKeys?: string[];
   hotspots: RawHotspot[];
   hotspotsPageIndex: number;
@@ -61,9 +65,8 @@ interface State {
   loading: boolean;
   loadingMeasure: boolean;
   loadingMore: boolean;
-  securityCategories: T.StandardSecurityCategories;
   selectedHotspot: RawHotspot | undefined;
-  filters: HotspotFilters;
+  standards: Standards;
 }
 
 export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
@@ -80,8 +83,13 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
       hotspots: [],
       hotspotsTotal: 0,
       hotspotsPageIndex: 1,
-      securityCategories: {},
       selectedHotspot: undefined,
+      standards: {
+        [SecurityStandard.OWASP_TOP10]: {},
+        [SecurityStandard.SANS_TOP25]: {},
+        [SecurityStandard.SONARSOURCE]: {},
+        [SecurityStandard.CWE]: {}
+      },
       filters: {
         ...this.constructFiltersFromProps(props),
         status: HotspotStatusFilter.TO_REVIEW
@@ -99,7 +107,8 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
   componentDidUpdate(previous: Props) {
     if (
       this.props.component.key !== previous.component.key ||
-      this.props.location.query.hotspots !== previous.location.query.hotspots
+      this.props.location.query.hotspots !== previous.location.query.hotspots ||
+      SECURITY_STANDARDS.some(s => this.props.location.query[s] !== previous.location.query[s])
     ) {
       this.fetchInitialData();
     }
@@ -175,27 +184,19 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
       this.fetchSecurityHotspots(),
       this.fetchSecurityHotspotsReviewed()
     ])
-      .then(([{ sonarsourceSecurity }, { hotspots, paging }]) => {
+      .then(([standards, { hotspots, paging }]) => {
         if (!this.mounted) {
           return;
         }
 
-        const requestedCategory = this.props.location.query.category;
-
-        let selectedHotspot;
-        if (hotspots.length > 0) {
-          const hotspotForCategory = requestedCategory
-            ? hotspots.find(h => h.securityCategory === requestedCategory)
-            : undefined;
-          selectedHotspot = hotspotForCategory ?? hotspots[0];
-        }
+        const selectedHotspot = hotspots.length > 0 ? hotspots[0] : undefined;
 
         this.setState({
           hotspots,
           hotspotsTotal: paging.total,
           loading: false,
-          securityCategories: sonarsourceSecurity,
-          selectedHotspot
+          selectedHotspot,
+          standards
         });
       })
       .catch(this.handleCallFailure);
@@ -241,7 +242,12 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
       ? (location.query.hotspots as string).split(',')
       : undefined;
 
-    this.setState({ hotspotKeys });
+    const standard = SECURITY_STANDARDS.find(stnd => location.query[stnd] !== undefined);
+    const filterByCategory = standard
+      ? { standard, category: location.query[standard] }
+      : undefined;
+
+    this.setState({ filterByCategory, hotspotKeys });
 
     if (hotspotKeys && hotspotKeys.length > 0) {
       return getSecurityHotspotList(hotspotKeys, {
@@ -250,6 +256,17 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
       });
     }
 
+    if (filterByCategory) {
+      return getSecurityHotspots({
+        [filterByCategory.standard]: filterByCategory.category,
+        projectKey: component.key,
+        p: page,
+        ps: PAGE_SIZE,
+        status: HotspotStatus.TO_REVIEW, // we're only interested in unresolved hotspots
+        ...getBranchLikeQuery(branchLike)
+      });
+    }
+
     const status =
       filters.status === HotspotStatusFilter.TO_REVIEW
         ? HotspotStatus.TO_REVIEW
@@ -333,7 +350,13 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
   handleShowAllHotspots = () => {
     this.props.router.push({
       ...this.props.location,
-      query: { ...this.props.location.query, hotspots: undefined }
+      query: {
+        ...this.props.location.query,
+        hotspots: undefined,
+        [SecurityStandard.OWASP_TOP10]: undefined,
+        [SecurityStandard.SANS_TOP25]: undefined,
+        [SecurityStandard.SONARSOURCE]: undefined
+      }
     });
   };
 
@@ -360,6 +383,8 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
   render() {
     const { branchLike, component } = this.props;
     const {
+      filterByCategory,
+      filters,
       hotspotKeys,
       hotspots,
       hotspotsReviewedMeasure,
@@ -367,9 +392,8 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
       loading,
       loadingMeasure,
       loadingMore,
-      securityCategories,
       selectedHotspot,
-      filters
+      standards
     } = this.state;
 
     return (
@@ -377,10 +401,13 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
         branchLike={branchLike}
         component={component}
         filters={filters}
+        filterByCategory={filterByCategory}
         hotspots={hotspots}
         hotspotsReviewedMeasure={hotspotsReviewedMeasure}
         hotspotsTotal={hotspotsTotal}
-        isStaticListOfHotspots={Boolean(hotspotKeys && hotspotKeys.length > 0)}
+        isStaticListOfHotspots={Boolean(
+          (hotspotKeys && hotspotKeys.length > 0) || filterByCategory
+        )}
         loading={loading}
         loadingMeasure={loadingMeasure}
         loadingMore={loadingMore}
@@ -389,8 +416,9 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> {
         onLoadMore={this.handleLoadMore}
         onShowAllHotspots={this.handleShowAllHotspots}
         onUpdateHotspot={this.handleHotspotUpdate}
-        securityCategories={securityCategories}
+        securityCategories={standards[SecurityStandard.SONARSOURCE]}
         selectedHotspot={selectedHotspot}
+        standards={standards}
       />
     );
   }
index 06862abf159192c978c62192d3e1a2fdb39a85da..046e0f04240eb40e541b10d9d9760cf56a841f55 100644 (file)
@@ -27,16 +27,22 @@ import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
 import ScreenPositionHelper from '../../components/common/ScreenPositionHelper';
 import { isBranch } from '../../helpers/branch-like';
 import { BranchLike } from '../../types/branch-like';
+import { SecurityStandard, Standards } from '../../types/security';
 import { HotspotFilters, HotspotStatusFilter, RawHotspot } from '../../types/security-hotspots';
 import EmptyHotspotsPage from './components/EmptyHotspotsPage';
 import FilterBar from './components/FilterBar';
 import HotspotList from './components/HotspotList';
+import HotspotSimpleList from './components/HotspotSimpleList';
 import HotspotViewer from './components/HotspotViewer';
 import './styles.css';
 
 export interface SecurityHotspotsAppRendererProps {
   branchLike?: BranchLike;
   component: T.Component;
+  filterByCategory?: {
+    standard: SecurityStandard;
+    category: string;
+  };
   filters: HotspotFilters;
   hotspots: RawHotspot[];
   hotspotsReviewedMeasure?: string;
@@ -52,12 +58,15 @@ export interface SecurityHotspotsAppRendererProps {
   onUpdateHotspot: (hotspotKey: string) => Promise<void>;
   selectedHotspot: RawHotspot | undefined;
   securityCategories: T.StandardSecurityCategories;
+  standards: Standards;
 }
 
 export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) {
   const {
     branchLike,
     component,
+    filterByCategory,
+    filters,
     hotspots,
     hotspotsReviewedMeasure,
     hotspotsTotal,
@@ -67,7 +76,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
     loadingMore,
     securityCategories,
     selectedHotspot,
-    filters
+    standards
   } = props;
 
   const scrollableRef = React.useRef(null);
@@ -116,17 +125,30 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe
               {({ top }) => (
                 <div className="layout-page-side" ref={scrollableRef} style={{ top }}>
                   <div className="layout-page-side-inner">
-                    <HotspotList
-                      hotspots={hotspots}
-                      hotspotsTotal={hotspotsTotal}
-                      isStaticListOfHotspots={isStaticListOfHotspots}
-                      loadingMore={loadingMore}
-                      onHotspotClick={props.onHotspotClick}
-                      onLoadMore={props.onLoadMore}
-                      securityCategories={securityCategories}
-                      selectedHotspot={selectedHotspot}
-                      statusFilter={filters.status}
-                    />
+                    {filterByCategory ? (
+                      <HotspotSimpleList
+                        filterByCategory={filterByCategory}
+                        hotspots={hotspots}
+                        hotspotsTotal={hotspotsTotal}
+                        loadingMore={loadingMore}
+                        onHotspotClick={props.onHotspotClick}
+                        onLoadMore={props.onLoadMore}
+                        selectedHotspot={selectedHotspot}
+                        standards={standards}
+                      />
+                    ) : (
+                      <HotspotList
+                        hotspots={hotspots}
+                        hotspotsTotal={hotspotsTotal}
+                        isStaticListOfHotspots={isStaticListOfHotspots}
+                        loadingMore={loadingMore}
+                        onHotspotClick={props.onHotspotClick}
+                        onLoadMore={props.onLoadMore}
+                        securityCategories={securityCategories}
+                        selectedHotspot={selectedHotspot}
+                        statusFilter={filters.status}
+                      />
+                    )}
                   </div>
                 </div>
               )}
index ce99870b36eba883d5d06640983264b2063e0174..8adca8d86d66103d3680bafadddab8385190104a 100644 (file)
@@ -24,7 +24,7 @@ import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
 import { getMeasures } from '../../../api/measures';
 import { getSecurityHotspotList, getSecurityHotspots } from '../../../api/security-hotspots';
 import { mockBranch, mockPullRequest } from '../../../helpers/mocks/branch-like';
-import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
+import { mockRawHotspot, mockStandards } from '../../../helpers/mocks/security-hotspots';
 import { getStandards } from '../../../helpers/security-standard';
 import {
   mockComponent,
@@ -33,6 +33,7 @@ import {
   mockLoggedInUser,
   mockRouter
 } from '../../../helpers/testMocks';
+import { SecurityStandard } from '../../../types/security';
 import {
   HotspotResolution,
   HotspotStatus,
@@ -100,30 +101,26 @@ it('should load data correctly', async () => {
   expect(wrapper.state().loading).toBe(false);
   expect(wrapper.state().hotspots).toEqual(hotspots);
   expect(wrapper.state().selectedHotspot).toBe(hotspots[0]);
-  expect(wrapper.state().securityCategories).toEqual({
-    cat1: { title: 'cat 1' }
+  expect(wrapper.state().standards).toEqual({
+    sonarsourceSecurity: {
+      cat1: { title: 'cat 1' }
+    }
   });
   expect(wrapper.state().loadingMeasure).toBe(false);
   expect(wrapper.state().hotspotsReviewedMeasure).toBe('86.6');
 });
 
-it('should handle category request', async () => {
-  const hotspots = [mockRawHotspot(), mockRawHotspot({ securityCategory: 'log-injection' })];
-  (getSecurityHotspots as jest.Mock).mockResolvedValue({
-    hotspots,
-    paging: {
-      total: 1
-    }
-  });
+it('should handle category request', () => {
+  (getStandards as jest.Mock).mockResolvedValue(mockStandards());
   (getMeasures as jest.Mock).mockResolvedValue([{ value: '86.6' }]);
 
-  const wrapper = shallowRender({
-    location: mockLocation({ query: { category: hotspots[1].securityCategory } })
+  shallowRender({
+    location: mockLocation({ query: { [SecurityStandard.OWASP_TOP10]: 'a1' } })
   });
 
-  await waitAndUpdate(wrapper);
-
-  expect(wrapper.state().selectedHotspot).toBe(hotspots[1]);
+  expect(getSecurityHotspots).toBeCalledWith(
+    expect.objectContaining({ [SecurityStandard.OWASP_TOP10]: 'a1' })
+  );
 });
 
 it('should load data correctly when hotspot key list is forced', async () => {
index de4d561db97e2cf4842b53f5e2adeaceb8a5d97d..a6e09f9dc9efeffa11877bed56f0e1857c3f8faa 100644 (file)
@@ -21,7 +21,7 @@ import { shallow } from 'enzyme';
 import * as React from 'react';
 import { scrollToElement } from 'sonar-ui-common/helpers/scrolling';
 import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
-import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots';
+import { mockRawHotspot, mockStandards } from '../../../helpers/mocks/security-hotspots';
 import { mockComponent } from '../../../helpers/testMocks';
 import { HotspotStatusFilter } from '../../../types/security-hotspots';
 import FilterBar from '../components/FilterBar';
@@ -126,6 +126,7 @@ function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) {
       onUpdateHotspot={jest.fn()}
       securityCategories={{}}
       selectedHotspot={undefined}
+      standards={mockStandards()}
       {...props}
     />
   );
index 70ad7d22a8af400e940bd09449806567df793933..5ab13c4826ef2450b99d047ec55b67f7f341c221 100644 (file)
@@ -52,5 +52,13 @@ exports[`should render correctly 1`] = `
   onShowAllHotspots={[Function]}
   onUpdateHotspot={[Function]}
   securityCategories={Object {}}
+  standards={
+    Object {
+      "cwe": Object {},
+      "owaspTop10": Object {},
+      "sansTop25": Object {},
+      "sonarsourceSecurity": Object {},
+    }
+  }
 />
 `;
index e78ac19127293a9ca5863c05767dc5dc2d52656c..2fd73447a3249ea087bb95c4f4f763eab874e424 100644 (file)
@@ -29,7 +29,7 @@ export interface HotspotCategoryProps {
   expanded: boolean;
   hotspots: RawHotspot[];
   onHotspotClick: (hotspot: RawHotspot) => void;
-  onToggleExpand: (categoryKey: string, value: boolean) => void;
+  onToggleExpand?: (categoryKey: string, value: boolean) => void;
   selectedHotspot: RawHotspot;
   title: string;
   isLastAndIncomplete: boolean;
@@ -46,26 +46,32 @@ export default function HotspotCategory(props: HotspotCategoryProps) {
 
   return (
     <div className={classNames('hotspot-category', risk)}>
-      <a
-        className={classNames(
-          'hotspot-category-header display-flex-space-between display-flex-center',
-          { 'contains-selected-hotspot': selectedHotspot.securityCategory === categoryKey }
-        )}
-        href="#"
-        onClick={() => props.onToggleExpand(categoryKey, !expanded)}>
-        <strong className="flex-1 spacer-right break-word">{title}</strong>
-        <span>
-          <span className="counter-badge">
-            {hotspots.length}
-            {isLastAndIncomplete && '+'}
-          </span>
-          {expanded ? (
-            <ChevronUpIcon className="big-spacer-left" />
-          ) : (
-            <ChevronDownIcon className="big-spacer-left" />
+      {props.onToggleExpand ? (
+        <a
+          className={classNames(
+            'hotspot-category-header display-flex-space-between display-flex-center',
+            { 'contains-selected-hotspot': selectedHotspot.securityCategory === categoryKey }
           )}
-        </span>
-      </a>
+          href="#"
+          onClick={() => props.onToggleExpand && props.onToggleExpand(categoryKey, !expanded)}>
+          <strong className="flex-1 spacer-right break-word">{title}</strong>
+          <span>
+            <span className="counter-badge">
+              {hotspots.length}
+              {isLastAndIncomplete && '+'}
+            </span>
+            {expanded ? (
+              <ChevronUpIcon className="big-spacer-left" />
+            ) : (
+              <ChevronDownIcon className="big-spacer-left" />
+            )}
+          </span>
+        </a>
+      ) : (
+        <div className="hotspot-category-header">
+          <strong className="flex-1 spacer-right break-word">{title}</strong>
+        </div>
+      )}
       {expanded && (
         <ul>
           {hotspots.map(h => (
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx
new file mode 100644 (file)
index 0000000..c77d712
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import ListFooter from 'sonar-ui-common/components/controls/ListFooter';
+import SecurityHotspotIcon from 'sonar-ui-common/components/icons/SecurityHotspotIcon';
+import { translateWithParameters } from 'sonar-ui-common/helpers/l10n';
+import { SecurityStandard, Standards } from '../../../types/security';
+import { RawHotspot } from '../../../types/security-hotspots';
+import { SECURITY_STANDARD_RENDERER } from '../utils';
+import HotspotListItem from './HotspotListItem';
+
+export interface HotspotSimpleListProps {
+  filterByCategory: {
+    standard: SecurityStandard;
+    category: string;
+  };
+  hotspots: RawHotspot[];
+  hotspotsTotal: number;
+  loadingMore: boolean;
+  onHotspotClick: (hotspot: RawHotspot) => void;
+  onLoadMore: () => void;
+  selectedHotspot: RawHotspot;
+  standards: Standards;
+}
+
+export default function HotspotSimpleList(props: HotspotSimpleListProps) {
+  const {
+    filterByCategory,
+    hotspots,
+    hotspotsTotal,
+    loadingMore,
+    selectedHotspot,
+    standards
+  } = props;
+
+  return (
+    <div className="hotspots-list-single-category huge-spacer-bottom">
+      <h1 className="hotspot-list-header bordered-bottom">
+        <SecurityHotspotIcon className="spacer-right" />
+        {translateWithParameters('hotspots.list_title', hotspotsTotal)}
+      </h1>
+      <div className="big-spacer-bottom">
+        <div className="hotspot-category">
+          <div className="hotspot-category-header">
+            <strong className="flex-1 spacer-right break-word">
+              {SECURITY_STANDARD_RENDERER[filterByCategory.standard](
+                standards,
+                filterByCategory.category
+              )}
+            </strong>
+          </div>
+          <ul>
+            {hotspots.map(h => (
+              <li data-hotspot-key={h.key} key={h.key}>
+                <HotspotListItem
+                  hotspot={h}
+                  onClick={props.onHotspotClick}
+                  selected={h.key === selectedHotspot.key}
+                />
+              </li>
+            ))}
+          </ul>
+        </div>
+      </div>
+      <ListFooter
+        count={hotspots.length}
+        loadMore={!loadingMore ? props.onLoadMore : undefined}
+        loading={loadingMore}
+        total={hotspotsTotal}
+      />
+    </div>
+  );
+}
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
new file mode 100644 (file)
index 0000000..b00d5d7
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { mockRawHotspot } from '../../../../helpers/mocks/security-hotspots';
+import { SecurityStandard } from '../../../../types/security';
+import HotspotSimpleList, { HotspotSimpleListProps } from '../HotspotSimpleList';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<HotspotSimpleListProps> = {}) {
+  const hotspots = [mockRawHotspot({ key: 'h1' }), mockRawHotspot({ key: 'h2' })];
+
+  return shallow(
+    <HotspotSimpleList
+      filterByCategory={{ standard: SecurityStandard.OWASP_TOP10, category: 'a1' }}
+      hotspots={hotspots}
+      hotspotsTotal={2}
+      loadingMore={false}
+      onHotspotClick={jest.fn()}
+      onLoadMore={jest.fn()}
+      selectedHotspot={hotspots[0]}
+      standards={{
+        cwe: {},
+        owaspTop10: {
+          a1: { title: 'A1 - SQL Injection' },
+          a3: { title: 'A3 - Sensitive Data Exposure' }
+        },
+        sansTop25: {},
+        sonarsourceSecurity: {}
+      }}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap
new file mode 100644 (file)
index 0000000..24eb95c
--- /dev/null
@@ -0,0 +1,93 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="hotspots-list-single-category huge-spacer-bottom"
+>
+  <h1
+    className="hotspot-list-header bordered-bottom"
+  >
+    <SecurityHotspotIcon
+      className="spacer-right"
+    />
+    hotspots.list_title.2
+  </h1>
+  <div
+    className="big-spacer-bottom"
+  >
+    <div
+      className="hotspot-category"
+    >
+      <div
+        className="hotspot-category-header"
+      >
+        <strong
+          className="flex-1 spacer-right break-word"
+        >
+          A1 - A1 - SQL Injection
+        </strong>
+      </div>
+      <ul>
+        <li
+          data-hotspot-key="h1"
+          key="h1"
+        >
+          <HotspotListItem
+            hotspot={
+              Object {
+                "author": "Developer 1",
+                "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+                "creationDate": "2013-05-13T17:55:39+0200",
+                "key": "h1",
+                "line": 81,
+                "message": "'3' is a magic number.",
+                "project": "com.github.kevinsawicki:http-request",
+                "resolution": undefined,
+                "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+                "securityCategory": "command-injection",
+                "status": "TO_REVIEW",
+                "updateDate": "2013-05-13T17:55:39+0200",
+                "vulnerabilityProbability": "HIGH",
+              }
+            }
+            onClick={[MockFunction]}
+            selected={true}
+          />
+        </li>
+        <li
+          data-hotspot-key="h2"
+          key="h2"
+        >
+          <HotspotListItem
+            hotspot={
+              Object {
+                "author": "Developer 1",
+                "component": "com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest",
+                "creationDate": "2013-05-13T17:55:39+0200",
+                "key": "h2",
+                "line": 81,
+                "message": "'3' is a magic number.",
+                "project": "com.github.kevinsawicki:http-request",
+                "resolution": undefined,
+                "rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
+                "securityCategory": "command-injection",
+                "status": "TO_REVIEW",
+                "updateDate": "2013-05-13T17:55:39+0200",
+                "vulnerabilityProbability": "HIGH",
+              }
+            }
+            onClick={[MockFunction]}
+            selected={false}
+          />
+        </li>
+      </ul>
+    </div>
+  </div>
+  <ListFooter
+    count={2}
+    loadMore={[MockFunction]}
+    loading={false}
+    total={2}
+  />
+</div>
+`;
index e9095d08038ffb09755ef893178918bbc73b9fbb..16139eb1655ef6ab54a254d23f0c344bc002c985 100644 (file)
@@ -71,3 +71,7 @@
   height: 0;
   overflow: hidden;
 }
+
+#security_hotspots .hotspots-list-single-category .hotspot-category .hotspot-category-header {
+  color: var(--blue);
+}
index af8c5be1a9a617903a7ce24cf7736e9882ec6cd1..3a4dc9bfe21966b2b915db10acfa395c8ce142b9 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { groupBy, sortBy } from 'lodash';
+import {
+  renderCWECategory,
+  renderOwaspTop10Category,
+  renderSansTop25Category,
+  renderSonarSourceSecurityCategory
+} from '../../helpers/security-standard';
+import { SecurityStandard } from '../../types/security';
 import {
   Hotspot,
   HotspotResolution,
@@ -30,6 +37,18 @@ import {
 } from '../../types/security-hotspots';
 
 export const RISK_EXPOSURE_LEVELS = [RiskExposure.HIGH, RiskExposure.MEDIUM, RiskExposure.LOW];
+export const SECURITY_STANDARDS = [
+  SecurityStandard.SONARSOURCE,
+  SecurityStandard.OWASP_TOP10,
+  SecurityStandard.SANS_TOP25
+];
+
+export const SECURITY_STANDARD_RENDERER = {
+  [SecurityStandard.OWASP_TOP10]: renderOwaspTop10Category,
+  [SecurityStandard.SANS_TOP25]: renderSansTop25Category,
+  [SecurityStandard.SONARSOURCE]: renderSonarSourceSecurityCategory,
+  [SecurityStandard.CWE]: renderCWECategory
+};
 
 export function mapRules(rules: Array<{ key: string; name: string }>): T.Dict<string> {
   return rules.reduce((ruleMap: T.Dict<string>, r) => {
index bde1c1fe541e30c2b36f1cac4252711be08cc92a..d7a0b2a4b118651b1a6521c8ce5dae3d0fc7105a 100644 (file)
@@ -1,3 +1,4 @@
+import { Standards } from '../../types/security';
 /*
  * SonarQube
  * Copyright (C) 2009-2020 SonarSource SA
@@ -25,7 +26,7 @@ import {
 } from '../security-standard';
 
 describe('renderCWECategory', () => {
-  const standards: T.Standards = {
+  const standards: Standards = {
     cwe: {
       '1004': {
         title: "Sensitive Cookie Without 'HttpOnly' Flag"
@@ -38,7 +39,7 @@ describe('renderCWECategory', () => {
     sansTop25: {},
     sonarsourceSecurity: {}
   };
-  it('should render categories correctly', () => {
+  it('should render cwe categories correctly', () => {
     expect(renderCWECategory(standards, '1004')).toEqual(
       "CWE-1004 - Sensitive Cookie Without 'HttpOnly' Flag"
     );
@@ -48,7 +49,7 @@ describe('renderCWECategory', () => {
 });
 
 describe('renderOwaspTop10Category', () => {
-  const standards: T.Standards = {
+  const standards: Standards = {
     cwe: {},
     owaspTop10: {
       a1: {
@@ -58,7 +59,7 @@ describe('renderOwaspTop10Category', () => {
     sansTop25: {},
     sonarsourceSecurity: {}
   };
-  it('should render categories correctly', () => {
+  it('should render owasp categories correctly', () => {
     expect(renderOwaspTop10Category(standards, 'a1')).toEqual('A1 - Injection');
     expect(renderOwaspTop10Category(standards, 'a1', true)).toEqual('OWASP A1 - Injection');
     expect(renderOwaspTop10Category(standards, 'a2')).toEqual('A2');
@@ -67,7 +68,7 @@ describe('renderOwaspTop10Category', () => {
 });
 
 describe('renderSansTop25Category', () => {
-  const standards: T.Standards = {
+  const standards: Standards = {
     cwe: {},
     owaspTop10: {},
     sansTop25: {
@@ -77,7 +78,7 @@ describe('renderSansTop25Category', () => {
     },
     sonarsourceSecurity: {}
   };
-  it('should render categories correctly', () => {
+  it('should render sans categories correctly', () => {
     expect(renderSansTop25Category(standards, 'insecure-interaction')).toEqual(
       'Insecure Interaction Between Components'
     );
@@ -90,7 +91,7 @@ describe('renderSansTop25Category', () => {
 });
 
 describe('renderSonarSourceSecurityCategory', () => {
-  const standards: T.Standards = {
+  const standards: Standards = {
     cwe: {},
     owaspTop10: {},
     sansTop25: {},
@@ -103,7 +104,7 @@ describe('renderSonarSourceSecurityCategory', () => {
       }
     }
   };
-  it('should render categories correctly', () => {
+  it('should render sonarsource categories correctly', () => {
     expect(renderSonarSourceSecurityCategory(standards, 'xss')).toEqual(
       'Cross-Site Scripting (XSS)'
     );
index 728c014bf60c9f85ef0e1be868e966c4a07e4677..81c73746ad848dad9612486d4fccd274dc39ee55 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { ComponentQualifier } from '../../types/component';
+import { Standards } from '../../types/security';
 import {
   Hotspot,
   HotspotResolution,
@@ -104,3 +105,49 @@ export function mockHotspotReviewHistoryElement(
     ...overrides
   };
 }
+
+export function mockStandards(): Standards {
+  return {
+    cwe: {
+      unknown: {
+        title: 'No CWE associated'
+      },
+      '1004': {
+        title: "Sensitive Cookie Without 'HttpOnly' Flag"
+      }
+    },
+    owaspTop10: {
+      a1: {
+        title: 'Injection'
+      },
+      a2: {
+        title: 'Broken Authentication'
+      },
+      a3: {
+        title: 'Sensitive Data Exposure'
+      }
+    },
+    sansTop25: {
+      'insecure-interaction': {
+        title: 'Insecure Interaction Between Components'
+      },
+      'risky-resource': {
+        title: 'Risky Resource Management'
+      },
+      'porous-defenses': {
+        title: 'Porous Defenses'
+      }
+    },
+    sonarsourceSecurity: {
+      'buffer-overflow': {
+        title: 'Buffer Overflow'
+      },
+      'sql-injection': {
+        title: 'SQL Injection'
+      },
+      rce: {
+        title: 'Code Injection (RCE)'
+      }
+    }
+  };
+}
index d3ae7a7d41ec3dbde07b1838f08e7d1fecee6d8a..3aeb12f2262469ba7880d1a784f337e1bfead518 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-export function getStandards(): Promise<T.Standards> {
+import { Standards } from '../types/security';
+
+export function getStandards(): Promise<Standards> {
   return import('./standards.json').then(x => x.default);
 }
 
-export function renderCWECategory(standards: T.Standards, category: string): string {
+export function renderCWECategory(standards: Standards, category: string): string {
   const record = standards.cwe[category];
   if (!record) {
     return `CWE-${category}`;
@@ -33,7 +35,7 @@ export function renderCWECategory(standards: T.Standards, category: string): str
 }
 
 export function renderOwaspTop10Category(
-  standards: T.Standards,
+  standards: Standards,
   category: string,
   withPrefix = false
 ): string {
@@ -46,7 +48,7 @@ export function renderOwaspTop10Category(
 }
 
 export function renderSansTop25Category(
-  standards: T.Standards,
+  standards: Standards,
   category: string,
   withPrefix = false
 ): string {
@@ -55,7 +57,7 @@ export function renderSansTop25Category(
 }
 
 export function renderSonarSourceSecurityCategory(
-  standards: T.Standards,
+  standards: Standards,
   category: string,
   withPrefix = false
 ): string {
index ca06577c46da1dcbe8ef394ab0d93bffe79a8f1b..b6e55ea8111e923ce47783cb0b9083bb012053b5 100644 (file)
@@ -1,3 +1,4 @@
+import { pick } from 'lodash';
 /*
  * SonarQube
  * Copyright (C) 2009-2020 SonarSource SA
@@ -22,6 +23,7 @@ import { getProfilePath } from '../apps/quality-profiles/utils';
 import { BranchLike, BranchParameters } from '../types/branch-like';
 import { ComponentQualifier, isPortfolioLike } from '../types/component';
 import { GraphType } from '../types/project-activity';
+import { SecurityStandard } from '../types/security';
 import { getBranchLikeQuery, isBranch, isMainBranch, isPullRequest } from './branch-like';
 
 type Query = Location['query'];
@@ -93,7 +95,7 @@ export function getComponentIssuesUrl(componentKey: string, query?: Query): Loca
  * Generate URL for a component's security hotspot page
  */
 export function getComponentSecurityHotspotsUrl(componentKey: string, query: Query = {}): Location {
-  const { branch, pullRequest, sinceLeakPeriod, hotspots, assignedToMe, category } = query;
+  const { branch, pullRequest, sinceLeakPeriod, hotspots, assignedToMe } = query;
   return {
     pathname: '/security_hotspots',
     query: {
@@ -103,7 +105,11 @@ export function getComponentSecurityHotspotsUrl(componentKey: string, query: Que
       sinceLeakPeriod,
       hotspots,
       assignedToMe,
-      category
+      ...pick(query, [
+        SecurityStandard.SONARSOURCE,
+        SecurityStandard.OWASP_TOP10,
+        SecurityStandard.SANS_TOP25
+      ])
     }
   };
 }
diff --git a/server/sonar-web/src/main/js/types/security.ts b/server/sonar-web/src/main/js/types/security.ts
new file mode 100644 (file)
index 0000000..938695e
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+export enum SecurityStandard {
+  OWASP_TOP10 = 'owaspTop10',
+  SANS_TOP25 = 'sansTop25',
+  SONARSOURCE = 'sonarsourceSecurity',
+  CWE = 'cwe'
+}
+
+export type StandardType = SecurityStandard;
+
+export type Standards = {
+  [key in StandardType]: T.Dict<{ title: string; description?: string }>;
+};
index 3128665d16143f75323fe1ade1bf960001ce1768..e4d8534fee2510a22f2bbd739d05b634fa3a05a5 100644 (file)
@@ -824,12 +824,6 @@ declare namespace T {
 
   export type StandardSecurityCategories = T.Dict<{ title: string; description?: string }>;
 
-  export type Standards = {
-    [key in StandardType]: T.Dict<{ title: string; description?: string }>;
-  };
-
-  export type StandardType = 'owaspTop10' | 'sansTop25' | 'cwe' | 'sonarsourceSecurity';
-
   export type Status = 'ERROR' | 'OK';
 
   export interface SubscriptionPlan {