Restructure solution layout by module
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user