mirror of
https://github.com/roddhjav/apparmor.d.git
synced 2025-02-11 12:45:10 +01:00
Merge branch 'roddhjav:main' into main
This commit is contained in:
commit
5d6a1ef621
18 changed files with 602 additions and 70 deletions
2
.github/local/needrestart
vendored
Normal file
2
.github/local/needrestart
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
/var/lib/waagent/** r,
|
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
|
@ -94,6 +94,7 @@ jobs:
|
||||||
sudo apt-get install -y \
|
sudo apt-get install -y \
|
||||||
apparmor-profiles apparmor-utils \
|
apparmor-profiles apparmor-utils \
|
||||||
bats bats-support
|
bats bats-support
|
||||||
|
sudo install -Dm0644 .github/local/needrestart /etc/apparmor.d/local/needrestart
|
||||||
|
|
||||||
- name: Install apparmor.d
|
- name: Install apparmor.d
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -27,6 +27,8 @@ profile cron @{exec_path} flags=(attach_disconnected) {
|
||||||
|
|
||||||
ptrace (read) peer=unconfined,
|
ptrace (read) peer=unconfined,
|
||||||
|
|
||||||
|
unix bind type=stream addr=@@{udbus}/bus/cron/system,
|
||||||
|
|
||||||
@{exec_path} mr,
|
@{exec_path} mr,
|
||||||
|
|
||||||
@{sh_path} rix,
|
@{sh_path} rix,
|
||||||
|
|
|
@ -21,8 +21,9 @@ profile networkd-dispatcher @{exec_path} {
|
||||||
@{exec_path} mr,
|
@{exec_path} mr,
|
||||||
|
|
||||||
@{bin}/ r,
|
@{bin}/ r,
|
||||||
@{bin}/networkctl rPx,
|
@{bin}/chronyc rPx,
|
||||||
@{bin}/ls rix,
|
@{bin}/ls rix,
|
||||||
|
@{bin}/networkctl rPx,
|
||||||
@{bin}/sed rix,
|
@{bin}/sed rix,
|
||||||
|
|
||||||
@{lib}/networkd-dispatcher/routable.d/postfix rix,
|
@{lib}/networkd-dispatcher/routable.d/postfix rix,
|
||||||
|
|
|
@ -27,11 +27,10 @@ profile networkctl @{exec_path} flags=(attach_disconnected) {
|
||||||
unix (bind) type=stream addr=@@{udbus}/bus/networkctl/system,
|
unix (bind) type=stream addr=@@{udbus}/bus/networkctl/system,
|
||||||
|
|
||||||
#aa:dbus talk bus=system name=org.freedesktop.network1 label=systemd-networkd
|
#aa:dbus talk bus=system name=org.freedesktop.network1 label=systemd-networkd
|
||||||
# No label available
|
dbus send bus=system path=/org/freedesktop/network1{,/**}
|
||||||
dbus send bus=system path=/org/freedesktop/network@{int}
|
|
||||||
interface=org.freedesktop.DBus.Properties
|
interface=org.freedesktop.DBus.Properties
|
||||||
member=Get
|
member=Get
|
||||||
peer=(name=org.freedesktop.network@{int}),
|
peer=(name=org.freedesktop.network1),
|
||||||
|
|
||||||
@{exec_path} mr,
|
@{exec_path} mr,
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ profile apparmor_parser @{exec_path} flags=(attach_disconnected) {
|
||||||
|
|
||||||
@{exec_path} mr,
|
@{exec_path} mr,
|
||||||
|
|
||||||
|
@{lib_dirs}/@{multiarch}/** mr,
|
||||||
@{lib_dirs}/snapd/apparmor.d/{,**} r,
|
@{lib_dirs}/snapd/apparmor.d/{,**} r,
|
||||||
@{lib_dirs}/snapd/apparmor/{,**} r,
|
@{lib_dirs}/snapd/apparmor/{,**} r,
|
||||||
|
|
||||||
|
|
|
@ -12,19 +12,9 @@ profile cgrulesengd @{exec_path} {
|
||||||
include <abstractions/base>
|
include <abstractions/base>
|
||||||
include <abstractions/nameservice-strict>
|
include <abstractions/nameservice-strict>
|
||||||
|
|
||||||
# For creating Unix domain sockets/IPC sockets:
|
|
||||||
# socket(AF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR) = 3
|
|
||||||
# ...
|
|
||||||
# bind(3, {sa_family=AF_NETLINK, nl_pid=13284, nl_groups=0x000001}, 12) = -1 EPERM (Operation
|
|
||||||
# not permitted)
|
|
||||||
capability net_admin,
|
|
||||||
|
|
||||||
# To remove the following errors:
|
|
||||||
# readlink("/proc/12/exe", 0x7ffc9fa85cd0, 4096) = -1 EACCES (Permission denied)
|
|
||||||
capability sys_ptrace,
|
|
||||||
|
|
||||||
# To be able to read the /proc/ files of all processes in the system.
|
|
||||||
capability dac_read_search,
|
capability dac_read_search,
|
||||||
|
capability net_admin,
|
||||||
|
capability sys_ptrace,
|
||||||
|
|
||||||
network netlink dgram,
|
network netlink dgram,
|
||||||
|
|
||||||
|
@ -32,22 +22,22 @@ profile cgrulesengd @{exec_path} {
|
||||||
|
|
||||||
@{exec_path} mr,
|
@{exec_path} mr,
|
||||||
|
|
||||||
@{sys}/fs/cgroup/**/tasks w,
|
|
||||||
|
/etc/cgconfig.conf r,
|
||||||
|
/etc/cgconfig.d/{,*} r,
|
||||||
|
|
||||||
|
/etc/cgrules.conf r,
|
||||||
|
/etc/cgrules.d/{,*} r,
|
||||||
|
|
||||||
|
owner @{run}/cgred.socket w,
|
||||||
|
|
||||||
|
@{sys}/fs/cgroup/** rw,
|
||||||
|
|
||||||
@{PROC}/ r,
|
@{PROC}/ r,
|
||||||
@{PROC}/@{pids}/cmdline r,
|
@{PROC}/@{pids}/cmdline r,
|
||||||
@{PROC}/@{pids}/task/ r,
|
@{PROC}/@{pids}/task/ r,
|
||||||
owner @{PROC}/@{pid}/mounts r,
|
|
||||||
@{PROC}/cgroups r,
|
@{PROC}/cgroups r,
|
||||||
|
owner @{PROC}/@{pid}/mounts r,
|
||||||
@{sys}/fs/cgroup/unified/cgroup.controllers r,
|
|
||||||
|
|
||||||
owner @{run}/cgred.socket w,
|
|
||||||
|
|
||||||
/etc/cgconfig.conf r,
|
|
||||||
/etc/cgrules.conf r,
|
|
||||||
/etc/cgconfig.d/ r,
|
|
||||||
|
|
||||||
|
|
||||||
include if exists <local/cgrulesengd>
|
include if exists <local/cgrulesengd>
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,27 +10,35 @@ include <tunables/global>
|
||||||
@{exec_path} = @{bin}/chsh
|
@{exec_path} = @{bin}/chsh
|
||||||
profile chsh @{exec_path} {
|
profile chsh @{exec_path} {
|
||||||
include <abstractions/base>
|
include <abstractions/base>
|
||||||
include <abstractions/consoles>
|
|
||||||
include <abstractions/authentication>
|
include <abstractions/authentication>
|
||||||
|
include <abstractions/bus-system>
|
||||||
|
include <abstractions/consoles>
|
||||||
include <abstractions/nameservice-strict>
|
include <abstractions/nameservice-strict>
|
||||||
include <abstractions/wutmp>
|
include <abstractions/wutmp>
|
||||||
|
|
||||||
capability audit_write,
|
capability audit_write,
|
||||||
capability chown,
|
capability chown,
|
||||||
capability fsetid,
|
capability fsetid,
|
||||||
|
capability net_admin,
|
||||||
capability setuid,
|
capability setuid,
|
||||||
|
|
||||||
network netlink raw,
|
network netlink raw,
|
||||||
|
|
||||||
|
unix type=stream addr=@@{udbus}/bus/chsh/system,
|
||||||
|
|
||||||
|
#aa:dbus talk bus=system name=org.freedesktop.home1 label=systemd-homed
|
||||||
|
|
||||||
@{exec_path} mr,
|
@{exec_path} mr,
|
||||||
|
|
||||||
/etc/shells r,
|
/etc/shells r,
|
||||||
|
|
||||||
|
/etc/.chsh.@{rand6} rw,
|
||||||
/etc/passwd rw,
|
/etc/passwd rw,
|
||||||
/etc/passwd- w,
|
/etc/passwd- w,
|
||||||
/etc/passwd+ rw,
|
|
||||||
/etc/passwd.@{pid} w,
|
/etc/passwd.@{pid} w,
|
||||||
/etc/passwd.lock wl -> /etc/passwd.@{pid},
|
/etc/passwd.lock wl -> /etc/passwd.@{pid},
|
||||||
|
/etc/passwd.OLD wl -> /etc/passwd,
|
||||||
|
/etc/passwd+ rw,
|
||||||
|
|
||||||
/etc/shadow r,
|
/etc/shadow r,
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,12 @@ profile flatpak @{exec_path} flags=(attach_disconnected,mediate_deleted,complain
|
||||||
owner @{HOME}/.var/ w,
|
owner @{HOME}/.var/ w,
|
||||||
owner @{HOME}/.var/app/{,**} rw,
|
owner @{HOME}/.var/app/{,**} rw,
|
||||||
|
|
||||||
owner @{user_documents_dirs}/ rw,
|
# Can create dotfile directories for any app
|
||||||
|
owner @{user_cache_dirs}/*/ w,
|
||||||
|
owner @{user_config_dirs}/*/ w,
|
||||||
|
owner @{user_share_dirs}/*/ w,
|
||||||
|
owner @{user_games_dirs}/{,**/} w,
|
||||||
|
owner @{user_documents_dirs}/ w,
|
||||||
|
|
||||||
owner @{user_cache_dirs}/flatpak/{,**} rw,
|
owner @{user_cache_dirs}/flatpak/{,**} rw,
|
||||||
owner @{user_config_dirs}/pulse/client.conf r,
|
owner @{user_config_dirs}/pulse/client.conf r,
|
||||||
|
|
|
@ -10,32 +10,28 @@ include <tunables/global>
|
||||||
@{exec_path} = @{bin}/iotop
|
@{exec_path} = @{bin}/iotop
|
||||||
profile iotop @{exec_path} {
|
profile iotop @{exec_path} {
|
||||||
include <abstractions/base>
|
include <abstractions/base>
|
||||||
include <abstractions/python>
|
|
||||||
include <abstractions/nameservice-strict>
|
include <abstractions/nameservice-strict>
|
||||||
|
include <abstractions/python>
|
||||||
|
|
||||||
# Needed?
|
|
||||||
audit deny capability net_admin,
|
|
||||||
|
|
||||||
# To set processes' priorities
|
|
||||||
capability sys_nice,
|
capability sys_nice,
|
||||||
|
|
||||||
@{exec_path} r,
|
network netlink raw,
|
||||||
@{bin}/python3.@{int} r,
|
|
||||||
|
|
||||||
@{bin}/file rix,
|
@{exec_path} r,
|
||||||
|
|
||||||
@{bin}/ r,
|
@{bin}/ r,
|
||||||
|
@{bin}/file rix,
|
||||||
|
@{bin}/python3.@{int} r,
|
||||||
|
|
||||||
|
/etc/magic r,
|
||||||
|
|
||||||
@{PROC}/ r,
|
@{PROC}/ r,
|
||||||
@{PROC}/vmstat r,
|
|
||||||
owner @{PROC}/@{pid}/mounts r,
|
|
||||||
owner @{PROC}/@{pid}/fd/ r,
|
|
||||||
@{PROC}/@{pids}/cmdline r,
|
@{PROC}/@{pids}/cmdline r,
|
||||||
@{PROC}/@{pids}/task/ r,
|
@{PROC}/@{pids}/task/ r,
|
||||||
@{PROC}/sys/kernel/pid_max r,
|
@{PROC}/sys/kernel/pid_max r,
|
||||||
|
@{PROC}/vmstat r,
|
||||||
# For file
|
owner @{PROC}/@{pid}/fd/ r,
|
||||||
/etc/magic r,
|
owner @{PROC}/@{pid}/mounts r,
|
||||||
|
|
||||||
include if exists <local/iotop>
|
include if exists <local/iotop>
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,10 +22,11 @@ profile needrestart @{exec_path} flags=(attach_disconnected) {
|
||||||
|
|
||||||
ptrace (read),
|
ptrace (read),
|
||||||
|
|
||||||
mqueue r type=posix /,
|
mqueue (r,getattr) type=posix /,
|
||||||
|
|
||||||
@{exec_path} mrix,
|
@{exec_path} mrix,
|
||||||
|
|
||||||
|
@{bin}/* r,
|
||||||
@{sh_path} rix,
|
@{sh_path} rix,
|
||||||
@{bin}/dpkg-query rpx,
|
@{bin}/dpkg-query rpx,
|
||||||
@{bin}/fail2ban-server rPx,
|
@{bin}/fail2ban-server rPx,
|
||||||
|
@ -42,8 +43,6 @@ profile needrestart @{exec_path} flags=(attach_disconnected) {
|
||||||
@{lib}/needrestart/* rPx,
|
@{lib}/needrestart/* rPx,
|
||||||
/usr/share/debconf/frontend rix,
|
/usr/share/debconf/frontend rix,
|
||||||
|
|
||||||
@{bin}/networkd-dispatcher r,
|
|
||||||
@{bin}/gettext.sh r,
|
|
||||||
/usr/share/needrestart/{,**} r,
|
/usr/share/needrestart/{,**} r,
|
||||||
/usr/share/unattended-upgrades/unattended-upgrade-shutdown r,
|
/usr/share/unattended-upgrades/unattended-upgrade-shutdown r,
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@ profile snap-seccomp @{exec_path} {
|
||||||
include <abstractions/consoles>
|
include <abstractions/consoles>
|
||||||
include <abstractions/nameservice-strict>
|
include <abstractions/nameservice-strict>
|
||||||
|
|
||||||
|
capability dac_read_search,
|
||||||
|
|
||||||
network netlink raw,
|
network netlink raw,
|
||||||
|
|
||||||
@{exec_path} mr,
|
@{exec_path} mr,
|
||||||
|
|
|
@ -68,6 +68,7 @@ profile snapd @{exec_path} {
|
||||||
@{sh_path} rix,
|
@{sh_path} rix,
|
||||||
@{bin}/apparmor_parser rPx,
|
@{bin}/apparmor_parser rPx,
|
||||||
@{bin}/cp rix,
|
@{bin}/cp rix,
|
||||||
|
@{bin}/getent rix,
|
||||||
@{bin}/gzip rix,
|
@{bin}/gzip rix,
|
||||||
@{bin}/journalctl rPx,
|
@{bin}/journalctl rPx,
|
||||||
@{bin}/kmod rPx,
|
@{bin}/kmod rPx,
|
||||||
|
@ -93,7 +94,7 @@ profile snapd @{exec_path} {
|
||||||
@{lib_dirs}/snapd/snap-update-ns rPx,
|
@{lib_dirs}/snapd/snap-update-ns rPx,
|
||||||
|
|
||||||
/usr/share/bash-completion/{,**} r,
|
/usr/share/bash-completion/{,**} r,
|
||||||
/usr/share/dbus-1/{system,session}.d/{,snapd*} r,
|
/usr/share/dbus-1/{system,session}.d/{,snapd*} rw,
|
||||||
/usr/share/dbus-1/services/*snap* r,
|
/usr/share/dbus-1/services/*snap* r,
|
||||||
/usr/share/polkit-1/actions/{,**/} r,
|
/usr/share/polkit-1/actions/{,**/} r,
|
||||||
|
|
||||||
|
|
23
apparmor.d/profiles-s-z/swayimg
Normal file
23
apparmor.d/profiles-s-z/swayimg
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# apparmor.d - Full set of apparmor profiles
|
||||||
|
# Copyright (C) 2024 valoq <valoq@mailbox.org>
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
|
||||||
|
abi <abi/4.0>,
|
||||||
|
|
||||||
|
include <tunables/global>
|
||||||
|
|
||||||
|
@{exec_path} = @{bin}/swayimg
|
||||||
|
profile swayimg @{exec_path} {
|
||||||
|
include <abstractions/base>
|
||||||
|
include <abstractions/desktop>
|
||||||
|
include <abstractions/fontconfig-cache-read>
|
||||||
|
include <abstractions/user-read-strict>
|
||||||
|
|
||||||
|
@{exec_path} mr,
|
||||||
|
|
||||||
|
owner @{user_config_dirs}/swayimg/** r,
|
||||||
|
|
||||||
|
include if exists <local/swayimg>
|
||||||
|
}
|
||||||
|
|
||||||
|
# vim:syntax=apparmor
|
26
apparmor.d/profiles-s-z/wttrbar
Normal file
26
apparmor.d/profiles-s-z/wttrbar
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# apparmor.d - Full set of apparmor profiles
|
||||||
|
# Copyright (C) 2024 odomingao
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
|
||||||
|
abi <abi/4.0>,
|
||||||
|
|
||||||
|
include <tunables/global>
|
||||||
|
|
||||||
|
@{exec_path} = @{bin}/wttrbar
|
||||||
|
profile wttrbar @{exec_path} {
|
||||||
|
include <abstractions/base>
|
||||||
|
include <abstractions/nameservice-strict>
|
||||||
|
|
||||||
|
network inet dgram,
|
||||||
|
network inet6 dgram,
|
||||||
|
network inet stream,
|
||||||
|
network inet6 stream,
|
||||||
|
|
||||||
|
@{exec_path} mr,
|
||||||
|
|
||||||
|
owner /tmp/wttrbar--wttr.in.json rw,
|
||||||
|
|
||||||
|
include if exists <local/wttrbar>
|
||||||
|
}
|
||||||
|
|
||||||
|
# vim:syntax=apparmor
|
|
@ -311,24 +311,24 @@
|
||||||
@{video_ext} += 3[gG]2 # 3g2
|
@{video_ext} += 3[gG]2 # 3g2
|
||||||
|
|
||||||
# Subtitles
|
# Subtitles
|
||||||
@{suntitles_ext} = [aA][qQ][tT] # aqt
|
@{subtitles_ext} = [aA][qQ][tT] # aqt
|
||||||
@{suntitles_ext} += [aA][sS][sS] # ass
|
@{subtitles_ext} += [aA][sS][sS] # ass
|
||||||
@{suntitles_ext} += [gG][sS][uU][bB] # gsub
|
@{subtitles_ext} += [gG][sS][uU][bB] # gsub
|
||||||
@{suntitles_ext} += [uU][sS][fF] # usf
|
@{subtitles_ext} += [uU][sS][fF] # usf
|
||||||
@{suntitles_ext} += [pP][aA][cC] # pac
|
@{subtitles_ext} += [pP][aA][cC] # pac
|
||||||
@{suntitles_ext} += [pP][jJ][sS] # pjs
|
@{subtitles_ext} += [pP][jJ][sS] # pjs
|
||||||
@{suntitles_ext} += [pP][sS][bB] # psb
|
@{subtitles_ext} += [pP][sS][bB] # psb
|
||||||
@{suntitles_ext} += [rR][tT] # rt
|
@{subtitles_ext} += [rR][tT] # rt
|
||||||
@{suntitles_ext} += [sS][bB][vV] # sbv
|
@{subtitles_ext} += [sS][bB][vV] # sbv
|
||||||
@{suntitles_ext} += [sS][mM][iI] # smi
|
@{subtitles_ext} += [sS][mM][iI] # smi
|
||||||
@{suntitles_ext} += [sS][rR][tT] # srt
|
@{subtitles_ext} += [sS][rR][tT] # srt
|
||||||
@{suntitles_ext} += [sS][sS][aA] # ssa
|
@{subtitles_ext} += [sS][sS][aA] # ssa
|
||||||
@{suntitles_ext} += [sS][sS][fF] # ssf
|
@{subtitles_ext} += [sS][sS][fF] # ssf
|
||||||
@{suntitles_ext} += [sS][tT][lL] # stl
|
@{subtitles_ext} += [sS][tT][lL] # stl
|
||||||
@{suntitles_ext} += [sS][uU][bB] # sub
|
@{subtitles_ext} += [sS][uU][bB] # sub
|
||||||
@{suntitles_ext} += [tT][t][mM][lL] # ttml
|
@{subtitles_ext} += [tT][t][mM][lL] # ttml
|
||||||
@{suntitles_ext} += [tT][t][xX][tT] # ttxt
|
@{subtitles_ext} += [tT][t][xX][tT] # ttxt
|
||||||
@{suntitles_ext} += [vV][tT][t] # vtt
|
@{subtitles_ext} += [vV][tT][t] # vtt
|
||||||
|
|
||||||
# Images
|
# Images
|
||||||
@{image_ext} = [aA][pP][nN][gG] # apng
|
@{image_ext} = [aA][pP][nN][gG] # apng
|
||||||
|
|
|
@ -5,10 +5,6 @@
|
||||||
|
|
||||||
load common
|
load common
|
||||||
|
|
||||||
setup_file() {
|
|
||||||
skip "mqueue raised despite the rule being present. See https://gitlab.com/apparmor/apparmor/-/issues/362"
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "needrestart: List outdated processes" {
|
@test "needrestart: List outdated processes" {
|
||||||
needrestart
|
needrestart
|
||||||
}
|
}
|
||||||
|
|
480
tests/profile_check.py
Normal file
480
tests/profile_check.py
Normal file
|
@ -0,0 +1,480 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# SPDX-License-Identifier: GPL-2.0-only
|
||||||
|
|
||||||
|
# KNOWN ISSUES:
|
||||||
|
# No guards for file type - expects AppArmor
|
||||||
|
# Diffirent suggestions for single line are mutually exclusive
|
||||||
|
# Suggestion could point to changed profile name, based on other suggestion
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import pathlib
|
||||||
|
import shlex
|
||||||
|
import json
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
try:
|
||||||
|
from apparmor.regex import *
|
||||||
|
from apparmor.aa import is_skippable_file
|
||||||
|
from apparmor.rule.file import FileRule, FileRuleset
|
||||||
|
from apparmor.common import convert_regexp
|
||||||
|
try:
|
||||||
|
from apparmor.rule.variable import separate_vars
|
||||||
|
except ImportError:
|
||||||
|
from apparmor.aa import separate_vars
|
||||||
|
|
||||||
|
LIBAPPARMOR = True
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
LIBAPPARMOR = False
|
||||||
|
|
||||||
|
def sanitizeProfileName(name):
|
||||||
|
|
||||||
|
if name.startswith('/') or name.startswith('@{'):
|
||||||
|
name = pathlib.Path(name).stem
|
||||||
|
|
||||||
|
if ' ' in name:
|
||||||
|
name = re.sub(r'\s+', '-', name)
|
||||||
|
|
||||||
|
return name
|
||||||
|
|
||||||
|
def makeLocalIdentity(nestingStacker_):
|
||||||
|
|
||||||
|
newStacker = []
|
||||||
|
for i in nestingStacker_:
|
||||||
|
i = sanitizeProfileName(i)
|
||||||
|
newStacker.append(i)
|
||||||
|
|
||||||
|
identity = '_'.join(newStacker) # separate each (sub)profile identity with underscores
|
||||||
|
|
||||||
|
return identity
|
||||||
|
|
||||||
|
def getCurrentProfile(stacker):
|
||||||
|
|
||||||
|
if stacker:
|
||||||
|
profile = stacker[-1]
|
||||||
|
else:
|
||||||
|
profile = None
|
||||||
|
|
||||||
|
return profile
|
||||||
|
|
||||||
|
def handleFileMessages(l, file, profile, lineNum):
|
||||||
|
|
||||||
|
wholeFileAccessProfiles = (
|
||||||
|
# '',
|
||||||
|
)
|
||||||
|
suggestOwner = ( # TODO: switch to AARE
|
||||||
|
r'^@{HOME}/',
|
||||||
|
r'^/home/\w+/',
|
||||||
|
r'^@{run}/user/@{uid}/',
|
||||||
|
r'^/run/user/\d+/',
|
||||||
|
r'^@{tmp}/',
|
||||||
|
r'^/tmp/',
|
||||||
|
r'^/var/tmp/',
|
||||||
|
r'^/dev/shm/',
|
||||||
|
)
|
||||||
|
|
||||||
|
lG = l.groupdict()
|
||||||
|
reason_ = None
|
||||||
|
if lG.get('path'):
|
||||||
|
if lG.get('path').startswith('/**') and profile not in wholeFileAccessProfiles: # false positives
|
||||||
|
severity_ = 'ERROR'
|
||||||
|
reason_ = 'Whole filesystem access is too broad'
|
||||||
|
suggestion_ = None
|
||||||
|
|
||||||
|
for r in suggestOwner:
|
||||||
|
if re.match(r, lG.get('path')) and not lG.get('owner'):
|
||||||
|
indentRe = re.match(r'^\s+', l.group())
|
||||||
|
if indentRe:
|
||||||
|
indent = indentRe.group()
|
||||||
|
else:
|
||||||
|
indent = ''
|
||||||
|
|
||||||
|
severity_ = 'NOTICE'
|
||||||
|
reason_ = "'owner' is likely required"
|
||||||
|
suggestion_ = indent + 'owner ' + l.group().lstrip()
|
||||||
|
break
|
||||||
|
|
||||||
|
elif lG.get('bare_file') and profile not in wholeFileAccessProfiles:
|
||||||
|
severity_ = 'ERROR'
|
||||||
|
reason_ = 'Whole filesystem access is too broad'
|
||||||
|
suggestion_ = None
|
||||||
|
|
||||||
|
if reason_: # something matched
|
||||||
|
msg = {'filename': file,
|
||||||
|
'profile': profile,
|
||||||
|
'severity': severity_,
|
||||||
|
'line': lineNum,
|
||||||
|
'reason': reason_,
|
||||||
|
'suggestion': suggestion_}
|
||||||
|
else:
|
||||||
|
msg = None
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def readApparmorFile(fullpath):
|
||||||
|
'''AA file could contain multiple AA profiles'''
|
||||||
|
headers = (
|
||||||
|
'# apparmor.d - Full set of apparmor profiles',
|
||||||
|
'# Copyright (C) ',
|
||||||
|
'# SPDX-License-Identifier: GPL-2.0-only',
|
||||||
|
)
|
||||||
|
|
||||||
|
file_data = {}
|
||||||
|
fileVars = {}
|
||||||
|
nestingStacker = []
|
||||||
|
duplicateProfilesCounter = []
|
||||||
|
localExists = {}
|
||||||
|
localExists_eol = {}
|
||||||
|
messages = []
|
||||||
|
exceptionMsg = None
|
||||||
|
line = None
|
||||||
|
gotAbi = False
|
||||||
|
gotHeaders = {}
|
||||||
|
gotAttach = False
|
||||||
|
isAfterProfileStart = False
|
||||||
|
lastLineNum = None
|
||||||
|
try:
|
||||||
|
with open(fullpath, 'r') as f:
|
||||||
|
for n,line in enumerate(f, start=1):
|
||||||
|
if isAfterProfileStart:
|
||||||
|
isAfterProfileStart = False
|
||||||
|
expectedIndent = len(nestingStacker) * ' '
|
||||||
|
indentRe = re.match(r'^\s+', line)
|
||||||
|
if indentRe:
|
||||||
|
indent = indentRe.group()
|
||||||
|
else:
|
||||||
|
indent = ''
|
||||||
|
|
||||||
|
if indent != expectedIndent:
|
||||||
|
spacesCount = len(nestingStacker) * 2
|
||||||
|
nestingCount = len(nestingStacker)
|
||||||
|
messages.append({'filename': fullpath,
|
||||||
|
'profile': getCurrentProfile(nestingStacker),
|
||||||
|
'severity': 'WARNING',
|
||||||
|
'line': n,
|
||||||
|
'reason': f"Expected {spacesCount} spaces for {nestingCount} nesting",
|
||||||
|
'suggestion': f"{expectedIndent}{line.lstrip()}"})
|
||||||
|
|
||||||
|
if line.endswith(' \n'):
|
||||||
|
messages.append({'filename': fullpath,
|
||||||
|
'profile': getCurrentProfile(nestingStacker),
|
||||||
|
'severity': 'WARNING',
|
||||||
|
'line': n,
|
||||||
|
'reason': "Redundant trailing whitespace",
|
||||||
|
'suggestion': line.rstrip()})
|
||||||
|
|
||||||
|
if '\t' in line:
|
||||||
|
messages.append({'filename': fullpath,
|
||||||
|
'profile': getCurrentProfile(nestingStacker),
|
||||||
|
'severity': 'WARNING',
|
||||||
|
'line': n,
|
||||||
|
'reason': "Tabs are not allowed",
|
||||||
|
'suggestion': line.replace('\t', ' ')})
|
||||||
|
|
||||||
|
if len(gotHeaders) < 3 and not nestingStacker:
|
||||||
|
for nH,i in enumerate(headers):
|
||||||
|
if line.startswith(i):
|
||||||
|
gotHeaders[nH] = True
|
||||||
|
|
||||||
|
if RE_ABI.search(line):
|
||||||
|
gotAbi = line
|
||||||
|
|
||||||
|
elif RE_PROFILE_START.search(line) or RE_PROFILE_HAT_DEF.search(line):
|
||||||
|
isAfterProfileStart = True
|
||||||
|
m = parse_profile_start_line(line, fullpath)
|
||||||
|
if m.get('profile'):
|
||||||
|
nestingStacker.append(m.get('profile')) # set early
|
||||||
|
|
||||||
|
if m.get('attachment') == '@{exec_path}' and not gotAttach: # can be only singular
|
||||||
|
gotAttach = True
|
||||||
|
|
||||||
|
profileMsg = {'filename': fullpath,
|
||||||
|
'profile': getCurrentProfile(nestingStacker),
|
||||||
|
'severity': 'WARNING',
|
||||||
|
'line': n,
|
||||||
|
'reason': "A short named profile must be defined",
|
||||||
|
'suggestion': None}
|
||||||
|
if m.get('plainprofile'):
|
||||||
|
messages.append(profileMsg)
|
||||||
|
elif m.get('namedprofile'):
|
||||||
|
if m.get('namedprofile').startswith('/'):
|
||||||
|
messages.append(profileMsg)
|
||||||
|
|
||||||
|
if m.get('flags'):
|
||||||
|
m['flags'] = set(shlex.split(m.pop('flags').replace(',', '')))
|
||||||
|
if 'complain' in m['flags']:
|
||||||
|
messages.append({'filename': fullpath,
|
||||||
|
'profile': getCurrentProfile(nestingStacker),
|
||||||
|
'severity': 'WARNING',
|
||||||
|
'line': n,
|
||||||
|
'reason': "'complain' flag must be defined in 'dists/flags'",
|
||||||
|
'suggestion': None})
|
||||||
|
else:
|
||||||
|
m['flags'] = set()
|
||||||
|
|
||||||
|
if m.get('profile'):
|
||||||
|
duplicateProfilesCounter.append(m.get('profile'))
|
||||||
|
profileIdentity = '//'.join(nestingStacker)
|
||||||
|
file_data[profileIdentity] = m
|
||||||
|
|
||||||
|
elif RE_PROFILE_VARIABLE.search(line):
|
||||||
|
lineV = RE_PROFILE_VARIABLE.search(line).groups()
|
||||||
|
|
||||||
|
name = strip_quotes(lineV[0])
|
||||||
|
operation = lineV[1]
|
||||||
|
val = separate_vars(lineV[2])
|
||||||
|
if fileVars.get(name):
|
||||||
|
fileVars[name].update(set(val))
|
||||||
|
if operation == '=':
|
||||||
|
messages.append({'filename': fullpath,
|
||||||
|
'profile': getCurrentProfile(nestingStacker),
|
||||||
|
'severity': 'DEGRADED',
|
||||||
|
'line': n,
|
||||||
|
'reason': "Tunable must be appended with '+='",
|
||||||
|
'suggestion': None})
|
||||||
|
else:
|
||||||
|
fileVars[name] = set(val)
|
||||||
|
if operation == '+=':
|
||||||
|
messages.append({'filename': fullpath,
|
||||||
|
'profile': getCurrentProfile(nestingStacker),
|
||||||
|
'severity': 'DEGRADED',
|
||||||
|
'line': n,
|
||||||
|
'reason': "Tunable must be defined with '='",
|
||||||
|
'suggestion': None})
|
||||||
|
|
||||||
|
elif RE_INCLUDE.search(line):
|
||||||
|
if nestingStacker:
|
||||||
|
profileIdentity = '//'.join(nestingStacker)
|
||||||
|
localIdentity = makeLocalIdentity(nestingStacker)
|
||||||
|
localValue = f'include if exists <local/{localIdentity}>' # commented out will also match
|
||||||
|
if localValue in line:
|
||||||
|
localExists[profileIdentity] = localValue
|
||||||
|
|
||||||
|
# Handle file entries
|
||||||
|
elif RE_PROFILE_FILE_ENTRY.search(line):
|
||||||
|
lineF = RE_PROFILE_FILE_ENTRY.search(line)
|
||||||
|
fileMsg = handleFileMessages(lineF, fullpath, getCurrentProfile(nestingStacker), n)
|
||||||
|
if fileMsg:
|
||||||
|
messages.append(fileMsg)
|
||||||
|
|
||||||
|
elif RE_PROFILE_END.search(line):
|
||||||
|
if getCurrentProfile(nestingStacker):
|
||||||
|
if not nestingStacker:
|
||||||
|
messages.append({'filename': fullpath,
|
||||||
|
'profile': None,
|
||||||
|
'severity': 'DEGRADED',
|
||||||
|
'line': n,
|
||||||
|
'reason': "Unbalanced parenthesis?", # not fully covered
|
||||||
|
'suggestion': None})
|
||||||
|
else:
|
||||||
|
profileIdentity = '//'.join(nestingStacker)
|
||||||
|
localExists_eol[profileIdentity] = n
|
||||||
|
del nestingStacker[-1] # remove last
|
||||||
|
|
||||||
|
lastLineNum = n
|
||||||
|
|
||||||
|
except PermissionError:
|
||||||
|
exceptionMsg = 'Unable to read the file (PermissionError)'
|
||||||
|
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
exceptionMsg = 'Unable to read the file (UnicodeDecodeError)'
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
exceptionMsg = 'No such file or directory (FileNotFoundError)'
|
||||||
|
|
||||||
|
if exceptionMsg:
|
||||||
|
messages.append({'filename': fullpath,
|
||||||
|
'profile': None,
|
||||||
|
'severity': 'NOTICE',
|
||||||
|
'line': None,
|
||||||
|
'reason': exceptionMsg,
|
||||||
|
'suggestion': None})
|
||||||
|
|
||||||
|
# Ensure proper header is present
|
||||||
|
if len(gotHeaders) < 3:
|
||||||
|
combinedHeader = '\n'.join(headers)
|
||||||
|
messages.append({'filename': fullpath,
|
||||||
|
'profile': None,
|
||||||
|
'severity': 'WARNING',
|
||||||
|
'line': 1,
|
||||||
|
'reason': 'No proper header',
|
||||||
|
'suggestion': combinedHeader})
|
||||||
|
|
||||||
|
# Ensure ABI is present
|
||||||
|
changeAbi = False
|
||||||
|
abi = 'abi <abi/4.0>,'
|
||||||
|
if gotAbi:
|
||||||
|
if gotAbi.strip() != abi:
|
||||||
|
changeAbi = True
|
||||||
|
else:
|
||||||
|
changeAbi = True
|
||||||
|
|
||||||
|
if changeAbi:
|
||||||
|
messages.append({'filename': fullpath,
|
||||||
|
'profile': None,
|
||||||
|
'severity': 'WARNING',
|
||||||
|
'line': None,
|
||||||
|
'reason': 'ABI is required',
|
||||||
|
'suggestion': abi})
|
||||||
|
|
||||||
|
# Ensure singular '@{exec_path}'
|
||||||
|
if not gotAttach:
|
||||||
|
messages.append({'filename': fullpath,
|
||||||
|
'profile': None,
|
||||||
|
'severity': 'WARNING',
|
||||||
|
'line': None,
|
||||||
|
'reason': "'@{exec_path}' must be defined as main path attachment",
|
||||||
|
'suggestion': None})
|
||||||
|
|
||||||
|
# Ensure trailing vim syntax
|
||||||
|
if line:
|
||||||
|
trailingSyntax = '# vim:syntax=apparmor\n'
|
||||||
|
if line != trailingSyntax:
|
||||||
|
messages.append({'filename': fullpath,
|
||||||
|
'profile': None,
|
||||||
|
'severity': 'WARNING',
|
||||||
|
'line': lastLineNum,
|
||||||
|
'reason': 'No trailing syntax hint',
|
||||||
|
'suggestion': trailingSyntax})
|
||||||
|
|
||||||
|
# Assign variables to profile attachments as paths and assign filenames
|
||||||
|
for p,d in deepcopy(file_data).items():
|
||||||
|
file_data[p]['filename'] = fullpath
|
||||||
|
attachment = d.get('attachment')
|
||||||
|
if attachment:
|
||||||
|
if attachment.startswith('@{'):
|
||||||
|
if fileVars.get(attachment):
|
||||||
|
file_data[p]['attach_paths'] = fileVars[attachment] # incoming set
|
||||||
|
else:
|
||||||
|
messages.append({'filename': fullpath,
|
||||||
|
'profile': p,
|
||||||
|
'severity': 'ERROR',
|
||||||
|
'line': None,
|
||||||
|
'reason': f"Unknown global variable as profile attachment: {attachment}",
|
||||||
|
'suggestion': None})
|
||||||
|
|
||||||
|
else:
|
||||||
|
if isinstance(file_data[p].get('attachment'), set):
|
||||||
|
raise ValueError("Expecting 'str' or 'None', not 'set'")
|
||||||
|
file_data[p]['attach_paths'] = {file_data[p]['attachment']}
|
||||||
|
|
||||||
|
# Check if profile block does not have corresponding 'local' include
|
||||||
|
for p,d in file_data.items():
|
||||||
|
if not localExists.get(p): # not found previously
|
||||||
|
if '//' in p:
|
||||||
|
identity = p.split('//')
|
||||||
|
else:
|
||||||
|
identity = [p]
|
||||||
|
|
||||||
|
localIdentity = makeLocalIdentity(identity)
|
||||||
|
filename = file_data[p]['filename']
|
||||||
|
messages.append({'filename': filename,
|
||||||
|
'profile': p,
|
||||||
|
'severity': 'WARNING',
|
||||||
|
'line': localExists_eol.get(p), # None? Unbalanced parenthesis?
|
||||||
|
'reason': "The (sub)profile block does not have expected 'local' include",
|
||||||
|
'suggestion': f'include if exists <local/{localIdentity}>'})
|
||||||
|
|
||||||
|
# Track multiple definitions inside single file
|
||||||
|
for profile in duplicateProfilesCounter:
|
||||||
|
counter = duplicateProfilesCounter.count(profile)
|
||||||
|
if counter >= 2:
|
||||||
|
messages.append({'filename': fullpath,
|
||||||
|
'profile': profile,
|
||||||
|
'severity': 'DEGRADED',
|
||||||
|
'line': None,
|
||||||
|
'reason': "Profile has been defined {counter} times in the same file",
|
||||||
|
'suggestion': None})
|
||||||
|
|
||||||
|
return (messages, file_data)
|
||||||
|
|
||||||
|
def findAllProfileFilenames(profile_dir):
|
||||||
|
|
||||||
|
profiles = set()
|
||||||
|
for path in pathlib.Path(profile_dir).iterdir():
|
||||||
|
if path.is_file() and not is_skippable_file(path):
|
||||||
|
profiles.add(path.resolve())
|
||||||
|
|
||||||
|
# Not default, dig deeper
|
||||||
|
if not profiles:
|
||||||
|
nestedDirs = (
|
||||||
|
'groups',
|
||||||
|
'profiles-a-f',
|
||||||
|
'profiles-g-l',
|
||||||
|
'profiles-m-r',
|
||||||
|
'profiles-s-z',
|
||||||
|
)
|
||||||
|
for d in nestedDirs:
|
||||||
|
dirpath = pathlib.Path(pathlib.Path(profile_dir).resolve(), pathlib.Path(d))
|
||||||
|
for p in dirpath.rglob("*"):
|
||||||
|
if p.is_file():
|
||||||
|
profiles.add(p)
|
||||||
|
|
||||||
|
return profiles
|
||||||
|
|
||||||
|
def handleArgs():
|
||||||
|
"""DEGRADED are purposed for fatal errors - when the profile set will fail to load entirely"""
|
||||||
|
|
||||||
|
allSeverities = ['DEBUG', 'NOTICE', 'WARNING', 'ERROR', 'CRITICAL', 'DEGRADED']
|
||||||
|
aaRoot = '/etc/apparmor.d'
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('-d', '--aa-root-dir', action='store',
|
||||||
|
default=aaRoot,
|
||||||
|
help='Target different AppArmor root directory rather than default')
|
||||||
|
parser.add_argument('-p', '--profile', action='append',
|
||||||
|
help='Handle only specified profile')
|
||||||
|
# parser.add_argument('-s', '--severity', action='append',
|
||||||
|
# choices=allSeverities,
|
||||||
|
# help='Handle only specified severity event')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# if not args.severity:
|
||||||
|
# args.severity = allSeverities
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
|
||||||
|
args = handleArgs()
|
||||||
|
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
profile_dir = args.aa_root_dir
|
||||||
|
if not args.profile:
|
||||||
|
profiles = findAllProfileFilenames(profile_dir)
|
||||||
|
else:
|
||||||
|
profiles = set()
|
||||||
|
for p in args.profile:
|
||||||
|
absolutePath = pathlib.Path(p).resolve()
|
||||||
|
profiles.add(absolutePath)
|
||||||
|
|
||||||
|
profile_data = {}
|
||||||
|
for path in sorted(profiles):
|
||||||
|
if not is_skippable_file(path):
|
||||||
|
readApparmorFile_Out = readApparmorFile(path)
|
||||||
|
profilesInFile = readApparmorFile_Out[1]
|
||||||
|
messages.extend(readApparmorFile_Out[0])
|
||||||
|
profile_data.update(profilesInFile)
|
||||||
|
|
||||||
|
for m in messages:
|
||||||
|
if m.get('suggestion'):
|
||||||
|
if m['suggestion'].endswith('\n'):
|
||||||
|
m['suggestion'] = m.get('suggestion').removesuffix('\n')
|
||||||
|
m['filename'] = str(m.get('filename'))
|
||||||
|
print(json.dumps(m, indent=2))
|
||||||
|
|
||||||
|
if messages:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
if not LIBAPPARMOR:
|
||||||
|
raise ImportError(f"""Can't find 'python3-apparmor' package! Install with:
|
||||||
|
$ sudo apt install python3-apparmor""")
|
||||||
|
|
||||||
|
main(sys.argv)
|
Loading…
Reference in a new issue