Skip to content

Commit

Permalink
[Recorder] Central sanitizers (#29331)
Browse files Browse the repository at this point in the history
### Packages impacted by this PR
`@azure-tools/test-recorder` - Adding the central sanitizers

### Issues associated with this PR
**References:**
- Azure/azure-sdk-for-java#39700
- Azure/azure-sdk-for-python#35196
- And the patterns found

### Describe the problem that is addressed by this PR
- Introducing fallback sanitizers into the test recorder to handle
potential secret leaks.
- The new sanitizers are designed to work in conjunction with the
existing `handleEnvSetup` mechanism and the fake secrets.
  - The sanitizers include:
- `BodyKeySanitizers` that redact sensitive information in the JSON body
of the requests.
- `FindReplaceSanitizers` that redact sensitive information based on
provided regular expressions.
- `HeaderSanitizers` that redact sensitive information in the headers of
the requests.

## Tests
I've ran the tests for the following and they work fine
- [x] recorder
- [x] template
- [x] notification-hubs (needed to make a few fixes for browser tests in
notification hubs which do feel like unrelated to this PR, but fixing
them here anyway.)

___Currently only these three packages depend on recorder v4.___

## Future work (future PRs)
- Once this PR is merged, cherrypick the commit and release a hotfix 3.x
version
- Add more tests at some point
  • Loading branch information
HarshaNalluru authored Apr 16, 2024
1 parent 3ca26ee commit b047b37
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 3 deletions.
2 changes: 1 addition & 1 deletion sdk/notificationhubs/notification-hubs/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "js",
"TagPrefix": "js/notificationhubs/notification-hubs",
"Tag": "js/notificationhubs/notification-hubs_44f522d99a"
"Tag": "js/notificationhubs/notification-hubs_0718648c50"
}
2 changes: 1 addition & 1 deletion sdk/notificationhubs/notification-hubs/test.env
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Used in most samples. Retrieve these values from a Azure Notification Hub in the Azure Portal.
NOTIFICATIONHUBS_CONNECTION_STRING="<connection string>"
NOTIFICATION_HUB_CONNECTION_STRING="<connection string>"

# Used in most samples. Provide the name of a Notification Hub within a namespace.
NOTIFICATION_HUB_NAME="<hub name>"
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import { Recorder, RecorderStartOptions, env } from "@azure-tools/test-recorder";
import { NotificationHubsClientContext, createClientContext } from "../../../src/api/index.js";
import { isBrowser } from "@azure/core-util";

