feat: Add initial implementation of Vulnerability Resolver Jobs
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Created project for StellaOps.Scanner.Analyzers.Native.Tests with necessary dependencies. - Documented roles and guidelines in AGENTS.md for Scheduler module. - Implemented IResolverJobService interface and InMemoryResolverJobService for handling resolver jobs. - Added ResolverBacklogNotifier and ResolverBacklogService for monitoring job metrics. - Developed API endpoints for managing resolver jobs and retrieving metrics. - Defined models for resolver job requests and responses. - Integrated dependency injection for resolver job services. - Implemented ImpactIndexSnapshot for persisting impact index data. - Introduced SignalsScoringOptions for configurable scoring weights in reachability scoring. - Added unit tests for ReachabilityScoringService and RuntimeFactsIngestionService. - Created dotnet-filter.sh script to handle command-line arguments for dotnet. - Established nuget-prime project for managing package downloads.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
root = false
|
||||
|
||||
[*.cs]
|
||||
dotnet_diagnostic.CA2022.severity = none
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Native;
|
||||
|
||||
public sealed record NativeBinaryIdentity(
|
||||
NativeFormat Format,
|
||||
string? CpuArchitecture,
|
||||
string? OperatingSystem,
|
||||
string? Endianness,
|
||||
string? BuildId,
|
||||
string? Uuid,
|
||||
string? InterpreterPath);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace StellaOps.Scanner.Analyzers.Native;
|
||||
|
||||
public enum NativeFormat
|
||||
{
|
||||
Unknown = 0,
|
||||
Elf = 1,
|
||||
Pe = 2,
|
||||
MachO = 3,
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
|
||||
#pragma warning disable CA2022 // Stream.Read validation handled via ReadExactly/ReadAtLeast
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Native;
|
||||
|
||||
public static class NativeFormatDetector
|
||||
{
|
||||
public static bool TryDetect(Stream stream, out NativeBinaryIdentity identity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
|
||||
Span<byte> header = stackalloc byte[512];
|
||||
var read = stream.ReadAtLeast(header, 4, throwOnEndOfStream: false);
|
||||
if (read < 4)
|
||||
{
|
||||
identity = new NativeBinaryIdentity(NativeFormat.Unknown, null, null, null, null, null, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
var span = header[..read];
|
||||
|
||||
if (IsElf(span, stream, out identity))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsPe(span, out identity))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsMachO(span, stream, out identity))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
identity = new NativeBinaryIdentity(NativeFormat.Unknown, null, null, null, null, null, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsElf(ReadOnlySpan<byte> span, Stream stream, out NativeBinaryIdentity identity)
|
||||
{
|
||||
identity = default!;
|
||||
if (span.Length < 20)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (span[0] != 0x7F || span[1] != (byte)'E' || span[2] != (byte)'L' || span[3] != (byte)'F')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var elfClass = span[4]; // 1=32,2=64
|
||||
var dataEncoding = span[5]; // 1=LE,2=BE
|
||||
var osAbi = span[7];
|
||||
var endianness = dataEncoding == 2 ? "be" : "le";
|
||||
|
||||
ushort machine;
|
||||
if (dataEncoding == 2)
|
||||
{
|
||||
machine = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(18, 2));
|
||||
}
|
||||
else
|
||||
{
|
||||
machine = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(18, 2));
|
||||
}
|
||||
|
||||
var arch = MapElfMachine(machine);
|
||||
var os = MapElfOs(osAbi);
|
||||
|
||||
var phoff = dataEncoding == 2
|
||||
? BinaryPrimitives.ReadUInt64BigEndian(span.Slice(32, 8))
|
||||
: BinaryPrimitives.ReadUInt64LittleEndian(span.Slice(32, 8));
|
||||
|
||||
var phentsize = dataEncoding == 2
|
||||
? BinaryPrimitives.ReadUInt16BigEndian(span.Slice(54, 2))
|
||||
: BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(54, 2));
|
||||
var phnum = dataEncoding == 2
|
||||
? BinaryPrimitives.ReadUInt16BigEndian(span.Slice(56, 2))
|
||||
: BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(56, 2));
|
||||
|
||||
string? buildId = null;
|
||||
string? interp = null;
|
||||
|
||||
if (phentsize > 0 && phnum > 0 && phoff > 0)
|
||||
{
|
||||
for (var i = 0; i < phnum; i++)
|
||||
{
|
||||
var entryOffset = (long)(phoff + (ulong)(i * phentsize));
|
||||
if (entryOffset + phentsize > stream.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var ph = new byte[phentsize];
|
||||
stream.Seek(entryOffset, SeekOrigin.Begin);
|
||||
stream.ReadExactly(ph, 0, ph.Length);
|
||||
var phSpan = ph.AsSpan();
|
||||
|
||||
var pType = dataEncoding == 2
|
||||
? BinaryPrimitives.ReadUInt32BigEndian(phSpan)
|
||||
: BinaryPrimitives.ReadUInt32LittleEndian(phSpan);
|
||||
|
||||
if (pType == 3 && interp is null) // PT_INTERP
|
||||
{
|
||||
ulong offset = dataEncoding == 2
|
||||
? BinaryPrimitives.ReadUInt64BigEndian(phSpan.Slice(8, 8))
|
||||
: BinaryPrimitives.ReadUInt64LittleEndian(phSpan.Slice(8, 8));
|
||||
uint fileSize = dataEncoding == 2
|
||||
? BinaryPrimitives.ReadUInt32BigEndian(phSpan.Slice(32, 4))
|
||||
: BinaryPrimitives.ReadUInt32LittleEndian(phSpan.Slice(32, 4));
|
||||
|
||||
if (fileSize > 0 && offset + fileSize <= (ulong)stream.Length)
|
||||
{
|
||||
var buffer = new byte[fileSize];
|
||||
stream.Seek((long)offset, SeekOrigin.Begin);
|
||||
stream.ReadExactly(buffer, 0, buffer.Length);
|
||||
var str = System.Text.Encoding.ASCII.GetString(buffer).TrimEnd('\0');
|
||||
if (!string.IsNullOrWhiteSpace(str))
|
||||
{
|
||||
interp = str;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (pType == 4 && buildId is null) // PT_NOTE
|
||||
{
|
||||
ulong offset = dataEncoding == 2
|
||||
? BinaryPrimitives.ReadUInt64BigEndian(phSpan.Slice(8, 8))
|
||||
: BinaryPrimitives.ReadUInt64LittleEndian(phSpan.Slice(8, 8));
|
||||
uint fileSize = dataEncoding == 2
|
||||
? BinaryPrimitives.ReadUInt32BigEndian(phSpan.Slice(32, 4))
|
||||
: BinaryPrimitives.ReadUInt32LittleEndian(phSpan.Slice(32, 4));
|
||||
|
||||
if (fileSize > 0 && offset + fileSize <= (ulong)stream.Length)
|
||||
{
|
||||
var buffer = new byte[fileSize];
|
||||
stream.Seek((long)offset, SeekOrigin.Begin);
|
||||
stream.ReadExactly(buffer, 0, buffer.Length);
|
||||
ParseElfNote(buffer, dataEncoding == 2, ref buildId);
|
||||
}
|
||||
}
|
||||
|
||||
if (buildId is not null && interp is not null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
identity = new NativeBinaryIdentity(NativeFormat.Elf, arch, os, endianness, buildId, null, interp);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsPe(ReadOnlySpan<byte> span, out NativeBinaryIdentity identity)
|
||||
{
|
||||
identity = default!;
|
||||
if (span.Length < 0x40)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (span[0] != 'M' || span[1] != 'Z')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var peHeaderOffset = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(0x3C, 4));
|
||||
if (peHeaderOffset < 0 || peHeaderOffset + 6 > span.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (peHeaderOffset + 6 > span.Length)
|
||||
{
|
||||
identity = new NativeBinaryIdentity(NativeFormat.Unknown, null, null, null, null, null, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (span[peHeaderOffset] != 'P' || span[peHeaderOffset + 1] != 'E' || span[peHeaderOffset + 2] != 0 || span[peHeaderOffset + 3] != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var machine = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(peHeaderOffset + 4, 2));
|
||||
var arch = MapPeMachine(machine);
|
||||
|
||||
identity = new NativeBinaryIdentity(NativeFormat.Pe, arch, "windows", Endianness: "le", BuildId: null, Uuid: null, InterpreterPath: null);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsMachO(ReadOnlySpan<byte> span, Stream stream, out NativeBinaryIdentity identity)
|
||||
{
|
||||
identity = default!;
|
||||
if (span.Length < 12)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var magic = BinaryPrimitives.ReadUInt32BigEndian(span);
|
||||
var isFat = magic is 0xCAFEBABE or 0xBEBAFECA;
|
||||
var is64 = magic is 0xFEEDFACF or 0xCFFAEDFE;
|
||||
var is32 = magic is 0xFEEDFACE or 0xCEFAEDFE;
|
||||
|
||||
if (!(isFat || is64 || is32))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool bigEndian = magic is 0xCAFEBABE or 0xFEEDFACE or 0xFEEDFACF;
|
||||
|
||||
uint cputype;
|
||||
if (isFat)
|
||||
{
|
||||
var cputypeOffset = 8; // first architecture entry
|
||||
if (span.Length < cputypeOffset + 4)
|
||||
{
|
||||
identity = new NativeBinaryIdentity(NativeFormat.Unknown, null, null, null, null, null, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
cputype = bigEndian
|
||||
? BinaryPrimitives.ReadUInt32BigEndian(span.Slice(cputypeOffset, 4))
|
||||
: BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(cputypeOffset, 4));
|
||||
}
|
||||
else
|
||||
{
|
||||
cputype = bigEndian
|
||||
? BinaryPrimitives.ReadUInt32BigEndian(span.Slice(4, 4))
|
||||
: BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(4, 4));
|
||||
}
|
||||
|
||||
var arch = MapMachCpuType(cputype);
|
||||
var endianness = bigEndian ? "be" : "le";
|
||||
|
||||
var uuid = ExtractMachUuid(stream, bigEndian);
|
||||
|
||||
identity = new NativeBinaryIdentity(NativeFormat.MachO, arch, "darwin", Endianness: endianness, BuildId: null, Uuid: uuid, InterpreterPath: null);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string? ExtractMachUuid(Stream stream, bool bigEndian)
|
||||
{
|
||||
try
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
using var reader = new BinaryReader(stream, System.Text.Encoding.ASCII, leaveOpen: true);
|
||||
|
||||
var magic = reader.ReadUInt32();
|
||||
var is64 = magic is 0xFEEDFACF or 0xCFFAEDFE;
|
||||
var headerSize = is64 ? 32 : 28;
|
||||
stream.Seek(16, SeekOrigin.Begin);
|
||||
var ncmds = ReadUInt32(reader, bigEndian);
|
||||
_ = ReadUInt32(reader, bigEndian); // sizeofcmds
|
||||
|
||||
stream.Seek(headerSize, SeekOrigin.Begin);
|
||||
for (var i = 0; i < ncmds; i++)
|
||||
{
|
||||
var cmdStart = stream.Position;
|
||||
var cmd = ReadUInt32(reader, bigEndian);
|
||||
var cmdsize = ReadUInt32(reader, bigEndian);
|
||||
if (cmd == 0x1B) // LC_UUID
|
||||
{
|
||||
var uuidBytes = reader.ReadBytes(16);
|
||||
return new Guid(uuidBytes).ToString();
|
||||
}
|
||||
|
||||
stream.Seek(cmdStart + cmdsize, SeekOrigin.Begin);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(BinaryReader reader, bool bigEndian)
|
||||
{
|
||||
var data = reader.ReadBytes(4);
|
||||
return bigEndian ? BinaryPrimitives.ReadUInt32BigEndian(data) : BinaryPrimitives.ReadUInt32LittleEndian(data);
|
||||
}
|
||||
|
||||
private static string? MapElfMachine(ushort machine) => machine switch
|
||||
{
|
||||
0x03 => "x86",
|
||||
0x08 => "mips",
|
||||
0x14 => "powerpc",
|
||||
0x28 => "arm",
|
||||
0x32 => "ia64",
|
||||
0x3E => "x86_64",
|
||||
0xB7 => "aarch64",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static string? MapElfOs(byte abi) => abi switch
|
||||
{
|
||||
0x00 => "linux",
|
||||
0x03 => "linux",
|
||||
0x06 => "solaris",
|
||||
0x07 => "aix",
|
||||
0x08 => "irix",
|
||||
0x09 => "freebsd",
|
||||
0x0C => "openbsd",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static string? MapPeMachine(ushort machine) => machine switch
|
||||
{
|
||||
0x014c => "x86",
|
||||
0x0200 => "ia64",
|
||||
0x8664 => "x86_64",
|
||||
0x01c0 => "arm",
|
||||
0x01c4 => "armv7",
|
||||
0xAA64 => "arm64",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static string? MapMachCpuType(uint cpuType) => cpuType switch
|
||||
{
|
||||
0x00000007 => "x86",
|
||||
0x01000007 => "x86_64",
|
||||
0x0000000C => "arm",
|
||||
0x0100000C => "arm64",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static void ParseElfNote(ReadOnlySpan<byte> note, bool bigEndian, ref string? buildId)
|
||||
{
|
||||
if (note.Length < 12)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var namesz = bigEndian
|
||||
? BinaryPrimitives.ReadUInt32BigEndian(note)
|
||||
: BinaryPrimitives.ReadUInt32LittleEndian(note);
|
||||
var descsz = bigEndian
|
||||
? BinaryPrimitives.ReadUInt32BigEndian(note.Slice(4))
|
||||
: BinaryPrimitives.ReadUInt32LittleEndian(note.Slice(4));
|
||||
var type = bigEndian
|
||||
? BinaryPrimitives.ReadUInt32BigEndian(note.Slice(8))
|
||||
: BinaryPrimitives.ReadUInt32LittleEndian(note.Slice(8));
|
||||
|
||||
var offset = 12;
|
||||
var nameEnd = offset + (int)namesz;
|
||||
if (nameEnd > note.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var name = System.Text.Encoding.ASCII.GetString(note.Slice(offset, (int)namesz)).TrimEnd('\0');
|
||||
offset = Align(nameEnd, 4);
|
||||
var descEnd = offset + (int)descsz;
|
||||
if (descEnd > note.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (name == "GNU" && type == 3 && descsz > 0)
|
||||
{
|
||||
var desc = note.Slice(offset, (int)descsz);
|
||||
buildId = Convert.ToHexString(desc).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
private static int Align(int value, int alignment) => (value + (alignment - 1)) & ~(alignment - 1);
|
||||
}
|
||||
#pragma warning restore CA2022
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<NoWarn>CA2022</NoWarn>
|
||||
<WarningsNotAsErrors>CA2022</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user