diff --git a/.github/workflows/visual-tests-demos.yml b/.github/workflows/visual-tests-demos.yml index edad31a95aa7..9fe341f26cf1 100644 --- a/.github/workflows/visual-tests-demos.yml +++ b/.github/workflows/visual-tests-demos.yml @@ -632,7 +632,6 @@ jobs: uses: ./.github/actions/setup-chrome with: chrome-version: '145.0.7632.67' - runner-type: 'github-hosted' - name: Use Node.js uses: actions/setup-node@v4 @@ -787,7 +786,6 @@ jobs: uses: ./.github/actions/setup-chrome with: chrome-version: '145.0.7632.67' - runner-type: 'github-hosted' - name: Use Node.js uses: actions/setup-node@v4 @@ -916,7 +914,6 @@ jobs: uses: ./.github/actions/setup-chrome with: chrome-version: '145.0.7632.67' - runner-type: 'github-hosted' - name: Use Node.js uses: actions/setup-node@v4 @@ -1069,3 +1066,221 @@ jobs: pattern: accessibility-reports-* delete-merged: true + csp-check-jquery: + name: CSP check (jQuery) + needs: [check-should-run, build-devextreme] + if: | + always() && + needs.check-should-run.outputs.should-run == 'true' && + needs.build-devextreme.result == 'success' + runs-on: devextreme-shr2 + timeout-minutes: 60 + + steps: + - name: Get sources + uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: devextreme-artifacts-jquery + path: ./packages/devextreme + + - name: Unpack artifacts + working-directory: ./packages/devextreme + run: 7z x artifacts.zip -aoa + + - name: Setup Chrome + uses: ./.github/actions/setup-chrome + with: + chrome-version: '145.0.7632.67' + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache/restore@v4 + name: Restore pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-cache-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-cache + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Start CSP Server + run: node apps/demos/utils/server/csp-server.js 8080 & + + - name: Run CSP Check + working-directory: apps/demos + env: + CSP_FRAMEWORKS: jQuery + CHROME_PATH: google-chrome-stable + run: node utils/server/csp-check.js + + - name: Upload CSP report + if: always() + uses: actions/upload-artifact@v4 + with: + name: csp-violations-jquery + path: apps/demos/csp-reports/ + if-no-files-found: ignore + + csp-check-frameworks: + name: CSP check (${{ matrix.FRAMEWORK }}) + needs: [check-should-run, determine-framework-tests-scope, build-devextreme] + if: | + always() && + needs.check-should-run.outputs.should-run == 'true' && + needs.determine-framework-tests-scope.result == 'success' && + needs.determine-framework-tests-scope.outputs.framework-tests-scope != 'none' && + needs.build-devextreme.result == 'success' + strategy: + fail-fast: false + matrix: + FRAMEWORK: [React, Vue, Angular] + runs-on: devextreme-shr2 + timeout-minutes: 60 + + steps: + - name: Get sources + uses: actions/checkout@v4 + + - name: Download devextreme sources + uses: actions/download-artifact@v4 + with: + name: devextreme-sources + + - name: Setup Chrome + uses: ./.github/actions/setup-chrome + with: + chrome-version: '145.0.7632.67' + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache/restore@v4 + name: Restore pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-cache-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-cache + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install tgz + working-directory: apps/demos + run: pnpm add ../../devextreme-installer.tgz ../../devextreme-dist-installer.tgz ../../devextreme-react-installer.tgz ../../devextreme-vue-installer.tgz ../../devextreme-angular-installer.tgz + + - name: Start CSP Server + run: node apps/demos/utils/server/csp-server.js 8080 & + + - name: Run CSP Check + working-directory: apps/demos + env: + CSP_FRAMEWORKS: ${{ matrix.FRAMEWORK }} + CHROME_PATH: google-chrome-stable + run: node utils/server/csp-check.js + + - name: Upload CSP report + if: always() + uses: actions/upload-artifact@v4 + with: + name: csp-violations-${{ matrix.FRAMEWORK }} + path: apps/demos/csp-reports/ + if-no-files-found: ignore + + csp-report-summary: + name: CSP Violations Summary + runs-on: devextreme-shr2 + needs: [check-should-run, csp-check-jquery, csp-check-frameworks] + if: always() && needs.check-should-run.outputs.should-run == 'true' + timeout-minutes: 5 + + steps: + - name: Get sources + uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Download all CSP reports + uses: actions/download-artifact@v4 + with: + pattern: csp-violations-* + path: csp-reports-all + merge-multiple: true + continue-on-error: true + + - name: Summarize CSP violations + run: | + mkdir -p apps/demos/csp-reports + + echo "## CSP Violations Report" >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + + GRAND_TOTAL=0 + for report in csp-reports-all/csp-violations-*.jsonl; do + [ -f "$report" ] || continue + FRAMEWORK=$(basename "$report" | sed 's/csp-violations-//;s/\.jsonl//') + cp "$report" "apps/demos/csp-reports/" + + if [ -s "$report" ]; then + COUNT=$(wc -l < "$report" | tr -d ' ') + GRAND_TOTAL=$((GRAND_TOTAL + COUNT)) + echo "### ⚠️ ${FRAMEWORK}: ${COUNT} violation(s)" >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + echo '
' >> $GITHUB_STEP_SUMMARY + echo 'Show detailed report' >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + CSP_REPORT_FILE="$report" node apps/demos/utils/server/csp-report-summary.js >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + echo '
' >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + else + echo "### ✅ ${FRAMEWORK}: No violations" >> $GITHUB_STEP_SUMMARY + echo '' >> $GITHUB_STEP_SUMMARY + fi + done + + if [ "$GRAND_TOTAL" -eq 0 ]; then + echo "✅ No CSP violations detected across all frameworks." + else + echo "⚠️ Total: $GRAND_TOTAL CSP violation(s)" + fi + + - name: Upload merged CSP reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: csp-violations-report + path: apps/demos/csp-reports/ + if-no-files-found: ignore + diff --git a/apps/demos/.gitignore b/apps/demos/.gitignore index 045269d9a84b..82d6e6478557 100644 --- a/apps/demos/.gitignore +++ b/apps/demos/.gitignore @@ -33,5 +33,7 @@ Demos/**/tsconfig.json **/.DS_Store publish-demos +csp-reports + .angular angular.json diff --git a/apps/demos/Demos/DataGrid/PDFExportImages/jQuery/index.html b/apps/demos/Demos/DataGrid/PDFExportImages/jQuery/index.html index ec99855d024d..d3428ce8d006 100644 --- a/apps/demos/Demos/DataGrid/PDFExportImages/jQuery/index.html +++ b/apps/demos/Demos/DataGrid/PDFExportImages/jQuery/index.html @@ -11,10 +11,6 @@ integrity="sha512-+EeCylkt9WHJk5tGJxYdecHOcXFRME7qnbsfeMsdQL6NUPYm2+uGFmyleEqsmVoap/f3dN/sc3BX9t9kHXkHHg==" crossorigin="anonymous" > - - diff --git a/apps/demos/Demos/DataGrid/PDFExportMultipleGrids/jQuery/index.html b/apps/demos/Demos/DataGrid/PDFExportMultipleGrids/jQuery/index.html index c99a50ef7e3b..26b87f672084 100644 --- a/apps/demos/Demos/DataGrid/PDFExportMultipleGrids/jQuery/index.html +++ b/apps/demos/Demos/DataGrid/PDFExportMultipleGrids/jQuery/index.html @@ -15,9 +15,6 @@ integrity="sha512-+EeCylkt9WHJk5tGJxYdecHOcXFRME7qnbsfeMsdQL6NUPYm2+uGFmyleEqsmVoap/f3dN/sc3BX9t9kHXkHHg==" crossorigin="anonymous" > - diff --git a/apps/demos/Demos/Stepper/FormIntegration/jQuery/index.html b/apps/demos/Demos/Stepper/FormIntegration/jQuery/index.html index 808e13261fc6..7658b170789d 100644 --- a/apps/demos/Demos/Stepper/FormIntegration/jQuery/index.html +++ b/apps/demos/Demos/Stepper/FormIntegration/jQuery/index.html @@ -7,7 +7,7 @@ - + diff --git a/apps/demos/package.json b/apps/demos/package.json index 001dd0183254..7be95763bc38 100644 --- a/apps/demos/package.json +++ b/apps/demos/package.json @@ -177,7 +177,10 @@ "lint-non-demos": "pnpx nx run-many -t lint-js lint-css lint-html -p devextreme-demos", "lint": "pnpm run lint-non-demos && pnpm run lint-demos", "test-testcafe": "ts-node utils/visual-tests/testcafe-runner.ts", + "test-testcafe:csp": "cross-env CSP_REPORT=true ts-node utils/visual-tests/testcafe-runner.ts", "test-testcafe:accessibility": "cross-env STRATEGY=accessibility CONSTEL=jquery node utils/visual-tests/testcafe-runner.ts", + "csp-server": "node utils/server/csp-server.js 8080", + "csp-check": "node utils/server/csp-check.js", "fix-lint": "prettier --write . && eslint --fix . && stylelint **/*.{css,vue} --fix", "prettier": "prettier", "build-bundles": "gulp bundles", diff --git a/apps/demos/testing/common.test.ts b/apps/demos/testing/common.test.ts index 22650163014e..346cca76faae 100644 --- a/apps/demos/testing/common.test.ts +++ b/apps/demos/testing/common.test.ts @@ -1,8 +1,9 @@ import { glob } from 'glob'; import { join } from 'path'; -import { existsSync } from 'fs'; +import { existsSync, mkdirSync, appendFileSync } from 'fs'; import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; import { axeCheck, createReport } from '@testcafe-community/axe'; +import { ClientFunction } from 'testcafe'; import { runTestAtPage, shouldRunFramework, @@ -32,6 +33,28 @@ const execTestCafeCode = (t, code) => { return testCafeFunction(t); }; +const getClientCspViolations = ClientFunction(() => (window as any).__cspViolations || []); + +const isCspEnabled = () => process.env.CSP_REPORT === 'true'; + +const cspReportDir = join(__dirname, '..', 'csp-reports'); +const cspReportFile = join(cspReportDir, 'csp-violations.jsonl'); + +const writeCspReport = (testName: string, framework: string, violations: any[]) => { + if (!violations.length) return; + if (!existsSync(cspReportDir)) { + mkdirSync(cspReportDir, { recursive: true }); + } + for (const v of violations) { + const entry = { + test: testName, + framework, + ...v, + }; + appendFileSync(cspReportFile, `${JSON.stringify(entry)}\n`); + } +}; + const getIgnoredRules = (testName) => { const ignoredRules = []; @@ -103,6 +126,13 @@ const getClientScripts = () => { scripts.push({ module: 'axe-core/axe.min.js' }); } + if (isCspEnabled()) { + scripts.push( + // @ts-expect-error + join(__dirname, '../utils/visual-tests/inject/csp-listener.js'), + ); + } + scripts.push( // @ts-expect-error join(__dirname, '../utils/visual-tests/inject/test-utils.js'), @@ -225,9 +255,15 @@ Object.values(FRAMEWORKS).forEach((approach) => { } else { const consoleMessages = await t.getBrowserConsoleMessages(); const errors = [...consoleMessages.error, ...consoleMessages.warn] - .filter((e) => !knownWarnings.some((kw) => e.startsWith(kw))); + .filter((e) => !knownWarnings.some((kw) => e.startsWith(kw))) + .filter((e) => !e.startsWith('[CSP Violation]')); await t.expect(errors).eql([]); + if (isCspEnabled()) { + const cspViolations = await getClientCspViolations(); + writeCspReport(testName, approach, cspViolations); + } + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); await testScreenshot(t, takeScreenshot, `${testName}.png`, undefined, comparisonOptions); diff --git a/apps/demos/utils/server/csp-check.js b/apps/demos/utils/server/csp-check.js new file mode 100644 index 000000000000..d0928a8fb506 --- /dev/null +++ b/apps/demos/utils/server/csp-check.js @@ -0,0 +1,202 @@ +const { execFile, execFileSync } = require('child_process'); +const { join } = require('path'); +const { + readdirSync, existsSync, writeFileSync, mkdirSync, +} = require('fs'); +const http = require('http'); + +const DEMO_ROOT = join(__dirname, '..', '..'); +const REPORT_DIR = join(DEMO_ROOT, 'csp-reports'); +const SERVER_URL = process.env.CSP_SERVER_URL || 'http://localhost:8080'; +const FRAMEWORK = (process.env.CSP_FRAMEWORKS || 'jQuery').trim(); +const CONCURRENCY = parseInt(process.env.CSP_CONCURRENCY, 10) || 8; + +function findChrome() { + const candidates = [ + process.env.CHROME_PATH, + 'google-chrome-stable', + 'google-chrome', + 'chromium-browser', + 'chromium', + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/usr/bin/google-chrome-stable', + '/usr/bin/google-chrome', + ].filter(Boolean); + + for (const candidate of candidates) { + try { + if (candidate.startsWith('/')) { + execFileSync('test', ['-x', candidate], { stdio: 'ignore' }); + } else { + execFileSync('which', [candidate], { stdio: 'ignore' }); + } + return candidate; + } catch { + // try next candidate + } + } + throw new Error('Chrome not found. Set CHROME_PATH environment variable.'); +} + +const CHROME_PATH = findChrome(); + +function findDemos() { + const demosDir = join(DEMO_ROOT, 'Demos'); + const result = []; + + if (!existsSync(demosDir)) { + console.error(`Demos directory not found: ${demosDir}`); + return result; + } + + const widgets = readdirSync(demosDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()); + + for (const widget of widgets) { + const widgetDir = join(demosDir, widget.name); + let demos = []; + + demos = readdirSync(widgetDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()); + + for (const demo of demos) { + const fwDir = join(widgetDir, demo.name, FRAMEWORK); + if (existsSync(join(fwDir, 'index.html'))) { + result.push({ + url: `${SERVER_URL}/apps/demos/Demos/${widget.name}/${demo.name}/${FRAMEWORK}/`, + widget: widget.name, + demo: demo.name, + framework: FRAMEWORK, + }); + } + } + } + + return result; +} + +function visitPage(url) { + return new Promise((resolve, reject) => { + const child = execFile(CHROME_PATH, [ + '--headless=new', + '--no-sandbox', + '--disable-gpu', + '--disable-software-rasterizer', + '--disable-dev-shm-usage', + '--dump-dom', + '--virtual-time-budget=5000', + '--window-size=100,100', + url, + ], { timeout: 30000 }, () => resolve()); + child.on('error', (err) => { + reject(new Error(`Failed to launch Chrome at "${CHROME_PATH}": ${err.message}`)); + }); + }); +} + +function httpRequest(url, method) { + return new Promise((resolve, reject) => { + const req = http.request(url, { method: method || 'GET' }, (res) => { + let data = ''; + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => { + try { + resolve(JSON.parse(data)); + } catch { + resolve(data); + } + }); + }); + req.on('error', reject); + req.end(); + }); +} + +async function main() { + console.log(`Chrome: ${CHROME_PATH}`); + console.log(`Server: ${SERVER_URL}`); + console.log(`Framework: ${FRAMEWORK}`); + console.log(`Concurrency: ${CONCURRENCY}\n`); + + const demos = findDemos(); + console.log(`Found ${demos.length} demo page(s) to check\n`); + + if (demos.length === 0) { + console.log('No demos found. Exiting.'); + return; + } + + let totalViolations = 0; + let demosWithViolations = 0; + const allViolations = []; + + for (let batchStart = 0; batchStart < demos.length; batchStart += CONCURRENCY) { + const batch = demos.slice(batchStart, batchStart + CONCURRENCY); + + await httpRequest(`${SERVER_URL}/csp-violations`, 'DELETE'); + + await Promise.all(batch.map((demo) => visitPage(demo.url))); + + await new Promise((resolve) => { setTimeout(resolve, 500); }); + + const result = await httpRequest(`${SERVER_URL}/csp-violations`); + const violations = result.violations || []; + + const violationsByUrl = {}; + for (const v of violations) { + const uri = v.documentUri || ''; + if (!violationsByUrl[uri]) violationsByUrl[uri] = []; + violationsByUrl[uri].push(v); + } + + for (let j = 0; j < batch.length; j += 1) { + const demo = batch[j]; + const idx = batchStart + j + 1; + const demoViolations = violationsByUrl[demo.url] + || violationsByUrl[`${demo.url}index.html`] + || []; + + if (demoViolations.length > 0) { + demosWithViolations += 1; + totalViolations += demoViolations.length; + + console.log(` ❌ [${idx}/${demos.length}] ${demo.widget}/${demo.demo}/${demo.framework} — ${demoViolations.length} violation(s)`); + for (const v of demoViolations) { + const blocked = v.blockedUri || 'N/A'; + const directive = v.effectiveDirective || v.violatedDirective || '?'; + console.log(` ${directive}: ${blocked}`); + allViolations.push({ ...v, framework: FRAMEWORK }); + } + } else { + console.log(` ✅ [${idx}/${demos.length}] ${demo.widget}/${demo.demo}/${demo.framework}`); + } + } + } + + const reportFile = join(REPORT_DIR, `csp-violations-${FRAMEWORK.toLowerCase()}.jsonl`); + + if (allViolations.length > 0) { + mkdirSync(REPORT_DIR, { recursive: true }); + const lines = allViolations.map((v) => JSON.stringify(v)).join('\n'); + writeFileSync(reportFile, `${lines}\n`); + } + + console.log(`\n${'='.repeat(60)}`); + console.log(`Framework: ${FRAMEWORK}`); + console.log(`Demos checked: ${demos.length}`); + console.log(`Demos with violations: ${demosWithViolations}`); + console.log(`Total violations: ${totalViolations}`); + + if (totalViolations > 0) { + console.log(`\n⚠️ ${totalViolations} CSP violation(s) detected in ${demosWithViolations} demo(s)`); + console.log(`Report: ${reportFile}`); + process.exitCode = 1; + } else { + console.log('\n✅ No CSP violations detected'); + } +} + +main().catch((err) => { + console.error('CSP check failed:', err.message); + process.exit(1); +}); diff --git a/apps/demos/utils/server/csp-report-summary.js b/apps/demos/utils/server/csp-report-summary.js new file mode 100644 index 000000000000..5963db40093e --- /dev/null +++ b/apps/demos/utils/server/csp-report-summary.js @@ -0,0 +1,77 @@ +const { readFileSync, existsSync } = require('fs'); +const { join } = require('path'); + +const reportFile = process.env.CSP_REPORT_FILE || join(__dirname, '..', '..', 'csp-reports', 'csp-violations.jsonl'); + +if (!existsSync(reportFile)) { + console.log('No CSP violations report found.'); + console.log(`Expected at: ${reportFile}`); + console.log('Run tests with CSP_REPORT=true to generate a report.'); + process.exit(0); +} + +const lines = readFileSync(reportFile, 'utf-8') + .split('\n') + .filter(Boolean) + .map((line) => JSON.parse(line)); + +if (lines.length === 0) { + console.log('✅ No CSP violations detected!'); + process.exit(0); +} + +console.log(`\n⚠️ CSP Violations Report: ${lines.length} violation(s) found\n`); +console.log('='.repeat(80)); + +const byDirective = {}; +for (const v of lines) { + const key = v.violatedDirective || v.effectiveDirective || 'unknown'; + if (!byDirective[key]) { + byDirective[key] = []; + } + byDirective[key].push(v); +} + +for (const [directive, violations] of Object.entries(byDirective)) { + console.log(`\n📋 ${directive} (${violations.length} violation(s)):`); + console.log('-'.repeat(60)); + + const uniqueViolations = new Map(); + for (const v of violations) { + const key = `${v.blockedURI}|${v.sourceFile}|${v.lineNumber}`; + if (!uniqueViolations.has(key)) { + uniqueViolations.set(key, { ...v, count: 1, tests: [v.test] }); + } else { + const existing = uniqueViolations.get(key); + existing.count += 1; + if (!existing.tests.includes(v.test)) { + existing.tests.push(v.test); + } + } + } + + for (const [, v] of uniqueViolations) { + console.log(` blocked: ${v.blockedURI || 'N/A'}`); + console.log(` source: ${v.sourceFile || 'N/A'}:${v.lineNumber || '?'}`); + console.log(` count: ${v.count} occurrence(s) in ${v.tests.length} test(s)`); + if (v.tests.length <= 5) { + console.log(` tests: ${v.tests.join(', ')}`); + } else { + console.log(` tests: ${v.tests.slice(0, 5).join(', ')} ... and ${v.tests.length - 5} more`); + } + console.log(); + } +} + +const byFramework = {}; +for (const v of lines) { + const fw = v.framework || 'unknown'; + byFramework[fw] = (byFramework[fw] || 0) + 1; +} + +console.log('='.repeat(80)); +console.log('\n📊 Summary by framework:'); +for (const [fw, count] of Object.entries(byFramework)) { + console.log(` ${fw}: ${count} violation(s)`); +} +console.log(`\n Total: ${lines.length} violation(s)\n`); diff --git a/apps/demos/utils/server/csp-server.js b/apps/demos/utils/server/csp-server.js new file mode 100644 index 000000000000..48ae19c2cedf --- /dev/null +++ b/apps/demos/utils/server/csp-server.js @@ -0,0 +1,410 @@ +/* eslint-disable import/no-extraneous-dependencies */ + +const crypto = require('crypto'); +const express = require('express'); +const serveStatic = require('serve-static'); +const cookieParser = require('cookie-parser'); +const { join, resolve } = require('path'); +const { readFileSync, readdirSync } = require('fs'); + +const root = join(__dirname, '..', '..', '..', '..'); +const indexFileName = 'index.html'; +const port = process.argv[2] ?? 8080; +const host = process.env.CSP_SERVER_HOST || '127.0.0.1'; + +const cspViolations = []; +let cspViolationIdCounter = 0; + +const CSP_BASE_DIRECTIVES = { + 'default-src': ["'none'"], + 'script-src': ["'self'", 'https://esm.sh', 'https://cdnjs.cloudflare.com', 'https://cdn.jsdelivr.net'], + 'style-src': ["'self'", 'https://maxcdn.bootstrapcdn.com'], + 'img-src': ["'self'"], + 'font-src': ["'self'"], + 'connect-src': ["'self'", 'https://js.devexpress.com', 'https://demos.devexpress.com'], + 'worker-src': ["'self'"], + 'frame-src': ["'self'"], + 'object-src': ["'none'"], + 'base-uri': ["'none'"], + 'form-action': ["'self'"], + 'frame-ancestors': ["'none'"], +}; + +const CSP_DEMO_ALLOWLIST = { + 'Button/Icons': { + 'font-src': ['https://maxcdn.bootstrapcdn.com'], + }, + 'CardView/WebAPIService': { + 'img-src': ['data:'], + }, + // Azure Maps SDK: inline styles, blob workers, data: images, + // and font glyphs from atlas.microsoft.com + Map: { + 'script-src': ['https://atlas.microsoft.com'], + 'style-src': ["'unsafe-inline'"], + 'connect-src': ['https://atlas.microsoft.com'], + 'worker-src': ['blob:'], + 'img-src': ['data:'], + 'font-src': ['https://atlas.microsoft.com'], + }, + 'DataGrid/CollaborativeEditing': { + 'connect-src': ['wss://js.devexpress.com'], + }, + 'Charts/SignalRService': { + 'connect-src': ['wss://js.devexpress.com'], + }, + 'Charts/SpiderWeb': { + 'font-src': ['https://fonts.gstatic.com'], + }, + 'Common/ListsOverview': { + 'img-src': ['data:'], + }, + 'DataGrid/SignalRService': { + 'connect-src': ['wss://js.devexpress.com'], + }, + 'Scheduler/SignalRService': { + 'connect-src': ['wss://js.devexpress.com'], + }, + 'DataGrid/Cell': { + 'img-src': ['data:'], + }, + // AI demo: inline