Add unit tests for PackRunAttestation and SealedInstallEnforcer
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
Policy Lint & Smoke / policy-lint (push) Has been cancelled
release-manifest-verify / verify (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
Policy Lint & Smoke / policy-lint (push) Has been cancelled
release-manifest-verify / verify (push) Has been cancelled
- Implement comprehensive tests for PackRunAttestationService, covering attestation generation, verification, and event emission. - Add tests for SealedInstallEnforcer to validate sealed install requirements and enforcement logic. - Introduce a MonacoLoaderService stub for testing purposes to prevent Monaco workers/styles from loading during Karma runs.
This commit is contained in:
@@ -1,12 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.BuildMetadata;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Discovery;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Gradle;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Maven;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
|
||||
|
||||
internal static class JavaLockFileCollector
|
||||
{
|
||||
private static readonly string[] GradleLockPatterns = { "gradle.lockfile" };
|
||||
private static readonly string[] GradleLockPatterns = ["gradle.lockfile"];
|
||||
|
||||
public static async Task<JavaLockData> LoadAsync(LanguageAnalyzerContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -15,6 +20,10 @@ internal static class JavaLockFileCollector
|
||||
var entries = new Dictionary<string, JavaLockEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
var root = context.RootPath;
|
||||
|
||||
// Discover all build files
|
||||
var buildFiles = JavaBuildFileDiscovery.Discover(root);
|
||||
|
||||
// Priority 1: Gradle lockfiles (most reliable)
|
||||
foreach (var pattern in GradleLockPatterns)
|
||||
{
|
||||
var lockPath = Path.Combine(root, pattern);
|
||||
@@ -33,15 +42,35 @@ internal static class JavaLockFileCollector
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: If no lockfiles, parse Gradle build files with version catalog
|
||||
if (entries.Count == 0 && buildFiles.UsesGradle && !buildFiles.HasGradleLockFiles)
|
||||
{
|
||||
await ParseGradleBuildFilesAsync(context, buildFiles, entries, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Priority 3: Parse Maven POMs with property resolution
|
||||
foreach (var pomFile in buildFiles.MavenPoms)
|
||||
{
|
||||
await ParsePomWithResolutionAsync(context, pomFile.AbsolutePath, entries, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Fallback: original pom.xml scanning for any POMs not caught by discovery
|
||||
foreach (var pomPath in Directory.EnumerateFiles(root, "pom.xml", SearchOption.AllDirectories))
|
||||
{
|
||||
await ParsePomAsync(context, pomPath, entries, cancellationToken).ConfigureAwait(false);
|
||||
if (!buildFiles.MavenPoms.Any(p => p.AbsolutePath.Equals(pomPath, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
await ParsePomWithResolutionAsync(context, pomPath, entries, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return entries.Count == 0 ? JavaLockData.Empty : new JavaLockData(entries);
|
||||
}
|
||||
|
||||
private static async Task ParseGradleLockFileAsync(LanguageAnalyzerContext context, string path, IDictionary<string, JavaLockEntry> entries, CancellationToken cancellationToken)
|
||||
private static async Task ParseGradleLockFileAsync(
|
||||
LanguageAnalyzerContext context,
|
||||
string path,
|
||||
IDictionary<string, JavaLockEntry> entries,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
using var reader = new StreamReader(stream);
|
||||
@@ -52,7 +81,7 @@ internal static class JavaLockFileCollector
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
line = line.Trim();
|
||||
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#", StringComparison.Ordinal))
|
||||
if (string.IsNullOrWhiteSpace(line) || line.StartsWith('#'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -76,6 +105,9 @@ internal static class JavaLockFileCollector
|
||||
continue;
|
||||
}
|
||||
|
||||
var scope = MapGradleConfigurationToScope(configuration);
|
||||
var riskLevel = JavaScopeClassifier.GetRiskLevel(scope);
|
||||
|
||||
var entry = new JavaLockEntry(
|
||||
groupId.Trim(),
|
||||
artifactId.Trim(),
|
||||
@@ -84,13 +116,193 @@ internal static class JavaLockFileCollector
|
||||
NormalizeLocator(context, path),
|
||||
configuration,
|
||||
null,
|
||||
null,
|
||||
scope,
|
||||
riskLevel,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
entries[entry.Key] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ParsePomAsync(LanguageAnalyzerContext context, string path, IDictionary<string, JavaLockEntry> entries, CancellationToken cancellationToken)
|
||||
private static async Task ParseGradleBuildFilesAsync(
|
||||
LanguageAnalyzerContext context,
|
||||
JavaBuildFiles buildFiles,
|
||||
IDictionary<string, JavaLockEntry> entries,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Load version catalog if present
|
||||
GradleVersionCatalog? versionCatalog = null;
|
||||
if (buildFiles.HasVersionCatalog)
|
||||
{
|
||||
var catalogFile = buildFiles.VersionCatalogFiles.FirstOrDefault();
|
||||
if (catalogFile is not null)
|
||||
{
|
||||
versionCatalog = await GradleVersionCatalogParser.ParseAsync(
|
||||
catalogFile.AbsolutePath,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Load gradle.properties
|
||||
GradleProperties? gradleProperties = null;
|
||||
var propsFile = buildFiles.GradlePropertiesFiles.FirstOrDefault();
|
||||
if (propsFile is not null)
|
||||
{
|
||||
gradleProperties = await GradlePropertiesParser.ParseAsync(
|
||||
propsFile.AbsolutePath,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Parse Kotlin DSL files
|
||||
foreach (var ktsFile in buildFiles.GradleKotlinFiles)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var buildFile = await GradleKotlinParser.ParseAsync(
|
||||
ktsFile.AbsolutePath,
|
||||
gradleProperties,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
AddGradleDependencies(context, buildFile, versionCatalog, entries);
|
||||
}
|
||||
|
||||
// Parse Groovy DSL files
|
||||
foreach (var groovyFile in buildFiles.GradleGroovyFiles)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var buildFile = await GradleGroovyParser.ParseAsync(
|
||||
groovyFile.AbsolutePath,
|
||||
gradleProperties,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
AddGradleDependencies(context, buildFile, versionCatalog, entries);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddGradleDependencies(
|
||||
LanguageAnalyzerContext context,
|
||||
GradleBuildFile buildFile,
|
||||
GradleVersionCatalog? versionCatalog,
|
||||
IDictionary<string, JavaLockEntry> entries)
|
||||
{
|
||||
foreach (var dep in buildFile.Dependencies)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dep.GroupId) || string.IsNullOrWhiteSpace(dep.ArtifactId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var version = dep.Version;
|
||||
|
||||
// Try to resolve from version catalog if version is missing
|
||||
if (string.IsNullOrWhiteSpace(version) && versionCatalog is not null)
|
||||
{
|
||||
// Check if this dependency matches a catalog library
|
||||
var catalogLib = versionCatalog.Libraries.Values
|
||||
.FirstOrDefault(l =>
|
||||
l.GroupId.Equals(dep.GroupId, StringComparison.OrdinalIgnoreCase) &&
|
||||
l.ArtifactId.Equals(dep.ArtifactId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
version = catalogLib?.Version;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var scope = dep.Scope ?? "compile";
|
||||
var riskLevel = JavaScopeClassifier.GetRiskLevel(scope);
|
||||
|
||||
var entry = new JavaLockEntry(
|
||||
dep.GroupId,
|
||||
dep.ArtifactId,
|
||||
version,
|
||||
Path.GetFileName(buildFile.SourcePath),
|
||||
NormalizeLocator(context, buildFile.SourcePath),
|
||||
scope,
|
||||
null,
|
||||
null,
|
||||
scope,
|
||||
riskLevel,
|
||||
dep.VersionSource.ToString().ToLowerInvariant(),
|
||||
dep.VersionProperty,
|
||||
null);
|
||||
|
||||
entries.TryAdd(entry.Key, entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ParsePomWithResolutionAsync(
|
||||
LanguageAnalyzerContext context,
|
||||
string path,
|
||||
IDictionary<string, JavaLockEntry> entries,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pom = await MavenPomParser.ParseAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
if (pom == MavenPom.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Build effective POM with property resolution
|
||||
var effectivePomBuilder = new MavenEffectivePomBuilder(context.RootPath);
|
||||
var effectivePom = await effectivePomBuilder.BuildAsync(pom, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var dep in effectivePom.ResolvedDependencies)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dep.GroupId) ||
|
||||
string.IsNullOrWhiteSpace(dep.ArtifactId) ||
|
||||
string.IsNullOrWhiteSpace(dep.Version) ||
|
||||
!dep.IsVersionResolved)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var scope = dep.Scope ?? "compile";
|
||||
var riskLevel = JavaScopeClassifier.GetRiskLevel(scope);
|
||||
|
||||
// Get license info if available
|
||||
var license = effectivePom.Licenses.FirstOrDefault();
|
||||
|
||||
var entry = new JavaLockEntry(
|
||||
dep.GroupId,
|
||||
dep.ArtifactId,
|
||||
dep.Version,
|
||||
"pom.xml",
|
||||
NormalizeLocator(context, path),
|
||||
scope,
|
||||
null,
|
||||
null,
|
||||
scope,
|
||||
riskLevel,
|
||||
dep.VersionSource.ToString().ToLowerInvariant(),
|
||||
dep.VersionProperty,
|
||||
license?.SpdxId);
|
||||
|
||||
entries.TryAdd(entry.Key, entry);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall back to simple parsing if resolution fails
|
||||
await ParsePomSimpleAsync(context, path, entries, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ParsePomSimpleAsync(
|
||||
LanguageAnalyzerContext context,
|
||||
string path,
|
||||
IDictionary<string, JavaLockEntry> entries,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var document = await XDocument.LoadAsync(stream, LoadOptions.None, cancellationToken).ConfigureAwait(false);
|
||||
@@ -117,6 +329,9 @@ internal static class JavaLockFileCollector
|
||||
continue;
|
||||
}
|
||||
|
||||
scope ??= "compile";
|
||||
var riskLevel = JavaScopeClassifier.GetRiskLevel(scope);
|
||||
|
||||
var entry = new JavaLockEntry(
|
||||
groupId,
|
||||
artifactId,
|
||||
@@ -125,12 +340,49 @@ internal static class JavaLockFileCollector
|
||||
NormalizeLocator(context, path),
|
||||
scope,
|
||||
repository,
|
||||
null,
|
||||
scope,
|
||||
riskLevel,
|
||||
"direct",
|
||||
null,
|
||||
null);
|
||||
|
||||
entries[entry.Key] = entry;
|
||||
entries.TryAdd(entry.Key, entry);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? MapGradleConfigurationToScope(string? configuration)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configuration))
|
||||
{
|
||||
return "compile";
|
||||
}
|
||||
|
||||
// Parse configuration like "compileClasspath,runtimeClasspath"
|
||||
var configs = configuration.Split(',', StringSplitOptions.TrimEntries);
|
||||
|
||||
foreach (var config in configs)
|
||||
{
|
||||
var scope = config.ToLowerInvariant() switch
|
||||
{
|
||||
"compileclasspath" or "implementation" or "api" => "compile",
|
||||
"runtimeclasspath" or "runtimeonly" => "runtime",
|
||||
"testcompileclasspath" or "testimplementation" => "test",
|
||||
"testruntimeclasspath" or "testruntimeonly" => "test",
|
||||
"compileonly" => "provided",
|
||||
"annotationprocessor" => "compile",
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (scope is not null)
|
||||
{
|
||||
return scope;
|
||||
}
|
||||
}
|
||||
|
||||
return "compile";
|
||||
}
|
||||
|
||||
private static string NormalizeLocator(LanguageAnalyzerContext context, string path)
|
||||
=> context.GetRelativePath(path).Replace('\\', '/');
|
||||
}
|
||||
@@ -143,7 +395,12 @@ internal sealed record JavaLockEntry(
|
||||
string Locator,
|
||||
string? Configuration,
|
||||
string? Repository,
|
||||
string? ResolvedUrl)
|
||||
string? ResolvedUrl,
|
||||
string? Scope,
|
||||
string? RiskLevel,
|
||||
string? VersionSource,
|
||||
string? VersionProperty,
|
||||
string? License)
|
||||
{
|
||||
public string Key => BuildKey(GroupId, ArtifactId, Version);
|
||||
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Maven;
|
||||
|
||||
/// <summary>
|
||||
/// Discovers and accesses the local Maven repository (~/.m2/repository).
|
||||
/// </summary>
|
||||
internal sealed class MavenLocalRepository
|
||||
{
|
||||
private readonly string? _repositoryPath;
|
||||
|
||||
public MavenLocalRepository()
|
||||
{
|
||||
_repositoryPath = DiscoverRepositoryPath();
|
||||
}
|
||||
|
||||
public MavenLocalRepository(string repositoryPath)
|
||||
{
|
||||
_repositoryPath = repositoryPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository path, or null if not found.
|
||||
/// </summary>
|
||||
public string? RepositoryPath => _repositoryPath;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the local repository exists.
|
||||
/// </summary>
|
||||
public bool Exists => _repositoryPath is not null && Directory.Exists(_repositoryPath);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to a POM file in the local repository.
|
||||
/// </summary>
|
||||
public string? GetPomPath(string groupId, string artifactId, string version)
|
||||
{
|
||||
if (_repositoryPath is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var relativePath = GetRelativePath(groupId, artifactId, version, $"{artifactId}-{version}.pom");
|
||||
return Path.Combine(_repositoryPath, relativePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to a JAR file in the local repository.
|
||||
/// </summary>
|
||||
public string? GetJarPath(string groupId, string artifactId, string version, string? classifier = null)
|
||||
{
|
||||
if (_repositoryPath is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var fileName = classifier is null
|
||||
? $"{artifactId}-{version}.jar"
|
||||
: $"{artifactId}-{version}-{classifier}.jar";
|
||||
|
||||
var relativePath = GetRelativePath(groupId, artifactId, version, fileName);
|
||||
return Path.Combine(_repositoryPath, relativePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the directory path for an artifact version in the local repository.
|
||||
/// </summary>
|
||||
public string? GetArtifactDirectory(string groupId, string artifactId, string version)
|
||||
{
|
||||
if (_repositoryPath is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var groupPath = groupId.Replace('.', Path.DirectorySeparatorChar);
|
||||
return Path.Combine(_repositoryPath, groupPath, artifactId, version);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a POM exists in the local repository.
|
||||
/// </summary>
|
||||
public bool HasPom(string groupId, string artifactId, string version)
|
||||
{
|
||||
var path = GetPomPath(groupId, artifactId, version);
|
||||
return path is not null && File.Exists(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a JAR exists in the local repository.
|
||||
/// </summary>
|
||||
public bool HasJar(string groupId, string artifactId, string version, string? classifier = null)
|
||||
{
|
||||
var path = GetJarPath(groupId, artifactId, version, classifier);
|
||||
return path is not null && File.Exists(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists all versions of an artifact in the local repository.
|
||||
/// </summary>
|
||||
public IEnumerable<string> GetAvailableVersions(string groupId, string artifactId)
|
||||
{
|
||||
if (_repositoryPath is null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var groupPath = groupId.Replace('.', Path.DirectorySeparatorChar);
|
||||
var artifactDir = Path.Combine(_repositoryPath, groupPath, artifactId);
|
||||
|
||||
if (!Directory.Exists(artifactDir))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var versionDir in Directory.EnumerateDirectories(artifactDir))
|
||||
{
|
||||
var version = Path.GetFileName(versionDir);
|
||||
var pomPath = Path.Combine(versionDir, $"{artifactId}-{version}.pom");
|
||||
|
||||
if (File.Exists(pomPath))
|
||||
{
|
||||
yield return version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a POM from the local repository.
|
||||
/// </summary>
|
||||
public async Task<MavenPom?> ReadPomAsync(
|
||||
string groupId,
|
||||
string artifactId,
|
||||
string version,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var path = GetPomPath(groupId, artifactId, version);
|
||||
if (path is null || !File.Exists(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await MavenPomParser.ParseAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string GetRelativePath(string groupId, string artifactId, string version, string fileName)
|
||||
{
|
||||
var groupPath = groupId.Replace('.', Path.DirectorySeparatorChar);
|
||||
return Path.Combine(groupPath, artifactId, version, fileName);
|
||||
}
|
||||
|
||||
private static string? DiscoverRepositoryPath()
|
||||
{
|
||||
// Check M2_REPO environment variable
|
||||
var m2Repo = Environment.GetEnvironmentVariable("M2_REPO");
|
||||
if (!string.IsNullOrEmpty(m2Repo) && Directory.Exists(m2Repo))
|
||||
{
|
||||
return m2Repo;
|
||||
}
|
||||
|
||||
// Check MAVEN_REPOSITORY environment variable
|
||||
var mavenRepo = Environment.GetEnvironmentVariable("MAVEN_REPOSITORY");
|
||||
if (!string.IsNullOrEmpty(mavenRepo) && Directory.Exists(mavenRepo))
|
||||
{
|
||||
return mavenRepo;
|
||||
}
|
||||
|
||||
// Check for custom settings in ~/.m2/settings.xml
|
||||
var settingsPath = GetSettingsPath();
|
||||
if (settingsPath is not null)
|
||||
{
|
||||
var customPath = TryParseLocalRepositoryFromSettings(settingsPath);
|
||||
if (!string.IsNullOrEmpty(customPath) && Directory.Exists(customPath))
|
||||
{
|
||||
return customPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Default: ~/.m2/repository
|
||||
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var defaultPath = Path.Combine(userHome, ".m2", "repository");
|
||||
|
||||
return Directory.Exists(defaultPath) ? defaultPath : null;
|
||||
}
|
||||
|
||||
private static string? GetSettingsPath()
|
||||
{
|
||||
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var settingsPath = Path.Combine(userHome, ".m2", "settings.xml");
|
||||
|
||||
return File.Exists(settingsPath) ? settingsPath : null;
|
||||
}
|
||||
|
||||
private static string? TryParseLocalRepositoryFromSettings(string settingsPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var content = File.ReadAllText(settingsPath);
|
||||
var startTag = "<localRepository>";
|
||||
var endTag = "</localRepository>";
|
||||
|
||||
var startIndex = content.IndexOf(startTag, StringComparison.OrdinalIgnoreCase);
|
||||
if (startIndex < 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
startIndex += startTag.Length;
|
||||
var endIndex = content.IndexOf(endTag, startIndex, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (endIndex > startIndex)
|
||||
{
|
||||
var path = content[startIndex..endIndex].Trim();
|
||||
|
||||
// Expand environment variables
|
||||
path = Environment.ExpandEnvironmentVariables(path);
|
||||
|
||||
// Handle ${user.home}
|
||||
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
path = path.Replace("${user.home}", userHome, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore parsing errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal stub for shaded JAR analysis results pending full Postgres migration cleanup.
|
||||
/// </summary>
|
||||
internal sealed record ShadedJarAnalysisResult(
|
||||
bool IsShaded,
|
||||
double Confidence,
|
||||
IReadOnlyList<string> Markers,
|
||||
IReadOnlyList<string> EmbeddedArtifacts,
|
||||
IReadOnlyList<string> RelocatedPrefixes)
|
||||
{
|
||||
public static ShadedJarAnalysisResult None { get; } =
|
||||
new(false, 0, Array.Empty<string>(), Array.Empty<string>(), Array.Empty<string>());
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,108 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Gradle;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests.Parsers;
|
||||
|
||||
public sealed class GradleGroovyParserTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ParsesStringNotationDependenciesAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var content = """
|
||||
dependencies {
|
||||
implementation 'org.slf4j:slf4j-api:1.7.36'
|
||||
api "com.google.guava:guava:31.1-jre"
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
}
|
||||
""";
|
||||
|
||||
var tempFile = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(tempFile, content, cancellationToken);
|
||||
var result = await GradleGroovyParser.ParseAsync(tempFile, null, cancellationToken);
|
||||
|
||||
Assert.Equal(3, result.Dependencies.Length);
|
||||
|
||||
var slf4j = result.Dependencies.First(d => d.ArtifactId == "slf4j-api");
|
||||
Assert.Equal("org.slf4j", slf4j.GroupId);
|
||||
Assert.Equal("1.7.36", slf4j.Version);
|
||||
Assert.Equal("implementation", slf4j.Scope);
|
||||
|
||||
var guava = result.Dependencies.First(d => d.ArtifactId == "guava");
|
||||
Assert.Equal("com.google.guava", guava.GroupId);
|
||||
Assert.Equal("31.1-jre", guava.Version);
|
||||
Assert.Equal("api", guava.Scope);
|
||||
|
||||
var junit = result.Dependencies.First(d => d.ArtifactId == "junit");
|
||||
Assert.Equal("junit", junit.GroupId);
|
||||
Assert.Equal("4.13.2", junit.Version);
|
||||
Assert.Equal("testImplementation", junit.Scope);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParsesMapNotationDependenciesAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var content = """
|
||||
dependencies {
|
||||
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
|
||||
compileOnly(group: "javax.servlet", name: "servlet-api", version: "2.5")
|
||||
}
|
||||
""";
|
||||
|
||||
var tempFile = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(tempFile, content, cancellationToken);
|
||||
var result = await GradleGroovyParser.ParseAsync(tempFile, null, cancellationToken);
|
||||
|
||||
Assert.Equal(2, result.Dependencies.Length);
|
||||
|
||||
var commons = result.Dependencies.First(d => d.ArtifactId == "commons-lang3");
|
||||
Assert.Equal("org.apache.commons", commons.GroupId);
|
||||
Assert.Equal("3.12.0", commons.Version);
|
||||
Assert.Equal("implementation", commons.Scope);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolvesPropertyPlaceholdersAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var content = """
|
||||
dependencies {
|
||||
implementation "org.slf4j:slf4j-api:${slf4jVersion}"
|
||||
}
|
||||
""";
|
||||
|
||||
var tempFile = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(tempFile, content, cancellationToken);
|
||||
var properties = new GradleProperties(
|
||||
new Dictionary<string, string> { ["slf4jVersion"] = "2.0.7" }.ToImmutableDictionary(),
|
||||
ImmutableDictionary<string, string>.Empty);
|
||||
|
||||
var result = await GradleGroovyParser.ParseAsync(tempFile, properties, cancellationToken);
|
||||
|
||||
Assert.Single(result.Dependencies);
|
||||
var dep = result.Dependencies[0];
|
||||
Assert.Equal("2.0.7", dep.Version);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Maven;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests.Parsers;
|
||||
|
||||
public sealed class MavenPomParserTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ParsesDependenciesAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>demo</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>1.7.36</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
""";
|
||||
|
||||
var tempFile = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(tempFile, content, cancellationToken);
|
||||
var result = await MavenPomParser.ParseAsync(tempFile, cancellationToken);
|
||||
|
||||
Assert.NotEqual(MavenPom.Empty, result);
|
||||
Assert.Equal("com.example", result.GroupId);
|
||||
Assert.Equal("demo", result.ArtifactId);
|
||||
Assert.Equal("1.0.0", result.Version);
|
||||
Assert.Equal(2, result.Dependencies.Length);
|
||||
|
||||
var slf4j = result.Dependencies.First(d => d.ArtifactId == "slf4j-api");
|
||||
Assert.Equal("org.slf4j", slf4j.GroupId);
|
||||
Assert.Equal("1.7.36", slf4j.Version);
|
||||
Assert.Null(slf4j.Scope);
|
||||
|
||||
var junit = result.Dependencies.First(d => d.ArtifactId == "junit");
|
||||
Assert.Equal("test", junit.Scope);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParsesPropertiesAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project>
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>demo</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<properties>
|
||||
<slf4j.version>2.0.7</slf4j.version>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
""";
|
||||
|
||||
var tempFile = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(tempFile, content, cancellationToken);
|
||||
var result = await MavenPomParser.ParseAsync(tempFile, cancellationToken);
|
||||
|
||||
Assert.Equal(2, result.Properties.Count);
|
||||
Assert.Equal("2.0.7", result.Properties["slf4j.version"]);
|
||||
Assert.Equal("17", result.Properties["java.version"]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParsesLicensesAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project>
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>demo</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Apache License, Version 2.0</name>
|
||||
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
|
||||
</license>
|
||||
</licenses>
|
||||
</project>
|
||||
""";
|
||||
|
||||
var tempFile = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(tempFile, content, cancellationToken);
|
||||
var result = await MavenPomParser.ParseAsync(tempFile, cancellationToken);
|
||||
|
||||
Assert.Single(result.Licenses);
|
||||
var license = result.Licenses[0];
|
||||
Assert.Equal("Apache License, Version 2.0", license.Name);
|
||||
Assert.Equal("https://www.apache.org/licenses/LICENSE-2.0", license.Url);
|
||||
Assert.Equal("Apache-2.0", license.SpdxId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParsesParentReferenceAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.1.0</version>
|
||||
</parent>
|
||||
<artifactId>demo</artifactId>
|
||||
</project>
|
||||
""";
|
||||
|
||||
var tempFile = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(tempFile, content, cancellationToken);
|
||||
var result = await MavenPomParser.ParseAsync(tempFile, cancellationToken);
|
||||
|
||||
Assert.NotNull(result.Parent);
|
||||
Assert.Equal("org.springframework.boot", result.Parent.GroupId);
|
||||
Assert.Equal("spring-boot-starter-parent", result.Parent.ArtifactId);
|
||||
Assert.Equal("3.1.0", result.Parent.Version);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParsesDependencyManagementAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var content = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project>
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>demo</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
</project>
|
||||
""";
|
||||
|
||||
var tempFile = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(tempFile, content, cancellationToken);
|
||||
var result = await MavenPomParser.ParseAsync(tempFile, cancellationToken);
|
||||
|
||||
Assert.Single(result.DependencyManagement);
|
||||
var bom = result.DependencyManagement[0];
|
||||
Assert.Equal("org.springframework.boot", bom.GroupId);
|
||||
Assert.Equal("spring-boot-dependencies", bom.ArtifactId);
|
||||
Assert.Equal("pom", bom.Type);
|
||||
Assert.Equal("import", bom.Scope);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Osgi;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests.Parsers;
|
||||
|
||||
public sealed class OsgiBundleParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void ParsesBasicBundleManifest()
|
||||
{
|
||||
var manifest = """
|
||||
Manifest-Version: 1.0
|
||||
Bundle-SymbolicName: com.example.bundle
|
||||
Bundle-Version: 1.0.0.SNAPSHOT
|
||||
Bundle-Name: Example Bundle
|
||||
Bundle-Vendor: Example Corp
|
||||
""";
|
||||
|
||||
var dict = OsgiBundleParser.ParseManifest(manifest);
|
||||
var result = OsgiBundleParser.Parse(dict);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("com.example.bundle", result.SymbolicName);
|
||||
Assert.Equal("1.0.0.SNAPSHOT", result.Version);
|
||||
Assert.Equal("Example Bundle", result.Name);
|
||||
Assert.Equal("Example Corp", result.Vendor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesImportPackage()
|
||||
{
|
||||
var manifest = """
|
||||
Manifest-Version: 1.0
|
||||
Bundle-SymbolicName: com.example.bundle
|
||||
Bundle-Version: 1.0.0
|
||||
Import-Package: org.osgi.framework;version="[1.8,2)",
|
||||
org.slf4j;version="[1.7,2)",
|
||||
javax.servlet;resolution:=optional
|
||||
""";
|
||||
|
||||
var dict = OsgiBundleParser.ParseManifest(manifest);
|
||||
var result = OsgiBundleParser.Parse(dict);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(3, result.ImportPackage.Length);
|
||||
|
||||
var osgi = result.ImportPackage.First(p => p.PackageName == "org.osgi.framework");
|
||||
Assert.Equal("[1.8,2)", osgi.Version);
|
||||
Assert.False(osgi.IsOptional);
|
||||
|
||||
var servlet = result.ImportPackage.First(p => p.PackageName == "javax.servlet");
|
||||
Assert.True(servlet.IsOptional);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesExportPackage()
|
||||
{
|
||||
var manifest = """
|
||||
Manifest-Version: 1.0
|
||||
Bundle-SymbolicName: com.example.bundle
|
||||
Bundle-Version: 1.0.0
|
||||
Export-Package: com.example.api;version="1.0.0",
|
||||
com.example.impl;version="1.0.0"
|
||||
""";
|
||||
|
||||
var dict = OsgiBundleParser.ParseManifest(manifest);
|
||||
var result = OsgiBundleParser.Parse(dict);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.ExportPackage.Length);
|
||||
|
||||
var api = result.ExportPackage.First(p => p.PackageName == "com.example.api");
|
||||
Assert.Equal("1.0.0", api.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesSingletonBundle()
|
||||
{
|
||||
var manifest = """
|
||||
Manifest-Version: 1.0
|
||||
Bundle-SymbolicName: com.example.singleton;singleton:=true
|
||||
Bundle-Version: 1.0.0
|
||||
""";
|
||||
|
||||
var dict = OsgiBundleParser.ParseManifest(manifest);
|
||||
var result = OsgiBundleParser.Parse(dict);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("com.example.singleton", result.SymbolicName);
|
||||
Assert.True(result.IsSingleton);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesFragmentHost()
|
||||
{
|
||||
var manifest = """
|
||||
Manifest-Version: 1.0
|
||||
Bundle-SymbolicName: com.example.fragment
|
||||
Bundle-Version: 1.0.0
|
||||
Fragment-Host: com.example.host;bundle-version="[1.0,2.0)"
|
||||
""";
|
||||
|
||||
var dict = OsgiBundleParser.ParseManifest(manifest);
|
||||
var result = OsgiBundleParser.Parse(dict);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.IsFragment);
|
||||
Assert.Contains("com.example.host", result.FragmentHost);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReturnsNullForNonOsgiManifest()
|
||||
{
|
||||
var manifest = """
|
||||
Manifest-Version: 1.0
|
||||
Implementation-Title: Regular JAR
|
||||
Implementation-Version: 1.0.0
|
||||
""";
|
||||
|
||||
var dict = OsgiBundleParser.ParseManifest(manifest);
|
||||
var result = OsgiBundleParser.Parse(dict);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesManifestContinuationLines()
|
||||
{
|
||||
var manifest = "Manifest-Version: 1.0\r\n" +
|
||||
"Bundle-SymbolicName: com.example.bundle\r\n" +
|
||||
"Import-Package: org.osgi.framework;version=\"[1.8,2)\",org.slf4j;v\r\n" +
|
||||
" ersion=\"[1.7,2)\"\r\n" +
|
||||
"Bundle-Version: 1.0.0\r\n";
|
||||
|
||||
var dict = OsgiBundleParser.ParseManifest(manifest);
|
||||
var result = OsgiBundleParser.Parse(dict);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(2, result.ImportPackage.Length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Shading;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests.Parsers;
|
||||
|
||||
public sealed class ShadedJarDetectorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task DetectsMultiplePomPropertiesAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var jarPath = Path.Combine(Path.GetTempPath(), $"shaded-{Guid.NewGuid()}.jar");
|
||||
|
||||
try
|
||||
{
|
||||
using (var archive = ZipFile.Open(jarPath, ZipArchiveMode.Create))
|
||||
{
|
||||
WritePomProperties(archive, "com.example", "shaded", "1.0.0");
|
||||
WritePomProperties(archive, "org.slf4j", "slf4j-api", "1.7.36");
|
||||
WritePomProperties(archive, "com.google.guava", "guava", "31.1-jre");
|
||||
}
|
||||
|
||||
var result = await ShadedJarDetector.AnalyzeAsync(jarPath, cancellationToken);
|
||||
|
||||
Assert.True(result.IsShaded);
|
||||
Assert.Contains("multiple-pom-properties", result.Markers);
|
||||
Assert.Equal(3, result.EmbeddedArtifacts.Length);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(jarPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectsDependencyReducedPomAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var jarPath = Path.Combine(Path.GetTempPath(), $"shade-plugin-{Guid.NewGuid()}.jar");
|
||||
|
||||
try
|
||||
{
|
||||
using (var archive = ZipFile.Open(jarPath, ZipArchiveMode.Create))
|
||||
{
|
||||
WritePomProperties(archive, "com.example", "shaded", "1.0.0");
|
||||
var entry = archive.CreateEntry("META-INF/maven/com.example/shaded/dependency-reduced-pom.xml");
|
||||
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8);
|
||||
writer.Write("<project></project>");
|
||||
}
|
||||
|
||||
var result = await ShadedJarDetector.AnalyzeAsync(jarPath, cancellationToken);
|
||||
|
||||
Assert.True(result.IsShaded);
|
||||
Assert.Contains("dependency-reduced-pom.xml", result.Markers);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(jarPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DetectsRelocatedPackagesAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var jarPath = Path.Combine(Path.GetTempPath(), $"relocated-{Guid.NewGuid()}.jar");
|
||||
|
||||
try
|
||||
{
|
||||
using (var archive = ZipFile.Open(jarPath, ZipArchiveMode.Create))
|
||||
{
|
||||
WritePomProperties(archive, "com.example", "shaded", "1.0.0");
|
||||
// Create relocated class files
|
||||
CreateEmptyClass(archive, "shaded/com/google/common/collect/ImmutableList.class");
|
||||
CreateEmptyClass(archive, "shaded/com/google/common/base/Preconditions.class");
|
||||
}
|
||||
|
||||
var result = await ShadedJarDetector.AnalyzeAsync(jarPath, cancellationToken);
|
||||
|
||||
Assert.True(result.IsShaded);
|
||||
Assert.Contains("relocated-packages", result.Markers);
|
||||
Assert.NotEmpty(result.RelocatedPrefixes);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(jarPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnsNotShadedForRegularJarAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var jarPath = Path.Combine(Path.GetTempPath(), $"regular-{Guid.NewGuid()}.jar");
|
||||
|
||||
try
|
||||
{
|
||||
using (var archive = ZipFile.Open(jarPath, ZipArchiveMode.Create))
|
||||
{
|
||||
WritePomProperties(archive, "com.example", "regular", "1.0.0");
|
||||
CreateEmptyClass(archive, "com/example/Main.class");
|
||||
}
|
||||
|
||||
var result = await ShadedJarDetector.AnalyzeAsync(jarPath, cancellationToken);
|
||||
|
||||
Assert.False(result.IsShaded);
|
||||
Assert.Empty(result.Markers);
|
||||
Assert.Equal(ShadingConfidence.None, result.Confidence);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(jarPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WritePomProperties(ZipArchive archive, string groupId, string artifactId, string version)
|
||||
{
|
||||
var path = $"META-INF/maven/{groupId}/{artifactId}/pom.properties";
|
||||
var entry = archive.CreateEntry(path);
|
||||
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8);
|
||||
writer.WriteLine($"groupId={groupId}");
|
||||
writer.WriteLine($"artifactId={artifactId}");
|
||||
writer.WriteLine($"version={version}");
|
||||
}
|
||||
|
||||
private static void CreateEmptyClass(ZipArchive archive, string path)
|
||||
{
|
||||
var entry = archive.CreateEntry(path);
|
||||
using var stream = entry.Open();
|
||||
// Minimal class file header
|
||||
stream.Write([0xCA, 0xFE, 0xBA, 0xBE]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.BuildMetadata;
|
||||
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.Conflicts;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Lang.Java.Tests.Parsers;
|
||||
|
||||
public sealed class VersionConflictDetectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void DetectsMajorVersionConflicts()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("org.slf4j", "slf4j-api", "1.7.36", "pom.xml"),
|
||||
CreateDependency("org.slf4j", "slf4j-api", "2.0.7", "gradle.lockfile")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(dependencies);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Equal(1, result.TotalConflicts);
|
||||
Assert.Equal(ConflictSeverity.High, result.MaxSeverity);
|
||||
|
||||
var conflict = result.Conflicts[0];
|
||||
Assert.Equal("org.slf4j", conflict.GroupId);
|
||||
Assert.Equal("slf4j-api", conflict.ArtifactId);
|
||||
Assert.Contains("1.7.36", conflict.UniqueVersions);
|
||||
Assert.Contains("2.0.7", conflict.UniqueVersions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectsMinorVersionConflicts()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("com.google.guava", "guava", "31.0-jre", "pom.xml"),
|
||||
CreateDependency("com.google.guava", "guava", "31.1-jre", "gradle.lockfile")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(dependencies);
|
||||
|
||||
Assert.True(result.HasConflicts);
|
||||
Assert.Equal(ConflictSeverity.Medium, result.MaxSeverity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IgnoresIdenticalVersions()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("org.slf4j", "slf4j-api", "1.7.36", "pom.xml"),
|
||||
CreateDependency("org.slf4j", "slf4j-api", "1.7.36", "gradle.lockfile")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(dependencies);
|
||||
|
||||
Assert.False(result.HasConflicts);
|
||||
Assert.Equal(0, result.TotalConflicts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReturnsEmptyForNoDependencies()
|
||||
{
|
||||
var result = VersionConflictDetector.Analyze(Array.Empty<JavaDependencyDeclaration>());
|
||||
|
||||
Assert.False(result.HasConflicts);
|
||||
Assert.Equal(VersionConflictAnalysis.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetConflictReturnsNullForNonConflicting()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("org.slf4j", "slf4j-api", "1.7.36", "pom.xml"),
|
||||
CreateDependency("com.google.guava", "guava", "31.1-jre", "pom.xml")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(dependencies);
|
||||
|
||||
Assert.Null(result.GetConflict("org.slf4j", "slf4j-api"));
|
||||
Assert.Null(result.GetConflict("com.google.guava", "guava"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetConflictFindsConflictingArtifact()
|
||||
{
|
||||
var dependencies = new[]
|
||||
{
|
||||
CreateDependency("org.slf4j", "slf4j-api", "1.7.36", "pom.xml"),
|
||||
CreateDependency("org.slf4j", "slf4j-api", "2.0.7", "gradle.lockfile"),
|
||||
CreateDependency("com.google.guava", "guava", "31.1-jre", "pom.xml")
|
||||
};
|
||||
|
||||
var result = VersionConflictDetector.Analyze(dependencies);
|
||||
|
||||
var conflict = result.GetConflict("org.slf4j", "slf4j-api");
|
||||
Assert.NotNull(conflict);
|
||||
Assert.Equal(2, conflict.UniqueVersions.Count());
|
||||
|
||||
Assert.Null(result.GetConflict("com.google.guava", "guava"));
|
||||
}
|
||||
|
||||
private static JavaDependencyDeclaration CreateDependency(
|
||||
string groupId,
|
||||
string artifactId,
|
||||
string version,
|
||||
string source)
|
||||
{
|
||||
return new JavaDependencyDeclaration
|
||||
{
|
||||
GroupId = groupId,
|
||||
ArtifactId = artifactId,
|
||||
Version = version,
|
||||
Source = source,
|
||||
Locator = source
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user