feat: Implement Runtime Facts ingestion service and NDJSON reader
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added RuntimeFactsNdjsonReader for reading NDJSON formatted runtime facts.
- Introduced IRuntimeFactsIngestionService interface and its implementation.
- Enhanced Program.cs to register new services and endpoints for runtime facts.
- Updated CallgraphIngestionService to include CAS URI in stored artifacts.
- Created RuntimeFactsValidationException for validation errors during ingestion.
- Added tests for RuntimeFactsIngestionService and RuntimeFactsNdjsonReader.
- Implemented SignalsSealedModeMonitor for compliance checks in sealed mode.
- Updated project dependencies for testing utilities.
This commit is contained in:
master
2025-11-10 07:56:15 +02:00
parent 9df52d84aa
commit 69c59defdc
132 changed files with 19718 additions and 9334 deletions

View File

@@ -0,0 +1,184 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal;
internal static class JavaLockFileCollector
{
private static readonly string[] GradleLockPatterns = { "gradle.lockfile" };
public static async Task<JavaLockData> LoadAsync(LanguageAnalyzerContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
var entries = new Dictionary<string, JavaLockEntry>(StringComparer.OrdinalIgnoreCase);
var root = context.RootPath;
foreach (var pattern in GradleLockPatterns)
{
var lockPath = Path.Combine(root, pattern);
if (File.Exists(lockPath))
{
await ParseGradleLockFileAsync(context, lockPath, entries, cancellationToken).ConfigureAwait(false);
}
}
var dependencyLocksDir = Path.Combine(root, "gradle", "dependency-locks");
if (Directory.Exists(dependencyLocksDir))
{
foreach (var file in Directory.EnumerateFiles(dependencyLocksDir, "*.lockfile", SearchOption.AllDirectories))
{
await ParseGradleLockFileAsync(context, file, entries, cancellationToken).ConfigureAwait(false);
}
}
foreach (var pomPath in Directory.EnumerateFiles(root, "pom.xml", SearchOption.AllDirectories))
{
await ParsePomAsync(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)
{
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream);
string? line;
while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) is not null)
{
cancellationToken.ThrowIfCancellationRequested();
line = line.Trim();
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#", StringComparison.Ordinal))
{
continue;
}
var parts = line.Split('=', 2, StringSplitOptions.TrimEntries);
var coordinates = parts[0];
var configuration = parts.Length > 1 ? parts[1] : null;
var coordinateParts = coordinates.Split(':');
if (coordinateParts.Length < 3)
{
continue;
}
var groupId = coordinateParts[0];
var artifactId = coordinateParts[1];
var version = coordinateParts[^1];
if (string.IsNullOrWhiteSpace(groupId) || string.IsNullOrWhiteSpace(artifactId) || string.IsNullOrWhiteSpace(version))
{
continue;
}
var entry = new JavaLockEntry(
groupId.Trim(),
artifactId.Trim(),
version.Trim(),
Path.GetFileName(path),
NormalizeLocator(context, path),
configuration,
null,
null);
entries[entry.Key] = entry;
}
}
private static async Task ParsePomAsync(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);
var dependencies = document
.Descendants()
.Where(static element => element.Name.LocalName.Equals("dependency", StringComparison.OrdinalIgnoreCase));
foreach (var dependency in dependencies)
{
cancellationToken.ThrowIfCancellationRequested();
var groupId = dependency.Elements().FirstOrDefault(static e => e.Name.LocalName.Equals("groupId", StringComparison.OrdinalIgnoreCase))?.Value?.Trim();
var artifactId = dependency.Elements().FirstOrDefault(static e => e.Name.LocalName.Equals("artifactId", StringComparison.OrdinalIgnoreCase))?.Value?.Trim();
var version = dependency.Elements().FirstOrDefault(static e => e.Name.LocalName.Equals("version", StringComparison.OrdinalIgnoreCase))?.Value?.Trim();
var scope = dependency.Elements().FirstOrDefault(static e => e.Name.LocalName.Equals("scope", StringComparison.OrdinalIgnoreCase))?.Value?.Trim();
var repository = dependency.Elements().FirstOrDefault(static e => e.Name.LocalName.Equals("repository", StringComparison.OrdinalIgnoreCase))?.Value?.Trim();
if (string.IsNullOrWhiteSpace(groupId) ||
string.IsNullOrWhiteSpace(artifactId) ||
string.IsNullOrWhiteSpace(version) ||
version.Contains("${", StringComparison.Ordinal))
{
continue;
}
var entry = new JavaLockEntry(
groupId,
artifactId,
version,
"pom.xml",
NormalizeLocator(context, path),
scope,
repository,
null);
entries[entry.Key] = entry;
}
}
private static string NormalizeLocator(LanguageAnalyzerContext context, string path)
=> context.GetRelativePath(path).Replace('\\', '/');
}
internal sealed record JavaLockEntry(
string GroupId,
string ArtifactId,
string Version,
string Source,
string Locator,
string? Configuration,
string? Repository,
string? ResolvedUrl)
{
public string Key => BuildKey(GroupId, ArtifactId, Version);
private static string BuildKey(string groupId, string artifactId, string version)
=> $"{groupId}:{artifactId}:{version}".ToLowerInvariant();
}
internal sealed class JavaLockData
{
public static readonly JavaLockData Empty = new(new Dictionary<string, JavaLockEntry>(StringComparer.OrdinalIgnoreCase));
private readonly Dictionary<string, JavaLockEntry> _entriesByKey;
private readonly IReadOnlyList<JavaLockEntry> _orderedEntries;
public JavaLockData(Dictionary<string, JavaLockEntry> entries)
{
_entriesByKey = entries ?? throw new ArgumentNullException(nameof(entries));
_orderedEntries = entries.Count == 0
? Array.Empty<JavaLockEntry>()
: entries.Values
.OrderBy(static entry => entry.GroupId, StringComparer.OrdinalIgnoreCase)
.ThenBy(static entry => entry.ArtifactId, StringComparer.OrdinalIgnoreCase)
.ThenBy(static entry => entry.Version, StringComparer.OrdinalIgnoreCase)
.ThenBy(static entry => entry.Source, StringComparer.OrdinalIgnoreCase)
.ThenBy(static entry => entry.Locator, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
public IReadOnlyList<JavaLockEntry> Entries => _orderedEntries;
public bool HasEntries => _entriesByKey.Count > 0;
public bool TryGet(string groupId, string artifactId, string version, out JavaLockEntry? entry)
{
var key = $"{groupId}:{artifactId}:{version}".ToLowerInvariant();
return _entriesByKey.TryGetValue(key, out entry);
}
}