project: apparmor
backends:
    garden:
        # The garden backend relies on https://gitlab.com/zygoon/image-garden
        # TODO: Switch to a released version for better stability.
        type: adhoc
        allocate: |
            # Spread automatically injects /snap/bin to PATH. When we are
            # running from the image-garden snap then SPREAD_HOST_PATH is the
            # original path before such modifications were applied. Snap
            # applications cannot normally run /snap/bin/* entry-points
            # successfully so re-set PATH to the original value, as provided by
            # snapcraft.
            if [ -n "${SPREAD_HOST_PATH-}" ]; then
                PATH="${SPREAD_HOST_PATH}"
            fi
            # Use just enough RAM to link the parser on a virtual system with
            # two cores. Using more cores may easily consume more memory, due
            # to make -j$(nproc), used below than a small CI/CD system is
            # typically granted.  It is better to have more workers than to
            # have one big worker with lots of resources.
            export QEMU_MEM_OPTION="-m 1536"
            export QEMU_SMP_OPTION="-smp 2"
            ARCH="$(uname -m)"
            # If a locally built kernel image exist then use it for booting.
            # Care needs to be taken to make sure the kernel is compatible with
            # loadable modules present in the file system.
            if [ -f bzImage ]; then
                # Create the qcow2/run files before setting custom kernel
                # options, so that first boot and initialization happen in a
                # standardized manner.
                image-garden make "$SPREAD_SYSTEM"."$ARCH".run "$SPREAD_SYSTEM"."$ARCH".qcow2 1>&2
                # Pass a simple drive instead of the more elaborate virtio
                # configuration that is used by default. Some images may not
                # support virtio enough for booting.
                export QEMU_STORAGE_OPTION="-drive file=$SPREAD_SYSTEM.$ARCH.qcow2,format=qcow2"
                # Refrain from passing EFI firmware to qemu so that we boot a
                # kernel directly and bypass both EFI and BIOS.
                export QEMU_BOOT_FIRMWARE_OPTION=""
                # Pass the kernel and cmdline by hand. At present this is tuned
                # to the Ubuntu cloud images that have the rootfs as the first
                # partition.
                exec image-garden allocate "$SPREAD_SYSTEM"."$ARCH" \
                  -usb -usbdevice keyboard -usbdevice mouse \
                  -kernel bzImage \
                  -append 'root=/dev/sda1 ro console=tty1 console=ttyS0'
            fi
            # Ask image garden to allocate the system and relay the result back
            # to spread as either success of failure.
            exec image-garden allocate "$SPREAD_SYSTEM"."$ARCH" \
                -usb -usbdevice keyboard -usbdevice mouse \
        discard: |
            # See above for an explanation.
            if [ -n "${SPREAD_HOST_PATH-}" ]; then
                PATH="${SPREAD_HOST_PATH}"
            fi
            image-garden discard "$SPREAD_SYSTEM_ADDRESS"
        systems:
            # All systems except for the one Ubuntu system are marked as
            # manual. This way we don't accidentally spin up everything when
            # someone runs spread without knowing better.
            - opensuse-cloud-tumbleweed:
                  username: opensuse
                  password: opensuse
                  workers: 4
                  manual: true
            - debian-cloud-12:
                  username: debian
                  password: debian
                  workers: 4
                  manual: true
            - debian-cloud-13:
                  username: debian
                  password: debian
                  workers: 4
                  manual: true
            - ubuntu-cloud-22.04:
                  username: ubuntu
                  password: ubuntu
                  workers: 4
                  manual: true
            - ubuntu-cloud-24.04:
                  username: ubuntu
                  password: ubuntu
                  manual: true
            - ubuntu-cloud-24.10:
                  username: ubuntu
                  password: ubuntu
                  workers: 4
            - fedora-cloud-41:
                  username: fedora
                  password: fedora

