UNPKG

@cyclonedx/cdxgen

Version:

Creates CycloneDX Software Bill of Materials (SBOM) from source or container image

248 lines (214 loc) 9.14 kB
import path from "node:path"; import { fileURLToPath } from "node:url"; import { assert, describe, it } from "poku"; import { gitlabCiParser } from "./gitlabCi.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, "../../.."); describe("gitlabCiParser", () => { it("has correct metadata", () => { assert.strictEqual(gitlabCiParser.id, "gitlab-ci"); assert.ok(Array.isArray(gitlabCiParser.patterns)); assert.ok(gitlabCiParser.patterns.length > 0); assert.strictEqual(typeof gitlabCiParser.parse, "function"); }); it("returns empty arrays for no files", () => { const result = gitlabCiParser.parse([], {}); assert.deepStrictEqual(result.workflows, []); assert.deepStrictEqual(result.components, []); assert.deepStrictEqual(result.services, []); assert.deepStrictEqual(result.properties, []); assert.deepStrictEqual(result.dependencies, []); }); it("parses the GitLab CI fixture", () => { const f = path.join(repoRoot, "test", "data", "gitlab-ci.yml"); const result = gitlabCiParser.parse([f], {}); assert.ok(Array.isArray(result.workflows)); assert.strictEqual(result.workflows.length, 1, "expected one workflow"); const wf = result.workflows[0]; assert.ok(wf["bom-ref"]); assert.strictEqual(wf.name, "GitLab CI Pipeline"); assert.ok(Array.isArray(wf.tasks)); assert.ok(wf.tasks.length > 0, "expected at least one task (job)"); const jobNames = wf.tasks.map((t) => t.name); assert.ok(jobNames.includes("build"), "expected build job"); assert.ok(jobNames.includes("test"), "expected test job"); // image used in jobs captured as components assert.ok(Array.isArray(result.components)); const compNames = result.components.map((c) => c.name); assert.ok( compNames.includes("node:20"), "expected node:20 container component", ); }); it("extracts services from jobs", () => { const f = path.join(repoRoot, "test", "data", "gitlab-ci.yml"); const result = gitlabCiParser.parse([f], {}); // The test job has services: [postgres:14, redis:7] assert.ok(Array.isArray(result.services)); assert.ok( result.services.length > 0, "expected at least one service from jobs", ); const svcNames = result.services.map((s) => s.name); assert.ok( svcNames.some((n) => n.includes("postgres")), "expected postgres service", ); assert.ok( svcNames.some((n) => n.includes("redis")), "expected redis service", ); }); it("produces workflow dependency links", () => { const f = path.join(repoRoot, "test", "data", "gitlab-ci.yml"); const result = gitlabCiParser.parse([f], {}); assert.ok(result.dependencies.length > 0); const wfDep = result.dependencies.find( (d) => d.ref === result.workflows[0]["bom-ref"], ); assert.ok(wfDep); assert.ok(wfDep.dependsOn.length > 0); }); it("gracefully handles missing file", () => { const result = gitlabCiParser.parse(["/no/such/file/.gitlab-ci.yml"], {}); assert.deepStrictEqual(result.workflows, []); assert.deepStrictEqual(result.components, []); }); it("skips anchor/reserved keys", () => { const f = path.join(repoRoot, "test", "data", "gitlab-ci.yml"); const result = gitlabCiParser.parse([f], {}); const taskNames = result.workflows[0].tasks.map((t) => t.name); // Should not include 'image', 'stages', 'variables', 'cache', 'services' assert.ok(!taskNames.includes("image")); assert.ok(!taskNames.includes("stages")); assert.ok(!taskNames.includes("variables")); assert.ok(!taskNames.includes("cache")); }); it("parses gitlab-ci-rules.yml: all jobs extracted (rules-based pipeline)", () => { const f = path.join(repoRoot, "test", "data", "gitlab-ci-rules.yml"); const result = gitlabCiParser.parse([f], {}); assert.strictEqual(result.workflows.length, 1); const taskNames = result.workflows[0].tasks.map((t) => t.name); // Core jobs must be present assert.ok(taskNames.includes("flake8"), "expected flake8 job"); assert.ok(taskNames.includes("mypy"), "expected mypy job"); assert.ok(taskNames.includes("build:wheel"), "expected build:wheel job"); assert.ok(taskNames.includes("build:docker"), "expected build:docker job"); assert.ok(taskNames.includes("test:unit"), "expected test:unit job"); assert.ok( taskNames.includes("test:integration"), "expected test:integration job", ); assert.ok(taskNames.includes("test:matrix"), "expected test:matrix job"); assert.ok( taskNames.includes("deploy:staging"), "expected deploy:staging job", ); assert.ok( taskNames.includes("deploy:production"), "expected deploy:production job", ); // Hidden jobs (starts with '.') must NOT appear assert.ok( !taskNames.some((n) => n.startsWith(".")), "hidden jobs must not appear", ); }); it("parses gitlab-ci-rules.yml: job-level image object extracted", () => { const f = path.join(repoRoot, "test", "data", "gitlab-ci-rules.yml"); const result = gitlabCiParser.parse([f], {}); // build:docker uses `image: { name: gcr.io/kaniko-project/executor:debug, entrypoint: [""] }` const compNames = result.components.map((c) => c.name); assert.ok( compNames.some((n) => n.includes("kaniko")), "expected kaniko image as component from image object syntax", ); }); it("parses gitlab-ci-rules.yml: services with alias captured", () => { const f = path.join(repoRoot, "test", "data", "gitlab-ci-rules.yml"); const result = gitlabCiParser.parse([f], {}); const svcNames = result.services.map((s) => s.name); assert.ok( svcNames.some((n) => n.includes("postgres")), "expected postgres:15-alpine service", ); assert.ok( svcNames.some((n) => n.includes("redis")), "expected redis:7-alpine service", ); // test:integration job should record services in its properties const task = result.workflows[0].tasks.find( (t) => t.name === "test:integration", ); assert.ok(task, "test:integration task must exist"); const svcProp = task.properties.find( (p) => p.name === "cdx:gitlab:job:services", ); assert.ok(svcProp, "expected cdx:gitlab:job:services property"); assert.ok( svcProp.value.includes("postgres"), "services property must include postgres", ); }); it("parses gitlab-ci-rules.yml: DAG needs recorded in job properties", () => { const f = path.join(repoRoot, "test", "data", "gitlab-ci-rules.yml"); const result = gitlabCiParser.parse([f], {}); // test:unit needs build:wheel const unitTask = result.workflows[0].tasks.find( (t) => t.name === "test:unit", ); assert.ok(unitTask, "test:unit task must exist"); const needsProp = unitTask.properties.find( (p) => p.name === "cdx:gitlab:job:needs", ); assert.ok(needsProp, "expected cdx:gitlab:job:needs property on test:unit"); assert.ok( needsProp.value.includes("build:wheel"), "needs must reference build:wheel", ); }); it("parses gitlab-ci-rules.yml: stages property recorded on workflow", () => { const f = path.join(repoRoot, "test", "data", "gitlab-ci-rules.yml"); const result = gitlabCiParser.parse([f], {}); const stagesProp = result.workflows[0].properties.find( (p) => p.name === "cdx:gitlab:stages", ); assert.ok(stagesProp, "expected cdx:gitlab:stages property"); assert.ok(stagesProp.value.includes("lint"), "stages must include lint"); assert.ok( stagesProp.value.includes("deploy"), "stages must include deploy", ); }); it("parses gitlab-ci-minimal.yml: minimal config — no stages, single job, no image", () => { const f = path.join(repoRoot, "test", "data", "gitlab-ci-minimal.yml"); const result = gitlabCiParser.parse([f], {}); assert.strictEqual(result.workflows.length, 1, "expected one workflow"); const taskNames = result.workflows[0].tasks.map((t) => t.name); assert.ok( taskNames.includes("build_and_test"), "expected build_and_test job", ); // No global image → no container components assert.strictEqual( result.components.filter((c) => c.type === "container").length, 0, "no container components expected for minimal config", ); // No stages property (stages array is empty) const stagesProp = result.workflows[0].properties.find( (p) => p.name === "cdx:gitlab:stages", ); assert.ok(!stagesProp, "no stages property expected for minimal config"); }); it("parses multiple files: two separate .gitlab-ci.yml configs produce two workflows", () => { const f1 = path.join(repoRoot, "test", "data", "gitlab-ci.yml"); const f2 = path.join(repoRoot, "test", "data", "gitlab-ci-minimal.yml"); const result = gitlabCiParser.parse([f1, f2], {}); assert.strictEqual( result.workflows.length, 2, "expected two workflows for two files", ); }); });