mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-27 11:59:27 +00:00
add audit hook for fs, network and processes
This commit is contained in:
@@ -14,12 +14,103 @@
|
|||||||
#include <slic3r/GUI/GUI_App.hpp>
|
#include <slic3r/GUI/GUI_App.hpp>
|
||||||
#include <slic3r/plugin/PluginFsUtils.hpp>
|
#include <slic3r/plugin/PluginFsUtils.hpp>
|
||||||
#include <slic3r/plugin/PluginManager.hpp>
|
#include <slic3r/plugin/PluginManager.hpp>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
#include <wx/event.h>
|
#include <wx/event.h>
|
||||||
#include <wx/msgdlg.h>
|
#include <wx/msgdlg.h>
|
||||||
|
|
||||||
namespace Slic3r {
|
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<std::string, AuditEventCategory> 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
|
// Path safety
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -344,7 +435,7 @@ void PluginAuditManager::report_violation(const AuditViolation& violation)
|
|||||||
m_audit_denial_pending = true;
|
m_audit_denial_pending = true;
|
||||||
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "[AUDIT BLOCKED] plugin=" << violation.plugin_key << " event=" << violation.event_name
|
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; }
|
bool PluginAuditManager::audit_denial_pending() const { return m_audit_denial_pending; }
|
||||||
@@ -366,6 +457,34 @@ bool PluginAuditManager::last_violation(AuditViolation& violation) const
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PluginAuditManager::has_approved_ancestor(const std::string& plugin_key,
|
||||||
|
const std::vector<std::string>& call_site_ids) const
|
||||||
|
{
|
||||||
|
if (plugin_key.empty() || call_site_ids.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> 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<std::string>& call_site_ids)
|
||||||
|
{
|
||||||
|
if (plugin_key.empty() || call_site_ids.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> 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
|
// The C-level audit hook
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -398,38 +517,163 @@ std::string python_path(PyObject* object)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void add_filesystem_path(std::vector<boost::filesystem::path>& paths, PyObject* object)
|
std::string python_unicode(PyObject* object)
|
||||||
{
|
{
|
||||||
const std::string path = python_path(object);
|
if (!object || !PyUnicode_Check(object))
|
||||||
if (!path.empty())
|
return {};
|
||||||
paths.emplace_back(path);
|
const char* text = PyUnicode_AsUTF8(object);
|
||||||
|
if (!text) {
|
||||||
|
PyErr_Clear();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<boost::filesystem::path> filesystem_paths(const std::string& event_name, PyObject* args)
|
std::string python_str(PyObject* object)
|
||||||
{
|
{
|
||||||
std::vector<boost::filesystem::path> paths;
|
if (!object)
|
||||||
|
return {};
|
||||||
|
|
||||||
if (event_name == "open") {
|
PyObject* str_object = PyObject_Str(object);
|
||||||
add_filesystem_path(paths, tuple_item(args, 0));
|
if (!str_object) {
|
||||||
} else if (event_name == "os.rename") {
|
PyErr_Clear();
|
||||||
add_filesystem_path(paths, tuple_item(args, 0));
|
return {};
|
||||||
add_filesystem_path(paths, tuple_item(args, 1));
|
|
||||||
} else if (event_name == "os.remove") {
|
|
||||||
add_filesystem_path(paths, tuple_item(args, 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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<std::string> call_site_identities(const std::string& plugin_root)
|
||||||
{
|
{
|
||||||
return std::find(state.permissions.fs_read.begin(), state.permissions.fs_read.end(), path.string()) !=
|
std::vector<std::string> ids;
|
||||||
state.permissions.fs_read.end();
|
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<PyObject*>(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<PyObject*>(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,
|
static const std::unordered_set<std::string> two_path_fs_events{
|
||||||
PluginInstallState& state,
|
"os.rename", "os.link", "os.symlink", "shutil.copyfile",
|
||||||
const boost::filesystem::path& path)
|
"shutil.copytree", "shutil.copymode", "shutil.copystat", "shutil.move",
|
||||||
|
"shutil.unpack_archive", "_winapi.CreateJunction",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const std::unordered_map<std::string, std::vector<Py_ssize_t>> 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<std::string> audit_targets(const std::string& event_name, AuditEventCategory category, PyObject* args)
|
||||||
|
{
|
||||||
|
std::vector<std::string> 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<std::string>* 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<std::string>& 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<std::string>& permission_list,
|
||||||
|
const std::string& target)
|
||||||
{
|
{
|
||||||
PluginDescriptor descriptor;
|
PluginDescriptor descriptor;
|
||||||
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
|
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;
|
state.enabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string path_string = path.string();
|
if (!has_permission(permission_list, target))
|
||||||
if (std::find(state.permissions.fs_read.begin(), state.permissions.fs_read.end(), path_string) ==
|
permission_list.push_back(target);
|
||||||
state.permissions.fs_read.end())
|
|
||||||
state.permissions.fs_read.push_back(path_string);
|
|
||||||
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
|
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,
|
int report_denied(PluginAuditManager& mgr,
|
||||||
const std::string& event_name,
|
const std::string& event_name,
|
||||||
const boost::filesystem::path& target,
|
|
||||||
const AuditDecision& decision)
|
const AuditDecision& decision)
|
||||||
{
|
{
|
||||||
AuditViolation violation;
|
AuditViolation violation;
|
||||||
violation.plugin_key = mgr.current_plugin();
|
violation.plugin_key = mgr.current_plugin();
|
||||||
violation.event_name = event_name;
|
violation.event_name = event_name;
|
||||||
violation.path = target;
|
|
||||||
violation.reason = decision.reason;
|
violation.reason = decision.reason;
|
||||||
mgr.report_violation(violation);
|
mgr.report_violation(violation);
|
||||||
|
|
||||||
@@ -469,6 +708,74 @@ int report_denied(PluginAuditManager& mgr,
|
|||||||
return -1;
|
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<std::string>& targets,
|
||||||
|
std::vector<std::string>* permission_list,
|
||||||
|
const std::vector<std::string>& call_site_ids)
|
||||||
|
{
|
||||||
|
std::vector<std::string> 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
|
} // namespace PluginAuditDetail
|
||||||
|
|
||||||
int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data)
|
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 : "");
|
std::string event_name(event ? event : "");
|
||||||
|
|
||||||
|
if (event_name.empty())
|
||||||
|
return 0;
|
||||||
|
|
||||||
// Verbose logging of every audit event (can be noisy)
|
// Verbose logging of every audit event (can be noisy)
|
||||||
if (mgr->verbose_events) {
|
if (mgr->verbose_events) {
|
||||||
BOOST_LOG_TRIVIAL(debug) << "[AUDIT EVENT] " << event_name;
|
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<std::string> 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;
|
return 0;
|
||||||
|
|
||||||
PluginInstallState state;
|
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::string plugin_name = state.plugin_name.empty() ? mgr->current_plugin() : state.plugin_name;
|
||||||
const std::vector<boost::filesystem::path> paths = PluginAuditDetail::filesystem_paths(event_name, args);
|
|
||||||
for (const auto& path : paths) {
|
|
||||||
if (PluginAuditDetail::has_filesystem_permission(state, path))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
wxMessageDialog dialog(
|
const std::vector<std::string> targets = PluginAuditDetail::audit_targets(event_name, event_type, args);
|
||||||
nullptr,
|
std::vector<std::string>* permission_list = PluginAuditDetail::permission_list_for(event_type, state.permissions);
|
||||||
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;
|
|
||||||
|
|
||||||
return PluginAuditDetail::report_denied(
|
return PluginAuditDetail::decide_audited_event(*mgr, state, mgr->current_plugin(), plugin_name, event_name,
|
||||||
*mgr, event_name, path, {false, "filesystem permission required"});
|
event_type, targets, permission_list, call_site_ids);
|
||||||
}
|
|
||||||
|
|
||||||
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"});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PluginAuditManager::install_hook()
|
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.
|
// data_dir() is the only globally-allowed root during plugin execution.
|
||||||
// The executable directory and resources directory are intentionally NOT allowed
|
// The executable directory and resources directory are intentionally NOT allowed
|
||||||
// here: plugins must not access outside data_dir() (G-code plugins additionally get
|
// 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.
|
// (see its comment for why all four config names are denied); the tests seed from it too.
|
||||||
for (const auto& name : default_denied_filenames())
|
for (const auto& name : default_denied_filenames())
|
||||||
add_denied_filename(name);
|
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
|
} // namespace Slic3r
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
@@ -19,10 +21,23 @@ struct AuditDecision {
|
|||||||
struct AuditViolation {
|
struct AuditViolation {
|
||||||
std::string plugin_key;
|
std::string plugin_key;
|
||||||
std::string event_name;
|
std::string event_name;
|
||||||
boost::filesystem::path path;
|
|
||||||
std::string reason;
|
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.
|
// Returns true if candidate resolves to a path inside allowed_root.
|
||||||
// Uses weakly_canonical and component-wise comparison to reject traversal attacks.
|
// Uses weakly_canonical and component-wise comparison to reject traversal attacks.
|
||||||
bool is_inside_allowed_root(const boost::filesystem::path& candidate,
|
bool is_inside_allowed_root(const boost::filesystem::path& candidate,
|
||||||
@@ -94,6 +109,16 @@ public:
|
|||||||
void clear_last_violation();
|
void clear_last_violation();
|
||||||
bool last_violation(AuditViolation& violation) const;
|
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<std::string>& call_site_ids) const;
|
||||||
|
void record_approved_call_sites(const std::string& plugin_key, const std::vector<std::string>& call_site_ids);
|
||||||
|
|
||||||
bool verbose_events = true;
|
bool verbose_events = true;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -110,10 +135,11 @@ private:
|
|||||||
static thread_local bool m_has_last_violation;
|
static thread_local bool m_has_last_violation;
|
||||||
static thread_local AuditViolation m_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;
|
mutable std::mutex m_mutex;
|
||||||
std::vector<boost::filesystem::path> m_global_allowed_roots;
|
std::vector<boost::filesystem::path> m_global_allowed_roots;
|
||||||
std::vector<std::string> m_denied_filenames;
|
std::vector<std::string> m_denied_filenames;
|
||||||
|
std::unordered_map<std::string, std::unordered_set<std::string>> m_approved_call_sites; // plugin_key -> call-site ids
|
||||||
};
|
};
|
||||||
|
|
||||||
// RAII guard that sets the current plugin key and capability name, restoring the previous
|
// RAII guard that sets the current plugin key and capability name, restoring the previous
|
||||||
|
|||||||
@@ -788,18 +788,18 @@ bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstall
|
|||||||
|
|
||||||
if (state.contains("permissions") && state["permissions"].is_object()) {
|
if (state.contains("permissions") && state["permissions"].is_object()) {
|
||||||
const auto& permissions = state["permissions"];
|
const auto& permissions = state["permissions"];
|
||||||
if (permissions.contains("networking") && permissions["networking"].is_array())
|
auto read_string_list = [&permissions](const char* key, std::vector<std::string>& out) {
|
||||||
for (const auto& host : permissions["networking"])
|
if (!permissions.contains(key) || !permissions[key].is_array())
|
||||||
if (host.is_string())
|
return;
|
||||||
parsed.permissions.networking.push_back(host.get<std::string>());
|
for (const auto& entry : permissions[key])
|
||||||
if (permissions.contains("fs_read") && permissions["fs_read"].is_array())
|
if (entry.is_string())
|
||||||
for (const auto& path : permissions["fs_read"])
|
out.push_back(entry.get<std::string>());
|
||||||
if (path.is_string())
|
};
|
||||||
parsed.permissions.fs_read.push_back(path.get<std::string>());
|
read_string_list("fs_read", parsed.permissions.fs_read);
|
||||||
if (permissions.contains("fs_write") && permissions["fs_write"].is_array())
|
read_string_list("fs_readwrite", parsed.permissions.fs_readwrite);
|
||||||
for (const auto& path : permissions["fs_write"])
|
read_string_list("network_http", parsed.permissions.network_http);
|
||||||
if (path.is_string())
|
read_string_list("network_socket", parsed.permissions.network_socket);
|
||||||
parsed.permissions.fs_write.push_back(path.get<std::string>());
|
read_string_list("process", parsed.permissions.process);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.contains("enabled") && state["enabled"].is_boolean())
|
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["cloud_uuid"] = state.cloud_uuid;
|
||||||
|
|
||||||
json["permissions"] = {
|
json["permissions"] = {
|
||||||
{"networking", state.permissions.networking},
|
|
||||||
{"fs_read", state.permissions.fs_read},
|
{"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();
|
nlohmann::json capabilities = nlohmann::json::array();
|
||||||
|
|||||||
@@ -83,9 +83,11 @@ inline nlohmann::json py_to_json(const pybind11::handle& o)
|
|||||||
|
|
||||||
struct PluginPermissions
|
struct PluginPermissions
|
||||||
{
|
{
|
||||||
std::vector<std::string> networking;
|
|
||||||
std::vector<std::string> fs_read;
|
std::vector<std::string> fs_read;
|
||||||
std::vector<std::string> fs_write;
|
std::vector<std::string> fs_readwrite;
|
||||||
|
std::vector<std::string> network_http;
|
||||||
|
std::vector<std::string> network_socket;
|
||||||
|
std::vector<std::string> process;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PluginInstallState {
|
struct PluginInstallState {
|
||||||
|
|||||||
@@ -628,10 +628,6 @@ bool PythonInterpreter::initialize()
|
|||||||
else
|
else
|
||||||
BOOST_LOG_TRIVIAL(info) << "Bundled uv executable not found";
|
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();
|
PluginAuditManager::instance().install_hook();
|
||||||
|
|
||||||
// Persist Python stderr (plugin tracebacks, including uncaught
|
// Persist Python stderr (plugin tracebacks, including uncaught
|
||||||
|
|||||||
@@ -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));
|
REQUIRE(read_install_state(plugin_dir, state));
|
||||||
CHECK(state.installed_version == "1.2.0");
|
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_read = {"/path/to/read"};
|
state.permissions.fs_readwrite = {"/path/to/readwrite"};
|
||||||
state.permissions.fs_write = {"/path/to/write"};
|
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));
|
REQUIRE(write_install_state(plugin_dir, state));
|
||||||
|
|
||||||
// Permission data is persisted in the same sidecar as the installation metadata.
|
// Permission data is persisted in the same sidecar as the installation metadata.
|
||||||
PluginInstallState persisted;
|
PluginInstallState persisted;
|
||||||
REQUIRE(read_install_state(plugin_dir, 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_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
|
// 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
|
// 1.0.0) must surface the cloud-installed 1.2.0. This is what lets update_cloud_metadata compare
|
||||||
|
|||||||
Reference in New Issue
Block a user