[安全脚本]近期各种漏洞的Linux一键安全体检脚本(20261022)

Flanker 2026-10-22 11:50 1

nano stack_cve_audit_v2.sh

chmod +x stack_cve_audit_v2.sh

sudo ./stack_cve_audit_v2.sh


仅供参考吧



以下根据 Stack CVE Audit V2(2026 年 10 月 22 日版本)实际脚本实现整理,覆盖原先描述的 nginx、PHP、Node.js、GitLab、LiteLLM、HTTPS 证书、eMudhra/emSign、Docker、Kubernetes、WordPress 等技术栈。


检测方式说明:自动表示默认执行;静态表示读取配置或文件特征;联网表示查询漏洞数据库或指定站点;可选表示需要添加参数。


一、完整漏洞检测清单












































































































































































































































































































































































序号 分类 检测对象 可检测内容 检测方式
1 Web 服务 nginx 运行版本、2026 年 10 月已知高危 CVE 修复基线 自动
2 Web 服务 nginx TLS TLS 1.0/1.1 配置、证书验证关闭、会话票据配置 静态
3 Web 服务 nginx mTLS ssl_verify_client off/optional/optional_no_ca 静态
4 Web 服务 Traefik 运行版本、insecureSkipVerify、客户端证书验证配置 自动+静态
5 运行环境 PHP PHP CLI 版本、PHP 8.1 及更早版本的官方安全支持状态 自动
6 运行环境 Node.js 版本、Brotli CVE-2026-111497 等安全修复基线 自动
7 运行环境 Go Go 版本、crypto/x509 CVE-2026-111943 修复基线 自动
8 运行环境 Go 二进制 ELF 文件中的 Go 构建版本,发现旧版编译产物 静态
9 运行环境 Java / GraalVM Java、native-image 版本及 Maven 依赖 自动+静态
10 运行环境 Ruby Ruby 版本、Gemfile.lock 依赖 自动+静态
11 运行环境 Perl Perl 解释器版本 自动
12 运行环境 Rust rustc 版本、Cargo.lock 依赖漏洞 自动+联网
13 运行环境 Vyper Vyper 编译器版本 自动
14 运行环境 ASP.NET Core 已安装运行时版本、CVE-2026-88092 修复基线 自动
15 运行环境 Blazor Blazor WebAssembly Server 项目依赖特征、CVE-2026-96230 排查提示 静态
16 运行环境 Linux Kernel 内核版本、构建信息记录,供后续核查补丁 自动
17 AI 供应链 LiteLLM 是否安装被投毒的 1.107.0 版本 自动
18 AI 供应链 Python virtualenv 已安装包元数据、受影响依赖版本 静态
19 AI 供应链 Python 投毒 IOC cook.pth、cook.py、sitecustomize.py 可疑文件 静态
20 AI 供应链 LiteLLM Docker 受影响镜像标签、官方已知恶意镜像 Digest 自动
21 GitLab GitLab CE / EE 安装版本及 CVE-2026-97229、CVE-2026-85706 自动
22 GitLab GitLab Duo CVE-2026-96893 对应版本风险提示 自动
23 GitLab GitLab Runner Shell Executor、特权模式、TLS 校验关闭 静态
24 CI/CD GitLab CI 未固定版本的依赖安装、疑似明文密钥 静态
25 CI/CD GitHub Actions Action 使用 main/master/latest 等浮动引用 静态
26 依赖安全 Python / PyPI dist-info 软件包版本、OSV 已知漏洞 自动+联网
27 依赖安全 Node.js / npm package-lock.json、npm-shrinkwrap.json 依赖漏洞 静态+联网
28 依赖安全 PHP / Composer composer.lock 依赖漏洞 静态+联网
29 依赖安全 Go Modules go.mod 依赖漏洞 静态+联网
30 依赖安全 Rust / Cargo Cargo.lock 依赖漏洞 静态+联网
31 依赖安全 Java / Maven pom.xml 中明确声明版本的依赖漏洞 静态+联网
32 依赖安全 RubyGems Gemfile.lock 依赖漏洞 静态+联网
33 依赖安全 .NET / NuGet packages.lock.json 依赖漏洞 静态+联网
34 容器安全 Docker 容器特权模式、宿主机网络与 PID 共享 自动
35 容器安全 Docker Mount 宿主机根目录、可写 Docker Socket 挂载 自动
36 容器安全 Docker 镜像 latest/main 浮动镜像标签、LiteLLM 风险标签 自动
37 容器安全 Docker TLS NODE_TLS_REJECT_UNAUTHORIZED=0 等绕过验证的环境变量 自动
38 容器安全 Docker Compose privileged、宿主机目录及 Docker Socket 挂载 静态
39 Kubernetes Pod privileged、hostNetwork、hostPID、hostIPC、hostPath 可选 --k8s
40 Kubernetes ServiceAccount Token 自动挂载、关联高权限身份 可选 --k8s
41 Kubernetes RBAC ServiceAccount 或已认证用户组绑定 cluster-admin 可选 --k8s
42 WordPress WordPress Core 核心版本识别、调试模式 自动
43 WordPress 主题 / 插件 已安装目录、部分插件主题版本信息 静态
44 WordPress 可疑 PHP 代码 eval(base64_decode()) 等混淆执行组合 静态
45 WordPress 敏感备份文件 网站根目录的 wp-config 备份、SQL 备份 静态
46 运维面板 宝塔 BT Panel 常见安装目录、版本文件、插件数量 自动
47 系统安全 SSH Root 登录、密码登录配置 静态
48 网络安全 Redis / MySQL / MongoDB 等 高风险端口是否监听非本地地址 自动
49 网络安全 Docker API / kubelet 2375、10250 等服务端口监听风险 自动
50 配置安全 CORS nginx Access-Control-Allow-Origin: * 配置 静态

二、HTTPS、证书和 CA 信任链专项检测





































































































































序号 检测对象 具体检测项 检测方式
51 HTTPS 服务器证书 证书过期、30 天内到期 本地+指定目标
52 HTTPS 域名验证 证书域名与目标主机名不匹配 --tls-target
53 HTTPS 信任链 当前系统信任库是否接受服务端证书 --tls-target
54 HTTPS 签名算法 SHA-1、MD5 弱签名识别 本地+指定目标
55 TLS 协议 是否仍支持 TLS 1.0、TLS 1.1 --tls-target
56 eMudhra / emSign 相关 CA 根证书 SPKI SHA-256 公钥指纹匹配 自动
57 eMudhra / emSign 受影响中间 CA 的名称特征 自动+指定目标
58 eMudhra / emSign 服务端证书链是否出现疑似受影响签发者 --tls-target
59 根证书库 Linux 常见 CA Bundle 中相关根证书识别 自动
60 Python certifi 2026.10.19 修复基线 静态
61 Python pip 26.2.2 修复基线 静态
62 Python uv 0.13.4 修复基线 静态
63 Python sigstore 4.5.1 修复基线 静态
64 Rust webpki-roots 1.0.10 及相关兼容分支修复基线 静态
65 Go crypto/x509 Go 工具链和部分 ELF 构建版本 自动
66 nginx HTTPS proxy_ssl_verify off 等上游 TLS 验证关闭配置 静态
67 nginx mTLS 客户端证书可选、跳过可信 CA 验证 静态
68 Traefik TLS insecureSkipVerify、VerifyIfGiven、NoClientCert 静态
69 容器 TLS 常见关闭 TLS 校验的环境变量 自动
70 本地证书文件 PEM/CRT/CER/证书链的有效期、签名与 CA 特征 自动

三、现有脚本尚不能完整确认的内容


以下项目不能因为未报错就认定安全。


















































































检测对象 当前限制 状态
破解宝塔后门 无法完整识别任意恶意植入或隐蔽后门 仅初筛
子比破解版主题 无法确认来源、授权状态或是否遭篡改 仅初筛
WordPress 插件全部 CVE 尚未逐个按插件 Slug、版本关联完整漏洞库 未完整覆盖
GitLab 权限与变量 无法确认全部 CI/CD Protected Variables、Token 和权限设置 未完整覆盖
Docker 镜像内部漏洞 不解包扫描镜像文件系统和全部内置依赖 未完整覆盖
Kubernetes 集群整体安全 不覆盖全部 NetworkPolicy、Admission、Secret 暴露等项目 部分覆盖
Linux 内核特定提交 不自动判断 c6e50df8 是否由发行版回移补丁修复 需人工核对
Vyper 智能合约 仅识别编译器,不进行合约逻辑或链上字节码审计 未完整覆盖
TLS 证书吊销 未完成独立的 OCSP/CRL、浏览器吊销机制验证 未完整覆盖
Java / GraalVM 不能全面识别 Native Image 内打包的依赖漏洞 部分覆盖
Perl 不扫描 CPAN 全部依赖及已知 CVE 部分覆盖
SSH / 数据库公网暴露 只能判断监听地址,不能证明云安全组允许公网访问 部分覆盖
凭证泄露 不验证密钥是否真实泄露、被滥用或已经轮换 未完整覆盖
入侵取证 不做内存取证、持久化后门全面搜索和攻击时间线分析 未覆盖

四、检测规模与执行限制



















































项目 当前默认限制
OSV 漏洞数据库 最多查询 600 个软件依赖,可调整
Docker 容器 最多检查 120 个
Kubernetes Pod 最多检查 600 个
Kubernetes ClusterRoleBinding 最多检查 300 个
HTTPS 目标 最多 30 个,需指定
CA 证书解析 最多 2,400 个去重证书
本地服务证书 最多扫描 150 个文件
Go ELF 分析 最多检查 250 个候选
文件系统遍历 最多 24,000 个目录
扫描结果 Markdown + JSON

