aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMathieu Suen <mathieu.suen@sonarsource.com>2022-07-06 08:51:20 +0200
committersonartech <sonartech@sonarsource.com>2022-07-07 20:03:10 +0000
commit233c3c9320d8f22b75eb657dc58d7b1974f1478e (patch)
tree71e54363f35165944872d2f8ad6c2c51b27dcecc
parentb36232ed7731541643dcebd4e4662e6cad3d4b2d (diff)
downloadsonarqube-233c3c9320d8f22b75eb657dc58d7b1974f1478e.tar.gz
sonarqube-233c3c9320d8f22b75eb657dc58d7b1974f1478e.zip
SONAR-16598 Add generic concepts to rule advance description
-rw-r--r--server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts1
-rw-r--r--server/sonar-web/src/main/js/app/theme.js2
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts8
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx68
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx104
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx72
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap268
-rw-r--r--server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx75
-rw-r--r--server/sonar-web/src/main/js/components/rules/RuleDescription.tsx (renamed from server/sonar-web/src/main/js/components/rules/RuleContextDescription.tsx)70
-rw-r--r--server/sonar-web/src/main/js/components/rules/genericConcepts/DefenseInDepth.tsx48
-rw-r--r--server/sonar-web/src/main/js/components/rules/genericConcepts/LeastTrustPrinciple.tsx50
-rw-r--r--server/sonar-web/src/main/js/components/rules/style.css30
-rw-r--r--server/sonar-web/src/main/js/types/types.ts1
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties7
15 files changed, 562 insertions, 253 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
index f9d3fdf7b53..0957a3b5f80 100644
--- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
@@ -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>' },
diff --git a/server/sonar-web/src/main/js/app/theme.js b/server/sonar-web/src/main/js/app/theme.js
index 4285cdc06a5..d4119df1c66 100644
--- a/server/sonar-web/src/main/js/app/theme.js
+++ b/server/sonar-web/src/main/js/app/theme.js
@@ -58,6 +58,8 @@ module.exports = {
globalNavBarBg: '#262626',
+ genericConceptBgColor: '#F4F6FF',
+
// table
rowHoverHighlight: '#ecf6fe',
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
index 33be963dc88..c2b66033a6b 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
+++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
@@ -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();
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx
index 46fa9dd12cf..3c3e647ef73 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx
@@ -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>
</>
);
diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
index 4aefa525a1f..36f41a9600f 100644
--- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
@@ -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();
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx
index 7e57160826d..4821f37896c 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx
@@ -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>
)}
</>
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
index bd263ec5b9d..7e53f144e69 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx
@@ -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>
</>
);
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap
index 5a95181fd54..ab90bf8c2ba 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap
@@ -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",
},
diff --git a/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx b/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx
new file mode 100644
index 00000000000..bafca86796f
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx
@@ -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>
+ );
+ })}
+ </>
+ )}
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/rules/RuleContextDescription.tsx b/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx
index ccc3c031748..8935766ded8 100644
--- a/server/sonar-web/src/main/js/components/rules/RuleContextDescription.tsx
+++ b/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx
@@ -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>
);
}
diff --git a/server/sonar-web/src/main/js/components/rules/genericConcepts/DefenseInDepth.tsx b/server/sonar-web/src/main/js/components/rules/genericConcepts/DefenseInDepth.tsx
new file mode 100644
index 00000000000..e338d15b045
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/rules/genericConcepts/DefenseInDepth.tsx
@@ -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>
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/rules/genericConcepts/LeastTrustPrinciple.tsx b/server/sonar-web/src/main/js/components/rules/genericConcepts/LeastTrustPrinciple.tsx
new file mode 100644
index 00000000000..5c2f8fb132c
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/rules/genericConcepts/LeastTrustPrinciple.tsx
@@ -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 &quot;How to fix it?&quot;).
+ </p>
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/rules/style.css b/server/sonar-web/src/main/js/components/rules/style.css
new file mode 100644
index 00000000000..de8c16594e0
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/rules/style.css
@@ -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;
+}
diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts
index c2abb28ee85..64d9b22c788 100644
--- a/server/sonar-web/src/main/js/types/types.ts
+++ b/server/sonar-web/src/main/js/types/types.ts
@@ -588,6 +588,7 @@ export interface RuleDetails extends Rule {
defaultRemFnBaseEffort?: string;
defaultRemFnType?: string;
descriptionSections?: RuleDescriptionSection[];
+ genericConcepts?: string[];
effortToFixDescription?: string;
htmlDesc?: string;
htmlNote?: string;
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index cd1fd65f021..23dac66b829 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -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