OrcaSlicer latest
OrcaSlicer
Loading...
Searching...
No Matches
Slic3r::PluginAuditManager Class Reference

#include </home/runner/work/OrcaSlicer/OrcaSlicer/src/slic3r/plugin/PluginAuditManager.hpp>

Collaboration diagram for Slic3r::PluginAuditManager:

Public Types

using PermissionPrompt = std::function<bool(const std::string& event_name, const std::vector<std::string>& targets)>
using PermissionMessagePrompt = std::function<bool(const std::string& message)>

Public Member Functions

void install_hook ()
void set_current_plugin (const std::string &plugin_key)
std::string current_plugin () const
void clear_current_plugin ()
void set_current_capability (const std::string &capability_name)
std::string current_capability () const
void clear_current_capability ()
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)
void add_denied_filename (const std::string &filename)
bool is_denied_filename (const boost::filesystem::path &candidate) const
void add_denied_path_keyword (const std::string &keyword)
bool is_denied_path_keyword (const boost::filesystem::path &candidate) const
bool is_denied_path (const boost::filesystem::path &candidate) const
AuditDecision check_path_access (const boost::filesystem::path &candidate, bool is_write)
AuditDecision check_open (const std::string &path, const std::string &mode)
bool request_permissions (const std::string &plugin_key, const PluginPermissions &requested)
bool request_network_permissions (const std::string &plugin_key, const std::vector< std::string > &urls)
void report_violation (const AuditViolation &violation)
bool audit_denial_pending () const
void clear_audit_denial ()
void clear_last_violation ()
bool last_violation (AuditViolation &violation) const
void record_root_frame ()
bool covered_by_root_frame () const
bool releasing_frames () const

Static Public Member Functions

static PluginAuditManager & instance ()
static std::vector< std::string > default_denied_filenames ()
static std::vector< std::string > default_denied_path_keywords ()
static std::string normalize_network_url (const std::string &url)
static bool network_permission_matches (const std::vector< std::string > &granted, const std::string &target)
static bool is_stdlib_file (const std::string &filename)
static void set_permission_prompt_for_testing (PermissionPrompt prompt)
static void set_permission_message_prompt_for_testing (PermissionMessagePrompt prompt)

Public Attributes

bool verbose_events = true

Private Member Functions

 PluginAuditManager ()=default
void release_frames (std::vector< PyObject * > &frames)

Static Private Member Functions

static int audit_hook (const char *event, PyObject *args, void *user_data)
static bool is_stdlib_path (const boost::filesystem::path &path)

Private Attributes

std::mutex m_mutex
std::vector< AllowedRoot > m_global_allowed_roots
std::vector< std::string > m_denied_filenames
std::vector< std::string > m_denied_path_keywords

Static Private Attributes

static std::string m_current_plugin_key = ""
static std::string m_current_capability_name = ""
static std::vector< AllowedRoot > m_scoped_allowed_roots
static std::vector< PyObject * > m_root_frames
static int m_releasing_frames = 0
static bool m_audit_denial_pending = false
static bool m_has_last_violation = false
static AuditViolation m_last_violation

Friends

class ScopedPluginAuditContext

Detailed Description

Member Typedef Documentation

◆ PermissionMessagePrompt

using Slic3r::PluginAuditManager::PermissionMessagePrompt = std::function<bool(const std::string& message)>

◆ PermissionPrompt

using Slic3r::PluginAuditManager::PermissionPrompt = std::function<bool(const std::string& event_name, const std::vector<std::string>& targets)>

Constructor & Destructor Documentation

◆ PluginAuditManager()

Slic3r::PluginAuditManager::PluginAuditManager ( )
privatedefault

Referenced by audit_hook(), and instance().

Here is the caller graph for this function:

Member Function Documentation

◆ add_denied_filename()

void Slic3r::PluginAuditManager::add_denied_filename ( const std::string & filename)
578{
579 if (filename.empty())
580 return;
581
582 std::lock_guard<std::mutex> lock(m_mutex);
583 m_denied_filenames.push_back(filename);
584 BOOST_LOG_TRIVIAL(info) << "[AUDIT] Denied filename: " << filename;
585}
std::vector< std::string > m_denied_filenames
Definition PluginAuditManager.hpp:256
std::mutex m_mutex
Definition PluginAuditManager.hpp:253
@ lock
Definition GLGizmoEmboss.cpp:209

References m_denied_filenames, and m_mutex.

Referenced by install_hook().

Here is the caller graph for this function:

◆ add_denied_path_keyword()

void Slic3r::PluginAuditManager::add_denied_path_keyword ( const std::string & keyword)
630{
631 if (keyword.empty())
632 return;
633
634 std::string lower = keyword;
635 std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return std::tolower(c); });
636
637 std::lock_guard<std::mutex> lock(m_mutex);
638 m_denied_path_keywords.push_back(lower);
639 BOOST_LOG_TRIVIAL(info) << "[AUDIT] Denied path keyword: " << lower;
640}
std::vector< std::string > m_denied_path_keywords
Definition PluginAuditManager.hpp:257

References m_denied_path_keywords, and m_mutex.

Referenced by install_hook().

Here is the caller graph for this function:

◆ add_global_allowed_root()

void Slic3r::PluginAuditManager::add_global_allowed_root ( const boost::filesystem::path & root,
bool allow_write = true )
554{
555 if (root.empty())
556 return;
557
558 std::lock_guard<std::mutex> lock(m_mutex);
559 m_global_allowed_roots.push_back({root, allow_write});
560 BOOST_LOG_TRIVIAL(info) << "[AUDIT] Global allowed root: " << root.string() << " allow_write=" << allow_write;
561}
std::vector< AllowedRoot > m_global_allowed_roots
Definition PluginAuditManager.hpp:255

References m_global_allowed_roots, and m_mutex.

Referenced by install_hook().

Here is the caller graph for this function:

◆ add_scoped_allowed_root()

