Skip to content

Commit fe4c8b8

Browse files
authored
Update SSI auto-injection tests to validate workload selection policies (#6501)
Co-authored-by: anna.cai <anna.cai@datadoghq.com>
1 parent 663c4dc commit fe4c8b8

File tree

4 files changed

+92
-83
lines changed

4 files changed

+92
-83
lines changed

manifests/java.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2765,7 +2765,6 @@ manifest:
27652765
tests/auto_inject/test_auto_inject_install.py::TestSimpleInstallerAutoInjectManualProfiling::test_profiling:
27662766
- declaration: bug (SCP-962)
27672767
component_version: '>=1.5.0'
2768-
tests/auto_inject/test_blocklist_auto_inject.py::TestAutoInjectBlockListInstallManualHost::test_builtin_block_args: bug (INPLAT-1018)
27692768
tests/debugger/test_debugger_capture_expressions.py::Test_Debugger_Line_Capture_Expressions:
27702769
- weblog_declaration:
27712770
"*": missing_feature

tests/auto_inject/test_blocklist_auto_inject.py

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from utils.onboarding.injection_log_parser import command_injection_skipped
66

77

8-
class _AutoInjectBlockListBaseTest:
9-
"""Base class to test the block list on auto instrumentation"""
8+
class _AutoInjectWorkloadSelectionBaseTest:
9+
"""Base class to test workload selection policies on auto instrumentation."""
1010

1111
def _execute_remote_command(self, ssh_client, command):
1212
"""Execute remote command and get remote log file from the vm. You can use this method using env variables or using injection config file"""
@@ -32,35 +32,37 @@ def _execute_remote_command(self, ssh_client, command):
3232
@features.host_block_list
3333
@scenarios.installer_auto_injection
3434
@irrelevant(condition=context.weblog_variant == "test-app-dotnet-iis")
35-
class TestAutoInjectBlockListInstallManualHost(_AutoInjectBlockListBaseTest):
36-
builtin_args_commands_block = {
35+
class TestAutoInjectWorkloadSelectionInstallManualHost(_AutoInjectWorkloadSelectionBaseTest):
36+
"""Test that auto instrumentation respects workload selection policies (excluded specific commands and args)."""
37+
38+
# Commands with args excluded by workload selection policy per language (should not be instrumented)
39+
commands_excluded_by_workload_policy = {
3740
"java": ["java -version", "MY_ENV_VAR=hello java -version"],
38-
"donet": [
41+
"dotnet": [
3942
"dotnet restore",
4043
"dotnet build -c Release",
41-
"sudo -E dotnet publish",
44+
"dotnet publish",
4245
"MY_ENV_VAR=hello dotnet build -c Release",
4346
],
4447
}
4548

46-
builtin_args_commands_injected = {
49+
# Commands with args included by workload selection policy per language (should be instrumented)
50+
commands_not_excluded_by_workload_policy = {
4751
"java": [
4852
"java -jar myjar.jar",
4953
"sudo -E java -jar myjar.jar",
5054
"version=-version java -jar myjar.jar",
5155
"java -Dversion=-version -jar myapp.jar",
5256
],
53-
"donet": [
57+
"dotnet": [
5458
"dotnet run -- -p build",
5559
"dotnet build.dll -- -p build",
5660
"sudo -E dotnet run myapp.dll -- -p build",
57-
"sudo dotnet publish",
5861
"MY_ENV_VAR=build dotnet myapp.dll",
5962
],
6063
}
6164

62-
builtin_commands_not_injected = [
63-
"ps -fea",
65+
no_language_found_commands = [
6466
"touch myfile.txt",
6567
"hello=hola cat myfile.txt",
6668
"ls -la",
@@ -72,45 +74,51 @@ class TestAutoInjectBlockListInstallManualHost(_AutoInjectBlockListBaseTest):
7274
or "alpine" in context.weblog_variant
7375
or "buildpack" in context.weblog_variant
7476
)
75-
def test_builtin_block_commands(self):
76-
"""Check that commands are skipped from the auto injection. This commands are defined on the buildIn processes to block"""
77+
def test_no_language_found_commands(self):
78+
"""Check that commands with no language found are skipped from auto injection."""
7779
virtual_machine = context.virtual_machine
78-
logger.info(f"[{virtual_machine.get_ip()}] Executing commands that should be blocked")
80+
logger.info(f"[{virtual_machine.get_ip()}] Executing commands with no language found")
7981
ssh_client = virtual_machine.get_ssh_connection()
80-
for command in self.builtin_commands_not_injected:
82+
for command in self.no_language_found_commands:
8183
local_log_file = self._execute_remote_command(ssh_client, command)
82-
assert command_injection_skipped(command, local_log_file), f"The command {command} was instrumented!"
84+
assert command_injection_skipped(command, local_log_file), (
85+
f"The command '{command}' was allowed by auto injection but should have been denied"
86+
)
8387

8488
@irrelevant(
8589
condition="container" in context.weblog_variant
8690
or "alpine" in context.weblog_variant
8791
or "buildpack" in context.weblog_variant
8892
)
89-
def test_builtin_block_args(self):
90-
"""Check that we are blocking command with args. These args are defined in the buildIn args ignore list for each language."""
93+
def test_commands_denied_by_workload_selection(self):
94+
"""Check that commands are skipped from auto injection based on workload selection policies."""
9195
virtual_machine = context.virtual_machine
92-
logger.info(f"[{virtual_machine.get_ip()}] Executing test_builtIn_block_args")
96+
logger.info(f"[{virtual_machine.get_ip()}] Executing commands that are denied by workload selection policies")
9397
language = context.library.name
94-
if language in self.builtin_args_commands_block:
95-
ssh_client = virtual_machine.get_ssh_connection()
96-
for command in self.builtin_args_commands_block[language]:
97-
local_log_file = self._execute_remote_command(ssh_client, command)
98-
assert command_injection_skipped(command, local_log_file), f"The command {command} was instrumented!"
98+
if language not in self.commands_excluded_by_workload_policy:
99+
return
100+
ssh_client = virtual_machine.get_ssh_connection()
101+
for command in self.commands_excluded_by_workload_policy[language]:
102+
local_log_file = self._execute_remote_command(ssh_client, command)
103+
assert command_injection_skipped(command, local_log_file), (
104+
f"The command '{command}' was allowed by auto injection but should have been denied"
105+
)
99106

100107
@irrelevant(
101108
condition="container" in context.weblog_variant
102109
or "alpine" in context.weblog_variant
103110
or "buildpack" in context.weblog_variant
104111
)
105-
def test_builtin_instrument_args(self):
106-
"""Check that we are instrumenting the command with args that it should be instrumented. The args are not included on the buildIn args list"""
112+
def test_commands_allowed_by_workload_selection(self):
113+
"""Check that commands are allowed to be instrumented based on workload selection policies."""
107114
virtual_machine = context.virtual_machine
108-
logger.info(f"[{virtual_machine.get_ip()}] Executing test_builtIn_instrument_args")
115+
logger.info(f"[{virtual_machine.get_ip()}] Executing commands that are allowed by workload selection policies")
109116
language = context.library.name
110-
if language in self.builtin_args_commands_injected:
111-
ssh_client = virtual_machine.get_ssh_connection()
112-
for command in self.builtin_args_commands_injected[language]:
113-
local_log_file = self._execute_remote_command(ssh_client, command)
114-
assert command_injection_skipped(command, local_log_file) is False, (
115-
f"The command {command} was not instrumented, but it should be instrumented!"
116-
)
117+
if language not in self.commands_not_excluded_by_workload_policy:
118+
return
119+
ssh_client = virtual_machine.get_ssh_connection()
120+
for command in self.commands_not_excluded_by_workload_policy[language]:
121+
local_log_file = self._execute_remote_command(ssh_client, command)
122+
assert command_injection_skipped(command, local_log_file) is False, (
123+
f"The command '{command}' was denied by auto injection but should have been allowed"
124+
)

tests/test_the_test/scenarios.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3150,13 +3150,13 @@
31503150
"tests/auto_inject/test_auto_inject_install.py::TestContainerAutoInjectInstallScriptAppsec::test_appsec": [
31513151
"CONTAINER_AUTO_INJECTION_INSTALL_SCRIPT_APPSEC"
31523152
],
3153-
"tests/auto_inject/test_blocklist_auto_inject.py::TestAutoInjectBlockListInstallManualHost::test_builtin_block_commands": [
3153+
"tests/auto_inject/test_blocklist_auto_inject.py::TestAutoInjectWorkloadSelectionInstallManualHost::test_no_language_found_commands": [
31543154
"INSTALLER_AUTO_INJECTION"
31553155
],
3156-
"tests/auto_inject/test_blocklist_auto_inject.py::TestAutoInjectBlockListInstallManualHost::test_builtin_block_args": [
3156+
"tests/auto_inject/test_blocklist_auto_inject.py::TestAutoInjectWorkloadSelectionInstallManualHost::test_commands_denied_by_workload_selection": [
31573157
"INSTALLER_AUTO_INJECTION"
31583158
],
3159-
"tests/auto_inject/test_blocklist_auto_inject.py::TestAutoInjectBlockListInstallManualHost::test_builtin_instrument_args": [
3159+
"tests/auto_inject/test_blocklist_auto_inject.py::TestAutoInjectWorkloadSelectionInstallManualHost::test_commands_allowed_by_workload_selection": [
31603160
"INSTALLER_AUTO_INJECTION"
31613161
],
31623162
"tests/debugger/test_debugger_code_origins.py::Test_Debugger_Code_Origins::test_code_origin_entry_present": [

utils/onboarding/injection_log_parser.py

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,52 @@
1+
import re
12
from collections.abc import Callable
2-
import json
33
from pathlib import Path
44

55
from utils._logger import logger
66

7+
WLS_DENIED_INJECTION = "Workload selection denied injection"
8+
WLS_ALLOWED_INJECTION = "Workload selection allowed injection: continuing"
9+
NO_KNOWN_RUNTIME = "No known runtime was detected - not injecting!"
10+
711

812
def exclude_telemetry_logs_filter(line: str):
913
return '"command":"telemetry"' not in line and '"caller":"telemetry/' not in line
1014

1115

1216
def command_injection_skipped(command_line: str, log_local_path: str):
13-
"""From parsed log, search on the list of logged commands
14-
if one command has been skipped from the instrumentation
17+
"""Determine if the given command was skipped from auto injection
18+
(e.g. by workload selection policies or no language matched).
1519
"""
16-
command, command_args = _parse_command(command_line)
17-
logger.debug(f"- Checking command: {command_args}")
18-
for command_desc in _get_commands_from_log_file(log_local_path, exclude_telemetry_logs_filter):
19-
# First line contains the name of the intercepted command
20-
first_line_json = json.loads(command_desc[0])
21-
if command in first_line_json["inFilename"]:
22-
# last line contains the skip message. The command was skipped by build-in deny list or by user deny list
23-
last_line_json = json.loads(command_desc[-1])
24-
# pylint: disable=R1705
25-
if last_line_json["msg"] == "not injecting; on deny list":
26-
logger.debug(f" Command {command_args} was skipped by build-in deny list")
27-
return True
28-
elif last_line_json["msg"] == "not injecting; on user deny list":
29-
logger.debug(f" Command {command_args} was skipped by user defined deny process list")
30-
return True
31-
elif last_line_json["msg"] in ["error injecting", "error when parsing", "skipping"] and (
32-
last_line_json["error"].startswith(
33-
(
34-
"skipping due to ignore rules for language",
35-
"error when parsing: skipping due to ignore rules for language",
36-
)
37-
)
38-
):
39-
logger.info(f" Command {command_args} was skipped by ignore arguments")
40-
return True
41-
logger.info(f" Missing injection deny: {last_line_json}")
42-
return False
20+
command, _ = _parse_command(command_line)
21+
logger.debug(f"- Checking command: {command_line}")
22+
for process_logs in _get_process_logs_from_log_file(log_local_path, exclude_telemetry_logs_filter):
23+
process_exe = _get_exe_from_log_line(process_logs[0])
24+
if process_exe is None or command != process_exe:
25+
continue
26+
if _process_chunk_means_skipped(process_logs):
27+
logger.debug(f" Command '{command_line}' was skipped by workload selection")
28+
return True
29+
logger.info(f" Command '{command_line}' was allowed and injected")
30+
return False
4331

4432
logger.info(f" Command {command} was NOT FOUND")
4533
raise ValueError(f"Command {command} was NOT FOUND")
4634

4735

36+
def _process_chunk_means_skipped(chunk: list[str]) -> bool:
37+
"""True if injection was skipped: denied by workload selection or no known runtime detected."""
38+
text = "\n".join(chunk)
39+
return WLS_DENIED_INJECTION in text or NO_KNOWN_RUNTIME in text
40+
41+
42+
def _get_exe_from_log_line(line: str) -> str | None:
43+
"""Extract executable name from the log line "process_exe: 'X'"."""
44+
match = re.search(r"process_exe:\s*['\"]([^'\"]+)['\"]", line)
45+
if match:
46+
return Path(match.group(1)).name
47+
return None
48+
49+
4850
def _parse_command(command: str):
4951
command_args = command.split()
5052
command = None
@@ -64,33 +66,33 @@ def _parse_command(command: str):
6466
return None, None
6567

6668

67-
def _get_commands_from_log_file(log_local_path: str, line_filter: Callable):
68-
"""From instrumentation log file, extract all commands parsed by dd-injection (the log level should be DEBUG)"""
69+
def _get_process_logs_from_log_file(log_local_path: str, line_filter: Callable):
70+
r"""From instrumentation log file, extract all log lines per process.
6971
70-
store_as_command = False
71-
command_lines = []
72+
A process chunk starts at the line containing \"process_exe:\" and runs until the next \"process_exe:\". This includes WLS decision
73+
lines and post-WLS lines like \"No known runtime was detected - not injecting!\".
74+
"""
75+
process_logs: list[str] = []
7276
with open(log_local_path, encoding="utf-8") as f:
7377
for line in f:
7478
if not line_filter(line):
7579
continue
76-
if "starting process" in line:
77-
store_as_command = True
80+
if "process_exe:" in line:
81+
if process_logs:
82+
yield process_logs.copy()
83+
process_logs = [line]
7884
continue
79-
if "exiting process" in line:
80-
store_as_command = False
81-
yield command_lines.copy()
82-
command_lines = []
83-
continue
84-
85-
if store_as_command:
86-
command_lines.append(line)
85+
if process_logs:
86+
process_logs.append(line)
87+
if process_logs:
88+
yield process_logs.copy()
8789

8890

8991
def main():
9092
log_file = "logs_onboarding_host_block_list/host_injection_21711f84-86b3-4125-9a5f-cd129195d99a.log"
9193
command = "java -Dversion=-version -jar myapp.jar"
9294
skipped = command_injection_skipped(command, log_file)
93-
logger.info(f"The command was skiped? {skipped}")
95+
logger.info(f"The command was skipped? {skipped}")
9496

9597

9698
if __name__ == "__main__":

0 commit comments

Comments
 (0)