This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
//
|
||||
// The software bill of materials of the built image and the THIRD-PARTY-NOTICES generated from it
|
||||
// (plan item AT5.1): the SBOM reads what is installed in a pnpm store (a runtime dependency and not a
|
||||
// link to one, once per version), and the notices list every component of it, with each licence text
|
||||
// printed once however many packages share it. The committed THIRD-PARTY-NOTICES is checked against
|
||||
// the dependencies this template declares.
|
||||
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { dirname, join } from "node:path"
|
||||
import { afterEach, describe, expect, it } from "vitest"
|
||||
import { dpkgComponents, npmComponents, sbom } from "../scripts/sbom.mjs"
|
||||
import { notices, unlisted } from "../scripts/third-party-notices.mjs"
|
||||
|
||||
const APP_ROOT = new URL("..", import.meta.url).pathname
|
||||
|
||||
const made: string[] = []
|
||||
afterEach(() => {
|
||||
for (const dir of made.splice(0)) rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
const MIT = "MIT License\n\nCopyright (c) 2020 Someone\n\nPermission is hereby granted, free of charge, ..."
|
||||
|
||||
/** A pnpm virtual store: each entry holds its own package and links to the packages it depends on. */
|
||||
function image(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), "sbom-"))
|
||||
made.push(root)
|
||||
const put = (path: string, text: string) => {
|
||||
mkdirSync(dirname(join(root, path)), { recursive: true })
|
||||
writeFileSync(join(root, path), text)
|
||||
}
|
||||
const store = "node_modules/.pnpm"
|
||||
put("package.json", JSON.stringify({ name: "demo-app", version: "1.2.3" }))
|
||||
put(
|
||||
`${store}/alpha@1.0.0/node_modules/alpha/package.json`,
|
||||
JSON.stringify({ name: "alpha", version: "1.0.0", license: "MIT" }),
|
||||
)
|
||||
put(`${store}/alpha@1.0.0/node_modules/alpha/LICENSE`, MIT)
|
||||
put(
|
||||
`${store}/beta@2.0.0/node_modules/beta/package.json`,
|
||||
JSON.stringify({ name: "beta", version: "2.0.0", license: "MIT" }),
|
||||
)
|
||||
put(`${store}/beta@2.0.0/node_modules/beta/LICENSE.md`, MIT)
|
||||
// the same version under two peer sets is one component
|
||||
put(
|
||||
`${store}/beta@2.0.0_react@19/node_modules/beta/package.json`,
|
||||
JSON.stringify({ name: "beta", version: "2.0.0", license: "MIT" }),
|
||||
)
|
||||
put(
|
||||
`${store}/@scope+gamma@3.0.0/node_modules/@scope/gamma/package.json`,
|
||||
JSON.stringify({
|
||||
name: "@scope/gamma",
|
||||
version: "3.0.0",
|
||||
license: { type: "ISC" },
|
||||
repository: { url: "git+https://example.test/gamma.git" },
|
||||
}),
|
||||
)
|
||||
put(
|
||||
`${store}/@conusai-kit+core@0.1.0/node_modules/@conusai-kit/core/package.json`,
|
||||
JSON.stringify({ name: "@conusai-kit/core", version: "0.1.0", license: "LicenseRef-ConusAI-Template-1.0" }),
|
||||
)
|
||||
put(
|
||||
`${store}/delta@4.0.0/node_modules/delta/package.json`,
|
||||
JSON.stringify({ name: "delta", version: "4.0.0", license: "(MIT OR Apache-2.0)" }),
|
||||
)
|
||||
// alpha depends on beta: a link inside alpha's entry, which is not a second copy of beta
|
||||
symlinkSync(join(root, `${store}/beta@2.0.0/node_modules/beta`), join(root, `${store}/alpha@1.0.0/node_modules/beta`))
|
||||
return root
|
||||
}
|
||||
|
||||
describe("the SBOM of an image", () => {
|
||||
it("holds each installed package once, with its licence and the text of its licence file", () => {
|
||||
const components = npmComponents(image())
|
||||
expect(components.map((component) => component["bom-ref"])).toEqual([
|
||||
"pkg:npm/%40conusai-kit/core@0.1.0",
|
||||
"pkg:npm/%40scope/gamma@3.0.0",
|
||||
"pkg:npm/alpha@1.0.0",
|
||||
"pkg:npm/beta@2.0.0",
|
||||
"pkg:npm/delta@4.0.0",
|
||||
])
|
||||
const alpha = components.find((component) => component.name === "alpha")
|
||||
expect(alpha?.licenses[0]?.license).toMatchObject({ id: "MIT", text: { content: MIT } })
|
||||
expect(components.find((component) => component.name === "gamma")).toMatchObject({
|
||||
group: "@scope",
|
||||
licenses: [{ license: { id: "ISC" } }],
|
||||
externalReferences: [{ type: "vcs", url: "https://example.test/gamma" }],
|
||||
})
|
||||
expect(components.find((component) => component.name === "delta")?.licenses).toEqual([
|
||||
{ expression: "(MIT OR Apache-2.0)" },
|
||||
])
|
||||
})
|
||||
|
||||
it("marks the ConusAI packages, whose licence is LICENSE-TEMPLATE, and names their licence as a reference", () => {
|
||||
const core = npmComponents(image()).find((component) => component.name === "core")
|
||||
expect(core?.properties).toContainEqual({ name: "conusai:component", value: "true" })
|
||||
expect(core?.licenses[0]?.license).toEqual({ name: "LicenseRef-ConusAI-Template-1.0" })
|
||||
})
|
||||
|
||||
it("is a CycloneDX 1.5 document with the application, the Node.js runtime and the packages", () => {
|
||||
const document = sbom({ root: image(), dpkg: "/nonexistent", nodeVersion: "v24.1.0" })
|
||||
expect(document).toMatchObject({
|
||||
bomFormat: "CycloneDX",
|
||||
specVersion: "1.5",
|
||||
metadata: { component: { type: "application", name: "app", version: "0.0.0" } },
|
||||
})
|
||||
expect(document.components[0]).toMatchObject({ type: "platform", name: "node", version: "24.1.0" })
|
||||
expect(document.components).toHaveLength(6)
|
||||
})
|
||||
|
||||
it("reads the operating system packages that are installed, and no others", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "dpkg-"))
|
||||
made.push(dir)
|
||||
const status = join(dir, "status")
|
||||
writeFileSync(
|
||||
status,
|
||||
[
|
||||
"Package: libc6\nStatus: install ok installed\nVersion: 2.41-1\nArchitecture: amd64",
|
||||
"Package: gone\nStatus: deinstall ok config-files\nVersion: 1.0",
|
||||
"Package: zlib1g\nStatus: install ok installed\nVersion: 1:1.3.dfsg-1",
|
||||
"",
|
||||
].join("\n\n"),
|
||||
)
|
||||
const components = dpkgComponents(status, dir)
|
||||
expect(components.map((component) => `${component.name} ${component.version}`)).toEqual([
|
||||
"libc6 2.41-1",
|
||||
"zlib1g 1:1.3.dfsg-1",
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("THIRD-PARTY-NOTICES generated from an SBOM", () => {
|
||||
const document = () =>
|
||||
sbom({ root: image(), dpkg: "/nonexistent", name: "demo-app", version: "1.2.3", nodeVersion: "v24.1.0" })
|
||||
|
||||
it("lists every component, and prints a licence text shared by several packages once", () => {
|
||||
const text = notices(document())
|
||||
expect(unlisted(document(), text)).toEqual([])
|
||||
expect(text).toContain("@conusai-kit/core@0.1.0")
|
||||
expect(text).toContain("node@24.1.0")
|
||||
expect(text.split("Permission is hereby granted").length - 1).toBe(1)
|
||||
expect(text).toContain("alpha@1.0.0")
|
||||
expect(text).toContain("beta@2.0.0")
|
||||
expect(text).toContain("these packages ship no licence file; they declare ISC")
|
||||
})
|
||||
|
||||
it("is the same text every time it is generated from the same SBOM", () => {
|
||||
expect(notices(document())).toBe(notices(document()))
|
||||
})
|
||||
|
||||
it("is checked against the SBOM: a component it does not list is named", () => {
|
||||
const text = notices(document())
|
||||
const more = document()
|
||||
more.components.push({
|
||||
type: "library",
|
||||
"bom-ref": "pkg:npm/epsilon@5.0.0",
|
||||
name: "epsilon",
|
||||
version: "5.0.0",
|
||||
purl: "pkg:npm/epsilon@5.0.0",
|
||||
licenses: [],
|
||||
})
|
||||
expect(unlisted(more, text)).toEqual(["epsilon@5.0.0"])
|
||||
// a version that moved is drift by name, not a missing component
|
||||
const moved = document()
|
||||
moved.components = moved.components.map((component) =>
|
||||
component.name === "alpha" ? { ...component, version: "1.0.1" } : component,
|
||||
)
|
||||
expect(unlisted(moved, text)).toEqual(["alpha@1.0.1"])
|
||||
expect(unlisted(moved, text, { byName: true })).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("the committed THIRD-PARTY-NOTICES", () => {
|
||||
const text = readFileSync(join(APP_ROOT, "THIRD-PARTY-NOTICES"), "utf8")
|
||||
|
||||
it("says what it is generated from, and that it is not edited by hand", () => {
|
||||
expect(text).toContain("software bill of materials of the built image")
|
||||
expect(text).toContain("do not edit it by hand")
|
||||
})
|
||||
|
||||
it("lists every runtime dependency this template declares, by name", () => {
|
||||
const declared = new Set<string>()
|
||||
for (const path of [
|
||||
"package.json",
|
||||
"apps/web/package.json",
|
||||
"apps/worker/package.json",
|
||||
"config/package.json",
|
||||
"modules/package.json",
|
||||
]) {
|
||||
const manifest = JSON.parse(readFileSync(join(APP_ROOT, path), "utf8")) as {
|
||||
dependencies?: Record<string, string>
|
||||
}
|
||||
for (const name of Object.keys(manifest.dependencies ?? {})) if (!name.startsWith("@app/")) declared.add(name)
|
||||
}
|
||||
expect(declared.size).toBeGreaterThan(15)
|
||||
const missing = [...declared].filter((name) => !text.includes(` ${name}@`))
|
||||
expect(missing).toEqual([])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user