This commit is contained in:
StellaOps Bot
2025-12-09 00:20:52 +02:00
parent 3d01bf9edc
commit bc0762e97d
261 changed files with 14033 additions and 4427 deletions

View File

@@ -0,0 +1,222 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StellaOps.Scanner.Core.Contracts;
namespace StellaOps.Scanner.Analyzers.OS.Helpers;
/// <summary>
/// Enriches OS package file evidence with layer attribution and stable hashes/sizes.
/// </summary>
public sealed class OsFileEvidenceFactory
{
private readonly string _rootPath;
private readonly ImmutableArray<(string? Digest, string Path)> _layerDirectories;
private readonly string? _defaultLayerDigest;
private OsFileEvidenceFactory(string rootPath, ImmutableArray<(string? Digest, string Path)> layerDirectories, string? defaultLayerDigest)
{
_rootPath = rootPath;
_layerDirectories = layerDirectories;
_defaultLayerDigest = NormalizeDigest(defaultLayerDigest);
}
public static OsFileEvidenceFactory Create(string rootPath, IReadOnlyDictionary<string, string> metadata)
{
ArgumentException.ThrowIfNullOrWhiteSpace(rootPath);
ArgumentNullException.ThrowIfNull(metadata);
var layerDirectories = ParseLayerEntries(metadata, ScanMetadataKeys.LayerDirectories);
metadata.TryGetValue(ScanMetadataKeys.CurrentLayerDigest, out var defaultLayerDigest);
return new OsFileEvidenceFactory(rootPath, layerDirectories, defaultLayerDigest);
}
public OSPackageFileEvidence Create(string path, bool isConfigFile, IDictionary<string, string>? digests = null)
{
var digestMap = digests is null
? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, string>(digests, StringComparer.OrdinalIgnoreCase);
var layerDigest = ResolveLayerDigest(path) ?? _defaultLayerDigest;
string? sha256 = null;
long? size = null;
var fullPath = CombineWithRoot(path);
if (fullPath is not null && File.Exists(fullPath))
{
try
{
var info = new FileInfo(fullPath);
size = info.Length;
if (info.Length > 0 && !digestMap.TryGetValue("sha256", out sha256))
{
sha256 = ComputeSha256(fullPath);
digestMap["sha256"] = sha256;
}
}
catch (IOException)
{
// Best-effort: ignore IO failures and fall back to existing metadata
}
catch (UnauthorizedAccessException)
{
// Ignore permission issues
}
}
return new OSPackageFileEvidence(
path,
layerDigest: layerDigest,
sha256: sha256,
sizeBytes: size,
isConfigFile: isConfigFile,
digests: digestMap);
}
private string? CombineWithRoot(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return null;
}
var trimmed = path.TrimStart('/', '\\');
var combined = Path.Combine(_rootPath, trimmed);
return Path.GetFullPath(combined);
}
private string? ResolveLayerDigest(string path)
{
if (_layerDirectories.IsDefaultOrEmpty)
{
return null;
}
var relative = path.TrimStart('/', '\\');
foreach (var (digest, layerPath) in _layerDirectories)
{
string? layerDigest = NormalizeDigest(digest);
string candidate;
try
{
candidate = Path.GetFullPath(Path.Combine(layerPath, relative));
}
catch
{
continue;
}
if (!File.Exists(candidate))
{
continue;
}
return layerDigest ?? ComputeDirectoryDigest(layerPath);
}
return null;
}
private static string ComputeSha256(string path)
{
using var stream = File.OpenRead(path);
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
}
private static string ComputeDirectoryDigest(string path)
{
var payload = Encoding.UTF8.GetBytes(Path.GetFullPath(path));
var hash = SHA256.HashData(payload);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
private static ImmutableArray<(string? Digest, string Path)> ParseLayerEntries(
IReadOnlyDictionary<string, string> metadata,
string metadataKey)
{
if (string.IsNullOrWhiteSpace(metadataKey) ||
!metadata.TryGetValue(metadataKey, out var rawValue) ||
string.IsNullOrWhiteSpace(rawValue))
{
return ImmutableArray<(string?, string)>.Empty;
}
rawValue = rawValue.Trim();
IEnumerable<string> tokens;
if (rawValue.StartsWith("[", StringComparison.Ordinal))
{
try
{
var parsed = JsonSerializer.Deserialize<string[]>(rawValue);
tokens = parsed ?? Array.Empty<string>();
}
catch
{
tokens = SplitLayerString(rawValue);
}
}
else
{
tokens = SplitLayerString(rawValue);
}
var builder = ImmutableArray.CreateBuilder<(string?, string)>();
foreach (var token in tokens)
{
var entry = token.Trim();
if (entry.Length == 0)
{
continue;
}
var separator = entry.IndexOf('=');
string? digest = null;
var pathPart = entry;
if (separator >= 0)
{
digest = entry[..separator].Trim();
pathPart = entry[(separator + 1)..].Trim();
}
if (pathPart.Length == 0)
{
continue;
}
builder.Add((NormalizeDigest(digest), pathPart));
}
return builder.ToImmutable();
}
private static IEnumerable<string> SplitLayerString(string raw)
=> raw.Split(new[] { '\n', '\r', ';' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
private static string? NormalizeDigest(string? digest)
{
if (string.IsNullOrWhiteSpace(digest))
{
return null;
}
var trimmed = digest.Trim();
if (!trimmed.Contains(':', StringComparison.Ordinal))
{
return trimmed.ToLowerInvariant();
}
var parts = trimmed.Split(':', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return parts.Length == 2
? $"{parts[0].ToLowerInvariant()}:{parts[1].ToLowerInvariant()}"
: trimmed.ToLowerInvariant();
}
}

View File

@@ -4,6 +4,7 @@ using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Globalization;
using StellaOps.Scanner.Core.Contracts;
namespace StellaOps.Scanner.Analyzers.OS.Mapping;
@@ -144,6 +145,11 @@ public static class OsComponentMapper
properties[$"digest.{digest.Key}.{NormalizePathKey(file.Path)}"] = digest.Value.Trim();
}
if (file.SizeBytes.HasValue)
{
properties[$"size.{NormalizePathKey(file.Path)}"] = file.SizeBytes.Value.ToString(CultureInfo.InvariantCulture);
}
}
IReadOnlyList<string>? licenses = null;