Skip to content

Add name squatting admin dashboard and exact match publish time refusal logic - #2081

Draft
autumnfound wants to merge 4 commits into
eclipse-openvsx:mainfrom
autumnfound:malowe/main/ovsxorg-1949
Draft

Add name squatting admin dashboard and exact match publish time refusal logic#2081
autumnfound wants to merge 4 commits into
eclipse-openvsx:mainfrom
autumnfound:malowe/main/ovsxorg-1949

Conversation

@autumnfound

Copy link
Copy Markdown
Contributor

This patch contains 2 new features. The first is a new admin dashboard tile for monitoring and addressing name_squatting flagged extensions + versions in a new view. This will allow for each of the versions to be resolved as false positive or to soft delete an extension if they are found to be malicious.

The second feature is a publish-time check that compares the display name to other active extensions outside of the users' namespaces. This does a case insensitive match on the display name, and if there is a match either for new extensions or a renamed new version of an extension, the upload gets rejected.

There is currently a small carve out in the second feature for currently existing extensions that collide as we cannot know which is the legitimate version and we don't want to immediately reject those uploads. It is assumed that the NAME_SQUATTING check results will be monitored and resolved to remove these conflicts as they are surfaced.

Resolves #1949.

@autumnfound
autumnfound marked this pull request as draft August 20, 2026 18:23
@autumnfound
autumnfound force-pushed the malowe/main/ovsxorg-1949 branch from 0060c12 to 3337bed Compare August 20, 2026 18:24
@autumnfound

Copy link
Copy Markdown
Contributor Author

Report generated by Claude Opus 5 when requested to evaluate the performance with similar to production numbers for the impact of checking for a same-named extension with the new index.

TLDR: Actual lookup performance is very fast and should not be noticable in a running environment. We should consider doing some real world testing on staging to ensure that there is no significant impact with real data.

Performance: display name conflict check

What runs, and when

The check has two components. Both are gated so that a publication is only checked when it adopts a display name — i.e. when the extension is new, or when this version renames it.

┌───────────────────────────────────────────────────┬───────────────────────────────────────────────────────────┐
│                     Component                     │                          Runs on                          │
├───────────────────────────────────────────────────┼───────────────────────────────────────────────────────────┤
│ findActiveExtensionByDisplayName (conflict query) │ New extensions, and versions that change the display name │
├───────────────────────────────────────────────────┼───────────────────────────────────────────────────────────┤
│ findLatestVersion (rename comparison)             │ Every publish of an existing extension                    │
└───────────────────────────────────────────────────┴───────────────────────────────────────────────────────────┘

Both call sites — checkPublishPreconditions (scanning enabled only, fails fast before scanners are occupied) and doPublish (every publication) — apply the same gate.

Method

PostgreSQL 16.15; shared_buffers=2GB, effective_cache_size=6GB, work_mem=32MB, random_page_cost=1.1. Synthetic dataset shaped from current registry growth (~50–100 new extensions/day, ~450 new versions/day), carrying the full index set from V1_7, V1_10, V1_13, V1_22, V1_37 plus the new V1_72 index, and the personal_access_token / user_data / signature_key_pair tables that findLatestVersion joins.

Latency via pgbench -M prepared (server-side prepared statements, as pgjdbc uses), single connection, 10s per measurement, warm cache. Display names are held stable across an extension's releases, as they are in practice — so a name carried by N extensions matches N × (their version counts) rows in the index, not N rows. That is the realistic and less favourable shape.

┌─────────────────────────┬──────────────────────────┬──────────────────────────────┐
│                         │    Scale A (current)     │    Scale B (3× headroom)     │
├─────────────────────────┼──────────────────────────┼──────────────────────────────┤
│ Extensions              │ 20,000 (19,600 active)   │ 60,000 (58,800 active)       │
├─────────────────────────┼──────────────────────────┼──────────────────────────────┤
│ Namespaces              │ 5,000                    │ 15,000                       │
├─────────────────────────┼──────────────────────────┼──────────────────────────────┤
│ Versions                │ 470,000 (432,000 active) │ 1,410,000 (1,296,000 active) │
├─────────────────────────┼──────────────────────────┼──────────────────────────────┤
│ extension_version table │ 71 MB                    │ 215 MB                       │
├─────────────────────────┼──────────────────────────┼──────────────────────────────┤
│ Display-name index      │ 3.6 MB                   │ 11 MB                        │
└─────────────────────────┴──────────────────────────┴──────────────────────────────┘

Conflict query — findActiveExtensionByDisplayName

