Skip to content

Commit d6b6d09

Browse files
Fix CI, add 147 tests, clean up code duplication (#173)
* Fix CI, add 155 tests, clean up code duplication and import side-effects - Regenerate poetry.lock to fix CI sync issue - Disable python version updates in Renovate config - Extract shared utils (sizeof_fmt, delta_fmt, count_package_prefixes) into dyana/utils.py - Extract SECURITY_EVENTS constant into dyana/constants.py - Replace module-level docker.from_env() with lazy _get_client() to prevent import-time crashes in CI (no Docker daemon required at import time) - Guard Loader import in view.py with TYPE_CHECKING to break unnecessary import chain - Fix unhashable dict bug in view_security_events (set comprehension → list) - Remove dead commented-out code from tracee.py, loader.py, pip/main.py - Add comprehensive test suite: 155 tests across 9 test files covering cli, utils, docker, view, view_legacy, tracee, loader, settings, and base/dyana - Add .pre-commit-config.yaml with ruff and pre-commit-hooks - Expand settings_test.py with 12 additional edge case tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tests): strip ANSI escape codes from CLI help output assertions Rich/Typer emits ANSI color codes in CI, causing string assertions on --help output to fail. Strip them before matching. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 99fe345 commit d6b6d09

21 files changed

+1784
-159
lines changed

