d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
300 lines
9.4 KiB
TypeScript
300 lines
9.4 KiB
TypeScript
import { createEnvMock, envFlagsMock } from '@sim/testing'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
vi.mock('@/lib/core/config/env', () =>
|
|
createEnvMock({
|
|
NEXT_PUBLIC_APP_URL: 'https://example.com',
|
|
NEXT_PUBLIC_SOCKET_URL: 'https://socket.example.com',
|
|
OLLAMA_URL: 'http://localhost:11434',
|
|
S3_BUCKET_NAME: 'test-bucket',
|
|
AWS_REGION: 'us-east-1',
|
|
S3_KB_BUCKET_NAME: 'test-kb-bucket',
|
|
S3_CHAT_BUCKET_NAME: 'test-chat-bucket',
|
|
NEXT_PUBLIC_BRAND_LOGO_URL: 'https://brand.example.com/logo.png',
|
|
NEXT_PUBLIC_BRAND_FAVICON_URL: 'https://brand.example.com/favicon.ico',
|
|
NEXT_PUBLIC_PRIVACY_URL: 'https://legal.example.com/privacy',
|
|
NEXT_PUBLIC_TERMS_URL: 'https://legal.example.com/terms',
|
|
})
|
|
)
|
|
|
|
vi.mock('@/lib/core/config/env-flags', () => envFlagsMock)
|
|
|
|
import {
|
|
addCSPSource,
|
|
buildCSPString,
|
|
buildTimeCSPDirectives,
|
|
type CSPDirectives,
|
|
generateRuntimeCSP,
|
|
getChatEmbedCSPPolicy,
|
|
getMainCSPPolicy,
|
|
getWorkflowExecutionCSPPolicy,
|
|
removeCSPSource,
|
|
} from './csp'
|
|
|
|
describe('buildCSPString', () => {
|
|
it('should build CSP string from directives', () => {
|
|
const directives: CSPDirectives = {
|
|
'default-src': ["'self'"],
|
|
'script-src': ["'self'", "'unsafe-inline'"],
|
|
}
|
|
|
|
const result = buildCSPString(directives)
|
|
|
|
expect(result).toContain("default-src 'self'")
|
|
expect(result).toContain("script-src 'self' 'unsafe-inline'")
|
|
expect(result).toContain(';')
|
|
})
|
|
|
|
it('should handle empty directives', () => {
|
|
const directives: CSPDirectives = {}
|
|
const result = buildCSPString(directives)
|
|
expect(result).toBe('')
|
|
})
|
|
|
|
it('should skip empty source arrays', () => {
|
|
const directives: CSPDirectives = {
|
|
'default-src': ["'self'"],
|
|
'script-src': [],
|
|
}
|
|
|
|
const result = buildCSPString(directives)
|
|
|
|
expect(result).toContain("default-src 'self'")
|
|
expect(result).not.toContain('script-src')
|
|
})
|
|
|
|
it('should filter out empty string sources', () => {
|
|
const directives: CSPDirectives = {
|
|
'default-src': ["'self'", '', ' ', 'https://example.com'],
|
|
}
|
|
|
|
const result = buildCSPString(directives)
|
|
|
|
expect(result).toContain("default-src 'self' https://example.com")
|
|
expect(result).not.toMatch(/\s{2,}/)
|
|
})
|
|
|
|
it('should handle all directive types', () => {
|
|
const directives: CSPDirectives = {
|
|
'default-src': ["'self'"],
|
|
'script-src': ["'self'"],
|
|
'style-src': ["'self'"],
|
|
'img-src': ["'self'", 'data:'],
|
|
'media-src': ["'self'"],
|
|
'font-src': ["'self'"],
|
|
'connect-src': ["'self'"],
|
|
'frame-src': ["'none'"],
|
|
'frame-ancestors': ["'self'"],
|
|
'form-action': ["'self'"],
|
|
'base-uri': ["'self'"],
|
|
'object-src': ["'none'"],
|
|
}
|
|
|
|
const result = buildCSPString(directives)
|
|
|
|
expect(result).toContain("default-src 'self'")
|
|
expect(result).toContain("script-src 'self'")
|
|
expect(result).toContain("object-src 'none'")
|
|
})
|
|
})
|
|
|
|
describe('getMainCSPPolicy', () => {
|
|
it('should return a valid CSP policy string', () => {
|
|
const policy = getMainCSPPolicy()
|
|
|
|
expect(policy).toContain("default-src 'self'")
|
|
expect(policy).toContain('script-src')
|
|
expect(policy).toContain('style-src')
|
|
expect(policy).toContain('img-src')
|
|
})
|
|
|
|
it('should include security directives', () => {
|
|
const policy = getMainCSPPolicy()
|
|
|
|
expect(policy).toContain("object-src 'none'")
|
|
expect(policy).toContain("frame-ancestors 'self'")
|
|
expect(policy).toContain("form-action 'self'")
|
|
expect(policy).toContain("base-uri 'self'")
|
|
})
|
|
|
|
it('should include necessary external resources', () => {
|
|
const policy = getMainCSPPolicy()
|
|
|
|
expect(policy).toContain('https://fonts.googleapis.com')
|
|
expect(policy).toContain('https://fonts.gstatic.com')
|
|
expect(policy).toContain('https://*.google.com')
|
|
})
|
|
})
|
|
|
|
describe('getWorkflowExecutionCSPPolicy', () => {
|
|
it('should return permissive CSP for workflow execution', () => {
|
|
const policy = getWorkflowExecutionCSPPolicy()
|
|
|
|
expect(policy).toContain('default-src *')
|
|
expect(policy).toContain("'unsafe-inline'")
|
|
expect(policy).toContain("'unsafe-eval'")
|
|
expect(policy).toContain('connect-src *')
|
|
})
|
|
|
|
it('should be more permissive than main CSP', () => {
|
|
const mainPolicy = getMainCSPPolicy()
|
|
const execPolicy = getWorkflowExecutionCSPPolicy()
|
|
|
|
expect(execPolicy.length).toBeLessThan(mainPolicy.length)
|
|
expect(execPolicy).toContain('*')
|
|
})
|
|
})
|
|
|
|
describe('generateRuntimeCSP', () => {
|
|
it('should generate CSP with runtime environment variables', () => {
|
|
const csp = generateRuntimeCSP()
|
|
|
|
expect(csp).toContain("default-src 'self'")
|
|
expect(csp).toContain('https://example.com')
|
|
})
|
|
|
|
it('should include socket URL and WebSocket variant', () => {
|
|
const csp = generateRuntimeCSP()
|
|
|
|
expect(csp).toContain('https://socket.example.com')
|
|
expect(csp).toContain('wss://socket.example.com')
|
|
})
|
|
|
|
it('should include brand URLs', () => {
|
|
const csp = generateRuntimeCSP()
|
|
|
|
expect(csp).toContain('https://brand.example.com')
|
|
})
|
|
|
|
it('should not have excessive whitespace', () => {
|
|
const csp = generateRuntimeCSP()
|
|
|
|
expect(csp).not.toMatch(/\s{3,}/)
|
|
expect(csp.trim()).toBe(csp)
|
|
})
|
|
|
|
it('should allow blob URLs for iframe-based PDF previews', () => {
|
|
const csp = generateRuntimeCSP()
|
|
const frameSrcDirective = csp
|
|
.split('; ')
|
|
.find((directive) => directive.startsWith('frame-src '))
|
|
|
|
expect(frameSrcDirective).toBeDefined()
|
|
expect(frameSrcDirective).toContain('blob:')
|
|
})
|
|
})
|
|
|
|
describe('addCSPSource', () => {
|
|
const originalDirectives = structuredClone(buildTimeCSPDirectives)
|
|
|
|
afterEach(() => {
|
|
Object.keys(buildTimeCSPDirectives).forEach((key) => {
|
|
const k = key as keyof CSPDirectives
|
|
buildTimeCSPDirectives[k] = originalDirectives[k]
|
|
})
|
|
})
|
|
|
|
it('should add a source to an existing directive', () => {
|
|
const originalLength = buildTimeCSPDirectives['img-src']?.length || 0
|
|
|
|
addCSPSource('img-src', 'https://new-source.com')
|
|
|
|
expect(buildTimeCSPDirectives['img-src']).toContain('https://new-source.com')
|
|
expect(buildTimeCSPDirectives['img-src']?.length).toBe(originalLength + 1)
|
|
})
|
|
|
|
it('should not add duplicate sources', () => {
|
|
addCSPSource('img-src', 'https://duplicate.com')
|
|
const lengthAfterFirst = buildTimeCSPDirectives['img-src']?.length || 0
|
|
|
|
addCSPSource('img-src', 'https://duplicate.com')
|
|
|
|
expect(buildTimeCSPDirectives['img-src']?.length).toBe(lengthAfterFirst)
|
|
})
|
|
|
|
it('should create directive array if it does not exist', () => {
|
|
;(buildTimeCSPDirectives as any)['worker-src'] = undefined
|
|
|
|
addCSPSource('script-src', 'https://worker.example.com')
|
|
|
|
expect(buildTimeCSPDirectives['script-src']).toContain('https://worker.example.com')
|
|
})
|
|
})
|
|
|
|
describe('removeCSPSource', () => {
|
|
const originalDirectives = structuredClone(buildTimeCSPDirectives)
|
|
|
|
afterEach(() => {
|
|
Object.keys(buildTimeCSPDirectives).forEach((key) => {
|
|
const k = key as keyof CSPDirectives
|
|
buildTimeCSPDirectives[k] = originalDirectives[k]
|
|
})
|
|
})
|
|
|
|
it('should remove a source from an existing directive', () => {
|
|
addCSPSource('img-src', 'https://to-remove.com')
|
|
expect(buildTimeCSPDirectives['img-src']).toContain('https://to-remove.com')
|
|
|
|
removeCSPSource('img-src', 'https://to-remove.com')
|
|
|
|
expect(buildTimeCSPDirectives['img-src']).not.toContain('https://to-remove.com')
|
|
})
|
|
|
|
it('should handle removing non-existent source gracefully', () => {
|
|
const originalLength = buildTimeCSPDirectives['img-src']?.length || 0
|
|
|
|
removeCSPSource('img-src', 'https://non-existent.com')
|
|
|
|
expect(buildTimeCSPDirectives['img-src']?.length).toBe(originalLength)
|
|
})
|
|
|
|
it('should handle removing from non-existent directive gracefully', () => {
|
|
;(buildTimeCSPDirectives as any)['worker-src'] = undefined
|
|
|
|
expect(() => {
|
|
removeCSPSource('script-src', 'https://anything.com')
|
|
}).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('buildTimeCSPDirectives', () => {
|
|
it('should have all required security directives', () => {
|
|
expect(buildTimeCSPDirectives['default-src']).toBeDefined()
|
|
expect(buildTimeCSPDirectives['object-src']).toContain("'none'")
|
|
expect(buildTimeCSPDirectives['frame-ancestors']).toContain("'self'")
|
|
expect(buildTimeCSPDirectives['base-uri']).toContain("'self'")
|
|
})
|
|
|
|
it('should have self as default source', () => {
|
|
expect(buildTimeCSPDirectives['default-src']).toContain("'self'")
|
|
})
|
|
|
|
it('should allow Google fonts', () => {
|
|
expect(buildTimeCSPDirectives['style-src']).toContain('https://fonts.googleapis.com')
|
|
expect(buildTimeCSPDirectives['font-src']).toContain('https://fonts.gstatic.com')
|
|
})
|
|
|
|
it('should allow data: and blob: for images', () => {
|
|
expect(buildTimeCSPDirectives['img-src']).toContain('data:')
|
|
expect(buildTimeCSPDirectives['img-src']).toContain('blob:')
|
|
})
|
|
})
|
|
|
|
describe('getChatEmbedCSPPolicy', () => {
|
|
it('allows iframe embedding from any origin', () => {
|
|
expect(getChatEmbedCSPPolicy()).toContain('frame-ancestors *')
|
|
})
|
|
|
|
it('allows Office.js to load from Microsoft for Excel/Word add-in embedding', () => {
|
|
const policy = getChatEmbedCSPPolicy()
|
|
expect(policy).toMatch(/script-src[^;]*https:\/\/appsforoffice\.microsoft\.com/)
|
|
expect(policy).toMatch(/connect-src[^;]*https:\/\/appsforoffice\.microsoft\.com/)
|
|
})
|
|
|
|
it('does not regress object-src or base-uri restrictions', () => {
|
|
const policy = getChatEmbedCSPPolicy()
|
|
expect(policy).toContain("object-src 'none'")
|
|
expect(policy).toContain("base-uri 'self'")
|
|
})
|
|
})
|