Commit e226e83d authored by Steve Dower's avatar Steve Dower Committed by GitHub

bpo-37363: Add audit events on startup for the run commands (GH-14524)

parent 0f4e8132
...@@ -905,6 +905,12 @@ always available. ...@@ -905,6 +905,12 @@ always available.
read, so that you can set this hook there. The :mod:`site` module read, so that you can set this hook there. The :mod:`site` module
:ref:`sets this <rlcompleter-config>`. :ref:`sets this <rlcompleter-config>`.
.. audit-event:: cpython.run_interactivehook hook sys.__interactivehook__
Raises an :ref:`auditing event <auditing>`
``cpython.run_interactivehook`` with the hook object as the argument when
the hook is called on startup.
.. versionadded:: 3.4 .. versionadded:: 3.4
......
...@@ -199,13 +199,18 @@ class AuditEvent(Directive): ...@@ -199,13 +199,18 @@ class AuditEvent(Directive):
.format(name, info['args'], new_info['args']) .format(name, info['args'], new_info['args'])
) )
if len(self.arguments) >= 3 and self.arguments[2]: ids = []
target = self.arguments[2] try:
ids = [] target = self.arguments[2].strip("\"'")
else: except (IndexError, TypeError):
target = "audit_event_{}_{}".format(name, len(info['source'])) target = None
target = re.sub(r'\W', '_', label) if not target:
ids = [target] target = "audit_event_{}_{}".format(
re.sub(r'\W', '_', name),
len(info['source']),
)
ids.append(target)
info['source'].append((env.docname, target)) info['source'].append((env.docname, target))
pnode = nodes.paragraph(text, classes=["audit-hook"], ids=ids) pnode = nodes.paragraph(text, classes=["audit-hook"], ids=ids)
...@@ -560,7 +565,8 @@ def process_audit_events(app, doctree, fromdocname): ...@@ -560,7 +565,8 @@ def process_audit_events(app, doctree, fromdocname):
row += nodes.entry('', node) row += nodes.entry('', node)
node = nodes.paragraph() node = nodes.paragraph()
for i, (doc, label) in enumerate(audit_event['source'], start=1): backlinks = enumerate(sorted(set(audit_event['source'])), start=1)
for i, (doc, label) in backlinks:
if isinstance(label, str): if isinstance(label, str):
ref = nodes.reference("", nodes.Text("[{}]".format(i)), internal=True) ref = nodes.reference("", nodes.Text("[{}]".format(i)), internal=True)
ref['refuri'] = "{}#{}".format( ref['refuri'] = "{}#{}".format(
......
...@@ -70,6 +70,7 @@ source. ...@@ -70,6 +70,7 @@ source.
:data:`sys.path` (allowing modules in that directory to be imported as top :data:`sys.path` (allowing modules in that directory to be imported as top
level modules). level modules).
.. audit-event:: cpython.run_command command cmdoption-c
.. cmdoption:: -m <module-name> .. cmdoption:: -m <module-name>
...@@ -106,13 +107,14 @@ source. ...@@ -106,13 +107,14 @@ source.
python -mtimeit -s 'setup here' 'benchmarked code here' python -mtimeit -s 'setup here' 'benchmarked code here'
python -mtimeit -h # for details python -mtimeit -h # for details
.. audit-event:: cpython.run_module module-name cmdoption-m
.. seealso:: .. seealso::
:func:`runpy.run_module` :func:`runpy.run_module`
Equivalent functionality directly available to Python code Equivalent functionality directly available to Python code
:pep:`338` -- Executing modules as scripts :pep:`338` -- Executing modules as scripts
.. versionchanged:: 3.1 .. versionchanged:: 3.1
Supply the package name to run a ``__main__`` submodule. Supply the package name to run a ``__main__`` submodule.
...@@ -129,6 +131,7 @@ source. ...@@ -129,6 +131,7 @@ source.
``"-"`` and the current directory will be added to the start of ``"-"`` and the current directory will be added to the start of
:data:`sys.path`. :data:`sys.path`.
.. audit-event:: cpython.run_stdin "" ""
.. describe:: <script> .. describe:: <script>
...@@ -148,6 +151,8 @@ source. ...@@ -148,6 +151,8 @@ source.
added to the start of :data:`sys.path` and the ``__main__.py`` file in added to the start of :data:`sys.path` and the ``__main__.py`` file in
that location is executed as the :mod:`__main__` module. that location is executed as the :mod:`__main__` module.
.. audit-event:: cpython.run_file filename
.. seealso:: .. seealso::
:func:`runpy.run_path` :func:`runpy.run_path`
Equivalent functionality directly available to Python code Equivalent functionality directly available to Python code
...@@ -540,6 +545,11 @@ conflict. ...@@ -540,6 +545,11 @@ conflict.
the interactive session. You can also change the prompts :data:`sys.ps1` and the interactive session. You can also change the prompts :data:`sys.ps1` and
:data:`sys.ps2` and the hook :data:`sys.__interactivehook__` in this file. :data:`sys.ps2` and the hook :data:`sys.__interactivehook__` in this file.
.. audit-event:: cpython.run_startup filename PYTHONSTARTUP
Raises an :ref:`auditing event <auditing>` ``cpython.run_startup`` with
the filename as the argument when called on startup.
.. envvar:: PYTHONOPTIMIZE .. envvar:: PYTHONOPTIMIZE
......
...@@ -57,7 +57,8 @@ class EmbeddingTestsMixin: ...@@ -57,7 +57,8 @@ class EmbeddingTestsMixin:
def tearDown(self): def tearDown(self):
os.chdir(self.oldcwd) os.chdir(self.oldcwd)
def run_embedded_interpreter(self, *args, env=None): def run_embedded_interpreter(self, *args, env=None,
timeout=None, returncode=0, input=None):
"""Runs a test in the embedded interpreter""" """Runs a test in the embedded interpreter"""
cmd = [self.test_exe] cmd = [self.test_exe]
cmd.extend(args) cmd.extend(args)
...@@ -73,18 +74,18 @@ class EmbeddingTestsMixin: ...@@ -73,18 +74,18 @@ class EmbeddingTestsMixin:
universal_newlines=True, universal_newlines=True,
env=env) env=env)
try: try:
(out, err) = p.communicate() (out, err) = p.communicate(input=input, timeout=timeout)
except: except:
p.terminate() p.terminate()
p.wait() p.wait()
raise raise
if p.returncode != 0 and support.verbose: if p.returncode != returncode and support.verbose:
print(f"--- {cmd} failed ---") print(f"--- {cmd} failed ---")
print(f"stdout:\n{out}") print(f"stdout:\n{out}")
print(f"stderr:\n{err}") print(f"stderr:\n{err}")
print(f"------") print(f"------")
self.assertEqual(p.returncode, 0, self.assertEqual(p.returncode, returncode,
"bad returncode %d, stderr is %r" % "bad returncode %d, stderr is %r" %
(p.returncode, err)) (p.returncode, err))
return out, err return out, err
...@@ -955,6 +956,37 @@ class AuditingTests(EmbeddingTestsMixin, unittest.TestCase): ...@@ -955,6 +956,37 @@ class AuditingTests(EmbeddingTestsMixin, unittest.TestCase):
def test_audit_subinterpreter(self): def test_audit_subinterpreter(self):
self.run_embedded_interpreter("test_audit_subinterpreter") self.run_embedded_interpreter("test_audit_subinterpreter")
def test_audit_run_command(self):
self.run_embedded_interpreter("test_audit_run_command", timeout=3, returncode=1)
def test_audit_run_file(self):
self.run_embedded_interpreter("test_audit_run_file", timeout=3, returncode=1)
def test_audit_run_interactivehook(self):
startup = os.path.join(self.oldcwd, support.TESTFN) + ".py"
with open(startup, "w", encoding="utf-8") as f:
print("import sys", file=f)
print("sys.__interactivehook__ = lambda: None", file=f)
try:
env = {**remove_python_envvars(), "PYTHONSTARTUP": startup}
self.run_embedded_interpreter("test_audit_run_interactivehook", timeout=5,
returncode=10, env=env)
finally:
os.unlink(startup)
def test_audit_run_startup(self):
startup = os.path.join(self.oldcwd, support.TESTFN) + ".py"
with open(startup, "w", encoding="utf-8") as f:
print("pass", file=f)
try:
env = {**remove_python_envvars(), "PYTHONSTARTUP": startup}
self.run_embedded_interpreter("test_audit_run_startup", timeout=5,
returncode=10, env=env)
finally:
os.unlink(startup)
def test_audit_run_stdin(self):
self.run_embedded_interpreter("test_audit_run_stdin", timeout=3, returncode=1)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
Adds audit events for the range of supported run commands (see
:ref:`using-on-general`).
...@@ -247,6 +247,10 @@ pymain_run_command(wchar_t *command, PyCompilerFlags *cf) ...@@ -247,6 +247,10 @@ pymain_run_command(wchar_t *command, PyCompilerFlags *cf)
goto error; goto error;
} }
if (PySys_Audit("cpython.run_command", "O", unicode) < 0) {
return pymain_exit_err_print();
}
bytes = PyUnicode_AsUTF8String(unicode); bytes = PyUnicode_AsUTF8String(unicode);
Py_DECREF(unicode); Py_DECREF(unicode);
if (bytes == NULL) { if (bytes == NULL) {
...@@ -267,6 +271,9 @@ static int ...@@ -267,6 +271,9 @@ static int
pymain_run_module(const wchar_t *modname, int set_argv0) pymain_run_module(const wchar_t *modname, int set_argv0)
{ {
PyObject *module, *runpy, *runmodule, *runargs, *result; PyObject *module, *runpy, *runmodule, *runargs, *result;
if (PySys_Audit("cpython.run_module", "u", modname) < 0) {
return pymain_exit_err_print();
}
runpy = PyImport_ImportModule("runpy"); runpy = PyImport_ImportModule("runpy");
if (runpy == NULL) { if (runpy == NULL) {
fprintf(stderr, "Could not import runpy module\n"); fprintf(stderr, "Could not import runpy module\n");
...@@ -311,6 +318,9 @@ static int ...@@ -311,6 +318,9 @@ static int
pymain_run_file(PyConfig *config, PyCompilerFlags *cf) pymain_run_file(PyConfig *config, PyCompilerFlags *cf)
{ {
const wchar_t *filename = config->run_filename; const wchar_t *filename = config->run_filename;
if (PySys_Audit("cpython.run_file", "u", filename) < 0) {
return pymain_exit_err_print();
}
FILE *fp = _Py_wfopen(filename, L"rb"); FILE *fp = _Py_wfopen(filename, L"rb");
if (fp == NULL) { if (fp == NULL) {
char *cfilename_buffer; char *cfilename_buffer;
...@@ -383,6 +393,9 @@ pymain_run_startup(PyConfig *config, PyCompilerFlags *cf, int *exitcode) ...@@ -383,6 +393,9 @@ pymain_run_startup(PyConfig *config, PyCompilerFlags *cf, int *exitcode)
if (startup == NULL) { if (startup == NULL) {
return 0; return 0;
} }
if (PySys_Audit("cpython.run_startup", "s", startup) < 0) {
return pymain_err_print(exitcode);
}
FILE *fp = _Py_fopen(startup, "r"); FILE *fp = _Py_fopen(startup, "r");
if (fp == NULL) { if (fp == NULL) {
...@@ -420,6 +433,10 @@ pymain_run_interactive_hook(int *exitcode) ...@@ -420,6 +433,10 @@ pymain_run_interactive_hook(int *exitcode)
return 0; return 0;
} }
if (PySys_Audit("cpython.run_interactivehook", "O", hook) < 0) {
goto error;
}
result = _PyObject_CallNoArg(hook); result = _PyObject_CallNoArg(hook);
Py_DECREF(hook); Py_DECREF(hook);
if (result == NULL) { if (result == NULL) {
...@@ -457,6 +474,10 @@ pymain_run_stdin(PyConfig *config, PyCompilerFlags *cf) ...@@ -457,6 +474,10 @@ pymain_run_stdin(PyConfig *config, PyCompilerFlags *cf)
return pymain_exit_err_print(); return pymain_exit_err_print();
} }
if (PySys_Audit("cpython.run_stdin", NULL) < 0) {
return pymain_exit_err_print();
}
int run = PyRun_AnyFileExFlags(stdin, "<stdin>", 0, cf); int run = PyRun_AnyFileExFlags(stdin, "<stdin>", 0, cf);
return (run != 0); return (run != 0);
} }
......
...@@ -1235,6 +1235,101 @@ static int test_audit_subinterpreter(void) ...@@ -1235,6 +1235,101 @@ static int test_audit_subinterpreter(void)
} }
} }
typedef struct {
const char* expected;
int exit;
} AuditRunCommandTest;
static int _audit_hook_run(const char *eventName, PyObject *args, void *userData)
{
AuditRunCommandTest *test = (AuditRunCommandTest*)userData;
if (strcmp(eventName, test->expected)) {
return 0;
}
if (test->exit) {
PyObject *msg = PyUnicode_FromFormat("detected %s(%R)", eventName, args);
if (msg) {
printf("%s\n", PyUnicode_AsUTF8(msg));
Py_DECREF(msg);
}
exit(test->exit);
}
PyErr_Format(PyExc_RuntimeError, "detected %s(%R)", eventName, args);
return -1;
}
static int test_audit_run_command(void)
{
AuditRunCommandTest test = {"cpython.run_command"};
wchar_t *argv[] = {L"./_testembed", L"-c", L"pass"};
Py_IgnoreEnvironmentFlag = 0;
PySys_AddAuditHook(_audit_hook_run, (void*)&test);
return Py_Main(Py_ARRAY_LENGTH(argv), argv);
}
static int test_audit_run_file(void)
{
AuditRunCommandTest test = {"cpython.run_file"};
wchar_t *argv[] = {L"./_testembed", L"filename.py"};
Py_IgnoreEnvironmentFlag = 0;
PySys_AddAuditHook(_audit_hook_run, (void*)&test);
return Py_Main(Py_ARRAY_LENGTH(argv), argv);
}
static int run_audit_run_test(int argc, wchar_t **argv, void *test)
{
PyStatus status;
PyConfig config;
status = PyConfig_InitPythonConfig(&config);
if (PyStatus_Exception(status)) {
Py_ExitStatusException(status);
}
config.argv.length = argc;
config.argv.items = argv;
config.parse_argv = 1;
config.program_name = argv[0];
config.interactive = 1;
config.isolated = 0;
config.use_environment = 1;
config.quiet = 1;
PySys_AddAuditHook(_audit_hook_run, test);
status = Py_InitializeFromConfig(&config);
if (PyStatus_Exception(status)) {
Py_ExitStatusException(status);
}
return Py_RunMain();
}
static int test_audit_run_interactivehook(void)
{
AuditRunCommandTest test = {"cpython.run_interactivehook", 10};
wchar_t *argv[] = {L"./_testembed"};
return run_audit_run_test(Py_ARRAY_LENGTH(argv), argv, &test);
}
static int test_audit_run_startup(void)
{
AuditRunCommandTest test = {"cpython.run_startup", 10};
wchar_t *argv[] = {L"./_testembed"};
return run_audit_run_test(Py_ARRAY_LENGTH(argv), argv, &test);
}
static int test_audit_run_stdin(void)
{
AuditRunCommandTest test = {"cpython.run_stdin"};
wchar_t *argv[] = {L"./_testembed"};
return run_audit_run_test(Py_ARRAY_LENGTH(argv), argv, &test);
}
static int test_init_read_set(void) static int test_init_read_set(void)
{ {
PyStatus status; PyStatus status;
...@@ -1413,6 +1508,11 @@ static struct TestCase TestCases[] = { ...@@ -1413,6 +1508,11 @@ static struct TestCase TestCases[] = {
{"test_open_code_hook", test_open_code_hook}, {"test_open_code_hook", test_open_code_hook},
{"test_audit", test_audit}, {"test_audit", test_audit},
{"test_audit_subinterpreter", test_audit_subinterpreter}, {"test_audit_subinterpreter", test_audit_subinterpreter},
{"test_audit_run_command", test_audit_run_command},
{"test_audit_run_file", test_audit_run_file},
{"test_audit_run_interactivehook", test_audit_run_interactivehook},
{"test_audit_run_startup", test_audit_run_startup},
{"test_audit_run_stdin", test_audit_run_stdin},
{NULL, NULL} {NULL, NULL}
}; };
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment