vllixn 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. vllixn-0.2.0/LICENSE +21 -0
  2. vllixn-0.2.0/PKG-INFO +92 -0
  3. vllixn-0.2.0/README.md +74 -0
  4. vllixn-0.2.0/pyproject.toml +39 -0
  5. vllixn-0.2.0/setup.cfg +4 -0
  6. vllixn-0.2.0/tests/test_accounts.py +146 -0
  7. vllixn-0.2.0/tests/test_audit.py +92 -0
  8. vllixn-0.2.0/tests/test_boot.py +59 -0
  9. vllixn-0.2.0/tests/test_cli_report.py +199 -0
  10. vllixn-0.2.0/tests/test_kernel.py +106 -0
  11. vllixn-0.2.0/tests/test_network.py +109 -0
  12. vllixn-0.2.0/tests/test_patches.py +58 -0
  13. vllixn-0.2.0/tests/test_permissions.py +62 -0
  14. vllixn-0.2.0/tests/test_services.py +73 -0
  15. vllixn-0.2.0/tests/test_ssh.py +111 -0
  16. vllixn-0.2.0/vllixn/__init__.py +9 -0
  17. vllixn-0.2.0/vllixn/checks/__init__.py +30 -0
  18. vllixn-0.2.0/vllixn/checks/accounts.py +400 -0
  19. vllixn-0.2.0/vllixn/checks/audit.py +152 -0
  20. vllixn-0.2.0/vllixn/checks/boot.py +117 -0
  21. vllixn-0.2.0/vllixn/checks/kernel.py +130 -0
  22. vllixn-0.2.0/vllixn/checks/network.py +191 -0
  23. vllixn-0.2.0/vllixn/checks/patches.py +94 -0
  24. vllixn-0.2.0/vllixn/checks/permissions.py +171 -0
  25. vllixn-0.2.0/vllixn/checks/services.py +124 -0
  26. vllixn-0.2.0/vllixn/checks/ssh.py +263 -0
  27. vllixn-0.2.0/vllixn/cli.py +108 -0
  28. vllixn-0.2.0/vllixn/compare.py +50 -0
  29. vllixn-0.2.0/vllixn/model.py +153 -0
  30. vllixn-0.2.0/vllixn/probe.py +250 -0
  31. vllixn-0.2.0/vllixn/report/__init__.py +9 -0
  32. vllixn-0.2.0/vllixn/report/html_export.py +263 -0
  33. vllixn-0.2.0/vllixn/report/json_export.py +42 -0
  34. vllixn-0.2.0/vllixn/report/terminal.py +109 -0
  35. vllixn-0.2.0/vllixn/score.py +45 -0
  36. vllixn-0.2.0/vllixn.egg-info/PKG-INFO +92 -0
  37. vllixn-0.2.0/vllixn.egg-info/SOURCES.txt +39 -0
  38. vllixn-0.2.0/vllixn.egg-info/dependency_links.txt +1 -0
  39. vllixn-0.2.0/vllixn.egg-info/entry_points.txt +2 -0
  40. vllixn-0.2.0/vllixn.egg-info/requires.txt +3 -0
  41. vllixn-0.2.0/vllixn.egg-info/top_level.txt +1 -0
