Scanner Language Analyzer Review - 2026-05-06

Summary

Status: completed review with follow-up implementation now tracked in docs-archive/implplan/SPRINT_20260506_004_Scanner_first_class_detector_corrections.md and docs/implplan/SPRINT_20260506_005_Scanner_language_detector_uplift_program.md.

Scope covered the shared Scanner language analyzer core plus Bun, Deno, DotNet, Go, Java, Node, PHP, Python, Ruby, and Rust.

Evidence summary:

Top risks recorded by the initial review:

Implementation Update - 2026-05-06

Implemented in docs-archive/implplan/SPRINT_20260506_004_Scanner_first_class_detector_corrections.md:

Verification evidence from the implementation pass:

Continuation implemented after the initial pass:

Additional verification evidence from the continuation:

Final uplift implemented in docs-archive/implplan/SPRINT_20260506_005_Scanner_language_detector_uplift_program.md:

Final uplift verification evidence:

Remaining work:

Enhanced first-class detector audit:

First-Class Detector Standard

Treat an analyzer as first class only when it satisfies all of these gates:

  1. Restart/deployment gate.

    • Either it is a restart-loadable analyzer plugin with a manifest, copied dependency closure, and smoke profile, or it is explicitly documented as a built-in Worker stage with equivalent startup evidence.
    • Test projects must never be copied as plugins.
  2. Inventory gate.

    • It emits deterministic SBOM components only from authoritative on-disk evidence, with PURL vs explicit-key decisions matching language-analyzers-contract.md.
    • It records evidence locators, hashes when bounded, package scope, provenance, license, vendoring/bundling, and usedByEntrypoint where available.
  3. Detector gate.

    • It emits language-specific detections, not only package records: entrypoints, routes, framework identity, runtime surface, dynamic code, native/FFI, network, filesystem, process execution, database, queue, crypto/TLS, deserialization, template rendering, and secret/config access.
    • Detections must be typed and queryable, not hidden only as ad hoc metadata strings.
  4. Reachability gate.

    • It joins inventory to entrypoints, dependency/call/import graphs, runtime evidence, and confidence.
    • It can distinguish “present” from “used by runtime path” and “declared only” from “observed”.
  5. Secrets gate.

    • The cross-cutting secrets detector scans raw files and emits secret findings.
    • Language analyzers should add context around secret use without duplicating raw-value detection: environment-variable names, config keys, dotenv loaders, framework secret stores, credentials file conventions, and secret sinks/sources.
    • No analyzer may emit raw secret values in component metadata, logs, observations, SARIF, or policy payloads.
  6. Policy/SARIF gate.

    • Detector output must map to stable rule ids, severity, confidence, evidence locator, deterministic id/fingerprint, SARIF export, and policy predicates.
    • Findings need suppressions/exception handling and baseline/diff semantics where false positives are expected.
  7. Test gate.

    • Each analyzer needs targeted .csproj tests, fixture/golden outputs, negative cases, plugin smoke, packaging checks, and at least one fixture that proves secret/config context is masked and deterministic.

Secrets Detection Audit

Current implementation:

Secrets test evidence:

dotnet test "src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Secrets.Tests/StellaOps.Scanner.Analyzers.Secrets.Tests.csproj" -v minimal /m:1 /p:UseSharedCompilation=false

Result: PASS, 198 passed, 0 failed, 0 skipped.

Secrets detector findings:

S1 - Worker enablement does not appear to enable the analyzer host

Severity: HIGH

Evidence:

Impact: enabling the Worker secrets stage can still leave SecretsAnalyzerHost disabled, so no ruleset is loaded and _secretsAnalyzer.AnalyzeAsync(...) returns no findings.

Follow-up:

S2 - Bundle verification defaults are fail-open in analyzer options

Severity: HIGH

Evidence:

Impact: production-looking deployments can run with unsigned or invalid rule bundles unless stricter configuration is explicitly bridged into the analyzer host.

Follow-up:

