Files
git.stella-ops.org/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.OS.Rpm/RpmDatabaseReader.cs
StellaOps Bot 11597679ed
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
console-runner-image / build-runner-image (push) Has been cancelled
wine-csp-build / Build Wine CSP Image (push) Has been cancelled
wine-csp-build / Integration Tests (push) Has been cancelled
wine-csp-build / Security Scan (push) Has been cancelled
wine-csp-build / Generate SBOM (push) Has been cancelled
wine-csp-build / Publish Image (push) Has been cancelled
wine-csp-build / Air-Gap Bundle (push) Has been cancelled
wine-csp-build / Test Summary (push) Has been cancelled
feat: Implement BerkeleyDB reader for RPM databases
- Added BerkeleyDbReader class to read and extract RPM header blobs from BerkeleyDB hash databases.
- Implemented methods to detect BerkeleyDB format and extract values, including handling of page sizes and magic numbers.
- Added tests for BerkeleyDbReader to ensure correct functionality and header extraction.

feat: Add Yarn PnP data tests

- Created YarnPnpDataTests to validate package resolution and data loading from Yarn PnP cache.
- Implemented tests for resolved keys, package presence, and loading from cache structure.

test: Add egg-info package fixtures for Python tests

- Created egg-info package fixtures for testing Python analyzers.
- Included PKG-INFO, entry_points.txt, and installed-files.txt for comprehensive coverage.

test: Enhance RPM database reader tests

- Added tests for RpmDatabaseReader to validate fallback to legacy packages when SQLite is missing.
- Implemented helper methods to create legacy package files and RPM headers for testing.

test: Implement dual signing tests

- Added DualSignTests to validate secondary signature addition when configured.
- Created stub implementations for crypto providers and key resolvers to facilitate testing.

chore: Update CI script for Playwright Chromium installation

- Modified ci-console-exports.sh to ensure deterministic Chromium binary installation for console exports tests.
- Added checks for Windows compatibility and environment variable setups for Playwright browsers.
2025-12-07 16:24:45 +02:00

