diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index d5728f6e3..c267f026c 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -10,24 +10,36 @@ on: description: "Release tag to register with the MCP Registry (e.g. 15.8.0)" required: true -permissions: - id-token: write - contents: read +permissions: {} jobs: + # Builds, packs and inventories the release. Holds no publishing rights and no OIDC token. build: + name: Build and inventory if: ${{ github.event_name == 'release' }} runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: read + env: + VERSION: ${{ github.event.release.tag_name }} + outputs: + npm-tag: ${{ steps.npm-tag.outputs.npm-tag }} + packages: ${{ steps.digests.outputs.packages }} + checksums: ${{ steps.digests.outputs.checksums }} steps: - - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - node-version: 24 - registry-url: "https://registry.npmjs.org" + persist-credentials: false - - run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV - - run: echo ${VERSION} + # No registry-url: this job never publishes, and the .npmrc it writes references + # ${NODE_AUTH_TOKEN}, which setup-node v7 no longer defaults and yarn 1 refuses to leave unset. + - name: Setup Node.js + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24 - name: Install packages run: yarn install --frozen-lockfile @@ -41,27 +53,309 @@ jobs: run: npm run validate:package --workspace=@igniteui/mcp-server -- --expected-version "$VERSION" - name: Define npm tag + id: npm-tag run: | if [[ ${VERSION} == *"alpha"* || ${VERSION} == *"beta"* || ${VERSION} == *"rc"* ]]; then NPM_TAG=next else NPM_TAG=latest fi - echo "NPM_TAG=$NPM_TAG" >> $GITHUB_ENV + echo "npm-tag=$NPM_TAG" >> $GITHUB_OUTPUT echo "$NPM_TAG" + # Packs every publishable package, then describes the exact tarballs it produced. Everything + # downstream uses those files, so their digests, SBOMs, attestations and publication agree. + - name: Generate delivered SBOMs (CycloneDX 1.6) + run: node scripts/build-sbom.js --release="$VERSION" --out=artifacts + + - name: Generate build environment SBOM (CycloneDX 1.6) + # Supplementary best-effort inventory: npm tree errors are captured for the release notes. + run: node scripts/build-sbom.js --release="$VERSION" --out=artifacts --build-env + + - name: Record package digests + id: digests + run: | + set -euo pipefail + cd artifacts + + { + echo "checksums<> $GITHUB_OUTPUT + + { echo "### Packed artifacts"; echo '```'; sha256sum *.tgz; echo '```'; } >> $GITHUB_STEP_SUMMARY + + # Rendered here because this job has the compiled tooling; the write-scoped job only applies it. + - name: Prepare release notes + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + mkdir -p release-notes + gh release view "$VERSION" -R "${{ github.repository }}" --json body --jq .body > release-notes/original.md + node scripts/update-release-notes.js \ + release-notes/original.md \ + "artifacts/igniteui-cli-release-${VERSION}.supply-chain.md" \ + release-notes/updated.md + + - name: Upload release artifacts + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-artifacts + path: artifacts/* + retention-days: 30 + if-no-files-found: error + + - name: Upload release notes + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-notes + path: release-notes/* + retention-days: 30 + if-no-files-found: error + + attest: + name: Attest build provenance + needs: build + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + id-token: write + attestations: write + env: + # Artifact attestations are unavailable in private repositories outside GitHub Enterprise Cloud. + ATTESTATIONS_ENABLED: ${{ !github.event.repository.private }} + + steps: + - name: Download release artifacts + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: release-artifacts + path: artifacts + digest-mismatch: error + + - name: Verify the packages are the ones that were built + env: + EXPECTED: ${{ needs.build.outputs.checksums }} + run: | + set -euo pipefail + cd artifacts + printf '%s\n' "$EXPECTED" | sha256sum --check --strict + + - name: Attest build provenance + id: attest-provenance + if: env.ATTESTATIONS_ENABLED == 'true' + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-path: artifacts/*.tgz + + - name: Collect attestation bundle + if: env.ATTESTATIONS_ENABLED == 'true' + run: | + set -euo pipefail + mkdir -p attestations + cp "${{ steps.attest-provenance.outputs.bundle-path }}" attestations/provenance.sigstore.json + echo "- Provenance: ${{ steps.attest-provenance.outputs.attestation-url }}" >> $GITHUB_STEP_SUMMARY + + - name: Upload attestation bundle + if: env.ATTESTATIONS_ENABLED == 'true' + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: attestations-provenance + path: attestations/* + retention-days: 30 + if-no-files-found: error + + # One SBOM attestation per package: each binds that package's delivered SBOM to its tarball digest. + attest-sbom: + name: Attest SBOM (${{ matrix.package.name }}) + needs: build + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + id-token: write + attestations: write + strategy: + matrix: + package: ${{ fromJSON(needs.build.outputs.packages) }} + env: + ATTESTATIONS_ENABLED: ${{ !github.event.repository.private }} + + steps: + - name: Download release artifacts + if: env.ATTESTATIONS_ENABLED == 'true' + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: release-artifacts + path: artifacts + digest-mismatch: error + + - name: Verify the packages are the ones that were built + if: env.ATTESTATIONS_ENABLED == 'true' + env: + EXPECTED: ${{ needs.build.outputs.checksums }} + run: | + set -euo pipefail + cd artifacts + printf '%s\n' "$EXPECTED" | sha256sum --check --strict + + - name: Attest SBOM (CycloneDX) + id: attest-sbom + if: env.ATTESTATIONS_ENABLED == 'true' + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-path: artifacts/${{ matrix.package.tarball }} + sbom-path: artifacts/${{ matrix.package.sbom }} + + - name: Collect attestation bundle + if: env.ATTESTATIONS_ENABLED == 'true' + env: + PACKAGE_ID: ${{ matrix.package.id }} + PACKAGE_NAME: ${{ matrix.package.name }} + run: | + set -euo pipefail + mkdir -p attestations + cp "${{ steps.attest-sbom.outputs.bundle-path }}" "attestations/${PACKAGE_ID}.sbom-cyclonedx.sigstore.json" + echo "- SBOM (CycloneDX) for ${PACKAGE_NAME}: ${{ steps.attest-sbom.outputs.attestation-url }}" >> $GITHUB_STEP_SUMMARY + + - name: Upload attestation bundle + if: env.ATTESTATIONS_ENABLED == 'true' + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: attestations-sbom-${{ matrix.package.id }} + path: attestations/* + retention-days: 30 + if-no-files-found: error + + # The only job that can publish to npm. It compiles nothing and never checks out the repository. + publish: + name: Publish to npmjs.org + needs: [build, attest, attest-sbom] + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + id-token: write + + steps: + - name: Setup Node.js + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24 + registry-url: "https://registry.npmjs.org" + + - name: Download release artifacts + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: release-artifacts + path: artifacts + digest-mismatch: error + + - name: Verify the packages that were built and attested + env: + EXPECTED: ${{ needs.build.outputs.checksums }} + run: | + set -euo pipefail + cd artifacts + printf '%s\n' "$EXPECTED" | sha256sum --check --strict + + # Publishing the packed tarballs guarantees the published bytes are the attested bytes. + # Authentication and npm provenance both come from the OIDC token, so no npm token is needed. - name: Publish packages - # use npm run as yarn run changes the registry and publishes to https://registry.yarnpkg.com - run: npm run deploy ${NPM_TAG} + env: + NPM_TAG: ${{ needs.build.outputs.npm-tag }} + PACKAGES: ${{ needs.build.outputs.packages }} + run: | + set -euo pipefail + cd artifacts + # In dependency order, so no package is published before the workspace packages it depends on. + jq -r '.[].tarball' <<< "$PACKAGES" | while read -r tarball; do + npm publish "./${tarball}" --tag "$NPM_TAG" + done + + attach-to-release: + name: Attach release evidence + needs: [build, attest, attest-sbom] + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + env: + VERSION: ${{ github.event.release.tag_name }} + + steps: + - name: Download release artifacts + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: release-artifacts + path: artifacts + + - name: Download attestation bundles + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + if: ${{ !github.event.repository.private }} + with: + pattern: attestations-* + merge-multiple: true + path: attestations + + - name: Download release notes + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: release-notes + path: release-notes + + - name: Publish supply-chain notes to the release + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + # The notes were merged into the body the build job saw; never overwrite a later edit. + gh release view "$VERSION" -R "${{ github.repository }}" --json body --jq .body > current.md + if cmp -s current.md release-notes/original.md; then + gh release edit "$VERSION" -R "${{ github.repository }}" --notes-file release-notes/updated.md + else + echo "::warning title=Release notes::The release notes changed during the build; the supply-chain section was not added. It is in the SBOM archive." + fi + + - name: Package the SBOM archive + run: | + set -euo pipefail + + zip -j "igniteui-cli-release-${VERSION}.sbom.zip" \ + artifacts/*.cdx.json \ + "artifacts/igniteui-cli-release-${VERSION}.supply-chain.md" \ + "artifacts/igniteui-cli-release-${VERSION}.sbom.README.md" + + - name: Attach evidence to the release + env: + GH_TOKEN: ${{ github.token }} + PACKAGES: ${{ needs.build.outputs.packages }} + run: | + set -euo pipefail + + gh release upload "$VERSION" --clobber -R "${{ github.repository }}" \ + artifacts/*.tgz \ + artifacts/*.tgz.sha256 \ + artifacts/*.tgz.sha512 \ + $(jq -r '.[].sbom | "artifacts/" + .' <<< "$PACKAGES") \ + "igniteui-cli-release-${VERSION}.sbom.zip" \ + $(ls attestations/*.json 2>/dev/null || true) # Separate job so a failed registry publish can be retried with "Re-run failed jobs" # without repeating the npm publish. # Skipped for prereleases — the MCP registry has no dist-tag equivalent, # so publishing an alpha/beta/rc would make it the current version. mcp-registry: - needs: build - if: ${{ !cancelled() && (github.event_name == 'workflow_dispatch' || (needs.build.result == 'success' && !github.event.release.prerelease)) }} + needs: publish + if: ${{ !cancelled() && (github.event_name == 'workflow_dispatch' || (needs.publish.result == 'success' && !github.event.release.prerelease)) }} runs-on: ubuntu-latest + permissions: + contents: read + id-token: write defaults: run: working-directory: packages/igniteui-mcp/igniteui-doc-mcp diff --git a/package.json b/package.json index 50bc11532..c3875dff1 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ }, "scripts": { "release": "lerna version --conventional-commits --force-publish=* --yes --no-git-tag-version --no-push", - "deploy": "lerna exec 'npm publish --tag'", "version": "node ./scripts/versionScript.js", "build": "node ./node_modules/typescript/lib/tsc.js && npm run config-schema && npm run build:mcp", "build:mcp": "cd packages/igniteui-mcp/igniteui-doc-mcp && npx tsc && npx tsx scripts/build.ts", @@ -65,6 +64,7 @@ "handlebars": "4.7.10" }, "devDependencies": { + "@cyclonedx/cyclonedx-npm": "^6.0.1", "@inquirer/prompts": "^7.9.0", "@types/jasmine": "^5.1.13", "@types/minimatch": "^5.1.2", diff --git a/packages/cli/package.json b/packages/cli/package.json index 5cb9f244c..f20f79331 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "igniteui-cli", - "version": "15.8.2", + "version": "15.8.3-alpha.0", "description": "CLI tool for creating Ignite UI projects", "keywords": [ "CLI", @@ -66,9 +66,9 @@ "all": true }, "dependencies": { - "@igniteui/angular-templates": "~22.2.1582", - "@igniteui/cli-core": "~15.8.2", - "@igniteui/mcp-server": "~15.8.2", + "@igniteui/angular-templates": "~22.2.1583-alpha.0", + "@igniteui/cli-core": "~15.8.3-alpha.0", + "@igniteui/mcp-server": "~15.8.3-alpha.0", "@inquirer/prompts": "^7.9.0", "chalk": "^5.3.0", "glob": "^11.0.0", diff --git a/packages/core/package.json b/packages/core/package.json index f2fe37e0d..b1781e1ec 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@igniteui/cli-core", - "version": "15.8.2", + "version": "15.8.3-alpha.0", "description": "Base types and functionality for Ignite UI CLI", "repository": { "type": "git", diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/package.json b/packages/igniteui-mcp/igniteui-doc-mcp/package.json index 2584f359d..730f0e9f0 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/package.json +++ b/packages/igniteui-mcp/igniteui-doc-mcp/package.json @@ -1,8 +1,9 @@ { "name": "@igniteui/mcp-server", "mcpName": "io.github.IgniteUI/mcp-server", - "version": "15.8.2", + "version": "15.8.3-alpha.0", "description": "Unified MCP server for Ignite UI — documentation, GitHub API, and CLI scaffolding", + "license": "MIT", "repository": { "type": "git", "url": "https://github.com/IgniteUI/igniteui-cli.git", diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/server.json b/packages/igniteui-mcp/igniteui-doc-mcp/server.json index d08dbfa9d..1f508ea4d 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/server.json +++ b/packages/igniteui-mcp/igniteui-doc-mcp/server.json @@ -7,12 +7,12 @@ "source": "github", "url": "https://github.com/IgniteUI/igniteui-cli" }, - "version": "15.8.2", + "version": "15.8.3-alpha.0", "packages": [ { "registryType": "npm", "identifier": "@igniteui/mcp-server", - "version": "15.8.2", + "version": "15.8.3-alpha.0", "transport": { "type": "stdio" } diff --git a/packages/igx-templates/package.json b/packages/igx-templates/package.json index 61a268da2..b7b85829a 100644 --- a/packages/igx-templates/package.json +++ b/packages/igx-templates/package.json @@ -1,6 +1,6 @@ { "name": "@igniteui/angular-templates", - "version": "22.2.1582", + "version": "22.2.1583-alpha.0", "description": "Templates for Ignite UI for Angular projects and components", "repository": { "type": "git", @@ -12,7 +12,7 @@ "author": "Infragistics", "license": "MIT", "dependencies": { - "@igniteui/cli-core": "~15.8.2", + "@igniteui/cli-core": "~15.8.3-alpha.0", "typescript": "~5.5.4" } } diff --git a/packages/ng-schematics/package.json b/packages/ng-schematics/package.json index 3a72d2e39..159a89cb9 100644 --- a/packages/ng-schematics/package.json +++ b/packages/ng-schematics/package.json @@ -1,6 +1,6 @@ { "name": "@igniteui/angular-schematics", - "version": "22.2.1582", + "version": "22.2.1583-alpha.0", "description": "Ignite UI for Angular Schematics for ng new and ng generate", "repository": { "type": "git", @@ -20,8 +20,8 @@ "dependencies": { "@angular-devkit/core": "^22.0.0", "@angular-devkit/schematics": "^22.0.0", - "@igniteui/angular-templates": "~22.2.1582", - "@igniteui/cli-core": "~15.8.2", + "@igniteui/angular-templates": "~22.2.1583-alpha.0", + "@igniteui/cli-core": "~15.8.3-alpha.0", "@schematics/angular": "^22.0.0", "minimatch": "^11.0.1", "rxjs": "~7.8.1", diff --git a/scripts/build-sbom.ts b/scripts/build-sbom.ts new file mode 100644 index 000000000..ff7b2ff9b --- /dev/null +++ b/scripts/build-sbom.ts @@ -0,0 +1,435 @@ +import { execFileSync, spawnSync } from "child_process"; +import { createHash } from "crypto"; +import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs"; +import { createRequire } from "module"; +import * as path from "path"; +import { parseArgs } from "util"; +import { + assertDeliveredShape, Bom, npmPurl, PackedArtifact, promoteMainComponent, Supplier +} from "./sbom-document"; +import { AuditResult, formatSupplyChainNotes, NpmTreeDiagnostics, PackageEvidence } from "./sbom-notes"; + +const SPEC_VERSION = "1.6"; +const SUPPLIER: Supplier = { + name: "Infragistics", + url: ["https://www.infragistics.com"] +}; + +const repoRoot = path.resolve(__dirname, ".."); +// Resolved by path: the package's exports map deliberately hides both the CLI and its manifest. +const cycloneDxDir = path.join(repoRoot, "node_modules", "@cyclonedx", "cyclonedx-npm"); + +interface WorkspacePackage { + dir: string; + name: string; + version: string; + /** Other publishable workspace packages this one depends on. */ + siblings: string[]; +} + +function log(message: string) { + // eslint-disable-next-line no-console + console.log(`[sbom] ${message}`); +} + +/** Map lookup for keys the caller knows are present; a miss is a programming error. */ +function lookup(map: Map, key: string): V { + const value = map.get(key); + + if (value === undefined) { + throw new Error(`No entry for ${key}.`); + } + + return value; +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function readJson(file: string) { + return JSON.parse(readFileSync(file, "utf8")); +} + +/** + * Locate npm's JS entry point. Node refuses to spawn "npm.cmd" without a shell on Windows, + * and the same path lets the CycloneDX generator reuse this exact npm via npm_execpath. + */ +function resolveNpmCli(): string { + const nodeDir = path.dirname(process.execPath); + const candidates = [ + process.env.npm_execpath, + path.join(nodeDir, "node_modules", "npm", "bin", "npm-cli.js"), + path.join(nodeDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js") + ]; + const cli = candidates.find(candidate => candidate?.endsWith(".js") && existsSync(candidate)); + + if (!cli) { + throw new Error("Could not locate npm-cli.js next to the running Node.js installation."); + } + + return cli; +} + +const npmCli = resolveNpmCli(); + +/** Run a command without a shell, so no argument can be interpreted as shell syntax. */ +function run(command: string, args: string[], cwd: string, stdout: "pipe" | "inherit" = "pipe"): string { + return execFileSync(command, args, { + cwd, + encoding: "utf8", + maxBuffer: 256 * 1024 * 1024, + stdio: ["ignore", stdout, "inherit"], + env: { ...process.env, npm_execpath: npmCli } + }) ?? ""; +} + +function npm(args: string[], cwd: string, stdout: "pipe" | "inherit" = "pipe"): string { + return run(process.execPath, [npmCli, ...args], cwd, stdout); +} + +function sbomFileOf(artifact: PackedArtifact): string { + return artifact.name.replace(/\.tgz$/, ".cdx.json"); +} + +function digest(file: string, algorithm: string): string { + return createHash(algorithm).update(readFileSync(file)).digest("hex"); +} + +function ensureDir(dir: string): string { + mkdirSync(dir, { recursive: true }); + return dir; +} + +/** The CycloneDX CLI from node_modules, so generation never fetches a generator from the network. */ +function resolveCycloneDxCli(): string { + const cli = path.join(cycloneDxDir, "bin", "cyclonedx-npm-cli.js"); + + if (!existsSync(cli)) { + throw new Error(`@cyclonedx/cyclonedx-npm CLI not found at ${cli}. Run "yarn install" first.`); + } + + return cli; +} + +function cycloneDxToolVersion(): string { + return readJson(path.join(cycloneDxDir, "package.json")).version; +} + +/** + * The packages lerna publishes, ordered so each one follows the workspace packages it depends on. + * Read from lerna.json rather than listed here, so a new package cannot silently ship without an SBOM. + */ +function publishablePackages(): WorkspacePackage[] { + const patterns: string[] = readJson(path.join(repoRoot, "lerna.json")).packages; + const dirs = patterns.flatMap(pattern => { + if (!pattern.endsWith("/*")) { + return [pattern]; + } + const parent = pattern.slice(0, -2); + return readdirSync(path.join(repoRoot, parent), { withFileTypes: true }) + .filter(entry => entry.isDirectory()) + .map(entry => `${parent}/${entry.name}`); + }); + const manifests = dirs + .filter(dir => existsSync(path.join(repoRoot, dir, "package.json"))) + .map(dir => ({ dir, manifest: readJson(path.join(repoRoot, dir, "package.json")) })) + .filter(({ manifest }) => !manifest.private); + const names = new Set(manifests.map(({ manifest }) => manifest.name as string)); + const packages = new Map(manifests.map(({ dir, manifest }) => [manifest.name, { + dir, + name: manifest.name, + version: manifest.version, + siblings: Object.keys({ ...manifest.dependencies, ...manifest.peerDependencies }).filter(name => names.has(name)) + }])); + + const ordered: WorkspacePackage[] = []; + const visit = (pkg: WorkspacePackage, trail: string[]) => { + if (ordered.includes(pkg)) { + return; + } + if (trail.includes(pkg.name)) { + throw new Error(`Workspace dependency cycle: ${[...trail, pkg.name].join(" -> ")}`); + } + pkg.siblings.forEach(name => visit(lookup(packages, name), [...trail, pkg.name])); + ordered.push(pkg); + }; + [...packages.values()].sort((a, b) => a.name.localeCompare(b.name)).forEach(pkg => visit(pkg, [])); + + return ordered; +} + +/** Pack a built package so the SBOM subject is the exact artifact that gets published. */ +function packArtifact(pkg: WorkspacePackage, outDir: string): PackedArtifact { + // Leading "./" is required: without it npm treats "packages/..." as a GitHub shorthand spec. + const output = npm(["pack", `./${pkg.dir}`, "--pack-destination", outDir, "--json"], repoRoot); + const name = JSON.parse(output)[0].filename; + const tarball = path.join(outDir, name); + + log(`packed ${name}`); + + return { + name, + path: tarball, + sha256: digest(tarball, "sha256"), + sha512: digest(tarball, "sha512") + }; +} + +/** + * Install the packed tarball in isolation, producing the dependency closure a consumer resolves. + * Workspace siblings are released together and may not be on npm yet, so they resolve from the + * tarballs packed by this same build instead of the registry. + */ +function installDeliveredClosure(pkg: WorkspacePackage, artifacts: Map, workspaceDir: string): string { + rmSync(workspaceDir, { recursive: true, force: true }); + ensureDir(workspaceDir); + + const tarballSpec = (name: string) => + `file:${path.relative(workspaceDir, lookup(artifacts, name).path).replace(/\\/g, "/")}`; + // npm rejects an override for a direct dependency, so the package itself is left out. + const overrides = [...artifacts.keys()] + .filter(name => name !== pkg.name) + .reduce((result, name) => ({ ...result, [name]: tarballSpec(name) }), {}); + + writeFileSync( + path.join(workspaceDir, "package.json"), + `${JSON.stringify({ + name: "igniteui-cli-sbom-workspace", + version: "0.0.0", + private: true, + dependencies: { [pkg.name]: tarballSpec(pkg.name) }, + overrides + }, null, 2)}\n` + ); + + // The lockfile this writes is the evidence npm audit reads back from this same tree. + npm(["install", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund"], workspaceDir, "inherit"); + + log(`installed the delivered dependency closure of ${pkg.name}`); + return workspaceDir; +} + +function generateCycloneDx(cwd: string, extraArgs: string[]): Bom { + // Not --output-reproducible: that flag drops serialNumber, which actions/attest requires + // (alongside bomFormat and specVersion) to recognize the document as CycloneDX. + const document = run( + process.execPath, + [resolveCycloneDxCli(), "--spec-version", SPEC_VERSION, "--output-format", "JSON", "--output-file", "-", "--validate", ...extraArgs], + cwd + ); + + return JSON.parse(document); +} + +function writeBom(file: string, bom: Bom) { + writeFileSync(file, `${JSON.stringify(bom, null, 2)}\n`); + log(`wrote ${path.relative(repoRoot, file)}`); +} + +/** Capture npm's dependency-tree diagnostics without failing supplementary inventory generation. */ +function captureNpmTreeDiagnostics(): NpmTreeDiagnostics { + const result = spawnSync(process.execPath, [npmCli, "ls", "--json", "--all"], { + cwd: repoRoot, + encoding: "utf8", + maxBuffer: 256 * 1024 * 1024, + env: { ...process.env, npm_execpath: npmCli } + }); + const problems: string[] = []; + + try { + const pending = [JSON.parse(result.stdout || "{}")]; + while (pending.length) { + const entry = pending.pop(); + if (!entry || typeof entry !== "object") { + continue; + } + if (Array.isArray(entry.problems)) { + problems.push(...entry.problems); + } + pending.push(...Object.values(entry)); + } + } catch (error) { + problems.push(`Could not parse npm ls JSON: ${errorMessage(error)}`); + } + + return { + status: result.status ?? 1, + problems, + stderr: [result.error?.message, result.stderr].filter(Boolean).join("\n") + }; +} + +/** Audit a delivered closure. A nonzero exit only means findings, so it never fails the release. */ +function captureDeliveredAudit(workspaceDir: string): AuditResult { + const result = spawnSync(process.execPath, [npmCli, "audit", "--omit=dev", "--json"], { + cwd: workspaceDir, + encoding: "utf8", + maxBuffer: 256 * 1024 * 1024, + env: { ...process.env, npm_execpath: npmCli } + }); + + try { + const report = JSON.parse(result.stdout || "{}"); + + if (!report.metadata?.vulnerabilities) { + throw new Error(report.error?.summary ?? (result.stderr.trim() || "npm audit produced no report")); + } + + const packageName = Object.keys(readJson(path.join(workspaceDir, "package.json")).dependencies)[0]; + const manifest = readJson(path.join(workspaceDir, "node_modules", packageName, "package.json")); + const directDependencies = new Set(Object.keys({ + ...manifest.dependencies, ...manifest.peerDependencies, ...manifest.optionalDependencies + })); + + return { + available: true, + counts: report.metadata.vulnerabilities, + packages: Object.values(report.vulnerabilities ?? {}).map(entry => ({ + name: entry.name, + severity: entry.severity, + direct: directDependencies.has(entry.name), + fixAvailable: entry.fixAvailable !== false + })) + }; + } catch (error) { + return { available: false, reason: errorMessage(error) }; + } +} + +/** Validate a written document against the official CycloneDX schema. */ +async function assertValidCycloneDx(file: string) { + const requireFromGenerator = createRequire(path.join(cycloneDxDir, "package.json")); + const { Validation } = requireFromGenerator("@cyclonedx/cyclonedx-library"); + const error = await new Validation.JsonValidator(SPEC_VERSION).validate(readFileSync(file, "utf8")); + + if (error !== null) { + throw new Error(`${path.basename(file)} is not valid CycloneDX ${SPEC_VERSION}: ${JSON.stringify(error.details ?? error)}`); + } + + log(`validated ${path.basename(file)} against CycloneDX ${SPEC_VERSION}`); +} + +function renderNote(values: Record): string { + const template = readFileSync(path.join(repoRoot, "scripts", "templates", "sbom-readme.md"), "utf8"); + + return template.replace(/{{(\w+)}}/g, (match, key) => { + if (!(key in values)) { + throw new Error(`No value supplied for template placeholder ${match}.`); + } + return values[key]; + }); +} + +const { values: args } = parseArgs({ + options: { + "out": { type: "string", default: "artifacts" }, + "release": { type: "string" }, + "build-env": { type: "boolean", default: false }, + "keep": { type: "boolean", default: false } + } +}); + +const outDir = ensureDir(path.resolve(repoRoot, args.out)); +const release = args.release || readJson(path.join(repoRoot, "packages", "cli", "package.json")).version; +const releaseBaseName = `igniteui-cli-release-${release}`; +const fileNames = { + buildEnvCycloneDxFile: `${releaseBaseName}.build-env.cdx.json`, + supplyChainNotesFile: `${releaseBaseName}.supply-chain.md`, + readmeFile: `${releaseBaseName}.sbom.README.md` +}; + +/** The repository tree, dev dependencies included: an inventory of the toolchain, not the product. */ +async function buildEnvironmentSbom() { + // Tolerates npm-ls complaints about the yarn-installed dev tree; the delivered documents never do. + const bom = generateCycloneDx(repoRoot, ["--mc-type", "application", "--ignore-npm-errors"]); + bom.metadata.supplier = SUPPLIER; + + const file = path.join(outDir, fileNames.buildEnvCycloneDxFile); + writeBom(file, bom); + await assertValidCycloneDx(file); +} + +async function deliveredSboms() { + const packages = publishablePackages(); + log(`publishable packages: ${packages.map(pkg => `${pkg.name}@${pkg.version}`).join(", ")}`); + + const artifacts = new Map(packages.map(pkg => [pkg.name, packArtifact(pkg, outDir)] as [string, PackedArtifact])); + const evidence: PackageEvidence[] = []; + + for (const pkg of packages) { + const artifact = lookup(artifacts, pkg.name); + const workspaceDir = installDeliveredClosure( + pkg, artifacts, path.join(outDir, ".delivered", artifact.name.replace(/\.tgz$/, ""))); + const bom = promoteMainComponent( + generateCycloneDx(workspaceDir, ["--mc-type", "library", "--omit", "dev"]), + { artifact, packageName: pkg.name, supplier: SUPPLIER, version: pkg.version } + ); + const componentCount = assertDeliveredShape(bom, { packageName: pkg.name, version: pkg.version }); + log(`${pkg.name} SBOM covers ${componentCount} components`); + + const sbomFile = path.join(outDir, sbomFileOf(artifact)); + writeBom(sbomFile, bom); + await assertValidCycloneDx(sbomFile); + + writeFileSync(`${artifact.path}.sha256`, `${artifact.sha256} ${artifact.name}\n`); + writeFileSync(`${artifact.path}.sha512`, `${artifact.sha512} ${artifact.name}\n`); + + evidence.push({ name: pkg.name, version: pkg.version, audit: captureDeliveredAudit(workspaceDir) }); + } + + if (!args.keep) { + rmSync(path.join(outDir, ".delivered"), { recursive: true, force: true }); + } + + const supplyChainNotes = formatSupplyChainNotes({ packages: evidence, npmTree: captureNpmTreeDiagnostics() }); + writeFileSync(path.join(outDir, fileNames.supplyChainNotesFile), supplyChainNotes); + + const delivered = packages.map(pkg => ({ pkg, artifact: lookup(artifacts, pkg.name) })); + + // Read by the release workflow to attest and publish each package in dependency order. + writeFileSync(path.join(outDir, "packages.json"), `${JSON.stringify(delivered.map(({ pkg, artifact }) => ({ + name: pkg.name, + version: pkg.version, + id: artifact.name.replace(/\.tgz$/, ""), + tarball: artifact.name, + sbom: sbomFileOf(artifact) + })), null, 2)}\n`); + + const repository = process.env.GITHUB_REPOSITORY ?? "IgniteUI/igniteui-cli"; + writeFileSync(path.join(outDir, fileNames.readmeFile), renderNote({ + ...fileNames, + release, + generatedAt: new Date().toISOString().slice(0, 10), + documentRows: delivered.map(({ pkg, artifact }) => + `| \`${sbomFileOf(artifact)}\` | CycloneDX ${SPEC_VERSION} JSON | Delivered - every component resolved by \`npm install ${pkg.name}\` |` + ).join("\n"), + packageRows: delivered.map(({ pkg, artifact }) => + `| \`${pkg.name}\` | \`${pkg.version}\` | \`${npmPurl(pkg.name, pkg.version)}\` | \`${artifact.name}\` | \`${artifact.sha256}\` |` + ).join("\n"), + hashCommands: delivered.flatMap(({ artifact }) => + [`shasum -a 256 ${artifact.name}`, `shasum -a 512 ${artifact.name}`] + ).join("\n"), + powerShellHashCommands: delivered.flatMap(({ artifact }) => + [`Get-FileHash -Algorithm SHA256 ${artifact.name}`, `Get-FileHash -Algorithm SHA512 ${artifact.name}`] + ).join("\n"), + validateCommands: [...delivered.map(({ artifact }) => sbomFileOf(artifact)), fileNames.buildEnvCycloneDxFile] + .map(file => `cyclonedx-cli validate --input-file ${file} --input-format json --input-version v1_6 --fail-on-errors`) + .join("\n"), + attestationCommands: delivered.flatMap(({ artifact }) => [ + `gh attestation verify ${artifact.name} --repo ${repository}`, + `gh attestation verify ${artifact.name} --repo ${repository} --predicate-type https://cyclonedx.org/bom` + ]).join("\n"), + cycloneDxToolVersion: cycloneDxToolVersion(), + nodeVersion: process.version, + repository, + workflow: process.env.GITHUB_WORKFLOW ?? "a local build", + supplyChainNotes: supplyChainNotes.trim() + })); +} + +(args["build-env"] ? buildEnvironmentSbom() : deliveredSboms()).catch(error => { + // eslint-disable-next-line no-console + console.error("[sbom] SBOM generation failed", error); + process.exitCode = 1; +}); diff --git a/scripts/sbom-document.ts b/scripts/sbom-document.ts new file mode 100644 index 000000000..b566cbb3e --- /dev/null +++ b/scripts/sbom-document.ts @@ -0,0 +1,190 @@ +export interface BomProperty { + name: string; + value: string; +} + +/** The parts of a CycloneDX component this repository reads or rewrites. */ +export interface BomComponent { + "name": string; + "group"?: string; + "version"?: string; + "bom-ref"?: string; + "purl"?: string; + "properties"?: BomProperty[]; + "components"?: BomComponent[]; + [key: string]: unknown; +} + +export interface BomDependency { + ref: string; + dependsOn?: string[]; + provides?: string[]; +} + +export interface Bom { + metadata: { component?: BomComponent; [key: string]: unknown }; + components: BomComponent[]; + dependencies: BomDependency[]; + [key: string]: unknown; +} + +export interface Supplier { + name: string; + url: string[]; +} + +export interface PackedArtifact { + name: string; + path: string; + sha256: string; + sha512: string; +} + +/** Every component in the document, nested components included. */ +export function componentsIn(components: BomComponent[]): BomComponent[] { + return components.flatMap(component => [component, ...componentsIn(component.components ?? [])]); +} + +/** CycloneDX splits a scoped npm name into group and name; this joins them back. */ +export function packageNameOf(component: BomComponent): string { + return component.group ? `${component.group}/${component.name}` : component.name; +} + +export function npmPurl(packageName: string, version: string): string { + return `pkg:npm/${packageName.replace(/^@/, "%40")}@${version}`; +} + +function replacePrefix(value: string, prefix: string): string { + return value.startsWith(prefix) ? value.slice(prefix.length) : value; +} + +/** + * Replace the generated workspace root with the published package and normalize all graph refs. + */ +export function promoteMainComponent( + bom: Bom, + options: { artifact: PackedArtifact, packageName: string, supplier: Supplier, version: string } +): Bom { + const { artifact, packageName, supplier, version } = options; + const index = bom.components.findIndex(component => packageNameOf(component) === packageName); + + if (index === -1) { + throw new Error(`${packageName} is missing from the generated SBOM.`); + } + + const [main] = bom.components.splice(index, 1); + const workspaceRef = bom.metadata.component?.["bom-ref"]; + + if (workspaceRef === undefined) { + throw new Error("The generated SBOM has no metadata.component bom-ref to replace."); + } + + const workspacePrefix = `${workspaceRef}|`; + const normalizeRef = (value: string) => replacePrefix(value, workspacePrefix); + + for (const component of [main, ...componentsIn(bom.components)]) { + const ref = component["bom-ref"]; + if (ref !== undefined) { + component["bom-ref"] = normalizeRef(ref); + } + } + + bom.metadata.component = { + ...main, + type: "library", + purl: npmPurl(packageName, version), + supplier, + publisher: supplier.name, + hashes: [ + { alg: "SHA-256", content: artifact.sha256 }, + { alg: "SHA-512", content: artifact.sha512 } + ], + properties: [ + ...(main.properties ?? []), + { name: "ig:artifact:fileName", value: artifact.name } + ] + }; + bom.metadata.supplier = supplier; + bom.dependencies = bom.dependencies + .filter(entry => entry.ref !== workspaceRef) + .map(entry => ({ + ...entry, + ref: normalizeRef(entry.ref), + ...(entry.dependsOn && { dependsOn: entry.dependsOn.map(normalizeRef) }), + ...(entry.provides && { provides: entry.provides.map(normalizeRef) }) + })); + + return bom; +} + +/** + * Validate the identity, references and reachability of a delivered CycloneDX dependency graph. + * @returns Total dependency component count, including nested components. + */ +export function assertDeliveredShape(bom: Bom, expected: { packageName: string, version: string }): number { + const { packageName, version } = expected; + const failures: string[] = []; + const main = bom.metadata?.component; + const components = componentsIn(bom.components ?? []); + const componentRefs = components.map(component => component["bom-ref"]); + const knownRefs = new Set([main?.["bom-ref"], ...componentRefs]); + const dependencyGraph = new Map( + (bom.dependencies ?? []).map(entry => [entry.ref, entry.dependsOn ?? []] as [string, string[]]) + ); + + if (main?.purl !== npmPurl(packageName, version)) { + failures.push(`metadata.component.purl is "${main?.purl}"`); + } + if (components.length === 0) { + failures.push("no dependency components were resolved"); + } + if (components.some(component => packageNameOf(component) === packageName)) { + failures.push(`${packageName} is still listed as its own dependency`); + } + + const duplicateRefs = componentRefs.filter((ref, index) => componentRefs.indexOf(ref) !== index); + for (const ref of new Set(duplicateRefs)) { + failures.push(`component bom-ref "${ref}" is duplicated`); + } + + for (const entry of bom.dependencies ?? []) { + if (!knownRefs.has(entry.ref)) { + failures.push(`dependency entry references unknown component "${entry.ref}"`); + } + for (const ref of [...(entry.dependsOn ?? []), ...(entry.provides ?? [])]) { + if (!knownRefs.has(ref)) { + failures.push(`dependency entry references unknown component "${ref}"`); + } + } + } + + const mainRef = main?.["bom-ref"]; + + if (mainRef === undefined || !dependencyGraph.has(mainRef)) { + failures.push("the main component is not the root of the dependency graph"); + } else { + const reachable = new Set(); + const pending = [mainRef]; + let ref: string | undefined; + + while ((ref = pending.pop()) !== undefined) { + if (reachable.has(ref)) { + continue; + } + reachable.add(ref); + pending.push(...(dependencyGraph.get(ref) ?? [])); + } + + for (const ref of componentRefs) { + if (ref === undefined || !reachable.has(ref)) { + failures.push(`component "${ref}" is unreachable from the main component`); + } + } + } + + if (failures.length > 0) { + throw new Error(`SBOM validation failed:\n - ${failures.join("\n - ")}`); + } + + return components.length; +} diff --git a/scripts/sbom-notes.ts b/scripts/sbom-notes.ts new file mode 100644 index 000000000..1104afaf4 --- /dev/null +++ b/scripts/sbom-notes.ts @@ -0,0 +1,144 @@ +const SECTION_START = ""; +const SECTION_END = ""; +const SEVERITIES = ["critical", "high", "moderate", "low", "info"]; +const MAX_DIAGNOSTIC_LINES = 20; +const MAX_LISTED_PACKAGES = 25; +const MAX_AUDIT_REASON_LENGTH = 500; + +export interface AuditPackage { + name: string; + severity: string; + direct: boolean; + fixAvailable: boolean; +} + +export type AuditResult = + | { available: true, counts: Record, packages: AuditPackage[] } + | { available: false, reason: string }; + +export interface NpmTreeDiagnostics { + status: number; + problems: string[]; + stderr: string; +} + +export interface PackageEvidence { + name: string; + version: string; + audit: AuditResult; +} + +/** + * Strip terminal noise, npm log prefixes and absolute paths from one diagnostic line. + * Nothing is HTML-escaped: the result is rendered inside a fenced code block. + */ +function sanitize(line: string): string { + return String(line) + .replace(/\u001b\[[0-9;]*m/g, "") + .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "") + .replace(/^npm (?:error|warn|notice)\s+/i, "") + .replace(/\b(?:https?|file):\/\/[^\s"'<>]+/gi, "[URL]") + .replace(/(^|\s)["']?(?:[A-Za-z]:)?[\\/].*$/g, "$1") + .replace(/```/g, "'''") + .trim(); +} + +function markdownText(value: string, maxLength = Number.POSITIVE_INFINITY): string { + return sanitize(value) + .replace(/\s+/g, " ") + .slice(0, maxLength) + .replace(/([\\`*_[\]<>|])/g, "\\$1"); +} + +/** Deduplicated, bounded diagnostic lines. */ +function diagnosticLines(npmTree: NpmTreeDiagnostics): string[] { + return [...npmTree.problems, ...npmTree.stderr.split(/\r?\n/)] + .map(sanitize) + .filter(line => line && !/complete log of this run/i.test(line)) + .filter((line, index, lines) => lines.indexOf(line) === index) + .slice(0, MAX_DIAGNOSTIC_LINES); +} + +function vulnerabilityLines(audit: AuditResult): string[] { + if (audit.available === false) { + const reason = markdownText(audit.reason, MAX_AUDIT_REASON_LENGTH) || "npm audit returned no diagnostic"; + return [`The vulnerability scan could not be completed: ${reason}.`]; + } + + if (audit.counts.total === 0) { + return ["`npm audit` reported no known vulnerabilities in the delivered dependencies."]; + } + + const packages = [...audit.packages] + .sort((left, right) => + SEVERITIES.indexOf(left.severity) - SEVERITIES.indexOf(right.severity) || + left.name.localeCompare(right.name)) + .slice(0, MAX_LISTED_PACKAGES); + + return [ + `\`npm audit\` reported ${audit.counts.total} known vulnerabilities in the delivered dependencies.`, + "", + "| Severity | Count |", + "|---|---|", + ...SEVERITIES.filter(severity => audit.counts[severity] > 0).map( + severity => `| ${severity} | ${audit.counts[severity]} |` + ), + "", + "| Package | Severity | Direct | Fix available |", + "|---|---|---|---|", + ...packages.map(entry => + `| ${markdownText(entry.name)} | ${markdownText(entry.severity)} | ${entry.direct ? "yes" : "no"} | ${entry.fixAvailable ? "yes" : "no"} |` + ) + ]; +} + +/** + * Render the supply-chain section published to the SBOM archive and the GitHub release notes. + */ +export function formatSupplyChainNotes(evidence: { packages: PackageEvidence[], npmTree: NpmTreeDiagnostics }): string { + const diagnostics = diagnosticLines(evidence.npmTree); + const lines = ["## Supply chain", ""]; + + for (const entry of evidence.packages) { + lines.push( + `### \`${markdownText(entry.name)}\` ${markdownText(entry.version)}`, + "", + ...vulnerabilityLines(entry.audit), + "" + ); + } + + lines.push( + "
", + "Build-environment SBOM diagnostics (best-effort)", + "", + "The build-environment CycloneDX SBOM is a best-effort inventory of the repository toolchain. npm dependency-tree errors are tolerated there and do not affect the delivered-package SBOMs.", + "" + ); + + if (diagnostics.length === 0) { + lines.push("`npm ls` reported no dependency-tree problems."); + } else { + lines.push(`\`npm ls\` exited with status ${evidence.npmTree.status}.`, "", "```text", ...diagnostics, "```"); + } + + lines.push("", "
"); + + return `${lines.join("\n")}\n`; +} + +/** + * Add or replace the generated section while preserving authored GitHub release notes. + */ +export function mergeSupplyChainNotes(releaseNotes: string, notes: string): string { + const section = `${SECTION_START}\n${notes.trim()}\n${SECTION_END}`; + const start = releaseNotes.indexOf(SECTION_START); + const end = releaseNotes.indexOf(SECTION_END, start); + + if (start >= 0 && end >= start) { + return `${releaseNotes.slice(0, start)}${section}${releaseNotes.slice(end + SECTION_END.length)}`; + } + + const authored = releaseNotes.trimEnd(); + return authored ? `${authored}\n\n${section}\n` : `${section}\n`; +} diff --git a/scripts/templates/sbom-readme.md b/scripts/templates/sbom-readme.md new file mode 100644 index 000000000..b070e5978 --- /dev/null +++ b/scripts/templates/sbom-readme.md @@ -0,0 +1,66 @@ +# Software Bill of Materials - Ignite UI CLI {{release}} + +Generated {{generatedAt}} from the release build of the Ignite UI CLI packages. + +## Documents in this archive + +| File | Format | Scope | +|---|---|---| +{{documentRows}} +| `{{buildEnvCycloneDxFile}}` | CycloneDX 1.6 JSON | Best-effort build environment - development and build-time tooling | +| `{{supplyChainNotesFile}}` | Markdown | Known vulnerabilities in the delivered dependencies, plus build diagnostics | + +## Scope + +Each **delivered** document describes exactly what a consuming application resolves when it installs one package: that package plus the transitive closure of its runtime `dependencies` and the `peerDependencies` it requires at runtime. Development-only packages are excluded, because they are never installed by consumers. The packages of this release depend on each other; those dependencies are resolved from the tarballs built in the same release, so every document describes the versions released together. These are the documents to feed a vulnerability scanner or a compliance review of the shipped product. + +The **build environment** document is a supplementary, best-effort view of the repository dependency tree, including development and build-time tooling. npm dependency-tree errors are tolerated for this document and are recorded below. It exists to answer toolchain inventory questions and is deliberately *not* an inventory of what ships to customers. + +Components in the delivered documents are enumerated from evidence - the installed package tree - not from declared ranges. All CycloneDX documents are validated against the official CycloneDX schema before packaging. Only the delivered documents are included in signed GitHub SBOM attestations. + +{{supplyChainNotes}} + +## Subject artifacts + +| Package | Version | PURL | File | SHA-256 | +|---|---|---|---|---| +{{packageRows}} + +The SHA-512 digest of each file is attached to the GitHub release next to it, as `.sha512`. + +## Verify + +Recompute the digests against the artifacts you received: + +```bash +{{hashCommands}} +``` + +```powershell +{{powerShellHashCommands}} +``` + +To re-validate the documents independently, install the [official CycloneDX CLI](https://github.com/CycloneDX/cyclonedx-cli/releases) and run: + +```bash +{{validateCommands}} +``` + +Each delivered document is embedded as the predicate of a signed GitHub artifact attestation bound to its package's digest above. The build-environment document is not attested. Verify the build provenance and delivered SBOM predicate of each package explicitly with: + +```bash +{{attestationCommands}} +``` + +## Generated by + +| Tool | Version | +|---|---| +| `@cyclonedx/cyclonedx-npm` | {{cycloneDxToolVersion}} | +| Node.js | {{nodeVersion}} | + +Built by {{workflow}} in `{{repository}}`. + +## Questions + +For license clarification or a scope decision recorded in this document, contact [Infragistics support](https://www.infragistics.com/about-us/contact-us). diff --git a/scripts/update-release-notes.ts b/scripts/update-release-notes.ts new file mode 100644 index 000000000..330a68c94 --- /dev/null +++ b/scripts/update-release-notes.ts @@ -0,0 +1,13 @@ +import { readFileSync, writeFileSync } from "fs"; +import { mergeSupplyChainNotes } from "./sbom-notes"; + +const [releaseNotesFile, notesFile, outputFile] = process.argv.slice(2); + +if (!releaseNotesFile || !notesFile || !outputFile) { + throw new Error("Usage: node scripts/update-release-notes.js "); +} + +const releaseNotes = readFileSync(releaseNotesFile, "utf8"); +const notes = readFileSync(notesFile, "utf8"); + +writeFileSync(outputFile, mergeSupplyChainNotes(releaseNotes, notes)); diff --git a/yarn.lock b/yarn.lock index 0a56a9ae5..a9df94565 100644 --- a/yarn.lock +++ b/yarn.lock @@ -205,6 +205,28 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@cyclonedx/cyclonedx-library@^10.0.0": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@cyclonedx/cyclonedx-library/-/cyclonedx-library-10.3.0.tgz#92ccd1553d64d7c593b1adc31203cba01583d3df" + integrity sha512-RIT28sp1U0PCHaaeF3yNRwk6Jig/XJ8UsYAVOMYaHQ+0VVOvsjcp0PTWQFLUqJFF8XMud+Kdq6G+wtDAh+QJFA== + +"@cyclonedx/cyclonedx-npm@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@cyclonedx/cyclonedx-npm/-/cyclonedx-npm-6.0.1.tgz#269085c8d52880e81697918dbc8ae8a43735727d" + integrity sha512-/aU3bBC6qP6cV/qQ5SfUSygE/+2hQhwgg6sJML31/gZ96NyMvIUuwdk637H4z+LS/NryRT2kjR2wtD0qBEVVHQ== + dependencies: + "@cyclonedx/cyclonedx-library" "^10.0.0" + commander "^14.0.0" + normalize-package-data "^7.0.0 || ^8.0.0" + packageurl-js "^2.0.1" + spdx-expression-parse "^3.0.1 || ^4.0.0" + xmlbuilder2 "^3.0.2 || ^4.0.3" + optionalDependencies: + ajv "^8.12.0" + ajv-formats "^3.0.1" + ajv-formats-draft2019 "^1.6.1" + libxmljs2 "^0.35||^0.37" + "@emnapi/core@1.4.5": version "1.4.5" resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.5.tgz#bfbb0cbbbb9f96ec4e2c4fd917b7bbe5495ceccb" @@ -1666,6 +1688,35 @@ "@octokit/request-error" "^7.0.0" "@octokit/webhooks-methods" "^6.0.0" +"@oozcitak/dom@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@oozcitak/dom/-/dom-2.0.2.tgz#0f447f0b736aa6f36c5556ba811a44e56c71c26c" + integrity sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w== + dependencies: + "@oozcitak/infra" "^2.0.2" + "@oozcitak/url" "^3.0.0" + "@oozcitak/util" "^10.0.0" + +"@oozcitak/infra@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@oozcitak/infra/-/infra-2.0.2.tgz#e2f1cc0eeca3ac5cd551f0326a5f66f00cf1138b" + integrity sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA== + dependencies: + "@oozcitak/util" "^10.0.0" + +"@oozcitak/url@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@oozcitak/url/-/url-3.0.0.tgz#a03c959c67e28aba9e29b2d35cd50d26cb5f2cc4" + integrity sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ== + dependencies: + "@oozcitak/infra" "^2.0.2" + "@oozcitak/util" "^10.0.0" + +"@oozcitak/util@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@oozcitak/util/-/util-10.0.0.tgz#f6b40472d96c210094a556ee5ccb8e77f1bd30af" + integrity sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA== + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" @@ -2279,6 +2330,16 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +ajv-formats-draft2019@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/ajv-formats-draft2019/-/ajv-formats-draft2019-1.6.1.tgz#6affe2220e7828360793776f1976de0420acccfb" + integrity sha512-JQPvavpkWDvIsBp2Z33UkYCtXCSpW4HD3tAZ+oL4iEFOk9obQZffx0yANwECt6vzr6ET+7HN5czRyqXbnq/u0Q== + dependencies: + punycode "^2.1.1" + schemes "^1.4.0" + smtp-address-parser "^1.0.3" + uri-js "^4.4.1" + ajv-formats@3.0.1, ajv-formats@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz" @@ -2286,7 +2347,7 @@ ajv-formats@3.0.1, ajv-formats@^3.0.1: dependencies: ajv "^8.0.0" -ajv@8.20.0, ajv@^8.0.0, ajv@^8.17.1: +ajv@8.20.0, ajv@^8.0.0, ajv@^8.12.0, ajv@^8.17.1: version "8.20.0" resolved "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz" integrity sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA== @@ -2588,7 +2649,7 @@ binary-extensions@^2.0.0: resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== -bindings@^1.5.0: +bindings@^1.5.0, bindings@~1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz" integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== @@ -3118,7 +3179,12 @@ combined-stream@1.0.8, combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@^2.2.0: +commander@^14.0.0: + version "14.0.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-14.0.3.tgz#425d79b48f9af82fcd9e4fc1ea8af6c5ec07bbc2" + integrity sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw== + +commander@^2.19.0, commander@^2.2.0: version "2.20.3" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -3544,6 +3610,11 @@ diff@^4.0.1: resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +discontinuous-range@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" + integrity sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ== + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" @@ -4199,6 +4270,11 @@ express@^5.2.1: type-is "^2.0.1" vary "^1.1.2" +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + fast-content-type-parse@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz" @@ -5889,6 +5965,16 @@ libnpmpublish@11.1.2: sigstore "^4.0.0" ssri "^12.0.0" +libxmljs2@^0.35||^0.37: + version "0.37.0" + resolved "https://registry.yarnpkg.com/libxmljs2/-/libxmljs2-0.37.0.tgz#ee637d6220b348b74ef2697d1a9d2b2007628643" + integrity sha512-Xb78V8GZouoZFrq8cCwx7+G3WYOcJG0xb3YUbweSyE4z2EIrQCZMr3Ye/dHn4mESs6YxUMeQeUZm5IXg+iLHog== + dependencies: + bindings "~1.5.0" + nan "~2.22.2" + node-gyp "^11.2.0" + prebuild-install "^7.1.3" + limiter@^1.0.5: version "1.1.5" resolved "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz" @@ -6472,6 +6558,11 @@ modify-values@^1.0.1: resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== +moo@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.3.tgz#dfcdb40ff6f1a03c34af5df7f9717cecfa88c38c" + integrity sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA== + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -6507,6 +6598,11 @@ mz@^2.4.0: object-assign "^4.0.1" thenify-all "^1.0.0" +nan@~2.22.2: + version "2.22.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.2.tgz#6b504fd029fb8f38c0990e52ad5c26772fdacfbb" + integrity sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ== + nanoid@^3.3.18: version "3.3.19" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.19.tgz#336d4aa4bcd4fb24d2cddede7ffeae40bec03f0a" @@ -6522,6 +6618,16 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +nearley@^2.20.1: + version "2.20.1" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474" + integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ== + dependencies: + commander "^2.19.0" + moo "^0.5.0" + railroad-diagrams "^1.0.0" + randexp "0.4.6" + negotiator@0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" @@ -6549,7 +6655,7 @@ node-abi@^3.3.0: dependencies: semver "^7.3.5" -node-gyp@^11.0.0: +node-gyp@^11.0.0, node-gyp@^11.2.0: version "11.5.0" resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz" integrity sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ== @@ -6604,6 +6710,15 @@ normalize-package-data@^3.0.0, normalize-package-data@^3.0.3: semver "^7.3.4" validate-npm-package-license "^3.0.1" +"normalize-package-data@^7.0.0 || ^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-8.0.0.tgz#bdce7ff2d6ba891b853e179e45a5337766e304a7" + integrity sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ== + dependencies: + hosted-git-info "^9.0.0" + semver "^7.3.5" + validate-npm-package-license "^3.0.4" + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" @@ -7206,6 +7321,11 @@ package-json-from-dist@^1.0.0: resolved "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== +packageurl-js@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/packageurl-js/-/packageurl-js-2.0.1.tgz#a8fa43a64971b5dd0dca5fb904b950a6cc317a6f" + integrity sha512-N5ixXjzTy4QDQH0Q9YFjqIWd6zH6936Djpl2m9QNFmDv5Fum8q8BjkpAcHNMzOFE0IwQrFhJWex3AN6kS0OSwg== + pacote@21.0.1, pacote@^21.0.0: version "21.0.1" resolved "https://registry.npmjs.org/pacote/-/pacote-21.0.1.tgz" @@ -7467,7 +7587,7 @@ postcss@^8.5.6: picocolors "^1.1.1" source-map-js "^1.2.1" -prebuild-install@^7.1.1: +prebuild-install@^7.1.1, prebuild-install@^7.1.3: version "7.1.3" resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz" integrity sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug== @@ -7597,7 +7717,7 @@ punycode.js@^2.3.1: resolved "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz" integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== -punycode@^2.1.0: +punycode@^2.1.0, punycode@^2.1.1: version "2.3.1" resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== @@ -7620,6 +7740,19 @@ quick-lru@^4.0.1: resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +railroad-diagrams@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" + integrity sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A== + +randexp@0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" + integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== + dependencies: + discontinuous-range "1.0.0" + ret "~0.1.10" + range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" @@ -7875,6 +8008,11 @@ restore-cursor@^5.0.0: onetime "^7.0.0" signal-exit "^4.1.0" +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + retry@^0.12.0: version "0.12.0" resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" @@ -8022,6 +8160,13 @@ safe-stable-stringify@^2.2.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +schemes@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/schemes/-/schemes-1.4.0.tgz#9d03302275e562488dd3afa7b654ed42e00569ec" + integrity sha512-ImFy9FbCsQlVgnE3TCWmLPCFnVzx0lHL/l+umHplDqAKd0dzFpnS6lFZIpagBlYhKwzVmlV36ec0Y1XTu8JBAQ== + dependencies: + extend "^3.0.0" + "semver@2 || 3 || 4 || 5", semver@^5.6.0: version "5.7.2" resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" @@ -8318,6 +8463,13 @@ smol-toml@1.6.1: resolved "https://registry.yarnpkg.com/smol-toml/-/smol-toml-1.6.1.tgz#4fceb5f7c4b86c2544024ef686e12ff0983465be" integrity sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg== +smtp-address-parser@^1.0.3: + version "1.1.1" + resolved "https://registry.yarnpkg.com/smtp-address-parser/-/smtp-address-parser-1.1.1.tgz#71070328e9f2006ad4fcabc7aa36c1ea246bbf78" + integrity sha512-02QRuo0uL+I6+VHY5LxYiKJMjbyrTJWFM6JWkWOcdLkfInjLvGdPQTjC+mUOt/0PzeNnRSCYNIDZfn6QRIQYug== + dependencies: + nearley "^2.20.1" + socket.io-adapter@~2.5.2: version "2.5.5" resolved "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz" @@ -8446,6 +8598,14 @@ spdx-expression-parse@^3.0.0: spdx-exceptions "^2.1.0" spdx-license-ids "^3.0.0" +"spdx-expression-parse@^3.0.1 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz#a23af9f3132115465dac215c099303e4ceac5794" + integrity sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + spdx-license-ids@^3.0.0: version "3.0.21" resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz" @@ -9194,7 +9354,7 @@ update-browserslist-db@^1.3.3: escalade "^3.2.0" picocolors "^1.1.1" -uri-js@^4.2.2: +uri-js@^4.2.2, uri-js@^4.4.1: version "4.4.1" resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== @@ -9499,6 +9659,16 @@ ws@~8.21.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.3.tgz#660b4faddb6a3e575c86e078126919961f4de4fc" integrity sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw== +"xmlbuilder2@^3.0.2 || ^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz#91660fa6d30f19d716f8b1194c567686d4402c63" + integrity sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA== + dependencies: + "@oozcitak/dom" "^2.0.2" + "@oozcitak/infra" "^2.0.2" + "@oozcitak/util" "^10.0.0" + js-yaml "^4.1.1" + xmlhttprequest-ssl@~2.1.1: version "2.1.2" resolved "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz"