diff --git a/x-pack/platform/plugins/shared/fleet/common/services/agent_cm_to_yaml.test.ts b/x-pack/platform/plugins/shared/fleet/common/services/agent_cm_to_yaml.test.ts new file mode 100644 index 0000000000000..f8003fa65bc0a --- /dev/null +++ b/x-pack/platform/plugins/shared/fleet/common/services/agent_cm_to_yaml.test.ts @@ -0,0 +1,105 @@ +/* + * 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 * as yaml from 'yaml'; + +import type { FullAgentConfigMap } from '../types/models/agent_cm'; + +import { fullAgentConfigMapToYaml } from './agent_cm_to_yaml'; + +function makeConfigMap(overrides?: Partial): FullAgentConfigMap { + return { + apiVersion: 'v1', + kind: 'ConfigMap', + metadata: { + name: 'agent-node-datastreams', + namespace: 'kube-system', + labels: { 'k8s-app': 'elastic-agent' }, + }, + data: { + 'agent.yml': {} as any, + }, + ...overrides, + }; +} + +describe('fullAgentConfigMapToYaml', () => { + it('serializes a ConfigMap to valid YAML', () => { + const cm = makeConfigMap(); + const result = fullAgentConfigMapToYaml(cm, yaml); + + const parsed = yaml.parse(result); + expect(parsed.apiVersion).toBe('v1'); + expect(parsed.kind).toBe('ConfigMap'); + expect(parsed.metadata.name).toBe('agent-node-datastreams'); + expect(parsed.metadata.namespace).toBe('kube-system'); + expect(parsed.metadata.labels['k8s-app']).toBe('elastic-agent'); + }); + + it('orders top-level keys as apiVersion, kind, metadata, data', () => { + // Supply keys in reverse order to confirm sorting is applied + const cm = { + data: { 'agent.yml': {} as any }, + metadata: { + name: 'agent-node-datastreams', + namespace: 'kube-system', + labels: { 'k8s-app': 'elastic-agent' }, + }, + kind: 'ConfigMap', + apiVersion: 'v1', + } as FullAgentConfigMap; + + const result = fullAgentConfigMapToYaml(cm, yaml); + + const lines = result.split('\n').filter((l) => /^\w/.test(l)); + expect(lines[0]).toMatch(/^apiVersion/); + expect(lines[1]).toMatch(/^kind/); + expect(lines[2]).toMatch(/^metadata/); + expect(lines[3]).toMatch(/^data/); + }); + + it('quotes date-only strings to prevent YAML 1.1 timestamp interpretation by the agent', () => { + // The Elastic Agent parses policy YAML with a YAML 1.1 parser, which treats unquoted + // YYYY-MM-DD values as timestamps and converts them to RFC3339 (2021-06-01T00:00:00Z). + const cm = makeConfigMap({ + data: { + 'agent.yml': { + state: { + assessment_api_version: '2021-06-01', + sub_assessment_api_version: '2019-01-01-preview', + }, + } as any, + }, + }); + + const result = fullAgentConfigMapToYaml(cm, yaml); + + expect(result).toContain('assessment_api_version: "2021-06-01"'); + expect(result).toContain('sub_assessment_api_version: 2019-01-01-preview'); + }); + + it('preserves nested agent policy data unchanged', () => { + const cm = makeConfigMap({ + data: { + 'agent.yml': { + id: 'policy-id', + outputs: { + default: { type: 'elasticsearch', hosts: ['http://localhost:9200'] }, + }, + inputs: [{ id: 'input-1', type: 'logfile', streams: [] }], + } as any, + }, + }); + + const result = fullAgentConfigMapToYaml(cm, yaml); + const parsed = yaml.parse(result); + + expect(parsed.data['agent.yml'].id).toBe('policy-id'); + expect(parsed.data['agent.yml'].outputs.default.type).toBe('elasticsearch'); + expect(parsed.data['agent.yml'].inputs[0].id).toBe('input-1'); + }); +}); diff --git a/x-pack/platform/plugins/shared/fleet/common/services/agent_cm_to_yaml.ts b/x-pack/platform/plugins/shared/fleet/common/services/agent_cm_to_yaml.ts index 173a977d3c5ca..1bcb4c4740b98 100644 --- a/x-pack/platform/plugins/shared/fleet/common/services/agent_cm_to_yaml.ts +++ b/x-pack/platform/plugins/shared/fleet/common/services/agent_cm_to_yaml.ts @@ -14,5 +14,5 @@ const CM_KEYS_ORDER = ['apiVersion', 'kind', 'metadata', 'data']; export const fullAgentConfigMapToYaml = (policy: FullAgentConfigMap, yaml: YamlModule): string => { const sortCmKeys = createYamlKeysSorter(CM_KEYS_ORDER, yaml); - return toYaml(policy, { sortMapEntries: sortCmKeys, strict: false }, yaml); + return toYaml(policy, { sortMapEntries: sortCmKeys, strict: false, schema: 'yaml-1.1' }, yaml); }; diff --git a/x-pack/platform/plugins/shared/fleet/common/services/full_agent_policy_to_yaml.test.ts b/x-pack/platform/plugins/shared/fleet/common/services/full_agent_policy_to_yaml.test.ts index fb1acd0c7532a..d4a431db81a32 100644 --- a/x-pack/platform/plugins/shared/fleet/common/services/full_agent_policy_to_yaml.test.ts +++ b/x-pack/platform/plugins/shared/fleet/common/services/full_agent_policy_to_yaml.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +import * as yaml from 'yaml'; + import type { FullAgentPolicy } from '../types'; import { fullAgentPolicyToYaml } from './full_agent_policy_to_yaml'; @@ -65,4 +67,37 @@ describe('fullAgentPolicyToYaml', () => { `"{\\"id\\":\\"1234\\",\\"outputs\\":{\\"default\\":{\\"type\\":\\"elasticsearch\\",\\"hosts\\":[\\"http://localhost:9200\\"]}},\\"inputs\\":[{\\"id\\":\\"test_input-secrets-abcd1234\\",\\"revision\\":1,\\"name\\":\\"secrets-1\\",\\"type\\":\\"test_input\\",\\"data_stream\\":{\\"namespace\\":\\"default\\"},\\"use_output\\":\\"default\\",\\"package_policy_id\\":\\"abcd1234\\",\\"package_var_secret\\":\\"\${SECRET_0}\\",\\"input_var_secret\\":\\"\${SECRET_1}\\",\\"streams\\":[{\\"id\\":\\"test_input-secrets.log-abcd1234\\",\\"data_stream\\":{\\"type\\":\\"logs\\",\\"dataset\\":\\"secrets.log\\"},\\"package_var_secret\\":\\"\${SECRET_0}\\",\\"input_var_secret\\":\\"\${SECRET_1}\\",\\"stream_var_secret\\":\\"\${SECRET_2}\\"}],\\"meta\\":{\\"package\\":{\\"name\\":\\"secrets\\",\\"version\\":\\"1.0.0\\"}}}],\\"secret_references\\":[{\\"id\\":\\"secret-id-1\\"},{\\"id\\":\\"secret-id-2\\"},{\\"id\\":\\"secret-id-3\\"}],\\"revision\\":2,\\"agent\\":{},\\"signed\\":{},\\"output_permissions\\":{},\\"fleet\\":{}}"` ); }); + + it('should quote date-only strings to prevent YAML 1.1 timestamp interpretation by the agent', () => { + // Integrations like microsoft_defender_cloud use date-only API versions (e.g. 2021-06-01). + // The Elastic Agent parses policy YAML with a YAML 1.1 parser, which treats unquoted + // YYYY-MM-DD values as timestamps and converts them to RFC3339 (2021-06-01T00:00:00Z). + // The yaml-1.1 schema in Document options forces these values to be quoted. + const policy = { + id: 'test-policy', + outputs: {}, + inputs: [ + { + id: 'test-input', + type: 'cel', + streams: [ + { + id: 'test-stream', + state: { + assessment_api_version: '2021-06-01', + sub_assessment_api_version: '2019-01-01-preview', + }, + }, + ], + }, + ], + revision: 1, + agent: {}, + } as unknown as FullAgentPolicy; + + const result = fullAgentPolicyToYaml(policy, yaml); + + expect(result).toContain('assessment_api_version: "2021-06-01"'); + expect(result).toContain('sub_assessment_api_version: 2019-01-01-preview'); + }); }); diff --git a/x-pack/platform/plugins/shared/fleet/common/services/full_agent_policy_to_yaml.ts b/x-pack/platform/plugins/shared/fleet/common/services/full_agent_policy_to_yaml.ts index 23c706522b49c..f908b2d39310f 100644 --- a/x-pack/platform/plugins/shared/fleet/common/services/full_agent_policy_to_yaml.ts +++ b/x-pack/platform/plugins/shared/fleet/common/services/full_agent_policy_to_yaml.ts @@ -35,7 +35,11 @@ export const fullAgentPolicyToYaml = ( apiKey?: string ): string => { const sortYamlKeys = createYamlKeysSorter(POLICY_KEYS_ORDER, yaml); - const yamlText = toYaml(policy, { sortMapEntries: sortYamlKeys, strict: false }, yaml); + const yamlText = toYaml( + policy, + { sortMapEntries: sortYamlKeys, strict: false, schema: 'yaml-1.1' }, + yaml + ); const formattedYml = apiKey ? replaceApiKey(yamlText, apiKey) : yamlText; if (!policy?.secret_references?.length) return formattedYml;