#!/usr/bin/env bash
# Stack CVE Audit v2: read-only, Python stdlib only, 2026-10-22 verified baseline
set -euo pipefail
umask 077
if ! command -v python3 >/dev/null 2>&1; then
echo '需要系统 python3 (>=3.8),不会自动安装软件。' >&2
exit 2
fi
# -I and -S deliberately prevent PYTHONPATH, user site and .pth/sitecustomize execution.
exec python3 -I -S - "$@" <<'PY'
import os, sys, re, json, time, pathlib, argparse, platform, subprocess, collections
import urllib.request
import xml.etree.ElementTree as ET
from datetime import datetime, timezone

BASELINE = '2026-10-22 / v2 TLS+PKI'
BAD_LITELLM_DIGEST = 'sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e'
REFERENCES = {
'litellm':'https://docs.litellm.ai/blog/security-incident-oct-21-2026',
'node':'https://nodejs.org/en/blog/vulnerability/october-2026-security-releases',
'nginx':'https://nginx.org/en/security_advisories.html',
'gitlab':'https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-4-2-released/',
'gitlab_old':'https://www.cve.org/CVERecord?id=CVE-2026-85706',
'dotnet':'https://github.com/dotnet/announcements/issues/472',
'dotnet_blazor':'https://github.com/dotnet/announcements/issues/478',
'osv':'https://google.github.io/osv.dev/post-v1-querybatch/',
}
findings, inventory, coverage, packages = [], {}, [], []
package_keys = set()
LEVELS = ('CRITICAL', 'HIGH', 'MEDIUM', 'REVIEW', 'INFO')

def add(level, area, title, evidence='', ref='', guidance=''):
findings.append({'level':level, 'component':area, 'title':title, 'evidence':str(evidence)[:400], 'reference':ref, 'guidance':guidance})

