Skip to content
Merged
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
12 changes: 10 additions & 2 deletions packages/app/src/cli/services/security-check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,8 +522,9 @@ describe('securityCheck', () => {

await securityCheck(testOptions(), dependencies)

expect(appSecurityInstructionsPrompt).toEqual({
message: 'How would you like to hand the results to your coding agent?',
expect(appSecurityInstructionsPrompt(31)).toEqual({
message:
'31 recommended agent checks available to complete your scan. How do you want to pass that prompt to your agent?',
choices: [
{label: 'Copy instructions to the clipboard', value: 'copy'},
{label: 'Print instructions to the terminal', value: 'print'},
Expand All @@ -532,6 +533,7 @@ describe('securityCheck', () => {
defaultValue: 'copy',
})
expect(dependencies.selectInstructionsDestination).toHaveBeenCalledOnce()
expect(dependencies.selectInstructionsDestination).toHaveBeenCalledWith(31)
expect(dependencies.deliverInstructions).toHaveBeenCalledWith({
appDirectory,
resultsKey: 'shopify.app',
Expand All @@ -541,6 +543,12 @@ describe('securityCheck', () => {
})
})

test('names a single recommended agent check in the singular', () => {
expect(appSecurityInstructionsPrompt(1).message).toBe(
'1 recommended agent check available to complete your scan. How do you want to pass that prompt to your agent?',
)
})

test('gives the instructions the exact scope of the run, as typed', async () => {
await inTemporaryDirectory(async (directory) => {
await mkdir(joinPath(directory, 'backend'))
Expand Down
35 changes: 23 additions & 12 deletions packages/app/src/cli/services/security-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ interface SecurityDependencies {
artifacts: {deterministicFindings: DeterministicFindingsDocument; agentChecks: AgentChecks},
): Promise<CheckArtifactPaths>
canPrompt(): boolean
selectInstructionsDestination(): Promise<AppSecurityInstructionsDestination>
selectInstructionsDestination(agentCheckCount: number): Promise<AppSecurityInstructionsDestination>
deliverInstructions(options: {
appDirectory: string
resultsKey: string
Expand All @@ -91,14 +91,18 @@ interface SecurityDependencies {
setExitCode(exitCode: number): void
}

export const appSecurityInstructionsPrompt: RenderSelectPromptOptions<AppSecurityInstructionsDestination> = {
message: 'How would you like to hand the results to your coding agent?',
choices: [
{label: 'Copy instructions to the clipboard', value: 'copy'},
{label: 'Print instructions to the terminal', value: 'print'},
{label: 'Nothing', value: 'nothing'},
],
defaultValue: 'copy',
export function appSecurityInstructionsPrompt(
agentCheckCount: number,
): RenderSelectPromptOptions<AppSecurityInstructionsDestination> {
return {
message: `${agentCheckCount} recommended agent ${agentCheckCount === 1 ? 'check' : 'checks'} available to complete your scan. How do you want to pass that prompt to your agent?`,
choices: [
{label: 'Copy instructions to the clipboard', value: 'copy'},
{label: 'Print instructions to the terminal', value: 'print'},
{label: 'Nothing', value: 'nothing'},
],
defaultValue: 'copy',
}
}

const defaultDependencies: SecurityDependencies = {
Expand All @@ -107,7 +111,8 @@ const defaultDependencies: SecurityDependencies = {
listFiles: listAppSecurityFiles,
writeArtifacts: writeCheckArtifacts,
canPrompt: terminalSupportsPrompting,
selectInstructionsDestination: () => renderSelectPrompt(appSecurityInstructionsPrompt),
selectInstructionsDestination: (agentCheckCount) =>
renderSelectPrompt(appSecurityInstructionsPrompt(agentCheckCount)),
deliverInstructions: deliverAppSecurityInstructions,
output: outputResult,
renderInfo,
Expand All @@ -122,11 +127,12 @@ async function instructionsDestination(
options: SecurityOptions,
dependencies: SecurityDependencies,
canPrompt: boolean,
agentCheckCount: number,
): Promise<AppSecurityInstructionsDestination> {
if (options.json || options.skipInstructions) return 'nothing'
if (options.yes) return 'print'
if (!canPrompt) return 'nothing'
return dependencies.selectInstructionsDestination()
return dependencies.selectInstructionsDestination(agentCheckCount)
}

function securityReportInput(
Expand Down Expand Up @@ -237,7 +243,12 @@ export default async function securityCheck(
)
}

const destination = await instructionsDestination(options, dependencies, canPrompt)
const destination = await instructionsDestination(
options,
dependencies,
canPrompt,
execution.agentChecks.checks.length,
)
if (destination !== 'nothing') {
await dependencies.deliverInstructions({
appDirectory,
Expand Down
8 changes: 4 additions & 4 deletions packages/app/src/cli/services/security-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ describe('buildSecurityAlert', () => {
expect(customSections[artifactsIndex]?.body).toEqual({
list: {
items: [
['Deterministic findings:', {filePath: '/tmp/app/.shopify/app-security/deterministic-findings.json'}],
['Agent checks:', {filePath: '/tmp/app/.shopify/app-security/agent-checks.json'}],
['Deterministic findings:', {filePath: '.shopify/app-security/deterministic-findings.json'}],
['Agent security check instructions:', {filePath: '.shopify/app-security/agent-checks.json'}],
],
},
})
Expand All @@ -202,7 +202,7 @@ describe('buildSecurityAlert', () => {
body: {
list: {
items: [
['Have your coding agent read', {filePath: '/tmp/app/.shopify/app-security/agent-checks.json'}],
['Have your coding agent run the agent checks'],
['Record the agent results with', {command: formatAppSecurityCommand(commands.record)}],
['Review the results with', {command: formatAppSecurityCommand(commands.review)}],
],
Expand Down Expand Up @@ -302,7 +302,7 @@ describe('buildSecurityAlert', () => {
expect(section(reportInput({commands}), 'Next steps')?.body).toEqual({
list: {
items: [
['Have your coding agent read', {filePath: '/tmp/app/.shopify/app-security/agent-checks.json'}],
['Have your coding agent run the agent checks'],
['Record the agent results with', {command: recordCommand}],
['Review the results with', {command: formatAppSecurityCommand(commands.review)}],
],
Expand Down
7 changes: 4 additions & 3 deletions packages/app/src/cli/services/security-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function securityBody(input: SecurityReportInput): TokenItem {

function securityNextSteps(input: SecurityReportInput): TokenItem<InlineToken>[] {
return [
['Have your coding agent read', {filePath: input.agentChecksPath}],
['Have your coding agent run the agent checks'],
['Record the agent results with', {command: formatAppSecurityCommand(input.commands.record)}],
['Review the results with', {command: formatAppSecurityCommand(input.commands.review)}],
]
Expand Down Expand Up @@ -187,13 +187,14 @@ function securityCustomSections(input: SecurityReportInput, groups: IssueGroup[]
sections.push({title: 'Coverage gaps', body: {list: {items}}})
}

const {appDirectory} = input.selection
sections.push({
title: 'Artifacts',
body: {
list: {
items: [
['Deterministic findings:', {filePath: input.deterministicFindingsPath}],
['Agent checks:', {filePath: input.agentChecksPath}],
['Deterministic findings:', {filePath: relativePath(appDirectory, input.deterministicFindingsPath)}],
['Agent security check instructions:', {filePath: relativePath(appDirectory, input.agentChecksPath)}],
],
},
},
Expand Down
Loading