exclude:
    - .git
    - "*.o"
    - "*.a"
    - "*.la"
    - "*.lo"
    - "*.so"
    - "*.so.*"
    - .deps
    - .libs
    # Generated autotool content
    - aclocal.m4
    - autom4te.cache
    - config.guess
    - config.log
    - config.status
    - config.sub
    - configure
    - depcomp
    - missing
    - ylwrap
    # Generated automake makefiles
    - Makefile.in
    - libraries/libapparmor/Makefile
    - libraries/libapparmor/doc/Makefile
    - libraries/libapparmor/include/Makefile
    - libraries/libapparmor/include/sys/Makefile
    - libraries/libapparmor/src/Makefile
    - libraries/libapparmor/swig/Makefile
    - libraries/libapparmor/swig/perl/Makefile
    - libraries/libapparmor/swig/python/Makefile
    - libraries/libapparmor/swig/python/test/Makefile
    - libraries/libapparmor/swig/ruby/Makefile
    - libraries/libapparmor/testsuite/Makefile
    - libraries/libapparmor/testsuite/config/Makefile
    - libraries/libapparmor/testsuite/lib/Makefile
    - libraries/libapparmor/testsuite/libaalogparse.test/Makefile
    - libraries/libapparmor/testsuite/test_multi.multi
    # Generated wrapper scripts
    - libraries/libapparmor/testsuite/test_multi.multi
    # Files related to spread and image-garden.
    - "*.qcow2"
    - "*.iso"
    - "*.img"
    - "*.log"
    - "*.run"
    - "*.lock"
    - spread-logs
    - spread-artifacts
    # Locally provided kernel image. See allocate section in system backends,
    # this image, if present, is passed directly to qemu.
    - bzImage

# Copy the project to this path on the test system.
# This is also available as $SPREAD_PATH.
path: /tmp/apparmor

prepare: |
    # Configure libapparmor but only if a makefile is not already present.
    # This makes repeated iteration with -reuse much faster, as the chain of
    # invocations of make below are efficient if nothing needs to be done.
    if [ ! -f "$SPREAD_PATH"/libraries/libapparmor/Makefile ]; then
      (
        cd "$SPREAD_PATH"/libraries/libapparmor || exit 1
        if ! sh ./autogen.sh; then
            echo "The autogen.sh script has failed"
            exit 1
        fi
        if ! sh ./configure --prefix=/usr --with-perl --with-python; then
            echo "The generated configure script has failed"
            cat config.log
            exit 1
        fi
      )
    fi
    # Build libapparmor.
    make -C "$SPREAD_PATH"/libraries/libapparmor -j"$(nproc)"
    # Build apparmor_parser.
    make -C "$SPREAD_PATH"/parser -j"$(nproc)"
    # Build binary utilities (aa-exec and firends).
    make -C "$SPREAD_PATH"/binutils -j"$(nproc)"
    # Build python utilities.
    make -C "$SPREAD_PATH"/utils -j"$(nproc)"
    # Build apache and pam modules.
    make -C "$SPREAD_PATH"/changehat/mod_apparmor -j"$(nproc)"
    make -C "$SPREAD_PATH"/changehat/pam_apparmor -j"$(nproc)"

# In case of failure, include the kernel version in the log.
debug-each: |
    uname -a

