*/
import { cloneDeep, countBy, pick, trim } from 'lodash';
import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
+import { getStandards } from '../../helpers/security-standard';
import {
mockCurrentUser,
mockPaging,
import { RuleRepository, SearchRulesResponse } from '../../types/coding-rules';
import { RawIssuesResponse } from '../../types/issues';
import { SearchRulesQuery } from '../../types/rules';
+import { SecurityStandard, Standards } from '../../types/security';
import { Dict, Rule, RuleActivation, RuleDetails, RulesUpdateRequest } from '../../types/types';
import { NoticeType } from '../../types/users';
import { getFacet } from '../issues';
createRule,
deleteRule,
getRuleDetails,
+ getRuleRepositories,
getRuleTags,
getRulesApp,
searchRules,
} from '../rules';
import { dismissNotice, getCurrentUser } from '../users';
-interface FacetFilter {
- languages?: string;
- tags?: string;
- available_since?: string;
- q?: string;
- types?: string;
- severities?: string;
- is_template?: string | boolean;
-}
+type FacetFilter = Pick<
+ SearchRulesQuery,
+ | 'languages'
+ | 'tags'
+ | 'available_since'
+ | 'q'
+ | 'types'
+ | 'severities'
+ | 'repositories'
+ | 'qprofile'
+ | 'sonarsourceSecurity'
+ | 'owaspTop10'
+ | 'owaspTop10-2021'
+ | 'cwe'
+ | 'is_template'
+>;
const FACET_RULE_MAP: { [key: string]: keyof Rule } = {
languages: 'lang',
types: 'type',
+ severities: 'severity',
+ statuses: 'status',
+ tags: 'tags',
};
export const RULE_TAGS_MOCK = ['awesome', 'cute', 'nice'];
isAdmin = false;
applyWithWarning = false;
dismissedNoticesEP = false;
+ standardsToRules: Partial<{ [category in keyof Standards]: { [standard: string]: string[] } }> =
+ {};
+
+ qualityProfilesToRules: { [qp: string]: string[] } = {};
constructor() {
this.repositories = [
- mockRuleRepository({ key: 'repo1' }),
- mockRuleRepository({ key: 'repo2' }),
+ mockRuleRepository({ key: 'repo1', name: 'Repository 1' }),
+ mockRuleRepository({ key: 'repo2', name: 'Repository 2' }),
];
this.qualityProfile = [
mockQualityProfile({ key: 'p1', name: 'QP Foo', language: 'java', languageName: 'Java' }),
this.defaultRules = [
mockRuleDetails({
key: 'rule1',
+ repo: 'repo1',
type: 'BUG',
lang: 'java',
langName: 'Java',
name: 'Awsome java rule',
+ tags: ['awesome'],
params: [
{ key: '1', type: 'TEXT', htmlDesc: 'html description for key 1' },
{ key: '2', type: 'NUMBER', defaultValue: 'default value for key 2' },
}),
mockRuleDetails({
key: 'rule2',
+ repo: 'repo1',
name: 'Hot hotspot',
+ tags: ['awesome'],
type: 'SECURITY_HOTSPOT',
lang: 'js',
descriptionSections: [
],
langName: 'JavaScript',
}),
- mockRuleDetails({ key: 'rule3', name: 'Unknown rule', lang: 'js', langName: 'JavaScript' }),
+ mockRuleDetails({
+ key: 'rule3',
+ repo: 'repo2',
+ name: 'Unknown rule',
+ lang: 'js',
+ langName: 'JavaScript',
+ }),
mockRuleDetails({
key: 'rule4',
type: 'BUG',
severity: 'MINOR',
lang: 'py',
langName: 'Python',
- tags: ['awesome'],
+ tags: ['awesome', 'cute'],
name: 'Custom Rule based on rule8',
params: [
{ key: '1', type: 'TEXT', htmlDesc: 'html description for key 1' },
[this.defaultRules[0].key]: [mockRuleActivation({ qProfile: 'p1' })],
};
+ this.standardsToRules = {
+ [SecurityStandard.SONARSOURCE]: {
+ 'buffer-overflow': ['rule1', 'rule2', 'rule3', 'rule4', 'rule5', 'rule6'],
+ },
+ [SecurityStandard.OWASP_TOP10_2021]: {
+ a2: ['rule1', 'rule2', 'rule3', 'rule4', 'rule5'],
+ },
+ [SecurityStandard.OWASP_TOP10]: {
+ a3: ['rule1', 'rule2', 'rule3', 'rule4'],
+ },
+ [SecurityStandard.CWE]: {
+ '102': ['rule1', 'rule2', 'rule3'],
+ '297': ['rule1', 'rule4'],
+ },
+ };
+
+ this.qualityProfilesToRules = {
+ p3: ['rule1', 'rule2', 'rule3', 'rule4', 'rule5', 'rule6', 'rule7', 'rule8'],
+ };
+
jest.mocked(updateRule).mockImplementation(this.handleUpdateRule);
jest.mocked(createRule).mockImplementation(this.handleCreateRule);
jest.mocked(deleteRule).mockImplementation(this.handleDeleteRule);
jest.mocked(searchRules).mockImplementation(this.handleSearchRules);
jest.mocked(getRuleDetails).mockImplementation(this.handleGetRuleDetails);
+ jest.mocked(getRuleRepositories).mockImplementation(this.handleGetRuleRepositories);
jest.mocked(searchQualityProfiles).mockImplementation(this.handleSearchQualityProfiles);
jest.mocked(getRulesApp).mockImplementation(this.handleGetRulesApp);
jest.mocked(bulkActivateRules).mockImplementation(this.handleBulkActivateRules);
types,
tags,
is_template,
+ repositories,
+ qprofile,
+ sonarsourceSecurity,
+ owaspTop10,
+ 'owaspTop10-2021': owasp2021Top10,
+ cwe,
}: FacetFilter) {
let filteredRules = this.rules;
if (types) {
if (is_template !== undefined) {
filteredRules = filteredRules.filter((r) => (is_template ? r.isTemplate : !r.isTemplate));
}
+ if (repositories) {
+ filteredRules = filteredRules.filter((r) => r.lang && repositories.includes(r.repo));
+ }
+ if (qprofile) {
+ const rules = this.qualityProfilesToRules[qprofile] ?? [];
+ filteredRules = filteredRules.filter((r) => rules.includes(r.key));
+ }
+ if (sonarsourceSecurity) {
+ const matchingRules =
+ this.standardsToRules[SecurityStandard.SONARSOURCE]?.[sonarsourceSecurity] ?? [];
+ filteredRules = filteredRules.filter((r) => matchingRules.includes(r.key));
+ }
+ if (owasp2021Top10) {
+ const matchingRules =
+ this.standardsToRules[SecurityStandard.OWASP_TOP10_2021]?.[owasp2021Top10] ?? [];
+ filteredRules = filteredRules.filter((r) => matchingRules.includes(r.key));
+ }
+ if (owaspTop10) {
+ const matchingRules = this.standardsToRules[SecurityStandard.OWASP_TOP10]?.[owaspTop10] ?? [];
+ filteredRules = filteredRules.filter((r) => matchingRules.includes(r.key));
+ }
+ if (cwe) {
+ const matchingRules = this.standardsToRules[SecurityStandard.CWE]?.[cwe] ?? [];
+ filteredRules = filteredRules.filter((r) => matchingRules.includes(r.key));
+ }
if (q && q.length > 2) {
filteredRules = filteredRules.filter((r) => r.name.includes(q));
}
-
if (tags) {
filteredRules = filteredRules.filter((r) => r.tags && r.tags.some((t) => tags.includes(t)));
}
});
};
+ handleGetRuleRepositories = (parameters: {
+ q: string;
+ }): Promise<Array<{ key: string; language: string; name: string }>> => {
+ return this.reply(this.repositories.filter((r) => r.name.includes(parameters.q)));
+ };
+
handleUpdateRule = (data: RulesUpdateRequest): Promise<RuleDetails> => {
const rule = this.rules.find((r) => r.key === data.key);
if (rule === undefined) {
return this.reply(undefined);
};
- handleSearchRules = ({
+ handleSearchRules = async ({
facets,
types,
languages,
ps,
available_since,
severities,
+ repositories,
+ qprofile,
+ sonarsourceSecurity,
+ owaspTop10,
+ 'owaspTop10-2021': owasp2021Top10,
+ cwe,
tags,
q,
rule_key,
is_template,
}: SearchRulesQuery): Promise<SearchRulesResponse> => {
- const countFacet = (facets || '').split(',').map((facet: keyof Rule) => {
- const facetCount = countBy(
- this.rules.map((r) => r[FACET_RULE_MAP[facet] || facet] as string)
- );
- return {
- property: facet,
- values: Object.keys(facetCount).map((val) => ({ val, count: facetCount[val] })),
- };
- });
- const currentPs = ps || 10;
- const currentP = p || 1;
+ const standards = await getStandards();
+ const facetCounts: Array<{ property: string; values: { val: string; count: number }[] }> = [];
+ for (const facet of facets?.split(',') ?? []) {
+ // If we can count facet values from the list of rules
+ if (FACET_RULE_MAP[facet]) {
+ const counts = countBy(this.rules.map((r) => r[FACET_RULE_MAP[facet]]));
+ const values = Object.keys(counts).map((val) => ({ val, count: counts[val] }));
+ facetCounts.push({
+ property: facet,
+ values,
+ });
+ } else if (facet === 'repositories') {
+ facetCounts.push({
+ property: facet,
+ values: this.repositories.map((repo) => ({
+ val: repo.key,
+ count: this.rules.filter((r) => r.repo === repo.key).length,
+ })),
+ });
+ } else if (typeof (standards as Dict<object>)[facet] === 'object') {
+ // When a standards facet is requested, we return all the values with a count of 1
+ facetCounts.push({
+ property: facet,
+ values: Object.keys((standards as any)[facet]).map((val: string) => ({
+ val,
+ count: 1,
+ })),
+ });
+ } else {
+ facetCounts.push({
+ property: facet,
+ values: [],
+ });
+ }
+ }
+ const currentPs = ps ?? 10;
+ const currentP = p ?? 1;
let filteredRules: Rule[] = [];
if (rule_key) {
filteredRules = this.getRulesWithoutDetails(this.rules).filter((r) => r.key === rule_key);
available_since,
q,
severities,
+ repositories,
types,
tags,
is_template,
+ qprofile,
+ sonarsourceSecurity,
+ owaspTop10,
+ 'owaspTop10-2021': owasp2021Top10,
+ cwe,
});
}
const responseRules = filteredRules.slice((currentP - 1) * currentPs, currentP * currentPs);
return this.reply({
rules: responseRules,
- facets: countFacet,
+ facets: facetCounts,
paging: mockPaging({
total: filteredRules.length,
pageIndex: currentP,
});
describe('filtering', () => {
- it('filters by facets', async () => {
+ it('combine facet filters', async () => {
const { ui, user } = getPageObjects();
const { pickDate } = dateInputEvent(user);
renderCodingRulesApp(mockCurrentUser());
expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
// Filter by language facet
- await user.click(ui.facetItem('py').get());
+ await act(async () => {
+ await user.type(ui.facetSearchInput('search.search_for_languages').get(), 'ja');
+ await user.click(ui.facetItem('JavaScript').get());
+ });
+ expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(2);
+
+ // Clear language facet and search box, and filter by python language
+ await act(async () => {
+ await user.clear(ui.facetSearchInput('search.search_for_languages').get());
+ await user.click(ui.facetItem('py').get());
+ });
expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(6);
// Filter by date facet
- await user.click(await ui.availableSinceFacet.find());
- await pickDate(ui.availableSinceDateField.get(), parseDate('Nov 1, 2022'));
+ await act(async () => {
+ await user.click(await ui.availableSinceFacet.find());
+ await pickDate(ui.availableSinceDateField.get(), parseDate('Nov 1, 2022'));
+ });
expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
// Clear filters
- await user.click(ui.clearAllFiltersButton.get());
+ await act(async () => {
+ await user.click(ui.clearAllFiltersButton.get());
+ });
+ expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
+
+ // Filter by repository
+ await act(async () => {
+ await user.click(ui.repositoriesFacet.get());
+ await user.click(ui.facetItem('Repository 1').get());
+ });
+ expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(2);
+
+ // Search second repository
+ await act(async () => {
+ await user.type(ui.facetSearchInput('search.search_for_repositories').get(), 'y 2');
+ await user.click(ui.facetItem('Repository 2').get());
+ });
+ expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
+
+ // Clear filters
+ await act(async () => {
+ await user.click(ui.clearAllFiltersButton.get());
+ });
+ expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
+
+ // Filter by quality profile
+ await act(async () => {
+ await user.click(ui.qpFacet.get());
+ await user.click(ui.facetItem('QP FooBar Java').get());
+ });
+ expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(8);
+
+ // Filter by tag
+ await act(async () => {
+ await user.click(ui.facetClear('coding_rules.facet.qprofile').get()); // Clear quality profile facet
+ await user.click(ui.tagsFacet.get());
+ await user.click(ui.facetItem('awesome').get());
+ });
+ expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(5);
+
+ // Search by tag
+ await act(async () => {
+ await user.type(ui.facetSearchInput('search.search_for_tags').get(), 'te');
+ await user.click(ui.facetItem('cute').get());
+ });
+ expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(1);
+ });
+
+ it('filter by standards', async () => {
+ const { ui, user } = getPageObjects();
+ renderCodingRulesApp(mockCurrentUser());
+ await ui.appLoaded();
+
+ expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
+ await act(async () => {
+ await user.click(ui.standardsFacet.get());
+ await user.click(ui.facetItem('Buffer Overflow').get());
+ });
+ expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(6);
+
+ await act(async () => {
+ await user.click(ui.standardsOwasp2021Top10Facet.get());
+ await user.click(ui.facetItem('A2 - Cryptographic Failures').get());
+ await user.click(ui.standardsOwasp2021Top10Facet.get()); // Close facet
+ });
+ expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(5);
+
+ await act(async () => {
+ await user.click(ui.standardsOwasp2017Top10Facet.get());
+ await user.click(ui.facetItem('A3 - Sensitive Data Exposure').get());
+ });
+ expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(4);
+
+ await act(async () => {
+ await user.click(ui.standardsCweFacet.get());
+ await user.click(ui.facetItem('CWE-102 - Struts: Duplicate Validation Forms').get());
+ });
+ expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(3);
+
+ await act(async () => {
+ await user.type(ui.facetSearchInput('search.search_for_cwe').get(), 'Certificate');
+ await user.click(
+ ui.facetItem('CWE-297 - Improper Validation of Certificate with Host Mismatch').get()
+ );
+ });
+ expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(2);
+
+ await act(async () => {
+ await user.click(ui.facetClear('issues.facet.standards').get());
+ });
expect(ui.ruleListItem.getAll(ui.rulesList.get())).toHaveLength(10);
});
languages: Languages;
}
-export class LanguageFacet extends React.PureComponent<Props> {
+class LanguageFacet extends React.PureComponent<Props> {
getLanguageName = (languageKey: string) => {
const language = this.props.languages[languageKey];
return language ? language.name : languageKey;
referencedRepositories: Dict<{ key: string; language: string; name: string }>;
}
-export class RepositoryFacet extends React.PureComponent<Props> {
+class RepositoryFacet extends React.PureComponent<Props> {
getLanguageName = (languageKey: string) => {
const { languages } = this.props;
const language = languages[languageKey];
import ListStyleFacet from '../../../components/facet/ListStyleFacet';
import ListStyleFacetFooter from '../../../components/facet/ListStyleFacetFooter';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';
+import DeferredSpinner from '../../../components/ui/DeferredSpinner';
import { translate } from '../../../helpers/l10n';
import { highlightTerm } from '../../../helpers/search';
import {
return `facet_${property}`;
};
- handleHeaderClick = () => {
- this.props.onToggle(this.property);
- };
-
- handleOwaspTop10HeaderClick = () => {
- this.props.onToggle('owaspTop10');
- };
-
- handleOwaspTop102021HeaderClick = () => {
- this.props.onToggle('owaspTop10-2021');
- };
-
- handleSonarSourceSecurityHeaderClick = () => {
- this.props.onToggle('sonarsourceSecurity');
- };
-
handleClear = () => {
this.props.onChange({
[this.property]: [],
: Promise.resolve({});
};
- renderList = (
+ renderOwaspList = (
statsProp: StatsProp,
valuesProp: ValuesProp,
renderName: (standards: Standards, category: string) => string,
const values = this.props[valuesProp];
if (!stats) {
- return null;
+ return <DeferredSpinner className="sw-ml-4" />;
}
const categories = sortBy(Object.keys(stats), (key) => -stats[key]);
};
renderOwaspTop10List() {
- return this.renderList(
+ return this.renderOwaspList(
'owaspTop10Stats',
SecurityStandard.OWASP_TOP10,
renderOwaspTop10Category,
}
renderOwaspTop102021List() {
- return this.renderList(
+ return this.renderOwaspList(
'owaspTop10-2021Stats',
SecurityStandard.OWASP_TOP10_2021,
renderOwaspTop102021Category,
const values = this.props.sonarsourceSecurity;
if (!stats) {
- return null;
+ return <DeferredSpinner className="sw-ml-4" />;
}
const sortedItems = sortBy(
fetching={fetchingSonarSourceSecurity}
id={this.getFacetHeaderId(SecurityStandard.SONARSOURCE)}
name={translate('issues.facet.sonarsourceSecurity')}
- onClick={this.handleSonarSourceSecurityHeaderClick}
+ onClick={() => this.props.onToggle('sonarsourceSecurity')}
open={sonarsourceSecurityOpen}
values={sonarsourceSecurity.map((item) =>
renderSonarSourceSecurityCategory(this.state.standards, item)
fetching={fetchingOwaspTop102021}
id={this.getFacetHeaderId(SecurityStandard.OWASP_TOP10_2021)}
name={translate('issues.facet.owaspTop10_2021')}
- onClick={this.handleOwaspTop102021HeaderClick}
+ onClick={() => this.props.onToggle('owaspTop10-2021')}
open={owaspTop102021Open}
values={owaspTop102021.map((item) =>
renderOwaspTop102021Category(this.state.standards, item)
fetching={fetchingOwaspTop10}
id={this.getFacetHeaderId(SecurityStandard.OWASP_TOP10)}
name={translate('issues.facet.owaspTop10')}
- onClick={this.handleOwaspTop10HeaderClick}
+ onClick={() => this.props.onToggle('owaspTop10')}
open={owaspTop10Open}
values={owaspTop10.map((item) => renderOwaspTop10Category(this.state.standards, item))}
/>
id={this.getFacetHeaderId(this.property)}
name={translate('issues.facet', this.property)}
onClear={this.handleClear}
- onClick={this.handleHeaderClick}
+ onClick={() => this.props.onToggle(this.property)}
open={open}
values={this.getValues()}
/>
severetiesFacet: byRole('button', { name: 'coding_rules.facet.severities' }),
statusesFacet: byRole('button', { name: 'coding_rules.facet.statuses' }),
standardsFacet: byRole('button', { name: 'issues.facet.standards' }),
+ standardsOwasp2017Top10Facet: byRole('button', { name: 'issues.facet.owaspTop10' }),
+ standardsOwasp2021Top10Facet: byRole('button', { name: 'issues.facet.owaspTop10_2021' }),
+ standardsCweFacet: byRole('button', { name: 'issues.facet.cwe' }),
availableSinceFacet: byRole('button', { name: 'coding_rules.facet.available_since' }),
templateFacet: byRole('button', { name: 'coding_rules.facet.template' }),
qpFacet: byRole('button', { name: 'coding_rules.facet.qprofile' }),
+ facetClear: (name: string) => byRole('button', { name: `clear_x_filter.${name}` }),
+ facetSearchInput: (name: string) => byRole('searchbox', { name }),
facetItem: (name: string) => byRole('checkbox', { name }),
availableSinceDateField: byPlaceholderText('date'),
is_template?: boolean | string;
languages?: string;
owaspTop10?: string;
+ ['owaspTop10-2021']?: string;
p?: number;
ps?: number;
q?: string;