const replaceableVariables: { [k: string]: string } = {
// Used in record and playback modes
Expand Down Expand Up @@ -30,6 +31,24 @@ export async function createRecordedClientContext(
recorder: Recorder,
): Promise<NotificationHubsClientContext> {
await recorder.start(recorderOptions);
if (isBrowser) {
// there are timestamps in the body, so do not match body
await recorder.setMatcher("BodilessMatcher");
await recorder.addSanitizers(
{
// looks like the registration id is dynamic, redacting it instead
generalSanitizers: [
{
regex: true,
target: "registrations/(?<secret>.*?)?api-version=",
value: "registration-id-redacted",
groupForReplace: "secret",
},
],
},
["record", "playback"],
);
}

if (!env.NOTIFICATION_HUB_CONNECTION_STRING || !env.NOTIFICATION_HUB_NAME) {
throw new Error(
Expand Down
11 changes: 11 additions & 0 deletions sdk/test-utils/recorder/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Release History

## 4.1.0

### Features Added

- Introduced fallback sanitizers into the test recorder to handle potential secret leaks.
- The new sanitizers are designed to work in conjunction with the existing `handleEnvSetup` mechanism and the fake secrets.
- The sanitizers include:
- `BodyKeySanitizers` that redact sensitive information in the JSON body of the requests.
- `FindReplaceSanitizers` that redact sensitive information based on provided regular expressions.
- `HeaderSanitizers` that redact sensitive information in the headers of the requests.

## 4.0.0 (2024-04-09)

### Features Added
Expand Down
4 changes: 4 additions & 0 deletions sdk/test-utils/recorder/src/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { decodeBase64 } from "./utils/encoding.js";
import { AdditionalPolicyConfig } from "@azure/core-client";
import { isVitestTestContext, TestInfo, VitestSuite } from "./testInfo.js";
import { env } from "./utils/env.js";
import { fallbackSanitizers } from "./utils/fallbackSanitizers.js";

/**
* Caculates session file path and JSON assets path from test context
Expand Down Expand Up @@ -355,6 +356,9 @@ export class Recorder {
options.envSetupForPlayback,
);

// Fallback sanitizers to be added in both record/playback modes
await fallbackSanitizers(this.httpClient, Recorder.url, this.recordingId);

// Sanitizers to be added only in record mode
if (isRecordMode() && options.sanitizerOptions) {
// Makes a call to the proxy-tool to add the sanitizers for the current recording id
Expand Down
134 changes: 134 additions & 0 deletions sdk/test-utils/recorder/src/utils/fallbackSanitizers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { HttpClient } from "@azure/core-rest-pipeline";
import { addSanitizers } from "../sanitizer.js";
import { BodyKeySanitizer, FindReplaceSanitizer, HeaderSanitizer } from "./utils.js";

const JSON_BODY_KEYS_TO_REDACT = [
"authHeader",
"accountKey",
"accessToken",
"accountName",
"applicationId",
"apiKey",
"client_secret",
"connectionString",
"url",
"host",
"password",
"userName",
"applicationSecret",
"aliasSecondaryConnectionString",
"aliasPrimaryConnectionString",
"primaryKey",
"secondaryKey",
"adminPassword.value",
"administratorLoginPassword",
"runAsPassword",
"adminPassword",
"accessSAS",
"WEBSITE_AUTH_ENCRYPTION_KEY",
"decryptionKey",
"primaryMasterKey",
"primaryReadonlyMasterKey",
"secondaryMasterKey",
"secondaryReadonlyMasterKey",
"certificatePassword",
"clientSecret",
"keyVaultClientSecret",
"authHeader",
"httpHeader",
"encryptedCredential",
"appkey",
"functionKey",
"atlasKafkaPrimaryEndpoint",
"atlasKafkaSecondaryEndpoint",
"certificatePassword",
"storageAccountPrimaryKey",
"privateKey",
"fencingClientPassword",
"acrToken",
"scriptUrlSasToken",
"azureBlobSource.containerUrl",
"properties.DOCKER_REGISTRY_SEVER_PASSWORD",
];

const BODY_REGEXES_TO_REDACT = [
"(?:(Password|User ID)=)(?<secret>.*)(?:;)",
"client_secret=(?<secret>[^&]+)",
"<PrimaryKey>(?<secret>.*?)</PrimaryKey>",
"<SecondaryKey>(?<secret>.*?)</SecondaryKey>",
"<UserDelegationKey>.*?<SignedOid>(?<secret>.*?)</SignedOid>.*?</UserDelegationKey>",
"<UserDelegationKey>.*?<SignedTid>(?<secret>.*?)</SignedTid>.*?</UserDelegationKey>",
"<UserDelegationKey>.*?<Value>(?<secret>.*?)</Value>.*?</UserDelegationKey>",
'SharedAccessKey=(?<secret>[^;\\"]+)',
'AccountKey=(?<secret>[^;\\"]+)',
'accesskey=(?<secret>[^;\\"]+)',
'AccessKey=(?<secret>[^;\\"]+)',
'Secret=(?<secret>[^;\\"]+)',
"access_token=(?<secret>.*?)(?=&|$)",
"refresh_token=(?<secret>.*?)(?=&|$)",
'(?:(sv|sig|se|srt|ss|sp)=)(?<secret>[^&\\"]*)',
];

const URL_REGEX = "(?<=http://|https://)([^/?]+)";

const HEADER_KEYS_TO_REDACT = [
"Ocp-Apim-Subscription-Key",
"api-key",
"x-api-key",
"subscription-key",
"x-ms-encryption-key",
"sshPassword",
];

export async function fallbackSanitizers(
httpClient: HttpClient,
url: string,
recordingId: string,
): Promise<void> {
const bodyKeySanitizers: BodyKeySanitizer[] = JSON_BODY_KEYS_TO_REDACT.map((prop) => ({
jsonPath: `$..${prop}`, // Handles the request body
value: "REDACTED",
}));

const generalSanitizers: FindReplaceSanitizer[] = BODY_REGEXES_TO_REDACT.map((regex) => ({
value: "REDACTED",
regex: true,
groupForReplace: "secret",
target: regex,
}));

const headerSanitizers: HeaderSanitizer[] = [
{
key: "Operation-location",
groupForReplace: "secret",
regex: true,
target: URL_REGEX,
value: "REDACTED",
},
{
key: "ServiceBusDlqSupplementaryAuthorization",
groupForReplace: "secret",
regex: true,
target: '(?:(sv|sig|se|srt|ss|sp)=)(?<secret>[^&\\"]+)',
value: "REDACTED",
},
{
key: "ServiceBusSupplementaryAuthorization",
groupForReplace: "secret",
regex: true,
target: '(?:(sv|sig|se|srt|ss|sp)=)(?<secret>[^&\\"]+)',
value: "REDACTED",
},
];

const headersForRemoval: string[] = HEADER_KEYS_TO_REDACT;
await addSanitizers(httpClient, url, recordingId, {
bodyKeySanitizers,
generalSanitizers,
removeHeaderSanitizer: { headersForRemoval },
headerSanitizers,
});
}
2 changes: 1 addition & 1 deletion sdk/test-utils/recorder/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export function isStringSanitizer(sanitizer: FindReplaceSanitizer): sanitizer is
*
* If the body is NOT a JSON object, this sanitizer will NOT be applied.
*/
type BodyKeySanitizer = {
export type BodyKeySanitizer = {
regex?: string;

value?: string;
Expand Down

0 comments on commit b047b37

Please sign in to comment.