void Slic3r::PluginAuditManager::add_scoped_allowed_root ( const boost::filesystem::path & root,
bool allow_write = true )
564{
565 if (root.empty())
566 return;
567
568 m_scoped_allowed_roots.push_back({root, allow_write});
569 BOOST_LOG_TRIVIAL(info) << "[AUDIT] Scoped allowed root for plugin " << current_plugin() << ": " << root.string()
570 << " allow_write=" << allow_write;
571}
static std::vector< AllowedRoot > m_scoped_allowed_roots
Definition PluginAuditManager.hpp:245
std::string current_plugin() const
Definition PluginAuditManager.cpp:543

References current_plugin(), and m_scoped_allowed_roots.

Referenced by Slic3r::PySlicingPipelinePluginCapabilityTrampoline::execute().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ audit_denial_pending()

bool Slic3r::PluginAuditManager::audit_denial_pending ( ) const
830{ return m_audit_denial_pending; }
static bool m_audit_denial_pending
Definition PluginAuditManager.hpp:248

References m_audit_denial_pending.

Referenced by Slic3r::invoke_script_execute().

Here is the caller graph for this function:

◆ audit_hook()

int Slic3r::PluginAuditManager::audit_hook ( const char * event,
PyObject * args,
void * user_data )
staticprivate
1765{
1766 auto* mgr = static_cast<PluginAuditManager*>(user_data);
1767 if (!mgr)
1768 return 0;
1769
1770 std::string event_name(event ? event : "");
1771
1772 if (event_name.empty())
1773 return 0;
1774
1775 // Verbose logging of every audit event (can be noisy)
1776 if (mgr->verbose_events) {
1777 BOOST_LOG_TRIVIAL(debug) << "[AUDIT EVENT] " << event_name;
1778 }
1779
1780 if (mgr->current_plugin().empty()) {
1781 BOOST_LOG_TRIVIAL(trace) << "[AUDIT] event=" << event_name << " bypassed (no plugin context)";
1782 return 0;
1783 }
1784
1785 // the open function can take in different flags that determine if it is a read or readwrite.
1786 const AuditEventCategory event_type =
1787 event_name == "open" ? PluginAuditDetail::open_category(args) : event_category(event_name);
1788 if (event_type == AuditEventCategory::None)
1789 return 0;
1790
1791 // Root frames whose operation has returned are let go once this event is decided, so that a
1792 // finalizer they run re-enters a hook whose held frames are already consistent.
1793 std::vector<PyObject*> returned_frames;
1795 const int result = PluginAuditDetail::audit_plugin_event(*mgr, event_name, event_type, args);
1796 mgr->release_frames(returned_frames);
1797 return result;
1798}
static std::vector< PyObject * > m_root_frames
Definition PluginAuditManager.hpp:246
void take_returned_frames(std::vector< PyObject * > &held, std::vector< PyObject * > &dropped)
Definition PluginAuditManager.cpp:992
AuditEventCategory open_category(PyObject *args)
Definition PluginAuditManager.cpp:1045
int audit_plugin_event(PluginAuditManager &mgr, const std::string &event_name, AuditEventCategory event_type, PyObject *args)
Definition PluginAuditManager.cpp:1488
static AuditEventCategory event_category(const std::string &event_name)
Definition PluginAuditManager.cpp:132
void trace(unsigned int level, const char *message)
Definition utils.cpp:247
AuditEventCategory
Definition PluginAuditManager.hpp:43
@ None
Definition PluginAuditManager.hpp:44

References PluginAuditManager(), Slic3r::PluginAuditDetail::audit_plugin_event(), current_plugin(), Slic3r::event_category(), m_root_frames, Slic3r::None, Slic3r::PluginAuditDetail::open_category(), release_frames(), Slic3r::PluginAuditDetail::take_returned_frames(), Slic3r::trace(), and verbose_events.

Referenced by install_hook().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ check_open()

AuditDecision Slic3r::PluginAuditManager::check_open ( const std::string & path,
const std::string & mode )
778{
779 const bool is_write = mode.find('w') != std::string::npos || mode.find('a') != std::string::npos ||
780 mode.find('+') != std::string::npos || mode.find('x') != std::string::npos;
781 return check_path_access(boost::filesystem::path(path_str), is_write);
782}
AuditDecision check_path_access(const boost::filesystem::path &candidate, bool is_write)
Definition PluginAuditManager.cpp:713

References check_path_access().

Here is the call graph for this function:

◆ check_path_access()

AuditDecision Slic3r::PluginAuditManager::check_path_access ( const boost::filesystem::path & candidate,
bool is_write )
714{
715 if (path.empty())
716 return {true, ""};
717
718 std::string plugin_key = current_plugin();
719 if (plugin_key.empty())
720 return {true, ""}; // not running inside a plugin context
721
722 // Denied filenames/keywords are checked before the allowed roots. The app config and the
723 // cloud refresh token live directly inside data_dir(), which is a global allowed root, and
724 // the bundled TLS client cert lives inside resources_dir(), a read-only global allowed
725 // root, so a deny placed any lower would be unreachable.
726 if (is_denied_filename(path)) {
727 BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << path.string() << " is_write=" << is_write
728 << " plugin=" << plugin_key << " reason=denied filename";
729 return {false, "denied filename"};
730 }
731 if (is_denied_path_keyword(path)) {
732 BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << path.string() << " is_write=" << is_write
733 << " plugin=" << plugin_key << " reason=denied path keyword";
734 return {false, "denied path keyword"};
735 }
736
737 namespace fs = boost::filesystem;
738 fs::path candidate = path;
739
740 // Resolve relative paths against the current working directory
741 if (candidate.is_relative()) {
742 boost::system::error_code ec;
743 fs::path absolute_candidate = fs::absolute(candidate, ec);
744 if (!ec)
745 candidate = absolute_candidate;
746 }
747
748 // Importing Python modules reads their source and may inspect package directories. These
749 // reads are normal interpreter work, not plugin data access; keep the exemption read-only
750 // and exclude third-party packages from the stdlib roots.
751 if (!is_write && is_stdlib_path(candidate))
752 return {true, ""};
753
754 // A root that doesn't allow writes only matches a read-shaped request; a write/create/
755 // delete-shaped one falls through to "outside allowed root" for that root even though the
756 // path is physically inside it.
757 for (const auto& root : m_scoped_allowed_roots) {
758 if ((!is_write || root.allow_write) && is_inside_allowed_root(candidate, root.path)) {
759 return {true, ""};
760 }
761 }
762
763 {
764 std::lock_guard<std::mutex> lock(m_mutex);
765 for (const auto& root : m_global_allowed_roots) {
766 if ((!is_write || root.allow_write) && is_inside_allowed_root(candidate, root.path)) {
767 return {true, ""};
768 }
769 }
770 }
771
772 BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << candidate.string() << " is_write=" << is_write
773 << " plugin=" << plugin_key;
774 return {false, "outside allowed root"};
775}
static bool is_stdlib_path(const boost::filesystem::path &path)
Definition PluginAuditManager.cpp:1717
bool is_denied_filename(const boost::filesystem::path &candidate) const
Definition PluginAuditManager.cpp:605
bool is_denied_path_keyword(const boost::filesystem::path &candidate) const
Definition PluginAuditManager.cpp:665
bool is_inside_allowed_root(const boost::filesystem::path &candidate, const boost::filesystem::path &allowed_root)
Definition PluginAuditManager.cpp:463

