From 8ba8711dedcc2d85ee8fca451f53fd1fa16783a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 09:15:23 +0900 Subject: [PATCH 1/4] test(supply-chain): expose Docker secret-context gap --- src/dockerBuildContextSecurity.test.ts | 90 ++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/dockerBuildContextSecurity.test.ts diff --git a/src/dockerBuildContextSecurity.test.ts b/src/dockerBuildContextSecurity.test.ts new file mode 100644 index 00000000..d86735f7 --- /dev/null +++ b/src/dockerBuildContextSecurity.test.ts @@ -0,0 +1,90 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +const dockerIgnoreRules = readFileSync(resolve(process.cwd(), '.dockerignore'), 'utf8') + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter((line) => line.length > 0 && !line.startsWith('#')); + +function escapeRegexCharacter(character: string): string { + return /[\\^$.*+?()[\]{}|]/u.test(character) ? `\\${character}` : character; +} + +function dockerPatternToRegex(pattern: string): RegExp { + let source = '^'; + let offset = 0; + while (offset < pattern.length) { + if (pattern.startsWith('**/', offset)) { + source += '(?:.*/)?'; + offset += 3; + continue; + } + if (pattern.startsWith('**', offset)) { + source += '.*'; + offset += 2; + continue; + } + if (pattern[offset] === '*') { + source += '[^/]*'; + offset += 1; + continue; + } + source += escapeRegexCharacter(pattern[offset] ?? ''); + offset += 1; + } + return new RegExp(`${source}$`, 'u'); +} + +function isExcludedFromDockerContext(path: string): boolean { + let excluded = false; + for (const rule of dockerIgnoreRules) { + const negated = rule.startsWith('!'); + const pattern = negated ? rule.slice(1) : rule; + if (dockerPatternToRegex(pattern).test(path)) { + excluded = !negated; + } + } + return excluded; +} + +describe('Docker build-context secret boundary', () => { + it('excludes local environment and package-registry credentials recursively', () => { + for (const privatePath of [ + '.env', + '.env.production', + 'demo/.env.local', + '.npmrc', + 'packages/editor/.pnpmrc', + '.yarnrc.yml', + 'nested/.netrc', + ]) { + expect(isExcludedFromDockerContext(privatePath), privatePath).toBe(true); + } + }); + + it('excludes common private-key and credential-container files recursively', () => { + for (const privatePath of [ + 'certificate.pem', + 'secrets/signing.key', + 'credentials/client.p12', + 'credentials/client.pfx', + ]) { + expect(isExcludedFromDockerContext(privatePath), privatePath).toBe(true); + } + }); + + it('keeps explicit public examples and required build inputs in context', () => { + for (const publicPath of [ + '.env.example', + 'demo/.env.example', + 'package.json', + 'pnpm-lock.yaml', + 'src/styles.css', + 'demo/App.tsx', + ]) { + expect(isExcludedFromDockerContext(publicPath), publicPath).toBe(false); + } + }); +}); From 6e3d1722b5e44470d6b249cb4b1cbad8578f1396 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 09:27:30 +0900 Subject: [PATCH 2/4] fix(supply-chain): exclude Docker build-context secrets --- .dockerignore | 14 +++++ src/dockerBuildContextSecurity.test.ts | 84 ++++++++++++++++++-------- 2 files changed, 74 insertions(+), 24 deletions(-) diff --git a/.dockerignore b/.dockerignore index 7181b19c..2aa55cfa 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,3 +4,17 @@ dist-demo coverage .git *.log + +# The standalone demo build is credential-free. Keep local configuration, +# registry credentials, and private key material out of recursive build context. +**/.env +**/.env.* +!**/.env.example +**/.npmrc +**/.pnpmrc +**/.yarnrc* +**/.netrc +**/*.pem +**/*.key +**/*.p12 +**/*.pfx diff --git a/src/dockerBuildContextSecurity.test.ts b/src/dockerBuildContextSecurity.test.ts index d86735f7..e11c3d76 100644 --- a/src/dockerBuildContextSecurity.test.ts +++ b/src/dockerBuildContextSecurity.test.ts @@ -8,33 +8,69 @@ const dockerIgnoreRules = readFileSync(resolve(process.cwd(), '.dockerignore'), .map((line) => line.trim()) .filter((line) => line.length > 0 && !line.startsWith('#')); -function escapeRegexCharacter(character: string): string { - return /[\\^$.*+?()[\]{}|]/u.test(character) ? `\\${character}` : character; -} +function matchesPathSegment(pattern: string, value: string): boolean { + let previous = Array.from({ length: value.length + 1 }, (_, index) => index === 0); -function dockerPatternToRegex(pattern: string): RegExp { - let source = '^'; - let offset = 0; - while (offset < pattern.length) { - if (pattern.startsWith('**/', offset)) { - source += '(?:.*/)?'; - offset += 3; - continue; - } - if (pattern.startsWith('**', offset)) { - source += '.*'; - offset += 2; - continue; + for (const token of pattern) { + const current = Array.from({ length: value.length + 1 }, () => false); + if (token === '*') { + current[0] = previous[0] ?? false; + for (let index = 1; index <= value.length; index += 1) { + current[index] = + (previous[index] ?? false) || (current[index - 1] ?? false); + } + } else { + for (let index = 1; index <= value.length; index += 1) { + current[index] = + (previous[index - 1] ?? false) && + (token === '?' || token === value[index - 1]); + } } - if (pattern[offset] === '*') { - source += '[^/]*'; - offset += 1; - continue; + previous = current; + } + + return previous[value.length] ?? false; +} + +function matchesDockerPattern(pattern: string, path: string): boolean { + const patternSegments = pattern.split('/').filter(Boolean); + const pathSegments = path.split('/').filter(Boolean); + + if (patternSegments.length === 1) { + const [singlePattern = ''] = patternSegments; + return pathSegments.some((segment) => + matchesPathSegment(singlePattern, segment), + ); + } + + const memo = new Map(); + function visit(patternIndex: number, pathIndex: number): boolean { + const key = `${patternIndex}:${pathIndex}`; + const cached = memo.get(key); + if (cached !== undefined) return cached; + + let matched: boolean; + if (patternIndex === patternSegments.length) { + matched = pathIndex === pathSegments.length; + } else if (patternSegments[patternIndex] === '**') { + matched = + visit(patternIndex + 1, pathIndex) || + (pathIndex < pathSegments.length && visit(patternIndex, pathIndex + 1)); + } else { + matched = + pathIndex < pathSegments.length && + matchesPathSegment( + patternSegments[patternIndex] ?? '', + pathSegments[pathIndex] ?? '', + ) && + visit(patternIndex + 1, pathIndex + 1); } - source += escapeRegexCharacter(pattern[offset] ?? ''); - offset += 1; + + memo.set(key, matched); + return matched; } - return new RegExp(`${source}$`, 'u'); + + return visit(0, 0); } function isExcludedFromDockerContext(path: string): boolean { @@ -42,7 +78,7 @@ function isExcludedFromDockerContext(path: string): boolean { for (const rule of dockerIgnoreRules) { const negated = rule.startsWith('!'); const pattern = negated ? rule.slice(1) : rule; - if (dockerPatternToRegex(pattern).test(path)) { + if (matchesDockerPattern(pattern, path)) { excluded = !negated; } } From c475a69057c3f19595bc6014329b68583169a399 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 09:14:49 +0900 Subject: [PATCH 3/4] test(supply-chain): require Python credential build-context exclusions --- src/dockerPythonCredentialContextSecurity.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/dockerPythonCredentialContextSecurity.test.ts diff --git a/src/dockerPythonCredentialContextSecurity.test.ts b/src/dockerPythonCredentialContextSecurity.test.ts new file mode 100644 index 00000000..ee60240a --- /dev/null +++ b/src/dockerPythonCredentialContextSecurity.test.ts @@ -0,0 +1,13 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +const dockerIgnore = readFileSync(resolve(process.cwd(), '.dockerignore'), 'utf8'); + +describe('Docker build-context Python credential boundary', () => { + it('excludes Python registry and installer credential files recursively', () => { + expect(dockerIgnore).toContain('**/.pypirc'); + expect(dockerIgnore).toContain('**/pip.conf'); + }); +}); From 810c5f9c086aa676668c8ce9f6ebeec43b19f629 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 09:18:43 +0900 Subject: [PATCH 4/4] fix(supply-chain): exclude Python build credentials --- .dockerignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index 2aa55cfa..792fe906 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,6 +13,8 @@ coverage **/.npmrc **/.pnpmrc **/.yarnrc* +**/.pypirc +**/pip.conf **/.netrc **/*.pem **/*.key