S3 - Worker scan limits and analyzer scan limits are split

Severity: MEDIUM

Evidence:

Impact: scan behavior can differ between direct analyzer execution and Worker stage execution, causing noisy findings, unexpected cost, or missed large/small-file policy decisions.

Follow-up:

S4 - Direct ILanguageAnalyzer path does not persist findings

Severity: MEDIUM

Evidence:

Impact: secrets detection works only through the Worker stage path. If SecretsAnalyzer is ever loaded as a normal language analyzer plugin, findings can disappear.

Follow-up:

S5 - Secrets detector is language-blind and lacks language-context enrichment

Severity: MEDIUM

Evidence:

Impact: the system can find raw leaked values, but it cannot yet say whether a finding is in a framework secret store, a runtime-only environment bridge, a build-time config, a test fixture, or an actually reachable entrypoint path.

Follow-up:

S6 - Secrets detector has no manifest/plugin packaging story

Severity: LOW

Evidence:

Impact: this is acceptable if secrets stays built into Worker, but it should be explicit because language analyzers are moving toward restart plugin smoke gates.

Follow-up:

Inventory Matrix

AreaSource projectManifestPlugin entryLocal AGENTS/TASKSTests and fixturesDocs
Shared coresrc/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Langnone expectednone expectedAGENTS yes, TASKS yesLang.Tests: 19 test files, 81 fixture files, 15 goldenslanguage-analyzers-contract.md
Bun...Lang.Bunstellaops.analyzer.lang.bun; BunAnalyzerPluginBunAnalyzerPlugin : ILanguageAnalyzerPluginAGENTS yes, TASKS yesBun.Tests: 7 test files, 59 fixture files, 18 goldensanalyzers-bun.md
Deno...Lang.Denostellaops.analyzer.lang.deno; DenoAnalyzerPluginDenoAnalyzerPlugin : ILanguageAnalyzerPluginAGENTS missing in __Libraries, TASKS yes; legacy AGENTS exists at src/Scanner/StellaOps.Scanner.Analyzers.Lang.DenoDeno.Tests: 22 test files, 12 fixture files, 0 goldensdesign/deno-analyzer-plan.md, design/deno-analyzer-scope.md
DotNet...Lang.DotNetstellaops.analyzer.lang.dotnet; DotNetAnalyzerPluginDotNetAnalyzerPlugin : ILanguageAnalyzerPluginAGENTS yes, TASKS yesDotNet.Tests: 11 test files, 0 fixture files, 0 goldensdotnet-analyzer.md
Go...Lang.Gostellaops.analyzer.lang.go; GoAnalyzerPluginGoAnalyzerPlugin : ILanguageAnalyzerPluginAGENTS yes, TASKS yesGo.Tests: 5 test files, 18 fixture files, 7 goldensanalyzers-go.md
Java...Lang.Javastellaops.analyzer.lang.java; JavaLanguageAnalyzermissing *AnalyzerPlugin.csAGENTS yes, TASKS yesJava.Tests: 27 test files, 38 fixture files, 15 goldensanalyzers-java.md
Node...Lang.Nodestellaops.analyzer.lang.node; NodeAnalyzerPluginNodeAnalyzerPlugin : ILanguageAnalyzerPluginAGENTS yes, TASKS yesNode.Tests: 16 test files, 58 fixture files, 17 goldens; Node.SmokeTests: 2 test filesanalyzers-node.md
PHP...Lang.PhpmissingPhpAnalyzerPlugin : ILanguageAnalyzerPluginAGENTS missing in __Libraries, TASKS yes; legacy AGENTS exists at src/Scanner/StellaOps.Scanner.Analyzers.Lang.PhpPhp.Tests: 9 test files, 14 fixture files, 7 goldensphp-analyzer-owner-manifest.md, design/php-autoload-design.md
Python...Lang.Pythonstellaops.analyzer.lang.python; PythonAnalyzerPluginPythonAnalyzerPlugin : ILanguageAnalyzerPluginAGENTS yes, TASKS yesPython.Tests: 21 test files, 102 fixture files, 14 goldensanalyzers-python.md
Ruby...Lang.Rubystellaops.analyzer.lang.ruby; RubyAnalyzerPluginRubyAnalyzerPlugin : ILanguageAnalyzerPluginAGENTS missing in __Libraries, TASKS yes; legacy AGENTS exists at src/Scanner/StellaOps.Scanner.Analyzers.Lang.RubyRuby.Tests: 2 test files, 48 fixture files, 7 goldensdesign/ruby-analyzer.md
Rust...Lang.Ruststellaops.analyzer.lang.rust; RustAnalyzerPluginRustAnalyzerPlugin : ILanguageAnalyzerPluginAGENTS yes, TASKS yes; test project TASKS missingRust.Tests: 4 test files, 0 fixture files, 0 goldensarchitecture.md Rust row

