diff --git a/src/cli/commands/test/iac/index.ts b/src/cli/commands/test/iac/index.ts index 4147084088..dc9a798265 100644 --- a/src/cli/commands/test/iac/index.ts +++ b/src/cli/commands/test/iac/index.ts @@ -1,28 +1,16 @@ import * as Debug from 'debug'; import { EOL } from 'os'; -import * as cloneDeep from 'lodash.clonedeep'; -import * as assign from 'lodash.assign'; import chalk from 'chalk'; -import { - IacFileInDirectory, - IacOutputMeta, - Options, - TestOptions, -} from '../../../../lib/types'; import { MethodArgs } from '../../../args'; import { TestCommandResult } from '../../types'; -import { - LegacyVulnApiResult, - TestResult, -} from '../../../../lib/snyk-test/legacy'; +import { LegacyVulnApiResult } from '../../../../lib/snyk-test/legacy'; import { mapIacTestResult } from '../../../../lib/snyk-test/iac-test-result'; import { summariseErrorResults, summariseVulnerableResults, } from '../../../../lib/formatters'; -import * as utils from '../utils'; import { failuresTipOutput, formatIacTestFailures, @@ -33,43 +21,30 @@ import { getIacDisplayErrorFileOutput, iacTestTitle, shouldLogUserMessages, - spinnerMessage, spinnerSuccessMessage, IaCTestFailure, } from '../../../../lib/formatters/iac-output'; import { extractDataToSendFromResults } from '../../../../lib/formatters/test/format-test-results'; -import { test as iacTest } from './local-execution'; import { validateCredentials } from '../validate-credentials'; import { validateTestOptions } from '../validate-test-options'; import { setDefaultTestOptions } from '../set-default-test-options'; import { processCommandArgs } from '../../process-command-args'; -import { formatTestError } from '../format-test-error'; import { displayResult } from '../../../../lib/formatters/test/display-result'; -import { - assertIaCOptionsFlags, - isIacShareResultsOptions, -} from './local-execution/assert-iac-options-flag'; +import { isIacShareResultsOptions } from './local-execution/assert-iac-options-flag'; import { hasFeatureFlag } from '../../../../lib/feature-flags'; -import { - buildDefaultOciRegistry, - initRules, -} from './local-execution/rules/rules'; -import { - cleanLocalCache, - getIacOrgSettings, -} from './local-execution/measurable-methods'; +import { buildDefaultOciRegistry } from './local-execution/rules/rules'; +import { getIacOrgSettings } from './local-execution/measurable-methods'; import config from '../../../../lib/config'; import { UnsupportedEntitlementError } from '../../../../lib/errors/unsupported-entitlement-error'; import * as ora from 'ora'; import { CustomError, FormattedCustomError } from '../../../../lib/errors'; +import { scan } from './scan'; const debug = Debug('snyk-test'); const SEPARATOR = '\n-------------------------------------------------------\n'; -// TODO: avoid using `as any` whenever it's possible - // The hardcoded `isReportCommand` argument is temporary and will be removed together with the `snyk iac report` command deprecation export default async function( isReportCommand: boolean, @@ -88,18 +63,10 @@ export default async function( throw new UnsupportedEntitlementError('infrastructureAsCode'); } - const ociRegistryBuilder = () => buildDefaultOciRegistry(iacOrgSettings); + const buildOciRegistry = () => buildDefaultOciRegistry(iacOrgSettings); let testSpinner: ora.Ora | undefined; - const resultOptions: Array = []; - const results = [] as any[]; - - // Holds an array of scanned file metadata for output. - let iacScanFailures: IacFileInDirectory[] = []; - let iacIgnoredIssuesCount = 0; - let iacOutputMeta: IacOutputMeta | undefined; - const isNewIacOutputSupported = config.IAC_OUTPUT_V2 || (await hasFeatureFlag('iacCliOutputRelease', options)); @@ -110,76 +77,25 @@ export default async function( testSpinner = ora({ isSilent: options.quiet, stream: process.stdout }); } - try { - const rulesOrigin = await initRules( - ociRegistryBuilder, - iacOrgSettings, - options, - ); - - testSpinner?.start(spinnerMessage); - - for (const path of paths) { - // Create a copy of the options so a specific test can - // modify them i.e. add `options.file` etc. We'll need - // these options later. - const testOpts = cloneDeep(options); - testOpts.path = path; - testOpts.projectName = testOpts['project-name']; - - let res: (TestResult | TestResult[]) | Error; - try { - assertIaCOptionsFlags(process.argv); - const { results, failures, ignoreCount } = await iacTest( - path, - testOpts, - orgPublicId, - iacOrgSettings, - rulesOrigin, - ); - iacOutputMeta = { - orgName: results[0]?.org, - projectName: results[0]?.projectName, - gitRemoteUrl: results[0]?.meta?.gitRemoteUrl, - }; - - res = results; - iacScanFailures = [...iacScanFailures, ...(failures || [])]; - iacIgnoredIssuesCount += ignoreCount; - } catch (error) { - res = formatTestError(error); - } - - // Not all test results are arrays in order to be backwards compatible - // with scripts that use a callback with test. Coerce results/errors to be arrays - // and add the result options to each to be displayed - const resArray: any[] = Array.isArray(res) ? res : [res]; - - for (let i = 0; i < resArray.length; i++) { - const pathWithOptionalProjectName = utils.getPathWithOptionalProjectName( - path, - resArray[i], - ); - results.push( - assign(resArray[i], { path: pathWithOptionalProjectName }), - ); - // currently testOpts are identical for each test result returned even if it's for multiple projects. - // we want to return the project names, so will need to be crafty in a way that makes sense. - if (!testOpts.projectNames) { - resultOptions.push(testOpts); - } else { - resultOptions.push( - assign(cloneDeep(testOpts), { - projectName: testOpts.projectNames[i], - }), - ); - } - } - } - } finally { - cleanLocalCache(); + if (!iacOrgSettings.entitlements?.infrastructureAsCode) { + throw new UnsupportedEntitlementError('infrastructureAsCode'); } + const { + iacOutputMeta, + iacScanFailures, + iacIgnoredIssuesCount, + results, + resultOptions, + } = await scan( + iacOrgSettings, + options, + testSpinner, + paths, + orgPublicId, + buildOciRegistry, + ); + // this is any[] to follow the resArray type above const successResults: any[] = [], errorResults: any[] = []; diff --git a/src/cli/commands/test/iac/local-execution/index.ts b/src/cli/commands/test/iac/local-execution/index.ts index 910abcf718..5594f15af7 100644 --- a/src/cli/commands/test/iac/local-execution/index.ts +++ b/src/cli/commands/test/iac/local-execution/index.ts @@ -23,7 +23,7 @@ import { } from './measurable-methods'; import { findAndLoadPolicy } from '../../../../../lib/policy'; import { NoFilesToScanError } from './file-loader'; -import { processResults } from './process-results'; +import { ResultsProcessor } from './process-results'; import { generateProjectAttributes, generateTags } from '../../../monitor'; import { getAllDirectoriesForPath, @@ -35,9 +35,9 @@ import { getErrorStringCode } from './error-utils'; // this method executes the local processing engine and then formats the results to adapt with the CLI output. // this flow is the default GA flow for IAC scanning. export async function test( + resultsProcessor: ResultsProcessor, pathToScan: string, options: IaCTestFlags, - orgPublicId: string, iacOrgSettings: IacOrgSettings, rulesOrigin: RulesOrigin, ): Promise { @@ -110,15 +110,11 @@ export async function test( iacOrgSettings.customPolicies, ); - const { filteredIssues, ignoreCount } = await processResults( + const { filteredIssues, ignoreCount } = await resultsProcessor.processResults( resultsWithCustomSeverities, - orgPublicId, - iacOrgSettings, policy, tags, attributes, - options, - pathToScan, ); try { diff --git a/src/cli/commands/test/iac/local-execution/measurable-methods.ts b/src/cli/commands/test/iac/local-execution/measurable-methods.ts index 313f40f5b3..7fae234170 100644 --- a/src/cli/commands/test/iac/local-execution/measurable-methods.ts +++ b/src/cli/commands/test/iac/local-execution/measurable-methods.ts @@ -1,6 +1,7 @@ import { parseFiles } from './file-parser'; import { scanFiles } from './file-scanner'; -import { formatScanResults } from './process-results/results-formatter'; +import { formatScanResults as formatScanResultsV1 } from './process-results/v1/results-formatter'; +import { formatScanResults as formatScanResultsV2 } from './process-results/v2/results-formatter'; import { trackUsage } from './usage-tracking'; import { cleanLocalCache, initLocalCache } from './local-cache'; import { applyCustomSeverities } from './org-settings/apply-custom-severities'; @@ -82,8 +83,13 @@ const measurableCleanLocalCache = performanceAnalyticsDecorator( PerformanceAnalyticsKey.CacheCleanup, ); -const measurableFormatScanResults = performanceAnalyticsDecorator( - formatScanResults, +const measurableFormatScanResultsV1 = performanceAnalyticsDecorator( + formatScanResultsV1, + PerformanceAnalyticsKey.ResultFormatting, +); + +const measurableFormatScanResultsV2 = performanceAnalyticsDecorator( + formatScanResultsV2, PerformanceAnalyticsKey.ResultFormatting, ); @@ -109,7 +115,8 @@ export { measurableScanFiles as scanFiles, measurableGetIacOrgSettings as getIacOrgSettings, measurableApplyCustomSeverities as applyCustomSeverities, - measurableFormatScanResults as formatScanResults, + measurableFormatScanResultsV1 as formatScanResultsV1, + measurableFormatScanResultsV2 as formatScanResultsV2, measurableTrackUsage as trackUsage, measurableCleanLocalCache as cleanLocalCache, measurableLocalTest as localTest, diff --git a/src/cli/commands/test/iac/local-execution/process-results/index.ts b/src/cli/commands/test/iac/local-execution/process-results/index.ts index 2969545c54..d2aa6233bd 100644 --- a/src/cli/commands/test/iac/local-execution/process-results/index.ts +++ b/src/cli/commands/test/iac/local-execution/process-results/index.ts @@ -1,6 +1,3 @@ -import { filterIgnoredIssues } from './policy'; -import { formatAndShareResults } from './share-results'; -import { formatScanResults } from '../measurable-methods'; import { Policy } from '../../../../../../lib/policy/find-and-load-policy'; import { ProjectAttributes, Tag } from '../../../../../../lib/types'; import { @@ -9,42 +6,71 @@ import { IacOrgSettings, IaCTestFlags, } from '../types'; +import { processResults as processResultsV2 } from './v2'; +import { processResults as processResultsV1 } from './v1'; -export async function processResults( - resultsWithCustomSeverities: IacFileScanResult[], - orgPublicId: string, - iacOrgSettings: IacOrgSettings, - policy: Policy | undefined, - tags: Tag[] | undefined, - attributes: ProjectAttributes | undefined, - options: IaCTestFlags, - pathToScan: string, -): Promise<{ - filteredIssues: FormattedResult[]; - ignoreCount: number; -}> { - let projectPublicIds: Record = {}; - let gitRemoteUrl: string | undefined; +export interface ResultsProcessor { + processResults( + resultsWithCustomSeverities: IacFileScanResult[], + policy: Policy | undefined, + tags: Tag[] | undefined, + attributes: ProjectAttributes | undefined, + ): Promise<{ + filteredIssues: FormattedResult[]; + ignoreCount: number; + }>; +} + +export class SingleGroupResultsProcessor implements ResultsProcessor { + constructor( + private projectRoot: string, + private orgPublicId: string, + private iacOrgSettings: IacOrgSettings, + private options: IaCTestFlags, + ) {} - if (options.report) { - ({ projectPublicIds, gitRemoteUrl } = await formatAndShareResults({ - results: resultsWithCustomSeverities, - options, - orgPublicId, + processResults( + resultsWithCustomSeverities: IacFileScanResult[], + policy: Policy | undefined, + tags: Tag[] | undefined, + attributes: ProjectAttributes | undefined, + ): Promise<{ filteredIssues: FormattedResult[]; ignoreCount: number }> { + return processResultsV2( + resultsWithCustomSeverities, + this.orgPublicId, + this.iacOrgSettings, policy, tags, attributes, - pathToScan, - })); + this.options, + this.projectRoot, + ); } +} - const formattedResults = formatScanResults( - resultsWithCustomSeverities, - options, - iacOrgSettings.meta, - projectPublicIds, - gitRemoteUrl, - ); +export class MultipleGroupsResultsProcessor implements ResultsProcessor { + constructor( + private pathToScan: string, + private orgPublicId: string, + private iacOrgSettings: IacOrgSettings, + private options: IaCTestFlags, + ) {} - return filterIgnoredIssues(policy, formattedResults); + processResults( + resultsWithCustomSeverities: IacFileScanResult[], + policy: Policy | undefined, + tags: Tag[] | undefined, + attributes: ProjectAttributes | undefined, + ): Promise<{ filteredIssues: FormattedResult[]; ignoreCount: number }> { + return processResultsV1( + resultsWithCustomSeverities, + this.orgPublicId, + this.iacOrgSettings, + policy, + tags, + attributes, + this.options, + this.pathToScan, + ); + } } diff --git a/src/cli/commands/test/iac/local-execution/process-results/extract-line-number.ts b/src/cli/commands/test/iac/local-execution/process-results/v1/extract-line-number.ts similarity index 82% rename from src/cli/commands/test/iac/local-execution/process-results/extract-line-number.ts rename to src/cli/commands/test/iac/local-execution/process-results/v1/extract-line-number.ts index 1ed5045ad7..7c8a3c30f1 100644 --- a/src/cli/commands/test/iac/local-execution/process-results/extract-line-number.ts +++ b/src/cli/commands/test/iac/local-execution/process-results/v1/extract-line-number.ts @@ -1,14 +1,14 @@ -import { IaCErrorCodes } from '../types'; -import { CustomError } from '../../../../../../lib/errors'; +import { IaCErrorCodes } from '../../types'; +import { CustomError } from '../../../../../../../lib/errors'; import { CloudConfigFileTypes, MapsDocIdToTree, getLineNumber, } from '@snyk/cloud-config-parser'; -import { UnsupportedFileTypeError } from '../file-parser'; -import * as analytics from '../../../../../../lib/analytics'; +import { UnsupportedFileTypeError } from '../../file-parser'; +import * as analytics from '../../../../../../../lib/analytics'; import * as Debug from 'debug'; -import { getErrorStringCode } from '../error-utils'; +import { getErrorStringCode } from '../../error-utils'; const debug = Debug('iac-extract-line-number'); export function getFileTypeForParser(fileType: string): CloudConfigFileTypes { diff --git a/src/cli/commands/test/iac/local-execution/process-results/v1/index.ts b/src/cli/commands/test/iac/local-execution/process-results/v1/index.ts new file mode 100644 index 0000000000..6c728797cf --- /dev/null +++ b/src/cli/commands/test/iac/local-execution/process-results/v1/index.ts @@ -0,0 +1,47 @@ +import { filterIgnoredIssues } from './policy'; +import { formatAndShareResults } from './share-results'; +import { formatScanResultsV1 } from '../../measurable-methods'; +import { Policy } from '../../../../../../../lib/policy/find-and-load-policy'; +import { ProjectAttributes, Tag } from '../../../../../../../lib/types'; +import { + FormattedResult, + IacFileScanResult, + IacOrgSettings, + IaCTestFlags, +} from '../../types'; + +export async function processResults( + resultsWithCustomSeverities: IacFileScanResult[], + orgPublicId: string, + iacOrgSettings: IacOrgSettings, + policy: Policy | undefined, + tags: Tag[] | undefined, + attributes: ProjectAttributes | undefined, + options: IaCTestFlags, + pathToScan: string, +): Promise<{ filteredIssues: FormattedResult[]; ignoreCount: number }> { + let projectPublicIds: Record = {}; + let gitRemoteUrl: string | undefined; + + if (options.report) { + ({ projectPublicIds, gitRemoteUrl } = await formatAndShareResults({ + results: resultsWithCustomSeverities, + options, + orgPublicId, + policy, + tags, + attributes, + pathToScan, + })); + } + + const formattedResults = formatScanResultsV1( + resultsWithCustomSeverities, + options, + iacOrgSettings.meta, + projectPublicIds, + gitRemoteUrl, + ); + + return filterIgnoredIssues(policy, formattedResults); +} diff --git a/src/cli/commands/test/iac/local-execution/process-results/policy.ts b/src/cli/commands/test/iac/local-execution/process-results/v1/policy.ts similarity index 95% rename from src/cli/commands/test/iac/local-execution/process-results/policy.ts rename to src/cli/commands/test/iac/local-execution/process-results/v1/policy.ts index d55a1e779a..eda5271e5c 100644 --- a/src/cli/commands/test/iac/local-execution/process-results/policy.ts +++ b/src/cli/commands/test/iac/local-execution/process-results/v1/policy.ts @@ -1,5 +1,5 @@ -import { FormattedResult, PolicyMetadata } from '../types'; -import { Policy } from '../../../../../../lib/policy/find-and-load-policy'; +import { FormattedResult, PolicyMetadata } from '../../types'; +import { Policy } from '../../../../../../../lib/policy/find-and-load-policy'; export function filterIgnoredIssues( policy: Policy | undefined, diff --git a/src/cli/commands/test/iac/local-execution/process-results/results-formatter.ts b/src/cli/commands/test/iac/local-execution/process-results/v1/results-formatter.ts similarity index 94% rename from src/cli/commands/test/iac/local-execution/process-results/results-formatter.ts rename to src/cli/commands/test/iac/local-execution/process-results/v1/results-formatter.ts index 6e2e8b813f..a8e5cecce0 100644 --- a/src/cli/commands/test/iac/local-execution/process-results/results-formatter.ts +++ b/src/cli/commands/test/iac/local-execution/process-results/v1/results-formatter.ts @@ -6,19 +6,22 @@ import { IaCTestFlags, PolicyMetadata, TestMeta, -} from '../types'; -import { SEVERITY, SEVERITIES } from '../../../../../../lib/snyk-test/common'; -import { IacProjectType } from '../../../../../../lib/iac/constants'; -import { CustomError } from '../../../../../../lib/errors'; +} from '../../types'; +import { + SEVERITY, + SEVERITIES, +} from '../../../../../../../lib/snyk-test/common'; +import { IacProjectType } from '../../../../../../../lib/iac/constants'; +import { CustomError } from '../../../../../../../lib/errors'; import { extractLineNumber, getFileTypeForParser } from './extract-line-number'; -import { getErrorStringCode } from '../error-utils'; +import { getErrorStringCode } from '../../error-utils'; import { MapsDocIdToTree, getTrees, parsePath, } from '@snyk/cloud-config-parser'; import * as path from 'path'; -import { isLocalFolder } from '../../../../../../lib/detect'; +import { isLocalFolder } from '../../../../../../../lib/detect'; const severitiesArray = SEVERITIES.map((s) => s.verboseName); diff --git a/src/cli/commands/test/iac/local-execution/process-results/share-results-formatter.ts b/src/cli/commands/test/iac/local-execution/process-results/v1/share-results-formatter.ts similarity index 94% rename from src/cli/commands/test/iac/local-execution/process-results/share-results-formatter.ts rename to src/cli/commands/test/iac/local-execution/process-results/v1/share-results-formatter.ts index eb46416379..4855b3c12e 100644 --- a/src/cli/commands/test/iac/local-execution/process-results/share-results-formatter.ts +++ b/src/cli/commands/test/iac/local-execution/process-results/v1/share-results-formatter.ts @@ -1,9 +1,9 @@ -import { IacFileScanResult, IacShareResultsFormat } from '../types'; +import { IacFileScanResult, IacShareResultsFormat } from '../../types'; import * as path from 'path'; import { getRepositoryRootForPath, getWorkingDirectoryForPath, -} from '../../../../../../lib/iac/git'; +} from '../../../../../../../lib/iac/git'; export function formatShareResults( scanResults: IacFileScanResult[], diff --git a/src/cli/commands/test/iac/local-execution/process-results/share-results.ts b/src/cli/commands/test/iac/local-execution/process-results/v1/share-results.ts similarity index 68% rename from src/cli/commands/test/iac/local-execution/process-results/share-results.ts rename to src/cli/commands/test/iac/local-execution/process-results/v1/share-results.ts index bf86a3b07c..138f8e3ced 100644 --- a/src/cli/commands/test/iac/local-execution/process-results/share-results.ts +++ b/src/cli/commands/test/iac/local-execution/process-results/v1/share-results.ts @@ -1,10 +1,14 @@ -import { isFeatureFlagSupportedForOrg } from '../../../../../../lib/feature-flags'; -import { shareResults } from '../../../../../../lib/iac/cli-share-results'; -import { Policy } from '../../../../../../lib/policy/find-and-load-policy'; -import { ProjectAttributes, Tag } from '../../../../../../lib/types'; -import { FeatureFlagError } from '../assert-iac-options-flag'; +import { isFeatureFlagSupportedForOrg } from '../../../../../../../lib/feature-flags'; +import { shareResults } from '../../../../../../../lib/iac/cli-share-results'; +import { Policy } from '../../../../../../../lib/policy/find-and-load-policy'; +import { ProjectAttributes, Tag } from '../../../../../../../lib/types'; +import { FeatureFlagError } from '../../assert-iac-options-flag'; import { formatShareResults } from './share-results-formatter'; -import { IacFileScanResult, IaCTestFlags, ShareResultsOutput } from '../types'; +import { + IacFileScanResult, + IaCTestFlags, + ShareResultsOutput, +} from '../../types'; export async function formatAndShareResults({ results, diff --git a/src/cli/commands/test/iac/local-execution/process-results/v2/cli-share-results.ts b/src/cli/commands/test/iac/local-execution/process-results/v2/cli-share-results.ts new file mode 100644 index 0000000000..222b7c785c --- /dev/null +++ b/src/cli/commands/test/iac/local-execution/process-results/v2/cli-share-results.ts @@ -0,0 +1,110 @@ +import config from '../../../../../../../lib/config'; +import { makeRequest } from '../../../../../../../lib/request'; +import { getAuthHeader } from '../../../../../../../lib/api-token'; +import { + IacShareResultsFormat, + IaCTestFlags, + ShareResultsOutput, +} from '../../types'; +import { convertIacResultToScanResult } from '../../../../../../../lib/iac/envelope-formatters'; +import { Policy } from '../../../../../../../lib/policy/find-and-load-policy'; +import { getInfo } from '../../../../../../../lib/project-metadata/target-builders/git'; +import { GitTarget } from '../../../../../../../lib/ecosystems/types'; +import { Contributor } from '../../../../../../../lib/types'; +import * as analytics from '../../../../../../../lib/analytics'; +import { getContributors } from '../../../../../../../lib/monitor/dev-count-analysis'; +import * as Debug from 'debug'; +import { + AuthFailedError, + ValidationError, +} from '../../../../../../../lib/errors'; +import * as pathLib from 'path'; + +const debug = Debug('iac-cli-share-results'); +import { ProjectAttributes, Tag } from '../../../../../../../lib/types'; +import { TestLimitReachedError } from '../../usage-tracking'; +import { getRepositoryRootForPath } from '../../../../../../../lib/iac/git'; + +export async function shareResults({ + results, + policy, + tags, + attributes, + options, + projectRoot, +}: { + results: IacShareResultsFormat[]; + policy: Policy | undefined; + tags?: Tag[]; + attributes?: ProjectAttributes; + options?: IaCTestFlags; + projectRoot: string; +}): Promise { + const gitTarget = await readGitInfoForProjectRoot(projectRoot); + const scanResults = results.map((result) => + convertIacResultToScanResult(result, policy, gitTarget, options), + ); + + let contributors: Contributor[] = []; + if (gitTarget.remoteUrl) { + if (analytics.allowAnalytics()) { + try { + contributors = await getContributors(); + } catch (err) { + debug('error getting repo contributors', err); + } + } + } + const { res, body } = await makeRequest({ + method: 'POST', + url: `${config.API}/iac-cli-share-results`, + json: true, + qs: { org: options?.org ?? config.org }, + headers: { + authorization: getAuthHeader(), + }, + body: { + scanResults, + contributors, + tags, + attributes, + }, + }); + + switch (res.statusCode) { + case 401: + throw AuthFailedError(); + case 422: + throw new ValidationError( + res.body.error ?? 'An error occurred, please contact Snyk support', + ); + case 429: + throw new TestLimitReachedError(); + } + + return { projectPublicIds: body, gitRemoteUrl: gitTarget?.remoteUrl }; +} + +async function readGitInfoForProjectRoot( + projectRoot: string, +): Promise { + const repositoryRoot = getRepositoryRootForPath(projectRoot); + + const resolvedRepositoryRoot = pathLib.resolve(repositoryRoot); + const resolvedProjectRoot = pathLib.resolve(projectRoot); + + if (resolvedRepositoryRoot != resolvedProjectRoot) { + return {}; + } + + const gitInfo = await getInfo({ + isFromContainer: false, + cwd: projectRoot, + }); + + if (gitInfo) { + return gitInfo; + } + + return {}; +} diff --git a/src/cli/commands/test/iac/local-execution/process-results/v2/extract-line-number.ts b/src/cli/commands/test/iac/local-execution/process-results/v2/extract-line-number.ts new file mode 100644 index 0000000000..7c8a3c30f1 --- /dev/null +++ b/src/cli/commands/test/iac/local-execution/process-results/v2/extract-line-number.ts @@ -0,0 +1,52 @@ +import { IaCErrorCodes } from '../../types'; +import { CustomError } from '../../../../../../../lib/errors'; +import { + CloudConfigFileTypes, + MapsDocIdToTree, + getLineNumber, +} from '@snyk/cloud-config-parser'; +import { UnsupportedFileTypeError } from '../../file-parser'; +import * as analytics from '../../../../../../../lib/analytics'; +import * as Debug from 'debug'; +import { getErrorStringCode } from '../../error-utils'; +const debug = Debug('iac-extract-line-number'); + +export function getFileTypeForParser(fileType: string): CloudConfigFileTypes { + switch (fileType) { + case 'yaml': + case 'yml': + return CloudConfigFileTypes.YAML; + case 'json': + return CloudConfigFileTypes.JSON; + case 'tf': + return CloudConfigFileTypes.TF; + default: + throw new UnsupportedFileTypeError(fileType); + } +} + +export function extractLineNumber( + cloudConfigPath: string[], + fileType: CloudConfigFileTypes, + treeByDocId: MapsDocIdToTree, +): number { + try { + return getLineNumber(cloudConfigPath, fileType, treeByDocId); + } catch { + const err = new FailedToExtractLineNumberError(); + analytics.add('error-code', err.code); + debug('Parser library failed. Could not assign lineNumber to issue'); + return -1; + } +} + +class FailedToExtractLineNumberError extends CustomError { + constructor(message?: string) { + super( + message || 'Parser library failed. Could not assign lineNumber to issue', + ); + this.code = IaCErrorCodes.FailedToExtractLineNumberError; + this.strCode = getErrorStringCode(this.code); + this.userMessage = ''; // Not a user facing error. + } +} diff --git a/src/cli/commands/test/iac/local-execution/process-results/v2/index.ts b/src/cli/commands/test/iac/local-execution/process-results/v2/index.ts new file mode 100644 index 0000000000..10c636ed00 --- /dev/null +++ b/src/cli/commands/test/iac/local-execution/process-results/v2/index.ts @@ -0,0 +1,51 @@ +import { filterIgnoredIssues } from './policy'; +import { formatAndShareResults } from './share-results'; +import { formatScanResultsV2 } from '../../measurable-methods'; +import { Policy } from '../../../../../../../lib/policy/find-and-load-policy'; +import { ProjectAttributes, Tag } from '../../../../../../../lib/types'; +import { + FormattedResult, + IacFileScanResult, + IacOrgSettings, + IaCTestFlags, +} from '../../types'; + +export async function processResults( + resultsWithCustomSeverities: IacFileScanResult[], + orgPublicId: string, + iacOrgSettings: IacOrgSettings, + policy: Policy | undefined, + tags: Tag[] | undefined, + attributes: ProjectAttributes | undefined, + options: IaCTestFlags, + projectRoot: string, +): Promise<{ + filteredIssues: FormattedResult[]; + ignoreCount: number; +}> { + let projectPublicIds: Record = {}; + let gitRemoteUrl: string | undefined; + + if (options.report) { + ({ projectPublicIds, gitRemoteUrl } = await formatAndShareResults({ + results: resultsWithCustomSeverities, + options, + orgPublicId, + policy, + tags, + attributes, + projectRoot, + })); + } + + const formattedResults = formatScanResultsV2( + resultsWithCustomSeverities, + options, + iacOrgSettings.meta, + projectPublicIds, + projectRoot, + gitRemoteUrl, + ); + + return filterIgnoredIssues(policy, formattedResults); +} diff --git a/src/cli/commands/test/iac/local-execution/process-results/v2/policy.ts b/src/cli/commands/test/iac/local-execution/process-results/v2/policy.ts new file mode 100644 index 0000000000..eda5271e5c --- /dev/null +++ b/src/cli/commands/test/iac/local-execution/process-results/v2/policy.ts @@ -0,0 +1,92 @@ +import { FormattedResult, PolicyMetadata } from '../../types'; +import { Policy } from '../../../../../../../lib/policy/find-and-load-policy'; + +export function filterIgnoredIssues( + policy: Policy | undefined, + results: FormattedResult[], +) { + if (!policy) { + return { filteredIssues: results, ignoreCount: 0 }; + } + const vulns = results.map((res) => + policy.filter(toIaCVulnAdapter(res), undefined, 'exact'), + ); + const ignoreCount: number = vulns.reduce( + (totalIgnored, vuln) => totalIgnored + vuln.filtered.ignore.length, + 0, + ); + const filteredIssues = vulns.map((vuln) => toFormattedResult(vuln)); + return { filteredIssues, ignoreCount }; +} + +type IacVulnAdapter = { + vulnerabilities: { + id: string; + from: string[]; + }[]; + originalResult: FormattedResult; + filtered?: { ignore: any[] }; +}; + +// This is a total cop-out. The type I really want is AnnotatedIacIssue from +// src/lib/snyk-test/iac-test-result.ts, but that appears to only be used in the +// legacy flow and I gave up on adapting it to work in both flows. By the time +// this code is called, cloudConfigPath is present on the result object. +type AnnotatedResult = PolicyMetadata & { + cloudConfigPath: string[]; +}; + +function toIaCVulnAdapter(result: FormattedResult): IacVulnAdapter { + return { + vulnerabilities: result.result.cloudConfigResults.map( + (cloudConfigResult) => { + const annotatedResult = cloudConfigResult as AnnotatedResult; + + // Copy the cloudConfigPath array to avoid modifying the original with + // splice. + // Insert the targetFile into the path so that it is taken into account + // when determining whether an ignore rule should be applied. + const path = [...annotatedResult.cloudConfigPath]; + path.splice(0, 0, result.targetFile); + + return { + id: cloudConfigResult.id as string, + from: path, + }; + }, + ), + originalResult: result, + }; +} + +function toFormattedResult(adapter: IacVulnAdapter): FormattedResult { + const original = adapter.originalResult; + const filteredCloudConfigResults = original.result.cloudConfigResults.filter( + (res) => { + return adapter.vulnerabilities.some((vuln) => { + if (vuln.id !== res.id) { + return false; + } + + // Unfortunately we are forced to duplicate the logic in + // toIaCVulnAdapter so that we're comparing path components properly, + // including target file context. As that logic changes, so must this. + const annotatedResult = res as AnnotatedResult; + const significantPath = [...annotatedResult.cloudConfigPath]; + significantPath.splice(0, 0, original.targetFile); + + if (vuln.from.length !== significantPath.length) { + return false; + } + for (let i = 0; i < vuln.from.length; i++) { + if (vuln.from[i] !== significantPath[i]) { + return false; + } + } + return true; + }); + }, + ); + original.result.cloudConfigResults = filteredCloudConfigResults; + return original; +} diff --git a/src/cli/commands/test/iac/local-execution/process-results/v2/results-formatter.ts b/src/cli/commands/test/iac/local-execution/process-results/v2/results-formatter.ts new file mode 100644 index 0000000000..8324f0d481 --- /dev/null +++ b/src/cli/commands/test/iac/local-execution/process-results/v2/results-formatter.ts @@ -0,0 +1,207 @@ +import { + EngineType, + FormattedResult, + IaCErrorCodes, + IacFileScanResult, + IaCTestFlags, + PolicyMetadata, + TestMeta, +} from '../../types'; +import { + SEVERITY, + SEVERITIES, +} from '../../../../../../../lib/snyk-test/common'; +import { IacProjectType } from '../../../../../../../lib/iac/constants'; +import { CustomError } from '../../../../../../../lib/errors'; +import { extractLineNumber, getFileTypeForParser } from './extract-line-number'; +import { getErrorStringCode } from '../../error-utils'; +import { + MapsDocIdToTree, + getTrees, + parsePath, +} from '@snyk/cloud-config-parser'; +import * as path from 'path'; +import { isLocalFolder } from '../../../../../../../lib/detect'; + +const severitiesArray = SEVERITIES.map((s) => s.verboseName); + +export function formatScanResults( + scanResults: IacFileScanResult[], + options: IaCTestFlags, + meta: TestMeta, + projectPublicIds: Record, + porjectRoot: string, + gitRemoteUrl?: string, +): FormattedResult[] { + try { + const groupedByFile = scanResults.reduce((memo, scanResult) => { + const res = formatScanResult(scanResult, meta, options, porjectRoot); + + if (memo[scanResult.filePath]) { + memo[scanResult.filePath].result.cloudConfigResults.push( + ...res.result.cloudConfigResults, + ); + } else { + res.meta.gitRemoteUrl = gitRemoteUrl; + res.meta.projectId = projectPublicIds[res.targetFile]; + memo[scanResult.filePath] = res; + } + return memo; + }, {} as { [key: string]: FormattedResult }); + return Object.values(groupedByFile); + } catch (e) { + throw new FailedToFormatResults(); + } +} + +const engineTypeToProjectType = { + [EngineType.Kubernetes]: IacProjectType.K8S, + [EngineType.Terraform]: IacProjectType.TERRAFORM, + [EngineType.CloudFormation]: IacProjectType.CLOUDFORMATION, + [EngineType.ARM]: IacProjectType.ARM, + [EngineType.Custom]: IacProjectType.CUSTOM, +}; + +function formatScanResult( + scanResult: IacFileScanResult, + meta: TestMeta, + options: IaCTestFlags, + projectRoot: string, +): FormattedResult { + const fileType = getFileTypeForParser(scanResult.fileType); + const isGeneratedByCustomRule = scanResult.engineType === EngineType.Custom; + let treeByDocId: MapsDocIdToTree; + try { + treeByDocId = getTrees(fileType, scanResult.fileContent); + } catch (err) { + // we do nothing intentionally. + // Even if the building of the tree fails in the external parser, + // we still pass an undefined tree and not calculated line number for those + } + + const formattedIssues = scanResult.violatedPolicies.map((policy) => { + const cloudConfigPath = + scanResult.docId !== undefined + ? [`[DocId: ${scanResult.docId}]`].concat(parsePath(policy.msg)) + : policy.msg.split('.'); + + const lineNumber: number = treeByDocId + ? extractLineNumber(cloudConfigPath, fileType, treeByDocId) + : -1; + + return { + ...policy, + id: policy.publicId, + name: policy.title, + cloudConfigPath, + isIgnored: false, + iacDescription: { + issue: policy.issue, + impact: policy.impact, + resolve: policy.resolve, + }, + severity: policy.severity, + lineNumber, + documentation: !isGeneratedByCustomRule + ? `https://snyk.io/security-rules/${policy.publicId}` + : undefined, + isGeneratedByCustomRule, + }; + }); + + const { targetFilePath, projectName, targetFile } = computePaths( + projectRoot, + scanResult.filePath, + options.path, + ); + return { + result: { + cloudConfigResults: filterPoliciesBySeverity( + formattedIssues, + options.severityThreshold, + ), + projectType: scanResult.projectType, + }, + meta: { + ...meta, + projectId: '', // we do not have a project at this stage + policy: '', // we do not have the concept of policy + }, + filesystemPolicy: false, // we do not have the concept of policy + vulnerabilities: [], + dependencyCount: 0, + licensesPolicy: null, // we do not have the concept of license policies + ignoreSettings: null, + targetFile, + projectName, + org: meta.org, + policy: '', // we do not have the concept of policy + isPrivate: true, + targetFilePath, + packageManager: engineTypeToProjectType[scanResult.engineType], + }; +} + +export function filterPoliciesBySeverity( + violatedPolicies: PolicyMetadata[], + severityThreshold?: SEVERITY, +): PolicyMetadata[] { + if (!severityThreshold || severityThreshold === SEVERITY.LOW) { + return violatedPolicies.filter((violatedPolicy) => { + return violatedPolicy.severity !== 'none'; + }); + } + + const severitiesToInclude = severitiesArray.slice( + severitiesArray.indexOf(severityThreshold), + ); + return violatedPolicies.filter((policy) => { + return ( + policy.severity !== 'none' && + severitiesToInclude.includes(policy.severity) + ); + }); +} + +export class FailedToFormatResults extends CustomError { + constructor(message?: string) { + super(message || 'Failed to format results'); + this.code = IaCErrorCodes.FailedToFormatResults; + this.strCode = getErrorStringCode(this.code); + this.userMessage = + 'We failed printing the results, please contact support@snyk.io'; + } +} + +function computePaths( + projectRoot: string, + filePath: string, + pathArg = '.', +): { targetFilePath: string; projectName: string; targetFile: string } { + const targetFilePath = path.resolve(filePath, '.'); + + // the absolute path is needed to compute the full project path + const cmdPath = path.resolve(pathArg); + + let projectPath: string; + let targetFile: string; + if (!isLocalFolder(cmdPath)) { + // if the provided path points to a file, then the project starts at the parent folder of that file + // and the target file was provided as the path argument + projectPath = path.dirname(cmdPath); + targetFile = path.isAbsolute(pathArg) + ? path.relative(process.cwd(), pathArg) + : pathArg; + } else { + // otherwise, the project starts at the provided path + // and the target file must be the relative path from the project path to the path of the scanned file + projectPath = cmdPath; + targetFile = path.relative(projectPath, targetFilePath); + } + + return { + targetFilePath, + projectName: path.basename(projectRoot), + targetFile, + }; +} diff --git a/src/cli/commands/test/iac/local-execution/process-results/v2/share-results-formatter.ts b/src/cli/commands/test/iac/local-execution/process-results/v2/share-results-formatter.ts new file mode 100644 index 0000000000..5d42aa2de8 --- /dev/null +++ b/src/cli/commands/test/iac/local-execution/process-results/v2/share-results-formatter.ts @@ -0,0 +1,60 @@ +import { IacFileScanResult, IacShareResultsFormat } from '../../types'; +import * as path from 'path'; + +export function formatShareResults( + projectRoot: string, + scanResults: IacFileScanResult[], +): IacShareResultsFormat[] { + const resultsGroupedByFilePath = groupByFilePath(scanResults); + + return resultsGroupedByFilePath.map((result) => { + const { projectName, targetFile } = computePaths( + projectRoot, + result.filePath, + ); + + return { + projectName, + targetFile, + filePath: result.filePath, + fileType: result.fileType, + projectType: result.projectType, + violatedPolicies: result.violatedPolicies, + }; + }); +} + +function groupByFilePath(scanResults: IacFileScanResult[]) { + const groupedByFilePath = scanResults.reduce((memo, scanResult) => { + scanResult.violatedPolicies.forEach((violatedPolicy) => { + violatedPolicy.docId = scanResult.docId; + }); + if (memo[scanResult.filePath]) { + memo[scanResult.filePath].violatedPolicies.push( + ...scanResult.violatedPolicies, + ); + } else { + memo[scanResult.filePath] = scanResult; + } + return memo; + }, {} as Record); + + return Object.values(groupedByFilePath); +} + +function computePaths( + projectRoot: string, + filePath: string, +): { targetFilePath: string; projectName: string; targetFile: string } { + const projectDirectory = path.resolve(projectRoot); + const currentDirectoryName = path.basename(projectDirectory); + const absoluteFilePath = path.resolve(filePath); + const relativeFilePath = path.relative(projectDirectory, absoluteFilePath); + const unixRelativeFilePath = relativeFilePath.split(path.sep).join('/'); + + return { + targetFilePath: absoluteFilePath, + projectName: currentDirectoryName, + targetFile: unixRelativeFilePath, + }; +} diff --git a/src/cli/commands/test/iac/local-execution/process-results/v2/share-results.ts b/src/cli/commands/test/iac/local-execution/process-results/v2/share-results.ts new file mode 100644 index 0000000000..1368dc6837 --- /dev/null +++ b/src/cli/commands/test/iac/local-execution/process-results/v2/share-results.ts @@ -0,0 +1,48 @@ +import { isFeatureFlagSupportedForOrg } from '../../../../../../../lib/feature-flags'; +import { shareResults } from './cli-share-results'; +import { Policy } from '../../../../../../../lib/policy/find-and-load-policy'; +import { ProjectAttributes, Tag } from '../../../../../../../lib/types'; +import { FeatureFlagError } from '../../assert-iac-options-flag'; +import { formatShareResults } from './share-results-formatter'; +import { + IacFileScanResult, + IaCTestFlags, + ShareResultsOutput, +} from '../../types'; + +export async function formatAndShareResults({ + results, + options, + orgPublicId, + policy, + tags, + attributes, + projectRoot, +}: { + results: IacFileScanResult[]; + options: IaCTestFlags; + orgPublicId: string; + policy: Policy | undefined; + tags?: Tag[]; + attributes?: ProjectAttributes; + projectRoot: string; +}): Promise { + const isCliReportEnabled = await isFeatureFlagSupportedForOrg( + 'iacCliShareResults', + orgPublicId, + ); + if (!isCliReportEnabled.ok) { + throw new FeatureFlagError('report', 'iacCliShareResults'); + } + + const formattedResults = formatShareResults(projectRoot, results); + + return await shareResults({ + results: formattedResults, + policy, + tags, + attributes, + options, + projectRoot, + }); +} diff --git a/src/cli/commands/test/iac/scan.ts b/src/cli/commands/test/iac/scan.ts new file mode 100644 index 0000000000..2c1d8691f4 --- /dev/null +++ b/src/cli/commands/test/iac/scan.ts @@ -0,0 +1,156 @@ +import * as cloneDeep from 'lodash.clonedeep'; +import * as assign from 'lodash.assign'; + +import { + IacFileInDirectory, + IacOutputMeta, + Options, + TestOptions, +} from '../../../../lib/types'; +import { TestResult } from '../../../../lib/snyk-test/legacy'; + +import * as utils from '../utils'; +import { spinnerMessage } from '../../../../lib/formatters/iac-output'; + +import { test as iacTest } from './local-execution'; +import { formatTestError } from '../format-test-error'; + +import { assertIaCOptionsFlags } from './local-execution/assert-iac-options-flag'; +import { initRules } from './local-execution/rules/rules'; +import { cleanLocalCache } from './local-execution/measurable-methods'; +import * as ora from 'ora'; +import { IacOrgSettings } from './local-execution/types'; +import * as pathLib from 'path'; +import { CustomError } from '../../../../lib/errors'; +import { OciRegistry } from './local-execution/rules/oci-registry'; +import { + MultipleGroupsResultsProcessor, + ResultsProcessor, + SingleGroupResultsProcessor, +} from './local-execution/process-results'; + +export async function scan( + iacOrgSettings: IacOrgSettings, + options: any, + testSpinner: ora.Ora | undefined, + paths: string[], + orgPublicId: string, + buildOciRules: () => OciRegistry, + projectRoot?: string, +): Promise<{ + iacOutputMeta: IacOutputMeta | undefined; + iacScanFailures: IacFileInDirectory[]; + iacIgnoredIssuesCount: number; + results: any[]; + resultOptions: (Options & TestOptions)[]; +}> { + const results = [] as any[]; + const resultOptions: Array = []; + + let iacOutputMeta: IacOutputMeta | undefined; + let iacScanFailures: IacFileInDirectory[] = []; + let iacIgnoredIssuesCount = 0; + + try { + const rulesOrigin = await initRules(buildOciRules, iacOrgSettings, options); + + testSpinner?.start(spinnerMessage); + + for (const path of paths) { + // Create a copy of the options so a specific test can + // modify them i.e. add `options.file` etc. We'll need + // these options later. + const testOpts = cloneDeep(options); + testOpts.path = path; + testOpts.projectName = testOpts['project-name']; + + let res: (TestResult | TestResult[]) | Error; + try { + assertIaCOptionsFlags(process.argv); + + let resultsProcessor: ResultsProcessor; + + if (projectRoot) { + if (pathLib.relative(projectRoot, path).includes('..')) { + throw new CurrentWorkingDirectoryTraversalError(); + } + + resultsProcessor = new SingleGroupResultsProcessor( + projectRoot, + orgPublicId, + iacOrgSettings, + testOpts, + ); + } else { + resultsProcessor = new MultipleGroupsResultsProcessor( + path, + orgPublicId, + iacOrgSettings, + testOpts, + ); + } + + const { results, failures, ignoreCount } = await iacTest( + resultsProcessor, + path, + testOpts, + iacOrgSettings, + rulesOrigin, + ); + iacOutputMeta = { + orgName: results[0]?.org, + projectName: results[0]?.projectName, + gitRemoteUrl: results[0]?.meta?.gitRemoteUrl, + }; + + res = results; + iacScanFailures = [...iacScanFailures, ...(failures || [])]; + iacIgnoredIssuesCount += ignoreCount; + } catch (error) { + res = formatTestError(error); + } + + // Not all test results are arrays in order to be backwards compatible + // with scripts that use a callback with test. Coerce results/errors to be arrays + // and add the result options to each to be displayed + const resArray: any[] = Array.isArray(res) ? res : [res]; + + for (let i = 0; i < resArray.length; i++) { + const pathWithOptionalProjectName = utils.getPathWithOptionalProjectName( + path, + resArray[i], + ); + results.push( + assign(resArray[i], { path: pathWithOptionalProjectName }), + ); + // currently testOpts are identical for each test result returned even if it's for multiple projects. + // we want to return the project names, so will need to be crafty in a way that makes sense. + if (!testOpts.projectNames) { + resultOptions.push(testOpts); + } else { + resultOptions.push( + assign(cloneDeep(testOpts), { + projectName: testOpts.projectNames[i], + }), + ); + } + } + } + } finally { + cleanLocalCache(); + } + + return { + iacOutputMeta, + iacScanFailures, + iacIgnoredIssuesCount, + results, + resultOptions, + }; +} + +class CurrentWorkingDirectoryTraversalError extends CustomError { + constructor() { + super('Path is outside the current working directory'); + } +} diff --git a/test/jest/unit/iac/index.spec.ts b/test/jest/unit/iac/index.spec.ts index 56af1be690..0a54cf5d71 100644 --- a/test/jest/unit/iac/index.spec.ts +++ b/test/jest/unit/iac/index.spec.ts @@ -53,6 +53,7 @@ import { } from '../../../../src/cli/commands/test/iac/local-execution/types'; import { IacProjectType } from '../../../../src/lib/iac/constants'; +import { MultipleGroupsResultsProcessor } from '../../../../src/cli/commands/test/iac/local-execution/process-results'; const parsedFiles: IacFileParsed[] = [ { engineType: EngineType.Terraform, @@ -104,10 +105,17 @@ describe('test()', () => { it('returns the unparsable files excluding content', async () => { const opts: IaCTestFlags = {}; + const resultsProcessor = new MultipleGroupsResultsProcessor( + './storage/', + 'org-name', + iacOrgSettings, + opts, + ); + const { failures } = await test( + resultsProcessor, './storage/', opts, - 'org-name', iacOrgSettings, RulesOrigin.Internal, ); diff --git a/test/jest/unit/iac/process-results/policy.spec.ts b/test/jest/unit/iac/process-results/policy.spec.ts index 9c9551ce3b..49a1a8962c 100644 --- a/test/jest/unit/iac/process-results/policy.spec.ts +++ b/test/jest/unit/iac/process-results/policy.spec.ts @@ -1,4 +1,4 @@ -import { filterIgnoredIssues } from '../../../../../src/cli/commands/test/iac/local-execution/process-results/policy'; +import { filterIgnoredIssues } from '../../../../../src/cli/commands/test/iac/local-execution/process-results/v1/policy'; import { FormattedResult } from '../../../../../src/cli/commands/test/iac/local-execution/types'; import * as fs from 'fs'; import * as path from 'path'; diff --git a/test/jest/unit/iac/process-results/results-formatter.spec.ts b/test/jest/unit/iac/process-results/results-formatter.spec.ts index 18c12bec4c..121b191bcd 100644 --- a/test/jest/unit/iac/process-results/results-formatter.spec.ts +++ b/test/jest/unit/iac/process-results/results-formatter.spec.ts @@ -1,7 +1,7 @@ import { filterPoliciesBySeverity, formatScanResults, -} from '../../../../../src/cli/commands/test/iac/local-execution/process-results/results-formatter'; +} from '../../../../../src/cli/commands/test/iac/local-execution/process-results/v1/results-formatter'; import { SEVERITY } from '../../../../../src/lib/snyk-test/common'; import { expectedFormattedResultsWithLineNumber, diff --git a/test/jest/unit/iac/process-results/share-results-formatters.spec.ts b/test/jest/unit/iac/process-results/share-results-formatters.spec.ts index f90913bbd7..0f5eb5df8f 100644 --- a/test/jest/unit/iac/process-results/share-results-formatters.spec.ts +++ b/test/jest/unit/iac/process-results/share-results-formatters.spec.ts @@ -1,4 +1,4 @@ -import { formatShareResults } from '../../../../../src/cli/commands/test/iac/local-execution/process-results/share-results-formatter'; +import { formatShareResults } from '../../../../../src/cli/commands/test/iac/local-execution/process-results/v1/share-results-formatter'; import { generateScanResults } from '../results-formatter.fixtures'; import { expectedFormattedResultsForShareResults } from './share-results-formatters.fixtures'; import * as git from '../../../../../src/lib/iac/git';