Compare commits

..

19 Commits

Author SHA1 Message Date
Kris Austin
9933cab59f build: drop the pkg-config requirement from the Windows build (#15469)
The FFmpeg camera view port made pkg-config a required build tool on
Windows. Windows does not ship one, so every Windows developer has to
install it before the build will configure:

  Could NOT find PkgConfig (missing: PKG_CONFIG_EXECUTABLE)
  Call Stack (most recent call first):
    CMakeLists.txt:480 (find_package)

Nothing on Windows needs it. FFmpeg there is a prebuilt zip unpacked
into the deps prefix, whose DLLs the top level CMakeLists already names
by exact soname. The version is fixed before configure runs, so
find_library against that prefix does the job, as on macOS.

Also drops the CI step that installed pkg-config, gated on !SELF_HOSTED
so it never ran on self-hosted runners, and re-comments the if(WIN32)
block that #15234 uncommented only for that find_package.
2026-09-01 09:19:58 +08:00
Kris Austin
5436e422b9 ci: build macOS with 3 parallel jobs instead of 1 (#15393) 2026-08-31 16:59:58 -03:00
Leo Lobato
ca903faa56 Fix uninitialized first_layer_time on CLI-sliced 3MF (#13429)
PartPlate::store_to_3mf_structure read first_layer_time from the indirect cali_bboxes_data struct,
which the GUI populates at Plater.cpp:10600 but the CLI never writes to. The result was uninitialized
memory leaking into slice_info.config

Read directly from get_slice_result()->initial_layer_time, which is populated by
GCodeProcessor::finalize() in both code paths and matches the pattern already used a few lines
above for gcode_prediction.

Also default-initialize PlateBBoxData::first_layer_time to 0.0f as a defense against any other consumer
reading it without an explicit write.
2026-08-31 16:36:55 -03:00
Kris Austin
fcf6f0a3a5 build: clear 54 warnings - dead private fields (#15423)
build: clear 54 dead private fields

54 of the 161 -Wunused-private-field warnings, across 31 files. These are
the ones needing no judgment. Each member is declared once and appears
nowhere else in src/, counting the .mm and .c sources as well as .cpp and
.hpp, so nothing writes them and nothing reads them. Every removal is a
whole line, and no declaration shares a line with another member.

The remaining 107 are left alone. Those members are mentioned elsewhere,
usually assigned and never read, where the fix might be deleting the
member or might be restoring a read that went missing.
2026-08-31 16:36:52 -03:00
Kris Austin
36740ffdd8 build: clear 53 warnings - discarded values and i18n markers (#15421)
build: clear 53 unused value warnings

49 of them are deliberate i18n markers. L(s) expands to s, so
L("Main Extruder"); is a string literal as a statement and its value is
discarded. The strings have to stay, because the real values come from
printers/*.json at runtime and xgettext cannot scan those. Each block is
now a static const char *const markers[], which uses the values rather
than discarding them. Extraction is unchanged: the xgettext invocation
from scripts/run_gettext.bat gives 76 msgids over the two marker files
before and after, with identical msgid and msgctxt sets.

The other 4 are statements with no effect. AMSItem.cpp:117 and :174
construct and drop a wxColour(255, 255, 255); AMS_TRAY_DEFAULT_COL is
that colour, and the line above already assigns it. UpgradePanel.cpp:865
reads a member and drops it.

wgtDeviceNozzleSelect.cpp:269 writes
if (item; auto ptr = m_nozzle_rack.lock()), which puts the null check in
the init-statement position where its value is discarded, so the check
never runs, and sGetNozzlePosId then dereferences item. Nothing reaches
that today, because the only sender of the event sets itself as the
event object and the dynamic_cast always succeeds. The check now runs.
2026-08-31 10:55:42 -03:00
Ian Chua
27e99ca713 feat: initial plugin auditing workflow (#14989)
# Description

This is an initial draft of the plugin audit workflow.

It focuses on the user experience and developer-facing permission
workflow. It does not yet include the complete implementation of every
operation that should be audited, such as the full filesystem,
networking, and process-spawning event coverage.

## User workflow

When a plugin is loaded:
1. The plugin’s register_capabilities() function is executed.
2. The plugin declares the permissions it requires.
3. OrcaSlicer displays a permission dialog listing the requested
resources.
4. If the user grants access:
  - The permission is persisted in the plugin’s .install_state.json.
  - Capability registration continues.
  - The plugin is materialized and loaded.
5. If the user denies access:
    - Plugin loading fails before capabilities are materialized.
    - on_load() is not called.
- The plugin’s install state is marked with "enabled": false to prevent
repeated automatic load attempts.

At runtime, if a plugin accesses a resource that was not approved during
loading, the audit hook displays another permission dialog. For
filesystem requests, the dialog identifies the requested filepath.
  - Granting access persists the permission and allows the operation.
  - Denying access raises a Python PermissionError.
- The error propagates to the host, which records the failure and
unloads the plugin.

Host-side traceback logging is performed outside the plugin audit
context so that logging does not generate additional permission dialogs.

## Developer-facing API

Plugins can declare filesystem read permissions through the new API:
```python

import orca
AUDIT_PATH = __file__


@orca.plugin
class ExamplePackage(orca.base):
   def register_capabilities(self):
        orca.request_permissions(
            fs_read=[AUDIT_PATH],
        )
        orca.register_capability(ExampleCapability)
```
orca.request_permissions() must be called from register_capabilities()
while the plugin is being loaded.

Currently supported permission:
orca.request_permissions(fs_read=[...])

The paths should be explicit filesystem paths that the plugin intends to
read. The host deduplicates repeated paths, presents the request after
registration completes, and persists granted paths in the plugin
install-state sidecar. This API is still experimental, and is by no
means the final implementation.

Support for additional permission categories, including filesystem write
access, networking, and process spawning, is reserved for subsequent
work.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

<img width="869" height="799" alt="image"
src="https://github.com/user-attachments/assets/8a5903cc-0cbb-45a8-b88a-706d6cba790f"
/>


## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-08-31 14:29:36 +08:00
Ian Chua
f9c415fc41 Merge branch 'main' into feat/plugin-auditing 2026-08-27 14:52:20 +08:00
Ian Chua
1e392e4437 fix: remove audit scope from plugin pages 2026-08-25 14:38:51 +08:00
Ian Chua
4c4eb9a94e Merge branch 'main' into feat/plugin-auditing 2026-08-25 13:46:18 +08:00
Ian Chua
83a00843a0 fix: revoke plugin permissions on install/update 2026-08-19 14:13:56 +08:00
Ian Chua
991c36d649 Merge branch 'feat/plugin-auditing' of https://github.com/OrcaSlicer/OrcaSlicer into feat/plugin-auditing 2026-08-14 14:49:54 +08:00
Ian Chua
f6f68573b6 fix: add resources folder to the allowed roots as readonly 2026-08-14 14:48:57 +08:00
Ian Chua
28d0d218ff Merge branch 'main' into feat/plugin-auditing 2026-08-14 12:33:35 +08:00
Ian Chua
d99f4c8164 Merge branch 'main' into feat/plugin-auditing 2026-08-14 12:33:08 +08:00
Ian Chua
ddd62e25df add audit hook for fs, network and processes 2026-08-13 19:16:18 +08:00
Ian Chua
35970f61dc fix: lift audit while reporting errors to log files 2026-07-29 16:52:16 +08:00
Ian Chua
2169b72c94 fix: update tests 2026-07-28 19:28:58 +08:00
Ian Chua
0cb56b3cfa Merge branch 'main' into feat/plugin-auditing 2026-07-28 18:48:58 +08:00
Ian Chua
81b06d68f1 feat: initial plugin auditing workflow 2026-07-28 17:11:54 +08:00
62 changed files with 1262 additions and 349 deletions

View File

@@ -151,7 +151,7 @@ jobs:
if [ -z "${{ vars.SELF_HOSTED }}" ]; then if [ -z "${{ vars.SELF_HOSTED }}" ]; then
brew install automake texinfo libtool pkgconf yasm nasm brew install automake texinfo libtool pkgconf yasm nasm
fi fi
./build_release_macos.sh -dx ${{ !vars.SELF_HOSTED && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15 ./build_release_macos.sh -dx ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a ${{ inputs.arch }} -t 10.15
(cd "${{ github.workspace }}/deps/build/${{ inputs.arch }}" && \ (cd "${{ github.workspace }}/deps/build/${{ inputs.arch }}" && \
find . -mindepth 1 -maxdepth 1 ! -name 'OrcaSlicer_dep' -exec rm -rf {} +) find . -mindepth 1 -maxdepth 1 ! -name 'OrcaSlicer_dep' -exec rm -rf {} +)

View File

@@ -145,7 +145,7 @@ jobs:
env: env:
ORCA_TESTS_BUILD_ONLY: ${{ inputs.arch == 'arm64' && '1' || '' }} ORCA_TESTS_BUILD_ONLY: ${{ inputs.arch == 'arm64' && '1' || '' }}
run: | run: |
./build_release_macos.sh -s -n -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15 ${{ inputs.arch == 'arm64' && '-T' || '' }} ./build_release_macos.sh -s -n -x ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a ${{ inputs.arch }} -t 10.15 ${{ inputs.arch == 'arm64' && '-T' || '' }}
- name: Pack unit tests mac - name: Pack unit tests mac
if: runner.os == 'macOS' && !inputs.macos-combine-only && inputs.arch == 'arm64' if: runner.os == 'macOS' && !inputs.macos-combine-only && inputs.arch == 'arm64'
@@ -204,7 +204,7 @@ jobs:
if: runner.os == 'macOS' && inputs.macos-combine-only if: runner.os == 'macOS' && inputs.macos-combine-only
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}
run: | run: |
./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a universal -t 10.15 ./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a universal -t 10.15
# Thanks to RaySajuuk, it's working now # Thanks to RaySajuuk, it's working now
- name: Sign app and notary - name: Sign app and notary
@@ -385,13 +385,6 @@ jobs:
dir "C:/Program Files (x86)/Windows Kits/10/Include" dir "C:/Program Files (x86)/Windows Kits/10/Include"
choco install nsis choco install nsis
- name: Install pkg-config
# FFmpeg is discovered via pkg-config (pkg_check_modules LIBAV in
# src/slic3r/CMakeLists.txt); the Windows runners don't ship it.
if: runner.os == 'Windows' && !vars.SELF_HOSTED
run: |
choco install pkgconfiglite -y
- name: Build slicer Win - name: Build slicer Win
if: runner.os == 'Windows' if: runner.os == 'Windows'
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}

View File

@@ -476,8 +476,7 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# WIN10SDK_PATH is used to point CMake to the WIN10 SDK installation directory. # WIN10SDK_PATH is used to point CMake to the WIN10 SDK installation directory.
# We pick it from environment if it is not defined in another way # We pick it from environment if it is not defined in another way
# ORCA: Removed Netfabb STL fixing service support in favor of CGAL. # ORCA: Removed Netfabb STL fixing service support in favor of CGAL.
if(WIN32) # if(WIN32)
find_package(PkgConfig REQUIRED)
# if(NOT DEFINED WIN10SDK_PATH) # if(NOT DEFINED WIN10SDK_PATH)
# if(DEFINED ENV{WIN10SDK_PATH}) # if(DEFINED ENV{WIN10SDK_PATH})
# set(WIN10SDK_PATH "$ENV{WIN10SDK_PATH}") # set(WIN10SDK_PATH "$ENV{WIN10SDK_PATH}")
@@ -513,7 +512,7 @@ if(WIN32)
# else() # else()
# message("Building without Win10 Netfabb STL fixing service support") # message("Building without Win10 Netfabb STL fixing service support")
# endif() # endif()
endif() # endif()
if (APPLE) if (APPLE)
message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}") message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}")

View File

@@ -8,7 +8,7 @@ SCRIPT_PATH=$(dirname "$(readlink -f "${0}")")
pushd "${SCRIPT_PATH}" > /dev/null pushd "${SCRIPT_PATH}" > /dev/null
function usage() { function usage() {
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-p][-r][-s][-t][-u][-l][-L][-M]" echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-p][-r][-s][-t][-u][-l][-L]"
echo " -1: limit builds to one core (where possible)" echo " -1: limit builds to one core (where possible)"
echo " -j N: limit builds to N cores (where possible)" echo " -j N: limit builds to N cores (where possible)"
echo " -b: build in Debug mode" echo " -b: build in Debug mode"
@@ -28,7 +28,6 @@ function usage() {
echo " -u: install system dependencies (asks for sudo password; build prerequisite)" echo " -u: install system dependencies (asks for sudo password; build prerequisite)"
echo " -l: use Clang instead of GCC (default: GCC)" echo " -l: use Clang instead of GCC (default: GCC)"
echo " -L: use ld.lld as linker (if available)" echo " -L: use ld.lld as linker (if available)"
echo " -M: use mold as linker (if available)"
echo "For a first use, you want to './${SCRIPT_NAME} -u'" echo "For a first use, you want to './${SCRIPT_NAME} -u'"
echo " and then './${SCRIPT_NAME} -dsi'" echo " and then './${SCRIPT_NAME} -dsi'"
echo "For a GitHub Actions-like Linux build locally, use './${SCRIPT_NAME} -g -istrlL'" echo "For a GitHub Actions-like Linux build locally, use './${SCRIPT_NAME} -g -istrlL'"
@@ -42,7 +41,7 @@ unset name
BUILD_DIR=build BUILD_DIR=build
BUILD_CONFIG=Release BUILD_CONFIG=Release
FORWARDED_ARGS=() FORWARDED_ARGS=()
while getopts ":1j:bcCdDeFghiprstulLM" opt ; do while getopts ":1j:bcCdDeFghiprstulL" opt ; do
case ${opt} in case ${opt} in
1 ) 1 )
export CMAKE_BUILD_PARALLEL_LEVEL=1 export CMAKE_BUILD_PARALLEL_LEVEL=1
@@ -119,10 +118,6 @@ while getopts ":1j:bcCdDeFghiprstulLM" opt ; do
USE_LLD="1" USE_LLD="1"
FORWARDED_ARGS+=("-L") FORWARDED_ARGS+=("-L")
;; ;;
M )
USE_MOLD="1"
FORWARDED_ARGS+=("-M")
;;
* ) * )
echo "Unknown argument '${opt}', aborting." echo "Unknown argument '${opt}', aborting."
exit 1 exit 1
@@ -140,11 +135,6 @@ if [[ -n "${CLEAN_DOCKER_IMAGE}" ]] && [[ -z "${USE_DOCKER}" ]] ; then
exit 1 exit 1
fi fi
if [[ -n "${USE_LLD}" ]] && [[ -n "${USE_MOLD}" ]] ; then
echo "Error: -L and -M are mutually exclusive."
exit 1
fi
function check_available_memory_and_disk() { function check_available_memory_and_disk() {
FREE_MEM_GB=$(free --gibi --total | grep 'Mem' | rev | cut --delimiter=" " --fields=1 | rev) FREE_MEM_GB=$(free --gibi --total | grep 'Mem' | rev | cut --delimiter=" " --fields=1 | rev)
MIN_MEM_GB=10 MIN_MEM_GB=10
@@ -514,18 +504,6 @@ if [[ -n "${USE_LLD}" ]] ; then
fi fi
fi fi
# Configure use of mold as the linker when requested
export CMAKE_MOLD_LINKER_ARGS=()
if [[ -n "${USE_MOLD}" ]] ; then
if command -v mold >/dev/null 2>&1 ; then
MOLD_BIN=$(command -v mold)
export CMAKE_MOLD_LINKER_ARGS=(-DCMAKE_LINKER="${MOLD_BIN}" -DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=mold -DCMAKE_SHARED_LINKER_FLAGS=-fuse-ld=mold -DCMAKE_MODULE_LINKER_FLAGS=-fuse-ld=mold)
else
echo "Error: mold not found. Please install the 'mold' package or omit -M."
exit 1
fi
fi
export CMAKE_CCACHE_ARGS=() export CMAKE_CCACHE_ARGS=()
CMAKE_CCACHE=${CMAKE_CCACHE:-} CMAKE_CCACHE=${CMAKE_CCACHE:-}
if [ -n "$CMAKE_CCACHE" ]; then if [ -n "$CMAKE_CCACHE" ]; then
@@ -558,7 +536,7 @@ if [[ -n "${BUILD_DEPS}" ]] ; then
BUILD_ARGS+=(-DCMAKE_BUILD_TYPE="${BUILD_CONFIG}") BUILD_ARGS+=(-DCMAKE_BUILD_TYPE="${BUILD_CONFIG}")
fi fi
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_MOLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}" print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}"
print_and_run cmake --build deps/$BUILD_DIR -j1 print_and_run cmake --build deps/$BUILD_DIR -j1
fi fi
@@ -578,7 +556,7 @@ if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then
BUILD_ARGS+=(-DORCA_UPDATER_SIG_KEY="${ORCA_UPDATER_SIG_KEY}") BUILD_ARGS+=(-DORCA_UPDATER_SIG_KEY="${ORCA_UPDATER_SIG_KEY}")
fi fi
print_and_run cmake -S . -B $BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_MOLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G "Ninja Multi-Config" \ print_and_run cmake -S . -B $BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G "Ninja Multi-Config" \
-DSLIC3R_PCH=${SLIC3R_PRECOMPILED_HEADERS} \ -DSLIC3R_PCH=${SLIC3R_PRECOMPILED_HEADERS} \
-DORCA_TOOLS=ON \ -DORCA_TOOLS=ON \
"${COLORED_OUTPUT}" \ "${COLORED_OUTPUT}" \

View File

@@ -4,7 +4,7 @@ set -e
set -o pipefail set -o pipefail
SECONDS=0 SECONDS=0
while getopts ":dpa:snt:xbc:i:1Tuh" opt; do while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
case "${opt}" in case "${opt}" in
d ) d )
export BUILD_TARGET="deps" export BUILD_TARGET="deps"
@@ -38,8 +38,8 @@ while getopts ":dpa:snt:xbc:i:1Tuh" opt; do
i ) i )
export CMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH:+$CMAKE_IGNORE_PREFIX_PATH;}$OPTARG" export CMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH:+$CMAKE_IGNORE_PREFIX_PATH;}$OPTARG"
;; ;;
1 ) j )
export CMAKE_BUILD_PARALLEL_LEVEL=1 export CMAKE_BUILD_PARALLEL_LEVEL="$OPTARG"
;; ;;
T ) T )
export BUILD_TESTS="1" export BUILD_TESTS="1"
@@ -58,7 +58,7 @@ while getopts ":dpa:snt:xbc:i:1Tuh" opt; do
echo " -b: Build without reconfiguring CMake" echo " -b: Build without reconfiguring CMake"
echo " -c: Set CMake build configuration, default is Release" echo " -c: Set CMake build configuration, default is Release"
echo " -i: Add a prefix to ignore during CMake dependency discovery (repeatable), defaults to /opt/local:/usr/local:/opt/homebrew" echo " -i: Add a prefix to ignore during CMake dependency discovery (repeatable), defaults to /opt/local:/usr/local:/opt/homebrew"
echo " -1: Use single job for building" echo " -j: Set the number of parallel build jobs (CMAKE_BUILD_PARALLEL_LEVEL)"
echo " -T: Build and run tests (set ORCA_TESTS_BUILD_ONLY=1 to build without running)" echo " -T: Build and run tests (set ORCA_TESTS_BUILD_ONLY=1 to build without running)"
exit 0 exit 0
;; ;;

View File

@@ -78,7 +78,7 @@ struct PlateBBoxData
int first_extruder = 0; int first_extruder = 0;
float nozzle_diameter = 0.4; float nozzle_diameter = 0.4;
std::string bed_type; std::string bed_type;
float first_layer_time; float first_layer_time = 0.0f;
// version 1: use view type ColorPrint (filament color) // version 1: use view type ColorPrint (filament color)
// version 2: use view type FilamentId (filament id) // version 2: use view type FilamentId (filament id)
int version = 2; int version = 2;

View File

@@ -165,7 +165,6 @@ public:
//BBS //BBS
unsigned int m_last_additional_fan_speed;
int m_last_bed_temperature; int m_last_bed_temperature;
bool m_last_bed_temperature_reached; bool m_last_bed_temperature_reached;
double m_lifted; double m_lifted;

View File

@@ -915,6 +915,18 @@ if (APPLE)
endif () endif ()
target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY}) target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include) target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include)
elseif (WIN32)
# Prebuilt shared FFmpeg from the deps install. Windows has no pkg-config,
# so resolve the import libraries out of the deps prefix directly; the DLLs
# are copied next to the executable by the top level CMakeLists.
find_library(LIBAVCODEC_LIBRARY NAMES avcodec PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBSWSCALE_LIBRARY NAMES swscale PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBAVUTIL_LIBRARY NAMES avutil PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY)
message(FATAL_ERROR "FFmpeg (avcodec/swscale/avutil) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps.")
endif ()
target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include)
else () else ()
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
libavcodec libavcodec

View File

@@ -55,7 +55,6 @@ private:
Label* m_text_label; Label* m_text_label;
wxStaticBitmap* m_icon_bitmap; wxStaticBitmap* m_icon_bitmap;
int m_target_size;
std::string m_icon_name; std::string m_icon_name;
ScalableBitmap m_icon; ScalableBitmap m_icon;
}; };

View File

@@ -60,7 +60,6 @@ class AboutDialog : public DPIDialog
wxHtmlWindow* m_html; wxHtmlWindow* m_html;
wxStaticBitmap* m_logo; wxStaticBitmap* m_logo;
int m_copy_rights_btn_id { wxID_ANY }; int m_copy_rights_btn_id { wxID_ANY };
int m_copy_version_btn_id { wxID_ANY };
public: public:
AboutDialog(); AboutDialog();

View File

@@ -93,7 +93,6 @@ private:
CenteredTitle* m_title_ctrl { nullptr }; CenteredTitle* m_title_ctrl { nullptr };
wxString m_titleText; wxString m_titleText;
wxAuiToolBarItem* m_account_item;
wxAuiToolBarItem* m_model_store_item; wxAuiToolBarItem* m_model_store_item;
//wxAuiToolBarItem *m_publish_item; //wxAuiToolBarItem *m_publish_item;

View File

@@ -74,12 +74,10 @@ private:
std::unordered_set<std::string> m_system_filament_types_set; std::unordered_set<std::string> m_system_filament_types_set;
std::set<std::string> m_visible_printers; std::set<std::string> m_visible_printers;
CreateType m_create_type; CreateType m_create_type;
Button * m_button_create = nullptr;
Button * m_button_cancel = nullptr; Button * m_button_cancel = nullptr;
ComboBox * m_filament_vendor_combobox = nullptr; ComboBox * m_filament_vendor_combobox = nullptr;
::CheckBox * m_can_not_find_vendor_checkbox = nullptr; ::CheckBox * m_can_not_find_vendor_checkbox = nullptr;
ComboBox * m_filament_type_combobox = nullptr; ComboBox * m_filament_type_combobox = nullptr;
ComboBox * m_exist_vendor_combobox = nullptr;
ComboBox * m_filament_preset_combobox = nullptr; ComboBox * m_filament_preset_combobox = nullptr;
TextInput * m_filament_custom_vendor_input = nullptr; TextInput * m_filament_custom_vendor_input = nullptr;
wxGridSizer * m_filament_presets_sizer = nullptr; wxGridSizer * m_filament_presets_sizer = nullptr;

View File

@@ -16,24 +16,27 @@ 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()
{ {
// Dynamic toolhead display names from JSON config — xgettext cannot scan these // Possible runtime values of tool_head_display_names, marked for extraction.
L("Main Extruder"); L("Main extruder"); L("main extruder"); static const char *const markers[] = {
L("Auxiliary Extruder"); L("Auxiliary extruder"); L("auxiliary extruder"); L("Main Extruder"), L("Main extruder"), L("main extruder"),
L("Left Extruder"); L("Left extruder"); L("left extruder"); L("Auxiliary Extruder"), L("Auxiliary extruder"), L("auxiliary extruder"),
L("Right Extruder"); L("Right extruder"); L("right extruder"); L("Left Extruder"), L("Left extruder"), L("left extruder"),
L("Main Nozzle"); L("Main nozzle"); L("main nozzle"); L("Right Extruder"), L("Right extruder"), L("right extruder"),
L("Auxiliary Nozzle"); L("Auxiliary nozzle"); L("auxiliary nozzle"); L("Main Nozzle"), L("Main nozzle"), L("main nozzle"),
L("Left Nozzle"); L("Left nozzle"); L("left nozzle"); L("Auxiliary Nozzle"), L("Auxiliary nozzle"), L("auxiliary nozzle"),
L("Right Nozzle"); L("Right nozzle"); L("right nozzle"); L("Left Nozzle"), L("Left nozzle"), L("left nozzle"),
L("Main Hotend"); L("Main hotend"); L("main hotend"); L("Right Nozzle"), L("Right nozzle"), L("right nozzle"),
L("Auxiliary Hotend"); L("Auxiliary hotend"); L("auxiliary hotend"); L("Main Hotend"), L("Main hotend"), L("main hotend"),
L("Left Hotend"); L("Left hotend"); L("left hotend"); L("Auxiliary Hotend"), L("Auxiliary hotend"), L("auxiliary hotend"),
L("Right Hotend"); L("Right hotend"); L("right hotend"); L("Left Hotend"), L("Left hotend"), L("left hotend"),
// standalone position words (short_name=true runtime results) L("Right Hotend"), L("Right hotend"), L("right hotend"),
L("main"); L("auxiliary"); // standalone position words (short_name=true runtime results)
L("Main"); L("Auxiliary"); L("main"), L("auxiliary"),
L("left"); L("right"); L("Main"), L("Auxiliary"),
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

@@ -199,7 +199,6 @@ private:
void OnRefreshButton(wxCommandEvent& event); void OnRefreshButton(wxCommandEvent& event);
private: private:
int saveTimes{0};
wxBoxSizer* mainSizer{nullptr}; wxBoxSizer* mainSizer{nullptr};
wxPanel* textPanel{nullptr}; wxPanel* textPanel{nullptr};
wxBoxSizer* textSizer{nullptr}; wxBoxSizer* textSizer{nullptr};

View File

@@ -122,7 +122,6 @@ private:
private: private:
int m_ext_nozzle_id = -1; int m_ext_nozzle_id = -1;
int m_rack_nozzle_id = -1; int m_rack_nozzle_id = -1;
bool m_isRefreshFinish = false;
bool findNozzleImage = false; bool findNozzleImage = false;
NozzleStatus m_nozzle_status = NOZZLE_STATUS_DC; NozzleStatus m_nozzle_status = NOZZLE_STATUS_DC;
@@ -154,7 +153,6 @@ private:
Label* m_diameter_label; Label* m_diameter_label;
Label* m_flowtype_label; Label* m_flowtype_label;
Label* m_type_label; Label* m_type_label;
ScalableButton* m_error_button{ nullptr };
Label* m_sn_label; Label* m_sn_label;
Label* m_version_label; Label* m_version_label;

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 (item; auto ptr = m_nozzle_rack.lock()) { if (auto ptr = m_nozzle_rack.lock(); item && ptr) {
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

@@ -72,7 +72,6 @@ public:
float m_model_z_offset{ 0.5f }; float m_model_z_offset{ 0.5f };
bool m_visible{ true }; bool m_visible{ true };
bool m_is_dark = false; bool m_is_dark = false;
bool m_fixed_screen_size{ false };
float m_scale_factor{ 1.0f }; float m_scale_factor{ 1.0f };
#if ENABLE_ACTUAL_SPEED_DEBUG #if ENABLE_ACTUAL_SPEED_DEBUG
ActualSpeedImguiWidget m_actual_speed_imgui_widget; ActualSpeedImguiWidget m_actual_speed_imgui_widget;

View File

@@ -327,7 +327,6 @@ private:
GLTexture m_icons_texture; GLTexture m_icons_texture;
bool m_icons_texture_dirty; bool m_icons_texture_dirty;
mutable GLTexture m_images_texture; mutable GLTexture m_images_texture;
mutable bool m_images_texture_dirty;
BackgroundTexture m_background_texture; BackgroundTexture m_background_texture;
GLTexture m_arrow_texture; GLTexture m_arrow_texture;
Layout m_layout; Layout m_layout;

View File

@@ -86,7 +86,6 @@ private:
boost::thread m_thread; boost::thread m_thread;
// Mutex and condition variable to synchronize m_thread with the UI thread. // Mutex and condition variable to synchronize m_thread with the UI thread.
std::mutex m_mutex; std::mutex m_mutex;
int m_generate_count;
// This map holds all translated description texts, so they can be easily referenced during layout calculations // This map holds all translated description texts, so they can be easily referenced during layout calculations
// etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect. // etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect.

View File

@@ -176,7 +176,6 @@ private:
// Use those values to disable selection of active extruders // Use those values to disable selection of active extruders
bool m_is_dark = false; bool m_is_dark = false;
bool is_osx{false};
int m_min_value; int m_min_value;
int m_max_value; int m_max_value;
int m_lower_value; int m_lower_value;
@@ -201,10 +200,6 @@ private:
void *m_one_layer_on_hover_id; void *m_one_layer_on_hover_id;
void *m_one_layer_off_id; void *m_one_layer_off_id;
void *m_one_layer_off_hover_id; void *m_one_layer_off_hover_id;
void* m_one_layer_on_light_id;
void* m_one_layer_on_hover_light_id;
void* m_one_layer_off_light_id;
void* m_one_layer_off_hover_light_id;
void* m_one_layer_on_dark_id; void* m_one_layer_on_dark_id;
void* m_one_layer_on_hover_dark_id; void* m_one_layer_on_hover_dark_id;
void* m_one_layer_off_dark_id; void* m_one_layer_off_dark_id;

View File

@@ -61,7 +61,6 @@ private:
::Button *m_button_year = nullptr; ::Button *m_button_year = nullptr;
::Button *m_button_month = nullptr; ::Button *m_button_month = nullptr;
::Button *m_button_all = nullptr; ::Button *m_button_all = nullptr;
::Label *m_switch_label = nullptr;
::StaticBox * m_type_panel = nullptr; ::StaticBox * m_type_panel = nullptr;
::Button * m_button_video = nullptr; ::Button * m_button_video = nullptr;

View File

@@ -86,11 +86,8 @@ private:
/* side tools */ /* side tools */
SideTools* m_side_tools{nullptr}; SideTools* m_side_tools{nullptr};
wxStaticBitmap* m_bitmap_printer_type;
wxStaticBitmap* m_bitmap_arrow; wxStaticBitmap* m_bitmap_arrow;
wxStaticText* m_staticText_printer_name;
wxStaticBitmap* m_bitmap_wifi_signal; wxStaticBitmap* m_bitmap_wifi_signal;
wxBoxSizer * m_side_tools_sizer;
SelectMachinePopup m_select_machine; SelectMachinePopup m_select_machine;
/* images */ /* images */
@@ -101,7 +98,6 @@ private:
wxBitmap m_printer_img; wxBitmap m_printer_img;
wxBitmap m_arrow_img; wxBitmap m_arrow_img;
int last_wifi_signal = -1;
int last_status; int last_status;
bool m_initialized { false }; bool m_initialized { false };
bool update_flag{false}; bool update_flag{false};

View File

@@ -88,7 +88,6 @@ private:
Button* m_task_name{ nullptr }; Button* m_task_name{ nullptr };
Button* m_status{ nullptr }; Button* m_status{ nullptr };
Button* m_action{ nullptr }; Button* m_action{ nullptr };
Button* m_stop_all_botton{nullptr};
// tip when no device // tip when no device
wxStaticText* m_tip_text{ nullptr }; wxStaticText* m_tip_text{ nullptr };

View File

@@ -79,7 +79,6 @@ private:
std::vector<wxBoxSizer *> m_row_col_boxsizer_list; std::vector<wxBoxSizer *> m_row_col_boxsizer_list;
std::vector<ButtonState*> m_result_icon_list; std::vector<ButtonState*> m_result_icon_list;
int m_last_cluster_num{-1}; int m_last_cluster_num{-1};
const int m_combox_width{50};
int m_combox_icon_width; int m_combox_icon_width;
int m_combox_icon_height; int m_combox_icon_height;
wxButton * m_image_button = nullptr; wxButton * m_image_button = nullptr;

View File

@@ -6470,7 +6470,7 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w
plate_data_item->filament_change_sequence = m_plate_list[i]->m_gcode_result->filament_change_sequence; plate_data_item->filament_change_sequence = m_plate_list[i]->m_gcode_result->filament_change_sequence;
plate_data_item->nozzle_change_sequence = m_plate_list[i]->m_gcode_result->nozzle_change_sequence; plate_data_item->nozzle_change_sequence = m_plate_list[i]->m_gcode_result->nozzle_change_sequence;
plate_data_item->optimal_assignment = m_plate_list[i]->m_gcode_result->optimal_assignment; plate_data_item->optimal_assignment = m_plate_list[i]->m_gcode_result->optimal_assignment;
plate_data_item->first_layer_time = std::to_string(m_plate_list[i]->cali_bboxes_data.first_layer_time); plate_data_item->first_layer_time = std::to_string(m_plate_list[i]->get_slice_result()->initial_layer_time);
Print *print = nullptr; Print *print = nullptr;
m_plate_list[i]->get_print((PrintBase **) &print, nullptr, nullptr); m_plate_list[i]->get_print((PrintBase **) &print, nullptr, nullptr);
if (print) { if (print) {

View File

@@ -128,7 +128,6 @@ private:
bool is_drag_mode(); bool is_drag_mode();
boost::shared_ptr<PrinterFileSystem> m_file_sys; boost::shared_ptr<PrinterFileSystem> m_file_sys;
bool m_file_sys_result{false};
std::string m_timestamp; std::string m_timestamp;
std::string m_tmp_path; std::string m_tmp_path;
std::vector<string> m_local_paths; std::vector<string> m_local_paths;

View File

@@ -33,7 +33,6 @@ class PhysicalPrinterDialog : public DPIDialog
Button* m_printhost_test_btn {nullptr}; Button* m_printhost_test_btn {nullptr};
Button* m_printhost_logout_btn {nullptr}; Button* m_printhost_logout_btn {nullptr};
Button* m_printhost_cafile_browse_btn {nullptr}; Button* m_printhost_cafile_browse_btn {nullptr};
Button* m_printhost_client_cert_browse_btn {nullptr};
Button* m_printhost_port_browse_btn {nullptr}; Button* m_printhost_port_browse_btn {nullptr};
RoundedRectangle* m_input_area {nullptr}; RoundedRectangle* m_input_area {nullptr};

View File

@@ -63,7 +63,6 @@ class ProjectPanel : public wxPanel
{ {
private: private:
std::atomic<bool> m_web_init_completed{false}; std::atomic<bool> m_web_init_completed{false};
bool m_reload_already = {false};
std::shared_ptr<std::atomic<bool>> m_reload_cancel_token{std::make_shared<std::atomic<bool>>(false)}; std::shared_ptr<std::atomic<bool>> m_reload_cancel_token{std::make_shared<std::atomic<bool>>(false)};
std::unique_ptr<boost::thread> m_reload_task; std::unique_ptr<boost::thread> m_reload_task;

View File

@@ -10,8 +10,6 @@ namespace Slic3r { namespace GUI {
class RecenterDialog : public DPIDialog class RecenterDialog : public DPIDialog
{ {
private: private:
wxStaticText* m_staticText_hint;
wxStaticBitmap* m_bitmap_home;
ScalableBitmap m_home_bmp; ScalableBitmap m_home_bmp;
wxString hint1; wxString hint1;
wxString hint2; wxString hint2;

View File

@@ -181,8 +181,6 @@ private:
PinCodePanel* m_panel_direct_connection{nullptr}; PinCodePanel* m_panel_direct_connection{nullptr};
wxWindow* m_placeholder_panel{nullptr}; wxWindow* m_placeholder_panel{nullptr};
HyperLink* m_hyperlink{nullptr}; // ORCA HyperLink* m_hyperlink{nullptr}; // ORCA
Label* m_ping_code_text{nullptr};
wxStaticBitmap* m_img_ping_code{nullptr};
wxBoxSizer * m_sizer_body{nullptr}; wxBoxSizer * m_sizer_body{nullptr};
wxBoxSizer * m_sizer_my_devices{nullptr}; wxBoxSizer * m_sizer_my_devices{nullptr};
wxBoxSizer * m_sizer_other_devices{nullptr}; wxBoxSizer * m_sizer_other_devices{nullptr};

View File

@@ -149,7 +149,6 @@ private:
ScalableBitmap* print_time{ nullptr }; ScalableBitmap* print_time{ nullptr };
wxStaticBitmap* weightimg{ nullptr }; wxStaticBitmap* weightimg{ nullptr };
ScalableBitmap* print_weight{ nullptr }; ScalableBitmap* print_weight{ nullptr };
wxBoxSizer* m_thumbnail_sizer{ nullptr };
ThumbnailPanel* m_thumbnail_panel{nullptr}; ThumbnailPanel* m_thumbnail_panel{nullptr};
wxPanel* m_panel_image{ nullptr }; wxPanel* m_panel_image{ nullptr };
wxBoxSizer* m_image_sizer{ nullptr }; wxBoxSizer* m_image_sizer{ nullptr };

View File

@@ -62,7 +62,6 @@ private:
bool m_is_in_sending_mode{ false }; bool m_is_in_sending_mode{ false };
bool m_is_rename_mode{ false }; bool m_is_rename_mode{ false };
bool enable_prepare_mode{ true }; bool enable_prepare_mode{ true };
bool m_need_adaptation_screen{ false };
bool m_export_3mf_cancel{ false }; bool m_export_3mf_cancel{ false };
bool m_is_canceled{ false }; bool m_is_canceled{ false };
bool m_tcp_try_connect{true}; bool m_tcp_try_connect{true};
@@ -81,7 +80,6 @@ private:
wxStaticBitmap* m_staticbitmap{ nullptr }; wxStaticBitmap* m_staticbitmap{ nullptr };
ThumbnailPanel* m_thumbnailPanel{ nullptr }; ThumbnailPanel* m_thumbnailPanel{ nullptr };
ComboBox* m_comboBox_printer{ nullptr }; ComboBox* m_comboBox_printer{ nullptr };
ComboBox* m_comboBox_bed{ nullptr };
Button* m_rename_button{ nullptr }; Button* m_rename_button{ nullptr };
Button* m_button_refresh{ nullptr }; Button* m_button_refresh{ nullptr };
Button* m_button_ensure{ nullptr }; Button* m_button_ensure{ nullptr };

View File

@@ -87,7 +87,6 @@ private:
std::unordered_map < uint32_t, std::vector<std::vector<FloatPoint>>> parts_triangles_; std::unordered_map < uint32_t, std::vector<std::vector<FloatPoint>>> parts_triangles_;
std::unordered_map < uint32_t, std::vector<std::vector<cv::Point>>> pick_parts_; std::unordered_map < uint32_t, std::vector<std::vector<cv::Point>>> pick_parts_;
std::unordered_map<uint32_t, PartState> parts_state_; std::unordered_map<uint32_t, PartState> parts_state_;
bool gl_inited_{false};
int zoom_percent_{100}; int zoom_percent_{100};
wxPoint offset_{0,0}; wxPoint offset_{0,0};
wxPoint drag_start_offset_{0,0}; wxPoint drag_start_offset_{0,0};

View File

@@ -307,8 +307,6 @@ private:
// wxBoxSizer * m_plate_combox_sizer = nullptr; // wxBoxSizer * m_plate_combox_sizer = nullptr;
wxBoxSizer * m_mode_combox_sizer = nullptr; wxBoxSizer * m_mode_combox_sizer = nullptr;
//wxStaticText * m_printer_title = nullptr; //wxStaticText * m_printer_title = nullptr;
wxStaticText * m_printer_device_name = nullptr;
wxStaticText * m_printer_is_map_title = nullptr;
CapsuleButton * m_colormap_btn = nullptr; CapsuleButton * m_colormap_btn = nullptr;
CapsuleButton * m_override_btn = nullptr; CapsuleButton * m_override_btn = nullptr;
@@ -326,7 +324,6 @@ private:
bool m_check_dirty_fialment = true; bool m_check_dirty_fialment = true;
bool m_expand_more_settings = true; bool m_expand_more_settings = true;
bool m_image_is_top = false;
const int LEFT_THUMBNAIL_SIZE_WIDTH = 100; const int LEFT_THUMBNAIL_SIZE_WIDTH = 100;
const int RIGHT_THUMBNAIL_SIZE_WIDTH = 300; const int RIGHT_THUMBNAIL_SIZE_WIDTH = 300;

View File

@@ -626,8 +626,6 @@ private:
bool m_rebuild_kinematics_page = false; bool m_rebuild_kinematics_page = false;
void update_input_shaper_menu(GCodeFlavor flavor); void update_input_shaper_menu(GCodeFlavor flavor);
ogStaticText* m_fff_print_host_upload_description_line {nullptr};
ogStaticText* m_sla_print_host_upload_description_line {nullptr};
std::vector<PageShp> m_pages_fff; std::vector<PageShp> m_pages_fff;
std::vector<PageShp> m_pages_sla; std::vector<PageShp> m_pages_sla;

View File

@@ -31,7 +31,6 @@ private:
wxStaticText *m_remaining_time_label; wxStaticText *m_remaining_time_label;
wxStaticText *m_explanation_label; wxStaticText *m_explanation_label;
wxButton *m_ok_button; wxButton *m_ok_button;
wxStaticBitmap *m_title_bitmap;
DECLARE_EVENT_TABLE() DECLARE_EVENT_TABLE()
}; };

View File

@@ -862,7 +862,6 @@ 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

@@ -112,7 +112,6 @@ private:
AppConfig m_appconfig_new; AppConfig m_appconfig_new;
wxWebView *m_browser; wxWebView *m_browser;
wxButton * m_TestBtn;
wxString m_SectionName; wxString m_SectionName;

View File

@@ -125,10 +125,6 @@ private:
wxMenuItem* m_edit_undo; wxMenuItem* m_edit_undo;
wxMenuItem* m_edit_redo; wxMenuItem* m_edit_redo;
wxMenuItem* m_edit_mode; wxMenuItem* m_edit_mode;
wxMenuItem* m_scroll_line_up;
wxMenuItem* m_scroll_line_down;
wxMenuItem* m_scroll_page_up;
wxMenuItem* m_scroll_page_down;
wxMenuItem* m_script_string; wxMenuItem* m_script_string;
wxMenuItem* m_script_integer; wxMenuItem* m_script_integer;
wxMenuItem* m_script_double; wxMenuItem* m_script_double;

View File

@@ -114,7 +114,6 @@ 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) {
@@ -171,7 +170,6 @@ 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

@@ -684,7 +684,6 @@ public:
private: private:
int m_nozzle_num = {1}; int m_nozzle_num = {1};
AMSRoadShowMode m_single_ext_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
AMSRoadShowMode m_left_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR}; AMSRoadShowMode m_left_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
AMSRoadShowMode m_right_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR}; AMSRoadShowMode m_right_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
bool m_selected = {false}; bool m_selected = {false};
@@ -693,7 +692,6 @@ private:
int m_right_road_length = {-1}; int m_right_road_length = {-1};
int m_passroad_width = {6}; int m_passroad_width = {6};
double m_radius = {4}; double m_radius = {4};
AMSPassRoadType m_pass_road_type = {AMSPassRoadType::AMS_ROAD_TYPE_NONE};
AMSPassRoadSTEP m_pass_road_left_step = {AMSPassRoadSTEP::AMS_ROAD_STEP_NONE}; AMSPassRoadSTEP m_pass_road_left_step = {AMSPassRoadSTEP::AMS_ROAD_STEP_NONE};
AMSPassRoadSTEP m_pass_road_right_step = {AMSPassRoadSTEP::AMS_ROAD_STEP_NONE}; AMSPassRoadSTEP m_pass_road_right_step = {AMSPassRoadSTEP::AMS_ROAD_STEP_NONE};
@@ -866,11 +864,7 @@ private:
AMSinfo m_info; AMSinfo m_info;
wxBoxSizer * sizer_can = {nullptr}; wxBoxSizer * sizer_can = {nullptr};
wxGridSizer* sizer_can_extra = { nullptr }; wxGridSizer* sizer_can_extra = { nullptr };
wxBoxSizer * sizer_humidity = { nullptr };
wxBoxSizer * sizer_item = { nullptr }; wxBoxSizer * sizer_item = { nullptr };
wxBoxSizer * sizer_can_middle = {nullptr};
wxBoxSizer * sizer_can_left = {nullptr};
wxBoxSizer * sizer_can_right = {nullptr};
AMSExtImage* m_ext_image = { nullptr }; //the ext image upon the ext ams AMSExtImage* m_ext_image = { nullptr }; //the ext image upon the ext ams
AMSExtText* m_ext_text = { nullptr }; //the ext text upon the ext ams AMSExtText* m_ext_text = { nullptr }; //the ext text upon the ext ams
}; };

View File

@@ -1014,8 +1014,12 @@ 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()) {
L("Cooling mode is suitable for printing PLA/PETG/TPU materials."); //some potential text, add i18n flags // Possible runtime values of special_cooling_text, marked for extraction.
L("Cooling mode is suitable for printing PLA/PETG/TPU materials and filters the chamber air."); static const char *const markers[] = {
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);
} }
} }
@@ -1028,9 +1032,13 @@ 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())
{ {
L_CONTEXT("Right(Aux)", "air_duct"); // Possible runtime values of func_text, marked for extraction.
L_CONTEXT("Right(Filter)", "air_duct"); static const char *const markers[] = {
L_CONTEXT("Left(Aux)", "air_duct"); L_CONTEXT("Right(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

@@ -109,7 +109,6 @@ public:
void decrease_fan_speeds(); void decrease_fan_speeds();
private: private:
int m_current_speeds; int m_current_speeds;
int m_target_speed;
int m_min_speeds; int m_min_speeds;
int m_max_speeds; int m_max_speeds;
ScalableBitmap m_bitmap_add; ScalableBitmap m_bitmap_add;

View File

@@ -204,7 +204,6 @@ private:
Label* m_caution; Label* m_caution;
wxTimer* m_refresh_timer {nullptr}; wxTimer* m_refresh_timer {nullptr};
size_t m_rack_event_token;
Button* m_cancel_btn; Button* m_cancel_btn;
Button* m_confirm_btn; Button* m_confirm_btn;
}; };

View File

@@ -7,11 +7,127 @@
#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
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -78,24 +194,21 @@ bool is_inside_allowed_root(const boost::filesystem::path& candidate, const boos
// ScopedPluginAuditContext // ScopedPluginAuditContext
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
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 PluginAuditManager::AuditMode PluginAuditManager::m_audit_mode = PluginAuditManager::AuditMode::Loading; thread_local std::vector<AllowedRoot> PluginAuditManager::m_scoped_allowed_roots;
thread_local std::vector<boost::filesystem::path> PluginAuditManager::m_scoped_allowed_roots; thread_local bool PluginAuditManager::m_audit_denial_pending = false;
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();
} }
@@ -103,7 +216,6 @@ 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);
} }
@@ -129,23 +241,24 @@ 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) void PluginAuditManager::add_global_allowed_root(const boost::filesystem::path& root, bool allow_write)
{ {
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); m_global_allowed_roots.push_back({root, allow_write});
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Global allowed root: " << root.string(); BOOST_LOG_TRIVIAL(info) << "[AUDIT] Global allowed root: " << root.string() << " allow_write=" << allow_write;
} }
void PluginAuditManager::add_scoped_allowed_root(const boost::filesystem::path& root) void PluginAuditManager::add_scoped_allowed_root(const boost::filesystem::path& root, bool allow_write)
{ {
if (root.empty()) if (root.empty())
return; return;
m_scoped_allowed_roots.push_back(root); m_scoped_allowed_roots.push_back({root, allow_write});
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;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -199,12 +312,65 @@ bool PluginAuditManager::is_denied_filename(const boost::filesystem::path& candi
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Audit mode // Denied path keywords
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void PluginAuditManager::set_audit_mode(AuditMode mode) { m_audit_mode = mode; } void PluginAuditManager::add_denied_path_keyword(const std::string& keyword)
{
if (keyword.empty())
return;
PluginAuditManager::AuditMode PluginAuditManager::audit_mode() const { return m_audit_mode; } std::string lower = keyword;
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
@@ -219,20 +385,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 are checked first, above both the Loading exemption below and the // Denied filenames/keywords are checked before the allowed roots. The app config and the
// allowed roots. The app config and the cloud refresh token live directly inside // cloud refresh token live directly inside data_dir(), which is a global allowed root, and
// data_dir(), which is a global allowed root, and no scope ever sets Enforcing — so a // the bundled TLS client cert lives inside resources_dir(), a read-only global allowed
// deny placed any lower would be unreachable for reads. // root, so a deny placed any lower would be unreachable.
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)) {
// During import/loading, only block writes. Python must be able to read BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << path.string() << " is_write=" << is_write
// stdlib modules and the plugin file itself during import. << " plugin=" << plugin_key << " reason=denied path keyword";
if (m_audit_mode == AuditMode::Loading && !is_write) return {false, "denied path keyword"};
return {true, ""}; }
namespace fs = boost::filesystem; namespace fs = boost::filesystem;
fs::path candidate = path; fs::path candidate = path;
@@ -245,8 +411,11 @@ 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_inside_allowed_root(candidate, root)) { if ((!is_write || root.allow_write) && is_inside_allowed_root(candidate, root.path)) {
return {true, ""}; return {true, ""};
} }
} }
@@ -254,14 +423,13 @@ 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_inside_allowed_root(candidate, root)) { if ((!is_write || root.allow_write) && is_inside_allowed_root(candidate, root.path)) {
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"};
} }
@@ -269,19 +437,99 @@ 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('+') != std::string::npos || mode.find('x') != 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
<< " path=" << violation.path.string() << " reason=" << violation.reason; << " 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;
@@ -297,31 +545,326 @@ 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 { namespace PluginAuditDetail {
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 to access a blocked file path"); PyErr_SetString(PyExc_PermissionError, "Plugin attempted an audited operation without permission");
return -1; return -1;
} }
} // namespace wxString audit_message(AuditEventCategory category, const wxString& plugin_name, const wxString& event_name,
const wxString& target_list)
{
if (target_list.IsEmpty())
return wxString::Format(
_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\".\n\n"
"This operation does not expose a target the audit hook can display."),
plugin_name, event_name);
switch (category) {
case AuditEventCategory::FsRead:
return wxString::Format(_L("Plugin \"%s\" is requesting to read the following file(s):\n%s"), plugin_name, target_list);
case AuditEventCategory::FsReadWrite:
return wxString::Format(_L("Plugin \"%s\" is requesting to read/write the following file(s):\n%s"), plugin_name, target_list);
case AuditEventCategory::FsCreate:
return wxString::Format(_L("Plugin \"%s\" is requesting to create the following file(s):\n%s"), plugin_name, target_list);
case AuditEventCategory::FsDelete:
return wxString::Format(_L("Plugin \"%s\" is requesting to delete the following file(s):\n%s"), plugin_name, target_list);
case AuditEventCategory::Http:
return wxString::Format(_L("Plugin \"%s\" is requesting to make an HTTP request to:\n%s"), plugin_name, target_list);
case AuditEventCategory::Socket:
return wxString::Format(_L("Plugin \"%s\" is requesting to open a network connection to:\n%s"), plugin_name, target_list);
case AuditEventCategory::ProcessCreate:
return wxString::Format(_L("Plugin \"%s\" is requesting to run the following command(s):\n%s"), plugin_name, target_list);
default:
return wxString::Format(_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\"."), plugin_name, event_name);
}
}
int decide_audited_event(PluginAuditManager& mgr,
PluginInstallState& state,
const std::string& plugin_key,
const std::string& plugin_name,
const std::string& event_name,
AuditEventCategory category,
const std::vector<std::string>& targets,
std::vector<std::string>* permission_list,
const std::vector<std::string>& call_site_ids)
{
std::vector<std::string> unresolved = targets;
if (permission_list) {
unresolved.clear();
for (const auto& target : targets)
if (!has_permission(*permission_list, target))
unresolved.push_back(target);
if (!targets.empty() && unresolved.empty())
return 0;
}
wxString target_list;
for (const auto& target : unresolved)
target_list += wxString::FromUTF8(target.c_str()) + "\n";
wxMessageDialog dialog(nullptr,
audit_message(category, wxString::FromUTF8(plugin_name.c_str()),
wxString::FromUTF8(event_name.c_str()), target_list),
_L("Plugin permission request"), wxYES_NO | wxICON_WARNING);
if (dialog.ShowModal() != wxID_YES)
return report_denied(mgr, event_name, {false, "audit permission required"});
if (permission_list)
for (const auto& target : unresolved)
persist_permission(plugin_key, state, *permission_list, target);
mgr.record_approved_call_sites(plugin_key, call_site_ids);
return 0;
}
} // namespace PluginAuditDetail
int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data) int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data)
{ {
@@ -331,94 +874,95 @@ 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;
} }
// extensive list of audit events can be found at https://docs.python.org/3/library/audit_events.html if (mgr->current_plugin().empty()) {
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;
}
std::string path_str(path_cstr ? path_cstr : "");
std::string mode_str(mode_cstr ? mode_cstr : "r");
AuditDecision decision = mgr->check_open(path_str, mode_str);
if (!decision.allowed)
return report_denied(*mgr, event_name, path_str, decision);
return 0; return 0;
} }
// --- os.rename event (raised by os.rename and os.replace) --- // the open function can take in different flags that determine if it is a read or readwrite.
if (event_name == "os.rename") { const AuditEventCategory event_type =
const char* src_cstr = nullptr; event_name == "open" ? PluginAuditDetail::open_category(args) : event_category(event_name);
const char* dst_cstr = nullptr; if (event_type == AuditEventCategory::None)
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;
}
// 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; return 0;
const bool fs_category = is_fs_category(event_type);
const std::vector<std::string> targets = PluginAuditDetail::audit_targets(event_name, event_type, args);
// 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"});
}
} }
// --- os.remove event (raised by os.remove and os.unlink) --- PluginDescriptor plugin_descriptor;
if (event_name == "os.remove") { PluginManager::instance().try_get_plugin_descriptor(mgr->current_plugin(), plugin_descriptor);
const char* path_cstr = nullptr;
PyObject* dir_fd = nullptr;
// os.remove(path, dir_fd) — path may be str, bytes, or int fd // Some plugins call other audited events, e.g. urlib.request will call socket.connect. So this is so that if urlib.request
if (!PyArg_ParseTuple(args, "s|O", &path_cstr, &dir_fd)) { // was already approved, socket.connect won't trigger another permission dialog.
PyErr_Clear(); // couldn't parse; allow const std::vector<std::string> call_site_ids =
return 0; PluginAuditDetail::call_site_identities(plugin_descriptor.plugin_root);
} if (mgr->has_approved_ancestor(mgr->current_plugin(), call_site_ids))
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; 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;
} }
// Unknown event — allow by default PluginInstallState state;
return 0; const bool have_install_state = PluginManager::instance().get_install_state(mgr->current_plugin(), state);
if (!have_install_state) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Failed to get install state for " << mgr->current_plugin();
}
const std::string plugin_name = state.plugin_name.empty() ? mgr->current_plugin() : state.plugin_name;
std::vector<std::string>* permission_list = PluginAuditDetail::permission_list_for(event_type, state.permissions);
return PluginAuditDetail::decide_audited_event(*mgr, state, mgr->current_plugin(), plugin_name, event_name,
event_type, targets, permission_list, call_site_ids);
} }
void PluginAuditManager::install_hook() void PluginAuditManager::install_hook()
{ {
if (PySys_AddAuditHook(audit_hook, this) < 0) { // data_dir() is the primary globally-allowed root during plugin execution: read+write. It
BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install CPython audit hook"; // covers the plugin system's own workflow needs -- each plugin's storage folder
return; // (data_dir()/orca_plugins) and the installed/system profile cache (data_dir()/system) --
} // without a separate, narrower grant for either (G-code plugins additionally get the temp
BOOST_LOG_TRIVIAL(info) << "[AUDIT] CPython audit hook installed successfully"; // G-code folder via a scoped root, see SlicingPipelinePluginCapabilityTrampoline).
// data_dir() is the only globally-allowed root during enforced plugin execution.
// The executable directory and resources directory are intentionally NOT allowed
// here: plugins must not write outside data_dir() (G-code plugins additionally get
// the temp G-code folder via a scoped root). Reads remain permissive in Loading mode.
add_global_allowed_root(data_dir()); add_global_allowed_root(data_dir());
// resources_dir() holds the app's bundled, shared assets (installed system profiles, the
// bundled TLS client cert, web assets). Plugins may read from it -- e.g. inspecting bundled
// profiles -- but must never write into the shared, potentially multi-user app install, so
// it is granted read-only. The bundled cert itself stays unreachable regardless, via the
// "cert" denied-path keyword seeded below.
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.
// //
@@ -430,6 +974,19 @@ 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,8 +2,11 @@
#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>
@@ -18,10 +21,31 @@ struct AuditDecision {
struct AuditViolation { struct AuditViolation {
std::string plugin_key; std::string plugin_key;
std::string event_name; std::string event_name;
boost::filesystem::path path;
std::string reason; std::string reason;
}; };
// 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;
bool allow_write = true;
};
// The set of CPython audit events PluginAuditManager recognizes, grouped by the kind of
// operation they represent. None means the event isn't one audit_hook() acts on at all.
enum class AuditEventCategory {
None,
FsRead,
FsReadWrite,
FsCreate,
FsDelete,
Http,
Socket,
ProcessCreate,
Threading,
};
// Returns true if candidate resolves to a path inside allowed_root. // Returns true if candidate resolves to a path inside allowed_root.
// Uses weakly_canonical and component-wise comparison to reject traversal attacks. // Uses weakly_canonical and component-wise comparison to reject traversal attacks.
bool is_inside_allowed_root(const boost::filesystem::path& candidate, bool is_inside_allowed_root(const boost::filesystem::path& candidate,
@@ -49,12 +73,15 @@ public:
void clear_current_capability(); void clear_current_capability();
// --- allowed-roots registry --- // --- allowed-roots registry ---
void add_global_allowed_root(const boost::filesystem::path& root); // allow_write = false registers a read-only root: a read-shaped event inside it is allowed,
void add_scoped_allowed_root(const boost::filesystem::path& root); // but a write/create/delete-shaped event is not, so it falls through to the normal
// 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 audit mode or // Filenames a plugin may never touch, in any directory, regardless of the enclosing allowed
// enclosing allowed root. A candidate is denied when its filename starts with a // 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
@@ -73,33 +100,60 @@ 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;
// --- enforcement mode --- // --- denied-path-keyword registry ---
enum class AuditMode { // Keywords that categorically deny a path if ANY of its components (directory or file
// Import/loading phase: allow reads anywhere, only block writes // name), not just the base name, contains one case-insensitively -- e.g. a "secrets"
// outside allowed roots. Python needs to read stdlib modules // subfolder, a "certificates" folder, or a "conf"/"config" file anywhere the plugin can
// during import and those are not inside plugin directories. // otherwise reach, including inside an allowed root. This is intentionally broader and
Loading, // fuzzier than the exact-name is_denied_filename registry: it exists to categorically rule
// 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);
// Execution phase: block both reads and writes outside allowed // The list install_hook() seeds into the keyword registry. Exposed so tests seed the exact
// roots, plus subprocess/socket/ctypes. // same set without a live interpreter.
Enforcing, static std::vector<std::string> default_denied_path_keywords();
};
void set_audit_mode(AuditMode mode); // True when any component of candidate's (canonicalized) path contains a registered
AuditMode audit_mode() const; // keyword, case-insensitively.
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 list is consulted above the // Shared core for every audited filesystem event. The deny checks are consulted above the
// Loading-mode read exemption and above the allowed roots, so a denied filename is // allowed roots, so a denied path is blocked even when it sits inside an allowed root (e.g.
// blocked even though every scope currently runs in Loading and the files in question // data_dir(), which is a global allowed root).
// 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:
@@ -109,17 +163,19 @@ private:
static int audit_hook(const char* event, PyObject* args, void* user_data); 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_plugin_key;
static thread_local std::string m_current_capability_name; static thread_local std::string m_current_capability_name;
static thread_local AuditMode m_audit_mode; static thread_local std::vector<AllowedRoot> m_scoped_allowed_roots;
static thread_local std::vector<boost::filesystem::path> m_scoped_allowed_roots; static thread_local bool m_audit_denial_pending;
static thread_local bool m_has_last_violation; static thread_local bool m_has_last_violation;
static thread_local AuditViolation m_last_violation; static thread_local AuditViolation m_last_violation;
// mutable: is_denied_filename() is a const query that must lock. // mutable: is_denied_filename() and has_approved_ancestor() are const queries that must lock.
mutable std::mutex m_mutex; mutable std::mutex m_mutex;
std::vector<boost::filesystem::path> m_global_allowed_roots; std::vector<AllowedRoot> 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
@@ -130,8 +186,7 @@ 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();
@@ -141,8 +196,7 @@ public:
private: private:
std::string m_previous_id; std::string m_previous_id;
std::string m_previous_capability; std::string m_previous_capability;
PluginAuditManager::AuditMode m_previous_mode; std::vector<AllowedRoot> m_previous_scoped_roots;
std::vector<boost::filesystem::path> m_previous_scoped_roots;
}; };
} // namespace Slic3r } // namespace Slic3r

View File

@@ -784,6 +784,23 @@ 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>();
@@ -819,6 +836,14 @@ 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}});
@@ -836,6 +861,9 @@ 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
@@ -852,10 +880,16 @@ 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". PluginManager writes the real // sidecar is (re)initialized to "auto-load, nothing disabled". This intentionally resets
// per-capability flags once the package is loaded, via the (dir, entry, enabled, capabilities) // permissions on a fresh install/reinstall. PluginManager writes the real per-capability flags
// overload. // while preserving permissions once the package is loaded, via the overload above.
return write_install_state(plugin_dir, entry, true, {}); PluginInstallState state;
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,11 +82,23 @@ 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, bridge_error); auto capabilities_found = bridge.finalize_plugin_capture(descriptor.entry_path, descriptor.plugin_key, 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,6 +21,7 @@
#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>
@@ -622,6 +623,20 @@ 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
@@ -910,6 +925,8 @@ 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;
} }
@@ -1135,6 +1152,51 @@ 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)
@@ -1354,6 +1416,11 @@ 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);
@@ -1741,6 +1808,7 @@ 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,6 +159,8 @@ 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();
@@ -255,6 +257,9 @@ 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,25 +26,30 @@
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(mode) \ #define ORCA_PY_AUDIT_SCOPE() \
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(), mode) _orca_audit_scope.emplace(_orca_audit_key, this->name())
#define ORCA_PY_OVERRIDE_AUDITED(mode, audit_setup, override_macro, ret, base, name, ...) \ #define ORCA_PY_OVERRIDE_AUDITED(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(mode); \ ORCA_PY_AUDIT_SCOPE(); \
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__)); \
@@ -58,7 +63,7 @@ public:
std::string get_name() const override std::string get_name() const override
{ {
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, Base, get_name); ORCA_PY_OVERRIDE_AUDITED([] {}, 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
@@ -66,7 +71,6 @@ 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,
@@ -77,7 +81,6 @@ 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,
@@ -94,7 +97,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(::Slic3r::PluginAuditManager::AuditMode::Loading); ORCA_PY_AUDIT_SCOPE();
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");
@@ -117,17 +120,17 @@ public:
void on_load() override void on_load() override
{ {
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_load); ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_load);
} }
void on_unload() override void on_unload() override
{ {
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_unload); ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_unload);
} }
void on_cancelled() override void on_cancelled() override
{ {
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled); ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled);
} }
}; };
@@ -139,7 +142,7 @@ public:
PluginCapabilityType get_type() const override PluginCapabilityType get_type() const override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, PluginCapabilityType, PluginCapabilityInterface, [] {}, PYBIND11_OVERRIDE, PluginCapabilityType, PluginCapabilityInterface,
get_type); get_type);
} }
}; };

