[EDR Workflows][API] Gate Agent Tamper Protection setting on Agent Policy Settings (#174400)

This PR is part of an effort to limit EDR Workflow features to the
Endpoint Complete tier on serverless and focuses on server skde part of
gating Agent Tamper Protection.

Related PRs:

https://github.com/elastic/kibana/pull/174278
https://github.com/elastic/kibana/pull/175129

**We decided to stick with the existing Fleet privileges for this
component, and no extra changes are needed RBAC wise (confirmed with
@roxana-gheorghe).**

**Plugin/Policy Watcher Changes**:
To monitor agent policies for a downgrade in tier (from complete to
essentials) and disable agent protections if enabled, the following
steps have been taken:

1. A new app feature, `endpoint_agent_tamper_protection`, has been
introduced and linked to the `endpoint:complete` tier.
2. An additional method, `bumpRevision`, has been exposed in the fleet's
agent policy service. This method utilizes the service's internal update
function and includes a `disable_protection` flag, allowing it to be
used without further modifications.
3. The security solution side calls this method upon successful fleet
plugin setup. If the `endpoint_agent_tamper_protection` app feature is
not enabled, it retrieves all agent policies with `is_protected: true`
and updates these policies with `is_protected: false`.

**API Changes**:
To respond to attempts to activate agent protection via the API by users
on the Essentials tier, the following steps have been taken:

1. External callback functionality has been added to the agentPolicy
service, following the implementation in packagePolicy.
2. Update and create agent policy callbacks have been registered in the
security solution. These callbacks check for the enabled status of the
`endpoint_agent_tamper_protection` app feature. If disabled, the
callback throws an error.
3. External callback execution has been added to the update and create
methods in agent policy route handlers.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Konrad Szwarc 2024-02-01 22:35:40 +01:00 committed by GitHub
parent 7c45bfdc30
commit 8166d18a37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 744 additions and 38 deletions

View file

@ -32,10 +32,10 @@ import {
UpdatePackagePolicy,
} from '@kbn/fleet-plugin/common';
import {
ExternalCallback,
FleetStartContract,
PostPackagePolicyPostDeleteCallback,
PostPackagePolicyPostCreateCallback,
ExternalCallback,
} from '@kbn/fleet-plugin/server';
import { CLOUD_SECURITY_POSTURE_PACKAGE_NAME } from '../common/constants';
import Chance from 'chance';

View file

@ -157,6 +157,7 @@ export const createMockAgentPolicyService = (): jest.Mocked<AgentPolicyServiceIn
list: jest.fn(),
getFullAgentPolicy: jest.fn(),
getByIds: jest.fn(),
bumpRevision: jest.fn(),
};
};

View file

