From c66ffdb4332892c8b832265f6903c802322107a7 Mon Sep 17 00:00:00 2001 From: Alan Pena Date: Sat, 28 Mar 2026 01:36:27 -0400 Subject: [PATCH] fix(policies): guard extractPresetEntries against null/undefined input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit extractPresetEntries() crashes with TypeError when called with null or undefined because it calls .match() on the input without a falsy check. While applyPreset() guards against this via loadPreset() returning null first, the function is exported and callable directly — any caller passing an unexpected falsy value hits an unhandled crash. Add an early return for falsy input, consistent with the existing parseCurrentPolicy() pattern which already guards against null/empty. Add 15 regression tests covering both extractPresetEntries (8 tests) and parseCurrentPolicy (7 tests), which previously had zero direct test coverage despite being exported pure functions. --- bin/lib/policies.js | 1 + test/policies.test.js | 115 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/bin/lib/policies.js b/bin/lib/policies.js index 145ad85e4..b17e65570 100644 --- a/bin/lib/policies.js +++ b/bin/lib/policies.js @@ -63,6 +63,7 @@ function getPresetEndpoints(content) { * `preset:` metadata header. */ function extractPresetEntries(presetContent) { + if (!presetContent) return null; const npMatch = presetContent.match(/^network_policies:\n([\s\S]*)$/m); if (!npMatch) return null; return npMatch[1].trimEnd(); diff --git a/test/policies.test.js b/test/policies.test.js index a3050435e..bf3f18db9 100644 --- a/test/policies.test.js +++ b/test/policies.test.js @@ -136,6 +136,121 @@ describe("policies", () => { }); }); + describe("extractPresetEntries", () => { + it("returns null for null input", () => { + expect(policies.extractPresetEntries(null)).toBe(null); + }); + + it("returns null for undefined input", () => { + expect(policies.extractPresetEntries(undefined)).toBe(null); + }); + + it("returns null for empty string", () => { + expect(policies.extractPresetEntries("")).toBe(null); + }); + + it("returns null when no network_policies section exists", () => { + const content = "preset:\n name: test\n description: test preset"; + expect(policies.extractPresetEntries(content)).toBe(null); + }); + + it("extracts indented entries from network_policies section", () => { + const content = [ + "preset:", + " name: test", + "", + "network_policies:", + " test_rule:", + " name: test_rule", + " endpoints:", + " - host: example.com", + " port: 443", + ].join("\n"); + const entries = policies.extractPresetEntries(content); + expect(entries).toContain("test_rule:"); + expect(entries).toContain("host: example.com"); + expect(entries).toContain("port: 443"); + }); + + it("strips trailing whitespace from extracted entries", () => { + const content = "network_policies:\n rule:\n name: rule\n\n\n"; + const entries = policies.extractPresetEntries(content); + expect(entries).not.toMatch(/\n$/); + }); + + it("works on every real preset file", () => { + for (const p of policies.listPresets()) { + const content = policies.loadPreset(p.name); + const entries = policies.extractPresetEntries(content); + expect(entries).toBeTruthy(); + expect(entries).toContain("endpoints:"); + } + }); + + it("does not include preset metadata header", () => { + const content = [ + "preset:", + " name: test", + " description: desc", + "", + "network_policies:", + " rule:", + " name: rule", + ].join("\n"); + const entries = policies.extractPresetEntries(content); + expect(entries).not.toContain("preset:"); + expect(entries).not.toContain("description:"); + }); + }); + + describe("parseCurrentPolicy", () => { + it("returns empty string for null input", () => { + expect(policies.parseCurrentPolicy(null)).toBe(""); + }); + + it("returns empty string for undefined input", () => { + expect(policies.parseCurrentPolicy(undefined)).toBe(""); + }); + + it("returns empty string for empty string input", () => { + expect(policies.parseCurrentPolicy("")).toBe(""); + }); + + it("strips metadata header before --- separator", () => { + const raw = [ + "Version: 3", + "Hash: abc123", + "Updated: 2026-03-26", + "---", + "version: 1", + "", + "network_policies:", + " rule: {}", + ].join("\n"); + const result = policies.parseCurrentPolicy(raw); + expect(result).toBe("version: 1\n\nnetwork_policies:\n rule: {}"); + expect(result).not.toContain("Hash:"); + expect(result).not.toContain("Updated:"); + }); + + it("returns raw content when no --- separator exists", () => { + const raw = "version: 1\nnetwork_policies:\n rule: {}"; + expect(policies.parseCurrentPolicy(raw)).toBe(raw); + }); + + it("trims whitespace around extracted YAML", () => { + const raw = "Header: value\n---\n \nversion: 1\n "; + const result = policies.parseCurrentPolicy(raw); + expect(result).toBe("version: 1"); + }); + + it("handles --- appearing as first line", () => { + const raw = "---\nversion: 1\nnetwork_policies: {}"; + const result = policies.parseCurrentPolicy(raw); + expect(result).toBe("version: 1\nnetwork_policies: {}"); + }); + }); + describe("mergePresetIntoPolicy", () => { const sampleEntries = " - host: example.com\n allow: true";