mirror of
https://gitlab.com/apparmor/apparmor.git
synced 2025-03-04 08:24:42 +01:00

Contrary to what the name would imply aa-unconfined displays info for both confined and unconfined processes. Add a --short option that only output processes that are not confined. Eg. $ ./utils/aa-unconfined 17192 /snap/chromium/2890/usr/lib/chromium-browser/chrome (/snap/chromium/2890/usr/lib/chromium-browser/chrome --password-store=basic --disable-features=TFLiteLanguageDetectionEnabled) confined by 'snap.chromium.chromium (enforce)' 17395 /snap/chromium/2890/usr/lib/chromium-browser/chrome (/snap/chromium/2890/usr/lib/chromium-browser/chrome --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --crashpad-handler-pid=17337 --enable-crash-reporter=,snap --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,16674663885832976354,18417931519279121981,262144 --disable-features=TFLiteLanguageDetectionEnabled --variations-seed-version) confined by 'snap.chromium.chromium (enforce)' 17981 /snap/firefox/4451/usr/lib/firefox/firefox confined by 'snap.firefox.firefox (enforce)' 1353664 /tmp/.mount_OrcaSl7G1va5/bin/orca-slicer not confined is trimmed to $ ./utils/aa-unconfined --short 1353664 /tmp/.mount_OrcaSl7G1va5/bin/orca-slicer not confined Signed-off-by: John Johansen <john.johansen@canonical.com>
202 lines
8.1 KiB
Python
Executable file
202 lines
8.1 KiB
Python
Executable file
#! /usr/bin/python3
|
|
# ----------------------------------------------------------------------
|
|
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
|
|
# Copyright (C) 2016 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 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 argparse
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
import apparmor.aa as aa
|
|
import apparmor.ui as ui
|
|
from apparmor.common import AppArmorException, open_file_read
|
|
from apparmor.fail import enable_aa_exception_handler
|
|
from apparmor.translations import init_translation
|
|
|
|
enable_aa_exception_handler() # setup exception handling
|
|
_ = init_translation() # setup module translations
|
|
|
|
parser = argparse.ArgumentParser(description=_("Lists unconfined processes having tcp or udp ports"))
|
|
parser.add_argument("--paranoid", action="store_true", help=_("scan all processes"))
|
|
parser.add_argument("--show", default=None, type=str, help=_("all | network | server | client"))
|
|
parser.add_argument("--short", action="store_true", help=_("only display processes that are unconfined"))
|
|
parser.add_argument('--configdir', type=str, help=argparse.SUPPRESS)
|
|
bin_group = parser.add_mutually_exclusive_group()
|
|
bin_group.add_argument("--with-ss", action='store_true', help=_("use ss(8) to find listening processes (default)"))
|
|
bin_group.add_argument("--with-netstat", action='store_true', help=_("use netstat(8) to find listening processes"))
|
|
args = parser.parse_args()
|
|
|
|
# set default set of processes to show
|
|
show = 'server'
|
|
|
|
if args.paranoid:
|
|
if args.show is not None and args.show != 'all':
|
|
raise AppArmorException(_("Arguments --paranoid and --show=%s conflict") % args.show)
|
|
show = 'all'
|
|
if args.show is not None:
|
|
if not args.show or args.show not in ['all', 'network', 'server', 'client']:
|
|
raise AppArmorException(_("Argument --show invalid value '%s'") % args.show)
|
|
show = args.show
|
|
|
|
aa.init_aa(confdir=args.configdir)
|
|
|
|
aa_mountpoint = aa.check_for_apparmor()
|
|
if not aa_mountpoint:
|
|
raise AppArmorException(_("It seems AppArmor was not started. Please enable AppArmor and try again."))
|
|
|
|
|
|
def map_show_to_flags(show):
|
|
flags = '-nlp'
|
|
if show == 'client':
|
|
flags = '-np'
|
|
elif show == 'network':
|
|
flags = '-nap'
|
|
return flags
|
|
|
|
|
|
def get_all_pids():
|
|
"""Return a set of all pids via walking /proc"""
|
|
return set(filter(lambda x: re.search(r"^\d+$", x), aa.get_subdirectories("/proc")))
|
|
|
|
|
|
def get_pids_ss(flags, ss='ss'):
|
|
"""Get a set of pids listening on network sockets via ss(8)"""
|
|
regex_lines = re.compile(r"^(tcp|udp|raw|p_dgr)\s.+\s+users:(?P<users>\(\(.*\)\))$")
|
|
regex_users_pids = re.compile(r'(\("[^"]+",(pid=)?(\d+),[^)]+\))')
|
|
|
|
pids = set()
|
|
my_env = os.environ.copy()
|
|
my_env['LANG'] = 'C'
|
|
my_env['PATH'] = '/bin:/usr/bin:/sbin:/usr/sbin'
|
|
for family in ['inet', 'inet6', 'link']:
|
|
cmd = [ss, flags, '--family', family]
|
|
if sys.version_info < (3, 0):
|
|
output = subprocess.check_output(cmd, shell=False, env=my_env).split("\n")
|
|
else:
|
|
# Python3 needs to translate a stream of bytes to string with specified encoding
|
|
output = str(subprocess.check_output(cmd, shell=False, env=my_env), encoding='utf8').split("\n")
|
|
|
|
for line in output:
|
|
match = regex_lines.search(line.strip())
|
|
if match:
|
|
users = match.group('users')
|
|
for (_, _, pid) in regex_users_pids.findall(users):
|
|
pids.add(pid)
|
|
return pids
|
|
|
|
|
|
def get_pids_netstat(flags, netstat='netstat'):
|
|
"""Get a set of pids listening on network sockets via netstat(8)"""
|
|
regex_tcp_udp = re.compile(r"^(tcp|udp|raw)6?\s+\d+\s+\d+\s+\S+:(\d+)\s+\S+:(\*|\d+)\s+(LISTEN|\d+|\s+)\s+(?P<pid>\d+)/(\S+)")
|
|
|
|
cmd = [netstat, flags, '--protocol', 'inet,inet6']
|
|
my_env = os.environ.copy()
|
|
my_env['LANG'] = 'C'
|
|
my_env['PATH'] = '/bin:/usr/bin:/sbin:/usr/sbin'
|
|
if sys.version_info < (3, 0):
|
|
output = subprocess.check_output(cmd, shell=False, env=my_env).split("\n")
|
|
else:
|
|
# Python3 needs to translate a stream of bytes to string with specified encoding
|
|
output = str(subprocess.check_output(cmd, shell=False, env=my_env), encoding='utf8').split("\n")
|
|
|
|
pids = set()
|
|
for line in output:
|
|
match = regex_tcp_udp.search(line)
|
|
if match:
|
|
pids.add(match.group('pid'))
|
|
return pids
|
|
|
|
|
|
def read_proc_current(filename):
|
|
attr = None
|
|
|
|
try:
|
|
# don't bother with if os.path.exists(filename): there is always a race
|
|
with open_file_read(filename) as current:
|
|
for line in current:
|
|
line = line.strip()
|
|
if line.endswith(' (complain)', 1) or line.endswith(' (enforce)', 1) or line.endswith(' (kill)', 1) or line.endswith(' (user)', 1) or line.endswith(' (mixed)', 1): # enforce at least one char as profile name
|
|
# intentionally not checking for '(unconfined)', because $binary confined by $profile (unconfined) would look very confusing
|
|
attr = line
|
|
except OSError:
|
|
# just ignore errors atm
|
|
# print("Error trying to open {filename}")
|
|
return None
|
|
|
|
return attr
|
|
|
|
|
|
def escape_special_chars(data):
|
|
"""escape special characters in program names so that they can't mess up the terminal"""
|
|
data = repr(data)
|
|
if len(data) > 1 and data.startswith("'") and data.endswith("'"):
|
|
return data[1:-1]
|
|
else:
|
|
return data
|
|
|
|
|
|
pids = set()
|
|
if show == 'all':
|
|
pids = get_all_pids()
|
|
elif args.with_ss or (not args.with_netstat and (aa.which("ss") is not None)):
|
|
pids = get_pids_ss(map_show_to_flags(show))
|
|
else:
|
|
pids = get_pids_netstat(map_show_to_flags(show))
|
|
|
|
for pid in sorted(map(int, pids)):
|
|
try:
|
|
prog = os.readlink("/proc/%s/exe" % pid)
|
|
prog = escape_special_chars(prog)
|
|
except OSError:
|
|
continue
|
|
|
|
if os.path.exists("/proc/%s/attr/apparmor/current" % pid):
|
|
attr = read_proc_current("/proc/%s/attr/apparmor/current" % pid)
|
|
else:
|
|
# fallback to shared attr/current if attr/apparmor/current doesn't exist
|
|
attr = read_proc_current("/proc/%s/attr/current" % pid)
|
|
|
|
pname = None
|
|
cmdline = None
|
|
with open_file_read("/proc/%s/cmdline" % pid) as cmd:
|
|
cmdline = cmd.readlines()[0]
|
|
pname = cmdline.split("\0")[0]
|
|
if '/' in pname and pname != prog:
|
|
pname = "(%s)" % pname
|
|
pname = escape_special_chars(pname)
|
|
else:
|
|
pname = ""
|
|
regex_interpreter = re.compile(r"^(/usr)?/bin/(python|perl|bash|dash|sh)$")
|
|
if not attr:
|
|
if regex_interpreter.search(prog):
|
|
cmdline = re.sub(r"\x00", " ", cmdline)
|
|
cmdline = re.sub(r"\s+$", "", cmdline).strip()
|
|
cmdline = escape_special_chars(cmdline)
|
|
|
|
ui.UI_Info(_("%(pid)s %(program)s (%(commandline)s) not confined") % {'pid': pid, 'program': prog, 'commandline': cmdline})
|
|
else:
|
|
if pname and pname[-1] == ')':
|
|
pname = ' ' + pname
|
|
ui.UI_Info(_("%(pid)s %(program)s%(pname)s not confined") % {'pid': pid, 'program': prog, 'pname': pname})
|
|
elif not args.short:
|
|
if regex_interpreter.search(prog):
|
|
cmdline = re.sub(r"\0", " ", cmdline)
|
|
cmdline = re.sub(r"\s+$", "", cmdline).strip()
|
|
cmdline = escape_special_chars(cmdline)
|
|
ui.UI_Info(_("%(pid)s %(program)s (%(commandline)s) confined by '%(attribute)s'") % {'pid': pid, 'program': prog, 'commandline': cmdline, 'attribute': attr})
|
|
else:
|
|
if pname and pname[-1] == ')':
|
|
pname = ' ' + pname
|
|
ui.UI_Info(_("%(pid)s %(program)s%(pname)s confined by '%(attribute)s'") % {'pid': pid, 'program': prog, 'pname': pname, 'attribute': attr})
|