Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/services/override-merger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ const GRANDCHILD_OVERRIDE_MAP: Partial<Record<ResourceType, {
[ResourceType.ApiOperationPolicy]: { parentSection: 'apis', childKey: 'operations', grandchildKey: 'policies' },
};

/**
* Check whether a named value has an explicit override entry.
* Uses case-insensitive matching to align with override-merger behavior.
*/
export 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
);
}

/**
* Apply environment overrides from OverrideConfig to a resource JSON payload.
* Deep-merges matching override properties using case-insensitive key matching.
Expand Down
13 changes: 1 addition & 12 deletions src/services/publish-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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.
Expand Down
14 changes: 13 additions & 1 deletion src/services/secret-redaction-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -103,6 +105,16 @@ async function scanNamedValue(
config: PublishConfig,
descriptor: ResourceDescriptor
): Promise<RedactionMarkerFinding | undefined> {
// Auto-generated named values (e.g. Logger-Credentials--<hex>) 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;
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/services/secret-redaction-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down