- Flanker3天前15565
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 |
nginxAccess-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 bashStack 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, timezoneBASELINE = '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 Nonedef earlier(v, fixed):
x = version(v)
return x is not None and x < fixeddef 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)^\sproxy_ssl_verify\s+off\s;', 'nginx 到上游关闭 TLS 证书验证','HIGH'),
(r'(?m)^\sssl_verify_client\s+optional_no_ca\s;', 'nginx mTLS 客户端证书不强制可信 CA','HIGH'),
(r'(?mi)^\sinsecureSkipVerify\s:\s*true\b', 'Traefik TLS 证书验证被关闭','HIGH'),
(r'(?mi)^\sclientAuthType\s:\s*["\']?VerifyIfGiven\b','Traefik 客户端证书可选:不是严格 mTLS','MEDIUM'),
(r'(?m)^\sadd_header\s+Access-Control-Allow-Origin\s+["\']?\','CORS 允许任意 Origin,需结合鉴权检查','REVIEW'),
]
patterns += [
(r'(?mi)^\sssl_verify_client\s+optional\s;', 'nginx mTLS 客户端证书 optional,不等于强制验证', 'MEDIUM'),
(r'(?mi)^\sssl_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)^\sinsecureSkipVerify\s[:=]\s*true\b','Traefik/代理关闭 TLS 上游校验','HIGH'),
(r'(?mi)^\scaOptional\s:\s*true\b','代理 mTLS CA 可选(配置需确认)','MEDIUM'),
(r'(?mi)^\sssl_verify_client\s+off\s;', 'nginx TLS 客户端证书认证关闭(如要求 mTLS 则配置不符)', 'REVIEW'),
(r'(?mi)^\sclientAuthType\s:\s*["\']?NoClientCert\b', 'Traefik 不进行客户端证书认证(如要求 mTLS 则配置不符)','REVIEW'),
(r'(?mi)^\sssl_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,\strue\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 timedeltaCERT_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):passdef 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)^\sexecutor\s=\s*["\']shell["\']','Shell Executor:CI 作业可能在宿主机权限域运行','HIGH'),
(r'(?mi)^\sprivileged\s=\s*true','GitLab Runner 启用特权容器','HIGH'),
(r'(?mi)^\stls_verify\s=\s*false','Runner 关闭 TLS 校验','HIGH')]
if name in ('compose.yaml','docker-compose.yml','compose.yml','Dockerfile'):
patterns=[(r'(?mi)^\sprivileged\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\?\sPlugin Name\s:',txt):
v=re.search(r'(?mi)^\s\?\sVersion\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\?\sVersion\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主楼 - ringda3天前8097
谢谢
- 13611
感谢,下午搞一下
- Luen3天前13637
要root跑的脚本还是先自己过一遍再执行 好在这个写的是只读
- Flanker3天前15565
- cookies3天前18403
bd