Compare commits

..

1 Commits

Author SHA1 Message Date
Hanif Koh
a5f7b8acdd [TEMP] Update Workflow to test new CLI Test Suite in its own branch 2026-08-31 13:12:11 +08:00
24 changed files with 284 additions and 1269 deletions

View File

@@ -26,8 +26,8 @@ jobs:
date: date:
ver: ver:
ver_pure: ver_pure:
ubuntu-ver: '2404' ubuntu-ver: "2404"
ubuntu-ver-str: '_Ubuntu2404' ubuntu-ver-str: "_Ubuntu2404"
ORCA_UPDATER_SIG_KEY: ${{ secrets.ORCA_UPDATER_SIG_KEY }} ORCA_UPDATER_SIG_KEY: ${{ secrets.ORCA_UPDATER_SIG_KEY }}
# Branches whose builds are published to the nightly release. The # Branches whose builds are published to the nightly release. The
# belt-printer branch ships alongside main but its assets carry a `_belt` # belt-printer branch ships alongside main but its assets carry a `_belt`
@@ -39,7 +39,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v7 uses: actions/checkout@v7
with: with:
lfs: 'false' lfs: "false"
- name: load cached deps - name: load cached deps
if: ${{ !(runner.os == 'macOS' && inputs.macos-combine-only) }} if: ${{ !(runner.os == 'macOS' && inputs.macos-combine-only) }}
@@ -120,7 +120,7 @@ jobs:
echo "date: ${{ env.date }} version: ${{ env.ver }}" echo "date: ${{ env.date }} version: ${{ env.ver }}"
shell: pwsh shell: pwsh
# Mac # Mac
- name: Install tools mac - name: Install tools mac
if: runner.os == 'macOS' && !inputs.macos-combine-only if: runner.os == 'macOS' && !inputs.macos-combine-only
run: | run: |
@@ -362,7 +362,7 @@ jobs:
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
max_releases: 1 max_releases: 1
# Windows # Windows
- name: Set Windows build variables - name: Set Windows build variables
if: runner.os == 'Windows' if: runner.os == 'Windows'
shell: pwsh shell: pwsh
@@ -529,7 +529,7 @@ jobs:
name: OrcaSlicer_Windows_MSIX_${{ env.ver }}${{ env.ARCH_SUFFIX }} name: OrcaSlicer_Windows_MSIX_${{ env.ver }}${{ env.ARCH_SUFFIX }}
path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_MSIX_${{ env.ver }}${{ env.ARCH_SUFFIX }}.msix path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_MSIX_${{ env.ver }}${{ env.ARCH_SUFFIX }}.msix
# Ubuntu # Ubuntu
- name: Apt-Install Dependencies - name: Apt-Install Dependencies
if: runner.os == 'Linux' && !vars.SELF_HOSTED if: runner.os == 'Linux' && !vars.SELF_HOSTED
uses: ./.github/actions/apt-install-deps uses: ./.github/actions/apt-install-deps
@@ -596,7 +596,7 @@ jobs:
run: | run: |
test_repo_dir="${{ runner.temp }}/orca-test-repo" test_repo_dir="${{ runner.temp }}/orca-test-repo"
rm -rf "$test_repo_dir" rm -rf "$test_repo_dir"
git clone --depth 1 https://github.com/OrcaSlicer/orca-test-repo.git "$test_repo_dir" git clone --depth 1 --branch cli-test-suite https://github.com/OrcaSlicer/orca-test-repo.git "$test_repo_dir"
python3 "$test_repo_dir/run_test.py" "${{ github.workspace }}/build/package/bin/orca-slicer" python3 "$test_repo_dir/run_test.py" "${{ github.workspace }}/build/package/bin/orca-slicer"
- name: Build orca_custom_preset_tests - name: Build orca_custom_preset_tests
@@ -620,7 +620,7 @@ jobs:
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }} name: OrcaSlicer_profile_validator_Linux_ubuntu_${{ env.ubuntu-ver }}_${{ env.ver }}
path: './build/src/Release/OrcaSlicer_profile_validator' path: "./build/src/Release/OrcaSlicer_profile_validator"
- name: Deploy Ubuntu release - name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }}

View File

@@ -16,27 +16,24 @@ namespace Slic3r
// This block is never executed at runtime. // This block is never executed at runtime.
static void _toolhead_translation_markers() static void _toolhead_translation_markers()
{ {
// Possible runtime values of tool_head_display_names, marked for extraction. // Dynamic toolhead display names from JSON config — xgettext cannot scan these
static const char *const markers[] = { L("Main Extruder"); L("Main extruder"); L("main extruder");
L("Main Extruder"), L("Main extruder"), L("main extruder"), L("Auxiliary Extruder"); L("Auxiliary extruder"); L("auxiliary extruder");
L("Auxiliary Extruder"), L("Auxiliary extruder"), L("auxiliary extruder"), L("Left Extruder"); L("Left extruder"); L("left extruder");
L("Left Extruder"), L("Left extruder"), L("left extruder"), L("Right Extruder"); L("Right extruder"); L("right extruder");
L("Right Extruder"), L("Right extruder"), L("right extruder"), L("Main Nozzle"); L("Main nozzle"); L("main nozzle");
L("Main Nozzle"), L("Main nozzle"), L("main nozzle"), L("Auxiliary Nozzle"); L("Auxiliary nozzle"); L("auxiliary nozzle");
L("Auxiliary Nozzle"), L("Auxiliary nozzle"), L("auxiliary nozzle"), L("Left Nozzle"); L("Left nozzle"); L("left nozzle");
L("Left Nozzle"), L("Left nozzle"), L("left nozzle"), L("Right Nozzle"); L("Right nozzle"); L("right nozzle");
L("Right Nozzle"), L("Right nozzle"), L("right nozzle"), L("Main Hotend"); L("Main hotend"); L("main hotend");
L("Main Hotend"), L("Main hotend"), L("main hotend"), L("Auxiliary Hotend"); L("Auxiliary hotend"); L("auxiliary hotend");
L("Auxiliary Hotend"), L("Auxiliary hotend"), L("auxiliary hotend"), L("Left Hotend"); L("Left hotend"); L("left hotend");
L("Left Hotend"), L("Left hotend"), L("left hotend"), L("Right Hotend"); L("Right hotend"); L("right hotend");
L("Right Hotend"), L("Right hotend"), L("right hotend"),
// standalone position words (short_name=true runtime results) // standalone position words (short_name=true runtime results)
L("main"), L("auxiliary"), L("main"); L("auxiliary");
L("Main"), L("Auxiliary"), L("Main"); L("Auxiliary");
L("left"), L("right"), L("left"); L("right");
L("Left"), L("Right"), L("Left"); L("Right");
};
(void) markers;
} }
std::string DevPrinterConfigUtil::m_resource_file_path = ""; std::string DevPrinterConfigUtil::m_resource_file_path = "";

View File

