feat: Add Go module and workspace test fixtures

- Created expected JSON files for Go modules and workspaces.
- Added go.mod and go.sum files for example projects.
- Implemented private module structure with expected JSON output.
- Introduced vendored dependencies with corresponding expected JSON.
- Developed PostgresGraphJobStore for managing graph jobs.
- Established SQL migration scripts for graph jobs schema.
- Implemented GraphJobRepository for CRUD operations on graph jobs.
- Created IGraphJobRepository interface for repository abstraction.
- Added unit tests for GraphJobRepository to ensure functionality.
This commit is contained in:
StellaOps Bot
2025-12-06 20:04:03 +02:00
parent a6f1406509
commit 05597616d6
178 changed files with 12022 additions and 4545 deletions

View File

@@ -18,7 +18,94 @@ public sealed class GoLanguageAnalyzer : ILanguageAnalyzer
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(writer);
var candidatePaths = new List<string>(GoBinaryScanner.EnumerateCandidateFiles(context.RootPath));
// Track emitted modules to avoid duplicates (binary takes precedence over source)
var emittedModules = new HashSet<string>(StringComparer.Ordinal);
// Phase 1: Source scanning (go.mod, go.sum, go.work, vendor)
ScanSourceFiles(context, writer, emittedModules, cancellationToken);
// Phase 2: Binary scanning (existing behavior)
ScanBinaries(context, writer, emittedModules, cancellationToken);
return ValueTask.CompletedTask;
}
private void ScanSourceFiles(
LanguageAnalyzerContext context,
LanguageComponentWriter writer,
HashSet<string> emittedModules,
CancellationToken cancellationToken)
{
// Discover Go projects
var projects = GoProjectDiscoverer.Discover(context.RootPath, cancellationToken);
if (projects.Count == 0)
{
return;
}
foreach (var project in projects)
{
cancellationToken.ThrowIfCancellationRequested();
IReadOnlyList<GoSourceInventory.SourceInventoryResult> inventories;
if (project.IsWorkspace)
{
// Handle workspace with multiple modules
inventories = GoSourceInventory.BuildWorkspaceInventory(project, cancellationToken);
}
else
{
// Single module
var inventory = GoSourceInventory.BuildInventory(project);
inventories = inventory.IsEmpty
? Array.Empty<GoSourceInventory.SourceInventoryResult>()
: new[] { inventory };
}
foreach (var inventory in inventories)
{
if (inventory.IsEmpty)
{
continue;
}
// Emit the main module
if (!string.IsNullOrEmpty(inventory.ModulePath))
{
EmitMainModuleFromSource(inventory, project, context, writer, emittedModules);
}
// Emit dependencies
foreach (var module in inventory.Modules.OrderBy(m => m.Path, StringComparer.Ordinal))
{
cancellationToken.ThrowIfCancellationRequested();
EmitSourceModule(module, inventory, project, context, writer, emittedModules);
}
}
}
}
private void ScanBinaries(
LanguageAnalyzerContext context,
LanguageComponentWriter writer,
HashSet<string> emittedModules,
CancellationToken cancellationToken)
{
var candidatePaths = new List<string>();
// Use binary format pre-filtering for efficiency
foreach (var path in GoBinaryScanner.EnumerateCandidateFiles(context.RootPath))
{
cancellationToken.ThrowIfCancellationRequested();
// Quick check for known binary formats
if (GoBinaryFormatDetector.IsPotentialBinary(path))
{
candidatePaths.Add(path);
}
}
candidatePaths.Sort(StringComparer.Ordinal);
var fallbackBinaries = new List<GoStrippedBinaryClassification>();
@@ -37,7 +124,7 @@ public sealed class GoLanguageAnalyzer : ILanguageAnalyzer
continue;
}
EmitComponents(buildInfo, context, writer);
EmitComponents(buildInfo, context, writer, emittedModules);
}
foreach (var fallback in fallbackBinaries)
@@ -45,11 +132,197 @@ public sealed class GoLanguageAnalyzer : ILanguageAnalyzer
cancellationToken.ThrowIfCancellationRequested();
EmitFallbackComponent(fallback, context, writer);
}
return ValueTask.CompletedTask;
}
private void EmitComponents(GoBuildInfo buildInfo, LanguageAnalyzerContext context, LanguageComponentWriter writer)
private void EmitMainModuleFromSource(
GoSourceInventory.SourceInventoryResult inventory,
GoProjectDiscoverer.GoProject project,
LanguageAnalyzerContext context,
LanguageComponentWriter writer,
HashSet<string> emittedModules)
{
// Main module from go.mod (typically no version in source)
var modulePath = inventory.ModulePath!;
var moduleKey = $"{modulePath}@(devel)";
if (!emittedModules.Add(moduleKey))
{
return; // Already emitted
}
var relativePath = context.GetRelativePath(project.RootPath);
var goModRelative = project.HasGoMod ? context.GetRelativePath(project.GoModPath!) : null;
var metadata = new SortedDictionary<string, string?>(StringComparer.Ordinal)
{
["modulePath"] = modulePath,
["modulePath.main"] = modulePath,
["provenance"] = "source"
};
if (!string.IsNullOrEmpty(inventory.GoVersion))
{
metadata["go.version"] = inventory.GoVersion;
}
if (!string.IsNullOrEmpty(relativePath))
{
metadata["projectPath"] = relativePath;
}
if (project.IsWorkspace)
{
metadata["workspace"] = "true";
}
var evidence = new List<LanguageComponentEvidence>();
if (!string.IsNullOrEmpty(goModRelative))
{
evidence.Add(new LanguageComponentEvidence(
LanguageEvidenceKind.File,
"go.mod",
goModRelative,
modulePath,
null));
}
evidence.Sort(static (l, r) => string.CompareOrdinal(l.ComparisonKey, r.ComparisonKey));
// Main module typically has (devel) as version in source context
writer.AddFromExplicitKey(
analyzerId: Id,
componentKey: $"golang::source::{modulePath}::(devel)",
purl: null,
name: modulePath,
version: "(devel)",
type: "golang",
metadata: metadata,
evidence: evidence);
}
private void EmitSourceModule(
GoSourceInventory.GoSourceModule module,
GoSourceInventory.SourceInventoryResult inventory,
GoProjectDiscoverer.GoProject project,
LanguageAnalyzerContext context,
LanguageComponentWriter writer,
HashSet<string> emittedModules)
{
var moduleKey = $"{module.Path}@{module.Version}";
if (!emittedModules.Add(moduleKey))
{
return; // Already emitted (binary takes precedence)
}
var purl = BuildPurl(module.Path, module.Version);
var goModRelative = project.HasGoMod ? context.GetRelativePath(project.GoModPath!) : null;
var metadata = new SortedDictionary<string, string?>(StringComparer.Ordinal)
{
["modulePath"] = module.Path,
["moduleVersion"] = module.Version,
["provenance"] = "source"
};
if (!string.IsNullOrEmpty(module.Checksum))
{
metadata["moduleSum"] = module.Checksum;
}
if (module.IsDirect)
{
metadata["dependency.direct"] = "true";
}
if (module.IsIndirect)
{
metadata["dependency.indirect"] = "true";
}
if (module.IsVendored)
{
metadata["vendored"] = "true";
}
if (module.IsPrivate)
{
metadata["private"] = "true";
}
if (module.ModuleCategory != "public")
{
metadata["moduleCategory"] = module.ModuleCategory;
}
if (!string.IsNullOrEmpty(module.Registry))
{
metadata["registry"] = module.Registry;
}
if (module.IsReplaced)
{
metadata["replaced"] = "true";
if (!string.IsNullOrEmpty(module.ReplacementPath))
{
metadata["replacedBy.path"] = module.ReplacementPath;
}
if (!string.IsNullOrEmpty(module.ReplacementVersion))
{
metadata["replacedBy.version"] = module.ReplacementVersion;
}
}
if (module.IsExcluded)
{
metadata["excluded"] = "true";
}
var evidence = new List<LanguageComponentEvidence>();
// Evidence from go.mod
if (!string.IsNullOrEmpty(goModRelative))
{
evidence.Add(new LanguageComponentEvidence(
LanguageEvidenceKind.Metadata,
module.Source,
goModRelative,
$"{module.Path}@{module.Version}",
module.Checksum));
}
evidence.Sort(static (l, r) => string.CompareOrdinal(l.ComparisonKey, r.ComparisonKey));
if (!string.IsNullOrEmpty(purl))
{
writer.AddFromPurl(
analyzerId: Id,
purl: purl,
name: module.Path,
version: module.Version,
type: "golang",
metadata: metadata,
evidence: evidence,
usedByEntrypoint: false);
}
else
{
writer.AddFromExplicitKey(
analyzerId: Id,
componentKey: $"golang::source::{module.Path}@{module.Version}",
purl: null,
name: module.Path,
version: module.Version,
type: "golang",
metadata: metadata,
evidence: evidence);
}
}
private void EmitComponents(GoBuildInfo buildInfo, LanguageAnalyzerContext context, LanguageComponentWriter writer, HashSet<string> emittedModules)
{
var components = new List<GoModule> { buildInfo.MainModule };
components.AddRange(buildInfo.Dependencies
@@ -61,6 +334,10 @@ public sealed class GoLanguageAnalyzer : ILanguageAnalyzer
foreach (var module in components)
{
// Track emitted modules (binary evidence is more accurate than source)
var moduleKey = $"{module.Path}@{module.Version ?? "(devel)"}";
emittedModules.Add(moduleKey);
var metadata = BuildMetadata(buildInfo, module, binaryRelativePath);
var evidence = BuildEvidence(buildInfo, module, binaryRelativePath, context, ref binaryHash);
var usedByEntrypoint = module.IsMain && context.UsageHints.IsPathUsed(buildInfo.AbsoluteBinaryPath);