Skip to content
Draft
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ Options:
--project-group Dependency track project group
--project-name Dependency track project name. Default use the directory name
--project-version Dependency track project version [string] [default: ""]
--project-tag Dependency track project tag. Multiple values allowed. [array]
--project-id Dependency track project id. Either provide the id or the project name and version tog
ether [string]
--parent-project-id Dependency track parent project id [string]
Expand Down
3 changes: 3 additions & 0 deletions bin/cdxgen.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ const args = _yargs
default: "",
type: "string",
})
.option("project-tag", {
description: "Dependency track project tag. Multiple values allowed.",
})
.option("project-id", {
description:
"Dependency track project id. Either provide the id or the project name and version together",
Expand Down
1 change: 1 addition & 0 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Options:
--project-group Dependency track project group
--project-name Dependency track project name. Default use the directory name
--project-version Dependency track project version [string] [default: ""]
--project-tag Dependency track project tags. Multiple values allowed. [array]
--project-id Dependency track project id. Either provide the id or the project name and version tog
ether [string]
--parent-project-id Dependency track parent project id [string]
Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ Invoke cdxgen with the below arguments to automatically submit the BOM to your o
--project-name Dependency track project name. Default use the di
rectory name
--project-version Dependency track project version [default: ""]
--project-tag Dependency track project tag. Multiple values all
owed. [array]
--project-id Dependency track project id. Either provide the i
d or the project name and version together
--parent-project-id Dependency track parent project id
Expand Down
14 changes: 14 additions & 0 deletions lib/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8688,6 +8688,20 @@ export async function submitBom(args, bomContents) {
) {
bomPayload.parentUUID = args.parentProjectId || args.parentUUID;
}
// Add project tags if provided
// see https://docs.dependencytrack.org/2024/10/01/v4.12.0/
// corresponding API usage documentation can be found on the
// API docs site of your instance, see
// https://docs.dependencytrack.org/integrations/rest-api/
// or public instance see https://yoursky.blue/documentation/rest-api
if (typeof args.projectTag !== "undefined") {
// If args.projectTag is not an array, convert it to an array
// Attention, array items should be of form { name: "tagName " }
// see https://yoursky.blue/documentation/rest-api#tag/bom/operation/UploadBomBase64Encoded
bomPayload.projectTags = (
Array.isArray(args.projectTag) ? args.projectTag : [args.projectTag]
).map((tag) => ({ name: tag }));
}
if (DEBUG_MODE) {
console.log(
"Submitting BOM to",
Expand Down
133 changes: 133 additions & 0 deletions lib/cli/index.poku.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { afterEach, assert, beforeEach, describe, it } from "poku";
import quibble from "quibble";
import sinon from "sinon";

describe("CLI tests", () => {
describe("submitBom()", () => {
let gotStub;
let submitBom;

beforeEach(async () => {
// Create a sinon stub that mimics got()
const fakeGotResponse = {
json: sinon.stub().resolves({ success: true }),
};

gotStub = sinon.stub().returns(fakeGotResponse);

// Attach extend to the function itself
gotStub.extend = sinon.stub().returns(gotStub);

// Replace the real 'got' module with our stub
await quibble.esm("got", {
default: gotStub,
});

// Import the module under test AFTER quibble
({ submitBom } = await import(`./index.js?update=${Date.now()}`));
});

afterEach(async () => {
await quibble.reset();
sinon.reset();
});

it("should successfully report the SBOM with given project id, name, version and a single tag", async () => {
const serverUrl = "https://dtrack.example.com";
const projectId = "f7cb9f02-8041-4991-9101-b01fa07a6522";
const projectName = "cdxgen-test-project";
const projectVersion = "1.0.0";
const projectTag = "tag1";
const bomContent = {
bom: "test",
};
const apiKey = "TEST_API_KEY";
const skipDtTlsCheck = false;

const expectedRequestPayload = {
autoCreate: "true",
bom: "eyJib20iOiJ0ZXN0In0=", // stringified and base64 encoded bomContent
project: projectId,
projectName,
projectVersion,
projectTags: [{ name: projectTag }],
};

await submitBom(
{
serverUrl,
projectId,
projectName,
projectVersion,
apiKey,
skipDtTlsCheck,
projectTag,
},
bomContent,
);

// Verify got was called exactly once
sinon.assert.calledOnce(gotStub);

// Grab call arguments
const [calledUrl, options] = gotStub.firstCall.args;

// Assert call arguments against expectations
assert.equal(calledUrl, `${serverUrl}/api/v1/bom`);
assert.equal(options.method, "PUT");
assert.equal(options.https.rejectUnauthorized, !skipDtTlsCheck);
assert.equal(options.headers["X-Api-Key"], apiKey);
assert.match(options.headers["user-agent"], /@CycloneDX\/cdxgen/);
assert.deepEqual(options.json, expectedRequestPayload);
});

it("should successfully report the SBOM with given parent project, name, version and multiple single tags", async () => {
const serverUrl = "https://dtrack.example.com";
const projectName = "cdxgen-test-project";
const projectVersion = "1.0.0";
const projectTag = "tag1";
Comment on lines +84 to +88
Copy link
Collaborator

Choose a reason for hiding this comment

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

You're doing such an awesome job, that I hate to bring this up: isn't this just a copy of the above test except now it has a parent set? I'm asking because the test-description says 'multiple' (although it also says 'single'), so I figured this would test with multiple tags...

Copy link
Author

Choose a reason for hiding this comment

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

Yup, test is not ready yet... I'm massively struggling with test stubs (from ESM modules [got]) , which seems not to reset correctly between tests or/and affecting each other concerning expecations (call count). I've "consultated" various info sources (yes, even GPT and co.) but unfortunately without success. I'll dive deeper into this next week when I have some free time.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please take your time. It will be super cool to have such advanced tests!

const parentProjectId = "f7cb9f02-8041-4991-9101-b01fa07a6522";
const bomContent = {
bom: "test",
};
const apiKey = "TEST_API_KEY";
const skipDtTlsCheck = false;

const expectedRequestPayload = {
autoCreate: "true",
bom: "eyJib20iOiJ0ZXN0In0=", // stringified and base64 encoded bomContent
parentUUID: parentProjectId,
projectName,
projectVersion,
projectTags: [{ name: projectTag }],
};

await submitBom(
{
serverUrl,
parentProjectId,
projectName,
projectVersion,
apiKey,
skipDtTlsCheck,
projectTag,
},
bomContent,
);

// Verify got was called exactly once
sinon.assert.calledOnce(gotStub);

// Grab call arguments
const [calledUrl, options] = gotStub.firstCall.args;

// Assert call arguments against expectations
assert.equal(calledUrl, `${serverUrl}/api/v1/bom`);
assert.equal(options.method, "PUT");
assert.equal(options.https.rejectUnauthorized, !skipDtTlsCheck);
assert.equal(options.headers["X-Api-Key"], apiKey);
assert.match(options.headers["user-agent"], /@CycloneDX\/cdxgen/);
assert.deepEqual(options.json, expectedRequestPayload);
});
});
});
4 changes: 2 additions & 2 deletions lib/helpers/utils.poku.js
Original file line number Diff line number Diff line change
Expand Up @@ -3942,8 +3942,8 @@ it("parsePnpmLock", async () => {
3,
);
parsedList = await parsePnpmLock("./pnpm-lock.yaml");
assert.deepStrictEqual(parsedList.pkgList.length, 355);
assert.deepStrictEqual(parsedList.dependenciesList.length, 355);
assert.deepStrictEqual(parsedList.pkgList.length, 367);
assert.deepStrictEqual(parsedList.dependenciesList.length, 367);
assert.ok(parsedList.pkgList[0]);
assert.ok(parsedList.dependenciesList[0]);
parsedList = await parsePnpmLock(
Expand Down
1 change: 1 addition & 0 deletions lib/server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const ALLOWED_PARAMS = [
"projectId",
"projectName",
"projectGroup",
"projectTag",
"projectVersion",
"parentUUID",
"serverUrl",
Expand Down
16 changes: 16 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@
"@npmcli/package-json": "7.0.1",
"@npmcli/query": "4.0.1",
"@npmcli/redact": "3.2.2",
"@sinonjs/commons": "3.0.1",
"@sinonjs/fake-timers": "13.0.5",
"@sinonjs/samsam": "8.0.3",
"abbrev": "4.0.0",
"ajv": "8.17.1",
"ajv-formats": "3.0.1",
Expand Down Expand Up @@ -191,11 +194,13 @@
"promise-all-reject-late": "1.0.1",
"promise-call-limit": "3.0.2",
"properties-reader": "2.3.0",
"quibble": "0.9.2",
"read-package-json-fast": "4.0.0",
"responselike": "4.0.2",
"semver": "7.7.3",
"sequelize": "6.37.7",
"signal-exit": "4.1.0",
"sinon": "21.0.0",
"sprintf-js": "1.1.3",
"sqlite3": "npm:@appthreat/[email protected]",
"ssri": "12.0.0",
Expand Down Expand Up @@ -273,6 +278,8 @@
"devDependencies": {
"@biomejs/biome": "2.2.6",
"poku": "3.0.2",
"quibble": "0.9.2",
"sinon": "21.0.0",
"typescript": "5.9.3"
},
"optionalDependencies": {
Expand Down Expand Up @@ -335,6 +342,9 @@
"@npmcli/package-json": "7.0.1",
"@npmcli/query": "4.0.1",
"@npmcli/redact": "3.2.2",
"@sinonjs/commons": "3.0.1",
"@sinonjs/fake-timers": "13.0.5",
"@sinonjs/samsam": "8.0.3",
"abbrev": "4.0.0",
"ajv": "8.17.1",
"ajv-formats": "3.0.1",
Expand Down Expand Up @@ -387,11 +397,13 @@
"promise-all-reject-late": "1.0.1",
"promise-call-limit": "3.0.2",
"properties-reader": "2.3.0",
"quibble": "0.9.2",
"read-package-json-fast": "4.0.0",
"responselike": "4.0.2",
"semver": "7.7.3",
"sequelize": "6.37.7",
"signal-exit": "4.1.0",
"sinon": "21.0.0",
"sprintf-js": "1.1.3",
"sqlite3": "npm:@appthreat/[email protected]",
"ssri": "12.0.0",
Expand Down Expand Up @@ -437,5 +449,9 @@
"onFail": "ignore"
}
]
},
"volta": {
"node": "22.21.0",
"pnpm": "10.19.0"
}
}
Loading
Loading