Add unit tests for RancherHubConnector and various exporters

- Implemented tests for RancherHubConnector to validate fetching documents, handling errors, and managing state.
- Added tests for CsafExporter to ensure deterministic serialization of CSAF documents.
- Created tests for CycloneDX exporters and reconciler to verify correct handling of VEX claims and output structure.
- Developed OpenVEX exporter tests to confirm the generation of canonical OpenVEX documents and statement merging logic.
- Introduced Rust file caching and license scanning functionality, including a cache key structure and hash computation.
- Added sample Cargo.toml and LICENSE files for testing Rust license scanning functionality.
This commit is contained in:
master
2025-10-30 07:52:39 +02:00
parent 0bc882e75a
commit a3822c88cd
62 changed files with 3631 additions and 423 deletions

View File

@@ -77,26 +77,59 @@ public sealed class JavaReflectionAnalyzerTests
}
[Fact]
public void Analyze_SpringBootFatJar_ScansEmbeddedAndBootSegments()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
JavaFixtureBuilder.CreateSpringBootFatJar(root, "apps/app-fat.jar");
var cancellationToken = TestContext.Current.CancellationToken;
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
var analysis = JavaReflectionAnalyzer.Analyze(classPath, cancellationToken);
// Expect at least one edge originating from BOOT-INF classes
Assert.Contains(analysis.Edges, edge => edge.SourceClass == "com.example.App" && edge.Reason == JavaReflectionReason.ClassForName);
Assert.Contains(analysis.Edges, edge => edge.SourceClass == "com.example.Lib" && edge.Reason == JavaReflectionReason.ClassForName);
}
finally
{
TestPaths.SafeDelete(root);
}
}
}
public void Analyze_SpringBootFatJar_ScansEmbeddedAndBootSegments()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
JavaFixtureBuilder.CreateSpringBootFatJar(root, "apps/app-fat.jar");
var cancellationToken = TestContext.Current.CancellationToken;
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
var analysis = JavaReflectionAnalyzer.Analyze(classPath, cancellationToken);
// Expect at least one edge originating from BOOT-INF classes
Assert.Contains(analysis.Edges, edge => edge.SourceClass == "com.example.App" && edge.Reason == JavaReflectionReason.ClassForName);
Assert.Contains(analysis.Edges, edge => edge.SourceClass == "com.example.Lib" && edge.Reason == JavaReflectionReason.ClassForName);
}
finally
{
TestPaths.SafeDelete(root);
}
}
[Fact]
public void Analyze_ClassResourceLookup_ProducesResourceEdge()
{
var root = TestPaths.CreateTemporaryDirectory();
try
{
var jarPath = Path.Combine(root, "libs", "resources.jar");
Directory.CreateDirectory(Path.GetDirectoryName(jarPath)!);
using (var archive = new ZipArchive(new FileStream(jarPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None), ZipArchiveMode.Create, leaveOpen: false))
{
var entry = archive.CreateEntry("com/example/Resources.class");
var bytes = JavaClassFileFactory.CreateClassResourceLookup("com/example/Resources", "/META-INF/plugin.properties");
using var stream = entry.Open();
stream.Write(bytes);
}
var cancellationToken = TestContext.Current.CancellationToken;
var context = new LanguageAnalyzerContext(root, TimeProvider.System);
var workspace = JavaWorkspaceNormalizer.Normalize(context, cancellationToken);
var classPath = JavaClassPathBuilder.Build(workspace, cancellationToken);
var analysis = JavaReflectionAnalyzer.Analyze(classPath, cancellationToken);
var edge = Assert.Single(analysis.Edges.Where(edge => edge.Reason == JavaReflectionReason.ResourceLookup));
Assert.Equal("com.example.Resources", edge.SourceClass);
Assert.Equal("/META-INF/plugin.properties", edge.TargetType);
Assert.Equal(JavaReflectionConfidence.High, edge.Confidence);
}
finally
{
TestPaths.SafeDelete(root);
}
}
}

