#ifndef slic3r_PluginAuditManager_hpp_ #define slic3r_PluginAuditManager_hpp_ #include #include #include #include #include #include #include #include namespace Slic3r { struct AuditDecision { bool allowed = true; std::string reason; }; struct AuditViolation { std::string plugin_key; std::string event_name; std::string reason; }; // The set of CPython audit events PluginAuditManager recognizes, grouped by the kind of // operation they represent. None means the event isn't one audit_hook() acts on at all. enum class AuditEventCategory { None, FsRead, FsReadWrite, FsCreate, FsDelete, Http, Socket, ProcessCreate, Threading, }; // Returns true if candidate resolves to a path inside allowed_root. // Uses weakly_canonical and component-wise comparison to reject traversal attacks. bool is_inside_allowed_root(const boost::filesystem::path& candidate, const boost::filesystem::path& allowed_root); class PluginAuditManager { public: static PluginAuditManager& instance(); // Call once after Py_Initialize to install the global audit hook. void install_hook(); // --- current-plugin context (thread_local) --- void set_current_plugin(const std::string& plugin_key); std::string current_plugin() const; void clear_current_plugin(); // --- current-capability context (thread_local) --- // The capability whose method is currently executing, within the current plugin. Empty // while a plugin-wide call runs, and during capture (get_name/get_type), where the // capability has no cached name yet. void set_current_capability(const std::string& capability_name); std::string current_capability() const; void clear_current_capability(); // --- allowed-roots registry --- void add_global_allowed_root(const boost::filesystem::path& root); void add_scoped_allowed_root(const boost::filesystem::path& root); // --- denied-filenames registry --- // Filenames a plugin may never touch, in any directory, regardless of the 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. // // The comparison is case-insensitive on every platform, unlike the _WIN32-only iequals // in is_inside_allowed_root: the default macOS APFS configuration is case-insensitive // too, so `orcaslicer.conf` reaches the real file there. Over-blocking a genuinely // distinct name on Linux is the fail-safe direction and costs nothing real. void add_denied_filename(const std::string& filename); // The list install_hook() seeds into the deny registry: the app config (both app keys and // both extensions) and the cloud refresh token. Exposed so tests seed the exact same set // without a live interpreter, so the test and production seeding cannot drift apart. static std::vector default_denied_filenames(); // True when candidate's base name starts with a denied name (case-insensitive). No path // resolution: laundering a denied file through a symlink, hardlink, subprocess, or Windows // 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; // --- policy checks --- // Shared core for every audited filesystem event. The deny list is consulted above the // allowed roots, so a denied filename is blocked even when the file sits inside data_dir(), // which is itself a global allowed root. AuditDecision check_path_access(const boost::filesystem::path& candidate, bool is_write); 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& paths); 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; // --- call-site cascade cache --- // A single plugin action often fires several nested CPython audit events as it passes // through stdlib layers (urllib.request calling http.client calling socket, for example). // Once the user approves one event, every stdlib frame still on the stack for that call // is recorded here by (filename, function, first line) identity. A later event whose own // ancestor chain still contains one of those frames is the same logical action seen from // a deeper layer, so it is auto-approved instead of prompting again. bool has_approved_ancestor(const std::string& plugin_key, const std::vector& call_site_ids) const; void record_approved_call_sites(const std::string& plugin_key, const std::vector& call_site_ids); bool verbose_events = true; private: friend class ScopedPluginAuditContext; PluginAuditManager() = default; static int audit_hook(const char* event, PyObject* args, void* user_data); static thread_local std::string m_current_plugin_key; static thread_local std::string m_current_capability_name; static thread_local std::vector m_scoped_allowed_roots; static thread_local bool m_audit_denial_pending; static thread_local bool m_has_last_violation; static thread_local AuditViolation m_last_violation; // mutable: is_denied_filename() and has_approved_ancestor() are const queries that must lock. mutable std::mutex m_mutex; std::vector m_global_allowed_roots; std::vector m_denied_filenames; std::unordered_map> m_approved_call_sites; // plugin_key -> call-site ids }; // RAII guard that sets the current plugin key and capability name, restoring the previous // pair on scope exit. `capability_name` may be empty for calls that are not scoped to a // single capability. class ScopedPluginAuditContext { public: explicit ScopedPluginAuditContext( const std::string& plugin_key, const std::string& capability_name = {}); ~ScopedPluginAuditContext(); ScopedPluginAuditContext(const ScopedPluginAuditContext&) = delete; ScopedPluginAuditContext& operator=(const ScopedPluginAuditContext&) = delete; private: std::string m_previous_id; std::string m_previous_capability; std::vector m_previous_scoped_roots; }; } // namespace Slic3r #endif // slic3r_PluginAuditManager_hpp_