Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,385 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Linq;
using StellaOps.Scanner.Analyzers.Lang.Go.Internal;
namespace StellaOps.Scanner.Analyzers.Lang.Go;
public sealed class GoLanguageAnalyzer : ILanguageAnalyzer
{
public string Id => "golang";
public string DisplayName => "Go Analyzer";
public ValueTask AnalyzeAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(writer);
var candidatePaths = new List<string>(GoBinaryScanner.EnumerateCandidateFiles(context.RootPath));
candidatePaths.Sort(StringComparer.Ordinal);
var fallbackBinaries = new List<GoStrippedBinaryClassification>();
foreach (var absolutePath in candidatePaths)
{
cancellationToken.ThrowIfCancellationRequested();
if (!GoBuildInfoProvider.TryGetBuildInfo(absolutePath, out var buildInfo) || buildInfo is null)
{
if (GoBinaryScanner.TryClassifyStrippedBinary(absolutePath, out var classification))
{
fallbackBinaries.Add(classification);
}
continue;
}
EmitComponents(buildInfo, context, writer);
}
foreach (var fallback in fallbackBinaries)
{
cancellationToken.ThrowIfCancellationRequested();
EmitFallbackComponent(fallback, context, writer);
}
return ValueTask.CompletedTask;
}
private void EmitComponents(GoBuildInfo buildInfo, LanguageAnalyzerContext context, LanguageComponentWriter writer)
{
var components = new List<GoModule> { buildInfo.MainModule };
components.AddRange(buildInfo.Dependencies
.OrderBy(static module => module.Path, StringComparer.Ordinal)
.ThenBy(static module => module.Version, StringComparer.Ordinal));
string? binaryHash = null;
var binaryRelativePath = context.GetRelativePath(buildInfo.AbsoluteBinaryPath);
foreach (var module in components)
{
var metadata = BuildMetadata(buildInfo, module, binaryRelativePath);
var evidence = BuildEvidence(buildInfo, module, binaryRelativePath, context, ref binaryHash);
var usedByEntrypoint = module.IsMain && context.UsageHints.IsPathUsed(buildInfo.AbsoluteBinaryPath);
var purl = BuildPurl(module.Path, module.Version);
if (!string.IsNullOrEmpty(purl))
{
writer.AddFromPurl(
analyzerId: Id,
purl: purl,
name: module.Path,
version: module.Version,
type: "golang",
metadata: metadata,
evidence: evidence,
usedByEntrypoint: usedByEntrypoint);
}
else
{
var componentKey = BuildFallbackComponentKey(module, buildInfo, binaryRelativePath, ref binaryHash);
writer.AddFromExplicitKey(
analyzerId: Id,
componentKey: componentKey,
purl: null,
name: module.Path,
version: module.Version,
type: "golang",
metadata: metadata,
evidence: evidence,
usedByEntrypoint: usedByEntrypoint);
}
}
}
private static IEnumerable<KeyValuePair<string, string?>> BuildMetadata(GoBuildInfo buildInfo, GoModule module, string binaryRelativePath)
{
var entries = new List<KeyValuePair<string, string?>>(16)
{
new("modulePath", module.Path),
new("binaryPath", string.IsNullOrEmpty(binaryRelativePath) ? "." : binaryRelativePath),
};
if (!string.IsNullOrEmpty(module.Version))
{
entries.Add(new KeyValuePair<string, string?>("moduleVersion", module.Version));
}
if (!string.IsNullOrEmpty(module.Sum))
{
entries.Add(new KeyValuePair<string, string?>("moduleSum", module.Sum));
}
if (module.Replacement is not null)
{
entries.Add(new KeyValuePair<string, string?>("replacedBy.path", module.Replacement.Path));
if (!string.IsNullOrEmpty(module.Replacement.Version))
{
entries.Add(new KeyValuePair<string, string?>("replacedBy.version", module.Replacement.Version));
}
if (!string.IsNullOrEmpty(module.Replacement.Sum))
{
entries.Add(new KeyValuePair<string, string?>("replacedBy.sum", module.Replacement.Sum));
}
}
if (module.IsMain)
{
entries.Add(new KeyValuePair<string, string?>("go.version", buildInfo.GoVersion));
entries.Add(new KeyValuePair<string, string?>("modulePath.main", buildInfo.ModulePath));
foreach (var setting in buildInfo.Settings)
{
var key = $"build.{setting.Key}";
if (!entries.Any(pair => string.Equals(pair.Key, key, StringComparison.Ordinal)))
{
entries.Add(new KeyValuePair<string, string?>(key, setting.Value));
}
}
if (buildInfo.DwarfMetadata is { } dwarf)
{
AddIfMissing(entries, "build.vcs", dwarf.VcsSystem);
AddIfMissing(entries, "build.vcs.revision", dwarf.Revision);
AddIfMissing(entries, "build.vcs.modified", dwarf.Modified?.ToString()?.ToLowerInvariant());
AddIfMissing(entries, "build.vcs.time", dwarf.TimestampUtc);
}
}
entries.Sort(static (left, right) => string.CompareOrdinal(left.Key, right.Key));
return entries;
}
private void EmitFallbackComponent(GoStrippedBinaryClassification strippedBinary, LanguageAnalyzerContext context, LanguageComponentWriter writer)
{
var relativePath = context.GetRelativePath(strippedBinary.AbsolutePath);
var normalizedRelative = string.IsNullOrEmpty(relativePath) ? "." : relativePath;
var usedByEntrypoint = context.UsageHints.IsPathUsed(strippedBinary.AbsolutePath);
var binaryHash = ComputeBinaryHash(strippedBinary.AbsolutePath);
var metadata = new List<KeyValuePair<string, string?>>
{
new("binaryPath", normalizedRelative),
new("languageHint", "golang"),
new("provenance", "binary"),
};
if (!string.IsNullOrEmpty(binaryHash))
{
metadata.Add(new KeyValuePair<string, string?>("binary.sha256", binaryHash));
}
if (!string.IsNullOrEmpty(strippedBinary.GoVersionHint))
{
metadata.Add(new KeyValuePair<string, string?>("go.version.hint", strippedBinary.GoVersionHint));
}
metadata.Sort(static (left, right) => string.CompareOrdinal(left.Key, right.Key));
var evidence = new List<LanguageComponentEvidence>
{
new(
LanguageEvidenceKind.File,
"binary",
normalizedRelative,
null,
string.IsNullOrEmpty(binaryHash) ? null : binaryHash),
};
var detectionSource = strippedBinary.Indicator switch
{
GoStrippedBinaryIndicator.BuildId => "build-id",
GoStrippedBinaryIndicator.GoRuntimeMarkers => "runtime-markers",
_ => null,
};
if (!string.IsNullOrEmpty(detectionSource))
{
evidence.Add(new LanguageComponentEvidence(
LanguageEvidenceKind.Metadata,
"go.heuristic",
"classification",
detectionSource,
null));
}
evidence.Sort(static (left, right) => string.CompareOrdinal(left.ComparisonKey, right.ComparisonKey));
var componentName = Path.GetFileName(strippedBinary.AbsolutePath);
if (string.IsNullOrWhiteSpace(componentName))
{
componentName = "golang-binary";
}
var componentKey = string.IsNullOrEmpty(binaryHash)
? $"golang::bin::{normalizedRelative}"
: $"golang::bin::sha256:{binaryHash}";
writer.AddFromExplicitKey(
analyzerId: Id,
componentKey: componentKey,
purl: null,
name: componentName,
version: null,
type: "bin",
metadata: metadata,
evidence: evidence,
usedByEntrypoint: usedByEntrypoint);
GoAnalyzerMetrics.RecordHeuristic(strippedBinary.Indicator, !string.IsNullOrEmpty(strippedBinary.GoVersionHint));
}
private static IEnumerable<LanguageComponentEvidence> BuildEvidence(GoBuildInfo buildInfo, GoModule module, string binaryRelativePath, LanguageAnalyzerContext context, ref string? binaryHash)
{
var evidence = new List<LanguageComponentEvidence>
{
new(
LanguageEvidenceKind.Metadata,
"go.buildinfo",
$"module:{module.Path}",
module.Version ?? string.Empty,
module.Sum)
};
if (module.IsMain)
{
foreach (var setting in buildInfo.Settings)
{
evidence.Add(new LanguageComponentEvidence(
LanguageEvidenceKind.Metadata,
"go.buildinfo.setting",
setting.Key,
setting.Value,
null));
}
if (buildInfo.DwarfMetadata is { } dwarf)
{
if (!string.IsNullOrWhiteSpace(dwarf.VcsSystem))
{
evidence.Add(new LanguageComponentEvidence(
LanguageEvidenceKind.Metadata,
"go.dwarf",
"vcs",
dwarf.VcsSystem,
null));
}
if (!string.IsNullOrWhiteSpace(dwarf.Revision))
{
evidence.Add(new LanguageComponentEvidence(
LanguageEvidenceKind.Metadata,
"go.dwarf",
"vcs.revision",
dwarf.Revision,
null));
}
if (dwarf.Modified.HasValue)
{
evidence.Add(new LanguageComponentEvidence(
LanguageEvidenceKind.Metadata,
"go.dwarf",
"vcs.modified",
dwarf.Modified.Value ? "true" : "false",
null));
}
if (!string.IsNullOrWhiteSpace(dwarf.TimestampUtc))
{
evidence.Add(new LanguageComponentEvidence(
LanguageEvidenceKind.Metadata,
"go.dwarf",
"vcs.time",
dwarf.TimestampUtc,
null));
}
}
}
// Attach binary hash evidence for fallback components without purl.
if (string.IsNullOrEmpty(module.Version))
{
binaryHash ??= ComputeBinaryHash(buildInfo.AbsoluteBinaryPath);
if (!string.IsNullOrEmpty(binaryHash))
{
evidence.Add(new LanguageComponentEvidence(
LanguageEvidenceKind.File,
"binary",
string.IsNullOrEmpty(binaryRelativePath) ? "." : binaryRelativePath,
null,
binaryHash));
}
}
evidence.Sort(static (left, right) => string.CompareOrdinal(left.ComparisonKey, right.ComparisonKey));
return evidence;
}
private static string? BuildPurl(string path, string? version)
{
if (string.IsNullOrWhiteSpace(path) || string.IsNullOrWhiteSpace(version))
{
return null;
}
var cleanedPath = path.Trim();
var cleanedVersion = version.Trim();
var encodedVersion = Uri.EscapeDataString(cleanedVersion);
return $"pkg:golang/{cleanedPath}@{encodedVersion}";
}
private static string BuildFallbackComponentKey(GoModule module, GoBuildInfo buildInfo, string binaryRelativePath, ref string? binaryHash)
{
var relative = string.IsNullOrEmpty(binaryRelativePath) ? "." : binaryRelativePath;
binaryHash ??= ComputeBinaryHash(buildInfo.AbsoluteBinaryPath);
if (!string.IsNullOrEmpty(binaryHash))
{
return $"golang::module:{module.Path}::{relative}::{binaryHash}";
}
return $"golang::module:{module.Path}::{relative}";
}
private static void AddIfMissing(List<KeyValuePair<string, string?>> entries, string key, string? value)
{
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(value))
{
return;
}
if (entries.Any(entry => string.Equals(entry.Key, key, StringComparison.Ordinal)))
{
return;
}
entries.Add(new KeyValuePair<string, string?>(key, value));
}
private static string? ComputeBinaryHash(string path)
{
try
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
using var sha = SHA256.Create();
var hash = sha.ComputeHash(stream);
return Convert.ToHexString(hash).ToLowerInvariant();
}
catch (IOException)
{
return null;
}
catch (UnauthorizedAccessException)
{
return null;
}
}
}