@@ -266,7 +266,7 @@ void wgtDeviceNozzleRackSelect::OnNozzleItemSelected(wxCommandEvent &evt)
} }
auto *item = dynamic_cast<wgtDeviceNozzleRackNozzleItem *>(evt.GetEventObject()); auto *item = dynamic_cast<wgtDeviceNozzleRackNozzleItem *>(evt.GetEventObject());
if (auto ptr = m_nozzle_rack.lock(); item && ptr) { if (item; auto ptr = m_nozzle_rack.lock()) {
int to_select_pos_id = sGetNozzlePosId(item, m_toolhead_nozzle_l, m_toolhead_nozzle_r); int to_select_pos_id = sGetNozzlePosId(item, m_toolhead_nozzle_l, m_toolhead_nozzle_r);
if (to_select_pos_id > -1 && to_select_pos_id != GetSelectedNozzlePosID()) { if (to_select_pos_id > -1 && to_select_pos_id != GetSelectedNozzlePosID()) {
SetSelectedNozzle(ptr->GetNozzleSystem()->GetNozzleByPosId(to_select_pos_id)); SetSelectedNozzle(ptr->GetNozzleSystem()->GetNozzleByPosId(to_select_pos_id));

View File

@@ -862,6 +862,7 @@ void MachineInfoPanel::update_ams_ext(MachineObject *obj)
if (new_extra_ams_ver != obj->new_ver_list.end()) if (new_extra_ams_ver != obj->new_ver_list.end())
has_new_version = true; has_new_version = true;
extra_ams_it->second.sw_new_ver;
if (has_new_version) { if (has_new_version) {
m_extra_ams_panel->m_ams_new_version_img->Show(); m_extra_ams_panel->m_ams_new_version_img->Show();
ver_text = new_extra_ams_ver->second.sw_ver; ver_text = new_extra_ams_ver->second.sw_ver;

View File

@@ -114,6 +114,7 @@ bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag,
info.ctype = 0; info.ctype = 0;
info.material_colour = AMS_TRAY_DEFAULT_COL; info.material_colour = AMS_TRAY_DEFAULT_COL;
info.material_state = AMSCanType::AMS_CAN_TYPE_THIRDBRAND; info.material_state = AMSCanType::AMS_CAN_TYPE_THIRDBRAND;
wxColour(255, 255, 255);
} }
if (it->second->is_tray_info_ready() && obj->cali_version >= 0) { if (it->second->is_tray_info_ready() && obj->cali_version >= 0) {
@@ -170,6 +171,7 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) {
info.filament_id = ""; info.filament_id = "";
info.ctype = 0; info.ctype = 0;
info.material_colour = AMS_TRAY_DEFAULT_COL; info.material_colour = AMS_TRAY_DEFAULT_COL;
wxColour(255, 255, 255);
} }
info.material_state = AMSCanType::AMS_CAN_TYPE_VIRTUAL; info.material_state = AMSCanType::AMS_CAN_TYPE_VIRTUAL;
if (tray.is_tray_info_ready() && obj->cali_version >= 0) { if (tray.is_tray_info_ready() && obj->cali_version >= 0) {

View File

@@ -1014,12 +1014,8 @@ void FanControlPopupNew::init_names(MachineObject* obj) {
if (obj) { if (obj) {
const std::string& special_cooling_text = DevPrinterConfigUtil::get_fan_text(obj->printer_type, "special_cooling_text"); const std::string& special_cooling_text = DevPrinterConfigUtil::get_fan_text(obj->printer_type, "special_cooling_text");
if (!special_cooling_text.empty()) { if (!special_cooling_text.empty()) {
// Possible runtime values of special_cooling_text, marked for extraction. L("Cooling mode is suitable for printing PLA/PETG/TPU materials."); //some potential text, add i18n flags
static const char *const markers[] = { L("Cooling mode is suitable for printing PLA/PETG/TPU materials and filters the chamber air.");
L("Cooling mode is suitable for printing PLA/PETG/TPU materials."),
L("Cooling mode is suitable for printing PLA/PETG/TPU materials and filters the chamber air."),
};
(void) markers;
label_text[AIR_DUCT::AIR_DUCT_COOLING_FILT] = _L(special_cooling_text); label_text[AIR_DUCT::AIR_DUCT_COOLING_FILT] = _L(special_cooling_text);
} }
} }
@@ -1032,13 +1028,9 @@ wxString FanControlPopupNew::get_fan_func_name(int mode, int submode, AIR_FUN fu
const std::string& func_text = DevPrinterConfigUtil::get_fan_text(m_obj->printer_type, mode, (int)func, submode); const std::string& func_text = DevPrinterConfigUtil::get_fan_text(m_obj->printer_type, mode, (int)func, submode);
if (!func_text.empty()) if (!func_text.empty())
{ {
// Possible runtime values of func_text, marked for extraction. L_CONTEXT("Right(Aux)", "air_duct");
static const char *const markers[] = { L_CONTEXT("Right(Filter)", "air_duct");
L_CONTEXT("Right(Aux)", "air_duct"), L_CONTEXT("Left(Aux)", "air_duct");
L_CONTEXT("Right(Filter)", "air_duct"),
L_CONTEXT("Left(Aux)", "air_duct"),
};
(void) markers;
return _L_CONTEXT(func_text, "air_duct"); return _L_CONTEXT(func_text, "air_duct");
} }
} }

View File

@@ -7,127 +7,11 @@
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <algorithm>
#include <cctype>
#include <cstdlib> #include <cstdlib>
#include <future>
#include <slic3r/GUI/BindDialog.hpp>
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/plugin/PluginFsUtils.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <unordered_map>
#include <unordered_set>
#include <utility> #include <utility>
#include <vector>
#include <wx/event.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;
}
// True for the categories whose targets are filesystem paths, as opposed to a network
// address or a process command line -- the deny-path and allowed-root checks only make sense
// against a path.
static bool is_fs_category(AuditEventCategory category)
{
switch (category) {
case AuditEventCategory::FsRead:
case AuditEventCategory::FsReadWrite:
case AuditEventCategory::FsCreate:
case AuditEventCategory::FsDelete:
return true;
default:
return false;
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Path safety // Path safety
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -196,19 +80,22 @@ bool is_inside_allowed_root(const boost::filesystem::path& candidate, const boos
thread_local std::string PluginAuditManager::m_current_plugin_key = ""; thread_local std::string PluginAuditManager::m_current_plugin_key = "";
thread_local std::string PluginAuditManager::m_current_capability_name = ""; thread_local std::string PluginAuditManager::m_current_capability_name = "";
thread_local std::vector<AllowedRoot> PluginAuditManager::m_scoped_allowed_roots; thread_local PluginAuditManager::AuditMode PluginAuditManager::m_audit_mode = PluginAuditManager::AuditMode::Loading;
thread_local bool PluginAuditManager::m_audit_denial_pending = false; thread_local std::vector<boost::filesystem::path> PluginAuditManager::m_scoped_allowed_roots;
thread_local bool PluginAuditManager::m_has_last_violation = false; thread_local bool PluginAuditManager::m_has_last_violation = false;
thread_local AuditViolation PluginAuditManager::m_last_violation; thread_local AuditViolation PluginAuditManager::m_last_violation;
ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key, ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key,
const std::string& capability_name) const std::string& capability_name,
PluginAuditManager::AuditMode mode)
: m_previous_id(PluginAuditManager::instance().current_plugin()) : m_previous_id(PluginAuditManager::instance().current_plugin())
, m_previous_capability(PluginAuditManager::instance().current_capability()) , m_previous_capability(PluginAuditManager::instance().current_capability())
, m_previous_mode(PluginAuditManager::instance().audit_mode())
, m_previous_scoped_roots(PluginAuditManager::m_scoped_allowed_roots) , m_previous_scoped_roots(PluginAuditManager::m_scoped_allowed_roots)
{ {
PluginAuditManager::instance().set_current_plugin(plugin_key); PluginAuditManager::instance().set_current_plugin(plugin_key);
PluginAuditManager::instance().set_current_capability(capability_name); PluginAuditManager::instance().set_current_capability(capability_name);
PluginAuditManager::instance().set_audit_mode(mode);
PluginAuditManager::m_scoped_allowed_roots.clear(); PluginAuditManager::m_scoped_allowed_roots.clear();
} }
@@ -216,6 +103,7 @@ ScopedPluginAuditContext::~ScopedPluginAuditContext()
{ {
PluginAuditManager::instance().set_current_plugin(m_previous_id); PluginAuditManager::instance().set_current_plugin(m_previous_id);
PluginAuditManager::instance().set_current_capability(m_previous_capability); PluginAuditManager::instance().set_current_capability(m_previous_capability);
PluginAuditManager::instance().set_audit_mode(m_previous_mode);
PluginAuditManager::m_scoped_allowed_roots = std::move(m_previous_scoped_roots); PluginAuditManager::m_scoped_allowed_roots = std::move(m_previous_scoped_roots);
} }
@@ -241,24 +129,23 @@ std::string PluginAuditManager::current_capability() const { return m_current_ca
void PluginAuditManager::clear_current_capability() { m_current_capability_name.clear(); } void PluginAuditManager::clear_current_capability() { m_current_capability_name.clear(); }
void PluginAuditManager::add_global_allowed_root(const boost::filesystem::path& root, bool allow_write) void PluginAuditManager::add_global_allowed_root(const boost::filesystem::path& root)
{ {
if (root.empty()) if (root.empty())
return; return;
std::lock_guard<std::mutex> lock(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
m_global_allowed_roots.push_back({root, allow_write}); m_global_allowed_roots.push_back(root);
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Global allowed root: " << root.string() << " allow_write=" << allow_write; BOOST_LOG_TRIVIAL(info) << "[AUDIT] Global allowed root: " << root.string();
} }
void PluginAuditManager::add_scoped_allowed_root(const boost::filesystem::path& root, bool allow_write) void PluginAuditManager::add_scoped_allowed_root(const boost::filesystem::path& root)
{ {
if (root.empty()) if (root.empty())
return; return;
m_scoped_allowed_roots.push_back({root, allow_write}); m_scoped_allowed_roots.push_back(root);
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Scoped allowed root for plugin " << current_plugin() << ": " << root.string() BOOST_LOG_TRIVIAL(info) << "[AUDIT] Scoped allowed root for plugin " << current_plugin() << ": " << root.string();
<< " allow_write=" << allow_write;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -312,65 +199,12 @@ bool PluginAuditManager::is_denied_filename(const boost::filesystem::path& candi
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Denied path keywords // Audit mode
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void PluginAuditManager::add_denied_path_keyword(const std::string& keyword) void PluginAuditManager::set_audit_mode(AuditMode mode) { m_audit_mode = mode; }
{
if (keyword.empty())
return;
std::string lower = keyword; PluginAuditManager::AuditMode PluginAuditManager::audit_mode() const { return m_audit_mode; }
std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return std::tolower(c); });
std::lock_guard<std::mutex> lock(m_mutex);
m_denied_path_keywords.push_back(lower);
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Denied path keyword: " << lower;
}
std::vector<std::string> PluginAuditManager::default_denied_path_keywords()
{
// Broad, categorical rules on top of the exact-name is_denied_filename registry: a plugin
// must never be able to reach a secret, a certificate, or a configuration file just because
// it happens to live inside an otherwise-allowed root (e.g. the bundled TLS client cert at
// resources_dir()/cert/..., which would become reachable the moment resources_dir() is
// granted as a read-only allowed root).
return {"secret", "cert", "conf"};
}
bool PluginAuditManager::is_denied_path_keyword(const boost::filesystem::path& candidate) const
{
namespace fs = boost::filesystem;
boost::system::error_code ec;
fs::path canon = fs::weakly_canonical(candidate, ec);
if (ec) {
canon = fs::absolute(candidate, ec).lexically_normal();
if (ec)
canon = candidate;
}
std::lock_guard<std::mutex> lock(m_mutex);
if (m_denied_path_keywords.empty())
return false;
for (const auto& component : canon) {
std::string name = component.string();
if (name.empty())
continue;
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return std::tolower(c); });
for (const auto& keyword : m_denied_path_keywords) {
if (name.find(keyword) != std::string::npos)
return true;
}
}
return false;
}
bool PluginAuditManager::is_denied_path(const boost::filesystem::path& candidate) const
{
return is_denied_filename(candidate) || is_denied_path_keyword(candidate);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Policy checks // Policy checks
@@ -385,20 +219,20 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
if (plugin_key.empty()) if (plugin_key.empty())
return {true, ""}; // not running inside a plugin context return {true, ""}; // not running inside a plugin context
// Denied filenames/keywords are checked before the allowed roots. The app config and the // Denied filenames are checked first, above both the Loading exemption below and the
// cloud refresh token live directly inside data_dir(), which is a global allowed root, and // allowed roots. The app config and the cloud refresh token live directly inside
// the bundled TLS client cert lives inside resources_dir(), a read-only global allowed // data_dir(), which is a global allowed root, and no scope ever sets Enforcing — so a
// root, so a deny placed any lower would be unreachable. // deny placed any lower would be unreachable for reads.
if (is_denied_filename(path)) { if (is_denied_filename(path)) {
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << path.string() << " is_write=" << is_write BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << path.string() << " is_write=" << is_write
<< " plugin=" << plugin_key << " reason=denied filename"; << " plugin=" << plugin_key << " reason=denied filename";
return {false, "denied filename"}; return {false, "denied filename"};
} }
if (is_denied_path_keyword(path)) {
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << path.string() << " is_write=" << is_write // During import/loading, only block writes. Python must be able to read
<< " plugin=" << plugin_key << " reason=denied path keyword"; // stdlib modules and the plugin file itself during import.
return {false, "denied path keyword"}; if (m_audit_mode == AuditMode::Loading && !is_write)
} return {true, ""};
namespace fs = boost::filesystem; namespace fs = boost::filesystem;
fs::path candidate = path; fs::path candidate = path;
@@ -411,11 +245,8 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
candidate = absolute_candidate; candidate = absolute_candidate;
} }
// A root that doesn't allow writes only matches a read-shaped request; a write/create/
// delete-shaped one falls through to "outside allowed root" for that root even though the
// path is physically inside it.
for (const auto& root : m_scoped_allowed_roots) { for (const auto& root : m_scoped_allowed_roots) {
if ((!is_write || root.allow_write) && is_inside_allowed_root(candidate, root.path)) { if (is_inside_allowed_root(candidate, root)) {
return {true, ""}; return {true, ""};
} }
} }
@@ -423,13 +254,14 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
{ {
std::lock_guard<std::mutex> lock(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
for (const auto& root : m_global_allowed_roots) { for (const auto& root : m_global_allowed_roots) {
if ((!is_write || root.allow_write) && is_inside_allowed_root(candidate, root.path)) { if (is_inside_allowed_root(candidate, root)) {
return {true, ""}; return {true, ""};
} }
} }
} }
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << candidate.string() << " is_write=" << is_write BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << candidate.string() << " is_write=" << is_write
<< " audit_mode=" << (m_audit_mode == AuditMode::Loading ? "Loading" : "Enforcing")
<< " plugin=" << plugin_key; << " plugin=" << plugin_key;
return {false, "outside allowed root"}; return {false, "outside allowed root"};
} }
@@ -437,99 +269,19 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
AuditDecision PluginAuditManager::check_open(const std::string& path_str, const std::string& mode) AuditDecision PluginAuditManager::check_open(const std::string& path_str, const std::string& mode)
{ {
const bool is_write = mode.find('w') != std::string::npos || mode.find('a') != std::string::npos || const bool is_write = mode.find('w') != std::string::npos || mode.find('a') != std::string::npos ||
mode.find('+') != std::string::npos || mode.find('x') != std::string::npos; mode.find('+') != std::string::npos;
return check_path_access(boost::filesystem::path(path_str), is_write); return check_path_access(boost::filesystem::path(path_str), is_write);
} }
bool PluginAuditManager::request_filesystem_read_permissions(const std::string& plugin_key,
const std::vector<std::string>& paths)
{
if (plugin_key.empty() || paths.empty())
return true;
PluginDescriptor descriptor;
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return false;
PluginInstallState state;
read_install_state(boost::filesystem::path(descriptor.plugin_root), state);
std::vector<std::string> missing;
for (const std::string& path : paths) {
if (std::find(state.permissions.fs_read.begin(), state.permissions.fs_read.end(), path) == state.permissions.fs_read.end())
missing.push_back(path);
}
if (missing.empty())
return true;
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return false;
auto show_dialog = [&descriptor, &missing]() {
wxString requested_paths;
for (const std::string& path : missing)
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
wxMessageDialog dialog(
nullptr,
wxString::Format("Plugin \"%s\" requests filesystem read access to:\n%s",
wxString::FromUTF8(descriptor.name.c_str()), requested_paths),
"Plugin permissions",
wxYES_NO | wxICON_WARNING);
return dialog.ShowModal() == wxID_YES;
};
bool granted = false;
if (wxIsMainThread()) {
granted = show_dialog();
} else {
auto result = std::make_shared<std::promise<bool>>();
auto future = result->get_future();
GUI::wxGetApp().CallAfter([result, descriptor_name = descriptor.name, missing]() {
wxString requested_paths;
for (const std::string& path : missing)
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
wxMessageDialog dialog(
nullptr,
wxString::Format("Plugin \"%s\" requests filesystem read access to:\n%s",
wxString::FromUTF8(descriptor_name.c_str()), requested_paths),
"Plugin permissions",
wxYES_NO | wxICON_WARNING);
result->set_value(dialog.ShowModal() == wxID_YES);
});
granted = future.get();
}
if (!granted)
return false;
if (state.plugin_name.empty()) {
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
state.plugin_name = descriptor.name;
state.cloud_uuid = descriptor.cloud_uuid();
state.enabled = true;
}
state.permissions.fs_read.insert(state.permissions.fs_read.end(), missing.begin(), missing.end());
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
}
void PluginAuditManager::report_violation(const AuditViolation& violation) void PluginAuditManager::report_violation(const AuditViolation& violation)
{ {
m_last_violation = violation; m_last_violation = violation;
m_has_last_violation = true; m_has_last_violation = 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
<< " reason=" << violation.reason; << " path=" << violation.path.string() << " reason=" << violation.reason;
} }
bool PluginAuditManager::audit_denial_pending() const { return m_audit_denial_pending; }
void PluginAuditManager::clear_audit_denial() { m_audit_denial_pending = false; }
void PluginAuditManager::clear_last_violation() void PluginAuditManager::clear_last_violation()
{ {
m_has_last_violation = false; m_has_last_violation = false;
@@ -545,326 +297,31 @@ 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
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
namespace PluginAuditDetail { namespace {
PyObject* tuple_item(PyObject* args, Py_ssize_t index)
{
if (!args || !PyTuple_Check(args) || index < 0 || index >= PyTuple_GET_SIZE(args))
return nullptr;
return PyTuple_GET_ITEM(args, index);
}
std::string python_path(PyObject* object)
{
if (!object)
return {};
PyObject* path_object = PyOS_FSPath(object);
if (!path_object) {
PyErr_Clear();
return {};
}
const char* path = PyUnicode_Check(path_object) ? PyUnicode_AsUTF8(path_object) : PyBytes_AsString(path_object);
std::string result = path ? path : "";
Py_DECREF(path_object);
if (!path)
PyErr_Clear();
return result;
}
std::string python_unicode(PyObject* object)
{
if (!object || !PyUnicode_Check(object))
return {};
const char* text = PyUnicode_AsUTF8(object);
if (!text) {
PyErr_Clear();
return {};
}
return text;
}
std::string python_str(PyObject* object)
{
if (!object)
return {};
PyObject* str_object = PyObject_Str(object);
if (!str_object) {
PyErr_Clear();
return {};
}
const std::string result = python_unicode(str_object);
Py_DECREF(str_object);
return result;
}
std::vector<std::string> call_site_identities(const std::string& plugin_root)
{
std::vector<std::string> 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<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;
}
static const std::unordered_set<std::string> 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<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;
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return false;
// A sandbox plugin may not have a sidecar yet. Seed the small amount of install metadata
// needed by the writer so clicking Yes still creates the JSON file.
if (state.plugin_name.empty()) {
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
state.plugin_name = descriptor.name;
state.cloud_uuid = descriptor.cloud_uuid();
state.enabled = true;
}
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. Returns -1
// so an event branch can `return report_denied(...)` directly.
int report_denied(PluginAuditManager& mgr, int report_denied(PluginAuditManager& mgr,
const std::string& event_name, const std::string& event_name,
const boost::filesystem::path& path,
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 = path;
violation.reason = decision.reason; violation.reason = decision.reason;
mgr.report_violation(violation); mgr.report_violation(violation);
PyErr_SetString(PyExc_PermissionError, "Plugin attempted an audited operation without permission"); PyErr_SetString(PyExc_PermissionError, "Plugin attempted to access a blocked file path");
return -1; return -1;
} }
wxString audit_message(AuditEventCategory category, const wxString& plugin_name, const wxString& event_name, } // namespace
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
int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data) int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data)
{ {
@@ -874,94 +331,93 @@ 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()) { // extensive list of audit events can be found at https://docs.python.org/3/library/audit_events.html
BOOST_LOG_TRIVIAL(trace) << "[AUDIT] event=" << event_name << " bypassed (no plugin context)";
// --- open event ---
if (event_name == "open") {
const char* path_cstr = nullptr;
const char* mode_cstr = nullptr;
int flags = 0;
// open(path, mode, flags) — path may be str, bytes, or int fd
if (!PyArg_ParseTuple(args, "s|si", &path_cstr, &mode_cstr, &flags)) {
PyErr_Clear(); // couldn't parse; allow
return 0; return 0;
} }
// the open function can take in different flags that determine if it is a read or readwrite. std::string path_str(path_cstr ? path_cstr : "");
const AuditEventCategory event_type = std::string mode_str(mode_cstr ? mode_cstr : "r");
event_name == "open" ? PluginAuditDetail::open_category(args) : event_category(event_name);
if (event_type == AuditEventCategory::None)
return 0;
const bool fs_category = is_fs_category(event_type); AuditDecision decision = mgr->check_open(path_str, mode_str);
const std::vector<std::string> targets = PluginAuditDetail::audit_targets(event_name, event_type, args); if (!decision.allowed)
return report_denied(*mgr, event_name, path_str, decision);
// A denied path (secrets, certificates, config files -- see is_denied_path) is an
// unconditional block: checked before the ancestor-cascade check below, so a cascade
// approval recorded for an unrelated action can never launder access to one, and before
// the allowed-root shortcut further down, so an allowed root (e.g. the read-only resources
// folder) cannot make a denied path underneath it reachable.
if (fs_category) {
for (const auto& target : targets) {
if (mgr->is_denied_path(boost::filesystem::path(target)))
return PluginAuditDetail::report_denied(*mgr, event_name, {false, "denied path"});
}
}
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;
// A filesystem target that resolves entirely inside a pre-determined allowed root -- the
// plugin system's own data_dir() tree (which holds each plugin's storage folder and the
// installed/system profile cache), the read-only bundled resources folder, or a per-call
// scoped root such as the current G-code folder -- is part of the plugin system's normal
// workflow and does not need a prompt.
if (fs_category && !targets.empty()) {
const bool is_write = event_type != AuditEventCategory::FsRead;
const bool all_inside_allowed_root =
std::all_of(targets.begin(), targets.end(), [&](const std::string& target) {
return mgr->check_path_access(boost::filesystem::path(target), is_write).allowed;
});
if (all_inside_allowed_root)
return 0; return 0;
} }
PluginInstallState state; // --- os.rename event (raised by os.rename and os.replace) ---
const bool have_install_state = PluginManager::instance().get_install_state(mgr->current_plugin(), state); if (event_name == "os.rename") {
if (!have_install_state) { const char* src_cstr = nullptr;
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Failed to get install state for " << mgr->current_plugin(); const char* dst_cstr = nullptr;
PyObject* src_dir_fd = nullptr;
PyObject* dst_dir_fd = nullptr;
// os.rename(src, dst, src_dir_fd, dst_dir_fd) — paths may be str, bytes, or int fd.
// The dir_fd arguments are unused, but must be accepted for the tuple to parse.
if (!PyArg_ParseTuple(args, "ss|OO", &src_cstr, &dst_cstr, &src_dir_fd, &dst_dir_fd)) {
PyErr_Clear(); // couldn't parse; allow
return 0;
} }
const std::string plugin_name = state.plugin_name.empty() ? mgr->current_plugin() : state.plugin_name; // A rename writes at both ends, so either end being denied blocks the call.
for (const char* path_cstr : {src_cstr, dst_cstr}) {
std::string path_str(path_cstr ? path_cstr : "");
AuditDecision decision = mgr->check_path_access(path_str, /* is_write */ true);
if (!decision.allowed)
return report_denied(*mgr, event_name, path_str, decision);
}
return 0;
}
std::vector<std::string>* permission_list = PluginAuditDetail::permission_list_for(event_type, state.permissions); // --- os.remove event (raised by os.remove and os.unlink) ---
if (event_name == "os.remove") {
const char* path_cstr = nullptr;
PyObject* dir_fd = nullptr;
return PluginAuditDetail::decide_audited_event(*mgr, state, mgr->current_plugin(), plugin_name, event_name, // os.remove(path, dir_fd) — path may be str, bytes, or int fd
event_type, targets, permission_list, call_site_ids); if (!PyArg_ParseTuple(args, "s|O", &path_cstr, &dir_fd)) {
PyErr_Clear(); // couldn't parse; allow
return 0;
}
std::string path_str(path_cstr ? path_cstr : "");
AuditDecision decision = mgr->check_path_access(path_str, /* is_write */ true);
if (!decision.allowed)
return report_denied(*mgr, event_name, path_str, decision);
return 0;
}
// Unknown event — allow by default
return 0;
} }
void PluginAuditManager::install_hook() void PluginAuditManager::install_hook()
{ {
// data_dir() is the primary globally-allowed root during plugin execution: read+write. It if (PySys_AddAuditHook(audit_hook, this) < 0) {
// covers the plugin system's own workflow needs -- each plugin's storage folder BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install CPython audit hook";
// (data_dir()/orca_plugins) and the installed/system profile cache (data_dir()/system) -- return;
// without a separate, narrower grant for either (G-code plugins additionally get the temp }
// G-code folder via a scoped root, see SlicingPipelinePluginCapabilityTrampoline). BOOST_LOG_TRIVIAL(info) << "[AUDIT] CPython audit hook installed successfully";
add_global_allowed_root(data_dir());
// resources_dir() holds the app's bundled, shared assets (installed system profiles, the // data_dir() is the only globally-allowed root during enforced plugin execution.
// bundled TLS client cert, web assets). Plugins may read from it -- e.g. inspecting bundled // The executable directory and resources directory are intentionally NOT allowed
// profiles -- but must never write into the shared, potentially multi-user app install, so // here: plugins must not write outside data_dir() (G-code plugins additionally get
// it is granted read-only. The bundled cert itself stays unreachable regardless, via the // the temp G-code folder via a scoped root). Reads remain permissive in Loading mode.
// "cert" denied-path keyword seeded below. add_global_allowed_root(data_dir());
add_global_allowed_root(resources_dir(), /*allow_write=*/false);
// The user's app config and cloud credentials live directly inside data_dir(), so the // The user's app config and cloud credentials live directly inside data_dir(), so the
// root just granted would otherwise expose them to any plugin. Deny them by name. // root just granted would otherwise expose them to any plugin. Deny them by name.
@@ -974,19 +430,6 @@ 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);
// Categorical denies on top of the exact-name list above: no path a plugin can reach may
// contain a "secret", "cert"(ificate), or "conf"(ig) path component, regardless of which
// allowed root it happens to sit inside. default_denied_path_keywords() is the single
// source of this list; the tests seed from it too.
for (const auto& keyword : default_denied_path_keywords())
add_denied_path_keyword(keyword);
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

View File

@@ -2,11 +2,8 @@
#define slic3r_PluginAuditManager_hpp_ #define slic3r_PluginAuditManager_hpp_
#include <Python.h> #include <Python.h>
#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>
@@ -21,29 +18,8 @@ struct AuditDecision {
struct AuditViolation { struct AuditViolation {
std::string plugin_key; std::string plugin_key;
std::string event_name; std::string event_name;
std::string reason;
};
// A filesystem root a plugin may access while an audit context is active. allow_write is
// false for a root that only grants reads (e.g. the bundled, shared resources folder) --
// a write-shaped event never matches such a root, even though a read-shaped one does.
struct AllowedRoot {
boost::filesystem::path path; boost::filesystem::path path;
bool allow_write = true; 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.
@@ -73,15 +49,12 @@ public:
void clear_current_capability(); void clear_current_capability();
// --- allowed-roots registry --- // --- allowed-roots registry ---
// allow_write = false registers a read-only root: a read-shaped event inside it is allowed, void add_global_allowed_root(const boost::filesystem::path& root);
// but a write/create/delete-shaped event is not, so it falls through to the normal void add_scoped_allowed_root(const boost::filesystem::path& root);
// prompt-or-deny path instead.
void add_global_allowed_root(const boost::filesystem::path& root, bool allow_write = true);
void add_scoped_allowed_root(const boost::filesystem::path& root, bool allow_write = true);
// --- denied-filenames registry --- // --- denied-filenames registry ---
// Filenames a plugin may never touch, in any directory, regardless of the enclosing allowed // Filenames a plugin may never touch, in any directory, regardless of audit mode or
// root. A candidate is denied when its filename starts with a // enclosing allowed root. A candidate is denied when its filename starts with a
// registered name, so .bak/.tmp companions are covered by the same entry. // registered name, so .bak/.tmp companions are covered by the same entry.
// //
// The comparison is case-insensitive on every platform, unlike the _WIN32-only iequals // The comparison is case-insensitive on every platform, unlike the _WIN32-only iequals
@@ -100,60 +73,33 @@ public:
// 8.3 short name is out of scope (see the design doc). This blocks direct access only. // 8.3 short name is out of scope (see the design doc). This blocks direct access only.
bool is_denied_filename(const boost::filesystem::path& candidate) const; bool is_denied_filename(const boost::filesystem::path& candidate) const;
// --- denied-path-keyword registry --- // --- enforcement mode ---
// Keywords that categorically deny a path if ANY of its components (directory or file enum class AuditMode {
// name), not just the base name, contains one case-insensitively -- e.g. a "secrets" // Import/loading phase: allow reads anywhere, only block writes
// subfolder, a "certificates" folder, or a "conf"/"config" file anywhere the plugin can // outside allowed roots. Python needs to read stdlib modules
// otherwise reach, including inside an allowed root. This is intentionally broader and // during import and those are not inside plugin directories.
// fuzzier than the exact-name is_denied_filename registry: it exists to categorically rule Loading,
// out whole classes of sensitive paths (secrets, certificates, config) rather than name
// specific known files, at the cost of over-blocking an unrelated name that happens to
// contain the keyword -- the fail-safe direction, same rationale as is_denied_filename.
void add_denied_path_keyword(const std::string& keyword);
// The list install_hook() seeds into the keyword registry. Exposed so tests seed the exact // Execution phase: block both reads and writes outside allowed
// same set without a live interpreter. // roots, plus subprocess/socket/ctypes.
static std::vector<std::string> default_denied_path_keywords(); Enforcing,
};
// True when any component of candidate's (canonicalized) path contains a registered void set_audit_mode(AuditMode mode);
// keyword, case-insensitively. AuditMode audit_mode() const;
bool is_denied_path_keyword(const boost::filesystem::path& candidate) const;
// is_denied_filename(candidate) || is_denied_path_keyword(candidate). Convenience for
// call sites that only need to know whether a path is categorically off-limits, not which
// specific rule fired.
bool is_denied_path(const boost::filesystem::path& candidate) const;
// --- policy checks --- // --- policy checks ---
// Shared core for every audited filesystem event. The deny checks are consulted above the // Shared core for every audited filesystem event. The deny list is consulted above the
// allowed roots, so a denied path is blocked even when it sits inside an allowed root (e.g. // Loading-mode read exemption and above the allowed roots, so a denied filename is
// data_dir(), which is a global allowed root). // blocked even though every scope currently runs in Loading and the files in question
// sit inside data_dir(), which is itself a global allowed root.
AuditDecision check_path_access(const boost::filesystem::path& candidate, bool is_write); AuditDecision check_path_access(const boost::filesystem::path& candidate, bool is_write);
AuditDecision check_open(const std::string& path, const std::string& mode); AuditDecision check_open(const std::string& path, const std::string& mode);
// Ask the user to grant the requested filesystem-read paths. The request may originate on a
// plugin load worker, so the implementation marshals the modal dialog to the wx main thread.
// Returns true only when every missing path was granted and persisted; denial aborts the plugin
// load without adding a permission.
bool request_filesystem_read_permissions(const std::string& plugin_key,
const std::vector<std::string>& paths);
void report_violation(const AuditViolation& violation); void report_violation(const AuditViolation& violation);
bool audit_denial_pending() const;
void clear_audit_denial();
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:
@@ -165,17 +111,15 @@ private:
static thread_local std::string m_current_plugin_key; static thread_local std::string m_current_plugin_key;
static thread_local std::string m_current_capability_name; static thread_local std::string m_current_capability_name;
static thread_local std::vector<AllowedRoot> m_scoped_allowed_roots; static thread_local AuditMode m_audit_mode;
static thread_local bool m_audit_denial_pending; static thread_local std::vector<boost::filesystem::path> m_scoped_allowed_roots;
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() and has_approved_ancestor() are const queries that must lock. // mutable: is_denied_filename() is a const query that must lock.
mutable std::mutex m_mutex; mutable std::mutex m_mutex;
std::vector<AllowedRoot> 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::vector<std::string> m_denied_path_keywords;
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
@@ -186,7 +130,8 @@ class ScopedPluginAuditContext
public: public:
explicit ScopedPluginAuditContext( explicit ScopedPluginAuditContext(
const std::string& plugin_key, const std::string& plugin_key,
const std::string& capability_name = {}); const std::string& capability_name = {},
PluginAuditManager::AuditMode mode = PluginAuditManager::AuditMode::Loading);
~ScopedPluginAuditContext(); ~ScopedPluginAuditContext();
@@ -196,7 +141,8 @@ public:
private: private:
std::string m_previous_id; std::string m_previous_id;
std::string m_previous_capability; std::string m_previous_capability;
std::vector<AllowedRoot> m_previous_scoped_roots; PluginAuditManager::AuditMode m_previous_mode;
std::vector<boost::filesystem::path> m_previous_scoped_roots;
}; };
} // namespace Slic3r } // namespace Slic3r

View File

@@ -784,23 +784,6 @@ bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstall
parsed.plugin_name = state["plugin_name"].get<std::string>(); parsed.plugin_name = state["plugin_name"].get<std::string>();
if (state.contains("cloud_uuid") && state["cloud_uuid"].is_string()) if (state.contains("cloud_uuid") && state["cloud_uuid"].is_string())
parsed.cloud_uuid = state["cloud_uuid"].get<std::string>(); parsed.cloud_uuid = state["cloud_uuid"].get<std::string>();
if (state.contains("permissions") && state["permissions"].is_object()) {
const auto& permissions = state["permissions"];
auto read_string_list = [&permissions](const char* key, std::vector<std::string>& 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<std::string>());
};
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()) if (state.contains("enabled") && state["enabled"].is_boolean())
parsed.enabled = state["enabled"].get<bool>(); parsed.enabled = state["enabled"].get<bool>();
@@ -836,14 +819,6 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
if (!state.cloud_uuid.empty()) if (!state.cloud_uuid.empty())
json["cloud_uuid"] = state.cloud_uuid; json["cloud_uuid"] = state.cloud_uuid;
json["permissions"] = {
{"fs_read", state.permissions.fs_read},
{"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();
for (const auto& [name, enabled] : state.capabilities) for (const auto& [name, enabled] : state.capabilities)
capabilities.push_back(nlohmann::json{{name, enabled}}); capabilities.push_back(nlohmann::json{{name, enabled}});
@@ -861,9 +836,6 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
const std::vector<std::pair<std::string, bool>>& capabilities) const std::vector<std::pair<std::string, bool>>& capabilities)
{ {
PluginInstallState state; PluginInstallState state;
// Loading a plugin updates its lifecycle/capability state, but must retain permissions granted
// during register_capabilities() or by a previous runtime audit prompt.
read_install_state(plugin_dir, state);
state.installed_from = entry.is_cloud_plugin() ? "cloud" : "local"; state.installed_from = entry.is_cloud_plugin() ? "cloud" : "local";
// Prefer the descriptor's recorded installed_version (the version fetched from the cloud // Prefer the descriptor's recorded installed_version (the version fetched from the cloud
// at install time, preserved across sidecar re-writes) so a stale manifest/PEP723 header // at install time, preserved across sidecar re-writes) so a stale manifest/PEP723 header
@@ -880,16 +852,10 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry) bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry)
{ {
// Install-time writer: the package is not loaded, so its capabilities are not known yet and the // Install-time writer: the package is not loaded, so its capabilities are not known yet and the
// sidecar is (re)initialized to "auto-load, nothing disabled". This intentionally resets // sidecar is (re)initialized to "auto-load, nothing disabled". PluginManager writes the real
// permissions on a fresh install/reinstall. PluginManager writes the real per-capability flags // per-capability flags once the package is loaded, via the (dir, entry, enabled, capabilities)
// while preserving permissions once the package is loaded, via the overload above. // overload.
PluginInstallState state; return write_install_state(plugin_dir, entry, true, {});
state.installed_from = entry.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !entry.installed_version.empty() ? entry.installed_version : entry.version;
state.plugin_name = entry.name;
state.cloud_uuid = entry.cloud_uuid();
state.enabled = true;
return write_install_state(plugin_dir, state);
} }
bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginDescriptor& descriptor, std::string& error) bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginDescriptor& descriptor, std::string& error)

View File

@@ -82,23 +82,11 @@ inline nlohmann::json py_to_json(const pybind11::handle& o)
return py::str(o).cast<std::string>(); // fallback: str() return py::str(o).cast<std::string>(); // fallback: str()
} }
struct PluginPermissions
{
std::vector<std::string> fs_read;
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 {
std::string installed_from; // "local" | "cloud" std::string installed_from; // "local" | "cloud"
std::string installed_version; std::string installed_version;
std::string plugin_name; std::string plugin_name;
std::string cloud_uuid; // empty for local std::string cloud_uuid; // empty for local
PluginPermissions permissions;
bool enabled = true; bool enabled = true;
std::vector<std::pair<std::string, bool>> capabilities; // name -> enabled, ordered std::vector<std::pair<std::string, bool>> capabilities; // name -> enabled, ordered
}; };

View File

@@ -280,7 +280,7 @@ bool load(const PluginDescriptor& descriptor,
// (while the active plugin key is set), then instantiates each registered capability and caches // (while the active plugin key is set), then instantiates each registered capability and caches
// its get_name(). Returns one entry per capability. // its get_name(). Returns one entry per capability.
std::string bridge_error; std::string bridge_error;
auto capabilities_found = bridge.finalize_plugin_capture(descriptor.entry_path, descriptor.plugin_key, bridge_error); auto capabilities_found = bridge.finalize_plugin_capture(descriptor.entry_path, bridge_error);
if (!bridge_error.empty()) { if (!bridge_error.empty()) {
capabilities_found.clear(); capabilities_found.clear();
error = "Plugin registration failed: " + bridge_error; error = "Plugin registration failed: " + bridge_error;

View File

@@ -21,7 +21,6 @@
#include <chrono> #include <chrono>
#include <mutex> #include <mutex>
#include <slic3r/plugin/PluginConfig.hpp> #include <slic3r/plugin/PluginConfig.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PluginLoader.hpp> #include <slic3r/plugin/PluginLoader.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp> #include <slic3r/plugin/PythonPluginInterface.hpp>
#include <slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp> #include <slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp>
@@ -623,20 +622,6 @@ std::shared_ptr<PluginCapabilityInterface> PluginManager::get_plugin_capability(
return nullptr; return nullptr;
} }
bool PluginManager::get_install_state(const std::string& plugin_key, PluginInstallState& install_state)
{
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor)) {
return false;
}
if (!read_install_state(boost::filesystem::path(descriptor.plugin_root), install_state)) {
return false;
}
return true;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────────────────── // ── Lifecycle ───────────────────────────────────────────────────────────────────────────────
bool PluginManager::is_plugin_loaded(const std::string& plugin_key) const bool PluginManager::is_plugin_loaded(const std::string& plugin_key) const
@@ -925,8 +910,6 @@ void PluginManager::load_plugin_impl(const std::string& plugin_key, bool skip_de
if (!plugin_loader::load(descriptor, skip_deps, capabilities_to_enable, registry_precheck, plugin, error)) { if (!plugin_loader::load(descriptor, skip_deps, capabilities_to_enable, registry_precheck, plugin, error)) {
if (error == LOAD_CANCELLED) if (error == LOAD_CANCELLED)
return; // cancelled: nothing materialized survives, and no error is recorded return; // cancelled: nothing materialized survives, and no error is recorded
if (error.rfind("Plugin registration failed:", 0) == 0)
mark_plugin_install_state_disabled(plugin_key);
fail(std::move(error)); fail(std::move(error));
return; return;
} }
@@ -1152,51 +1135,6 @@ void PluginManager::write_loaded_plugin_install_state(const std::string& plugin_
write_install_state(boost::filesystem::path(descriptor.plugin_root), descriptor, /*enabled=*/true, capabilities); write_install_state(boost::filesystem::path(descriptor.plugin_root), descriptor, /*enabled=*/true, capabilities);
} }
void PluginManager::mark_plugin_install_state_disabled(const std::string& plugin_key)
{
std::lock_guard<std::mutex> state_lock(m_install_state_mutex);
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return;
const boost::filesystem::path root(descriptor.plugin_root);
PluginInstallState state;
if (!read_install_state(root, state)) {
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
state.plugin_name = descriptor.name;
state.cloud_uuid = descriptor.cloud_uuid();
}
state.enabled = false;
if (!write_install_state(root, state))
return;
std::lock_guard<std::mutex> lock(m_mutex);
if (Plugin* plugin = find_plugin_locked(plugin_key))
plugin->descriptor.enabled = false;
}
void PluginManager::revoke_plugin_permissions(const std::string& plugin_key)
{
std::lock_guard<std::mutex> state_lock(m_install_state_mutex);
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return;
const boost::filesystem::path root(descriptor.plugin_root);
PluginInstallState state;
if (!read_install_state(root, state)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to read install state for " << plugin_key;
return;
}
state.permissions = {};
if (!write_install_state(root, state))
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to revoke permissions for " << plugin_key;
}
// ── Callbacks ─────────────────────────────────────────────────────────────────────────────── // ── Callbacks ───────────────────────────────────────────────────────────────────────────────
void PluginManager::subscribe_on_load_callback(PluginLifecycleCompleteFn fn) void PluginManager::subscribe_on_load_callback(PluginLifecycleCompleteFn fn)
@@ -1416,11 +1354,6 @@ bool PluginManager::install_plugin(const boost::filesystem::path& filepath, Plug
return false; return false;
} }
// Every successful install may have replaced executable plugin code. Revoke any permissions
// associated with the previous package so the newly installed version must request them again.
if (!plugin_descriptor.plugin_key.empty())
revoke_plugin_permissions(plugin_descriptor.plugin_key);
if (!plugin_descriptor.plugin_key.empty()) if (!plugin_descriptor.plugin_key.empty())
clear_plugin_error(plugin_descriptor.plugin_key); clear_plugin_error(plugin_descriptor.plugin_key);
@@ -1808,7 +1741,6 @@ bool PluginManager::update_cloud_plugin(const std::string& plugin_key, std::stri
} }
clear_plugin_error(plugin_key); clear_plugin_error(plugin_key);
return true; return true;
} }

View File

@@ -159,8 +159,6 @@ public:
PluginCapabilityType type = PluginCapabilityType::Unknown, PluginCapabilityType type = PluginCapabilityType::Unknown,
bool only_enabled = true) const; bool only_enabled = true) const;
bool get_install_state(const std::string& plugin_key, PluginInstallState& install_state);
void load_plugin(const std::string& plugin_key, bool skip_deps = false, std::vector<std::string> capabilities_to_enable = {}); void load_plugin(const std::string& plugin_key, bool skip_deps = false, std::vector<std::string> capabilities_to_enable = {});
bool unload_plugin(const std::string& plugin_key); bool unload_plugin(const std::string& plugin_key);
void unload_all_plugins(); void unload_all_plugins();
@@ -257,9 +255,6 @@ private:
// Writes the sidecar for a loaded plugin (enabled=true plus the current per-capability flags). // Writes the sidecar for a loaded plugin (enabled=true plus the current per-capability flags).
void write_loaded_plugin_install_state(const std::string& plugin_key); void write_loaded_plugin_install_state(const std::string& plugin_key);
void mark_plugin_install_state_disabled(const std::string& plugin_key);
// Revoke permissions after a package replacement so the new package must request them again.
void revoke_plugin_permissions(const std::string& plugin_key);
bool finalize_cloud_plugin_removal(const PluginDescriptor& plugin, bool keep_local, std::string& error); bool finalize_cloud_plugin_removal(const PluginDescriptor& plugin, bool keep_local, std::string& error);
bool delete_installed_plugin_package(const PluginDescriptor& plugin, std::string& error); bool delete_installed_plugin_package(const PluginDescriptor& plugin, std::string& error);

