feat: Implement Runtime Facts ingestion service and NDJSON reader
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user