Skip to content

1.8.0: engine hardening — precision, badge = popup, 18 new technologies, real-site proof - #2

Merged
deepu0 merged 8 commits into
mainfrom
audit/engine-hardening
Sep 26, 2026
Merged

deepu0 merged 8 commits into
mainfrom
audit/engine-hardening

Conversation

@kiro-agent

@kiro-agent kiro-agent Bot commented Sep 26, 2026

Copy link
Copy Markdown

Implements every finding in the Sep 2026 audit (docs/AUDIT-2026-09.md) and bumps to 1.8.0. Every fix comes with a test that reproduces the 1.7.2 input and fails without the fix. Full notes: docs/RELEASE-1.8.0.md.

Results on 48 real sites (same harness before and after)

1.7.2 1.8.0
Badge count = popup count 21 / 48 48 / 48
"Tailwind MEDIUM" from class names alone 17 2 (only where Tailwind-only syntax is present)
Confirmed wrong results from the audit 9 0
Sites with nothing detected 3 2
Deep scan median / p95 46 / 208 ms 60 / 243 ms
Technologies 64 82
Tests 134 433, plus 17 real-browser checks

Fixed examples: linear.app no longer shows "Next.js v1.0.0-beta.9", clerk.com no longer shows "Bootstrap HIGH", getbootstrap.com no longer shows "Tailwind MEDIUM", and hotjar.com's Parcel (from an embedded widget) is now low. Newly detected: youtube.com → Polymer 3.5.0, astro.build → Astro, and discord.com → Webflow.

What changed

Precision (fix(detect))

  • window.next is a version hint, never proof. A DOM-clobbered <a id="next"> no longer produces Next.js + React.
  • The MAIN-world probe rejects DOM-clobbered globals and requires each library's real shape.
  • URL rules match host and path only, never the query string or a word in a file name. Query strings are also stripped from evidence and reports.
  • Inline rules must look like code, and only executable inline scripts are sampled. Prose and JSON data islands never fire a rule.
  • Tailwind: no more double counting. --tw-* variables → high, Tailwind-only syntax → medium, generic utility classes → low.
  • Versions are read from the right package and checked against plausible version ranges. Fixes remix→React, @tanstack/react-router→React Router, react-bootstrap→Bootstrap and preact→React.
  • Build tools seen only in third-party scripts, or conflicting with the site's own bundler, are shown as low.

Runtime (fix(runtime))

  • The badge now matches the popup. Each page load schedules a debounced background MAIN-world probe, and a later light scan never lowers a deep result.
  • Rescan always re-collects fresh signals.
  • The service worker always answers. The probe and signal collection time out, so the popup can no longer hang on "Scanning…".
  • Page lifecycle is handled: prerendered pages and frames other than the top one are ignored, SPA route changes clear the badge and rescan, pages restored with Back or Forward and prerendered pages that become visible scan again, and open tabs get the content script again after an install or update.
  • Web Store, file:, data: and PDF pages show an honest "can't read this page" message instead of "nothing found". The popup and service worker share one list of such pages.

Recall (18 new technologies)

  • Frameworks: Astro, Gatsby, Qwik, Preact, Lit, Polymer, Ember, AngularJS, Alpine.js, htmx.

  • CMS / site builders (new category): WordPress, Shopify, Webflow, Framer.

  • Analytics: PostHog, Plausible, Fathom, Amplitude.

  • Detectors now looking for the right names:

    Technology Now reads
    Turbopack TURBOPACK
    SvelteKit __sveltekit_<hash>
    Svelte 5 version __svelte.v
    React without DevTools _reactListening
    Vue 2 el.__vue__
    webpack 4 webpackJsonp
    Angular _ngcontent-* styles an attribute scan (the old selector could never match)

Proof (test: / chore:)

  • audit-regressions: one test per audit finding.
  • main-probe: the real probe, run in node:vm against throwing getters, Proxies and DOM clobbering.
  • signature-coverage: every rule has a positive case and a prose / file-name / search-query case. This gate found three more false positives, which are fixed here.
  • Golden corpus: 46 real sites captured with npm run corpus:capture and replayed offline in npm test.
  • scan-orchestrator: 22 tests covering the background probe, badge behaviour, timeouts, re-injection and SPA rescans.
  • Browser suite: runs headless. It checks that the badge before opening the popup equals the popup count, plus clobbering, prose, Bootstrap-class and real-Tailwind fixtures, SPA rebuild and restricted pages.
  • CI: lint, type check, unit + corpus tests on Node 20/22, a coverage gate at ≥ 80 %, the browser suite, and a store zip artifact.
  • Stronger audits: the local-only audit now checks every shipped file for network access and unexpected URLs, the manifest audit follows every import, and a version check keeps manifest, package, lockfile and release notes in sync.