View File

@@ -0,0 +1,4 @@
[package]
name = "my_app"
version = "0.1.0"
license = "MIT"

View File

@@ -0,0 +1,16 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -7,12 +7,13 @@
"version": "0.1.0",
"type": "cargo",
"usedByEntrypoint": false,
"metadata": {
"cargo.lock.path": "Cargo.lock",
"fingerprint.profile": "debug",
"fingerprint.targetKind": "bin",
"source": "registry\u002Bhttps://github.com/rust-lang/crates.io-index"
},
"metadata": {
"cargo.lock.path": "Cargo.lock",
"fingerprint.profile": "debug",
"fingerprint.targetKind": "bin",
"license.expression[0]": "MIT",
"source": "registry\u002Bhttps://github.com/rust-lang/crates.io-index"
},
"evidence": [
{
"kind": "file",
@@ -36,13 +37,14 @@
"version": "1.0.188",
"type": "cargo",
"usedByEntrypoint": false,
"metadata": {
"cargo.lock.path": "Cargo.lock",
"checksum": "abc123",
"fingerprint.profile": "release",
"fingerprint.targetKind": "lib",
"source": "registry\u002Bhttps://github.com/rust-lang/crates.io-index"
},
"metadata": {
"cargo.lock.path": "Cargo.lock",
"checksum": "abc123",
"fingerprint.profile": "release",
"fingerprint.targetKind": "lib",
"license.expression[0]": "Apache-2.0",
"source": "registry\u002Bhttps://github.com/rust-lang/crates.io-index"
},
"evidence": [
{
"kind": "file",
@@ -59,4 +61,4 @@
}
]
}
]
]

View File

@@ -0,0 +1,4 @@
[package]
name = "serde"
version = "1.0.188"
license = "Apache-2.0"

View File

@@ -1,4 +1,6 @@
using System;
using System.IO;
using System.Linq;
using StellaOps.Scanner.Analyzers.Lang.Rust;
using StellaOps.Scanner.Analyzers.Lang.Tests.Harness;
using StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
@@ -31,4 +33,27 @@ public sealed class RustLanguageAnalyzerTests
cancellationToken,
usageHints);
}
[Fact]
public async Task AnalyzerIsThreadSafeUnderConcurrencyAsync()
{
var cancellationToken = TestContext.Current.CancellationToken;
var fixturePath = TestPaths.ResolveFixture("lang", "rust", "simple");
var analyzers = new ILanguageAnalyzer[]
{
new RustLanguageAnalyzer()
};
var workers = Math.Max(Environment.ProcessorCount, 4);
var tasks = Enumerable.Range(0, workers)
.Select(_ => LanguageAnalyzerTestHarness.RunToJsonAsync(fixturePath, analyzers, cancellationToken));
var results = await Task.WhenAll(tasks);
var baseline = results[0];
foreach (var result in results)
{
Assert.Equal(baseline, result);
}
}
}

View File

