diff --git a/AppImage/scripts/native_app_discovery.py b/AppImage/scripts/native_app_discovery.py index a87dfc6e..169fd4a2 100644 --- a/AppImage/scripts/native_app_discovery.py +++ b/AppImage/scripts/native_app_discovery.py @@ -62,6 +62,12 @@ _SYSTEM = { 'ip6tables', 'firewalld', 'ufw', 'resolvconf', 'wpa-supplicant', 'modemmanager', 'console-setup', 'keyboard-setup', 'plymouth', 'halt', 'reboot', 'shutdown', 'rc-local', 'ntpsec', 'networkd-dispatcher', 'busybox', 'atd', 'uuidd', + # Common packaged daemons are infrastructure, not a user-managed app. + 'nginx', 'apache', 'apache2', 'httpd', 'lighttpd', 'haproxy', 'traefik', + 'redis', 'redis-server', 'memcached', 'mysql', 'mysqld', 'mariadb', 'mariadbd', + 'postgres', 'postgresql', 'mongod', 'mongodb', 'rabbitmq-server', + 'elasticsearch', 'opensearch', 'fail2ban', 'named', 'bind9', 'dnsmasq', + 'avahi-daemon', 'smbd', 'nmbd', 'samba', 'vsftpd', 'proftpd', 'cups', 'tor', } _RUNTIMES = re.compile(r'(?:python[0-9.]*|node(?:js)?|sh|bash|dash|env|java|ruby|perl)\Z') @@ -205,9 +211,11 @@ def _path(value): def _system(name): raw = name.lower().removesuffix('.service') stem = raw.split('@')[0] - return raw.startswith('user@') or stem in _SYSTEM or stem.startswith(( + return (raw.startswith('user@') or stem in _SYSTEM + or (raw.startswith('php') and raw.endswith('-fpm')) + or stem.startswith(( 'systemd-', 'dbus-', 'getty', 'containerd-', 'apt-', 'dpkg-', 'e2scrub', - 'unattended-', 'ifup', 'ifdown', 'user@', 'btrfs-', 'lvm2-', 'lxc-')) + 'unattended-', 'ifup', 'ifdown', 'user@', 'btrfs-', 'lvm2-', 'lxc-'))) def _custom(path): @@ -240,6 +248,13 @@ def parse_records(text, kind): or install.startswith(('/usr/lib/systemd/', '/lib/systemd/', '/usr/lib/udev/', '/lib/udev/', '/usr/lib/apt/'))): continue + # /etc units are operator-owned. Do not infer an application from an + # arbitrary packaged systemd unit: without a package-db probe it is + # impossible to distinguish vendor infrastructure from a user app. + if (manager == 'systemd' + and source.startswith(('/usr/lib/systemd/system/', '/lib/systemd/system/')) + and not _custom(install)): + continue root = directory if _custom(directory) else str(Path(install).parent) # Generic runtime without an absolute application script is ambiguous. if runtime and not _custom(install): diff --git a/AppImage/scripts/tests/test_native_app_discovery.py b/AppImage/scripts/tests/test_native_app_discovery.py index cc1539b1..21962223 100644 --- a/AppImage/scripts/tests/test_native_app_discovery.py +++ b/AppImage/scripts/tests/test_native_app_discovery.py @@ -19,9 +19,9 @@ import native_app_discovery as native import lxc_apps -def service(name='handmade', exe='/opt/handmade/server', directory='/opt/handmade', manager='systemd', script=''): - path = ('/etc/systemd/system/' + name + '.service' if manager == 'systemd' - else '/etc/init.d/' + name) +def service(name='handmade', exe='/opt/handmade/server', directory='/opt/handmade', manager='systemd', script='', source=None): + path = source or ('/etc/systemd/system/' + name + '.service' if manager == 'systemd' + else '/etc/init.d/' + name) return '\t'.join(['S', path, manager, exe, script, directory]) + '\nEND\n' @@ -100,6 +100,25 @@ class DiscoveryFixtures(unittest.TestCase): self.assertEqual(result['candidates'], []) self.assertFalse(any(native.FOOTPRINT_SCRIPT in call for call in self.calls)) + def test_common_system_daemons_and_vendor_units_have_zero_footprint_probes(self): + cases = [ + ('nginx', '/usr/sbin/nginx', 'openrc', None), + ('apache2', '/usr/sbin/apache2', 'systemd', None), + ('redis-server', '/usr/bin/redis-server', 'systemd', None), + ('mariadb', '/usr/sbin/mariadbd', 'systemd', None), + ('postgresql', '/usr/lib/postgresql/16/bin/postgres', 'systemd', None), + ('php8.3-fpm', '/usr/sbin/php-fpm8.3', 'systemd', None), + ('vendor-daemon', '/usr/bin/vendor-daemon', 'systemd', + '/usr/lib/systemd/system/vendor-daemon.service'), + ] + for name, exe, manager, source in cases: + with self.subTest(name=name): + self.calls.clear() + result = native.scan(101, self.runner( + service(name=name, exe=exe, manager=manager, source=source))) + self.assertEqual(result['candidates'], []) + self.assertFalse(any(native.FOOTPRINT_SCRIPT in call for call in self.calls)) + def test_missing_remote_qemu_stopped_busy_oci_guards_precede_exec(self): scenarios = ['missing', 'stopped', 'busy', 'oci', 'invalid-oci', 'unmanaged'] for scenario in scenarios: