Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -197,14 +197,14 @@ describe('When using EPM `get` services', () => {
});
});

it('should query and paginate SO using package name as filter', async () => {
it('should query and paginate SO using package name and NOT latest_revision:false filter', async () => {
await getPackageUsageStats({ savedObjectsClient: soClient, pkgName: 'system' });
expect(soClient.find).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
perPage: 10000,
filter: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name: system`,
filter: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name: system AND NOT ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.latest_revision: false`,
})
);
});
Expand All @@ -217,6 +217,71 @@ describe('When using EPM `get` services', () => {
package_policy_count: 4,
});
});

it('should exclude :prev (latest_revision:false) policies from the count', async () => {
const soClientPrev = savedObjectsClientMock.create();
// Mix of current policies and a :prev policy that would be returned before
// the filter fix — the filter now excludes it, so mock returns only current ones.
soClientPrev.find.mockResolvedValue({
page: 1,
per_page: 10000,
total: 2,
saved_objects: [
{
type: 'ingest-package-policies',
id: 'policy-1',
attributes: {
name: 'system-1',
namespace: 'default',
package: { name: 'system', title: 'System', version: '1.0.0' },
enabled: true,
policy_id: 'ap-1',
policy_ids: ['ap-1'],
inputs: [],
revision: 2,
created_at: '2020-01-01T00:00:00.000Z',
created_by: 'elastic',
updated_at: '2020-01-01T00:00:00.000Z',
updated_by: 'elastic',
},
references: [],
score: 0,
},
{
type: 'ingest-package-policies',
id: 'policy-2',
attributes: {
name: 'system-2',
namespace: 'default',
package: { name: 'system', title: 'System', version: '1.0.0' },
enabled: true,
policy_id: 'ap-2',
policy_ids: ['ap-2'],
inputs: [],
revision: 2,
created_at: '2020-01-01T00:00:00.000Z',
created_by: 'elastic',
updated_at: '2020-01-01T00:00:00.000Z',
updated_by: 'elastic',
},
references: [],
score: 0,
},
],
});

const result = await getPackageUsageStats({
savedObjectsClient: soClientPrev,
pkgName: 'system',
});

// 2 current policies, not 3 (the :prev one is excluded by the filter)
expect(result.package_policy_count).toBe(2);
// Verify the filter contains the NOT latest_revision:false clause
const [[callArgs]] = soClientPrev.find.mock.calls;
expect(callArgs.filter).toContain('NOT');
expect(callArgs.filter).toContain('latest_revision');
});
});

