2
0
mirror of https://gitlab.com/apparmor/apparmor synced 2025-08-31 14:25:52 +00:00

Compare commits

...

84 Commits

Author SHA1 Message Date
Steve Beattie
b37bd8a1aa Point makefile at correct launchpad branch to generate tarball from. 2015-04-23 12:46:45 -07:00
Christian Boltz
ead71a306a Enable testloops for nosetests
Ensure nosetests sees all tests in the tests[] tuples. This requires
some name changes because nosetests thinks all function names containing
"test" are tests. (A "not a test" docorator would be an alternative, but
that would require some try/except magic to avoid a dependency on nose.)

To avoid nosetests thinks the functions are a test,
- rename setup_all_tests() to setup_all_loops()
- rename regex_test() to _regex_test() (in test-regex_matches.py)

Also add the module_name as parameter to setup_all_loops and always run
it (not only if __name__ == '__main__').

Known issue: nosetests errors out with
    ValueError: no such test method in <class ...>: stub_test
when trying to run a single test generated out of tests[].
(debugging hint: stub_test is the name used in setup_test_loop().)
But that's still an improvement over not seeing those tests at all ;-)


Acked-by: Steve Beattie <steve@nxnw.org> for trunk and 2.9.
2015-04-22 22:03:36 +02:00
Christian Boltz
aa45be1c10 Fix crash in serialize_profile_from_old_profiles()
Assume you have a profile like

    /bin/foo {
      /etc/ r,
      network,
      /usr/ r,
    }

(important: there must be be a non-path rule between the two path blocks)

Then run aa-logprof and add another path event. When choosing (V)iew changes,
it will crash with a misleading

  File ".../utils/apparmor/aamode.py", line 205, in split_mode
      other = mode - user
      TypeError: unsupported operand type(s) for -: 'collections.defaultdict' and 'set'

The reason for this is our beloved hasher, which is playing funny games
another time.

The patch wraps the hasher usage with a check for the parent element to
avoid auto-creation of empty childs, which then lead to the above crash.


BTW: This is another issue uncovered by the LibreOffice profile ;-)


Acked-by: Steve Beattie <steve@nxnw.org> for trunk and 2.9.
2015-04-16 13:11:39 +02:00
Steve Beattie
c1c5192532 profiles: update postfix-common abstraction
Merge from trunk revision 3012

Update the postfix-common abstraction to cope with signal and unix
socket mediation, update the access to the sasl library locations
in a multiarch compliant way, and allow access to limited bits
of the filesystem paths under which postfix chroots itself to
(/var/spool/postfix/ on Ubuntu).

Signed-off-by: Steve Beattie <steve@nxnw.org>
Acked-by: Tyler Hicks <tyhicks@canonical.com>
2015-04-15 23:35:52 -07:00
Christian Boltz
2b9260f27a Fix serialize_profile_from_old_profiles() to not crash on "@{var} +="
serialize_profile_from_old_profiles() calls store_list_var() with an
empty hasher. This fails for "+=" because in this case store_list_var()
expects a non-empty hasher with the variable already defined, and raises
an exception because of the empty hasher.

This patch sets "correct = False" if a "+=" operation appears, which
means the variable will be written in "clean" mode instead.

Adding proper support for "add to variable" needs big changes (like
storing a variable's "history" - where it was initially defined and what
got added where).



Acked-by: Steve Beattie <steve@nxnw.org> for trunk and 2.9.
2015-04-16 02:02:02 +02:00
Christian Boltz
4063647a5f fix handling of adding to variables
the LibreOffice profile uncovered that handling of @{var} += is broken:

  File ".../utils/apparmor/aa.py", line 3272, in store_list_var
    var[list_var] = set(var[list_var] + vlist)
TypeError: unsupported operand type(s) for +: 'set' and 'list'

This patch fixes it:
- change separate_vars() to use and return a set instead of a list
  (FYI: separate_vars() is only called by store_list_var())
- adoptstore_list_var() to expect a set
- remove some old comments in these functions
- explain the less-intuitive parameters of store_list_var()

Also add some tests for separate_vars() and store_list_var().
The tests were developed based on the old code, but not all of them
succeed with the old code.

As usual, the tests uncovered some interesting[tm] behaviour in
separate_vars() (see the XXX comments and tell me what the really
expected behaviour is ;-)


Acked-by: Steve Beattie <steve@nxnw.org> for trunk and 2.9
2015-04-16 01:59:10 +02:00
Christian Boltz
b4048cf3de logparser.py: change mask only for path events
Move the code that does the c -> a and d -> w replacement in denied_mask
and requested_mask so that it only runs for path and exec events, but not
for other events (like dbus and ptrace). The validate_log_mode() and
log_str_to_mode() calls are also moved.

Technically, this means moving code from parse_event() to the path
and exec sections in add_event_to_tree().

This also means aa-logprof no longer crashes if it hits a ptrace or
dbus event in the log.

The "if dmask:" and "if rmask:" checks are removed - if a path event
doesn't have these two, it is totally broken and worth a aa-logprof
crash ;-)

Also adjust the parse_event() tests to expect the "raw" mask instead of
a set.

Note: the 2.9 branch doesn't contain test-capability.py, therefore I
skipped this part of the patch for obvious reasons ;-)

This patch fixes
https://bugs.launchpad.net/apparmor/+bug/1426651 and
https://bugs.launchpad.net/apparmor/+bug/1243932


I manually tested that
- c and d log events are still converted to a and w
- aa-logprof handles exec events correctly
- ptrace events no longer crash aa-logprof

Note: add_event_to_tree() is not covered by tests.


Acked-by: Steve Beattie <steve@nxnw.org> for trunk and 2.9
2015-04-16 01:53:39 +02:00
Christian Boltz
8a475341e8 utils: simplify serialize_parse_profile_start()
Merge from trunk revision 3001

Change serialize_parse_profile_start() to use parse_profile_start()
instead of using duplicated code.

The behaviour is mostly kept, with the exception that the function is
more strict now and raises exceptions instead of ignoring errors.

In practise, this won't change anything because the profiles are parsed
with parse_profile() (which calls parse_profile_start()) - and that
already errors out.

The tests are updated to match the more strict behaviour.

The next step would be to drop serialize_parse_profile_start()
completely, but this isn't urgent and can/should be done when we have
test coverage for serialize_profile_from_old_profile() one day ;-)

Acked-by: Steve Beattie <steve@nxnw.org>
2015-04-13 19:07:47 -07:00
Christian Boltz
5ca6986b43 utils: fix is_skippable_dir() and add tests
Merge from trunk revision 3000

Fix is_skippable_dir() - the regex also matched things like
/etc/apparmor.d/dont_disable, while it should match on the full
directory name.

Also add some tests based on a real-world aa-logprof run (with "print (path)"
in is_skippable_dir()) and some additional "funny"[tm] dirs.

Needless to say that the tests
        ('dont_disable',                False),
        ('/etc/apparmor.d/cache_foo',   False),
will fail with the old is_skippable_dir().

Acked-by: Steve Beattie <steve@nxnw.org>
2015-04-13 19:00:47 -07:00
Christian Boltz
4b58cf3bc4 utils: replace RE_PROFILE_START
Merge from trunk revision 2999

Replace RE_PROFILE_START with RE_PROFILE_START_2 and adjust all
code sections that used RE_PROFILE_START_2.

The only real change is that test_get_flags_invalid_01 and
test_get_flags_invalid_02 now expect AppArmorException instead of
AppArmorBug.

Acked-by: Steve Beattie <steve@nxnw.org> for trunk
2015-04-13 18:58:25 -07:00
Christian Boltz
a373b4ee93 utils: implement attachment handling after rewriting set_profile_flags()
to use write_header(), and making set_profile_flags
more strict.

Merge from trunk revisions 2996, 2997, and 2998.

Changes in set_profile_flags():
- rewrite set_profile_flags to use parse_profile_start_line() and
  write_header().
- replace the silent failure for non-existing files with a proper
  exception (using lazy programming - the check is done by removing the
  "if os.path.isfile()" check, open_file_read then raises the
  exception ;-)
- comment out regex_hat_flag and the code that was supposed to handle
  hat flags, which were totally broken. We'll need another patch to fix
  it, and we also need to decide if we want to do that because it
  introduces a behaviour change (currently, aa-complain etc. don't
  change hat flags).

The tests for set_profile_flags() are also updated:
- prepend a space to comments because write_header always adds a space
  between '{' and the comment
- remove a test with superfluous quotes that are no longer kept
  (that's
  just a profile cleanup, so dropping that test is the easiest way)
- update test_set_flags_10 and test_set_flags_12 to use the correct
  profile name
- enable the tests for invalid (empty) flags
- update the test for a non-existing file

this patch makes set_profile_flags more strict:
- raise AppArmorBug if newflags contains only whitespace
- raise AppArmorBug if the file doesn't contain the specified profile or
  no profile at all

The tests are adjusted to expect AppArmorBug instead of a silent
failure. Also, some tests are added for profile=None, which means to
change the flags for all profiles in a file.
- test_set_flags_08 is now test_set_flags_invalid_04
- test_set_flags_invalid_03 is changed to only contain one reason for
  a failure, not two ;-)

Finally implement attachment handling

This patch implements attachment handling - aa-logprof now works with
profiles that have an attachment defined, instead of ignoring audit.log
entries for those profiles.

Changes:
- parse_profile_start_line(): remove workaround that merged the
  attachment into the profile name
- parse_profile_data(): store attachment when parsing a profile
- update test_parse_profile_start_03,
  test_serialize_parse_profile_start_03,
  test_set_flags_nochange_09 and some parse_profile_start_line() tests -
  they now expect correct attachment handling

Acked-by: Steve Beattie <steve@nxnw.org>
2015-04-13 18:53:59 -07:00
Christian Boltz
8d5569f20b utils: rewrite parse_profile_start()
Merge from trunk revision 2990

Rewrite parse_profile_start() in aa.py to a more readable version.
The behaviour remains unchanged (and is covered by tests).

Acked-by: Kshitij Gupta <kgupta8592@gmail.com>
2015-04-13 18:49:37 -07:00
Christian Boltz
5390777e45 utils: add tests for RE_PROFILE_START_2 and parse_profile_start_line()
Merge from trunk revision 2989

Also add AANamedRegexTest class that can be used to test a regex with
named match groups.

Acked-by: Steve Beattie <steve@nxnw.org>
2015-04-13 18:47:12 -07:00
Christian Boltz
79240e7ddd utils: test new parameters of write_header()
Merge from trunk revision 2988

Change the write_header tests so that the 'profile_keyword' and
'header_comment' parameters can be (and are) tested:
- add a None for both to the existing tests
- add some tests that come with the profile keyword and/or a comment

Acked-by: Steve Beattie <steve@nxnw.org>
2015-04-13 18:45:05 -07:00
Christian Boltz
494daee246 utils: extend and partially rewrite write_header()
Merge from trunk revision 2987

- add support for prof_data['header_comment'] (comment after '{')
  and prof_data['profile_keyword'] (to force the 'profile' keyword, even
  if it isn't needed) to write_header().
  (set_profile_flags() will be the only user of these two for now)

- fix a crash if depth is not an integer - for example,
      len('   ')/2   # 3 spaces = 1.5
  would cause a crash.
  Also add a test for 1.5 and 1.3 spaces.

- rewrite the handling of flags to avoid we have to maintain two
  different template lines.

- update the tests to set 'profile_keyword' and 'header_comment' to None.
  This avoids big changes in the test code. I'll send another patch that
  makes sure profile_keyword and header_comment are tested ;-)

Acked-by: Steve Beattie <steve@nxnw.org>
2015-04-13 18:41:59 -07:00
Christian Boltz
194cbfa94c utils: add attachment to parse_profile_start() return values
Merge from trunk revision 2986

Add the attachment to the parse_profile_start() and
serialize_parse_profile_start() return values, and adjust the functions
calling the *parse_profile_start() functions to save the attachment in
the "attachment" variable (which isn't used yet).

Also adjust the tests for the added return value.

(Sorry for not getting the resultset right from the beginning!)

Acked-by: Steve Beattie <steve@nxnw.org>
2015-04-13 18:36:42 -07:00
Christian Boltz
9452e1e2af utils: Add support for attachments to write_header()
Merge from trunk revision 2985

Also fix a little bug that added the profile keyword if the path needed
quotes (profile "/foo bar" - but "/foo bar" is enough). This was caused
by a regex that always matched on quoted paths (hint: "/ matches
^[^/] ;-)

Also add some tests with attachments and update the test for the bugfix
mentioned above.

Now the remaining part is to make sure that prof_data['attachment'] gets
set when parsing the profiles :-)

Acked-by: Steve Beattie <steve@nxnw.org>
2015-04-13 18:34:33 -07:00
Christian Boltz
1556f782e3 utils: add tests for set_profile_flags() (and some fun)
Merge from trunk commit 2983

Add various tests for set_profile_flags, and document various
interesting[tm] things I discovered while writing the tests (see
the inline comments for details).

Also adds a read_file() function to common_test.py.

The most interesting[tm] thing I found is:
    regex_hat_flag = re.compile('^([a-z]*)\s+([A-Z]*)\s*(#.*)?$')
which matches various unexpected things - but not a hat :-/
(see mailinglist for all funny details)

Acked-by: Steve Beattie <steve@nxnw.org>
2015-04-13 18:31:53 -07:00
Christian Boltz
7d1ff607fe utils: add and use parse_profile_start_line(); convert
serialize_parse_profile_start() to use parse_profile_start_line();
update test-aa.py to match parse_profile_start() and
get_profile_flags() changes

Merge from trunk commits 2978, 2979, and 2982

Add the parse_profile_start_line() function to regex.py, which is
a wrapper for RE_PROFILE_START_2 and returns an array with named matches.

Also change some places in aa.py from using RE_PROFILE_START to
the parse_profile_start_line() function.

Notes: - until everything is migrated to the new function, I'll
keep the old
  RE_PROFILE_START unchanged - that's the reason to add the new
  regex as RE_PROFILE_START_2
- the patch changes only aa.py sections that are covered by tests
  already (which means some users of RE_PROFILE_START are remaining)
- parse_profile_start_line() merges 'profile' and 'attachment' into
  'profile' (aka the old, broken behaviour) until aa.py can handle
  the attachment properly. The alternative would be to ignore
'attachment', which would be worse.

Convert serialize_parse_profile_start() to use
parse_profile_start_line(), and adjust a test to expect an AppArmorBug
instead of an AttributeError exception.

Also add two tests (they succeed with the old and the new code).
Note that these tests document interesting[tm] behaviour - I tend to
think that those cases should raise an exception, but I'm not sure about
this because serialize_profile_from_old_profile() is a good example for
interesting[tm] code :-/

I couldn't come up with a real-world test profile that would hit those
cases without erroring out aa-logprof earlier - maybe the (more
sane-looking) parse_profiles() / serialize_parse_profile_start()
protects us from hitting this interesting[tm] behaviour.

The previous patch slightly changed the behaviour of parse_profile_start()
and get_profile_flags() - they raise AppArmorBug instead of
AppArmorException when specifying a line that is not the start of a
profile and therefore doesn't match RE_PROFILE_START_2.

This patch updates test-aa.py to expect the correct exceptions, and adds
another test with quoted profile name to ensure that stripping the
quotes works as expected.

Acked-by: Steve Beattie <steve@nxnw.org>
2015-04-13 18:24:20 -07:00
Steve Beattie
242ece320a utils: merge test-only changes from trunk
Merge from trunk revisions 2976+2980, 2977, 2981, and 2984.
2015-04-13 18:03:55 -07:00
Christian Boltz
dc1d8e5253 add tests for write_header()
Merge from trunk revision 2984

Also add loop support to test-aa.py.

BTW: In case you wonder - the need to replace unittest.TestCase with
AATest is intentional. It might look annoying, but it makes sure that
a test-*.py file doesn't contain a test class where tests = [...] is
ignored because it's still unittest.TestCase.
(Technically, setup_all_tests() will error out if a test class doesn't
contain tests = [...] - either explicit or via its parent AATest.)

Acked-by: Steve Beattie <steve@nxnw.org>
2015-04-13 14:42:40 -07:00
Christian Boltz
0ac23ee34a add tests for serialize_parse_profile_start() to test-aa.py
to document the function's behaviour.
Merge from trunk revision 2981.

Acked-by: Steve Beattie <steve@nxnw.org>
2015-04-13 14:38:08 -07:00
Christian Boltz
5bc15cda41 Convert test-regex_matches.py to the new tests[] loop.
Merge from trunk revision 2977

The test behaviour is the same with and without this patch - 166 tests
run, all successful.

Acked-by: Steve Beattie <steve@nxnw.org>
2015-04-13 14:32:15 -07:00
Christian Boltz
9ebb1913bd add better loop support to common_test.py
Merge from trunk revisions 2976 and 2980

Add better support for looping over a tests[] array to common_test.py:
- class AATest - a base class we can use for all tests, and that will
  probably get more features in the future (for example tempdir
  handling)
- setup_all_tests() - a function that iterates over all classes in the
  given file and calls setup_test_loops() for each of them
- setup_tests_loop() - a function that creates tests based on tests[]
  in the given class. Those tests call the class' _run_test() method for
  each test specified in tests[]  (inspired by setup_regex_tests() ;-)

This means we can get rid of the manually maintained tests list in
test-regex_matches.py and just need to call setup_all_tests() once in
each file.

The patch also adds test-example.py, which is
- a demo of the code added to common_test.py
- a template file that we can copy for future test-*.py

Acked-by: Steve Beattie <steve@nxnw.org>
2015-04-13 14:28:48 -07:00
Christian Boltz
720f6624e6 write_net_rules() fixes, part 3
Thanks to the used data structure, write_net_rules() replaces bare
'network,' rules with the invalid 'network all,' when saving a profile.
This patch makes sure a correct 'network,' rule is written.

Also reset 'audit' to avoid all (remaining) rules get the audit flag
after writing an audit network rule.

Note: The first section of the function (that claims to be responsible
for bare 'network,' rules) is probably never hit - but I'm not too keen
to remove it and try it out ;-)


Acked-by: Steve Beattie <steve@nxnw.org> for trunk and 2.9.
2015-04-11 02:21:05 +02:00
Christian Boltz
387de4458f Fix doubled arrow in exec rules
When parsing a profile with named exec rules, the exec target included
the arrow. This resulted in two arrows when writing the profile (and one
more each time the profile was updated).

Fix this by using the match group that only contains the exec target
without the arrow in parse_profile_data() and
serialize_profile_from_old_profile().

References: https://bugs.launchpad.net/apparmor/+bug/1437901


Acked-by: Steve Beattie <steve@nxnw.org> for trunk and 2.9.
2015-04-11 02:16:08 +02:00
Steve Beattie
38a69f5ebc profiles: allow ubuntu-helpers to generate texlive fonts
Merge from trunk revision 3004

When evince opens a dvi file, it updates the user fonts using
texlive commands in /usr/share/texlive/texmf-dist/web2c/ (or possibly
/usr/share/texlive/texmf/web2c/ in older releases). This patch adjusts
the sanitized_helper profile to allow these tools to run.

Bug: https://bugs.launchpad.net/ubuntu/+source/apparmor/+bug/1010909

Signed-off-by: Steve Beattie <steve@nxnw.org>
Acked-By: Jamie Strandboge <jamie@canonical.com>
2015-04-10 09:52:36 -07:00
Christian Boltz
7d84c61b6c Fix writing network rules, part 2
write_net_rules() doesn't add a space after 'audit' in two of three
cases, leading to invalid network rules.
This patch adds the missing spaces.


Acked-by: Seth Arnold <seth.arnold@canonical.com>
for both trunk and 2.9
2015-04-10 01:53:42 +02:00
Christian Boltz
f836ebd42b Fix writing network rules
write_net_rules() creates invalid rules for network rules with one
parameter (for example "network bluetooth").
Add a trailing comma to create valid rules.


Acked-by: Seth Arnold <seth.arnold@canonical.com>
for both trunk and 2.9.
2015-04-09 13:29:09 +02:00
Christian Boltz
52b6aeb04c logparser.py: merge path handling in add_event_to_tree()
Merge path handling for 'inode_*' in add_event_to_tree() with the
handling for other path events.

The code is slightly more strict now - 'inode_' in e['operation'] is
replaced with e['operation'].startswith('inode_').

This patch is a cleanup and also a preparation to fix
https://bugs.launchpad.net/apparmor/+bug/1426651 and
https://bugs.launchpad.net/apparmor/+bug/1243932


Acked-by: Seth Arnold <seth.arnold@canonical.com>
Acked for both 2.9 and trunk.
2015-04-03 17:45:48 +02:00
Christian Boltz
475a9bc691 honor 'chmod' events in logparser.py / aa-logprof
aa-logprof doesn't ask anything for

type=AVC msg=audit(1427633461.202:281): apparmor="DENIED" operation="chmod" profile="/usr/lib64/firefox/plugin-container" name="/home/cb/.config/ibus/bus/" pid=7779 comm="plugin-containe" requested_mask="w" denied_mask="w" fsuid=1000 ouid=1000

This patch fixes this by adding 'chmod' to the list of file operation
types in logparser.py.


Acked-by: Seth Arnold <seth.arnold@canonical.com>
for both trunk and 2.9.
2015-04-03 17:44:38 +02:00
Christian Boltz
0f7bf53afb Tell python2 about math
if 3/2 == 1:
    print("python2 inside")

Add "from __future__ import division" so that python2 returns the
correct result (if needed, as float)

On related news: At least python3 knows how to calculate correctly.


Acked-by: Steve Beattie <steve@nxnw.org> for trunk and 2.9
2015-04-02 23:40:15 +02:00
Jamie Strandboge
8dcd54e365 cherrypick from trunk:
add --include-templates-dir and --include-policy-groups-dir options to easyprof
  to support framework policy on Snappy for Ubuntu Core

  Signed-off-by: Jamie Strandboge <jamie@canonical.com>
  Acked-by: Seth Arnold <seth.arnold@canonical.com>
2015-03-31 16:24:31 -05:00
Steve Beattie
097eb4258f fix two issues for older linux releases
Merge from trunk revision 2975

The following patch addresses two issues on older releases:

1) In trunk commit 2911, the line 'undefine VERBOSE' was added to
   parser/tst/Makefile so that the equality tests would not generate
   verbose output when $VERBOSE != 1. Unfortunately, the 'undefine'
   keyword was not introduced in GNU Make until version 3.82. On
   distro releases like Ubuntu 12.04 LTS that include versions of Make
   older than that, make check and make clean abort when VERBOSE is
   not set to 1. The patch fixes that by setting VERBOSE to a zero
   length string if does not already equal 1.