Notes:

Command Evidence

Build sweep command:

$projects = @(
'src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang/StellaOps.Scanner.Analyzers.Lang.csproj',
'src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Bun/StellaOps.Scanner.Analyzers.Lang.Bun.csproj',
'src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Deno/StellaOps.Scanner.Analyzers.Lang.Deno.csproj',
'src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.DotNet/StellaOps.Scanner.Analyzers.Lang.DotNet.csproj',
'src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Go/StellaOps.Scanner.Analyzers.Lang.Go.csproj',
'src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Java/StellaOps.Scanner.Analyzers.Lang.Java.csproj',
'src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node/StellaOps.Scanner.Analyzers.Lang.Node.csproj',
'src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Php/StellaOps.Scanner.Analyzers.Lang.Php.csproj',
'src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Python/StellaOps.Scanner.Analyzers.Lang.Python.csproj',
'src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Ruby/StellaOps.Scanner.Analyzers.Lang.Ruby.csproj',
'src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Rust/StellaOps.Scanner.Analyzers.Lang.Rust.csproj'
)
foreach ($project in $projects) {
  dotnet build $project -v minimal /m:1 /p:UseSharedCompilation=false
}

Build results:

ProjectResultNotes
Shared corePASSDependency warnings only.
BunPASSNo analyzer-local warnings captured.
DenoPASSNo analyzer-local warnings captured.
DotNetPASSAnalyzer warning: MsBuildPropertyResolver.cs:122 STELLA0102 deterministic ordering.
GoPASSNo analyzer-local warnings captured.
JavaPASSAnalyzer warnings: JavaPropertyResolver.cs:117 and JavaEntrypointResolver.cs:439 STELLA0102 deterministic ordering.
NodePASSNo analyzer-local warnings captured.
PHPPASSNo analyzer-local warnings captured.
PythonPASSNo analyzer-local warnings captured.
RubyPASSNo analyzer-local warnings captured.
RustPASSNo analyzer-local warnings captured.

Test sweep command:

$projects = @(
'src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Tests/StellaOps.Scanner.Analyzers.Lang.Tests.csproj',
'src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Bun.Tests/StellaOps.Scanner.Analyzers.Lang.Bun.Tests.csproj',
'src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests/StellaOps.Scanner.Analyzers.Lang.Deno.Tests.csproj',
'src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests/StellaOps.Scanner.Analyzers.Lang.DotNet.Tests.csproj',
'src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Go.Tests/StellaOps.Scanner.Analyzers.Lang.Go.Tests.csproj',
'src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Java.Tests/StellaOps.Scanner.Analyzers.Lang.Java.Tests.csproj',
'src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Node.Tests/StellaOps.Scanner.Analyzers.Lang.Node.Tests.csproj',
'src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests/StellaOps.Scanner.Analyzers.Lang.Node.SmokeTests.csproj',
'src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Php.Tests/StellaOps.Scanner.Analyzers.Lang.Php.Tests.csproj',
'src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Python.Tests/StellaOps.Scanner.Analyzers.Lang.Python.Tests.csproj',
'src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Ruby.Tests/StellaOps.Scanner.Analyzers.Lang.Ruby.Tests.csproj',
'src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Rust.Tests/StellaOps.Scanner.Analyzers.Lang.Rust.Tests.csproj'
)
foreach ($project in $projects) {
  dotnet test $project -v minimal /m:1 /p:UseSharedCompilation=false
}