View File

@@ -26,30 +26,25 @@
try { \ try { \
override_call; \ override_call; \
} catch (pybind11::error_already_set & err) { \ } catch (pybind11::error_already_set & err) { \
const bool _orca_audit_denial = ::Slic3r::PluginAuditManager::instance().audit_denial_pending(); \
if (_orca_audit_denial) \
::Slic3r::PluginAuditManager::instance().clear_current_plugin(); \
::Slic3r::log_python_exception_keep(err); \ ::Slic3r::log_python_exception_keep(err); \
if (_orca_audit_denial) \
::Slic3r::PluginAuditManager::instance().clear_audit_denial(); \
throw; \ throw; \
} }
// Opens the plugin's filesystem audit scope for the duration of a C++ -> Python call, and publishes // Opens the plugin's filesystem audit scope for the duration of a C++ -> Python call, and publishes
// the calling capability's cached name so host APIs invoked from Python can tell which capability // the calling capability's cached name so host APIs invoked from Python can tell which capability
// they are serving. No-op without an audit plugin key. Declares a local `_orca_audit_scope`. // they are serving. No-op without an audit plugin key. Declares a local `_orca_audit_scope`.
#define ORCA_PY_AUDIT_SCOPE() \ #define ORCA_PY_AUDIT_SCOPE(mode) \
std::optional<::Slic3r::ScopedPluginAuditContext> _orca_audit_scope; \ std::optional<::Slic3r::ScopedPluginAuditContext> _orca_audit_scope; \
if (const std::string& _orca_audit_key = this->audit_plugin_key(); !_orca_audit_key.empty()) \ if (const std::string& _orca_audit_key = this->audit_plugin_key(); !_orca_audit_key.empty()) \
_orca_audit_scope.emplace(_orca_audit_key, this->name()) _orca_audit_scope.emplace(_orca_audit_key, this->name(), mode)
#define ORCA_PY_OVERRIDE_AUDITED(audit_setup, override_macro, ret, base, name, ...) \ #define ORCA_PY_OVERRIDE_AUDITED(mode, audit_setup, override_macro, ret, base, name, ...) \
do { \ do { \
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this); \ ::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this); \
::Slic3r::PythonGILState _orca_python_gil; \ ::Slic3r::PythonGILState _orca_python_gil; \
if (!_orca_python_gil) \ if (!_orca_python_gil) \
throw std::runtime_error("Python interpreter is shutting down"); \ throw std::runtime_error("Python interpreter is shutting down"); \
ORCA_PY_AUDIT_SCOPE(); \ ORCA_PY_AUDIT_SCOPE(mode); \
if (_orca_audit_scope) \ if (_orca_audit_scope) \
audit_setup(); \ audit_setup(); \
ORCA_PY_LOGGED_OVERRIDE_BODY(override_macro(ret, base, name, ##__VA_ARGS__)); \ ORCA_PY_LOGGED_OVERRIDE_BODY(override_macro(ret, base, name, ##__VA_ARGS__)); \
@@ -63,7 +58,7 @@ public:
std::string get_name() const override std::string get_name() const override
{ {
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE_PURE, std::string, Base, get_name); ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, Base, get_name);
} }
// Config UI hooks. Available on every capability type, so they live here rather than in // Config UI hooks. Available on every capability type, so they live here rather than in
@@ -71,6 +66,7 @@ public:
bool has_config_ui() const override bool has_config_ui() const override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {}, [] {},
PYBIND11_OVERRIDE, PYBIND11_OVERRIDE,
bool, bool,
@@ -81,6 +77,7 @@ public:
std::string get_config_ui() const override std::string get_config_ui() const override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {}, [] {},
PYBIND11_OVERRIDE, PYBIND11_OVERRIDE,
std::string, std::string,
@@ -97,7 +94,7 @@ public:
// mistake), both fall back to the base's empty object rather than writing `"cap_config": null`. // mistake), both fall back to the base's empty object rather than writing `"cap_config": null`.
nlohmann::json get_default_config() const override nlohmann::json get_default_config() const override
{ {
ORCA_PY_AUDIT_SCOPE(); ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
try { try {
pybind11::gil_scoped_acquire gil; pybind11::gil_scoped_acquire gil;
pybind11::function override = pybind11::get_override(static_cast<const Base*>(this), "get_default_config"); pybind11::function override = pybind11::get_override(static_cast<const Base*>(this), "get_default_config");
@@ -120,17 +117,17 @@ public:
void on_load() override void on_load() override
{ {
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_load); ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_load);
} }
void on_unload() override void on_unload() override
{ {
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_unload); ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_unload);
} }
void on_cancelled() override void on_cancelled() override
{ {
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled); ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled);
} }
}; };
@@ -142,7 +139,7 @@ public:
PluginCapabilityType get_type() const override PluginCapabilityType get_type() const override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, PluginCapabilityType, PluginCapabilityInterface, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, PluginCapabilityType, PluginCapabilityInterface,
get_type); get_type);
} }
}; };

