← 中文资源

简体中文版本

软件物料清单(SBOM)

SBOM 是构建中每个组件的机器可读清单,包含版本、标识和关系,用于回答交付软件里到底有什么。

阅读英文原文

SBOM 包含什么

SBOM 是清单,不是漏洞报告。它列出每个组件的名称、版本、供应方,以及 package URL(pURL)或 CPE 标识,还包括哈希和组件之间的关系。容器 SBOM 应包含操作系统包和基础镜像层,而不只是应用的语言依赖。

工作机制

生成应放在构建流水线中,因为那里有解析后的依赖图。工具读取清单和锁文件、检查容器层,输出 SPDX 或 CycloneDX JSON。产物应签名并与发布一起存储,最好由镜像或发布元数据引用。

使用方在事件响应中查询 SBOM:新 CVE 出现时,确认哪些已发布版本包含受影响组件、位于何处。结合 VEX 声明,即记录产品是否真的受影响,可以把存在与可利用区分开。

何时使用与取舍

每个对外交付的产物和容器镜像都应生成 SBOM,内部升级、许可证审计和并购尽调也用得上。受监管行业和公共采购越来越要求每次发布都提供。

完整 SBOM 体积大且噪声多,容器尤其如此。准确性取决于生成器,只读锁文件的工具会漏掉系统库。格式细节不同,工具必须规范化 pURL。持续生成并签名会增加构建时间和存储。

示例与常见误解

某日志库漏洞爆发后,团队查询 SBOM,发现三个服务包含受影响版本,其中一个是通过 SDK 间接引入的。没有 SBOM,这个服务就会被漏掉。

常见误解是 SBOM 等于安全评估。它只是配料表;风险取决于配料在上下文中是否可达、是否可利用。相关概念:供应链安全、SLSA、漏洞披露。

代码示例

import unicodedata

SCRIPT_RANGES = {
    "LATIN": [(0x0041, 0x024F), (0x1E00, 0x1EFF)],
    "CYRILLIC": [(0x0400, 0x04FF)],
    "GREEK": [(0x0370, 0x03FF)],
}


def script_of(char: str) -> str:
    code = ord(char)
    for name, ranges in SCRIPT_RANGES.items():
        if any(start <= code <= end for start, end in ranges):
            return name
    return "OTHER"


def find_mixed_scripts(text: str) -> list[str]:
    scripts = {script_of(char) for char in text if char.isalpha()}
    return sorted(scripts - {"OTHER"})


def is_suspicious(text: str) -> bool:
    scripts = find_mixed_scripts(text)
    return "LATIN" in scripts and len(scripts) > 1


def explain(text: str) -> list[dict[str, str]]:
    return [
        {
            "char": char,
            "name": unicodedata.name(char, "UNKNOWN"),
            "script": script_of(char),
        }
        for char in text
        if char.isalpha() and script_of(char) not in {"LATIN", "OTHER"}
    ]


if __name__ == "__main__":
    for sample in ["paypal", "pаypal", "admin"]:
        print(sample, find_mixed_scripts(sample), is_suspicious(sample))
    print(explain("pаypal"))
export type SqlDialect = "postgres" | "mysql";

export function quoteIdentifier(
  identifier: string,
  dialect: SqlDialect = "postgres",
): string {
  if (typeof identifier !== "string" || identifier.length === 0) {
    throw new TypeError("identifier must be a non-empty string");
  }
  if (identifier.includes("\0")) {
    throw new RangeError("identifier must not contain NUL");
  }
  if (dialect !== "postgres" && dialect !== "mysql") {
    throw new RangeError("unsupported SQL dialect");
  }
  const quote = dialect === "mysql" ? "`" : '"';
  return quote + identifier.split(quote).join(quote + quote) + quote;
}

export function qualifyTable(
  schema: string,
  table: string,
  dialect: SqlDialect = "postgres",
): string {
  return quoteIdentifier(schema, dialect) + "." + quoteIdentifier(table, dialect);
}