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
1 change: 1 addition & 0 deletions .ai/context.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dependencies:
runtime:
- vscode: VS Code Extension API
- js-yaml: YAML parsing
- yaml: Source ranges for placing component inserts in the include list
dev:
- typescript: Type-safe development
- esbuild: Fast bundling
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/security-hardening.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
# SHA-pinned rather than tag-pinned for supply-chain safety: a tag can be moved, a commit cannot.
# Dependabot bumps this SHA and names the tag it resolves to in the PR title, so read the version there
# rather than trusting a hand-written one here (this comment claimed v1.11.0 through three bumps past it).
uses: huntridge-labs/argus/.github/workflows/reusable-security-hardening.yml@3fb133a5d03ec81a7a534cf451dd19991e04a975
uses: huntridge-labs/argus/.github/workflows/reusable-security-hardening.yml@309e971918f9852c4a35e34fa145b5412697829f
with:
scanners: codeql,gitleaks,osv,dependency-review
enable_code_security: true
Expand Down
47 changes: 46 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@ const js = require('@eslint/js');
const tseslint = require('typescript-eslint');
const globals = require('globals');

/**
* JSX the webview views must not contain.
*
* @param {{ allowRawHtml?: boolean }} [options] `allowRawHtml` admits `dangerouslySetInnerHTML`, for `Page.tsx` only.
* @returns {{ selector: string, message: string }[]} `no-restricted-syntax` entries.
*/
function viewRestrictions({ allowRawHtml = false } = {}) {
return [
{
selector: "JSXAttribute[name.name='style']",
message: 'Inline styles are blocked by the webview CSP; add a class in src/webview/styles.',
},
{
selector: 'JSXAttribute[name.name=/^on/]',
message: 'Inline handlers are blocked by the webview CSP; use a data-action attribute.',
},
{
selector: "JSXOpeningElement[name.name='script']:not(:has(JSXAttribute[name.name=/^(src|type)$/]))",
message: 'Inline scripts are blocked by the webview CSP; put code in src/webview/client.',
},
{
selector: "JSXOpeningElement[name.name='style']",
message: 'Inline styles are blocked by the webview CSP; add a stylesheet in src/webview/styles.',
},
...(allowRawHtml ? [] : [{
selector: "JSXAttribute[name.name='dangerouslySetInnerHTML']",
message: 'Raw HTML bypasses escaping; use JsonScript or InlineMarkdown from Page.tsx.',
}]),
];
}

/** @type {import('eslint').Linter.Config[]} */
module.exports = [
{
Expand All @@ -20,7 +51,7 @@ module.exports = [
},
},
...tseslint.config({
files: ['**/*.ts'],
files: ['**/*.ts', '**/*.tsx'],
extends: [...tseslint.configs.recommended],
rules: {
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
Expand Down Expand Up @@ -51,6 +82,20 @@ module.exports = [
...js.configs.recommended.rules,
},
},
{
// Webview views render under a nonce CSP: inline styles and handlers are blocked, and raw HTML bypasses escaping.
files: ['src/webview/views/**/*.tsx'],
rules: {
'no-restricted-syntax': ['error', ...viewRestrictions()],
},
},
{
// The two helpers that emit pre-sanitised content are the only place raw HTML is allowed.
files: ['src/webview/views/Page.tsx'],
rules: {
'no-restricted-syntax': ['error', ...viewRestrictions({ allowRawHtml: true })],
},
},
{
// Webview client scripts run in the Electron renderer, not the extension host
files: ['src/webview/client/**/*.ts'],
Expand Down
Loading
Loading