From ddd62e25df4ffb908037040faeadf562b29cae68 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Thu, 13 Aug 2026 19:16:18 +0800 Subject: [PATCH] add audit hook for fs, network and processes --- src/slic3r/plugin/PluginAuditManager.cpp | 437 ++++++++++++++++++---- src/slic3r/plugin/PluginAuditManager.hpp | 30 +- src/slic3r/plugin/PluginFsUtils.cpp | 30 +- src/slic3r/plugin/PluginFsUtils.hpp | 6 +- src/slic3r/plugin/PythonInterpreter.cpp | 4 - tests/slic3rutils/test_plugin_install.cpp | 14 +- 6 files changed, 424 insertions(+), 97 deletions(-) diff --git a/src/slic3r/plugin/PluginAuditManager.cpp b/src/slic3r/plugin/PluginAuditManager.cpp index f027dd072d..023e55bd22 100644 --- a/src/slic3r/plugin/PluginAuditManager.cpp +++ b/src/slic3r/plugin/PluginAuditManager.cpp @@ -14,12 +14,103 @@ #include #include #include +#include +#include #include +#include #include #include namespace Slic3r { +// extensive list of audit events can be found at https://docs.python.org/3/library/audit_events.html +static const std::unordered_map audit_event_categories{ + // fsread + {"glob.glob", AuditEventCategory::FsRead}, + {"glob.glob/2", AuditEventCategory::FsRead}, + {"os.fwalk", AuditEventCategory::FsRead}, + {"os.getxattr", AuditEventCategory::FsRead}, + {"os.listdir", AuditEventCategory::FsRead}, + {"os.listdrives", AuditEventCategory::FsRead}, + {"os.listmounts", AuditEventCategory::FsRead}, + {"os.listvolumes", AuditEventCategory::FsRead}, + {"os.listxattr", AuditEventCategory::FsRead}, + {"os.scandir", AuditEventCategory::FsRead}, + {"os.walk", AuditEventCategory::FsRead}, + {"pathlib.Path.glob", AuditEventCategory::FsRead}, + {"pathlib.Path.rglob", AuditEventCategory::FsRead}, + + // fsreadwrite + {"os.chflags", AuditEventCategory::FsReadWrite}, + {"os.chmod", AuditEventCategory::FsReadWrite}, + {"os.chown", AuditEventCategory::FsReadWrite}, + {"os.removexattr", AuditEventCategory::FsReadWrite}, + {"os.rename", AuditEventCategory::FsReadWrite}, + {"os.setxattr", AuditEventCategory::FsReadWrite}, + {"os.truncate", AuditEventCategory::FsReadWrite}, + {"os.utime", AuditEventCategory::FsReadWrite}, + {"shutil.chown", AuditEventCategory::FsReadWrite}, + {"shutil.copymode", AuditEventCategory::FsReadWrite}, + {"shutil.copystat", AuditEventCategory::FsReadWrite}, + {"shutil.copyfile", AuditEventCategory::FsReadWrite}, + {"shutil.copytree", AuditEventCategory::FsReadWrite}, + {"shutil.make_archive", AuditEventCategory::FsReadWrite}, + {"shutil.move", AuditEventCategory::FsReadWrite}, + {"shutil.unpack_archive", AuditEventCategory::FsReadWrite}, + + // fscreate + {"os.link", AuditEventCategory::FsCreate}, + {"os.mkdir", AuditEventCategory::FsCreate}, + {"os.symlink", AuditEventCategory::FsCreate}, + {"tempfile.mkdtemp", AuditEventCategory::FsCreate}, + {"tempfile.mkstemp", AuditEventCategory::FsCreate}, + {"_winapi.CreateJunction", AuditEventCategory::FsCreate}, + + // fsdelete + {"os.remove", AuditEventCategory::FsDelete}, + {"os.rmdir", AuditEventCategory::FsDelete}, + {"shutil.rmtree", AuditEventCategory::FsDelete}, + + // http + {"http.client.connect", AuditEventCategory::Http}, + {"http.client.send", AuditEventCategory::Http}, + {"urllib.Request", AuditEventCategory::Http}, + + // socket + {"socket.__new__", AuditEventCategory::Socket}, + {"socket.bind", AuditEventCategory::Socket}, + {"socket.connect", AuditEventCategory::Socket}, + {"socket.getaddrinfo", AuditEventCategory::Socket}, + {"socket.gethostbyaddr", AuditEventCategory::Socket}, + {"socket.gethostbyname", AuditEventCategory::Socket}, + {"socket.gethostname", AuditEventCategory::Socket}, + {"socket.getnameinfo", AuditEventCategory::Socket}, + {"socket.getservbyname", AuditEventCategory::Socket}, + {"socket.getservbyport", AuditEventCategory::Socket}, + {"socket.sendmsg", AuditEventCategory::Socket}, + {"socket.sendto", AuditEventCategory::Socket}, + + // processcreate + {"os.fork", AuditEventCategory::ProcessCreate}, + {"os.forkpty", AuditEventCategory::ProcessCreate}, + {"os.posix_spawn", AuditEventCategory::ProcessCreate}, + {"os.spawn", AuditEventCategory::ProcessCreate}, + {"os.system", AuditEventCategory::ProcessCreate}, + {"os.startfile", AuditEventCategory::ProcessCreate}, + {"os.startfile/2", AuditEventCategory::ProcessCreate}, + {"pty.spawn", AuditEventCategory::ProcessCreate}, + {"subprocess.Popen", AuditEventCategory::ProcessCreate}, + {"_winapi.CreateProcess", AuditEventCategory::ProcessCreate}, + {"_posixsubprocess.fork_exec", AuditEventCategory::ProcessCreate}, +}; + +// Returns the category event_name belongs to, or AuditEventCategory::None when it isn't audited. +static AuditEventCategory event_category(const std::string& event_name) +{ + const auto it = audit_event_categories.find(event_name); + return it == audit_event_categories.end() ? AuditEventCategory::None : it->second; +} + // --------------------------------------------------------------------------- // Path safety // --------------------------------------------------------------------------- @@ -344,7 +435,7 @@ void PluginAuditManager::report_violation(const AuditViolation& violation) m_audit_denial_pending = true; BOOST_LOG_TRIVIAL(warning) << "[AUDIT BLOCKED] plugin=" << violation.plugin_key << " event=" << violation.event_name - << " path=" << violation.path.string() << " reason=" << violation.reason; + << " reason=" << violation.reason; } bool PluginAuditManager::audit_denial_pending() const { return m_audit_denial_pending; } @@ -366,6 +457,34 @@ bool PluginAuditManager::last_violation(AuditViolation& violation) const return true; } +bool PluginAuditManager::has_approved_ancestor(const std::string& plugin_key, + const std::vector& call_site_ids) const +{ + if (plugin_key.empty() || call_site_ids.empty()) + return false; + + std::lock_guard lock(m_mutex); + auto it = m_approved_call_sites.find(plugin_key); + if (it == m_approved_call_sites.end()) + return false; + + for (const auto& id : call_site_ids) + if (it->second.count(id)) + return true; + return false; +} + +void PluginAuditManager::record_approved_call_sites(const std::string& plugin_key, + const std::vector& call_site_ids) +{ + if (plugin_key.empty() || call_site_ids.empty()) + return; + + std::lock_guard lock(m_mutex); + auto& approved = m_approved_call_sites[plugin_key]; + approved.insert(call_site_ids.begin(), call_site_ids.end()); +} + // --------------------------------------------------------------------------- // The C-level audit hook // --------------------------------------------------------------------------- @@ -398,38 +517,163 @@ std::string python_path(PyObject* object) return result; } -void add_filesystem_path(std::vector& paths, PyObject* object) +std::string python_unicode(PyObject* object) { - const std::string path = python_path(object); - if (!path.empty()) - paths.emplace_back(path); + if (!object || !PyUnicode_Check(object)) + return {}; + const char* text = PyUnicode_AsUTF8(object); + if (!text) { + PyErr_Clear(); + return {}; + } + return text; } -std::vector filesystem_paths(const std::string& event_name, PyObject* args) +std::string python_str(PyObject* object) { - std::vector paths; + if (!object) + return {}; - if (event_name == "open") { - add_filesystem_path(paths, tuple_item(args, 0)); - } else if (event_name == "os.rename") { - add_filesystem_path(paths, tuple_item(args, 0)); - add_filesystem_path(paths, tuple_item(args, 1)); - } else if (event_name == "os.remove") { - add_filesystem_path(paths, tuple_item(args, 0)); + PyObject* str_object = PyObject_Str(object); + if (!str_object) { + PyErr_Clear(); + return {}; } - return paths; + const std::string result = python_unicode(str_object); + Py_DECREF(str_object); + return result; } -bool has_filesystem_permission(const PluginInstallState& state, const boost::filesystem::path& path) +std::vector call_site_identities(const std::string& plugin_root) { - return std::find(state.permissions.fs_read.begin(), state.permissions.fs_read.end(), path.string()) != - state.permissions.fs_read.end(); + std::vector ids; + if (plugin_root.empty()) + return ids; + + PyFrameObject* frame = PyEval_GetFrame(); // borrowed reference + Py_XINCREF(frame); // normalize to an owned reference for the loop below + + while (frame) { + PyCodeObject* code = PyFrame_GetCode(frame); // new reference + const std::string filename = python_unicode(reinterpret_cast(code->co_filename)); + const bool is_plugin_frame = !filename.empty() && boost::algorithm::starts_with(filename, plugin_root); + + std::string id; + if (!is_plugin_frame && !filename.empty()) { + const std::string funcname = python_unicode(reinterpret_cast(code->co_name)); + id = filename + ":" + funcname + ":" + std::to_string(code->co_firstlineno); + } + Py_DECREF(code); + + PyFrameObject* back = PyFrame_GetBack(frame); // new reference, or nullptr at the top of the stack + Py_DECREF(frame); + frame = back; + + if (is_plugin_frame) + break; + if (!id.empty()) + ids.push_back(std::move(id)); + } + + Py_XDECREF(frame); // only holds a reference here if the loop exited via break + + return ids; } -bool persist_filesystem_permission(const std::string& plugin_key, - PluginInstallState& state, - const boost::filesystem::path& path) +static const std::unordered_set two_path_fs_events{ + "os.rename", "os.link", "os.symlink", "shutil.copyfile", + "shutil.copytree", "shutil.copymode", "shutil.copystat", "shutil.move", + "shutil.unpack_archive", "_winapi.CreateJunction", +}; + +static const std::unordered_map> audit_target_arg_indices{ + {"http.client.connect", {1}}, + {"urllib.Request", {0}}, + + {"socket.connect", {1}}, + {"socket.bind", {1}}, + {"socket.getaddrinfo", {0}}, + {"socket.gethostbyname", {0}}, + {"socket.gethostbyaddr", {0}}, + {"socket.getnameinfo", {0}}, + {"socket.getservbyname", {0}}, + {"socket.getservbyport", {0}}, + + {"os.system", {0}}, + {"subprocess.Popen", {1, 0}}, + {"os.posix_spawn", {1, 0}}, + {"os.spawn", {2, 1}}, + {"os.startfile", {0}}, + {"pty.spawn", {0}}, + {"_winapi.CreateProcess", {1, 0}}, + {"_posixsubprocess.fork_exec", {0}}, +}; + +AuditEventCategory open_category(PyObject* args) +{ + const std::string mode = python_unicode(tuple_item(args, 1)); + if (!mode.empty() && mode.find_first_of("wax+") == std::string::npos) + return AuditEventCategory::FsRead; + return AuditEventCategory::FsReadWrite; +} + +std::vector audit_targets(const std::string& event_name, AuditEventCategory category, PyObject* args) +{ + std::vector targets; + + switch (category) { + case AuditEventCategory::FsRead: + case AuditEventCategory::FsReadWrite: + case AuditEventCategory::FsCreate: + case AuditEventCategory::FsDelete: { + const Py_ssize_t path_count = two_path_fs_events.count(event_name) ? 2 : 1; + for (Py_ssize_t index = 0; index < path_count; ++index) { + const std::string path = python_path(tuple_item(args, index)); + if (!path.empty()) + targets.push_back(path); + } + return targets; + } + default: + break; + } + + const auto it = audit_target_arg_indices.find(event_name); + if (it != audit_target_arg_indices.end()) { + for (const Py_ssize_t index : it->second) { + std::string value = python_str(tuple_item(args, index)); + if (!value.empty()) { + targets.push_back(std::move(value)); + break; + } + } + } + + return targets; +} + +std::vector* permission_list_for(AuditEventCategory category, PluginPermissions& permissions) +{ + switch (category) { + case AuditEventCategory::FsRead: return &permissions.fs_read; + case AuditEventCategory::FsReadWrite: return &permissions.fs_readwrite; + case AuditEventCategory::Http: return &permissions.network_http; + case AuditEventCategory::Socket: return &permissions.network_socket; + case AuditEventCategory::ProcessCreate: return &permissions.process; + default: return nullptr; + } +} + +bool has_permission(const std::vector& granted, const std::string& target) +{ + return std::find(granted.begin(), granted.end(), target) != granted.end(); +} + +bool persist_permission(const std::string& plugin_key, + PluginInstallState& state, + std::vector& permission_list, + const std::string& target) { PluginDescriptor descriptor; if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty()) @@ -445,23 +689,18 @@ bool persist_filesystem_permission(const std::string& plugin_key, state.enabled = true; } - const std::string path_string = path.string(); - if (std::find(state.permissions.fs_read.begin(), state.permissions.fs_read.end(), path_string) == - state.permissions.fs_read.end()) - state.permissions.fs_read.push_back(path_string); + if (!has_permission(permission_list, target)) + permission_list.push_back(target); return write_install_state(boost::filesystem::path(descriptor.plugin_root), state); } -// Records a blocked event and raises PermissionError in the calling interpreter. int report_denied(PluginAuditManager& mgr, const std::string& event_name, - const boost::filesystem::path& target, const AuditDecision& decision) { AuditViolation violation; violation.plugin_key = mgr.current_plugin(); violation.event_name = event_name; - violation.path = target; violation.reason = decision.reason; mgr.report_violation(violation); @@ -469,6 +708,74 @@ int report_denied(PluginAuditManager& mgr, return -1; } +wxString audit_message(AuditEventCategory category, const wxString& plugin_name, const wxString& event_name, + const wxString& target_list) +{ + if (target_list.IsEmpty()) + return wxString::Format( + _L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\".\n\n" + "This operation does not expose a target the audit hook can display."), + plugin_name, event_name); + + switch (category) { + case AuditEventCategory::FsRead: + return wxString::Format(_L("Plugin \"%s\" is requesting to read the following file(s):\n%s"), plugin_name, target_list); + case AuditEventCategory::FsReadWrite: + return wxString::Format(_L("Plugin \"%s\" is requesting to read/write the following file(s):\n%s"), plugin_name, target_list); + case AuditEventCategory::FsCreate: + return wxString::Format(_L("Plugin \"%s\" is requesting to create the following file(s):\n%s"), plugin_name, target_list); + case AuditEventCategory::FsDelete: + return wxString::Format(_L("Plugin \"%s\" is requesting to delete the following file(s):\n%s"), plugin_name, target_list); + case AuditEventCategory::Http: + return wxString::Format(_L("Plugin \"%s\" is requesting to make an HTTP request to:\n%s"), plugin_name, target_list); + case AuditEventCategory::Socket: + return wxString::Format(_L("Plugin \"%s\" is requesting to open a network connection to:\n%s"), plugin_name, target_list); + case AuditEventCategory::ProcessCreate: + return wxString::Format(_L("Plugin \"%s\" is requesting to run the following command(s):\n%s"), plugin_name, target_list); + default: + return wxString::Format(_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\"."), plugin_name, event_name); + } +} + +int decide_audited_event(PluginAuditManager& mgr, + PluginInstallState& state, + const std::string& plugin_key, + const std::string& plugin_name, + const std::string& event_name, + AuditEventCategory category, + const std::vector& targets, + std::vector* permission_list, + const std::vector& call_site_ids) +{ + std::vector unresolved = targets; + if (permission_list) { + unresolved.clear(); + for (const auto& target : targets) + if (!has_permission(*permission_list, target)) + unresolved.push_back(target); + if (!targets.empty() && unresolved.empty()) + return 0; + } + + wxString target_list; + for (const auto& target : unresolved) + target_list += wxString::FromUTF8(target.c_str()) + "\n"; + + wxMessageDialog dialog(nullptr, + audit_message(category, wxString::FromUTF8(plugin_name.c_str()), + wxString::FromUTF8(event_name.c_str()), target_list), + _L("Plugin permission request"), wxYES_NO | wxICON_WARNING); + if (dialog.ShowModal() != wxID_YES) + return report_denied(mgr, event_name, {false, "audit permission required"}); + + if (permission_list) + for (const auto& target : unresolved) + persist_permission(plugin_key, state, *permission_list, target); + + mgr.record_approved_call_sites(plugin_key, call_site_ids); + return 0; +} + } // namespace PluginAuditDetail int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data) @@ -479,12 +786,33 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user std::string event_name(event ? event : ""); + if (event_name.empty()) + return 0; + // Verbose logging of every audit event (can be noisy) if (mgr->verbose_events) { BOOST_LOG_TRIVIAL(debug) << "[AUDIT EVENT] " << event_name; } - if (mgr->current_plugin().empty()) + if (mgr->current_plugin().empty()) { + BOOST_LOG_TRIVIAL(trace) << "[AUDIT] event=" << event_name << " bypassed (no plugin context)"; + return 0; + } + + PluginDescriptor plugin_descriptor; + PluginManager::instance().try_get_plugin_descriptor(mgr->current_plugin(), plugin_descriptor); + + // Some plugins call other audited events, e.g. urlib.request will call socket.connect. So this is so that if urlib.request + // was already approved, socket.connect won't trigger another permission dialog. + const std::vector call_site_ids = + PluginAuditDetail::call_site_identities(plugin_descriptor.plugin_root); + if (mgr->has_approved_ancestor(mgr->current_plugin(), call_site_ids)) + return 0; + + // the open function can take in different flags that determine if it is a read or readwrite. + const AuditEventCategory event_type = + event_name == "open" ? PluginAuditDetail::open_category(args) : event_category(event_name); + if (event_type == AuditEventCategory::None) return 0; PluginInstallState state; @@ -494,53 +822,16 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user } const std::string plugin_name = state.plugin_name.empty() ? mgr->current_plugin() : state.plugin_name; - const std::vector paths = PluginAuditDetail::filesystem_paths(event_name, args); - for (const auto& path : paths) { - if (PluginAuditDetail::has_filesystem_permission(state, path)) - continue; - wxMessageDialog dialog( - nullptr, - wxString::Format(_L("Plugin \"%s\" is requesting filesystem access to:\n%s"), - wxString::FromUTF8(plugin_name.c_str()), - wxString::FromUTF8(path.string().c_str())), - _L("Plugin permission request"), - wxYES_NO | wxICON_WARNING); - if (dialog.ShowModal() == wxID_YES && - PluginAuditDetail::persist_filesystem_permission(mgr->current_plugin(), state, path)) - continue; + const std::vector targets = PluginAuditDetail::audit_targets(event_name, event_type, args); + std::vector* permission_list = PluginAuditDetail::permission_list_for(event_type, state.permissions); - return PluginAuditDetail::report_denied( - *mgr, event_name, path, {false, "filesystem permission required"}); - } - - if (!paths.empty()) - return 0; - - wxMessageDialog dialog( - nullptr, - wxString::Format( - _L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\".\n\n" - "This operation does not expose a filesystem path to the audit hook."), - wxString::FromUTF8(plugin_name.c_str()), - wxString::FromUTF8(event_name.c_str())), - _L("Plugin permission request"), - wxYES_NO | wxICON_WARNING); - if (dialog.ShowModal() == wxID_YES) - return 0; - - return PluginAuditDetail::report_denied( - *mgr, event_name, boost::filesystem::path(), {false, "audit permission required"}); + return PluginAuditDetail::decide_audited_event(*mgr, state, mgr->current_plugin(), plugin_name, event_name, + event_type, targets, permission_list, call_site_ids); } void PluginAuditManager::install_hook() { - if (PySys_AddAuditHook(audit_hook, this) < 0) { - BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install CPython audit hook"; - return; - } - BOOST_LOG_TRIVIAL(info) << "[AUDIT] CPython audit hook installed successfully"; - // data_dir() is the only globally-allowed root during plugin execution. // The executable directory and resources directory are intentionally NOT allowed // here: plugins must not access outside data_dir() (G-code plugins additionally get @@ -558,6 +849,12 @@ void PluginAuditManager::install_hook() // (see its comment for why all four config names are denied); the tests seed from it too. for (const auto& name : default_denied_filenames()) add_denied_filename(name); + + if (PySys_AddAuditHook(audit_hook, this) < 0) { + BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install CPython audit hook"; + return; + } + BOOST_LOG_TRIVIAL(info) << "[AUDIT] CPython audit hook installed successfully"; } } // namespace Slic3r diff --git a/src/slic3r/plugin/PluginAuditManager.hpp b/src/slic3r/plugin/PluginAuditManager.hpp index f3fb43d20a..3b86014d42 100644 --- a/src/slic3r/plugin/PluginAuditManager.hpp +++ b/src/slic3r/plugin/PluginAuditManager.hpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include @@ -19,10 +21,23 @@ struct AuditDecision { struct AuditViolation { std::string plugin_key; std::string event_name; - boost::filesystem::path path; std::string reason; }; +// The set of CPython audit events PluginAuditManager recognizes, grouped by the kind of +// operation they represent. None means the event isn't one audit_hook() acts on at all. +enum class AuditEventCategory { + None, + FsRead, + FsReadWrite, + FsCreate, + FsDelete, + Http, + Socket, + ProcessCreate, + Threading, +}; + // Returns true if candidate resolves to a path inside allowed_root. // Uses weakly_canonical and component-wise comparison to reject traversal attacks. bool is_inside_allowed_root(const boost::filesystem::path& candidate, @@ -94,6 +109,16 @@ public: void clear_last_violation(); bool last_violation(AuditViolation& violation) const; + // --- call-site cascade cache --- + // A single plugin action often fires several nested CPython audit events as it passes + // through stdlib layers (urllib.request calling http.client calling socket, for example). + // Once the user approves one event, every stdlib frame still on the stack for that call + // is recorded here by (filename, function, first line) identity. A later event whose own + // ancestor chain still contains one of those frames is the same logical action seen from + // a deeper layer, so it is auto-approved instead of prompting again. + bool has_approved_ancestor(const std::string& plugin_key, const std::vector& call_site_ids) const; + void record_approved_call_sites(const std::string& plugin_key, const std::vector& call_site_ids); + bool verbose_events = true; private: @@ -110,10 +135,11 @@ private: static thread_local bool m_has_last_violation; static thread_local AuditViolation m_last_violation; - // mutable: is_denied_filename() is a const query that must lock. + // mutable: is_denied_filename() and has_approved_ancestor() are const queries that must lock. mutable std::mutex m_mutex; std::vector m_global_allowed_roots; std::vector m_denied_filenames; + std::unordered_map> m_approved_call_sites; // plugin_key -> call-site ids }; // RAII guard that sets the current plugin key and capability name, restoring the previous diff --git a/src/slic3r/plugin/PluginFsUtils.cpp b/src/slic3r/plugin/PluginFsUtils.cpp index c152ae165e..3e014eaec7 100644 --- a/src/slic3r/plugin/PluginFsUtils.cpp +++ b/src/slic3r/plugin/PluginFsUtils.cpp @@ -788,18 +788,18 @@ bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstall if (state.contains("permissions") && state["permissions"].is_object()) { const auto& permissions = state["permissions"]; - if (permissions.contains("networking") && permissions["networking"].is_array()) - for (const auto& host : permissions["networking"]) - if (host.is_string()) - parsed.permissions.networking.push_back(host.get()); - if (permissions.contains("fs_read") && permissions["fs_read"].is_array()) - for (const auto& path : permissions["fs_read"]) - if (path.is_string()) - parsed.permissions.fs_read.push_back(path.get()); - if (permissions.contains("fs_write") && permissions["fs_write"].is_array()) - for (const auto& path : permissions["fs_write"]) - if (path.is_string()) - parsed.permissions.fs_write.push_back(path.get()); + auto read_string_list = [&permissions](const char* key, std::vector& out) { + if (!permissions.contains(key) || !permissions[key].is_array()) + return; + for (const auto& entry : permissions[key]) + if (entry.is_string()) + out.push_back(entry.get()); + }; + read_string_list("fs_read", parsed.permissions.fs_read); + read_string_list("fs_readwrite", parsed.permissions.fs_readwrite); + read_string_list("network_http", parsed.permissions.network_http); + read_string_list("network_socket", parsed.permissions.network_socket); + read_string_list("process", parsed.permissions.process); } if (state.contains("enabled") && state["enabled"].is_boolean()) @@ -838,9 +838,11 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin json["cloud_uuid"] = state.cloud_uuid; json["permissions"] = { - {"networking", state.permissions.networking}, {"fs_read", state.permissions.fs_read}, - {"fs_write", state.permissions.fs_write}, + {"fs_readwrite", state.permissions.fs_readwrite}, + {"network_http", state.permissions.network_http}, + {"network_socket", state.permissions.network_socket}, + {"process", state.permissions.process}, }; nlohmann::json capabilities = nlohmann::json::array(); diff --git a/src/slic3r/plugin/PluginFsUtils.hpp b/src/slic3r/plugin/PluginFsUtils.hpp index be7f5bb551..980d05144e 100644 --- a/src/slic3r/plugin/PluginFsUtils.hpp +++ b/src/slic3r/plugin/PluginFsUtils.hpp @@ -83,9 +83,11 @@ inline nlohmann::json py_to_json(const pybind11::handle& o) struct PluginPermissions { - std::vector networking; std::vector fs_read; - std::vector fs_write; + std::vector fs_readwrite; + std::vector network_http; + std::vector network_socket; + std::vector process; }; struct PluginInstallState { diff --git a/src/slic3r/plugin/PythonInterpreter.cpp b/src/slic3r/plugin/PythonInterpreter.cpp index 299a2dd7ba..3d180c2f2a 100644 --- a/src/slic3r/plugin/PythonInterpreter.cpp +++ b/src/slic3r/plugin/PythonInterpreter.cpp @@ -628,10 +628,6 @@ bool PythonInterpreter::initialize() else BOOST_LOG_TRIVIAL(info) << "Bundled uv executable not found"; - // Install the CPython audit hook for plugin policy enforcement. - // This is defense-in-depth: today it only inspects the `open` audit event - // and blocks writes outside the allowed roots; subprocess/socket/ctypes and - // other events are not yet handled. It is NOT a full security sandbox. PluginAuditManager::instance().install_hook(); // Persist Python stderr (plugin tracebacks, including uncaught diff --git a/tests/slic3rutils/test_plugin_install.cpp b/tests/slic3rutils/test_plugin_install.cpp index c6ad670ff7..ae16703fa6 100644 --- a/tests/slic3rutils/test_plugin_install.cpp +++ b/tests/slic3rutils/test_plugin_install.cpp @@ -112,17 +112,21 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins REQUIRE(read_install_state(plugin_dir, state)); CHECK(state.installed_version == "1.2.0"); - state.permissions.networking = {"api.example.com", "192.168.45.6"}; - state.permissions.fs_read = {"/path/to/read"}; - state.permissions.fs_write = {"/path/to/write"}; + state.permissions.fs_read = {"/path/to/read"}; + state.permissions.fs_readwrite = {"/path/to/readwrite"}; + state.permissions.network_http = {"https://api.example.com"}; + state.permissions.network_socket = {"192.168.45.6:443"}; + state.permissions.process = {"/usr/bin/curl"}; REQUIRE(write_install_state(plugin_dir, state)); // Permission data is persisted in the same sidecar as the installation metadata. PluginInstallState persisted; REQUIRE(read_install_state(plugin_dir, persisted)); - CHECK(persisted.permissions.networking == state.permissions.networking); CHECK(persisted.permissions.fs_read == state.permissions.fs_read); - CHECK(persisted.permissions.fs_write == state.permissions.fs_write); + CHECK(persisted.permissions.fs_readwrite == state.permissions.fs_readwrite); + CHECK(persisted.permissions.network_http == state.permissions.network_http); + CHECK(persisted.permissions.network_socket == state.permissions.network_socket); + CHECK(persisted.permissions.process == state.permissions.process); // Reading the sidecar back onto a freshly-scanned descriptor (whose header version is still // 1.0.0) must surface the cloud-installed 1.2.0. This is what lets update_cloud_metadata compare