Scanner Surface — Attack-Surface Analysis Framework

Contract ID: CONTRACT-SCANNER-SURFACE-014 Status: Published (reconciled against implementation 2026-05-30) Version: 1.1.0  ·  Published: 2025-12-05 Owners: Scanner Guild Source of truth: src/Scanner/__Libraries/StellaOps.Scanner.Surface (+ .Env, .FS, .Secrets, .Validation)

Overview

This contract defines the Stella Ops Scanner Surface analysis framework: pattern-based attack-surface enumeration and entry-point discovery in StellaOps.Scanner.Surface. It maps the exposed surface of a scanned artifact — network endpoints, process execution, secret access, external calls, and the application’s entry points — into deterministic, attestable records that feed risk scoring and policy signals.

Audience: engineers extending the surface analyzer (new collectors, languages, or signals) and consumers of its JSON/NDJSON output. Sections below are reconciled against the implementation; each section’s “Source:” note points at the authoritative .cs file, and roadmap items are marked not implemented / roadmap.

Scope

SCANNER-SURFACE-01 establishes the foundational surface analysis patterns:

The sibling libraries Surface.FS, Surface.Env, and Surface.Secrets are general-purpose Scanner surface-storage / environment / secrets components, not direct collaborators of this analyzer — see Integration Points.


Surface Analysis Model

Surface Types

The SurfaceType enum has eight members. Detection today is regex/pattern-based (PatternBasedSurfaceCollector) and route-pattern-based for entry points (NodeJsEntryPointCollector); call-graph, VFS, and data-flow methods are not yet implemented (see Implementation Status).

SurfaceTypeDescriptionBuilt-in collectorDetection method
NetworkEndpointExposed ports, listeners, endpointsNetworkEndpointCollector (surface.network-endpoint)Regex patterns
FileOperationFile-system operations, sensitive file accessnone built-inRegex patterns (when registered)
ProcessExecutionCommand execution, subprocess spawnProcessExecutionCollector (surface.process-execution)Regex patterns
CryptoOperationCryptographic operations, key handlingnone built-inRegex patterns (when registered)
AuthenticationPointAuthentication points, session handlingnone built-inRegex patterns (when registered)
InputHandlingUser input handling, injection pointsnone built-inRegex patterns (when registered)
SecretAccessSecret/credential access patternsSecretAccessCollector (surface.secret-access)Regex patterns
ExternalCallExternal service calls, outbound connectionsExternalCallCollector (surface.external-call)Regex patterns

Built-in collectors registered by AddSurfaceAnalysisWithDefaultCollectors(): NetworkEndpointCollector, SecretAccessCollector, ProcessExecutionCollector, ExternalCallCollector (surface entries) and NodeJsEntryPointCollector (entry points). The remaining types (FileOperation, CryptoOperation, AuthenticationPoint, InputHandling) are valid enum values but have no built-in collector wired by default; register a custom PatternBasedSurfaceCollector via AddSurfaceCollector<T>() to emit them.

Surface Entry

Source: src/Scanner/__Libraries/StellaOps.Scanner.Surface/Models/SurfaceEntry.cs, Models/SurfaceType.cs.

public sealed record SurfaceEntry
{
    public required string Id { get; init; }            // SHA256(type|path|context), lowercase hex
    public required SurfaceType Type { get; init; }
    public required string Path { get; init; }          // File path or endpoint path
    public required string Context { get; init; }       // Function/method context
    public required ConfidenceLevel Confidence { get; init; }  // enum, not a 0..1 double
    public IReadOnlyList<string> Tags { get; init; } = [];
    public required SurfaceEvidence Evidence { get; init; }

    // Deterministic ID helper.
    public static string ComputeId(SurfaceType type, string path, string context);
}

public sealed record SurfaceEvidence
{
    public required string File { get; init; }
    public required int Line { get; init; }
    public required string Hash { get; init; }          // SHA256("file:line:content"), lowercase hex
    public string? Snippet { get; init; }
    public IReadOnlyDictionary<string, string>? Metadata { get; init; }
}

public enum SurfaceType
{
    NetworkEndpoint,
    FileOperation,
    ProcessExecution,
    CryptoOperation,
    AuthenticationPoint,
    InputHandling,
    SecretAccess,
    ExternalCall
}

