feat: Implement Runtime Facts ingestion service and NDJSON reader
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 RuntimeFactsNdjsonReader for reading NDJSON formatted runtime facts. - Introduced IRuntimeFactsIngestionService interface and its implementation. - Enhanced Program.cs to register new services and endpoints for runtime facts. - Updated CallgraphIngestionService to include CAS URI in stored artifacts. - Created RuntimeFactsValidationException for validation errors during ingestion. - Added tests for RuntimeFactsIngestionService and RuntimeFactsNdjsonReader. - Implemented SignalsSealedModeMonitor for compliance checks in sealed mode. - Updated project dependencies for testing utilities.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestFixtures;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Bundles;
|
||||
|
||||
public sealed class BundleInspectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void EszipInspectorExtractsManifest()
|
||||
{
|
||||
var temp = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var eszipPath = BundleFixtureBuilder.CreateSampleEszip(temp);
|
||||
var result = DenoBundleInspector.TryInspect(eszipPath, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("eszip", result!.BundleType);
|
||||
Assert.Equal("mod.ts", result.Entrypoint);
|
||||
Assert.Equal(2, result.Modules.Length);
|
||||
Assert.Contains(result.Modules, module => module.Specifier == "file:///mod.ts");
|
||||
Assert.Single(result.Resources);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(temp);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DenoCompileInspectorExtractsEmbeddedEszip()
|
||||
{
|
||||
var temp = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var binaryPath = BundleFixtureBuilder.CreateSampleCompiledBinary(temp);
|
||||
var result = DenoCompileInspector.TryInspect(binaryPath, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("deno-compile", result!.BundleType);
|
||||
Assert.Equal("mod.ts", result.Entrypoint);
|
||||
Assert.Equal(2, result.Modules.Length);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(temp);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BundleScannerProducesObservations()
|
||||
{
|
||||
var temp = TestPaths.CreateTemporaryDirectory();
|
||||
try
|
||||
{
|
||||
var eszip = BundleFixtureBuilder.CreateSampleEszip(temp);
|
||||
var binary = BundleFixtureBuilder.CreateSampleCompiledBinary(temp);
|
||||
Assert.True(File.Exists(eszip));
|
||||
Assert.True(File.Exists(binary));
|
||||
|
||||
var scan = DenoBundleScanner.Scan(temp, CancellationToken.None);
|
||||
var observations = DenoBundleScanner.ToObservations(scan);
|
||||
|
||||
Assert.Equal(1, scan.EszipBundles.Length);
|
||||
Assert.Equal(1, scan.CompiledBundles.Length);
|
||||
Assert.Equal(2, observations.Length);
|
||||
Assert.Contains(observations, obs => obs.BundleType == "eszip");
|
||||
Assert.Contains(observations, obs => obs.BundleType == "deno-compile");
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestFixtures;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Containers;
|
||||
|
||||
public sealed class ContainerAdapterTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CollectInputsIncludesCacheVendorAndBundlesAsync()
|
||||
{
|
||||
var (root, envDir) = DenoWorkspaceTestFixture.Create();
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable("DENO_DIR", envDir);
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var workspace = await DenoWorkspaceNormalizer.NormalizeAsync(context, CancellationToken.None);
|
||||
var bundleScan = DenoBundleScanner.Scan(root, CancellationToken.None);
|
||||
var observations = DenoBundleScanner.ToObservations(bundleScan);
|
||||
|
||||
var inputs = DenoContainerAdapter.CollectInputs(workspace, observations);
|
||||
|
||||
Assert.NotEmpty(inputs);
|
||||
Assert.Contains(inputs, input => input.Kind == DenoContainerSourceKind.Cache);
|
||||
Assert.Contains(inputs, input => input.Kind == DenoContainerSourceKind.Vendor);
|
||||
Assert.Contains(inputs, input => input.Kind == DenoContainerSourceKind.Bundle);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("DENO_DIR", null);
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Containers;
|
||||
|
||||
public sealed class ContainerEmitterTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildRecordsProducesComponents()
|
||||
{
|
||||
var inputs = new[]
|
||||
{
|
||||
new DenoContainerInput(
|
||||
DenoContainerSourceKind.Cache,
|
||||
"cache-alias",
|
||||
"sha256:abc",
|
||||
new Dictionary<string, string?> { ["path"] = "/cache/path" },
|
||||
Bundle: null),
|
||||
new DenoContainerInput(
|
||||
DenoContainerSourceKind.Bundle,
|
||||
"/path/bundle.eszip",
|
||||
null,
|
||||
new Dictionary<string, string?>(),
|
||||
new DenoBundleObservation(
|
||||
"/path/bundle.eszip",
|
||||
"eszip",
|
||||
"mod.ts",
|
||||
ImmutableArray<DenoBundleModule>.Empty,
|
||||
ImmutableArray<DenoBundleResource>.Empty))
|
||||
};
|
||||
|
||||
var records = DenoContainerEmitter.BuildRecords("deno", inputs);
|
||||
|
||||
Assert.Equal(2, records.Length);
|
||||
Assert.Contains(records, record => record.Metadata.ContainsKey("deno.container.bundle.entrypoint"));
|
||||
Assert.Contains(records, record => record.Metadata.ContainsKey("deno.container.layerDigest"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using System.Linq;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestFixtures;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Deno;
|
||||
|
||||
public sealed class DenoWorkspaceNormalizerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task WorkspaceFixtureProducesDeterministicOutputAsync()
|
||||
{
|
||||
var (root, envDenoDir) = DenoWorkspaceTestFixture.Create();
|
||||
var previousDenoDir = Environment.GetEnvironmentVariable("DENO_DIR");
|
||||
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable("DENO_DIR", envDenoDir);
|
||||
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var workspace = await DenoWorkspaceNormalizer.NormalizeAsync(context, CancellationToken.None);
|
||||
|
||||
var config = Assert.Single(workspace.Configurations);
|
||||
Assert.EndsWith("deno.jsonc", config.AbsolutePath, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(config.LockEnabled);
|
||||
Assert.NotNull(config.InlineImportMap);
|
||||
Assert.NotNull(config.ImportMapPath);
|
||||
Assert.NotNull(config.VendorDirectoryPath);
|
||||
|
||||
Assert.Contains(workspace.ImportMaps, map => map.IsInline);
|
||||
Assert.Contains(
|
||||
workspace.ImportMaps,
|
||||
map => map.AbsolutePath is not null && map.AbsolutePath.EndsWith("import_map.json", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
Assert.Contains(workspace.LockFiles, file => file.AbsolutePath.EndsWith("deno.lock", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.Contains(workspace.Vendors, vendor => vendor.AbsolutePath.EndsWith(Path.Combine("vendor"), StringComparison.OrdinalIgnoreCase));
|
||||
Assert.Contains(workspace.CacheLocations, cache => cache.Kind == DenoCacheLocationKind.Env);
|
||||
Assert.Contains(
|
||||
workspace.CacheLocations,
|
||||
cache => cache.Kind == DenoCacheLocationKind.Workspace && cache.AbsolutePath.EndsWith(".deno", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
Assert.Contains(workspace.FileSystem.Files, file => file.VirtualPath == "workspace://deno.jsonc");
|
||||
Assert.Contains(workspace.FileSystem.Files, file => file.VirtualPath.StartsWith("vendor://", StringComparison.Ordinal));
|
||||
Assert.Contains(
|
||||
workspace.FileSystem.Files,
|
||||
file => file.VirtualPath.StartsWith("deno-dir://", StringComparison.Ordinal) ||
|
||||
file.VirtualPath.StartsWith("layer://", StringComparison.Ordinal));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("DENO_DIR", previousDenoDir);
|
||||
DenoWorkspaceTestFixture.Cleanup(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GraphResolverCapturesImportAndCacheEdgesAsync()
|
||||
{
|
||||
var (root, envDenoDir) = DenoWorkspaceTestFixture.Create();
|
||||
var previousDenoDir = Environment.GetEnvironmentVariable("DENO_DIR");
|
||||
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable("DENO_DIR", envDenoDir);
|
||||
|
||||
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
|
||||
var workspace = await DenoWorkspaceNormalizer.NormalizeAsync(context, CancellationToken.None);
|
||||
var lockFile = workspace.LockFiles.Single(lf => string.Equals(lf.RelativePath, "deno.lock", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.Contains(lockFile.RemoteEntries, entry => entry.Key.Contains("server.ts", StringComparison.Ordinal));
|
||||
|
||||
var graph = DenoModuleGraphResolver.Resolve(workspace, CancellationToken.None);
|
||||
var compatibility = DenoNpmCompatibilityAdapter.Analyze(workspace, graph, CancellationToken.None);
|
||||
|
||||
Assert.NotEmpty(graph.Nodes);
|
||||
Assert.NotEmpty(graph.Edges);
|
||||
|
||||
var remoteNode = graph.Nodes.FirstOrDefault(
|
||||
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);
|
||||
|
||||
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" &&
|
||||
edge.Resolution == "dayjs@1.11.12");
|
||||
|
||||
Assert.Contains(
|
||||
graph.Edges,
|
||||
edge => edge.ImportKind == DenoImportKind.JsonAssertion &&
|
||||
edge.Specifier == "data" &&
|
||||
edge.Resolution?.EndsWith("data/data.json", StringComparison.OrdinalIgnoreCase) == true);
|
||||
|
||||
Assert.Contains(
|
||||
graph.Edges,
|
||||
edge => edge.ImportKind == DenoImportKind.Redirect &&
|
||||
edge.Specifier == "https://deno.land/std/http/server.ts");
|
||||
|
||||
Assert.Contains(
|
||||
compatibility.BuiltinUsages,
|
||||
usage => usage.Specifier == "node:fs");
|
||||
|
||||
var npmResolution = compatibility.NpmResolutions.First(res => res.Specifier == "npm:dayjs@1");
|
||||
Assert.True(npmResolution.ExistsOnDisk);
|
||||
Assert.Equal("deno", npmResolution.Condition);
|
||||
Assert.True(npmResolution.ResolvedPath?.EndsWith("deno.mod.ts", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("DENO_DIR", previousDenoDir);
|
||||
DenoWorkspaceTestFixture.Cleanup(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scanner.Analyzers.Lang;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestFixtures;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Observations;
|
||||
|
||||
public sealed class DenoLanguageAnalyzerObservationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AnalyzerStoresObservationPayloadInAnalysisStoreAsync()
|
||||
{
|
||||
var (root, envDenoDir) = DenoWorkspaceTestFixture.Create();
|
||||
var previousDenoDir = Environment.GetEnvironmentVariable("DENO_DIR");
|
||||
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable("DENO_DIR", envDenoDir);
|
||||
|
||||
var store = new ScanAnalysisStore();
|
||||
var context = new LanguageAnalyzerContext(
|
||||
root,
|
||||
TimeProvider.System,
|
||||
usageHints: null,
|
||||
services: null,
|
||||
analysisStore: store);
|
||||
|
||||
var analyzer = new DenoLanguageAnalyzer();
|
||||
var engine = new LanguageAnalyzerEngine(new[] { analyzer });
|
||||
await engine.AnalyzeAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.True(store.TryGet(ScanAnalysisKeys.DenoObservationPayload, out AnalyzerObservationPayload payload));
|
||||
Assert.Equal("deno.observation", payload.Kind);
|
||||
Assert.Equal("application/json", payload.MediaType);
|
||||
Assert.True(payload.Content.Length > 0);
|
||||
Assert.NotNull(payload.Metadata);
|
||||
Assert.True(payload.Metadata!.ContainsKey("deno.observation.hash"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("DENO_DIR", previousDenoDir);
|
||||
DenoWorkspaceTestFixture.Cleanup(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Deno.Internal.Observations;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.Observations;
|
||||
|
||||
public sealed class ObservationSerializerTests
|
||||
{
|
||||
[Fact]
|
||||
public void SerializeProducesDeterministicJson()
|
||||
{
|
||||
var document = new DenoObservationDocument(
|
||||
ImmutableArray.Create("mod.ts"),
|
||||
ImmutableArray.Create("https://example.com/deps.ts"),
|
||||
ImmutableArray<DenoCapabilityRecord>.Empty,
|
||||
ImmutableArray<DenoDynamicImportObservation>.Empty,
|
||||
ImmutableArray<DenoLiteralFetchObservation>.Empty,
|
||||
ImmutableArray.Create(new DenoObservationBundleSummary("bundle.eszip", "eszip", "mod.ts", 2, 1)));
|
||||
|
||||
var json = DenoObservationSerializer.Serialize(document);
|
||||
var hash = DenoObservationSerializer.ComputeSha256(json);
|
||||
|
||||
Assert.Equal("sha256:" + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(json))).ToLowerInvariant(), hash);
|
||||
Assert.Contains("\"entrypoints\":[\"mod.ts\"]", json, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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>
|
||||
<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.Deno\\StellaOps.Scanner.Analyzers.Lang.Deno.csproj" />
|
||||
<ProjectReference Include="..\\..\\__Libraries\\StellaOps.Scanner.Core\\StellaOps.Scanner.Core.csproj" />
|
||||
</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>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestFixtures;
|
||||
|
||||
internal static class BundleFixtureBuilder
|
||||
{
|
||||
public static string CreateSampleEszip(string directory)
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
var bundlePath = Path.Combine(directory, "sample.eszip");
|
||||
|
||||
using (var archive = ZipFile.Open(bundlePath, ZipArchiveMode.Create))
|
||||
{
|
||||
var manifest = archive.CreateEntry("manifest.json", CompressionLevel.NoCompression);
|
||||
using (var writer = new StreamWriter(manifest.Open(), Encoding.UTF8))
|
||||
{
|
||||
writer.Write("""
|
||||
{
|
||||
"entry": "mod.ts",
|
||||
"modules": {
|
||||
"mod.ts": {
|
||||
"specifier": "file:///mod.ts",
|
||||
"path": "modules/mod.ts",
|
||||
"mediaType": "application/typescript"
|
||||
},
|
||||
"deps.ts": {
|
||||
"specifier": "https://example.com/deps.ts",
|
||||
"path": "modules/deps.ts",
|
||||
"mediaType": "application/typescript"
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"name": "assets/config.json",
|
||||
"mediaType": "application/json",
|
||||
"size": 42
|
||||
}
|
||||
]
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
AddTextEntry(archive, "modules/mod.ts", "import \"./deps.ts\";\nconsole.log('hello');\n");
|
||||
AddTextEntry(archive, "modules/deps.ts", "export const value = 42;\n");
|
||||
AddTextEntry(archive, "assets/config.json", "{ \"ok\": true }\n");
|
||||
}
|
||||
|
||||
return bundlePath;
|
||||
}
|
||||
|
||||
public static string CreateSampleCompiledBinary(string directory)
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
var binaryPath = Path.Combine(directory, "sample.deno");
|
||||
var eszipPath = CreateSampleEszip(directory);
|
||||
var eszipBytes = File.ReadAllBytes(eszipPath);
|
||||
|
||||
using var stream = File.Create(binaryPath);
|
||||
using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
|
||||
writer.Write(Encoding.UTF8.GetBytes("FAKE_DENO_COMPILE_HEADER"));
|
||||
writer.Write(Encoding.UTF8.GetBytes(DenoCompileInspector.EszipMarker));
|
||||
writer.Flush();
|
||||
stream.Write(eszipBytes, 0, eszipBytes.Length);
|
||||
|
||||
return binaryPath;
|
||||
}
|
||||
|
||||
private static void AddTextEntry(ZipArchive archive, string entryPath, string contents)
|
||||
{
|
||||
var entry = archive.CreateEntry(entryPath, CompressionLevel.NoCompression);
|
||||
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8);
|
||||
writer.Write(contents);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Deno.Tests.TestFixtures;
|
||||
|
||||
internal static class DenoWorkspaceTestFixture
|
||||
{
|
||||
public static (string RootPath, string EnvDenoDir) Create()
|
||||
{
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
CreateDenoFixture(root, out var envDir);
|
||||
return (root, envDir);
|
||||
}
|
||||
|
||||
public static void Cleanup(string root)
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
|
||||
private static void CreateDenoFixture(string root, out string envDenoDir)
|
||||
{
|
||||
Directory.CreateDirectory(root);
|
||||
|
||||
File.WriteAllText(Path.Combine(root, "deno.jsonc"), """
|
||||
// sample deno config
|
||||
{
|
||||
"importMap": "./import_map.json",
|
||||
"lock": {
|
||||
"enabled": true,
|
||||
"path": "./deno.lock"
|
||||
},
|
||||
"vendor": {
|
||||
"enabled": true,
|
||||
"path": "./vendor"
|
||||
},
|
||||
"nodeModulesDir": false,
|
||||
"imports": {
|
||||
"$std/": "https://deno.land/std@0.207.0/",
|
||||
"app/": "./src/app/",
|
||||
"data": "./data/data.json",
|
||||
"npmDynamic": "npm:dayjs@1",
|
||||
"nodeFs": "node:fs"
|
||||
},
|
||||
"scopes": {
|
||||
"https://deno.land/": {
|
||||
"fmt/": "https://deno.land/std@0.207.0/fmt/"
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
Directory.CreateDirectory(Path.Combine(root, "src", "app"));
|
||||
Directory.CreateDirectory(Path.Combine(root, "data"));
|
||||
File.WriteAllText(Path.Combine(root, "data", "data.json"), "{ \"ok\": true }");
|
||||
File.WriteAllText(
|
||||
Path.Combine(root, "import_map.json"),
|
||||
"""
|
||||
{
|
||||
"imports": {
|
||||
"app/": "./src/app/main.ts",
|
||||
"vendor/": "https://deno.land/std@0.207.0/"
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(root, "deno.lock"),
|
||||
"""
|
||||
{
|
||||
"version": "4",
|
||||
"remote": {
|
||||
"https://deno.land/std@0.207.0/http/server.ts": "sha256-deadbeef",
|
||||
"https://example.com/mod.ts": "sha256-feedface",
|
||||
"node:fs": "builtin"
|
||||
},
|
||||
"redirects": {
|
||||
"https://deno.land/std/http/server.ts": "https://deno.land/std@0.207.0/http/server.ts"
|
||||
},
|
||||
"npm": {
|
||||
"specifiers": {
|
||||
"npm:dayjs@1": "dayjs@1.11.12"
|
||||
},
|
||||
"packages": {
|
||||
"dayjs@1.11.12": {
|
||||
"integrity": "sha512-sample",
|
||||
"dependencies": {
|
||||
"tslib": "tslib@2.6.3"
|
||||
}
|
||||
},
|
||||
"tslib@2.6.3": {
|
||||
"integrity": "sha512-tslib",
|
||||
"dependencies": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var vendorRoot = Path.Combine(root, "vendor", "https", "deno.land", "std@0.207.0", "http");
|
||||
Directory.CreateDirectory(vendorRoot);
|
||||
File.WriteAllText(Path.Combine(vendorRoot, "server.ts"), "export const vendor = true;");
|
||||
|
||||
var vendorBase = Path.Combine(root, "vendor");
|
||||
File.WriteAllText(
|
||||
Path.Combine(vendorBase, "import_map.json"),
|
||||
"""
|
||||
{
|
||||
"imports": {
|
||||
"std/http/server.ts": "https://deno.land/std@0.207.0/http/server.ts"
|
||||
}
|
||||
}
|
||||
""");
|
||||
File.WriteAllText(
|
||||
Path.Combine(vendorBase, "deno.lock"),
|
||||
"""
|
||||
{
|
||||
"version": "1",
|
||||
"remote": {}
|
||||
}
|
||||
""");
|
||||
|
||||
CreateDenoDir(Path.Combine(root, ".deno"), "workspace.ts", includeRegistry: true);
|
||||
|
||||
envDenoDir = Path.Combine(root, "env-deno");
|
||||
CreateDenoDir(envDenoDir, "env.ts");
|
||||
|
||||
var layerFs = Path.Combine(root, "layers", "sha256-deadbeef", "fs");
|
||||
Directory.CreateDirectory(layerFs);
|
||||
CreateDenoDir(Path.Combine(layerFs, ".deno"), "layer.ts");
|
||||
|
||||
var layerVendor = Path.Combine(layerFs, "vendor", "https", "layer.example");
|
||||
Directory.CreateDirectory(layerVendor);
|
||||
File.WriteAllText(Path.Combine(layerFs, "vendor", "import_map.json"), "{\"imports\":{\"layer/\": \"https://layer.example/\"}}");
|
||||
}
|
||||
|
||||
private static void CreateDenoDir(string root, string fileName, bool includeRegistry = false)
|
||||
{
|
||||
var deps = Path.Combine(root, "deps", "https", "example.com");
|
||||
var gen = Path.Combine(root, "gen");
|
||||
var npm = Path.Combine(root, "npm");
|
||||
Directory.CreateDirectory(deps);
|
||||
Directory.CreateDirectory(gen);
|
||||
Directory.CreateDirectory(npm);
|
||||
|
||||
File.WriteAllText(Path.Combine(deps, fileName), "export const cache = true;");
|
||||
File.WriteAllText(Path.Combine(gen, $"{Path.GetFileNameWithoutExtension(fileName)}.js"), "console.log('gen');");
|
||||
File.WriteAllText(Path.Combine(npm, "package.json"), "{}");
|
||||
|
||||
if (includeRegistry)
|
||||
{
|
||||
CreateNpmRegistryPackage(root);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateNpmRegistryPackage(string denoDirRoot)
|
||||
{
|
||||
var registryRoot = Path.Combine(denoDirRoot, "npm", "registry.npmjs.org");
|
||||
var dayjsRoot = Path.Combine(registryRoot, "dayjs", "1.11.12");
|
||||
Directory.CreateDirectory(dayjsRoot);
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(dayjsRoot, "package.json"),
|
||||
"""
|
||||
{
|
||||
"name": "dayjs",
|
||||
"version": "1.11.12",
|
||||
"exports": {
|
||||
".": {
|
||||
"deno": "./deno.mod.ts",
|
||||
"import": "./esm/index.js",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./plugin": "./plugin/index.js"
|
||||
},
|
||||
"main": "./lib/index.js"
|
||||
}
|
||||
""");
|
||||
|
||||
File.WriteAllText(Path.Combine(dayjsRoot, "deno.mod.ts"), "export const deno = true;");
|
||||
Directory.CreateDirectory(Path.Combine(dayjsRoot, "esm"));
|
||||
File.WriteAllText(Path.Combine(dayjsRoot, "esm", "index.js"), "export const esm = true;");
|
||||
Directory.CreateDirectory(Path.Combine(dayjsRoot, "lib"));
|
||||
File.WriteAllText(Path.Combine(dayjsRoot, "lib", "index.js"), "module.exports = true;");
|
||||
Directory.CreateDirectory(Path.Combine(dayjsRoot, "plugin"));
|
||||
File.WriteAllText(Path.Combine(dayjsRoot, "plugin", "index.js"), "export const plugin = true;");
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,35 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "java",
|
||||
"componentKey": "purl::pkg:maven/com/example/demo@1.0.0",
|
||||
"purl": "pkg:maven/com/example/demo@1.0.0",
|
||||
"name": "demo",
|
||||
"version": "1.0.0",
|
||||
"type": "maven",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"artifactId": "demo",
|
||||
"displayName": "Demo Library",
|
||||
"groupId": "com.example",
|
||||
"jarPath": "libs/demo.jar",
|
||||
"manifestTitle": "Demo",
|
||||
"manifestVendor": "Example Corp",
|
||||
"manifestVersion": "1.0.0",
|
||||
"packaging": "jar"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "MANIFEST.MF",
|
||||
"locator": "libs/demo.jar!META-INF/MANIFEST.MF",
|
||||
"value": "title=Demo;version=1.0.0;vendor=Example Corp"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "pom.properties",
|
||||
"locator": "libs/demo.jar!META-INF/maven/com.example/demo/pom.properties",
|
||||
"sha256": "c20f36aa1b9d89d28cf9ed131519ffd6287a4dac0c7cb926130496f3f8157bf1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
[
|
||||
{
|
||||
"analyzerId": "java",
|
||||
"componentKey": "purl::pkg:maven/com/example/demo@1.0.0",
|
||||
"purl": "pkg:maven/com/example/demo@1.0.0",
|
||||
"name": "demo",
|
||||
"version": "1.0.0",
|
||||
"type": "maven",
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"artifactId": "demo",
|
||||
"displayName": "Demo Library",
|
||||
"groupId": "com.example",
|
||||
"jarPath": "libs/demo.jar",
|
||||
"manifestTitle": "Demo",
|
||||
"manifestVendor": "Example Corp",
|
||||
"manifestVersion": "1.0.0",
|
||||
"packaging": "jar"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "MANIFEST.MF",
|
||||
"locator": "libs/demo.jar!META-INF/MANIFEST.MF",
|
||||
"value": "title=Demo;version=1.0.0;vendor=Example Corp"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "pom.properties",
|
||||
"locator": "libs/demo.jar!META-INF/maven/com.example/demo/pom.properties",
|
||||
"sha256": "82e3c738508fbe8110680d88b0db8c2d8013e2a3be3c3a3c6cddfd065e94249d"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
@@ -30,4 +34,103 @@ public sealed class JavaLanguageAnalyzerTests
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LockfilesProduceDeclaredOnlyComponentsAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var root = TestPaths.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
var jarPath = CreateSampleJar(root, "com.example", "runtime-only", "1.0.0");
|
||||
var lockPath = Path.Combine(root, "gradle.lockfile");
|
||||
var lockContent = new StringBuilder()
|
||||
.AppendLine("com.example:declared-only:2.0.0=runtimeClasspath")
|
||||
.ToString();
|
||||
await File.WriteAllTextAsync(lockPath, lockContent, cancellationToken);
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[] { new JavaLanguageAnalyzer() };
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
root,
|
||||
analyzers,
|
||||
cancellationToken,
|
||||
new LanguageUsageHints(new[] { jarPath }));
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var rootElement = document.RootElement;
|
||||
|
||||
Assert.True(ComponentHasMetadata(rootElement, "declared-only", "declaredOnly", "true"));
|
||||
Assert.True(ComponentHasMetadata(rootElement, "declared-only", "lockSource", "gradle.lockfile"));
|
||||
Assert.True(ComponentHasMetadata(rootElement, "runtime-only", "lockMissing", "true"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(root);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ComponentHasMetadata(JsonElement root, string componentName, string key, string expected)
|
||||
{
|
||||
foreach (var element in root.EnumerateArray())
|
||||
{
|
||||
if (!element.TryGetProperty("name", out var nameElement) ||
|
||||
!string.Equals(nameElement.GetString(), componentName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!element.TryGetProperty("metadata", out var metadataElement) || metadataElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!metadataElement.TryGetProperty(key, out var valueElement))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(valueElement.GetString(), expected, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string CreateSampleJar(string root, string groupId, string artifactId, string version)
|
||||
{
|
||||
var jarPath = Path.Combine(root, $"{artifactId}-{version}.jar");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
|
||||
|
||||
using var archive = ZipFile.Open(jarPath, ZipArchiveMode.Create);
|
||||
var pomPropertiesPath = $"META-INF/maven/{groupId}/{artifactId}/pom.properties";
|
||||
var pomPropertiesEntry = archive.CreateEntry(pomPropertiesPath);
|
||||
using (var writer = new StreamWriter(pomPropertiesEntry.Open(), Encoding.UTF8))
|
||||
{
|
||||
writer.WriteLine($"groupId={groupId}");
|
||||
writer.WriteLine($"artifactId={artifactId}");
|
||||
writer.WriteLine($"version={version}");
|
||||
writer.WriteLine("packaging=jar");
|
||||
writer.WriteLine("name=Sample");
|
||||
}
|
||||
|
||||
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
|
||||
using (var writer = new StreamWriter(manifestEntry.Open(), Encoding.UTF8))
|
||||
{
|
||||
writer.WriteLine("Manifest-Version: 1.0");
|
||||
writer.WriteLine($"Implementation-Title: {artifactId}");
|
||||
writer.WriteLine($"Implementation-Version: {version}");
|
||||
writer.WriteLine($"Implementation-Vendor: {groupId}");
|
||||
}
|
||||
|
||||
var classEntry = archive.CreateEntry($"{artifactId.Replace('-', '_')}/Main.class");
|
||||
using (var stream = classEntry.Open())
|
||||
{
|
||||
stream.Write(new byte[] { 0xCA, 0xFE, 0xBA, 0xBE });
|
||||
}
|
||||
|
||||
return jarPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,134 +1,157 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/left-pad@1.3.0",
|
||||
"purl": "pkg:npm/left-pad@1.3.0",
|
||||
"name": "left-pad",
|
||||
"version": "1.3.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"integrity": "sha512-LEFTPAD",
|
||||
"path": "packages/app/node_modules/left-pad",
|
||||
"resolved": "https://registry.example/left-pad-1.3.0.tgz"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "packages/app/node_modules/left-pad/package.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/lib@2.0.1",
|
||||
"purl": "pkg:npm/lib@2.0.1",
|
||||
"name": "lib",
|
||||
"version": "2.0.1",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"integrity": "sha512-LIB",
|
||||
"path": "packages/lib",
|
||||
"resolved": "https://registry.example/lib-2.0.1.tgz",
|
||||
"workspaceLink": "packages/app/node_modules/lib",
|
||||
"workspaceMember": "true",
|
||||
"workspaceRoot": "packages/lib"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "packages/app/node_modules/lib/package.json"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "packages/lib/package.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/root-workspace@1.0.0",
|
||||
"purl": "pkg:npm/root-workspace@1.0.0",
|
||||
"name": "root-workspace",
|
||||
"version": "1.0.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"path": ".",
|
||||
"private": "true"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "package.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/shared@3.1.4",
|
||||
"purl": "pkg:npm/shared@3.1.4",
|
||||
"name": "shared",
|
||||
"version": "3.1.4",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"integrity": "sha512-SHARED",
|
||||
"path": "packages/shared",
|
||||
"resolved": "https://registry.example/shared-3.1.4.tgz",
|
||||
"workspaceLink": "packages/app/node_modules/shared",
|
||||
"workspaceMember": "true",
|
||||
"workspaceRoot": "packages/shared",
|
||||
"workspaceTargets": "packages/lib"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "packages/app/node_modules/shared/package.json"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "packages/shared/package.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/workspace-app@1.0.0",
|
||||
"purl": "pkg:npm/workspace-app@1.0.0",
|
||||
"name": "workspace-app",
|
||||
"version": "1.0.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"installScripts": "true",
|
||||
"path": "packages/app",
|
||||
"policyHint.installLifecycle": "postinstall",
|
||||
"script.postinstall": "node scripts/setup.js",
|
||||
"workspaceMember": "true",
|
||||
"workspaceRoot": "packages/app",
|
||||
"workspaceTargets": "packages/lib;packages/shared"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "packages/app/package.json"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:scripts",
|
||||
"locator": "packages/app/package.json#scripts.postinstall",
|
||||
"value": "node scripts/setup.js",
|
||||
"sha256": "f9ae4e4c9313857d1acc31947cee9984232cbefe93c8a56c718804744992728a"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
[
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/declared-only@9.9.9",
|
||||
"purl": "pkg:npm/declared-only@9.9.9",
|
||||
"name": "declared-only",
|
||||
"version": "9.9.9",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"declaredOnly": "true",
|
||||
"integrity": "sha512-DECLAREDONLY",
|
||||
"lockLocator": "package-lock.json:packages/app/node_modules/declared-only",
|
||||
"lockSource": "package-lock.json",
|
||||
"path": ".",
|
||||
"resolved": "https://registry.example/declared-only-9.9.9.tgz"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package-lock.json",
|
||||
"locator": "package-lock.json:packages/app/node_modules/declared-only"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/left-pad@1.3.0",
|
||||
"purl": "pkg:npm/left-pad@1.3.0",
|
||||
"name": "left-pad",
|
||||
"version": "1.3.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"declaredOnly": "true",
|
||||
"integrity": "sha512-LEFTPAD",
|
||||
"lockLocator": "package-lock.json:packages/app/node_modules/left-pad",
|
||||
"lockSource": "package-lock.json",
|
||||
"path": ".",
|
||||
"resolved": "https://registry.example/left-pad-1.3.0.tgz"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package-lock.json",
|
||||
"locator": "package-lock.json:packages/app/node_modules/left-pad"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/lib@2.0.1",
|
||||
"purl": "pkg:npm/lib@2.0.1",
|
||||
"name": "lib",
|
||||
"version": "2.0.1",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"integrity": "sha512-LIB",
|
||||
"lockLocator": "package-lock.json:packages/lib",
|
||||
"lockSource": "package-lock.json",
|
||||
"path": "packages/lib",
|
||||
"resolved": "https://registry.example/lib-2.0.1.tgz",
|
||||
"workspaceMember": "true",
|
||||
"workspaceRoot": "packages/lib"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "packages/lib/package.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/root-workspace@1.0.0",
|
||||
"purl": "pkg:npm/root-workspace@1.0.0",
|
||||
"name": "root-workspace",
|
||||
"version": "1.0.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"lockLocator": "package-lock.json",
|
||||
"lockSource": "package-lock.json",
|
||||
"path": ".",
|
||||
"private": "true"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "package.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/shared@3.1.4",
|
||||
"purl": "pkg:npm/shared@3.1.4",
|
||||
"name": "shared",
|
||||
"version": "3.1.4",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"integrity": "sha512-SHARED",
|
||||
"lockLocator": "package-lock.json:packages/shared",
|
||||
"lockSource": "package-lock.json",
|
||||
"path": "packages/shared",
|
||||
"resolved": "https://registry.example/shared-3.1.4.tgz",
|
||||
"workspaceMember": "true",
|
||||
"workspaceRoot": "packages/shared",
|
||||
"workspaceTargets": "packages/lib"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "packages/shared/package.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/workspace-app@1.0.0",
|
||||
"purl": "pkg:npm/workspace-app@1.0.0",
|
||||
"name": "workspace-app",
|
||||
"version": "1.0.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"installScripts": "true",
|
||||
"lockLocator": "package-lock.json:packages/app",
|
||||
"lockSource": "package-lock.json",
|
||||
"path": "packages/app",
|
||||
"policyHint.installLifecycle": "postinstall",
|
||||
"script.postinstall": "node scripts/setup.js",
|
||||
"workspaceMember": "true",
|
||||
"workspaceRoot": "packages/app",
|
||||
"workspaceTargets": "packages/lib;packages/shared"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "packages/app/package.json"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:scripts",
|
||||
"locator": "packages/app/package.json#scripts.postinstall",
|
||||
"value": "node scripts/setup.js",
|
||||
"sha256": "f9ae4e4c9313857d1acc31947cee9984232cbefe93c8a56c718804744992728a"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -39,11 +39,17 @@
|
||||
"resolved": "https://registry.example/shared-3.1.4.tgz",
|
||||
"integrity": "sha512-SHARED"
|
||||
},
|
||||
"packages/app/node_modules/left-pad": {
|
||||
"name": "left-pad",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.example/left-pad-1.3.0.tgz",
|
||||
"integrity": "sha512-LEFTPAD"
|
||||
}
|
||||
}
|
||||
}
|
||||
"packages/app/node_modules/left-pad": {
|
||||
"name": "left-pad",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.example/left-pad-1.3.0.tgz",
|
||||
"integrity": "sha512-LEFTPAD"
|
||||
},
|
||||
"packages/app/node_modules/declared-only": {
|
||||
"name": "declared-only",
|
||||
"version": "9.9.9",
|
||||
"resolved": "https://registry.example/declared-only-9.9.9.tgz",
|
||||
"integrity": "sha512-DECLAREDONLY"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Python;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Python.Tests;
|
||||
|
||||
@@ -80,4 +83,126 @@ public sealed class PythonLanguageAnalyzerTests
|
||||
cancellationToken,
|
||||
usageHints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LockfileCollectorEmitsDeclaredOnlyComponentsAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = CreateTemporaryWorkspace();
|
||||
try
|
||||
{
|
||||
await CreatePythonPackageAsync(fixturePath, "locked", "1.0.0", cancellationToken);
|
||||
await CreatePythonPackageAsync(fixturePath, "runtime-only", "2.0.0", cancellationToken);
|
||||
|
||||
var requirementsPath = Path.Combine(fixturePath, "requirements.txt");
|
||||
var requirements = new StringBuilder()
|
||||
.AppendLine("locked==1.0.0")
|
||||
.AppendLine("declared-only==3.0.0")
|
||||
.ToString();
|
||||
await File.WriteAllTextAsync(requirementsPath, requirements, cancellationToken);
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new PythonLanguageAnalyzer()
|
||||
};
|
||||
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
fixturePath,
|
||||
analyzers,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
|
||||
Assert.True(ComponentHasMetadata(root, "declared-only", "declaredOnly", "true"));
|
||||
Assert.True(ComponentHasMetadata(root, "declared-only", "lockSource", "requirements.txt"));
|
||||
Assert.True(ComponentHasMetadata(root, "locked", "lockSource", "requirements.txt"));
|
||||
Assert.True(ComponentHasMetadata(root, "runtime-only", "lockMissing", "true"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(fixturePath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task CreatePythonPackageAsync(string root, string name, string version, CancellationToken cancellationToken)
|
||||
{
|
||||
var sitePackages = Path.Combine(root, "lib", "python3.11", "site-packages");
|
||||
Directory.CreateDirectory(sitePackages);
|
||||
|
||||
var packageDir = Path.Combine(sitePackages, name);
|
||||
Directory.CreateDirectory(packageDir);
|
||||
|
||||
var modulePath = Path.Combine(packageDir, "__init__.py");
|
||||
var moduleContent = $"__version__ = \"{version}\"{Environment.NewLine}";
|
||||
await File.WriteAllTextAsync(modulePath, moduleContent, cancellationToken);
|
||||
|
||||
var distInfoDir = Path.Combine(sitePackages, $"{name}-{version}.dist-info");
|
||||
Directory.CreateDirectory(distInfoDir);
|
||||
|
||||
var metadataPath = Path.Combine(distInfoDir, "METADATA");
|
||||
var metadataContent = $"Metadata-Version: 2.1{Environment.NewLine}Name: {name}{Environment.NewLine}Version: {version}{Environment.NewLine}";
|
||||
await File.WriteAllTextAsync(metadataPath, metadataContent, cancellationToken);
|
||||
|
||||
var wheelPath = Path.Combine(distInfoDir, "WHEEL");
|
||||
await File.WriteAllTextAsync(wheelPath, "Wheel-Version: 1.0", cancellationToken);
|
||||
|
||||
var entryPointsPath = Path.Combine(distInfoDir, "entry_points.txt");
|
||||
await File.WriteAllTextAsync(entryPointsPath, string.Empty, cancellationToken);
|
||||
|
||||
var recordPath = Path.Combine(distInfoDir, "RECORD");
|
||||
var recordContent = new StringBuilder()
|
||||
.AppendLine($"{name}/__init__.py,sha256={ComputeSha256Base64(modulePath)},{new FileInfo(modulePath).Length}")
|
||||
.AppendLine($"{name}-{version}.dist-info/METADATA,sha256={ComputeSha256Base64(metadataPath)},{new FileInfo(metadataPath).Length}")
|
||||
.AppendLine($"{name}-{version}.dist-info/RECORD,,")
|
||||
.AppendLine($"{name}-{version}.dist-info/WHEEL,sha256={ComputeSha256Base64(wheelPath)},{new FileInfo(wheelPath).Length}")
|
||||
.AppendLine($"{name}-{version}.dist-info/entry_points.txt,sha256={ComputeSha256Base64(entryPointsPath)},{new FileInfo(entryPointsPath).Length}")
|
||||
.ToString();
|
||||
await File.WriteAllTextAsync(recordPath, recordContent, cancellationToken);
|
||||
}
|
||||
|
||||
private static bool ComponentHasMetadata(JsonElement root, string componentName, string key, string? expectedValue)
|
||||
{
|
||||
foreach (var component in root.EnumerateArray())
|
||||
{
|
||||
if (!component.TryGetProperty("name", out var nameElement) ||
|
||||
!string.Equals(nameElement.GetString(), componentName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!component.TryGetProperty("metadata", out var metadataElement) || metadataElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!metadataElement.TryGetProperty(key, out var valueElement))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var actual = valueElement.GetString();
|
||||
if (string.Equals(actual, expectedValue, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ComputeSha256Base64(string path)
|
||||
{
|
||||
using var sha = SHA256.Create();
|
||||
using var stream = File.OpenRead(path);
|
||||
var hash = sha.ComputeHash(stream);
|
||||
return Convert.ToBase64String(hash);
|
||||
}
|
||||
|
||||
private static string CreateTemporaryWorkspace()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"stellaops-python-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
require "net/http"
|
||||
require "yaml"
|
||||
require "oj"
|
||||
require "puma"
|
||||
require "rake"
|
||||
require "clockwork"
|
||||
require "resque"
|
||||
require "sidekiq"
|
||||
require "custom-gem"
|
||||
|
||||
class ExampleJob < ActiveJob::Base
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform(payload)
|
||||
Kernel.system("echo #{payload}")
|
||||
response = Net::HTTP.get(URI("https://scanner.example.invalid"))
|
||||
Marshal.load(response)
|
||||
YAML.load("---\nvalue: #{payload}")
|
||||
Oj.load('{"a":1}')
|
||||
Resque.enqueue(ExampleJob, payload)
|
||||
Sidekiq::Client.push("class" => "ExampleJob", "args" => [payload])
|
||||
end
|
||||
end
|
||||
|
||||
Clockwork.every(1.minute, "heartbeat.job") do
|
||||
system("echo heartbeat")
|
||||
end
|
||||
@@ -6,10 +6,22 @@
|
||||
"name": "custom-gem",
|
||||
"version": "1.0.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "activejob;clockwork;resque;sidekiq",
|
||||
"capability.scheduler.activejob": "true",
|
||||
"capability.scheduler.clockwork": "true",
|
||||
"capability.scheduler.resque": "true",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"lockfile": "vendor/cache/custom-gem-1.0.0.gem",
|
||||
"runtime.entrypoints": "app/main.rb",
|
||||
"runtime.files": "app/main.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "vendor-cache"
|
||||
},
|
||||
"evidence": [
|
||||
@@ -27,10 +39,22 @@
|
||||
"name": "puma",
|
||||
"version": "6.4.2",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "activejob;clockwork;resque;sidekiq",
|
||||
"capability.scheduler.activejob": "true",
|
||||
"capability.scheduler.clockwork": "true",
|
||||
"capability.scheduler.resque": "true",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.entrypoints": "app/main.rb",
|
||||
"runtime.files": "app/main.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
@@ -48,10 +72,22 @@
|
||||
"name": "rake",
|
||||
"version": "13.1.0",
|
||||
"type": "gem",
|
||||
"usedByEntrypoint": false,
|
||||
"usedByEntrypoint": true,
|
||||
"metadata": {
|
||||
"capability.exec": "true",
|
||||
"capability.net": "true",
|
||||
"capability.scheduler": "activejob;clockwork;resque;sidekiq",
|
||||
"capability.scheduler.activejob": "true",
|
||||
"capability.scheduler.clockwork": "true",
|
||||
"capability.scheduler.resque": "true",
|
||||
"capability.scheduler.sidekiq": "true",
|
||||
"capability.serialization": "true",
|
||||
"declaredOnly": "true",
|
||||
"lockfile": "Gemfile.lock",
|
||||
"runtime.entrypoints": "app/main.rb",
|
||||
"runtime.files": "app/main.rb",
|
||||
"runtime.reasons": "require-static",
|
||||
"runtime.used": "true",
|
||||
"source": "https://rubygems.org/"
|
||||
},
|
||||
"evidence": [
|
||||
@@ -62,4 +98,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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;
|
||||
@@ -11,10 +12,13 @@ public sealed class RubyLanguageAnalyzerTests
|
||||
{
|
||||
var fixture = TestPaths.ResolveFixture("lang", "ruby", "basic");
|
||||
var golden = Path.Combine(fixture, "expected.json");
|
||||
var usageHints = new LanguageUsageHints(new[] { Path.Combine(fixture, "app", "main.rb") });
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixture,
|
||||
golden,
|
||||
new ILanguageAnalyzer[] { new RubyLanguageAnalyzer() },
|
||||
cancellationToken: TestContext.Current.CancellationToken);
|
||||
cancellationToken: TestContext.Current.CancellationToken,
|
||||
usageHints: usageHints);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@ public static class JavaClassFileFactory
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("(Ljava/lang/String;)Ljava/net/URL;"); // #13
|
||||
writer.WriteByte((byte)ConstantTag.NameAndType); writer.WriteUInt16(12); writer.WriteUInt16(13); // #14
|
||||
writer.WriteByte((byte)ConstantTag.Methodref); writer.WriteUInt16(11); writer.WriteUInt16(14); // #15
|
||||
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("dummy"); // #16
|
||||
writer.WriteByte((byte)ConstantTag.String); writer.WriteUInt16(16); // #17
|
||||
|
||||
writer.WriteUInt16(0x0001); // public
|
||||
writer.WriteUInt16(2); // this class
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
@@ -155,6 +156,48 @@ public sealed class SurfaceManifestStageExecutorTests
|
||||
context.Analysis.Set(ScanAnalysisKeys.LayerComponentFragments, ImmutableArray.Create(fragment));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_IncludesDenoObservationPayloadWhenPresent()
|
||||
{
|
||||
var metrics = new ScannerWorkerMetrics();
|
||||
var publisher = new TestSurfaceManifestPublisher("tenant-a");
|
||||
var cache = new RecordingSurfaceCache();
|
||||
var environment = new TestSurfaceEnvironment("tenant-a");
|
||||
var hash = new DefaultCryptoHash();
|
||||
var executor = new SurfaceManifestStageExecutor(
|
||||
publisher,
|
||||
cache,
|
||||
environment,
|
||||
metrics,
|
||||
NullLogger<SurfaceManifestStageExecutor>.Instance,
|
||||
hash);
|
||||
|
||||
var context = CreateContext();
|
||||
var observationBytes = Encoding.UTF8.GetBytes("{\"entrypoints\":[\"mod.ts\"]}");
|
||||
var metadata = new Dictionary<string, string?>
|
||||
{
|
||||
["deno.observation.hash"] = "sha256:abc",
|
||||
["deno.observation.entrypoints"] = "1"
|
||||
};
|
||||
|
||||
var observation = new AnalyzerObservationPayload(
|
||||
analyzerId: "deno",
|
||||
kind: "deno.observation",
|
||||
mediaType: "application/json",
|
||||
content: observationBytes,
|
||||
metadata: metadata,
|
||||
view: "observations");
|
||||
|
||||
context.Analysis.Set(ScanAnalysisKeys.DenoObservationPayload, observation);
|
||||
|
||||
await executor.ExecuteAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(1, publisher.PublishCalls);
|
||||
var request = Assert.NotNull(publisher.LastRequest);
|
||||
Assert.Contains(request.Payloads, payload => payload.Kind == "deno.observation");
|
||||
Assert.Contains(cache.Entries.Keys, key => key.Namespace == "surface.artifacts.deno.observation");
|
||||
}
|
||||
|
||||
private sealed class RecordingSurfaceCache : ISurfaceCache
|
||||
{
|
||||
private readonly Dictionary<SurfaceCacheKey, byte[]> _entries = new();
|
||||
|
||||
Reference in New Issue
Block a user