Skip to content

Commit 580ff10

Browse files
feat: add support for masking sensitive data (#5)
1 parent 0abc87c commit 580ff10

File tree

38 files changed

+1323
-191
lines changed

38 files changed

+1323
-191
lines changed

README.md

+83-14
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,28 @@ import express from "express";
100100

101101
const app = express();
102102

103-
// Configure a speakeasy SDK instance
104-
const cfg: Config = {
105-
apiKey: "YOUR API KEY HERE", // retrieve from Speakeasy API dashboard.
106-
apiID: "YOUR API ID HERE", // custom Api ID to associate captured requests with.
107-
versionID: "YOUR VERSION ID HERE", // custom Version ID to associate captured requests
108-
port: 3000, // The port number your express app is listening on (required to build full URLs on non-standard ports)
109-
};
110-
const sdk = new SpeakeasySDK(cfg);
103+
// Configure a new instance of the SDK for the store API
104+
const storeSDK = new SpeakeasySDK({
105+
apiKey: "YOUR API KEY HERE", // retrieved from Speakeasy API dashboard.
106+
apiID: "store_api", // this is an ID you provide that you would like to associate captured requests with.
107+
versionID: "1.0.0", // this is a Version you provide that you would like to associate captured requests with.
108+
port: 3000, // The port number your express app is listening on (required to build full URLs on non-standard ports)
109+
});
110+
111+
// Configure a new instance of the SDK for the product AP
112+
const productSDK = new SpeakeasySDK({
113+
apiKey: "YOUR API KEY HERE", // retrieved from Speakeasy API dashboard.
114+
apiID: "product_api", // this is an ID you provide that you would like to associate captured requests with.
115+
versionID: "1.0.0", // this is a Version you provide that you would like to associate captured requests with.
116+
port: 3000, // The port number your express app is listening on (required to build full URLs on non-standard ports)
117+
});
111118

112-
// Add the speakeasy middleware to your express app/router
113-
app.use(sdk.expressMiddleware());
119+
// The different instances of the SDK (with differnt IDs or even versions assigned) can be used to associate requests with different APIs and Versions.
120+
const storeRouter = app.route("/store");
121+
storeRouter.use(storeSDK.expressMiddleware());
122+
123+
const productsRouter = app.route("/products");
124+
productsRouter.use(productSDK.expressMiddleware());
114125

115126
// Rest of your express app setup code
116127
```
@@ -252,9 +263,7 @@ Create a file called `expressmonkeypatch.ts` or similar and import it into your
252263

253264
## Capturing Customer IDs
254265

255-
To help associate requests with customers/users of your APIs you can provide a customer ID per request handler:
256-
257-
266+
To help associate requests with customers/users of your APIs you can provide a customer ID per request handler:
258267

259268
```typescript
260269
const app = express();
@@ -267,4 +276,64 @@ app.all("/", (req, res) => {
267276
});
268277
```
269278

270-
Note: This is not required, but is highly recommended. By setting a customer ID you can easily associate requests with your customers/users in the Speakeasy Dashboard, powering filters in the [Request Viewer](https://docs.speakeasyapi.dev/speakeasy-user-guide/request-viewer-coming-soon).
279+
Note: This is not required, but is highly recommended. By setting a customer ID you can easily associate requests with your customers/users in the Speakeasy Dashboard, powering filters in the [Request Viewer](https://docs.speakeasyapi.dev/speakeasy-user-guide/request-viewer-coming-soon).
280+
281+
## Masking sensitive data
282+
283+
Speakeasy can mask sensitive data in the query string parameters, headers, cookies and request/response bodies captured by the SDK. This is useful for maintaining sensitive data isolation, and retaining control over the data that is captured.
284+
285+
Using the `Advanced Configuration` section above you can completely ignore certain routes by not assigning the middleware to their router, causing the SDK to not capture any requests to that router.
286+
287+
But if you would like to be more selective you can mask certain sensitive data using our middleware controller allowing you to mask fields as needed in different handlers:
288+
289+
```typescript
290+
import { Masking } from '@speakeasy-api/speakeasy-typescript-sdk';
291+
292+
const app = express();
293+
app.use(speakeasy.expressMiddleware());
294+
app.all("/", (req, res) => {
295+
ctrl := req.controller;
296+
ctrl.setMaskingOpts(Masking.withRequestHeaderMask("authorization")) // Mask the authorization header in the request
297+
298+
// the rest of your handlers code
299+
}
300+
```
301+
302+
The `Masking` function takes a number of different options to mask sensitive data in the request:
303+
304+
* `Masking.withQueryStringMask` - **withQueryStringMask** will mask the specified query strings with an optional mask string.
305+
* `Masking.withRequestHeaderMask` - **withRequestHeaderMask** will mask the specified request headers with an optional mask string.
306+
* `Masking.withResponseHeaderMask` - **withResponseHeaderMask** will mask the specified response headers with an optional mask string.
307+
* `Masking.withRequestCookieMask` - **withRequestCookieMask** will mask the specified request cookies with an optional mask string.
308+
* `Masking.withResponseCookieMask` - **withResponseCookieMask** will mask the specified response cookies with an optional mask string.
309+
* `Masking.withRequestFieldMaskString` - **withRequestFieldMaskString** will mask the specified request body fields with an optional mask. Supports string fields only. Matches using regex.
310+
* `Masking.withRequestFieldMaskNumber` - **withRequestFieldMaskNumber** will mask the specified request body fields with an optional mask. Supports number fields only. Matches using regex.
311+
* `Masking.withResponseFieldMaskString` - **withResponseFieldMaskString** will mask the specified response body fields with an optional mask. Supports string fields only. Matches using regex.
312+
* `Masking.withResponseFieldMaskNumber` - **withResponseFieldMaskNumber** will mask the specified response body fields with an optional mask. Supports number fields only. Matches using regex.
313+
314+
Masking can also be done more globally on all routes or a selection of routes by taking advantage of middleware. Here is an example:
315+
316+
```typescript
317+
import speakeasy, { Config, Masking } from "@speakeasy-api/speakeasy-typescript-sdk";
318+
import express from "express";
319+
320+
const app = express();
321+
322+
// Configure the global speakeasy SDK instance
323+
const cfg: Config = {
324+
apiKey: "YOUR API KEY HERE", // retrieve from Speakeasy API dashboard.
325+
apiID: "YOUR API ID HERE", // custom Api ID to associate captured requests with.
326+
versionID: "YOUR VERSION ID HERE", // custom Version ID to associate captured requests
327+
port: 3000, // The port number your express app is listening on (required to build full URLs on non-standard ports)
328+
};
329+
speakeasy.configure(cfg);
330+
331+
// Add the speakeasy middleware to your express app
332+
app.use(speakeasy.expressMiddleware());
333+
app.use((req: Request, res: Response, next: NextFunction) => {
334+
// Mask the authorization header in the request for all requests served by this middleware
335+
ctrl := req.controller;
336+
ctrl.setMaskingOpts(Masking.withRequestHeaderMask("authorization"))
337+
next();
338+
});
339+
```

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@speakeasy-api/speakeasy-typescript-sdk",
3-
"version": "1.1.3",
3+
"version": "1.2.0",
44
"repository": {
55
"type": "git",
66
"url": "https://github.com/speakeasy-api/speakeasy-typescript-sdk"
@@ -13,6 +13,7 @@
1313
"/dist"
1414
],
1515
"devDependencies": {
16+
"@nestjs/common": "^9.0.8",
1617
"@types/content-type": "^1.1.5",
1718
"@types/cookie": "^0.5.1",
1819
"@types/express": "^4.17.13",
@@ -31,8 +32,7 @@
3132
"supertest": "^6.2.4",
3233
"ts-jest": "^28.0.7",
3334
"ts-node": "^10.9.1",
34-
"typescript": "^4.7.4",
35-
"@nestjs/common": "^9.0.8"
35+
"typescript": "^4.7.4"
3636
},
3737
"peerDepedencies": {
3838
"express": "^4.16.0"

src/bodymasking.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as contentType from "content-type";
2+
3+
export function maskBodyRegex(
4+
body: string,
5+
mimeType: string,
6+
stringMasks: Record<string, string>,
7+
numberMasks: Record<string, string>
8+
): string {
9+
const ct = contentType.parse(mimeType);
10+
if (ct.type !== "application/json") {
11+
return body;
12+
}
13+
14+
for (const field in stringMasks) {
15+
const escapedField = escapeRegExp(field);
16+
17+
const regexString = `("${escapedField}": *)(".*?[^\\\\]")( *[, \\n\\r}]?)`;
18+
19+
body = body.replace(
20+
new RegExp(regexString, "g"),
21+
(_: string, ...groups: string[]): string => {
22+
return groups[0] + `"${stringMasks[field]}"` + groups[2];
23+
}
24+
);
25+
}
26+
27+
for (const field in numberMasks) {
28+
const escapedField = escapeRegExp(field);
29+
30+
const regexString = `("${escapedField}": *)(-?[0-9]+\\.?[0-9]*)( *[, \\n\\r}]?)`;
31+
32+
body = body.replace(
33+
new RegExp(regexString, "g"),
34+
(_: string, ...groups: string[]): string => {
35+
return groups[0] + numberMasks[field] + groups[2];
36+
}
37+
);
38+
}
39+
40+
return body;
41+
}
42+
43+
function escapeRegExp(string) {
44+
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
45+
}

0 commit comments

Comments
 (0)