diff --git a/README.md b/README.md index d0d0fd32..c0c958cc 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ DESCRIPTION Display help for hd. ``` -_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.34/src/commands/help.ts)_ +_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v6.2.36/src/commands/help.ts)_ ## `hd report committers` @@ -121,10 +121,10 @@ USAGE FLAGS -c, --csv Output in CSV format -d, --directory= Directory to search - -e, --afterDate= [default: 2024-11-19] Start date (format: yyyy-MM-dd) + -e, --afterDate= [default: 2025-01-29] Start date (format: yyyy-MM-dd) -m, --months= [default: 12] The number of months of git history to review. Cannot be used along beforeDate and afterDate - -s, --beforeDate= [default: 2025-11-19] End date (format: yyyy-MM-dd) + -s, --beforeDate= [default: 2026-01-29] End date (format: yyyy-MM-dd) -s, --save Save the committers report as herodevs.committers. -x, --exclude=... Path Exclusions (eg -x="./src/bin" -x="./dist") --json Output to JSON format @@ -152,7 +152,7 @@ Scan a given SBOM for EOL data ``` USAGE $ hd scan eol [--json] [-f | -d ] [-s] [-o ] [--saveSbom] [--sbomOutput ] - [--saveTrimmedSbom] [--hideReportUrl] [--version] + [--saveTrimmedSbom] [--hideReportUrl] [--automated] [--version] FLAGS -d, --dir= [default: ] The directory to scan in order to create a cyclonedx SBOM @@ -160,6 +160,7 @@ FLAGS -o, --output= Save the generated report to a custom path (defaults to herodevs.report.json when not provided) -s, --save Save the generated report as herodevs.report.json in the scanned directory + --automated Mark scan as automated (for CI/CD pipelines) --hideReportUrl Hide the generated web report URL for this scan --saveSbom Save the generated SBOM as herodevs.sbom.json in the scanned directory --saveTrimmedSbom Save the trimmed SBOM as herodevs.sbom-trimmed.json in the scanned directory @@ -189,10 +190,6 @@ EXAMPLES $ hd scan eol --save --saveSbom - Save the report and SBOM to custom paths - - $ hd scan eol --dir . --save --saveSbom --output ./reports/my-report.json --sbomOutput ./reports/my-sbom.json - Output the report in JSON format (for APIs, CI, etc.) $ hd scan eol --json @@ -297,7 +294,7 @@ EXAMPLES $ hd update --available ``` -_See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.7.13/src/commands/update.ts)_ +_See code: [@oclif/plugin-update](https://github.com/oclif/plugin-update/blob/v4.7.16/src/commands/update.ts)_ ## CI/CD Usage diff --git a/package-lock.json b/package-lock.json index 2824f432..d956440b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@amplitude/analytics-node": "^1.5.26", "@apollo/client": "^4.0.9", "@cyclonedx/cdxgen": "^12.0.0", - "@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.14", + "@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.16", "@inquirer/prompts": "^8.0.2", "@napi-rs/keyring": "^1.2.0", "@oclif/core": "^4.8.0", @@ -2195,7 +2195,7 @@ }, "node_modules/@herodevs/eol-shared": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/herodevs/eol-shared.git#23455493ffd7e25ca56dd360c494a02fbe72c4fe", + "resolved": "git+ssh://git@github.com/herodevs/eol-shared.git#0ff5f37a7f1d4cd6c1d0bed308238f021b67c9c5", "license": "ISC", "dependencies": { "@cyclonedx/cyclonedx-library": "^9.4.1", @@ -4250,7 +4250,6 @@ "node_modules/@oclif/core": { "version": "4.8.0", "license": "MIT", - "peer": true, "dependencies": { "ansi-escapes": "^4.3.2", "ansis": "^3.17.0", @@ -6118,7 +6117,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -6138,7 +6136,6 @@ "node_modules/@types/react": { "version": "18.3.23", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -8916,7 +8913,6 @@ "node_modules/graphql": { "version": "16.12.0", "license": "MIT", - "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -11381,7 +11377,6 @@ "node_modules/react": { "version": "18.3.1", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11682,7 +11677,6 @@ "node_modules/rxjs": { "version": "7.8.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -12637,7 +12631,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12746,7 +12739,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -12807,7 +12799,6 @@ "version": "5.9.3", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13097,7 +13088,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -13191,7 +13181,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index b1921758..dad17fa4 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@amplitude/analytics-node": "^1.5.26", "@apollo/client": "^4.0.9", "@cyclonedx/cdxgen": "^12.0.0", - "@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.14", + "@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.16", "@inquirer/prompts": "^8.0.2", "@napi-rs/keyring": "^1.2.0", "@oclif/core": "^4.8.0", diff --git a/src/commands/scan/eol.ts b/src/commands/scan/eol.ts index 78f42b59..cf2de400 100644 --- a/src/commands/scan/eol.ts +++ b/src/commands/scan/eol.ts @@ -4,7 +4,7 @@ import { Command, Flags } from '@oclif/core'; import ora from 'ora'; import { ApiError } from '../../api/errors.ts'; import { submitScan } from '../../api/nes.client.ts'; -import { config, filenamePrefix } from '../../config/constants.ts'; +import { config, filenamePrefix, SCAN_ORIGIN_AUTOMATED, SCAN_ORIGIN_CLI } from '../../config/constants.ts'; import { track } from '../../service/analytics.svc.ts'; import { requireAccessTokenForScan } from '../../service/auth.svc.ts'; import { createSbom } from '../../service/cdx.svc.ts'; @@ -78,6 +78,10 @@ export default class ScanEol extends Command { default: false, description: 'Hide the generated web report URL for this scan', }), + automated: Flags.boolean({ + default: false, + description: 'Mark scan as automated (for CI/CD pipelines)', + }), version: Flags.version(), }; @@ -210,7 +214,8 @@ export default class ScanEol extends Command { spinner.start('Scanning for EOL packages'); try { - const scan = await submitScan({ sbom: trimmedSbom }); + const scanOrigin = flags.automated ? SCAN_ORIGIN_AUTOMATED : SCAN_ORIGIN_CLI; + const scan = await submitScan({ sbom: trimmedSbom, scanOrigin }); spinner.succeed('Scan completed'); return scan; } catch (error) { diff --git a/src/config/constants.ts b/src/config/constants.ts index 00d200d5..06dc8944 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -45,3 +45,6 @@ export const config = { }; export const filenamePrefix = 'herodevs'; + +export const SCAN_ORIGIN_CLI = 'CLI Scan'; +export const SCAN_ORIGIN_AUTOMATED = 'Automated Scan'; diff --git a/test/api/nes.client.test.ts b/test/api/nes.client.test.ts index dba71db0..775d9f3e 100644 --- a/test/api/nes.client.test.ts +++ b/test/api/nes.client.test.ts @@ -1,7 +1,16 @@ import type { CreateEolReportInput } from '@herodevs/eol-shared'; import { submitScan } from '../../src/api/nes.client.ts'; +import { SCAN_ORIGIN_AUTOMATED, SCAN_ORIGIN_CLI } from '../../src/config/constants.ts'; import { FetchMock } from '../utils/mocks/fetch.mock.ts'; +function getGraphQLVariables(fetchMock: FetchMock, callIndex = 0): Record { + const calls = fetchMock.getCalls(); + const init = calls[callIndex]?.init; + if (!init?.body) return {}; + const body = JSON.parse(init.body as string); + return body.variables ?? {}; +} + describe('nes.client', () => { let fetchMock: FetchMock; @@ -107,4 +116,95 @@ describe('nes.client', () => { }; await expect(submitScan(input)).rejects.toThrow(/Failed to create EOL report/); }); + + describe('scanOrigin', () => { + it('passes scanOrigin to createReport mutation when provided', async () => { + const components = [{ purl: 'pkg:npm/test@1.0.0', metadata: { isEol: false } }]; + + fetchMock + .addGraphQL({ + eol: { createReport: { success: true, id: 'test-origin', totalRecords: components.length } }, + }) + .addGraphQL({ + eol: { + report: { + id: 'test-origin', + createdOn: new Date().toISOString(), + metadata: {}, + components, + page: 1, + totalRecords: components.length, + }, + }, + }); + + const input: CreateEolReportInput = { + sbom: { bomFormat: 'CycloneDX', components: [], specVersion: '1.4', version: 1 }, + scanOrigin: SCAN_ORIGIN_CLI, + }; + await submitScan(input); + + const variables = getGraphQLVariables(fetchMock, 0); + expect(variables.input).toHaveProperty('scanOrigin', SCAN_ORIGIN_CLI); + }); + + it('passes automated scanOrigin when specified', async () => { + const components = [{ purl: 'pkg:npm/test@1.0.0', metadata: { isEol: false } }]; + + fetchMock + .addGraphQL({ + eol: { createReport: { success: true, id: 'test-automated', totalRecords: components.length } }, + }) + .addGraphQL({ + eol: { + report: { + id: 'test-automated', + createdOn: new Date().toISOString(), + metadata: {}, + components, + page: 1, + totalRecords: components.length, + }, + }, + }); + + const input: CreateEolReportInput = { + sbom: { bomFormat: 'CycloneDX', components: [], specVersion: '1.4', version: 1 }, + scanOrigin: SCAN_ORIGIN_AUTOMATED, + }; + await submitScan(input); + + const variables = getGraphQLVariables(fetchMock, 0); + expect(variables.input).toHaveProperty('scanOrigin', SCAN_ORIGIN_AUTOMATED); + }); + + it('does not include scanOrigin when not provided', async () => { + const components = [{ purl: 'pkg:npm/test@1.0.0', metadata: { isEol: false } }]; + + fetchMock + .addGraphQL({ + eol: { createReport: { success: true, id: 'test-no-origin', totalRecords: components.length } }, + }) + .addGraphQL({ + eol: { + report: { + id: 'test-no-origin', + createdOn: new Date().toISOString(), + metadata: {}, + components, + page: 1, + totalRecords: components.length, + }, + }, + }); + + const input: CreateEolReportInput = { + sbom: { bomFormat: 'CycloneDX', components: [], specVersion: '1.4', version: 1 }, + }; + await submitScan(input); + + const variables = getGraphQLVariables(fetchMock, 0); + expect(variables.input).not.toHaveProperty('scanOrigin'); + }); + }); });