From 156cd86444e15eddd1cfa897fdc97f3406056bf2 Mon Sep 17 00:00:00 2001 From: David Broadlick Date: Tue, 28 Apr 2026 17:09:51 -0500 Subject: [PATCH] Populate conda-lock md5 and sha256 in scan manifests - Add optional Sha256 on CondaComponent (JSON sha256) and extend ctor with md5/sha256 from lockfile hash map - Add optional Md5 and Sha256 on PipComponent for pip-managed conda-lock entries; include digests in component id when present - CondaDependencyResolver: TryGetHash reads per-package hash.md5 / hash.sha256 from conda-lock.yml - Extend CondaLock detector tests to assert digest values from fixture Made-with: Cursor --- .../TypedComponent/CondaComponent.cs | 9 +++- .../TypedComponent/PipComponent.cs | 23 +++++++++- .../conda/CondaDependencyResolver.cs | 44 +++++++++++++++++-- .../CondaLockComponentDetectorTests.cs | 26 +++++++++++ 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CondaComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CondaComponent.cs index 247e02eae..1c6849d3b 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CondaComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/CondaComponent.cs @@ -5,7 +5,7 @@ namespace Microsoft.ComponentDetection.Contracts.TypedComponent; public class CondaComponent : TypedComponent { - public CondaComponent(string name, string version, string build, string channel, string subdir, string @namespace, string url, string md5) + public CondaComponent(string name, string version, string build, string channel, string subdir, string @namespace, string url, string md5, string sha256 = null) { this.Name = this.ValidateRequiredInput(name, nameof(this.Name), nameof(ComponentType.Conda)); this.Version = this.ValidateRequiredInput(version, nameof(this.Version), nameof(ComponentType.Conda)); @@ -15,6 +15,7 @@ public CondaComponent(string name, string version, string build, string channel, this.Namespace = @namespace; this.Url = url; this.MD5 = md5; + this.Sha256 = sha256; } public CondaComponent() @@ -46,8 +47,12 @@ public CondaComponent() [JsonPropertyName("mD5")] public string MD5 { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sha256")] + public string Sha256 { get; set; } + [JsonIgnore] public override ComponentType Type => ComponentType.Conda; - protected override string ComputeBaseId() => $"{this.Name} {this.Version} {this.Build} {this.Channel} {this.Subdir} {this.Namespace} {this.Url} {this.MD5} - {this.Type}"; + protected override string ComputeBaseId() => $"{this.Name} {this.Version} {this.Build} {this.Channel} {this.Subdir} {this.Namespace} {this.Url} {this.MD5} {this.Sha256} - {this.Type}"; } diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PipComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PipComponent.cs index 36bb04a4c..7ee228954 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PipComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PipComponent.cs @@ -34,6 +34,14 @@ public PipComponent(string name, string version, string author = null, string li [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("license")] public string? License { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("mD5")] + public string? Md5 { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sha256")] + public string? Sha256 { get; set; } #nullable disable [JsonIgnore] @@ -43,5 +51,18 @@ public PipComponent(string name, string version, string author = null, string li public override PackageURL PackageUrl => new PackageURL("pypi", null, this.Name, this.Version, null, null); [SuppressMessage("Usage", "CA1308:Normalize String to Uppercase", Justification = "Casing cannot be overwritten.")] - protected override string ComputeBaseId() => $"{this.Name} {this.Version} - {this.Type}".ToLowerInvariant(); + protected override string ComputeBaseId() + { + var digestSuffix = string.Empty; + if (!string.IsNullOrEmpty(this.Sha256)) + { + digestSuffix = $" {this.Sha256}"; + } + else if (!string.IsNullOrEmpty(this.Md5)) + { + digestSuffix = $" {this.Md5}"; + } + + return $"{this.Name} {this.Version}{digestSuffix} - {this.Type}".ToLowerInvariant(); + } } diff --git a/src/Microsoft.ComponentDetection.Detectors/conda/CondaDependencyResolver.cs b/src/Microsoft.ComponentDetection.Detectors/conda/CondaDependencyResolver.cs index c84637640..923647ddf 100644 --- a/src/Microsoft.ComponentDetection.Detectors/conda/CondaDependencyResolver.cs +++ b/src/Microsoft.ComponentDetection.Detectors/conda/CondaDependencyResolver.cs @@ -122,9 +122,47 @@ private static List GetPackages(CondaLock condaLock) /// The CondaPackage to convert. /// The TypedComponent. private static TypedComponent CreateComponent(CondaPackage package) - => IsPythonPackage(package) - ? new PipComponent(package.Name, package.Version) - : new CondaComponent(package.Name, package.Version, null, package.Category, null, null, null, null); + { + var md5 = TryGetHash(package.Hash, "md5"); + var sha256 = TryGetHash(package.Hash, "sha256"); + + if (IsPythonPackage(package)) + { + return new PipComponent(package.Name, package.Version) + { + Md5 = md5, + Sha256 = sha256, + }; + } + + return new CondaComponent(package.Name, package.Version, null, package.Category, null, null, null, md5, sha256); + } + + /// + /// Reads a digest value from conda-lock's per-package hash map (YAML keys are typically lowercase md5 / sha256). + /// + private static string TryGetHash(Dictionary hash, string key) + { + if (hash is null || key is null) + { + return null; + } + + if (hash.TryGetValue(key, out var exact) && !string.IsNullOrEmpty(exact)) + { + return exact; + } + + foreach (var kv in hash) + { + if (string.Equals(kv.Key, key, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(kv.Value)) + { + return kv.Value; + } + } + + return null; + } /// /// Checks if a package is a python package. diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/CondaLockComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/CondaLockComponentDetectorTests.cs index decb0b063..0e6c171f6 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/CondaLockComponentDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/CondaLockComponentDetectorTests.cs @@ -102,6 +102,32 @@ public async Task CondaComponentDetector_TestCondaLockFileAsync() this.AssertPipComponentNameAndVersion(detectedComponents, "requests", "2.31.0"); detectedComponents.Should().HaveCount(4); + + var condaLockTop = detectedComponents.Single(c => + c.Component is CondaComponent cl && + cl.Name.Equals("conda-lock") && + cl.Version.Equals("2.1.0")).Component as CondaComponent; + condaLockTop!.MD5.Should().Be("1e07afcf3d3e371fc3a3681fe9b78e90"); + condaLockTop.Sha256.Should().Be("05319e84cbd36f6a05563954d2dbff041de6ece406a59650784918026080c98c"); + + var urllib = detectedComponents.Single(c => + c.Component is CondaComponent u && + u.Name.Equals("urllib3") && + u.Version.Equals("1.26.16")).Component as CondaComponent; + urllib!.MD5.Should().Be("4b62a74f7e797800039971833968e23f"); + urllib.Sha256.Should().Be("b9e919a9bcb4cb291fe60952895bf0c3ce9dbcbeaa3d5706131f862756fabc40"); + + var requests = detectedComponents.Single(c => + c.Component is PipComponent r && + r.Name.Equals("requests") && + r.Version.Equals("2.31.0")).Component as PipComponent; + requests!.Sha256.Should().Be("58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"); + + var certifi = detectedComponents.Single(c => + c.Component is PipComponent cf && + cf.Name.Equals("certifi") && + cf.Version.Equals("2023.5.7")).Component as PipComponent; + certifi!.Sha256.Should().Be("c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"); } private void AssertCondaLockComponentNameAndVersion(IEnumerable detectedComponents, string name, string version)