diff --git a/Makefile.am b/Makefile.am index 09de8d43..2aacfe89 100644 --- a/Makefile.am +++ b/Makefile.am @@ -5,7 +5,7 @@ TEST_FIXTURES = \ src/tests/fixtures/filter-minimal.conf \ src/tests/fixtures/filter-cases.txt \ src/tests/fixtures/broken-filter.conf \ -init/fapolicyd-filter.conf \ +init/data/fapolicyd-filter.conf \ src/tests/fixtures/rules-valid.rules EXTRA_DIST = ChangeLog AUTHORS NEWS README.md INSTALL fapolicyd.spec \ diff --git a/README.md b/README.md index de0b135a..290d0a4a 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ that are designed to work together. They are copied into /etc/fapolicyd/rules.d/ When the service starts, the systemd service file runs fagenrules which assembles the units of rules into a comprehensive policy. The policy is evaluated from top to bottom with the first match winning. You can see the -assembled policy by running +assembled policy by running ``` fapolicyd-cli --list @@ -183,7 +183,7 @@ Policy. But you can do that. It is not recommended to do this except when necessary. Every rule that is added has to potentially be evaluated - which delays the decision. -If you needed to allow admins access to ping, but deny it to everyone +If you needed to allow admins access to ping, but deny it to everyone else, you could do that with the following rules: ``` @@ -246,10 +246,10 @@ The report gives some basic forensic information about what was being accessed. PERFORMANCE ----------- -When a program opens a file or calls execve, that thread has to wait for +When a program opens a file or calls execve, that thread has to wait for fapolicyd to make a decision. To make a decision, fapolicyd has to lookup information about the process and the file being accessed. Each system call -fapolicyd has to make slows down the system. +fapolicyd has to make slows down the system. To speed things up, fapolicyd caches everything it looks up so that subsequent access uses the cache rather than looking things up from @@ -387,7 +387,7 @@ in the lmdb database is 512 bytes. So, for each 4k page, we can have data on 8 trusted files. An ideal size for the database is for the statistics to come up around 75% in -case you decide to install new software some day. The formula is +case you decide to install new software some day. The formula is ``` (db_max_size x percentage in use) / desired percentage = new db_max_size @@ -482,24 +482,24 @@ to debug the policy is: Look at the rule that triggered and see if it makes sense that it triggered. If the rule is a catch all denial, then check if the file is in the trust db. To see the rule that is being triggered, either reproduce the problem with the daemon running in debug-deny mode or change the rules from deny_audit to deny_syslog. If you choose this method, the denials will go into syslog. To see them run: + ``` journalctl -b -u fapolicyd.service ``` + to list out any events since boot by the fapolicyd service. Starting with 1.1, fapolicyd-cli includes some diagnostic capabilities. -| Option | What it does | -|------------------------|--------------------------------------------| -| --check-config | Opens fapolicyd.conf and parses it to see if there are any syntax errors in the file. | -| --check-path | Check that every file in $PATH is in the trustdb. (New in 1.1.5) | -| --check-status | Output internal metrics kept by the daemon. (New in 1.1.4) | -| --check-trustdb | Check the trustdb against the files on disk to look for mismatches that will cause problems at run time. | -| --check-watch_fs | Check the mounted file systems against the watch_fs daemon config entry to determine if any file systems need to be added to the configuration. | -| --check-ignore_mounts | Check the configured mounts that are ignored to see that they are mounted noexec and there are no suspicious files in the partition. (New in 1.4) | -| --test-filter | Test a path to a file against the filter rules to determine if a file will be trusted. (New in 1.3.7) | - - +| Option | What it does | +| :---------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--check-config` | Opens fapolicyd.conf and parses it to see if there are any syntax errors in the file. | +| `--check-path` | Check that every file in `$PATH` is in the trustdb. (New in 1.1.5) | +| `--check-status` | Output internal metrics kept by the daemon. (New in 1.1.4) | +| `--check-trustdb` | Check the trustdb against the files on disk to look for mismatches that will cause problems at run time. | +| `--check-watch_fs` | Check the mounted file systems against the watch_fs daemon config entry to determine if any file systems need to be added to the configuration. | +| `--check-ignore_mounts` | Check the configured mounts that are ignored to see that they are mounted noexec and there are no suspicious files in the partition. (New in 1.4) | +| `--test-filter` | Test a path to a file against the filter rules to determine if a file will be trusted. (New in 1.3.7) | MANAGING TRUST -------------- @@ -615,113 +615,129 @@ FAQ --- 1) Can this work with other distributions? -Absolutely! There is a backend API that any trust source has to implement. -This API is located in fapolicyd-backend.h. A new backend needs an init, load, -and destroy function. So, someone who knows the debian package database, -for example, could implement a new backend and send a pull request. We are -looking for collaborators. + Absolutely! There is a backend API that any trust source has to implement. + This API is located in `fapolicyd-backend.h`. A new backend needs an init, load, + and destroy function. -An initial implementation for Debian distributions has been added. -Run: -``` -cd deb -./build_deb.sh -``` + An initial implementation for Debian distributions has been added, run: + + ``` + cd deb + ./build_deb.sh + ``` + + To build the `.deb` package that uses the `debdb` backend. + You must add rules to `/etc/fapolicyd/rules.d/` and change configuration + in `/etc/fapolicyd/fapolicyd.conf` to use `trust=debdb` after installation. + + Gentoo-based distributions can try using the ebuild backend: -To build the `.deb` package that uses the `debdb` backend. -You must add rules to `/etc/fapolicyd/rules.d/` and change configuration -in `/etc/fapolicyd/fapolicyd.conf` to use `trust=debdb` after installation. + ``` + ./configure --with-ebuild --with-audit + make -j + make install + ``` -Also, if the distribution is very small, you can use the file trust database -file. Just add the places where libraries and applications are stored. + To use the ebuild backend: + + 1. Enable the ebuild backend by adding `trust = ebuilddb` to `/etc/fapolicyd/fapolicyd.conf` + 2. Increase `db_max_size` to 100 or more in `/etc/fapolicyd/fapolicyd.conf` + 3. Copy the example rules to `/etc/fapolicyd/rules.d/` and run `fagenrules` to compile them. + + There is also an ebuild in the Gentoo Repository to simplify installation which + does these things automatically. + + Finally, if the distribution is very small (or in an embedded context), consider using + the trust file database - Just whitelist applications or libraries and their hashes. 2) Can SE Linux or AppArmor do this instead? -SE Linux is modeling how an application behaves. It is not concerned about -where the application came from or whether it's known to the system. Basically, -anything in /bin gets bin_t type by default which is not a very restrictive -label. MAC systems serve a different purpose. Fapolicyd by design cares solely -about if this is a known application/library. These are complimentary security -subsystems. There is more information about application whitelisting use cases -at the following NIST website: + SE Linux is modeling how an application behaves. It is not concerned about + where the application came from or whether it's known to the system. Basically, + anything in /bin gets bin_t type by default which is not a very restrictive + label. MAC systems serve a different purpose. Fapolicyd by design cares solely + about if this is a known application/library. These are complimentary security + subsystems. There is more information about application whitelisting use cases + at the following NIST website: -https://www.nist.gov/publications/guide-application-whitelisting + https://www.nist.gov/publications/guide-application-whitelisting 3) Does the daemon check file integrity? -Version 0.9.5 and later supports 3 modes of integrity checking. The first is -based on file size. In this mode, fapolicyd will take the size information -from the trust db and compare it with the measured file size. This test -incurs no overhead since the file size is collected when establishing -uniqueness for caching purposes. It is intended to detect accidental overwrites -as opposed to malicious activity where the attacker can make the file size -match. + Version 0.9.5 and later supports 3 modes of integrity checking. The first is + based on file size. In this mode, fapolicyd will take the size information + from the trust db and compare it with the measured file size. This test + incurs no overhead since the file size is collected when establishing + uniqueness for caching purposes. It is intended to detect accidental overwrites + as opposed to malicious activity where the attacker can make the file size + match. -The second mode is based on using IMA to calculate sha256 hashes and make them -available through extended attributes. This incurs only the overhead of calling -fgetxattr which is fast since there is no path name resolution. The file system -must support i_version. For XFS, this is enabled by default. For other file -systems, this means you need to add the iversion mount option. In either -case, IMA must be setup appropriately. + The second mode is based on using IMA to calculate sha256 hashes and make them + available through extended attributes. This incurs only the overhead of calling + fgetxattr which is fast since there is no path name resolution. The file system + must support i_version. For XFS, this is enabled by default. For other file + systems, this means you need to add the iversion mount option. In either + case, IMA must be setup appropriately. -The third mode is where fapolicyd calculates a SHA256 hash of the file itself -and compares that with what is stored in the trust db. + The third mode is where fapolicyd calculates a SHA256 hash of the file itself + and compares that with what is stored in the trust db. 4) This is only looking at location. Can't this be defeated by simply moving the files to another location? -Yes, this is checking to see if this is a known file. Known files have a known -location. The shipped policy prevents execution from /tmp, /var/tmp, and $HOME -based on the fact that no rpm package puts anything there. Also, moving a file -means it's no longer "known" and will be blocked from executing. And if -something were moved to overwrite it, then the hash is no longer the same and -that will make it no longer trusted. + Yes, this is checking to see if this is a known file. Known files have a known + location. The shipped policy prevents execution from /tmp, /var/tmp, and $HOME + based on the fact that no rpm package puts anything there. Also, moving a file + means it's no longer "known" and will be blocked from executing. And if + something were moved to overwrite it, then the hash is no longer the same and + that will make it no longer trusted. 5) Does this protect against root modifications? -If you are root, you can change the fapolicyd rules or simply turn off the -daemon. So, this is not designed to prevent root from doing things. None of -the integrity subsystems on Linux are designed to prevent root from doing -things. There has to be a way of doing updates or disabling something for -troubleshooting. For example, you can change IMA to ima_appraise=fix in -/etc/default/grub. You can run setenforce 0 to turn off SELinux. You can also -set selinux=0 or enforcing=0 for the boot prompt. The IPE integrity subsystem -can be turned off via + If you are root, you can change the fapolicyd rules or simply turn off the + daemon. So, this is not designed to prevent root from doing things. None of + the integrity subsystems on Linux are designed to prevent root from doing + things. There has to be a way of doing updates or disabling something for + troubleshooting. For example, you can change IMA to ima_appraise=fix in + /etc/default/grub. You can run setenforce 0 to turn off SELinux. You can also + set selinux=0 or enforcing=0 for the boot prompt. The IPE integrity subsystem + can be turned off via -``` -echo -n 0 > "/sys/kernel/security/ipe/Ex Policy/active" -``` + ``` + echo -n 0 > "/sys/kernel/security/ipe/Ex Policy/active" + ``` -and so on. Since they can all be disabled, the fact that an admin can issue a -service stop command is not a unique weakness. + and so on. Since they can all be disabled, the fact that an admin can issue a + service stop command is not a unique weakness. 6) How do you prevent race conditions on startup? Can something execute before the daemon takes control? -One of the design goals is to take control before users can login. Users are -the main problem being addressed. They can pip install apps to the home dir -or do other things an admin may wish to prevent. Only root can install things -that run before login. And again, root can change the rules or turn off the -daemon. + One of the design goals is to take control before users can login. Users are + the main problem being addressed. They can pip install apps to the home dir + or do other things an admin may wish to prevent. Only root can install things + that run before login. And again, root can change the rules or turn off the + daemon. -Another design goal is to prevent malicious apps from running. Suppose someone -guesses your password and they login to your account. Perhaps they wish to -ransomware your home dir. The app they try to run is not known to the system -and will be stopped. Or suppose there is an exploitable service on your system. -The attacker is lucky enough to pop a shell. Now they want to download -privilege escalation tools or perhaps an LD_PRELOAD key logger. Since neither -of these are in the trust database, they won't be allowed to run. + Another design goal is to prevent malicious apps from running. Suppose someone + guesses your password and they login to your account. Perhaps they wish to + ransomware your home dir. The app they try to run is not known to the system + and will be stopped. Or suppose there is an exploitable service on your system. + The attacker is lucky enough to pop a shell. Now they want to download + privilege escalation tools or perhaps an LD_PRELOAD key logger. Since neither + of these are in the trust database, they won't be allowed to run. -This is really about stopping escalation or exploitation before the attacker -can gain any advantage to install root kits. If we can do that, UEFI secure -boot can make sure no other problems exist during boot. + This is really about stopping escalation or exploitation before the attacker + can gain any advantage to install root kits. If we can do that, UEFI secure + boot can make sure no other problems exist during boot. -Wrt to the second question being asked, fapolicyd starts very early in the -boot process and startup is very fast. It's running well before other login -daemons. + Wrt to the second question being asked, fapolicyd starts very early in the + boot process and startup is very fast. It's running well before other login + daemons. NOTES ----- + * It's highly recommended to run in permissive mode while you are testing the daemon's policy. @@ -742,12 +758,11 @@ file content modifications can occur. * If for some reason rpm database errors are detected, you may need to do the following: -``` -1. db_verify /var/lib/rpm/Packages -if OK, then -2. rm -f /var/lib/rpm/__db* -3. rpm --rebuilddb -``` + ``` + 1. db_verify /var/lib/rpm/Packages + if OK, then + 2. rm -f /var/lib/rpm/__db* + 3. rpm --rebuilddb + ``` [1] - https://git.kernel.org/pub/scm/linux/kernel/git/jack/linux-fs.git/commit/?id=66917a3130f218dcef9eeab4fd11a71cd00cd7c9 - diff --git a/configure.ac b/configure.ac index 8ecd9f8f..e6389f51 100644 --- a/configure.ac +++ b/configure.ac @@ -150,7 +150,21 @@ if test x$use_deb = xyes ; then fi AM_CONDITIONAL(WITH_DEB, test x$use_deb = xyes) -AM_CONDITIONAL(NEED_MD5, test x$use_deb = xyes) +withval="" +AC_ARG_WITH(ebuild, +AS_HELP_STRING([--with-ebuild],[Use the ebuild database as a trust source]), +use_ebuild=$withval,use_ebuild=no) + +if test x$use_ebuild = xyes ; then + AC_DEFINE(USE_EBUILD,1,[Define if you want to use the ebuild database as trust source.]) + AC_CHECK_LIB(md, MD5Final, , [AC_MSG_ERROR([libmd is missing])], -lmd) +fi +AM_CONDITIONAL(WITH_EBUILD, test x$use_ebuild = xyes) + +if test x$use_deb = xyes || test x$use_ebuild = xyes ; then + AC_DEFINE(NEED_MD5, 1, [Define if MD5 hashing is needed]) +fi +AM_CONDITIONAL(NEED_MD5, test x$use_deb = xyes || test x$use_ebuild = xyes) dnl FIXME some day pass this on the command line def_systemdsystemunitdir=${prefix}/lib/systemd/system diff --git a/fapolicyd.spec b/fapolicyd.spec index 055d08a3..af6df5dd 100644 --- a/fapolicyd.spec +++ b/fapolicyd.spec @@ -250,11 +250,13 @@ fi %doc README.md %{!?_licensedir:%global license %%doc} %license COPYING -%attr(755,root,root) %dir %{_datadir}/%{name} -%attr(755,root,root) %dir %{_datadir}/%{name}/sample-rules -%attr(644,root,root) %{_datadir}/%{name}/default-ruleset.known-libs -%attr(644,root,root) %{_datadir}/%{name}/sample-rules/* -%attr(644,root,root) %{_datadir}/%{name}/fapolicyd-magic.mgc +%attr(755,root,%{name}) %dir %{_datadir}/%{name} +%attr(755,root,%{name}) %dir %{_datadir}/%{name}/sample-rules +%attr(644,root,%{name}) %{_datadir}/%{name}/default-ruleset.known-libs +%attr(644,root,%{name}) %{_datadir}/%{name}/sample-rules/* +%attr(644,root,%{name}) %{_datadir}/%{name}/fapolicyd-magic.mgc +%exclude %{_sysconfdir}/init.d/%{name} +%exclude %{_sysconfdir}/conf.d/%{name} %attr(750,root,%{name}) %dir %{_sysconfdir}/%{name} %attr(750,root,%{name}) %dir %{_sysconfdir}/%{name}/trust.d %attr(750,root,%{name}) %dir %{_sysconfdir}/%{name}/rules.d diff --git a/init/Makefile.am b/init/Makefile.am index 45731537..8e7b5ba2 100644 --- a/init/Makefile.am +++ b/init/Makefile.am @@ -1,8 +1,10 @@ EXTRA_DIST = \ - fapolicyd.service \ - fapolicyd.conf \ - fapolicyd-filter.conf \ - fapolicyd.trust \ + data/fapolicyd-filter.conf \ + data/fapolicyd.conf \ + data/fapolicyd.trust \ + openrc/conf.d/fapolicyd \ + openrc/init.d/fapolicyd \ + systemd/fapolicyd.service \ fapolicyd-tmpfiles.conf \ fapolicyd-magic \ fapolicyd.bash_completion \ @@ -11,12 +13,17 @@ EXTRA_DIST = \ fapolicyddir = $(sysconfdir)/fapolicyd dist_fapolicyd_DATA = \ - fapolicyd.conf \ - fapolicyd-filter.conf \ - fapolicyd.trust + data/fapolicyd.conf \ + data/fapolicyd-filter.conf \ + data/fapolicyd.trust systemdservicedir = $(systemdsystemunitdir) -dist_systemdservice_DATA = fapolicyd.service +dist_systemdservice_DATA = systemd/fapolicyd.service + +openrcinitdir = $(sysconfdir)/init.d +dist_openrcinit_DATA = openrc/init.d/fapolicyd +openrcconfdir = $(sysconfdir)/conf.d +dist_openrcconf_DATA = openrc/conf.d/fapolicyd sbin_SCRIPTS = fagenrules @@ -24,7 +31,7 @@ completiondir = $(sysconfdir)/bash_completion.d/ dist_completion_DATA = fapolicyd.bash_completion MAGIC = fapolicyd-magic.mgc -pkgdata_DATA = ${MAGIC} +pkgdata_DATA = ${MAGIC} CLEANFILES = ${MAGIC} ${MAGIC}: $(EXTRA_DIST) diff --git a/init/fapolicyd-filter.conf b/init/data/fapolicyd-filter.conf similarity index 100% rename from init/fapolicyd-filter.conf rename to init/data/fapolicyd-filter.conf diff --git a/init/fapolicyd.conf b/init/data/fapolicyd.conf similarity index 100% rename from init/fapolicyd.conf rename to init/data/fapolicyd.conf diff --git a/init/fapolicyd.trust b/init/data/fapolicyd.trust similarity index 100% rename from init/fapolicyd.trust rename to init/data/fapolicyd.trust diff --git a/init/fapolicyd-magic b/init/fapolicyd-magic index 57d11af4..1cc05eda 100644 --- a/init/fapolicyd-magic +++ b/init/fapolicyd-magic @@ -13,7 +13,7 @@ 0 string/wt #!\ /bin/rc Plan 9 shell script text executable !:mime text/x-plan9-shellscript -0 string/wb #!\ /usr/bin/ocamlrun Ocaml byte-compiled executable +0 string/wb #!\ /usr/bin/ocamlrun Ocaml byte-compiled executable !:mime application/x-bytecode.ocaml 0 string/wt #!\ /usr/bin/lua Lua script text executable diff --git a/init/openrc/conf.d/fapolicyd b/init/openrc/conf.d/fapolicyd new file mode 100644 index 00000000..57072a37 --- /dev/null +++ b/init/openrc/conf.d/fapolicyd @@ -0,0 +1 @@ +fapolicyd_opts="--permissive --debug" diff --git a/init/openrc/init.d/fapolicyd b/init/openrc/init.d/fapolicyd new file mode 100644 index 00000000..c9c52114 --- /dev/null +++ b/init/openrc/init.d/fapolicyd @@ -0,0 +1,19 @@ +#!/sbin/openrc-run + +name=$RC_SVCNAME +cfgfile="/etc/$RC_SVCNAME/$RC_SVCNAME.conf" +command="/usr/sbin/fapolicyd" +command_args="${fapolicyd_opts}" +command_user="fapolicyd" +pidfile="/run/$RC_SVCNAME/$RC_SVCNAME.pid" + +# Depend on local disks being mounted +depend() { + need localmount +} + +# Before starting the service update the rulesfile in /etc/fapolicyd +# from the fragments in /etc/fapolicyd/rules.d +start_pre() { + /usr/sbin/fagenrules +} diff --git a/init/fapolicyd.service b/init/systemd/fapolicyd.service similarity index 100% rename from init/fapolicyd.service rename to init/systemd/fapolicyd.service diff --git a/meson.build b/meson.build new file mode 100644 index 00000000..b092474b --- /dev/null +++ b/meson.build @@ -0,0 +1,560 @@ +project( + 'fapolicyd', + 'c', + version: '1.4.3', + default_options: [ + 'c_std=gnu11', + 'warning_level=2', + 'buildtype=debugoptimized', + 'b_pie=true', + ], + meson_version: '>= 1.1.0', # meson.options +) + +fs = import('fs') + +cc = meson.get_compiler('c') + +# Configuration +conf_data = configuration_data() +conf_data.set_quoted('VERSION', meson.project_version()) +conf_data.set('_GNU_SOURCE', 1) +conf_data.set('PIE', 1) +conf_data.set('STDC_HEADERS', 1) + +if get_option('debug') + conf_data.set('DEBUG', 1) +endif + +# Standard headers that autotools checks for +check_std_headers = [ + 'dlfcn.h', + 'inttypes.h', + 'minix/config.h', + 'stdint.h', + 'stdio.h', + 'stdlib.h', + 'strings.h', + 'string.h', + 'sys/stat.h', + 'sys/types.h', + 'unistd.h', + 'wchar.h', +] + +foreach h : check_std_headers + if cc.has_header(h) + conf_data.set('HAVE_' + h.to_upper().underscorify(), 1) + endif +endforeach + +# Check for headers +check_headers = ['sys/fanotify.h', 'uthash.h', 'linux/fanotify.h'] + +foreach h : check_headers + if not cc.has_header(h) + error('Header ' + h + ' not found') + endif +endforeach + +# Check for functions +check_funcs = ['fexecve', 'gettid', 'mallinfo2'] + +foreach f : check_funcs + if cc.has_function(f) + conf_data.set('HAVE_' + f.to_upper(), 1) + endif +endforeach + +# Check for struct members +if cc.has_member( + 'struct fanotify_response_info_audit_rule', + 'rule_number', + prefix: '#include ', +) + conf_data.set('FAN_AUDIT_RULE_NUM', 1) +endif + +# Check for declarations +check_decls = [ + ['FAN_AUDIT', 'linux/fanotify.h'], + ['FAN_OPEN_EXEC_PERM', 'linux/fanotify.h'], + ['FAN_MARK_FILESYSTEM', 'linux/fanotify.h'], +] + +foreach d : check_decls + if cc.has_header_symbol(d[1], d[0]) + conf_data.set('HAVE_DECL_' + d[0], 1) + endif + # Special check for FAN_OPEN_EXEC_PERM as it is required + if d[0] == 'FAN_OPEN_EXEC_PERM' and not cc.has_header_symbol(d[1], d[0]) + error( + d[0] + ' is not defined in ' + d[1] + '. It is required for the kernel to support it', + ) + endif +endforeach + +# Dependencies + +deps = [] +deps_summary = {} + +foreach dep : [ + 'libcap-ng', + 'libcrypto', + 'lmdb', + 'libmagic', + 'libseccomp', + 'libudev', + 'threads', +] + d = dependency(dep, required: true) + deps += d + deps_summary += {dep: d} +endforeach + +# Options +if get_option('audit') + conf_data.set('USE_AUDIT', 1) +endif + +librpm = dependency('rpm', required: get_option('rpm')) + +if librpm.found() + deps += [librpm] + conf_data.set('USE_RPM', 1) + conf_data.set('HAVE_LIBRPM', 1) +endif + + +libdpkg = dependency('libdpkg', required: get_option('deb')) + +if libdpkg.found() + deps += libdpkg + conf_data.set('USE_DEB', 1) + conf_data.set('LIBDPKG_VOLATILE_API', 1) +endif + +use_ebuild = get_option('ebuild').allowed() # no dependency to check for found +if use_ebuild + conf_data.set('USE_EBUILD', 1) +endif + +if libdpkg.found() or use_ebuild + conf_data.set('NEED_MD5', 1) +endif + +ld_so_path = get_option('ld_so_path') + +if ld_so_path != '' + message('Using user-specified dynamic linker: ' + ld_so_path) + +else + + message('Auto-detecting dynamic linker...') + + python = import('python').find_installation() + bash = find_program('bash') + + elf_tool = find_program('readelf', required: false) + tool_mode = 'readelf' + + if not elf_tool.found() + elf_tool = find_program('objdump', required: false) + tool_mode = 'objdump' + endif + + if not elf_tool.found() + error( + 'Found neither readelf nor objdump. Please set -Dsystem_ld_so=/path/to/loader manually.', + ) + endif + + get_interp_script = ''' +import sys, subprocess + +tool_bin = sys.argv[1] +mode = sys.argv[2] +target_bin = sys.argv[3] + +try: + if mode == 'readelf': + output = subprocess.check_output([tool_bin, '-p', '.interp', target_bin], text=True) + # Extract the string from the dump format + for line in output.split('\\n'): + if ']' in line and line.strip() and not 'Hex' in line: + # Get the ASCII part after the closing bracket + part = line.split(']')[1].strip() + if part: + print(part.rstrip('\\x00'), end='') + sys.exit(0) + sys.exit(1) + elif mode == 'objdump': + output = subprocess.check_output([tool_bin, '-s', '-j', '.interp', target_bin], text=True) + hex_data = [] + for line in output.split('\\n'): + if line.strip().startswith('0'): + parts = line.split() + # First part is address, last part is ASCII, middle are hex + hex_parts = parts[1:-1] if len(parts) > 2 else parts[1:] + hex_data.extend(hex_parts) + + if hex_data: + hex_string = ''.join(hex_data) + result = bytes.fromhex(hex_string).decode('ascii', errors='ignore').rstrip('\\x00') + print(result, end='') + else: + sys.exit(1) +except Exception: + sys.exit(1) +''' + + interp_cmd = run_command( + python, + '-c', + get_interp_script, + elf_tool.full_path(), + tool_mode, + bash.full_path(), + check: true, + ) + + ld_so_path = interp_cmd.stdout() + + if not fs.exists(ld_so_path) or ld_so_path == '' + error('Computed dynamic linker does not exist: ' + ld_so_path) + endif + + message('Auto-detected dynamic linker (' + tool_mode + '): ' + ld_so_path) +endif + +conf_data.set_quoted('SYSTEM_LD_SO', ld_so_path) + +# Compiler flags +project_args = [ + '-D_GNU_SOURCE', + '-Wshadow', + '-Wundef', + '-Wno-unused-result', + '-Wno-unused-parameter', +] + +if libdpkg.found() + project_args += '-DLIBDPKG_VOLATILE_API' +endif + +add_project_arguments(project_args, language: 'c') + +# Generate config.h +configure_file(output: 'config.h', configuration: conf_data) + +# Include directories +inc = include_directories('.', 'src', 'src/library') + +# Source files +lib_sources = files( + 'src/library/attr-sets.c', + 'src/library/avl.c', + 'src/library/backend-manager.c', + 'src/library/daemon-config.c', + 'src/library/database.c', + 'src/library/escape.c', + 'src/library/event.c', + 'src/library/fd-fgets.c', + 'src/library/file-backend.c', + 'src/library/file.c', + 'src/library/filter.c', + 'src/library/llist.c', + 'src/library/lru.c', + 'src/library/message.c', + 'src/library/object-attr.c', + 'src/library/object.c', + 'src/library/policy.c', + 'src/library/process.c', + 'src/library/queue.c', + 'src/library/rules.c', + 'src/library/stack.c', + 'src/library/string-util.c', + 'src/library/subject-attr.c', + 'src/library/subject.c', + 'src/library/trust-file.c', +) + +if librpm.found() + lib_sources += files('src/library/rpm-backend.c') +endif + +if libdpkg.found() or use_ebuild + lib_sources += files('src/library/md5-backend.c') +endif + +if libdpkg.found() + lib_sources += files('src/library/deb-backend.c') +endif + +if use_ebuild + lib_sources += files('src/library/ebuild-backend.c') +endif + +# Library +libfapolicyd = static_library( + 'fapolicyd', + lib_sources, + include_directories: inc, + dependencies: deps, +) + +# Executables +executable( + 'fapolicyd', + ['src/daemon/fapolicyd.c', 'src/daemon/mounts.c', 'src/daemon/notify.c'], + include_directories: inc, + link_with: libfapolicyd, + dependencies: deps, + install: true, + install_dir: get_option('sbindir'), +) + +executable( + 'fapolicyd-cli', + ['src/cli/fapolicyd-cli.c', 'src/cli/file-cli.c'], + include_directories: inc, + link_with: libfapolicyd, + dependencies: deps, + install: true, + install_dir: get_option('sbindir'), +) + +if librpm.found() + executable( + 'fapolicyd-rpm-loader', + ['src/handler/fapolicyd-rpm-loader.c'], + include_directories: inc, + link_with: libfapolicyd, + dependencies: deps, + install: true, + install_dir: get_option('bindir'), + ) +endif + +# Man pages +man_pages = files( + 'doc/fagenrules.8', + 'doc/fapolicyd-cli.8', + 'doc/fapolicyd-filter.conf.5', + 'doc/fapolicyd.8', + 'doc/fapolicyd.conf.5', + 'doc/fapolicyd.rules.5', + 'doc/fapolicyd.trust.5', + 'doc/rpm-filter.conf.5', +) + +install_man(man_pages) + +# Rules +install_subdir( + 'rules.d', + install_dir: get_option('sysconfdir') / 'fapolicyd', + strip_directory: true, + exclude_files: ['Makefile', 'Makefile.am', 'Makefile.in'], +) + +# Init scripts +install_data( + 'init/systemd/fapolicyd.service', + install_dir: get_option('prefix') / 'lib/systemd/system', +) + + +install_data( + 'init/fapolicyd-tmpfiles.conf', + install_dir: get_option('prefix') / 'lib/tmpfiles.d', + rename: 'fapolicyd.conf', +) + +install_data( + 'init/openrc/init.d/fapolicyd', + install_dir: '/etc/init.d', + install_mode: 'rwxr-xr-x', +) +install_data('init/openrc/conf.d/fapolicyd', install_dir: '/etc/conf.d') + + +# Config files +install_data( + 'init/data/fapolicyd.conf', + install_dir: get_option('sysconfdir') / 'fapolicyd', +) +install_data( + 'init/data/fapolicyd-filter.conf', + install_dir: get_option('sysconfdir') / 'fapolicyd', +) +install_data( + 'init/data/fapolicyd.trust', + install_dir: get_option('sysconfdir') / 'fapolicyd', +) + +# Magic file compilation +file_prog = find_program('file', required: true) +magic_src = files('init/fapolicyd-magic') + +custom_target( + 'fapolicyd-magic.mgc', + input: magic_src, + output: 'fapolicyd-magic.mgc', + command: [file_prog, '-C', '-m', '@INPUT@'], + install: true, + install_dir: get_option('datadir') / 'fapolicyd', +) + +# Scripts +install_data( + 'init/fagenrules', + install_dir: get_option('sbindir'), + install_mode: 'rwxr-xr-x', +) + +install_data( + 'init/fapolicyd.bash_completion', + install_dir: get_option('sysconfdir') / 'bash_completion.d', +) + +# Tests +test_inc = include_directories('src/library') + +# Standalone tests +standalone_tests = [ + ['avl_test', ['src/tests/avl_test.c', 'src/library/avl.c']], + ['trustdb_format_test', ['src/tests/trustdb_format_test.c']], +] + +foreach t : standalone_tests + exe = executable( + t[0], + t[1], + include_directories: test_inc, + dependencies: deps, + ) + test(t[0], exe) +endforeach + +# Tests linking libfapolicyd +linked_tests = [ + ['gid_proc_test', ['src/tests/gid_proc_test.c']], + ['uid_proc_test', ['src/tests/uid_proc_test.c']], + ['escape_test', ['src/tests/escape_test.c']], + ['attr_sets_test', ['src/tests/attr_sets_test.c']], + ['elf_file_test', ['src/tests/elf_file_test.c']], + ['fd_fgets_test', ['src/tests/fd_fgets_test.c']], +] + +if use_ebuild + linked_tests += [['ebuild_test', ['src/tests/ebuild_test.c']]] +endif + +if libdpkg.found() + linked_tests += [['deb_test', ['src/tests/deb_test.c']]] +endif + +foreach t : linked_tests + exe = executable( + t[0], + t[1], + include_directories: test_inc, + link_with: libfapolicyd, + dependencies: deps, + ) + test( + t[0], + exe, + env: {'srcdir': meson.current_source_dir() / 'src/tests'}, + ) +endforeach + +# rules_test needs special define +rules_test_exe = executable( + 'rules_test', + ['src/tests/rules_test.c'], + include_directories: test_inc, + link_with: libfapolicyd, + dependencies: deps, + c_args: '-DTEST_BASE="' + meson.current_source_dir() + '"', +) +test('rules_test', rules_test_exe) + +event_test_exe = executable( + 'event_test', + ['src/tests/event_test.c'], + include_directories: test_inc, + link_with: libfapolicyd, + dependencies: deps, +) +test('event_test', event_test_exe) + +# filter_test needs special define +filter_test_exe = executable( + 'filter_test', + ['src/tests/filter_test.c'], + include_directories: test_inc, + link_with: libfapolicyd, + dependencies: deps, + c_args: '-DTEST_BASE="' + meson.current_source_dir() + '"', +) +test('filter_test', filter_test_exe) + +# file_filter_test needs special define +file_filter_test_exe = executable( + 'file_filter_test', + ['src/tests/file_filter_test.c'], + include_directories: test_inc, + link_with: libfapolicyd, + dependencies: deps, + c_args: '-DTEST_BASE="' + meson.current_source_dir() + '"', +) +test('file_filter_test', file_filter_test_exe) + +summary( + { + 'prefix': get_option('prefix'), + 'bindir': get_option('bindir'), + 'datadir': get_option('datadir'), + 'libdir': get_option('libdir'), + 'libexecdir': get_option('libexecdir'), + }, + section: 'Directories', +) + +environment_summary = { + 'Build': build_machine.system(), + 'Build CPU Family': build_machine.cpu_family(), + 'Host CPU Family': host_machine.cpu_family(), + 'Cross-compiling': meson.is_cross_build(), + 'Build Endianness': build_machine.endian(), + 'Host Endianness': host_machine.endian(), + 'Target': host_machine.system(), + 'C Compiler': cc.get_id(), + 'C Compiler Version': cc.version(), + 'Linker': cc.get_linker_id(), +} + +summary( + { + 'Audit support': get_option('audit'), + 'RPM backend': librpm.found(), + 'Debian backend': libdpkg.found(), + 'Ebuild backend': use_ebuild, + 'ld.so path': ld_so_path, + }, + section: 'Configuration', + bool_yn: true, +) + +if librpm.found() + deps_summary += {'rpm': librpm} +endif + +if libdpkg.found() + deps_summary += {'libdpkg': libdpkg} +endif + +summary(deps_summary, section: 'Dependencies', bool_yn: true) diff --git a/meson.options b/meson.options new file mode 100644 index 00000000..358b1725 --- /dev/null +++ b/meson.options @@ -0,0 +1,29 @@ +option( + 'audit', + type: 'boolean', + value: false, + description: 'Enable decision auditing', +) +option( + 'rpm', + type: 'feature', + value: 'enabled', + description: 'Enable RPM backend', +) +option( + 'deb', + type: 'feature', + value: 'disabled', + description: 'Enable Debian backend', +) +option( + 'ebuild', + type: 'feature', + value: 'disabled', + description: 'Enable Ebuild backend', +) +option( + 'ld_so_path', + type: 'string', + description: 'Ignore detection and use this path to the dynamic linker/loader', +) diff --git a/src/Makefile.am b/src/Makefile.am index e8fe350c..7bb61fc6 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -87,6 +87,12 @@ fapolicyd_rpm_loader_LDFLAGS = $(fapolicyd_LDFLAGS) fapolicyd_rpm_loader_LDADD = libfapolicyd.la endif +if NEED_MD5 +libfapolicyd_la_SOURCES += \ + library/md5-backend.c \ + library/md5-backend.h +endif + if WITH_DEB libfapolicyd_la_SOURCES += library/deb-backend.c libfapolicyd_la_LIBADD = -ldpkg -lmd @@ -94,12 +100,15 @@ fapolicyd_CFLAGS += -DLIBDPKG_VOLATILE_API LIBS += -ldpkg -lmd endif -if NEED_MD5 -libfapolicyd_la_SOURCES += \ - library/md5-backend.c \ - library/md5-backend.h +if WITH_EBUILD +libfapolicyd_la_SOURCES += library/ebuild-backend.c \ + library/filter.c \ + library/filter.h endif +fapolicyd_cli_CFLAGS = $(fapolicyd_CFLAGS) +fapolicyd_cli_LDFLAGS = $(fapolicyd_LDFLAGS) + libfapolicyd_la_CFLAGS = $(fapolicyd_CFLAGS) libfapolicyd_la_LDFLAGS = $(fapolicyd_LDFLAGS) -lpthread diff --git a/src/daemon/notify.c b/src/daemon/notify.c index 56b31aa2..b2585e5a 100644 --- a/src/daemon/notify.c +++ b/src/daemon/notify.c @@ -278,6 +278,7 @@ void shutdown_fanotify(mlist *m) // Clean up q_close(q); + q = NULL; close(rpt_timer_fd); close(fd); @@ -288,7 +289,8 @@ void shutdown_fanotify(mlist *m) void nudge_queue(void) { - q_shutdown(q); + if (q) + q_shutdown(q); } void decision_report(FILE *f) @@ -524,4 +526,3 @@ void handle_events(void) metadata = FAN_EVENT_NEXT(metadata, len); } } - diff --git a/src/library/backend-manager.c b/src/library/backend-manager.c index 130510ea..f90dad2c 100644 --- a/src/library/backend-manager.c +++ b/src/library/backend-manager.c @@ -41,6 +41,9 @@ extern backend rpm_backend; #ifdef USE_DEB extern backend deb_backend; #endif +#ifdef USE_EBUILD +extern backend ebuild_backend; +#endif static backend* compiled[] = { @@ -50,6 +53,9 @@ static backend* compiled[] = #endif #ifdef USE_DEB &deb_backend, +#endif +#ifdef USE_EBUILD + &ebuild_backend, #endif NULL, }; @@ -176,4 +182,3 @@ backend_entry* backend_get_first(void) { return backends; } - diff --git a/src/library/database.c b/src/library/database.c index e6769935..7108ea89 100644 --- a/src/library/database.c +++ b/src/library/database.c @@ -35,7 +35,7 @@ #include #include #include -#include +#include #include #include #include @@ -133,6 +133,8 @@ const char *lookup_tsource(unsigned int tsource) return "rpmdb"; case SRC_DEB: return "debdb"; + case SRC_EBUILD: + return "ebuilddb"; case SRC_FILE_DB: return "filedb"; } @@ -618,11 +620,32 @@ static char *path_to_hash(const char *path, const size_t path_len) { unsigned char hptr[80]; char *digest; + unsigned int md_len; + EVP_MD_CTX *mdctx; if (path_len == 0) return NULL; - SHA512((unsigned char *)path, path_len, (unsigned char *)&hptr); + mdctx = EVP_MD_CTX_new(); + if (mdctx == NULL) + return NULL; + + if (EVP_DigestInit_ex(mdctx, EVP_sha512(), NULL) != 1) { + EVP_MD_CTX_free(mdctx); + return NULL; + } + + if (EVP_DigestUpdate(mdctx, path, path_len) != 1) { + EVP_MD_CTX_free(mdctx); + return NULL; + } + + if (EVP_DigestFinal_ex(mdctx, hptr, &md_len) != 1) { + EVP_MD_CTX_free(mdctx); + return NULL; + } + EVP_MD_CTX_free(mdctx); + digest = malloc((SHA512_LEN * 2) + 1); if (digest == NULL) return digest; diff --git a/src/library/ebuild-backend.c b/src/library/ebuild-backend.c new file mode 100644 index 00000000..1faac0eb --- /dev/null +++ b/src/library/ebuild-backend.c @@ -0,0 +1,570 @@ +/** + * @file ebuild-backend.c + * @brief Implementation of the ebuild backend for fapolicyd. + * + * This file contains the implementation of the ebuild backend for fapolicyd. + * The ebuild backend is responsible for loading the list of installed packages + * and their corresponding files and directories from the VDB (/var/db/pkg/). + * It parses the CONTENTS file of each package and extracts the information + * about the installed files, including their paths, MD5 checksums, and modification timestamps. + * + * The ebuild_load_list function is the entry point for loading the package list. + * It takes a pointer to the conf_t structure, which contains the configuration options + * for fapolicyd, and returns an integer indicating the success or failure of the operation. + */ + +#include "config.h" // for DEBUG +#include // for dirent, closedir, opendir, DIR, readdir +#include // for errno +#include // for NULL, perror, asprintf, getline, fopen +#include // for free, malloc, abort, reallocarray +#include // for strcmp, strdup, strlen, strtok_r, strcat +#include // for stat, fstatat, S_ISDIR, S_ISREG +#include // for LOG_ERR, LOG_DEBUG, LOG_INFO +#include // for atomic_bool +#include // for memfd_create, MFD_CLOEXEC, MFD_ALLOW_SEALING +#include // for fcntl, F_ADD_SEALS, F_SEAL_SHRINK +#include // for close +#include "conf.h" // for conf_t +#include "fapolicyd-backend.h" // for SRC_EBUILD, backend +#include "filter.h" // for filter_destroy, filter_init, filter_l... +#include "llist.h" // for list_empty, list_init +#include "md5-backend.h" // for add_file_to_backend_by_md5 +#include "message.h" // for msg + +#ifndef VDB_PATH +#define VDB_PATH "/var/db/pkg" +#endif + +extern atomic_bool stop; + +static const char *get_vdb_path(void) { + const char *path = getenv("FAPOLICYD_VDB_PATH"); + if (path) return path; + return VDB_PATH; +} + +static const char kEbuildBackend[] = "ebuilddb"; + +static int ebuild_init_backend(void); +static int ebuild_load_list(const conf_t *); +static int ebuild_destroy_backend(void); + +backend ebuild_backend = { + kEbuildBackend, + ebuild_init_backend, + ebuild_load_list, + ebuild_destroy_backend, + -1, + -1, +}; + +/* + * Collection of paths and MD5s for a package + */ +typedef struct contents { + char *md5; + char *path; +} ebuildfiles; + +/* + * Struct that contains the information we need about a package + */ +struct epkg { + char *cpv; + char *slot; + char *repo; + int files; + ebuildfiles *content; +}; + +/* + * Holds the category name and package name while we recurse + */ +typedef struct { + char *category; + char *package; +} PackageData; + +/* + * Remove the trailing newline from a string + * + * This function takes a string as input and removes the trailing newline character, if present. + * It modifies the input string in-place and returns a pointer to the modified string. + * + * @param string - The input string to remove the trailing newline from + * @return A pointer to the modified string + */ +char* remove_newline(char* string) { + int len = strlen(string); + if (len > 0 && string[len-1] == '\n') { + string[len-1] = '\0'; + } + return string; +} + + +/** + * Recursively process a directory + * + * This function takes a directory pointer and a function pointer as input. + * It processes the directory based on the provided function pointer. + * + * @param dir The directory pointer to be processed. + * @param process_entry The function pointer that defines how each entry in the directory should be processed. + * @param packages A pointer to an integer that will store the number of packages processed. + * @param capacity A pointer to an integer that will store the current capacity of the packages array. + * @param vdbpackages A pointer to an array of struct epkg pointers that will store the processed packages. + * @param data A pointer to a PackageData pointer that will store additional data related to the processed packages. + * @return The updated array of struct epkg pointers representing the processed packages. + */ +struct epkg** process_directory(DIR *dir, struct epkg** (*process_entry)(struct dirent *, int *, int *, struct epkg **, PackageData **), + int *packages, int *capacity, struct epkg **vdbpackages, PackageData **data) { + struct dirent *dp; + int dir_fd = dirfd(dir); + while ((dp = readdir(dir)) != NULL) { + if (stop) + break; + + if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0) + continue; + + unsigned char d_type = dp->d_type; + + if (d_type == DT_UNKNOWN) { + struct stat sb; + if (fstatat(dir_fd, dp->d_name, &sb, 0) == 0) { + if (S_ISDIR(sb.st_mode)) { + d_type = DT_DIR; + } else if (S_ISREG(sb.st_mode)) { + d_type = DT_REG; + } + } + } + + if (d_type == DT_DIR || d_type == DT_REG) { + vdbpackages = process_entry(dp, packages, capacity, vdbpackages, data); + } + } + + return vdbpackages; +} + + +/* + * Read and process SLOT, repository, CONTENTS from a VDB package directory + * CATEGORY and PF are already known, but could be read at this stage + * + * @param packages A pointer to an integer representing the number of packages + * @param capacity A pointer to an integer representing the capacity of the packages array + * @param vdbpackages An array of pointers to struct epkg representing VDB packages + * @param data A pointer to a pointer to PackageData struct representing package data + * + * @return The updated array of pointers to struct epkg representing processed packages. + * If an error occurs or the operation is stopped, the function cleans up + * locally allocated memory and returns the original (or potentially resized) array. + */ +struct epkg** process_pkgdir(int *packages, int *capacity, struct epkg **vdbpackages, PackageData **data) { + char *pkgrepo = NULL; + char *pkgslot = NULL; + int pkgfiles = 0; + int pkgfiles_capacity = 0; + ebuildfiles* pkgcontents = NULL; + + char *filenames[] = {"repository", "SLOT", "CONTENTS"}; + int nfilenames = sizeof(filenames) / sizeof(filenames[0]); + + + for (int i = 0; i < nfilenames; i++) { + if (stop) + goto cleanup; + + #ifdef DEBUG + msg(LOG_DEBUG, "\tProcessing %s", filenames[i]); + #endif + char *filepath; + if (asprintf(&filepath, "%s/%s/%s/%s", get_vdb_path(), (*data)->category, (*data)->package, filenames[i]) == -1) { + perror("asprintf"); + filepath = NULL; + } + if (filepath) { + FILE *fp; + char *line = NULL; + size_t len = 0; + ssize_t read; + if ((fp = fopen(filepath, "r")) == NULL) { + msg(LOG_ERR, "Could not open %s", filepath); + free(filepath); + goto cleanup; + } + + if (strcmp(filenames[i], "SLOT") == 0 || strcmp(filenames[i], "repository") == 0) { + // SLOT and repository will only ever contain a single line + if ((read = getline(&line, &len, fp)) != -1) { + if (strcmp(filenames[i], "SLOT") == 0) { + pkgslot = strdup(line); + remove_newline(pkgslot); + #ifdef DEBUG + msg(LOG_DEBUG, "\t\tslot: %s", pkgslot); + #endif + } else if (strcmp(filenames[i], "repository") == 0) { + pkgrepo = strdup(line); + remove_newline(pkgrepo); + #ifdef DEBUG + msg(LOG_DEBUG, "\t\trepo: %s", pkgrepo); + #endif + } + } + } else if (strcmp(filenames[i], "CONTENTS") == 0) { + while ((read = getline(&line, &len, fp)) != -1) { + // Format: type path md5 timestamp + // e.g. obj /usr/bin/clamscan 3ade185bd024e29880e959e6ad187515 1693552964 + + // Parse from right to left - there might be spaces in the path + // Remove trailing newline + if (read > 0 && line[read - 1] == '\n') { + line[read - 1] = '\0'; + read--; + } + + if (strncmp(line, "obj ", 4) != 0) { + continue; + } + + // Find the last space (before timestamp) + char *last_space = strrchr(line, ' '); + if (!last_space) continue; + *last_space = '\0'; + + // Find the space before that (before md5) + char *md5_space = strrchr(line, ' '); + if (!md5_space) continue; + *md5_space = '\0'; + + char *path_start = line + 4; // Skip "obj " + char *md5_start = md5_space + 1; + + ebuildfiles file; + file.path = strdup(path_start); + file.md5 = strdup(md5_start); + + if (!file.path || !file.md5) { + msg(LOG_ERR, "Memory allocation failed"); + abort(); + } + + if (pkgfiles >= pkgfiles_capacity) { + int new_capacity = (pkgfiles_capacity == 0) ? 16 : pkgfiles_capacity * 2; + ebuildfiles *newpkgcontents = reallocarray(pkgcontents, new_capacity, sizeof(ebuildfiles)); + if (newpkgcontents == NULL) { + abort(); + } + pkgcontents = newpkgcontents; + pkgfiles_capacity = new_capacity; + } + pkgcontents[pkgfiles] = file; + pkgfiles++; + } + #ifdef DEBUG + msg(LOG_DEBUG, "\t\tfiles: %i", pkgfiles); + #endif + } + + free(line); + fclose(fp); + free(filepath); + } + } + + // Construct a CPVR string e.g. dev-libs/libxml2-2.9.10{-r0} + // We're not processing based on this information, but it's useful for logging + // If there's a need to split into components see + // https://github.com/gentoo/portage-utils/blob/master/libq/atom.c + char *catpkgver = malloc(strlen((*data)->category) + strlen((*data)->package) + 2); + if (catpkgver == NULL) { + msg(LOG_ERR, "Could not allocate memory."); + perror("malloc"); + goto cleanup; + } + strcpy(catpkgver, (*data)->category); + strcat(catpkgver, "/"); + strcat(catpkgver, (*data)->package); + + // make a new package + struct epkg *package = malloc(sizeof(struct epkg)); + if (package == NULL) { + msg(LOG_ERR, "Could not allocate memory."); + free(catpkgver); + goto cleanup; + } + package->cpv = catpkgver; + package->slot = pkgslot; + package->repo = pkgrepo; + package->files = pkgfiles; + package->content = pkgcontents; + + #ifdef DEBUG + msg(LOG_DEBUG, "Stored:\n\tPackage: %s\n\tSlot: %s\n\tRepo: %s\n\tFiles: %i", + package->cpv, package->slot, package->repo, package->files); + msg(LOG_DEBUG, "Package number %i", *packages + 1); + #endif + + if (*packages >= *capacity) { + int new_capacity = (*capacity == 0) ? 16 : (*capacity) * 2; + struct epkg** expanded_vdbpackages = reallocarray(vdbpackages, new_capacity, sizeof(struct epkg *)); + if(expanded_vdbpackages == NULL) { + msg(LOG_ERR, "Could not allocate memory."); + abort(); + } + vdbpackages = expanded_vdbpackages; + *capacity = new_capacity; + } + vdbpackages[*packages] = package; + (*packages)++; + + return vdbpackages; + +cleanup: + if (pkgrepo) free(pkgrepo); + if (pkgslot) free(pkgslot); + if (pkgcontents) { + for (int k = 0; k < pkgfiles; k++) { + free(pkgcontents[k].path); + free(pkgcontents[k].md5); + } + free(pkgcontents); + } + return vdbpackages; +} + + +/** + * Process a package within a directory pointer in the vdb (portage internal database). + * + * This function takes a directory pointer `pkgdp` within a category and processes the package. + * It updates the number of packages `*packages`, the array of vdb packages `**vdbpackages`, + * and the package data `**data`. + * + * @param pkgdp A pointer to the `dirent` structure representing the package directory. + * @param packages A pointer to the number of packages in the vdb. + * @param capacity A pointer to the capacity of the packages array. + * @param vdbpackages A pointer to the array of vdb packages. + * @param data A pointer to the package data. + * @return The updated array of vdb packages. + */ +struct epkg** process_vdb_package(struct dirent *pkgdp, int *packages, int *capacity, struct epkg **vdbpackages, PackageData **data) { + char *pkgpath; + // construct the package directory path using the category name and package name + if (asprintf(&pkgpath, "%s/%s/%s", get_vdb_path(), (*data)->category, pkgdp->d_name) == -1) { + pkgpath = NULL; + perror("asprintf"); + } + + msg(LOG_INFO, "Loading package %s/%s", (*data)->category, pkgdp->d_name); + #ifdef DEBUG + msg(LOG_DEBUG, "\tPath: %s", pkgpath); + #endif + + if((*data)->package != NULL) { + free((*data)->package); + (*data)->package = NULL; + } + (*data)->package = strdup(pkgdp->d_name); + + if((*data)->package == NULL) { + msg(LOG_ERR, "Memory allocation failed!"); + perror("strdup"); + abort(); + } + + + if (pkgpath) { + free(pkgpath); + vdbpackages = process_pkgdir(packages, capacity, vdbpackages, data); + } + + return vdbpackages; +} + + +/** + * Process a directory (category) within the VDB root. + * + * This function opens a category directory and processes its contents. + * + * @param vdbdp A pointer to the dirent structure representing the category directory. + * @param packages A pointer to an integer variable to store the number of packages processed. + * @param capacity A pointer to an integer variable to store the capacity of the packages array. + * @param vdbpackages An array of pointers to epkg structures representing the processed packages. + * @param data A pointer to the PackageData structure to store additional package data. + * @return The updated array of pointers to epkg structures representing the processed packages. + */ +struct epkg** process_vdb_category(struct dirent *vdbdp, int *packages, int *capacity, struct epkg **vdbpackages, PackageData **data) { + + char *catdir; + // construct the category directory path + if (asprintf(&catdir, "%s/%s", get_vdb_path(), vdbdp->d_name) == -1) { + catdir = NULL; + perror("asprintf"); + } + + msg(LOG_INFO, "Loading category %s", vdbdp->d_name); + if ((*data)->category != NULL) { + free((*data)->category); + (*data)->category = NULL; + } + ((*data)->category) = strdup(vdbdp->d_name); + + if (catdir) { + DIR *category; + if ((category = opendir(catdir)) == NULL) { + msg(LOG_ERR, "Could not open %s", catdir); + msg(LOG_ERR, "Error: %s", strerror(errno)); + free(catdir); + return vdbpackages; + } + + vdbpackages = process_directory(category, process_vdb_package, packages, capacity, vdbpackages, data); + + closedir(category); + free(catdir); + } + return vdbpackages; +} + +/* + * Portage stores data about installed packages in the VDB (/var/db/pkg/). + * We care about /var/db/pkg/category/package-version/CONTENTS + * which lists files and directories that are installed as part of a package 'merge' + * operation. All files are prefixed with 'obj' and are in the format: + * obj /path/to/file $(md5sum /path/to/file) $(date -r /path/to/file "+%s") + * e.g. + * obj /usr/bin/clamscan 3ade185bd024e29880e959e6ad187515 1693552964 + */ +static int ebuild_load_list(const conf_t *conf) { + struct _hash_record *hashtable = NULL; + struct _hash_record **hashtable_ptr = &hashtable; + + // Initialise filter for this load operation + if (filter_init()) + return 1; + + if (filter_load_file(NULL)) { + filter_destroy(); + return 1; + } + + int memfd = memfd_create("ebuild_snapshot", MFD_CLOEXEC | MFD_ALLOW_SEALING); + if (memfd < 0) { + msg(LOG_ERR, "memfd_create failed for ebuild backend (%s)", + strerror(errno)); + filter_destroy(); + return 1; + } + ebuild_backend.memfd = memfd; + ebuild_backend.entries = 0; + + DIR *vdbdir; + + if ((vdbdir = opendir(get_vdb_path())) == NULL) { + msg(LOG_ERR, "Could not open %s", get_vdb_path()); + filter_destroy(); + return 1; + } + + struct epkg **vdbpackages = NULL; + int packages = 0; + int capacity = 0; + + msg(LOG_INFO, "Initialising ebuild backend"); + msg(LOG_DEBUG, "Processing VDB"); + + /* + * recurse through category/package-version/ dirs, + * process CONTENTS (files, md5s), repository, SLOT, + * store in epkg array + */ + PackageData *data = malloc(sizeof(PackageData)); + data->category = NULL; + data->package = NULL; + vdbpackages = process_directory(vdbdir, process_vdb_category, &packages, &capacity, vdbpackages, &data); + if (data->category) free(data->category); + if (data->package) free(data->package); + free(data); + closedir(vdbdir); + + msg(LOG_INFO, "Processed %d packages.", packages); + + for (int j = 0; j < packages; j++) { + struct epkg *package = vdbpackages[j]; + + // slot "0" is the default slot for packages that aren't slotted; we don't need to include it in the log + #ifdef DEBUG + if (!stop) { + if ((strcmp(package->slot,"0")) == 0) { + msg(LOG_DEBUG, "Adding %s (::%s) to the ebuild backend; %i files", + package->cpv, package->repo, package->files); + } else { + msg(LOG_DEBUG, "Adding %s:%s (::%s) to the ebuild backend; %i files", + package->cpv, package->slot, package->repo, package->files); + } + } + #endif + for (int k = 0; k < package->files; k++) { + ebuildfiles *file = &package->content[k]; + if (!stop) { + if (filter_check(file->path)) { + if (add_file_to_backend_by_md5(file->path, file->md5, hashtable_ptr, SRC_EBUILD, &ebuild_backend) == 0) + ebuild_backend.entries++; + } else { + #ifdef DEBUG + msg(LOG_DEBUG, "File %s is in the filter list; ignoring", file->path); + #endif + } + } + free(file->path); + free(file->md5); + } + free(package->content); + free(package->cpv); + free(package->slot); + free(package->repo); + free(package); + } + free(vdbpackages); + + struct _hash_record *item, *tmp; + HASH_ITER(hh, hashtable, item, tmp) { + HASH_DEL(hashtable, item); + free((void *)item->key); + free(item); + } + + if (fcntl(ebuild_backend.memfd, F_ADD_SEALS, F_SEAL_SHRINK | + F_SEAL_GROW | F_SEAL_WRITE) == -1) + // Not a fatal error + msg(LOG_WARNING, "Failed to seal ebuild backend memfd (%s)", + strerror(errno)); + + filter_destroy(); + + if (stop) + return 1; + + return 0; + +} + +static int ebuild_init_backend(void) +{ + return 0; +} + +static int ebuild_destroy_backend(void) +{ + if (ebuild_backend.memfd >= 0) { + close(ebuild_backend.memfd); + ebuild_backend.memfd = -1; + } + return 0; +} diff --git a/src/library/fapolicyd-backend.h b/src/library/fapolicyd-backend.h index b47e2120..744b4bd2 100644 --- a/src/library/fapolicyd-backend.h +++ b/src/library/fapolicyd-backend.h @@ -29,7 +29,7 @@ #include "file.h" // If this gets extended, please put the new items at the end. -typedef enum { SRC_UNKNOWN, SRC_RPM, SRC_FILE_DB, SRC_DEB } trust_src_t; +typedef enum { SRC_UNKNOWN, SRC_RPM, SRC_FILE_DB, SRC_DEB, SRC_EBUILD } trust_src_t; // source, size, sha // Do not pad the hash value so SHA1 and SHA256 digests parse correctly diff --git a/src/library/file.c b/src/library/file.c index 499973cf..58e3c559 100644 --- a/src/library/file.c +++ b/src/library/file.c @@ -31,8 +31,7 @@ #include #include #include -#include -#include +#include #include #include #include @@ -783,31 +782,34 @@ char *get_hash_from_fd2(int fd, size_t size, file_hash_alg_t alg) mapped = mmap(0, size, PROT_READ, MAP_PRIVATE|MAP_POPULATE, fd, 0); if (mapped != MAP_FAILED) { - unsigned char hptr[SHA512_DIGEST_LENGTH]; + unsigned char hptr[EVP_MAX_MD_SIZE]; int computed = 0; + const EVP_MD *md = NULL; switch (alg) { case FILE_HASH_ALG_SHA1: - SHA1(mapped, size, hptr); - computed = 1; + md = EVP_sha1(); break; case FILE_HASH_ALG_SHA256: - SHA256(mapped, size, hptr); - computed = 1; + md = EVP_sha256(); break; case FILE_HASH_ALG_SHA512: - SHA512(mapped, size, hptr); - computed = 1; + md = EVP_sha512(); break; case FILE_HASH_ALG_MD5: -#ifdef USE_DEB - MD5(mapped, size, hptr); - computed = 1; +#ifdef NEED_MD5 + md = EVP_md5(); #endif break; default: break; } + + if (md) { + unsigned int len = 0; + if (EVP_Digest(mapped, size, hptr, &len, md, NULL)) + computed = 1; + } munmap(mapped, size); if (computed) { @@ -1270,4 +1272,3 @@ uint32_t gather_elf(int fd, off_t size) rewind_fd(fd); return info; } - diff --git a/src/library/md5-backend.c b/src/library/md5-backend.c index 1234cbc3..3648ebc9 100644 --- a/src/library/md5-backend.c +++ b/src/library/md5-backend.c @@ -19,22 +19,21 @@ * * Authors: * Stephen Tridgell - * Matt Jolly + * Matt Jolly */ -#include "config.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "file.h" -#include "fapolicyd-backend.h" -#include "message.h" +#include "config.h" // for DEBUG +#include // for errno, ELOOP +#include // for open, O_NOFOLLOW, O_RDONLY +#include // for NULL, asprintf, dprintf, snprintf +#include // for free, malloc +#include // for strerror, strlen, strcmp, strdup +#include // for close +#include // for stat, fstat, S_ISREG +#include // for LOG_DEBUG, LOG_ERR, LOG_WARNING + +#include "file.h" // for bytes2hex, MD5_LEN, SHA256_LEN +#include "fapolicyd-backend.h" // for DATA_FORMAT, backend, trust_src_t +#include "message.h" // for msg #include "md5-backend.h" /* @@ -153,4 +152,3 @@ int add_file_to_backend_by_md5(const char *path, const char *expected_md5, } return 1; } - diff --git a/src/library/message.c b/src/library/message.c index 6dbb9880..29119068 100644 --- a/src/library/message.c +++ b/src/library/message.c @@ -23,16 +23,21 @@ */ #include "config.h" -#include -#include -#include -#include +#include // for pthread_mutex_lock, pthread_mutex_unlock, PTHREAD_MUTEX_INITIALIZER, pthread_mutex_t +#include // for va_end, va_list, va_start +#include // for fputs, stderr, fflush, fileno, fputc, vfprintf +#include // for getenv +#include // for LOG_DEBUG, LOG_ALERT, LOG_CRIT, LOG_EMERG, LOG_ERR, LOG_INFO, LOG_NOTICE, LOG_WARNING, vsyslog +#include // for localtime_r, strftime, time, time_t, tm +#include // for isatty #include "message.h" /* The message mode refers to where informational messages go 0 - stderr, 1 - syslog, 2 - quiet. The default is quiet. */ static message_t message_mode = MSG_QUIET; static debug_message_t debug_message = DBG_NO; +static pthread_mutex_t msg_lock = PTHREAD_MUTEX_INITIALIZER; +static int use_color = -1; void set_message_mode(message_t mode, debug_message_t debug) { @@ -50,6 +55,17 @@ void msg(int priority, const char *fmt, ...) if (priority == LOG_DEBUG && debug_message == DBG_NO) return; + if (use_color == -1) { + const char *nc = getenv("NO_COLOR"); + if (nc && nc[0] != '\0') + use_color = 0; + else if (!isatty(fileno(stderr))) + use_color = 0; + else + use_color = 1; + } + + pthread_mutex_lock(&msg_lock); va_start(ap, fmt); if (message_mode == MSG_SYSLOG) vsyslog(priority, fmt, ap); @@ -57,18 +73,35 @@ void msg(int priority, const char *fmt, ...) // For stderr we'll include the log level, use ANSI escape // codes to colourise the it, and prefix lines with the time // and date. - const char *color; + const char *color = ""; + const char *reset = ""; const char *level; + + if (use_color) { + reset = "\x1b[0m"; + switch (priority) { + case LOG_EMERG: color = "\x1b[31m"; break; /* Red */ + case LOG_ALERT: color = "\x1b[35m"; break; /* Magenta */ + case LOG_CRIT: color = "\x1b[33m"; break; /* Yellow */ + case LOG_ERR: color = "\x1b[31m"; break; /* Red */ + case LOG_WARNING: color = "\x1b[33m"; break; /* Yellow */ + case LOG_NOTICE: color = "\x1b[32m"; break; /* Green */ + case LOG_INFO: color = "\x1b[36m"; break; /* Cyan */ + case LOG_DEBUG: color = "\x1b[34m"; break; /* Blue */ + default: color = "\x1b[0m"; break; /* Reset */ + } + } + switch (priority) { - case LOG_EMERG: color = "\x1b[31m"; level = "EMERGENCY"; break; /* Red */ - case LOG_ALERT: color = "\x1b[35m"; level = "ALERT"; break; /* Magenta */ - case LOG_CRIT: color = "\x1b[33m"; level = "CRITICAL"; break; /* Yellow */ - case LOG_ERR: color = "\x1b[31m"; level = "ERROR"; break; /* Red */ - case LOG_WARNING: color = "\x1b[33m"; level = "WARNING"; break; /* Yellow */ - case LOG_NOTICE: color = "\x1b[32m"; level = "NOTICE"; break; /* Green */ - case LOG_INFO: color = "\x1b[36m"; level = "INFO"; break; /* Cyan */ - case LOG_DEBUG: color = "\x1b[34m"; level = "DEBUG"; break; /* Blue */ - default: color = "\x1b[0m"; level = "UNKNOWN"; break; /* Reset */ + case LOG_EMERG: level = "EMERGENCY"; break; + case LOG_ALERT: level = "ALERT"; break; + case LOG_CRIT: level = "CRITICAL"; break; + case LOG_ERR: level = "ERROR"; break; + case LOG_WARNING: level = "WARNING"; break; + case LOG_NOTICE: level = "NOTICE"; break; + case LOG_INFO: level = "INFO"; break; + case LOG_DEBUG: level = "DEBUG"; break; + default: level = "UNKNOWN"; break; } time_t rawtime; @@ -84,7 +117,8 @@ void msg(int priority, const char *fmt, ...) fputs(color, stderr); fputs(level, stderr); - fputs("\x1b[0m ]: ", stderr); + fputs(reset, stderr); + fputs(" ]: ", stderr); vfprintf(stderr, fmt, ap); fputc('\n', stderr); @@ -92,4 +126,5 @@ void msg(int priority, const char *fmt, ...) fflush(stderr); } va_end(ap); + pthread_mutex_unlock(&msg_lock); } diff --git a/src/library/object.c b/src/library/object.c index 80c3bc35..4c8b9735 100644 --- a/src/library/object.c +++ b/src/library/object.c @@ -44,7 +44,7 @@ void object_create(o_array *a) } #ifdef DEBUG -static void sanity_check_array(o_array *a, const char *id) +static void sanity_check_array(const o_array *a, const char *id) { int i; unsigned int num = 0; @@ -88,8 +88,14 @@ int object_add(o_array *a, const object_attr_t *obj) } else return 1; + if (a->obj[obj->type - OBJ_START]) { + free(a->obj[obj->type - OBJ_START]->o); + free(a->obj[obj->type - OBJ_START]); + } else { + a->cnt++; + } + a->obj[obj->type - OBJ_START] = newnode; - a->cnt++; return 0; } diff --git a/src/library/subject.c b/src/library/subject.c index 60c7394a..565ccb22 100644 --- a/src/library/subject.c +++ b/src/library/subject.c @@ -111,8 +111,19 @@ int subject_add(s_array *a, const subject_attr_t *subj) } else return 1; + if (a->subj[t - SUBJ_START]) { + subject_attr_t *old = a->subj[t - SUBJ_START]; + if (old->type == GID || old->type == UID) { + destroy_attr_set(old->set); + free(old->set); + } else if (old->type >= COMM) + free(old->str); + free(old); + } else { + a->cnt++; + } + a->subj[t - SUBJ_START] = newnode; - a->cnt++; sanity_check_array(a, "subject_add 2"); return 0; diff --git a/src/tests/Makefile.am b/src/tests/Makefile.am index e5dfb6f6..688888c9 100644 --- a/src/tests/Makefile.am +++ b/src/tests/Makefile.am @@ -80,4 +80,10 @@ deb_test_SOURCES = \ ${top_srcdir}/src/library/deb-backend.c endif +if WITH_EBUILD +check_PROGRAMS += ebuild_test +ebuild_test_SOURCES = ebuild_test.c +ebuild_test_LDADD = ${top_builddir}/src/.libs/libfapolicyd.la +endif + TESTS = $(check_PROGRAMS) diff --git a/src/tests/ebuild_test.c b/src/tests/ebuild_test.c new file mode 100644 index 00000000..e98f1d9e --- /dev/null +++ b/src/tests/ebuild_test.c @@ -0,0 +1,154 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "backend-manager.h" +#include "conf.h" +#include "message.h" + +// Mock globals required by backend +atomic_bool stop = 0; +unsigned int debug_mode = 0; + +// Helper to create directories +void create_dir(const char *path) { + if (mkdir(path, 0755) != 0 && errno != EEXIST) { + perror("mkdir"); + exit(1); + } +} + +// Helper to write file content +void write_file(const char *path, const char *content) { + FILE *fp = fopen(path, "w"); + if (!fp) { + perror("fopen"); + exit(1); + } + fprintf(fp, "%s", content); + fclose(fp); +} + +// Helper to create a dummy installed file +void create_dummy_file(const char *path) { + FILE *fp = fopen(path, "w"); + if (!fp) { + perror("fopen dummy"); + exit(1); + } + fprintf(fp, + "test content"); // Matches the MD5 9473fdd0d880a43c21b7778d34872157 + fclose(fp); +} + +// Helper to create a package +void create_package(const char *vdb_path, const char *category, + const char *package, const char *version, + const char *installed_file_path) { + char path[1024]; + + // Create category dir + snprintf(path, sizeof(path), "%s/%s", vdb_path, category); + if (mkdir(path, 0755) != 0 && errno != EEXIST) { + perror("mkdir category"); + exit(1); + } + + // Create package dir + snprintf(path, sizeof(path), "%s/%s/%s-%s", vdb_path, category, package, + version); + create_dir(path); + + // Create metadata + char file_path[2048]; + snprintf(file_path, sizeof(file_path), "%s/SLOT", path); + write_file(file_path, "0\n"); + + snprintf(file_path, sizeof(file_path), "%s/repository", path); + write_file(file_path, "gentoo\n"); + + // Create CONTENTS + snprintf(file_path, sizeof(file_path), "%s/CONTENTS", path); + char contents[2048]; + // Using the MD5 for "test content" -> 9473fdd0d880a43c21b7778d34872157 + snprintf(contents, sizeof(contents), + "obj %s 9473fdd0d880a43c21b7778d34872157 1234567890\n", + installed_file_path); + write_file(file_path, contents); +} + +int main(void) { + char vdb_path[] = "/tmp/fapolicyd_ebuild_test_XXXXXX"; + if (!mkdtemp(vdb_path)) { + perror("mkdtemp"); + return 1; + } + + printf("Using VDB path: %s\n", vdb_path); + setenv("FAPOLICYD_VDB_PATH", vdb_path, 1); + + // Create dummy files + char file1[] = "/tmp/fapolicyd_test_1_XXXXXX"; + char file2[] = "/tmp/fapolicyd_test_2_XXXXXX"; + char file3[] = "/tmp/fapolicyd_test_3_XXXXXX"; + + int fd; + if ((fd = mkstemp(file1)) != -1) + close(fd); + if ((fd = mkstemp(file2)) != -1) + close(fd); + if ((fd = mkstemp(file3)) != -1) + close(fd); + + create_dummy_file(file1); + create_dummy_file(file2); + create_dummy_file(file3); + + // Create packages + create_package(vdb_path, "app-test", "pkg-one", "1.0", file1); + create_package(vdb_path, "app-test", "pkg-two", "1.0", file2); + create_package(vdb_path, "sys-test", "pkg-three", "2.0", file3); + + // Initialize backend + set_message_mode(MSG_STDERR, DBG_YES); + conf_t conf; + conf.trust = "ebuilddb"; + + backend_init(&conf); + if (backend_load(&conf)) { + fprintf(stderr, "Failed to load backend\n"); + return 1; + } + + backend_entry *entry = backend_get_first(); + if (!entry || !entry->backend) { + fprintf(stderr, "No backend found\n"); + return 1; + } + + // We expect 3 entries because we added 3 unique files + if (entry->backend->entries != 3) { + fprintf(stderr, "Expected 3 entries, got %ld\n", entry->backend->entries); + return 1; + } + + printf("Test passed!\n"); + + // Cleanup + backend_close(); + unlink(file1); + unlink(file2); + unlink(file3); + + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "rm -rf %s", vdb_path); + system(cmd); + + return 0; +} diff --git a/src/tests/filter_test.c b/src/tests/filter_test.c index 34786603..9864c38d 100644 --- a/src/tests/filter_test.c +++ b/src/tests/filter_test.c @@ -48,7 +48,7 @@ #define CASES_FILE TEST_BASE "/src/tests/fixtures/filter-cases.txt" #define MIN_CONF TEST_BASE "/src/tests/fixtures/filter-minimal.conf" #define BROKEN_CONF TEST_BASE "/src/tests/fixtures/broken-filter.conf" -#define PROD_CONF TEST_BASE "/init/fapolicyd-filter.conf" +#define PROD_CONF TEST_BASE "/init/data/fapolicyd-filter.conf" extern filter_t *global_filter;