diff --git a/src/cmds/generate.ts b/src/cmds/generate.ts index 7d0b21c..a0f3e33 100644 --- a/src/cmds/generate.ts +++ b/src/cmds/generate.ts @@ -1,5 +1,7 @@ import * as debugLib from 'debug'; import * as pathLib from 'path'; +import * as _ from 'lodash'; + import { getApiToken } from '../lib/get-api-token'; import { LicenseReportData, @@ -46,6 +48,10 @@ export const builder = { desc: 'How should the data be represented. Defaults to a license based view.', }, + project: { + default: [], + desc: 'Project ID to filter the results by. E.g. --project=uuid --project=uuid2', + }, }; export const aliases = ['g']; @@ -54,16 +60,29 @@ export async function handler(argv: { outputFormat: OutputFormat; template: string; view: SupportedViews; + project?: string | string[]; }) { try { - const { orgPublicId, outputFormat, template, view } = argv; + const { orgPublicId, outputFormat, template, view, project } = argv; debug( 'ℹ️ Options: ' + - JSON.stringify({ orgPublicId, outputFormat, template, view }), + JSON.stringify({ + orgPublicId, + outputFormat, + template, + view, + project: _.castArray(project), + }), ); getApiToken(); + const options = { + filters: { + projects: _.castArray(project), + }, + }; const licenseData: LicenseReportData = await generateLicenseData( orgPublicId, + options, ); const orgData = await getOrgData(orgPublicId); const reportData = await generateHtmlReport( @@ -80,7 +99,10 @@ export async function handler(argv: { )}.${outputFormat}`; await generateReportFunc(reportFileName, reportData); console.log( - `${outputFormat.toUpperCase()} license report saved at: ${pathLib.resolve(process.cwd(), reportFileName)}`, + `${outputFormat.toUpperCase()} license report saved at: ${pathLib.resolve( + __dirname, + reportFileName, + )}`, ); } catch (e) { console.error(e); diff --git a/src/cmds/json.ts b/src/cmds/json.ts index cb01982..27010a3 100644 --- a/src/cmds/json.ts +++ b/src/cmds/json.ts @@ -3,6 +3,7 @@ import * as debugLib from 'debug'; import { getApiToken } from '../lib/get-api-token'; import { generateLicenseData } from '../lib/generate-org-license-report'; import { SupportedViews } from '../lib/types'; +import * as _ from 'lodash'; const debug = debugLib('snyk-licenses:json'); @@ -12,22 +13,33 @@ export const builder = { orgPublicId: { required: true, default: undefined, - } + }, + project: { + default: [], + desc: + 'Project ID to filter the results by. E.g. --project=uuid --project=uuid2', + }, }; export const aliases = ['j']; export async function handler(argv: { orgPublicId: string; view: SupportedViews; + project?: string | string[]; }) { try { - const { orgPublicId, view } = argv; - debug('ℹ️ Options: ' + JSON.stringify({ orgPublicId, view })); - // check SNYK_TOKEN is set as the sdk uses it + const { orgPublicId, view, project } = argv; + debug( + 'ℹ️ Options: ' + + JSON.stringify({ orgPublicId, view, project: _.castArray(project) }), + ); getApiToken(); - // TODO: define and pass options to help filter the response - // based on filters available in API - const data = await generateLicenseData(orgPublicId, {}); + const options = { + filters: { + projects: _.castArray(project), + }, + }; + const data = await generateLicenseData(orgPublicId, options); console.log(JSON.stringify(data)); } catch (e) { console.error(e); diff --git a/src/lib/api/org/dependencies.ts b/src/lib/api/org/dependencies.ts index ecf4da2..288e8cb 100644 --- a/src/lib/api/org/dependencies.ts +++ b/src/lib/api/org/dependencies.ts @@ -2,33 +2,23 @@ import 'source-map-support/register'; import * as debugLib from 'debug'; import * as snykApiSdk from 'snyk-api-ts-client'; import { getApiToken } from '../../get-api-token'; -import { SortBy, Order } from './types'; +import { GetLicenseDataOptions } from '../../types'; const debug = debugLib('snyk-licenses:getDependenciesDataForOrg'); -interface GetDependenciesDataOptions { - sortBy: SortBy; - order: Order; - filters?: snykApiSdk.OrgTypes.DependenciesPostBodyType; -} - export async function getDependenciesDataForOrg( orgPublicId: string, - options?: GetDependenciesDataOptions, + options?: GetLicenseDataOptions, ): Promise { getApiToken(); const snykApiClient = await new snykApiSdk.Org({ orgId: orgPublicId }); - const sortBy = options?.sortBy; - const order = options?.order; const body: snykApiSdk.OrgTypes.DependenciesPostBodyType = { - ...options?.filters, + filters: options?.filters, }; try { const dependenciesData = await getAllDependenciesData( snykApiClient, body, - sortBy, - order, ); return dependenciesData; } catch (e) { @@ -40,15 +30,11 @@ export async function getDependenciesDataForOrg( async function getAllDependenciesData( snykApiClient, body, - sortBy, - order, page = 1, ): Promise { const perPage = 200; const dependenciesData = await snykApiClient.dependencies.post( body, - sortBy, - order, page, perPage, ); @@ -58,8 +44,6 @@ async function getAllDependenciesData( const data = await getAllDependenciesData( snykApiClient, body, - sortBy, - order, nextPage, ); result.results = [...result.results, ...data.results]; diff --git a/src/lib/api/org/licenses.ts b/src/lib/api/org/licenses.ts index 9e747a7..21c5122 100644 --- a/src/lib/api/org/licenses.ts +++ b/src/lib/api/org/licenses.ts @@ -2,29 +2,21 @@ import 'source-map-support/register'; import * as debugLib from 'debug'; import * as snykApiSdk from 'snyk-api-ts-client'; import { getApiToken } from '../../get-api-token'; -import { SortBy, Order } from './types'; +import { GetLicenseDataOptions } from '../../types'; const debug = debugLib('snyk-licenses:getLicenseDataForOrg'); -interface GetLicenseDataOptions { - sortBy: SortBy; - order: Order; - filters?: snykApiSdk.OrgTypes.LicensesPostBodyType; -} - export async function getLicenseDataForOrg( orgPublicId: string, options?: GetLicenseDataOptions, ): Promise { getApiToken(); const snykApiClient = await new snykApiSdk.Org({ orgId: orgPublicId }); - const sortBy = options?.sortBy; - const order = options?.order; const body: snykApiSdk.OrgTypes.LicensesPostBodyType = { - ...options?.filters, + filters: options?.filters, }; try { - const licenseData = await getAllLicensesData(snykApiClient, body, sortBy, order); + const licenseData = await getAllLicensesData(snykApiClient, body); return licenseData; } catch (e) { debug('❌ Failed to fetch licenses' + e); @@ -35,15 +27,11 @@ export async function getLicenseDataForOrg( async function getAllLicensesData( snykApiClient, body, - sortBy, - order, page = 1, ): Promise { const perPage = 200; const licensesData = await snykApiClient.licenses.post( body, - sortBy, - order, page, perPage, ); @@ -53,8 +41,6 @@ async function getAllLicensesData( const data = await getAllLicensesData( snykApiClient, body, - sortBy, - order, nextPage, ); result.results.push(data.results); diff --git a/src/lib/generate-org-license-report.ts b/src/lib/generate-org-license-report.ts index 3ccaa61..7d4927d 100644 --- a/src/lib/generate-org-license-report.ts +++ b/src/lib/generate-org-license-report.ts @@ -4,8 +4,16 @@ import * as _ from 'lodash'; export * from './license-text'; export * from './get-api-token'; import { getLicenseDataForOrg, getDependenciesDataForOrg } from './api/org'; -import { fetchSpdxLicenseTextAndUrl, fetchNonSpdxLicenseTextAndUrl } from './license-text'; -import { LicenseReportDataEntry, EnrichedDependency, Dependency, DependencyData } from './types'; +import { + fetchSpdxLicenseTextAndUrl, + fetchNonSpdxLicenseTextAndUrl, +} from './license-text'; +import { + LicenseReportDataEntry, + EnrichedDependency, + Dependency, + DependencyData, +} from './types'; const debug = debugLib('snyk-licenses:generateOrgLicensesReport'); @@ -15,7 +23,11 @@ export interface LicenseReportData { export async function generateLicenseData( orgPublicId: string, - options?, + options?: { + filters?: { + projects: string[]; + }; + }, ): Promise { debug(`ℹ️ Generating license data for Org:${orgPublicId}`); @@ -37,9 +49,10 @@ export async function generateLicenseData( debug(`⏳ Processing ${licenseData.total} licenses`); const dependenciesAll = []; + for (const license of licenseData.results) { const dependencies = license.dependencies; - if(!dependencies.length) { + if (!dependencies.length) { continue; } dependenciesAll.push(...dependencies); @@ -50,9 +63,7 @@ export async function generateLicenseData( if (dependenciesEnriched.length) { license.dependencies = dependenciesEnriched; } - const licenseData = await getLicenseTextAndUrl( - license.id, - ); + const licenseData = await getLicenseTextAndUrl(license.id); licenseReportData[license.id] = { ...(license as any), licenseText: licenseData?.licenseText, @@ -68,12 +79,12 @@ export async function generateLicenseData( } } - function enrichDependencies( dependencies: Dependency[], dependenciesData, ): EnrichedDependency[] { - const enrichDependencies: EnrichedDependency [] = []; + const enrichDependencies: EnrichedDependency[] = []; + for (const dependency of dependencies) { const dep: DependencyData[] = dependenciesData[dependency.id]; if (dep && dep[0]) { @@ -82,7 +93,7 @@ function enrichDependencies( ...dep[0], }); } else { - debug('Dep information not found for ' + dependency.id); + debug('Dep information not available from /dependencies API response for ' + dependency.id); } } diff --git a/src/lib/types.ts b/src/lib/types.ts index 0a3a1c7..0bd471e 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -5,6 +5,7 @@ export interface Dependency { packageManager: string; } +export type LicenseSeverity = 'none' | 'high' | 'medium' | 'low'; export type EnrichedDependency = Dependency & DependencyData; export interface LicenseReportDataEntry { @@ -24,7 +25,7 @@ export interface LicenseReportDataEntry { /** * Snyk license severity setup on the org license policy */ - severity?: 'none' | 'high' | 'medium' | 'low'; + severity?: LicenseSeverity; /** * Snyk license instruction setup on the org license policy */ @@ -129,8 +130,24 @@ export interface DependencyData { } []; - export const enum SupportedViews { ORG_LICENSES = 'org-licenses', PROJECT_DEPENDENCIES = 'project-dependencies', } +export interface GetLicenseDataOptions { + filters?: { + projects?: string[]; + /** + * The list of dependency IDs to filter the results by + */ + dependencies?: string[]; + /** + * The list of license IDs to filter the results by + */ + licenses?: string[]; + /** + * The severities to filter the results by + */ + severity?: LicenseSeverity[]; + }; +} diff --git a/test/lib/generate-license-report-data.test.ts b/test/lib/generate-license-report-data.test.ts index 6322b0f..09459e2 100644 --- a/test/lib/generate-license-report-data.test.ts +++ b/test/lib/generate-license-report-data.test.ts @@ -4,6 +4,7 @@ describe('Get org licenses', () => { const OLD_ENV = process.env; process.env.SNYK_TOKEN = process.env.SNYK_TEST_TOKEN; const ORG_ID = process.env.TEST_ORG_ID as string; + const PROJECT_ID = process.env.TEST_PROJECT_ID as string; afterAll(async () => { process.env = { ...OLD_ENV }; @@ -12,18 +13,59 @@ describe('Get org licenses', () => { expect(process.env.SNYK_TOKEN).not.toBeNull(); expect(process.env.ORG_ID).not.toBeNull(); }); + test('License data is generated as expected', async () => { const licenseRes = await generateLicenseData(ORG_ID, {}); expect(Object.keys(licenseRes).length >= 11).toBeTruthy(); expect(licenseRes['Unknown'].licenseUrl).toBeUndefined(); expect(licenseRes['Unknown'].licenseText).toBeUndefined(); expect(licenseRes['Unlicense'].licenseText).not.toBeNull(); - expect(licenseRes['Unlicense'].licenseUrl).toBe('https://spdx.org/licenses/Unlicense.html'); - expect(licenseRes['Unlicense'].licenseUrl).toBe('https://spdx.org/licenses/Unlicense.html'); + expect(licenseRes['Unlicense'].licenseUrl).toBe( + 'https://spdx.org/licenses/Unlicense.html', + ); + expect(licenseRes['Unlicense'].licenseUrl).toBe( + 'https://spdx.org/licenses/Unlicense.html', + ); expect(licenseRes['Unlicense'].dependencies[0].copyright).not.toBeNull(); expect(licenseRes['Unlicense'].dependencies[0].issuesMedium).not.toBeNull(); - expect(licenseRes['Unlicense'].dependencies[0].latestVersion).not.toBeNull(); - }, 50000); + expect( + licenseRes['Unlicense'].dependencies[0].latestVersion, + ).not.toBeNull(); + + expect(licenseRes['Zlib'].projects.length).toEqual(2); + expect(licenseRes['ISC'].projects.length).toEqual(3); + expect(licenseRes['ISC'].dependencies[0].copyright).toEqual( + ['Copyright (c) Isaac Z. Schlueter and Contributors'], + ); + }, 70000); + + test('License data is generated as expected', async () => { + const licenseRes = await generateLicenseData(ORG_ID, { + filters: { + projects: [PROJECT_ID], + }, + }); + expect(Object.keys(licenseRes).length >= 11).toBeTruthy(); + expect(licenseRes['Unknown']).toBeUndefined(); + expect(licenseRes['Unlicense'].licenseText).not.toBeNull(); + expect(licenseRes['Unlicense'].licenseUrl).toBe( + 'https://spdx.org/licenses/Unlicense.html', + ); + expect(licenseRes['Unlicense'].licenseUrl).toBe( + 'https://spdx.org/licenses/Unlicense.html', + ); + expect(licenseRes['Unlicense'].dependencies[0].copyright).not.toBeNull(); + expect(licenseRes['Unlicense'].dependencies[0].issuesMedium).not.toBeNull(); + expect( + licenseRes['Unlicense'].dependencies[0].latestVersion, + ).not.toBeNull(); + + expect(licenseRes['Zlib']).toBeUndefined(); + expect(licenseRes['ISC'].projects.length).toEqual(1); + expect(licenseRes['ISC'].dependencies[0].copyright).toEqual([ + 'Copyright (c) Isaac Z. Schlueter and Contributors', + ]); + }, 70000); test.todo('Test for when API fails aka bad org id provided'); }); diff --git a/test/system/__snapshots__/generate.test.ts.snap b/test/system/__snapshots__/generate.test.ts.snap index 246fca7..b6af730 100644 --- a/test/system/__snapshots__/generate.test.ts.snap +++ b/test/system/__snapshots__/generate.test.ts.snap @@ -16,6 +16,8 @@ Options: --view How should the data be represented. Defaults to a license based view. [choices: \\"org-licenses\\", \\"project-dependencies\\"] [default: \\"org-licenses\\"] + --project Project ID to filter the results by. E.g. --project=uuid + --project=uuid2 [default: []] Missing required argument: orgPublicId" `; diff --git a/test/system/__snapshots__/json.test.ts.snap b/test/system/__snapshots__/json.test.ts.snap index 6270fd1..ce90da9 100644 --- a/test/system/__snapshots__/json.test.ts.snap +++ b/test/system/__snapshots__/json.test.ts.snap @@ -10,6 +10,8 @@ Options: --version Show version number [boolean] --help Show help [boolean] --orgPublicId [required] + --project Project ID to filter the results by. E.g. --project=uuid + --project=uuid2 [default: []] Missing required argument: orgPublicId" `; diff --git a/test/system/generate.test.ts b/test/system/generate.test.ts index 88e69b5..8604085 100644 --- a/test/system/generate.test.ts +++ b/test/system/generate.test.ts @@ -8,7 +8,12 @@ describe('`snyk-licenses-report generate <...>`', () => { it('Shows error when missing --orgPublicId', async (done) => { exec( `node ${main} generate`, - { env: { SNYK_TOKEN: process.env.SNYK_TEST_TOKEN } }, + { + env: { + PATH: process.env.PATH, + SNYK_TOKEN: process.env.SNYK_TEST_TOKEN, + }, + }, (err, stdout) => { expect(stdout).toBe(''); expect(err.message.trim()).toMatchSnapshot(); @@ -20,7 +25,12 @@ describe('`snyk-licenses-report generate <...>`', () => { it('generated the report successfully with default params', (done) => { exec( `node ${main} generate --orgPublicId=${ORG_ID}`, - { env: { SNYK_TOKEN: process.env.SNYK_TEST_TOKEN } }, + { + env: { + PATH: process.env.PATH, + SNYK_TOKEN: process.env.SNYK_TEST_TOKEN, + }, + }, (err, stdout) => { expect(err).toBeNull(); expect(stdout).toMatch('HTML license report saved at'); @@ -32,7 +42,12 @@ describe('`snyk-licenses-report generate <...>`', () => { it.skip('generated the report successfully as PDF', (done) => { exec( `node ${main} generate --orgPublicId=${ORG_ID} --outputFormat=pdf`, - { env: { SNYK_TOKEN: process.env.SNYK_TEST_TOKEN } }, + { + env: { + PATH: process.env.PATH, + SNYK_TOKEN: process.env.SNYK_TEST_TOKEN, + }, + }, (err, stdout) => { expect(err).toBeNull(); expect(stdout).toMatch('PDF license report saved at'); @@ -42,8 +57,14 @@ describe('`snyk-licenses-report generate <...>`', () => { }, 50000); it('generated the report successfully with custom template', (done) => { exec( - `node ${main} generate --orgPublicId=${ORG_ID} --template=${__dirname + '/fixtures/custom-view.hbs'}`, - { env: { SNYK_TOKEN: process.env.SNYK_TEST_TOKEN } }, + `node ${main} generate --orgPublicId=${ORG_ID} --template=${__dirname + + '/fixtures/custom-view.hbs'}`, + { + env: { + PATH: process.env.PATH, + SNYK_TOKEN: process.env.SNYK_TEST_TOKEN, + }, + }, (err, stdout) => { expect(err).toBeNull(); expect(stdout).toMatch('HTML license report saved at'); diff --git a/test/system/json.test.ts b/test/system/json.test.ts index 86851ab..acdff8a 100644 --- a/test/system/json.test.ts +++ b/test/system/json.test.ts @@ -2,27 +2,55 @@ import { exec } from 'child_process'; import { sep } from 'path'; const main = './dist/index.js'.replace(/\//g, sep); const ORG_ID = process.env.TEST_ORG_ID as string; +const PROJECT_ID = process.env.TEST_PROJECT_ID as string; + describe('`snyk-licenses-report json <...>`', () => { it('Shows error when missing --orgPublicId', async (done) => { exec( `node ${main} json`, - { env: { SNYK_TOKEN: process.env.SNYK_TEST_TOKEN } }, + { + env: { + PATH: process.env.PATH, + SNYK_TOKEN: process.env.SNYK_TEST_TOKEN, + }, + }, (err, stdout) => { expect(stdout).toBe(''); expect(err.message.trim()).toMatchSnapshot(); done(); }, ); - }); + }, 70000); it('Generated JSON data with correct --orgPublicId', async (done) => { exec( `node ${main} json --orgPublicId=${ORG_ID}`, - { env: { SNYK_TOKEN: process.env.SNYK_TEST_TOKEN } }, - (err, stdout) => { + { + env: { + PATH: process.env.PATH, + SNYK_TOKEN: process.env.SNYK_TEST_TOKEN, + }, + }, + async (err, stdout) => { + expect(err).toBeNull(); + expect(stdout).not.toBeNull(); + done(); + }, + ); + }, 70000); + it('Generated JSON data with correct --orgPublicId --project', async (done) => { + exec( + `node ${main} json --orgPublicId=${ORG_ID} --project=${PROJECT_ID}}`, + { + env: { + PATH: process.env.PATH, + SNYK_TOKEN: process.env.SNYK_TEST_TOKEN, + }, + }, + async (err, stdout) => { expect(err).toBeNull(); - expect(stdout.trim()).toMatch('BSD-2-Clause'); + expect(stdout).not.toBeNull(); done(); }, ); - }, 30000); + }, 70000); });