┌─────────────────────────────────────────┬────────────────────┬─────────────────┬──────────┬───────┬───────┬────────┬─────────────────┬──────────┬───────┬───────┬────────┐
│                Scenario                 │   Candidate rows   │ Scale A buffers │  A p50   │ A p95 │ A p99 │ A tps  │ Scale B buffers │  B p50   │ B p95 │ B p99 │ B tps  │
├─────────────────────────────────────────┼────────────────────┼─────────────────┼──────────┼───────┼───────┼────────┼─────────────────┼──────────┼───────┼───────┼────────┤
│ No conflict                             │ 0 versions / 0 ext │ 3               │ 0.029 ms │ 0.044 │ 0.055 │ 25,561 │ 3               │ 0.030 ms │ 0.045 │ 0.060 │ 28,638 │
├─────────────────────────────────────────┼────────────────────┼─────────────────┼──────────┼───────┼───────┼────────┼─────────────────┼──────────┼───────┼───────┼────────┤
│ 1 conflict                              │ 18 / 1             │ 82              │ 0.059 ms │ 0.099 │ 0.128 │ 12,477 │ 82              │ 0.058 ms │ 0.094 │ 0.114 │ 12,404 │
├─────────────────────────────────────────┼────────────────────┼─────────────────┼──────────┼───────┼───────┼────────┼─────────────────┼──────────┼───────┼───────┼────────┤
│ 5 conflicts                             │ 100 / 5            │ 154             │ 0.078 ms │ 0.117 │ 0.141 │ 10,710 │ 153             │ 0.078 ms │ 0.137 │ 0.161 │ 10,673 │
├─────────────────────────────────────────┼────────────────────┼─────────────────┼──────────┼───────┼───────┼────────┼─────────────────┼──────────┼───────┼───────┼────────┤
│ 5 conflicts, all in excluded namespaces │ 100 / 5            │ 612             │ 0.189 ms │ 0.200 │ 0.233 │ 4,910  │ 612             │ 0.201 ms │ 0.281 │ 0.429 │ 4,890  │
└─────────────────────────────────────────┴────────────────────┴─────────────────┴──────────┴───────┴───────┴────────┴─────────────────┴──────────┴───────┴───────┴────────┘

Rename comparison — findLatestVersion

┌────────────────────────────────────────┬───────────────────────────────────┬─────────────────┬──────────┬───────┬───────┬─────────────────┬──────────┬───────┬───────┐
│                 Lookup                 │             Call site             │ Scale A buffers │  A p50   │ A p95 │ A p99 │ Scale B buffers │  B p50   │ B p95 │ B p99 │
├────────────────────────────────────────┼───────────────────────────────────┼─────────────────┼──────────┼───────┼───────┼─────────────────┼──────────┼───────┼───────┤
│ By entity                              │ doPublish, existing extension     │ 14              │ 0.033 ms │ 0.045 │ 0.074 │ 14              │ 0.034 ms │ 0.044 │ 0.059 │
├────────────────────────────────────────┼───────────────────────────────────┼─────────────────┼──────────┼───────┼───────┼─────────────────┼──────────┼───────┼───────┤
│ By namespace + name                    │ preconditions, existing extension │ 232             │ 0.102 ms │ 0.142 │ 0.198 │ 233             │ 0.098 ms │ 0.111 │ 0.165 │
├────────────────────────────────────────┼───────────────────────────────────┼─────────────────┼──────────┼───────┼───────┼─────────────────┼──────────┼───────┼───────┤
│ By namespace + name, no such extension │ preconditions, new extension      │ 19              │ 0.037 ms │ 0.050 │ 0.066 │ 20              │ 0.039 ms │ 0.065 │ 0.091 │
└────────────────────────────────────────┴───────────────────────────────────┴─────────────────┴──────────┴───────┴───────┴─────────────────┴──────────┴───────┴───────┘

Total per publication

Arithmetic sums of the Scale A p50 figures above, with scanning enabled so both call sites run:

┌──────────────────────────────────────────┬────────────────────────────────────────────────────────────────────┬────────────┐
│               Publication                │                            Queries run                             │ Summed p50 │
├──────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼────────────┤
│ New extension, name free                 │ latest-by-name (null) + 2 × conflict query                         │ ~0.095 ms  │
├──────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼────────────┤
│ New extension, name taken                │ latest-by-name (null) + conflict query → rejected at preconditions │ ~0.096 ms  │
├──────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼────────────┤
│ Further version, name unchanged          │ latest-by-name + latest-by-entity, conflict query skipped          │ ~0.135 ms  │
├──────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼────────────┤
│ Further version, renamed to a free name  │ 2 × latest lookup + 2 × conflict query                             │ ~0.193 ms  │
├──────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼────────────┤
│ Further version, renamed to a taken name │ latest-by-name + conflict query → rejected at preconditions        │ ~0.161 ms  │
└──────────────────────────────────────────┴────────────────────────────────────────────────────────────────────┴────────────┘

