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
160 changes: 160 additions & 0 deletions src/services/__tests__/bundleSecurityContext.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { describe, it, expect, beforeEach } from 'vitest';
import {
BundleSecurityContext,
bundleSecurityContext,
type ChunkSecurityPolicy,
} from '../bundleSecurityContext';
import type { BundleChunk } from '../bundleOptimizer';

const makeChunk = (over: Partial<BundleChunk> = {}): BundleChunk => ({
id: 'default-id',
name: 'default-chunk',
size: 100,
priority: 'medium',
...over,
});

describe('BundleSecurityContext (#409)', () => {
let ctx: BundleSecurityContext;

beforeEach(() => {
ctx = new BundleSecurityContext();
});

describe('default policy', () => {
it('requires SRI by default', () => {
const violations = ctx.audit(makeChunk({ id: 'a', name: 'a' }));
expect(violations).toContain('Chunk "a" is missing SRI integrity attribute.');
});

it('is satisfied when an integrity hash is supplied', () => {
ctx.attachMetadata('a', {
integrity: 'sha384-abc',
trustLevel: 'verified',
});
expect(ctx.audit(makeChunk({ id: 'a', name: 'a' }))).toEqual([]);
});
});

describe('per-chunk override', () => {
it('uses per-chunk policy over default', () => {
ctx.setPolicyFor('trusted', {
trustLevel: 'trusted',
requiredIntegrity: false,
});
ctx.attachMetadata('trusted', { trustLevel: 'trusted' });
// Lax on integrity, trust level matches policy
const v = ctx.audit(makeChunk({ id: 'trusted', name: 'trusted' }));
expect(v).toEqual([]);
// Default still requires integrity for other chunks
expect(ctx.audit(makeChunk({ id: 'other', name: 'other' })).length).toBeGreaterThan(0);
});

it('falls back to untrusted when no metadata is attached', () => {
// Secure-by-default: a chunk with no metadata is treated as untrusted
const v = ctx.audit(makeChunk({ id: 'a', name: 'a' }));
expect(v.some((s) => s.includes('trust level'))).toBe(true);
});

it('detachMetadata clears the chunk security state', () => {
ctx.attachMetadata('a', { trustLevel: 'verified' });
ctx.detachMetadata('a');
expect(ctx.getMetadata('a')).toBeUndefined();
// After detach, chunk falls back to untrusted
const v = ctx.audit(makeChunk({ id: 'a', name: 'a' }));
expect(v.some((s) => s.includes('trust level'))).toBe(true);
});

it('attachMetadata overwrites prior metadata for the same id', () => {
ctx.attachMetadata('a', { trustLevel: 'verified', origin: 'https://x.example.com/a.js' });
ctx.attachMetadata('a', { trustLevel: 'trusted' });
const m = ctx.getMetadata('a')!;
expect(m.trustLevel).toBe('trusted');
expect(m.origin).toBeUndefined();
});
});

describe('origin allow-list', () => {
it('reports unknown origins', () => {
ctx.setDefaultPolicy({
trustLevel: 'verified',
requiredIntegrity: false,
allowedOrigins: [/^https:\/\/cdn\.example\.com\//],
});
ctx.attachMetadata('a', { trustLevel: 'verified' });
const v = ctx.audit(makeChunk({ id: 'a', name: 'a' }));
expect(v).toContain('Chunk "a" has no declared origin.');
});

it('passes when origin matches', () => {
ctx.setDefaultPolicy({
trustLevel: 'verified',
requiredIntegrity: false,
allowedOrigins: [/^https:\/\/cdn\.example\.com\//],
});
ctx.attachMetadata('a', {
trustLevel: 'verified',
origin: 'https://cdn.example.com/lib.js',
});
expect(ctx.audit(makeChunk({ id: 'a', name: 'a' }))).toEqual([]);
});
});

describe('size limit', () => {
it('flags oversized chunks', () => {
ctx.setDefaultPolicy({ trustLevel: 'verified', requiredIntegrity: false });
ctx.attachMetadata('a', { trustLevel: 'verified' });
const v = ctx.audit(makeChunk({ id: 'a', name: 'a', size: 600 }));
expect(v[0]).toMatch(/exceeds maximum 500KB/);
});
});

describe('trust level', () => {
it('flags chunk trust below required', () => {
ctx.setDefaultPolicy({
trustLevel: 'trusted',
requiredIntegrity: false,
});
ctx.attachMetadata('a', { trustLevel: 'untrusted' });
const v = ctx.audit(makeChunk({ id: 'a', name: 'a', size: 10 }));
expect(v).toContain(
'Chunk "a" declared trust level "untrusted" is below required "trusted".',
);
});
});

describe('auditBatch', () => {
it('returns only chunks with violations', () => {
ctx.setDefaultPolicy({ trustLevel: 'verified', requiredIntegrity: false });
ctx.attachMetadata('good', { trustLevel: 'verified' });
const audit = ctx.auditBatch([
makeChunk({ id: 'good', name: 'good', size: 50 }),
makeChunk({ id: 'bad', name: 'bad', size: 9999 }),
]);
expect(audit.has('good')).toBe(false);
expect(audit.has('bad')).toBe(true);
});
});

describe('singleton', () => {
it('exports a shared instance', () => {
expect(bundleSecurityContext).toBeInstanceOf(BundleSecurityContext);
});
});

describe('policy immutability', () => {
it('getPolicyFor returns a defensive copy', () => {
const p: ChunkSecurityPolicy = { trustLevel: 'verified', requiredIntegrity: false };
ctx.setPolicyFor('a', p);
const fetched = ctx.getPolicyFor('a')!;
fetched.requiredIntegrity = true;
expect(ctx.getPolicyFor('a')!.requiredIntegrity).toBe(false);
});

it('getEffectivePolicy returns a defensive copy', () => {
const fetched = ctx.getEffectivePolicy('a');
fetched.requiredIntegrity = false;
expect(ctx.getEffectivePolicy('a').requiredIntegrity).toBe(true);
});
});
});
26 changes: 24 additions & 2 deletions src/services/bundleOptimizer.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,44 @@
/**
* Service for bundle analysis and dynamic loading optimization.
*
* Includes a Security Context integration that enforces integrity,
* origin allow-listing, size limits and trust levels per chunk
* (see #409: Bundle Optimization: Security Context).
*/

import { bundleSecurityContext, type ChunkSecurityMetadata } from './bundleSecurityContext';

export interface BundleChunk {
id: string;
name: string;
size?: number;
priority: 'high' | 'medium' | 'low';
}

/** Optional security metadata that can be supplied when registering a chunk. */
export interface BundleChunkRegistration extends BundleChunk {
security?: ChunkSecurityMetadata;
}

class BundleOptimizer {
private chunks: Map<string, BundleChunk> = new Map();

/**
* Registers a chunk for monitoring.
* Registers a chunk for monitoring. Optional security metadata is forwarded
* to the shared BundleSecurityContext so the chunk participates in audits.
*/
registerChunk(chunk: BundleChunk) {
registerChunk(chunk: BundleChunk | BundleChunkRegistration) {
this.chunks.set(chunk.id, chunk);
const registration = chunk as BundleChunkRegistration;
if (registration.security) {
bundleSecurityContext.attachMetadata(chunk.id, registration.security);
}
}

/** Detaches and forgets a chunk. Also clears its security metadata. */
unregisterChunk(chunkId: string) {
this.chunks.delete(chunkId);
bundleSecurityContext.detachMetadata(chunkId);
}

/**
Expand Down
160 changes: 160 additions & 0 deletions src/services/bundleSecurityContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* Bundle Security Context (#409)
*
* Provides per-chunk security policies for the bundle optimizer:
* - Subresource Integrity (SRI) enforcement
* - Origin allow-listing (regex)
* - Maximum chunk size
* - Trust-level tagging
*
* The context is fully decoupled from the bundle registry so it can be
* unit-tested in isolation and consumed by any bundle-loading pipeline.
*/

import type { BundleChunk } from './bundleOptimizer';

/** Trust level required to load a chunk. */
export type TrustLevel = 'trusted' | 'verified' | 'untrusted';

/** Per-chunk security policy applied during audit. */
export interface ChunkSecurityPolicy {
/** Regex patterns; if set, chunk.origin must match at least one. */
allowedOrigins?: RegExp[];
/** When true, chunk must declare an SRI `integrity` hash. */
requiredIntegrity?: boolean;
/** Maximum size in KB the chunk is allowed to occupy. */
maxSize?: number;
/** Minimum trust level the chunk must declare. */
trustLevel: TrustLevel;
}

/** Security metadata attached to a registered chunk. */
export interface ChunkSecurityMetadata {
origin?: string;
integrity?: string;
trustLevel: TrustLevel;
}

const DEFAULT_POLICY: ChunkSecurityPolicy = {
requiredIntegrity: true,
maxSize: 500, // KB
trustLevel: 'verified',
};

/**
* Ranks trust levels so we can compare "at least" requirements.
* Lower number = higher privilege.
*/
const TRUST_RANK: Record<TrustLevel, number> = {
trusted: 0,
verified: 1,
untrusted: 2,
};

/**
* Holds the effective security policy for every registered chunk and
* exposes an `audit()` helper used by the bundle optimizer's health report.
*/
export class BundleSecurityContext {
private policies: Map<string, ChunkSecurityPolicy> = new Map();
private metadata: Map<string, ChunkSecurityMetadata> = new Map();
private defaultPolicy: ChunkSecurityPolicy = { ...DEFAULT_POLICY };

setDefaultPolicy(policy: Partial<ChunkSecurityPolicy>): void {
this.defaultPolicy = { ...DEFAULT_POLICY, ...policy };
}

getDefaultPolicy(): ChunkSecurityPolicy {
return { ...this.defaultPolicy };
}

setPolicyFor(chunkId: string, policy: ChunkSecurityPolicy): void {
this.policies.set(chunkId, policy);
}

getPolicyFor(chunkId: string): ChunkSecurityPolicy | undefined {
const p = this.policies.get(chunkId);
return p ? { ...p } : undefined;
}

attachMetadata(chunkId: string, metadata: ChunkSecurityMetadata): void {
this.metadata.set(chunkId, metadata);
}

/** Removes metadata for a chunk (called when a chunk is unregistered). */
detachMetadata(chunkId: string): void {
this.metadata.delete(chunkId);
}

getMetadata(chunkId: string): ChunkSecurityMetadata | undefined {
const m = this.metadata.get(chunkId);
return m ? { ...m } : undefined;
}

/**
* Resolves the effective policy for a chunk (per-chunk override or default).
* Default fields act as a fallback; per-chunk fields take precedence.
*/
getEffectivePolicy(chunkId: string): ChunkSecurityPolicy {
const per = this.policies.get(chunkId);
return per ? { ...this.defaultPolicy, ...per } : { ...this.defaultPolicy };
}

/**
* Audits a single chunk against its effective policy.
* Returns a list of human-readable violations (empty = compliant).
*/
audit(chunk: BundleChunk): string[] {
const policy = this.getEffectivePolicy(chunk.id);
const meta = this.metadata.get(chunk.id) ?? { trustLevel: 'untrusted' as const };
const violations: string[] = [];

if (policy.requiredIntegrity) {
const integrity = meta.integrity?.trim() ?? '';
if (integrity.length === 0) {
violations.push(`Chunk "${chunk.name}" is missing SRI integrity attribute.`);
}
}

if (policy.allowedOrigins && policy.allowedOrigins.length > 0) {
const origin = meta.origin?.trim() ?? '';
if (origin.length === 0) {
violations.push(`Chunk "${chunk.name}" has no declared origin.`);
} else if (!policy.allowedOrigins.some((re) => re.test(origin))) {
violations.push(`Chunk "${chunk.name}" origin "${origin}" is not in the allow-list.`);
}
}

if (typeof policy.maxSize === 'number' && typeof chunk.size === 'number') {
if (chunk.size > policy.maxSize) {
violations.push(
`Chunk "${chunk.name}" size ${chunk.size}KB exceeds maximum ${policy.maxSize}KB.`,
);
}
}

if (TRUST_RANK[meta.trustLevel] > TRUST_RANK[policy.trustLevel]) {
violations.push(
`Chunk "${chunk.name}" declared trust level "${meta.trustLevel}" is below required "${policy.trustLevel}".`,
);
}

return violations;
}

/**
* Audits every provided chunk. Returns the chunks that have at least one
* violation, indexed by chunk id along with their violation messages.
*/
auditBatch(chunks: BundleChunk[]): Map<string, string[]> {
const results = new Map<string, string[]>();
for (const chunk of chunks) {
const v = this.audit(chunk);
if (v.length > 0) results.set(chunk.id, v);
}
return results;
}
}

/** Shared singleton used by the bundle optimizer and webpack hooks. */
export const bundleSecurityContext = new BundleSecurityContext();
Loading