Procházet zdrojové kódy

SONAR-16598 Add generic concepts to rule advance description

tags/9.6.0.59041
Mathieu Suen před 1 rokem
rodič
revize
233c3c9320

+ 1
- 0
server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts Zobrazit soubor

@@ -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>' },

+ 2
- 0
server/sonar-web/src/main/js/app/theme.js Zobrazit soubor

@@ -58,6 +58,8 @@ module.exports = {

globalNavBarBg: '#262626',

genericConceptBgColor: '#F4F6FF',

// table
rowHoverHighlight: '#ecf6fe',


+ 4
- 4
server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts Zobrazit soubor

@@ -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();

+ 35
- 33
server/sonar-web/src/main/js/apps/coding-rules/components/RuleTabViewer.tsx Zobrazit soubor

@@ -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>
</>
);

+ 9
- 2
server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx Zobrazit soubor

@@ -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();

+ 45
- 59
server/sonar-web/src/main/js/apps/issues/components/IssueTabViewer.tsx Zobrazit soubor

@@ -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>
)}
</>

+ 31
- 41
server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx Zobrazit soubor

@@ -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>
</>
);
}

+ 178
- 90
server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap Zobrazit soubor

@@ -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",
},

+ 75
- 0
server/sonar-web/src/main/js/components/rules/MoreInfoRuleDescription.tsx Zobrazit soubor

@@ -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>
);
})}
</>
)}
</>
);
}

server/sonar-web/src/main/js/components/rules/RuleContextDescription.tsx → server/sonar-web/src/main/js/components/rules/RuleDescription.tsx Zobrazit soubor

@@ -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>
);
}

+ 48
- 0
server/sonar-web/src/main/js/components/rules/genericConcepts/DefenseInDepth.tsx Zobrazit soubor

@@ -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>
</>
);
}

+ 50
- 0
server/sonar-web/src/main/js/components/rules/genericConcepts/LeastTrustPrinciple.tsx Zobrazit soubor

@@ -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>
</>
);
}

+ 30
- 0
server/sonar-web/src/main/js/components/rules/style.css Zobrazit soubor

@@ -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;
}

+ 1
- 0
server/sonar-web/src/main/js/types/types.ts Zobrazit soubor

@@ -588,6 +588,7 @@ export interface RuleDetails extends Rule {
defaultRemFnBaseEffort?: string;
defaultRemFnType?: string;
descriptionSections?: RuleDescriptionSection[];
genericConcepts?: string[];
effortToFixDescription?: string;
htmlDesc?: string;
htmlNote?: string;

+ 5
- 2
sonar-core/src/main/resources/org/sonar/l10n/core.properties Zobrazit soubor

@@ -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

Načítá se…
Zrušit
Uložit