|
| 1 | +--- |
| 2 | +"@objectstack/core": minor |
| 3 | +--- |
| 4 | + |
| 5 | +feat(core)!: retire `PluginSecurityScanner` — plugin security scanning is not a platform capability (#14919) |
| 6 | + |
| 7 | +<!-- adr-0087: registered plugin-security-scanner-retired --> |
| 8 | + |
| 9 | +**ADR-0087 disposition: registered**, as `plugin-security-scanner-retired` in |
| 10 | +`MIGRATIONS_BY_MAJOR[18].semantic` — a **D3 semantic** entry, not a D2 conversion, |
| 11 | +and so not the metadata migration the ruling excludes. The class has no spec schema |
| 12 | +and never had one, so there is no authorable key to tombstone with `retiredKey()` |
| 13 | +and no stored `sys_metadata` row a conversion could rewrite: a scanner was |
| 14 | +constructed per call and every result lived in a per-instance Map discarded with the |
| 15 | +object, so `applyConversionsToStoredItem` has no seam that would ever see one. An |
| 16 | +entry is nevertheless owed rather than optional, because this changeset carries a |
| 17 | +real consumer prescription — the enforced channel is tsc at the import site, and for |
| 18 | +any consumer it does not reach, the ledger and the generated upgrade guide are the |
| 19 | +only channel there is. Same disposition as `contracts.IDataDriver.findStream` and |
| 20 | +`actor-user-roles-to-positions`. |
| 21 | + |
| 22 | +**BREAKING** — `PluginSecurityScanner` is removed from `@objectstack/core`, |
| 23 | +together with its two companion types `ScanTarget` and `SecurityIssue`. Landing |
| 24 | +as `minor` under the repo's launch-window convention for breaking changes. |
| 25 | +**There is no replacement**, and none is planned. |
| 26 | + |
| 27 | +⚠️ **The out-of-repo consumer population for these three exports is NOT |
| 28 | +MEASURED.** This changeset can state only what was measured *inside* the |
| 29 | +sources this repo can read: zero constructors in objectstack, zero in objectui |
| 30 | +at the pinned sha, and zero in the deleted example itself. How many published |
| 31 | +consumers of `@objectstack/core` import the class is unknown — no download, |
| 32 | +dependent or source telemetry was consulted. Read the removal as breaking for |
| 33 | +an unmeasured population, not as a removal proven to break nobody. |
| 34 | + |
| 35 | +## Why it was removed rather than repaired |
| 36 | + |
| 37 | +The class was a shell that reported success. `scan()` composed five private |
| 38 | +scanners: four of them (`scanCode`, `scanMalware`, `scanLicenses`, |
| 39 | +`scanConfiguration`) allocated an empty issue array, logged, and returned it |
| 40 | +with no code in between — none could report a finding for any input. The fifth, |
| 41 | +`scanDependencies`, ran a real loop but matched only against an in-memory |
| 42 | +vulnerability database whose sole writer, the public `addVulnerability`, had |
| 43 | +zero callers; `updateVulnerabilityDatabase()` logged twice and fetched nothing. |
| 44 | +The database was therefore empty on every code path that has ever executed, so |
| 45 | +no issue was ever produced, the score stayed 100, and the result was |
| 46 | +`status: 'passed'` for every plugin the scanner was ever handed — a malicious |
| 47 | +one included. |
| 48 | + |
| 49 | +A security control that cannot fail is worse than no security control, because |
| 50 | +callers rely on it. Repair — writing a real vulnerability scanner — was refused |
| 51 | +by name: it is a feature with a design surface and no demand, not a defect fix. |
| 52 | + |
| 53 | +## FROM → TO |
| 54 | + |
| 55 | +```ts |
| 56 | +// FROM — compiles today, and passes every plugin it is given |
| 57 | +import { PluginSecurityScanner } from '@objectstack/core'; |
| 58 | + |
| 59 | +const scanner = new PluginSecurityScanner(kernel.logger); |
| 60 | +const result = await scanner.scan({ pluginId, version, dependencies }); |
| 61 | +if (result.status === 'passed') { await kernel.use(plugin); } |
| 62 | + |
| 63 | +// TO — delete it. The condition above was always true. |
| 64 | +await kernel.use(plugin); |
| 65 | +``` |
| 66 | + |
| 67 | +**The one-line fix:** delete the import and every call; no symbol replaces it. |
| 68 | +If your code branched on `result.status`, take the `'passed'` branch — that is |
| 69 | +the only branch it ever took. |
| 70 | + |
| 71 | +**If you were relying on it for actual security**, you were not getting any. |
| 72 | +Audit dependencies with the tools built for it (`npm audit` / `pnpm audit`, |
| 73 | +Dependabot, the GitHub Advisory Database, OSV) and treat an unaudited |
| 74 | +third-party plugin as untrusted code. What ObjectStack does still enforce is |
| 75 | +artifact **integrity and signatures** (`verifyPluginArtifactIntegrity`, the |
| 76 | +plugin signature verifier — "is this what the publisher signed?", never "is |
| 77 | +this safe?"), explicit plugin **permissions**, and the sandbox **resource |
| 78 | +limits**; all three are unchanged. |
| 79 | + |
| 80 | +Removed under ADR-0049 enforce-or-remove, per the maintainer ruling of |
| 81 | +2026-09-05 (director summon #14, decision batch #42). The retirement is pinned |
| 82 | +as an export-list assertion on both barrels in |
| 83 | +`packages/core/src/security/security-scanner-retirement.pin.test.ts`. |
0 commit comments