Scanner Language Analyzer Contracts (Identity / Evidence / Container Layout)
This document freezes the cross-analyzer contracts that are shared by the language analyzers (Java, .NET, Python, Node, Bun). These rules exist to prevent false matches, keep outputs deterministic, and protect against host-path leakage.
1) Identity Safety Contract (PURL vs Explicit Key)
1.1 Goals
- No fake versions: never encode version ranges, tags, local paths, or git URLs as a versioned PURL.
- No collisions: explicit-key identities must not collide with concrete PURLs and must be deterministic across OS path separators.
- Proof-first: emit concrete PURLs only when the analyzer has concrete, replayable evidence for the version.
1.2 When to emit a concrete PURL
Emit a concrete (versioned) PURL only when both are true:
- The analyzer can determine a concrete version (ecosystem-specific) for the component.
- The version is backed by replayable evidence (e.g., installed artifact metadata or lockfile-resolved entry).
Typical sources that qualify:
- Installed inventory (e.g.,
node_modules/**/package.json, Python*.dist-info/METADATA, .NETdeps.jsonentries). - Lockfile-resolved inventory (e.g.,
bun.lockentry withname@versionand integrity/resolved URL).
1.3 When to emit an explicit-key component (required)
Emit an explicit-key component when the dependency is declared-only or otherwise non-concrete:
- Version ranges / operators (
^,~,>=,<,*,x,latest, etc.). - Workspace/link/file dependencies (
workspace:*,link:,file:, local path refs, editable installs). - Git dependencies (git URL / commit / ref) when a concrete semantic version is not provable from local evidence.
- Unknown / missing version.
Rule: If the analyzer cannot prove a concrete version from local evidence, it must not emit a versioned PURL for that dependency.
1.4 Explicit-key format (canonical)
For declared-only / non-concrete identities, analyzers must emit:
componentKey:explicit::<analyzerId>::<ecosystem>::<name>::sha256:<digest>purl:nullversion:null
Where <digest> is sha256 of the canonical UTF-8 string:
<ecosystem>\n<normalizedName>\n<normalizedSpec>\n<originLocator>
Canonicalization rules:
<normalizedName>uses ecosystem naming rules (e.g., npm scoped names keep@scope/name).<normalizedSpec>is the original declared specifier (range/tag/url/path), trimmed; for unknown, use"".<originLocator>is project-relative with/separators (e.g.,package.json#dependencies,requirements.txt,Directory.Packages.props#PackageVersion:Foo).- No absolute paths, drive letters, or host roots appear in any input to the digest.
1.5 Required metadata for explicit-key components
Explicit-key components must include (at minimum) these metadata keys:
declaredOnly=truedeclared.source=<file>(e.g.,package.json,Directory.Packages.props)declared.locator=<originLocator>(same string used in digest)declared.versionSpec=<normalizedSpec>(original specifier or empty)declared.scope=<prod|dev|peer|optional|unknown>when applicabledeclared.sourceType=<range|tag|git|tarball|file|link|workspace|path|editable|unknown>
2) Evidence Locator Contract
2.1 General rules
- Evidence locators are external-facing and must be stable and parseable.
- Every locator is project-relative with
/separators (never absolute). - Evidence content/hashing must be bounded; when bounds are exceeded, emit deterministic
skippedmarkers in metadata instead of silently omitting.
2.2 Locator formats (canonical)
File evidence
locator:<relativePath>(e.g.,packages/app/package.json)source: a stable discriminator (e.g.,package.json,pom.xml,METADATA)
Lockfile entry evidence
locator:<lockfileRelativePath>:<selector>- Examples:
- Node package-lock:
package-lock.json:packages/app/node_modules/foo - Bun lock:
bun.lock:packages[foo@1.2.3] - Maven/Gradle lock:
gradle.lockfile:com.example:foo:1.2.3
- Node package-lock:
Nested artifact evidence
locator:<outer>!<inner>!<path>- Example:
demo-jni.jar!META-INF/native-image/demo/jni-config.json
Derived evidence
locator: a stable synthetic name (e.g.,phase22.ndjson)source: a stable synthetic source (e.g.,node.observation)
2.3 Hashing rules (baseline)
- Hash only bounded inputs (default: 1 MiB per evidence value/file; analyzers may choose a tighter cap).
- Hash algorithm:
sha256over UTF-8 bytes for textual evidence, raw bytes for file evidence. - If hashing is skipped due to bounds or errors, emit deterministic metadata markers (e.g.,
hashSkipped=true,hashSkipped.reason=sizeCap).
3) Container Layout Discovery Contract
3.1 Layer root candidates
Language analyzers that support container-root discovery must treat these as candidate roots under the analysis root:
layers/*(direct children).layers/*(direct children; must not be skipped)layer*(direct children of the analysis root, e.g.,layer1/,layer2/)
Each candidate root is scanned independently for projects.
3.2 Bounds and traversal safety (required)
- Deterministic traversal (sorted directory enumeration).
- Depth caps per candidate root; hard cap on total discovered project roots.
- Must never recurse into
node_modules/(Node/Bun) or equivalent heavy dirs. - Hidden directories may be skipped except
.layerswhich is treated as a top-level candidate root. - No symlink escape: if symlinks are followed, resolved targets must remain within the candidate root prefix and cycles must be prevented.
3.3 Overlay/whiteout semantics
- If an analyzer implements overlay semantics (notably Python container adapters), whiteouts and precedence rules must be explicit, deterministic, and fixture-tested.
- If an analyzer does not implement overlay semantics, it must still keep discovery bounded and must not silently drop projects; emit deterministic “skipped” markers when bounds prevent full traversal.
4) First-Class Detector Output Contract
Language analyzers that emit detector evidence must use shared Scanner contracts rather than analyzer-local metadata parsing:
ScanAnalysisKeys.LanguageDetectionsstoresLanguageDetectionFinding[].ScanAnalysisKeys.SecretContextSignalsstoresSecretContextSignal[].ScanAnalysisKeys.SecretFindingsremains the raw secret-finding report. Language analyzers may enrich context, but raw secret matching stays centralized in the secrets analyzer.
4.1 Required detector fields
Every detector record must carry enough data for SARIF, policy, UI, and audit consumers to operate without parsing analyzer-specific payloads:
- Stable rule ID or predicate (
RuleId,PolicyPredicate,SarifRuleId). - Finding kind (
Kind) and severity (Severity). - Confidence as a deterministic numeric score.
- Evidence locator that is project-relative when it points to source.
- Component key, entrypoint ID, and reachability summary when known.
- Deterministic fingerprint from
DetectorFindingFingerprint. - Suppressibility flag and normalized tags/metadata.
Records may omit fields that are genuinely unknown, but they must not substitute absolute host paths, random IDs, or wall-clock values.
4.2 SARIF and policy bridge
StellaOps.Scanner.Sarif.DetectorFindingSarifMapper maps shared detector records into SARIF FindingInput values:
- Language detector findings use
STELLA-LANG-001unless the record supplies a narrowerSarifRuleId. - Secret context signals use existing secret SARIF rules and include context only. Raw secret values must not appear in title, description, metadata, fingerprints, or custom properties.
- Fingerprints are derived from the shared detector fingerprint and threaded through SARIF artifact digests to keep result IDs stable.
ScanAnalysisCompositionBuilder.BuildDetectorPolicyFindings consumes the same shared analysis keys and emits policy-facing detector findings. This prevents policy/SBOM emitters from parsing analyzer-specific observation JSON.
4.3 Workspace source-pattern extraction
StellaOps.Scanner.Analyzers.Lang.LanguageDetectorFindingExtractor normalizes analyzer results into shared detector records and also performs bounded source-pattern extraction for first-class language detector coverage:
- Supported language families: Python, Node, Deno, Bun, Ruby, PHP, Rust, Go, DotNet, and Java.
- Emitted source-pattern kinds:
language.route,language.entrypoint,language.secret-context,language.security-sink, andlanguage.remote-importwhere applicable. - Traversal is deterministic, skips heavy/generated directories, caps source files and per-file bytes, and emits project-relative evidence locators only.
- Metadata must describe the detector rule and locator context, not matched source lines or raw secret values.
- Worker persistence stores merged component-derived and source-derived detections under
ScanAnalysisKeys.LanguageDetections.
UI-facing policy/evidence linksets may carry a detector reference with rule id, kind, fingerprint, evidence locator, policy predicate, language, and analyzer id. The UI must treat that reference as an opaque shared detector contract and must not parse analyzer-specific payloads.
Compliance
Sprints docs/implplan/SPRINT_0403_0001_0001_scanner_java_detection_gaps.md through docs/implplan/SPRINT_0407_0001_0001_scanner_bun_detection_gaps.md (and the program sprint docs/implplan/SPRINT_0408_0001_0001_scanner_language_detection_gaps_program.md) carry the historical per-analyzer implementation and test evidence required to enforce this contract. The 2026-05-06 first-class detector correction and uplift sprints extend the active contract with shared detector records, fingerprints, bounded all-language source-pattern extraction, SARIF mapping, policy bridge coverage, and UI detector references.
