Skip to content
Open
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
15 changes: 6 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand All @@ -121,10 +121,10 @@ USAGE
FLAGS
-c, --csv Output in CSV format
-d, --directory=<value> Directory to search
-e, --afterDate=<value> [default: 2024-11-19] Start date (format: yyyy-MM-dd)
-e, --afterDate=<value> [default: 2025-01-29] Start date (format: yyyy-MM-dd)
-m, --months=<value> [default: 12] The number of months of git history to review. Cannot be used along beforeDate
and afterDate
-s, --beforeDate=<value> [default: 2025-11-19] End date (format: yyyy-MM-dd)
-s, --beforeDate=<value> [default: 2026-01-29] End date (format: yyyy-MM-dd)
-s, --save Save the committers report as herodevs.committers.<output>
-x, --exclude=<value>... Path Exclusions (eg -x="./src/bin" -x="./dist")
--json Output to JSON format
Expand Down Expand Up @@ -152,14 +152,15 @@ Scan a given SBOM for EOL data
```
USAGE
$ hd scan eol [--json] [-f <value> | -d <value>] [-s] [-o <value>] [--saveSbom] [--sbomOutput <value>]
[--saveTrimmedSbom] [--hideReportUrl] [--version]
[--saveTrimmedSbom] [--hideReportUrl] [--automated] [--version]

FLAGS
-d, --dir=<value> [default: <current directory>] The directory to scan in order to create a cyclonedx SBOM
-f, --file=<value> The file path of an existing SBOM to scan for EOL (supports CycloneDX and SPDX 2.3 formats)
-o, --output=<value> 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)_
<!-- commandsstop -->

## CI/CD Usage
Expand Down
15 changes: 2 additions & 13 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 7 additions & 2 deletions src/commands/scan/eol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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(),
};

Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
100 changes: 100 additions & 0 deletions test/api/nes.client.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> {
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;

Expand Down Expand Up @@ -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/[email protected]', 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/[email protected]', 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/[email protected]', 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');
});
});
});