References current_plugin(), is_denied_filename(), is_denied_path_keyword(), Slic3r::is_inside_allowed_root(), is_stdlib_path(), m_global_allowed_roots, m_mutex, and m_scoped_allowed_roots.

Referenced by check_open().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ clear_audit_denial()

void Slic3r::PluginAuditManager::clear_audit_denial ( )
832{ m_audit_denial_pending = false; }

References m_audit_denial_pending.

Referenced by Slic3r::invoke_script_execute().

Here is the caller graph for this function:

◆ clear_current_capability()

void Slic3r::PluginAuditManager::clear_current_capability ( )
551{ m_current_capability_name.clear(); }
static std::string m_current_capability_name
Definition PluginAuditManager.hpp:244

References m_current_capability_name.

◆ clear_current_plugin()

void Slic3r::PluginAuditManager::clear_current_plugin ( )
545{ m_current_plugin_key.clear(); }
static std::string m_current_plugin_key
Definition PluginAuditManager.hpp:243

References m_current_plugin_key.

Referenced by Slic3r::invoke_script_execute().

Here is the caller graph for this function:

◆ clear_last_violation()

void Slic3r::PluginAuditManager::clear_last_violation ( )
835{
836 m_has_last_violation = false;
837 m_last_violation = AuditViolation{};
838}
static bool m_has_last_violation
Definition PluginAuditManager.hpp:249
static AuditViolation m_last_violation
Definition PluginAuditManager.hpp:250

References m_has_last_violation, and m_last_violation.

◆ covered_by_root_frame()

bool Slic3r::PluginAuditManager::covered_by_root_frame ( ) const
1678{
1679 if (m_root_frames.empty())
1680 return false;
1681
1682 // Only stdlib frames may sit between the event and the operation: plugin or library code
1683 // the operation calls back into (an os.walk onerror, a urllib handler) acts on its own.
1684 for (PyObject* frame : PluginAuditDetail::stack_frames()) {
1685 if (std::find(m_root_frames.begin(), m_root_frames.end(), frame) != m_root_frames.end())
1686 return true;
1688 return false;
1689 }
1690 return false;
1691}
bool is_stdlib_frame(PyObject *frame)
Definition PluginAuditManager.cpp:965
std::vector< PyObject * > stack_frames()
Definition PluginAuditManager.cpp:975

References Slic3r::PluginAuditDetail::is_stdlib_frame(), m_root_frames, and Slic3r::PluginAuditDetail::stack_frames().

Referenced by Slic3r::PluginAuditDetail::audit_plugin_event().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ current_capability()

std::string Slic3r::PluginAuditManager::current_capability ( ) const

◆ current_plugin()

◆ default_denied_filenames()

std::vector< std::string > Slic3r::PluginAuditManager::default_denied_filenames ( )
static
588{
589 // AppConfig::config_path() picks .conf vs .ini on the USE_JSON_CONFIG ifdef and the app key
590 // on its mode, and is a non-static member we cannot call without an instance. Denying all
591 // four names is cheaper and more robust than replicating that; the two unused names cost one
592 // string comparison each. Keep the plugin install-state sidecar here too: it controls which
593 // packages load on startup and must remain host-managed. Single-sourced so install_hook() and
594 // the tests seed from the same list and cannot drift apart.
595 return {
596 SLIC3R_APP_KEY ".conf",
597 GCODEVIEWER_APP_KEY ".conf",
598 SLIC3R_APP_KEY ".ini",
599 GCODEVIEWER_APP_KEY ".ini",
601 ".install_state.json",
602 };
603}
#define GCODEVIEWER_APP_KEY
Definition libslic3r.h:13
constexpr const char * USER_SECRET_FILENAME
Definition OrcaCloudServiceAgent.hpp:69

References GCODEVIEWER_APP_KEY, and Slic3r::secret_constants::USER_SECRET_FILENAME.

Referenced by install_hook().

Here is the caller graph for this function:

◆ default_denied_path_keywords()

std::vector< std::string > Slic3r::PluginAuditManager::default_denied_path_keywords ( )
static
643{
644 // Broad, categorical rules on top of the exact-name is_denied_filename registry: a plugin
645 // must never be able to reach a secret, a certificate, or a configuration file just because
646 // it happens to live inside an otherwise-allowed root (e.g. the bundled TLS client cert at
647 // resources_dir()/cert/..., which would become reachable the moment resources_dir() is
648 // granted as a read-only allowed root). Match as whole path components, not substrings, so
649 // imports such as numpy/__config__.py and stdlib configparser.py remain usable. Inside
650 // data_dir() they match below that root only; see is_denied_path_keyword().
651 return {"secret", "secrets", "cert", "certs", "certificate", "certificates", "conf", "config"};
652}

Referenced by install_hook().

Here is the caller graph for this function:

◆ install_hook()