View File

@@ -87,12 +87,6 @@ void log_python_exception_keep(pybind11::error_already_set& err)
if (!gil) if (!gil)
return; return;
// Traceback output is host-owned work. In particular, the stderr tee opens the Python log
// file on every write. Keep that open outside the plugin audit context, otherwise an ordinary
// exception raised by a plugin can recursively trigger the filesystem permission dialog while
// its original exception is being reported.
ScopedPluginAuditContext audit_suppression("");
// Non-destructive: print the traceback to sys.stderr (tee'd to the session log) // Non-destructive: print the traceback to sys.stderr (tee'd to the session log)
// WITHOUT consuming err, so the caller can rethrow it intact. For example, downstream C++ // WITHOUT consuming err, so the caller can rethrow it intact. For example, downstream C++
// catchers can still read err.what() for the user-facing dialog. We must NOT use // catchers can still read err.what() for the user-facing dialog. We must NOT use
@@ -628,6 +622,10 @@ 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

View File

@@ -1,6 +1,5 @@
#include "PythonPluginBridge.hpp" #include "PythonPluginBridge.hpp"
#include <algorithm>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <exception> #include <exception>
#include <memory> #include <memory>
@@ -41,7 +40,6 @@ thread_local std::string g_active_plugin_key;
std::mutex g_registry_mutex; std::mutex g_registry_mutex;
std::unordered_map<std::string, std::vector<py::object>> g_pending_capabilities; std::unordered_map<std::string, std::vector<py::object>> g_pending_capabilities;
std::unordered_map<std::string, py::object> g_pending_package; std::unordered_map<std::string, py::object> g_pending_package;
std::unordered_map<std::string, std::vector<std::string>> g_pending_fs_read_permissions;
struct PluginInstanceHandle struct PluginInstanceHandle
{ {
// The C++ plugin interface points into a Python object. Keep both alive through one // The C++ plugin interface points into a Python object. Keep both alive through one
@@ -79,8 +77,6 @@ void discard_pending_capture_without_python(const std::string& plugin_key)
(void) package->second.release(); (void) package->second.release();
g_pending_package.erase(package); g_pending_package.erase(package);
} }
g_pending_fs_read_permissions.erase(plugin_key);
} }
} // namespace } // namespace
@@ -105,38 +101,34 @@ void PythonPluginBridge::begin_plugin_capture(const std::string& plugin_key)
// for this same entry path. // for this same entry path.
g_pending_capabilities.erase(plugin_key); g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key); g_pending_package.erase(plugin_key);
g_pending_fs_read_permissions.erase(plugin_key);
} }
// From now until finalize/cancel, @orca.plugin and register_capability() calls made by // From now until finalize/cancel, @orca.plugin and register_capability() calls made by
// Python code on this thread are attributed to this plugin. // Python code on this thread are attributed to this plugin.
g_active_plugin_key = plugin_key; g_active_plugin_key = plugin_key;
} }
std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& capture_key, std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& plugin_key, std::string& error)
const std::string& plugin_key,
std::string& error)
{ {
PythonGILState gil; PythonGILState gil;
if (!gil) { if (!gil) {
error = "Python interpreter is shutting down"; error = "Python interpreter is shutting down";
return {}; return {};
} }
BOOST_LOG_TRIVIAL(info) << "Finalizing Python plugin capture for key " << capture_key; BOOST_LOG_TRIVIAL(info) << "Finalizing Python plugin capture for key " << plugin_key;
// Phase 1: run the package class's register_capabilities() while the active key is // Phase 1: run the package class's register_capabilities() while the active key is
// still set. That method is expected to call orca.register_capability() once per // still set. That method is expected to call orca.register_capability() once per
// capability class, and register_capability() needs g_active_plugin_key to know which // capability class, and register_capability() needs g_active_plugin_key to know which
// pending bucket to append to. // pending bucket to append to.
{ {
auto clear_active_key = [&capture_key]() { auto clear_active_key = [&plugin_key]() {
if (g_active_plugin_key == capture_key) if (g_active_plugin_key == plugin_key)
g_active_plugin_key.clear(); g_active_plugin_key.clear();
}; };
auto discard_pending_for_key = [&capture_key]() { auto discard_pending_for_key = [&plugin_key]() {
std::lock_guard<std::mutex> lock(g_registry_mutex); std::lock_guard<std::mutex> lock(g_registry_mutex);
g_pending_capabilities.erase(capture_key); g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(capture_key); g_pending_package.erase(plugin_key);
g_pending_fs_read_permissions.erase(capture_key);
}; };
try { try {
@@ -146,7 +138,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
py::object package_cls; py::object package_cls;
{ {
std::lock_guard<std::mutex> lock(g_registry_mutex); std::lock_guard<std::mutex> lock(g_registry_mutex);
auto it = g_pending_package.find(capture_key); auto it = g_pending_package.find(plugin_key);
if (it != g_pending_package.end()) { if (it != g_pending_package.end()) {
package_cls = it->second; package_cls = it->second;
g_pending_package.erase(it); g_pending_package.erase(it);
@@ -165,33 +157,6 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
// are kept. // are kept.
py::object package = package_cls(); py::object package = package_cls();
package.attr("register_capabilities")(); package.attr("register_capabilities")();
// Permission declarations are collected while register_capabilities() runs so the
// plugin can describe its needs without touching wx from the Python load worker. Ask
// only after registration returns, before capability instances are materialized.
std::vector<std::string> fs_read_permissions;
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto it = g_pending_fs_read_permissions.find(capture_key);
if (it != g_pending_fs_read_permissions.end()) {
fs_read_permissions = std::move(it->second);
g_pending_fs_read_permissions.erase(it);
}
}
if (!fs_read_permissions.empty()) {
bool granted = false;
{
py::gil_scoped_release release;
granted = PluginAuditManager::instance().request_filesystem_read_permissions(plugin_key, fs_read_permissions);
}
if (!granted) {
error = "Plugin filesystem read permission request denied";
BOOST_LOG_TRIVIAL(warning) << error << " for key " << plugin_key;
discard_pending_for_key();
clear_active_key();
return {};
}
}
} catch (py::error_already_set& err) { } catch (py::error_already_set& err) {
log_python_exception_keep(err); log_python_exception_keep(err);
error = err.what(); error = err.what();
@@ -215,7 +180,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
std::vector<py::object> classes; std::vector<py::object> classes;
{ {
std::lock_guard<std::mutex> lock(g_registry_mutex); std::lock_guard<std::mutex> lock(g_registry_mutex);
auto it = g_pending_capabilities.find(capture_key); auto it = g_pending_capabilities.find(plugin_key);
if (it != g_pending_capabilities.end()) { if (it != g_pending_capabilities.end()) {
classes = std::move(it->second); classes = std::move(it->second);
g_pending_capabilities.erase(it); g_pending_capabilities.erase(it);
@@ -224,10 +189,10 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
// Registration is complete. Later register_capability() calls should fail instead of // Registration is complete. Later register_capability() calls should fail instead of
// accidentally attaching themselves to this plugin. // accidentally attaching themselves to this plugin.
if (g_active_plugin_key == capture_key) if (g_active_plugin_key == plugin_key)
g_active_plugin_key.clear(); g_active_plugin_key.clear();
BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << capture_key; BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << plugin_key;
std::vector<CapturedCapability> capabilities; std::vector<CapturedCapability> capabilities;
capabilities.reserve(classes.size()); capabilities.reserve(classes.size());
@@ -239,7 +204,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
py::object instance = cls(); py::object instance = cls();
if (!py::isinstance<PluginCapabilityInterface>(instance)) { if (!py::isinstance<PluginCapabilityInterface>(instance)) {
error = "Registered capability must inherit from a PluginCapability base"; error = "Registered capability must inherit from a PluginCapability base";
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed type check for key " << capture_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed type check for key " << plugin_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} }
@@ -247,7 +212,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
auto capability_iface = instance.cast<std::shared_ptr<PluginCapabilityInterface>>(); auto capability_iface = instance.cast<std::shared_ptr<PluginCapabilityInterface>>();
if (!capability_iface) { if (!capability_iface) {
error = "Failed to cast Python capability to PluginCapabilityInterface"; error = "Failed to cast Python capability to PluginCapabilityInterface";
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed cast for key " << capture_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed cast for key " << plugin_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} }
@@ -261,7 +226,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
// here is a hard error that rejects the whole plugin capture. // here is a hard error that rejects the whole plugin capture.
if (name.find(';') != std::string::npos) { if (name.find(';') != std::string::npos) {
error = "Capability name must not contain ';': " + name; error = "Capability name must not contain ';': " + name;
BOOST_LOG_TRIVIAL(error) << "Python plugin capture rejected capability for key " << capture_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture rejected capability for key " << plugin_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} }
@@ -281,12 +246,12 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
// log the traceback here. GIL is held for the duration of finalize_plugin_capture. // log the traceback here. GIL is held for the duration of finalize_plugin_capture.
log_python_exception_keep(err); log_python_exception_keep(err);
error = err.what(); error = err.what();
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised Python exception for key " << capture_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised Python exception for key " << plugin_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
error = ex.what(); error = ex.what();
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised exception for key " << capture_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised exception for key " << plugin_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} }
@@ -314,7 +279,6 @@ void PythonPluginBridge::cancel_plugin_capture(const std::string& plugin_key)
// may already have registered under this key. // may already have registered under this key.
g_pending_capabilities.erase(plugin_key); g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key); g_pending_package.erase(plugin_key);
g_pending_fs_read_permissions.erase(plugin_key);
} }
if (g_active_plugin_key == plugin_key) if (g_active_plugin_key == plugin_key)
@@ -341,7 +305,6 @@ void PythonPluginBridge::clear_pending_captures()
(void) pkg.release(); (void) pkg.release();
} }
g_pending_package.clear(); g_pending_package.clear();
g_pending_fs_read_permissions.clear();
g_active_plugin_key.clear(); g_active_plugin_key.clear();
return; return;
} }
@@ -350,7 +313,6 @@ void PythonPluginBridge::clear_pending_captures()
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)"; BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)";
g_pending_capabilities.clear(); g_pending_capabilities.clear();
g_pending_package.clear(); g_pending_package.clear();
g_pending_fs_read_permissions.clear();
g_active_plugin_key.clear(); g_active_plugin_key.clear();
} }
@@ -488,27 +450,6 @@ void bind_python_api(pybind11::module_& m)
}, },
R"pbdoc(Register a PluginCapability subclass while OrcaSlicer loads your module.)pbdoc"); R"pbdoc(Register a PluginCapability subclass while OrcaSlicer loads your module.)pbdoc");
m.def(
"request_permissions",
[](const std::vector<std::string>& fs_read) {
if (g_active_plugin_key.empty())
throw py::value_error("request_permissions() called outside plugin discovery context");
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto& requested = g_pending_fs_read_permissions[g_active_plugin_key];
for (const std::string& path : fs_read) {
if (path.empty())
throw py::value_error("request_permissions(fs_read=...) does not accept empty paths");
if (std::find(requested.begin(), requested.end(), path) == requested.end())
requested.push_back(path);
}
},
py::arg("fs_read") = std::vector<std::string>{},
R"pbdoc(Request filesystem read permissions while OrcaSlicer loads your module.
The host presents the request to the user after register_capabilities() returns. Denying the
request aborts the plugin load.)pbdoc");
m.def("plugin", [](py::object cls) { m.def("plugin", [](py::object cls) {
if (g_active_plugin_key.empty()) if (g_active_plugin_key.empty())
throw py::value_error("@orca.plugin used outside plugin discovery context"); throw py::value_error("@orca.plugin used outside plugin discovery context");

View File

@@ -31,7 +31,7 @@ public:
// Returns one CapturedCapability per capability, or an empty vector on failure // Returns one CapturedCapability per capability, or an empty vector on failure
// (error message populated). // (error message populated).
std::vector<CapturedCapability> finalize_plugin_capture( std::vector<CapturedCapability> finalize_plugin_capture(
const std::string& capture_key, const std::string& plugin_key, std::string& error); const std::string& plugin_key, std::string& error);
// Clear any pending registrations for the key. Safe to call when import fails. // Clear any pending registrations for the key. Safe to call when import fails.
void cancel_plugin_capture(const std::string& plugin_key); void cancel_plugin_capture(const std::string& plugin_key);

View File

@@ -16,6 +16,7 @@ public:
std::string get_icon() override std::string get_icon() override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {}, [] {},
PYBIND11_OVERRIDE, PYBIND11_OVERRIDE,
std::string, std::string,
@@ -26,6 +27,7 @@ public:
std::string get_ui() override std::string get_ui() override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {}, [] {},
PYBIND11_OVERRIDE_PURE, PYBIND11_OVERRIDE_PURE,
std::string, std::string,
@@ -40,7 +42,7 @@ public:
if (!gil) if (!gil)
throw std::runtime_error("Python interpreter is shutting down"); throw std::runtime_error("Python interpreter is shutting down");
ORCA_PY_AUDIT_SCOPE(); ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
pybind11::function override = pybind11::get_override(static_cast<PagesPluginCapability*>(this), "on_message"); pybind11::function override = pybind11::get_override(static_cast<PagesPluginCapability*>(this), "on_message");
if (!override) if (!override)

View File

@@ -16,206 +16,206 @@ public:
AgentInfo get_agent_info() override AgentInfo get_agent_info() override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, AgentInfo, PrinterAgentPluginCapability, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, AgentInfo, PrinterAgentPluginCapability,
get_agent_info); get_agent_info);
} }
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, connect_printer, dev_id, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, connect_printer, dev_id,
dev_ip, username, password, use_ssl); dev_ip, username, password, use_ssl);
} }
int disconnect_printer() override int disconnect_printer() override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, disconnect_printer); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, disconnect_printer);
} }
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override int send_message(std::string dev_id, std::string json_str, int qos, int flag) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message, dev_id, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message, dev_id,
json_str, qos, flag); json_str, qos, flag);
} }
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message_to_printer, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message_to_printer,
dev_id, json_str, qos, flag); dev_id, json_str, qos, flag);
} }
bool start_discovery(bool start, bool sending) override bool start_discovery(bool start, bool sending) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, start_discovery, start, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, start_discovery, start,
sending); sending);
} }
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind_detect, dev_ip, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind_detect, dev_ip,
sec_link, detect); sec_link, detect);
} }
std::string get_user_selected_machine() override std::string get_user_selected_machine() override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability,
get_user_selected_machine); get_user_selected_machine);
} }
int set_user_selected_machine(std::string dev_id) override int set_user_selected_machine(std::string dev_id) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
set_user_selected_machine, dev_id); set_user_selected_machine, dev_id);
} }
int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
start_send_gcode_to_sdcard, params, update_fn, cancel_fn, wait_fn); start_send_gcode_to_sdcard, params, update_fn, cancel_fn, wait_fn);
} }
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print,
params, update_fn, cancel_fn); params, update_fn, cancel_fn);
} }
FilamentSyncMode get_filament_sync_mode() const override FilamentSyncMode get_filament_sync_mode() const override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, FilamentSyncMode, PrinterAgentPluginCapability, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, FilamentSyncMode, PrinterAgentPluginCapability,
get_filament_sync_mode); get_filament_sync_mode);
} }
bool fetch_filament_info(std::string dev_id) override bool fetch_filament_info(std::string dev_id) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id);
} }
int check_cert() override int check_cert() override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, check_cert); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, check_cert);
} }
void install_device_cert(std::string dev_id, bool lan_only) override void install_device_cert(std::string dev_id, bool lan_only) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, void, PrinterAgentPluginCapability, install_device_cert, dev_id, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, void, PrinterAgentPluginCapability, install_device_cert, dev_id,
lan_only); lan_only);
} }
int ping_bind(std::string ping_code) override int ping_bind(std::string ping_code) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ping_bind, ping_code); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ping_bind, ping_code);
} }
int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id,
dev_model, sec_link, timezone, improved, update_fn); dev_model, sec_link, timezone, improved, update_fn);
} }
int unbind(std::string dev_id) override int unbind(std::string dev_id) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, unbind, dev_id); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, unbind, dev_id);
} }
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_print, params, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_print, params,
update_fn, cancel_fn, wait_fn); update_fn, cancel_fn, wait_fn);
} }
int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
start_local_print_with_record, params, update_fn, cancel_fn, wait_fn); start_local_print_with_record, params, update_fn, cancel_fn, wait_fn);
} }
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_sdcard_print, params, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_sdcard_print, params,
update_fn, cancel_fn); update_fn, cancel_fn);
} }
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, get_hms_snapshot, dev_id, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, get_hms_snapshot, dev_id,
file_name, callback); file_name, callback);
} }
int set_server_callback(OnServerErrFn fn) override int set_server_callback(OnServerErrFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_server_callback, fn); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_server_callback, fn);
} }
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_ssdp_msg_fn, fn); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_ssdp_msg_fn, fn);
} }
int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_printer_connected_fn, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_printer_connected_fn,
fn); fn);
} }
int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_subscribe_failure_fn, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_subscribe_failure_fn,
fn); fn);
} }
int set_on_message_fn(OnMessageFn fn) override int set_on_message_fn(OnMessageFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_message_fn, fn); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_message_fn, fn);
} }
int set_on_user_message_fn(OnMessageFn fn) override int set_on_user_message_fn(OnMessageFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_user_message_fn, fn); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_user_message_fn, fn);
} }
int set_on_local_connect_fn(OnLocalConnectedFn fn) override int set_on_local_connect_fn(OnLocalConnectedFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_connect_fn, fn); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_connect_fn, fn);
} }
int set_on_local_message_fn(OnMessageFn fn) override int set_on_local_message_fn(OnMessageFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_message_fn, fn); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_message_fn, fn);
} }
int set_queue_on_main_fn(QueueOnMainFn fn) override int set_queue_on_main_fn(QueueOnMainFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_queue_on_main_fn, fn); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_queue_on_main_fn, fn);
} }
// request_bind_ticket returns its ticket through a std::string* out-param, which pybind11 // request_bind_ticket returns its ticket through a std::string* out-param, which pybind11
@@ -223,7 +223,7 @@ public:
// returns a (result, ticket) tuple, which we unpack into the int result and the out-param. // returns a (result, ticket) tuple, which we unpack into the int result and the out-param.
int request_bind_ticket(std::string* ticket) override int request_bind_ticket(std::string* ticket) override
{ {
ORCA_PY_AUDIT_SCOPE(); ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this); ::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this);
::Slic3r::PythonGILState gil; ::Slic3r::PythonGILState gil;
if (!gil) if (!gil)

