Skip to content

feat(security-questionnaire): add browser extension - #3064

Open
tofikwest wants to merge 9 commits into
mainfrom
feat/security-questionnaire-extension
Open

feat(security-questionnaire): add browser extension#3064
tofikwest wants to merge 9 commits into
mainfrom
feat/security-questionnaire-extension

Conversation

@tofikwest

@tofikwest tofikwest commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Adds the Comp AI Security Questionnaire Chrome extension under apps/browser-extension/security-questionnaire-ext, including popup/sidepanel/content scripts, org switching, Google Forms and Sheets support, and Chrome Web Store release workflow.

API changes:

  • Allows webpage-only /v1/questionnaire/answer-single generation without persisting questionnaire records.
  • Adds COMP_EXTENSION_TRUSTED_ORIGINS and method/path-scoped extension CORS/origin handling.

Validation:

  • bun run --filter '@trycompai/security-questionnaire-extension' test
  • bun run --filter '@trycompai/security-questionnaire-extension' typecheck
  • bun run --filter '@trycompai/security-questionnaire-extension' build
  • cd apps/api && bunx jest src/auth/origin-check.middleware.spec.ts src/auth/cors-origin.middleware.spec.ts --passWithNoTests

Note:

  • Full API typecheck currently fails in unrelated existing cloud-security/integration-platform spec files, not in this PR's auth/extension files.

Summary by cubic

Adds a Chrome MV3 extension to draft and insert security questionnaire answers on vendor pages and Google Forms/Docs/Sheets, and replaces CORS handling with a stricter origin policy and route-scoped extension checks. Google Sheets OAuth is optional so the extension can ship without direct Sheets API access.

  • New Features

    • Extension: @trycompai/security-questionnaire-extension with popup and side panel, inline buttons/preview, org switching, domain confirmation, auto content-script injection, and a review/approve/insert queue. Google Sheets via direct Sheets API (auto width/wrap) or guided paste; Google Docs adds a footer copy action.
    • API: extracted origin-policy (HTTPS-only wildcard trust, Chrome extension route allowlist) and added cors-origin.middleware with preflight/credentials, Vary headers, and HEAD in allowed methods; /v1/questionnaire/answer-single accepts webpage-only drafts and enforces org-scoped saves with improved Swagger.
    • Release/Docs: GitHub Action to build/sign/publish from release; publish via service account JWT using Chrome Web Store v1.1 endpoints, with a publish_target selector; .wxt/dist ignored; Google Sheets OAuth client ID optional and manifest check gated; added extension docs and nav link.
  • Bug Fixes

    • Detection: keep short questions containing “control/requirement/question”; ignore answer-style statements; reject invalid sheet targets (row/col 0) and negative sheet IDs for formatting.
    • Concurrency/data loss: apply batch results to the stored queue and skip items edited/approved in-flight; serialize storage reads/merges; keep sheet mapping dialog populated on save errors; surface autosave failures.
    • Robustness: guard JSON and GViz parsing and gid decoding; fall back across Sheets endpoints and to local detection when background messages fail; add Vary on all CORS paths (including rejected) and keep HEAD allowed to preserve cross-origin preflights; trim organization IDs and reject empty question IDs or whitespace-only approvals; tighten response validation.
    • UX/polish: closed Shadow DOM for domain confirmation; escape leading formula chars in Sheets TSV; prevent generate-all from overwriting approved/edited answers; scope inline previews per field; finalize item status after single-generate failures; add tests for paste plans, batch selection, Docs footer/clipboard, origin policy, and more.

Written for commit 717bc8a. Summary will update on new commits.

Review in cubic

@vercel

vercel Bot commented Jun 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app Ready Ready Preview, Comment Jul 27, 2026 8:03pm
comp-framework-editor Ready Ready Preview, Comment Jul 27, 2026 8:03pm
portal Ready Ready Preview, Comment Jul 27, 2026 8:03pm

Request Review

@mintlify

mintlify Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
CompAI 🟢 Ready View Preview Jun 8, 2026, 11:18 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@cubic-dev-ai cubic-dev-ai 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.

3 issues found across 117 files

Confidence score: 2/5

  • In apps/browser-extension/security-questionnaire-ext/src/lib/queue.ts, the regenerate flow can overwrite manually edited non-empty item.answer values, which risks losing user-entered content during normal use — preserve edited answers and only seed generated text for untouched/empty items.
  • In apps/browser-extension/security-questionnaire-ext/src/lib/dom/sheets-dom.ts, matrix allocation appears to use absolute sheet indices instead of visible-range-normalized indices, which can create oversized arrays and slow parsing on larger sheets — normalize indices to visible bounds before allocation.
  • In apps/browser-extension/security-questionnaire-ext/src/entrypoints/sidepanel/active-tab.ts (and duplicate logic in popup/main.ts), duplicated getHost implementations can drift and cause inconsistent host handling over time — import and reuse a single shared implementation.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/browser-extension/security-questionnaire-ext/src/entrypoints/sidepanel/active-tab.ts">

