Skip to content

Commit 4b57587

Browse files
authored
Merge pull request #4877 from Crivella/feature-exclude_env_from_report
Avoid leaking keys by mistake with `--upload-test-report`
2 parents e219933 + 7fa8b10 commit 4b57587

File tree

3 files changed

+137
-13
lines changed

3 files changed

+137
-13
lines changed

easybuild/tools/testing.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"""
3838
import copy
3939
import os
40+
import re
4041
import sys
4142
from datetime import datetime
4243
from time import gmtime, strftime
@@ -58,6 +59,48 @@
5859

5960
_log = fancylogger.getLogger('testing', fname=False)
6061

62+
DEFAULT_EXCLUDE_FROM_TEST_REPORT_ENV_VAR_NAMES = [
63+
'KEY',
64+
'SECRET',
65+
'TOKEN',
66+
'PASSWORD',
67+
'API',
68+
'AUTH',
69+
'CREDENTIAL',
70+
'PRIVATE',
71+
'LICENSE',
72+
'LICENCE',
73+
]
74+
DEFAULT_EXCLUDE_FROM_TEST_REPORT_VALUE_REGEX = [
75+
# From PR comments https://github.com/easybuilders/easybuild-framework/pull/4877
76+
r'AKIA[0-9A-Z]{16}', # AWS access key
77+
r'[A-Za-z0-9/+=]{40}', # AWS secret key
78+
r'eyJ[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+', # JWT token
79+
r'gh[pousr]_[A-Za-z0-9_]{36,}', # GitHub token
80+
r'xox[baprs]-[A-Za-z0-9-]+', # Slack token
81+
82+
# https://github.com/odomojuli/regextokens
83+
# This is too aggressive and can end up excluding any alphanumeric string with length multiple of 4
84+
# r'^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$', # Base64
85+
r'[1-9][0-9]+-[0-9a-zA-Z]{40}', # Twitter token
86+
r'EAACEdEose0cBA[0-9A-Za-z]+', # Facebook token
87+
r'[0-9a-fA-F]{7}.[0-9a-fA-F]{32}', # Instagram token
88+
r'AIza[0-9A-Za-z-_]{35}', # Google API key
89+
r'4/[0-9A-Za-z-_]+', # Google OAuth 2.0 Auth code
90+
r'ya29.[0-9A-Za-z-_]+', # Google OAuth 2.0 access token
91+
r'[rs]k_live_[0-9a-z]{32}', # Picatic/Stripe API key
92+
r'sqOatp-[0-9A-Za-z-_]{22}', # Square Access token
93+
r'access_token,production$[0-9a-z]{161[0-9a,]{32}', # PayPal token
94+
r'55[0-9a-fA-F]{32}', # Twilio token
95+
r'key-[0-9a-zA-Z]{32}', # Mailgun API key
96+
r'[0-9a-f]{32}-us[0-9]{1,2}', # Mailchimp API key
97+
r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}', # Google Cloud Oauth 2.0 token
98+
r'[A-Za-z0-9_]{21}--[A-Za-z0-9_]{8}', # Google Cloud API key
99+
r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}', # Heroku token
100+
r'sk-(.*-)?[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}', # OpenAI API key
101+
r'waka_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', # WakaTime API key
102+
]
103+
61104

62105
def regtest(easyconfig_paths, modtool, build_specs=None):
63106
"""
@@ -265,8 +308,12 @@ def create_test_report(msg, ecs_with_res, init_session_state, pr_nrs=None, gist_
265308
for key in sorted(environ_dump.keys()):
266309
if env_filter is not None and env_filter.search(key):
267310
continue
268-
else:
269-
environment += ["%s = %s" % (key, environ_dump[key])]
311+
if any(x in key.upper() for x in DEFAULT_EXCLUDE_FROM_TEST_REPORT_ENV_VAR_NAMES):
312+
continue
313+
value = environ_dump[key]
314+
if any(re.match(rgx, value) for rgx in DEFAULT_EXCLUDE_FROM_TEST_REPORT_VALUE_REGEX):
315+
continue
316+
environment += ["%s = %s" % (key, value)]
270317

271318
test_report.extend(["#### Environment", "```"] + environment + ["```"])
272319

test/framework/github.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,26 +1317,83 @@ def test_github_create_test_report(self):
13171317
'log_file': logfile,
13181318
}),
13191319
]
1320+
environ = {
1321+
'USER': 'test',
1322+
}
1323+
JWT_HDR = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
1324+
JWT_PLD = 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNzA4MzQ1MTIzLCJleHAiOjE3MDgzNTUxMjN9'
1325+
JWT_SIG = 'SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
1326+
secret_environ = {
1327+
# Test default removal based on variable value
1328+
'TOTALLYPUBLICVAR1': 'AKIAIOSFODNN7EXAMPLE', # AWS_ACCESS_KEY
1329+
'TOTALLYPUBLICVAR2': 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', # AWS_SECRET_KEY
1330+
'TOTALLYPUBLICVAR3': '.'.join([JWT_HDR, JWT_PLD, JWT_SIG]), # JWT
1331+
'TOTALLYPUBLICVAR4': 'ghp_123456789_ABCDEFGHIJKlmnopqrstuvwxyz', # GH_TOKEN
1332+
'TOTALLYPUBLICVAR5': 'xoxb-1234567890-1234567890123-ABCDEFabcdef', # SLACK_TOKEN
1333+
1334+
# Test default removal based on variable name
1335+
'API_SOMETHING': '1234567890',
1336+
'MY_PASSWORD': '1234567890',
1337+
'ABC_TOKEN': '1234567890',
1338+
'AUTH_XXX': '1234567890',
1339+
'LICENSE': '1234567890',
1340+
'WORLD_KEY': '1234567890',
1341+
'PRIVATE_INFO': '1234567890',
1342+
'SECRET_SECRET': '1234567890',
1343+
'INFO_CREDENTIALS': '1234567890',
1344+
}
13201345
init_session_state = {
13211346
'easybuild_configuration': ['EASYBUILD_DEBUG=1'],
1322-
'environment': {'USER': 'test'},
1347+
'environment': {**environ, **secret_environ},
13231348
'module_list': [{'mod_name': 'test'}],
13241349
'system_info': {'name': 'test'},
13251350
'time': gmtime(0),
13261351
}
1352+
13271353
res = create_test_report("just a test", ecs_with_res, init_session_state)
13281354
patterns = [
13291355
"**SUCCESS** _test.eb_",
13301356
"**FAIL (build issue)** _fail.eb_",
13311357
"01 Jan 1970 00:00:00",
13321358
"EASYBUILD_DEBUG=1",
1359+
"USER = test",
13331360
]
13341361
for pattern in patterns:
13351362
self.assertIn(pattern, res['full'])
13361363

