Hey, it's me, josh - as a memoji

josh.miami

Redacting personal identifiable information with Pino.js

Josh Echeverri
Josh Echeverri

Redacting personal identifiable information with Pino.js

Before Pino 5, developers using Pino to manage their applications logs would add a library known as pino-noir to hide sensitive information. Now Pino 5 has redaction built right in! The developer documentation is excellent, but I figured a microblog about configuring Pino to target specific information would still be helpful as the pattern has changed slightly.

Back in the days of pino-noir you would need to do something like this:

var noir = require("pino-noir");

var redaction = noir(
  ["key", "path.to.key", "path.leading.to.another.key", "check.*", "also[*]"],
  "Redacted!"
);

var pino = require("pino")({
  serializers: redaction,
});

pino.info({
  key: "will be redacted",
  path: {
    to: { key: "sensitive", another: "thing" },
    leading: { to: { another: { key: "wow" } } },
  },
  check: { out: "the", wildards: "yo!" },
  also: ["works", { with: "arrays" }],
});

// {"pid":89590,"hostname":"x","level":30,"time":1475104592035,"key":"Redacted!","path":{"to":{"key":"Redacted!","another":"thing"},"leading":{"to":{"another":{"key":"Redacted!"}}}},"check":{"out":"Redacted!","wildards":"Redacted!"},"also":["Redacted!","Redacted!"],"v":1}

var redaction2 = noir(["key"], (val) => "was " + val.substr(-8));

var pino2 = require("pino")({
  serializers: redaction2,
});

pino2.info({
  key: "was redacted",
});

// {"pid":89590,"hostname":"x","level":30,"time":1475104592035,{"key":"was redacted"},"v":1}

Pino 5 provides developers with nifty helpers for alternate redaction options rather than replacing the information.

Redact, censor, and remove`

Redact

Take this example of mine where we are redacting PII to be replaced with [Redacted]:

const pino = require("pino");

const redactOptions = {
  paths: [
    "user[*].name",
    "user[*].address",
    "user[*].dob",
    "user[*].credit_card_number",
    "user[*].credit_card_expiration",
    "user[*].credit_card_csv",
    "user[*].credit_card_billing_address",
  ],
};

const transports = pino.transport({
  targets: [
    {
      target: "pino-pretty",
      level: "info",
    },
  ],
});

module.exports = pino({
  transports,
  redact: redactOptions,
  level: "info",
});

When logging any data from the user table in my database, pino will output:

{
  "id": 1,
  "name": "[Redacted]",
  "email": "user@gmail.com",
  "address": "[Redacted]",
  "dob": "[Redacted]",
  "credit_card_number": "[Redacted]",
  "credit_card_expiration": "[Redacted]",
  "credit_card_csv": "[Redacted]",
  "credit_card_billing_address": "[Redacted]"
}

Censor

Pino also provides the ability to add custom messages as before. Here's an example to indicate the redaction of that data is [GDPR Compliant]:

const pino = require("pino");

const redactOptions = {
  paths: [
    "user[*].name",
    "user[*].address",
    "user[*].dob",
    "user[*].credit_card_number",
    "user[*].credit_card_expiration",
    "user[*].credit_card_csv",
    "user[*].credit_card_billing_address",
  ],
  censor: "[GDPR Compliant]",
};

const transports = pino.transport({
  targets: [
    {
      target: "pino-pretty",
      level: "info",
    },
  ],
});

module.exports = pino({
  transports,
  redact: redactOptions,
  level: "info",
});

When logging any data from the user table in my database, pino willnow output:

{
  "id": 1,
  "name": "[GDPR Compliant]",
  "email": "user@gmail.com",
  "address": "[GDPR Compliant]",
  "dob": "[GDPR Compliant]",
  "credit_card_number": "[GDPR Compliant]",
  "credit_card_expiration": "[GDPR Compliant]",
  "credit_card_csv": "[GDPR Compliant]",
  "credit_card_billing_address": "[GDPR Compliant]"
}

Remove

The best option, in my opinion, is to remove the data altogether. Check out this example:

const pino = require("pino");

const redactOptions = {
  paths: [
    "user[*].name",
    "user[*].address",
    "user[*].dob",
    "user[*].credit_card_number",
    "user[*].credit_card_expiration",
    "user[*].credit_card_csv",
    "user[*].credit_card_billing_address",
  ],
  remove: true,
};

const transports = pino.transport({
  targets: [
    {
      target: "pino-pretty",
      level: "info",
    },
  ],
});

module.exports = pino({
  transports,
  redact: redactOptions,
  level: "info",
});

When logging any data from the user table in my database, pino will now output:

{ "id": 1, "email": "user@gmail.com" }

Wrapping up and caveats

The three main redaction options to redact, censor, and remove are straightforward to implement.

Path syntax

To summarize, Pino is a fantastic library to make your life and apps compliance a breeze, and I'd suggest giving it a try. 😎

Sometimes, if a developer is working with an unfamiliar set of data, it may prove not easy to pinpoint its origin. Luckily, Pino provides various ways to target different path structures. The basic rules are:

  • paths may start with bracket notation
  • paths may contain the asterisk * to denote a wildcard
  • paths are case sensitive

Path examples:

- `a.b.c`
- `a["b-c"].d`
- `["a-b"].c`
- `a.b.*`
- `a[*].b`

Caveat

One thing to note is that redaction works with data provided at initialization. Path strings mustn't originate from user input or any data that may change after the initial render.