HotspotStatus
} from '../types/security-hotspots';
+const HOTSPOTS_SEARCH_URL = '/api/hotspots/search';
+
export function assignSecurityHotspot(
hotspotKey: string,
data: HotspotAssignRequest
sinceLeakPeriod?: boolean;
} & BranchParameters
): Promise<HotspotSearchResponse> {
- return getJSON('/api/hotspots/search', data).catch(throwGlobalError);
+ return getJSON(HOTSPOTS_SEARCH_URL, data).catch(throwGlobalError);
}
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
);
}
getMyOrganizations,
Store
} from '../../../store/rootReducer';
+import { SecurityStandard } from '../../../types/security';
import {
shouldOpenSonarSourceSecurityFacet,
shouldOpenStandardsChildFacet,
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
* 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,
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);
});
} 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';
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),
renderSansTop25Category,
renderSonarSourceSecurityCategory
} from '../../../helpers/security-standard';
+import { SecurityStandard, Standards, StandardType } from '../../../types/security';
import { Facet, formatFacetStat, Query, STANDARDS } from '../utils';
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;
loadStandards = () => {
getStandards().then(
- ({ owaspTop10, sansTop25, cwe, sonarsourceSecurity }: T.Standards) => {
+ ({ owaspTop10, sansTop25, cwe, sonarsourceSecurity }: Standards) => {
if (this.mounted) {
this.setState({ standards: { owaspTop10, sansTop25, cwe, sonarsourceSecurity } });
}
};
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) => {
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];
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) {
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')}
</>
)}
</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')}
</>
)}
</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')}
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) =>
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;
}
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
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))
);
}
): 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))
);
}
"query": Object {
"assignedToMe": undefined,
"branch": undefined,
- "category": undefined,
"hotspots": undefined,
"id": "my-project",
"pullRequest": "1001",
"query": Object {
"assignedToMe": undefined,
"branch": undefined,
- "category": undefined,
"hotspots": undefined,
"id": "my-project",
"pullRequest": "1001",
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,
} 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;
}
interface State {
+ filterByCategory?: { standard: SecurityStandard; category: string };
+ filters: HotspotFilters;
hotspotKeys?: string[];
hotspots: RawHotspot[];
hotspotsPageIndex: number;
loading: boolean;
loadingMeasure: boolean;
loadingMore: boolean;
- securityCategories: T.StandardSecurityCategories;
selectedHotspot: RawHotspot | undefined;
- filters: HotspotFilters;
+ standards: Standards;
}
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
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();
}
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);
? (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, {
});
}
+ 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
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
+ }
});
};
render() {
const { branchLike, component } = this.props;
const {
+ filterByCategory,
+ filters,
hotspotKeys,
hotspots,
hotspotsReviewedMeasure,
loading,
loadingMeasure,
loadingMore,
- securityCategories,
selectedHotspot,
- filters
+ standards
} = this.state;
return (
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}
onLoadMore={this.handleLoadMore}
onShowAllHotspots={this.handleShowAllHotspots}
onUpdateHotspot={this.handleHotspotUpdate}
- securityCategories={securityCategories}
+ securityCategories={standards[SecurityStandard.SONARSOURCE]}
selectedHotspot={selectedHotspot}
+ standards={standards}
/>
);
}
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;
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,
loadingMore,
securityCategories,
selectedHotspot,
- filters
+ standards
} = props;
const scrollableRef = React.useRef(null);
{({ 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>
)}
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,
mockLoggedInUser,
mockRouter
} from '../../../helpers/testMocks';
+import { SecurityStandard } from '../../../types/security';
import {
HotspotResolution,
HotspotStatus,
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 () => {
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';
onUpdateHotspot={jest.fn()}
securityCategories={{}}
selectedHotspot={undefined}
+ standards={mockStandards()}
{...props}
/>
);
onShowAllHotspots={[Function]}
onUpdateHotspot={[Function]}
securityCategories={Object {}}
+ standards={
+ Object {
+ "cwe": Object {},
+ "owaspTop10": Object {},
+ "sansTop25": Object {},
+ "sonarsourceSecurity": Object {},
+ }
+ }
/>
`;
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;
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 => (
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
--- /dev/null
+// 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>
+`;
height: 0;
overflow: hidden;
}
+
+#security_hotspots .hotspots-list-single-category .hotspot-category .hotspot-category-header {
+ color: var(--blue);
+}
* 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,
} 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) => {
+import { Standards } from '../../types/security';
/*
* SonarQube
* Copyright (C) 2009-2020 SonarSource SA
} from '../security-standard';
describe('renderCWECategory', () => {
- const standards: T.Standards = {
+ const standards: Standards = {
cwe: {
'1004': {
title: "Sensitive Cookie Without 'HttpOnly' Flag"
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"
);
});
describe('renderOwaspTop10Category', () => {
- const standards: T.Standards = {
+ const standards: Standards = {
cwe: {},
owaspTop10: {
a1: {
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');
});
describe('renderSansTop25Category', () => {
- const standards: T.Standards = {
+ const standards: Standards = {
cwe: {},
owaspTop10: {},
sansTop25: {
},
sonarsourceSecurity: {}
};
- it('should render categories correctly', () => {
+ it('should render sans categories correctly', () => {
expect(renderSansTop25Category(standards, 'insecure-interaction')).toEqual(
'Insecure Interaction Between Components'
);
});
describe('renderSonarSourceSecurityCategory', () => {
- const standards: T.Standards = {
+ const standards: Standards = {
cwe: {},
owaspTop10: {},
sansTop25: {},
}
}
};
- it('should render categories correctly', () => {
+ it('should render sonarsource categories correctly', () => {
expect(renderSonarSourceSecurityCategory(standards, 'xss')).toEqual(
'Cross-Site Scripting (XSS)'
);
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { ComponentQualifier } from '../../types/component';
+import { Standards } from '../../types/security';
import {
Hotspot,
HotspotResolution,
...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)'
+ }
+ }
+ };
+}
* 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}`;
}
export function renderOwaspTop10Category(
- standards: T.Standards,
+ standards: Standards,
category: string,
withPrefix = false
): string {
}
export function renderSansTop25Category(
- standards: T.Standards,
+ standards: Standards,
category: string,
withPrefix = false
): string {
}
export function renderSonarSourceSecurityCategory(
- standards: T.Standards,
+ standards: Standards,
category: string,
withPrefix = false
): string {
+import { pick } from 'lodash';
/*
* SonarQube
* Copyright (C) 2009-2020 SonarSource SA
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'];
* 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: {
sinceLeakPeriod,
hotspots,
assignedToMe,
- category
+ ...pick(query, [
+ SecurityStandard.SONARSOURCE,
+ SecurityStandard.OWASP_TOP10,
+ SecurityStandard.SANS_TOP25
+ ])
}
};
}
--- /dev/null
+/*
+ * 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 }>;
+};
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 {