2) In trunk commit 2923, a workaround for systemd as init was added
   to the pivot_root regression test. The workaround included a
   call to ps(1) to determine if systemd is pid 1. Unfortunately,
   in older versions of the procps package (such as the version in
   Ubuntu 12.04 LTS), 'ps -hp1' emits the warning

     Warning: bad ps syntax, perhaps a bogus '-'? See http://procps.sf.net/faq.html

   The patch below converts the ps call to 'ps hp1' which does not
   generate the warning.

Signed-off-by: Steve Beattie <steve@nxnw.org>
Acked-by: John Johansen <john.johansen@canonical.com>
2015-03-31 02:50:40 -07:00
Cameron Norman
9bc15eb6b8 profiles: update dnsmasq profile for lxc support
Merge from trunk revision 2974

Patch from Cameron Norman <camerontnorman@gmail.com> based on a patch
from Christian Boltz <apparmor@cboltz.de>.

This patch allows /var/lib/misc/dnsmasq.*.leases rw and
/{,var/}run/lxc/dnsmasq.pid rw for LXC networking setup.

Acked-by: Steve Beattie <steve@nxnw.org>
2015-03-30 22:26:32 -07:00
Seth Arnold
9d6f7f53cb Add new gdm path for Xauthority file
Bruce Pieterse reports that AppArmor denied evince, among other
applications, from starting properly:
https://bugs.launchpad.net/ubuntu/+source/apparmor/+bug/1432126

He tested a slight variant of the attached patch and reported success. I
propose this patch for both trunk and 2.9.

Signed-off-by: Seth Arnold <seth.arnold@canonical.com>
Acked-by: Tyler Hicks <tyhicks@canonical.com>
2015-03-25 15:32:29 -07:00
Steve Beattie
c1ae887576 tests: work around systemd mounting / shared in pivot_root tests
Merge from trunk revision 2923


The systemd init daemon mounts the / filesystem as shared [1], which
breaks pivot_root(2). The following patch adjusts the pivot_root
test script to remount / as private if it detects that its shared,
allowing the tests to run successfully, and then undoes it once the
tests are complete.

[1] http://cgit.freedesktop.org/systemd/systemd/commit/?id=b3ac5f8cb98757416d8660023d6564a7c411f0a0

Signed-off-by: Steve Beattie <steve@nxnw.org>
Acked-by: Tyler Hicks <tyhicks@canonical.com>
Acked-by: Seth Arnold <seth.arnold@canonical.com>
2015-03-24 17:12:18 -07:00
Steve Beattie
0ec6ce96d2 parser: fix compilation failure of deny link rules, expand equality tests
Merge from trunk commits 2909, 2910, 2911, and 2912

BugLink: http://bugs.launchpad.net/bugs/1433829

The apparmor_parser fails to compile deny rules with only link
permissions.

  Eg.
       deny /f l,
       deny l /f,
       deny link /f -> /d,

