aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/jail_prober.py
blob: a5f279cb47b5276f93548aedc246da8efec72bce (plain) (blame)
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
161
162
163
#!/usr/bin/env python3
"""
Figure out which profile options may be causing a particular program to break
when run in firejail.

Instead of having to comment out each line in a profile by hand, and then
enable each line individually until the bad line or lines are found, this
largely automates the process. Users only have to provide the path to the
profile, program name, and answer 'y' for yes or 'n' for no when prompted.

After completion, you'll be provided with some information to copy and then
paste into a GitHub issue in the Firejail project repository:
https://github.com/netblue30/firejail/issues

Paths to the profile should be absolute. If the program is in your path, then
you only have to type the profile name. Else, you'll need to provide the
absolute path to the profile.

Examples:
python jail_prober.py /etc/firejail/spotify.profile spotify
python jail_prober.py /usr/local/etc/firejail/firefox.profile /usr/bin/firefox
"""

import sys
import os
import subprocess


def check_params(profilePath):
    """
    Ensure the path to the profile is valid and that an actual profile has been
    passed (as opposed to a config or .local file).

    :params profilePath: The absolute path to the problematic profile.
    """
    if not os.path.isfile(profilePath):
        raise FileNotFoundError(
            'The path %s is not a valid system path.' % profilePath)
    if not profilePath.endswith('.profile'):
        raise ValueError('%s is not a valid Firejail profile.' % profilePath)


def get_args(profilePath):
    """
    Read the profile, stripping out comments and newlines

    :params profilePath: The absolute path to the problematic profile.

    :returns profile: A list containing all active profile arguments
    """
    with open(profilePath, 'r') as f:
        profile = f.readlines()
        profile = [
            arg.strip() for arg in profile
            if not arg.startswith('#') and arg.strip() != ''
        ]

    return profile


def arg_converter(argList, style):
    """
    Convert between firejail command-line arguments (--example=something) and
    profile arguments (example something)

    :params argList: A list of firejail arguments

    :params style: Whether to convert arguments to command-line form or profile
    form
    """
    if style == 'to_profile':
        oldSep = '='
        newSep = ' '
        prefix = ''
    elif style == 'to_commandline':
        oldSep = ' '
        newSep = '='
        prefix = '--'
    newArgs = [prefix + word.replace(oldSep, newSep) for word in argList]
    # Additional strip of '--' if converting to profile form
    if style == 'to_profile':
        newArgs = [word[2:] for word in newArgs]

    # Remove invalid '--include' args if converting to command-line form
    elif style == 'to_commandline':
        newArgs = [word for word in newArgs if 'include' not in word]

    return newArgs


def run_firejail(program, allArgs):
    """
    Attempt to run the program in firejail, incrementally adding to the number
    of firejail arguments. Initial run has no additional params besides
    noprofile.

    :params program: The program name. If it doesn't exist in the user's path
    then the full path should be provided.

    :params allArgs: A list of all Firejail arguments to try, in command-line
    format.

    :returns goodArgs: A list of arguments that the user has reported to not
    affect the program

    :returns badArgs: A list of arguments that the user has reported to break
    the program when sandboxing with Firejail
    """
    goodArgs = ['firejail', '--noprofile', program]
    badArgs = []
    print('Attempting to run %s in Firejail' % program)
    for arg in allArgs:
        print('Running with', arg)
        subprocess.call(goodArgs)
        ans = input('Did %s run correctly? [y]/n ' % program)
        if ans == 'n' or ans == 'N':
            badArgs.append(arg)
        else:
            goodArgs.insert(-1, arg)
        print('\n')
    # Don't include 'firejail', '--noprofile', or program name in arguments
    goodArgs = goodArgs[2:-1]

    return goodArgs, badArgs


def main():
    profilePath = sys.argv[1]
    program = sys.argv[2]
    # Quick error check and extract arguments
    check_params(profilePath)
    profile = get_args(profilePath)
    allArgs = arg_converter(profile, 'to_commandline')
    # Find out which profile options break the program when running in firejail
    goodArgs, badArgs = run_firejail(program, allArgs)

    goodArgs = arg_converter(goodArgs, 'to_profile')
    badArgs = arg_converter(badArgs, 'to_profile')

    print('\n###########################')
    print('Debugging completed.')
    print(
        'Please copy the following and report it to the Firejail development',
        'team on GitHub at %s \n\n' %
        'https://github.com/netblue30/firejail/issues')

    subprocess.call(['firejail', '--version'])

    print('These profile options break the program.')
    print('```')
    for item in badArgs:
        print(item)
    print('```\n\n\n')

    print('This is a minimal working profile:')
    print('```')
    for item in goodArgs:
        print(item)
    print('```')


if __name__ == '__main__':
    main()