Skip to content

Add project-level security scan rules#744

Open
aidenybai wants to merge 28 commits into
mainfrom
feat/security-posture-scanner
Open

Add project-level security scan rules#744
aidenybai wants to merge 28 commits into
mainfrom
feat/security-posture-scanner

Conversation

@aidenybai

@aidenybai aidenybai commented Jun 8, 2026

Copy link
Copy Markdown
Member

Summary

Adds project-level security scan rules: 36 first-class rules that detect what per-file linting never sees — browser-artifact secret leaks, Firebase/Supabase authorization mistakes, CI/install trust-boundary issues, clickjacking/SVG risks, agent/MCP tool capability risks, injection patterns, and committed secret files.

Architecture

Scan rules follow the same lifecycle as every other rule in the repo:

  • One rule, one file — each lives in oxlint-plugin-react-doctor/src/plugin/rules/security-scan/<rule-id>.ts as defineRule({ id, title, severity, recommendation, scan }) — same definition as AST rules, with scan in place of create. The scan(file) => findings field replaces AST visitors for this kind; per-finding severity/title/help overrides cover the two dynamic rules (public-debug-artifact, active-static-asset).
  • Normal registration — rules flow through the standard codegen registry (bucket → category Security, auto-tag security-scan), so tags, severities, titles, and help text are single-sourced. The registry generator keeps main's one-rule-per-file contract.
  • Executed as an environment check@react-doctor/core's checkSecurityScan (shaped like checkPnpmHardening) runs in runInspect's environment-checks phase: one bounded whole-tree walk, per-rule gating through the real shouldEnableRule (capabilities, ignoredTags, disabledBy), and diagnostics streamed through the per-element pipeline so severity controls, inline disables, and surfaces apply like any other diagnostic. services/linter.ts is untouched vs main.
  • Never shipped as no-ops — scan rules are excluded from generated oxlint configs and from the ESLint flat-config presets (regression-tested in both paths).

Behavior notes

  • Skipped on diff/staged scans (gate is diff mode, not the old includePaths === undefined proxy) — projects configuring ignore.files get the security scan.
  • User rules/categories severity overrides, inline disables, and surface filtering apply to scan-rule diagnostics.
  • ignore.tags: ["security-scan"] (or rules ignore-tag security-scan) silences the whole family.

Verification

  • A 757-line fixture suite (packages/core/tests/check-security-scan.test.ts) over 31 fixture projects; a differential harness compared the original monolith implementation vs the final engine over all fixtures: 0 diagnostic differences.
  • runInspect integration tests (full-scan emission, diff-mode gate, severity restamp), ESLint-preset + oxlint-config exclusion regressions, registry invariants (exactly 36 tagged scan rules; scan nowhere else), runScanRule unit harness with regressions for the AST and dynamic-severity rules.
  • pnpm typecheck, pnpm build, core (713 tests), react-doctor (1737 tests), and plugin suites green (the 6 failing plugin files are pre-existing on main).

Test plan

  • pnpm --filter @react-doctor/core test
  • pnpm --filter oxlint-plugin-react-doctor test
  • pnpm --filter ./packages/react-doctor test
  • pnpm typecheck && pnpm build && pnpm format:check

Note

High Risk
Large new security-detection surface over whole-repo file reads; false positives/negatives and performance caps matter, though diff-mode skip, tag ignore, and extensive fixture tests mitigate rollout risk.

Overview
Introduces 36 project-level security scan rules in the security-scan bucket: each rule is a normal defineRule module with a scan(file) hook instead of AST visitors, auto-tagged and categorized as Security, but never registered in generated oxlint or ESLint configs (dead visitors avoided).

@react-doctor/core adds checkSecurityScan, which enables rules through the same shouldEnableRule / ignoredTags / disabledBy path as lint, walks the repo with bounded depth/file/size limits and priority bucketing (config/SQL vs shipped artifacts), and maps ScanFindings into standard Security diagnostics (severity overrides, inline disables, surfaces). The check runs in runInspect's environment-check phase on full scans and is skipped when isDiffMode (includePaths.length > 0), so diff/staged runs behave like other whole-tree checks while projects with ignore.files still get the scan on full runs.

Supporting changes: shared scan path/classification helpers and secret patterns in the plugin, registry/codegen/docs updates (HOW_TO_WRITE_A_RULE scan section, README note), parseTailwindMajorMinor now uses semver lower bounds instead of regex, and broad defineRule<Rule>defineRule cleanup across existing a11y rules. Coverage is a large fixture suite plus integration tests for oxlint exclusion, runInspect, and tag-based silencing (security-scan).

Reviewed by Cursor Bugbot for commit 3a260f5. Bugbot is set up for automated code reviews on this repo. Configure here.

@pkg-pr-new

pkg-pr-new Bot commented Jun 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/eslint-plugin-react-doctor@744
npm i https://pkg.pr.new/oxlint-plugin-react-doctor@744
npm i https://pkg.pr.new/react-doctor@744

commit: 3a260f5

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

No React Doctor issues found. 🎉

Reviewed by React Doctor for commit 3a260f5.

github-advanced-security[bot]

This comment was marked as resolved.

cursor[bot]

This comment was marked as resolved.

@aidenybai aidenybai force-pushed the feat/security-posture-scanner branch from 51e8816 to cdfe430 Compare June 8, 2026 06:57
cursor[bot]

This comment was marked as resolved.

@aidenybai aidenybai force-pushed the feat/security-posture-scanner branch from cdfe430 to b1fe41d Compare June 8, 2026 07:14
cursor[bot]

This comment was marked as resolved.

@aidenybai aidenybai force-pushed the feat/security-posture-scanner branch from b1fe41d to d1f18fc Compare June 8, 2026 07:23
cursor[bot]

This comment was marked as resolved.

@aidenybai aidenybai force-pushed the feat/security-posture-scanner branch from d1f18fc to d9adf7e Compare June 8, 2026 07:34
cursor[bot]

This comment was marked as resolved.

@aidenybai aidenybai force-pushed the feat/security-posture-scanner branch from d9adf7e to 4a1840e Compare June 8, 2026 07:41
github-advanced-security[bot]

This comment was marked as resolved.

cursor[bot]

This comment was marked as resolved.

@aidenybai aidenybai force-pushed the feat/security-posture-scanner branch from 4a1840e to b18753e Compare June 8, 2026 07:47
cursor[bot]

This comment was marked as resolved.

@aidenybai aidenybai force-pushed the feat/security-posture-scanner branch from b18753e to a940685 Compare June 8, 2026 08:05
github-advanced-security[bot]

This comment was marked as resolved.

cursor[bot]

This comment was marked as resolved.

@aidenybai

Copy link
Copy Markdown
Member Author

/rde parity

@react-doctor-evals

react-doctor-evals Bot commented Jun 9, 2026

Copy link
Copy Markdown

Parity changed: +25 added · -10 removed across 12 repos

Baseline: main · This PR: feat/security-posture-scanner (3a260f5)

tldraw/tldraw (packages/sync) — +1 / -1

✨ Added in this PR (not present in baseline)

low-supply-chain-scorepackage.json:53:3