View File

@@ -87,6 +87,12 @@ 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
@@ -622,10 +628,6 @@ bool PythonInterpreter::initialize()
else else
BOOST_LOG_TRIVIAL(info) << "Bundled uv executable not found"; BOOST_LOG_TRIVIAL(info) << "Bundled uv executable not found";
// Install the CPython audit hook for plugin policy enforcement.
// This is defense-in-depth: today it only inspects the `open` audit event
// and blocks writes outside the allowed roots; subprocess/socket/ctypes and
// other events are not yet handled. It is NOT a full security sandbox.
PluginAuditManager::instance().install_hook(); PluginAuditManager::instance().install_hook();
// Persist Python stderr (plugin tracebacks, including uncaught // Persist Python stderr (plugin tracebacks, including uncaught

View File

@@ -1,5 +1,6 @@
#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>
@@ -40,6 +41,7 @@ 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
@@ -77,6 +79,8 @@ 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
@@ -101,34 +105,38 @@ 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& plugin_key, std::string& error) std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& capture_key,
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 " << plugin_key; BOOST_LOG_TRIVIAL(info) << "Finalizing Python plugin capture for key " << capture_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 = [&plugin_key]() { auto clear_active_key = [&capture_key]() {
if (g_active_plugin_key == plugin_key) if (g_active_plugin_key == capture_key)
g_active_plugin_key.clear(); g_active_plugin_key.clear();
}; };
auto discard_pending_for_key = [&plugin_key]() { auto discard_pending_for_key = [&capture_key]() {
std::lock_guard<std::mutex> lock(g_registry_mutex); std::lock_guard<std::mutex> lock(g_registry_mutex);
g_pending_capabilities.erase(plugin_key); g_pending_capabilities.erase(capture_key);
g_pending_package.erase(plugin_key); g_pending_package.erase(capture_key);
g_pending_fs_read_permissions.erase(capture_key);
}; };
try { try {
@@ -138,7 +146,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(plugin_key); auto it = g_pending_package.find(capture_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);
@@ -157,6 +165,33 @@ 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();
@@ -180,7 +215,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(plugin_key); auto it = g_pending_capabilities.find(capture_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);
@@ -189,10 +224,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 == plugin_key) if (g_active_plugin_key == capture_key)
g_active_plugin_key.clear(); g_active_plugin_key.clear();
BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << plugin_key; BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << capture_key;
std::vector<CapturedCapability> capabilities; std::vector<CapturedCapability> capabilities;
capabilities.reserve(classes.size()); capabilities.reserve(classes.size());
@@ -204,7 +239,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 " << plugin_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed type check for key " << capture_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} }
@@ -212,7 +247,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 " << plugin_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed cast for key " << capture_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} }
@@ -226,7 +261,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 " << plugin_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture rejected capability for key " << capture_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} }
@@ -246,12 +281,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 " << plugin_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised Python exception for key " << capture_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 " << plugin_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised exception for key " << capture_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} }
@@ -279,6 +314,7 @@ 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)
@@ -305,6 +341,7 @@ 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;
} }
@@ -313,6 +350,7 @@ 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();
} }
@@ -450,6 +488,27 @@ 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& plugin_key, std::string& error); const std::string& capture_key, 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,7 +16,6 @@ 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,
@@ -27,7 +26,6 @@ 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,
@@ -42,7 +40,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(::Slic3r::PluginAuditManager::AuditMode::Loading); ORCA_PY_AUDIT_SCOPE();
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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, AgentInfo, PrinterAgentPluginCapability, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, connect_printer, dev_id, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, disconnect_printer); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message, dev_id, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message_to_printer, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, start_discovery, start, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind_detect, dev_ip, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, FilamentSyncMode, PrinterAgentPluginCapability, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, check_cert); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, void, PrinterAgentPluginCapability, install_device_cert, dev_id, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ping_bind, ping_code); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, unbind, dev_id); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_print, params, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_sdcard_print, params, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, get_hms_snapshot, dev_id, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_server_callback, fn); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_ssdp_msg_fn, fn); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_printer_connected_fn, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_subscribe_failure_fn, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_message_fn, fn); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_user_message_fn, fn); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_connect_fn, fn); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_message_fn, fn); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_queue_on_main_fn, fn); [] {}, 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(::Slic3r::PluginAuditManager::AuditMode::Loading); ORCA_PY_AUDIT_SCOPE();
::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,7 +13,6 @@ 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,7 +10,6 @@ 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,4 +34,24 @@ 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,6 +27,16 @@ 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]")
@@ -81,7 +91,7 @@ TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit
} }
} }
TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption", "[audit]") TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
{ {
ScopedDataDir data_dir_guard("plugin-audit-deny"); ScopedDataDir data_dir_guard("plugin-audit-deny");
seed_denied_names(); seed_denied_names();
@@ -91,8 +101,8 @@ TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption"
// 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 in Loading mode, which every scope runs in. // Enter a plugin context. The deny must hold even inside a globally allowed root.
ScopedPluginAuditContext ctx("test_plugin", "", PluginAuditManager::AuditMode::Loading); ScopedPluginAuditContext ctx("test_plugin", "");
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;
@@ -103,6 +113,13 @@ TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption"
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");
@@ -110,16 +127,14 @@ TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption"
CHECK(decision.reason == "denied filename"); CHECK(decision.reason == "denied filename");
} }
SECTION("reading the app config is blocked even though Loading exempts reads") SECTION("reading the app config is blocked despite the allowed root")
{ {
// 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 in Loading mode") SECTION("reading the cloud refresh token is blocked")
{ {
AuditDecision decision = mgr.check_open(token.string(), "r"); AuditDecision decision = mgr.check_open(token.string(), "r");
CHECK_FALSE(decision.allowed); CHECK_FALSE(decision.allowed);
@@ -150,7 +165,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", "", PluginAuditManager::AuditMode::Loading); ScopedPluginAuditContext ctx("test_plugin", "");
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")
@@ -185,3 +200,139 @@ 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,6 +112,22 @@ 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
@@ -120,4 +136,4 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins
scanned.version = "1.0.0"; // as parsed from the unchanged PEP723 header scanned.version = "1.0.0"; // as parsed from the unchanged PEP723 header
read_install_state(plugin_dir, scanned); read_install_state(plugin_dir, scanned);
CHECK(scanned.installed_version == "1.2.0"); CHECK(scanned.installed_version == "1.2.0");
} }