// Confidence is an ordinal enum (NOT a 0.0..1.0 double on the entry).
public enum ConfidenceLevel
{
    Low,        // ~0.25 numeric value used for threshold/risk math
    Medium,     // ~0.50
    High,       // ~0.75
    VeryHigh    // ~1.00
}

The eight SurfaceType members above match the implementation exactly. The id is computed as SHA256(type|path|context) (lowercase hex via SurfaceEntry.ComputeId) — see Determinism Requirements.

Orchestration API

ISurfaceAnalyzer is the entry point. It runs all registered surface-entry collectors and (optionally) entry-point collectors, sorts results for determinism, computes a risk score, emits signals, and writes output.

Source: src/Scanner/__Libraries/StellaOps.Scanner.Surface/SurfaceAnalyzer.cs.

public interface ISurfaceAnalyzer
{
    Task<SurfaceAnalysisResult> AnalyzeAsync(
        string scanId,
        string rootPath,
        SurfaceAnalysisOptions? options = null,
        CancellationToken cancellationToken = default);
}

public sealed record SurfaceAnalysisOptions
{
    public SurfaceCollectorOptions CollectorOptions { get; init; } = new();
    public SurfaceOutputOptions OutputOptions { get; init; } = new();
    public bool EmitSignals { get; init; } = true;
    public bool DiscoverEntryPoints { get; init; } = true;
    public IReadOnlySet<string> Languages { get; init; } = new HashSet<string>();
}

public sealed record SurfaceCollectorOptions
{
    public int MaxDepth { get; init; } = 3;               // reserved; pattern collectors do not yet walk a call graph
    public double MinimumConfidence { get; init; } = 0.7; // threshold against ConfidenceLevel's numeric value
    public IReadOnlySet<SurfaceType> IncludeTypes { get; init; } = new HashSet<SurfaceType>();
    public IReadOnlySet<SurfaceType> ExcludeTypes { get; init; } = new HashSet<SurfaceType>();
    public bool IncludeSnippets { get; init; } = true;
    public int MaxSnippetLength { get; init; } = 500;
}

Risk score (summary.riskScore, 0.0–1.0): a confidence-weighted average over entries (per-type weights — SecretAccess 1.0 down to FileOperation 0.3 — times the entry’s ConfidenceLevel numeric value), plus a flat contribution from the entry-point count, clamped to [0, 1]. Computed by SurfaceAnalyzer.CalculateRiskScore.

DI: AddSurfaceAnalysis() registers the analyzer/registry/emitter/writer; AddSurfaceAnalysisWithDefaultCollectors() additionally registers the built-in collectors and a SurfaceCollectorInitializer whose Initialize() must be called once at startup to populate the registry.


Integration Points

Scope note. StellaOps.Scanner.Surface.FS, .Env, and .Secrets are general-purpose Scanner surface-storage / environment / secrets libraries used across Scanner.Worker and Zastava. They are not dedicated outputs of this surface-analysis framework, and their public APIs differ substantially from earlier drafts of this contract. The StellaOps.Scanner.Surface project references only .FS and .Env (not .Secrets and not EntryTrace).

Surface.FS Integration

StellaOps.Scanner.Surface.FS publishes a canonical surface manifest (artifact index + determinism metadata), not a list of SurfaceEntry objects.

Source: src/Scanner/__Libraries/StellaOps.Scanner.Surface.FS/ISurfaceManifestWriter.cs, SurfaceManifestModels.cs.

public interface ISurfaceManifestWriter
{
    Task<SurfaceManifestPublishResult> PublishAsync(
        SurfaceManifestDocument document,
        CancellationToken cancellationToken = default);
}

// Canonical manifest (schema "stellaops.surface.manifest@1").
public sealed record SurfaceManifestDocument
{
    public const string DefaultSchema = "stellaops.surface.manifest@1";
    public string Schema { get; init; } = DefaultSchema;
    public string Tenant { get; init; } = "";
    public string? ImageDigest { get; init; }
    public string? ScanId { get; init; }
    public DateTimeOffset GeneratedAt { get; init; }
    public SurfaceManifestSource? Source { get; init; }
    public IReadOnlyList<SurfaceManifestArtifact> Artifacts { get; init; }
    public string? DeterminismMerkleRoot { get; init; }
    public SurfaceDeterminismMetadata? Determinism { get; init; }
    public ReplayBundleReference? ReplayBundle { get; init; }
    public SurfaceFacetSeals? FacetSeals { get; init; }   // per-facet drift seals
}