Permissions unchanged: scripting, tabs, http/https host access.

Verified locally

  • npm run check: lint, type check, 433 tests, manifest and local-only audits all pass.
  • npm run test:coverage: 85 % line coverage.
  • HEADLESS=1 npm run test:browser: every case passes.

Before publishing

  • Load the unpacked build and spot-check a few sites in real Chrome.
  • Optionally regenerate the store screenshots with npm run assets.
  • The golden corpus reflects live sites as of Sep 26, 2026. When a site changes its stack, re-capture it with npm run corpus:capture -- <host>.

…validated versions

- MAIN-world probe: one try per detector, safe reads, clipped strings, never
  String() page objects; rejects DOM-clobbered globals (Node/HTMLCollection)
  and requires library shape (axios.get, bootstrap.Modal, gtag(), ...)
- window.next is a version hint only (weight 1); Next.js is promoted to high
  only with strong proof. Fixes 'Next.js v1.0.0-beta.9' on linear.app and
  <a id="next"> producing Next.js + React
- script/css rules match host+path with the query stripped (opt-in
  matchQuery for ?dpl=); unanchored vendor words anchored to hosts/packages
- inline rules are code-shaped; content script samples executable scripts
  only (no JSON/ld+json data islands)
- Tailwind: no double count; Tailwind-only syntax => medium, --tw-* vars =>
  high, generic utility soup => low (fixes Tailwind MEDIUM on getbootstrap.com)
- versions: package-boundary regexes, per-tech major ranges, build metadata
  dropped (fixes remix->react, @tanstack/react-router->react-router,
  react-bootstrap->bootstrap, preact->react, pagination cursor->next)
- correct globals: TURBOPACK, __sveltekit_<hash>, __svelte.v, webpackJsonp,
  _reactListening, el.__vue__; _ngcontent attribute scan now works
- new: AngularJS, Astro, Gatsby, Qwik, Preact, Lit, Polymer, Ember, Alpine.js,
  htmx, WordPress, Shopify, Webflow, Framer, PostHog, Plausible, Fathom,
  Amplitude (new 'CMS / Site builder' category)
- build tools seen only in third-party scripts, or conflicting with a
  meta-framework's bundler, are demoted to low
- exports: headline cap, Markdown escaping, JSON schemaVersion, report
  evidence without query strings
…uards

- every LIGHT_SCAN schedules a debounced background MAIN-world probe; light
  results never overwrite a deep one for the same document (badge = popup)
- deep scan always re-collects signals; cache is only a fallback
- service worker always answers (try/catch); probe/collect time out
- results are labelled with the document that answered
- LIGHT_SCAN ignored for non-top frames and non-active documents; SPA route
  changes clear the badge and rescan; bfcache/prerender re-post; open tabs
  re-injected on install/update; content-script guard survives reloads
- shared URL policy (file:, data:, blob:, Web Store...) and an honest
  'unreachable' state instead of 'nothing found'
- popup: sequenced loads, Rescan disabled while scanning, focus-visible,
  pluralised tooltip ('1 technology')
…ules)

The gate found three more prose false positives, fixed here:
- webpack: any path containing 'webpack-' (an image named webpack-logo.png)
- tanstack-start: any URL containing 'tanstack-start'
- turbopack: case-insensitive inline match on the word 'Turbopack'
Identifier-based inline rules are now case-sensitive.
scripts/capture-corpus.mjs loads the real extension, captures the content
script's light signals and the MAIN-world probe globals (via a new
extension-page-only CAPTURE_SIGNALS message), scrubs query strings and
non-evidence inline text, and writes tests/corpus/<host>.json.
tests/corpus.test.js replays them through merge + detect() with
hand-reviewed must/mustNot/mustNotSolid/noVersion expectations and a full
snapshot (UPDATE_CORPUS=1 to accept reviewed changes).
- GitHub Actions: lint, type check, unit + corpus (Node 20/22), coverage
  gate (>=80% lines), real-extension browser suite in headless Chromium,
  store zip artifact
- ESLint 9 flat config; tsc --checkJs over shared/, background/, popup/
- local-only audit scans every shipped file for fetch/XHR/sendBeacon/
  WebSocket/EventSource/importScripts/remote import/beacons and unexpected
  URLs; manifest audit follows side-effect and dynamic imports
