diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d37cd7b..b3aefc45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: name: "${{matrix.platform}} w/ Node.js ${{matrix.node}}.x" runs-on: ${{matrix.platform}} env: - GRAPHQL_HOST: ${{ secrets.GRAPHQL_HOST }} + GRAPHQL_HOST: ${{ secrets.NEW_GRAPHQL_HOST }} EOL_REPORT_URL: ${{ secrets.EOL_REPORT_URL }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/e2e/scan/eol.test.ts b/e2e/scan/eol.test.ts index 642ba2e4..19329d7b 100644 --- a/e2e/scan/eol.test.ts +++ b/e2e/scan/eol.test.ts @@ -5,12 +5,12 @@ import { existsSync, readFileSync, unlinkSync } from 'node:fs'; import { mkdir } from 'node:fs/promises'; import path from 'node:path'; import { describe, it } from 'node:test'; -import { fileURLToPath } from 'node:url'; import { promisify } from 'node:util'; import { runCommand } from '@oclif/test'; import { config, filenamePrefix } from '../../src/config/constants'; const execAsync = promisify(exec); +const fixturesDir = path.resolve(import.meta.dirname, '../fixtures'); describe('environment', () => { it('should not be configured to run against the production environment', () => { @@ -26,8 +26,7 @@ describe('default arguments', () => { // Run the CLI directly with no arguments const { stdout } = await execAsync('node bin/run.js'); - // Match EOL count - match(stdout, /1( .*)End-of-Life \(EOL\)/, 'Should show EOL count'); + match(stdout, /[1-9]\d*( .*)End-of-Life \(EOL\)/, 'Should show non-zero EOL count'); }); it('runs scan:eol --json when --json is passed in', async () => { @@ -59,13 +58,10 @@ describe('default arguments', () => { }); describe('scan:eol e2e', () => { - const __dirname = path.dirname(fileURLToPath(import.meta.url)); - const fixturesDir = path.resolve(__dirname, '../fixtures'); - const simplePurls = path.resolve(__dirname, '../fixtures/npm/simple.purls.json'); + const simplePurls = path.resolve(fixturesDir, 'npm/simple.purls.json'); const simpleSbom = path.join(fixturesDir, `npm/${filenamePrefix}.sbom.json`); const reportPath = path.resolve(fixturesDir, `${filenamePrefix}.report.json`); - const upToDatePurls = path.resolve(__dirname, '../fixtures/npm/up-to-date.purls.json'); - const emptyPurlsPath = path.resolve(__dirname, '../fixtures/npm/empty.purls.json'); + const emptyPurlsPath = path.resolve(fixturesDir, 'npm/empty.purls.json'); async function run(cmd: string) { // Ensure fixtures directory exists and is clean @@ -133,7 +129,7 @@ describe('scan:eol e2e', () => { }); it.skip('scans extra-large.purls.json for EOL components', async () => { - const extraLargePurlsPath = path.resolve(__dirname, '../fixtures/npm/extra-large.purls.json'); + const extraLargePurlsPath = path.resolve(fixturesDir, 'npm/extra-large.purls.json'); const cmd = `scan:eol --purls ${extraLargePurlsPath}`; const { stdout } = await run(cmd); @@ -171,19 +167,22 @@ describe('scan:eol e2e', () => { const json = JSON.parse(stdout); const bootstrap = json.components.find((component) => component.purl.startsWith('pkg:npm/bootstrap@')); - strictEqual(bootstrap?.info.status, 'EOL', 'Should match EOL count'); - strictEqual(bootstrap?.info.nesAvailable, true, 'Should match remediation count'); + strictEqual(bootstrap?.metadata.isEol, true, 'Bootstrap should be marked as EOL'); + strictEqual( + !!bootstrap?.nesRemediation?.remediations?.length, + true, + 'Bootstrap should have NES remediation available', + ); }); it('correctly identifies Angular 17 as having a EOL date when using --json flag', async () => { - const angular17Purls = path.resolve(__dirname, '../fixtures/npm/angular-17.purls.json'); + const angular17Purls = path.resolve(fixturesDir, 'npm/angular-17.purls.json'); const cmd = `scan:eol --purls=${angular17Purls} --json`; const { stdout } = await run(cmd); const json = JSON.parse(stdout); const angular17 = json.components.find((component) => component.purl.startsWith('pkg:npm/%40angular/core@')); - // Match EOL count - strictEqual(angular17?.info.status, 'EOL', 'Should match EOL status'); + strictEqual(angular17?.metadata.isEol, true, 'Angular 17 should be marked as EOL'); }); describe('web report URL', () => { @@ -210,9 +209,7 @@ describe('scan:eol e2e', () => { * Please see CONTRIBUTING.md before adding new tests to this section. */ describe('with directory flag', () => { - const __dirname = path.dirname(fileURLToPath(import.meta.url)); - const simpleDir = path.resolve(__dirname, '../fixtures/npm/simple'); - const upToDateDir = path.resolve(__dirname, '../fixtures/npm/up-to-date'); + const simpleDir = path.resolve(fixturesDir, 'npm/simple'); const reportPath = path.join(simpleDir, `${filenamePrefix}.report.json`); async function run(cmd: string) { diff --git a/package-lock.json b/package-lock.json index 5bf737b6..24a89e13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@apollo/client": "^3.13.8", "@cyclonedx/cdxgen": "^11.4.3", + "@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.4", "@oclif/core": "^4.4.0", "@oclif/plugin-help": "^6.2.29", "@oclif/plugin-update": "^4.6.45", @@ -55,6 +56,23 @@ "node": ">=14.13.1" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", + "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, "node_modules/@apollo/client": { "version": "3.13.8", "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.13.8.tgz", @@ -1664,6 +1682,32 @@ "integrity": "sha512-fWC4ZPxo80qlh3xN5FxfIoQD3phVY4+EyzTIqyksjhKNDmaicdpxSvkWwIrYTtv9C1/RcUN6pxaTwGmj2NzS6A==", "license": "MIT" }, + "node_modules/@cyclonedx/cyclonedx-library": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@cyclonedx/cyclonedx-library/-/cyclonedx-library-8.5.0.tgz", + "integrity": "sha512-1EkCTcch4Sek6masF4yY1AV0iYg34tx18HoS7nn5v4DLNRELoqvDv64+pPnHzr67K35M7JviCzIOQBEdIsuv0w==", + "funding": [ + { + "type": "individual", + "url": "https://owasp.org/donate/?reponame=www-project-cyclonedx&title=OWASP+CycloneDX" + } + ], + "license": "Apache-2.0", + "dependencies": { + "packageurl-js": "^2.0.1", + "spdx-expression-parse": "^3.0.1 || ^4" + }, + "engines": { + "node": ">=20.18.0" + }, + "optionalDependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "ajv-formats-draft2019": "^1.6.1", + "libxmljs2": "^0.35||^0.37", + "xmlbuilder2": "^3.0.2" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", @@ -2098,6 +2142,50 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@herodevs/eol-shared": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/herodevs/eol-shared.git#1e8326d833217847c470bd890f00b67d67f07998", + "license": "ISC", + "dependencies": { + "@cyclonedx/cyclonedx-library": "^8.5.0", + "fast-xml-parser": "^5.2.5", + "json-schema-to-typescript": "^15.0.4", + "packageurl-js": "^2.0.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@herodevs/eol-shared/node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@herodevs/eol-shared/node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/@iarna/toml": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", @@ -3349,6 +3437,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4002,6 +4096,58 @@ "@oclif/core": ">= 3.0.0" } }, + "node_modules/@oozcitak/dom": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz", + "integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@oozcitak/infra": "1.0.8", + "@oozcitak/url": "1.0.4", + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@oozcitak/infra": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz", + "integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@oozcitak/url": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz", + "integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@oozcitak/infra": "1.0.8", + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@oozcitak/util": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz", + "integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -5072,6 +5218,18 @@ "rxjs": "^7.2.0" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -5296,6 +5454,22 @@ } } }, + "node_modules/ajv-formats-draft2019": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ajv-formats-draft2019/-/ajv-formats-draft2019-1.6.1.tgz", + "integrity": "sha512-JQPvavpkWDvIsBp2Z33UkYCtXCSpW4HD3tAZ+oL4iEFOk9obQZffx0yANwECt6vzr6ET+7HN5czRyqXbnq/u0Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "punycode": "^2.1.1", + "schemes": "^1.4.0", + "smtp-address-parser": "^1.0.3", + "uri-js": "^4.4.1" + }, + "peerDependencies": { + "ajv": "*" + } + }, "node_modules/ansi": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz", @@ -5376,6 +5550,12 @@ "dev": true, "license": "MIT" }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", @@ -6183,6 +6363,13 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/commander": { + "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==", + "license": "MIT", + "optional": true + }, "node_modules/common-ancestor-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", @@ -6632,6 +6819,13 @@ "node": ">=0.3.1" } }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", + "license": "MIT", + "optional": true + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -7205,6 +7399,13 @@ "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", "license": "Apache-2.0" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT", + "optional": true + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -8637,7 +8838,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8659,7 +8859,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -8924,6 +9123,18 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsbn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", @@ -8964,6 +9175,29 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/json-schema-to-typescript": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz", + "integrity": "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.5.5", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.7", + "is-glob": "^4.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "prettier": "^3.2.5", + "tinyglobby": "^0.2.9" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -9096,6 +9330,23 @@ "node": ">=0.10.0" } }, + "node_modules/libxmljs2": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/libxmljs2/-/libxmljs2-0.37.0.tgz", + "integrity": "sha512-Xb78V8GZouoZFrq8cCwx7+G3WYOcJG0xb3YUbweSyE4z2EIrQCZMr3Ye/dHn4mESs6YxUMeQeUZm5IXg+iLHog==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "~1.5.0", + "nan": "~2.22.2", + "node-gyp": "^11.2.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -9572,6 +9823,13 @@ "node": "*" } }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", + "license": "BSD-3-Clause", + "optional": true + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9587,6 +9845,13 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/nan": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", + "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", + "license": "MIT", + "optional": true + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -9603,6 +9868,29 @@ "node": ">=18" } }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, "node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", @@ -10765,6 +11053,21 @@ "node": ">=10" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prettify-xml": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/prettify-xml/-/prettify-xml-1.2.0.tgz", @@ -10922,6 +11225,16 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/pupa": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz", @@ -10986,6 +11299,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", + "license": "CC0-1.0", + "optional": true + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/raw-body": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", @@ -11266,6 +11600,16 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -11386,6 +11730,16 @@ "loose-envify": "^1.1.0" } }, + "node_modules/schemes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/schemes/-/schemes-1.4.0.tgz", + "integrity": "sha512-ImFy9FbCsQlVgnE3TCWmLPCFnVzx0lHL/l+umHplDqAKd0dzFpnS6lFZIpagBlYhKwzVmlV36ec0Y1XTu8JBAQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "extend": "^3.0.0" + } + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -11799,6 +12153,19 @@ "npm": ">= 3.0.0" } }, + "node_modules/smtp-address-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/smtp-address-parser/-/smtp-address-parser-1.1.0.tgz", + "integrity": "sha512-Gz11jbNU0plrReU9Sj7fmshSBxxJ9ShdD2q4ktHIHo/rpTH6lFyQoYHYKINPJtPe8aHFnsbtW46Ls0tCCBsIZg==", + "license": "MIT", + "optional": true, + "dependencies": { + "nearley": "^2.20.1" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", @@ -12870,6 +13237,16 @@ "tslib": "^2.0.3" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -13172,6 +13549,53 @@ "xml-js": "bin/cli.js" } }, + "node_modules/xmlbuilder2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.1.1.tgz", + "integrity": "sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@oozcitak/dom": "1.15.10", + "@oozcitak/infra": "1.0.8", + "@oozcitak/util": "8.3.8", + "js-yaml": "3.14.1" + }, + "engines": { + "node": ">=12.0" + } + }, + "node_modules/xmlbuilder2/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/xmlbuilder2/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "optional": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/xmlbuilder2/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause", + "optional": true + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index db4a746a..20bd4438 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "dependencies": { "@apollo/client": "^3.13.8", "@cyclonedx/cdxgen": "^11.4.3", + "@herodevs/eol-shared": "github:herodevs/eol-shared#v0.1.4", "@oclif/core": "^4.4.0", "@oclif/plugin-help": "^6.2.29", "@oclif/plugin-update": "^4.6.45", diff --git a/src/api/nes/nes.client.ts b/src/api/nes/nes.client.ts index 6e573fd5..50c222d9 100644 --- a/src/api/nes/nes.client.ts +++ b/src/api/nes/nes.client.ts @@ -1,26 +1,15 @@ import type * as apollo from '@apollo/client/core/index.js'; import { PackageURL } from 'packageurl-js'; +import type { CreateEolReportInput, EolReport } from '@herodevs/eol-shared'; import { ApolloClient } from '../../api/client.ts'; -import type { - InsightsEolScanComponent, - InsightsEolScanInput, - InsightsEolScanResult, -} from '../../api/types/nes.types.ts'; import { config } from '../../config/constants.ts'; import { debugLogger } from '../../service/log.svc.ts'; -import { SbomScanner, buildScanResult } from '../../service/nes/nes.svc.ts'; -import { - DEFAULT_SCAN_BATCH_SIZE, - DEFAULT_SCAN_INPUT_OPTIONS, - type ProcessBatchOptions, - type ScanInputOptions, - type ScanResult, -} from '../types/hd-cli.types.ts'; +import { SbomScanner } from '../../service/nes/nes.svc.ts'; export interface NesClient { scan: { - purls: (purls: string[], options: ScanInputOptions) => Promise; + purls: (input: CreateEolReportInput) => Promise; }; } @@ -44,42 +33,37 @@ export class NesApolloClient implements NesClient { } /** - * Submit a scan for a list of purls after they've been batched by batchSubmitPurls + * Submit a scan for a list of purls */ -function submitScan(purls: string[], options: ScanInputOptions): Promise { +export function submitScan(input: CreateEolReportInput): Promise { const host = config.graphqlHost; const path = config.graphqlPath; const url = host + path; debugLogger('Submitting scan to %s', url); const client = new NesApolloClient(url); - return client.scan.purls(purls, options); + return client.scan.purls(input); } -export const batchSubmitPurls = async ( - purls: string[], - options = DEFAULT_SCAN_INPUT_OPTIONS, - batchSize = DEFAULT_SCAN_BATCH_SIZE, -): Promise => { +export const submitPurls = async (purls: string[]): Promise => { try { const dedupedAndEncodedPurls = dedupeAndEncodePurls(purls); - const batches = createBatches(dedupedAndEncodedPurls, batchSize); - debugLogger('Processing %d batches', batches.length); + debugLogger('Submitting %d purls', dedupedAndEncodedPurls.length); - if (batches.length === 0) { + if (dedupedAndEncodedPurls.length === 0) { return { - components: new Map(), - message: 'No batches to process', - success: true, - warnings: [], - scanId: undefined, - createdOn: undefined, - } satisfies ScanResult; + components: [], + createdOn: new Date().toISOString(), + id: '', + metadata: { + unknownComponentsCount: 0, + totalComponentsCount: 0, + }, + } satisfies EolReport; } - const results = await processBatches(batches, options); - return handleBatchResults(results); + return submitScan({ components: dedupedAndEncodedPurls }); } catch (error) { - debugLogger('Fatal error in batchSubmitPurls: %s', error); + debugLogger('Fatal error in submitPurls: %s', error); throw new Error(`Failed to process purls: ${error instanceof Error ? error.message : String(error)}`); } }; @@ -101,78 +85,3 @@ export const dedupeAndEncodePurls = (purls: string[]): string[] => { return Array.from(dedupedAndEncodedPurls); }; - -export const createBatches = (items: string[], batchSize: number): string[][] => { - const numberOfBatches = Math.ceil(items.length / batchSize); - - return Array.from({ length: numberOfBatches }, (_, index) => { - const startIndex = index * batchSize; - const endIndex = startIndex + batchSize; - return items.slice(startIndex, endIndex); - }); -}; - -export const processBatch = async ({ - batch, - index, - totalPages, - scanOptions, - previousScanId, -}: ProcessBatchOptions): Promise => { - const page = index + 1; - if (page > totalPages) { - throw new Error('Total pages exceeded'); - } - - debugLogger('Processing batch %d of %d', page, totalPages); - debugLogger('ScanID: %s', previousScanId); - const result = await submitScan(batch, { - ...scanOptions, - page, - totalPages, - scanId: previousScanId, - }); - return result; -}; - -export const processBatches = async ( - batches: string[][], - scanOptions: ScanInputOptions, -): Promise => { - const totalPages = batches.length; - const results: InsightsEolScanResult[] = []; - - for (const [index, batch] of batches.entries()) { - const previousScanId = results[index - 1]?.scanId; - const result = await processBatch({ - batch, - index, - totalPages, - scanOptions, - previousScanId, - }); - results.push(result); - } - - return results; -}; - -export const handleBatchResults = (results: InsightsEolScanResult[]): ScanResult => { - if (results.length === 0) { - throw new Error('No results to process'); - } - // The API returns placeholders for each batch except the last one. - const finalResult = results[results.length - 1]; - return buildScanResult(finalResult); -}; - -export const buildInsightsEolScanInput = (purls: string[], options: ScanInputOptions): InsightsEolScanInput => { - const { type, page, totalPages } = options; - - return { - components: purls, - type, - page, - totalPages, - } satisfies InsightsEolScanInput; -}; diff --git a/src/api/queries/nes/sbom.ts b/src/api/queries/nes/sbom.ts index bc4209f8..58b56119 100644 --- a/src/api/queries/nes/sbom.ts +++ b/src/api/queries/nes/sbom.ts @@ -2,36 +2,25 @@ import { gql } from '@apollo/client/core/core.cjs'; export const M_SCAN = { gql: gql` - mutation EolScan($input: InsightsEolScanInput!) { - insights { - scan { - eol(input: $input) { + mutation createReport($input: CreateEolReportInput) { + eol { + createReport(input: $input) { + success + report { + createdOn + id + metadata components { purl - info { - isEol - isUnsafe - eolAt - daysEol - status - vulnCount - } - remediation { - id + metadata + nesRemediation { + remediations { + urls { + main + } + } } } - diagnostics - message - scanId - createdOn - success - warnings { - purl - type - message - error - diagnostics - } } } } diff --git a/src/api/queries/nes/telemetry.ts b/src/api/queries/nes/telemetry.ts deleted file mode 100644 index 044ef7d8..00000000 --- a/src/api/queries/nes/telemetry.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { gql } from '@apollo/client/core/core.cjs'; - -export const TELEMETRY_INITIALIZE_MUTATION = gql` - mutation Telemetry($clientName: String!) { - telemetry { - initialize(input: { context: { client: { id: $clientName } } }) { - success - oid - message - } - } - } -`; - -export const TELEMETRY_REPORT_MUTATION = gql` - mutation Report($key: String!, $report: JSON!, $metadata: JSON) { - telemetry { - report(input: { key: $key, report: $report, metadata: $metadata }) { - txId - success - message - diagnostics - } - } - } -`; diff --git a/src/api/types/hd-cli.types.ts b/src/api/types/hd-cli.types.ts deleted file mode 100644 index 9ebf19d8..00000000 --- a/src/api/types/hd-cli.types.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { type ComponentStatus, type InsightsEolScanComponent, type ScanWarning, VALID_STATUSES } from './nes.types.ts'; - -export const isValidComponentStatus = (status: string): status is ComponentStatus => { - return VALID_STATUSES.includes(status as ComponentStatus); -}; - -export interface ScanInputOptions { - type: 'SBOM' | 'OTHER'; - page: number; - totalPages: number; - scanId?: string; -} - -export const DEFAULT_SCAN_BATCH_SIZE = 1000; - -export const DEFAULT_SCAN_INPUT_OPTIONS: ScanInputOptions = { - type: 'SBOM', - page: 1, - totalPages: 1, -} satisfies ScanInputOptions; - -export type ScanInput = { - components: string[]; - options: ScanInputOptions; -}; -export interface ScanResult { - components: Map; - createdOn?: string; - diagnostics?: Record; - message: string; - success: boolean; - warnings: ScanWarning[]; - scanId: string | undefined; -} - -export interface ProcessBatchOptions { - batch: string[]; - index: number; - totalPages: number; - scanOptions: ScanInputOptions; - previousScanId?: string; -} diff --git a/src/api/types/nes.types.ts b/src/api/types/nes.types.ts deleted file mode 100644 index 1a29bd30..00000000 --- a/src/api/types/nes.types.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Input parameters for the EOL scan operation - */ -export interface InsightsEolScanInput { - scanId?: string; - /** Array of package URLs in purl format to scan */ - components: string[]; - /** The type of scan being performed (e.g. 'SBOM') */ - type: string; - - // if it's chunked - page: number; - totalPages: number; -} - -export interface ScanResponse { - insights: { - scan: { - eol: InsightsEolScanResult; - }; - }; -} - -/** - * Result of the EOL scan operation - */ -export interface InsightsEolScanResult { - scanId?: string; - createdOn: string; - success: boolean; - message: string; - components: InsightsEolScanComponent[]; - warnings: ScanWarning[]; -} - -/** - * Information about a component's EOL status - */ -export interface InsightsEolScanComponentInfo { - isEol: boolean; - isUnsafe: boolean; - eolAt: Date | null; - status: ComponentStatus; - daysEol: number | null; - vulnCount: number | null; - nesAvailable?: boolean; -} - -export interface InsightsEolScanComponent { - info: InsightsEolScanComponentInfo; - purl: string; - remediation?: { id: string } | null; -} - -export interface ScanWarning { - purl: string; - message: string; - type?: string; - error?: unknown; - diagnostics?: Record; -} - -export type ComponentStatus = (typeof VALID_STATUSES)[number]; -export const VALID_STATUSES = ['UNKNOWN', 'OK', 'EOL', 'EOL_UPCOMING'] as const; diff --git a/src/commands/scan/eol.ts b/src/commands/scan/eol.ts index 8b05d822..e628e04c 100644 --- a/src/commands/scan/eol.ts +++ b/src/commands/scan/eol.ts @@ -1,16 +1,16 @@ import fs from 'node:fs'; import path from 'node:path'; +import { deriveComponentStatus, trimCdxBom } from '@herodevs/eol-shared'; +import type { CdxBom, ComponentStatus, EolReport } from '@herodevs/eol-shared'; import { Command, Flags, ux } from '@oclif/core'; import ora from 'ora'; import terminalLink from 'terminal-link'; -import { batchSubmitPurls } from '../../api/nes/nes.client.ts'; -import type { ScanResult } from '../../api/types/hd-cli.types.js'; -import type { ComponentStatus, InsightsEolScanComponent } from '../../api/types/nes.types.ts'; +import { submitPurls, submitScan } from '../../api/nes/nes.client.ts'; import { config, filenamePrefix } from '../../config/constants.ts'; import type { Sbom } from '../../service/eol/cdx.svc.ts'; import { getErrorMessage, isErrnoException } from '../../service/error.svc.ts'; -import { extractPurls, parsePurlsFile } from '../../service/purls.svc.ts'; -import { INDICATORS, SCAN_ID_KEY, STATUS_COLORS } from '../../ui/shared.ui.ts'; +import { parsePurlsFile } from '../../service/purls.svc.ts'; +import { INDICATORS, STATUS_COLORS } from '../../ui/shared.ui.ts'; import ScanSbom from './sbom.ts'; export default class ScanEol extends Command { @@ -42,23 +42,22 @@ export default class ScanEol extends Command { }), }; - public async run(): Promise<{ components: InsightsEolScanComponent[]; createdOn: string }> { + public async run(): Promise { const { flags } = await this.parse(ScanEol); const scan = await this.getScan(flags, this.config); - const components = Array.from(scan.components.values()); ux.action.stop(); if (flags.save) { - await this.saveReport(components, scan.createdOn); + await this.saveReport(scan); } if (!this.jsonEnabled()) { - this.displayResults(components); + this.displayResults(scan); - if (scan.scanId) { - this.printWebReportUrl(scan.scanId); + if (scan.id) { + this.printWebReportUrl(scan.id); } this.log('* Use --json to output the report payload'); @@ -66,10 +65,10 @@ export default class ScanEol extends Command { this.log('* Use --help for more commands or options'); } - return { components, createdOn: scan.createdOn ?? '' }; + return scan; } - private async getScan(flags: Record, config: Command['config']): Promise { + private async getScan(flags: Record, config: Command['config']): Promise { if (flags.purls) { const purls = this.getPurlsFromFile(flags.purls); return this.scanPurls(purls); @@ -92,9 +91,8 @@ export default class ScanEol extends Command { } } - private printWebReportUrl(scanId: string): void { + private printWebReportUrl(id: string): void { this.log(ux.colorize('bold', '-'.repeat(40))); - const id = scanId.split(SCAN_ID_KEY)[1]; const reportCardUrl = config.eolReportUrl; const url = ux.colorize( 'blue', @@ -103,19 +101,22 @@ export default class ScanEol extends Command { this.log(`🌐 View your full EOL report at: ${url}\n`); } - private async scanSbom(sbom: Sbom): Promise { + private async scanSbom(sbom: Sbom): Promise { + const spinner = ora().start('Scanning for EOL packages'); try { - const purls = await extractPurls(sbom); - return this.scanPurls(purls); + const scan = await submitScan({ sbom: trimCdxBom(sbom as CdxBom) }); + spinner.succeed('Scan completed'); + return scan; } catch (error) { - this.error(`Failed to extract purls from sbom. ${getErrorMessage(error)}`); + spinner.fail('Scanning failed'); + this.error(`Failed to submit scan to NES. ${getErrorMessage(error)}`); } } - private async scanPurls(purls: string[]): Promise { + private async scanPurls(purls: string[]): Promise { const spinner = ora().start('Scanning for EOL packages'); try { - const scan = await batchSubmitPurls(purls); + const scan = await submitPurls(purls); spinner.succeed('Scan completed'); return scan; } catch (error) { @@ -124,12 +125,12 @@ export default class ScanEol extends Command { } } - private async saveReport(components: InsightsEolScanComponent[], createdOn?: string): Promise { + private async saveReport(report: EolReport): Promise { const { flags } = await this.parse(ScanEol); const reportPath = path.join(flags.dir || process.cwd(), `${filenamePrefix}.report.json`); try { - fs.writeFileSync(reportPath, JSON.stringify({ components, createdOn }, null, 2)); + fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); this.log(`Report saved to ${filenamePrefix}.report.json`); } catch (error) { if (!isErrnoException(error)) { @@ -145,8 +146,8 @@ export default class ScanEol extends Command { } } - private displayResults(components: InsightsEolScanComponent[]) { - const { UNKNOWN, OK, EOL_UPCOMING, EOL, NES_AVAILABLE } = countComponentsByStatus(components); + private displayResults(report: EolReport) { + const { UNKNOWN, OK, EOL_UPCOMING, EOL, NES_AVAILABLE } = countComponentsByStatus(report); if (!UNKNOWN && !OK && !EOL_UPCOMING && !EOL) { this.log(ux.colorize('yellow', 'No components found in scan.')); @@ -155,7 +156,7 @@ export default class ScanEol extends Command { this.log(ux.colorize('bold', 'Scan results:')); this.log(ux.colorize('bold', '-'.repeat(40))); - this.log(ux.colorize('bold', `${components.length.toLocaleString()} total packages scanned`)); + this.log(ux.colorize('bold', `${report.components.length.toLocaleString()} total packages scanned`)); this.log(ux.colorize(STATUS_COLORS.EOL, `${INDICATORS.EOL} ${EOL.toLocaleString().padEnd(5)} End-of-Life (EOL)`)); this.log( ux.colorize( @@ -176,9 +177,7 @@ export default class ScanEol extends Command { } } -export function countComponentsByStatus( - components: InsightsEolScanComponent[], -): Record { +export function countComponentsByStatus(report: EolReport): Record { const grouped: Record = { UNKNOWN: 0, OK: 0, @@ -187,10 +186,11 @@ export function countComponentsByStatus( NES_AVAILABLE: 0, }; - for (const component of components) { - grouped[component.info.status]++; + for (const component of report.components) { + const status = deriveComponentStatus(component.metadata); + grouped[status]++; - if (component.info.nesAvailable) { + if (component.nesRemediation?.remediations?.length) { grouped.NES_AVAILABLE++; } } diff --git a/src/service/nes/nes.svc.ts b/src/service/nes/nes.svc.ts index 48703c81..42dba399 100644 --- a/src/service/nes/nes.svc.ts +++ b/src/service/nes/nes.svc.ts @@ -1,54 +1,20 @@ +import type { CdxBom, CreateEolReportInput, EolReport, EolReportMutationResponse } from '@herodevs/eol-shared'; import type { NesApolloClient } from '../../api/nes/nes.client.ts'; import { M_SCAN } from '../../api/queries/nes/sbom.ts'; -import type { ScanInputOptions, ScanResult } from '../../api/types/hd-cli.types.ts'; -import type { - ComponentStatus, - InsightsEolScanComponent, - InsightsEolScanInput, - InsightsEolScanResult, - ScanResponse, -} from '../../api/types/nes.types.ts'; import { debugLogger } from '../log.svc.ts'; -export const buildScanResult = (scan: InsightsEolScanResult): ScanResult => { - const components = new Map(); - for (const c of scan.components) { - const status = c.info.status as ComponentStatus | 'SUPPORTED'; - components.set(c.purl, { - info: { - ...c.info, - nesAvailable: c.remediation !== null, - status: status === 'SUPPORTED' ? 'EOL_UPCOMING' : status, - }, - purl: c.purl, - }); - } - - return { - components, - message: scan.message, - success: true, - warnings: scan.warnings || [], - scanId: scan.scanId, - createdOn: scan.createdOn, - }; -}; - export const SbomScanner = (client: NesApolloClient) => - async (purls: string[], options: ScanInputOptions): Promise => { - const { type, page, totalPages, scanId } = options; - const input: InsightsEolScanInput = { components: purls, type, page, totalPages, scanId }; - - const res = await client.mutate(M_SCAN.gql, { input }); + async (input: CreateEolReportInput): Promise => { + const res = await client.mutate(M_SCAN.gql, { input }); - const scan = res.data?.insights?.scan?.eol; - if (!scan?.success) { - debugLogger('failed scan %o', scan || {}); + const result = res.data?.eol?.createReport; + if (!result?.success || !result.report) { + debugLogger('failed scan %o', result || {}); debugLogger('scan failed'); - throw new Error('Failed to provide scan: '); + throw new Error('Failed to create EOL report'); } - return scan; + return result.report; }; diff --git a/src/ui/shared.ui.ts b/src/ui/shared.ui.ts index f49e5468..79d5ef72 100644 --- a/src/ui/shared.ui.ts +++ b/src/ui/shared.ui.ts @@ -1,5 +1,5 @@ +import type { ComponentStatus } from '@herodevs/eol-shared'; import { ux } from '@oclif/core'; -import type { ComponentStatus } from '../api/types/nes.types.ts'; export const STATUS_COLORS: Record = { EOL: 'red', @@ -14,5 +14,3 @@ export const INDICATORS: Record = { OK: ux.colorize(STATUS_COLORS.OK, '✔'), EOL_UPCOMING: ux.colorize(STATUS_COLORS.EOL_UPCOMING, '⚡'), }; - -export const SCAN_ID_KEY = 'eol-scan-v1-'; diff --git a/test/api/nes.client.test.ts b/test/api/nes.client.test.ts index d6b3eca5..bd1e8619 100644 --- a/test/api/nes.client.test.ts +++ b/test/api/nes.client.test.ts @@ -1,40 +1,8 @@ import assert from 'node:assert'; import { describe, it } from 'node:test'; -import { createBatches, dedupeAndEncodePurls } from '../../src/api/nes/nes.client.ts'; -import { DEFAULT_SCAN_BATCH_SIZE } from '../../src/api/types/hd-cli.types.ts'; +import { dedupeAndEncodePurls } from '../../src/api/nes/nes.client.ts'; describe('nes.client', () => { - describe('createBatches', () => { - it('should handle empty array', () => { - const result = createBatches([], DEFAULT_SCAN_BATCH_SIZE); - assert.deepStrictEqual(result, []); - }); - - it('should create single batch when items length is less than batch size', () => { - const items = ['a', 'b', 'c']; - const result = createBatches(items, 5); - assert.deepStrictEqual(result, [['a', 'b', 'c']]); - }); - - it('should create single batch when items length equals batch size', () => { - const items = ['a', 'b', 'c', 'd', 'e']; - const result = createBatches(items, 5); - assert.deepStrictEqual(result, [['a', 'b', 'c', 'd', 'e']]); - }); - - it('should create multiple batches when items length exceeds batch size', () => { - const items = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; - const result = createBatches(items, 3); - assert.deepStrictEqual(result, [['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i'], ['j']]); - }); - - it('should handle batch size of 1', () => { - const items = ['a', 'b', 'c']; - const result = createBatches(items, 1); - assert.deepStrictEqual(result, [['a'], ['b'], ['c']]); - }); - }); - describe('dedupeAndEncodePurls', () => { const inputs = [ { diff --git a/test/utils/mocks/scan-result-component.mock.ts b/test/utils/mocks/scan-result-component.mock.ts deleted file mode 100644 index f0edda6e..00000000 --- a/test/utils/mocks/scan-result-component.mock.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ScanResult } from '../../../src/api/types/hd-cli.types.ts'; -import type { ComponentStatus, InsightsEolScanComponent } from '../../../src/api/types/nes.types.ts'; - -export const createMockComponent = ( - purl: string, - status: ComponentStatus = 'OK', - eolAt: Date | null = null, - daysEol: number | null = null, - vulnCount = 0, -): InsightsEolScanComponent => ({ - purl, - info: { - eolAt, - isEol: status === 'EOL', - isUnsafe: false, - status, - daysEol, - vulnCount, - }, -}); - -export const createMockScan = (components: InsightsEolScanComponent[]): ScanResult => ({ - components: new Map(components.map((c) => [c.purl, c])), - message: 'Test scan', - success: true, - warnings: [], -});