up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
This commit is contained in:
@@ -1,86 +1,86 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Tests;
|
||||
|
||||
public sealed class EntryTraceImageContextFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_UsesEnvironmentAndEntrypointFromConfig()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"config": {
|
||||
"Env": ["PATH=/custom/bin:/usr/bin", "FOO=bar"],
|
||||
"Entrypoint": ["/bin/sh", "-c"],
|
||||
"Cmd": ["./start.sh"],
|
||||
"WorkingDir": "/srv/app",
|
||||
"User": "1000:1000"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var config = OciImageConfigLoader.Load(new MemoryStream(Encoding.UTF8.GetBytes(json)));
|
||||
var options = new EntryTraceAnalyzerOptions
|
||||
{
|
||||
DefaultPath = "/default/bin"
|
||||
};
|
||||
|
||||
var fs = new TestRootFileSystem();
|
||||
var imageContext = EntryTraceImageContextFactory.Create(
|
||||
config,
|
||||
fs,
|
||||
options,
|
||||
"sha256:testimage",
|
||||
"scan-001",
|
||||
NullLogger.Instance);
|
||||
|
||||
Assert.Equal("/bin/sh", imageContext.Entrypoint.Entrypoint[0]);
|
||||
Assert.Equal("./start.sh", imageContext.Entrypoint.Command[0]);
|
||||
|
||||
Assert.Equal("/srv/app", imageContext.Context.WorkingDirectory);
|
||||
Assert.Equal("1000:1000", imageContext.Context.User);
|
||||
Assert.Equal("sha256:testimage", imageContext.Context.ImageDigest);
|
||||
Assert.Equal("scan-001", imageContext.Context.ScanId);
|
||||
|
||||
Assert.True(imageContext.Context.Environment.ContainsKey("FOO"));
|
||||
Assert.Equal("bar", imageContext.Context.Environment["FOO"]);
|
||||
|
||||
Assert.Equal("/custom/bin:/usr/bin", string.Join(":", imageContext.Context.Path));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_FallsBackToDefaultPathWhenMissing()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"config": {
|
||||
"Env": ["FOO=bar"],
|
||||
"Cmd": ["node", "server.js"]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var config = OciImageConfigLoader.Load(new MemoryStream(Encoding.UTF8.GetBytes(json)));
|
||||
var options = new EntryTraceAnalyzerOptions
|
||||
{
|
||||
DefaultPath = "/usr/local/sbin:/usr/local/bin"
|
||||
};
|
||||
|
||||
var fs = new TestRootFileSystem();
|
||||
var imageContext = EntryTraceImageContextFactory.Create(
|
||||
config,
|
||||
fs,
|
||||
options,
|
||||
"sha256:abc",
|
||||
"scan-xyz",
|
||||
NullLogger.Instance);
|
||||
|
||||
Assert.Equal("/usr/local/sbin:/usr/local/bin", string.Join(":", imageContext.Context.Path));
|
||||
Assert.Equal("root", imageContext.Context.User);
|
||||
Assert.Equal("/", imageContext.Context.WorkingDirectory);
|
||||
}
|
||||
}
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Tests;
|
||||
|
||||
public sealed class EntryTraceImageContextFactoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_UsesEnvironmentAndEntrypointFromConfig()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"config": {
|
||||
"Env": ["PATH=/custom/bin:/usr/bin", "FOO=bar"],
|
||||
"Entrypoint": ["/bin/sh", "-c"],
|
||||
"Cmd": ["./start.sh"],
|
||||
"WorkingDir": "/srv/app",
|
||||
"User": "1000:1000"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var config = OciImageConfigLoader.Load(new MemoryStream(Encoding.UTF8.GetBytes(json)));
|
||||
var options = new EntryTraceAnalyzerOptions
|
||||
{
|
||||
DefaultPath = "/default/bin"
|
||||
};
|
||||
|
||||
var fs = new TestRootFileSystem();
|
||||
var imageContext = EntryTraceImageContextFactory.Create(
|
||||
config,
|
||||
fs,
|
||||
options,
|
||||
"sha256:testimage",
|
||||
"scan-001",
|
||||
NullLogger.Instance);
|
||||
|
||||
Assert.Equal("/bin/sh", imageContext.Entrypoint.Entrypoint[0]);
|
||||
Assert.Equal("./start.sh", imageContext.Entrypoint.Command[0]);
|
||||
|
||||
Assert.Equal("/srv/app", imageContext.Context.WorkingDirectory);
|
||||
Assert.Equal("1000:1000", imageContext.Context.User);
|
||||
Assert.Equal("sha256:testimage", imageContext.Context.ImageDigest);
|
||||
Assert.Equal("scan-001", imageContext.Context.ScanId);
|
||||
|
||||
Assert.True(imageContext.Context.Environment.ContainsKey("FOO"));
|
||||
Assert.Equal("bar", imageContext.Context.Environment["FOO"]);
|
||||
|
||||
Assert.Equal("/custom/bin:/usr/bin", string.Join(":", imageContext.Context.Path));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_FallsBackToDefaultPathWhenMissing()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"config": {
|
||||
"Env": ["FOO=bar"],
|
||||
"Cmd": ["node", "server.js"]
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var config = OciImageConfigLoader.Load(new MemoryStream(Encoding.UTF8.GetBytes(json)));
|
||||
var options = new EntryTraceAnalyzerOptions
|
||||
{
|
||||
DefaultPath = "/usr/local/sbin:/usr/local/bin"
|
||||
};
|
||||
|
||||
var fs = new TestRootFileSystem();
|
||||
var imageContext = EntryTraceImageContextFactory.Create(
|
||||
config,
|
||||
fs,
|
||||
options,
|
||||
"sha256:abc",
|
||||
"scan-xyz",
|
||||
NullLogger.Instance);
|
||||
|
||||
Assert.Equal("/usr/local/sbin:/usr/local/bin", string.Join(":", imageContext.Context.Path));
|
||||
Assert.Equal("root", imageContext.Context.User);
|
||||
Assert.Equal("/", imageContext.Context.WorkingDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,20 @@ using System.IO;
|
||||
using System.Text;
|
||||
using StellaOps.Scanner.EntryTrace.FileSystem;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Tests;
|
||||
|
||||
public sealed class LayeredRootFileSystemTests : IDisposable
|
||||
{
|
||||
private readonly string _tempRoot;
|
||||
|
||||
public LayeredRootFileSystemTests()
|
||||
{
|
||||
_tempRoot = Path.Combine(Path.GetTempPath(), $"entrytrace-layerfs-{Guid.NewGuid():n}");
|
||||
Directory.CreateDirectory(_tempRoot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Tests;
|
||||
|
||||
public sealed class LayeredRootFileSystemTests : IDisposable
|
||||
{
|
||||
private readonly string _tempRoot;
|
||||
|
||||
public LayeredRootFileSystemTests()
|
||||
{
|
||||
_tempRoot = Path.Combine(Path.GetTempPath(), $"entrytrace-layerfs-{Guid.NewGuid():n}");
|
||||
Directory.CreateDirectory(_tempRoot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromDirectories_HandlesWhiteoutsAndResolution()
|
||||
{
|
||||
var layer1 = CreateLayerDirectory("layer1");
|
||||
@@ -33,32 +33,32 @@ public sealed class LayeredRootFileSystemTests : IDisposable
|
||||
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
|
||||
UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
|
||||
#endif
|
||||
|
||||
var optDirectory1 = Path.Combine(layer1, "opt");
|
||||
Directory.CreateDirectory(optDirectory1);
|
||||
File.WriteAllText(Path.Combine(optDirectory1, "setup.sh"), "echo setup\n");
|
||||
|
||||
var optDirectory2 = Path.Combine(layer2, "opt");
|
||||
Directory.CreateDirectory(optDirectory2);
|
||||
File.WriteAllText(Path.Combine(optDirectory2, ".wh.setup.sh"), string.Empty);
|
||||
|
||||
var fs = LayeredRootFileSystem.FromDirectories(new[]
|
||||
{
|
||||
new LayeredRootFileSystem.LayerDirectory("sha256:layer1", layer1),
|
||||
new LayeredRootFileSystem.LayerDirectory("sha256:layer2", layer2)
|
||||
});
|
||||
|
||||
Assert.True(fs.TryResolveExecutable("entrypoint.sh", new[] { "/usr/bin" }, out var descriptor));
|
||||
Assert.Equal("/usr/bin/entrypoint.sh", descriptor.Path);
|
||||
Assert.Equal("sha256:layer1", descriptor.LayerDigest);
|
||||
|
||||
Assert.True(fs.TryReadAllText("/usr/bin/entrypoint.sh", out var textDescriptor, out var content));
|
||||
Assert.Equal(descriptor.Path, textDescriptor.Path);
|
||||
Assert.Contains("echo layer1", content);
|
||||
|
||||
Assert.False(fs.TryReadAllText("/opt/setup.sh", out _, out _));
|
||||
|
||||
var optEntries = fs.EnumerateDirectory("/opt");
|
||||
|
||||
var optDirectory1 = Path.Combine(layer1, "opt");
|
||||
Directory.CreateDirectory(optDirectory1);
|
||||
File.WriteAllText(Path.Combine(optDirectory1, "setup.sh"), "echo setup\n");
|
||||
|
||||
var optDirectory2 = Path.Combine(layer2, "opt");
|
||||
Directory.CreateDirectory(optDirectory2);
|
||||
File.WriteAllText(Path.Combine(optDirectory2, ".wh.setup.sh"), string.Empty);
|
||||
|
||||
var fs = LayeredRootFileSystem.FromDirectories(new[]
|
||||
{
|
||||
new LayeredRootFileSystem.LayerDirectory("sha256:layer1", layer1),
|
||||
new LayeredRootFileSystem.LayerDirectory("sha256:layer2", layer2)
|
||||
});
|
||||
|
||||
Assert.True(fs.TryResolveExecutable("entrypoint.sh", new[] { "/usr/bin" }, out var descriptor));
|
||||
Assert.Equal("/usr/bin/entrypoint.sh", descriptor.Path);
|
||||
Assert.Equal("sha256:layer1", descriptor.LayerDigest);
|
||||
|
||||
Assert.True(fs.TryReadAllText("/usr/bin/entrypoint.sh", out var textDescriptor, out var content));
|
||||
Assert.Equal(descriptor.Path, textDescriptor.Path);
|
||||
Assert.Contains("echo layer1", content);
|
||||
|
||||
Assert.False(fs.TryReadAllText("/opt/setup.sh", out _, out _));
|
||||
|
||||
var optEntries = fs.EnumerateDirectory("/opt");
|
||||
Assert.DoesNotContain(optEntries, entry => entry.Path.EndsWith("setup.sh", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
@@ -81,122 +81,122 @@ public sealed class LayeredRootFileSystemTests : IDisposable
|
||||
Assert.Equal("abcd", Encoding.UTF8.GetString(preview.Span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromArchives_ResolvesSymlinkAndWhiteout()
|
||||
{
|
||||
var layer1Path = Path.Combine(_tempRoot, "layer1.tar");
|
||||
var layer2Path = Path.Combine(_tempRoot, "layer2.tar");
|
||||
|
||||
CreateArchive(layer1Path, writer =>
|
||||
{
|
||||
var scriptEntry = new PaxTarEntry(TarEntryType.RegularFile, "usr/local/bin/start.sh");
|
||||
scriptEntry.Mode = UnixFileMode.UserRead | UnixFileMode.UserExecute |
|
||||
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
|
||||
UnixFileMode.OtherRead | UnixFileMode.OtherExecute;
|
||||
scriptEntry.DataStream = new MemoryStream(Encoding.UTF8.GetBytes("#!/bin/sh\necho start\n"));
|
||||
writer.WriteEntry(scriptEntry);
|
||||
|
||||
var oldScript = new PaxTarEntry(TarEntryType.RegularFile, "opt/old.sh");
|
||||
oldScript.Mode = UnixFileMode.UserRead | UnixFileMode.UserExecute |
|
||||
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
|
||||
UnixFileMode.OtherRead | UnixFileMode.OtherExecute;
|
||||
oldScript.DataStream = new MemoryStream(Encoding.UTF8.GetBytes("echo old\n"));
|
||||
writer.WriteEntry(oldScript);
|
||||
});
|
||||
|
||||
CreateArchive(layer2Path, writer =>
|
||||
{
|
||||
var symlinkEntry = new PaxTarEntry(TarEntryType.SymbolicLink, "usr/bin/start.sh");
|
||||
symlinkEntry.LinkName = "/usr/local/bin/start.sh";
|
||||
writer.WriteEntry(symlinkEntry);
|
||||
|
||||
var whiteout = new PaxTarEntry(TarEntryType.RegularFile, "opt/.wh.old.sh");
|
||||
whiteout.DataStream = new MemoryStream(Array.Empty<byte>());
|
||||
writer.WriteEntry(whiteout);
|
||||
});
|
||||
|
||||
var fs = LayeredRootFileSystem.FromArchives(new[]
|
||||
{
|
||||
new LayeredRootFileSystem.LayerArchive("sha256:base", layer1Path),
|
||||
new LayeredRootFileSystem.LayerArchive("sha256:update", layer2Path)
|
||||
});
|
||||
|
||||
Assert.True(fs.TryResolveExecutable("start.sh", new[] { "/usr/bin" }, out var descriptor));
|
||||
Assert.Equal("/usr/local/bin/start.sh", descriptor.Path);
|
||||
Assert.Equal("sha256:base", descriptor.LayerDigest);
|
||||
|
||||
Assert.True(fs.TryReadAllText("/usr/bin/start.sh", out var resolvedDescriptor, out var content));
|
||||
Assert.Equal(descriptor.Path, resolvedDescriptor.Path);
|
||||
Assert.Contains("echo start", content);
|
||||
|
||||
Assert.False(fs.TryReadAllText("/opt/old.sh", out _, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromArchives_ResolvesHardLinkContent()
|
||||
{
|
||||
var baseLayer = Path.Combine(_tempRoot, "base.tar");
|
||||
var hardLinkLayer = Path.Combine(_tempRoot, "hardlink.tar");
|
||||
|
||||
CreateArchive(baseLayer, writer =>
|
||||
{
|
||||
var baseEntry = new PaxTarEntry(TarEntryType.RegularFile, "usr/bin/tool.sh");
|
||||
baseEntry.Mode = UnixFileMode.UserRead | UnixFileMode.UserExecute |
|
||||
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
|
||||
UnixFileMode.OtherRead | UnixFileMode.OtherExecute;
|
||||
baseEntry.DataStream = new MemoryStream(Encoding.UTF8.GetBytes("#!/bin/sh\necho tool\n"));
|
||||
writer.WriteEntry(baseEntry);
|
||||
});
|
||||
|
||||
CreateArchive(hardLinkLayer, writer =>
|
||||
{
|
||||
var hardLink = new PaxTarEntry(TarEntryType.HardLink, "bin/tool.sh")
|
||||
{
|
||||
LinkName = "/usr/bin/tool.sh",
|
||||
Mode = UnixFileMode.UserRead | UnixFileMode.UserExecute |
|
||||
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
|
||||
UnixFileMode.OtherRead | UnixFileMode.OtherExecute
|
||||
};
|
||||
writer.WriteEntry(hardLink);
|
||||
});
|
||||
|
||||
var fs = LayeredRootFileSystem.FromArchives(new[]
|
||||
{
|
||||
new LayeredRootFileSystem.LayerArchive("sha256:base", baseLayer),
|
||||
new LayeredRootFileSystem.LayerArchive("sha256:hardlink", hardLinkLayer)
|
||||
});
|
||||
|
||||
Assert.True(fs.TryReadAllText("/bin/tool.sh", out var descriptor, out var content));
|
||||
Assert.Equal("/usr/bin/tool.sh", descriptor.Path);
|
||||
Assert.Contains("echo tool", content);
|
||||
}
|
||||
|
||||
private string CreateLayerDirectory(string name)
|
||||
{
|
||||
var path = Path.Combine(_tempRoot, name);
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void CreateArchive(string path, Action<TarWriter> writerAction)
|
||||
{
|
||||
using var stream = File.Create(path);
|
||||
using var writer = new TarWriter(stream, leaveOpen: false);
|
||||
writerAction(writer);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_tempRoot))
|
||||
{
|
||||
Directory.Delete(_tempRoot, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore cleanup failures
|
||||
}
|
||||
}
|
||||
}
|
||||
[Fact]
|
||||
public void FromArchives_ResolvesSymlinkAndWhiteout()
|
||||
{
|
||||
var layer1Path = Path.Combine(_tempRoot, "layer1.tar");
|
||||
var layer2Path = Path.Combine(_tempRoot, "layer2.tar");
|
||||
|
||||
CreateArchive(layer1Path, writer =>
|
||||
{
|
||||
var scriptEntry = new PaxTarEntry(TarEntryType.RegularFile, "usr/local/bin/start.sh");
|
||||
scriptEntry.Mode = UnixFileMode.UserRead | UnixFileMode.UserExecute |
|
||||
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
|
||||
UnixFileMode.OtherRead | UnixFileMode.OtherExecute;
|
||||
scriptEntry.DataStream = new MemoryStream(Encoding.UTF8.GetBytes("#!/bin/sh\necho start\n"));
|
||||
writer.WriteEntry(scriptEntry);
|
||||
|
||||
var oldScript = new PaxTarEntry(TarEntryType.RegularFile, "opt/old.sh");
|
||||
oldScript.Mode = UnixFileMode.UserRead | UnixFileMode.UserExecute |
|
||||
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
|
||||
UnixFileMode.OtherRead | UnixFileMode.OtherExecute;
|
||||
oldScript.DataStream = new MemoryStream(Encoding.UTF8.GetBytes("echo old\n"));
|
||||
writer.WriteEntry(oldScript);
|
||||
});
|
||||
|
||||
CreateArchive(layer2Path, writer =>
|
||||
{
|
||||
var symlinkEntry = new PaxTarEntry(TarEntryType.SymbolicLink, "usr/bin/start.sh");
|
||||
symlinkEntry.LinkName = "/usr/local/bin/start.sh";
|
||||
writer.WriteEntry(symlinkEntry);
|
||||
|
||||
var whiteout = new PaxTarEntry(TarEntryType.RegularFile, "opt/.wh.old.sh");
|
||||
whiteout.DataStream = new MemoryStream(Array.Empty<byte>());
|
||||
writer.WriteEntry(whiteout);
|
||||
});
|
||||
|
||||
var fs = LayeredRootFileSystem.FromArchives(new[]
|
||||
{
|
||||
new LayeredRootFileSystem.LayerArchive("sha256:base", layer1Path),
|
||||
new LayeredRootFileSystem.LayerArchive("sha256:update", layer2Path)
|
||||
});
|
||||
|
||||
Assert.True(fs.TryResolveExecutable("start.sh", new[] { "/usr/bin" }, out var descriptor));
|
||||
Assert.Equal("/usr/local/bin/start.sh", descriptor.Path);
|
||||
Assert.Equal("sha256:base", descriptor.LayerDigest);
|
||||
|
||||
Assert.True(fs.TryReadAllText("/usr/bin/start.sh", out var resolvedDescriptor, out var content));
|
||||
Assert.Equal(descriptor.Path, resolvedDescriptor.Path);
|
||||
Assert.Contains("echo start", content);
|
||||
|
||||
Assert.False(fs.TryReadAllText("/opt/old.sh", out _, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromArchives_ResolvesHardLinkContent()
|
||||
{
|
||||
var baseLayer = Path.Combine(_tempRoot, "base.tar");
|
||||
var hardLinkLayer = Path.Combine(_tempRoot, "hardlink.tar");
|
||||
|
||||
CreateArchive(baseLayer, writer =>
|
||||
{
|
||||
var baseEntry = new PaxTarEntry(TarEntryType.RegularFile, "usr/bin/tool.sh");
|
||||
baseEntry.Mode = UnixFileMode.UserRead | UnixFileMode.UserExecute |
|
||||
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
|
||||
UnixFileMode.OtherRead | UnixFileMode.OtherExecute;
|
||||
baseEntry.DataStream = new MemoryStream(Encoding.UTF8.GetBytes("#!/bin/sh\necho tool\n"));
|
||||
writer.WriteEntry(baseEntry);
|
||||
});
|
||||
|
||||
CreateArchive(hardLinkLayer, writer =>
|
||||
{
|
||||
var hardLink = new PaxTarEntry(TarEntryType.HardLink, "bin/tool.sh")
|
||||
{
|
||||
LinkName = "/usr/bin/tool.sh",
|
||||
Mode = UnixFileMode.UserRead | UnixFileMode.UserExecute |
|
||||
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
|
||||
UnixFileMode.OtherRead | UnixFileMode.OtherExecute
|
||||
};
|
||||
writer.WriteEntry(hardLink);
|
||||
});
|
||||
|
||||
var fs = LayeredRootFileSystem.FromArchives(new[]
|
||||
{
|
||||
new LayeredRootFileSystem.LayerArchive("sha256:base", baseLayer),
|
||||
new LayeredRootFileSystem.LayerArchive("sha256:hardlink", hardLinkLayer)
|
||||
});
|
||||
|
||||
Assert.True(fs.TryReadAllText("/bin/tool.sh", out var descriptor, out var content));
|
||||
Assert.Equal("/usr/bin/tool.sh", descriptor.Path);
|
||||
Assert.Contains("echo tool", content);
|
||||
}
|
||||
|
||||
private string CreateLayerDirectory(string name)
|
||||
{
|
||||
var path = Path.Combine(_tempRoot, name);
|
||||
Directory.CreateDirectory(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void CreateArchive(string path, Action<TarWriter> writerAction)
|
||||
{
|
||||
using var stream = File.Create(path);
|
||||
using var writer = new TarWriter(stream, leaveOpen: false);
|
||||
writerAction(writer);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(_tempRoot))
|
||||
{
|
||||
Directory.Delete(_tempRoot, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore cleanup failures
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
using StellaOps.Scanner.EntryTrace.Parsing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Tests;
|
||||
|
||||
public sealed class ShellParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_ProducesDeterministicNodes()
|
||||
{
|
||||
const string script = """
|
||||
#!/bin/sh
|
||||
source /opt/init.sh
|
||||
if [ -f /etc/profile ]; then
|
||||
. /etc/profile
|
||||
fi
|
||||
|
||||
run-parts /etc/entry.d
|
||||
exec python -m app.main --flag
|
||||
""";
|
||||
|
||||
var first = ShellParser.Parse(script);
|
||||
var second = ShellParser.Parse(script);
|
||||
|
||||
Assert.Equal(first.Nodes.Length, second.Nodes.Length);
|
||||
var actual = first.Nodes.Select(n => n.GetType().Name).ToArray();
|
||||
var expected = new[] { nameof(ShellIncludeNode), nameof(ShellIfNode), nameof(ShellRunPartsNode), nameof(ShellExecNode) };
|
||||
Assert.Equal(expected, actual);
|
||||
|
||||
var actualSecond = second.Nodes.Select(n => n.GetType().Name).ToArray();
|
||||
Assert.Equal(expected, actualSecond);
|
||||
}
|
||||
}
|
||||
using StellaOps.Scanner.EntryTrace.Parsing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Tests;
|
||||
|
||||
public sealed class ShellParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_ProducesDeterministicNodes()
|
||||
{
|
||||
const string script = """
|
||||
#!/bin/sh
|
||||
source /opt/init.sh
|
||||
if [ -f /etc/profile ]; then
|
||||
. /etc/profile
|
||||
fi
|
||||
|
||||
run-parts /etc/entry.d
|
||||
exec python -m app.main --flag
|
||||
""";
|
||||
|
||||
var first = ShellParser.Parse(script);
|
||||
var second = ShellParser.Parse(script);
|
||||
|
||||
Assert.Equal(first.Nodes.Length, second.Nodes.Length);
|
||||
var actual = first.Nodes.Select(n => n.GetType().Name).ToArray();
|
||||
var expected = new[] { nameof(ShellIncludeNode), nameof(ShellIfNode), nameof(ShellRunPartsNode), nameof(ShellExecNode) };
|
||||
Assert.Equal(expected, actual);
|
||||
|
||||
var actualSecond = second.Nodes.Select(n => n.GetType().Name).ToArray();
|
||||
Assert.Equal(expected, actualSecond);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using StellaOps.Scanner.EntryTrace.FileSystem;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Tests;
|
||||
|
||||
internal sealed class TestRootFileSystem : IRootFileSystem
|
||||
{
|
||||
private readonly Dictionary<string, FileEntry> _entries = new(StringComparer.Ordinal);
|
||||
private readonly HashSet<string> _directories = new(StringComparer.Ordinal);
|
||||
|
||||
public TestRootFileSystem()
|
||||
{
|
||||
_directories.Add("/");
|
||||
}
|
||||
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Tests;
|
||||
|
||||
internal sealed class TestRootFileSystem : IRootFileSystem
|
||||
{
|
||||
private readonly Dictionary<string, FileEntry> _entries = new(StringComparer.Ordinal);
|
||||
private readonly HashSet<string> _directories = new(StringComparer.Ordinal);
|
||||
|
||||
public TestRootFileSystem()
|
||||
{
|
||||
_directories.Add("/");
|
||||
}
|
||||
|
||||
public void AddFile(string path, string content, bool executable = true, string? layer = "sha256:layer-a")
|
||||
{
|
||||
var normalized = Normalize(path);
|
||||
@@ -40,42 +40,42 @@ internal sealed class TestRootFileSystem : IRootFileSystem
|
||||
|
||||
_entries[normalized] = FileEntry.Create(normalized, content, text: null, executable, layer, isDirectory: false);
|
||||
}
|
||||
|
||||
public void AddDirectory(string path)
|
||||
{
|
||||
|
||||
public void AddDirectory(string path)
|
||||
{
|
||||
var normalized = Normalize(path);
|
||||
EnsureDirectoryChain(normalized);
|
||||
}
|
||||
|
||||
public bool TryResolveExecutable(string name, IReadOnlyList<string> searchPaths, out RootFileDescriptor descriptor)
|
||||
{
|
||||
if (name.Contains('/', StringComparison.Ordinal))
|
||||
{
|
||||
var normalized = Normalize(name);
|
||||
if (_entries.TryGetValue(normalized, out var file) && file.IsExecutable)
|
||||
{
|
||||
descriptor = file.ToDescriptor();
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var prefix in searchPaths)
|
||||
{
|
||||
var candidate = Combine(prefix, name);
|
||||
if (_entries.TryGetValue(candidate, out var file) && file.IsExecutable)
|
||||
{
|
||||
descriptor = file.ToDescriptor();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
descriptor = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public bool TryResolveExecutable(string name, IReadOnlyList<string> searchPaths, out RootFileDescriptor descriptor)
|
||||
{
|
||||
if (name.Contains('/', StringComparison.Ordinal))
|
||||
{
|
||||
var normalized = Normalize(name);
|
||||
if (_entries.TryGetValue(normalized, out var file) && file.IsExecutable)
|
||||
{
|
||||
descriptor = file.ToDescriptor();
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var prefix in searchPaths)
|
||||
{
|
||||
var candidate = Combine(prefix, name);
|
||||
if (_entries.TryGetValue(candidate, out var file) && file.IsExecutable)
|
||||
{
|
||||
descriptor = file.ToDescriptor();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
descriptor = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryReadAllText(string path, out RootFileDescriptor descriptor, out string content)
|
||||
{
|
||||
var normalized = Normalize(path);
|
||||
@@ -103,7 +103,7 @@ internal sealed class TestRootFileSystem : IRootFileSystem
|
||||
content = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public ImmutableArray<RootFileDescriptor> EnumerateDirectory(string path)
|
||||
{
|
||||
var normalized = Normalize(path);
|
||||
@@ -136,57 +136,57 @@ internal sealed class TestRootFileSystem : IRootFileSystem
|
||||
entries.Sort(static (left, right) => string.CompareOrdinal(left.Path, right.Path));
|
||||
return entries.ToImmutableArray();
|
||||
}
|
||||
|
||||
public bool DirectoryExists(string path)
|
||||
{
|
||||
var normalized = Normalize(path);
|
||||
return _directories.Contains(normalized);
|
||||
}
|
||||
|
||||
|
||||
public bool DirectoryExists(string path)
|
||||
{
|
||||
var normalized = Normalize(path);
|
||||
return _directories.Contains(normalized);
|
||||
}
|
||||
|
||||
private static string Combine(string prefix, string name)
|
||||
{
|
||||
var normalizedPrefix = Normalize(prefix);
|
||||
if (normalizedPrefix == "/")
|
||||
{
|
||||
return Normalize("/" + name);
|
||||
}
|
||||
|
||||
return Normalize($"{normalizedPrefix}/{name}");
|
||||
}
|
||||
|
||||
{
|
||||
var normalizedPrefix = Normalize(prefix);
|
||||
if (normalizedPrefix == "/")
|
||||
{
|
||||
return Normalize("/" + name);
|
||||
}
|
||||
|
||||
return Normalize($"{normalizedPrefix}/{name}");
|
||||
}
|
||||
|
||||
private static string Normalize(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return "/";
|
||||
}
|
||||
|
||||
var text = path.Replace('\\', '/').Trim();
|
||||
if (!text.StartsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
text = "/" + text;
|
||||
}
|
||||
|
||||
var parts = new List<string>();
|
||||
foreach (var part in text.Split('/', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (part == ".")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part == "..")
|
||||
{
|
||||
if (parts.Count > 0)
|
||||
{
|
||||
parts.RemoveAt(parts.Count - 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
parts.Add(part);
|
||||
}
|
||||
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return "/";
|
||||
}
|
||||
|
||||
var text = path.Replace('\\', '/').Trim();
|
||||
if (!text.StartsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
text = "/" + text;
|
||||
}
|
||||
|
||||
var parts = new List<string>();
|
||||
foreach (var part in text.Split('/', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (part == ".")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part == "..")
|
||||
{
|
||||
if (parts.Count > 0)
|
||||
{
|
||||
parts.RemoveAt(parts.Count - 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
parts.Add(part);
|
||||
}
|
||||
|
||||
return "/" + string.Join('/', parts);
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ internal sealed class TestRootFileSystem : IRootFileSystem
|
||||
_directories.Add(current);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private sealed class FileEntry
|
||||
{
|
||||
private readonly byte[] _content;
|
||||
@@ -300,4 +300,4 @@ internal sealed class TestRootFileSystem : IRootFileSystem
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user