audit, advisories and doctors/setup work

This commit is contained in:
master
2026-01-13 18:53:39 +02:00
parent 9ca7cb183e
commit d7be6ba34b
811 changed files with 54242 additions and 4056 deletions

View File

@@ -0,0 +1,452 @@
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Options;
using StellaOps.Cryptography;
using StellaOps.Scanner.Contracts;
namespace StellaOps.Scanner.Analyzers.Native;
public sealed class ElfSectionHashExtractor : IElfSectionHashExtractor
{
private static readonly byte[] ElfMagic = [0x7F, 0x45, 0x4C, 0x46];
private readonly TimeProvider _timeProvider;
private readonly ElfSectionHashOptions _options;
private readonly ICryptoHash? _cryptoHash;
private readonly HashSet<string> _sectionAllowList;
private readonly bool _includeBlake3;
public ElfSectionHashExtractor(
TimeProvider timeProvider,
IOptions<ElfSectionHashOptions> options,
ICryptoHash? cryptoHash = null)
{
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_cryptoHash = cryptoHash;
_sectionAllowList = _options.Sections
.Where(section => !string.IsNullOrWhiteSpace(section))
.Select(section => section.Trim())
.ToHashSet(StringComparer.Ordinal);
_includeBlake3 = _options.Algorithms.Any(alg =>
string.Equals(alg?.Trim(), "blake3", StringComparison.OrdinalIgnoreCase));
}
public async Task<ElfSectionHashSet?> ExtractAsync(
string elfPath,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(elfPath);
if (!_options.Enabled)
{
return null;
}
var bytes = await File.ReadAllBytesAsync(elfPath, cancellationToken).ConfigureAwait(false);
return await ExtractFromBytesAsync(bytes, elfPath, cancellationToken).ConfigureAwait(false);
}
public Task<ElfSectionHashSet?> ExtractFromBytesAsync(
ReadOnlyMemory<byte> elfBytes,
string virtualPath,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(virtualPath);
if (!_options.Enabled)
{
return Task.FromResult<ElfSectionHashSet?>(null);
}
var data = elfBytes.Span;
if (data.Length < 16 || !data[..4].SequenceEqual(ElfMagic))
{
return Task.FromResult<ElfSectionHashSet?>(null);
}
cancellationToken.ThrowIfCancellationRequested();
if (!TryReadHeader(data, out var header))
{
return Task.FromResult<ElfSectionHashSet?>(null);
}
var sections = ParseSections(data, header, cancellationToken);
if (sections.Count == 0)
{
return Task.FromResult<ElfSectionHashSet?>(null);
}
var sectionHashes = BuildSectionHashes(data, sections, cancellationToken);
var fileHash = ComputeSha256Hex(data);
var buildId = TryExtractBuildId(data, sections, header.IsLittleEndian);
var version = typeof(ElfSectionHashExtractor).Assembly.GetName().Version?.ToString() ?? "unknown";
var result = new ElfSectionHashSet
{
FilePath = virtualPath,
FileHash = fileHash,
BuildId = buildId,
Sections = sectionHashes,
ExtractedAt = _timeProvider.GetUtcNow(),
ExtractorVersion = version
};
return Task.FromResult<ElfSectionHashSet?>(result);
}
private ImmutableArray<ElfSectionHash> BuildSectionHashes(
ReadOnlySpan<byte> data,
List<ElfSectionHeader> sections,
CancellationToken cancellationToken)
{
var builder = new List<ElfSectionHash>(sections.Count);
foreach (var section in sections)
{
cancellationToken.ThrowIfCancellationRequested();
if (!_sectionAllowList.Contains(section.Name))
{
continue;
}
if (section.Size < 0 || section.Size > _options.MaxSectionSizeBytes)
{
continue;
}
if (section.Offset < 0 || section.Offset > data.Length)
{
continue;
}
if (section.Size > 0 && section.Offset + section.Size > data.Length)
{
continue;
}
var sectionData = section.Size == 0
? ReadOnlySpan<byte>.Empty
: data.Slice((int)section.Offset, (int)section.Size);
var sha256 = ComputeSha256Hex(sectionData);
var blake3 = ComputeBlake3Hex(sectionData);
builder.Add(new ElfSectionHash
{
Name = section.Name,
Offset = section.Offset,
Size = section.Size,
Sha256 = sha256,
Blake3 = blake3,
SectionType = section.Type,
Flags = section.Flags
});
}
return builder
.OrderBy(section => section.Name, StringComparer.Ordinal)
.ToImmutableArray();
}
private static string ComputeSha256Hex(ReadOnlySpan<byte> data)
{
Span<byte> hash = stackalloc byte[32];
SHA256.HashData(data, hash);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private string? ComputeBlake3Hex(ReadOnlySpan<byte> data)
{
if (!_includeBlake3 || _cryptoHash is null)
{
return null;
}
try
{
return _cryptoHash.ComputeHashHex(data, HashAlgorithms.Blake3_256);
}
catch (InvalidOperationException)
{
return null;
}
}
private static List<ElfSectionHeader> ParseSections(
ReadOnlySpan<byte> data,
ElfHeaderInfo header,
CancellationToken cancellationToken)
{
var sections = new List<ElfSectionHeader>(header.SectionHeaderCount);
var entrySize = header.SectionHeaderEntrySize;
var totalBytes = header.SectionHeaderOffset + (long)entrySize * header.SectionHeaderCount;
if (header.SectionHeaderOffset < 0 || totalBytes > data.Length)
{
return sections;
}
var stringTable = ReadStringTable(data, header, entrySize);
for (var i = 0; i < header.SectionHeaderCount; i++)
{
cancellationToken.ThrowIfCancellationRequested();
var offset = header.SectionHeaderOffset + (long)entrySize * i;
if (offset + entrySize > data.Length)
{
break;
}
var headerSpan = data.Slice((int)offset, entrySize);
var nameIndex = ReadUInt32(headerSpan.Slice(0, 4), header.IsLittleEndian);
var type = (ElfSectionType)ReadUInt32(headerSpan.Slice(4, 4), header.IsLittleEndian);
ulong flags;
ulong sectionOffset;
ulong sectionSize;
if (header.Is64Bit)
{
flags = ReadUInt64(headerSpan.Slice(8, 8), header.IsLittleEndian);
sectionOffset = ReadUInt64(headerSpan.Slice(24, 8), header.IsLittleEndian);
sectionSize = ReadUInt64(headerSpan.Slice(32, 8), header.IsLittleEndian);
}
else
{
flags = ReadUInt32(headerSpan.Slice(8, 4), header.IsLittleEndian);
sectionOffset = ReadUInt32(headerSpan.Slice(16, 4), header.IsLittleEndian);
sectionSize = ReadUInt32(headerSpan.Slice(20, 4), header.IsLittleEndian);
}
var name = ReadString(stringTable, nameIndex);
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
sections.Add(new ElfSectionHeader(
nameIndex,
name,
type,
(ElfSectionFlags)flags,
(long)sectionOffset,
(long)sectionSize));
}
return sections;
}
private static byte[] ReadStringTable(ReadOnlySpan<byte> data, ElfHeaderInfo header, int entrySize)
{
if (header.SectionNameStringTableIndex < 0 || header.SectionNameStringTableIndex >= header.SectionHeaderCount)
{
return Array.Empty<byte>();
}
var stringHeaderOffset = header.SectionHeaderOffset + (long)entrySize * header.SectionNameStringTableIndex;
if (stringHeaderOffset + entrySize > data.Length)
{
return Array.Empty<byte>();
}
var headerSpan = data.Slice((int)stringHeaderOffset, entrySize);
ulong offset;
ulong size;
if (header.Is64Bit)
{
offset = ReadUInt64(headerSpan.Slice(24, 8), header.IsLittleEndian);
size = ReadUInt64(headerSpan.Slice(32, 8), header.IsLittleEndian);
}
else
{
offset = ReadUInt32(headerSpan.Slice(16, 4), header.IsLittleEndian);
size = ReadUInt32(headerSpan.Slice(20, 4), header.IsLittleEndian);
}
if (offset + size > (ulong)data.Length)
{
return Array.Empty<byte>();
}
return data.Slice((int)offset, (int)size).ToArray();
}
private static string ReadString(byte[] table, uint offset)
{
if (table.Length == 0 || offset >= table.Length)
{
return string.Empty;
}
var end = Array.IndexOf(table, (byte)0, (int)offset);
if (end < 0)
{
end = table.Length;
}
return Encoding.UTF8.GetString(table, (int)offset, end - (int)offset);
}
private static bool TryReadHeader(ReadOnlySpan<byte> data, out ElfHeaderInfo header)
{
header = default;
if (data.Length < 52)
{
return false;
}
var is64Bit = data[4] == 2;
var isLittleEndian = data[5] == 1;
if (!is64Bit && data[4] != 1)
{
return false;
}
if (!isLittleEndian && data[5] != 2)
{
return false;
}
long shoff;
int shentsize;
int shnum;
int shstrndx;
if (is64Bit)
{
if (data.Length < 64)
{
return false;
}
shoff = (long)ReadUInt64(data.Slice(40, 8), isLittleEndian);
shentsize = ReadUInt16(data.Slice(58, 2), isLittleEndian);
shnum = ReadUInt16(data.Slice(60, 2), isLittleEndian);
shstrndx = ReadUInt16(data.Slice(62, 2), isLittleEndian);
}
else
{
shoff = ReadUInt32(data.Slice(32, 4), isLittleEndian);
shentsize = ReadUInt16(data.Slice(46, 2), isLittleEndian);
shnum = ReadUInt16(data.Slice(48, 2), isLittleEndian);
shstrndx = ReadUInt16(data.Slice(50, 2), isLittleEndian);
}
if (shoff <= 0 || shentsize <= 0 || shnum <= 0)
{
return false;
}
header = new ElfHeaderInfo(is64Bit, isLittleEndian, shoff, shentsize, shnum, shstrndx);
return true;
}
private static string? TryExtractBuildId(
ReadOnlySpan<byte> data,
List<ElfSectionHeader> sections,
bool isLittleEndian)
{
var noteSection = sections.FirstOrDefault(section =>
string.Equals(section.Name, ".note.gnu.build-id", StringComparison.Ordinal));
if (noteSection is null || noteSection.Size <= 0)
{
return null;
}
if (noteSection.Offset < 0 || noteSection.Offset + noteSection.Size > data.Length)
{
return null;
}
var noteSpan = data.Slice((int)noteSection.Offset, (int)noteSection.Size);
return ParseGnuNote(noteSpan, isLittleEndian);
}
private static string? ParseGnuNote(ReadOnlySpan<byte> note, bool isLittleEndian)
{
var offset = 0;
while (offset + 12 <= note.Length)
{
var namesz = ReadUInt32(note.Slice(offset, 4), isLittleEndian);
var descsz = ReadUInt32(note.Slice(offset + 4, 4), isLittleEndian);
var type = ReadUInt32(note.Slice(offset + 8, 4), isLittleEndian);
var nameStart = offset + 12;
var namePadded = AlignTo4(namesz);
var descStart = nameStart + namePadded;
var descPadded = AlignTo4(descsz);
var next = descStart + descPadded;
if (next > note.Length)
{
break;
}
if (type == 3 && namesz >= 3)
{
var nameSpan = note.Slice(nameStart, (int)namesz);
if (nameSpan.Length >= 3 && nameSpan[0] == (byte)'G' && nameSpan[1] == (byte)'N' && nameSpan[2] == (byte)'U')
{
var desc = note.Slice(descStart, (int)descsz);
var hex = Convert.ToHexString(desc).ToLowerInvariant();
return "gnu-build-id:" + hex;
}
}
offset = next;
}
return null;
}
private static int AlignTo4(uint value) => (int)((value + 3) & ~3u);
private static ushort ReadUInt16(ReadOnlySpan<byte> span, bool littleEndian)
{
return littleEndian
? BinaryPrimitives.ReadUInt16LittleEndian(span)
: BinaryPrimitives.ReadUInt16BigEndian(span);
}
private static uint ReadUInt32(ReadOnlySpan<byte> span, bool littleEndian)
{
return littleEndian
? BinaryPrimitives.ReadUInt32LittleEndian(span)
: BinaryPrimitives.ReadUInt32BigEndian(span);
}
private static ulong ReadUInt64(ReadOnlySpan<byte> span, bool littleEndian)
{
return littleEndian
? BinaryPrimitives.ReadUInt64LittleEndian(span)
: BinaryPrimitives.ReadUInt64BigEndian(span);
}
private readonly record struct ElfHeaderInfo(
bool Is64Bit,
bool IsLittleEndian,
long SectionHeaderOffset,
int SectionHeaderEntrySize,
int SectionHeaderCount,
int SectionNameStringTableIndex);
private sealed record ElfSectionHeader(
uint NameIndex,
string Name,
ElfSectionType Type,
ElfSectionFlags Flags,
long Offset,
long Size);
}

View File

@@ -0,0 +1,96 @@
using Microsoft.Extensions.Options;
namespace StellaOps.Scanner.Analyzers.Native;
/// <summary>
/// Options for ELF section hash extraction.
/// </summary>
public sealed class ElfSectionHashOptions
{
/// <summary>
/// Whether section hashing is enabled.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Section names to include (e.g., .text, .rodata, .data).
/// </summary>
public IList<string> Sections { get; } = new List<string>
{
".text",
".rodata",
".data",
".symtab",
".dynsym"
};
/// <summary>
/// Hash algorithms to compute (sha256 required, blake3 optional).
/// </summary>
public IList<string> Algorithms { get; } = new List<string>
{
"sha256"
};
/// <summary>
/// Maximum section size to hash (bytes).
/// </summary>
public long MaxSectionSizeBytes { get; set; } = 100 * 1024 * 1024;
}
/// <summary>
/// Validates <see cref="ElfSectionHashOptions"/>.
/// </summary>
public sealed class ElfSectionHashOptionsValidator : IValidateOptions<ElfSectionHashOptions>
{
private static readonly HashSet<string> AllowedAlgorithms = new(StringComparer.OrdinalIgnoreCase)
{
"sha256",
"blake3"
};
public ValidateOptionsResult Validate(string? name, ElfSectionHashOptions options)
{
ArgumentNullException.ThrowIfNull(options);
if (!options.Enabled)
{
return ValidateOptionsResult.Success;
}
if (options.MaxSectionSizeBytes <= 0)
{
return ValidateOptionsResult.Fail("MaxSectionSizeBytes must be greater than zero.");
}
var sections = options.Sections
.Where(section => !string.IsNullOrWhiteSpace(section))
.Select(section => section.Trim())
.ToArray();
if (sections.Length == 0)
{
return ValidateOptionsResult.Fail("At least one section name must be configured.");
}
if (options.Algorithms.Count == 0 ||
!options.Algorithms.Any(alg => string.Equals(alg?.Trim(), "sha256", StringComparison.OrdinalIgnoreCase)))
{
return ValidateOptionsResult.Fail("Algorithms must include sha256.");
}
var invalid = options.Algorithms
.Where(alg => !string.IsNullOrWhiteSpace(alg))
.Select(alg => alg.Trim())
.Where(alg => !AllowedAlgorithms.Contains(alg))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
if (invalid.Length > 0)
{
return ValidateOptionsResult.Fail($"Unsupported algorithms: {string.Join(", ", invalid)}");
}
return ValidateOptionsResult.Success;
}
}

View File

@@ -0,0 +1,24 @@
using StellaOps.Scanner.Contracts;
namespace StellaOps.Scanner.Analyzers.Native;
public interface IElfSectionHashExtractor
{
/// <summary>
/// Extracts section hashes from an ELF binary.
/// </summary>
/// <param name="elfPath">Path to the ELF file.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Section hash set, or null if not a valid ELF.</returns>
Task<ElfSectionHashSet?> ExtractAsync(
string elfPath,
CancellationToken cancellationToken = default);
/// <summary>
/// Extracts section hashes from ELF bytes in memory.
/// </summary>
Task<ElfSectionHashSet?> ExtractFromBytesAsync(
ReadOnlyMemory<byte> elfBytes,
string virtualPath,
CancellationToken cancellationToken = default);
}

View File

@@ -1,6 +1,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using StellaOps.Determinism;
using StellaOps.Scanner.Analyzers.Native.Plugin;
using StellaOps.Scanner.Analyzers.Native.RuntimeCapture;
@@ -71,6 +72,16 @@ public static class ServiceCollectionExtensions
// Register core services
services.TryAddSingleton<INativeAnalyzerPluginCatalog, NativeAnalyzerPluginCatalog>();
services.TryAddSingleton<INativeAnalyzer, NativeAnalyzer>();
services.TryAddSingleton<IValidateOptions<ElfSectionHashOptions>, ElfSectionHashOptionsValidator>();
var sectionOptionsBuilder = services.AddOptions<ElfSectionHashOptions>();
if (configuration != null)
{
sectionOptionsBuilder.Bind(configuration.GetSection($"{ConfigSectionName}:SectionHashes"));
}
sectionOptionsBuilder.ValidateOnStart();
services.TryAddSingleton<IElfSectionHashExtractor, ElfSectionHashExtractor>();
return services;
}