The .FS library also exposes ISurfaceManifestReader, ISurfaceCache / FileSurfaceCache (content cache with quota), IFacetSealExtractor, and SurfaceManifestDeterminismVerifier.

Surface.Env Integration

StellaOps.Scanner.Surface.Env resolves component-scoped surface settings from environment-variable prefixes (first match wins) — it does not define the STELLA_SURFACE_* variables shown in earlier drafts.

Source: src/Scanner/__Libraries/StellaOps.Scanner.Surface.Env/ISurfaceEnvironment.cs, SurfaceEnvironmentSettings.cs, SurfaceEnvironmentOptions.cs.

public interface ISurfaceEnvironment
{
    SurfaceEnvironmentSettings Settings { get; }
    IReadOnlyDictionary<string, string> RawVariables { get; }
}

public sealed record SurfaceEnvironmentSettings(
    Uri SurfaceFsEndpoint,
    string SurfaceFsBucket,
    string? SurfaceFsRegion,
    DirectoryInfo CacheRoot,
    int CacheQuotaMegabytes,
    bool PrefetchEnabled,
    IReadOnlyCollection<string> FeatureFlags,
    SurfaceSecretsConfiguration Secrets,
    string Tenant,
    SurfaceTlsConfiguration Tls);

SurfaceEnvironmentOptions carries the logical ComponentName (default "Scanner.Worker"), the ordered prefix list (via AddPrefix), RequireSurfaceEndpoint (default true), an optional TenantResolver, and a set of KnownFeatureFlags.

Surface.Secrets Integration

StellaOps.Scanner.Surface.Secrets is a secret resolution/provider framework (it hands out secret material such as CAS-access tokens, registry credentials, and attestation keys to surface components). It does not scan files for leaked secrets, and there is no ISurfaceSecretScanner / SecretFinding / SecretScanOptions / IPhysicalFileProvider type anywhere in the codebase.

Source: src/Scanner/__Libraries/StellaOps.Scanner.Surface.Secrets/ISurfaceSecretProvider.cs, SurfaceSecretRequest.cs, SurfaceSecretHandle.cs.

public interface ISurfaceSecretProvider
{
    SurfaceSecretHandle Get(SurfaceSecretRequest request);
    ValueTask<SurfaceSecretHandle> GetAsync(
        SurfaceSecretRequest request,
        CancellationToken cancellationToken = default);
}

public sealed record SurfaceSecretRequest(
    string Tenant,
    string Component,
    string SecretType,
    string? Name = null);

// SurfaceSecretHandle wraps secret bytes (pooled, zeroed on Dispose) or an
// X509Certificate2Collection plus metadata.

Built-in providers include FileSurfaceSecretProvider, InlineSurfaceSecretProvider, InMemorySurfaceSecretProvider, OfflineSurfaceSecretProvider, and the decorators CompositeSurfaceSecretProvider, CachingSurfaceSecretProvider, and AuditingSurfaceSecretProvider. Known secret types: CAS access, registry access, and attestation (CasAccessSecret, RegistryAccessSecret, AttestationSecret).

Surface.Validation

StellaOps.Scanner.Surface.Validation (not referenced in earlier drafts) provides ISurfaceValidator / ISurfaceValidatorRunner with built-in validators (SurfaceCacheValidator, SurfaceEndpointValidator, SurfaceSecretsValidator) that check resolved surface configuration and emit SurfaceValidationIssues.


Policy Signals

Surface Signal Keys

Source: src/Scanner/__Libraries/StellaOps.Scanner.Surface/Signals/SurfaceSignalEmitter.cs.

public static class SurfaceSignalKeys
{
    public const string NetworkEndpoints = "surface.network.endpoints";
    public const string ExposedPorts     = "surface.network.ports";
    public const string FileOperations   = "surface.file.operations";
    public const string ProcessSpawns    = "surface.process.spawns";
    public const string CryptoUsage      = "surface.crypto.usage";
    public const string AuthPoints       = "surface.auth.points";
    public const string InputHandlers    = "surface.input.handlers";
    public const string SecretAccess     = "surface.secrets.access";
    public const string ExternalCalls    = "surface.external.calls";
    public const string TotalSurfaceArea = "surface.total.area";
    public const string RiskScore        = "surface.risk.score";
    public const string EntryPointCount  = "surface.entrypoints.count";
}

