diff options
Diffstat (limited to 'poetry_restrict_plugin/plugin.py')
-rw-r--r-- | poetry_restrict_plugin/plugin.py | 86 |
1 files changed, 86 insertions, 0 deletions
diff --git a/poetry_restrict_plugin/plugin.py b/poetry_restrict_plugin/plugin.py new file mode 100644 index 0000000..b75c669 --- /dev/null +++ b/poetry_restrict_plugin/plugin.py @@ -0,0 +1,86 @@ +import os.path +import pathlib +import sys +import traceback + +import poetry as poetry_package +from cleo.io.io import IO +from landlock import FSAccess, Ruleset +from poetry.plugins.plugin import Plugin +from poetry.poetry import Poetry + + +def existing_paths(paths): + for path in paths: + if os.path.exists(path): + yield path + + +class RestrictPlugin(Plugin): + def landlock(self, poetry: Poetry): + poetry_libs_path = pathlib.Path(poetry_package.__path__._path[0]).parent + + ruleset = Ruleset() + + # Rules for Poetry's virtual environment management + # Storing the virtual environment + ruleset.allow(poetry.config.virtualenvs_path, rules=FSAccess.all()) + # Cached dependencies + ruleset.allow(poetry.config.artifacts_cache_directory, rules=FSAccess.all()) + ruleset.allow(poetry.config.repository_cache_directory, rules=FSAccess.all()) + # Temporary storage + ruleset.allow("/tmp", rules=FSAccess.all() & ~FSAccess.EXECUTE) + # Poetry may also want to late-import some of its dependencies, or built-in modules + ruleset.allow(*existing_paths(sys.path), rules=FSAccess.READ_FILE | FSAccess.READ_DIR) + + # Finally, the Python executable may need to import some of its shared libraries + ruleset.allow( + *existing_paths(("/lib", "/lib64")), + rules=FSAccess.READ_FILE | FSAccess.READ_DIR | FSAccess.EXECUTE, + ) + # and in poetry shell, we might want to run some system executables, too + ruleset.allow("/usr/bin", rules=FSAccess.READ_FILE | FSAccess.READ_DIR | FSAccess.EXECUTE) + + + # We allow read access here, later we might want to restrict the pid namespace though + ruleset.allow("/proc", rules=FSAccess.READ_FILE | FSAccess.READ_DIR) + # needed for /dev/tty and /dev/pty devices, see /usr/lib/python3.11/pty.py + ruleset.allow("/dev", rules=FSAccess.READ_FILE | FSAccess.READ_DIR | FSAccess.WRITE_FILE) + + # Python's `zoneinfo` module + ruleset.allow("/usr/share/zoneinfo/", rules=FSAccess.READ_FILE | FSAccess.READ_DIR) + + ruleset.allow( + # We need to know which DNS resolver to use, and any custom hosts + *existing_paths(("/etc/resolv.conf", "/etc/hosts")), + # pip reads this file in _vendor/distro/distro.py + *existing_paths(("/etc/debian_version")), + # I'm not opposed to including things like this because I don't want to annoy people + # when their tooling doesn't work. But we have to be conservative. I think shells + # are fine, but if there was some further tooling (e.g. shell tools run at startup) + # I don't think those should be included. + *existing_paths(("/etc/bash.bashrc", os.path.expanduser("~/.bashrc"))), + rules=FSAccess.READ_FILE, + ) + ruleset.allow("/etc/ssl/certs", "/usr/local/share/ca-certificates", rules=FSAccess.READ_FILE | FSAccess.READ_DIR) + + pre_commit_cache = os.path.expanduser("~/.cache/pre-commit") + if os.path.exists(pre_commit_cache): + ruleset.allow(pre_commit_cache) + + # Allow manipulation of files in our projects, e.g. for linters. + # We might need to check this more thoroughly. For instance, configuring custom + # filter programs in gitattributes might allow a sandbox escape. + ruleset.allow(".") + + ruleset.apply() + + def activate(self, poetry: Poetry, io: IO): + try: + self.landlock(poetry) + io.write_line("<info>poetry-restrict-plugin</info>: Landlock engaged.") + except Exception as err: + io.write_line("<error>Fatal error trying to enforce Landlock rules:</error>") + traceback.print_exception(err) + io.write_line("<error>This is an issue of the Poetry restrict plugin, not of Poetry itself.</error>") + raise |