hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
234 lines • 9.86 kB
JavaScript
import * as path from "node:path";
// This file is executed in a subprocess, and it is always run in this context.
// Therefore, it is acceptable to avoid using dynamic imports and instead import all necessary modules at the beginning.
import * as czech from "ethereum-cryptography/bip39/wordlists/czech.js";
import * as english from "ethereum-cryptography/bip39/wordlists/english.js";
import * as french from "ethereum-cryptography/bip39/wordlists/french.js";
import * as italian from "ethereum-cryptography/bip39/wordlists/italian.js";
import * as japanese from "ethereum-cryptography/bip39/wordlists/japanese.js";
import * as korean from "ethereum-cryptography/bip39/wordlists/korean.js";
import * as simplifiedChinese from "ethereum-cryptography/bip39/wordlists/simplified-chinese.js";
import * as SPANISH from "ethereum-cryptography/bip39/wordlists/spanish.js";
import * as traditionalChinese from "ethereum-cryptography/bip39/wordlists/traditional-chinese.js";
import { ANONYMIZED_PATH, anonymizeUserPaths } from "./anonymize-paths.js";
import { GENERIC_SERVER_NAME } from "./constants.js";
const ANONYMIZED_MNEMONIC = "<mnemonic>";
const MNEMONIC_PHRASE_LENGTH_THRESHOLD = 7;
const MINIMUM_AMOUNT_OF_WORDS_TO_ANONYMIZE = 4;
export class Anonymizer {
#configPath;
constructor(configPath) {
this.#configPath = configPath;
}
/**
* Anonymizes the events in the envelope in place, modifying the envelope.
*/
async anonymizeEventsFromEnvelope(envelope) {
for (const item of envelope[1]) {
if (item[0].type === "event") {
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-- We know that the item is an event item */
const eventItem = item;
const anonymizedEvent = await this.anonymizeEvent(eventItem[1]);
if (anonymizedEvent.success) {
eventItem[1] = anonymizedEvent.event;
}
else {
return { success: false, error: anonymizedEvent.error };
}
}
}
return { success: true, envelope };
}
/**
* Given a sentry serialized exception
* (https://develop.sentry.dev/sdk/event-payloads/exception/), return an
* anonymized version of the event.
*/
async anonymizeEvent(event) {
const result = {
event_id: event.event_id,
platform: event.platform,
timestamp: event.timestamp,
extra: event.extra,
release: event.release,
contexts: event.contexts,
sdk: event.sdk,
level: event.level,
server_name: GENERIC_SERVER_NAME,
environment: event.environment,
};
if (event.exception !== undefined && event.exception.values !== undefined) {
const anonymizedExceptions = await this.#anonymizeExceptions(event.exception.values);
result.exception = {
values: anonymizedExceptions,
};
}
return { success: true, event: result };
}
/**
* Return the anonymized filename and a boolean indicating if the content of
* the file should be anonymized
*/
async anonymizeFilename(filename) {
if (filename === this.#configPath) {
return {
anonymizedFilename: path.basename(filename),
anonymizeContent: true,
};
}
const anonymizedFilename = anonymizeUserPaths(filename);
// We anonymize the content if the file path is entirely anonymized, or if
// it wasn't anonymized because it's just a basename
const anonymizeContent = anonymizedFilename === ANONYMIZED_PATH ||
path.basename(filename) === filename;
return {
anonymizedFilename,
anonymizeContent,
};
}
anonymizeErrorMessage(errorMessage) {
errorMessage = this.#anonymizeMnemonic(errorMessage);
// We intentionally replace the config path with its own
// anonymized token, to help differentiate the config
// file in exception reports while keeping it anonymized.
if (this.#configPath !== undefined) {
errorMessage = errorMessage.replaceAll(this.#configPath, "<hardhat-config-file>");
}
// hide hex strings of 20 chars or more
const hexRegex = /(0x)?[0-9A-Fa-f]{20,}/g;
return anonymizeUserPaths(errorMessage).replace(hexRegex, (match) => match.replace(/./g, "x"));
}
async #anonymizeExceptions(exceptions) {
const anonymizedExceptions = await Promise.all(exceptions.map((exception) => this.#anonymizeException(exception)));
return anonymizedExceptions;
}
async #anonymizeException(value) {
const result = {
type: value.type,
mechanism: value.mechanism,
};
if (value.value !== undefined) {
result.value = this.anonymizeErrorMessage(value.value);
}
if (value.stacktrace !== undefined) {
result.stacktrace = await this.#anonymizeStacktrace(value.stacktrace);
}
return result;
}
async #anonymizeStacktrace(stacktrace) {
if (stacktrace.frames !== undefined) {
const anonymizedFrames = await this.#anonymizeFrames(stacktrace.frames);
return {
frames: anonymizedFrames,
};
}
return {};
}
async #anonymizeFrames(frames) {
const anonymizedFrames = await Promise.all(frames.map(async (frame) => {
return await this.#anonymizeFrame(frame);
}));
return anonymizedFrames;
}
async #anonymizeFrame(frame) {
const result = {
lineno: frame.lineno,
colno: frame.colno,
function: frame.function,
};
let anonymizeContent = true;
if (frame.filename !== undefined) {
const anonymizationResult = await this.anonymizeFilename(frame.filename);
result.filename = anonymizationResult.anonymizedFilename;
anonymizeContent = anonymizationResult.anonymizeContent;
}
if (!anonymizeContent) {
result.context_line = frame.context_line;
result.pre_context = frame.pre_context;
result.post_context = frame.post_context;
result.vars = frame.vars;
}
return result;
}
#anonymizeMnemonic(errorMessage) {
const matches = getAllWordMatches(errorMessage);
// If there are enough consecutive words, there's a good chance of there being a mnemonic phrase
if (matches.length < MNEMONIC_PHRASE_LENGTH_THRESHOLD) {
return errorMessage;
}
const mnemonicWordList = [
czech,
english,
french,
italian,
japanese,
korean,
simplifiedChinese,
SPANISH,
traditionalChinese,
]
.map((wordlistModule) => wordlistModule.wordlist)
.flat();
let anonymizedMessage = errorMessage.slice(0, matches[0].index);
// Determine all mnemonic phrase maximal fragments.
// We check sequences of n consecutive words just in case there is a typo
let wordIndex = 0;
while (wordIndex < matches.length) {
const maximalPhrase = getMaximalMnemonicPhrase(matches, errorMessage, wordIndex, mnemonicWordList);
if (maximalPhrase.length >= MINIMUM_AMOUNT_OF_WORDS_TO_ANONYMIZE) {
const lastAnonymizedWord = maximalPhrase[maximalPhrase.length - 1];
const nextWordIndex = wordIndex + maximalPhrase.length < matches.length
? matches[wordIndex + maximalPhrase.length].index
: errorMessage.length;
const sliceUntilNextWord = errorMessage.slice(lastAnonymizedWord.index + lastAnonymizedWord.word.length, nextWordIndex);
anonymizedMessage += `${ANONYMIZED_MNEMONIC}${sliceUntilNextWord}`;
wordIndex += maximalPhrase.length;
}
else {
const thisWord = matches[wordIndex];
const nextWordIndex = wordIndex + 1 < matches.length
? matches[wordIndex + 1].index
: errorMessage.length;
const sliceUntilNextWord = errorMessage.slice(thisWord.index, nextWordIndex);
anonymizedMessage += sliceUntilNextWord;
wordIndex++;
}
}
return anonymizedMessage;
}
}
function getMaximalMnemonicPhrase(matches, originalMessage, startIndex, mnemonicWordList) {
const maximalPhrase = [];
for (let i = startIndex; i < matches.length; i++) {
const thisMatch = matches[i];
if (!mnemonicWordList.includes(thisMatch.word)) {
break;
}
if (maximalPhrase.length > 0) {
// Check that there's only whitespace until this word.
const lastMatch = maximalPhrase[maximalPhrase.length - 1];
const lastIndex = lastMatch.index + lastMatch.word.length;
const sliceBetweenWords = originalMessage.slice(lastIndex, thisMatch.index);
if (!/\s+/u.test(sliceBetweenWords)) {
break;
}
}
maximalPhrase.push(thisMatch);
}
return maximalPhrase;
}
function getAllWordMatches(errorMessage) {
const matches = [];
const re = /\p{Letter}+/gu;
let match = re.exec(errorMessage);
while (match !== null) {
matches.push({
word: match[0],
index: match.index,
});
match = re.exec(errorMessage);
}
return matches;
}
//# sourceMappingURL=anonymizer.js.map