@@ -23,8 +23,10 @@ import { BranchParameters } from '../types/branch-like'; | |||
import { | |||
DetailedHotspot, | |||
HotspotAssignRequest, | |||
HotspotResolution, | |||
HotspotSearchResponse, | |||
HotspotSetStatusRequest | |||
HotspotSetStatusRequest, | |||
HotspotStatus | |||
} from '../types/security-hotspots'; | |||
export function assignSecurityHotspot( | |||
@@ -48,6 +50,8 @@ export function getSecurityHotspots( | |||
projectKey: string; | |||
p: number; | |||
ps: number; | |||
status?: HotspotStatus; | |||
resolution?: HotspotResolution; | |||
} & BranchParameters | |||
): Promise<HotspotSearchResponse> { | |||
return getJSON('/api/hotspots/search', data).catch(throwGlobalError); |
@@ -23,7 +23,13 @@ import { getSecurityHotspots } from '../../api/security-hotspots'; | |||
import { getBranchLikeQuery } from '../../helpers/branch-like'; | |||
import { getStandards } from '../../helpers/security-standard'; | |||
import { BranchLike } from '../../types/branch-like'; | |||
import { HotspotUpdate, RawHotspot } from '../../types/security-hotspots'; | |||
import { | |||
HotspotResolution, | |||
HotspotStatus, | |||
HotspotStatusFilters, | |||
HotspotUpdate, | |||
RawHotspot | |||
} from '../../types/security-hotspots'; | |||
import SecurityHotspotsAppRenderer from './SecurityHotspotsAppRenderer'; | |||
import './styles.css'; | |||
import { sortHotspots } from './utils'; | |||
@@ -40,6 +46,7 @@ interface State { | |||
loading: boolean; | |||
securityCategories: T.StandardSecurityCategories; | |||
selectedHotspotKey: string | undefined; | |||
statusFilter: HotspotStatusFilters; | |||
} | |||
export default class SecurityHotspotsApp extends React.PureComponent<Props, State> { | |||
@@ -48,7 +55,8 @@ export default class SecurityHotspotsApp extends React.PureComponent<Props, Stat | |||
loading: true, | |||
hotspots: [], | |||
securityCategories: {}, | |||
selectedHotspotKey: undefined | |||
selectedHotspotKey: undefined, | |||
statusFilter: HotspotStatusFilters.TO_REVIEW | |||
}; | |||
componentDidMount() { | |||
@@ -68,18 +76,14 @@ export default class SecurityHotspotsApp extends React.PureComponent<Props, Stat | |||
this.mounted = false; | |||
} | |||
fetchInitialData() { | |||
const { branchLike, component } = this.props; | |||
handleCallFailure = () => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}; | |||
return Promise.all([ | |||
getStandards(), | |||
getSecurityHotspots({ | |||
projectKey: component.key, | |||
p: 1, | |||
ps: PAGE_SIZE, | |||
...getBranchLikeQuery(branchLike) | |||
}) | |||
]) | |||
fetchInitialData() { | |||
return Promise.all([getStandards(), this.fetchSecurityHotspots()]) | |||
.then(([{ sonarsourceSecurity }, response]) => { | |||
if (!this.mounted) { | |||
return; | |||
@@ -94,13 +98,57 @@ export default class SecurityHotspotsApp extends React.PureComponent<Props, Stat | |||
selectedHotspotKey: hotspots.length > 0 ? hotspots[0].key : undefined | |||
}); | |||
}) | |||
.catch(() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}); | |||
.catch(this.handleCallFailure); | |||
} | |||
fetchSecurityHotspots() { | |||
const { branchLike, component } = this.props; | |||
const { statusFilter } = this.state; | |||
const status = | |||
statusFilter === HotspotStatusFilters.TO_REVIEW | |||
? HotspotStatus.TO_REVIEW | |||
: HotspotStatus.REVIEWED; | |||
const resolution = | |||
statusFilter === HotspotStatusFilters.TO_REVIEW ? undefined : HotspotResolution[statusFilter]; | |||
return getSecurityHotspots({ | |||
projectKey: component.key, | |||
p: 1, | |||
ps: PAGE_SIZE, | |||
status, | |||
resolution, | |||
...getBranchLikeQuery(branchLike) | |||
}); | |||
} | |||
reloadSecurityHotspotList = () => { | |||
const { securityCategories } = this.state; | |||
this.setState({ loading: true }); | |||
return this.fetchSecurityHotspots() | |||
.then(response => { | |||
if (!this.mounted) { | |||
return; | |||
} | |||
const hotspots = sortHotspots(response.hotspots, securityCategories); | |||
this.setState({ | |||
hotspots, | |||
loading: false, | |||
selectedHotspotKey: hotspots.length > 0 ? hotspots[0].key : undefined | |||
}); | |||
}) | |||
.catch(this.handleCallFailure); | |||
}; | |||
handleChangeStatusFilter = (statusFilter: HotspotStatusFilters) => { | |||
this.setState({ statusFilter }, this.reloadSecurityHotspotList); | |||
}; | |||
handleHotspotClick = (key: string) => this.setState({ selectedHotspotKey: key }); | |||
handleHotspotUpdate = ({ key, status, resolution }: HotspotUpdate) => { | |||
@@ -122,17 +170,19 @@ export default class SecurityHotspotsApp extends React.PureComponent<Props, Stat | |||
render() { | |||
const { branchLike } = this.props; | |||
const { hotspots, loading, securityCategories, selectedHotspotKey } = this.state; | |||
const { hotspots, loading, securityCategories, selectedHotspotKey, statusFilter } = this.state; | |||
return ( | |||
<SecurityHotspotsAppRenderer | |||
branchLike={branchLike} | |||
hotspots={hotspots} | |||
loading={loading} | |||
onChangeStatusFilter={this.handleChangeStatusFilter} | |||
onHotspotClick={this.handleHotspotClick} | |||
onUpdateHotspot={this.handleHotspotUpdate} | |||
securityCategories={securityCategories} | |||
selectedHotspotKey={selectedHotspotKey} | |||
statusFilter={statusFilter} | |||
/> | |||
); | |||
} |
@@ -27,7 +27,7 @@ import A11ySkipTarget from '../../app/components/a11y/A11ySkipTarget'; | |||
import Suggestions from '../../app/components/embed-docs-modal/Suggestions'; | |||
import ScreenPositionHelper from '../../components/common/ScreenPositionHelper'; | |||
import { BranchLike } from '../../types/branch-like'; | |||
import { HotspotUpdate, RawHotspot } from '../../types/security-hotspots'; | |||
import { HotspotStatusFilters, HotspotUpdate, RawHotspot } from '../../types/security-hotspots'; | |||
import FilterBar from './components/FilterBar'; | |||
import HotspotList from './components/HotspotList'; | |||
import HotspotViewer from './components/HotspotViewer'; | |||
@@ -37,18 +37,27 @@ export interface SecurityHotspotsAppRendererProps { | |||
branchLike?: BranchLike; | |||
hotspots: RawHotspot[]; | |||
loading: boolean; | |||
onChangeStatusFilter: (status: HotspotStatusFilters) => void; | |||
onHotspotClick: (key: string) => void; | |||
onUpdateHotspot: (hotspot: HotspotUpdate) => void; | |||
selectedHotspotKey?: string; | |||
securityCategories: T.StandardSecurityCategories; | |||
statusFilter: HotspotStatusFilters; | |||
} | |||
export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) { | |||
const { branchLike, hotspots, loading, securityCategories, selectedHotspotKey } = props; | |||
const { | |||
branchLike, | |||
hotspots, | |||
loading, | |||
securityCategories, | |||
selectedHotspotKey, | |||
statusFilter | |||
} = props; | |||
return ( | |||
<div id="security_hotspots"> | |||
<FilterBar /> | |||
<FilterBar onChangeStatus={props.onChangeStatusFilter} statusFilter={statusFilter} /> | |||
<ScreenPositionHelper> | |||
{({ top }) => ( | |||
<div className="wrapper" style={{ top }}> | |||
@@ -85,6 +94,7 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe | |||
onHotspotClick={props.onHotspotClick} | |||
securityCategories={securityCategories} | |||
selectedHotspotKey={selectedHotspotKey} | |||
statusFilter={statusFilter} | |||
/> | |||
</div> | |||
<div className="main"> |
@@ -26,7 +26,11 @@ import { mockBranch } from '../../../helpers/mocks/branch-like'; | |||
import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots'; | |||
import { getStandards } from '../../../helpers/security-standard'; | |||
import { mockComponent } from '../../../helpers/testMocks'; | |||
import { HotspotResolution, HotspotStatus } from '../../../types/security-hotspots'; | |||
import { | |||
HotspotResolution, | |||
HotspotStatus, | |||
HotspotStatusFilters | |||
} from '../../../types/security-hotspots'; | |||
import SecurityHotspotsApp from '../SecurityHotspotsApp'; | |||
import SecurityHotspotsAppRenderer from '../SecurityHotspotsAppRenderer'; | |||
@@ -54,7 +58,7 @@ it('should load data correctly', async () => { | |||
(getStandards as jest.Mock).mockResolvedValue({ sonarsourceSecurity }); | |||
const hotspots = [mockRawHotspot()]; | |||
(getSecurityHotspots as jest.Mock).mockResolvedValue({ | |||
(getSecurityHotspots as jest.Mock).mockResolvedValueOnce({ | |||
hotspots | |||
}); | |||
@@ -104,6 +108,45 @@ it('should handle hotspot update', async () => { | |||
}); | |||
}); | |||
it('should handle status filter change', async () => { | |||
const hotspots = [mockRawHotspot({ key: 'key1' })]; | |||
const hotspots2 = [mockRawHotspot({ key: 'key2' })]; | |||
(getSecurityHotspots as jest.Mock) | |||
.mockResolvedValueOnce({ hotspots }) | |||
.mockResolvedValueOnce({ hotspots: hotspots2 }) | |||
.mockResolvedValueOnce({ hotspots: [] }); | |||
const wrapper = shallowRender(); | |||
expect(getSecurityHotspots).toBeCalledWith( | |||
expect.objectContaining({ status: HotspotStatus.TO_REVIEW, resolution: undefined }) | |||
); | |||
await waitAndUpdate(wrapper); | |||
// Set filter to SAFE: | |||
wrapper.instance().handleChangeStatusFilter(HotspotStatusFilters.SAFE); | |||
expect(getSecurityHotspots).toBeCalledWith( | |||
expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.SAFE }) | |||
); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().hotspots[0]).toBe(hotspots2[0]); | |||
// Set filter to FIXED | |||
wrapper.instance().handleChangeStatusFilter(HotspotStatusFilters.FIXED); | |||
expect(getSecurityHotspots).toBeCalledWith( | |||
expect.objectContaining({ status: HotspotStatus.REVIEWED, resolution: HotspotResolution.FIXED }) | |||
); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().hotspots).toHaveLength(0); | |||
}); | |||
function shallowRender(props: Partial<SecurityHotspotsApp['props']> = {}) { | |||
return shallow<SecurityHotspotsApp>( | |||
<SecurityHotspotsApp branchLike={branch} component={mockComponent()} {...props} /> |
@@ -21,6 +21,7 @@ import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; | |||
import { mockRawHotspot } from '../../../helpers/mocks/security-hotspots'; | |||
import { HotspotStatusFilters } from '../../../types/security-hotspots'; | |||
import SecurityHotspotsAppRenderer, { | |||
SecurityHotspotsAppRendererProps | |||
} from '../SecurityHotspotsAppRenderer'; | |||
@@ -53,9 +54,11 @@ function shallowRender(props: Partial<SecurityHotspotsAppRendererProps> = {}) { | |||
<SecurityHotspotsAppRenderer | |||
hotspots={[]} | |||
loading={false} | |||
onChangeStatusFilter={jest.fn()} | |||
onHotspotClick={jest.fn()} | |||
onUpdateHotspot={jest.fn()} | |||
securityCategories={{}} | |||
statusFilter={HotspotStatusFilters.TO_REVIEW} | |||
{...props} | |||
/> | |||
); |
@@ -12,8 +12,10 @@ exports[`should render correctly 1`] = ` | |||
} | |||
hotspots={Array []} | |||
loading={true} | |||
onChangeStatusFilter={[Function]} | |||
onHotspotClick={[Function]} | |||
onUpdateHotspot={[Function]} | |||
securityCategories={Object {}} | |||
statusFilter="TO_REVIEW" | |||
/> | |||
`; |
@@ -4,7 +4,10 @@ exports[`should render correctly 1`] = ` | |||
<div | |||
id="security_hotspots" | |||
> | |||
<FilterBar /> | |||
<FilterBar | |||
onChangeStatus={[MockFunction]} | |||
statusFilter="TO_REVIEW" | |||
/> | |||
<ScreenPositionHelper> | |||
<Component /> | |||
</ScreenPositionHelper> | |||
@@ -144,6 +147,7 @@ exports[`should render correctly with hotspots 1`] = ` | |||
} | |||
onHotspotClick={[MockFunction]} | |||
securityCategories={Object {}} | |||
statusFilter="TO_REVIEW" | |||
/> | |||
</div> | |||
<div | |||
@@ -225,6 +229,7 @@ exports[`should render correctly with hotspots 2`] = ` | |||
onHotspotClick={[MockFunction]} | |||
securityCategories={Object {}} | |||
selectedHotspotKey="h2" | |||
statusFilter="TO_REVIEW" | |||
/> | |||
</div> | |||
<div |
@@ -18,13 +18,36 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import Select from 'sonar-ui-common/components/controls/Select'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { HotspotStatusFilters } from '../../../types/security-hotspots'; | |||
export interface FilterBarProps {} | |||
export interface FilterBarProps { | |||
onChangeStatus: (status: HotspotStatusFilters) => void; | |||
statusFilter: HotspotStatusFilters; | |||
} | |||
const statusOptions: Array<{ label: string; value: string }> = [ | |||
{ label: translate('hotspot.filters.status.to_review'), value: HotspotStatusFilters.TO_REVIEW }, | |||
{ label: translate('hotspot.filters.status.fixed'), value: HotspotStatusFilters.FIXED }, | |||
{ label: translate('hotspot.filters.status.safe'), value: HotspotStatusFilters.SAFE } | |||
]; | |||
export default function FilterBar(props: FilterBarProps) { | |||
const { statusFilter } = props; | |||
return ( | |||
<div className="filter-bar display-flex-center"> | |||
<h3 {...props}>Filter</h3> | |||
<h3 className="big-spacer-right">{translate('hotspot.filters.title')}</h3> | |||
<span className="spacer-right">{translate('status')}</span> | |||
<Select | |||
className="input-medium big-spacer-right" | |||
clearable={false} | |||
onChange={(option: { value: HotspotStatusFilters }) => props.onChangeStatus(option.value)} | |||
options={statusOptions} | |||
searchable={false} | |||
value={statusFilter} | |||
/> | |||
</div> | |||
); | |||
} |
@@ -22,7 +22,7 @@ import { groupBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import SecurityHotspotIcon from 'sonar-ui-common/components/icons/SecurityHotspotIcon'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import { RawHotspot, RiskExposure } from '../../../types/security-hotspots'; | |||
import { HotspotStatusFilters, RawHotspot, RiskExposure } from '../../../types/security-hotspots'; | |||
import { groupByCategory, RISK_EXPOSURE_LEVELS } from '../utils'; | |||
import HotspotCategory from './HotspotCategory'; | |||
import './HotspotList.css'; | |||
@@ -32,10 +32,11 @@ export interface HotspotListProps { | |||
onHotspotClick: (key: string) => void; | |||
securityCategories: T.StandardSecurityCategories; | |||
selectedHotspotKey: string | undefined; | |||
statusFilter: HotspotStatusFilters; | |||
} | |||
export default function HotspotList(props: HotspotListProps) { | |||
const { hotspots, securityCategories, selectedHotspotKey } = props; | |||
const { hotspots, securityCategories, selectedHotspotKey, statusFilter } = props; | |||
const groupedHotspots: Array<{ | |||
risk: RiskExposure; | |||
@@ -53,7 +54,7 @@ export default function HotspotList(props: HotspotListProps) { | |||
<> | |||
<h1 className="hotspot-list-header bordered-bottom"> | |||
<SecurityHotspotIcon className="spacer-right" /> | |||
{translateWithParameters(`hotspots.list_title.TO_REVIEW`, hotspots.length)} | |||
{translateWithParameters(`hotspots.list_title.${statusFilter}`, hotspots.length)} | |||
</h1> | |||
<ul className="huge-spacer-bottom"> | |||
{groupedHotspots.map(riskGroup => ( |
@@ -28,7 +28,7 @@ export interface HotspotListItemProps { | |||
selected: boolean; | |||
} | |||
export function HotspotListItem(props: HotspotListItemProps) { | |||
export default function HotspotListItem(props: HotspotListItemProps) { | |||
const { hotspot, selected } = props; | |||
return ( | |||
<a | |||
@@ -36,9 +36,9 @@ export function HotspotListItem(props: HotspotListItemProps) { | |||
href="#" | |||
onClick={() => !selected && props.onClick(hotspot.key)}> | |||
<div className="little-spacer-left">{hotspot.message}</div> | |||
<div className="badge spacer-top">{translate('issue.status', hotspot.status)}</div> | |||
<div className="badge spacer-top"> | |||
{translate('hotspot.status', hotspot.resolution || hotspot.status)} | |||
</div> | |||
</a> | |||
); | |||
} | |||
export default React.memo(HotspotListItem); |
@@ -0,0 +1,51 @@ | |||
/* | |||
* 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 Select from 'sonar-ui-common/components/controls/Select'; | |||
import { HotspotStatusFilters } from '../../../../types/security-hotspots'; | |||
import FilterBar, { FilterBarProps } from '../FilterBar'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should trigger onChange', () => { | |||
const onChangeStatus = jest.fn(); | |||
const wrapper = shallowRender({ onChangeStatus }); | |||
const { onChange } = wrapper.find(Select).props(); | |||
if (!onChange) { | |||
return fail("Select's onChange should be defined"); | |||
} | |||
onChange({ value: HotspotStatusFilters.SAFE }); | |||
expect(onChangeStatus).toBeCalledWith(HotspotStatusFilters.SAFE); | |||
}); | |||
function shallowRender(props: Partial<FilterBarProps> = {}) { | |||
return shallow( | |||
<FilterBar | |||
onChangeStatus={jest.fn()} | |||
statusFilter={HotspotStatusFilters.TO_REVIEW} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -20,7 +20,7 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockRawHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { RiskExposure } from '../../../../types/security-hotspots'; | |||
import { HotspotStatusFilters, RiskExposure } from '../../../../types/security-hotspots'; | |||
import HotspotList, { HotspotListProps } from '../HotspotList'; | |||
it('should render correctly', () => { | |||
@@ -57,6 +57,7 @@ function shallowRender(props: Partial<HotspotListProps> = {}) { | |||
onHotspotClick={jest.fn()} | |||
securityCategories={{}} | |||
selectedHotspotKey="h2" | |||
statusFilter={HotspotStatusFilters.TO_REVIEW} | |||
{...props} | |||
/> | |||
); |
@@ -20,7 +20,7 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockRawHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { HotspotListItem, HotspotListItemProps } from '../HotspotListItem'; | |||
import HotspotListItem, { HotspotListItemProps } from '../HotspotListItem'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); |
@@ -0,0 +1,41 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="filter-bar display-flex-center" | |||
> | |||
<h3 | |||
className="big-spacer-right" | |||
> | |||
hotspot.filters.title | |||
</h3> | |||
<span | |||
className="spacer-right" | |||
> | |||
status | |||
</span> | |||
<Select | |||
className="input-medium big-spacer-right" | |||
clearable={false} | |||
onChange={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "hotspot.filters.status.to_review", | |||
"value": "TO_REVIEW", | |||
}, | |||
Object { | |||
"label": "hotspot.filters.status.fixed", | |||
"value": "FIXED", | |||
}, | |||
Object { | |||
"label": "hotspot.filters.status.safe", | |||
"value": "SAFE", | |||
}, | |||
] | |||
} | |||
searchable={false} | |||
value="TO_REVIEW" | |||
/> | |||
</div> | |||
`; |
@@ -14,7 +14,7 @@ exports[`should render correctly 1`] = ` | |||
<div | |||
className="badge spacer-top" | |||
> | |||
issue.status.TO_REVIEW | |||
hotspot.status.TO_REVIEW | |||
</div> | |||
</a> | |||
`; | |||
@@ -33,7 +33,7 @@ exports[`should render correctly 2`] = ` | |||
<div | |||
className="badge spacer-top" | |||
> | |||
issue.status.TO_REVIEW | |||
hotspot.status.TO_REVIEW | |||
</div> | |||
</a> | |||
`; |
@@ -34,7 +34,7 @@ | |||
#security_hotspots .filter-bar { | |||
max-width: 1280px; | |||
margin: 0 auto; | |||
padding: var(--gridSize) 20px; | |||
padding: calc(2 * var(--gridSize)) 20px; | |||
border-bottom: 1px solid var(--barBorderColor); | |||
} | |||
@@ -33,6 +33,12 @@ export enum HotspotResolution { | |||
SAFE = 'SAFE' | |||
} | |||
export enum HotspotStatusFilters { | |||
FIXED = 'FIXED', | |||
SAFE = 'SAFE', | |||
TO_REVIEW = 'TO_REVIEW' | |||
} | |||
export enum HotspotStatusOptions { | |||
FIXED = 'FIXED', | |||
SAFE = 'SAFE', |
@@ -649,7 +649,8 @@ hotspots.no_hotspots.title=There are no Security Hotspots to review | |||
hotspots.no_hotspots.description=Next time you analyse a piece of code that contains a potential security risk, it will show up here. | |||
hotspots.learn_more=Learn more about Security Hotspots | |||
hotspots.list_title.TO_REVIEW={0} Security Hotspots to review | |||
hotspots.list_title.REVIEWED={0} reviewed Security Hotspots | |||
hotspots.list_title.FIXED={0} Security Hotspots reviewed as fixed | |||
hotspots.list_title.SAFE={0} Security Hotspots reviewed as safe | |||
hotspots.risk_exposure=Review priority: | |||
hotspot.category=Category: | |||
@@ -660,6 +661,15 @@ hotspot.tabs.vulnerability_description=Are you vulnerable? | |||
hotspot.tabs.fix_recommendations=How can you fix it? | |||
hotspots.review_hotspot=Review Hotspot | |||
hotspot.status.TO_REVIEW=To review | |||
hotspot.status.FIXED=Fixed | |||
hotspot.status.SAFE=Safe | |||
hotspot.filters.title=Filters | |||
hotspot.filters.status.to_review=To review | |||
hotspot.filters.status.fixed=Reviewed as fixed | |||
hotspot.filters.status.safe=Reviewed as safe | |||
hotspots.form.title=Mark Security Hotspot as: | |||
hotspots.form.assign_to=Assign to: |