Skip to content
Merged
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
2 changes: 2 additions & 0 deletions docs/detectors/spdx.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ The detector:
- Document name
- SPDX version
- Root element ID from `documentDescribes` (defaults to `SPDXRef-Document` if not specified)
- Creator tool from `creationInfo.creators` (e.g., `Tool: microsoft/sbom-tool-2.2.0`)
- Creator organization from `creationInfo.creators` (e.g., `Organization: Microsoft`)
- Creates an `SpdxComponent` to represent the SPDX document

The detector does not parse or register individual packages listed within the SPDX document; it only registers the SPDX document itself as a component.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,15 @@ public SpdxComponent(string spdxVersion, Uri documentNamespace, string name, str
[JsonPropertyName("path")]
public string Path { get; set; }

#nullable enable
[JsonPropertyName("creatorTool")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? CreatorTool { get; set; }

[JsonPropertyName("creatorOrganization")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? CreatorOrganization { get; set; }
#nullable disable

protected override string ComputeBaseId() => $"{this.Name}-{this.SpdxVersion}-{this.Checksum}";
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,42 @@ private SpdxComponent ConvertJsonElementToSbomComponent(ProcessRequest processRe
var path = processRequest.ComponentStream.Location;
var component = new SpdxComponent(spdxVersion, new Uri(sbomNamespace), name, fileHash, rootElementId, path);

if (document.TryGetProperty("creationInfo", out var creationInfoElement)
&& creationInfoElement.TryGetProperty("creators", out var creatorsElement)
&& creatorsElement.ValueKind == JsonValueKind.Array)
{
foreach (var creator in creatorsElement.EnumerateArray())
{
if (creator.ValueKind != JsonValueKind.String)
{
continue;
}
Comment thread
grvillic marked this conversation as resolved.

var creatorString = creator.GetString();
if (creatorString == null)
{
continue;
Comment thread
alisonlomaka marked this conversation as resolved.
}

if (component.CreatorTool == null && creatorString.StartsWith("Tool: ", StringComparison.Ordinal))
{
var tool = creatorString["Tool: ".Length..].Trim();
if (!string.IsNullOrWhiteSpace(tool))
{
component.CreatorTool = tool;
}
}
else if (component.CreatorOrganization == null && creatorString.StartsWith("Organization: ", StringComparison.Ordinal))
{
var org = creatorString["Organization: ".Length..].Trim();
if (!string.IsNullOrWhiteSpace(org))
{
component.CreatorOrganization = org;
}
}
Comment thread
alisonlomaka marked this conversation as resolved.
}
}

return component;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ public async Task TestSbomDetector_SimpleSbomAsync()
sbomComponent.SpdxVersion.Should().Be("SPDX-2.2");
sbomComponent.Checksum.Should().Be(checksum);
sbomComponent.Path.Should().Be(Path.Combine(Path.GetTempPath(), spdxFileName));

sbomComponent.CreatorTool.Should().Be("Microsoft.SBOMTool-1.0.0");
sbomComponent.CreatorOrganization.Should().Be("Microsoft");
}

[TestMethod]
Expand Down Expand Up @@ -160,4 +163,213 @@ public async Task TestSbomDetector_InvalidFileAsync()
var components = detectedComponents.ToList();
components.Should().BeEmpty();
}

[TestMethod]
public async Task TestSbomDetector_ExtractsCreatorToolAndOrganizationAsync()
{
var spdxFile = /*lang=json,strict*/ @"{
""spdxVersion"": ""SPDX-2.2"",
""SPDXID"": ""SPDXRef-DOCUMENT"",
""name"": ""TestDoc"",
""documentNamespace"": ""https://sbom.microsoft/test/1.0.0/abc"",
""creationInfo"": {
""created"": ""2024-01-01T00:00:00Z"",
""creators"": [
""Tool: microsoft/sbom-tool-2.2.0"",
""Organization: Microsoft""
]
},
""documentDescribes"": [""SPDXRef-RootPackage""],
""packages"": [],
""relationships"": []
}";

var (scanResult, componentRecorder) = await this.detectorTestUtility
.WithFile("manifest.spdx.json", spdxFile)
.ExecuteDetectorAsync();

scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);

var sbomComponent = (SpdxComponent)componentRecorder.GetDetectedComponents().Single().Component;
sbomComponent.CreatorTool.Should().Be("microsoft/sbom-tool-2.2.0");
sbomComponent.CreatorOrganization.Should().Be("Microsoft");
}

[TestMethod]
public async Task TestSbomDetector_MissingCreationInfoReturnsNullAsync()
{
var spdxFile = /*lang=json,strict*/ @"{
""spdxVersion"": ""SPDX-2.2"",
""SPDXID"": ""SPDXRef-DOCUMENT"",
""name"": ""TestDoc"",
""documentNamespace"": ""https://sbom.microsoft/test/1.0.0/abc"",
""documentDescribes"": [""SPDXRef-RootPackage""],
""packages"": [],
""relationships"": []
}";

var (scanResult, componentRecorder) = await this.detectorTestUtility
.WithFile("manifest.spdx.json", spdxFile)
.ExecuteDetectorAsync();

scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);

var sbomComponent = (SpdxComponent)componentRecorder.GetDetectedComponents().Single().Component;
sbomComponent.CreatorTool.Should().BeNull();
sbomComponent.CreatorOrganization.Should().BeNull();
}

