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

- 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:
StellaOps Bot
2025-12-06 22:25:30 +02:00
parent dd0067ea0b
commit 4042fc2184
110 changed files with 20084 additions and 639 deletions

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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>());
}