Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# StellaOps.Scanner.Analyzers.Lang.DotNet — Agent Charter
|
||||
|
||||
## Role
|
||||
Create the .NET analyzer plug-in that inspects `*.deps.json`, `runtimeconfig.json`, assemblies, and RID-specific assets to deliver accurate NuGet components with signing metadata.
|
||||
|
||||
## Scope
|
||||
- Parse dependency graphs from `*.deps.json` and merge with `runtimeconfig.json` and bundle manifests.
|
||||
- Capture assembly metadata (strong name, file version, Authenticode) and correlate with packages.
|
||||
- Handle RID-specific asset selection, self-contained apps, and crossgen/native dependency hints.
|
||||
- Package plug-in manifest, determinism fixtures, benchmarks, and Offline Kit documentation.
|
||||
|
||||
## Out of Scope
|
||||
- Policy evaluation or Signer integration (handled elsewhere).
|
||||
- Native dependency resolution outside RID mapping.
|
||||
- Windows-specific MSI/SxS analyzers (covered by native analyzer roadmap).
|
||||
|
||||
## Expectations
|
||||
- Performance target: multi-target app fixture <1.2 s, memory <250 MB.
|
||||
- Deterministic RID collapsing to reduce component duplication by ≥40 % vs naive approach.
|
||||
- Offline-first; support air-gapped strong-name/Authenticode validation using cached root store.
|
||||
- Rich telemetry (components per RID, strong-name validations) conforming to Scanner metrics.
|
||||
|
||||
## Dependencies
|
||||
- Shared language analyzer infrastructure; Worker dispatcher; optional security key store for signature verification.
|
||||
|
||||
## Testing & Artifacts
|
||||
- Fixtures for framework-dependent and self-contained apps (linux-musl, win-x64).
|
||||
- Golden outputs capturing signature metadata and RID grouping.
|
||||
- Benchmark comparing analyzer fidelity vs market competitors.
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Plugin;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet;
|
||||
|
||||
public sealed class DotNetAnalyzerPlugin : ILanguageAnalyzerPlugin
|
||||
{
|
||||
public string Name => "StellaOps.Scanner.Analyzers.Lang.DotNet";
|
||||
|
||||
public bool IsAvailable(IServiceProvider services) => services is not null;
|
||||
|
||||
public ILanguageAnalyzer CreateAnalyzer(IServiceProvider services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
return new DotNetLanguageAnalyzer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet.Internal;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet;
|
||||
|
||||
public sealed class DotNetLanguageAnalyzer : ILanguageAnalyzer
|
||||
{
|
||||
public string Id => "dotnet";
|
||||
|
||||
public string DisplayName => ".NET Analyzer (preview)";
|
||||
|
||||
public async ValueTask AnalyzeAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(writer);
|
||||
|
||||
var packages = await DotNetDependencyCollector.CollectAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
if (packages.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var package in packages)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
writer.AddFromPurl(
|
||||
analyzerId: Id,
|
||||
purl: package.Purl,
|
||||
name: package.Name,
|
||||
version: package.Version,
|
||||
type: "nuget",
|
||||
metadata: package.Metadata,
|
||||
evidence: package.Evidence,
|
||||
usedByEntrypoint: package.UsedByEntrypoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.IO;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
|
||||
global using StellaOps.Scanner.Analyzers.Lang;
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet;
|
||||
|
||||
public interface IDotNetAuthenticodeInspector
|
||||
{
|
||||
DotNetAuthenticodeMetadata? TryInspect(string assemblyPath, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed record DotNetAuthenticodeMetadata(
|
||||
string? Subject,
|
||||
string? Issuer,
|
||||
DateTimeOffset? NotBefore,
|
||||
DateTimeOffset? NotAfter,
|
||||
string? Thumbprint,
|
||||
string? SerialNumber);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,518 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Internal;
|
||||
|
||||
internal sealed class DotNetDepsFile
|
||||
{
|
||||
private DotNetDepsFile(string relativePath, IReadOnlyDictionary<string, DotNetLibrary> libraries)
|
||||
{
|
||||
RelativePath = relativePath;
|
||||
Libraries = libraries;
|
||||
}
|
||||
|
||||
public string RelativePath { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, DotNetLibrary> Libraries { get; }
|
||||
|
||||
public static DotNetDepsFile? Load(string absolutePath, string relativePath, CancellationToken cancellationToken)
|
||||
{
|
||||
using var stream = File.OpenRead(absolutePath);
|
||||
using var document = JsonDocument.Parse(stream, new JsonDocumentOptions
|
||||
{
|
||||
AllowTrailingCommas = true,
|
||||
CommentHandling = JsonCommentHandling.Skip
|
||||
});
|
||||
|
||||
var root = document.RootElement;
|
||||
if (root.ValueKind is not JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var libraries = ParseLibraries(root, cancellationToken);
|
||||
if (libraries.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
PopulateTargets(root, libraries, cancellationToken);
|
||||
return new DotNetDepsFile(relativePath, libraries);
|
||||
}
|
||||
|
||||
private static Dictionary<string, DotNetLibrary> ParseLibraries(JsonElement root, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new Dictionary<string, DotNetLibrary>(StringComparer.Ordinal);
|
||||
|
||||
if (!root.TryGetProperty("libraries", out var librariesElement) || librariesElement.ValueKind is not JsonValueKind.Object)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var property in librariesElement.EnumerateObject())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (DotNetLibrary.TryCreate(property.Name, property.Value, out var library))
|
||||
{
|
||||
result[property.Name] = library;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void PopulateTargets(JsonElement root, IDictionary<string, DotNetLibrary> libraries, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!root.TryGetProperty("targets", out var targetsElement) || targetsElement.ValueKind is not JsonValueKind.Object)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var targetProperty in targetsElement.EnumerateObject())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var (tfm, rid) = ParseTargetKey(targetProperty.Name);
|
||||
if (targetProperty.Value.ValueKind is not JsonValueKind.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var libraryProperty in targetProperty.Value.EnumerateObject())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (!libraries.TryGetValue(libraryProperty.Name, out var library))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(tfm))
|
||||
{
|
||||
library.AddTargetFramework(tfm);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(rid))
|
||||
{
|
||||
library.AddRuntimeIdentifier(rid);
|
||||
}
|
||||
|
||||
library.MergeTargetMetadata(libraryProperty.Value, tfm, rid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static (string tfm, string? rid) ParseTargetKey(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return (string.Empty, null);
|
||||
}
|
||||
|
||||
var separatorIndex = value.IndexOf('/');
|
||||
if (separatorIndex < 0)
|
||||
{
|
||||
return (value.Trim(), null);
|
||||
}
|
||||
|
||||
var tfm = value[..separatorIndex].Trim();
|
||||
var rid = value[(separatorIndex + 1)..].Trim();
|
||||
return (tfm, string.IsNullOrEmpty(rid) ? null : rid);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DotNetLibrary
|
||||
{
|
||||
private readonly HashSet<string> _dependencies = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<string> _runtimeIdentifiers = new(StringComparer.Ordinal);
|
||||
private readonly List<DotNetLibraryAsset> _runtimeAssets = new();
|
||||
private readonly HashSet<string> _targetFrameworks = new(StringComparer.Ordinal);
|
||||
|
||||
private DotNetLibrary(
|
||||
string key,
|
||||
string id,
|
||||
string version,
|
||||
string type,
|
||||
bool? serviceable,
|
||||
string? sha512,
|
||||
string? path,
|
||||
string? hashPath)
|
||||
{
|
||||
Key = key;
|
||||
Id = id;
|
||||
Version = version;
|
||||
Type = type;
|
||||
Serviceable = serviceable;
|
||||
Sha512 = NormalizeValue(sha512);
|
||||
PackagePath = NormalizePath(path);
|
||||
HashPath = NormalizePath(hashPath);
|
||||
}
|
||||
|
||||
public string Key { get; }
|
||||
|
||||
public string Id { get; }
|
||||
|
||||
public string Version { get; }
|
||||
|
||||
public string Type { get; }
|
||||
|
||||
public bool? Serviceable { get; }
|
||||
|
||||
public string? Sha512 { get; }
|
||||
|
||||
public string? PackagePath { get; }
|
||||
|
||||
public string? HashPath { get; }
|
||||
|
||||
public bool IsPackage => string.Equals(Type, "package", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public IReadOnlyCollection<string> Dependencies => _dependencies;
|
||||
|
||||
public IReadOnlyCollection<string> TargetFrameworks => _targetFrameworks;
|
||||
|
||||
public IReadOnlyCollection<string> RuntimeIdentifiers => _runtimeIdentifiers;
|
||||
|
||||
public IReadOnlyCollection<DotNetLibraryAsset> RuntimeAssets => _runtimeAssets;
|
||||
|
||||
public static bool TryCreate(string key, JsonElement element, [NotNullWhen(true)] out DotNetLibrary? library)
|
||||
{
|
||||
library = null;
|
||||
if (!TrySplitNameAndVersion(key, out var id, out var version))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var type = element.TryGetProperty("type", out var typeElement) && typeElement.ValueKind == JsonValueKind.String
|
||||
? typeElement.GetString() ?? string.Empty
|
||||
: string.Empty;
|
||||
|
||||
bool? serviceable = null;
|
||||
if (element.TryGetProperty("serviceable", out var serviceableElement))
|
||||
{
|
||||
if (serviceableElement.ValueKind is JsonValueKind.True)
|
||||
{
|
||||
serviceable = true;
|
||||
}
|
||||
else if (serviceableElement.ValueKind is JsonValueKind.False)
|
||||
{
|
||||
serviceable = false;
|
||||
}
|
||||
}
|
||||
|
||||
var sha512 = element.TryGetProperty("sha512", out var sha512Element) && sha512Element.ValueKind == JsonValueKind.String
|
||||
? sha512Element.GetString()
|
||||
: null;
|
||||
|
||||
var path = element.TryGetProperty("path", out var pathElement) && pathElement.ValueKind == JsonValueKind.String
|
||||
? pathElement.GetString()
|
||||
: null;
|
||||
|
||||
var hashPath = element.TryGetProperty("hashPath", out var hashElement) && hashElement.ValueKind == JsonValueKind.String
|
||||
? hashElement.GetString()
|
||||
: null;
|
||||
|
||||
library = new DotNetLibrary(key, id, version, type, serviceable, sha512, path, hashPath);
|
||||
library.MergeLibraryMetadata(element);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AddTargetFramework(string tfm)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(tfm))
|
||||
{
|
||||
_targetFrameworks.Add(tfm);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddRuntimeIdentifier(string rid)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(rid))
|
||||
{
|
||||
_runtimeIdentifiers.Add(rid);
|
||||
}
|
||||
}
|
||||
|
||||
public void MergeTargetMetadata(JsonElement element, string? tfm, string? rid)
|
||||
{
|
||||
if (element.TryGetProperty("dependencies", out var dependenciesElement) && dependenciesElement.ValueKind is JsonValueKind.Object)
|
||||
{
|
||||
foreach (var dependencyProperty in dependenciesElement.EnumerateObject())
|
||||
{
|
||||
AddDependency(dependencyProperty.Name);
|
||||
}
|
||||
}
|
||||
|
||||
MergeRuntimeAssets(element, tfm, rid);
|
||||
}
|
||||
|
||||
public void MergeLibraryMetadata(JsonElement element)
|
||||
{
|
||||
if (element.TryGetProperty("dependencies", out var dependenciesElement) && dependenciesElement.ValueKind is JsonValueKind.Object)
|
||||
{
|
||||
foreach (var dependencyProperty in dependenciesElement.EnumerateObject())
|
||||
{
|
||||
AddDependency(dependencyProperty.Name);
|
||||
}
|
||||
}
|
||||
|
||||
MergeRuntimeAssets(element, tfm: null, rid: null);
|
||||
}
|
||||
|
||||
private void MergeRuntimeAssets(JsonElement element, string? tfm, string? rid)
|
||||
{
|
||||
AddRuntimeAssetsFromRuntime(element, tfm, rid);
|
||||
AddRuntimeAssetsFromRuntimeTargets(element, tfm, rid);
|
||||
}
|
||||
|
||||
private void AddRuntimeAssetsFromRuntime(JsonElement element, string? tfm, string? rid)
|
||||
{
|
||||
if (!element.TryGetProperty("runtime", out var runtimeElement) || runtimeElement.ValueKind is not JsonValueKind.Object)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var assetProperty in runtimeElement.EnumerateObject())
|
||||
{
|
||||
if (DotNetLibraryAsset.TryCreateFromRuntime(assetProperty.Name, assetProperty.Value, tfm, rid, out var asset))
|
||||
{
|
||||
_runtimeAssets.Add(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddRuntimeAssetsFromRuntimeTargets(JsonElement element, string? tfm, string? rid)
|
||||
{
|
||||
if (!element.TryGetProperty("runtimeTargets", out var runtimeTargetsElement) || runtimeTargetsElement.ValueKind is not JsonValueKind.Object)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var assetProperty in runtimeTargetsElement.EnumerateObject())
|
||||
{
|
||||
if (DotNetLibraryAsset.TryCreateFromRuntimeTarget(assetProperty.Name, assetProperty.Value, tfm, rid, out var asset))
|
||||
{
|
||||
_runtimeAssets.Add(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddDependency(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dependencyId = name;
|
||||
if (TrySplitNameAndVersion(name, out var parsedName, out _))
|
||||
{
|
||||
dependencyId = parsedName;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(dependencyId))
|
||||
{
|
||||
_dependencies.Add(dependencyId);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TrySplitNameAndVersion(string key, out string name, out string version)
|
||||
{
|
||||
name = string.Empty;
|
||||
version = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var separatorIndex = key.LastIndexOf('/');
|
||||
if (separatorIndex <= 0 || separatorIndex >= key.Length - 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
name = key[..separatorIndex].Trim();
|
||||
version = key[(separatorIndex + 1)..].Trim();
|
||||
return name.Length > 0 && version.Length > 0;
|
||||
}
|
||||
|
||||
private static string? NormalizePath(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return path.Replace('\\', '/');
|
||||
}
|
||||
|
||||
private static string? NormalizeValue(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
internal enum DotNetLibraryAssetKind
|
||||
{
|
||||
Runtime,
|
||||
Native
|
||||
}
|
||||
|
||||
internal sealed record DotNetLibraryAsset(
|
||||
string RelativePath,
|
||||
string? TargetFramework,
|
||||
string? RuntimeIdentifier,
|
||||
string? AssemblyVersion,
|
||||
string? FileVersion,
|
||||
DotNetLibraryAssetKind Kind)
|
||||
{
|
||||
public static bool TryCreateFromRuntime(string name, JsonElement element, string? tfm, string? rid, [NotNullWhen(true)] out DotNetLibraryAsset? asset)
|
||||
{
|
||||
asset = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalizedPath = NormalizePath(name);
|
||||
if (string.IsNullOrEmpty(normalizedPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsManagedAssembly(normalizedPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string? assemblyVersion = null;
|
||||
string? fileVersion = null;
|
||||
|
||||
if (element.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (element.TryGetProperty("assemblyVersion", out var assemblyVersionElement) && assemblyVersionElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
assemblyVersion = NormalizeValue(assemblyVersionElement.GetString());
|
||||
}
|
||||
|
||||
if (element.TryGetProperty("fileVersion", out var fileVersionElement) && fileVersionElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
fileVersion = NormalizeValue(fileVersionElement.GetString());
|
||||
}
|
||||
}
|
||||
|
||||
asset = new DotNetLibraryAsset(
|
||||
RelativePath: normalizedPath,
|
||||
TargetFramework: NormalizeValue(tfm),
|
||||
RuntimeIdentifier: NormalizeValue(rid),
|
||||
AssemblyVersion: assemblyVersion,
|
||||
FileVersion: fileVersion,
|
||||
Kind: DotNetLibraryAssetKind.Runtime);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryCreateFromRuntimeTarget(string name, JsonElement element, string? tfm, string? rid, [NotNullWhen(true)] out DotNetLibraryAsset? asset)
|
||||
{
|
||||
asset = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name) || element.ValueKind is not JsonValueKind.Object)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var assetType = element.TryGetProperty("assetType", out var assetTypeElement) && assetTypeElement.ValueKind == JsonValueKind.String
|
||||
? NormalizeValue(assetTypeElement.GetString())
|
||||
: null;
|
||||
|
||||
var normalizedPath = NormalizePath(name);
|
||||
if (string.IsNullOrEmpty(normalizedPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DotNetLibraryAssetKind kind;
|
||||
if (assetType is null || string.Equals(assetType, "runtime", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!IsManagedAssembly(normalizedPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
kind = DotNetLibraryAssetKind.Runtime;
|
||||
}
|
||||
else if (string.Equals(assetType, "native", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
kind = DotNetLibraryAssetKind.Native;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string? assemblyVersion = null;
|
||||
string? fileVersion = null;
|
||||
|
||||
if (kind == DotNetLibraryAssetKind.Runtime &&
|
||||
element.TryGetProperty("assemblyVersion", out var assemblyVersionElement) &&
|
||||
assemblyVersionElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
assemblyVersion = NormalizeValue(assemblyVersionElement.GetString());
|
||||
}
|
||||
|
||||
if (kind == DotNetLibraryAssetKind.Runtime &&
|
||||
element.TryGetProperty("fileVersion", out var fileVersionElement) &&
|
||||
fileVersionElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
fileVersion = NormalizeValue(fileVersionElement.GetString());
|
||||
}
|
||||
|
||||
string? runtimeIdentifier = rid;
|
||||
if (element.TryGetProperty("rid", out var ridElement) && ridElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
runtimeIdentifier = NormalizeValue(ridElement.GetString());
|
||||
}
|
||||
|
||||
asset = new DotNetLibraryAsset(
|
||||
RelativePath: normalizedPath,
|
||||
TargetFramework: NormalizeValue(tfm),
|
||||
RuntimeIdentifier: NormalizeValue(runtimeIdentifier),
|
||||
AssemblyVersion: assemblyVersion,
|
||||
FileVersion: fileVersion,
|
||||
Kind: kind);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string NormalizePath(string value)
|
||||
{
|
||||
var normalized = NormalizeValue(value);
|
||||
if (string.IsNullOrEmpty(normalized))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return normalized.Replace('\\', '/');
|
||||
}
|
||||
|
||||
private static bool IsManagedAssembly(string path)
|
||||
=> path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string? NormalizeValue(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.Trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Security;
|
||||
using System.Xml;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Internal;
|
||||
|
||||
internal static class DotNetFileMetadataCache
|
||||
{
|
||||
private static readonly ConcurrentDictionary<DotNetFileCacheKey, Optional<string>> Sha256Cache = new();
|
||||
private static readonly ConcurrentDictionary<DotNetFileCacheKey, Optional<AssemblyName>> AssemblyCache = new();
|
||||
private static readonly ConcurrentDictionary<DotNetFileCacheKey, Optional<FileVersionInfo>> VersionCache = new();
|
||||
|
||||
public static bool TryGetSha256(string path, out string? sha256)
|
||||
=> TryGet(path, Sha256Cache, ComputeSha256, out sha256);
|
||||
|
||||
public static bool TryGetAssemblyName(string path, out AssemblyName? assemblyName)
|
||||
=> TryGet(path, AssemblyCache, TryReadAssemblyName, out assemblyName);
|
||||
|
||||
public static bool TryGetFileVersionInfo(string path, out FileVersionInfo? versionInfo)
|
||||
=> TryGet(path, VersionCache, TryReadFileVersionInfo, out versionInfo);
|
||||
|
||||
private static bool TryGet<T>(string path, ConcurrentDictionary<DotNetFileCacheKey, Optional<T>> cache, Func<string, T?> resolver, out T? value)
|
||||
where T : class
|
||||
{
|
||||
value = null;
|
||||
|
||||
DotNetFileCacheKey key;
|
||||
try
|
||||
{
|
||||
var info = new FileInfo(path);
|
||||
if (!info.Exists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
key = new DotNetFileCacheKey(info.FullName, info.Length, info.LastWriteTimeUtc.Ticks);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (SecurityException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var optional = cache.GetOrAdd(key, static (cacheKey, state) => CreateOptional(cacheKey.Path, state.resolver), (resolver, path));
|
||||
if (!optional.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = optional.Value;
|
||||
return value is not null;
|
||||
}
|
||||
|
||||
private static Optional<T> CreateOptional<T>(string path, Func<string, T?> resolver) where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = resolver(path);
|
||||
return Optional<T>.From(value);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return Optional<T>.None;
|
||||
}
|
||||
catch (FileLoadException)
|
||||
{
|
||||
return Optional<T>.None;
|
||||
}
|
||||
catch (BadImageFormatException)
|
||||
{
|
||||
return Optional<T>.None;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Optional<T>.None;
|
||||
}
|
||||
catch (SecurityException)
|
||||
{
|
||||
return Optional<T>.None;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return Optional<T>.None;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ComputeSha256(string path)
|
||||
{
|
||||
using var stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
using var sha = System.Security.Cryptography.SHA256.Create();
|
||||
var hash = sha.ComputeHash(stream);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static AssemblyName? TryReadAssemblyName(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return AssemblyName.GetAssemblyName(path);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (FileLoadException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (BadImageFormatException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static FileVersionInfo? TryReadFileVersionInfo(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return FileVersionInfo.GetVersionInfo(path);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static class DotNetLicenseCache
|
||||
{
|
||||
private static readonly ConcurrentDictionary<DotNetFileCacheKey, Optional<DotNetLicenseInfo>> Licenses = new();
|
||||
|
||||
public static bool TryGetLicenseInfo(string nuspecPath, out DotNetLicenseInfo? info)
|
||||
{
|
||||
info = null;
|
||||
|
||||
DotNetFileCacheKey key;
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(nuspecPath);
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
key = new DotNetFileCacheKey(fileInfo.FullName, fileInfo.Length, fileInfo.LastWriteTimeUtc.Ticks);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (SecurityException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var optional = Licenses.GetOrAdd(key, static (cacheKey, path) => CreateOptional(path), nuspecPath);
|
||||
if (!optional.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
info = optional.Value;
|
||||
return info is not null;
|
||||
}
|
||||
|
||||
private static Optional<DotNetLicenseInfo> CreateOptional(string nuspecPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = Parse(nuspecPath);
|
||||
return Optional<DotNetLicenseInfo>.From(info);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return Optional<DotNetLicenseInfo>.None;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Optional<DotNetLicenseInfo>.None;
|
||||
}
|
||||
catch (SecurityException)
|
||||
{
|
||||
return Optional<DotNetLicenseInfo>.None;
|
||||
}
|
||||
catch (XmlException)
|
||||
{
|
||||
return Optional<DotNetLicenseInfo>.None;
|
||||
}
|
||||
}
|
||||
|
||||
private static DotNetLicenseInfo? Parse(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
using var reader = XmlReader.Create(stream, new XmlReaderSettings
|
||||
{
|
||||
DtdProcessing = DtdProcessing.Ignore,
|
||||
IgnoreComments = true,
|
||||
IgnoreWhitespace = true,
|
||||
});
|
||||
|
||||
var expressions = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var files = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var urls = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(reader.LocalName, "license", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var type = reader.GetAttribute("type");
|
||||
var value = reader.ReadElementContentAsString()?.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(type, "expression", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
expressions.Add(value);
|
||||
}
|
||||
else if (string.Equals(type, "file", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
files.Add(NormalizeLicensePath(value));
|
||||
}
|
||||
else
|
||||
{
|
||||
expressions.Add(value);
|
||||
}
|
||||
}
|
||||
else if (string.Equals(reader.LocalName, "licenseUrl", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = reader.ReadElementContentAsString()?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
urls.Add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expressions.Count == 0 && files.Count == 0 && urls.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DotNetLicenseInfo(
|
||||
expressions.ToArray(),
|
||||
files.ToArray(),
|
||||
urls.ToArray());
|
||||
}
|
||||
|
||||
private static string NormalizeLicensePath(string value)
|
||||
=> value.Replace('\\', '/').Trim();
|
||||
}
|
||||
|
||||
internal sealed record DotNetLicenseInfo(
|
||||
IReadOnlyList<string> Expressions,
|
||||
IReadOnlyList<string> Files,
|
||||
IReadOnlyList<string> Urls);
|
||||
|
||||
internal readonly record struct DotNetFileCacheKey(string Path, long Length, long LastWriteTicks)
|
||||
{
|
||||
private readonly string _normalizedPath = OperatingSystem.IsWindows()
|
||||
? Path.ToLowerInvariant()
|
||||
: Path;
|
||||
|
||||
public bool Equals(DotNetFileCacheKey other)
|
||||
=> Length == other.Length
|
||||
&& LastWriteTicks == other.LastWriteTicks
|
||||
&& string.Equals(_normalizedPath, other._normalizedPath, StringComparison.Ordinal);
|
||||
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(_normalizedPath, Length, LastWriteTicks);
|
||||
}
|
||||
|
||||
internal readonly struct Optional<T> where T : class
|
||||
{
|
||||
private Optional(bool hasValue, T? value)
|
||||
{
|
||||
HasValue = hasValue;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public bool HasValue { get; }
|
||||
|
||||
public T? Value { get; }
|
||||
|
||||
public static Optional<T> From(T? value)
|
||||
=> value is null ? None : new Optional<T>(true, value);
|
||||
|
||||
public static Optional<T> None => default;
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.DotNet.Internal;
|
||||
|
||||
internal sealed class DotNetRuntimeConfig
|
||||
{
|
||||
private DotNetRuntimeConfig(
|
||||
string relativePath,
|
||||
IReadOnlyCollection<string> tfms,
|
||||
IReadOnlyCollection<string> frameworks,
|
||||
IReadOnlyCollection<RuntimeGraphEntry> runtimeGraph)
|
||||
{
|
||||
RelativePath = relativePath;
|
||||
Tfms = tfms;
|
||||
Frameworks = frameworks;
|
||||
RuntimeGraph = runtimeGraph;
|
||||
}
|
||||
|
||||
public string RelativePath { get; }
|
||||
|
||||
public IReadOnlyCollection<string> Tfms { get; }
|
||||
|
||||
public IReadOnlyCollection<string> Frameworks { get; }
|
||||
|
||||
public IReadOnlyCollection<RuntimeGraphEntry> RuntimeGraph { get; }
|
||||
|
||||
public static DotNetRuntimeConfig? Load(string absolutePath, string relativePath, CancellationToken cancellationToken)
|
||||
{
|
||||
using var stream = File.OpenRead(absolutePath);
|
||||
using var document = JsonDocument.Parse(stream, new JsonDocumentOptions
|
||||
{
|
||||
AllowTrailingCommas = true,
|
||||
CommentHandling = JsonCommentHandling.Skip
|
||||
});
|
||||
|
||||
var root = document.RootElement;
|
||||
if (!root.TryGetProperty("runtimeOptions", out var runtimeOptions) || runtimeOptions.ValueKind is not JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var tfms = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var frameworks = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var runtimeGraph = new List<RuntimeGraphEntry>();
|
||||
|
||||
if (runtimeOptions.TryGetProperty("tfm", out var tfmElement) && tfmElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
AddIfPresent(tfms, tfmElement.GetString());
|
||||
}
|
||||
|
||||
if (runtimeOptions.TryGetProperty("framework", out var frameworkElement) && frameworkElement.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
var frameworkId = FormatFramework(frameworkElement);
|
||||
AddIfPresent(frameworks, frameworkId);
|
||||
}
|
||||
|
||||
if (runtimeOptions.TryGetProperty("frameworks", out var frameworksElement) && frameworksElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var item in frameworksElement.EnumerateArray())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var frameworkId = FormatFramework(item);
|
||||
AddIfPresent(frameworks, frameworkId);
|
||||
}
|
||||
}
|
||||
|
||||
if (runtimeOptions.TryGetProperty("includedFrameworks", out var includedElement) && includedElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var item in includedElement.EnumerateArray())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var frameworkId = FormatFramework(item);
|
||||
AddIfPresent(frameworks, frameworkId);
|
||||
}
|
||||
}
|
||||
|
||||
if (runtimeOptions.TryGetProperty("runtimeGraph", out var runtimeGraphElement) &&
|
||||
runtimeGraphElement.ValueKind == JsonValueKind.Object &&
|
||||
runtimeGraphElement.TryGetProperty("runtimes", out var runtimesElement) &&
|
||||
runtimesElement.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var ridProperty in runtimesElement.EnumerateObject())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ridProperty.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fallbacks = new List<string>();
|
||||
if (ridProperty.Value.ValueKind == JsonValueKind.Object &&
|
||||
ridProperty.Value.TryGetProperty("fallbacks", out var fallbacksElement) &&
|
||||
fallbacksElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var fallback in fallbacksElement.EnumerateArray())
|
||||
{
|
||||
if (fallback.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var fallbackValue = fallback.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(fallbackValue))
|
||||
{
|
||||
fallbacks.Add(fallbackValue.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runtimeGraph.Add(new RuntimeGraphEntry(ridProperty.Name.Trim(), fallbacks));
|
||||
}
|
||||
}
|
||||
|
||||
return new DotNetRuntimeConfig(
|
||||
relativePath,
|
||||
tfms.ToArray(),
|
||||
frameworks.ToArray(),
|
||||
runtimeGraph);
|
||||
}
|
||||
|
||||
private static void AddIfPresent(ISet<string> set, string? value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
set.Add(value.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
private static string? FormatFramework(JsonElement element)
|
||||
{
|
||||
if (element.ValueKind is not JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var name = element.TryGetProperty("name", out var nameElement) && nameElement.ValueKind == JsonValueKind.String
|
||||
? nameElement.GetString()
|
||||
: null;
|
||||
|
||||
var version = element.TryGetProperty("version", out var versionElement) && versionElement.ValueKind == JsonValueKind.String
|
||||
? versionElement.GetString()
|
||||
: null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
return name.Trim();
|
||||
}
|
||||
|
||||
return $"{name.Trim()}@{version.Trim()}";
|
||||
}
|
||||
|
||||
internal sealed record RuntimeGraphEntry(string Rid, IReadOnlyList<string> Fallbacks);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<EnableDefaultItems>false</EnableDefaultItems>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="**\\*.cs" Exclude="obj\\**;bin\\**" />
|
||||
<EmbeddedResource Include="**\\*.json" Exclude="obj\\**;bin\\**" />
|
||||
<None Include="**\\*" Exclude="**\\*.cs;**\\*.json;bin\\**;obj\\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang\StellaOps.Scanner.Analyzers.Lang.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,19 @@
|
||||
# .NET Analyzer Task Flow
|
||||
|
||||
| Seq | ID | Status | Depends on | Description | Exit Criteria |
|
||||
|-----|----|--------|------------|-------------|---------------|
|
||||
| 1 | SCANNER-ANALYZERS-LANG-10-305A | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-307 | Parse `*.deps.json` + `runtimeconfig.json`, build RID graph, and normalize to `pkg:nuget` components. | RID graph deterministic; fixtures confirm consistent component ordering; fallback to `bin:{sha256}` documented. |
|
||||
| 2 | SCANNER-ANALYZERS-LANG-10-305B | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-305A | Extract assembly metadata (strong name, file/product info) and optional Authenticode details when offline cert bundle provided. | Signing metadata captured for signed assemblies; offline trust store documented; hash validations deterministic. |
|
||||
| 3 | SCANNER-ANALYZERS-LANG-10-305C | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-305B | Handle self-contained apps and native assets; merge with EntryTrace usage hints. | Self-contained fixtures map to components with RID flags; usage hints propagate; tests cover linux/win variants. |
|
||||
| 4 | SCANNER-ANALYZERS-LANG-10-307D | DONE (2025-10-22) | SCANNER-ANALYZERS-LANG-10-305C | Integrate shared helpers (license mapping, quiet provenance) and concurrency-safe caches. | Shared helpers reused; concurrency tests for parallel layer scans pass; no redundant allocations. |
|
||||
| 5 | SCANNER-ANALYZERS-LANG-10-308D | DONE (2025-10-23) | SCANNER-ANALYZERS-LANG-10-307D | Determinism fixtures + benchmark harness; compare to competitor scanners for accuracy/perf. | Fixtures in `Fixtures/lang/dotnet/`; determinism CI guard; benchmark demonstrates lower duplication + faster runtime. |
|
||||
| 6 | SCANNER-ANALYZERS-LANG-10-309D | DONE (2025-10-23) | SCANNER-ANALYZERS-LANG-10-308D | Package plug-in (manifest, DI registration) and update Offline Kit instructions. | Manifest copied to `plugins/scanner/analyzers/lang/`; Worker loads analyzer; Offline Kit doc updated. |
|
||||
|
||||
## .NET Entry-Point & Dependency Resolver (Sprint 11)
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| SCANNER-ANALYZERS-LANG-11-001 | TODO | StellaOps.Scanner EPDR Guild, Language Analyzer Guild | - | Build entrypoint resolver that maps project/publish artifacts to entrypoint identities (assembly name, MVID, TFM, RID) and environment profiles (publish mode, host kind, probing paths). Output normalized `entrypoints[]` records with deterministic IDs. | Entrypoint records produced for fixtures (framework-dependent, self-contained, single-file, multi-TFM/RID); determinism check passes; docs updated. |
|
||||
| SCANNER-ANALYZERS-LANG-11-002 | TODO | StellaOps.Scanner EPDR Guild | SCANNER-ANALYZERS-LANG-11-001 | Implement static analyzer (IL + reflection heuristics) capturing AssemblyRef, ModuleRef/PInvoke, DynamicDependency, reflection literals, DI patterns, and custom AssemblyLoadContext probing hints. Emit dependency edges with reason codes and confidence. | Static analysis coverage demonstrated on fixtures; edges carry reason codes (`il-assemblyref`, `il-moduleref`, `reflection-literal`, `alc-probing`); tests cover trimmed/single-file cases. |
|
||||
| SCANNER-ANALYZERS-LANG-11-003 | TODO | StellaOps.Scanner EPDR Guild, Signals Guild | SCANNER-ANALYZERS-LANG-11-002 | Ingest optional runtime evidence (AssemblyLoad, Resolving, P/Invoke) via event listener harness; merge runtime edges with static/declared ones and attach reason codes/confidence. | Runtime listener service pluggable; fixtures record runtime edges; merged output shows combined reason set with confidence per edge. |
|
||||
| SCANNER-ANALYZERS-LANG-11-004 | TODO | StellaOps.Scanner EPDR Guild, SBOM Service Guild | SCANNER-ANALYZERS-LANG-11-002 | Produce normalized observation export to Scanner writer: entrypoints + dependency edges + environment profiles (AOC compliant). Wire to SBOM service entrypoint tagging. | Analyzer writes observation records consumed by SBOM service tests; AOC compliance docs updated; determinism checked. |
|
||||
| SCANNER-ANALYZERS-LANG-11-005 | TODO | StellaOps.Scanner EPDR Guild, QA Guild | SCANNER-ANALYZERS-LANG-11-004 | Add comprehensive fixtures/benchmarks covering framework-dependent, self-contained, single-file, trimmed, NativeAOT, multi-RID scenarios; include explain traces and perf benchmarks vs previous analyzer. | Fixtures stored under `fixtures/lang/dotnet/epdr`; determinism + perf thresholds validated; benchmark results documented. |
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"schemaVersion": "1.0",
|
||||
"id": "stellaops.analyzer.lang.dotnet",
|
||||
"displayName": "StellaOps .NET Analyzer (preview)",
|
||||
"version": "0.1.0",
|
||||
"requiresRestart": true,
|
||||
"entryPoint": {
|
||||
"type": "dotnet",
|
||||
"assembly": "StellaOps.Scanner.Analyzers.Lang.DotNet.dll",
|
||||
"typeName": "StellaOps.Scanner.Analyzers.Lang.DotNet.DotNetAnalyzerPlugin"
|
||||
},
|
||||
"capabilities": [
|
||||
"language-analyzer",
|
||||
"dotnet",
|
||||
"nuget"
|
||||
],
|
||||
"metadata": {
|
||||
"org.stellaops.analyzer.language": "dotnet",
|
||||
"org.stellaops.analyzer.kind": "language",
|
||||
"org.stellaops.restart.required": "true",
|
||||
"org.stellaops.analyzer.status": "preview"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user