suites:
    tests/profiles/:
        summary: Tests that exercise specific application profiles
        systems:
            # AppArmor is not enabled in the kernel.
            - -fedora-cloud-*
        # variables:
        #     PROFILE_NAME: name of the profile on disk
        #     PROGRAM_NAME: name of the program to execute
        prepare-each: |
            rm -f denials.txt

            # Disable rate-limiting so that we see all denials.
            sysctl --values kernel.printk_ratelimit >old-ratelimit.txt
            sysctl --write kernel.printk_ratelimit=0

            # Stop auditd so that all denials end up in the ring buffer.
            if [ "$(systemctl is-active auditd.service)" != inactive ]; then
                systemctl stop auditd.service
                touch did-stop-auditd.txt
            fi

            # Clear the kernel ring buffer.
            dmesg --clear

            # Compute profile name from the name of the task.
            echo "PROFILE_NAME=${PROFILE_NAME:=$(basename "$SPREAD_TASK")}"

            "$SPREAD_PATH"/parser/apparmor_parser \
                --warn=all \
                --replace \
                --skip-cache \
                --base="$SPREAD_PATH"/profiles/apparmor.d \
                "$SPREAD_PATH"/profiles/apparmor.d/"$PROFILE_NAME" 2>parser.txt
            if [ -s parser.txt ]; then
                echo "Parser produced warnings:"
                cat parser.txt
                exit 1
            fi

        restore-each: |
            # Compute profile name from the name of the task.
            echo "PROFILE_NAME=${PROFILE_NAME:=$(basename "$SPREAD_TASK")}"

            "$SPREAD_PATH"/parser/apparmor_parser \
                --base="$SPREAD_PATH"/profiles/apparmor.d \
                --remove \
                "$SPREAD_PATH"/profiles/apparmor.d/"$PROFILE_NAME"

            # Restore auditd and old rate-limit.
            if [ -f did-stop-auditd.txt ]; then
                systemctl start auditd.service
                rm -f did-stop-auditd.txt
            fi
            if [ -f old-ratelimit.txt ]; then
                sysctl -w kernel.printk_ratelimit="$(cat old-ratelimit.txt)"
                rm -f old-ratelimit.txt
            fi

            # Check if running the test resulted in any logged denials.
            if dmesg | grep DENIED > denials.txt; then
                if [ -z "${EXPECT_DENIALS:-}" ]; then
                    echo "Denials were emitted during the test."
                    cat denials.txt
                    exit 1
                else
                    readarray -t regexes <<< $(printf "%b" "$EXPECT_DENIALS")
                    declare -a found_regex_array

                    # Check if all generated denials match the expected ones
                    while read denial; do
                        found=0
                        for i in "${!regexes[@]}"; do
                            if grep -E -q "${regexes[i]}" <<< "$denial"; then
                                found_regex_array[$i]=1
                                found=1
                            fi
                        done

                        if [ $found -eq 0 ]; then
                            echo "Unexpected denial: $denial"
                            exit 1
                        fi
                    done <denials.txt

                    # Check if all denials correspond to a regex
                    for i in "${!regexes[@]}"; do
                        if [ -z ${found_regex_array[$i]:-} ] ; then
                            echo "Exected denial ${regexes[i]} was not found"
                            exit 1
                        fi
                    done
                fi
            fi
        debug-each: |
            echo "PROGRAM_NAME=${PROGRAM_NAME:=$(basename "$SPREAD_TASK")}"
            command -v "$PROGRAM_NAME"

    utils/:
        summary: Unit tests for the Python utilities.
        prepare: |
            # Generate apparmor profiles that the tests rely on.
            make -C "$SPREAD_PATH"/parser/tst gen_xtrans gen_dbus
            # Spread does not support programmatically generated test variants.
            # Ensure that the list baked into utils/test/task.yaml contains all
            # the files matching utils/test/test-*.py
            fail=0
            for V in $SPREAD_PATH/utils/test/test-*.py; do
              Vdash="$(basename "$V" | sed -e 's,^test-,,' -e 's,\.py$,,')"
              Vunder="$(basename "$V" | sed -e 's,^test-,,' -e 's,\.py$,,' -e 's,-,_,g')"
              if ! grep -xF '    TEST/'"$Vunder"': '"$Vdash" "$SPREAD_PATH"/utils/test/task.yaml; then
                echo "utils/test/task.yaml: missing test variant: TEST/$Vunder: $Vdash" >&2
                fail=1
              fi
            done
            if [ "$fail" -ne 0 ]; then
              echo "exiting due to missing variants listed above" >&2
              exit 1
            fi
    tests/unit/:
        summary: Unit tests that do not exercise the kernel layer.
    tests/regression/:
        summary: Regression tests for parser-kernel interaction.
        systems:
            # AppArmor is not enabled in the kernel.
            - -fedora-cloud-*
        prepare: |
            # Spread does not support programmatically generated test variants.
            # Ensure that the list baked into tests/regression/apparmor/task.yaml
            # contains all the tests defined in tests/regression/apparmor/Makefile.
            echo '$(foreach t,$(TESTS),$(info TEST/$t))' | \
              make -n -f "$SPREAD_PATH"/tests/regression/apparmor/Makefile -f /dev/stdin | \
              grep -F TEST/ | \
              cut -d / -f 2 > apparmor-regression-tests.txt
            fail=0
            missing_tests=()
            while read -r V; do
              if ! grep -xqF '    TEST/'"$V"': 1' "$SPREAD_PATH"/tests/regression/apparmor/task.yaml; then
                missing_tests+=("tests/regression/task.yaml: missing test variant: TEST/$V")
                fail=1
              fi
            done <apparmor-regression-tests.txt
            if [ "$fail" -ne 0 ]; then
              echo "exiting due to missing variants:" >&2
              for msg in "${missing_tests[@]}"; do
                echo "$msg" >&2
              done
              exit 1
            fi

            # Build all the apparmor regression test programs.
            make -C "$SPREAD_PATH"/tests/regression/apparmor -j"$(nproc)"
        restore: |
            rm -f apparmor-regression-tests.txt
    tests/snapd/:
        summary: Tests exercising a subset of behavior of snapd
        systems:
            # AppArmor is not enabled in the kernel.
            - -fedora-cloud-*
