diff --git a/src/Common/ErrorHandlingUtils.ts b/src/Common/ErrorHandlingUtils.ts index bfa321fe9..ea25d8e34 100644 --- a/src/Common/ErrorHandlingUtils.ts +++ b/src/Common/ErrorHandlingUtils.ts @@ -7,16 +7,27 @@ import { HttpStatusCodes } from "./Constants"; import { logError } from "./Logger"; import { sendMessage } from "./MessageHandler"; -export const handleError = (error: string | ARMError | Error, area: string, consoleErrorPrefix?: string): void => { +export interface HandleErrorOptions { + /** Optional redacted error to use for telemetry logging instead of the original error */ + redactedError?: string | ARMError | Error; +} + +export const handleError = ( + error: string | ARMError | Error, + area: string, + consoleErrorPrefix?: string, + options?: HandleErrorOptions, +): void => { const errorMessage = getErrorMessage(error); const errorCode = error instanceof ARMError ? error.code : undefined; - // logs error to data explorer console + // logs error to data explorer console (always shows original, non-redacted message) const consoleErrorMessage = consoleErrorPrefix ? `${consoleErrorPrefix}:\n ${errorMessage}` : errorMessage; logConsoleError(consoleErrorMessage); - // logs error to both app insight and kusto - logError(errorMessage, area, errorCode); + // logs error to both app insight and kusto (use redacted message if provided) + const telemetryErrorMessage = options?.redactedError ? getErrorMessage(options.redactedError) : errorMessage; + logError(telemetryErrorMessage, area, errorCode); // checks for errors caused by firewall and sends them to portal to handle sendNotificationForError(errorMessage, errorCode); diff --git a/src/Common/dataAccess/queryDocumentsPage.test.ts b/src/Common/dataAccess/queryDocumentsPage.test.ts new file mode 100644 index 000000000..adf68bd02 --- /dev/null +++ b/src/Common/dataAccess/queryDocumentsPage.test.ts @@ -0,0 +1,171 @@ +import { redactSyntaxErrorMessage } from "./queryDocumentsPage"; + +/* Typical error to redact looks like this (the message property contains a JSON string with nested structure): +{ + "message": "{\"code\":\"BadRequest\",\"message\":\"{\\\"errors\\\":[{\\\"severity\\\":\\\"Error\\\",\\\"location\\\":{\\\"start\\\":0,\\\"end\\\":5},\\\"code\\\":\\\"SC1001\\\",\\\"message\\\":\\\"Syntax error, incorrect syntax near 'Crazy'.\\\"}]}\\r\\nActivityId: d5424e10-51bd-46f7-9aec-7b40bed36f17, Windows/10.0.20348 cosmos-netstandard-sdk/3.18.0\"}" +} +*/ + +// Helper to create the nested error structure that matches what the SDK returns +const createNestedError = ( + errors: Array<{ severity?: string; location?: { start: number; end: number }; code: string; message: string }>, + activityId: string = "test-activity-id", +): { message: string } => { + const innerErrorsJson = JSON.stringify({ errors }); + const innerMessage = `${innerErrorsJson}\r\n${activityId}`; + const outerJson = JSON.stringify({ code: "BadRequest", message: innerMessage }); + return { message: outerJson }; +}; + +// Helper to parse the redacted result +const parseRedactedResult = (result: { message: string }) => { + const outerParsed = JSON.parse(result.message); + const [innerErrorsJson, activityIdPart] = outerParsed.message.split("\r\n"); + const innerErrors = JSON.parse(innerErrorsJson); + return { outerParsed, innerErrors, activityIdPart }; +}; + +describe("redactSyntaxErrorMessage", () => { + it("should redact SC1001 error message", () => { + const error = createNestedError( + [ + { + severity: "Error", + location: { start: 0, end: 5 }, + code: "SC1001", + message: "Syntax error, incorrect syntax near 'Crazy'.", + }, + ], + "ActivityId: d5424e10-51bd-46f7-9aec-7b40bed36f17", + ); + + const result = redactSyntaxErrorMessage(error) as { message: string }; + const { outerParsed, innerErrors, activityIdPart } = parseRedactedResult(result); + + expect(outerParsed.code).toBe("BadRequest"); + expect(innerErrors.errors[0].message).toBe("__REDACTED__"); + expect(activityIdPart).toContain("ActivityId: d5424e10-51bd-46f7-9aec-7b40bed36f17"); + }); + + it("should redact SC2001 error message", () => { + const error = createNestedError( + [ + { + severity: "Error", + location: { start: 0, end: 10 }, + code: "SC2001", + message: "Some sensitive syntax error message.", + }, + ], + "ActivityId: abc123", + ); + + const result = redactSyntaxErrorMessage(error) as { message: string }; + const { outerParsed, innerErrors, activityIdPart } = parseRedactedResult(result); + + expect(outerParsed.code).toBe("BadRequest"); + expect(innerErrors.errors[0].message).toBe("__REDACTED__"); + expect(activityIdPart).toContain("ActivityId: abc123"); + }); + + it("should redact multiple errors with SC1001 and SC2001 codes", () => { + const error = createNestedError( + [ + { severity: "Error", code: "SC1001", message: "First error" }, + { severity: "Error", code: "SC2001", message: "Second error" }, + ], + "ActivityId: xyz", + ); + + const result = redactSyntaxErrorMessage(error) as { message: string }; + const { innerErrors } = parseRedactedResult(result); + + expect(innerErrors.errors[0].message).toBe("__REDACTED__"); + expect(innerErrors.errors[1].message).toBe("__REDACTED__"); + }); + + it("should not redact errors with other codes", () => { + const error = createNestedError( + [{ severity: "Error", code: "SC9999", message: "This should not be redacted." }], + "ActivityId: test123", + ); + + const result = redactSyntaxErrorMessage(error); + + expect(result).toBe(error); // Should return original error unchanged + }); + + it("should not modify non-BadRequest errors", () => { + const innerMessage = JSON.stringify({ errors: [{ code: "SC1001", message: "Should not be redacted" }] }); + const error = { + message: JSON.stringify({ code: "NotFound", message: innerMessage }), + }; + + const result = redactSyntaxErrorMessage(error); + + expect(result).toBe(error); + }); + + it("should handle errors without message property", () => { + const error = { code: "BadRequest" }; + + const result = redactSyntaxErrorMessage(error); + + expect(result).toBe(error); + }); + + it("should handle non-object errors", () => { + const stringError = "Simple string error"; + const nullError: null = null; + const undefinedError: undefined = undefined; + + expect(redactSyntaxErrorMessage(stringError)).toBe(stringError); + expect(redactSyntaxErrorMessage(nullError)).toBe(nullError); + expect(redactSyntaxErrorMessage(undefinedError)).toBe(undefinedError); + }); + + it("should handle malformed JSON in message", () => { + const error = { + message: "not valid json", + }; + + const result = redactSyntaxErrorMessage(error); + + expect(result).toBe(error); + }); + + it("should handle message without ActivityId suffix", () => { + const innerErrorsJson = JSON.stringify({ + errors: [{ severity: "Error", code: "SC1001", message: "Syntax error near something." }], + }); + const error = { + message: JSON.stringify({ code: "BadRequest", message: innerErrorsJson + "\r\n" }), + }; + + const result = redactSyntaxErrorMessage(error) as { message: string }; + const { innerErrors } = parseRedactedResult(result); + + expect(innerErrors.errors[0].message).toBe("__REDACTED__"); + }); + + it("should preserve other error properties", () => { + const baseError = createNestedError([{ code: "SC1001", message: "Error" }], "ActivityId: test"); + const error = { + ...baseError, + statusCode: 400, + additionalInfo: "extra data", + }; + + const result = redactSyntaxErrorMessage(error) as { + message: string; + statusCode: number; + additionalInfo: string; + }; + + expect(result.statusCode).toBe(400); + expect(result.additionalInfo).toBe("extra data"); + + const { innerErrors } = parseRedactedResult(result); + expect(innerErrors.errors[0].message).toBe("__REDACTED__"); + }); +}); diff --git a/src/Common/dataAccess/queryDocumentsPage.ts b/src/Common/dataAccess/queryDocumentsPage.ts index 556ed290c..b5ec9c684 100644 --- a/src/Common/dataAccess/queryDocumentsPage.ts +++ b/src/Common/dataAccess/queryDocumentsPage.ts @@ -4,6 +4,51 @@ import { getEntityName } from "../DocumentUtility"; import { handleError } from "../ErrorHandlingUtils"; import { MinimalQueryIterator, nextPage } from "../IteratorUtilities"; +// Redact sensitive information from BadRequest errors with specific codes +export const redactSyntaxErrorMessage = (error: unknown): unknown => { + const codesToRedact = ["SC1001", "SC2001"]; + + try { + // Handle error objects with a message property + if (error && typeof error === "object" && "message" in error) { + const errorObj = error as { code?: string; message?: string }; + if (typeof errorObj.message === "string") { + // Parse the inner JSON from the message + const innerJson = JSON.parse(errorObj.message); + if (innerJson.code === "BadRequest" && typeof innerJson.message === "string") { + const [innerErrorsJson, activityIdPart] = innerJson.message.split("\r\n"); + const innerErrorsObj = JSON.parse(innerErrorsJson); + if (Array.isArray(innerErrorsObj.errors)) { + let modified = false; + innerErrorsObj.errors = innerErrorsObj.errors.map((err: { code?: string; message?: string }) => { + if (err.code && codesToRedact.includes(err.code)) { + modified = true; + return { ...err, message: "__REDACTED__" }; + } + return err; + }); + + if (modified) { + // Reconstruct the message with the redacted content + const redactedMessage = JSON.stringify(innerErrorsObj) + `\r\n${activityIdPart}`; + const redactedError = { + ...error, + message: JSON.stringify({ ...innerJson, message: redactedMessage }), + body: undefined as unknown, // Clear body to avoid sensitive data + }; + return redactedError; + } + } + } + } + } + } catch { + // If parsing fails, return the original error + } + + return error; +}; + export const queryDocumentsPage = async ( resourceName: string, documentsIterator: MinimalQueryIterator, @@ -18,7 +63,12 @@ export const queryDocumentsPage = async ( logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`); return result; } catch (error) { - handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`); + // Redact sensitive information for telemetry while showing original in console + const redactedError = redactSyntaxErrorMessage(error); + + handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`, { + redactedError: redactedError as Error, + }); throw error; } finally { clearMessage();