[Cloud Security] find csp rule template api

This commit is contained in:
Ido Cohen 2023-05-31 19:09:31 +03:00 committed by GitHub
parent 9bb0b0dd01
commit 172e84bdc9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 618 additions and 119 deletions

View file

@ -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": {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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