View File

@@ -14,8 +14,10 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\\__Libraries\\StellaOps.Scanner.Contracts\\StellaOps.Scanner.Contracts.csproj" />
<ProjectReference Include="..\\__Libraries\\StellaOps.Scanner.ProofSpine\\StellaOps.Scanner.ProofSpine.csproj" />
<ProjectReference Include="..\\..\\__Libraries\\StellaOps.Determinism.Abstractions\\StellaOps.Determinism.Abstractions.csproj" />
<ProjectReference Include="..\\..\\__Libraries\\StellaOps.Cryptography\\StellaOps.Cryptography.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,15 @@
# Scanner Native Analyzer Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20260113_001_001_SCANNER_elf_section_hashes.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| ELF-SECTION-MODELS-0001 | DONE | Define section hash models in Scanner.Contracts. |
| ELF-SECTION-EXTRACTOR-0001 | DONE | Implement extractor and integrate with ELF parsing. |
| ELF-SECTION-CONFIG-0001 | DONE | Add options and validation. |
| ELF-SECTION-EVIDENCE-0001 | DONE | Emit section hashes as SBOM properties. |
| ELF-SECTION-DI-0001 | DONE | Register extractor in DI. |
| ELF-SECTION-TESTS-0001 | DONE | Add unit tests for section hashing. |
| ELF-SECTION-FIXTURES-0001 | DONE | Add ELF fixtures with golden hashes. |
| ELF-SECTION-DETERMINISM-0001 | DONE | Add determinism regression test. |