vitest (declared in package.json as "^3.2.4", scored at 3.2.4) has a Socket supply-chain score of 25/100 (below the minimum of 50). Axis scores — supply chain 97, vulnerability 25, maintenance 98, quality 78, license 100.

  41 | 		"prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
  42 | 		"postpack": "../../internal/scripts/postpack.sh",
  43 | 		"pack-tarball": "yarn pack"
  44 | 	},
  45 | 	"devDependencies": {
  46 | 		"@types/react": "^19.2.7",
  47 | 		"@types/react-dom": "^19.2.3",
  48 | 		"react": "^19.2.1",
  49 | 		"react-dom": "^19.2.1",
  50 | 		"typescript": "^5.8.3",
  51 | 		"uuid-by-string": "^4.0.0",
  52 | 		"uuid-readable": "^0.0.2",
> 53 | 		"vitest": "^3.2.4"
  54 | 	},
  55 | 	"dependencies": {
  56 | 		"@tldraw/state": "workspace:*",
  57 | 		"@tldraw/state-react": "workspace:*",
  58 | 		"@tldraw/sync-core": "workspace:*",
  59 | 		"@tldraw/utils": "workspace:*",
  60 | 		"nanoevents": "^7.0.1",
  61 | 		"tldraw": "workspace:*",
  62 | 		"ws": "^8.18.0"
  63 | 	},
  64 | 	"peerDependencies": {
  65 | 		"react": "^18.2.0 || ^19.2.1",

Rule docs


✅ Removed (fixed) in this PR (were in baseline)

low-supply-chain-scorepackage.json:53:3

vitest (declared in package.json as "^3.2.4", scored at 3.2.4) has a Socket vulnerability score of 25/100 (below the minimum of 50). Axis scores — supply chain 97, vulnerability 25, maintenance 98, quality 78, license 100.

  41 | 		"prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
  42 | 		"postpack": "../../internal/scripts/postpack.sh",
  43 | 		"pack-tarball": "yarn pack"
  44 | 	},
  45 | 	"devDependencies": {
  46 | 		"@types/react": "^19.2.7",
  47 | 		"@types/react-dom": "^19.2.3",
  48 | 		"react": "^19.2.1",
  49 | 		"react-dom": "^19.2.1",
  50 | 		"typescript": "^5.8.3",
  51 | 		"uuid-by-string": "^4.0.0",
  52 | 		"uuid-readable": "^0.0.2",
> 53 | 		"vitest": "^3.2.4"
  54 | 	},
  55 | 	"dependencies": {
  56 | 		"@tldraw/state": "workspace:*",
  57 | 		"@tldraw/state-react": "workspace:*",
  58 | 		"@tldraw/sync-core": "workspace:*",
  59 | 		"@tldraw/utils": "workspace:*",
  60 | 		"nanoevents": "^7.0.1",
  61 | 		"tldraw": "workspace:*",
  62 | 		"ws": "^8.18.0"
  63 | 	},
  64 | 	"peerDependencies": {
  65 | 		"react": "^18.2.0 || ^19.2.1",

Rule docs

twentyhq/twenty (packages/twenty-companion) — +6 / -1

✨ Added in this PR (not present in baseline)

dangerous-html-sinksrc/renderer.js:253:3

HTML is injected from a dynamic-looking source, which can become XSS if the value is user-controlled or unsanitized.

  241 |       <div class="meeting-icon document">
  242 |         <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  243 |           <path d="M14 2H6C4.9 2 4.01 2.9 4.01 4L4 20C4 21.1 4.89 22 5.99 22H18C19.1 22 20 21.1 20 20V8L14 2ZM16 18H8V16H16V18ZM16 14H8V12H16V14ZM13 9V3.5L18.5 9H13Z" fill="#4CAF50"/>
  244 |         </svg>
  245 |       </div>
  246 |     `;
  247 |   }
  248 | 
  249 |   let subtitleHtml = meeting.hasDemo
  250 |     ? `<div class="meeting-time"><a class="meeting-demo-link">${meeting.subtitle}</a></div>`
  251 |     : `<div class="meeting-time">${meeting.subtitle}</div>`;
  252 | 
> 253 |   card.innerHTML = `
  254 |     ${iconHtml}
  255 |     <div class="meeting-content">
  256 |       <div class="meeting-title">${meeting.title}</div>
  257 |       ${subtitleHtml}
  258 |     </div>
  259 |     <div class="meeting-actions">
  260 |       <button class="delete-meeting-btn" data-id="${meeting.id}" title="Delete note">
  261 |         <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  262 |           <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" fill="currentColor"/>
  263 |         </svg>
  264 |       </button>
  265 |     </div>

dangerous-html-sinksrc/renderer.js:764:5

HTML is injected from a dynamic-looking source, which can become XSS if the value is user-controlled or unsanitized.

  752 |     entryDiv.className = 'transcript-entry';
  753 | 
  754 |     const speaker = entry.participant?.name || entry.speaker || 'Unknown';
  755 |     const text = entry.words
  756 |       ? entry.words.map(word => word.text).join(' ')
  757 |       : entry.text || '';
  758 |     const firstWord = entry.words?.[0];
  759 |     const absTimestamp = firstWord?.start_timestamp?.absolute;
  760 |     const formattedTime = absTimestamp
  761 |       ? new Date(absTimestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
  762 |       : '';
  763 | 
> 764 |     entryDiv.innerHTML = `
  765 |       <div class="transcript-speaker">${speaker}</div>
  766 |       <div class="transcript-text">${text}</div>
  767 |       <div class="transcript-timestamp">${formattedTime}</div>
  768 |     `;
  769 | 
  770 |     // Add a highlight class for the newest entry
  771 |     if (index === transcript.length - 1) {
  772 |       entryDiv.classList.add('newest-entry');
  773 |     }
  774 | 
  775 |     transcriptDiv.appendChild(entryDiv);
  776 |   });

dangerous-html-sinksrc/renderer.js:1210:7

HTML is injected from a dynamic-looking source, which can become XSS if the value is user-controlled or unsanitized.

  1198 | 
  1199 |         case 'error':
  1200 |           content = `<span class="error-type">Error: ${entry.errorType}</span>`;
  1201 |           if (entry.message) {
  1202 |             content += `<div class="params">${entry.message}</div>`;
  1203 |           }
  1204 |           break;
  1205 | 
  1206 |         default:
  1207 |           content = entry.message;
  1208 |       }
  1209 | 
> 1210 |       logElement.innerHTML += content;
  1211 | 
  1212 |       // Add to the top of the log
  1213 |       loggerContent.insertBefore(logElement, loggerContent.firstChild);
  1214 | 
  1215 |       // Only auto-scroll to top if user is already at the top
  1216 |       const isAtTop = loggerContent.scrollTop <= 5;
  1217 |       if (isAtTop) {
  1218 |         loggerContent.scrollTop = 0;
  1219 |       }
  1220 |     }
  1221 |   },
  1222 | 

dangerous-html-sinksrc/renderer.js:1443:17