void Slic3r::PluginAuditManager::install_hook ( )
1801{
1802 // Identifies stdlib frames for operation roots and stdlib paths for read-only access.
1804
1805 // data_dir() is the primary globally-allowed root during plugin execution: read+write. It
1806 // covers the plugin system's own workflow needs -- each plugin's storage folder
1807 // (data_dir()/orca_plugins) and the installed/system profile cache (data_dir()/system) --
1808 // without a separate, narrower grant for either (G-code plugins additionally get the temp
1809 // G-code folder via a scoped root, see SlicingPipelinePluginCapabilityTrampoline).
1811
1812 // resources_dir() holds the app's bundled, shared assets (installed system profiles, the
1813 // bundled TLS client cert, web assets). Plugins may read from it -- e.g. inspecting bundled
1814 // profiles -- but must never write into the shared, potentially multi-user app install, so
1815 // it is granted read-only. The bundled cert itself stays unreachable regardless, via the
1816 // "cert" denied-path keyword seeded below.
1817 add_global_allowed_root(resources_dir(), /*allow_write=*/false);
1818
1819 // The user's app config and cloud credentials live directly inside data_dir(), and plugin
1820 // install state lives beneath it, so the root just granted would otherwise expose them to
1821 // plugins. Deny them by name.
1822 //
1823 // Seeded here rather than by each secret's owner because install_hook() runs during lazy
1824 // interpreter init and therefore provably precedes any plugin bytecode, whereas
1825 // OrcaCloudServiceAgent::set_config_dir runs during networking init — neither strictly
1826 // precedes the other, and if Orca cloud never initializes an owner-registered token deny
1827 // would never exist at all. default_denied_filenames() is the single source of that list
1828 // (see its comment for why all four config names are denied); the tests seed from it too.
1829 for (const auto& name : default_denied_filenames())
1830 add_denied_filename(name);
1831
1832 // Categorical denies on top of the exact-name list above: a path a plugin reaches must not
1833 // have a "secret", "cert"(ificate), or "conf"(ig) component below data_dir() -- or anywhere
1834 // in the path outside data_dir(), whose own location may legitimately contain one (Flatpak
1835 // keeps it under .../config/OrcaSlicer). This holds regardless of which allowed root the
1836 // path happens to sit inside. default_denied_path_keywords() is the single source of this
1837 // list; the tests seed from it too.
1838 for (const auto& keyword : default_denied_path_keywords())
1839 add_denied_path_keyword(keyword);
1840
1841 if (PySys_AddAuditHook(audit_hook, this) < 0) {
1842 BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install CPython audit hook";
1843 return;
1844 }
1845 BOOST_LOG_TRIVIAL(info) << "[AUDIT] CPython audit hook installed successfully";
1846}
static std::vector< std::string > default_denied_filenames()
Definition PluginAuditManager.cpp:587
static std::vector< std::string > default_denied_path_keywords()
Definition PluginAuditManager.cpp:642
static int audit_hook(const char *event, PyObject *args, void *user_data)
Definition PluginAuditManager.cpp:1764
void add_denied_path_keyword(const std::string &keyword)
Definition PluginAuditManager.cpp:629
void add_denied_filename(const std::string &filename)
Definition PluginAuditManager.cpp:577
void add_global_allowed_root(const boost::filesystem::path &root, bool allow_write=true)
Definition PluginAuditManager.cpp:553
void collect_stdlib_dirs()
Definition PluginAuditManager.cpp:935
const std::string & data_dir()
Definition utils.cpp:365
const std::string & resources_dir()
Definition utils.cpp:296

References add_denied_filename(), add_denied_path_keyword(), add_global_allowed_root(), audit_hook(), Slic3r::PluginAuditDetail::collect_stdlib_dirs(), Slic3r::data_dir(), default_denied_filenames(), default_denied_path_keywords(), and Slic3r::resources_dir().

Referenced by Slic3r::PythonInterpreter::initialize().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ instance()

◆ is_denied_filename()

bool Slic3r::PluginAuditManager::is_denied_filename ( const boost::filesystem::path & candidate) const
606{
607 // Match on the base name alone, with no path resolution. Traversal is handled for free,
608 // because filename() of data_dir()/plugins/../OrcaSlicer.conf is already "OrcaSlicer.conf",
609 // and the prefix rule covers the .bak/.tmp companions that hold the same secrets plus Windows
610 // alternate data streams ("OrcaSlicer.conf:stream"). A plugin that launders a denied file
611 // through a symlink, a hardlink, a subprocess, or a Windows 8.3 short name is out of scope
612 // (see the design doc): this blocks direct access, not an actively evasive plugin.
613 const std::string filename = candidate.filename().string();
614 if (filename.empty())
615 return false;
616
617 std::lock_guard<std::mutex> lock(m_mutex);
618 for (const auto& denied : m_denied_filenames) {
619 if (boost::algorithm::istarts_with(filename, denied))
620 return true;
621 }
622 return false;
623}

References m_denied_filenames, and m_mutex.

Referenced by check_path_access(), and is_denied_path().

Here is the caller graph for this function:

◆ is_denied_path()

bool Slic3r::PluginAuditManager::is_denied_path ( const boost::filesystem::path & candidate) const
705{
706 return is_denied_filename(candidate) || is_denied_path_keyword(candidate);
707}

References is_denied_filename(), and is_denied_path_keyword().

Referenced by Slic3r::PluginAuditDetail::audit_plugin_event().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ is_denied_path_keyword()

