Files
git.stella-ops.org/src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.OS.Dpkg/DpkgPackageAnalyzer.cs
StellaOps Bot 564df71bfb
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
up
2025-12-13 00:20:26 +02:00

269 lines
9.2 KiB
C#

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;
using StellaOps.Scanner.Core.Contracts;
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<ExecutionResult> 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(ExecutionResult.FromPackages(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>();
var evidenceFactory = OsFileEvidenceFactory.Create(context.RootPath, context.Metadata);
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, evidenceFactory, 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(ExecutionResult.FromPackages(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,
OsFileEvidenceFactory evidenceFactory,
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 => evidenceFactory.Create(builder.Path, builder.IsConfig, builder.Digests))
.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);
}
}