HTML is injected from a dynamic-looking source, which can become XSS if the value is user-controlled or unsanitized.

  1431 | 
  1432 |           updateDebugTranscript(meeting.transcript);
  1433 | 
  1434 |           const debugPanel = document.getElementById('debugPanel');
  1435 |           if (debugPanel && debugPanel.classList.contains('hidden')) {
  1436 |             const debugPanelToggle = document.getElementById('debugPanelToggle');
  1437 |             if (debugPanelToggle) {
  1438 |               debugPanelToggle.classList.add('has-new-content');
  1439 | 
  1440 |               if (window.isRecording) {
  1441 |                 const miniNotification = document.createElement('div');
  1442 |                 miniNotification.className = 'debug-notification transcript-notification';
> 1443 |                 miniNotification.innerHTML = `
  1444 |                   <span class="debug-notification-speaker">${latestSpeaker}</span>:
  1445 |                   <span class="debug-notification-text">${latestText.slice(0, 40)}${latestText.length > 40 ? '...' : ''}</span>
  1446 |                 `;
  1447 | 
  1448 |                 // Add to document
  1449 |                 document.body.appendChild(miniNotification);
  1450 | 
  1451 |                 // Remove after a short time
  1452 |                 setTimeout(() => {
  1453 |                   miniNotification.classList.add('fade-out');
  1454 |                   setTimeout(() => {
  1455 |                     document.body.removeChild(miniNotification);

dangerous-html-sinksrc/renderer.js:2036:9

HTML is injected from a dynamic-looking source, which can become XSS if the value is user-controlled or unsanitized.

  2024 |               document.body.removeChild(toast);
  2025 |             }, 300);
  2026 |           }, 3000);
  2027 |         } else {
  2028 |           console.error('Failed to generate summary:', result.error);
  2029 |           alert('Failed to generate summary: ' + result.error);
  2030 |         }
  2031 |       } catch (error) {
  2032 |         console.error('Error generating summary:', error);
  2033 |         alert('Error generating summary: ' + (error.message || error));
  2034 |       } finally {
  2035 |         // Reset button state with the original HTML (including sparkle icon)
> 2036 |         generateButton.innerHTML = originalHTML;
  2037 |         generateButton.disabled = false;
  2038 |       }
  2039 |     });
  2040 |   }
  2041 | 
  2042 | 
  2043 | 
  2044 |   // Listen for recording completed events
  2045 |   window.electronAPI.onRecordingCompleted((meetingId) => {
  2046 |     console.log('Recording completed for meeting:', meetingId);
  2047 |     if (currentEditingMeetingId === meetingId) {
  2048 |       // Reload the meeting data first

low-supply-chain-scorepackage.json:47:5

axios (declared in package.json as "^1.9.0", scored at 1.9.0) has a Socket supply-chain score of 25/100 (below the minimum of 50). Axis scores — supply chain 98, vulnerability 25, maintenance 94, quality 100, license 100.

  35 |     "css-loader": "^6.11.0",
  36 |     "electron": "36.0.1",
  37 |     "node-loader": "^2.1.0",
  38 |     "style-loader": "^3.3.4"
  39 |   },
  40 |   "resolutions": {
  41 |     "@electron/packager/@electron/osx-sign": "github:recallai/osx-sign"
  42 |   },
  43 |   "dependencies": {
  44 |     "@anthropic-ai/sdk": "^0.40.1",
  45 |     "@recallai/desktop-sdk": "^2.0.0",
  46 |     "@timfish/forge-externals-plugin": "^0.2.1",
> 47 |     "axios": "^1.9.0",
  48 |     "codemirror": "^6.0.1",
  49 |     "concurrently": "^9.2.1",
  50 |     "dotenv": "^16.5.0",
  51 |     "easymde": "^2.20.0",
  52 |     "electron-squirrel-startup": "^1.0.1",
  53 |     "express": "^4.18.2",
  54 |     "marked": "^5.0.0",
  55 |     "openai": "^4.97.0",
  56 |     "react": "^19.1.0",
  57 |     "react-dom": "^19.1.0",
  58 |     "react-markdown": "^10.1.0",
  59 |     "simplemde": "^1.11.2",

Rule docs


✅ Removed (fixed) in this PR (were in baseline)

low-supply-chain-scorepackage.json:47:5

axios (declared in package.json as "^1.9.0", scored at 1.9.0) has a Socket vulnerability score of 25/100 (below the minimum of 50). Axis scores — supply chain 98, vulnerability 25, maintenance 94, quality 100, license 100.

  35 |     "css-loader": "^6.11.0",
  36 |     "electron": "36.0.1",
  37 |     "node-loader": "^2.1.0",
  38 |     "style-loader": "^3.3.4"
  39 |   },
  40 |   "resolutions": {
  41 |     "@electron/packager/@electron/osx-sign": "github:recallai/osx-sign"
  42 |   },
  43 |   "dependencies": {
  44 |     "@anthropic-ai/sdk": "^0.40.1",
  45 |     "@recallai/desktop-sdk": "^2.0.0",
  46 |     "@timfish/forge-externals-plugin": "^0.2.1",
> 47 |     "axios": "^1.9.0",
  48 |     "codemirror": "^6.0.1",
  49 |     "concurrently": "^9.2.1",
  50 |     "dotenv": "^16.5.0",
  51 |     "easymde": "^2.20.0",
  52 |     "electron-squirrel-startup": "^1.0.1",
  53 |     "express": "^4.18.2",
  54 |     "marked": "^5.0.0",
  55 |     "openai": "^4.97.0",
  56 |     "react": "^19.1.0",
  57 |     "react-dom": "^19.1.0",
  58 |     "react-markdown": "^10.1.0",
  59 |     "simplemde": "^1.11.2",

Rule docs

tldraw/tldraw (packages/state-react) — +1 / -1

✨ Added in this PR (not present in baseline)

low-supply-chain-scorepackage.json:58:3

vitest (declared in package.json as "^3.2.4", scored at 3.2.4) has a Socket supply-chain score of 25/100 (below the minimum of 50). Axis scores — supply chain 97, vulnerability 25, maintenance 98, quality 78, license 100.

  46 | 		"@tldraw/state": "workspace:*",
  47 | 		"@tldraw/utils": "workspace:*"
  48 | 	},
  49 | 	"devDependencies": {
  50 | 		"@testing-library/dom": "^10.0.0",
  51 | 		"@testing-library/react": "^16.0.0",
  52 | 		"@types/lodash": "^4.17.14",
  53 | 		"@types/react": "^19.2.7",
  54 | 		"@types/react-dom": "^19.2.3",
  55 | 		"lodash": "^4.17.21",
  56 | 		"react": "^19.2.1",
  57 | 		"react-dom": "^19.2.1",
> 58 | 		"vitest": "^3.2.4"
  59 | 	},
  60 | 	"peerDependencies": {
  61 | 		"react": "^18.2.0 || ^19.2.1",
  62 | 		"react-dom": "^18.2.0 || ^19.2.1"
  63 | 	},
  64 | 	"typedoc": {
  65 | 		"readmeFile": "none",
  66 | 		"entryPoint": "./src/index.ts",
  67 | 		"displayName": "@tldraw/state",
  68 | 		"tsconfig": "./tsconfig.json"
  69 | 	}
  70 | }

Rule docs


✅ Removed (fixed) in this PR (were in baseline)

low-supply-chain-scorepackage.json:58:3

vitest (declared in package.json as "^3.2.4", scored at 3.2.4) has a Socket vulnerability score of 25/100 (below the minimum of 50). Axis scores — supply chain 97, vulnerability 25, maintenance 98, quality 78, license 100.

  46 | 		"@tldraw/state": "workspace:*",
  47 | 		"@tldraw/utils": "workspace:*"
  48 | 	},
  49 | 	"devDependencies": {
  50 | 		"@testing-library/dom": "^10.0.0",
  51 | 		"@testing-library/react": "^16.0.0",
  52 | 		"@types/lodash": "^4.17.14",
  53 | 		"@types/react": "^19.2.7",
  54 | 		"@types/react-dom": "^19.2.3",
  55 | 		"lodash": "^4.17.21",
  56 | 		"react": "^19.2.1",
  57 | 		"react-dom": "^19.2.1",
> 58 | 		"vitest": "^3.2.4"
  59 | 	},
  60 | 	"peerDependencies": {
  61 | 		"react": "^18.2.0 || ^19.2.1",
  62 | 		"react-dom": "^18.2.0 || ^19.2.1"
  63 | 	},
  64 | 	"typedoc": {
  65 | 		"readmeFile": "none",
  66 | 		"entryPoint": "./src/index.ts",
  67 | 		"displayName": "@tldraw/state",
  68 | 		"tsconfig": "./tsconfig.json"
  69 | 	}
  70 | }

Rule docs

tldraw/tldraw (packages/mermaid) — +2 / -1

✨ Added in this PR (not present in baseline)

dangerous-html-sinksrc/createMermaidDiagram.ts:92:4