bool Slic3r::PluginAuditManager::is_denied_path_keyword ( const boost::filesystem::path & candidate) const
666{
667 namespace fs = boost::filesystem;
668
669 const fs::path canon = canonical_for_comparison(candidate);
670
671 // The app's data folder can itself sit below a component named like one of the keywords
672 // without being one: Flatpak keeps it under <home>/.var/app/<app-id>/config/OrcaSlicer, and
673 // the sandbox's home layout is not ours to change. A keyword above data_dir() therefore says
674 // nothing about what a plugin may reach inside it, so inside data_dir() the keywords apply to
675 // the suffix below data_dir() only. Everywhere else -- resources_dir() with the bundled
676 // certificates, a plugin's own folders, arbitrary files -- the whole path is still checked,
677 // and so is a keyword under data_dir() itself.
678 fs::path canonical_data_dir;
679 if (!data_dir().empty())
680 canonical_data_dir = canonical_for_comparison(data_dir());
681
682 fs::path scope = canon;
683 fs::path suffix;
684 if (!canonical_data_dir.empty() && suffix_inside_root(canon, canonical_data_dir, suffix))
685 scope = suffix;
686
687 std::lock_guard<std::mutex> lock(m_mutex);
688 if (m_denied_path_keywords.empty())
689 return false;
690
691 for (const auto& component : scope) {
692 std::string name = component.string();
693 if (name.empty())
694 continue;
695 std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return std::tolower(c); });
696 for (const auto& keyword : m_denied_path_keywords) {
697 if (name == keyword || (keyword == "conf" && has_denied_config_extension(name)))
698 return true;
699 }
700 }
701 return false;
702}
bool suffix_inside_root(const boost::filesystem::path &canonical_candidate, const boost::filesystem::path &canonical_root, boost::filesystem::path &suffix)
Definition PluginAuditManager.cpp:429
boost::filesystem::path canonical_for_comparison(const boost::filesystem::path &path)
Definition PluginAuditManager.cpp:374
static bool has_denied_config_extension(std::string name)
Definition PluginAuditManager.cpp:654
bool empty(const BoundingBoxBase< PointType, PointsType > &bb)
Definition BoundingBox.hpp:273

References Slic3r::data_dir(), Slic3r::empty(), Slic3r::has_denied_config_extension(), m_denied_path_keywords, and m_mutex.

Referenced by check_path_access(), and is_denied_path().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ is_stdlib_file()

bool Slic3r::PluginAuditManager::is_stdlib_file ( const std::string & filename)
static
1696{
1697 // Frozen stdlib modules (os, posixpath, codecs, ...) carry a pseudo filename.
1698 if (boost::algorithm::starts_with(filename, "<frozen "))
1699 return true;
1700 if (filename.empty() || filename.front() == '<')
1701 return false;
1702
1703 // Both sides are resolved: co_filename follows the Python home as it was given, which may
1704 // be a symlink.
1705 const auto [it, inserted] = PluginAuditDetail::s_stdlib_file_verdicts.try_emplace(filename, false);
1706 if (inserted) {
1707 const boost::filesystem::path file(filename);
1708 const auto inside_any = [&file](const std::vector<boost::filesystem::path>& dirs) {
1709 return std::any_of(dirs.begin(), dirs.end(),
1710 [&file](const boost::filesystem::path& dir) { return is_inside_allowed_root(file, dir); });
1711 };
1712 it->second = inside_any(PluginAuditDetail::s_stdlib_dirs) && !inside_any(PluginAuditDetail::s_site_package_dirs);
1713 }
1714 return it->second;
1715}
static std::vector< boost::filesystem::path > s_stdlib_dirs
Definition PluginAuditManager.cpp:931
static std::vector< boost::filesystem::path > s_site_package_dirs
Definition PluginAuditManager.cpp:932
static std::unordered_map< std::string, bool > s_stdlib_file_verdicts
Definition PluginAuditManager.cpp:933

References Slic3r::PluginAuditDetail::s_site_package_dirs, Slic3r::PluginAuditDetail::s_stdlib_dirs, and Slic3r::PluginAuditDetail::s_stdlib_file_verdicts.

Referenced by Slic3r::PluginAuditDetail::is_stdlib_frame().

Here is the caller graph for this function:

◆ is_stdlib_path()

bool Slic3r::PluginAuditManager::is_stdlib_path ( const boost::filesystem::path & path)
staticprivate
1718{
1719 if (path.empty())
1720 return false;
1721
1722 const auto inside_any = [&path](const std::vector<boost::filesystem::path>& dirs) {
1723 return std::any_of(dirs.begin(), dirs.end(),
1724 [&path](const boost::filesystem::path& dir) { return is_inside_allowed_root(path, dir); });
1725 };
1727}

References Slic3r::PluginAuditDetail::s_site_package_dirs, and Slic3r::PluginAuditDetail::s_stdlib_dirs.

Referenced by check_path_access().

Here is the caller graph for this function:

◆ last_violation()

bool Slic3r::PluginAuditManager::last_violation ( AuditViolation & violation) const
841{
843 return false;
844
845 violation = m_last_violation;
846 return true;
847}

References m_has_last_violation, and m_last_violation.

◆ network_permission_matches()

bool Slic3r::PluginAuditManager::network_permission_matches ( const std::vector< std::string > & granted,
const std::string & target )
static
297{
298 if (target.empty())
299 return false;
300
301 const bool target_is_url = target.find("://") != std::string::npos;
302 const std::string target_url = normalize_network_url(target);
303 if (target_is_url && target_url.empty()) {
304 // A non-http(s) URL has no network identity we can generalize; only an exact grant
305 // recorded by an earlier approval stays valid, so an unrelated scheme never matches.
306 return std::find(granted.begin(), granted.end(), target) != granted.end();
307 }
308
309 const std::string target_host =
310 target_is_url ? std::string{} : without_ipv6_brackets(network_host_from_target(target));
311 if (!target_is_url && target_host.empty())
312 return false;
313
314 for (const std::string& entry : granted) {
315 // Preserve an exact grant recorded by an older release, e.g. a full URL with a query.
316 if (entry == target)
317 return true;
318
319 const std::string entry_url = normalize_network_url(entry);
320 if (target_is_url) {
321 // Canonical scheme://host grants are host-wide. Older grants that persisted a full
322 // URL remain scoped to that exact URL, preserving the operation-root URL check.
323 if (!entry_url.empty() && entry_url == entry && entry_url == target_url)
324 return true;
325 continue;
326 }
327
328 if (!entry_url.empty()) {
329 // A bare host carries no scheme: only a canonical scheme://host grant is host-wide.
330 // A grant that persisted a full URL stays scoped to that URL, so it must not
331 // authorize raw socket access to its whole host. Normalization lowercases the scheme
332 // and host, so a canonical grant compares equal to its lowercased form.
333 if (ascii_lower(entry) == entry_url) {
334 const std::string entry_host = entry_url.substr(entry_url.find("://") + 3);
335 if (without_ipv6_brackets(entry_host) == target_host)
336 return true;
337 }
338 } else if (without_ipv6_brackets(network_host_from_target(entry)) == target_host) {
339 // Legacy persisted address tuples, e.g. "('host', 443)".
340 return true;
341 }
342 }
343
344 return false;
345}
static std::string normalize_network_url(const std::string &url)
Definition PluginAuditManager.cpp:272
static std::string ascii_lower(std::string value)
Definition PluginAuditManager.cpp:180
static std::string network_host_from_target(const std::string &target)
Definition PluginAuditManager.cpp:232
static std::string without_ipv6_brackets(std::string host)
Definition PluginAuditManager.cpp:194