[TestMethod]
Comment thread
alisonlomaka marked this conversation as resolved.
public async Task TestSbomDetector_WhitespaceOnlyCreatorsReturnNullAsync()
{
var spdxFile = /*lang=json,strict*/ @"{
""spdxVersion"": ""SPDX-2.2"",
""SPDXID"": ""SPDXRef-DOCUMENT"",
""name"": ""TestDoc"",
""documentNamespace"": ""https://sbom.microsoft/test/1.0.0/abc"",
""creationInfo"": {
""created"": ""2024-01-01T00:00:00Z"",
""creators"": [
""Tool: "",
""Organization: ""
]
},
""documentDescribes"": [""SPDXRef-RootPackage""],
""packages"": [],
""relationships"": []
}";

var (scanResult, componentRecorder) = await this.detectorTestUtility
.WithFile("manifest.spdx.json", spdxFile)
.ExecuteDetectorAsync();

scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);

var sbomComponent = (SpdxComponent)componentRecorder.GetDetectedComponents().Single().Component;
sbomComponent.CreatorTool.Should().BeNull();
sbomComponent.CreatorOrganization.Should().BeNull();
}

[TestMethod]
public async Task TestSbomDetector_MultipleCreatorsPicksFirstToolAndOrgAsync()
{
var spdxFile = /*lang=json,strict*/ @"{
""spdxVersion"": ""SPDX-2.2"",
""SPDXID"": ""SPDXRef-DOCUMENT"",
""name"": ""TestDoc"",
""documentNamespace"": ""https://sbom.microsoft/test/1.0.0/abc"",
""creationInfo"": {
""created"": ""2024-01-01T00:00:00Z"",
""creators"": [
""Tool: first-tool-1.0"",
""Tool: second-tool-2.0"",
""Organization: FirstOrg"",
""Organization: SecondOrg""
]
},
""documentDescribes"": [""SPDXRef-RootPackage""],
""packages"": [],
""relationships"": []
}";

var (scanResult, componentRecorder) = await this.detectorTestUtility
.WithFile("manifest.spdx.json", spdxFile)
.ExecuteDetectorAsync();

scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);

var sbomComponent = (SpdxComponent)componentRecorder.GetDetectedComponents().Single().Component;
sbomComponent.CreatorTool.Should().Be("first-tool-1.0");
sbomComponent.CreatorOrganization.Should().Be("FirstOrg");
}

[TestMethod]
public async Task TestSbomDetector_OnlyToolNoOrganizationAsync()
{
var spdxFile = /*lang=json,strict*/ @"{
""spdxVersion"": ""SPDX-2.2"",
""SPDXID"": ""SPDXRef-DOCUMENT"",
""name"": ""TestDoc"",
""documentNamespace"": ""https://sbom.microsoft/test/1.0.0/abc"",
""creationInfo"": {
""created"": ""2024-01-01T00:00:00Z"",
""creators"": [
""Tool: my-tool-3.0""
]
},
""documentDescribes"": [""SPDXRef-RootPackage""],
""packages"": [],
""relationships"": []
}";

var (scanResult, componentRecorder) = await this.detectorTestUtility
.WithFile("manifest.spdx.json", spdxFile)
.ExecuteDetectorAsync();

scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);

var sbomComponent = (SpdxComponent)componentRecorder.GetDetectedComponents().Single().Component;
sbomComponent.CreatorTool.Should().Be("my-tool-3.0");
sbomComponent.CreatorOrganization.Should().BeNull();
}

[TestMethod]
public async Task TestSbomDetector_OnlyOrganizationNoToolAsync()
{
var spdxFile = /*lang=json,strict*/ @"{
""spdxVersion"": ""SPDX-2.2"",
""SPDXID"": ""SPDXRef-DOCUMENT"",
""name"": ""TestDoc"",
""documentNamespace"": ""https://sbom.microsoft/test/1.0.0/abc"",
""creationInfo"": {
""created"": ""2024-01-01T00:00:00Z"",
""creators"": [
""Organization: Contoso""
]
},
""documentDescribes"": [""SPDXRef-RootPackage""],
""packages"": [],
""relationships"": []
}";

var (scanResult, componentRecorder) = await this.detectorTestUtility
.WithFile("manifest.spdx.json", spdxFile)
.ExecuteDetectorAsync();

scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);

var sbomComponent = (SpdxComponent)componentRecorder.GetDetectedComponents().Single().Component;
sbomComponent.CreatorTool.Should().BeNull();
sbomComponent.CreatorOrganization.Should().Be("Contoso");
}

[TestMethod]
public async Task TestSbomDetector_CreatorsWithNoToolOrOrgPrefixAsync()
{
var spdxFile = /*lang=json,strict*/ @"{
""spdxVersion"": ""SPDX-2.2"",
""SPDXID"": ""SPDXRef-DOCUMENT"",
""name"": ""TestDoc"",
""documentNamespace"": ""https://sbom.microsoft/test/1.0.0/abc"",
""creationInfo"": {
""created"": ""2024-01-01T00:00:00Z"",
""creators"": [
""Person: John Doe (john@example.com)""
]
},
""documentDescribes"": [""SPDXRef-RootPackage""],
""packages"": [],
""relationships"": []
}";

var (scanResult, componentRecorder) = await this.detectorTestUtility
.WithFile("manifest.spdx.json", spdxFile)
.ExecuteDetectorAsync();

scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);

var sbomComponent = (SpdxComponent)componentRecorder.GetDetectedComponents().Single().Component;
sbomComponent.CreatorTool.Should().BeNull();
sbomComponent.CreatorOrganization.Should().BeNull();
}
}
Loading