Browse Source

SONAR-13566 Display hotspots of a specific category

tags/8.6.0.39681
Jeremy Davis 3 years ago
parent
commit
7c6f52df2e
24 changed files with 568 additions and 139 deletions
  1. 4
    2
      server/sonar-web/src/main/js/api/security-hotspots.ts
  2. 3
    2
      server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
  3. 33
    12
      server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts
  4. 3
    2
      server/sonar-web/src/main/js/apps/issues/components/App.tsx
  5. 20
    19
      server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
  6. 9
    8
      server/sonar-web/src/main/js/apps/issues/utils.ts
  7. 0
    2
      server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap
  8. 50
    22
      server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx
  9. 34
    12
      server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx
  10. 13
    16
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx
  11. 2
    1
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx
  12. 8
    0
      server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap
  13. 26
    20
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx
  14. 90
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx
  15. 54
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx
  16. 93
    0
      server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap
  17. 4
    0
      server/sonar-web/src/main/js/apps/security-hotspots/styles.css
  18. 19
    0
      server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
  19. 9
    8
      server/sonar-web/src/main/js/helpers/__tests__/security-standard-test.ts
  20. 47
    0
      server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts
  21. 7
    5
      server/sonar-web/src/main/js/helpers/security-standard.ts
  22. 8
    2
      server/sonar-web/src/main/js/helpers/urls.ts
  23. 32
    0
      server/sonar-web/src/main/js/types/security.ts
  24. 0
    6
      server/sonar-web/src/main/js/types/types.d.ts

+ 4
- 2
server/sonar-web/src/main/js/api/security-hotspots.ts View 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
);
}

+ 3
- 2
server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx View 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

+ 33
- 12
server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts View 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);
});

+ 3
- 2
server/sonar-web/src/main/js/apps/issues/components/App.tsx View 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),

+ 20
- 19
server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx View 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) =>

+ 9
- 8
server/sonar-web/src/main/js/apps/issues/utils.ts View 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))
);
}

+ 0
- 2
server/sonar-web/src/main/js/apps/overview/components/__tests__/__snapshots__/IssueLabel-test.tsx.snap View 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",

+ 50
- 22
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsApp.tsx View 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}
/>
);
}

+ 34
- 12
server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx View 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>
)}

+ 13
- 16
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsApp-test.tsx View 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 () => {

+ 2
- 1
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/SecurityHotspotsAppRenderer-test.tsx View 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}
/>
);

+ 8
- 0
server/sonar-web/src/main/js/apps/security-hotspots/__tests__/__snapshots__/SecurityHotspotsApp-test.tsx.snap View 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 {},
}
}
/>
`;

+ 26
- 20
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotCategory.tsx View 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 => (

+ 90
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotSimpleList.tsx View File

@@ -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>
);
}

+ 54
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotSimpleList-test.tsx View File

@@ -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}
/>
);
}

+ 93
- 0
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotSimpleList-test.tsx.snap View File

@@ -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>
`;

+ 4
- 0
server/sonar-web/src/main/js/apps/security-hotspots/styles.css View File

@@ -71,3 +71,7 @@
height: 0;
overflow: hidden;
}

#security_hotspots .hotspots-list-single-category .hotspot-category .hotspot-category-header {
color: var(--blue);
}

+ 19
- 0
server/sonar-web/src/main/js/apps/security-hotspots/utils.ts View File

@@ -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<string> {
return rules.reduce((ruleMap: T.Dict<string>, r) => {

+ 9
- 8
server/sonar-web/src/main/js/helpers/__tests__/security-standard-test.ts View 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)'
);

+ 47
- 0
server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts View 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)'
}
}
};
}

+ 7
- 5
server/sonar-web/src/main/js/helpers/security-standard.ts View File

@@ -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<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 {

+ 8
- 2
server/sonar-web/src/main/js/helpers/urls.ts View 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
])
}
};
}

+ 32
- 0
server/sonar-web/src/main/js/types/security.ts View File

@@ -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 }>;
};

+ 0
- 6
server/sonar-web/src/main/js/types/types.d.ts View 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 {

Loading…
Cancel
Save