Skip to content

Commit e1a765a

Browse files
authored
Merge branch 'main' into jupinzer/docker_compose_detector
2 parents a5a66f1 + 87ae1f8 commit e1a765a

4 files changed

Lines changed: 260 additions & 0 deletions

File tree

docs/detectors/spdx.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ The detector:
1919
- Document name
2020
- SPDX version
2121
- Root element ID from `documentDescribes` (defaults to `SPDXRef-Document` if not specified)
22+
- Creator tool from `creationInfo.creators` (e.g., `Tool: microsoft/sbom-tool-2.2.0`)
23+
- Creator organization from `creationInfo.creators` (e.g., `Organization: Microsoft`)
2224
- Creates an `SpdxComponent` to represent the SPDX document
2325

2426
The detector does not parse or register individual packages listed within the SPDX document; it only registers the SPDX document itself as a component.

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,15 @@ public SpdxComponent(string spdxVersion, Uri documentNamespace, string name, str
4242
[JsonPropertyName("path")]
4343
public string Path { get; set; }
4444

45+
#nullable enable
46+
[JsonPropertyName("creatorTool")]
47+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
48+
public string? CreatorTool { get; set; }
49+
50+
[JsonPropertyName("creatorOrganization")]
51+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
52+
public string? CreatorOrganization { get; set; }
53+
#nullable disable
54+
4555
protected override string ComputeBaseId() => $"{this.Name}-{this.SpdxVersion}-{this.Checksum}";
4656
}

src/Microsoft.ComponentDetection.Detectors/spdx/Spdx22ComponentDetector.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,42 @@ private SpdxComponent ConvertJsonElementToSbomComponent(ProcessRequest processRe
121121
var path = processRequest.ComponentStream.Location;
122122
var component = new SpdxComponent(spdxVersion, new Uri(sbomNamespace), name, fileHash, rootElementId, path);
123123

124+
if (document.TryGetProperty("creationInfo", out var creationInfoElement)
125+
&& creationInfoElement.TryGetProperty("creators", out var creatorsElement)
126+
&& creatorsElement.ValueKind == JsonValueKind.Array)
127+
{
128+
foreach (var creator in creatorsElement.EnumerateArray())
129+
{
130+
if (creator.ValueKind != JsonValueKind.String)
131+
{
132+
continue;
133+
}
134+
135+
var creatorString = creator.GetString();
136+
if (creatorString == null)
137+
{
138+
continue;
139+
}
140+
141+
if (component.CreatorTool == null && creatorString.StartsWith("Tool: ", StringComparison.Ordinal))
142+
{
143+
var tool = creatorString["Tool: ".Length..].Trim();
144+
if (!string.IsNullOrWhiteSpace(tool))
145+
{
146+
component.CreatorTool = tool;
147+
}
148+
}
149+
else if (component.CreatorOrganization == null && creatorString.StartsWith("Organization: ", StringComparison.Ordinal))
150+
{
151+
var org = creatorString["Organization: ".Length..].Trim();
152+
if (!string.IsNullOrWhiteSpace(org))
153+
{
154+
component.CreatorOrganization = org;
155+
}
156+
}
157+
}
158+
}
159+
124160
return component;
125161
}
126162

test/Microsoft.ComponentDetection.Detectors.Tests/SPDX22ComponentDetectorTests.cs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ public async Task TestSbomDetector_SimpleSbomAsync()
127127
sbomComponent.SpdxVersion.Should().Be("SPDX-2.2");
128128
sbomComponent.Checksum.Should().Be(checksum);
129129
sbomComponent.Path.Should().Be(Path.Combine(Path.GetTempPath(), spdxFileName));
130+
131+
sbomComponent.CreatorTool.Should().Be("Microsoft.SBOMTool-1.0.0");
132+
sbomComponent.CreatorOrganization.Should().Be("Microsoft");
130133
}
131134