@ -8,33 +8,32 @@
import { backOff } from 'exponential-backoff';
import type { Observable } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import { take, filter } from 'rxjs/operators';
import { filter, take } from 'rxjs/operators';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { i18n } from '@kbn/i18n';
import type {
CoreSetup,
CoreStart,
ElasticsearchClient,
ElasticsearchServiceStart,
HttpServiceSetup,
KibanaRequest,
Logger,
Plugin,
PluginInitializerContext,
SavedObjectsServiceStart,
HttpServiceSetup,
KibanaRequest,
ServiceStatus,
ElasticsearchClient,
SavedObjectsClientContract,
SavedObjectsServiceStart,
ServiceStatus,
} from '@kbn/core/server';
import { DEFAULT_APP_CATEGORIES, SavedObjectsClient, ServiceStatusLevels } from '@kbn/core/server';
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import type { TelemetryPluginSetup, TelemetryPluginStart } from '@kbn/telemetry-plugin/server';
import { DEFAULT_APP_CATEGORIES, SavedObjectsClient, ServiceStatusLevels } from '@kbn/core/server';
import type { PluginStart as DataPluginStart } from '@kbn/data-plugin/server';
import type { LicensingPluginStart } from '@kbn/licensing-plugin/server';
import type {
EncryptedSavedObjectsPluginStart,
EncryptedSavedObjectsPluginSetup,
EncryptedSavedObjectsPluginStart,
} from '@kbn/encrypted-saved-objects-plugin/server';
import type {
AuditLogger,
@ -57,61 +56,60 @@ import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server';
import type { FleetConfigType } from '../common/types';
import type { FleetAuthz } from '../common';
import type { ExperimentalFeatures } from '../common/experimental_features';
import {
MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE,
INTEGRATIONS_PLUGIN_ID,
MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE,
UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
} from '../common';
import type { ExperimentalFeatures } from '../common/experimental_features';
import { parseExperimentalConfigValue } from '../common/experimental_features';
import { getFilesClientFactory } from './services/files/get_files_client_factory';
import type { MessageSigningServiceInterface } from './services/security';
import {
getRouteRequiredAuthz,
makeRouterWithFleetAuthz,
calculateRouteAuthz,
getAuthzFromRequest,
getRouteRequiredAuthz,
makeRouterWithFleetAuthz,
MessageSigningService,
} from './services/security';
import {
PLUGIN_ID,
OUTPUT_SAVED_OBJECT_TYPE,
AGENT_POLICY_SAVED_OBJECT_TYPE,
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
PACKAGES_SAVED_OBJECT_TYPE,
ASSETS_SAVED_OBJECT_TYPE,
PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE,
DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE,
FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
OUTPUT_SAVED_OBJECT_TYPE,
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
PACKAGES_SAVED_OBJECT_TYPE,
PLUGIN_ID,
PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE,
} from './constants';
import { registerSavedObjects, registerEncryptedSavedObjects } from './saved_objects';
import { registerEncryptedSavedObjects, registerSavedObjects } from './saved_objects';
import { registerRoutes } from './routes';
import type { ExternalCallback, FleetRequestHandlerContext } from './types';
import type {
ESIndexPatternService,
AgentService,
AgentPolicyServiceInterface,
AgentService,
ESIndexPatternService,
PackageService,
} from './services';
import { FleetUsageSender } from './services';
import {
appContextService,
licenseService,
ESIndexPatternSavedObjectService,
agentPolicyService,
packagePolicyService,
AgentServiceImpl,
appContextService,
ESIndexPatternSavedObjectService,
FleetUsageSender,
licenseService,
packagePolicyService,
PackageServiceImpl,
} from './services';
import {
registerFleetUsageCollector,
fetchAgentsUsage,
fetchFleetUsage,
registerFleetUsageCollector,
} from './collectors/register';
import { FleetArtifactsClient } from './services/artifacts';
import type { FleetRouter } from './types/request_context';
@ -627,6 +625,7 @@ export class FleetPlugin
list: agentPolicyService.list,
getFullAgentPolicy: agentPolicyService.getFullAgentPolicy,
getByIds: agentPolicyService.getByIDs,
bumpRevision: agentPolicyService.bumpRevision.bind(agentPolicyService),
},
packagePolicyService,
registerExternalCallback: (type: ExternalCallback[0], callback: ExternalCallback[1]) => {

View file

@ -195,6 +195,12 @@ export const createAgentPolicyHandler: FleetRequestHandler<
body,
});
} catch (error) {
if (error.statusCode) {
return response.customError({
statusCode: error.statusCode,
body: { message: error.message },
});
}
return defaultFleetErrorHandler({ error, response });
}
};
@ -229,6 +235,12 @@ export const updateAgentPolicyHandler: FleetRequestHandler<
body,
});
} catch (error) {
if (error.statusCode) {
return response.customError({
statusCode: error.statusCode,
body: { message: error.message },
});
}
return defaultFleetErrorHandler({ error, response });
}
};

View file

