audit, advisories and doctors/setup work
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
15
src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md
Normal file
15
src/Scanner/StellaOps.Scanner.Analyzers.Native/TASKS.md
Normal 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. |
|
||||
Reference in New Issue
Block a user