- scripts/check-version.mjs: manifest, package, lockfile, release notes
- replace two no-op tests (conditional Tailwind assertion, SHA length)

@xhawk-ai xhawk-ai 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.

📋 Review Summary — Not ready to merge

2 medium findings would be worth fixing before merging.

Findings

  1. Medium Correctness pushState route changes never invalidate or rescan the tab ▶
  2. Medium Correctness Images and fetches are still treated as script evidence ▶
Fix with agent prompt
These are the findings from a code review of this pull request.

## 1. pushState route changes never invalidate or rescan the tab
Path: background/service-worker.js
Line: 74

Issue: History API navigations emit `tabs.onUpdated` with `changeInfo.url`, not `changeInfo.status === 'loading'` or `'complete'`. Since the content script explicitly does not patch `history.pushState`/`replaceState`, an SPA route change that changes the route stack leaves the old cache and badge in place until some unrelated scan happens.
Suggested fix:
- Handle `changeInfo.url` in this listener: ignore same-document hash changes, invalidate the tab for a new route, clear the badge, and schedule a deep scan for `changeInfo.url`.

## 2. Images and fetches are still treated as script evidence
Path: content/content-script.js
Line: 219

Issue: Every Resource Timing entry is pushed into the `scripts` pool, and the detector later runs script rules over any http(s) URL. A page that only loads `https://cdn.shopify.com/s/files/.../photo.jpg` as an image is therefore reported as Shopify with high confidence even though no Shopify script or runtime was present; the same failure mode applies to other host-only vendor rules.
Suggested fix:
- Filter performance entries by `initiatorType` before adding them: keep script-like entries in `scripts`, stylesheet/css entries in `stylesheets`, and exclude image/fetch/beacon/font/media resources from signature matching.

---

For each finding above, determine whether it is valid and should be fixed. If so, fix it directly. Where a finding offers several remedies, pick one and say why. Leave the pull request's own changed files alone unless a fix requires touching them.

Summary

The change hardens WhatStack’s detection engine, adds background deep scans so the badge should match the popup, expands the signature set, and adds CI/corpus/browser coverage. Two correctness issues remain: SPA pushState navigations are not actually handled by the service-worker listener, and non-script resource timing entries are still fed into script evidence, allowing high-confidence vendor false positives from images or fetches.

Commands

Re-review the latest changes:

@xhawk-ai review again

Resolve all review threads and post a summary:

@xhawk-ai resolve all

Comment on lines +68 to +74
if (changeInfo.status === 'loading') {
// A new document (including a reload of the same URL) or an SPA route
// change. Drop what we knew; the next LIGHT_SCAN or onTabComplete rebuilds it.
orchestrator.invalidateTab(tabId);
}
if (changeInfo.status === 'complete' && tab && tab.url) {
orchestrator.onTabComplete(tabId, tab.url);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium Correctness
pushState route changes never invalidate or rescan the tab

History API navigations emit tabs.onUpdated with changeInfo.url, not changeInfo.status === 'loading' or 'complete'. Since the content script explicitly does not patch history.pushState/replaceState, an SPA route change that changes the route stack leaves the old cache and badge in place until some unrelated scan happens.

Suggestions

Handle changeInfo.url in this listener: ignore same-document hash changes, invalidate the tab for a new route, clear the badge, and schedule a deep scan for changeInfo.url.

Comment thread content/content-script.js
var perf = performance.getEntriesByType ? performance.getEntriesByType('resource') : [];
var limit = Math.min(perf.length, 600);
for (var i = 0; i < limit; i++) {
if (perf[i] && perf[i].name) scripts.push(String(perf[i].name).slice(0, 2000));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium Correctness
Images and fetches are still treated as script evidence

Every Resource Timing entry is pushed into the scripts pool, and the detector later runs script rules over any http(s) URL. A page that only loads https://cdn.shopify.com/s/files/.../photo.jpg as an image is therefore reported as Shopify with high confidence even though no Shopify script or runtime was present; the same failure mode applies to other host-only vendor rules.

Suggestions

Filter performance entries by initiatorType before adding them: keep script-like entries in scripts, stylesheet/css entries in stylesheets, and exclude image/fetch/beacon/font/media resources from signature matching.

@deepu0
deepu0 merged commit 49abc75 into main Sep 26, 2026
6 checks passed
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.

2 participants