@@ -5,11 +5,11 @@ namespace StellaOps.Scanner.Analyzers.Lang.Tests.TestUtilities;
public static class JavaClassFileFactory
{
public static byte[] CreateClassForNameInvoker(string internalClassName, string targetClassName)
{
using var buffer = new MemoryStream();
using var writer = new BigEndianWriter(buffer);
public static byte[] CreateClassForNameInvoker(string internalClassName, string targetClassName)
{
using var buffer = new MemoryStream();
using var writer = new BigEndianWriter(buffer);
WriteClassFileHeader(writer, constantPoolCount: 16);
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8(internalClassName); // #1
@@ -40,8 +40,46 @@ public static class JavaClassFileFactory
writer.WriteUInt16(0); // class attributes
return buffer.ToArray();
}
return buffer.ToArray();
}
public static byte[] CreateClassResourceLookup(string internalClassName, string resourcePath)
{
using var buffer = new MemoryStream();
using var writer = new BigEndianWriter(buffer);
WriteClassFileHeader(writer, constantPoolCount: 18);
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8(internalClassName); // #1
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(1); // #2
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("java/lang/Object"); // #3
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(3); // #4
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("load"); // #5
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("()V"); // #6
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("Code"); // #7
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8(resourcePath); // #8
writer.WriteByte((byte)ConstantTag.String); writer.WriteUInt16(8); // #9
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("java/lang/Class"); // #10
writer.WriteByte((byte)ConstantTag.Class); writer.WriteUInt16(10); // #11
writer.WriteByte((byte)ConstantTag.Utf8); writer.WriteUtf8("getResource"); // #12
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.WriteUInt16(0x0001); // public
writer.WriteUInt16(2); // this class
writer.WriteUInt16(4); // super class
writer.WriteUInt16(0); // interfaces
writer.WriteUInt16(0); // fields
writer.WriteUInt16(1); // methods
WriteResourceLookupMethod(writer, methodNameIndex: 5, descriptorIndex: 6, classConstantIndex: 4, stringIndex: 9, methodRefIndex: 15);
writer.WriteUInt16(0); // class attributes
return buffer.ToArray();
}
public static byte[] CreateTcclChecker(string internalClassName)
{
@@ -119,11 +157,11 @@ public static class JavaClassFileFactory
writer.WriteBytes(codeBytes);
}
private static void WriteTcclMethod(BigEndianWriter writer, ushort methodNameIndex, ushort descriptorIndex, ushort currentThreadMethodRefIndex, ushort getContextMethodRefIndex)
{
writer.WriteUInt16(0x0009);
writer.WriteUInt16(methodNameIndex);
writer.WriteUInt16(descriptorIndex);
private static void WriteTcclMethod(BigEndianWriter writer, ushort methodNameIndex, ushort descriptorIndex, ushort currentThreadMethodRefIndex, ushort getContextMethodRefIndex)
{
writer.WriteUInt16(0x0009);
writer.WriteUInt16(methodNameIndex);
writer.WriteUInt16(descriptorIndex);
writer.WriteUInt16(1);
writer.WriteUInt16(7);
@@ -144,9 +182,40 @@ public static class JavaClassFileFactory
}
var codeBytes = codeBuffer.ToArray();
writer.WriteUInt32((uint)codeBytes.Length);
writer.WriteBytes(codeBytes);
}
writer.WriteUInt32((uint)codeBytes.Length);
writer.WriteBytes(codeBytes);
}
private static void WriteResourceLookupMethod(BigEndianWriter writer, ushort methodNameIndex, ushort descriptorIndex, ushort classConstantIndex, ushort stringIndex, ushort methodRefIndex)
{
writer.WriteUInt16(0x0009);
writer.WriteUInt16(methodNameIndex);
writer.WriteUInt16(descriptorIndex);
writer.WriteUInt16(1);
writer.WriteUInt16(7);
using var codeBuffer = new MemoryStream();
using (var codeWriter = new BigEndianWriter(codeBuffer))
{
codeWriter.WriteUInt16(2);
codeWriter.WriteUInt16(0);
codeWriter.WriteUInt32(8);
codeWriter.WriteByte(0x13); // ldc_w for class literal
codeWriter.WriteUInt16(classConstantIndex);
codeWriter.WriteByte(0x12);
codeWriter.WriteByte((byte)stringIndex);
codeWriter.WriteByte(0xB6);
codeWriter.WriteUInt16(methodRefIndex);
codeWriter.WriteByte(0x57);
codeWriter.WriteByte(0xB1);
codeWriter.WriteUInt16(0);
codeWriter.WriteUInt16(0);
}
var codeBytes = codeBuffer.ToArray();
writer.WriteUInt32((uint)codeBytes.Length);
writer.WriteBytes(codeBytes);
}
private sealed class BigEndianWriter : IDisposable
{