[Security Solution][Risk Score]Code changes for limiting the transformID length to 36 characters (#213405)

## Summary

The code changes in this PR ensure that the transform ID is limited to
36 characters when creating or updating the transform for risk-score.

This adjustment aligns with ES constraint on transform ID length.


## Test Steps

1. Create a new namespace with a very long name. Ex :
`namespace_that_stretches_farther_than_the_universe_and_beyond_like_buzz`
🚀
2. Enable the Risk Score in the new namespace. It should successfully
get enabled.
3. Check the transform that was created (using dev tools)

```
GET _transform/risk_score_latest_transform_*?filter_path=transforms.id,transforms._meta.space_id
```

Output 


![image](https://github.com/user-attachments/assets/3b5d5e67-cddf-4c6a-b8ff-675517c123b2)

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: Mark Hopkin <mark.hopkin@elastic.co>
This commit is contained in:
Abhishek Bhatia 2025-03-20 11:23:55 +05:30 committed by GitHub
parent bfe2db38e3
commit a3f89ec2c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 40 additions and 5 deletions

View file

@ -12,6 +12,7 @@ describe('getTransformOptions', () => {
const options = getTransformOptions({
dest: 'dest',
source: ['source'],
namespace: 'tests',
});
expect(options).toMatchInlineSnapshot(`
@ -19,6 +20,7 @@ describe('getTransformOptions', () => {
"_meta": Object {
"managed": true,
"managed_by": "security-entity-analytics",
"space_id": "tests",
"version": 3,
},
"dest": Object {

View file

@ -164,9 +164,11 @@ export type TransformOptions = Omit<TransformPutTransformRequest, 'transform_id'
export const getTransformOptions = ({
dest,
source,
namespace,
}: {
dest: string;
source: string[];
namespace: string;
}): Omit<TransformPutTransformRequest, 'transform_id'> => ({
dest: {
index: dest,
@ -206,5 +208,6 @@ export const getTransformOptions = ({
version: 3, // When this field is updated we automatically update the transform
managed: true, // Metadata that identifies the transform. It has no functionality
managed_by: 'security-entity-analytics', // Metadata that identifies the transform. It has no functionality
space_id: namespace, // Metadata that identifies the space where the transform is running. Helps in debugging as the original transformid could be hashed if longer than 64 characters
},
});

View file

@ -45,6 +45,7 @@ const totalFieldsLimit = 1000;
describe('RiskScoreDataClient', () => {
let riskScoreDataClient: RiskScoreDataClient;
let riskScoreDataClientWithNameSpace: RiskScoreDataClient;
let riskScoreDataClientWithLongNameSpace: RiskScoreDataClient;
let mockSavedObjectClient: ReturnType<typeof savedObjectsClientMock.create>;
beforeEach(() => {
@ -60,6 +61,8 @@ describe('RiskScoreDataClient', () => {
riskScoreDataClient = new RiskScoreDataClient(options);
const optionsWithNamespace = { ...options, namespace: 'space-1' };
riskScoreDataClientWithNameSpace = new RiskScoreDataClient(optionsWithNamespace);
const optionsWithLongNamespace = { ...options, namespace: 'a_a-'.repeat(200) };
riskScoreDataClientWithLongNameSpace = new RiskScoreDataClient(optionsWithLongNamespace);
});
afterEach(() => {
@ -103,6 +106,10 @@ describe('RiskScoreDataClient', () => {
assertIndex('space-1');
assertTransform('space-1');
// Space with more than 36 characters
await riskScoreDataClientWithLongNameSpace.init();
assertTransform('a_a-'.repeat(200));
expect(
(createOrUpdateComponentTemplate as jest.Mock).mock.lastCall[0].template.template
).toMatchSnapshot();
@ -443,7 +450,7 @@ const assertTransform = (namespace: string) => {
field: '@timestamp',
},
},
transform_id: `risk_score_latest_transform_${namespace}`,
transform_id: transforms.getLatestTransformId(namespace),
settings: {
unattended: true,
},
@ -451,6 +458,7 @@ const assertTransform = (namespace: string) => {
version: 3,
managed: true,
managed_by: 'security-entity-analytics',
space_id: namespace,
},
},
});

View file

@ -199,6 +199,7 @@ export class RiskScoreDataClient {
...getTransformOptions({
dest: getRiskScoreLatestIndex(namespace),
source: [indexPatterns.alias],
namespace: this.options.namespace,
}),
},
});
@ -363,6 +364,7 @@ export class RiskScoreDataClient {
...getTransformOptions({
dest: getRiskScoreLatestIndex(namespace),
source: [indexPatterns.alias],
namespace: this.options.namespace,
}),
},
});

View file

@ -19,6 +19,7 @@ import {
scheduleLatestTransformNow,
scheduleTransformNow,
upgradeLatestTransformIfNeeded,
getLatestTransformId,
} from './transforms';
const transformId = 'test_transform_id';
@ -48,6 +49,7 @@ const timeSeriesIndex = getRiskScoreTimeSeriesIndex('tests');
const transformConfig = getTransformOptions({
dest: latestIndex,
source: [timeSeriesIndex],
namespace: 'tests',
});
const updatedTransformsMock = {
@ -206,4 +208,12 @@ describe('transforms utils', () => {
expect(esClient.transform.putTransform).toHaveBeenCalled();
});
});
describe('checkTransformNameLength', () => {
it('should limit the length of tranformId to less than or equal 64 characters', async () => {
const longTransformId = 'a_a-'.repeat(1000);
const response = await getLatestTransformId(longTransformId);
expect(response.length).toBeLessThanOrEqual(36);
});
});
});

View file

@ -14,6 +14,7 @@ import type {
TransformGetTransformStatsTransformStats,
AcknowledgedResponseBase,
} from '@elastic/elasticsearch/lib/api/types';
import murmurhash from 'murmurhash';
import {
getRiskScoreLatestIndex,
getRiskScoreTimeSeriesIndex,
@ -116,8 +117,15 @@ export const reinstallTransform = async ({
});
};
export const getLatestTransformId = (namespace: string): string =>
`risk_score_latest_transform_${namespace}`;
export const getLatestTransformId = (namespace: string): string => {
const maxTransformId = 64;
const prefix = `risk_score_latest_transform_`;
const fullName = `${prefix}${namespace}`;
const processedNamespace =
fullName.length > maxTransformId ? murmurhash.v3(namespace).toString(16) : namespace;
return `${prefix}${processedNamespace}`;
};
const hasTransformStarted = (transformStats: TransformGetTransformStatsTransformStats): boolean => {
return transformStats.state === 'indexing' || transformStats.state === 'started';
@ -174,6 +182,7 @@ export const upgradeLatestTransformIfNeeded = async ({
const newConfig = getTransformOptions({
dest: latestIndex,
source: [timeSeriesIndex],
namespace,
});
if (isTransformOutdated(response.transforms[0], newConfig)) {

View file

@ -8,6 +8,7 @@
import expect from '@kbn/expect';
import { riskEngineConfigurationTypeName } from '@kbn/security-solution-plugin/server/lib/entity_analytics/risk_engine/saved_object';
import { getLatestTransformId } from '@kbn/security-solution-plugin/server/lib/entity_analytics/utils/transforms';
import { riskEngineRouteHelpersFactory } from '../../utils';
import { FtrProviderContext } from '../../../../ftr_provider_context';
@ -70,7 +71,7 @@ export default ({ getService }: FtrProviderContext) => {
const indexTemplateName = '.risk-score.risk-score-default-index-template';
const dataStreamName = 'risk-score.risk-score-default';
const latestIndexName = 'risk-score.risk-score-latest-default';
const transformId = 'risk_score_latest_transform_default';
const transformId = getLatestTransformId('default');
const defaultPipeline =
'entity_analytics_create_eventIngest_from_timestamp-pipeline-default';
@ -350,7 +351,7 @@ export default ({ getService }: FtrProviderContext) => {
const indexTemplateName = `.risk-score.risk-score-${customSpaceName}-index-template`;
const dataStreamName = `risk-score.risk-score-${customSpaceName}`;
const latestIndexName = `risk-score.risk-score-latest-${customSpaceName}`;
const transformId = `risk_score_latest_transform_${customSpaceName}`;
const transformId = getLatestTransformId(customSpaceName);
const defaultPipeline = `entity_analytics_create_eventIngest_from_timestamp-pipeline-${customSpaceName}`;
await riskEngineRoutesWithNamespace.init();