[Cloud Posture] test latest findings table sort (#144668)

This commit is contained in:
Or Ouziel 2022-11-28 11:46:37 +02:00 committed by GitHub
parent 1c22c801a4
commit b9ffc29cd3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 329 additions and 3 deletions

View file

@ -133,6 +133,7 @@ enabled:
- x-pack/test/cases_api_integration/security_and_spaces/config_trial.ts
- x-pack/test/cases_api_integration/security_and_spaces/config_no_public_base_url.ts
- x-pack/test/cases_api_integration/spaces_only/config.ts
- x-pack/test/cloud_security_posture_functional/config.ts
- x-pack/test/detection_engine_api_integration/basic/config.ts
- x-pack/test/detection_engine_api_integration/security_and_spaces/group1/config.ts
- x-pack/test/detection_engine_api_integration/security_and_spaces/group2/config.ts

2
.github/CODEOWNERS vendored
View file

@ -662,6 +662,8 @@ x-pack/test/threat_intelligence_cypress @elastic/protections-experience
/x-pack/plugins/cloud_security_posture/ @elastic/kibana-cloud-security-posture
/x-pack/plugins/security_solution/public/cloud_security_posture @elastic/kibana-cloud-security-posture
/x-pack/test/api_integration/apis/cloud_security_posture/ @elastic/kibana-cloud-security-posture
/x-pack/test/cloud_security_posture_functional/ @elastic/kibana-cloud-security-posture
# Security Solution onboarding tour
/x-pack/plugins/security_solution/public/common/components/guided_onboarding @elastic/security-threat-hunting-explore

View file

@ -62,6 +62,7 @@ interface BaseCspSetupStatus {
latestPackageVersion: string;
installedPackagePolicies: number;
healthyAgents: number;
isPluginInitialized: boolean;
}
interface CspSetupNotInstalledStatus extends BaseCspSetupStatus {

View file

@ -239,7 +239,9 @@ const FilterableCell: React.FC<{
}
`}
>
<div className="__filter_value eui-textTruncate">{children}</div>
<div className="__filter_value eui-textTruncate" data-test-subj="filter_cell_value">
{children}
</div>
<div
className="__filter_buttons"
css={css`

View file

@ -31,6 +31,7 @@ export const createCspRequestHandlerContextMock = () => {
agentService: createMockAgentService(),
packagePolicyService: createPackagePolicyServiceMock(),
packageService: createMockPackageService(),
isPluginInitialized: () => false,
},
};
};

View file