References Slic3r::ascii_lower(), Slic3r::network_host_from_target(), normalize_network_url(), and Slic3r::without_ipv6_brackets().

Referenced by Slic3r::PluginAuditDetail::has_network_permission().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ normalize_network_url()

std::string Slic3r::PluginAuditManager::normalize_network_url ( const std::string & url)
static
273{
274 const size_t scheme_end = url.find("://");
275 if (scheme_end == std::string::npos || scheme_end == 0)
276 return {};
277
278 const std::string scheme = ascii_lower(url.substr(0, scheme_end));
279 if (scheme != "http" && scheme != "https")
280 return {};
281
282 const size_t authority_start = scheme_end + 3;
283 const size_t authority_end = url.find_first_of("/?#", authority_start);
284 const std::string authority = url.substr(authority_start, authority_end == std::string::npos
285 ? std::string::npos
286 : authority_end - authority_start);
287
288 const std::string host = host_from_authority(authority);
289 if (host.empty())
290 return {};
291
292 return scheme + "://" + host;
293}
static std::string host_from_authority(std::string authority)
Definition PluginAuditManager.cpp:203

References Slic3r::ascii_lower(), and Slic3r::host_from_authority().

Referenced by Slic3r::bind_python_api(), network_permission_matches(), and request_network_permissions().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ record_root_frame()

void Slic3r::PluginAuditManager::record_root_frame ( )
1667{
1668 PyObject* frame = reinterpret_cast<PyObject*>(PyEval_GetFrame()); // borrowed reference
1669 if (!frame || !PluginAuditDetail::is_stdlib_frame(frame))
1670 return;
1671 if (std::find(m_root_frames.begin(), m_root_frames.end(), frame) != m_root_frames.end())
1672 return; // glob.glob and glob.glob/2 share a frame
1673 Py_INCREF(frame);
1674 m_root_frames.push_back(frame);
1675}

References Slic3r::PluginAuditDetail::is_stdlib_frame(), and m_root_frames.

Referenced by Slic3r::PluginAuditDetail::audit_plugin_event().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ release_frames()

void Slic3r::PluginAuditManager::release_frames ( std::vector< PyObject * > & frames)
private
1730{
1731 if (frames.empty())
1732 return;
1733
1734 // A finalizer's own denial must neither replace the PermissionError of the event being
1735 // decided nor leave a stale denial behind for the next call.
1736 PyObject* const error = PyErr_GetRaisedException();
1737 const bool denial_pending = m_audit_denial_pending;
1738 const bool has_violation = m_has_last_violation;
1739 const AuditViolation violation = m_last_violation;
1740
1741 std::vector<PyObject*> released;
1742 released.swap(frames);
1744 for (PyObject* frame : released)
1745 Py_DECREF(frame);
1747
1748 m_audit_denial_pending = denial_pending;
1749 m_has_last_violation = has_violation;
1750 m_last_violation = violation;
1751 PyErr_SetRaisedException(error);
1752}
static int m_releasing_frames
Definition PluginAuditManager.hpp:247

References m_audit_denial_pending, m_has_last_violation, m_last_violation, and m_releasing_frames.

Referenced by Slic3r::ScopedPluginAuditContext::~ScopedPluginAuditContext(), and audit_hook().

Here is the caller graph for this function:

◆ releasing_frames()

bool Slic3r::PluginAuditManager::releasing_frames ( ) const
1693{ return m_releasing_frames > 0; }

References m_releasing_frames.

Referenced by Slic3r::PluginAuditDetail::decide_audited_event().

Here is the caller graph for this function:

◆ report_violation()

void Slic3r::PluginAuditManager::report_violation ( const AuditViolation & violation)
821{
822 m_last_violation = violation;
825
826 BOOST_LOG_TRIVIAL(warning) << "[AUDIT BLOCKED] plugin=" << violation.plugin_key << " event=" << violation.event_name
827 << " reason=" << violation.reason;
828}

References Slic3r::AuditViolation::event_name, m_audit_denial_pending, m_has_last_violation, m_last_violation, Slic3r::AuditViolation::plugin_key, and Slic3r::AuditViolation::reason.

◆ request_network_permissions()

bool Slic3r::PluginAuditManager::request_network_permissions ( const std::string & plugin_key,
const std::vector< std::string > & urls )
798{
799 if (plugin_key.empty() || urls.empty())
800 return true;
801
802 // Reduce every request to scheme://host before comparing or persisting it, so a URL with a
803 // path or query cannot create a second grant for the same host.
804 PluginPermissions requested;
805 for (const std::string& url : urls) {
806 const std::string normalized = normalize_network_url(url);
807 if (normalized.empty()) {
808 BOOST_LOG_TRIVIAL(warning) << "[AUDIT] Invalid network permission request for plugin " << plugin_key << ": " << url;
809 return false;
810 }
811 if (std::find(requested.network_http.begin(), requested.network_http.end(), normalized) == requested.network_http.end())
812 requested.network_http.push_back(normalized);
813 }
814
815 // network_permission_matches() also applies the recorded host grant to raw socket events, so
816 // one HTTP grant covers both event layers without prompting for each list separately.
817 return request_permissions(plugin_key, requested);
818}
bool request_permissions(const std::string &plugin_key, const PluginPermissions &requested)
Definition PluginAuditManager.cpp:1545

