diff options
| author | 2024-07-27 18:51:59 +0200 | |
|---|---|---|
| committer | 2024-07-27 18:51:59 +0200 | |
| commit | 0d8e16eee4e09785043eabb5050e0832af07a30b (patch) | |
| tree | 872e319a5155c5b55cbb5efa4f2c92cc3e730421 /poetry_restrict_plugin/plugin.py | |
Initial commit
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  |