• Flanker

    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&#039;,
    'node':'https://nodejs.org/en/blog/vulnerability/october-2026-security-releases&#039;,
    'nginx':'https://nginx.org/en/security_advisories.html&#039;,
    'gitlab':'https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-4-2-released/&#039;,
    'gitlab_old':'https://www.cve.org/CVERecord?id=CVE-2026-85706&#039;,
    'dotnet':'https://github.com/dotnet/announcements/issues/472&#039;,
    'dotnet_blazor':'https://github.com/dotnet/announcements/issues/478&#039;,
    'osv':'https://google.github.io/osv.dev/post-v1-querybatch/&#039;,
    }
    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&#039;,
    '检查发行版延长维护计划,安排升级应用与运行时。')
    # 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/&#039;,
    '请对插件/主题做来源核验和文件完整性校验(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/&#039;
    CERT_ROOT_ADVISORY='https://www.openwall.com/lists/oss-security/2026/10/20/1&#039;
    GO_ADVISORY='https://github.com/golang/go/issues/82345&#039;
    RUST_ADVISORY='https://rustsec.org/advisories/RUSTSEC-2026-0381&#039;
    PY_CA_ADVISORY='https://discuss.python.org/t/information-for-python-users-regarding-emudhra-emsign-ca-certificate-security-incident/109576&#039;

    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)^\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

    主楼
  • ringda

    谢谢

    #1
  • visitor-nodeseek

    感谢,下午搞一下

    #2
  • Luen

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

    #3
  • Flanker

    发布于2026/10/22 13:47:39
    要root跑的脚本还是先自己过一遍再执行 好在这个写的是只读

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

    #4
  • cookies

    bd

    #5

发表回复

登录后回复