132135
[TestMethod]
@@ -160,4 +163,213 @@ public async Task TestSbomDetector_InvalidFileAsync()
160163
var components = detectedComponents.ToList();
161164
components.Should().BeEmpty();
162165
}
166+
167+
[TestMethod]
168+
public async Task TestSbomDetector_ExtractsCreatorToolAndOrganizationAsync()
169+
{
170+
var spdxFile = /*lang=json,strict*/ @"{
171+
""spdxVersion"": ""SPDX-2.2"",
172+
""SPDXID"": ""SPDXRef-DOCUMENT"",
173+
""name"": ""TestDoc"",
174+
""documentNamespace"": ""https://sbom.microsoft/test/1.0.0/abc"",
175+
""creationInfo"": {
176+
""created"": ""2024-01-01T00:00:00Z"",
177+
""creators"": [
178+
""Tool: microsoft/sbom-tool-2.2.0"",
179+
""Organization: Microsoft""
180+
]
181+
},
182+
""documentDescribes"": [""SPDXRef-RootPackage""],
183+
""packages"": [],
184+
""relationships"": []
185+
}";
186+
187+
var (scanResult, componentRecorder) = await this.detectorTestUtility
188+
.WithFile("manifest.spdx.json", spdxFile)
189+
.ExecuteDetectorAsync();
190+
191+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
192+
193+
var sbomComponent = (SpdxComponent)componentRecorder.GetDetectedComponents().Single().Component;
194+
sbomComponent.CreatorTool.Should().Be("microsoft/sbom-tool-2.2.0");
195+
sbomComponent.CreatorOrganization.Should().Be("Microsoft");
196+
}
197+
198+
[TestMethod]
199+
public async Task TestSbomDetector_MissingCreationInfoReturnsNullAsync()
200+
{
201+
var spdxFile = /*lang=json,strict*/ @"{
202+
""spdxVersion"": ""SPDX-2.2"",
203+
""SPDXID"": ""SPDXRef-DOCUMENT"",
204+
""name"": ""TestDoc"",
205+
""documentNamespace"": ""https://sbom.microsoft/test/1.0.0/abc"",
206+
""documentDescribes"": [""SPDXRef-RootPackage""],
207+
""packages"": [],
208+
""relationships"": []
209+
}";
210+
211+
var (scanResult, componentRecorder) = await this.detectorTestUtility
212+
.WithFile("manifest.spdx.json", spdxFile)
213+
.ExecuteDetectorAsync();
214+
215+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
216+
217+
var sbomComponent = (SpdxComponent)componentRecorder.GetDetectedComponents().Single().Component;
218+
sbomComponent.CreatorTool.Should().BeNull();
219+
sbomComponent.CreatorOrganization.Should().BeNull();
220+
}
221+
222+
[TestMethod]
223+
public async Task TestSbomDetector_WhitespaceOnlyCreatorsReturnNullAsync()
224+
{
225+
var spdxFile = /*lang=json,strict*/ @"{
226+
""spdxVersion"": ""SPDX-2.2"",
227+
""SPDXID"": ""SPDXRef-DOCUMENT"",
228+
""name"": ""TestDoc"",
229+
""documentNamespace"": ""https://sbom.microsoft/test/1.0.0/abc"",
230+
""creationInfo"": {
231+
""created"": ""2024-01-01T00:00:00Z"",
232+
""creators"": [
233+
""Tool: "",
234+
""Organization: ""
235+
]
236+
},
237+
""documentDescribes"": [""SPDXRef-RootPackage""],
238+
""packages"": [],
239+
""relationships"": []
240+
}";
241+
242+
var (scanResult, componentRecorder) = await this.detectorTestUtility
243+
.WithFile("manifest.spdx.json", spdxFile)
244+
.ExecuteDetectorAsync();
245+
246+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
247+
248+
var sbomComponent = (SpdxComponent)componentRecorder.GetDetectedComponents().Single().Component;
249+
sbomComponent.CreatorTool.Should().BeNull();
250+
sbomComponent.CreatorOrganization.Should().BeNull();
251+
}
252+
253+
[TestMethod]
254+
public async Task TestSbomDetector_MultipleCreatorsPicksFirstToolAndOrgAsync()
255+
{
256+
var spdxFile = /*lang=json,strict*/ @"{
257+
""spdxVersion"": ""SPDX-2.2"",
258+
""SPDXID"": ""SPDXRef-DOCUMENT"",
259+
""name"": ""TestDoc"",
260+
""documentNamespace"": ""https://sbom.microsoft/test/1.0.0/abc"",
261+
""creationInfo"": {
262+
""created"": ""2024-01-01T00:00:00Z"",
263+
""creators"": [
264+
""Tool: first-tool-1.0"",
265+
""Tool: second-tool-2.0"",
266+
""Organization: FirstOrg"",
267+
""Organization: SecondOrg""
268+
]
269+
},
270+
""documentDescribes"": [""SPDXRef-RootPackage""],
271+
""packages"": [],
272+
""relationships"": []
273+
}";
274+
275+
var (scanResult, componentRecorder) = await this.detectorTestUtility
276+
.WithFile("manifest.spdx.json", spdxFile)
277+
.ExecuteDetectorAsync();
278+
279+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
280+
281+
var sbomComponent = (SpdxComponent)componentRecorder.GetDetectedComponents().Single().Component;
282+
sbomComponent.CreatorTool.Should().Be("first-tool-1.0");
283+
sbomComponent.CreatorOrganization.Should().Be("FirstOrg");
284+
}
285+
286+
[TestMethod]
287+
public async Task TestSbomDetector_OnlyToolNoOrganizationAsync()
288+
{
289+
var spdxFile = /*lang=json,strict*/ @"{
290+
""spdxVersion"": ""SPDX-2.2"",
291+
""SPDXID"": ""SPDXRef-DOCUMENT"",
292+
""name"": ""TestDoc"",
293+
""documentNamespace"": ""https://sbom.microsoft/test/1.0.0/abc"",
294+
""creationInfo"": {
295+
""created"": ""2024-01-01T00:00:00Z"",
296+
""creators"": [
297+
""Tool: my-tool-3.0""
298+
]
299+
},
300+
""documentDescribes"": [""SPDXRef-RootPackage""],
301+
""packages"": [],
302+
""relationships"": []
303+
}";
304+
305+
var (scanResult, componentRecorder) = await this.detectorTestUtility
306+
.WithFile("manifest.spdx.json", spdxFile)
307+
.ExecuteDetectorAsync();
308+
309+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
310+
311+
var sbomComponent = (SpdxComponent)componentRecorder.GetDetectedComponents().Single().Component;
312+
sbomComponent.CreatorTool.Should().Be("my-tool-3.0");
313+
sbomComponent.CreatorOrganization.Should().BeNull();
314+
}
315+
316+
[TestMethod]
317+
public async Task TestSbomDetector_OnlyOrganizationNoToolAsync()
318+
{
319+
var spdxFile = /*lang=json,strict*/ @"{
320+
""spdxVersion"": ""SPDX-2.2"",
321+
""SPDXID"": ""SPDXRef-DOCUMENT"",
322+
""name"": ""TestDoc"",
323+
""documentNamespace"": ""https://sbom.microsoft/test/1.0.0/abc"",
324+
""creationInfo"": {
325+
""created"": ""2024-01-01T00:00:00Z"",
326+
""creators"": [
327+
""Organization: Contoso""
328+
]
329+
},
330+
""documentDescribes"": [""SPDXRef-RootPackage""],
331+
""packages"": [],
332+
""relationships"": []
333+
}";
334+
335+
var (scanResult, componentRecorder) = await this.detectorTestUtility
336+
.WithFile("manifest.spdx.json", spdxFile)
337+
.ExecuteDetectorAsync();
338+
339+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
340+
341+
var sbomComponent = (SpdxComponent)componentRecorder.GetDetectedComponents().Single().Component;
342+
sbomComponent.CreatorTool.Should().BeNull();
343+
sbomComponent.CreatorOrganization.Should().Be("Contoso");
344+
}
345+
346+
[TestMethod]
347+
public async Task TestSbomDetector_CreatorsWithNoToolOrOrgPrefixAsync()
348+
{
349+
var spdxFile = /*lang=json,strict*/ @"{
350+
""spdxVersion"": ""SPDX-2.2"",
351+
""SPDXID"": ""SPDXRef-DOCUMENT"",
352+
""name"": ""TestDoc"",
353+
""documentNamespace"": ""https://sbom.microsoft/test/1.0.0/abc"",
354+
""creationInfo"": {
355+
""created"": ""2024-01-01T00:00:00Z"",
356+
""creators"": [
357+
""Person: John Doe (john@example.com)""
358+
]
359+
},
360+
""documentDescribes"": [""SPDXRef-RootPackage""],
361+
""packages"": [],
362+
""relationships"": []
363+
}";
364+
365+
var (scanResult, componentRecorder) = await this.detectorTestUtility
366+
.WithFile("manifest.spdx.json", spdxFile)
367+
.ExecuteDetectorAsync();
368+
369+
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
370+
371+
var sbomComponent = (SpdxComponent)componentRecorder.GetDetectedComponents().Single().Component;
372+
sbomComponent.CreatorTool.Should().BeNull();
373+
sbomComponent.CreatorOrganization.Should().BeNull();
374+
}
163375
}

0 commit comments

Comments
 (0)