@ -44,6 +44,9 @@ import type {
FullAgentPolicy,
ListWithKuery,
NewPackagePolicy,
PostAgentPolicyCreateCallback,
PostAgentPolicyUpdateCallback,
ExternalCallback,
} from '../types';
import {
getAllowedOutputTypeForPolicy,
@ -234,6 +237,43 @@ class AgentPolicyService {
return policyHasSyntheticsIntegration(agentPolicy);
}
public async runExternalCallbacks(
externalCallbackType: ExternalCallback[0],
agentPolicy: NewAgentPolicy | Partial<AgentPolicy>
): Promise<NewAgentPolicy | Partial<AgentPolicy>> {
const logger = appContextService.getLogger();
logger.debug(`Running external callbacks for ${externalCallbackType}`);
try {
const externalCallbacks = appContextService.getExternalCallbacks(externalCallbackType);
let newAgentPolicy = agentPolicy;
if (externalCallbacks && externalCallbacks.size > 0) {
let updatedNewAgentPolicy = newAgentPolicy;
for (const callback of externalCallbacks) {
let result;
if (externalCallbackType === 'agentPolicyCreate') {
result = await (callback as PostAgentPolicyCreateCallback)(
newAgentPolicy as NewAgentPolicy
);
updatedNewAgentPolicy = result;
}
if (externalCallbackType === 'agentPolicyUpdate') {
result = await (callback as PostAgentPolicyUpdateCallback)(
newAgentPolicy as Partial<AgentPolicy>
);
updatedNewAgentPolicy = result;
}
}
newAgentPolicy = updatedNewAgentPolicy;
}
return newAgentPolicy;
} catch (error) {
logger.error(`Error running external callbacks for ${externalCallbackType}`);
logger.error(error);
throw error;
}
}
public async create(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
@ -254,7 +294,7 @@ class AgentPolicyService {
id: options.id,
savedObjectType: AGENT_POLICY_SAVED_OBJECT_TYPE,
});
await this.runExternalCallbacks('agentPolicyCreate', agentPolicy);
this.checkTamperProtectionLicense(agentPolicy);
const logger = appContextService.getLogger();
@ -519,7 +559,14 @@ class AgentPolicyService {
if (!existingAgentPolicy) {
throw new AgentPolicyNotFoundError('Agent policy not found');
}
try {
await this.runExternalCallbacks('agentPolicyUpdate', agentPolicy);
} catch (error) {
logger.error(`Error running external callbacks for agentPolicyUpdate`);
if (error.apiPassThrough) {
throw error;
}
}
this.checkTamperProtectionLicense(agentPolicy);
await this.checkForValidUninstallToken(agentPolicy, id);

View file

@ -42,6 +42,8 @@ import type {
PostPackagePolicyPostDeleteCallback,
PostPackagePolicyPostCreateCallback,
PutPackagePolicyUpdateCallback,
PostAgentPolicyCreateCallback,
PostAgentPolicyUpdateCallback,
} from '../types';
import type { FleetAppContext } from '../plugin';
import type { TelemetryEventsSender } from '../telemetry/sender';
@ -245,7 +247,11 @@ class AppContextService {
type: T
):
| Set<
T extends 'packagePolicyCreate'
T extends 'agentPolicyCreate'
? PostAgentPolicyCreateCallback
: T extends 'agentPolicyUpdate'
? PostAgentPolicyUpdateCallback
: T extends 'packagePolicyCreate'
? PostPackagePolicyCreateCallback
: T extends 'packagePolicyDelete'
? PostPackagePolicyDeleteCallback
@ -258,7 +264,11 @@ class AppContextService {
| undefined {
if (this.externalCallbacks) {
return this.externalCallbacks.get(type) as Set<
T extends 'packagePolicyCreate'
T extends 'agentPolicyCreate'
? PostAgentPolicyCreateCallback
: T extends 'agentPolicyUpdate'
? PostAgentPolicyUpdateCallback
: T extends 'packagePolicyCreate'
? PostPackagePolicyCreateCallback
: T extends 'packagePolicyDelete'
? PostPackagePolicyDeleteCallback

View file

@ -33,6 +33,7 @@ export interface AgentPolicyServiceInterface {
list: typeof agentPolicyService['list'];
getFullAgentPolicy: typeof agentPolicyService['getFullAgentPolicy'];
getByIds: typeof agentPolicyService['getByIDs'];
bumpRevision: typeof agentPolicyService['bumpRevision'];
}
// Agent services

View file

@ -283,6 +283,7 @@ jest.mock('./app_context', () => ({
getUninstallTokenService: () => ({
generateTokenForPolicyId: jest.fn(),
}),
getExternalCallbacks: jest.fn(),
},
}));

View file

@ -16,6 +16,8 @@ import type {
UpdatePackagePolicy,
PackagePolicy,
DeletePackagePoliciesResponse,
NewAgentPolicy,
AgentPolicy,
} from '../../common/types';
export type PostPackagePolicyDeleteCallback = (
@ -58,6 +60,14 @@ export type PutPackagePolicyUpdateCallback = (
request?: KibanaRequest
) => Promise<UpdatePackagePolicy>;
export type PostAgentPolicyCreateCallback = (
agentPolicy: NewAgentPolicy
) => Promise<NewAgentPolicy>;
export type PostAgentPolicyUpdateCallback = (
agentPolicy: Partial<AgentPolicy>
) => Promise<Partial<AgentPolicy>>;
export type ExternalCallbackCreate = ['packagePolicyCreate', PostPackagePolicyCreateCallback];
export type ExternalCallbackPostCreate = [
'packagePolicyPostCreate',
@ -71,6 +81,15 @@ export type ExternalCallbackPostDelete = [
];
export type ExternalCallbackUpdate = ['packagePolicyUpdate', PutPackagePolicyUpdateCallback];
export type ExternalCallbackAgentPolicyCreate = [
'agentPolicyCreate',
PostAgentPolicyCreateCallback
];
export type ExternalCallbackAgentPolicyUpdate = [
'agentPolicyUpdate',
PostAgentPolicyUpdateCallback
];
/**
* Callbacks supported by the Fleet plugin
*/
@ -79,6 +98,8 @@ export type ExternalCallback =
| ExternalCallbackPostCreate
| ExternalCallbackDelete
| ExternalCallbackPostDelete
| ExternalCallbackUpdate;
| ExternalCallbackUpdate
| ExternalCallbackAgentPolicyCreate
| ExternalCallbackAgentPolicyUpdate;
export type ExternalCallbacksStorage = Map<ExternalCallback[0], Set<ExternalCallback[1]>>;

View file

@ -0,0 +1,66 @@
/*
* 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 { IndexedFleetEndpointPolicyResponse } from '../../../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import {
createAgentPolicyTask,
createAgentPolicyWithAgentTamperProtectionsEnabled,
enableAgentTamperProtectionFeatureFlagInPolicy,
getEndpointIntegrationVersion,
} from '../../../../tasks/fleet';
import { login } from '../../../../tasks/login';
describe(
'Agent policy settings API operations on Complete',
{
tags: ['@serverless'],
env: {
ftrConfig: {
productTypes: [
{ product_line: 'security', product_tier: 'complete' },
{ product_line: 'endpoint', product_tier: 'complete' },
],
},
},
},
() => {
let indexedPolicy: IndexedFleetEndpointPolicyResponse;
// let policy: PolicyData;
beforeEach(() => {
getEndpointIntegrationVersion().then((version) =>
createAgentPolicyTask(version).then((data) => {
indexedPolicy = data;
})
);
login();
});
afterEach(() => {
if (indexedPolicy) {
cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy);
}
});
describe('Agent tamper protections', () => {
it('allow enabling the feature', () => {
enableAgentTamperProtectionFeatureFlagInPolicy(indexedPolicy.agentPolicies[0].id).then(
(response) => {
expect(response.status).to.equal(200);
expect(response.body.item.is_protected).to.equal(true);
}
);
});
it('throw error when trying to create agent policy', () => {
createAgentPolicyWithAgentTamperProtectionsEnabled().then((response) => {
expect(response.status).to.equal(200);
expect(response.body.item.is_protected).to.equal(false); // We don't allow creating a policy with the feature enabled
});
});
});
}
);

View file

@ -0,0 +1,76 @@
/*
* 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 { UpdateAgentPolicyResponse } from '@kbn/fleet-plugin/common/types';
import type { IndexedFleetEndpointPolicyResponse } from '../../../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import {
createAgentPolicyTask,
createAgentPolicyWithAgentTamperProtectionsEnabled,
enableAgentTamperProtectionFeatureFlagInPolicy,
getEndpointIntegrationVersion,
} from '../../../../tasks/fleet';
import { login } from '../../../../tasks/login';
describe(
'Agent policy settings API operations on Essentials',
{
tags: ['@serverless'],
env: {
ftrConfig: {
productTypes: [
{ product_line: 'security', product_tier: 'essentials' },
{ product_line: 'endpoint', product_tier: 'essentials' },
],
},
},
},
() => {
let indexedPolicy: IndexedFleetEndpointPolicyResponse;
beforeEach(() => {
getEndpointIntegrationVersion().then((version) =>
createAgentPolicyTask(version).then((data) => {
indexedPolicy = data;
})
);
login();
});
afterEach(() => {
if (indexedPolicy) {
cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy);
}
});
describe('Agent tamper protections', () => {
it('throw error when trying to update agent policy settings', () => {
enableAgentTamperProtectionFeatureFlagInPolicy(indexedPolicy.agentPolicies[0].id, {
failOnStatusCode: false,
}).then((res) => {
const response = res as Cypress.Response<UpdateAgentPolicyResponse & { message: string }>;
expect(response.status).to.equal(403);
expect(response.body.message).to.equal(
'Agent Tamper Protection is not allowed in current environment'
);
});
});
it('throw error when trying to create agent policy', () => {
createAgentPolicyWithAgentTamperProtectionsEnabled({ failOnStatusCode: false }).then(
(res) => {
const response = res as Cypress.Response<
UpdateAgentPolicyResponse & { message: string }
>;
expect(response.status).to.equal(403);
expect(response.body.message).to.equal(
'Agent Tamper Protection is not allowed in current environment'
);
}
);
});
});
}
);

View file

@ -11,6 +11,7 @@ import type {
GetInfoResponse,
GetPackagePoliciesResponse,
GetOneAgentPolicyResponse,
CreateAgentPolicyResponse,
} from '@kbn/fleet-plugin/common';
import {
agentRouteService,
@ -99,7 +100,30 @@ export const createAgentPolicyTask = (
);
};
export const enableAgentTamperProtectionFeatureFlagInPolicy = (agentPolicyId: string) => {
export const createAgentPolicyWithAgentTamperProtectionsEnabled = (
overwrite?: Record<string, unknown>
) => {
return request<CreateAgentPolicyResponse>({
method: 'POST',
url: agentPolicyRouteService.getCreatePath(),
body: {
name: `With agent tamper protection enabled ${Math.random().toString(36).substring(2, 7)}`,
agent_features: [{ name: 'tamper_protection', enabled: true }],
is_protected: true,
description: 'test',
namespace: 'default',
monitoring_enabled: ['logs', 'metrics'],
inactivity_timeout: 1209600,
},
headers: { 'Elastic-Api-Version': API_VERSIONS.public.v1 },
...(overwrite ?? {}),
});
};
export const enableAgentTamperProtectionFeatureFlagInPolicy = (
agentPolicyId: string,
overwrite?: Record<string, unknown>
) => {
return request<UpdateAgentPolicyResponse>({
method: 'PUT',
url: agentPolicyRouteService.getUpdatePath(agentPolicyId),
@ -113,6 +137,7 @@ export const enableAgentTamperProtectionFeatureFlagInPolicy = (agentPolicyId: st
inactivity_timeout: 1209600,
},
headers: { 'Elastic-Api-Version': API_VERSIONS.public.v1 },
...(overwrite ?? {}),
});
};

View file

@ -24,6 +24,8 @@ import type { PluginStartContract as AlertsPluginStartContract } from '@kbn/aler
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { FleetActionsClientInterface } from '@kbn/fleet-plugin/server/services/actions/types';
import {
getAgentPolicyCreateCallback,
getAgentPolicyUpdateCallback,
getPackagePolicyCreateCallback,
getPackagePolicyDeleteCallback,
getPackagePolicyPostCreateCallback,
@ -119,6 +121,15 @@ export class EndpointAppContextService {
savedObjectsClient,
} = dependencies;
registerIngestCallback(
'agentPolicyCreate',
getAgentPolicyCreateCallback(logger, appFeaturesService)
);
registerIngestCallback(
'agentPolicyUpdate',
getAgentPolicyUpdateCallback(logger, appFeaturesService)
);
registerIngestCallback(
'packagePolicyCreate',
getPackagePolicyCreateCallback(

View file

@ -0,0 +1,167 @@
/*
* 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 { createMockEndpointAppContextServiceStartContract } from '../mocks';
import type { Logger } from '@kbn/logging';
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import type { EndpointInternalFleetServicesInterface } from '../services/fleet';
import { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-features/keys';
import type { AppFeaturesService } from '../../lib/app_features_service/app_features_service';
import { createAppFeaturesServiceMock } from '../../lib/app_features_service/mocks';
import { turnOffAgentPolicyFeatures } from './turn_off_agent_policy_features';
import { FleetAgentPolicyGenerator } from '../../../common/endpoint/data_generators/fleet_agent_policy_generator';
import type { AgentPolicy, GetAgentPoliciesResponseItem } from '@kbn/fleet-plugin/common';
describe('Turn Off Agent Policy Features Migration', () => {
let esClient: ElasticsearchClient;
let fleetServices: EndpointInternalFleetServicesInterface;
let appFeatureService: AppFeaturesService;
let logger: Logger;
const callTurnOffAgentPolicyFeatures = () =>
turnOffAgentPolicyFeatures(esClient, fleetServices, appFeatureService, logger);
beforeEach(() => {
const endpointContextStartContract = createMockEndpointAppContextServiceStartContract();
({ esClient, logger } = endpointContextStartContract);
appFeatureService = endpointContextStartContract.appFeaturesService;
fleetServices = endpointContextStartContract.endpointFleetServicesFactory.asInternalUser();
});
describe('and `agentTamperProtection` is enabled', () => {
it('should do nothing', async () => {
await callTurnOffAgentPolicyFeatures();
expect(fleetServices.agentPolicy.list as jest.Mock).not.toHaveBeenCalled();
expect(logger.info).toHaveBeenLastCalledWith(
'App feature [endpoint_agent_tamper_protection] is enabled. Nothing to do!'
);
});
});
describe('and `agentTamperProtection` is disabled', () => {
let policyGenerator: FleetAgentPolicyGenerator;
let page1Items: GetAgentPoliciesResponseItem[] = [];
let page2Items: GetAgentPoliciesResponseItem[] = [];
let page3Items: GetAgentPoliciesResponseItem[] = [];
let bulkUpdateResponse: AgentPolicy[];
const generatePolicyMock = (): GetAgentPoliciesResponseItem => {
return policyGenerator.generate({ is_protected: true });
};
beforeEach(() => {
policyGenerator = new FleetAgentPolicyGenerator('seed');
const agentPolicyListSrv = fleetServices.agentPolicy.list as jest.Mock;
appFeatureService = createAppFeaturesServiceMock(
ALL_APP_FEATURE_KEYS.filter((key) => key !== 'endpoint_agent_tamper_protection')
);
page1Items = [generatePolicyMock(), generatePolicyMock()];
page2Items = [generatePolicyMock(), generatePolicyMock()];
page3Items = [generatePolicyMock()];
agentPolicyListSrv
.mockImplementationOnce(async () => {
return {
total: 2500,
page: 1,
perPage: 1000,
items: page1Items,
};
})
.mockImplementationOnce(async () => {
return {
total: 2500,
page: 2,
perPage: 1000,
items: page2Items,
};
})
.mockImplementationOnce(async () => {
return {
total: 2500,
page: 3,
perPage: 1000,
items: page3Items,
};
});
bulkUpdateResponse = [
page1Items[0],
page1Items[1],
page2Items[0],
page2Items[1],
page3Items[0],
];
(fleetServices.agentPolicy.bumpRevision as jest.Mock).mockImplementation(async () => {
return bulkUpdateResponse;
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('should update only policies that have protections turn on', async () => {
await callTurnOffAgentPolicyFeatures();
expect(fleetServices.agentPolicy.list as jest.Mock).toHaveBeenCalledTimes(3);
const updates = Array.from({ length: 5 }, (_, i) => ({
soClient: fleetServices.internalSoClient,
esClient,
id: bulkUpdateResponse![i].id,
}));
expect(fleetServices.agentPolicy.bumpRevision as jest.Mock).toHaveBeenCalledTimes(5);
updates.forEach((args, i) => {
expect(fleetServices.agentPolicy.bumpRevision as jest.Mock).toHaveBeenNthCalledWith(
i + 1,
args.soClient,
args.esClient,
args.id,
{ removeProtection: true, user: { username: 'elastic' } }
);
});
expect(logger.info).toHaveBeenCalledWith(
'App feature [endpoint_agent_tamper_protection] is disabled. Checking fleet agent policies for compliance'
);
expect(logger.info).toHaveBeenCalledWith(
`Found 5 policies that need updates:\n${bulkUpdateResponse!
.map(
(policy) =>
`Policy [${policy.id}][${policy.name}] updated to disable agent tamper protection.`
)
.join('\n')}`
);
expect(logger.info).toHaveBeenCalledWith('Done. All updates applied successfully');
});
it('should log failures', async () => {
(fleetServices.agentPolicy.bumpRevision as jest.Mock).mockImplementationOnce(async () => {
throw new Error('oh noo');
});
await callTurnOffAgentPolicyFeatures();
expect(logger.error).toHaveBeenCalledWith(
`Done - 1 out of 5 were successful. Errors encountered:\nPolicy [${
bulkUpdateResponse![0].id
}] failed to update due to error: Error: oh noo`
);
expect(fleetServices.agentPolicy.bumpRevision as jest.Mock).toHaveBeenCalledTimes(5);
});
});
});

View file

@ -0,0 +1,92 @@
/*
* 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 { Logger, ElasticsearchClient } from '@kbn/core/server';
import type { AgentPolicy } from '@kbn/fleet-plugin/common';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import { AppFeatureSecurityKey } from '@kbn/security-solution-features/keys';
import pMap from 'p-map';
import type { EndpointInternalFleetServicesInterface } from '../services/fleet';
import type { AppFeaturesService } from '../../lib/app_features_service/app_features_service';
export const turnOffAgentPolicyFeatures = async (
esClient: ElasticsearchClient,
fleetServices: EndpointInternalFleetServicesInterface,
appFeaturesService: AppFeaturesService,
logger: Logger
): Promise<void> => {
const log = logger.get('endpoint', 'agentPolicyFeatures');
if (appFeaturesService.isEnabled(AppFeatureSecurityKey.endpointAgentTamperProtection)) {
log.info(
`App feature [${AppFeatureSecurityKey.endpointAgentTamperProtection}] is enabled. Nothing to do!`
);
return;
}
log.info(
`App feature [${AppFeatureSecurityKey.endpointAgentTamperProtection}] is disabled. Checking fleet agent policies for compliance`
);
const { agentPolicy: agentPolicyService, internalSoClient } = fleetServices;
const updates: AgentPolicy[] = [];
const messages: string[] = [];
const perPage = 1000;
let hasMoreData = true;
let total = 0;
let page = 1;
do {
const currentPage = page++;
const { items, total: totalPolicies } = await agentPolicyService.list(internalSoClient, {
page: currentPage,
kuery: 'ingest-agent-policies.is_protected: true',
perPage,
});
total = totalPolicies;
hasMoreData = currentPage * perPage < total;
for (const item of items) {
messages.push(
`Policy [${item.id}][${item.name}] updated to disable agent tamper protection.`
);
updates.push({ ...item, is_protected: false });
}
} while (hasMoreData);
if (updates.length > 0) {
logger.info(`Found ${updates.length} policies that need updates:\n${messages.join('\n')}`);
const policyUpdateErrors: Array<{ id: string; error: Error }> = [];
await pMap(updates, async (update) => {
try {
return await agentPolicyService.bumpRevision(internalSoClient, esClient, update.id, {
user: { username: 'elastic' } as AuthenticatedUser,
removeProtection: true,
});
} catch (error) {
policyUpdateErrors.push({ error, id: update.id });
}
});
if (policyUpdateErrors.length > 0) {
logger.error(
`Done - ${policyUpdateErrors.length} out of ${
updates.length
} were successful. Errors encountered:\n${policyUpdateErrors
.map((e) => `Policy [${e.id}] failed to update due to error: ${e.error}`)
.join('\n')}`
);
} else {
logger.info(`Done. All updates applied successfully`);
}
} else {
logger.info(`Done. Checked ${total} policies and no updates needed`);
}
};

View file

@ -24,13 +24,15 @@ import {
} from '../../common/endpoint/models/policy_config';
import { buildManifestManagerMock } from '../endpoint/services/artifacts/manifest_manager/manifest_manager.mock';
import {
getAgentPolicyCreateCallback,
getAgentPolicyUpdateCallback,
getPackagePolicyCreateCallback,
getPackagePolicyDeleteCallback,
getPackagePolicyPostCreateCallback,
getPackagePolicyUpdateCallback,
} from './fleet_integration';
import type { KibanaRequest } from '@kbn/core/server';
import { ALL_APP_FEATURE_KEYS } from '@kbn/security-solution-features/keys';
import type { KibanaRequest, Logger } from '@kbn/core/server';
import { ALL_APP_FEATURE_KEYS, AppFeatureSecurityKey } from '@kbn/security-solution-features/keys';
import { requestContextMock } from '../lib/detection_engine/routes/__mocks__';
import { requestContextFactoryMock } from '../request_context_factory.mock';
import type { EndpointAppContextServiceStartContract } from '../endpoint/endpoint_app_context_services';
@ -50,7 +52,10 @@ import { getMockArtifacts, toArtifactRecords } from '../endpoint/lib/artifacts/m
import { Manifest } from '../endpoint/lib/artifacts';
import type { NewPackagePolicy, PackagePolicy } from '@kbn/fleet-plugin/common/types/models';
import type { ManifestSchema } from '../../common/endpoint/schema/manifest';
import type { PostDeletePackagePoliciesResponse } from '@kbn/fleet-plugin/common';
import type {
GetAgentPoliciesResponseItem,
PostDeletePackagePoliciesResponse,
} from '@kbn/fleet-plugin/common';
import { createMockPolicyData } from '../endpoint/services/feature_usage/mocks';
import { ALL_ENDPOINT_ARTIFACT_LIST_IDS } from '../../common/endpoint/service/artifacts/constants';
import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants';
@ -58,6 +63,7 @@ import { disableProtections } from '../../common/endpoint/models/policy_config_h
import type { AppFeaturesService } from '../lib/app_features_service/app_features_service';
import { createAppFeaturesServiceMock } from '../lib/app_features_service/mocks';
import * as moment from 'moment';
import type { PostAgentPolicyCreateCallback } from '@kbn/fleet-plugin/server/types';
jest.mock('uuid', () => ({
v4: (): string => 'NEW_UUID',
@ -382,6 +388,115 @@ describe('ingest_integration tests ', () => {
});
});
describe('agent policy update callback', () => {
it('AppFeature disabled - returns an error if higher tier features are turned on in the policy', async () => {
const logger = loggingSystemMock.create().get('ingest_integration.test');
appFeaturesService = createAppFeaturesServiceMock(
ALL_APP_FEATURE_KEYS.filter(
(key) => key !== AppFeatureSecurityKey.endpointAgentTamperProtection
)
);
const callback = getAgentPolicyUpdateCallback(logger, appFeaturesService);
const policyConfig = generator.generateAgentPolicy();
policyConfig.is_protected = true;
await expect(() => callback(policyConfig)).rejects.toThrow(
'Agent Tamper Protection is not allowed in current environment'
);
});
it('AppFeature disabled - returns agent policy if higher tier features are turned off in the policy', async () => {
const logger = loggingSystemMock.create().get('ingest_integration.test');
appFeaturesService = createAppFeaturesServiceMock(
ALL_APP_FEATURE_KEYS.filter(
(key) => key !== AppFeatureSecurityKey.endpointAgentTamperProtection
)
);
const callback = getAgentPolicyUpdateCallback(logger, appFeaturesService);
const policyConfig = generator.generateAgentPolicy();
const updatedPolicyConfig = await callback(policyConfig);
expect(updatedPolicyConfig).toEqual(policyConfig);
});
it('AppFeature enabled - returns agent policy if higher tier features are turned on in the policy', async () => {
const logger = loggingSystemMock.create().get('ingest_integration.test');
const callback = getAgentPolicyUpdateCallback(logger, appFeaturesService);
const policyConfig = generator.generateAgentPolicy();
policyConfig.is_protected = true;
const updatedPolicyConfig = await callback(policyConfig);
expect(updatedPolicyConfig).toEqual(policyConfig);
});
it('AppFeature enabled - returns agent policy if higher tier features are turned off in the policy', async () => {
const logger = loggingSystemMock.create().get('ingest_integration.test');
const callback = getAgentPolicyUpdateCallback(logger, appFeaturesService);
const policyConfig = generator.generateAgentPolicy();
const updatedPolicyConfig = await callback(policyConfig);
expect(updatedPolicyConfig).toEqual(policyConfig);
});
});
describe('agent policy create callback', () => {
let logger: Logger;
let callback: PostAgentPolicyCreateCallback;
let policyConfig: GetAgentPoliciesResponseItem;
beforeEach(() => {
logger = loggingSystemMock.create().get('ingest_integration.test');
callback = getAgentPolicyCreateCallback(logger, appFeaturesService);
policyConfig = generator.generateAgentPolicy();
});
it('AppFeature disabled - returns an error if higher tier features are turned on in the policy', async () => {
appFeaturesService = createAppFeaturesServiceMock(
ALL_APP_FEATURE_KEYS.filter(
(key) => key !== AppFeatureSecurityKey.endpointAgentTamperProtection
)
);
callback = getAgentPolicyCreateCallback(logger, appFeaturesService);
policyConfig.is_protected = true;
await expect(() => callback(policyConfig)).rejects.toThrow(
'Agent Tamper Protection is not allowed in current environment'
);
});
it('AppFeature disabled - returns agent policy if higher tier features are turned off in the policy', async () => {
appFeaturesService = createAppFeaturesServiceMock(
ALL_APP_FEATURE_KEYS.filter(
(key) => key !== AppFeatureSecurityKey.endpointAgentTamperProtection
)
);
callback = getAgentPolicyCreateCallback(logger, appFeaturesService);
const updatedPolicyConfig = await callback(policyConfig);
expect(updatedPolicyConfig).toEqual(policyConfig);
});
it('AppFeature enabled - returns agent policy if higher tier features are turned on in the policy', async () => {
policyConfig.is_protected = true;
const updatedPolicyConfig = await callback(policyConfig);
expect(updatedPolicyConfig).toEqual(policyConfig);
});
it('AppFeature enabled - returns agent policy if higher tier features are turned off in the policy', async () => {
const updatedPolicyConfig = await callback(policyConfig);
expect(updatedPolicyConfig).toEqual(policyConfig);
});
});
describe('package policy update callback (when the license is below platinum)', () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;

View file

@ -16,6 +16,8 @@ import type {
} from '@kbn/fleet-plugin/server';
import type {
AgentPolicy,
NewAgentPolicy,
NewPackagePolicy,
PackagePolicy,
UpdatePackagePolicy,
@ -23,6 +25,10 @@ import type {
import type { CloudSetup } from '@kbn/cloud-plugin/server';
import type { InfoResponse } from '@elastic/elasticsearch/lib/api/types';
import { AppFeatureSecurityKey } from '@kbn/security-solution-features/keys';
import type {
PostAgentPolicyCreateCallback,
PostAgentPolicyUpdateCallback,
} from '@kbn/fleet-plugin/server/types';
import { validatePolicyAgainstAppFeatures } from './handlers/validate_policy_against_app_features';
import { validateEndpointPackagePolicy } from './handlers/validate_endpoint_package_policy';
import {
@ -291,6 +297,54 @@ export const getPackagePolicyPostCreateCallback = (
};
};
const throwAgentTamperProtectionUnavailableError = (
logger: Logger,
policyName?: string,
policyId?: string
): void => {
const agentTamperProtectionUnavailableError: Error & {
statusCode?: number;
apiPassThrough?: boolean;
} = new Error('Agent Tamper Protection is not allowed in current environment');
// Agent Policy Service will check for apiPassThrough and rethrow. Route handler will check for statusCode and overwrite.
agentTamperProtectionUnavailableError.statusCode = 403;
agentTamperProtectionUnavailableError.apiPassThrough = true;
logger.error(
`Policy [${policyName}:${policyId}] error: Agent Tamper Protection requires Complete Endpoint Security tier`
);
throw agentTamperProtectionUnavailableError;
};
export const getAgentPolicyCreateCallback = (
logger: Logger,
appFeatures: AppFeaturesService
): PostAgentPolicyCreateCallback => {
return async (agentPolicy: NewAgentPolicy): Promise<NewAgentPolicy> => {
if (
agentPolicy.is_protected &&
!appFeatures.isEnabled(AppFeatureSecurityKey.endpointAgentTamperProtection)
) {
throwAgentTamperProtectionUnavailableError(logger, agentPolicy.name, agentPolicy.id);
}
return agentPolicy;
};
};
export const getAgentPolicyUpdateCallback = (
logger: Logger,
appFeatures: AppFeaturesService
): PostAgentPolicyUpdateCallback => {
return async (agentPolicy: Partial<AgentPolicy>): Promise<Partial<AgentPolicy>> => {
if (
agentPolicy.is_protected &&
!appFeatures.isEnabled(AppFeatureSecurityKey.endpointAgentTamperProtection)
) {
throwAgentTamperProtectionUnavailableError(logger, agentPolicy.name, agentPolicy.id);
}
return agentPolicy;
};
};
export const getPackagePolicyDeleteCallback = (
exceptionsClient: ExceptionListClient | undefined,
savedObjectsClient: SavedObjectsClientContract | undefined

View file

@ -115,6 +115,7 @@ import {
} from '../common/entity_analytics/risk_engine';
import { isEndpointPackageV2 } from '../common/endpoint/utils/package_v2';
import { getAssistantTools } from './assistant/tools';
import { turnOffAgentPolicyFeatures } from './endpoint/migrations/turn_off_agent_policy_features';
export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract';
@ -555,6 +556,13 @@ export class Plugin implements ISecuritySolutionPlugin {
appFeaturesService,
logger
);
turnOffAgentPolicyFeatures(
core.elasticsearch.client.asInternalUser,
endpointFleetServicesFactory.asInternalUser(),
appFeaturesService,
logger
);
});
// License related start