@ -62,6 +62,13 @@ export class CspPlugin
private readonly logger: Logger;
private isCloudEnabled?: boolean;
/**
* CSP is initialized when the Fleet package is installed.
* either directly after installation, or
* when the plugin is started and a package is present.
*/
#isInitialized: boolean = false;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
}
@ -75,6 +82,7 @@ export class CspPlugin
setupRoutes({
core,
logger: this.logger,
isPluginInitialized: () => this.#isInitialized,
});
const coreStartServices = core.getStartServices();
@ -162,12 +170,16 @@ export class CspPlugin
public stop() {}
/**
* Initialization is idempotent and required for (re)creating indices and transforms.
*/
async initialize(core: CoreStart, taskManager: TaskManagerStartContract): Promise<void> {
this.logger.debug('initialize');
const esClient = core.elasticsearch.client.asInternalUser;
await initializeCspIndices(esClient, this.logger);
await initializeCspTransforms(esClient, this.logger);
await scheduleFindingsStatsTask(taskManager, this.logger);
this.#isInitialized = true;
}
async uninstallResources(taskManager: TaskManagerStartContract, logger: Logger): Promise<void> {

View file

@ -24,9 +24,11 @@ import { defineGetCspStatusRoute } from './status/status';
export function setupRoutes({
core,
logger,
isPluginInitialized,
}: {
core: CoreSetup<CspServerPluginStartDeps, CspServerPluginStart>;
logger: Logger;
isPluginInitialized(): boolean;
}) {
const router = core.http.createRouter<CspRequestHandlerContext>();
defineGetComplianceDashboardRoute(router);
@ -57,6 +59,7 @@ export function setupRoutes({
agentService: fleet.agentService,
packagePolicyService: fleet.packagePolicyService,
packageService: fleet.packageService,
isPluginInitialized,
};
}
);

View file

@ -119,6 +119,7 @@ describe('CspSetupStatus route', () => {
installedPackagePolicies: 0,
healthyAgents: 0,
installedPackageVersion: undefined,
isPluginInitialized: false,
});
});
@ -159,6 +160,7 @@ describe('CspSetupStatus route', () => {
installedPackagePolicies: 3,
healthyAgents: 0,
installedPackageVersion: '0.0.14',
isPluginInitialized: false,
});
});
@ -208,6 +210,7 @@ describe('CspSetupStatus route', () => {
installedPackagePolicies: 3,
healthyAgents: 1,
installedPackageVersion: '0.0.14',
isPluginInitialized: false,
});
});
@ -245,6 +248,7 @@ describe('CspSetupStatus route', () => {
latestPackageVersion: '0.0.14',
installedPackagePolicies: 0,
healthyAgents: 0,
isPluginInitialized: false,
});
});
@ -295,6 +299,7 @@ describe('CspSetupStatus route', () => {
installedPackagePolicies: 1,
healthyAgents: 0,
installedPackageVersion: '0.0.14',
isPluginInitialized: false,
});
});
@ -352,6 +357,7 @@ describe('CspSetupStatus route', () => {
installedPackagePolicies: 1,
healthyAgents: 1,
installedPackageVersion: '0.0.14',
isPluginInitialized: false,
});
});
@ -408,6 +414,7 @@ describe('CspSetupStatus route', () => {
installedPackagePolicies: 1,
healthyAgents: 1,
installedPackageVersion: '0.0.14',
isPluginInitialized: false,
});
});
});

View file

