import { getSecurityHotspots } from '../../api/security-hotspots';
import { getStandards } from '../../helpers/security-standard';
import { BranchLike } from '../../types/branch-like';
-import { RawHotspot } from '../../types/security-hotspots';
+import { HotspotUpdate, RawHotspot } from '../../types/security-hotspots';
import SecurityHotspotsAppRenderer from './SecurityHotspotsAppRenderer';
import './styles.css';
import { sortHotspots } from './utils';
handleHotspotClick = (key: string) => this.setState({ selectedHotspotKey: key });
+ handleHotspotUpdate = ({ key, status, resolution }: HotspotUpdate) => {
+ this.setState(({ hotspots }) => {
+ const index = hotspots.findIndex(h => h.key === key);
+
+ if (index > -1) {
+ const hotspot = {
+ ...hotspots[index],
+ status,
+ resolution
+ };
+
+ return { hotspots: [...hotspots.slice(0, index), hotspot, ...hotspots.slice(index + 1)] };
+ }
+ return null;
+ });
+ };
+
render() {
const { branchLike } = this.props;
const { hotspots, loading, securityCategories, selectedHotspotKey } = this.state;
hotspots={hotspots}
loading={loading}
onHotspotClick={this.handleHotspotClick}
+ onUpdateHotspot={this.handleHotspotUpdate}
securityCategories={securityCategories}
selectedHotspotKey={selectedHotspotKey}
/>
import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import ScreenPositionHelper from '../../components/common/ScreenPositionHelper';
import { BranchLike } from '../../types/branch-like';
-import { RawHotspot } from '../../types/security-hotspots';
+import { HotspotUpdate, RawHotspot } from '../../types/security-hotspots';
import FilterBar from './components/FilterBar';
import HotspotList from './components/HotspotList';
import HotspotViewer from './components/HotspotViewer';
hotspots: RawHotspot[];
loading: boolean;
onHotspotClick: (key: string) => void;
+ onUpdateHotspot: (hotspot: HotspotUpdate) => void;
selectedHotspotKey?: string;
securityCategories: T.StandardSecurityCategories;
}
<HotspotViewer
branchLike={branchLike}
hotspotKey={selectedHotspotKey}
+ onUpdateHotspot={props.onUpdateHotspot}
securityCategories={securityCategories}
/>
)}
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 SecurityHotspotsApp from '../SecurityHotspotsApp';
+import SecurityHotspotsAppRenderer from '../SecurityHotspotsAppRenderer';
jest.mock('sonar-ui-common/helpers/pages', () => ({
addNoFooterPageClass: jest.fn(),
expect(wrapper.state());
});
+it('should handle hotspot update', async () => {
+ const key = 'hotspotKey';
+ const hotspots = [mockRawHotspot(), mockRawHotspot({ key })];
+ (getSecurityHotspots as jest.Mock).mockResolvedValue({
+ hotspots
+ });
+
+ const wrapper = shallowRender();
+
+ await waitAndUpdate(wrapper);
+
+ wrapper
+ .find(SecurityHotspotsAppRenderer)
+ .props()
+ .onUpdateHotspot({ key, status: HotspotStatus.REVIEWED, resolution: HotspotResolution.SAFE });
+
+ expect(wrapper.state().hotspots[0]).toEqual(hotspots[0]);
+ expect(wrapper.state().hotspots[1]).toEqual({
+ ...hotspots[1],
+ status: HotspotStatus.REVIEWED,
+ resolution: HotspotResolution.SAFE
+ });
+});
+
function shallowRender(props: Partial<SecurityHotspotsApp['props']> = {}) {
return shallow<SecurityHotspotsApp>(
<SecurityHotspotsApp branchLike={mockMainBranch()} component={mockComponent()} {...props} />
hotspots={[]}
loading={false}
onHotspotClick={jest.fn()}
+ onUpdateHotspot={jest.fn()}
securityCategories={{}}
{...props}
/>
hotspots={Array []}
loading={true}
onHotspotClick={[Function]}
+ onUpdateHotspot={[Function]}
securityCategories={Object {}}
/>
`;
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
- "resolution": "FALSE-POSITIVE",
+ "resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
- "status": "RESOLVED",
+ "status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
},
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
- "resolution": "FALSE-POSITIVE",
+ "resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
- "status": "RESOLVED",
+ "status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
},
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
- "resolution": "FALSE-POSITIVE",
+ "resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
- "status": "RESOLVED",
+ "status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
},
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
- "resolution": "FALSE-POSITIVE",
+ "resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
- "status": "RESOLVED",
+ "status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
},
>
<HotspotViewer
hotspotKey="h2"
+ onUpdateHotspot={[MockFunction]}
securityCategories={Object {}}
/>
</div>
import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon';
import { PopupPlacement } from 'sonar-ui-common/components/ui/popups';
import { translate } from 'sonar-ui-common/helpers/l10n';
+import { HotspotUpdateFields } from '../../../types/security-hotspots';
import HotspotActionsForm from './HotspotActionsForm';
export interface HotspotActionsProps {
hotspotKey: string;
+ onSubmit: (hotspot: HotspotUpdateFields) => void;
}
const ESCAPE_KEY = 'Escape';
{open && (
<OutsideClickHandler onClickOutside={() => setOpen(false)}>
<DropdownOverlay placement={PopupPlacement.BottomRight}>
- <HotspotActionsForm hotspotKey={props.hotspotKey} onSubmit={() => setOpen(false)} />
+ <HotspotActionsForm
+ hotspotKey={props.hotspotKey}
+ onSubmit={data => {
+ setOpen(false);
+ props.onSubmit(data);
+ }}
+ />
</DropdownOverlay>
</OutsideClickHandler>
)}
HotspotResolution,
HotspotSetStatusRequest,
HotspotStatus,
- HotspotStatusOptions
+ HotspotStatusOptions,
+ HotspotUpdateFields
} from '../../../types/security-hotspots';
import HotspotActionsFormRenderer from './HotspotActionsFormRenderer';
interface Props {
hotspotKey: string;
- onSubmit: () => void;
+ onSubmit: (data: HotspotUpdateFields) => void;
}
interface State {
this.setState({ submitting: true });
return setSecurityHotspotStatus(data)
.then(() => {
- this.props.onSubmit();
+ this.props.onSubmit({ status, resolution: data.resolution });
})
- .finally(() => {
+ .catch(() => {
this.setState({ submitting: false });
});
};
import * as React from 'react';
import { getSecurityHotspotDetails } from '../../../api/security-hotspots';
import { BranchLike } from '../../../types/branch-like';
-import { DetailedHotspot } from '../../../types/security-hotspots';
+import {
+ DetailedHotspot,
+ HotspotUpdate,
+ HotspotUpdateFields
+} from '../../../types/security-hotspots';
import HotspotViewerRenderer from './HotspotViewerRenderer';
interface Props {
branchLike?: BranchLike;
hotspotKey: string;
+ onUpdateHotspot: (hotspot: HotspotUpdate) => void;
securityCategories: T.StandardSecurityCategories;
}
.finally(() => this.mounted && this.setState({ loading: false }));
}
+ handleHotspotUpdate = (data: HotspotUpdateFields) => {
+ this.props.onUpdateHotspot({ key: this.props.hotspotKey, ...data });
+ };
+
render() {
const { branchLike, securityCategories } = this.props;
const { hotspot, loading } = this.state;
branchLike={branchLike}
hotspot={hotspot}
loading={loading}
+ onUpdateHotspot={this.handleHotspotUpdate}
securityCategories={securityCategories}
/>
);
import { withCurrentUser } from '../../../components/hoc/withCurrentUser';
import { isLoggedIn } from '../../../helpers/users';
import { BranchLike } from '../../../types/branch-like';
-import { DetailedHotspot } from '../../../types/security-hotspots';
+import { DetailedHotspot, HotspotUpdateFields } from '../../../types/security-hotspots';
import HotspotActions from './HotspotActions';
import HotspotSnippetContainer from './HotspotSnippetContainer';
import HotspotViewerTabs from './HotspotViewerTabs';
currentUser: T.CurrentUser;
hotspot?: DetailedHotspot;
loading: boolean;
+ onUpdateHotspot: (hotspot: HotspotUpdateFields) => void;
securityCategories: T.StandardSecurityCategories;
}
<div className="big-spacer-bottom">
<div className="display-flex-space-between">
<h1>{hotspot.message}</h1>
- {isLoggedIn(currentUser) && <HotspotActions hotspotKey={hotspot.key} />}
+ {isLoggedIn(currentUser) && (
+ <HotspotActions hotspotKey={hotspot.key} onSubmit={props.onUpdateHotspot} />
+ )}
</div>
<div className="text-muted">
<span>{translate('hotspot.category')}</span>
});
function shallowRender(props: Partial<HotspotActionsProps> = {}) {
- return shallow(<HotspotActions hotspotKey="key" {...props} />);
+ return shallow(<HotspotActions hotspotKey="key" onSubmit={jest.fn()} {...props} />);
}
expect(wrapper.state().submitting).toBe(true);
await promise;
- expect(wrapper.state().submitting).toBe(false);
expect(setSecurityHotspotStatus).toBeCalledWith({
hotspot: 'key',
status: HotspotStatus.TO_REVIEW
return shallow<HotspotViewer>(
<HotspotViewer
hotspotKey={hotspotKey}
+ onUpdateHotspot={jest.fn()}
securityCategories={{ cat1: { title: 'cat1' } }}
{...props}
/>
currentUser={mockCurrentUser()}
hotspot={mockDetailledHotspot()}
loading={false}
+ onUpdateHotspot={jest.fn()}
securityCategories={{ 'sql-injection': { title: 'SQL injection' } }}
{...props}
/>
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
- "resolution": "FALSE-POSITIVE",
+ "resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
- "status": "RESOLVED",
+ "status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
}
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
- "resolution": "FALSE-POSITIVE",
+ "resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
- "status": "RESOLVED",
+ "status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
}
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
- "resolution": "FALSE-POSITIVE",
+ "resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "command-injection",
- "status": "RESOLVED",
+ "status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
}
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
- "resolution": "FALSE-POSITIVE",
+ "resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "cat1",
- "status": "RESOLVED",
+ "status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
},
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
- "resolution": "FALSE-POSITIVE",
+ "resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "cat2",
- "status": "RESOLVED",
+ "status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "HIGH",
},
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
- "resolution": "FALSE-POSITIVE",
+ "resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "cat1",
- "status": "RESOLVED",
+ "status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "MEDIUM",
},
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
- "resolution": "FALSE-POSITIVE",
+ "resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "cat1",
- "status": "RESOLVED",
+ "status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "MEDIUM",
},
"line": 81,
"message": "'3' is a magic number.",
"project": "com.github.kevinsawicki:http-request",
- "resolution": "FALSE-POSITIVE",
+ "resolution": undefined,
"rule": "checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck",
"securityCategory": "cat2",
- "status": "RESOLVED",
+ "status": "TO_REVIEW",
"updateDate": "2013-05-13T17:55:39+0200",
"vulnerabilityProbability": "MEDIUM",
},
<div
className="badge spacer-top"
>
- issue.status.RESOLVED
+ issue.status.TO_REVIEW
</div>
</a>
`;
<div
className="badge spacer-top"
>
- issue.status.RESOLVED
+ issue.status.TO_REVIEW
</div>
</a>
`;
exports[`should render correctly 1`] = `
<Connect(withCurrentUser(HotspotViewerRenderer))
loading={true}
+ onUpdateHotspot={[Function]}
securityCategories={
Object {
"cat1": Object {
}
}
loading={false}
+ onUpdateHotspot={[Function]}
securityCategories={
Object {
"cat1": Object {
</h1>
<HotspotActions
hotspotKey="01fc972e-2a3c-433e-bcae-0bd7f88f5123"
+ onSubmit={[MockFunction]}
/>
</div>
<div
import {
DetailedHotspot,
DetailedHotspotRule,
+ HotspotStatus,
RawHotspot,
RiskExposure
} from '../../types/security-hotspots';
component: 'com.github.kevinsawicki:http-request:com.github.kevinsawicki.http.HttpRequest',
project: 'com.github.kevinsawicki:http-request',
rule: 'checkstyle:com.puppycrawl.tools.checkstyle.checks.coding.MagicNumberCheck',
- status: 'RESOLVED',
- resolution: 'FALSE-POSITIVE',
+ status: HotspotStatus.TO_REVIEW,
+ resolution: undefined,
securityCategory: 'command-injection',
vulnerabilityProbability: RiskExposure.HIGH,
message: "'3' is a magic number.",
line?: number;
message: string;
project: string;
- resolution: string;
+ resolution?: string;
rule: string;
securityCategory: string;
status: string;
line?: number;
message: string;
project: T.Component;
- resolution: string;
+ resolution?: string;
rule: DetailedHotspotRule;
status: string;
textRange: T.TextRange;
updateDate: string;
}
+export interface HotspotUpdateFields {
+ status: HotspotStatus;
+ resolution?: HotspotResolution;
+}
+
+export interface HotspotUpdate extends HotspotUpdateFields {
+ key: string;
+}
+
export interface DetailedHotspotRule {
fixRecommendations?: string;
key: string;