Will all fail to compile with the following assert

  apparmor_parser: aare_rules.cc:99: Node* convert_file_perms(int, uint32_t, uint32_t, bool): Assertion `perms != 0' failed.

NOTE: this is a minimal patch a bigger patch that cleans-up and separates
      and reorganizes file, link, exec, and change_profile rules is needed

parser: Expand Equality tests

This adds several new equality tests and turned up a couple of more
bugs
https://launchpad.net/bugs/1433829
https://launchpad.net/bugs/1434018

- add link/link subset tests
- add pix, Pix, cix, Cix, pux, Pux, cux, Cux and specified profile
  transitions (/f px -> b ...)
- test equality of leading and trailing permission file rules
  ie.   /foo rw, == rw /foo,
- test that specific x match overrides generic x rule. ie.
  /** ix, /foo px, is different than /** ix, /foo ix,
- test that deny removes permission
  /f[abc] r, deny /fb r,  is differnt than /f[abc] r,

In addition to adding the new tests, it changes the output of the
equality tests, so that if the $verbose variable is not set successful
tests only output a period, with failed tests outputing the full
info.  If verbose is set the full test info is output as before.

It also does:

- make the verbose output of equality.sh honor whether or not
  the environment variable VERBOSE is set
- thereby making the output verbose when 'make check V=1' or 'make
  check VERBOSE=1' is given from within the parser/ directory. This
  will make distribution packagers happy when diagnosing build
  failures caused by test failures.
- if verbose output is not emitted and the tests were successful, emit
  a newline before printing PASS.
- verify audit and audit allow is equal
- verify audit differs from deny and audit deny
- verify deny differs from audit deny
- make the verbose text a little more useful for some cases
- correct overlap exec tests to substitute in looped perms

Signed-off-by: John Johansen <john.johansen@canonical.com>
Signed-off-by: Steve Beattie <steve@nxnw.org>
Acked-by: Seth Arnold <seth.arnold@canonical.com>
2015-03-23 12:43:57 -07:00
Christian Boltz
8c19eb5521 dovecot auth needs to read openssl.cnf
Darix' guess is that this is needed by libpq because he uses a postgresql
database with dovecot and has ssl enabled in postgresql.

Acked-by: Seth Arnold <seth.arnold@canonical.com> for trunk and 2.9
2015-03-19 13:58:11 +01:00
Steve Beattie
21a41deabe parser: fix equality and valgrind test scripts to use features file
Merge from trunk commit revision 2907

This patch fixes the equality test script and the valgrind wrapper
script to make the parser under test use the features.all features file
from the features_files/ subdirectory. Otherwise, the equality tests
will fail on systems where the not all of the current language features
are supported. The equality fix does so in a way to make the script work
correctly regardless of the directory it is run from.

Signed-off-by: Steve Beattie <steve@nxnw.org>
Acked-by: John Johansen <john.johansen@canonical.com>
2015-03-19 04:37:25 -07:00
Steve Beattie
576e8fe33b parser: fix warning in net_find_af_name
Merge from trunk commit 2906

The fix to prevent the compiler from SEGV'ing when dumping network
rules in commit 2888 introduced the following compiler warning:

  network.c: In function ‘const char* net_find_af_name(unsigned int)’:
  network.c:331:16: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
    for (i = 0; i < sizeof(network_mappings) / sizeof(*network_mappings); i++) {

The problem is that the counter i is an int, but sizeof returns size_t
which is unsigned. The following patch fixes the issue by converting the
type of i to size_t.

Signed-off-by: Steve Beattie <steve@nxnw.org>
Acked-by: John Johansen <john.johansen@canonical.com>
2015-03-19 00:14:12 -07:00
Tyler Hicks
3c928c04e1 parser: Test the 'allow' modifier
Signed-off-by: Tyler Hicks <tyhicks@canonical.com>
Acked-by: John Johansen <john.johansen@canonical.com>
Acked-by: Steve Beattie <steve@nxnw.org>
2015-03-18 12:34:49 -05:00
Tyler Hicks
37b872b155 parser: Test the 'audit allow' modifier
Signed-off-by: Tyler Hicks <tyhicks@canonical.com>
Acked-by: John Johansen <john.johansen@canonical.com>
2015-03-18 12:34:44 -05:00
Tyler Hicks
5ab8b7a483 parser: Verify policies change with the audit and deny modifiers
Signed-off-by: Tyler Hicks <tyhicks@canonical.com>
Acked-by: John Johansen <john.johansen@canonical.com>
2015-03-18 12:34:41 -05:00
Tyler Hicks
b813f4ba53 parser: Add ability to test the inequality of binary policies
Previously, we only had the ability to test that binary policy files
were equal. This patch allows for the testing of binary policy files
that are not equal.

Signed-off-by: Tyler Hicks <tyhicks@canonical.com>
Acked-by: John Johansen <john.johansen@canonical.com>
2015-03-18 12:34:38 -05:00
John Johansen
05ab11fec4 Fix compilation of audit modifiers
cherry-pick: -r2901

This fixes the incorrect compilation of audit modifiers for exec and
pivot_root as detailed in

https://launchpad.net/bugs/1431717
https://launchpad.net/bugs/1432045

The permission accumulation routine on the backend was incorrectly setting
the audit mask based off of the exec type bits (info about the exec) and
not the actual exec permission.

This bug could have also caused permissions issues around overlapping exec
generic and exact match exec rules, except the encoding of EXEC_MODIFIERS
ensured that the
  exact_match_allow & AA_USER/OTHER_EXEC_TYPE
  test would never fail for a permission accumulation with the exec permission
  set.

Signed-off-by: John Johansen <john.johansen@canonical.com>
Acked-by: Steve Beattie <steve@nxnw.org>
2015-03-18 10:22:31 -07:00
Christian Boltz
2d7ba0871f Split off serialize_parse_profile_start_line() from
serialize_profile_from_old_profile() in aa.py, as a preparation to add
tests and then switch to the upcoming RE_PROFILE_START wrapper function.

Besides moving the code, I replaced write_prof_data[profile][hat]['profile']
and write_prof_data[profile][hat]['external'] with function parameters
to avoid that I have to pass around the full write_prof_data.

Note: The "lineno" parameter is technically superfluous - I kept it to
have the parameters as close to parse_profile_start() as possible and
hope that I can merge those functions later (when we have test coverage).


Acked-by: Steve Beattie <steve@nxnw.org> for trunk and 2.9.
2015-03-10 19:09:16 +01:00
Christian Boltz
c98b26069a tools.py: add functions to unload and reload profiles
and change the code to use them

Also add a comment to act() that it's only used by aa-cleanprof.

Note: The new functions add the --base parameter to the apparmor_parser
calls, which also means the disable directory inside the given profile
dir (and not always /etc/apparmor.d/disable) is now honored.


Acked-by: Steve Beattie <steve@nxnw.org> for trunk and 2.9.
2015-03-07 20:32:11 +01:00
Christian Boltz
70dc81c4fd merge 'path' if conditions in logparser.py / add_event_to_tree()
logparser.py / add_event_to_tree() has 5 places to handle 'path' events.
This patch merges most if conditions to reduce that to 2 places.

It also makes the matching a bit more strict - instead of using 'in',
'xattr' has to be an exact match and 'file_' is matched with startswith().

Also, 'getattr' is added to the list of file events.


Acked-by: Steve Beattie <steve@nxnw.org> (also for 2.9)



---------- trunk only, unclear for 2.9 --------------
2015-03-07 20:26:32 +01:00
Christian Boltz
1b68baf7a3 let load_include raise an exception if an include file can't be found
instead of ignoring the error silently

Acked-by: Steve Beattie <steve@nxnw.org> for both trunk and 2.9.
2015-03-07 13:28:41 +01:00
Christian Boltz
6af7faa2b7 add read_profiles() call to cmd_disable()
Without it, aa-disable
- didn't error out when hitting a broken profile directory
- didn't find a profile if it doesn't use the default naming scheme
  (for example /bin/true profile hiding in bin.false)


Acked-by: Steve Beattie <steve@nxnw.org> for trunk and 2.9
2015-03-07 13:27:57 +01:00
Steve Beattie
a1529a16bd profiles: add mir abstraction
Merge from trunk revision 2893

As mir has come into use in Ubuntu touch and is available for testing on
Ubuntu desktop, confined apps need access to a few mir specific things.
This patch adds a mir abstraction.

Signed-off-by: Steve Beattie <steve@nxnw.org>
Acked-by: Tyler Hicks <tyhicks@canonical.com>
2015-03-05 11:51:16 -08:00
Steve Beattie
321a2c1dcb regression tests: fix gcc-5 inline confusion
Merge from trunk revision 2889

Parts of the regression tests that use the do_open() inline function
from changehat.h fail to build under gcc-5 like so:

  cc -g -O0 -Wall -Wstrict-prototypes    changeprofile.c  -lapparmor -o changeprofile /tmp/ccT6GE6k.o: In function `main':
    /home/ubuntu/bzr/apparmor/tests/regression/apparmor/changeprofile.c:43: undefined reference to `do_open'
    collect2: error: ld returned 1 exit status
    <builtin>: recipe for target 'changeprofile' failed

This patch converts the do_open function declaration to be static
inline, which apparently keeps gcc-5 from getting confused.

Signed-off-by: Steve Beattie <steve@nxnw.org>
Acked-by: John Johansen <john.johansen@canonical.com>
2015-03-03 23:32:26 -08:00
Alain BENEDETTI
735ef5d32b utils/aa-status: don't crash when non-ASCII mountpoints are in use
Merge from trunk revision 2892

aa-status was crashing when parsing through /proc/mounts looking
to see if and where the securityfs synthetic file system is mounted
if there was a mount point that contained characters outside of the
charset in use in the environment of aa-status. This patch fixes the
issue by converting the read of /proc/mounts into a binary read and
then uses decode on the elements.

Patch by Alain BENEDETTI.
Acked-by: Steve Beattie <steve@nxnw.org>
2015-03-03 22:25:32 -08:00
Tyler Hicks
9428498d90 parser: Fix error checking of file opening in features_dir_cb()
The error path was being taken when openat() return 0 but openat()
returns -1 on error.

Signed-off-by: Tyler Hicks <tyhicks@canonical.com>
Acked-by: Seth Arnold <seth.arnold@canonical.com>
2015-03-03 20:28:22 -06:00
Christian Boltz
3ea1e541c7 Add test for disconnected path
As a follow-up to the logparser.py change that converts disconnected
path events to an error, add a testcase to test-logparser.py.


Acked-by: Steve Beattie <steve@nxnw.org> for both trunk and 2.9.
2015-03-03 22:18:24 +01:00
Philip Withnall
29b0634f34 parser: net_find_af_name: do not assume that address families are consecutive,
remove unused net_find_af_val function, and network_families array
Merge from trunk commit 2888.

net_find_af_name: do not assume that address families are consecutive

The network_families array is automatically built from AF_NAMES, which is
extracted from the defines in <bits/socket.h>. The code assumes that
network_families is indexed by the AF defines. However, since the
defines are sparse, and the gaps in the array are not packed with
zeroes, the array is shorter than expected, and the indexing is wrong.

When this function was written, the network families that were
covered might well have been consecutive, but this is no longer true:
there's a gap between AF_LLC (26) and AF_CAN (29).

This assumption caused a crash in our testing while parsing the rule
"network raw".

Remove unused net_find_af_val function, and network_families array

Like net_find_af_name, this assumed that AF_* values were consecutive.

Patches from Philip Withnall and Simon McVittie.
2015-03-03 12:04:13 -08:00
Christian Boltz
586222c94e move strip_quotes() from aa.py to regex.py
The upcoming function parse_profile_start() (which is a wrapper around
the updated RE_PROFILE_START, and will live in regex.py) needs
strip_profile(), but importing it from aa.py fails with an import loop.
Therefore this patch moves strip_quotes() from aa.py to regex.py and
re-imports it into aa.py.

As a bonus, the patch also adds some tests for strip_quotes() ;-)


Also add TestStripQuotes to the test_suite list because it won't run
otherwise.

Acked-by: Steve Beattie <steve@nxnw.org> for both trunk and 2.9
2015-03-03 20:18:30 +01:00
Christian Boltz
232b51504c let logparser.py parse_event() change disconnected path events to 'ERROR'
This means that aa-logprof will ignore the event instead of crashing with
    AppArmorException: 'Unexpected rank input: var/run/nscd/passwd'

Note that I made the check as specific as possible to be sure it doesn't
hide other events.

References: https://bugzilla.opensuse.org/show_bug.cgi?id=918787



Acked-by: Steve Beattie <steve@nxnw.org> for 2.9



(This is a backport of trunk r2877, but without the test-capability.py
adjustment because that file doesn't exist in 2.9)
2015-03-03 12:34:47 +01:00
Christian Boltz
df099620dd aa.py: split off parse_profile_start() from parse_profile_data() and add tests
Move the code for parsing the profile start ("/foo {") from aa.py
parse_profile_data() to a separate function parse_profile_start().

Most of the changes are just moving around code, with some small
exceptions:
- instead of handing over profile_data to parse_profile_start() to
  modify it, it sets two variables (pps_set_profile and
  pps_set_hat_external) as part of its return value, which are then
  used in parse_profile_data() to set the flags in profile_data.
- existing_profiles[profile] = file   is executed later, which means
  it used the strip_quotes() version of profile now
- whitespace / tab level changes

The patch also adds some tests for the parse_profile_start() function.



Acked-by: Steve Beattie <steve@nxnw.org> for 2.9 as well.
2015-03-02 21:46:45 +01:00
Christian Boltz
22d647ecb1 Add some tests for aa.py get_profile_flags().
Also adds a check to get_profile_flags() to catch an invalid syntax:
    /foo (  ) {
was accepted by get_profile_flags, while
    /foo () {
failed.

When testing with the parser, both result in a syntax error, therefore
the patch makes sure it also fails in get_profile_flags().


Acked-by: Steve Beattie <steve@nxnw.org> for trunk and 2.9
2015-03-02 19:38:34 +01:00
Tyler Hicks
07b0886796 parser: Fix "PDEBUG" redefined warning
Only present when building with DEBUG=1.

Signed-off-by: Tyler Hicks <tyhicks@canonical.com>
Acked-by: Seth Arnold <seth.arnold@canonical.com>
2015-03-02 09:59:17 -06:00
Tyler Hicks
9da31bf281 parser: Fix -Wformat-extra-args warning
Only present when building with DEBUG=1.

Signed-off-by: Tyler Hicks <tyhicks@canonical.com>
Acked-by: Seth Arnold <seth.arnold@canonical.com>
Acked-by: Steve Beattie <steve@nxnw.org>
2015-03-02 09:59:14 -06:00
Tyler Hicks
c5ff27a91b parser: Send PDEBUG() to stderr
PDEBUG() and PERROR() should both go to stderr.

Signed-off-by: Tyler Hicks <tyhicks@canonical.com>
Acked-by: Seth Arnold <seth.arnold@canonical.com>
2015-03-02 09:59:11 -06:00
Tyler Hicks
cf4afcb860 parser: Fix return value of dirat_for_each()
Seth pointed out that dirat_for_each() didn't correctly handle the
return value from readdir_r(). On error, it directly returns a positive
errno value. This would have resulted in that positive errno value being
returned, with an undefined errno value set, from dirat_for_each().
However, the dirat_for_each() documentation states that -1 is returned,
with errno set, on error.

This patch results in readdir_r()'s return value being handled
appropriately. In addition, it ensures that 0 is always returned on
success.

Signed-off-by: Tyler Hicks <tyhicks@canonical.com>
Reported-by: Seth Arnold <seth.arnold@canonical.com>
Acked-by: Steve Beattie <steve@nxnw.org>
2015-03-02 09:54:57 -06:00
Cameron Norman
75a186fa9f profiles: add geary email client to ubuntu-email abstraction
Merge from trunk revision 2876

Merge from Cameron Norman <camerontnorman@gmail.com>
Acked-by: Steve Beattie <steve@nxnw.org>
2015-02-27 23:28:05 -08:00
Felix Geyer
05bef291d7 profiles: allow aspell access to /usr/share/aspell/
Merge from trunk revision 2875

From: Felix Geyer <debfx@ubuntu.com>

At least Debian/Ubuntu started shipping some aspell files in
/usr/share/aspell/.
For example:
/usr/share/aspell/iso-8859-1.cmap
/usr/share/aspell/iso-8859-1.cset

The abstraction should allow read access to these files.

Acked-by: Steve Beattie <steve@nxnw.org>
2015-02-27 23:16:32 -08:00
Steve Beattie
76f71f7d84 profiles: add support for /etc/pki/ in ssl certs abstraction
Merge from trunk revision 2874.

These appear to be related to the update-ca-trust tool.

Thanks to Gregor Dschung <dschung@cs.uni-kl.de>
2015-02-27 22:57:41 -08:00
Christian Boltz
34f2c1c6ea cleanup aa-disable handling in tools.py
Remove the check if the disable directory exists. If it's really
missing, it will be auto-created by create_symlink(), so we
automagically fix things instead of annoying the user with an
error message ;-)

Acked-by: Steve Beattie <steve@nxnw.org> for both trunk and 2.9.
2015-02-28 00:25:45 +01:00
Christian Boltz
67dae2f1cf Fix the minitools (aa-audit, aa-complain, aa-enforce, aa-autodep)
to work with multiple profiles at once.

References: https://bugs.launchpad.net/apparmor/+bug/1378095

Acked-by: Steve Beattie <steve@nxnw.org> for trunk and 2.9.
2015-02-27 14:21:05 +01:00
Seth Arnold
bbaaa00249 parser: fix dbus.cc issue when building with gcc 5
Merge from trunk revision 2868.

This should fix a gcc 5 build failure (untested) with os << .. << os

This build failure was discovered by doko's archive rebuild:
http://people.ubuntuwire.org/~wgrant/rebuild-ftbfs-test/test-rebuild-20150202-gcc5-vivid.html

Acked-by: Steve Beattie <steve@nxnw.org>
2015-02-26 16:18:15 -08:00
Steve Beattie
9ed8789918 parser: fix more gcc 5 compilation problems
Merge from trunk revision 2871

Don't pass an ostream reference into another ostream via <<.

Signed-off-by: Steve Beattie <steve@nxnw.org>
Acked-by: Seth Arnold <seth.arnold@canonical.com>
2015-02-26 15:20:19 -08:00
Christian Boltz
f45628d749 delete traces of program-chunks directory from apparmor.d(5)
Acked-by: Tyler Hicks <tyhicks@canonical.com>
2015-02-26 18:45:41 +01:00
Christian Boltz
602decfbfc Update is_skippable_file() to match all extensions that are listed in
libapparmor _aa_is_blacklisted() - some extensions were missing in the
python code.

Also make the code more readable and add some testcases.

Notes:
- the original code additionally ignored *.swp. I didn't include that -
  *.swp looks like vim swap files which are also dot files
- the python code ignores README files, but the C code doesn't
  (do we need to add README in the C code?)


Acked-by: Kshitij Gupta <kgupta8592@gmail.com> for 2.9 and trunk
Acked-by: Steve Beattie <steve@nxnw.org>
2015-02-04 13:18:47 +01:00
Christian Boltz
9aa1efd744 Fix aa-unconfined to work with profile names that don't start with / or null
Reported by u on the debian pkg-apparmor-team ML.


Acked-by: John Johansen <john.johansen@canonical.com>
2015-02-02 20:53:29 +01:00
Jamie Strandboge
c51a68eaaf Description: Allow writes to /{,var}/run/systemd/journal/dev-log, the systemd
journal socket. On Debian and Ubuntu systems, /dev/log is a symlink to
 /run/systemd/journal/dev-log, so this access is now required in the base
 abstraction to maintain current behavior.
Bug: https://bugs.launchpad.net/apparmor/+bug/1413232

Acked-By: Jamie Strandboge <jamie@canonical.com>
Acked-by: Steve Beattie <steve@nxnw.org>
2015-01-21 13:37:49 -06:00
Christian Boltz
49b739b184 Add some tests for logparser.py based on the log lines from
https://bugs.launchpad.net/apparmor/+bug/1399027

Also move some existing tests from aa_test.py to test-logparser.py and
adds checks for RE_LOG_v2_6_audit and RE_LOG_v2_6_syslog to them.


Acked-by: Steve Beattie <steve@nxnw.org> for trunk and 2.9
2015-01-18 14:57:10 +01:00
Christian Boltz
53d071adf5 update logparser.py to support the changed syslog format by adding
(audit:\s+)?   to RE_LOG_v2_6_syslog

References: https://bugs.launchpad.net/apparmor/+bug/1399027


Acked-by: Seth Arnold <seth.arnold@canonical.com> (for trunk)

Acked-by: Steve Beattie <steve@nxnw.org> for 2.9 as well
2015-01-17 14:35:38 +01:00
Christian Boltz
70cda06789 Fix the dnsmasq profile to allow executing bash to run the --dhcp-script
argument. Also fixed /usr/lib -> /usr/{lib,lib64} to get libvirt
leasehelper script to run even on x86_64.

References: https://bugzilla.opensuse.org/show_bug.cgi?id=911001

Patch by "Cédric Bosdonnat" <cbosdonnat@suse.com>

Note: the original patch used {lib,lib64} - I changed it to lib{,64} to
match the style we typically use.

Acked-by: John Johansen <john.johansen@canonical.com>

(backport of trunk r2841)
2014-12-22 17:57:40 +01:00
Christian Boltz
e8ffc1c4e8 update and cleanup usr.sbin.dovecot profile
Add #include <abstractions/dovecot-common> to the usr.sbin.dovecot
profile. Effectively this adds "deny capability block_suspend," which
is the only missing part from
https://bugs.launchpad.net/apparmor/+bug/1296667/

Also remove "capability setgid," (covered by
abstractions/dovecot-common) and "@{PROC}/filesystems r," (part of
abstractions/base).

Acked-by: John Johansen <john.johansen@canonical.com>

(backport of trunk r2840)
2014-12-22 17:51:02 +01:00
Christian Boltz
09c93be47c Add some missing /run/dovecot/* to usr.lib.dovecot.imap{, -login}
Add the needed permissions as reported in
https://bugs.launchpad.net/apparmor/+bug/1296667/ comment #1
to the usr.lib.dovecot.imap and imap-login profiles.

Acked-by: John Johansen <john.johansen@canonical.com>

(backport of trunk r2839)
2014-12-22 17:43:54 +01:00
Christian Boltz
ac8d886645 update the mysqld profile in the extras directory to
something that works on my servers ;-)

Acked-by: John Johansen <john.johansen@canonical.com>

(backport of trunk r2838)
2014-12-22 17:39:29 +01:00
Christian Boltz
ec1dda24d0 fix network rule description in apparmor.d.pod
(backport from trunk r2837)

Acked-by: John Johansen <john.johansen@canonical.com> (for trunk)

Acked-by: Steve Beattie <steve@nxnw.org> (for 2.9)
2014-12-19 13:57:12 +01:00
Steve Beattie
e7e9053598 Update version in preparation for 2.9.2 development. 2014-12-16 13:37:58 -08:00
51 changed files with 1746 additions and 401 deletions

View File

@@ -14,7 +14,7 @@ DIRS=parser \
#REPO_URL?=lp:apparmor
# --per-file-timestamps is failing over SSH, https://bugs.launchpad.net/bzr/+bug/1257078
REPO_URL?=https://code.launchpad.net/~apparmor-dev/apparmor/master
REPO_URL?=https://code.launchpad.net/~apparmor-dev/apparmor/2.9
# alternate possibilities to export from
#REPO_URL=.
#REPO_URL="bzr+ssh://bazaar.launchpad.net/~sbeattie/+junk/apparmor-dev/"

View File

@@ -1 +1 @@
2.9.1
2.9.2

View File

@@ -148,11 +148,14 @@ ostream &af_rule::dump_peer(ostream &os)
ostream &af_rule::dump(ostream &os)
{
os << dump_prefix(os);
dump_prefix(os);
os << af_name;
os << dump_local(os);
if (has_peer_conds())
os << " peer=(" << dump_peer(os) << ")";
dump_local(os);
if (has_peer_conds()) {
os << " peer=(";
dump_peer(os);
os << ")";
}
os << ",\n";
return os;

View File

@@ -61,7 +61,7 @@ B<SUBPROFILE> = [ I<COMMENT> ... ] ( I<PROGRAMHAT> | 'profile ' I<PROGRAMCHILD>
B<CAPABILITY> = (lowercase capability name without 'CAP_' prefix; see
capabilities(7))
B<NETWORK RULE> = 'network' [ [ I<DOMAIN> ] [ I<TYPE> ] [ I<PROTOCOL> ] ] ','
B<NETWORK RULE> = 'network' [ [ I<DOMAIN> [ I<TYPE> | I<PROTOCOL> ] ] | [ I<PROTOCOL> ] ] ','
B<DOMAIN> = ( 'inet' | 'ax25' | 'ipx' | 'appletalk' | 'netrom' | 'bridge' | 'atmpvc' | 'x25' | 'inet6' | 'rose' | 'netbeui' | 'security' | 'key' | 'packet' | 'ash' | 'econet' | 'atmsvc' | 'sna' | 'irda' | 'pppox' | 'wanpipe' | 'bluetooth' | 'netlink' ) ','
@@ -1192,10 +1192,6 @@ files, and the X socket.
=back
The abstractions stored in F</etc/apparmor.d/program-chunks/> are
intended for use by specific program suites, and are not generally
useful.
Some of the abstractions rely on variables that are set in files in the
F</etc/apparmor.d/tunables/> directory. These variables are currently
B<@{HOME}> and B<@{HOMEDIRS}>. Variables cannot be set in profile scope;

View File

@@ -149,7 +149,7 @@ ostream &dbus_rule::dump(ostream &os)
if (interface)
os << " interface=\"" << interface << "\"";
if (member)
os << " member=\"" << member << os << "\"";
os << " member=\"" << member << "\"";
if (!(mode & AA_DBUS_BIND) && (peer_label || name)) {
os << " peer=( ";

View File

@@ -62,9 +62,9 @@
int dirat_for_each(DIR *dir, const char *name, void *data,
int (* cb)(DIR *, const char *, struct stat *, void *))
{
struct dirent *dirent = NULL, *ent;
struct dirent *dirent = NULL;
DIR *d = NULL;
int error = 0;
int error;
if (!cb || (!dir && !name)) {
errno = EINVAL;
@@ -102,11 +102,19 @@ int dirat_for_each(DIR *dir, const char *name, void *data,
d = dir;
}
for (error = readdir_r(d, dirent, &ent);
error == 0 && ent != NULL;
error = readdir_r(d, dirent, &ent)) {
for (;;) {
struct dirent *ent;
struct stat my_stat;
error = readdir_r(d, dirent, &ent);
if (error) {
PDEBUG("readdir_r failed");
errno = error; /* readdir_r directly returns an errno */
goto fail;
} else if (!ent) {
break;
}
if (strcmp(ent->d_name, ".") == 0 ||
strcmp(ent->d_name, "..") == 0)
continue;
@@ -126,7 +134,7 @@ int dirat_for_each(DIR *dir, const char *name, void *data,
closedir(d);
free(dirent);
return error;
return 0;
fail:
error = errno;

View File

@@ -1335,19 +1335,16 @@ int accept_perms(NodeSet *state, perms_t &perms)
}
perms.allow |= exact_match_allow & ~(ALL_AA_EXEC_TYPE);
if (exact_match_allow & AA_USER_EXEC_TYPE) {
perms.audit |= exact_audit & ~(ALL_AA_EXEC_TYPE);
if (exact_match_allow & AA_USER_EXEC) {
perms.allow = (exact_match_allow & AA_USER_EXEC_TYPE) |
(perms.allow & ~AA_USER_EXEC_TYPE);
perms.audit = (exact_audit & AA_USER_EXEC_TYPE) |
(perms.audit & ~AA_USER_EXEC_TYPE);
perms.exact = AA_USER_EXEC_TYPE;
}
if (exact_match_allow & AA_OTHER_EXEC_TYPE) {
if (exact_match_allow & AA_OTHER_EXEC) {
perms.allow = (exact_match_allow & AA_OTHER_EXEC_TYPE) |
(perms.allow & ~AA_OTHER_EXEC_TYPE);
perms.audit = (exact_audit & AA_OTHER_EXEC_TYPE) |
(perms.audit & ~AA_OTHER_EXEC_TYPE);
perms.exact |= AA_OTHER_EXEC_TYPE;
}
if (AA_USER_EXEC & perms.deny)

View File

@@ -321,31 +321,19 @@ struct aa_network_entry *network_entry(const char *family, const char *type,
#define ALL_TYPES 0x43e
/* another case of C++ not supporting non-trivial designated initializers */
#undef AA_GEN_NET_ENT
#define AA_GEN_NET_ENT(name, AF) name, /* [AF] = name, */
static const char *network_families[] = {
#include "af_names.h"
};
int net_find_af_val(const char *af)
{
int i;
for (i = 0; network_families[i]; i++) {
if (strcmp(network_families[i], af) == 0)
return i;
}
return -1;
}
const char *net_find_af_name(unsigned int af)
{
size_t i;
if (af < 0 || af > get_af_max())
return NULL;
return network_families[af];
for (i = 0; i < sizeof(network_mappings) / sizeof(*network_mappings); i++) {
if (network_mappings[i].family == af)
return network_mappings[i].family_name;
}
return NULL;
}
void __debug_network(unsigned int *array, const char *name)
@@ -375,7 +363,7 @@ void __debug_network(unsigned int *array, const char *name)
for (i = 0; i < af_max; i++) {
if (array[i]) {
const char *fam = network_families[i];
const char *fam = net_find_af_name(i);
if (fam)
printf("%s ", fam);
else

View File

@@ -125,7 +125,6 @@ struct network {
int net_find_type_val(const char *type);
const char *net_find_type_name(int type);
int net_find_af_val(const char *af);
const char *net_find_af_name(unsigned int af);
const struct network_tuple *net_find_mapping(const struct network_tuple *map,
const char *family,

View File

@@ -172,7 +172,7 @@ extern int preprocess_only;
#ifdef DEBUG
#define PDEBUG(fmt, args...) printf("parser: " fmt, ## args)
#define PDEBUG(fmt, args...) fprintf(stderr, "parser: " fmt, ## args)
#else
#define PDEBUG(fmt, args...) /* Do nothing */
#endif

View File

@@ -587,7 +587,9 @@ static int features_dir_cb(DIR *dir, const char *name, struct stat *st,
if (S_ISREG(st->st_mode)) {
int len, file;
int remaining = fst->size - (fst->pos - *fst->buffer);
if (!(file = openat(dirfd(dir), name, O_RDONLY))) {
file = openat(dirfd(dir), name, O_RDONLY);
if (file == -1) {
PDEBUG("Could not open '%s'", name);
return -1;
}

View File

@@ -43,7 +43,7 @@
/* #define DEBUG */
#ifdef DEBUG
#undef PDEBUG
#define PDEBUG(fmt, args...) printf("Lexer: " fmt, ## args)
#define PDEBUG(fmt, args...) fprintf(stderr, "Lexer: " fmt, ## args)
#else
#undef PDEBUG
#define PDEBUG(fmt, args...) /* Do nothing */
@@ -534,7 +534,7 @@ static int parse_X_sub_mode(const char *X, const char *str_mode, int *result, in
int mode = 0;
const char *p;
PDEBUG("Parsing X mode: %s\n", X, str_mode);
PDEBUG("Parsing %s mode: %s\n", X, str_mode);
if (!str_mode)
return 0;

View File

@@ -34,8 +34,10 @@
/* #define DEBUG */
#ifdef DEBUG
#define PDEBUG(fmt, args...) printf("Lexer: " fmt, ## args)
#undef PDEBUG
#define PDEBUG(fmt, args...) fprintf(stderr, "Lexer: " fmt, ## args)
#else
#undef PDEBUG
#define PDEBUG(fmt, args...) /* Do nothing */
#endif
#define NPDEBUG(fmt, args...) /* Do nothing */

View File

@@ -491,9 +491,14 @@ static int process_dfa_entry(aare_rules *dfarules, struct cod_entry *entry)
* out by a deny rule, as both pieces of the link pair must
* match. audit info for the link is carried on the second
* entry of the pair
*
* So if a deny rule only record it if there are permissions other
* than link in the entry.
* TODO: split link and change_profile entries earlier
*/
if (entry->deny && (entry->mode & AA_LINK_BITS)) {
if (!dfarules->add_rule(tbuf.c_str(), entry->deny,
if (entry->deny) {
if ((entry->mode & ~(AA_LINK_BITS | AA_CHANGE_PROFILE)) &&
!dfarules->add_rule(tbuf.c_str(), entry->deny,
entry->mode & ~AA_LINK_BITS,
entry->audit & ~AA_LINK_BITS, dfaflags))
return FALSE;

View File

@@ -9,6 +9,8 @@ PROVE_ARG=-f
ifeq ($(VERBOSE),1)
PROVE_ARG+=-v
PYTEST_ARG = -v
else
VERBOSE=
endif
all: tests

View File

@@ -22,37 +22,51 @@
set -o pipefail
APPARMOR_PARSER="${APPARMOR_PARSER:-../apparmor_parser}"
_SCRIPTDIR=$(dirname "${BASH_SOURCE[0]}" )
APPARMOR_PARSER="${APPARMOR_PARSER:-${_SCRIPTDIR}/../apparmor_parser}"
fails=0
errors=0
verbose="${VERBOSE:-}"
hash_binary_policy()
{
printf %s "$1" | ${APPARMOR_PARSER} -qS 2>/dev/null| md5sum | cut -d ' ' -f 1
printf %s "$1" | ${APPARMOR_PARSER} --features-file ${_SCRIPTDIR}/features_files/features.all -qS 2>/dev/null| md5sum | cut -d ' ' -f 1
return $?
}
# verify_binary_equality - compares the binary policy of multiple profiles
# $1: A short description of the test
# $2: The known-good profile
# $3..$n: The profiles to compare against $2
# verify_binary - compares the binary policy of multiple profiles
# $1: Test type (equality or inequality)
# $2: A short description of the test
# $3: The known-good profile
# $4..$n: The profiles to compare against $3
#
# Upon failure/error, prints out the test description and profiles that failed
# and increments $fails or $errors for each failure and error, respectively
verify_binary_equality()
verify_binary()
{
local desc=$1
local good_profile=$2
local t=$1
local desc=$2
local good_profile=$3
local good_hash
local ret=0
shift
shift
shift
printf "Binary equality %s" "$desc"
if [ "$t" != "equality" ] && [ "$t" != "inequality" ]
then
printf "\nERROR: Unknown test mode:\n%s\n\n" "$t" 1>&2
((errors++))
return $((ret + 1))
fi
if [ -n "$verbose" ] ; then printf "Binary %s %s" "$t" "$desc" ; fi
good_hash=$(hash_binary_policy "$good_profile")
if [ $? -ne 0 ]
then
if [ -z "$verbose" ] ; then printf "Binary %s %s" "$t" "$desc" ; fi
printf "\nERROR: Error hashing the following \"known-good\" profile:\n%s\n\n" \
"$good_profile" 1>&2
((errors++))
@@ -64,28 +78,54 @@ verify_binary_equality()
hash=$(hash_binary_policy "$profile")
if [ $? -ne 0 ]
then
if [ -z "$verbose" ] ; then printf "Binary %s %s" "$t" "$desc" ; fi
printf "\nERROR: Error hashing the following profile:\n%s\n\n" \
"$profile" 1>&2
((errors++))
((ret++))
elif [ "$hash" != "$good_hash" ]
elif [ "$t" == "equality" ] && [ "$hash" != "$good_hash" ]
then
if [ -z "$verbose" ] ; then printf "Binary %s %s" "$t" "$desc" ; fi
printf "\nFAIL: Hash values do not match\n" 2>&1
printf "known-good (%s) != profile-under-test (%s) for the following profile:\n%s\n\n" \
"$good_hash" "$hash" "$profile" 1>&2
((fails++))
((ret++))
elif [ "$t" == "inequality" ] && [ "$hash" == "$good_hash" ]
then
if [ -z "$verbose" ] ; then printf "Binary %s %s" "$t" "$desc" ; fi
printf "\nFAIL: Hash values match\n" 2>&1
printf "known-good (%s) == profile-under-test (%s) for the following profile:\n%s\n\n" \
"$good_hash" "$hash" "$profile" 1>&2
((fails++))
((ret++))
fi
done
if [ $ret -eq 0 ]
then
printf " ok\n"
fi
if [ -z "$verbose" ] ; then
printf "."
else
printf " ok\n"
fi
fi
return $ret
}
verify_binary_equality()
{
verify_binary "equality" "$@"
}
verify_binary_inequality()
{
verify_binary "inequality" "$@"
}
printf "Equality Tests:\n"
verify_binary_equality "dbus send" \
"/t { dbus send, }" \
"/t { dbus write, }" \
@@ -225,11 +265,205 @@ verify_binary_equality "dbus minimization found in dbus abstractions" \
peer=(name=org.freedesktop.DBus),
dbus send bus=session, }"
# Rules compatible with audit, deny, and audit deny
# note: change_profile does not support audit/allow/deny atm
for rule in "capability" "capability mac_admin" \
"network" "network tcp" "network inet6 tcp"\
"mount" "mount /a" "mount /a -> /b" "mount options in (ro) /a -> b" \
"remount" "remount /a" \
"umount" "umount /a" \
"pivot_root" "pivot_root /a" "pivot_root oldroot=/" \
"pivot_root oldroot=/ /a" "pivot_root oldroot=/ /a -> foo" \
"ptrace" "ptrace trace" "ptrace (readby,tracedby) peer=unconfined" \
"signal" "signal (send,receive)" "signal peer=unconfined" \
"signal receive set=(kill)" \
"dbus" "dbus send" "dbus bus=system" "dbus bind name=foo" \
"dbus peer=(label=foo)" "dbus eavesdrop" \
"unix" "unix (create, listen, accept)" "unix addr=@*" "unix addr=none" \
"unix peer=(label=foo)" \
"/f r" "/f w" "/f rwmlk" "/** r" "/**/ w" \
"file /f r" "file /f w" "file /f rwmlk" \
"link /a -> /b" "link subset /a -> /b" \
"l /a -> /b" "l subset /a -> /b" \
"file l /a -> /b" "l subset /a -> /b"
do
verify_binary_equality "allow modifier for \"${rule}\"" \
"/t { ${rule}, }" \
"/t { allow ${rule}, }"
verify_binary_equality "audit allow modifier for \"${rule}\"" \
"/t { audit ${rule}, }" \
"/t { audit allow ${rule}, }"
verify_binary_inequality "audit, deny, and audit deny modifiers for \"${rule}\"" \
"/t { ${rule}, }" \
"/t { audit ${rule}, }" \
"/t { audit allow ${rule}, }" \
"/t { deny ${rule}, }" \
"/t { audit deny ${rule}, }"
verify_binary_inequality "audit vs deny and audit deny modifiers for \"${rule}\"" \
"/t { audit ${rule}, }" \
"/t { deny ${rule}, }" \
"/t { audit deny ${rule}, }"
verify_binary_inequality "deny and audit deny modifiers for \"${rule}\"" \
"/t { deny ${rule}, }" \
"/t { audit deny ${rule}, }"
done
# Rules that need special treatment for the deny modifier
for rule in "/f ux" "/f Ux" "/f px" "/f Px" "/f cx" "/f Cx" "/f ix" \
"/f pux" "/f Pux" "/f pix" "/f Pix" \
"/f cux" "/f Cux" "/f cix" "/f Cix" \
"/* ux" "/* Ux" "/* px" "/* Px" "/* cx" "/* Cx" "/* ix" \
"/* pux" "/* Pux" "/* pix" "/* Pix" \
"/* cux" "/* Cux" "/* cix" "/* Cix" \
"/f px -> b " "/f Px -> b" "/f cx -> b" "/f Cx -> b" \
"/f pux -> b" "/f Pux -> b" "/f pix -> b" "/f Pix -> b" \
"/f cux -> b" "/f Cux -> b" "/f cix -> b" "/f Cix -> b" \
"/* px -> b" "/* Px -> b" "/* cx -> b" "/* Cx -> b" \
"/* pux -> b" "/* Pux -> b" "/* pix -> b" "/* Pix -> b" \
"/* cux -> b" "/* Cux -> b" "/* cix -> b" "/* Cix -> b" \
"file /f ux" "file /f Ux" "file /f px" "file /f Px" \
"file /f cx" "file /f Cx" "file /f ix" \
"file /f pux" "file /f Pux" "file /f pix" "file /f Pix" \
"/f cux" "/f Cux" "/f cix" "/f Cix" \
"file /* ux" "file /* Ux" "file /* px" "file /* Px" \
"file /* cx" "file /* Cx" "file /* ix" \
"file /* pux" "file /* Pux" "file /* pix" "file /* Pix" \
"file /* cux" "file /* Cux" "file /* cix" "file /* Cix" \
"file /f px -> b " "file /f Px -> b" "file /f cx -> b" "file /f Cx -> b" \
"file /f pux -> b" "file /f Pux -> b" "file /f pix -> b" "file /f Pix -> b" \
"file /f cux -> b" "file /f Cux -> b" "file /f cix -> b" "file /f Cix -> b" \
"file /* px -> b" "file /* Px -> b" "file /* cx -> b" "file /* Cx -> b" \
"file /* pux -> b" "file /* Pux -> b" "file /* pix -> b" "file /* Pix -> b" \
"file /* cux -> b" "file /* Cux -> b" "file /* cix -> b" "file /* Cix -> b"
do
verify_binary_equality "allow modifier for \"${rule}\"" \
"/t { ${rule}, }" \
"/t { allow ${rule}, }"
verify_binary_equality "audit allow modifier for \"${rule}\"" \
"/t { audit ${rule}, }" \
"/t { audit allow ${rule}, }"
# skip rules that don't end with x perm
if [ -n "${rule##*x}" ] ; then continue ; fi
verify_binary_inequality "deny, audit deny modifier for \"${rule}\"" \
"/t { ${rule}, }" \
"/t { audit ${rule}, }" \
"/t { audit allow ${rule}, }" \
"/t { deny ${rule% *} x, }" \
"/t { audit deny ${rule% *} x, }"
verify_binary_inequality "audit vs deny and audit deny modifiers for \"${rule}\"" \
"/t { audit ${rule}, }" \
"/t { deny ${rule% *} x, }" \
"/t { audit deny ${rule% *} x, }"
done
# verify deny and audit deny differ for x perms
for prefix in "/f" "/*" "file /f" "file /*" ; do
verify_binary_inequality "deny and audit deny x modifiers for \"${prefix}\"" \
"/t { deny ${prefix} x, }" \
"/t { audit deny ${prefix} x, }"
done
#Test equality of leading and trailing file permissions
for audit in "" "audit" ; do
for allow in "" "allow" "deny" ; do
for owner in "" "owner" ; do
for f in "" "file" ; do
prefix="$audit $allow $owner $f"
for perm in "r" "w" "a" "l" "k" "m" "rw" "ra" \
"rl" "rk" "rm" "wl" "wk" "wm" \
"rwl" "rwk" "rwm" "ral" "rak" \
"ram" "rlk" "rlm" "rkm" "wlk" \
"wlm" "wkm" "alk" "alm" "akm" \
"lkm" "rwlk" "rwlm" "rwkm" \
"ralk" "ralm" "wlkm" "alkm" \
"rwlkm" "ralkm" ; do
verify_binary_equality "leading and trailing perms for \"${perm}\"" \
"/t { ${prefix} /f ${perm}, }" \
"/t { ${prefix} ${perm} /f, }"
done
if [ "$allow" == "deny" ] ; then continue ; fi
for perm in "ux" "Ux" "px" "Px" "cx" "Cx" \
"ix" "pux" "Pux" "pix" "Pix" \
"cux" "Cux" "cix" "Cix"
do
verify_binary_equality "leading and trailing perms for \"${perm}\"" \
"/t { ${prefix} /f ${perm}, }" \
"/t { ${prefix} ${perm} /f, }"
done
for perm in "px" "Px" "cx" "Cx" \
"pux" "Pux" "pix" "Pix" \
"cux" "Cux" "cix" "Cix"
do
verify_binary_equality "leading and trailing perms for x-transition \"${perm}\"" \
"/t { ${prefix} /f ${perm} -> b, }" \
"/t { ${prefix} ${perm} /f -> b, }"
done
done
done
done
done
#Test rule overlap for x most specific match
for perm1 in "ux" "Ux" "px" "Px" "cx" "Cx" "ix" "pux" "Pux" \
"pix" "Pix" "cux" "Cux" "cix" "Cix" "px -> b" \
"Px -> b" "cx -> b" "Cx -> b" "pux -> b" "Pux ->b" \
"pix -> b" "Pix -> b" "cux -> b" "Cux -> b" \
"cix -> b" "Cix -> b"
do
for perm2 in "ux" "Ux" "px" "Px" "cx" "Cx" "ix" "pux" "Pux" \
"pix" "Pix" "cux" "Cux" "cix" "Cix" "px -> b" \
"Px -> b" "cx -> b" "Cx -> b" "pux -> b" "Pux ->b" \
"pix -> b" "Pix -> b" "cux -> b" "Cux -> b" \
"cix -> b" "Cix -> b"
do
if [ "$perm1" == "$perm2" ] ; then
verify_binary_equality "Exec perm \"${perm1}\" - most specific match: same as glob" \
"/t { /* ${perm1}, /f ${perm2}, }" \
"/t { /* ${perm1}, }"
else
verify_binary_inequality "Exec \"${perm1}\" vs \"${perm2}\" - most specific match: different from glob" \
"/t { /* ${perm1}, /f ${perm2}, }" \
"/t { /* ${perm1}, }"
fi
done
verify_binary_inequality "Exec \"${perm1}\" vs deny x - most specific match: different from glob" \
"/t { /* ${perm1}, audit deny /f x, }" \
"/t { /* ${perm1}, }"
done
#Test deny carves out permission
verify_binary_inequality "Deny removes r perm" \
"/t { /foo/[abc] r, audit deny /foo/b r, }" \
"/t { /foo/[abc] r, }"
verify_binary_equality "Deny removes r perm" \
"/t { /foo/[abc] r, audit deny /foo/b r, }" \
"/t { /foo/[ac] r, }"
#this one may not be true in the future depending on if the compiled profile
#is explicitly including deny permissions for dynamic composition
verify_binary_equality "Deny of ungranted perm" \
"/t { /foo/[abc] r, audit deny /foo/b w, }" \
"/t { /foo/[abc] r, }"
if [ $fails -ne 0 -o $errors -ne 0 ]
then
printf "ERRORS: %d\nFAILS: %d\n" $errors $fails 2>&1
exit $(($fails + $errors))
fi
[ -z "${verbose}" ] && printf "\n"
printf "PASS\n"
exit 0

View File

@@ -0,0 +1,9 @@
#
#=DESCRIPTION simple link access test
#=EXRESULT PASS
#
profile test {
audit deny link /alpha/beta -> /tmp/**,
}

View File

@@ -0,0 +1,9 @@
#
#=DESCRIPTION simple link access test
#=EXRESULT PASS
#
profile test {
deny link /alpha/beta -> /tmp/**,
}

View File

@@ -42,7 +42,7 @@ class AAParserValgrindTests(testlib.AATestTemplate):
self.maxDiff = None
def _runtest(self, testname, config):
parser_args = ['-Q', '-I', config.testdir]
parser_args = ['-Q', '-I', config.testdir, '-M', './features_files/features.all']
failure_rc = [VALGRIND_ERROR_CODE, testlib.TIMEOUT_ERROR_CODE]
command = [config.valgrind]
command.extend(VALGRIND_ARGS)

View File

@@ -20,6 +20,7 @@
owner /{,var/}run/gdm{,3}/*/database r,
owner /{,var/}run/lightdm/authority/[0-9]* r,
owner /{,var/}run/lightdm/*/xauthority r,
owner /{,var/}run/user/*/gdm/Xauthority r,
# the unix socket to use to connect to the display
/tmp/.X11-unix/* w,

View File

@@ -8,4 +8,6 @@
/usr/lib/aspell/ r,
/usr/lib/aspell/* r,
/usr/lib/aspell/*.so m,
/usr/share/aspell/ r,
/usr/share/aspell/* r,
/var/lib/aspell/* r,

View File

@@ -32,6 +32,7 @@
/usr/share/zoneinfo/ r,
/usr/share/zoneinfo/** r,
/usr/share/X11/locale/** r,
/{,var/}run/systemd/journal/dev-log w,
/usr/lib{,32,64}/locale/** mr,
/usr/lib{,32,64}/gconv/*.so mr,

View File

@@ -0,0 +1,17 @@
# vim:syntax=apparmor
# ------------------------------------------------------------------
#
# Copyright (C) 2015 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License published by the Free Software Foundation.
#
# ------------------------------------------------------------------
# mir libraries sometimes do not have a lib prefix
# see LP: #1422521
/usr/lib/@{multiarch}/mir/*.so* mr,
/usr/lib/@{multiarch}/mir/**/*.so* mr,
# unprivileged mir socket for clients

View File

@@ -1,6 +1,7 @@
# ------------------------------------------------------------------
#
# Copyright (C) 2002-2005 Novell/SUSE
# Copyright (C) 2015 Canonical, Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@@ -14,11 +15,21 @@
capability setgid,
capability sys_chroot,
# postfix's master can send us signals
signal receive peer=/usr/lib/postfix/master,
unix (send, receive) peer=(label=/usr/lib/postfix/master),
/etc/mailname r,
/etc/postfix/*.cf r,
/etc/postfix/*.db r,
@{PROC}/net/if_inet6 r,
/usr/lib/postfix/*.so mr,
/usr/lib64/sasl2/* mr,
/usr/lib64/sasl2/ r,
/usr/lib/sasl2/* mr,
/usr/lib/sasl2/ r,
/usr/lib{,32,64}/sasl2/* mr,
/usr/lib{,32,64}/sasl2/ r,
/usr/lib/@{multiarch}/sasl2/* mr,
/usr/lib/@{multiarch}/sasl2/ r,
/var/spool/postfix/etc/* r,
/var/spool/postfix/lib/lib*.so* mr,
/var/spool/postfix/lib/@{multiarch}/lib*.so* mr,

View File

@@ -12,6 +12,10 @@
/etc/ssl/ r,
/etc/ssl/certs/ r,
/etc/ssl/certs/* r,
/etc/pki/trust/ r,
/etc/pki/trust/* r,
/etc/pki/trust/anchors/ r,
/etc/pki/trust/anchors/** r,
/usr/share/ca-certificates/ r,
/usr/share/ca-certificates/** r,
/usr/share/ssl/certs/ca-bundle.crt r,

View File

@@ -10,6 +10,7 @@
/usr/bin/balsa Cx -> sanitized_helper,
/usr/bin/claws-mail Cx -> sanitized_helper,
/usr/bin/evolution Cx -> sanitized_helper,
/usr/bin/geary Cx -> sanitized_helper,
/usr/bin/gnome-gmail Cx -> sanitized_helper,
/usr/lib/GNUstep/Applications/GNUMail.app/GNUMail Cx -> sanitized_helper,
/usr/bin/kmail Cx -> sanitized_helper,

View File

@@ -59,6 +59,9 @@ profile sanitized_helper {
# permissions for /usr/share, but for now just do this. (LP: #972367)
/usr/share/software-center/* Pixr,
# Allow exec of texlive font build scripts (LP: #1010909)
/usr/share/texlive/texmf{,-dist}/web2c/{,**/}* Pixr,
# While the chromium and chrome sandboxes are setuid root, they only link
# in limited libraries so glibc's secure execution should be enough to not
# require the santized_helper (ie, LD_PRELOAD will only use standard system

View File

@@ -17,6 +17,7 @@
#include <abstractions/base>
#include <abstractions/mysql>
#include <abstractions/nameservice>
#include <abstractions/openssl>
#include <abstractions/wutmp>
#include <abstractions/dovecot-common>

View File

@@ -26,6 +26,7 @@
@{HOME} r, # ???
/usr/lib/dovecot/imap mr,
/{,var/}run/dovecot/auth-master rw,
# Site-specific additions and overrides. See local/README for details.
#include <local/usr.lib.dovecot.imap>

View File

@@ -24,6 +24,7 @@
network inet6 stream,
/usr/lib/dovecot/imap-login mr,
/{,var/}run/dovecot/anvil rw,
/{,var/}run/dovecot/login/ r,
/{,var/}run/dovecot/login/* rw,

View File

@@ -45,6 +45,8 @@
/var/lib/misc/dnsmasq.leases rw, # Required only for DHCP server usage
/bin/bash ix, # Required to execute --dhcp-script argument
# access to iface mtu needed for Router Advertisement messages in IPv6
# Neighbor Discovery protocol (RFC 2461)
@{PROC}/sys/net/ipv6/conf/*/mtu r,
@@ -64,9 +66,13 @@
/{,var/}run/libvirt/network/*.pid rw,
# libvirt lease helper
/usr/lib/libvirt/libvirt_leaseshelper ix,
/usr/lib{,64}/libvirt/libvirt_leaseshelper ix,
/{,var/}run/leaseshelper.pid rwk,
# lxc-net pid and lease files
/{,var/}run/lxc/dnsmasq.pid rw,
/var/lib/misc/dnsmasq.*.leases rw,
# NetworkManager integration
/{,var/}run/nm-dns-dnsmasq.conf r,
/{,var/}run/sendsigs.omit.d/*dnsmasq.pid w,

View File

@@ -15,6 +15,7 @@
/usr/sbin/dovecot {
#include <abstractions/authentication>
#include <abstractions/base>
#include <abstractions/dovecot-common>
#include <abstractions/mysql>
#include <abstractions/nameservice>
#include <abstractions/ssl_certs>
@@ -25,7 +26,6 @@
capability fsetid,
capability kill,
capability net_bind_service,
capability setgid,
capability setuid,
capability sys_chroot,
@@ -34,7 +34,6 @@
/etc/lsb-release r,
/etc/SuSE-release r,
@{PROC}/@{pid}/mounts r,
@{PROC}/filesystems r,
/usr/bin/doveconf rix,
/usr/lib/dovecot/anvil Px,
/usr/lib/dovecot/auth Px,

View File

@@ -1,6 +1,9 @@
# Last Modified: Mon Dec 1 22:23:12 2014
# ------------------------------------------------------------------
#
# Copyright (C) 2002-2005 Novell/SUSE
# Copyright (C) 2014 Christian Boltz
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@@ -8,12 +11,12 @@
#
# ------------------------------------------------------------------
# vim:syntax=apparmor
# Last Modified: Wed Aug 17 14:28:07 2005
#include <tunables/global>
/usr/sbin/mysqld {
#include <abstractions/base>
#include <abstractions/mysql>
#include <abstractions/nameservice>
#include <abstractions/user-tmp>
@@ -21,8 +24,22 @@
capability setgid,
capability setuid,
/etc/hosts.allow r,
/etc/hosts.deny r,
/etc/my.cnf r,
/etc/my.cnf.d/ r,
/etc/my.cnf.d/*.cnf r,
/root/.my.cnf r,
/usr/lib{,32,64}/**.so mr,
/usr/sbin/mysqld r,
/usr/share/mariadb/*/errmsg.sys r,
/usr/share/mysql-community-server/*/errmsg.sys r,
/usr/share/mysql/** r,
/var/lib/mysql/** lrw,
/var/lib/mysql/ r,
/var/lib/mysql/** rwl,
/var/log/mysql/mysqld-upgrade-run.log w,
/var/log/mysql/mysqld.log w,
/var/log/mysql/mysqld.log-20* w,
/{,var/}run/mysql/mysqld.pid w,
}

View File

@@ -4,7 +4,7 @@
#define SD_ID_MAGIC 0x8c235e38
inline int do_open (char * file)
static inline int do_open (char * file)
{
int fd, rc;
char buf[128];

View File

@@ -25,6 +25,7 @@ put_old=${new_root}put_old/
bad=$tmpdir/BAD/
proc=$new_root/proc
fstype="ext2"
root_was_shared="no"
pivot_root_cleanup() {
mountpoint -q "$proc"
@@ -36,9 +37,32 @@ pivot_root_cleanup() {
if [ $? -eq 0 ] ; then
umount "$new_root"
fi
if [ "${root_was_shared}" = "yes" ] ; then
[ -n "$VERBOSE" ] && echo 'notice: re-mounting / as shared'
mount --make-shared /
fi
}
do_onexit="pivot_root_cleanup"
# systemd mounts / and everything under it MS_SHARED. This breaks
# pivot_root entirely, so attempt to detect it, and remount /
# MS_PRIVATE temporarily.
FINDMNT=/bin/findmnt
if [ -x "${FINDMNT}" ] && ${FINDMNT} -no PROPAGATION / > /dev/null 2>&1 ; then
if [ "$(${FINDMNT} -no PROPAGATION /)" == "shared" ] ; then
root_was_shared="yes"
fi
elif [ "$(ps hp1 -ocomm)" = "systemd" ] ; then
# no findmnt or findmnt doesn't know the PROPAGATION column,
# but init is systemd so assume rootfs is shared
root_was_shared="yes"
fi
if [ "${root_was_shared}" = "yes" ] ; then
[ -n "$VERBOSE" ] && echo 'notice: re-mounting / as private'
mount --make-private /
fi
# Create disk image since pivot_root doesn't allow old root and new root to be
# on the same filesystem
dd if=/dev/zero of="$disk_img" bs=1024 count=512 2> /dev/null

View File

@@ -1,7 +1,7 @@
#! /usr/bin/env python
# ------------------------------------------------------------------
#
# Copyright (C) 2011-2013 Canonical Ltd.
# Copyright (C) 2011-2015 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@@ -68,16 +68,38 @@ if __name__ == "__main__":
apparmor.easyprof.print_basefilenames(easyp.get_templates())
sys.exit(0)
elif options.template and options.show_template:
files = [os.path.join(easyp.dirs['templates'], options.template)]
apparmor.easyprof.print_files(files)
sys_t = os.path.join(easyp.dirs['templates'], options.template)
inc_t = None
if options.include_templates_dir:
inc_t = os.path.join(easyp.dirs['templates_include'],
options.template)
if os.path.exists(sys_t):
apparmor.easyprof.print_files([sys_t])
elif os.path.exists(inc_t):
apparmor.easyprof.print_files([inc_t])
else:
error("Could not find '%s'" % options.template)
sys.exit(0)
elif options.list_policy_groups:
apparmor.easyprof.print_basefilenames(easyp.get_policy_groups())
sys.exit(0)
elif options.policy_groups and options.show_policy_group:
files = []
for g in options.policy_groups.split(','):
files = [os.path.join(easyp.dirs['policygroups'], g)]
apparmor.easyprof.print_files(files)
sys_g = os.path.join(easyp.dirs['policygroups'], g)
inc_g = None
if options.include_policy_groups_dir:
inc_g = os.path.join(easyp.dirs['policygroups_include'], g)
if os.path.exists(sys_g):
files.append(sys_g)
elif os.path.exists(inc_g):
files.append(inc_g)
else:
error("Could not find '%s'" % g)
apparmor.easyprof.print_files(files)
sys.exit(0)
elif binary == None and not options.profile_name and \
not options.manifest:

View File

@@ -97,7 +97,7 @@ the specified template uses this value. May be specified multiple times.
List available templates.
=item --show-template=TEMPLATE
=item --show-template
Display template specified with --template.
@@ -105,18 +105,30 @@ Display template specified with --template.
Use PATH instead of system templates directory.
=item --include-templates-dir=PATH
Include PATH when searching for templates in addition to the system templates
directory (or the one specified with --templates-dir). System templates will
match before those in PATH.
=item --list-policy-groups
List available policy groups.
=item --show-policy-group
Display policy groups specified with --policy.
Display policy groups specified with --policy-groups.
=item --policy-groups-dir=PATH
Use PATH instead of system policy-groups directory.
=item --include-policy-groups-dir=PATH
Include PATH when searching for policy groups in addition to the system
policy-groups directory (or the one specified with --policy-groups-dir). System
policy-groups will match before those in PATH.
=item --policy-version=VERSION
Must be used with --policy-vendor and is used to specify the version of policy

View File

@@ -134,10 +134,10 @@ def filter_processes(processes, status):
def find_apparmorfs():
'''Finds AppArmor mount point'''
for p in open("/proc/mounts").readlines():
if p.split()[2] == "securityfs" and \
os.path.exists(os.path.join(p.split()[1], "apparmor")):
return os.path.join(p.split()[1], "apparmor")
for p in open("/proc/mounts","rb").readlines():
if p.split()[2].decode() == "securityfs" and \
os.path.exists(os.path.join(p.split()[1].decode(), "apparmor")):
return os.path.join(p.split()[1].decode(), "apparmor")
return False
def errormsg(message):

View File

@@ -63,8 +63,9 @@ for pid in sorted(pids):
if os.path.exists("/proc/%s/attr/current"%pid):
with aa.open_file_read("/proc/%s/attr/current"%pid) as current:
for line in current:
if line.startswith("/") or line.startswith("null"):
attr = line.strip()
line = line.strip()
if line.endswith(' (complain)', 1) or line.endswith(' (enforce)', 1): # enforce at least one char as profile name
attr = line
cmdline = apparmor.common.cmd(["cat", "/proc/%s/cmdline"%pid])[1]
pname = cmdline.split("\0")[0]

View File

@@ -1,6 +1,6 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
# Copyright (C) 2014 Christian Boltz <apparmor@cboltz.de>
# Copyright (C) 2014-2015 Christian Boltz <apparmor@cboltz.de>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@@ -13,7 +13,7 @@
#
# ----------------------------------------------------------------------
# No old version logs, only 2.6 + supported
from __future__ import with_statement
from __future__ import division, with_statement
import inspect
import os
import re
@@ -31,7 +31,7 @@ import apparmor.severity
from copy import deepcopy
from apparmor.common import (AppArmorException, open_file_read, valid_path, hasher,
from apparmor.common import (AppArmorException, AppArmorBug, open_file_read, valid_path, hasher,
open_file_write, convert_regexp, DebugLogger)
import apparmor.ui as aaui
@@ -48,7 +48,8 @@ from apparmor.regex import (RE_PROFILE_START, RE_PROFILE_END, RE_PROFILE_CAP, RE
RE_NETWORK_FAMILY_TYPE, RE_NETWORK_FAMILY, RE_PROFILE_CHANGE_HAT,
RE_PROFILE_HAT_DEF, RE_PROFILE_DBUS, RE_PROFILE_MOUNT,
RE_PROFILE_SIGNAL, RE_PROFILE_PTRACE, RE_PROFILE_PIVOT_ROOT,
RE_PROFILE_UNIX, RE_RULE_HAS_COMMA, RE_HAS_COMMENT_SPLIT )
RE_PROFILE_UNIX, RE_RULE_HAS_COMMA, RE_HAS_COMMENT_SPLIT,
strip_quotes, parse_profile_start_line )
import apparmor.rules as aarules
@@ -603,9 +604,9 @@ def get_profile_flags(filename, program):
with open_file_read(filename) as f_in:
for line in f_in:
if RE_PROFILE_START.search(line):
matches = RE_PROFILE_START.search(line).groups()
profile = matches[1] or matches[3]
flags = matches[6]
matches = parse_profile_start_line(line, filename)
profile = matches['profile']
flags = matches['flags']
if profile == program or program is None:
return flags
@@ -638,49 +639,52 @@ def change_profile_flags(filename, program, flag, set_flag):
def set_profile_flags(prof_filename, program, newflags):
"""Reads the old profile file and updates the flags accordingly"""
regex_bin_flag = re.compile('^(\s*)("?(/.+?)"??|(profile\s+"?(.+?)"??))\s+((flags=)?\((.*)\)\s+)?\{\s*(#.*)?$')
# TODO: use RE_PROFILE_START (only difference: doesn't have a match group for the leading space)
# TODO: also use the global regex for matching the hat
# TODO: count the number of matching lines (separated by profile and hat?) and return it
# so that code calling this function can make sure to only report success if there was a match
regex_hat_flag = re.compile('^([a-z]*)\s+([A-Z]*)\s*(#.*)?$')
if os.path.isfile(prof_filename):
with open_file_read(prof_filename) as f_in:
temp_file = tempfile.NamedTemporaryFile('w', prefix=prof_filename, suffix='~', delete=False, dir=profile_dir)
shutil.copymode(prof_filename, temp_file.name)
with open_file_write(temp_file.name) as f_out:
for line in f_in:
comment = ''
if '#' in line:
comment = '#' + line.split('#', 1)[1].rstrip()
match = regex_bin_flag.search(line)
if not line.strip() or line.strip().startswith('#'):
pass
elif match:
matches = match.groups()
space = matches[0]
profile = matches[1] # profile name including quotes and "profile" keyword
if matches[2]:
binary = matches[2]
else:
binary = matches[4]
flag = matches[6] or 'flags='
flags = matches[7]
if binary == program or program is None:
if newflags:
line = '%s%s %s(%s) {%s\n' % (space, profile, flag, newflags, comment)
else:
line = '%s%s {%s\n' % (space, profile, comment)
else:
match = regex_hat_flag.search(line)
if match:
hat, flags = match.groups()[:2]
if newflags:
line = '%s flags=(%s) {%s\n' % (hat, newflags, comment)
else:
line = '%s {%s\n' % (hat, comment)
f_out.write(line)
os.rename(temp_file.name, prof_filename)
# TODO: use RE_PROFILE_HAT_DEF for matching the hat (regex_hat_flag is totally broken!)
#regex_hat_flag = re.compile('^([a-z]*)\s+([A-Z]*)\s*(#.*)?$')
found = False
if newflags and newflags.strip() == '':
raise AppArmorBug('New flags for %s contain only whitespace' % prof_filename)
with open_file_read(prof_filename) as f_in:
temp_file = tempfile.NamedTemporaryFile('w', prefix=prof_filename, suffix='~', delete=False, dir=profile_dir)
shutil.copymode(prof_filename, temp_file.name)
with open_file_write(temp_file.name) as f_out:
for line in f_in:
if RE_PROFILE_START.search(line):
matches = parse_profile_start_line(line, prof_filename)
space = matches['leadingspace'] or ''
profile = matches['profile']
if profile == program or program is None:
found = True
header_data = {
'attachment': matches['attachment'] or '',
'flags': newflags,
'profile_keyword': matches['profile_keyword'],
'header_comment': matches['comment'] or '',
}
line = write_header(header_data, len(space)/2, profile, False, True)
line = '%s\n' % line[0]
#else:
# match = regex_hat_flag.search(line)
# if match:
# hat, flags = match.groups()[:2]
# if newflags:
# line = '%s flags=(%s) {%s\n' % (hat, newflags, comment)
# else:
# line = '%s {%s\n' % (hat, comment)
f_out.write(line)
os.rename(temp_file.name, prof_filename)
if not found:
if program is None:
raise AppArmorBug("%(file)s doesn't contain a valid profile (syntax error?)" % {'file': prof_filename})
else:
raise AppArmorBug("%(file)s doesn't contain a valid profile for %(profile)s (syntax error?)" % {'file': prof_filename, 'profile': program})
def profile_exists(program):
"""Returns True if profile exists, False otherwise"""
@@ -2546,17 +2550,25 @@ def validate_profile_mode(mode, allow, nt_name=None):
else:
return False
# rpm backup files, dotfiles, emacs backup files should not be processed
# The skippable files type needs be synced with apparmor initscript
def is_skippable_file(path):
"""Returns True if filename matches something to be skipped"""
if (re.search('(^|/)\.[^/]*$', path) or re.search('\.rpm(save|new)$', path)
or re.search('\.dpkg-(old|new)$', path) or re.search('\.swp$', path)
or path[-1] == '~' or path == 'README'):
"""Returns True if filename matches something to be skipped (rpm or dpkg backup files, hidden files etc.)
The list of skippable files needs to be synced with apparmor initscript and libapparmor _aa_is_blacklisted()
path: filename (with or without directory)"""
basename = os.path.basename(path)
if not basename or basename[0] == '.' or basename == 'README':
return True
skippable_suffix = ('.dpkg-new', '.dpkg-old', '.dpkg-dist', '.dpkg-bak', '.rpmnew', '.rpmsave', '.orig', '.rej', '~')
if basename.endswith(skippable_suffix):
return True
return False
def is_skippable_dir(path):
if re.search('(disable|cache|force-complain|lxc)', path):
if re.search('^(.*/)?(disable|cache|force-complain|lxc)/?$', path):
return True
return False
@@ -2620,6 +2632,41 @@ def attach_profile_data(profiles, profile_data):
for p in profile_data.keys():
profiles[p] = deepcopy(profile_data[p])
def parse_profile_start(line, file, lineno, profile, hat):
matches = parse_profile_start_line(line, file)
if profile: # we are inside a profile, so we expect a child profile
if not matches['profile_keyword']:
raise AppArmorException(_('%(profile)s profile in %(file)s contains syntax errors in line %(line)s: missing "profile" keyword.') % {
'profile': profile, 'file': file, 'line': lineno + 1 })
if profile != hat:
# nesting limit reached - a child profile can't contain another child profile
raise AppArmorException(_('%(profile)s profile in %(file)s contains syntax errors in line %(line)s: a child profile inside another child profile is not allowed.') % {
'profile': profile, 'file': file, 'line': lineno + 1 })
hat = matches['profile']
in_contained_hat = True
pps_set_profile = True
pps_set_hat_external = False
else: # stand-alone profile
profile = matches['profile']
if len(profile.split('//')) >= 2:
profile, hat = profile.split('//')[:2]
pps_set_hat_external = True
else:
hat = profile
pps_set_hat_external = False
in_contained_hat = False
pps_set_profile = False
attachment = matches['attachment']
flags = matches['flags']
return (profile, hat, attachment, flags, in_contained_hat, pps_set_profile, pps_set_hat_external)
def parse_profile_data(data, file, do_include):
profile_data = hasher()
profile = None
@@ -2643,41 +2690,17 @@ def parse_profile_data(data, file, do_include):
lastline = None
# Starting line of a profile
if RE_PROFILE_START.search(line):
matches = RE_PROFILE_START.search(line).groups()
if profile:
#print(profile, hat)
if profile != hat or not matches[3]:
raise AppArmorException(_('%(profile)s profile in %(file)s contains syntax errors in line: %(line)s.') % { 'profile': profile, 'file': file, 'line': lineno + 1 })
# Keep track of the start of a profile
if profile and profile == hat and matches[3]:
# local profile
hat = matches[3]
in_contained_hat = True
(profile, hat, attachment, flags, in_contained_hat, pps_set_profile, pps_set_hat_external) = parse_profile_start(line, file, lineno, profile, hat)
if attachment:
profile_data[profile][hat]['attachment'] = attachment
if pps_set_profile:
profile_data[profile][hat]['profile'] = True
else:
if matches[1]:
profile = matches[1]
else:
profile = matches[3]
#print(profile)
if len(profile.split('//')) >= 2:
profile, hat = profile.split('//')[:2]
else:
hat = None
in_contained_hat = False
if hat:
profile_data[profile][hat]['external'] = True
else:
hat = profile
if pps_set_hat_external:
profile_data[profile][hat]['external'] = True
# Profile stored
existing_profiles[profile] = file
flags = matches[6]
profile = strip_quotes(profile)
if hat:
hat = strip_quotes(hat)
# save profile name and filename
profile_data[profile][hat]['name'] = profile
profile_data[profile][hat]['filename'] = file
@@ -2878,7 +2901,7 @@ def parse_profile_data(data, file, do_include):
path = strip_quotes(matches[4].strip())
mode = matches[5]
nt_name = matches[6]
nt_name = matches[7]
if nt_name:
nt_name = nt_name.strip()
@@ -3236,13 +3259,12 @@ def parse_unix_rule(line):
def separate_vars(vs):
"""Returns a list of all the values for a variable"""
data = []
data = set()
#data = [i.strip('"') for i in vs.split()]
RE_VARS = re.compile('\s*((\".+?\")|([^\"]\S+))\s*(.*)$')
while RE_VARS.search(vs):
matches = RE_VARS.search(vs).groups()
data.append(strip_quotes(matches[0]))
data.add(strip_quotes(matches[0]))
vs = matches[3]
return data
@@ -3254,29 +3276,25 @@ def is_active_profile(pname):
return False
def store_list_var(var, list_var, value, var_operation, filename):
"""Store(add new variable or add values to variable) the variables encountered in the given list_var"""
"""Store(add new variable or add values to variable) the variables encountered in the given list_var
- the 'var' parameter will be modified
- 'list_var' is the variable name, for example '@{foo}'
"""
vlist = separate_vars(value)
if var_operation == '=':
if not var.get(list_var, False):
var[list_var] = set(vlist)
else:
#print('Ignored: New definition for variable for:',list_var,'=', value, 'operation was:',var_operation,'old value=', var[list_var])
raise AppArmorException(_('Redefining existing variable %(variable)s: %(value)s in %(file)s') % { 'variable': list_var, 'value': value, 'file': filename })
elif var_operation == '+=':
if var.get(list_var, False):
var[list_var] = set(var[list_var] + vlist)
var[list_var] |= vlist
else:
raise AppArmorException(_('Values added to a non-existing variable %(variable)s: %(value)s in %(file)s') % { 'variable': list_var, 'value': value, 'file': filename })
else:
raise AppArmorException(_('Unknown variable operation %(operation)s for variable %(variable)s in %(file)s') % { 'operation': var_operation, 'variable': list_var, 'file': filename })
def strip_quotes(data):
if data[0] + data[-1] == '""':
return data[1:-1]
else:
return data
def quote_if_needed(data):
# quote data if it contains whitespace
if ' ' in data:
@@ -3291,17 +3309,27 @@ def escape(escape):
return escape
def write_header(prof_data, depth, name, embedded_hat, write_flags):
pre = ' ' * depth
pre = ' ' * int(depth * 2)
data = []
unquoted_name = name
name = quote_if_needed(name)
if (not embedded_hat and re.search('^[^/]|^"[^/]', name)) or (embedded_hat and re.search('^[^^]', name)):
name = 'profile %s' % name
attachment = ''
if prof_data['attachment']:
attachment = ' %s' % quote_if_needed(prof_data['attachment'])
comment = ''
if prof_data['header_comment']:
comment = ' %s' % prof_data['header_comment']
if (not embedded_hat and re.search('^[^/]', unquoted_name)) or (embedded_hat and re.search('^[^^]', unquoted_name)) or prof_data['attachment'] or prof_data['profile_keyword']:
name = 'profile %s%s' % (name, attachment)
flags = ''
if write_flags and prof_data['flags']:
data.append('%s%s flags=(%s) {' % (pre, name, prof_data['flags']))
else:
data.append('%s%s {' % (pre, name))
flags = ' flags=(%s)' % prof_data['flags']
data.append('%s%s%s {%s' % (pre, name, flags, comment))
return data
@@ -3408,14 +3436,18 @@ def write_net_rules(prof_data, depth, allow):
data.append('%s%snetwork,' % (pre, audit))
else:
for fam in sorted(prof_data[allow]['netdomain']['rule'].keys()):
audit = ''
if prof_data[allow]['netdomain']['rule'][fam] is True:
if prof_data[allow]['netdomain']['audit'][fam]:
audit = 'audit'
data.append('%s%s%snetwork %s' % (pre, audit, allowstr, fam))
audit = 'audit '
if fam == 'all':
data.append('%s%s%snetwork,' % (pre, audit, allowstr))
else:
data.append('%s%s%snetwork %s,' % (pre, audit, allowstr, fam))
else:
for typ in sorted(prof_data[allow]['netdomain']['rule'][fam].keys()):
if prof_data[allow]['netdomain']['audit'][fam].get(typ, False):
audit = 'audit'
audit = 'audit '
data.append('%s%s%snetwork %s %s,' % (pre, audit, allowstr, fam, typ))
if prof_data[allow].get('netdomain', False):
data.append('')
@@ -3733,6 +3765,14 @@ def serialize_profile(profile_data, name, options):
return string + '\n'
def serialize_parse_profile_start(line, file, lineno, profile, hat, prof_data_profile, prof_data_external, correct):
(profile, hat, attachment, flags, in_contained_hat, pps_set_profile, pps_set_hat_external) = parse_profile_start(line, file, lineno, profile, hat)
if hat and profile != hat and '%s//%s'%(profile, hat) in line and not prof_data_external:
correct = False
return (profile, hat, attachment, flags, in_contained_hat, correct)
def serialize_profile_from_old_profile(profile_data, name, options):
data = []
string = ''
@@ -3835,31 +3875,9 @@ def serialize_profile_from_old_profile(profile_data, name, options):
line = line.rstrip('\n')
#data.append(' ')#data.append('read: '+line)
if RE_PROFILE_START.search(line):
matches = RE_PROFILE_START.search(line).groups()
if profile and profile == hat and matches[3]:
hat = matches[3]
in_contained_hat = True
if write_prof_data[profile][hat]['profile']:
pass
else:
if matches[1]:
profile = matches[1]
else:
profile = matches[3]
if len(profile.split('//')) >= 2:
profile, hat = profile.split('//')[:2]
else:
hat = None
in_contained_hat = False
if hat and not write_prof_data[profile][hat]['external']:
correct = False
else:
hat = profile
flags = matches[6]
profile = strip_quotes(profile)
if hat:
hat = strip_quotes(hat)
(profile, hat, attachment, flags, in_contained_hat, correct) = serialize_parse_profile_start(
line, prof_filename, None, profile, hat, write_prof_data[profile][hat]['profile'], write_prof_data[profile][hat]['external'], correct)
if not write_prof_data[hat]['name'] == profile:
correct = False
@@ -4085,7 +4103,11 @@ def serialize_profile_from_old_profile(profile_data, name, options):
var_operation = matches[1]
value = strip_quotes(matches[2])
var_set = hasher()
if profile:
if var_operation == '+=':
correct = False # adding proper support for "add to variable" needs big changes
# (like storing a variable's "history" - where it was initially defined and what got added where)
# so just skip any comparison and assume a non-match
elif profile:
store_list_var(var_set, list_var, value, var_operation, prof_filename)
if not var_set[list_var] == write_prof_data['lvar'].get(list_var, False):
correct = False
@@ -4147,7 +4169,7 @@ def serialize_profile_from_old_profile(profile_data, name, options):
path = strip_quotes(matches[4].strip())
mode = matches[5]
nt_name = matches[6]
nt_name = matches[7]
if nt_name:
nt_name = nt_name.strip()
@@ -4157,14 +4179,17 @@ def serialize_profile_from_old_profile(profile_data, name, options):
else:
tmpmode = str_to_mode(mode)
if not write_prof_data[hat][allow]['path'][path].get('mode', set()) & tmpmode:
if not write_prof_data[hat][allow]['path'].get(path):
correct = False
else:
if not write_prof_data[hat][allow]['path'][path].get('mode', set()) & tmpmode:
correct = False
if nt_name and not write_prof_data[hat][allow]['path'][path].get('to', False) == nt_name:
correct = False
if nt_name and not write_prof_data[hat][allow]['path'][path].get('to', False) == nt_name:
correct = False
if audit and not write_prof_data[hat][allow]['path'][path].get('audit', set()) & tmpmode:
correct = False
if audit and not write_prof_data[hat][allow]['path'][path].get('audit', set()) & tmpmode:
correct = False
if correct:
if not segments['path'] and True in segments.values():
@@ -4435,6 +4460,8 @@ def load_include(incname):
#If the include is a directory means include all subfiles
elif os.path.isdir(profile_dir + '/' + incfile):
load_includeslist += list(map(lambda x: incfile + '/' + x, os.listdir(profile_dir + '/' + incfile)))
else:
raise AppArmorException("Include file %s not found" % (profile_dir + '/' + incfile) )
return 0

View File

@@ -1,6 +1,6 @@
# ------------------------------------------------------------------
#
# Copyright (C) 2011-2013 Canonical Ltd.
# Copyright (C) 2011-2015 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@@ -312,15 +312,26 @@ class AppArmorEasyProfile:
# the templates directory to the parent of the template so we don't
# have to require --template-dir with absolute paths.
self.dirs['templates'] = os.path.abspath(os.path.dirname(opt.template))
if opt.include_templates_dir and \
os.path.isdir(opt.include_templates_dir):
self.dirs['templates_include'] = os.path.abspath(opt.include_templates_dir)
if opt.policy_groups_dir and os.path.isdir(opt.policy_groups_dir):
self.dirs['policygroups'] = os.path.abspath(opt.policy_groups_dir)
if opt.include_policy_groups_dir and \
os.path.isdir(opt.include_policy_groups_dir):
self.dirs['policygroups_include'] = os.path.abspath(opt.include_policy_groups_dir)
self.policy_version = None
self.policy_vendor = None
if (opt.policy_version and not opt.policy_vendor) or \
(opt.policy_vendor and not opt.policy_version):
raise AppArmorException("Must specify both policy version and vendor")
# If specified --policy-version and --policy-vendor, use
# templates_dir/policy_vendor/policy_version
if opt.policy_version and opt.policy_vendor:
self.policy_vendor = opt.policy_vendor
self.policy_version = str(opt.policy_version)
@@ -361,11 +372,22 @@ class AppArmorEasyProfile:
for f in get_directory_contents(self.dirs['templates']):
if os.path.isfile(f):
self.templates.append(f)
if 'templates_include' in self.dirs:
for f in get_directory_contents(self.dirs['templates_include']):
if os.path.isfile(f) and f not in self.templates:
self.templates.append(f)
self.policy_groups = []
for f in get_directory_contents(self.dirs['policygroups']):
if os.path.isfile(f):
self.policy_groups.append(f)
if 'policygroups_include' in self.dirs:
for f in get_directory_contents(self.dirs['policygroups_include']):
if os.path.isfile(f) and f not in self.policy_groups:
self.policy_groups.append(f)
def _get_defaults(self):
'''Read in defaults from configuration'''
if not os.path.exists(self.conffile):
@@ -411,13 +433,25 @@ class AppArmorEasyProfile:
elif template.startswith('/') and not allow_abs_path:
raise AppArmorException("Cannot use an absolute path template '%s'" % template)
# If have an abs path, just use it
if template.startswith('/'):
if not os.path.exists(template):
raise AppArmorException('%s does not exist' % (template))
self.template = template
else:
self.template = os.path.join(self.dirs['templates'], template)
return
if not os.path.exists(self.template):
raise AppArmorException('%s does not exist' % (self.template))
# Find the template since we don't have an abs path
sys_t = os.path.join(self.dirs['templates'], template)
inc_t = None
if 'templates_include' in self.dirs:
inc_t = os.path.join(self.dirs['templates_include'], template)
if os.path.exists(sys_t):
self.template = sys_t
elif inc_t is not None and os.path.exists(inc_t):
self.template = inc_t
else:
raise AppArmorException('%s does not exist' % (template))
def get_templates(self):
'''Get list of all available templates by filename'''
@@ -427,7 +461,16 @@ class AppArmorEasyProfile:
'''Get contents of specific policygroup'''
p = policygroup
if not p.startswith('/'):
p = os.path.join(self.dirs['policygroups'], p)
sys_p = os.path.join(self.dirs['policygroups'], p)
inc_p = None
if 'policygroups_include' in self.dirs:
inc_p = os.path.join(self.dirs['policygroups_include'], p)
if os.path.exists(sys_p):
p = sys_p
elif inc_p is not None and os.path.exists(inc_p):
p = inc_p
if self.policy_groups == None or not p in self.policy_groups:
raise AppArmorException("Policy group '%s' does not exist" % p)
return open(p).read()
@@ -437,11 +480,25 @@ class AppArmorEasyProfile:
self.policy_groups = []
if policygroups != None:
for p in policygroups.split(','):
if not p.startswith('/'):
p = os.path.join(self.dirs['policygroups'], p)
if not os.path.exists(p):
# If have abs path, just use it
if p.startswith('/'):
if not os.path.exists(p):
raise AppArmorException('%s does not exist' % (p))
self.policy_groups.append(p)
continue
# Find the policy group since we don't have and abs path
sys_p = os.path.join(self.dirs['policygroups'], p)
inc_p = None
if 'policygroups_include' in self.dirs:
inc_p = os.path.join(self.dirs['policygroups_include'], p)
if os.path.exists(sys_p):
self.policy_groups.append(sys_p)
elif inc_p is not None and os.path.exists(inc_p):
self.policy_groups.append(inc_p)
else:
raise AppArmorException('%s does not exist' % (p))
self.policy_groups.append(p)
def get_policy_groups(self):
'''Get list of all policy groups by filename'''
@@ -777,6 +834,10 @@ def add_parser_policy_args(parser):
dest="templates_dir",
help="Use non-default templates directory",
metavar="DIR")
parser.add_option("--include-templates-dir",
dest="include_templates_dir",
help="Also search DIR for templates",
metavar="DIR")
parser.add_option("-p", "--policy-groups",
action="callback",
callback=check_for_manifest_arg,
@@ -787,6 +848,10 @@ def add_parser_policy_args(parser):
dest="policy_groups_dir",
help="Use non-default policy-groups directory",
metavar="DIR")
parser.add_option("--include-policy-groups-dir",
dest="include_policy_groups_dir",
help="Also search DIR for policy groups",
metavar="DIR")
parser.add_option("--policy-version",
action="callback",
callback=check_for_manifest_arg,

View File

@@ -1,5 +1,6 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
# Copyright (C) 2015 Christian Boltz <apparmor@cboltz.de>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@@ -25,7 +26,7 @@ from apparmor.translations import init_translation
_ = init_translation()
class ReadLog:
RE_LOG_v2_6_syslog = re.compile('kernel:\s+(\[[\d\.\s]+\]\s+)?type=\d+\s+audit\([\d\.\:]+\):\s+apparmor=')
RE_LOG_v2_6_syslog = re.compile('kernel:\s+(\[[\d\.\s]+\]\s+)?(audit:\s+)?type=\d+\s+audit\([\d\.\:]+\):\s+apparmor=')
RE_LOG_v2_6_audit = re.compile('type=AVC\s+(msg=)?audit\([\d\.\:]+\):\s+apparmor=')
# Used by netdomain to identify the operation types
# New socket names
@@ -111,34 +112,15 @@ class ReadLog:
ev['pid'] = event.pid
ev['task'] = event.task
ev['info'] = event.info
dmask = event.denied_mask
rmask = event.requested_mask
ev['error_code'] = event.error_code
ev['denied_mask'] = event.denied_mask
ev['request_mask'] = event.requested_mask
ev['magic_token'] = event.magic_token
if ev['operation'] and self.op_type(ev['operation']) == 'net':
ev['family'] = event.net_family
ev['protocol'] = event.net_protocol
ev['sock_type'] = event.net_sock_type
LibAppArmor.free_record(event)
# Map c (create) to a and d (delete) to w, logprof doesn't support c and d
if rmask:
rmask = rmask.replace('c', 'a')
rmask = rmask.replace('d', 'w')
if not validate_log_mode(hide_log_mode(rmask)):
raise AppArmorException(_('Log contains unknown mode %s') % rmask)
if dmask:
dmask = dmask.replace('c', 'a')
dmask = dmask.replace('d', 'w')
if not validate_log_mode(hide_log_mode(dmask)):
raise AppArmorException(_('Log contains unknown mode %s') % dmask)
#print('parse_event:', ev['profile'], dmask, ev['name2'])
mask, name = log_str_to_mode(ev['profile'], dmask, ev['name2'])
ev['denied_mask'] = mask
ev['name2'] = name
mask, name = log_str_to_mode(ev['profile'], rmask, ev['name2'])
ev['request_mask'] = mask
ev['name2'] = name
if not ev['time']:
ev['time'] = int(time.time())
@@ -162,6 +144,11 @@ class ReadLog:
except KeyError:
ev['aamode'] = None
# "translate" disconnected paths to errors, which means the event will be ignored.
# XXX Ideally we should propose to add the attach_disconnected flag to the profile
if ev['error_code'] == 13 and ev['info'] == 'Failed name lookup - disconnected path':
ev['aamode'] = 'ERROR'
if ev['aamode']:
#debug_logger.debug(ev)
return ev
@@ -248,6 +235,10 @@ class ReadLog:
if profile != 'null-complain-profile' and not self.profile_exists(profile):
return None
if e['operation'] == 'exec':
# convert rmask and dmask to mode arrays
e['denied_mask'], e['name2'] = log_str_to_mode(e['profile'], e['denied_mask'], e['name2'])
e['request_mask'], e['name2'] = log_str_to_mode(e['profile'], e['request_mask'], e['name2'])
if e.get('info', False) and e['info'] == 'mandatory profile missing':
self.add_to_tree(e['pid'], e['parent'], 'exec',
[profile, hat, aamode, 'PERMITTING', e['denied_mask'], e['name'], e['name2']])
@@ -257,21 +248,29 @@ class ReadLog:
else:
self.debug_logger.debug('add_event_to_tree: dropped exec event in %s' % e['profile'])
elif 'file_' in e['operation']:
self.add_to_tree(e['pid'], e['parent'], 'path',
[profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
elif e['operation'] in ['open', 'truncate', 'mkdir', 'mknod', 'rename_src',
'rename_dest', 'unlink', 'rmdir', 'symlink_create', 'link']:
#print(e['operation'], e['name'])
self.add_to_tree(e['pid'], e['parent'], 'path',
[profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
elif e['operation'] == 'capable':
self.add_to_tree(e['pid'], e['parent'], 'capability',
[profile, hat, prog, aamode, e['name'], ''])
elif e['operation'] == 'setattr' or 'xattr' in e['operation']:
self.add_to_tree(e['pid'], e['parent'], 'path',
[profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
elif 'inode_' in e['operation']:
elif ( e['operation'].startswith('file_') or e['operation'].startswith('inode_') or
e['operation'] in ['open', 'truncate', 'mkdir', 'mknod', 'chmod', 'rename_src',
'rename_dest', 'unlink', 'rmdir', 'symlink_create', 'link',
'sysctl', 'getattr', 'setattr', 'xattr'] ):
# Map c (create) to a and d (delete) to w (logging is more detailed than the profile language)
rmask = e['request_mask']
rmask = rmask.replace('c', 'a')
rmask = rmask.replace('d', 'w')
if not validate_log_mode(hide_log_mode(rmask)):
raise AppArmorException(_('Log contains unknown mode %s') % rmask)
dmask = e['denied_mask']
dmask = dmask.replace('c', 'a')
dmask = dmask.replace('d', 'w')
if not validate_log_mode(hide_log_mode(dmask)):
raise AppArmorException(_('Log contains unknown mode %s') % dmask)
# convert rmask and dmask to mode arrays
e['denied_mask'], e['name2'] = log_str_to_mode(e['profile'], dmask, e['name2'])
e['request_mask'], e['name2'] = log_str_to_mode(e['profile'], rmask, e['name2'])
# check if this is an exec event
is_domain_change = False
if e['operation'] == 'inode_permission' and (e['denied_mask'] & AA_MAY_EXEC) and aamode == 'PERMITTING':
following = self.peek_at_next_log_entry()
@@ -288,9 +287,9 @@ class ReadLog:
self.add_to_tree(e['pid'], e['parent'], 'path',
[profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
elif e['operation'] == 'sysctl':
self.add_to_tree(e['pid'], e['parent'], 'path',
[profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
elif e['operation'] == 'capable':
self.add_to_tree(e['pid'], e['parent'], 'capability',
[profile, hat, prog, aamode, e['name'], ''])
elif e['operation'] == 'clone':
parent, child = e['pid'], e['task']

View File

@@ -1,6 +1,6 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
# Copyright (C) 2014 Christian Boltz <apparmor@cboltz.de>
# Copyright (C) 2014-2015 Christian Boltz <apparmor@cboltz.de>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@@ -14,6 +14,11 @@
# ----------------------------------------------------------------------
import re
from apparmor.common import AppArmorBug, AppArmorException
# setup module translations
from apparmor.translations import init_translation
_ = init_translation()
## Profile parsing Regex
RE_AUDIT_DENY = '^\s*(?P<audit>audit\s+)?(?P<allow>allow\s+|deny\s+)?' # line start, optionally: leading whitespace, <audit> and <allow>/deny
@@ -21,7 +26,6 @@ RE_OWNER = '(?P<owner>owner\s+)?' # optionally: <owner>
RE_EOL = '\s*(?P<comment>#.*?)?\s*$' # optional whitespace, optional <comment>, optional whitespace, end of the line
RE_COMMA_EOL = '\s*,' + RE_EOL # optional whitespace, comma + RE_EOL
RE_PROFILE_START = re.compile('^\s*("?(/.+?)"??|(profile\s+"?(.+?)"??))\s+((flags=)?\((.+)\)\s+)?\{' + RE_EOL)
RE_PROFILE_END = re.compile('^\s*\}' + RE_EOL)
RE_PROFILE_CAP = re.compile(RE_AUDIT_DENY + 'capability(?P<capability>(\s+\S+)+)?' + RE_COMMA_EOL)
RE_PROFILE_LINK = re.compile(RE_AUDIT_DENY + 'link\s+(((subset)|(<=))\s+)?([\"\@\/].*?"??)\s+->\s*([\"\@\/].*?"??)' + RE_COMMA_EOL)
@@ -55,3 +59,51 @@ RE_RULE_HAS_COMMA = re.compile('^' + __re_no_or_quoted_hash +
RE_HAS_COMMENT_SPLIT = re.compile('^(?P<not_comment>' + __re_no_or_quoted_hash + ')' + # store in 'not_comment' group
'(?P<comment>#.*)$') # match trailing comment and store in 'comment' group
RE_PROFILE_START = re.compile(
'^(?P<leadingspace>\s*)' +
'(' +
'(?P<plainprofile>(/\S+|"[^"]+"))' + # just a path
'|' + # or
'(' + 'profile' + '\s+(?P<namedprofile>(\S+|"[^"]+"))' + '(\s+(?P<attachment>(/\S+|"/[^"]+")))?' + ')' + # 'profile', profile name, optionally attachment
')' +
'\s+((flags=)?\((?P<flags>.+)\)\s+)?\{' +
RE_EOL)
def parse_profile_start_line(line, filename):
matches = RE_PROFILE_START.search(line)
if not matches:
raise AppArmorBug('The given line from file %(filename)s is not the start of a profile: %(line)s' % { 'filename': filename, 'line': line } )
result = {}
for section in [ 'leadingspace', 'plainprofile', 'namedprofile', 'attachment', 'flags', 'comment']:
if matches.group(section):
result[section] = matches.group(section)
# sections with optional quotes
if section in ['plainprofile', 'namedprofile', 'attachment']:
result[section] = strip_quotes(result[section])
else:
result[section] = None
if result['flags'] and result['flags'].strip() == '':
raise AppArmorException(_('Invalid syntax in %(filename)s: Empty set of flags in line %(line)s.' % { 'filename': filename, 'line': line } ))
if result['plainprofile']:
result['profile'] = result['plainprofile']
result['profile_keyword'] = False
else:
result['profile'] = result['namedprofile']
result['profile_keyword'] = True
return result
def strip_quotes(data):
if data[0] + data[-1] == '""':
return data[1:-1]
else:
return data

View File

@@ -32,9 +32,6 @@ class aa_tools:
if tool_name in ['audit']:
self.remove = args.remove
elif tool_name == 'disable':
self.disabledir = apparmor.profile_dir + '/disable'
self.check_disable_dir()
elif tool_name == 'autodep':
self.force = args.force
self.aa_mountpoint = apparmor.check_for_apparmor()
@@ -50,10 +47,6 @@ class aa_tools:
if not user_perm(apparmor.profile_dir):
raise apparmor.AppArmorException("Cannot write to profile directory: %s" % (apparmor.profile_dir))
def check_disable_dir(self):
if not os.path.isdir(self.disabledir):
raise apparmor.AppArmorException("Can't find AppArmor disable directory %s" % self.disabledir)
def get_next_to_profile(self):
'''Iterator function to walk the list of arguments passed'''
@@ -89,12 +82,13 @@ class aa_tools:
yield (program, profile)
def act(self):
# used by aa-cleanprof
apparmor.read_profiles()
for (program, profile) in self.get_next_to_profile():
if program is None:
program = profile
apparmor.read_profiles()
if not program or not(os.path.exists(program) or apparmor.profile_exists(program)):
if program and not program.startswith('/'):
program = aaui.UI_GetString(_('The given program cannot be found, please try with the fully qualified path name of the program: '), '')
@@ -116,10 +110,7 @@ class aa_tools:
# One simply does not walk in here!
raise apparmor.AppArmorException('Unknown tool: %s' % self.name)
cmd_info = cmd([apparmor.parser, '-I%s' % apparmor.profile_dir, '-R', filename])
if cmd_info[0] != 0:
raise apparmor.AppArmorException(cmd_info[1])
self.reload_profile(profile)
else:
if '/' not in program:
@@ -129,6 +120,8 @@ class aa_tools:
sys.exit(1)
def cmd_disable(self):
apparmor.read_profiles()
for (program, profile) in self.get_next_to_profile():
output_name = profile if program is None else program
@@ -140,17 +133,13 @@ class aa_tools:
aaui.UI_Info(_('Disabling %s.') % output_name)
self.disable_profile(profile)
# FIXME: this should be a profile_remove function/method
# FIXME: should ensure profile is loaded before unloading
cmd_info = cmd([apparmor.parser, '-I%s' % apparmor.profile_dir, '-R', profile])
if cmd_info[0] != 0:
raise apparmor.AppArmorException(cmd_info[1])
self.unload_profile(profile)
def cmd_enforce(self):
apparmor.read_profiles()
for (program, profile) in self.get_next_to_profile():
apparmor.read_profiles()
output_name = profile if program is None else program
if not os.path.isfile(profile) or apparmor.is_skippable_file(profile):
@@ -159,16 +148,13 @@ class aa_tools:
apparmor.set_enforce(profile, program)
# FIXME: this should be a profile_reload function/method
cmd_info = cmd([apparmor.parser, '-I%s' % apparmor.profile_dir, '-r', profile])
if cmd_info[0] != 0:
raise apparmor.AppArmorException(cmd_info[1])
self.reload_profile(profile)
def cmd_complain(self):
apparmor.read_profiles()
for (program, profile) in self.get_next_to_profile():
apparmor.read_profiles()
output_name = profile if program is None else program
if not os.path.isfile(profile) or apparmor.is_skippable_file(profile):
@@ -177,16 +163,13 @@ class aa_tools:
apparmor.set_complain(profile, program)
# FIXME: this should be a profile_reload function/method
cmd_info = cmd([apparmor.parser, '-I%s' % apparmor.profile_dir, '-r', profile])
if cmd_info[0] != 0:
raise apparmor.AppArmorException(cmd_info[1])
self.reload_profile(profile)
def cmd_audit(self):
apparmor.read_profiles()
for (program, profile) in self.get_next_to_profile():
apparmor.read_profiles()
output_name = profile if program is None else program
if not os.path.isfile(profile) or apparmor.is_skippable_file(profile):
@@ -200,20 +183,16 @@ class aa_tools:
aaui.UI_Info(_('Removing audit mode from %s.') % output_name)
apparmor.change_profile_flags(profile, program, 'audit', not self.remove)
# FIXME: this should be a profile_reload function/method
cmd_info = cmd([apparmor.parser, '-I%s' % apparmor.profile_dir, '-r', profile])
if cmd_info[0] != 0:
raise apparmor.AppArmorException(cmd_info[1])
self.reload_profile(profile)
def cmd_autodep(self):
apparmor.read_profiles()
for (program, profile) in self.get_next_to_profile():
if not program:
aaui.UI_Info(_('Please pass an application to generate a profile for, not a profile itself - skipping %s.') % profile)
continue
apparmor.read_profiles()
apparmor.check_qualifiers(program)
if os.path.exists(apparmor.get_profile_filename(program)) and not self.force:
@@ -263,3 +242,16 @@ class aa_tools:
def disable_profile(self, filename):
apparmor.create_symlink('disable', filename)
def unload_profile(self, profile):
# FIXME: should ensure profile is loaded before unloading
cmd_info = cmd([apparmor.parser, '-I%s' % apparmor.profile_dir, '--base', apparmor.profile_dir, '-R', profile])
if cmd_info[0] != 0:
raise apparmor.AppArmorException(cmd_info[1])
def reload_profile(self, profile):
cmd_info = cmd([apparmor.parser, '-I%s' % apparmor.profile_dir, '--base', apparmor.profile_dir, '-r', profile])
if cmd_info[0] != 0:
raise apparmor.AppArmorException(cmd_info[1])

View File

@@ -86,29 +86,6 @@ class Test(unittest.TestCase):
for path in globs.keys():
self.assertEqual(apparmor.aa.glob_path_withext(path), globs[path], 'Unexpected glob generated for path: %s'%path)
def test_parse_event(self):
parser = apparmor.logparser.ReadLog('', '', '', '', '')
event = 'type=AVC msg=audit(1345027352.096:499): apparmor="ALLOWED" operation="rename_dest" parent=6974 profile="/usr/sbin/httpd2-prefork//vhost_foo" name=2F686F6D652F7777772F666F6F2E6261722E696E2F68747470646F63732F61707061726D6F722F696D616765732F746573742F696D61676520312E6A7067 pid=20143 comm="httpd2-prefork" requested_mask="wc" denied_mask="wc" fsuid=30 ouid=30'
parsed_event = parser.parse_event(event)
self.assertEqual(parsed_event['name'], '/home/www/foo.bar.in/httpdocs/apparmor/images/test/image 1.jpg', 'Incorrectly parsed/decoded name')
self.assertEqual(parsed_event['profile'], '/usr/sbin/httpd2-prefork//vhost_foo', 'Incorrectly parsed/decode profile name')
self.assertEqual(parsed_event['aamode'], 'PERMITTING')
self.assertEqual(parsed_event['request_mask'], set(['w', 'a', '::w', '::a']))
#print(parsed_event)
#event = 'type=AVC msg=audit(1322614912.304:857): apparmor="ALLOWED" operation="getattr" parent=16001 profile=74657374207370616365 name=74657374207370616365 pid=17011 comm="bash" requested_mask="r" denied_mask="r" fsuid=0 ouid=0'
#parsed_event = apparmor.aa.parse_event(event)
#print(parsed_event)
event = 'type=AVC msg=audit(1322614918.292:4376): apparmor="ALLOWED" operation="file_perm" parent=16001 profile=666F6F20626172 name="/home/foo/.bash_history" pid=17011 comm="bash" requested_mask="rw" denied_mask="rw" fsuid=0 ouid=1000'
parsed_event = parser.parse_event(event)
self.assertEqual(parsed_event['name'], '/home/foo/.bash_history', 'Incorrectly parsed/decoded name')
self.assertEqual(parsed_event['profile'], 'foo bar', 'Incorrectly parsed/decode profile name')
self.assertEqual(parsed_event['aamode'], 'PERMITTING')
self.assertEqual(parsed_event['request_mask'], set(['r', 'w', 'a','::r' , '::w', '::a']))
#print(parsed_event)
def test_modes_to_string(self):
for string in self.MODE_TEST.keys():

View File

@@ -1,5 +1,6 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
# Copyright (C) 2015 Christian Boltz <apparmor@cboltz.de>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@@ -12,8 +13,10 @@
#
# ----------------------------------------------------------------------
import unittest
import inspect
import os
import re
import sys
import apparmor.common
import apparmor.config
@@ -34,6 +37,10 @@ class Test(unittest.TestCase):
# print("Please press the Y button on the keyboard.")
# self.assertEqual(apparmor.common.readkey().lower(), 'y', 'Error reading key from shell!')
class AATest(unittest.TestCase):
tests = []
class AAParseTest(unittest.TestCase):
parse_function = None
@@ -44,6 +51,32 @@ class AAParseTest(unittest.TestCase):
'parse object %s returned "%s", expected "%s"' \
%(self.parse_function.__doc__, parsed.serialize(), rule))
def setup_all_loops(module_name):
'''call setup_tests_loop() for each class in module_name'''
for name, obj in inspect.getmembers(sys.modules[module_name]):
if inspect.isclass(obj):
if issubclass(obj, unittest.TestCase):
setup_tests_loop(obj)
def setup_tests_loop(test_class):
'''Create tests in test_class using test_class.tests and self._run_test()
test_class.tests should be tuples of (test_data, expected_results)
test_data and expected_results can be of any type as long as test_class._run_test()
know how to handle them.
A typical definition for _run_test() is:
def test_class._run_test(self, test_data, expected)
'''
for (i, (test_data, expected)) in enumerate(test_class.tests):
def stub_test(self, test_data=test_data, expected=expected):
self._run_test(test_data, expected)
stub_test.__doc__ = "test '%s'" % (test_data)
setattr(test_class, 'test_%d' % (i), stub_test)
def setup_regex_tests(test_class):
'''Create tests in test_class using test_class.tests and AAParseTest._test_parse_rule()
@@ -63,6 +96,13 @@ def write_file(directory, file, contents):
f.write(contents)
return path
def read_file(path):
'''read and return file contents'''
with open(path, 'r') as f:
return f.read()
if __name__ == "__main__":
#import sys;sys.argv = ['', 'Test.test_RegexParser']
unittest.main()

View File

@@ -1,7 +1,7 @@
#! /usr/bin/env python
# ------------------------------------------------------------------
#
# Copyright (C) 2011-2013 Canonical Ltd.
# Copyright (C) 2011-2015 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@@ -112,7 +112,8 @@ class T(unittest.TestCase):
# Copy everything into place
for d in ['easyprof/policygroups', 'easyprof/templates']:
shutil.copytree(os.path.join(topdir, d), os.path.join(self.tmpdir, os.path.basename(d)))
shutil.copytree(os.path.join(topdir, d),
os.path.join(self.tmpdir, os.path.basename(d)))
# Create a test template
self.test_template = "test-template"
@@ -167,6 +168,17 @@ TEMPLATES_DIR="%s/templates"
(self.options, self.args) = easyprof.parse_args(self.full_args + [self.binary])
# Now create some differently prefixed files in the include-dir
self.test_include_dir = os.path.join(self.tmpdir, 'include-dir')
os.mkdir(self.test_include_dir)
os.mkdir(os.path.join(self.test_include_dir, "templates"))
os.mkdir(os.path.join(self.test_include_dir, "policygroups"))
for d in ['policygroups', 'templates']:
for f in easyprof.get_directory_contents(os.path.join(
self.tmpdir, d)):
shutil.copy(f, os.path.join(self.test_include_dir, d,
"inc_%s" % os.path.basename(f)))
def tearDown(self):
'''Teardown for tests'''
if os.path.exists(self.tmpdir):
@@ -505,6 +517,53 @@ POLICYGROUPS_DIR="%s/templates"
self.assertTrue(os.path.exists(path), "Could not find '%s'" % path)
open(path).read()
def test_templates_list_include(self):
'''Test templates (list with --include-templates-dir)'''
args = self.full_args
args.append('--list-templates')
(self.options, self.args) = easyprof.parse_args(args)
easyp = easyprof.AppArmorEasyProfile(None, self.options)
orig_templates = easyp.get_templates()
args = self.full_args
args.append('--list-templates')
args.append('--include-templates-dir=%s' %
os.path.join(self.test_include_dir, 'templates'))
(self.options, self.args) = easyprof.parse_args(args)
easyp = easyprof.AppArmorEasyProfile(None, self.options)
inc_templates = easyp.get_templates()
self.assertTrue(len(inc_templates) == len(orig_templates) * 2,
"templates missing: %s" % inc_templates)
for i in inc_templates:
self.assertTrue(os.path.exists(i), "Could not find '%s'" % i)
def test_templates_show_include(self):
'''Test templates (show with --include-templates-dir)'''
files = []
for f in glob.glob("%s/templates/*" % self.test_include_dir):
files.append(f)
for f in files:
args = self.full_args
args += ['--show-template',
'--template', f,
'--include-templates-dir=%s' %
os.path.join(self.test_include_dir, 'templates')]
(self.options, self.args) = easyprof.parse_args(args)
easyp = easyprof.AppArmorEasyProfile(None, self.options)
path = os.path.join(easyp.dirs['templates_include'], f)
self.assertTrue(os.path.exists(path), "Could not find '%s'" % path)
open(path).read()
bn = os.path.basename(f)
# setup() copies everything in the include prefixed with inc_
self.assertTrue(bn.startswith('inc_'),
"'%s' does not start with 'inc_'" % bn)
#
# Policygroups tests
#
@@ -515,7 +574,7 @@ POLICYGROUPS_DIR="%s/templates"
(self.options, self.args) = easyprof.parse_args(args)
easyp = easyprof.AppArmorEasyProfile(None, self.options)
for i in easyp.get_templates():
for i in easyp.get_policy_groups():
self.assertTrue(os.path.exists(i), "Could not find '%s'" % i)
def test_policygroups_show(self):
@@ -526,7 +585,8 @@ POLICYGROUPS_DIR="%s/templates"
for f in files:
args = self.full_args
args += ['--show-template', '--template', f]
args += ['--show-policy-group',
'--policy-groups', os.path.basename(f)]
(self.options, self.args) = easyprof.parse_args(args)
easyp = easyprof.AppArmorEasyProfile(None, self.options)
@@ -534,6 +594,53 @@ POLICYGROUPS_DIR="%s/templates"
self.assertTrue(os.path.exists(path), "Could not find '%s'" % path)
open(path).read()
def test_policygroups_list_include(self):
'''Test policygroups (list with --include-policy-groups-dir)'''
args = self.full_args
args.append('--list-policy-groups')
(self.options, self.args) = easyprof.parse_args(args)
easyp = easyprof.AppArmorEasyProfile(None, self.options)
orig_policy_groups = easyp.get_policy_groups()
args = self.full_args
args.append('--list-policy-groups')
args.append('--include-policy-groups-dir=%s' %
os.path.join(self.test_include_dir, 'policygroups'))
(self.options, self.args) = easyprof.parse_args(args)
easyp = easyprof.AppArmorEasyProfile(None, self.options)
inc_policy_groups = easyp.get_policy_groups()
self.assertTrue(len(inc_policy_groups) == len(orig_policy_groups) * 2,
"policy_groups missing: %s" % inc_policy_groups)
for i in inc_policy_groups:
self.assertTrue(os.path.exists(i), "Could not find '%s'" % i)
def test_policygroups_show_include(self):
'''Test policygroups (show with --include-policy-groups-dir)'''
files = []
for f in glob.glob("%s/policygroups/*" % self.test_include_dir):
files.append(f)
for f in files:
args = self.full_args
args += ['--show-policy-group',
'--policy-groups', os.path.basename(f),
'--include-policy-groups-dir=%s' %
os.path.join(self.test_include_dir, 'policygroups')]
(self.options, self.args) = easyprof.parse_args(args)
easyp = easyprof.AppArmorEasyProfile(None, self.options)
path = os.path.join(easyp.dirs['policygroups_include'], f)
self.assertTrue(os.path.exists(path), "Could not find '%s'" % path)
open(path).read()
bn = os.path.basename(f)
# setup() copies everything in the include prefixed with inc_
self.assertTrue(bn.startswith('inc_'),
"'%s' does not start with 'inc_'" % bn)
#
# Manifest file argument tests
#

View File

@@ -1,7 +1,7 @@
#! /usr/bin/env python
# ------------------------------------------------------------------
#
# Copyright (C) 2014 Christian Boltz
# Copyright (C) 2014-2015 Christian Boltz
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
@@ -10,14 +10,25 @@
# ------------------------------------------------------------------
import unittest
from common_test import AATest, setup_all_loops
import os
import shutil
import tempfile
from common_test import write_file
from common_test import read_file, write_file
from apparmor.aa import check_for_apparmor
from apparmor.aa import check_for_apparmor, get_profile_flags, set_profile_flags, is_skippable_file, is_skippable_dir, parse_profile_start, separate_vars, store_list_var, write_header, serialize_parse_profile_start
from apparmor.common import AppArmorException, AppArmorBug
class AaTest_check_for_apparmor(unittest.TestCase):
class AaTestWithTempdir(AATest):
def setUp(self):
self.tmpdir = tempfile.mkdtemp(prefix='aa-py-')
def tearDown(self):
if os.path.exists(self.tmpdir):
shutil.rmtree(self.tmpdir)
class AaTest_check_for_apparmor(AaTestWithTempdir):
FILESYSTEMS_WITH_SECURITYFS = 'nodev\tdevtmpfs\nnodev\tsecurityfs\nnodev\tsockfs\n\text3\n\text2\n\text4'
FILESYSTEMS_WITHOUT_SECURITYFS = 'nodev\tdevtmpfs\nnodev\tsockfs\n\text3\n\text2\n\text4'
@@ -28,13 +39,6 @@ class AaTest_check_for_apparmor(unittest.TestCase):
MOUNTS_WITHOUT_SECURITYFS = ( 'proc /proc proc rw,relatime 0 0\n'
'/dev/sda1 / ext3 rw,noatime,data=ordered 0 0' )
def setUp(self):
self.tmpdir = tempfile.mkdtemp(prefix='aa-py-')
def tearDown(self):
if os.path.exists(self.tmpdir):
shutil.rmtree(self.tmpdir)
def test_check_for_apparmor_None_1(self):
filesystems = write_file(self.tmpdir, 'filesystems', self.FILESYSTEMS_WITHOUT_SECURITYFS)
mounts = write_file(self.tmpdir, 'mounts', self.MOUNTS_WITH_SECURITYFS)
@@ -70,6 +74,467 @@ class AaTest_check_for_apparmor(unittest.TestCase):
mounts = write_file(self.tmpdir, 'mounts', self.MOUNTS_WITH_SECURITYFS % self.tmpdir)
self.assertEqual('%s/security/apparmor' % self.tmpdir, check_for_apparmor(filesystems, mounts))
class AaTest_get_profile_flags(AaTestWithTempdir):
def _test_get_flags(self, profile_header, expected_flags):
file = write_file(self.tmpdir, 'profile', '%s {\n}\n' % profile_header)
flags = get_profile_flags(file, '/foo')
self.assertEqual(flags, expected_flags)
def test_get_flags_01(self):
self._test_get_flags('/foo', None)
def test_get_flags_02(self):
self._test_get_flags('/foo ( complain )', ' complain ')
def test_get_flags_04(self):
self._test_get_flags('/foo (complain)', 'complain')
def test_get_flags_05(self):
self._test_get_flags('/foo flags=(complain)', 'complain')
def test_get_flags_06(self):
self._test_get_flags('/foo flags=(complain, audit)', 'complain, audit')
def test_get_flags_invalid_01(self):
with self.assertRaises(AppArmorException):
self._test_get_flags('/foo ()', None)
def test_get_flags_invalid_02(self):
with self.assertRaises(AppArmorException):
self._test_get_flags('/foo flags=()', None)
def test_get_flags_invalid_03(self):
with self.assertRaises(AppArmorException):
self._test_get_flags('/foo ( )', ' ')
def test_get_flags_other_profile(self):
with self.assertRaises(AppArmorException):
self._test_get_flags('/no-such-profile flags=(complain)', 'complain')
class AaTest_set_profile_flags(AaTestWithTempdir):
def _test_set_flags(self, profile, old_flags, new_flags, whitespace='', comment='', more_rules='',
expected_flags='@-@-@', check_new_flags=True, profile_name='/foo'):
if old_flags:
old_flags = ' %s' % old_flags
if expected_flags == '@-@-@':
expected_flags = new_flags
if expected_flags:
expected_flags = ' flags=(%s)' % (expected_flags)
else:
expected_flags = ''
if comment:
comment = ' %s' % comment
dummy_profile_content = ' #include <abstractions/base>\n capability chown,\n /bar r,'
prof_template = '%s%s%s {%s\n%s\n%s\n}\n'
old_prof = prof_template % (whitespace, profile, old_flags, comment, more_rules, dummy_profile_content)
new_prof = prof_template % (whitespace, profile, expected_flags, comment, more_rules, dummy_profile_content)
self.file = write_file(self.tmpdir, 'profile', old_prof)
set_profile_flags(self.file, profile_name, new_flags)
if check_new_flags:
real_new_prof = read_file(self.file)
self.assertEqual(new_prof, real_new_prof)
# tests that actually don't change the flags
def test_set_flags_nochange_01(self):
self._test_set_flags('/foo', '', '')
def test_set_flags_nochange_02(self):
self._test_set_flags('/foo', '( complain )', ' complain ', whitespace=' ')
def test_set_flags_nochange_03(self):
self._test_set_flags('/foo', '(complain)', 'complain')
def test_set_flags_nochange_04(self):
self._test_set_flags('/foo', 'flags=(complain)', 'complain')
def test_set_flags_nochange_05(self):
self._test_set_flags('/foo', 'flags=(complain, audit)', 'complain, audit', whitespace=' ')
def test_set_flags_nochange_06(self):
self._test_set_flags('/foo', 'flags=(complain, audit)', 'complain, audit', whitespace=' ', comment='# a comment')
def test_set_flags_nochange_07(self):
self._test_set_flags('/foo', 'flags=(complain, audit)', 'complain, audit', whitespace=' ', more_rules=' # a comment\n#another comment')
def test_set_flags_nochange_08(self):
self._test_set_flags('profile /foo', 'flags=(complain)', 'complain')
def test_set_flags_nochange_09(self):
self._test_set_flags('profile xy /foo', 'flags=(complain)', 'complain', profile_name='xy')
def test_set_flags_nochange_10(self):
self._test_set_flags('profile "/foo bar"', 'flags=(complain)', 'complain', profile_name='/foo bar')
def test_set_flags_nochange_11(self):
self._test_set_flags('/foo', '(complain)', 'complain', profile_name=None)
#def test_set_flags_nochange_12(self):
# XXX changes the flags for the child profile (which happens to have the same profile name) to 'complain'
# self._test_set_flags('/foo', 'flags=(complain)', 'complain', more_rules=' profile /foo {\n}')
# tests that change the flags
def test_set_flags_01(self):
self._test_set_flags('/foo', '', 'audit')
def test_set_flags_02(self):
self._test_set_flags('/foo', '( complain )', 'audit ', whitespace=' ')
def test_set_flags_04(self):
self._test_set_flags('/foo', '(complain)', 'audit')
def test_set_flags_05(self):
self._test_set_flags('/foo', 'flags=(complain)', 'audit')
def test_set_flags_06(self):
self._test_set_flags('/foo', 'flags=(complain, audit)', None, whitespace=' ')
def test_set_flags_07(self):
self._test_set_flags('/foo', 'flags=(complain, audit)', '', expected_flags=None)
def test_set_flags_08(self):
self._test_set_flags('/foo', '( complain )', 'audit ', whitespace=' ', profile_name=None)
def test_set_flags_09(self):
self._test_set_flags('profile /foo', 'flags=(complain)', 'audit')
def test_set_flags_10(self):
self._test_set_flags('profile xy /foo', 'flags=(complain)', 'audit', profile_name='xy')
def test_set_flags_11(self):
self._test_set_flags('profile "/foo bar"', 'flags=(complain)', 'audit', profile_name='/foo bar')
def test_set_flags_12(self):
self._test_set_flags('profile xy "/foo bar"', 'flags=(complain)', 'audit', profile_name='xy')
# XXX regex_hat_flag in set_profile_flags() is totally broken - it matches for ' ' and ' X ', but doesn't match for ' ^foo {'
# oh, it matches on a line like 'dbus' and changes it to 'dbus flags=(...)' if there's no leading whitespace (and no comma)
#def test_set_flags_hat_01(self):
# self._test_set_flags(' ^hat', '', 'audit')
def test_set_flags_invalid_01(self):
with self.assertRaises(AppArmorBug):
self._test_set_flags('/foo', '()', None, check_new_flags=False)
def test_set_flags_invalid_02(self):
with self.assertRaises(AppArmorBug):
self._test_set_flags('/foo', 'flags=()', None, check_new_flags=False)
def test_set_flags_invalid_03(self):
with self.assertRaises(AppArmorException):
self._test_set_flags('/foo', '( )', '', check_new_flags=False)
def test_set_flags_invalid_04(self):
with self.assertRaises(AppArmorBug):
self._test_set_flags('/foo', 'flags=(complain, audit)', ' ', check_new_flags=False) # whitespace-only newflags
def test_set_flags_other_profile(self):
# test behaviour if the file doesn't contain the specified /foo profile
orig_prof = '/no-such-profile flags=(complain) {\n}'
self.file = write_file(self.tmpdir, 'profile', orig_prof)
with self.assertRaises(AppArmorBug):
set_profile_flags(self.file, '/foo', 'audit')
# the file should not be changed
real_new_prof = read_file(self.file)
self.assertEqual(orig_prof, real_new_prof)
def test_set_flags_no_profile_found(self):
# test behaviour if the file doesn't contain any profile
orig_prof = '# /comment flags=(complain) {\n# }'
self.file = write_file(self.tmpdir, 'profile', orig_prof)
with self.assertRaises(AppArmorBug):
set_profile_flags(self.file, None, 'audit')
# the file should not be changed
real_new_prof = read_file(self.file)
self.assertEqual(orig_prof, real_new_prof)
def test_set_flags_file_not_found(self):
with self.assertRaises(IOError):
set_profile_flags('%s/file-not-found' % self.tmpdir, '/foo', 'audit')
class AaTest_is_skippable_file(AATest):
def test_not_skippable_01(self):
self.assertFalse(is_skippable_file('bin.ping'))
def test_not_skippable_02(self):
self.assertFalse(is_skippable_file('usr.lib.dovecot.anvil'))
def test_not_skippable_03(self):
self.assertFalse(is_skippable_file('bin.~ping'))
def test_not_skippable_04(self):
self.assertFalse(is_skippable_file('bin.rpmsave.ping'))
def test_not_skippable_05(self):
# normally is_skippable_file should be called without directory, but it shouldn't hurt too much
self.assertFalse(is_skippable_file('/etc/apparmor.d/bin.ping'))
def test_not_skippable_06(self):
self.assertFalse(is_skippable_file('bin.pingrej'))
def test_skippable_01(self):
self.assertTrue(is_skippable_file('bin.ping.dpkg-new'))
def test_skippable_02(self):
self.assertTrue(is_skippable_file('bin.ping.dpkg-old'))
def test_skippable_03(self):
self.assertTrue(is_skippable_file('bin.ping..dpkg-dist'))
def test_skippable_04(self):
self.assertTrue(is_skippable_file('bin.ping..dpkg-bak'))
def test_skippable_05(self):
self.assertTrue(is_skippable_file('bin.ping.rpmnew'))
def test_skippable_06(self):
self.assertTrue(is_skippable_file('bin.ping.rpmsave'))
def test_skippable_07(self):
self.assertTrue(is_skippable_file('bin.ping.orig'))
def test_skippable_08(self):
self.assertTrue(is_skippable_file('bin.ping.rej'))
def test_skippable_09(self):
self.assertTrue(is_skippable_file('bin.ping~'))
def test_skippable_10(self):
self.assertTrue(is_skippable_file('.bin.ping'))
def test_skippable_11(self):
self.assertTrue(is_skippable_file('')) # empty filename
def test_skippable_12(self):
self.assertTrue(is_skippable_file('/etc/apparmor.d/')) # directory without filename
def test_skippable_13(self):
self.assertTrue(is_skippable_file('README'))
class AaTest_is_skippable_dir(AATest):
tests = [
('disable', True),
('cache', True),
('lxc', True),
('force-complain', True),
('/etc/apparmor.d/cache', True),
('/etc/apparmor.d/lxc/', True),
('dont_disable', False),
('/etc/apparmor.d/cache_foo', False),
('abstractions', False),
('apache2.d', False),
('/etc/apparmor.d/apache2.d', False),
('local', False),
('/etc/apparmor.d/local/', False),
('tunables', False),
('/etc/apparmor.d/tunables', False),
('/etc/apparmor.d/tunables/multiarch.d', False),
('/etc/apparmor.d/tunables/xdg-user-dirs.d', False),
('/etc/apparmor.d/tunables/home.d', False),
('/etc/apparmor.d/abstractions', False),
('/etc/apparmor.d/abstractions/ubuntu-browsers.d', False),
('/etc/apparmor.d/abstractions/apparmor_api', False),
]
def _run_test(self, params, expected):
self.assertEqual(is_skippable_dir(params), expected)
class AaTest_parse_profile_start(AATest):
def _parse(self, line, profile, hat):
return parse_profile_start(line, 'somefile', 1, profile, hat)
# (profile, hat, flags, in_contained_hat, pps_set_profile, pps_set_hat_external)
def test_parse_profile_start_01(self):
result = self._parse('/foo {', None, None)
expected = ('/foo', '/foo', None, None, False, False, False)
self.assertEqual(result, expected)
def test_parse_profile_start_02(self):
result = self._parse('/foo (complain) {', None, None)
expected = ('/foo', '/foo', None, 'complain', False, False, False)
self.assertEqual(result, expected)
def test_parse_profile_start_03(self):
result = self._parse('profile foo /foo {', None, None) # named profile
expected = ('foo', 'foo', '/foo', None, False, False, False)
self.assertEqual(result, expected)
def test_parse_profile_start_04(self):
result = self._parse('profile /foo {', '/bar', '/bar') # child profile
expected = ('/bar', '/foo', None, None, True, True, False)
self.assertEqual(result, expected)
def test_parse_profile_start_05(self):
result = self._parse('/foo//bar {', None, None) # external hat
expected = ('/foo', 'bar', None, None, False, False, True)
self.assertEqual(result, expected)
def test_parse_profile_start_06(self):
result = self._parse('profile "/foo" (complain) {', None, None)
expected = ('/foo', '/foo', None, 'complain', False, False, False)
self.assertEqual(result, expected)
def test_parse_profile_start_invalid_01(self):
with self.assertRaises(AppArmorException):
self._parse('/foo {', '/bar', '/bar') # child profile without profile keyword
def test_parse_profile_start_invalid_02(self):
with self.assertRaises(AppArmorBug):
self._parse('xy', '/bar', '/bar') # not a profile start
class AaTest_separate_vars(AATest):
tests = [
('' , set() ),
(' ' , set() ),
(' foo bar' , {'foo', 'bar' }),
('foo " ' , {'foo' }), # XXX " is ignored
(' " foo ' , {' "', 'foo' }), # XXX really?
(' foo bar ' , {'foo', 'bar' }),
(' foo bar # comment' , {'foo', 'bar', 'comment' }), # XXX should comments be stripped?
('foo' , {'foo' }),
('"foo" "bar baz"' , {'foo', 'bar baz' }),
('foo "bar baz" xy' , {'foo', 'bar baz', 'xy' }),
('foo "bar baz ' , {'foo', 'bar', 'baz' }), # half-quoted
(' " foo" bar' , {' foo', 'bar' }),
]
def _run_test(self, params, expected):
result = separate_vars(params)
self.assertEqual(result, expected)
class AaTest_store_list_var(AATest):
tests = [
# old var value operation expected (False for exception)
([ {} , 'foo' , '=' ], {'foo'} ), # set
([ {} , 'foo bar' , '=' ], {'foo', 'bar'} ), # set multi
([ {'@{var}': {'foo'}} , 'bar' , '=' ], False ), # redefine var
([ {} , 'bar' , '+=' ], False ), # add to undefined var
([ {'@{var}': {'foo'}} , 'bar' , '+=' ], {'foo', 'bar'} ), # add
([ {'@{var}': {'foo'}} , 'bar baz' , '+=' ], {'foo', 'bar', 'baz'} ), # add multi
([ {'@{var}': {'foo', 'xy'}} , 'bar baz' , '+=' ], {'foo', 'xy', 'bar', 'baz'} ), # add multi to multi
([ {} , 'foo' , '-=' ], False ), # unknown operation
]
def _run_test(self, params, expected):
var = params[0]
value = params[1]
operation = params[2]
if not expected:
with self.assertRaises(AppArmorException):
store_list_var(var, '@{var}', value, operation, 'somefile')
return
# dumy value that must not be changed
var['@{foo}'] = {'one', 'two'}
exp_var = {
'@{foo}': {'one', 'two'},
'@{var}': expected,
}
store_list_var(var, '@{var}', value, operation, 'somefile')
self.assertEqual(var.keys(), exp_var.keys())
for key in exp_var:
self.assertEqual(var[key], exp_var[key])
class AaTest_write_header(AATest):
tests = [
# name embedded_hat write_flags depth flags attachment prof.keyw. comment expected
(['/foo', False, True, 1, 'complain', None, None, None ], ' /foo flags=(complain) {'),
(['/foo', True, True, 1, 'complain', None, None, None ], ' profile /foo flags=(complain) {'),
(['/foo sp', False, False, 2, 'complain', None, None, None ], ' "/foo sp" {'),
(['/foo' ,False, False, 2, 'complain', None, None, None ], ' /foo {'),
(['/foo', True, False, 2, 'complain', None, None, None ], ' profile /foo {'),
(['/foo', False, True, 0, None, None, None, None ], '/foo {'),
(['/foo', True, True, 0, None, None, None, None ], 'profile /foo {'),
(['/foo', False, False, 0, None, None, None, None ], '/foo {'),
(['/foo', True, False, 0, None, None, None, None ], 'profile /foo {'),
(['bar', False, True, 1, 'complain', None, None, None ], ' profile bar flags=(complain) {'),
(['bar', False, True, 1, 'complain', '/foo', None, None ], ' profile bar /foo flags=(complain) {'),
(['bar', True, True, 1, 'complain', '/foo', None, None ], ' profile bar /foo flags=(complain) {'),
(['bar baz', False, True, 1, None, '/foo', None, None ], ' profile "bar baz" /foo {'),
(['bar', True, True, 1, None, '/foo', None, None ], ' profile bar /foo {'),
(['bar baz', False, True, 1, 'complain', '/foo sp', None, None ], ' profile "bar baz" "/foo sp" flags=(complain) {'),
(['^foo', False, True, 1, 'complain', None, None, None ], ' profile ^foo flags=(complain) {'),
(['^foo', True, True, 1, 'complain', None, None, None ], ' ^foo flags=(complain) {'),
(['^foo', True, True, 1.5, 'complain', None, None, None ], ' ^foo flags=(complain) {'),
(['^foo', True, True, 1.3, 'complain', None, None, None ], ' ^foo flags=(complain) {'),
(['/foo', False, True, 1, 'complain', None, 'profile', None ], ' profile /foo flags=(complain) {'),
(['/foo', True, True, 1, 'complain', None, 'profile', None ], ' profile /foo flags=(complain) {'),
(['/foo', False, True, 1, 'complain', None, None, '# x' ], ' /foo flags=(complain) { # x'),
(['/foo', True, True, 1, None, None, None, '# x' ], ' profile /foo { # x'),
(['/foo', False, True, 1, None, None, 'profile', '# x' ], ' profile /foo { # x'),
(['/foo', True, True, 1, 'complain', None, 'profile', '# x' ], ' profile /foo flags=(complain) { # x'),
]
def _run_test(self, params, expected):
name = params[0]
embedded_hat = params[1]
write_flags = params[2]
depth = params[3]
prof_data = { 'flags': params[4], 'attachment': params[5], 'profile_keyword': params[6], 'header_comment': params[7] }
result = write_header(prof_data, depth, name, embedded_hat, write_flags)
self.assertEqual(result, [expected])
class AaTest_serialize_parse_profile_start(AATest):
def _parse(self, line, profile, hat, prof_data_profile, prof_data_external):
# 'correct' is always True in the code that uses serialize_parse_profile_start() (set some lines above the function call)
return serialize_parse_profile_start(line, 'somefile', 1, profile, hat, prof_data_profile, prof_data_external, True)
def test_serialize_parse_profile_start_01(self):
result = self._parse('/foo {', None, None, False, False)
expected = ('/foo', '/foo', None, None, False, True)
self.assertEqual(result, expected)
def test_serialize_parse_profile_start_02(self):
result = self._parse('/foo (complain) {', None, None, False, False)
expected = ('/foo', '/foo', None, 'complain', False, True)
self.assertEqual(result, expected)
def test_serialize_parse_profile_start_03(self):
result = self._parse('profile foo /foo {', None, None, False, False) # named profile
expected = ('foo', 'foo', '/foo', None, False, True)
self.assertEqual(result, expected)
def test_serialize_parse_profile_start_04(self):
result = self._parse('profile /foo {', '/bar', '/bar', False, False) # child profile
expected = ('/bar', '/foo', None, None, True, True)
self.assertEqual(result, expected)
def test_serialize_parse_profile_start_05(self):
result = self._parse('/foo//bar {', None, None, False, False) # external hat
expected = ('/foo', 'bar', None, None, False, False) # note correct == False here
self.assertEqual(result, expected)
def test_serialize_parse_profile_start_06(self):
result = self._parse('profile "/foo" (complain) {', None, None, False, False)
expected = ('/foo', '/foo', None, 'complain', False, True)
self.assertEqual(result, expected)
def test_serialize_parse_profile_start_07(self):
result = self._parse('/foo {', None, None, True, False)
expected = ('/foo', '/foo', None, None, False, True)
self.assertEqual(result, expected)
def test_serialize_parse_profile_start_08(self):
result = self._parse('/foo {', None, None, False, True)
expected = ('/foo', '/foo', None, None, False, True)
self.assertEqual(result, expected)
def test_serialize_parse_profile_start_09(self):
result = self._parse('/foo {', None, None, True, True)
expected = ('/foo', '/foo', None, None, False, True)
self.assertEqual(result, expected)
def test_serialize_parse_profile_start_10(self):
result = self._parse('profile /foo {', '/bar', '/bar', True, False) # child profile
expected = ('/bar', '/foo', None, None, True, True)
self.assertEqual(result, expected)
def test_serialize_parse_profile_start_11(self):
result = self._parse('profile /foo {', '/bar', '/bar', False, True) # child profile
expected = ('/bar', '/foo', None, None, True, True)
self.assertEqual(result, expected)
def test_serialize_parse_profile_start_12(self):
result = self._parse('profile /foo {', '/bar', '/bar', True, True) # child profile
expected = ('/bar', '/foo', None, None, True, True)
self.assertEqual(result, expected)
class AaTestInvalid_serialize_parse_profile_start(AATest):
tests = [
# line profile hat p_d_profile p_d_external expected
(['/foo {', '/bar', '/bar', False, False ], AppArmorException), # child profile without 'profile' keyword
(['profile /foo {', '/bar', '/xy', False, False ], AppArmorException), # already inside a child profile - nesting level reached
(['/ext//hat {', '/bar', '/bar', True, True ], AppArmorException), # external hat inside a profile
(['/ext//hat {', '/bar', '/bar', True, False ], AppArmorException), # external hat inside a profile
(['xy', '/bar', '/bar', False, False ], AppArmorBug ), # not a profile start
]
def _run_test(self, params, expected):
line = params[0]
profile = params[1]
hat = params[2]
prof_data_profile = params[3]
prof_data_external = params[4]
with self.assertRaises(expected):
# 'correct' is always True in the code that uses serialize_parse_profile_start() (set some lines above the function call)
serialize_parse_profile_start(line, 'somefile', 1, profile, hat, prof_data_profile, prof_data_external, True)
setup_all_loops(__name__)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@@ -0,0 +1,45 @@
#! /usr/bin/env python
# ------------------------------------------------------------------
#
# Copyright (C) 2015 Christian Boltz <apparmor@cboltz.de>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License published by the Free Software Foundation.
#
# ------------------------------------------------------------------
import unittest
from common_test import AATest, setup_all_loops
class TestFoo(AATest):
tests = [
(0, 0 ),
(42, 42),
]
def _run_test(self, params, expected):
self.assertEqual(params, expected)
class TestBar(AATest):
tests = [
('a', 'foo'),
('b', 'bar'),
('c', 'baz'),
]
def _run_test(self, params, expected):
self.assertNotEqual(params, expected)
def testAdditionalBarTest(self):
self.assertEqual(1, 1)
class TestBaz(AATest):
def test_Baz_only_one_test(self):
self.assertEqual("baz", "baz")
setup_all_loops(__name__)
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@@ -0,0 +1,99 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
# Copyright (C) 2015 Christian Boltz <apparmor@cboltz.de>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# 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.
#
# ----------------------------------------------------------------------
import unittest
from apparmor.logparser import ReadLog
class TestParseEvent(unittest.TestCase):
def setUp(self):
self.parser = ReadLog('', '', '', '', '')
def test_parse_event_audit_1(self):
event = 'type=AVC msg=audit(1345027352.096:499): apparmor="ALLOWED" operation="rename_dest" parent=6974 profile="/usr/sbin/httpd2-prefork//vhost_foo" name=2F686F6D652F7777772F666F6F2E6261722E696E2F68747470646F63732F61707061726D6F722F696D616765732F746573742F696D61676520312E6A7067 pid=20143 comm="httpd2-prefork" requested_mask="wc" denied_mask="wc" fsuid=30 ouid=30'
parsed_event = self.parser.parse_event(event)
self.assertEqual(parsed_event['name'], '/home/www/foo.bar.in/httpdocs/apparmor/images/test/image 1.jpg')
self.assertEqual(parsed_event['profile'], '/usr/sbin/httpd2-prefork//vhost_foo')
self.assertEqual(parsed_event['aamode'], 'PERMITTING')
self.assertEqual(parsed_event['request_mask'], 'wc')
self.assertIsNotNone(ReadLog.RE_LOG_v2_6_audit.search(event))
self.assertIsNone(ReadLog.RE_LOG_v2_6_syslog.search(event))
def test_parse_event_audit_2(self):
event = 'type=AVC msg=audit(1322614918.292:4376): apparmor="ALLOWED" operation="file_perm" parent=16001 profile=666F6F20626172 name="/home/foo/.bash_history" pid=17011 comm="bash" requested_mask="rw" denied_mask="rw" fsuid=0 ouid=1000'
parsed_event = self.parser.parse_event(event)
self.assertEqual(parsed_event['name'], '/home/foo/.bash_history')
self.assertEqual(parsed_event['profile'], 'foo bar')
self.assertEqual(parsed_event['aamode'], 'PERMITTING')
self.assertEqual(parsed_event['request_mask'], 'rw')
self.assertIsNotNone(ReadLog.RE_LOG_v2_6_audit.search(event))
self.assertIsNone(ReadLog.RE_LOG_v2_6_syslog.search(event))
def test_parse_event_syslog_1(self):
# from https://bugs.launchpad.net/apparmor/+bug/1399027
event = '2014-06-09T20:37:28.975070+02:00 geeko kernel: [21028.143765] type=1400 audit(1402339048.973:1421): apparmor="ALLOWED" operation="open" profile="/home/cb/linuxtag/apparmor/scripts/hello" name="/dev/tty" pid=14335 comm="hello" requested_mask="rw" denied_mask="rw" fsuid=1000 ouid=0'
parsed_event = self.parser.parse_event(event)
self.assertEqual(parsed_event['name'], '/dev/tty')
self.assertEqual(parsed_event['profile'], '/home/cb/linuxtag/apparmor/scripts/hello')
self.assertEqual(parsed_event['aamode'], 'PERMITTING')
self.assertEqual(parsed_event['request_mask'], 'rw')
self.assertIsNone(ReadLog.RE_LOG_v2_6_audit.search(event))
self.assertIsNotNone(ReadLog.RE_LOG_v2_6_syslog.search(event))
def test_parse_event_syslog_2(self):
# from https://bugs.launchpad.net/apparmor/+bug/1399027
event = 'Dec 7 13:18:59 rosa kernel: audit: type=1400 audit(1417954745.397:82): apparmor="ALLOWED" operation="open" profile="/home/simi/bin/aa-test" name="/usr/bin/" pid=3231 comm="ls" requested_mask="r" denied_mask="r" fsuid=1000 ouid=0'
parsed_event = self.parser.parse_event(event)
self.assertEqual(parsed_event['name'], '/usr/bin/')
self.assertEqual(parsed_event['profile'], '/home/simi/bin/aa-test')
self.assertEqual(parsed_event['aamode'], 'PERMITTING')
self.assertEqual(parsed_event['request_mask'], 'r')
self.assertIsNone(ReadLog.RE_LOG_v2_6_audit.search(event))
self.assertIsNotNone(ReadLog.RE_LOG_v2_6_syslog.search(event))
def test_parse_disconnected_path(self):
# from https://bugzilla.opensuse.org/show_bug.cgi?id=918787
event = 'type=AVC msg=audit(1424425690.883:716630): apparmor="ALLOWED" operation="file_mmap" info="Failed name lookup - disconnected path" error=-13 profile="/sbin/klogd" name="var/run/nscd/passwd" pid=25333 comm="id" requested_mask="r" denied_mask="r" fsuid=1002 ouid=0'
parsed_event = self.parser.parse_event(event)
self.assertEqual(parsed_event, {
'aamode': 'ERROR', # aamode for disconnected paths overridden aamode in parse_event()
'active_hat': None,
'attr': None,
'denied_mask': 'r',
'error_code': 13,
'info': 'Failed name lookup - disconnected path',
'magic_token': 0,
'name': 'var/run/nscd/passwd',
'name2': None,
'operation': 'file_mmap',
'parent': 0,
'pid': 25333,
'profile': '/sbin/klogd',
'request_mask': 'r',
'resource': 'Failed name lookup - disconnected path',
'task': 0,
'time': 1424425690
})
self.assertIsNotNone(ReadLog.RE_LOG_v2_6_audit.search(event))
self.assertIsNone(ReadLog.RE_LOG_v2_6_syslog.search(event))
if __name__ == "__main__":
unittest.main(verbosity=2)

View File

@@ -11,9 +11,41 @@
import apparmor.aa as aa
import unittest
from common_test import AATest, setup_all_loops
from apparmor.common import AppArmorBug
from apparmor.regex import strip_quotes, parse_profile_start_line, RE_PROFILE_START
class AARegexHasComma(unittest.TestCase):
class AARegexTest(AATest):
def _run_test(self, params, expected):
return _regex_test(self, params, expected)
class AANamedRegexTest(AATest):
def _run_test(self, line, expected):
'''Run a line through self.regex.search() and verify the result
Keyword arguments:
line -- the line to search
expected -- False if the search isn't expected to match or, if the search
is expected to match, a tuple of expected match groups.
'''
matches = self.regex.search(line)
if not expected:
self.assertFalse(matches)
return
self.assertTrue(matches)
for exp in expected:
match = matches.group(exp)
if match:
match = match
self.assertEqual(match, expected[exp], 'Group %s mismatch in rule %s' % (exp,line))
class AARegexHasComma(AATest):
'''Tests for apparmor.aa.RE_RULE_HAS_COMMA'''
def _check(self, line, expected=True):
@@ -93,7 +125,7 @@ def setup_has_comma_testcases():
setattr(AARegexHasComma, 'test_comma_%d' % (i), stub_test_comma)
setattr(AARegexHasComma, 'test_no_comma_%d' % (i), stub_test_no_comma)
class AARegexSplitComment(unittest.TestCase):
class AARegexSplitComment(AATest):
'''Tests for RE_HAS_COMMENT_SPLIT'''
def _check(self, line, expected, comment=None, not_comment=None):
@@ -141,7 +173,7 @@ def setup_split_comment_testcases():
setattr(AARegexSplitComment, 'test_split_comment_%d' % (i), stub_test)
def regex_test(self, line, expected):
def _regex_test(self, line, expected):
'''Run a line through self.regex.search() and verify the result
Keyword arguments:
@@ -165,23 +197,10 @@ def regex_test(self, line, expected):
self.assertEqual(group, expected[i], 'Group %d mismatch in rule %s' % (i,line))
def setup_regex_tests(test_class):
'''Create tests in test_class using test_class.tests and regex_tests()
test_class.tests should be tuples of (line, expected_results) where
expected_results is False if test_class.regex.search(line) should not
match. If the search should match, expected_results should be a tuple of
the expected groups, with all of the strings stripped.
'''
for (i, (line, expected)) in enumerate(test_class.tests):
def stub_test(self, line=line, expected=expected):
regex_test(self, line, expected)
stub_test.__doc__ = "test '%s'" % (line)
setattr(test_class, 'test_%d' % (i), stub_test)
class AARegexCapability(unittest.TestCase):
class AARegexCapability(AARegexTest):
'''Tests for RE_PROFILE_CAP'''
def setUp(self):
@@ -196,7 +215,7 @@ class AARegexCapability(unittest.TestCase):
]
class AARegexPath(unittest.TestCase):
class AARegexPath(AARegexTest):
'''Tests for RE_PROFILE_PATH_ENTRY'''
def setUp(self):
@@ -215,7 +234,7 @@ class AARegexPath(unittest.TestCase):
]
class AARegexBareFile(unittest.TestCase):
class AARegexBareFile(AARegexTest):
'''Tests for RE_PROFILE_BARE_FILE_ENTRY'''
def setUp(self):
@@ -233,7 +252,7 @@ class AARegexBareFile(unittest.TestCase):
]
class AARegexDbus(unittest.TestCase):
class AARegexDbus(AARegexTest):
'''Tests for RE_PROFILE_DBUS'''
def setUp(self):
@@ -249,7 +268,7 @@ class AARegexDbus(unittest.TestCase):
(' audit dbusdriver,', False),
]
class AARegexMount(unittest.TestCase):
class AARegexMount(AARegexTest):
'''Tests for RE_PROFILE_MOUNT'''
def setUp(self):
@@ -273,7 +292,7 @@ class AARegexMount(unittest.TestCase):
class AARegexSignal(unittest.TestCase):
class AARegexSignal(AARegexTest):
'''Tests for RE_PROFILE_SIGNAL'''
def setUp(self):
@@ -299,7 +318,7 @@ class AARegexSignal(unittest.TestCase):
]
class AARegexPtrace(unittest.TestCase):
class AARegexPtrace(AARegexTest):
'''Tests for RE_PROFILE_PTRACE'''
def setUp(self):
@@ -321,7 +340,7 @@ class AARegexPtrace(unittest.TestCase):
]
class AARegexPivotRoot(unittest.TestCase):
class AARegexPivotRoot(AARegexTest):
'''Tests for RE_PROFILE_PIVOT_ROOT'''
def setUp(self):
@@ -348,7 +367,7 @@ class AARegexPivotRoot(unittest.TestCase):
('pivot_rootbeer /new, # comment', False),
]
class AARegexUnix(unittest.TestCase):
class AARegexUnix(AARegexTest):
'''Tests for RE_PROFILE_UNIX'''
def setUp(self):
@@ -373,22 +392,102 @@ class AARegexUnix(unittest.TestCase):
('deny unixlike,', False),
]
if __name__ == '__main__':
verbosity = 2
class AANamedRegexProfileStart_2(AANamedRegexTest):
'''Tests for RE_PROFILE_START'''
def setUp(self):
self.regex = RE_PROFILE_START
tests = [
('/bin/foo ', False), # no '{'
('/bin/foo /bin/bar', False), # missing 'profile' keyword
('profile {', False), # no attachment
(' profile foo bar /foo {', False), # missing quotes around "foo bar"
(' /foo {', { 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': None, 'comment': None }),
(' "/foo" {', { 'plainprofile': '"/foo"', 'namedprofile': None, 'attachment': None, 'flags': None, 'comment': None }),
(' profile /foo {', { 'plainprofile': None, 'namedprofile': '/foo', 'attachment': None, 'flags': None, 'comment': None }),
(' profile "/foo" {', { 'plainprofile': None, 'namedprofile': '"/foo"', 'attachment': None, 'flags': None, 'comment': None }),
(' profile foo /foo {', { 'plainprofile': None, 'namedprofile': 'foo', 'attachment': '/foo', 'flags': None, 'comment': None }),
(' profile foo /foo (audit) {', { 'plainprofile': None, 'namedprofile': 'foo', 'attachment': '/foo', 'flags': 'audit', 'comment': None }),
(' profile "foo" "/foo" {', { 'plainprofile': None, 'namedprofile': '"foo"', 'attachment': '"/foo"', 'flags': None, 'comment': None }),
(' profile "foo bar" /foo {', { 'plainprofile': None, 'namedprofile': '"foo bar"', 'attachment': '/foo', 'flags': None, 'comment': None }),
(' /foo (complain) {', { 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': 'complain', 'comment': None }),
(' /foo flags=(complain) {', { 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': 'complain', 'comment': None }),
(' /foo (complain) { # x', { 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': 'complain', 'comment': '# x'}),
(' /foo {', { 'plainprofile': '/foo', 'namedprofile': None, 'leadingspace': ' ' }),
('/foo {', { 'plainprofile': '/foo', 'namedprofile': None, 'leadingspace': '' }),
(' profile foo {', { 'plainprofile': None, 'namedprofile': 'foo', 'leadingspace': ' ' }),
('profile foo {', { 'plainprofile': None, 'namedprofile': 'foo', 'leadingspace': '' }),
]
class Test_parse_profile_start_line(AATest):
tests = [
(' /foo {', { 'profile': '/foo', 'profile_keyword': False, 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': None, 'comment': None }),
(' "/foo" {', { 'profile': '/foo', 'profile_keyword': False, 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': None, 'comment': None }),
(' profile /foo {', { 'profile': '/foo', 'profile_keyword': True, 'plainprofile': None, 'namedprofile': '/foo', 'attachment': None, 'flags': None, 'comment': None }),
(' profile "/foo" {', { 'profile': '/foo', 'profile_keyword': True, 'plainprofile': None, 'namedprofile': '/foo', 'attachment': None, 'flags': None, 'comment': None }),
(' profile foo /foo {', { 'profile': 'foo', 'profile_keyword': True, 'plainprofile': None, 'namedprofile': 'foo', 'attachment': '/foo', 'flags': None, 'comment': None }),
(' profile foo /foo (audit) {', { 'profile': 'foo', 'profile_keyword': True, 'plainprofile': None, 'namedprofile': 'foo', 'attachment': '/foo', 'flags': 'audit', 'comment': None }),
(' profile "foo" "/foo" {', { 'profile': 'foo', 'profile_keyword': True, 'plainprofile': None, 'namedprofile': 'foo', 'attachment': '/foo', 'flags': None, 'comment': None }),
(' profile "foo bar" /foo {', { 'profile': 'foo bar', 'profile_keyword': True, 'plainprofile': None, 'namedprofile': 'foo bar','attachment': '/foo', 'flags': None, 'comment': None }),
(' /foo (complain) {', { 'profile': '/foo', 'profile_keyword': False, 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': 'complain', 'comment': None }),
(' /foo flags=(complain) {', { 'profile': '/foo', 'profile_keyword': False, 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': 'complain', 'comment': None }),
(' /foo (complain) { # x', { 'profile': '/foo', 'profile_keyword': False, 'plainprofile': '/foo', 'namedprofile': None, 'attachment': None, 'flags': 'complain', 'comment': '# x'}),
(' /foo {', { 'profile': '/foo', 'plainprofile': '/foo', 'namedprofile': None, 'leadingspace': ' ' }),
('/foo {', { 'profile': '/foo', 'plainprofile': '/foo', 'namedprofile': None, 'leadingspace': None }),
(' profile foo {', { 'profile': 'foo', 'plainprofile': None, 'namedprofile': 'foo', 'leadingspace': ' ' }),
('profile foo {', { 'profile': 'foo', 'plainprofile': None, 'namedprofile': 'foo', 'leadingspace': None }),
]
def _run_test(self, line, expected):
matches = parse_profile_start_line(line, 'somefile')
self.assertTrue(matches)
for exp in expected:
self.assertEqual(matches[exp], expected[exp], 'Group %s mismatch in rule %s' % (exp,line))
class TestInvalid_parse_profile_start_line(AATest):
tests = [
('/bin/foo ', False), # no '{'
('/bin/foo /bin/bar', False), # missing 'profile' keyword
('profile {', False), # no attachment
(' profile foo bar /foo {', False), # missing quotes around "foo bar"
]
def _run_test(self, line, expected):
with self.assertRaises(AppArmorBug):
parse_profile_start_line(line, 'somefile')
class TestStripQuotes(AATest):
def test_strip_quotes_01(self):
self.assertEqual('foo', strip_quotes('foo'))
def test_strip_quotes_02(self):
self.assertEqual('foo', strip_quotes('"foo"'))
def test_strip_quotes_03(self):
self.assertEqual('"foo', strip_quotes('"foo'))
def test_strip_quotes_04(self):
self.assertEqual('foo"', strip_quotes('foo"'))
def test_strip_quotes_05(self):
self.assertEqual('', strip_quotes('""'))
def test_strip_quotes_06(self):
self.assertEqual('foo"bar', strip_quotes('foo"bar'))
def test_strip_quotes_07(self):
self.assertEqual('foo"bar', strip_quotes('"foo"bar"'))
def test_strip_quotes_08(self):
self.assertEqual('"""foo"bar"""', strip_quotes('""""foo"bar""""'))
setup_all_loops(__name__)
if __name__ == '__main__':
# these two are not converted to a tests[] loop yet
setup_has_comma_testcases()
setup_split_comment_testcases()
test_suite = unittest.TestSuite()
test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AARegexHasComma))
test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AARegexSplitComment))
for tests in (AARegexCapability, AARegexPath, AARegexBareFile,
AARegexDbus, AARegexMount, AARegexUnix,
AARegexSignal, AARegexPtrace, AARegexPivotRoot):
setup_regex_tests(tests)
test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(tests))
result = unittest.TextTestRunner(verbosity=verbosity).run(test_suite)
if not result.wasSuccessful():
exit(1)
unittest.main(verbosity=2)