Compare commits

..

9 Commits

Author SHA1 Message Date
Samuel Gaist
fd13688ad7 refactor(webserver): improve failure handling
If starting the server throws for some reason,
the result will be instantly shown.

Signed-off-by: Samuel Gaist <samuel.gaist@idiap.ch>
2026-02-12 00:20:11 +01:00
Samuel Gaist
91d586900e fix: use nobody uid/gid as default values for webserver
Signed-off-by: Samuel Gaist <samuel.gaist@idiap.ch>
2026-02-12 00:19:54 +01:00
Samuel Gaist
1ec2d74546 refactor: reword information
Improve readability

Signed-off-by: Samuel Gaist <samuel.gaist@idiap.ch>
2026-02-10 11:47:50 +01:00
Samuel Gaist
ef495bc48e refactor: use configured listen address for liveness check
Signed-off-by: Samuel Gaist <samuel.gaist@idiap.ch>
2026-02-10 11:44:03 +01:00
Samuel Gaist
ccbc4138e8 fix: use correct parameter for setgid and setuid
They were inverted.

Signed-off-by: Samuel Gaist <samuel.gaist@idiap.ch>
2026-02-10 11:42:49 +01:00
Samuel Gaist
23ba06de51 fix: do not throw from a nothrow function
Signed-off-by: Samuel Gaist <samuel.gaist@idiap.ch>
2026-02-09 22:48:05 +01:00
Samuel Gaist
b12eb3d85d fix: use std::strerror rather than just errno
Signed-off-by: Samuel Gaist <samuel.gaist@idiap.ch>
2026-02-09 22:48:00 +01:00
Samuel Gaist
66e4c986ca refactor: use client to check if the web server is running
Using a local variable won't work since the server and the
main application don't share the same memory space anymore.

To check that the server is running, a call to the health
endpoint is used.

While the start of the server should be pretty fast, a
simple exponential backoff has been implemented to allow
for room in case the start is slowed for some reason.

Signed-off-by: Samuel Gaist <samuel.gaist@idiap.ch>
2026-02-09 22:47:16 +01:00
Samuel Gaist
be4d9bbc19 refactor: run the webserver unprivileged
The webserver was running in a thread with the same
privileges as the falco process. In order to make running
it more secure, use fork rather than thread and then change
the user and group id to unprivileged values.

Signed-off-by: Samuel Gaist <samuel.gaist@idiap.ch>
2026-02-09 21:21:33 +01:00
54 changed files with 248 additions and 1457 deletions

View File

@@ -6,9 +6,6 @@ on:
schedule:
- cron: '30 6 * * 1' # on each monday 6:30
permissions:
contents: read
# Checks if any concurrent jobs is running for kernels CI and eventually cancel it.
concurrency:
group: bump-libs-ci

View File

@@ -6,9 +6,6 @@ on:
- master
- "release/**"
permissions:
contents: read
jobs:
format:
name: format code 🐲

View File

@@ -3,9 +3,6 @@ on:
push:
branches: [master]
permissions:
contents: read
# Checks if any concurrent jobs is running for master CI and eventually cancel it
concurrency:
group: ci-master

View File

@@ -3,9 +3,6 @@ on:
release:
types: [published]
permissions:
contents: read
# Checks if any concurrent jobs is running for release CI and eventually cancel it.
concurrency:
group: ci-release

View File

