Skip to content

Commit

Permalink
Anonymizes all paths before sending
Browse files Browse the repository at this point in the history
Change-type: patch
  • Loading branch information
otaviojacobi committed Jan 12, 2023
1 parent f09c006 commit 7939956
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 8 deletions.
7 changes: 6 additions & 1 deletion lib/gui/app/components/safe-webview/safe-webview.tsx
Expand Up @@ -183,7 +183,12 @@ export class SafeWebview extends React.PureComponent<
// only care about this event if it's a request for the main frame
if (event.resourceType === 'mainFrame') {
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({
shouldShow: event.statusCode === HTTP_OK,
});
Expand Down
122 changes: 117 additions & 5 deletions lib/gui/app/modules/analytics.ts
Expand Up @@ -21,12 +21,14 @@ import * as settings from '../models/settings';
import { store } from '../models/store';
import * as packageJSON from '../../../../package.json';

type AnalyticsPayload = _.Dictionary<any>;

const clearUserPath = (filename: string): string => {
const generatedFile = filename.split('generated').reverse()[0];
return generatedFile !== filename ? `generated${generatedFile}` : filename;
};

export const anonymizeData = (
export const anonymizeSentryData = (
event: SentryRenderer.Event,
): SentryRenderer.Event => {
event.exception?.values?.forEach((exception) => {
Expand All @@ -50,6 +52,68 @@ export const anonymizeData = (
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;
/**
* @summary Init analytics configurations
Expand All @@ -58,7 +122,7 @@ export const initAnalytics = _.once(() => {
const dsn =
settings.getSync('analyticsSentryToken') ||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
SentryRenderer.init({ dsn, beforeSend: anonymizeData });
SentryRenderer.init({ dsn, beforeSend: anonymizeSentryData });

const projectName =
settings.getSync('analyticsAmplitudeToken') ||
Expand All @@ -75,16 +139,64 @@ export const initAnalytics = _.once(() => {
: 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
.getState()
.toJS();

analyticsClient.track(message, {
const event = formatEvent({
...data,
applicationSessionUuid,
flashingWorkflowUuid,
});
analyticsClient.track(message, event);
}

/**
Expand All @@ -93,7 +205,7 @@ function reportAnalytics(message: string, data: _.Dictionary<any> = {}) {
* @description
* 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');
if (shouldReportAnalytics) {
initAnalytics();
Expand Down
4 changes: 2 additions & 2 deletions lib/gui/etcher.ts
Expand Up @@ -32,7 +32,7 @@ import { buildWindowMenu } from './menu';
import * as i18n from 'i18next';
import * as SentryMain from '@sentry/electron/main';
import * as packageJSON from '../../package.json';
import { anonymizeData } from './app/modules/analytics';
import { anonymizeSentryData } from './app/modules/analytics';

const customProtocol = 'etcher';
const scheme = `${customProtocol}://`;
Expand Down Expand Up @@ -110,7 +110,7 @@ const initSentryMain = _.once(() => {
settings.getSync('analyticsSentryToken') ||
_.get(packageJSON, ['analytics', 'sentry', 'token']);

SentryMain.init({ dsn, beforeSend: anonymizeData });
SentryMain.init({ dsn, beforeSend: anonymizeSentryData });
});

const sourceSelectorReady = new Promise((resolve) => {
Expand Down

0 comments on commit 7939956

Please sign in to comment.