HTML is injected from a dynamic-looking source, which can become XSS if the value is user-controlled or unsanitized.

   80 | 	document.body.appendChild(offscreen)
   81 | 
   82 | 	try {
   83 | 		const parsedSvg = (await mermaid.render(`mermaid-${nextMermaidId++}`, text, offscreen)).svg
   84 | 
   85 | 		// Reuse the live SVG that mermaid.render() already mounted into the
   86 | 		// offscreen container.  This avoids a second DOM mount and ensures
   87 | 		// getBBox() works for every diagram type (state diagrams in particular
   88 | 		// lack explicit dimension attributes and rely on live layout).
   89 | 		let liveSvg = offscreen.querySelector('svg')
   90 | 
   91 | 		if (!liveSvg) {
>  92 | 			offscreen.innerHTML = parsedSvg
   93 | 			liveSvg = offscreen.querySelector('svg')
   94 | 			if (!liveSvg) {
   95 | 				throw new MermaidDiagramError(parsedResult.diagramType, 'parse')
   96 | 			}
   97 | 		}
   98 | 
   99 | 		// eslint-disable-next-line @typescript-eslint/no-deprecated
  100 | 		const diagramResult = await mermaid.mermaidAPI.getDiagramFromText(text)
  101 | 
  102 | 		let blueprint
  103 | 		switch (parsedResult.diagramType) {
  104 | 			case 'flowchart-v2': {

low-supply-chain-scorepackage.json:54:3

vitest (declared in package.json as "^3.2.4", scored at 3.2.4) has a Socket supply-chain score of 25/100 (below the minimum of 50). Axis scores — supply chain 97, vulnerability 25, maintenance 98, quality 78, license 100.

  42 | 	"dependencies": {
  43 | 		"@tldraw/tlschema": "workspace:*",
  44 | 		"@tldraw/utils": "workspace:*",
  45 | 		"mermaid": "11.12.2"
  46 | 	},
  47 | 	"peerDependencies": {
  48 | 		"react": "^18.2.0 || ^19.2.1",
  49 | 		"react-dom": "^18.2.0 || ^19.2.1",
  50 | 		"tldraw": "workspace:*"
  51 | 	},
  52 | 	"devDependencies": {
  53 | 		"lazyrepo": "0.0.0-alpha.27",
> 54 | 		"vitest": "^3.2.4"
  55 | 	}
  56 | }
  57 | 

Rule docs


✅ Removed (fixed) in this PR (were in baseline)

low-supply-chain-scorepackage.json:54:3

vitest (declared in package.json as "^3.2.4", scored at 3.2.4) has a Socket vulnerability score of 25/100 (below the minimum of 50). Axis scores — supply chain 97, vulnerability 25, maintenance 98, quality 78, license 100.

  42 | 	"dependencies": {
  43 | 		"@tldraw/tlschema": "workspace:*",
  44 | 		"@tldraw/utils": "workspace:*",
  45 | 		"mermaid": "11.12.2"
  46 | 	},
  47 | 	"peerDependencies": {
  48 | 		"react": "^18.2.0 || ^19.2.1",
  49 | 		"react-dom": "^18.2.0 || ^19.2.1",
  50 | 		"tldraw": "workspace:*"
  51 | 	},
  52 | 	"devDependencies": {
  53 | 		"lazyrepo": "0.0.0-alpha.27",
> 54 | 		"vitest": "^3.2.4"
  55 | 	}
  56 | }
  57 | 

Rule docs

tldraw/tldraw (packages/sync-core) — +1 / -1

✨ Added in this PR (not present in baseline)

low-supply-chain-scorepackage.json:55:3

vitest (declared in package.json as "^3.2.4", scored at 3.2.4) has a Socket supply-chain score of 25/100 (below the minimum of 50). Axis scores — supply chain 97, vulnerability 25, maintenance 98, quality 78, license 100.

  43 | 		"pack-tarball": "yarn pack"
  44 | 	},
  45 | 	"devDependencies": {
  46 | 		"@types/react": "^19.2.7",
  47 | 		"@types/react-dom": "^19.2.3",
  48 | 		"@types/uuid-readable": "^0.0.3",
  49 | 		"react": "^19.2.1",
  50 | 		"react-dom": "^19.2.1",
  51 | 		"tldraw": "workspace:*",
  52 | 		"typescript": "^5.8.3",
  53 | 		"uuid-by-string": "^4.0.0",
  54 | 		"uuid-readable": "^0.0.2",
> 55 | 		"vitest": "^3.2.4"
  56 | 	},
  57 | 	"dependencies": {
  58 | 		"@tldraw/state": "workspace:*",
  59 | 		"@tldraw/store": "workspace:*",
  60 | 		"@tldraw/tlschema": "workspace:*",
  61 | 		"@tldraw/utils": "workspace:*",
  62 | 		"nanoevents": "^7.0.1",
  63 | 		"ws": "^8.18.0"
  64 | 	},
  65 | 	"peerDependencies": {
  66 | 		"react": "^18.2.0 || ^19.2.1",
  67 | 		"react-dom": "^18.2.0 || ^19.2.1"

Rule docs


✅ Removed (fixed) in this PR (were in baseline)

low-supply-chain-scorepackage.json:55:3

vitest (declared in package.json as "^3.2.4", scored at 3.2.4) has a Socket vulnerability score of 25/100 (below the minimum of 50). Axis scores — supply chain 97, vulnerability 25, maintenance 98, quality 78, license 100.

  43 | 		"pack-tarball": "yarn pack"
  44 | 	},
  45 | 	"devDependencies": {
  46 | 		"@types/react": "^19.2.7",
  47 | 		"@types/react-dom": "^19.2.3",
  48 | 		"@types/uuid-readable": "^0.0.3",
  49 | 		"react": "^19.2.1",
  50 | 		"react-dom": "^19.2.1",
  51 | 		"tldraw": "workspace:*",
  52 | 		"typescript": "^5.8.3",
  53 | 		"uuid-by-string": "^4.0.0",
  54 | 		"uuid-readable": "^0.0.2",
> 55 | 		"vitest": "^3.2.4"
  56 | 	},
  57 | 	"dependencies": {
  58 | 		"@tldraw/state": "workspace:*",
  59 | 		"@tldraw/store": "workspace:*",
  60 | 		"@tldraw/tlschema": "workspace:*",
  61 | 		"@tldraw/utils": "workspace:*",
  62 | 		"nanoevents": "^7.0.1",
  63 | 		"ws": "^8.18.0"
  64 | 	},
  65 | 	"peerDependencies": {
  66 | 		"react": "^18.2.0 || ^19.2.1",
  67 | 		"react-dom": "^18.2.0 || ^19.2.1"

Rule docs

tldraw/tldraw (packages/store) — +1 / -1

✨ Added in this PR (not present in baseline)

low-supply-chain-scorepackage.json:56:3

vitest (declared in package.json as "^3.2.4", scored at 3.2.4) has a Socket supply-chain score of 25/100 (below the minimum of 50). Axis scores — supply chain 97, vulnerability 25, maintenance 98, quality 78, license 100.

  44 | 	},
  45 | 	"dependencies": {
  46 | 		"@tldraw/state": "workspace:*",
  47 | 		"@tldraw/utils": "workspace:*"
  48 | 	},
  49 | 	"peerDependencies": {
  50 | 		"react": "^18.2.0 || ^19.2.1"
  51 | 	},
  52 | 	"devDependencies": {
  53 | 		"@peculiar/webcrypto": "^1.5.0",
  54 | 		"lazyrepo": "0.0.0-alpha.27",
  55 | 		"raf": "^3.4.1",
> 56 | 		"vitest": "^3.2.4"
  57 | 	}
  58 | }
  59 | 

Rule docs


✅ Removed (fixed) in this PR (were in baseline)

low-supply-chain-scorepackage.json:56:3

