Add unit tests for SBOM ingestion and transformation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly. - Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps. - Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges. - Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges. - Set up project file for the test project with necessary dependencies and configurations. - Include JSON fixture files for testing purposes.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| SCAN-REPLAY-186-001 | TODO | Scanner WebService Guild | REPLAY-CORE-185-001 | Implement scan `record` mode producing replay manifests/bundles, capture policy/feed/tool hashes, and update `docs/modules/scanner/architecture.md` referencing `docs/replay/DETERMINISTIC_REPLAY.md` Section 6. | API/worker integration tests cover record mode; docs merged; replay artifacts stored per spec. |
|
||||
| SCANNER-SURFACE-02 | DOING (2025-11-02) | Scanner WebService Guild | SURFACE-FS-02 | Publish Surface.FS pointers (CAS URIs, manifests) via scan/report APIs and update attestation metadata.<br>2025-11-02: Scan/report API responses now include preview CAS URIs; attestation metadata draft published. | OpenAPI updated; clients regenerated; integration tests validate pointer presence and tenancy. |
|
||||
| SCANNER-ENV-02 | DOING (2025-11-02) | Scanner WebService Guild, Ops Guild | SURFACE-ENV-02 | Wire Surface.Env helpers into WebService hosting (cache roots, feature flags) and document configuration.<br>2025-11-02: Cache root resolution switched to helper; feature flag bindings updated; Helm/Compose updates pending review. | Service uses helper; env table documented; helm/compose templates updated. |
|
||||
| SCANNER-SECRETS-02 | DOING (2025-11-02) | Scanner WebService Guild, Security Guild | SURFACE-SECRETS-02 | Replace ad-hoc secret wiring with Surface.Secrets for report/export operations (registry and CAS tokens).<br>2025-11-02: Export/report flows now depend on Surface.Secrets stub; integration tests in progress. | Secrets fetched through shared provider; unit/integration tests cover rotation + failure cases. |
|
||||
|
||||
@@ -10,8 +10,10 @@ public sealed class ScannerWorkerMetrics
|
||||
private readonly Histogram<double> _queueLatencyMs;
|
||||
private readonly Histogram<double> _jobDurationMs;
|
||||
private readonly Histogram<double> _stageDurationMs;
|
||||
private readonly Counter<long> _jobsCompleted;
|
||||
private readonly Counter<long> _jobsFailed;
|
||||
private readonly Counter<long> _jobsCompleted;
|
||||
private readonly Counter<long> _jobsFailed;
|
||||
private readonly Counter<long> _languageCacheHits;
|
||||
private readonly Counter<long> _languageCacheMisses;
|
||||
|
||||
public ScannerWorkerMetrics()
|
||||
{
|
||||
@@ -27,12 +29,18 @@ public sealed class ScannerWorkerMetrics
|
||||
"scanner_worker_stage_duration_ms",
|
||||
unit: "ms",
|
||||
description: "Stage execution duration per job.");
|
||||
_jobsCompleted = ScannerWorkerInstrumentation.Meter.CreateCounter<long>(
|
||||
"scanner_worker_jobs_completed_total",
|
||||
description: "Number of successfully completed scan jobs.");
|
||||
_jobsFailed = ScannerWorkerInstrumentation.Meter.CreateCounter<long>(
|
||||
"scanner_worker_jobs_failed_total",
|
||||
description: "Number of scan jobs that failed permanently.");
|
||||
_jobsCompleted = ScannerWorkerInstrumentation.Meter.CreateCounter<long>(
|
||||
"scanner_worker_jobs_completed_total",
|
||||
description: "Number of successfully completed scan jobs.");
|
||||
_jobsFailed = ScannerWorkerInstrumentation.Meter.CreateCounter<long>(
|
||||
"scanner_worker_jobs_failed_total",
|
||||
description: "Number of scan jobs that failed permanently.");
|
||||
_languageCacheHits = ScannerWorkerInstrumentation.Meter.CreateCounter<long>(
|
||||
"scanner_worker_language_cache_hits_total",
|
||||
description: "Number of language analyzer cache hits encountered by the worker.");
|
||||
_languageCacheMisses = ScannerWorkerInstrumentation.Meter.CreateCounter<long>(
|
||||
"scanner_worker_language_cache_misses_total",
|
||||
description: "Number of language analyzer cache misses encountered by the worker.");
|
||||
}
|
||||
|
||||
public void RecordQueueLatency(ScanJobContext context, TimeSpan latency)
|
||||
@@ -70,19 +78,29 @@ public sealed class ScannerWorkerMetrics
|
||||
_jobsCompleted.Add(1, CreateTags(context));
|
||||
}
|
||||
|
||||
public void IncrementJobFailed(ScanJobContext context, string failureReason)
|
||||
{
|
||||
_jobsFailed.Add(1, CreateTags(context, failureReason: failureReason));
|
||||
}
|
||||
|
||||
private static KeyValuePair<string, object?>[] CreateTags(ScanJobContext context, string? stage = null, string? failureReason = null)
|
||||
{
|
||||
var tags = new List<KeyValuePair<string, object?>>(stage is null ? 5 : 6)
|
||||
{
|
||||
new("job.id", context.JobId),
|
||||
new("scan.id", context.ScanId),
|
||||
new("attempt", context.Lease.Attempt),
|
||||
};
|
||||
public void IncrementJobFailed(ScanJobContext context, string failureReason)
|
||||
{
|
||||
_jobsFailed.Add(1, CreateTags(context, failureReason: failureReason));
|
||||
}
|
||||
|
||||
public void RecordLanguageCacheHit(ScanJobContext context, string analyzerId)
|
||||
{
|
||||
_languageCacheHits.Add(1, CreateTags(context, analyzerId: analyzerId));
|
||||
}
|
||||
|
||||
public void RecordLanguageCacheMiss(ScanJobContext context, string analyzerId)
|
||||
{
|
||||
_languageCacheMisses.Add(1, CreateTags(context, analyzerId: analyzerId));
|
||||
}
|
||||
|
||||
private static KeyValuePair<string, object?>[] CreateTags(ScanJobContext context, string? stage = null, string? failureReason = null, string? analyzerId = null)
|
||||
{
|
||||
var tags = new List<KeyValuePair<string, object?>>(stage is null ? 5 : 6)
|
||||
{
|
||||
new("job.id", context.JobId),
|
||||
new("scan.id", context.ScanId),
|
||||
new("attempt", context.Lease.Attempt),
|
||||
};
|
||||
|
||||
if (context.Lease.Metadata.TryGetValue("queue", out var queueName) && !string.IsNullOrWhiteSpace(queueName))
|
||||
{
|
||||
@@ -99,11 +117,16 @@ public sealed class ScannerWorkerMetrics
|
||||
tags.Add(new KeyValuePair<string, object?>("stage", stage));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(failureReason))
|
||||
{
|
||||
tags.Add(new KeyValuePair<string, object?>("reason", failureReason));
|
||||
}
|
||||
|
||||
return tags.ToArray();
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(failureReason))
|
||||
{
|
||||
tags.Add(new KeyValuePair<string, object?>("reason", failureReason));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(analyzerId))
|
||||
{
|
||||
tags.Add(new KeyValuePair<string, object?>("analyzer.id", analyzerId));
|
||||
}
|
||||
|
||||
return tags.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,44 +2,53 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Plugin;
|
||||
using StellaOps.Scanner.Analyzers.OS;
|
||||
using StellaOps.Scanner.Analyzers.OS.Abstractions;
|
||||
using StellaOps.Scanner.Analyzers.OS.Mapping;
|
||||
using StellaOps.Scanner.Analyzers.OS.Plugin;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Worker.Options;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Internal;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Plugin;
|
||||
using StellaOps.Scanner.Analyzers.OS;
|
||||
using StellaOps.Scanner.Analyzers.OS.Abstractions;
|
||||
using StellaOps.Scanner.Analyzers.OS.Mapping;
|
||||
using StellaOps.Scanner.Analyzers.OS.Plugin;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
using StellaOps.Scanner.Surface.Validation;
|
||||
using StellaOps.Scanner.Worker.Options;
|
||||
|
||||
namespace StellaOps.Scanner.Worker.Processing;
|
||||
|
||||
internal sealed class CompositeScanAnalyzerDispatcher : IScanAnalyzerDispatcher
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IOSAnalyzerPluginCatalog _osCatalog;
|
||||
private readonly ILanguageAnalyzerPluginCatalog _languageCatalog;
|
||||
private readonly ScannerWorkerOptions _options;
|
||||
private readonly ILogger<CompositeScanAnalyzerDispatcher> _logger;
|
||||
internal sealed class CompositeScanAnalyzerDispatcher : IScanAnalyzerDispatcher
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IOSAnalyzerPluginCatalog _osCatalog;
|
||||
private readonly ILanguageAnalyzerPluginCatalog _languageCatalog;
|
||||
private readonly ScannerWorkerOptions _options;
|
||||
private readonly ILogger<CompositeScanAnalyzerDispatcher> _logger;
|
||||
private readonly ScannerWorkerMetrics _metrics;
|
||||
private IReadOnlyList<string> _osPluginDirectories = Array.Empty<string>();
|
||||
private IReadOnlyList<string> _languagePluginDirectories = Array.Empty<string>();
|
||||
|
||||
public CompositeScanAnalyzerDispatcher(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOSAnalyzerPluginCatalog osCatalog,
|
||||
ILanguageAnalyzerPluginCatalog languageCatalog,
|
||||
IOptions<ScannerWorkerOptions> options,
|
||||
ILogger<CompositeScanAnalyzerDispatcher> logger)
|
||||
{
|
||||
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
|
||||
_osCatalog = osCatalog ?? throw new ArgumentNullException(nameof(osCatalog));
|
||||
_languageCatalog = languageCatalog ?? throw new ArgumentNullException(nameof(languageCatalog));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
ILanguageAnalyzerPluginCatalog languageCatalog,
|
||||
IOptions<ScannerWorkerOptions> options,
|
||||
ILogger<CompositeScanAnalyzerDispatcher> logger,
|
||||
ScannerWorkerMetrics metrics)
|
||||
{
|
||||
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
|
||||
_osCatalog = osCatalog ?? throw new ArgumentNullException(nameof(osCatalog));
|
||||
_languageCatalog = languageCatalog ?? throw new ArgumentNullException(nameof(languageCatalog));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
|
||||
|
||||
LoadPlugins();
|
||||
}
|
||||
@@ -131,72 +140,126 @@ internal sealed class CompositeScanAnalyzerDispatcher : IScanAnalyzerDispatcher
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteLanguageAnalyzersAsync(
|
||||
ScanJobContext context,
|
||||
IReadOnlyList<ILanguageAnalyzer> analyzers,
|
||||
IServiceProvider services,
|
||||
string? workspacePath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (workspacePath is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Metadata key '{MetadataKey}' missing for job {JobId}; unable to locate workspace. Language analyzers skipped.",
|
||||
_options.Analyzers.WorkspaceMetadataKey,
|
||||
context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
var usageHints = LanguageUsageHints.Empty;
|
||||
var analyzerContext = new LanguageAnalyzerContext(workspacePath, context.TimeProvider, usageHints, services);
|
||||
var results = new Dictionary<string, LanguageAnalyzerResult>(StringComparer.OrdinalIgnoreCase);
|
||||
var fragments = new List<LayerComponentFragment>();
|
||||
|
||||
foreach (var analyzer in analyzers)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
var engine = new LanguageAnalyzerEngine(new[] { analyzer });
|
||||
var result = await engine.AnalyzeAsync(analyzerContext, cancellationToken).ConfigureAwait(false);
|
||||
results[analyzer.Id] = result;
|
||||
|
||||
var components = result.Components
|
||||
.Where(component => string.Equals(component.AnalyzerId, analyzer.Id, StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
|
||||
if (components.Length > 0)
|
||||
{
|
||||
var fragment = LanguageComponentMapper.ToLayerFragment(analyzer.Id, components);
|
||||
fragments.Add(fragment);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Language analyzer {AnalyzerId} failed for job {JobId}.", analyzer.Id, context.JobId);
|
||||
}
|
||||
}
|
||||
|
||||
if (results.Count == 0 && fragments.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.Count > 0)
|
||||
{
|
||||
context.Analysis.Set(
|
||||
ScanAnalysisKeys.LanguageAnalyzerResults,
|
||||
new ReadOnlyDictionary<string, LanguageAnalyzerResult>(results));
|
||||
}
|
||||
|
||||
if (fragments.Count > 0)
|
||||
{
|
||||
var immutableFragments = ImmutableArray.CreateRange(fragments);
|
||||
context.Analysis.AppendLayerFragments(immutableFragments);
|
||||
context.Analysis.Set(ScanAnalysisKeys.LanguageComponentFragments, immutableFragments);
|
||||
}
|
||||
}
|
||||
private async Task ExecuteLanguageAnalyzersAsync(
|
||||
ScanJobContext context,
|
||||
IReadOnlyList<ILanguageAnalyzer> analyzers,
|
||||
IServiceProvider services,
|
||||
string? workspacePath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (workspacePath is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Metadata key '{MetadataKey}' missing for job {JobId}; unable to locate workspace. Language analyzers skipped.",
|
||||
_options.Analyzers.WorkspaceMetadataKey,
|
||||
context.JobId);
|
||||
return;
|
||||
}
|
||||
|
||||
var surfaceEnvironment = services.GetRequiredService<ISurfaceEnvironment>();
|
||||
var validatorRunner = services.GetRequiredService<ISurfaceValidatorRunner>();
|
||||
|
||||
var validationProperties = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["jobId"] = context.JobId,
|
||||
["scanId"] = context.ScanId,
|
||||
["workspacePath"] = workspacePath,
|
||||
["analyzerCount"] = analyzers.Count
|
||||
};
|
||||
|
||||
var validationContext = SurfaceValidationContext.Create(
|
||||
services,
|
||||
"Scanner.Worker.LanguageAnalyzers",
|
||||
surfaceEnvironment.Settings,
|
||||
validationProperties);
|
||||
|
||||
await validatorRunner.EnsureAsync(validationContext, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
string workspaceFingerprint;
|
||||
try
|
||||
{
|
||||
workspaceFingerprint = LanguageWorkspaceFingerprint.Compute(workspacePath, cancellationToken);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Failed to compute workspace fingerprint for job {JobId}; falling back to workspace path hash.",
|
||||
context.JobId);
|
||||
|
||||
var fallbackBytes = Encoding.UTF8.GetBytes(workspacePath);
|
||||
workspaceFingerprint = Convert.ToHexString(SHA256.HashData(fallbackBytes)).ToLowerInvariant();
|
||||
}
|
||||
|
||||
var cache = services.GetRequiredService<ISurfaceCache>();
|
||||
var cacheAdapter = new LanguageAnalyzerSurfaceCache(cache, surfaceEnvironment.Settings.Tenant);
|
||||
|
||||
var usageHints = LanguageUsageHints.Empty;
|
||||
var analyzerContext = new LanguageAnalyzerContext(workspacePath, context.TimeProvider, usageHints, services);
|
||||
var results = new Dictionary<string, LanguageAnalyzerResult>(StringComparer.OrdinalIgnoreCase);
|
||||
var fragments = new List<LayerComponentFragment>();
|
||||
|
||||
foreach (var analyzer in analyzers)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
var engine = new LanguageAnalyzerEngine(new[] { analyzer });
|
||||
var cacheEntry = await cacheAdapter.GetOrCreateAsync(
|
||||
_logger,
|
||||
analyzer.Id,
|
||||
workspaceFingerprint,
|
||||
token => engine.AnalyzeAsync(analyzerContext, token),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var result = cacheEntry.Result;
|
||||
if (cacheEntry.IsHit)
|
||||
{
|
||||
_metrics.RecordLanguageCacheHit(context, analyzer.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
_metrics.RecordLanguageCacheMiss(context, analyzer.Id);
|
||||
}
|
||||
|
||||
results[analyzer.Id] = result;
|
||||
|
||||
var components = result.Components
|
||||
.Where(component => string.Equals(component.AnalyzerId, analyzer.Id, StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
|
||||
if (components.Length > 0)
|
||||
{
|
||||
var fragment = LanguageComponentMapper.ToLayerFragment(analyzer.Id, components);
|
||||
fragments.Add(fragment);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Language analyzer {AnalyzerId} failed for job {JobId}.", analyzer.Id, context.JobId);
|
||||
}
|
||||
}
|
||||
|
||||
if (results.Count == 0 && fragments.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.Count > 0)
|
||||
{
|
||||
context.Analysis.Set(
|
||||
ScanAnalysisKeys.LanguageAnalyzerResults,
|
||||
new ReadOnlyDictionary<string, LanguageAnalyzerResult>(results));
|
||||
}
|
||||
|
||||
if (fragments.Count > 0)
|
||||
{
|
||||
var immutableFragments = ImmutableArray.CreateRange(fragments);
|
||||
context.Analysis.AppendLayerFragments(immutableFragments);
|
||||
context.Analysis.Set(ScanAnalysisKeys.LanguageComponentFragments, immutableFragments);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadPlugins()
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| SCAN-REPLAY-186-002 | TODO | Scanner Worker Guild | REPLAY-CORE-185-001 | Enforce deterministic analyzer execution when consuming replay input bundles, emit layer Merkle metadata, and author `docs/modules/scanner/deterministic-execution.md` summarising invariants from `docs/replay/DETERMINISTIC_REPLAY.md` Section 4. | Replay mode analyzers pass determinism tests; new doc merged; integration fixtures updated. |
|
||||
| SCANNER-SURFACE-01 | DOING (2025-11-02) | Scanner Worker Guild | SURFACE-FS-02 | Persist Surface.FS manifests after analyzer stages, including layer CAS metadata and EntryTrace fragments.<br>2025-11-02: Draft Surface.FS manifests emitted for sample scans; telemetry counters under review. | Integration tests prove cache entries exist; telemetry counters exported. |
|
||||
| SCANNER-ENV-01 | DOING (2025-11-02) | Scanner Worker Guild | SURFACE-ENV-02 | Replace ad-hoc environment reads with `StellaOps.Scanner.Surface.Env` helpers for cache roots and CAS endpoints.<br>2025-11-02: Worker bootstrap now resolves cache roots via helper; warning path documented; smoke tests running. | Worker boots with helper; misconfiguration warnings documented; smoke tests updated. |
|
||||
| SCANNER-SECRETS-01 | DOING (2025-11-02) | Scanner Worker Guild, Security Guild | SURFACE-SECRETS-02 | Adopt `StellaOps.Scanner.Surface.Secrets` for registry/CAS credentials during scan execution.<br>2025-11-02: Surface.Secrets provider wired for CAS token retrieval; integration tests added. | Secrets fetched via shared provider; legacy secret code removed; integration tests cover rotation. |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Ruby.Internal;
|
||||
|
||||
internal sealed record RubyCapabilities(
|
||||
bool UsesExec,
|
||||
bool UsesNetwork,
|
||||
bool UsesSerialization);
|
||||
@@ -38,7 +38,7 @@ internal sealed class RubyPackage
|
||||
|
||||
public string ComponentKey => $"purl::{Purl}";
|
||||
|
||||
public IReadOnlyCollection<KeyValuePair<string, string?>> CreateMetadata(RubyCapabilities? capabilities)
|
||||
public IReadOnlyCollection<KeyValuePair<string, string?>> CreateMetadata(RubyCapabilities? capabilities = null)
|
||||
{
|
||||
var metadata = new List<KeyValuePair<string, string?>>
|
||||
{
|
||||
|
||||
@@ -10,17 +10,17 @@ internal static class RubyPackageCollector
|
||||
if (!lockData.IsEmpty)
|
||||
{
|
||||
var relativeLockPath = lockData.LockFilePath is null
|
||||
? Gemfile.lock
|
||||
? "Gemfile.lock"
|
||||
: context.GetRelativePath(lockData.LockFilePath);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(relativeLockPath))
|
||||
{
|
||||
relativeLockPath = Gemfile.lock;
|
||||
relativeLockPath = "Gemfile.lock";
|
||||
}
|
||||
|
||||
foreach (var entry in lockData.Entries)
|
||||
{
|
||||
var key = ${entry.Name}@{entry.Version};
|
||||
var key = $"{entry.Name}@{entry.Version}";
|
||||
if (!seen.Add(key))
|
||||
{
|
||||
continue;
|
||||
@@ -37,27 +37,27 @@ internal static class RubyPackageCollector
|
||||
|
||||
private static void CollectVendorCachePackages(LanguageAnalyzerContext context, List<RubyPackage> packages, HashSet<string> seen)
|
||||
{
|
||||
var vendorCache = Path.Combine(context.RootPath, vendor, cache);
|
||||
var vendorCache = Path.Combine(context.RootPath, "vendor", "cache");
|
||||
if (!Directory.Exists(vendorCache))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var gemPath in Directory.EnumerateFiles(vendorCache, *.gem, SearchOption.AllDirectories))
|
||||
foreach (var gemPath in Directory.EnumerateFiles(vendorCache, "*.gem", SearchOption.AllDirectories))
|
||||
{
|
||||
if (!TryParseGemArchive(gemPath, out var name, out var version, out var platform))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = ${name}@{version};
|
||||
var key = $"{name}@{version}";
|
||||
if (!seen.Add(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var locator = context.GetRelativePath(gemPath);
|
||||
packages.Add(RubyPackage.FromVendor(name, version, source: vendor-cache, platform, locator));
|
||||
packages.Add(RubyPackage.FromVendor(name, version, source: "vendor-cache", platform, locator));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace StellaOps.Scanner.Analyzers.Lang.Ruby;
|
||||
|
||||
public sealed class RubyAnalyzerPlugin : ILanguageAnalyzerPlugin
|
||||
{
|
||||
public string Name => StellaOps.Scanner.Analyzers.Lang.Ruby;
|
||||
public string Name => "ruby";
|
||||
|
||||
public bool IsAvailable(IServiceProvider services) => services is not null;
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Internal;
|
||||
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
|
||||
public sealed class LanguageAnalyzerSurfaceCache
|
||||
{
|
||||
private const string CacheNamespace = "scanner/lang/analyzers";
|
||||
private static readonly JsonSerializerOptions JsonOptions = LanguageAnalyzerJson.CreateDefault(indent: false);
|
||||
|
||||
private readonly ISurfaceCache _cache;
|
||||
private readonly string _tenant;
|
||||
|
||||
public LanguageAnalyzerSurfaceCache(ISurfaceCache cache, string tenant)
|
||||
{
|
||||
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
||||
_tenant = string.IsNullOrWhiteSpace(tenant) ? "default" : tenant.Trim();
|
||||
}
|
||||
|
||||
public async ValueTask<LanguageAnalyzerResult> GetOrCreateAsync(
|
||||
ILogger logger,
|
||||
string analyzerId,
|
||||
string fingerprint,
|
||||
Func<CancellationToken, ValueTask<LanguageAnalyzerResult>> factory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
ArgumentNullException.ThrowIfNull(factory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(analyzerId))
|
||||
{
|
||||
throw new ArgumentException("Analyzer identifier is required.", nameof(analyzerId));
|
||||
}
|
||||
|
||||
var contentKey = $"{fingerprint}:{analyzerId}";
|
||||
var key = new SurfaceCacheKey(CacheNamespace, _tenant, contentKey);
|
||||
var cacheHit = true;
|
||||
|
||||
LanguageAnalyzerResult result;
|
||||
try
|
||||
{
|
||||
result = await _cache.GetOrCreateAsync(
|
||||
key,
|
||||
async token =>
|
||||
{
|
||||
cacheHit = false;
|
||||
return await factory(token).ConfigureAwait(false);
|
||||
},
|
||||
Serialize,
|
||||
Deserialize,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException)
|
||||
{
|
||||
cacheHit = false;
|
||||
logger.LogWarning(
|
||||
ex,
|
||||
"Surface cache lookup failed for analyzer {AnalyzerId} (tenant {Tenant}, fingerprint {Fingerprint}); running analyzer without cache.",
|
||||
analyzerId,
|
||||
_tenant,
|
||||
fingerprint);
|
||||
|
||||
result = await factory(cancellationToken).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (cacheHit)
|
||||
{
|
||||
logger.LogDebug(
|
||||
"Surface cache hit for analyzer {AnalyzerId} (tenant {Tenant}, fingerprint {Fingerprint}).",
|
||||
analyzerId,
|
||||
_tenant,
|
||||
fingerprint);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug(
|
||||
"Surface cache miss for analyzer {AnalyzerId} (tenant {Tenant}, fingerprint {Fingerprint}); stored result.",
|
||||
analyzerId,
|
||||
_tenant,
|
||||
fingerprint);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ReadOnlyMemory<byte> Serialize(LanguageAnalyzerResult result)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
var json = result.ToJson(indent: false);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static LanguageAnalyzerResult Deserialize(ReadOnlyMemory<byte> payload)
|
||||
{
|
||||
if (payload.IsEmpty)
|
||||
{
|
||||
return LanguageAnalyzerResult.FromSnapshots(Array.Empty<LanguageComponentSnapshot>());
|
||||
}
|
||||
|
||||
var snapshots = JsonSerializer.Deserialize<IReadOnlyList<LanguageComponentSnapshot>>(payload.Span, JsonOptions)
|
||||
?? Array.Empty<LanguageComponentSnapshot>();
|
||||
|
||||
return LanguageAnalyzerResult.FromSnapshots(snapshots);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Internal;
|
||||
|
||||
using System.Buffers;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
public static class LanguageWorkspaceFingerprint
|
||||
{
|
||||
private static readonly EnumerationOptions Enumeration = new()
|
||||
{
|
||||
RecurseSubdirectories = true,
|
||||
IgnoreInaccessible = true,
|
||||
AttributesToSkip = FileAttributes.ReparsePoint,
|
||||
ReturnSpecialDirectories = false
|
||||
};
|
||||
|
||||
public static string Compute(string rootPath, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rootPath))
|
||||
{
|
||||
throw new ArgumentException("Workspace root path is required.", nameof(rootPath));
|
||||
}
|
||||
|
||||
var fullRoot = Path.GetFullPath(rootPath);
|
||||
if (!Directory.Exists(fullRoot))
|
||||
{
|
||||
return HashPrimitive(fullRoot);
|
||||
}
|
||||
|
||||
var entries = Directory
|
||||
.EnumerateFileSystemEntries(fullRoot, "*", Enumeration)
|
||||
.Select(Path.GetFullPath)
|
||||
.ToList();
|
||||
|
||||
entries.Sort(StringComparer.Ordinal);
|
||||
|
||||
using var aggregate = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
|
||||
Append(aggregate, $"ROOT|{NormalizeRelative(fullRoot, fullRoot)}");
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (Directory.Exists(entry))
|
||||
{
|
||||
Append(aggregate, $"D|{NormalizeRelative(fullRoot, entry)}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var relative = NormalizeRelative(fullRoot, entry);
|
||||
try
|
||||
{
|
||||
var info = new FileInfo(entry);
|
||||
var timestamp = new DateTimeOffset(info.LastWriteTimeUtc).ToUnixTimeMilliseconds();
|
||||
Append(aggregate, $"F|{relative}|{info.Length}|{timestamp}");
|
||||
Append(aggregate, $"H|{ComputeFileHash(entry, cancellationToken)}");
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
Append(aggregate, $"E|{relative}|{ex.GetType().Name}");
|
||||
}
|
||||
}
|
||||
|
||||
return Convert.ToHexString(aggregate.GetHashAndReset()).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string ComputeFileHash(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(64 * 1024);
|
||||
|
||||
try
|
||||
{
|
||||
int read;
|
||||
while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
hash.AppendData(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
return Convert.ToHexString(hash.GetHashAndReset()).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static void Append(IncrementalHash hash, string value)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(value + "\n");
|
||||
hash.AppendData(bytes);
|
||||
}
|
||||
|
||||
private static string NormalizeRelative(string root, string path)
|
||||
{
|
||||
if (string.Equals(root, path, StringComparison.Ordinal))
|
||||
{
|
||||
return ".";
|
||||
}
|
||||
|
||||
var relative = Path.GetRelativePath(root, path);
|
||||
return relative.Replace('\\', '/');
|
||||
}
|
||||
|
||||
private static string HashPrimitive(string value)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(value);
|
||||
return Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Lang;
|
||||
|
||||
public sealed class LanguageAnalyzerContext
|
||||
{
|
||||
public LanguageAnalyzerContext(string rootPath, TimeProvider timeProvider, LanguageUsageHints? usageHints = null, IServiceProvider? services = null)
|
||||
{
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.Secrets;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang;
|
||||
|
||||
public sealed class LanguageAnalyzerContext
|
||||
{
|
||||
private const string SecretsComponentName = "ScannerWorkerLanguageAnalyzers";
|
||||
|
||||
public LanguageAnalyzerContext(string rootPath, TimeProvider timeProvider, LanguageUsageHints? usageHints = null, IServiceProvider? services = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rootPath))
|
||||
{
|
||||
throw new ArgumentException("Root path is required", nameof(rootPath));
|
||||
@@ -15,24 +20,27 @@ public sealed class LanguageAnalyzerContext
|
||||
throw new DirectoryNotFoundException($"Root path '{RootPath}' does not exist.");
|
||||
}
|
||||
|
||||
TimeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
UsageHints = usageHints ?? LanguageUsageHints.Empty;
|
||||
Services = services;
|
||||
}
|
||||
|
||||
public string RootPath { get; }
|
||||
|
||||
public TimeProvider TimeProvider { get; }
|
||||
|
||||
public LanguageUsageHints UsageHints { get; }
|
||||
|
||||
public IServiceProvider? Services { get; }
|
||||
|
||||
public bool TryGetService<T>([NotNullWhen(true)] out T? service) where T : class
|
||||
{
|
||||
if (Services is null)
|
||||
{
|
||||
service = null;
|
||||
TimeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
UsageHints = usageHints ?? LanguageUsageHints.Empty;
|
||||
Services = services;
|
||||
Secrets = CreateSecrets(services);
|
||||
}
|
||||
|
||||
public string RootPath { get; }
|
||||
|
||||
public TimeProvider TimeProvider { get; }
|
||||
|
||||
public LanguageUsageHints UsageHints { get; }
|
||||
|
||||
public IServiceProvider? Services { get; }
|
||||
|
||||
public LanguageAnalyzerSecrets Secrets { get; }
|
||||
|
||||
public bool TryGetService<T>([NotNullWhen(true)] out T? service) where T : class
|
||||
{
|
||||
if (Services is null)
|
||||
{
|
||||
service = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -48,11 +56,11 @@ public sealed class LanguageAnalyzerContext
|
||||
}
|
||||
|
||||
var relativeString = new string(relative);
|
||||
var combined = Path.Combine(RootPath, relativeString);
|
||||
return Path.GetFullPath(combined);
|
||||
}
|
||||
|
||||
public string GetRelativePath(string absolutePath)
|
||||
var combined = Path.Combine(RootPath, relativeString);
|
||||
return Path.GetFullPath(combined);
|
||||
}
|
||||
|
||||
public string GetRelativePath(string absolutePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(absolutePath))
|
||||
{
|
||||
@@ -62,6 +70,23 @@ public sealed class LanguageAnalyzerContext
|
||||
var relative = Path.GetRelativePath(RootPath, absolutePath);
|
||||
return OperatingSystem.IsWindows()
|
||||
? relative.Replace('\\', '/')
|
||||
: relative;
|
||||
}
|
||||
}
|
||||
: relative;
|
||||
}
|
||||
|
||||
private static LanguageAnalyzerSecrets CreateSecrets(IServiceProvider? services)
|
||||
{
|
||||
if (services is null)
|
||||
{
|
||||
return LanguageAnalyzerSecrets.Empty;
|
||||
}
|
||||
|
||||
var environment = services.GetService(typeof(ISurfaceEnvironment)) as ISurfaceEnvironment;
|
||||
if (environment is null)
|
||||
{
|
||||
return LanguageAnalyzerSecrets.Empty;
|
||||
}
|
||||
|
||||
var provider = services.GetService(typeof(ISurfaceSecretProvider)) as ISurfaceSecretProvider;
|
||||
return new LanguageAnalyzerSecrets(provider, environment.Settings.Tenant, SecretsComponentName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,14 +22,25 @@ public sealed class LanguageAnalyzerResult
|
||||
public LayerComponentFragment ToLayerFragment(string analyzerId, string? layerDigest = null)
|
||||
=> LanguageComponentMapper.ToLayerFragment(analyzerId, _components, layerDigest);
|
||||
|
||||
public IReadOnlyList<LanguageComponentSnapshot> ToSnapshots()
|
||||
=> _components.Select(static component => component.ToSnapshot()).ToImmutableArray();
|
||||
|
||||
public string ToJson(bool indent = true)
|
||||
{
|
||||
var snapshots = ToSnapshots();
|
||||
var options = Internal.LanguageAnalyzerJson.CreateDefault(indent);
|
||||
return JsonSerializer.Serialize(snapshots, options);
|
||||
public IReadOnlyList<LanguageComponentSnapshot> ToSnapshots()
|
||||
=> _components.Select(static component => component.ToSnapshot()).ToImmutableArray();
|
||||
|
||||
internal static LanguageAnalyzerResult FromSnapshots(IEnumerable<LanguageComponentSnapshot> snapshots)
|
||||
{
|
||||
if (snapshots is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(snapshots));
|
||||
}
|
||||
|
||||
var records = snapshots.Select(LanguageComponentRecord.FromSnapshot).ToArray();
|
||||
return new LanguageAnalyzerResult(records);
|
||||
}
|
||||
|
||||
public string ToJson(bool indent = true)
|
||||
{
|
||||
var snapshots = ToSnapshots();
|
||||
var options = Internal.LanguageAnalyzerJson.CreateDefault(indent);
|
||||
return JsonSerializer.Serialize(snapshots, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Lang;
|
||||
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.Secrets;
|
||||
|
||||
public sealed class LanguageAnalyzerSecrets
|
||||
{
|
||||
private const string DefaultComponent = "ScannerWorkerLanguageAnalyzers";
|
||||
|
||||
public static LanguageAnalyzerSecrets Empty { get; } = new(null, "default", DefaultComponent);
|
||||
|
||||
private readonly ISurfaceSecretProvider? _provider;
|
||||
private readonly string _tenant;
|
||||
private readonly string _component;
|
||||
|
||||
internal LanguageAnalyzerSecrets(ISurfaceSecretProvider? provider, string tenant, string component)
|
||||
{
|
||||
_provider = provider;
|
||||
_tenant = string.IsNullOrWhiteSpace(tenant) ? "default" : tenant.Trim();
|
||||
_component = NormalizeComponentName(component);
|
||||
}
|
||||
|
||||
public bool IsAvailable => _provider is not null;
|
||||
|
||||
public async ValueTask<SurfaceSecretHandle> GetAsync(
|
||||
string secretType,
|
||||
string? name = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(secretType))
|
||||
{
|
||||
throw new ArgumentException("Secret type is required.", nameof(secretType));
|
||||
}
|
||||
|
||||
if (_provider is null)
|
||||
{
|
||||
throw new SurfaceSecretNotFoundException(CreateRequest(secretType, name));
|
||||
}
|
||||
|
||||
return await _provider.GetAsync(CreateRequest(secretType, name), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask<SurfaceSecretHandle?> TryGetAsync(
|
||||
string secretType,
|
||||
string? name = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(secretType) || _provider is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await _provider.GetAsync(CreateRequest(secretType, name), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (SurfaceSecretNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private SurfaceSecretRequest CreateRequest(string secretType, string? name)
|
||||
=> new(_tenant, _component, secretType, name);
|
||||
|
||||
private static string NormalizeComponentName(string component)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(component))
|
||||
{
|
||||
return DefaultComponent;
|
||||
}
|
||||
|
||||
var normalized = component.Trim()
|
||||
.Replace(".", string.Empty, StringComparison.Ordinal)
|
||||
.Replace("-", string.Empty, StringComparison.Ordinal)
|
||||
.Replace(" ", string.Empty, StringComparison.Ordinal);
|
||||
|
||||
return string.IsNullOrWhiteSpace(normalized) ? DefaultComponent : normalized;
|
||||
}
|
||||
}
|
||||
@@ -109,23 +109,71 @@ public sealed class LanguageComponentRecord
|
||||
throw new ArgumentException("Component key is required", nameof(componentKey));
|
||||
}
|
||||
|
||||
return new LanguageComponentRecord(
|
||||
analyzerId,
|
||||
componentKey.Trim(),
|
||||
purl,
|
||||
name,
|
||||
version,
|
||||
type,
|
||||
metadata ?? Array.Empty<KeyValuePair<string, string?>>(),
|
||||
evidence ?? Array.Empty<LanguageComponentEvidence>(),
|
||||
usedByEntrypoint);
|
||||
}
|
||||
|
||||
internal void Merge(LanguageComponentRecord other)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
if (!ComponentKey.Equals(other.ComponentKey, StringComparison.Ordinal))
|
||||
return new LanguageComponentRecord(
|
||||
analyzerId,
|
||||
componentKey.Trim(),
|
||||
purl,
|
||||
name,
|
||||
version,
|
||||
type,
|
||||
metadata ?? Array.Empty<KeyValuePair<string, string?>>(),
|
||||
evidence ?? Array.Empty<LanguageComponentEvidence>(),
|
||||
usedByEntrypoint);
|
||||
}
|
||||
|
||||
internal static LanguageComponentRecord FromSnapshot(LanguageComponentSnapshot snapshot)
|
||||
{
|
||||
if (snapshot is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(snapshot));
|
||||
}
|
||||
|
||||
var metadata = snapshot.Metadata is null
|
||||
? Array.Empty<KeyValuePair<string, string?>>()
|
||||
: snapshot.Metadata.Select(static entry => new KeyValuePair<string, string?>(entry.Key, entry.Value));
|
||||
|
||||
var evidence = snapshot.Evidence is null or { Count: 0 }
|
||||
? Array.Empty<LanguageComponentEvidence>()
|
||||
: snapshot.Evidence
|
||||
.Where(static item => item is not null)
|
||||
.Select(static item => new LanguageComponentEvidence(
|
||||
item.Kind,
|
||||
item.Source ?? string.Empty,
|
||||
item.Locator ?? string.Empty,
|
||||
item.Value,
|
||||
item.Sha256))
|
||||
.ToArray();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(snapshot.Purl))
|
||||
{
|
||||
return FromPurl(
|
||||
snapshot.AnalyzerId,
|
||||
snapshot.Purl!,
|
||||
snapshot.Name,
|
||||
snapshot.Version,
|
||||
snapshot.Type,
|
||||
metadata,
|
||||
evidence,
|
||||
snapshot.UsedByEntrypoint);
|
||||
}
|
||||
|
||||
return FromExplicitKey(
|
||||
snapshot.AnalyzerId,
|
||||
snapshot.ComponentKey,
|
||||
snapshot.Purl,
|
||||
snapshot.Name,
|
||||
snapshot.Version,
|
||||
snapshot.Type,
|
||||
metadata,
|
||||
evidence,
|
||||
snapshot.UsedByEntrypoint);
|
||||
}
|
||||
|
||||
internal void Merge(LanguageComponentRecord other)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(other);
|
||||
|
||||
if (!ComponentKey.Equals(other.ComponentKey, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot merge component '{ComponentKey}' with '{other.ComponentKey}'.");
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.Scanner.Core/StellaOps.Scanner.Core.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Scanner.Surface.Env/StellaOps.Scanner.Surface.Env.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Scanner.Surface.FS/StellaOps.Scanner.Surface.FS.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Scanner.Surface.Secrets/StellaOps.Scanner.Surface.Secrets.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
| ID | Status | Owner(s) | Depends on | Description | Exit Criteria |
|
||||
|----|--------|----------|------------|-------------|---------------|
|
||||
| LANG-SURFACE-01 | TODO | Language Analyzer Guild | SURFACE-VAL-02, SURFACE-FS-02 | Invoke Surface.Validation checks (env/cache/secrets) before analyzer execution to ensure consistent prerequisites. | Validation pipeline integrated; regression tests updated; failures bubble with actionable errors. |
|
||||
| LANG-SURFACE-02 | TODO | Language Analyzer Guild | SURFACE-FS-02 | Consume Surface.FS APIs for layer/source caching (instead of bespoke caches) to improve determinism. | Analyzer outputs match baseline; performance benchmarks recorded; docs updated. |
|
||||
| LANG-SURFACE-03 | TODO | Language Analyzer Guild | SURFACE-SECRETS-02 | Replace direct secret/env reads with Surface.Secrets references when fetching package feeds or registry creds. | Analyzer uses shared provider; tests cover rotation/failure; config docs updated. |
|
||||
| LANG-SURFACE-01 | DONE | Language Analyzer Guild | SURFACE-VAL-02, SURFACE-FS-02 | Invoke Surface.Validation checks (env/cache/secrets) before analyzer execution to ensure consistent prerequisites. | Validation pipeline integrated; regression tests updated; failures bubble with actionable errors. |
|
||||
| LANG-SURFACE-02 | DONE | Language Analyzer Guild | SURFACE-FS-02 | Consume Surface.FS APIs for layer/source caching (instead of bespoke caches) to improve determinism. | Analyzer outputs match baseline; performance benchmarks recorded; docs updated. |
|
||||
| LANG-SURFACE-03 | DONE | Language Analyzer Guild | SURFACE-SECRETS-02 | Replace direct secret/env reads with Surface.Secrets references when fetching package feeds or registry creds. | Analyzer uses shared provider; tests cover rotation/failure; config docs updated. |
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.Secrets;
|
||||
using StellaOps.Scanner.Surface.Secrets.Providers;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Tests.Core;
|
||||
|
||||
public sealed class LanguageAnalyzerContextTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SecretsProviderAvailable_ReturnsHandle()
|
||||
{
|
||||
using var workspace = new TempDirectory();
|
||||
var services = new ServiceCollection();
|
||||
|
||||
var settings = new SurfaceEnvironmentSettings(
|
||||
new Uri("https://surface.test"),
|
||||
"unit-bucket",
|
||||
null,
|
||||
new DirectoryInfo(workspace.Path),
|
||||
128,
|
||||
false,
|
||||
Array.Empty<string>(),
|
||||
new SurfaceSecretsConfiguration("inline", "testtenant", null, null, null, true),
|
||||
"testtenant",
|
||||
new SurfaceTlsConfiguration(null, null, null));
|
||||
|
||||
var environment = new StubSurfaceEnvironment(settings);
|
||||
var provider = new InMemorySurfaceSecretProvider();
|
||||
|
||||
var request = new SurfaceSecretRequest("testtenant", "ScannerWorkerLanguageAnalyzers", "registry", "default");
|
||||
var handle = SurfaceSecretHandle.FromBytes(Encoding.UTF8.GetBytes("token"), new Dictionary<string, string> { ["source"] = "test" });
|
||||
provider.Add(request, handle);
|
||||
|
||||
services.AddSingleton<ISurfaceEnvironment>(environment);
|
||||
services.AddSingleton<ISurfaceSecretProvider>(provider);
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
var context = new LanguageAnalyzerContext(workspace.Path, TimeProvider.System, services: serviceProvider);
|
||||
|
||||
Assert.True(context.Secrets.IsAvailable);
|
||||
using var retrieved = await context.Secrets.GetAsync("registry", "default", TestContext.Current.CancellationToken);
|
||||
Assert.Same(handle, retrieved);
|
||||
Assert.Equal("test", retrieved.Metadata["source"]);
|
||||
Assert.Equal("token", Encoding.UTF8.GetString(retrieved.AsBytes().Span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SecretsProviderMissing_UsesEmptyInstance()
|
||||
{
|
||||
using var workspace = new TempDirectory();
|
||||
var context = new LanguageAnalyzerContext(workspace.Path, TimeProvider.System);
|
||||
|
||||
Assert.False(context.Secrets.IsAvailable);
|
||||
var secret = await context.Secrets.TryGetAsync("registry", cancellationToken: TestContext.Current.CancellationToken);
|
||||
Assert.Null(secret);
|
||||
}
|
||||
|
||||
private sealed class StubSurfaceEnvironment : ISurfaceEnvironment
|
||||
{
|
||||
public StubSurfaceEnvironment(SurfaceEnvironmentSettings settings)
|
||||
{
|
||||
Settings = settings;
|
||||
}
|
||||
|
||||
public SurfaceEnvironmentSettings Settings { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, string> RawVariables { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private sealed class TempDirectory : IDisposable
|
||||
{
|
||||
public TempDirectory()
|
||||
{
|
||||
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"stellaops-langctx-{Guid.NewGuid():n}");
|
||||
Directory.CreateDirectory(Path);
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(Path))
|
||||
{
|
||||
Directory.Delete(Path, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +1,120 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "dotnet",
|
||||
"componentKey": "purl::pkg:nuget/stellaops.logging@2.5.1",
|
||||
"purl": "pkg:nuget/stellaops.logging@2.5.1",
|
||||
"name": "StellaOps.Logging",
|
||||
"version": "2.5.1",
|
||||
"type": "nuget",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"assembly[0].assetPath": "lib/net10.0/StellaOps.Logging.dll",
|
||||
"assembly[0].fileVersion": "2.5.1.12345",
|
||||
"assembly[0].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"assembly[0].version": "2.5.1.0",
|
||||
"assembly[1].assetPath": "runtimes/linux-arm64/lib/net10.0/StellaOps.Logging.dll",
|
||||
"assembly[1].rid[0]": "linux-arm64",
|
||||
"assembly[1].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"assembly[2].assetPath": "runtimes/linux-x64/lib/net10.0/StellaOps.Logging.dll",
|
||||
"assembly[2].rid[0]": "linux-x64",
|
||||
"assembly[2].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"assembly[3].assetPath": "runtimes/osx-arm64/lib/net10.0/StellaOps.Logging.dll",
|
||||
"assembly[3].rid[0]": "osx-arm64",
|
||||
"assembly[3].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"deps.path[0]": "AppA.deps.json",
|
||||
"deps.path[1]": "AppB.deps.json",
|
||||
"deps.rid[0]": "linux-arm64",
|
||||
"deps.rid[1]": "linux-x64",
|
||||
"deps.rid[2]": "osx-arm64",
|
||||
"deps.rid[3]": "win-arm64",
|
||||
"deps.tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"license.expression[0]": "Apache-2.0",
|
||||
"native[0].assetPath": "runtimes/win-arm64/native/stellaops.logging.dll",
|
||||
"native[0].rid[0]": "win-arm64",
|
||||
"native[0].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"package.hashPath[0]": "stellaops.logging.2.5.1.nupkg.sha512",
|
||||
"package.id": "StellaOps.Logging",
|
||||
"package.id.normalized": "stellaops.logging",
|
||||
"package.path[0]": "stellaops.logging/2.5.1",
|
||||
"package.serviceable": "true",
|
||||
"package.sha512[0]": "sha512-FAKE_LOGGING_SHA==",
|
||||
"package.version": "2.5.1",
|
||||
"provenance": "manifest"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deps.json",
|
||||
"locator": "AppA.deps.json",
|
||||
"value": "StellaOps.Logging/2.5.1"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deps.json",
|
||||
"locator": "AppB.deps.json",
|
||||
"value": "StellaOps.Logging/2.5.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "dotnet",
|
||||
"componentKey": "purl::pkg:nuget/stellaops.toolkit@1.2.3",
|
||||
"purl": "pkg:nuget/stellaops.toolkit@1.2.3",
|
||||
"name": "StellaOps.Toolkit",
|
||||
"version": "1.2.3",
|
||||
"type": "nuget",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"assembly[0].assetPath": "lib/net10.0/StellaOps.Toolkit.dll",
|
||||
"assembly[0].fileVersion": "1.2.3.0",
|
||||
"assembly[0].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"assembly[0].version": "1.2.3.0",
|
||||
"deps.dependency[0]": "stellaops.logging",
|
||||
"deps.path[0]": "AppA.deps.json",
|
||||
"deps.path[1]": "AppB.deps.json",
|
||||
"deps.rid[0]": "linux-x64",
|
||||
"deps.rid[1]": "osx-arm64",
|
||||
"deps.rid[2]": "win-arm64",
|
||||
"deps.tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"license.file.sha256[0]": "604e182900b0ecb1ffb911c817bcbd148a31b8f55ad392a3b770be8005048c5c",
|
||||
"license.file[0]": "packages/stellaops.toolkit/1.2.3/LICENSE.txt",
|
||||
"native[0].assetPath": "runtimes/linux-x64/native/libstellaops.toolkit.so",
|
||||
"native[0].rid[0]": "linux-x64",
|
||||
"native[0].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"native[1].assetPath": "runtimes/osx-arm64/native/libstellaops.toolkit.dylib",
|
||||
"native[1].rid[0]": "osx-arm64",
|
||||
"native[1].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"native[2].assetPath": "runtimes/win-arm64/native/stellaops.toolkit.dll",
|
||||
"native[2].rid[0]": "win-arm64",
|
||||
"native[2].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"package.hashPath[0]": "stellaops.toolkit.1.2.3.nupkg.sha512",
|
||||
"package.id": "StellaOps.Toolkit",
|
||||
"package.id.normalized": "stellaops.toolkit",
|
||||
"package.path[0]": "stellaops.toolkit/1.2.3",
|
||||
"package.serviceable": "true",
|
||||
"package.sha512[0]": "sha512-FAKE_TOOLKIT_SHA==",
|
||||
"package.version": "1.2.3",
|
||||
"provenance": "manifest"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deps.json",
|
||||
"locator": "AppA.deps.json",
|
||||
"value": "StellaOps.Toolkit/1.2.3"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deps.json",
|
||||
"locator": "AppB.deps.json",
|
||||
"value": "StellaOps.Toolkit/1.2.3"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "license",
|
||||
"locator": "packages/stellaops.toolkit/1.2.3/LICENSE.txt",
|
||||
"sha256": "604e182900b0ecb1ffb911c817bcbd148a31b8f55ad392a3b770be8005048c5c"
|
||||
}
|
||||
]
|
||||
}
|
||||
[
|
||||
{
|
||||
"analyzerId": "dotnet",
|
||||
"componentKey": "purl::pkg:nuget/stellaops.logging@2.5.1",
|
||||
"purl": "pkg:nuget/stellaops.logging@2.5.1",
|
||||
"name": "StellaOps.Logging",
|
||||
"version": "2.5.1",
|
||||
"type": "nuget",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"assembly[0].assetPath": "lib/net10.0/StellaOps.Logging.dll",
|
||||
"assembly[0].fileVersion": "2.5.1.12345",
|
||||
"assembly[0].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"assembly[0].version": "2.5.1.0",
|
||||
"assembly[1].assetPath": "runtimes/linux-arm64/lib/net10.0/StellaOps.Logging.dll",
|
||||
"assembly[1].rid[0]": "linux-arm64",
|
||||
"assembly[1].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"assembly[2].assetPath": "runtimes/linux-x64/lib/net10.0/StellaOps.Logging.dll",
|
||||
"assembly[2].rid[0]": "linux-x64",
|
||||
"assembly[2].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"assembly[3].assetPath": "runtimes/osx-arm64/lib/net10.0/StellaOps.Logging.dll",
|
||||
"assembly[3].rid[0]": "osx-arm64",
|
||||
"assembly[3].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"deps.path[0]": "AppA.deps.json",
|
||||
"deps.path[1]": "AppB.deps.json",
|
||||
"deps.rid[0]": "linux-arm64",
|
||||
"deps.rid[1]": "linux-x64",
|
||||
"deps.rid[2]": "osx-arm64",
|
||||
"deps.rid[3]": "win-arm64",
|
||||
"deps.tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"license.expression[0]": "Apache-2.0",
|
||||
"native[0].assetPath": "runtimes/win-arm64/native/stellaops.logging.dll",
|
||||
"native[0].rid[0]": "win-arm64",
|
||||
"native[0].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"package.hashPath[0]": "stellaops.logging.2.5.1.nupkg.sha512",
|
||||
"package.id": "StellaOps.Logging",
|
||||
"package.id.normalized": "stellaops.logging",
|
||||
"package.path[0]": "stellaops.logging/2.5.1",
|
||||
"package.serviceable": "true",
|
||||
"package.sha512[0]": "sha512-FAKE_LOGGING_SHA==",
|
||||
"package.version": "2.5.1",
|
||||
"provenance": "manifest"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deps.json",
|
||||
"locator": "AppA.deps.json",
|
||||
"value": "StellaOps.Logging/2.5.1"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deps.json",
|
||||
"locator": "AppB.deps.json",
|
||||
"value": "StellaOps.Logging/2.5.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "dotnet",
|
||||
"componentKey": "purl::pkg:nuget/stellaops.toolkit@1.2.3",
|
||||
"purl": "pkg:nuget/stellaops.toolkit@1.2.3",
|
||||
"name": "StellaOps.Toolkit",
|
||||
"version": "1.2.3",
|
||||
"type": "nuget",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"assembly[0].assetPath": "lib/net10.0/StellaOps.Toolkit.dll",
|
||||
"assembly[0].fileVersion": "1.2.3.0",
|
||||
"assembly[0].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"assembly[0].version": "1.2.3.0",
|
||||
"deps.dependency[0]": "stellaops.logging",
|
||||
"deps.path[0]": "AppA.deps.json",
|
||||
"deps.path[1]": "AppB.deps.json",
|
||||
"deps.rid[0]": "linux-x64",
|
||||
"deps.rid[1]": "osx-arm64",
|
||||
"deps.rid[2]": "win-arm64",
|
||||
"deps.tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"license.file.sha256[0]": "09065a51df7b52a7183d6ceae2c201e5629bc9b5c5347a0890667a3aa3f65623",
|
||||
"license.file[0]": "packages/stellaops.toolkit/1.2.3/LICENSE.txt",
|
||||
"native[0].assetPath": "runtimes/linux-x64/native/libstellaops.toolkit.so",
|
||||
"native[0].rid[0]": "linux-x64",
|
||||
"native[0].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"native[1].assetPath": "runtimes/osx-arm64/native/libstellaops.toolkit.dylib",
|
||||
"native[1].rid[0]": "osx-arm64",
|
||||
"native[1].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"native[2].assetPath": "runtimes/win-arm64/native/stellaops.toolkit.dll",
|
||||
"native[2].rid[0]": "win-arm64",
|
||||
"native[2].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"package.hashPath[0]": "stellaops.toolkit.1.2.3.nupkg.sha512",
|
||||
"package.id": "StellaOps.Toolkit",
|
||||
"package.id.normalized": "stellaops.toolkit",
|
||||
"package.path[0]": "stellaops.toolkit/1.2.3",
|
||||
"package.serviceable": "true",
|
||||
"package.sha512[0]": "sha512-FAKE_TOOLKIT_SHA==",
|
||||
"package.version": "1.2.3",
|
||||
"provenance": "manifest"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deps.json",
|
||||
"locator": "AppA.deps.json",
|
||||
"value": "StellaOps.Toolkit/1.2.3"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deps.json",
|
||||
"locator": "AppB.deps.json",
|
||||
"value": "StellaOps.Toolkit/1.2.3"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "license",
|
||||
"locator": "packages/stellaops.toolkit/1.2.3/LICENSE.txt",
|
||||
"sha256": "09065a51df7b52a7183d6ceae2c201e5629bc9b5c5347a0890667a3aa3f65623"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,94 +1,94 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "dotnet",
|
||||
"componentKey": "purl::pkg:nuget/stellaops.runtime.selfcontained@2.1.0",
|
||||
"purl": "pkg:nuget/stellaops.runtime.selfcontained@2.1.0",
|
||||
"name": "StellaOps.Runtime.SelfContained",
|
||||
"version": "2.1.0",
|
||||
"type": "nuget",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"deps.path[0]": "MyApp.deps.json",
|
||||
"deps.rid[0]": "linux-x64",
|
||||
"deps.rid[1]": "win-x64",
|
||||
"deps.tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"license.expression[0]": "Apache-2.0",
|
||||
"native[0].assetPath": "runtimes/linux-x64/native/libstellaopsnative.so",
|
||||
"native[0].path": "runtimes/linux-x64/native/libstellaopsnative.so",
|
||||
"native[0].rid[0]": "linux-x64",
|
||||
"native[0].sha256": "6cf3d2a487d6a42fc7c3e2edbc452224e99a3656287a534f1164ee6ec9daadf0",
|
||||
"native[0].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"native[1].assetPath": "runtimes/win-x64/native/stellaopsnative.dll",
|
||||
"native[1].rid[0]": "win-x64",
|
||||
"native[1].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"package.hashPath[0]": "stellaops.runtime.selfcontained.2.1.0.nupkg.sha512",
|
||||
"package.id": "StellaOps.Runtime.SelfContained",
|
||||
"package.id.normalized": "stellaops.runtime.selfcontained",
|
||||
"package.path[0]": "stellaops.runtime.selfcontained/2.1.0",
|
||||
"package.serviceable": "true",
|
||||
"package.sha512[0]": "sha512-FAKE_RUNTIME_SHA==",
|
||||
"package.version": "2.1.0",
|
||||
"provenance": "manifest"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deps.json",
|
||||
"locator": "MyApp.deps.json",
|
||||
"value": "StellaOps.Runtime.SelfContained/2.1.0"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "native",
|
||||
"locator": "runtimes/linux-x64/native/libstellaopsnative.so",
|
||||
"value": "runtimes/linux-x64/native/libstellaopsnative.so",
|
||||
"sha256": "6cf3d2a487d6a42fc7c3e2edbc452224e99a3656287a534f1164ee6ec9daadf0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "dotnet",
|
||||
"componentKey": "purl::pkg:nuget/stellaops.toolkit@1.2.3",
|
||||
"purl": "pkg:nuget/stellaops.toolkit@1.2.3",
|
||||
"name": "StellaOps.Toolkit",
|
||||
"version": "1.2.3",
|
||||
"type": "nuget",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"assembly[0].assetPath": "lib/net10.0/StellaOps.Toolkit.dll",
|
||||
"assembly[0].fileVersion": "1.2.3.0",
|
||||
"assembly[0].rid[0]": "linux-x64",
|
||||
"assembly[0].rid[1]": "win-x64",
|
||||
"assembly[0].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"assembly[0].version": "1.2.3.0",
|
||||
"deps.path[0]": "MyApp.deps.json",
|
||||
"deps.rid[0]": "linux-x64",
|
||||
"deps.rid[1]": "win-x64",
|
||||
"deps.tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"license.file.sha256[0]": "f94d89a576c63e8ba6ee01760c52fa7861ba609491d7c6e6c01ead5ca66b6048",
|
||||
"license.file[0]": "packages/stellaops.toolkit/1.2.3/LICENSE.txt",
|
||||
"package.hashPath[0]": "stellaops.toolkit.1.2.3.nupkg.sha512",
|
||||
"package.id": "StellaOps.Toolkit",
|
||||
"package.id.normalized": "stellaops.toolkit",
|
||||
"package.path[0]": "stellaops.toolkit/1.2.3",
|
||||
"package.serviceable": "true",
|
||||
"package.sha512[0]": "sha512-FAKE_TOOLKIT_SHA==",
|
||||
"package.version": "1.2.3",
|
||||
"provenance": "manifest"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deps.json",
|
||||
"locator": "MyApp.deps.json",
|
||||
"value": "StellaOps.Toolkit/1.2.3"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "license",
|
||||
"locator": "packages/stellaops.toolkit/1.2.3/LICENSE.txt",
|
||||
"sha256": "f94d89a576c63e8ba6ee01760c52fa7861ba609491d7c6e6c01ead5ca66b6048"
|
||||
}
|
||||
]
|
||||
}
|
||||
[
|
||||
{
|
||||
"analyzerId": "dotnet",
|
||||
"componentKey": "purl::pkg:nuget/stellaops.runtime.selfcontained@2.1.0",
|
||||
"purl": "pkg:nuget/stellaops.runtime.selfcontained@2.1.0",
|
||||
"name": "StellaOps.Runtime.SelfContained",
|
||||
"version": "2.1.0",
|
||||
"type": "nuget",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"deps.path[0]": "MyApp.deps.json",
|
||||
"deps.rid[0]": "linux-x64",
|
||||
"deps.rid[1]": "win-x64",
|
||||
"deps.tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"license.expression[0]": "Apache-2.0",
|
||||
"native[0].assetPath": "runtimes/linux-x64/native/libstellaopsnative.so",
|
||||
"native[0].path": "runtimes/linux-x64/native/libstellaopsnative.so",
|
||||
"native[0].rid[0]": "linux-x64",
|
||||
"native[0].sha256": "c22d4a6584a3bb8fad4d255d1ab9e5a80d553eec35ea8dfcc2dd750e8581d3cb",
|
||||
"native[0].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"native[1].assetPath": "runtimes/win-x64/native/stellaopsnative.dll",
|
||||
"native[1].rid[0]": "win-x64",
|
||||
"native[1].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"package.hashPath[0]": "stellaops.runtime.selfcontained.2.1.0.nupkg.sha512",
|
||||
"package.id": "StellaOps.Runtime.SelfContained",
|
||||
"package.id.normalized": "stellaops.runtime.selfcontained",
|
||||
"package.path[0]": "stellaops.runtime.selfcontained/2.1.0",
|
||||
"package.serviceable": "true",
|
||||
"package.sha512[0]": "sha512-FAKE_RUNTIME_SHA==",
|
||||
"package.version": "2.1.0",
|
||||
"provenance": "manifest"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deps.json",
|
||||
"locator": "MyApp.deps.json",
|
||||
"value": "StellaOps.Runtime.SelfContained/2.1.0"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "native",
|
||||
"locator": "runtimes/linux-x64/native/libstellaopsnative.so",
|
||||
"value": "runtimes/linux-x64/native/libstellaopsnative.so",
|
||||
"sha256": "c22d4a6584a3bb8fad4d255d1ab9e5a80d553eec35ea8dfcc2dd750e8581d3cb"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "dotnet",
|
||||
"componentKey": "purl::pkg:nuget/stellaops.toolkit@1.2.3",
|
||||
"purl": "pkg:nuget/stellaops.toolkit@1.2.3",
|
||||
"name": "StellaOps.Toolkit",
|
||||
"version": "1.2.3",
|
||||
"type": "nuget",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"assembly[0].assetPath": "lib/net10.0/StellaOps.Toolkit.dll",
|
||||
"assembly[0].fileVersion": "1.2.3.0",
|
||||
"assembly[0].rid[0]": "linux-x64",
|
||||
"assembly[0].rid[1]": "win-x64",
|
||||
"assembly[0].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"assembly[0].version": "1.2.3.0",
|
||||
"deps.path[0]": "MyApp.deps.json",
|
||||
"deps.rid[0]": "linux-x64",
|
||||
"deps.rid[1]": "win-x64",
|
||||
"deps.tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"license.file.sha256[0]": "1c05159789c5dd80b97e7a20dc2b7b716e63514f3a8d40b2f593030973a9fcdb",
|
||||
"license.file[0]": "packages/stellaops.toolkit/1.2.3/LICENSE.txt",
|
||||
"package.hashPath[0]": "stellaops.toolkit.1.2.3.nupkg.sha512",
|
||||
"package.id": "StellaOps.Toolkit",
|
||||
"package.id.normalized": "stellaops.toolkit",
|
||||
"package.path[0]": "stellaops.toolkit/1.2.3",
|
||||
"package.serviceable": "true",
|
||||
"package.sha512[0]": "sha512-FAKE_TOOLKIT_SHA==",
|
||||
"package.version": "1.2.3",
|
||||
"provenance": "manifest"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deps.json",
|
||||
"locator": "MyApp.deps.json",
|
||||
"value": "StellaOps.Toolkit/1.2.3"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "license",
|
||||
"locator": "packages/stellaops.toolkit/1.2.3/LICENSE.txt",
|
||||
"sha256": "1c05159789c5dd80b97e7a20dc2b7b716e63514f3a8d40b2f593030973a9fcdb"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,87 +1,87 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "dotnet",
|
||||
"componentKey": "purl::pkg:nuget/microsoft.extensions.logging@9.0.0",
|
||||
"purl": "pkg:nuget/microsoft.extensions.logging@9.0.0",
|
||||
"name": "Microsoft.Extensions.Logging",
|
||||
"version": "9.0.0",
|
||||
"type": "nuget",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"assembly[0].assetPath": "lib/net9.0/Microsoft.Extensions.Logging.dll",
|
||||
"assembly[0].fileVersion": "9.0.24.52809",
|
||||
"assembly[0].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"assembly[0].version": "9.0.0.0",
|
||||
"assembly[1].assetPath": "runtimes/linux-x64/lib/net9.0/Microsoft.Extensions.Logging.dll",
|
||||
"assembly[1].rid[0]": "linux-x64",
|
||||
"assembly[1].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"assembly[2].assetPath": "runtimes/win-x86/lib/net9.0/Microsoft.Extensions.Logging.dll",
|
||||
"assembly[2].rid[0]": "win-x86",
|
||||
"assembly[2].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"deps.path[0]": "Sample.App.deps.json",
|
||||
"deps.rid[0]": "linux-x64",
|
||||
"deps.rid[1]": "win-x86",
|
||||
"deps.tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"license.expression[0]": "MIT",
|
||||
"package.hashPath[0]": "microsoft.extensions.logging.9.0.0.nupkg.sha512",
|
||||
"package.id": "Microsoft.Extensions.Logging",
|
||||
"package.id.normalized": "microsoft.extensions.logging",
|
||||
"package.path[0]": "microsoft.extensions.logging/9.0.0",
|
||||
"package.serviceable": "true",
|
||||
"package.sha512[0]": "sha512-FAKE_LOGGING_SHA==",
|
||||
"package.version": "9.0.0",
|
||||
"provenance": "manifest"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deps.json",
|
||||
"locator": "Sample.App.deps.json",
|
||||
"value": "Microsoft.Extensions.Logging/9.0.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "dotnet",
|
||||
"componentKey": "purl::pkg:nuget/stellaops.toolkit@1.2.3",
|
||||
"purl": "pkg:nuget/stellaops.toolkit@1.2.3",
|
||||
"name": "StellaOps.Toolkit",
|
||||
"version": "1.2.3",
|
||||
"type": "nuget",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"assembly[0].assetPath": "lib/net10.0/StellaOps.Toolkit.dll",
|
||||
"assembly[0].fileVersion": "1.2.3.0",
|
||||
"assembly[0].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"assembly[0].version": "1.2.3.0",
|
||||
"deps.dependency[0]": "microsoft.extensions.logging",
|
||||
"deps.path[0]": "Sample.App.deps.json",
|
||||
"deps.rid[0]": "linux-x64",
|
||||
"deps.tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"license.file.sha256[0]": "604e182900b0ecb1ffb911c817bcbd148a31b8f55ad392a3b770be8005048c5c",
|
||||
"license.file[0]": "packages/stellaops.toolkit/1.2.3/LICENSE.txt",
|
||||
"package.hashPath[0]": "stellaops.toolkit.1.2.3.nupkg.sha512",
|
||||
"package.id": "StellaOps.Toolkit",
|
||||
"package.id.normalized": "stellaops.toolkit",
|
||||
"package.path[0]": "stellaops.toolkit/1.2.3",
|
||||
"package.serviceable": "true",
|
||||
"package.sha512[0]": "sha512-FAKE_TOOLKIT_SHA==",
|
||||
"package.version": "1.2.3",
|
||||
"provenance": "manifest"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deps.json",
|
||||
"locator": "Sample.App.deps.json",
|
||||
"value": "StellaOps.Toolkit/1.2.3"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "license",
|
||||
"locator": "packages/stellaops.toolkit/1.2.3/LICENSE.txt",
|
||||
"sha256": "604e182900b0ecb1ffb911c817bcbd148a31b8f55ad392a3b770be8005048c5c"
|
||||
}
|
||||
]
|
||||
}
|
||||
[
|
||||
{
|
||||
"analyzerId": "dotnet",
|
||||
"componentKey": "purl::pkg:nuget/microsoft.extensions.logging@9.0.0",
|
||||
"purl": "pkg:nuget/microsoft.extensions.logging@9.0.0",
|
||||
"name": "Microsoft.Extensions.Logging",
|
||||
"version": "9.0.0",
|
||||
"type": "nuget",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"assembly[0].assetPath": "lib/net9.0/Microsoft.Extensions.Logging.dll",
|
||||
"assembly[0].fileVersion": "9.0.24.52809",
|
||||
"assembly[0].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"assembly[0].version": "9.0.0.0",
|
||||
"assembly[1].assetPath": "runtimes/linux-x64/lib/net9.0/Microsoft.Extensions.Logging.dll",
|
||||
"assembly[1].rid[0]": "linux-x64",
|
||||
"assembly[1].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"assembly[2].assetPath": "runtimes/win-x86/lib/net9.0/Microsoft.Extensions.Logging.dll",
|
||||
"assembly[2].rid[0]": "win-x86",
|
||||
"assembly[2].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"deps.path[0]": "Sample.App.deps.json",
|
||||
"deps.rid[0]": "linux-x64",
|
||||
"deps.rid[1]": "win-x86",
|
||||
"deps.tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"license.expression[0]": "MIT",
|
||||
"package.hashPath[0]": "microsoft.extensions.logging.9.0.0.nupkg.sha512",
|
||||
"package.id": "Microsoft.Extensions.Logging",
|
||||
"package.id.normalized": "microsoft.extensions.logging",
|
||||
"package.path[0]": "microsoft.extensions.logging/9.0.0",
|
||||
"package.serviceable": "true",
|
||||
"package.sha512[0]": "sha512-FAKE_LOGGING_SHA==",
|
||||
"package.version": "9.0.0",
|
||||
"provenance": "manifest"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deps.json",
|
||||
"locator": "Sample.App.deps.json",
|
||||
"value": "Microsoft.Extensions.Logging/9.0.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "dotnet",
|
||||
"componentKey": "purl::pkg:nuget/stellaops.toolkit@1.2.3",
|
||||
"purl": "pkg:nuget/stellaops.toolkit@1.2.3",
|
||||
"name": "StellaOps.Toolkit",
|
||||
"version": "1.2.3",
|
||||
"type": "nuget",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"assembly[0].assetPath": "lib/net10.0/StellaOps.Toolkit.dll",
|
||||
"assembly[0].fileVersion": "1.2.3.0",
|
||||
"assembly[0].tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"assembly[0].version": "1.2.3.0",
|
||||
"deps.dependency[0]": "microsoft.extensions.logging",
|
||||
"deps.path[0]": "Sample.App.deps.json",
|
||||
"deps.rid[0]": "linux-x64",
|
||||
"deps.tfm[0]": ".NETCoreApp,Version=v10.0",
|
||||
"license.file.sha256[0]": "09065a51df7b52a7183d6ceae2c201e5629bc9b5c5347a0890667a3aa3f65623",
|
||||
"license.file[0]": "packages/stellaops.toolkit/1.2.3/LICENSE.txt",
|
||||
"package.hashPath[0]": "stellaops.toolkit.1.2.3.nupkg.sha512",
|
||||
"package.id": "StellaOps.Toolkit",
|
||||
"package.id.normalized": "stellaops.toolkit",
|
||||
"package.path[0]": "stellaops.toolkit/1.2.3",
|
||||
"package.serviceable": "true",
|
||||
"package.sha512[0]": "sha512-FAKE_TOOLKIT_SHA==",
|
||||
"package.version": "1.2.3",
|
||||
"provenance": "manifest"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deps.json",
|
||||
"locator": "Sample.App.deps.json",
|
||||
"value": "StellaOps.Toolkit/1.2.3"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "license",
|
||||
"locator": "packages/stellaops.toolkit/1.2.3/LICENSE.txt",
|
||||
"sha256": "09065a51df7b52a7183d6ceae2c201e5629bc9b5c5347a0890667a3aa3f65623"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,65 +1,65 @@
|
||||
[
|
||||
{
|
||||
analyzerId: ruby,
|
||||
componentKey: purl::pkg:gem/custom-gem@1.0.0,
|
||||
purl: pkg:gem/custom-gem@1.0.0,
|
||||
name: custom-gem,
|
||||
version: 1.0.0,
|
||||
type: gem,
|
||||
usedByEntrypoint: false,
|
||||
metadata: {
|
||||
declaredOnly: true,
|
||||
lockfile: vendor/cache/custom-gem-1.0.0.gem,
|
||||
source: vendor-cache
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/custom-gem@1.0.0",
|
||||
"purl": "pkg:gem/custom-gem@1.0.0",
|
||||
"name": "custom-gem",
|
||||
"version": "1.0.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"declaredOnly": "true",
|
||||
"lockfile": "vendor/cache/custom-gem-1.0.0.gem",
|
||||
"source": "vendor-cache"
|
||||
},
|
||||
evidence: [
|
||||
"evidence": [
|
||||
{
|
||||
kind: file,
|
||||
source: vendor-cache,
|
||||
locator: vendor/cache/custom-gem-1.0.0.gem
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "vendor/cache/custom-gem-1.0.0.gem"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
analyzerId: ruby,
|
||||
componentKey: purl::pkg:gem/puma@6.4.2,
|
||||
purl: pkg:gem/puma@6.4.2,
|
||||
name: puma,
|
||||
version: 6.4.2,
|
||||
type: gem,
|
||||
usedByEntrypoint: false,
|
||||
metadata: {
|
||||
declaredOnly: true,
|
||||
lockfile: Gemfile.lock,
|
||||
source: rubygems
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/puma@6.4.2",
|
||||
"purl": "pkg:gem/puma@6.4.2",
|
||||
"name": "puma",
|
||||
"version": "6.4.2",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"declaredOnly": "true",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
evidence: [
|
||||
"evidence": [
|
||||
{
|
||||
kind: file,
|
||||
source: rubygems,
|
||||
locator: Gemfile.lock
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
analyzerId: ruby,
|
||||
componentKey: purl::pkg:gem/rake@13.1.0,
|
||||
purl: pkg:gem/rake@13.1.0,
|
||||
name: rake,
|
||||
version: 13.1.0,
|
||||
type: gem,
|
||||
usedByEntrypoint: false,
|
||||
metadata: {
|
||||
declaredOnly: true,
|
||||
lockfile: Gemfile.lock,
|
||||
source: rubygems
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/rake@13.1.0",
|
||||
"purl": "pkg:gem/rake@13.1.0",
|
||||
"name": "rake",
|
||||
"version": "13.1.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"declaredOnly": "true",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
evidence: [
|
||||
"evidence": [
|
||||
{
|
||||
kind: file,
|
||||
source: rubygems,
|
||||
locator: Gemfile.lock
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -2,9 +2,7 @@
|
||||
{
|
||||
"analyzerId": "rust",
|
||||
"componentKey": "bin::sha256:10f3c03766e4403be40add0467a2b2d07fd7006e4b8515ab88740ffa327ea775",
|
||||
"purl": null,
|
||||
"name": "opaque_bin",
|
||||
"version": null,
|
||||
"type": "bin",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
@@ -17,9 +15,8 @@
|
||||
"kind": "file",
|
||||
"source": "binary",
|
||||
"locator": "usr/local/bin/opaque_bin",
|
||||
"value": null,
|
||||
"sha256": "10f3c03766e4403be40add0467a2b2d07fd7006e4b8515ab88740ffa327ea775"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -9,11 +9,12 @@ public sealed class RubyLanguageAnalyzerTests
|
||||
[Fact]
|
||||
public async Task GemfileLockProducesDeterministicInventoryAsync()
|
||||
{
|
||||
var fixture = TestPaths.ResolveFixture(lang, ruby, basic);
|
||||
var golden = Path.Combine(fixture, expected.json);
|
||||
var fixture = TestPaths.ResolveFixture("lang", "ruby", "basic");
|
||||
var golden = Path.Combine(fixture, "expected.json");
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixture,
|
||||
golden,
|
||||
new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() });
|
||||
new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() },
|
||||
cancellationToken: TestContext.Current.CancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Plugin;
|
||||
using StellaOps.Scanner.Analyzers.OS.Abstractions;
|
||||
using StellaOps.Scanner.Analyzers.OS.Plugin;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Worker.Processing;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Plugin;
|
||||
using StellaOps.Scanner.Analyzers.OS.Abstractions;
|
||||
using StellaOps.Scanner.Analyzers.OS.Plugin;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Surface.Env;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
using StellaOps.Scanner.Surface.Secrets;
|
||||
using StellaOps.Scanner.Surface.Validation;
|
||||
using StellaOps.Scanner.Worker.Diagnostics;
|
||||
using StellaOps.Scanner.Worker.Processing;
|
||||
using Xunit;
|
||||
using WorkerOptions = StellaOps.Scanner.Worker.Options.ScannerWorkerOptions;
|
||||
|
||||
@@ -20,44 +29,120 @@ namespace StellaOps.Scanner.Worker.Tests;
|
||||
|
||||
public sealed class CompositeScanAnalyzerDispatcherTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_RunsLanguageAnalyzers_StoresResults()
|
||||
{
|
||||
using var workspace = new TempDirectory();
|
||||
var metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
{ ScanMetadataKeys.RootFilesystemPath, workspace.Path },
|
||||
{ ScanMetadataKeys.WorkspacePath, workspace.Path },
|
||||
};
|
||||
|
||||
var osCatalog = new FakeOsCatalog();
|
||||
var languageCatalog = new FakeLanguageCatalog(new FakeLanguageAnalyzer());
|
||||
|
||||
var services = new ServiceCollection()
|
||||
.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Debug))
|
||||
.BuildServiceProvider();
|
||||
|
||||
var scopeFactory = services.GetRequiredService<IServiceScopeFactory>();
|
||||
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new WorkerOptions());
|
||||
var dispatcher = new CompositeScanAnalyzerDispatcher(
|
||||
scopeFactory,
|
||||
osCatalog,
|
||||
languageCatalog,
|
||||
options,
|
||||
loggerFactory.CreateLogger<CompositeScanAnalyzerDispatcher>());
|
||||
|
||||
var lease = new TestJobLease(metadata);
|
||||
var context = new ScanJobContext(lease, TimeProvider.System, TimeProvider.System.GetUtcNow(), CancellationToken.None);
|
||||
|
||||
await dispatcher.ExecuteAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.True(context.Analysis.TryGet<ReadOnlyDictionary<string, LanguageAnalyzerResult>>(ScanAnalysisKeys.LanguageAnalyzerResults, out var results));
|
||||
Assert.Single(results);
|
||||
Assert.True(context.Analysis.TryGet<ImmutableArray<LayerComponentFragment>>(ScanAnalysisKeys.LanguageComponentFragments, out var fragments));
|
||||
Assert.False(fragments.IsDefaultOrEmpty);
|
||||
Assert.True(context.Analysis.GetLayerFragments().Any(fragment => fragment.Components.Any(component => component.Identity.Name == "demo-package")));
|
||||
}
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_RunsLanguageAnalyzers_StoresResults()
|
||||
{
|
||||
using var workspace = new TempDirectory();
|
||||
using var cacheRoot = new TempDirectory();
|
||||
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_FS_ENDPOINT", "https://surface.test");
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_FS_BUCKET", "unit-test-bucket");
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_CACHE_ROOT", cacheRoot.Path);
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_SECRETS_PROVIDER", "inline");
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_SECRETS_TENANT", "testtenant");
|
||||
Environment.SetEnvironmentVariable(
|
||||
"SURFACE_SECRET_TESTTENANT_SCANNERWORKERLANGUAGEANALYZERS_REGISTRY_DEFAULT",
|
||||
Convert.ToBase64String(Encoding.UTF8.GetBytes("token-placeholder")));
|
||||
|
||||
var metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
{ ScanMetadataKeys.RootFilesystemPath, workspace.Path },
|
||||
{ ScanMetadataKeys.WorkspacePath, workspace.Path },
|
||||
};
|
||||
|
||||
var osCatalog = new FakeOsCatalog();
|
||||
var analyzer = new FakeLanguageAnalyzer();
|
||||
var languageCatalog = new FakeLanguageCatalog(analyzer);
|
||||
|
||||
long hits = 0;
|
||||
long misses = 0;
|
||||
MeterListener? meterListener = null;
|
||||
ServiceProvider? services = null;
|
||||
try
|
||||
{
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build());
|
||||
serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Debug));
|
||||
serviceCollection.AddSingleton(TimeProvider.System);
|
||||
serviceCollection.AddSurfaceEnvironment(options => options.ComponentName = "Scanner.Worker");
|
||||
serviceCollection.AddSurfaceValidation();
|
||||
serviceCollection.AddSurfaceFileCache(options => options.RootDirectory = cacheRoot.Path);
|
||||
serviceCollection.AddSurfaceSecrets();
|
||||
|
||||
var metrics = new ScannerWorkerMetrics();
|
||||
serviceCollection.AddSingleton(metrics);
|
||||
|
||||
meterListener = new MeterListener();
|
||||
|
||||
meterListener.InstrumentPublished = (instrument, listener) =>
|
||||
{
|
||||
if (instrument.Meter.Name == ScannerWorkerInstrumentation.MeterName &&
|
||||
(instrument.Name == "scanner_worker_language_cache_hits_total" || instrument.Name == "scanner_worker_language_cache_misses_total"))
|
||||
{
|
||||
listener.EnableMeasurementEvents(instrument);
|
||||
}
|
||||
};
|
||||
|
||||
meterListener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
|
||||
{
|
||||
if (instrument.Name == "scanner_worker_language_cache_hits_total")
|
||||
{
|
||||
Interlocked.Add(ref hits, measurement);
|
||||
}
|
||||
else if (instrument.Name == "scanner_worker_language_cache_misses_total")
|
||||
{
|
||||
Interlocked.Add(ref misses, measurement);
|
||||
}
|
||||
});
|
||||
|
||||
meterListener.Start();
|
||||
|
||||
services = serviceCollection.BuildServiceProvider();
|
||||
|
||||
var scopeFactory = services.GetRequiredService<IServiceScopeFactory>();
|
||||
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new WorkerOptions());
|
||||
var dispatcher = new CompositeScanAnalyzerDispatcher(
|
||||
scopeFactory,
|
||||
osCatalog,
|
||||
languageCatalog,
|
||||
options,
|
||||
loggerFactory.CreateLogger<CompositeScanAnalyzerDispatcher>(),
|
||||
metrics);
|
||||
|
||||
var lease = new TestJobLease(metadata);
|
||||
var context = new ScanJobContext(lease, TimeProvider.System, TimeProvider.System.GetUtcNow(), CancellationToken.None);
|
||||
|
||||
await dispatcher.ExecuteAsync(context, CancellationToken.None);
|
||||
|
||||
// Re-run with a new context to exercise cache reuse.
|
||||
var leaseSecond = new TestJobLease(metadata);
|
||||
var contextSecond = new ScanJobContext(leaseSecond, TimeProvider.System, TimeProvider.System.GetUtcNow(), CancellationToken.None);
|
||||
await dispatcher.ExecuteAsync(contextSecond, CancellationToken.None);
|
||||
|
||||
meterListener.RecordObservableInstruments();
|
||||
|
||||
Assert.Equal(1, analyzer.InvocationCount);
|
||||
Assert.True(context.Analysis.TryGet<ReadOnlyDictionary<string, LanguageAnalyzerResult>>(ScanAnalysisKeys.LanguageAnalyzerResults, out var results));
|
||||
Assert.Single(results);
|
||||
Assert.True(context.Analysis.TryGet<ImmutableArray<LayerComponentFragment>>(ScanAnalysisKeys.LanguageComponentFragments, out var fragments));
|
||||
Assert.False(fragments.IsDefaultOrEmpty);
|
||||
Assert.True(context.Analysis.GetLayerFragments().Any(fragment => fragment.Components.Any(component => component.Identity.Name == "demo-package")));
|
||||
Assert.Equal(1, hits);
|
||||
Assert.Equal(1, misses);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_FS_ENDPOINT", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_FS_BUCKET", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_CACHE_ROOT", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_SECRETS_PROVIDER", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER_SURFACE_SECRETS_TENANT", null);
|
||||
Environment.SetEnvironmentVariable("SURFACE_SECRET_TESTTENANT_SCANNERWORKERLANGUAGEANALYZERS_REGISTRY_DEFAULT", null);
|
||||
meterListener?.Dispose();
|
||||
services?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeOsCatalog : IOSAnalyzerPluginCatalog
|
||||
{
|
||||
@@ -94,17 +179,23 @@ public sealed class CompositeScanAnalyzerDispatcherTests
|
||||
|
||||
public string DisplayName => "Fake Language Analyzer";
|
||||
|
||||
public ValueTask AnalyzeAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken)
|
||||
{
|
||||
writer.AddFromPurl(
|
||||
analyzerId: Id,
|
||||
purl: "pkg:npm/demo-package@1.0.0",
|
||||
name: "demo-package",
|
||||
version: "1.0.0",
|
||||
type: "npm");
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
public int InvocationCount { get; private set; }
|
||||
|
||||
public ValueTask AnalyzeAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken)
|
||||
{
|
||||
Interlocked.Increment(ref _invocationCount);
|
||||
InvocationCount = _invocationCount;
|
||||
writer.AddFromPurl(
|
||||
analyzerId: Id,
|
||||
purl: "pkg:npm/demo-package@1.0.0",
|
||||
name: "demo-package",
|
||||
version: "1.0.0",
|
||||
type: "npm");
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private int _invocationCount;
|
||||
}
|
||||
|
||||
private sealed class TestJobLease : IScanJobLease
|
||||
{
|
||||
|
||||
@@ -104,8 +104,7 @@ public sealed class WorkerBasicScanScenarioTests
|
||||
await worker.StopAsync(CancellationToken.None);
|
||||
|
||||
Assert.True(lease.Completed.Task.IsCompletedSuccessfully, "Job should complete successfully.");
|
||||
Assert.Single(analyzer.Executions);
|
||||
Assert.True(lease.RenewalCount >= 1, "Lease should have been renewed at least once.");
|
||||
Assert.Single(analyzer.Executions);
|
||||
|
||||
var stageOrder = testLoggerProvider
|
||||
.GetEntriesForCategory(typeof(ScanProgressReporter).FullName!)
|
||||
@@ -123,7 +122,8 @@ public sealed class WorkerBasicScanScenarioTests
|
||||
|
||||
var jobDuration = listener.Measurements.Where(m => m.InstrumentName == "scanner_worker_job_duration_ms").ToArray();
|
||||
Assert.Single(jobDuration);
|
||||
Assert.True(jobDuration[0].Value > 0, "Job duration should be positive.");
|
||||
var jobDurationMs = jobDuration[0].Value;
|
||||
Assert.True(jobDurationMs > 0, "Job duration should be positive.");
|
||||
|
||||
var stageDurations = listener.Measurements.Where(m => m.InstrumentName == "scanner_worker_stage_duration_ms").ToArray();
|
||||
Assert.Contains(stageDurations, m => m.Tags.TryGetValue("stage", out var stage) && Equals(stage, ScanStageNames.ExecuteAnalyzers));
|
||||
|
||||
Reference in New Issue
Block a user