353 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Buffers.Binary;
using System.IO;
using System.Threading;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.Analyzers.OS.Rpm.Internal;
namespace StellaOps.Scanner.Analyzers.OS.Rpm;
internal sealed class RpmDatabaseReader : IRpmDatabaseReader
{
private readonly ILogger _logger;
private readonly RpmHeaderParser _parser = new();
public RpmDatabaseReader(ILogger logger)
{
_logger = logger;
}
public IReadOnlyList<RpmHeader> ReadHeaders(string rootPath, CancellationToken cancellationToken)
{
var sqlitePath = ResolveSqlitePath(rootPath);
if (sqlitePath is null)
{
_logger.LogWarning("rpmdb.sqlite not found under root {RootPath}; attempting legacy rpmdb fallback.", rootPath);
return ReadLegacyHeaders(rootPath, cancellationToken);
}
var headers = new List<RpmHeader>();
try
{
var connectionString = new SqliteConnectionStringBuilder
{
DataSource = sqlitePath,
Mode = SqliteOpenMode.ReadOnly,
}.ToString();
using var connection = new SqliteConnection(connectionString);
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "SELECT * FROM Packages";
using var reader = command.ExecuteReader();
while (reader.Read())
{
cancellationToken.ThrowIfCancellationRequested();
var blob = ExtractHeaderBlob(reader);
if (blob is null)
{
continue;
}
try
{
headers.Add(_parser.Parse(blob));
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to parse RPM header record (pkgKey={PkgKey}).", TryGetPkgKey(reader));
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Unable to read rpmdb.sqlite at {Path}.", sqlitePath);
return ReadLegacyHeaders(rootPath, cancellationToken);
}
if (headers.Count == 0)
{
return ReadLegacyHeaders(rootPath, cancellationToken);
}
return headers;
}
private static string? ResolveSqlitePath(string rootPath)
{
var candidates = new[]
{
Path.Combine(rootPath, "var", "lib", "rpm", "rpmdb.sqlite"),
Path.Combine(rootPath, "usr", "lib", "sysimage", "rpm", "rpmdb.sqlite"),
};
foreach (var candidate in candidates)
{
if (File.Exists(candidate))
{
return candidate;
}
}
return null;
}
private IReadOnlyList<RpmHeader> ReadLegacyHeaders(string rootPath, CancellationToken cancellationToken)
{
var packagesPath = ResolveLegacyPackagesPath(rootPath);
if (packagesPath is null)
{
_logger.LogWarning("Legacy rpmdb Packages file not found under root {RootPath}; rpm analyzer will skip.", rootPath);
return Array.Empty<RpmHeader>();
}
byte[] data;
try
{
data = File.ReadAllBytes(packagesPath);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Unable to read legacy rpmdb Packages file at {Path}.", packagesPath);
return Array.Empty<RpmHeader>();
}
// Detect BerkeleyDB format and use appropriate extraction method
if (BerkeleyDbReader.IsBerkeleyDb(data))
{
_logger.LogDebug("Detected BerkeleyDB format for rpmdb at {Path}; using BDB extraction.", packagesPath);
return ReadBerkeleyDbHeaders(data, packagesPath, cancellationToken);
}
// Fall back to raw RPM header scanning for non-BDB files
return ReadRawRpmHeaders(data, packagesPath, cancellationToken);
}
private IReadOnlyList<RpmHeader> ReadBerkeleyDbHeaders(byte[] data, string packagesPath, CancellationToken cancellationToken)
{
var results = new List<RpmHeader>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// Try page-aware extraction first
var headerBlobs = BerkeleyDbReader.ExtractValues(data);
if (headerBlobs.Count == 0)
{
// Fall back to overflow-aware extraction for fragmented data
headerBlobs = BerkeleyDbReader.ExtractValuesWithOverflow(data);
}
foreach (var blob in headerBlobs)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var header = _parser.Parse(blob);
var key = $"{header.Name}::{header.Version}::{header.Release}::{header.Architecture}";
if (seen.Add(key))
{
results.Add(header);
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to parse RPM header blob from BerkeleyDB.");
}
}
if (results.Count == 0)
{
_logger.LogWarning("No RPM headers parsed from BerkeleyDB rpmdb at {Path}.", packagesPath);
}
else
{
_logger.LogDebug("Extracted {Count} RPM headers from BerkeleyDB rpmdb at {Path}.", results.Count, packagesPath);
}
return results;
}
private IReadOnlyList<RpmHeader> ReadRawRpmHeaders(byte[] data, string packagesPath, CancellationToken cancellationToken)
{
var headerBlobs = new List<byte[]>();
if (BerkeleyDbReader.IsBerkeleyDb(data))
{
headerBlobs.AddRange(BerkeleyDbReader.ExtractValues(data));
if (headerBlobs.Count == 0)
{
headerBlobs.AddRange(BerkeleyDbReader.ExtractValuesWithOverflow(data));
}
}
else
{
headerBlobs.AddRange(ExtractRpmHeadersFromRaw(data, cancellationToken));
}
if (headerBlobs.Count == 0)
{
_logger.LogWarning("No RPM headers parsed from legacy rpmdb Packages at {Path}.", packagesPath);
return Array.Empty<RpmHeader>();
}
var results = new List<RpmHeader>(headerBlobs.Count);
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var blob in headerBlobs)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var header = _parser.Parse(blob);
var key = $"{header.Name}::{header.Version}::{header.Release}::{header.Architecture}";
if (seen.Add(key))
{
results.Add(header);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to parse RPM header from legacy rpmdb blob.");
}
}
return results;
}
private static string? ResolveLegacyPackagesPath(string rootPath)
{
var candidates = new[]
{
Path.Combine(rootPath, "var", "lib", "rpm", "Packages"),
Path.Combine(rootPath, "usr", "lib", "sysimage", "rpm", "Packages"),
};
foreach (var candidate in candidates)
{
if (File.Exists(candidate))
{
return candidate;
}
}
return null;
}
private static IEnumerable<byte[]> ExtractRpmHeadersFromRaw(byte[] data, CancellationToken cancellationToken)
{
var magicBytes = new byte[] { 0x8e, 0xad, 0xe8, 0xab };
var seenOffsets = new HashSet<int>();
var offset = 0;
while (offset <= data.Length - magicBytes.Length)
{
cancellationToken.ThrowIfCancellationRequested();
var candidateIndex = FindNextMagic(data, magicBytes, offset);
if (candidateIndex < 0)
{
yield break;
}
if (!seenOffsets.Add(candidateIndex))
{
offset = candidateIndex + 1;
continue;
}
if (TryExtractHeaderSlice(data, candidateIndex, out var slice))
{
yield return slice;
}
offset = candidateIndex + 1;
}
}
private static bool TryExtractHeaderSlice(byte[] data, int offset, out byte[] slice)
{
slice = Array.Empty<byte>();
if (offset + 16 >= data.Length)
{
return false;
}
try
{
var span = data.AsSpan(offset);
var indexCount = BinaryPrimitives.ReadInt32BigEndian(span.Slice(8, 4));
var storeSize = BinaryPrimitives.ReadInt32BigEndian(span.Slice(12, 4));
if (indexCount <= 0 || storeSize <= 0)
{
return false;
}
var totalLength = 16 + (indexCount * 16) + storeSize;
if (totalLength <= 0 || offset + totalLength > data.Length)
{
return false;
}
slice = new byte[totalLength];
Buffer.BlockCopy(data, offset, slice, 0, totalLength);
return true;
}
catch
{
return false;
}
}
private static int FindNextMagic(byte[] data, byte[] magic, int startIndex)
{
for (var i = startIndex; i <= data.Length - magic.Length; i++)
{
if (data[i] == magic[0] &&
data[i + 1] == magic[1] &&
data[i + 2] == magic[2] &&
data[i + 3] == magic[3])
{
return i;
}
}
return -1;
}
private static byte[]? ExtractHeaderBlob(SqliteDataReader reader)
{
for (var i = 0; i < reader.FieldCount; i++)
{
if (reader.GetFieldType(i) == typeof(byte[]))
{
return reader.GetFieldValue<byte[]>(i);
}
}
return null;
}
private static object? TryGetPkgKey(SqliteDataReader reader)
{
try
{
var ordinal = reader.GetOrdinal("pkgKey");
if (ordinal >= 0)
{
return reader.GetValue(ordinal);
}
}
catch
{
}
return null;
}
}