Skip to content

Commit

Permalink
Bug 1636251: Patch Sentry events to ensure a raw username isn't sent …
Browse files Browse the repository at this point in the history
…to Sentry r=rstewart

To avoid sending identifying information, common absolute paths are patched with placeholder values. For example, devs
may place their Firefox repository within their home dir, so absolute paths are doctored to be prefixed with
"<topsrcdir"> instead.

Additionally, any paths including the user's home directory are patched to instead be a relate path from "~".

Differential Revision: https://phabricator.services.mozilla.com/D78962
  • Loading branch information
Mitchell Hentges committed Jun 11, 2020
1 parent 98acb2c commit d6fe34f
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 27 deletions.
2 changes: 1 addition & 1 deletion build/mach_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ def post_dispatch_handler(context, handler, instance, result,
state_dir],
stdout=devnull, stderr=devnull)

def populate_context(context, key=None):
def populate_context(key=None):
if key is None:
return
if key == 'state_dir':
Expand Down
14 changes: 7 additions & 7 deletions python/mach/mach/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def __getattribute__(self, key):
return getattr(object.__getattribute__(self, '_context'), key)
except AttributeError as e:
try:
ret = object.__getattribute__(self, '_handler')(self, key)
ret = object.__getattribute__(self, '_handler')(key)
except (AttributeError, TypeError):
# TypeError is in case the handler comes from old code not
# taking a key argument.
Expand All @@ -175,14 +175,11 @@ class Mach(object):
populate_context_handler -- If defined, it must be a callable. The
callable signature is the following:
populate_context_handler(context, key=None)
populate_context_handler(key=None)
It acts as a fallback getter for the mach.base.CommandContext
instance.
This allows to augment the context instance with arbitrary data
for use in command handlers.
For backwards compatibility, it is also called before command
dispatch without a key, allowing the context handler to add
attributes to the context instance.
require_conditions -- If True, commands that do not have any condition
functions applied will be skipped. Defaults to False.
Expand Down Expand Up @@ -320,7 +317,11 @@ def run(self, argv, stdin=None, stdout=None, stderr=None):
Returns the integer exit code that should be used. 0 means success. All
other values indicate failure.
"""
register_sentry()
if self.populate_context_handler:
topsrcdir = self.populate_context_handler('topdir')
register_sentry(topsrcdir)
else:
register_sentry()

# If no encoding is defined, we default to UTF-8 because without this
# Python 2.7 will assume the default encoding of ASCII. This will blow
Expand Down Expand Up @@ -401,7 +402,6 @@ def _run(self, argv):
commands=Registrar)

if self.populate_context_handler:
self.populate_context_handler(context)
context = ContextWrapper(context, self.populate_context_handler)

parser = self.get_argument_parser(context)
Expand Down
67 changes: 61 additions & 6 deletions python/mach/mach/sentry.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@

import os
import re
import sys
from os.path import expanduser

import mozpack.path as mozpath
import sentry_sdk
from six.moves.configparser import SafeConfigParser, NoOptionError

from mozboot.util import get_state_dir

from six import string_types
from six.moves.configparser import SafeConfigParser, NoOptionError

# https://sentry.prod.mozaws.net/operations/mach/
_SENTRY_DSN = "https://[email protected]/525"


def register_sentry():
def register_sentry(topsrcdir=None):
cfg_file = os.path.join(get_state_dir(), 'machrc')
config = SafeConfigParser()

Expand All @@ -32,10 +34,17 @@ def register_sentry():
if not telemetry_enabled:
return

sentry_sdk.init(_SENTRY_DSN, before_send=_settle_mach_module_id)
sentry_sdk.init(_SENTRY_DSN,
before_send=lambda event, _: _process_event(event, topsrcdir))


def _process_event(sentry_event, topsrcdir):
for map_fn in (_settle_mach_module_id, _patch_absolute_paths):
sentry_event = map_fn(sentry_event, topsrcdir)
return sentry_event


def _settle_mach_module_id(sentry_event, exception):
def _settle_mach_module_id(sentry_event, _):
# Sentry groups issues according to the stack frames and their associated
# "module" properties. However, one of the modules is being reported
# like "mach.commands.26a828ef5164403eaff4305ab4cb0fab" (with a generated id).
Expand All @@ -54,6 +63,52 @@ def _settle_mach_module_id(sentry_event, exception):
return sentry_event


def _resolve_topobjdir():
topobjdir = os.path.join(os.path.dirname(sys.prefix), "..")
return mozpath.normsep(os.path.normpath(topobjdir))


def _patch_absolute_paths(sentry_event, topsrcdir):
# As discussed here (https://bugzilla.mozilla.org/show_bug.cgi?id=1636251#c28),
# we remove usernames from file names with a best-effort basis. The most likely
# place for usernames to manifest in Sentry information is within absolute paths,
# such as: "/home/mitch/dev/firefox/mach"
# We replace the state_dir, obj_dir, src_dir with "<...>" placeholders.
# Note that we also do a blanket find-and-replace of the user's name with "<user>",
# which may have ill effects if the user's name is, by happenstance, a substring
# of some other value within the Sentry event.
def recursive_patch(value, needle, replacement):
if isinstance(value, list):
return [recursive_patch(v, needle, replacement) for v in value]
elif isinstance(value, dict):
for key in list(value.keys()):
next_value = value.pop(key)
key = key.replace(needle, replacement)
value[key] = recursive_patch(next_value, needle, replacement)
return value
elif isinstance(value, string_types):
return value.replace(needle, replacement)
else:
return value

for (needle, replacement) in (
(get_state_dir(), "<statedir>"),
(_resolve_topobjdir(), "<topobjdir>"),
(topsrcdir, "<topsrcdir>"),
(expanduser("~"), "~"),
# Sentry converts "vars" to their "representations". When paths are in local
# variables on Windows, "C:\Users\MozillaUser\Desktop" becomes
# "'C:\\Users\\MozillaUser\\Desktop'". To still catch this case, we "repr"
# the home directory and scrub the beginning and end quotes, then
# find-and-replace on that.
(repr(expanduser("~"))[1:-1], "~"),
):
if needle is None:
continue # topsrcdir isn't always defined
sentry_event = recursive_patch(sentry_event, needle, replacement)
return sentry_event


def report_exception(exception):
# sentry_sdk won't report the exception if `sentry-sdk.init(...)` hasn't been called
sentry_sdk.capture_exception(exception)
5 changes: 3 additions & 2 deletions python/mozbuild/mozbuild/code-analysis/mach_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,9 @@ def static_analysis(self):
"""
mach = Mach(os.getcwd())

