Skip to content

Commit 85a2e29

Browse files
authored
Merge pull request #8825 from Shopify/jplhomer/app-security-monorepo-lockfiles
Skip lockfiles in every app security check scan directory
2 parents ce4fac6 + a0f0139 commit 85a2e29

7 files changed

Lines changed: 65 additions & 6 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@shopify/app': patch
3+
'@shopify/cli': patch
4+
---
5+
6+
Skip lockfiles in every `shopify app security check` scan directory, so a monorepo root lockfile no longer leaves the secret check unresolved

‎packages/app/src/cli/services/app-security-engine/checks/COMMITTED_SECRET.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
id: COMMITTED_SECRET
3-
version: 3
3+
version: 4
44
severity: high
55
precedence: union
66
---

‎packages/app/src/cli/services/app-security-engine/checks/embedded.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎packages/app/src/cli/services/app-security-engine/scanners/discover.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -784,8 +784,8 @@ export function findSensitiveFiles(
784784
): SourceFile[] {
785785
const paths = repositoryFiles
786786
.filter(isSensitiveFile)
787-
// Compares the whole relative path, so only the app root's own lockfiles are dropped.
788-
.filter((path) => !LOCKFILE_MANAGERS.has(path))
787+
// Matches by file name, so lockfiles in every scan directory are dropped, not only the app root's.
788+
.filter((path) => !LOCKFILE_MANAGERS.has(basename(path)))
789789
// Only the selected app configuration file is scanned, wherever another one with the same name sits.
790790
.filter((path) => !isValidFormatAppConfigurationFileName(basename(path)) || path === selectedAppConfigPath)
791791

‎packages/app/src/cli/services/app-security-engine/scanners/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ const DETERMINISTIC_CHECK_DEFINITIONS: ReadonlyArray<DeterministicCheckDefinitio
145145
configRule(insecureWebhookUrl, 2),
146146
{
147147
id: 'COMMITTED_SECRET',
148-
version: 3,
148+
version: 4,
149149
lifecycle: 'active',
150150
analysisMode: 'regex',
151151
target: 'secrets',

‎packages/app/src/cli/services/app-security-engine/tests/deterministic-rules.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('deterministic rules product contract', () => {
5050
expect(DETERMINISTIC_CHECKS.get('REQUEST_CONTROLLED_ADMIN_CONTEXT')?.version).toBe(3)
5151
expect(DETERMINISTIC_CHECKS.get('APP_PROXY_LIQUID_INJECTION')?.version).toBe(2)
5252
expect(DETERMINISTIC_CHECKS.get('INSECURE_WEBHOOK_URL')?.version).toBe(2)
53-
expect(DETERMINISTIC_CHECKS.get('COMMITTED_SECRET')?.version).toBe(3)
53+
expect(DETERMINISTIC_CHECKS.get('COMMITTED_SECRET')?.version).toBe(4)
5454
expect(DETERMINISTIC_CHECKS.get('UNAUTHENTICATED_ENDPOINT')?.version).toBe(2)
5555
})
5656

‎packages/app/src/cli/services/app-security-engine/tests/scan-directories.test.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,3 +393,56 @@ describe('the secret scan in a second repository', () => {
393393
})
394394
})
395395
})
396+
397+
describe('lockfiles in a monorepo', () => {
398+
const oversizedLockfile = `lockfileVersion: '9.0'\n${'#'.repeat(600_000)}\n`
399+
400+
function secretCheck(result: Awaited<ReturnType<typeof scanAll>>) {
401+
return result.scan.checks_executed.find((execution) => execution.id === 'COMMITTED_SECRET')
402+
}
403+
404+
test('skips the repository root lockfile when the repository root is a scan directory', async () => {
405+
const monorepo = await makeRepository({
406+
'pnpm-lock.yaml': oversizedLockfile,
407+
'apps/foo/shopify.app.toml': appConfiguration,
408+
'packages/server/app/shopify.server.ts': 'export const server = true',
409+
})
410+
commitEverything(monorepo)
411+
412+
const result = await scanAll({appDirectory: join(monorepo, 'apps', 'foo'), scanDirectories: [monorepo]})
413+
414+
expect(result.scan.files_skipped_count).toBe(0)
415+
expect(secretCheck(result)).toMatchObject({status: 'executed', version: 4})
416+
})
417+
418+
test('skips the lockfile of an include directory outside the app directory', async () => {
419+
const monorepo = await makeRepository({
420+
'apps/foo/shopify.app.toml': appConfiguration,
421+
'packages/server/package-lock.json': oversizedLockfile,
422+
'packages/server/app/shopify.server.ts': 'export const server = true',
423+
})
424+
commitEverything(monorepo)
425+
const app = join(monorepo, 'apps', 'foo')
426+
427+
const result = await scanAll({appDirectory: app, scanDirectories: [app, join(monorepo, 'packages', 'server')]})
428+
429+
expect(result.scan.files_skipped_count).toBe(0)
430+
expect(secretCheck(result)).toMatchObject({status: 'executed'})
431+
})
432+
433+
test('still reports a secret in an include directory that also has a lockfile', async () => {
434+
const monorepo = await makeRepository({
435+
'pnpm-lock.yaml': oversizedLockfile,
436+
'apps/foo/shopify.app.toml': appConfiguration,
437+
'packages/server/config.json': `{"apiSecret": "${secret}"}\n`,
438+
})
439+
commitEverything(monorepo)
440+
441+
const result = await scanAll({appDirectory: join(monorepo, 'apps', 'foo'), scanDirectories: [monorepo]})
442+
443+
const issue = result.issues.find((candidate) => candidate.id === 'COMMITTED_SECRET')
444+
expect(issue?.location.file).toBe('../../packages/server/config.json')
445+
expect(issue?.rule_version).toBe(4)
446+
expect(secretCheck(result)).toMatchObject({status: 'executed'})
447+
})
448+
})

0 commit comments

Comments
 (0)