Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .changeset/plugin-security-scanner-ledger-entry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
"@objectstack/spec": patch
---

ADR-0087 semantic-migration ledger: register the retirement of `@objectstack/core`'s `PluginSecurityScanner` (#14919)

`PluginSecurityScanner`, `ScanTarget` and `SecurityIssue` are removed from
`@objectstack/core` in the same PR, under ADR-0049 enforce-or-remove (maintainer
ruling 2026-09-05, director summon #14, decision batch #42). This is the ledger
half: a D3 semantic entry
(`src/migrations/entries/semantic/18.plugin-security-scanner-retired.ts`,
concatenated into `MIGRATIONS_BY_MAJOR[18].semantic` by `gen:migration-registry`)
so the retirement reaches `spec-changes.json` and the generated upgrade guide
rather than being invisible to every upgrade channel.

FROM `new PluginSecurityScanner(kernel.logger)` → TO nothing: delete the import
and every call. There is no replacement export, and a caller that branched on
`result.status === 'passed'` takes that branch unconditionally — it is the only
branch the scanner ever produced, because four of its five scan methods returned
an empty issue list on every input and the fifth read a vulnerability database
whose only writer had zero callers.

Why an entry is owed at all, and why D3 rather than a D2 conversion: the class
has no spec schema and never had one. It is a runtime TS class, so there is no
authorable key to tombstone with `retiredKey()` and no stored `sys_metadata` row
a conversion could 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. The enforced
channel is tsc at the consumer's own import site; for anyone it does not reach,
this entry and the upgrade guide are the only channel. That is the
`contracts.IDataDriver.findStream` and `actor-user-roles-to-positions`
disposition, applied to a surface one layer further out than either — those are
declared in `packages/spec`, this one only in `packages/core`.

Measured, and worth recording because the entries README warns of a regeneration
lap that did not materialise here: `check:generated` reports all 15 artifacts up
to date after the entry landed, and running `gen:spec-changes` and
`gen:upgrade-guide` explicitly moved neither file — a major-18 semantic entry is
not yet projected into either. `registry.ts` is the whole generated diff.

No behaviour in `@objectstack/spec` changes; this adds a ledger row and the
regenerated region that carries it.
83 changes: 83 additions & 0 deletions .changeset/plugin-security-scanner-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
"@objectstack/core": minor
---

feat(core)!: retire `PluginSecurityScanner` — plugin security scanning is not a platform capability (#14919)

<!-- adr-0087: registered plugin-security-scanner-retired -->

**ADR-0087 disposition: registered**, as `plugin-security-scanner-retired` in
`MIGRATIONS_BY_MAJOR[18].semantic` — a **D3 semantic** entry, not a D2 conversion,
and so not the metadata migration the ruling excludes. The class has no spec schema
and never had one, so there is no authorable key to tombstone with `retiredKey()`
and no stored `sys_metadata` row a conversion could 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. An
entry is nevertheless owed rather than optional, because this changeset carries a
real consumer prescription — the enforced channel is tsc at the import site, and for
any consumer it does not reach, the ledger and the generated upgrade guide are the
only channel there is. Same disposition as `contracts.IDataDriver.findStream` and
`actor-user-roles-to-positions`.

**BREAKING** — `PluginSecurityScanner` is removed from `@objectstack/core`,
together with its two companion types `ScanTarget` and `SecurityIssue`. Landing
as `minor` under the repo's launch-window convention for breaking changes.
**There is no replacement**, and none is planned.

⚠️ **The out-of-repo consumer population for these three exports is NOT
MEASURED.** This changeset can state only what was measured *inside* the
sources this repo can read: zero constructors in objectstack, zero in objectui
at the pinned sha, and zero in the deleted example itself. How many published
consumers of `@objectstack/core` import the class is unknown — no download,
dependent or source telemetry was consulted. Read the removal as breaking for
an unmeasured population, not as a removal proven to break nobody.

## Why it was removed rather than repaired

The class was a shell that reported success. `scan()` composed five private
scanners: four of them (`scanCode`, `scanMalware`, `scanLicenses`,
`scanConfiguration`) allocated an empty issue array, logged, and returned it
with no code in between — none could report a finding for any input. The fifth,
`scanDependencies`, ran a real loop but matched only against an in-memory
vulnerability database whose sole writer, the public `addVulnerability`, had
zero callers; `updateVulnerabilityDatabase()` logged twice and fetched nothing.
The database was therefore empty on every code path that has ever executed, so
no issue was ever produced, the score stayed 100, and the result was
`status: 'passed'` for every plugin the scanner was ever handed — a malicious
one included.

A security control that cannot fail is worse than no security control, because
callers rely on it. Repair — writing a real vulnerability scanner — was refused
by name: it is a feature with a design surface and no demand, not a defect fix.

## FROM → TO

```ts
// FROM — compiles today, and passes every plugin it is given
import { PluginSecurityScanner } from '@objectstack/core';

const scanner = new PluginSecurityScanner(kernel.logger);
const result = await scanner.scan({ pluginId, version, dependencies });
if (result.status === 'passed') { await kernel.use(plugin); }

// TO — delete it. The condition above was always true.
await kernel.use(plugin);
```

**The one-line fix:** delete the import and every call; no symbol replaces it.
If your code branched on `result.status`, take the `'passed'` branch — that is
the only branch it ever took.

**If you were relying on it for actual security**, you were not getting any.
Audit dependencies with the tools built for it (`npm audit` / `pnpm audit`,
Dependabot, the GitHub Advisory Database, OSV) and treat an unaudited
third-party plugin as untrusted code. What ObjectStack does still enforce is
artifact **integrity and signatures** (`verifyPluginArtifactIntegrity`, the
plugin signature verifier — "is this what the publisher signed?", never "is
this safe?"), explicit plugin **permissions**, and the sandbox **resource
limits**; all three are unchanged.

Removed under ADR-0049 enforce-or-remove, per the maintainer ruling of
2026-09-05 (director summon #14, decision batch #42). The retirement is pinned
as an export-list assertion on both barrels in
`packages/core/src/security/security-scanner-retirement.pin.test.ts`.
22 changes: 13 additions & 9 deletions docs/qa/platform-checklist/FOLLOW-UPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,19 +183,23 @@ governance hole.

| surface | evidence | the deadness, precisely |
|---|---|---|
| `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. |
| `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. |
| `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. |
| `PluginQualityMetrics.securityScan` (`packages/spec/src/kernel/plugin-registry.zod.ts`) | spec self-test only | Nothing reads or writes it at runtime. |
| 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. |
| 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. |

Compounding the first row: `packages/core/PHASE2_IMPLEMENTATION.md` advertises
the scanner as a working feature, tells readers to import from `@objectstack/core/security`
(a subpath `packages/core/package.json` does not export), and its sample fields
(`scanResult.passed`/`.score`/`.summary.critical`) do not exist on the actual schema —
the example (`examples/phase2-integration.ts`) sits outside every tsconfig and is never
typechecked. Enforce or remove; if removed, the spec-property-retirement playbook applies
to the authorable-surface rows.
The scanner row above was **CLOSED by removal** in #14919 (maintainer ruling,
director summon #14, decision batch #42): the class, its barrel export, its
`packages/core/examples/` demonstration and the `PHASE2_IMPLEMENTATION.md` section that
advertised it are gone, and that section now states plainly that plugin security scanning
is not a platform capability. Repair was refused by name. Do not re-derive it.

**What SURVIVES that removal, in the same document.** `PHASE2_IMPLEMENTATION.md` sections
4 and 5 still tell readers to `import … from '@objectstack/core/security'` — a subpath
`packages/core/package.json` declares in no `exports` entry, so it resolves for no
consumer of the published package. Deliberately left: the two repairs (declare the
subpath, or repoint both sections at the root barrel) differ in whether they widen the
published contract, which is not a lane's call. Filed separately.

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

Expand Down
89 changes: 38 additions & 51 deletions packages/core/PHASE2_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,52 +263,41 @@ const { withinLimits, violations } = sandbox.checkResourceLimits('my-plugin');
const usage = sandbox.getResourceUsage('my-plugin');
```

### 6. Security Scanner (`security/security-scanner.ts`)

The Security Scanner performs comprehensive security analysis of plugins.

**Features:**
- Code vulnerability scanning
- Dependency vulnerability detection (CVE database integration)
- Malware pattern detection
- License compliance checking
- Configuration security analysis
- Security scoring (0-100)
- Issue categorization (critical, high, medium, low, info)

**Usage:**

```typescript
import { PluginSecurityScanner } from '@objectstack/core/security';

const scanner = new PluginSecurityScanner(logger);

// Perform security scan
const result = await scanner.scan({
pluginId: 'my-plugin',
version: '1.0.0',
files: ['src/**/*.ts'],
dependencies: {
'express': '4.18.0',
'lodash': '4.17.21',
},
});

console.log(`Security Score: ${result.score}/100`);
console.log(`Passed: ${result.passed}`);
console.log(`Issues:`, result.summary);

// Add vulnerability to database
scanner.addVulnerability('lodash', '4.17.20', {
cve: 'CVE-2021-23337',
severity: 'high',
affectedVersions: ['<=4.17.20'],
fixedIn: ['4.17.21'],
});

// Update vulnerability database
await scanner.updateVulnerabilityDatabase();
```
### 6. Plugin security scanning — NOT a platform capability

**ObjectStack does not scan plugins for vulnerabilities, malware or license
compliance, and it never has.** There is no scanner to import, no security
score, and no CVE database. Nothing in the runtime, the CLI, the plugin loader
or the REST layer inspects a plugin's code, its dependencies or its
configuration for security issues.

This section used to document a `PluginSecurityScanner` class exported from
`@objectstack/core`. That class was retired in #14919 under ADR-0049
enforce-or-remove, because it was a shell that reported success rather than a
scanner that found anything: four of its five scan methods returned an empty
issue list unconditionally, and the fifth matched dependencies against an
in-memory vulnerability database whose only writer had zero callers anywhere.
Every `scan()` it was ever asked to perform therefore answered
`status: 'passed'` with a perfect score — for a benign plugin and a malicious
one alike. Advertising it here was the failure Prime Directive #10 names:
advertising a capability the runtime does not deliver. It has no replacement,
and none is planned; building a real scanner is a feature with a design surface
of its own, not a repair.

**What the platform does enforce**, and what to use instead of a scan:

- **Artifact integrity and signatures** — `verifyPluginArtifactIntegrity` and
the signature verifier (`security/plugin-artifact-integrity.ts`,
`security/plugin-artifact-signature.ts`) answer *"is this the artifact the
publisher signed?"*. They do not answer *"is this artifact safe?"*.
- **Permissions** — section 4 above. A plugin gets what it is explicitly
granted.
- **Sandboxing** — section 5 above. Resource and access limits at run time.

For dependency vulnerabilities, use the tools built for it against your own
project — `npm audit`, `pnpm audit`, GitHub's Dependabot / Advisory Database,
or OSV. Treat an unaudited third-party plugin as untrusted code, because
nothing here has audited it for you.

## Integration with Kernel

Expand All @@ -321,8 +310,7 @@ import {
HotReloadManager,
DependencyResolver,
PluginPermissionManager,
PluginSandboxRuntime,
PluginSecurityScanner
PluginSandboxRuntime
} from '@objectstack/core';

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

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

## Security

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

Expand Down
Loading
Loading