feat(scanner): Implement Deno analyzer and associated tests
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added Deno analyzer with comprehensive metadata and evidence structure. - Created a detailed implementation plan for Sprint 130 focusing on Deno analyzer. - Introduced AdvisoryAiGuardrailOptions for managing guardrail configurations. - Developed GuardrailPhraseLoader for loading blocked phrases from JSON files. - Implemented tests for AdvisoryGuardrailOptions binding and phrase loading. - Enhanced telemetry for Advisory AI with metrics tracking. - Added VexObservationProjectionService for querying VEX observations. - Created extensive tests for VexObservationProjectionService functionality. - Introduced Ruby language analyzer with tests for simple and complex workspaces. - Added Ruby application fixtures for testing purposes.
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Hosting;
|
||||
|
||||
public sealed class AdvisoryAiGuardrailOptions
|
||||
{
|
||||
private const int DefaultMaxPromptLength = 16000;
|
||||
|
||||
public int? MaxPromptLength { get; set; } = DefaultMaxPromptLength;
|
||||
|
||||
public bool RequireCitations { get; set; } = true;
|
||||
|
||||
public string? BlockedPhraseFile { get; set; }
|
||||
= null;
|
||||
|
||||
public List<string> BlockedPhrases { get; set; } = new();
|
||||
}
|
||||
@@ -18,6 +18,8 @@ public sealed class AdvisoryAiServiceOptions
|
||||
|
||||
public AdvisoryAiInferenceOptions Inference { get; set; } = new();
|
||||
|
||||
public AdvisoryAiGuardrailOptions Guardrails { get; set; } = new();
|
||||
|
||||
internal string ResolveQueueDirectory(string contentRoot)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(contentRoot);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using StellaOps.AdvisoryAI.Inference;
|
||||
@@ -71,6 +72,15 @@ internal static class AdvisoryAiServiceOptionsValidator
|
||||
}
|
||||
}
|
||||
|
||||
options.Guardrails ??= new AdvisoryAiGuardrailOptions();
|
||||
options.Guardrails.BlockedPhrases ??= new List<string>();
|
||||
|
||||
if (options.Guardrails.MaxPromptLength.HasValue && options.Guardrails.MaxPromptLength.Value <= 0)
|
||||
{
|
||||
error = "AdvisoryAI:Guardrails:MaxPromptLength must be greater than zero when specified.";
|
||||
return false;
|
||||
}
|
||||
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Hosting;
|
||||
|
||||
internal static class GuardrailPhraseLoader
|
||||
{
|
||||
public static IReadOnlyCollection<string> Load(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ArgumentException("Guardrail phrase file path must be provided.", nameof(path));
|
||||
}
|
||||
|
||||
using var stream = File.OpenRead(path);
|
||||
using var document = JsonDocument.Parse(stream);
|
||||
var root = document.RootElement;
|
||||
return root.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Array => ExtractValues(root),
|
||||
JsonValueKind.Object when root.TryGetProperty("phrases", out var phrases) => ExtractValues(phrases),
|
||||
_ => throw new InvalidDataException($"Guardrail phrase file {path} must be a JSON array or object with a phrases array."),
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyCollection<string> ExtractValues(JsonElement element)
|
||||
{
|
||||
var phrases = new List<string>();
|
||||
foreach (var item in element.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var value = item.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
phrases.Add(value.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return phrases;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.AdvisoryAI.Caching;
|
||||
using StellaOps.AdvisoryAI.DependencyInjection;
|
||||
using StellaOps.AdvisoryAI.Inference;
|
||||
using StellaOps.AdvisoryAI.Metrics;
|
||||
using StellaOps.AdvisoryAI.Guardrails;
|
||||
using StellaOps.AdvisoryAI.Outputs;
|
||||
using StellaOps.AdvisoryAI.Providers;
|
||||
using StellaOps.AdvisoryAI.Queue;
|
||||
@@ -86,6 +91,12 @@ public static class ServiceCollectionExtensions
|
||||
services.AddAdvisoryPipeline();
|
||||
services.AddAdvisoryPipelineInfrastructure();
|
||||
|
||||
services.AddOptions<AdvisoryGuardrailOptions>()
|
||||
.Configure<IOptions<AdvisoryAiServiceOptions>, IHostEnvironment>((options, aiOptions, environment) =>
|
||||
{
|
||||
ApplyGuardrailConfiguration(options, aiOptions.Value.Guardrails, environment);
|
||||
});
|
||||
|
||||
services.Replace(ServiceDescriptor.Singleton<IAdvisoryTaskQueue, FileSystemAdvisoryTaskQueue>());
|
||||
services.Replace(ServiceDescriptor.Singleton<IAdvisoryPlanCache, FileSystemAdvisoryPlanCache>());
|
||||
services.Replace(ServiceDescriptor.Singleton<IAdvisoryOutputStore, FileSystemAdvisoryOutputStore>());
|
||||
@@ -93,4 +104,87 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static void ApplyGuardrailConfiguration(
|
||||
AdvisoryGuardrailOptions target,
|
||||
AdvisoryAiGuardrailOptions? source,
|
||||
IHostEnvironment? environment)
|
||||
{
|
||||
if (source is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.MaxPromptLength.HasValue && source.MaxPromptLength.Value > 0)
|
||||
{
|
||||
target.MaxPromptLength = source.MaxPromptLength.Value;
|
||||
}
|
||||
|
||||
target.RequireCitations = source.RequireCitations;
|
||||
|
||||
var defaults = target.BlockedPhrases.ToList();
|
||||
var merged = new SortedSet<string>(defaults, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (source.BlockedPhrases is { Count: > 0 })
|
||||
{
|
||||
foreach (var phrase in source.BlockedPhrases)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(phrase))
|
||||
{
|
||||
merged.Add(phrase.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(source.BlockedPhraseFile))
|
||||
{
|
||||
var resolvedPath = ResolveGuardrailPath(source.BlockedPhraseFile!, environment);
|
||||
foreach (var phrase in GuardrailPhraseLoader.Load(resolvedPath))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(phrase))
|
||||
{
|
||||
merged.Add(phrase.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (merged.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
target.BlockedPhrases.Clear();
|
||||
foreach (var phrase in merged)
|
||||
{
|
||||
target.BlockedPhrases.Add(phrase);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveGuardrailPath(string configuredPath, IHostEnvironment? environment)
|
||||
{
|
||||
var trimmed = configuredPath.Trim();
|
||||
if (Path.IsPathRooted(trimmed))
|
||||
{
|
||||
if (!File.Exists(trimmed))
|
||||
{
|
||||
throw new FileNotFoundException($"Guardrail phrase file {trimmed} was not found.", trimmed);
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
var root = environment?.ContentRootPath;
|
||||
if (string.IsNullOrWhiteSpace(root))
|
||||
{
|
||||
root = AppContext.BaseDirectory;
|
||||
}
|
||||
|
||||
var resolved = Path.GetFullPath(Path.Combine(root!, trimmed));
|
||||
if (!File.Exists(resolved))
|
||||
{
|
||||
throw new FileNotFoundException($"Guardrail phrase file {resolved} was not found.", resolved);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.AdvisoryAI.Guardrails;
|
||||
using StellaOps.AdvisoryAI.Hosting;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.Tests;
|
||||
|
||||
public sealed class AdvisoryGuardrailOptionsBindingTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AddAdvisoryAiCore_ConfiguresGuardrailOptionsFromServiceOptions()
|
||||
{
|
||||
var tempRoot = CreateTempDirectory();
|
||||
var phrasePath = Path.Combine(tempRoot, "guardrail-phrases.json");
|
||||
await File.WriteAllTextAsync(phrasePath, "{\n \"phrases\": [\"extract secrets\", \"dump cache\"]\n}");
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["AdvisoryAI:Guardrails:MaxPromptLength"] = "32000",
|
||||
["AdvisoryAI:Guardrails:RequireCitations"] = "false",
|
||||
["AdvisoryAI:Guardrails:BlockedPhraseFile"] = "guardrail-phrases.json",
|
||||
["AdvisoryAI:Guardrails:BlockedPhrases:0"] = "custom override"
|
||||
})
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IHostEnvironment>(new FakeHostEnvironment(tempRoot));
|
||||
services.AddAdvisoryAiCore(configuration);
|
||||
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var options = provider.GetRequiredService<IOptions<AdvisoryGuardrailOptions>>().Value;
|
||||
|
||||
options.MaxPromptLength.Should().Be(32000);
|
||||
options.RequireCitations.Should().BeFalse();
|
||||
options.BlockedPhrases.Should().Contain("custom override");
|
||||
options.BlockedPhrases.Should().Contain("extract secrets");
|
||||
options.BlockedPhrases.Should().Contain("dump cache");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddAdvisoryAiCore_ThrowsWhenPhraseFileMissing()
|
||||
{
|
||||
var tempRoot = CreateTempDirectory();
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["AdvisoryAI:Guardrails:BlockedPhraseFile"] = "missing.json"
|
||||
})
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IHostEnvironment>(new FakeHostEnvironment(tempRoot));
|
||||
services.AddAdvisoryAiCore(configuration);
|
||||
|
||||
await using var provider = services.BuildServiceProvider();
|
||||
var action = () => provider.GetRequiredService<IOptions<AdvisoryGuardrailOptions>>().Value;
|
||||
action.Should().Throw<FileNotFoundException>();
|
||||
}
|
||||
|
||||
private static string CreateTempDirectory()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), "advisoryai-guardrails", Guid.NewGuid().ToString("n"));
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private sealed class FakeHostEnvironment : IHostEnvironment
|
||||
{
|
||||
public FakeHostEnvironment(string contentRoot)
|
||||
{
|
||||
ContentRootPath = contentRoot;
|
||||
}
|
||||
|
||||
public string EnvironmentName { get; set; } = Environments.Development;
|
||||
|
||||
public string ApplicationName { get; set; } = "StellaOps.AdvisoryAI.Tests";
|
||||
|
||||
public string ContentRootPath { get; set; }
|
||||
= string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,12 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.AdvisoryAI.Hosting\StellaOps.AdvisoryAI.Hosting.csproj" />
|
||||
|
||||
@@ -6942,6 +6942,35 @@ internal static class CommandHandlers
|
||||
return;
|
||||
}
|
||||
|
||||
if (report.Observation is { } observation)
|
||||
{
|
||||
var bundler = string.IsNullOrWhiteSpace(observation.BundlerVersion)
|
||||
? "n/a"
|
||||
: observation.BundlerVersion;
|
||||
|
||||
AnsiConsole.MarkupLine(
|
||||
"[grey]Observation[/] bundler={0} • packages={1} • runtimeEdges={2}",
|
||||
Markup.Escape(bundler),
|
||||
observation.PackageCount,
|
||||
observation.RuntimeEdgeCount);
|
||||
|
||||
AnsiConsole.MarkupLine(
|
||||
"[grey]Capabilities[/] exec={0} net={1} serialization={2}",
|
||||
observation.UsesExec ? "[green]on[/]" : "[red]off[/]",
|
||||
observation.UsesNetwork ? "[green]on[/]" : "[red]off[/]",
|
||||
observation.UsesSerialization ? "[green]on[/]" : "[red]off[/]");
|
||||
|
||||
if (observation.SchedulerCount > 0)
|
||||
{
|
||||
var schedulerLabel = observation.Schedulers.Count > 0
|
||||
? string.Join(", ", observation.Schedulers)
|
||||
: observation.SchedulerCount.ToString(CultureInfo.InvariantCulture);
|
||||
AnsiConsole.MarkupLine("[grey]Schedulers[/] {0}", Markup.Escape(schedulerLabel));
|
||||
}
|
||||
|
||||
AnsiConsole.WriteLine();
|
||||
}
|
||||
|
||||
var table = new Table().Border(TableBorder.Rounded);
|
||||
table.AddColumn("Package");
|
||||
table.AddColumn("Version");
|
||||
@@ -7088,14 +7117,19 @@ internal static class CommandHandlers
|
||||
[JsonPropertyName("packages")]
|
||||
public IReadOnlyList<RubyInspectEntry> Packages { get; }
|
||||
|
||||
private RubyInspectReport(IReadOnlyList<RubyInspectEntry> packages)
|
||||
[JsonPropertyName("observation")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public RubyObservationSummary? Observation { get; }
|
||||
|
||||
private RubyInspectReport(IReadOnlyList<RubyInspectEntry> packages, RubyObservationSummary? observation)
|
||||
{
|
||||
Packages = packages;
|
||||
Observation = observation;
|
||||
}
|
||||
|
||||
public static RubyInspectReport Create(IEnumerable<LanguageComponentSnapshot>? snapshots)
|
||||
{
|
||||
var source = snapshots ?? Array.Empty<LanguageComponentSnapshot>();
|
||||
var source = snapshots?.ToArray() ?? Array.Empty<LanguageComponentSnapshot>();
|
||||
|
||||
var entries = source
|
||||
.Where(static snapshot => string.Equals(snapshot.Type, "gem", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -7104,7 +7138,9 @@ internal static class CommandHandlers
|
||||
.ThenBy(static entry => entry.Version ?? string.Empty, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
return new RubyInspectReport(entries);
|
||||
var observation = RubyObservationSummary.TryCreate(source);
|
||||
|
||||
return new RubyInspectReport(entries, observation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7149,6 +7185,41 @@ internal static class CommandHandlers
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record RubyObservationSummary(
|
||||
[property: JsonPropertyName("packageCount")] int PackageCount,
|
||||
[property: JsonPropertyName("runtimeEdgeCount")] int RuntimeEdgeCount,
|
||||
[property: JsonPropertyName("bundlerVersion")] string? BundlerVersion,
|
||||
[property: JsonPropertyName("usesExec")] bool UsesExec,
|
||||
[property: JsonPropertyName("usesNetwork")] bool UsesNetwork,
|
||||
[property: JsonPropertyName("usesSerialization")] bool UsesSerialization,
|
||||
[property: JsonPropertyName("schedulerCount")] int SchedulerCount,
|
||||
[property: JsonPropertyName("schedulers")] IReadOnlyList<string> Schedulers)
|
||||
{
|
||||
public static RubyObservationSummary? TryCreate(IEnumerable<LanguageComponentSnapshot> snapshots)
|
||||
{
|
||||
var observation = snapshots.FirstOrDefault(static snapshot =>
|
||||
string.Equals(snapshot.Type, "ruby-observation", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (observation is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var metadata = RubyMetadataHelpers.Clone(observation.Metadata);
|
||||
var schedulers = RubyMetadataHelpers.GetList(metadata, "ruby.observation.capability.scheduler_list");
|
||||
|
||||
return new RubyObservationSummary(
|
||||
RubyMetadataHelpers.GetInt(metadata, "ruby.observation.packages") ?? 0,
|
||||
RubyMetadataHelpers.GetInt(metadata, "ruby.observation.runtime_edges") ?? 0,
|
||||
RubyMetadataHelpers.GetString(metadata, "ruby.observation.bundler_version"),
|
||||
RubyMetadataHelpers.GetBool(metadata, "ruby.observation.capability.exec") ?? false,
|
||||
RubyMetadataHelpers.GetBool(metadata, "ruby.observation.capability.net") ?? false,
|
||||
RubyMetadataHelpers.GetBool(metadata, "ruby.observation.capability.serialization") ?? false,
|
||||
RubyMetadataHelpers.GetInt(metadata, "ruby.observation.capability.schedulers") ?? schedulers.Count,
|
||||
schedulers);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RubyResolveReport
|
||||
{
|
||||
[JsonPropertyName("scanId")]
|
||||
@@ -7343,6 +7414,22 @@ internal static class CommandHandlers
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static int? GetInt(IDictionary<string, string?> metadata, string key)
|
||||
{
|
||||
var value = GetString(metadata, key);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record LockValidationEntry(
|
||||
|
||||
@@ -457,6 +457,10 @@ public sealed class CommandHandlersTests
|
||||
&& string.Equals(entry.GetProperty("lockfile").GetString(), "Gemfile.lock", StringComparison.OrdinalIgnoreCase)
|
||||
&& entry.GetProperty("runtimeEntrypoints").EnumerateArray().Any(value =>
|
||||
string.Equals(value.GetString(), "app.rb", StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
var observation = document.RootElement.GetProperty("observation");
|
||||
Assert.Equal("2.5.4", observation.GetProperty("bundlerVersion").GetString());
|
||||
Assert.Equal(packages.GetArrayLength(), observation.GetProperty("packageCount").GetInt32());
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -495,6 +499,9 @@ public sealed class CommandHandlersTests
|
||||
"app.rb",
|
||||
entry.GetProperty("runtimeEntrypoints").EnumerateArray().Select(e => e.GetString() ?? string.Empty),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var observation = document.RootElement.GetProperty("observation");
|
||||
Assert.True(observation.GetProperty("runtimeEdgeCount").GetInt32() >= 1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -902,9 +902,8 @@ var advisoryChunksEndpoint = app.MapGet("/advisories/{advisoryKey}/chunks", asyn
|
||||
}
|
||||
|
||||
var duration = timeProvider.GetElapsedTime(requestStart);
|
||||
var guardrailCounts = cacheHit
|
||||
? ImmutableDictionary<AdvisoryChunkGuardrailReason, int>.Empty
|
||||
: buildResult.Telemetry.GuardrailCounts;
|
||||
var guardrailCounts = buildResult.Telemetry.GuardrailCounts ??
|
||||
ImmutableDictionary<AdvisoryChunkGuardrailReason, int>.Empty;
|
||||
|
||||
telemetry.TrackChunkResult(new AdvisoryAiChunkRequestTelemetry(
|
||||
tenant,
|
||||
|
||||
@@ -51,7 +51,7 @@ internal sealed class AdvisoryAiTelemetry : IAdvisoryAiTelemetry
|
||||
AdvisoryAiMetrics.BuildCacheTags(tenant, "hit"));
|
||||
}
|
||||
|
||||
if (!telemetry.CacheHit && telemetry.GuardrailCounts.Count > 0)
|
||||
if (telemetry.GuardrailCounts.Count > 0)
|
||||
{
|
||||
foreach (var kvp in telemetry.GuardrailCounts)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Concelier.WebService.Services;
|
||||
using StellaOps.Concelier.WebService.Diagnostics;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.WebService.Tests;
|
||||
|
||||
public sealed class AdvisoryAiTelemetryTests : IDisposable
|
||||
{
|
||||
private readonly MeterListener _listener;
|
||||
private readonly List<Measurement<long>> _guardrailMeasurements = new();
|
||||
|
||||
public AdvisoryAiTelemetryTests()
|
||||
{
|
||||
_listener = new MeterListener
|
||||
{
|
||||
InstrumentPublished = (instrument, listener) =>
|
||||
{
|
||||
if (instrument.Meter.Name == AdvisoryAiMetrics.MeterName)
|
||||
{
|
||||
listener.EnableMeasurementEvents(instrument);
|
||||
}
|
||||
}
|
||||
};
|
||||
_listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
|
||||
{
|
||||
if (instrument.Meter.Name == AdvisoryAiMetrics.MeterName &&
|
||||
instrument.Name == "advisory_ai_guardrail_blocks_total")
|
||||
{
|
||||
_guardrailMeasurements.Add(new Measurement<long>(measurement, tags, state));
|
||||
}
|
||||
});
|
||||
_listener.Start();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrackChunkResult_RecordsGuardrailCounts_ForCacheHits()
|
||||
{
|
||||
var telemetry = new AdvisoryAiTelemetry(NullLogger<AdvisoryAiTelemetry>.Instance);
|
||||
var guardrailCounts = new Dictionary<AdvisoryChunkGuardrailReason, int>
|
||||
{
|
||||
{ AdvisoryChunkGuardrailReason.BelowMinimumLength, 2 }
|
||||
};
|
||||
|
||||
telemetry.TrackChunkResult(new AdvisoryAiChunkRequestTelemetry(
|
||||
Tenant: "tenant-a",
|
||||
AdvisoryKey: "CVE-2099-0001",
|
||||
Result: "ok",
|
||||
Truncated: false,
|
||||
CacheHit: true,
|
||||
ObservationCount: 1,
|
||||
SourceCount: 1,
|
||||
ChunkCount: 1,
|
||||
Duration: TimeSpan.FromMilliseconds(5),
|
||||
GuardrailCounts: guardrailCounts));
|
||||
|
||||
_guardrailMeasurements.Should().ContainSingle();
|
||||
var measurement = _guardrailMeasurements[0];
|
||||
measurement.Value.Should().Be(2);
|
||||
measurement.Tags.Should().Contain(tag => tag.Key == "cache" && (string?)tag.Value == "hit");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_listener.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Excititor.WebService.Contracts;
|
||||
|
||||
public sealed record VexObservationProjectionResponse(
|
||||
[property: JsonPropertyName("vulnerabilityId")] string VulnerabilityId,
|
||||
[property: JsonPropertyName("productKey")] string ProductKey,
|
||||
[property: JsonPropertyName("generatedAt") ] DateTimeOffset GeneratedAt,
|
||||
[property: JsonPropertyName("totalCount")] int TotalCount,
|
||||
[property: JsonPropertyName("truncated")] bool Truncated,
|
||||
[property: JsonPropertyName("statements")] IReadOnlyList<VexObservationStatementResponse> Statements);
|
||||
|
||||
public sealed record VexObservationStatementResponse(
|
||||
[property: JsonPropertyName("observationId")] string ObservationId,
|
||||
[property: JsonPropertyName("providerId")] string ProviderId,
|
||||
[property: JsonPropertyName("status")] string Status,
|
||||
[property: JsonPropertyName("justification")] string? Justification,
|
||||
[property: JsonPropertyName("detail")] string? Detail,
|
||||
[property: JsonPropertyName("firstSeen")] DateTimeOffset FirstSeen,
|
||||
[property: JsonPropertyName("lastSeen")] DateTimeOffset LastSeen,
|
||||
[property: JsonPropertyName("scope")] VexObservationScopeResponse Scope,
|
||||
[property: JsonPropertyName("anchors")] IReadOnlyList<string> Anchors,
|
||||
[property: JsonPropertyName("document")] VexObservationDocumentResponse Document,
|
||||
[property: JsonPropertyName("signature")] VexObservationSignatureResponse? Signature);
|
||||
|
||||
public sealed record VexObservationScopeResponse(
|
||||
[property: JsonPropertyName("key")] string Key,
|
||||
[property: JsonPropertyName("name")] string? Name,
|
||||
[property: JsonPropertyName("version")] string? Version,
|
||||
[property: JsonPropertyName("purl")] string? Purl,
|
||||
[property: JsonPropertyName("cpe")] string? Cpe,
|
||||
[property: JsonPropertyName("componentIdentifiers")] IReadOnlyList<string> ComponentIdentifiers);
|
||||
|
||||
public sealed record VexObservationDocumentResponse(
|
||||
[property: JsonPropertyName("digest")] string Digest,
|
||||
[property: JsonPropertyName("format")] string Format,
|
||||
[property: JsonPropertyName("revision")] string? Revision,
|
||||
[property: JsonPropertyName("sourceUri")] string SourceUri);
|
||||
|
||||
public sealed record VexObservationSignatureResponse(
|
||||
[property: JsonPropertyName("type")] string Type,
|
||||
[property: JsonPropertyName("keyId")] string? KeyId,
|
||||
[property: JsonPropertyName("issuer")] string? Issuer,
|
||||
[property: JsonPropertyName("verifiedAt")] DateTimeOffset? VerifiedAtUtc);
|
||||
@@ -1,12 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using MongoDB.Bson;
|
||||
using StellaOps.Excititor.Core;
|
||||
using StellaOps.Excititor.Core.Aoc;
|
||||
using StellaOps.Excititor.Storage.Mongo;
|
||||
using StellaOps.Excititor.WebService.Contracts;
|
||||
using StellaOps.Excititor.WebService.Services;
|
||||
public partial class Program
|
||||
{
|
||||
private const string TenantHeaderName = "X-Stella-Tenant";
|
||||
@@ -127,4 +132,106 @@ public partial class Program
|
||||
["primaryCode"] = exception.PrimaryErrorCode,
|
||||
});
|
||||
}
|
||||
|
||||
private static ImmutableHashSet<string> BuildStringFilterSet(StringValues values)
|
||||
{
|
||||
if (values.Count == 0)
|
||||
{
|
||||
return ImmutableHashSet<string>.Empty;
|
||||
}
|
||||
|
||||
var builder = ImmutableHashSet.CreateBuilder<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
builder.Add(value.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
private static ImmutableHashSet<VexClaimStatus> BuildStatusFilter(StringValues values)
|
||||
{
|
||||
if (values.Count == 0)
|
||||
{
|
||||
return ImmutableHashSet<VexClaimStatus>.Empty;
|
||||
}
|
||||
|
||||
var builder = ImmutableHashSet.CreateBuilder<VexClaimStatus>();
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (Enum.TryParse<VexClaimStatus>(value, ignoreCase: true, out var status))
|
||||
{
|
||||
builder.Add(status);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
private static DateTimeOffset? ParseSinceTimestamp(StringValues values)
|
||||
{
|
||||
if (values.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var candidate = values[0];
|
||||
return DateTimeOffset.TryParse(candidate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsed)
|
||||
? parsed
|
||||
: null;
|
||||
}
|
||||
|
||||
private static int ResolveLimit(StringValues values, int defaultValue, int min, int max)
|
||||
{
|
||||
if (values.Count == 0)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (!int.TryParse(values[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed))
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return Math.Clamp(parsed, min, max);
|
||||
}
|
||||
|
||||
private static VexObservationStatementResponse ToResponse(VexObservationStatementProjection projection)
|
||||
{
|
||||
var scope = projection.Scope;
|
||||
var document = projection.Document;
|
||||
var signature = projection.Signature;
|
||||
|
||||
return new VexObservationStatementResponse(
|
||||
projection.ObservationId,
|
||||
projection.ProviderId,
|
||||
projection.Status.ToString().ToLowerInvariant(),
|
||||
projection.Justification?.ToString().ToLowerInvariant(),
|
||||
projection.Detail,
|
||||
projection.FirstSeen,
|
||||
projection.LastSeen,
|
||||
new VexObservationScopeResponse(
|
||||
scope.Key,
|
||||
scope.Name,
|
||||
scope.Version,
|
||||
scope.Purl,
|
||||
scope.Cpe,
|
||||
scope.ComponentIdentifiers),
|
||||
projection.Anchors,
|
||||
new VexObservationDocumentResponse(
|
||||
document.Digest,
|
||||
document.Format.ToString().ToLowerInvariant(),
|
||||
document.Revision,
|
||||
document.SourceUri.ToString()),
|
||||
signature is null
|
||||
? null
|
||||
: new VexObservationSignatureResponse(
|
||||
signature.Type,
|
||||
signature.KeyId,
|
||||
signature.Issuer,
|
||||
signature.VerifiedAt));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Collections.Immutable;
|
||||
@@ -5,7 +6,10 @@ using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using StellaOps.Excititor.Attestation.Verification;
|
||||
using StellaOps.Excititor.Attestation.Extensions;
|
||||
using StellaOps.Excititor.Attestation;
|
||||
@@ -51,9 +55,11 @@ services.AddVexAttestation();
|
||||
services.Configure<VexAttestationClientOptions>(configuration.GetSection("Excititor:Attestation:Client"));
|
||||
services.Configure<VexAttestationVerificationOptions>(configuration.GetSection("Excititor:Attestation:Verification"));
|
||||
services.AddVexPolicy();
|
||||
services.AddRedHatCsafConnector();
|
||||
services.Configure<MirrorDistributionOptions>(configuration.GetSection(MirrorDistributionOptions.SectionName));
|
||||
services.AddSingleton<MirrorRateLimiter>();
|
||||
services.AddRedHatCsafConnector();
|
||||
services.Configure<MirrorDistributionOptions>(configuration.GetSection(MirrorDistributionOptions.SectionName));
|
||||
services.AddSingleton<MirrorRateLimiter>();
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
services.AddSingleton<IVexObservationProjectionService, VexObservationProjectionService>();
|
||||
|
||||
var rekorSection = configuration.GetSection("Excititor:Attestation:Rekor");
|
||||
if (rekorSection.Exists())
|
||||
@@ -434,6 +440,60 @@ app.MapGet("/vex/raw/{digest}/provenance", async (
|
||||
return Results.Json(response);
|
||||
});
|
||||
|
||||
app.MapGet("/v1/vex/observations/{vulnerabilityId}/{productKey}", async (
|
||||
HttpContext context,
|
||||
string vulnerabilityId,
|
||||
string productKey,
|
||||
[FromServices] IVexObservationProjectionService projectionService,
|
||||
[FromServices] IOptions<VexMongoStorageOptions> storageOptions,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out var tenant, out var tenantError))
|
||||
{
|
||||
return tenantError;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(vulnerabilityId) || string.IsNullOrWhiteSpace(productKey))
|
||||
{
|
||||
return ValidationProblem("vulnerabilityId and productKey are required.");
|
||||
}
|
||||
|
||||
var providerFilter = BuildStringFilterSet(context.Request.Query["providerId"]);
|
||||
var statusFilter = BuildStatusFilter(context.Request.Query["status"]);
|
||||
var since = ParseSinceTimestamp(context.Request.Query["since"]);
|
||||
var limit = ResolveLimit(context.Request.Query["limit"], defaultValue: 200, min: 1, max: 500);
|
||||
|
||||
var request = new VexObservationProjectionRequest(
|
||||
tenant,
|
||||
vulnerabilityId.Trim(),
|
||||
productKey.Trim(),
|
||||
providerFilter,
|
||||
statusFilter,
|
||||
since,
|
||||
limit);
|
||||
|
||||
var result = await projectionService.QueryAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
var statements = result.Statements
|
||||
.Select(ToResponse)
|
||||
.ToList();
|
||||
|
||||
var response = new VexObservationProjectionResponse(
|
||||
request.VulnerabilityId,
|
||||
request.ProductKey,
|
||||
result.GeneratedAtUtc,
|
||||
result.TotalCount,
|
||||
result.Truncated,
|
||||
statements);
|
||||
|
||||
return Results.Json(response);
|
||||
});
|
||||
|
||||
app.MapPost("/aoc/verify", async (
|
||||
HttpContext context,
|
||||
VexAocVerifyRequest? request,
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Excititor.Core;
|
||||
|
||||
namespace StellaOps.Excititor.WebService.Services;
|
||||
|
||||
internal interface IVexObservationProjectionService
|
||||
{
|
||||
Task<VexObservationProjectionResult> QueryAsync(
|
||||
VexObservationProjectionRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
internal sealed record VexObservationProjectionRequest(
|
||||
string Tenant,
|
||||
string VulnerabilityId,
|
||||
string ProductKey,
|
||||
ImmutableHashSet<string> ProviderIds,
|
||||
ImmutableHashSet<VexClaimStatus> Statuses,
|
||||
DateTimeOffset? Since,
|
||||
int Limit);
|
||||
|
||||
internal sealed record VexObservationProjectionResult(
|
||||
IReadOnlyList<VexObservationStatementProjection> Statements,
|
||||
bool Truncated,
|
||||
int TotalCount,
|
||||
DateTimeOffset GeneratedAtUtc);
|
||||
|
||||
internal sealed record VexObservationStatementProjection(
|
||||
string ObservationId,
|
||||
string ProviderId,
|
||||
VexClaimStatus Status,
|
||||
VexJustification? Justification,
|
||||
string? Detail,
|
||||
DateTimeOffset FirstSeen,
|
||||
DateTimeOffset LastSeen,
|
||||
VexProductScope Scope,
|
||||
IReadOnlyList<string> Anchors,
|
||||
VexClaimDocument Document,
|
||||
VexSignatureMetadata? Signature);
|
||||
|
||||
internal sealed record VexProductScope(
|
||||
string Key,
|
||||
string? Name,
|
||||
string? Version,
|
||||
string? Purl,
|
||||
string? Cpe,
|
||||
IReadOnlyList<string> ComponentIdentifiers);
|
||||
|
||||
internal sealed class VexObservationProjectionService : IVexObservationProjectionService
|
||||
{
|
||||
private static readonly string[] AnchorKeys =
|
||||
{
|
||||
"json_pointer",
|
||||
"jsonPointer",
|
||||
"statement_locator",
|
||||
"locator",
|
||||
"paragraph",
|
||||
"section",
|
||||
"path"
|
||||
};
|
||||
|
||||
private readonly IVexClaimStore _claimStore;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public VexObservationProjectionService(IVexClaimStore claimStore, TimeProvider? timeProvider = null)
|
||||
{
|
||||
_claimStore = claimStore ?? throw new ArgumentNullException(nameof(claimStore));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<VexObservationProjectionResult> QueryAsync(
|
||||
VexObservationProjectionRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var claims = await _claimStore.FindAsync(
|
||||
request.VulnerabilityId,
|
||||
request.ProductKey,
|
||||
request.Since,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var filtered = claims
|
||||
.Where(claim => MatchesProvider(claim, request.ProviderIds))
|
||||
.Where(claim => MatchesStatus(claim, request.Statuses))
|
||||
.OrderByDescending(claim => claim.LastSeen)
|
||||
.ThenBy(claim => claim.ProviderId, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
var total = filtered.Count;
|
||||
var page = filtered.Take(request.Limit).ToList();
|
||||
var statements = page
|
||||
.Select(claim => MapClaim(claim))
|
||||
.ToList();
|
||||
|
||||
return new VexObservationProjectionResult(
|
||||
statements,
|
||||
total > request.Limit,
|
||||
total,
|
||||
_timeProvider.GetUtcNow());
|
||||
}
|
||||
|
||||
private static bool MatchesProvider(VexClaim claim, ImmutableHashSet<string> providers)
|
||||
=> providers.Count == 0 || providers.Contains(claim.ProviderId, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static bool MatchesStatus(VexClaim claim, ImmutableHashSet<VexClaimStatus> statuses)
|
||||
=> statuses.Count == 0 || statuses.Contains(claim.Status);
|
||||
|
||||
private static VexObservationStatementProjection MapClaim(VexClaim claim)
|
||||
{
|
||||
var observationId = string.Create(CultureInfo.InvariantCulture, $"{claim.ProviderId}:{claim.Document.Digest}");
|
||||
var anchors = ExtractAnchors(claim.AdditionalMetadata);
|
||||
var scope = new VexProductScope(
|
||||
claim.Product.Key,
|
||||
claim.Product.Name,
|
||||
claim.Product.Version,
|
||||
claim.Product.Purl,
|
||||
claim.Product.Cpe,
|
||||
claim.Product.ComponentIdentifiers);
|
||||
|
||||
return new VexObservationStatementProjection(
|
||||
observationId,
|
||||
claim.ProviderId,
|
||||
claim.Status,
|
||||
claim.Justification,
|
||||
claim.Detail,
|
||||
claim.FirstSeen,
|
||||
claim.LastSeen,
|
||||
scope,
|
||||
anchors,
|
||||
claim.Document,
|
||||
claim.Document.Signature);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> ExtractAnchors(ImmutableSortedDictionary<string, string> metadata)
|
||||
{
|
||||
if (metadata.Count == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var anchors = new List<string>();
|
||||
foreach (var key in AnchorKeys)
|
||||
{
|
||||
if (metadata.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
anchors.Add(value.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
return anchors.Count == 0 ? Array.Empty<string>() : anchors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Excititor.Core;
|
||||
using StellaOps.Excititor.Storage.Mongo;
|
||||
using StellaOps.Excititor.WebService.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Excititor.WebService.Tests;
|
||||
|
||||
public sealed class VexObservationProjectionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task QueryAsync_FiltersByProviderAndStatus()
|
||||
{
|
||||
var now = new DateTimeOffset(2025, 11, 10, 12, 0, 0, TimeSpan.Zero);
|
||||
var claims = new[]
|
||||
{
|
||||
CreateClaim("provider-a", VexClaimStatus.Affected, now.AddHours(-6), now.AddHours(-5)),
|
||||
CreateClaim("provider-b", VexClaimStatus.NotAffected, now.AddHours(-4), now.AddHours(-3))
|
||||
};
|
||||
|
||||
var store = new FakeClaimStore(claims);
|
||||
var service = new VexObservationProjectionService(store, new FixedTimeProvider(now));
|
||||
var request = new VexObservationProjectionRequest(
|
||||
Tenant: "tenant-a",
|
||||
VulnerabilityId: "CVE-2025-0001",
|
||||
ProductKey: "pkg:docker/demo",
|
||||
ProviderIds: ImmutableHashSet.Create("provider-b"),
|
||||
Statuses: ImmutableHashSet.Create(VexClaimStatus.NotAffected),
|
||||
Since: null,
|
||||
Limit: 10);
|
||||
|
||||
var result = await service.QueryAsync(request, CancellationToken.None);
|
||||
|
||||
result.Truncated.Should().BeFalse();
|
||||
result.TotalCount.Should().Be(1);
|
||||
result.GeneratedAtUtc.Should().Be(now);
|
||||
var statement = result.Statements.Single();
|
||||
statement.ProviderId.Should().Be("provider-b");
|
||||
statement.Status.Should().Be(VexClaimStatus.NotAffected);
|
||||
statement.Justification.Should().Be(VexJustification.ComponentNotPresent);
|
||||
statement.Anchors.Should().ContainSingle().Which.Should().Be("/statements/0");
|
||||
statement.Scope.ComponentIdentifiers.Should().Contain("demo:component");
|
||||
statement.Document.Digest.Should().Contain("provider-b");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryAsync_TruncatesWhenLimitExceeded()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var claims = Enumerable.Range(0, 3)
|
||||
.Select(index => CreateClaim($"provider-{index}", VexClaimStatus.NotAffected, now.AddHours(-index - 2), now.AddHours(-index - 1)))
|
||||
.ToArray();
|
||||
|
||||
var store = new FakeClaimStore(claims);
|
||||
var service = new VexObservationProjectionService(store, new FixedTimeProvider(now));
|
||||
var request = new VexObservationProjectionRequest(
|
||||
Tenant: "tenant-a",
|
||||
VulnerabilityId: "CVE-2025-0001",
|
||||
ProductKey: "pkg:docker/demo",
|
||||
ProviderIds: ImmutableHashSet<string>.Empty,
|
||||
Statuses: ImmutableHashSet<VexClaimStatus>.Empty,
|
||||
Since: null,
|
||||
Limit: 2);
|
||||
|
||||
var result = await service.QueryAsync(request, CancellationToken.None);
|
||||
|
||||
result.Truncated.Should().BeTrue();
|
||||
result.TotalCount.Should().Be(3);
|
||||
result.Statements.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
private static VexClaim CreateClaim(string providerId, VexClaimStatus status, DateTimeOffset firstSeen, DateTimeOffset lastSeen)
|
||||
{
|
||||
var product = new VexProduct(
|
||||
key: "pkg:docker/demo",
|
||||
name: "demo",
|
||||
version: "1.0.0",
|
||||
purl: "pkg:docker/demo@1.0.0",
|
||||
cpe: "cpe:/a:demo:demo:1.0.0",
|
||||
componentIdentifiers: new[] { "demo:component" });
|
||||
|
||||
var document = new VexClaimDocument(
|
||||
VexDocumentFormat.Csaf,
|
||||
$"sha256:{providerId}",
|
||||
new Uri("https://example.org/vex.json"),
|
||||
revision: "v1");
|
||||
|
||||
var metadata = ImmutableDictionary<string, string>.Empty.Add("json_pointer", "/statements/0");
|
||||
|
||||
return new VexClaim(
|
||||
"CVE-2025-0001",
|
||||
providerId,
|
||||
product,
|
||||
status,
|
||||
document,
|
||||
firstSeen,
|
||||
lastSeen,
|
||||
justification: VexJustification.ComponentNotPresent,
|
||||
detail: "not affected",
|
||||
confidence: null,
|
||||
signals: null,
|
||||
additionalMetadata: metadata);
|
||||
}
|
||||
|
||||
private sealed class FakeClaimStore : IVexClaimStore
|
||||
{
|
||||
private readonly IReadOnlyCollection<VexClaim> _claims;
|
||||
|
||||
public FakeClaimStore(IReadOnlyCollection<VexClaim> claims)
|
||||
{
|
||||
_claims = claims;
|
||||
}
|
||||
|
||||
public ValueTask AppendAsync(IEnumerable<VexClaim> claims, DateTimeOffset observedAt, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public ValueTask<IReadOnlyCollection<VexClaim>> FindAsync(string vulnerabilityId, string productKey, DateTimeOffset? since, CancellationToken cancellationToken, IClientSessionHandle? session = null)
|
||||
{
|
||||
var query = _claims
|
||||
.Where(claim => string.Equals(claim.VulnerabilityId, vulnerabilityId, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(claim => string.Equals(claim.Product.Key, productKey, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (since.HasValue)
|
||||
{
|
||||
query = query.Where(claim => claim.LastSeen >= since.Value);
|
||||
}
|
||||
|
||||
return ValueTask.FromResult<IReadOnlyCollection<VexClaim>>(query.ToList());
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
private readonly DateTimeOffset _timestamp;
|
||||
|
||||
public FixedTimeProvider(DateTimeOffset timestamp)
|
||||
{
|
||||
_timestamp = timestamp;
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _timestamp;
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,5 @@
|
||||
| 4 | `SCANNER-ANALYZERS-DENO-26-004` | DONE | Permission/capability analyzer for FS/net/env/process/crypto/FFI/workers plus dynamic import heuristics with reason codes. |
|
||||
| 5 | `SCANNER-ANALYZERS-DENO-26-005` | DONE | Bundle/binary inspectors for eszip and `deno compile` executables to recover graphs/config/resources/snapshots. |
|
||||
| 6 | `SCANNER-ANALYZERS-DENO-26-006` | DONE | OCI/container adapter that stitches per-layer Deno caches, vendor trees, and compiled binaries into provenance-aware inputs. |
|
||||
| 7 | `SCANNER-ANALYZERS-DENO-26-007` | DOING | AOC-compliant observation writers (entrypoints, modules, capability edges, workers, warnings, binaries) with deterministic reason codes. |
|
||||
| 8 | `SCANNER-ANALYZERS-DENO-26-008` | TODO | Fixture and benchmark suite for vendor/npm/FFI/worker/dynamic import/bundle/cache/container cases. |
|
||||
| 7 | `SCANNER-ANALYZERS-DENO-26-007` | DONE | AOC-compliant observation writers (entrypoints, modules, capability edges, workers, warnings, binaries) with deterministic reason codes. |
|
||||
| 8 | `SCANNER-ANALYZERS-DENO-26-008` | DONE | Fixture and benchmark suite for vendor/npm/FFI/worker/dynamic import/bundle/cache/container cases. |
|
||||
|
||||
@@ -113,6 +113,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__Benchmarks", "__Benchmark
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Rust.Benchmarks", "__Benchmarks\StellaOps.Scanner.Analyzers.Lang.Rust.Benchmarks\StellaOps.Scanner.Analyzers.Lang.Rust.Benchmarks.csproj", "{E76AE786-599B-434C-8E52-1B1211768386}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks", "__Benchmarks\StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks\StellaOps.Scanner.Analyzers.Lang.Deno.Benchmarks.csproj", "{37E2DB38-F316-4A0E-968C-1381A3DABD6F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.Validation", "__Libraries\StellaOps.Scanner.Surface.Validation\StellaOps.Scanner.Surface.Validation.csproj", "{B6C4BB91-BC9F-4F5F-904F-9B19C80D4E4A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Surface.FS", "__Libraries\StellaOps.Scanner.Surface.FS\StellaOps.Scanner.Surface.FS.csproj", "{B2597D13-8733-4F20-B157-B4B5D36FB59A}"
|
||||
@@ -125,6 +127,26 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Deno.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Deno.Tests\StellaOps.Scanner.Analyzers.Lang.Deno.Tests.csproj", "{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Scanner.Analyzers.Lang.Ruby.Tests", "__Tests\StellaOps.Scanner.Analyzers.Lang.Ruby.Tests\StellaOps.Scanner.Analyzers.Lang.Ruby.Tests.csproj", "{E0104A8E-2C39-48C1-97EC-66C171310944}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Testing", "..\Concelier\__Libraries\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj", "{9724C2EE-7351-41A3-A874-0856CF406E04}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Connector.Common", "..\Concelier\__Libraries\StellaOps.Concelier.Connector.Common\StellaOps.Concelier.Connector.Common.csproj", "{09F93E81-05B5-46CB-818D-BDD2812CCF71}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Storage.Mongo", "..\Concelier\__Libraries\StellaOps.Concelier.Storage.Mongo\StellaOps.Concelier.Storage.Mongo.csproj", "{87E9CDA0-F6EB-4D7F-85E1-0C9288E2717C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Core", "..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj", "{9CBE8002-B289-4A86-91C9-5CD405149B2A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Models", "..\Concelier\__Libraries\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj", "{9A16F25A-99B9-4082-85AD-C5F2224B90C3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.RawModels", "..\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj", "{06B9A55F-BB97-4163-BCCF-DF5F3CEC46DA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Normalization", "..\Concelier\__Libraries\StellaOps.Concelier.Normalization\StellaOps.Concelier.Normalization.csproj", "{C5281EB5-7985-4431-A29D-EBB2D94792DC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Ingestion.Telemetry", "..\__Libraries\StellaOps.Ingestion.Telemetry\StellaOps.Ingestion.Telemetry.csproj", "{2DF6D629-9FF0-4813-903A-AF1454A625EA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc", "..\Aoc\__Libraries\StellaOps.Aoc\StellaOps.Aoc.csproj", "{8237425A-933A-440E-AE6B-1DF57F228681}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -207,30 +229,6 @@ Global
|
||||
{02C16715-9BF3-43D7-AC97-D6940365907A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{02C16715-9BF3-43D7-AC97-D6940365907A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{02C16715-9BF3-43D7-AC97-D6940365907A}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B53FEE71-9EBE-4479-9B07-0C3F8EA2C02E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B53FEE71-9EBE-4479-9B07-0C3F8EA2C02E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B53FEE71-9EBE-4479-9B07-0C3F8EA2C02E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
@@ -783,6 +781,18 @@ Global
|
||||
{E76AE786-599B-434C-8E52-1B1211768386}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E76AE786-599B-434C-8E52-1B1211768386}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E76AE786-599B-434C-8E52-1B1211768386}.Release|x86.Build.0 = Release|Any CPU
|
||||
{37E2DB38-F316-4A0E-968C-1381A3DABD6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{37E2DB38-F316-4A0E-968C-1381A3DABD6F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{37E2DB38-F316-4A0E-968C-1381A3DABD6F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{37E2DB38-F316-4A0E-968C-1381A3DABD6F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{37E2DB38-F316-4A0E-968C-1381A3DABD6F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{37E2DB38-F316-4A0E-968C-1381A3DABD6F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{37E2DB38-F316-4A0E-968C-1381A3DABD6F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{37E2DB38-F316-4A0E-968C-1381A3DABD6F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{37E2DB38-F316-4A0E-968C-1381A3DABD6F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{37E2DB38-F316-4A0E-968C-1381A3DABD6F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{37E2DB38-F316-4A0E-968C-1381A3DABD6F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{37E2DB38-F316-4A0E-968C-1381A3DABD6F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B6C4BB91-BC9F-4F5F-904F-9B19C80D4E4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B6C4BB91-BC9F-4F5F-904F-9B19C80D4E4A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B6C4BB91-BC9F-4F5F-904F-9B19C80D4E4A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
@@ -831,6 +841,150 @@ Global
|
||||
{482026BC-2E89-4789-8A73-523FAAC8476F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{482026BC-2E89-4789-8A73-523FAAC8476F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{482026BC-2E89-4789-8A73-523FAAC8476F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C71D4A4C-637C-4A7C-B0F8-4F9E62FBBE3A}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B8D28D0E-FAD8-48B8-8F9C-9E1C6582F19E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E0104A8E-2C39-48C1-97EC-66C171310944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E0104A8E-2C39-48C1-97EC-66C171310944}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E0104A8E-2C39-48C1-97EC-66C171310944}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E0104A8E-2C39-48C1-97EC-66C171310944}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E0104A8E-2C39-48C1-97EC-66C171310944}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E0104A8E-2C39-48C1-97EC-66C171310944}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E0104A8E-2C39-48C1-97EC-66C171310944}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E0104A8E-2C39-48C1-97EC-66C171310944}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E0104A8E-2C39-48C1-97EC-66C171310944}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E0104A8E-2C39-48C1-97EC-66C171310944}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E0104A8E-2C39-48C1-97EC-66C171310944}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E0104A8E-2C39-48C1-97EC-66C171310944}.Release|x86.Build.0 = Release|Any CPU
|
||||
{9724C2EE-7351-41A3-A874-0856CF406E04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9724C2EE-7351-41A3-A874-0856CF406E04}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9724C2EE-7351-41A3-A874-0856CF406E04}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{9724C2EE-7351-41A3-A874-0856CF406E04}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{9724C2EE-7351-41A3-A874-0856CF406E04}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{9724C2EE-7351-41A3-A874-0856CF406E04}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{9724C2EE-7351-41A3-A874-0856CF406E04}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9724C2EE-7351-41A3-A874-0856CF406E04}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9724C2EE-7351-41A3-A874-0856CF406E04}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{9724C2EE-7351-41A3-A874-0856CF406E04}.Release|x64.Build.0 = Release|Any CPU
|
||||
{9724C2EE-7351-41A3-A874-0856CF406E04}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{9724C2EE-7351-41A3-A874-0856CF406E04}.Release|x86.Build.0 = Release|Any CPU
|
||||
{09F93E81-05B5-46CB-818D-BDD2812CCF71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{09F93E81-05B5-46CB-818D-BDD2812CCF71}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{09F93E81-05B5-46CB-818D-BDD2812CCF71}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{09F93E81-05B5-46CB-818D-BDD2812CCF71}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{09F93E81-05B5-46CB-818D-BDD2812CCF71}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{09F93E81-05B5-46CB-818D-BDD2812CCF71}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{09F93E81-05B5-46CB-818D-BDD2812CCF71}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{09F93E81-05B5-46CB-818D-BDD2812CCF71}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{09F93E81-05B5-46CB-818D-BDD2812CCF71}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{09F93E81-05B5-46CB-818D-BDD2812CCF71}.Release|x64.Build.0 = Release|Any CPU
|
||||
{09F93E81-05B5-46CB-818D-BDD2812CCF71}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{09F93E81-05B5-46CB-818D-BDD2812CCF71}.Release|x86.Build.0 = Release|Any CPU
|
||||
{87E9CDA0-F6EB-4D7F-85E1-0C9288E2717C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{87E9CDA0-F6EB-4D7F-85E1-0C9288E2717C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{87E9CDA0-F6EB-4D7F-85E1-0C9288E2717C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{87E9CDA0-F6EB-4D7F-85E1-0C9288E2717C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{87E9CDA0-F6EB-4D7F-85E1-0C9288E2717C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{87E9CDA0-F6EB-4D7F-85E1-0C9288E2717C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{87E9CDA0-F6EB-4D7F-85E1-0C9288E2717C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{87E9CDA0-F6EB-4D7F-85E1-0C9288E2717C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{87E9CDA0-F6EB-4D7F-85E1-0C9288E2717C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{87E9CDA0-F6EB-4D7F-85E1-0C9288E2717C}.Release|x64.Build.0 = Release|Any CPU
|
||||
{87E9CDA0-F6EB-4D7F-85E1-0C9288E2717C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{87E9CDA0-F6EB-4D7F-85E1-0C9288E2717C}.Release|x86.Build.0 = Release|Any CPU
|
||||
{9CBE8002-B289-4A86-91C9-5CD405149B2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9CBE8002-B289-4A86-91C9-5CD405149B2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9CBE8002-B289-4A86-91C9-5CD405149B2A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{9CBE8002-B289-4A86-91C9-5CD405149B2A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{9CBE8002-B289-4A86-91C9-5CD405149B2A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{9CBE8002-B289-4A86-91C9-5CD405149B2A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{9CBE8002-B289-4A86-91C9-5CD405149B2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9CBE8002-B289-4A86-91C9-5CD405149B2A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9CBE8002-B289-4A86-91C9-5CD405149B2A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{9CBE8002-B289-4A86-91C9-5CD405149B2A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{9CBE8002-B289-4A86-91C9-5CD405149B2A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{9CBE8002-B289-4A86-91C9-5CD405149B2A}.Release|x86.Build.0 = Release|Any CPU
|
||||
{9A16F25A-99B9-4082-85AD-C5F2224B90C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9A16F25A-99B9-4082-85AD-C5F2224B90C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9A16F25A-99B9-4082-85AD-C5F2224B90C3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{9A16F25A-99B9-4082-85AD-C5F2224B90C3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{9A16F25A-99B9-4082-85AD-C5F2224B90C3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{9A16F25A-99B9-4082-85AD-C5F2224B90C3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{9A16F25A-99B9-4082-85AD-C5F2224B90C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9A16F25A-99B9-4082-85AD-C5F2224B90C3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9A16F25A-99B9-4082-85AD-C5F2224B90C3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{9A16F25A-99B9-4082-85AD-C5F2224B90C3}.Release|x64.Build.0 = Release|Any CPU
|
||||
{9A16F25A-99B9-4082-85AD-C5F2224B90C3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{9A16F25A-99B9-4082-85AD-C5F2224B90C3}.Release|x86.Build.0 = Release|Any CPU
|
||||
{06B9A55F-BB97-4163-BCCF-DF5F3CEC46DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{06B9A55F-BB97-4163-BCCF-DF5F3CEC46DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{06B9A55F-BB97-4163-BCCF-DF5F3CEC46DA}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{06B9A55F-BB97-4163-BCCF-DF5F3CEC46DA}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{06B9A55F-BB97-4163-BCCF-DF5F3CEC46DA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{06B9A55F-BB97-4163-BCCF-DF5F3CEC46DA}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{06B9A55F-BB97-4163-BCCF-DF5F3CEC46DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{06B9A55F-BB97-4163-BCCF-DF5F3CEC46DA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{06B9A55F-BB97-4163-BCCF-DF5F3CEC46DA}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{06B9A55F-BB97-4163-BCCF-DF5F3CEC46DA}.Release|x64.Build.0 = Release|Any CPU
|
||||
{06B9A55F-BB97-4163-BCCF-DF5F3CEC46DA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{06B9A55F-BB97-4163-BCCF-DF5F3CEC46DA}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C5281EB5-7985-4431-A29D-EBB2D94792DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C5281EB5-7985-4431-A29D-EBB2D94792DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C5281EB5-7985-4431-A29D-EBB2D94792DC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C5281EB5-7985-4431-A29D-EBB2D94792DC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C5281EB5-7985-4431-A29D-EBB2D94792DC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C5281EB5-7985-4431-A29D-EBB2D94792DC}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C5281EB5-7985-4431-A29D-EBB2D94792DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C5281EB5-7985-4431-A29D-EBB2D94792DC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C5281EB5-7985-4431-A29D-EBB2D94792DC}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C5281EB5-7985-4431-A29D-EBB2D94792DC}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C5281EB5-7985-4431-A29D-EBB2D94792DC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C5281EB5-7985-4431-A29D-EBB2D94792DC}.Release|x86.Build.0 = Release|Any CPU
|
||||
{2DF6D629-9FF0-4813-903A-AF1454A625EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2DF6D629-9FF0-4813-903A-AF1454A625EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2DF6D629-9FF0-4813-903A-AF1454A625EA}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{2DF6D629-9FF0-4813-903A-AF1454A625EA}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{2DF6D629-9FF0-4813-903A-AF1454A625EA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{2DF6D629-9FF0-4813-903A-AF1454A625EA}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{2DF6D629-9FF0-4813-903A-AF1454A625EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2DF6D629-9FF0-4813-903A-AF1454A625EA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2DF6D629-9FF0-4813-903A-AF1454A625EA}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{2DF6D629-9FF0-4813-903A-AF1454A625EA}.Release|x64.Build.0 = Release|Any CPU
|
||||
{2DF6D629-9FF0-4813-903A-AF1454A625EA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{2DF6D629-9FF0-4813-903A-AF1454A625EA}.Release|x86.Build.0 = Release|Any CPU
|
||||
{8237425A-933A-440E-AE6B-1DF57F228681}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8237425A-933A-440E-AE6B-1DF57F228681}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8237425A-933A-440E-AE6B-1DF57F228681}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{8237425A-933A-440E-AE6B-1DF57F228681}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{8237425A-933A-440E-AE6B-1DF57F228681}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8237425A-933A-440E-AE6B-1DF57F228681}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8237425A-933A-440E-AE6B-1DF57F228681}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8237425A-933A-440E-AE6B-1DF57F228681}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8237425A-933A-440E-AE6B-1DF57F228681}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8237425A-933A-440E-AE6B-1DF57F228681}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8237425A-933A-440E-AE6B-1DF57F228681}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8237425A-933A-440E-AE6B-1DF57F228681}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -872,9 +1026,11 @@ Global
|
||||
{782652F5-A7C3-4070-8B42-F7DC2C17973E} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
|
||||
{51CAC6CD-ED38-4AFC-AE81-84A4BDD45DB2} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
|
||||
{E76AE786-599B-434C-8E52-1B1211768386} = {7FECE895-ECB6-33CE-12BE-877282A67F5D}
|
||||
{37E2DB38-F316-4A0E-968C-1381A3DABD6F} = {7FECE895-ECB6-33CE-12BE-877282A67F5D}
|
||||
{B6C4BB91-BC9F-4F5F-904F-9B19C80D4E4A} = {41F15E67-7190-CF23-3BC4-77E87134CADD}
|
||||
{B2597D13-8733-4F20-B157-B4B5D36FB59A} = {41F15E67-7190-CF23-3BC4-77E87134CADD}
|
||||
{C2B2B38A-D67D-429E-BB2E-023E25EBD7D3} = {41F15E67-7190-CF23-3BC4-77E87134CADD}
|
||||
{482026BC-2E89-4789-8A73-523FAAC8476F} = {41F15E67-7190-CF23-3BC4-77E87134CADD}
|
||||
{E0104A8E-2C39-48C1-97EC-66C171310944} = {56BCE1BF-7CBA-7CE8-203D-A88051F1D642}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Internal;
|
||||
|
||||
internal sealed class DenoConfigDocument
|
||||
@@ -12,7 +15,8 @@ internal sealed class DenoConfigDocument
|
||||
bool vendorEnabled,
|
||||
string? vendorDirectoryPath,
|
||||
bool nodeModulesDirEnabled,
|
||||
string? nodeModulesDir)
|
||||
string? nodeModulesDir,
|
||||
ImmutableArray<string> entrypoints)
|
||||
{
|
||||
AbsolutePath = Path.GetFullPath(absolutePath);
|
||||
RelativePath = DenoPathUtilities.NormalizeRelativePath(relativePath);
|
||||
@@ -25,6 +29,7 @@ internal sealed class DenoConfigDocument
|
||||
VendorDirectoryPath = vendorDirectoryPath;
|
||||
NodeModulesDirEnabled = nodeModulesDirEnabled;
|
||||
NodeModulesDirectory = nodeModulesDir;
|
||||
Entrypoints = entrypoints;
|
||||
}
|
||||
|
||||
public string AbsolutePath { get; }
|
||||
@@ -49,6 +54,8 @@ internal sealed class DenoConfigDocument
|
||||
|
||||
public string? NodeModulesDirectory { get; }
|
||||
|
||||
public ImmutableArray<string> Entrypoints { get; }
|
||||
|
||||
public static bool TryLoad(
|
||||
string absolutePath,
|
||||
string relativePath,
|
||||
@@ -80,6 +87,7 @@ internal sealed class DenoConfigDocument
|
||||
var (lockEnabled, lockFilePath) = ResolveLockPath(root, directory);
|
||||
var (vendorEnabled, vendorDirectory) = ResolveVendorDirectory(root, directory);
|
||||
var (nodeModulesDirEnabled, nodeModulesDir) = ResolveNodeModulesDirectory(root, directory);
|
||||
var entrypoints = ResolveEntrypoints(root, directory);
|
||||
|
||||
document = new DenoConfigDocument(
|
||||
absolutePath,
|
||||
@@ -91,7 +99,8 @@ internal sealed class DenoConfigDocument
|
||||
vendorEnabled,
|
||||
vendorDirectory,
|
||||
nodeModulesDirEnabled,
|
||||
nodeModulesDir);
|
||||
nodeModulesDir,
|
||||
entrypoints);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -192,6 +201,79 @@ internal sealed class DenoConfigDocument
|
||||
return results;
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> ResolveEntrypoints(JsonElement root, string directory)
|
||||
{
|
||||
if (!root.TryGetProperty("tasks", out var tasksElement) || tasksElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
var builder = ImmutableArray.CreateBuilder<string>();
|
||||
|
||||
foreach (var task in tasksElement.EnumerateObject())
|
||||
{
|
||||
if (task.Value.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var command = task.Value.GetString();
|
||||
if (string.IsNullOrWhiteSpace(command))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var candidate in ExtractEntrypointCandidates(command))
|
||||
{
|
||||
var normalized = NormalizeEntrypoint(directory, candidate);
|
||||
if (!string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
builder.Add(normalized!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder
|
||||
.Where(static entry => !string.IsNullOrWhiteSpace(entry))
|
||||
.Select(static entry => entry!)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(static entry => entry, StringComparer.OrdinalIgnoreCase)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ExtractEntrypointCandidates(string command)
|
||||
{
|
||||
foreach (Match match in EntrypointRegex.Matches(command ?? string.Empty))
|
||||
{
|
||||
var path = match.Groups["path"].Value;
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
yield return path.Trim('"', '\'');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? NormalizeEntrypoint(string directory, string candidate)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(candidate))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string fullPath = Path.IsPathFullyQualified(candidate)
|
||||
? candidate
|
||||
: Path.Combine(directory, candidate);
|
||||
|
||||
fullPath = Path.GetFullPath(fullPath);
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var relative = Path.GetRelativePath(directory, fullPath);
|
||||
return DenoPathUtilities.NormalizeRelativePath(relative);
|
||||
}
|
||||
|
||||
private static (bool Enabled, string? Path) ResolveLockPath(JsonElement root, string directory)
|
||||
{
|
||||
if (!root.TryGetProperty("lock", out var lockElement))
|
||||
@@ -327,4 +409,8 @@ internal sealed class DenoConfigDocument
|
||||
_ => (false, null),
|
||||
};
|
||||
}
|
||||
|
||||
private static readonly Regex EntrypointRegex = new(
|
||||
@"(?<path>(?:\.\.?/|/)[^""'\s]+?\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs))",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,18 @@ internal static class DenoModuleGraphResolver
|
||||
foreach (var config in workspace.Configurations)
|
||||
{
|
||||
_cancellationToken.ThrowIfCancellationRequested();
|
||||
var metadata = new Dictionary<string, string?>(StringComparer.Ordinal)
|
||||
{
|
||||
["vendor.enabled"] = config.VendorEnabled.ToString(CultureInfo.InvariantCulture),
|
||||
["lock.enabled"] = config.LockEnabled.ToString(CultureInfo.InvariantCulture),
|
||||
["nodeModules.enabled"] = config.NodeModulesDirEnabled.ToString(CultureInfo.InvariantCulture),
|
||||
};
|
||||
|
||||
if (config.Entrypoints.Length > 0)
|
||||
{
|
||||
metadata["entrypoints"] = string.Join(";", config.Entrypoints);
|
||||
}
|
||||
|
||||
var configNodeId = GetOrAddNode(
|
||||
$"config::{config.RelativePath}",
|
||||
config.RelativePath,
|
||||
@@ -45,12 +57,7 @@ internal static class DenoModuleGraphResolver
|
||||
config.AbsolutePath,
|
||||
layerDigest: null,
|
||||
integrity: null,
|
||||
metadata: new Dictionary<string, string?>()
|
||||
{
|
||||
["vendor.enabled"] = config.VendorEnabled.ToString(CultureInfo.InvariantCulture),
|
||||
["lock.enabled"] = config.LockEnabled.ToString(CultureInfo.InvariantCulture),
|
||||
["nodeModules.enabled"] = config.NodeModulesDirEnabled.ToString(CultureInfo.InvariantCulture),
|
||||
});
|
||||
metadata: metadata);
|
||||
|
||||
if (config.ImportMapPath is not null)
|
||||
{
|
||||
|
||||
@@ -14,6 +14,9 @@ internal static class DenoNpmCompatibilityAdapter
|
||||
|
||||
private static readonly Regex DynamicImportRegex = new(@"import\s*\(\s*['""](?<url>https?://[^'""]+)['""]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex LiteralFetchRegex = new(@"fetch\s*\(\s*['""](?<url>https?://[^'""]+)['""]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex DynamicImportIdentifierRegex = new(@"import\s*\(\s*(?<identifier>[A-Za-z_][A-Za-z0-9_]*)\s*\)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex LiteralFetchIdentifierRegex = new(@"fetch\s*\(\s*(?<identifier>[A-Za-z_][A-Za-z0-9_]*)\s*\)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex LiteralAssignmentRegex = new(@"(?:(?:const|let|var)\s+)(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*['""](?<url>https?://[^'""]+)['""]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly HashSet<string> SourceFileExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
@@ -67,19 +70,41 @@ internal static class DenoNpmCompatibilityAdapter
|
||||
private static ImmutableArray<DenoBuiltinUsage> CollectBuiltins(DenoModuleGraph graph)
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<DenoBuiltinUsage>();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var edge in graph.Edges)
|
||||
{
|
||||
if (edge.Specifier.StartsWith("node:", StringComparison.OrdinalIgnoreCase) ||
|
||||
edge.Specifier.StartsWith("deno:", StringComparison.OrdinalIgnoreCase))
|
||||
foreach (var candidate in EnumerateBuiltinCandidates(edge))
|
||||
{
|
||||
builder.Add(new DenoBuiltinUsage(edge.Specifier, edge.FromId, edge.Provenance));
|
||||
var key = $"{candidate}::{edge.FromId}";
|
||||
if (seen.Add(key))
|
||||
{
|
||||
builder.Add(new DenoBuiltinUsage(candidate, edge.FromId, edge.Provenance));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateBuiltinCandidates(DenoModuleEdge edge)
|
||||
{
|
||||
if (IsBuiltin(edge.Specifier))
|
||||
{
|
||||
yield return edge.Specifier;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(edge.Resolution) && IsBuiltin(edge.Resolution))
|
||||
{
|
||||
yield return edge.Resolution!;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsBuiltin(string? value)
|
||||
=> !string.IsNullOrWhiteSpace(value) &&
|
||||
(value.StartsWith("node:", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.StartsWith("deno:", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static ImmutableArray<DenoNpmResolution> ResolveNpmPackages(
|
||||
DenoWorkspace workspace,
|
||||
DenoModuleGraph graph,
|
||||
@@ -326,6 +351,7 @@ internal static class DenoNpmCompatibilityAdapter
|
||||
}
|
||||
|
||||
var lineNumber = 0;
|
||||
var literalAssignments = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
using var stream = new StreamReader(file.AbsolutePath);
|
||||
string? line;
|
||||
while ((line = stream.ReadLine()) is not null)
|
||||
@@ -333,6 +359,18 @@ internal static class DenoNpmCompatibilityAdapter
|
||||
lineNumber++;
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
foreach (Match assignment in LiteralAssignmentRegex.Matches(line))
|
||||
{
|
||||
var name = assignment.Groups["name"].Value;
|
||||
var url = assignment.Groups["url"].Value;
|
||||
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
literalAssignments[name] = url;
|
||||
}
|
||||
|
||||
foreach (Match match in DynamicImportRegex.Matches(line))
|
||||
{
|
||||
var specifier = match.Groups["url"].Value;
|
||||
@@ -362,6 +400,36 @@ internal static class DenoNpmCompatibilityAdapter
|
||||
url,
|
||||
"network.fetch.literal"));
|
||||
}
|
||||
|
||||
foreach (Match match in DynamicImportIdentifierRegex.Matches(line))
|
||||
{
|
||||
var identifier = match.Groups["identifier"].Value;
|
||||
if (!literalAssignments.TryGetValue(identifier, out var url) || string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dynamicBuilder.Add(new DenoDynamicImportObservation(
|
||||
file.AbsolutePath,
|
||||
lineNumber,
|
||||
url,
|
||||
"network.dynamic_import.identifier"));
|
||||
}
|
||||
|
||||
foreach (Match match in LiteralFetchIdentifierRegex.Matches(line))
|
||||
{
|
||||
var identifier = match.Groups["identifier"].Value;
|
||||
if (!literalAssignments.TryGetValue(identifier, out var url) || string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fetchBuilder.Add(new DenoLiteralFetchObservation(
|
||||
file.AbsolutePath,
|
||||
lineNumber,
|
||||
url,
|
||||
"network.fetch.identifier"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,11 +68,14 @@ internal sealed class DenoVirtualFileSystem
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var files = new List<DenoVirtualFile>();
|
||||
AddConfigFiles(context, configs, files, cancellationToken);
|
||||
AddImportMaps(importMaps, files, cancellationToken);
|
||||
AddLockFiles(lockFiles, files, cancellationToken);
|
||||
AddVendorFiles(vendors, files, cancellationToken);
|
||||
AddCacheFiles(cacheLocations, files, cancellationToken);
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
AddConfigFiles(context, configs, files, seen, cancellationToken);
|
||||
AddImportMaps(context, importMaps, files, seen, cancellationToken);
|
||||
AddLockFiles(context, lockFiles, files, seen, cancellationToken);
|
||||
AddVendorFiles(vendors, files, seen, cancellationToken);
|
||||
AddCacheFiles(cacheLocations, files, seen, cancellationToken);
|
||||
AddWorkspaceFiles(context, vendors, cacheLocations, files, seen, cancellationToken);
|
||||
|
||||
return new DenoVirtualFileSystem(files);
|
||||
}
|
||||
@@ -81,6 +84,7 @@ internal sealed class DenoVirtualFileSystem
|
||||
LanguageAnalyzerContext context,
|
||||
IEnumerable<DenoConfigDocument> configs,
|
||||
ICollection<DenoVirtualFile> files,
|
||||
HashSet<string> seen,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var config in configs ?? Array.Empty<DenoConfigDocument>())
|
||||
@@ -89,34 +93,45 @@ internal sealed class DenoVirtualFileSystem
|
||||
|
||||
if (File.Exists(config.AbsolutePath))
|
||||
{
|
||||
files.Add(CreateVirtualFile(
|
||||
TryAddFile(
|
||||
files,
|
||||
seen,
|
||||
config.AbsolutePath,
|
||||
context.GetRelativePath(config.AbsolutePath),
|
||||
DenoVirtualFileSource.Workspace,
|
||||
layerDigest: DenoLayerMetadata.TryExtractDigest(config.AbsolutePath)));
|
||||
DenoLayerMetadata.TryExtractDigest(config.AbsolutePath));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(config.ImportMapPath) && File.Exists(config.ImportMapPath))
|
||||
{
|
||||
files.Add(CreateVirtualFile(
|
||||
TryAddFile(
|
||||
files,
|
||||
seen,
|
||||
config.ImportMapPath!,
|
||||
context.GetRelativePath(config.ImportMapPath!),
|
||||
DenoVirtualFileSource.ImportMap,
|
||||
layerDigest: DenoLayerMetadata.TryExtractDigest(config.ImportMapPath!)));
|
||||
DenoLayerMetadata.TryExtractDigest(config.ImportMapPath!));
|
||||
}
|
||||
|
||||
if (config.LockEnabled && !string.IsNullOrWhiteSpace(config.LockFilePath) && File.Exists(config.LockFilePath))
|
||||
{
|
||||
files.Add(CreateVirtualFile(
|
||||
TryAddFile(
|
||||
files,
|
||||
seen,
|
||||
config.LockFilePath!,
|
||||
context.GetRelativePath(config.LockFilePath!),
|
||||
DenoVirtualFileSource.LockFile,
|
||||
layerDigest: DenoLayerMetadata.TryExtractDigest(config.LockFilePath!)));
|
||||
DenoLayerMetadata.TryExtractDigest(config.LockFilePath!));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddImportMaps(IEnumerable<DenoImportMapDocument> maps, ICollection<DenoVirtualFile> files, CancellationToken cancellationToken)
|
||||
private static void AddImportMaps(
|
||||
LanguageAnalyzerContext context,
|
||||
IEnumerable<DenoImportMapDocument> maps,
|
||||
ICollection<DenoVirtualFile> files,
|
||||
HashSet<string> seen,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var map in maps ?? Array.Empty<DenoImportMapDocument>())
|
||||
{
|
||||
@@ -127,15 +142,26 @@ internal sealed class DenoVirtualFileSystem
|
||||
continue;
|
||||
}
|
||||
|
||||
files.Add(CreateVirtualFile(
|
||||
var virtualPath = string.IsNullOrWhiteSpace(map.Origin)
|
||||
? context.GetRelativePath(map.AbsolutePath)
|
||||
: map.Origin;
|
||||
|
||||
TryAddFile(
|
||||
files,
|
||||
seen,
|
||||
map.AbsolutePath,
|
||||
map.Origin,
|
||||
virtualPath,
|
||||
DenoVirtualFileSource.ImportMap,
|
||||
layerDigest: DenoLayerMetadata.TryExtractDigest(map.AbsolutePath)));
|
||||
DenoLayerMetadata.TryExtractDigest(map.AbsolutePath));
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddLockFiles(IEnumerable<DenoLockFile> lockFiles, ICollection<DenoVirtualFile> files, CancellationToken cancellationToken)
|
||||
private static void AddLockFiles(
|
||||
LanguageAnalyzerContext context,
|
||||
IEnumerable<DenoLockFile> lockFiles,
|
||||
ICollection<DenoVirtualFile> files,
|
||||
HashSet<string> seen,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var lockFile in lockFiles ?? Array.Empty<DenoLockFile>())
|
||||
{
|
||||
@@ -146,15 +172,25 @@ internal sealed class DenoVirtualFileSystem
|
||||
continue;
|
||||
}
|
||||
|
||||
files.Add(CreateVirtualFile(
|
||||
var virtualPath = string.IsNullOrWhiteSpace(lockFile.RelativePath)
|
||||
? context.GetRelativePath(lockFile.AbsolutePath)
|
||||
: lockFile.RelativePath;
|
||||
|
||||
TryAddFile(
|
||||
files,
|
||||
seen,
|
||||
lockFile.AbsolutePath,
|
||||
lockFile.RelativePath,
|
||||
virtualPath,
|
||||
DenoVirtualFileSource.LockFile,
|
||||
layerDigest: DenoLayerMetadata.TryExtractDigest(lockFile.AbsolutePath)));
|
||||
DenoLayerMetadata.TryExtractDigest(lockFile.AbsolutePath));
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddVendorFiles(IEnumerable<DenoVendorDirectory> vendors, ICollection<DenoVirtualFile> files, CancellationToken cancellationToken)
|
||||
private static void AddVendorFiles(
|
||||
IEnumerable<DenoVendorDirectory> vendors,
|
||||
ICollection<DenoVirtualFile> files,
|
||||
HashSet<string> seen,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var vendor in vendors ?? Array.Empty<DenoVendorDirectory>())
|
||||
{
|
||||
@@ -168,34 +204,45 @@ internal sealed class DenoVirtualFileSystem
|
||||
foreach (var file in SafeEnumerateFiles(vendor.AbsolutePath))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
files.Add(CreateVirtualFile(
|
||||
var virtualPath = $"vendor://{vendor.Alias}/{DenoPathUtilities.NormalizeRelativePath(Path.GetRelativePath(vendor.AbsolutePath, file))}";
|
||||
TryAddFile(
|
||||
files,
|
||||
seen,
|
||||
file,
|
||||
$"vendor://{vendor.Alias}/{DenoPathUtilities.NormalizeRelativePath(Path.GetRelativePath(vendor.AbsolutePath, file))}",
|
||||
virtualPath,
|
||||
DenoVirtualFileSource.Vendor,
|
||||
vendor.LayerDigest ?? DenoLayerMetadata.TryExtractDigest(file)));
|
||||
vendor.LayerDigest ?? DenoLayerMetadata.TryExtractDigest(file));
|
||||
}
|
||||
|
||||
if (vendor.ImportMap is { AbsolutePath: not null } importMapFile && File.Exists(importMapFile.AbsolutePath))
|
||||
{
|
||||
files.Add(CreateVirtualFile(
|
||||
TryAddFile(
|
||||
files,
|
||||
seen,
|
||||
importMapFile.AbsolutePath,
|
||||
$"vendor://{vendor.Alias}/import_map.json",
|
||||
DenoVirtualFileSource.ImportMap,
|
||||
vendor.LayerDigest ?? DenoLayerMetadata.TryExtractDigest(importMapFile.AbsolutePath)));
|
||||
vendor.LayerDigest ?? DenoLayerMetadata.TryExtractDigest(importMapFile.AbsolutePath));
|
||||
}
|
||||
|
||||
if (vendor.LockFile is { AbsolutePath: not null } vendorLock && File.Exists(vendorLock.AbsolutePath))
|
||||
{
|
||||
files.Add(CreateVirtualFile(
|
||||
TryAddFile(
|
||||
files,
|
||||
seen,
|
||||
vendorLock.AbsolutePath,
|
||||
$"vendor://{vendor.Alias}/deno.lock",
|
||||
DenoVirtualFileSource.LockFile,
|
||||
vendor.LayerDigest ?? DenoLayerMetadata.TryExtractDigest(vendorLock.AbsolutePath)));
|
||||
vendor.LayerDigest ?? DenoLayerMetadata.TryExtractDigest(vendorLock.AbsolutePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddCacheFiles(IEnumerable<DenoCacheLocation> cacheLocations, ICollection<DenoVirtualFile> files, CancellationToken cancellationToken)
|
||||
private static void AddCacheFiles(
|
||||
IEnumerable<DenoCacheLocation> cacheLocations,
|
||||
ICollection<DenoVirtualFile> files,
|
||||
HashSet<string> seen,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var cache in cacheLocations ?? Array.Empty<DenoCacheLocation>())
|
||||
{
|
||||
@@ -208,15 +255,105 @@ internal sealed class DenoVirtualFileSystem
|
||||
foreach (var file in SafeEnumerateFiles(cache.AbsolutePath))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
files.Add(CreateVirtualFile(
|
||||
var virtualPath = $"deno-dir://{cache.Alias}/{DenoPathUtilities.NormalizeRelativePath(Path.GetRelativePath(cache.AbsolutePath, file))}";
|
||||
TryAddFile(
|
||||
files,
|
||||
seen,
|
||||
file,
|
||||
$"deno-dir://{cache.Alias}/{DenoPathUtilities.NormalizeRelativePath(Path.GetRelativePath(cache.AbsolutePath, file))}",
|
||||
virtualPath,
|
||||
cache.Kind == DenoCacheLocationKind.Layer ? DenoVirtualFileSource.Layer : DenoVirtualFileSource.DenoDir,
|
||||
cache.LayerDigest ?? DenoLayerMetadata.TryExtractDigest(file)));
|
||||
cache.LayerDigest ?? DenoLayerMetadata.TryExtractDigest(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddWorkspaceFiles(
|
||||
LanguageAnalyzerContext context,
|
||||
IEnumerable<DenoVendorDirectory> vendors,
|
||||
IEnumerable<DenoCacheLocation> cacheLocations,
|
||||
ICollection<DenoVirtualFile> files,
|
||||
HashSet<string> seen,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var skipRoots = BuildSkipRoots(vendors, cacheLocations);
|
||||
foreach (var file in SafeEnumerateFiles(context.RootPath))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (ShouldSkip(file, skipRoots))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TryAddFile(
|
||||
files,
|
||||
seen,
|
||||
file,
|
||||
context.GetRelativePath(file),
|
||||
DenoVirtualFileSource.Workspace,
|
||||
DenoLayerMetadata.TryExtractDigest(file));
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> BuildSkipRoots(
|
||||
IEnumerable<DenoVendorDirectory> vendors,
|
||||
IEnumerable<DenoCacheLocation> cacheLocations)
|
||||
{
|
||||
var list = new List<string>();
|
||||
|
||||
foreach (var vendor in vendors ?? Array.Empty<DenoVendorDirectory>())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(vendor.AbsolutePath))
|
||||
{
|
||||
list.Add(Path.GetFullPath(vendor.AbsolutePath));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var cache in cacheLocations ?? Array.Empty<DenoCacheLocation>())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(cache.AbsolutePath))
|
||||
{
|
||||
list.Add(Path.GetFullPath(cache.AbsolutePath));
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static bool ShouldSkip(string path, IReadOnlyList<string> skipRoots)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
foreach (var root in skipRoots)
|
||||
{
|
||||
if (fullPath.StartsWith(root, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void TryAddFile(
|
||||
ICollection<DenoVirtualFile> files,
|
||||
HashSet<string> seen,
|
||||
string absolutePath,
|
||||
string virtualPath,
|
||||
DenoVirtualFileSource source,
|
||||
string? layerDigest)
|
||||
{
|
||||
var normalized = Path.GetFullPath(absolutePath);
|
||||
if (!seen.Add($"{source}:{normalized}"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
files.Add(CreateVirtualFile(
|
||||
normalized,
|
||||
virtualPath,
|
||||
source,
|
||||
layerDigest));
|
||||
}
|
||||
|
||||
private static IEnumerable<string> SafeEnumerateFiles(string root)
|
||||
{
|
||||
IEnumerable<string> iterator;
|
||||
|
||||
@@ -39,9 +39,24 @@ internal static class DenoObservationBuilder
|
||||
|
||||
foreach (var node in moduleGraph.Nodes)
|
||||
{
|
||||
if (node.Kind == DenoModuleKind.WorkspaceConfig &&
|
||||
node.Metadata.TryGetValue("entry", out var entry) &&
|
||||
!string.IsNullOrWhiteSpace(entry))
|
||||
if (node.Kind != DenoModuleKind.WorkspaceConfig)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.Metadata.TryGetValue("entrypoints", out var entries) &&
|
||||
!string.IsNullOrWhiteSpace(entries))
|
||||
{
|
||||
foreach (var entry in entries.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(entry))
|
||||
{
|
||||
entrypoints.Add(entry!);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (node.Metadata.TryGetValue("entry", out var entry) &&
|
||||
!string.IsNullOrWhiteSpace(entry))
|
||||
{
|
||||
entrypoints.Add(entry!);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ internal static class RubyObservationBuilder
|
||||
public static RubyObservationDocument Build(
|
||||
IReadOnlyList<RubyPackage> packages,
|
||||
RubyRuntimeGraph runtimeGraph,
|
||||
RubyCapabilities capabilities)
|
||||
RubyCapabilities capabilities,
|
||||
string? bundledWith)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(packages);
|
||||
ArgumentNullException.ThrowIfNull(runtimeGraph);
|
||||
@@ -34,7 +35,11 @@ internal static class RubyObservationBuilder
|
||||
.OrderBy(static scheduler => scheduler, StringComparer.OrdinalIgnoreCase)
|
||||
.ToImmutableArray());
|
||||
|
||||
return new RubyObservationDocument(packageItems, runtimeItems, capabilitySummary);
|
||||
var normalizedBundler = string.IsNullOrWhiteSpace(bundledWith)
|
||||
? null
|
||||
: bundledWith.Trim();
|
||||
|
||||
return new RubyObservationDocument(packageItems, runtimeItems, capabilitySummary, normalizedBundler);
|
||||
}
|
||||
|
||||
private static RubyObservationPackage CreatePackage(RubyPackage package)
|
||||
|
||||
@@ -5,7 +5,8 @@ namespace StellaOps.Scanner.Analyzers.Lang.Ruby.Internal.Observations;
|
||||
internal sealed record RubyObservationDocument(
|
||||
ImmutableArray<RubyObservationPackage> Packages,
|
||||
ImmutableArray<RubyObservationRuntimeEdge> RuntimeEdges,
|
||||
RubyObservationCapabilitySummary Capabilities);
|
||||
RubyObservationCapabilitySummary Capabilities,
|
||||
string? BundledWith);
|
||||
|
||||
internal sealed record RubyObservationPackage(
|
||||
string Name,
|
||||
|
||||
@@ -20,6 +20,7 @@ internal static class RubyObservationSerializer
|
||||
WritePackages(writer, document.Packages);
|
||||
WriteRuntimeEdges(writer, document.RuntimeEdges);
|
||||
WriteCapabilities(writer, document.Capabilities);
|
||||
WriteBundledWith(writer, document.BundledWith);
|
||||
|
||||
writer.WriteEndObject();
|
||||
writer.Flush();
|
||||
@@ -100,6 +101,16 @@ internal static class RubyObservationSerializer
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private static void WriteBundledWith(Utf8JsonWriter writer, string? bundledWith)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(bundledWith))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteString("bundledWith", bundledWith);
|
||||
}
|
||||
|
||||
private static void WriteStringArray(Utf8JsonWriter writer, string propertyName, ImmutableArray<string> values)
|
||||
{
|
||||
writer.WritePropertyName(propertyName);
|
||||
|
||||
@@ -50,7 +50,7 @@ public sealed class RubyLanguageAnalyzer : ILanguageAnalyzer
|
||||
|
||||
if (packages.Count > 0)
|
||||
{
|
||||
EmitObservation(context, writer, packages, runtimeGraph, capabilities);
|
||||
EmitObservation(context, writer, packages, runtimeGraph, capabilities, lockData.BundledWith);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,8 @@ public sealed class RubyLanguageAnalyzer : ILanguageAnalyzer
|
||||
LanguageComponentWriter writer,
|
||||
IReadOnlyList<RubyPackage> packages,
|
||||
RubyRuntimeGraph runtimeGraph,
|
||||
RubyCapabilities capabilities)
|
||||
RubyCapabilities capabilities,
|
||||
string? bundledWith)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(writer);
|
||||
@@ -95,7 +96,7 @@ public sealed class RubyLanguageAnalyzer : ILanguageAnalyzer
|
||||
ArgumentNullException.ThrowIfNull(runtimeGraph);
|
||||
ArgumentNullException.ThrowIfNull(capabilities);
|
||||
|
||||
var observationDocument = RubyObservationBuilder.Build(packages, runtimeGraph, capabilities);
|
||||
var observationDocument = RubyObservationBuilder.Build(packages, runtimeGraph, capabilities, bundledWith);
|
||||
var observationJson = RubyObservationSerializer.Serialize(observationDocument);
|
||||
var observationHash = RubyObservationSerializer.ComputeSha256(observationJson);
|
||||
var observationBytes = Encoding.UTF8.GetBytes(observationJson);
|
||||
@@ -103,7 +104,8 @@ public sealed class RubyLanguageAnalyzer : ILanguageAnalyzer
|
||||
var observationMetadata = BuildObservationMetadata(
|
||||
packages.Count,
|
||||
observationDocument.RuntimeEdges.Length,
|
||||
observationDocument.Capabilities);
|
||||
observationDocument.Capabilities,
|
||||
observationDocument.BundledWith);
|
||||
|
||||
TryPersistObservation(Id, context, observationBytes, observationMetadata);
|
||||
|
||||
@@ -131,7 +133,8 @@ public sealed class RubyLanguageAnalyzer : ILanguageAnalyzer
|
||||
private static IEnumerable<KeyValuePair<string, string?>> BuildObservationMetadata(
|
||||
int packageCount,
|
||||
int runtimeEdgeCount,
|
||||
RubyObservationCapabilitySummary capabilities)
|
||||
RubyObservationCapabilitySummary capabilities,
|
||||
string? bundledWith)
|
||||
{
|
||||
yield return new KeyValuePair<string, string?>("ruby.observation.packages", packageCount.ToString(CultureInfo.InvariantCulture));
|
||||
yield return new KeyValuePair<string, string?>("ruby.observation.runtime_edges", runtimeEdgeCount.ToString(CultureInfo.InvariantCulture));
|
||||
@@ -139,6 +142,17 @@ public sealed class RubyLanguageAnalyzer : ILanguageAnalyzer
|
||||
yield return new KeyValuePair<string, string?>("ruby.observation.capability.net", capabilities.UsesNetwork ? "true" : "false");
|
||||
yield return new KeyValuePair<string, string?>("ruby.observation.capability.serialization", capabilities.UsesSerialization ? "true" : "false");
|
||||
yield return new KeyValuePair<string, string?>("ruby.observation.capability.schedulers", capabilities.JobSchedulers.Length.ToString(CultureInfo.InvariantCulture));
|
||||
if (capabilities.JobSchedulers.Length > 0)
|
||||
{
|
||||
yield return new KeyValuePair<string, string?>(
|
||||
"ruby.observation.capability.scheduler_list",
|
||||
string.Join(';', capabilities.JobSchedulers));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(bundledWith))
|
||||
{
|
||||
yield return new KeyValuePair<string, string?>("ruby.observation.bundler_version", bundledWith);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryPersistObservation(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
| Task ID | State | Notes |
|
||||
| --- | --- | --- |
|
||||
| `SCANNER-ENG-0009` | DOING (2025-11-12) | Added bundler-version metadata + observation summaries, richer CLI output, and the `complex-app` fixture to drive parity validation. |
|
||||
| `SCANNER-ENG-0016` | DONE (2025-11-10) | RubyLockCollector merged with vendor cache ingestion; workspace overrides, bundler groups, git/path fixture, and offline-kit mirror updated. |
|
||||
| `SCANNER-ENG-0017` | DONE (2025-11-09) | Build runtime require/autoload graph builder with tree-sitter Ruby per design §4.4, feed EntryTrace hints. |
|
||||
| `SCANNER-ENG-0018` | DONE (2025-11-09) | Emit Ruby capability + framework surface signals, align with design §4.5 / Sprint 138. |
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Deno;
|
||||
|
||||
public sealed class DenoWorkspaceNormalizerTests
|
||||
{
|
||||
|
||||
[Fact]
|
||||
public async Task WorkspaceFixtureProducesDeterministicOutputAsync()
|
||||
{
|
||||
@@ -79,18 +80,50 @@ public sealed class DenoWorkspaceNormalizerTests
|
||||
node => node.Kind == DenoModuleKind.RemoteModule &&
|
||||
node.Id == "remote::https://deno.land/std@0.207.0/http/server.ts");
|
||||
Assert.NotNull(remoteNode);
|
||||
Assert.Equal("sha256-deadbeef", remoteNode!.Integrity);
|
||||
var expectedIntegrity = lockFile.RemoteEntries["https://deno.land/std@0.207.0/http/server.ts"];
|
||||
Assert.Equal(expectedIntegrity, remoteNode!.Integrity);
|
||||
|
||||
var vendorCacheEdges = graph.Edges
|
||||
.Where(edge => edge.ImportKind == DenoImportKind.Cache &&
|
||||
edge.Provenance.StartsWith("vendor-cache:", StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
|
||||
if (vendorCacheEdges.Length == 0)
|
||||
{
|
||||
var sample = string.Join(
|
||||
Environment.NewLine,
|
||||
graph.Edges
|
||||
.Select(edge => $"{edge.ImportKind}:{edge.Specifier}:{edge.Provenance}")
|
||||
.Take(10));
|
||||
Assert.Fail($"Expected vendor cache edges but none were found. Sample edges:{Environment.NewLine}{sample}");
|
||||
}
|
||||
|
||||
var vendorEdge = vendorCacheEdges.FirstOrDefault(
|
||||
edge => edge.Specifier.Contains("https://deno.land/std@0.207.0/http/server.ts", StringComparison.Ordinal));
|
||||
if (vendorEdge is null)
|
||||
{
|
||||
var details = string.Join(
|
||||
Environment.NewLine,
|
||||
vendorCacheEdges.Select(edge => $"{edge.Specifier} [{edge.Provenance}] -> {edge.Resolution}"));
|
||||
Assert.Fail($"Unable to locate vendor cache edge for std server.ts. Observed edges:{Environment.NewLine}{details}");
|
||||
}
|
||||
|
||||
var npmBridgeEdges = graph.Edges
|
||||
.Where(edge => edge.ImportKind == DenoImportKind.NpmBridge)
|
||||
.ToArray();
|
||||
if (npmBridgeEdges.Length == 0)
|
||||
{
|
||||
var bridgeSample = string.Join(
|
||||
Environment.NewLine,
|
||||
graph.Edges
|
||||
.Select(edge => $"{edge.ImportKind}:{edge.Specifier}:{edge.Resolution}")
|
||||
.Take(10));
|
||||
Assert.Fail($"No npm bridge edges discovered. Sample:{Environment.NewLine}{bridgeSample}");
|
||||
}
|
||||
|
||||
Assert.Contains(
|
||||
graph.Edges,
|
||||
edge => edge.ImportKind == DenoImportKind.Cache &&
|
||||
edge.Provenance.StartsWith("vendor-cache:", StringComparison.Ordinal) &&
|
||||
edge.Specifier.Contains("https://deno.land/std@0.207.0/http/server.ts", StringComparison.Ordinal));
|
||||
|
||||
Assert.Contains(
|
||||
graph.Edges,
|
||||
edge => edge.ImportKind == DenoImportKind.NpmBridge &&
|
||||
edge.Specifier == "npm:dayjs@1" &&
|
||||
npmBridgeEdges,
|
||||
edge => edge.Specifier == "npm:dayjs@1" &&
|
||||
edge.Resolution == "dayjs@1.11.12");
|
||||
|
||||
Assert.Contains(
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
using System.Linq;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestFixtures;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Golden;
|
||||
@@ -9,25 +14,37 @@ public sealed class DenoAnalyzerGoldenTests
|
||||
[Fact]
|
||||
public async Task AnalyzerMatchesGoldenSnapshotAsync()
|
||||
{
|
||||
var fixture = TestPaths.ResolveFixture("lang", "deno", "full");
|
||||
var golden = Path.Combine(fixture, "expected.json");
|
||||
var fixtureRoot = TestPaths.ResolveFixture("lang", "deno", "full");
|
||||
var golden = Path.Combine(fixtureRoot, "expected.json");
|
||||
var analyzers = new ILanguageAnalyzer[] { new DenoLanguageAnalyzer() };
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(fixture, analyzers, cancellationToken);
|
||||
var normalized = Normalize(json, fixture);
|
||||
var expected = await File.ReadAllTextAsync(golden, cancellationToken);
|
||||
|
||||
normalized = normalized.TrimEnd();
|
||||
expected = expected.TrimEnd();
|
||||
|
||||
if (!string.Equals(expected, normalized, StringComparison.Ordinal))
|
||||
var (workspaceRoot, envDir) = DenoWorkspaceTestFixture.Create();
|
||||
var previousDenoDir = Environment.GetEnvironmentVariable("DENO_DIR");
|
||||
try
|
||||
{
|
||||
var actualPath = golden + ".actual";
|
||||
await File.WriteAllTextAsync(actualPath, normalized, cancellationToken);
|
||||
}
|
||||
Environment.SetEnvironmentVariable("DENO_DIR", envDir);
|
||||
|
||||
Assert.Equal(expected, normalized);
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(workspaceRoot, analyzers, cancellationToken);
|
||||
var normalized = Normalize(json, workspaceRoot);
|
||||
var expected = await File.ReadAllTextAsync(golden, cancellationToken);
|
||||
|
||||
normalized = normalized.TrimEnd();
|
||||
expected = expected.TrimEnd();
|
||||
|
||||
if (!string.Equals(expected, normalized, StringComparison.Ordinal))
|
||||
{
|
||||
var actualPath = golden + ".actual";
|
||||
await File.WriteAllTextAsync(actualPath, normalized, cancellationToken);
|
||||
}
|
||||
|
||||
Assert.Equal(expected, normalized);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("DENO_DIR", previousDenoDir);
|
||||
DenoWorkspaceTestFixture.Cleanup(workspaceRoot);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Normalize(string json, string workspaceRoot)
|
||||
@@ -37,10 +54,206 @@ public sealed class DenoAnalyzerGoldenTests
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var normalizedRoot = workspaceRoot.Replace("\\", "/", StringComparison.Ordinal);
|
||||
var builder = json.Replace(normalizedRoot, "<workspace>", StringComparison.Ordinal);
|
||||
var altRoot = workspaceRoot.Replace("/", "\\", StringComparison.Ordinal);
|
||||
builder = builder.Replace(altRoot, "<workspace>", StringComparison.Ordinal);
|
||||
return builder;
|
||||
var node = JsonNode.Parse(json) ?? new JsonArray();
|
||||
if (node is JsonArray array)
|
||||
{
|
||||
foreach (var element in array.OfType<JsonObject>())
|
||||
{
|
||||
NormalizeComponent(element);
|
||||
}
|
||||
}
|
||||
|
||||
var normalized = node.ToJsonString(JsonSerializerOptionsProvider);
|
||||
normalized = ReplaceWorkspacePaths(normalized, workspaceRoot);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static void NormalizeComponent(JsonObject component)
|
||||
{
|
||||
if (component is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SortMetadata(component);
|
||||
|
||||
if (!component.TryGetPropertyValue("type", out var typeNode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var type = typeNode?.GetValue<string>();
|
||||
if (string.Equals(type, "deno-container", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
NormalizeContainer(component);
|
||||
}
|
||||
else if (string.Equals(type, "deno-observation", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
NormalizeObservation(component);
|
||||
}
|
||||
}
|
||||
|
||||
private static void NormalizeContainer(JsonObject container)
|
||||
{
|
||||
NormalizeAliasProperty(container, "name");
|
||||
NormalizeComponentKey(container);
|
||||
|
||||
if (container.TryGetPropertyValue("metadata", out var metadataNode) &&
|
||||
metadataNode is JsonObject metadata)
|
||||
{
|
||||
NormalizeAliasProperty(metadata, "deno.container.identifier");
|
||||
NormalizeAliasProperty(metadata, "deno.container.meta.alias");
|
||||
}
|
||||
|
||||
if (container.TryGetPropertyValue("evidence", out var evidenceNode) &&
|
||||
evidenceNode is JsonArray evidenceArray)
|
||||
{
|
||||
foreach (var evidence in evidenceArray.OfType<JsonObject>())
|
||||
{
|
||||
if (evidence.TryGetPropertyValue("source", out var sourceNode) &&
|
||||
string.Equals(sourceNode?.GetValue<string>(), "deno.container", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
NormalizeAliasProperty(evidence, "value");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void NormalizeComponentKey(JsonObject container)
|
||||
{
|
||||
if (!container.TryGetPropertyValue("componentKey", out var keyNode) ||
|
||||
keyNode is not JsonValue keyValue ||
|
||||
!keyValue.TryGetValue<string>(out var componentKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var lastSeparator = componentKey.LastIndexOf(':');
|
||||
if (lastSeparator < 0)
|
||||
{
|
||||
container["componentKey"] = NormalizeAliasValue(componentKey);
|
||||
return;
|
||||
}
|
||||
|
||||
var prefix = componentKey[..(lastSeparator + 1)];
|
||||
var alias = componentKey[(lastSeparator + 1)..];
|
||||
container["componentKey"] = prefix + NormalizeAliasValue(alias);
|
||||
}
|
||||
|
||||
private static void NormalizeAliasProperty(JsonObject obj, string propertyName)
|
||||
{
|
||||
if (!obj.TryGetPropertyValue(propertyName, out var node) ||
|
||||
node is not JsonValue valueNode ||
|
||||
!valueNode.TryGetValue<string>(out var value) ||
|
||||
string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
obj[propertyName] = NormalizeAliasValue(value);
|
||||
}
|
||||
|
||||
private static string NormalizeAliasValue(string value)
|
||||
=> TryNormalizeAlias(value, out var normalized) ? normalized : value;
|
||||
|
||||
private static bool TryNormalizeAlias(string value, out string normalized)
|
||||
{
|
||||
normalized = value;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var lastDash = value.LastIndexOf('-');
|
||||
if (lastDash <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var suffix = value[(lastDash + 1)..];
|
||||
if (suffix.Length != 12)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var character in suffix)
|
||||
{
|
||||
if (!IsLowerHex(character))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
normalized = value[..lastDash] + "-<hash>";
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsLowerHex(char value)
|
||||
=> (value is >= '0' and <= '9') || (value is >= 'a' and <= 'f');
|
||||
|
||||
private static string ReplaceWorkspacePaths(string value, string workspaceRoot)
|
||||
{
|
||||
var normalizedRoot = workspaceRoot.Replace("\\", "/", StringComparison.Ordinal);
|
||||
var normalizedRootLower = normalizedRoot.ToLowerInvariant();
|
||||
var result = value
|
||||
.Replace(normalizedRoot, "<workspace>", StringComparison.Ordinal)
|
||||
.Replace(normalizedRootLower, "<workspace>", StringComparison.Ordinal);
|
||||
|
||||
var altRoot = workspaceRoot.Replace("/", "\\", StringComparison.Ordinal);
|
||||
var altRootLower = altRoot.ToLowerInvariant();
|
||||
result = result
|
||||
.Replace(altRoot, "<workspace>", StringComparison.Ordinal)
|
||||
.Replace(altRootLower, "<workspace>", StringComparison.Ordinal);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void NormalizeObservation(JsonObject observation)
|
||||
{
|
||||
if (observation.TryGetPropertyValue("metadata", out var metadataNode) &&
|
||||
metadataNode is JsonObject metadata &&
|
||||
metadata.TryGetPropertyValue("deno.observation.hash", out var hashNode) &&
|
||||
hashNode is JsonValue)
|
||||
{
|
||||
metadata["deno.observation.hash"] = "<hash>";
|
||||
}
|
||||
|
||||
if (observation.TryGetPropertyValue("evidence", out var evidenceNode) &&
|
||||
evidenceNode is JsonArray evidenceArray)
|
||||
{
|
||||
foreach (var evidence in evidenceArray.OfType<JsonObject>())
|
||||
{
|
||||
if (evidence.TryGetPropertyValue("source", out var sourceNode) &&
|
||||
string.Equals(sourceNode?.GetValue<string>(), "deno.observation", StringComparison.OrdinalIgnoreCase) &&
|
||||
evidence.TryGetPropertyValue("sha256", out var shaNode) &&
|
||||
shaNode is JsonValue)
|
||||
{
|
||||
evidence["sha256"] = "<hash>";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SortMetadata(JsonObject component)
|
||||
{
|
||||
if (!component.TryGetPropertyValue("metadata", out var metadataNode) ||
|
||||
metadataNode is not JsonObject metadata)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sorted = new JsonObject();
|
||||
foreach (var entry in metadata.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
||||
{
|
||||
sorted[entry.Key] = entry.Value?.DeepClone();
|
||||
}
|
||||
|
||||
component["metadata"] = sorted;
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonSerializerOptionsProvider = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
BUNDLE_GEMFILE: Gemfile
|
||||
BUNDLE_PATH: vendor/custom-bundle
|
||||
@@ -0,0 +1,20 @@
|
||||
source "https://rubygems.org/"
|
||||
|
||||
gem "rack", "~> 3.1"
|
||||
|
||||
group :web do
|
||||
gem "sinatra", "~> 3.1"
|
||||
gem "pagy"
|
||||
end
|
||||
|
||||
group :jobs do
|
||||
gem "sidekiq", "~> 7.2"
|
||||
end
|
||||
|
||||
group :ops do
|
||||
gem "clockwork"
|
||||
end
|
||||
|
||||
group :tools do
|
||||
gem "pry", "= 0.14.2"
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
clockwork (3.0.0)
|
||||
pagy (6.5.0)
|
||||
pry (0.14.2)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
rack (3.1.2)
|
||||
sidekiq (7.2.1)
|
||||
rack (~> 2.0)
|
||||
sinatra (3.1.0)
|
||||
rack (~> 3.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
clockwork
|
||||
pagy
|
||||
pry (= 0.14.2)
|
||||
rack (~> 3.1)
|
||||
sidekiq (~> 7.2)
|
||||
sinatra (~> 3.1)
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.3
|
||||
@@ -0,0 +1,18 @@
|
||||
require "rack"
|
||||
require "sinatra"
|
||||
require "pagy/backend"
|
||||
require "net/http"
|
||||
require_relative '../config/environment'
|
||||
|
||||
module ConsoleApp
|
||||
class Server < Sinatra::Base
|
||||
get '/' do
|
||||
http = Net::HTTP.new('example.invalid', 443)
|
||||
http.use_ssl = true
|
||||
http.start do |client|
|
||||
client.get('/')
|
||||
end
|
||||
'ok'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
require "pagy"
|
||||
require "json"
|
||||
|
||||
module ConsoleApp
|
||||
module Boot
|
||||
def self.load!
|
||||
JSON.parse('{feature:advisory-ai}')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,202 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "observation::ruby",
|
||||
"name": "Ruby Observation Summary",
|
||||
"type": "ruby-observation",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"ruby.observation.bundler_version": "2.5.3",
|
||||
"ruby.observation.capability.exec": "false",
|
||||
"ruby.observation.capability.net": "true",
|
||||
"ruby.observation.capability.scheduler_list": "clockwork;sidekiq",
|
||||
"ruby.observation.capability.schedulers": "2",
|
||||
"ruby.observation.capability.serialization": "false",
|
||||
"ruby.observation.packages": "6",
|
||||
"ruby.observation.runtime_edges": "5"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "derived",
|
||||
"source": "ruby.observation",
|
||||
"locator": "document",
|
||||
"value": "{\u0022packages\u0022:[{\u0022name\u0022:\u0022clockwork\u0022,\u0022version\u0022:\u00223.0.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022ops\u0022]},{\u0022name\u0022:\u0022pagy\u0022,\u0022version\u0022:\u00226.5.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022web\u0022]},{\u0022name\u0022:\u0022pry\u0022,\u0022version\u0022:\u00220.14.2\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022tools\u0022]},{\u0022name\u0022:\u0022rack\u0022,\u0022version\u0022:\u00223.1.2\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022sidekiq\u0022,\u0022version\u0022:\u00227.2.1\u0022,\u0022source\u0022:\u0022vendor\u0022,\u0022declaredOnly\u0022:false,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022artifact\u0022:\u0022vendor/custom-bundle/cache/sidekiq-7.2.1.gem\u0022,\u0022groups\u0022:[\u0022jobs\u0022]},{\u0022name\u0022:\u0022sinatra\u0022,\u0022version\u0022:\u00223.1.0\u0022,\u0022source\u0022:\u0022vendor-cache\u0022,\u0022declaredOnly\u0022:false,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022artifact\u0022:\u0022vendor/cache/sinatra-3.1.0.gem\u0022,\u0022groups\u0022:[\u0022web\u0022]}],\u0022runtimeEdges\u0022:[{\u0022package\u0022:\u0022clockwork\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022scripts/worker.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022pagy\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app/main.rb\u0022,\u0022config/environment.rb\u0022],\u0022entrypoints\u0022:[\u0022config/environment.rb\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022rack\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022app/main.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022sidekiq\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022scripts/worker.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022sinatra\u0022,\u0022usedByEntrypoint\u0022:false,\u0022files\u0022:[\u0022app/main.rb\u0022],\u0022entrypoints\u0022:[],\u0022reasons\u0022:[\u0022require-static\u0022]}],\u0022capabilities\u0022:{\u0022usesExec\u0022:false,\u0022usesNetwork\u0022:true,\u0022usesSerialization\u0022:false,\u0022jobSchedulers\u0022:[\u0022clockwork\u0022,\u0022sidekiq\u0022]},\u0022bundledWith\u0022:\u00222.5.3\u0022}",
|
||||
"sha256": "sha256:beaefa12ec1f49e62343781ffa949ec3fa006f0452cf8a342a9a12be3cda1d82"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/clockwork@3.0.0",
|
||||
"purl": "pkg:gem/clockwork@3.0.0",
|
||||
"name": "clockwork",
|
||||
"version": "3.0.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "clockwork;sidekiq",
|
||||
"capability.scheduler.clockwork": "true",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "ops",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.files": "scripts/worker.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/pagy@6.5.0",
|
||||
"purl": "pkg:gem/pagy@6.5.0",
|
||||
"name": "pagy",
|
||||
"version": "6.5.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "clockwork;sidekiq",
|
||||
"capability.scheduler.clockwork": "true",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "web",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.entrypoints": "config/environment.rb",
|
||||
"runtime.files": "app/main.rb;config/environment.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/pry@0.14.2",
|
||||
"purl": "pkg:gem/pry@0.14.2",
|
||||
"name": "pry",
|
||||
"version": "0.14.2",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "clockwork;sidekiq",
|
||||
"capability.scheduler.clockwork": "true",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "tools",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/rack@3.1.2",
|
||||
"purl": "pkg:gem/rack@3.1.2",
|
||||
"name": "rack",
|
||||
"version": "3.1.2",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "clockwork;sidekiq",
|
||||
"capability.scheduler.clockwork": "true",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.files": "app/main.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/sidekiq@7.2.1",
|
||||
"purl": "pkg:gem/sidekiq@7.2.1",
|
||||
"name": "sidekiq",
|
||||
"version": "7.2.1",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"artifact": "vendor/custom-bundle/cache/sidekiq-7.2.1.gem",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "clockwork;sidekiq",
|
||||
"capability.scheduler.clockwork": "true",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"declaredOnly": "false",
|
||||
"groups": "jobs",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.files": "scripts/worker.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "vendor"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "sidekiq-7.2.1.gem",
|
||||
"locator": "vendor/custom-bundle/cache/sidekiq-7.2.1.gem"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/sinatra@3.1.0",
|
||||
"purl": "pkg:gem/sinatra@3.1.0",
|
||||
"name": "sinatra",
|
||||
"version": "3.1.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"artifact": "vendor/cache/sinatra-3.1.0.gem",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "clockwork;sidekiq",
|
||||
"capability.scheduler.clockwork": "true",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"declaredOnly": "false",
|
||||
"groups": "web",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.files": "app/main.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "vendor-cache"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "sinatra-3.1.0.gem",
|
||||
"locator": "vendor/cache/sinatra-3.1.0.gem"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
require "sidekiq"
|
||||
require "clockwork"
|
||||
require "open3"
|
||||
|
||||
module ConsoleApp
|
||||
class Worker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform
|
||||
Clockwork.every(1.hour, 'ping') do
|
||||
Open3.popen3('echo', 'ping') { |_stdin, stdout, _stderr, wait_thr| wait_thr.value }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
ruby "3.1.2"
|
||||
|
||||
gem "rack", "~> 3.0"
|
||||
gem "rails", "7.1.0"
|
||||
gem "puma", "~> 6.1", group: [:web]
|
||||
gem "sqlite3", group: :db
|
||||
|
||||
group :jobs do
|
||||
gem "sidekiq", "7.2.1"
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
gem "pry", "0.14.2"
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem "rspec", "3.12.0"
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
coderay (1.1.3)
|
||||
connection_pool (2.4.1)
|
||||
method_source (1.0.0)
|
||||
pry (0.14.2)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
puma (6.1.1)
|
||||
rack (3.0.8)
|
||||
rails (7.1.0)
|
||||
rspec (3.12.0)
|
||||
sidekiq (7.2.1)
|
||||
connection_pool (>= 2.3.0)
|
||||
rack (~> 2.0)
|
||||
sqlite3 (1.6.0-x86_64-linux)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
pry (= 0.14.2)
|
||||
puma (~> 6.1)
|
||||
rack (~> 3.0)
|
||||
rails (= 7.1.0)
|
||||
rspec (= 3.12.0)
|
||||
sidekiq (= 7.2.1)
|
||||
sqlite3
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.22
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env ruby
|
||||
require "rack"
|
||||
require "puma"
|
||||
require "sidekiq"
|
||||
require "yaml"
|
||||
require "net/http"
|
||||
|
||||
require_relative "app/workers/email_worker"
|
||||
|
||||
class App
|
||||
def call(env)
|
||||
EmailWorker.perform_async(env["PATH_INFO"])
|
||||
[200, { "Content-Type" => "text/plain" }, ["ok"]]
|
||||
end
|
||||
end
|
||||
|
||||
run App.new
|
||||
@@ -0,0 +1,13 @@
|
||||
require "sidekiq"
|
||||
require "net/http"
|
||||
require "yaml"
|
||||
|
||||
class EmailWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform(user_id)
|
||||
system("echo sending email #{user_id}")
|
||||
Net::HTTP.get(URI("https://example.com/users/#{user_id}"))
|
||||
YAML.load(File.read("config/clock.rb"))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,4 @@
|
||||
require "rack"
|
||||
require_relative "app.rb"
|
||||
|
||||
run App.new
|
||||
@@ -0,0 +1,5 @@
|
||||
require "clockwork"
|
||||
|
||||
module Clockwork
|
||||
every(1.hour, "cleanup") { puts "cleanup" }
|
||||
end
|
||||
@@ -0,0 +1,319 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "observation::ruby",
|
||||
"name": "Ruby Observation Summary",
|
||||
"type": "ruby-observation",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"ruby.observation.bundler_version": "2.4.22",
|
||||
"ruby.observation.capability.exec": "true",
|
||||
"ruby.observation.capability.net": "true",
|
||||
"ruby.observation.capability.scheduler_list": "sidekiq",
|
||||
"ruby.observation.capability.schedulers": "1",
|
||||
"ruby.observation.capability.serialization": "true",
|
||||
"ruby.observation.packages": "11",
|
||||
"ruby.observation.runtime_edges": "3"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "derived",
|
||||
"source": "ruby.observation",
|
||||
"locator": "document",
|
||||
"value": "{\u0022packages\u0022:[{\u0022name\u0022:\u0022coderay\u0022,\u0022version\u0022:\u00221.1.3\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022connection_pool\u0022,\u0022version\u0022:\u00222.4.1\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022method_source\u0022,\u0022version\u0022:\u00221.0.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022pry\u0022,\u0022version\u0022:\u00220.14.2\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022development\u0022,\u0022test\u0022]},{\u0022name\u0022:\u0022puma\u0022,\u0022version\u0022:\u00226.1.1\u0022,\u0022source\u0022:\u0022vendor-cache\u0022,\u0022declaredOnly\u0022:false,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022artifact\u0022:\u0022vendor/cache/puma-6.1.1.gem\u0022,\u0022groups\u0022:[\u0022web\u0022]},{\u0022name\u0022:\u0022rack\u0022,\u0022version\u0022:\u00223.0.8\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022rails\u0022,\u0022version\u0022:\u00227.1.0\u0022,\u0022source\u0022:\u0022vendor-cache\u0022,\u0022platform\u0022:\u0022x86_64-linux\u0022,\u0022declaredOnly\u0022:false,\u0022artifact\u0022:\u0022vendor/cache/rails-7.1.0-x86_64-linux.gem\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022rails\u0022,\u0022version\u0022:\u00227.1.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022default\u0022]},{\u0022name\u0022:\u0022rspec\u0022,\u0022version\u0022:\u00223.12.0\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022test\u0022]},{\u0022name\u0022:\u0022sidekiq\u0022,\u0022version\u0022:\u00227.2.1\u0022,\u0022source\u0022:\u0022vendor-bundle\u0022,\u0022declaredOnly\u0022:false,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022artifact\u0022:\u0022vendor/bundle/ruby/3.1.0/gems/sidekiq-7.2.1\u0022,\u0022groups\u0022:[\u0022jobs\u0022]},{\u0022name\u0022:\u0022sqlite3\u0022,\u0022version\u0022:\u00221.6.0-x86_64-linux\u0022,\u0022source\u0022:\u0022https://rubygems.org/\u0022,\u0022declaredOnly\u0022:true,\u0022lockfile\u0022:\u0022Gemfile.lock\u0022,\u0022groups\u0022:[\u0022db\u0022]}],\u0022runtimeEdges\u0022:[{\u0022package\u0022:\u0022puma\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app.rb\u0022],\u0022entrypoints\u0022:[\u0022app.rb\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022rack\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app.rb\u0022,\u0022config.ru\u0022],\u0022entrypoints\u0022:[\u0022app.rb\u0022,\u0022config.ru\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]},{\u0022package\u0022:\u0022sidekiq\u0022,\u0022usedByEntrypoint\u0022:true,\u0022files\u0022:[\u0022app.rb\u0022,\u0022app/workers/email_worker.rb\u0022],\u0022entrypoints\u0022:[\u0022app.rb\u0022,\u0022app/workers/email_worker.rb\u0022],\u0022reasons\u0022:[\u0022require-static\u0022]}],\u0022capabilities\u0022:{\u0022usesExec\u0022:true,\u0022usesNetwork\u0022:true,\u0022usesSerialization\u0022:true,\u0022jobSchedulers\u0022:[\u0022sidekiq\u0022]},\u0022bundledWith\u0022:\u00222.4.22\u0022}",
|
||||
"sha256": "sha256:30b34afcf1a3ae3a32f1088ca535ca5359f9ed1ecf53850909b2bcd4da663ace"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/coderay@1.1.3",
|
||||
"purl": "pkg:gem/coderay@1.1.3",
|
||||
"name": "coderay",
|
||||
"version": "1.1.3",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/connection_pool@2.4.1",
|
||||
"purl": "pkg:gem/connection_pool@2.4.1",
|
||||
"name": "connection_pool",
|
||||
"version": "2.4.1",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/method_source@1.0.0",
|
||||
"purl": "pkg:gem/method_source@1.0.0",
|
||||
"name": "method_source",
|
||||
"version": "1.0.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/pry@0.14.2",
|
||||
"purl": "pkg:gem/pry@0.14.2",
|
||||
"name": "pry",
|
||||
"version": "0.14.2",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "development;test",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/puma@6.1.1",
|
||||
"purl": "pkg:gem/puma@6.1.1",
|
||||
"name": "puma",
|
||||
"version": "6.1.1",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"artifact": "vendor/cache/puma-6.1.1.gem",
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "false",
|
||||
"groups": "web",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.entrypoints": "app.rb",
|
||||
"runtime.files": "app.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "vendor-cache"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "puma-6.1.1.gem",
|
||||
"locator": "vendor/cache/puma-6.1.1.gem"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/rack@3.0.8",
|
||||
"purl": "pkg:gem/rack@3.0.8",
|
||||
"name": "rack",
|
||||
"version": "3.0.8",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "default",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.entrypoints": "app.rb;config.ru",
|
||||
"runtime.files": "app.rb;config.ru",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/rails@7.1.0",
|
||||
"purl": "pkg:gem/rails@7.1.0",
|
||||
"name": "rails",
|
||||
"version": "7.1.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"artifact": "vendor/cache/rails-7.1.0-x86_64-linux.gem",
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "false",
|
||||
"groups": "default",
|
||||
"lockfile": "vendor/cache/rails-7.1.0-x86_64-linux.gem",
|
||||
"platform": "x86_64-linux",
|
||||
"source": "vendor-cache"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "rails-7.1.0-x86_64-linux.gem",
|
||||
"locator": "vendor/cache/rails-7.1.0-x86_64-linux.gem"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/rspec@3.12.0",
|
||||
"purl": "pkg:gem/rspec@3.12.0",
|
||||
"name": "rspec",
|
||||
"version": "3.12.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "test",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/sidekiq@7.2.1",
|
||||
"purl": "pkg:gem/sidekiq@7.2.1",
|
||||
"name": "sidekiq",
|
||||
"version": "7.2.1",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"artifact": "vendor/bundle/ruby/3.1.0/gems/sidekiq-7.2.1",
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "false",
|
||||
"groups": "jobs",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.entrypoints": "app.rb;app/workers/email_worker.rb",
|
||||
"runtime.files": "app.rb;app/workers/email_worker.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "vendor-bundle"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "sidekiq-7.2.1",
|
||||
"locator": "vendor/bundle/ruby/3.1.0/gems/sidekiq-7.2.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "ruby",
|
||||
"componentKey": "purl::pkg:gem/sqlite3@1.6.0-x86_64-linux",
|
||||
"purl": "pkg:gem/sqlite3@1.6.0-x86_64-linux",
|
||||
"name": "sqlite3",
|
||||
"version": "1.6.0-x86_64-linux",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "sidekiq",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"groups": "db",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "Gemfile.lock",
|
||||
"locator": "Gemfile.lock"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,89 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Ruby;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Ruby.Tests;
|
||||
|
||||
public sealed class RubyLanguageAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SimpleWorkspaceProducesDeterministicOutputAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "simple-app");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnalyzerEmitsObservationPayloadWithSummaryAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "simple-app");
|
||||
var store = new ScanAnalysisStore();
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
var engine = new LanguageAnalyzerEngine(analyzers);
|
||||
var context = new LanguageAnalyzerContext(
|
||||
fixturePath,
|
||||
TimeProvider.System,
|
||||
usageHints: null,
|
||||
services: null,
|
||||
analysisStore: store);
|
||||
|
||||
var result = await engine.AnalyzeAsync(context, TestContext.Current.CancellationToken);
|
||||
var snapshots = result.ToSnapshots();
|
||||
|
||||
var summary = Assert.Single(snapshots, snapshot => snapshot.Type == "ruby-observation");
|
||||
Assert.Equal("Ruby Observation Summary", summary.Name);
|
||||
Assert.Equal("observation::ruby", summary.ComponentKey);
|
||||
Assert.True(summary.Metadata.TryGetValue("ruby.observation.packages", out var packageCount));
|
||||
Assert.Equal("11", packageCount);
|
||||
Assert.Equal("3", summary.Metadata["ruby.observation.runtime_edges"]);
|
||||
Assert.Equal("true", summary.Metadata["ruby.observation.capability.exec"]);
|
||||
Assert.Equal("true", summary.Metadata["ruby.observation.capability.net"]);
|
||||
Assert.Equal("true", summary.Metadata["ruby.observation.capability.serialization"]);
|
||||
Assert.Equal("2.4.22", summary.Metadata["ruby.observation.bundler_version"]);
|
||||
|
||||
Assert.True(store.TryGet(ScanAnalysisKeys.RubyObservationPayload, out AnalyzerObservationPayload payload));
|
||||
Assert.Equal("ruby", payload.AnalyzerId);
|
||||
Assert.Equal("ruby.observation", payload.Kind);
|
||||
Assert.Equal("application/json", payload.MediaType);
|
||||
Assert.NotNull(payload.Metadata);
|
||||
Assert.Equal("11", payload.Metadata!["ruby.observation.packages"]);
|
||||
|
||||
using var document = JsonDocument.Parse(payload.Content.ToArray());
|
||||
var root = document.RootElement;
|
||||
var packages = root.GetProperty("packages");
|
||||
Assert.Equal(11, packages.GetArrayLength());
|
||||
|
||||
var runtimeEdges = root.GetProperty("runtimeEdges");
|
||||
Assert.True(runtimeEdges.GetArrayLength() >= 1);
|
||||
|
||||
var capabilities = root.GetProperty("capabilities");
|
||||
Assert.True(capabilities.GetProperty("usesExec").GetBoolean());
|
||||
Assert.True(capabilities.GetProperty("usesNetwork").GetBoolean());
|
||||
Assert.True(capabilities.GetProperty("usesSerialization").GetBoolean());
|
||||
Assert.Equal("2.4.22", root.GetProperty("bundledWith").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ComplexWorkspaceProducesDeterministicOutputAsync()
|
||||
{
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "ruby", "complex-app");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
var analyzers = new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() };
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
TestContext.Current.CancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Remove="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Remove="xunit" />
|
||||
<PackageReference Remove="xunit.runner.visualstudio" />
|
||||
<PackageReference Remove="Microsoft.AspNetCore.Mvc.Testing" />
|
||||
<PackageReference Remove="Mongo2Go" />
|
||||
<PackageReference Remove="coverlet.collector" />
|
||||
<PackageReference Remove="Microsoft.Extensions.TimeProvider.Testing" />
|
||||
<ProjectReference Remove="..\StellaOps.Concelier.Testing\StellaOps.Concelier.Testing.csproj" />
|
||||
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\AssemblyInfo.cs" />
|
||||
<Compile Remove="$(MSBuildThisFileDirectory)..\StellaOps.Concelier.Tests.Shared\MongoFixtureCollection.cs" />
|
||||
<Using Remove="StellaOps.Concelier.Testing" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit.v3" Version="3.0.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Analyzers.Lang.Tests\StellaOps.Scanner.Analyzers.Lang.Tests.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang/StellaOps.Scanner.Analyzers.Lang.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Analyzers.Lang.Ruby/StellaOps.Scanner.Analyzers.Lang.Ruby.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Core/StellaOps.Scanner.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\**\*" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,6 +1,17 @@
|
||||
// Deterministic Deno workspace exercising vendor, npm, FFI, worker, and fetch flows.
|
||||
{
|
||||
"importMap": "./import_map.json",
|
||||
"imports": {
|
||||
"app/": "./src/",
|
||||
"ffi/": "./src/ffi/",
|
||||
"workers/": "./src/workers/",
|
||||
"npmDynamic": "npm:dayjs@1",
|
||||
"nodeFs": "node:fs",
|
||||
"nodeCrypto": "node:crypto",
|
||||
"nodeWorker": "node:worker_threads",
|
||||
"denoFfi": "deno:ffi",
|
||||
"data": "./data/data.json"
|
||||
},
|
||||
"lock": {
|
||||
"enabled": true,
|
||||
"path": "./deno.lock"
|
||||
|
||||
@@ -1 +1,198 @@
|
||||
"pending"
|
||||
[
|
||||
{
|
||||
"analyzerId": "deno",
|
||||
"componentKey": "container::bundle:<workspace>/bundles/sample.deno",
|
||||
"name": "<workspace>/bundles/sample.deno",
|
||||
"type": "deno-container",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"deno.container.bundle.entrypoint": "mod.ts",
|
||||
"deno.container.bundle.modules": "2",
|
||||
"deno.container.bundle.resources": "1",
|
||||
"deno.container.identifier": "<workspace>/bundles/sample.deno",
|
||||
"deno.container.kind": "bundle",
|
||||
"deno.container.meta.entrypoint": "mod.ts",
|
||||
"deno.container.meta.moduleCount": "2",
|
||||
"deno.container.meta.resourceCount": "1"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deno.bundle",
|
||||
"locator": "<workspace>/bundles/sample.deno",
|
||||
"value": "mod.ts"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "deno.container",
|
||||
"locator": "Bundle",
|
||||
"value": "<workspace>/bundles/sample.deno"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "deno",
|
||||
"componentKey": "container::bundle:<workspace>/bundles/sample.eszip",
|
||||
"name": "<workspace>/bundles/sample.eszip",
|
||||
"type": "deno-container",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"deno.container.bundle.entrypoint": "mod.ts",
|
||||
"deno.container.bundle.modules": "2",
|
||||
"deno.container.bundle.resources": "1",
|
||||
"deno.container.identifier": "<workspace>/bundles/sample.eszip",
|
||||
"deno.container.kind": "bundle",
|
||||
"deno.container.meta.entrypoint": "mod.ts",
|
||||
"deno.container.meta.moduleCount": "2",
|
||||
"deno.container.meta.resourceCount": "1"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "deno.bundle",
|
||||
"locator": "<workspace>/bundles/sample.eszip",
|
||||
"value": "mod.ts"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "deno.container",
|
||||
"locator": "Bundle",
|
||||
"value": "<workspace>/bundles/sample.eszip"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "deno",
|
||||
"componentKey": "container::cache:.deno-<hash>",
|
||||
"name": ".deno-<hash>",
|
||||
"type": "deno-container",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"deno.container.identifier": ".deno-<hash>",
|
||||
"deno.container.kind": "cache",
|
||||
"deno.container.meta.alias": ".deno-<hash>",
|
||||
"deno.container.meta.kind": "Workspace",
|
||||
"deno.container.meta.path": "<workspace>/.deno"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "deno.container",
|
||||
"locator": "Cache",
|
||||
"value": ".deno-<hash>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "deno",
|
||||
"componentKey": "container::cache:.deno-<hash>",
|
||||
"name": ".deno-<hash>",
|
||||
"type": "deno-container",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"deno.container.identifier": ".deno-<hash>",
|
||||
"deno.container.kind": "cache",
|
||||
"deno.container.layerDigest": "deadbeef",
|
||||
"deno.container.meta.alias": ".deno-<hash>",
|
||||
"deno.container.meta.kind": "Layer",
|
||||
"deno.container.meta.path": "<workspace>/layers/sha256-deadbeef/fs/.deno"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "deno.container",
|
||||
"locator": "Cache",
|
||||
"value": ".deno-<hash>",
|
||||
"sha256": "deadbeef"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "deno",
|
||||
"componentKey": "container::cache:env-deno-<hash>",
|
||||
"name": "env-deno-<hash>",
|
||||
"type": "deno-container",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"deno.container.identifier": "env-deno-<hash>",
|
||||
"deno.container.kind": "cache",
|
||||
"deno.container.meta.alias": "env-deno-<hash>",
|
||||
"deno.container.meta.kind": "Env",
|
||||
"deno.container.meta.path": "<workspace>/env-deno"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "deno.container",
|
||||
"locator": "Cache",
|
||||
"value": "env-deno-<hash>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "deno",
|
||||
"componentKey": "container::vendor:vendor-<hash>",
|
||||
"name": "vendor-<hash>",
|
||||
"type": "deno-container",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"deno.container.identifier": "vendor-<hash>",
|
||||
"deno.container.kind": "vendor",
|
||||
"deno.container.layerDigest": "deadbeef",
|
||||
"deno.container.meta.alias": "vendor-<hash>",
|
||||
"deno.container.meta.path": "<workspace>/layers/sha256-deadbeef/fs/vendor"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "deno.container",
|
||||
"locator": "Vendor",
|
||||
"value": "vendor-<hash>",
|
||||
"sha256": "deadbeef"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "deno",
|
||||
"componentKey": "container::vendor:vendor-<hash>",
|
||||
"name": "vendor-<hash>",
|
||||
"type": "deno-container",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"deno.container.identifier": "vendor-<hash>",
|
||||
"deno.container.kind": "vendor",
|
||||
"deno.container.meta.alias": "vendor-<hash>",
|
||||
"deno.container.meta.path": "<workspace>/vendor"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "deno.container",
|
||||
"locator": "Vendor",
|
||||
"value": "vendor-<hash>"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "deno",
|
||||
"componentKey": "observation::deno",
|
||||
"name": "Deno Observation Summary",
|
||||
"type": "deno-observation",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"deno.observation.bundles": "2",
|
||||
"deno.observation.capabilities": "1",
|
||||
"deno.observation.entrypoints": "1",
|
||||
"deno.observation.hash": "<hash>"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "derived",
|
||||
"source": "deno.observation",
|
||||
"locator": "document",
|
||||
"value": "{\"entrypoints\":[\"mod.ts\"],\"modules\":[\"./src/\",\"./src/ffi/\",\"./src/workers/\",\"https://api.example.com/data.json\",\"https://cdn.example.com/dynamic/mod.ts\",\"https://deno.land/std@0.207.0/http/server.ts\",\"https://example.com/env.ts\",\"https://example.com/layer.ts\",\"https://import_map.json\",\"https://layer.example/\"],\"capabilities\":[{\"capability\":\"Network\",\"reason\":\"network.remote_module_import\",\"sources\":[\"https://api.example.com/data.json\",\"https://cdn.example.com/dynamic/mod.ts\",\"https://deno.land/std/http/server.ts\",\"https://deno.land/std@0.207.0/http/server.ts\",\"https://example.com/env.ts\",\"https://example.com/layer.ts\",\"https://import_map.json\"]}],\"dynamicImports\":[],\"literalFetches\":[],\"bundles\":[{\"path\":\"<workspace>/bundles/sample.deno\",\"type\":\"deno-compile\",\"entrypoint\":\"mod.ts\",\"modules\":2,\"resources\":1},{\"path\":\"<workspace>/bundles/sample.eszip\",\"type\":\"eszip\",\"entrypoint\":\"mod.ts\",\"modules\":2,\"resources\":1}]}",
|
||||
"sha256": "<hash>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -7,6 +7,7 @@
|
||||
"nodeFs": "node:fs",
|
||||
"nodeCrypto": "node:crypto",
|
||||
"nodeWorker": "node:worker_threads",
|
||||
"denoFfi": "deno:ffi"
|
||||
"denoFfi": "deno:ffi",
|
||||
"data": "./data/data.json"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user