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
37 changes: 31 additions & 6 deletions src/lib/plugins/get-deps-from-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,37 @@ export async function getDepsFromPlugin(
if (!options.docker && !(options.file || options.packageManager)) {
throw NoSupportedManifestsFoundError([...root]);
}
const inspectRes = await getSinglePluginResult(
root,
options,
'',
featureFlags,
);
let inspectRes;
try {
inspectRes = await getSinglePluginResult(
root,
options,
'',
featureFlags,
);
} catch (error) {
if (options['print-effective-graph-with-errors']) {
const errMessage =
error?.message ?? 'Something went wrong getting dependencies';
debug(
`Single plugin scan failed for ${options.file}, collecting as failed result: ${errMessage}`,
);
return {
plugin: {
name: options.packageManager || 'unknown',
},
scannedProjects: [],
failedResults: [
{
targetFile: options.file,
error,
errMessage,
},
],
} as MultiProjectResultCustom;
}
throw error;
}

if (!pluginApi.isMultiResult(inspectRes)) {
if (!inspectRes.package && !inspectRes.dependencyGraph) {
Expand Down
14 changes: 13 additions & 1 deletion src/lib/plugins/get-multi-plugin-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
SUPPORTED_MANIFEST_FILES,
SupportedPackageManagers,
} from '../package-managers';
const { SHOW_NPM_SCOPE } = require('../feature-flags');
import { SHOW_NPM_SCOPE } from '../feature-flags';
import { getSinglePluginResult } from './get-single-plugin-result';
import { convertSingleResultToMultiCustom } from './convert-single-splugin-res-to-multi-custom';
import { convertMultiResultToMultiCustom } from './convert-multi-plugin-res-to-multi-custom';
Expand Down Expand Up @@ -182,6 +182,18 @@ export async function getMultiPluginResult(
}

if (!allResults.length) {
// When allow-incomplete-sbom is active, return instead of throwing
// so the caller can print per-project JSONL error entries
if (options['print-effective-graph-with-errors']) {
return {
plugin: {
name: 'custom-auto-detect',
},
scannedProjects: allResults,
failedResults,
};
}

// No projects were scanned successfully
let message = `Failed to get dependencies for all ${targetFiles.length} potential projects.\n`;

Expand Down
8 changes: 6 additions & 2 deletions src/lib/snyk-test/run-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -654,14 +654,18 @@ async function assembleLocalPayloads(
const failedResults = (deps as MultiProjectResultCustom).failedResults;
if (failedResults?.length) {
await spinner.clear<void>(spinnerLbl)();
const isNotJsonOrQueiet = !options.json && !options.quiet;
// When printing effective dep-graph with errors, suppress warning output —
// the failures will be embedded in the generated SBOM as annotations.
const suppressWarnings = shouldPrintEffectiveDepGraphWithErrors(options);
const isNotJsonOrQueiet =
!options.json && !options.quiet && !suppressWarnings;

const errorMessages = extractErrorMessages(
failedResults,
isNotJsonOrQueiet,
);

if (!options.json && !options.quiet) {
if (!options.json && !options.quiet && !suppressWarnings) {
console.warn(
chalk.bold.red(
`${icon.ISSUE} ${failedResults.length}/${
Expand Down
73 changes: 73 additions & 0 deletions test/jest/unit/lib/plugins/get-deps-from-plugin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { getDepsFromPlugin } from '../../../../../src/lib/plugins/get-deps-from-plugin';
import { Options, TestOptions } from '../../../../../src/lib/types';
import * as singlePluginResult from '../../../../../src/lib/plugins/get-single-plugin-result';
import * as detect from '../../../../../src/lib/detect';

jest.mock('../../../../../src/lib/plugins/get-single-plugin-result');
jest.mock('../../../../../src/lib/detect', () => ({
...jest.requireActual('../../../../../src/lib/detect'),
detectPackageFile: jest.fn().mockReturnValue('package.json'),
}));

describe('getDepsFromPlugin - print-effective-graph-with-errors', () => {
beforeEach(() => {
jest.clearAllMocks();
});

const baseOptions: Options & TestOptions = {
path: '/test',
packageManager: 'npm',
file: 'package.json',
showVulnPaths: 'some',
};

it('should return failedResults when plugin throws and flag is set', async () => {
const pluginError = new Error('missing lockfile');
(
singlePluginResult.getSinglePluginResult as jest.Mock
).mockRejectedValue(pluginError);

const options = {
...baseOptions,
'print-effective-graph-with-errors': true,
};

const result = await getDepsFromPlugin('/test', options);

expect(result.scannedProjects).toEqual([]);
expect((result as any).failedResults).toHaveLength(1);
expect((result as any).failedResults[0]).toEqual({
targetFile: 'package.json',
error: pluginError,
errMessage: 'missing lockfile',
});
});

it('should rethrow when plugin throws and flag is not set', async () => {
const pluginError = new Error('missing lockfile');
(
singlePluginResult.getSinglePluginResult as jest.Mock
).mockRejectedValue(pluginError);

await expect(
getDepsFromPlugin('/test', baseOptions),
).rejects.toThrow('missing lockfile');
});

it('should use fallback message when error has no message', async () => {
(
singlePluginResult.getSinglePluginResult as jest.Mock
).mockRejectedValue({ code: 'UNKNOWN' });

const options = {
...baseOptions,
'print-effective-graph-with-errors': true,
};

const result = await getDepsFromPlugin('/test', options);

expect((result as any).failedResults[0].errMessage).toBe(
'Something went wrong getting dependencies',
);
});
});
Loading