<violation number="1" location="apps/browser-extension/security-questionnaire-ext/src/entrypoints/sidepanel/active-tab.ts:9">
P3: Duplicate `getHost` implementation in popup/main.ts — import from active-tab.ts instead.</violation>
</file>

<file name="apps/browser-extension/security-questionnaire-ext/src/lib/queue.ts">

<violation number="1" location="apps/browser-extension/security-questionnaire-ext/src/lib/queue.ts:137">
P1: Regenerate path clobbers manually edited answers. Preserve edited non-empty item.answer and skip seeding generated text for those items.</violation>
</file>

<file name="apps/browser-extension/security-questionnaire-ext/src/lib/dom/sheets-dom.ts">

<violation number="1" location="apps/browser-extension/security-questionnaire-ext/src/lib/dom/sheets-dom.ts:27">
P2: Matrix allocation uses absolute sheet indices instead of normalizing to visible bounds, causing unnecessary large arrays and slow parsing.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic
Partial review: This PR has more than 100 files, so cubic reviewed the highest-priority files first.
For a deeper review of large PRs, comment @cubic-dev-ai ultrareview. Learn more.

Re-trigger cubic

return {
...item,
status: hasAnswer ? 'generated' : 'flagged',
answer: hasAnswer ? answer : null,

@cubic-dev-ai cubic-dev-ai Bot Jun 8, 2026

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.

P1: Regenerate path clobbers manually edited answers. Preserve edited non-empty item.answer and skip seeding generated text for those items.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/browser-extension/security-questionnaire-ext/src/lib/queue.ts, line 137:

<comment>Regenerate path clobbers manually edited answers. Preserve edited non-empty item.answer and skip seeding generated text for those items.</comment>

<file context>
@@ -0,0 +1,272 @@
+    return {
+      ...item,
+      status: hasAnswer ? 'generated' : 'flagged',
+      answer: hasAnswer ? answer : null,
+      confidence: hasAnswer ? getAnswerConfidence(params.answer) : 'low',
+      sources: params.answer.sources,
</file context>
Fix with cubic

Comment thread apps/browser-extension/security-questionnaire-ext/src/entrypoints/content.ts Outdated
Comment thread apps/browser-extension/security-questionnaire-ext/src/lib/sheets-paste-plan.ts Outdated
Comment thread apps/api/src/auth/origin-policy.ts
Comment thread apps/browser-extension/security-questionnaire-ext/src/lib/scan-debug.ts Outdated
Comment thread apps/browser-extension/security-questionnaire-ext/src/lib/dom/review-panel.ts Outdated
return tab;
}

export function getHost(url: string): string {

@cubic-dev-ai cubic-dev-ai Bot Jun 8, 2026

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.

P3: Duplicate getHost implementation in popup/main.ts — import from active-tab.ts instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/browser-extension/security-questionnaire-ext/src/entrypoints/sidepanel/active-tab.ts, line 9:

<comment>Duplicate `getHost` implementation in popup/main.ts — import from active-tab.ts instead.</comment>

<file context>
@@ -0,0 +1,15 @@
+  return tab;
+}
+
+export function getHost(url: string): string {
+  try {
+    return new URL(url).host;
</file context>
Fix with cubic

- Use a closed shadow root for the domain confirmation dialog so the host
  page cannot reach its controls.
- Escape leading formula characters when building Sheets paste TSV, so
  answers starting with "-" stay text instead of being evaluated.
- Restrict the wildcard trusted-origin match to HTTPS; explicit localhost
  entries are unaffected.
- Stop generate-all from overwriting approved or manually edited answers.
- Hold the inline preview locally so concurrent generates cannot render a
  result into another field's preview.
- Finalize queue item status when single generation fails, instead of
  leaving the item stuck in "generating".
- Implement the Google Docs footer copy action, which had no handler, and
  label/enable the button to match.

Adds coverage for the paste plan, batch candidate selection, docs footer,
clipboard text, and the origin policy.
…view-fixes

# Conflicts:
#	apps/api/src/auth/auth.server.ts
#	bun.lock
…release, add docs

The release workflow required a Google OAuth client id and failed the build
when the manifest had none, so the extension could not ship without direct
Google Sheets API access. Treat it as optional: verify the manifest only
when the secret is set, and drop it from the required-secret check.

Adds the extension documentation page and links it from the docs nav.
Detection and data correctness:
- Stop dropping questionnaire rows whose text looks like a column label.
  Any cell under 48 characters containing "control", "requirement" or
  "question" was discarded, which silently lost common questions such as
  "Are security controls documented?".
- Ignore answer-style statements in the keyword fallback so filled-in
  answers are not re-detected as new questions.
- Reject row/column 0 when parsing sheet targets; A1 notation is 1-based.
- Reject negative sheet ids when building formatting requests.

Concurrency and data loss:
- Apply batch results to the stored queue rather than a snapshot, and skip
  items the user edited or approved while generation was in flight.
- Serialize storage read-merge-writes so concurrent updates are not lost.
- Keep the sheet mapping dialog populated when a save fails.
- Surface autosave failures instead of dropping them.

Robustness:
- Guard JSON parsing in the API client, GViz table parsing and gid decoding.
- Let one Sheets endpoint failure fall through to the next.
- Fall back to local detection when the background message fails.
- Add Vary headers to CORS responses, including rejected origins.
- Trim organization ids and reject empty question ids and whitespace-only
  approvals.
- Tighten isItemResponse to validate the queue it claims to carry.

Cleanup: drop the unused ReviewPanel and the unhandled
comp:generate-visible-page message, dedupe getHost and surface validation.
@vercel
vercel Bot temporarily deployed to Preview – portal July 27, 2026 18:18 Inactive
The replacement CORS middleware dropped HEAD, which app.enableCors allowed
by default, so cross-origin HEAD preflights would have started failing.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 30 files (changes from recent commits).

Confidence score: 3/5

  • In apps/browser-extension/security-questionnaire-ext/src/lib/background/batch-generation.ts, commit updates writeQueue with chained promises but doesn’t recover from a rejected loadQueue()/saveQueue(), so one transient failure can poison the queue and block all later writes; this can stall batch generation and leave data unsaved—wrap each queued step with error handling/recovery so the chain continues after failures.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/browser-extension/security-questionnaire-ext/src/lib/background/batch-generation.ts">

<violation number="1" location="apps/browser-extension/security-questionnaire-ext/src/lib/background/batch-generation.ts:50">
P1: The `commit` function chains writes via `writeQueue = writeQueue.then(...)` but does not isolate failures. If a single `loadQueue()` or `saveQueue()` call rejects, the entire `writeQueue` promise chain becomes permanently rejected, causing all subsequent in-flight commits to silently skip their work — the generated answers are computed but never persisted. This is a data persistence gap during concurrent batch generation.

Unlike the similar `serializeWrite` in `storage.ts` (which uses `result.catch(() => undefined)` to keep the chain alive), the `commit` function has no error isolation. The `runConcurrent` worker pool runs with shared concurrency, so a failure in one worker can leave other in-flight workers' answers unsaved without any indication to the caller (the handler is simply never invoked on a rejected chain).

**Recommendation**: Add error isolation similar to `serializeWrite` so a single commit failure does not silently disable all subsequent commits.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

// Each result is applied to the freshly stored queue inside a serialized
// section, so a long-running request cannot write back a stale snapshot.
let writeQueue: Promise<void> = Promise.resolve();
const commit = (

@cubic-dev-ai cubic-dev-ai Bot Jul 27, 2026

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.

P1: The commit function chains writes via writeQueue = writeQueue.then(...) but does not isolate failures. If a single loadQueue() or saveQueue() call rejects, the entire writeQueue promise chain becomes permanently rejected, causing all subsequent in-flight commits to silently skip their work — the generated answers are computed but never persisted. This is a data persistence gap during concurrent batch generation.

Unlike the similar serializeWrite in storage.ts (which uses result.catch(() => undefined) to keep the chain alive), the commit function has no error isolation. The runConcurrent worker pool runs with shared concurrency, so a failure in one worker can leave other in-flight workers' answers unsaved without any indication to the caller (the handler is simply never invoked on a rejected chain).

Recommendation: Add error isolation similar to serializeWrite so a single commit failure does not silently disable all subsequent commits.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/browser-extension/security-questionnaire-ext/src/lib/background/batch-generation.ts, line 50:

<comment>The `commit` function chains writes via `writeQueue = writeQueue.then(...)` but does not isolate failures. If a single `loadQueue()` or `saveQueue()` call rejects, the entire `writeQueue` promise chain becomes permanently rejected, causing all subsequent in-flight commits to silently skip their work — the generated answers are computed but never persisted. This is a data persistence gap during concurrent batch generation.

Unlike the similar `serializeWrite` in `storage.ts` (which uses `result.catch(() => undefined)` to keep the chain alive), the `commit` function has no error isolation. The `runConcurrent` worker pool runs with shared concurrency, so a failure in one worker can leave other in-flight workers' answers unsaved without any indication to the caller (the handler is simply never invoked on a rejected chain).

**Recommendation**: Add error isolation similar to `serializeWrite` so a single commit failure does not silently disable all subsequent commits.</comment>

<file context>
@@ -41,8 +42,22 @@ export async function generateQueueItemsInBatches(params: {
+  // Each result is applied to the freshly stored queue inside a serialized
+  // section, so a long-running request cannot write back a stale snapshot.
+  let writeQueue: Promise<void> = Promise.resolve();
+  const commit = (
+    apply: (current: TabQuestionQueue) => TabQuestionQueue,
+  ): Promise<void> => {
</file context>
Fix with cubic

… the working API

Switches Chrome Web Store auth from a personal refresh token to a service
account JWT, which does not expire and is not tied to an individual account.

Verified against the live API: the token exchange succeeds and the service
account can read the store item. Every v2 endpoint returns 404 for this
publisher while v1.1 works with the same token, so the upload, status and
publish calls now use v1.1. That API is supported until 2026-10-15.

Adds a publish_target choice so a manual run can ship to trusted testers
before going public.
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.

1 participant