BuildSignals(result) always emits TotalSurfaceArea, RiskScore, and EntryPointCount, plus one per-type count keyed by the entries actually detected. ExposedPorts is declared but not emitted by BuildSignals today.

Signal Emission

The default SurfaceSignalEmitter logs the signals (it does not yet emit to a message bus or the policy engine — the production wiring is a TODO in the source).

public interface ISurfaceSignalEmitter
{
    Task EmitAsync(
        string scanId,
        IDictionary<string, object> signals,
        CancellationToken cancellationToken = default);
}

Entry Point Discovery

Language Analyzer Integration

Entry-point discovery is implemented by IEntryPointCollectors registered in the ISurfaceEntryRegistry. Today exactly one collector is shipped and registered by default: NodeJsEntryPointCollector (CollectorId = "entrypoint.nodejs", SupportedLanguages = { javascript, typescript, js, ts }). It detects routes for:

AnalyzerStatusFrameworks detected
Node (JS/TS)Implemented (NodeJsEntryPointCollector)Express, Fastify, Koa, Hapi, NestJS
.NETNot implemented (roadmap — Phase 2)Controllers, Minimal APIs, SignalR hubs
JavaNot implemented (roadmap — Phase 2)Servlets, JAX-RS, Spring MVC
PythonNot implemented (roadmap — Phase 2)Flask/Django views, FastAPI
GoNot implemented (roadmap)HTTP handlers, gRPC
PHPNot implemented (roadmap)Routes, controller actions
DenoNot implemented (roadmap)HTTP handlers, permissions

The Node collector’s EntryPoint.Id is a 16-char prefix of SHA256("file:method:path"), and it always sets Language = "javascript" (even for TypeScript inputs) — the original source language is reflected in Framework/file extension, not in Language.

Entry Point Model

Source: src/Scanner/__Libraries/StellaOps.Scanner.Surface/Models/EntryPoint.cs.

public sealed record EntryPoint
{
    public required string Id { get; init; }
    public required string Language { get; init; }
    public string? Framework { get; init; }              // nullable
    public required string Path { get; init; }           // URL path or route
    public string? Method { get; init; }                 // HTTP method or RPC, nullable
    public required string Handler { get; init; }        // function/method name
    public required string File { get; init; }
    public int Line { get; init; }
    public IReadOnlyList<string> Parameters { get; init; } = [];
    public IReadOnlyList<string> Middlewares { get; init; } = [];
}

Output Schema

Surface Analysis Result

SurfaceAnalysisWriter serializes SurfaceAnalysisResult with camelCase property names and a JsonStringEnumConverter(camelCase) — so type and confidence serialize as lowercase enum names (e.g. "networkEndpoint", "high"), not as numbers, and byType keys are camelCase enum names. riskScore lives under summary (there is no top-level riskScore). Null members (e.g. an absent snippet/metadata) are omitted. Output is compact JSON by default; set SurfaceOutputOptions.PrettyPrint for indented output.

{
  "scanId": "scan-abc123",
  "timestamp": "2025-12-05T12:00:00Z",
  "summary": {
    "totalEntries": 42,
    "byType": {
      "networkEndpoint": 15,
      "fileOperation": 10,
      "processExecution": 5,
      "cryptoOperation": 8,
      "secretAccess": 4
    },
    "riskScore": 0.65
  },
  "entries": [
    {
      "id": "9f1c…",
      "type": "networkEndpoint",
      "path": "src/Controllers/UserController.cs",
      "context": "GetUsers",
      "confidence": "veryHigh",
      "tags": ["network", "listener", "port"],
      "evidence": {
        "file": "src/Controllers/UserController.cs",
        "line": 42,
        "hash": "…",
        "metadata": { "pattern_id": "net-listen-port", "match": "…" }
      }
    }
  ],
  "entryPoints": []
}

SurfaceAnalysisWriter writes to {OutputPath}/surface-{scanId}.json when WriteToFile is true and OutputPath is set. It also exposes WriteNdjsonAsync(result) which streams one { "type": "summary"|"entry"|"entrypoint", "data": … } record per line.

