diff --git a/parser/tst/test_profile.py b/parser/tst/test_profile.py new file mode 100755 index 000000000..77a347f46 --- /dev/null +++ b/parser/tst/test_profile.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +import sys +import subprocess +import re +import os + + +VERBOSE = bool(os.environ.get('VERBOSE')) + +NAME_RE = re.compile(r'^Name:\s*(\S+)') +PERMS_ALL = re.compile(r'^Perms:.*r.*:.*:.*\(/\{?,?\*\*,?}?\)') +ATTACH_RE = re.compile(r'^Attachment:\s*(.+)') + +RED = '\033[0;31m' +GREEN = '\033[0;32m' +NORMAL = '\033[0m' + +def die(msg): + print(RED + msg + NORMAL, file=sys.stderr) + sys.exit(1) + +def check_profile(name, prof_file, skip, attachment, lines): + if skip: + if VERBOSE: + print('Profile "{}" skipped: {}'.format(name, skip)) + return + + pat = re.compile(r'^Perms:.*r.*:.*:.*\({}\)'.format(re.escape(attachment))) + for l in lines: + if pat.match(l): + if VERBOSE: + print(GREEN + 'Profile {} ({}): OK "{}" found'.format(prof_file, name, attachment) + NORMAL) + return + + die('Profile {} ({}): ERROR: no Perms rule for "{}".'.format(prof_file, name, attachment)) + +def process_profile(profile, extra_args): + cmd = ['../parser/apparmor_parser'] + extra_args + ['-d', profile] + proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + if proc.returncode != 0: + die('ERROR: Failed to parse "{}": {}'.format(profile, proc.stdout.strip())) + + lines = proc.stdout.splitlines() + curr_name = '' + attachment = '' + skip = None + in_entries = False + block = [] + + for line in lines: + m = NAME_RE.match(line) + if m: + if curr_name: + # check previously found profile + check_profile(curr_name, profile, skip, attachment, block) + + # remember newly found profile + curr_name = m.group(1) + attachment = '' + skip = None + in_entries = False + block = [] + continue + + if PERMS_ALL.match(line): + skip = 'All files available' + continue + + m = ATTACH_RE.match(line) + if m: + attachment = m.group(1) + if attachment == '': + skip = 'no attachment' + continue + + if line.strip() == '--- Entries ---': + in_entries = True + continue + + if in_entries: + block.append(line) + + # Last profile + check_profile(curr_name, profile, skip, attachment, block) + +if __name__ == '__main__': + if len(sys.argv) < 2: + print('Usage: {0} [parser_extra_args]'.format(sys.argv[0])) + sys.exit(1) + process_profile(sys.argv[1], sys.argv[2:]) diff --git a/parser/tst/test_profile.sh b/parser/tst/test_profile.sh deleted file mode 100755 index de8ea9090..000000000 --- a/parser/tst/test_profile.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Check if the current profile allow reading its attachment -check_entry() { - local prof_name="$1" - local -n lines_ref="$2" - local attachment="$3" - local found=0 - - for line in "${lines_ref[@]}"; do - if [[ $line == Perms:*r*:*"($attachment)"* ]]; then - found=1 - break - fi - done - - if [[ $found -eq 0 ]]; then - echo -e "\e[0;31mProfile $prof_name: ERROR: no Perms rule for '$attachment'.\e[0m" - exit 1 - fi - - [[ -n "${VERBOSE:-}" ]] && echo -e "\e[0;32mProfile $prof_name: OK '$attachment' found\e[0m" || true -} - -# Handle the end of a profile block: either skip it or check for the entry. -finish_profile() { - local name="$1" - local prof_file="$2" - local skip="$3" - local attachment="$4" - local arr_name="$5" - - if [[ -n $name ]]; then - if [[ $skip != 0 ]]; then - [[ -n "${VERBOSE:-}" ]] && echo "Profile '$name' skipped: $skip" || true - else - check_entry "$prof_file ($name)" "$arr_name" "$attachment" - fi - fi -} - -process_profile() { - local prof_file="$1" - shift - local dump curr_name="" attachment="" skip_profile=0 in_entries=0 - local block_lines=() - - if ! dump=$(../parser/apparmor_parser $@ -d "$prof_file" 2>&1); then - echo "\e[0;31mERROR: Failed to parse '$prof_file': $dump\e[0m" >&2 - exit 1 - fi - - IFS=$'\n' read -r -d '' -a lines < <(printf '%s\n' "$dump" && printf '\0') - - for line in "${lines[@]}"; do - if [[ $line =~ ^[[:space:]]*Name:[[:space:]]*([^[:space:]]+) ]]; then - finish_profile "$curr_name" "$prof_file" "$skip_profile" "$attachment" block_lines - curr_name="${BASH_REMATCH[1]}" - attachment="" skip_profile=0 in_entries=0 block_lines=() - elif [[ $line =~ ^[[:space:]]*Mode:[[:space:]]*unconfined ]]; then - skip_profile="unconfined" - elif [[ $line =~ ^Perms:.*r.*:.*:.*\(/(\{?,?\*\*,*\}?)\) ]]; then - skip_profile="All files available" - elif [[ $line =~ ^[[:space:]]*Attachment:[[:space:]]*(.+) ]]; then - attachment="${BASH_REMATCH[1]}" - [[ $attachment == "" ]] && skip_profile="no attachment" - elif [[ $line == ---\ Entries\ --- ]]; then - in_entries=1 - elif [[ $in_entries -ne 0 ]]; then - block_lines+=("$line") - fi - done - - # Last profile - finish_profile "$curr_name" "$prof_file" "$skip_profile" "$attachment" block_lines -} - -if (( $# < 1 )); then - echo "Usage: $0 [parser_extra_args]" - exit 1 -fi - -process_profile $@ diff --git a/profiles/Makefile b/profiles/Makefile index d48d26974..77a1e8e64 100644 --- a/profiles/Makefile +++ b/profiles/Makefile @@ -123,12 +123,14 @@ check-parser: test-dependencies $(Q)for profile in $$(find ${PROFILES_SOURCE} -maxdepth 1 -type f) ; do \ [ -n "${VERBOSE}" ] && echo "Testing $${profile}" ; \ ${PARSER} --config-file=../parser/tst/parser.conf -S -b ${PROFILES_SOURCE} $${profile} > /dev/null || exit 1; \ + ../parser/tst/test_profile.py $${profile} --config-file=../parser/tst/parser.conf -S -b ${PROFILES_SOURCE} || exit 1; \ done @echo "*** Checking profiles from ${EXTRAS_SOURCE} against apparmor_parser" $(Q)for profile in $$(find ${EXTRAS_SOURCE} -maxdepth 1 -type f -not -name README) ; do \ [ -n "${VERBOSE}" ] && echo "Testing $${profile}" ; \ ${PARSER} --config-file=../parser/tst/parser.conf -S -b ${EXTRAS_SOURCE} -I ${PROFILES_SOURCE} $${profile} > /dev/null || exit 1; \ + ../parser/tst/test_profile.py $${profile} --config-file=../parser/tst/parser.conf -S -b ${PROFILES_SOURCE} || exit 1; \ done @echo "*** Checking abstractions from ${ABSTRACTIONS_SOURCE} against apparmor_parser"