up
This commit is contained in:
@@ -40,7 +40,8 @@
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "package.json"
|
||||
"locator": "package.json",
|
||||
"sha256": "65e86ba14f0beebc4573039ac34a58f6dfa0133aa4a9e7f2dcdbb36a4e5c2814"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -594,11 +594,7 @@ public sealed class NodePackageCollectorTests
|
||||
}
|
||||
|
||||
private static string InvokeBuildDeclarationKey(string name, string? version)
|
||||
{
|
||||
var method = typeof(NodePackageCollector).GetMethod("BuildDeclarationKey",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
|
||||
return (string?)method?.Invoke(null, [name, version]) ?? string.Empty;
|
||||
}
|
||||
=> NodeDeclarationKeyBuilder.Build(name, version);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
"projectUrl": "Documentation, https://example.com/layered/docs",
|
||||
"provenance": "dist-info",
|
||||
"record.hashMismatches": "0",
|
||||
"record.hashedEntries": "8",
|
||||
"record.hashedEntries": "7",
|
||||
"record.ioErrors": "0",
|
||||
"record.missingFiles": "0",
|
||||
"record.missingFiles": "1",
|
||||
"record.totalEntries": "9",
|
||||
"requiresDist": "requests",
|
||||
"requiresPython": "\u003E=3.9",
|
||||
@@ -44,6 +44,12 @@
|
||||
"wheel.version": "1.0"
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "derived",
|
||||
"source": "RECORD",
|
||||
"locator": "layer1/usr/bin/layered-cli",
|
||||
"value": "missing"
|
||||
},
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "INSTALLER",
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
"record.unsupportedAlgorithms": "md5",
|
||||
"requiresDist": "click",
|
||||
"requiresPython": "\u003E=3.8",
|
||||
"runtime.libPaths.count": "1",
|
||||
"runtime.versions": "3.11",
|
||||
"summary": "Cache test package for hashed RECORD coverage",
|
||||
"version": "1.2.3",
|
||||
"wheel.generator": "pip 24.0",
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
"record.totalEntries": "10",
|
||||
"requiresDist": "requests (\u003E=2.0)",
|
||||
"requiresPython": "\u003E=3.9",
|
||||
"runtime.libPaths.count": "1",
|
||||
"runtime.versions": "3.11",
|
||||
"sourceCommit": "abc123def",
|
||||
"sourceSubdirectory": "src/simple",
|
||||
"sourceUrl": "https://example.com/simple-1.0.0.tar.gz",
|
||||
|
||||
@@ -360,7 +360,8 @@ public sealed class PythonLanguageAnalyzerTests
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
|
||||
var expectedPath = Path.Combine("lib", "python3.11", "site-packages", "egg_info_pkg-1.2.3.egg-info");
|
||||
var expectedPath = Path.Combine("lib", "python3.11", "site-packages", "egg_info_pkg-1.2.3.egg-info")
|
||||
.Replace('\\', '/');
|
||||
|
||||
Assert.True(ComponentHasMetadata(root, "egg-info-pkg", "provenance", "egg-info"));
|
||||
Assert.True(ComponentHasMetadata(root, "egg-info-pkg", "record.totalEntries", "4"));
|
||||
|
||||
@@ -77,16 +77,22 @@ public sealed class PythonVirtualFileSystemTests
|
||||
using (var archive = ZipFile.Open(wheelPath, ZipArchiveMode.Create))
|
||||
{
|
||||
var entry = archive.CreateEntry("mypackage/__init__.py");
|
||||
using var writer = new StreamWriter(entry.Open());
|
||||
writer.Write("# Package init");
|
||||
using (var writer = new StreamWriter(entry.Open()))
|
||||
{
|
||||
writer.Write("# Package init");
|
||||
}
|
||||
|
||||
entry = archive.CreateEntry("mypackage/core.py");
|
||||
using var writer2 = new StreamWriter(entry.Open());
|
||||
writer2.Write("def main(): pass");
|
||||
using (var writer2 = new StreamWriter(entry.Open()))
|
||||
{
|
||||
writer2.Write("def main(): pass");
|
||||
}
|
||||
|
||||
entry = archive.CreateEntry("mypackage-1.0.0.dist-info/METADATA");
|
||||
using var writer3 = new StreamWriter(entry.Open());
|
||||
writer3.Write("Name: mypackage\nVersion: 1.0.0");
|
||||
using (var writer3 = new StreamWriter(entry.Open()))
|
||||
{
|
||||
writer3.Write("Name: mypackage\nVersion: 1.0.0");
|
||||
}
|
||||
}
|
||||
|
||||
var vfs = PythonVirtualFileSystem.CreateBuilder()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
@@ -7,72 +7,72 @@ using System.Threading.Tasks;
|
||||
using StellaOps.Scanner.Analyzers.Lang.DotNet;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Tests.DotNet;
|
||||
|
||||
public sealed class DotNetLanguageAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SimpleFixtureProducesDeterministicOutputAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "dotnet", "simple");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new DotNetLanguageAnalyzer()
|
||||
};
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SignedFixtureCapturesAssemblyMetadataAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "dotnet", "signed");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new DotNetLanguageAnalyzer()
|
||||
};
|
||||
|
||||
var inspector = new StubAuthenticodeInspector();
|
||||
var services = new SingleServiceProvider(inspector);
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken,
|
||||
usageHints: null,
|
||||
services: services);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SelfContainedFixtureHandlesNativeAssetsAndUsageAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "dotnet", "selfcontained");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var usageHints = new LanguageUsageHints(new[]
|
||||
{
|
||||
Path.Combine(fixturePath, "lib", "net10.0", "StellaOps.Toolkit.dll"),
|
||||
Path.Combine(fixturePath, "runtimes", "linux-x64", "native", "libstellaopsnative.so")
|
||||
});
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new DotNetLanguageAnalyzer()
|
||||
};
|
||||
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Tests.DotNet;
|
||||
|
||||
public sealed class DotNetLanguageAnalyzerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SimpleFixtureProducesDeterministicOutputAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "dotnet", "simple");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new DotNetLanguageAnalyzer()
|
||||
};
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SignedFixtureCapturesAssemblyMetadataAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "dotnet", "signed");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new DotNetLanguageAnalyzer()
|
||||
};
|
||||
|
||||
var inspector = new StubAuthenticodeInspector();
|
||||
var services = new SingleServiceProvider(inspector);
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken,
|
||||
usageHints: null,
|
||||
services: services);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SelfContainedFixtureHandlesNativeAssetsAndUsageAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "dotnet", "selfcontained");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var usageHints = new LanguageUsageHints(new[]
|
||||
{
|
||||
Path.Combine(fixturePath, "lib", "net10.0", "StellaOps.Toolkit.dll"),
|
||||
Path.Combine(fixturePath, "runtimes", "linux-x64", "native", "libstellaopsnative.so")
|
||||
});
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new DotNetLanguageAnalyzer()
|
||||
};
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
@@ -104,6 +104,68 @@ public sealed class DotNetLanguageAnalyzerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConfigEnablesIlMetadataEdgesAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var sourceFixture = TestPaths.ResolveFixture("lang", "dotnet", "simple");
|
||||
var tempRoot = TestPaths.CreateTemporaryDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
CopyDirectory(sourceFixture, tempRoot);
|
||||
|
||||
File.WriteAllText(
|
||||
Path.Combine(tempRoot, "dotnet-il.config.json"),
|
||||
"""
|
||||
{
|
||||
"emitDependencyEdges": true,
|
||||
"includeEntrypoints": true,
|
||||
"runtimeEvidencePath": "runtime-evidence.ndjson",
|
||||
"runtimeEvidenceConfidence": "medium"
|
||||
}
|
||||
""");
|
||||
|
||||
File.WriteAllLines(
|
||||
Path.Combine(tempRoot, "runtime-evidence.ndjson"),
|
||||
new[]
|
||||
{
|
||||
"{\"package\":\"stellaops.toolkit\",\"target\":\"native-lib\",\"reason\":\"runtime-load\",\"confidence\":\"medium\",\"source\":\"trace\"}"
|
||||
});
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new DotNetLanguageAnalyzer()
|
||||
};
|
||||
|
||||
var json = await LanguageAnalyzerTestHarness.RunToJsonAsync(
|
||||
tempRoot,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var packages = document.RootElement.EnumerateArray().ToArray();
|
||||
var toolkit = packages.First(element => element.GetProperty("name").GetString() == "StellaOps.Toolkit");
|
||||
var logging = packages.First(element => element.GetProperty("name").GetString() == "Microsoft.Extensions.Logging");
|
||||
|
||||
var toolkitMetadata = toolkit.GetProperty("metadata");
|
||||
Assert.Equal("microsoft.extensions.logging", toolkitMetadata.GetProperty("edge[0].target").GetString());
|
||||
Assert.Equal("declared-dependency", toolkitMetadata.GetProperty("edge[0].reason").GetString());
|
||||
Assert.Equal("native-lib", toolkitMetadata.GetProperty("edge.runtime[0].target").GetString());
|
||||
Assert.Equal("runtime-load", toolkitMetadata.GetProperty("edge.runtime[0].reason").GetString());
|
||||
|
||||
var entrypointId = toolkitMetadata.GetProperty("entrypoint[0].id").GetString();
|
||||
Assert.NotNull(entrypointId);
|
||||
Assert.StartsWith("Sample.App:", entrypointId, StringComparison.Ordinal);
|
||||
Assert.True(toolkitMetadata.TryGetProperty("entrypoint[0].tfm[0]", out _));
|
||||
Assert.True(logging.GetProperty("metadata").TryGetProperty("entrypoint[0].rid[0]", out _));
|
||||
}
|
||||
finally
|
||||
{
|
||||
TestPaths.SafeDelete(tempRoot);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MultiFixtureMergesRuntimeMetadataAsync()
|
||||
{
|
||||
@@ -151,29 +213,49 @@ public sealed class DotNetLanguageAnalyzerTests
|
||||
Assert.Contains("osx-arm64", ridValues);
|
||||
Assert.Contains("win-arm64", ridValues);
|
||||
}
|
||||
|
||||
private sealed class StubAuthenticodeInspector : IDotNetAuthenticodeInspector
|
||||
{
|
||||
public DotNetAuthenticodeMetadata? TryInspect(string assemblyPath, CancellationToken cancellationToken)
|
||||
=> new DotNetAuthenticodeMetadata(
|
||||
Subject: "CN=StellaOps Test Signing",
|
||||
Issuer: "CN=StellaOps Root",
|
||||
NotBefore: new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
|
||||
NotAfter: new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero),
|
||||
Thumbprint: "AA11BB22CC33DD44EE55FF66GG77HH88II99JJ00",
|
||||
SerialNumber: "0123456789ABCDEF");
|
||||
}
|
||||
|
||||
private sealed class SingleServiceProvider : IServiceProvider
|
||||
{
|
||||
private readonly object _service;
|
||||
|
||||
public SingleServiceProvider(object service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
public object? GetService(Type serviceType)
|
||||
=> serviceType == typeof(IDotNetAuthenticodeInspector) ? _service : null;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubAuthenticodeInspector : IDotNetAuthenticodeInspector
|
||||
{
|
||||
public DotNetAuthenticodeMetadata? TryInspect(string assemblyPath, CancellationToken cancellationToken)
|
||||
=> new DotNetAuthenticodeMetadata(
|
||||
Subject: "CN=StellaOps Test Signing",
|
||||
Issuer: "CN=StellaOps Root",
|
||||
NotBefore: new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
|
||||
NotAfter: new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero),
|
||||
Thumbprint: "AA11BB22CC33DD44EE55FF66GG77HH88II99JJ00",
|
||||
SerialNumber: "0123456789ABCDEF");
|
||||
}
|
||||
|
||||
private sealed class SingleServiceProvider : IServiceProvider
|
||||
{
|
||||
private readonly object _service;
|
||||
|
||||
public SingleServiceProvider(object service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
public object? GetService(Type serviceType)
|
||||
=> serviceType == typeof(IDotNetAuthenticodeInspector) ? _service : null;
|
||||
}
|
||||
|
||||
private static void CopyDirectory(string sourceDir, string destinationDir)
|
||||
{
|
||||
if (!Directory.Exists(destinationDir))
|
||||
{
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
}
|
||||
|
||||
foreach (var file in Directory.EnumerateFiles(sourceDir, "*", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
var targetPath = Path.Combine(destinationDir, Path.GetFileName(file));
|
||||
File.Copy(file, targetPath, overwrite: true);
|
||||
}
|
||||
|
||||
foreach (var directory in Directory.EnumerateDirectories(sourceDir, "*", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
var targetDirectory = Path.Combine(destinationDir, Path.GetFileName(directory));
|
||||
CopyDirectory(directory, targetDirectory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Scanner.Analyzers.Native.Observations;
|
||||
using StellaOps.Scanner.Analyzers.Native.Reachability;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Tests.Reachability;
|
||||
|
||||
public class NativeReachabilityGraphBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public void Build_ConstructsDeterministicGraphWithSyntheticRoots()
|
||||
{
|
||||
// Arrange
|
||||
var document = new NativeObservationDocument
|
||||
{
|
||||
Binary = new NativeObservationBinary
|
||||
{
|
||||
Path = "/usr/bin/test",
|
||||
Format = "elf",
|
||||
BuildId = "abcd1234",
|
||||
Architecture = "x86_64",
|
||||
Is64Bit = true,
|
||||
},
|
||||
Entrypoints =
|
||||
[
|
||||
new NativeObservationEntrypoint
|
||||
{
|
||||
Type = "main",
|
||||
Symbol = "_start",
|
||||
}
|
||||
],
|
||||
DeclaredEdges =
|
||||
[
|
||||
new NativeObservationDeclaredEdge
|
||||
{
|
||||
Target = "libc.so.6",
|
||||
Reason = "elf-dtneeded",
|
||||
}
|
||||
],
|
||||
HeuristicEdges =
|
||||
[
|
||||
new NativeObservationHeuristicEdge
|
||||
{
|
||||
Target = "libplugin.so",
|
||||
Reason = "string-dlopen",
|
||||
Confidence = "medium",
|
||||
}
|
||||
],
|
||||
Environment = new NativeObservationEnvironment()
|
||||
};
|
||||
|
||||
// Act
|
||||
var graph = NativeReachabilityGraphBuilder.Build(document, layerDigest: "sha256:layer1");
|
||||
|
||||
// Assert
|
||||
graph.LayerDigest.Should().Be("sha256:layer1");
|
||||
graph.BuildId.Should().Be("abcd1234");
|
||||
graph.Nodes.Should().ContainSingle(n => n.Kind == "root");
|
||||
graph.Nodes.Should().ContainSingle(n => n.Kind == "binary");
|
||||
graph.Nodes.Should().Contain(n => n.Name == "libc.so.6");
|
||||
graph.Edges.Should().Contain(e => e.Reason == "entrypoint");
|
||||
graph.Edges.Should().Contain(e => e.Reason == "elf-dtneeded");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToBundle_WrapsGraphWithPayloadType()
|
||||
{
|
||||
// Arrange
|
||||
var document = new NativeObservationDocument
|
||||
{
|
||||
Binary = new NativeObservationBinary
|
||||
{
|
||||
Path = "/usr/bin/test",
|
||||
Format = "elf",
|
||||
Sha256 = "aa",
|
||||
Is64Bit = true,
|
||||
},
|
||||
Environment = new NativeObservationEnvironment()
|
||||
};
|
||||
|
||||
// Act
|
||||
var bundle = NativeReachabilityGraphBuilder.ToBundle(document);
|
||||
|
||||
// Assert
|
||||
bundle.PayloadType.Should().Be("stellaops.native.graph@1");
|
||||
bundle.Graph.Nodes.Should().NotBeEmpty();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user