Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (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
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
265 lines
7.5 KiB
C#
265 lines
7.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Buffers;
|
|
using System.IO;
|
|
using System.Text;
|
|
|
|
namespace StellaOps.Scanner.Analyzers.Lang.Go.Internal;
|
|
|
|
internal static class GoBinaryScanner
|
|
{
|
|
private static readonly ReadOnlyMemory<byte> BuildInfoMagic = new byte[]
|
|
{
|
|
0xFF, (byte)' ', (byte)'G', (byte)'o', (byte)' ', (byte)'b', (byte)'u', (byte)'i', (byte)'l', (byte)'d', (byte)'i', (byte)'n', (byte)'f', (byte)':'
|
|
};
|
|
|
|
private static readonly ReadOnlyMemory<byte> BuildIdMarker = Encoding.ASCII.GetBytes("Go build ID:");
|
|
private static readonly ReadOnlyMemory<byte> GoPclnTabMarker = Encoding.ASCII.GetBytes(".gopclntab");
|
|
private static readonly ReadOnlyMemory<byte> GoVersionPrefix = Encoding.ASCII.GetBytes("go1.");
|
|
|
|
public static IEnumerable<string> EnumerateCandidateFiles(string rootPath)
|
|
{
|
|
var enumeration = new EnumerationOptions
|
|
{
|
|
RecurseSubdirectories = true,
|
|
IgnoreInaccessible = true,
|
|
AttributesToSkip = FileAttributes.Device | FileAttributes.ReparsePoint,
|
|
MatchCasing = MatchCasing.CaseSensitive,
|
|
};
|
|
|
|
foreach (var path in Directory.EnumerateFiles(rootPath, "*", enumeration))
|
|
{
|
|
yield return path;
|
|
}
|
|
}
|
|
|
|
public static bool TryReadBuildInfo(string filePath, out string? goVersion, out string? moduleData)
|
|
{
|
|
goVersion = null;
|
|
moduleData = null;
|
|
|
|
FileInfo info;
|
|
try
|
|
{
|
|
info = new FileInfo(filePath);
|
|
if (!info.Exists || info.Length < 64 || info.Length > 128 * 1024 * 1024)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
catch (IOException)
|
|
{
|
|
return false;
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
return false;
|
|
}
|
|
catch (System.Security.SecurityException)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var length = info.Length;
|
|
if (length <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var inspectLength = (int)Math.Min(length, int.MaxValue);
|
|
var buffer = ArrayPool<byte>.Shared.Rent(inspectLength);
|
|
|
|
try
|
|
{
|
|
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
var totalRead = 0;
|
|
|
|
while (totalRead < inspectLength)
|
|
{
|
|
var read = stream.Read(buffer, totalRead, inspectLength - totalRead);
|
|
if (read <= 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
totalRead += read;
|
|
}
|
|
|
|
if (totalRead < 64)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var span = new ReadOnlySpan<byte>(buffer, 0, totalRead);
|
|
var offset = span.IndexOf(BuildInfoMagic.Span);
|
|
if (offset < 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var view = span[offset..];
|
|
return GoBuildInfoDecoder.TryDecode(view, out goVersion, out moduleData);
|
|
}
|
|
catch (IOException)
|
|
{
|
|
return false;
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
return false;
|
|
}
|
|
finally
|
|
{
|
|
Array.Clear(buffer, 0, inspectLength);
|
|
ArrayPool<byte>.Shared.Return(buffer);
|
|
}
|
|
}
|
|
|
|
public static bool TryClassifyStrippedBinary(string filePath, out GoStrippedBinaryClassification classification)
|
|
{
|
|
classification = default;
|
|
|
|
FileInfo fileInfo;
|
|
try
|
|
{
|
|
fileInfo = new FileInfo(filePath);
|
|
if (!fileInfo.Exists)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
catch (IOException)
|
|
{
|
|
return false;
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
return false;
|
|
}
|
|
catch (System.Security.SecurityException)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var length = fileInfo.Length;
|
|
if (length < 128)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const int WindowSize = 128 * 1024;
|
|
var readSize = (int)Math.Min(length, WindowSize);
|
|
var buffer = ArrayPool<byte>.Shared.Rent(readSize);
|
|
|
|
try
|
|
{
|
|
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
|
|
var headRead = stream.Read(buffer, 0, readSize);
|
|
if (headRead <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var headSpan = new ReadOnlySpan<byte>(buffer, 0, headRead);
|
|
var hasBuildId = headSpan.IndexOf(BuildIdMarker.Span) >= 0;
|
|
var hasPcln = headSpan.IndexOf(GoPclnTabMarker.Span) >= 0;
|
|
var goVersion = ExtractGoVersion(headSpan);
|
|
|
|
if (length > headRead)
|
|
{
|
|
var tailSize = Math.Min(readSize, (int)length);
|
|
if (tailSize > 0)
|
|
{
|
|
stream.Seek(-tailSize, SeekOrigin.End);
|
|
var tailRead = stream.Read(buffer, 0, tailSize);
|
|
if (tailRead > 0)
|
|
{
|
|
var tailSpan = new ReadOnlySpan<byte>(buffer, 0, tailRead);
|
|
hasBuildId |= tailSpan.IndexOf(BuildIdMarker.Span) >= 0;
|
|
hasPcln |= tailSpan.IndexOf(GoPclnTabMarker.Span) >= 0;
|
|
goVersion ??= ExtractGoVersion(tailSpan);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasBuildId)
|
|
{
|
|
classification = new GoStrippedBinaryClassification(
|
|
filePath,
|
|
GoStrippedBinaryIndicator.BuildId,
|
|
goVersion);
|
|
return true;
|
|
}
|
|
|
|
if (hasPcln && !string.IsNullOrEmpty(goVersion))
|
|
{
|
|
classification = new GoStrippedBinaryClassification(
|
|
filePath,
|
|
GoStrippedBinaryIndicator.GoRuntimeMarkers,
|
|
goVersion);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
finally
|
|
{
|
|
Array.Clear(buffer, 0, readSize);
|
|
ArrayPool<byte>.Shared.Return(buffer);
|
|
}
|
|
}
|
|
|
|
private static string? ExtractGoVersion(ReadOnlySpan<byte> data)
|
|
{
|
|
var prefix = GoVersionPrefix.Span;
|
|
var span = data;
|
|
|
|
while (!span.IsEmpty)
|
|
{
|
|
var index = span.IndexOf(prefix);
|
|
if (index < 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var absoluteIndex = data.Length - span.Length + index;
|
|
|
|
if (absoluteIndex > 0)
|
|
{
|
|
var previous = (char)data[absoluteIndex - 1];
|
|
if (char.IsLetterOrDigit(previous))
|
|
{
|
|
span = span[(index + 1)..];
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var start = absoluteIndex;
|
|
var end = start + prefix.Length;
|
|
|
|
while (end < data.Length && IsVersionCharacter((char)data[end]))
|
|
{
|
|
end++;
|
|
}
|
|
|
|
if (end - start <= prefix.Length)
|
|
{
|
|
span = span[(index + 1)..];
|
|
continue;
|
|
}
|
|
|
|
var candidate = data[start..end];
|
|
return Encoding.ASCII.GetString(candidate);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static bool IsVersionCharacter(char value)
|
|
=> (value >= '0' && value <= '9')
|
|
|| (value >= 'a' && value <= 'z')
|
|
|| (value >= 'A' && value <= 'Z')
|
|
|| value is '.' or '-' or '+' or '_';
|
|
}
|