dot_sandbox 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. #!/usr/bin/env python3
  2. """
  3. Graphviz sandbox
  4. This program is a wrapper around Graphviz. It aims to provide a safe environment for the
  5. processing of untrusted input graphs and command line options. More precisely:
  6. 1. No network access will be allowed.
  7. 2. The file system will be read-only. Command line options like `-o …` and `-O` will
  8. not work. It is expected that the caller will render to stdout and pipe the output
  9. to their desired file.
  10. """
  11. import abc
  12. import platform
  13. import shutil
  14. import subprocess as sp
  15. import sys
  16. from pathlib import Path
  17. from typing import List, Type, Union
  18. class Sandbox:
  19. """
  20. API for sandbox interaction
  21. Specific sandbox mechanisms should be implemented as derived classes of this.
  22. """
  23. @staticmethod
  24. @abc.abstractmethod
  25. def is_usable() -> bool:
  26. """is this sandbox available on the current platform?"""
  27. raise NotImplementedError
  28. @staticmethod
  29. @abc.abstractmethod
  30. def _run(args: List[Union[Path, str]]) -> int:
  31. """run the given command line within the sandbox"""
  32. raise NotImplementedError
  33. @classmethod
  34. def run(cls, args: List[Union[Path, str]]) -> int:
  35. """wrapper around `_run` to perform common sanity checks"""
  36. assert cls.is_usable(), "attempted to use unusable sandbox"
  37. return cls._run(args)
  38. class Bubblewrap(Sandbox):
  39. """
  40. Bubblewrap¹-based sandbox
  41. ¹ https://github.com/containers/bubblewrap
  42. """
  43. def is_usable() -> bool:
  44. return shutil.which("bwrap") is not None
  45. def _run(args: List[Union[Path, str]]) -> sp.CompletedProcess:
  46. prefix = ["bwrap", "--ro-bind", "/", "/", "--unshare-all", "--"]
  47. return sp.call(prefix + args)
  48. def main(args: List[str]) -> int:
  49. """entry point"""
  50. # available sandboxes in order of preference
  51. SANDBOXES: Tuple[Type[Sandbox]] = (Bubblewrap,)
  52. # locate Graphviz, preferring the version collocated with us
  53. exe = ".exe" if platform.system() == "Windows" else ""
  54. dot = Path(__file__).parent / f"dot{exe}"
  55. if not dot.exists():
  56. dot = shutil.which("dot")
  57. if dot is None:
  58. sys.stderr.write("Graphviz (`dot`) not found\n")
  59. return -1
  60. # find a usable sandbox
  61. sandbox: Optional[Type[Sandbox]] = None
  62. for box in SANDBOXES:
  63. if not box.is_usable():
  64. continue
  65. sandbox = box
  66. break
  67. if sandbox is None:
  68. sys.stderr.write("no usable sandbox found\n")
  69. return -1
  70. dot_args = args[1:]
  71. # run Graphviz
  72. return sandbox.run([dot] + dot_args)
  73. if __name__ == "__main__":
  74. sys.exit(main(sys.argv))