@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
#
# Copyright (C) 2026 The Falco Authors.
# Copyright (C) 2023 The Falco Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
@@ -29,17 +29,6 @@ option(BUILD_FALCO_UNIT_TESTS "Build falco unit tests" OFF)
option(USE_ASAN "Build with AddressSanitizer" OFF)
option(USE_UBSAN "Build with UndefinedBehaviorSanitizer" OFF)
option(UBSAN_HALT_ON_ERROR "Halt on error when building with UBSan" ON)
option(USE_GPERFTOOLS "Build with gperftools CPU profiler support" OFF)
option(USE_FRAME_POINTER "Build with frame pointers for accurate profiling" OFF)
# Enable frame pointers by default when using gperftools for accurate stack traces
if(USE_GPERFTOOLS AND NOT USE_FRAME_POINTER)
set(USE_FRAME_POINTER
ON
CACHE BOOL "Build with frame pointers for accurate profiling" FORCE
)
message(STATUS "Enabling USE_FRAME_POINTER since USE_GPERFTOOLS is enabled")
endif()
# Mem allocators - linux only for now
if(NOT WIN32
@@ -134,25 +123,16 @@ if(NOT DEFINED FALCO_COMPONENT_NAME)
endif()
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
if(WIN32)
set(CMAKE_INSTALL_PREFIX
"C:/Program Files/${CMAKE_PROJECT_NAME}"
CACHE PATH "Default install path" FORCE
)
else()
set(CMAKE_INSTALL_PREFIX
/usr
CACHE PATH "Default install path" FORCE
)
endif()
set(CMAKE_INSTALL_PREFIX
/usr
CACHE PATH "Default install path" FORCE
)
endif()
set(CMD_MAKE make)
include(ExternalProject)
include(cxxopts)
# libs
include(falcosecurity-libs)
@@ -197,6 +177,8 @@ if(NOT WIN32
include(cpp-httplib)
endif()
include(cxxopts)
# One TBB
if(NOT EMSCRIPTEN)
include(tbb)
@@ -204,11 +186,6 @@ endif()
include(zlib)
include(valijson)
# CPU Profiling with gperftools
if(USE_GPERFTOOLS)
include(gperftools)
endif()
if(NOT MINIMAL_BUILD)
if(NOT WIN32
AND NOT APPLE
@@ -298,7 +275,7 @@ if(NOT WIN32
# Generate a binary_dir/falco.yaml that automatically enables the plugin to be used for local
# testing.
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/falco.yaml ${CMAKE_BINARY_DIR} COPYONLY)
configure_file(${CMAKE_SOURCE_DIR}/falco.yaml ${CMAKE_BINARY_DIR} COPYONLY)
# The custom target configures the plugin and set its path
add_custom_target(
container

View File

@@ -27,7 +27,7 @@ The `pre-commit` framework allows you to automatically install different `git-ho
1. The `clang-format` hook: this is a `pre-commit` git hook that runs `clang-format` on your staged changes.
2. The `cmake-format` hook: this is a `pre-commit` git hook that runs `cmake-format` on your staged changes.
3. The `DCO signed-off` hook: this is a `pre-commit-msg` git hook that adds the `DCO` on your commit if not present. This hook is not strictly related to the coding style so we will talk about it in a separate section: [Add DCO signed-off to your commits](#add-dco-signed-off-to-your-commits-).
3. The `DCO signed-off` hook: this is a `pre-commit-msg` git hook that adds the `DCO` on your commit if not present. This hook is not strictly related to the coding style so we will talk about it in a separate section: [Add DCO signed-off to your commits](#add-dco-signed-off-to-your-commits).
Now let's see what we need to use `pre-commit` framework.
@@ -47,7 +47,7 @@ This simple command allows you to install the two `pre-commit` git hooks, `clang
pre-commit install --install-hooks --hook-type pre-commit --overwrite
```
If you want to install also the `pre-commit-msg` git hook for the DCO you have to type the following command, but be sure to have configured all you need as said in the [dedicated section](#add-dco-signed-off-to-your-commits-)
If you want to install also the `pre-commit-msg` git hook for the DCO you have to type the following command, but be sure to have configured all you need as said in the [dedicated section]((#add-dco-signed-off-to-your-commits))
```bash
pre-commit install --install-hooks --hook-type prepare-commit-msg --overwrite
@@ -81,7 +81,7 @@ To install `cmake-format` you can follow the official documentation [here](https
##### Step 2
Once you have installed the __right__ versions of the 2 tools, you can simply type `make format-all` from the root directory of the project to format all your code according to the coding style.
Once you have installed the __right__ versions of the 2 tools, you can simply type `make format-all` from the root directory of the project (`/libs`) to format all your code according to the coding style.
Remember to do that before submitting a new patch upstream! 😁
@@ -93,7 +93,7 @@ Obviously, you can also install the 2 tools locally and enable some extension of
### Introduction
Another requirement for contributing to the `falco` repository, is applying the [DCO](https://cert-manager.io/docs/contributing/sign-off/) to every commit you want to push upstream.
Another requirement for contributing to the `libs` repository, is applying the [DCO](https://cert-manager.io/docs/contributing/sign-off/) to every commit you want to push upstream.
Before doing this you have to configure your git user `name` and `email` if you haven't already done it. To check your actual `name` and `email` type:
```bash

View File

@@ -68,10 +68,6 @@ if(NOT MSVC)
endif()
endif()
if(USE_FRAME_POINTER)
set(FALCO_SECURITY_FLAGS "${FALCO_SECURITY_FLAGS} -fno-omit-frame-pointer")
endif()
set(CMAKE_COMMON_FLAGS
"${FALCO_SECURITY_FLAGS} -Wall -ggdb ${FALCO_EXTRA_FEATURE_FLAGS} ${MINIMAL_BUILD_FLAGS} ${MUSL_FLAGS}"
)

View File

@@ -24,7 +24,7 @@ if(CXXOPTS_INCLUDE_DIR)
elseif(NOT USE_BUNDLED_CXXOPTS)
find_package(cxxopts CONFIG REQUIRED)
get_target_property(CXXOPTS_INCLUDE_DIR cxxopts::cxxopts INTERFACE_INCLUDE_DIRECTORIES)
elseif(NOT TARGET cxxopts)
else()
set(CXXOPTS_SRC "${PROJECT_BINARY_DIR}/cxxopts-prefix/src/cxxopts/")
set(CXXOPTS_INCLUDE_DIR "${CXXOPTS_SRC}/include")

View File

@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
#
# Copyright (C) 2026 The Falco Authors.
# Copyright (C) 2025 The Falco Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
@@ -35,9 +35,9 @@ else()
# FALCOSECURITY_LIBS_VERSION. In case you want to test against another driver version (or
# branch, or commit) just pass the variable - ie., `cmake -DDRIVER_VERSION=dev ..`
if(NOT DRIVER_VERSION)
set(DRIVER_VERSION "7b08f8a0a12b56d59eab73052e637ca123623f61")
set(DRIVER_VERSION "9.1.0+driver")
set(DRIVER_CHECKSUM
"SHA256=43c72a98e48d04177c8223ccdfe88de6f09958f2330b6b9ee26882f1a77e369f"
"SHA256=14cba5b610bf48cd0a0a94b1156ed86bfb552c7ed24b68b1028360fa3af18cbb"
)
endif()

View File

@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
#
# Copyright (C) 2026 The Falco Authors.
# Copyright (C) 2025 The Falco Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
@@ -42,9 +42,9 @@ else()
# version (or branch, or commit) just pass the variable - ie., `cmake
# -DFALCOSECURITY_LIBS_VERSION=dev ..`
if(NOT FALCOSECURITY_LIBS_VERSION)
set(FALCOSECURITY_LIBS_VERSION "7b08f8a0a12b56d59eab73052e637ca123623f61")
set(FALCOSECURITY_LIBS_VERSION "0.23.1")
set(FALCOSECURITY_LIBS_CHECKSUM
"SHA256=43c72a98e48d04177c8223ccdfe88de6f09958f2330b6b9ee26882f1a77e369f"
"SHA256=38c580626b072ed24518e8285a629923c8c4c6d6794b91b3b93474db7fd85cf7"
)
endif()
@@ -105,10 +105,6 @@ set(BUILD_LIBSCAP_EXAMPLES
OFF
CACHE BOOL ""
)
set(BUILD_LIBSINSP_EXAMPLES
OFF
CACHE BOOL ""
)
set(USE_BUNDLED_TBB
ON

View File

@@ -1,132 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
#
# Copyright (C) 2026 The Falco Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
# or implied. See the License for the specific language governing permissions and limitations under
# the License.
#
# gperftools CPU profiler support This module provides: GPERFTOOLS_INCLUDE_DIR (include directory)
# and GPERFTOOLS_PROFILER_LIB (the profiler library path)
option(USE_BUNDLED_GPERFTOOLS "Build gperftools from source" ${USE_BUNDLED_DEPS})
if(GPERFTOOLS_INCLUDE_DIR)
# Already have gperftools configured
elseif(NOT USE_BUNDLED_GPERFTOOLS)
# Use system gperftools
find_path(
GPERFTOOLS_INCLUDE_DIR
NAMES gperftools/profiler.h
PATHS /usr/include /usr/local/include
)
find_library(
GPERFTOOLS_PROFILER_LIB
NAMES profiler
PATHS /usr/lib /usr/local/lib /usr/lib/x86_64-linux-gnu /usr/lib/aarch64-linux-gnu
)
if(GPERFTOOLS_INCLUDE_DIR AND GPERFTOOLS_PROFILER_LIB)
message(
STATUS
"Found system gperftools: include: ${GPERFTOOLS_INCLUDE_DIR}, lib: ${GPERFTOOLS_PROFILER_LIB}"
)
else()
message(
FATAL_ERROR
"Couldn't find system gperftools. Install it or use -DUSE_BUNDLED_GPERFTOOLS=ON\n"
" Ubuntu/Debian: sudo apt-get install libgoogle-perftools-dev\n"
" Fedora/RHEL: sudo dnf install gperftools-devel\n"
" macOS: brew install gperftools"
)
endif()
else()
# Build gperftools from source
set(GPERFTOOLS_SRC "${PROJECT_BINARY_DIR}/gperftools-prefix/src/gperftools")
set(GPERFTOOLS_INCLUDE_DIR "${GPERFTOOLS_SRC}/src")
if(BUILD_SHARED_LIBS)
set(GPERFTOOLS_LIB_SUFFIX ${CMAKE_SHARED_LIBRARY_SUFFIX})
else()
set(GPERFTOOLS_LIB_SUFFIX ${CMAKE_STATIC_LIBRARY_SUFFIX})
endif()
# The library is built in .libs subdirectory
set(GPERFTOOLS_PROFILER_LIB "${GPERFTOOLS_SRC}/.libs/libprofiler${GPERFTOOLS_LIB_SUFFIX}")
# gperftools version 2.15 (latest stable as of 2024)
set(GPERFTOOLS_VERSION "2.15")
set(GPERFTOOLS_URL
"https://github.com/gperftools/gperftools/releases/download/gperftools-${GPERFTOOLS_VERSION}/gperftools-${GPERFTOOLS_VERSION}.tar.gz"
)
set(GPERFTOOLS_URL_HASH
"SHA256=c69fef855628c81ef56f12e3c58f2b7ce1f326c0a1fe783e5cae0b88cbbe9a80"
)
message(STATUS "Building gperftools ${GPERFTOOLS_VERSION} from source")
# Configure options for gperftools
set(GPERFTOOLS_CONFIGURE_ARGS --enable-cpu-profiler --disable-heap-profiler
--disable-heap-checker --disable-debugalloc
)
# Check if libunwind is available for better stack traces
find_library(LIBUNWIND_LIBRARY NAMES unwind)
if(LIBUNWIND_LIBRARY)
list(APPEND GPERFTOOLS_CONFIGURE_ARGS --enable-libunwind)
message(STATUS "gperftools: libunwind found, enabling for better stack traces")
else()
list(APPEND GPERFTOOLS_CONFIGURE_ARGS --disable-libunwind)
message(STATUS "gperftools: libunwind not found, using frame pointers for stack traces")
endif()
ExternalProject_Add(
gperftools
PREFIX "${PROJECT_BINARY_DIR}/gperftools-prefix"
URL "${GPERFTOOLS_URL}"
URL_HASH "${GPERFTOOLS_URL_HASH}"
CONFIGURE_COMMAND <SOURCE_DIR>/configure ${GPERFTOOLS_CONFIGURE_ARGS}
BUILD_COMMAND ${CMD_MAKE} ${PROCESSOUR_COUNT_MAKE_FLAG}
BUILD_IN_SOURCE 1
INSTALL_COMMAND ""
UPDATE_COMMAND ""
BUILD_BYPRODUCTS ${GPERFTOOLS_PROFILER_LIB}
)
install(
FILES "${GPERFTOOLS_PROFILER_LIB}"
DESTINATION "${CMAKE_INSTALL_LIBDIR}/${LIBS_PACKAGE_NAME}"
COMPONENT "libs-deps"
OPTIONAL
)
endif()
# Create a custom target so we can always depend on 'gperftools' regardless of bundled/system
if(NOT TARGET gperftools)
add_custom_target(gperftools)
endif()
# Add include directory globally
include_directories(${GPERFTOOLS_INCLUDE_DIR})
# Add compile definition so code can detect profiling support
add_compile_definitions(HAS_GPERFTOOLS)
# Wrap the profiler library with --whole-archive to ensure the profiler's initialization code is
# linked even though we don't call ProfilerStart() directly. This is required for the CPUPROFILE
# environment variable to work.
set(GPERFTOOLS_PROFILER_LIB "-Wl,--whole-archive" "${GPERFTOOLS_PROFILER_LIB}"
"-Wl,--no-whole-archive"
)
message(STATUS "gperftools CPU profiler enabled")
message(STATUS " Include dir: ${GPERFTOOLS_INCLUDE_DIR}")
message(STATUS " Library: ${GPERFTOOLS_PROFILER_LIB}")

View File

@@ -921,6 +921,9 @@ webserver:
# $ cat certificate.pem key.pem > falco.pem $ sudo cp falco.pem /etc/falco/falco.pem
# ```
ssl_certificate: /etc/falco/falco.pem
# User and group id under which the server should run
uid: 65534
gid: 65534
##############################################################################
# Falco logging / alerting / metrics related to software functioning (basic) #

View File

@@ -1,186 +0,0 @@
# Multi-Threaded Falco High-Level Design (Working draft)
## Summary
This document outlines a high-level design for implementing multi-threading in Falco. The goal of this proposal is to overcome Falco's single-threaded architecture to improve scalability in scenarios where the amount of events produced cannot be processed in a single thread. This is achieved by leveraging multiple threads for event processing, rule evaluation, and output handling, enabling Falco to better utilize multi-core systems and reduce event drops under high event rates.
## Goals
* Address the problems related to single CPU core saturation, leading to dropped events.
* Minimize the performance impact on the single threaded usage, that remains the default.
## Non-Goals
* This document does not cover low-level implementation details that will be addressed in specific design documents for each component or directly in the implementation phase.
* This document does not focus on performance optimization, the primary goal is scalability improvements to handle higher event rates that exceed single-thread processing capacity.
## Success Metrics
The success of this multi-threading initiative will be measured by the following key metrics:
* **Event Drop Rate Reduction**: The primary success metric is the reduction in event drops under high event rates. A successful implementation should significantly reduce or eliminate event drops that occur when a single thread cannot keep up with the event rate.
* **Throughput Scaling**: The system should demonstrate improved throughput (events processed per second) that scales with the number of worker threads, up to a reasonable limit based on available CPU cores and workload characteristics.
* **CPU Utilization**: Multi-threaded Falco should better utilize available CPU cores, with worker threads distributing load across multiple cores instead of saturating a single core.
* **Single-Threaded Performance Preservation**: The single-threaded mode (default) should maintain its current performance characteristics, with minimal or no performance regression when multi-threading is disabled.
These metrics will be evaluated through benchmarking and real-world deployment scenarios to validate that the multi-threaded architecture achieves its scalability goals without compromising correctness or introducing significant overhead.
## High-Level Design
### Current Architecture
![Current Falco Architecture](images/falco-architecture.png)
* The kernel driver (via kmod or eBPF) writes events into per-CPU ring buffers. Each CPU has its own buffer to avoid lock contention. We have a ring buffer per CPU core, and a single userspace.
* Userspace (libscap) performs an `O(n_cpus)` scan on every next() call, it peeks at the head event from each ring buffer, finds the event with the minimum timestamp across all buffers and returns that event to Falco for processing. The consumer position is only advanced after the event has been consumed (on the next call), ensuring the caller can safely read the event data and avoiding the need to perform copies of the event data.
* Libsinsp processes the events sequentially as they are received from libscap, building a stateful representation of the system and providing the necessary context for rule evaluation.
* Falco evaluates the rules against the processed events and generates alerts based on the defined security policies.
### Proposed Architecture Overview
![Multi-Threaded Falco Architecture](images/falco-multi-thread-architecture.png)
* The kernel driver (modern eBPF probe) routes events into per-partition ring buffers based on TGID. The routing logic executes in kernel space (within the eBPF program), where each event's TGID is hashed and used to select the target ring buffer. Only the modern eBPF probe is supported, as it relies on [BPF_MAP_TYPE_RINGBUF](https://docs.ebpf.io/linux/map-type/BPF_MAP_TYPE_RINGBUF/) which does not have a per-CPU design as opposed to the `BPF_MAP_TYPE_PERF_EVENT_ARRAY` used by the legacy eBPF probe.
* Each ring buffer is associated with an event loop worker thread that processes events from its assigned ring buffer.
* The `libsinsp` state (e.g., the thread state) is maintained in a shared data structure, allowing all workers to access data pushed by other workers. This is crucial for handling events like clone() that rely on data written by other partitions. This requires designing lightweight synchronization mechanisms to ensure efficient access to shared state without introducing significant contention. A dedicated proposal document will address the design of the shared state and synchronization mechanisms, and data consistency.
* Falco's rule evaluation is performed in parallel by multiple worker threads, each evaluating rules against the events they process. Current Falco plugins are not supposed to be thread-safe. A dedicated proposal document will address the design of a thread-safe plugin architecture.
* **Output handling** is already designed for multi-threaded access. The `falco_outputs` class implements a thread-safe, queue-based architecture using Intel TBB's `concurrent_bounded_queue`, which is specifically designed for multi-producer, single-consumer scenarios. Multiple worker threads can concurrently call `handle_event()` to enqueue alert messages using the thread-safe `try_push()` operation. A dedicated output worker thread consumes messages from the queue using `pop()` and sends them to all configured outputs (stdout, file, syslog, gRPC, HTTP, etc.). This design is already proven in production, as Falco's multi-source support (where different event sources run in separate threads) already uses this same queue concurrently. The existing implementation requires no changes to support multi-threaded event processing. Note that while outputs are processed in order within the queue, alerts from different worker threads may be interleaved, meaning strict temporal ordering of alerts across different processes is not guaranteed. This is acceptable for security monitoring use cases where the primary concern is detecting and reporting security events rather than maintaining precise event ordering.
### Work Partitioning Strategies
A crucial and challenging design aspect is partitioning the work to achieve a good trade-off among the following properties:
1. **Even load balancing** between threads
2. **Low contention** on shared data (or no shared data)
3. **Avoiding temporal inconsistencies and causality violations** (e.g., processing a file-opening event before the related process-forking event)
The first two properties are primarily focused on performance, while the third is essential for the correctness of the solution. These aspects are intrinsically linked.
Based on the analysis below, **Static Partitioning by TGID** is the proposed approach for the initial implementation.
#### Static Partitioning by TGID (Thread Group ID / Process ID)
Events are routed based on the TGID in kernel space (within the eBPF program) to a ring buffer dedicated to a specific partition. The routing logic executes at the point where events are captured, before they are written to any ring buffer. This partition is then consumed by a dedicated worker thread in userspace. The routing in the eBPF program can be accomplished with a simple hash and modulo operation, depending on the desired number of worker threads:
```
ring_buffer_index = hash(event->tgid) % num_workers
```
The hash function and number of workers are configured at eBPF program initialization time, allowing the kernel to route events directly to the appropriate ring buffer without userspace intervention.
**Pros:**
* **Reduced need for thread synchronization**: While structures keeping thread group data are shared across all worker threads and require synchronization, TGID partitioning minimizes cross-partition access. For data stored per thread-group (such as file descriptors), TGID partitioning guarantees a single writer (the worker thread assigned to that TGID), resulting in low contention since the data is mostly accessed by the same partition. Synchronization is needed only in two specific cases:
1. **Clone/fork events**: When handling clone/fork events, the worker thread needs to access thread information from the parent thread, which may reside in a different partition. This requires synchronization to read the parent's state (e.g., file descriptors, environment variables) that will be inherited by the child.
2. **Proc exit events**: When a process exits, reparenting logic may need to access thread information from other partitions to handle child processes that are being reparented to a different thread group.
* Guarantee of sequential order processing of events related to the same thread group/process, as they are handled by the same worker thread. This limits the chance of temporal inconsistencies.
**Cons:**
* **Load Imbalance / "Hot" Process Vulnerability**: This static partitioning is susceptible to uneven worker load distribution, as a small number of high-activity ("hot") processes can overload the specific worker thread assigned to their TGID, creating a bottleneck.
* **Cross-Partition Temporal Inconsistency**: Events that require information from a parent thread (e.g., fork/clone events) can still lead to causality issues. If the parent's related event is handled by a different, lagging partition, the required context might be incomplete or arrive out of order. Note that load imbalance amplifies this issue. Missing thread information is easy to detect, but there are also cases where information is present but not up-to-date or ahead of the time the clone event happened.
**Ancestor information during rule evaluation**: When evaluating rules that require ancestor information, the worker thread may need to access thread data from other partitions. Falco rules commonly check ancestor process attributes using fields that traverse the process hierarchy. Based on actual usage in Falco rules, commonly used ancestor fields include:
- `proc.aname` / `proc.aname[N]` - ancestor process name (where N is the generation level: 1=parent, 2=grandparent, 3=great-grandparent, etc., up to at least level 7)
- `proc.aexepath[N]` - ancestor executable path (e.g., `proc.aexepath[2]` for grandparent)
- `proc.aexe[N]` - ancestor executable (e.g., `proc.aexe[2]` for grandparent)
Accessing stale or "ahead" ancestor data (where the ancestor's state may be out of date or from events processed by other partitions with different timestamps) could lead to false positives or false negatives in rule evaluation. We acknowledge this potential issue and plan to assess its impact and determine appropriate mitigations once we have a running prototype.
**Mitigations:**
* **Last-Resort Fetching**: Fetching the thread information from a different channel to resolve the drift (e.g., proc scan, eBPF iterator). This solution is considered as a last resort because it risks slowing down the event processing loop, potentially negating the performance benefits of multi-threading.
* **Context Synchronization**: Wait for the required thread information to become available. This can be decomposed into two orthogonal concerns:
**How to handle the wait:**
* **Wait/Sleep (Blocking)**: The worker thread blocks (sleeping or spinning) until the required data becomes available. Simple to implement, but the worker is idle during the wait, reducing throughput.
* **Deferring (Non-blocking)**: The event is copied/buffered for later processing; the worker continues with other events from its ring buffer. More complex (requires event copying, a pending queue, and a retry mechanism), but keeps the worker productive.
**How to detect data readiness:**
* **Polling**: Periodically check if the required data is available (spin-check for Wait/Sleep, or periodic retry for Deferring). Simple but wastes CPU cycles.
* **Signaling**: Partitions proactively notify each other when data is ready. More efficient but requires coordination infrastructure (e.g., condition variables, eventfd, or message queues).
These combine into four possible approaches:
| | Polling | Signaling |
|---|---------|-----------|
| **Wait/Sleep** | Spin-check until ready | Sleep on condition variable, wake on signal |
| **Deferring** | Periodically retry deferred events | Process deferred events when signaled |
**Synchronization point**: A natural synchronization point is the **clone exit parent event**. At this point, the parent process has completed setting up the child's initial state (inherited file descriptors, environment, etc.), making it safe to start processing events for the newly created thread group.
**Special case — `vfork()` / `CLONE_VFORK`**: When `vfork()` is used, the parent thread is blocked until the child calls `exec()` or exits, delaying the clone exit parent event. An alternative synchronization point may be needed (e.g., adding back clone enter parent).
### Other Considered Approaches
#### Static Partitioning by TID (Thread ID)
Similar to the previous approach, but events are routed by TID instead of TGID.
**Pros:**
* Guarantee of sequential order processing of events related to the same thread, as they are handled by the same worker thread. This limits the chance of temporal inconsistencies.
* Good enough load balancing between partitions.
**Cons:**
* **Cross-Partition Temporal Inconsistency**: This approach can lead to temporal inconsistencies when accessing/writing information from/to other processes or from the Thread Group Leader (e.g., environment, file descriptor information is stored in the thread group leader).
#### Static Partitioning by CPU Core
This approach routes events based on the CPU core where the event was captured. Each CPU core has its own ring buffer (per-CPU buffers), and multiple CPU buffers are assigned to the same partition. Each partition is consumed by a dedicated worker thread that reads from all the per-CPU buffers assigned to it. The number of partitions does not necessarily match the number of CPU cores—a single partition can read from multiple per-CPU buffers, allowing flexibility in choosing the number of worker threads independently from the number of CPU cores. This leverages the existing per-CPU ring buffer infrastructure used by the kernel module (kmod) and legacy eBPF probe, where events are written to per-CPU buffers that are then grouped into partitions consumed by worker threads.
**Pros:**
* **Natural Load Distribution**: Events are naturally distributed across CPUs based on where processes execute, providing inherent load balancing that reflects actual system activity.
* **No Routing Logic Required**: Uses the existing per-CPU ring buffer design, eliminating the need for custom routing logic in kernel or userspace. CPU cores are simply mapped to partitions (e.g., via modulo operation: `partition = cpu_id % num_workers`), and each worker thread reads from all per-CPU buffers assigned to its partition.
* **Low Synchronization Overhead**: Events from per-CPU buffers assigned to the same partition are processed sequentially by the same worker thread, reducing cross-thread synchronization needs.
* **Flexible Partitioning**: The number of partitions (and thus worker threads) can be chosen independently from the number of CPU cores, allowing optimization based on workload characteristics rather than hardware topology.
**Cons:**
* **Cross-CPU Temporal Inconsistency**: Events from the same process or thread group can be processed by different worker threads if the process migrates between CPUs, leading to potential temporal inconsistencies and causality violations. This is particularly problematic for multi-threaded applications that may execute on different CPUs.
* **Process Migration Effects**: CPU migration can cause events from the same process to be processed out of order, as events captured on different CPUs are handled by different worker threads.
* **Load Imbalance with CPU Grouping**: When multiple per-CPU buffers are assigned to the same partition, the worker thread must process events from all assigned buffers. If the activity levels across these CPUs are uneven, the worker thread may experience load imbalance, with some partitions handling more active CPUs than others. The worker thread must also coordinate reading from multiple buffers, potentially using techniques similar to the current `O(n_cpus)` scan to maintain event ordering.
* **Modern eBPF Probe Limitation**: The modern eBPF probe uses `BPF_MAP_TYPE_RINGBUF`, which does not have a per-CPU design. This approach would only be viable with the kernel module (kmod) or legacy eBPF probe that use `BPF_MAP_TYPE_PERF_EVENT_ARRAY` with per-CPU buffers.
#### Functional Partitioning (Pipelining)
Instead of partitioning the data, this approach partitions the work by splitting processing into phases:
1. **Parsing**: Runs in a single thread, the state is updated in this phase.
2. **Rules evaluation**: Runs in a thread chosen from a worker thread pool, the state is accessed but not modified.
**Pros:**
* The state handling remains single-threaded, avoiding any synchronization issue on the write side.
* The load balancing of the Rules evaluation phase is good as it does not require any form of stickiness. Every worker can take whatever event, and a simple round-robin policy can be applied.
**Cons:**
* The "Parsing" stage is likely to become the bottleneck; a single thread here limits total throughput regardless of how many cores you have.
* As we are parallelizing parsing and rules evaluation phases, we need an MVCC (multi-version concurrency control) technique to maintain multiple levels of state to use the state at the right point in time during rules evaluation.
* Processing multiple events in parallel involves changes at the driver and libscap level. At the moment we are processing one event at a time from the driver memory without copying. To be able to process multiple events in parallel, we need to adapt the ring-buffer to make sure that `next()` does not consume the event. We would also need some flow control (e.g., backpressure) to avoid processing too many events in parallel. This problem would arise only if the rules evaluation phase is slower than the parsing phase.
#### Comparison Summary
| Approach | Load Balancing | Contention | Temporal Consistency |
|----------|----------------|------------|----------------------|
| TGID | Moderate (hot process risk) | Low | Good (within process) |
| TID | Good | Higher | Partial (thread-level only) |
| CPU Core | Good | Low | Poor (process migration issues) |
| Pipelining | Good (rules evaluation phase) | Low (writes) | Requires MVCC |
#### Rationale for TGID Partitioning
TGID partitioning was chosen because it offers the best balance between synchronization complexity and correctness guarantees. TID partitioning increases cross-partition access for thread group leader data (e.g., file descriptor table, working directory, environment variables), increasing the coordination cost. Per-CPU partitioning, while leveraging existing infrastructure, suffers from process migration issues that can cause significant temporal inconsistencies when processes move between CPUs. Functional partitioning, while elegant in its separation of concerns, introduces a single-threaded bottleneck in the parsing phase that limits scalability regardless of available cores, and requires complex MVCC mechanisms for data consistency and mechanisms for handling multiple events in parallel.
### Risks and Mitigations
- **Increased Complexity**: Multi-threading introduces complexity in terms of synchronization and state management. Mitigation: Careful design of shared state and synchronization mechanisms, along with thorough testing.
- **Synchronization Overhead vs Performance Gains**: The overhead of synchronization might negate the performance benefits of multi-threading. Mitigation: Use lightweight synchronization techniques and minimize shared state access.
- **Synchronization Overhead vs Data Consistency**: In order to keep the synchronization overhead low with the shared state, we might need to relax some data consistency guarantees. Mitigation: Analyze the trade-offs and ensure that any relaxed guarantees do not compromise security.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

View File

@@ -1328,162 +1328,6 @@ TEST_F(test_falco_engine, empty_string_source_addl_rule) {
EXPECT_TRUE(load_rules(rules_content, "rules.yaml"));
}
// Phase 1: Schema correctness — no false positives for valid rule properties
TEST_F(test_falco_engine, rule_with_warn_evttypes) {
std::string rules_content = R"END(
- rule: test_rule
desc: test rule description
condition: evt.type = close
output: user=%user.name command=%proc.cmdline file=%fd.name
priority: INFO
warn_evttypes: false
)END";
ASSERT_TRUE(load_rules(rules_content, "rules.yaml")) << m_load_result_string;
ASSERT_VALIDATION_STATUS(yaml_helper::validation_ok) << m_load_result->schema_validation();
}
TEST_F(test_falco_engine, rule_with_skip_if_unknown_filter) {
std::string rules_content = R"END(
- rule: test_rule
desc: test rule description
condition: evt.type = close
output: user=%user.name command=%proc.cmdline file=%fd.name
priority: INFO
skip-if-unknown-filter: true
)END";
ASSERT_TRUE(load_rules(rules_content, "rules.yaml")) << m_load_result_string;
ASSERT_VALIDATION_STATUS(yaml_helper::validation_ok) << m_load_result->schema_validation();
}
TEST_F(test_falco_engine, override_replace_warn_evttypes) {
std::string rules_content = R"END(
- rule: test_rule
desc: test rule description
condition: evt.type = close
output: user=%user.name command=%proc.cmdline file=%fd.name
priority: INFO
warn_evttypes: true
- rule: test_rule
warn_evttypes: false
override:
warn_evttypes: replace
)END";
ASSERT_TRUE(load_rules(rules_content, "rules.yaml")) << m_load_result_string;
ASSERT_VALIDATION_STATUS(yaml_helper::validation_ok) << m_load_result->schema_validation();
}
TEST_F(test_falco_engine, override_replace_capture) {
std::string rules_content = R"END(
- rule: test_rule
desc: test rule description
condition: evt.type = close
output: user=%user.name command=%proc.cmdline file=%fd.name
priority: INFO
- rule: test_rule
capture: true
override:
capture: replace
)END";
ASSERT_TRUE(load_rules(rules_content, "rules.yaml")) << m_load_result_string;
ASSERT_VALIDATION_STATUS(yaml_helper::validation_ok) << m_load_result->schema_validation();
}
TEST_F(test_falco_engine, override_replace_tags) {
std::string rules_content = R"END(
- rule: test_rule
desc: test rule description
condition: evt.type = close
output: user=%user.name command=%proc.cmdline file=%fd.name
priority: INFO
tags: [filesystem]
- rule: test_rule
tags: [network]
override:
tags: replace
)END";
ASSERT_TRUE(load_rules(rules_content, "rules.yaml")) << m_load_result_string;
ASSERT_VALIDATION_STATUS(yaml_helper::validation_ok) << m_load_result->schema_validation();
}
// Phase 2: Unknown key detection
TEST_F(test_falco_engine, rule_unknown_key) {
std::string rules_content = R"END(
- rule: test_rule
desc: test rule description
condition: evt.type = close
output: user=%user.name command=%proc.cmdline file=%fd.name
priority: INFO
typo_field: some_value
)END";
ASSERT_TRUE(load_rules(rules_content, "rules.yaml")) << m_load_result_string;
ASSERT_TRUE(check_warning_message("Unknown key 'typo_field'"));
}
TEST_F(test_falco_engine, list_unknown_key) {
std::string rules_content = R"END(
- list: my_list
items: [cat, bash]
typo_field: some_value
- rule: test_rule
desc: test rule description
condition: evt.type = close and proc.name in (my_list)
output: user=%user.name command=%proc.cmdline file=%fd.name
priority: INFO
)END";
ASSERT_TRUE(load_rules(rules_content, "rules.yaml")) << m_load_result_string;
ASSERT_TRUE(check_warning_message("Unknown key 'typo_field'"));
}
TEST_F(test_falco_engine, macro_unknown_key) {
std::string rules_content = R"END(
- macro: my_macro
condition: evt.type = close
typo_field: some_value
- rule: test_rule
desc: test rule description
condition: my_macro
output: user=%user.name command=%proc.cmdline file=%fd.name
priority: INFO
)END";
ASSERT_TRUE(load_rules(rules_content, "rules.yaml")) << m_load_result_string;
ASSERT_TRUE(check_warning_message("Unknown key 'typo_field'"));
}
TEST_F(test_falco_engine, list_cross_type_key_priority) {
std::string rules_content = R"END(
- list: my_list
items: [cat, bash]
priority: INFO
- rule: test_rule
desc: test rule description
condition: evt.type = close and proc.name in (my_list)
output: user=%user.name command=%proc.cmdline file=%fd.name
priority: INFO
)END";
ASSERT_TRUE(load_rules(rules_content, "rules.yaml")) << m_load_result_string;
// The flat-union schema accepts 'priority' on a list (validation_ok),
// but procedural detection catches the cross-type misuse.
ASSERT_VALIDATION_STATUS(yaml_helper::validation_ok) << m_load_result->schema_validation();
ASSERT_TRUE(check_warning_message("Unknown key 'priority'"));
}
TEST_F(test_falco_engine, deprecated_field_in_output) {
std::string rules_content = R"END(
- rule: test_rule_with_evt_dir_in_output

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright (C) 2026 The Falco Authors.
Copyright (C) 2023 The Falco Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -146,57 +146,6 @@ plugins:
EXPECT_EQ(falco_config.m_plugins[0].m_init_config, "");
}
TEST(Configuration, plugin_library_path_traversal) {
falco_configuration falco_config;
config_loaded_res res;
// A relative path that stays within the plugins dir should succeed.
std::string config = R"(
plugins:
- name: myplugin
library_path: libmyplugin.so
)";
EXPECT_NO_THROW(res = falco_config.init_from_content(config, {}));
EXPECT_VALIDATION_STATUS(res, yaml_helper::validation_ok);
// A relative path with ".." that escapes the plugins dir must be rejected.
config = R"(
plugins:
- name: evil
library_path: ../../tmp/evil.so
)";
EXPECT_THROW(falco_config.init_from_content(config, {}), std::exception);
// Traversal via "./" prefix followed by ".." must also be rejected.
config = R"(
plugins:
- name: evil
library_path: ./../../tmp/evil.so
)";
EXPECT_THROW(falco_config.init_from_content(config, {}), std::exception);
// Nested traversal that descends then escapes must be rejected.
config = R"(
plugins:
- name: evil
library_path: subdir/../../../tmp/evil.so
)";
EXPECT_THROW(falco_config.init_from_content(config, {}), std::exception);
#ifndef _WIN32
// Absolute paths bypass the prefix logic and are allowed as-is.
// This test uses a Unix absolute path syntax.
config = R"(
plugins:
- name: myplugin
library_path: /opt/falco/plugins/libmyplugin.so
)";
EXPECT_NO_THROW(res = falco_config.init_from_content(config, {}));
EXPECT_VALIDATION_STATUS(res, yaml_helper::validation_ok);
EXPECT_EQ(falco_config.m_plugins[0].m_library_path, "/opt/falco/plugins/libmyplugin.so");
#endif
}
TEST(Configuration, schema_yaml_helper_validator) {
yaml_helper conf;
falco_configuration falco_config;

View File

@@ -21,7 +21,6 @@ add_library(
filter_ruleset.cpp
evttype_index_ruleset.cpp
formats.cpp
field_formatter.cpp
filter_details_resolver.cpp
filter_macro_resolver.cpp
filter_warning_resolver.cpp

View File

@@ -42,7 +42,6 @@ limitations under the License.
#include "falco_engine_version.h"
#include "formats.h"
#include "field_formatter.h"
#include "evttype_index_ruleset.h"
@@ -118,7 +117,7 @@ static std::string fieldclass_key(const sinsp_filter_factory::filter_fieldclass_
void falco_engine::list_fields(const std::string &source,
bool verbose,
bool names_only,
output_format format) const {
bool markdown) const {
// Maps from field class name + short desc to list of event
// sources for which this field class can be used.
std::map<std::string, std::set<std::string>> fieldclass_event_sources;
@@ -139,10 +138,6 @@ void falco_engine::list_fields(const std::string &source,
// printing field classes multiple times for different sources
std::set<std::string> seen_fieldclasses;
// Create the appropriate formatter and use it
auto formatter = FieldFormatter::create(format, verbose);
formatter->begin();
// In the second pass, actually print info, skipping duplicate
// field classes and also printing info on supported sources.
for(const auto &it : m_sources) {
@@ -165,15 +160,21 @@ void falco_engine::list_fields(const std::string &source,
continue;
}
formatter->print_field_name(field.name);
printf("%s\n", field.name.c_str());
}
} else if(markdown) {
printf("%s\n",
fld_class.as_markdown(fieldclass_event_sources[fieldclass_key(fld_class)])
.c_str());
} else {
formatter->print_fieldclass(fld_class, fieldclass_event_sources[key]);
printf("%s\n",
fld_class
.as_string(verbose,
fieldclass_event_sources[fieldclass_key(fld_class)])
.c_str());
}
}
}
formatter->end();
}
std::unique_ptr<load_result> falco_engine::load_rules(const std::string &rules_content,

View File

@@ -34,7 +34,6 @@ limitations under the License.
#include "falco_source.h"
#include "falco_load_result.h"
#include "filter_details_resolver.h"
#include "output_format.h"
//
// This class acts as the primary interface between a program and the
@@ -63,10 +62,7 @@ public:
// Print to stdout (using printf) a description of each field supported by this engine.
// If source is non-empty, only fields for the provided source are printed.
void list_fields(const std::string &source,
bool verbose,
bool names_only,
output_format format) const;
void list_fields(const std::string &source, bool verbose, bool names_only, bool markdown) const;
// Provide an alternate rule reader, collector, and compiler
// to compile any rules provided via load_rules*

View File

@@ -20,7 +20,7 @@ limitations under the License.
// The version of this Falco engine
#define FALCO_ENGINE_VERSION_MAJOR 0
#define FALCO_ENGINE_VERSION_MINOR 60
#define FALCO_ENGINE_VERSION_MINOR 58
#define FALCO_ENGINE_VERSION_PATCH 0
#define FALCO_ENGINE_VERSION \
@@ -36,4 +36,4 @@ limitations under the License.
// It represents the fields supported by this version of Falco,
// the event types, and the underlying driverevent schema. It's used to
// detetect changes in engine version in our CI jobs.
#define FALCO_ENGINE_CHECKSUM "17c1ac99576c032a58895a10f7091cf777008a1059b7f1bff3c78a6451b17fdf"
#define FALCO_ENGINE_CHECKSUM "952fa3356bfd266419810be51ae659eab48093a66da26fff3897022a9de2c08c"

View File

@@ -73,12 +73,12 @@ static const std::string warning_codes[] = {"LOAD_UNKNOWN_SOURCE",
"LOAD_EXCEPTION_NAME_NOT_UNIQUE",
"LOAD_INVALID_MACRO_NAME",
"LOAD_INVALID_LIST_NAME",
"LOAD_COMPILE_CONDITION",
"LOAD_UNKNOWN_KEY"};
"LOAD_COMPILE_CONDITION"};
// Compile-time check to ensure warning_codes array has the correct size
static_assert(std::size(warning_codes) ==
static_cast<int>(falco::load_result::warning_code::LOAD_UNKNOWN_KEY) + 1,
static_cast<int>(falco::load_result::warning_code::LOAD_COMPILE_CONDITION) +
1,
"warning_codes array size must match the last warning_code enum value + 1");
const std::string& falco::load_result::warning_code_str(warning_code wc) {
@@ -98,12 +98,12 @@ static const std::string warning_strings[] = {"Unknown event source",
"Multiple exceptions defined with the same name",
"Invalid macro name",
"Invalid list name",
"Warning in rule condition",
"Unknown key in item definition"};
"Warning in rule condition"};
// Compile-time check to ensure warning_strings array has the correct size
static_assert(std::size(warning_strings) ==
static_cast<int>(falco::load_result::warning_code::LOAD_UNKNOWN_KEY) + 1,
static_cast<int>(falco::load_result::warning_code::LOAD_COMPILE_CONDITION) +
1,
"warning_strings array size must match the last warning_code enum value + 1");
const std::string& falco::load_result::warning_str(warning_code wc) {
@@ -131,13 +131,12 @@ static const std::string warning_descs[] = {
"A rule is defining multiple exceptions with the same name",
"A macro is defined with an invalid name",
"A list is defined with an invalid name",
"A rule condition or output have been parsed with a warning",
"An item in the rules content contains an unrecognized key. The key will be ignored. "
"This may indicate a typo or a property placed on the wrong item type."};
"A rule condition or output have been parsed with a warning"};
// Compile-time check to ensure warning_descs array has the correct size
static_assert(std::size(warning_descs) ==
static_cast<int>(falco::load_result::warning_code::LOAD_UNKNOWN_KEY) + 1,
static_cast<int>(falco::load_result::warning_code::LOAD_COMPILE_CONDITION) +
1,
"warning_descs array size must match the last warning_code enum value + 1");
const std::string& falco::load_result::warning_desc(warning_code wc) {

View File

@@ -62,8 +62,7 @@ public:
LOAD_EXCEPTION_NAME_NOT_UNIQUE,
LOAD_INVALID_MACRO_NAME,
LOAD_INVALID_LIST_NAME,
LOAD_COMPILE_CONDITION,
LOAD_UNKNOWN_KEY
LOAD_COMPILE_CONDITION
};
// The warning code as a string

View File

@@ -1,122 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright (C) 2026 The Falco Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "field_formatter.h"
#include "formats.h"
using namespace falco;
// Factory method
std::unique_ptr<FieldFormatter> FieldFormatter::create(output_format format, bool verbose) {
switch(format) {
case output_format::JSON:
return std::make_unique<JsonFieldFormatter>(verbose);
case output_format::MARKDOWN:
return std::make_unique<MarkdownFieldFormatter>(verbose);
case output_format::TEXT:
default:
return std::make_unique<TextFieldFormatter>(verbose);
}
}
// ============================================================================
// TextFieldFormatter implementation
// ============================================================================
TextFieldFormatter::TextFieldFormatter(bool verbose): m_verbose(verbose) {}
void TextFieldFormatter::begin() {
// Nothing to do for text format
}
void TextFieldFormatter::print_fieldclass(
const sinsp_filter_factory::filter_fieldclass_info& fld_class,
const std::set<std::string>& event_sources) {
printf("%s\n", fld_class.as_string(m_verbose, event_sources).c_str());
}
void TextFieldFormatter::print_field_name(const std::string& name) {
printf("%s\n", name.c_str());
}
void TextFieldFormatter::end() {
// Nothing to do for text format
}
// ============================================================================
// MarkdownFieldFormatter implementation
// ============================================================================
MarkdownFieldFormatter::MarkdownFieldFormatter(bool verbose): m_verbose(verbose) {}
void MarkdownFieldFormatter::begin() {
// Nothing to do for markdown format
}
void MarkdownFieldFormatter::print_fieldclass(
const sinsp_filter_factory::filter_fieldclass_info& fld_class,
const std::set<std::string>& event_sources) {
printf("%s\n", fld_class.as_markdown(event_sources).c_str());
}
void MarkdownFieldFormatter::print_field_name(const std::string& name) {
printf("%s\n", name.c_str());
}
void MarkdownFieldFormatter::end() {
// Nothing to do for markdown format
}
// ============================================================================
// JsonFieldFormatter implementation
// ============================================================================
JsonFieldFormatter::JsonFieldFormatter(bool verbose): m_verbose(verbose) {}
void JsonFieldFormatter::begin() {
m_fieldclasses_array = nlohmann::json::array();
m_fieldnames_array = nlohmann::json::array();
m_has_fieldclasses = false;
m_has_fieldnames = false;
}
void JsonFieldFormatter::print_fieldclass(
const sinsp_filter_factory::filter_fieldclass_info& fld_class,
const std::set<std::string>& event_sources) {
std::string json_str = fld_class.as_json(event_sources);
if(!json_str.empty()) {
m_fieldclasses_array.push_back(nlohmann::json::parse(json_str));
m_has_fieldclasses = true;
}
}
void JsonFieldFormatter::print_field_name(const std::string& name) {
m_fieldnames_array.push_back(name);
m_has_fieldnames = true;
}
void JsonFieldFormatter::end() {
nlohmann::json root;
if(m_has_fieldclasses) {
root["fieldclasses"] = m_fieldclasses_array;
printf("%s\n", root.dump(2).c_str());
} else if(m_has_fieldnames) {
root["fieldnames"] = m_fieldnames_array;
printf("%s\n", root.dump(2).c_str());
}
}

View File

@@ -1,102 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright (C) 2026 The Falco Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#pragma once
#include <string>
#include <set>
#include <memory>
#include <nlohmann/json.hpp>
#include <libsinsp/sinsp.h>
enum class output_format;
namespace falco {
// Abstract formatter interface for field listing
class FieldFormatter {
public:
virtual ~FieldFormatter() = default;
// Initialize formatter
virtual void begin() = 0;
// Print a field class with its event sources
virtual void print_fieldclass(const sinsp_filter_factory::filter_fieldclass_info& fld_class,
const std::set<std::string>& event_sources) = 0;
// Print a single field name (for names_only mode)
virtual void print_field_name(const std::string& name) = 0;
// Finalize and output
virtual void end() = 0;
// Factory method
static std::unique_ptr<FieldFormatter> create(output_format format, bool verbose);
};
// Text formatter (default)
class TextFieldFormatter : public FieldFormatter {
public:
explicit TextFieldFormatter(bool verbose);
void begin() override;
void print_fieldclass(const sinsp_filter_factory::filter_fieldclass_info& fld_class,
const std::set<std::string>& event_sources) override;
void print_field_name(const std::string& name) override;
void end() override;
private:
bool m_verbose;
};
// Markdown formatter
class MarkdownFieldFormatter : public FieldFormatter {
public:
explicit MarkdownFieldFormatter(bool verbose);
void begin() override;
void print_fieldclass(const sinsp_filter_factory::filter_fieldclass_info& fld_class,
const std::set<std::string>& event_sources) override;
void print_field_name(const std::string& name) override;
void end() override;
private:
bool m_verbose;
};
// JSON formatter
class JsonFieldFormatter : public FieldFormatter {
public:
explicit JsonFieldFormatter(bool verbose);
void begin() override;
void print_fieldclass(const sinsp_filter_factory::filter_fieldclass_info& fld_class,
const std::set<std::string>& event_sources) override;
void print_field_name(const std::string& name) override;
void end() override;
private:
bool m_verbose;
nlohmann::json m_fieldclasses_array;
nlohmann::json m_fieldnames_array;
bool m_has_fieldclasses{false};
bool m_has_fieldnames{false};
};
} // namespace falco

View File

@@ -125,13 +125,5 @@ void filter_details_resolver::visitor::visit(ast::field_expr* e) {
void filter_details_resolver::visitor::visit(ast::field_transformer_expr* e) {
m_details.transformers.insert(e->transformer);
for(auto& value : e->values) {
value->accept(this);
}
}
void filter_details_resolver::visitor::visit(ast::transformer_list_expr* e) {
for(auto& child : e->children) {
child->accept(this);
}
e->value->accept(this);
}

View File

@@ -72,7 +72,6 @@ private:
void visit(libsinsp::filter::ast::binary_check_expr* e) override;
void visit(libsinsp::filter::ast::field_expr* e) override;
void visit(libsinsp::filter::ast::field_transformer_expr* e) override;
void visit(libsinsp::filter::ast::transformer_list_expr* e) override;
filter_details& m_details;
bool m_expect_list;

View File

@@ -105,10 +105,6 @@ void filter_macro_resolver::visitor::visit(ast::field_transformer_expr* e) {
m_node_substitute = nullptr;
}
void filter_macro_resolver::visitor::visit(ast::transformer_list_expr* e) {
m_node_substitute = nullptr;
}
void filter_macro_resolver::visitor::visit(ast::identifier_expr* e) {
const auto& macro = m_macros.find(e->identifier);
if(macro != m_macros.end() && macro->second) // skip null-ptr macros

View File

@@ -121,7 +121,6 @@ private:
void visit(libsinsp::filter::ast::binary_check_expr* e) override;
void visit(libsinsp::filter::ast::field_expr* e) override;
void visit(libsinsp::filter::ast::field_transformer_expr* e) override;
void visit(libsinsp::filter::ast::transformer_list_expr* e) override;
};
std::vector<value_info> m_errors;

View File

@@ -1,20 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright (C) 2026 The Falco Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#pragma once
enum class output_format { TEXT, MARKDOWN, JSON };

View File

@@ -97,12 +97,6 @@ const char rule_schema_string[] = LONG_STRING_CONST(
"items": {
"type": "string"
}
},
"warn_evttypes": {
"type": "boolean"
},
"skip-if-unknown-filter": {
"type": "boolean"
}
},
"required": [],
@@ -156,7 +150,7 @@ const char rule_schema_string[] = LONG_STRING_CONST(
"append",
"replace"
],
"title": "OverriddenItem"
"title": "Priority"
},
"Override": {
"type": "object",
@@ -182,21 +176,6 @@ const char rule_schema_string[] = LONG_STRING_CONST(
},
"exceptions": {
"$ref": "#/definitions/OverriddenItem"
},
"capture": {
"$ref": "#/definitions/OverriddenItem"
},
"capture_duration": {
"$ref": "#/definitions/OverriddenItem"
},
"tags": {
"$ref": "#/definitions/OverriddenItem"
},
"warn_evttypes": {
"$ref": "#/definitions/OverriddenItem"
},
"skip-if-unknown-filter": {
"$ref": "#/definitions/OverriddenItem"
}
},
"minProperties": 1,

View File

@@ -346,8 +346,7 @@ void rule_loader::compiler::compile_macros_infos(const configuration& cfg,
static bool err_is_unknown_type_or_field(const std::string& err) {
return err.find("nonexistent field") != std::string::npos ||
err.find("invalid formatting token") != std::string::npos ||
err.find("unknown event type") != std::string::npos ||
err.find("unknown filter:") != std::string::npos;
err.find("unknown event type") != std::string::npos;
}
bool rule_loader::compiler::compile_condition(const configuration& cfg,

View File

@@ -404,23 +404,6 @@ static void read_rule_exceptions(
exceptions = decoded;
}
static void warn_unknown_keys(const YAML::Node& item,
const std::set<std::string>& expected_keys,
rule_loader::configuration& cfg,
const rule_loader::context& ctx) {
if(!item.IsMap()) {
return;
}
for(auto it = item.begin(); it != item.end(); ++it) {
std::string key = it->first.as<std::string>();
if(expected_keys.find(key) == expected_keys.end()) {
cfg.res->add_warning(falco::load_result::warning_code::LOAD_UNKNOWN_KEY,
"Unknown key '" + key + "'. The key will be ignored.",
ctx);
}
}
}
inline static bool check_update_expected(std::set<std::string>& expected_keys,
const std::set<std::string>& overrides,
const std::string& override_type,
@@ -477,10 +460,6 @@ void rule_loader::reader::read_item(rule_loader::configuration& cfg,
}
collector.define(cfg, v);
static const std::set<std::string> expected_required_engine_version_keys{
"required_engine_version"};
warn_unknown_keys(item, expected_required_engine_version_keys, cfg, ctx);
} else if(item["required_plugin_versions"].IsDefined()) {
const YAML::Node& req_plugin_vers = item["required_plugin_versions"];
rule_loader::context ctx(req_plugin_vers,
@@ -535,14 +514,6 @@ void rule_loader::reader::read_item(rule_loader::configuration& cfg,
collector.define(cfg, v);
}
static const std::set<std::string> expected_required_plugin_versions_keys{
"required_plugin_versions"};
rule_loader::context rpv_ctx(item,
rule_loader::context::REQUIRED_PLUGIN_VERSIONS,
"",
parent);
warn_unknown_keys(item, expected_required_plugin_versions_keys, cfg, rpv_ctx);
} else if(item["list"].IsDefined()) {
std::string name;
// Using tmp context until name is decoded
@@ -588,12 +559,6 @@ void rule_loader::reader::read_item(rule_loader::configuration& cfg,
} else {
collector.define(cfg, v);
}
static const std::set<std::string> expected_list_keys{"list",
"items",
"append",
"override"};
warn_unknown_keys(item, expected_list_keys, cfg, ctx);
} else if(item["macro"].IsDefined()) {
std::string name;
// Using tmp context until name is decoded
@@ -645,12 +610,6 @@ void rule_loader::reader::read_item(rule_loader::configuration& cfg,
} else {
collector.define(cfg, v);
}
static const std::set<std::string> expected_macro_keys{"macro",
"condition",
"append",
"override"};
warn_unknown_keys(item, expected_macro_keys, cfg, ctx);
} else if(item["rule"].IsDefined()) {
std::string name;
@@ -939,23 +898,6 @@ void rule_loader::reader::read_item(rule_loader::configuration& cfg,
collector.define(cfg, v);
}
}
static const std::set<std::string> expected_rule_keys{"rule",
"condition",
"output",
"desc",
"priority",
"source",
"enabled",
"capture",
"capture_duration",
"warn_evttypes",
"skip-if-unknown-filter",
"tags",
"exceptions",
"override",
"append"};
warn_unknown_keys(item, expected_rule_keys, cfg, ctx);
} else {
rule_loader::context ctx(item, rule_loader::context::RULES_CONTENT_ITEM, "", parent);
cfg.res->add_warning(falco::load_result::warning_code::LOAD_UNKNOWN_ITEM,

View File

@@ -40,7 +40,6 @@ add_library(
app/actions/print_plugin_info.cpp
app/actions/print_support.cpp
app/actions/print_syscall_events.cpp
app/actions/event_formatter.cpp
app/actions/print_version.cpp
app/actions/print_page_size.cpp
app/actions/configure_syscall_buffer_size.cpp
@@ -49,7 +48,6 @@ add_library(
app/actions/start_webserver.cpp
app/actions/validate_rules_files.cpp
app/actions/close_inspectors.cpp
app/actions/cleanup_outputs.cpp
app/actions/print_config_schema.cpp
app/actions/print_rule_schema.cpp
configuration.cpp
@@ -74,11 +72,6 @@ if(USE_JEMALLOC OR USE_MIMALLOC)
list(APPEND FALCO_LIBRARIES ${MALLOC_LIB})
endif()
if(USE_GPERFTOOLS)
list(APPEND FALCO_DEPENDENCIES gperftools)
list(APPEND FALCO_LIBRARIES "${GPERFTOOLS_PROFILER_LIB}")
endif()
if(NOT WIN32)
target_sources(falco_application PRIVATE outputs_program.cpp outputs_syslog.cpp)
endif()

View File

@@ -55,7 +55,6 @@ falco::app::run_result stop_webserver(falco::app::state& s);
falco::app::run_result unregister_signal_handlers(falco::app::state& s);
falco::app::run_result validate_rules_files(falco::app::state& s);
falco::app::run_result close_inspectors(falco::app::state& s);
falco::app::run_result cleanup_outputs(falco::app::state& s);
}; // namespace actions
}; // namespace app

View File

@@ -1,29 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright (C) 2025 The Falco Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "actions.h"
using namespace falco::app;
using namespace falco::app::actions;
falco::app::run_result falco::app::actions::cleanup_outputs(falco::app::state& s) {
if(s.outputs) {
s.outputs.reset();
s.engine->print_stats();
}
return run_result::ok();
}

View File

@@ -1,199 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright (C) 2026 The Falco Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "event_formatter.h"
#include <libsinsp/sinsp.h>
#include <libsinsp/event.h>
using namespace falco::app::actions;
static bool is_flag_type(ppm_param_type type) {
return (type == PT_FLAGS8 || type == PT_FLAGS16 || type == PT_FLAGS32 ||
type == PT_ENUMFLAGS8 || type == PT_ENUMFLAGS16 || type == PT_ENUMFLAGS32);
}
// Factory method
std::unique_ptr<EventFormatter> EventFormatter::create(output_format format) {
switch(format) {
case output_format::JSON:
return std::make_unique<JsonFormatter>();
case output_format::MARKDOWN:
return std::make_unique<MarkdownFormatter>();
case output_format::TEXT:
default:
return std::make_unique<TextFormatter>();
}
}
// ============================================================================
// TextFormatter implementation
// ============================================================================
void TextFormatter::begin(const std::string& schema_version) {
printf("The events below are valid for Falco *Schema Version*: %s\n", schema_version.c_str());
}
void TextFormatter::begin_category(const std::string& category) {
printf("## %s\n\n", category.c_str());
}
void TextFormatter::print_event(const event_entry& e) {
char dir = e.is_enter ? '>' : '<';
printf("%c %s(", dir, e.name.c_str());
for(uint32_t k = 0; k < e.info->nparams; k++) {
if(k != 0) {
printf(", ");
}
print_param(&e.info->params[k]);
}
printf(")\n");
}
void TextFormatter::end_category() {
printf("\n");
}
void TextFormatter::end() {
// Nothing to do for text format
}
void TextFormatter::print_param(const struct ppm_param_info* param) {
printf("%s **%s**", param_type_to_string(param->type), param->name);
if(is_flag_type(param->type) && param->info) {
auto flag_info = static_cast<const ppm_name_value*>(param->info);
printf(": ");
for(size_t i = 0; flag_info[i].name != NULL; i++) {
if(i != 0) {
printf(", ");
}
printf("%s", flag_info[i].name);
}
}
}
// ============================================================================
// MarkdownFormatter implementation
// ============================================================================
void MarkdownFormatter::begin(const std::string& schema_version) {
printf("The events below are valid for Falco *Schema Version*: %s\n", schema_version.c_str());
}
void MarkdownFormatter::begin_category(const std::string& category) {
printf("## %s\n\n", category.c_str());
printf("Default | Dir | Name | Params \n");
printf(":-------|:----|:-----|:-----\n");
}
void MarkdownFormatter::print_event(const event_entry& e) {
char dir = e.is_enter ? '>' : '<';
printf(e.available ? "Yes" : "No");
printf(" | `%c` | `%s` | ", dir, e.name.c_str());
for(uint32_t k = 0; k < e.info->nparams; k++) {
if(k != 0) {
printf(", ");
}
print_param(&e.info->params[k]);
}
printf("\n");
}
void MarkdownFormatter::end_category() {
printf("\n");
}
void MarkdownFormatter::end() {
// Nothing to do for markdown format
}
void MarkdownFormatter::print_param(const struct ppm_param_info* param) {
printf("%s **%s**", param_type_to_string(param->type), param->name);
if(is_flag_type(param->type) && param->info) {
auto flag_info = static_cast<const ppm_name_value*>(param->info);
printf(": ");
for(size_t i = 0; flag_info[i].name != NULL; i++) {
if(i != 0) {
printf(", ");
}
printf("*%s*", flag_info[i].name);
}
}
}
// ============================================================================
// JsonFormatter implementation
// ============================================================================
void JsonFormatter::begin(const std::string& schema_version) {
m_root = nlohmann::json::object();
m_root["schema_version"] = schema_version;
}
void JsonFormatter::begin_category(const std::string& category) {
m_current_category = nlohmann::json::array();
m_current_category_name = category;
}
void JsonFormatter::print_event(const event_entry& e) {
m_current_category.push_back(event_to_json(e));
}
void JsonFormatter::end_category() {
m_root[m_current_category_name] = m_current_category;
}
void JsonFormatter::end() {
printf("%s\n", m_root.dump(2).c_str());
}
nlohmann::json JsonFormatter::event_to_json(const event_entry& e) {
nlohmann::json event;
event["name"] = e.name;
event["dir"] = e.is_enter ? ">" : "<";
event["available"] = e.available;
nlohmann::json params = nlohmann::json::array();
for(uint32_t k = 0; k < e.info->nparams; k++) {
nlohmann::json param;
param["type"] = param_type_to_string(e.info->params[k].type);
param["name"] = e.info->params[k].name;
if(is_flag_type(e.info->params[k].type) && e.info->params[k].info) {
auto flag_info = static_cast<const ppm_name_value*>(e.info->params[k].info);
nlohmann::json flags = nlohmann::json::array();
for(size_t i = 0; flag_info[i].name != NULL; i++) {
flags.push_back(flag_info[i].name);
}
param["flags"] = flags;
}
params.push_back(param);
}
event["params"] = params;
return event;
}

View File

@@ -1,111 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright (C) 2026 The Falco Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#pragma once
#include <string>
#include <vector>
#include <cstdint>
#include <memory>
#include <nlohmann/json.hpp>
#include "../../../engine/output_format.h"
struct ppm_param_info;
struct ppm_event_info;
namespace falco {
namespace app {
namespace actions {
struct event_entry {
bool is_enter;
bool available;
std::string name;
const ppm_event_info* info;
};
// Abstract formatter interface
class EventFormatter {
public:
virtual ~EventFormatter() = default;
// Initialize formatter with schema version
virtual void begin(const std::string& schema_version) = 0;
// Print category header
virtual void begin_category(const std::string& category) = 0;
// Print a single event
virtual void print_event(const event_entry& e) = 0;
// End category
virtual void end_category() = 0;
// Finalize and output
virtual void end() = 0;
// Factory method
static std::unique_ptr<EventFormatter> create(output_format format);
};
// Text formatter (default)
class TextFormatter : public EventFormatter {
public:
void begin(const std::string& schema_version) override;
void begin_category(const std::string& category) override;
void print_event(const event_entry& e) override;
void end_category() override;
void end() override;
private:
void print_param(const struct ppm_param_info* param);
};
// Markdown formatter
class MarkdownFormatter : public EventFormatter {
public:
void begin(const std::string& schema_version) override;
void begin_category(const std::string& category) override;
void print_event(const event_entry& e) override;
void end_category() override;
void end() override;
private:
void print_param(const struct ppm_param_info* param);
};
// JSON formatter
class JsonFormatter : public EventFormatter {
public:
void begin(const std::string& schema_version) override;
void begin_category(const std::string& category) override;
void print_event(const event_entry& e) override;
void end_category() override;
void end() override;
private:
nlohmann::json m_root;
nlohmann::json m_current_category;
std::string m_current_category_name;
nlohmann::json event_to_json(const event_entry& e);
};
} // namespace actions
} // namespace app
} // namespace falco

View File

@@ -33,6 +33,6 @@ falco::app::run_result falco::app::actions::list_fields(falco::app::state& s) {
s.engine->list_fields(s.options.list_source_fields,
s.options.verbose,
s.options.names_only,
s.options.output_fmt);
s.options.markdown);
return run_result::exit();
}

View File

@@ -17,13 +17,19 @@ limitations under the License.
#include "actions.h"
#include "helpers.h"
#include "event_formatter.h"
#include "../app.h"
#include "../../versions_info.h"
using namespace falco::app;
using namespace falco::app::actions;
struct event_entry {
bool is_enter;
bool available;
std::string name;
const ppm_event_info* info;
};
struct events_by_category {
std::vector<event_entry> syscalls;
std::vector<event_entry> tracepoints;
@@ -63,32 +69,6 @@ struct events_by_category {
return;
}
}
void print_all(EventFormatter& formatter) {
formatter.begin_category("Syscall events");
for(const auto& e : syscalls) {
formatter.print_event(e);
}
formatter.end_category();
formatter.begin_category("Tracepoint events");
for(const auto& e : tracepoints) {
formatter.print_event(e);
}
formatter.end_category();
formatter.begin_category("Plugin events");
for(const auto& e : pluginevents) {
formatter.print_event(e);
}
formatter.end_category();
formatter.begin_category("Metaevents");
for(const auto& e : metaevents) {
formatter.print_event(e);
}
formatter.end_category();
}
};
static struct events_by_category get_event_entries_by_category(
@@ -120,21 +100,86 @@ static struct events_by_category get_event_entries_by_category(
return result;
}
static bool is_flag_type(ppm_param_type type) {
return (type == PT_FLAGS8 || type == PT_FLAGS16 || type == PT_FLAGS32 ||
type == PT_ENUMFLAGS8 || type == PT_ENUMFLAGS16 || type == PT_ENUMFLAGS32);
}
static void print_param(const struct ppm_param_info* param, bool markdown) {
printf("%s **%s**", param_type_to_string(param->type), param->name);
if(is_flag_type(param->type) && param->info) {
auto flag_info = static_cast<const ppm_name_value*>(param->info);
printf(": ");
for(size_t i = 0; flag_info[i].name != NULL; i++) {
if(i != 0) {
printf(", ");
}
if(markdown) {
printf("*%s*", flag_info[i].name);
} else {
printf("%s", flag_info[i].name);
}
}
}
}
static void print_events(const std::vector<event_entry>& events, bool markdown) {
if(markdown) {
printf("Default | Dir | Name | Params \n");
printf(":-------|:----|:-----|:-----\n");
}
for(const auto& e : events) {
char dir = e.is_enter ? '>' : '<';
if(markdown) {
printf(e.available ? "Yes" : "No");
printf(" | `%c` | `%s` | ", dir, e.name.c_str());
} else {
printf("%c %s(", dir, e.name.c_str());
}
for(uint32_t k = 0; k < e.info->nparams; k++) {
if(k != 0) {
printf(", ");
}
print_param(&e.info->params[k], markdown);
}
if(markdown) {
printf("\n");
} else {
printf(")\n");
}
}
}
falco::app::run_result falco::app::actions::print_syscall_events(falco::app::state& s) {
if(!s.options.list_syscall_events) {
return run_result::ok();
}
const falco::versions_info info(s.offline_inspector);
printf("The events below are valid for Falco *Schema Version*: %s\n",
info.driver_schema_version.c_str());
const libsinsp::events::set<ppm_event_code> available = libsinsp::events::all_event_set().diff(
sc_set_to_event_set(falco::app::ignored_sc_set()));
struct events_by_category events_bc = get_event_entries_by_category(true, available);
const struct events_by_category events_bc = get_event_entries_by_category(true, available);
// Create the appropriate formatter and use it
auto formatter = EventFormatter::create(s.options.output_fmt);
formatter->begin(info.driver_schema_version);
events_bc.print_all(*formatter);
formatter->end();
printf("## Syscall events\n\n");
print_events(events_bc.syscalls, s.options.markdown);
printf("\n\n## Tracepoint events\n\n");
print_events(events_bc.tracepoints, s.options.markdown);
printf("\n\n## Plugin events\n\n");
print_events(events_bc.pluginevents, s.options.markdown);
printf("\n\n## Metaevents\n\n");
print_events(events_bc.metaevents, s.options.markdown);
return run_result::exit();
}

View File

@@ -646,5 +646,13 @@ falco::app::run_result falco::app::actions::process_events(falco::app::state& s)
}
}
// By deleting s.outputs, we make sure that the engine will wait until
// regular output has been completely sent before printing stats, avoiding
// intermixed stats with output.
// Note that this will only work if this is the last reference held by the
// shared pointer.
s.outputs.reset();
s.engine->print_stats();
return res;
}

View File

@@ -17,7 +17,7 @@ limitations under the License.
#include "actions.h"
#if defined(__linux__) && !defined(__EMSCRIPTEN__) && !defined(MINIMAL_BUILD)
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) && !defined(MINIMAL_BUILD)
#include "webserver.h"
#endif
@@ -25,7 +25,7 @@ using namespace falco::app;
using namespace falco::app::actions;
falco::app::run_result falco::app::actions::start_webserver(falco::app::state& state) {
#if defined(__linux__) && !defined(__EMSCRIPTEN__) && !defined(MINIMAL_BUILD)
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) && !defined(MINIMAL_BUILD)
if(state.is_capture_mode() || !state.config->m_webserver_enabled) {
return run_result::ok();
}
@@ -50,7 +50,7 @@ falco::app::run_result falco::app::actions::start_webserver(falco::app::state& s
}
falco::app::run_result falco::app::actions::stop_webserver(falco::app::state& state) {
#if defined(__linux__) && !defined(__EMSCRIPTEN__) && !defined(MINIMAL_BUILD)
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) && !defined(MINIMAL_BUILD)
if(state.is_capture_mode() || !state.config->m_webserver_enabled) {
return run_result::ok();
}

View File

@@ -87,8 +87,6 @@ bool falco::app::run(falco::app::state& s, bool& restart, std::string& errstr) {
std::list<app_action> const teardown_steps = {
falco::app::actions::unregister_signal_handlers,
falco::app::actions::stop_webserver,
// Note: calls print_stats internally after resetting outputs.
falco::app::actions::cleanup_outputs,
falco::app::actions::close_inspectors,
};

View File

@@ -25,35 +25,10 @@ limitations under the License.
#include <cxxopts.hpp>
#include <fstream>
#include <algorithm>
namespace falco {
namespace app {
static bool parse_output_format(const std::string &format_str,
output_format &out,
std::string &errstr) {
if(format_str.empty()) {
return true;
}
std::string lower_format = format_str;
std::transform(lower_format.begin(), lower_format.end(), lower_format.begin(), ::tolower);
if(lower_format == "text") {
out = output_format::TEXT;
} else if(lower_format == "markdown") {
out = output_format::MARKDOWN;
} else if(lower_format == "json") {
out = output_format::JSON;
} else {
errstr = "Invalid format '" + format_str + "'. Valid values are: text, markdown, json";
return false;
}
return true;
}
bool options::parse(int argc, char **argv, std::string &errstr) {
cxxopts::Options opts("falco", "Falco - Cloud Native Runtime Security");
define(opts);
@@ -106,23 +81,6 @@ bool options::parse(int argc, char **argv, std::string &errstr) {
list_fields = m_cmdline_parsed.count("list") > 0;
// Validate that both markdown and format are not specified together
if(m_cmdline_parsed.count("markdown") > 0 && m_cmdline_parsed.count("format") > 0) {
errstr = "Cannot specify both --markdown and --format options together";
return false;
}
// Parse and validate the format option
if(!format.empty()) {
if(!parse_output_format(format, output_fmt, errstr)) {
return false;
}
} else if(markdown) {
// If markdown flag is set and format is not specified, use MARKDOWN format
fprintf(stderr, "WARNING: --markdown is deprecated, use --format markdown instead.\n");
output_fmt = output_format::MARKDOWN;
}
return true;
}
@@ -152,8 +110,7 @@ void options::define(cxxopts::Options& opts)
("list-events", "List all defined syscall events, metaevents, tracepoint events and exit.", cxxopts::value<bool>(list_syscall_events))
("list-plugins", "Print info on all loaded plugins and exit.", cxxopts::value(list_plugins)->default_value("false"))
("M", "Stop Falco execution after <num_seconds> are passed.", cxxopts::value(duration_to_tot)->default_value("0"), "<num_seconds>")
("markdown", "DEPRECATED: use --format markdown instead. Print output in Markdown format when used in conjunction with --list or --list-events options. It has no effect when used with other options.", cxxopts::value<bool>(markdown))
("format", "Print output in the specified <format> when used in conjunction with --list or --list-events options. Valid values are 'text', 'markdown', or 'json'. It has no effect when used with other options. Cannot be used together with --markdown.", cxxopts::value(format), "<format>")
("markdown", "Print output in Markdown format when used in conjunction with --list or --list-events options. It has no effect when used with other options.", cxxopts::value<bool>(markdown))
("N", "Only print field names when used in conjunction with the --list option. It has no effect when used with other options.", cxxopts::value(names_only)->default_value("false"))
("o,option", "Set the value of option <opt> to <val>. Overrides values in the configuration file. <opt> can be identified using its location in the configuration file using dot notation. Elements of list entries can be accessed via square brackets [].\n E.g. base.id = val\n base.subvalue.subvalue2 = val\n base.list[1]=val", cxxopts::value(cmdline_config_options), "<opt>=<val>")
("plugin-info", "Print info for the plugin specified by <plugin_name> and exit.\nThis includes all descriptive information like name and author, along with the\nschema format for the init configuration and a list of suggested open parameters.\n<plugin_name> can be the plugin's name or its configured 'library_path'.", cxxopts::value(print_plugin_info), "<plugin_name>")

View File

@@ -18,7 +18,6 @@ limitations under the License.
#pragma once
#include <libsinsp/event.h>
#include "../../engine/output_format.h"
#include <string>
#include <vector>
@@ -58,8 +57,6 @@ public:
std::string print_plugin_info;
bool list_syscall_events = false;
bool markdown = false;
std::string format;
output_format output_fmt = output_format::TEXT;
int duration_to_tot = 0;
bool names_only = false;
std::vector<std::string> cmdline_config_options;

View File

@@ -23,7 +23,7 @@ limitations under the License.
#include "restart_handler.h"
#include "../configuration.h"
#include "../stats_writer.h"
#if defined(__linux__) && !defined(__EMSCRIPTEN__) && !defined(MINIMAL_BUILD)
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) && !defined(MINIMAL_BUILD)
#include "../webserver.h"
#endif
@@ -109,7 +109,7 @@ struct state {
// Helper responsible for watching of handling hot application restarts
std::shared_ptr<restart_handler> restarter;
#if defined(__linux__) && !defined(__EMSCRIPTEN__) && !defined(MINIMAL_BUILD)
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) && !defined(MINIMAL_BUILD)
falco_webserver webserver;
#endif
// Set by start_webserver to start prometheus metrics

View File

@@ -737,6 +737,12 @@ const char config_schema_string[] = LONG_STRING_CONST(
},
"ssl_certificate": {
"type": "string"
},
"uid": {
"type": "integer"
},
"gid": {
"type": "integer"
}
},
"minProperties": 1,

View File

@@ -489,6 +489,8 @@ void falco_configuration::load_yaml(const std::string &config_name) {
}
m_webserver_config.m_prometheus_metrics_enabled =
m_config.get_scalar<bool>("webserver.prometheus_metrics_enabled", false);
m_webserver_config.m_uid = m_config.get_scalar<uint32_t>("webserver.uid", 65534);
m_webserver_config.m_gid = m_config.get_scalar<uint32_t>("webserver.gid", 65534);
std::list<std::string> syscall_event_drop_acts;
m_config.get_sequence(syscall_event_drop_acts, "syscall_event_drops.actions");

View File

@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright (C) 2026 The Falco Authors.
Copyright (C) 2025 The Falco Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -31,7 +31,6 @@ limitations under the License.
#include <set>
#include <iostream>
#include <fstream>
#include <filesystem>
#include "config_falco.h"
#include "yaml_helper.h"
@@ -85,6 +84,8 @@ public:
bool m_ssl_enabled = false;
std::string m_ssl_certificate;
bool m_prometheus_metrics_enabled = false;
uint32_t m_uid = 1000;
uint32_t m_gid = 1000;
};
enum class rule_selection_operation { enable, disable };
@@ -401,35 +402,9 @@ struct convert<falco_configuration::plugin_config> {
return false;
}
rhs.m_library_path = node["library_path"].as<std::string>();
if(!rhs.m_library_path.empty() &&
!std::filesystem::path(rhs.m_library_path).is_absolute() &&
rhs.m_library_path.at(0) != '/') {
// Relative path: resolve against the plugins directory
// and verify the result stays within it.
auto full_path = std::filesystem::path(FALCO_ENGINE_PLUGINS_DIR) / rhs.m_library_path;
// lexically_normal resolves . and .. purely lexically,
// without filesystem access (unlike weakly_canonical which
// leaves .. unresolved for non-existent path components).
auto normalized = full_path.lexically_normal();
auto plugins_dir = std::filesystem::path(FALCO_ENGINE_PLUGINS_DIR).lexically_normal();
auto rel = normalized.lexically_relative(plugins_dir);
if(rel.empty()) {
throw YAML::Exception(node["library_path"].Mark(),
"plugin library_path '" +
node["library_path"].as<std::string>() +
"' resolves outside the plugins directory (" +
std::string(FALCO_ENGINE_PLUGINS_DIR) + ")");
}
for(const auto& component : rel) {
if(component == "..") {
throw YAML::Exception(node["library_path"].Mark(),
"plugin library_path '" +
node["library_path"].as<std::string>() +
"' resolves outside the plugins directory (" +
std::string(FALCO_ENGINE_PLUGINS_DIR) + ")");
}
}
rhs.m_library_path = normalized.string();
if(!rhs.m_library_path.empty() && rhs.m_library_path.at(0) != '/') {
// prepend share dir if path is not absolute
rhs.m_library_path = std::string(FALCO_ENGINE_PLUGINS_DIR) + rhs.m_library_path;
}
if(node["init_config"] && !node["init_config"].IsNull()) {

View File

@@ -28,7 +28,7 @@ limitations under the License.
#include "outputs_program.h"
#include "outputs_syslog.h"
#endif
#if defined(__linux__) && !defined(__EMSCRIPTEN__) && !defined(MINIMAL_BUILD)
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) && !defined(MINIMAL_BUILD)
#include "outputs_http.h"
#endif
@@ -93,7 +93,7 @@ void falco_outputs::add_output(const falco::outputs::config &oc) {
oo = std::make_unique<falco::outputs::output_syslog>();
}
#endif
#if defined(__linux__) && !defined(__EMSCRIPTEN__) && !defined(MINIMAL_BUILD)
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) && !defined(MINIMAL_BUILD)
else if(oc.name == "http") {
oo = std::make_unique<falco::outputs::output_http>();
}

View File

@@ -35,7 +35,7 @@ public:
const auto no_deadline = time_point{};
timeout_data curr;
while(m_is_running.load(std::memory_order_acquire)) {
auto t = m_timeout.exchange(nullptr, std::memory_order_acq_rel);
auto t = m_timeout.exchange(nullptr, std::memory_order_release);
if(t) {
curr = *t;
delete t;
@@ -56,7 +56,7 @@ public:
if(m_thread.joinable()) {
m_thread.join();
}
delete m_timeout.exchange(nullptr, std::memory_order_acq_rel);
delete m_timeout.exchange(nullptr, std::memory_order_release);
}
}

View File

@@ -21,6 +21,17 @@ limitations under the License.
#include "app/state.h"
#include "versions_info.h"
#include <atomic>
#include <signal.h>
#include <sys/wait.h>
using namespace std::chrono_literals;
namespace {
std::function<void(int)> sigchld_func;
void sigchld_handler(int signal) {
sigchld_func(signal);
}
} // namespace
falco_webserver::~falco_webserver() {
stop();
@@ -58,47 +69,95 @@ void falco_webserver::start(const falco::app::state &state,
res.set_content(versions_json_str, "application/json");
});
// run server in a separate thread
if(!m_server->is_valid()) {
m_server = nullptr;
throw falco_exception("invalid webserver configuration");
}
m_failed.store(false, std::memory_order_release);
m_server_thread = std::thread([this, webserver_config] {
sigchld_func = [&](int signal) {
wait(nullptr);
m_child_stopped = true;
};
signal(SIGCHLD, sigchld_handler);
// fork the server
m_pid = fork();
if(m_pid < 0) {
throw falco_exception("webserver: an error occurred while forking webserver");
} else if(m_pid == 0) {
falco_logger::log(falco_logger::level::INFO, "Webserver: forked\n");
int res = setgid(webserver_config.m_gid);
if(res != NOERROR) {
throw falco_exception(
std::string("webserver: an error occurred while setting group id: ") +
std::strerror(errno));
}
res = setuid(webserver_config.m_uid);
if(res != NOERROR) {
throw falco_exception(
std::string("webserver: an error occurred while setting user id: ") +
std::strerror(errno));
}
falco_logger::log(falco_logger::level::INFO,
"Webserver: process running as " +
std::to_string(webserver_config.m_uid) + ":" +
std::to_string(webserver_config.m_gid) + "\n");
try {
this->m_server->listen(webserver_config.m_listen_address,
webserver_config.m_listen_port);
} catch(std::exception &e) {
falco_logger::log(falco_logger::level::ERR,
"falco_webserver: " + std::string(e.what()) + "\n");
"Webserver error: " + std::string(e.what()) + "\n");
throw;
}
this->m_failed.store(true, std::memory_order_release);
});
} else {
std::string schema = "http";
if(webserver_config.m_ssl_enabled) {
schema = "https";
}
std::string url = schema + "://" + webserver_config.m_listen_address + ":" +
std::to_string(webserver_config.m_listen_port);
httplib::Client cli(url);
// wait for the server to actually start up
// note: is_running() is atomic
while(!m_server->is_running() && !m_failed.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
m_running = true;
if(m_failed.load(std::memory_order_acquire)) {
stop();
throw falco_exception("an error occurred while starting webserver");
const int max_retries = 10;
const std::chrono::seconds delay = 1s;
int retry = 0;
m_running = false;
m_child_stopped = false;
while(retry++ < max_retries && !m_child_stopped) {
if(auto res = cli.Get(webserver_config.m_k8s_healthz_endpoint)) {
falco_logger::log(falco_logger::level::INFO, "Webserver: successfully started\n");
m_running = true;
break;
}
std::this_thread::sleep_for(delay * retry);
}
if(!m_running) {
throw falco_exception("webserver: the server is not running");
}
}
}
void falco_webserver::stop() {
if(m_running) {
if(m_server != nullptr) {
m_server->stop();
if(m_pid > 0) {
falco_logger::log(falco_logger::level::INFO,
"Webserver: terminating process " + std::to_string(m_pid) + "\n");
int res = kill(m_pid, SIGKILL);
if(res != 0) {
falco_logger::log(
falco_logger::level::ERR,
std::string("Webserver: an error occurred while terminating process: ") +
std::strerror(errno));
}
if(m_server_thread.joinable()) {
m_server_thread.join();
}
m_server = nullptr;
m_running = false;
waitpid(m_pid, nullptr, 0);
m_pid = -1;
falco_logger::log(falco_logger::level::INFO, "Webserver: stopping process done\n");
}
m_server = nullptr;
m_running = false;
m_child_stopped = true;
}
void falco_webserver::enable_prometheus_metrics(const falco::app::state &state) {

View File

@@ -33,8 +33,8 @@ class falco_webserver {
public:
falco_webserver() = default;
virtual ~falco_webserver();
falco_webserver(falco_webserver&&) = delete;
falco_webserver& operator=(falco_webserver&&) = delete;
falco_webserver(falco_webserver&&) = default;
falco_webserver& operator=(falco_webserver&&) = default;
falco_webserver(const falco_webserver&) = delete;
falco_webserver& operator=(const falco_webserver&) = delete;
virtual void start(const falco::app::state& state,
@@ -44,7 +44,7 @@ public:
private:
bool m_running = false;
bool m_child_stopped = true;
std::unique_ptr<httplib::Server> m_server = nullptr;
std::thread m_server_thread;
std::atomic<bool> m_failed;
int m_pid = -1;
};