At ~100 new extensions and ~450 version publishes per day, that totals under 100 ms of database time per day. With scanning disabled only the doPublish call site runs, roughly halving each figure.

Verification

  • Zero sequential scans on extension, extension_version, or namespace in all 14 measurements. Every plan is index-driven.
  • Latency is flat across a 3× increase in data. Cost tracks how many extensions carry the queried name, not table size, so it does not drift as the registry grows.
  • The generic plan is identical. pgjdbc switches to a server-side generic plan after prepareThreshold=5; confirmed via pg_prepared_statements (generic_plans=5, custom_plans=5) that plan shape and buffer counts are unchanged. No degradation on the hot path.
  • No locks. Both queries are plain SELECTs — MVCC snapshot only.
  • The expression matches the index: jOOQ emits lower(TRIM(BOTH FROM display_name)), which PostgreSQL canonicalises to the indexed lower(btrim(display_name)), and the query's active = true matches the partial index predicate.

Reading notes

  • The "5 conflicts" row is not 5× the cost of one. LIMIT 1 exits at the first conflicting extension found (rows=1), so its cost depends on where in the index scan the first match falls, not on the total.
  • The all-excluded row is the true worst case for a given candidate count: every one of the 100 candidates must be evaluated because nothing can satisfy the query (rows=0), so there's no early exit. It's still ~0.2 ms. Given the same-namespace carve-out, high conflict counts for a single name shouldn't arise in practice.

Caveats

  • Synthetic data shaped from stated growth rates, not a production dump.
  • Single connection, warm cache, no concurrent write load.
  • signature_key_pair holds one row, so its join shows a sequential scan on a one-row table — cheap and realistic for a tiny table, but a synthetic-data artifact rather than a measured production plan.
  • The per-publication totals are sums of independently measured components, not end-to-end request measurements.

Copilot AI 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.

Pull request overview

Adds end-to-end support for detecting and moderating name squatting: a new admin dashboard section for reviewing NAME_SQUATTING findings and taking actions (clear as false-positive or soft-delete), plus a publish-time “exact display name” collision check to reject obvious impersonation attempts (while excluding the publisher’s own namespaces and skipping checks when mirroring).

