diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2022-07-06 08:51:20 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-07-07 20:03:10 +0000 |
commit | 233c3c9320d8f22b75eb657dc58d7b1974f1478e (patch) | |
tree | 71e54363f35165944872d2f8ad6c2c51b27dcecc | |
parent | b36232ed7731541643dcebd4e4662e6cad3d4b2d (diff) | |
download | sonarqube-233c3c9320d8f22b75eb657dc58d7b1974f1478e.tar.gz sonarqube-233c3c9320d8f22b75eb657dc58d7b1974f1478e.zip |
SONAR-16598 Add generic concepts to rule advance description
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 "How to fix it?"). + </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 |