summaryrefslogtreecommitdiffstats
path: root/config/chroot_local-includes/usr/local/sbin/tails-debugging-info
blob: a0babdef1a50ae4029b867105877293ed14fceb9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#! /usr/bin/env python3
"""
Debug Tails.

Test with "python3 tails-debugging-info.py doctest" as root.

goodcrypto.com converted from bash to python and added basic tests.

*** WARNING about debug_file and debug_directory *********************

Great attention must be given to the ownership situation of these
files and their parent directories in order to avoid a symlink-based
attack that could read the contents of any file and make it
accessible to the user running this script (typically the live
user). Therefore, when adding a new file, give as the first argument
'root' only if the complete path to it (including the file itself)
is owned by root and already exists before the system is connected to
the network (that is, before GDM's PostLogin script is run).
If not, the following rules must be followed strictly:

* only one non-root user is involved in the ownership situation (the
  file, its dir and the parent dirs). From now on let's assume it is
  the case and call it $USER.

* if any non-root group has write access, it must not have any
  members.

If any of these rules does not apply, the file cannot be added here
safely and something is probably quite wrong and should be
investigated carefully.

>>> # run script
>>> import sh
>>> this_command = sh.Command(sys.argv[0])
>>> this_command()
<BLANKLINE>
...
"""

import json
import os
import sys
import subprocess
from pwd import getpwuid


# AppArmor Ux rules don't sanitize PATH, which can lead to an
# exploited application (that's allowed to run this script unconfined)
# having this script run arbitrary code, violating that application's
# confinement. Let's prevent that by setting PATH to a list of
# directories where only root can write.
os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'


def main():
    """Print debug information serialized as json.

    >>> main()
    <BLANKLINE>
    ...
    """

    config = None
    with open('/etc/whisperback/debugging-info.json', 'r') as conf_file:
        config = json.load(conf_file)

    info = []
    for _type, _args in config:
        if _type == 'command':
            info.append(debug_command(_args['args'][0], *_args['args'][1:]))
        elif _type == 'directory':
            info.append(debug_directory(_args['user'], _args['path']))
        else:
            info.append(debug_file(_args['user'], _args['path']))
    print()
    print(json.dumps(info, indent=4))


def debug_command(command, *args):
    """Return the command and it's standard output as dict.

    >>> debug_command('echo', 'foo')
    {...'key': 'echo foo'...}
    """
    command_output = subprocess.check_output([command, *args])
    command_output = command_output.decode('UTF-8').strip().split('\n')
    return {'key': '{}'.format(' '.join((command,) + args)), 'content': command_output}


def debug_file(user, filename):
    """Return the filename and the file content as dict.

    >>> import tempfile, getpass
    >>> with tempfile.NamedTemporaryFile('w') as f:
    ...     _ = f.write("foo\\nbar")
    ...     _ = f.seek(0)
    ...     debug_file(getpass.getuser(), f.name)
    {...'content': ['foo', 'bar']...}
    """
    if not os.path.isfile(filename):
        return {'key': filename, 'content': 'Not found'}

    # This check is not sufficient, see the comment at the top of the file
    # for the complete requirements required for security
    owner = getpwuid(os.stat(filename).st_uid).pw_name
    if owner != user:
        return {'key': filename, 'content': '''WARNING: not opening file {}, because it is '''
                '''owned by {} instead of {}'''.format(filename, owner, user)}

    file_content = []
    with open(filename) as f:
        for l in f:
            file_content.append(l.replace('\n', ''))
    return {'key': filename, 'content': file_content}


def debug_directory(user, dir_name):
    """Return a dict with the dir_name and dicts with
    the content of all contained files (non-recursively).

    >>> import os, getpass
    >>> tmpdir = '/tmp/mytempdir'
    >>> os.makedirs(tmpdir)
    >>> with open(os.path.join(tmpdir, 'foo'), 'w') as f:
    ...     _ = f.write("foobar\\nbar")
    ...     _ = f.seek(0)
    ...     result = debug_directory(getpass.getuser(), tmpdir)
    >>> os.remove(os.path.join(tmpdir, 'foo'))
    >>> os.rmdir(tmpdir)
    >>> result
    {...[{...['foobar', 'bar']...}]}
    """
    if not os.path.isdir(dir_name):
        return {'key': dir_name, 'content': 'Not found'}

    # This check is not sufficient, see the comment at the top of the file
    # for the complete requirements required for security
    owner = getpwuid(os.stat(dir_name).st_uid).pw_name
    if owner != user:
            return {'key': dir_name, 'content': '''WARNING: not opening directory {}, because '''
                    '''it is owned by {} instead of {}'''.format(dir_name, owner, user)}

    files = os.listdir(dir_name)

    listing = []

    for f in files:
        listing.append(debug_file(user, os.path.join(dir_name, f)))
    return {'key': dir_name, 'content': listing}


if __name__ == '__main__':
    if sys.argv and len(sys.argv) > 1:
        if sys.argv[1] == 'doctest':
            import doctest
            doctest.testmod(optionflags=doctest.ELLIPSIS)
        else:
            main()
    else:
        main()