Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Assert, AITestClass } from "@microsoft/ai-test-framework";
import { DiagnosticLogger } from "../../../../src/diagnostics/DiagnosticLogger";
import { IConfiguration } from "../../../../src/interfaces/ai/IConfiguration";
import { dataSanitizeInput, dataSanitizeKey, dataSanitizeMessage, DataSanitizerValues, dataSanitizeString, dataSanitizeUrl } from "../../../../src/telemetry/ai/Common/DataSanitizer";

import { UrlRedactionOptions } from "../../../../src/enums/ai/UrlRedactionOptions"

export class ApplicationInsightsTests extends AITestClass {
logger = new DiagnosticLogger();
Expand Down Expand Up @@ -395,6 +395,7 @@ export class ApplicationInsightsTests extends AITestClass {
test: () => {
// URLs with sensitive query parameters
let config = {
redactUrls: UrlRedactionOptions.appendToDefault,
redactQueryParams: ["authorize", "api_key", "password"]
} as IConfiguration;
const urlWithSensitiveParams = "https://example.com/api?Signature=secret&authorize=value";
Expand All @@ -405,5 +406,22 @@ export class ApplicationInsightsTests extends AITestClass {
Assert.equal(expectedRedactedUrl, result);
}
});

this.testCase({
name: 'DataSanitizerTests: dataSanitizeUrl properly redacts sensitive query parameters (only custom)',
test: () => {
// URLs with sensitive query parameters
let config = {
redactUrls: UrlRedactionOptions.replaceDefault,
redactQueryParams: ["authorize", "api_key", "password"]
} as IConfiguration;
const urlWithSensitiveParams = "https://example.com/api?Signature=secret&authorize=value";
const expectedRedactedUrl = "https://example.com/api?Signature=secret&authorize=REDACTED";

// Act & Assert
const result = dataSanitizeUrl(this.logger, urlWithSensitiveParams, config);
Assert.equal(expectedRedactedUrl, result);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { _InternalLogMessage, DiagnosticLogger } from "../../../../src/diagnosti
import { ActiveStatus } from "../../../../src/enums/ai/InitActiveStatusEnum";
import { createAsyncPromise, createAsyncRejectedPromise, createAsyncResolvedPromise, createTimeoutPromise, doAwaitResponse } from "@nevware21/ts-async";
import { setBypassLazyCache } from "@nevware21/ts-utils";
import { UrlRedactionOptions } from "../../../../src/enums/ai/UrlRedactionOptions"

const AIInternalMessagePrefix = "AITR_";
const MaxInt32 = 0xFFFFFFFF;
Expand Down Expand Up @@ -2067,6 +2068,20 @@ export class ApplicationInsightsCoreTests extends AITestClass {
"Complex URL should have credentials and sensitive query parameters redacted while preserving other components");
}
});

this.testCase({
name: "FieldRedaction: should not redact URLs when redaction is disabled in config, even if they contain credentials and sensitive query parameters",
test: () => {
let config = {
redactUrls: false,
} as IConfiguration;
const url = "https://username:password@example.com:8443/path/to/resource?sig=secret&color=blue#section2";
const redactedLocation = fieldRedaction(url, config);
Assert.equal(redactedLocation, "https://username:password@example.com:8443/path/to/resource?sig=secret&color=blue#section2",
"URL should not redact credentials and sensitive query parameters when redaction is disabled in config");
}
});

this.testCase({
name: "FieldRedaction: should handle completely empty URL string",
test: () => {
Expand Down Expand Up @@ -2197,9 +2212,10 @@ export class ApplicationInsightsCoreTests extends AITestClass {
});

this.testCase({
name: "FieldRedaction: should redact custom query parameters defined in redactQueryParams",
name: "FieldRedaction: should redact custom query parameters defined in redactQueryParams and replace custom queryParams",
test: () => {
let config = {
redactUrls: UrlRedactionOptions.replaceDefault,
redactQueryParams: ["authorize", "api_key", "password"]
} as IConfiguration;

Expand All @@ -2209,10 +2225,12 @@ export class ApplicationInsightsCoreTests extends AITestClass {
"URL with custom sensitive parameters should have them redacted while preserving other parameters");
}
});

this.testCase({
name: "FieldRedaction: should redact both default and custom query parameters",
test: () => {
let config = {
redactUrls: UrlRedactionOptions.appendToDefault,
redactQueryParams: ["auth_token"]
} as IConfiguration;

Expand All @@ -2222,28 +2240,42 @@ export class ApplicationInsightsCoreTests extends AITestClass {
"URL with both default and custom sensitive parameters should have all redacted");
}
});

this.testCase({
name: "FieldRedaction:should not redact custom parameters when redaction is disabled",
name: "FieldRedaction:should replace custom parameters redactQueryParams when user specifies the replace config",
test: () => {
let config = {
redactUrls: false,
redactUrls: UrlRedactionOptions.replaceDefault,
redactQueryParams: ["authorize", "api_key"]
} as IConfiguration;

const url = "https://example.com/path?auth_token=12345&authorize=secret";
const url = "https://username:password@example.com/path?auth_token=12345&authorize=secret";
const redactedLocation = fieldRedaction(url, config);
Assert.equal(redactedLocation, "https://example.com/path?auth_token=12345&authorize=secret",
"URL with custom sensitive parameters should not be redacted when redaction is disabled");
Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com/path?auth_token=12345&authorize=REDACTED",
"URL with custom sensitive parameters should be redacted when query redaction is not disabled");
}
});

this.testCase({
name: "FieldRedaction: should handle empty redactQueryParams array",
name: "FieldRedaction: should not redact any query string values when custom query parameters are empty",
test: () => {
let config = {
redactUrls: UrlRedactionOptions.replaceDefault,
redactQueryParams: []
} as IConfiguration;

const url = "https://example.com/path?auth_token=12345&name=test&authorize=secret";
const redactedLocation = fieldRedaction(url, config);
Assert.equal(redactedLocation, "https://example.com/path?auth_token=12345&name=test&authorize=secret",
"URL with custom sensitive parameters should not be redacted when custom query parameters are empty");
}
});

this.testCase({
name: "FieldRedaction: should handle empty redactQueryParams array",
test: () => {
let config = {} as IConfiguration;

// Should still redact default parameters
const url = "https://example.com/path?Signature=secret&custom_param=value";
const redactedLocation = fieldRedaction(url, config);
Expand All @@ -2256,6 +2288,7 @@ export class ApplicationInsightsCoreTests extends AITestClass {
name: "FieldRedaction:should handle complex URLs with both credentials and custom query parameters",
test: () => {
let config = {
redactUrls: UrlRedactionOptions.appendToDefault,
redactQueryParams: ["authorize", "session_id"]
} as IConfiguration;

Expand Down Expand Up @@ -2404,7 +2437,6 @@ export class ApplicationInsightsCoreTests extends AITestClass {
}
});


this.testCase({
name: "FieldRedaction: should handle extremely long usernames without infinite looping",
test: () => {
Expand Down Expand Up @@ -2584,6 +2616,34 @@ export class ApplicationInsightsCoreTests extends AITestClass {
}
});

this.testCase({
name: "FieldRedaction: should redact credentials while preserving query strings when redactQueryParams is false",
test: () => {
let config = {
redactUrls: 5
} as IConfiguration;
const url = "https://user:password@example.com/path?sig=secret&color=blue&token=abc123";
const redactedLocation = fieldRedaction(url, config);
Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com/path?sig=secret&color=blue&token=abc123",
"Credentials should be redacted while query string values remain unchanged when redactQueryParams is false");
}
});

this.testCase({
name: "FieldRedaction: should handle custom parameters with multiple occurrences and empty values",
test: () => {
let config = {
redactUrls: UrlRedactionOptions.replaceDefault,
redactQueryParams: ["auth_token", "session_id"]
} as IConfiguration;
const url = "https://example.com/path?auth_token=first&name=test&auth_token=&session_id=abc&session_id=";
const redactedLocation = fieldRedaction(url, config);
// Only redact parameters that have actual values, not empty ones
Assert.equal(redactedLocation, "https://example.com/path?auth_token=REDACTED&name=test&auth_token=&session_id=REDACTED&session_id=",
"Only non-empty custom sensitive parameters should be redacted");
}
});

this.testCase({
name: "FieldRedaction: should handle parameters without values mixed with valued parameters",
test: () => {
Expand All @@ -2598,16 +2658,28 @@ export class ApplicationInsightsCoreTests extends AITestClass {
});

this.testCase({
name: "FieldRedaction: should handle custom parameters with multiple occurrences and empty values",
name: "FieldRedaction: should redact all parts of the URL (username, password, default query params) when redactUrls is set to True",
test: () => {
let config = {
redactQueryParams: ["auth_token", "session_id"]
redactUrls: true
} as IConfiguration;
const url = "https://example.com/path?auth_token=first&name=test&auth_token=&session_id=abc&session_id=";
const url = "https://user:password@example.com/path?sig=secret&color=blue&token=abc123";
const redactedLocation = fieldRedaction(url, config);
// Only redact parameters that have actual values, not empty ones
Assert.equal(redactedLocation, "https://example.com/path?auth_token=REDACTED&name=test&auth_token=&session_id=REDACTED&session_id=",
"Only non-empty custom sensitive parameters should be redacted");
Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com/path?sig=REDACTED&color=blue&token=abc123",
"All parts of the URL should be redacted when redactUrls is true");
}
});

this.testCase({
name: "FieldRedaction: should not redact credentials or query strings when redactUrls and redactQueryParams are false",
test: () => {
let config = {
redactUrls: UrlRedactionOptions.false
} as IConfiguration;
const url = "https://user:password@example.com/path?sig=secret&color=blue&token=abc123";
const redactedLocation = fieldRedaction(url, config);
Assert.equal(redactedLocation, url,
"Nothing should be redacted when both redactUrls and redactQueryParams are false");
}
});

Expand Down
80 changes: 80 additions & 0 deletions shared/AppInsightsCore/src/enums/ai/UrlRedactionOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { createEnumStyle } from "../EnumHelperFuncs";

/**
* Controls how the user can configure which parts of the URL should be redacted. Example, certain query parameters, username and password, etc.
*/

export const enum eUrlRedactionOptions {
/**
* The default value, will redact the username and password as well as the default set of query parameters
*/
true = 1,

/**
* Does not redact username and password or any query parameters, the URL will be left as is. Note: this is not recommended as it may lead
* to sensitive data being sent in clear text.
*/
false = 2,

/**
* This will append any additional queryParams that the user has provided through redactQueryParams config to the default set i.e to
* @defaultValue ["sig", "Signature", "AWSAccessKeyId", "X-Goog-Signature"].
*/
appendToDefault = 3,

/**
* This will replace the default set of query parameters to redact with the query parameters defined in redactQueryParams config, if provided by the user.
*/
replaceDefault = 4,

/**
* This will redact username and password in the URL but will not redact any query parameters, even those in the default set.
*/
usernamePasswordOnly = 5,

/**
* This will only redact the query parameter in the default set of query parameters to redact. It will not redact username and password.
*/
queryParamsOnly = 6,

}

export const UrlRedactionOptions = (/* @__PURE__ */ createEnumStyle<typeof eUrlRedactionOptions>({
/**
* The default value, will redact the username and password as well as the default set of query parameters
*/
true: eUrlRedactionOptions.true,

/**
* Does not redact username and password or any query parameters, the URL will be left as is. Note: this is not recommended as it may lead
* to sensitive data being sent in clear text.
*/
false: eUrlRedactionOptions.false,

/**
* This will append any additional queryParams that the user has provided through redactQueryParams config to the default set i.e to
* @defaultValue ["sig", "Signature", "AWSAccessKeyId", "X-Goog-Signature"].
*/
appendToDefault: eUrlRedactionOptions.appendToDefault,

/**
* This will replace the default set of query parameters to redact with the query parameters defined in redactQueryParams config, if provided by the user.
*/
replaceDefault: eUrlRedactionOptions.replaceDefault,

/**
* This will redact username and password in the URL but will not redact any query parameters, even those in the default set.
*/
usernamePasswordOnly: eUrlRedactionOptions.usernamePasswordOnly,

/**
* This will only redact the query parameter in the default set of query parameters to redact. It will not redact username and password.
*/
queryParamsOnly: eUrlRedactionOptions.queryParamsOnly,

}));

export type UrlRedactionOptions = boolean | eUrlRedactionOptions;
1 change: 1 addition & 0 deletions shared/AppInsightsCore/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export { SendRequestReason } from "./enums/ai/SendRequestReason";
//export { StatsType, eStatsType } from "./enums/ai/StatsType";
export { TelemetryUpdateReason } from "./enums/ai/TelemetryUpdateReason";
export { TelemetryUnloadReason } from "./enums/ai/TelemetryUnloadReason";
export { eUrlRedactionOptions, UrlRedactionOptions } from "./enums/ai/UrlRedactionOptions"
export { eActiveStatus, ActiveStatus } from "./enums/ai/InitActiveStatusEnum";
export { throwAggregationError } from "./core/AggregationError";
export { AppInsightsCore } from "./core/AppInsightsCore";
Expand Down
5 changes: 3 additions & 2 deletions shared/AppInsightsCore/src/interfaces/ai/IConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.
import { IPromise } from "@nevware21/ts-async";
import { eTraceHeadersMode } from "../../enums/ai/TraceHeadersMode";
import { UrlRedactionOptions } from "../../enums/ai/UrlRedactionOptions";
import { IOTelConfig } from "../otel/config/IOTelConfig";
import { IAppInsightsCore } from "./IAppInsightsCore";
import { IChannelControls } from "./IChannelControls";
Expand Down Expand Up @@ -232,10 +233,10 @@ export interface IConfiguration extends IOTelConfig {
expCfg?: IExceptionConfig;

/**
* [Optional] A flag to enable or disable the use of the field redaction for urls.
* [Optional] A flag to enable or disable redaction for query parameters and username/password.
* @defaultValue true
*/
redactUrls?: boolean;
redactUrls?: UrlRedactionOptions;

/**
* [Optional] Additional query parameters to redact beyond the default set.
Expand Down
26 changes: 22 additions & 4 deletions shared/AppInsightsCore/src/utils/EnvUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
isFunction, isNullOrUndefined, isString, isUndefined, mathMax, strIndexOf, strSubstring
} from "@nevware21/ts-utils";
import { DEFAULT_SENSITIVE_PARAMS, STR_EMPTY, STR_REDACTED } from "../constants/InternalConstants";
import { eUrlRedactionOptions } from "../enums/ai/UrlRedactionOptions";
import { IConfiguration } from "../interfaces/ai/IConfiguration";
import { strContains } from "./HelperFuncs";

Expand Down Expand Up @@ -455,8 +456,12 @@ function redactQueryParameters(url: string, config?: IConfiguration): string {
return url;
}

if (config && config.redactQueryParams) {
const option = config ? config.redactUrls : undefined;
Comment thread
MSNev marked this conversation as resolved.

if (option === eUrlRedactionOptions.appendToDefault) {
sensitiveParams = DEFAULT_SENSITIVE_PARAMS.concat(config.redactQueryParams);
} else if (option === eUrlRedactionOptions.replaceDefault) {
sensitiveParams = config.redactQueryParams;
} else {
sensitiveParams = DEFAULT_SENSITIVE_PARAMS;
Comment thread
rads-1996 marked this conversation as resolved.
}
Expand Down Expand Up @@ -543,17 +548,30 @@ export function fieldRedaction(input: string, config: IConfiguration): string {
if (!input || !isString(input) || strIndexOf(input, " ") !== -1) {
return input;
}
const isRedactionDisabled = config && config.redactUrls === false;

const option = config ? config.redactUrls : undefined;

const isRedactionDisabled = option === false || option === eUrlRedactionOptions.false;
if (isRedactionDisabled) {
return input;
}
const hasCredentials = strIndexOf(input, "@") !== -1;
const hasQueryParams = strIndexOf(input, "?") !== -1;

let hasCredentials = strIndexOf(input, "@") !== -1;
let hasQueryParams = strIndexOf(input, "?") !== -1;

// If no credentials and no query params, return original
if (!hasCredentials && !hasQueryParams) {
return input;
}

if (option === eUrlRedactionOptions.usernamePasswordOnly) {
hasQueryParams = false;
}

if (option === eUrlRedactionOptions.queryParamsOnly) {
hasCredentials = false;
}

try {
let result = input;
if (hasCredentials) {
Expand Down
Loading