vitest (declared in package.json as "^3.2.4", scored at 3.2.4) has a Socket vulnerability score of 25/100 (below the minimum of 50). Axis scores — supply chain 97, vulnerability 25, maintenance 98, quality 78, license 100.

  44 | 	},
  45 | 	"dependencies": {
  46 | 		"@tldraw/state": "workspace:*",
  47 | 		"@tldraw/utils": "workspace:*"
  48 | 	},
  49 | 	"peerDependencies": {
  50 | 		"react": "^18.2.0 || ^19.2.1"
  51 | 	},
  52 | 	"devDependencies": {
  53 | 		"@peculiar/webcrypto": "^1.5.0",
  54 | 		"lazyrepo": "0.0.0-alpha.27",
  55 | 		"raf": "^3.4.1",
> 56 | 		"vitest": "^3.2.4"
  57 | 	}
  58 | }
  59 | 

Rule docs

aidenybai/bippy (packages/bippy) — +1 / -1

✨ Added in this PR (not present in baseline)

low-supply-chain-scorepackage.json:106:5

happy-dom (declared in package.json as "^15.11.7", scored at 15.11.7) has a Socket supply-chain score of 25/100 (below the minimum of 50). Axis scores — supply chain 73, vulnerability 25, maintenance 95, quality 88, license 100.

   94 |     "test": "vp test",
   95 |     "coverage": "vp test --coverage",
   96 |     "prepublishOnly": "cp ../../README.md . && pnpm build"
   97 |   },
   98 |   "devDependencies": {
   99 |     "@jridgewell/sourcemap-codec": "^1.5.0",
  100 |     "@testing-library/dom": "^10.4.0",
  101 |     "@testing-library/react": "^16.1.0",
  102 |     "@types/node": "^20",
  103 |     "@types/react": "^19.0.4",
  104 |     "@types/react-dom": "^19.0.2",
  105 |     "esbuild": "^0.27.0",
> 106 |     "happy-dom": "^15.11.7",
  107 |     "publint": "^0.3.0",
  108 |     "react": "19.0.0",
  109 |     "react-devtools-inline": "^6.0.1",
  110 |     "react-dom": "19.0.0",
  111 |     "react-refresh": "^0.16.0",
  112 |     "terser": "^5.36.0",
  113 |     "tsx": "^4.21.0",
  114 |     "vite-plus": "latest"
  115 |   },
  116 |   "peerDependencies": {
  117 |     "react": ">=17.0.1"
  118 |   }

Rule docs


✅ Removed (fixed) in this PR (were in baseline)

low-supply-chain-scorepackage.json:106:5

happy-dom (declared in package.json as "^15.11.7", scored at 15.11.7) has a Socket vulnerability score of 25/100 (below the minimum of 50). Axis scores — supply chain 73, vulnerability 25, maintenance 95, quality 88, license 100.

   94 |     "test": "vp test",
   95 |     "coverage": "vp test --coverage",
   96 |     "prepublishOnly": "cp ../../README.md . && pnpm build"
   97 |   },
   98 |   "devDependencies": {
   99 |     "@jridgewell/sourcemap-codec": "^1.5.0",
  100 |     "@testing-library/dom": "^10.4.0",
  101 |     "@testing-library/react": "^16.1.0",
  102 |     "@types/node": "^20",
  103 |     "@types/react": "^19.0.4",
  104 |     "@types/react-dom": "^19.0.2",
  105 |     "esbuild": "^0.27.0",
> 106 |     "happy-dom": "^15.11.7",
  107 |     "publint": "^0.3.0",
  108 |     "react": "19.0.0",
  109 |     "react-devtools-inline": "^6.0.1",
  110 |     "react-dom": "19.0.0",
  111 |     "react-refresh": "^0.16.0",
  112 |     "terser": "^5.36.0",
  113 |     "tsx": "^4.21.0",
  114 |     "vite-plus": "latest"
  115 |   },
  116 |   "peerDependencies": {
  117 |     "react": ">=17.0.1"
  118 |   }

Rule docs

millionco/expect (packages/browser) — +2 / -0

✨ Added in this PR (not present in baseline)

mcp-tool-capability-risksrc/mcp/rules-resources.ts:26:10

An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability.

  14 |     "",
  15 |     "Available rule resources:",
  16 |     "",
  17 |   ];
  18 |   for (const rule of rules) {
  19 |     const subRuleHint =
  20 |       rule.subRules.length > 0
  21 |         ? ` — fetch \`expect://rules/${rule.slug}/{sub-rule}\` for ${rule.subRules.length} detailed sub-rules`
  22 |         : "";
  23 |     promptLines.push(`- \`expect://rules/${rule.slug}\` — ${rule.description}${subRuleHint}`);
  24 |   }
  25 | 
> 26 |   server.registerPrompt(
  27 |     "rules",
  28 |     { description: "Available rule resources for fixing domain-specific issues" },
  29 |     () => ({
  30 |       messages: [
  31 |         {
  32 |           role: "user" as const,
  33 |           content: { type: "text" as const, text: promptLines.join("\n") },
  34 |         },
  35 |       ],
  36 |     }),
  37 |   );
  38 | 

mcp-tool-capability-risksrc/mcp/server.ts:194:18

An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability.

  182 |     "- Assertion-first: navigate, act, then validate before moving on. Check at least two independent signals per step (e.g. URL changed AND new content appeared).",
  183 |     "</best_practices>",
  184 |   ].join("\n");
  185 | 
  186 | // HACK: tool annotations (readOnlyHint, destructiveHint) are required for parallel execution in the Claude Agent SDK
  187 | export const createBrowserMcpServer = <E>(
  188 |   runtime: ManagedRuntime.ManagedRuntime<McpSession | OverlayController | FileSystem, E>,
  189 | ) => {
  190 |   const runMcp = <A>(
  191 |     effect: Effect.Effect<A, unknown, McpSession | OverlayController | FileSystem>,
  192 |   ) => runtime.runPromise(effect);
  193 | 
> 194 |   const server = new McpServer({
  195 |     name: "expect",
  196 |     version: "0.0.1",
  197 |   });
  198 | 
  199 |   const openTool = server.registerTool(
  200 |     "open",
  201 |     {
  202 |       title: "Open URL",
  203 |       description:
  204 |         "Navigate to a URL, launching a browser if needed. Set 'cdp' to a WebSocket URL (e.g. 'ws://localhost:9222/devtools/browser/...') to connect to an already-running Chrome via CDP instead of launching a new browser.",
  205 |       inputSchema: {
  206 |         url: z.string().describe("URL to navigate to"),
tldraw/tldraw (packages/tlschema) — +2 / -2

✨ Added in this PR (not present in baseline)

low-supply-chain-scorepackage.json:50:3

vitest (declared in package.json as "^3.2.4", scored at 3.2.4) has a Socket supply-chain score of 25/100 (below the minimum of 50). Axis scores — supply chain 97, vulnerability 25, maintenance 98, quality 78, license 100.

  38 | 		"format": "yarn run -T oxfmt \"src/**/*.{ts,tsx,js,jsx,json,md}\"",
  39 | 		"build": "yarn run -T tsx ../../internal/scripts/build-package.ts",
  40 | 		"build-api": "yarn run -T tsx ../../internal/scripts/build-api.ts",
  41 | 		"prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
  42 | 		"postpack": "../../internal/scripts/postpack.sh",
  43 | 		"pack-tarball": "yarn pack",
  44 | 		"lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
  45 | 	},
  46 | 	"devDependencies": {
  47 | 		"kleur": "^4.1.5",
  48 | 		"lazyrepo": "0.0.0-alpha.27",
  49 | 		"react": "^19.2.1",
> 50 | 		"vitest": "^3.2.4"
  51 | 	},
  52 | 	"dependencies": {
  53 | 		"@tldraw/state": "workspace:*",
  54 | 		"@tldraw/store": "workspace:*",
  55 | 		"@tldraw/utils": "workspace:*",
  56 | 		"@tldraw/validate": "workspace:*"
  57 | 	},
  58 | 	"peerDependencies": {
  59 | 		"react": "^18.2.0 || ^19.2.1",
  60 | 		"react-dom": "^18.2.0 || ^19.2.1"
  61 | 	}
  62 | }

Rule docs

circular-dependencysrc/TLStore.ts:0:0

Circular import cycle: src/TLStore.ts → src/records/TLAsset.ts → src/records/TLShape.ts → src/shapes/TLArrowShape.ts → src/records/TLBinding.ts → src/recordsWithProps.ts → src/createTLSchema.ts. Modules in the cycle can observe partially initialized exports, causing order-dependent bugs.

   1 | import { Signal, computed } from '@tldraw/state'
   2 | import {
   3 | 	SerializedStore,
   4 | 	Store,
   5 | 	StoreSchema,
   6 | 	StoreSnapshot,
   7 | 	StoreValidationFailure,
   8 | } from '@tldraw/store'
   9 | import { IndexKey, JsonObject, annotateError, sortByIndex, structuredClone } from '@tldraw/utils'
  10 | import { TLAsset, TLAssetId } from './records/TLAsset'
  11 | import { CameraRecordType, TLCameraId } from './records/TLCamera'
  12 | import { DocumentRecordType, TLDOCUMENT_ID } from './records/TLDocument'

✅ Removed (fixed) in this PR (were in baseline)

low-supply-chain-scorepackage.json:50:3

vitest (declared in package.json as "^3.2.4", scored at 3.2.4) has a Socket vulnerability score of 25/100 (below the minimum of 50). Axis scores — supply chain 97, vulnerability 25, maintenance 98, quality 78, license 100.

  38 | 		"format": "yarn run -T oxfmt \"src/**/*.{ts,tsx,js,jsx,json,md}\"",
  39 | 		"build": "yarn run -T tsx ../../internal/scripts/build-package.ts",
  40 | 		"build-api": "yarn run -T tsx ../../internal/scripts/build-api.ts",
  41 | 		"prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
  42 | 		"postpack": "../../internal/scripts/postpack.sh",
  43 | 		"pack-tarball": "yarn pack",
  44 | 		"lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
  45 | 	},
  46 | 	"devDependencies": {
  47 | 		"kleur": "^4.1.5",
  48 | 		"lazyrepo": "0.0.0-alpha.27",
  49 | 		"react": "^19.2.1",
> 50 | 		"vitest": "^3.2.4"
  51 | 	},
  52 | 	"dependencies": {
  53 | 		"@tldraw/state": "workspace:*",
  54 | 		"@tldraw/store": "workspace:*",
  55 | 		"@tldraw/utils": "workspace:*",
  56 | 		"@tldraw/validate": "workspace:*"
  57 | 	},
  58 | 	"peerDependencies": {
  59 | 		"react": "^18.2.0 || ^19.2.1",
  60 | 		"react-dom": "^18.2.0 || ^19.2.1"
  61 | 	}
  62 | }

