Skip to content
Draft
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
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,19 @@ 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*
**/.pypirc
**/pip.conf
**/.netrc
**/*.pem
**/*.key
**/*.p12
**/*.pfx
126 changes: 126 additions & 0 deletions src/dockerBuildContextSecurity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
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 matchesPathSegment(pattern: string, value: string): boolean {
let previous = Array.from({ length: value.length + 1 }, (_, index) => index === 0);

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]);
}
}
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<string, boolean>();
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);
}

memo.set(key, matched);
return matched;
}

return visit(0, 0);
}

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 (matchesDockerPattern(pattern, 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);
}
});
});
13 changes: 13 additions & 0 deletions src/dockerPythonCredentialContextSecurity.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
Loading