~ubuntu-branches/ubuntu/vivid/apparmor/vivid-proposed

« back to all changes in this revision

Viewing changes to .pc/easyprof-framework-policy.patch/utils/apparmor/easyprof.py

  • Committer: Package Import Robot
  • Author(s): Jamie Strandboge, Steve Beattie, Jamie Strandboge, Robie Basak
  • Date: 2015-03-28 07:22:30 UTC
  • Revision ID: package-import@ubuntu.com-20150328072230-9ojeskqf1ukx5146
Tags: 2.9.1-0ubuntu8
[ Steve Beattie ]
* debian/rules: run make check on the libapparmor library
* add-chromium-browser.patch: add support for chromium policies
  (LP: #1419294)
* debian/apparmor.{init,upstart}: add support for triggering
  aa-profile-hook runs when packages are updated via snappy system
  image updates (LP: #1434143)
* parser-fix_modifier_compilation_+_tests.patch: fix compilation
  of audit modifiers for exec and pivot_root and deny modifiers on
  link rules as well as significantly expand related tests
  (LP: #1431717, LP: #1432045, LP: #1433829)
* tests-fix_systemd_breakage_in_pivot_root-lp1436109.patch: work
  around pivot_root test failures due to init=systemd (LP: #1436109)
* GDM_X_authority-lp1432126.patch: add location GDM creates Xauthority
  file to X abstraction (LP: #1432126)

[ Jamie Strandboge ]
* easyprof-framework-policy.patch: add --include-templates-dir and
  --include-policy-groups-dir options to easyprof to support framework
  policy on snappy

[ Robie Basak ]
* Add /lib/apparmor/profile-load; moved from
  /lib/init/apparmor-profile-load from the upstart package. A wrapper at
  the original path is now provided by init-system-helpers. (LP: #1432683)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# ------------------------------------------------------------------
 
2
#
 
3
#    Copyright (C) 2011-2013 Canonical Ltd.
 
4
#
 
5
#    This program is free software; you can redistribute it and/or
 
6
#    modify it under the terms of version 2 of the GNU General Public
 
7
#    License published by the Free Software Foundation.
 
8
#
 
9
# ------------------------------------------------------------------
 
10
 
 
11
from __future__ import with_statement
 
12
 
 
13
import codecs
 
14
import copy
 
15
import glob
 
16
import json
 
17
import optparse
 
18
import os
 
19
import re
 
20
import shutil
 
21
import subprocess
 
22
import sys
 
23
import tempfile
 
24
 
 
25
#
 
26
# TODO: move this out to the common library
 
27
#
 
28
#from apparmor import AppArmorException
 
29
class AppArmorException(Exception):
 
30
    '''This class represents AppArmor exceptions'''
 
31
    def __init__(self, value):
 
32
        self.value = value
 
33
 
 
34
    def __str__(self):
 
35
        return repr(self.value)
 
36
#
 
37
# End common
 
38
#
 
39
 
 
40
DEBUGGING = False
 
41
 
 
42
#
 
43
# TODO: move this out to a utilities library
 
44
#
 
45
def error(out, exit_code=1, do_exit=True):
 
46
    '''Print error message and exit'''
 
47
    try:
 
48
        sys.stderr.write("ERROR: %s\n" % (out))
 
49
    except IOError:
 
50
        pass
 
51
 
 
52
    if do_exit:
 
53
        sys.exit(exit_code)
 
54
 
 
55
 
 
56
def warn(out):
 
57
    '''Print warning message'''
 
58
    try:
 
59
        sys.stderr.write("WARN: %s\n" % (out))
 
60
    except IOError:
 
61
        pass
 
62
 
 
63
 
 
64
def msg(out, output=sys.stdout):
 
65
    '''Print message'''
 
66
    try:
 
67
        sys.stdout.write("%s\n" % (out))
 
68
    except IOError:
 
69
        pass
 
70
 
 
71
 
 
72
def cmd(command):
 
73
    '''Try to execute the given command.'''
 
74
    debug(command)
 
75
    try:
 
76
        sp = subprocess.Popen(command, stdout=subprocess.PIPE,
 
77
                              stderr=subprocess.STDOUT)
 
78
    except OSError as ex:
 
79
        return [127, str(ex)]
 
80
 
 
81
    out = sp.communicate()[0]
 
82
    return [sp.returncode, out]
 
83
 
 
84
 
 
85
def cmd_pipe(command1, command2):
 
86
    '''Try to pipe command1 into command2.'''
 
87
    try:
 
88
        sp1 = subprocess.Popen(command1, stdout=subprocess.PIPE)
 
89
        sp2 = subprocess.Popen(command2, stdin=sp1.stdout)
 
90
    except OSError as ex:
 
91
        return [127, str(ex)]
 
92
 
 
93
    out = sp2.communicate()[0]
 
94
    return [sp2.returncode, out]
 
95
 
 
96
 
 
97
def debug(out):
 
98
    '''Print debug message'''
 
99
    if DEBUGGING:
 
100
        try:
 
101
            sys.stderr.write("DEBUG: %s\n" % (out))
 
102
        except IOError:
 
103
            pass
 
104
 
 
105
 
 
106
def valid_binary_path(path):
 
107
    '''Validate name'''
 
108
    try:
 
109
        a_path = os.path.abspath(path)
 
110
    except Exception:
 
111
        debug("Could not find absolute path for binary")
 
112
        return False
 
113
 
 
114
    if path != a_path:
 
115
        debug("Binary should use a normalized absolute path")
 
116
        return False
 
117
 
 
118
    if not os.path.exists(a_path):
 
119
        return True
 
120
 
 
121
    r_path = os.path.realpath(path)
 
122
    if r_path != a_path:
 
123
        debug("Binary should not be a symlink")
 
124
        return False
 
125
 
 
126
    return True
 
127
 
 
128
 
 
129
def valid_variable(v):
 
130
    '''Validate variable name'''
 
131
    debug("Checking '%s'" % v)
 
132
    try:
 
133
        (key, value) = v.split('=')
 
134
    except Exception:
 
135
        return False
 
136
 
 
137
    if not re.search(r'^@\{[a-zA-Z0-9_]+\}$', key):
 
138
        return False
 
139
 
 
140
    if '/' in value:
 
141
        rel_ok = False
 
142
        if not value.startswith('/'):
 
143
            rel_ok = True
 
144
        if not valid_path(value, relative_ok=rel_ok):
 
145
            return False
 
146
 
 
147
    if '"' in value:
 
148
        return False
 
149
 
 
150
    # If we made it here, we are safe
 
151
    return True
 
152
 
 
153
 
 
154
def valid_path(path, relative_ok=False):
 
155
    '''Valid path'''
 
156
    m = "Invalid path: %s" % (path)
 
157
    if not relative_ok and not path.startswith('/'):
 
158
        debug("%s (relative)" % (m))
 
159
        return False
 
160
 
 
161
    if '"' in path: # We double quote elsewhere
 
162
        debug("%s (quote)" % (m))
 
163
        return False
 
164
 
 
165
    if '../' in path:
 
166
        debug("%s (../ path escape)" % (m))
 
167
        return False
 
168
 
 
169
    try:
 
170
        p = os.path.normpath(path)
 
171
    except Exception:
 
172
        debug("%s (could not normalize)" % (m))
 
173
        return False
 
174
 
 
175
    if p != path:
 
176
        debug("%s (normalized path != path (%s != %s))" % (m, p, path))
 
177
        return False
 
178
 
 
179
    # If we made it here, we are safe
 
180
    return True
 
181
 
 
182
 
 
183
def _is_safe(s):
 
184
    '''Known safe regex'''
 
185
    if re.search(r'^[a-zA-Z_0-9\-\.]+$', s):
 
186
        return True
 
187
    return False
 
188
 
 
189
 
 
190
def valid_policy_vendor(s):
 
191
    '''Verify the policy vendor'''
 
192
    return _is_safe(s)
 
193
 
 
194
 
 
195
def valid_policy_version(v):
 
196
    '''Verify the policy version'''
 
197
    try:
 
198
        float(v)
 
199
    except ValueError:
 
200
        return False
 
201
    if float(v) < 0:
 
202
        return False
 
203
    return True
 
204
 
 
205
 
 
206
def valid_template_name(s, strict=False):
 
207
    '''Verify the template name'''
 
208
    if not strict and s.startswith('/'):
 
209
        if not valid_path(s):
 
210
            return False
 
211
        return True
 
212
    return _is_safe(s)
 
213
 
 
214
 
 
215
def valid_abstraction_name(s):
 
216
    '''Verify the template name'''
 
217
    return _is_safe(s)
 
218
 
 
219
 
 
220
def valid_profile_name(s):
 
221
    '''Verify the profile name'''
 
222
    # profile name specifies path
 
223
    if s.startswith('/'):
 
224
        if not valid_path(s):
 
225
            return False
 
226
        return True
 
227
 
 
228
    # profile name does not specify path
 
229
    # alpha-numeric and Debian version, plus '_'
 
230
    if re.search(r'^[a-zA-Z0-9][a-zA-Z0-9_\+\-\.:~]+$', s):
 
231
        return True
 
232
    return False
 
233
 
 
234
 
 
235
def valid_policy_group_name(s):
 
236
    '''Verify policy group name'''
 
237
    return _is_safe(s)
 
238
 
 
239
 
 
240
def get_directory_contents(path):
 
241
    '''Find contents of the given directory'''
 
242
    if not valid_path(path):
 
243
        return None
 
244
 
 
245
    files = []
 
246
    for f in glob.glob(path + "/*"):
 
247
        files.append(f)
 
248
 
 
249
    files.sort()
 
250
    return files
 
251
 
 
252
def open_file_read(path):
 
253
    '''Open specified file read-only'''
 
254
    try:
 
255
        orig = codecs.open(path, 'r', "UTF-8")
 
256
    except Exception:
 
257
        raise
 
258
 
 
259
    return orig
 
260
 
 
261
 
 
262
def verify_policy(policy):
 
263
    '''Verify policy compiles'''
 
264
    exe = "/sbin/apparmor_parser"
 
265
    if not os.path.exists(exe):
 
266
        rc, exe = cmd(['which', 'apparmor_parser'])
 
267
        if rc != 0:
 
268
            warn("Could not find apparmor_parser. Skipping verify")
 
269
            return True
 
270
 
 
271
    fn = ""
 
272
    # if policy starts with '/' and is one line, assume it is a path
 
273
    if len(policy.splitlines()) == 1 and valid_path(policy):
 
274
        fn = policy
 
275
    else:
 
276
        f, fn = tempfile.mkstemp(prefix='aa-easyprof')
 
277
        if not isinstance(policy, bytes):
 
278
            policy = policy.encode('utf-8')
 
279
        os.write(f, policy)
 
280
        os.close(f)
 
281
 
 
282
    rc, out = cmd([exe, '-QTK', fn])
 
283
    os.unlink(fn)
 
284
    if rc == 0:
 
285
        return True
 
286
    return False
 
287
 
 
288
#
 
289
# End utility functions
 
290
#
 
291
 
 
292
 
 
293
class AppArmorEasyProfile:
 
294
    '''Easy profile class'''
 
295
    def __init__(self, binary, opt):
 
296
        verify_options(opt)
 
297
        opt.ensure_value("conffile", "/etc/apparmor/easyprof.conf")
 
298
        self.conffile = os.path.abspath(opt.conffile)
 
299
        debug("Examining confile=%s" % (self.conffile))
 
300
 
 
301
        self.dirs = dict()
 
302
        if os.path.isfile(self.conffile):
 
303
            self._get_defaults()
 
304
 
 
305
        if opt.templates_dir and os.path.isdir(opt.templates_dir):
 
306
            self.dirs['templates'] = os.path.abspath(opt.templates_dir)
 
307
        elif not opt.templates_dir and \
 
308
             opt.template and \
 
309
             os.path.isfile(opt.template) and \
 
310
             valid_path(opt.template):
 
311
            # If we specified the template and it is an absolute path, just set
 
312
            # the templates directory to the parent of the template so we don't
 
313
            # have to require --template-dir with absolute paths.
 
314
            self.dirs['templates'] = os.path.abspath(os.path.dirname(opt.template))
 
315
        if opt.policy_groups_dir and os.path.isdir(opt.policy_groups_dir):
 
316
            self.dirs['policygroups'] = os.path.abspath(opt.policy_groups_dir)
 
317
 
 
318
 
 
319
        self.policy_version = None
 
320
        self.policy_vendor = None
 
321
        if (opt.policy_version and not opt.policy_vendor) or \
 
322
           (opt.policy_vendor and not opt.policy_version):
 
323
            raise AppArmorException("Must specify both policy version and vendor")
 
324
        if opt.policy_version and opt.policy_vendor:
 
325
            self.policy_vendor = opt.policy_vendor
 
326
            self.policy_version = str(opt.policy_version)
 
327
 
 
328
            for i in ['templates', 'policygroups']:
 
329
                d = os.path.join(self.dirs[i], \
 
330
                                 self.policy_vendor, \
 
331
                                 self.policy_version)
 
332
                if not os.path.isdir(d):
 
333
                    raise AppArmorException(
 
334
                            "Could not find %s directory '%s'" % (i, d))
 
335
                self.dirs[i] = d
 
336
 
 
337
        if not 'templates' in self.dirs:
 
338
            raise AppArmorException("Could not find templates directory")
 
339
        if not 'policygroups' in self.dirs:
 
340
            raise AppArmorException("Could not find policygroups directory")
 
341
 
 
342
        self.aa_topdir = "/etc/apparmor.d"
 
343
 
 
344
        self.binary = binary
 
345
        if binary:
 
346
            if not valid_binary_path(binary):
 
347
                raise AppArmorException("Invalid path for binary: '%s'" % binary)
 
348
 
 
349
        if opt.manifest:
 
350
            self.set_template(opt.template, allow_abs_path=False)
 
351
        else:
 
352
            self.set_template(opt.template)
 
353
 
 
354
        self.set_policygroup(opt.policy_groups)
 
355
        if opt.name:
 
356
            self.set_name(opt.name)
 
357
        elif self.binary != None:
 
358
            self.set_name(self.binary)
 
359
 
 
360
        self.templates = []
 
361
        for f in get_directory_contents(self.dirs['templates']):
 
362
            if os.path.isfile(f):
 
363
                self.templates.append(f)
 
364
        self.policy_groups = []
 
365
        for f in get_directory_contents(self.dirs['policygroups']):
 
366
            if os.path.isfile(f):
 
367
                self.policy_groups.append(f)
 
368
 
 
369
    def _get_defaults(self):
 
370
        '''Read in defaults from configuration'''
 
371
        if not os.path.exists(self.conffile):
 
372
            raise AppArmorException("Could not find '%s'" % self.conffile)
 
373
 
 
374
        # Read in the configuration
 
375
        f = open_file_read(self.conffile)
 
376
 
 
377
        pat = re.compile(r'^\w+=".*"?')
 
378
        for line in f:
 
379
            if not pat.search(line):
 
380
                continue
 
381
            if line.startswith("POLICYGROUPS_DIR="):
 
382
                d = re.split(r'=', line.strip())[1].strip('["\']')
 
383
                self.dirs['policygroups'] = d
 
384
            elif line.startswith("TEMPLATES_DIR="):
 
385
                d = re.split(r'=', line.strip())[1].strip('["\']')
 
386
                self.dirs['templates'] = d
 
387
        f.close()
 
388
 
 
389
        keys = self.dirs.keys()
 
390
        if 'templates' not in keys:
 
391
            raise AppArmorException("Could not find TEMPLATES_DIR in '%s'" % self.conffile)
 
392
        if 'policygroups' not in keys:
 
393
            raise AppArmorException("Could not find POLICYGROUPS_DIR in '%s'" % self.conffile)
 
394
 
 
395
        for k in self.dirs.keys():
 
396
            if not os.path.isdir(self.dirs[k]):
 
397
                raise AppArmorException("Could not find '%s'" % self.dirs[k])
 
398
 
 
399
    def set_name(self, name):
 
400
        '''Set name of policy'''
 
401
        self.name = name
 
402
 
 
403
    def get_template(self):
 
404
        '''Get contents of current template'''
 
405
        return open(self.template).read()
 
406
 
 
407
    def set_template(self, template, allow_abs_path=True):
 
408
        '''Set current template'''
 
409
        if "../" in template:
 
410
            raise AppArmorException('template "%s" contains "../" escape path' % (template))
 
411
        elif template.startswith('/') and not allow_abs_path:
 
412
            raise AppArmorException("Cannot use an absolute path template '%s'" % template)
 
413
 
 
414
        if template.startswith('/'):
 
415
            self.template = template
 
416
        else:
 
417
            self.template = os.path.join(self.dirs['templates'], template)
 
418
 
 
419
        if not os.path.exists(self.template):
 
420
            raise AppArmorException('%s does not exist' % (self.template))
 
421
 
 
422
    def get_templates(self):
 
423
        '''Get list of all available templates by filename'''
 
424
        return self.templates
 
425
 
 
426
    def get_policygroup(self, policygroup):
 
427
        '''Get contents of specific policygroup'''
 
428
        p = policygroup
 
429
        if not p.startswith('/'):
 
430
            p = os.path.join(self.dirs['policygroups'], p)
 
431
        if self.policy_groups == None or not p in self.policy_groups:
 
432
            raise AppArmorException("Policy group '%s' does not exist" % p)
 
433
        return open(p).read()
 
434
 
 
435
    def set_policygroup(self, policygroups):
 
436
        '''Set policygroups'''
 
437
        self.policy_groups = []
 
438
        if policygroups != None:
 
439
            for p in policygroups.split(','):
 
440
                if not p.startswith('/'):
 
441
                    p = os.path.join(self.dirs['policygroups'], p)
 
442
                if not os.path.exists(p):
 
443
                    raise AppArmorException('%s does not exist' % (p))
 
444
                self.policy_groups.append(p)
 
445
 
 
446
    def get_policy_groups(self):
 
447
        '''Get list of all policy groups by filename'''
 
448
        return self.policy_groups
 
449
 
 
450
    def gen_abstraction_rule(self, abstraction):
 
451
        '''Generate an abstraction rule'''
 
452
        p = os.path.join(self.aa_topdir, "abstractions", abstraction)
 
453
        if not os.path.exists(p):
 
454
            raise AppArmorException("%s does not exist" % p)
 
455
        return "#include <abstractions/%s>" % abstraction
 
456
 
 
457
    def gen_variable_declaration(self, dec):
 
458
        '''Generate a variable declaration'''
 
459
        if not valid_variable(dec):
 
460
            raise AppArmorException("Invalid variable declaration '%s'" % dec)
 
461
        # Make sure we always quote
 
462
        k, v = dec.split('=')
 
463
        return '%s="%s"' % (k, v)
 
464
 
 
465
    def gen_path_rule(self, path, access):
 
466
        rule = []
 
467
        if not path.startswith('/') and not path.startswith('@'):
 
468
            raise AppArmorException("'%s' should not be relative path" % path)
 
469
 
 
470
        owner = ""
 
471
        if path.startswith('/home/') or path.startswith("@{HOME"):
 
472
            owner = "owner "
 
473
 
 
474
        if path.endswith('/'):
 
475
            rule.append("%s %s," % (path, access))
 
476
            rule.append("%s%s** %s," % (owner, path, access))
 
477
        elif path.endswith('/**') or path.endswith('/*'):
 
478
            rule.append("%s %s," % (os.path.dirname(path), access))
 
479
            rule.append("%s%s %s," % (owner, path, access))
 
480
        else:
 
481
            rule.append("%s%s %s," % (owner, path, access))
 
482
 
 
483
        return rule
 
484
 
 
485
 
 
486
    def gen_policy(self, name,
 
487
                         binary=None,
 
488
                         profile_name=None,
 
489
                         template_var=[],
 
490
                         abstractions=None,
 
491
                         policy_groups=None,
 
492
                         read_path=[],
 
493
                         write_path=[],
 
494
                         author=None,
 
495
                         comment=None,
 
496
                         copyright=None,
 
497
                         no_verify=False):
 
498
        def find_prefix(t, s):
 
499
            '''Calculate whitespace prefix based on occurrence of s in t'''
 
500
            pat = re.compile(r'^ *%s' % s)
 
501
            p = ""
 
502
            for line in t.splitlines():
 
503
                if pat.match(line):
 
504
                    p = " " * (len(line) - len(line.lstrip()))
 
505
                    break
 
506
            return p
 
507
 
 
508
        policy = self.get_template()
 
509
        if '###ENDUSAGE###' in policy:
 
510
            found = False
 
511
            tmp = ""
 
512
            for line in policy.splitlines():
 
513
                if not found:
 
514
                    if line.startswith('###ENDUSAGE###'):
 
515
                        found = True
 
516
                    continue
 
517
                tmp += line + "\n"
 
518
            policy = tmp
 
519
 
 
520
        attachment = ""
 
521
        if binary:
 
522
            if not valid_binary_path(binary):
 
523
                raise AppArmorException("Invalid path for binary: '%s'" % \
 
524
                                        binary)
 
525
            if profile_name:
 
526
                attachment = 'profile "%s" "%s"' % (profile_name, binary)
 
527
            else:
 
528
                attachment = '"%s"' % binary
 
529
        elif profile_name:
 
530
            attachment = 'profile "%s"' % profile_name
 
531
        else:
 
532
            raise AppArmorException("Must specify binary and/or profile name")
 
533
        policy = re.sub(r'###PROFILEATTACH###', attachment, policy)
 
534
 
 
535
        policy = re.sub(r'###NAME###', name, policy)
 
536
 
 
537
        # Fill-in various comment fields
 
538
        if comment != None:
 
539
            policy = re.sub(r'###COMMENT###', "Comment: %s" % comment, policy)
 
540
 
 
541
        if author != None:
 
542
            policy = re.sub(r'###AUTHOR###', "Author: %s" % author, policy)
 
543
 
 
544
        if copyright != None:
 
545
            policy = re.sub(r'###COPYRIGHT###', "Copyright: %s" % copyright, policy)
 
546
 
 
547
        # Fill-in rules and variables with proper indenting
 
548
        search = '###ABSTRACTIONS###'
 
549
        prefix = find_prefix(policy, search)
 
550
        s = "%s# No abstractions specified" % prefix
 
551
        if abstractions != None:
 
552
            s = "%s# Specified abstractions" % (prefix)
 
553
            t = abstractions.split(',')
 
554
            t.sort()
 
555
            for i in t:
 
556
                s += "\n%s%s" % (prefix, self.gen_abstraction_rule(i))
 
557
        policy = re.sub(r' *%s' % search, s, policy)
 
558
 
 
559
        search = '###POLICYGROUPS###'
 
560
        prefix = find_prefix(policy, search)
 
561
        s = "%s# No policy groups specified" % prefix
 
562
        if policy_groups != None:
 
563
            s = "%s# Rules specified via policy groups" % (prefix)
 
564
            t = policy_groups.split(',')
 
565
            t.sort()
 
566
            for i in t:
 
567
                for line in self.get_policygroup(i).splitlines():
 
568
                    s += "\n%s%s" % (prefix, line)
 
569
                if i != policy_groups.split(',')[-1]:
 
570
                    s += "\n"
 
571
        policy = re.sub(r' *%s' % search, s, policy)
 
572
 
 
573
        search = '###VAR###'
 
574
        prefix = find_prefix(policy, search)
 
575
        s = "%s# No template variables specified" % prefix
 
576
        if len(template_var) > 0:
 
577
            s = "%s# Specified profile variables" % (prefix)
 
578
            template_var.sort()
 
579
            for i in template_var:
 
580
                s += "\n%s%s" % (prefix, self.gen_variable_declaration(i))
 
581
        policy = re.sub(r' *%s' % search, s, policy)
 
582
 
 
583
        search = '###READS###'
 
584
        prefix = find_prefix(policy, search)
 
585
        s = "%s# No read paths specified" % prefix
 
586
        if len(read_path) > 0:
 
587
            s = "%s# Specified read permissions" % (prefix)
 
588
            read_path.sort()
 
589
            for i in read_path:
 
590
                for r in self.gen_path_rule(i, 'rk'):
 
591
                    s += "\n%s%s" % (prefix, r)
 
592
        policy = re.sub(r' *%s' % search, s, policy)
 
593
 
 
594
        search = '###WRITES###'
 
595
        prefix = find_prefix(policy, search)
 
596
        s = "%s# No write paths specified" % prefix
 
597
        if len(write_path) > 0:
 
598
            s = "%s# Specified write permissions" % (prefix)
 
599
            write_path.sort()
 
600
            for i in write_path:
 
601
                for r in self.gen_path_rule(i, 'rwk'):
 
602
                    s += "\n%s%s" % (prefix, r)
 
603
        policy = re.sub(r' *%s' % search, s, policy)
 
604
 
 
605
        if no_verify:
 
606
            debug("Skipping policy verification")
 
607
        elif not verify_policy(policy):
 
608
            msg("\n" + policy)
 
609
            raise AppArmorException("Invalid policy")
 
610
 
 
611
        return policy
 
612
 
 
613
    def output_policy(self, params, count=0, dir=None):
 
614
        '''Output policy'''
 
615
        policy = self.gen_policy(**params)
 
616
        if not dir:
 
617
            if count:
 
618
                sys.stdout.write('### aa-easyprof profile #%d ###\n' % count)
 
619
            sys.stdout.write('%s\n' % policy)
 
620
        else:
 
621
            out_fn = ""
 
622
            if 'profile_name' in params:
 
623
                out_fn = params['profile_name']
 
624
            elif 'binary' in params:
 
625
                out_fn = params['binary']
 
626
            else: # should not ever reach this
 
627
                raise AppArmorException("Could not determine output filename")
 
628
 
 
629
            # Generate an absolute path, convertng any path delimiters to '.'
 
630
            out_fn = os.path.join(dir, re.sub(r'/', '.', out_fn.lstrip('/')))
 
631
            if os.path.exists(out_fn):
 
632
                raise AppArmorException("'%s' already exists" % out_fn)
 
633
 
 
634
            if not os.path.exists(dir):
 
635
                os.mkdir(dir)
 
636
 
 
637
            if not os.path.isdir(dir):
 
638
                raise AppArmorException("'%s' is not a directory" % dir)
 
639
 
 
640
            f, fn = tempfile.mkstemp(prefix='aa-easyprof')
 
641
            if not isinstance(policy, bytes):
 
642
                policy = policy.encode('utf-8')
 
643
            os.write(f, policy)
 
644
            os.close(f)
 
645
 
 
646
            shutil.move(fn, out_fn)
 
647
 
 
648
    def gen_manifest(self, params):
 
649
        '''Take params list and output a JSON file'''
 
650
        d = dict()
 
651
        d['security'] = dict()
 
652
        d['security']['profiles'] = dict()
 
653
 
 
654
        pkey = ""
 
655
        if 'profile_name' in params:
 
656
            pkey = params['profile_name']
 
657
        elif 'binary' in params:
 
658
            # when profile_name is not specified, the binary (path attachment)
 
659
            # also functions as the profile name
 
660
            pkey = params['binary']
 
661
        else:
 
662
            raise AppArmorException("Must supply binary or profile name")
 
663
 
 
664
        d['security']['profiles'][pkey] = dict()
 
665
 
 
666
        # Add the template since it isn't part of 'params'
 
667
        template = os.path.basename(self.template)
 
668
        if template != 'default':
 
669
            d['security']['profiles'][pkey]['template'] = template
 
670
 
 
671
        # Add the policy_version since it isn't part of 'params'
 
672
        if self.policy_version:
 
673
            d['security']['profiles'][pkey]['policy_version'] = float(self.policy_version)
 
674
        if self.policy_vendor:
 
675
            d['security']['profiles'][pkey]['policy_vendor'] = self.policy_vendor
 
676
 
 
677
        for key in params:
 
678
            if key == 'profile_name' or \
 
679
               (key == 'binary' and not 'profile_name' in params):
 
680
                continue # don't re-add the pkey
 
681
            elif key == 'binary' and not params[key]:
 
682
                continue # binary can by None when specifying --profile-name
 
683
            elif key == 'template_var':
 
684
                d['security']['profiles'][pkey]['template_variables'] = dict()
 
685
                for tvar in params[key]:
 
686
                    if not self.gen_variable_declaration(tvar):
 
687
                        raise AppArmorException("Malformed template_var '%s'" % tvar)
 
688
                    (k, v) = tvar.split('=')
 
689
                    k = k.lstrip('@').lstrip('{').rstrip('}')
 
690
                    d['security']['profiles'][pkey]['template_variables'][k] = v
 
691
            elif key == 'abstractions' or key == 'policy_groups':
 
692
                d['security']['profiles'][pkey][key] = params[key].split(",")
 
693
                d['security']['profiles'][pkey][key].sort()
 
694
            else:
 
695
                d['security']['profiles'][pkey][key] = params[key]
 
696
        json_str = json.dumps(d,
 
697
                              sort_keys=True,
 
698
                              indent=2,
 
699
                              separators=(',', ': ')
 
700
                             )
 
701
        return json_str
 
702
 
 
703
def print_basefilenames(files):
 
704
    for i in files:
 
705
        sys.stdout.write("%s\n" % (os.path.basename(i)))
 
706
 
 
707
def print_files(files):
 
708
    for i in files:
 
709
        with open(i) as f:
 
710
            sys.stdout.write(f.read()+"\n")
 
711
 
 
712
def check_manifest_conflict_args(option, opt_str, value, parser):
 
713
    '''Check for -m/--manifest with conflicting args'''
 
714
    conflict_args = ['abstractions',
 
715
                     'read_path',
 
716
                     'write_path',
 
717
                     # template always get set to 'default', can't conflict
 
718
                     # 'template',
 
719
                     'policy_groups',
 
720
                     'policy_version',
 
721
                     'policy_vendor',
 
722
                     'name',
 
723
                     'profile_name',
 
724
                     'comment',
 
725
                     'copyright',
 
726
                     'author',
 
727
                     'template_var']
 
728
    for conflict in conflict_args:
 
729
        if getattr(parser.values, conflict, False):
 
730
            raise optparse.OptionValueError("can't use --%s with --manifest " \
 
731
                                            "argument" % conflict)
 
732
    setattr(parser.values, option.dest, value)
 
733
 
 
734
def check_for_manifest_arg(option, opt_str, value, parser):
 
735
    '''Check for -m/--manifest with conflicting args'''
 
736
    if parser.values.manifest:
 
737
        raise optparse.OptionValueError("can't use --%s with --manifest " \
 
738
                                        "argument" % opt_str.lstrip('-'))
 
739
    setattr(parser.values, option.dest, value)
 
740
 
 
741
def check_for_manifest_arg_append(option, opt_str, value, parser):
 
742
    '''Check for -m/--manifest with conflicting args (with append)'''
 
743
    if parser.values.manifest:
 
744
         raise optparse.OptionValueError("can't use --%s with --manifest " \
 
745
                                         "argument" % opt_str.lstrip('-'))
 
746
    parser.values.ensure_value(option.dest, []).append(value)
 
747
 
 
748
def add_parser_policy_args(parser):
 
749
    '''Add parser arguments'''
 
750
    parser.add_option("-a", "--abstractions",
 
751
                      action="callback",
 
752
                      callback=check_for_manifest_arg,
 
753
                      type=str,
 
754
                      dest="abstractions",
 
755
                      help="Comma-separated list of abstractions",
 
756
                      metavar="ABSTRACTIONS")
 
757
    parser.add_option("--read-path",
 
758
                      action="callback",
 
759
                      callback=check_for_manifest_arg_append,
 
760
                      type=str,
 
761
                      dest="read_path",
 
762
                      help="Path allowing owner reads",
 
763
                      metavar="PATH")
 
764
    parser.add_option("--write-path",
 
765
                      action="callback",
 
766
                      callback=check_for_manifest_arg_append,
 
767
                      type=str,
 
768
                      dest="write_path",
 
769
                      help="Path allowing owner writes",
 
770
                      metavar="PATH")
 
771
    parser.add_option("-t", "--template",
 
772
                      dest="template",
 
773
                      help="Use non-default policy template",
 
774
                      metavar="TEMPLATE",
 
775
                      default='default')
 
776
    parser.add_option("--templates-dir",
 
777
                      dest="templates_dir",
 
778
                      help="Use non-default templates directory",
 
779
                      metavar="DIR")
 
780
    parser.add_option("-p", "--policy-groups",
 
781
                      action="callback",
 
782
                      callback=check_for_manifest_arg,
 
783
                      type=str,
 
784
                      help="Comma-separated list of policy groups",
 
785
                      metavar="POLICYGROUPS")
 
786
    parser.add_option("--policy-groups-dir",
 
787
                      dest="policy_groups_dir",
 
788
                      help="Use non-default policy-groups directory",
 
789
                      metavar="DIR")
 
790
    parser.add_option("--policy-version",
 
791
                      action="callback",
 
792
                      callback=check_for_manifest_arg,
 
793
                      type=str,
 
794
                      dest="policy_version",
 
795
                      help="Specify version for templates and policy groups",
 
796
                      metavar="VERSION")
 
797
    parser.add_option("--policy-vendor",
 
798
                      action="callback",
 
799
                      callback=check_for_manifest_arg,
 
800
                      type=str,
 
801
                      dest="policy_vendor",
 
802
                      help="Specify vendor for templates and policy groups",
 
803
                      metavar="VENDOR")
 
804
    parser.add_option("--profile-name",
 
805
                      action="callback",
 
806
                      callback=check_for_manifest_arg,
 
807
                      type=str,
 
808
                      dest="profile_name",
 
809
                      help="AppArmor profile name",
 
810
                      metavar="PROFILENAME")
 
811
 
 
812
def parse_args(args=None, parser=None):
 
813
    '''Parse arguments'''
 
814
    global DEBUGGING
 
815
 
 
816
    if parser == None:
 
817
        parser = optparse.OptionParser()
 
818
 
 
819
    parser.add_option("-c", "--config-file",
 
820
                      dest="conffile",
 
821
                      help="Use alternate configuration file",
 
822
                      metavar="FILE")
 
823
    parser.add_option("-d", "--debug",
 
824
                      help="Show debugging output",
 
825
                      action='store_true',
 
826
                      default=False)
 
827
    parser.add_option("--no-verify",
 
828
                      help="Don't verify policy using 'apparmor_parser -p'",
 
829
                      action='store_true',
 
830
                      default=False)
 
831
    parser.add_option("--list-templates",
 
832
                      help="List available templates",
 
833
                      action='store_true',
 
834
                      default=False)
 
835
    parser.add_option("--show-template",
 
836
                      help="Show specified template",
 
837
                      action='store_true',
 
838
                      default=False)
 
839
    parser.add_option("--list-policy-groups",
 
840
                      help="List available policy groups",
 
841
                      action='store_true',
 
842
                      default=False)
 
843
    parser.add_option("--show-policy-group",
 
844
                      help="Show specified policy groups",
 
845
                      action='store_true',
 
846
                      default=False)
 
847
    parser.add_option("-n", "--name",
 
848
                      action="callback",
 
849
                      callback=check_for_manifest_arg,
 
850
                      type=str,
 
851
                      dest="name",
 
852
                      help="Name of policy (not AppArmor profile name)",
 
853
                      metavar="COMMENT")
 
854
    parser.add_option("--comment",
 
855
                      action="callback",
 
856
                      callback=check_for_manifest_arg,
 
857
                      type=str,
 
858
                      dest="comment",
 
859
                      help="Comment for policy",
 
860
                      metavar="COMMENT")
 
861
    parser.add_option("--author",
 
862
                      action="callback",
 
863
                      callback=check_for_manifest_arg,
 
864
                      type=str,
 
865
                      dest="author",
 
866
                      help="Author of policy",
 
867
                      metavar="COMMENT")
 
868
    parser.add_option("--copyright",
 
869
                      action="callback",
 
870
                      callback=check_for_manifest_arg,
 
871
                      type=str,
 
872
                      dest="copyright",
 
873
                      help="Copyright for policy",
 
874
                      metavar="COMMENT")
 
875
    parser.add_option("--template-var",
 
876
                      action="callback",
 
877
                      callback=check_for_manifest_arg_append,
 
878
                      type=str,
 
879
                      dest="template_var",
 
880
                      help="Declare AppArmor variable",
 
881
                      metavar="@{VARIABLE}=VALUE")
 
882
    parser.add_option("--output-format",
 
883
                      action="store",
 
884
                      dest="output_format",
 
885
                      help="Specify output format as text (default) or json",
 
886
                      metavar="FORMAT",
 
887
                      default="text")
 
888
    parser.add_option("--output-directory",
 
889
                      action="store",
 
890
                      dest="output_directory",
 
891
                      help="Output policy to this directory",
 
892
                      metavar="DIR")
 
893
    # This option conflicts with any of the value arguments, e.g. name,
 
894
    # author, template-var, etc.
 
895
    parser.add_option("-m", "--manifest",
 
896
                      action="callback",
 
897
                      callback=check_manifest_conflict_args,
 
898
                      type=str,
 
899
                      dest="manifest",
 
900
                      help="JSON manifest file",
 
901
                      metavar="FILE")
 
902
    parser.add_option("--verify-manifest",
 
903
                      action="store_true",
 
904
                      default=False,
 
905
                      dest="verify_manifest",
 
906
                      help="Verify JSON manifest file")
 
907
 
 
908
 
 
909
    # add policy args now
 
910
    add_parser_policy_args(parser)
 
911
 
 
912
    (my_opt, my_args) = parser.parse_args(args)
 
913
 
 
914
    if my_opt.debug:
 
915
        DEBUGGING = True
 
916
    return (my_opt, my_args)
 
917
 
 
918
def gen_policy_params(binary, opt):
 
919
    '''Generate parameters for gen_policy'''
 
920
    params = dict(binary=binary)
 
921
 
 
922
    if not binary and not opt.profile_name:
 
923
        raise AppArmorException("Must specify binary and/or profile name")
 
924
 
 
925
    if opt.profile_name:
 
926
        params['profile_name'] = opt.profile_name
 
927
 
 
928
    if opt.name:
 
929
        params['name'] = opt.name
 
930
    else:
 
931
        if opt.profile_name:
 
932
            params['name'] = opt.profile_name
 
933
        elif binary:
 
934
            params['name'] = os.path.basename(binary)
 
935
 
 
936
    if opt.template_var: # What about specified multiple times?
 
937
        params['template_var'] = opt.template_var
 
938
    if opt.abstractions:
 
939
        params['abstractions'] = opt.abstractions
 
940
    if opt.policy_groups:
 
941
        params['policy_groups'] = opt.policy_groups
 
942
    if opt.read_path:
 
943
        params['read_path'] = opt.read_path
 
944
    if opt.write_path:
 
945
        params['write_path'] = opt.write_path
 
946
    if opt.comment:
 
947
        params['comment'] = opt.comment
 
948
    if opt.author:
 
949
        params['author'] = opt.author
 
950
    if opt.copyright:
 
951
        params['copyright'] = opt.copyright
 
952
    if opt.policy_version and opt.output_format == "json":
 
953
        params['policy_version'] = opt.policy_version
 
954
    if opt.policy_vendor and opt.output_format == "json":
 
955
        params['policy_vendor'] = opt.policy_vendor
 
956
 
 
957
    return params
 
958
 
 
959
def parse_manifest(manifest, opt_orig):
 
960
    '''Take a JSON manifest as a string and updates options, returning an
 
961
       updated binary. Note that a JSON file may contain multiple profiles.'''
 
962
 
 
963
    try:
 
964
        m = json.loads(manifest)
 
965
    except ValueError:
 
966
        raise AppArmorException("Could not parse manifest")
 
967
 
 
968
    if 'security' in m:
 
969
        top_table = m['security']
 
970
    else:
 
971
        top_table = m
 
972
 
 
973
    if 'profiles' not in top_table:
 
974
        raise AppArmorException("Could not parse manifest (could not find 'profiles')")
 
975
    table = top_table['profiles']
 
976
 
 
977
    # generally mirrors what is settable in gen_policy_params()
 
978
    valid_keys = ['abstractions',
 
979
                  'author',
 
980
                  'binary',
 
981
                  'comment',
 
982
                  'copyright',
 
983
                  'name',
 
984
                  'policy_groups',
 
985
                  'policy_version',
 
986
                  'policy_vendor',
 
987
                  'profile_name',
 
988
                  'read_path',
 
989
                  'template',
 
990
                  'template_variables',
 
991
                  'write_path',
 
992
                 ]
 
993
 
 
994
    profiles = []
 
995
 
 
996
    for profile_name in table:
 
997
        if not isinstance(table[profile_name], dict):
 
998
            raise AppArmorException("Wrong JSON structure")
 
999
        opt = copy.deepcopy(opt_orig)
 
1000
 
 
1001
        # The JSON structure is:
 
1002
        # {
 
1003
        #   "security": {
 
1004
        #     <profile_name>: {
 
1005
        #       "binary": ...
 
1006
        #       ...
 
1007
        # but because binary can be the profile name, we need to handle
 
1008
        # 'profile_name' and 'binary' special. If a profile_name starts with
 
1009
        # '/', then it is considered the binary. Otherwise, set the
 
1010
        # profile_name and set the binary if it is in the JSON.
 
1011
        binary = None
 
1012
        if profile_name.startswith('/'):
 
1013
            if 'binary' in table[profile_name]:
 
1014
                raise AppArmorException("Profile name should not specify path with binary")
 
1015
            binary = profile_name
 
1016
        else:
 
1017
            setattr(opt, 'profile_name', profile_name)
 
1018
            if 'binary' in table[profile_name]:
 
1019
                binary = table[profile_name]['binary']
 
1020
                setattr(opt, 'binary', binary)
 
1021
 
 
1022
        for key in table[profile_name]:
 
1023
            if key not in valid_keys:
 
1024
                raise AppArmorException("Invalid key '%s'" % key)
 
1025
 
 
1026
            if key == 'binary':
 
1027
                continue #  handled above
 
1028
            elif key == 'abstractions' or key == 'policy_groups':
 
1029
                setattr(opt, key, ",".join(table[profile_name][key]))
 
1030
            elif key == "template_variables":
 
1031
                t = table[profile_name]['template_variables']
 
1032
                vlist = []
 
1033
                for v in t.keys():
 
1034
                    vlist.append("@{%s}=%s" % (v, t[v]))
 
1035
                    setattr(opt, 'template_var', vlist)
 
1036
            else:
 
1037
                if hasattr(opt, key):
 
1038
                    setattr(opt, key, table[profile_name][key])
 
1039
 
 
1040
        profiles.append( (binary, opt) )
 
1041
 
 
1042
    return profiles
 
1043
 
 
1044
 
 
1045
def verify_options(opt, strict=False):
 
1046
    '''Make sure our options are valid'''
 
1047
    if hasattr(opt, 'binary') and opt.binary and not valid_path(opt.binary):
 
1048
        raise AppArmorException("Invalid binary '%s'" % opt.binary)
 
1049
    if hasattr(opt, 'profile_name') and opt.profile_name != None and \
 
1050
       not valid_profile_name(opt.profile_name):
 
1051
        raise AppArmorException("Invalid profile name '%s'" % opt.profile_name)
 
1052
    if hasattr(opt, 'binary') and opt.binary and \
 
1053
       hasattr(opt, 'profile_name') and opt.profile_name != None and \
 
1054
       opt.profile_name.startswith('/'):
 
1055
        raise AppArmorException("Profile name should not specify path with binary")
 
1056
    if hasattr(opt, 'policy_vendor') and opt.policy_vendor and \
 
1057
       not valid_policy_vendor(opt.policy_vendor):
 
1058
        raise AppArmorException("Invalid policy vendor '%s'" % \
 
1059
                                opt.policy_vendor)
 
1060
    if hasattr(opt, 'policy_version') and opt.policy_version and \
 
1061
       not valid_policy_version(opt.policy_version):
 
1062
        raise AppArmorException("Invalid policy version '%s'" % \
 
1063
                                opt.policy_version)
 
1064
    if hasattr(opt, 'template') and opt.template and \
 
1065
       not valid_template_name(opt.template, strict):
 
1066
        raise AppArmorException("Invalid template '%s'" % opt.template)
 
1067
    if hasattr(opt, 'template_var') and opt.template_var:
 
1068
        for i in opt.template_var:
 
1069
            if not valid_variable(i):
 
1070
                raise AppArmorException("Invalid variable '%s'" % i)
 
1071
    if hasattr(opt, 'policy_groups') and opt.policy_groups:
 
1072
        for i in opt.policy_groups.split(','):
 
1073
            if not valid_policy_group_name(i):
 
1074
                raise AppArmorException("Invalid policy group '%s'" % i)
 
1075
    if hasattr(opt, 'abstractions') and opt.abstractions:
 
1076
        for i in opt.abstractions.split(','):
 
1077
            if not valid_abstraction_name(i):
 
1078
                raise AppArmorException("Invalid abstraction '%s'" % i)
 
1079
    if hasattr(opt, 'read_paths') and opt.read_paths:
 
1080
        for i in opt.read_paths:
 
1081
            if not valid_path(i):
 
1082
                raise AppArmorException("Invalid read path '%s'" % i)
 
1083
    if hasattr(opt, 'write_paths') and opt.write_paths:
 
1084
        for i in opt.write_paths:
 
1085
            if not valid_path(i):
 
1086
                raise AppArmorException("Invalid write path '%s'" % i)
 
1087
 
 
1088
 
 
1089
def verify_manifest(params, args=None):
 
1090
    '''Verify manifest for safe and unsafe options'''
 
1091
    err_str = ""
 
1092
    (opt, args) = parse_args(args)
 
1093
    fake_easyp = AppArmorEasyProfile(None, opt)
 
1094
 
 
1095
    unsafe_keys = ['read_path', 'write_path']
 
1096
    safe_abstractions = ['base']
 
1097
    for k in params:
 
1098
        debug("Examining %s=%s" % (k, params[k]))
 
1099
        if k in unsafe_keys:
 
1100
            err_str += "\nfound %s key" % k
 
1101
        elif k == 'profile_name':
 
1102
            if params['profile_name'].startswith('/') or \
 
1103
               '*' in params['profile_name']:
 
1104
                err_str += "\nprofile_name '%s'" % params['profile_name']
 
1105
        elif k == 'abstractions':
 
1106
            for a in params['abstractions'].split(','):
 
1107
                if not a in safe_abstractions:
 
1108
                    err_str += "\nfound '%s' abstraction" % a
 
1109
        elif k == "template_var":
 
1110
            pat = re.compile(r'[*/\{\}\[\]]')
 
1111
            for tv in params['template_var']:
 
1112
                if not fake_easyp.gen_variable_declaration(tv):
 
1113
                    err_str += "\n%s" % tv
 
1114
                    continue
 
1115
                tv_val = tv.split('=')[1]
 
1116
                debug("Examining %s" % tv_val)
 
1117
                if '..' in tv_val or pat.search(tv_val):
 
1118
                     err_str += "\n%s" % tv
 
1119
 
 
1120
    if err_str:
 
1121
        warn("Manifest definition is potentially unsafe%s" % err_str)
 
1122
        return False
 
1123
 
 
1124
    return True
 
1125