References Slic3r::PluginPermissions::network_http, normalize_network_url(), and request_permissions().

Here is the call graph for this function:

◆ request_permissions()

bool Slic3r::PluginAuditManager::request_permissions ( const std::string & plugin_key,
const PluginPermissions & requested )
1546{
1547 if (plugin_key.empty())
1548 return true;
1549
1550 PluginDescriptor descriptor;
1551 if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
1552 return false;
1553
1554 PluginInstallState state;
1555 read_install_state(boost::filesystem::path(descriptor.plugin_root), state);
1556
1557 // Per bucket, keep only the targets that no earlier grant already covers. Dropping these
1558 // before prompting keeps a repeated declaration from asking again for what is on disk.
1559 struct MissingBucket {
1560 AuditEventCategory category;
1561 std::vector<std::string> targets;
1562 };
1563 std::vector<MissingBucket> missing_buckets;
1564
1565 // A network target counts as granted when either network list covers it, matched the same
1566 // way the audit hook matches it (canonical scheme://host, bare host, legacy address tuple),
1567 // so a declaration that is already reachable is not asked for again.
1568 const auto collect_missing = [&missing_buckets, &state](AuditEventCategory category,
1569 const std::vector<std::string>& requested_targets,
1570 const std::vector<std::string>& granted_targets) {
1571 // Only the filesystem buckets accept a tree grant. Network targets match by host and the
1572 // remaining buckets verbatim.
1573 const bool tree_grant_bucket =
1575 MissingBucket bucket{category, {}};
1576 for (const std::string& requested_target : requested_targets) {
1577 if (requested_target.empty())
1578 continue;
1579 // A relative wildcard names a directory relative to the working directory the request
1580 // is made in, while the audit hook reports absolute targets. Resolve it once, here, so
1581 // the prompt shows and the sidecar stores the scope the grant actually covers.
1582 const std::string target = tree_grant_bucket
1583 ? PluginAuditDetail::absolute_tree_grant(requested_target)
1584 : requested_target;
1585 // A saved wildcard already covers a more specific request. Deduplicate inside the
1586 // request verbatim only: two requested entries may cover different sets.
1587 const bool already_granted =
1588 is_network_category(category)
1589 ? PluginAuditDetail::has_network_permission(state.permissions, target)
1590 : PluginAuditDetail::has_permission(category, /*by_event=*/false, granted_targets, target);
1591
1592 if (!already_granted && std::find(bucket.targets.begin(), bucket.targets.end(), target) == bucket.targets.end())
1593 bucket.targets.push_back(target);
1594 }
1595 if (!bucket.targets.empty())
1596 missing_buckets.push_back(std::move(bucket));
1597 };
1598
1599 collect_missing(AuditEventCategory::FsRead, requested.fs_read, state.permissions.fs_read);
1600 collect_missing(AuditEventCategory::FsReadWrite, requested.fs_readwrite, state.permissions.fs_readwrite);
1601 collect_missing(AuditEventCategory::Http, requested.network_http, state.permissions.network_http);
1602 collect_missing(AuditEventCategory::Socket, requested.network_socket, state.permissions.network_socket);
1603 collect_missing(AuditEventCategory::ProcessCreate, requested.process, state.permissions.process);
1604 collect_missing(AuditEventCategory::Threading, requested.threading, state.permissions.threading);
1605
1606 if (missing_buckets.empty())
1607 return true;
1608
1609 const auto build_message = [](const std::string& plugin_name, const std::vector<MissingBucket>& buckets) {
1610 wxString message;
1611 for (const MissingBucket& bucket : buckets) {
1612 wxString targets;
1613 for (const std::string& target : bucket.targets)
1614 targets += wxString::FromUTF8(target.c_str()) + "\n";
1615
1616 if (!message.IsEmpty())
1617 message += "\n\n";
1618 message += PluginAuditDetail::audit_message(bucket.category, wxString::FromUTF8(plugin_name.c_str()),
1619 wxEmptyString, targets, bucket.targets.size());
1620 }
1621 return message;
1622 };
1623
1624 bool granted = false;
1626 // Tests run without a wxApp to host the dialog: hand the seam the message it would show.
1628 build_message(descriptor.name, missing_buckets).utf8_string());
1629 } else if (wxTheApp == nullptr || GUI::wxGetApp().is_closing()) {
1630 return false;
1631 } else if (wxIsMainThread()) {
1632 granted = PluginAuditDetail::prompt_for_permission_message(build_message(descriptor.name, missing_buckets));
1633 } else {
1634 auto result = std::make_shared<std::promise<bool>>();
1635 auto future = result->get_future();
1636 GUI::wxGetApp().CallAfter([result, plugin_name = descriptor.name, missing_buckets, build_message]() {
1637 result->set_value(PluginAuditDetail::prompt_for_permission_message(build_message(plugin_name, missing_buckets)));
1638 });
1639 granted = future.get();
1640 }
1641
1642 if (!granted)
1643 return false;
1644
1645 // The dialog can stay open while another grant is persisted; re-read before saving so this
1646 // request adds to the current state instead of replacing it.
1647 PluginInstallState latest_state;
1648 if (read_install_state(boost::filesystem::path(descriptor.plugin_root), latest_state))
1649 state = std::move(latest_state);
1650
1652
1653 for (const MissingBucket& bucket : missing_buckets) {
1654 std::vector<std::string>* permission_list =
1655 PluginAuditDetail::permission_list_for(bucket.category, /*by_event=*/false, state.permissions);
1656 if (!permission_list)
1657 continue;
1658 for (const std::string& target : bucket.targets)
1659 if (std::find(permission_list->begin(), permission_list->end(), target) == permission_list->end())
1660 permission_list->push_back(target);
1661 }
1662
1663 return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
1664}
static PluginManager & instance()
Definition PluginManager.cpp:74
std::vector< std::string > * permission_list_for(AuditEventCategory category, bool by_event, PluginPermissions &permissions)
Definition PluginAuditManager.cpp:1101
void seed_install_metadata(const PluginDescriptor &descriptor, PluginInstallState &state)
Definition PluginAuditManager.cpp:1238
bool prompt_for_permission_message(const wxString &message)
Definition PluginAuditManager.cpp:1355
std::string absolute_tree_grant(const std::string &grant)
Definition PluginAuditManager.cpp:1165
bool has_network_permission(const PluginPermissions &permissions, const std::string &target)
Definition PluginAuditManager.cpp:1223
bool has_permission(AuditEventCategory category, bool by_event, const std::vector< std::string > &granted, const std::string &target)
Definition PluginAuditManager.cpp:1206
wxString audit_message(AuditEventCategory category, const wxString &plugin_name, const wxString &event_name, const wxString &target_list, size_t target_count)
Definition PluginAuditManager.cpp:1289
static PluginAuditManager::PermissionMessagePrompt s_permission_message_prompt_for_testing
Definition PluginAuditManager.cpp:1378
static bool is_network_category(AuditEventCategory category)
Definition PluginAuditManager.cpp:171
@ FsRead
Definition PluginAuditManager.hpp:45
@ Socket
Definition PluginAuditManager.hpp:50
@ Http
Definition PluginAuditManager.hpp:49
@ ProcessCreate
Definition PluginAuditManager.hpp:51
@ Threading
Definition PluginAuditManager.hpp:53
@ FsReadWrite
Definition PluginAuditManager.hpp:46
bool write_install_state(const boost::filesystem::path &plugin_dir, const PluginInstallState &state)
Definition PluginFsUtils.cpp:859
bool read_install_state(const boost::filesystem::path &plugin_dir, PluginDescriptor &entry)
Definition PluginFsUtils.cpp:738

