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.
- vllixn-0.2.0/LICENSE +21 -0
- vllixn-0.2.0/PKG-INFO +92 -0
- vllixn-0.2.0/README.md +74 -0
- vllixn-0.2.0/pyproject.toml +39 -0
- vllixn-0.2.0/setup.cfg +4 -0
- vllixn-0.2.0/tests/test_accounts.py +146 -0
- vllixn-0.2.0/tests/test_audit.py +92 -0
- vllixn-0.2.0/tests/test_boot.py +59 -0
- vllixn-0.2.0/tests/test_cli_report.py +199 -0
- vllixn-0.2.0/tests/test_kernel.py +106 -0
- vllixn-0.2.0/tests/test_network.py +109 -0
- vllixn-0.2.0/tests/test_patches.py +58 -0
- vllixn-0.2.0/tests/test_permissions.py +62 -0
- vllixn-0.2.0/tests/test_services.py +73 -0
- vllixn-0.2.0/tests/test_ssh.py +111 -0
- vllixn-0.2.0/vllixn/__init__.py +9 -0
- vllixn-0.2.0/vllixn/checks/__init__.py +30 -0
- vllixn-0.2.0/vllixn/checks/accounts.py +400 -0
- vllixn-0.2.0/vllixn/checks/audit.py +152 -0
- vllixn-0.2.0/vllixn/checks/boot.py +117 -0
- vllixn-0.2.0/vllixn/checks/kernel.py +130 -0
- vllixn-0.2.0/vllixn/checks/network.py +191 -0
- vllixn-0.2.0/vllixn/checks/patches.py +94 -0
- vllixn-0.2.0/vllixn/checks/permissions.py +171 -0
- vllixn-0.2.0/vllixn/checks/services.py +124 -0
- vllixn-0.2.0/vllixn/checks/ssh.py +263 -0
- vllixn-0.2.0/vllixn/cli.py +108 -0
- vllixn-0.2.0/vllixn/compare.py +50 -0
- vllixn-0.2.0/vllixn/model.py +153 -0
- vllixn-0.2.0/vllixn/probe.py +250 -0
- vllixn-0.2.0/vllixn/report/__init__.py +9 -0
- vllixn-0.2.0/vllixn/report/html_export.py +263 -0
- vllixn-0.2.0/vllixn/report/json_export.py +42 -0
- vllixn-0.2.0/vllixn/report/terminal.py +109 -0
- vllixn-0.2.0/vllixn/score.py +45 -0
- vllixn-0.2.0/vllixn.egg-info/PKG-INFO +92 -0
- vllixn-0.2.0/vllixn.egg-info/SOURCES.txt +39 -0
- vllixn-0.2.0/vllixn.egg-info/dependency_links.txt +1 -0
- vllixn-0.2.0/vllixn.egg-info/entry_points.txt +2 -0
- vllixn-0.2.0/vllixn.egg-info/requires.txt +3 -0
- 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,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} == {"启动"}
|