Changes:

  • Web UI: new “Name Squatting” admin dashboard route/view with filtering, paging, per-state counts, and moderation dialogs + unit tests.
  • Server: new admin API endpoints and service logic to list/count flagged extensions and apply moderation actions.
  • Publish pipeline + DB: reject publications that adopt a display name already used by another active extension; add a supporting partial expression index.

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
webui/test/unit/support/name-squatting-data.ts Adds test fixtures for name-squatting flags/counts.
webui/test/unit/pages/admin-dashboard/name-squatting/name-squatting.spec.tsx Adds Name Squatting page behavior tests (listing, filtering, actions).
webui/test/unit/pages/admin-dashboard/name-squatting/name-squatting-row.spec.tsx Adds row-level UI tests (actions/disabled states/findings expansion).
webui/test/setup.ts Adds jsdom getComputedStyle workaround for MUI dialog calc() issue.
webui/src/pages/admin-dashboard/name-squatting/use-name-squatting.ts Adds React Query hooks + query keys for name-squatting endpoints.
webui/src/pages/admin-dashboard/name-squatting/name-squatting.tsx Implements Name Squatting admin page (filters, counts, paging, actions).
webui/src/pages/admin-dashboard/name-squatting/name-squatting-row.tsx Implements per-extension flagged row UI and findings table.
webui/src/pages/admin-dashboard/name-squatting/name-squatting-action-dialog.tsx Implements confirmation dialog + error display for moderation actions.
webui/src/pages/admin-dashboard/admin-dashboard.tsx Adds nav tile + route to Name Squatting section.
webui/src/pages/admin-dashboard/admin-dashboard-routes.ts Adds route constant for name-squatting page.
webui/src/extension-registry-types.ts Adds frontend types for name-squatting list/count/action payloads.
webui/src/extension-registry-service.ts Adds admin service methods hitting /admin/name-squatting/* endpoints.
webui/CHANGELOG.md Documents new admin dashboard Name Squatting section.
server/src/test/java/org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.java Covers new repository methods in smoke test execution list.
server/src/test/java/org/eclipse/openvsx/repositories/ExtensionJooqRepositoryTest.java Adds tests for display-name conflict lookup behavior.
server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java Updates handler wiring due to new constructor parameter.
server/src/test/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandlerTest.java Adds tests for publish-time display-name collision rejection/exclusions.
server/src/test/java/org/eclipse/openvsx/admin/NameSquattingAPITest.java Adds HTTP-layer tests for the new name-squatting admin API.
server/src/test/java/org/eclipse/openvsx/admin/NameSquattingAdminServiceTest.java Adds unit tests for moderation grouping/counting/actions.
server/src/main/resources/db/migration/V1_72__ExtensionVersion_DisplayName_Index.sql Adds index backing the display-name conflict check.
server/src/main/java/org/eclipse/openvsx/repositories/RepositoryService.java Exposes display-name conflict lookup + flagged-extension moderation queries.
server/src/main/java/org/eclipse/openvsx/repositories/ExtensionValidationFailureRepository.java Adds moderation queries: list/count flagged extensions + delete failures by extension.
server/src/main/java/org/eclipse/openvsx/repositories/ExtensionJooqRepository.java Implements findActiveExtensionByDisplayName using latest active version semantics.
server/src/main/java/org/eclipse/openvsx/publish/PublishExtensionVersionHandler.java Adds “adopts display name” logic + conflict checks (skipped for mirrors).
server/src/main/java/org/eclipse/openvsx/json/NameSquattingTargetJson.java Adds API JSON model for moderation targets.
server/src/main/java/org/eclipse/openvsx/json/NameSquattingFlagListJson.java Adds API JSON model for paginated flags response.
server/src/main/java/org/eclipse/openvsx/json/NameSquattingFlagJson.java Adds API JSON model for a flagged extension (grouped findings).
server/src/main/java/org/eclipse/openvsx/json/NameSquattingFindingJson.java Adds API JSON model for individual findings.
server/src/main/java/org/eclipse/openvsx/json/NameSquattingCountsJson.java Adds API JSON model for per-state totals.
server/src/main/java/org/eclipse/openvsx/json/NameSquattingActionResultJson.java Adds API JSON model for per-target action outcome.
server/src/main/java/org/eclipse/openvsx/json/NameSquattingActionResponseJson.java Adds API JSON model for bulk action response.
server/src/main/java/org/eclipse/openvsx/json/NameSquattingActionRequest.java Adds API JSON model for moderation requests.
server/src/main/java/org/eclipse/openvsx/admin/NameSquattingAPI.java Adds admin endpoints for list/count/clear/delete name-squatting findings.
server/src/main/java/org/eclipse/openvsx/admin/AdminService.java Implements moderation listing/counting/actions + state filter parsing.
Suppressed comments (1)

webui/src/pages/admin-dashboard/name-squatting/name-squatting-action-dialog.tsx:88

  • Same as above: color='warning.main' is likely treated as an invalid CSS color string. Prefer sx={{ color: 'warning.main' }} to reliably use the theme palette warning color.
                        <Typography variant='body2' color='warning.main'>

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

{flag?.findingCount === 1 ? 'finding' : 'findings'} recorded for{' '}
<strong>{extensionId}</strong>?
</Typography>
<Typography variant='body2' color='warning.main'>
Comment on lines +126 to +131
<Tooltip
title={
flag.activeVersionCount === 0
? 'The extension has no active versions left to deactivate'
: ''
}>
To best fulfill the requirements of an easy to use system to manage typosquatting, this PR was created. This leverages the existing NAME_SQUATTING publish check that uses levenshtein distance to check for similarly named extensions post-publish. The new dashboard view allows an admin to either mark a found check as a false positive and clear the check, or to soft-delete any extensions that are found to be maliciously typo squatting another extension. Any admin action taken will also be logged in the admin log table to make auditing these changes possible.

This check is left as unenforced as the check is too sensitive, and has many false positives. This does provide all of the features that were requested in the initial PRD for management of the system, outside of exclusion keywords.

Part of eclipse-openvsx#1949

Assisted-by: Claude Opus 5
…isplay name matches

To best accomadate the current infrastructure, a new check was added to the publish workflow. This check will compare the display name of new extensions and new versions to existing data. For new extensions, exact matches will be blocked from upload.

In the case of versions, extensions whose name changes to conflict will be blocked, but existing names will not be blocked. This carve out was added to address any existing conflict of display name without potentially blocking a legitimate version from being submitted.

This grandfathered clause makes the assumption that the name squatting results will be monitored and addressed to get to a known good state. Once we get to a clean state, the grandfathered clause will effectively work as a standard exact match gate without any notable performance impact.

Assisted-by: Claude Opus 5
@autumnfound
autumnfound force-pushed the malowe/main/ovsxorg-1949 branch from d92db4e to 1f1cbe2 Compare August 26, 2026 17:08
@autumnfound

Copy link
Copy Markdown
Contributor Author

Failure to publish due to exact name duplicate:
Screenshot_2026-08-26_13-33-40

Example name-squatting result:

Screenshot_2026-08-26_15-57-50

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.

Update handling of similarly named extensions

2 participants