From e460669c650fb63f43528639a6231f6a0dc79a70 Mon Sep 17 00:00:00 2001 From: Nathan Cooper Date: Thu, 27 Aug 2026 13:03:22 -0400 Subject: [PATCH] record events the import allowance passes --- README.md | 2 +- fastaudit/core.py | 4 +++- tests/test_core.py | 12 +++++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 25ad005..a95ee45 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ Some packages have import-time side effects that raise sensitive audit events. F mypkg = "mypkg" ``` -The entries are module prefixes, so `mypkg` also covers `mypkg.submodule`. `fastaudit` does not import these modules when reading metadata. During an audit context, if the stack contains a frame for an allowed module whose `__spec__` is currently initializing, audit events from that import are allowed. Hosts can also pass `allow_imports=('mypkg',)` to `mk_audit()` or call `audit_perms.add_imports('mypkg')` outside the sandbox. +The entries are module prefixes, so `mypkg` also covers `mypkg.submodule`. `fastaudit` does not import these modules when reading metadata. During an audit context, if the stack contains a frame for an allowed module whose `__spec__` is currently initializing, audit events from that import are allowed. Hosts can also pass `allow_imports=('mypkg',)` to `mk_audit()` or call `audit_perms.add_imports('mypkg')` outside the sandbox. A host that passes `data=` with an `import_approved` list gets every event the allowance let through appended to it as `(event, args)`, so it can reconcile what a trusted import did. ### get/set attr hooks diff --git a/fastaudit/core.py b/fastaudit/core.py index b9d6e67..51d5133 100644 --- a/fastaudit/core.py +++ b/fastaudit/core.py @@ -223,7 +223,9 @@ def chk(cfg, event, args): if event in audit_allow or event.startswith(audit_allow_prefix): return if event in ('_thread.start_new_thread','_thread.start_joinable_thread') and asyncio_executor_thread(args): return if event.startswith('audit_perms.'): return deny(cfg, event, args, err_msg(event, args)) - if importing_allowed_module(cfg.import_allow): return + if importing_allowed_module(cfg.import_allow): + if (rec := (cfg.data or {}).get('import_approved')) is not None: rec.append((event, args)) + return if event in ('os.putenv','os.unsetenv'): if env_denied(args[0]): return deny(cfg, event, args, err_msg(event, args)) return diff --git a/tests/test_core.py b/tests/test_core.py index 62eb919..0aa73de 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,5 +1,5 @@ import asyncio, contextvars, fastaudit.core as core, importlib, nbformat, numpy as np, orjson, os, pytest, regex, shutil, subprocess, sys, tempfile, threading, traceback -from exhash import exhash_file +from exhash import file_exhash from exhash.exhash import line_hash as native_line_hash from fastcore.basics import Self from fastcore.foundation import working_directory @@ -112,7 +112,7 @@ def cached(): return 'ok' with expect_fail(PermissionError, 'lxml.etree._ElementTree.write_c14n'): tree.write_c14n('lxml-c14n.xml') with expect_fail(PermissionError, 'lxml.etree.xmlfile'): etree.xmlfile('lxml-file.xml') with expect_fail(PermissionError, 'lxml.etree._XSLTResultTree.write_output'): etree.XSLT(style)(xml).write_output('lxml-xslt.xml') - with expect_fail(PermissionError, 'exhash.exhash_file -> exhash._apply_file_command'): exhash_file('exhash.txt', ('0|0000|', 'a', 'x'), inplace=True) + with expect_fail(PermissionError, 'exhash.file_exhash -> exhash.'): file_exhash('exhash.txt', ('0|0000|', 'a', 'x'), inplace=True) with expect_fail(PermissionError): partial(native_line_hash, 'x')() # Audit policy cannot be replaced from inside the sandbox. @@ -150,7 +150,7 @@ def on_call(caller, callee, fn, code, off, data, calls): with mk_audit([tmp_path], before_deny=before_deny, on_call=on_call)(): # Host callbacks can allow native calls beyond the entry-point allowlist. f = tmp_path/'exhash.txt' - exhash_file(str(f), ('0|0000|', 'a', 'x'), inplace=True) + file_exhash(str(f), ('0|0000|', 'a', 'x'), inplace=True) assert f.read_text().strip() == 'x' # Host callbacks can also allow unknown or package-provided audit events. sys.audit('gc.get_objects', 0) @@ -193,6 +193,12 @@ def import_mod(nm): with expect_fail(PermissionError): audit_perms.add_imports('blocked_import') audit_perms.add_imports('blocked_import') with audit_perms(): assert import_mod('blocked_import').f() is None + + # Passing `data` with an `import_approved` list records the events the allowance let through. + rec = [] + with mk_audit([tmp_path], allow_imports=('runtime_import_ok',), monitor_calls=False, data=dict(import_approved=rec))(): + import_mod('runtime_import_ok') + assert any(ev=='object.__setattr__' for ev,_ in rec) finally: sys.path.remove(str(tmp_path))