Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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()
Expand Down Expand Up @@ -46,8 +47,12 @@ public CondaComponent()
[JsonPropertyName("mD5")]
public string MD5 { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("sha256")]
public string Sha256 { get; set; }
Comment on lines 48 to +52
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sha256 is ignored when null, but MD5 is still always serialized. If MD5 is missing for a package, this will emit "mD5": null in scan manifests, which is inconsistent with the new sha256 behavior and may create noisy diffs. Consider adding JsonIgnore(Condition = WhenWritingNull) to MD5 as well (or otherwise aligning null-handling for both digest fields).

Copilot uses AI. Check for mistakes.

[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}";
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ComputeBaseId() now always interpolates this.Sha256. When Sha256 is null/empty (e.g., callers like the Linux conda artifact factory don’t pass it), this changes the resulting component ID string (extra whitespace) compared to previous releases, which can break manifest diffing and component de-duplication. Consider building the ID from non-empty parts (or conditionally appending sha256 only when present) so IDs are stable when sha256 isn’t available.

Suggested change
protected override string ComputeBaseId() => $"{this.Name} {this.Version} {this.Build} {this.Channel} {this.Subdir} {this.Namespace} {this.Url} {this.MD5} {this.Sha256} - {this.Type}";
protected override string ComputeBaseId()
{
var baseIdWithoutSha256 = $"{this.Name} {this.Version} {this.Build} {this.Channel} {this.Subdir} {this.Namespace} {this.Url} {this.MD5}";
if (string.IsNullOrEmpty(this.Sha256))
{
return $"{baseIdWithoutSha256} - {this.Type}";
}
return $"{baseIdWithoutSha256} {this.Sha256} - {this.Type}";
}

Copilot uses AI. Check for mistakes.
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Comment on lines +38 to +44
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Md5/Sha256 are mutable (set;) but are also used to compute (and then cache) BaseId/Id. If these properties are ever set/changed after the component has been registered (or after Id has been accessed once), the cached ID won’t reflect the updated digest, causing inconsistent graph keys. To prevent this class of bugs, consider making these init-only or constructor-set so identity inputs can’t change after creation.

Copilot uses AI. Check for mistakes.
#nullable disable

[JsonIgnore]
Expand All @@ -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();
Comment on lines +56 to +66
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ComputeBaseId() now incorporates Sha256/Md5 into the Pip component identity. Since Component.Id is used as the stable key in manifests and dependency graphs, this is a breaking change: the same package/version can now produce different IDs depending on whether a digest is present, and verification tests that diff manifests across versions will fail. If the goal is to surface digests in output without changing identity semantics, consider keeping BaseId as name version - type and emitting digests as separate fields (or via extended ID properties if the ID format must include them).

Suggested change
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();
return $"{this.Name} {this.Version} - {this.Type}".ToLowerInvariant();

Copilot uses AI. Check for mistakes.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,47 @@ private static List<CondaPackage> GetPackages(CondaLock condaLock)
/// <param name="package">The CondaPackage to convert.</param>
/// <returns>The TypedComponent.</returns>
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);
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing md5/sha256 into CondaComponent will make the component ID platform/artifact-specific (hashes differ per platform/build), reintroducing the duplicate-component scenario the comment above is trying to avoid by omitting URL. If the intent is still to dedupe the same name/version across platforms, consider storing hashes as metadata fields but keeping them out of the identity used for Component.Id (or otherwise explicitly scoping identity changes to the desired cases).

Suggested change
return new CondaComponent(package.Name, package.Version, null, package.Category, null, null, null, md5, sha256);
return new CondaComponent(package.Name, package.Version, null, package.Category, null, null, null, null, null);

Copilot uses AI. Check for mistakes.
}

/// <summary>
/// Reads a digest value from conda-lock's per-package hash map (YAML keys are typically lowercase md5 / sha256).
/// </summary>
private static string TryGetHash(Dictionary<string, string> 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;
}

/// <summary>
/// Checks if a package is a python package.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DetectedComponent> detectedComponents, string name, string version)
Expand Down
Loading