@@ -0,0 +1,91 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { cloneDeep } from 'lodash'; | |||
import { RequestData } from '../../helpers/request'; | |||
import { getStandards } from '../../helpers/security-standard'; | |||
import { mockPaging } from '../../helpers/testMocks'; | |||
import { RawFacet, RawIssuesResponse, ReferencedComponent } from '../../types/issues'; | |||
import { Standards } from '../../types/security'; | |||
import { searchIssues } from '../issues'; | |||
function mockReferenceComponent(override?: Partial<ReferencedComponent>) { | |||
return { | |||
key: 'component1', | |||
name: 'Component1', | |||
uuid: 'id1', | |||
...override | |||
}; | |||
} | |||
export default class IssueServiceMock { | |||
isAdmin = false; | |||
standards?: Standards; | |||
constructor() { | |||
(searchIssues as jest.Mock).mockImplementation(this.listHandler); | |||
} | |||
reset() { | |||
this.setIsAdmin(false); | |||
} | |||
async getStandards(): Promise<Standards> { | |||
if (this.standards) { | |||
return this.standards; | |||
} | |||
this.standards = await getStandards(); | |||
return this.standards; | |||
} | |||
owasp2021FacetList(): RawFacet { | |||
return { | |||
property: 'owaspTop10-2021', | |||
values: [{ val: 'a1', count: 0 }] | |||
}; | |||
} | |||
setIsAdmin(isAdmin: boolean) { | |||
this.isAdmin = isAdmin; | |||
} | |||
listHandler = (query: RequestData): Promise<RawIssuesResponse> => { | |||
const facets = query.facets.split(',').map((name: string) => { | |||
if (name === 'owaspTop10-2021') { | |||
return this.owasp2021FacetList(); | |||
} | |||
return { | |||
property: name, | |||
values: [] | |||
}; | |||
}); | |||
return this.reply({ | |||
components: [mockReferenceComponent()], | |||
effortTotal: 199629, | |||
facets, | |||
issues: [], | |||
languages: [], | |||
paging: mockPaging() | |||
}); | |||
}; | |||
reply<T>(response: T): Promise<T> { | |||
return Promise.resolve(cloneDeep(response)); | |||
} | |||
} |
@@ -108,6 +108,11 @@ export class App extends React.PureComponent<Props, State> { | |||
openFacets: { | |||
languages: true, | |||
owaspTop10: shouldOpenStandardsChildFacet({}, query, SecurityStandard.OWASP_TOP10), | |||
'owaspTop10-2021': shouldOpenStandardsChildFacet( | |||
{}, | |||
query, | |||
SecurityStandard.OWASP_TOP10_2021 | |||
), | |||
sansTop25: shouldOpenStandardsChildFacet({}, query, SecurityStandard.SANS_TOP25), | |||
sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet({}, query), | |||
standards: shouldOpenStandardsFacet({}, query), |
@@ -110,6 +110,7 @@ export default function FacetsList(props: FacetsListProps) { | |||
cweStats={props.facets && props.facets.cwe} | |||
fetchingCwe={false} | |||
fetchingOwaspTop10={false} | |||
fetchingOwaspTop10-2021={false} | |||
fetchingSansTop25={false} | |||
fetchingSonarSourceSecurity={false} | |||
onChange={props.onFilterChange} | |||
@@ -118,6 +119,9 @@ export default function FacetsList(props: FacetsListProps) { | |||
owaspTop10={props.query.owaspTop10} | |||
owaspTop10Open={!!props.openFacets.owaspTop10} | |||
owaspTop10Stats={props.facets && props.facets.owaspTop10} | |||
owaspTop10-2021={props.query['owaspTop10-2021']} | |||
owaspTop10-2021Open={!!props.openFacets['owaspTop10-2021']} | |||
owaspTop10-2021Stats={props.facets && props.facets['owaspTop10-2021']} | |||
query={props.query} | |||
sansTop25={props.query.sansTop25} | |||
sansTop25Open={!!props.openFacets.sansTop25} |
@@ -16,6 +16,7 @@ exports[`renderBulkButton should show bulk change button when user has edit righ | |||
"inheritance": undefined, | |||
"languages": Array [], | |||
"owaspTop10": Array [], | |||
"owaspTop10-2021": Array [], | |||
"profile": undefined, | |||
"repositories": Array [], | |||
"ruleKey": undefined, | |||
@@ -80,6 +81,7 @@ exports[`renderBulkButton should show bulk change button when user has global ad | |||
"inheritance": undefined, | |||
"languages": Array [], | |||
"owaspTop10": Array [], | |||
"owaspTop10-2021": Array [], | |||
"profile": undefined, | |||
"repositories": Array [], | |||
"ruleKey": undefined, | |||
@@ -139,6 +141,7 @@ exports[`should render correctly: loaded (ScreenPositionHelper) 1`] = ` | |||
Object { | |||
"languages": true, | |||
"owaspTop10": false, | |||
"owaspTop10-2021": false, | |||
"sansTop25": false, | |||
"sonarsourceSecurity": false, | |||
"standards": false, | |||
@@ -155,6 +158,7 @@ exports[`should render correctly: loaded (ScreenPositionHelper) 1`] = ` | |||
"inheritance": undefined, | |||
"languages": Array [], | |||
"owaspTop10": Array [], | |||
"owaspTop10-2021": Array [], | |||
"profile": undefined, | |||
"repositories": Array [], | |||
"ruleKey": undefined, | |||
@@ -230,6 +234,7 @@ exports[`should render correctly: loaded 1`] = ` | |||
"inheritance": undefined, | |||
"languages": Array [], | |||
"owaspTop10": Array [], | |||
"owaspTop10-2021": Array [], | |||
"profile": undefined, | |||
"repositories": Array [], | |||
"ruleKey": undefined, | |||
@@ -434,6 +439,7 @@ exports[`should render correctly: open rule (ScreenPositionHelper) 1`] = ` | |||
Object { | |||
"languages": true, | |||
"owaspTop10": false, | |||
"owaspTop10-2021": false, | |||
"sansTop25": false, | |||
"sonarsourceSecurity": false, | |||
"standards": false, | |||
@@ -450,6 +456,7 @@ exports[`should render correctly: open rule (ScreenPositionHelper) 1`] = ` | |||
"inheritance": undefined, | |||
"languages": Array [], | |||
"owaspTop10": Array [], | |||
"owaspTop10-2021": Array [], | |||
"profile": undefined, | |||
"repositories": Array [], | |||
"ruleKey": undefined, |
@@ -38,11 +38,13 @@ exports[`should render correctly 1`] = ` | |||
cweOpen={false} | |||
fetchingCwe={false} | |||
fetchingOwaspTop10={false} | |||
fetchingOwaspTop10-2021={false} | |||
fetchingSansTop25={false} | |||
fetchingSonarSourceSecurity={false} | |||
onChange={[MockFunction]} | |||
onToggle={[MockFunction]} | |||
open={false} | |||
owaspTop10-2021Open={false} | |||
owaspTop10Open={false} | |||
query={Object {}} | |||
sansTop25Open={false} |
@@ -41,6 +41,7 @@ export interface Query { | |||
inheritance: RuleInheritance | undefined; | |||
languages: string[]; | |||
owaspTop10: string[]; | |||
'owaspTop10-2021': string[]; | |||
profile: string | undefined; | |||
repositories: string[]; | |||
ruleKey: string | undefined; | |||
@@ -85,6 +86,7 @@ export function parseQuery(query: RawQuery): Query { | |||
inheritance: parseAsInheritance(query.inheritance), | |||
languages: parseAsArray(query.languages, parseAsString), | |||
owaspTop10: parseAsArray(query.owaspTop10, parseAsString), | |||
'owaspTop10-2021': parseAsArray(query['owaspTop10-2021'], parseAsString), | |||
profile: parseAsOptionalString(query.qprofile), | |||
repositories: parseAsArray(query.repositories, parseAsString), | |||
ruleKey: parseAsOptionalString(query.rule_key), | |||
@@ -110,6 +112,7 @@ export function serializeQuery(query: Query): RawQuery { | |||
is_template: serializeOptionalBoolean(query.template), | |||
languages: serializeStringArray(query.languages), | |||
owaspTop10: serializeStringArray(query.owaspTop10), | |||
'owaspTop10-2021': serializeStringArray(query['owaspTop10-2021']), | |||
q: serializeString(query.searchQuery), | |||
qprofile: serializeString(query.profile), | |||
repositories: serializeStringArray(query.repositories), | |||
@@ -133,6 +136,7 @@ export function shouldRequestFacet(facet: string): facet is FacetKey { | |||
'cwe', | |||
'languages', | |||
'owaspTop10', | |||
'owaspTop10-2021', | |||
'repositories', | |||
'sansTop25', | |||
'severities', | |||
@@ -164,9 +168,8 @@ export function hasRuleKey(query: RawQuery) { | |||
function parseAsInheritance(value?: string): RuleInheritance | undefined { | |||
if (value === 'INHERITED' || value === 'NONE' || value === 'OVERRIDES') { | |||
return value; | |||
} else { | |||
return undefined; | |||
} | |||
return undefined; | |||
} | |||
function serializeInheritance(value: RuleInheritance | undefined): string | undefined { |
@@ -0,0 +1,59 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock'; | |||
import { renderOwaspTop102021Category } from '../../../helpers/security-standard'; | |||
import { renderComponentApp } from '../../../helpers/testReactTestingUtils'; | |||
import AppContainer from '../components/AppContainer'; | |||
jest.mock('../../../api/issues'); | |||
let handler: IssuesServiceMock; | |||
beforeAll(() => { | |||
handler = new IssuesServiceMock(); | |||
}); | |||
afterEach(() => handler.reset()); | |||
jest.setTimeout(10_000); | |||
it('should support OWASP Top 10 version 2021', async () => { | |||
const user = userEvent.setup(); | |||
renderIssueApp(); | |||
await user.click(await screen.findByRole('link', { name: 'issues.facet.standards' })); | |||
const owaspTop102021 = screen.getByRole('link', { name: 'issues.facet.owaspTop10_2021' }); | |||
expect(owaspTop102021).toBeInTheDocument(); | |||
await user.click(owaspTop102021); | |||
await Promise.all( | |||
handler.owasp2021FacetList().values.map(async ({ val }) => { | |||
const standard = await handler.getStandards(); | |||
/* eslint-disable-next-line testing-library/render-result-naming-convention */ | |||
const linkName = renderOwaspTop102021Category(standard, val); | |||
expect(await screen.findByRole('link', { name: linkName })).toBeInTheDocument(); | |||
}) | |||
); | |||
}); | |||
function renderIssueApp() { | |||
renderComponentApp('project/issues', AppContainer); | |||
} |
@@ -153,6 +153,11 @@ export default class App extends React.PureComponent<Props, State> { | |||
myIssues: areMyIssuesSelected(props.location.query), | |||
openFacets: { | |||
owaspTop10: shouldOpenStandardsChildFacet({}, query, SecurityStandard.OWASP_TOP10), | |||
'owaspTop10-2021': shouldOpenStandardsChildFacet( | |||
{}, | |||
query, | |||
SecurityStandard.OWASP_TOP10_2021 | |||
), | |||
sansTop25: shouldOpenStandardsChildFacet({}, query, SecurityStandard.SANS_TOP25), | |||
severities: true, | |||
sonarsourceSecurity: shouldOpenSonarSourceSecurityFacet({}, query), | |||
@@ -406,7 +411,7 @@ export default class App extends React.PureComponent<Props, State> { | |||
const facets = requestFacets | |||
? Object.keys(openFacets) | |||
.filter(facet => facet !== STANDARDS) | |||
.filter(facet => facet !== STANDARDS && openFacets[facet]) | |||
.join(',') | |||
: undefined; | |||
@@ -432,7 +437,6 @@ export default class App extends React.PureComponent<Props, State> { | |||
if (myIssues) { | |||
Object.assign(parameters, { assignees: '__me__' }); | |||
} | |||
return this.props.fetchIssues(parameters); | |||
}; | |||
@@ -363,6 +363,7 @@ it('should display the right facets open', () => { | |||
}).state('openFacets') | |||
).toEqual({ | |||
owaspTop10: false, | |||
'owaspTop10-2021': false, | |||
sansTop25: false, | |||
severities: true, | |||
standards: false, | |||
@@ -375,6 +376,7 @@ it('should display the right facets open', () => { | |||
}).state('openFacets') | |||
).toEqual({ | |||
owaspTop10: true, | |||
'owaspTop10-2021': false, | |||
sansTop25: false, | |||
severities: true, | |||
standards: true, |
@@ -99,6 +99,7 @@ exports[`should show warnning when not all projects are accessible 1`] = ` | |||
openFacets={ | |||
Object { | |||
"owaspTop10": false, | |||
"owaspTop10-2021": false, | |||
"sansTop25": false, | |||
"severities": true, | |||
"sonarsourceSecurity": false, | |||
@@ -121,6 +122,7 @@ exports[`should show warnning when not all projects are accessible 1`] = ` | |||
"issues": Array [], | |||
"languages": Array [], | |||
"owaspTop10": Array [], | |||
"owaspTop10-2021": Array [], | |||
"projects": Array [], | |||
"resolutions": Array [], | |||
"resolved": true, |
@@ -1,55 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 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 { Location } from '../../helpers/urls'; | |||
import { RawQuery } from '../../types/types'; | |||
import { areMyIssuesSelected, parseQuery, serializeQuery } from './utils'; | |||
function parseHash(hash: string) { | |||
const query: RawQuery = {}; | |||
const parts = hash.split('|'); | |||
parts.forEach(part => { | |||
const tokens = part.split('='); | |||
if (tokens.length === 2) { | |||
const property = decodeURIComponent(tokens[0]); | |||
const value = decodeURIComponent(tokens[1]); | |||
if (property === 'assigned_to_me' && value === 'true') { | |||
query.myIssues = 'true'; | |||
} else { | |||
query[property] = value; | |||
} | |||
} | |||
}); | |||
return query; | |||
} | |||
export function onEnter(state: any, replace: (location: Location) => void) { | |||
const { hash } = window.location; | |||
if (hash.length > 1) { | |||
const query = parseHash(hash.substr(1)); | |||
const normalizedQuery = { | |||
...serializeQuery(parseQuery(query)), | |||
myIssues: areMyIssuesSelected(query) ? 'true' : undefined | |||
}; | |||
replace({ | |||
pathname: state.location.pathname, | |||
query: normalizedQuery | |||
}); | |||
} | |||
} |
@@ -180,6 +180,7 @@ export class Sidebar extends React.PureComponent<Props> { | |||
cweStats={facets.cwe} | |||
fetchingCwe={this.props.loadingFacets.cwe === true} | |||
fetchingOwaspTop10={this.props.loadingFacets.owaspTop10 === true} | |||
fetchingOwaspTop10-2021={this.props.loadingFacets['owaspTop10-2021'] === true} | |||
fetchingSansTop25={this.props.loadingFacets.sansTop25 === true} | |||
fetchingSonarSourceSecurity={this.props.loadingFacets.sonarsourceSecurity === true} | |||
loadSearchResultCount={this.props.loadSearchResultCount} | |||
@@ -189,6 +190,9 @@ export class Sidebar extends React.PureComponent<Props> { | |||
owaspTop10={query.owaspTop10} | |||
owaspTop10Open={!!openFacets.owaspTop10} | |||
owaspTop10Stats={facets.owaspTop10} | |||
owaspTop10-2021={query['owaspTop10-2021']} | |||
owaspTop10-2021Open={!!openFacets['owaspTop10-2021']} | |||
owaspTop10-2021Stats={facets['owaspTop10-2021']} | |||
query={query} | |||
sansTop25={query.sansTop25} | |||
sansTop25Open={!!openFacets.sansTop25} |
@@ -30,6 +30,7 @@ import { highlightTerm } from '../../../helpers/search'; | |||
import { | |||
getStandards, | |||
renderCWECategory, | |||
renderOwaspTop102021Category, | |||
renderOwaspTop10Category, | |||
renderSansTop25Category, | |||
renderSonarSourceSecurityCategory | |||
@@ -45,6 +46,7 @@ interface Props { | |||
cweStats: Dict<number> | undefined; | |||
fetchingCwe: boolean; | |||
fetchingOwaspTop10: boolean; | |||
'fetchingOwaspTop10-2021': boolean; | |||
fetchingSansTop25: boolean; | |||
fetchingSonarSourceSecurity: boolean; | |||
loadSearchResultCount?: (property: string, changes: Partial<Query>) => Promise<Facet>; | |||
@@ -54,6 +56,9 @@ interface Props { | |||
owaspTop10: string[]; | |||
owaspTop10Open: boolean; | |||
owaspTop10Stats: Dict<number> | undefined; | |||
'owaspTop10-2021': string[]; | |||
'owaspTop10-2021Open': boolean; | |||
'owaspTop10-2021Stats': Dict<number> | undefined; | |||
query: Partial<Query>; | |||
sansTop25: string[]; | |||
sansTop25Open: boolean; | |||
@@ -67,14 +72,25 @@ interface State { | |||
standards: Standards; | |||
} | |||
type StatsProp = 'owaspTop10Stats' | 'cweStats' | 'sansTop25Stats' | 'sonarsourceSecurityStats'; | |||
type StatsProp = | |||
| 'owaspTop10-2021Stats' | |||
| 'owaspTop10Stats' | |||
| 'cweStats' | |||
| 'sansTop25Stats' | |||
| 'sonarsourceSecurityStats'; | |||
type ValuesProp = StandardType; | |||
export default class StandardFacet extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
property = STANDARDS; | |||
state: State = { | |||
standards: { owaspTop10: {}, sansTop25: {}, cwe: {}, sonarsourceSecurity: {} } | |||
standards: { | |||
owaspTop10: {}, | |||
'owaspTop10-2021': {}, | |||
sansTop25: {}, | |||
cwe: {}, | |||
sonarsourceSecurity: {} | |||
} | |||
}; | |||
componentDidMount() { | |||
@@ -84,6 +100,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> { | |||
if ( | |||
this.props.open || | |||
this.props.owaspTop10.length > 0 || | |||
this.props['owaspTop10-2021'].length > 0 || | |||
this.props.cwe.length > 0 || | |||
this.props.sansTop25.length > 0 || | |||
this.props.sonarsourceSecurity.length > 0 | |||
@@ -104,9 +121,23 @@ export default class StandardFacet extends React.PureComponent<Props, State> { | |||
loadStandards = () => { | |||
getStandards().then( | |||
({ owaspTop10, sansTop25, cwe, sonarsourceSecurity }: Standards) => { | |||
({ | |||
'owaspTop10-2021': owaspTop102021, | |||
owaspTop10, | |||
sansTop25, | |||
cwe, | |||
sonarsourceSecurity | |||
}: Standards) => { | |||
if (this.mounted) { | |||
this.setState({ standards: { owaspTop10, sansTop25, cwe, sonarsourceSecurity } }); | |||
this.setState({ | |||
standards: { | |||
'owaspTop10-2021': owaspTop102021, | |||
owaspTop10, | |||
sansTop25, | |||
cwe, | |||
sonarsourceSecurity | |||
} | |||
}); | |||
} | |||
}, | |||
() => {} | |||
@@ -121,6 +152,9 @@ export default class StandardFacet extends React.PureComponent<Props, State> { | |||
...this.props.owaspTop10.map(item => | |||
renderOwaspTop10Category(this.state.standards, item, true) | |||
), | |||
...this.props['owaspTop10-2021'].map(item => | |||
renderOwaspTop102021Category(this.state.standards, item, true) | |||
), | |||
...this.props.sansTop25.map(item => | |||
renderSansTop25Category(this.state.standards, item, true) | |||
), | |||
@@ -136,6 +170,10 @@ export default class StandardFacet extends React.PureComponent<Props, State> { | |||
this.props.onToggle('owaspTop10'); | |||
}; | |||
handleOwaspTop102021HeaderClick = () => { | |||
this.props.onToggle('owaspTop10-2021'); | |||
}; | |||
handleSansTop25HeaderClick = () => { | |||
this.props.onToggle('sansTop25'); | |||
}; | |||
@@ -148,6 +186,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> { | |||
this.props.onChange({ | |||
[this.property]: [], | |||
owaspTop10: [], | |||
'owaspTop10-2021': [], | |||
sansTop25: [], | |||
cwe: [], | |||
sonarsourceSecurity: [] | |||
@@ -172,6 +211,10 @@ export default class StandardFacet extends React.PureComponent<Props, State> { | |||
this.handleItemClick(SecurityStandard.OWASP_TOP10, itemValue, multiple); | |||
}; | |||
handleOwaspTop102021ItemClick = (itemValue: string, multiple: boolean) => { | |||
this.handleItemClick(SecurityStandard.OWASP_TOP10_2021, itemValue, multiple); | |||
}; | |||
handleSansTop25ItemClick = (itemValue: string, multiple: boolean) => { | |||
this.handleItemClick(SecurityStandard.SANS_TOP25, itemValue, multiple); | |||
}; | |||
@@ -265,8 +308,13 @@ export default class StandardFacet extends React.PureComponent<Props, State> { | |||
); | |||
} | |||
renderOwaspTop10Hint() { | |||
return this.renderHint('owaspTop10Stats', SecurityStandard.OWASP_TOP10); | |||
renderOwaspTop102021List() { | |||
return this.renderList( | |||
'owaspTop10-2021Stats', | |||
SecurityStandard.OWASP_TOP10_2021, | |||
renderOwaspTop102021Category, | |||
this.handleOwaspTop102021ItemClick | |||
); | |||
} | |||
renderSansTop25List() { | |||
@@ -278,10 +326,6 @@ export default class StandardFacet extends React.PureComponent<Props, State> { | |||
); | |||
} | |||
renderSansTop25Hint() { | |||
return this.renderHint('sansTop25Stats', SecurityStandard.SANS_TOP25); | |||
} | |||
renderSonarSourceSecurityList() { | |||
return this.renderList( | |||
'sonarsourceSecurityStats', | |||
@@ -291,6 +335,18 @@ export default class StandardFacet extends React.PureComponent<Props, State> { | |||
); | |||
} | |||
renderOwaspTop10Hint() { | |||
return this.renderHint('owaspTop10Stats', SecurityStandard.OWASP_TOP10); | |||
} | |||
renderOwaspTop102021Hint() { | |||
return this.renderHint('owaspTop10-2021Stats', SecurityStandard.OWASP_TOP10_2021); | |||
} | |||
renderSansTop25Hint() { | |||
return this.renderHint('sansTop25Stats', SecurityStandard.SANS_TOP25); | |||
} | |||
renderSonarSourceSecurityHint() { | |||
return this.renderHint('sonarsourceSecurityStats', SecurityStandard.SONARSOURCE); | |||
} | |||
@@ -315,6 +371,23 @@ export default class StandardFacet extends React.PureComponent<Props, State> { | |||
</> | |||
)} | |||
</FacetBox> | |||
<FacetBox className="is-inner" property={SecurityStandard.OWASP_TOP10_2021}> | |||
<FacetHeader | |||
fetching={this.props['fetchingOwaspTop10-2021']} | |||
name={translate('issues.facet.owaspTop10_2021')} | |||
onClick={this.handleOwaspTop102021HeaderClick} | |||
open={this.props['owaspTop10-2021Open']} | |||
values={this.props['owaspTop10-2021'].map(item => | |||
renderOwaspTop102021Category(this.state.standards, item) | |||
)} | |||
/> | |||
{this.props['owaspTop10-2021Open'] && ( | |||
<> | |||
{this.renderOwaspTop102021List()} | |||
{this.renderOwaspTop102021Hint()} | |||
</> | |||
)} | |||
</FacetBox> | |||
<FacetBox className="is-inner" property={SecurityStandard.OWASP_TOP10}> | |||
<FacetHeader | |||
fetching={this.props.fetchingOwaspTop10} |
@@ -78,6 +78,7 @@ it('should clear standards facet', () => { | |||
expect(onChange).toBeCalledWith({ | |||
cwe: [], | |||
owaspTop10: [], | |||
'owaspTop10-2021': [], | |||
sansTop25: [], | |||
sonarsourceSecurity: [], | |||
standards: [] | |||
@@ -161,6 +162,7 @@ it('should display correct selection', () => { | |||
const wrapper = shallowRender({ | |||
open: true, | |||
owaspTop10: ['a1', 'a3'], | |||
'owaspTop10-2021': ['a1', 'a2'], | |||
sansTop25: ['risky-resource', 'foo'], | |||
cwe: ['42', '1111', 'unknown'], | |||
sonarsourceSecurity: ['sql-injection', 'others'] | |||
@@ -170,6 +172,8 @@ it('should display correct selection', () => { | |||
'Others', | |||
'OWASP A1 - a1 title', | |||
'OWASP A3', | |||
'OWASP A1 - a1 title', | |||
'OWASP A2', | |||
'SANS Risky Resource Management', | |||
'SANS foo', | |||
'CWE-42 - cwe-42 title', | |||
@@ -177,6 +181,7 @@ it('should display correct selection', () => { | |||
'Unknown CWE' | |||
]); | |||
checkValues('owaspTop10', ['A1 - a1 title', 'A3']); | |||
checkValues('owaspTop10-2021', ['A1 - a1 title', 'A2']); | |||
checkValues('sansTop25', ['Risky Resource Management', 'foo']); | |||
checkValues('sonarsourceSecurity', ['SQL Injection', 'Others']); | |||
@@ -198,6 +203,7 @@ function shallowRender(props: Partial<StandardFacet['props']> = {}) { | |||
cweStats={{}} | |||
fetchingCwe={false} | |||
fetchingOwaspTop10={false} | |||
fetchingOwaspTop10-2021={false} | |||
fetchingSansTop25={false} | |||
fetchingSonarSourceSecurity={false} | |||
loadSearchResultCount={jest.fn()} | |||
@@ -207,6 +213,9 @@ function shallowRender(props: Partial<StandardFacet['props']> = {}) { | |||
owaspTop10={[]} | |||
owaspTop10Open={false} | |||
owaspTop10Stats={{}} | |||
owaspTop10-2021={[]} | |||
owaspTop10-2021Open={false} | |||
owaspTop10-2021Stats={{}} | |||
query={{} as Query} | |||
sansTop25={[]} | |||
sansTop25Open={false} | |||
@@ -220,6 +229,7 @@ function shallowRender(props: Partial<StandardFacet['props']> = {}) { | |||
wrapper.setState({ | |||
standards: { | |||
owaspTop10: { a1: { title: 'a1 title' } }, | |||
'owaspTop10-2021': { a1: { title: 'a1 title' } }, | |||
sansTop25: { 'risky-resource': { title: 'Risky Resource Management' } }, | |||
cwe: { 42: { title: 'cwe-42 title' }, unknown: { title: 'Unknown CWE' } }, | |||
sonarsourceSecurity: { |
@@ -90,6 +90,18 @@ exports[`should render sub-facets 1`] = ` | |||
values={1} | |||
/> | |||
</FacetBox> | |||
<FacetBox | |||
className="is-inner" | |||
property="owaspTop10-2021" | |||
> | |||
<FacetHeader | |||
fetching={false} | |||
name="issues.facet.owaspTop10_2021" | |||
onClick={[Function]} | |||
open={false} | |||
values={Array []} | |||
/> | |||
</FacetBox> | |||
<FacetBox | |||
className="is-inner" | |||
property="owaspTop10" |
@@ -53,6 +53,7 @@ export interface Query { | |||
issues: string[]; | |||
languages: string[]; | |||
owaspTop10: string[]; | |||
'owaspTop10-2021': string[]; | |||
projects: string[]; | |||
resolutions: string[]; | |||
resolved: boolean; | |||
@@ -95,6 +96,7 @@ export function parseQuery(query: RawQuery): Query { | |||
issues: parseAsArray(query.issues, parseAsString), | |||
languages: parseAsArray(query.languages, parseAsString), | |||
owaspTop10: parseAsArray(query.owaspTop10, parseAsString), | |||
'owaspTop10-2021': parseAsArray(query['owaspTop10-2021'], parseAsString), | |||
projects: parseAsArray(query.projects, parseAsString), | |||
resolutions: parseAsArray(query.resolutions, parseAsString), | |||
resolved: parseAsBoolean(query.resolved), | |||
@@ -132,6 +134,7 @@ export function serializeQuery(query: Query): RawQuery { | |||
issues: serializeStringArray(query.issues), | |||
languages: serializeStringArray(query.languages), | |||
owaspTop10: serializeStringArray(query.owaspTop10), | |||
'owaspTop10-2021': serializeStringArray(query['owaspTop10-2021']), | |||
projects: serializeStringArray(query.projects), | |||
resolutions: serializeStringArray(query.resolutions), | |||
resolved: query.resolved ? undefined : 'false', |
@@ -98,6 +98,7 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { | |||
selectedHotspot: undefined, | |||
standards: { | |||
[SecurityStandard.OWASP_TOP10]: {}, | |||
[SecurityStandard.OWASP_TOP10_2021]: {}, | |||
[SecurityStandard.SANS_TOP25]: {}, | |||
[SecurityStandard.SONARSOURCE]: {}, | |||
[SecurityStandard.CWE]: {} |
@@ -58,6 +58,7 @@ exports[`should render correctly 1`] = ` | |||
Object { | |||
"cwe": Object {}, | |||
"owaspTop10": Object {}, | |||
"owaspTop10-2021": Object {}, | |||
"sansTop25": Object {}, | |||
"sonarsourceSecurity": Object {}, | |||
} |
@@ -157,6 +157,17 @@ exports[`should render correctly when filtered by category or cwe: category 1`] | |||
"title": "Sensitive Data Exposure", | |||
}, | |||
}, | |||
"owaspTop10-2021": Object { | |||
"a1": Object { | |||
"title": "Injection", | |||
}, | |||
"a2": Object { | |||
"title": "Broken Authentication", | |||
}, | |||
"a3": Object { | |||
"title": "Sensitive Data Exposure", | |||
}, | |||
}, | |||
"sansTop25": Object { | |||
"insecure-interaction": Object { | |||
"title": "Insecure Interaction Between Components", | |||
@@ -278,6 +289,17 @@ exports[`should render correctly when filtered by category or cwe: cwe 1`] = ` | |||
"title": "Sensitive Data Exposure", | |||
}, | |||
}, | |||
"owaspTop10-2021": Object { | |||
"a1": Object { | |||
"title": "Injection", | |||
}, | |||
"a2": Object { | |||
"title": "Broken Authentication", | |||
}, | |||
"a3": Object { | |||
"title": "Sensitive Data Exposure", | |||
}, | |||
}, | |||
"sansTop25": Object { | |||
"insecure-interaction": Object { | |||
"title": "Insecure Interaction Between Components", |
@@ -73,6 +73,10 @@ function shallowRender(props: Partial<HotspotSimpleListProps> = {}) { | |||
a1: { title: 'A1 - SQL Injection' }, | |||
a3: { title: 'A3 - Sensitive Data Exposure' } | |||
}, | |||
'owaspTop10-2021': { | |||
a1: { title: 'A1 - SQL Injection' }, | |||
a3: { title: 'A3 - Sensitive Data Exposure' } | |||
}, | |||
sansTop25: {}, | |||
sonarsourceSecurity: {} | |||
}} |
@@ -20,6 +20,7 @@ | |||
import { flatten, groupBy, sortBy } from 'lodash'; | |||
import { | |||
renderCWECategory, | |||
renderOwaspTop102021Category, | |||
renderOwaspTop10Category, | |||
renderSansTop25Category, | |||
renderSonarSourceSecurityCategory | |||
@@ -47,12 +48,14 @@ export const RISK_EXPOSURE_LEVELS = [RiskExposure.HIGH, RiskExposure.MEDIUM, Ris | |||
export const SECURITY_STANDARDS = [ | |||
SecurityStandard.SONARSOURCE, | |||
SecurityStandard.OWASP_TOP10, | |||
SecurityStandard.OWASP_TOP10_2021, | |||
SecurityStandard.SANS_TOP25, | |||
SecurityStandard.CWE | |||
]; | |||
export const SECURITY_STANDARD_RENDERER = { | |||
[SecurityStandard.OWASP_TOP10]: renderOwaspTop10Category, | |||
[SecurityStandard.OWASP_TOP10_2021]: renderOwaspTop102021Category, | |||
[SecurityStandard.SANS_TOP25]: renderSansTop25Category, | |||
[SecurityStandard.SONARSOURCE]: renderSonarSourceSecurityCategory, | |||
[SecurityStandard.CWE]: renderCWECategory |
@@ -20,6 +20,7 @@ | |||
import { Standards } from '../../types/security'; | |||
import { | |||
renderCWECategory, | |||
renderOwaspTop102021Category, | |||
renderOwaspTop10Category, | |||
renderSansTop25Category, | |||
renderSonarSourceSecurityCategory | |||
@@ -36,6 +37,7 @@ describe('renderCWECategory', () => { | |||
} | |||
}, | |||
owaspTop10: {}, | |||
'owaspTop10-2021': {}, | |||
sansTop25: {}, | |||
sonarsourceSecurity: {} | |||
}; | |||
@@ -56,6 +58,7 @@ describe('renderOwaspTop10Category', () => { | |||
title: 'Injection' | |||
} | |||
}, | |||
'owaspTop10-2021': {}, | |||
sansTop25: {}, | |||
sonarsourceSecurity: {} | |||
}; | |||
@@ -67,10 +70,31 @@ describe('renderOwaspTop10Category', () => { | |||
}); | |||
}); | |||
describe('renderOwaspTop102021Category', () => { | |||
const standards: Standards = { | |||
cwe: {}, | |||
owaspTop10: {}, | |||
'owaspTop10-2021': { | |||
a1: { | |||
title: 'Injection' | |||
} | |||
}, | |||
sansTop25: {}, | |||
sonarsourceSecurity: {} | |||
}; | |||
it('should render owasp categories correctly', () => { | |||
expect(renderOwaspTop102021Category(standards, 'a1')).toEqual('A1 - Injection'); | |||
expect(renderOwaspTop102021Category(standards, 'a1', true)).toEqual('OWASP A1 - Injection'); | |||
expect(renderOwaspTop102021Category(standards, 'a2')).toEqual('A2'); | |||
expect(renderOwaspTop102021Category(standards, 'a2', true)).toEqual('OWASP A2'); | |||
}); | |||
}); | |||
describe('renderSansTop25Category', () => { | |||
const standards: Standards = { | |||
cwe: {}, | |||
owaspTop10: {}, | |||
'owaspTop10-2021': {}, | |||
sansTop25: { | |||
'insecure-interaction': { | |||
title: 'Insecure Interaction Between Components' | |||
@@ -94,6 +118,7 @@ describe('renderSonarSourceSecurityCategory', () => { | |||
const standards: Standards = { | |||
cwe: {}, | |||
owaspTop10: {}, | |||
'owaspTop10-2021': {}, | |||
sansTop25: {}, | |||
sonarsourceSecurity: { | |||
xss: { |
@@ -154,6 +154,17 @@ export function mockStandards(): Standards { | |||
title: 'Sensitive Data Exposure' | |||
} | |||
}, | |||
'owaspTop10-2021': { | |||
a1: { | |||
title: 'Injection' | |||
}, | |||
a2: { | |||
title: 'Broken Authentication' | |||
}, | |||
a3: { | |||
title: 'Sensitive Data Exposure' | |||
} | |||
}, | |||
sansTop25: { | |||
'insecure-interaction': { | |||
title: 'Insecure Interaction Between Components' |
@@ -39,12 +39,28 @@ export function renderOwaspTop10Category( | |||
category: string, | |||
withPrefix = false | |||
): string { | |||
const record = standards.owaspTop10[category]; | |||
return renderOwaspCategory('owaspTop10', standards, category, withPrefix); | |||
} | |||
export function renderOwaspTop102021Category( | |||
standards: Standards, | |||
category: string, | |||
withPrefix = false | |||
): string { | |||
return renderOwaspCategory('owaspTop10-2021', standards, category, withPrefix); | |||
} | |||
function renderOwaspCategory( | |||
type: 'owaspTop10' | 'owaspTop10-2021', | |||
standards: Standards, | |||
category: string, | |||
withPrefix: boolean | |||
) { | |||
const record = standards[type][category]; | |||
if (!record) { | |||
return addPrefix(category.toUpperCase(), 'OWASP', withPrefix); | |||
} else { | |||
return addPrefix(`${category.toUpperCase()} - ${record.title}`, 'OWASP', withPrefix); | |||
} | |||
return addPrefix(`${category.toUpperCase()} - ${record.title}`, 'OWASP', withPrefix); | |||
} | |||
export function renderSansTop25Category( |
@@ -23,29 +23,53 @@ import * as React from 'react'; | |||
import { HelmetProvider } from 'react-helmet-async'; | |||
import { IntlProvider } from 'react-intl'; | |||
import { Provider } from 'react-redux'; | |||
import { createMemoryHistory, RouteConfig, Router } from 'react-router'; | |||
import { createMemoryHistory, Route, RouteComponent, RouteConfig, Router } from 'react-router'; | |||
import { Store } from 'redux'; | |||
import AppStateContextProvider from '../app/components/app-state/AppStateContextProvider'; | |||
import CurrentUserContextProvider from '../app/components/current-user/CurrentUserContextProvider'; | |||
import { MetricsContext } from '../app/components/metrics/MetricsContext'; | |||
import getStore from '../app/utils/getStore'; | |||
import { RouteWithChildRoutes } from '../app/utils/startReactApp'; | |||
import { Store as State } from '../store/rootReducer'; | |||
import { AppState } from '../types/appstate'; | |||
import { Dict, Metric } from '../types/types'; | |||
import { CurrentUser } from '../types/users'; | |||
import { DEFAULT_METRICS } from './mocks/metrics'; | |||
import { mockAppState } from './testMocks'; | |||
import { mockAppState, mockCurrentUser } from './testMocks'; | |||
interface RenderContext { | |||
metrics?: Dict<Metric>; | |||
store?: Store<State, any>; | |||
history?: History; | |||
appState?: AppState; | |||
currentUser?: CurrentUser; | |||
} | |||
export function renderComponentApp( | |||
indexPath: string, | |||
component: RouteComponent, | |||
context: RenderContext = {} | |||
): RenderResult { | |||
return renderRoutedApp(<Route path={indexPath} component={component} />, indexPath, context); | |||
} | |||
export function renderApp( | |||
indexPath: string, | |||
routes: RouteConfig, | |||
context: RenderContext | |||
): RenderResult { | |||
return renderRoutedApp( | |||
<RouteWithChildRoutes path={indexPath} childRoutes={routes} />, | |||
indexPath, | |||
context | |||
); | |||
} | |||
function renderRoutedApp( | |||
children: React.ReactElement, | |||
indexPath: string, | |||
{ | |||
currentUser = mockCurrentUser(), | |||
metrics = DEFAULT_METRICS, | |||
store = getStore(), | |||
appState = mockAppState(), | |||
@@ -58,11 +82,11 @@ export function renderApp( | |||
<IntlProvider defaultLocale="en" locale="en"> | |||
<MetricsContext.Provider value={metrics}> | |||
<Provider store={store}> | |||
<AppStateContextProvider appState={appState}> | |||
<Router history={history}> | |||
<RouteWithChildRoutes path={indexPath} childRoutes={routes} /> | |||
</Router> | |||
</AppStateContextProvider> | |||
<CurrentUserContextProvider currentUser={currentUser}> | |||
<AppStateContextProvider appState={appState}> | |||
<Router history={history}>{children}</Router> | |||
</AppStateContextProvider> | |||
</CurrentUserContextProvider> | |||
</Provider> | |||
</MetricsContext.Provider> | |||
</IntlProvider> |
@@ -20,6 +20,7 @@ | |||
import { Dict } from './types'; | |||
export enum SecurityStandard { | |||
OWASP_TOP10_2021 = 'owaspTop10-2021', | |||
OWASP_TOP10 = 'owaspTop10', | |||
SANS_TOP25 = 'sansTop25', | |||
SONARSOURCE = 'sonarsourceSecurity', |
@@ -966,7 +966,8 @@ issues.facet.mode=Display Mode | |||
issues.facet.mode.count=Issues | |||
issues.facet.mode.effort=Effort | |||
issues.facet.standards=Security Category | |||
issues.facet.owaspTop10=OWASP Top 10 | |||
issues.facet.owaspTop10=OWASP Top 10 2017 | |||
issues.facet.owaspTop10_2021=OWASP Top 10 2021 | |||
issues.facet.sansTop25=SANS Top 25 | |||
issues.facet.sonarsourceSecurity=SonarSource | |||
issues.facet.cwe=CWE |