Anonymizes all paths before sending

Change-type: patch
This commit is contained in:
Otávio Jacobi 2023-01-10 15:24:26 +00:00
parent 6c417e35a1
commit 86d43a536f
3 changed files with 125 additions and 8 deletions

View File

@ -183,7 +183,12 @@ export class SafeWebview extends React.PureComponent<
// only care about this event if it's a request for the main frame // only care about this event if it's a request for the main frame
if (event.resourceType === 'mainFrame') { if (event.resourceType === 'mainFrame') {
const HTTP_OK = 200; const HTTP_OK = 200;
analytics.logEvent('SafeWebview loaded', { event }); const { webContents, ...webviewEvent } = event;
analytics.logEvent('SafeWebview loaded', {
...webviewEvent,
screen_height: webContents?.hostWebContents.browserWindowOptions.height,
screen_width: webContents?.hostWebContents.browserWindowOptions.width,
});
this.setState({ this.setState({
shouldShow: event.statusCode === HTTP_OK, shouldShow: event.statusCode === HTTP_OK,
}); });

View File

@ -21,12 +21,14 @@ import * as settings from '../models/settings';
import { store } from '../models/store'; import { store } from '../models/store';
import * as packageJSON from '../../../../package.json'; import * as packageJSON from '../../../../package.json';
type AnalyticsPayload = _.Dictionary<any>;
const clearUserPath = (filename: string): string => { const clearUserPath = (filename: string): string => {
const generatedFile = filename.split('generated').reverse()[0]; const generatedFile = filename.split('generated').reverse()[0];
return generatedFile !== filename ? `generated${generatedFile}` : filename; return generatedFile !== filename ? `generated${generatedFile}` : filename;
}; };
export const anonymizeData = ( export const anonymizeSentryData = (
event: SentryRenderer.Event, event: SentryRenderer.Event,
): SentryRenderer.Event => { ): SentryRenderer.Event => {
event.exception?.values?.forEach((exception) => { event.exception?.values?.forEach((exception) => {
@ -50,6 +52,68 @@ export const anonymizeData = (
return event; return event;
}; };
const extractPathRegex = /(.*)(^|\s)(file\:\/\/)?(\w\:)?([\\\/].+)/;
const etcherSegmentMarkers = ['app.asar', 'Resources'];
export const anonymizePath = (input: string) => {
// First, extract a part of the value that matches a path pattern.
const match = extractPathRegex.exec(input);
if (match === null) {
return input;
}
const mainPart = match[5];
const space = match[2];
const beginning = match[1];
const uriPrefix = match[3] || '';
// We have to deal with both Windows and POSIX here.
// The path starts with its separator (we work with absolute paths).
const sep = mainPart[0];
const segments = mainPart.split(sep);
// Moving from the end, find the first marker and cut the path from there.
const startCutIndex = _.findLastIndex(segments, (segment) =>
etcherSegmentMarkers.includes(segment),
);
return (
beginning +
space +
uriPrefix +
'[PERSONAL PATH]' +
sep +
segments.splice(startCutIndex).join(sep)
);
};
const safeAnonymizePath = (input: string) => {
try {
return anonymizePath(input);
} catch (e) {
return '[ANONYMIZE PATH FAILED]';
}
};
const sensitiveEtcherProperties = [
'error.description',
'error.message',
'error.stack',
'image',
'image.path',
'path',
];
export const anonymizeAnalyticsPayload = (
data: AnalyticsPayload,
): AnalyticsPayload => {
for (const prop of sensitiveEtcherProperties) {
const value = data[prop];
if (value != null) {
data[prop] = safeAnonymizePath(value.toString());
}
}
return data;
};
let analyticsClient: Client; let analyticsClient: Client;
/** /**
* @summary Init analytics configurations * @summary Init analytics configurations
@ -58,7 +122,7 @@ export const initAnalytics = _.once(() => {
const dsn = const dsn =
settings.getSync('analyticsSentryToken') || settings.getSync('analyticsSentryToken') ||
_.get(packageJSON, ['analytics', 'sentry', 'token']); _.get(packageJSON, ['analytics', 'sentry', 'token']);
SentryRenderer.init({ dsn, beforeSend: anonymizeData }); SentryRenderer.init({ dsn, beforeSend: anonymizeSentryData });
const projectName = const projectName =
settings.getSync('analyticsAmplitudeToken') || settings.getSync('analyticsAmplitudeToken') ||
@ -75,16 +139,64 @@ export const initAnalytics = _.once(() => {
: createNoopClient(); : createNoopClient();
}); });
function reportAnalytics(message: string, data: _.Dictionary<any> = {}) { const getCircularReplacer = () => {
const seen = new WeakSet();
return (key: any, value: any) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
function flattenObject(obj: any) {
const toReturn: AnalyticsPayload = {};
for (const i in obj) {
if (!obj.hasOwnProperty(i)) {
continue;
}
if (Array.isArray(obj[i])) {
toReturn[i] = obj[i];
continue;
}
if (typeof obj[i] === 'object' && obj[i] !== null) {
const flatObject = flattenObject(obj[i]);
for (const x in flatObject) {
if (!flatObject.hasOwnProperty(x)) {
continue;
}
toReturn[i.toLowerCase() + '.' + x.toLowerCase()] = flatObject[x];
}
} else {
toReturn[i] = obj[i];
}
}
return toReturn;
}
function formatEvent(data: any): AnalyticsPayload {
const event = JSON.parse(JSON.stringify(data, getCircularReplacer()));
return anonymizeAnalyticsPayload(flattenObject(event));
}
function reportAnalytics(message: string, data: AnalyticsPayload = {}) {
const { applicationSessionUuid, flashingWorkflowUuid } = store const { applicationSessionUuid, flashingWorkflowUuid } = store
.getState() .getState()
.toJS(); .toJS();
analyticsClient.track(message, { const event = formatEvent({
...data, ...data,
applicationSessionUuid, applicationSessionUuid,
flashingWorkflowUuid, flashingWorkflowUuid,
}); });
analyticsClient.track(message, event);
} }
/** /**
@ -93,7 +205,7 @@ function reportAnalytics(message: string, data: _.Dictionary<any> = {}) {
* @description * @description
* This function sends the debug message to product analytics services. * This function sends the debug message to product analytics services.
*/ */
export async function logEvent(message: string, data: _.Dictionary<any> = {}) { export async function logEvent(message: string, data: AnalyticsPayload = {}) {
const shouldReportAnalytics = await settings.get('errorReporting'); const shouldReportAnalytics = await settings.get('errorReporting');
if (shouldReportAnalytics) { if (shouldReportAnalytics) {
initAnalytics(); initAnalytics();

View File

@ -32,7 +32,7 @@ import { buildWindowMenu } from './menu';
import * as i18n from 'i18next'; import * as i18n from 'i18next';
import * as SentryMain from '@sentry/electron/main'; import * as SentryMain from '@sentry/electron/main';
import * as packageJSON from '../../package.json'; import * as packageJSON from '../../package.json';
import { anonymizeData } from './app/modules/analytics'; import { anonymizeSentryData } from './app/modules/analytics';
const customProtocol = 'etcher'; const customProtocol = 'etcher';
const scheme = `${customProtocol}://`; const scheme = `${customProtocol}://`;
@ -110,7 +110,7 @@ const initSentryMain = _.once(() => {
settings.getSync('analyticsSentryToken') || settings.getSync('analyticsSentryToken') ||
_.get(packageJSON, ['analytics', 'sentry', 'token']); _.get(packageJSON, ['analytics', 'sentry', 'token']);
SentryMain.init({ dsn, beforeSend: anonymizeData }); SentryMain.init({ dsn, beforeSend: anonymizeSentryData });
}); });
const sourceSelectorReady = new Promise((resolve) => { const sourceSelectorReady = new Promise((resolve) => {