Skip to content

Commit cc00df2

Browse files
feat(core)!: retire PluginSecurityScanner — plugin security scanning is not a platform capability (#15930)
* feat(core)!: retire PluginSecurityScanner across all four surfaces (#14919) ADR-0049 enforce-or-remove; maintainer ruling 2026-09-05 (director summon #14, decision batch #42). The class was a shell that reported success: four of its five private scanners returned an empty issue list unconditionally, and the fifth matched against an in-memory vulnerability database whose only writer had zero callers -- so every scan() ever performed answered status: 'passed' with a perfect score, for a malicious plugin as readily as a benign one. A security control that cannot fail is worse than none, because callers rely on it. - delete packages/core/examples/phase2-integration.ts (the only constructor) - delete src/security/security-scanner.ts; drop its export block from src/security/index.ts, leaving a tombstone naming the retirement - rewrite PHASE2_IMPLEMENTATION.md section 6 to state plainly that plugin security scanning is NOT a platform capability, and drop the two capability claims elsewhere in the same document that outlived their subject - delete the FOLLOW-UPS.md row, repair the paragraph that existed only to compound it, and correct the neighbouring row whose evidence the deletion falsified - pin the retirement as an export-list assertion on both barrels Repair was refused by name: a real vulnerability scanner is a feature with a design surface, not a defect fix. There is no replacement export. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ * feat(spec): register the PluginSecurityScanner retirement in the ADR-0087 ledger (#14919) check-adr-0087-registration refused the previous disposition, correctly: the changeset carries a real consumer prescription (delete the import and every call), so `not-required (no-migration-prescription)` was a self-contradiction. Every other not-required category is false too -- @objectstack/core publishes, so `unpublished` is out; `already-registered` has no entry to name; `type-surface-only` needs an any/unknown-to-concrete narrowing this is not; and `runtime-interface-only` explicitly inherits the same prescription refusal (#8299). The only truthful disposition left is `registered`. That is also the repo's settled convention for this exact shape -- a published TS symbol with no spec schema, no stored source and no tombstone, where the ledger is the only channel that reaches an upgrader. contracts.IDataDriver.findStream and actor-user-roles-to-positions are both registered on those grounds. D3 semantic, not a D2 conversion: the class has no spec schema, so there is no authorable key to tombstone and no stored sys_metadata row to rewrite -- a scanner was constructed per call and every result lived in a per-instance Map discarded with the object, so applyConversionsToStoredItem has no seam that would ever see one. This is what the ruling's "no metadata migration" excludes, and it is excluded. - add entries/semantic/18.plugin-security-scanner-retired.ts (one file, per the entries README kit -- no hand edit inside registry.ts's generated markers) - regenerate registry.ts via gen:migration-registry (157 semantic entries) - flip the core changeset's marker to `registered plugin-security-scanner-retired`, keeping the BREAKING banner, the no-replacement statement and the NOT MEASURED paragraph untouched - add the @objectstack/spec patch changeset, mirroring the #6138 backfill Measured and recorded in that changeset: the regeneration lap the entries README warns about did not materialise. check:generated reports all 15 artifacts up to date, and running gen:spec-changes and gen:upgrade-guide explicitly moved neither file -- a major-18 semantic entry is not yet projected into either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ * fix(changeset): the `registered` marker takes ids only — move its rationale into the body check-adr-0087-registration parses everything after `registered` as a comma/space-separated id list, so the trailing `why` prose that the `not-required (...)` forms accept was read as 131 nonexistent migration ids. The asymmetry is real and AGENTS.md spells it: `registered SOME-MIGRATION-ID` carries no `why`, the three `not-required` forms do. The rationale is unchanged, only relocated into the changeset body where a reader gets it anyway. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3f89967 commit cc00df2

10 files changed

Lines changed: 395 additions & 839 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
ADR-0087 semantic-migration ledger: register the retirement of `@objectstack/core`'s `PluginSecurityScanner` (#14919)
6+
7+
`PluginSecurityScanner`, `ScanTarget` and `SecurityIssue` are removed from
8+
`@objectstack/core` in the same PR, under ADR-0049 enforce-or-remove (maintainer
9+
ruling 2026-09-05, director summon #14, decision batch #42). This is the ledger
10+
half: a D3 semantic entry
11+
(`src/migrations/entries/semantic/18.plugin-security-scanner-retired.ts`,
12+
concatenated into `MIGRATIONS_BY_MAJOR[18].semantic` by `gen:migration-registry`)
13+
so the retirement reaches `spec-changes.json` and the generated upgrade guide
14+
rather than being invisible to every upgrade channel.
15+
16+
FROM `new PluginSecurityScanner(kernel.logger)` → TO nothing: delete the import
17+
and every call. There is no replacement export, and a caller that branched on
18+
`result.status === 'passed'` takes that branch unconditionally — it is the only
19+
branch the scanner ever produced, because four of its five scan methods returned
20+
an empty issue list on every input and the fifth read a vulnerability database
21+
whose only writer had zero callers.
22+
23+
Why an entry is owed at all, and why D3 rather than a D2 conversion: the class
24+
has no spec schema and never had one. It is a runtime TS class, so there is no
25+
authorable key to tombstone with `retiredKey()` and no stored `sys_metadata` row
26+
a conversion could rewrite — a scanner was constructed per call and every result
27+
lived in a per-instance Map discarded with the object, so
28+
`applyConversionsToStoredItem` has no seam that would ever see one. The enforced
29+
channel is tsc at the consumer's own import site; for anyone it does not reach,
30+
this entry and the upgrade guide are the only channel. That is the
31+
`contracts.IDataDriver.findStream` and `actor-user-roles-to-positions`
32+
disposition, applied to a surface one layer further out than either — those are
33+
declared in `packages/spec`, this one only in `packages/core`.
34+
35+
Measured, and worth recording because the entries README warns of a regeneration
36+
lap that did not materialise here: `check:generated` reports all 15 artifacts up
37+
to date after the entry landed, and running `gen:spec-changes` and
38+
`gen:upgrade-guide` explicitly moved neither file — a major-18 semantic entry is
39+
not yet projected into either. `registry.ts` is the whole generated diff.
40+
41+
No behaviour in `@objectstack/spec` changes; this adds a ledger row and the
42+
regenerated region that carries it.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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`.

docs/qa/platform-checklist/FOLLOW-UPS.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -183,19 +183,23 @@ governance hole.
183183

184184
| surface | evidence | the deadness, precisely |
185185
|---|---|---|
186-
| `PluginSecurityScanner` (`packages/core/src/security/security-scanner.ts`) | zero constructors outside `packages/core/examples/`; not in plugin-loader, service-package, rest, or any CLI path | Exported dead code on the PUBLIC barrel (`packages/core/src/index.ts` re-exports `./security/index.js`). 3 of 5 scan methods are empty stubs; `scanDependencies` has a real loop whose only data source (`addVulnerability`, ``) has zero callers; `updateVulnerabilityDatabase` (``) is a log-only no-op. |
187-
| `KernelSecurityScanResult` / `KernelSecurityVulnerability` / `PluginSecurityManifest.scanResults` (`packages/spec/src/kernel/plugin-security-advanced.zod.ts,476,625`) | no `.parse`/`.safeParse` site anywhere; only consumer is the dead scanner (type-only import) | 22 rows published to `packages/spec/authorable-surface/kernel.json` with zero authors and zero parsers. The whole `plugin-security-advanced` module has no runtime consumer. |
186+
| `KernelSecurityScanResult` / `KernelSecurityVulnerability` / `PluginSecurityManifest.scanResults` (`packages/spec/src/kernel/plugin-security-advanced.zod.ts,476,625`) | no `.parse`/`.safeParse` site anywhere; **zero** consumers of any kind since #14919 retired the dead scanner that was the last type-only importer | 22 rows published to `packages/spec/authorable-surface/kernel.json` with zero authors and zero parsers. The whole `plugin-security-advanced` module has no runtime consumer. |
188187
| `PluginQualityMetrics.securityScan` (`packages/spec/src/kernel/plugin-registry.zod.ts`) | spec self-test only | Nothing reads or writes it at runtime. |
189188
| Marketplace/incident scan vocab (`marketplace.zod.ts` 'scanning' status, `marketplace-admin.zod.ts,193`, `incident-response.zod.ts` 'malware') | declared-only enum members, no producer in this repo | Cloud/EE surface. Same shape as the `'failed'`/`'expired'` upload statuses #7667 had to close: declared, published, no writer. |
190189
| MetadataPlugin FS scan + `metadata-fs` boot scan (`packages/metadata/src/plugin.ts,270``watch ?? false`; `packages/runtime/src/standalone-stack.ts` hard-off; `metadata-fs` unwired from any `os dev`/`os serve` lane) | unit-pinned in-package only | No reachable fixture from any shipped boot; if a future lane wires `metadata-fs`, the boot-scan/watcher dot-entry divergence is the risk to test first. |
191190

192-
Compounding the first row: `packages/core/PHASE2_IMPLEMENTATION.md` advertises
193-
the scanner as a working feature, tells readers to import from `@objectstack/core/security`
194-
(a subpath `packages/core/package.json` does not export), and its sample fields
195-
(`scanResult.passed`/`.score`/`.summary.critical`) do not exist on the actual schema —
196-
the example (`examples/phase2-integration.ts`) sits outside every tsconfig and is never
197-
typechecked. Enforce or remove; if removed, the spec-property-retirement playbook applies
198-
to the authorable-surface rows.
191+
The scanner row above was **CLOSED by removal** in #14919 (maintainer ruling,
192+
director summon #14, decision batch #42): the class, its barrel export, its
193+
`packages/core/examples/` demonstration and the `PHASE2_IMPLEMENTATION.md` section that
194+
advertised it are gone, and that section now states plainly that plugin security scanning
195+
is not a platform capability. Repair was refused by name. Do not re-derive it.
196+
197+
**What SURVIVES that removal, in the same document.** `PHASE2_IMPLEMENTATION.md` sections
198+
4 and 5 still tell readers to `import … from '@objectstack/core/security'` — a subpath
199+
`packages/core/package.json` declares in no `exports` entry, so it resolves for no
200+
consumer of the published package. Deliberately left: the two repairs (declare the
201+
subpath, or repoint both sections at the root barrel) differ in whether they widen the
202+
published contract, which is not a lane's call. Filed separately.
199203

200204
### 7b. Docs drift (PD#10 class — file as docs fixes, not checklist items)
201205

packages/core/PHASE2_IMPLEMENTATION.md

Lines changed: 38 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -263,52 +263,41 @@ const { withinLimits, violations } = sandbox.checkResourceLimits('my-plugin');
263263
const usage = sandbox.getResourceUsage('my-plugin');
264264
```
265265

266-
### 6. Security Scanner (`security/security-scanner.ts`)
267-
268-
The Security Scanner performs comprehensive security analysis of plugins.
269-
270-
**Features:**
271-
- Code vulnerability scanning
272-
- Dependency vulnerability detection (CVE database integration)
273-
- Malware pattern detection
274-
- License compliance checking
275-
- Configuration security analysis
276-
- Security scoring (0-100)
277-
- Issue categorization (critical, high, medium, low, info)
278-
279-
**Usage:**
280-
281-
```typescript
282-
import { PluginSecurityScanner } from '@objectstack/core/security';
283-
284-
const scanner = new PluginSecurityScanner(logger);
285-
286-
// Perform security scan
287-
const result = await scanner.scan({
288-
pluginId: 'my-plugin',
289-
version: '1.0.0',
290-
files: ['src/**/*.ts'],
291-
dependencies: {
292-
'express': '4.18.0',
293-
'lodash': '4.17.21',
294-
},
295-
});
296-
297-
console.log(`Security Score: ${result.score}/100`);
298-
console.log(`Passed: ${result.passed}`);
299-
console.log(`Issues:`, result.summary);
300-
301-
// Add vulnerability to database
302-
scanner.addVulnerability('lodash', '4.17.20', {
303-
cve: 'CVE-2021-23337',
304-
severity: 'high',
305-
affectedVersions: ['<=4.17.20'],
306-
fixedIn: ['4.17.21'],
307-
});
308-
309-
// Update vulnerability database
310-
await scanner.updateVulnerabilityDatabase();
311-
```
266+
### 6. Plugin security scanning — NOT a platform capability
267+
268+
**ObjectStack does not scan plugins for vulnerabilities, malware or license
269+
compliance, and it never has.** There is no scanner to import, no security
270+
score, and no CVE database. Nothing in the runtime, the CLI, the plugin loader
271+
or the REST layer inspects a plugin's code, its dependencies or its
272+
configuration for security issues.
273+
274+
This section used to document a `PluginSecurityScanner` class exported from
275+
`@objectstack/core`. That class was retired in #14919 under ADR-0049
276+
enforce-or-remove, because it was a shell that reported success rather than a
277+
scanner that found anything: four of its five scan methods returned an empty
278+
issue list unconditionally, and the fifth matched dependencies against an
279+
in-memory vulnerability database whose only writer had zero callers anywhere.
280+
Every `scan()` it was ever asked to perform therefore answered
281+
`status: 'passed'` with a perfect score — for a benign plugin and a malicious
282+
one alike. Advertising it here was the failure Prime Directive #10 names:
283+
advertising a capability the runtime does not deliver. It has no replacement,
284+
and none is planned; building a real scanner is a feature with a design surface
285+
of its own, not a repair.
286+
287+
**What the platform does enforce**, and what to use instead of a scan:
288+
289+
- **Artifact integrity and signatures**`verifyPluginArtifactIntegrity` and
290+
the signature verifier (`security/plugin-artifact-integrity.ts`,
291+
`security/plugin-artifact-signature.ts`) answer *"is this the artifact the
292+
publisher signed?"*. They do not answer *"is this artifact safe?"*.
293+
- **Permissions** — section 4 above. A plugin gets what it is explicitly
294+
granted.
295+
- **Sandboxing** — section 5 above. Resource and access limits at run time.
296+
297+
For dependency vulnerabilities, use the tools built for it against your own
298+
project — `npm audit`, `pnpm audit`, GitHub's Dependabot / Advisory Database,
299+
or OSV. Treat an unaudited third-party plugin as untrusted code, because
300+
nothing here has audited it for you.
312301

313302
## Integration with Kernel
314303

@@ -321,8 +310,7 @@ import {
321310
HotReloadManager,
322311
DependencyResolver,
323312
PluginPermissionManager,
324-
PluginSandboxRuntime,
325-
PluginSecurityScanner
313+
PluginSandboxRuntime
326314
} from '@objectstack/core';
327315

328316
const kernel = new ObjectKernel({ logger: { level: 'info' } });
@@ -333,7 +321,6 @@ const hotReload = new HotReloadManager(kernel.logger);
333321
const depResolver = new DependencyResolver(kernel.logger);
334322
const permManager = new PluginPermissionManager(kernel.logger);
335323
const sandbox = new PluginSandboxRuntime(kernel.logger);
336-
const scanner = new PluginSecurityScanner(kernel.logger);
337324

338325
// Register plugins with enhanced features
339326
// ... plugin registration code ...
@@ -362,13 +349,13 @@ npm test
362349
- State preservation uses checksums for integrity verification
363350
- Dependency resolution uses efficient topological sorting
364351
- Resource monitoring is throttled (default 5 seconds)
365-
- Security scanning can be run asynchronously
366352

367353
## Security
368354

369355
- All permissions must be explicitly granted
370356
- Sandbox provides multiple isolation levels
371-
- Security scanner integrates with CVE databases
357+
- **No plugin security scanning.** The platform performs no vulnerability,
358+
malware or license analysis of a plugin — see section 6
372359
- Resource limits prevent DoS attacks
373360
- State preservation uses checksums to detect tampering
374361

0 commit comments

Comments
 (0)