View File

@@ -13,6 +13,7 @@ public:
ExecutionResult execute() override ExecutionResult execute() override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {}, [] {},
PYBIND11_OVERRIDE_PURE, PYBIND11_OVERRIDE_PURE,
ExecutionResult, ExecutionResult,

View File

@@ -10,6 +10,7 @@ public:
using PyPluginCommonTrampoline<SlicingPipelinePluginCapability>::PyPluginCommonTrampoline; using PyPluginCommonTrampoline<SlicingPipelinePluginCapability>::PyPluginCommonTrampoline;
ExecutionResult execute(SlicingPipelineContext& ctx) override { ExecutionResult execute(SlicingPipelineContext& ctx) override {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[&]{ [&]{
// At Step.psGCodePostProcess the plugin edits the exported G-code file, which lives // At Step.psGCodePostProcess the plugin edits the exported G-code file, which lives
// outside data_dir() (a temp/output folder), so writing to it would otherwise be // outside data_dir() (a temp/output folder), so writing to it would otherwise be

View File

@@ -34,24 +34,4 @@ struct ScopedDataDir
ScopedDataDir& operator=(const ScopedDataDir&) = delete; ScopedDataDir& operator=(const ScopedDataDir&) = delete;
}; };
// Point resources_dir() at a throwaway directory for the lifetime of a test and restore the
// previous value afterwards, mirroring ScopedDataDir.
struct ScopedResourcesDir
{
ScopedTemporaryDir tmp;
boost::filesystem::path dir;
std::string previous;
explicit ScopedResourcesDir(const std::string& tag)
: tmp("orca-" + tag), dir(tmp.path()), previous(resources_dir())
{
set_resources_dir(dir.string());
}
~ScopedResourcesDir() { set_resources_dir(previous); }
ScopedResourcesDir(const ScopedResourcesDir&) = delete;
ScopedResourcesDir& operator=(const ScopedResourcesDir&) = delete;
};
} // namespace Slic3r } // namespace Slic3r

View File

@@ -27,16 +27,6 @@ void seed_denied_names()
mgr.add_denied_filename(name); mgr.add_denied_filename(name);
} }
// Seed the keyword registry with the same list install_hook() uses. Same rationale as
// seed_denied_names(): a process-singleton registry, seeded from the single shared source so
// production and tests cannot drift apart.
void seed_denied_keywords()
{
PluginAuditManager& mgr = PluginAuditManager::instance();
for (const auto& keyword : PluginAuditManager::default_denied_path_keywords())
mgr.add_denied_path_keyword(keyword);
}
} // namespace } // namespace
TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit]") TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit]")
@@ -91,7 +81,7 @@ TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit
} }
} }
TEST_CASE("Plugin audit deny beats allowed roots", "[audit]") TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption", "[audit]")
{ {
ScopedDataDir data_dir_guard("plugin-audit-deny"); ScopedDataDir data_dir_guard("plugin-audit-deny");
seed_denied_names(); seed_denied_names();
@@ -101,8 +91,8 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
// config and the token would otherwise be reachable simply by living inside it. // config and the token would otherwise be reachable simply by living inside it.
mgr.add_global_allowed_root(data_dir()); mgr.add_global_allowed_root(data_dir());
// Enter a plugin context. The deny must hold even inside a globally allowed root. // Enter a plugin context. The deny must hold in Loading mode, which every scope runs in.
ScopedPluginAuditContext ctx("test_plugin", ""); ScopedPluginAuditContext ctx("test_plugin", "", PluginAuditManager::AuditMode::Loading);
const fs::path conf = fs::path(data_dir()) / (SLIC3R_APP_KEY ".conf"); const fs::path conf = fs::path(data_dir()) / (SLIC3R_APP_KEY ".conf");
const fs::path token = fs::path(data_dir()) / secret_constants::USER_SECRET_FILENAME; const fs::path token = fs::path(data_dir()) / secret_constants::USER_SECRET_FILENAME;
@@ -113,13 +103,6 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
CHECK(decision.allowed); CHECK(decision.allowed);
} }
SECTION("a file outside the allowed root is blocked for reads as well as writes")
{
AuditDecision decision = mgr.check_open((fs::path(data_dir()).parent_path() / "outside.txt").string(), "r");
CHECK_FALSE(decision.allowed);
CHECK(decision.reason == "outside allowed root");
}
SECTION("writing the app config is blocked despite data_dir() being allowed") SECTION("writing the app config is blocked despite data_dir() being allowed")
{ {
AuditDecision decision = mgr.check_open(conf.string(), "w"); AuditDecision decision = mgr.check_open(conf.string(), "w");
@@ -127,14 +110,16 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
CHECK(decision.reason == "denied filename"); CHECK(decision.reason == "denied filename");
} }
SECTION("reading the app config is blocked despite the allowed root") SECTION("reading the app config is blocked even though Loading exempts reads")
{ {
// Without the deny, a read in Loading mode short-circuits to allow. The deny sits above
// that exemption, so this must still be blocked.
AuditDecision decision = mgr.check_open(conf.string(), "r"); AuditDecision decision = mgr.check_open(conf.string(), "r");
CHECK_FALSE(decision.allowed); CHECK_FALSE(decision.allowed);
CHECK(decision.reason == "denied filename"); CHECK(decision.reason == "denied filename");
} }
SECTION("reading the cloud refresh token is blocked") SECTION("reading the cloud refresh token is blocked in Loading mode")
{ {
AuditDecision decision = mgr.check_open(token.string(), "r"); AuditDecision decision = mgr.check_open(token.string(), "r");
CHECK_FALSE(decision.allowed); CHECK_FALSE(decision.allowed);
@@ -165,7 +150,7 @@ TEST_CASE("Plugin audit deny beats a plugin's own scoped root", "[audit]")
const fs::path plugin_dir = fs::path(data_dir()) / "plugins" / "test_plugin"; const fs::path plugin_dir = fs::path(data_dir()) / "plugins" / "test_plugin";
fs::create_directories(plugin_dir); fs::create_directories(plugin_dir);
ScopedPluginAuditContext ctx("test_plugin", ""); ScopedPluginAuditContext ctx("test_plugin", "", PluginAuditManager::AuditMode::Loading);
mgr.add_scoped_allowed_root(plugin_dir); mgr.add_scoped_allowed_root(plugin_dir);
SECTION("the plugin's own non-denied file opens for read and write") SECTION("the plugin's own non-denied file opens for read and write")
@@ -200,139 +185,3 @@ TEST_CASE("Plugin audit does not constrain non-plugin code", "[audit]")
CHECK(mgr.check_open(conf.string(), "w").allowed); CHECK(mgr.check_open(conf.string(), "w").allowed);
CHECK(mgr.check_open(conf.string(), "r").allowed); CHECK(mgr.check_open(conf.string(), "r").allowed);
} }
TEST_CASE("Plugin audit denies secret/certificate/config-like paths by keyword", "[audit]")
{
seed_denied_keywords();
const PluginAuditManager& mgr = PluginAuditManager::instance();
SECTION("a 'secrets' directory component is denied")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/secrets/api_key.json")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/secret/token.txt")));
}
SECTION("a 'certificate(s)' directory component is denied")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/cert/slicer_base64.cer")));
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/certificates/ca.pem")));
}
SECTION("a 'conf'/'config' directory or file component is denied")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/conf/settings.json")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/config/settings.json")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/plugin.conf")));
}
SECTION("matching is case-insensitive")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/SECRETS/token.txt")));
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/CertBundle/ca.pem")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/CONFIG.JSON")));
}
SECTION("matching is not limited to the base name -- any ancestor component counts")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/data/secrets/nested/deep/file.txt")));
}
SECTION("an unrelated path is not denied")
{
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/output/model.gcode")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/storage/state.json")));
}
SECTION("an empty path is not denied")
{
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path()));
}
}
TEST_CASE("Plugin audit is_denied_path combines the filename and keyword registries", "[audit]")
{
seed_denied_names();
seed_denied_keywords();
const PluginAuditManager& mgr = PluginAuditManager::instance();
SECTION("a filename-registry match is denied")
{
CHECK(mgr.is_denied_path(fs::path(SLIC3R_APP_KEY ".conf")));
}
SECTION("a keyword-registry match is denied")
{
CHECK(mgr.is_denied_path(fs::path("/plugin/secrets/token.txt")));
}
SECTION("a path matching neither registry is not denied")
{
CHECK_FALSE(mgr.is_denied_path(fs::path("/plugin/output/model.gcode")));
}
}
TEST_CASE("Plugin audit a read-only allowed root blocks writes but not reads", "[audit]")
{
ScopedDataDir data_dir_guard("plugin-audit-readonly");
ScopedResourcesDir resources_dir_guard("plugin-audit-readonly-resources");
seed_denied_names();
seed_denied_keywords();
PluginAuditManager& mgr = PluginAuditManager::instance();
mgr.add_global_allowed_root(resources_dir(), /*allow_write=*/false);
ScopedPluginAuditContext ctx("test_plugin", "");
const fs::path readonly_file = fs::path(resources_dir()) / "profiles" / "vendor.json";
SECTION("a read inside the read-only root is allowed")
{
CHECK(mgr.check_open(readonly_file.string(), "r").allowed);
}
SECTION("a write inside the read-only root is blocked")
{
AuditDecision decision = mgr.check_open(readonly_file.string(), "w");
CHECK_FALSE(decision.allowed);
CHECK(decision.reason == "outside allowed root");
}
SECTION("a create inside the read-only root is blocked")
{
AuditDecision decision = mgr.check_path_access(readonly_file, /*is_write=*/true);
CHECK_FALSE(decision.allowed);
}
SECTION("the bundled cert underneath the read-only root is denied even for reads")
{
const fs::path cert = fs::path(resources_dir()) / "cert" / "slicer_base64.cer";
AuditDecision decision = mgr.check_open(cert.string(), "r");
CHECK_FALSE(decision.allowed);
CHECK(decision.reason == "denied path keyword");
}
}
TEST_CASE("Plugin audit a scoped root can also be registered read-only", "[audit]")
{
ScopedDataDir data_dir_guard("plugin-audit-scoped-readonly");
seed_denied_names();
seed_denied_keywords();
PluginAuditManager& mgr = PluginAuditManager::instance();
const fs::path readonly_dir = fs::path(data_dir()) / "readonly_scope";
fs::create_directories(readonly_dir);
ScopedPluginAuditContext ctx("test_plugin", "");
mgr.add_scoped_allowed_root(readonly_dir, /*allow_write=*/false);
SECTION("a read inside the scoped read-only root is allowed")
{
CHECK(mgr.check_open((readonly_dir / "vendor.json").string(), "r").allowed);
}
SECTION("a write inside the scoped read-only root is blocked")
{
CHECK_FALSE(mgr.check_open((readonly_dir / "vendor.json").string(), "w").allowed);
}
}

View File

@@ -112,22 +112,6 @@ 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.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.fs_read == state.permissions.fs_read);
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
// the cloud's latest version against the installed version instead of the stale header, so an // the cloud's latest version against the installed version instead of the stale header, so an