1337-
for pattern in patterns[:2]:
1364+
# Test that known token regexes for ENV vars are excluded by default
1365+
exclude_patterns = [
1366+
'TOTALLYPUBLICVAR1',
1367+
'TOTALLYPUBLICVAR2',
1368+
'TOTALLYPUBLICVAR3',
1369+
'TOTALLYPUBLICVAR4',
1370+
'TOTALLYPUBLICVAR5',
1371+
1372+
'API_SOMETHING',
1373+
'MY_PASSWORD',
1374+
'ABC_TOKEN',
1375+
'AUTH_XXX',
1376+
'LICENSE',
1377+
'WORLD_KEY',
1378+
'PRIVATE_INFO',
1379+
'SECRET_SECRET',
1380+
'INFO_CREDENTIALS',
1381+
]
1382+
for pattern in exclude_patterns:
1383+
# .lower() test that variable name is not case sensitive for excluding
1384+
self.assertNotIn(pattern.lower(), res['full'])
1385+
1386+
res = create_test_report("just a test", ecs_with_res, init_session_state)
1387+
for pattern in patterns:
13381388
self.assertIn(pattern, res['full'])
13391389

1390+
for pattern in patterns[:2]:
1391+
self.assertIn(pattern, res['overview'])
1392+
1393+
for pattern in exclude_patterns:
1394+
# .lower() test that variable name is not case sensitive for excluding
1395+
self.assertNotIn(pattern.lower(), res['full'])
1396+
13401397
# mock create_gist function, we don't want to actually create a gist every time we run this test...
13411398
def fake_create_gist(*args, **kwargs):
13421399
return 'https://gist.github.com/%s/test' % GITHUB_TEST_ACCOUNT
@@ -1353,7 +1410,7 @@ def fake_create_gist(*args, **kwargs):
13531410
self.assertIn(pattern, res['full'])
13541411

