mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Cloud Security] find csp rule template api
This commit is contained in:
parent
9bb0b0dd01
commit
172e84bdc9
15 changed files with 618 additions and 119 deletions
|
@ -2052,11 +2052,37 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"section": {
|
||||
"type": "keyword",
|
||||
"fields": {
|
||||
"text": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"benchmark": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"name": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"posture_type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"version": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"rule_number": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2114,8 +2140,12 @@
|
|||
},
|
||||
"sources": {
|
||||
"properties": {
|
||||
"id": { "type": "keyword" },
|
||||
"revision": { "type": "integer" }
|
||||
"id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"revision": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
|
|
|
@ -80,7 +80,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"config-global": "8e8a134a2952df700d7d4ec51abb794bbd4cf6da",
|
||||
"connector_token": "5a9ac29fe9c740eb114e9c40517245c71706b005",
|
||||
"core-usage-stats": "b3c04da317c957741ebcdedfea4524049fdc79ff",
|
||||
"csp-rule-template": "2a9d5f6481d8ca81d6e5ab0a7cc4ba0b59a93420",
|
||||
"csp-rule-template": "c151324d5f85178169395eecb12bac6b96064654",
|
||||
"dashboard": "cf7c9c2334decab716fe519780cb4dc52967a91d",
|
||||
"endpoint:user-artifact-manifest": "1c3533161811a58772e30cdc77bac4631da3ef2b",
|
||||
"enterprise_search_telemetry": "9ac912e1417fc8681e0cd383775382117c9e3d3d",
|
||||
|
|
|
@ -10,6 +10,7 @@ import { PostureTypes } from './types';
|
|||
export const STATUS_ROUTE_PATH = '/internal/cloud_security_posture/status';
|
||||
export const STATS_ROUTE_PATH = '/internal/cloud_security_posture/stats/{policy_template}';
|
||||
export const BENCHMARKS_ROUTE_PATH = '/internal/cloud_security_posture/benchmarks';
|
||||
export const FIND_CSP_RULE_TEMPLATE_ROUTE_PATH = '/internal/cloud_security_posture/rules/_find';
|
||||
|
||||
export const CLOUD_SECURITY_POSTURE_PACKAGE_NAME = 'cloud_security_posture';
|
||||
// TODO: REMOVE CSP_LATEST_FINDINGS_DATA_VIEW and replace it with LATEST_FINDINGS_INDEX_PATTERN
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
// this pages follows versioning interface strategy https://docs.elastic.dev/kibana-dev-docs/versioning-interfaces
|
||||
|
||||
const DEFAULT_RULES_TEMPLATE_PER_PAGE = 25;
|
||||
|
||||
export const findCspRuleTemplateRequest = schema.object({
|
||||
/**
|
||||
* An Elasticsearch simple_query_string
|
||||
*/
|
||||
search: schema.maybe(schema.string()),
|
||||
|
||||
/**
|
||||
* The page of objects to return
|
||||
*/
|
||||
page: schema.number({ defaultValue: 1, min: 1 }),
|
||||
|
||||
/**
|
||||
* The number of objects to include in each page
|
||||
*/
|
||||
perPage: schema.number({ defaultValue: DEFAULT_RULES_TEMPLATE_PER_PAGE, min: 0 }),
|
||||
|
||||
/**
|
||||
* Fields to retrieve from CspRuleTemplate saved object
|
||||
*/
|
||||
fields: schema.maybe(schema.arrayOf(schema.string())),
|
||||
|
||||
/**
|
||||
* The fields to perform the parsed query against.
|
||||
* Valid fields are fields which mapped to 'text' in cspRuleTemplateSavedObjectMapping
|
||||
*/
|
||||
searchFields: schema.arrayOf(
|
||||
schema.oneOf([schema.literal('metadata.name.text'), schema.literal('metadata.section.text')]),
|
||||
{ defaultValue: ['metadata.name.text'] }
|
||||
),
|
||||
|
||||
/**
|
||||
* Sort Field
|
||||
*/
|
||||
sortField: schema.oneOf(
|
||||
[
|
||||
schema.literal('metadata.name'),
|
||||
schema.literal('metadata.section'),
|
||||
schema.literal('metadata.id'),
|
||||
schema.literal('metadata.version'),
|
||||
schema.literal('metadata.benchmark.id'),
|
||||
schema.literal('metadata.benchmark.name'),
|
||||
schema.literal('metadata.benchmark.posture_type'),
|
||||
schema.literal('metadata.benchmark.version'),
|
||||
schema.literal('metadata.benchmark.rule_number'),
|
||||
],
|
||||
{
|
||||
defaultValue: 'metadata.name',
|
||||
}
|
||||
),
|
||||
|
||||
/**
|
||||
* The order to sort by
|
||||
*/
|
||||
sortOrder: schema.oneOf([schema.literal('asc'), schema.literal('desc')], {
|
||||
defaultValue: 'asc',
|
||||
}),
|
||||
|
||||
/**
|
||||
* benchmark id
|
||||
*/
|
||||
benchmarkId: schema.maybe(
|
||||
schema.oneOf([schema.literal('cis_k8s'), schema.literal('cis_eks'), schema.literal('cis_aws')])
|
||||
),
|
||||
|
||||
/**
|
||||
* package_policy_id
|
||||
*/
|
||||
packagePolicyId: schema.maybe(schema.string()),
|
||||
});
|
|
@ -4,11 +4,13 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { type TypeOf } from '@kbn/config-schema';
|
||||
import type { PackagePolicy, AgentPolicy } from '@kbn/fleet-plugin/common';
|
||||
import { CspFinding } from './schemas/csp_finding';
|
||||
import { SUPPORTED_CLOUDBEAT_INPUTS, SUPPORTED_POLICY_TEMPLATES } from './constants';
|
||||
import { CspRuleTemplateMetadata } from './schemas/csp_rule_template_metadata';
|
||||
import { CspRuleTemplate } from './schemas';
|
||||
import { findCspRuleTemplateRequest } from './schemas/csp_rule_template_api/get_csp_rule_template';
|
||||
|
||||
export type Evaluation = 'passed' | 'failed' | 'NA';
|
||||
|
||||
|
@ -114,3 +116,12 @@ export interface BenchmarkResponse {
|
|||
page: number;
|
||||
perPage: number;
|
||||
}
|
||||
|
||||
export type GetCspRuleTemplateRequest = TypeOf<typeof findCspRuleTemplateRequest>;
|
||||
|
||||
export interface GetCspRuleTemplateResponse {
|
||||
items: CspRuleTemplate[];
|
||||
total: number;
|
||||
page: number;
|
||||
perPage: number;
|
||||
}
|
||||
|
|
|
@ -9,12 +9,13 @@ import React from 'react';
|
|||
import { RulesContainer } from './rules_container';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { useFindCspRuleTemplates, type RuleSavedObject } from './use_csp_rules';
|
||||
import { useFindCspRuleTemplates } from './use_csp_rules';
|
||||
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';
|
||||
import { CspRuleTemplate } from '../../../common/schemas';
|
||||
|
||||
const chance = new Chance();
|
||||
|
||||
|
@ -51,41 +52,30 @@ const getWrapper =
|
|||
return <TestProvider core={core}>{children}</TestProvider>;
|
||||
};
|
||||
|
||||
const getRuleMock = ({
|
||||
savedObjectId = chance.guid(),
|
||||
id = chance.guid(),
|
||||
}: {
|
||||
savedObjectId?: string;
|
||||
id?: string;
|
||||
enabled: boolean;
|
||||
}): RuleSavedObject =>
|
||||
const getRuleMock = (id = chance.guid()): CspRuleTemplate =>
|
||||
({
|
||||
id: savedObjectId,
|
||||
updatedAt: chance.date().toISOString(),
|
||||
attributes: {
|
||||
metadata: {
|
||||
audit: chance.sentence(),
|
||||
benchmark: {
|
||||
name: chance.word(),
|
||||
version: chance.sentence(),
|
||||
id: chance.word(),
|
||||
},
|
||||
default_value: chance.sentence(),
|
||||
description: chance.sentence(),
|
||||
id,
|
||||
impact: chance.sentence(),
|
||||
name: chance.sentence(),
|
||||
profile_applicability: chance.sentence(),
|
||||
rationale: chance.sentence(),
|
||||
references: chance.sentence(),
|
||||
rego_rule_id: chance.word(),
|
||||
remediation: chance.sentence(),
|
||||
section: chance.sentence(),
|
||||
tags: [chance.word(), chance.word()],
|
||||
metadata: {
|
||||
audit: chance.sentence(),
|
||||
benchmark: {
|
||||
name: chance.word(),
|
||||
version: chance.sentence(),
|
||||
id: chance.word(),
|
||||
},
|
||||
default_value: chance.sentence(),
|
||||
description: chance.sentence(),
|
||||
id,
|
||||
impact: chance.sentence(),
|
||||
name: chance.sentence(),
|
||||
profile_applicability: chance.sentence(),
|
||||
rationale: chance.sentence(),
|
||||
references: chance.sentence(),
|
||||
rego_rule_id: chance.word(),
|
||||
remediation: chance.sentence(),
|
||||
section: chance.sentence(),
|
||||
tags: [chance.word(), chance.word()],
|
||||
version: chance.sentence(),
|
||||
},
|
||||
} as RuleSavedObject);
|
||||
} as CspRuleTemplate);
|
||||
|
||||
const params = {
|
||||
packagePolicyId: chance.guid(),
|
||||
|
@ -101,15 +91,14 @@ describe('<RulesContainer />', () => {
|
|||
|
||||
it('displays rules with their initial state', async () => {
|
||||
const Wrapper = getWrapper();
|
||||
const rule1 = getRuleMock({ enabled: true });
|
||||
const rule1 = getRuleMock();
|
||||
|
||||
(useFindCspRuleTemplates as jest.Mock).mockReturnValue({
|
||||
status: 'success',
|
||||
data: {
|
||||
total: 1,
|
||||
savedObjects: [rule1],
|
||||
items: [rule1],
|
||||
},
|
||||
policyId: params.packagePolicyId,
|
||||
});
|
||||
|
||||
render(
|
||||
|
@ -119,6 +108,6 @@ describe('<RulesContainer />', () => {
|
|||
);
|
||||
|
||||
expect(await screen.findByTestId(TEST_SUBJECTS.CSP_RULES_CONTAINER)).toBeInTheDocument();
|
||||
expect(await screen.findByText(rule1.attributes.metadata.name)).toBeInTheDocument();
|
||||
expect(await screen.findByText(rule1.metadata.name)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,28 +7,23 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import { EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { extractErrorMessage, isNonNullable } from '../../../common/utils/helpers';
|
||||
import { CspRuleTemplate } from '../../../common/schemas';
|
||||
import { extractErrorMessage } from '../../../common/utils/helpers';
|
||||
import { RulesTable } from './rules_table';
|
||||
import { RulesTableHeader } from './rules_table_header';
|
||||
import {
|
||||
useFindCspRuleTemplates,
|
||||
type RuleSavedObject,
|
||||
type RulesQuery,
|
||||
type RulesQueryResult,
|
||||
} from './use_csp_rules';
|
||||
import { useFindCspRuleTemplates, type RulesQuery, type RulesQueryResult } from './use_csp_rules';
|
||||
import * as TEST_SUBJECTS from './test_subjects';
|
||||
import { RuleFlyout } from './rules_flyout';
|
||||
import { LOCAL_STORAGE_PAGE_SIZE_RULES_KEY } from '../../common/constants';
|
||||
import { usePageSize } from '../../common/hooks/use_page_size';
|
||||
|
||||
interface RulesPageData {
|
||||
rules_page: RuleSavedObject[];
|
||||
all_rules: RuleSavedObject[];
|
||||
rules_map: Map<string, RuleSavedObject>;
|
||||
rules_page: CspRuleTemplate[];
|
||||
all_rules: CspRuleTemplate[];
|
||||
rules_map: Map<string, CspRuleTemplate>;
|
||||
total: number;
|
||||
error?: string;
|
||||
loading: boolean;
|
||||
lastModified: string | null;
|
||||
}
|
||||
|
||||
export type RulesState = RulesPageData & RulesQuery;
|
||||
|
@ -37,26 +32,21 @@ const getRulesPageData = (
|
|||
{ status, data, error }: Pick<RulesQueryResult, 'data' | 'status' | 'error'>,
|
||||
query: RulesQuery
|
||||
): RulesPageData => {
|
||||
const rules = data?.savedObjects || [];
|
||||
const rules = data?.items || ([] as CspRuleTemplate[]);
|
||||
|
||||
const page = getPage(rules, query);
|
||||
|
||||
return {
|
||||
loading: status === 'loading',
|
||||
error: error ? extractErrorMessage(error) : undefined,
|
||||
all_rules: rules,
|
||||
rules_map: new Map(rules.map((rule) => [rule.id, rule])),
|
||||
rules_map: new Map(rules.map((rule) => [rule.metadata.id, rule])),
|
||||
rules_page: page,
|
||||
total: data?.total || 0,
|
||||
lastModified: getLastModified(rules) || null,
|
||||
};
|
||||
};
|
||||
|
||||
const getLastModified = (data: RuleSavedObject[]): string | undefined =>
|
||||
data
|
||||
.map((v) => v.updatedAt)
|
||||
.filter(isNonNullable)
|
||||
.sort((a, b) => new Date(b).getTime() - new Date(a).getTime())[0];
|
||||
|
||||
const getPage = (data: readonly RuleSavedObject[], { page, perPage }: RulesQuery) =>
|
||||
const getPage = (data: CspRuleTemplate[], { page, perPage }: RulesQuery) =>
|
||||
data.slice(page * perPage, (page + 1) * perPage);
|
||||
|
||||
const MAX_ITEMS_PER_PAGE = 10000;
|
||||
|
@ -68,7 +58,6 @@ export const RulesContainer = () => {
|
|||
const [selectedRuleId, setSelectedRuleId] = useState<string | null>(null);
|
||||
const { pageSize, setPageSize } = usePageSize(LOCAL_STORAGE_PAGE_SIZE_RULES_KEY);
|
||||
const [rulesQuery, setRulesQuery] = useState<RulesQuery>({
|
||||
filter: '',
|
||||
search: '',
|
||||
page: 0,
|
||||
perPage: pageSize || 10,
|
||||
|
@ -76,7 +65,6 @@ export const RulesContainer = () => {
|
|||
|
||||
const { data, status, error } = useFindCspRuleTemplates(
|
||||
{
|
||||
filter: rulesQuery.filter,
|
||||
search: rulesQuery.search,
|
||||
page: 1,
|
||||
perPage: MAX_ITEMS_PER_PAGE,
|
||||
|
|
|
@ -18,14 +18,14 @@ import {
|
|||
EuiFlexGroup,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CspRuleTemplate, CspRuleTemplateMetadata } from '../../../common/schemas';
|
||||
import { getRuleList } from '../configurations/findings_flyout/rule_tab';
|
||||
import { getRemediationList } from '../configurations/findings_flyout/overview_tab';
|
||||
import type { RuleSavedObject } from './use_csp_rules';
|
||||
import * as TEST_SUBJECTS from './test_subjects';
|
||||
|
||||
interface RuleFlyoutProps {
|
||||
onClose(): void;
|
||||
rule: RuleSavedObject;
|
||||
rule: CspRuleTemplate;
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
|
@ -59,7 +59,7 @@ export const RuleFlyout = ({ onClose, rule }: RuleFlyoutProps) => {
|
|||
>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="l">
|
||||
<h2>{rule.attributes.metadata.name}</h2>
|
||||
<h2>{rule.metadata.name}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiTabs>
|
||||
|
@ -76,22 +76,19 @@ export const RuleFlyout = ({ onClose, rule }: RuleFlyoutProps) => {
|
|||
</EuiTabs>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
{tab === 'overview' && <RuleOverviewTab rule={rule} />}
|
||||
{tab === 'overview' && <RuleOverviewTab rule={rule.metadata} />}
|
||||
{tab === 'remediation' && (
|
||||
<EuiDescriptionList
|
||||
compressed={false}
|
||||
listItems={getRemediationList(rule.attributes.metadata)}
|
||||
/>
|
||||
<EuiDescriptionList compressed={false} listItems={getRemediationList(rule.metadata)} />
|
||||
)}
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
||||
|
||||
const RuleOverviewTab = ({ rule }: { rule: RuleSavedObject }) => (
|
||||
const RuleOverviewTab = ({ rule }: { rule: CspRuleTemplateMetadata }) => (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList listItems={getRuleList(rule.attributes.metadata)} />
|
||||
<EuiDescriptionList listItems={getRuleList(rule)} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -14,9 +14,9 @@ import {
|
|||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CspRuleTemplate } from '../../../common/schemas';
|
||||
import type { RulesState } from './rules_container';
|
||||
import * as TEST_SUBJECTS from './test_subjects';
|
||||
import type { RuleSavedObject } from './use_csp_rules';
|
||||
|
||||
type RulesTableProps = Pick<
|
||||
RulesState,
|
||||
|
@ -41,26 +41,28 @@ export const RulesTable = ({
|
|||
const { euiTheme } = useEuiTheme();
|
||||
const columns = useMemo(() => getColumns({ setSelectedRuleId }), [setSelectedRuleId]);
|
||||
|
||||
const euiPagination: EuiBasicTableProps<RuleSavedObject>['pagination'] = {
|
||||
const euiPagination: EuiBasicTableProps<CspRuleTemplate>['pagination'] = {
|
||||
pageIndex: page,
|
||||
pageSize,
|
||||
totalItemCount: total,
|
||||
pageSizeOptions: [10, 25, 100],
|
||||
};
|
||||
|
||||
const onTableChange = ({ page: pagination }: Criteria<RuleSavedObject>) => {
|
||||
const onTableChange = ({ page: pagination }: Criteria<CspRuleTemplate>) => {
|
||||
if (!pagination) return;
|
||||
setPagination({ page: pagination.index, perPage: pagination.size });
|
||||
};
|
||||
|
||||
const rowProps = (row: RuleSavedObject) => ({
|
||||
['data-test-subj']: TEST_SUBJECTS.getCspRuleTemplatesTableRowItemTestId(row.id),
|
||||
style: { background: row.id === selectedRuleId ? euiTheme.colors.highlight : undefined },
|
||||
const rowProps = (row: CspRuleTemplate) => ({
|
||||
['data-test-subj']: TEST_SUBJECTS.getCspRuleTemplatesTableRowItemTestId(row.metadata.id),
|
||||
style: {
|
||||
background: row.metadata.id === selectedRuleId ? euiTheme.colors.highlight : undefined,
|
||||
},
|
||||
onClick: (e: MouseEvent) => {
|
||||
const tag = (e.target as HTMLDivElement).tagName;
|
||||
// Ignore checkbox and switch toggle columns
|
||||
if (tag === 'BUTTON' || tag === 'INPUT') return;
|
||||
setSelectedRuleId(row.id);
|
||||
setSelectedRuleId(row.metadata.id);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -73,7 +75,7 @@ export const RulesTable = ({
|
|||
columns={columns}
|
||||
pagination={euiPagination}
|
||||
onChange={onTableChange}
|
||||
itemId={(v) => v.id}
|
||||
itemId={(v) => v.metadata.id}
|
||||
rowProps={rowProps}
|
||||
/>
|
||||
);
|
||||
|
@ -83,9 +85,9 @@ type GetColumnProps = Pick<RulesTableProps, 'setSelectedRuleId'>;
|
|||
|
||||
const getColumns = ({
|
||||
setSelectedRuleId,
|
||||
}: GetColumnProps): Array<EuiTableFieldDataColumnType<RuleSavedObject>> => [
|
||||
}: GetColumnProps): Array<EuiTableFieldDataColumnType<CspRuleTemplate>> => [
|
||||
{
|
||||
field: 'attributes.metadata.name',
|
||||
field: 'metadata.name',
|
||||
name: i18n.translate('xpack.csp.rules.rulesTable.nameColumnLabel', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
|
@ -97,7 +99,7 @@ const getColumns = ({
|
|||
title={name}
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
setSelectedRuleId(rule.id);
|
||||
setSelectedRuleId(rule.metadata.id);
|
||||
}}
|
||||
data-test-subj={TEST_SUBJECTS.CSP_RULES_TABLE_ROW_ITEM_NAME}
|
||||
>
|
||||
|
@ -106,7 +108,7 @@ const getColumns = ({
|
|||
),
|
||||
},
|
||||
{
|
||||
field: 'attributes.metadata.section',
|
||||
field: 'metadata.section',
|
||||
name: i18n.translate('xpack.csp.rules.rulesTable.cisSectionColumnLabel', {
|
||||
defaultMessage: 'CIS Section',
|
||||
}),
|
||||
|
|
|
@ -5,48 +5,29 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { FunctionKeys } from 'utility-types';
|
||||
import type { SavedObjectsFindOptions, SimpleSavedObject } from '@kbn/core/public';
|
||||
import { NewPackagePolicy, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
|
||||
import {
|
||||
getBenchmarkFromPackagePolicy,
|
||||
getBenchmarkTypeFilter,
|
||||
} from '../../../common/utils/helpers';
|
||||
import { CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE } from '../../../common/constants';
|
||||
import { CspRuleTemplate } from '../../../common/schemas';
|
||||
import { GetCspRuleTemplateRequest, GetCspRuleTemplateResponse } from '../../../common/types';
|
||||
import { useKibana } from '../../common/hooks/use_kibana';
|
||||
|
||||
export type RuleSavedObject = Omit<
|
||||
SimpleSavedObject<CspRuleTemplate>,
|
||||
FunctionKeys<SimpleSavedObject>
|
||||
>;
|
||||
import {
|
||||
CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE,
|
||||
FIND_CSP_RULE_TEMPLATE_ROUTE_PATH,
|
||||
} from '../../../common/constants';
|
||||
|
||||
export type RulesQuery = Required<
|
||||
Pick<SavedObjectsFindOptions, 'search' | 'page' | 'perPage' | 'filter'>
|
||||
>;
|
||||
export type RulesQuery = Required<Pick<GetCspRuleTemplateRequest, 'search' | 'page' | 'perPage'>>;
|
||||
export type RulesQueryResult = ReturnType<typeof useFindCspRuleTemplates>;
|
||||
|
||||
export const useFindCspRuleTemplates = (
|
||||
{ search, page, perPage, filter }: RulesQuery,
|
||||
{ search, page, perPage }: RulesQuery,
|
||||
packagePolicyId: string
|
||||
) => {
|
||||
const { savedObjects } = useKibana().services;
|
||||
const { http } = useKibana().services;
|
||||
|
||||
return useQuery([CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE, { search, page, perPage }], () =>
|
||||
savedObjects.client
|
||||
.get<NewPackagePolicy>(PACKAGE_POLICY_SAVED_OBJECT_TYPE, packagePolicyId)
|
||||
.then((res) => {
|
||||
const benchmarkId = getBenchmarkFromPackagePolicy(res.attributes.inputs);
|
||||
|
||||
return savedObjects.client.find<CspRuleTemplate>({
|
||||
type: CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE,
|
||||
search: search ? `"${search}"*` : '',
|
||||
searchFields: ['metadata.name.text'],
|
||||
page: 1,
|
||||
sortField: 'metadata.name',
|
||||
perPage,
|
||||
filter: getBenchmarkTypeFilter(benchmarkId),
|
||||
});
|
||||
})
|
||||
return useQuery(
|
||||
[CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE, { search, page, perPage, packagePolicyId }],
|
||||
() => {
|
||||
return http.get<GetCspRuleTemplateResponse>(FIND_CSP_RULE_TEMPLATE_ROUTE_PATH, {
|
||||
query: { packagePolicyId, page, perPage },
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { NewPackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
|
||||
import pMap from 'p-map';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { GetCspRuleTemplateRequest, GetCspRuleTemplateResponse } from '../../../common/types';
|
||||
import { CspRuleTemplate } from '../../../common/schemas';
|
||||
import { findCspRuleTemplateRequest } from '../../../common/schemas/csp_rule_template_api/get_csp_rule_template';
|
||||
import {
|
||||
getBenchmarkFromPackagePolicy,
|
||||
getBenchmarkTypeFilter,
|
||||
} from '../../../common/utils/helpers';
|
||||
|
||||
import {
|
||||
CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE,
|
||||
FIND_CSP_RULE_TEMPLATE_ROUTE_PATH,
|
||||
} from '../../../common/constants';
|
||||
import { CspRouter } from '../../types';
|
||||
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../benchmarks/benchmarks';
|
||||
|
||||
const getBenchmarkIdFromPackagePolicyId = async (
|
||||
soClient: SavedObjectsClientContract,
|
||||
packagePolicyId: string
|
||||
): Promise<string> => {
|
||||
const res = await soClient.get<NewPackagePolicy>(
|
||||
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
packagePolicyId
|
||||
);
|
||||
return getBenchmarkFromPackagePolicy(res.attributes.inputs);
|
||||
};
|
||||
|
||||
const findCspRuleTemplateHandler = async (
|
||||
soClient: SavedObjectsClientContract,
|
||||
options: GetCspRuleTemplateRequest
|
||||
): Promise<GetCspRuleTemplateResponse> => {
|
||||
if (
|
||||
(!options.packagePolicyId && !options.benchmarkId) ||
|
||||
(options.packagePolicyId && options.benchmarkId)
|
||||
) {
|
||||
throw new Error('Please provide either benchmarkId or packagePolicyId, but not both');
|
||||
}
|
||||
|
||||
const benchmarkId = options.benchmarkId
|
||||
? options.benchmarkId
|
||||
: await getBenchmarkIdFromPackagePolicyId(soClient, options.packagePolicyId!);
|
||||
|
||||
const cspRulesTemplatesSo = await soClient.find<CspRuleTemplate>({
|
||||
type: CSP_RULE_TEMPLATE_SAVED_OBJECT_TYPE,
|
||||
searchFields: options.searchFields,
|
||||
search: options.search ? `"${options.search}"*` : '',
|
||||
page: options.page,
|
||||
perPage: options.perPage,
|
||||
sortField: options.sortField,
|
||||
fields: options?.fields,
|
||||
filter: getBenchmarkTypeFilter(benchmarkId),
|
||||
});
|
||||
|
||||
const cspRulesTemplates = await pMap(
|
||||
cspRulesTemplatesSo.saved_objects,
|
||||
async (cspRuleTemplate) => {
|
||||
return { ...cspRuleTemplate.attributes };
|
||||
},
|
||||
{ concurrency: 50 }
|
||||
);
|
||||
|
||||
return {
|
||||
items: cspRulesTemplates,
|
||||
total: cspRulesTemplatesSo.total,
|
||||
page: options.page,
|
||||
perPage: options.perPage,
|
||||
};
|
||||
};
|
||||
|
||||
export const defineFindCspRuleTemplateRoute = (router: CspRouter) =>
|
||||
router.versioned
|
||||
.get({
|
||||
access: 'internal',
|
||||
path: FIND_CSP_RULE_TEMPLATE_ROUTE_PATH,
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
validate: {
|
||||
request: {
|
||||
query: findCspRuleTemplateRequest,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
if (!(await context.fleet).authz.fleet.all) {
|
||||
return response.forbidden();
|
||||
}
|
||||
|
||||
const requestBody: GetCspRuleTemplateRequest = request.query;
|
||||
const cspContext = await context.csp;
|
||||
|
||||
try {
|
||||
const cspRulesTemplates: GetCspRuleTemplateResponse = await findCspRuleTemplateHandler(
|
||||
cspContext.soClient,
|
||||
requestBody
|
||||
);
|
||||
return response.ok({ body: cspRulesTemplates });
|
||||
} catch (err) {
|
||||
const error = transformError(err);
|
||||
cspContext.logger.error(`Failed to fetch csp rules templates ${err}`);
|
||||
return response.customError({
|
||||
body: { message: error.message },
|
||||
statusCode: error.statusCode,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
|
@ -16,6 +16,7 @@ import { PLUGIN_ID } from '../../common';
|
|||
import { defineGetComplianceDashboardRoute } from './compliance_dashboard/compliance_dashboard';
|
||||
import { defineGetBenchmarksRoute } from './benchmarks/benchmarks';
|
||||
import { defineGetCspStatusRoute } from './status/status';
|
||||
import { defineFindCspRuleTemplateRoute } from './csp_rule_template/get_csp_rule_template';
|
||||
|
||||
/**
|
||||
* 1. Registers routes
|
||||
|
@ -34,6 +35,7 @@ export function setupRoutes({
|
|||
defineGetComplianceDashboardRoute(router);
|
||||
defineGetBenchmarksRoute(router);
|
||||
defineGetCspStatusRoute(router);
|
||||
defineFindCspRuleTemplateRoute(router);
|
||||
|
||||
core.http.registerRouteHandlerContext<CspRequestHandlerContext, typeof PLUGIN_ID>(
|
||||
PLUGIN_ID,
|
||||
|
|
|
@ -21,6 +21,20 @@ export const cspRuleTemplateSavedObjectMapping: SavedObjectsTypeMappingDefinitio
|
|||
},
|
||||
},
|
||||
},
|
||||
id: {
|
||||
type: 'keyword',
|
||||
},
|
||||
section: {
|
||||
type: 'keyword',
|
||||
fields: {
|
||||
text: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
version: {
|
||||
type: 'keyword',
|
||||
},
|
||||
benchmark: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
@ -28,6 +42,18 @@ export const cspRuleTemplateSavedObjectMapping: SavedObjectsTypeMappingDefinitio
|
|||
// Needed for filtering rule templates by benchmark.id
|
||||
type: 'keyword',
|
||||
},
|
||||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
posture_type: {
|
||||
type: 'keyword',
|
||||
},
|
||||
version: {
|
||||
type: 'keyword',
|
||||
},
|
||||
rule_number: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import expect from '@kbn/expect';
|
||||
import type { GetCspRuleTemplateResponse } from '@kbn/cloud-security-posture-plugin/common/types';
|
||||
import type { SuperTest, Test } from 'supertest';
|
||||
import { CspRuleTemplate } from '@kbn/cloud-security-posture-plugin/common/schemas';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('GET internal/cloud_security_posture/rules/_find', () => {
|
||||
let agentPolicyId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
|
||||
|
||||
const { body: agentPolicyResponse } = await supertest
|
||||
.post(`/api/fleet/agent_policies`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: 'Test policy',
|
||||
namespace: 'default',
|
||||
});
|
||||
|
||||
agentPolicyId = agentPolicyResponse.item.id;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
|
||||
});
|
||||
|
||||
it(`Should return 500 error code when not provide package policy id or benchmark id`, async () => {
|
||||
await createPackagePolicy(
|
||||
supertest,
|
||||
agentPolicyId,
|
||||
'kspm',
|
||||
'cloudbeat/cis_k8s',
|
||||
'vanilla',
|
||||
'kspm'
|
||||
);
|
||||
|
||||
const { body }: { body: { message: string } } = await supertest
|
||||
.get(`/internal/cloud_security_posture/rules/_find`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.expect(500);
|
||||
|
||||
expect(body.message).to.be(
|
||||
'Please provide either benchmarkId or packagePolicyId, but not both'
|
||||
);
|
||||
});
|
||||
|
||||
it(`Should return 500 error code when provide both package policy id and benchmark id`, async () => {
|
||||
await createPackagePolicy(
|
||||
supertest,
|
||||
agentPolicyId,
|
||||
'kspm',
|
||||
'cloudbeat/cis_k8s',
|
||||
'vanilla',
|
||||
'kspm'
|
||||
);
|
||||
|
||||
const { body }: { body: { message: string } } = await supertest
|
||||
.get(`/internal/cloud_security_posture/rules/_find`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.query({
|
||||
packagePolicyId: 'your-package-policy-id',
|
||||
benchmarkId: 'cis_aws',
|
||||
})
|
||||
.expect(500);
|
||||
|
||||
expect(body.message).to.be(
|
||||
'Please provide either benchmarkId or packagePolicyId, but not both'
|
||||
);
|
||||
});
|
||||
|
||||
it(`Should return 404 status code when the package policy ID does not exist`, async () => {
|
||||
const { body }: { body: { statusCode: number; error: string } } = await supertest
|
||||
.get(`/internal/cloud_security_posture/rules/_find`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.query({
|
||||
packagePolicyId: 'non-existing-packagePolicy-id',
|
||||
})
|
||||
.expect(404);
|
||||
|
||||
expect(body.statusCode).to.be(404);
|
||||
expect(body.error).to.be('Not Found');
|
||||
});
|
||||
|
||||
it(`Should return 200 status code and filter rules by benchmarkId`, async () => {
|
||||
await createPackagePolicy(
|
||||
supertest,
|
||||
agentPolicyId,
|
||||
'kspm',
|
||||
'cloudbeat/cis_k8s',
|
||||
'vanilla',
|
||||
'kspm'
|
||||
);
|
||||
|
||||
const { body }: { body: GetCspRuleTemplateResponse } = await supertest
|
||||
.get(`/internal/cloud_security_posture/rules/_find`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.query({
|
||||
benchmarkId: 'cis_k8s',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body.items.length).greaterThan(0);
|
||||
|
||||
const allRulesHaveCorrectBenchmarkId = body.items.every(
|
||||
(rule: CspRuleTemplate) => rule.metadata.benchmark.id === 'cis_k8s'
|
||||
);
|
||||
|
||||
expect(allRulesHaveCorrectBenchmarkId).to.be(true);
|
||||
});
|
||||
|
||||
it(`Should return 200 status code, and only requested fields in the response`, async () => {
|
||||
await createPackagePolicy(
|
||||
supertest,
|
||||
agentPolicyId,
|
||||
'kspm',
|
||||
'cloudbeat/cis_k8s',
|
||||
'vanilla',
|
||||
'kspm'
|
||||
);
|
||||
|
||||
const { body }: { body: GetCspRuleTemplateResponse } = await supertest
|
||||
.get(`/internal/cloud_security_posture/rules/_find`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.query({
|
||||
benchmarkId: 'cis_k8s',
|
||||
fields: ['metadata.name', 'metadata.section', 'metadata.id'],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body.items.length).greaterThan(0);
|
||||
|
||||
const allowedFields = ['name', 'section', 'id'];
|
||||
const fieldsMatched = body.items.every((rule: CspRuleTemplate) => {
|
||||
const keys = Object.keys(rule.metadata);
|
||||
return (
|
||||
keys.length === allowedFields.length && keys.every((key) => allowedFields.includes(key))
|
||||
);
|
||||
});
|
||||
|
||||
expect(fieldsMatched).to.be(true);
|
||||
});
|
||||
|
||||
it(`Should return 200 status code, items sorted by metadata.section field`, async () => {
|
||||
await createPackagePolicy(
|
||||
supertest,
|
||||
agentPolicyId,
|
||||
'kspm',
|
||||
'cloudbeat/cis_k8s',
|
||||
'vanilla',
|
||||
'kspm'
|
||||
);
|
||||
|
||||
const { body }: { body: GetCspRuleTemplateResponse } = await supertest
|
||||
.get(`/internal/cloud_security_posture/rules/_find`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.query({
|
||||
benchmarkId: 'cis_k8s',
|
||||
sortField: 'metadata.section',
|
||||
sortOrder: 'asc',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body.items.length).greaterThan(0);
|
||||
|
||||
// check if the items are sorted by metadata.section field
|
||||
const sections = body.items.map((rule: CspRuleTemplate) => rule.metadata.section);
|
||||
const isSorted = sections.every(
|
||||
(section, index) => index === 0 || section >= sections[index - 1]
|
||||
);
|
||||
expect(isSorted).to.be(true);
|
||||
});
|
||||
|
||||
it(`Should return 200 status code and paginate rules with a limit of PerPage`, async () => {
|
||||
const perPage = 10;
|
||||
|
||||
await createPackagePolicy(
|
||||
supertest,
|
||||
agentPolicyId,
|
||||
'kspm',
|
||||
'cloudbeat/cis_k8s',
|
||||
'vanilla',
|
||||
'kspm'
|
||||
);
|
||||
|
||||
const { body }: { body: GetCspRuleTemplateResponse } = await supertest
|
||||
.get(`/internal/cloud_security_posture/rules/_find`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.query({
|
||||
benchmarkId: 'cis_k8s',
|
||||
perPage,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body.items.length).to.be(perPage);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function createPackagePolicy(
|
||||
supertest: SuperTest<Test>,
|
||||
agentPolicyId: string,
|
||||
policyTemplate: string,
|
||||
input: string,
|
||||
deployment: string,
|
||||
posture: string
|
||||
) {
|
||||
const version = posture === 'kspm' || posture === 'cspm' ? '1.2.8' : '1.3.0-preview2';
|
||||
const title = 'Security Posture Management';
|
||||
const streams = [
|
||||
{
|
||||
enabled: false,
|
||||
data_stream: {
|
||||
type: 'logs',
|
||||
dataset: 'cloud_security_posture.vulnerabilities',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const inputTemplate = {
|
||||
enabled: true,
|
||||
type: input,
|
||||
policy_template: policyTemplate,
|
||||
};
|
||||
|
||||
const inputs = posture === 'vuln_mgmt' ? { ...inputTemplate, streams } : { ...inputTemplate };
|
||||
|
||||
const { body: postPackageResponse } = await supertest
|
||||
.post(`/api/fleet/package_policies`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
force: true,
|
||||
name: 'cloud_security_posture-1',
|
||||
description: '',
|
||||
namespace: 'default',
|
||||
policy_id: agentPolicyId,
|
||||
enabled: true,
|
||||
inputs: [inputs],
|
||||
package: {
|
||||
name: 'cloud_security_posture',
|
||||
title,
|
||||
version,
|
||||
},
|
||||
vars: {
|
||||
deployment: {
|
||||
value: deployment,
|
||||
type: 'text',
|
||||
},
|
||||
posture: {
|
||||
value: posture,
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
return postPackageResponse.item;
|
||||
}
|
|
@ -12,6 +12,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
this.tags(['cloud_security_posture']);
|
||||
loadTestFile(require.resolve('./status'));
|
||||
loadTestFile(require.resolve('./benchmark'));
|
||||
loadTestFile(require.resolve('./get_csp_rule_template'));
|
||||
|
||||
// Place your tests files under this directory and add the following here:
|
||||
// loadTestFile(require.resolve('./your test name'));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue