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,267 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.Analyzers.OS;
using StellaOps.Scanner.Analyzers.OS.Abstractions;
using StellaOps.Scanner.Analyzers.OS.Analyzers;
using StellaOps.Scanner.Analyzers.OS.Helpers;
namespace StellaOps.Scanner.Analyzers.OS.Dpkg;
internal sealed class DpkgPackageAnalyzer : OsPackageAnalyzerBase
{
private static readonly IReadOnlyList<OSPackageRecord> EmptyPackages =
new ReadOnlyCollection<OSPackageRecord>(System.Array.Empty<OSPackageRecord>());
private readonly DpkgStatusParser _parser = new();
public DpkgPackageAnalyzer(ILogger<DpkgPackageAnalyzer> logger)
: base(logger)
{
}
public override string AnalyzerId => "dpkg";
protected override ValueTask<IReadOnlyList<OSPackageRecord>> ExecuteCoreAsync(OSPackageAnalyzerContext context, CancellationToken cancellationToken)
{
var statusPath = Path.Combine(context.RootPath, "var", "lib", "dpkg", "status");
if (!File.Exists(statusPath))
{
Logger.LogInformation("dpkg status file not found at {Path}; skipping analyzer.", statusPath);
return ValueTask.FromResult<IReadOnlyList<OSPackageRecord>>(EmptyPackages);
}
using var stream = File.OpenRead(statusPath);
var entries = _parser.Parse(stream, cancellationToken);
var infoDirectory = Path.Combine(context.RootPath, "var", "lib", "dpkg", "info");
var records = new List<OSPackageRecord>();
foreach (var entry in entries)
{
if (!IsInstalled(entry.Status))
{
continue;
}
if (string.IsNullOrWhiteSpace(entry.Name) || string.IsNullOrWhiteSpace(entry.Version) || string.IsNullOrWhiteSpace(entry.Architecture))
{
continue;
}
var versionParts = PackageVersionParser.ParseDebianVersion(entry.Version);
var sourceName = ParseSource(entry.Source) ?? entry.Name;
var distribution = entry.Origin;
if (distribution is null && entry.Metadata.TryGetValue("origin", out var originValue))
{
distribution = originValue;
}
distribution ??= "debian";
var purl = PackageUrlBuilder.BuildDebian(distribution!, entry.Name, entry.Version, entry.Architecture);
var vendorMetadata = new Dictionary<string, string?>(StringComparer.Ordinal)
{
["source"] = entry.Source,
["homepage"] = entry.Homepage,
["maintainer"] = entry.Maintainer,
["origin"] = entry.Origin,
["priority"] = entry.Priority,
["section"] = entry.Section,
};
foreach (var kvp in entry.Metadata)
{
vendorMetadata[$"dpkg:{kvp.Key}"] = kvp.Value;
}
var dependencies = entry.Depends.Concat(entry.PreDepends).ToArray();
var provides = entry.Provides.ToArray();
var fileEvidence = BuildFileEvidence(infoDirectory, entry, cancellationToken);
var cveHints = CveHintExtractor.Extract(entry.Description, string.Join(' ', dependencies), string.Join(' ', provides));
var record = new OSPackageRecord(
AnalyzerId,
purl,
entry.Name,
versionParts.UpstreamVersion,
entry.Architecture,
PackageEvidenceSource.DpkgStatus,
epoch: versionParts.Epoch,
release: versionParts.Revision,
sourcePackage: sourceName,
license: entry.License,
cveHints: cveHints,
provides: provides,
depends: dependencies,
files: fileEvidence,
vendorMetadata: vendorMetadata);
records.Add(record);
}
records.Sort();
return ValueTask.FromResult<IReadOnlyList<OSPackageRecord>>(records);
}
private static bool IsInstalled(string? status)
=> status?.Contains("install ok installed", System.StringComparison.OrdinalIgnoreCase) == true;
private static string? ParseSource(string? sourceField)
{
if (string.IsNullOrWhiteSpace(sourceField))
{
return null;
}
var parts = sourceField.Split(' ', 2, System.StringSplitOptions.TrimEntries | System.StringSplitOptions.RemoveEmptyEntries);
return parts.Length == 0 ? null : parts[0];
}
private static IReadOnlyList<OSPackageFileEvidence> BuildFileEvidence(string infoDirectory, DpkgPackageEntry entry, CancellationToken cancellationToken)
{
if (!Directory.Exists(infoDirectory))
{
return Array.Empty<OSPackageFileEvidence>();
}
var files = new Dictionary<string, FileEvidenceBuilder>(StringComparer.Ordinal);
void EnsureFile(string path)
{
if (!files.TryGetValue(path, out _))
{
files[path] = new FileEvidenceBuilder(path);
}
}
foreach (var conffile in entry.Conffiles)
{
var normalized = conffile.Path.Trim();
if (string.IsNullOrWhiteSpace(normalized))
{
continue;
}
EnsureFile(normalized);
files[normalized].IsConfig = true;
if (!string.IsNullOrWhiteSpace(conffile.Checksum))
{
files[normalized].Digests["md5"] = conffile.Checksum.Trim();
}
}
foreach (var candidate in GetInfoFileCandidates(entry.Name!, entry.Architecture!))
{
var listPath = Path.Combine(infoDirectory, candidate + ".list");
if (File.Exists(listPath))
{
foreach (var line in File.ReadLines(listPath))
{
cancellationToken.ThrowIfCancellationRequested();
var trimmed = line.Trim();
if (string.IsNullOrWhiteSpace(trimmed))
{
continue;
}
EnsureFile(trimmed);
}
}
var confFilePath = Path.Combine(infoDirectory, candidate + ".conffiles");
if (File.Exists(confFilePath))
{
foreach (var line in File.ReadLines(confFilePath))
{
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
var parts = line.Split(' ', System.StringSplitOptions.RemoveEmptyEntries | System.StringSplitOptions.TrimEntries);
if (parts.Length == 0)
{
continue;
}
var path = parts[0];
EnsureFile(path);
files[path].IsConfig = true;
if (parts.Length >= 2)
{
files[path].Digests["md5"] = parts[1];
}
}
}
var md5sumsPath = Path.Combine(infoDirectory, candidate + ".md5sums");
if (File.Exists(md5sumsPath))
{
foreach (var line in File.ReadLines(md5sumsPath))
{
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
var parts = line.Split(' ', 2, System.StringSplitOptions.RemoveEmptyEntries | System.StringSplitOptions.TrimEntries);
if (parts.Length != 2)
{
continue;
}
var hash = parts[0];
var path = parts[1];
EnsureFile(path);
files[path].Digests["md5"] = hash;
}
}
}
if (files.Count == 0)
{
return Array.Empty<OSPackageFileEvidence>();
}
var evidence = files.Values
.Select(builder => builder.ToEvidence())
.OrderBy(e => e)
.ToArray();
return new ReadOnlyCollection<OSPackageFileEvidence>(evidence);
}
private static IEnumerable<string> GetInfoFileCandidates(string packageName, string architecture)
{
yield return packageName + ":" + architecture;
yield return packageName;
}
private sealed class FileEvidenceBuilder
{
public FileEvidenceBuilder(string path)
{
Path = path;
}
public string Path { get; }
public bool IsConfig { get; set; }
public Dictionary<string, string> Digests { get; } = new(StringComparer.OrdinalIgnoreCase);
public OSPackageFileEvidence ToEvidence()
{
return new OSPackageFileEvidence(Path, isConfigFile: IsConfig, digests: Digests);
}
}
}