PageSourceSearch

https://nestlens-docs.vercel.app/assets/js/aedc2929.b4b42faf.js

js nestlens-docs.vercel.app collected 2026-10-03 07:25:11 UTC 37,727 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([["8631"],{3166(e,n,s){s.r(n),s.d(n,{metadata:()=>i,default:()=>h,frontMatter:()=>r,contentTitle:()=>d,toc:()=>c,assets:()=>l});var i=JSON.parse('{"id":"security/data-masking","title":"Data Masking","description":"NestLens automatically masks sensitive data to protect credentials, tokens, and personal information from being stored in logs.","source":"@site/docs/security/data-masking.md","sourceDirName":"security","slug":"/security/data-masking","permalink":"/docs/security/data-masking","draft":false,"unlisted":false,"editUrl":"https://github.com/mogretici/nestlens/tree/main/docs/docs/security/data-masking.md","tags":[],"version":"current","sidebarPosition":4,"frontMatter":{"sidebar_position":4},"sidebar":"docsSidebar","previous":{"title":"Network Isolation","permalink":"/docs/security/network-isolation"},"next":{"title":"Production Usage","permalink":"/docs/security/production-usage"}}'),t=s(4848),a=s(8453);let r={sidebar_position:4},d="Data Masking",l={},c=[{value:"Overview",id:"overview",level:2},{value:"What it does not protect",id:"what-it-does-not-protect",level:3},{value:"Automatic Masking",id:"automatic-masking",level:2},{value:"HTTP Request Headers",id:"http-request-headers",level:3},{value:"Masked Header Patterns",id:"masked-header-patterns",level:3},{value:"HTTP Client Requests",id:"http-client-requests",level:3},{value:"Redis Data Masking",id:"redis-data-masking",level:2},{value:"Auto-Masked Key Patterns",id:"auto-masked-key-patterns",level:3},{value:"Example",id:"example",level:3},{value:"Model Data Masking",id:"model-data-masking",level:2},{value:"Sensitive Field Patterns",id:"sensitive-field-patterns",level:3},{value:"Example",id:"example-1",level:3},{value:"Disable Data Capture",id:"disable-data-capture",level:3},{value:"What Masking Cannot Reach",id:"what-masking-cannot-reach",level:2},{value:"Global Security Configuration",id:"global-security-configuration",level:2},{value:"Masking Only What You Name",id:"masking-only-what-you-name",level:3},{value:"Input limits on the dashboard API",id:"input-limits-on-the-dashboard-api",level:3},{value:"How a Field Name Is Matched",id:"how-a-field-name-is-matched",level:3},{value:"Default Masked Body/Query Parameters",id:"default-masked-bodyquery-parameters",level:3},{value:"Default Masked User Fields",id:"default-masked-user-fields",level:3},{value:"Custom Masking",id:"custom-masking",level:2},{value:"Custom Sensitive Headers",id:"custom-sensitive-headers",level:3},{value:"Custom Request Parameters",id:"custom-request-parameters",level:3},{value:"Custom Response Parameters",id:"custom-response-parameters",level:3},{value:"Entry Filtering for Sensitive Data",id:"entry-filtering-for-sensitive-data",level:2},{value:"Filter by Route",id:"filter-by-route",level:3},{value:"Filter by Content",id:"filter-by-content",level:3},{value:"Custom Masking in Filter",id:"custom-masking-in-filter",level:3},{value:"Request Body Masking",id:"request-body-masking",level:2},{value:"Disable Body Capture",id:"disable-body-capture",level:3},{value:"Limit Body Size",id:"limit-body-size",level:3},{value:"Response Body Masking",id:"response-body-masking",level:2},{value:"Disable Response Capture",id:"disable-response-capture",level:3},{value:"Selective Response Capture",id:"selective-response-capture",level:3},{value:"Production Security",id:"production-security",level:2},{value:"Recommended Production Config",id:"recommended-production-config",level:3},{value:"Compliance Considerations",id:"compliance-considerations",level:2},{value:"GDPR Compliance",id:"gdpr-compliance",level:3},{value:"PCI DSS Compliance",id:"pci-dss-compliance",level:3},{value:"HIPAA Compliance",id:"hipaa-compliance",level:3},{value:"Verification",id:"verification",level:2},{value:"Audit Captured Data",id:"audit-captured-data",level:3},{value:"Test Masking",id:"test-masking",level:3},{value:"Best Practices",id:"best-practices",level:2},{value:"1. Default to Minimal Capture",id:"1-default-to-minimal-capture",level:3},{value:"2. Use Filtering Liberally",id:"2-use-filtering-liberally",level:3},{value:"3. Regular Security Audits",id:"3-regular-security-audits",level:3},{value:"4. Educate Team",id:"4-educate-team",level:3},{value:"5. Document Sensitive Patterns",id:"5-document-sensitive-patterns",level:3},{value:"Next Steps",id:"next-steps",level:2}];function o(e){let n={a:"a",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,a.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.header,{children:(0,t.jsx)(n.h1,{id:"data-masking",children:"Data Masking"})}),"\n",(0,t.jsx)(n.p,{children:"NestLens automatically masks sensitive data to protect credentials, tokens, and personal information from being stored in logs."}),"\n",(0,t.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,t.jsx)(n.p,{children:"Data masking protects:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Authentication tokens and passwords"}),"\n",(0,t.jsx)(n.li,{children:"API keys and secrets"}),"\n",(0,t.jsx)(n.li,{children:"Card numbers, card verification codes and national identifiers"}),"\n",(0,t.jsx)(n.li,{children:"Session cookies and credentials"}),"\n",(0,t.jsx)(n.li,{children:"Database connection strings"}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"what-it-does-not-protect",children:"What it does not protect"}),"\n",(0,t.jsx)(n.p,{children:"Masking works by field name, and the names it knows are credentials and\npayment details. Everything else your application handles reaches storage as\nit was \u2014 including personal data that is not a credential:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{ "email": "[email protected]", "phone": "+44 7700 900000", "dateOfBirth": "1990-01-01" }\n'})}),"\n",(0,t.jsxs)(n.p,{children:["That is deliberate: a mail watcher that hid every recipient would have nothing\nto show, and the body you are debugging is usually the point. Where a field is\nsensitive in your domain, name it \u2014 ",(0,t.jsx)(n.code,{children:"sensitiveParams"})," is read on every payload,\nand ",(0,t.jsx)(n.a,{href:"#masking-only-what-you-name",children:"Masking Only What You Name"})," narrows the list\ninstead of extending it:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"security: {\n  dataMasking: {\n    sensitiveParams: ['email', 'phone', 'dateOfBirth', 'iban'],\n  },\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"automatic-masking",children:"Automatic Masking"}),"\n",(0,t.jsx)(n.p,{children:"NestLens automatically masks sensitive data across different watchers."}),"\n",(0,t.jsx)(n.h3,{id:"http-request-headers",children:"HTTP Request Headers"}),"\n",(0,t.jsx)(n.p,{children:"Sensitive headers are automatically masked:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// Original request\nheaders: {\n  'authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',\n  'x-api-key': 'sk_live_1234567890',\n  'cookie': 'session=abc123; token=xyz789'\n}\n\n// Stored in NestLens\nheaders: {\n  'authorization': '***REDACTED***',\n  'x-api-key': '***REDACTED***',\n  'cookie': '***REDACTED***'\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"masked-header-patterns",children:"Masked Header Patterns"}),"\n",(0,t.jsx)(n.p,{children:"By default, these 9 headers are masked (case-insensitive):"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"authorization"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"cookie"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"set-cookie"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"x-api-key"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"x-auth-token"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"x-access-token"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"x-refresh-token"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"x-csrf-token"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"proxy-authorization"})}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["The default replacement string is ",(0,t.jsx)(n.code,{children:"***REDACTED***"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"http-client-requests",children:"HTTP Client Requests"}),"\n",(0,t.jsx)(n.p,{children:"Outbound HTTP requests also mask sensitive headers:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// HTTP Client Watcher config\nNestLensModule.forRoot({\n  watchers: {\n    httpClient: {\
1n      sensitiveHeaders: [\n        'authorization',\n        'x-api-key',\n        'api-key',\n      ],\n    },\n  },\n})\n"})}),"\n",(0,t.jsx)(n.h2,{id:"redis-data-masking",children:"Redis Data Masking"}),"\n",(0,t.jsx)(n.p,{children:"Redis keys containing sensitive patterns are automatically masked."}),"\n",(0,t.jsx)(n.h3,{id:"auto-masked-key-patterns",children:"Auto-Masked Key Patterns"}),"\n",(0,t.jsx)(n.p,{children:"Keys matching these patterns have their values masked:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"password"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"token"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"secret"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"auth"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"key"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"credential"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"session"})}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"example",children:"Example"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// Command: redis.set('user:token:abc', 'secret-value')\n\n// Stored as:\n{\n  command: 'set',\n  args: ['user:token:abc', '********'],\n  result: '********'\n}\n\n// Command: redis.get('user:name')\n\n// Stored as:\n{\n  command: 'get',\n  args: ['user:name'],\n  result: 'John Doe'  // Not masked\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"model-data-masking",children:"Model Data Masking"}),"\n",(0,t.jsx)(n.p,{children:"Entity data is automatically masked when captured."}),"\n",(0,t.jsx)(n.h3,{id:"sensitive-field-patterns",children:"Sensitive Field Patterns"}),"\n",(0,t.jsx)(n.p,{children:"These field names are automatically masked:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"password"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"passwordHash"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"secret"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"token"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"apiKey"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"accessToken"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"refreshToken"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"creditCard"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"ssn"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"privateKey"})}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"example-1",children:"Example"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// User entity\n{\n  id: 123,\n  email: '[email protected]',\n  password: 'hashed_password',\n  apiKey: 'sk_live_123456',\n}\n\n// Stored in NestLens\n{\n  id: 123,\n  email: '[email protected]',\n  password: '***MASKED***',\n  apiKey: '***MASKED***',\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"disable-data-capture",children:"Disable Data Capture"}),"\n",(0,t.jsx)(n.p,{children:"For maximum security, disable data capture entirely:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  watchers: {\n    model: {\n      captureData: false,  // Don't capture entity data at all\n    },\n  },\n})\n"})}),"\n",(0,t.jsx)(n.h2,{id:"what-masking-cannot-reach",children:"What Masking Cannot Reach"}),"\n",(0,t.jsx)(n.p,{children:"Masking works from field names. Where a value carries no name of its own,\nnothing here can decide about it, and these watchers record what they are shown:"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Watcher"}),(0,t.jsx)(n.th,{children:"Field"}),(0,t.jsx)(n.th,{children:"Why it cannot be masked"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Query"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"query"})}),(0,t.jsxs)(n.td,{children:["A literal embedded in SQL text \u2014 ",(0,t.jsx)(n.code,{children:"WHERE token = 'abc'"})," \u2014 is inside a string, not a named field. Parameterised queries keep values in ",(0,t.jsx)(n.code,{children:"parameters"}),", which is also unnamed: masking it would blank every value a slow query is read for."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Command"}),(0,t.jsx)(n.td,{children:"positional arguments"}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"seed hunter2"})," marks nothing. Values passed to a ",(0,t.jsx)(n.strong,{children:"named flag"})," ",(0,t.jsx)(n.em,{children:"are"})," masked: ",(0,t.jsx)(n.code,{children:"--password hunter2"})," and ",(0,t.jsx)(n.code,{children:"--token=abc"})," both lose their value and keep the flag."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Redis"}),(0,t.jsx)(n.td,{children:"command arguments"}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"SET session:1 <value>"})," is positional in the same way."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Cache"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"value"})}),(0,t.jsx)(n.td,{children:"Seeing what was cached is the reason the watcher exists."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Mail"}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"text"}),", ",(0,t.jsx)(n.code,{children:"html"})]}),(0,t.jsx)(n.td,{children:"Free text. A token pasted into an email body reads like any other sentence."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Exception"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"message"})}),(0,t.jsxs)(n.td,{children:["Free text as well: an application that interpolates a token into an error message puts the value inside a sentence. Stack traces are handled separately by ",(0,t.jsx)(n.code,{children:"stackTraceSanitization"}),"."]})]})]})]}),"\n",(0,t.jsx)(n.p,{children:"Two things do get masked wherever they appear, because their shape is\nunambiguou
1s:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Query strings inside URLs."})," ",(0,t.jsx)(n.code,{children:"GET /reset?token=abc"})," is masked in ",(0,t.jsx)(n.code,{children:"url"})," as\nwell as in ",(0,t.jsx)(n.code,{children:"query"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Credentials inside URLs."})," ",(0,t.jsx)(n.code,{children:"postgres://app:hunter2@db/orders"})," keeps the\nuser and host and loses the password \u2014 in ",(0,t.jsx)(n.code,{children:"url"}),", ",(0,t.jsx)(n.code,{children:"connectionString"}),",\n",(0,t.jsx)(n.code,{children:"connectionUri"})," and ",(0,t.jsx)(n.code,{children:"dsn"}),"."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["Where a watcher in the table above would record something you cannot store,\nturn it off, narrow it with ",(0,t.jsx)(n.code,{children:"filter"}),", or add the field name to\n",(0,t.jsx)(n.code,{children:"sensitiveParams"})," if it has one."]}),"\n",(0,t.jsxs)(n.p,{children:["The same applies to ",(0,t.jsx)(n.a,{href:"../configuration/alerting",children:"alerting webhooks"}),": they are\nsent the entry ",(0,t.jsx)(n.strong,{children:"after"})," masking, so a redacted field is redacted there too \u2014\nbut an unmaskable value stays unmaskable on its way out."]}),"\n",(0,t.jsx)(n.h2,{id:"global-security-configuration",children:"Global Security Configuration"}),"\n",(0,t.jsxs)(n.p,{children:["The top-level ",(0,t.jsx)(n.code,{children:"security"})," option configures the global ",(0,t.jsx)(n.code,{children:"DataMaskerService"}),", which masks request headers, body/query parameters, and user fields across wat
1chers. Anything you list here is ",(0,t.jsx)(n.strong,{children:"added"})," to the built-in defaults. To mask only what you name instead, see ",(0,t.jsx)(n.a,{href:"#masking-only-what-you-name",children:"Masking Only What You Name"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  security: {\n    dataMasking: {\n      // Additional headers to mask (merged with the 9 defaults)\n      sensitiveHeaders: ['x-internal-token'],\n      // Additional body/query params to mask (merged with defaults)\n      sensitiveParams: ['otp', 'pin'],\n      // Additional user object fields to mask (merged with defaults)\n      sensitiveUserFields: ['mfaSecret'],\n      // Replacement string. Default: '***REDACTED***'\n      maskReplacement: '***REDACTED***',\n    },\n\n    // Stack trace handling: 'none' | 'partial' | 'full'\n    // 'partial' is the default behavior in production\n    stackTraceSanitization: 'partial',\n  },\n})\n"})}),"\n",(0,t.jsx)(n.h3,{id:"masking-only-what-you-name",children:"Masking Only What You Name"}),"\n",(0,t.jsxs)(n.p,{children:["Each of the three lists accepts ",(0,t.jsx)(n.code,{children:"{ replace: [...] }"})," instead of an array. That\ndrops the built-in defaults for that list and masks exactly what is named:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"security: {\n  dataMasking: {\n    // 'pin' here is a map pin, not a credential \u2014 and the defaults redact it\n    sensitiveParams: { replace: ['otp', 'iban'] },\n  },\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Written out rather than inferred, because the built-in list is what stops a\npassword reaching storage when nobody thought about it. An array adds; only\n",(0,t.jsx)(n.code,{children:"{ replace: [...] }"})," takes away."]}),"\n",(0,t.jsxs)(n.p,{children:["GraphQL variables and responses are masked by\n",(0,t.jsx)(n.a,{href:"../watchers/graphql#which-terms-apply",children:(0,t.jsx)(n.code,{children:"watchers.graphql.sensitiveVariables"})}),",\nwhich resolves ",(0,t.jsx)(n.code,{children:"sensitiveParams"})," into its own list \u2014 so a term added here is\nmasked there too, and narrowing GraphQL alone is done there."]}),"\n",(0,t.jsx)(n.h3,{id:"input-limits-on-the-dashboard-api",children:"Input limits on the dashboard API"}),"\n",(0,t.jsx)(n.p,{children:"The API caps what a query may ask for. These are fixed rather than configurable\n\u2014 they are safety rails, not a knob:"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"limit"}),(0,t.jsx)(n.th,{children:"value"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsxs)(n.td,{children:["values per filter (",(0,t.jsx)(n.code,{children:"methods"}),", ",(0,t.jsx)(n.code,{children:"statuses"}),", ",(0,t.jsx)(n.code,{children:"tags"}),", \u2026)"]}),(0,t.jsx)(n.td,{children:"100"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"search term length"}),(0,t.jsx)(n.td,{children:"500 characters"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"tag name length"}),(0,t.jsx)(n.td,{children:"100 characters"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsxs)(n.td,{children:["entries per page (",(0,t.jsx)(n.code,{children:"limit"}),")"]}),(0,t.jsx)(n.td,{children:"1000"})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["Exceeding one is answered with ",(0,t.jsx)(n.code,{children:"400"})," naming the parameter."]}),"\n",(0,t.jsx)(n.h3,{id:"how-a-field-name-is-matched",children:"How a Field Name Is Matched"}),"\n",(0,t.jsxs)(n.p,{children:["A field is masked when its name ",(0,t.jsx)(n.strong,{children:"contains"})," one of the terms below. Case,\nunderscores and dashes are ignored, so ",(0,t.jsx)(n.code,{children:"access_token"}),", ",(0,t.jsx)(n.code,{children:"access-token"})," and\n",(0,t.jsx)(n.code,{children:"accessToken"})," are the same name \u2014 and ",(0,t.jsx)(n.code,{children:"confirmPassword"}),", ",(0,t.jsx)(n.code,{children:"currentPassword"}),",\n",(0,t.jsx)(n.code,{children:"stripeSecretKey"})," and ",(0,t.jsx)(n.code,{children:"refreshToken"})," all match too, which is what payloads\nactually look like."]}),"\n",(0,t.jsxs)(n.p,{children:["This masks a little more than strictly necessary \u2014 a field called ",(0,t.jsx)(n.code,{children:"tokenCount"}),"\ngoes as well. That is the safer direction for something recording production\ntraffic; narrow the configured list if a particular field needs to stay\nreadable."]}),"\n",(0,t.jsx)(n.h3,{id:"default-masked-bodyquery-parameters",children:"Default Masked Body/Query Parameters"}),"\n",(0,t.jsx)(n.p,{children:"A field is masked when its name contains any of these:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"password"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"passwd"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"secret"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"token"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"api_key"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"apikey"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"api-key"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"access_token"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"refresh_token"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"auth_token"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"credit_card"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"creditcard"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"card_number"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"cvv"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"cvc"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"ssn"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"social_security"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"private_key"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"privatekey"})}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"default-masked-user-fields",children:"Default Masked User Fields"}),"\n",(0,t.jsx)(n.p,{children:"When request user context is captured, a field of the user object is masked when\nits name contains any of these:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"password"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"passwordHash"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"password_hash"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"hashedPassword"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"token"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"apiKey"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"api_key"})}),"\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.code,{children:"secret"})}),"\n"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Note:"}
1)," These defaults belong to the global ",(0,t.jsx)(n.code,{children:"DataMaskerService"}),". The Model Watcher uses a separate, smaller list (see ",(0,t.jsx)(n.a,{href:"#model-data-masking",children:"Model Data Masking"}),") and replaces matched values with ",(0,t.jsx)(n.code,{children:"***MASKED***"})," rather than ",(0,t.jsx)(n.code,{children:"***REDACTED***"}),"."]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"custom-masking",children:"Custom Masking"}),"\n",(0,t.jsx)(n.p,{children:"Extend masking to protect additional sensitive data."}),"\n",(0,t.jsx)(n.h3,{id:"custom-sensitive-headers",children:"Custom Sensitive Headers"}),"\n",(0,t.jsx)(n.p,{children:"Add custom headers to mask:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  watchers: {\n    httpClient: {\n      sensitiveHeaders: [\n        'authorization',\n        'x-api-key',\n        'x-custom-token',      // Add custom header\n        'x-internal-secret',   // Add another\n      ],\n    },\n  },\n})\n"})}),"\n",(0,t.jsx)(n.h3,{id:"custom-request-parameters",children:"Custom Request Parameters"}),"\n",(0,t.jsx)(n.p,{children:"Mask specific request body parameters:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  watchers: {\n    httpClient: {\
1n      sensitiveRequestParams: [\n        'password',\n        'credit_card',\n        'ssn',\n        'api_secret',\n      ],\n    },\n  },\n})\n"})}),"\n",(0,t.jsx)(n.h3,{id:"custom-response-parameters",children:"Custom Response Parameters"}),"\n",(0,t.jsx)(n.p,{children:"Mask response data:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  watchers: {\n    httpClient: {\n      sensitiveResponseParams: [\n        'access_token',\n        'refresh_token',\n        'api_key',\n        'secret',\n      ],\n    },\n  },\n})\n"})}),"\n",(0,t.jsx)(n.h2,{id:"entry-filtering-for-sensitive-data",children:"Entry Filtering for Sensitive Data"}),"\n",(0,t.jsx)(n.p,{children:"Use the filter function to prevent sensitive data from being stored."}),"\n",(0,t.jsx)(n.h3,{id:"filter-by-route",children:"Filter by Route"}),"\n",(0,t.jsx)(n.p,{children:"Don't track sensitive endpoints:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  filter: (entry) => {\n    if (entry.type === 'request') {\n      // Don't track auth endpoints\n      if (entry.payload.path.startsWith('/auth/')) {\n        return false;\n      }\n\n      // Don't track password reset\n      if (entry.payload.path.includes('/reset-password')) {\n        return false;\n      }\n    }\n\n    return true;\n  },\n})\n"})}),"\n",(0,t.jsx)(n.h3,{id:"filter-by-content",children:"Filter by Content"}),"\n",(0,t.jsx)(n.p,{children:"Remove entries with sensitive data:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  filter: (entry) => {\n    // Don't store logs containing passwords\n    if (entry.type === 'log') {\n      const message = entry.payload.message.toLowerCase();\n      if (message.includes('password') || message.includes('secret')) {\n        return false;\n      }\n    }\n\n    return true;\n  },\n})\n"})}),"\n",(0,t.jsx)(n.h3,{id:"custom-masking-in-filter",children:"Custom Masking in Filter"}),"\n",(0,t.jsx)(n.p,{children:"Mask data before storage:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  filter: (entry) => {\n    if (entry.type === 'request' && entry.payload.body) {\n      // Mask sensitive body fields\n      const body = { ...entry.payload.body };\n\n      if (body.password) body.password = '********';\n      if (body.credit_card) body.credit_card = '********';\n\n      entry.payload.body = body;\n    }\n\n    return true;\n  },\n})\n"})}),"\n",(0,t.jsx)(n.h2,{id:"request-body-masking",children:"Request Body Masking"}),"\n",(0,t.jsx)(n.p,{children:"Control what request body data is captured."}),"\n",(0,t.jsx)(n.h3,{id:"disable-body-capture",children:"Disable Body Capture"}),"\n",(0,t.jsx)(n.p,{children:"Don't capture request bodies at all:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  watchers: {\n    request: {\n      captureBody: false,  // Never capture request bodies\n    },\n  },\n})\n"})}),"\n",(0,t.jsx)(n.h3,{id:"limit-body-size",children:"Limit Body Size"}),"\n",(0,t.jsx)(n.p,{children:"Prevent large payloads from being stored:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  watchers: {\n    request: {\n      maxBodySize: 1024,  // 1KB limit\n    },\n  },\n})\n"})}),"\n",(0,t.jsx)(n.p,{children:"Bodies exceeding this size are truncated:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"{\n  payload: {\n    body: {\n      _truncated: true,\n      _size: 524288  // 512KB actual size\n    }\n  }\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"response-body-masking",children:"Response Body Masking"}),"\n",(0,t.jsx)(n.p,{children:"Control response data capture."}),"\n",(0,t.jsx)(n.h3,{id:"disable-response-capture",children:"Disable Response Capture"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  watchers: {\n    request: {\n      captureResponse: false,  // Never capture responses\n    }
1,\n    httpClient: {\n      captureResponseBody: false,  // No outbound response bodies\n    },\n  },\n})\n"})}),"\n",(0,t.jsx)(n.h3,{id:"selective-response-capture",children:"Selective Response Capture"}),"\n",(0,t.jsx)(n.p,{children:"Capture only specific status codes:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  filter: (entry) => {\n    if (entry.type === 'request') {\n      // Only capture error responses\n      if (entry.payload.statusCode < 400) {\n        delete entry.payload.responseBody;\n      }\n    }\n    return true;\n  },\n})\n"})}),"\n",(0,t.jsx)(n.h2,{id:"production-security",children:"Production Security"}),"\n",(0,t.jsx)(n.h3,{id:"recommended-production-config",children:"Recommended Production Config"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  // Disable in production (safest)\n  enabled: process.env.NODE_ENV !== 'production',\n\n  // OR if enabled in production:\n  watchers: {\n    request: {\n      captureBody: false,           // Don't capture request bodies\n      captureResponse: false,        // Don't capture responses\n      captureSession: false,         // Don't capture sessions\n    }
1,\n    httpClient: {\n      captureRequestBody: false,     // No outbound request bodies\n      captureResponseBody: false,    // No outbound responses\n      sensitiveHeaders: [            // Mask all auth headers\n        'authorization',\n        'x-api-key',\n        'cookie',\n      ],\n    },\n    model: {\n      captureData: false,            // Never capture entity data\n    },\n  },\n\n  filter: (entry) => {\n    // Don't track auth endpoints\n    if (entry.type === 'request' &&\n        entry.payload.path.startsWith('/auth/')) {\n      return false;\n    }\n    return true;\n  },\n})\n"})}),"\n",(0,t.jsx)(n.h2,{id:"compliance-considerations",children:"Compliance Considerations"}),"\n",(0,t.jsx)(n.h3,{id:"gdpr-compliance",children:"GDPR Compliance"}),"\n",(0,t.jsx)(n.p,{children:"Protect personal data:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  watchers: {\n    model: {\n      captureData: false,  // Don't store personal data\n    },\n  },\n\n  filter: (entry) => {\n    // Mask email addresses\n    if (entry.type === 'log') {\n      entry.payload.message = entry.payload.message.replace(\n        /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g,\n        '***EMAIL***'\n      );\n    }\n    return true;\n  },\n})\n"})}),"\n",(0,t.jsx)(n.h3,{id:"pci-dss-compliance",children:"PCI DSS Compliance"}),"\n",(0,t.jsx)(n.p,{children:"Never store credit card data:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  filter: (entry) => {\n    const json = JSON.stringify(entry);\n\n    // Detect credit card patterns (simple example)\n    const cardPattern = /\\b\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}\\b/;\n\n    if (cardPattern.test(json)) {\n      // Don't store entries with potential card numbers\n      return false;\n    }\n\n    return true;\n  },\n})\n"})}),"\n",(0,t.jsx)(n.h3,{id:"hipaa-compliance",children:"HIPAA Compliance"}),"\n",(0,t.jsx)(n.p,{children:"Protect health information:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  watchers: {\n    request: {\n      captureBody: false,\n      captureResponse: false,\n    },\n  },\n\n  filter: (entry) => {\n    // Don't track medical endpoints\n    if (entry.type === 'request') {\n      const medicalPaths = ['/patient/', '/medical/', '/health/'];\n      if (medicalPaths.some(path => entry.payload.path.includes(path))) {\n        return false;\n      }\n    }\n    return true;\n  },\n})\n"})}),"\n",(0,t.jsx)(n.h2,{id:"verification",children:"Verification"}),"\n",(0,t.jsx)(n.h3,{id:"audit-captured-data",children:"Audit Captured Data"}),"\n",(0,t.jsx)(n.p,{children:"Regularly review what's being stored:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Check Dashboard"})," - Review entries for sensitive data"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Export Data"})," - Download entries and inspect"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Database Inspection"})," - Query storage directly"]}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sql",children:"-- SQLite example\nSELECT type, payload FROM entries\nWHERE payload LIKE '%password%'\n   OR payload LIKE '%token%'\n   OR payload LIKE '%secret%';\n"})}),"\n",(0,t.jsx)(n.h3,{id:"test-masking",children:"Test Masking"}),"\n",(0,t.jsx)(n.p,{children:"Verify masking is working:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:'// Make request with auth\ncurl -H "Authorization: Bearer secret-token" \\\n     http://localhost:3000/api/users\n\n// Check dashboard\n// Authorization header should show: ********\n'})}),"\n",(0,t.jsx)(n.h2,{id:"best-practices",children:"Best Practices"}),"\n",(0,t.jsx)(n.h3,{id:"1-default-to-minimal-capture",children:"1. Default to Minimal Capture"}),"\n",(0,t.jsx)(n.p,{children:"Start with minimal data capture:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"NestLensModule.forRoot({\n  watchers: {\n    request: {\n      captureBody: false,\n      captureResponse: false,\n    },\n  },\n})\n"})}),"\n",(0,t.jsx)(n.p,{children:"Enable only what you need for debugging."}),"\n",(0,t.jsx)(n.h3,{id:"2-use-filtering-liberally",children:"2. Use Filtering Liberally"}),"\n",(0,t.jsx)(n.p,{children:"Filter out sensitive routes:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"filter: (entry) => {\n  const sensitivePaths = [\n    '/auth/',\n    '/login',\n    '/password',\n    '/payment/',\n  ];\n\n  if (entry.type === 'request') {\n    return !sensitivePaths.some(path =>\n      entry.payload.path.includes(path)\n    );\n  }\n\n  return true;\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"3-regular-security-audits",children:"3. Regular Security Audits"}),"\n",(0,t.jsx)(n.p,{children:"Schedule reviews:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Monthly data audit"}),"\n",(0,t.jsx)(n.li,{children:"Quarterly security review"}),"\n",(0,t.jsx)(n.li,{children:"After any configuration changes"}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"4-educate-team",children:"4. Educate Team"}),"\n",(0,t.jsx)(n.p,{children:"Ensure developers know:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"What data is being captured"}),"\n",(0,t.jsx)(n.li,{children:"How to add custom masking"}),"\n",(0,t.jsx)(n.li,{children:"When to disable tracking"}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"5-document-sensitive-patterns",children:"5. Document Sensitive Patterns"}),"\n",(0,t.jsx)(n.p,{children:"Keep a list of what's considered sensitive in your app:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// sensitive-patterns.ts\nexport const SENSITIVE_HEADERS = [\n  'authorization',\n  'x-api-key',\n  'x-custom-auth',\n];\n\nexport const SENSITIVE_PATHS = [\n  '/auth/',\n  '/payment/',\n  '/admin/secrets/',\n];\n\nexport const SENSITIVE_FIELDS = [\n  'password',\n  'creditCard',\n  'ssn',\n];\n"})}),"\n",(0,t.jsx)(n.h2,{id:"next-steps",children:"Next Steps"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Configure ",(0,t.jsx)(n.a,{href:"/docs/security/access-control",children:"Access Control"})]}),"\n",(0,t.jsxs)(n.li,{children:["Set up ",(0,t.jsx)(n.a,{href:"/docs/security/ip-whitelisting",children:"IP Whitelisting"})]}),"\n",(0,t.jsxs)(n.li,{children:["Review ",(0,t.jsx)(n.a,{href:"/docs/security/production-usage",children:"Production Usage"})]}),"\n"]})]})}function h(e={}){let{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(o,{...e})}):o(e)}},8453(e,n,s){s.d(n,{R:()=>r,x:()=>d});var i=s(6540);let t={},a=i.createContext(t);function r(e){let n=i.useContext(a);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:r(e.components),i.createElement(a.Provider,{value:n},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.