vllixn-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TSVMV
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
vllixn-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: vllixn
3
+ Version: 0.2.0
4
+ Summary: Read-only Linux host security checkup: audit accounts, SSH, permissions, network and kernel
5
+ License-Expression: MIT
6
+ Keywords: security,linux,hardening,audit,blue-team,cis
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Topic :: Security
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest; extra == "test"
17
+ Dynamic: license-file
18
+
19
+ # vllixn
20
+
21
+ 只读 Linux 主机安全体检工具:一条命令检查账户、SSH、文件权限、网络、服务、内核、补丁、启动与审计配置,输出中文报告、分类雷达图与 0-100 评分,还能与上一次报告对比出新增/已修复的问题。
22
+
23
+ - 纯 Python 标准库,零第三方依赖
24
+ - 全程只读:不写任何系统配置,只读取文件元数据与白名单内的只读命令(`ss`、`sysctl`、`systemctl`、`nft`、`iptables`、`ufw`、`firewall-cmd`、`getenforce`、`auditctl`、`timedatectl` 等)
25
+ - 面向蓝队:每个未通过项给出说明、建议与证据;支持白名单忽略与基线对比,便于持续跟踪加固进展
26
+ - 报告可导出自包含 HTML(纯 SVG,无 JS,可直接截图存档)与 JSON
27
+
28
+ ## 安装
29
+
30
+ ```bash
31
+ pip install vllixn
32
+ ```
33
+
34
+ ## 使用
35
+
36
+ ```bash
37
+ # 本机体检,终端输出报告与分类得分
38
+ vllixn
39
+
40
+ # 同时导出自包含 HTML(含分类雷达图,无 JS)
41
+ vllixn --html report.html
42
+
43
+ # 导出 JSON 供其他系统消费
44
+ vllixn --json report.json
45
+
46
+ # 忽略误报项(可重复传入检查项 ID)
47
+ vllixn --ignore ssh.x11_forwarding --ignore accounts.default_umask
48
+
49
+ # 与上次报告对比,看新增问题与已修复项
50
+ vllixn --baseline last.json --html now.html
51
+
52
+ # 对挂载的镜像目录做离线分析(命令探测会自动跳过)
53
+ vllixn --root /mnt/target-image
54
+
55
+ # 非 root 也能跑:需要读 /etc/shadow、防火墙规则的检查会标记为跳过并说明原因
56
+ ```
57
+
58
+ ## 检查范围
59
+
60
+ | 分类 | 内容 |
61
+ |------|------|
62
+ | 账户 | UID 0 别名、空口令、系统账户可登录 shell、重复 UID、sudo NOPASSWD、authorized_keys 权限、口令有效期(login.defs)、默认 umask、PAM 口令复杂度、登录失败锁定 |
63
+ | SSH | root 登录、口令认证、空口令、端口转发、X11 转发、MaxAuthTries、协议 1、LoginGraceTime、ClientAliveInterval、登录用户范围、HostbasedAuthentication(含 Include 子配置) |
64
+ | 文件权限 | /etc/passwd、shadow、sudoers 等敏感文件,系统目录与家目录全局可写,高风险 SUID/SGID |
65
+ | 网络 | 对外监听端口(明文协议与数据服务单独定级)、nftables/iptables/ufw/firewalld 活动规则 |
66
+ | 服务 | systemd 开机启用的高风险服务、/etc/rc.local、cron/at 访问控制 |
67
+ | 内核 | 网络加固(含 syncookies、IPv6 重定向)、信息泄露、ASLR、文件系统保护四组 sysctl 参数 |
68
+ | 补丁 | 依据 apt/dnf 更新日志判断超过 60 天未安装更新 |
69
+ | 启动 | GRUB 配置权限、高危内核模块黑名单、core dump 限制 |
70
+ | 审计 | SELinux/AppArmor 运行状态、auditd、时间同步、日志持久化 |
71
+
72
+ ## 评分
73
+
74
+ 每个未通过项按严重程度扣分:严重 -25、高 -12、中 -5、低 -2,下限 0 分。
75
+ 等级:90 优秀 / 75 良好 / 60 一般 / 40 较差 / 其余危险。
76
+ 跳过的检查项不计分。HTML 报告额外提供各分类独立雷达图,便于定位短板。
77
+
78
+ ## 开发
79
+
80
+ ```bash
81
+ # 运行测试(fixture 目录树 + 脚本化命令注入,无需真实系统)
82
+ python3 -m pytest tests/ -q
83
+
84
+ # 静态检查
85
+ python3 -m ruff check .
86
+ ```
87
+
88
+ 架构说明:`probe.py` 提供 rooted 文件访问与白名单命令执行,检查逻辑全部基于注入的 probe,可在测试中用 fixture 目录树与脚本化命令结果离线复现;`checks/` 下每个模块一个分类,向 `checks/__init__.py` 的 MODULES 注册;`compare.py` 做基线对比;`report/` 负责终端、HTML、JSON 三种渲染;`score.py` 计分。
89
+
90
+ ## License
91
+
92
+ MIT
vllixn-0.2.0/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # vllixn
2
+
3
+ 只读 Linux 主机安全体检工具:一条命令检查账户、SSH、文件权限、网络、服务、内核、补丁、启动与审计配置,输出中文报告、分类雷达图与 0-100 评分,还能与上一次报告对比出新增/已修复的问题。
4
+
5
+ - 纯 Python 标准库,零第三方依赖
6
+ - 全程只读:不写任何系统配置,只读取文件元数据与白名单内的只读命令(`ss`、`sysctl`、`systemctl`、`nft`、`iptables`、`ufw`、`firewall-cmd`、`getenforce`、`auditctl`、`timedatectl` 等)
7
+ - 面向蓝队:每个未通过项给出说明、建议与证据;支持白名单忽略与基线对比,便于持续跟踪加固进展
8
+ - 报告可导出自包含 HTML(纯 SVG,无 JS,可直接截图存档)与 JSON
9
+
10
+ ## 安装
11
+
12
+ ```bash
13
+ pip install vllixn
14
+ ```
15
+
16
+ ## 使用
17
+
18
+ ```bash
19
+ # 本机体检,终端输出报告与分类得分
20
+ vllixn
21
+
22
+ # 同时导出自包含 HTML(含分类雷达图,无 JS)
23
+ vllixn --html report.html
24
+
25
+ # 导出 JSON 供其他系统消费
26
+ vllixn --json report.json
27
+
28
+ # 忽略误报项(可重复传入检查项 ID)
29
+ vllixn --ignore ssh.x11_forwarding --ignore accounts.default_umask
30
+
31
+ # 与上次报告对比,看新增问题与已修复项
32
+ vllixn --baseline last.json --html now.html
33
+
34
+ # 对挂载的镜像目录做离线分析(命令探测会自动跳过)
35
+ vllixn --root /mnt/target-image
36
+
37
+ # 非 root 也能跑:需要读 /etc/shadow、防火墙规则的检查会标记为跳过并说明原因
38
+ ```
39
+
40
+ ## 检查范围
41
+
42
+ | 分类 | 内容 |
43
+ |------|------|
44
+ | 账户 | UID 0 别名、空口令、系统账户可登录 shell、重复 UID、sudo NOPASSWD、authorized_keys 权限、口令有效期(login.defs)、默认 umask、PAM 口令复杂度、登录失败锁定 |
45
+ | SSH | root 登录、口令认证、空口令、端口转发、X11 转发、MaxAuthTries、协议 1、LoginGraceTime、ClientAliveInterval、登录用户范围、HostbasedAuthentication(含 Include 子配置) |
46
+ | 文件权限 | /etc/passwd、shadow、sudoers 等敏感文件,系统目录与家目录全局可写,高风险 SUID/SGID |
47
+ | 网络 | 对外监听端口(明文协议与数据服务单独定级)、nftables/iptables/ufw/firewalld 活动规则 |
48
+ | 服务 | systemd 开机启用的高风险服务、/etc/rc.local、cron/at 访问控制 |
49
+ | 内核 | 网络加固(含 syncookies、IPv6 重定向)、信息泄露、ASLR、文件系统保护四组 sysctl 参数 |
50
+ | 补丁 | 依据 apt/dnf 更新日志判断超过 60 天未安装更新 |
51
+ | 启动 | GRUB 配置权限、高危内核模块黑名单、core dump 限制 |
52
+ | 审计 | SELinux/AppArmor 运行状态、auditd、时间同步、日志持久化 |
53
+
54
+ ## 评分
55
+
56
+ 每个未通过项按严重程度扣分:严重 -25、高 -12、中 -5、低 -2,下限 0 分。
57
+ 等级:90 优秀 / 75 良好 / 60 一般 / 40 较差 / 其余危险。
58
+ 跳过的检查项不计分。HTML 报告额外提供各分类独立雷达图,便于定位短板。
59
+
60
+ ## 开发
61
+
62
+ ```bash
63
+ # 运行测试(fixture 目录树 + 脚本化命令注入,无需真实系统)
64
+ python3 -m pytest tests/ -q
65
+
66
+ # 静态检查
67
+ python3 -m ruff check .
68
+ ```
69
+
70
+ 架构说明:`probe.py` 提供 rooted 文件访问与白名单命令执行,检查逻辑全部基于注入的 probe,可在测试中用 fixture 目录树与脚本化命令结果离线复现;`checks/` 下每个模块一个分类,向 `checks/__init__.py` 的 MODULES 注册;`compare.py` 做基线对比;`report/` 负责终端、HTML、JSON 三种渲染;`score.py` 计分。
71
+
72
+ ## License
73
+
74
+ MIT
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vllixn"
7
+ version = "0.2.0"
8
+ description = "Read-only Linux host security checkup: audit accounts, SSH, permissions, network and kernel"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ keywords = ["security", "linux", "hardening", "audit", "blue-team", "cis"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Operating System :: POSIX :: Linux",
18
+ "Topic :: Security",
19
+ ]
20
+ dependencies = []
21
+
22
+ [project.scripts]
23
+ vllixn = "vllixn.cli:main"
24
+
25
+ [project.optional-dependencies]
26
+ test = ["pytest"]
27
+
28
+ [tool.setuptools.packages.find]
29
+ include = ["vllixn*"]
30
+
31
+ [tool.ruff]
32
+ line-length = 100
33
+ target-version = "py311"
34
+
35
+ [tool.ruff.lint]
36
+ ignore = ["B905", "FLY002"]
37
+
38
+ [tool.ruff.lint.per-file-ignores]
39
+ "tests/*" = ["E501"]
vllixn-0.2.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,146 @@
1
+ """Tests for account and privilege checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from conftest import make_probe, write
6
+
7
+ from vllixn.checks import accounts
8
+ from vllixn.model import Severity, Status
9
+
10
+ PASSWD_BASE = "\n".join(
11
+ [
12
+ "root:x:0:0:root:/root:/bin/bash",
13
+ "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin",
14
+ "alice:x:1000:1000:Alice:/home/alice:/bin/bash",
15
+ ]
16
+ )
17
+
18
+
19
+ def test_uid_zero_alias_fails(root):
20
+ write(root, "/etc/passwd", PASSWD_BASE + "\ntoor:x:0:0:backdoor:/root:/bin/bash")
21
+ finding = accounts._uid_zero(accounts.parse_passwd(make_probe(root)))
22
+ assert finding.status is Status.FAIL
23
+ assert finding.severity is Severity.CRITICAL
24
+ assert "toor" in finding.evidence
25
+
26
+
27
+ def test_uid_zero_clean(root):
28
+ write(root, "/etc/passwd", PASSWD_BASE)
29
+ finding = accounts._uid_zero(accounts.parse_passwd(make_probe(root)))
30
+ assert finding.status is Status.PASS
31
+
32
+
33
+ def test_empty_password_detected_as_root(root):
34
+ write(root, "/etc/passwd", PASSWD_BASE)
35
+ write(root, "/etc/shadow", "root:$6$hash:1:0:99:7:::\nalice::1:0:99:7:::\n")
36
+ findings = accounts._password_fields(make_probe(root), accounts.parse_passwd(make_probe(root)))
37
+ by_id = {item.id: item for item in findings}
38
+ assert by_id["accounts.empty_password"].status is Status.FAIL
39
+ assert "alice" in by_id["accounts.empty_password"].evidence
40
+
41
+
42
+ def test_empty_password_skipped_without_root(root):
43
+ write(root, "/etc/passwd", PASSWD_BASE)
44
+ findings = accounts._password_fields(
45
+ make_probe(root, uid=1000), accounts.parse_passwd(make_probe(root, uid=1000))
46
+ )
47
+ assert all(item.id != "accounts.empty_password" or item.status is Status.SKIP for item in findings)
48
+
49
+
50
+ def test_sudo_nopasswd_found(root):
51
+ write(root, "/etc/passwd", PASSWD_BASE)
52
+ write(root, "/etc/sudoers.d/admin", "alice ALL=(ALL) NOPASSWD: ALL\n")
53
+ finding = accounts._sudo_nopasswd(make_probe(root))
54
+ assert finding.status is Status.FAIL
55
+ assert "NOPASSWD" in finding.evidence
56
+
57
+
58
+ def test_duplicate_uid(root):
59
+ write(root, "/etc/passwd", PASSWD_BASE + "\ndup:x:1000:1000:Dup:/home/dup:/bin/bash")
60
+ finding = accounts._duplicate_uids(accounts.parse_passwd(make_probe(root)))
61
+ assert finding.status is Status.FAIL
62
+ assert "UID 1000" in finding.evidence
63
+
64
+
65
+ def test_system_account_with_login_shell(root):
66
+ rows = "\n".join(
67
+ [
68
+ "root:x:0:0:root:/root:/bin/bash",
69
+ "svc:x:999:999:svc:/srv:/bin/bash",
70
+ ]
71
+ )
72
+ write(root, "/etc/passwd", rows)
73
+ finding = accounts._system_login_shells(accounts.parse_passwd(make_probe(root)))
74
+ assert finding.status is Status.FAIL
75
+ assert finding.severity is Severity.LOW
76
+
77
+
78
+ def test_writable_authorized_keys(root):
79
+ write(root, "/etc/passwd", PASSWD_BASE)
80
+ write(root, "/home/alice/.ssh/authorized_keys", "ssh-ed25519 AAAA demo\n", mode=0o666)
81
+ finding_by_id = {
82
+ item.id: item
83
+ for item in accounts._ssh_key_perms(make_probe(root), accounts.parse_passwd(make_probe(root)))
84
+ }
85
+ assert finding_by_id["accounts.ssh_key_writable"].status is Status.FAIL
86
+ assert finding_by_id["accounts.ssh_key_writable"].severity is Severity.HIGH
87
+
88
+
89
+ def test_module_run_smoke(root):
90
+ write(root, "/etc/passwd", PASSWD_BASE)
91
+ findings = accounts.run(make_probe(root))
92
+ assert findings
93
+ assert all(isinstance(item.id, str) and item.category == "账户" for item in findings)
94
+
95
+
96
+ def test_password_policy_too_long_fails(root):
97
+ write(root, "/etc/login.defs", "PASS_MAX_DAYS 999\nUMASK 027\n")
98
+ findings = {item.id: item for item in accounts._password_policy(make_probe(root))}
99
+ assert findings["accounts.password_policy"].status is Status.FAIL
100
+ assert findings["accounts.password_policy"].severity is Severity.MEDIUM
101
+
102
+
103
+ def test_password_policy_reasonable_passes(root):
104
+ write(root, "/etc/login.defs", "PASS_MAX_DAYS 90\nUMASK 027\n")
105
+ findings = {item.id: item for item in accounts._password_policy(make_probe(root))}
106
+ assert findings["accounts.password_policy"].status is Status.PASS
107
+ assert findings["accounts.default_umask"].status is Status.PASS
108
+
109
+
110
+ def test_umask_022_flagged_low(root):
111
+ write(root, "/etc/login.defs", "PASS_MAX_DAYS 90\nUMASK 022\n")
112
+ findings = {item.id: item for item in accounts._password_policy(make_probe(root))}
113
+ assert findings["accounts.default_umask"].status is Status.FAIL
114
+ assert findings["accounts.default_umask"].severity is Severity.LOW
115
+
116
+
117
+ def test_pam_pwquality_missing_fails(root):
118
+ write(root, "/etc/pam.d/common-password", "password requisite pam_unix.so\n")
119
+ assert accounts._pam_quality(make_probe(root)).status is Status.FAIL
120
+
121
+
122
+ def test_pam_pwquality_enabled_passes(root):
123
+ write(root, "/etc/pam.d/common-password", "password requisite pam_pwquality.so retry=3\n")
124
+ assert accounts._pam_quality(make_probe(root)).status is Status.PASS
125
+
126
+
127
+ def test_pam_pwquality_short_minlen_low(root):
128
+ write(root, "/etc/pam.d/common-password", "password requisite pam_pwquality.so\n")
129
+ write(root, "/etc/security/pwquality.conf", "minlen = 6\n")
130
+ finding = accounts._pam_quality(make_probe(root))
131
+ assert finding.status is Status.FAIL
132
+ assert finding.severity is Severity.LOW
133
+
134
+
135
+ def test_pam_faillock_missing_fails(root):
136
+ write(root, "/etc/pam.d/common-auth", "auth required pam_unix.so\n")
137
+ assert accounts._pam_faillock(make_probe(root)).status is Status.FAIL
138
+
139
+
140
+ def test_pam_faillock_enabled_passes(root):
141
+ write(root, "/etc/pam.d/common-auth", "auth required pam_faillock.so deny=5\n")
142
+ assert accounts._pam_faillock(make_probe(root)).status is Status.PASS
143
+
144
+
145
+ def test_pam_faillock_no_pam_files_skips(root):
146
+ assert accounts._pam_faillock(make_probe(root)).status is Status.SKIP
@@ -0,0 +1,92 @@
1
+ """Tests for audit and hardening layer checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from conftest import make_probe, write
6
+
7
+ from vllixn.checks import audit
8
+ from vllixn.model import Severity, Status
9
+ from vllixn.probe import CommandResult
10
+
11
+ DATECTL_SYNCED = "System clock synchronized: yes\nNTP service: active\n"
12
+ DATECTL_UNSYNCED = "System clock synchronized: no\nNTP service: inactive\n"
13
+
14
+
15
+ def test_selinux_enforcing_passes(root):
16
+ probe = make_probe(root, {("getenforce",): CommandResult((), 0, "Enforcing\n")})
17
+ assert audit._mac(probe).status is Status.PASS
18
+
19
+
20
+ def test_selinux_permissive_is_medium(root):
21
+ probe = make_probe(root, {("getenforce",): CommandResult((), 0, "Permissive\n")})
22
+ finding = audit._mac(probe)
23
+ assert finding.status is Status.FAIL
24
+ assert finding.severity is Severity.MEDIUM
25
+
26
+
27
+ def test_apparmor_enabled_passes(root):
28
+ write(root, "/sys/module/apparmor/parameters/enabled", "Y\n")
29
+ assert audit._mac(make_probe(root)).status is Status.PASS
30
+
31
+
32
+ def test_no_mac_layer_fails_low(root):
33
+ finding = audit._mac(make_probe(root))
34
+ assert finding.status is Status.FAIL
35
+ assert finding.severity is Severity.LOW
36
+
37
+
38
+ def test_auditd_enabled_passes(root):
39
+ probe = make_probe(root, {("auditctl", "-s"): CommandResult((), 0, "enabled 1\npid 100\n")})
40
+ assert audit._auditd(probe).status is Status.PASS
41
+
42
+
43
+ def test_auditd_disabled_fails(root):
44
+ probe = make_probe(root, {("auditctl", "-s"): CommandResult((), 0, "enabled 0\n")})
45
+ finding = audit._auditd(probe)
46
+ assert finding.status is Status.FAIL
47
+ assert finding.severity is Severity.MEDIUM
48
+
49
+
50
+ def test_auditd_absent_fails_low(root):
51
+ finding = audit._auditd(make_probe(root))
52
+ assert finding.status is Status.FAIL
53
+ assert finding.severity is Severity.LOW
54
+
55
+
56
+ def test_auditd_installed_but_unreadable_skips(root):
57
+ write(root, "/etc/audit/auditd.conf", "log_file = /var/log/audit/audit.log\n")
58
+ finding = audit._auditd(make_probe(root))
59
+ assert finding.status is Status.SKIP
60
+
61
+
62
+ def test_time_synced_passes(root):
63
+ probe = make_probe(root, {("timedatectl",): CommandResult((), 0, DATECTL_SYNCED)})
64
+ assert audit._time_sync(probe).status is Status.PASS
65
+
66
+
67
+ def test_time_unsynced_fails(root):
68
+ probe = make_probe(root, {("timedatectl",): CommandResult((), 0, DATECTL_UNSYNCED)})
69
+ finding = audit._time_sync(probe)
70
+ assert finding.status is Status.FAIL
71
+ assert finding.severity is Severity.LOW
72
+
73
+
74
+ def test_time_sync_config_only_skips(root):
75
+ write(root, "/etc/chrony.conf", "pool pool.ntp.org iburst\n")
76
+ assert audit._time_sync(make_probe(root)).status is Status.SKIP
77
+
78
+
79
+ def test_persistent_logs_rsyslog_passes(root):
80
+ write(root, "/etc/rsyslog.conf", "*.* /var/log/syslog\n")
81
+ assert audit._persistent_logs(make_probe(root)).status is Status.PASS
82
+
83
+
84
+ def test_persistent_logs_missing_fails(root):
85
+ finding = audit._persistent_logs(make_probe(root))
86
+ assert finding.status is Status.FAIL
87
+ assert finding.severity is Severity.LOW
88
+
89
+
90
+ def test_persistent_logs_journald_dir_passes(root):
91
+ (root / "var" / "log" / "journal").mkdir(parents=True)
92
+ assert audit._persistent_logs(make_probe(root)).status is Status.PASS
@@ -0,0 +1,59 @@
1
+ """Tests for boot loader, module blacklist and core dump checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from conftest import make_probe, write
6
+
7
+ from vllixn.checks import boot
8
+ from vllixn.model import Severity, Status
9
+
10
+
11
+ def test_grub_world_writable_is_high(root):
12
+ write(root, "/boot/grub/grub.cfg", "set default=0\n", mode=0o666)
13
+ finding = boot._grub_perms(make_probe(root))
14
+ assert finding.status is Status.FAIL
15
+ assert finding.severity is Severity.HIGH
16
+ assert "grub.cfg" in finding.evidence
17
+
18
+
19
+ def test_grub_clean_passes(root):
20
+ write(root, "/boot/grub/grub.cfg", "set default=0\n", mode=0o600)
21
+ assert boot._grub_perms(make_probe(root)).status is Status.PASS
22
+
23
+
24
+ def test_grub_missing_skips(root):
25
+ assert boot._grub_perms(make_probe(root)).status is Status.SKIP
26
+
27
+
28
+ def test_module_blacklist_empty_fails_low(root):
29
+ write(root, "/etc/modprobe.d/dummy.conf", "# nothing\n")
30
+ finding = boot._module_blacklist(make_probe(root))
31
+ assert finding.status is Status.FAIL
32
+ assert finding.severity is Severity.LOW
33
+
34
+
35
+ def test_module_blacklist_full_passes(root):
36
+ lines = "\n".join(f"blacklist {name}" for name in boot.RISKY_MODULES)
37
+ write(root, "/etc/modprobe.d/cis.conf", lines + "\n")
38
+ assert boot._module_blacklist(make_probe(root)).status is Status.PASS
39
+
40
+
41
+ def test_core_dump_limits_conf_passes(root):
42
+ write(root, "/etc/security/limits.conf", "* hard core 0\n")
43
+ assert boot._core_dump(make_probe(root)).status is Status.PASS
44
+
45
+
46
+ def test_core_dump_systemd_config_passes(root):
47
+ write(root, "/etc/systemd/coredump.conf", "[Coredump]\nStorage=none\n")
48
+ assert boot._core_dump(make_probe(root)).status is Status.PASS
49
+
50
+
51
+ def test_core_dump_missing_fails(root):
52
+ finding = boot._core_dump(make_probe(root))
53
+ assert finding.status is Status.FAIL
54
+ assert finding.severity is Severity.LOW
55
+
56
+
57
+ def test_module_smoke(root):
58
+ findings = boot.run(make_probe(root))
59
+ assert {item.category for item in findings} == {"启动"}