Analysis Store Key

The store-key constant is defined on SurfaceAnalysisResult, not as a free-standing SurfaceAnalysisKey field:

// SurfaceAnalysisResult.StoreKey
public const string StoreKey = "scanner.surface.analysis";

Determinism Requirements

  1. Stable IDs: Surface-entry IDs computed as SHA256(type|path|context), lowercase hex (SurfaceEntry.ComputeId). Node entry-point IDs use a 16-char prefix of SHA256("file:method:path").
  2. Sorted Output: SurfaceAnalyzer sorts both entries and entryPoints by Id using StringComparison.Ordinal before building the result.
  3. Reproducible Hashes: All hashing is SHA-256 — evidence hashes (SHA256("file:line:content")), entry/entry-point IDs, and the .FS manifest Merkle root (SurfaceManifestDeterminismVerifier computes sha256:… digests). BLAKE3 is not used.
  4. JSON serialization: Output uses System.Text.Json with camelCase naming and JsonStringEnumConverter. Keys are not re-sorted into a canonical order beyond the record’s declaration order; the determinism guarantee comes from the sorted entry/entry-point arrays, not from key canonicalization.

Implementation Status

Phase 1 (core framework) is implemented in StellaOps.Scanner.Surface. Phases 2–3 are partially complete; the boxes below reflect the current state of the code.

Phase 1: Core Framework — implemented

Note: the .FS ISurfaceManifestWriter / manifest model is a separate Scanner surface-storage concern (used by Scanner.Worker), not invoked by SurfaceAnalyzer.

Phase 2: Language Integration — partial

Phase 3: Advanced Analysis — not started


Project Structure

src/Scanner/__Libraries/StellaOps.Scanner.Surface/
├── StellaOps.Scanner.Surface.csproj
├── SurfaceAnalyzer.cs                       # ISurfaceAnalyzer + risk scoring
├── SurfaceServiceCollectionExtensions.cs    # AddSurfaceAnalysis[WithDefaultCollectors]
├── Models/
│   ├── SurfaceEntry.cs                       # SurfaceEntry + SurfaceEvidence
│   ├── SurfaceType.cs                        # SurfaceType + ConfidenceLevel
│   └── EntryPoint.cs                         # EntryPoint + Summary + Result
├── Discovery/
│   ├── ISurfaceEntryCollector.cs             # + IEntryPointCollector, options, context
│   └── SurfaceEntryRegistry.cs
├── Collectors/
│   ├── PatternBasedSurfaceCollector.cs       # base class + SurfacePattern
│   ├── NetworkEndpointCollector.cs
│   ├── SecretAccessCollector.cs
│   ├── ProcessExecutionCollector.cs
│   ├── ExternalCallCollector.cs
│   └── NodeJsEntryPointCollector.cs
├── Signals/
│   └── SurfaceSignalEmitter.cs               # + SurfaceSignalKeys
├── Output/
│   └── SurfaceAnalysisWriter.cs              # + SurfaceOutputOptions
└── TASKS.md

There is no README.mdin this project; status/notes live in TASKS.md.


Dependencies

The StellaOps.Scanner.Surface.csproj references exactly two sibling libraries (plus DI/Logging/Options abstractions):

Related libraries that are not referenced by this project (used elsewhere in Scanner, listed for context):



Changelog

VersionDateAuthorChanges
1.0.02025-12-05Scanner GuildInitial contract
1.1.02026-05-30Docs reconciliationReconciled against StellaOps.Scanner.Surface implementation: corrected SurfaceEntry/EntryPoint/SurfaceType models (sealed records, required, enum ConfidenceLevel); rewrote .FS/.Env/.Secrets integration to match real APIs (manifest writer, ISurfaceEnvironment, secret-provider framework — removed fabricated ISurfaceSecretScanner/STELLA_SURFACE_*); added missing signal keys (ExternalCalls, RiskScore, EntryPointCount); documented ISurfaceAnalyzer/options/risk score; fixed output JSON (camelCase + string enums); corrected determinism (SHA-256, not BLAKE3); listed built-in collectors + Node-only entry-point discovery; fixed project structure (no README; added Collectors) and dependencies (only .FS+.Env; no .Secrets/EntryTrace).