mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-19 17:26:34 +00:00
Anonymizes all paths before sending
Change-type: patch
This commit is contained in:
parent
6c417e35a1
commit
86d43a536f
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
|
@ -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) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user