diff --git a/src/services/override-merger.ts b/src/services/override-merger.ts index 53a91cd..5bc4e25 100644 --- a/src/services/override-merger.ts +++ b/src/services/override-merger.ts @@ -58,6 +58,18 @@ const GRANDCHILD_OVERRIDE_MAP: Partial key.toLowerCase() === lowerName + ); +} + /** * Apply environment overrides from OverrideConfig to a resource JSON payload. * Deep-merges matching override properties using case-insensitive key matching. diff --git a/src/services/publish-service.ts b/src/services/publish-service.ts index 121d563..ed9604c 100644 --- a/src/services/publish-service.ts +++ b/src/services/publish-service.ts @@ -27,6 +27,7 @@ import { generateDryRunReport, DryRunReport } from './dry-run-reporter.js'; import { computeDeleteActions } from './delete-unmatched-service.js'; import { computeGitDiff } from './git-diff-service.js'; import { scanForRedactionMarkers } from './secret-redaction-guard.js'; +import { hasNamedValueOverride } from './override-merger.js'; import { REDACTION_MARKER } from './secret-redactor.js'; /** @@ -446,18 +447,6 @@ function splitNamedValues( return { namedValues, otherTier1 }; } -/** - * Check whether a named value has an explicit override entry. - * Uses case-insensitive matching to align with override-merger behavior. - */ -function hasNamedValueOverride(name: string, overrides?: OverrideConfig): boolean { - if (!overrides?.namedValues) return false; - const lowerName = name.toLowerCase(); - return Object.keys(overrides.namedValues).some( - (key) => key.toLowerCase() === lowerName - ); -} - /** * Separates Backend descriptors that are pool backends (properties.type === "Pool") * from all other descriptors in the supplied list. diff --git a/src/services/secret-redaction-guard.ts b/src/services/secret-redaction-guard.ts index cad1351..ba1a844 100644 --- a/src/services/secret-redaction-guard.ts +++ b/src/services/secret-redaction-guard.ts @@ -22,10 +22,12 @@ import type { IArtifactStore } from '../clients/iartifact-store.js'; import type { PublishConfig } from '../models/config.js'; import type { ResourceDescriptor } from '../models/types.js'; import { ResourceType } from '../models/resource-types.js'; -import { applyOverrides } from './override-merger.js'; +import { applyOverrides, hasNamedValueOverride } from './override-merger.js'; import { POLICY_TYPES } from './resource-publisher.js'; import { REDACTION_MARKER } from './secret-redactor.js'; import { buildResourceLabel } from '../lib/resource-uri.js'; +import { getNamePart } from '../lib/resource-path.js'; +import { isAutoGeneratedId } from '../lib/auto-generated.js'; /** * A single artifact that still contains a redaction marker after overrides. @@ -103,6 +105,16 @@ async function scanNamedValue( config: PublishConfig, descriptor: ResourceDescriptor ): Promise { + // Auto-generated named values (e.g. Logger-Credentials--) are skipped + // during publish because APIM recreates them when the referencing resource + // (Logger, etc.) is published. splitNamedValues() drops them unless the user + // supplies an explicit override, so scanning them here would flag markers in + // artifacts that are never sent to APIM. Mirror the publish filter exactly. + const name = getNamePart(descriptor.nameParts, 0); + if (isAutoGeneratedId(name) && !hasNamedValueOverride(name, config.overrides)) { + return undefined; + } + const json = await store.readResource(config.sourceDir, descriptor); if (!json) { return undefined; diff --git a/tests/unit/services/secret-redaction-guard.test.ts b/tests/unit/services/secret-redaction-guard.test.ts index cc58e81..381bb9a 100644 --- a/tests/unit/services/secret-redaction-guard.test.ts +++ b/tests/unit/services/secret-redaction-guard.test.ts @@ -123,6 +123,55 @@ describe('secret-redaction-guard', () => { expect(findings).toEqual([]); }); + it('ignores an auto-generated named value (logger credential) with no override', async () => { + const store = createMockStore(); + store.readResource.mockResolvedValue({ + properties: { secret: true, value: REDACTION_MARKER }, + }); + + const autoGeneratedDescriptor: ResourceDescriptor = { + type: ResourceType.NamedValue, + nameParts: ['6a46928afa1f751d044d805e'], + }; + + const findings = await scanForRedactionMarkers(store, testConfig, [autoGeneratedDescriptor]); + + // APIM recreates these when the referencing logger publishes, so publish + // skips them and the guard must not flag them. It should skip before any read. + expect(findings).toEqual([]); + expect(store.readResource).not.toHaveBeenCalled(); + }); + + it('scans an auto-generated named value when an explicit override exists', async () => { + const store = createMockStore(); + store.readResource.mockResolvedValue({ + properties: { secret: true, value: REDACTION_MARKER }, + }); + + const autoGeneratedDescriptor: ResourceDescriptor = { + type: ResourceType.NamedValue, + nameParts: ['6a46928afa1f751d044d805e'], + }; + + // Override targets the auto-generated name but does not replace the value, + // so the marker remains and the resource is published — it must be scanned. + const configWithOverride: PublishConfig = { + ...testConfig, + overrides: { + namedValues: { + '6a46928afa1f751d044d805e': { properties: { tags: ['keep'] } }, + }, + }, + }; + + const findings = await scanForRedactionMarkers(store, configWithOverride, [ + autoGeneratedDescriptor, + ]); + + expect(findings).toHaveLength(1); + expect(findings[0].location).toBe('properties.value'); + }); + it('does not flag a marker that an override replaces with clean content', async () => { const store = createMockStore(); store.readContent.mockResolvedValue({