From 9ee7c16532dd6982fdeeb994168364d5ee75a390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Tue, 27 Jun 2017 04:46:35 +0200 Subject: [PATCH] qubespolicy: add a tool to analyze policy in form of graph Output possible connections between VMs in form of dot file. Fixes QubesOS/qubes-issues#2873 --- doc/manpages/qrexec_policy_graph.rst | 73 +++++++++++++++ qubes-rpc-policy/generate-admin-policy | 2 +- qubespolicy/graph.py | 121 +++++++++++++++++++++++++ rpm_spec/core-dom0.spec | 2 + setup.py | 1 + 5 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 doc/manpages/qrexec_policy_graph.rst create mode 100644 qubespolicy/graph.py diff --git a/doc/manpages/qrexec_policy_graph.rst b/doc/manpages/qrexec_policy_graph.rst new file mode 100644 index 000000000..f0ef070c4 --- /dev/null +++ b/doc/manpages/qrexec_policy_graph.rst @@ -0,0 +1,73 @@ +.. program:: qrexec-policy-graph + +:program:`qrexec-policy-graph` -- Graph qrexec policy +===================================================== + +Synopsis +-------- + +:command:`qrexec-policy-graph` skel-manpage.py [-h] [--include-ask] [--source *SOURCE* [*SOURCE* ...]] [--target *TARGET* [*TARGET* ...]] [--service *SERVICE* [*SERVICE* ...]] [--output *OUTPUT*] [--policy-dir POLICY_DIR] [--system-info SYSTEM_INFO] + + +Options +------- + +.. option:: --help, -h + + show this help message and exit + +.. option:: --include-ask + + Include `ask` action in graph. In most cases produce unreadable graphs + because many services contains `$anyvm $anyvm ask` rules. It's recommended to + limit graph using other options. + +.. option:: --source + + Limit graph to calls from *source*. You can specify multiple names. + +.. option:: --target + + Limit graph to calls to *target*. You can specify multiple names. + +.. option:: --service + + Limit graph to *service*. You can specify multiple names. This can be either + bare service name, or service with argument (joined with `+`). If bare + service name is given, output will contain also policies for specific + arguments. + +.. option:: --output + + Write to *output* instead of stdout. The file will be overwritten without + confirmation. + +.. option:: --policy-dir + + Look for policy in *policy-dir*. This can be useful to process policy + extracted from other system. This option adjust only base directory, if any + policy file contains `$include:path` with absolute path, it will try to load + the file from that location. + See also --system-info option. + +.. option:: --system-info + + Load system information from file instead of querying local qubesd instance. + The file should be in json format, as returned by `internal.GetSystemInfo` + qubesd method. This can be obtained by running in dom0: + + qubesd-query -e -c /var/run/qubesd.internal.sock dom0 \ + internal.GetSystemInfo dom0 | cut -b 3- + +.. option:: --skip-labels + + Do not include service names on the graph. Also, include only a single + connection between qubes if any service call is allowed there. + + +Authors +------- + +| Marek Marczykowski-Górecki + +.. vim: ts=3 sw=3 et tw=80 diff --git a/qubes-rpc-policy/generate-admin-policy b/qubes-rpc-policy/generate-admin-policy index eea592a6e..480cb0d0c 100755 --- a/qubes-rpc-policy/generate-admin-policy +++ b/qubes-rpc-policy/generate-admin-policy @@ -29,7 +29,7 @@ import qubes.api.admin parser = argparse.ArgumentParser( description='Generate default Admin API policy') parser.add_argument('--include-base', action='store', - default='/etc/qubes-rpc/policy/include', + default='include', help='Base path for included paths (default: %(default)s)') parser.add_argument('--destdir', action='store', default='/etc/qubes-rpc/policy', diff --git a/qubespolicy/graph.py b/qubespolicy/graph.py new file mode 100644 index 000000000..3fc90c529 --- /dev/null +++ b/qubespolicy/graph.py @@ -0,0 +1,121 @@ +# -*- encoding: utf8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2017 Marek Marczykowski-Górecki +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, see . + +import argparse +import json +import os + +import sys + +import qubespolicy + +parser = argparse.ArgumentParser(description='Graph qrexec policy') +parser.add_argument('--include-ask', action='store_true', + help='Include `ask` action in graph') +parser.add_argument('--source', action='store', nargs='+', + help='Limit graph to calls from *source*') +parser.add_argument('--target', action='store', nargs='+', + help='Limit graph to calls to *target*') +parser.add_argument('--service', action='store', nargs='+', + help='Limit graph to *service*') +parser.add_argument('--output', action='store', + help='Write to *output* instead of stdout') +parser.add_argument('--policy-dir', action='store', + default=qubespolicy.POLICY_DIR, + help='Look for policy in *policy-dir*') +parser.add_argument('--system-info', action='store', + help='Load system information from file instead of querying qubesd') +parser.add_argument('--skip-labels', action='store_true', + help='Do not include service names on the graph, also deduplicate ' + 'connections.') + +def handle_single_action(args, action): + '''Get single policy action and output (or not) a line to add''' + if args.skip_labels: + service = '' + else: + service = action.service + if action.action == qubespolicy.Action.ask: + if args.include_ask: + # handle forced target= + if len(action.targets_for_ask) == 1: + return ' "{}" -> "{}" [label="{}" color=orange];\n'.format( + action.source, action.targets_for_ask[0], service) + else: + return ' "{}" -> "{}" [label="{}" color=orange];\n'.format( + action.source, action.original_target, service) + elif action.action == qubespolicy.Action.allow: + return ' "{}" -> "{}" [label="{}" color=red];\n'.format( + action.source, action.target, service) + return '' + +def main(args=None): + args = parser.parse_args(args) + + output = sys.stdout + if args.output: + output = open(args.output, 'w') + + if args.system_info: + with open(args.system_info) as f_system_info: + system_info = json.load(f_system_info) + else: + system_info = qubespolicy.get_system_info() + + sources = list(system_info['domains'].keys()) + if args.source: + sources = args.source + + targets = list(system_info['domains'].keys()) + if args.target: + targets = args.target + else: + targets.append('$dispvm') + targets.extend('$dispvm:' + dom for dom in system_info['domains'] + if system_info['domains'][dom]['dispvm_allowed']) + + connections = set() + + output.write('digraph g {\n') + for service in os.listdir(args.policy_dir): + if args.service and service not in args.service and \ + not any(service.startswith(srv + '+') for srv in args.service): + continue + + policy = qubespolicy.Policy(service, args.policy_dir) + for source in sources: + for target in targets: + try: + action = policy.evaluate(system_info, source, target) + line = handle_single_action(args, action) + if line in connections: + continue + if line: + output.write(line) + connections.add(line) + except qubespolicy.AccessDenied: + continue + + output.write('}\n') + if args.output: + output.close() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/rpm_spec/core-dom0.spec b/rpm_spec/core-dom0.spec index abebb345f..6285be2a8 100644 --- a/rpm_spec/core-dom0.spec +++ b/rpm_spec/core-dom0.spec @@ -213,6 +213,7 @@ fi /usr/bin/qubesd* /usr/bin/qrexec-policy /usr/bin/qrexec-policy-agent +/usr/bin/qrexec-policy-graph %{_mandir}/man1/qubes*.1* @@ -372,6 +373,7 @@ fi %{python3_sitelib}/qubespolicy/gtkhelpers.py %{python3_sitelib}/qubespolicy/rpcconfirmation.py %{python3_sitelib}/qubespolicy/utils.py +%{python3_sitelib}/qubespolicy/graph.py %dir %{python3_sitelib}/qubespolicy/tests %dir %{python3_sitelib}/qubespolicy/tests/__pycache__ diff --git a/setup.py b/setup.py index d5d5262e5..0f1f40e1b 100644 --- a/setup.py +++ b/setup.py @@ -34,6 +34,7 @@ def get_console_scripts(): 'console_scripts': list(get_console_scripts()) + [ 'qrexec-policy = qubespolicy.cli:main', 'qrexec-policy-agent = qubespolicy.agent:main', + 'qrexec-policy-graph = qubespolicy.graph:main', ], 'qubes.vm': [ 'AppVM = qubes.vm.appvm:AppVM',