Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/validate-app-security-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/app': patch
---

Validate the selected App Security configuration before showing recovery guidance for existing review artifacts.
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,40 @@ describe('app security check command boundary', () => {
})
})

test('rejects a missing config before reporting a protected trace', async () => {
await inTemporaryDirectory(async (directory) => {
const paths = appSecurityArtifactPaths(directory)
await createApp(directory)

const scan = await runCommand(['--path', directory, '--json', '--skip-instructions'])
expect(scan.exitCode).toBe(0)

await writeFile(paths.findingsPath, findingsDocument(await readReviewPack(paths.reviewPath)))
const compile = await runCommand([
'--path',
directory,
'--findings',
paths.findingsPath,
'--json',
'--skip-instructions',
])
expect(compile.exitCode).toBe(0)

const refused = await runCommand([
'--path',
directory,
'--config',
'shopify.app.dev-dashboard.json',
'--skip-instructions',
])

expect(refused.exitCode).toBe(1)
const message = errorText(refused.stderr)
expect(message).toContain("Couldn't find app configuration at")
expectMentionsPath(message, joinPath(directory, 'shopify.app.shopifyappdev-dashboardjson.toml'))
})
})

test('refuses a plain scan while default agent findings are pending', async () => {
await inTemporaryDirectory(async (directory) => {
const {nestedDirectory} = await createApp(directory)
Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/cli/services/app-security-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,9 +619,9 @@ describe('App Security CLI integration', () => {
artifactPaths: appSecurityArtifactPaths,
findingsFileExists: fileExists,
readTrace,
execute: async ({appRoot, findingsPath}) => {
execute: async ({appRoot, configFileName, findingsPath}) => {
const findings = findingsPath ? await loadAppSecurityFindings(findingsPath) : undefined
return executeAppSecurity({appRoot, findings})
return executeAppSecurity({appRoot, configFileName, findings})
},
writeArtifacts: writeAppSecurityArtifacts,
canPrompt: () => false,
Expand Down
10 changes: 8 additions & 2 deletions packages/app/src/cli/services/security-check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import type {AppSecurityExecution} from './app-security-api.js'
import type {AppSecurityInstructionsDestination} from './security-check.js'
import type {ScanResult, TraceV3} from './app-security-engine/index.js'

vi.mock('./app-security-config.js', async (importOriginal) => {
const original = await importOriginal<typeof import('./app-security-config.js')>()
// Unit tests use a fake app directory; check.integration.test.ts covers the missing-config error.
return {...original, requireSecurityConfigFileName: original.resolveSecurityConfigFileName}
})

const scan: ScanResult = {
version: '0.1.0',
timestamp: '2026-08-24T00:00:00.000Z',
Expand Down Expand Up @@ -140,7 +146,7 @@ describe('securityCheck', () => {
expect(dependencies.resolveRoot).toHaveBeenCalledWith('/tmp/unlinked-app')
expect(dependencies.execute).toHaveBeenCalledWith({
appRoot: '/tmp/unlinked-app',
configName: undefined,
configFileName: 'shopify.app.toml',
findingsPath: undefined,
})
expect(dependencies.writeArtifacts).toHaveBeenCalledWith(scanExecution, {clean: false})
Expand All @@ -165,7 +171,7 @@ describe('securityCheck', () => {

expect(dependencies.execute).toHaveBeenCalledWith({
appRoot: '/tmp/unlinked-app',
configName: 'staging',
configFileName: 'shopify.app.staging.toml',
findingsPath: undefined,
})
expect(dependencies.renderReport).toHaveBeenCalledWith(
Expand Down
17 changes: 7 additions & 10 deletions packages/app/src/cli/services/security-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
resolveAppSecurityRoot,
} from './app-security-api.js'
import {appSecurityArtifactPaths, readTrace, writeAppSecurityArtifacts} from './app-security-artifacts.js'
import {requireSecurityConfigFileName, resolveSecurityConfigFileName} from './app-security-config.js'
import {requireSecurityConfigFileName} from './app-security-config.js'
import deliverAppSecurityInstructions from './app-security-instructions.js'
import {
formatAppSecurityCommand,
Expand Down Expand Up @@ -49,7 +49,7 @@ interface SecurityDependencies {
artifactPaths(appRoot: string): ResolvedAppSecurityArtifactPaths
findingsFileExists(path: string): Promise<boolean>
readTrace(path: string): Promise<ReadTraceResult>
execute(options: {appRoot: string; configName?: string; findingsPath?: string}): Promise<AppSecurityExecution>
execute(options: {appRoot: string; configFileName: string; findingsPath?: string}): Promise<AppSecurityExecution>
writeArtifacts(
execution: AppSecurityExecution,
options: WriteAppSecurityArtifactsOptions,
Expand Down Expand Up @@ -82,13 +82,9 @@ const defaultDependencies: SecurityDependencies = {
artifactPaths: appSecurityArtifactPaths,
findingsFileExists: fileExists,
readTrace,
execute: async ({appRoot, configName, findingsPath}) => {
execute: async ({appRoot, configFileName, findingsPath}) => {
const findings = findingsPath ? await loadAppSecurityFindings(findingsPath) : undefined
return executeAppSecurity({
appRoot,
findings,
configFileName: requireSecurityConfigFileName(appRoot, configName),
})
return executeAppSecurity({appRoot, findings, configFileName})
},
writeArtifacts: writeAppSecurityArtifacts,
canPrompt: terminalSupportsPrompting,
Expand Down Expand Up @@ -156,14 +152,15 @@ export default async function securityCheck(
dependencies: SecurityDependencies = defaultDependencies,
): Promise<void> {
const appRoot = dependencies.resolveRoot(options.directory)
const commands = resolveAppSecurityCommands(appRoot, resolveSecurityConfigFileName(appRoot, options.configName))
const configFileName = requireSecurityConfigFileName(appRoot, options.configName)
const commands = resolveAppSecurityCommands(appRoot, configFileName)
if (!options.findingsPath && !options.clean) {
await assertCanStartScan(dependencies.artifactPaths(appRoot), commands, dependencies)
}

const execution = await dependencies.execute({
appRoot,
configName: options.configName,
configFileName,
findingsPath: options.findingsPath,
})
const artifacts = await dependencies.writeArtifacts(execution, {clean: options.clean})
Expand Down
Loading