mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution] add doc signing tests (#156916)
This commit is contained in:
parent
a22561a524
commit
6591da49df
6 changed files with 200 additions and 1 deletions
|
@ -167,4 +167,50 @@ describe('Response console', () => {
|
|||
waitForCommandToBeExecuted();
|
||||
});
|
||||
});
|
||||
|
||||
describe('document signing', () => {
|
||||
let response: IndexedFleetEndpointPolicyResponse;
|
||||
let initialAgentData: Agent;
|
||||
|
||||
before(() => {
|
||||
getAgentByHostName(endpointHostname).then((agentData) => {
|
||||
initialAgentData = agentData;
|
||||
});
|
||||
|
||||
getEndpointIntegrationVersion().then((version) =>
|
||||
createAgentPolicyTask(version).then((data) => {
|
||||
response = data;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (initialAgentData?.policy_id) {
|
||||
reassignAgentPolicy(initialAgentData.id, initialAgentData.policy_id);
|
||||
}
|
||||
if (response) {
|
||||
cy.task('deleteIndexedFleetEndpointPolicies', response);
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail if data tampered', () => {
|
||||
waitForEndpointListPageToBeLoaded(endpointHostname);
|
||||
checkEndpointListForOnlyUnIsolatedHosts();
|
||||
openResponseConsoleFromEndpointList();
|
||||
performCommandInputChecks('isolate');
|
||||
|
||||
// stop host so that we ensure tamper happens before endpoint processes the action
|
||||
cy.task('stopEndpointHost');
|
||||
// get action doc before we submit command so we know when the new action doc is indexed
|
||||
cy.task('getLatestActionDoc').then((previousActionDoc) => {
|
||||
submitCommand();
|
||||
cy.task('tamperActionDoc', previousActionDoc);
|
||||
});
|
||||
cy.task('startEndpointHost');
|
||||
|
||||
const actionValidationErrorMsg =
|
||||
'Fleet action response error: Failed to validate action signature; check Endpoint logs for details';
|
||||
cy.contains(actionValidationErrorMsg, { timeout: 120000 }).should('exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -46,8 +46,11 @@ import {
|
|||
indexEndpointRuleAlerts,
|
||||
} from '../../../../common/endpoint/data_loaders/index_endpoint_rule_alerts';
|
||||
import {
|
||||
startEndpointHost,
|
||||
createAndEnrollEndpointHost,
|
||||
destroyEndpointHost,
|
||||
getEndpointHosts,
|
||||
stopEndpointHost,
|
||||
} from '../../../../scripts/endpoint/common/endpoint_host_services';
|
||||
|
||||
/**
|
||||
|
@ -220,5 +223,17 @@ export const dataLoadersForRealEndpoints = (
|
|||
const { kbnClient } = await stackServicesPromise;
|
||||
return destroyEndpointHost(kbnClient, createdHost).then(() => null);
|
||||
},
|
||||
|
||||
stopEndpointHost: async () => {
|
||||
const hosts = await getEndpointHosts();
|
||||
const hostName = hosts[0].name;
|
||||
return stopEndpointHost(hostName);
|
||||
},
|
||||
|
||||
startEndpointHost: async () => {
|
||||
const hosts = await getEndpointHosts();
|
||||
const hostName = hosts[0].name;
|
||||
return startEndpointHost(hostName);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// / <reference types="cypress" />
|
||||
|
||||
import { get } from 'lodash';
|
||||
|
||||
import {
|
||||
getLatestActionDoc,
|
||||
updateActionDoc,
|
||||
waitForNewActionDoc,
|
||||
} from '../../../../scripts/endpoint/common/response_actions';
|
||||
import { createRuntimeServices } from '../../../../scripts/endpoint/common/stack_services';
|
||||
|
||||
export const responseActionTasks = (
|
||||
on: Cypress.PluginEvents,
|
||||
config: Cypress.PluginConfigOptions
|
||||
): void => {
|
||||
const stackServicesPromise = createRuntimeServices({
|
||||
kibanaUrl: config.env.KIBANA_URL,
|
||||
elasticsearchUrl: config.env.ELASTICSEARCH_URL,
|
||||
username: config.env.ELASTICSEARCH_USERNAME,
|
||||
password: config.env.ELASTICSEARCH_PASSWORD,
|
||||
asSuperuser: true,
|
||||
});
|
||||
|
||||
on('task', {
|
||||
getLatestActionDoc: async () => {
|
||||
const { esClient } = await stackServicesPromise;
|
||||
// cypress doesn't like resolved undefined values
|
||||
return getLatestActionDoc(esClient).then((doc) => doc || null);
|
||||
},
|
||||
|
||||
// previousActionDoc is used to determine when a new action doc is received
|
||||
tamperActionDoc: async (previousActionDoc) => {
|
||||
const { esClient } = await stackServicesPromise;
|
||||
const newActionDoc = await waitForNewActionDoc(esClient, previousActionDoc);
|
||||
|
||||
if (!newActionDoc) {
|
||||
throw new Error('no action doc found');
|
||||
}
|
||||
|
||||
const signed = get(newActionDoc, '_source.signed');
|
||||
const signedDataBuffer = Buffer.from(signed.data, 'base64');
|
||||
const signedDataJson = JSON.parse(signedDataBuffer.toString());
|
||||
const tamperedAgentsList = [...signedDataJson.agents, 'anotheragent'];
|
||||
const tamperedData = {
|
||||
...signedDataJson,
|
||||
agents: tamperedAgentsList,
|
||||
};
|
||||
const tamperedDataString = Buffer.from(JSON.stringify(tamperedData), 'utf8').toString(
|
||||
'base64'
|
||||
);
|
||||
const tamperedDoc = {
|
||||
signed: {
|
||||
...signed,
|
||||
data: tamperedDataString,
|
||||
},
|
||||
};
|
||||
return updateActionDoc(esClient, newActionDoc._id, tamperedDoc);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -8,6 +8,8 @@
|
|||
import { defineCypressConfig } from '@kbn/cypress-config';
|
||||
// eslint-disable-next-line @kbn/imports/no_boundary_crossing
|
||||
import { dataLoaders, dataLoadersForRealEndpoints } from './cypress/support/data_loaders';
|
||||
// eslint-disable-next-line @kbn/imports/no_boundary_crossing
|
||||
import { responseActionTasks } from './cypress/support/response_actions';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default defineCypressConfig({
|
||||
|
@ -43,6 +45,7 @@ export default defineCypressConfig({
|
|||
dataLoaders(on, config);
|
||||
// Data loaders specific to "real" Endpoint testing
|
||||
dataLoadersForRealEndpoints(on, config);
|
||||
responseActionTasks(on, config);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -264,3 +264,18 @@ const enrollHostWithFleet = async ({
|
|||
agentId: agent.id,
|
||||
};
|
||||
};
|
||||
|
||||
export async function getEndpointHosts(): Promise<
|
||||
Array<{ name: string; state: string; ipv4: string; image: string }>
|
||||
> {
|
||||
const output = await execa('multipass', ['list', '--format', 'json']);
|
||||
return JSON.parse(output.stdout).list;
|
||||
}
|
||||
|
||||
export function stopEndpointHost(hostName: string) {
|
||||
return execa('multipass', ['stop', hostName]);
|
||||
}
|
||||
|
||||
export function startEndpointHost(hostName: string) {
|
||||
return execa('multipass', ['start', hostName]);
|
||||
}
|
||||
|
|
|
@ -6,13 +6,15 @@
|
|||
*/
|
||||
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import type { SearchHit } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { basename } from 'path';
|
||||
import * as cborx from 'cbor-x';
|
||||
import { AGENT_ACTIONS_RESULTS_INDEX } from '@kbn/fleet-plugin/common';
|
||||
import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '@kbn/fleet-plugin/common';
|
||||
import { FleetActionGenerator } from '../../../common/endpoint/data_generators/fleet_action_generator';
|
||||
import { EndpointActionGenerator } from '../../../common/endpoint/data_generators/endpoint_action_generator';
|
||||
import type {
|
||||
ActionDetails,
|
||||
EndpointAction,
|
||||
EndpointActionData,
|
||||
EndpointActionResponse,
|
||||
FileUploadMetadata,
|
||||
|
@ -309,3 +311,54 @@ const getOutputDataIfNeeded = (action: ActionDetails): ResponseOutput => {
|
|||
return { output: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
export async function getLatestActionDoc(
|
||||
esClient: Client
|
||||
): Promise<SearchHit<EndpointAction> | undefined> {
|
||||
return (
|
||||
await esClient.search<EndpointAction>({
|
||||
index: AGENT_ACTIONS_INDEX,
|
||||
ignore_unavailable: true,
|
||||
query: {
|
||||
match: {
|
||||
type: 'INPUT_ACTION',
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
'@timestamp': {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
size: 1,
|
||||
})
|
||||
).hits.hits.at(0);
|
||||
}
|
||||
|
||||
export async function waitForNewActionDoc(
|
||||
esClient: Client,
|
||||
previousActionDoc?: SearchHit<EndpointAction>,
|
||||
options: {
|
||||
maxAttempts: number;
|
||||
interval: number;
|
||||
} = { maxAttempts: 3, interval: 10000 }
|
||||
): Promise<SearchHit<EndpointAction> | undefined> {
|
||||
const { maxAttempts, interval } = options;
|
||||
let attempts = 1;
|
||||
let latestDoc = await getLatestActionDoc(esClient);
|
||||
while ((!latestDoc || latestDoc._id === previousActionDoc?._id) && attempts <= maxAttempts) {
|
||||
await new Promise((res) => setTimeout(res, interval));
|
||||
latestDoc = await getLatestActionDoc(esClient);
|
||||
attempts++;
|
||||
}
|
||||
|
||||
return latestDoc;
|
||||
}
|
||||
|
||||
export function updateActionDoc<T = unknown>(esClient: Client, id: string, doc: T) {
|
||||
return esClient.update({
|
||||
index: AGENT_ACTIONS_INDEX,
|
||||
id,
|
||||
doc,
|
||||
refresh: true,
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue