mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Fleet] Implement audit logging for core CRUD operations (#152118)
## Summary Adds custom [audit logging ](https://www.elastic.co/guide/en/kibana/current/xpack-security-audit-logging.html) for the majority of Fleet's persistence operations. ### Operations with custom audit logging - Agent Policy CRUD - Package Policy CRUD - EPM (packages) CRUD - Fleet Settings CRUD - Outputs CRUD - `.fleet-actions` reads/writes - `.fleet-policies` reads/writes - `.fleet-enrollment-api-keys` read/writes - `.fleet-agents` reads/writes ## Background Fleet uses an "internal" saved objects client and commonly performs persistence operations as `kibana_system` while using its own security/role implementation around Fleet/Integrations permissions. This means we can't simply enable Kibana's default audit logger and receive meaningful logs for Fleet usage. Instead, we need to implement our own custom audit logging around these operations. ## Implementation This PR adds audit logging helpers to our `appContextService` class, meaning they can be used anywhere throughout the Fleet codebase. In HTTP request contexts, the audit logger on the `AppContext` object will include request-level values. In non-HTTP contexts (e.g. Fleet setup running during Kibana boot), the logs will be "un-scoped" and won't include request info. For saved objects specifically, there are a few expectations around how audit logs are formatted. To capture this, I've added a `writeCustomSoAuditLog` helper that includes the expected field structure and message format for consistency across SO operations. ## Testing Add the following to your `kibana.dev.yml` ```yaml xpack.security.audit.enabled: true ``` Run Kibana and perform various Fleet operations. Note the `logs/audit.log` file that's created in your local Kibana directory. As you perform actions, grep the `audit.log` for your policy ID's, request paths, etc and note the data. You can also upload your `audit.log` file via Kibana's file upload and explore the data that way.  --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
b80f4fd74c
commit
ab30f3f123
41 changed files with 2531 additions and 683 deletions
|
@ -35,7 +35,11 @@ import type {
|
|||
EncryptedSavedObjectsPluginStart,
|
||||
EncryptedSavedObjectsPluginSetup,
|
||||
} from '@kbn/encrypted-saved-objects-plugin/server';
|
||||
import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server';
|
||||
import type {
|
||||
AuditLogger,
|
||||
SecurityPluginSetup,
|
||||
SecurityPluginStart,
|
||||
} from '@kbn/security-plugin/server';
|
||||
import type { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server';
|
||||
import type {
|
||||
TaskManagerSetupContract,
|
||||
|
@ -111,6 +115,7 @@ import type { PackagePolicyService } from './services/package_policy_service';
|
|||
import { PackagePolicyServiceImpl } from './services/package_policy';
|
||||
import { registerFleetUsageLogger, startFleetUsageLogger } from './services/fleet_usage_logger';
|
||||
import { CheckDeletedFilesTask } from './tasks/check_deleted_files_task';
|
||||
import { getRequestStore } from './services/request_store';
|
||||
|
||||
export interface FleetSetupDeps {
|
||||
security: SecurityPluginSetup;
|
||||
|
@ -154,6 +159,7 @@ export interface FleetAppContext {
|
|||
telemetryEventsSender: TelemetryEventsSender;
|
||||
bulkActionsResolver: BulkActionsResolver;
|
||||
messageSigningService: MessageSigningServiceInterface;
|
||||
auditLogger?: AuditLogger;
|
||||
}
|
||||
|
||||
export type FleetSetupContract = void;
|
||||
|
@ -362,39 +368,42 @@ export class FleetPlugin
|
|||
.getSavedObjects()
|
||||
.getScopedClient(request, { excludedExtensions: [SECURITY_EXTENSION_ID] });
|
||||
|
||||
return {
|
||||
get agentClient() {
|
||||
const agentService = plugin.setupAgentService(esClient.asInternalUser, soClient);
|
||||
const requestStore = getRequestStore();
|
||||
|
||||
return {
|
||||
asCurrentUser: agentService.asScoped(request),
|
||||
asInternalUser: agentService.asInternalUser,
|
||||
};
|
||||
},
|
||||
get packagePolicyService() {
|
||||
const service = plugin.setupPackagePolicyService();
|
||||
return requestStore.run(request, () => {
|
||||
return {
|
||||
get agentClient() {
|
||||
const agentService = plugin.setupAgentService(esClient.asInternalUser, soClient);
|
||||
|
||||
return {
|
||||
asCurrentUser: service.asScoped(request),
|
||||
asInternalUser: service.asInternalUser,
|
||||
};
|
||||
},
|
||||
authz,
|
||||
return {
|
||||
asCurrentUser: agentService.asScoped(request),
|
||||
asInternalUser: agentService.asInternalUser,
|
||||
};
|
||||
},
|
||||
get packagePolicyService() {
|
||||
const service = plugin.setupPackagePolicyService();
|
||||
|
||||
get internalSoClient() {
|
||||
// Use a lazy getter to avoid constructing this client when not used by a request handler
|
||||
return getInternalSoClient();
|
||||
},
|
||||
get spaceId() {
|
||||
return deps.spaces?.spacesService?.getSpaceId(request) ?? DEFAULT_SPACE_ID;
|
||||
},
|
||||
return {
|
||||
asCurrentUser: service.asScoped(request),
|
||||
asInternalUser: service.asInternalUser,
|
||||
};
|
||||
},
|
||||
authz,
|
||||
get internalSoClient() {
|
||||
// Use a lazy getter to avoid constructing this client when not used by a request handler
|
||||
return getInternalSoClient();
|
||||
},
|
||||
get spaceId() {
|
||||
return deps.spaces?.spacesService?.getSpaceId(request) ?? DEFAULT_SPACE_ID;
|
||||
},
|
||||
|
||||
get limitedToPackages() {
|
||||
if (routeAuthz && routeAuthz.granted) {
|
||||
return routeAuthz.scopeDataToPackages;
|
||||
}
|
||||
},
|
||||
};
|
||||
get limitedToPackages() {
|
||||
if (routeAuthz && routeAuthz.granted) {
|
||||
return routeAuthz.scopeDataToPackages;
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -267,11 +267,15 @@ export const deleteAgentPoliciesHandler: RequestHandler<
|
|||
const coreContext = await context.core;
|
||||
const soClient = coreContext.savedObjects.client;
|
||||
const esClient = coreContext.elasticsearch.client.asInternalUser;
|
||||
const user = await appContextService.getSecurity()?.authc.getCurrentUser(request);
|
||||
try {
|
||||
const body: DeleteAgentPolicyResponse = await agentPolicyService.delete(
|
||||
soClient,
|
||||
esClient,
|
||||
request.body.agentPolicyId
|
||||
request.body.agentPolicyId,
|
||||
{
|
||||
user: user || undefined,
|
||||
}
|
||||
);
|
||||
return response.ok({
|
||||
body,
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import { securityMock } from '@kbn/security-plugin/server/mocks';
|
||||
|
||||
import { appContextService } from '..';
|
||||
import { outputService } from '../output';
|
||||
|
||||
|
@ -16,6 +18,10 @@ jest.mock('../app_context');
|
|||
jest.mock('../output');
|
||||
|
||||
const mockedAppContextService = appContextService as jest.Mocked<typeof appContextService>;
|
||||
mockedAppContextService.getSecuritySetup.mockImplementation(() => ({
|
||||
...securityMock.createSetup(),
|
||||
}));
|
||||
|
||||
const mockedOutputService = outputService as jest.Mocked<typeof outputService>;
|
||||
|
||||
function mockHasLicence(res: boolean) {
|
||||
|
|
|
@ -6,18 +6,16 @@
|
|||
*/
|
||||
|
||||
import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { securityMock } from '@kbn/security-plugin/server/mocks';
|
||||
|
||||
import { PackagePolicyRestrictionRelatedError } from '../errors';
|
||||
|
||||
import type {
|
||||
AgentPolicy,
|
||||
FullAgentPolicy,
|
||||
NewAgentPolicy,
|
||||
PreconfiguredAgentPolicy,
|
||||
} from '../types';
|
||||
|
||||
import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../constants';
|
||||
|
||||
import { AGENT_POLICY_INDEX } from '../../common';
|
||||
|
@ -31,6 +29,8 @@ import { appContextService } from './app_context';
|
|||
import { outputService } from './output';
|
||||
import { downloadSourceService } from './download_source';
|
||||
import { getFullAgentPolicy } from './agent_policies';
|
||||
import * as outputsHelpers from './agent_policies/outputs_helpers';
|
||||
import { auditLoggingService } from './audit_logging';
|
||||
|
||||
function getSavedObjectMock(agentPolicyAttributes: any) {
|
||||
const mock = savedObjectsClientMock.create();
|
||||
|
@ -70,9 +70,17 @@ jest.mock('./agent_policy_update');
|
|||
jest.mock('./agents');
|
||||
jest.mock('./package_policy');
|
||||
jest.mock('./app_context');
|
||||
jest.mock('./audit_logging');
|
||||
jest.mock('./agent_policies/full_agent_policy');
|
||||
jest.mock('./agent_policies/outputs_helpers');
|
||||
|
||||
const mockedAppContextService = appContextService as jest.Mocked<typeof appContextService>;
|
||||
mockedAppContextService.getSecuritySetup.mockImplementation(() => ({
|
||||
...securityMock.createSetup(),
|
||||
}));
|
||||
|
||||
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
|
||||
const mockOutputsHelpers = outputsHelpers as jest.Mocked<typeof outputsHelpers>;
|
||||
const mockedOutputService = outputService as jest.Mocked<typeof outputService>;
|
||||
const mockedDownloadSourceService = downloadSourceService as jest.Mocked<
|
||||
typeof downloadSourceService
|
||||
|
@ -83,12 +91,6 @@ const mockedGetFullAgentPolicy = getFullAgentPolicy as jest.Mock<
|
|||
ReturnType<typeof getFullAgentPolicy>
|
||||
>;
|
||||
|
||||
function getAgentPolicyUpdateMock() {
|
||||
return agentPolicyUpdateEventHandler as unknown as jest.Mock<
|
||||
typeof agentPolicyUpdateEventHandler
|
||||
>;
|
||||
}
|
||||
|
||||
function getAgentPolicyCreateMock() {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
soClient.create.mockImplementation(async (type, attributes) => {
|
||||
|
@ -103,8 +105,8 @@ function getAgentPolicyCreateMock() {
|
|||
}
|
||||
|
||||
describe('agent policy', () => {
|
||||
beforeEach(() => {
|
||||
getAgentPolicyUpdateMock().mockClear();
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
|
@ -141,6 +143,148 @@ describe('agent policy', () => {
|
|||
const [, attributes] = soClient.create.mock.calls[0];
|
||||
expect(attributes).toHaveProperty('is_managed', true);
|
||||
});
|
||||
|
||||
it('should call audit logger', async () => {
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
|
||||
soClient.find.mockResolvedValueOnce({
|
||||
total: 0,
|
||||
saved_objects: [],
|
||||
per_page: 0,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
soClient.create.mockResolvedValueOnce({
|
||||
id: 'test-agent-policy',
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
attributes: {},
|
||||
references: [],
|
||||
});
|
||||
|
||||
mockOutputsHelpers.validateOutputForPolicy.mockResolvedValueOnce(undefined);
|
||||
|
||||
await agentPolicyService.create(
|
||||
soClient,
|
||||
esClient,
|
||||
{
|
||||
name: 'test',
|
||||
namespace: 'default',
|
||||
},
|
||||
{ id: 'test-agent-policy' }
|
||||
);
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'create',
|
||||
id: 'test-agent-policy',
|
||||
savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Add more test coverage to `get` service method
|
||||
describe('get', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
|
||||
soClient.get.mockResolvedValueOnce({
|
||||
id: 'test-agent-policy',
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
await agentPolicyService.get(soClient, 'test-agent-policy', false);
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toBeCalledWith({
|
||||
action: 'get',
|
||||
id: 'test-agent-policy',
|
||||
savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getByIDs', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
|
||||
soClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
id: 'test-agent-policy-1',
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
},
|
||||
{
|
||||
id: 'test-agent-policy-2',
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await agentPolicyService.getByIDs(soClient, ['test-agent-policy-1', 'test-agent-policy-2']);
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenNthCalledWith(1, {
|
||||
action: 'get',
|
||||
id: 'test-agent-policy-1',
|
||||
savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenNthCalledWith(2, {
|
||||
action: 'get',
|
||||
id: 'test-agent-policy-2',
|
||||
savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
|
||||
soClient.find.mockResolvedValueOnce({
|
||||
total: 2,
|
||||
saved_objects: [
|
||||
{
|
||||
id: 'test-agent-policy-1',
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
score: 0,
|
||||
},
|
||||
{
|
||||
id: 'test-agent-policy-2',
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
score: 0,
|
||||
},
|
||||
],
|
||||
per_page: 0,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
await agentPolicyService.list(soClient, {
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
kuery: '',
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenNthCalledWith(1, {
|
||||
action: 'find',
|
||||
id: 'test-agent-policy-1',
|
||||
savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenNthCalledWith(2, {
|
||||
action: 'find',
|
||||
id: 'test-agent-policy-2',
|
||||
savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
|
@ -187,6 +331,16 @@ describe('agent policy', () => {
|
|||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should call audit logger', async () => {
|
||||
await agentPolicyService.delete(soClient, esClient, 'mocked');
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'delete',
|
||||
id: 'mocked',
|
||||
savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('bumpRevision', () => {
|
||||
|
@ -406,6 +560,30 @@ describe('agent policy', () => {
|
|||
calledWith = soClient.update.mock.calls[1];
|
||||
expect(calledWith[2]).toHaveProperty('is_managed', true);
|
||||
});
|
||||
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
|
||||
soClient.get.mockResolvedValue({
|
||||
attributes: {},
|
||||
references: [],
|
||||
id: 'test-agent-policy',
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
await agentPolicyService.update(soClient, esClient, 'test-agent-policy', {
|
||||
name: 'Test Agent Policy',
|
||||
namespace: 'default',
|
||||
is_managed: false,
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'update',
|
||||
id: 'test-agent-policy',
|
||||
savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deployPolicy', () => {
|
||||
|
@ -528,47 +706,72 @@ describe('agent policy', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('ensurePreconfiguredAgentPolicy', () => {
|
||||
it('should use preconfigured id if provided for policy', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
it('should call audit logger', async () => {
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
|
||||
const preconfiguredAgentPolicy: PreconfiguredAgentPolicy = {
|
||||
id: 'my-unique-id',
|
||||
name: 'My Preconfigured Policy',
|
||||
package_policies: [
|
||||
{
|
||||
name: 'my-package-policy',
|
||||
id: 'my-package-policy-id',
|
||||
package: {
|
||||
name: 'test-package',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
mockedAppContextService.getInternalUserESClient.mockReturnValue(esClient);
|
||||
mockedOutputService.getDefaultDataOutputId.mockResolvedValueOnce('default-output');
|
||||
|
||||
soClient.find.mockResolvedValueOnce({ total: 0, saved_objects: [], page: 1, per_page: 10 });
|
||||
soClient.get.mockRejectedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError());
|
||||
|
||||
soClient.create.mockResolvedValueOnce({
|
||||
id: 'my-unique-id',
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
attributes: {},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await agentPolicyService.ensurePreconfiguredAgentPolicy(
|
||||
soClient,
|
||||
esClient,
|
||||
preconfiguredAgentPolicy
|
||||
);
|
||||
|
||||
expect(soClient.create).toHaveBeenCalledWith(
|
||||
AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
expect.anything(),
|
||||
expect.objectContaining({ id: 'my-unique-id' })
|
||||
);
|
||||
soClient.bulkGet.mockResolvedValue({
|
||||
saved_objects: [
|
||||
{
|
||||
attributes: {},
|
||||
references: [],
|
||||
id: 'test-agent-policy',
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await agentPolicyService.deployPolicy(soClient, 'test-agent-policy');
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomAuditLog).toHaveBeenCalledWith({
|
||||
message: `User deploying policy [id=test-agent-policy]`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensurePreconfiguredAgentPolicy', () => {
|
||||
it('should use preconfigured id if provided for policy', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
|
||||
const preconfiguredAgentPolicy: PreconfiguredAgentPolicy = {
|
||||
id: 'my-unique-id',
|
||||
name: 'My Preconfigured Policy',
|
||||
package_policies: [
|
||||
{
|
||||
name: 'my-package-policy',
|
||||
id: 'my-package-policy-id',
|
||||
package: {
|
||||
name: 'test-package',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
soClient.find.mockResolvedValueOnce({ total: 0, saved_objects: [], page: 1, per_page: 10 });
|
||||
soClient.get.mockRejectedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError());
|
||||
|
||||
soClient.create.mockResolvedValueOnce({
|
||||
id: 'my-unique-id',
|
||||
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
attributes: {},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await agentPolicyService.ensurePreconfiguredAgentPolicy(
|
||||
soClient,
|
||||
esClient,
|
||||
preconfiguredAgentPolicy
|
||||
);
|
||||
|
||||
expect(soClient.create).toHaveBeenCalledWith(
|
||||
AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
expect.anything(),
|
||||
expect.objectContaining({ id: 'my-unique-id' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -626,4 +829,18 @@ describe('agent policy', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteFleetServerPoliciesForPolicyId', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
|
||||
esClient.deleteByQuery.mockResolvedValueOnce({} as any);
|
||||
|
||||
await agentPolicyService.deleteFleetServerPoliciesForPolicyId(esClient, 'test-agent-policy');
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomAuditLog).toHaveBeenCalledWith({
|
||||
message: 'User deleting policy [id=test-agent-policy]',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import type {
|
|||
SavedObjectsClientContract,
|
||||
SavedObjectsBulkUpdateResponse,
|
||||
} from '@kbn/core/server';
|
||||
import { SavedObjectsUtils } from '@kbn/core/server';
|
||||
|
||||
import type { AuthenticatedUser } from '@kbn/security-plugin/server';
|
||||
import type { BulkResponseItem } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
@ -83,6 +84,7 @@ import { normalizeKuery, escapeSearchQueryPhrase } from './saved_object';
|
|||
import { appContextService } from './app_context';
|
||||
import { getFullAgentPolicy } from './agent_policies';
|
||||
import { validateOutputForPolicy } from './agent_policies';
|
||||
import { auditLoggingService } from './audit_logging';
|
||||
|
||||
const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE;
|
||||
|
||||
|
@ -106,6 +108,12 @@ class AgentPolicyService {
|
|||
user?: AuthenticatedUser,
|
||||
options: { bumpRevision: boolean } = { bumpRevision: true }
|
||||
): Promise<AgentPolicy> {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'update',
|
||||
id,
|
||||
savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
const existingAgentPolicy = await this.get(soClient, id, true);
|
||||
|
||||
if (!existingAgentPolicy) {
|
||||
|
@ -201,8 +209,19 @@ class AgentPolicyService {
|
|||
soClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient,
|
||||
agentPolicy: NewAgentPolicy,
|
||||
options?: { id?: string; user?: AuthenticatedUser }
|
||||
options: { id?: string; user?: AuthenticatedUser } = {}
|
||||
): Promise<AgentPolicy> {
|
||||
// Ensure an ID is provided, so we can include it in the audit logs below
|
||||
if (!options.id) {
|
||||
options.id = SavedObjectsUtils.generateId();
|
||||
}
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'create',
|
||||
id: options.id,
|
||||
savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
await this.requireUniqueName(soClient, agentPolicy);
|
||||
|
||||
await validateOutputForPolicy(soClient, agentPolicy);
|
||||
|
@ -270,6 +289,12 @@ class AgentPolicyService {
|
|||
(await packagePolicyService.findAllForAgentPolicy(soClient, id)) || [];
|
||||
}
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'get',
|
||||
id,
|
||||
savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
return agentPolicy;
|
||||
}
|
||||
|
||||
|
@ -313,7 +338,19 @@ class AgentPolicyService {
|
|||
{ concurrency: 50 }
|
||||
);
|
||||
|
||||
return agentPolicies.filter((agentPolicy): agentPolicy is AgentPolicy => agentPolicy !== null);
|
||||
const result = agentPolicies.filter(
|
||||
(agentPolicy): agentPolicy is AgentPolicy => agentPolicy !== null
|
||||
);
|
||||
|
||||
for (const agentPolicy of result) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'get',
|
||||
id: agentPolicy.id,
|
||||
savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async list(
|
||||
|
@ -386,6 +423,14 @@ class AgentPolicyService {
|
|||
{ concurrency: 50 }
|
||||
);
|
||||
|
||||
for (const agentPolicy of agentPolicies) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'find',
|
||||
id: agentPolicy.id,
|
||||
savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
items: agentPolicies,
|
||||
total: agentPoliciesSO.total,
|
||||
|
@ -659,8 +704,14 @@ class AgentPolicyService {
|
|||
soClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient,
|
||||
id: string,
|
||||
options?: { force?: boolean; removeFleetServerDocuments?: boolean }
|
||||
options?: { force?: boolean; removeFleetServerDocuments?: boolean; user?: AuthenticatedUser }
|
||||
): Promise<DeleteAgentPolicyResponse> {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'delete',
|
||||
id,
|
||||
savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
const agentPolicy = await this.get(soClient, id, false);
|
||||
if (!agentPolicy) {
|
||||
throw new Error('Agent policy not found');
|
||||
|
@ -736,6 +787,12 @@ class AgentPolicyService {
|
|||
return;
|
||||
}
|
||||
|
||||
for (const policyId of agentPolicyIds) {
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `User deploying policy [id=${policyId}]`,
|
||||
});
|
||||
}
|
||||
|
||||
const policies = await agentPolicyService.getByIDs(soClient, agentPolicyIds);
|
||||
const policiesMap = keyBy(policies, 'id');
|
||||
const fullPolicies = await Promise.all(
|
||||
|
@ -835,6 +892,10 @@ class AgentPolicyService {
|
|||
esClient: ElasticsearchClient,
|
||||
agentPolicyId: string
|
||||
) {
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `User deleting policy [id=${agentPolicyId}]`,
|
||||
});
|
||||
|
||||
await esClient.deleteByQuery({
|
||||
index: AGENT_POLICY_INDEX,
|
||||
ignore_unavailable: true,
|
||||
|
|
|
@ -56,6 +56,7 @@ async function createPackagePolicy(
|
|||
// rollback agent policy on error
|
||||
await agentPolicyService.delete(soClient, esClient, agentPolicy.id, {
|
||||
force: true,
|
||||
user: options.user,
|
||||
});
|
||||
throw error;
|
||||
});
|
||||
|
|
|
@ -7,15 +7,26 @@
|
|||
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import type { NewAgentAction } from '../../../common/types';
|
||||
|
||||
import { createAppContextStartContractMock } from '../../mocks';
|
||||
import { appContextService } from '../app_context';
|
||||
import { auditLoggingService } from '../audit_logging';
|
||||
|
||||
import { cancelAgentAction, getAgentsByActionsIds } from './actions';
|
||||
import {
|
||||
bulkCreateAgentActionResults,
|
||||
bulkCreateAgentActions,
|
||||
cancelAgentAction,
|
||||
createAgentAction,
|
||||
getAgentsByActionsIds,
|
||||
} from './actions';
|
||||
import { bulkUpdateAgents } from './crud';
|
||||
|
||||
jest.mock('./crud');
|
||||
jest.mock('../audit_logging');
|
||||
|
||||
const mockedBulkUpdateAgents = bulkUpdateAgents as jest.Mock;
|
||||
const mockedBulkUpdateAgents = bulkUpdateAgents as jest.MockedFunction<typeof bulkUpdateAgents>;
|
||||
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
|
||||
|
||||
describe('Agent actions', () => {
|
||||
beforeEach(async () => {
|
||||
|
@ -24,7 +35,115 @@ describe('Agent actions', () => {
|
|||
|
||||
afterEach(() => {
|
||||
appContextService.stop();
|
||||
mockedAuditLoggingService.writeCustomAuditLog.mockReset();
|
||||
});
|
||||
|
||||
describe('getAgentActions', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const esClientMock = elasticsearchServiceMock.createInternalClient();
|
||||
|
||||
esClientMock.search.mockResolvedValue({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
action_id: 'action1',
|
||||
agents: ['agent1'],
|
||||
expiration: new Date().toISOString(),
|
||||
type: 'UPGRADE',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any);
|
||||
|
||||
await getAgentsByActionsIds(esClientMock, ['action1']);
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomAuditLog).toHaveBeenCalledWith({
|
||||
message: `User retrieved Fleet action [id=action1]`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAgentAction', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const esClient = elasticsearchServiceMock.createInternalClient();
|
||||
esClient.search.mockResolvedValue({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
type: 'UPGRADE',
|
||||
action_id: 'action1',
|
||||
agents: ['agent1', 'agent2'],
|
||||
expiration: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any);
|
||||
|
||||
await createAgentAction(esClient, {
|
||||
id: 'action1',
|
||||
type: 'UPGRADE',
|
||||
agents: ['agent1'],
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomAuditLog).toHaveBeenCalledWith({
|
||||
message: expect.stringMatching(/User created Fleet action/),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkCreateAgentAction', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const esClient = elasticsearchServiceMock.createInternalClient();
|
||||
|
||||
const newActions: NewAgentAction[] = [
|
||||
{
|
||||
id: 'action1',
|
||||
type: 'UPGRADE',
|
||||
agents: ['agent1'],
|
||||
},
|
||||
{
|
||||
id: 'action2',
|
||||
type: 'UPGRADE',
|
||||
agents: ['agent2'],
|
||||
},
|
||||
{
|
||||
id: 'action3',
|
||||
type: 'UPGRADE',
|
||||
agents: ['agent3'],
|
||||
},
|
||||
];
|
||||
|
||||
await bulkCreateAgentActions(esClient, newActions);
|
||||
|
||||
for (const action of newActions) {
|
||||
expect(mockedAuditLoggingService.writeCustomAuditLog).toHaveBeenCalledWith({
|
||||
message: `User created Fleet action [id=${action.id}]`,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkCreateAgentActionResults', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const mockEsClient = elasticsearchServiceMock.createInternalClient();
|
||||
|
||||
await bulkCreateAgentActionResults(mockEsClient, [
|
||||
{
|
||||
actionId: 'action1',
|
||||
agentId: 'agent1',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomAuditLog).toHaveBeenCalledWith({
|
||||
message: `User created Fleet action result [id=action1]`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelAgentAction', () => {
|
||||
it('throw if the target action is not found', async () => {
|
||||
const esClient = elasticsearchServiceMock.createInternalClient();
|
||||
|
@ -114,6 +233,7 @@ describe('Agent actions', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAgentsByActionsIds', () => {
|
||||
const esClientMock = elasticsearchServiceMock.createElasticsearchClient();
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ import {
|
|||
} from '../../../common/constants';
|
||||
import { AgentActionNotFoundError } from '../../errors';
|
||||
|
||||
import { auditLoggingService } from '../audit_logging';
|
||||
|
||||
import { bulkUpdateAgents } from './crud';
|
||||
|
||||
const ONE_MONTH_IN_MS = 2592000000;
|
||||
|
@ -57,6 +59,10 @@ export async function createAgentAction(
|
|||
refresh: 'wait_for',
|
||||
});
|
||||
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `User created Fleet action [id=${actionId}]`,
|
||||
});
|
||||
|
||||
return {
|
||||
id: actionId,
|
||||
...newAgentAction,
|
||||
|
@ -105,6 +111,12 @@ export async function bulkCreateAgentActions(
|
|||
}),
|
||||
});
|
||||
|
||||
for (const action of actions) {
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `User created Fleet action [id=${action.id}]`,
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
|
@ -164,6 +176,12 @@ export async function bulkCreateAgentActionResults(
|
|||
];
|
||||
});
|
||||
|
||||
for (const result of results) {
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `User created Fleet action result [id=${result.actionId}]`,
|
||||
});
|
||||
}
|
||||
|
||||
await esClient.bulk({
|
||||
index: AGENT_ACTIONS_RESULTS_INDEX,
|
||||
body: bulkBody,
|
||||
|
@ -192,10 +210,20 @@ export async function getAgentActions(esClient: ElasticsearchClient, actionId: s
|
|||
throw new AgentActionNotFoundError('Action not found');
|
||||
}
|
||||
|
||||
return res.hits.hits.map((hit) => ({
|
||||
...hit._source,
|
||||
id: hit._id,
|
||||
})) as FleetServerAgentAction[];
|
||||
const result: FleetServerAgentAction[] = [];
|
||||
|
||||
for (const hit of res.hits.hits) {
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `User retrieved Fleet action [id=${hit._source?.action_id}]`,
|
||||
});
|
||||
|
||||
result.push({
|
||||
...hit._source,
|
||||
id: hit._id,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getUnenrollAgentActions(
|
||||
|
@ -227,10 +255,20 @@ export async function getUnenrollAgentActions(
|
|||
size: SO_SEARCH_LIMIT,
|
||||
});
|
||||
|
||||
return res.hits.hits.map((hit) => ({
|
||||
...hit._source,
|
||||
id: hit._id,
|
||||
}));
|
||||
const result: FleetServerAgentAction[] = [];
|
||||
|
||||
for (const hit of res.hits.hits) {
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `User retrieved Fleet action [id=${hit._source?.action_id}]`,
|
||||
});
|
||||
|
||||
result.push({
|
||||
...hit._source,
|
||||
id: hit._id,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function cancelAgentAction(esClient: ElasticsearchClient, actionId: string) {
|
||||
|
@ -255,6 +293,12 @@ export async function cancelAgentAction(esClient: ElasticsearchClient, actionId:
|
|||
throw new AgentActionNotFoundError('Action not found');
|
||||
}
|
||||
|
||||
for (const hit of res.hits.hits) {
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `User retrieved Fleet action [id=${hit._source?.action_id}]}]`,
|
||||
});
|
||||
}
|
||||
|
||||
const upgradeActions: FleetServerAgentAction[] = res.hits.hits
|
||||
.map((hit) => hit._source as FleetServerAgentAction)
|
||||
.filter(
|
||||
|
@ -368,10 +412,20 @@ async function getAgentActionsByIds(esClient: ElasticsearchClient, actionIds: st
|
|||
throw new AgentActionNotFoundError('Action not found');
|
||||
}
|
||||
|
||||
return res.hits.hits.map((hit) => ({
|
||||
...hit._source,
|
||||
id: hit._id,
|
||||
})) as FleetServerAgentAction[];
|
||||
const result: FleetServerAgentAction[] = [];
|
||||
|
||||
for (const hit of res.hits.hits) {
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `User retrieved Fleet action [id=${hit._source?.action_id}]`,
|
||||
});
|
||||
|
||||
result.push({
|
||||
...hit._source,
|
||||
id: hit._id,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const getAgentsByActionsIds = async (
|
||||
|
|
|
@ -6,16 +6,28 @@
|
|||
*/
|
||||
import { errors } from '@elastic/elasticsearch';
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import { AGENTS_INDEX } from '../../constants';
|
||||
import type { Agent } from '../../types';
|
||||
|
||||
import { getAgentsByKuery, getAgentTags } from './crud';
|
||||
import { auditLoggingService } from '../audit_logging';
|
||||
|
||||
import {
|
||||
closePointInTime,
|
||||
getAgentsByKuery,
|
||||
getAgentTags,
|
||||
openPointInTime,
|
||||
updateAgent,
|
||||
} from './crud';
|
||||
|
||||
jest.mock('../audit_logging');
|
||||
jest.mock('../../../common/services/is_agent_upgradeable', () => ({
|
||||
isAgentUpgradeable: jest.fn().mockImplementation((agent: Agent) => agent.id.includes('up')),
|
||||
}));
|
||||
|
||||
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
|
||||
|
||||
describe('Agents CRUD test', () => {
|
||||
const soClientMock = savedObjectsClientMock.create();
|
||||
let esClientMock: ElasticsearchClient;
|
||||
|
@ -308,4 +320,44 @@ describe('Agents CRUD test', () => {
|
|||
expect(searchMock.mock.calls.at(-1)[0].sort).toEqual([{ policy_id: { order: 'desc' } }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should write to audit log', async () => {
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
|
||||
esClient.update.mockResolvedValueOnce({} as any);
|
||||
|
||||
await updateAgent(esClient, 'test-agent-id', { tags: ['new-tag'] });
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomAuditLog).toHaveBeenCalledWith({
|
||||
message: 'User updated agent [id=test-agent-id]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('openPointInTime', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
esClient.openPointInTime.mockResolvedValueOnce({ id: 'test-pit' } as any);
|
||||
|
||||
await openPointInTime(esClient, AGENTS_INDEX);
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomAuditLog).toHaveBeenCalledWith({
|
||||
message: `User opened point in time query [index=${AGENTS_INDEX}] [pitId=test-pit]`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('closePointInTime', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
esClient.closePointInTime.mockResolvedValueOnce({} as any);
|
||||
|
||||
await closePointInTime(esClient, 'test-pit');
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomAuditLog).toHaveBeenCalledWith({
|
||||
message: `User closing point in time query [pitId=test-pit]`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,6 +19,8 @@ import { isAgentUpgradeable } from '../../../common/services';
|
|||
import { AGENTS_INDEX } from '../../constants';
|
||||
import { FleetError, isESClientError, AgentNotFoundError } from '../../errors';
|
||||
|
||||
import { auditLoggingService } from '../audit_logging';
|
||||
|
||||
import { searchHitToAgent, agentSOAttributesToFleetServerAgentDoc } from './helpers';
|
||||
|
||||
import { buildAgentStatusRuntimeField } from './build_status_runtime_field';
|
||||
|
@ -105,10 +107,19 @@ export async function openPointInTime(
|
|||
index,
|
||||
keep_alive: pitKeepAlive,
|
||||
});
|
||||
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `User opened point in time query [index=${index}] [pitId=${pitRes.id}]`,
|
||||
});
|
||||
|
||||
return pitRes.id;
|
||||
}
|
||||
|
||||
export async function closePointInTime(esClient: ElasticsearchClient, pitId: string) {
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `User closing point in time query [pitId=${pitId}]`,
|
||||
});
|
||||
|
||||
try {
|
||||
await esClient.closePointInTime({ id: pitId });
|
||||
} catch (error) {
|
||||
|
@ -465,6 +476,10 @@ export async function updateAgent(
|
|||
agentId: string,
|
||||
data: Partial<AgentSOAttributes>
|
||||
) {
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `User updated agent [id=${agentId}]`,
|
||||
});
|
||||
|
||||
await esClient.update({
|
||||
id: agentId,
|
||||
index: AGENTS_INDEX,
|
||||
|
|
|
@ -14,49 +14,7 @@ import { createAppContextStartContractMock } from '../../mocks';
|
|||
import { reassignAgent, reassignAgents } from './reassign';
|
||||
import { createClientMock } from './action.mock';
|
||||
|
||||
describe('reassignAgent (singular)', () => {
|
||||
it('can reassign from regular agent policy to regular', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc, regularAgentPolicySO } = createClientMock();
|
||||
await reassignAgent(soClient, esClient, agentInRegularDoc._id, regularAgentPolicySO.id);
|
||||
|
||||
// calls ES update with correct values
|
||||
expect(esClient.update).toBeCalledTimes(1);
|
||||
const calledWith = esClient.update.mock.calls[0];
|
||||
expect(calledWith[0]?.id).toBe(agentInRegularDoc._id);
|
||||
expect((calledWith[0] as estypes.UpdateRequest)?.body?.doc).toHaveProperty(
|
||||
'policy_id',
|
||||
regularAgentPolicySO.id
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot reassign from regular agent policy to hosted', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc, hostedAgentPolicySO } = createClientMock();
|
||||
await expect(
|
||||
reassignAgent(soClient, esClient, agentInRegularDoc._id, hostedAgentPolicySO.id)
|
||||
).rejects.toThrowError(HostedAgentPolicyRestrictionRelatedError);
|
||||
|
||||
// does not call ES update
|
||||
expect(esClient.update).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('cannot reassign from hosted agent policy', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc, hostedAgentPolicySO, regularAgentPolicySO } =
|
||||
createClientMock();
|
||||
await expect(
|
||||
reassignAgent(soClient, esClient, agentInHostedDoc._id, regularAgentPolicySO.id)
|
||||
).rejects.toThrowError(HostedAgentPolicyRestrictionRelatedError);
|
||||
// does not call ES update
|
||||
expect(esClient.update).toBeCalledTimes(0);
|
||||
|
||||
await expect(
|
||||
reassignAgent(soClient, esClient, agentInHostedDoc._id, hostedAgentPolicySO.id)
|
||||
).rejects.toThrowError(HostedAgentPolicyRestrictionRelatedError);
|
||||
// does not call ES update
|
||||
expect(esClient.update).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reassignAgents (plural)', () => {
|
||||
describe('reassignAgent', () => {
|
||||
beforeEach(async () => {
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
|
@ -64,68 +22,122 @@ describe('reassignAgents (plural)', () => {
|
|||
afterEach(() => {
|
||||
appContextService.stop();
|
||||
});
|
||||
it('agents in hosted policies are not updated', async () => {
|
||||
const {
|
||||
soClient,
|
||||
esClient,
|
||||
agentInRegularDoc,
|
||||
agentInHostedDoc,
|
||||
agentInHostedDoc2,
|
||||
regularAgentPolicySO2,
|
||||
} = createClientMock();
|
||||
describe('reassignAgent (singular)', () => {
|
||||
it('can reassign from regular agent policy to regular', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc, regularAgentPolicySO } = createClientMock();
|
||||
await reassignAgent(soClient, esClient, agentInRegularDoc._id, regularAgentPolicySO.id);
|
||||
|
||||
esClient.search.mockResponse({
|
||||
hits: {
|
||||
hits: [agentInRegularDoc, agentInHostedDoc, agentInHostedDoc2],
|
||||
},
|
||||
} as any);
|
||||
|
||||
const idsToReassign = [agentInRegularDoc._id, agentInHostedDoc._id, agentInHostedDoc2._id];
|
||||
await reassignAgents(soClient, esClient, { agentIds: idsToReassign }, regularAgentPolicySO2.id);
|
||||
|
||||
// calls ES update with correct values
|
||||
const calledWith = esClient.bulk.mock.calls[0][0];
|
||||
// only 1 are regular and bulk write two line per update
|
||||
expect((calledWith as estypes.BulkRequest).body?.length).toBe(2);
|
||||
// @ts-expect-error
|
||||
expect(calledWith.body[0].update._id).toEqual(agentInRegularDoc._id);
|
||||
|
||||
// hosted policy is updated in action results with error
|
||||
const calledWithActionResults = esClient.bulk.mock.calls[1][0] as estypes.BulkRequest;
|
||||
// bulk write two line per create
|
||||
expect(calledWithActionResults.body?.length).toBe(4);
|
||||
const expectedObject = expect.objectContaining({
|
||||
'@timestamp': expect.anything(),
|
||||
action_id: expect.anything(),
|
||||
agent_id: 'agent-in-hosted-policy',
|
||||
error:
|
||||
'Cannot reassign an agent from hosted agent policy hosted-agent-policy in Fleet because the agent policy is managed by an external orchestration solution, such as Elastic Cloud, Kubernetes, etc. Please make changes using your orchestration solution.',
|
||||
// calls ES update with correct values
|
||||
expect(esClient.update).toBeCalledTimes(1);
|
||||
const calledWith = esClient.update.mock.calls[0];
|
||||
expect(calledWith[0]?.id).toBe(agentInRegularDoc._id);
|
||||
expect((calledWith[0] as estypes.UpdateRequest)?.body?.doc).toHaveProperty(
|
||||
'policy_id',
|
||||
regularAgentPolicySO.id
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot reassign from regular agent policy to hosted', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc, hostedAgentPolicySO } = createClientMock();
|
||||
await expect(
|
||||
reassignAgent(soClient, esClient, agentInRegularDoc._id, hostedAgentPolicySO.id)
|
||||
).rejects.toThrowError(HostedAgentPolicyRestrictionRelatedError);
|
||||
|
||||
// does not call ES update
|
||||
expect(esClient.update).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('cannot reassign from hosted agent policy', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc, hostedAgentPolicySO, regularAgentPolicySO } =
|
||||
createClientMock();
|
||||
await expect(
|
||||
reassignAgent(soClient, esClient, agentInHostedDoc._id, regularAgentPolicySO.id)
|
||||
).rejects.toThrowError(HostedAgentPolicyRestrictionRelatedError);
|
||||
// does not call ES update
|
||||
expect(esClient.update).toBeCalledTimes(0);
|
||||
|
||||
await expect(
|
||||
reassignAgent(soClient, esClient, agentInHostedDoc._id, hostedAgentPolicySO.id)
|
||||
).rejects.toThrowError(HostedAgentPolicyRestrictionRelatedError);
|
||||
// does not call ES update
|
||||
expect(esClient.update).toBeCalledTimes(0);
|
||||
});
|
||||
expect(calledWithActionResults.body?.[1] as any).toEqual(expectedObject);
|
||||
});
|
||||
|
||||
it('should report errors from ES agent update call', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc, regularAgentPolicySO2 } = createClientMock();
|
||||
esClient.bulk.mockResponse({
|
||||
items: [
|
||||
{
|
||||
update: {
|
||||
_id: agentInRegularDoc._id,
|
||||
error: new Error('version conflict'),
|
||||
},
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
const idsToReassign = [agentInRegularDoc._id];
|
||||
await reassignAgents(soClient, esClient, { agentIds: idsToReassign }, regularAgentPolicySO2.id);
|
||||
describe('reassignAgents (plural)', () => {
|
||||
it('agents in hosted policies are not updated', async () => {
|
||||
const {
|
||||
soClient,
|
||||
esClient,
|
||||
agentInRegularDoc,
|
||||
agentInHostedDoc,
|
||||
agentInHostedDoc2,
|
||||
regularAgentPolicySO2,
|
||||
} = createClientMock();
|
||||
|
||||
const calledWithActionResults = esClient.bulk.mock.calls[1][0] as estypes.BulkRequest;
|
||||
const expectedObject = expect.objectContaining({
|
||||
'@timestamp': expect.anything(),
|
||||
action_id: expect.anything(),
|
||||
agent_id: agentInRegularDoc._id,
|
||||
error: 'version conflict',
|
||||
esClient.search.mockResponse({
|
||||
hits: {
|
||||
hits: [agentInRegularDoc, agentInHostedDoc, agentInHostedDoc2],
|
||||
},
|
||||
} as any);
|
||||
|
||||
const idsToReassign = [agentInRegularDoc._id, agentInHostedDoc._id, agentInHostedDoc2._id];
|
||||
await reassignAgents(
|
||||
soClient,
|
||||
esClient,
|
||||
{ agentIds: idsToReassign },
|
||||
regularAgentPolicySO2.id
|
||||
);
|
||||
|
||||
// calls ES update with correct values
|
||||
const calledWith = esClient.bulk.mock.calls[0][0];
|
||||
// only 1 are regular and bulk write two line per update
|
||||
expect((calledWith as estypes.BulkRequest).body?.length).toBe(2);
|
||||
// @ts-expect-error
|
||||
expect(calledWith.body[0].update._id).toEqual(agentInRegularDoc._id);
|
||||
|
||||
// hosted policy is updated in action results with error
|
||||
const calledWithActionResults = esClient.bulk.mock.calls[1][0] as estypes.BulkRequest;
|
||||
// bulk write two line per create
|
||||
expect(calledWithActionResults.body?.length).toBe(4);
|
||||
const expectedObject = expect.objectContaining({
|
||||
'@timestamp': expect.anything(),
|
||||
action_id: expect.anything(),
|
||||
agent_id: 'agent-in-hosted-policy',
|
||||
error:
|
||||
'Cannot reassign an agent from hosted agent policy hosted-agent-policy in Fleet because the agent policy is managed by an external orchestration solution, such as Elastic Cloud, Kubernetes, etc. Please make changes using your orchestration solution.',
|
||||
});
|
||||
expect(calledWithActionResults.body?.[1] as any).toEqual(expectedObject);
|
||||
});
|
||||
|
||||
it('should report errors from ES agent update call', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc, regularAgentPolicySO2 } = createClientMock();
|
||||
esClient.bulk.mockResponse({
|
||||
items: [
|
||||
{
|
||||
update: {
|
||||
_id: agentInRegularDoc._id,
|
||||
error: new Error('version conflict'),
|
||||
},
|
||||
},
|
||||
],
|
||||
} as any);
|
||||
const idsToReassign = [agentInRegularDoc._id];
|
||||
await reassignAgents(
|
||||
soClient,
|
||||
esClient,
|
||||
{ agentIds: idsToReassign },
|
||||
regularAgentPolicySO2.id
|
||||
);
|
||||
|
||||
const calledWithActionResults = esClient.bulk.mock.calls[1][0] as estypes.BulkRequest;
|
||||
const expectedObject = expect.objectContaining({
|
||||
'@timestamp': expect.anything(),
|
||||
action_id: expect.anything(),
|
||||
agent_id: agentInRegularDoc._id,
|
||||
error: 'version conflict',
|
||||
});
|
||||
expect(calledWithActionResults.body?.[1] as any).toEqual(expectedObject);
|
||||
});
|
||||
expect(calledWithActionResults.body?.[1] as any).toEqual(expectedObject);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,24 +12,7 @@ import { createAppContextStartContractMock } from '../../mocks';
|
|||
import { createClientMock } from './action.mock';
|
||||
import { bulkRequestDiagnostics, requestDiagnostics } from './request_diagnostics';
|
||||
|
||||
describe('requestDiagnostics (singular)', () => {
|
||||
it('can request diagnostics for single agent', async () => {
|
||||
const { esClient, agentInRegularDoc } = createClientMock();
|
||||
await requestDiagnostics(esClient, agentInRegularDoc._id);
|
||||
|
||||
expect(esClient.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
agents: ['agent-in-regular-policy'],
|
||||
type: 'REQUEST_DIAGNOSTICS',
|
||||
}),
|
||||
index: '.fleet-actions',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestDiagnostics (plural)', () => {
|
||||
describe('requestDiagnostics', () => {
|
||||
beforeEach(async () => {
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
|
@ -37,46 +20,66 @@ describe('requestDiagnostics (plural)', () => {
|
|||
afterEach(() => {
|
||||
appContextService.stop();
|
||||
});
|
||||
it('can request diagnostics for multiple agents', async () => {
|
||||
const { soClient, esClient, agentInRegularDocNewer, agentInRegularDocNewer2 } =
|
||||
createClientMock();
|
||||
const idsToAction = [agentInRegularDocNewer._id, agentInRegularDocNewer2._id];
|
||||
await bulkRequestDiagnostics(esClient, soClient, { agentIds: idsToAction });
|
||||
|
||||
expect(esClient.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
agents: ['agent-in-regular-policy-newer', 'agent-in-regular-policy-newer2'],
|
||||
type: 'REQUEST_DIAGNOSTICS',
|
||||
}),
|
||||
index: '.fleet-actions',
|
||||
})
|
||||
);
|
||||
describe('requestDiagnostics (singular)', () => {
|
||||
it('can request diagnostics for single agent', async () => {
|
||||
const { esClient, agentInRegularDoc } = createClientMock();
|
||||
await requestDiagnostics(esClient, agentInRegularDoc._id);
|
||||
|
||||
expect(esClient.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
agents: ['agent-in-regular-policy'],
|
||||
type: 'REQUEST_DIAGNOSTICS',
|
||||
}),
|
||||
index: '.fleet-actions',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when diagnostics for older agent', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc, agentInRegularDocNewer } = createClientMock();
|
||||
const idsToAction = [agentInRegularDocNewer._id, agentInRegularDoc._id];
|
||||
await bulkRequestDiagnostics(esClient, soClient, { agentIds: idsToAction });
|
||||
describe('requestDiagnostics (plural)', () => {
|
||||
it('can request diagnostics for multiple agents', async () => {
|
||||
const { soClient, esClient, agentInRegularDocNewer, agentInRegularDocNewer2 } =
|
||||
createClientMock();
|
||||
const idsToAction = [agentInRegularDocNewer._id, agentInRegularDocNewer2._id];
|
||||
await bulkRequestDiagnostics(esClient, soClient, { agentIds: idsToAction });
|
||||
|
||||
expect(esClient.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
agents: ['agent-in-regular-policy-newer', 'agent-in-regular-policy'],
|
||||
type: 'REQUEST_DIAGNOSTICS',
|
||||
}),
|
||||
index: '.fleet-actions',
|
||||
})
|
||||
);
|
||||
const calledWithActionResults = esClient.bulk.mock.calls[0][0] as estypes.BulkRequest;
|
||||
// bulk write two line per create
|
||||
expect(calledWithActionResults.body?.length).toBe(2);
|
||||
const expectedObject = expect.objectContaining({
|
||||
'@timestamp': expect.anything(),
|
||||
action_id: expect.anything(),
|
||||
agent_id: 'agent-in-regular-policy',
|
||||
error: 'Agent agent-in-regular-policy does not support request diagnostics action.',
|
||||
expect(esClient.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
agents: ['agent-in-regular-policy-newer', 'agent-in-regular-policy-newer2'],
|
||||
type: 'REQUEST_DIAGNOSTICS',
|
||||
}),
|
||||
index: '.fleet-actions',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should report error when diagnostics for older agent', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc, agentInRegularDocNewer } = createClientMock();
|
||||
const idsToAction = [agentInRegularDocNewer._id, agentInRegularDoc._id];
|
||||
await bulkRequestDiagnostics(esClient, soClient, { agentIds: idsToAction });
|
||||
|
||||
expect(esClient.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
agents: ['agent-in-regular-policy-newer', 'agent-in-regular-policy'],
|
||||
type: 'REQUEST_DIAGNOSTICS',
|
||||
}),
|
||||
index: '.fleet-actions',
|
||||
})
|
||||
);
|
||||
const calledWithActionResults = esClient.bulk.mock.calls[0][0] as estypes.BulkRequest;
|
||||
// bulk write two line per create
|
||||
expect(calledWithActionResults.body?.length).toBe(2);
|
||||
const expectedObject = expect.objectContaining({
|
||||
'@timestamp': expect.anything(),
|
||||
action_id: expect.anything(),
|
||||
agent_id: 'agent-in-regular-policy',
|
||||
error: 'Agent agent-in-regular-policy does not support request diagnostics action.',
|
||||
});
|
||||
expect(calledWithActionResults.body?.[1] as any).toEqual(expectedObject);
|
||||
});
|
||||
expect(calledWithActionResults.body?.[1] as any).toEqual(expectedObject);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,62 +25,7 @@ jest.mock('../api_keys');
|
|||
|
||||
const mockedInvalidateAPIKeys = invalidateAPIKeys as jest.MockedFunction<typeof invalidateAPIKeys>;
|
||||
|
||||
describe('unenrollAgent (singular)', () => {
|
||||
it('can unenroll from regular agent policy', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc } = createClientMock();
|
||||
await unenrollAgent(soClient, esClient, agentInRegularDoc._id);
|
||||
|
||||
// calls ES update with correct values
|
||||
expect(esClient.update).toBeCalledTimes(1);
|
||||
const calledWith = esClient.update.mock.calls[0];
|
||||
expect(calledWith[0]?.id).toBe(agentInRegularDoc._id);
|
||||
expect((calledWith[0] as estypes.UpdateRequest)?.body).toHaveProperty(
|
||||
'doc.unenrollment_started_at'
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot unenroll from hosted agent policy by default', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc } = createClientMock();
|
||||
await expect(unenrollAgent(soClient, esClient, agentInHostedDoc._id)).rejects.toThrowError(
|
||||
HostedAgentPolicyRestrictionRelatedError
|
||||
);
|
||||
// does not call ES update
|
||||
expect(esClient.update).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('cannot unenroll from hosted agent policy with revoke=true', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc } = createClientMock();
|
||||
await expect(
|
||||
unenrollAgent(soClient, esClient, agentInHostedDoc._id, { revoke: true })
|
||||
).rejects.toThrowError(HostedAgentPolicyRestrictionRelatedError);
|
||||
// does not call ES update
|
||||
expect(esClient.update).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('can unenroll from hosted agent policy with force=true', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc } = createClientMock();
|
||||
await unenrollAgent(soClient, esClient, agentInHostedDoc._id, { force: true });
|
||||
// calls ES update with correct values
|
||||
expect(esClient.update).toBeCalledTimes(1);
|
||||
const calledWith = esClient.update.mock.calls[0];
|
||||
expect(calledWith[0]?.id).toBe(agentInHostedDoc._id);
|
||||
expect((calledWith[0] as estypes.UpdateRequest)?.body).toHaveProperty(
|
||||
'doc.unenrollment_started_at'
|
||||
);
|
||||
});
|
||||
|
||||
it('can unenroll from hosted agent policy with force=true and revoke=true', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc } = createClientMock();
|
||||
await unenrollAgent(soClient, esClient, agentInHostedDoc._id, { force: true, revoke: true });
|
||||
// calls ES update with correct values
|
||||
expect(esClient.update).toBeCalledTimes(1);
|
||||
const calledWith = esClient.update.mock.calls[0];
|
||||
expect(calledWith[0]?.id).toBe(agentInHostedDoc._id);
|
||||
expect((calledWith[0] as estypes.UpdateRequest)?.body).toHaveProperty('doc.unenrolled_at');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unenrollAgents (plural)', () => {
|
||||
describe('unenroll', () => {
|
||||
beforeEach(async () => {
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
|
@ -88,320 +33,381 @@ describe('unenrollAgents (plural)', () => {
|
|||
afterEach(() => {
|
||||
appContextService.stop();
|
||||
});
|
||||
it('can unenroll from an regular agent policy', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc, agentInRegularDoc2 } = createClientMock();
|
||||
const idsToUnenroll = [agentInRegularDoc._id, agentInRegularDoc2._id];
|
||||
await unenrollAgents(soClient, esClient, { agentIds: idsToUnenroll });
|
||||
|
||||
// calls ES update with correct values
|
||||
const calledWith = esClient.bulk.mock.calls[0][0];
|
||||
const ids = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.update !== undefined)
|
||||
.map((i: any) => i.update._id);
|
||||
const docs = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.doc)
|
||||
.map((i: any) => i.doc);
|
||||
expect(ids).toEqual(idsToUnenroll);
|
||||
for (const doc of docs!) {
|
||||
expect(doc).toHaveProperty('unenrollment_started_at');
|
||||
}
|
||||
});
|
||||
it('cannot unenroll from a hosted agent policy by default', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2 } =
|
||||
createClientMock();
|
||||
describe('unenrollAgent (singular)', () => {
|
||||
it('can unenroll from regular agent policy', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc } = createClientMock();
|
||||
await unenrollAgent(soClient, esClient, agentInRegularDoc._id);
|
||||
|
||||
const idsToUnenroll = [agentInRegularDoc._id, agentInHostedDoc._id, agentInRegularDoc2._id];
|
||||
await unenrollAgents(soClient, esClient, { agentIds: idsToUnenroll });
|
||||
|
||||
// calls ES update with correct values
|
||||
const onlyRegular = [agentInRegularDoc._id, agentInRegularDoc2._id];
|
||||
const calledWith = esClient.bulk.mock.calls[0][0];
|
||||
const ids = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.update !== undefined)
|
||||
.map((i: any) => i.update._id);
|
||||
const docs = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.doc)
|
||||
.map((i: any) => i.doc);
|
||||
expect(ids).toEqual(onlyRegular);
|
||||
for (const doc of docs!) {
|
||||
expect(doc).toHaveProperty('unenrollment_started_at');
|
||||
}
|
||||
|
||||
// hosted policy is updated in action results with error
|
||||
const calledWithActionResults = esClient.bulk.mock.calls[1][0] as estypes.BulkRequest;
|
||||
// bulk write two line per create
|
||||
expect(calledWithActionResults.body?.length).toBe(2);
|
||||
const expectedObject = expect.objectContaining({
|
||||
'@timestamp': expect.anything(),
|
||||
action_id: expect.anything(),
|
||||
agent_id: 'agent-in-hosted-policy',
|
||||
error:
|
||||
'Cannot unenroll agent-in-hosted-policy from a hosted agent policy hosted-agent-policy in Fleet because the agent policy is managed by an external orchestration solution, such as Elastic Cloud, Kubernetes, etc. Please make changes using your orchestration solution.',
|
||||
// calls ES update with correct values
|
||||
expect(esClient.update).toBeCalledTimes(1);
|
||||
const calledWith = esClient.update.mock.calls[0];
|
||||
expect(calledWith[0]?.id).toBe(agentInRegularDoc._id);
|
||||
expect((calledWith[0] as estypes.UpdateRequest)?.body).toHaveProperty(
|
||||
'doc.unenrollment_started_at'
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot unenroll from hosted agent policy by default', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc } = createClientMock();
|
||||
await expect(unenrollAgent(soClient, esClient, agentInHostedDoc._id)).rejects.toThrowError(
|
||||
HostedAgentPolicyRestrictionRelatedError
|
||||
);
|
||||
// does not call ES update
|
||||
expect(esClient.update).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('cannot unenroll from hosted agent policy with revoke=true', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc } = createClientMock();
|
||||
await expect(
|
||||
unenrollAgent(soClient, esClient, agentInHostedDoc._id, { revoke: true })
|
||||
).rejects.toThrowError(HostedAgentPolicyRestrictionRelatedError);
|
||||
// does not call ES update
|
||||
expect(esClient.update).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('can unenroll from hosted agent policy with force=true', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc } = createClientMock();
|
||||
await unenrollAgent(soClient, esClient, agentInHostedDoc._id, { force: true });
|
||||
// calls ES update with correct values
|
||||
expect(esClient.update).toBeCalledTimes(1);
|
||||
const calledWith = esClient.update.mock.calls[0];
|
||||
expect(calledWith[0]?.id).toBe(agentInHostedDoc._id);
|
||||
expect((calledWith[0] as estypes.UpdateRequest)?.body).toHaveProperty(
|
||||
'doc.unenrollment_started_at'
|
||||
);
|
||||
});
|
||||
|
||||
it('can unenroll from hosted agent policy with force=true and revoke=true', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc } = createClientMock();
|
||||
await unenrollAgent(soClient, esClient, agentInHostedDoc._id, { force: true, revoke: true });
|
||||
// calls ES update with correct values
|
||||
expect(esClient.update).toBeCalledTimes(1);
|
||||
const calledWith = esClient.update.mock.calls[0];
|
||||
expect(calledWith[0]?.id).toBe(agentInHostedDoc._id);
|
||||
expect((calledWith[0] as estypes.UpdateRequest)?.body).toHaveProperty('doc.unenrolled_at');
|
||||
});
|
||||
expect(calledWithActionResults.body?.[1] as any).toEqual(expectedObject);
|
||||
});
|
||||
|
||||
it('force unenroll updates in progress unenroll actions', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc, agentInRegularDoc2 } = createClientMock();
|
||||
esClient.search.mockReset();
|
||||
describe('unenrollAgents (plural)', () => {
|
||||
it('can unenroll from an regular agent policy', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc, agentInRegularDoc2 } = createClientMock();
|
||||
const idsToUnenroll = [agentInRegularDoc._id, agentInRegularDoc2._id];
|
||||
await unenrollAgents(soClient, esClient, { agentIds: idsToUnenroll });
|
||||
|
||||
esClient.search.mockImplementation(async (request) => {
|
||||
if (request?.index === AGENT_ACTIONS_INDEX) {
|
||||
return {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
agents: ['agent-in-regular-policy'],
|
||||
action_id: 'other-action',
|
||||
// calls ES update with correct values
|
||||
const calledWith = esClient.bulk.mock.calls[0][0];
|
||||
const ids = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.update !== undefined)
|
||||
.map((i: any) => i.update._id);
|
||||
const docs = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.doc)
|
||||
.map((i: any) => i.doc);
|
||||
expect(ids).toEqual(idsToUnenroll);
|
||||
for (const doc of docs!) {
|
||||
expect(doc).toHaveProperty('unenrollment_started_at');
|
||||
}
|
||||
});
|
||||
it('cannot unenroll from a hosted agent policy by default', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2 } =
|
||||
createClientMock();
|
||||
|
||||
const idsToUnenroll = [agentInRegularDoc._id, agentInHostedDoc._id, agentInRegularDoc2._id];
|
||||
await unenrollAgents(soClient, esClient, { agentIds: idsToUnenroll });
|
||||
|
||||
// calls ES update with correct values
|
||||
const onlyRegular = [agentInRegularDoc._id, agentInRegularDoc2._id];
|
||||
const calledWith = esClient.bulk.mock.calls[0][0];
|
||||
const ids = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.update !== undefined)
|
||||
.map((i: any) => i.update._id);
|
||||
const docs = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.doc)
|
||||
.map((i: any) => i.doc);
|
||||
expect(ids).toEqual(onlyRegular);
|
||||
for (const doc of docs!) {
|
||||
expect(doc).toHaveProperty('unenrollment_started_at');
|
||||
}
|
||||
|
||||
// hosted policy is updated in action results with error
|
||||
const calledWithActionResults = esClient.bulk.mock.calls[1][0] as estypes.BulkRequest;
|
||||
// bulk write two line per create
|
||||
expect(calledWithActionResults.body?.length).toBe(2);
|
||||
const expectedObject = expect.objectContaining({
|
||||
'@timestamp': expect.anything(),
|
||||
action_id: expect.anything(),
|
||||
agent_id: 'agent-in-hosted-policy',
|
||||
error:
|
||||
'Cannot unenroll agent-in-hosted-policy from a hosted agent policy hosted-agent-policy in Fleet because the agent policy is managed by an external orchestration solution, such as Elastic Cloud, Kubernetes, etc. Please make changes using your orchestration solution.',
|
||||
});
|
||||
expect(calledWithActionResults.body?.[1] as any).toEqual(expectedObject);
|
||||
});
|
||||
|
||||
it('force unenroll updates in progress unenroll actions', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc, agentInRegularDoc2 } = createClientMock();
|
||||
esClient.search.mockReset();
|
||||
|
||||
esClient.search.mockImplementation(async (request) => {
|
||||
if (request?.index === AGENT_ACTIONS_INDEX) {
|
||||
return {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
agents: ['agent-in-regular-policy'],
|
||||
action_id: 'other-action',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
],
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
|
||||
if (request?.index === AGENT_ACTIONS_RESULTS_INDEX) {
|
||||
return {
|
||||
hits: {
|
||||
hits: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
if (request?.index === AGENT_ACTIONS_RESULTS_INDEX) {
|
||||
return {
|
||||
hits: {
|
||||
hits: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { hits: { hits: [agentInRegularDoc, agentInRegularDoc2] } };
|
||||
return { hits: { hits: [agentInRegularDoc, agentInRegularDoc2] } };
|
||||
});
|
||||
|
||||
const idsToUnenroll = [agentInRegularDoc._id, agentInRegularDoc2._id];
|
||||
await unenrollAgents(soClient, esClient, {
|
||||
agentIds: idsToUnenroll,
|
||||
revoke: true,
|
||||
});
|
||||
|
||||
expect(esClient.bulk.mock.calls.length).toEqual(3);
|
||||
const bulkBody = (esClient.bulk.mock.calls[2][0] as estypes.BulkRequest)?.body?.[1] as any;
|
||||
expect(bulkBody.agent_id).toEqual(agentInRegularDoc._id);
|
||||
expect(bulkBody.action_id).toEqual('other-action');
|
||||
});
|
||||
|
||||
const idsToUnenroll = [agentInRegularDoc._id, agentInRegularDoc2._id];
|
||||
await unenrollAgents(soClient, esClient, {
|
||||
agentIds: idsToUnenroll,
|
||||
revoke: true,
|
||||
});
|
||||
|
||||
expect(esClient.bulk.mock.calls.length).toEqual(3);
|
||||
const bulkBody = (esClient.bulk.mock.calls[2][0] as estypes.BulkRequest)?.body?.[1] as any;
|
||||
expect(bulkBody.agent_id).toEqual(agentInRegularDoc._id);
|
||||
expect(bulkBody.action_id).toEqual('other-action');
|
||||
});
|
||||
|
||||
it('force unenroll should not update completed unenroll actions', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc, agentInRegularDoc2 } = createClientMock();
|
||||
esClient.search.mockReset();
|
||||
esClient.search.mockImplementation(async (request) => {
|
||||
if (request?.index === AGENT_ACTIONS_INDEX) {
|
||||
return {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
agents: ['agent-in-regular-policy'],
|
||||
action_id: 'other-action1',
|
||||
it('force unenroll should not update completed unenroll actions', async () => {
|
||||
const { soClient, esClient, agentInRegularDoc, agentInRegularDoc2 } = createClientMock();
|
||||
esClient.search.mockReset();
|
||||
esClient.search.mockImplementation(async (request) => {
|
||||
if (request?.index === AGENT_ACTIONS_INDEX) {
|
||||
return {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
agents: ['agent-in-regular-policy'],
|
||||
action_id: 'other-action1',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any;
|
||||
],
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
|
||||
if (request?.index === AGENT_ACTIONS_RESULTS_INDEX) {
|
||||
return {
|
||||
hits: {
|
||||
hits: [
|
||||
{ _source: { action_id: 'other-action1', agent_id: 'agent-in-regular-policy' } },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { hits: { hits: [agentInRegularDoc, agentInRegularDoc2] } };
|
||||
});
|
||||
|
||||
const idsToUnenroll = [agentInRegularDoc._id, agentInRegularDoc2._id];
|
||||
await unenrollAgents(soClient, esClient, {
|
||||
agentIds: idsToUnenroll,
|
||||
revoke: true,
|
||||
});
|
||||
|
||||
// agent and force unenroll results updated, no other action results
|
||||
expect(esClient.bulk.mock.calls.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('cannot unenroll from a hosted agent policy with revoke=true', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2 } =
|
||||
createClientMock();
|
||||
|
||||
const idsToUnenroll = [agentInRegularDoc._id, agentInHostedDoc._id, agentInRegularDoc2._id];
|
||||
|
||||
const unenrolledResponse = await unenrollAgents(soClient, esClient, {
|
||||
agentIds: idsToUnenroll,
|
||||
revoke: true,
|
||||
});
|
||||
expect(unenrolledResponse.actionId).toBeDefined();
|
||||
|
||||
// calls ES update with correct values
|
||||
const onlyRegular = [agentInRegularDoc._id, agentInRegularDoc2._id];
|
||||
const calledWith = esClient.bulk.mock.calls[0][0];
|
||||
const ids = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.update !== undefined)
|
||||
.map((i: any) => i.update._id);
|
||||
const docs = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.doc)
|
||||
.map((i: any) => i.doc);
|
||||
expect(ids).toEqual(onlyRegular);
|
||||
for (const doc of docs!) {
|
||||
expect(doc).toHaveProperty('unenrolled_at');
|
||||
}
|
||||
|
||||
if (request?.index === AGENT_ACTIONS_RESULTS_INDEX) {
|
||||
return {
|
||||
hits: {
|
||||
hits: [
|
||||
{ _source: { action_id: 'other-action1', agent_id: 'agent-in-regular-policy' } },
|
||||
],
|
||||
},
|
||||
};
|
||||
const errorResults = esClient.bulk.mock.calls[2][0];
|
||||
const errorIds = (errorResults as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.agent_id)
|
||||
.map((i: any) => i.agent_id);
|
||||
expect(errorIds).toEqual([agentInHostedDoc._id]);
|
||||
|
||||
const actionResults = esClient.bulk.mock.calls[1][0];
|
||||
const resultIds = (actionResults as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.agent_id)
|
||||
.map((i: any) => i.agent_id);
|
||||
expect(resultIds).toEqual(onlyRegular);
|
||||
|
||||
const action = esClient.create.mock.calls[0][0] as any;
|
||||
expect(action.body.type).toEqual('FORCE_UNENROLL');
|
||||
});
|
||||
|
||||
it('can unenroll from hosted agent policy with force=true', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2 } =
|
||||
createClientMock();
|
||||
const idsToUnenroll = [agentInRegularDoc._id, agentInHostedDoc._id, agentInRegularDoc2._id];
|
||||
await unenrollAgents(soClient, esClient, { agentIds: idsToUnenroll, force: true });
|
||||
|
||||
// calls ES update with correct values
|
||||
const calledWith = esClient.bulk.mock.calls[0][0];
|
||||
const ids = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.update !== undefined)
|
||||
.map((i: any) => i.update._id);
|
||||
const docs = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.doc)
|
||||
.map((i: any) => i.doc);
|
||||
expect(ids).toEqual(idsToUnenroll);
|
||||
for (const doc of docs!) {
|
||||
expect(doc).toHaveProperty('unenrollment_started_at');
|
||||
}
|
||||
});
|
||||
|
||||
it('can unenroll from hosted agent policy with force=true and revoke=true', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2 } =
|
||||
createClientMock();
|
||||
|
||||
const idsToUnenroll = [agentInRegularDoc._id, agentInHostedDoc._id, agentInRegularDoc2._id];
|
||||
|
||||
const unenrolledResponse = await unenrollAgents(soClient, esClient, {
|
||||
agentIds: idsToUnenroll,
|
||||
revoke: true,
|
||||
force: true,
|
||||
});
|
||||
|
||||
expect(unenrolledResponse.actionId).toBeDefined();
|
||||
|
||||
// calls ES update with correct values
|
||||
const calledWith = esClient.bulk.mock.calls[0][0];
|
||||
const ids = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.update !== undefined)
|
||||
.map((i: any) => i.update._id);
|
||||
const docs = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.doc)
|
||||
.map((i: any) => i.doc);
|
||||
expect(ids).toEqual(idsToUnenroll);
|
||||
for (const doc of docs!) {
|
||||
expect(doc).toHaveProperty('unenrolled_at');
|
||||
}
|
||||
|
||||
return { hits: { hits: [agentInRegularDoc, agentInRegularDoc2] } };
|
||||
});
|
||||
const actionResults = esClient.bulk.mock.calls[1][0];
|
||||
const resultIds = (actionResults as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.agent_id)
|
||||
.map((i: any) => i.agent_id);
|
||||
expect(resultIds).toEqual(idsToUnenroll);
|
||||
|
||||
const idsToUnenroll = [agentInRegularDoc._id, agentInRegularDoc2._id];
|
||||
await unenrollAgents(soClient, esClient, {
|
||||
agentIds: idsToUnenroll,
|
||||
revoke: true,
|
||||
const action = esClient.create.mock.calls[0][0] as any;
|
||||
expect(action.body.type).toEqual('FORCE_UNENROLL');
|
||||
});
|
||||
|
||||
// agent and force unenroll results updated, no other action results
|
||||
expect(esClient.bulk.mock.calls.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('cannot unenroll from a hosted agent policy with revoke=true', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2 } =
|
||||
createClientMock();
|
||||
|
||||
const idsToUnenroll = [agentInRegularDoc._id, agentInHostedDoc._id, agentInRegularDoc2._id];
|
||||
|
||||
const unenrolledResponse = await unenrollAgents(soClient, esClient, {
|
||||
agentIds: idsToUnenroll,
|
||||
revoke: true,
|
||||
describe('invalidateAPIKeysForAgents', () => {
|
||||
beforeEach(() => {
|
||||
mockedInvalidateAPIKeys.mockReset();
|
||||
});
|
||||
expect(unenrolledResponse.actionId).toBeDefined();
|
||||
it('revoke all the agents API keys', async () => {
|
||||
await invalidateAPIKeysForAgents([
|
||||
{
|
||||
id: 'agent1',
|
||||
default_api_key_id: 'defaultApiKey1',
|
||||
access_api_key_id: 'accessApiKey1',
|
||||
default_api_key_history: [
|
||||
{
|
||||
id: 'defaultApiKeyHistory1',
|
||||
},
|
||||
{
|
||||
id: 'defaultApiKeyHistory2',
|
||||
},
|
||||
],
|
||||
outputs: {
|
||||
output1: {
|
||||
api_key_id: 'outputApiKey1',
|
||||
to_retire_api_key_ids: [{ id: 'outputApiKeyRetire1' }, { id: 'outputApiKeyRetire2' }],
|
||||
},
|
||||
output2: {
|
||||
api_key_id: 'outputApiKey2',
|
||||
},
|
||||
output3: {
|
||||
api_key_id: 'outputApiKey3',
|
||||
to_retire_api_key_ids: [
|
||||
// Somes Fleet Server agents don't have an id here (probably a bug)
|
||||
{ retired_at: 'foo' },
|
||||
],
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
]);
|
||||
|
||||
// calls ES update with correct values
|
||||
const onlyRegular = [agentInRegularDoc._id, agentInRegularDoc2._id];
|
||||
const calledWith = esClient.bulk.mock.calls[0][0];
|
||||
const ids = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.update !== undefined)
|
||||
.map((i: any) => i.update._id);
|
||||
const docs = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.doc)
|
||||
.map((i: any) => i.doc);
|
||||
expect(ids).toEqual(onlyRegular);
|
||||
for (const doc of docs!) {
|
||||
expect(doc).toHaveProperty('unenrolled_at');
|
||||
}
|
||||
|
||||
const errorResults = esClient.bulk.mock.calls[2][0];
|
||||
const errorIds = (errorResults as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.agent_id)
|
||||
.map((i: any) => i.agent_id);
|
||||
expect(errorIds).toEqual([agentInHostedDoc._id]);
|
||||
|
||||
const actionResults = esClient.bulk.mock.calls[1][0];
|
||||
const resultIds = (actionResults as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.agent_id)
|
||||
.map((i: any) => i.agent_id);
|
||||
expect(resultIds).toEqual(onlyRegular);
|
||||
|
||||
const action = esClient.create.mock.calls[0][0] as any;
|
||||
expect(action.body.type).toEqual('FORCE_UNENROLL');
|
||||
expect(mockedInvalidateAPIKeys).toBeCalledTimes(1);
|
||||
expect(mockedInvalidateAPIKeys).toBeCalledWith([
|
||||
'accessApiKey1',
|
||||
'defaultApiKey1',
|
||||
'defaultApiKeyHistory1',
|
||||
'defaultApiKeyHistory2',
|
||||
'outputApiKey1',
|
||||
'outputApiKeyRetire1',
|
||||
'outputApiKeyRetire2',
|
||||
'outputApiKey2',
|
||||
'outputApiKey3',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('can unenroll from hosted agent policy with force=true', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2 } =
|
||||
createClientMock();
|
||||
const idsToUnenroll = [agentInRegularDoc._id, agentInHostedDoc._id, agentInRegularDoc2._id];
|
||||
await unenrollAgents(soClient, esClient, { agentIds: idsToUnenroll, force: true });
|
||||
|
||||
// calls ES update with correct values
|
||||
const calledWith = esClient.bulk.mock.calls[0][0];
|
||||
const ids = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.update !== undefined)
|
||||
.map((i: any) => i.update._id);
|
||||
const docs = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.doc)
|
||||
.map((i: any) => i.doc);
|
||||
expect(ids).toEqual(idsToUnenroll);
|
||||
for (const doc of docs!) {
|
||||
expect(doc).toHaveProperty('unenrollment_started_at');
|
||||
}
|
||||
});
|
||||
|
||||
it('can unenroll from hosted agent policy with force=true and revoke=true', async () => {
|
||||
const { soClient, esClient, agentInHostedDoc, agentInRegularDoc, agentInRegularDoc2 } =
|
||||
createClientMock();
|
||||
|
||||
const idsToUnenroll = [agentInRegularDoc._id, agentInHostedDoc._id, agentInRegularDoc2._id];
|
||||
|
||||
const unenrolledResponse = await unenrollAgents(soClient, esClient, {
|
||||
agentIds: idsToUnenroll,
|
||||
revoke: true,
|
||||
force: true,
|
||||
describe('isAgentUnenrolled', () => {
|
||||
it('should return true if revoke and unenrolled_at set', () => {
|
||||
expect(isAgentUnenrolled({ unenrolled_at: '2022-09-21' } as Agent, true)).toBe(true);
|
||||
});
|
||||
|
||||
expect(unenrolledResponse.actionId).toBeDefined();
|
||||
it('should return false if revoke and unenrolled_at null', () => {
|
||||
expect(
|
||||
isAgentUnenrolled(
|
||||
{ unenrolled_at: null, unenrollment_started_at: '2022-09-21' } as any,
|
||||
true
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
// calls ES update with correct values
|
||||
const calledWith = esClient.bulk.mock.calls[0][0];
|
||||
const ids = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.update !== undefined)
|
||||
.map((i: any) => i.update._id);
|
||||
const docs = (calledWith as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.doc)
|
||||
.map((i: any) => i.doc);
|
||||
expect(ids).toEqual(idsToUnenroll);
|
||||
for (const doc of docs!) {
|
||||
expect(doc).toHaveProperty('unenrolled_at');
|
||||
}
|
||||
it('should return true if unenrolled_at set', () => {
|
||||
expect(isAgentUnenrolled({ unenrolled_at: '2022-09-21' } as Agent)).toBe(true);
|
||||
});
|
||||
|
||||
const actionResults = esClient.bulk.mock.calls[1][0];
|
||||
const resultIds = (actionResults as estypes.BulkRequest)?.body
|
||||
?.filter((i: any) => i.agent_id)
|
||||
.map((i: any) => i.agent_id);
|
||||
expect(resultIds).toEqual(idsToUnenroll);
|
||||
it('should return true if unenrollment_started_at set and unenrolled_at null', () => {
|
||||
expect(
|
||||
isAgentUnenrolled({ unenrolled_at: null, unenrollment_started_at: '2022-09-21' } as any)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
const action = esClient.create.mock.calls[0][0] as any;
|
||||
expect(action.body.type).toEqual('FORCE_UNENROLL');
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalidateAPIKeysForAgents', () => {
|
||||
beforeEach(() => {
|
||||
mockedInvalidateAPIKeys.mockReset();
|
||||
});
|
||||
it('revoke all the agents API keys', async () => {
|
||||
await invalidateAPIKeysForAgents([
|
||||
{
|
||||
id: 'agent1',
|
||||
default_api_key_id: 'defaultApiKey1',
|
||||
access_api_key_id: 'accessApiKey1',
|
||||
default_api_key_history: [
|
||||
{
|
||||
id: 'defaultApiKeyHistory1',
|
||||
},
|
||||
{
|
||||
id: 'defaultApiKeyHistory2',
|
||||
},
|
||||
],
|
||||
outputs: {
|
||||
output1: {
|
||||
api_key_id: 'outputApiKey1',
|
||||
to_retire_api_key_ids: [{ id: 'outputApiKeyRetire1' }, { id: 'outputApiKeyRetire2' }],
|
||||
},
|
||||
output2: {
|
||||
api_key_id: 'outputApiKey2',
|
||||
},
|
||||
output3: {
|
||||
api_key_id: 'outputApiKey3',
|
||||
to_retire_api_key_ids: [
|
||||
// Somes Fleet Server agents don't have an id here (probably a bug)
|
||||
{ retired_at: 'foo' },
|
||||
],
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
]);
|
||||
|
||||
expect(mockedInvalidateAPIKeys).toBeCalledTimes(1);
|
||||
expect(mockedInvalidateAPIKeys).toBeCalledWith([
|
||||
'accessApiKey1',
|
||||
'defaultApiKey1',
|
||||
'defaultApiKeyHistory1',
|
||||
'defaultApiKeyHistory2',
|
||||
'outputApiKey1',
|
||||
'outputApiKeyRetire1',
|
||||
'outputApiKeyRetire2',
|
||||
'outputApiKey2',
|
||||
'outputApiKey3',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAgentUnenrolled', () => {
|
||||
it('should return true if revoke and unenrolled_at set', () => {
|
||||
expect(isAgentUnenrolled({ unenrolled_at: '2022-09-21' } as Agent, true)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if revoke and unenrolled_at null', () => {
|
||||
expect(
|
||||
isAgentUnenrolled({ unenrolled_at: null, unenrollment_started_at: '2022-09-21' } as any, true)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if unenrolled_at set', () => {
|
||||
expect(isAgentUnenrolled({ unenrolled_at: '2022-09-21' } as Agent)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if unenrollment_started_at set and unenrolled_at null', () => {
|
||||
expect(
|
||||
isAgentUnenrolled({ unenrolled_at: null, unenrollment_started_at: '2022-09-21' } as any)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if unenrollment_started_at null and unenrolled_at null', () => {
|
||||
expect(isAgentUnenrolled({ unenrolled_at: null, unenrollment_started_at: null } as any)).toBe(
|
||||
false
|
||||
);
|
||||
it('should return false if unenrollment_started_at null and unenrolled_at null', () => {
|
||||
expect(isAgentUnenrolled({ unenrolled_at: null, unenrollment_started_at: null } as any)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,6 +24,7 @@ jest.mock('../app_context', () => {
|
|||
},
|
||||
};
|
||||
});
|
||||
jest.mock('../audit_logging');
|
||||
|
||||
jest.mock('../agent_policy', () => {
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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 { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import { ENROLLMENT_API_KEYS_INDEX } from '../../constants';
|
||||
|
||||
import { agentPolicyService } from '../agent_policy';
|
||||
import { auditLoggingService } from '../audit_logging';
|
||||
import { appContextService } from '../app_context';
|
||||
|
||||
import { deleteEnrollmentApiKey, generateEnrollmentAPIKey } from './enrollment_api_key';
|
||||
|
||||
jest.mock('../audit_logging');
|
||||
jest.mock('../agent_policy');
|
||||
jest.mock('../app_context');
|
||||
|
||||
jest.mock('uuid', () => {
|
||||
return {
|
||||
v4: () => 'mock-uuid',
|
||||
};
|
||||
});
|
||||
|
||||
const mockedAgentPolicyService = agentPolicyService as jest.Mocked<typeof agentPolicyService>;
|
||||
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
|
||||
const mockedAppContextService = appContextService as jest.Mocked<typeof appContextService>;
|
||||
|
||||
describe('enrollment api keys', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('generateEnrollmentAPIKey', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
|
||||
esClient.create.mockResolvedValue({
|
||||
_id: 'test-enrollment-api-key-id',
|
||||
} as any);
|
||||
|
||||
esClient.security.createApiKey.mockResolvedValue({
|
||||
api_key: 'test-api-key-value',
|
||||
id: 'test-api-key-id',
|
||||
} as any);
|
||||
|
||||
mockedAgentPolicyService.get.mockResolvedValue({
|
||||
id: 'test-agent-policy',
|
||||
} as any);
|
||||
|
||||
await generateEnrollmentAPIKey(soClient, esClient, {
|
||||
name: 'test-api-key',
|
||||
expiration: '7d',
|
||||
agentPolicyId: 'test-agent-policy',
|
||||
forceRecreate: true,
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomAuditLog).toHaveBeenCalledWith({
|
||||
message:
|
||||
'User creating enrollment API key [name=test-api-key (mock-uuid)] [policy_id=test-agent-policy]',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteEnrollmentApiKey', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
|
||||
esClient.update.mockResolvedValue({} as any);
|
||||
|
||||
esClient.get.mockResolvedValue({
|
||||
_id: 'test-id',
|
||||
_index: ENROLLMENT_API_KEYS_INDEX,
|
||||
_source: {
|
||||
active: true,
|
||||
created_at: new Date().toISOString(),
|
||||
api_key_id: 'test-enrollment-api-key-id',
|
||||
},
|
||||
found: true,
|
||||
});
|
||||
|
||||
mockedAppContextService.getSecurity.mockReturnValue({
|
||||
authc: {
|
||||
apiKeys: {
|
||||
invalidateAsInternalUser: jest.fn().mockResolvedValue({}),
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
await deleteEnrollmentApiKey(esClient, 'test-enrollment-api-key-id');
|
||||
|
||||
expect(auditLoggingService.writeCustomAuditLog).toHaveBeenCalledWith({
|
||||
message:
|
||||
'User deleting enrollment API key [id=test-id] [api_key_id=test-enrollment-api-key-id]',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -21,6 +21,8 @@ import { ENROLLMENT_API_KEYS_INDEX } from '../../constants';
|
|||
import { agentPolicyService } from '../agent_policy';
|
||||
import { escapeSearchQueryPhrase } from '../saved_object';
|
||||
|
||||
import { auditLoggingService } from '../audit_logging';
|
||||
|
||||
import { invalidateAPIKeys } from './security';
|
||||
|
||||
const uuidRegex =
|
||||
|
@ -106,6 +108,10 @@ export async function deleteEnrollmentApiKey(
|
|||
) {
|
||||
const enrollmentApiKey = await getEnrollmentAPIKey(esClient, id);
|
||||
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `User deleting enrollment API key [id=${enrollmentApiKey.id}] [api_key_id=${enrollmentApiKey.api_key_id}]`,
|
||||
});
|
||||
|
||||
await invalidateAPIKeys([enrollmentApiKey.api_key_id]);
|
||||
|
||||
if (forceDelete) {
|
||||
|
@ -208,6 +214,10 @@ export async function generateEnrollmentAPIKey(
|
|||
|
||||
const name = providedKeyName ? `${providedKeyName} (${id})` : id;
|
||||
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `User creating enrollment API key [name=${name}] [policy_id=${agentPolicyId}]`,
|
||||
});
|
||||
|
||||
const key = await esClient.security
|
||||
.createApiKey({
|
||||
body: {
|
||||
|
|
|
@ -120,6 +120,10 @@ class AppContextService {
|
|||
return this.securityStart!;
|
||||
}
|
||||
|
||||
public getSecuritySetup() {
|
||||
return this.securitySetup!;
|
||||
}
|
||||
|
||||
public getSecurityLicense() {
|
||||
return this.securitySetup!.license;
|
||||
}
|
||||
|
|
77
x-pack/plugins/fleet/server/services/audit_logging.ts
Normal file
77
x-pack/plugins/fleet/server/services/audit_logging.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 { AuditLogger } from '@kbn/security-plugin/server';
|
||||
|
||||
import { appContextService } from './app_context';
|
||||
import { getRequestStore } from './request_store';
|
||||
|
||||
class AuditLoggingService {
|
||||
/**
|
||||
* Write a custom audit log record. If a current request is available, the log will include
|
||||
* user/session data. If not, an unscoped audit logger will be used.
|
||||
*/
|
||||
public writeCustomAuditLog(...args: Parameters<AuditLogger['log']>) {
|
||||
const securitySetup = appContextService.getSecuritySetup();
|
||||
let auditLogger: AuditLogger | undefined;
|
||||
|
||||
const request = getRequestStore().getStore();
|
||||
|
||||
if (request) {
|
||||
auditLogger = securitySetup.audit.asScoped(request);
|
||||
} else {
|
||||
auditLogger = securitySetup.audit.withoutRequest;
|
||||
}
|
||||
|
||||
auditLogger.log(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for writing saved object related audit logs. Since Fleet
|
||||
* uses an internal SO client to support its custom RBAC model around Fleet/Integrations
|
||||
* permissions, we need to implement our own audit logging for saved objects that use the
|
||||
* internal client. This helper reduces the boilerplate around audit logging in those cases.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* auditLoggingService.writeCustomSoAuditLog({
|
||||
* action: 'find',
|
||||
* id: 'some-id-123',
|
||||
* savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
public writeCustomSoAuditLog({
|
||||
action,
|
||||
id,
|
||||
savedObjectType,
|
||||
}: {
|
||||
action: 'find' | 'get' | 'create' | 'update' | 'delete';
|
||||
id: string;
|
||||
savedObjectType: string;
|
||||
}) {
|
||||
this.writeCustomAuditLog({
|
||||
message: `User ${
|
||||
action === 'find' || action === 'get' ? 'has accessed' : 'is accessing'
|
||||
} ${savedObjectType} [id=${id}]`,
|
||||
event: {
|
||||
action: `saved_object_${action}`,
|
||||
category: ['database'],
|
||||
outcome: 'unknown',
|
||||
type: ['access'],
|
||||
},
|
||||
kibana: {
|
||||
saved_object: {
|
||||
id,
|
||||
type: savedObjectType,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const auditLoggingService = new AuditLoggingService();
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import { securityMock } from '@kbn/security-plugin/server/mocks';
|
||||
|
||||
import type { DownloadSourceAttributes } from '../types';
|
||||
|
||||
import { DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE } from '../constants';
|
||||
|
@ -19,6 +21,10 @@ jest.mock('./app_context');
|
|||
jest.mock('./agent_policy');
|
||||
|
||||
const mockedAppContextService = appContextService as jest.Mocked<typeof appContextService>;
|
||||
mockedAppContextService.getSecuritySetup.mockImplementation(() => ({
|
||||
...securityMock.createSetup(),
|
||||
}));
|
||||
|
||||
const mockedAgentPolicyService = agentPolicyService as jest.Mocked<typeof agentPolicyService>;
|
||||
|
||||
function mockDownloadSourceSO(id: string, attributes: any = {}) {
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import type { Logger } from '@kbn/core/server';
|
||||
|
||||
import { securityMock } from '@kbn/security-plugin/server/mocks';
|
||||
|
||||
import { appContextService } from '../../../app_context';
|
||||
|
||||
import { buildDefaultSettings } from './default_settings';
|
||||
|
@ -15,6 +17,10 @@ import { buildDefaultSettings } from './default_settings';
|
|||
jest.mock('../../../app_context');
|
||||
|
||||
const mockedAppContextService = appContextService as jest.Mocked<typeof appContextService>;
|
||||
mockedAppContextService.getSecuritySetup.mockImplementation(() => ({
|
||||
...securityMock.createSetup(),
|
||||
}));
|
||||
|
||||
let mockedLogger: jest.Mocked<Logger>;
|
||||
describe('buildDefaultSettings', () => {
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -48,6 +48,8 @@ import { saveArchiveEntries } from '../archive/storage';
|
|||
import { ConcurrentInstallOperationError } from '../../../errors';
|
||||
import { appContextService, packagePolicyService } from '../..';
|
||||
|
||||
import { auditLoggingService } from '../../audit_logging';
|
||||
|
||||
import {
|
||||
createInstallation,
|
||||
restartInstallation,
|
||||
|
@ -289,6 +291,12 @@ export async function _installPackage({
|
|||
})
|
||||
);
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'update',
|
||||
id: pkgName,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
const updatedPackage = await withPackageSpan('Update install status', () =>
|
||||
savedObjectsClient.update<Installation>(PACKAGES_SAVED_OBJECT_TYPE, pkgName, {
|
||||
version: pkgVersion,
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
jest.mock('../registry');
|
||||
|
||||
import type { SavedObjectsClientContract, SavedObjectsFindResult } from '@kbn/core/server';
|
||||
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
|
@ -15,25 +13,39 @@ import { savedObjectsClientMock } from '@kbn/core/server/mocks';
|
|||
import { PACKAGES_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../common';
|
||||
import type { PackagePolicySOAttributes, RegistryPackage } from '../../../../common/types';
|
||||
|
||||
import * as Registry from '../registry';
|
||||
|
||||
import { createAppContextStartContractMock } from '../../../mocks';
|
||||
import { appContextService } from '../../app_context';
|
||||
|
||||
import { PackageNotFoundError } from '../../../errors';
|
||||
|
||||
import { getSettings } from '../../settings';
|
||||
import { auditLoggingService } from '../../audit_logging';
|
||||
|
||||
import * as Registry from '../registry';
|
||||
|
||||
import { getPackageInfo, getPackages, getPackageUsageStats } from './get';
|
||||
|
||||
jest.mock('../registry');
|
||||
jest.mock('../../settings');
|
||||
jest.mock('../../audit_logging');
|
||||
|
||||
const MockRegistry = jest.mocked(Registry);
|
||||
|
||||
jest.mock('../../settings');
|
||||
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
|
||||
|
||||
const mockGetSettings = getSettings as jest.Mock;
|
||||
mockGetSettings.mockResolvedValue({ prerelease_integrations_enabled: true });
|
||||
|
||||
describe('When using EPM `get` services', () => {
|
||||
beforeEach(() => {
|
||||
const mockContract = createAppContextStartContractMock();
|
||||
appContextService.start(mockContract);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
appContextService.stop();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('and invoking getPackageUsageStats()', () => {
|
||||
let soClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
|
||||
|
@ -188,9 +200,6 @@ describe('When using EPM `get` services', () => {
|
|||
|
||||
describe('getPackages', () => {
|
||||
beforeEach(() => {
|
||||
const mockContract = createAppContextStartContractMock();
|
||||
appContextService.start(mockContract);
|
||||
jest.clearAllMocks();
|
||||
MockRegistry.fetchList.mockResolvedValue([
|
||||
{
|
||||
name: 'nginx',
|
||||
|
@ -291,6 +300,45 @@ owner: elastic`,
|
|||
{ id: 'nginx', name: 'nginx', title: 'Nginx', version: '1.0.0' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
|
||||
soClient.find.mockResolvedValue({
|
||||
saved_objects: [
|
||||
{
|
||||
id: 'elasticsearch',
|
||||
attributes: {
|
||||
name: 'elasticsearch',
|
||||
version: '0.0.1',
|
||||
install_source: 'upload',
|
||||
install_version: '0.0.1',
|
||||
},
|
||||
score: 0,
|
||||
type: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
soClient.get.mockResolvedValue({
|
||||
id: 'elasticsearch',
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
await getPackages({ savedObjectsClient: soClient });
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'get',
|
||||
id: 'elasticsearch',
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPackageInfo', () => {
|
||||
|
|
|
@ -41,6 +41,8 @@ import { getEsPackage } from '../archive/storage';
|
|||
import { getArchivePackage } from '../archive';
|
||||
import { normalizeKuery } from '../../saved_object';
|
||||
|
||||
import { auditLoggingService } from '../../audit_logging';
|
||||
|
||||
import { createInstallableFrom } from '.';
|
||||
|
||||
export type { SearchParams } from '../registry';
|
||||
|
@ -114,6 +116,14 @@ export async function getPackages(
|
|||
.concat(uploadedPackagesNotInRegistry as Installable<any>)
|
||||
.sort(sortByName);
|
||||
|
||||
for (const pkg of packageList) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'get',
|
||||
id: pkg.id,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
if (!excludeInstallStatus) {
|
||||
return packageList;
|
||||
}
|
||||
|
@ -154,18 +164,39 @@ export async function getLimitedPackages(options: {
|
|||
});
|
||||
})
|
||||
);
|
||||
return installedPackagesInfo.filter(isPackageLimited).map((pkgInfo) => pkgInfo.name);
|
||||
|
||||
const packages = installedPackagesInfo.filter(isPackageLimited).map((pkgInfo) => pkgInfo.name);
|
||||
|
||||
for (const pkg of installedPackages) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'find',
|
||||
id: pkg.id,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
return packages;
|
||||
}
|
||||
|
||||
export async function getPackageSavedObjects(
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
options?: Omit<SavedObjectsFindOptions, 'type'>
|
||||
) {
|
||||
return savedObjectsClient.find<Installation>({
|
||||
const result = await savedObjectsClient.find<Installation>({
|
||||
...(options || {}),
|
||||
type: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
});
|
||||
|
||||
for (const savedObject of result.saved_objects) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'find',
|
||||
id: savedObject.id,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const getInstallations = getPackageSavedObjects;
|
||||
|
@ -267,6 +298,14 @@ export const getPackageUsageStats = async ({
|
|||
filter,
|
||||
});
|
||||
|
||||
for (const packagePolicy of packagePolicies.saved_objects) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'find',
|
||||
id: packagePolicy.id,
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
for (let index = 0, total = packagePolicies.saved_objects.length; index < total; index++) {
|
||||
agentPolicyCount.add(packagePolicies.saved_objects[index].attributes.policy_id);
|
||||
}
|
||||
|
@ -375,10 +414,24 @@ export async function getInstallationObject(options: {
|
|||
logger?: Logger;
|
||||
}) {
|
||||
const { savedObjectsClient, pkgName, logger } = options;
|
||||
return savedObjectsClient.get<Installation>(PACKAGES_SAVED_OBJECT_TYPE, pkgName).catch((e) => {
|
||||
logger?.error(e);
|
||||
return undefined;
|
||||
const installation = await savedObjectsClient
|
||||
.get<Installation>(PACKAGES_SAVED_OBJECT_TYPE, pkgName)
|
||||
.catch((e) => {
|
||||
logger?.error(e);
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (!installation) {
|
||||
return;
|
||||
}
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'find',
|
||||
id: installation.id,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
return installation;
|
||||
}
|
||||
|
||||
export async function getInstallationObjects(options: {
|
||||
|
@ -390,7 +443,17 @@ export async function getInstallationObjects(options: {
|
|||
pkgNames.map((pkgName) => ({ id: pkgName, type: PACKAGES_SAVED_OBJECT_TYPE }))
|
||||
);
|
||||
|
||||
return res.saved_objects.filter((so) => so?.attributes);
|
||||
const installations = res.saved_objects.filter((so) => so?.attributes);
|
||||
|
||||
for (const installation of installations) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'find',
|
||||
id: installation.id,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
return installations;
|
||||
}
|
||||
|
||||
export async function getInstallation(options: {
|
||||
|
|
|
@ -6,22 +6,24 @@
|
|||
*/
|
||||
|
||||
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
|
||||
import type { InstallablePackage } from '../../../../common';
|
||||
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../common';
|
||||
|
||||
import { sendTelemetryEvents } from '../../upgrade_sender';
|
||||
import { licenseService } from '../../license';
|
||||
import { auditLoggingService } from '../../audit_logging';
|
||||
|
||||
import * as Registry from '../registry';
|
||||
|
||||
import { sendTelemetryEvents } from '../../upgrade_sender';
|
||||
|
||||
import { licenseService } from '../../license';
|
||||
|
||||
import { installPackage } from './install';
|
||||
import { createInstallation, installPackage } from './install';
|
||||
import * as install from './_install_package';
|
||||
import * as obj from '.';
|
||||
import { getBundledPackages } from './bundled_packages';
|
||||
|
||||
import * as obj from '.';
|
||||
|
||||
jest.mock('../../app_context', () => {
|
||||
return {
|
||||
appContextService: {
|
||||
|
@ -66,8 +68,59 @@ jest.mock('../archive', () => {
|
|||
deleteVerificationResult: jest.fn(),
|
||||
};
|
||||
});
|
||||
jest.mock('../../audit_logging');
|
||||
|
||||
const mockGetBundledPackages = getBundledPackages as jest.MockedFunction<typeof getBundledPackages>;
|
||||
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
|
||||
|
||||
describe('createInstallation', () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
|
||||
const packageInfo: InstallablePackage = {
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
format_version: '1.0.0',
|
||||
title: 'Test Package',
|
||||
description: 'A package for testing',
|
||||
owner: {
|
||||
github: 'elastic',
|
||||
},
|
||||
};
|
||||
|
||||
describe('installSource: registry', () => {
|
||||
it('should call audit logger', async () => {
|
||||
await createInstallation({
|
||||
savedObjectsClient: soClient,
|
||||
packageInfo,
|
||||
installSource: 'registry',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'create',
|
||||
id: 'test-package',
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('installSource: upload', () => {
|
||||
it('should call audit logger', async () => {
|
||||
await createInstallation({
|
||||
savedObjectsClient: soClient,
|
||||
packageInfo,
|
||||
installSource: 'upload',
|
||||
spaceId: DEFAULT_SPACE_ID,
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'create',
|
||||
id: 'test-package',
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('install', () => {
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -68,6 +68,8 @@ import { prepareToInstallPipelines } from '../elasticsearch/ingest_pipeline';
|
|||
|
||||
import { prepareToInstallTemplates } from '../elasticsearch/template/install';
|
||||
|
||||
import { auditLoggingService } from '../../audit_logging';
|
||||
|
||||
import { formatVerificationResultForSO } from './package_verification';
|
||||
|
||||
import { getInstallation, getInstallationObject } from '.';
|
||||
|
@ -670,6 +672,12 @@ export const updateVersion = async (
|
|||
pkgName: string,
|
||||
pkgVersion: string
|
||||
) => {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'update',
|
||||
id: pkgName,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, {
|
||||
version: pkgVersion,
|
||||
});
|
||||
|
@ -684,6 +692,12 @@ export const updateInstallStatus = async ({
|
|||
pkgName: string;
|
||||
status: EpmPackageInstallStatus;
|
||||
}) => {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'update',
|
||||
id: pkgName,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, {
|
||||
install_status: status,
|
||||
});
|
||||
|
@ -713,6 +727,12 @@ export async function restartInstallation(options: {
|
|||
};
|
||||
}
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'update',
|
||||
id: pkgName,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, savedObjectUpdate);
|
||||
}
|
||||
|
||||
|
@ -757,6 +777,12 @@ export async function createInstallation(options: {
|
|||
savedObject = { ...savedObject, ...formatVerificationResultForSO(verificationResult) };
|
||||
}
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'create',
|
||||
id: pkgName,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
const created = await savedObjectsClient.create<Installation>(
|
||||
PACKAGES_SAVED_OBJECT_TYPE,
|
||||
savedObject,
|
||||
|
@ -771,6 +797,12 @@ export const saveKibanaAssetsRefs = async (
|
|||
pkgName: string,
|
||||
kibanaAssets: Record<KibanaAssetType, ArchiveAsset[]>
|
||||
) => {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'update',
|
||||
id: pkgName,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
const assetRefs = Object.values(kibanaAssets).flat().map(toAssetReference);
|
||||
// Because Kibana assets are installed in parallel with ES assets with refresh: false, we almost always run into an
|
||||
// issue that causes a conflict error due to this issue: https://github.com/elastic/kibana/issues/126240. This is safe
|
||||
|
@ -829,6 +861,12 @@ export const updateEsAssetReferences = async (
|
|||
({ type, id }) => `${type}-${id}`
|
||||
);
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'update',
|
||||
id: pkgName,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
const {
|
||||
attributes: { installed_es: updatedAssets },
|
||||
} =
|
||||
|
@ -864,7 +902,13 @@ export const optimisticallyAddEsAssetReferences = async (
|
|||
assetsToAdd: EsAssetReference[]
|
||||
): Promise<EsAssetReference[]> => {
|
||||
const addEsAssets = async () => {
|
||||
// TODO: Should this be replaced by a `get()` call from epm/get.ts?
|
||||
const so = await savedObjectsClient.get<Installation>(PACKAGES_SAVED_OBJECT_TYPE, pkgName);
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'get',
|
||||
id: pkgName,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
const installedEs = so.attributes.installed_es ?? [];
|
||||
|
||||
|
@ -873,6 +917,12 @@ export const optimisticallyAddEsAssetReferences = async (
|
|||
({ type, id }) => `${type}-${id}`
|
||||
);
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'update',
|
||||
id: pkgName,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
const {
|
||||
attributes: { installed_es: updatedAssets },
|
||||
} = await savedObjectsClient.update<Installation>(
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../common';
|
||||
|
||||
import { packagePolicyService } from '../..';
|
||||
import { auditLoggingService } from '../../audit_logging';
|
||||
|
||||
import { removeInstallation } from './remove';
|
||||
|
||||
|
@ -23,7 +26,9 @@ jest.mock('../..', () => {
|
|||
},
|
||||
};
|
||||
});
|
||||
jest.mock('../../audit_logging');
|
||||
|
||||
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
|
||||
const mockPackagePolicyService = packagePolicyService as jest.Mocked<typeof packagePolicyService>;
|
||||
|
||||
describe('removeInstallation', () => {
|
||||
|
@ -66,4 +71,20 @@ describe('removeInstallation', () => {
|
|||
`unable to remove package with existing package policy(s) in use by agent(s)`
|
||||
);
|
||||
});
|
||||
|
||||
it('should call audit logger', async () => {
|
||||
await removeInstallation({
|
||||
savedObjectsClient: soClientMock,
|
||||
pkgName: 'system',
|
||||
pkgVersion: '1.0.0',
|
||||
esClient: esClientMock,
|
||||
force: true,
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'delete',
|
||||
id: 'system',
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -41,6 +41,8 @@ import { deletePackageCache } from '../archive';
|
|||
import { deleteIlms } from '../elasticsearch/datastream_ilm/remove';
|
||||
import { removeArchiveEntries } from '../archive/storage';
|
||||
|
||||
import { auditLoggingService } from '../../audit_logging';
|
||||
|
||||
import { getInstallation, kibanaSavedObjectTypes } from '.';
|
||||
|
||||
export async function removeInstallation(options: {
|
||||
|
@ -85,6 +87,11 @@ export async function removeInstallation(options: {
|
|||
|
||||
// Delete the manager saved object with references to the asset objects
|
||||
// could also update with [] or some other state
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'delete',
|
||||
id: pkgName,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
await savedObjectsClient.delete(PACKAGES_SAVED_OBJECT_TYPE, pkgName);
|
||||
|
||||
// delete the index patterns if no packages are installed
|
||||
|
@ -118,6 +125,14 @@ async function deleteKibanaAssets(
|
|||
{ namespace }
|
||||
);
|
||||
|
||||
for (const { saved_object: savedObject } of resolvedObjects) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'get',
|
||||
id: savedObject.id,
|
||||
savedObjectType: savedObject.type,
|
||||
});
|
||||
}
|
||||
|
||||
const foundObjects = resolvedObjects.filter(
|
||||
({ saved_object: savedObject }) => savedObject?.error?.statusCode !== 404
|
||||
);
|
||||
|
@ -126,6 +141,14 @@ async function deleteKibanaAssets(
|
|||
// we filter these out before calling delete
|
||||
const assetsToDelete = foundObjects.map(({ saved_object: { id, type } }) => ({ id, type }));
|
||||
|
||||
for (const asset of assetsToDelete) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'delete',
|
||||
id: asset.id,
|
||||
savedObjectType: asset.type,
|
||||
});
|
||||
}
|
||||
|
||||
return savedObjectsClient.bulkDelete(assetsToDelete, { namespace });
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
|
||||
|
||||
import { createAppContextStartContractMock } from '../../../mocks';
|
||||
|
||||
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../common';
|
||||
import { appContextService } from '../../app_context';
|
||||
|
||||
import { auditLoggingService } from '../../audit_logging';
|
||||
|
||||
import { updatePackage } from './update';
|
||||
import { getPackageInfo, getInstallationObject } from './get';
|
||||
|
||||
jest.mock('./get');
|
||||
jest.mock('../../audit_logging');
|
||||
|
||||
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
|
||||
const mockGetPackageInfo = getPackageInfo as jest.MockedFunction<typeof getPackageInfo>;
|
||||
const mockGetInstallationObject = getInstallationObject as jest.MockedFunction<
|
||||
typeof getInstallationObject
|
||||
>;
|
||||
|
||||
describe('updatePackage', () => {
|
||||
let mockContract: ReturnType<typeof createAppContextStartContractMock>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContract = createAppContextStartContractMock();
|
||||
appContextService.start(mockContract);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
appContextService.stop();
|
||||
});
|
||||
|
||||
it('should call audit logger', async () => {
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
||||
mockGetPackageInfo.mockResolvedValueOnce({
|
||||
name: 'test-package',
|
||||
title: 'Test package',
|
||||
description: 'Test package',
|
||||
format_version: '1.0.0',
|
||||
version: '1.0.0',
|
||||
latestVersion: '1.0.0',
|
||||
owner: {
|
||||
github: 'elastic',
|
||||
},
|
||||
assets: {
|
||||
elasticsearch: {},
|
||||
kibana: {},
|
||||
},
|
||||
} as any);
|
||||
|
||||
mockGetInstallationObject.mockResolvedValueOnce({
|
||||
id: 'test-package',
|
||||
attributes: {} as any,
|
||||
references: [],
|
||||
type: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
await updatePackage({
|
||||
savedObjectsClient,
|
||||
pkgName: 'test-package',
|
||||
keepPoliciesUpToDate: true,
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'update',
|
||||
id: 'test-package',
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -14,6 +14,8 @@ import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants';
|
|||
import type { Installation, UpdatePackageRequestSchema } from '../../../types';
|
||||
import { FleetError } from '../../../errors';
|
||||
|
||||
import { auditLoggingService } from '../../audit_logging';
|
||||
|
||||
import { getInstallationObject, getPackageInfo } from './get';
|
||||
|
||||
export async function updatePackage(
|
||||
|
@ -30,6 +32,12 @@ export async function updatePackage(
|
|||
throw new FleetError(`package ${pkgName} is not installed`);
|
||||
}
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'update',
|
||||
id: installedPackage.id,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
await savedObjectsClient.update<Installation>(PACKAGES_SAVED_OBJECT_TYPE, installedPackage.id, {
|
||||
keep_policies_up_to_date: keepPoliciesUpToDate ?? false,
|
||||
});
|
||||
|
@ -51,6 +59,12 @@ export async function updateDatastreamExperimentalFeatures(
|
|||
features: Record<ExperimentalIndexingFeature, boolean>;
|
||||
}>
|
||||
) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'update',
|
||||
id: pkgName,
|
||||
savedObjectType: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
await savedObjectsClient.update<Installation>(
|
||||
PACKAGES_SAVED_OBJECT_TYPE,
|
||||
pkgName,
|
||||
|
|
|
@ -50,6 +50,7 @@ export { dataStreamService } from './data_streams';
|
|||
// Plugin services
|
||||
export { appContextService } from './app_context';
|
||||
export { licenseService } from './license';
|
||||
export { auditLoggingService } from './audit_logging';
|
||||
|
||||
// Artifacts services
|
||||
export * from './artifacts';
|
||||
|
|
|
@ -23,6 +23,7 @@ jest.mock('./app_context', () => {
|
|||
},
|
||||
};
|
||||
});
|
||||
jest.mock('./audit_logging');
|
||||
|
||||
describe('upgradeManagedPackagePolicies', () => {
|
||||
afterEach(() => {
|
||||
|
|
|
@ -7,16 +7,27 @@
|
|||
|
||||
import { savedObjectsClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import { securityMock } from '@kbn/security-plugin/server/mocks';
|
||||
|
||||
import type { OutputSOAttributes } from '../types';
|
||||
|
||||
import { OUTPUT_SAVED_OBJECT_TYPE } from '../constants';
|
||||
|
||||
import { outputService, outputIdToUuid } from './output';
|
||||
import { appContextService } from './app_context';
|
||||
import { agentPolicyService } from './agent_policy';
|
||||
import { auditLoggingService } from './audit_logging';
|
||||
|
||||
jest.mock('./app_context');
|
||||
jest.mock('./agent_policy');
|
||||
jest.mock('./audit_logging');
|
||||
|
||||
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
|
||||
const mockedAppContextService = appContextService as jest.Mocked<typeof appContextService>;
|
||||
mockedAppContextService.getSecuritySetup.mockImplementation(() => ({
|
||||
...securityMock.createSetup(),
|
||||
}));
|
||||
|
||||
const mockedAgentPolicyService = agentPolicyService as jest.Mocked<typeof agentPolicyService>;
|
||||
|
||||
const CLOUD_ID =
|
||||
|
@ -177,6 +188,7 @@ describe('Output Service', () => {
|
|||
mockedAgentPolicyService.removeOutputFromAll.mockReset();
|
||||
mockedAppContextService.getInternalUserSOClient.mockReset();
|
||||
mockedAppContextService.getEncryptedSavedObjectsSetup.mockReset();
|
||||
mockedAuditLoggingService.writeCustomSoAuditLog.mockReset();
|
||||
});
|
||||
describe('create', () => {
|
||||
it('work with a predefined id', async () => {
|
||||
|
@ -494,6 +506,28 @@ describe('Output Service', () => {
|
|||
{ force: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = getMockedSoClient();
|
||||
|
||||
await outputService.create(
|
||||
soClient,
|
||||
esClientMock,
|
||||
{
|
||||
is_default: false,
|
||||
is_default_monitoring: true,
|
||||
name: 'Test',
|
||||
type: 'elasticsearch',
|
||||
},
|
||||
{ id: 'output-test' }
|
||||
);
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'create',
|
||||
id: outputIdToUuid('output-test'),
|
||||
savedObjectType: OUTPUT_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
|
@ -823,6 +857,20 @@ describe('Output Service', () => {
|
|||
'Logstash output cannot be used with Fleet Server integration in fleet server policy. Please create a new ElasticSearch output.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = getMockedSoClient({ defaultOutputId: 'existing-es-output' });
|
||||
|
||||
await outputService.update(soClient, esClientMock, 'existing-es-output', {
|
||||
hosts: ['new-host:443'],
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'update',
|
||||
id: outputIdToUuid('existing-es-output'),
|
||||
savedObjectType: OUTPUT_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
|
@ -853,6 +901,17 @@ describe('Output Service', () => {
|
|||
expect(mockedAgentPolicyService.removeOutputFromAll).toBeCalled();
|
||||
expect(soClient.delete).toBeCalled();
|
||||
});
|
||||
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = getMockedSoClient();
|
||||
await outputService.delete(soClient, 'existing-es-output');
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'delete',
|
||||
id: outputIdToUuid('existing-es-output'),
|
||||
savedObjectType: OUTPUT_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
|
@ -864,6 +923,17 @@ describe('Output Service', () => {
|
|||
|
||||
expect(output.id).toEqual('output-test');
|
||||
});
|
||||
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = getMockedSoClient();
|
||||
await outputService.get(soClient, 'existing-es-output');
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'get',
|
||||
id: outputIdToUuid('existing-es-output'),
|
||||
savedObjectType: OUTPUT_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultDataOutputId', () => {
|
||||
|
|
|
@ -4,16 +4,17 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { v5 as uuidv5 } from 'uuid';
|
||||
import { omit } from 'lodash';
|
||||
import { safeLoad } from 'js-yaml';
|
||||
|
||||
import { SavedObjectsUtils } from '@kbn/core/server';
|
||||
import type {
|
||||
KibanaRequest,
|
||||
SavedObject,
|
||||
SavedObjectsClientContract,
|
||||
ElasticsearchClient,
|
||||
} from '@kbn/core/server';
|
||||
import { v5 as uuidv5 } from 'uuid';
|
||||
import { omit } from 'lodash';
|
||||
import { safeLoad } from 'js-yaml';
|
||||
|
||||
import type { NewOutput, Output, OutputSOAttributes, AgentPolicy } from '../types';
|
||||
import {
|
||||
|
@ -33,6 +34,7 @@ import {
|
|||
import { agentPolicyService } from './agent_policy';
|
||||
import { appContextService } from './app_context';
|
||||
import { escapeSearchQueryPhrase } from './saved_object';
|
||||
import { auditLoggingService } from './audit_logging';
|
||||
|
||||
type Nullable<T> = { [P in keyof T]: T[P] | null };
|
||||
|
||||
|
@ -198,19 +200,39 @@ class OutputService {
|
|||
}
|
||||
|
||||
private async _getDefaultDataOutputsSO(soClient: SavedObjectsClientContract) {
|
||||
return await this.encryptedSoClient.find<OutputSOAttributes>({
|
||||
const outputs = await this.encryptedSoClient.find<OutputSOAttributes>({
|
||||
type: OUTPUT_SAVED_OBJECT_TYPE,
|
||||
searchFields: ['is_default'],
|
||||
search: 'true',
|
||||
});
|
||||
|
||||
for (const output of outputs.saved_objects) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'get',
|
||||
id: output.id,
|
||||
savedObjectType: OUTPUT_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
return outputs;
|
||||
}
|
||||
|
||||
private async _getDefaultMonitoringOutputsSO(soClient: SavedObjectsClientContract) {
|
||||
return await this.encryptedSoClient.find<OutputSOAttributes>({
|
||||
const outputs = await this.encryptedSoClient.find<OutputSOAttributes>({
|
||||
type: OUTPUT_SAVED_OBJECT_TYPE,
|
||||
searchFields: ['is_default_monitoring'],
|
||||
search: 'true',
|
||||
});
|
||||
|
||||
for (const output of outputs.saved_objects) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'get',
|
||||
id: output.id,
|
||||
savedObjectType: OUTPUT_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
return outputs;
|
||||
}
|
||||
|
||||
public async ensureDefaultOutput(
|
||||
|
@ -360,9 +382,17 @@ class OutputService {
|
|||
}
|
||||
}
|
||||
|
||||
const id = options?.id ? outputIdToUuid(options.id) : SavedObjectsUtils.generateId();
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'create',
|
||||
id,
|
||||
savedObjectType: OUTPUT_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
const newSo = await this.encryptedSoClient.create<OutputSOAttributes>(SAVED_OBJECT_TYPE, data, {
|
||||
overwrite: options?.overwrite || options?.fromPreconfiguration,
|
||||
id: options?.id ? outputIdToUuid(options.id) : undefined,
|
||||
id,
|
||||
});
|
||||
|
||||
return outputSavedObjectToOutput(newSo);
|
||||
|
@ -400,6 +430,14 @@ class OutputService {
|
|||
sortOrder: 'desc',
|
||||
});
|
||||
|
||||
for (const output of outputs.saved_objects) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'get',
|
||||
id: output.id,
|
||||
savedObjectType: OUTPUT_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
items: outputs.saved_objects.map<Output>(outputSavedObjectToOutput),
|
||||
total: outputs.total,
|
||||
|
@ -417,6 +455,14 @@ class OutputService {
|
|||
search: escapeSearchQueryPhrase(proxyId),
|
||||
});
|
||||
|
||||
for (const output of outputs.saved_objects) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'get',
|
||||
id: output.id,
|
||||
savedObjectType: OUTPUT_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
items: outputs.saved_objects.map<Output>(outputSavedObjectToOutput),
|
||||
total: outputs.total,
|
||||
|
@ -431,6 +477,12 @@ class OutputService {
|
|||
outputIdToUuid(id)
|
||||
);
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'get',
|
||||
id: outputSO.id,
|
||||
savedObjectType: OUTPUT_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
if (outputSO.error) {
|
||||
throw new Error(outputSO.error.message);
|
||||
}
|
||||
|
@ -467,6 +519,12 @@ class OutputService {
|
|||
id
|
||||
);
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'delete',
|
||||
id: outputIdToUuid(id),
|
||||
savedObjectType: OUTPUT_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
return this.encryptedSoClient.delete(SAVED_OBJECT_TYPE, outputIdToUuid(id));
|
||||
}
|
||||
|
||||
|
@ -555,6 +613,12 @@ class OutputService {
|
|||
}
|
||||
}
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'update',
|
||||
id: outputIdToUuid(id),
|
||||
savedObjectType: OUTPUT_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
const outputSO = await this.encryptedSoClient.update<Nullable<OutputSOAttributes>>(
|
||||
SAVED_OBJECT_TYPE,
|
||||
outputIdToUuid(id),
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
|
||||
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
|
||||
import { securityMock } from '@kbn/security-plugin/server/mocks';
|
||||
|
||||
import type { NewPackagePolicy, PackagePolicy } from '../../types';
|
||||
import { appContextService } from '../app_context';
|
||||
import { updateCurrentWriteIndices } from '../epm/elasticsearch/template/template';
|
||||
import { getInstallation } from '../epm/packages';
|
||||
|
||||
|
@ -33,6 +35,11 @@ jest.mock('../epm/packages', () => {
|
|||
});
|
||||
|
||||
jest.mock('../app_context');
|
||||
const mockedAppContextService = appContextService as jest.Mocked<typeof appContextService>;
|
||||
mockedAppContextService.getSecuritySetup.mockImplementation(() => ({
|
||||
...securityMock.createSetup(),
|
||||
}));
|
||||
|
||||
jest.mock('../epm/elasticsearch/template/template');
|
||||
|
||||
const mockGetInstallation = getInstallation as jest.Mock;
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
import { produce } from 'immer';
|
||||
import type {
|
||||
KibanaRequest,
|
||||
SavedObjectsClient,
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsUpdateResponse,
|
||||
} from '@kbn/core/server';
|
||||
|
@ -23,7 +22,6 @@ import { SavedObjectsErrorHelpers } from '@kbn/core/server';
|
|||
import type {
|
||||
PackageInfo,
|
||||
PackagePolicySOAttributes,
|
||||
AgentPolicySOAttributes,
|
||||
PostPackagePolicyPostDeleteCallback,
|
||||
RegistryDataStream,
|
||||
PackagePolicyInputStream,
|
||||
|
@ -49,6 +47,8 @@ import { packageToPackagePolicy } from '../../common/services';
|
|||
|
||||
import { FleetError, PackagePolicyIneligibleForUpgradeError } from '../errors';
|
||||
|
||||
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../constants';
|
||||
|
||||
import {
|
||||
preconfigurePackageInputs,
|
||||
updatePackageInputs,
|
||||
|
@ -61,6 +61,8 @@ import { appContextService } from './app_context';
|
|||
|
||||
import { getPackageInfo } from './epm/packages';
|
||||
import { sendTelemetryEvents } from './upgrade_sender';
|
||||
import { auditLoggingService } from './audit_logging';
|
||||
import { agentPolicyService } from './agent_policy';
|
||||
|
||||
const mockedSendTelemetryEvents = sendTelemetryEvents as jest.MockedFunction<
|
||||
typeof sendTelemetryEvents
|
||||
|
@ -176,26 +178,8 @@ jest.mock('../../common/services/package_to_package_policy', () => ({
|
|||
|
||||
jest.mock('./epm/registry');
|
||||
|
||||
jest.mock('./agent_policy', () => {
|
||||
return {
|
||||
agentPolicyService: {
|
||||
get: async (soClient: SavedObjectsClient, id: string) => {
|
||||
const agentPolicySO = await soClient.get<AgentPolicySOAttributes>(
|
||||
'ingest-agent-policies',
|
||||
id
|
||||
);
|
||||
if (!agentPolicySO) {
|
||||
return null;
|
||||
}
|
||||
const agentPolicy = { id: agentPolicySO.id, ...agentPolicySO.attributes };
|
||||
agentPolicy.package_policies = [];
|
||||
return agentPolicy;
|
||||
},
|
||||
bumpRevision: () => {},
|
||||
getDefaultAgentPolicyId: () => Promise.resolve('1'),
|
||||
},
|
||||
};
|
||||
});
|
||||
jest.mock('./agent_policy');
|
||||
const mockAgentPolicyService = agentPolicyService as jest.Mocked<typeof agentPolicyService>;
|
||||
|
||||
jest.mock('./epm/packages/cleanup', () => {
|
||||
return {
|
||||
|
@ -209,9 +193,238 @@ jest.mock('./upgrade_sender', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('./audit_logging');
|
||||
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
|
||||
|
||||
type CombinedExternalCallback = PutPackagePolicyUpdateCallback | PostPackagePolicyCreateCallback;
|
||||
|
||||
const mockAgentPolicyGet = () => {
|
||||
mockAgentPolicyService.get.mockImplementation(
|
||||
(_soClient: SavedObjectsClientContract, id: string, _force = false, _errorMessage?: string) => {
|
||||
return Promise.resolve({
|
||||
id,
|
||||
name: 'Test Agent Policy',
|
||||
namespace: 'test',
|
||||
status: 'active',
|
||||
is_managed: false,
|
||||
updated_at: new Date().toISOString(),
|
||||
updated_by: 'test',
|
||||
revision: 1,
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
describe('Package policy service', () => {
|
||||
beforeEach(() => {
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
appContextService.stop();
|
||||
|
||||
// `jest.resetAllMocks` breaks a ton of tests in this file 🤷♂️
|
||||
mockAgentPolicyService.get.mockReset();
|
||||
mockedAuditLoggingService.writeCustomSoAuditLog.mockReset();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
|
||||
soClient.create.mockResolvedValueOnce({
|
||||
id: 'test-package-policy',
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
mockAgentPolicyGet();
|
||||
|
||||
await packagePolicyService.create(
|
||||
soClient,
|
||||
esClient,
|
||||
{
|
||||
name: 'Test Package Policy',
|
||||
namespace: 'test',
|
||||
enabled: true,
|
||||
policy_id: 'test',
|
||||
inputs: [],
|
||||
},
|
||||
// Skipping unique name verification just means we have to less mocking/setup
|
||||
{ id: 'test-package-policy', skipUniqueNameVerification: true }
|
||||
);
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toBeCalledWith({
|
||||
action: 'create',
|
||||
id: 'test-package-policy',
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkCreate', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
|
||||
soClient.bulkCreate.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
id: 'test-package-policy-1',
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
},
|
||||
{
|
||||
id: 'test-package-policy-2',
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
mockAgentPolicyGet();
|
||||
|
||||
await packagePolicyService.bulkCreate(soClient, esClient, [
|
||||
{
|
||||
id: 'test-package-policy-1',
|
||||
name: 'Test Package Policy 1',
|
||||
namespace: 'test',
|
||||
enabled: true,
|
||||
policy_id: 'test_agent_policy',
|
||||
inputs: [],
|
||||
},
|
||||
{
|
||||
id: 'test-package-policy-2',
|
||||
name: 'Test Package Policy 2',
|
||||
namespace: 'test',
|
||||
enabled: true,
|
||||
policy_id: 'test_agent_policy',
|
||||
inputs: [],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenNthCalledWith(1, {
|
||||
action: 'create',
|
||||
id: 'test-package-policy-1',
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenNthCalledWith(2, {
|
||||
action: 'create',
|
||||
id: 'test-package-policy-2',
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
soClient.get.mockResolvedValueOnce({
|
||||
id: 'test-package-policy',
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
await packagePolicyService.get(soClient, 'test-package-policy');
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toBeCalledWith({
|
||||
action: 'get',
|
||||
id: 'test-package-policy',
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getByIDs', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
soClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
id: 'test-package-policy-1',
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
},
|
||||
{
|
||||
id: 'test-package-policy-2',
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await packagePolicyService.getByIDs(soClient, [
|
||||
'test-package-policy-1',
|
||||
'test-package-policy-2',
|
||||
]);
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenNthCalledWith(1, {
|
||||
action: 'get',
|
||||
id: 'test-package-policy-1',
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenNthCalledWith(2, {
|
||||
action: 'get',
|
||||
id: 'test-package-policy-2',
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
soClient.find.mockResolvedValueOnce({
|
||||
total: 1,
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
saved_objects: [
|
||||
{
|
||||
id: 'test-package-policy-1',
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
score: 0,
|
||||
},
|
||||
{
|
||||
id: 'test-package-policy-2',
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
score: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await packagePolicyService.list(soClient, {
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
kuery: '',
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenNthCalledWith(1, {
|
||||
action: 'find',
|
||||
id: 'test-package-policy-1',
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenNthCalledWith(2, {
|
||||
action: 'find',
|
||||
id: 'test-package-policy-2',
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_compilePackagePolicyInputs', () => {
|
||||
it('should work with config variables from the stream', async () => {
|
||||
const inputs = await _compilePackagePolicyInputs(
|
||||
|
@ -640,12 +853,6 @@ describe('Package policy service', () => {
|
|||
});
|
||||
|
||||
describe('update', () => {
|
||||
beforeEach(() => {
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
afterEach(() => {
|
||||
appContextService.stop();
|
||||
});
|
||||
it('should fail to update on version conflict', async () => {
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
savedObjectsClient.get.mockResolvedValue({
|
||||
|
@ -1377,16 +1584,49 @@ describe('Package policy service', () => {
|
|||
|
||||
expect(result.name).toEqual('test');
|
||||
});
|
||||
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
|
||||
const mockPackagePolicy = createPackagePolicyMock();
|
||||
|
||||
const attributes = {
|
||||
...mockPackagePolicy,
|
||||
inputs: [],
|
||||
};
|
||||
|
||||
soClient.get.mockResolvedValue({
|
||||
id: 'test-package-policy',
|
||||
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
references: [],
|
||||
attributes,
|
||||
});
|
||||
|
||||
soClient.update.mockResolvedValue({
|
||||
id: 'test-package-policy',
|
||||
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
references: [],
|
||||
attributes,
|
||||
});
|
||||
|
||||
await packagePolicyService.update(soClient, esClient, 'test-package-policy', {
|
||||
...mockPackagePolicy,
|
||||
inputs: [],
|
||||
});
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'update',
|
||||
id: 'test-package-policy',
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkUpdate', () => {
|
||||
beforeEach(() => {
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
mockedSendTelemetryEvents.mockReset();
|
||||
});
|
||||
afterEach(() => {
|
||||
appContextService.stop();
|
||||
});
|
||||
|
||||
it('should throw if the user try to update input vars that are frozen', async () => {
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
|
@ -2113,10 +2353,86 @@ describe('Package policy service', () => {
|
|||
|
||||
expect(mockedSendTelemetryEvents).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
|
||||
const mockPackagePolicies = [
|
||||
{
|
||||
id: 'test-package-policy-1',
|
||||
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
{
|
||||
id: 'test-package-policy-2',
|
||||
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
attributes: {},
|
||||
references: [],
|
||||
},
|
||||
];
|
||||
|
||||
soClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [...mockPackagePolicies],
|
||||
});
|
||||
|
||||
soClient.bulkUpdate.mockResolvedValueOnce({
|
||||
saved_objects: [...mockPackagePolicies],
|
||||
});
|
||||
|
||||
await packagePolicyService.bulkUpdate(soClient, esClient, [
|
||||
{
|
||||
id: 'test-package-policy-1',
|
||||
name: 'Test Package Policy 1',
|
||||
namespace: 'test',
|
||||
enabled: true,
|
||||
policy_id: 'test-agent-policy',
|
||||
inputs: [],
|
||||
},
|
||||
{
|
||||
id: 'test-package-policy-2',
|
||||
name: 'Test Package Policy 2',
|
||||
namespace: 'test',
|
||||
enabled: true,
|
||||
policy_id: 'test-agent-policy',
|
||||
inputs: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
// TODO: Add tests
|
||||
it('should allow to delete a package policy', async () => {});
|
||||
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
|
||||
const mockPackagePolicy = {
|
||||
id: 'test-package-policy',
|
||||
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
attributes: {},
|
||||
references: [],
|
||||
};
|
||||
|
||||
soClient.get.mockResolvedValueOnce({
|
||||
...mockPackagePolicy,
|
||||
});
|
||||
|
||||
soClient.delete.mockResolvedValueOnce({
|
||||
...mockPackagePolicy,
|
||||
});
|
||||
|
||||
await packagePolicyService.delete(soClient, esClient, ['test-package-policy']);
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'delete',
|
||||
id: 'test-package-policy',
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('runPostDeleteExternalCallbacks', () => {
|
||||
|
@ -2126,7 +2442,6 @@ describe('Package policy service', () => {
|
|||
let deletedPackagePolicies: PostDeletePackagePoliciesResponse;
|
||||
|
||||
beforeEach(() => {
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
callingOrder = [];
|
||||
deletedPackagePolicies = [
|
||||
{ id: 'a', success: true },
|
||||
|
@ -2142,10 +2457,6 @@ describe('Package policy service', () => {
|
|||
appContextService.addExternalCallback('packagePolicyPostDelete', callbackTwo);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
appContextService.stop();
|
||||
});
|
||||
|
||||
it('should execute external callbacks', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
|
@ -2229,7 +2540,6 @@ describe('Package policy service', () => {
|
|||
let packagePolicies: DeletePackagePoliciesResponse;
|
||||
|
||||
beforeEach(() => {
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
callingOrder = [];
|
||||
packagePolicies = [{ id: 'a' }, { id: 'a' }] as DeletePackagePoliciesResponse;
|
||||
callbackOne = jest.fn(async (deletedPolicies, soClient, esClient) => {
|
||||
|
@ -2242,10 +2552,6 @@ describe('Package policy service', () => {
|
|||
appContextService.addExternalCallback('packagePolicyDelete', callbackTwo);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
appContextService.stop();
|
||||
});
|
||||
|
||||
it('should execute external callbacks', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
|
@ -2361,11 +2667,9 @@ describe('Package policy service', () => {
|
|||
beforeEach(() => {
|
||||
context = xpackMocks.createRequestHandlerContext();
|
||||
request = httpServerMock.createKibanaRequest();
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
appContextService.stop();
|
||||
jest.clearAllMocks();
|
||||
callbackCallingOrder.length = 0;
|
||||
});
|
||||
|
@ -2550,11 +2854,9 @@ describe('Package policy service', () => {
|
|||
beforeEach(() => {
|
||||
context = xpackMocks.createRequestHandlerContext();
|
||||
request = httpServerMock.createKibanaRequest();
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
appContextService.stop();
|
||||
jest.clearAllMocks();
|
||||
callbackCallingOrder.length = 0;
|
||||
});
|
||||
|
@ -4521,10 +4823,6 @@ describe('getUpgradeDryRunDiff', () => {
|
|||
let savedObjectsClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
beforeEach(() => {
|
||||
savedObjectsClient = savedObjectsClientMock.create();
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
afterEach(() => {
|
||||
appContextService.stop();
|
||||
});
|
||||
it('should return no errors if there is no conflict to upgrade', async () => {
|
||||
const res = await packagePolicyService.getUpgradeDryRunDiff(
|
||||
|
@ -4650,10 +4948,6 @@ describe('_applyIndexPrivileges()', () => {
|
|||
};
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
appContextService.start(createAppContextStartContractMock());
|
||||
});
|
||||
|
||||
it('should do nothing if packageStream has no privileges', () => {
|
||||
const packageStream = createPackageStream();
|
||||
const inputStream = createInputStream();
|
||||
|
|
|
@ -17,6 +17,7 @@ import type {
|
|||
Logger,
|
||||
RequestHandlerContext,
|
||||
} from '@kbn/core/server';
|
||||
import { SavedObjectsUtils } from '@kbn/core/server';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { safeLoad } from 'js-yaml';
|
||||
|
||||
|
@ -104,6 +105,7 @@ import { handleExperimentalDatastreamFeatureOptIn } from './package_policies';
|
|||
import { updateDatastreamExperimentalFeatures } from './epm/packages/update';
|
||||
import type { PackagePolicyClient, PackagePolicyService } from './package_policy_service';
|
||||
import { installAssetsForInputPackagePolicy } from './epm/packages/install';
|
||||
import { auditLoggingService } from './audit_logging';
|
||||
|
||||
export type InputsOverride = Partial<NewPackagePolicyInput> & {
|
||||
vars?: Array<NewPackagePolicyInput['vars'] & { name: string }>;
|
||||
|
@ -125,7 +127,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
soClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient,
|
||||
packagePolicy: NewPackagePolicy,
|
||||
options?: {
|
||||
options: {
|
||||
spaceId?: string;
|
||||
id?: string;
|
||||
user?: AuthenticatedUser;
|
||||
|
@ -135,10 +137,21 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
skipUniqueNameVerification?: boolean;
|
||||
overwrite?: boolean;
|
||||
packageInfo?: PackageInfo;
|
||||
},
|
||||
} = {},
|
||||
context?: RequestHandlerContext,
|
||||
request?: KibanaRequest
|
||||
): Promise<PackagePolicy> {
|
||||
// Ensure an ID is provided, so we can include it in the audit logs below
|
||||
if (!options.id) {
|
||||
options.id = SavedObjectsUtils.generateId();
|
||||
}
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'create',
|
||||
id: options.id,
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
const logger = appContextService.getLogger();
|
||||
|
||||
const enrichedPackagePolicy = await packagePolicyService.runExternalCallbacks(
|
||||
|
@ -260,6 +273,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
}
|
||||
|
||||
const createdPackagePolicy = { id: newSo.id, version: newSo.version, ...newSo.attributes };
|
||||
|
||||
return packagePolicyService.runExternalCallbacks(
|
||||
'packagePolicyPostCreate',
|
||||
createdPackagePolicy,
|
||||
|
@ -278,6 +292,18 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
force?: true;
|
||||
}
|
||||
): Promise<PackagePolicy[]> {
|
||||
for (const packagePolicy of packagePolicies) {
|
||||
if (!packagePolicy.id) {
|
||||
packagePolicy.id = SavedObjectsUtils.generateId();
|
||||
}
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'create',
|
||||
id: packagePolicy.id,
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
const agentPolicyIds = new Set(packagePolicies.map((pkgPolicy) => pkgPolicy.policy_id));
|
||||
|
||||
for (const agentPolicyId of agentPolicyIds) {
|
||||
|
@ -390,6 +416,12 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
response.package.experimental_data_stream_features = experimentalFeatures;
|
||||
}
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'get',
|
||||
id,
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
@ -406,11 +438,21 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
return [];
|
||||
}
|
||||
|
||||
return packagePolicySO.saved_objects.map((so) => ({
|
||||
const packagePolicies = packagePolicySO.saved_objects.map((so) => ({
|
||||
id: so.id,
|
||||
version: so.version,
|
||||
...so.attributes,
|
||||
}));
|
||||
|
||||
for (const packagePolicy of packagePolicies) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'find',
|
||||
id: packagePolicy.id,
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
return packagePolicies;
|
||||
}
|
||||
|
||||
public async getByIDs(
|
||||
|
@ -428,7 +470,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
return null;
|
||||
}
|
||||
|
||||
return packagePolicySO.saved_objects
|
||||
const packagePolicies = packagePolicySO.saved_objects
|
||||
.map((so): PackagePolicy | null => {
|
||||
if (so.error) {
|
||||
if (options.ignoreMissing && so.error.statusCode === 404) {
|
||||
|
@ -447,6 +489,16 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
};
|
||||
})
|
||||
.filter((packagePolicy): packagePolicy is PackagePolicy => packagePolicy !== null);
|
||||
|
||||
for (const packagePolicy of packagePolicies) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'get',
|
||||
id: packagePolicy.id,
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
return packagePolicies;
|
||||
}
|
||||
|
||||
public async list(
|
||||
|
@ -464,6 +516,14 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
filter: kuery ? normalizeKuery(SAVED_OBJECT_TYPE, kuery) : undefined,
|
||||
});
|
||||
|
||||
for (const packagePolicy of packagePolicies?.saved_objects ?? []) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'find',
|
||||
id: packagePolicy.id,
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
items: packagePolicies?.saved_objects.map((packagePolicySO) => ({
|
||||
id: packagePolicySO.id,
|
||||
|
@ -492,6 +552,14 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
filter: kuery ? normalizeKuery(SAVED_OBJECT_TYPE, kuery) : undefined,
|
||||
});
|
||||
|
||||
for (const packagePolicy of packagePolicies.saved_objects) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'find',
|
||||
id: packagePolicy.id,
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
items: packagePolicies.saved_objects.map((packagePolicySO) => packagePolicySO.id),
|
||||
total: packagePolicies.total,
|
||||
|
@ -507,6 +575,12 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
packagePolicyUpdate: UpdatePackagePolicy,
|
||||
options?: { user?: AuthenticatedUser; force?: boolean; skipUniqueNameVerification?: boolean }
|
||||
): Promise<PackagePolicy> {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'update',
|
||||
id,
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
let enrichedPackagePolicy: UpdatePackagePolicy;
|
||||
|
||||
try {
|
||||
|
@ -647,6 +721,13 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
options?: { user?: AuthenticatedUser; force?: boolean },
|
||||
currentVersion?: string
|
||||
): Promise<PackagePolicy[] | null> {
|
||||
for (const packagePolicy of packagePolicyUpdates) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'update',
|
||||
id: packagePolicy.id,
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
const oldPackagePolicies = await this.getByIDs(
|
||||
soClient,
|
||||
packagePolicyUpdates.map((p) => p.id)
|
||||
|
@ -770,6 +851,14 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
context?: RequestHandlerContext,
|
||||
request?: KibanaRequest
|
||||
): Promise<PostDeletePackagePoliciesResponse> {
|
||||
for (const id of ids) {
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'delete',
|
||||
id,
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
const result: PostDeletePackagePoliciesResponse = [];
|
||||
const logger = appContextService.getLogger();
|
||||
|
||||
|
|
|
@ -273,6 +273,8 @@ jest.mock('./app_context', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
jest.mock('./audit_logging');
|
||||
|
||||
const spyAgentPolicyServiceUpdate = jest.spyOn(agentPolicy.agentPolicyService, 'update');
|
||||
const spyAgentPolicyServicBumpAllAgentPoliciesForOutput = jest.spyOn(
|
||||
agentPolicy.agentPolicyService,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
import { securityMock } from '@kbn/security-plugin/server/mocks';
|
||||
|
||||
import { appContextService } from '../app_context';
|
||||
import { getDefaultFleetServerHost, createFleetServerHost } from '../fleet_server_host';
|
||||
|
@ -19,6 +20,10 @@ jest.mock('../fleet_server_host');
|
|||
jest.mock('../app_context');
|
||||
|
||||
const mockedAppContextService = appContextService as jest.Mocked<typeof appContextService>;
|
||||
mockedAppContextService.getSecuritySetup.mockImplementation(() => ({
|
||||
...securityMock.createSetup(),
|
||||
}));
|
||||
|
||||
const mockedGetDefaultFleetServerHost = getDefaultFleetServerHost as jest.MockedFunction<
|
||||
typeof getDefaultFleetServerHost
|
||||
>;
|
||||
|
|
16
x-pack/plugins/fleet/server/services/request_store.ts
Normal file
16
x-pack/plugins/fleet/server/services/request_store.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { AsyncLocalStorage } from 'async_hooks';
|
||||
|
||||
import type { KibanaRequest } from '@kbn/core-http-server';
|
||||
|
||||
export function getRequestStore() {
|
||||
const requestStore = new AsyncLocalStorage<KibanaRequest>();
|
||||
|
||||
return requestStore;
|
||||
}
|
|
@ -6,13 +6,31 @@
|
|||
*/
|
||||
|
||||
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
import { securityMock } from '@kbn/security-plugin/server/mocks';
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
|
||||
import { GLOBAL_SETTINGS_ID, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE } from '../../common/constants';
|
||||
|
||||
import type { Settings } from '../types';
|
||||
|
||||
import { appContextService } from './app_context';
|
||||
import { settingsSetup } from './settings';
|
||||
import { getSettings, saveSettings, settingsSetup } from './settings';
|
||||
import { auditLoggingService } from './audit_logging';
|
||||
import { listFleetServerHosts } from './fleet_server_host';
|
||||
|
||||
jest.mock('./app_context');
|
||||
jest.mock('./audit_logging');
|
||||
jest.mock('./fleet_server_host');
|
||||
|
||||
const mockListFleetServerHosts = listFleetServerHosts as jest.MockedFunction<
|
||||
typeof listFleetServerHosts
|
||||
>;
|
||||
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
|
||||
const mockedAppContextService = appContextService as jest.Mocked<typeof appContextService>;
|
||||
mockedAppContextService.getSecuritySetup.mockImplementation(() => ({
|
||||
...securityMock.createSetup(),
|
||||
}));
|
||||
|
||||
describe('settingsSetup', () => {
|
||||
afterEach(() => {
|
||||
|
@ -66,8 +84,145 @@ describe('settingsSetup', () => {
|
|||
type: 'so_type',
|
||||
});
|
||||
|
||||
mockListFleetServerHosts.mockResolvedValueOnce({
|
||||
items: [
|
||||
{
|
||||
id: 'fleet-server-host',
|
||||
name: 'Fleet Server Host',
|
||||
is_default: true,
|
||||
is_preconfigured: false,
|
||||
host_urls: ['http://localhost:8220'],
|
||||
},
|
||||
],
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
total: 1,
|
||||
});
|
||||
|
||||
await settingsSetup(soClientMock);
|
||||
|
||||
expect(soClientMock.create).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSettings', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
|
||||
soClient.find.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
id: GLOBAL_SETTINGS_ID,
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
score: 0,
|
||||
},
|
||||
],
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
total: 1,
|
||||
});
|
||||
|
||||
mockListFleetServerHosts.mockResolvedValueOnce({
|
||||
items: [
|
||||
{
|
||||
id: 'fleet-server-host',
|
||||
name: 'Fleet Server Host',
|
||||
is_default: true,
|
||||
is_preconfigured: false,
|
||||
host_urls: ['http://localhost:8220'],
|
||||
},
|
||||
],
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
total: 1,
|
||||
});
|
||||
|
||||
await getSettings(soClient);
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveSettings', () => {
|
||||
describe('when settings object exists', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
|
||||
const newData: Partial<Omit<Settings, 'id'>> = {
|
||||
fleet_server_hosts: ['http://localhost:8220'],
|
||||
};
|
||||
|
||||
soClient.find.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
id: GLOBAL_SETTINGS_ID,
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
score: 0,
|
||||
},
|
||||
],
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
total: 1,
|
||||
});
|
||||
|
||||
soClient.update.mockResolvedValueOnce({
|
||||
id: GLOBAL_SETTINGS_ID,
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
mockListFleetServerHosts.mockResolvedValueOnce({
|
||||
items: [
|
||||
{
|
||||
id: 'fleet-server-host',
|
||||
name: 'Fleet Server Host',
|
||||
is_default: true,
|
||||
is_preconfigured: false,
|
||||
host_urls: ['http://localhost:8220'],
|
||||
},
|
||||
],
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
total: 1,
|
||||
});
|
||||
|
||||
await saveSettings(soClient, newData);
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'create',
|
||||
id: GLOBAL_SETTINGS_ID,
|
||||
savedObjectType: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when settings object does not exist', () => {
|
||||
it('should call audit logger', async () => {
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
|
||||
const newData: Partial<Omit<Settings, 'id'>> = {
|
||||
fleet_server_hosts: ['http://localhost:8220'],
|
||||
};
|
||||
|
||||
soClient.find.mockRejectedValueOnce(Boom.notFound('not found'));
|
||||
|
||||
soClient.create.mockResolvedValueOnce({
|
||||
id: GLOBAL_SETTINGS_ID,
|
||||
attributes: {},
|
||||
references: [],
|
||||
type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
await saveSettings(soClient, newData);
|
||||
|
||||
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
|
||||
action: 'create',
|
||||
id: GLOBAL_SETTINGS_ID,
|
||||
savedObjectType: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,11 +14,17 @@ import type { SettingsSOAttributes, Settings, BaseSettings } from '../../common/
|
|||
|
||||
import { appContextService } from './app_context';
|
||||
import { listFleetServerHosts } from './fleet_server_host';
|
||||
import { auditLoggingService } from './audit_logging';
|
||||
|
||||
export async function getSettings(soClient: SavedObjectsClientContract): Promise<Settings> {
|
||||
const res = await soClient.find<SettingsSOAttributes>({
|
||||
type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'get',
|
||||
id: GLOBAL_SETTINGS_ID,
|
||||
savedObjectType: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
if (res.total === 0) {
|
||||
throw Boom.notFound('Global settings not found');
|
||||
|
@ -59,6 +65,12 @@ export async function saveSettings(
|
|||
try {
|
||||
const settings = await getSettings(soClient);
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'update',
|
||||
id: settings.id,
|
||||
savedObjectType: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
const res = await soClient.update<SettingsSOAttributes>(
|
||||
GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
settings.id,
|
||||
|
@ -72,6 +84,13 @@ export async function saveSettings(
|
|||
} catch (e) {
|
||||
if (e.isBoom && e.output.statusCode === 404) {
|
||||
const defaultSettings = createDefaultSettings();
|
||||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'create',
|
||||
id: GLOBAL_SETTINGS_ID,
|
||||
savedObjectType: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
const res = await soClient.create<SettingsSOAttributes>(
|
||||
GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue