Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ COPY . .
FROM builder AS build

ARG FACT_VERSION
ARG CARGO_ARGS=""
RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release && \
cargo build --release $CARGO_ARGS && \
cp target/release/fact fact

FROM ubi-micro-base
Expand Down
11 changes: 10 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ image:
-t $(FACT_IMAGE_NAME) \
$(CURDIR)

image-otel:
$(DOCKER) build \
-f Containerfile \
--build-arg FACT_VERSION=$(FACT_VERSION) \
--build-arg RUST_VERSION=$(RUST_VERSION) \
--build-arg CARGO_ARGS="--features otel" \
-t $(FACT_IMAGE_NAME)-otel \
$(CURDIR)

licenses:THIRD_PARTY_LICENSES.html

THIRD_PARTY_LICENSES.html:Cargo.lock
Expand Down Expand Up @@ -54,4 +63,4 @@ format:
make -C fact-ebpf format
ruff format tests/

.PHONY: tag mock-server integration-tests image image-name licenses coverage lint clean
.PHONY: tag mock-server integration-tests image image-otel image-name licenses coverage lint clean
10 changes: 8 additions & 2 deletions tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ include ../constants.mk
all: pytest

pytest: grpc-gen
pytest --image="${FACT_IMAGE_NAME}" --junit-xml=results.xml
pytest --image="${FACT_IMAGE_NAME}" --output=grpc --junit-xml=results.xml

pytest-otlp: grpc-gen
pytest --image="${FACT_IMAGE_NAME}" --output=otlp --junit-xml=results-otlp.xml

pytest-all: grpc-gen
pytest --image="${FACT_IMAGE_NAME}-otel" --output=all --junit-xml=results-all.xml

PYOUT = $(CURDIR)

Expand All @@ -29,4 +35,4 @@ clean:
rm -rf logs.tar.gz
rm -f results.xml

.PHONY: all pytest grpc-gen lint clean
.PHONY: all pytest pytest-otlp pytest-all grpc-gen lint clean
53 changes: 44 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import requests
import yaml

from server import FileActivityService
from server import EventServer, GrpcServer, OtlpServer

# Declare files holding fixtures
pytest_plugins = ['test_editors.commons']
Expand Down Expand Up @@ -67,12 +67,34 @@ def docker_client():
return docker.from_env()


def _get_output_modes(config: pytest.Config) -> list[str]:
output = config.getoption('--output')
assert isinstance(output, str)
if output == 'all':
return ['grpc', 'otlp']
return [output]


def pytest_generate_tests(metafunc: pytest.Metafunc):
if 'server' in metafunc.fixturenames:
modes = _get_output_modes(metafunc.config)
metafunc.parametrize('server', modes, indirect=True)


