cmake_minimum_required(VERSION 3.24)

# RootGuard can be built standalone or included directly from the main
# BastionGuard project with add_subdirectory(src/rootguard).
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
    project(BastionGuardRootGuard VERSION 2.5.7 LANGUAGES C CXX)
endif()

include(GNUInstallDirs)
include(CTest)
find_package(Threads REQUIRED)
find_package(PkgConfig REQUIRED)

option(ROOTGUARD_BUILD_DAEMON "Build the privileged eBPF/LSM daemon" ON)
option(ROOTGUARD_BUILD_GTK_PAGE "Build the GTK4/gtkmm RootGuardPage library" ON)
option(ROOTGUARD_BUILD_GTK_DEMO "Build a standalone RootGuardPage demo" OFF)
option(ROOTGUARD_BUILD_TESTS "Build RootGuard tests" ON)

set(ROOTGUARD_INIT_SYSTEM "auto" CACHE STRING
    "Init integration: auto, systemd, openrc, dinit, sysvinit or none")
set_property(CACHE ROOTGUARD_INIT_SYSTEM PROPERTY STRINGS
    auto systemd openrc dinit sysvinit none)

# Compatibility aliases accepted on the command line. The canonical spelling
# remains dinit and sysvinit.
if(ROOTGUARD_INIT_SYSTEM STREQUAL "sysv")
    set(ROOTGUARD_INIT_SYSTEM "sysvinit")
elseif(ROOTGUARD_INIT_SYSTEM STREQUAL "dninit")
    set(ROOTGUARD_INIT_SYSTEM "dinit")
endif()

set(_ROOTGUARD_INIT_VALUES auto systemd openrc dinit sysvinit none)
if(NOT ROOTGUARD_INIT_SYSTEM IN_LIST _ROOTGUARD_INIT_VALUES)
    message(FATAL_ERROR
        "Unknown ROOTGUARD_INIT_SYSTEM: ${ROOTGUARD_INIT_SYSTEM}. "
        "Accepted values: auto, systemd, openrc, dinit, sysvinit, none")
endif()

set(ROOTGUARD_VMLINUX_BTF "/sys/kernel/btf/vmlinux" CACHE FILEPATH
    "Kernel BTF source used for CO-RE")
set(ROOTGUARD_SYSCONFDIR "/etc/bastionguard" CACHE PATH
    "RootGuard configuration directory")
set(ROOTGUARD_SYSTEMD_UNIT_DIR "/usr/lib/systemd/system" CACHE PATH
    "systemd unit directory")
set(ROOTGUARD_SYSVINIT_DIR "/etc/init.d" CACHE PATH
    "SysV init script directory")
set(ROOTGUARD_OPENRC_INIT_DIR "/etc/init.d" CACHE PATH
    "OpenRC init script directory")
set(ROOTGUARD_OPENRC_CONF_DIR "/etc/conf.d" CACHE PATH
    "OpenRC configuration directory")
set(ROOTGUARD_DINIT_DIR "/etc/dinit.d" CACHE PATH
    "dinit service directory")
set(ROOTGUARD_POLKIT_ACTION_DIR "/usr/share/polkit-1/actions" CACHE PATH
    "polkit action directory")
set(ROOTGUARD_DOCDIR "${CMAKE_INSTALL_DATADIR}/doc/bastionguard-rootguard" CACHE PATH
    "RootGuard documentation directory")
set(ROOTGUARD_EVENT_LOG "/var/log/bastionguard/rootguard-events.jsonl" CACHE FILEPATH
    "RootGuard JSONL event log consumed by RootGuardPage")

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

function(rootguard_enable_warnings target)
    target_compile_options(${target} PRIVATE
        $<$<COMPILE_LANGUAGE:CXX>:-Wall;-Wextra;-Wpedantic;-Wshadow;-Wconversion>
        $<$<COMPILE_LANGUAGE:C>:-Wall;-Wextra;-Wpedantic>
    )
endfunction()

