From 7c6f52df2e18da76180d74b3533e709f5b47309a Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Mon, 5 Oct 2020 18:05:41 +0200 Subject: [PATCH] SONAR-13566 Display hotspots of a specific category --- .../src/main/js/api/security-hotspots.ts | 6 +- .../js/apps/coding-rules/components/App.tsx | 5 +- .../js/apps/issues/__tests__/utils-test.ts | 45 ++++++--- .../main/js/apps/issues/components/App.tsx | 5 +- .../js/apps/issues/sidebar/StandardFacet.tsx | 39 ++++---- .../src/main/js/apps/issues/utils.ts | 17 ++-- .../__snapshots__/IssueLabel-test.tsx.snap | 2 - .../security-hotspots/SecurityHotspotsApp.tsx | 72 +++++++++----- .../SecurityHotspotsAppRenderer.tsx | 46 ++++++--- .../__tests__/SecurityHotspotsApp-test.tsx | 29 +++--- .../SecurityHotspotsAppRenderer-test.tsx | 3 +- .../SecurityHotspotsApp-test.tsx.snap | 8 ++ .../components/HotspotCategory.tsx | 46 +++++---- .../components/HotspotSimpleList.tsx | 90 ++++++++++++++++++ .../__tests__/HotspotSimpleList-test.tsx | 54 +++++++++++ .../HotspotSimpleList-test.tsx.snap | 93 +++++++++++++++++++ .../main/js/apps/security-hotspots/styles.css | 4 + .../main/js/apps/security-hotspots/utils.ts | 19 ++++ .../__tests__/security-standard-test.ts | 17 ++-- .../js/helpers/mocks/security-hotspots.ts | 47 ++++++++++ .../src/main/js/helpers/security-standard.ts | 12 ++- server/sonar-web/src/main/js/helpers/urls.ts | 10 +- .../sonar-web/src/main/js/types/security.ts | 32 +++++++ server/sonar-web/src/main/js/types/types.d.ts | 6 -- 24 files changed, 568 insertions(+), 139 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/types/security.ts diff --git a/server/sonar-web/src/main/js/api/security-hotspots.ts b/server/sonar-web/src/main/js/api/security-hotspots.ts index f6ccad30b57..1dc1feb2fea 100644 --- a/server/sonar-web/src/main/js/api/security-hotspots.ts +++ b/server/sonar-web/src/main/js/api/security-hotspots.ts @@ -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 { - 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 { - return getJSON('/api/hotspots/search', { ...data, hotspots: hotspotKeys.join() }).catch( + return getJSON(HOTSPOTS_SEARCH_URL, { ...data, hotspots: hotspotKeys.join() }).catch( throwGlobalError ); } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx index 967f142878e..78c1e827e91 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx @@ -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 { 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 diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts index a88f0031c23..5d49a357089 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts @@ -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); }); diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx index a86834fbc1d..0c8163a8361 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx @@ -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 { 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), diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx index f43048c2a42..541989aabf1 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx @@ -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 { mounted = false; @@ -101,7 +102,7 @@ export default class StandardFacet extends React.PureComponent { 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 { }; 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 { 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 { 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 { 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 ( <> - + { )} - + { )} - + { 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) => diff --git a/server/sonar-web/src/main/js/apps/issues/utils.ts b/server/sonar-web/src/main/js/apps/issues/utils.ts index 8dc99ee6329..8e6807d394b 100644 --- a/server/sonar-web/src/main/js/apps/issues/utils.ts +++ b/server/sonar-web/src/main/js/apps/issues/utils.ts @@ -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, query: Partial, - 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)) ); } diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap index 7a16e77ae4e..aa95bf48b7d 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap @@ -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", diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx index 7f49f682cc8..e96fea1196f 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx @@ -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 { @@ -80,8 +83,13 @@ export class SecurityHotspotsApp extends React.PureComponent { 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 { 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 { 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 { ? (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 { }); } + 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 { 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 { render() { const { branchLike, component } = this.props; const { + filterByCategory, + filters, hotspotKeys, hotspots, hotspotsReviewedMeasure, @@ -367,9 +392,8 @@ export class SecurityHotspotsApp extends React.PureComponent { loading, loadingMeasure, loadingMore, - securityCategories, selectedHotspot, - filters + standards } = this.state; return ( @@ -377,10 +401,13 @@ export class SecurityHotspotsApp extends React.PureComponent { 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 { onLoadMore={this.handleLoadMore} onShowAllHotspots={this.handleShowAllHotspots} onUpdateHotspot={this.handleHotspotUpdate} - securityCategories={securityCategories} + securityCategories={standards[SecurityStandard.SONARSOURCE]} selectedHotspot={selectedHotspot} + standards={standards} /> ); } diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx index 06862abf159..046e0f04240 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx @@ -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; 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 }) => (
- + {filterByCategory ? ( + + ) : ( + + )}
)} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx index ce99870b36e..8adca8d86d6 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx @@ -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 () => { diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx index de4d561db97..a6e09f9dc9e 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx @@ -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 = {}) { onUpdateHotspot={jest.fn()} securityCategories={{}} selectedHotspot={undefined} + standards={mockStandards()} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap index 70ad7d22a8a..5ab13c4826e 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap @@ -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 {}, + } + } /> `; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx index e78ac191272..2fd73447a32 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx @@ -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 (
- props.onToggleExpand(categoryKey, !expanded)}> - {title} - - - {hotspots.length} - {isLastAndIncomplete && '+'} - - {expanded ? ( - - ) : ( - + {props.onToggleExpand ? ( + - + href="#" + onClick={() => props.onToggleExpand && props.onToggleExpand(categoryKey, !expanded)}> + {title} + + + {hotspots.length} + {isLastAndIncomplete && '+'} + + {expanded ? ( + + ) : ( + + )} + + + ) : ( +
+ {title} +
+ )} {expanded && (
    {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 index 00000000000..c77d712c9b8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx @@ -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 ( +
    +

    + + {translateWithParameters('hotspots.list_title', hotspotsTotal)} +

    +
    +
    +
    + + {SECURITY_STANDARD_RENDERER[filterByCategory.standard]( + standards, + filterByCategory.category + )} + +
    +
      + {hotspots.map(h => ( +
    • + +
    • + ))} +
    +
    +
    + +
    + ); +} 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 index 00000000000..b00d5d779c7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx @@ -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 = {}) { + const hotspots = [mockRawHotspot({ key: 'h1' }), mockRawHotspot({ key: 'h2' })]; + + return shallow( + + ); +} 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 index 00000000000..24eb95c2d4a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +
    +

    + + hotspots.list_title.2 +

    +
    +
    +
    + + A1 - A1 - SQL Injection + +
    +
      +
    • + +
    • +
    • + +
    • +
    +
    +
    + +
    +`; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/styles.css b/server/sonar-web/src/main/js/apps/security-hotspots/styles.css index e9095d08038..16139eb1655 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/styles.css +++ b/server/sonar-web/src/main/js/apps/security-hotspots/styles.css @@ -71,3 +71,7 @@ height: 0; overflow: hidden; } + +#security_hotspots .hotspots-list-single-category .hotspot-category .hotspot-category-header { + color: var(--blue); +} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts b/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts index af8c5be1a9a..3a4dc9bfe21 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts +++ b/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts @@ -18,6 +18,13 @@ * 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 { return rules.reduce((ruleMap: T.Dict, r) => { diff --git a/server/sonar-web/src/main/js/helpers/__tests__/security-standard-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/security-standard-test.ts index bde1c1fe541..d7a0b2a4b11 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/security-standard-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/security-standard-test.ts @@ -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)' ); diff --git a/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts b/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts index 728c014bf60..81c73746ad8 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts @@ -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)' + } + } + }; +} diff --git a/server/sonar-web/src/main/js/helpers/security-standard.ts b/server/sonar-web/src/main/js/helpers/security-standard.ts index d3ae7a7d41e..3aeb12f2262 100644 --- a/server/sonar-web/src/main/js/helpers/security-standard.ts +++ b/server/sonar-web/src/main/js/helpers/security-standard.ts @@ -17,11 +17,13 @@ * 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 { +import { Standards } from '../types/security'; + +export function getStandards(): Promise { 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 { diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts index ca06577c46d..b6e55ea8111 100644 --- a/server/sonar-web/src/main/js/helpers/urls.ts +++ b/server/sonar-web/src/main/js/helpers/urls.ts @@ -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 index 00000000000..938695e20d3 --- /dev/null +++ b/server/sonar-web/src/main/js/types/security.ts @@ -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 }>; +}; diff --git a/server/sonar-web/src/main/js/types/types.d.ts b/server/sonar-web/src/main/js/types/types.d.ts index 3128665d161..e4d8534fee2 100644 --- a/server/sonar-web/src/main/js/types/types.d.ts +++ b/server/sonar-web/src/main/js/types/types.d.ts @@ -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 { -- 2.39.5