Targeted test results:

Test projectResult
Lang.TestsPASS: 154 passed, 0 failed, 0 skipped
Bun.TestsPASS: 115 passed, 0 failed, 0 skipped
Deno.TestsPASS: 31 passed, 0 failed, 0 skipped
DotNet.TestsPASS: 181 passed, 0 failed, 0 skipped
Go.TestsPASS: 99 passed, 0 failed, 0 skipped
Java.TestsPASS: 376 passed, 0 failed, 0 skipped
Node.TestsPASS: 369 passed, 0 failed, 0 skipped
Node.SmokeTestsPASS: 1 passed, 0 failed, 0 skipped
Php.TestsPASS: 250 passed, 0 failed, 0 skipped
Python.TestsPASS: 473 passed, 0 failed, 0 skipped
Ruby.TestsPASS: 18 passed, 0 failed, 0 skipped
Rust.TestsPASS: 15 passed, 0 failed, 0 skipped

No-build rerun command used to capture counts that were truncated in the first sweep:

dotnet test <individual-test-project.csproj> --no-build --no-restore -v minimal /m:1 /p:UseSharedCompilation=false

Smoke harness commands:

dotnet run --project src/Tools/LanguageAnalyzerSmoke/LanguageAnalyzerSmoke.csproj -- --repo-root . --analyzer python
dotnet run --project src/Tools/LanguageAnalyzerSmoke/LanguageAnalyzerSmoke.csproj -- --repo-root . --analyzer rust

Smoke results:

AnalyzerResultEvidence
PythonPASSLoaded analyzer python; scenarios simple-venv, pip-cache, and layered-editable passed cold/warm determinism and golden comparison.
RustFAILLoaded analyzer rust, then failed: Could not load file or assembly 'Tomlyn, Version=0.19.0.0, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified. Plugin directory contained manifest.json, StellaOps.Scanner.Analyzers.Lang.Rust.dll, and pdb, but not Tomlyn.dll.

Findings

F1 - Restart plugin packaging misses copy-local dependencies

Severity: HIGH

Evidence:

Impact: analyzers can pass unit tests and normal source builds but fail when deployed as restart plugins. This directly affects offline kit promises and Worker plugin loadability.

Follow-up:

F2 - Plugin packaging rule also matches test projects

Severity: HIGH

Evidence:

Impact: building tests can copy test assemblies into plugins/scanner/analyzers/lang/<test-project>. That pollutes local plugin outputs and can hide release packaging mistakes.

Follow-up:

F3 - Java manifest is not compatible with the shared plugin catalog

Severity: HIGH

Evidence:

Impact: Java has a manifest but is unlikely to restart-load through the same plugin catalog used by the other language analyzers. Static/in-process tests still pass, so this needs a plugin-load smoke rather than more analyzer unit tests.

Follow-up:

F4 - PHP plugin has no manifest

Severity: HIGH

Evidence:

Impact: PHP cannot be copied as a complete restart plugin by the current manifest-based target and offline docs overstate current artifact reality.

Follow-up:

F5 - Node Phase 22 has nondeterministic explicit-key fallbacks

Severity: HIGH

Evidence:

Impact: identical observation input with a missing Path can produce different SBOM output between cold and warm runs. Existing tests appear to cover normal path-bearing observations, not the missing-path branch.

Follow-up:

F6 - Python runtime evidence timestamp fallback is wall-clock based

Severity: MEDIUM

Evidence:

Impact: static Python analysis remains covered, but optional runtime evidence with missing timestamps can differ between identical input runs.

Follow-up:

F7 - Analyzer build still emits deterministic-ordering warnings

