From 4068079282a25497f9921957b9075c37ad791f17 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Wed, 28 May 2025 15:16:48 -0700 Subject: [PATCH 01/14] Implement URL Redaction feature --- .../Tests/Unit/src/AISKULightSize.Tests.ts | 6 +- .../src/JavaScriptSDK/AnalyticsPlugin.ts | 13 +- .../Telemetry/PageViewManager.ts | 6 +- .../src/DataCollector.ts | 5 +- .../src/ajax.ts | 8 +- .../Tests/Unit/src/propertiesSize.tests.ts | 2 +- shared/1ds-core-js/src/Index.ts | 2 +- .../test/Unit/src/FileSizeCheckTest.ts | 4 +- .../Tests/Unit/src/AppInsightsCommon.tests.ts | 82 ++++++++- .../src/Telemetry/Common/DataSanitizer.ts | 8 +- .../Unit/src/AppInsightsCoreSize.Tests.ts | 8 +- .../Unit/src/ApplicationInsightsCore.Tests.ts | 156 +++++++++++++++++- .../IConfiguration.ts | 5 + .../src/JavaScriptSDK/EnvUtils.ts | 145 +++++++++++++++- .../src/applicationinsights-core-js.ts | 2 +- 15 files changed, 423 insertions(+), 29 deletions(-) diff --git a/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts b/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts index 6d6434efa..a8d3baf45 100644 --- a/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts +++ b/AISKULight/Tests/Unit/src/AISKULightSize.Tests.ts @@ -52,9 +52,9 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: export class AISKULightSizeCheck extends AITestClass { private readonly MAX_RAW_SIZE = 93; - private readonly MAX_BUNDLE_SIZE = 93; - private readonly MAX_RAW_DEFLATE_SIZE = 38; - private readonly MAX_BUNDLE_DEFLATE_SIZE = 38; + private readonly MAX_BUNDLE_SIZE = 94; + private readonly MAX_RAW_DEFLATE_SIZE = 39; + private readonly MAX_BUNDLE_DEFLATE_SIZE = 39; private readonly rawFilePath = "../dist/es5/applicationinsights-web-basic.min.js"; private readonly currentVer = "3.3.9"; private readonly prodFilePath = `../browser/es5/aib.${this.currentVer[0]}.min.js`; diff --git a/extensions/applicationinsights-analytics-js/src/JavaScriptSDK/AnalyticsPlugin.ts b/extensions/applicationinsights-analytics-js/src/JavaScriptSDK/AnalyticsPlugin.ts index 7d03423d7..af4136e35 100644 --- a/extensions/applicationinsights-analytics-js/src/JavaScriptSDK/AnalyticsPlugin.ts +++ b/extensions/applicationinsights-analytics-js/src/JavaScriptSDK/AnalyticsPlugin.ts @@ -17,9 +17,9 @@ import { IExceptionConfig, IInstrumentCallDetails, IPlugin, IProcessTelemetryContext, IProcessTelemetryUnloadContext, ITelemetryInitializerHandler, ITelemetryItem, ITelemetryPluginChain, ITelemetryUnloadState, InstrumentEvent, TelemetryInitializerFunction, _eInternalMessageId, arrForEach, cfgDfBoolean, cfgDfMerge, cfgDfSet, cfgDfString, cfgDfValidate, - createProcessTelemetryContext, createUniqueNamespace, dumpObj, eLoggingSeverity, eventOff, eventOn, findAllScripts, generateW3CId, - getDocument, getExceptionName, getHistory, getLocation, getWindow, hasHistory, hasWindow, isFunction, isNullOrUndefined, isString, - isUndefined, mergeEvtNamespace, onConfigChange, safeGetCookieMgr, strUndefined, throwError + createProcessTelemetryContext, createUniqueNamespace, dumpObj, eLoggingSeverity, eventOff, eventOn, fieldRedaction, findAllScripts, + generateW3CId, getDocument, getExceptionName, getHistory, getLocation, getWindow, hasHistory, hasWindow, isFunction, isNullOrUndefined, + isString, isUndefined, mergeEvtNamespace, onConfigChange, safeGetCookieMgr, strUndefined, throwError } from "@microsoft/applicationinsights-core-js"; import { PropertiesPlugin } from "@microsoft/applicationinsights-properties-js"; import { isArray, isError, objDeepFreeze, objDefine, scheduleTimeout, strIndexOf } from "@nevware21/ts-utils"; @@ -264,6 +264,7 @@ export class AnalyticsPlugin extends BaseTelemetryPlugin implements IAppInsights _self.trackPageView = (pageView?: IPageViewTelemetry, customProperties?: ICustomProperties) => { try { let inPv = pageView || {}; + inPv.uri = fieldRedaction(inPv.uri, _extConfig); _pageViewManager.trackPageView(inPv, {...inPv.properties, ...inPv.measurements, ...customProperties}); if (_autoTrackPageVisitTime) { @@ -289,6 +290,7 @@ export class AnalyticsPlugin extends BaseTelemetryPlugin implements IAppInsights if (doc) { pageView.refUri = pageView.refUri === undefined ? doc.referrer : pageView.refUri; } + pageView.refUri = fieldRedaction(pageView.refUri, _extConfig); if (isNullOrUndefined(pageView.startTime)) { // calculate the start time manually let duration = ((properties || pageView.properties || {}).duration || 0); @@ -385,7 +387,7 @@ export class AnalyticsPlugin extends BaseTelemetryPlugin implements IAppInsights let loc = getLocation(); url = loc && loc.href || ""; } - + url = fieldRedaction(url, _extConfig); _pageTracking.stop(name, url, properties, measurement); if (_autoTrackPageVisitTime) { @@ -801,7 +803,7 @@ export class AnalyticsPlugin extends BaseTelemetryPlugin implements IAppInsights } else { _currUri = locn && locn.href || ""; } - + _currUri = fieldRedaction(_currUri, _extConfig); if (_enableAutoRouteTracking) { let distributedTraceCtx = _getDistributedTraceCtx(); if (distributedTraceCtx) { @@ -912,6 +914,7 @@ export class AnalyticsPlugin extends BaseTelemetryPlugin implements IAppInsights // array with max length of 2 that store current url and previous url for SPA page route change trackPageview use. let location = getLocation(true); _prevUri = location && location.href || ""; + _prevUri = fieldRedaction(_prevUri, _extConfig); _currUri = null; _evtNamespace = null; _extConfig = null; diff --git a/extensions/applicationinsights-analytics-js/src/JavaScriptSDK/Telemetry/PageViewManager.ts b/extensions/applicationinsights-analytics-js/src/JavaScriptSDK/Telemetry/PageViewManager.ts index 8217c3d00..13d60fa27 100644 --- a/extensions/applicationinsights-analytics-js/src/JavaScriptSDK/Telemetry/PageViewManager.ts +++ b/extensions/applicationinsights-analytics-js/src/JavaScriptSDK/Telemetry/PageViewManager.ts @@ -7,7 +7,7 @@ import { } from "@microsoft/applicationinsights-common"; import { IAppInsightsCore, IDiagnosticLogger, IProcessTelemetryUnloadContext, ITelemetryUnloadState, _eInternalMessageId, _throwInternal, - arrForEach, dumpObj, eLoggingSeverity, getDocument, getExceptionName, getLocation, isNullOrUndefined + arrForEach, dumpObj, eLoggingSeverity, fieldRedaction, getDocument, getExceptionName, getLocation, isNullOrUndefined } from "@microsoft/applicationinsights-core-js"; import { ITimerHandler, getPerformance, isUndefined, isWebWorker, scheduleTimeout } from "@nevware21/ts-utils"; import { PageViewPerformanceManager } from "./PageViewPerformanceManager"; @@ -96,7 +96,9 @@ export class PageViewManager { let location = getLocation(); uri = pageView.uri = location && location.href || ""; } - + if (core && core.config){ + uri = pageView.uri = fieldRedaction(pageView.uri, core.config); + } if (!firstPageViewSent){ let perf = getPerformance(); // Access the performance timing object diff --git a/extensions/applicationinsights-clickanalytics-js/src/DataCollector.ts b/extensions/applicationinsights-clickanalytics-js/src/DataCollector.ts index 5733f794c..a7c14ffbe 100644 --- a/extensions/applicationinsights-clickanalytics-js/src/DataCollector.ts +++ b/extensions/applicationinsights-clickanalytics-js/src/DataCollector.ts @@ -2,7 +2,7 @@ * @copyright Microsoft 2020 */ -import { getDocument, getLocation, getWindow, hasDocument, isFunction } from "@microsoft/applicationinsights-core-js"; +import { getDocument, getLocation, getWindow, hasDocument, isFunction, IConfiguration, fieldRedaction } from "@microsoft/applicationinsights-core-js"; import { scheduleTimeout } from "@nevware21/ts-utils"; import { IClickAnalyticsConfiguration, IOverrideValues } from "./Interfaces/Datamodel"; import { findClosestAnchor, isValueAssigned } from "./common/Utils"; @@ -126,7 +126,7 @@ export function getPageName(config: IClickAnalyticsConfiguration, overrideValues * @param location - window.location or document.location * @returns Flag indicating if an element is market PII. */ -export function sanitizeUrl(config: IClickAnalyticsConfiguration, location: Location): string { +export function sanitizeUrl(config: IClickAnalyticsConfiguration, location: Location, extConfig?: IConfiguration): string { if (!location) { return null; } @@ -142,6 +142,7 @@ export function sanitizeUrl(config: IClickAnalyticsConfiguration, location: Loca url += (isValueAssigned(location.search)? location.search : ""); } + url = fieldRedaction(url, extConfig); return url; } diff --git a/extensions/applicationinsights-dependencies-js/src/ajax.ts b/extensions/applicationinsights-dependencies-js/src/ajax.ts index 8fb7ac66f..dd17bba8d 100644 --- a/extensions/applicationinsights-dependencies-js/src/ajax.ts +++ b/extensions/applicationinsights-dependencies-js/src/ajax.ts @@ -12,8 +12,8 @@ import { BaseTelemetryPlugin, IAppInsightsCore, IConfigDefaults, IConfiguration, ICustomProperties, IDistributedTraceContext, IInstrumentCallDetails, IInstrumentHooksCallbacks, IPlugin, IProcessTelemetryContext, ITelemetryItem, ITelemetryPluginChain, InstrumentFunc, InstrumentProto, _eInternalMessageId, _throwInternal, arrForEach, createProcessTelemetryContext, createUniqueNamespace, - dumpObj, eLoggingSeverity, eventOn, generateW3CId, getExceptionName, getGlobal, getIEVersion, getLocation, getPerformance, isFunction, - isNullOrUndefined, isString, isXhrSupported, mergeEvtNamespace, onConfigChange, strPrototype, strTrim + dumpObj, eLoggingSeverity, eventOn, fieldRedaction, generateW3CId, getExceptionName, getGlobal, getIEVersion, getLocation, + getPerformance, isFunction, isNullOrUndefined, isString, isXhrSupported, mergeEvtNamespace, onConfigChange, strPrototype, strTrim } from "@microsoft/applicationinsights-core-js"; import { isWebWorker, objFreeze, scheduleTimeout, strIndexOf, strSplit, strSubstr } from "@nevware21/ts-utils"; import { DependencyInitializerFunction, IDependencyInitializerDetails, IDependencyInitializerHandler } from "./DependencyInitializer"; @@ -1193,6 +1193,10 @@ export class AjaxMonitor extends BaseTelemetryPlugin implements IDependenciesPlu } } + if (_self.core && _self.core.config) { + requestUrl = fieldRedaction(requestUrl, _self.core.config); + } + ajaxData.requestUrl = requestUrl; let method = "GET"; diff --git a/extensions/applicationinsights-properties-js/Tests/Unit/src/propertiesSize.tests.ts b/extensions/applicationinsights-properties-js/Tests/Unit/src/propertiesSize.tests.ts index 345f087ac..bbf8069c1 100644 --- a/extensions/applicationinsights-properties-js/Tests/Unit/src/propertiesSize.tests.ts +++ b/extensions/applicationinsights-properties-js/Tests/Unit/src/propertiesSize.tests.ts @@ -51,7 +51,7 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: } export class PropertiesExtensionSizeCheck extends AITestClass { - private readonly MAX_DEFLATE_SIZE = 18; + private readonly MAX_DEFLATE_SIZE = 19; private readonly rawFilePath = "../dist/es5/applicationinsights-properties-js.min.js"; // Automatically updated by version scripts private readonly currentVer = "3.3.9"; diff --git a/shared/1ds-core-js/src/Index.ts b/shared/1ds-core-js/src/Index.ts index ddb00ff1f..29aa37a14 100644 --- a/shared/1ds-core-js/src/Index.ts +++ b/shared/1ds-core-js/src/Index.ts @@ -37,7 +37,7 @@ export { objForEachKey, strStartsWith, strEndsWith, strContains, strTrim, isDate, isArray, isError, isString, isNumber, isBoolean, toISOString, arrForEach, arrIndexOf, arrMap, arrReduce, objKeys, objDefineAccessors, dateNow, getExceptionName, throwError, setValue, getSetValue, isNotTruthy, isTruthy, proxyAssign, proxyFunctions, proxyFunctionAs, optimizeObject, - addEventHandler, newGuid, perfNow, newId, generateW3CId, safeGetLogger, objFreeze, objSeal, + addEventHandler, newGuid, perfNow, newId, generateW3CId, safeGetLogger, objFreeze, objSeal, fieldRedaction, // EnvUtils getGlobal, getGlobalInst, hasWindow, getWindow, hasDocument, getDocument, getCrypto, getMsCrypto, hasNavigator, getNavigator, hasHistory, getHistory, getLocation, getPerformance, hasJSON, getJSON, diff --git a/shared/1ds-core-js/test/Unit/src/FileSizeCheckTest.ts b/shared/1ds-core-js/test/Unit/src/FileSizeCheckTest.ts index d2fa9d3ac..c562246d5 100644 --- a/shared/1ds-core-js/test/Unit/src/FileSizeCheckTest.ts +++ b/shared/1ds-core-js/test/Unit/src/FileSizeCheckTest.ts @@ -51,8 +51,8 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: } export class FileSizeCheckTest extends AITestClass { - private readonly MAX_BUNDLE_SIZE = 69; - private readonly MAX_DEFLATE_SIZE = 29; + private readonly MAX_BUNDLE_SIZE = 70; + private readonly MAX_DEFLATE_SIZE = 30; private readonly bundleFilePath = "../bundle/es5/ms.core.min.js"; public testInitialize() { diff --git a/shared/AppInsightsCommon/Tests/Unit/src/AppInsightsCommon.tests.ts b/shared/AppInsightsCommon/Tests/Unit/src/AppInsightsCommon.tests.ts index 65b1a52ff..44a23ddab 100644 --- a/shared/AppInsightsCommon/Tests/Unit/src/AppInsightsCommon.tests.ts +++ b/shared/AppInsightsCommon/Tests/Unit/src/AppInsightsCommon.tests.ts @@ -1,7 +1,7 @@ import { strRepeat } from "@nevware21/ts-utils"; import { Assert, AITestClass } from "@microsoft/ai-test-framework"; import { DiagnosticLogger } from "@microsoft/applicationinsights-core-js"; -import { dataSanitizeInput, dataSanitizeKey, dataSanitizeMessage, DataSanitizerValues, dataSanitizeString } from "../../../src/Telemetry/Common/DataSanitizer"; +import { dataSanitizeInput, dataSanitizeKey, dataSanitizeMessage, DataSanitizerValues, dataSanitizeString, dataSanitizeUrl } from "../../../src/Telemetry/Common/DataSanitizer"; export class ApplicationInsightsTests extends AITestClass { @@ -293,5 +293,85 @@ export class ApplicationInsightsTests extends AITestClass { loggerStub.restore(); } }); + this.testCase({ + name: 'DataSanitizerTests: dataSanitizerUrl properly redacts credentials in URLs', + test: () => { + // URLs with credentials + const urlWithCredentials = "https://username:password@example.com/path"; + const expectedRedactedUrl = "https://REDACTED:REDACTED@example.com/path"; + + // Act & Assert + const result = dataSanitizeUrl(this.logger, urlWithCredentials); + Assert.equal(expectedRedactedUrl, result); + } + }); + this.testCase({ + name: 'DataSanitizerTests: dataSanitizerUrl handles invalid URLs', + test: () => { + // Invalid URL that will cause URL constructor to throw + const invalidUrl = 123545; + + // Act & Assert + const result = dataSanitizeUrl(this.logger, invalidUrl); + Assert.equal(invalidUrl, result, "Invalid URLs should be returned unchanged"); + } + }); + this.testCase({ + name: 'DataSanitizerTests: dataSanitizerUrl still enforces maximum length after redaction', + test: () => { + // Setup + const loggerStub = this.sandbox.stub(this.logger, "throwInternal"); + const MAX_URL_LENGTH = DataSanitizerValues.MAX_URL_LENGTH; + + // Create a very long URL with sensitive information + const longBaseUrl = "https://username:password@example.com/"; + const longPathPart = strRepeat("a", MAX_URL_LENGTH); + const longUrl = longBaseUrl + longPathPart; + + // Act + const result = dataSanitizeUrl(this.logger, longUrl); + + // Assert + Assert.equal(MAX_URL_LENGTH, result.length, "URL should be truncated to maximum length"); + Assert.equal(true, result.indexOf("REDACTED") > -1, "Redaction should happen before truncation"); + Assert.ok(loggerStub.calledOnce, "Logger should be called once for oversized URL"); + + loggerStub.restore(); + } + }); + this.testCase({ + name: 'DataSanitizerTests: dataSanitizerUrl handles null and undefined inputs', + test: () => { + // Act & Assert + const nullResult = dataSanitizeUrl(this.logger, null); + Assert.equal(null, nullResult, "Null input should return null"); + + const undefinedResult = dataSanitizeUrl(this.logger, undefined); + Assert.equal(undefined, undefinedResult, "Undefined input should return undefined"); + } + }); + this.testCase({ + name: 'DataSanitizerTests: dataSanitizerUrl preserves URLs with no sensitive information', + test: () => { + // URL with no sensitive information + const safeUrl = "https://example.com/api?param1=value1¶m2=value2"; + + // Act & Assert + const result = dataSanitizeUrl(this.logger, safeUrl); + Assert.equal(safeUrl, result, "URL with no sensitive info should remain unchanged"); + } + }); + this.testCase({ + name: 'DataSanitizerTests: dataSanitizerUrl properly redacts sensitive query parameters', + test: () => { + // URLs with sensitive query parameters + const urlWithSensitiveParams = "https://example.com/api?Signature=secret&normal=value"; + const expectedRedactedUrl = "https://example.com/api?Signature=REDACTED&normal=value"; + + // Act & Assert + const result = dataSanitizeUrl(this.logger, urlWithSensitiveParams); + Assert.equal(expectedRedactedUrl, result); + } + }); } } diff --git a/shared/AppInsightsCommon/src/Telemetry/Common/DataSanitizer.ts b/shared/AppInsightsCommon/src/Telemetry/Common/DataSanitizer.ts index 4615bb33b..07a5beee1 100644 --- a/shared/AppInsightsCommon/src/Telemetry/Common/DataSanitizer.ts +++ b/shared/AppInsightsCommon/src/Telemetry/Common/DataSanitizer.ts @@ -2,7 +2,8 @@ // Licensed under the MIT License. import { - IDiagnosticLogger, _eInternalMessageId, _throwInternal, eLoggingSeverity, getJSON, hasJSON, isObject, objForEachKey, strTrim + IConfiguration, IDiagnosticLogger, _eInternalMessageId, _throwInternal, eLoggingSeverity, fieldRedaction, getJSON, hasJSON, isObject, + objForEachKey, strTrim } from "@microsoft/applicationinsights-core-js"; import { asString, strSubstr, strSubstring } from "@nevware21/ts-utils"; @@ -98,7 +99,10 @@ export function dataSanitizeString(logger: IDiagnosticLogger, value: any, maxLen return valueTrunc || value; } -export function dataSanitizeUrl(logger: IDiagnosticLogger, url: any) { +export function dataSanitizeUrl(logger: IDiagnosticLogger, url: any, config?: IConfiguration) { + if (typeof url === "string"){ + url = fieldRedaction(url, config); + } return dataSanitizeInput(logger, url, DataSanitizerValues.MAX_URL_LENGTH, _eInternalMessageId.UrlTooLong); } diff --git a/shared/AppInsightsCore/Tests/Unit/src/AppInsightsCoreSize.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/AppInsightsCoreSize.Tests.ts index c8da7b41d..87bcfa7f6 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/AppInsightsCoreSize.Tests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/AppInsightsCoreSize.Tests.ts @@ -51,10 +51,10 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: } export class AppInsightsCoreSizeCheck extends AITestClass { - private readonly MAX_RAW_SIZE = 67; - private readonly MAX_BUNDLE_SIZE = 67; - private readonly MAX_RAW_DEFLATE_SIZE = 28; - private readonly MAX_BUNDLE_DEFLATE_SIZE = 28; + private readonly MAX_RAW_SIZE = 68; + private readonly MAX_BUNDLE_SIZE = 68; + private readonly MAX_RAW_DEFLATE_SIZE = 29; + private readonly MAX_BUNDLE_DEFLATE_SIZE = 29; private readonly rawFilePath = "../dist/es5/applicationinsights-core-js.min.js"; private readonly prodFilePath = "../browser/es5/applicationinsights-core-js.min.js"; diff --git a/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts index c843d46ce..8ebe68274 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts @@ -2,7 +2,7 @@ import { Assert, AITestClass, PollingAssert } from "@microsoft/ai-test-framework import { IConfiguration, ITelemetryPlugin, ITelemetryItem, IPlugin, IAppInsightsCore, normalizeJsName, random32, mwcRandomSeed, newId, randomValue, mwcRandom32, isNullOrUndefined, SenderPostManager, - OnCompleteCallback, IPayloadData, _ISenderOnComplete, TransportType, _ISendPostMgrConfig + OnCompleteCallback, IPayloadData, _ISenderOnComplete, TransportType, _ISendPostMgrConfig, fieldRedaction } from "../../../src/applicationinsights-core-js" import { AppInsightsCore } from "../../../src/JavaScriptSDK/AppInsightsCore"; import { IChannelControls } from "../../../src/JavaScriptSDK.Interfaces/IChannelControls"; @@ -1908,6 +1908,160 @@ export class ApplicationInsightsCoreTests extends AITestClass { } }); + this.testCase({ + name: "should redact basic auth credentials from URL", + test: () => { + let config = {} as IConfiguration; + + const url = "https://user:password@example.com"; + if (config.redactionEnabled){ + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com"); + } + Assert.notEqual(url, "https://REDACTED:REDACTED@example.com"); + + } + }); + + this.testCase({ + name: "should not modify URL without credentials", + test: () => { + let config = {} as IConfiguration; + const url = "https://example.com/path"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://example.com/path"); + } + }); + + this.testCase({ + name: "should preserve query parameters while redacting auth", + test: () => { + let config = {} as IConfiguration; + const url = "https://www.example.com/path?color=blue&X-Goog-Signature=secret"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://www.example.com/path?color=blue&X-Goog-Signature=REDACTED"); + } + }); + + this.testCase({ + name: "should preserve query parameters while redacting auth when the query string is not in the set values", + test: () => { + let config = {} as IConfiguration; + const url = "https://www.example.com/path?color=blue&query=secret"; + const redactedLocation = fieldRedaction(url, config); + Assert.notEqual(redactedLocation, "https://www.example.com/path?color=blue&query=REDACTED"); + } + }); + + this.testCase({ + name: "should preserve query parameters while redacting auth - AWSAccessKeyId", + test: () => { + let config = {redactionEnabled: false} as IConfiguration; + const url = "https://www.example.com/path?color=blue&AWSAccessKeyId=secret"; + if (config.redactionEnabled){ + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://www.example.com/path?color=blue&AWSAccessKeyId=REDACTED"); + } + Assert.equal(url, "https://www.example.com/path?color=blue&AWSAccessKeyId=secret"); + } + }); + + this.testCase({ + name: "should handle invalid URL format", + test: () => { + let config = {} as IConfiguration; + const url = "invalid-url"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "invalid-url"); + } + }); + + this.testCase({ + name: "should handle special characters in credentials", + test: () => { + let config = {} as IConfiguration; + const url = "https://user%20name:pass%20word@example.com" + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com", + "URL should have encoded credentials redacted"); + } + }); + + this.testCase({ + name: "should handle URLs with multiple @ symbols", + test: () => { + let config = {} as IConfiguration; + const url = "https://user:pass@example.com/path@somewhere" + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com/path@somewhere"); + } + }); + + this.testCase({ + name: "should handle empty URLs", + test: () => { + let config = {} as IConfiguration; + const url = " "; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, " "); + } + }); + + this.testCase({ + name: "should properly redact URLs with ports", + test: () => { + let config = {} as IConfiguration; + const url = "https://user:pass@example.com:8080/path"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com:8080/path", + "URL with port should have credentials redacted while preserving port"); + } + }); + + this.testCase({ + name: "should properly redact URLs with fragments", + test: () => { + let config = {} as IConfiguration; + const url = "https://user:pass@example.com/path?param=value#section"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com/path?param=value#section", + "URL with fragment should have credentials redacted while preserving fragment"); + } + }); + + this.testCase({ + name: "should handle port-only URLs without credentials", + test: () => { + let config = {} as IConfiguration; + const url = "https://example.com:8080/api"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://example.com:8080/api", + "URL with port but no credentials should remain unchanged"); + } + }); + + this.testCase({ + name: "should handle URLs with IP addresses, ports and credentials", + test: () => { + let config = {} as IConfiguration; + const url = "https://admin:secret@192.168.1.1:8443/admin"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@192.168.1.1:8443/admin", + "URL with IP address and port should have credentials redacted"); + } + }); + + this.testCase({ + name: "should handle complex URLs with port, query parameters and fragment", + test: () => { + let config = {} 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://REDACTED:REDACTED@example.com:8443/path/to/resource?sig=REDACTED&color=blue#section2", + "Complex URL should have credentials and sensitive query parameters redacted while preserving other components"); + } + }); + function _createBuckets(num: number) { // Using helper function as TypeScript 2.5.3 is complaining about new Array(100).fill(0); let buckets: number[] = []; diff --git a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts index 190ff8b24..ca01d36e3 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts @@ -232,6 +232,11 @@ export interface IConfiguration { */ expCfg?: IExceptionConfig; + /** + * [Optional] A flag to enable or disable the use of the field redaction for urls. + * @defaultValue true + */ + redactionEnabled?: boolean; ///** // * [Optional] Internal SDK configuration for developers diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts index f5279018e..6035e0ece 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts @@ -4,10 +4,12 @@ import { getGlobal, strShimObject, strShimPrototype, strShimUndefined } from "@microsoft/applicationinsights-shims"; import { - getDocument, getInst, getNavigator, getPerformance, hasNavigator, isFunction, isString, isUndefined, mathMax, strIndexOf + getDocument, getInst, getNavigator, getPerformance, hasNavigator, isFunction, isString, isUndefined, mathMax, objForEachKey, strIndexOf, + strSubstring } from "@nevware21/ts-utils"; +import { IConfiguration } from "../applicationinsights-core-js"; import { strContains } from "./HelperFuncs"; -import { STR_EMPTY } from "./InternalConstants"; +import { STR_EMPTY, UNDEFINED_VALUE } from "./InternalConstants"; // TypeScript removed this interface so we need to declare the global so we can check for it's existence. declare var XDomainRequest: any; @@ -30,6 +32,15 @@ const strMsie = "msie"; const strTrident = "trident/"; const strXMLHttpRequest = "XMLHttpRequest"; +const SENSITIVE_QUERY_PARAMS = [ + "sig", + "Signature", + "AWSAccessKeyId", + "X-Goog-Signature" +] as const; + +const STR_REDACTED = "REDACTED"; + let _isTrident: boolean = null; let _navUserAgentCheck: string = null; let _enableMocks = false; @@ -354,3 +365,133 @@ export function sendCustomEvent(evtName: string, cfg?: any, customDetails?: any) } return false; } + +/** + * Redacts user information from a URL + * @param url - The URL string to redact + * @returns The URL with user information redacted + */ +function redactUserInfo(url: string): string { + const schemeEndIndex = url.indexOf(":"); + if (schemeEndIndex === -1) { + // not a valid url + return url; + } + const len = url.length; + if (len <= schemeEndIndex + 2 + || url.charAt(schemeEndIndex + 1) !== "/" + || url.charAt(schemeEndIndex + 2) !== "/") { + return url; + } + + let index: number; + let atIndex = -1; + for (index = schemeEndIndex + 3; index < len; index++) { + const c = url.charAt(index); + + if (c === "@") { + atIndex = index; + } + + if (c === "/" || c === "?" || c === "#") { + break; + } + } + if (atIndex === -1 || atIndex === len - 1) { + return url; + } + return url.substring(0, schemeEndIndex + 3) + "REDACTED:REDACTED" + url.substring(atIndex); +} + +/** + * Redacts sensitive query parameters from a URL + * @param url - The URL string to redact + * @returns The URL with sensitive query parameters redacted + */ +function redactQueryParameters(url: string): string { + const questionMarkIndex = strIndexOf(url, "?"); + if (questionMarkIndex === -1) { + return url; + } + // To build a parameter name until we reach the '=' character + // If the parameter name is a one to redact, we will redact the value + const baseUrl = strSubstring(url, 0, questionMarkIndex + 1); + let queryString = strSubstring(url, questionMarkIndex + 1); + + // Extract fragment if present + let fragment = STR_EMPTY; + const hashIndex = strIndexOf(queryString, "#"); + if (hashIndex !== -1) { + fragment = strSubstring(queryString, hashIndex); + queryString = strSubstring(queryString, 0, hashIndex); + } + + // Extract parameters + const params: { [key: string]: string } = {}; + if (queryString && queryString.length) { + const pairs = queryString.split("&"); + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i]; + if (!pair) { + continue; + } + + const equalsIndex = strIndexOf(pair, "="); + if (equalsIndex === -1) { + params[pair] = null; + } else { + const paramName = pair.substring(0, equalsIndex); + const paramValue = pair.substring(equalsIndex + 1); + params[paramName] = paramValue; + } + } + } + + // Check if any parameters need redaction + let anyParamRedacted = false; + for (let i = 0; i < SENSITIVE_QUERY_PARAMS.length; i++) { + const sensParam = SENSITIVE_QUERY_PARAMS[i]; + if (params[sensParam] !== UNDEFINED_VALUE) { + params[sensParam] = STR_REDACTED; + anyParamRedacted = true; + } + } + + // If no parameters were redacted, return the original URL + if (!anyParamRedacted) { + return url; + } + + const parts: string[] = []; + objForEachKey(params, (key, value) => { + parts.push(value === null ? key : key + "=" + value); + }); + + return baseUrl + parts.join("&") + fragment; +} + +/** + * Redacts sensitive information from a URL string, including credentials and specific query parameters. + * @param input - The URL string to be redacted. + * @param config - Configuration object that contain redactionEnabled setting. + * @returns The redacted URL string or the original string if no redaction was needed or possible. + */ +export function fieldRedaction(input: string, config: IConfiguration): string { + if (!input) { + return input === UNDEFINED_VALUE ? "" : input; + } + if (input.indexOf(" ") !== -1) { + return input; // Checking for URLs with spaces + } + const isRedactionDisabled = config && config.redactionEnabled === false; + if (isRedactionDisabled) { + return input; + } + try { + let parsedUrl = redactUserInfo(input); + parsedUrl = redactQueryParameters(parsedUrl); + return parsedUrl; + } catch (e) { + return input; + } +} \ No newline at end of file diff --git a/shared/AppInsightsCore/src/applicationinsights-core-js.ts b/shared/AppInsightsCore/src/applicationinsights-core-js.ts index 1626815ed..12a4be475 100644 --- a/shared/AppInsightsCore/src/applicationinsights-core-js.ts +++ b/shared/AppInsightsCore/src/applicationinsights-core-js.ts @@ -60,7 +60,7 @@ export { getCrypto, getMsCrypto, getLocation, hasJSON, getJSON, isReactNative, getConsole, isIE, getIEVersion, isSafari, setEnableEnvMocks, isBeaconsSupported, isFetchSupported, useXDomainRequest, isXhrSupported, - findMetaTag, findNamedServerTiming, sendCustomEvent, dispatchEvent, createCustomDomEvent + findMetaTag, findNamedServerTiming, sendCustomEvent, dispatchEvent, createCustomDomEvent, fieldRedaction } from "./JavaScriptSDK/EnvUtils"; export { getGlobal, From 8787b2a25d67b581d10caf2aeef4ec5a7c82f334 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Thu, 29 May 2025 12:49:36 -0700 Subject: [PATCH 02/14] Modified how IConfiguration config was being declared for some files --- AISKU/Tests/Unit/src/AISKUSize.Tests.ts | 4 ++-- .../src/JavaScriptSDK/AnalyticsPlugin.ts | 20 ++++++++++++++----- .../src/DataCollector.ts | 11 ++++++---- .../src/Telemetry/Common/DataSanitizer.ts | 4 ++-- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts index 75f6cfc81..0c23d4cec 100644 --- a/AISKU/Tests/Unit/src/AISKUSize.Tests.ts +++ b/AISKU/Tests/Unit/src/AISKUSize.Tests.ts @@ -54,8 +54,8 @@ function _checkSize(checkType: string, maxSize: number, size: number, isNightly: } export class AISKUSizeCheck extends AITestClass { - private readonly MAX_RAW_SIZE = 147; - private readonly MAX_BUNDLE_SIZE = 147; + private readonly MAX_RAW_SIZE = 148; + private readonly MAX_BUNDLE_SIZE = 148; private readonly MAX_RAW_DEFLATE_SIZE = 59; private readonly MAX_BUNDLE_DEFLATE_SIZE = 59; private readonly rawFilePath = "../dist/es5/applicationinsights-web.min.js"; diff --git a/extensions/applicationinsights-analytics-js/src/JavaScriptSDK/AnalyticsPlugin.ts b/extensions/applicationinsights-analytics-js/src/JavaScriptSDK/AnalyticsPlugin.ts index af4136e35..a8cecd37a 100644 --- a/extensions/applicationinsights-analytics-js/src/JavaScriptSDK/AnalyticsPlugin.ts +++ b/extensions/applicationinsights-analytics-js/src/JavaScriptSDK/AnalyticsPlugin.ts @@ -264,7 +264,9 @@ export class AnalyticsPlugin extends BaseTelemetryPlugin implements IAppInsights _self.trackPageView = (pageView?: IPageViewTelemetry, customProperties?: ICustomProperties) => { try { let inPv = pageView || {}; - inPv.uri = fieldRedaction(inPv.uri, _extConfig); + if (_self.core && _self.core.config) { + inPv.uri = fieldRedaction(inPv.uri, _self.core.config); + } _pageViewManager.trackPageView(inPv, {...inPv.properties, ...inPv.measurements, ...customProperties}); if (_autoTrackPageVisitTime) { @@ -290,7 +292,9 @@ export class AnalyticsPlugin extends BaseTelemetryPlugin implements IAppInsights if (doc) { pageView.refUri = pageView.refUri === undefined ? doc.referrer : pageView.refUri; } - pageView.refUri = fieldRedaction(pageView.refUri, _extConfig); + if (_self.core && _self.core.config) { + pageView.refUri = fieldRedaction(pageView.refUri, _self.core.config); + } if (isNullOrUndefined(pageView.startTime)) { // calculate the start time manually let duration = ((properties || pageView.properties || {}).duration || 0); @@ -387,7 +391,9 @@ export class AnalyticsPlugin extends BaseTelemetryPlugin implements IAppInsights let loc = getLocation(); url = loc && loc.href || ""; } - url = fieldRedaction(url, _extConfig); + if (_self.core && _self.core.config) { + url = fieldRedaction(url, _self.core.config); + } _pageTracking.stop(name, url, properties, measurement); if (_autoTrackPageVisitTime) { @@ -803,7 +809,9 @@ export class AnalyticsPlugin extends BaseTelemetryPlugin implements IAppInsights } else { _currUri = locn && locn.href || ""; } - _currUri = fieldRedaction(_currUri, _extConfig); + if (_self.core && _self.core.config) { + _currUri = fieldRedaction(_currUri, _self.core.config); + } if (_enableAutoRouteTracking) { let distributedTraceCtx = _getDistributedTraceCtx(); if (distributedTraceCtx) { @@ -914,7 +922,9 @@ export class AnalyticsPlugin extends BaseTelemetryPlugin implements IAppInsights // array with max length of 2 that store current url and previous url for SPA page route change trackPageview use. let location = getLocation(true); _prevUri = location && location.href || ""; - _prevUri = fieldRedaction(_prevUri, _extConfig); + if (_self.core && _self.core.config) { + _prevUri = fieldRedaction(_prevUri, _self.core.config); + } _currUri = null; _evtNamespace = null; _extConfig = null; diff --git a/extensions/applicationinsights-clickanalytics-js/src/DataCollector.ts b/extensions/applicationinsights-clickanalytics-js/src/DataCollector.ts index a7c14ffbe..e725b5a27 100644 --- a/extensions/applicationinsights-clickanalytics-js/src/DataCollector.ts +++ b/extensions/applicationinsights-clickanalytics-js/src/DataCollector.ts @@ -2,7 +2,9 @@ * @copyright Microsoft 2020 */ -import { getDocument, getLocation, getWindow, hasDocument, isFunction, IConfiguration, fieldRedaction } from "@microsoft/applicationinsights-core-js"; +import { + IConfiguration, fieldRedaction, getDocument, getLocation, getWindow, hasDocument, isFunction +} from "@microsoft/applicationinsights-core-js"; import { scheduleTimeout } from "@nevware21/ts-utils"; import { IClickAnalyticsConfiguration, IOverrideValues } from "./Interfaces/Datamodel"; import { findClosestAnchor, isValueAssigned } from "./common/Utils"; @@ -126,7 +128,7 @@ export function getPageName(config: IClickAnalyticsConfiguration, overrideValues * @param location - window.location or document.location * @returns Flag indicating if an element is market PII. */ -export function sanitizeUrl(config: IClickAnalyticsConfiguration, location: Location, extConfig?: IConfiguration): string { +export function sanitizeUrl(config: IClickAnalyticsConfiguration, location: Location, rootConfig?: IConfiguration): string { if (!location) { return null; } @@ -141,8 +143,9 @@ export function sanitizeUrl(config: IClickAnalyticsConfiguration, location: Loca if (!!config.urlCollectQuery) { // false by default url += (isValueAssigned(location.search)? location.search : ""); } - - url = fieldRedaction(url, extConfig); + if (rootConfig) { + url = fieldRedaction(url, rootConfig); + } return url; } diff --git a/shared/AppInsightsCommon/src/Telemetry/Common/DataSanitizer.ts b/shared/AppInsightsCommon/src/Telemetry/Common/DataSanitizer.ts index 07a5beee1..daae63b91 100644 --- a/shared/AppInsightsCommon/src/Telemetry/Common/DataSanitizer.ts +++ b/shared/AppInsightsCommon/src/Telemetry/Common/DataSanitizer.ts @@ -5,7 +5,7 @@ import { IConfiguration, IDiagnosticLogger, _eInternalMessageId, _throwInternal, eLoggingSeverity, fieldRedaction, getJSON, hasJSON, isObject, objForEachKey, strTrim } from "@microsoft/applicationinsights-core-js"; -import { asString, strSubstr, strSubstring } from "@nevware21/ts-utils"; +import { asString, isString, strSubstr, strSubstring } from "@nevware21/ts-utils"; export const enum DataSanitizerValues { /** @@ -100,7 +100,7 @@ export function dataSanitizeString(logger: IDiagnosticLogger, value: any, maxLen } export function dataSanitizeUrl(logger: IDiagnosticLogger, url: any, config?: IConfiguration) { - if (typeof url === "string"){ + if (isString(url)) { url = fieldRedaction(url, config); } return dataSanitizeInput(logger, url, DataSanitizerValues.MAX_URL_LENGTH, _eInternalMessageId.UrlTooLong); From e9970849d16649a252c4a9c1f8890610a256a149 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Thu, 29 May 2025 17:51:00 -0700 Subject: [PATCH 03/14] Calling fieldRedaction method to redact URLs before being added to telemetry data --- examples/dependency/src/dependencies-example-index.ts | 9 ++++++++- .../src/events/PageAction.ts | 5 ++++- .../src/Context/TelemetryTrace.ts | 7 +++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/examples/dependency/src/dependencies-example-index.ts b/examples/dependency/src/dependencies-example-index.ts index 110c85684..c3cb5ec7a 100644 --- a/examples/dependency/src/dependencies-example-index.ts +++ b/examples/dependency/src/dependencies-example-index.ts @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { arrForEach } from "@microsoft/applicationinsights-core-js"; +import { arrForEach, fieldRedaction } from "@microsoft/applicationinsights-core-js"; +import { isString } from "@nevware21/ts-utils"; import { addDependencyListener, addDependencyInitializer, stopDependencyEvent, changeConfig, initApplicationInsights, getConfig, enableAjaxPerfTrackingConfig } from "./appinsights-init"; import { addHandlersButtonId, ajaxCallId, buttonSectionId, changeConfigButtonId, clearDetailsButtonId, clearDetailsList, clearEle, configContainerId, configDetails, createButton, createContainers, createDetailList, createFetchRequest, createUnTrackRequest, createXhrRequest, dependencyInitializerDetails, dependencyInitializerDetailsContainerId, dependencyListenerButtonId, dependencyListenerDetails, dependencyListenerDetailsContainerId, fetchCallId, fetchXhrId, removeAllHandlersId, stopDependencyEventButtonId, untrackFetchRequestId } from "./utils"; @@ -35,6 +36,12 @@ function addDependencyInitializerOnClick() { // Change properties of telemetry event "before" it's been processed details.item.name = "dependency-name"; details.item.properties.url = details.item?.target; + if (isString(details.item.properties.url)) { + const config = getConfig(); + if (config) { + details.item.properties.url = fieldRedaction(details.item.properties.url, config); + } + } details.context.initializer = "dependency-initializer-context"; createDetailList(dependencyInitializerDetails, details, dependencyInitializerDetailsContainerId, "Initializer"); diff --git a/extensions/applicationinsights-clickanalytics-js/src/events/PageAction.ts b/extensions/applicationinsights-clickanalytics-js/src/events/PageAction.ts index 8461e2d8f..408d321be 100644 --- a/extensions/applicationinsights-clickanalytics-js/src/events/PageAction.ts +++ b/extensions/applicationinsights-clickanalytics-js/src/events/PageAction.ts @@ -5,7 +5,7 @@ import dynamicProto from "@microsoft/dynamicproto-js"; import { strNotSpecified } from "@microsoft/applicationinsights-common"; import { - ICustomProperties, IDiagnosticLogger, ITelemetryItem, _eInternalMessageId, _throwInternal, eLoggingSeverity, getPerformance, objExtend, + ICustomProperties, IDiagnosticLogger, ITelemetryItem, _eInternalMessageId, _throwInternal, eLoggingSeverity, fieldRedaction, getPerformance, objExtend, objForEachKey } from "@microsoft/applicationinsights-core-js"; import { ClickAnalyticsPlugin } from "../ClickAnalyticsPlugin"; @@ -126,6 +126,9 @@ export class PageAction extends WebEvent { pageActionEvent.timeToAction = _getTimeToClick(); pageActionEvent.refUri = isValueAssigned(overrideValues.refUri) ? overrideValues.refUri : _self._config.coreData.referrerUri; + if (_self._clickAnalyticsPlugin.core.config) { + pageActionEvent.refUri = fieldRedaction(pageActionEvent.refUri, _self._clickAnalyticsPlugin.core.config); + } if(_isUndefinedEvent(pageActionEvent)) { return; } diff --git a/extensions/applicationinsights-properties-js/src/Context/TelemetryTrace.ts b/extensions/applicationinsights-properties-js/src/Context/TelemetryTrace.ts index f8b662391..06673d97f 100644 --- a/extensions/applicationinsights-properties-js/src/Context/TelemetryTrace.ts +++ b/extensions/applicationinsights-properties-js/src/Context/TelemetryTrace.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { ITelemetryTrace, ITraceState, dataSanitizeString } from "@microsoft/applicationinsights-common"; -import { IDiagnosticLogger, generateW3CId, getLocation } from "@microsoft/applicationinsights-core-js"; +import { IDiagnosticLogger, generateW3CId, getLocation, IConfiguration, fieldRedaction } from "@microsoft/applicationinsights-core-js"; export class TelemetryTrace implements ITelemetryTrace { @@ -12,13 +12,16 @@ export class TelemetryTrace implements ITelemetryTrace { public traceFlags: number; public name: string; - constructor(id?: string, parentId?: string, name?: string, logger?: IDiagnosticLogger) { + constructor(id?: string, parentId?: string, name?: string, logger?: IDiagnosticLogger, config?: IConfiguration) { const _self = this; _self.traceID = id || generateW3CId(); _self.parentID = parentId; let location = getLocation(); if (!name && location && location.pathname) { name = location.pathname; + if (config) { + name = fieldRedaction(name, config); + } } _self.name = dataSanitizeString(logger, name); From 18847f2c7812ce10fda1b227bd9572d232b9151b Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Wed, 4 Jun 2025 13:57:09 -0700 Subject: [PATCH 04/14] Fixed the AISKU failing test and added edge cases to unit tests for fieldredaction --- .../src/events/PageAction.ts | 4 +- .../src/Context/TelemetryTrace.ts | 2 +- .../Unit/src/ApplicationInsightsCore.Tests.ts | 111 +++++++++++++++++- .../src/JavaScriptSDK/EnvUtils.ts | 4 +- 4 files changed, 114 insertions(+), 7 deletions(-) diff --git a/extensions/applicationinsights-clickanalytics-js/src/events/PageAction.ts b/extensions/applicationinsights-clickanalytics-js/src/events/PageAction.ts index 408d321be..75d532993 100644 --- a/extensions/applicationinsights-clickanalytics-js/src/events/PageAction.ts +++ b/extensions/applicationinsights-clickanalytics-js/src/events/PageAction.ts @@ -5,8 +5,8 @@ import dynamicProto from "@microsoft/dynamicproto-js"; import { strNotSpecified } from "@microsoft/applicationinsights-common"; import { - ICustomProperties, IDiagnosticLogger, ITelemetryItem, _eInternalMessageId, _throwInternal, eLoggingSeverity, fieldRedaction, getPerformance, objExtend, - objForEachKey + ICustomProperties, IDiagnosticLogger, ITelemetryItem, _eInternalMessageId, _throwInternal, eLoggingSeverity, fieldRedaction, + getPerformance, objExtend, objForEachKey } from "@microsoft/applicationinsights-core-js"; import { ClickAnalyticsPlugin } from "../ClickAnalyticsPlugin"; import { getClickTarget } from "../DataCollector"; diff --git a/extensions/applicationinsights-properties-js/src/Context/TelemetryTrace.ts b/extensions/applicationinsights-properties-js/src/Context/TelemetryTrace.ts index 06673d97f..d5e35a5f3 100644 --- a/extensions/applicationinsights-properties-js/src/Context/TelemetryTrace.ts +++ b/extensions/applicationinsights-properties-js/src/Context/TelemetryTrace.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { ITelemetryTrace, ITraceState, dataSanitizeString } from "@microsoft/applicationinsights-common"; -import { IDiagnosticLogger, generateW3CId, getLocation, IConfiguration, fieldRedaction } from "@microsoft/applicationinsights-core-js"; +import { IConfiguration, IDiagnosticLogger, fieldRedaction, generateW3CId, getLocation } from "@microsoft/applicationinsights-core-js"; export class TelemetryTrace implements ITelemetryTrace { diff --git a/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts index 8ebe68274..59bff1a44 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts @@ -1947,9 +1947,9 @@ export class ApplicationInsightsCoreTests extends AITestClass { name: "should preserve query parameters while redacting auth when the query string is not in the set values", test: () => { let config = {} as IConfiguration; - const url = "https://www.example.com/path?color=blue&query=secret"; + const url = "AISKU/Tests/UnitTests.html?testId=7cff0834"; const redactedLocation = fieldRedaction(url, config); - Assert.notEqual(redactedLocation, "https://www.example.com/path?color=blue&query=REDACTED"); + Assert.equal(redactedLocation, "AISKU/Tests/UnitTests.html?testId=7cff0834"); } }); @@ -2061,6 +2061,113 @@ export class ApplicationInsightsCoreTests extends AITestClass { "Complex URL should have credentials and sensitive query parameters redacted while preserving other components"); } }); + this.testCase({ + name: "should handle completely empty URL string", + test: () => { + let config = {} as IConfiguration; + const url = ""; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "", "Empty string should be returned unchanged"); + } + }); + + this.testCase({ + name: "should handle URL with only whitespace characters", + test: () => { + let config = {} as IConfiguration; + const url = " \t\n "; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, " \t\n ", "String with only whitespace should be returned unchanged"); + } + }); + + this.testCase({ + name: "should handle malformed protocol URL", + test: () => { + let config = {} as IConfiguration; + const url = "http:/example.com"; // Missing slash + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "http:/example.com", "Malformed URL should be returned unchanged"); + } + }); + + this.testCase({ + name: "should handle URLs with unusual characters", + test: () => { + let config = {} as IConfiguration; + const url = "https://example.com/path with spaces?param=value with spaces"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://example.com/path with spaces?param=value with spaces", + "URL with spaces should be returned unchanged"); + } + }); + + this.testCase({ + name: "should handle URLs with Unicode characters", + test: () => { + let config = {} as IConfiguration; + const url = "https://user:пароль@例子.测试/路径?参数=值&sig=秘密"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@例子.测试/路径?参数=值&sig=REDACTED", + "URL with Unicode characters should have credentials and sensitive parameters redacted"); + } + }); + + this.testCase({ + name: "should handle improperly formatted credentials", + test: () => { + let config = {} as IConfiguration; + const url = "https://user:@example.com"; // Missing password + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com", + "URL with improperly formatted credentials should still redact auth"); + } + }); + + this.testCase({ + name: "should handle file URLs", + test: () => { + let config = {} as IConfiguration; + const url = "file:///C:/Users/username/Documents/file.txt"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "file:///C:/Users/username/Documents/file.txt", + "File URLs should be returned unchanged"); + } + }); + + this.testCase({ + name: "should handle data URLs", + test: () => { + let config = {} as IConfiguration; + const url = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==", + "Data URLs should be returned unchanged"); + } + }); + + this.testCase({ + name: "should handle URLs with multiple query parameters to redact", + test: () => { + let config = {} as IConfiguration; + const url = "https://example.com/path?sig=secret&X-Goog-Signature=anothersecret&AWSAccessKeyId=keyvalue&color=blue"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://example.com/path?sig=REDACTED&X-Goog-Signature=REDACTED&AWSAccessKeyId=REDACTED&color=blue", + "URL with multiple sensitive query parameters should have all sensitive parameters redacted"); + } + }); + + this.testCase({ + name: "should handle extremely long URLs", + test: () => { + let config = {} as IConfiguration; + let longParam = "value".repeat(1000); + const url = `https://user:pass@example.com/path?param=${longParam}`; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, `https://REDACTED:REDACTED@example.com/path?param=${longParam}`, + "Extremely long URLs should be handled correctly"); + } + }); function _createBuckets(num: number) { // Using helper function as TypeScript 2.5.3 is complaining about new Array(100).fill(0); diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts index 6035e0ece..74660273c 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts @@ -477,8 +477,8 @@ function redactQueryParameters(url: string): string { * @returns The redacted URL string or the original string if no redaction was needed or possible. */ export function fieldRedaction(input: string, config: IConfiguration): string { - if (!input) { - return input === UNDEFINED_VALUE ? "" : input; + if (!input){ + return input; } if (input.indexOf(" ") !== -1) { return input; // Checking for URLs with spaces From 08a311387a29cb4daa7db8fd59810d5b37264556 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Tue, 10 Jun 2025 13:37:02 -0700 Subject: [PATCH 05/14] Sensitive query keys is now configurable. Added corresponding unit tests --- .../Unit/src/ApplicationInsightsCore.Tests.ts | 77 ++++++++++++++++++- .../IConfiguration.ts | 10 ++- .../src/JavaScriptSDK/EnvUtils.ts | 23 +++--- 3 files changed, 94 insertions(+), 16 deletions(-) diff --git a/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts index 59bff1a44..974df05f4 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts @@ -1947,9 +1947,9 @@ export class ApplicationInsightsCoreTests extends AITestClass { name: "should preserve query parameters while redacting auth when the query string is not in the set values", test: () => { let config = {} as IConfiguration; - const url = "AISKU/Tests/UnitTests.html?testId=7cff0834"; + const url = "AISKU/Tests/UnitTests.html?sig=7cff0834"; const redactedLocation = fieldRedaction(url, config); - Assert.equal(redactedLocation, "AISKU/Tests/UnitTests.html?testId=7cff0834"); + Assert.equal(redactedLocation, "AISKU/Tests/UnitTests.html?sig=REDACTED"); } }); @@ -2123,7 +2123,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { "URL with improperly formatted credentials should still redact auth"); } }); - + this.testCase({ name: "should handle file URLs", test: () => { @@ -2168,6 +2168,77 @@ export class ApplicationInsightsCoreTests extends AITestClass { "Extremely long URLs should be handled correctly"); } }); + + this.testCase({ + name: "should redact custom query parameters defined in redactQueryParams", + test: () => { + let config = { + redactQueryParams: ["authorize", "api_key", "password"] + } 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=REDACTED", + "URL with custom sensitive parameters should have them redacted while preserving other parameters"); + } + }); + this.testCase({ + name: "should redact both default and custom query parameters", + test: () => { + let config = { + redactQueryParams: ["auth_token"] + } as IConfiguration; + + const url = "https://example.com/path?sig=abc123&auth_token=12345&name=test"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://example.com/path?sig=REDACTED&auth_token=REDACTED&name=test", + "URL with both default and custom sensitive parameters should have all redacted"); + } + }); + this.testCase({ + name: "should not redact custom parameters when redaction is disabled", + test: () => { + let config = { + redactionEnabled: false, + redactQueryParams: ["authorize", "api_key"] + } as IConfiguration; + + const url = "https://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"); + } + }); + + this.testCase({ + name: "should handle empty redactQueryParams array", + test: () => { + let config = { + redactQueryParams: [] + } as IConfiguration; + + // Should still redact default parameters + const url = "https://example.com/path?Signature=secret&custom_param=value"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://example.com/path?Signature=REDACTED&custom_param=value", + "URL with default sensitive parameters should still be redacted with empty custom array"); + } + }); + + this.testCase({ + name: "should handle complex URLs with both credentials and custom query parameters", + test: () => { + let config = { + redactQueryParams: ["authorize", "session_id"] + } as IConfiguration; + + const url = "https://user:pass@example.com/path?sig=secret&authorize=abc123&visible=true&session_id=xyz789"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, + "https://REDACTED:REDACTED@example.com/path?sig=REDACTED&authorize=REDACTED&visible=true&session_id=REDACTED", + "Complex URL should have both credentials and all sensitive parameters redacted"); + } + }); function _createBuckets(num: number) { // Using helper function as TypeScript 2.5.3 is complaining about new Array(100).fill(0); diff --git a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts index ca01d36e3..0d09a62dd 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts @@ -232,12 +232,20 @@ export interface IConfiguration { */ expCfg?: IExceptionConfig; - /** + /** * [Optional] A flag to enable or disable the use of the field redaction for urls. * @defaultValue true */ redactionEnabled?: boolean; + /** + * [Optional] Additional query parameters to redact beyond the default set. + * Use this to specify custom parameters that contain sensitive information. + * These will be combined with the default parameters that are redacted. + * @example ["auth_token", "api_key", "private_data"] + */ + redactQueryParams?: string[]; + ///** // * [Optional] Internal SDK configuration for developers // * @internal diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts index 74660273c..855de2ddd 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts @@ -31,14 +31,6 @@ const strReactNative = "ReactNative"; const strMsie = "msie"; const strTrident = "trident/"; const strXMLHttpRequest = "XMLHttpRequest"; - -const SENSITIVE_QUERY_PARAMS = [ - "sig", - "Signature", - "AWSAccessKeyId", - "X-Goog-Signature" -] as const; - const STR_REDACTED = "REDACTED"; let _isTrident: boolean = null; @@ -408,7 +400,9 @@ function redactUserInfo(url: string): string { * @param url - The URL string to redact * @returns The URL with sensitive query parameters redacted */ -function redactQueryParameters(url: string): string { +function redactQueryParameters(url: string, config?: IConfiguration): string { + const DEFAULT_SENSITIVE_PARAMS = ["sig", "Signature", "AWSAccessKeyId", "X-Goog-Signature"]; + let sensitiveParams: string[]; const questionMarkIndex = strIndexOf(url, "?"); if (questionMarkIndex === -1) { return url; @@ -449,8 +443,13 @@ function redactQueryParameters(url: string): string { // Check if any parameters need redaction let anyParamRedacted = false; - for (let i = 0; i < SENSITIVE_QUERY_PARAMS.length; i++) { - const sensParam = SENSITIVE_QUERY_PARAMS[i]; + if (config && config.redactQueryParams) { + sensitiveParams = [...DEFAULT_SENSITIVE_PARAMS, ...config.redactQueryParams]; + } else { + sensitiveParams = DEFAULT_SENSITIVE_PARAMS; + } + for (let i = 0; i < sensitiveParams.length; i++) { + const sensParam = sensitiveParams[i]; if (params[sensParam] !== UNDEFINED_VALUE) { params[sensParam] = STR_REDACTED; anyParamRedacted = true; @@ -489,7 +488,7 @@ export function fieldRedaction(input: string, config: IConfiguration): string { } try { let parsedUrl = redactUserInfo(input); - parsedUrl = redactQueryParameters(parsedUrl); + parsedUrl = redactQueryParameters(parsedUrl, config); return parsedUrl; } catch (e) { return input; From 5de2a8ca40787524902c5dae4b5746198fbad37a Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Mon, 23 Jun 2025 16:49:20 -0700 Subject: [PATCH 06/14] Addressed review comments --- .../src/dependencies-example-index.ts | 9 +--- .../src/DataCollector.ts | 4 +- .../src/events/PageAction.ts | 7 +-- .../Tests/Unit/src/AppInsightsCommon.tests.ts | 53 +++++++++++++++---- .../IConfiguration.ts | 3 +- .../src/JavaScriptSDK/InternalConstants.ts | 5 +- 6 files changed, 52 insertions(+), 29 deletions(-) diff --git a/examples/dependency/src/dependencies-example-index.ts b/examples/dependency/src/dependencies-example-index.ts index c3cb5ec7a..110c85684 100644 --- a/examples/dependency/src/dependencies-example-index.ts +++ b/examples/dependency/src/dependencies-example-index.ts @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { arrForEach, fieldRedaction } from "@microsoft/applicationinsights-core-js"; -import { isString } from "@nevware21/ts-utils"; +import { arrForEach } from "@microsoft/applicationinsights-core-js"; import { addDependencyListener, addDependencyInitializer, stopDependencyEvent, changeConfig, initApplicationInsights, getConfig, enableAjaxPerfTrackingConfig } from "./appinsights-init"; import { addHandlersButtonId, ajaxCallId, buttonSectionId, changeConfigButtonId, clearDetailsButtonId, clearDetailsList, clearEle, configContainerId, configDetails, createButton, createContainers, createDetailList, createFetchRequest, createUnTrackRequest, createXhrRequest, dependencyInitializerDetails, dependencyInitializerDetailsContainerId, dependencyListenerButtonId, dependencyListenerDetails, dependencyListenerDetailsContainerId, fetchCallId, fetchXhrId, removeAllHandlersId, stopDependencyEventButtonId, untrackFetchRequestId } from "./utils"; @@ -36,12 +35,6 @@ function addDependencyInitializerOnClick() { // Change properties of telemetry event "before" it's been processed details.item.name = "dependency-name"; details.item.properties.url = details.item?.target; - if (isString(details.item.properties.url)) { - const config = getConfig(); - if (config) { - details.item.properties.url = fieldRedaction(details.item.properties.url, config); - } - } details.context.initializer = "dependency-initializer-context"; createDetailList(dependencyInitializerDetails, details, dependencyInitializerDetailsContainerId, "Initializer"); diff --git a/extensions/applicationinsights-clickanalytics-js/src/DataCollector.ts b/extensions/applicationinsights-clickanalytics-js/src/DataCollector.ts index e725b5a27..f8dfec54a 100644 --- a/extensions/applicationinsights-clickanalytics-js/src/DataCollector.ts +++ b/extensions/applicationinsights-clickanalytics-js/src/DataCollector.ts @@ -143,9 +143,7 @@ export function sanitizeUrl(config: IClickAnalyticsConfiguration, location: Loca if (!!config.urlCollectQuery) { // false by default url += (isValueAssigned(location.search)? location.search : ""); } - if (rootConfig) { - url = fieldRedaction(url, rootConfig); - } + url = fieldRedaction(url, rootConfig); return url; } diff --git a/extensions/applicationinsights-clickanalytics-js/src/events/PageAction.ts b/extensions/applicationinsights-clickanalytics-js/src/events/PageAction.ts index 75d532993..8461e2d8f 100644 --- a/extensions/applicationinsights-clickanalytics-js/src/events/PageAction.ts +++ b/extensions/applicationinsights-clickanalytics-js/src/events/PageAction.ts @@ -5,8 +5,8 @@ import dynamicProto from "@microsoft/dynamicproto-js"; import { strNotSpecified } from "@microsoft/applicationinsights-common"; import { - ICustomProperties, IDiagnosticLogger, ITelemetryItem, _eInternalMessageId, _throwInternal, eLoggingSeverity, fieldRedaction, - getPerformance, objExtend, objForEachKey + ICustomProperties, IDiagnosticLogger, ITelemetryItem, _eInternalMessageId, _throwInternal, eLoggingSeverity, getPerformance, objExtend, + objForEachKey } from "@microsoft/applicationinsights-core-js"; import { ClickAnalyticsPlugin } from "../ClickAnalyticsPlugin"; import { getClickTarget } from "../DataCollector"; @@ -126,9 +126,6 @@ export class PageAction extends WebEvent { pageActionEvent.timeToAction = _getTimeToClick(); pageActionEvent.refUri = isValueAssigned(overrideValues.refUri) ? overrideValues.refUri : _self._config.coreData.referrerUri; - if (_self._clickAnalyticsPlugin.core.config) { - pageActionEvent.refUri = fieldRedaction(pageActionEvent.refUri, _self._clickAnalyticsPlugin.core.config); - } if(_isUndefinedEvent(pageActionEvent)) { return; } diff --git a/shared/AppInsightsCommon/Tests/Unit/src/AppInsightsCommon.tests.ts b/shared/AppInsightsCommon/Tests/Unit/src/AppInsightsCommon.tests.ts index 44a23ddab..1da2c675f 100644 --- a/shared/AppInsightsCommon/Tests/Unit/src/AppInsightsCommon.tests.ts +++ b/shared/AppInsightsCommon/Tests/Unit/src/AppInsightsCommon.tests.ts @@ -1,6 +1,6 @@ import { strRepeat } from "@nevware21/ts-utils"; import { Assert, AITestClass } from "@microsoft/ai-test-framework"; -import { DiagnosticLogger } from "@microsoft/applicationinsights-core-js"; +import { DiagnosticLogger, IConfiguration } from "@microsoft/applicationinsights-core-js"; import { dataSanitizeInput, dataSanitizeKey, dataSanitizeMessage, DataSanitizerValues, dataSanitizeString, dataSanitizeUrl } from "../../../src/Telemetry/Common/DataSanitizer"; @@ -294,19 +294,32 @@ export class ApplicationInsightsTests extends AITestClass { } }); this.testCase({ - name: 'DataSanitizerTests: dataSanitizerUrl properly redacts credentials in URLs', + name: 'DataSanitizerTests: dataSanitizeUrl properly redacts credentials in URLs with config enabled', test: () => { // URLs with credentials + let config = {} as IConfiguration; const urlWithCredentials = "https://username:password@example.com/path"; const expectedRedactedUrl = "https://REDACTED:REDACTED@example.com/path"; // Act & Assert - const result = dataSanitizeUrl(this.logger, urlWithCredentials); + const result = dataSanitizeUrl(this.logger, urlWithCredentials, config); Assert.equal(expectedRedactedUrl, result); } }); this.testCase({ - name: 'DataSanitizerTests: dataSanitizerUrl handles invalid URLs', + name: 'DataSanitizerTests: dataSanitizeUrl properly redacts credentials in URLs with config disabled', + test: () => { + // URLs with credentials + let config = {redactionEnabled: false} as IConfiguration; + const urlWithCredentials = "https://username:password@example.com/path"; + + // Act & Assert + const result = dataSanitizeUrl(this.logger, urlWithCredentials, config); + Assert.equal(urlWithCredentials, result); + } + }); + this.testCase({ + name: 'DataSanitizerTests: dataSanitizeUrl handles invalid URLs', test: () => { // Invalid URL that will cause URL constructor to throw const invalidUrl = 123545; @@ -317,9 +330,10 @@ export class ApplicationInsightsTests extends AITestClass { } }); this.testCase({ - name: 'DataSanitizerTests: dataSanitizerUrl still enforces maximum length after redaction', + name: 'DataSanitizerTests: dataSanitizeUrl still enforces maximum length after redaction', test: () => { // Setup + let config = {} as IConfiguration; const loggerStub = this.sandbox.stub(this.logger, "throwInternal"); const MAX_URL_LENGTH = DataSanitizerValues.MAX_URL_LENGTH; @@ -329,7 +343,7 @@ export class ApplicationInsightsTests extends AITestClass { const longUrl = longBaseUrl + longPathPart; // Act - const result = dataSanitizeUrl(this.logger, longUrl); + const result = dataSanitizeUrl(this.logger, longUrl, config); // Assert Assert.equal(MAX_URL_LENGTH, result.length, "URL should be truncated to maximum length"); @@ -340,7 +354,7 @@ export class ApplicationInsightsTests extends AITestClass { } }); this.testCase({ - name: 'DataSanitizerTests: dataSanitizerUrl handles null and undefined inputs', + name: 'DataSanitizerTests: dataSanitizeUrl handles null and undefined inputs', test: () => { // Act & Assert const nullResult = dataSanitizeUrl(this.logger, null); @@ -351,25 +365,42 @@ export class ApplicationInsightsTests extends AITestClass { } }); this.testCase({ - name: 'DataSanitizerTests: dataSanitizerUrl preserves URLs with no sensitive information', + name: 'DataSanitizerTests: dataSanitizeUrl preserves URLs with no sensitive information', test: () => { // URL with no sensitive information + let config = {} as IConfiguration; const safeUrl = "https://example.com/api?param1=value1¶m2=value2"; // Act & Assert - const result = dataSanitizeUrl(this.logger, safeUrl); + const result = dataSanitizeUrl(this.logger, safeUrl, config); Assert.equal(safeUrl, result, "URL with no sensitive info should remain unchanged"); } }); this.testCase({ - name: 'DataSanitizerTests: dataSanitizerUrl properly redacts sensitive query parameters', + name: 'DataSanitizerTests: dataSanitizeUrl properly redacts sensitive query parameters', test: () => { // URLs with sensitive query parameters + let config = {} as IConfiguration; const urlWithSensitiveParams = "https://example.com/api?Signature=secret&normal=value"; const expectedRedactedUrl = "https://example.com/api?Signature=REDACTED&normal=value"; // Act & Assert - const result = dataSanitizeUrl(this.logger, urlWithSensitiveParams); + const result = dataSanitizeUrl(this.logger, urlWithSensitiveParams, config); + Assert.equal(expectedRedactedUrl, result); + } + }); + this.testCase({ + name: 'DataSanitizerTests: dataSanitizeUrl properly redacts sensitive query parameters (default + custom)', + test: () => { + // URLs with sensitive query parameters + let config = { + redactQueryParams: ["authorize", "api_key", "password"] + } as IConfiguration; + const urlWithSensitiveParams = "https://example.com/api?Signature=secret&authorize=value"; + const expectedRedactedUrl = "https://example.com/api?Signature=REDACTED&authorize=REDACTED"; + + // Act & Assert + const result = dataSanitizeUrl(this.logger, urlWithSensitiveParams, config); Assert.equal(expectedRedactedUrl, result); } }); diff --git a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts index 0d09a62dd..6e94b0954 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts @@ -242,7 +242,8 @@ export interface IConfiguration { * [Optional] Additional query parameters to redact beyond the default set. * Use this to specify custom parameters that contain sensitive information. * These will be combined with the default parameters that are redacted. - * @example ["auth_token", "api_key", "private_data"] + * @defaultValue ["sig", "Signature", "AWSAccessKeyId", "X-Goog-Signature"] + * @example ["sig", "Signature", "AWSAccessKeyId", "X-Goog-Signature","auth_token", "api_key", "private_data"] */ redactQueryParams?: string[]; diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/InternalConstants.ts b/shared/AppInsightsCore/src/JavaScriptSDK/InternalConstants.ts index be722fa21..12da0a457 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/InternalConstants.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/InternalConstants.ts @@ -28,4 +28,7 @@ export const STR_GET_PERF_MGR = "getPerfMgr"; export const STR_DOMAIN = "domain"; export const STR_PATH = "path"; -export const STR_NOT_DYNAMIC_ERROR = "Not dynamic - "; \ No newline at end of file +export const STR_NOT_DYNAMIC_ERROR = "Not dynamic - "; + +export const STR_REDACTED = "REDACTED"; +export const DEFAULT_SENSITIVE_PARAMS = ["sig", "Signature", "AWSAccessKeyId", "X-Goog-Signature"]; \ No newline at end of file From 7f1e3682a3d7b3fb86755e424e00c6bf50217197 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Mon, 23 Jun 2025 18:47:45 -0700 Subject: [PATCH 07/14] Added concat for minification --- shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts index 855de2ddd..f2194d1ad 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts @@ -444,7 +444,7 @@ function redactQueryParameters(url: string, config?: IConfiguration): string { // Check if any parameters need redaction let anyParamRedacted = false; if (config && config.redactQueryParams) { - sensitiveParams = [...DEFAULT_SENSITIVE_PARAMS, ...config.redactQueryParams]; + sensitiveParams = DEFAULT_SENSITIVE_PARAMS.concat(config.redactQueryParams); } else { sensitiveParams = DEFAULT_SENSITIVE_PARAMS; } From 28519a7c56674dbd70279b0d6e6a750f884f2b2c Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Mon, 23 Jun 2025 18:58:27 -0700 Subject: [PATCH 08/14] Using default params from internal constants --- shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts index f2194d1ad..340bbd618 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts @@ -9,7 +9,7 @@ import { } from "@nevware21/ts-utils"; import { IConfiguration } from "../applicationinsights-core-js"; import { strContains } from "./HelperFuncs"; -import { STR_EMPTY, UNDEFINED_VALUE } from "./InternalConstants"; +import { DEFAULT_SENSITIVE_PARAMS, STR_EMPTY, UNDEFINED_VALUE } from "./InternalConstants"; // TypeScript removed this interface so we need to declare the global so we can check for it's existence. declare var XDomainRequest: any; @@ -401,7 +401,6 @@ function redactUserInfo(url: string): string { * @returns The URL with sensitive query parameters redacted */ function redactQueryParameters(url: string, config?: IConfiguration): string { - const DEFAULT_SENSITIVE_PARAMS = ["sig", "Signature", "AWSAccessKeyId", "X-Goog-Signature"]; let sensitiveParams: string[]; const questionMarkIndex = strIndexOf(url, "?"); if (questionMarkIndex === -1) { From 23f7096d5934b10e8e93338a04de7c6abb57765c Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Tue, 24 Jun 2025 08:40:43 -0700 Subject: [PATCH 09/14] Fixed usage of constant --- shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts index 340bbd618..9c5953c39 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts @@ -9,7 +9,7 @@ import { } from "@nevware21/ts-utils"; import { IConfiguration } from "../applicationinsights-core-js"; import { strContains } from "./HelperFuncs"; -import { DEFAULT_SENSITIVE_PARAMS, STR_EMPTY, UNDEFINED_VALUE } from "./InternalConstants"; +import { DEFAULT_SENSITIVE_PARAMS, STR_EMPTY, STR_REDACTED, UNDEFINED_VALUE } from "./InternalConstants"; // TypeScript removed this interface so we need to declare the global so we can check for it's existence. declare var XDomainRequest: any; @@ -31,7 +31,6 @@ const strReactNative = "ReactNative"; const strMsie = "msie"; const strTrident = "trident/"; const strXMLHttpRequest = "XMLHttpRequest"; -const STR_REDACTED = "REDACTED"; let _isTrident: boolean = null; let _navUserAgentCheck: string = null; From adffd01f9527d5719afec35e13eb9a6eaca37e3c Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Wed, 2 Jul 2025 14:12:49 -0700 Subject: [PATCH 10/14] Updated the config from redactionEnabled to redactUrls --- .../Tests/Unit/src/AppInsightsCommon.tests.ts | 2 +- .../Tests/Unit/src/ApplicationInsightsCore.Tests.ts | 8 ++++---- .../src/JavaScriptSDK.Interfaces/IConfiguration.ts | 6 +++--- shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/shared/AppInsightsCommon/Tests/Unit/src/AppInsightsCommon.tests.ts b/shared/AppInsightsCommon/Tests/Unit/src/AppInsightsCommon.tests.ts index 1da2c675f..16e034509 100644 --- a/shared/AppInsightsCommon/Tests/Unit/src/AppInsightsCommon.tests.ts +++ b/shared/AppInsightsCommon/Tests/Unit/src/AppInsightsCommon.tests.ts @@ -310,7 +310,7 @@ export class ApplicationInsightsTests extends AITestClass { name: 'DataSanitizerTests: dataSanitizeUrl properly redacts credentials in URLs with config disabled', test: () => { // URLs with credentials - let config = {redactionEnabled: false} as IConfiguration; + let config = {redactUrls: false} as IConfiguration; const urlWithCredentials = "https://username:password@example.com/path"; // Act & Assert diff --git a/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts index 974df05f4..d19220fed 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts @@ -1914,7 +1914,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { let config = {} as IConfiguration; const url = "https://user:password@example.com"; - if (config.redactionEnabled){ + if (config.redactUrls){ const redactedLocation = fieldRedaction(url, config); Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com"); } @@ -1956,9 +1956,9 @@ export class ApplicationInsightsCoreTests extends AITestClass { this.testCase({ name: "should preserve query parameters while redacting auth - AWSAccessKeyId", test: () => { - let config = {redactionEnabled: false} as IConfiguration; + let config = {redactUrls: false} as IConfiguration; const url = "https://www.example.com/path?color=blue&AWSAccessKeyId=secret"; - if (config.redactionEnabled){ + if (config.redactUrls){ const redactedLocation = fieldRedaction(url, config); Assert.equal(redactedLocation, "https://www.example.com/path?color=blue&AWSAccessKeyId=REDACTED"); } @@ -2199,7 +2199,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { name: "should not redact custom parameters when redaction is disabled", test: () => { let config = { - redactionEnabled: false, + redactUrls: false, redactQueryParams: ["authorize", "api_key"] } as IConfiguration; diff --git a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts index 6e94b0954..faeae362b 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts @@ -157,7 +157,7 @@ export interface IConfiguration { * [Optional] An array of the page unload events that you would like to be ignored, special note there must be at least one valid unload * event hooked, if you list all or the runtime environment only supports a listed "disabled" event it will still be hooked, if required by the SDK. * Unload events include "beforeunload", "unload", "visibilitychange" (with 'hidden' state) and "pagehide". - * + * * This can be used to avoid jQuery 3.7.1+ deprecation warnings and Chrome warnings about the unload event: * @example * ```javascript @@ -174,7 +174,7 @@ export interface IConfiguration { * [Optional] An array of page show events that you would like to be ignored, special note there must be at lease one valid show event * hooked, if you list all or the runtime environment only supports a listed (disabled) event it will STILL be hooked, if required by the SDK. * Page Show events include "pageshow" and "visibilitychange" (with 'visible' state). - * + * * @example * ```javascript * { @@ -236,7 +236,7 @@ export interface IConfiguration { * [Optional] A flag to enable or disable the use of the field redaction for urls. * @defaultValue true */ - redactionEnabled?: boolean; + redactUrls?: boolean; /** * [Optional] Additional query parameters to redact beyond the default set. diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts index 9c5953c39..31cfbe029 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts @@ -470,7 +470,7 @@ function redactQueryParameters(url: string, config?: IConfiguration): string { /** * Redacts sensitive information from a URL string, including credentials and specific query parameters. * @param input - The URL string to be redacted. - * @param config - Configuration object that contain redactionEnabled setting. + * @param config - Configuration object that contain redactUrls setting. * @returns The redacted URL string or the original string if no redaction was needed or possible. */ export function fieldRedaction(input: string, config: IConfiguration): string { @@ -480,7 +480,7 @@ export function fieldRedaction(input: string, config: IConfiguration): string { if (input.indexOf(" ") !== -1) { return input; // Checking for URLs with spaces } - const isRedactionDisabled = config && config.redactionEnabled === false; + const isRedactionDisabled = config && config.redactUrls === false; if (isRedactionDisabled) { return input; } From 9933fe38058e205cf93d4bf9bb73be553b62b9d2 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Wed, 2 Jul 2025 14:24:29 -0700 Subject: [PATCH 11/14] Fixed trailing spaces --- .../src/JavaScriptSDK.Interfaces/IConfiguration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts index faeae362b..8f421015c 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK.Interfaces/IConfiguration.ts @@ -157,7 +157,7 @@ export interface IConfiguration { * [Optional] An array of the page unload events that you would like to be ignored, special note there must be at least one valid unload * event hooked, if you list all or the runtime environment only supports a listed "disabled" event it will still be hooked, if required by the SDK. * Unload events include "beforeunload", "unload", "visibilitychange" (with 'hidden' state) and "pagehide". - * + * * This can be used to avoid jQuery 3.7.1+ deprecation warnings and Chrome warnings about the unload event: * @example * ```javascript @@ -174,7 +174,7 @@ export interface IConfiguration { * [Optional] An array of page show events that you would like to be ignored, special note there must be at lease one valid show event * hooked, if you list all or the runtime environment only supports a listed (disabled) event it will STILL be hooked, if required by the SDK. * Page Show events include "pageshow" and "visibilitychange" (with 'visible' state). - * + * * @example * ```javascript * { From fe9cc76f66be3add406f51d4219220e3ae798a3c Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Wed, 9 Jul 2025 15:15:17 -0700 Subject: [PATCH 12/14] Addressed review comments --- .../Unit/src/ApplicationInsightsCore.Tests.ts | 414 ++++++++++++++++-- .../src/JavaScriptSDK/EnvUtils.ts | 130 +++--- 2 files changed, 436 insertions(+), 108 deletions(-) diff --git a/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts index d19220fed..d257db44a 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts @@ -1909,12 +1909,12 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should redact basic auth credentials from URL", + name: "FieldRedaction: should redact basic auth credentials from URL", test: () => { let config = {} as IConfiguration; const url = "https://user:password@example.com"; - if (config.redactUrls){ + if (config.redactUrls === true){ const redactedLocation = fieldRedaction(url, config); Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com"); } @@ -1924,7 +1924,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should not modify URL without credentials", + name: "FieldRedaction:should not modify URL without credentials", test: () => { let config = {} as IConfiguration; const url = "https://example.com/path"; @@ -1934,7 +1934,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should preserve query parameters while redacting auth", + name: "FieldRedaction: should preserve query parameters while redacting auth", test: () => { let config = {} as IConfiguration; const url = "https://www.example.com/path?color=blue&X-Goog-Signature=secret"; @@ -1944,7 +1944,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should preserve query parameters while redacting auth when the query string is not in the set values", + name: "FieldRedaction: should preserve query parameters while redacting auth when the query string is not in the set values", test: () => { let config = {} as IConfiguration; const url = "AISKU/Tests/UnitTests.html?sig=7cff0834"; @@ -1954,11 +1954,11 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should preserve query parameters while redacting auth - AWSAccessKeyId", + name: "FieldRedaction: should preserve query parameters while redacting auth - AWSAccessKeyId", test: () => { let config = {redactUrls: false} as IConfiguration; const url = "https://www.example.com/path?color=blue&AWSAccessKeyId=secret"; - if (config.redactUrls){ + if (config.redactUrls === true){ const redactedLocation = fieldRedaction(url, config); Assert.equal(redactedLocation, "https://www.example.com/path?color=blue&AWSAccessKeyId=REDACTED"); } @@ -1967,7 +1967,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle invalid URL format", + name: "FieldRedaction: should handle invalid URL format", test: () => { let config = {} as IConfiguration; const url = "invalid-url"; @@ -1977,7 +1977,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle special characters in credentials", + name: "FieldRedaction: should handle special characters in credentials", test: () => { let config = {} as IConfiguration; const url = "https://user%20name:pass%20word@example.com" @@ -1988,7 +1988,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle URLs with multiple @ symbols", + name: "FieldRedaction: should handle URLs with multiple @ symbols", test: () => { let config = {} as IConfiguration; const url = "https://user:pass@example.com/path@somewhere" @@ -1998,7 +1998,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle empty URLs", + name: "FieldRedaction: should handle empty URLs", test: () => { let config = {} as IConfiguration; const url = " "; @@ -2008,7 +2008,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should properly redact URLs with ports", + name: "FieldRedaction: should properly redact URLs with ports", test: () => { let config = {} as IConfiguration; const url = "https://user:pass@example.com:8080/path"; @@ -2019,7 +2019,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should properly redact URLs with fragments", + name: "FieldRedaction: should properly redact URLs with fragments", test: () => { let config = {} as IConfiguration; const url = "https://user:pass@example.com/path?param=value#section"; @@ -2030,7 +2030,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle port-only URLs without credentials", + name: "FieldRedaction: should handle port-only URLs without credentials", test: () => { let config = {} as IConfiguration; const url = "https://example.com:8080/api"; @@ -2041,7 +2041,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle URLs with IP addresses, ports and credentials", + name: "FieldRedaction: should handle URLs with IP addresses, ports and credentials", test: () => { let config = {} as IConfiguration; const url = "https://admin:secret@192.168.1.1:8443/admin"; @@ -2052,7 +2052,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle complex URLs with port, query parameters and fragment", + name: "FieldRedaction: should handle complex URLs with port, query parameters and fragment", test: () => { let config = {} as IConfiguration; const url = "https://username:password@example.com:8443/path/to/resource?sig=secret&color=blue#section2"; @@ -2062,7 +2062,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { } }); this.testCase({ - name: "should handle completely empty URL string", + name: "FieldRedaction: should handle completely empty URL string", test: () => { let config = {} as IConfiguration; const url = ""; @@ -2072,7 +2072,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle URL with only whitespace characters", + name: "FieldRedaction: should handle URL with only whitespace characters", test: () => { let config = {} as IConfiguration; const url = " \t\n "; @@ -2082,7 +2082,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle malformed protocol URL", + name: "FieldRedaction: should handle malformed protocol URL", test: () => { let config = {} as IConfiguration; const url = "http:/example.com"; // Missing slash @@ -2092,7 +2092,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle URLs with unusual characters", + name: "FieldRedaction: should handle URLs with unusual characters", test: () => { let config = {} as IConfiguration; const url = "https://example.com/path with spaces?param=value with spaces"; @@ -2103,7 +2103,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle URLs with Unicode characters", + name: "FieldRedaction: should handle URLs with Unicode characters", test: () => { let config = {} as IConfiguration; const url = "https://user:пароль@例子.测试/路径?参数=值&sig=秘密"; @@ -2114,18 +2114,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle improperly formatted credentials", - test: () => { - let config = {} as IConfiguration; - const url = "https://user:@example.com"; // Missing password - const redactedLocation = fieldRedaction(url, config); - Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com", - "URL with improperly formatted credentials should still redact auth"); - } - }); - - this.testCase({ - name: "should handle file URLs", + name: "FieldRedaction: should handle file URLs", test: () => { let config = {} as IConfiguration; const url = "file:///C:/Users/username/Documents/file.txt"; @@ -2136,7 +2125,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle data URLs", + name: "FieldRedaction: should handle data URLs", test: () => { let config = {} as IConfiguration; const url = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="; @@ -2147,7 +2136,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle URLs with multiple query parameters to redact", + name: "FieldRedaction: should handle URLs with multiple query parameters to redact", test: () => { let config = {} as IConfiguration; const url = "https://example.com/path?sig=secret&X-Goog-Signature=anothersecret&AWSAccessKeyId=keyvalue&color=blue"; @@ -2158,7 +2147,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle extremely long URLs", + name: "FieldRedaction: should handle extremely long URLs", test: () => { let config = {} as IConfiguration; let longParam = "value".repeat(1000); @@ -2170,7 +2159,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should redact custom query parameters defined in redactQueryParams", + name: "FieldRedaction: should redact custom query parameters defined in redactQueryParams", test: () => { let config = { redactQueryParams: ["authorize", "api_key", "password"] @@ -2183,7 +2172,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { } }); this.testCase({ - name: "should redact both default and custom query parameters", + name: "FieldRedaction: should redact both default and custom query parameters", test: () => { let config = { redactQueryParams: ["auth_token"] @@ -2196,7 +2185,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { } }); this.testCase({ - name: "should not redact custom parameters when redaction is disabled", + name: "FieldRedaction:should not redact custom parameters when redaction is disabled", test: () => { let config = { redactUrls: false, @@ -2211,7 +2200,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle empty redactQueryParams array", + name: "FieldRedaction: should handle empty redactQueryParams array", test: () => { let config = { redactQueryParams: [] @@ -2226,7 +2215,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "should handle complex URLs with both credentials and custom query parameters", + name: "FieldRedaction:should handle complex URLs with both credentials and custom query parameters", test: () => { let config = { redactQueryParams: ["authorize", "session_id"] @@ -2240,6 +2229,349 @@ export class ApplicationInsightsCoreTests extends AITestClass { } }); + this.testCase({ + name: "FieldRedaction: should handle encoded username and password with special characters", + test: () => { + let config = {} as IConfiguration; + const url = "https://user%40domain.com:p%40ssw%24rd@example.com/path"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com/path", + "URL with encoded special characters in credentials should be redacted"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle URL-encoded colon and at symbols in credentials", + test: () => { + let config = {} as IConfiguration; + const url = "https://user%3Aname:pass%40word@example.com"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com", + "URL with encoded colon and @ symbols should be redacted"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle invalid protocol with credential pattern", + test: () => { + let config = {} as IConfiguration; + const url = "invalid://user:pass@domain.com"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "invalid://REDACTED:REDACTED@domain.com", + "Invalid protocol but valid credential pattern should still redact credentials"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle double-encoded credentials", + test: () => { + let config = {} as IConfiguration; + const url = "https://user%2540domain:pass%2540word@example.com"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com", + "Double-encoded credentials should be redacted"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle credentials with plus signs and spaces", + test: () => { + let config = {} as IConfiguration; + const url = "https://user+name:pass+word@example.com"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com", + "Credentials with plus signs should be redacted"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle malformed URLs with multiple colons in userinfo", + test: () => { + let config = {} as IConfiguration; + const url = "https://user:pass:extra@example.com"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com", + "Malformed userinfo with extra colons should still be redacted"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle hexadecimal encoded credentials", + test: () => { + let config = {} as IConfiguration; + const url = "https://user%41:pass%42@example.com"; // %41 = A, %42 = B + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com", + "Hexadecimal encoded credentials should be redacted"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle mixed case protocol with credentials", + test: () => { + let config = {} as IConfiguration; + const url = "HtTpS://User:Pass@Example.Com/Path"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "HtTpS://REDACTED:REDACTED@Example.Com/Path", + "Mixed case URLs should still have credentials redacted"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle non-standard port with encoded credentials", + test: () => { + let config = {} as IConfiguration; + const url = "https://admin%21:secret%21@server.com:9443/admin"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@server.com:9443/admin", + "Non-standard port with encoded credentials should be handled"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle credentials with international domain names", + test: () => { + let config = {} as IConfiguration; + const url = "https://user:пароль@тест.рф/path?param=value"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@тест.рф/path?param=value", + "International domain names with credentials should be handled"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle malformed percent encoding in credentials", + test: () => { + let config = {} as IConfiguration; + const url = "https://user%gg:pass%zz@example.com"; // Invalid percent encoding + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com", + "Malformed percent encoding should still result in redaction"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle extremely long encoded credentials", + test: () => { + let config = {} as IConfiguration; + let longUser = "user".repeat(100) + "%40domain"; // This exceeds 200 chars + let longPass = "pass".repeat(100) + "%21"; // This exceeds 200 chars + const url = `https://${longUser}:${longPass}@example.com`; + const redactedLocation = fieldRedaction(url, config); + + // Since both username and password exceed 200 chars, they should NOT be redacted + Assert.equal(redactedLocation, url, + "Extremely long encoded credentials should not be redacted due to length limits"); + } + }); + + + this.testCase({ + name: "FieldRedaction: should handle extremely long usernames without infinite looping", + test: () => { + let config = {} as IConfiguration; + let longUsername = "a".repeat(300); // Exceed reasonable limits + const url = `https://${longUsername}:password@example.com`; + const redactedLocation = fieldRedaction(url, config); + + // Should either redact or leave unchanged, but not hang + Assert.ok(redactedLocation.length > 0, "Should not cause infinite loop with long username"); + + // Since username exceeds 200 chars, it should NOT be redacted + Assert.equal(redactedLocation, url, "Should leave very long usernames unchanged"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle extremely long passwords without infinite looping", + test: () => { + let config = {} as IConfiguration; + let longPassword = "p".repeat(300); // Exceed reasonable limits + const url = `https://user:${longPassword}@example.com`; + const redactedLocation = fieldRedaction(url, config); + + // Should either redact or leave unchanged, but not hang + Assert.ok(redactedLocation.length > 0, "Should not cause infinite loop with long password"); + + // Since password exceeds 200 chars, it should NOT be redacted + Assert.equal(redactedLocation, url, "Should leave very long passwords unchanged"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle invalid scheme characters", + test: () => { + let config = {} as IConfiguration; + const url = "ht@tp://user:pass@example.com"; // Invalid scheme + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "ht@tp://user:pass@example.com", + "Invalid scheme should not be processed"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle scheme with numbers and special characters", + test: () => { + let config = {} as IConfiguration; + const url = "custom+scheme.2://user:pass@example.com"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "custom+scheme.2://REDACTED:REDACTED@example.com", + "Valid scheme with numbers and special characters should work"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle username with colon but no password separator", + test: () => { + let config = {} as IConfiguration; + const url = "https://user:name@example.com"; // No password, colon in username + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com", + "Username containing colon should be redacted"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle credentials with newlines (potential injection)", + test: () => { + let config = {} as IConfiguration; + const url = "https://user\nname:pass\nword@example.com"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com", + "Credentials with newlines should be redacted"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle credentials with tab characters", + test: () => { + let config = {} as IConfiguration; + const url = "https://user\tname:pass\tword@example.com"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com", + "Credentials with tab characters should be redacted"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle regex metacharacters in credentials", + test: () => { + let config = {} as IConfiguration; + const url = "https://user.*+?:pass[]{|}@example.com"; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com", + "Regex metacharacters in credentials should be redacted"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle credentials at exactly 200 character limit", + test: () => { + let config = {} as IConfiguration; + let username200 = "u".repeat(200); + let password200 = "p".repeat(200); + const url = `https://${username200}:${password200}@example.com`; + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com", + "Credentials at exactly 200 character limit should be redacted"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle credentials just over 200 character limit", + test: () => { + let config = {} as IConfiguration; + let username201 = "u".repeat(201); + let password201 = "p".repeat(201); + const url = `https://${username201}:${password201}@example.com`; + const redactedLocation = fieldRedaction(url, config); + + // With {0,200} limit, this should NOT match and remain unchanged + Assert.equal(redactedLocation, url, + "Credentials over 200 character limit should not be matched"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle performance with deeply nested encoded characters", + test: () => { + let config = {} as IConfiguration; + let complexUser = "%25".repeat(50) + "user"; // Deeply encoded + let complexPass = "%25".repeat(50) + "pass"; + const url = `https://${complexUser}:${complexPass}@example.com`; + + let startTime = performance.now(); + const redactedLocation = fieldRedaction(url, config); + let endTime = performance.now(); + + // Should complete quickly (under 10ms for safety) + Assert.ok(endTime - startTime < 10, "Should process complex encoded strings quickly"); + Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com", + "Complex encoded credentials should be redacted"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle scheme starting with number (invalid)", + test: () => { + let config = {} as IConfiguration; + const url = "2http://user:pass@example.com"; // Invalid - scheme can't start with number + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "2http://user:pass@example.com", + "Invalid scheme starting with number should not be processed"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle empty scheme", + test: () => { + let config = {} as IConfiguration; + const url = "://user:pass@example.com"; // Empty scheme + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, "://user:pass@example.com", + "Empty scheme should not be processed"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle multiple occurrences of same parameter with empty values", + test: () => { + let config = {} as IConfiguration; + const url = "https://example.com/path?sig=&color=blue&sig=value&sig="; + const redactedLocation = fieldRedaction(url, config); + // Empty values should NOT be redacted, only non-empty values should be redacted + Assert.equal(redactedLocation, "https://example.com/path?sig=&color=blue&sig=REDACTED&sig=", + "Only non-empty sensitive parameter values should be redacted"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle parameters without values mixed with valued parameters", + test: () => { + let config = {} as IConfiguration; + const url = "https://example.com/path?sig&color=blue&sig=secret&flag&sig"; + const redactedLocation = fieldRedaction(url, config); + // Parameters without values (no = sign) should remain unchanged + // Only parameters with actual values should be redacted + Assert.equal(redactedLocation, "https://example.com/path?sig&color=blue&sig=REDACTED&flag&sig", + "Parameters without values should remain unchanged while valued parameters are redacted"); + } + }); + + this.testCase({ + name: "FieldRedaction: should handle custom parameters with multiple occurrences and empty values", + test: () => { + let config = { + 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"); + } + }); + function _createBuckets(num: number) { // Using helper function as TypeScript 2.5.3 is complaining about new Array(100).fill(0); let buckets: number[] = []; diff --git a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts index 31cfbe029..2b8e9dcfc 100644 --- a/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts +++ b/shared/AppInsightsCore/src/JavaScriptSDK/EnvUtils.ts @@ -4,12 +4,11 @@ import { getGlobal, strShimObject, strShimPrototype, strShimUndefined } from "@microsoft/applicationinsights-shims"; import { - getDocument, getInst, getNavigator, getPerformance, hasNavigator, isFunction, isString, isUndefined, mathMax, objForEachKey, strIndexOf, - strSubstring + getDocument, getInst, getNavigator, getPerformance, hasNavigator, isFunction, isString, isUndefined, mathMax, strIndexOf, strSubstring } from "@nevware21/ts-utils"; import { IConfiguration } from "../applicationinsights-core-js"; import { strContains } from "./HelperFuncs"; -import { DEFAULT_SENSITIVE_PARAMS, STR_EMPTY, STR_REDACTED, UNDEFINED_VALUE } from "./InternalConstants"; +import { DEFAULT_SENSITIVE_PARAMS, STR_EMPTY, STR_REDACTED } from "./InternalConstants"; // TypeScript removed this interface so we need to declare the global so we can check for it's existence. declare var XDomainRequest: any; @@ -363,35 +362,7 @@ export function sendCustomEvent(evtName: string, cfg?: any, customDetails?: any) * @returns The URL with user information redacted */ function redactUserInfo(url: string): string { - const schemeEndIndex = url.indexOf(":"); - if (schemeEndIndex === -1) { - // not a valid url - return url; - } - const len = url.length; - if (len <= schemeEndIndex + 2 - || url.charAt(schemeEndIndex + 1) !== "/" - || url.charAt(schemeEndIndex + 2) !== "/") { - return url; - } - - let index: number; - let atIndex = -1; - for (index = schemeEndIndex + 3; index < len; index++) { - const c = url.charAt(index); - - if (c === "@") { - atIndex = index; - } - - if (c === "/" || c === "?" || c === "#") { - break; - } - } - if (atIndex === -1 || atIndex === len - 1) { - return url; - } - return url.substring(0, schemeEndIndex + 3) + "REDACTED:REDACTED" + url.substring(atIndex); + return url.replace(/^([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)([^:@]{1,200}):([^@]{1,200})@(.*)$/, "$1REDACTED:REDACTED@$4"); //(/^([a-zA-Z][a-zA-Z0-9+.-]{0,50}:\/\/)([^:@]{0,200})(?::([^@]{0,200}))?@(.*)$/, "$1REDACTED:REDACTED@$4"); } /** @@ -405,12 +376,16 @@ function redactQueryParameters(url: string, config?: IConfiguration): string { if (questionMarkIndex === -1) { return url; } - // To build a parameter name until we reach the '=' character - // If the parameter name is a one to redact, we will redact the value + + if (config && config.redactQueryParams) { + sensitiveParams = DEFAULT_SENSITIVE_PARAMS.concat(config.redactQueryParams); + } else { + sensitiveParams = DEFAULT_SENSITIVE_PARAMS; + } + const baseUrl = strSubstring(url, 0, questionMarkIndex + 1); let queryString = strSubstring(url, questionMarkIndex + 1); - // Extract fragment if present let fragment = STR_EMPTY; const hashIndex = strIndexOf(queryString, "#"); if (hashIndex !== -1) { @@ -418,8 +393,22 @@ function redactQueryParameters(url: string, config?: IConfiguration): string { queryString = strSubstring(queryString, 0, hashIndex); } - // Extract parameters - const params: { [key: string]: string } = {}; + let hasPotentialSensitiveParam = false; + for (let i = 0; i < sensitiveParams.length; i++) { + const paramCheck = sensitiveParams[i] + "="; + if (strIndexOf(queryString, paramCheck) !== -1) { + hasPotentialSensitiveParam = true; + break; + } + } + + if (!hasPotentialSensitiveParam) { + return url; + } + + const resultParts: string[] = []; + let anyParamRedacted = false; + if (queryString && queryString.length) { const pairs = queryString.split("&"); for (let i = 0; i < pairs.length; i++) { @@ -430,41 +419,39 @@ function redactQueryParameters(url: string, config?: IConfiguration): string { const equalsIndex = strIndexOf(pair, "="); if (equalsIndex === -1) { - params[pair] = null; + // Parameter without value + resultParts.push(pair); } else { const paramName = pair.substring(0, equalsIndex); const paramValue = pair.substring(equalsIndex + 1); - params[paramName] = paramValue; + if (paramValue === STR_EMPTY) { + resultParts.push(pair); + } else { + let shouldRedact = false; + for (let j = 0; j < sensitiveParams.length; j++) { + if (paramName === sensitiveParams[j]) { + shouldRedact = true; + anyParamRedacted = true; + break; + } + } + + if (shouldRedact) { + resultParts.push(paramName + "=" + STR_REDACTED); + } else { + resultParts.push(pair); + } + } } } } - // Check if any parameters need redaction - let anyParamRedacted = false; - if (config && config.redactQueryParams) { - sensitiveParams = DEFAULT_SENSITIVE_PARAMS.concat(config.redactQueryParams); - } else { - sensitiveParams = DEFAULT_SENSITIVE_PARAMS; - } - for (let i = 0; i < sensitiveParams.length; i++) { - const sensParam = sensitiveParams[i]; - if (params[sensParam] !== UNDEFINED_VALUE) { - params[sensParam] = STR_REDACTED; - anyParamRedacted = true; - } - } - // If no parameters were redacted, return the original URL if (!anyParamRedacted) { return url; } - const parts: string[] = []; - objForEachKey(params, (key, value) => { - parts.push(value === null ? key : key + "=" + value); - }); - - return baseUrl + parts.join("&") + fragment; + return baseUrl + resultParts.join("&") + fragment; } /** @@ -474,20 +461,29 @@ function redactQueryParameters(url: string, config?: IConfiguration): string { * @returns The redacted URL string or the original string if no redaction was needed or possible. */ export function fieldRedaction(input: string, config: IConfiguration): string { - if (!input){ + if (!input ||input.indexOf(" ") !== -1) { return input; } - if (input.indexOf(" ") !== -1) { - return input; // Checking for URLs with spaces - } const isRedactionDisabled = config && config.redactUrls === false; if (isRedactionDisabled) { return input; } + const hasCredentials = strIndexOf(input, "@") !== -1; + const hasQueryParams = strIndexOf(input, "?") !== -1; + + // If no credentials and no query params, return original + if (!hasCredentials && !hasQueryParams) { + return input; + } try { - let parsedUrl = redactUserInfo(input); - parsedUrl = redactQueryParameters(parsedUrl, config); - return parsedUrl; + let result = input; + if (hasCredentials) { + result = redactUserInfo(input); + } + if (hasQueryParams) { + result = redactQueryParameters(result, config); + } + return result; } catch (e) { return input; } From fe17ca11887409867992efea11649256cb13d469 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Wed, 9 Jul 2025 15:54:07 -0700 Subject: [PATCH 13/14] Fixed test config --- .../Tests/Unit/src/ApplicationInsightsCore.Tests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts index d257db44a..ac9e3138c 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts @@ -1911,7 +1911,7 @@ export class ApplicationInsightsCoreTests extends AITestClass { this.testCase({ name: "FieldRedaction: should redact basic auth credentials from URL", test: () => { - let config = {} as IConfiguration; + let config = {redactUrls: false} as IConfiguration; const url = "https://user:password@example.com"; if (config.redactUrls === true){ From 8fe7c3e33df4a584042753859dc2dd9501c74b37 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Wed, 9 Jul 2025 16:50:10 -0700 Subject: [PATCH 14/14] Fixed credential redaction test --- .../Unit/src/ApplicationInsightsCore.Tests.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts b/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts index ac9e3138c..66162dfd4 100644 --- a/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts +++ b/shared/AppInsightsCore/Tests/Unit/src/ApplicationInsightsCore.Tests.ts @@ -1909,16 +1909,20 @@ export class ApplicationInsightsCoreTests extends AITestClass { }); this.testCase({ - name: "FieldRedaction: should redact basic auth credentials from URL", + name: "FieldRedaction: should redact basic auth credentials from URL when config is enabled and should leave the URL unchanged when config is disabled", test: () => { - let config = {redactUrls: false} as IConfiguration; - + // Config is disabled + let config = { redactUrls: false } as IConfiguration; const url = "https://user:password@example.com"; - if (config.redactUrls === true){ - const redactedLocation = fieldRedaction(url, config); - Assert.equal(redactedLocation, "https://REDACTED:REDACTED@example.com"); - } - Assert.notEqual(url, "https://REDACTED:REDACTED@example.com"); + const redactedLocation = fieldRedaction(url, config); + Assert.equal(redactedLocation, url, + "URL should remain unchanged when redaction is disabled"); + + // Config is enabled + let configEnabled = {} as IConfiguration; + const redactedLocationEnabled = fieldRedaction(url, configEnabled); + Assert.equal(redactedLocationEnabled, "https://REDACTED:REDACTED@example.com", + "URL with credentials should be redacted when redaction is enabled"); } });