def populate_context(context, key=None):
context.topdir = self.topsrcdir
def populate_context(key=None):
if key == 'topdir':
return self.topsrcdir

mach.populate_context_handler = populate_context
mach.run(['static-analysis', '--help'])
Expand Down
30 changes: 19 additions & 11 deletions testing/tools/mach_test_package_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,17 +191,25 @@ def bootstrap(test_package_root):
import mach.main

def populate_context(context, key=None):
if key is None:
context.package_root = test_package_root
context.bin_dir = os.path.join(test_package_root, 'bin')
context.certs_dir = os.path.join(test_package_root, 'certs')
context.module_dir = os.path.join(test_package_root, 'modules')
context.ancestors = ancestors
context.normalize_test_path = normalize_test_path
return

# The values for the following 'key's will be set lazily, and cached
# after first being invoked.
# These values will be set lazily, and cached after first being invoked.
if key == "package_root":
return test_package_root

if key == "bin_dir":
return os.path.join(test_package_root, 'bin')

if key == "certs_dir":
return os.path.join(test_package_root, 'certs')

if key == "module_dir":
return os.path.join(test_package_root, 'modules')

if key == "ancestors":
return ancestors

if key == "normalize_test_path":
return normalize_test_path

if key == 'firefox_bin':
return find_firefox(context)

Expand Down

0 comments on commit d6fe34f

Please sign in to comment.