Severity: MEDIUM

Evidence:

Impact: builds pass, but deterministic analyzer warnings remain in language analyzer code. These should not be treated as release-clean.

Follow-up:

F8 - Smoke harness covers only Python and Rust

Severity: MEDIUM

Evidence:

Impact: several analyzers have manifests and unit tests but no restart-plugin load and cold/warm determinism gate.

Follow-up:

F9 - Deno writes a runtime shim into the analyzed root

Severity: MEDIUM

Evidence:

Impact: Deno runtime execution is guarded, but static analysis can still mutate the analyzed root by writing the shim. This may be acceptable by current design, but it should be an explicit contract.

Follow-up:

F10 - Local ownership docs drift for Deno, PHP, Ruby, and Rust tests

Severity: LOW

Evidence:

Impact: implementers may read stale local instructions or miss module-local workflow expectations.

Follow-up:

F11 - Offline-kit language analyzer list is out of sync with current source

Severity: LOW

Evidence:

Impact: release operators cannot tell from docs which language analyzers are expected to ship in the offline kit.

Follow-up:

F12 - Ruby runtime trace runner is not currently wired, but needs hardening before use

Severity: LOW

Evidence:

Impact: no current runtime-execution issue was found in Ruby analyzer flow, but the unused runner should not be wired later without root-containment, binary allowlisting, timeout, and tests comparable to Deno.

Follow-up:

Analyzer Notes

Shared Core

The core build and Lang.Tests passed. The contract doc is explicit about PURL vs explicit-key identity, evidence locators, traversal bounds, and deterministic behavior. The plugin catalog loads ILanguageAnalyzerPlugin from copied plugin directories, so manifest-only correctness is not enough; each restart analyzer needs a compatible plugin implementation and deployment-ready dependency closure.

Bun

Bun has a manifest and plugin entry, targeted tests passed, and fixture/golden coverage is comparatively strong. No analyzer-local build warnings were captured. Remaining risk is cross-cutting: smoke coverage is absent and the MSBuild packaging rule can misclassify tests.

Deno

Deno has a manifest and plugin entry, targeted tests passed, and the runtime runner validates entrypoint containment, binary allowlisting, and root-contained binaries. The review found a root-mutation question: the analyzer writes the runtime shim to context.RootPath even before static collection. Deno also needs current-directory AGENTS hygiene under __Libraries.

DotNet

DotNet has a manifest and plugin entry, targeted tests passed, and the main issue is deterministic-ordering warning STELLA0102 in MsBuildPropertyResolver.cs:122. Fixture/golden inventory is lighter than Java/Node/Python, so parity work should favor behavioral edge cases around property inheritance and lock/deps resolution.

Go

Go has a manifest and plugin entry, targeted tests passed, and no analyzer-local build warnings were captured. It has fixture/golden coverage and should be a good early smoke-profile candidate because offline-kit docs already promise Go plugin artifacts.

Java

Java has strong tests and fixture/golden coverage, but restart plugin loadability is the largest issue. Its manifest points directly to JavaLanguageAnalyzer, while the shared catalog expects ILanguageAnalyzerPlugin. Java also has deterministic-ordering warnings in property resolution and SPI edge resolution.

Node

Node targeted tests and the existing test-project smoke passed. The main issue is the Phase 22 Guid.NewGuid() fallback for pathless component and entrypoint records, which reintroduces a deterministic-output risk similar to the runtime fallback that architecture says was already closed.

PHP

PHP targeted tests passed and it has an analyzer plugin class, but no manifest. This conflicts with the offline-kit docs that say PHP ships with a manifest-bearing plugin directory. PHP should get a manifest and smoke profile before release evidence claims plugin deployment parity.

Python

Python targeted tests passed and the language analyzer smoke passed across simple-venv, pip-cache, and layered-editable scenarios. The remaining deterministic gap is optional runtime evidence that replaces absent event timestamps with wall-clock UTC.

Ruby