def cmd(argv, timeout=7):
try:
p = subprocess.run(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8', errors='replace', timeout=timeout)
return (p.stdout+'\n'+p.stderr)[:250000] if p.returncode == 0 else ''
except (OSError, subprocess.TimeoutExpired):
return ''

def version(v):
m = re.search(r'(?<!\d)(\d+)\.(\d+)(?:\.(\d+))?', v or '')
return tuple(int(x or 0) for x in m.groups()) if m else None

def earlier(v, fixed):
x = version(v)
return x is not None and x < fixed

def record_pkg(ecosystem, name, ver, source):
if not isinstance(name,str) or not isinstance(ver,str): return
name,ver = name.strip(),ver.strip()
if len(name)>150 or len(ver)>80 or not name or not ver: return
if not re.match(r'^\d', ver): return
key = (ecosystem, name.lower(), ver)
if key in package_keys: return
package_keys.add(key)
packages.append({'ecosystem':ecosystem,'name':name,'version':ver,'source':source})
if name.lower() in ('certifi','pip','uv','sigstore','webpki-roots'): installed_pkg(name.lower(),ver,source)

def check_versions():
checks = [('nginx',['nginx','-v']), ('node',['node','--version']), ('php',['php','-n','-v']),
('go',['go','version']),('java',['java','-version']),('ruby',['ruby','-v']),
('perl',['perl','-v']),('rustc',['rustc','--version']),('docker',['docker','--version']),
('traefik',['traefik','version']),('vyper',['vyper','--version']),
('openssl',['openssl','version']),('gitlab-runner',['gitlab-runner','--version']),
('native-image',['native-image','--version'])]
for name, args in checks:
out = cmd(args)
if out:
m = re.search(r'\d+\.\d+(?:\.\d+)?',out)
inventory[name] = m.group(0) if m else out.splitlines()[0][:100]
ng = inventory.get('nginx')
if ng:
v=version(ng)
if v and ((v[0],v[1])==(1,30) and v<(1,30,6) or (v[0],v[1])==(1,31) and v<(1,31,7)):
add('HIGH','nginx','版本落在 2026-10-14 官方安全修复之前',ng,REFERENCES['nginx'],
'核对 CVE-2026-15176/93479/95101/97629/97205 的模块与条件;升级或验证发行版回移补丁。')
elif v and v<(1,30,0):
add('REVIEW','nginx','较老分支可能受 2026 年 10 月漏洞影响;需核查发行版补丁',ng,REFERENCES['nginx'])
node=inventory.get('node')
if node:
v=version(node)
fixed={22:(22,23,4),24:(24,22,1),26:(26,12,1)}
if v and v[0] in fixed and v<fixed[v[0]]:
add('HIGH','Node.js','未达到 2026-10-21 安全修复版本(含 CVE-2026-111497)',node,REFERENCES['node'],
'升级当前大版本到对应安全版本,核对 TLS/DNS/Brotli 暴露条件。')
elif v and v[0] not in fixed:
add('REVIEW','Node.js','不在当期受支持的 22/24/26 更新分支',node,REFERENCES['node'])
php=inventory.get('php')
if php and version(php) and version(php)<(8,2,0):
add('HIGH','PHP','PHP 8.1 或更早分支已结束官方安全支持',php,'https://www.php.net/supported-versions.php',
'检查发行版延长维护计划,安排升级应用与运行时。')
# GitLab official package metadata, no Ruby application or plugin execution.
gitversions=[]
for pkg in ('gitlab-ce','gitlab-ee'):
out=cmd(['dpkg-query','-W','-f=${Version}',pkg],4)
if out and version(out): gitversions.append((pkg,out.strip()))
out=cmd(['rpm','-q','--qf','%{VERSION}',pkg],4)
if out and version(out): gitversions.append((pkg,out.strip()))
for pkg,v in gitversions: check_gitlab(pkg,v,'宿主机包')
# OpenSSL and package dependency checks below are advisory; OpenSSL may be backported.
dnet=cmd(['dotnet','--list-runtimes'],8)
if dnet:
inventory['dotnet_runtimes'] = [line.strip()[:160] for line in dnet.splitlines() if line.strip()][:25]
fixed={8:(8,0,32),9:(9,0,21),10:(10,0,13)}
for line in dnet.splitlines():
if not line.startswith('Microsoft.AspNetCore.App '): continue
m=re.search(r'Microsoft\.AspNetCore\.App\s+(\S+)',line)
if not m: continue
v=version(m.group(1))
if v and v[0] in fixed and v<fixed[v[0]]:
add('HIGH','ASP.NET Core','Kestrel 运行时未达到 CVE-2026-88092 修复版本',m.group(1),REFERENCES['dotnet'],
'仅说明有受影响运行时;核查应用实际加载版本及请求入口,再升级。')
elif v and v[0]==11 and ('preview' in m.group(1) or 'rc.1' in m.group(1)):
add('REVIEW','ASP.NET Core','11.0 预览/RC 运行时需核对 2026-10 安全公告',m.group(1),REFERENCES['dotnet'])

def check_gitlab(flavor, ver, source):
v=version(ver)
if not v: return
inventory.setdefault('gitlab',[]).append(flavor+' '+ver+' ('+source+')')
# CVE-2026-97229 (CE/EE) : 19.2 <19.2.8; 19.3 <19.3.5; 19.4 <19.4.2
floors={(19,2):(19,2,8),(19,3):(19,3,5),(19,4):(19,4,2)}
if (v[0],v[1]) in floors and v<floors[(v[0],v[1])]:
add('CRITICAL','GitLab','CVE-2026-97229:未授权 CI/CD 正则表达式 RCE(CVSS 10.0)',flavor+' '+ver,REFERENCES['gitlab'])
# CVE-2026-85706: 18.7..18.11.12; 19.0..19.3 per CNA; not all GitLab 19.x
old = ((v[0]==18 and v>=(18,7,0) and v<(18,11,12)) or
(v[0]==19 and v[1] in (0,1,2,3) and v<{0:(19,0,9),1:(19,1,8),2:(19,2,6),3:(19,3,2)}[v[1]]))
if old:
add('CRITICAL','GitLab','CVE-2026-85706:未授权任意文件读取(CISA KEV)',flavor+' '+ver,REFERENCES['gitlab_old'])
if 'ee' in flavor.lower() and (v[0],v[1]) in ((19,1),(19,2),(19,3),(19,4)):
duo = ((v[1] in (1,2) and v<(19,2,8)) or (v[1]==3 and v<(19,3,5)) or (v[1]==4 and v<(19,4,2)))
if duo:
add('HIGH','GitLab Duo','CVE-2026-96893 提示词注入风险:仅在启用相关 Agent 功能时可能暴露',flavor+' '+ver,REFERENCES['gitlab'])

def safe_read(path, maximum=1048576):
try:
if os.path.islink(path) or os.path.getsize(path)>maximum: return ''
with open(path,'r',encoding='utf-8',errors='replace') as f: return f.read(maximum)
except (OSError, UnicodeError): return ''

def check_config_file(path):
s=safe_read(path)
if not s: return
patterns=[
(r'(?m)^\s*proxy_ssl_verify\s+off\s*;', 'nginx 到上游关闭 TLS 证书验证','HIGH'),
(r'(?m)^\s*ssl_verify_client\s+optional_no_ca\s*;', 'nginx mTLS 客户端证书不强制可信 CA','HIGH'),
(r'(?mi)^\s*insecureSkipVerify\s*:\s*true\b', 'Traefik TLS 证书验证被关闭','HIGH'),
(r'(?mi)^\s*clientAuthType\s*:\s*["\']?VerifyIfGiven\b','Traefik 客户端证书可选:不是严格 mTLS','MEDIUM'),
(r'(?m)^\s*add_header\s+Access-Control-Allow-Origin\s+["\']?\*','CORS 允许任意 Origin,需结合鉴权检查','REVIEW'),
]
patterns += [
(r'(?mi)^\s*ssl_verify_client\s+optional\s*;', 'nginx mTLS 客户端证书 optional,不等于强制验证', 'MEDIUM'),
(r'(?mi)^\s*ssl_protocols\s+[^;]*(?:TLSv1\s|TLSv1\.1\s|TLSv1;)','nginx 配置允许旧版 TLS 协议(需检查生效上下文)','HIGH'),
(r'(?mi)^\s*ssl_client_certificate\s+[^;]+;', 'nginx 设置客户端 CA;请配合 ssl_verify_client 检查强制校验','INFO'),
(r'(?mi)^\s*insecureSkipVerify\s*[:=]\s*true\b','Traefik/代理关闭 TLS 上游校验','HIGH'),
(r'(?mi)^\s*caOptional\s*:\s*true\b','代理 mTLS CA 可选(配置需确认)','MEDIUM'),
(r'(?mi)^\s*ssl_verify_client\s+off\s*;', 'nginx TLS 客户端证书认证关闭(如要求 mTLS 则配置不符)', 'REVIEW'),
(r'(?mi)^\s*clientAuthType\s*:\s*["\']?NoClientCert\b', 'Traefik 不进行客户端证书认证(如要求 mTLS 则配置不符)','REVIEW'),
(r'(?mi)^\s*ssl_session_tickets\s+on\s*;', 'nginx TLS 会话票据启用,需管理轮换/密钥存储','REVIEW'),
]
for pat,title,level in patterns:
for m in list(re.finditer(pat,s))[:6]:
add(level,'TLS/反向代理',title,path+':'+str(s.count('\n',0,m.start())+1),guidance='静态命中,需结合 server/location 生效范围核实。')

def scan_python_site(path, maxfiles=2000):
try: names=os.listdir(path)[:maxfiles]
except OSError: return
found=set(names)
for n in ('cook.pth','cook.py','sitecustomize.py'):
if n in found:
level='CRITICAL' if n in ('cook.pth','cook.py') else 'REVIEW'
add(level,'LiteLLM 供应链','发现需调查的 Python 启动文件 '+n,
os.path.join(path,n),REFERENCES['litellm'],
'cook.pth/cook.py 为本次事件高价值 IOC;sitecustomize.py 可能由正常软件生成,必须做来源核查。')
# certifi CA bundle is not executed; inspect its data file later.
certbundle=os.path.join(path,'certifi','cacert.pem')
if os.path.isfile(certbundle): inventory.setdefault('ca_bundles',[]).append(certbundle)
for n in names:
if not n.endswith('.dist-info'): continue
meta=os.path.join(path,n,'METADATA')
data=safe_read(meta,65536)
if not data: continue
m=re.search(r'(?m)^Name:\s*(\S+)\s*$',data)
v=re.search(r'(?m)^Version:\s*(\S+)\s*$',data)
if not m or not v: continue
name,ver=m.group(1),v.group(1)
record_pkg('PyPI',name,ver,meta)
if name.lower().replace('_','-')=='litellm' and ver=='1.107.0':
add('CRITICAL','LiteLLM 供应链','发现已确认被投毒的 litellm==1.107.0',meta,REFERENCES['litellm'],
'不要运行该 venv 的 Python/pip;隔离环境,重建并轮换可触达的凭证。')

def scan_package_lock(path):
s=safe_read(path,8000000)
if not s: return
try: obj=json.loads(s)
except (ValueError,TypeError): return
# v2/v3 lockfile: installed package identity derives from node_modules path.
for key,val in list(obj.get('packages',{}).items())[:12000]:
if 'node_modules/' not in key or not isinstance(val,dict): continue
name=key.rsplit('node_modules/',1)[-1]
record_pkg('npm',name,str(val.get('version','')),path)
def nested(deps, depth=0):
if depth>12 or not isinstance(deps,dict): return
for name,data in list(deps.items())[:2000]:
if isinstance(data,dict):
record_pkg('npm',name,str(data.get('version','')),path)
nested(data.get('dependencies'),depth+1)
if not obj.get('packages'): nested(obj.get('dependencies'))

def scan_composer_lock(path):
s=safe_read(path,8000000)
if not s: return
try: obj=json.loads(s)
except (ValueError,TypeError): return
for pkg in obj.get('packages',[])+obj.get('packages-dev',[]):
if isinstance(pkg,dict): record_pkg('Packagist',str(pkg.get('name','')),str(pkg.get('version','')).lstrip('v'),path)

def scan_gem_lock(path):
s=safe_read(path,500000)
if not s: return
for m in re.finditer(r'(?m)^ ([a-zA-Z0-9_-]+) \((\d[^()\s]*)\)',s):
record_pkg('RubyGems',m.group(1),m.group(2),path)

def scan_go_mod(path):
s=safe_read(path,1500000)
for line in s.splitlines():
m=re.match(r'^\s*([A-Za-z0-9._~+/-]+)\s+v(\d[^\s]+)',line)
if m and ('/' in m.group(1) or '.' in m.group(1)):
record_pkg('Go',m.group(1),'v'+m.group(2),path)

def scan_cargo_lock(path):
s=safe_read(path,4000000)
for item in s.split('[[package]]')[1:]:
n=re.search(r'(?m)^name\s*=\s*"([^"]+)"',item)
v=re.search(r'(?m)^version\s*=\s*"([^"]+)"',item)
if n and v: record_pkg('crates.io',n.group(1),v.group(1),path)

def scan_nuget_lock(path):
s=safe_read(path,4000000)
if not s: return
try: data=json.loads(s)
except ValueError: return
for framework,pkgmap in (data.get('dependencies',{}) or {}).items():
if not isinstance(pkgmap,dict): continue
for name,item in pkgmap.items():
if isinstance(item,dict):
record_pkg('NuGet',name,str(item.get('resolved','')),path)

def scan_pom(path):
s=safe_read(path,3000000)
if not s: return
try: rt=ET.fromstring(s)
except ET.ParseError: return
# Static dependencies only; property-substituted and parent-managed versions are omitted.
for dep in rt.findall('.//{*}dependency')[:1000]:
def get(n):
x=dep.find('{*}'+n)
return x.text.strip() if x is not None and x.text else ''
group,artifact,ver=get('groupId'),get('artifactId'),get('version')
if group and artifact and ver and '${' not in ver:
record_pkg('Maven',group+':'+artifact,ver,path)

def scan_wp(root):
content=safe_read(os.path.join(root,'wp-includes','version.php'),20000)
m=re.search(r'\$wp_version\s*=\s*["\']([^"\']+)',content)
if not m: return
inventory.setdefault('wordpress',[]).append({'root':root,'version':m.group(1)})
cfgpath=os.path.join(root,'wp-config.php')
cfg=safe_read(cfgpath,128000)
if re.search(r'''define\s*\(\s*['"]WP_DEBUG['"]\s*,\s*true\s*\)''',cfg,re.I):
add('MEDIUM','WordPress','生产环境 WP_DEBUG=true',cfgpath)
# Only flag backups at the web root; never read or print their content.
try: names=os.listdir(root)
except OSError: names=[]
for n in names:
if re.search(r'(wp-config\.php\.(?:bak|old|txt|save|backup|swp)|wp-config\.bak|(?:backup|dump)\.sql(?:\.gz)?)$',n,re.I):
add('HIGH','WordPress','网站目录出现配置/数据库备份文件,需检查是否可公开下载',os.path.join(root,n))
for typ, subdir in [('plugin','plugins'),('theme','themes')]:
base=os.path.join(root,'wp-content',subdir)
try: dirs=[x for x in os.listdir(base) if os.path.isdir(os.path.join(base,x))][:120]
except OSError: dirs=[]
inventory.setdefault('wordpress_addons',[]).extend(typ+':'+x for x in dirs)
if os.path.isdir(base): wordpress_addon_static(base,typ,root)
add('INFO','WordPress','检测到 WordPress;破解版插件/主题及其后门无法仅靠版本号确定',root,
'https://wordpress.org/news/category/security/',
'请对插件/主题做来源核验和文件完整性校验(WP-CLI checksum 可在隔离环境中执行)。')

def scan_filesystem(extra_roots=()):
roots=['/etc/nginx','/etc/traefik','/opt','/srv','/var/www','/www','/home','/root','/usr/local/lib','/usr/lib']+list(extra_roots)
seen=0
skip={'.git','.cache','node_modules','vendor','__pycache__','proc','sys','tmp','logs','.npm','site-packages-backup'}
for root in roots:
if not os.path.isdir(root): continue
rootdepth=root.rstrip('/').count('/')
for base,dirs,files in os.walk(root, followlinks=False):
seen+=1
if seen>24000:
add('REVIEW','覆盖范围','目录扫描达到 24000 个目录限制;其余路径未遍历',root)
coverage.append('目录扫描到达上限')
return
depth=base.rstrip('/').count('/')-rootdepth
dirs[:]=[x for x in dirs if x not in skip and not os.path.islink(os.path.join(base,x)) and depth<7]
if os.path.basename(base) in ('site-packages','dist-packages'):
scan_python_site(base)
dirs[:]=[]
continue
for n in files:
path=os.path.join(base,n)
if n in ('nginx.conf','traefik.yml','traefik.yaml','dynamic.yml','dynamic.yaml') or (root=='/etc/nginx' and n.endswith('.conf')):
check_config_file(path)
elif n in ('package-lock.json','npm-shrinkwrap.json'):
scan_package_lock(path)
elif n=='composer.lock': scan_composer_lock(path)
elif n=='Gemfile.lock': scan_gem_lock(path)
elif n=='go.mod': scan_go_mod(path)
elif n=='Cargo.lock': scan_cargo_lock(path)
elif n=='packages.lock.json': scan_nuget_lock(path)
elif n=='pom.xml': scan_pom(path)
elif n=='version.php' and base.endswith('/wp-includes'):
scan_wp(os.path.dirname(base))
elif n.endswith('.csproj'):
s=safe_read(path,200000)
if 'Microsoft.AspNetCore.Components.WebAssembly.Server' in s:
add('REVIEW','ASP.NET Core','检测到 Blazor WebAssembly Server 依赖,核对 CVE-2026-96230 受影响版本',path,REFERENCES['dotnet_blazor'])
coverage.append('文件系统扫描完成(仅预设根目录,非全盘)')

def scan_docker():
if not cmd(['docker','version','--format','{{.Server.Version}}'],6):
coverage.append('Docker daemon 未连接/未安装/无权限:未检查容器配置与镜像')
return
ids=cmd(['docker','ps','-aq','--no-trunc'],8).splitlines()[:120]
images=cmd(['docker','images','--format','{{.Repository}}:{{.Tag}}'],8).splitlines()[:300]
inventory['docker_containers_checked']=len(ids)
inventory['docker_image_tags_checked']=len(images)
if len(ids)==120: coverage.append('Docker 容器只检查前 120 个')
for tag in images:
if 'litellm' in tag.lower() and (':1.107.0' in tag or ':v1.107.0' in tag):
add('CRITICAL','LiteLLM Docker','本地存在 1.107.0 受影响镜像标签(是否曾运行需进一步取证)',tag,REFERENCES['litellm'])
if 'litellm' in tag.lower() and any(tag.endswith(':'+x) for x in ('latest','main-latest','main-stable')):
add('REVIEW','LiteLLM Docker','本地存在曾在投毒时间窗被污染的可变镜像标签',tag,REFERENCES['litellm'],
'当前标签名本身不能证明感染;应核对历史拉取时间和真实 digest。')
for ident in ids:
raw=cmd(['docker','inspect',ident],8)
try: arr=json.loads(raw)
except (ValueError,TypeError): continue
if not isinstance(arr,list) or not arr: continue
c=arr[0]
name=str(c.get('Name','')).lstrip('/')[:100]
cfg=c.get('Config') or {}
host=c.get('HostConfig') or {}
tag=str(cfg.get('Image',''))
for env in cfg.get('Env',[]) or []:
if not isinstance(env,str) or '=' not in env:continue
k,v=env.split('=',1)
if ((k=='NODE_TLS_REJECT_UNAUTHORIZED' and v=='0') or
(k=='PYTHONHTTPSVERIFY' and v=='0') or
(k=='GIT_SSL_NO_VERIFY' and v.lower() in ('1','true','yes')) or
(k=='CURL_INSECURE' and v.lower() in ('1','true','yes'))):
add('HIGH','Docker / TLS','容器环境变量关闭 TLS 证书校验',name+' / env:'+k,
guidance='仅读取并输出变量名,敏感变量值不写入报告。')
if k in ('DJANGO_DEBUG','WP_DEBUG','APP_DEBUG') and v.lower() in ('1','true','yes'):
add('MEDIUM','Docker / 应用','容器开启应用调试',name+' / env:'+k)
if host.get('Privileged'):
add('HIGH','Docker','容器启用 privileged=true',name)
if tag.endswith(':latest') or tag.endswith(':main'):
add('REVIEW','Docker / 供应链','容器使用可变镜像标签,无法复现部署时摘要',name+' / '+tag)
if host.get('PidMode')=='host' or host.get('NetworkMode')=='host':
add('REVIEW','Docker','容器共享宿主机 PID/网络命名空间',name)
for m in c.get('Mounts') or []:
if m.get('Source')=='/' or (m.get('Source')=='/var/run/docker.sock' and not m.get('RW') is False):
add('HIGH','Docker','容器挂载宿主机根目录或可写 Docker socket',name+' -> '+str(m.get('Source')))
if re.search(r'(?:^|/)(?:gitlab-ce|gitlab-ee|gitlab/gitlab-ee|gitlab/gitlab-ce):',tag,re.I):
m=re.search(r':v?(\d+\.\d+\.\d+)',tag)
if m: check_gitlab('gitlab-ee' if 'ee' in tag else 'gitlab-ce',m.group(1),'容器标签(非镜像内实测)')
if 'litellm' in tag.lower():
if re.search(r':v?1\.107\.0(?:$|\b)',tag):
add('CRITICAL','LiteLLM Docker','存在使用 1.107.0 标签创建的容器',name+' '+tag,REFERENCES['litellm'])
digest=''
iraw=cmd(['docker','image','inspect',str(c.get('Image',''))],7)
try:
i=json.loads(iraw)[0]
digest=' '.join(i.get('RepoDigests',[]) or [])
except (ValueError,TypeError,KeyError,IndexError): pass
if BAD_LITELLM_DIGEST in digest:
add('CRITICAL','LiteLLM Docker','容器镜像 RepoDigest 命中官方已知恶意索引',name+' '+BAD_LITELLM_DIGEST,REFERENCES['litellm'])
elif re.search(r':(?:latest|main-latest|main-stable)$',tag):
add('REVIEW','LiteLLM Docker','运行容器使用曾被污染的浮动镜像标签,需调查历史构建',name+' '+tag,REFERENCES['litellm'])
coverage.append('Docker 本地容器配置和标签检查完成(未进入容器执行命令)')

def scan_k8s():
raw=cmd(['kubectl','get','pods','-A','-o','json','--request-timeout=12s'],18)
try: pods=json.loads(raw).get('items',[])
except (ValueError,TypeError,AttributeError):
coverage.append('Kubernetes 未连接/未授权:集群扫描未完成')
return
inventory['kubernetes_pods_checked']=len(pods)
for p in pods[:600]:
meta=p.get('metadata') or {}; spec=p.get('spec') or {}
label=str(meta.get('namespace',''))+'/'+str(meta.get('name',''))
if spec.get('hostNetwork') or spec.get('hostPID') or spec.get('hostIPC'):
add('REVIEW','Kubernetes','Pod 共享宿主机网络/PID/IPC 命名空间',label)
if spec.get('automountServiceAccountToken') is not False:
add('INFO','Kubernetes','未显式关闭 Pod ServiceAccount Token 自动挂载(需结合 SA 权限)',label)
for vol in spec.get('volumes',[]) or []:
if 'hostPath' in vol: add('REVIEW','Kubernetes','Pod 使用 hostPath',label)
for cont in (spec.get('containers') or [])+(spec.get('initContainers') or []):
sec=cont.get('securityContext') or {}
if sec.get('runAsUser')==0 or sec.get('runAsNonRoot') is False:
add('REVIEW','Kubernetes','容器以 root 运行或未强制非 root',label+'/'+cont.get('name',''))
if sec.get('privileged'): add('HIGH','Kubernetes','特权容器',label+'/'+cont.get('name',''))
if sec.get('allowPrivilegeEscalation') is True: add('REVIEW','Kubernetes','容器显式允许提权',label+'/'+cont.get('name',''))
bindingsraw=cmd(['kubectl','get','clusterrolebindings','-o','json','--request-timeout=8s'],12)
try: bindings=json.loads(bindingsraw).get('items',[])
except (ValueError,TypeError,AttributeError):
coverage.append('Kubernetes ClusterRoleBindings 无法读取(权限不足或未连接)')
bindings=[]
for bind in bindings[:300]:
role=bind.get('roleRef',{}) or {}
if role.get('name')!='cluster-admin':continue
for sub in bind.get('subjects',[]) or []:
typ=sub.get('kind','')
nam=sub.get('name','')
ns=sub.get('namespace','')
if typ=='ServiceAccount':
add('HIGH','Kubernetes / RBAC','ServiceAccount 持有 cluster-admin 集群管理权限',str(ns)+'/'+str(nam))
elif typ=='Group' and nam=='system:authenticated':
add('CRITICAL','Kubernetes / RBAC','所有已认证用户持有 cluster-admin 权限',str(bind.get('metadata',{}).get('name','')))
if len(bindings)>300:coverage.append('Kubernetes ClusterRoleBinding 仅检查前 300 个')
if len(pods)>600: coverage.append('Kubernetes Pod 扫描仅包含前 600 个')
coverage.append('Kubernetes Pod 配置读取完成(仅当前 kubeconfig 权限范围)')

def osv_online(limit):
if not packages:
coverage.append('未找到可用于 OSV 查询的锁文件或 Python dist-info')
return
prioritized={'litellm':0,'django':1,'requests':1,'cryptography':1,'certifi':1,'fastapi':1,'urllib3':1,'express':1,'next':1,'react':1}
queue=sorted(packages,key=lambda p:(prioritized.get(p['name'].lower(),5),p['ecosystem'],p['name']))
if len(queue)>limit:
coverage.append('OSV 仅查询 %d/%d 个包(--max-packages 可调整)'%(limit,len(queue)))
queue=queue[:limit]
count=0; failed=0
for offset in range(0,len(queue),100):
batch=queue[offset:offset+100]
pending=[(p,None) for p in batch]
seen=collections.defaultdict(set)
for _ in range(4): # support max four querybatch pages per package
if not pending: break
reqs=[]
for p,token in pending:
q={'version':p['version'],'package':{'ecosystem':p['ecosystem'],'name':p['name']}}
if token: q['page_token']=token
reqs.append(q)
try:
data=json.dumps({'queries':reqs}).encode('utf-8')
request=urllib.request.Request('https://api.osv.dev/v1/querybatch',data=data,
headers={'Content-Type':'application/json','User-Agent':'StackAudit/1.0'},method='POST')
with urllib.request.urlopen(request,timeout=14) as rsp:
obj=json.loads(rsp.read(5000000))
results=obj.get('results',[])
if len(results)!=len(pending): raise ValueError('OSV 返回数量与查询不符')
except Exception as e:
failed+=len(pending)
coverage.append('OSV 查询失败:'+type(e).__name__+'(当前批次 '+str(len(pending))+' 个包未检查)')
pending=[]
break
next_pending=[]
for (p,_token),result in zip(pending,results):
k=p['ecosystem']+'/'+p['name']+'@'+p['version']
for vuln in result.get('vulns',[]) or []:
vid=vuln.get('id','')
if vid and vid not in seen[k]:
seen[k].add(vid)
count+=1
add('REVIEW','OSV 最新依赖漏洞','已知漏洞 '+vid+':'+k,p['source'],
'https://osv.dev/vulnerability/'+vid,
'OSV 查询为已知漏洞匹配,需查看严重程度、触发条件、是否存在回移补丁。')
if result.get('next_page_token'):
next_pending.append((p,result['next_page_token']))
pending=next_pending
if pending: coverage.append('部分 OSV 匹配超过分页上限,可能不完整')
coverage.append('OSV 查询完成:%d 个候选包,%d 个漏洞记录,%d 个未成功查询'%(len(queue),count,failed))

def report(outdir):
os.makedirs(outdir,mode=0o700,exist_ok=False)
counts=collections.Counter(x['level'] for x in findings)
output={'generated_utc':datetime.now(timezone.utc).isoformat(), 'baseline':BASELINE,
'host':platform.node(),'kernel':platform.release(),'root':os.geteuid()==0,
'inventory':inventory,'coverage':coverage,'coverage_matrix':COVERAGE_MATRIX,'severity_counts':dict(counts),'findings':findings,
'osv_package_candidates':len(packages),
'limitations':['版本/配置匹配不能替代漏洞利用条件分析或入侵取证',
'发行版可能回移补丁:上游版本低并非已证实可利用',
'不会执行 venv 内 pip/python,也不会进入容器执行扫描',
'仅扫描本机和允许读取的配置;需分别检查各容器与远程节点',
'镜像浮动标签不能证明历史镜像 digest;IoC 未发现不代表机器干净',
'CA 根证书存在不代表失陷中间证书仍被接受;需结合 CT/吊销和进程信任路径',
'HTTPS 检查只使用系统默认 Python/OpenSSL,不代表浏览器/Go/Rust/Java 的验链结果一致']}
with open(os.path.join(outdir,'report.json'),'w',encoding='utf-8') as f: json.dump(output,f,ensure_ascii=False,indent=2)
def esc(v): return str(v or '').replace('|','\\|').replace('\n',' ').replace('\r',' ')[:500]
lines=['# Linux 技术栈漏洞体检','',
'- 检测时间(UTC):'+output['generated_utc'],
'- 静态规则基线:'+BASELINE,
'- 主机:'+esc(output['host']),
'- 内核:'+esc(output['kernel']),
'- root 运行:'+str(output['root']),
'- 扫描到的软件依赖版本条目:'+str(len(packages)),'',
'## 风险统计','']
for k in LEVELS: lines.append('- '+k+':'+str(counts[k]))
lines.extend(['','## 检测发现','','| 等级 | 组件 | 问题 | 证据 | 来源 |','|---|---|---|---|---|'])
for i in sorted(findings,key=lambda x:LEVELS.index(x['level'])):
lines.append('| '+ ' | '.join(esc(i[k]) for k in ('level','component','title','evidence','reference'))+' |')
lines.extend(['','## 原始架构逐项检测覆盖','','| 项目 | 覆盖方式/局限 |','|---|---|'])
for component,how in COVERAGE_MATRIX.items():lines.append('| '+esc(component)+' | '+esc(how)+' |')
lines.extend(['','## 覆盖与局限',''])
for s in coverage+output['limitations']: lines.append('- '+esc(s))
lines.extend(['','## 建议处理顺序','',
'1. LiteLLM 1.107.0/恶意 digest/可疑 cook.pth:立即隔离,不运行受影响环境,按官方建议重建与轮换凭证。',
'2. eMudhra/emSign 信任链、Go CVE-2026-111943、certifi/pip/uv/sigstore、webpki-roots:依据受影响组件的官方更新逐一升级并重建。',
'3. GitLab CVSS 10.0 与已被利用漏洞、Node.js 和 ASP.NET Core 受影响版本:核对暴露面并应用安全补丁。',
'4. 关闭非必要特权容器、宽松 mTLS 和不可信的插件/镜像来源;验证修复后再复测。',
'5. 依赖 OSV 匹配均应单独查看修复版本,报告的 REVIEW 不等于已可被利用。',''])
with open(os.path.join(outdir,'report.md'),'w',encoding='utf-8') as f: f.write('\n'.join(lines))
print('\n========== Stack CVE Audit ==========',flush=True)
for k in LEVELS: print('%-9s %d'%(k,counts[k]))
print('软件依赖候选:',len(packages))
print('扫描报告:',os.path.join(outdir,'report.md'))
print('JSON 数据:',os.path.join(outdir,'report.json'))
print('提示:检测是只读版本/配置核查,并非入侵取证;结果请人工复核。')

# --- Extended 2026-10-22 audit rules (read-only) ---
import ssl, socket, hashlib, ipaddress, glob, shlex
from datetime import timedelta

CERT_EVENT='https://blog.google/security/protecting-chrome-and-android-users-from-compromised-emudhra-intermediate-ca-certificates/'
CERT_ROOT_ADVISORY='https://www.openwall.com/lists/oss-security/2026/10/20/1'
GO_ADVISORY='https://github.com/golang/go/issues/82345'
RUST_ADVISORY='https://rustsec.org/advisories/RUSTSEC-2026-0381'
PY_CA_ADVISORY='https://discuss.python.org/t/information-for-python-users-regarding-emudhra-emsign-ca-certificate-security-incident/109576'
# Public SPKI SHA256 of trusted roots upstream of the affected intermediates.
# Being present is a *potential trust path*, not proof that a particular handshake is accepted.
RISKY_ROOT_SPKI={
'bd153ed7b0434f6886b17bce8bbe84ed340c7132d702a8f4fa318f756ecbd6f3':'AAA Certificate Services',
'c619e4780ed8e0abeda24ad732567de4383a7ea941338f6d1a7d1b49ac792fe9':'emSign Root TLS CA - G3',
'8d417db2dd8bf5e3084d1e3f196d583849d81bdd4c00c70b9d39369e96b8c782':'emSign ECC Root CA - G3',
}
# All affected eMudhra intermediate CA subject CNs. Subject matching is only screening,
# because a certificate with the same name is not necessarily the same compromised public key.
AFFECTED_ISSUERS=('EM DV TLS CA - G3B','EM DV TLS CA - G3C',
'EM OV TLS CA - G3B','EM OV TLS CA - G3C','EM EV TLS CA - G3B','EM EV TLS CA - G3C',
'EM OV CS CA - G3B','EM OV CS CA - G3C')

def run_input(args, data, timeout=7):
try:
p=subprocess.run(args,input=data,stdout=subprocess.PIPE,stderr=subprocess.PIPE,
timeout=timeout,check=False)
return p.stdout if p.returncode==0 else b''
except (OSError,subprocess.TimeoutExpired): return b''

def clean(s): return str(s).replace('\r',' ').replace('\n',' ')[:250]

def installed_pkg(name,ver,where):
vv=version(ver)
if not vv:return
ref=PY_CA_ADVISORY
if name=='certifi' and vv<(2026,10,19):
add('HIGH','证书信任库 / certifi','certifi 未达到 2026.10.19 eMudhra 事件修复基线',where+' @ '+ver,ref)
if name=='pip' and vv<(26,2,2):
add('HIGH','证书信任库 / pip','pip 内置 CA Bundle 或 TLS 栈需按公告升级至 26.2.2+',where+' @ '+ver,ref)
if name=='uv' and vv<(0,13,4):
add('HIGH','证书信任库 / uv','uv 低于 0.13.4,默认打包信任库可能仍信任受影响中间 CA',where+' @ '+ver,
'https://github.com/astral-sh/uv/security/advisories/GHSA-hq85-5cpj-mr62')
if name=='sigstore' and vv<(4,5,1):
add('HIGH','证书信任库 / sigstore','sigstore 低于 4.5.1,需核对受影响证书信任链',where+' @ '+ver,ref)
if name=='webpki-roots':
if vv[0]==1 and vv<(1,0,10):
add('HIGH','证书信任库 / Rust','RUSTSEC-2026-0381:webpki-roots 1.x 低于 1.0.10',where+' @ '+ver,RUST_ADVISORY)
elif vv[0]==0 and vv[:2]==(0,26):
# 0.26.11 reexports 1.x roots: parent 1.x must resolve to safe 1.0.10+.
if vv<(0,26,11):
add('HIGH','证书信任库 / Rust','webpki-roots 0.26 早期版本需升级并核对实际信任根',where+' @ '+ver,RUST_ADVISORY)
else:
add('REVIEW','证书信任库 / Rust','webpki-roots 0.26.11 是兼容包;须核对锁文件中最终解析的 webpki-roots 1.x >=1.0.10',where,RUST_ADVISORY)
elif vv[0]==0:
add('REVIEW','证书信任库 / Rust','旧版 webpki-roots 信任库需按公告核对',where+' @ '+ver,RUST_ADVISORY)

def check_go_ca():
gv=inventory.get('go')
if gv:
v=version(gv)
if v and ((v[0:2]==(1,26) and v<(1,26,10)) or (v[0:2]==(1,27) and v<(1,27,3)) or (v[0]==1 and v[1]<26)):
add('HIGH','Go crypto/x509','CVE-2026-111943:Go 工具链低于 CA 不信任修复基线',gv,GO_ADVISORY,
'升级 Go 后重编所有相关服务;仅检查 go version 不代表已部署二进制已修复。')
else: coverage.append('Go 工具链未发现;若二进制是静态编译 Go 服务,需另外检查编译版本。')
# go version -m reads embedded build metadata, does not run the program.
examined=0
candidates=('/usr/local/bin','/usr/bin','/opt','/srv')
for root in candidates:
if not os.path.isdir(root):continue
for base,dirs,files in os.walk(root,followlinks=False):
if base.count('/')-root.count('/')>3: dirs[:]=[];continue
dirs[:]=[x for x in dirs if x not in ('node_modules','site-packages','.git','vendor') and not os.path.islink(os.path.join(base,x))]
for name in files:
if examined>=250: break
path=os.path.join(base,name)
if os.path.islink(path):continue
try:
st=os.stat(path)
if not st.st_mode & 0o111 or st.st_size<120000 or st.st_size>250000000:continue
with open(path,'rb') as f: h=f.read(4)
if h!=b'\x7fELF':continue
except OSError:continue
examined+=1
# go tool reads metadata (no program execution)
if not gv:continue
out=cmd(['go','version','-m',path],timeout=2)
m=re.search(r'\bgo(1\.\d+(?:\.\d+)?)\b',out)
if not m:continue
v=version(m.group(1))
if v and ((v[:2]==(1,26) and v<(1,26,10)) or (v[:2]==(1,27) and v<(1,27,3)) or v[:2]<(1,26)):
add('HIGH','Go 编译产物','二进制 Go 构建版本在 CVE-2026-111943 修复前',path+' / go'+m.group(1),GO_ADVISORY,
'实际是否使用受影响 x509 校验路径需要进一步确认。')
if examined>=250:break
if examined>=250:break
coverage.append('Go ELF 静态元数据检查 %d 个候选(上限 250;未运行二进制)'%examined)

def discover_ca_bundles():
paths=set(('/etc/ssl/certs/ca-certificates.crt','/etc/pki/tls/certs/ca-bundle.crt',
'/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem',
'/etc/ssl/cert.pem','/usr/local/share/ca-certificates'))
paths.update(getattr(ssl.get_default_verify_paths(),k) for k in ('openssl_cafile','cafile')
if getattr(ssl.get_default_verify_paths(),k,None))
# local certifi pem packages discovered as part of site scanning
paths.update(inventory.get('ca_bundles',[]))
for directory in ('/etc/ssl/certs','/etc/pki/ca-trust/source/anchors','/usr/local/share/ca-certificates'):
if os.path.isdir(directory):
try:
for name in os.listdir(directory)[:600]:
if name.lower().endswith(('.pem','.crt')):paths.add(os.path.join(directory,name))
except OSError:pass
return sorted(p for p in paths if p and os.path.isfile(p) and not os.path.islink(p))[:650]

def cert_parts(pem):
return re.findall(rb'-----BEGIN CERTIFICATE-----\s*[A-Za-z0-9+/=\r\n]+-----END CERTIFICATE-----',pem)

def x509_detail(pem):
if not pem:return None
s=run_input(['openssl','x509','-noout','-subject','-issuer','-dates','-text'],pem,5).decode('utf-8','replace')
if not s:return None
def match(p):
m=re.search(p,s,re.I|re.M)
return m.group(1).strip() if m else ''
pubkey=run_input(['openssl','x509','-pubkey','-noout'],pem,5)
spki=run_input(['openssl','pkey','-pubin','-outform','DER'],pubkey,5) if pubkey else b''
fp=hashlib.sha256(spki).hexdigest() if spki else ''
return {'subject':match(r'^subject=(.*)$'), 'issuer':match(r'^issuer=(.*)$'),
'not_before':match(r'^notBefore=(.*)$'),'not_after':match(r'^notAfter=(.*)$'),
'signature':match(r'^\s*Signature Algorithm: ([^\s]+)'),
'spki_sha256':fp,'pem':pem}

def risk_cert(detail, source, trust=False):
sub,iss=detail['subject'],detail['issuer']
spki=detail.get('spki_sha256','')
root=RISKY_ROOT_SPKI.get(spki)
if root:
add('HIGH' if not trust else 'REVIEW','eMudhra / emSign CA','发现相关根 CA 公钥(不代表仍接受被吊销中间证书)',
clean(source)+' / '+root+' / SPKI='+spki[:16],CERT_ROOT_ADVISORY,
'须结合根库更新、吊销机制、交叉签发链检查,不能仅以根 CA 存在断言可利用。')
if any(x.casefold() in sub.casefold() for x in AFFECTED_ISSUERS):
add('CRITICAL' if not trust else 'HIGH','eMudhra / emSign CA','发现与已失陷中间 CA 同名的证书,需核对完整 SPKI',
clean(source)+' / '+sub,CERT_EVENT)
elif any(x.casefold() in iss.casefold() for x in AFFECTED_ISSUERS):
add('HIGH','eMudhra / emSign CA','证书颁发者疑似属于 2026-10 失陷中间 CA',
clean(source)+' / '+iss,CERT_EVENT)
try:
notafter=datetime.strptime(detail['not_after'],'%b %d %H:%M:%S %Y %Z').replace(tzinfo=timezone.utc)
remain=(notafter-datetime.now(timezone.utc)).days
if remain<0:add('HIGH','TLS 证书','证书已过期',clean(source)+' / '+str(-remain)+' 天')
elif remain<=30:add('MEDIUM','TLS 证书','证书 30 天内过期',clean(source)+' / 剩余 '+str(remain)+' 天')
except ValueError:pass
if 'sha1' in detail['signature'].lower() or 'md5' in detail['signature'].lower():
add('MEDIUM','TLS 证书','证书使用弱签名算法',clean(source)+' / '+detail['signature'])

def der_elem(b, offset):
if offset>=len(b):raise ValueError('DER eof')
tag=b[offset];offset+=1
if offset>=len(b):raise ValueError('DER length')
n=b[offset];offset+=1
if n & 128:
count=n&127
if count==0 or count>4 or offset+count>len(b):raise ValueError('DER length unsupported')
n=int.from_bytes(b[offset:offset+count],'big');offset+=count
end=offset+n
if end>len(b):raise ValueError('DER overflow')
return (tag,offset,end,end)

def certificate_spki_hash(der):
# X.509 Certificate -> TBSCertificate -> SubjectPublicKeyInfo.
# DER full TLV hash is SHA-256(SPKI), matching published root program fingerprints.
outer=der_elem(der,0)
if outer[0]!=0x30:raise ValueError('not certificate')
t=der_elem(der,outer[1]); pos=t[1]
if der[pos]==0xa0:pos=der_elem(der,pos)[3] # explicit v3 version
for _ in range(5):pos=der_elem(der,pos)[3] # serial, tbs signature, issuer, validity, subject
spki=der_elem(der,pos)
if spki[0]!=0x30:raise ValueError('bad SPKI tag')
return hashlib.sha256(der[pos:spki[3]]).hexdigest()

def ca_file_scan():
if not cmd(['openssl','version'],3):
coverage.append('没有 openssl:无法解析本机 PEM 根库与服务端证书文件')
return
bundles=discover_ca_bundles()
inventory['ca_bundle_files_checked']=len(bundles)
seen=set(); inspected=0; cap=2400
# Prefer major bundle files first for coverage.
bundles.sort(key=lambda p: (0 if os.path.basename(p) in ('ca-certificates.crt','cacert.pem','ca-bundle.crt') else 1,p))
for path in bundles:
content=safe_bytes(path,5000000)
if not content:continue
blocks=cert_parts(content)
for pem in blocks:
if inspected>=cap: break
try:
der=ssl.PEM_cert_to_DER_cert(pem.decode('ascii'))
digest=hashlib.sha256(der).hexdigest()
if digest in seen:continue
seen.add(digest);inspected+=1
spki=certificate_spki_hash(der)
affected_name=any(a.encode() in der for a in AFFECTED_ISSUERS)
if spki in RISKY_ROOT_SPKI or affected_name:
detail=x509_detail(pem)
if detail:risk_cert(detail,path,trust=True)
except (ValueError,UnicodeError,IndexError):continue
if inspected>=cap:break
coverage.append('解析本机根库/PEM %d 个不同证书(上限 %d);未验证操作系统或应用是否实际信任'%(inspected,cap))
if inspected>=cap:coverage.append('根库扫描达到证书数量上限,可能遗漏证书')

def safe_bytes(path,maximum=5000000):
try:
if os.path.islink(path) or os.path.getsize(path)>maximum:return b''
with open(path,'rb') as f:return f.read(maximum)
except OSError:return b''

def local_cert_scan():
if not cmd(['openssl','version'],3):return
scanroots=['/etc/nginx','/etc/traefik','/etc/letsencrypt/live','/etc/ssl/private','/etc/ssl/localcerts',
'/usr/local/etc','/opt','/srv']
inspected=0
for root in scanroots:
if not os.path.isdir(root):continue
for base,dirs,files in os.walk(root,followlinks=False):
depth=base.rstrip('/').count('/')-root.rstrip('/').count('/')
dirs[:]=[d for d in dirs if depth<5 and d not in ('node_modules','.git','site-packages','vendor') and not os.path.islink(os.path.join(base,d))]
for name in files:
if inspected>=150:break
path=os.path.join(base,name)
if not re.search(r'\.(?:crt|pem|cer|chain|fullchain)$',name,re.I):continue
if os.path.islink(path):
# Certbot's live directory intentionally contains symlinks.
if root=='/etc/letsencrypt/live':
path=os.path.realpath(path)
else:continue
s=safe_bytes(path,1000000)
if not s or b'BEGIN CERTIFICATE' not in s:continue
inspected+=1
for pem in cert_parts(s)[:10]:
detail=x509_detail(pem)
if detail:risk_cert(detail,path)
if inspected>=150:break
if inspected>=150:break
inventory['local_pem_files_checked']=inspected
if inspected>=150:coverage.append('本机服务证书仅检查前 150 个文件')
coverage.append('本机 TLS 文件只读检查完毕(不读取私钥内容)')

def tls_target_check(target):
# User-approved active, non-invasive TLS handshake; no HTTP request.
host=target;port=443
if target.startswith('['):
m=re.fullmatch(r'\[([^]]+)\](?::(\d+))?',target)
if not m: add('REVIEW','HTTPS 目标','无效的目标语法',target);return
host=m.group(1);port=int(m.group(2) or '443')
elif ':' in target:
host,ps=target.rsplit(':',1)
if not ps.isdecimal():add('REVIEW','HTTPS 目标','无效的端口',target);return
port=int(ps)
if not host or len(host)>253 or not (1<=port<=65535):
add('REVIEW','HTTPS 目标','无效的地址或端口',target);return
inventory.setdefault('tls_targets',[]).append(target)
# Check hostname, expiry and trust using Python/system certificate store.
try:
context=ssl.create_default_context()
context.check_hostname=True
context.verify_mode=ssl.CERT_REQUIRED
with socket.create_connection((host,port),timeout=6) as sock:
with context.wrap_socket(sock,server_hostname=host) as ss:
proto=ss.version();cert=ss.getpeercert()
if proto in ('TLSv1','TLSv1.1','SSLv3'):
add('HIGH','HTTPS 目标','协商到了过时的 TLS 协议',target+' / '+str(proto))
inventory.setdefault('tls_handshakes',[]).append({'target':target,'protocol':proto,
'notAfter':cert.get('notAfter','')})
except ssl.SSLCertVerificationError as e:
add('HIGH','HTTPS 目标','系统信任库验证 HTTPS 证书失败',target+' / '+clean(e),CERT_EVENT,
'可能是域名不匹配、过期、缺失中间证书或根 CA 不受信任;不等于一定属于 eMudhra 事件。')
except (OSError,ssl.SSLError) as e:
add('REVIEW','HTTPS 目标','TLS 握手失败或目标无法访问',target+' / '+type(e).__name__)
# Retrieve the actual peer's presented chain without executing any remote content.
args=['openssl','s_client','-connect',('['+host+']' if ':' in host else host)+':'+str(port),
'-servername',host,'-showcerts']
try:
r=subprocess.run(args,input=b'',stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=9)
transcript=r.stdout[:600000]
pems=cert_parts(transcript)[:15]
if not pems:
coverage.append(target+': 无法提取服务端证书链(未覆盖 CA 链检查)')
for n,pem in enumerate(pems):
info=x509_detail(pem)
if info:risk_cert(info,target+' peer#'+str(n))
if pems:
inventory.setdefault('tls_peer_certificates',{})[target]=len(pems)
except (OSError,subprocess.TimeoutExpired):
coverage.append(target+': openssl s_client 握手失败/不可用')

def tls_protocol_check(target):
# TLS 1.0/1.1 only when compiled OpenSSL allows their negotiation; no exploit payload.
if not cmd(['openssl','version'],3):return
host,port=target,443
if target.startswith('['):
m=re.fullmatch(r'\[([^]]+)\](?::(\d+))?',target)
if not m:return
host=m.group(1);port=int(m.group(2) or '443')
elif ':' in target:
host,p=target.rsplit(':',1)
if not p.isdecimal():return
port=int(p)
for flag in ('-tls1','-tls1_1'):
try:
r=subprocess.run(['openssl','s_client',flag,'-connect',('['+host+']' if ':' in host else host)+':'+str(port),
'-servername',host,'-brief'],input=b'',stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=5)
out=(r.stdout+r.stderr)[:6000].decode('utf-8','replace')
if r.returncode==0 and 'Protocol version: TLSv1' in out:
add('HIGH','HTTPS 协议','远端仍支持 '+('TLS 1.0' if flag=='-tls1' else 'TLS 1.1'),target)
except (OSError,subprocess.TimeoutExpired):pass

def ci_scan(extra_roots=()):
scanroots=['/etc/gitlab-runner','/etc/gitlab','/etc/ssh','/etc/traefik','/etc/nginx','/var/www','/www','/opt','/srv']+list(extra_roots)
checked=0
for root in scanroots:
if not os.path.isdir(root):continue
for base,dirs,files in os.walk(root,followlinks=False):
dep=base.rstrip('/').count('/')-root.rstrip('/').count('/')
dirs[:]=[x for x in dirs if dep<6 and x not in ('node_modules','site-packages','.git','vendor','dist','build') and not os.path.islink(os.path.join(base,x))]
for name in files:
path=os.path.join(base,name)
if checked>=3000:break
if name in ('config.toml','nginx.conf','sshd_config','config.php','settings.py','wp-config.php') or name in ('.gitlab-ci.yml','Dockerfile','docker-compose.yml','compose.yaml','compose.yml','wp-config-sample.php','traefik.yaml','traefik.yml') or (base.endswith(('.github/workflows','.gitlab')) and name.endswith(('.yml','.yaml'))):
checked+=1
s=safe_read(path,250000)
if not s:continue
patterns=[]
if name=='config.toml' and ('/gitlab-runner' in path):
patterns=[(r'(?mi)^\s*executor\s*=\s*["\']shell["\']','Shell Executor:CI 作业可能在宿主机权限域运行','HIGH'),
(r'(?mi)^\s*privileged\s*=\s*true','GitLab Runner 启用特权容器','HIGH'),
(r'(?mi)^\s*tls_verify\s*=\s*false','Runner 关闭 TLS 校验','HIGH')]
if name in ('compose.yaml','docker-compose.yml','compose.yml','Dockerfile'):
patterns=[(r'(?mi)^\s*privileged\s*:\s*true','Compose 容器 privileged=true','HIGH'),
(r'(?m)^\s*-\s*/var/run/docker.sock\s*:\s*/var/run/docker.sock','Compose 直接挂载 Docker Socket','HIGH'),
(r'(?m)^\s*-\s*/\s*:\s*/(?:host|root|mnt)\b','Compose 容器挂载宿主机根目录','HIGH')]
if name=='.gitlab-ci.yml' or base.endswith('.github/workflows'):
patterns=[(r'(?mi)\b(?:pip\s+install|npm\s+install|npm\s+i)\b[^\n]*\b(?:--pre|--upgrade|\blatest\b)','CI 非固定版本依赖拉取(供应链漂移风险)','REVIEW'),
(r'(?m)^\s*(?:-\s*)?uses\s*:\s*[^\s#]+@(main|master|latest)\b','GitHub Actions 未固定 commit SHA','MEDIUM'),
(r'(?mi)^\s*\S*(?:secret|password|api_key|token)\S*\s*:\s*["\']?[^\s${}][^\n#]{4,}','CI 疑似明文凭证(不会保存值)','HIGH')]
if name=='sshd_config':
patterns=[(r'(?mi)^\s*PermitRootLogin\s+yes\b','sshd 配置明确允许 root 登录','MEDIUM'),
(r'(?mi)^\s*PasswordAuthentication\s+yes\b','sshd 配置明确允许密码登录','REVIEW')]
if name in ('config.php','settings.py') or name.endswith('.yml') or name.endswith('.yaml'):
patterns += [(r'(?mi)\b(?:insecure_skip_verify|insecureSkipVerify|verify_tls|tls_verify)\s*[:=]\s*(?:true|false)\b','存在 TLS 校验开关,需核实生效方向','REVIEW')]
for pat,issue,severity in patterns:
m=re.search(pat,s)
if m:
add(severity,'CI/CD 与配置审计',issue,path+':'+str(s.count('\n',0,m.start())+1),guidance='仅静态命中,不暴露原始密钥值;条件需人工核实。')
if checked>=3000:break
if checked>=3000:break
coverage.append('CI/编排配置扫描 %d 文件;无法通过文件静态判断 GitLab 保护变量和完整权限设置'%checked)

def network_listen_scan():
out=cmd(['ss','-H','-lnt'],5)
if not out:
coverage.append('未获取本地监听端口(ss 不可用或权限不足)')
return
ports={2375:'Docker 未加密 API',6379:'Redis',9200:'Elasticsearch',27017:'MongoDB',
3306:'MySQL',5432:'PostgreSQL',9000:'MinIO/其他服务',10250:'kubelet',
8080:'管理或 Web 服务(待确认)',3000:'Grafana/Node 或其他 Web 服务'}
seen=set()
for line in out.splitlines():
cols=line.split()
if len(cols)<4:continue
endpoint=cols[3]
m=re.search(r':(\d+)$',endpoint)
if not m:continue
port=int(m.group(1))
if port not in ports:continue
addr=endpoint[:m.start()].strip('[]')
if addr in ('127.0.0.1','::1','localhost'):continue
mark=(port,addr)
if mark in seen:continue
seen.add(mark)
add('HIGH' if port in (2375,6379,9200,27017,10250) else 'REVIEW',
'网络暴露面','服务监听非 loopback 地址,需检查防火墙与鉴权:'+ports[port],endpoint,
guidance='仅证明监听,不代表实际允许公网访问。')
coverage.append('本机监听端口已检查;未探测外部防火墙或云安全组')

def wordpress_addon_static(base, typ, root):
limit=0
for name in sorted(os.listdir(base))[:120]:
loc=os.path.join(base,name)
if not os.path.isdir(loc):continue
limit+=1
meta=[]
try:
for fname in os.listdir(loc)[:60]:
if not fname.endswith(('.php','.css')):continue
text=safe_read(os.path.join(loc,fname),120000)
if not text:continue
meta.append((fname,text))
# Strong indicator only; many legitimate obfuscated plugins exist.
if re.search(r'(?is)\beval\s*\(\s*(?:base64_decode|gzinflate|str_rot13)\s*\(',text):
add('HIGH','WordPress 插件完整性','发现混淆代码执行组合,无法证明一定是后门',
os.path.join(loc,fname),guidance='按官方原版哈希校验,人工审查可疑文件。')
for fname,txt in meta:
if typ=='plugin' and re.search(r'(?mi)^\s*\*?\s*Plugin Name\s*:',txt):
v=re.search(r'(?mi)^\s*\*?\s*Version\s*:\s*([^\r\n*]+)',txt)
if v:inventory.setdefault('wp_addon_versions',[]).append({'addon':name,'version':v.group(1).strip()[:40],'source':os.path.join(loc,fname)})
break
if typ=='theme' and fname=='style.css':
v=re.search(r'(?mi)^\s*\*?\s*Version\s*:\s*([^\r\n*]+)',txt)
if v:inventory.setdefault('wp_addon_versions',[]).append({'addon':name,'version':v.group(1).strip()[:40],'source':os.path.join(loc,fname)})
break
except OSError:pass
coverage.append('WordPress %s 清单/可疑代码初筛 %d 个;破解版来源与恶意性不能仅靠正则确认'%(typ,limit))

def baota_scan():
bases=('/www/server/panel','/opt/btpanel','/usr/local/btpanel')
for base in bases:
if not os.path.isdir(base):continue
inventory.setdefault('baota_panels',[]).append(base)
versionfile=os.path.join(base,'data','version.pl')
pv=safe_read(versionfile,500).strip()
add('REVIEW','宝塔 / BT Panel','检测到宝塔面板安装目录;是否为开心版或含后门无法单凭目录确定',
base + (' / version='+clean(pv) if pv else ''),
guidance='不要运行不可信面板的管理脚本;与官方发行包、签名/哈希、文件增改历史人工比对。')
plugin=os.path.join(base,'plugin')
try: count=len(os.listdir(plugin))
except OSError:count=0
inventory.setdefault('baota_plugins_count',{})[base]=count
if not inventory.get('baota_panels'):
coverage.append('未在常见目录发现宝塔面板;自定义路径、Docker 内部署或伪装安装无法排除')
else:coverage.append('宝塔面板进行存在性与目录初筛,未验证第三方破解包是否注入后门')

COVERAGE_MATRIX={
'nginx':'运行时版本+静态 TLS 配置(非全部模块)',
'PHP 8.1':'CLI 版本 / 安全支持状态(FPM 多版本可能漏检)',
'Node.js Brotli':'已知版本基线 / 依赖锁文件(无法证明暴露条件)',
'Go crypto/x509':'Go 版本 + 选定 ELF 编译版本扫描(无法证明运行中代码使用新构建)',
'Java GraalVM':'Java/native-image 版本、Maven 清单(无法深审 Native Image)',
'Ruby':'Ruby 版本 + Gemfile.lock(无法覆盖所有已安装 Gem)',
'Perl':'Perl 版本(依赖模块需额外工具)',
'Rust/webpki-roots':'Rust 版本 + Cargo.lock 固定依赖/CA 信任(编译产物须另核)',
'Python LiteLLM 1.107.0':'virtualenv 元数据、投毒 IOC、Docker 标签/已知 Digest(IoC 阴性不代表未感染)',
'Vyper 合约':'CLI 版本(无法无授权源码/链访问验证已部署智能合约逻辑)',
'Docker':'容器运行配置、标签、运行时暴露条件(不解包所有镜像)',
'Kubernetes / RBAC':'--k8s 时可检查当前凭证可见的 Pod/clusterrolebindings',
'Traefik / mTLS fail-open':'已知静态配置写法、可执行版本(动态配置/真实鉴权可能未覆盖)',
'ASP.NET Core/.NET':'已安装运行时与 NuGet 锁文件(自包含发布可能无法检测)',
'Linux Kernel c6e50df8':'记录 uname;内核 commit 和回移补丁无法跨发行版通用判定',
'eMudhra/emSign CA':'本机根库 SPKI、配置证书链、--tls-target 证书链和系统验签(非所有客户端信任路径)',
'certifi/pip/uv/sigstore':'静态 dist-info 修复基线 / CA Bundle',
'HTTPS 证书/TLS':'--tls-target 在线握手、域名/有效性/链与 TLS1.0/1.1;本机 PEM 检查',
'GitLab':'dpkg/rpm/Docker 版本及已知 CVE;Runner/CI 静态配置',
'CI PyPI/npm 供应链':'Python 元数据、锁文件 + 在线 OSV(不能保证恶意包全部有已知条目)',
'破解宝塔':'存在性/插件目录初筛(无法自动判定第三方版本含后门)',
'WordPress/子比/破解版插件':'核心版本、插件和主题清单/版本、可疑混淆/备份(不能证实破解或恶意)',
'数据库和管理入口':'本地 ss 监听检测,无法推断防火墙/公网实际可达',
}

def finalize_coverage(args):
if not args.tls_target:
coverage.append('未指定 --tls-target:公网/内网 HTTPS 站点证书链、域名匹配、TLS 协议均未远程验证')
coverage.append('无法从本机单点可靠断言:远端证书是否遭中间人替换、云安全组公网可达、GitLab 权限/保护变量、WordPress 插件来源、任意第三方代码是否含后门')
coverage.append('证书根库与信任链检测无法替代各进程自己的信任决策;部分运行时内嵌根列表和吊销机制需另行核查')
coverage.append('Linux c6e50df8 是提交标识,不是统一内核版本阈值;内核受影响性请核对发行版安全公告、构建号和补丁回移记录')

def main():
ap=argparse.ArgumentParser(description='Linux 技术栈 CVE/供应链/配置安全检查(只读)')
ap.add_argument('--offline',action='store_true',help='不访问 OSV.dev(只用 2026-10-22 静态规则)')
ap.add_argument('--no-docker',action='store_true',help='不查询本地 Docker')
ap.add_argument('--k8s',action='store_true',help='额外按当前 kubeconfig 只读扫描 Kubernetes Pods')
ap.add_argument('--max-packages',type=int,default=600,help='OSV 查询包数量上限,默认 600')
ap.add_argument('--out',help='报告目录(须不存在)')
ap.add_argument('--tls-target',action='append',default=[],metavar='HOST[:PORT]',
help='显式授权的 HTTPS 目标,可重复(域名或 IP;IPv6 使用 [IP]:PORT)')
ap.add_argument('--path',action='append',default=[],metavar='DIRECTORY',
help='额外读取的本地项目目录,可重复;不跟随符号链接')
ap.add_argument('--skip-ca-files',action='store_true',help='跳过本机 PEM 文件和根证书扫描')
args=ap.parse_args()
if args.max_packages<1 or args.max_packages>10000: ap.error('--max-packages 必须在 1..10000')
print('[1/8] 本机运行时版本、官方补丁基线... ',flush=True)
check_versions()
check_go_ca()
print('[2/8] 静态扫描配置、virtualenv、lockfile、WordPress... ',flush=True)
scan_filesystem(args.path)
print('[3/8] 证书包、CA 根信任及失陷 CA 链... ',flush=True)
if not args.skip_ca_files: ca_file_scan(); local_cert_scan()
else:coverage.append('按参数跳过本地 CA/PEM 文件检查')
print('[4/8] CI/CD、SSH、Compose、配置核查...',flush=True)
ci_scan(args.path)
baota_scan()
network_listen_scan()
print('[5/8] 显式授权的 HTTPS 目标...',flush=True)
for item in args.tls_target[:30]:
tls_target_check(item)
tls_protocol_check(item)
if len(args.tls_target)>30: coverage.append('HTTPS 目标超过 30 个,只检查前 30 个')
print('[6/8] 本地 Docker 配置与镜像...',flush=True)
if not args.no_docker: scan_docker()
else: coverage.append('按参数跳过 Docker')
if args.k8s:
print('[7/8] Kubernetes 配置...',flush=True)
scan_k8s()
else:
coverage.append('默认不访问 Kubernetes;如需只读检查加 --k8s')
if not args.offline:
print('[8/8] 联网查询 OSV.dev 最新依赖漏洞公告...',flush=True)
osv_online(args.max_packages)
else:
coverage.append('已使用 --offline:未查询后续新增漏洞')
finalize_coverage(args)
dirname='stack-audit-'+datetime.now().strftime('%Y%m%d-%H%M%S')+'-'+str(os.getpid())
report(os.path.abspath(args.out or dirname))

if __name__=='__main__': main()
PY

最新回复 (5)
  • ringda 10-22 12:21
    1楼

    谢谢

  • visitor 10-22 12:26
    2楼

    感谢,下午搞一下

  • Luen 10-22 13:47
    3楼

    要root跑的脚本还是先自己过一遍再执行 好在这个写的是只读

  • Flanker 楼主 10-22 14:38
    4楼

    @Luen #3 发布于2026/10/22 13:47:39

    要root跑的脚本还是先自己过一遍再执行 好在这个写的是只读


    对啊 所以这不是脚本都贴出来了 自己看或者AI看看都很容易的

  • cookies 10-22 14:38
    5楼

    bd

* 帖子来源NodeSeek
返回