@@ -244,6 +244,7 @@ export default class IssuesServiceMock { | |||
rule: mockRuleDetails({ | |||
key: parameters.key, | |||
name: 'Advanced rule', | |||
genericConcepts: ['defense_in_depth'], | |||
descriptionSections: [ | |||
{ key: RuleDescriptionSections.INTRODUCTION, content: '<h1>Into</h1>' }, | |||
{ key: RuleDescriptionSections.ROOT_CAUSE, content: '<h1>Because</h1>' }, |
@@ -58,6 +58,8 @@ module.exports = { | |||
globalNavBarBg: '#262626', | |||
genericConceptBgColor: '#F4F6FF', | |||
// table | |||
rowHoverHighlight: '#ecf6fe', | |||
@@ -96,13 +96,13 @@ it('should show hotspot rule section', async () => { | |||
).toBeInTheDocument(); | |||
expect( | |||
screen.getByRole('button', { | |||
name: 'coding_rules.description_section.title.resources' | |||
name: 'coding_rules.description_section.title.more_info' | |||
}) | |||
).toBeInTheDocument(); | |||
// Check that we render plain html | |||
await user.click( | |||
screen.getByRole('button', { | |||
name: 'coding_rules.description_section.title.resources' | |||
name: 'coding_rules.description_section.title.more_info' | |||
}) | |||
); | |||
expect(screen.getByRole('link', { name: 'Awsome Reading' })).toBeInTheDocument(); | |||
@@ -122,13 +122,13 @@ it('should show rule advanced section', async () => { | |||
).toBeInTheDocument(); | |||
expect( | |||
screen.getByRole('button', { | |||
name: 'coding_rules.description_section.title.resources' | |||
name: 'coding_rules.description_section.title.more_info' | |||
}) | |||
).toBeInTheDocument(); | |||
// Check that we render plain html | |||
await user.click( | |||
screen.getByRole('button', { | |||
name: 'coding_rules.description_section.title.resources' | |||
name: 'coding_rules.description_section.title.more_info' | |||
}) | |||
); | |||
expect(screen.getByRole('link', { name: 'Awsome Reading' })).toBeInTheDocument(); |
@@ -20,11 +20,12 @@ | |||
import { groupBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import BoxedTabs from '../../../components/controls/BoxedTabs'; | |||
import MoreInfoRuleDescription from '../../../components/rules/MoreInfoRuleDescription'; | |||
import RuleDescription from '../../../components/rules/RuleDescription'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { sanitizeString } from '../../../helpers/sanitize'; | |||
import { RuleDetails } from '../../../types/types'; | |||
import { RuleDescriptionSection, RuleDescriptionSections } from '../rule'; | |||
import RuleContextDescription from '../../../components/rules/RuleContextDescription'; | |||
import { RuleDescriptionSections } from '../rule'; | |||
interface Props { | |||
ruleDetails: RuleDetails; | |||
@@ -36,16 +37,16 @@ interface State { | |||
} | |||
interface Tab { | |||
key: TabKeys; | |||
key: RuleTabKeys; | |||
label: React.ReactNode; | |||
descriptionSections: RuleDescriptionSection[]; | |||
content: React.ReactNode; | |||
} | |||
enum TabKeys { | |||
enum RuleTabKeys { | |||
WhyIsThisAnIssue = 'why', | |||
HowToFixIt = 'how_to_fix', | |||
AssessTheIssue = 'assess_the_problem', | |||
Resources = 'resources' | |||
MoreInfo = 'more_info' | |||
} | |||
export default class RuleViewerTabs extends React.PureComponent<Props, State> { | |||
@@ -60,7 +61,7 @@ export default class RuleViewerTabs extends React.PureComponent<Props, State> { | |||
} | |||
} | |||
handleSelectTabs = (currentTabKey: TabKeys) => { | |||
handleSelectTabs = (currentTabKey: RuleTabKeys) => { | |||
this.setState(({ tabs }) => ({ | |||
currentTab: tabs.find(tab => tab.key === currentTabKey) || tabs[0] | |||
})); | |||
@@ -72,29 +73,43 @@ export default class RuleViewerTabs extends React.PureComponent<Props, State> { | |||
const tabs = [ | |||
{ | |||
key: TabKeys.WhyIsThisAnIssue, | |||
key: RuleTabKeys.WhyIsThisAnIssue, | |||
label: | |||
ruleDetails.type === 'SECURITY_HOTSPOT' | |||
? translate('coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT') | |||
: translate('coding_rules.description_section.title.root_cause'), | |||
descriptionSections: groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE] | |||
content: groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE] && ( | |||
<RuleDescription description={groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE]} /> | |||
) | |||
}, | |||
{ | |||
key: TabKeys.AssessTheIssue, | |||
label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue), | |||
descriptionSections: groupedDescriptions[RuleDescriptionSections.ASSESS_THE_PROBLEM] | |||
key: RuleTabKeys.AssessTheIssue, | |||
label: translate('coding_rules.description_section.title', RuleTabKeys.AssessTheIssue), | |||
content: groupedDescriptions[RuleDescriptionSections.ASSESS_THE_PROBLEM] && ( | |||
<RuleDescription | |||
description={groupedDescriptions[RuleDescriptionSections.ASSESS_THE_PROBLEM]} | |||
/> | |||
) | |||
}, | |||
{ | |||
key: TabKeys.HowToFixIt, | |||
label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt), | |||
descriptionSections: groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX] | |||
key: RuleTabKeys.HowToFixIt, | |||
label: translate('coding_rules.description_section.title', RuleTabKeys.HowToFixIt), | |||
content: groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX] && ( | |||
<RuleDescription description={groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX]} /> | |||
) | |||
}, | |||
{ | |||
key: TabKeys.Resources, | |||
label: translate('coding_rules.description_section.title', TabKeys.Resources), | |||
descriptionSections: groupedDescriptions[RuleDescriptionSections.RESOURCES] | |||
key: RuleTabKeys.MoreInfo, | |||
label: translate('coding_rules.description_section.title', RuleTabKeys.MoreInfo), | |||
content: (ruleDetails.genericConcepts || | |||
groupedDescriptions[RuleDescriptionSections.RESOURCES]) && ( | |||
<MoreInfoRuleDescription | |||
genericConcepts={ruleDetails.genericConcepts} | |||
description={groupedDescriptions[RuleDescriptionSections.RESOURCES]} | |||
/> | |||
) | |||
} | |||
].filter(tab => tab.descriptionSections) as Array<Tab>; | |||
].filter(tab => tab.content) as Array<Tab>; | |||
return { | |||
currentTab: tabs[0], | |||
@@ -125,20 +140,7 @@ export default class RuleViewerTabs extends React.PureComponent<Props, State> { | |||
/> | |||
<div className="bordered-right bordered-left bordered-bottom huge-spacer-bottom"> | |||
{currentTab.descriptionSections.length === 1 && | |||
!currentTab.descriptionSections[0].context ? ( | |||
<div | |||
className="big-padded rule-desc" | |||
// eslint-disable-next-line react/no-danger | |||
dangerouslySetInnerHTML={{ | |||
__html: sanitizeString(currentTab.descriptionSections[0].content) | |||
}} | |||
/> | |||
) : ( | |||
<div className="big-padded rule-desc"> | |||
<RuleContextDescription description={currentTab.descriptionSections} /> | |||
</div> | |||
)} | |||
{currentTab.content} | |||
</div> | |||
</> | |||
); |
@@ -38,6 +38,13 @@ beforeEach(() => { | |||
handler = new IssuesServiceMock(); | |||
}); | |||
it('should show generic concpet', async () => { | |||
const user = userEvent.setup(); | |||
renderProjectIssuesApp('project/issues?issues=issue2&open=issue2&id=myproject'); | |||
await user.click(await screen.findByRole('button', { name: `issue.tabs.more_info` })); | |||
expect(screen.getByRole('heading', { name: 'Defense-In-Depth', level: 3 })).toBeInTheDocument(); | |||
}); | |||
it('should open issue and navigate', async () => { | |||
const user = userEvent.setup(); | |||
renderIssueApp(); | |||
@@ -46,8 +53,8 @@ it('should open issue and navigate', async () => { | |||
expect(screen.getByRole('heading', { level: 1, name: 'Fix that' })).toBeInTheDocument(); | |||
expect(screen.getByRole('link', { name: 'advancedRuleId' })).toBeInTheDocument(); | |||
expect(screen.getByRole('button', { name: `issue.tabs.resources` })).toBeInTheDocument(); | |||
await user.click(screen.getByRole('button', { name: `issue.tabs.resources` })); | |||
expect(screen.getByRole('button', { name: `issue.tabs.more_info` })).toBeInTheDocument(); | |||
await user.click(screen.getByRole('button', { name: `issue.tabs.more_info` })); | |||
expect(screen.getByRole('heading', { name: 'Link' })).toBeInTheDocument(); | |||
expect(screen.getByRole('button', { name: `issue.tabs.how` })).toBeInTheDocument(); |
@@ -22,12 +22,12 @@ import { groupBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import { Link } from 'react-router-dom'; | |||
import BoxedTabs from '../../../components/controls/BoxedTabs'; | |||
import MoreInfoRuleDescription from '../../../components/rules/MoreInfoRuleDescription'; | |||
import RuleDescription from '../../../components/rules/RuleDescription'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { sanitizeString } from '../../../helpers/sanitize'; | |||
import { getRuleUrl } from '../../../helpers/urls'; | |||
import { Component, Issue, RuleDetails } from '../../../types/types'; | |||
import RuleContextDescription from '../../../components/rules/RuleContextDescription'; | |||
import { RuleDescriptionSection, RuleDescriptionSections } from '../../coding-rules/rule'; | |||
import { RuleDescriptionSections } from '../../coding-rules/rule'; | |||
interface Props { | |||
component?: Component; | |||
@@ -37,22 +37,21 @@ interface Props { | |||
} | |||
interface State { | |||
currentTabKey: TabKeys; | |||
currentTabKey: IssueTabKeys; | |||
tabs: Tab[]; | |||
} | |||
interface Tab { | |||
key: TabKeys; | |||
key: IssueTabKeys; | |||
label: React.ReactNode; | |||
descriptionSections: RuleDescriptionSection[]; | |||
isDefault: boolean; | |||
content: React.ReactNode; | |||
} | |||
enum TabKeys { | |||
enum IssueTabKeys { | |||
Code = 'code', | |||
WhyIsThisAnIssue = 'why', | |||
HowToFixIt = 'how', | |||
Resources = 'resources' | |||
MoreInfo = 'more_info' | |||
} | |||
export default class IssueViewerTabs extends React.PureComponent<Props, State> { | |||
@@ -66,7 +65,10 @@ export default class IssueViewerTabs extends React.PureComponent<Props, State> { | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if (prevProps.ruleDetails !== this.props.ruleDetails) { | |||
if ( | |||
prevProps.ruleDetails !== this.props.ruleDetails || | |||
prevProps.codeTabContent !== this.props.codeTabContent | |||
) { | |||
const tabs = this.computeTabs(); | |||
this.setState({ | |||
currentTabKey: tabs[0].key, | |||
@@ -75,12 +77,12 @@ export default class IssueViewerTabs extends React.PureComponent<Props, State> { | |||
} | |||
} | |||
handleSelectTabs = (currentTabKey: TabKeys) => { | |||
handleSelectTabs = (currentTabKey: IssueTabKeys) => { | |||
this.setState({ currentTabKey }); | |||
}; | |||
computeTabs() { | |||
const { ruleDetails } = this.props; | |||
const { ruleDetails, codeTabContent } = this.props; | |||
const groupedDescriptions = groupBy(ruleDetails.descriptionSections, 'key'); | |||
if (ruleDetails.htmlNote) { | |||
@@ -99,42 +101,50 @@ export default class IssueViewerTabs extends React.PureComponent<Props, State> { | |||
} | |||
} | |||
const rootCause = | |||
groupedDescriptions[RuleDescriptionSections.DEFAULT] || | |||
groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE]; | |||
return [ | |||
{ | |||
key: TabKeys.Code, | |||
label: translate('issue.tabs', TabKeys.Code), | |||
descriptionSections: [] | |||
key: IssueTabKeys.Code, | |||
label: translate('issue.tabs', IssueTabKeys.Code), | |||
content: <div className="padded">{codeTabContent}</div> | |||
}, | |||
{ | |||
key: TabKeys.WhyIsThisAnIssue, | |||
label: translate('issue.tabs', TabKeys.WhyIsThisAnIssue), | |||
descriptionSections: | |||
groupedDescriptions[RuleDescriptionSections.DEFAULT] || | |||
groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE], | |||
isDefault: | |||
ruleDetails.descriptionSections?.filter( | |||
section => section.key === RuleDescriptionSections.DEFAULT | |||
) !== undefined | |||
key: IssueTabKeys.WhyIsThisAnIssue, | |||
label: translate('issue.tabs', IssueTabKeys.WhyIsThisAnIssue), | |||
content: rootCause && ( | |||
<RuleDescription | |||
description={rootCause} | |||
isDefault={groupedDescriptions[RuleDescriptionSections.DEFAULT] !== undefined} | |||
/> | |||
) | |||
}, | |||
{ | |||
key: TabKeys.HowToFixIt, | |||
label: translate('issue.tabs', TabKeys.HowToFixIt), | |||
descriptionSections: groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX], | |||
isDefault: false | |||
key: IssueTabKeys.HowToFixIt, | |||
label: translate('issue.tabs', IssueTabKeys.HowToFixIt), | |||
content: groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX] && ( | |||
<RuleDescription description={groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX]} /> | |||
) | |||
}, | |||
{ | |||
key: TabKeys.Resources, | |||
label: translate('issue.tabs', TabKeys.Resources), | |||
descriptionSections: groupedDescriptions[RuleDescriptionSections.RESOURCES], | |||
isDefault: false | |||
key: IssueTabKeys.MoreInfo, | |||
label: translate('issue.tabs', IssueTabKeys.MoreInfo), | |||
content: (ruleDetails.genericConcepts || | |||
groupedDescriptions[RuleDescriptionSections.RESOURCES]) && ( | |||
<MoreInfoRuleDescription | |||
genericConcepts={ruleDetails.genericConcepts} | |||
description={groupedDescriptions[RuleDescriptionSections.RESOURCES]} | |||
/> | |||
) | |||
} | |||
].filter(tab => tab.descriptionSections) as Array<Tab>; | |||
].filter(tab => tab.content) as Array<Tab>; | |||
} | |||
render() { | |||
const { | |||
component, | |||
codeTabContent, | |||
ruleDetails: { name, key }, | |||
issue: { message } | |||
} = this.props; | |||
@@ -162,31 +172,7 @@ export default class IssueViewerTabs extends React.PureComponent<Props, State> { | |||
</div> | |||
{selectedTab && ( | |||
<div className="bordered-right bordered-left bordered-bottom huge-spacer-bottom"> | |||
{selectedTab.key === TabKeys.Code && <div className="padded">{codeTabContent}</div>} | |||
{selectedTab.key !== TabKeys.Code && | |||
(selectedTab.descriptionSections.length === 1 && | |||
!selectedTab.descriptionSections[0].context ? ( | |||
<div | |||
key={selectedTab.key} | |||
className={classNames('big-padded', { | |||
markdown: selectedTab.isDefault, | |||
'rule-desc': !selectedTab.isDefault | |||
})} | |||
// eslint-disable-next-line react/no-danger | |||
dangerouslySetInnerHTML={{ | |||
__html: sanitizeString(selectedTab.descriptionSections[0].content) | |||
}} | |||
/> | |||
) : ( | |||
<div | |||
key={selectedTab.key} | |||
className={classNames('big-padded', { | |||
markdown: selectedTab.isDefault, | |||
'rule-desc': !selectedTab.isDefault | |||
})}> | |||
<RuleContextDescription description={selectedTab.descriptionSections} /> | |||
</div> | |||
))} | |||
{selectedTab.content} | |||
</div> | |||
)} | |||
</> |
@@ -20,12 +20,11 @@ | |||
import { groupBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import BoxedTabs from '../../../components/controls/BoxedTabs'; | |||
import RuleDescription from '../../../components/rules/RuleDescription'; | |||
import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; | |||
import { KeyboardKeys } from '../../../helpers/keycodes'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { sanitizeString } from '../../../helpers/sanitize'; | |||
import { Hotspot } from '../../../types/security-hotspots'; | |||
import RuleContextDescription from '../../../components/rules/RuleContextDescription'; | |||
import { RuleDescriptionSection, RuleDescriptionSections } from '../../coding-rules/rule'; | |||
interface Props { | |||
@@ -43,7 +42,7 @@ interface State { | |||
interface Tab { | |||
key: TabKeys; | |||
label: React.ReactNode; | |||
descriptionSections: RuleDescriptionSection[]; | |||
content: React.ReactNode; | |||
} | |||
export enum TabKeys { | |||
@@ -68,7 +67,10 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State> | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if (this.props.hotspot.key !== prevProps.hotspot.key) { | |||
if ( | |||
this.props.hotspot.key !== prevProps.hotspot.key || | |||
prevProps.codeTabContent !== this.props.codeTabContent | |||
) { | |||
const tabs = this.computeTabs(); | |||
this.setState({ | |||
currentTab: tabs[0], | |||
@@ -117,37 +119,44 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State> | |||
}; | |||
computeTabs() { | |||
const { ruleDescriptionSections } = this.props; | |||
const { ruleDescriptionSections, codeTabContent } = this.props; | |||
const groupedDescriptions = groupBy(ruleDescriptionSections, description => description.key); | |||
const rootCause = | |||
groupedDescriptions[RuleDescriptionSections.DEFAULT] || | |||
groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE]; | |||
const descriptionTabs = [ | |||
return [ | |||
{ | |||
key: TabKeys.Code, | |||
label: translate('hotspots.tabs.code'), | |||
content: <div className="padded">{codeTabContent}</div> | |||
}, | |||
{ | |||
key: TabKeys.RiskDescription, | |||
label: translate('hotspots.tabs.risk_description'), | |||
descriptionSections: | |||
groupedDescriptions[RuleDescriptionSections.DEFAULT] || | |||
groupedDescriptions[RuleDescriptionSections.ROOT_CAUSE] | |||
content: rootCause && <RuleDescription description={rootCause} isDefault={true} /> | |||
}, | |||
{ | |||
key: TabKeys.VulnerabilityDescription, | |||
label: translate('hotspots.tabs.vulnerability_description'), | |||
descriptionSections: groupedDescriptions[RuleDescriptionSections.ASSESS_THE_PROBLEM] | |||
content: groupedDescriptions[RuleDescriptionSections.ASSESS_THE_PROBLEM] && ( | |||
<RuleDescription | |||
description={groupedDescriptions[RuleDescriptionSections.ASSESS_THE_PROBLEM]} | |||
isDefault={true} | |||
/> | |||
) | |||
}, | |||
{ | |||
key: TabKeys.FixRecommendation, | |||
label: translate('hotspots.tabs.fix_recommendations'), | |||
descriptionSections: groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX] | |||
content: groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX] && ( | |||
<RuleDescription | |||
description={groupedDescriptions[RuleDescriptionSections.HOW_TO_FIX]} | |||
isDefault={true} | |||
/> | |||
) | |||
} | |||
].filter(tab => tab.descriptionSections); | |||
return [ | |||
{ | |||
key: TabKeys.Code, | |||
label: translate('hotspots.tabs.code'), | |||
descriptionSections: [] | |||
}, | |||
...descriptionTabs | |||
]; | |||
].filter(tab => tab.content); | |||
} | |||
selectNeighboringTab(shift: number) { | |||
@@ -166,30 +175,11 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State> | |||
} | |||
render() { | |||
const { codeTabContent } = this.props; | |||
const { tabs, currentTab } = this.state; | |||
return ( | |||
<> | |||
<BoxedTabs onSelect={this.handleSelectTabs} selected={currentTab.key} tabs={tabs} /> | |||
<div className="bordered huge-spacer-bottom"> | |||
{currentTab.key === TabKeys.Code && <div className="padded">{codeTabContent}</div>} | |||
{currentTab.key !== TabKeys.Code && | |||
(currentTab.descriptionSections.length === 1 && | |||
!currentTab.descriptionSections[0].context ? ( | |||
<div | |||
key={currentTab.key} | |||
className="markdown big-padded" | |||
// eslint-disable-next-line react/no-danger | |||
dangerouslySetInnerHTML={{ | |||
__html: sanitizeString(currentTab.descriptionSections[0].content) | |||
}} | |||
/> | |||
) : ( | |||
<div className="markdown big-padded"> | |||
<RuleContextDescription description={currentTab.descriptionSections} /> | |||
</div> | |||
))} | |||
</div> | |||
<div className="bordered huge-spacer-bottom">{currentTab.content}</div> | |||
</> | |||
); | |||
} |
@@ -8,37 +8,58 @@ exports[`should render correctly: fix 1`] = ` | |||
tabs={ | |||
Array [ | |||
Object { | |||
"descriptionSections": Array [], | |||
"content": <div | |||
className="padded" | |||
> | |||
<div> | |||
CodeTabContent | |||
</div> | |||
</div>, | |||
"key": "code", | |||
"label": "hotspots.tabs.code", | |||
}, | |||
Object { | |||
"descriptionSections": Array [ | |||
Object { | |||
"content": "cause", | |||
"key": "root_cause", | |||
}, | |||
], | |||
"content": <RuleDescription | |||
description={ | |||
Array [ | |||
Object { | |||
"content": "cause", | |||
"key": "root_cause", | |||
}, | |||
] | |||
} | |||
isDefault={true} | |||
/>, | |||
"key": "risk", | |||
"label": "hotspots.tabs.risk_description", | |||
}, | |||
Object { | |||
"descriptionSections": Array [ | |||
Object { | |||
"content": "assess", | |||
"key": "assess_the_problem", | |||
}, | |||
], | |||
"content": <RuleDescription | |||
description={ | |||
Array [ | |||
Object { | |||
"content": "assess", | |||
"key": "assess_the_problem", | |||
}, | |||
] | |||
} | |||
isDefault={true} | |||
/>, | |||
"key": "vulnerability", | |||
"label": "hotspots.tabs.vulnerability_description", | |||
}, | |||
Object { | |||
"descriptionSections": Array [ | |||
Object { | |||
"content": "how", | |||
"key": "how_to_fix", | |||
}, | |||
], | |||
"content": <RuleDescription | |||
description={ | |||
Array [ | |||
Object { | |||
"content": "how", | |||
"key": "how_to_fix", | |||
}, | |||
] | |||
} | |||
isDefault={true} | |||
/>, | |||
"key": "fix", | |||
"label": "hotspots.tabs.fix_recommendations", | |||
}, | |||
@@ -48,14 +69,16 @@ exports[`should render correctly: fix 1`] = ` | |||
<div | |||
className="bordered huge-spacer-bottom" | |||
> | |||
<div | |||
className="markdown big-padded" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "how", | |||
} | |||
<RuleDescription | |||
description={ | |||
Array [ | |||
Object { | |||
"content": "how", | |||
"key": "how_to_fix", | |||
}, | |||
] | |||
} | |||
key="fix" | |||
isDefault={true} | |||
/> | |||
</div> | |||
</Fragment> | |||
@@ -69,37 +92,58 @@ exports[`should render correctly: risk 1`] = ` | |||
tabs={ | |||
Array [ | |||
Object { | |||
"descriptionSections": Array [], | |||
"content": <div | |||
className="padded" | |||
> | |||
<div> | |||
CodeTabContent | |||
</div> | |||
</div>, | |||
"key": "code", | |||
"label": "hotspots.tabs.code", | |||
}, | |||
Object { | |||
"descriptionSections": Array [ | |||
Object { | |||
"content": "cause", | |||
"key": "root_cause", | |||
}, | |||
], | |||
"content": <RuleDescription | |||
description={ | |||
Array [ | |||
Object { | |||
"content": "cause", | |||
"key": "root_cause", | |||
}, | |||
] | |||
} | |||
isDefault={true} | |||
/>, | |||
"key": "risk", | |||
"label": "hotspots.tabs.risk_description", | |||
}, | |||
Object { | |||
"descriptionSections": Array [ | |||
Object { | |||
"content": "assess", | |||
"key": "assess_the_problem", | |||
}, | |||
], | |||
"content": <RuleDescription | |||
description={ | |||
Array [ | |||
Object { | |||
"content": "assess", | |||
"key": "assess_the_problem", | |||
}, | |||
] | |||
} | |||
isDefault={true} | |||
/>, | |||
"key": "vulnerability", | |||
"label": "hotspots.tabs.vulnerability_description", | |||
}, | |||
Object { | |||
"descriptionSections": Array [ | |||
Object { | |||
"content": "how", | |||
"key": "how_to_fix", | |||
}, | |||
], | |||
"content": <RuleDescription | |||
description={ | |||
Array [ | |||
Object { | |||
"content": "how", | |||
"key": "how_to_fix", | |||
}, | |||
] | |||
} | |||
isDefault={true} | |||
/>, | |||
"key": "fix", | |||
"label": "hotspots.tabs.fix_recommendations", | |||
}, | |||
@@ -128,37 +172,58 @@ exports[`should render correctly: vulnerability 1`] = ` | |||
tabs={ | |||
Array [ | |||
Object { | |||
"descriptionSections": Array [], | |||
"content": <div | |||
className="padded" | |||
> | |||
<div> | |||
CodeTabContent | |||
</div> | |||
</div>, | |||
"key": "code", | |||
"label": "hotspots.tabs.code", | |||
}, | |||
Object { | |||
"descriptionSections": Array [ | |||
Object { | |||
"content": "cause", | |||
"key": "root_cause", | |||
}, | |||
], | |||
"content": <RuleDescription | |||
description={ | |||
Array [ | |||
Object { | |||
"content": "cause", | |||
"key": "root_cause", | |||
}, | |||
] | |||
} | |||
isDefault={true} | |||
/>, | |||
"key": "risk", | |||
"label": "hotspots.tabs.risk_description", | |||
}, | |||
Object { | |||
"descriptionSections": Array [ | |||
Object { | |||
"content": "assess", | |||
"key": "assess_the_problem", | |||
}, | |||
], | |||
"content": <RuleDescription | |||
description={ | |||
Array [ | |||
Object { | |||
"content": "assess", | |||
"key": "assess_the_problem", | |||
}, | |||
] | |||
} | |||
isDefault={true} | |||
/>, | |||
"key": "vulnerability", | |||
"label": "hotspots.tabs.vulnerability_description", | |||
}, | |||
Object { | |||
"descriptionSections": Array [ | |||
Object { | |||
"content": "how", | |||
"key": "how_to_fix", | |||
}, | |||
], | |||
"content": <RuleDescription | |||
description={ | |||
Array [ | |||
Object { | |||
"content": "how", | |||
"key": "how_to_fix", | |||
}, | |||
] | |||
} | |||
isDefault={true} | |||
/>, | |||
"key": "fix", | |||
"label": "hotspots.tabs.fix_recommendations", | |||
}, | |||
@@ -168,14 +233,16 @@ exports[`should render correctly: vulnerability 1`] = ` | |||
<div | |||
className="bordered huge-spacer-bottom" | |||
> | |||
<div | |||
className="markdown big-padded" | |||
dangerouslySetInnerHTML={ | |||
Object { | |||
"__html": "assess", | |||
} | |||
<RuleDescription | |||
description={ | |||
Array [ | |||
Object { | |||
"content": "assess", | |||
"key": "assess_the_problem", | |||
}, | |||
] | |||
} | |||
key="vulnerability" | |||
isDefault={true} | |||
/> | |||
</div> | |||
</Fragment> | |||
@@ -189,37 +256,58 @@ exports[`should render correctly: with comments or changelog element 1`] = ` | |||
tabs={ | |||
Array [ | |||
Object { | |||
"descriptionSections": Array [], | |||
"content": <div | |||
className="padded" | |||
> | |||
<div> | |||
CodeTabContent | |||
</div> | |||
</div>, | |||
"key": "code", | |||
"label": "hotspots.tabs.code", | |||
}, | |||
Object { | |||
"descriptionSections": Array [ | |||
Object { | |||
"content": "cause", | |||
"key": "root_cause", | |||
}, | |||
], | |||
"content": <RuleDescription | |||
description={ | |||
Array [ | |||
Object { | |||
"content": "cause", | |||
"key": "root_cause", | |||
}, | |||
] | |||
} | |||
isDefault={true} | |||
/>, | |||
"key": "risk", | |||
"label": "hotspots.tabs.risk_description", | |||
}, | |||
Object { | |||
"descriptionSections": Array [ | |||
Object { | |||
"content": "assess", | |||
"key": "assess_the_problem", | |||
}, | |||
], | |||
"content": <RuleDescription | |||
description={ | |||
Array [ | |||
Object { | |||
"content": "assess", | |||
"key": "assess_the_problem", | |||
}, | |||
] | |||
} | |||
isDefault={true} | |||
/>, | |||
"key": "vulnerability", | |||
"label": "hotspots.tabs.vulnerability_description", | |||
}, | |||
Object { | |||
"descriptionSections": Array [ | |||
Object { | |||
"content": "how", | |||
"key": "how_to_fix", | |||
}, | |||
], | |||
"content": <RuleDescription | |||
description={ | |||
Array [ | |||
Object { | |||
"content": "how", | |||
"key": "how_to_fix", | |||
}, | |||
] | |||
} | |||
isDefault={true} | |||
/>, | |||
"key": "fix", | |||
"label": "hotspots.tabs.fix_recommendations", | |||
}, |
@@ -0,0 +1,75 @@ | |||
/* | |||
* 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 * as React from 'react'; | |||
import { RuleDescriptionSection } from '../../apps/coding-rules/rule'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { Dict } from '../../types/types'; | |||
import DefenseInDepth from './genericConcepts/DefenseInDepth'; | |||
import LeastTrustPrinciple from './genericConcepts/LeastTrustPrinciple'; | |||
import RuleDescription from './RuleDescription'; | |||
import './style.css'; | |||
interface Props { | |||
description?: RuleDescriptionSection[]; | |||
genericConcepts?: string[]; | |||
} | |||
const GENERIC_CONCPET_MAP: Dict<React.ComponentType> = { | |||
defense_in_depth: DefenseInDepth, | |||
least_trust_principle: LeastTrustPrinciple | |||
}; | |||
export default function MoreInfoRuleDescription({ description = [], genericConcepts = [] }: Props) { | |||
return ( | |||
<> | |||
{description.length > 0 && ( | |||
<> | |||
<div className="big-padded-left big-padded-right big-padded-top rule-desc"> | |||
<h2 className="null-spacer-bottom"> | |||
{translate('coding_rules.more_info.resources.title')} | |||
</h2> | |||
</div> | |||
<RuleDescription key="more-info" description={description} /> | |||
</> | |||
)} | |||
{genericConcepts.length > 0 && ( | |||
<> | |||
<div className="big-padded-left big-padded-right rule-desc"> | |||
<h2 className="null-spacer-top"> | |||
{translate('coding_rules.more_info.generic_concept.title')} | |||
</h2> | |||
</div> | |||
{genericConcepts.map(key => { | |||
const Concept = GENERIC_CONCPET_MAP[key]; | |||
if (Concept === undefined) { | |||
return null; | |||
} | |||
return ( | |||
<div key={key} className="generic-concept rule-desc"> | |||
<Concept /> | |||
</div> | |||
); | |||
})} | |||
</> | |||
)} | |||
</> | |||
); | |||
} |
@@ -17,22 +17,24 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { RuleDescriptionSection } from '../../apps/coding-rules/rule'; | |||
import { translate } from '../../helpers/l10n'; | |||
import RadioToggle from '../controls/RadioToggle'; | |||
import { sanitizeString } from '../../helpers/sanitize'; | |||
import { RuleDescriptionSection } from '../../apps/coding-rules/rule'; | |||
import RadioToggle from '../controls/RadioToggle'; | |||
import OtherContextOption from './OtherContextOption'; | |||
const OTHERS_KEY = 'others'; | |||
interface Props { | |||
isDefault?: boolean; | |||
description: RuleDescriptionSection[]; | |||
} | |||
interface State { | |||
contexts: RuleDescriptionContextDisplay[]; | |||
selectedContext: RuleDescriptionContextDisplay; | |||
selectedContext?: RuleDescriptionContextDisplay; | |||
} | |||
interface RuleDescriptionContextDisplay { | |||
@@ -41,7 +43,7 @@ interface RuleDescriptionContextDisplay { | |||
key: string; | |||
} | |||
export default class RuleContextDescription extends React.PureComponent<Props, State> { | |||
export default class RuleDescription extends React.PureComponent<Props, State> { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = this.computeState(props.description); | |||
@@ -87,6 +89,7 @@ export default class RuleContextDescription extends React.PureComponent<Props, S | |||
}; | |||
render() { | |||
const { description, isDefault } = this.props; | |||
const { contexts } = this.state; | |||
const { selectedContext } = this.state; | |||
@@ -95,26 +98,49 @@ export default class RuleContextDescription extends React.PureComponent<Props, S | |||
value: ctxt.displayName | |||
})); | |||
return ( | |||
<div className="rules-context-description"> | |||
<h2 className="rule-contexts-title"> | |||
{translate('coding_rules.description_context_title')} | |||
</h2> | |||
<RadioToggle | |||
className="big-spacer-bottom" | |||
name="filter" | |||
onCheck={this.handleToggleContext} | |||
options={options} | |||
value={selectedContext.displayName} | |||
if (!description[0].context && description.length === 1) { | |||
return ( | |||
<div | |||
className={classNames('big-padded', { | |||
markdown: isDefault, | |||
'rule-desc': !isDefault | |||
})} | |||
// eslint-disable-next-line react/no-danger | |||
dangerouslySetInnerHTML={{ | |||
__html: sanitizeString(description[0].content) | |||
}} | |||
/> | |||
{selectedContext.key === OTHERS_KEY ? ( | |||
<OtherContextOption /> | |||
) : ( | |||
<div | |||
/* eslint-disable-next-line react/no-danger */ | |||
dangerouslySetInnerHTML={{ __html: sanitizeString(selectedContext.content) }} | |||
); | |||
} | |||
if (!selectedContext) { | |||
return null; | |||
} | |||
return ( | |||
<div | |||
className={classNames('big-padded', { | |||
markdown: isDefault, | |||
'rule-desc': !isDefault | |||
})}> | |||
<div className="rules-context-description"> | |||
<h2 className="rule-contexts-title"> | |||
{translate('coding_rules.description_context_title')} | |||
</h2> | |||
<RadioToggle | |||
className="big-spacer-bottom" | |||
name="filter" | |||
onCheck={this.handleToggleContext} | |||
options={options} | |||
value={selectedContext.displayName} | |||
/> | |||
)} | |||
{selectedContext.key === OTHERS_KEY ? ( | |||
<OtherContextOption /> | |||
) : ( | |||
<div | |||
/* eslint-disable-next-line react/no-danger */ | |||
dangerouslySetInnerHTML={{ __html: sanitizeString(selectedContext.content) }} | |||
/> | |||
)} | |||
</div> | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,48 @@ | |||
/* | |||
* 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 * as React from 'react'; | |||
export default function DefenseInDepth() { | |||
return ( | |||
<> | |||
<h3>Defense-In-Depth</h3> | |||
<p> | |||
Applications and infrastructure benefit greatly from relying on multiple security mechanisms | |||
layered on top of each other. If one security mechanism fails, there is a high probability | |||
that the subsequent layer of security will successfully defend against the attack. | |||
</p> | |||
<p>A non-exhaustive list of these code protection ramparts includes the following:</p> | |||
<ul> | |||
<li>Minimizing the attack surface of the code</li> | |||
<li>Application of the principle of least privilege</li> | |||
<li>Validation and sanitization of data</li> | |||
<li>Encrypting incoming, outgoing, or stored data with secure cryptography</li> | |||
<li>Ensuring that internal errors cannot disrupt the overall runtime</li> | |||
<li>Separation of tasks and access to information</li> | |||
</ul> | |||
<p> | |||
Note that these layers must be simple enough to use in an everyday workflow. Harsh security | |||
measures can lead to users bypassing them. | |||
</p> | |||
</> | |||
); | |||
} |
@@ -0,0 +1,50 @@ | |||
/* | |||
* 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 * as React from 'react'; | |||
export default function LeastTrustPrinciple() { | |||
return ( | |||
<> | |||
<h3>Least Trust Principle</h3> | |||
<p>Applications must treat all third-party data as attacker-controlled data. </p> | |||
<p> | |||
First, the application must determine where the third-party data originates and treat that | |||
data source as an attack vector. | |||
</p> | |||
<p> | |||
Then, the application must validate the attacker-controlled data against predefined formats, | |||
such as: | |||
</p> | |||
<ul> | |||
<li>Character sets</li> | |||
<li>Sizes</li> | |||
<li>Types</li> | |||
<li>Or any strict schema</li> | |||
</ul> | |||
<p> | |||
Next, the code must sanitize the data before performing mission-critical operations on the | |||
attacker-controlled data. The code must know in which contexts the intercepted data is used | |||
and act accordingly (section "How to fix it?"). | |||
</p> | |||
</> | |||
); | |||
} |
@@ -0,0 +1,30 @@ | |||
/* | |||
* 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. | |||
*/ | |||
.generic-concept { | |||
background-color: var(--genericConceptBgColor); | |||
border-radius: 2px; | |||
display: inline-block; | |||
margin-left: 16px; | |||
margin-right: 16px; | |||
margin-bottom: 16px; | |||
padding-left: 16px; | |||
padding-right: 16px; | |||
} |
@@ -588,6 +588,7 @@ export interface RuleDetails extends Rule { | |||
defaultRemFnBaseEffort?: string; | |||
defaultRemFnType?: string; | |||
descriptionSections?: RuleDescriptionSection[]; | |||
genericConcepts?: string[]; | |||
effortToFixDescription?: string; | |||
htmlDesc?: string; | |||
htmlNote?: string; |
@@ -860,7 +860,7 @@ issue.transition.resetastoreview.description=The Security Hotspot should be anal | |||
issue.tabs.code=Where is the issue? | |||
issue.tabs.why=Why is this an issue? | |||
issue.tabs.how=How to fix it? | |||
issue.tabs.resources=Resources | |||
issue.tabs.more_info=More Info | |||
vulnerability.transition.resetastoreview=Reset as To Review | |||
vulnerability.transition.resetastoreview.description=The vulnerability can't be fixed as is and needs more details. The security hotspot needs to be reviewed again | |||
@@ -1910,11 +1910,14 @@ coding_rules.description_section.title.root_cause=Why is this an issue? | |||
coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT=What is the risk? | |||
coding_rules.description_section.title.assess_the_problem=Assess the risk? | |||
coding_rules.description_section.title.how_to_fix=How to fix it? | |||
coding_rules.description_section.title.resources=Resources | |||
coding_rules.description_section.title.more_info=More Info | |||
coding_rules.description_context_title=Which component or framework contains the issue? | |||
coding_rules.description_context_other=Other | |||
coding_rules.more_info.generic_concept.title=Security principles | |||
coding_rules.more_info.resources.title=Resources | |||
#------------------------------------------------------------------------------ | |||
# | |||
# EMAIL CONFIGURATION |