Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ jobs:

- run: make docker PRESET=Release ${{ matrix.edition.options }}

- name: SBOM (Enterprise)
run: |
node ./enterprise/scripts/sbom.js one /tmp/sbom.spdx.json
cat /tmp/sbom.spdx.json
npx --yes @sourcemeta/jsonschema validate \
vendor/spdx/schemas/spdx-schema.json /tmp/sbom.spdx.json --verbose
if: matrix.edition.name == 'enterprise'

- name: Sandbox (headless)
uses: ./.github/actions/sandbox
with:
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,23 @@ jobs:
with:
images: ghcr.io/${{ github.repository_owner }}/one-enterprise

- run: >
node ./enterprise/scripts/sbom.js
"ghcr.io/${{ github.repository_owner }}/one-enterprise:${{ steps.meta.outputs.version }}"
/tmp/sbom.spdx.json

- run: >
npx --yes @sourcemeta/jsonschema validate
vendor/spdx/schemas/spdx-schema.json
/tmp/sbom.spdx.json --verbose

- run: >
./enterprise/scripts/cosign.sh
"ghcr.io/${{ github.repository_owner }}/one-enterprise"
"${{ steps.meta.outputs.version }}"
"https://token.actions.githubusercontent.com"
"https://github.com/${{ github.repository }}/.github/workflows/deploy.yml@${{ github.ref }}"
"/tmp/sbom.spdx.json"

release:
needs: docker-multi-arch
Expand Down
1 change: 1 addition & 0 deletions DEPENDENCIES
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ jsonschema https://github.com/sourcemeta/jsonschema v14.13.4
bootstrap https://github.com/twbs/bootstrap v5.3.3
bootstrap-icons https://github.com/twbs/icons v1.11.3
collections/sourcemeta/std/v0 https://github.com/sourcemeta/std v0.4.0
spdx https://github.com/spdx/spdx-spec v2.3
public/modelcontextprotocol https://github.com/modelcontextprotocol/modelcontextprotocol 2025-11-25
public/a2a/v0.3.0 https://github.com/a2aproject/A2A v0.3.0
public/a2a/v0.2.6 https://github.com/a2aproject/A2A v0.2.6
Expand Down
16 changes: 14 additions & 2 deletions enterprise/scripts/cosign.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
set -o errexit
set -o nounset

if [ "$#" -ne 4 ]
if [ "$#" -ne 5 ]
then
echo "Usage: $0 <image> <version> <certificate-oidc-issuer> <certificate-identity>" 1>&2
echo "Usage: $0 <image> <version> <certificate-oidc-issuer> <certificate-identity> <sbom-file>" 1>&2
exit 1
fi

IMAGE="$1"
VERSION="$2"
CERTIFICATE_OIDC_ISSUER="$3"
CERTIFICATE_IDENTITY="$4"
SBOM_FILE="$5"