References Slic3r::PluginAuditDetail::absolute_tree_grant(), Slic3r::PluginAuditDetail::audit_message(), Slic3r::PluginPermissions::fs_read, Slic3r::PluginPermissions::fs_readwrite, Slic3r::FsRead, Slic3r::FsReadWrite, Slic3r::PluginAuditDetail::has_network_permission(), Slic3r::PluginAuditDetail::has_permission(), Slic3r::Http, Slic3r::PluginManager::instance(), Slic3r::is_network_category(), Slic3r::PluginDescriptor::name, Slic3r::PluginPermissions::network_http, Slic3r::PluginPermissions::network_socket, Slic3r::PluginAuditDetail::permission_list_for(), Slic3r::PluginInstallState::permissions, Slic3r::PluginDescriptor::plugin_root, Slic3r::PluginPermissions::process, Slic3r::ProcessCreate, Slic3r::PluginAuditDetail::prompt_for_permission_message(), Slic3r::read_install_state(), Slic3r::PluginAuditDetail::s_permission_message_prompt_for_testing, Slic3r::PluginAuditDetail::seed_install_metadata(), Slic3r::Socket, Slic3r::Threading, Slic3r::PluginPermissions::threading, and Slic3r::write_install_state().

Referenced by request_network_permissions().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ set_current_capability()

void Slic3r::PluginAuditManager::set_current_capability ( const std::string & capability_name)
547{ m_current_capability_name = capability_name; }

References m_current_capability_name.

Referenced by Slic3r::ScopedPluginAuditContext::ScopedPluginAuditContext(), and Slic3r::ScopedPluginAuditContext::~ScopedPluginAuditContext().

Here is the caller graph for this function:

◆ set_current_plugin()

void Slic3r::PluginAuditManager::set_current_plugin ( const std::string & plugin_key)
541{ m_current_plugin_key = plugin_key; }

References m_current_plugin_key.

Referenced by Slic3r::ScopedPluginAuditContext::ScopedPluginAuditContext(), and Slic3r::ScopedPluginAuditContext::~ScopedPluginAuditContext().

Here is the caller graph for this function:

◆ set_permission_message_prompt_for_testing()

void Slic3r::PluginAuditManager::set_permission_message_prompt_for_testing ( PermissionMessagePrompt prompt)
static

◆ set_permission_prompt_for_testing()

void Slic3r::PluginAuditManager::set_permission_prompt_for_testing ( PermissionPrompt prompt)
static
1755{
1757}
static PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing
Definition PluginAuditManager.cpp:1377

References Slic3r::PluginAuditDetail::s_permission_prompt_for_testing.

◆ ScopedPluginAuditContext

friend class ScopedPluginAuditContext
friend

Member Data Documentation

◆ m_audit_denial_pending

bool Slic3r::PluginAuditManager::m_audit_denial_pending = false
thread_localstaticprivate

◆ m_current_capability_name

std::string Slic3r::PluginAuditManager::m_current_capability_name = ""
thread_localstaticprivate

◆ m_current_plugin_key

std::string Slic3r::PluginAuditManager::m_current_plugin_key = ""
thread_localstaticprivate

◆ m_denied_filenames

std::vector<std::string> Slic3r::PluginAuditManager::m_denied_filenames
private

◆ m_denied_path_keywords

std::vector<std::string> Slic3r::PluginAuditManager::m_denied_path_keywords
private

◆ m_global_allowed_roots

std::vector<AllowedRoot> Slic3r::PluginAuditManager::m_global_allowed_roots
private

◆ m_has_last_violation

bool Slic3r::PluginAuditManager::m_has_last_violation = false
thread_localstaticprivate

◆ m_last_violation

AuditViolation Slic3r::PluginAuditManager::m_last_violation
thread_localstaticprivate

◆ m_mutex

std::mutex Slic3r::PluginAuditManager::m_mutex
mutableprivate

◆ m_releasing_frames

int Slic3r::PluginAuditManager::m_releasing_frames = 0
thread_localstaticprivate

Referenced by release_frames(), and releasing_frames().

◆ m_root_frames

std::vector< PyObject * > Slic3r::PluginAuditManager::m_root_frames
thread_localstaticprivate

◆ m_scoped_allowed_roots

std::vector< AllowedRoot > Slic3r::PluginAuditManager::m_scoped_allowed_roots
thread_localstaticprivate

◆ verbose_events

bool Slic3r::PluginAuditManager::verbose_events = true

Referenced by audit_hook().


The documentation for this class was generated from the following files: