Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/logging.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { createConsola } from "consola";
import { updateConfig } from "./format.js";
import type { RedactConfig } from "./redact.js";
import {
mergeRedactConfigs,
parseEnvRedactConfig,
type RedactConfig,
} from "./redact.js";
import DatadogReporter from "./reporters/datadog.js";
import DiscordReporter from "./reporters/discord.js";

const setupLogging = (config: RedactConfig = {}) => {
updateConfig(config);
updateConfig(mergeRedactConfigs(config, parseEnvRedactConfig()));
Comment thread
SynthLuvr marked this conversation as resolved.
const consola = createConsola({ fancy: true, level: 5 });

// Set Discord reporter as first so it can remove
Expand Down
62 changes: 61 additions & 1 deletion src/redact.ts
Comment thread
SynthLuvr marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DeepRedact } from "@hackylabs/deep-redact/index.ts";
import { validateMnemonic } from "@scure/bip39";
import { wordlist } from "@scure/bip39/wordlists/english.js";
import { type } from "arktype";

/**
* Configurable redaction for strings that *look like secrets*:
Expand Down Expand Up @@ -239,5 +240,64 @@ const createRedact = (config: RedactConfig) => {
};
};

export { createRedact };
const SerializableContextRuleType = type({
re: "string",
"flags?": "string",
"before?": "number",
"after?": "number",
}).pipe(
(rule): ContextRule => ({
re: new RegExp(rule.re, rule.flags ?? ""),
before: rule.before,
after: rule.after,
}),
);

const SerializableDetectorConfigType = type({
"allow?": SerializableContextRuleType.array(),
});

const SerializableRedactConfigType = type({
"hex?": SerializableDetectorConfigType,
"base64?": SerializableDetectorConfigType,
"base64url?": SerializableDetectorConfigType,
"base58?": SerializableDetectorConfigType,
"mnemonic?": SerializableDetectorConfigType,
});

const mergeDetectorConfigs = (
a?: DetectorConfig,
b?: DetectorConfig,
): DetectorConfig | undefined => {
if (!a && !b) return undefined;
const allowA = a?.allow ?? [];
const allowB = b?.allow ?? [];
const merged = [...allowA, ...allowB];
return { allow: merged.length > 0 ? merged : undefined };
};

const mergeRedactConfigs = (
a: RedactConfig,
b: RedactConfig,
): RedactConfig => ({
hex: mergeDetectorConfigs(a.hex, b.hex),
base64: mergeDetectorConfigs(a.base64, b.base64),
base64url: mergeDetectorConfigs(a.base64url, b.base64url),
base58: mergeDetectorConfigs(a.base58, b.base58),
mnemonic: mergeDetectorConfigs(a.mnemonic, b.mnemonic),
});

const parseEnvRedactConfig = (): RedactConfig => {
const raw = process.env.REDACT_CONFIG;
if (!raw) return {};

const json = Buffer.from(raw, "base64").toString("utf8");
const parsed: unknown = JSON.parse(json);
const validated = SerializableRedactConfigType(parsed);
Comment thread
SynthLuvr marked this conversation as resolved.
if (validated instanceof type.errors)
throw new Error(`REDACT_CONFIG validation failed: ${validated.summary}`);
return validated;
};

export { createRedact, mergeRedactConfigs, parseEnvRedactConfig };
export type { RedactConfig };
116 changes: 114 additions & 2 deletions tests/redact.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest";
import { createRedact } from "../src/redact.js";
import { afterEach, describe, expect, it } from "vitest";
import {
createRedact,
mergeRedactConfigs,
parseEnvRedactConfig,
} from "../src/redact.js";

describe("redact", () => {
it("redacts base64url", () => {
Expand Down Expand Up @@ -54,3 +58,111 @@ describe("redact", () => {
);
});
});

describe("mergeRedactConfigs", () => {
it("merges allow rules from both configs", () => {
const a = { hex: { allow: [{ re: /\b(event)\b/i }] } };
const b = { hex: { allow: [{ re: /\b(transfer)\b/i }] } };
const merged = mergeRedactConfigs(a, b);
const redact = createRedact(merged);

const eventResult = redact(
"Pushed event fcc6533b59301096a973b8be3e6518f0cd13f73a9821de558cca77ac9b014d6e.1771865100000",
);
expect(eventResult).toContain(
"fcc6533b59301096a973b8be3e6518f0cd13f73a9821de558cca77ac9b014d6e",
);

const transferResult = redact(
"transfer 538845bf2f418e0c7f3798d6bcb632273d46633545a5e261feceb7d378ed0761",
);
expect(transferResult).toContain(
"538845bf2f418e0c7f3798d6bcb632273d46633545a5e261feceb7d378ed0761",
);
});

it("handles empty configs", () => {
const merged = mergeRedactConfigs({}, {});
expect(merged).toEqual({
hex: undefined,
base64: undefined,
base64url: undefined,
base58: undefined,
mnemonic: undefined,
});
});

it("uses only a config when b is empty", () => {
const a = { hex: { allow: [{ re: /\b(event)\b/i }] } };
const merged = mergeRedactConfigs(a, {});
const redact = createRedact(merged);
const result = redact(
"Pushed event fcc6533b59301096a973b8be3e6518f0cd13f73a9821de558cca77ac9b014d6e.1771865100000",
);
expect(result).toContain(
"fcc6533b59301096a973b8be3e6518f0cd13f73a9821de558cca77ac9b014d6e",
);
});
});

describe("parseEnvRedactConfig", () => {
afterEach(() => {
delete process.env.REDACT_CONFIG;
});
Comment thread
SynthLuvr marked this conversation as resolved.

it("returns empty config when REDACT_CONFIG is not set", () => {
delete process.env.REDACT_CONFIG;
const config = parseEnvRedactConfig();
expect(config).toEqual({});
});

it("parses base64-encoded JSON config", () => {
const serializable = {
hex: { allow: [{ re: "\\b(event)\\b", flags: "i" }] },
};
process.env.REDACT_CONFIG = Buffer.from(
JSON.stringify(serializable),
).toString("base64");
const config = parseEnvRedactConfig();

expect(config.hex?.allow).toHaveLength(1);
expect(config.hex?.allow?.[0].re).toBeInstanceOf(RegExp);
expect(config.hex?.allow?.[0].re.source).toBe("\\b(event)\\b");
expect(config.hex?.allow?.[0].re.flags).toContain("i");
});

it("throws on invalid base64", () => {
process.env.REDACT_CONFIG = "!!!not-valid-base64!!!";
expect(() => parseEnvRedactConfig()).toThrow();
});

it("throws on invalid JSON", () => {
process.env.REDACT_CONFIG = Buffer.from("{not json}").toString("base64");
expect(() => parseEnvRedactConfig()).toThrow();
});

it("throws when JSON structure fails arktype validation", () => {
process.env.REDACT_CONFIG = Buffer.from(
JSON.stringify({ hex: { allow: [{ re: 123 }] } }),
).toString("base64");
expect(() => parseEnvRedactConfig()).toThrow();
});

it("applies env config allow rules when redacting", () => {
const serializable = {
hex: { allow: [{ re: "\\b(transfer)\\b", flags: "i" }] },
};
process.env.REDACT_CONFIG = Buffer.from(
JSON.stringify(serializable),
).toString("base64");
const envConfig = parseEnvRedactConfig();

const redact = createRedact(envConfig);
const result = redact(
"transfer 538845bf2f418e0c7f3798d6bcb632273d46633545a5e261feceb7d378ed0761",
);
expect(result).toBe(
"transfer 538845bf2f418e0c7f3798d6bcb632273d46633545a5e261feceb7d378ed0761",
);
});
});