2016-10-01 20:57:09 +02:00
#! /usr/bin/python3
2013-09-28 20:43:06 +05:30
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
2016-12-30 12:15:16 -08:00
# Copyright (C) 2016 Canonical, Ltd.
2013-09-28 20:43:06 +05:30
#
# 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.
#
# ----------------------------------------------------------------------
2013-08-26 00:23:59 +05:30
import argparse
2013-08-21 11:26:09 +05:30
import os
import re
2016-12-30 12:15:16 -08:00
import subprocess
2013-09-26 18:41:41 +05:30
import sys
2013-08-21 11:26:09 +05:30
2014-02-27 14:53:25 -08:00
import apparmor.aa as aa
import apparmor.ui as ui
2022-08-07 12:26:24 -04:00
from apparmor.common import AppArmorException, open_file_read
2015-07-06 22:02:34 +02:00
from apparmor.fail import enable_aa_exception_handler
2014-02-11 16:23:21 -08:00
from apparmor.translations import init_translation
2022-08-07 20:32:07 -04:00
enable_aa_exception_handler() # setup exception handling
_ = init_translation() # setup module translations
2014-02-10 22:15:05 -08:00
2013-12-20 03:12:58 +05:30
parser = argparse.ArgumentParser(description=_("Lists unconfined processes having tcp or udp ports"))
2024-07-06 00:37:01 -07:00
parser.add_argument("--paranoid", action="store_true", help=_("scan all processes"))
2024-07-07 04:34:27 -07:00
parser.add_argument("--show", default=None, type=str, help=_("all | network | server | client"))
2024-07-07 05:16:37 -07:00
parser.add_argument("--short", action="store_true", help=_("only display processes that are unconfined"))
2020-10-25 15:48:41 +01:00
parser.add_argument('--configdir', type=str, help=argparse.SUPPRESS)
2016-12-30 12:15:16 -08:00
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"))
2013-08-21 11:26:09 +05:30
args = parser.parse_args()
2024-07-06 00:37:01 -07:00
# 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:
2024-07-07 04:34:27 -07:00
if not args.show or args.show not in ['all', 'network', 'server', 'client']:
2024-07-06 00:37:01 -07:00
raise AppArmorException(_("Argument --show invalid value '%s'") % args.show)
show = args.show
2020-10-25 15:48:41 +01:00
aa.init_aa(confdir=args.configdir)
2014-02-27 14:53:25 -08:00
aa_mountpoint = aa.check_for_apparmor()
2013-08-21 11:26:09 +05:30
if not aa_mountpoint:
2021-08-24 22:09:21 +02:00
raise AppArmorException(_("It seems AppArmor was not started. Please enable AppArmor and try again."))
2013-08-21 11:26:09 +05:30
2016-12-30 12:15:16 -08:00
2024-07-06 00:38:58 -07:00
def map_show_to_flags(show):
flags = '-nlp'
if show == 'client':
flags = '-np'
2024-07-07 04:34:27 -07:00
elif show == 'network':
flags = '-nap'
2024-07-06 00:38:58 -07:00
return flags
2016-12-30 12:15:16 -08:00
def get_all_pids():
2022-08-07 14:57:30 -04:00
"""Return a set of all pids via walking /proc"""
2016-12-30 12:15:16 -08:00
return set(filter(lambda x: re.search(r"^\d+$", x), aa.get_subdirectories("/proc")))
2024-07-06 00:38:58 -07:00
def get_pids_ss(flags, ss='ss'):
2022-08-07 14:57:30 -04:00
"""Get a set of pids listening on network sockets via ss(8)"""
2016-12-30 12:15:16 -08:00
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']:
2024-07-06 00:38:58 -07:00
cmd = [ss, flags, '--family', family]
2016-12-30 12:15:16 -08:00
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
2024-07-06 00:38:58 -07:00
def get_pids_netstat(flags, netstat='netstat'):
2022-08-07 14:57:30 -04:00
"""Get a set of pids listening on network sockets via netstat(8)"""
2022-11-20 13:08:46 -05:00
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+)")
2016-12-30 12:15:16 -08:00
2024-07-06 00:38:58 -07:00
cmd = [netstat, flags, '--protocol', 'inet,inet6']
2016-12-30 12:15:16 -08:00
my_env = os.environ.copy()
my_env['LANG'] = 'C'
my_env['PATH'] = '/bin:/usr/bin:/sbin:/usr/sbin'
2013-12-20 03:12:58 +05:30
if sys.version_info < (3, 0):
2016-12-30 12:15:16 -08:00
output = subprocess.check_output(cmd, shell=False, env=my_env).split("\n")
2013-09-26 18:41:41 +05:30
else:
2016-12-30 12:15:16 -08:00
# 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")
2013-09-22 22:51:30 +05:30
2016-12-30 12:15:16 -08:00
pids = set()
2013-08-21 11:26:09 +05:30
for line in output:
match = regex_tcp_udp.search(line)
if match:
2016-12-30 12:15:16 -08:00
pids.add(match.group('pid'))
return pids
2020-09-18 13:31:05 +02:00
def read_proc_current(filename):
attr = None
2024-02-24 20:33:35 -08:00
try:
# don't bother with if os.path.exists(filename): there is always a race
2021-08-24 22:09:21 +02:00
with open_file_read(filename) as current:
2020-09-18 13:31:05 +02:00
for line in current:
line = line.strip()
2024-07-07 05:16:37 -07:00
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
2020-09-18 13:34:37 +02:00
# intentionally not checking for '(unconfined)', because $binary confined by $profile (unconfined) would look very confusing
2020-09-18 13:31:05 +02:00
attr = line
2024-02-24 20:33:35 -08:00
except OSError:
# just ignore errors atm
# print("Error trying to open {filename}")
return None
2020-09-18 13:31:05 +02:00
return attr
2024-01-20 20:08:39 +01:00
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
2016-12-30 12:15:16 -08:00
pids = set()
2024-07-06 00:37:01 -07:00
if show == 'all':
2016-12-30 12:15:16 -08:00
pids = get_all_pids()
2021-08-12 15:57:01 +02:00
elif args.with_ss or (not args.with_netstat and (aa.which("ss") is not None)):
2024-07-06 00:38:58 -07:00
pids = get_pids_ss(map_show_to_flags(show))
2016-12-30 12:15:16 -08:00
else:
2024-07-06 00:38:58 -07:00
pids = get_pids_netstat(map_show_to_flags(show))
2013-08-21 11:26:09 +05:30
2016-12-30 12:15:16 -08:00
for pid in sorted(map(int, pids)):
2013-08-21 11:26:09 +05:30
try:
2016-12-30 12:22:58 -08:00
prog = os.readlink("/proc/%s/exe" % pid)
2024-01-20 20:08:39 +01:00
prog = escape_special_chars(prog)
2013-12-29 15:12:30 +05:30
except OSError:
2013-08-21 11:26:09 +05:30
continue
2020-09-18 13:31:05 +02:00
2021-09-18 19:02:56 +02:00
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
2020-09-18 13:31:05 +02:00
attr = read_proc_current("/proc/%s/attr/current" % pid)
2013-09-22 22:51:30 +05:30
2016-12-30 12:18:14 -08:00
pname = None
cmdline = None
2021-08-24 22:09:21 +02:00
with open_file_read("/proc/%s/cmdline" % pid) as cmd:
2016-12-30 12:18:14 -08:00
cmdline = cmd.readlines()[0]
pname = cmdline.split("\0")[0]
2013-08-21 11:26:09 +05:30
if '/' in pname and pname != prog:
2016-12-30 12:22:58 -08:00
pname = "(%s)" % pname
2024-01-20 20:08:39 +01:00
pname = escape_special_chars(pname)
2013-08-21 11:26:09 +05:30
else:
2013-12-20 03:12:58 +05:30
pname = ""
regex_interpreter = re.compile(r"^(/usr)?/bin/(python|perl|bash|dash|sh)$")
2013-08-21 11:26:09 +05:30
if not attr:
2013-08-30 03:54:31 +05:30
if regex_interpreter.search(prog):
2013-12-20 03:12:58 +05:30
cmdline = re.sub(r"\x00", " ", cmdline)
cmdline = re.sub(r"\s+$", "", cmdline).strip()
2024-01-20 20:08:39 +01:00
cmdline = escape_special_chars(cmdline)
2013-08-30 03:54:31 +05:30
2016-12-30 12:22:58 -08:00
ui.UI_Info(_("%(pid)s %(program)s (%(commandline)s) not confined") % {'pid': pid, 'program': prog, 'commandline': cmdline})
2013-08-21 11:26:09 +05:30
else:
if pname and pname[-1] == ')':
2014-09-14 23:47:00 +05:30
pname = ' ' + pname
2016-12-30 12:22:58 -08:00
ui.UI_Info(_("%(pid)s %(program)s%(pname)s not confined") % {'pid': pid, 'program': prog, 'pname': pname})
2024-07-07 05:16:37 -07:00
elif not args.short:
2013-08-30 03:54:31 +05:30
if regex_interpreter.search(prog):
2013-12-20 03:12:58 +05:30
cmdline = re.sub(r"\0", " ", cmdline)
cmdline = re.sub(r"\s+$", "", cmdline).strip()
2024-01-20 20:08:39 +01:00
cmdline = escape_special_chars(cmdline)
2016-12-30 12:22:58 -08:00
ui.UI_Info(_("%(pid)s %(program)s (%(commandline)s) confined by '%(attribute)s'") % {'pid': pid, 'program': prog, 'commandline': cmdline, 'attribute': attr})
2013-08-21 11:26:09 +05:30
else:
if pname and pname[-1] == ')':
2014-09-14 23:47:00 +05:30
pname = ' ' + pname
2016-12-30 12:22:58 -08:00
ui.UI_Info(_("%(pid)s %(program)s%(pname)s confined by '%(attribute)s'") % {'pid': pid, 'program': prog, 'pname': pname, 'attribute': attr})