Ruby targeted tests passed, with good fixture/golden inventory but only two test source files. The active analyzer reads existing runtime evidence; the unused runtime trace runner should be hardened before it is wired. Ruby also needs current-directory AGENTS hygiene under __Libraries.

Rust

Rust targeted tests passed, but smoke failed because the plugin deployment directory did not include the Tomlyn dependency. This is the clearest evidence that test pass counts do not prove restart plugin deployability. Rust test project TASKS hygiene is also inconsistent with the rest of the language analyzer tests.

Per-Analyzer First-Class Detector Gap Report

Shared Language Analyzer Core

Current strengths:

Missing to be first class:

Improvement plan:

Bun

Current strengths:

Missing to be first class:

Improvement plan:

Deno

Current strengths:

Missing to be first class:

Improvement plan:

DotNet

Current strengths:

Missing to be first class:

Improvement plan:

Go

Current strengths:

Missing to be first class:

Improvement plan:

Java

Current strengths:

Missing to be first class:

Improvement plan:

Node

Current strengths:

Missing to be first class:

Improvement plan:

PHP

Current strengths:

Missing to be first class:

Improvement plan:

Python

Current strengths:

Missing to be first class:

Improvement plan:

Ruby

Current strengths:

Missing to be first class:

Improvement plan:

Rust

Current strengths:

Missing to be first class:

Improvement plan:

Cross-Analyzer Backlog

Recommended follow-up sprint candidates:

  1. Scanner language analyzer plugin packaging hardening.

    • Fix copy-local dependency inclusion.
    • Exclude test projects from plugin packaging.
    • Add packaging assertions for dependency-bearing analyzers.
    • Close Rust smoke failure.
  2. Restart plugin loadability parity.

    • Add Java ILanguageAnalyzerPlugin or document/test a separate loader.
    • Add PHP manifest.
    • Expand smoke profiles for Java, PHP, Go, Node, Ruby, DotNet, Bun, and Deno.
  3. Determinism cleanup.

    • Replace Node Phase 22 Guid.NewGuid() fallbacks.
    • Replace or inject Python runtime timestamp fallback.
    • Close DotNet/Java STELLA0102 warnings with tests where output ordering changes.
  4. Scanner analyzer docs and ownership hygiene.

    • Align offline-kit analyzer lists with real source manifests and plugin outputs.
    • Move or recreate Deno/PHP/Ruby AGENTS files under active __Libraries paths.
    • Add or explicitly waive Rust.Tests/TASKS.md.
  5. Runtime execution contract review.

    • Decide whether analyzers may write runtime shims into scan roots.
    • Before wiring Ruby runtime execution, add Deno-equivalent containment, allowlist, timeout, and skip-behavior tests.
  6. First-class detector framework.

    • Add shared typed detector output with rule ids, severity, confidence, evidence locators, deterministic fingerprints, and SARIF/policy mapping.
    • Add ScanAnalysisKeys.LanguageDetections and exporters for Policy, SARIF, UI, and evidence bundles.
    • Convert ad hoc capability/framework/crypto/runtime metadata into typed findings without losing SBOM component metadata.
  7. Secrets detector first-class integration.

    • Fix Worker/analyzer enablement mismatch.
    • Wire fail-closed signed bundle settings into the Worker path.
    • Unify file-size, file-count, extension, directory, and binary filters.
    • Decide whether secrets is Worker-built-in or restart plugin, and add the matching smoke/startup evidence.
    • Add language secret-context signals from each analyzer into the central secrets report.
  8. Per-language security detector expansion.

    • Add route/framework detectors for Bun, Deno, DotNet, Go, Java, Node, PHP, Python, Ruby, and Rust.
    • Add typed security findings for dynamic code, process execution, SSRF, SQL/NoSQL injection sinks, deserialization, weak crypto/TLS, native/FFI, template injection, and config/secret access.
    • Add one language-specific secret/config fixture per analyzer and one negative fixture proving masking/suppression behavior.