if(ROOTGUARD_BUILD_DAEMON)
    pkg_check_modules(LIBBPF REQUIRED IMPORTED_TARGET libbpf)
    pkg_check_modules(LIBELF REQUIRED IMPORTED_TARGET libelf)
    find_package(ZLIB REQUIRED)
    find_program(ROOTGUARD_CLANG NAMES clang REQUIRED)
    find_program(ROOTGUARD_BPFTOOL NAMES bpftool
        HINTS /usr/sbin /usr/bin /usr/local/sbin /usr/local/bin
        REQUIRED)

    if(NOT EXISTS "${ROOTGUARD_VMLINUX_BTF}")
        message(FATAL_ERROR
            "Kernel BTF is required for the RootGuard daemon build: ${ROOTGUARD_VMLINUX_BTF}")
    endif()

    string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" ROOTGUARD_PROCESSOR)
    if(ROOTGUARD_PROCESSOR MATCHES "^(x86_64|amd64|i[3-6]86)$")
        set(ROOTGUARD_BPF_ARCH x86)
    elseif(ROOTGUARD_PROCESSOR MATCHES "^(aarch64|arm64)$")
        set(ROOTGUARD_BPF_ARCH arm64)
    elseif(ROOTGUARD_PROCESSOR MATCHES "^arm")
        set(ROOTGUARD_BPF_ARCH arm)
    elseif(ROOTGUARD_PROCESSOR STREQUAL "ppc64le")
        set(ROOTGUARD_BPF_ARCH powerpc)
    elseif(ROOTGUARD_PROCESSOR STREQUAL "s390x")
        set(ROOTGUARD_BPF_ARCH s390)
    elseif(ROOTGUARD_PROCESSOR STREQUAL "riscv64")
        set(ROOTGUARD_BPF_ARCH riscv)
    else()
        message(FATAL_ERROR
            "Unsupported BPF target architecture: ${CMAKE_SYSTEM_PROCESSOR}")
    endif()

    set(ROOTGUARD_GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated")
    file(MAKE_DIRECTORY "${ROOTGUARD_GENERATED_DIR}")
    set(ROOTGUARD_VMLINUX_H "${ROOTGUARD_GENERATED_DIR}/vmlinux.h")
    set(ROOTGUARD_BPF_OBJECT "${ROOTGUARD_GENERATED_DIR}/rootguard.bpf.o")
    set(ROOTGUARD_SKELETON "${ROOTGUARD_GENERATED_DIR}/rootguard.skel.h")

    add_custom_command(
        OUTPUT "${ROOTGUARD_VMLINUX_H}"
        COMMAND "${CMAKE_COMMAND}"
            "-DBPFTOOL:FILEPATH=${ROOTGUARD_BPFTOOL}"
            "-DINPUT_BTF:FILEPATH=${ROOTGUARD_VMLINUX_BTF}"
            "-DOUTPUT_FILE:FILEPATH=${ROOTGUARD_VMLINUX_H}"
            -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/GenerateVmlinux.cmake"
        DEPENDS
            "${ROOTGUARD_VMLINUX_BTF}"
            "${CMAKE_CURRENT_SOURCE_DIR}/cmake/GenerateVmlinux.cmake"
            "${CMAKE_CURRENT_SOURCE_DIR}/cmake/StripOuterQuotes.cmake"
        COMMENT "Generating RootGuard vmlinux.h"
        VERBATIM
    )

    add_custom_command(
        OUTPUT "${ROOTGUARD_BPF_OBJECT}"
        COMMAND "${ROOTGUARD_CLANG}" -O2 -g -target bpf
            -D__TARGET_ARCH_${ROOTGUARD_BPF_ARCH}
            "-I${ROOTGUARD_GENERATED_DIR}"
            "-I${CMAKE_CURRENT_SOURCE_DIR}/include"
            -c "${CMAKE_CURRENT_SOURCE_DIR}/bpf/rootguard.bpf.c"
            -o "${ROOTGUARD_BPF_OBJECT}"
        DEPENDS
            "${CMAKE_CURRENT_SOURCE_DIR}/bpf/rootguard.bpf.c"
            "${CMAKE_CURRENT_SOURCE_DIR}/include/rootguard/rootguard_shared.h"
            "${ROOTGUARD_VMLINUX_H}"
        COMMENT "Compiling RootGuard eBPF LSM program"
        VERBATIM
    )

    add_custom_command(
        OUTPUT "${ROOTGUARD_SKELETON}"
        COMMAND "${CMAKE_COMMAND}"
            "-DBPFTOOL:FILEPATH=${ROOTGUARD_BPFTOOL}"
            "-DINPUT_OBJECT:FILEPATH=${ROOTGUARD_BPF_OBJECT}"
            "-DOUTPUT_FILE:FILEPATH=${ROOTGUARD_SKELETON}"
            -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/GenerateSkeleton.cmake"
        DEPENDS
            "${ROOTGUARD_BPF_OBJECT}"
            "${CMAKE_CURRENT_SOURCE_DIR}/cmake/GenerateSkeleton.cmake"
            "${CMAKE_CURRENT_SOURCE_DIR}/cmake/StripOuterQuotes.cmake"
        COMMENT "Generating RootGuard libbpf skeleton"
        VERBATIM
    )

    add_custom_target(rootguard_bpf_artifacts DEPENDS "${ROOTGUARD_SKELETON}")

    add_executable(bastionguard-rootguard
        src/main.cpp
        src/Policy.cpp
        src/PolicyLoader.cpp
        src/PolicySecurity.cpp
        src/FileIdentity.cpp
        src/RootGuardEngine.cpp
        src/AutomaticResponseSink.cpp
        src/SystemServiceControl.cpp
        src/ConsoleEventSink.cpp
        src/JsonEventSink.cpp
        src/MultiEventSink.cpp
        "${ROOTGUARD_SKELETON}"
    )
    set_source_files_properties("${ROOTGUARD_SKELETON}"
        PROPERTIES GENERATED TRUE HEADER_FILE_ONLY TRUE)
    add_dependencies(bastionguard-rootguard rootguard_bpf_artifacts)
    target_include_directories(bastionguard-rootguard PRIVATE
        "${CMAKE_CURRENT_SOURCE_DIR}/include"
        "${ROOTGUARD_GENERATED_DIR}"
    )
    target_link_libraries(bastionguard-rootguard PRIVATE
        PkgConfig::LIBBPF
        PkgConfig::LIBELF
        ZLIB::ZLIB
        Threads::Threads
    )
    rootguard_enable_warnings(bastionguard-rootguard)

    add_executable(bastionguard-rootguard-action
        src/ActionHelper.cpp
        src/Policy.cpp
        src/PolicyLoader.cpp
        src/PolicySecurity.cpp
        src/FileIdentity.cpp
        src/SystemServiceControl.cpp
        src/JsonEventSink.cpp
    )
    target_include_directories(bastionguard-rootguard-action PRIVATE
        "${CMAKE_CURRENT_SOURCE_DIR}/include")
    target_link_libraries(bastionguard-rootguard-action PRIVATE Threads::Threads)
    rootguard_enable_warnings(bastionguard-rootguard-action)

    install(TARGETS bastionguard-rootguard bastionguard-rootguard-action
        RUNTIME DESTINATION "${CMAKE_INSTALL_LIBEXECDIR}/bastionguard")
endif()

if(ROOTGUARD_BUILD_GTK_PAGE)
    pkg_check_modules(ROOTGUARD_GTKMM4 REQUIRED IMPORTED_TARGET gtkmm-4.0)
    find_program(ROOTGUARD_PKEXEC NAMES pkexec HINTS /usr/bin /bin)
    if(NOT ROOTGUARD_PKEXEC)
        set(ROOTGUARD_PKEXEC "/usr/bin/pkexec")
        message(WARNING
            "[RootGuard] pkexec was not found at configure time; using ${ROOTGUARD_PKEXEC}")
    endif()

    add_library(bastionguard-rootguard-gtk STATIC
        RootGuardEvent.cpp
        RootGuardClient.cpp
        RootGuardPage.cpp
    )
    add_library(BastionGuard::RootGuardUI ALIAS bastionguard-rootguard-gtk)

    target_include_directories(bastionguard-rootguard-gtk
        PRIVATE
            "${CMAKE_CURRENT_SOURCE_DIR}"
            "${CMAKE_CURRENT_SOURCE_DIR}/include"
        PUBLIC
            "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/..>"
            "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
    )
    target_link_libraries(bastionguard-rootguard-gtk PUBLIC
        PkgConfig::ROOTGUARD_GTKMM4
        Threads::Threads
    )
    target_compile_definitions(bastionguard-rootguard-gtk PRIVATE
        BASTIONGUARD_ROOTGUARD_SERVICE_HELPER="${CMAKE_INSTALL_FULL_LIBEXECDIR}/bastionguard/bastionguard-rootguard-service"
        BASTIONGUARD_ROOTGUARD_ACTION_HELPER="${CMAKE_INSTALL_FULL_LIBEXECDIR}/bastionguard/bastionguard-rootguard-action"
        BASTIONGUARD_ROOTGUARD_EVENT_LOG="${ROOTGUARD_EVENT_LOG}"
        BASTIONGUARD_ROOTGUARD_POLICY="${CMAKE_INSTALL_FULL_SYSCONFDIR}/bastionguard/rootguard.conf"
        BASTIONGUARD_ROOTGUARD_POLICY_VIEW="/run/bastionguard-rootguard.policy-view"
        BASTIONGUARD_PKEXEC="${ROOTGUARD_PKEXEC}"
    )
    set_target_properties(bastionguard-rootguard-gtk PROPERTIES
        POSITION_INDEPENDENT_CODE ON)
    rootguard_enable_warnings(bastionguard-rootguard-gtk)

    install(TARGETS bastionguard-rootguard-gtk
        ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}")
    install(FILES
        RootGuardEvent.hpp
        RootGuardClient.hpp
        RootGuardPage.hpp
        DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/rootguard")
endif()

if(ROOTGUARD_BUILD_GTK_DEMO)
    if(NOT ROOTGUARD_BUILD_GTK_PAGE)
        message(FATAL_ERROR
            "ROOTGUARD_BUILD_GTK_DEMO requires ROOTGUARD_BUILD_GTK_PAGE")
    endif()
    add_executable(bastionguard-rootguard-gtk-demo
        demo/RootGuardPageDemo.cpp)
    target_include_directories(bastionguard-rootguard-gtk-demo PRIVATE
        "${CMAKE_CURRENT_SOURCE_DIR}")
    target_link_libraries(bastionguard-rootguard-gtk-demo PRIVATE
        BastionGuard::RootGuardUI)
endif()

if(ROOTGUARD_BUILD_TESTS)
    add_executable(rootguard-policy-tests
        tests/PolicyLoaderTests.cpp
        src/Policy.cpp
        src/PolicyLoader.cpp
        src/PolicySecurity.cpp
    )
    target_include_directories(rootguard-policy-tests PRIVATE include)
    rootguard_enable_warnings(rootguard-policy-tests)
    add_test(NAME rootguard-policy-tests COMMAND rootguard-policy-tests)

    add_executable(rootguard-file-identity-tests
        tests/FileIdentityTests.cpp
        src/FileIdentity.cpp
    )
    target_include_directories(rootguard-file-identity-tests PRIVATE include)
    rootguard_enable_warnings(rootguard-file-identity-tests)
    add_test(NAME rootguard-file-identity-tests COMMAND rootguard-file-identity-tests)

    add_executable(rootguard-event-tests
        tests/RootGuardEventTests.cpp
        RootGuardEvent.cpp
    )
    target_include_directories(rootguard-event-tests PRIVATE
        "${CMAKE_CURRENT_SOURCE_DIR}"
        "${CMAKE_CURRENT_SOURCE_DIR}/include")
    rootguard_enable_warnings(rootguard-event-tests)
    add_test(NAME rootguard-event-tests COMMAND rootguard-event-tests)
endif()

# Common runtime files. These install rules are included automatically when
# this directory is added to the main BastionGuard build with add_subdirectory.
install(PROGRAMS scripts/bastionguard-rootguard-service
    DESTINATION "${CMAKE_INSTALL_LIBEXECDIR}/bastionguard")
install(PROGRAMS scripts/enable-service.sh
    DESTINATION "${CMAKE_INSTALL_DATADIR}/bastionguard-rootguard")
# Keep the packaged policy as a reference and never overwrite a policy edited
# through RootGuardPage. On a fresh installation only, seed rootguard.conf from
# the packaged default. DESTDIR is honoured for distro package staging.
install(FILES config/rootguard.conf
    DESTINATION "${ROOTGUARD_SYSCONFDIR}"
    RENAME rootguard.conf.default
    PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ)
set(_rootguard_policy_install_code [=[
    set(_rootguard_config_dir "$ENV{DESTDIR}@ROOTGUARD_SYSCONFDIR@")
    set(_rootguard_default_policy "${_rootguard_config_dir}/rootguard.conf.default")
    set(_rootguard_active_policy "${_rootguard_config_dir}/rootguard.conf")
    if(NOT EXISTS "${_rootguard_active_policy}")
        file(COPY_FILE
            "${_rootguard_default_policy}"
            "${_rootguard_active_policy}"
            ONLY_IF_DIFFERENT)
        file(CHMOD "${_rootguard_active_policy}"
            PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ)
        message(STATUS
            "[RootGuard] Installed initial policy: ${_rootguard_active_policy}")
    else()
        message(STATUS
            "[RootGuard] Preserving existing policy: ${_rootguard_active_policy}")
    endif()
]=])
string(CONFIGURE "${_rootguard_policy_install_code}"
    _rootguard_policy_install_code @ONLY)
install(CODE "${_rootguard_policy_install_code}")
install(FILES packaging/polkit/org.bastionguard.rootguard.policy
    DESTINATION "${ROOTGUARD_POLKIT_ACTION_DIR}")
install(FILES README.md DESTINATION "${ROOTGUARD_DOCDIR}")
install(DIRECTORY docs/ DESTINATION "${ROOTGUARD_DOCDIR}")
install(DIRECTORY packaging/
    DESTINATION "${CMAKE_INSTALL_DATADIR}/bastionguard-rootguard/init-samples")
install(DIRECTORY resources/
    DESTINATION "${CMAKE_INSTALL_DATADIR}/bastionguard-rootguard/resources")

# Select exactly one native service definition for the target machine.
# OpenRC and SysV cannot be installed together because both own the same
# /etc/init.d/bastionguard-rootguard path.
function(rootguard_detect_init_system output_variable)
    set(_detected "none")
    set(_pid1 "")

    if(EXISTS "/proc/1/comm")
        file(READ "/proc/1/comm" _pid1 LIMIT 64)
        string(STRIP "${_pid1}" _pid1)
        string(TOLOWER "${_pid1}" _pid1)
    endif()

    if(_pid1 STREQUAL "systemd")
        set(_detected "systemd")
    elseif(_pid1 STREQUAL "openrc-init" OR _pid1 STREQUAL "openrc")
        set(_detected "openrc")
    elseif(_pid1 STREQUAL "dinit")
        set(_detected "dinit")
    else()
        # Runtime markers are stronger evidence than a merely installed
        # compatibility command such as systemctl.
        find_program(_rootguard_systemctl NAMES systemctl
            HINTS /usr/bin /bin /usr/local/bin)
        find_program(_rootguard_rc_service NAMES rc-service
            HINTS /sbin /usr/sbin /bin /usr/bin)
        find_program(_rootguard_openrc_run NAMES openrc-run
            HINTS /sbin /usr/sbin /bin /usr/bin)
        find_program(_rootguard_dinitctl NAMES dinitctl
            HINTS /sbin /usr/sbin /bin /usr/bin)

        if(EXISTS "/run/systemd/system" AND _rootguard_systemctl)
            set(_detected "systemd")
        elseif((EXISTS "/run/openrc" OR EXISTS "/run/openrc/softlevel")
               AND _rootguard_rc_service AND _rootguard_openrc_run)
            set(_detected "openrc")
        elseif(_rootguard_dinitctl AND
               (EXISTS "/run/dinitctl" OR EXISTS "/run/dinitctl.sock"))
            set(_detected "dinit")
        elseif(EXISTS "/etc/init.d")
            set(_detected "sysvinit")
        endif()
    endif()

    set(${output_variable} "${_detected}" PARENT_SCOPE)
endfunction()

if(ROOTGUARD_INIT_SYSTEM STREQUAL "auto")
    rootguard_detect_init_system(ROOTGUARD_DETECTED_INIT)
else()
    set(ROOTGUARD_DETECTED_INIT "${ROOTGUARD_INIT_SYSTEM}")
endif()

message(STATUS "[RootGuard] Init integration selected: ${ROOTGUARD_DETECTED_INIT}")

if(ROOTGUARD_DETECTED_INIT STREQUAL "systemd")
    install(FILES packaging/systemd/bastionguard-rootguard.service
        DESTINATION "${ROOTGUARD_SYSTEMD_UNIT_DIR}")
elseif(ROOTGUARD_DETECTED_INIT STREQUAL "openrc")
    install(PROGRAMS packaging/openrc/bastionguard-rootguard
        DESTINATION "${ROOTGUARD_OPENRC_INIT_DIR}")
    install(FILES packaging/openrc/bastionguard-rootguard.conf
        DESTINATION "${ROOTGUARD_OPENRC_CONF_DIR}"
        RENAME bastionguard-rootguard)
elseif(ROOTGUARD_DETECTED_INIT STREQUAL "dinit")
    install(FILES packaging/dinit/bastionguard-rootguard
        DESTINATION "${ROOTGUARD_DINIT_DIR}")
elseif(ROOTGUARD_DETECTED_INIT STREQUAL "sysvinit")
    install(PROGRAMS packaging/sysvinit/bastionguard-rootguard
        DESTINATION "${ROOTGUARD_SYSVINIT_DIR}")
    install(FILES packaging/sysvinit/bastionguard-rootguard.default
        DESTINATION "/etc/default"
        RENAME bastionguard-rootguard)
elseif(NOT ROOTGUARD_DETECTED_INIT STREQUAL "none")
    message(FATAL_ERROR
        "Unknown ROOTGUARD_INIT_SYSTEM: ${ROOTGUARD_DETECTED_INIT}")
endif()