13551412
for pattern in patterns[:3]:
1356-
self.assertIn(pattern, res['full'])
1413+
self.assertIn(pattern, res['overview'])
13571414

13581415
self.assertIn("**SUCCESS** _test.eb_", res['overview'])
13591416

test/framework/options.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3390,26 +3390,46 @@ def toy(extra_args=None):
33903390
return test_report_txt
33913391

33923392
# define environment variables that should (not) show up in the test report
3393-
test_var_secret = 'THIS_IS_JUST_A_SECRET_ENV_VAR_FOR_EASYBUILD'
3394-
os.environ[test_var_secret] = 'thisshouldremainsecretonrequest'
3395-
test_var_secret_regex = re.compile(test_var_secret)
3393+
# The name contains an auto-excluded pattern `SECRET`
3394+
test_var_secret_always = 'THIS_IS_JUST_A_SECRET_ENV_VAR_FOR_EASYBUILD'
3395+
os.environ[test_var_secret_always] = 'thisshouldremainsecretonrequest'
3396+
test_var_secret_always_regex = re.compile(test_var_secret_always)
3397+
# The name contains an autoexcluded value as a recognized GH token
3398+
test_var_secret_always2 = 'THIS_IS_JUST_A_TOTALLY_PUBLIC_ENV_VAR_FOR_EASYBUILD'
3399+
os.environ[test_var_secret_always2] = 'ghp_123456789_ABCDEFGHIJKlmnopqrstuvwxyz'
3400+
test_var_secret_always_regex2 = re.compile(test_var_secret_always2)
3401+
# This should be in general present and excluded on demand
3402+
test_var_secret_ondemand = 'THIS_IS_A_CUSTOM_ENV_VAR_FOR_EASYBUILD'
3403+
os.environ[test_var_secret_ondemand] = 'thisshouldbehiddenondemand'
3404+
test_var_secret_ondemand_regex = re.compile(test_var_secret_ondemand)
33963405
test_var_public = 'THIS_IS_JUST_A_PUBLIC_ENV_VAR_FOR_EASYBUILD'
33973406
os.environ[test_var_public] = 'thisshouldalwaysbeincluded'
33983407
test_var_public_regex = re.compile(test_var_public)
33993408

34003409
# default: no filtering
34013410
test_report_txt = toy()
3402-
self.assertTrue(test_var_secret_regex.search(test_report_txt))
3411+
self.assertTrue(test_var_secret_ondemand_regex.search(test_report_txt))
34033412
self.assertTrue(test_var_public_regex.search(test_report_txt))
3413+
for rgx in [
3414+
test_var_secret_always_regex,
3415+
test_var_secret_always_regex2,
3416+
]:
3417+
res = rgx.search(test_report_txt)
3418+
self.assertFalse(res, "No match for %s in %s" % (rgx.pattern, test_report_txt))
34043419

34053420
# filter out env vars that match specified regex pattern
3406-
filter_arg = "--test-report-env-filter=.*_SECRET_ENV_VAR_FOR_EASYBUILD"
3421+
filter_arg = "--test-report-env-filter=.*_IS_A_CUSTOM_ENV_VAR_FOR_EASYBUILD"
34073422
test_report_txt = toy(extra_args=[filter_arg])
3408-
res = test_var_secret_regex.search(test_report_txt)
3409-
self.assertFalse(res, "No match for %s in %s" % (test_var_secret_regex.pattern, test_report_txt))
3423+
for rgx in [
3424+
test_var_secret_ondemand_regex,
3425+
test_var_secret_always_regex,
3426+
test_var_secret_always_regex2,
3427+
]:
3428+
res = rgx.search(test_report_txt)
3429+
self.assertFalse(res, "No match for %s in %s" % (rgx.pattern, test_report_txt))
34103430
self.assertTrue(test_var_public_regex.search(test_report_txt))
34113431
# make sure that used filter is reported correctly in test report
3412-
filter_arg_regex = re.compile(r"--test-report-env-filter='.\*_SECRET_ENV_VAR_FOR_EASYBUILD'")
3432+
filter_arg_regex = re.compile(r"--test-report-env-filter='.\*_IS_A_CUSTOM_ENV_VAR_FOR_EASYBUILD'")
34133433
tup = (filter_arg_regex.pattern, test_report_txt)
34143434
self.assertTrue(filter_arg_regex.search(test_report_txt), "%s in %s" % tup)
34153435

0 commit comments

Comments
 (0)