- Implemented tests for RancherHubConnector to validate fetching documents, handling errors, and managing state. - Added tests for CsafExporter to ensure deterministic serialization of CSAF documents. - Created tests for CycloneDX exporters and reconciler to verify correct handling of VEX claims and output structure. - Developed OpenVEX exporter tests to confirm the generation of canonical OpenVEX documents and statement merging logic. - Introduced Rust file caching and license scanning functionality, including a cache key structure and hash computation. - Added sample Cargo.toml and LICENSE files for testing Rust license scanning functionality.
187 lines
5.7 KiB
C#
187 lines
5.7 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Text.Json;
|
|
|
|
namespace StellaOps.Scanner.Analyzers.Lang.Rust.Internal;
|
|
|
|
internal static class RustFingerprintScanner
|
|
{
|
|
private static readonly EnumerationOptions Enumeration = new()
|
|
{
|
|
MatchCasing = MatchCasing.CaseSensitive,
|
|
IgnoreInaccessible = true,
|
|
RecurseSubdirectories = true,
|
|
AttributesToSkip = FileAttributes.Device | FileAttributes.ReparsePoint,
|
|
};
|
|
|
|
private static readonly string FingerprintSegment = $"{Path.DirectorySeparatorChar}.fingerprint{Path.DirectorySeparatorChar}";
|
|
private static readonly ConcurrentDictionary<RustFileCacheKey, RustFingerprintRecord?> Cache = new();
|
|
|
|
public static IReadOnlyList<RustFingerprintRecord> Scan(string rootPath, CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(rootPath))
|
|
{
|
|
throw new ArgumentException("Root path is required", nameof(rootPath));
|
|
}
|
|
|
|
var results = new List<RustFingerprintRecord>();
|
|
foreach (var path in Directory.EnumerateFiles(rootPath, "*.json", Enumeration))
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
if (!path.Contains(FingerprintSegment, StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!RustFileCacheKey.TryCreate(path, out var key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var record = Cache.GetOrAdd(
|
|
key,
|
|
static (_, state) => ParseFingerprint(state),
|
|
path);
|
|
|
|
if (record is not null)
|
|
{
|
|
results.Add(record);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private static RustFingerprintRecord? ParseFingerprint(string path)
|
|
{
|
|
try
|
|
{
|
|
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
using var document = JsonDocument.Parse(stream);
|
|
var root = document.RootElement;
|
|
|
|
var pkgId = TryGetString(root, "pkgid")
|
|
?? TryGetString(root, "package_id")
|
|
?? TryGetString(root, "packageId");
|
|
|
|
var (name, version, source) = ParseIdentity(pkgId, path);
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var profile = TryGetString(root, "profile");
|
|
var targetKind = TryGetKind(root);
|
|
|
|
return new RustFingerprintRecord(
|
|
Name: name!,
|
|
Version: version,
|
|
Source: source,
|
|
TargetKind: targetKind,
|
|
Profile: profile,
|
|
AbsolutePath: path);
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return null;
|
|
}
|
|
catch (IOException)
|
|
{
|
|
return null;
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static (string? Name, string? Version, string? Source) ParseIdentity(string? pkgId, string filePath)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(pkgId))
|
|
{
|
|
var span = pkgId.AsSpan().Trim();
|
|
var firstSpace = span.IndexOf(' ');
|
|
if (firstSpace > 0 && firstSpace < span.Length - 1)
|
|
{
|
|
var name = span[..firstSpace].ToString();
|
|
var remaining = span[(firstSpace + 1)..].Trim();
|
|
|
|
var secondSpace = remaining.IndexOf(' ');
|
|
if (secondSpace < 0)
|
|
{
|
|
return (name, remaining.ToString(), null);
|
|
}
|
|
|
|
var version = remaining[..secondSpace].ToString();
|
|
var potentialSource = remaining[(secondSpace + 1)..].Trim();
|
|
|
|
if (potentialSource.Length > 1 && potentialSource[0] == '(' && potentialSource[^1] == ')')
|
|
{
|
|
potentialSource = potentialSource[1..^1].Trim();
|
|
}
|
|
|
|
var source = potentialSource.Length == 0 ? null : potentialSource.ToString();
|
|
return (name, version, source);
|
|
}
|
|
}
|
|
|
|
var directory = Path.GetDirectoryName(filePath);
|
|
if (string.IsNullOrEmpty(directory))
|
|
{
|
|
return (null, null, null);
|
|
}
|
|
|
|
var crateDirectory = Path.GetFileName(directory);
|
|
if (string.IsNullOrWhiteSpace(crateDirectory))
|
|
{
|
|
return (null, null, null);
|
|
}
|
|
|
|
var dashIndex = crateDirectory.LastIndexOf('-');
|
|
if (dashIndex <= 0)
|
|
{
|
|
return (crateDirectory, null, null);
|
|
}
|
|
|
|
var maybeName = crateDirectory[..dashIndex];
|
|
return (maybeName, null, null);
|
|
}
|
|
|
|
private static string? TryGetKind(JsonElement root)
|
|
{
|
|
if (root.TryGetProperty("target_kind", out var array) && array.ValueKind == JsonValueKind.Array && array.GetArrayLength() > 0)
|
|
{
|
|
var first = array[0];
|
|
if (first.ValueKind == JsonValueKind.String)
|
|
{
|
|
return first.GetString();
|
|
}
|
|
}
|
|
|
|
if (root.TryGetProperty("target", out var target) && target.ValueKind == JsonValueKind.String)
|
|
{
|
|
return target.GetString();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string? TryGetString(JsonElement element, string propertyName)
|
|
{
|
|
if (element.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String)
|
|
{
|
|
return value.GetString();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
internal sealed record RustFingerprintRecord(
|
|
string Name,
|
|
string? Version,
|
|
string? Source,
|
|
string? TargetKind,
|
|
string? Profile,
|
|
string AbsolutePath);
|