diff --git a/src/cli/commands/test/iac-local-execution/index.ts b/src/cli/commands/test/iac-local-execution/index.ts index cb945e61d8..3e3a088b25 100644 --- a/src/cli/commands/test/iac-local-execution/index.ts +++ b/src/cli/commands/test/iac-local-execution/index.ts @@ -130,15 +130,17 @@ export async function test( ); let projectPublicIds: Record = {}; + let gitRemoteUrl: string | undefined; + if (options.report) { - projectPublicIds = await formatAndShareResults({ + ({ projectPublicIds, gitRemoteUrl } = await formatAndShareResults({ results: resultsWithCustomSeverities, options, orgPublicId, policy, tags, attributes, - }); + })); } const formattedResults = formatScanResults( @@ -146,6 +148,7 @@ export async function test( options, iacOrgSettings.meta, projectPublicIds, + gitRemoteUrl, ); const { filteredIssues, ignoreCount } = filterIgnoredIssues( diff --git a/src/cli/commands/test/iac-local-execution/results-formatter.ts b/src/cli/commands/test/iac-local-execution/results-formatter.ts index 425ebd0488..756dad2829 100644 --- a/src/cli/commands/test/iac-local-execution/results-formatter.ts +++ b/src/cli/commands/test/iac-local-execution/results-formatter.ts @@ -26,6 +26,7 @@ export function formatScanResults( options: IaCTestFlags, meta: TestMeta, projectPublicIds: Record, + gitRemoteUrl?: string, ): FormattedResult[] { try { const groupedByFile = scanResults.reduce((memo, scanResult) => { @@ -35,6 +36,7 @@ export function formatScanResults( ...res.result.cloudConfigResults, ); } else { + res.meta.gitRemoteUrl = gitRemoteUrl; res.meta.projectId = projectPublicIds[res.targetFile]; memo[scanResult.filePath] = res; } diff --git a/src/cli/commands/test/iac-local-execution/share-results.ts b/src/cli/commands/test/iac-local-execution/share-results.ts index 02e8ba702e..a6b8a230cd 100644 --- a/src/cli/commands/test/iac-local-execution/share-results.ts +++ b/src/cli/commands/test/iac-local-execution/share-results.ts @@ -4,7 +4,7 @@ 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 } from './types'; +import { IacFileScanResult, IaCTestFlags, ShareResultsOutput } from './types'; export async function formatAndShareResults({ results, @@ -20,7 +20,7 @@ export async function formatAndShareResults({ policy: Policy | undefined; tags?: Tag[]; attributes?: ProjectAttributes; -}): Promise> { +}): Promise { const isCliReportEnabled = await isFeatureFlagSupportedForOrg( 'iacCliShareResults', orgPublicId, diff --git a/src/cli/commands/test/iac-local-execution/types.ts b/src/cli/commands/test/iac-local-execution/types.ts index f17a308be5..28b188fb66 100644 --- a/src/cli/commands/test/iac-local-execution/types.ts +++ b/src/cli/commands/test/iac-local-execution/types.ts @@ -122,6 +122,7 @@ export interface TestMeta { ignoreSettings?: IgnoreSettings | null; projectId?: string; policy?: string; + gitRemoteUrl?: string; } export interface OpaWasmInstance { @@ -400,3 +401,8 @@ export enum PerformanceAnalyticsKey { CacheCleanup = 'cache-cleanup-ms', Total = 'total-iac-ms', } + +export interface ShareResultsOutput { + projectPublicIds: { [targetFile: string]: string }; + gitRemoteUrl?: string; +} diff --git a/src/cli/commands/test/index.ts b/src/cli/commands/test/index.ts index b0d16de881..9ee0c4a2ac 100644 --- a/src/cli/commands/test/index.ts +++ b/src/cli/commands/test/index.ts @@ -6,7 +6,12 @@ import chalk from 'chalk'; import { MissingArgError } from '../../../lib/errors'; import * as snyk from '../../../lib'; -import { IacFileInDirectory, Options, TestOptions } from '../../../lib/types'; +import { + IacFileInDirectory, + IacOutputMeta, + Options, + TestOptions, +} from '../../../lib/types'; import { MethodArgs } from '../../args'; import { TestCommandResult } from '../../commands/types'; import { LegacyVulnApiResult, TestResult } from '../../../lib/snyk-test/legacy'; @@ -17,7 +22,10 @@ import { summariseVulnerableResults, } from '../../../lib/formatters'; import * as utils from './utils'; -import { getIacDisplayErrorFileOutput } from '../../../lib/formatters/iac-output'; +import { + getIacDisplayErrorFileOutput, + shareResultsOutput, +} from '../../../lib/formatters/iac-output'; import { getEcosystemForTest, testEcosystem } from '../../../lib/ecosystems'; import { hasFixes, hasPatches, hasUpgrades } from '../../../lib/vuln-helpers'; import { FailOn } from '../../../lib/snyk-test/common'; @@ -43,7 +51,6 @@ import { containsSpotlightVulnIds, notificationForSpotlightVulns, } from '../../../lib/spotlight-vuln-notification'; -import config from '../../../lib/config'; import { isIacShareResultsOptions } from './iac-local-execution/assert-iac-options-flag'; import { assertIaCOptionsFlags } from './iac-local-execution/assert-iac-options-flag'; @@ -95,6 +102,7 @@ export default async function test( // Holds an array of scanned file metadata for output. let iacScanFailures: IacFileInDirectory[] | undefined; + let iacOutputMeta: IacOutputMeta | undefined; // Promise waterfall to test all other paths sequentially for (const path of paths) { @@ -110,8 +118,11 @@ export default async function test( if (options.iac) { assertIaCOptionsFlags(process.argv); const { results, failures } = await iacTest(path, testOpts); - testOpts.org = results[0]?.org; - testOpts.projectName = results[0]?.projectName; + iacOutputMeta = { + orgName: results[0]?.org, + projectName: results[0]?.projectName, + gitRemoteUrl: results[0]?.meta?.gitRemoteUrl, + }; res = results; iacScanFailures = failures; } else { @@ -297,10 +308,7 @@ export default async function test( response += spotlightVulnsMsg; if (isIacShareResultsOptions(options)) { - response += - chalk.bold.white( - `Your test results are available at: ${config.ROOT}/org/${resultOptions[0].org}/projects under the name ${resultOptions[0].projectName}`, - ) + EOL; + response += chalk.bold.white(shareResultsOutput(iacOutputMeta!)) + EOL; } const error = new Error(response) as any; @@ -322,10 +330,7 @@ export default async function test( ); if (isIacShareResultsOptions(options)) { - response += - chalk.bold.white( - `Your test results are available at: ${config.ROOT}/org/${resultOptions[0].org}/projects under the name ${resultOptions[0].projectName}`, - ) + EOL; + response += chalk.bold.white(shareResultsOutput(iacOutputMeta!)) + EOL; } return TestCommandResult.createHumanReadableTestCommandResult( diff --git a/src/lib/formatters/iac-output.ts b/src/lib/formatters/iac-output.ts index 9e3755e681..565cdb34e7 100644 --- a/src/lib/formatters/iac-output.ts +++ b/src/lib/formatters/iac-output.ts @@ -15,11 +15,12 @@ import { printPath } from './remediation-based-format-issues'; import { titleCaseText } from './legacy-format-issue'; import * as sarif from 'sarif'; import { colorTextBySeverity } from '../../lib/snyk-test/common'; -import { IacFileInDirectory } from '../../lib/types'; +import { IacFileInDirectory, IacOutputMeta } from '../../lib/types'; import { isLocalFolder } from '../../lib/detect'; import { getSeverityValue } from './get-severity-value'; import { getIssueLevel } from './sarif-output'; import { getVersion } from '../version'; +import config from '../config'; const debug = Debug('iac-output'); function formatIacIssue( @@ -317,3 +318,15 @@ function getPathRelativeToRepoRoot( const fullPath = pathLib.resolve(basePath, filePath).replace(/\\/g, '/'); return fullPath.replace(repoRoot, ''); } + +export function shareResultsOutput(iacOutputMeta: IacOutputMeta): string { + let projectName: string = iacOutputMeta.projectName; + if (iacOutputMeta?.gitRemoteUrl) { + // from "http://github.com/snyk/cli.git" to "snyk/cli" + projectName = iacOutputMeta.gitRemoteUrl.replace( + /^https?:\/\/github.com\/(.*)\.git$/, + '$1', + ); + } + return `Your test results are available at: ${config.ROOT}/org/${iacOutputMeta.orgName}/projects under the name ${projectName}`; +} diff --git a/src/lib/iac/cli-share-results.ts b/src/lib/iac/cli-share-results.ts index 27e98dc148..626acfdfde 100644 --- a/src/lib/iac/cli-share-results.ts +++ b/src/lib/iac/cli-share-results.ts @@ -4,6 +4,7 @@ import { getAuthHeader } from '../api-token'; import { IacShareResultsFormat, IaCTestFlags, + ShareResultsOutput, } from '../../cli/commands/test/iac-local-execution/types'; import { convertIacResultToScanResult } from './envelope-formatters'; import { Policy } from '../policy/find-and-load-policy'; @@ -30,7 +31,7 @@ export async function shareResults({ tags?: Tag[]; attributes?: ProjectAttributes; options?: IaCTestFlags; -}): Promise> { +}): Promise { const gitTarget = (await getInfo(false)) as GitTarget; const scanResults = results.map((result) => convertIacResultToScanResult(result, policy, gitTarget, options), @@ -72,5 +73,5 @@ export async function shareResults({ ); } - return body; + return { projectPublicIds: body, gitRemoteUrl: gitTarget?.remoteUrl }; } diff --git a/src/lib/types.ts b/src/lib/types.ts index b6d7e6539e..83c01143b2 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -266,3 +266,9 @@ export interface IacFileInDirectory { projectType?: IacProjectTypes; failureReason?: string; } + +export interface IacOutputMeta { + projectName: string; + orgName: string; + gitRemoteUrl?: string; +} diff --git a/test/jest/acceptance/iac/cli-share-results.spec.ts b/test/jest/acceptance/iac/cli-share-results.spec.ts index a9b9812dca..67a8f8fd2c 100644 --- a/test/jest/acceptance/iac/cli-share-results.spec.ts +++ b/test/jest/acceptance/iac/cli-share-results.spec.ts @@ -52,7 +52,7 @@ describe('CLI Share Results', () => { expect(exitCode).toBe(1); expect(stdout).toContain( - `Your test results are available at: http://localhost:${server.getPort()}/org/test-org/projects under the name arm`, + `Your test results are available at: http://localhost:${server.getPort()}/org/test-org/projects under the name snyk/cli`, ); }); diff --git a/test/jest/unit/lib/formatters/iac-output.spec.ts b/test/jest/unit/lib/formatters/iac-output.spec.ts index 8c1aae33c9..0a73af2c5f 100644 --- a/test/jest/unit/lib/formatters/iac-output.spec.ts +++ b/test/jest/unit/lib/formatters/iac-output.spec.ts @@ -1,4 +1,7 @@ -import { createSarifOutputForIac } from '../../../../../src/lib/formatters/iac-output'; +import { + createSarifOutputForIac, + shareResultsOutput, +} from '../../../../../src/lib/formatters/iac-output'; import { IacTestResponse, AnnotatedIacIssue, @@ -106,3 +109,23 @@ describe('createSarifOutputForIac', () => { expect(location?.physicalLocation?.region).not.toBeDefined(); }); }); + +describe('shareResultsOutput', () => { + it('returns the correct output when gitRemoteUrl is specified', () => { + const output = shareResultsOutput({ + projectName: 'test-project', + orgName: 'test-org', + gitRemoteUrl: 'http://github.com/test/repo.git', + }); + + expect(output).toContain('under the name test/repo'); + }); + it('returns the correct output when gitRemoteUrl is not specified', () => { + const output = shareResultsOutput({ + projectName: 'test-project', + orgName: 'test-org', + }); + + expect(output).toContain('under the name test-project'); + }); +});