feat: Add initial implementation of Vulnerability Resolver Jobs
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Created project for StellaOps.Scanner.Analyzers.Native.Tests with necessary dependencies. - Documented roles and guidelines in AGENTS.md for Scheduler module. - Implemented IResolverJobService interface and InMemoryResolverJobService for handling resolver jobs. - Added ResolverBacklogNotifier and ResolverBacklogService for monitoring job metrics. - Developed API endpoints for managing resolver jobs and retrieving metrics. - Defined models for resolver job requests and responses. - Integrated dependency injection for resolver job services. - Implemented ImpactIndexSnapshot for persisting impact index data. - Introduced SignalsScoringOptions for configurable scoring weights in reachability scoring. - Added unit tests for ReachabilityScoringService and RuntimeFactsIngestionService. - Created dotnet-filter.sh script to handle command-line arguments for dotnet. - Established nuget-prime project for managing package downloads.
This commit is contained in:
@@ -66,27 +66,30 @@ public sealed class JavaLanguageAnalyzer : ILanguageAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask ProcessArchiveAsync(
|
||||
JavaArchive archive,
|
||||
LanguageAnalyzerContext context,
|
||||
LanguageComponentWriter writer,
|
||||
JavaLockData lockData,
|
||||
HashSet<string> matchedLocks,
|
||||
bool hasLockEntries,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ManifestMetadata? manifestMetadata = null;
|
||||
if (archive.TryGetEntry("META-INF/MANIFEST.MF", out var manifestEntry))
|
||||
{
|
||||
manifestMetadata = await ParseManifestAsync(archive, manifestEntry, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (IsManifestEntry(entry.EffectivePath))
|
||||
{
|
||||
private async ValueTask ProcessArchiveAsync(
|
||||
JavaArchive archive,
|
||||
LanguageAnalyzerContext context,
|
||||
LanguageComponentWriter writer,
|
||||
JavaLockData lockData,
|
||||
HashSet<string> matchedLocks,
|
||||
bool hasLockEntries,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ManifestMetadata? manifestMetadata = null;
|
||||
if (archive.TryGetEntry("META-INF/MANIFEST.MF", out var manifestEntry))
|
||||
{
|
||||
manifestMetadata = await ParseManifestAsync(archive, manifestEntry, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var frameworkConfig = ScanFrameworkConfigs(archive, cancellationToken);
|
||||
var jniHints = ScanJniHints(archive, cancellationToken);
|
||||
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (IsManifestEntry(entry.EffectivePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -103,31 +106,44 @@ public sealed class JavaLanguageAnalyzer : ILanguageAnalyzer
|
||||
|
||||
var metadata = CreateInstalledMetadata(artifact, archive, manifestMetadata);
|
||||
|
||||
if (lockData.TryGet(artifact.GroupId, artifact.ArtifactId, artifact.Version, out var lockEntry))
|
||||
{
|
||||
matchedLocks.Add(lockEntry!.Key);
|
||||
AppendLockMetadata(metadata, lockEntry);
|
||||
}
|
||||
else if (hasLockEntries)
|
||||
{
|
||||
AddMetadata(metadata, "lockMissing", "true");
|
||||
}
|
||||
|
||||
var evidence = new List<LanguageComponentEvidence>
|
||||
{
|
||||
new(LanguageEvidenceKind.File, "pom.properties", BuildLocator(archive, entry.OriginalPath), null, artifact.PomSha256),
|
||||
};
|
||||
|
||||
if (manifestMetadata is not null)
|
||||
{
|
||||
evidence.Add(manifestMetadata.CreateEvidence(archive));
|
||||
}
|
||||
|
||||
var usedByEntrypoint = context.UsageHints.IsPathUsed(archive.AbsolutePath);
|
||||
|
||||
writer.AddFromPurl(
|
||||
analyzerId: Id,
|
||||
purl: artifact.Purl,
|
||||
if (lockData.TryGet(artifact.GroupId, artifact.ArtifactId, artifact.Version, out var lockEntry))
|
||||
{
|
||||
matchedLocks.Add(lockEntry!.Key);
|
||||
AppendLockMetadata(metadata, lockEntry);
|
||||
}
|
||||
else if (hasLockEntries)
|
||||
{
|
||||
AddMetadata(metadata, "lockMissing", "true");
|
||||
}
|
||||
|
||||
foreach (var hint in frameworkConfig.Metadata)
|
||||
{
|
||||
AddMetadata(metadata, hint.Key, hint.Value);
|
||||
}
|
||||
|
||||
foreach (var hint in jniHints.Metadata)
|
||||
{
|
||||
AddMetadata(metadata, hint.Key, hint.Value);
|
||||
}
|
||||
|
||||
var evidence = new List<LanguageComponentEvidence>
|
||||
{
|
||||
new(LanguageEvidenceKind.File, "pom.properties", BuildLocator(archive, entry.OriginalPath), null, artifact.PomSha256),
|
||||
};
|
||||
|
||||
if (manifestMetadata is not null)
|
||||
{
|
||||
evidence.Add(manifestMetadata.CreateEvidence(archive));
|
||||
}
|
||||
|
||||
evidence.AddRange(frameworkConfig.Evidence);
|
||||
evidence.AddRange(jniHints.Evidence);
|
||||
|
||||
var usedByEntrypoint = context.UsageHints.IsPathUsed(archive.AbsolutePath);
|
||||
|
||||
writer.AddFromPurl(
|
||||
analyzerId: Id,
|
||||
purl: artifact.Purl,
|
||||
name: artifact.ArtifactId,
|
||||
version: artifact.Version,
|
||||
type: "maven",
|
||||
@@ -150,24 +166,322 @@ public sealed class JavaLanguageAnalyzer : ILanguageAnalyzer
|
||||
return string.Concat(relativeArchive, "!", normalizedEntry);
|
||||
}
|
||||
|
||||
private static string NormalizeEntry(string entryPath)
|
||||
=> entryPath.Replace('\\', '/');
|
||||
|
||||
private static string NormalizeArchivePath(string relativePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(relativePath) || string.Equals(relativePath, ".", StringComparison.Ordinal))
|
||||
{
|
||||
return ".";
|
||||
}
|
||||
|
||||
return relativePath.Replace('\\', '/');
|
||||
}
|
||||
|
||||
private static bool IsPomPropertiesEntry(string entryName)
|
||||
=> entryName.StartsWith("META-INF/maven/", StringComparison.OrdinalIgnoreCase)
|
||||
&& entryName.EndsWith("/pom.properties", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsManifestEntry(string entryName)
|
||||
private static string NormalizeEntry(string entryPath)
|
||||
=> entryPath.Replace('\\', '/');
|
||||
|
||||
private static string NormalizeArchivePath(string relativePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(relativePath) || string.Equals(relativePath, ".", StringComparison.Ordinal))
|
||||
{
|
||||
return ".";
|
||||
}
|
||||
|
||||
return relativePath.Replace('\\', '/');
|
||||
}
|
||||
|
||||
private static FrameworkConfigSummary ScanFrameworkConfigs(JavaArchive archive, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(archive);
|
||||
|
||||
var metadata = new Dictionary<string, SortedSet<string>>(StringComparer.Ordinal);
|
||||
var evidence = new List<LanguageComponentEvidence>();
|
||||
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var path = entry.EffectivePath;
|
||||
|
||||
if (IsSpringFactories(path))
|
||||
{
|
||||
AddConfigHint(metadata, evidence, "config.spring.factories", archive, entry);
|
||||
}
|
||||
else if (IsSpringImports(path))
|
||||
{
|
||||
AddConfigHint(metadata, evidence, "config.spring.imports", archive, entry);
|
||||
}
|
||||
else if (IsSpringApplicationConfig(path))
|
||||
{
|
||||
AddConfigHint(metadata, evidence, "config.spring.properties", archive, entry);
|
||||
}
|
||||
else if (IsSpringBootstrapConfig(path))
|
||||
{
|
||||
AddConfigHint(metadata, evidence, "config.spring.bootstrap", archive, entry);
|
||||
}
|
||||
|
||||
if (IsWebXml(path))
|
||||
{
|
||||
AddConfigHint(metadata, evidence, "config.web.xml", archive, entry);
|
||||
}
|
||||
|
||||
if (IsWebFragment(path))
|
||||
{
|
||||
AddConfigHint(metadata, evidence, "config.web.fragment", archive, entry);
|
||||
}
|
||||
|
||||
if (IsJpaConfig(path))
|
||||
{
|
||||
AddConfigHint(metadata, evidence, "config.jpa", archive, entry);
|
||||
}
|
||||
|
||||
if (IsCdiConfig(path))
|
||||
{
|
||||
AddConfigHint(metadata, evidence, "config.cdi", archive, entry);
|
||||
}
|
||||
|
||||
if (IsJaxbConfig(path))
|
||||
{
|
||||
AddConfigHint(metadata, evidence, "config.jaxb", archive, entry);
|
||||
}
|
||||
|
||||
if (IsJaxRsConfig(path))
|
||||
{
|
||||
AddConfigHint(metadata, evidence, "config.jaxrs", archive, entry);
|
||||
}
|
||||
|
||||
if (IsLoggingConfig(path))
|
||||
{
|
||||
AddConfigHint(metadata, evidence, "config.logging", archive, entry);
|
||||
}
|
||||
|
||||
if (IsGraalConfig(path))
|
||||
{
|
||||
AddConfigHint(metadata, evidence, "config.graal", archive, entry);
|
||||
}
|
||||
}
|
||||
|
||||
var flattened = metadata.ToDictionary(
|
||||
static pair => pair.Key,
|
||||
static pair => string.Join(",", pair.Value),
|
||||
StringComparer.Ordinal);
|
||||
|
||||
return new FrameworkConfigSummary(flattened, evidence);
|
||||
}
|
||||
|
||||
private static JniHintSummary ScanJniHints(JavaArchive archive, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(archive);
|
||||
|
||||
var metadata = new Dictionary<string, SortedSet<string>>(StringComparer.Ordinal);
|
||||
var evidence = new List<LanguageComponentEvidence>();
|
||||
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var path = entry.EffectivePath;
|
||||
var locator = BuildLocator(archive, entry.OriginalPath);
|
||||
|
||||
if (IsNativeLibrary(path))
|
||||
{
|
||||
AddHint(metadata, evidence, "jni.nativeLibs", Path.GetFileName(path), locator, "jni-native");
|
||||
}
|
||||
|
||||
if (IsGraalJniConfig(path))
|
||||
{
|
||||
AddHint(metadata, evidence, "jni.graalConfig", locator, locator, "jni-graal");
|
||||
}
|
||||
|
||||
if (IsClassFile(path) && entry.Length is > 0 and < 1_000_000)
|
||||
{
|
||||
TryScanClassForLoadCalls(archive, entry, locator, metadata, evidence, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
var flattened = metadata.ToDictionary(
|
||||
static pair => pair.Key,
|
||||
static pair => string.Join(",", pair.Value),
|
||||
StringComparer.Ordinal);
|
||||
|
||||
return new JniHintSummary(flattened, evidence);
|
||||
}
|
||||
|
||||
private static void TryScanClassForLoadCalls(
|
||||
JavaArchive archive,
|
||||
JavaArchiveEntry entry,
|
||||
string locator,
|
||||
IDictionary<string, SortedSet<string>> metadata,
|
||||
ICollection<LanguageComponentEvidence> evidence,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = archive.OpenEntry(entry);
|
||||
using var buffer = new MemoryStream();
|
||||
stream.CopyTo(buffer);
|
||||
var bytes = buffer.ToArray();
|
||||
|
||||
if (ContainsAscii(bytes, "System.loadLibrary"))
|
||||
{
|
||||
AddHint(metadata, evidence, "jni.loadCalls", locator, locator, "jni-load");
|
||||
}
|
||||
else if (ContainsAscii(bytes, "System.load"))
|
||||
{
|
||||
AddHint(metadata, evidence, "jni.loadCalls", locator, locator, "jni-load");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best effort; skip unreadable class entries
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ContainsAscii(byte[] buffer, string ascii)
|
||||
{
|
||||
if (buffer.Length == 0 || string.IsNullOrEmpty(ascii))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var needle = Encoding.ASCII.GetBytes(ascii);
|
||||
return SpanSearch(buffer, needle) >= 0;
|
||||
}
|
||||
|
||||
private static int SpanSearch(byte[] haystack, byte[] needle)
|
||||
{
|
||||
if (needle.Length == 0 || haystack.Length < needle.Length)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var lastStart = haystack.Length - needle.Length;
|
||||
for (var i = 0; i <= lastStart; i++)
|
||||
{
|
||||
var matched = true;
|
||||
for (var j = 0; j < needle.Length; j++)
|
||||
{
|
||||
if (haystack[i + j] != needle[j])
|
||||
{
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matched)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static void AddHint(
|
||||
IDictionary<string, SortedSet<string>> metadata,
|
||||
ICollection<LanguageComponentEvidence> evidence,
|
||||
string key,
|
||||
string value,
|
||||
string locator,
|
||||
string evidenceSource)
|
||||
{
|
||||
if (!metadata.TryGetValue(key, out var items))
|
||||
{
|
||||
items = new SortedSet<string>(StringComparer.Ordinal);
|
||||
metadata[key] = items;
|
||||
}
|
||||
|
||||
items.Add(value);
|
||||
|
||||
evidence.Add(new LanguageComponentEvidence(
|
||||
LanguageEvidenceKind.File,
|
||||
evidenceSource,
|
||||
locator,
|
||||
value: null,
|
||||
sha256: null));
|
||||
}
|
||||
|
||||
private static void AddConfigHint(
|
||||
IDictionary<string, SortedSet<string>> metadata,
|
||||
ICollection<LanguageComponentEvidence> evidence,
|
||||
string key,
|
||||
JavaArchive archive,
|
||||
JavaArchiveEntry entry)
|
||||
{
|
||||
if (!metadata.TryGetValue(key, out var locators))
|
||||
{
|
||||
locators = new SortedSet<string>(StringComparer.Ordinal);
|
||||
metadata[key] = locators;
|
||||
}
|
||||
|
||||
var locator = BuildLocator(archive, entry.OriginalPath);
|
||||
locators.Add(locator);
|
||||
|
||||
evidence.Add(new LanguageComponentEvidence(
|
||||
LanguageEvidenceKind.File,
|
||||
"framework-config",
|
||||
locator,
|
||||
value: null,
|
||||
sha256: null));
|
||||
}
|
||||
|
||||
private static bool IsSpringFactories(string path)
|
||||
=> string.Equals(path, "META-INF/spring.factories", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsSpringImports(string path)
|
||||
=> path.StartsWith("META-INF/spring/", StringComparison.OrdinalIgnoreCase)
|
||||
&& path.EndsWith(".imports", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsSpringApplicationConfig(string path)
|
||||
=> path.EndsWith("application.properties", StringComparison.OrdinalIgnoreCase)
|
||||
|| path.EndsWith("application.yml", StringComparison.OrdinalIgnoreCase)
|
||||
|| path.EndsWith("application.yaml", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsSpringBootstrapConfig(string path)
|
||||
=> path.EndsWith("bootstrap.properties", StringComparison.OrdinalIgnoreCase)
|
||||
|| path.EndsWith("bootstrap.yml", StringComparison.OrdinalIgnoreCase)
|
||||
|| path.EndsWith("bootstrap.yaml", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsWebXml(string path)
|
||||
=> path.EndsWith("WEB-INF/web.xml", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsWebFragment(string path)
|
||||
=> path.EndsWith("META-INF/web-fragment.xml", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsJpaConfig(string path)
|
||||
=> path.EndsWith("META-INF/persistence.xml", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsCdiConfig(string path)
|
||||
=> path.EndsWith("META-INF/beans.xml", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsJaxbConfig(string path)
|
||||
=> path.EndsWith("META-INF/jaxb.index", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsJaxRsConfig(string path)
|
||||
=> path.StartsWith("META-INF/services/", StringComparison.OrdinalIgnoreCase)
|
||||
&& path.Contains("ws.rs", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsLoggingConfig(string path)
|
||||
=> path.EndsWith("log4j2.xml", StringComparison.OrdinalIgnoreCase)
|
||||
|| path.EndsWith("logback.xml", StringComparison.OrdinalIgnoreCase)
|
||||
|| path.EndsWith("logging.properties", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsGraalConfig(string path)
|
||||
=> path.StartsWith("META-INF/native-image/", StringComparison.OrdinalIgnoreCase)
|
||||
&& (path.EndsWith("reflect-config.json", StringComparison.OrdinalIgnoreCase)
|
||||
|| path.EndsWith("resource-config.json", StringComparison.OrdinalIgnoreCase)
|
||||
|| path.EndsWith("proxy-config.json", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static bool IsGraalJniConfig(string path)
|
||||
=> path.StartsWith("META-INF/native-image/", StringComparison.OrdinalIgnoreCase)
|
||||
&& path.EndsWith("jni-config.json", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsNativeLibrary(string path)
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
return extension.Equals(".so", StringComparison.OrdinalIgnoreCase)
|
||||
|| extension.Equals(".dll", StringComparison.OrdinalIgnoreCase)
|
||||
|| extension.Equals(".dylib", StringComparison.OrdinalIgnoreCase)
|
||||
|| extension.Equals(".jnilib", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsClassFile(string path)
|
||||
=> path.EndsWith(".class", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsPomPropertiesEntry(string entryName)
|
||||
=> entryName.StartsWith("META-INF/maven/", StringComparison.OrdinalIgnoreCase)
|
||||
&& entryName.EndsWith("/pom.properties", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsManifestEntry(string entryName)
|
||||
=> string.Equals(entryName, "META-INF/MANIFEST.MF", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static void AppendLockMetadata(ICollection<KeyValuePair<string, string?>> metadata, JavaLockEntry entry)
|
||||
@@ -283,9 +597,16 @@ public sealed class JavaLanguageAnalyzer : ILanguageAnalyzer
|
||||
else if (key.Equals("Implementation-Vendor", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
vendor ??= value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record FrameworkConfigSummary(
|
||||
IReadOnlyDictionary<string, string> Metadata,
|
||||
IReadOnlyCollection<LanguageComponentEvidence> Evidence);
|
||||
|
||||
internal sealed record JniHintSummary(
|
||||
IReadOnlyDictionary<string, string> Metadata,
|
||||
IReadOnlyCollection<LanguageComponentEvidence> Evidence);
|
||||
if (title is null && version is null && vendor is null)
|
||||
{
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user