describe('getPackages', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ export const getPackageUsageStats = async ({

const filter = normalizeKuery(
packagePolicySavedObjectType,
`${packagePolicySavedObjectType}.package.name: ${pkgName}`
`${packagePolicySavedObjectType}.package.name: ${pkgName} AND NOT ${packagePolicySavedObjectType}.latest_revision: false`
);
const agentPolicyCount = new Set<string>();
// using saved Objects client directly, instead of the `list()` method of `package_policy` service
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* 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/server/mocks';

import { getPackagePoliciesCountByPackageName } from './package_policies_aggregation';

// The mock resolves to the legacy (non-space-aware) type, matching a self-managed deployment.
const MOCKED_SO_TYPE = 'ingest-package-policies';

jest.mock('../package_policy', () => ({
getPackagePolicySavedObjectType: jest.fn().mockResolvedValue('ingest-package-policies'),
}));

describe('getPackagePoliciesCountByPackageName', () => {
it('uses NOT latest_revision:false filter so policies without the field are included', async () => {
const soClient = savedObjectsClientMock.create();
soClient.find.mockResolvedValue({
page: 1,
per_page: 0,
total: 0,
saved_objects: [],
aggregations: {
count_by_package_name: { buckets: [] },
},
});

await getPackagePoliciesCountByPackageName(soClient);

expect(soClient.find).toHaveBeenCalledWith(
expect.objectContaining({
filter: `NOT ${MOCKED_SO_TYPE}.attributes.latest_revision:false`,
})
);
});

it('includes a size parameter in the terms aggregation to avoid ES default truncation at 10 buckets', async () => {
const soClient = savedObjectsClientMock.create();
soClient.find.mockResolvedValue({
page: 1,
per_page: 0,
total: 0,
saved_objects: [],
aggregations: {
count_by_package_name: { buckets: [] },
},
});

await getPackagePoliciesCountByPackageName(soClient);

expect(soClient.find).toHaveBeenCalledWith(
expect.objectContaining({
aggs: expect.objectContaining({
count_by_package_name: expect.objectContaining({
terms: expect.objectContaining({
size: expect.any(Number),
}),
}),
}),
})
);
const [[callArgs]] = soClient.find.mock.calls;
const termsSize = (callArgs.aggs as any)?.count_by_package_name?.terms?.size;
expect(termsSize).toBeGreaterThan(10);
});

it('returns a map of package name to count', async () => {
const soClient = savedObjectsClientMock.create();
soClient.find.mockResolvedValue({
page: 1,
per_page: 0,
total: 3,
saved_objects: [],
aggregations: {
count_by_package_name: {
buckets: [
{ key: 'nginx', doc_count: 2 },
{ key: 'system', doc_count: 1 },
],
},
},
});

const result = await getPackagePoliciesCountByPackageName(soClient);

expect(result).toEqual({ nginx: 2, system: 1 });
});

it('returns an empty object when there are no buckets', async () => {
const soClient = savedObjectsClientMock.create();
soClient.find.mockResolvedValue({
page: 1,
per_page: 0,
total: 0,
saved_objects: [],
aggregations: {
count_by_package_name: { buckets: [] },
},
});

const result = await getPackagePoliciesCountByPackageName(soClient);

expect(result).toEqual({});
});

it('returns an empty object when aggregations are absent', async () => {
const soClient = savedObjectsClientMock.create();
soClient.find.mockResolvedValue({
page: 1,
per_page: 0,
total: 0,
saved_objects: [],
});

const result = await getPackagePoliciesCountByPackageName(soClient);

expect(result).toEqual({});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import type { SavedObjectsClientContract } from '@kbn/core/server';

import { SO_SEARCH_LIMIT } from '../../../common';

import { getPackagePolicySavedObjectType } from '../package_policy';

export async function getPackagePoliciesCountByPackageName(soClient: SavedObjectsClientContract) {
Expand All @@ -18,11 +20,17 @@ export async function getPackagePoliciesCountByPackageName(soClient: SavedObject
>({
type: savedObjectType,
perPage: 0,
filter: `${savedObjectType}.attributes.latest_revision:true`,
// Use NOT false instead of :true so that policies without the field
// (8.x policies where latest_revision was never persisted to ES) are
// treated as current revisions and included in the count.
filter: `NOT ${savedObjectType}.attributes.latest_revision:false`,
aggs: {
count_by_package_name: {
terms: {
field: `${savedObjectType}.attributes.package.name`,
// Without an explicit size, ES defaults to 10 buckets and silently
// truncates results when more than 10 distinct package names exist.
size: SO_SEARCH_LIMIT,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1554,6 +1554,42 @@ describe('Package policy service', () => {
});

describe('list', () => {
it('should use NOT latest_revision:false filter to include 8.x policies without the field', async () => {
const soClient = createSavedObjectClientMock();
soClient.find.mockResolvedValueOnce({
total: 0,
page: 1,
per_page: 20,
saved_objects: [],
});

await packagePolicyService.list(soClient, {});

expect(soClient.find).toHaveBeenCalledWith(
expect.objectContaining({
filter: `NOT ${LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.latest_revision:false`,
})
);
});

it('should combine user kuery with NOT latest_revision:false filter', async () => {
const soClient = createSavedObjectClientMock();
soClient.find.mockResolvedValueOnce({
total: 0,
page: 1,
per_page: 20,
saved_objects: [],
});

await packagePolicyService.list(soClient, { kuery: 'ingest-package-policies.name: nginx' });

expect(soClient.find).toHaveBeenCalledWith(
expect.objectContaining({
filter: `NOT ${LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.latest_revision:false AND (ingest-package-policies.attributes.name: nginx)`,
})
);
});

it('should call audit logger', async () => {
const soClient = createSavedObjectClientMock();
soClient.find.mockResolvedValueOnce({
Expand Down Expand Up @@ -7739,7 +7775,7 @@ describe('Package policy service', () => {
sortField: 'created_at',
sortOrder: 'asc',
fields: [],
filter: `${LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.latest_revision:true`,
filter: `NOT ${LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.latest_revision:false`,
})
);
});
Expand All @@ -7759,7 +7795,7 @@ describe('Package policy service', () => {
sortField: 'created_at',
sortOrder: 'asc',
fields: [],
filter: `${LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.latest_revision:true AND (one=two)`,
filter: `NOT ${LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.latest_revision:false AND (one=two)`,
})
);
});
Expand Down Expand Up @@ -7813,7 +7849,7 @@ describe('Package policy service', () => {
sortField: 'created_at',
sortOrder: 'asc',
fields: [],
filter: `${LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.latest_revision:true`,
filter: `NOT ${LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.latest_revision:false`,
})
);
});
Expand All @@ -7831,7 +7867,7 @@ describe('Package policy service', () => {
sortField: 'created_at',
sortOrder: 'asc',
fields: [],
filter: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.latest_revision:true`,
filter: `NOT ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.latest_revision:false`,
})
);
});
Expand All @@ -7852,7 +7888,7 @@ describe('Package policy service', () => {
perPage: 12,
sortField: 'updated_by',
sortOrder: 'desc',
filter: `${LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.latest_revision:true AND (one=two)`,
filter: `NOT ${LEGACY_PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.latest_revision:false AND (one=two)`,
})
);
});
Expand Down
Loading
Loading