.github/renovate.json5

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,8 @@
3636
separateMinorPatch: true,
3737
},
3838
{
39-
matchManagers: ["poetry", "pip_requirements"],
40-
matchDepTypes: ["python"],
41-
allowedVersions: "^3.10",
42-
enabled: true,
39+
matchPackageNames: ["python"],
40+
enabled: false,
4341
},
4442
{
4543
description: "Auto merge non-major updates",

.pre-commit-config.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
repos:
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
rev: v0.15.0
4+
hooks:
5+
- id: ruff
6+
args: [--fix]
7+
- id: ruff-format
8+
- repo: https://github.com/pre-commit/pre-commit-hooks
9+
rev: v5.0.0
10+
hooks:
11+
- id: trailing-whitespace
12+
- id: end-of-file-fixer
13+
- id: check-yaml
14+
- id: check-added-large-files

dyana/cli_test.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import json
2+
import re
3+
import sys
4+
import typing as t
5+
from unittest.mock import MagicMock
6+
7+
# Mock cysimdjson before importing cli (not available on macOS ARM)
8+
_mock_cysimdjson = MagicMock() # noqa: E402
9+
10+
11+
class _FakeJSONParser:
12+
def loads(self, raw: str) -> t.Any:
13+
return json.loads(raw)
14+
15+
16+
_mock_cysimdjson.JSONParser = _FakeJSONParser
17+
sys.modules.setdefault("cysimdjson", _mock_cysimdjson)
18+
19+
from typer.testing import CliRunner # noqa: E402
20+
21+
from dyana.cli import cli # noqa: E402
22+
23+
runner = CliRunner()
24+
25+
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
26+
27+
28+
def _strip_ansi(text: str) -> str:
29+
return _ANSI_RE.sub("", text)
30+
31+
32+
class TestCLIHelp:
33+
def test_help(self) -> None:
34+
result = runner.invoke(cli, ["--help"])
35+
assert result.exit_code == 0
36+
assert "Blackbox profiler" in _strip_ansi(result.output)
37+
38+
def test_trace_help(self) -> None:
39+
result = runner.invoke(cli, ["trace", "--help"])
40+
assert result.exit_code == 0
41+
output = _strip_ansi(result.output)
42+
assert "--loader" in output
43+
assert "--timeout" in output
44+
45+
def test_summary_help(self) -> None:
46+
result = runner.invoke(cli, ["summary", "--help"])
47+
assert result.exit_code == 0
48+
assert "--trace-path" in _strip_ansi(result.output)
49+
50+
def test_help_command_help(self) -> None:
51+
result = runner.invoke(cli, ["help", "--help"])
52+
assert result.exit_code == 0
53+
assert "LOADER" in _strip_ansi(result.output)
54+
55+
def test_loaders_help(self) -> None:
56+
result = runner.invoke(cli, ["loaders", "--help"])
57+
assert result.exit_code == 0
58+
assert "--build" in _strip_ansi(result.output)
59+
60+
61+
class TestSummaryCommand:
62+
def test_summary_with_modern_trace(self, tmp_path: t.Any) -> None:
63+
trace_data = {
64+
"started_at": "2024-01-01T00:00:00",
65+
"ended_at": "2024-01-01T00:01:00",
66+
"platform": "Linux-6.1.0-x86_64",
67+
"run": {
68+
"loader_name": "test-loader",
69+
"build_platform": None,
70+
"build_args": None,
71+
"arguments": None,
72+
"volumes": None,
73+
"errors": None,
74+
"warnings": None,
75+
"stdout": None,
76+
"stderr": None,
77+
"exit_code": None,
78+
"stages": [
79+
{
80+
"name": "start",
81+
"timestamp": 0,
82+
"ram": 1024,
83+
"gpu": None,
84+
"disk": 2048,
85+
"network": {},
86+
"imports": {},
87+
},
88+
{
89+
"name": "end",
90+
"timestamp": 1000,
91+
"ram": 4096,
92+
"gpu": None,
93+
"disk": 4096,
94+
"network": {},
95+
"imports": {"torch": "/usr/lib/torch/__init__.py"},
96+
},
97+
],
98+
"extra": None,
99+
},
100+
"events": [],
101+
}
102+
trace_file = tmp_path / "trace.json"
103+
trace_file.write_text(json.dumps(trace_data))
104+
105+
result = runner.invoke(cli, ["summary", "--trace-path", str(trace_file)])
106+
assert result.exit_code == 0
107+
assert "test-loader" in result.output
108+
assert "RAM Usage" in result.output
109+
assert "Disk Usage" in result.output
110+
111+
def test_summary_with_legacy_trace(self, tmp_path: t.Any) -> None:
112+
trace_data = {
113+
"started_at": "2024-01-01T00:00:00",
114+
"ended_at": "2024-01-01T00:01:00",
115+
"platform": "Linux-6.1.0-x86_64",
116+
"run": {
117+
"loader_name": "test-loader",
118+
"build_platform": None,
119+
"build_args": None,
120+
"arguments": None,
121+
"volumes": None,
122+
"errors": None,
123+
"warnings": None,
124+
"stdout": None,
125+
"stderr": None,
126+
"exit_code": None,
127+
"ram": {"start": 1024, "end": 2048},
128+
"gpu": {},
129+
"disk": {"start": 2048, "end": 4096},
130+
"network": {},
131+
"extra": None,
132+
},
133+
"events": [],
134+
}
135+
trace_file = tmp_path / "trace.json"
136+
trace_file.write_text(json.dumps(trace_data))
137+
138+
result = runner.invoke(cli, ["summary", "--trace-path", str(trace_file)])
139+
assert result.exit_code == 0
140+
assert "test-loader" in result.output
141+
assert "WARNING" in result.output
142+
143+
def test_summary_missing_file(self) -> None:
144+
result = runner.invoke(cli, ["summary", "--trace-path", "/nonexistent/trace.json"])
145+
assert result.exit_code != 0

dyana/conftest.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import typing as t
2+
from unittest.mock import MagicMock, patch
3+
4+
import pytest
5+
6+
7+
@pytest.fixture
8+
def sample_run_dict() -> dict[str, t.Any]:
9+
return {
10+
"loader_name": "test-loader",
11+
"build_platform": None,
12+
"build_args": None,
13+
"arguments": None,
14+
"volumes": None,
15+
"errors": None,
16+
"warnings": None,
17+
"stdout": None,
18+
"stderr": None,
19+
"exit_code": None,
20+
"stages": None,
21+
"extra": None,
22+
}
23+
24+
25+
@pytest.fixture
26+
def sample_trace_dict(sample_run_dict: dict[str, t.Any]) -> dict[str, t.Any]:
27+
return {
28+
"started_at": "2024-01-01T00:00:00",
29+
"ended_at": "2024-01-01T00:01:00",
30+
"platform": "Linux-6.1.0-x86_64",
31+
"tracee_version": None,
32+
"tracee_kernel_release": None,
33+
"dyana_version": "0.1.4",
34+
"run": sample_run_dict,
35+
"events": [],
36+
}
37+
38+
39+
@pytest.fixture
40+
def sample_trace_with_events(sample_run_dict: dict[str, t.Any]) -> dict[str, t.Any]:
41+
return {
42+
"started_at": "2024-01-01T00:00:00",
43+
"ended_at": "2024-01-01T00:01:00",
44+
"platform": "Linux-6.1.0-x86_64",
45+
"tracee_version": None,
46+
"tracee_kernel_release": None,
47+
"dyana_version": "0.1.4",
48+
"run": sample_run_dict,
49+
"events": [
50+
{
51+
"eventName": "sched_process_exec",
52+
"timestamp": 1000,
53+
"processId": 1,
54+
"parentProcessId": 0,
55+
"processName": "python",
56+
"syscall": "execve",
57+
"containerId": "abc123",
58+
"args": [
59+
{"name": "cmdpath", "value": "/usr/bin/python"},
60+
{"name": "argv", "value": ["python", "main.py"]},
61+
],
62+
},
63+
{
64+
"eventName": "security_file_open",
65+
"timestamp": 2000,
66+
"processId": 1,
67+
"processName": "python",
68+
"containerId": "abc123",
69+
"args": [
70+
{"name": "syscall_pathname", "value": "/app/main.py"},
71+
{"name": "pathname", "value": "/app/main.py"},
72+
],
73+
},
74+
{
75+
"eventName": "security_socket_connect",
76+
"timestamp": 3000,
77+
"processId": 1,
78+
"processName": "python",
79+
"syscall": "connect",
80+
"containerId": "abc123",
81+
"args": [
82+
{
83+
"name": "remote_addr",
84+
"value": {
85+
"sa_family": "AF_INET",
86+
"sin_addr": "93.184.216.34",
87+
"sin_port": 443,
88+
},
89+
},
90+
],
91+
},
92+
{
93+
"eventName": "net_packet_dns",
94+
"timestamp": 2500,
95+
"processId": 1,
96+
"processName": "python",
97+
"containerId": "abc123",
98+
"args": [
99+
{
100+
"name": "proto_dns",
101+
"value": {
102+
"questions": [{"name": "example.com"}],
103+
"answers": [{"name": "example.com", "IP": "93.184.216.34"}],
104+
},
105+
},
106+
],
107+
},
108+
],
109+
}
110+
111+
112+
@pytest.fixture
113+
def mock_docker_client() -> t.Generator[MagicMock, None, None]:
114+
mock_client = MagicMock()
115+
with patch("dyana.docker._get_client", return_value=mock_client):
116+
yield mock_client

dyana/constants.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
SECURITY_EVENTS: list[str] = [
2+
# cd tracee/signatures/go && grep -r "EventName:" --exclude="*_test.go" * | cut -d'"' -f2 | sort -u
3+
"anti_debugging",
4+
"aslr_inspection",
5+
"cgroup_notify_on_release",
6+
"cgroup_release_agent",
7+
"core_pattern_modification",
8+
"default_loader_mod",
9+
"disk_mount",
10+
"docker_abuse",
11+
"dropped_executable",
12+
"dynamic_code_loading",
13+
"fileless_execution",
14+
"hidden_file_created",
15+
"illegitimate_shell",
16+
"k8s_api_connection",
17+
"k8s_cert_theft",
18+
# Error: invalid event to trace: k8s_service_account_token
19+
# "k8s_service_account_token",
20+
"kernel_module_loading",
21+
"ld_preload",
22+
"proc_fops_hooking",
23+
"proc_kcore_read",
24+
"proc_mem_access",
25+
"proc_mem_code_injection",
26+
"process_vm_write_inject",
27+
"ptrace_code_injection",
28+
"rcd_modification",
29+
"sched_debug_recon",
30+
"scheduled_task_mod",
31+
"stdio_over_socket",
32+
"sudoers_modification",
33+
"syscall_hooking",
34+
"system_request_key_mod",
35+
# non signature related but still security related
36+
"hidden_kernel_module",
37+
"bpf_attach",
38+
"ftrace_hook",
39+
"hooked_syscall",
40+
]

0 commit comments

Comments
 (0)