@@ -10,9 +10,31 @@ import { ResourceType } from '../models/resource-types.js';
1010import { ResourceDescriptor } from '../models/types.js' ;
1111import { logger } from '../lib/logger.js' ;
1212import { getNamePart } from '../lib/resource-path.js' ;
13+ import { buildResourceLabel } from '../lib/resource-uri.js' ;
1314
1415/** Marker used to replace secret values in extracted artifacts */
1516export const REDACTION_MARKER = '*** REDACTED ***' ;
17+ const NAMED_VALUE_REFERENCE_PATTERN = / ^ \s * \{ \{ [ ^ { } ] + \} \} \s * $ / ;
18+ // Headers where inline literal values are typically secrets and should be
19+ // redacted when present in policy XML. Extend this allow-list when APIM adds
20+ // new secret-bearing header conventions.
21+ const SECRET_HEADER_NAMES = new Set ( [
22+ 'authorization' ,
23+ 'ocp-apim-subscription-key' ,
24+ 'x-functions-key' ,
25+ 'api-key' ,
26+ ] ) ;
27+ // Query parameters commonly used to carry secrets/tokens in APIM policies.
28+ // Keep focused on secret-bearing names to avoid over-redacting non-secrets.
29+ const SECRET_QUERY_PARAMETER_NAMES = new Set ( [
30+ 'code' ,
31+ 'sig' ,
32+ 'subscription-key' ,
33+ ] ) ;
34+
35+ export interface PolicySecretFinding {
36+ location : string ;
37+ }
1638
1739/**
1840 * Redact secret values from a resource's JSON payload.
@@ -56,3 +78,161 @@ export function redactSecrets(
5678 logger . debug ( `Redacted secret value for named value "${ getNamePart ( descriptor . nameParts , 0 ) } "` ) ;
5779 return redacted ;
5880}
81+
82+ function isApimNamedValueReference ( value : string ) : boolean {
83+ return NAMED_VALUE_REFERENCE_PATTERN . test ( value ) ;
84+ }
85+
86+ function shouldRedactLiteral ( value : string ) : boolean {
87+ const trimmed = value . trim ( ) ;
88+ if ( ! trimmed || trimmed === REDACTION_MARKER ) {
89+ return false ;
90+ }
91+ return ! isApimNamedValueReference ( trimmed ) ;
92+ }
93+
94+ /**
95+ * Redact inline literal secrets in policy XML content.
96+ */
97+ export function redactPolicySecrets (
98+ policyContent : string
99+ ) : { redactedContent : string ; findings : PolicySecretFinding [ ] } {
100+ const findings : PolicySecretFinding [ ] = [ ] ;
101+ const addFinding = ( location : string ) : void => {
102+ findings . push ( { location } ) ;
103+ } ;
104+
105+ let redacted = policyContent ;
106+
107+ redacted = redacted . replace ( / < s e t - h e a d e r \b [ \s \S ] * ?< \/ s e t - h e a d e r > / gi, ( setHeaderBlock ) => {
108+ const nameMatch = / \b n a m e \s * = \s * [ " ' ] ( [ ^ " ' ] + ) [ " ' ] / i. exec ( setHeaderBlock ) ;
109+ const headerName = nameMatch ?. [ 1 ] ?. toLowerCase ( ) ;
110+ if ( ! headerName || ! SECRET_HEADER_NAMES . has ( headerName ) ) {
111+ return setHeaderBlock ;
112+ }
113+
114+ return setHeaderBlock . replace (
115+ / ( < v a l u e \b [ ^ > ] * > ) ( [ \s \S ] * ?) ( < \/ v a l u e > ) / gi,
116+ ( _full , openTag : string , value : string , closeTag : string ) => {
117+ if ( ! shouldRedactLiteral ( value ) ) {
118+ return `${ openTag } ${ value } ${ closeTag } ` ;
119+ }
120+
121+ addFinding ( `set-header[${ headerName } ]` ) ;
122+ return `${ openTag } ${ REDACTION_MARKER } ${ closeTag } ` ;
123+ }
124+ ) ;
125+ } ) ;
126+
127+ redacted = redacted . replace ( / < s e t - q u e r y - p a r a m e t e r \b [ \s \S ] * ?< \/ s e t - q u e r y - p a r a m e t e r > / gi, ( setQueryBlock ) => {
128+ const nameMatch = / \b n a m e \s * = \s * [ " ' ] ( [ ^ " ' ] + ) [ " ' ] / i. exec ( setQueryBlock ) ;
129+ const parameterName = nameMatch ?. [ 1 ] ?. toLowerCase ( ) ;
130+ if ( ! parameterName || ! SECRET_QUERY_PARAMETER_NAMES . has ( parameterName ) ) {
131+ return setQueryBlock ;
132+ }
133+
134+ return setQueryBlock . replace (
135+ / ( < v a l u e \b [ ^ > ] * > ) ( [ \s \S ] * ?) ( < \/ v a l u e > ) / gi,
136+ ( _full , openTag : string , value : string , closeTag : string ) => {
137+ if ( ! shouldRedactLiteral ( value ) ) {
138+ return `${ openTag } ${ value } ${ closeTag } ` ;
139+ }
140+
141+ addFinding ( `set-query-parameter[${ parameterName } ]` ) ;
142+ return `${ openTag } ${ REDACTION_MARKER } ${ closeTag } ` ;
143+ }
144+ ) ;
145+ } ) ;
146+
147+ redacted = redacted . replace ( / < a u t h e n t i c a t i o n - b a s i c \b [ ^ > ] * > / gi, ( tag ) => {
148+ return tag . replace ( / ( \b p a s s w o r d \s * = \s * [ " ' ] ) ( [ ^ " ' ] * ) ( [ " ' ] ) / i, ( _full , prefix : string , value : string , suffix : string ) => {
149+ if ( ! shouldRedactLiteral ( value ) ) {
150+ return `${ prefix } ${ value } ${ suffix } ` ;
151+ }
152+
153+ addFinding ( 'authentication-basic@password' ) ;
154+ return `${ prefix } ${ REDACTION_MARKER } ${ suffix } ` ;
155+ } ) ;
156+ } ) ;
157+
158+ redacted = redacted . replace ( / < a u t h e n t i c a t i o n - c e r t i f i c a t e \b [ ^ > ] * > / gi, ( tag ) => {
159+ return tag . replace ( / ( \b b o d y \s * = \s * [ " ' ] ) ( [ ^ " ' ] * ) ( [ " ' ] ) / i, ( _full , prefix : string , value : string , suffix : string ) => {
160+ if ( ! shouldRedactLiteral ( value ) ) {
161+ return `${ prefix } ${ value } ${ suffix } ` ;
162+ }
163+
164+ addFinding ( 'authentication-certificate@body' ) ;
165+ return `${ prefix } ${ REDACTION_MARKER } ${ suffix } ` ;
166+ } ) ;
167+ } ) ;
168+
169+ redacted = redacted . replace ( / < a u t h e n t i c a t i o n - c e r t i f i c a t e \b [ \s \S ] * ?< \/ a u t h e n t i c a t i o n - c e r t i f i c a t e > / gi, ( certificateBlock ) => {
170+ return certificateBlock . replace (
171+ / ( < c e r t i f i c a t e \b [ ^ > ] * > ) ( [ \s \S ] * ?) ( < \/ c e r t i f i c a t e > ) / gi,
172+ ( _full , openTag : string , value : string , closeTag : string ) => {
173+ if ( ! shouldRedactLiteral ( value ) ) {
174+ return `${ openTag } ${ value } ${ closeTag } ` ;
175+ }
176+
177+ addFinding ( 'authentication-certificate/certificate' ) ;
178+ return `${ openTag } ${ REDACTION_MARKER } ${ closeTag } ` ;
179+ }
180+ ) ;
181+ } ) ;
182+
183+ for ( const keySection of [ 'issuer-signing-keys' , 'decryption-keys' ] ) {
184+ const sectionRegex = new RegExp ( `<${ keySection } \\b[\\s\\S]*?<\\/${ keySection } >` , 'gi' ) ;
185+ redacted = redacted . replace ( sectionRegex , ( sectionBlock ) => {
186+ return sectionBlock . replace (
187+ / ( < k e y \b [ ^ > ] * > ) ( [ \s \S ] * ?) ( < \/ k e y > ) / gi,
188+ ( _full , openTag : string , value : string , closeTag : string ) => {
189+ if ( ! shouldRedactLiteral ( value ) ) {
190+ return `${ openTag } ${ value } ${ closeTag } ` ;
191+ }
192+
193+ addFinding ( `validate-jwt ${ keySection } /key` ) ;
194+ return `${ openTag } ${ REDACTION_MARKER } ${ closeTag } ` ;
195+ }
196+ ) ;
197+ } ) ;
198+ }
199+
200+ // AccountKey/SharedAccessKey fragments are used by storage/service-bus style
201+ // connection strings. App Insights connection strings use InstrumentationKey
202+ // and therefore do not match this pattern (allow-listed by design).
203+ // Value exclusions:
204+ // - ';' stops at the next connection-string key/value delimiter
205+ // - whitespace/newlines avoid over-capturing adjacent text
206+ // - '<' and '"' avoid crossing into XML tags/attributes
207+ redacted = redacted . replace ( / ( A c c o u n t K e y | S h a r e d A c c e s s K e y ) \s * = \s * ( [ ^ ; \r \n < " \s ] + ) / gi, ( _full , key : string , value : string ) => {
208+ if ( ! shouldRedactLiteral ( value ) ) {
209+ return `${ key } =${ value } ` ;
210+ }
211+
212+ addFinding ( `connection-string[${ key } ]` ) ;
213+ return `${ key } =${ REDACTION_MARKER } ` ;
214+ } ) ;
215+
216+ return {
217+ redactedContent : redacted ,
218+ findings,
219+ } ;
220+ }
221+
222+ /**
223+ * Emit warning logs for policy secret redaction findings.
224+ */
225+ export function warnPolicySecretRedactions (
226+ descriptor : ResourceDescriptor ,
227+ findings : PolicySecretFinding [ ]
228+ ) : void {
229+ const label = buildResourceLabel ( descriptor ) ;
230+ for ( const finding of findings ) {
231+ logger . warn (
232+ `Found and redacted inline secret in ${ label } (${ finding . location } ). ` +
233+ `Publish will fail while '${ REDACTION_MARKER } ' remains. ` +
234+ 'Update the policy to use a named value: ' +
235+ 'https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-properties'
236+ ) ;
237+ }
238+ }
0 commit comments