From 646977dd6156da9930e51f3597c045cc63bba109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cjeramysoucy=E2=80=9D?= Date: Thu, 23 Apr 2026 09:56:18 -0400 Subject: [PATCH 1/3] Resolves issue with date formatting, adds unit tests --- .../common/services/agent_cm_to_yaml.test.ts | 105 ++++++++++++++++++ .../fleet/common/services/agent_cm_to_yaml.ts | 2 +- .../full_agent_policy_to_yaml.test.ts | 35 ++++++ .../services/full_agent_policy_to_yaml.ts | 6 +- 4 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 x-pack/platform/plugins/shared/fleet/common/services/agent_cm_to_yaml.test.ts 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; From 939ccc130f3d7e8d0515f21b8c7eda9057f05b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cjeramysoucy=E2=80=9D?= Date: Thu, 23 Apr 2026 10:23:10 -0400 Subject: [PATCH 2/3] Updates parse to use 1.1 schema --- .../server/services/epm/agent/agent.test.ts | 21 +++++++++++++++++++ .../fleet/server/services/epm/agent/agent.ts | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.test.ts index ad99eb8af9ee6..2ac7162f5cd14 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.test.ts @@ -521,6 +521,27 @@ paths: ); }); + it('should parse date-only values as YAML 1.1 timestamps (Date objects), not RFC3339 strings', () => { + // Integrations like microsoft_defender_cloud use date-only API versions (e.g. 2021-06-01). + // The compiled template is parsed with yaml-1.1 schema so that unquoted YYYY-MM-DD values + // are treated as YAML 1.1 timestamps (Date objects), matching the original js-yaml behavior. + // This allows downstream yaml-1.1 serialization to emit them as proper timestamps rather + // than as plain strings that the Elastic Agent's YAML 1.1 parser might misinterpret. + const streamTemplate = ` +state: + api_version: 2021-06-01 + preview_version: 2019-01-01-preview +`; + + const output = compileTemplate({}, getMockedMetaVariable(), streamTemplate); + + // YAML 1.1 parses unquoted YYYY-MM-DD as a timestamp → Date object + expect(output.state.api_version).toBeInstanceOf(Date); + expect((output.state.api_version as Date).toISOString().startsWith('2021-06-01')).toBe(true); + // A -preview suffix is not a valid YAML 1.1 timestamp, so it stays a string + expect(output.state.preview_version).toBe('2019-01-01-preview'); + }); + it('should inject package meta varaible', () => { const streamTemplate = ` input: {{_meta.input.id}} diff --git a/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.ts b/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.ts index fde5b00883cd0..144e4dbe5e70a 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.ts @@ -128,7 +128,7 @@ export function compileTemplate( ); try { - const yamlFromCompiledTemplate = parse(compiledTemplate); + const yamlFromCompiledTemplate = parse(compiledTemplate, { schema: 'yaml-1.1' }); // Hack to keep empty string ('') values around in the end yaml because // `load` replaces empty strings with null From 6e8bd20dc4086a628572a68be9603ef74d028f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cjeramysoucy=E2=80=9D?= Date: Thu, 23 Apr 2026 10:29:40 -0400 Subject: [PATCH 3/3] Revert "Updates parse to use 1.1 schema" This reverts commit 939ccc130f3d7e8d0515f21b8c7eda9057f05b16. --- .../server/services/epm/agent/agent.test.ts | 21 ------------------- .../fleet/server/services/epm/agent/agent.ts | 2 +- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.test.ts index 2ac7162f5cd14..ad99eb8af9ee6 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.test.ts @@ -521,27 +521,6 @@ paths: ); }); - it('should parse date-only values as YAML 1.1 timestamps (Date objects), not RFC3339 strings', () => { - // Integrations like microsoft_defender_cloud use date-only API versions (e.g. 2021-06-01). - // The compiled template is parsed with yaml-1.1 schema so that unquoted YYYY-MM-DD values - // are treated as YAML 1.1 timestamps (Date objects), matching the original js-yaml behavior. - // This allows downstream yaml-1.1 serialization to emit them as proper timestamps rather - // than as plain strings that the Elastic Agent's YAML 1.1 parser might misinterpret. - const streamTemplate = ` -state: - api_version: 2021-06-01 - preview_version: 2019-01-01-preview -`; - - const output = compileTemplate({}, getMockedMetaVariable(), streamTemplate); - - // YAML 1.1 parses unquoted YYYY-MM-DD as a timestamp → Date object - expect(output.state.api_version).toBeInstanceOf(Date); - expect((output.state.api_version as Date).toISOString().startsWith('2021-06-01')).toBe(true); - // A -preview suffix is not a valid YAML 1.1 timestamp, so it stays a string - expect(output.state.preview_version).toBe('2019-01-01-preview'); - }); - it('should inject package meta varaible', () => { const streamTemplate = ` input: {{_meta.input.id}} diff --git a/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.ts b/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.ts index 144e4dbe5e70a..fde5b00883cd0 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/epm/agent/agent.ts @@ -128,7 +128,7 @@ export function compileTemplate( ); try { - const yamlFromCompiledTemplate = parse(compiledTemplate, { schema: 'yaml-1.1' }); + const yamlFromCompiledTemplate = parse(compiledTemplate); // Hack to keep empty string ('') values around in the end yaml because // `load` replaces empty strings with null