@ -10,6 +10,7 @@ import type { SavedObjectsClientContract } from '@kbn/core/server';
import type { AgentPolicyServiceInterface, AgentService } from '@kbn/fleet-plugin/server';
import moment from 'moment';
import { PackagePolicy } from '@kbn/fleet-plugin/common';
import { schema } from '@kbn/config-schema';
import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME, STATUS_ROUTE_PATH } from '../../../common/constants';
import type { CspApiRequestHandlerContext, CspRouter } from '../../types';
import type { CspSetupStatus, CspStatusCode } from '../../../common/types';
@ -73,6 +74,7 @@ const getCspStatus = async ({
packagePolicyService,
agentPolicyService,
agentService,
isPluginInitialized,
}: CspApiRequestHandlerContext): Promise<CspSetupStatus> => {
const [hasFindings, installation, latestCspPackage, installedPackagePolicies] = await Promise.all(
[
@ -109,6 +111,7 @@ const getCspStatus = async ({
latestPackageVersion: latestCspPackageVersion,
healthyAgents,
installedPackagePolicies: installedPackagePoliciesTotal,
isPluginInitialized: isPluginInitialized(),
};
return {
@ -117,21 +120,37 @@ const getCspStatus = async ({
healthyAgents,
installedPackagePolicies: installedPackagePoliciesTotal,
installedPackageVersion: installation?.install_version,
isPluginInitialized: isPluginInitialized(),
};
};
export const statusQueryParamsSchema = schema.object({
/**
* CSP Plugin initialization includes creating indices/transforms/tasks.
* Prior to this initialization, the plugin is not ready to index findings.
*/
check: schema.oneOf([schema.literal('all'), schema.literal('init')], { defaultValue: 'all' }),
});
export const defineGetCspStatusRoute = (router: CspRouter): void =>
router.get(
{
path: STATUS_ROUTE_PATH,
validate: false,
validate: { query: statusQueryParamsSchema },
options: {
tags: ['access:cloud-security-posture-read'],
},
},
async (context, _, response) => {
async (context, request, response) => {
const cspContext = await context.csp;
try {
if (request.query.check === 'init') {
return response.ok({
body: {
isPluginInitialized: cspContext.isPluginInitialized(),
},
});
}
const status = await getCspStatus(cspContext);
return response.ok({
body: status,

View file

@ -72,6 +72,7 @@ export interface CspApiRequestHandlerContext {
agentService: AgentService;
packagePolicyService: PackagePolicyClient;
packageService: PackageService;
isPluginInitialized(): boolean;
}
export type CspRequestHandlerContext = CustomRequestHandlerContext<{

View file

@ -0,0 +1,34 @@
/*
* 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 { resolve } from 'path';
import type { FtrConfigProviderContext } from '@kbn/test';
import { pageObjects } from './page_objects';
import { getPreConfiguredFleetPackages, getPreConfiguredAgentPolicies } from './helpers';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const xpackFunctionalConfig = await readConfigFile(
require.resolve('../functional/config.base.js')
);
return {
...xpackFunctionalConfig.getAll(),
pageObjects,
testFiles: [resolve(__dirname, './pages')],
junit: {
reportName: 'X-Pack Cloud Security Posture Functional Tests',
},
kbnTestServer: {
...xpackFunctionalConfig.get('kbnTestServer'),
serverArgs: [
...xpackFunctionalConfig.get('kbnTestServer.serverArgs'),
...getPreConfiguredFleetPackages(),
...getPreConfiguredAgentPolicies(),
],
},
};
}

View file

@ -0,0 +1,13 @@
/*
* 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 { GenericFtrProviderContext } from '@kbn/test';
import { pageObjects } from './page_objects';
import { services } from '../functional/services';
export type FtrProviderContext = GenericFtrProviderContext<typeof services, typeof pageObjects>;

View file

@ -0,0 +1,27 @@
/*
* 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.
*/
const CLOUD_SECURITY_POSTURE_PACKAGE_NAME = 'cloud_security_posture';
/**
* flags to load kibana with fleet pre-configured to have 'cloud_security_posture' integration installed
*/
export const getPreConfiguredFleetPackages = () => [
`--xpack.fleet.packages.0.name=${CLOUD_SECURITY_POSTURE_PACKAGE_NAME}`,
`--xpack.fleet.packages.0.version=latest`,
];
/**
* flags to load kibana with pre-configured agent policy with a 'cloud_security_posture' package policy
*/
export const getPreConfiguredAgentPolicies = () => [
`--xpack.fleet.agentPolicies.0.id=agent-policy-csp`,
`--xpack.fleet.agentPolicies.0.name=example-agent-policy-csp`,
`--xpack.fleet.agentPolicies.0.package_policies.0.id=integration-policy-csp`,
`--xpack.fleet.agentPolicies.0.package_policies.0.name=example-integration-csp`,
`--xpack.fleet.agentPolicies.0.package_policies.0.package.name=${CLOUD_SECURITY_POSTURE_PACKAGE_NAME}`,
];

View file

@ -0,0 +1,127 @@
/*
* 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 { FtrProviderContext } from '../ftr_provider_context';
// Defined in CSP plugin
const STATUS_API_PATH = '/internal/cloud_security_posture/status?check=init';
const FINDINGS_INDEX = 'logs-cloud_security_posture.findings_latest-default';
const FINDINGS_ROUTE = 'cloud_security_posture/findings';
const FINDINGS_TABLE_TESTID = 'findings_table';
const getFilterValueSelector = (columnIndex: number) =>
`tbody tr td:nth-child(${columnIndex + 1}) div[data-test-subj="filter_cell_value"]`;
// Defined in Security Solution plugin
const SECURITY_SOLUTION_APP_NAME = 'securitySolution';
export function FindingsPageProvider({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common']);
const retry = getService('retry');
const es = getService('es');
const supertest = getService('supertest');
const log = getService('log');
/**
* required before indexing findings
*/
const waitForPluginInitialized = (): Promise<void> =>
retry.try(async () => {
log.debug('Check CSP plugin is initialized');
const response = await supertest.get(STATUS_API_PATH).expect(200);
expect(response.body).to.eql({ isPluginInitialized: true });
log.debug('CSP plugin is initialized');
});
const index = {
remove: () => es.indices.delete({ index: FINDINGS_INDEX, ignore_unavailable: true }),
add: async <T>(findingsMock: T[]) => {
await waitForPluginInitialized();
await Promise.all(
findingsMock.map((finding) =>
es.index({
index: FINDINGS_INDEX,
body: finding,
})
)
);
},
};
const table = {
getTableElement: () => testSubjects.find(FINDINGS_TABLE_TESTID),
getColumnIndex: async (columnName: string) => {
const tableElement = await table.getTableElement();
const headers = await tableElement.findAllByCssSelector('thead tr :is(th,td)');
const headerIndexes = await Promise.all(headers.map((header) => header.getVisibleText()));
const columnIndex = headerIndexes.findIndex((i) => i === columnName);
expect(columnIndex).to.be.greaterThan(-1);
return [columnIndex, headers[columnIndex]] as [
number,
Awaited<ReturnType<typeof testSubjects.find>>
];
},
getFilterColumnValues: async (columnName: string) => {
const tableElement = await table.getTableElement();
const [columnIndex] = await table.getColumnIndex(columnName);
const columnCells = await tableElement.findAllByCssSelector(
getFilterValueSelector(columnIndex)
);
return await Promise.all(columnCells.map((h) => h.getVisibleText()));
},
assertColumnSort: async (columnName: string, direction: 'asc' | 'desc') => {
const values = (await table.getFilterColumnValues(columnName)).filter(Boolean);
expect(values).to.not.be.empty();
const sorted = values
.slice()
.sort((a, b) => (direction === 'asc' ? a.localeCompare(b) : b.localeCompare(a)));
values.every((value, i) => expect(value).to.be(sorted[i]));
},
toggleColumnSortOrFail: async (columnName: string, direction: 'asc' | 'desc') => {
const getColumnElement = async () => (await table.getColumnIndex(columnName))[1];
const element = await getColumnElement();
const currentSort = await element.getAttribute('aria-sort');
if (currentSort === 'none') {
// a click is needed to focus on Eui column header
await element.click();
// default is ascending
if (direction === 'desc') {
const nonStaleElement = await getColumnElement();
await nonStaleElement.click();
}
}
if (
(currentSort === 'ascending' && direction === 'desc') ||
(currentSort === 'descending' && direction === 'asc')
) {
// Without getting the element again, the click throws an error (stale element reference)
const nonStaleElement = await getColumnElement();
await nonStaleElement.click();
}
await table.assertColumnSort(columnName, direction);
},
};
const navigateToFindingsPage = async () => {
await PageObjects.common.navigateToUrl(SECURITY_SOLUTION_APP_NAME, FINDINGS_ROUTE, {
shouldUseHashForSubUrl: false,
});
};
return {
navigateToFindingsPage,
table,
index,
};
}

View file

@ -0,0 +1,14 @@
/*
* 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 { pageObjects as xpackFunctionalPageObjects } from '../../functional/page_objects';
import { FindingsPageProvider } from './findings_page';
export const pageObjects = {
...xpackFunctionalPageObjects,
findings: FindingsPageProvider,
};

View file

@ -0,0 +1,47 @@
/*
* 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 type { FtrProviderContext } from '../ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function ({ getPageObjects, getService }: FtrProviderContext) {
const pageObjects = getPageObjects(['common', 'findings']);
const FINDINGS_SIZE = 2;
const findingsMock = Array.from({ length: FINDINGS_SIZE }, (_, id) => {
return {
resource: { id, name: `Resource ${id}` },
result: { evaluation: 'passed' },
rule: {
name: `Rule ${id}`,
section: 'Kubelet',
tags: ['Kubernetes'],
type: 'process',
},
};
});
describe('Findings Page', () => {
before(async () => {
await pageObjects.findings.index.add(findingsMock);
await pageObjects.findings.navigateToFindingsPage();
});
after(async () => {
await pageObjects.findings.index.remove();
});
describe('Sort', () => {
it('Sorts by rule name', async () => {
await pageObjects.findings.table.toggleColumnSortOrFail('Rule', 'asc');
});
it('Sorts by resource name', async () => {
await pageObjects.findings.table.toggleColumnSortOrFail('Resource Name', 'desc');
});
});
});
}

View file

@ -0,0 +1,15 @@
/*
* 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 { FtrProviderContext } from '../ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function ({ loadTestFile }: FtrProviderContext) {
describe('Cloud Security Posture', function () {
loadTestFile(require.resolve('./findings'));
});
}