echo "Cosign: Extracting manifest digest for ${IMAGE}:${VERSION}" 1>&2
MANIFEST=$(docker buildx imagetools inspect "${IMAGE}:${VERSION}" \
Expand All @@ -28,6 +29,9 @@ echo "Cosign: Manifest digest is ${DIGEST}" 1>&2
echo "Cosign: Signing ${IMAGE}@${DIGEST}" 1>&2
cosign sign --yes "${IMAGE}@${DIGEST}"

echo "Cosign: Attaching SBOM attestation to ${IMAGE}@${DIGEST}" 1>&2
cosign attest --yes --predicate "$SBOM_FILE" --type spdx "${IMAGE}@${DIGEST}"

echo "Cosign: Verifying signature for ${IMAGE}@${DIGEST}" 1>&2
echo "Cosign: OIDC issuer: ${CERTIFICATE_OIDC_ISSUER}" 1>&2
echo "Cosign: Certificate identity: ${CERTIFICATE_IDENTITY}" 1>&2
Expand All @@ -37,3 +41,11 @@ cosign verify \
"${IMAGE}@${DIGEST}"

echo "Cosign: Signature verified successfully" 1>&2

echo "Cosign: Verifying SBOM attestation for ${IMAGE}@${DIGEST}" 1>&2
cosign verify-attestation --type spdx \
--certificate-oidc-issuer "$CERTIFICATE_OIDC_ISSUER" \
--certificate-identity "$CERTIFICATE_IDENTITY" \
"${IMAGE}@${DIGEST}"

echo "Cosign: SBOM attestation verified successfully" 1>&2
3 changes: 0 additions & 3 deletions enterprise/scripts/sbom-vendorpull.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
#!/usr/bin/env node

// Generates an SPDX 2.3 JSON Software Bill of Materials (SBOM) for the
// vendored C++ and frontend dependencies managed through DEPENDENCIES files

import { readFileSync, readdirSync, existsSync } from "node:fs";
import { join, resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
Expand Down
114 changes: 114 additions & 0 deletions enterprise/scripts/sbom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env node

import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
import { join } from "node:path";
import { execSync } from "node:child_process";
import { tmpdir } from "node:os";

const image = process.argv[2];
const output = process.argv[3];
if (!image || !output) {
process.stderr.write(`Usage: ${process.argv[1]} <image> <output>\n`);
process.exit(1);
}

const workdir = mkdtempSync(join(tmpdir(), "sbom-"));
try {
try {
execSync(`docker image inspect ${image}`, { stdio: "ignore" });
} catch {
execSync(`docker pull ${image}`, { stdio: [ "ignore", "inherit", "inherit" ] });
}

const container = execSync(`docker create ${image}`, { encoding: "utf-8" }).trim();
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Avoid interpolating the untrusted image argument into shell commands executed via execSync; this enables command injection. Use execFile/execFileSync with an арг array to avoid shell parsing, or validate/sanitize image before use.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At enterprise/scripts/sbom.js, line 23:

<comment>Avoid interpolating the untrusted `image` argument into shell commands executed via execSync; this enables command injection. Use execFile/execFileSync with an арг array to avoid shell parsing, or validate/sanitize `image` before use.</comment>

<file context>
@@ -0,0 +1,114 @@
+    execSync(`docker pull ${image}`, { stdio: [ "ignore", "inherit", "inherit" ] });
+  }
+
+  const container = execSync(`docker create ${image}`, { encoding: "utf-8" }).trim();
+  execSync(`docker cp ${container}:/usr/share/sourcemeta/one/npm-packages.spdx.json ${workdir}/npm.json`);
+  execSync(`docker cp ${container}:/usr/share/sourcemeta/one/vendor-packages.spdx.json ${workdir}/vendor.json`);
</file context>
Fix with Cubic

execSync(`docker cp ${container}:/usr/share/sourcemeta/one/npm-packages.spdx.json ${workdir}/npm.json`);
execSync(`docker cp ${container}:/usr/share/sourcemeta/one/vendor-packages.spdx.json ${workdir}/vendor.json`);
execSync(`docker cp ${container}:/usr/share/sourcemeta/one/dpkg-packages ${workdir}/dpkg-packages`);
execSync(`docker rm ${container}`, { stdio: "ignore" });

const npm = JSON.parse(readFileSync(join(workdir, "npm.json"), "utf-8"));
const vendor = JSON.parse(readFileSync(join(workdir, "vendor.json"), "utf-8"));
const dpkg = readFileSync(join(workdir, "dpkg-packages"), "utf-8");

const version = vendor.packages
.find((entry) => entry.SPDXID === "SPDXRef-RootPackage").versionInfo;

const packages = [{
name: "sourcemeta-one-enterprise",
SPDXID: "SPDXRef-RootPackage",
versionInfo: version,
downloadLocation: "https://github.com/sourcemeta/one",
filesAnalyzed: false,
licenseConcluded: "NOASSERTION",
licenseDeclared: "NOASSERTION"
}];

const relationships = [{
spdxElementId: "SPDXRef-DOCUMENT",
relationshipType: "DESCRIBES",
relatedSpdxElement: "SPDXRef-RootPackage"
}];

let index = 0;

for (const entry of vendor.packages) {
if (entry.SPDXID === "SPDXRef-RootPackage") continue;
index += 1;
const spdxid = `SPDXRef-Vendor-${index}`;
packages.push({ ...entry, SPDXID: spdxid });
relationships.push({
spdxElementId: "SPDXRef-RootPackage",
relationshipType: "DEPENDS_ON",
relatedSpdxElement: spdxid
});
}

for (const entry of npm.packages || []) {
if (entry.SPDXID === "SPDXRef-RootPackage") continue;
index += 1;
const spdxid = `SPDXRef-Npm-${index}`;
packages.push({ ...entry, SPDXID: spdxid });
relationships.push({
spdxElementId: "SPDXRef-RootPackage",
relationshipType: "DEPENDS_ON",
relatedSpdxElement: spdxid
});
}

for (const line of dpkg.split("\n")) {
if (!line.trim()) continue;
const [name, entryVersion, homepage] = line.split("\t");
index += 1;
const spdxid = `SPDXRef-Dpkg-${index}`;
packages.push({
name,
SPDXID: spdxid,
versionInfo: entryVersion,
downloadLocation: homepage || "NOASSERTION",
filesAnalyzed: false,
licenseConcluded: "NOASSERTION",
licenseDeclared: "NOASSERTION"
});
relationships.push({
spdxElementId: "SPDXRef-RootPackage",
relationshipType: "DEPENDS_ON",
relatedSpdxElement: spdxid
});
}

writeFileSync(output, JSON.stringify({
spdxVersion: "SPDX-2.3",
dataLicense: "CC0-1.0",
SPDXID: "SPDXRef-DOCUMENT",
name: "sourcemeta-one-enterprise",
documentNamespace: `https://one.sourcemeta.com/sbom/${version}`,
creationInfo: {
created: new Date().toISOString(),
creators: [ "Tool: enterprise/scripts/sbom.js" ]
},
packages,
relationships
}, null, 2) + "\n");
} finally {
rmSync(workdir, { recursive: true, force: true });
}
12 changes: 12 additions & 0 deletions vendor/spdx.mask

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading