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:
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user