diff --git a/common-apparmor.sh b/common-apparmor.sh new file mode 100644 index 0000000000000000000000000000000000000000..f6a94977038e82708ec2f870dde7df9f396828f6 --- /dev/null +++ b/common-apparmor.sh @@ -0,0 +1,43 @@ +# Need to source this file. + +# Pass in audit string, returns formatted block as `aa_log_extract_tokens.pl` +# would have provided. Need to return: +# +# profile:/usr/bin/busctl +# sdmode:REJECTING +# denied_mask:r +# operation:open +# name:/proc/879/stat +# request_mask:r +# +# Note: apparmor uses the term "DENIED" rather than "REJECTING", but our +# expected values want "REJECTING", so tweak for now. +apparmor_parse_journal() { + awk -v event=$2 ' + { + for (i = 1; i <= NF; i++) { + n = index($i, "="); + if(n) { + vars[substr($i, 1, n - 1)] = substr($i, n + 1) + # Strip quotes + gsub("\"","",vars[substr($i, 1, n - 1)]) + } + } + } + { + if (vars["apparmor"] == event) { + print "====" + print "profile:"vars["profile"] + # Match old nomenclature + if (vars["apparmor"] == "DENIED") { + vars["apparmor"] = "REJECTING" + } + print "sdmode:"vars["apparmor"] + print "denied_mask:"vars["denied_mask"] + print "operation:"vars["operation"] + print "name:"vars["name"] + print "request_mask:"vars["requested_mask"] + } + } + ' $1 +} diff --git a/run-aa-test b/run-aa-test new file mode 100755 index 0000000000000000000000000000000000000000..ad6ec39bd77d976c4606991e96913e9b71141800 --- /dev/null +++ b/run-aa-test @@ -0,0 +1,180 @@ +#!/bin/sh +# +# Copyright © 2018 Collabora Ltd. +# +# Based on python version of run-aa-test +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +. common/common-apparmor.sh + +ALTERNATIVE_SEPARATOR="## alternative ##" +END=2 + +case $(echo ${LAUNCH_DBUS} | tr [A-Z] [a-z]) in +0 | no | false) + LAUNCH_DBUS="False" + ;; +*) + LAUNCH_DBUS="True" +esac + +case $(echo ${RUN_AS_USER} | tr [A-Z] [a-z]) in +0 | no | false) + RUN_AS_USER="False" + ;; +*) + RUN_AS_USER="True" +esac + +CHAIWALA_UID=1000 +CHAIWALA_USER="user" + +# Check parameters +if [ $# -lt 2 ]; then + + echo "Usage: run-aa-test <expectation-file> <command> <argument-1> <argument-2> …" + echo "\"export LAUNCH_DBUS=no\" in the test script to not launch a dbus session." + echo "\"export RUN_AS_USER=no\" in the test script to not run as ${CHAIWALA_USER}" + exit 1 +fi + +EXPECT_FILE=$1 +shift + +if [ ! -r ${EXPECT_FILE} ]; then + echo "Cannot read specified expectation file: ${EXPECT_FILE}" + exit 1 +fi + +if [ ! -x $1 ]; then + echo "Cannot execute specified test executable: $1" + exit 1 +fi + +# typically "normal.expected" or "malicious.expected" +TEST_TITLE=$( basename ${EXPECT_FILE} ) + +# Touch .bash_history, which we use in some tests, if it's not there. +bash_history="/home/${CHAIWALA_USER}/.bash_history" +if [ ! -r ${bash_history} ]; then + RET=$( sudo -u ${CHAIWALA_USER} touch ${bash_history} ) + if [ $RET != 0 ]; then + echo "Failed to create .bash_history: $RET" + exit 1 + fi +fi + +# Create a temporary directory for files +TMP_DIR=$(mktemp -d) + +# Log start time +START_TIME=$(date +"%F %T") + +if [ "${LAUNCH_DBUS}" = "True" ]; then + # Start a new D-Bus session for this test + CMD="dbus-run-session -- $*" +else + CMD="$*" +fi + +CMDLINE="common/run-test-in-systemd" +if [ ! -x $CMDLINE ]; then + echo "common/run-test-in-systemd not found" + exit 1 +fi + +CMDLINE="${CMDLINE} --no-lava" + +if [ "${RUN_AA_TEST_TIMEOUT}" != "" ]; then + CMDLINE="${CMDLINE} --timeout=${RUN_AA_TEST_TIMEOUT}" +fi + +if [ "${RUN_AS_USER}" = "True" ]; then + CMDLINE="${CMDLINE} --user=${CHAIWALA_UID}" +else + CMDLINE="${CMDLINE} --system" +fi + +CMDLINE="${CMDLINE} ${CMD}" + +echo "#=== running test script: ${CMDLINE} ===" + +setsid ${CMDLINE} +RET=$? + +echo "#--- end of test script, status: ${RET}" + +if [ "${RET}" = "0" ]; then + echo "${TEST_TITLE}_underlying_tests: pass" +else + echo "# ${CMDLINE} exited ${RET}" + # typically "normal.expected_underlying_tests: fail" + echo "${TEST_TITLE}_underlying_tests: fail" + + exit 1 +fi + +# Give journal time to log the entries. +sleep 3 + +# Get audit information from journal +AUDIT_FILE=${TMP_DIR}/AUDIT +journalctl -S "${START_TIME}" -t audit -o cat > ${AUDIT_FILE} + +echo "#=== ${TEST_TITLE} ===" + +echo "#---8<--- raw apparmor output from journal" +cat ${AUDIT_FILE} | sed 's/^/# /' +echo "#--->8---" + +echo "#---8<--- expected parsed apparmor output from journal" +cat ${EXPECT_FILE} | sed 's/^/# /' +echo "#--->8---" + +csplit ${EXPECT_FILE} -f ${TMP_DIR}/EXPECT -b "%d" "/^${ALTERNATIVE_SEPARATOR}$/" {*} + +# Old versions of csplit don't provide "--suppress-matched", strip separator separately +for FILE in ${TMP_DIR}/EXPECT*; do + sed -i "/^${ALTERNATIVE_SEPARATOR}$/d" ${FILE} +done + +PARSE_FILE="${TMP_DIR}/PARSE" + +# TODO: There is potential for other processes to cause messages to appear in +# the journal that may lead to false failures. If this is found to be an +# issue in practice, then additional filtering of results may be required +apparmor_parse_journal ${AUDIT_FILE} DENIED > ${PARSE_FILE} + +echo "#---8<--- actual parsed apparmor output from journal" +cat ${PARSE_FILE} | sed 's/^/# /' +echo "#--->8---" + +MATCH_EXPECTATION="False" + +# We might have alternative expectations, take that into consideration. +OUTPUT_MD5=$( cat ${PARSE_FILE} | md5sum ) +COUNT=$( ls -1 ${TMP_DIR}/EXPECT* | wc -l ) +NUM=0 +while [ $((${NUM} < ${COUNT})) = 1 ]; do + EXPECTED_MD5=$( cat ${TMP_DIR}/EXPECT${NUM} | md5sum ) + if [ "${OUTPUT_MD5}" = "${EXPECTED_MD5}" ]; then + echo "# audit log matches alternative expectation ${NUM}/${COUNT}" + MATCH_EXPECTATION="True" + fi + NUM=$((${NUM}+1)) +done + +if [ "${MATCH_EXPECTATION}" = "True" ]; then + echo "${TEST_TITLE}: pass" +else + echo "#---8<--- diff" + diff -urN ${TMP_DIR}/EXPECT${NUM} ${PARSE_FILE} + echo "#--->8---" + echo "${TEST_TITLE}: fail" + exit 1 +fi + +exit 0