Rule docs

circular-dependencysrc/TLStore.ts:0:0

Circular import cycle: src/TLStore.ts → src/records/TLAsset.ts → src/records/TLShape.ts �� src/shapes/TLArrowShape.ts → src/records/TLBinding.ts → src/recordsWithProps.ts → src/createTLSchema.ts. Modules in the cycle can observe partially initialized exports, causing order-dependent bugs.

   1 | import { Signal, computed } from '@tldraw/state'
   2 | import {
   3 | 	SerializedStore,
   4 | 	Store,
   5 | 	StoreSchema,
   6 | 	StoreSnapshot,
   7 | 	StoreValidationFailure,
   8 | } from '@tldraw/store'
   9 | import { IndexKey, JsonObject, annotateError, sortByIndex, structuredClone } from '@tldraw/utils'
  10 | import { TLAsset, TLAssetId } from './records/TLAsset'
  11 | import { CameraRecordType, TLCameraId } from './records/TLCamera'
  12 | import { DocumentRecordType, TLDOCUMENT_ID } from './records/TLDocument'
pierrecomputer/pierre (packages/trees) — +2 / -0

✨ Added in this PR (not present in baseline)

dangerous-html-sinksrc/render/slotHost.ts:72:5

HTML is injected from a dynamic-looking source, which can become XSS if the value is user-controlled or unsanitized.

  60 |       this.setSlotContent(slotName, null);
  61 |       return;
  62 |     }
  63 | 
  64 |     const currentContent = this.#getCurrentContent(slotName);
  65 |     if (currentContent != null && currentContent.innerHTML === normalizedHtml) {
  66 |       this.#contentBySlot.set(slotName, currentContent);
  67 |       this.#attachContent(slotName, currentContent);
  68 |       return;
  69 |     }
  70 | 
  71 |     const nextContent = document.createElement('div');