@pytest.fixture
def server():
def server(request: pytest.FixtureRequest):
"""
Fixture to start and stop the FileActivityService.
Start and stop an event server.

Parameterised via --output to create either a GrpcServer or an
OtlpServer. When --output=all, every test that uses this fixture
runs once per output mode.
"""
s = FileActivityService()
mode = request.param
if mode == 'otlp':
s: EventServer = OtlpServer()
else:
s = GrpcServer()
s.serve()
yield s
s.stop()
Expand Down Expand Up @@ -114,18 +136,16 @@ def fact_config(
request: pytest.FixtureRequest,
monitored_dir: str,
logs_dir: str,
server: EventServer,
):
cwd = os.getcwd()
config = {
config: dict = {
'paths': [
f'{monitored_dir}',
f'{monitored_dir}/**/*',
'/mounted/**/*',
'/container-dir/**/*',
],
'grpc': {
'url': 'http://127.0.0.1:9999',
},
'endpoint': {
'address': '127.0.0.1:9000',
'expose_metrics': True,
Expand All @@ -134,6 +154,12 @@ def fact_config(
'json': True,
'scan_interval': 0,
}

if server.output_mode == 'otlp':
config['otel'] = {'endpoint': 'http://127.0.0.1:4318/v1/logs'}
else:
config['grpc'] = {'url': 'http://127.0.0.1:9999'}

config_file = NamedTemporaryFile( # noqa: SIM115
prefix='fact-config-',
suffix='.yml',
Expand Down Expand Up @@ -190,7 +216,7 @@ def fact(
request: pytest.FixtureRequest,
docker_client: docker.DockerClient,
fact_config: tuple[dict, str],
server: FileActivityService,
server: EventServer,
logs_dir: str,
test_file: str,
):
Expand All @@ -206,6 +232,8 @@ def fact(
environment={
'FACT_LOGLEVEL': 'debug',
'FACT_HOST_MOUNT': '/host',
'OTEL_BLRP_SCHEDULE_DELAY': '100',
'OTEL_BLRP_MAX_EXPORT_BATCH_SIZE': '1',
},
name='fact',
network_mode='host',
Expand Down Expand Up @@ -264,3 +292,10 @@ def pytest_addoption(parser: pytest.Parser):
default='quay.io/stackrox-io/fact:latest',
help='The image to be used for testing',
)
parser.addoption(
'--output',
action='store',
default='grpc',
choices=['grpc', 'otlp', 'all'],
help='Output mode to test: grpc, otlp, or all (default: grpc)',
)
114 changes: 38 additions & 76 deletions tests/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ def override(func): # type: ignore[reportMissingParameterType]


import utils
from internalapi.sensor.collector_pb2 import ProcessSignal
from internalapi.sensor.sfa_pb2 import FileActivity


def extract_container_id(cgroup: str) -> str:
Expand Down Expand Up @@ -191,25 +189,26 @@ def container_id(self) -> str:
def loginuid(self) -> int:
return self._loginuid

def diff(self, other: ProcessSignal) -> dict | None:
def diff(self, other: Process) -> dict | None:
"""
Compare this Process with a ProcessSignal protobuf message.
Compare this Process with another Process instance.

PID comparison is skipped if self.pid is None.

Args:
other: ProcessSignal protobuf message to compare against
other: Process instance to compare against.

Returns:
None if identical, dict of differences if not matching
None if identical, dict of differences if not matching.
"""
diff = {}

# Compare each field
if self.pid is not None:
Event._diff_field(diff, 'pid', self.pid, other.pid)

Event._diff_field(diff, 'uid', self.uid, other.uid)
Event._diff_field(diff, 'gid', self.gid, other.gid)
Event._diff_field(diff, 'exe_path', self.exe_path, other.exec_file_path)
Event._diff_field(diff, 'exe_path', self.exe_path, other.exe_path)
Event._diff_field(diff, 'args', self.args, other.args)
Event._diff_field(diff, 'name', self.name, other.name)
Event._diff_field(
Expand All @@ -218,7 +217,7 @@ def diff(self, other: ProcessSignal) -> dict | None:
self.container_id,
other.container_id,
)
Event._diff_field(diff, 'loginuid', self.loginuid, other.login_uid)
Event._diff_field(diff, 'loginuid', self.loginuid, other.loginuid)

return diff if diff else None

Expand Down Expand Up @@ -328,128 +327,91 @@ def _diff_path(
diff: dict,
name: str,
expected: str | Pattern[str] | None,
actual: str,
actual: str | Pattern[str] | None,
):
"""
Compare paths with regex pattern support.

When expected is a compiled regex pattern, actual must be a
string that matches it. Otherwise a simple equality check is
performed.
"""
if isinstance(expected, Pattern):
if not expected.match(actual):
if not isinstance(actual, str) or not expected.match(actual):
diff[name] = {'expected': f'{expected}', 'actual': actual}
elif expected != actual:
diff[name] = {'expected': expected, 'actual': actual}

def diff(self, other: FileActivity) -> dict | None:
def diff(self, other: Event) -> dict | None:
"""
Compare this Event with a FileActivity protobuf message.
Compare this Event with another Event instance.

Both gRPC and OTLP servers translate their native messages
into Event objects, so this method provides a single
protocol-agnostic comparison path.

Args:
other: FileActivity protobuf message to compare against
other: Event instance to compare against.

Returns:
None if identical, dict of differences if not matching
None if identical, dict of differences if not matching.
"""
diff = {}

# Check process differences first
process_diff = self.process.diff(other.process)
if process_diff is not None:
diff['process'] = process_diff

# Check event type
event_type_expected = self.event_type.name.lower()
event_type_actual = other.WhichOneof('file')

Event._diff_field(
diff,
'event_type',
event_type_expected,
event_type_actual,
self.event_type,
other.event_type,
)
if diff:
return diff

# Get the appropriate event field based on type
event_field = getattr(other, event_type_expected)

# Rename handling is a bit different to the rest, since it has
# new and old paths.
if self.event_type == EventType.RENAME:
Event._diff_path(diff, 'new_file', self.file, event_field.new.path)
if self.event_type != EventType.RENAME:
Event._diff_path(diff, 'file', self.file, other.file)
Event._diff_path(diff, 'host_path', self.host_path, other.host_path)
else:
Event._diff_path(diff, 'new_file', self.file, other.file)
Event._diff_path(
diff,
'new_host_path',
self.host_path,
event_field.new.host_path,
diff, 'new_host_path', self.host_path, other.host_path
)
Event._diff_path(diff, 'old_file', self.old_file, other.old_file)
Event._diff_path(
diff,
'old_file',
self.old_file,
event_field.old.path,
diff, 'old_host_path', self.old_host_path, other.old_host_path
)
Event._diff_path(
diff,
'old_host_path',
self.old_host_path,
event_field.old.host_path,
)
return diff if diff else None

# Compare file and host_path (common to all event types)
# All event types have .activity.path and .activity.host_path
# accessed differently
Event._diff_path(diff, 'file', self.file, event_field.activity.path)
Event._diff_path(
diff,
'host_path',
self.host_path,
event_field.activity.host_path,
)

if self.event_type == EventType.PERMISSION:
Event._diff_field(diff, 'mode', self.mode, event_field.mode)
Event._diff_field(diff, 'mode', self.mode, other.mode)
elif self.event_type == EventType.OWNERSHIP:
Event._diff_field(
diff,
'owner_uid',
self.owner_uid,
event_field.uid,
diff, 'owner_uid', self.owner_uid, other.owner_uid
)
Event._diff_field(
diff,
'owner_gid',
self.owner_gid,
event_field.gid,
diff, 'owner_gid', self.owner_gid, other.owner_gid
)
elif self.event_type in (EventType.XATTR_SET, EventType.XATTR_REMOVE):
Event._diff_field(
diff,
'xattr_name',
self.xattr_name,
event_field.xattr_name,
diff, 'xattr_name', self.xattr_name, other.xattr_name
)
elif self.event_type == EventType.ACL:
Event._diff_field(
diff,
'acl_type',
self.acl_type,
event_field.acl_type,
other.acl_type,
)
if self.acl_entries is not None:
actual_entries = [
{
'tag': e.tag,
'perm': e.perm,
'id': e.id,
}
for e in event_field.entries
]
Event._diff_field(
diff,
'acl_entries',
self.acl_entries,
actual_entries,
other.acl_entries,
)

return diff if diff else None
Expand Down
1 change: 1 addition & 0 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
docker==7.1.0
grpcio==1.76.0
grpcio-tools==1.76.0
opentelemetry-proto==1.41.1
pytest==8.4.1
requests==2.32.4
pyyaml==6.0.3
Loading
Loading