|
| 1 | +// Copyright (c) Microsoft Corporation. |
| 2 | +// Licensed under the MIT license. |
| 3 | +/** |
| 4 | + * Secret redaction pre-flight guard |
| 5 | + * |
| 6 | + * Scans the set of artifacts that are about to be published for leftover |
| 7 | + * redaction markers ('*** REDACTED ***'). Any finding aborts the entire |
| 8 | + * publish before a single PUT is issued (for both real and dry-run modes), |
| 9 | + * so a partially-published service can never result from placeholder secrets. |
| 10 | + * |
| 11 | + * Detection mirrors the per-resource publish guards: |
| 12 | + * - Policies: build the publish payload, apply overrides, then check the |
| 13 | + * merged `properties.value` for the marker (an override may legitimately |
| 14 | + * replace the value with clean content). |
| 15 | + * - Named values: read the resource, apply overrides; KeyVault-backed values |
| 16 | + * have their `value` stripped before publish and are therefore ignored; |
| 17 | + * otherwise a `secret === true` value that exactly equals the marker is a |
| 18 | + * finding. |
| 19 | + */ |
| 20 | + |
| 21 | +import type { IArtifactStore } from '../clients/iartifact-store.js'; |
| 22 | +import type { PublishConfig } from '../models/config.js'; |
| 23 | +import type { ResourceDescriptor } from '../models/types.js'; |
| 24 | +import { ResourceType } from '../models/resource-types.js'; |
| 25 | +import { applyOverrides } from './override-merger.js'; |
| 26 | +import { POLICY_TYPES } from './resource-publisher.js'; |
| 27 | +import { REDACTION_MARKER } from './secret-redactor.js'; |
| 28 | +import { buildResourceLabel } from '../lib/resource-uri.js'; |
| 29 | + |
| 30 | +/** |
| 31 | + * A single artifact that still contains a redaction marker after overrides. |
| 32 | + */ |
| 33 | +export interface RedactionMarkerFinding { |
| 34 | + /** The descriptor for the offending artifact. */ |
| 35 | + descriptor: ResourceDescriptor; |
| 36 | + /** Human-readable resource label (e.g. `apis/echo/policies/policy`). */ |
| 37 | + label: string; |
| 38 | + /** Where the marker was found (e.g. `policy.xml`, `properties.value`). */ |
| 39 | + location: string; |
| 40 | +} |
| 41 | + |
| 42 | +/** |
| 43 | + * Scan the supplied descriptors for leftover redaction markers in the content |
| 44 | + * that would actually be published (i.e. after overrides are applied). |
| 45 | + * |
| 46 | + * Returns every finding so the caller can report all offenders at once rather |
| 47 | + * than failing on the first one. |
| 48 | + */ |
| 49 | +export async function scanForRedactionMarkers( |
| 50 | + store: IArtifactStore, |
| 51 | + config: PublishConfig, |
| 52 | + descriptors: ResourceDescriptor[] |
| 53 | +): Promise<RedactionMarkerFinding[]> { |
| 54 | + const findings: RedactionMarkerFinding[] = []; |
| 55 | + |
| 56 | + for (const descriptor of descriptors) { |
| 57 | + if (POLICY_TYPES.has(descriptor.type)) { |
| 58 | + const finding = await scanPolicy(store, config, descriptor); |
| 59 | + if (finding) findings.push(finding); |
| 60 | + } else if (descriptor.type === ResourceType.NamedValue) { |
| 61 | + const finding = await scanNamedValue(store, config, descriptor); |
| 62 | + if (finding) findings.push(finding); |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + return findings; |
| 67 | +} |
| 68 | + |
| 69 | +async function scanPolicy( |
| 70 | + store: IArtifactStore, |
| 71 | + config: PublishConfig, |
| 72 | + descriptor: ResourceDescriptor |
| 73 | +): Promise<RedactionMarkerFinding | undefined> { |
| 74 | + const policyContent = await store.readContent(config.sourceDir, descriptor, 'policy'); |
| 75 | + if (!policyContent) { |
| 76 | + return undefined; |
| 77 | + } |
| 78 | + |
| 79 | + const payload: Record<string, unknown> = { |
| 80 | + properties: { |
| 81 | + value: policyContent.content, |
| 82 | + format: 'rawxml', |
| 83 | + }, |
| 84 | + }; |
| 85 | + |
| 86 | + const merged = applyOverrides(descriptor, payload, config.overrides); |
| 87 | + const mergedProps = merged.properties as Record<string, unknown> | undefined; |
| 88 | + const mergedValue = mergedProps?.value; |
| 89 | + |
| 90 | + if (typeof mergedValue === 'string' && mergedValue.includes(REDACTION_MARKER)) { |
| 91 | + return { |
| 92 | + descriptor, |
| 93 | + label: buildResourceLabel(descriptor), |
| 94 | + location: 'policy.xml', |
| 95 | + }; |
| 96 | + } |
| 97 | + |
| 98 | + return undefined; |
| 99 | +} |
| 100 | + |
| 101 | +async function scanNamedValue( |
| 102 | + store: IArtifactStore, |
| 103 | + config: PublishConfig, |
| 104 | + descriptor: ResourceDescriptor |
| 105 | +): Promise<RedactionMarkerFinding | undefined> { |
| 106 | + const json = await store.readResource(config.sourceDir, descriptor); |
| 107 | + if (!json) { |
| 108 | + return undefined; |
| 109 | + } |
| 110 | + |
| 111 | + const merged = applyOverrides(descriptor, json, config.overrides); |
| 112 | + const props = merged.properties as Record<string, unknown> | undefined; |
| 113 | + |
| 114 | + // KeyVault-backed named values have `properties.value` stripped before publish, |
| 115 | + // so any marker still present in the file is never sent to APIM. |
| 116 | + if (props?.keyVault != null) { |
| 117 | + return undefined; |
| 118 | + } |
| 119 | + |
| 120 | + if (props?.secret === true && props.value === REDACTION_MARKER) { |
| 121 | + return { |
| 122 | + descriptor, |
| 123 | + label: buildResourceLabel(descriptor), |
| 124 | + location: 'properties.value', |
| 125 | + }; |
| 126 | + } |
| 127 | + |
| 128 | + return undefined; |
| 129 | +} |
0 commit comments