diff --git a/packages/app/src/cli/services/app-security-json-output.ts b/packages/app/src/cli/services/app-security-json-output.ts deleted file mode 100644 index bab6869bee8..00000000000 --- a/packages/app/src/cli/services/app-security-json-output.ts +++ /dev/null @@ -1,143 +0,0 @@ -import {effectiveClientId, type AppSecuritySelection} from './app-security-selection.js' -import {isAbsolutePath, resolvePath} from '@shopify/cli-kit/node/path' -import {zod} from '@shopify/cli-kit/node/schema' -// eslint-disable-next-line no-restricted-imports -- JSON file paths use native platform separators. -import {resolve} from 'node:path' -import type {AppSecurityBlockingLevel} from './app-security-api.js' -import type {AppSecurityScope as StoredAppSecurityScope, CoverageScanDirectory} from './app-security-engine/index.js' -import type {AppSecurityInstructionsDelivery} from './app-security-instructions.js' - -/** - * The parts of the app security commands' JSON output that several commands share, and the codecs that map the - * commands' data to them. This is the public contract: the stored results files have their own format, in - * app-security-engine/results/schema.ts, and never appear in it. - */ - -/** - * An absolute path with the platform's native separators. A function, so each field gets its own instance: the JSON - * Schema would render a shared instance as a `$ref` into the first field that uses it. - */ -export const createAbsolutePathSchema = () => - zod.string().refine(isAbsolutePath, 'Expected an absolute native filesystem path') - -/** The app the command worked on. */ -export const appSecurityAppSchema = zod - .object({ - directory: createAbsolutePathSchema().describe('The absolute app directory.'), - configurationPath: createAbsolutePathSchema() - .nullable() - .describe('The absolute path of the selected app configuration file, or null with --without-app-config.'), - clientId: zod - .string() - .min(1) - .nullable() - .describe( - "The --client-id value when passed, otherwise the app configuration's client_id. Null when the app configuration isn't linked.", - ), - }) - .strict() - -type AppSecurityApp = zod.infer - -/** The coding-agent instructions, which a JSON result includes instead of printing them. */ -export const appSecurityInstructionsSchema = zod - .object({ - content: zod.string().min(1).describe('The instructions in Markdown, as printed without --json.'), - copied: zod.boolean().describe('Whether the instructions were also copied to the clipboard.'), - }) - .strict() - -type AppSecurityInstructions = zod.infer - -export const appSecurityScopeSchema = zod - .object({ - includeDirs: zod.array(zod.string()).describe('The --include-dir values.'), - excludes: zod.array(zod.string()).describe('The --exclude globs.'), - noGitIgnore: zod.boolean().describe('Whether --no-git-ignore was passed.'), - }) - .strict() - .describe('The check flags that chose which files were gathered, exactly as typed.') - -type AppSecurityScope = zod.infer - -export const appSecurityScanDirectorySchema = zod - .object({ - directory: createAbsolutePathSchema().describe('The absolute path of the directory.'), - origin: zod - .enum(['app-directory', 'include-dir', 'app-config-directory']) - .describe( - 'Why it was scanned: it is the app directory, an --include-dir value, or an extension or web directory of the app configuration.', - ), - }) - .strict() - .describe('A directory that check scanned.') - -type AppSecurityScanDirectory = zod.infer - -const SCAN_DIRECTORY_ORIGINS: Record = { - app_directory: 'app-directory', - include_dir: 'include-dir', - app_config_directory: 'app-config-directory', -} - -export const appSecurityBlockingSchema = zod - .object({ - level: zod.enum(['high', 'medium', 'low', 'none']).describe('The --blocking severity.'), - blockedCheckCount: zod - .number() - .int() - .nonnegative() - .describe('The number of checks with findings at or above that severity. Always 0 for none.'), - }) - .strict() - .describe('The --blocking policy and the checks that fail it.') - -type AppSecurityBlocking = zod.infer - -/** CLI Kit joins paths with forward slashes, even on Windows. JSON output shows them absolute, with native separators. */ -export function projectAppSecurityPath(path: string): string { - return resolve(path) -} - -/** Projects a path that is relative to the app directory, as results files record them. An absolute path stays as it is. */ -export function projectAppSecurityRelativePath(appDirectory: string, path: string): string { - return projectAppSecurityPath(resolvePath(appDirectory, path)) -} - -export function projectAppSecurityApp(selection: AppSecuritySelection): AppSecurityApp { - return { - directory: projectAppSecurityPath(selection.appDirectory), - configurationPath: selection.kind === 'config' ? projectAppSecurityPath(selection.appConfigFilePath) : null, - clientId: effectiveClientId(selection) ?? null, - } -} - -/** Projects delivered instructions: `delivery.copy` means that they're on the clipboard. */ -export function projectAppSecurityInstructions( - instructions: string, - delivery: AppSecurityInstructionsDelivery, -): AppSecurityInstructions { - return {content: instructions, copied: delivery.copy} -} - -export function projectAppSecurityScope(scope: StoredAppSecurityScope): AppSecurityScope { - return {includeDirs: scope.include_dirs, excludes: scope.excludes, noGitIgnore: scope.no_git_ignore} -} - -/** Projects a scan directory, either absolute or relative to the app directory as results files record them. */ -export function projectAppSecurityScanDirectory( - appDirectory: string, - scanDirectory: CoverageScanDirectory, -): AppSecurityScanDirectory { - return { - directory: projectAppSecurityRelativePath(appDirectory, scanDirectory.directory), - origin: SCAN_DIRECTORY_ORIGINS[scanDirectory.origin], - } -} - -export function projectAppSecurityBlocking(blocking: { - level: AppSecurityBlockingLevel - blockedChecks: number -}): AppSecurityBlocking { - return {level: blocking.level, blockedCheckCount: blocking.blockedChecks} -} diff --git a/packages/app/src/cli/services/app-security-results.ts b/packages/app/src/cli/services/app-security-results.ts index 2140430dc58..55f44d535a7 100644 --- a/packages/app/src/cli/services/app-security-results.ts +++ b/packages/app/src/cli/services/app-security-results.ts @@ -10,7 +10,7 @@ import { resolveAppSecurityCommands, type AppSecurityCommands, } from './app-security-commands.js' -import {projectAppSecurityPath} from './app-security-json-output.js' +import {projectAppSecurityPath} from './app-security/result.js' import {resultsKey, type AppSecuritySelection} from './app-security-selection.js' import { combineFindings, diff --git a/packages/app/src/cli/services/app-security-json-output.test.ts b/packages/app/src/cli/services/app-security/result.test.ts similarity index 60% rename from packages/app/src/cli/services/app-security-json-output.test.ts rename to packages/app/src/cli/services/app-security/result.test.ts index f8ecce4c221..6d72e1fea5f 100644 --- a/packages/app/src/cli/services/app-security-json-output.test.ts +++ b/packages/app/src/cli/services/app-security/result.test.ts @@ -1,9 +1,4 @@ import { - appSecurityAppSchema, - appSecurityBlockingSchema, - appSecurityInstructionsSchema, - appSecurityScanDirectorySchema, - appSecurityScopeSchema, projectAppSecurityApp, projectAppSecurityBlocking, projectAppSecurityInstructions, @@ -11,10 +6,10 @@ import { projectAppSecurityRelativePath, projectAppSecurityScanDirectory, projectAppSecurityScope, -} from './app-security-json-output.js' +} from './result.js' import {joinPath} from '@shopify/cli-kit/node/path' import {describe, expect, test} from 'vitest' -import type {AppSecuritySelection} from './app-security-selection.js' +import type {AppSecuritySelection} from '../app-security-selection.js' // Selections hold the app directory's real path and paths joined from it with forward slashes, even on Windows. const onWindows = process.platform === 'win32' @@ -91,24 +86,6 @@ describe('projectAppSecurityApp', () => { }) }) -describe('appSecurityAppSchema', () => { - const app = {directory: appDirectory, configurationPath: nativeConfigurationPath, clientId: 'toml-client-id'} - - test('accepts a projected app', () => { - expect(appSecurityAppSchema.parse(app)).toStrictEqual(app) - }) - - test.each([ - ['an extra field', {...app, name: 'My app'}], - ['a missing field', {directory: app.directory, clientId: app.clientId}], - ['a relative directory', {...app, directory: 'my-app'}], - ['a relative configuration path', {...app, configurationPath: 'shopify.app.toml'}], - ['an empty client ID', {...app, clientId: ''}], - ])('rejects %s', (_description, value) => { - expect(appSecurityAppSchema.safeParse(value).success).toBe(false) - }) -}) - describe('projectAppSecurityInstructions', () => { test.each([ ['printed', {copy: false}, false], @@ -121,23 +98,6 @@ describe('projectAppSecurityInstructions', () => { }) }) -describe('appSecurityInstructionsSchema', () => { - const instructions = {content: '# Instructions', copied: true} - - test('accepts projected instructions', () => { - expect(appSecurityInstructionsSchema.parse(instructions)).toStrictEqual(instructions) - }) - - test.each([ - ['an extra field', {...instructions, path: '/apps/my-app/instructions.md'}], - ['a missing field', {content: instructions.content}], - ['empty content', {...instructions, content: ''}], - ['a copied flag that is not a boolean', {...instructions, copied: 'true'}], - ])('rejects %s', (_description, value) => { - expect(appSecurityInstructionsSchema.safeParse(value).success).toBe(false) - }) -}) - describe('projectAppSecurityScope', () => { test('keeps the values exactly as typed', () => { expect( @@ -146,21 +106,6 @@ describe('projectAppSecurityScope', () => { }) }) -describe('appSecurityScopeSchema', () => { - const scope = {includeDirs: ['../backend'], excludes: [], noGitIgnore: false} - - test('accepts a projected scope', () => { - expect(appSecurityScopeSchema.parse(scope)).toStrictEqual(scope) - }) - - test.each([ - ['an extra field', {...scope, include_dirs: []}], - ['a missing field', {includeDirs: [], excludes: []}], - ])('rejects %s', (_description, value) => { - expect(appSecurityScopeSchema.safeParse(value).success).toBe(false) - }) -}) - describe('projectAppSecurityScanDirectory', () => { test.each([ ['app_directory', '.', 'app-directory', appDirectory], @@ -188,22 +133,6 @@ describe('projectAppSecurityScanDirectory', () => { }) }) -describe('appSecurityScanDirectorySchema', () => { - const scanDirectory = {directory: appDirectory, origin: 'app-directory'} - - test('accepts a projected scan directory', () => { - expect(appSecurityScanDirectorySchema.parse(scanDirectory)).toStrictEqual(scanDirectory) - }) - - test.each([ - ['an extra field', {...scanDirectory, path: appDirectory}], - ['a relative directory', {...scanDirectory, directory: '.'}], - ['a stored origin', {...scanDirectory, origin: 'app_directory'}], - ])('rejects %s', (_description, value) => { - expect(appSecurityScanDirectorySchema.safeParse(value).success).toBe(false) - }) -}) - describe('projectAppSecurityBlocking', () => { test.each([ ['high', 2], @@ -212,18 +141,3 @@ describe('projectAppSecurityBlocking', () => { expect(projectAppSecurityBlocking({level, blockedChecks})).toStrictEqual({level, blockedCheckCount: blockedChecks}) }) }) - -describe('appSecurityBlockingSchema', () => { - test.each(['high', 'medium', 'low', 'none'])('accepts the %s level', (level) => { - expect(appSecurityBlockingSchema.parse({level, blockedCheckCount: 0})).toStrictEqual({level, blockedCheckCount: 0}) - }) - - test.each([ - ['an extra field', {level: 'high', blockedCheckCount: 1, blockedChecks: 1}], - ['an unknown level', {level: 'critical', blockedCheckCount: 1}], - ['a negative count', {level: 'high', blockedCheckCount: -1}], - ['a fractional count', {level: 'high', blockedCheckCount: 0.5}], - ])('rejects %s', (_description, value) => { - expect(appSecurityBlockingSchema.safeParse(value).success).toBe(false) - }) -}) diff --git a/packages/app/src/cli/services/app-security/result.ts b/packages/app/src/cli/services/app-security/result.ts new file mode 100644 index 00000000000..0e6cb30a01e --- /dev/null +++ b/packages/app/src/cli/services/app-security/result.ts @@ -0,0 +1,74 @@ +import {effectiveClientId, type AppSecuritySelection} from '../app-security-selection.js' +import {resolvePath} from '@shopify/cli-kit/node/path' +// eslint-disable-next-line no-restricted-imports -- JSON file paths use native platform separators. +import {resolve} from 'node:path' +import type { + AppSecurityApp, + AppSecurityBlocking, + AppSecurityInstructions, + AppSecurityScanDirectory, + AppSecurityScope, +} from './types.js' +import type {AppSecurityBlockingLevel} from '../app-security-api.js' +import type {AppSecurityScope as StoredAppSecurityScope, CoverageScanDirectory} from '../app-security-engine/index.js' +import type {AppSecurityInstructionsDelivery} from '../app-security-instructions.js' + +/** + * The projections from the app security commands' data to the shared parts of their JSON output, in types.ts. Some + * of that data has the stored results files' own format, from app-security-engine/results/schema.ts: the projections + * convert it, so that format never appears in the public contract. + */ + +const SCAN_DIRECTORY_ORIGINS: Record = { + app_directory: 'app-directory', + include_dir: 'include-dir', + app_config_directory: 'app-config-directory', +} + +/** CLI Kit joins paths with forward slashes, even on Windows. JSON output shows them absolute, with native separators. */ +export function projectAppSecurityPath(path: string): string { + return resolve(path) +} + +/** Projects a path that is relative to the app directory, as results files record them. An absolute path stays as it is. */ +export function projectAppSecurityRelativePath(appDirectory: string, path: string): string { + return projectAppSecurityPath(resolvePath(appDirectory, path)) +} + +export function projectAppSecurityApp(selection: AppSecuritySelection): AppSecurityApp { + return { + directory: projectAppSecurityPath(selection.appDirectory), + configurationPath: selection.kind === 'config' ? projectAppSecurityPath(selection.appConfigFilePath) : null, + clientId: effectiveClientId(selection) ?? null, + } +} + +/** Projects delivered instructions: `delivery.copy` means that they're on the clipboard. */ +export function projectAppSecurityInstructions( + instructions: string, + delivery: AppSecurityInstructionsDelivery, +): AppSecurityInstructions { + return {content: instructions, copied: delivery.copy} +} + +export function projectAppSecurityScope(scope: StoredAppSecurityScope): AppSecurityScope { + return {includeDirs: scope.include_dirs, excludes: scope.excludes, noGitIgnore: scope.no_git_ignore} +} + +/** Projects a scan directory, either absolute or relative to the app directory as results files record them. */ +export function projectAppSecurityScanDirectory( + appDirectory: string, + scanDirectory: CoverageScanDirectory, +): AppSecurityScanDirectory { + return { + directory: projectAppSecurityRelativePath(appDirectory, scanDirectory.directory), + origin: SCAN_DIRECTORY_ORIGINS[scanDirectory.origin], + } +} + +export function projectAppSecurityBlocking(blocking: { + level: AppSecurityBlockingLevel + blockedChecks: number +}): AppSecurityBlocking { + return {level: blocking.level, blockedCheckCount: blocking.blockedChecks} +} diff --git a/packages/app/src/cli/services/app-security/types.test.ts b/packages/app/src/cli/services/app-security/types.test.ts new file mode 100644 index 00000000000..bd396961309 --- /dev/null +++ b/packages/app/src/cli/services/app-security/types.test.ts @@ -0,0 +1,93 @@ +import { + appSecurityAppSchema, + appSecurityBlockingSchema, + appSecurityInstructionsSchema, + appSecurityScanDirectorySchema, + appSecurityScopeSchema, +} from './types.js' +import {describe, expect, test} from 'vitest' + +const onWindows = process.platform === 'win32' +const appDirectory = onWindows ? 'C:\\apps\\my-app' : '/apps/my-app' +const nativeConfigurationPath = onWindows ? 'C:\\apps\\my-app\\shopify.app.toml' : '/apps/my-app/shopify.app.toml' + +describe('appSecurityAppSchema', () => { + const app = {directory: appDirectory, configurationPath: nativeConfigurationPath, clientId: 'toml-client-id'} + + test('accepts a projected app', () => { + expect(appSecurityAppSchema.parse(app)).toStrictEqual(app) + }) + + test.each([ + ['an extra field', {...app, name: 'My app'}], + ['a missing field', {directory: app.directory, clientId: app.clientId}], + ['a relative directory', {...app, directory: 'my-app'}], + ['a relative configuration path', {...app, configurationPath: 'shopify.app.toml'}], + ['an empty client ID', {...app, clientId: ''}], + ])('rejects %s', (_description, value) => { + expect(appSecurityAppSchema.safeParse(value).success).toBe(false) + }) +}) + +describe('appSecurityInstructionsSchema', () => { + const instructions = {content: '# Instructions', copied: true} + + test('accepts projected instructions', () => { + expect(appSecurityInstructionsSchema.parse(instructions)).toStrictEqual(instructions) + }) + + test.each([ + ['an extra field', {...instructions, path: '/apps/my-app/instructions.md'}], + ['a missing field', {content: instructions.content}], + ['empty content', {...instructions, content: ''}], + ['a copied flag that is not a boolean', {...instructions, copied: 'true'}], + ])('rejects %s', (_description, value) => { + expect(appSecurityInstructionsSchema.safeParse(value).success).toBe(false) + }) +}) + +describe('appSecurityScopeSchema', () => { + const scope = {includeDirs: ['../backend'], excludes: [], noGitIgnore: false} + + test('accepts a projected scope', () => { + expect(appSecurityScopeSchema.parse(scope)).toStrictEqual(scope) + }) + + test.each([ + ['an extra field', {...scope, include_dirs: []}], + ['a missing field', {includeDirs: [], excludes: []}], + ])('rejects %s', (_description, value) => { + expect(appSecurityScopeSchema.safeParse(value).success).toBe(false) + }) +}) + +describe('appSecurityScanDirectorySchema', () => { + const scanDirectory = {directory: appDirectory, origin: 'app-directory'} + + test('accepts a projected scan directory', () => { + expect(appSecurityScanDirectorySchema.parse(scanDirectory)).toStrictEqual(scanDirectory) + }) + + test.each([ + ['an extra field', {...scanDirectory, path: appDirectory}], + ['a relative directory', {...scanDirectory, directory: '.'}], + ['a stored origin', {...scanDirectory, origin: 'app_directory'}], + ])('rejects %s', (_description, value) => { + expect(appSecurityScanDirectorySchema.safeParse(value).success).toBe(false) + }) +}) + +describe('appSecurityBlockingSchema', () => { + test.each(['high', 'medium', 'low', 'none'])('accepts the %s level', (level) => { + expect(appSecurityBlockingSchema.parse({level, blockedCheckCount: 0})).toStrictEqual({level, blockedCheckCount: 0}) + }) + + test.each([ + ['an extra field', {level: 'high', blockedCheckCount: 1, blockedChecks: 1}], + ['an unknown level', {level: 'critical', blockedCheckCount: 1}], + ['a negative count', {level: 'high', blockedCheckCount: -1}], + ['a fractional count', {level: 'high', blockedCheckCount: 0.5}], + ])('rejects %s', (_description, value) => { + expect(appSecurityBlockingSchema.safeParse(value).success).toBe(false) + }) +}) diff --git a/packages/app/src/cli/services/app-security/types.ts b/packages/app/src/cli/services/app-security/types.ts new file mode 100644 index 00000000000..2fb1d4b3322 --- /dev/null +++ b/packages/app/src/cli/services/app-security/types.ts @@ -0,0 +1,82 @@ +import {isAbsolutePath} from '@shopify/cli-kit/node/path' +import {zod} from '@shopify/cli-kit/node/schema' + +/** + * The parts of the app security commands' JSON output that several commands share. This is the public contract: the + * stored results files have their own format, in app-security-engine/results/schema.ts, and never appear in it. + */ + +/** + * An absolute path with the platform's native separators. A function, so each field gets its own instance: the JSON + * Schema would render a shared instance as a `$ref` into the first field that uses it. + */ +export const createAbsolutePathSchema = () => + zod.string().refine(isAbsolutePath, 'Expected an absolute native filesystem path') + +/** The app the command worked on. */ +export const appSecurityAppSchema = zod + .object({ + directory: createAbsolutePathSchema().describe('The absolute app directory.'), + configurationPath: createAbsolutePathSchema() + .nullable() + .describe('The absolute path of the selected app configuration file, or null with --without-app-config.'), + clientId: zod + .string() + .min(1) + .nullable() + .describe( + "The --client-id value when passed, otherwise the app configuration's client_id. Null when the app configuration isn't linked.", + ), + }) + .strict() + +export type AppSecurityApp = zod.infer + +/** The coding-agent instructions, which a JSON result includes instead of printing them. */ +export const appSecurityInstructionsSchema = zod + .object({ + content: zod.string().min(1).describe('The instructions in Markdown, as printed without --json.'), + copied: zod.boolean().describe('Whether the instructions were also copied to the clipboard.'), + }) + .strict() + +export type AppSecurityInstructions = zod.infer + +export const appSecurityScopeSchema = zod + .object({ + includeDirs: zod.array(zod.string()).describe('The --include-dir values.'), + excludes: zod.array(zod.string()).describe('The --exclude globs.'), + noGitIgnore: zod.boolean().describe('Whether --no-git-ignore was passed.'), + }) + .strict() + .describe('The check flags that chose which files were gathered, exactly as typed.') + +export type AppSecurityScope = zod.infer + +export const appSecurityScanDirectorySchema = zod + .object({ + directory: createAbsolutePathSchema().describe('The absolute path of the directory.'), + origin: zod + .enum(['app-directory', 'include-dir', 'app-config-directory']) + .describe( + 'Why it was scanned: it is the app directory, an --include-dir value, or an extension or web directory of the app configuration.', + ), + }) + .strict() + .describe('A directory that check scanned.') + +export type AppSecurityScanDirectory = zod.infer + +export const appSecurityBlockingSchema = zod + .object({ + level: zod.enum(['high', 'medium', 'low', 'none']).describe('The --blocking severity.'), + blockedCheckCount: zod + .number() + .int() + .nonnegative() + .describe('The number of checks with findings at or above that severity. Always 0 for none.'), + }) + .strict() + .describe('The --blocking policy and the checks that fail it.') + +export type AppSecurityBlocking = zod.infer diff --git a/packages/app/src/cli/services/security-check/result.ts b/packages/app/src/cli/services/security-check/result.ts index 9c76be5d605..649bd5fd200 100644 --- a/packages/app/src/cli/services/security-check/result.ts +++ b/packages/app/src/cli/services/security-check/result.ts @@ -14,7 +14,7 @@ import { projectAppSecurityRelativePath, projectAppSecurityScanDirectory, projectAppSecurityScope, -} from '../app-security-json-output.js' +} from '../app-security/result.js' import { formatAppSecurityCommand, quoteShellArgument, diff --git a/packages/app/src/cli/services/security-check/types.ts b/packages/app/src/cli/services/security-check/types.ts index 9fb0ad3fe4d..6ae9fddd74f 100644 --- a/packages/app/src/cli/services/security-check/types.ts +++ b/packages/app/src/cli/services/security-check/types.ts @@ -5,7 +5,7 @@ import { appSecurityScanDirectorySchema, appSecurityScopeSchema, createAbsolutePathSchema, -} from '../app-security-json-output.js' +} from '../app-security/types.js' import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' import {zod} from '@shopify/cli-kit/node/schema' diff --git a/packages/app/src/cli/services/security-clean/result.ts b/packages/app/src/cli/services/security-clean/result.ts index a94b2941b16..6bcbfe0602a 100644 --- a/packages/app/src/cli/services/security-clean/result.ts +++ b/packages/app/src/cli/services/security-clean/result.ts @@ -1,6 +1,6 @@ import {appSecurityCleanJsonOutputSchema, type AppSecurityCleanResult} from './types.js' import {appSecurityDirectory} from '../app-security-artifacts.js' -import {projectAppSecurityApp, projectAppSecurityPath} from '../app-security-json-output.js' +import {projectAppSecurityApp, projectAppSecurityPath} from '../app-security/result.js' import {outputResult} from '@shopify/cli-kit/node/output' import {renderInfo, renderSuccess} from '@shopify/cli-kit/node/ui' import type {SecurityCleanOptions, SecurityCleanResult} from '../security-clean.js' diff --git a/packages/app/src/cli/services/security-clean/types.ts b/packages/app/src/cli/services/security-clean/types.ts index e713f309719..d8902991433 100644 --- a/packages/app/src/cli/services/security-clean/types.ts +++ b/packages/app/src/cli/services/security-clean/types.ts @@ -1,4 +1,4 @@ -import {appSecurityAppSchema, createAbsolutePathSchema} from '../app-security-json-output.js' +import {appSecurityAppSchema, createAbsolutePathSchema} from '../app-security/types.js' import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' import {zod} from '@shopify/cli-kit/node/schema' diff --git a/packages/app/src/cli/services/security-instructions/result.ts b/packages/app/src/cli/services/security-instructions/result.ts index 8bd56e01ca3..642bb669ee9 100644 --- a/packages/app/src/cli/services/security-instructions/result.ts +++ b/packages/app/src/cli/services/security-instructions/result.ts @@ -1,11 +1,7 @@ import {appSecurityInstructionsJsonOutputSchema, type AppSecurityInstructionsResult} from './types.js' import {appSecurityArtifactPaths} from '../app-security-artifacts.js' import {renderAppSecurityInstructions, type AppSecurityInstructionsDelivery} from '../app-security-instructions.js' -import { - projectAppSecurityApp, - projectAppSecurityInstructions, - projectAppSecurityPath, -} from '../app-security-json-output.js' +import {projectAppSecurityApp, projectAppSecurityInstructions, projectAppSecurityPath} from '../app-security/result.js' import {resultsKey, type AppSecuritySelection} from '../app-security-selection.js' import {outputResult} from '@shopify/cli-kit/node/output' diff --git a/packages/app/src/cli/services/security-instructions/types.ts b/packages/app/src/cli/services/security-instructions/types.ts index 01802bc3374..b65c42249c4 100644 --- a/packages/app/src/cli/services/security-instructions/types.ts +++ b/packages/app/src/cli/services/security-instructions/types.ts @@ -1,8 +1,4 @@ -import { - appSecurityAppSchema, - appSecurityInstructionsSchema, - createAbsolutePathSchema, -} from '../app-security-json-output.js' +import {appSecurityAppSchema, appSecurityInstructionsSchema, createAbsolutePathSchema} from '../app-security/types.js' import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' import {zod} from '@shopify/cli-kit/node/schema' diff --git a/packages/app/src/cli/services/security-record/result.ts b/packages/app/src/cli/services/security-record/result.ts index 463ea5cdb8c..cdf4c6c9c8c 100644 --- a/packages/app/src/cli/services/security-record/result.ts +++ b/packages/app/src/cli/services/security-record/result.ts @@ -2,7 +2,7 @@ import {appSecurityRecordJsonOutputSchema, type AppSecurityRecordResult} from '. import {appSecurityArtifactPaths} from '../app-security-artifacts.js' import {formatAppSecurityCommand, resolveAppSecurityCommands} from '../app-security-commands.js' import {countLabel} from '../app-security-format.js' -import {projectAppSecurityApp, projectAppSecurityPath} from '../app-security-json-output.js' +import {projectAppSecurityApp, projectAppSecurityPath} from '../app-security/result.js' import {resultsKey} from '../app-security-selection.js' import {outputResult} from '@shopify/cli-kit/node/output' import {renderSuccess} from '@shopify/cli-kit/node/ui' diff --git a/packages/app/src/cli/services/security-record/types.ts b/packages/app/src/cli/services/security-record/types.ts index 9646ddd5ac7..4485eabac18 100644 --- a/packages/app/src/cli/services/security-record/types.ts +++ b/packages/app/src/cli/services/security-record/types.ts @@ -1,4 +1,4 @@ -import {appSecurityAppSchema, createAbsolutePathSchema} from '../app-security-json-output.js' +import {appSecurityAppSchema, createAbsolutePathSchema} from '../app-security/types.js' import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' import {zod} from '@shopify/cli-kit/node/schema' diff --git a/packages/app/src/cli/services/security-review/result.ts b/packages/app/src/cli/services/security-review/result.ts index 4e2cca07655..6ca399cf558 100644 --- a/packages/app/src/cli/services/security-review/result.ts +++ b/packages/app/src/cli/services/security-review/result.ts @@ -6,16 +6,14 @@ import { projectAppSecurityRelativePath, projectAppSecurityScanDirectory, projectAppSecurityScope, -} from '../app-security-json-output.js' +} from '../app-security/result.js' import {renderSecurityReview} from '../security-review-output.js' import {formatJsonOutputTimestamp, jsonOutputTimestampSchema} from '@shopify/cli-kit/node/json-output-schema' import {outputResult} from '@shopify/cli-kit/node/output' import type { - CheckPrecedence, CombinedCheck, CombinedFinding, DeterministicFindingsDocument, - Severity, SourceCheckResult, StoredCheckStatus, StoredEvidence, @@ -25,7 +23,6 @@ import type {ReviewedAppSecurityResults, SecurityReviewResult} from '../security type AppSecurityCheck = AppSecurityReviewResult['checks'][number] type AppSecurityFinding = AppSecurityCheck['findings'][number] -type AppSecurityAgentFinding = Extract type AppSecuritySourceResult = NonNullable type AppSecurityLocation = AppSecurityFinding['location'] type AppSecurityDetection = NonNullable @@ -36,28 +33,15 @@ type StoredDetection = DeterministicFindingsDocument['detection'] type StoredCoverage = DeterministicFindingsDocument['coverage'] /* - * The stored values of each closed enum, mapped to their public values. A new stored value fails to compile until - * it is mapped. + * The closed enums whose stored values have a different public spelling, each stored value mapped to its public one. + * A new stored value fails to compile until it is mapped. Enums with the same spelling are assigned directly, and + * type-check rejects a stored value that the public schema doesn't allow. */ -const SEVERITIES: Record = {high: 'high', medium: 'medium', low: 'low'} const CHECK_STATUSES: Record = { executed: 'executed', not_applicable: 'not-applicable', unresolved: 'unresolved', } -const PRECEDENCES: Record = { - union: 'union', - 'prefer-agent': 'prefer-agent', -} -const DISPOSITIONS: Record = { - active: 'active', - suppressed: 'suppressed', - superseded: 'superseded', -} -const CONFIDENCES: Record< - NonNullable, - NonNullable -> = {high: 'high', medium: 'medium', low: 'low'} const ANALYSIS_MODES: Record< NonNullable, NonNullable @@ -75,10 +59,6 @@ const SURFACES: Record = {supported: 'supported', unsupported: 'unsupported'} const SKIPPED_FILE_REASONS: Record< StoredCoverage['files_skipped'][number]['reason'], AppSecurityCoverage['filesSkipped'][number]['reason'] @@ -185,7 +165,7 @@ function projectDetection(detection: StoredDetection, appDirectory: string): App surface: SURFACES[detection.surface], languages: detection.languages.map((language) => ({ name: language.name, - support: LANGUAGE_SUPPORT[language.support], + support: language.support, files: language.files.map((file) => ({path: projectAppSecurityRelativePath(appDirectory, file)})), })), } @@ -216,12 +196,12 @@ function projectCheck(check: CombinedCheck, appDirectory: string): AppSecurityCh return { id: check.id, title: check.title, - severity: SEVERITIES[check.severity], + severity: check.severity, description: check.description, guide: projectOptionalText(check.guide), docsUrl: check.docs_url ?? null, - precedence: PRECEDENCES[check.precedence], - appliedPrecedence: PRECEDENCES[check.applied_precedence], + precedence: check.precedence, + appliedPrecedence: check.applied_precedence, status: CHECK_STATUSES[check.status], bySource: { deterministic: check.by_source.deterministic ? projectSourceResult(check.by_source.deterministic) : null, @@ -244,7 +224,7 @@ function projectSourceResult(result: SourceCheckResult): AppSecuritySourceResult /** Each source's finding has only its own fields: the stored format allows the other source's too. */ function projectFinding(finding: CombinedFinding, appDirectory: string): AppSecurityFinding { const common = { - disposition: DISPOSITIONS[finding.disposition], + disposition: finding.disposition, location: projectLocation(finding.location, appDirectory), message: finding.message, snippet: projectOptionalText(finding.snippet), @@ -266,7 +246,7 @@ function projectFinding(finding: CombinedFinding, appDirectory: string): AppSecu return { source: 'agent', ...common, - confidence: finding.confidence ? CONFIDENCES[finding.confidence] : null, + confidence: finding.confidence ?? null, reasoning: projectOptionalText(finding.reasoning), suppression: finding.suppression ? {justification: finding.suppression.justification} : null, } diff --git a/packages/app/src/cli/services/security-review/types.ts b/packages/app/src/cli/services/security-review/types.ts index 1c671f5322d..b9f93e72dd4 100644 --- a/packages/app/src/cli/services/security-review/types.ts +++ b/packages/app/src/cli/services/security-review/types.ts @@ -4,7 +4,7 @@ import { appSecurityScanDirectorySchema, appSecurityScopeSchema, createAbsolutePathSchema, -} from '../app-security-json-output.js' +} from '../app-security/types.js' import { defineJsonOutputSchema, jsonOutputTimestampSchema,