> 72 |     nextContent.innerHTML = normalizedHtml;
  73 |     this.setSlotContent(slotName, nextContent);
  74 |   }
  75 | 
  76 |   #getCurrentContent(slotName: string): HTMLElement | null {
  77 |     const trackedContent = this.#contentBySlot.get(slotName) ?? null;
  78 |     if (trackedContent != null) {
  79 |       return trackedContent;
  80 |     }
  81 | 
  82 |     const host = this.#host;
  83 |     if (host == null) {
  84 |       return null;

dangerous-html-sinksrc/react/FileTree.tsx:81:11

HTML is injected from a dynamic-looking source, which can become XSS if the value is user-controlled or unsanitized.

  69 | }
  70 | 
  71 | function renderPreloadedShadowDom(
  72 |   children: ReactNode,
  73 |   preloadedData: FileTreePreloadedData | undefined
  74 | ): ReactNode {
  75 |   if (typeof window === 'undefined' && preloadedData != null) {
  76 |     return (
  77 |       <>
  78 |         <template
  79 |           // @ts-expect-error React does not know the declarative shadow DOM attribute.
  80 |           shadowrootmode="open"
> 81 |           dangerouslySetInnerHTML={{ __html: preloadedData.shadowHtml }}
  82 |         />
  83 |         {children}
  84 |       </>
  85 |     );
  86 |   }
  87 | 
  88 |   return <>{children}</>;
  89 | }
  90 | 
  91 | function hasExistingPreloadedContent(host: HTMLElement): boolean {
  92 |   const shadowRoot = host.shadowRoot;
  93 |   if (
aidenybai/react-scan (packages/scan) — +1 / -1

✨ Added in this PR (not present in baseline)

low-supply-chain-scorepackage.json:241:5

vitest (declared in package.json as "^3.0.0", scored at 3.0.0) has a Socket supply-chain score of 25/100 (below the minimum of 50). Axis scores — supply chain 95, vulnerability 25, maintenance 99, quality 78, license 100.

  229 |     "es-module-lexer": "^2.1.0",
  230 |     "esbuild": "^0.28.0",
  231 |     "next": "*",
  232 |     "postcss": "^8.5.13",
  233 |     "postcss-cli": "^11.0.0",
  234 |     "publint": "^0.3.18",
  235 |     "react": "*",
  236 |     "react-dom": "*",
  237 |     "tailwind-merge": "^3.5.0",
  238 |     "tailwindcss": "^4.2.4",
  239 |     "terser": "^5.46.2",
  240 |     "tsup": "^8.5.1",
> 241 |     "vitest": "^3.0.0"
  242 |   },
  243 |   "peerDependencies": {
  244 |     "esbuild": ">=0.18.0",
  245 |     "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
  246 |     "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
  247 |   },
  248 |   "peerDependenciesMeta": {
  249 |     "esbuild": {
  250 |       "optional": true
  251 |     }
  252 |   },
  253 |   "optionalDependencies": {

Rule docs


✅ Removed (fixed) in this PR (were in baseline)

low-supply-chain-scorepackage.json:241:5

vitest (declared in package.json as "^3.0.0", scored at 3.0.0) has a Socket vulnerability score of 25/100 (below the minimum of 50). Axis scores — supply chain 95, vulnerability 25, maintenance 99, quality 78, license 100.

  229 |     "es-module-lexer": "^2.1.0",
  230 |     "esbuild": "^0.28.0",
  231 |     "next": "*",
  232 |     "postcss": "^8.5.13",
  233 |     "postcss-cli": "^11.0.0",
  234 |     "publint": "^0.3.18",
  235 |     "react": "*",
  236 |     "react-dom": "*",
  237 |     "tailwind-merge": "^3.5.0",
  238 |     "tailwindcss": "^4.2.4",
  239 |     "terser": "^5.46.2",
  240 |     "tsup": "^8.5.1",
> 241 |     "vitest": "^3.0.0"
  242 |   },
  243 |   "peerDependencies": {
  244 |     "esbuild": ">=0.18.0",
  245 |     "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
  246 |     "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
  247 |   },
  248 |   "peerDependenciesMeta": {
  249 |     "esbuild": {
  250 |       "optional": true
  251 |     }
  252 |   },
  253 |   "optionalDependencies": {

Rule docs

pierrecomputer/pierre (packages/diffs) — +5 / -0

✨ Added in this PR (not present in baseline)

dangerous-html-sinksrc/utils/prerenderHTMLIfNecessary.ts:9:5

HTML is injected from a dynamic-looking source, which can become XSS if the value is user-controlled or unsanitized.

   1 | export function prerenderHTMLIfNecessary(
   2 |   element: HTMLElement,
   3 |   html: string | undefined
   4 | ): void {
   5 |   if (html == null) return;
   6 |   const shadowRoot =
   7 |     element.shadowRoot ?? element.attachShadow({ mode: 'open' });
   8 |   if (shadowRoot.innerHTML === '') {
>  9 |     shadowRoot.innerHTML = html;
  10 |   }
  11 | }
  12 | 

dangerous-html-sinksrc/components/File.ts:778:5

HTML is injected from a dynamic-looking source, which can become XSS if the value is user-controlled or unsanitized.

  766 |       return;
  767 |     }
  768 |     this.themeCSSStyle.textContent = patchScrollbarGutterSize(
  769 |       this.themeCSSStyle.textContent ?? '',
  770 |       getMeasuredScrollbarGutter(shadowRoot)
  771 |     );
  772 |   }
  773 | 
  774 |   private applyFullRender(result: FileRenderResult, pre: HTMLPreElement): void {
  775 |     this.cleanupErrorWrapper();
  776 |     this.applyPreNodeAttributes(pre, result);
  777 |     this.code = getOrCreateCodeNode({ code: this.code });
> 778 |     this.code.innerHTML = this.fileRenderer.renderPartialHTML(
  779 |       this.fileRenderer.renderCodeAST(result)
  780 |     );
  781 |     pre.replaceChildren(this.code);
  782 |     this.lastRowCount = result.rowCount;
  783 |   }
  784 | 
  785 |   private applyPartialRender(
  786 |     previousRenderRange: RenderRange | undefined,
  787 |     renderRange: RenderRange | undefined
  788 |   ): boolean {
  789 |     if (previousRenderRange == null || renderRange == null) {
  790 |       return false;

dangerous-html-sinksrc/components/FileDiff.ts:1395:7

HTML is injected from a dynamic-looking source, which can become XSS if the value is user-controlled or unsanitized.

  1383 |       // Clean up addition/deletion elements if necessary
  1384 |       this.codeDeletions?.remove();
  1385 |       this.codeDeletions = undefined;
  1386 |       this.codeAdditions?.remove();
  1387 |       this.codeAdditions = undefined;
  1388 | 
  1389 |       this.codeUnified = getOrCreateCodeNode({
  1390 |         code: this.codeUnified,
  1391 |         columnType: 'unified',
  1392 |         rowSpan,
  1393 |         containerSize,
  1394 |       });
> 1395 |       this.codeUnified.innerHTML =
  1396 |         this.hunksRenderer.renderPartialHTML(unifiedAST);
  1397 |       codeElements.push(this.codeUnified);
  1398 |     } else if (deletionsAST != null || additionsAST != null) {
  1399 |       if (deletionsAST != null) {
  1400 |         shouldReplace = this.codeDeletions == null || this.codeUnified != null;
  1401 | 
  1402 |         // Clean up unified column if necessary
  1403 |         this.codeUnified?.remove();
  1404 |         this.codeUnified = undefined;
  1405 | 
  1406 |         this.codeDeletions = getOrCreateCodeNode({
  1407 |           code: this.codeDeletions,

dangerous-html-sinksrc/components/FileDiff.ts:1412:9

HTML is injected from a dynamic-looking source, which can become XSS if the value is user-controlled or unsanitized.

  1400 |         shouldReplace = this.codeDeletions == null || this.codeUnified != null;
  1401 | 
  1402 |         // Clean up unified column if necessary
  1403 |         this.codeUnified?.remove();
  1404 |         this.codeUnified = undefined;
  1405 | 
  1406 |         this.codeDeletions = getOrCreateCodeNode({
  1407 |           code: this.codeDeletions,
  1408 |           columnType: 'deletions',
  1409 |           rowSpan,
  1410 |           containerSize,
  1411 |         });
> 1412 |         this.codeDeletions.innerHTML =
  1413 |           this.hunksRenderer.renderPartialHTML(deletionsAST);
  1414 |         codeElements.push(this.codeDeletions);
  1415 |       } else {
  1416 |         // If we have no deletion column, lets clean it up if it exists
  1417 |         this.codeDeletions?.remove();
  1418 |         this.codeDeletions = undefined;
  1419 |       }
  1420 | 
  1421 |       if (additionsAST != null) {
  1422 |         shouldReplace =
  1423 |           shouldReplace ||
  1424 |           this.codeAdditions == null ||

dangerous-html-sinksrc/components/FileDiff.ts:1437:9

HTML is injected from a dynamic-looking source, which can become XSS if the value is user-controlled or unsanitized.

  1425 |           this.codeUnified != null;
  1426 | 
  1427 |         // Clean up unified column if necessary
  1428 |         this.codeUnified?.remove();
  1429 |         this.codeUnified = undefined;
  1430 | 
  1431 |         this.codeAdditions = getOrCreateCodeNode({
  1432 |           code: this.codeAdditions,
  1433 |           columnType: 'additions',
  1434 |           rowSpan,
  1435 |           containerSize,
  1436 |         });
> 1437 |         this.codeAdditions.innerHTML =
  1438 |           this.hunksRenderer.renderPartialHTML(additionsAST);
  1439 |         codeElements.push(this.codeAdditions);
  1440 |       } else {
  1441 |         // If we have no addition column, lets clean it up if it exists
  1442 |         this.codeAdditions?.remove();
  1443 |         this.codeAdditions = undefined;
  1444 |       }
  1445 |     } else {
  1446 |       // if we get in here, there's no content to render, so lets just clean
  1447 |       // everything up
  1448 |       this.codeUnified?.remove();
  1449 |       this.codeUnified = undefined;

⚠️ Partial coverage — 20 scans failed and are excluded from parity:

  • tldraw/tldraw#packages/tldraw — React Doctor failed: Invoking react-doctor on a worker failed
  • twentyhq/twenty#packages/twenty-website-new — React Doctor failed: Invoking react-doctor on a worker failed
  • twentyhq/twenty#packages/twenty-ui — React Doctor failed: Invoking react-doctor on a worker failed
  • twentyhq/twenty#packages/twenty-front — React Doctor failed: Invoking react-doctor on a worker failed
  • twentyhq/twenty#packages/twenty-sdk — React Doctor failed: Invoking react-doctor on a worker failed
  • makeplane/plane#packages/editor — React Doctor failed: Invoking react-doctor on a worker failed
  • tldraw/tldraw#packages/editor — React Doctor failed: Invoking react-doctor on a worker failed
  • twentyhq/twenty#packages/twenty-server — React Doctor failed: Invoking react-doctor on a worker failed
  • makeplane/plane#packages/propel — React Doctor failed: Invoking react-doctor on a worker failed
  • excalidraw/excalidraw#packages/excalidraw — React Doctor failed: Invoking react-doctor on a worker failed
  • tldraw/tldraw#packages/tldraw — React Doctor failed: Invoking react-doctor on a worker failed
  • twentyhq/twenty#packages/twenty-website-new — React Doctor failed: Invoking react-doctor on a worker failed
  • twentyhq/twenty#packages/twenty-front — React Doctor failed: Invoking react-doctor on a worker failed
  • excalidraw/excalidraw#packages/excalidraw — React Doctor failed: Invoking react-doctor on a worker failed
  • makeplane/plane#packages/editor — React Doctor failed: Invoking react-doctor on a worker failed
  • twentyhq/twenty#packages/twenty-server — React Doctor failed: Invoking react-doctor on a worker failed
  • makeplane/plane#packages/propel — React Doctor failed: Invoking react-doctor on a worker failed
  • twentyhq/twenty#packages/twenty-ui — React Doctor failed: Invoking react-doctor on a worker failed
  • tldraw/tldraw#packages/editor — React Doctor failed: Invoking react-doctor on a worker failed
  • twentyhq/twenty#packages/twenty-sdk — React Doctor failed: Invoking react-doctor on a worker failed

ℹ️ Re-run this parity check by commenting /rde parity on this PR.

trace 954126afc4f0e1ec717a5f57dd628d9a · rde

cursor[bot]

This comment was marked as resolved.

cursor[bot]

This comment was marked as resolved.

cursor[bot]

This comment was marked as resolved.

cursor[bot]

This comment was marked as resolved.

aidenybai added 10 commits June 9, 2026 14:17
…rule posture modules

One rule file = one rule, like every other bucket: 36 definePostureRule
modules under rules/security-posture/ replace the 1433-line scanner
monolith and the 364-line no-op stub file. The registry generator's
multi-export hack is reverted to main's single-match contract with
definePostureRule added to the alternation. checkSecurityPosture is now
a thin walk-and-dispatch over registry entries carrying `scan`.
Project-level posture rules now execute through core's environment
check machinery — same walker, same dispatch, same diagnostics —
selected via the shared shouldEnableRule capability/tag gate instead
of a hand-rolled filter. The plugin dispatcher remains only as a
parity reference until the next phase removes it.
The scan now joins reduced-motion, pnpm hardening, and expo/RN checks
in run-inspect's environment phase: skipped in diff mode, streamed
through the per-element pipeline (severity controls, inline disables,
surfaces), and gated per rule by shouldEnableRule. services/linter.ts
returns to main unchanged; posture rules are excluded from generated
oxlint configs and ESLint presets instead of shipping as no-ops.
… surface

The scanner file, the /ast subpath export, and the plugin-side copies
of core fs utils are gone; the posture rules and their bucket utils are
the single home for scan logic, and core owns the walk.
Adds ignoredTags + registry-metadata single-sourcing tests and a
package-metadata-secret fixture to the core suite, a runPostureRule
harness with colocated regressions tests for the AST and
dynamic-severity rules, registry invariants (exactly 36 tagged posture
rules, scan field nowhere else), and posture-rule authoring docs. The
changeset now describes the environment-check architecture and its
intentional behavior changes.
Mutation-verified gaps from review: the ESLint-preset posture filter in
rules.ts could be reverted without any test failing (same bug class as
the defaultEnabled preset leak), and the runInspect dispatch line could
be deleted silently. Adds a registry-derived preset regression and
three runInspect integration tests covering full-scan emission, the
diff-mode gate, and user severity overrides restamping posture
diagnostics.
CodeQL js/polynomial-redos: the unanchored (\d+)\.(\d+) scan over the
package.json version string backtracks quadratically on long digit
runs. Bounded {1,4} quantifiers keep it linear, matching the fix
parse-react-major-minor.ts already carries.
Replaces the hand-rolled digit regex (the CodeQL polynomial-redos
surface) with semver, which core already depends on: minVersion gives
the range's lower bound (>=3.4 <5 → 3.4) and coerce covers prefixed
specs like npm:tailwindcss@^3.4.1.
'Posture' meant nothing to anyone. Project-level rules are now plain
'scan rules': defineScanRule modules in rules/security-scan/, tagged
security-scan, typed ScanFinding/FileScan/ScannedFile, executed by
core's checkSecurityScan environment check. Same light shape as before
— a scan field on Rule and a distinctly-named wrapper — no parallel
type system.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c3f0d7c. Configure here.

@aidenybai aidenybai changed the title Add security posture scanner Add project-level security scan rules Jun 10, 2026
aidenybai added 4 commits June 9, 2026 18:16
One definition for both rule kinds: an AST rule provides create, a
scan rule provides scan, and defineRule injects the inert visitor
factory hosts require. defineScanRule is gone and the registry
generator's matcher is back to main's defineRule|defineRetiredRule
form. The catch-all generic overload is constrained to extends Rule so
context-sensitive scan arrows resolve to the scan overload instead of
freezing widened literal types.
The overload set existed only to serve 336 vestigial defineRule<Rule>
call sites. With the explicit type arguments (and their orphaned Rule
imports) removed, defineRule is one plain function over a two-arm
RuleDefinition union — create for AST rules, scan for scan rules — with
no generic catch-all and no overload-resolution subtleties.
…tall

Installing only the react-doctor tarball resolves
oxlint-plugin-react-doctor from the registry, so any PR adding
cross-package API fails the smoke before the pair is published.
Changesets bumps and ships both packages pinned together — packing and
installing both tarballs is what a release actually delivers.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/oxlint-plugin-react-doctor/src/plugin/utils/file-scan.ts Outdated
Comment thread packages/core/src/check-security-scan.ts
devin-ai-integration Bot and others added 7 commits June 11, 2026 02:58
Co-Authored-By: Aiden Bai <aiden.bai05@gmail.com>
…o scanByPattern

- scanByPattern gains pattern lists (first match locates the finding),
  requireAll conjunction gates, and a suppressWhen veto; 12 rules that
  re-implemented those shapes inline now declare them instead
- postmessage-origin-risk inlines its single-use AST helpers and walks
  with the shared plugin walkAst, deleting 9 one-consumer util files
  (including the duplicate scan-local walker and its TODO)
- repository-secret-file, insecure-crypto-risk, and active-static-asset
  stop evaluating the same patterns twice; public-debug-artifact anchors
  its whole-file finding explicitly so getMatchLocation can require a
  RegExp

Co-authored-by: Cursor <cursoragent@cursor.com>
…erver routes only

The extra SERVER_CONTEXT_PATTERN disjunct re-admitted test, generated,
and docs paths under api/-style directories that every sibling rule
excludes; isServerRouteSourcePath already includes the server-context
check behind the production-source filter.

Co-authored-by: Cursor <cursoragent@cursor.com>
…nto review/pr-744

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/git-provider-url-injection-risk.ts
#	packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/insecure-crypto-risk.ts
#	packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/postmessage-origin-risk.ts
#	packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/utils/scan-by-pattern.ts
…tree

The walk now collects only candidate paths up front and reads contents
lazily, one file per iteration, so the check never holds more than one
file in memory (previously up to 3x2500 files at the 2-8 MiB caps).
Bucket priority and the per-bucket cap on successful reads are
unchanged. Addresses the synchronous-resource concern from review.

Co-authored-by: Cursor <cursoragent@cursor.com>
@aidenybai

Copy link
Copy Markdown
Member Author

/rde parity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants