-
Notifications
You must be signed in to change notification settings - Fork 112
Initial RPM database detector #1534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| namespace Microsoft.ComponentDetection.Common; | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
|
|
||
| /// <summary> | ||
| /// Represents Linux distribution information parsed from /etc/os-release or /usr/lib/os-release. | ||
| /// </summary> | ||
| public sealed class LinuxDistribution | ||
| { | ||
| /// <summary> | ||
| /// Gets the lower-case operating system identifier (e.g., "ubuntu", "rhel", "fedora"). | ||
| /// </summary> | ||
| public string Id { get; init; } | ||
|
|
||
| /// <summary> | ||
| /// Gets the operating system version number or identifier. | ||
| /// </summary> | ||
| public string VersionId { get; init; } | ||
|
|
||
| /// <summary> | ||
| /// Gets the operating system name without version information. | ||
| /// </summary> | ||
| public string Name { get; init; } | ||
|
|
||
| /// <summary> | ||
| /// Gets a human-readable operating system name with version. | ||
| /// </summary> | ||
| public string PrettyName { get; init; } | ||
|
|
||
| /// <summary> | ||
| /// Parses an os-release file content and returns a LinuxDistribution object. | ||
| /// The os-release format is defined at https://www.freedesktop.org/software/systemd/man/os-release.html. | ||
| /// </summary> | ||
| /// <param name="content">The content of the os-release file.</param> | ||
| /// <returns>A LinuxDistribution object or null if parsing fails.</returns> | ||
| public static LinuxDistribution ParseOsRelease(string content) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(content)) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); | ||
|
|
||
| foreach (var line in content.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)) | ||
| { | ||
| var trimmedLine = line.Trim(); | ||
|
|
||
| // Skip comments and empty lines | ||
| if (string.IsNullOrEmpty(trimmedLine) || trimmedLine.StartsWith('#')) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var parts = trimmedLine.Split('=', 2); | ||
| if (parts.Length != 2) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var key = parts[0].Trim(); | ||
| var value = parts[1].Trim(); | ||
|
|
||
| // Remove quotes if present | ||
| if ( | ||
| value.Length >= 2 | ||
| && ( | ||
| (value.StartsWith('\"') && value.EndsWith('\"')) | ||
| || (value.StartsWith('\'') && value.EndsWith('\'')) | ||
| ) | ||
| ) | ||
| { | ||
| value = value[1..^1]; | ||
| } | ||
|
|
||
| values[key] = value; | ||
| } | ||
|
|
||
| // At minimum, we need an ID field | ||
| if (!values.ContainsKey("ID")) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| return new LinuxDistribution | ||
| { | ||
| Id = values.GetValueOrDefault("ID"), | ||
| VersionId = values.GetValueOrDefault("VERSION_ID"), | ||
| Name = values.GetValueOrDefault("NAME"), | ||
| PrettyName = values.GetValueOrDefault("PRETTY_NAME"), | ||
| }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,269 @@ | ||||||||||||||||||||||||||||||||||||||||||
| namespace Microsoft.ComponentDetection.Common; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| using System; | ||||||||||||||||||||||||||||||||||||||||||
| using System.Collections.Generic; | ||||||||||||||||||||||||||||||||||||||||||
| using System.IO; | ||||||||||||||||||||||||||||||||||||||||||
| using System.Runtime.InteropServices; | ||||||||||||||||||||||||||||||||||||||||||
| using System.Threading; | ||||||||||||||||||||||||||||||||||||||||||
| using System.Threading.Tasks; | ||||||||||||||||||||||||||||||||||||||||||
| using Microsoft.ComponentDetection.Contracts; | ||||||||||||||||||||||||||||||||||||||||||
| using Microsoft.ComponentDetection.Contracts.Internal; | ||||||||||||||||||||||||||||||||||||||||||
| using Microsoft.ComponentDetection.Contracts.TypedComponent; | ||||||||||||||||||||||||||||||||||||||||||
| using Microsoft.Extensions.Logging; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||
| /// Abstract base class for system package detectors (RPM, APK, DPKG, etc.). | ||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||
| public abstract class SystemPackageDetector : FileComponentDetector | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| /// <inheritdoc /> | ||||||||||||||||||||||||||||||||||||||||||
| protected override async Task OnFileFoundAsync( | ||||||||||||||||||||||||||||||||||||||||||
| ProcessRequest processRequest, | ||||||||||||||||||||||||||||||||||||||||||
| IDictionary<string, string> detectorArgs, | ||||||||||||||||||||||||||||||||||||||||||
| CancellationToken cancellationToken = default | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| // Only run on Linux | ||||||||||||||||||||||||||||||||||||||||||
| if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| this.Logger.LogDebug("Skipping {DetectorId} - not running on Linux", this.Id); | ||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| var file = processRequest.ComponentStream; | ||||||||||||||||||||||||||||||||||||||||||
| var recorder = processRequest.SingleFileComponentRecorder; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| try | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| // Find the Linux distribution | ||||||||||||||||||||||||||||||||||||||||||
| var distro = await this.FindDistributionAsync().ConfigureAwait(false); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (distro == null) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| this.Logger.LogWarning( | ||||||||||||||||||||||||||||||||||||||||||
| "Could not determine Linux distribution for {FilePath}, using 'linux' as default namespace", | ||||||||||||||||||||||||||||||||||||||||||
| file.Location | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Parse packages from the database | ||||||||||||||||||||||||||||||||||||||||||
| var packages = await this.ParsePackagesAsync(file.Stream, file.Location, distro) | ||||||||||||||||||||||||||||||||||||||||||
| .ConfigureAwait(false); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (packages.Count == 0) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| this.Logger.LogDebug("No packages found in {FilePath}", file.Location); | ||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Build dependency graph and register components | ||||||||||||||||||||||||||||||||||||||||||
| this.BuildDependencyGraph(packages, recorder, distro); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| catch (Exception ex) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| this.Logger.LogError( | ||||||||||||||||||||||||||||||||||||||||||
| ex, | ||||||||||||||||||||||||||||||||||||||||||
| "Error processing system package database at {FilePath}", | ||||||||||||||||||||||||||||||||||||||||||
| file.Location | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| throw; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||
| /// Parses packages from the system package database. | ||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||
| /// <param name="dbStream">The database file stream.</param> | ||||||||||||||||||||||||||||||||||||||||||
| /// <param name="location">The location of the database file.</param> | ||||||||||||||||||||||||||||||||||||||||||
| /// <param name="distro">The detected Linux distribution.</param> | ||||||||||||||||||||||||||||||||||||||||||
| /// <returns>A list of parsed package information.</returns> | ||||||||||||||||||||||||||||||||||||||||||
| protected abstract Task<List<SystemPackageInfo>> ParsePackagesAsync( | ||||||||||||||||||||||||||||||||||||||||||
| Stream dbStream, | ||||||||||||||||||||||||||||||||||||||||||
| string location, | ||||||||||||||||||||||||||||||||||||||||||
| LinuxDistribution distro | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||
| /// Creates a TypedComponent from system package information. | ||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||
| /// <param name="package">The package information.</param> | ||||||||||||||||||||||||||||||||||||||||||
| /// <param name="distro">The Linux distribution.</param> | ||||||||||||||||||||||||||||||||||||||||||
| /// <returns>A TypedComponent representing the package.</returns> | ||||||||||||||||||||||||||||||||||||||||||
| protected abstract TypedComponent CreateComponent( | ||||||||||||||||||||||||||||||||||||||||||
| SystemPackageInfo package, | ||||||||||||||||||||||||||||||||||||||||||
| LinuxDistribution distro | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||||||||||||||||||||||
| /// Finds the Linux distribution by looking for os-release files relative to the database location. | ||||||||||||||||||||||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||||||||||||||||||||||
| /// <returns>A LinuxDistribution object or null if not found.</returns> | ||||||||||||||||||||||||||||||||||||||||||
| protected virtual async Task<LinuxDistribution> FindDistributionAsync() | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| // Try common os-release locations relative to the database | ||||||||||||||||||||||||||||||||||||||||||
| var possiblePaths = new[] { "/etc/os-release", "/usr/lib/os-release" }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| foreach (var path in possiblePaths) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| try | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| if (File.Exists(path)) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| var content = await File.ReadAllTextAsync(path).ConfigureAwait(false); | ||||||||||||||||||||||||||||||||||||||||||
| var distro = LinuxDistribution.ParseOsRelease(content); | ||||||||||||||||||||||||||||||||||||||||||
| if (distro is not null) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| this.Logger.LogDebug( | ||||||||||||||||||||||||||||||||||||||||||
| "Found Linux distribution: {Id} {VersionId} at {Path}", | ||||||||||||||||||||||||||||||||||||||||||
| distro.Id, | ||||||||||||||||||||||||||||||||||||||||||
| distro.VersionId, | ||||||||||||||||||||||||||||||||||||||||||
| path | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| return distro; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| catch (Exception ex) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| this.Logger.LogTrace(ex, "Failed to read os-release file at {Path}", path); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+126
to
+129
|
||||||||||||||||||||||||||||||||||||||||||
| catch (Exception ex) | |
| { | |
| this.Logger.LogTrace(ex, "Failed to read os-release file at {Path}", path); | |
| } | |
| catch (IOException ex) | |
| { | |
| this.Logger.LogTrace(ex, "Failed to read os-release file at {Path}", path); | |
| } | |
| catch (UnauthorizedAccessException ex) | |
| { | |
| this.Logger.LogTrace(ex, "Failed to read os-release file at {Path}", path); | |
| } | |
| catch (FormatException ex) | |
| { | |
| this.Logger.LogTrace(ex, "Failed to parse os-release file at {Path}", path); | |
| } | |
| catch (InvalidOperationException ex) | |
| { | |
| this.Logger.LogTrace(ex, "Failed to parse os-release file at {Path}", path); | |
| } |
Copilot
AI
Nov 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The dependency relationship is inverted. When package A requires capability X (provided by package B), package B is a dependency of package A. Therefore, B should be registered with parentComponentId: A.Id, not the other way around.
Current code registers:
recorder.RegisterUsage(childComponent, // pkg that requires
isExplicitReferencedDependency: false,
parentComponentId: parentComponent.Component.Id); // providerIt should be:
recorder.RegisterUsage(parentComponent, // provider (the dependency)
isExplicitReferencedDependency: false,
parentComponentId: childComponent.Component.Id); // pkg that requires (the parent)This follows the pattern used in other detectors where dependencies are registered with their dependent as the parent.
| childComponent, | |
| isExplicitReferencedDependency: false, | |
| parentComponentId: parentComponent.Component.Id | |
| parentComponent, | |
| isExplicitReferencedDependency: false, | |
| parentComponentId: childComponent.Component.Id |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,4 +62,7 @@ public enum ComponentType : byte | |
|
|
||
| [EnumMember] | ||
| DotNet = 19, | ||
|
|
||
| [EnumMember] | ||
| Rpm = 20, | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.