feat(security): add CodeQL analysis workflow and ESLint security plugins
This commit is contained in:
59
.github/workflows/codeql.yml
vendored
Normal file
59
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '0 6 * * 1'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
actions: read
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [javascript-typescript]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: security-extended,security-and-quality
|
||||
config: |
|
||||
paths-ignore:
|
||||
- dist
|
||||
- dist-test
|
||||
- coverage
|
||||
- node_modules
|
||||
- vendor
|
||||
- bentopdf-pymupdf-wasm
|
||||
- libreoffice-wasm-package
|
||||
- bentopdf-airgap-bundle
|
||||
- public/pdfjs-viewer
|
||||
- public/pdfjs-annotation-viewer
|
||||
- public/libreoffice-wasm
|
||||
- public/coherentpdf.browser.min.js
|
||||
- public/workers
|
||||
- public/embedpdf
|
||||
- docs/.vitepress
|
||||
- '**/*.min.js'
|
||||
- '**/*.d.ts'
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: /language:${{ matrix.language }}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -36,6 +36,11 @@ public/sitemap.xml
|
||||
# Generated by scripts/generate-security-headers.mjs at build time
|
||||
security-headers.conf
|
||||
|
||||
# CodeQL local scan artifacts (npm run security:codeql)
|
||||
codeql-db/
|
||||
codeql-results.sarif
|
||||
codeql-results.csv
|
||||
|
||||
#backup
|
||||
.seo-backup
|
||||
libreoffice-wasm-package
|
||||
|
||||
@@ -2,6 +2,8 @@ import globals from 'globals';
|
||||
import pluginJs from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||
import security from 'eslint-plugin-security';
|
||||
import noUnsanitized from 'eslint-plugin-no-unsanitized';
|
||||
|
||||
export default [
|
||||
{ files: ['**/*.{js,mjs,cjs,ts}'] },
|
||||
@@ -12,14 +14,41 @@ export default [
|
||||
'node_modules/**',
|
||||
'**/.vitepress/cache/**',
|
||||
'public/**/*.min.js',
|
||||
'vendor/**',
|
||||
'bentopdf-pymupdf-wasm/**',
|
||||
'libreoffice-wasm-package/**',
|
||||
'bentopdf-airgap-bundle/**',
|
||||
'public/pdfjs-viewer/**',
|
||||
'public/pdfjs-annotation-viewer/**',
|
||||
'public/coherentpdf.browser.min.js',
|
||||
'public/libreoffice-wasm/**',
|
||||
'public/workers/**',
|
||||
],
|
||||
},
|
||||
{ languageOptions: { globals: { ...globals.browser, ...globals.node } } },
|
||||
pluginJs.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
eslintConfigPrettier,
|
||||
security.configs.recommended,
|
||||
noUnsanitized.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
'no-unsanitized/method': 'warn',
|
||||
'no-unsanitized/property': 'warn',
|
||||
'security/detect-eval-with-expression': 'error',
|
||||
'security/detect-new-buffer': 'error',
|
||||
'security/detect-unsafe-regex': 'warn',
|
||||
'security/detect-pseudoRandomBytes': 'error',
|
||||
'security/detect-bidi-characters': 'error',
|
||||
'security/detect-disable-mustache-escape': 'error',
|
||||
'security/detect-possible-timing-attacks': 'off',
|
||||
'security/detect-object-injection': 'off',
|
||||
'security/detect-non-literal-fs-filename': 'off',
|
||||
'security/detect-non-literal-regexp': 'off',
|
||||
'security/detect-non-literal-require': 'off',
|
||||
'security/detect-child-process': 'off',
|
||||
'security/detect-no-csrf-before-method-override': 'off',
|
||||
'security/detect-buffer-noassert': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
|
||||
48
package-lock.json
generated
48
package-lock.json
generated
@@ -94,6 +94,8 @@
|
||||
"@vitest/ui": "^4.0.18",
|
||||
"eslint": "^10.0.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-no-unsanitized": "^4.1.5",
|
||||
"eslint-plugin-security": "^4.0.0",
|
||||
"globals": "^17.4.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^28.1.0",
|
||||
@@ -6743,6 +6745,32 @@
|
||||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-no-unsanitized": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.1.5.tgz",
|
||||
"integrity": "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"peerDependencies": {
|
||||
"eslint": "^9 || ^10"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-security": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-4.0.0.tgz",
|
||||
"integrity": "sha512-tfuQT8K/Li1ZxhFzyD8wPIKtlzZxqBcPr9q0jFMQ77wWAbKBVEhaMPVQRTMTvCMUDhwBe5vPVqQPwAGk/ASfxQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-regex": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
|
||||
@@ -10401,6 +10429,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/regexp-tree": {
|
||||
"version": "0.1.27",
|
||||
"resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
|
||||
"integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"regexp-tree": "bin/regexp-tree"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
@@ -10731,6 +10769,16 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-regex": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
|
||||
"integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regexp-tree": "~0.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-regex-test": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint:security": "eslint . --no-inline-config --rule 'no-unsanitized/method:error' --rule 'no-unsanitized/property:error' --rule 'security/detect-eval-with-expression:error'",
|
||||
"security:codeql": "codeql database create ./codeql-db --language=javascript-typescript --source-root=. --overwrite --threads=0 && codeql database analyze ./codeql-db --format=sarif-latest --output=codeql-results.sarif --threads=0 codeql/javascript-queries:codeql-suites/javascript-security-extended.qls",
|
||||
"security:codeql:quick": "codeql database analyze ./codeql-db --format=csv --output=codeql-results.csv --threads=0 codeql/javascript-queries:codeql-suites/javascript-security-extended.qls",
|
||||
"security:audit": "npm audit --audit-level=high && npm run lint:security",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -48,6 +52,8 @@
|
||||
"@vitest/ui": "^4.0.18",
|
||||
"eslint": "^10.0.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-no-unsanitized": "^4.1.5",
|
||||
"eslint-plugin-security": "^4.0.0",
|
||||
"globals": "^17.4.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^28.1.0",
|
||||
|
||||
@@ -203,7 +203,13 @@ describe('XSS replay — CDN URL version pinning', () => {
|
||||
const urls = WasmProvider.getAllProviders();
|
||||
for (const [pkg, url] of Object.entries(urls)) {
|
||||
if (!url) continue;
|
||||
if (!url.includes('cdn.jsdelivr.net')) continue;
|
||||
let hostname: string;
|
||||
try {
|
||||
hostname = new URL(url).hostname;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (hostname !== 'cdn.jsdelivr.net') continue;
|
||||
expect(
|
||||
/@\d+\.\d+\.\d+/.test(url),
|
||||
`${pkg} URL "${url}" must be pinned to an exact version (e.g. pkg@1.2.3)`
|
||||
|
||||
@@ -12,7 +12,6 @@ import handlebars from 'vite-plugin-handlebars';
|
||||
import { resolve } from 'path';
|
||||
import fs from 'fs';
|
||||
import { constants as zlibConstants } from 'zlib';
|
||||
import type { OutputBundle } from 'rollup';
|
||||
|
||||
const SUPPORTED_LANGUAGES = [
|
||||
'en',
|
||||
@@ -360,19 +359,41 @@ function flattenPagesPlugin(): Plugin {
|
||||
return {
|
||||
name: 'flatten-pages',
|
||||
enforce: 'post',
|
||||
generateBundle(_: unknown, bundle: OutputBundle): void {
|
||||
writeBundle(options, bundle) {
|
||||
const outDir = options.dir;
|
||||
if (!outDir) return;
|
||||
|
||||
const moves: Array<{ from: string; to: string }> = [];
|
||||
|
||||
for (const fileName of Object.keys(bundle)) {
|
||||
if (fileName.startsWith('src/pages/') && fileName.endsWith('.html')) {
|
||||
const newFileName = fileName.replace('src/pages/', '');
|
||||
bundle[newFileName] = bundle[fileName];
|
||||
bundle[newFileName].fileName = newFileName;
|
||||
delete bundle[fileName];
|
||||
moves.push({
|
||||
from: fileName,
|
||||
to: fileName.replace('src/pages/', ''),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.SIMPLE_MODE === 'true' && bundle['simple-index.html']) {
|
||||
bundle['index.html'] = bundle['simple-index.html'];
|
||||
bundle['index.html'].fileName = 'index.html';
|
||||
delete bundle['simple-index.html'];
|
||||
moves.push({ from: 'simple-index.html', to: 'index.html' });
|
||||
}
|
||||
|
||||
for (const { from, to } of moves) {
|
||||
const oldPath = resolve(outDir, from);
|
||||
const newPath = resolve(outDir, to);
|
||||
if (!fs.existsSync(oldPath)) continue;
|
||||
fs.mkdirSync(resolve(newPath, '..'), { recursive: true });
|
||||
if (fs.existsSync(newPath)) fs.rmSync(newPath, { force: true });
|
||||
fs.renameSync(oldPath, newPath);
|
||||
}
|
||||
|
||||
const pagesDir = resolve(outDir, 'src/pages');
|
||||
if (fs.existsSync(pagesDir) && fs.readdirSync(pagesDir).length === 0) {
|
||||
fs.rmdirSync(pagesDir);
|
||||
}
|
||||
const srcDir = resolve(outDir, 'src');
|
||||
if (fs.existsSync(srcDir) && fs.readdirSync(srcDir).length === 0) {
|
||||
fs.rmdirSync(srcDir);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -387,13 +408,11 @@ function rewriteHtmlPathsPlugin(): Plugin {
|
||||
return {
|
||||
name: 'rewrite-html-paths',
|
||||
enforce: 'post',
|
||||
generateBundle(_: unknown, bundle: OutputBundle): void {
|
||||
writeBundle(options, bundle) {
|
||||
if (normalizedBase === '/') return;
|
||||
const outDir = options.dir;
|
||||
if (!outDir) return;
|
||||
|
||||
for (const fileName of Object.keys(bundle)) {
|
||||
if (fileName.endsWith('.html')) {
|
||||
const asset = bundle[fileName];
|
||||
if (asset.type === 'asset' && typeof asset.source === 'string') {
|
||||
const hrefRegex = new RegExp(
|
||||
`href="\\/(?!${escapedBase.slice(1)}|test\\/|http|\\/\\/)`,
|
||||
'g'
|
||||
@@ -407,11 +426,17 @@ function rewriteHtmlPathsPlugin(): Plugin {
|
||||
'g'
|
||||
);
|
||||
|
||||
asset.source = asset.source
|
||||
for (const fileName of Object.keys(bundle)) {
|
||||
if (!fileName.endsWith('.html')) continue;
|
||||
const diskPath = resolve(outDir, fileName);
|
||||
if (!fs.existsSync(diskPath)) continue;
|
||||
const source = fs.readFileSync(diskPath, 'utf8');
|
||||
const updated = source
|
||||
.replace(hrefRegex, `href="${normalizedBase}`)
|
||||
.replace(srcRegex, `src="${normalizedBase}`)
|
||||
.replace(contentRegex, `content="${normalizedBase}`);
|
||||
}
|
||||
if (updated !== source) {
|
||||
fs.writeFileSync(diskPath, updated);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user