[Cloud Posture] include CSP plugin in siem feature (#134564)

This commit is contained in:
Or Ouziel 2022-06-27 18:24:39 +03:00 committed by GitHub
parent 071c878717
commit b3da054084
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 180 additions and 20 deletions

View file

@ -17,6 +17,7 @@ import type { PageUrlParams } from './rules_container';
import * as TEST_SUBJECTS from './test_subjects';
import { useCisKubernetesIntegration } from '../../common/api/use_cis_kubernetes_integration';
import { createReactQueryResponse } from '../../test/fixtures/react_query';
import { coreMock } from '@kbn/core/public/mocks';
jest.mock('./use_csp_integration', () => ({
useCspIntegrationInfo: jest.fn(),
@ -31,9 +32,20 @@ const queryClient = new QueryClient({
const getTestComponent =
(params: PageUrlParams): React.FC =>
() =>
(
<TestProvider>
() => {
const coreStart = coreMock.createStart();
const core = {
...coreStart,
application: {
...coreStart.application,
capabilities: {
...coreStart.application.capabilities,
siem: { crud: true },
},
},
};
return (
<TestProvider core={core}>
<Rules
{...({
match: { params },
@ -41,11 +53,13 @@ const getTestComponent =
/>
</TestProvider>
);
};
describe('<Rules />', () => {
beforeEach(() => {
queryClient.clear();
jest.clearAllMocks();
(useCisKubernetesIntegration as jest.Mock).mockImplementation(() => ({
data: { item: { status: 'installed' } },
}));

View file

@ -14,6 +14,7 @@ import * as TEST_SUBJECTS from './test_subjects';
import { Chance } from 'chance';
import { TestProvider } from '../../test/test_provider';
import { useParams } from 'react-router-dom';
import { coreMock } from '@kbn/core/public/mocks';
const chance = new Chance();
@ -34,9 +35,21 @@ const queryClient = new QueryClient({
});
const getWrapper =
(): React.FC =>
({ children }) =>
<TestProvider>{children}</TestProvider>;
({ canUpdate = true }: { canUpdate: boolean } = { canUpdate: true }): React.FC =>
({ children }) => {
const coreStart = coreMock.createStart();
const core = {
...coreStart,
application: {
...coreStart.application,
capabilities: {
...coreStart.application.capabilities,
siem: { crud: canUpdate },
},
},
};
return <TestProvider core={core}>{children}</TestProvider>;
};
const getRuleMock = ({
packagePolicyId = chance.guid(),
@ -451,4 +464,79 @@ describe('<RulesContainer />', () => {
savedObjectRules: [getSavedObjectRuleEnable(rule, !rule.attributes.enabled)],
});
});
it("disables rule toggling in table for users without 'crud' ui-capability", async () => {
const Wrapper = getWrapper({ canUpdate: false });
const rule1 = getRuleMock({ enabled: true });
(useFindCspRules as jest.Mock).mockReturnValue({
status: 'success',
data: {
total: 1,
savedObjects: [rule1],
},
});
render(
<Wrapper>
<RulesContainer />
</Wrapper>
);
expect(
screen.getByTestId(TEST_SUBJECTS.getCspRulesTableItemSwitchTestId(rule1.id))
).toBeDisabled();
});
it("disables bulk rule toggling in table for users without 'crud' ui-capability", async () => {
const Wrapper = getWrapper({ canUpdate: false });
const rule1 = getRuleMock({ enabled: true });
(useFindCspRules as jest.Mock).mockReturnValue({
status: 'success',
data: {
total: 1,
savedObjects: [rule1],
},
});
render(
<Wrapper>
<RulesContainer />
</Wrapper>
);
fireEvent.click(screen.getByTestId(TEST_SUBJECTS.CSP_RULES_TABLE_BULK_MENU_BUTTON));
expect(screen.getByTestId(TEST_SUBJECTS.CSP_RULES_TABLE_BULK_ENABLE_BUTTON)).toBeDisabled();
expect(screen.getByTestId(TEST_SUBJECTS.CSP_RULES_TABLE_BULK_DISABLE_BUTTON)).toBeDisabled();
});
it("disables rule toggling in flyout for users without 'crud' ui-capability", async () => {
const Wrapper = getWrapper({ canUpdate: false });
const rule1 = getRuleMock({ enabled: true });
(useFindCspRules as jest.Mock).mockReturnValue({
status: 'success',
data: {
total: 1,
savedObjects: [rule1],
},
});
render(
<Wrapper>
<RulesContainer />
</Wrapper>
);
const rowId = TEST_SUBJECTS.getCspRulesTableRowItemTestId(rule1.id);
const switchId = TEST_SUBJECTS.getCspRulesTableItemSwitchTestId(rule1.id);
fireEvent.click(screen.getByTestId(rowId)); // open flyout
const flyout = screen.getByTestId(TEST_SUBJECTS.CSP_RULES_FLYOUT_CONTAINER);
expect(within(flyout).getByTestId(switchId)).toBeDisabled();
});
});

View file

@ -25,6 +25,7 @@ import {
import * as TEST_SUBJECTS from './test_subjects';
import { RuleFlyout } from './rules_flyout';
import { DATA_UPDATE_INFO } from './translations';
import { useKibana } from '../../common/hooks/use_kibana';
interface RulesPageData {
rules_page: RuleSavedObject[];
@ -93,6 +94,7 @@ const MAX_ITEMS_PER_PAGE = 10000;
export type PageUrlParams = Record<'policyId' | 'packagePolicyId', string>;
export const RulesContainer = () => {
const canUpdate = !!useKibana().services.application.capabilities.siem.crud;
const params = useParams<PageUrlParams>();
const tableRef = useRef<EuiBasicTable>(null);
const [changedRules, setChangedRules] = useState<Map<string, RuleSavedObject>>(new Map());
@ -192,6 +194,7 @@ export const RulesContainer = () => {
totalRulesCount={rulesPageData.all_rules.length}
isSearching={status === 'loading'}
lastModified={rulesPageData.lastModified}
canUpdate={canUpdate}
/>
<EuiSpacer />
<RulesTable
@ -212,6 +215,7 @@ export const RulesContainer = () => {
}
setSelectedRuleId={setSelectedRuleId}
selectedRuleId={selectedRuleId}
canUpdate={canUpdate}
/>
</EuiPanel>
{hasChanges && (
@ -222,6 +226,7 @@ export const RulesContainer = () => {
rule={changedRules.get(selectedRuleId) || rulesPageData.rules_map.get(selectedRuleId)!}
onClose={() => setSelectedRuleId(null)}
toggleRule={toggleRule}
canUpdate={canUpdate}
/>
)}
</div>

View file

@ -28,6 +28,7 @@ interface RuleFlyoutProps {
onClose(): void;
toggleRule(rule: RuleSavedObject): void;
rule: RuleSavedObject;
canUpdate: boolean;
}
const tabs = [
@ -37,7 +38,7 @@ const tabs = [
type RuleTab = typeof tabs[number]['id'];
export const RuleFlyout = ({ onClose, rule, toggleRule }: RuleFlyoutProps) => {
export const RuleFlyout = ({ onClose, rule, toggleRule, canUpdate }: RuleFlyoutProps) => {
const [tab, setTab] = useState<RuleTab>('overview');
return (
@ -65,7 +66,9 @@ export const RuleFlyout = ({ onClose, rule, toggleRule }: RuleFlyoutProps) => {
</EuiTabs>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{tab === 'overview' && <RuleOverviewTab rule={rule} toggleRule={() => toggleRule(rule)} />}
{tab === 'overview' && (
<RuleOverviewTab rule={rule} toggleRule={() => toggleRule(rule)} canUpdate={canUpdate} />
)}
{tab === 'remediation' && (
<EuiDescriptionList
compressed={false}
@ -77,11 +80,20 @@ export const RuleFlyout = ({ onClose, rule, toggleRule }: RuleFlyoutProps) => {
);
};
const RuleOverviewTab = ({ rule, toggleRule }: { rule: RuleSavedObject; toggleRule(): void }) => (
const RuleOverviewTab = ({
rule,
toggleRule,
canUpdate,
}: {
rule: RuleSavedObject;
toggleRule(): void;
canUpdate: RuleFlyoutProps['canUpdate'];
}) => (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<span>
<EuiSwitch
disabled={!canUpdate}
label={TEXT.ACTIVATED}
checked={rule.attributes.enabled}
onChange={toggleRule}

View file

@ -31,6 +31,7 @@ type RulesTableProps = Pick<
selectedRuleId: string | null;
// ForwardRef makes this ref not available in parent callbacks
tableRef: React.RefObject<EuiBasicTable<RuleSavedObject>>;
canUpdate: boolean;
};
export const RulesTable = ({
@ -46,11 +47,12 @@ export const RulesTable = ({
loading,
error,
selectedRuleId,
canUpdate,
}: RulesTableProps) => {
const { euiTheme } = useEuiTheme();
const columns = useMemo(
() => getColumns({ toggleRule, setSelectedRuleId }),
[setSelectedRuleId, toggleRule]
() => getColumns({ toggleRule, setSelectedRuleId, canUpdate }),
[setSelectedRuleId, toggleRule, canUpdate]
);
const euiPagination: EuiBasicTableProps<RuleSavedObject>['pagination'] = {
@ -99,13 +101,14 @@ export const RulesTable = ({
);
};
interface GetColumnProps extends Pick<RulesTableProps, 'setSelectedRuleId'> {
interface GetColumnProps extends Pick<RulesTableProps, 'setSelectedRuleId' | 'canUpdate'> {
toggleRule: (rule: RuleSavedObject) => void;
}
const getColumns = ({
toggleRule,
setSelectedRuleId,
canUpdate,
}: GetColumnProps): Array<EuiTableFieldDataColumnType<RuleSavedObject>> => [
{
field: 'attributes.metadata.name',
@ -142,6 +145,7 @@ const getColumns = ({
name: TEXT.ENABLED,
render: (enabled, rule) => (
<EuiSwitch
disabled={!canUpdate}
showLabel={false}
label={enabled ? TEXT.DISABLE : TEXT.ENABLE}
checked={enabled}

View file

@ -33,6 +33,7 @@ interface RulesTableToolbarProps {
searchValue: string;
isSearching: boolean;
lastModified: string | null;
canUpdate: boolean;
}
interface CounterProps {
@ -65,6 +66,7 @@ export const RulesTableHeader = ({
searchValue,
isSearching,
lastModified,
canUpdate,
}: RulesTableToolbarProps) => (
<div>
{lastModified && <LastModificationLabel lastModified={lastModified} />}
@ -77,6 +79,7 @@ export const RulesTableHeader = ({
select={selectAll}
/>
<BulkMenu
canUpdate={canUpdate}
bulkEnable={bulkEnable}
bulkDisable={bulkDisable}
selectedRulesCount={selectedRulesCount}
@ -113,20 +116,24 @@ const BulkMenu = ({
bulkEnable,
bulkDisable,
selectedRulesCount,
}: Pick<RulesTableToolbarProps, 'bulkDisable' | 'bulkEnable' | 'selectedRulesCount'>) => (
canUpdate,
}: Pick<
RulesTableToolbarProps,
'bulkDisable' | 'bulkEnable' | 'selectedRulesCount' | 'canUpdate'
>) => (
<EuiFlexItem grow={false}>
<RulesBulkActionsMenu
items={[
{
icon: 'eye',
disabled: !selectedRulesCount,
disabled: !selectedRulesCount || !canUpdate,
children: <ActivateRulesMenuItemText count={selectedRulesCount} />,
'data-test-subj': TEST_SUBJECTS.CSP_RULES_TABLE_BULK_ENABLE_BUTTON,
onClick: bulkEnable,
},
{
icon: 'eyeClosed',
disabled: !selectedRulesCount,
disabled: !selectedRulesCount || !canUpdate,
children: <DeactivateRulesMenuItemText count={selectedRulesCount} />,
'data-test-subj': TEST_SUBJECTS.CSP_RULES_TABLE_BULK_DISABLE_BUTTON,
onClick: bulkDisable,

View file

@ -201,6 +201,9 @@ export const defineGetBenchmarksRoute = (router: CspRouter, cspContext: CspAppCo
{
path: BENCHMARKS_ROUTE_PATH,
validate: { query: benchmarksInputSchema },
options: {
tags: ['access:cloud-security-posture-read'],
},
},
async (context, request, response) => {
if (!(await context.fleet).authz.fleet.all) {

View file

@ -41,6 +41,9 @@ export const defineGetComplianceDashboardRoute = (
{
path: STATS_ROUTE_PATH,
validate: false,
options: {
tags: ['access:cloud-security-posture-read'],
},
},
async (context, _, response) => {
try {

View file

@ -129,6 +129,9 @@ export const defineUpdateRulesConfigRoute = (router: CspRouter, cspContext: CspA
{
path: UPDATE_RULES_CONFIG_ROUTE_PATH,
validate: { body: configurationUpdateInputSchema },
options: {
tags: ['access:cloud-security-posture-all'],
},
},
async (context, request, response) => {
if (!(await context.fleet).authz.fleet.all) {

View file

@ -23,6 +23,9 @@ export const defineEsPitRoute = (router: CspRouter, cspContext: CspAppContext):
{
path: ES_PIT_ROUTE_PATH,
validate: { query: esPitInputSchema },
options: {
tags: ['access:cloud-security-posture-read'],
},
},
async (context, request, response) => {
if (!(await context.fleet).authz.fleet.all) {

View file

@ -36,6 +36,9 @@ export const defineGetCspSetupStatusRoute = (router: CspRouter, cspContext: CspA
{
path: INFO_ROUTE_PATH,
validate: false,
options: {
tags: ['access:cloud-security-posture-read'],
},
},
async (context, _, response) => {
try {

View file

@ -100,6 +100,11 @@ export const getAlertsSubFeature = (ruleTypes: string[]): SubFeatureConfig => ({
],
});
// Same as the plugin id defined by Cloud Security Posture
const CLOUD_POSTURE_APP_ID = 'csp';
// Same as the saved-object type for rules defined by Cloud Security Posture
const CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE = 'csp_rule';
export const getKibanaPrivilegesFeaturePrivileges = (ruleTypes: string[]): KibanaFeatureConfig => ({
id: SERVER_APP_ID,
name: i18n.translate('xpack.securitySolution.featureRegistry.linkSecuritySolutionTitle', {
@ -107,7 +112,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (ruleTypes: string[]): Kiban
}),
order: 1100,
category: DEFAULT_APP_CATEGORIES.security,
app: [APP_ID, 'kibana'],
app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'],
catalogue: [APP_ID],
management: {
insightsAndAlerting: ['triggersActions'],
@ -116,9 +121,17 @@ export const getKibanaPrivilegesFeaturePrivileges = (ruleTypes: string[]): Kiban
subFeatures: [],
privileges: {
all: {
app: [APP_ID, 'kibana'],
app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'],
catalogue: [APP_ID],
api: [APP_ID, 'lists-all', 'lists-read', 'lists-summary', 'rac'],
api: [
APP_ID,
'lists-all',
'lists-read',
'lists-summary',
'rac',
'cloud-security-posture-all',
'cloud-security-posture-read',
],
savedObject: {
all: [
'alert',
@ -126,6 +139,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (ruleTypes: string[]): Kiban
'exception-list-agnostic',
DATA_VIEW_SAVED_OBJECT_TYPE,
...savedObjectTypes,
CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE,
],
read: [],
},
@ -143,9 +157,9 @@ export const getKibanaPrivilegesFeaturePrivileges = (ruleTypes: string[]): Kiban
ui: ['show', 'crud'],
},
read: {
app: [APP_ID, 'kibana'],
app: [APP_ID, CLOUD_POSTURE_APP_ID, 'kibana'],
catalogue: [APP_ID],
api: [APP_ID, 'lists-read', 'rac'],
api: [APP_ID, 'lists-read', 'rac', 'cloud-security-posture-read'],
savedObject: {
all: [],
read: [
@ -153,6 +167,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (ruleTypes: string[]): Kiban
'exception-list-agnostic',
DATA_VIEW_SAVED_OBJECT_TYPE,
...savedObjectTypes,
CLOUD_POSTURE_SAVED_OBJECT_RULE_TYPE,
],
},
alerting: {