@@ -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 | |||
); | |||
} |
@@ -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 |
@@ -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); | |||
}); |
@@ -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), |
@@ -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) => |
@@ -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)) | |||
); | |||
} |
@@ -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", |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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> | |||
)} |
@@ -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 () => { |
@@ -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} | |||
/> | |||
); |
@@ -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 {}, | |||
} | |||
} | |||
/> | |||
`; |
@@ -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 => ( |
@@ -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> | |||
); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -71,3 +71,7 @@ | |||
height: 0; | |||
overflow: hidden; | |||
} | |||
#security_hotspots .hotspots-list-single-category .hotspot-category .hotspot-category-header { | |||
color: var(--blue); | |||
} |
@@ -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) => { |
@@ -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)' | |||
); |
@@ -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)' | |||
} | |||
} | |||
}; | |||
} |
@@ -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 { |
@@ -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 | |||
]) | |||
} | |||
}; | |||
} |
@@ -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 }>; | |||
}; |
@@ -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 { |