Skip to content

Commit 884c12e

Browse files
authored
Add LicenseConcluded and Suppliers to ScannedComponent (#1684)
* Add LicenseConcluded and Suppliers to ScannedComponent * further updates * test update * remove duped * update comment * pr feedback * update casing * pr feedback
1 parent 7096286 commit 884c12e

File tree

10 files changed

+758
-2
lines changed

10 files changed

+758
-2
lines changed

docs/schema/manifest.schema.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,27 @@
364364
"null"
365365
]
366366
}
367+
},
368+
"licensesConcluded": {
369+
"type": [
370+
"array",
371+
"null"
372+
],
373+
"items": {
374+
"type": [
375+
"string",
376+
"null"
377+
]
378+
}
379+
},
380+
"suppliers": {
381+
"type": [
382+
"array",
383+
"null"
384+
],
385+
"items": {
386+
"$ref": "#/definitions/ActorInfo"
387+
}
367388
}
368389
},
369390
"required": [

src/Microsoft.ComponentDetection.Common/DependencyGraph/ComponentRecorder.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,38 @@ public IEnumerable<DetectedComponent> GetDetectedComponents()
4848
.GroupBy(x => x.Component.Id)
4949
.Select(grouping =>
5050
{
51-
// We pick a winner here -- any stateful props could get lost at this point. Only stateful prop still outstanding is ContainerDetails.
51+
// We pick a winner here -- any stateful props could get lost at this point.
5252
var winningDetectedComponent = grouping.First();
53+
54+
HashSet<string> mergedLicenses = null;
55+
HashSet<ActorInfo> mergedSuppliers = null;
56+
5357
foreach (var component in grouping.Skip(1))
5458
{
5559
winningDetectedComponent.ContainerDetailIds.UnionWith(component.ContainerDetailIds);
60+
61+
// Defensive: merge in case different file recorders set different values for the same component.
62+
if (component.LicensesConcluded != null)
63+
{
64+
mergedLicenses ??= new HashSet<string>(winningDetectedComponent.LicensesConcluded ?? [], StringComparer.OrdinalIgnoreCase);
65+
mergedLicenses.UnionWith(component.LicensesConcluded);
66+
}
67+
68+
if (component.Suppliers != null)
69+
{
70+
mergedSuppliers ??= new HashSet<ActorInfo>(winningDetectedComponent.Suppliers ?? []);
71+
mergedSuppliers.UnionWith(component.Suppliers);
72+
}
73+
}
74+
75+
if (mergedLicenses != null)
76+
{
77+
winningDetectedComponent.LicensesConcluded = mergedLicenses.Where(x => x != null).OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList();
78+
}
79+
80+
if (mergedSuppliers != null)
81+
{
82+
winningDetectedComponent.Suppliers = mergedSuppliers.Where(s => s != null).OrderBy(s => s.Name).ThenBy(s => s.Type).ToList();
5683
}
5784

5885
return winningDetectedComponent;

src/Microsoft.ComponentDetection.Contracts/BcdeModels/ScannedComponent.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,14 @@ public class ScannedComponent
4040

4141
[JsonPropertyName("targetFrameworks")]
4242
public ISet<string> TargetFrameworks { get; set; }
43+
44+
[JsonProperty("licensesConcluded", NullValueHandling = NullValueHandling.Ignore)]
45+
[JsonPropertyName("licensesConcluded")]
46+
[System.Text.Json.Serialization.JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
47+
public IList<string> LicensesConcluded { get; set; }
48+
49+
[JsonProperty("suppliers", NullValueHandling = NullValueHandling.Ignore)]
50+
[JsonPropertyName("suppliers")]
51+
[System.Text.Json.Serialization.JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
52+
public IList<TypedComponent.ActorInfo> Suppliers { get; set; }
4353
}

src/Microsoft.ComponentDetection.Contracts/DetectedComponent.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ public DetectedComponent(TypedComponent.TypedComponent component, IComponentDete
6868
/// <summary> Gets Target Frameworks where the component was consumed.</summary>
6969
public ConcurrentHashSet<string> TargetFrameworks { get; set; }
7070

71+
/// <summary>Gets or sets Licenses resolved/concluded (e.g., via ClearlyDefined or curation).</summary>
72+
public IList<string> LicensesConcluded { get; set; }
73+
74+
/// <summary>Gets or sets the entity/entities that supplied/published this component.</summary>
75+
public IList<TypedComponent.ActorInfo> Suppliers { get; set; }
76+
7177
private string DebuggerDisplay => $"{this.Component.DebuggerDisplay}";
7278

7379
/// <summary>Adds a filepath to the FilePaths hashset for this detected component.

src/Microsoft.ComponentDetection.Contracts/TypedComponent/ActorInfo.cs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Microsoft.ComponentDetection.Contracts.TypedComponent;
1111
/// Aligned with SPDX 3.0.1 Agent subclasses.
1212
/// </summary>
1313
[JsonObject(MemberSerialization.OptOut, NamingStrategyType = typeof(CamelCaseNamingStrategy))]
14-
public class ActorInfo
14+
public class ActorInfo : IEquatable<ActorInfo>
1515
{
1616
[SystemTextJson.JsonPropertyName("name")]
1717
[SystemTextJson.JsonIgnore(Condition = SystemTextJson.JsonIgnoreCondition.WhenWritingNull)]
@@ -35,4 +35,46 @@ public class ActorInfo
3535
[SystemTextJson.JsonIgnore(Condition = SystemTextJson.JsonIgnoreCondition.WhenWritingNull)]
3636
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
3737
public string? Type { get; set; }
38+
39+
/// <inheritdoc/>
40+
public bool Equals(ActorInfo? other)
41+
{
42+
if (other is null)
43+
{
44+
return false;
45+
}
46+
47+
if (ReferenceEquals(this, other))
48+
{
49+
return true;
50+
}
51+
52+
return string.Equals(this.Name, other.Name, StringComparison.OrdinalIgnoreCase)
53+
&& string.Equals(this.Email, other.Email, StringComparison.OrdinalIgnoreCase)
54+
&& this.Url == other.Url
55+
&& string.Equals(this.Type, other.Type, StringComparison.OrdinalIgnoreCase);
56+
}
57+
58+
/// <inheritdoc/>
59+
public override bool Equals(object? obj) => this.Equals(obj as ActorInfo);
60+
61+
/// <summary>
62+
/// Included so that merge operations and collections that rely on hash codes (e.g., HashSet)
63+
/// will operate on ActorInfo instances based on value equality rather than reference equality.
64+
/// Uses multiply-and-add (seed 17, factor 31) with <see cref="StringComparer.OrdinalIgnoreCase"/>
65+
/// to stay consistent with the case-insensitive <see cref="Equals(ActorInfo)"/>.
66+
/// </summary>
67+
/// <returns>A hash code consistent with value-based equality.</returns>
68+
public override int GetHashCode()
69+
{
70+
unchecked
71+
{
72+
var hash = 17;
73+
hash = (hash * 31) + StringComparer.OrdinalIgnoreCase.GetHashCode(this.Name ?? string.Empty);
74+
hash = (hash * 31) + StringComparer.OrdinalIgnoreCase.GetHashCode(this.Email ?? string.Empty);
75+
hash = (hash * 31) + (this.Url?.GetHashCode() ?? 0);
76+
hash = (hash * 31) + StringComparer.OrdinalIgnoreCase.GetHashCode(this.Type ?? string.Empty);
77+
return hash;
78+
}
79+
}
3880
}

src/Microsoft.ComponentDetection.Orchestrator/Services/GraphTranslation/DefaultGraphTranslationService.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,9 @@ private DetectedComponent MergeComponents(IEnumerable<DetectedComponent> enumera
215215
// Multiple detected components for the same logical component id -- this happens when different files see the same component. This code should go away when we get all
216216
// mutable data out of detected component -- we can just take any component.
217217
var firstComponent = enumerable.First();
218+
HashSet<string> mergedLicenses = null;
219+
HashSet<ActorInfo> mergedSuppliers = null;
220+
218221
foreach (var nextComponent in enumerable.Skip(1))
219222
{
220223
foreach (var filePath in nextComponent.FilePaths ?? Enumerable.Empty<string>())
@@ -239,6 +242,28 @@ private DetectedComponent MergeComponents(IEnumerable<DetectedComponent> enumera
239242
}
240243

241244
firstComponent.TargetFrameworks = MergeTargetFrameworks(firstComponent.TargetFrameworks, nextComponent.TargetFrameworks);
245+
246+
if (nextComponent.LicensesConcluded != null)
247+
{
248+
mergedLicenses ??= new HashSet<string>(firstComponent.LicensesConcluded ?? [], StringComparer.OrdinalIgnoreCase);
249+
mergedLicenses.UnionWith(nextComponent.LicensesConcluded);
250+
}
251+
252+
if (nextComponent.Suppliers != null)
253+
{
254+
mergedSuppliers ??= new HashSet<ActorInfo>(firstComponent.Suppliers ?? []);
255+
mergedSuppliers.UnionWith(nextComponent.Suppliers);
256+
}
257+
}
258+
259+
if (mergedLicenses != null)
260+
{
261+
firstComponent.LicensesConcluded = mergedLicenses.Where(x => x != null).OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList();
262+
}
263+
264+
if (mergedSuppliers != null)
265+
{
266+
firstComponent.Suppliers = mergedSuppliers.Where(s => s != null).OrderBy(s => s.Name).ThenBy(s => s.Type).ToList();
242267
}
243268

244269
return firstComponent;
@@ -316,6 +341,8 @@ private ScannedComponent ConvertToContract(DetectedComponent component)
316341
ContainerDetailIds = component.ContainerDetailIds,
317342
ContainerLayerIds = component.ContainerLayerIds,
318343
TargetFrameworks = component.TargetFrameworks?.ToHashSet(),
344+
LicensesConcluded = component.LicensesConcluded,
345+
Suppliers = component.Suppliers,
319346
};
320347
}
321348
}

0 commit comments

Comments
 (0)