diff --git a/.github/workflows/build_on_push.yaml b/.github/workflows/build_on_push.yaml
index e7b257f..928dbed 100644
--- a/.github/workflows/build_on_push.yaml
+++ b/.github/workflows/build_on_push.yaml
@@ -16,7 +16,13 @@ jobs:
6.x
8.x
- - name: Install dependencies
+ - name: Restore Tools
+ run: >
+ dotnet
+ tool
+ restore
+
+ - name: Restore Dependencies
run: >
dotnet
restore
@@ -34,3 +40,31 @@ jobs:
test
--no-build
--configuration Release
+
+ - name: Generate SBOM
+ run: >
+ dotnet
+ sbom-tool
+ generate
+ -b src/DemaConsulting.SpdxTool/bin/Release
+ -bc src/DemaConsulting.SpdxTool
+ -pn DemaConsulting.SpdxTool
+ -pv 0.0.0-cibuild
+ -ps DemaConsulting
+ -nsb https://DemaConsulting.com/SpdxTool
+
+ - name: Run SBOM Workflow
+ run: >
+ dotnet
+ src/DemaConsulting.SpdxTool/bin/Release/net8.0/DemaConsulting.SpdxTool.dll
+ run-workflow
+ spdx-workflow.yaml
+
+ - name: Upload Artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: artifacts
+ path: |
+ **/manifest.spdx.json
+ **/manifest.spdx.json.sha256
+ manifest.spdx.summary.md
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 3562b8c..5e8dce5 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -31,7 +31,7 @@ jobs:
tool
restore
- - name: Install dependencies
+ - name: Restore Dependencies
run: >
dotnet
restore
@@ -64,13 +64,12 @@ jobs:
-ps DemaConsulting
-nsb https://DemaConsulting.com/SpdxTool
- - name: Generate SBOM Summary
+ - name: Run SBOM Workflow
run: >
dotnet
src/DemaConsulting.SpdxTool/bin/Release/net8.0/DemaConsulting.SpdxTool.dll
- to-markdown
- src/DemaConsulting.SpdxTool/bin/Release/_manifest/spdx_2.2/manifest.spdx.json
- manifest.spdx.summary.md
+ run-workflow
+ spdx-workflow.yaml
- name: Create Dotnet Tool
run: >
diff --git a/DemaConsulting.SpdxTool.sln.DotSettings b/DemaConsulting.SpdxTool.sln.DotSettings
index 489f800..af14ace 100644
--- a/DemaConsulting.SpdxTool.sln.DotSettings
+++ b/DemaConsulting.SpdxTool.sln.DotSettings
@@ -1,3 +1,4 @@
True
+ True
True
\ No newline at end of file
diff --git a/README.md b/README.md
index d2301d2..ae9b37c 100644
--- a/README.md
+++ b/README.md
@@ -38,10 +38,13 @@ Options:
Commands:
help Display extended help about a command
+ add-package Add package to SPDX document (workflow only).
+ copy-package Copy package information from one SPDX document to another.
+ query [arguments] Query program output for value
+ rename-id Rename an element ID in an SPDX document.
run-workflow Runs the workflow file
+ sha256 Generate or verify sha256 hashes of files
to-markdown Create Markdown summary for SPDX document
- rename-id Rename an element ID in an SPDX document.
- copy-package Copy package information from one SPDX document to another.
```
@@ -50,6 +53,11 @@ Commands:
The SpdxTool can be driven using workflow yaml files of the following format:
```yaml
+# Workflow parameters
+parameters:
+ parameter-name: value
+
+# Workflow steps
steps:
- command:
inputs:
@@ -57,35 +65,78 @@ steps:
- command:
inputs:
-
+ input1: value
+ input2: ${{ parameter-name }}
```
-## YAML Commands
+## YAML Variables
-The following are the supported commands and their formats:
+Variables are specified at the top of the workflow file in a parameters section:
```yaml
-steps:
+# Workflow parameters
+parameters:
+ parameter1: value1
+ parameter2: value2
+```
- # Run a separate workflow file
-- command: run-workflow
+Variables can be expanded in step inputs using the dollar expansion syntax
+
+```yaml
+# Workflow steps
+steps:
+- command:
inputs:
- file: other-workflow-file.yaml
- parameters:
-
+ input1: ${{ parameter1 }}
+ input2: Insert ${{ parameter2 }} in the middle
+```
- # Create a summary markdown from the specified SPDX document
-- command: to-markdown
+Variables can be overridden on the command line:
+
+```
+spdx-tool run-workflow workflow.yaml parameter1=command parameter2=line
+```
+
+Variables can be changed at runtime by some steps:
+
+```yaml
+# Workflow parameters
+parameters:
+ dotnet-version: unknown
+
+steps:
+- command: query
inputs:
- spdx: input.spdx.json
- markdown: output.md
+ output: dotnet-version
+ pattern: '(?\d+\.\d+\.\d+)'
+ program: dotnet
+ arguments:
+ - '--version'
+```
- # Rename the SPDX-ID of an element in an SPDX document
-- command: rename-id
+
+## YAML Commands
+
+The following are the supported commands and their formats:
+
+```yaml
+steps:
+
+ # Add a package to an SPDX document
+- command: add-package
inputs:
+ package:
+ id:
+ name:
+ copyright:
+ version:
+ download:
+ license: # optional
+ purl: # optional
+ cpe23: # optional
spdx:
- old:
- new:
+ relationship:
+ element:
# Copy a package from one SPDX document to another SPDX document
- command: copy-package
@@ -95,4 +146,40 @@ steps:
package:
relationship:
element:
+
+ # Query information from the output of a program
+- command: query
+ inputs:
+ output:
+ pattern:
+ program:
+ arguments:
+ -
+ -
+
+ # Rename the SPDX-ID of an element in an SPDX document
+- command: rename-id
+ inputs:
+ spdx:
+ old:
+ new:
+
+ # Run a separate workflow file
+- command: run-workflow
+ inputs:
+ file: other-workflow-file.yaml
+ parameters:
+
+
+ # Perform Sha256 operations on the specified file
+- command: help
+ inputs:
+ operation: generate | verify
+ file:
+
+ # Create a summary markdown from the specified SPDX document
+- command: to-markdown
+ inputs:
+ spdx: input.spdx.json
+ markdown: output.md
```
diff --git a/spdx-workflow.yaml b/spdx-workflow.yaml
new file mode 100644
index 0000000..0bf2294
--- /dev/null
+++ b/spdx-workflow.yaml
@@ -0,0 +1,45 @@
+# This workflow demonstrates using spdx-tool to manipulate an SPDX document
+# adding new packages, updating the sha256 digest, and generating a
+# summary markdown document describing the contents.
+
+
+# Workflow Parameters
+parameters:
+ dotnet-version: unknown
+ spdx: src/DemaConsulting.SpdxTool/bin/Release/_manifest/spdx_2.2/manifest.spdx.json
+ summary-markdown: manifest.spdx.summary.md
+
+# Steps
+steps:
+
+ # Query the version of dotnet
+- command: query
+ inputs:
+ output: dotnet-version
+ pattern: '(?\d+\.\d+\.\d+)'
+ program: dotnet
+ arguments:
+ - '--version'
+
+ # Add DotNet SDK as a build tool of the package
+- command: add-package
+ inputs:
+ package:
+ id: SPDXRef-Package-DotNetSDK
+ name: DotNet SDK ${{ dotnet-version }}
+ version: ${{ dotnet-version }}
+ download: https://dotnet.microsoft.com/download
+ spdx: ${{ spdx }}
+ relationship: BUILD_TOOL_OF
+ element: SPDXRef-RootPackage
+
+ # Update the Sha256 digest on the SPDX document
+- command: sha256
+ inputs:
+ operation: generate
+ file: ${{ spdx }}
+
+- command: to-markdown
+ inputs:
+ spdx: ${{ spdx }}
+ markdown: ${{ summary-markdown }}
\ No newline at end of file
diff --git a/src/DemaConsulting.SpdxTool/Commands/AddPackageCommand.cs b/src/DemaConsulting.SpdxTool/Commands/AddPackageCommand.cs
new file mode 100644
index 0000000..678f56a
--- /dev/null
+++ b/src/DemaConsulting.SpdxTool/Commands/AddPackageCommand.cs
@@ -0,0 +1,185 @@
+using DemaConsulting.SpdxModel;
+using DemaConsulting.SpdxModel.IO;
+using YamlDotNet.Core;
+using YamlDotNet.RepresentationModel;
+
+namespace DemaConsulting.SpdxTool.Commands;
+
+///
+/// Add a package to an SPDX document
+///
+public class AddPackageCommand : Command
+{
+ ///
+ /// Singleton instance of this command
+ ///
+ public static readonly AddPackageCommand Instance = new();
+
+ ///
+ /// Entry information for this command
+ ///
+ public static readonly CommandEntry Entry = new(
+ "add-package",
+ "add-package",
+ "Add package to SPDX document (workflow only).",
+ new[]
+ {
+ "This command adds a package to an SPDX document.",
+ "",
+ " - command: add-package",
+ " inputs:",
+ " package:",
+ " id: ",
+ " name: ",
+ " copyright: ",
+ " version: ",
+ " download: ",
+ " license: # optional",
+ " purl: # optional",
+ " cpe23: # optional",
+ " spdx: ",
+ " relationship: ",
+ " element: ",
+ "",
+ "The argument describes the relationship to .",
+ "The argument is the name of an element in the file.",
+ "",
+ "The is defined by the SPDX specification, and is usually one of:",
+ " DESCRIBES, DESCRIBED_BY, CONTAINS, BUILD_TOOL_OF, ..."
+ },
+ Instance);
+
+ ///
+ /// Private constructor - this is a singleton
+ ///
+ private AddPackageCommand()
+ {
+ }
+
+ ///
+ public override void Run(string[] args)
+ {
+ throw new CommandUsageException("'add-package' command is only valid in a workflow");
+ }
+
+ ///
+ public override void Run(YamlMappingNode step, Dictionary variables)
+ {
+ // Get the step inputs
+ var inputs = GetMapMap(step, "inputs");
+
+ // Get the package map
+ var packageMap = GetMapMap(inputs, "package") ??
+ throw new YamlException(step.Start, step.End, "'add-package' missing 'package' input");
+
+ // Get the 'spdx' input
+ var spdxFile = GetMapString(inputs, "spdx", variables) ??
+ throw new YamlException(step.Start, step.End, "'copy-package' missing 'spdx' input");
+
+ // Get the 'relationship' input
+ var relationship = GetMapString(inputs, "relationship", variables) ??
+ throw new YamlException(step.Start, step.End, "'copy-package' missing 'relationship' input");
+
+ // Get the 'element' input
+ var element = GetMapString(inputs, "element", variables) ??
+ throw new YamlException(step.Start, step.End, "'copy-package' missing 'element' input");
+
+ // Construct the package
+ var package = new SpdxPackage
+ {
+ // Get the package ID
+ Id = GetMapString(packageMap, "id", variables) ??
+ throw new YamlException(step.Start, step.End, "'add-package' missing package 'id' input"),
+
+ // Get the package name
+ Name = GetMapString(packageMap, "name", variables) ??
+ throw new YamlException(step.Start, step.End, "'add-package' missing package 'name' input"),
+
+ // Get the package version
+ Version = GetMapString(packageMap, "version", variables),
+
+ // Get the download location
+ DownloadLocation = GetMapString(packageMap, "download", variables) ??
+ throw new YamlException(step.Start, step.End, "'add-package' missing package 'download' input"),
+
+ // Get the package copyright
+ CopyrightText = GetMapString(packageMap, "copyright", variables),
+
+ // Get the package license
+ ConcludedLicense = GetMapString(packageMap, "license", variables) ?? "NOASSERTION",
+ DeclaredLicense = GetMapString(packageMap, "license", variables) ?? "NOASSERTION"
+ };
+
+ // Append the PURL if specified
+ var purl = GetMapString(packageMap, "purl", variables);
+ if (!string.IsNullOrEmpty(purl))
+ package.ExternalReferences = package.ExternalReferences.Append(
+ new SpdxExternalReference
+ {
+ Category = SpdxReferenceCategory.PackageManager,
+ Type = "purl",
+ Locator = purl
+ }).ToArray();
+
+ // Append the CPE23 if specified
+ var cpe23 = GetMapString(packageMap, "cpe23", variables);
+ if (!string.IsNullOrEmpty(cpe23))
+ package.ExternalReferences = package.ExternalReferences.Append(
+ new SpdxExternalReference
+ {
+ Category = SpdxReferenceCategory.Security,
+ Type = "cpe23Type",
+ Locator = cpe23
+ }).ToArray();
+
+ // Add the package
+ AddPackage(package, spdxFile, relationship, element);
+ }
+
+ ///
+ /// Add a package to the SPDX document
+ ///
+ /// Package to add
+ /// SPDX file
+ /// Relationship type
+ /// Element to relate package to
+ /// On usage error
+ public static void AddPackage(SpdxPackage package, string spdxFile, string relationshipName, string elementId)
+ {
+ // Verify to file exists
+ if (!File.Exists(spdxFile))
+ throw new CommandUsageException($"File not found: {spdxFile}");
+
+ // Verify package ID
+ if (package.Id.Length == 0 || package.Id == "SPDXRef-DOCUMENT")
+ throw new CommandUsageException("Invalid package ID");
+
+ // Parse the relationship
+ var relationship = SpdxRelationshipTypeExtensions.FromText(relationshipName);
+ if (relationship == SpdxRelationshipType.Missing)
+ throw new CommandUsageException("Invalid relationship");
+
+ // Verify element name
+ if (elementId.Length == 0)
+ throw new CommandUsageException("Invalid element name");
+
+ // Load the SPDX document
+ var doc = Spdx2JsonDeserializer.Deserialize(File.ReadAllText(spdxFile));
+
+ // Add the package (if not already present)
+ if (!Array.Exists(doc.Packages, p => p.Id == package.Id))
+ doc.Packages = doc.Packages.Append(package).ToArray();
+
+ // Add the relationship
+ doc.Relationships = doc.Relationships.Append(
+ new SpdxRelationship
+ {
+ Id = package.Id,
+ RelationshipType = relationship,
+ RelatedSpdxElement = elementId
+ }).ToArray();
+
+ // Save the SPDX document
+ File.WriteAllText(spdxFile, Spdx2JsonSerializer.Serialize(doc));
+ }
+}
\ No newline at end of file
diff --git a/src/DemaConsulting.SpdxTool/Commands/Command.cs b/src/DemaConsulting.SpdxTool/Commands/Command.cs
index 23322c4..c503731 100644
--- a/src/DemaConsulting.SpdxTool/Commands/Command.cs
+++ b/src/DemaConsulting.SpdxTool/Commands/Command.cs
@@ -101,4 +101,4 @@ public static string Expand(string text, Dictionary variables)
// Get the parameter
return map.Children.TryGetValue(key, out var value) ? Expand(value.ToString(), variables) : null;
}
-}
+}
\ No newline at end of file
diff --git a/src/DemaConsulting.SpdxTool/Commands/CommandRegistry.cs b/src/DemaConsulting.SpdxTool/Commands/CommandRegistry.cs
index 4e587b4..74fecb1 100644
--- a/src/DemaConsulting.SpdxTool/Commands/CommandRegistry.cs
+++ b/src/DemaConsulting.SpdxTool/Commands/CommandRegistry.cs
@@ -11,10 +11,13 @@ public static class CommandsRegistry
private static readonly Dictionary InternalCommands = new()
{
{ HelpCommand.Entry.Name, HelpCommand.Entry },
- { RunWorkflowCommand.Entry.Name, RunWorkflowCommand.Entry },
- { ToMarkdownCommand.Entry.Name, ToMarkdownCommand.Entry },
+ { AddPackageCommand.Entry.Name, AddPackageCommand.Entry },
+ { CopyPackageCommand.Entry.Name, CopyPackageCommand.Entry },
+ { QueryCommand.Entry.Name, QueryCommand.Entry },
{ RenameIdCommand.Entry.Name, RenameIdCommand.Entry },
- { CopyPackageCommand.Entry.Name, CopyPackageCommand.Entry }
+ { RunWorkflowCommand.Entry.Name, RunWorkflowCommand.Entry },
+ { Sha256Command.Entry.Name, Sha256Command.Entry },
+ { ToMarkdownCommand.Entry.Name, ToMarkdownCommand.Entry }
};
///
diff --git a/src/DemaConsulting.SpdxTool/Commands/CopyPackageCommand.cs b/src/DemaConsulting.SpdxTool/Commands/CopyPackageCommand.cs
index 7417d1a..0c896b0 100644
--- a/src/DemaConsulting.SpdxTool/Commands/CopyPackageCommand.cs
+++ b/src/DemaConsulting.SpdxTool/Commands/CopyPackageCommand.cs
@@ -39,10 +39,10 @@ public class CopyPackageCommand : Command
" element: ",
"",
"The argument is the name of a package in to copy.",
- "The argument describes the relationship to .",
+ "The argument describes the relationship to .",
"The argument is the name of an element in the file.",
"",
- "The is defined by the SPDX specification, and is usually one of:",
+ "The is defined by the SPDX specification, and is usually one of:",
" DESCRIBES, DESCRIBED_BY, CONTAINS, BUILD_TOOL_OF, ..."
},
Instance);
@@ -100,11 +100,11 @@ public override void Run(YamlMappingNode step, Dictionary variab
///
/// Source SPDX document filename
/// Destination SPDX document filename
- /// Package to copy
+ /// Package to copy
/// Relationship of package to element in destination
- /// Destination element
- public static void CopyPackage(string fromFile, string toFile, string packageName, string relationshipName,
- string elementName)
+ /// Destination element
+ public static void CopyPackage(string fromFile, string toFile, string packageId, string relationshipName,
+ string elementId)
{
// Verify from file exists
if (!File.Exists(fromFile))
@@ -115,7 +115,7 @@ public static void CopyPackage(string fromFile, string toFile, string packageNam
throw new CommandUsageException($"File not found: {toFile}");
// Verify package name
- if (packageName.Length == 0 || packageName == "SPDXRef-DOCUMENT")
+ if (packageId.Length == 0 || packageId == "SPDXRef-DOCUMENT")
throw new CommandUsageException("Invalid package name");
// Parse the relationship
@@ -124,7 +124,7 @@ public static void CopyPackage(string fromFile, string toFile, string packageNam
throw new CommandUsageException("Invalid relationship");
// Verify element name
- if (elementName.Length == 0)
+ if (elementId.Length == 0)
throw new CommandUsageException("Invalid element name");
// Read the SPDX documents
@@ -132,8 +132,8 @@ public static void CopyPackage(string fromFile, string toFile, string packageNam
var toDoc = Spdx2JsonDeserializer.Deserialize(File.ReadAllText(toFile));
// Verify the package exists in the source
- var package = Array.Find(fromDoc.Packages, p => p.Id == packageName) ??
- throw new CommandErrorException($"Package {packageName} not found in {fromFile}");
+ var package = Array.Find(fromDoc.Packages, p => p.Id == packageId) ??
+ throw new CommandErrorException($"Package {packageId} not found in {fromFile}");
// Verify the package does not exist in the destination
if (Array.Exists(toDoc.Packages, p => p.Id == package.Id))
@@ -160,9 +160,9 @@ public static void CopyPackage(string fromFile, string toFile, string packageNam
// Append the relationship to the destination document
var newRelationship = new SpdxRelationship
{
- Id = elementName,
+ Id = package.Id,
RelationshipType = relationship,
- RelatedSpdxElement = package.Id
+ RelatedSpdxElement = elementId
};
toDoc.Relationships = toDoc.Relationships.Append(newRelationship).ToArray();
diff --git a/src/DemaConsulting.SpdxTool/Commands/QueryCommand.cs b/src/DemaConsulting.SpdxTool/Commands/QueryCommand.cs
new file mode 100644
index 0000000..2c35c92
--- /dev/null
+++ b/src/DemaConsulting.SpdxTool/Commands/QueryCommand.cs
@@ -0,0 +1,153 @@
+using System.Diagnostics;
+using System.Text.RegularExpressions;
+using YamlDotNet.Core;
+using YamlDotNet.RepresentationModel;
+
+namespace DemaConsulting.SpdxTool.Commands;
+
+///
+/// Query a program output for a value
+///
+public class QueryCommand : Command
+{
+ ///
+ /// Singleton instance of this command
+ ///
+ public static readonly QueryCommand Instance = new();
+
+ ///
+ /// Entry information for this command
+ ///
+ public static readonly CommandEntry Entry = new(
+ "query",
+ "query [arguments]",
+ "Query program output for value",
+ new[]
+ {
+ "This command executes a program and inspects the output for a value.",
+ "When executed in a workflow this can be used to set a variable.",
+ "",
+ "From the command-line this can be used as:",
+ " spdx-tool query [arguments]",
+ "",
+ "From a YAML file this can be used as:",
+ " - command: query",
+ " inputs:",
+ " output: ",
+ " pattern: ",
+ " program: ",
+ " arguments:",
+ " - ",
+ " - "
+ },
+ Instance);
+
+ ///
+ /// Private constructor - this is a singleton
+ ///
+ private QueryCommand()
+ {
+ }
+
+ ///
+ public override void Run(string[] args)
+ {
+ // Report an error if the number of arguments is not 1
+ if (args.Length < 2)
+ throw new CommandUsageException("'query' command missing arguments");
+
+ // Generate the markdown
+ var found = Query(args[0], args[1], args.Skip(2).ToArray());
+
+ // Write the found value to the console
+ Console.WriteLine(found);
+ }
+
+ ///
+ public override void Run(YamlMappingNode step, Dictionary variables)
+ {
+ // Get the step inputs
+ var inputs = GetMapMap(step, "inputs");
+
+ // Get the 'output' input
+ var output = GetMapString(inputs, "output", variables) ??
+ throw new YamlException(step.Start, step.End, "'query' command missing 'output' input");
+
+ // Get the 'pattern' input
+ var pattern = GetMapString(inputs, "pattern", variables) ??
+ throw new YamlException(step.Start, step.End, "'query' command missing 'pattern' input");
+
+ // Get the 'program' input
+ var program = GetMapString(inputs, "program", variables) ??
+ throw new YamlException(step.Start, step.End, "'query' command missing 'program' input");
+
+ // Get the arguments
+ var argumentsSequence = GetMapSequence(inputs, "arguments");
+ var arguments = argumentsSequence?.Children.Select(c => Expand(c.ToString(), variables)).ToArray() ??
+ Array.Empty();
+
+ // Generate the markdown
+ var found = Query(pattern, program, arguments);
+
+ // Save the output to the variables
+ variables[output] = found;
+ }
+
+ ///
+ /// Run a program and query the output for a value
+ ///
+ /// Regular expression pattern to capture 'value'
+ /// Program to execute
+ /// Program arguments
+ /// Captured value
+ /// On bad usage
+ /// On error
+ public static string Query(string pattern, string program, string[] arguments)
+ {
+ // Construct the regular expression
+ var regex = new Regex(pattern);
+ if (!regex.GetGroupNames().Contains("value"))
+ throw new CommandUsageException("Pattern must contain a 'value' capture group");
+
+ // Construct the start information
+ var startInfo = new ProcessStartInfo(program)
+ {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ // Add the arguments
+ foreach (var argument in arguments)
+ startInfo.ArgumentList.Add(argument);
+
+ // Start the process
+ Process process;
+ try
+ {
+ process = Process.Start(startInfo) ??
+ throw new CommandErrorException($"Unable to start program '{program}'");
+ }
+ catch
+ {
+ throw new CommandErrorException($"Unable to start program '{program}'");
+ }
+
+ // Wait for the process to exit
+ process.WaitForExit();
+ if (process.ExitCode != 0)
+ throw new CommandErrorException($"Program '{program}' exited with code {process.ExitCode}");
+
+ // Save the output and return the exit code
+ var output = process.StandardOutput.ReadToEnd().Trim();
+
+ // Find the match
+ var match = regex.Match(output);
+ if (match == null)
+ throw new CommandErrorException($"Pattern '{pattern}' not found in program output");
+
+ // Return the captured value
+ return match.Groups["value"].Value;
+ }
+}
\ No newline at end of file
diff --git a/src/DemaConsulting.SpdxTool/Commands/RenameIdCommand.cs b/src/DemaConsulting.SpdxTool/Commands/RenameIdCommand.cs
index 0a46e7e..f80d14b 100644
--- a/src/DemaConsulting.SpdxTool/Commands/RenameIdCommand.cs
+++ b/src/DemaConsulting.SpdxTool/Commands/RenameIdCommand.cs
@@ -70,7 +70,7 @@ public override void Run(YamlMappingNode step, Dictionary variab
throw new YamlException(step.Start, step.End, "'rename-id' command missing 'new' input");
// Get the 'old' input
- var oldId = GetMapString(inputs, "old", variables) ??
+ var oldId = GetMapString(inputs, "old", variables) ??
throw new YamlException(step.Start, step.End, "'rename-id' command missing 'spdx' input");
// Rename the ID
diff --git a/src/DemaConsulting.SpdxTool/Commands/RunWorkflowCommand.cs b/src/DemaConsulting.SpdxTool/Commands/RunWorkflowCommand.cs
index cc06e13..661c949 100644
--- a/src/DemaConsulting.SpdxTool/Commands/RunWorkflowCommand.cs
+++ b/src/DemaConsulting.SpdxTool/Commands/RunWorkflowCommand.cs
@@ -77,13 +77,12 @@ public override void Run(YamlMappingNode step, Dictionary variab
var inputs = GetMapMap(step, "input");
// Get the 'file' input
- var file = GetMapString(inputs, "file", variables) ??
+ var file = GetMapString(inputs, "file", variables) ??
throw new YamlException(step.Start, step.End, "'run-workflow' command missing 'file' input");
// Get the parameters
var parameters = new Dictionary();
if (GetMapMap(inputs, "parameters") is { } parametersMap)
- {
// Process all the parameters
foreach (var (keyNode, valueNode) in parametersMap.Children)
{
@@ -91,7 +90,6 @@ public override void Run(YamlMappingNode step, Dictionary variab
var value = valueNode.ToString();
parameters[key] = Expand(value, variables);
}
- }
// Execute the workflow
Execute(file, parameters);
@@ -124,7 +122,6 @@ public static void Execute(string workflowFile, Dictionary param
// Process the parameters definitions into local variables
var variables = new Dictionary();
if (GetMapMap(root, "parameters") is { } parametersMap)
- {
// Process all the parameters
foreach (var (keyNode, valueNode) in parametersMap.Children)
{
@@ -132,7 +129,6 @@ public static void Execute(string workflowFile, Dictionary param
var value = Expand(valueNode.ToString(), variables);
variables[key] = Expand(value, parameters);
}
- }
// Apply the provided parameters to our variables
foreach (var (key, value) in parameters)
diff --git a/src/DemaConsulting.SpdxTool/Commands/Sha256Command.cs b/src/DemaConsulting.SpdxTool/Commands/Sha256Command.cs
new file mode 100644
index 0000000..5ee71ba
--- /dev/null
+++ b/src/DemaConsulting.SpdxTool/Commands/Sha256Command.cs
@@ -0,0 +1,154 @@
+using System.Security.Cryptography;
+using YamlDotNet.Core;
+using YamlDotNet.RepresentationModel;
+
+namespace DemaConsulting.SpdxTool.Commands;
+
+///
+/// Sha256 command
+///
+public class Sha256Command : Command
+{
+ ///
+ /// Singleton instance of this command
+ ///
+ public static readonly Sha256Command Instance = new();
+
+ ///
+ /// Entry information for this command
+ ///
+ public static readonly CommandEntry Entry = new(
+ "sha256",
+ "sha256 ",
+ "Generate or verify sha256 hashes of files",
+ new[]
+ {
+ "This command generates or verifies sha256 hashes.",
+ "",
+ "From the command-line this can be used as:",
+ " spdx-tool sha256 generate ",
+ " spdx-tool sha256 verify ",
+ "",
+ "From a YAML file this can be used as:",
+ " - command: help",
+ " inputs:",
+ " operation: generate | verify",
+ " file: ",
+ },
+ Instance);
+
+ ///
+ /// Private constructor - this is a singleton
+ ///
+ private Sha256Command()
+ {
+ }
+
+ ///
+ public override void Run(string[] args)
+ {
+ // Report an error if the number of arguments is not 2
+ if (args.Length != 2)
+ throw new CommandUsageException("'sha256' command missing arguments");
+
+ // Do the Sha256 operation
+ DoSha256(args[0], args[1]);
+ }
+
+ ///
+ public override void Run(YamlMappingNode step, Dictionary variables)
+ {
+ // Get the step inputs
+ var inputs = GetMapMap(step, "inputs");
+
+ // Get the 'operation' input
+ var operation = GetMapString(inputs, "operation", variables) ??
+ throw new YamlException(step.Start, step.End, "'sha256' command missing 'operation' input");
+
+ // Get the 'file' input
+ var file = GetMapString(inputs, "file", variables) ??
+ throw new YamlException(step.Start, step.End, "'sha256' command missing 'file' input");
+
+ // Do the Sha256 operation
+ DoSha256(operation, file);
+ }
+
+ ///
+ /// Do the requested Sha256 operation
+ ///
+ /// Operation to perform (generate or verify)
+ /// File to perform operation on
+ /// On usage error
+ public static void DoSha256(string operation, string file)
+ {
+ switch (operation)
+ {
+ case "generate":
+ GenerateSha256(file);
+ break;
+
+ case "verify":
+ VerifySha256(file);
+ break;
+
+ default:
+ throw new CommandUsageException($"'sha256' command invalid operation '{operation}'");
+ }
+ }
+
+ ///
+ /// Generate a Sha256 hash for a file
+ ///
+ /// File to generate hash for
+ public static void GenerateSha256(string file)
+ {
+ // Calculate the digest
+ var digest = CalculateSha256(file);
+
+ // Write the digest
+ File.WriteAllText(file + ".sha256", digest);
+ }
+
+ ///
+ /// Verify a Sha256 hash for a file
+ ///
+ ///
+ ///
+ public static void VerifySha256(string file)
+ {
+ // Read the digest
+ var digest = File.ReadAllText(file + ".sha256").Trim();
+
+ // Calculate the digest
+ var calculated = CalculateSha256(file);
+
+ // Verify the digest
+ if (digest != calculated)
+ throw new CommandErrorException($"Sha256 hash mismatch for '{file}'");
+
+ // Report the digest is OK
+ Console.WriteLine($"Sha256 Digest OK for '{file}'");
+ }
+
+ ///
+ /// Calculate the Sha256 hash of a file
+ ///
+ /// File to hash
+ /// Sh256 hash
+ /// On error
+ public static string CalculateSha256(string file)
+ {
+ try
+ {
+ // Calculate the Sha256 digest of the file
+ using var stream = new FileStream(file, FileMode.Open);
+ using var sha256 = SHA256.Create();
+ var hash = sha256.ComputeHash(stream);
+ return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+ }
+ catch (Exception ex)
+ {
+ throw new CommandErrorException($"Error calculating sha256 hash for '{file}': {ex.Message}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DemaConsulting.SpdxTool/Commands/ToMarkdownCommand.cs b/src/DemaConsulting.SpdxTool/Commands/ToMarkdownCommand.cs
index fadeda7..ddb033e 100644
--- a/src/DemaConsulting.SpdxTool/Commands/ToMarkdownCommand.cs
+++ b/src/DemaConsulting.SpdxTool/Commands/ToMarkdownCommand.cs
@@ -62,7 +62,7 @@ public override void Run(YamlMappingNode step, Dictionary variab
var inputs = GetMapMap(step, "inputs");
// Get the 'spdx' input
- var spdxFile = GetMapString(inputs, "spdx", variables) ??
+ var spdxFile = GetMapString(inputs, "spdx", variables) ??
throw new YamlException(step.Start, step.End, "'to-markdown' command missing 'spdx' input");
// Get the 'markdown' input
diff --git a/src/DemaConsulting.SpdxTool/Program.cs b/src/DemaConsulting.SpdxTool/Program.cs
index e5aacb0..87bf6df 100644
--- a/src/DemaConsulting.SpdxTool/Program.cs
+++ b/src/DemaConsulting.SpdxTool/Program.cs
@@ -26,14 +26,14 @@ public static class Program
public static void Main(string[] args)
{
// Handle printing usage information
- if (args.Length == 0 || args.Contains("-h") || args.Contains("--help"))
+ if (args.Length == 0 || args[0] == "-h" || args[0] == "--help")
{
PrintUsage();
Environment.Exit(1);
}
// Handle querying for version
- if (args.Contains("-v") || args.Contains("--version"))
+ if (args.Length == 1 && (args[0] == "-v" || args[0] == "--version"))
{
Console.WriteLine(Version);
Environment.Exit(0);
diff --git a/test/DemaConsulting.SpdxTool.Tests/TestAddPackageCommand.cs b/test/DemaConsulting.SpdxTool.Tests/TestAddPackageCommand.cs
new file mode 100644
index 0000000..3388223
--- /dev/null
+++ b/test/DemaConsulting.SpdxTool.Tests/TestAddPackageCommand.cs
@@ -0,0 +1,204 @@
+using DemaConsulting.SpdxModel.IO;
+using DemaConsulting.SpdxModel;
+
+namespace DemaConsulting.SpdxTool.Tests;
+
+[TestClass]
+public class TestAddPackageCommand
+{
+ [TestMethod]
+ public void AddPackageCommandLine()
+ {
+ // Run the command
+ var exitCode = Runner.Run(
+ out var output,
+ "dotnet",
+ "DemaConsulting.SpdxTool.dll",
+ "add-package");
+
+ // Verify error reported
+ Assert.AreEqual(1, exitCode);
+ Assert.IsTrue(output.Contains("'add-package' command is only valid in a workflow"));
+ }
+
+ [TestMethod]
+ public void AddPackageSimple()
+ {
+ // SPDX contents
+ const string spdxContents = "{\r\n" +
+ " \"files\": [],\r\n" +
+ " \"packages\": [" +
+ " {\r\n" +
+ " \"SPDXID\": \"SPDXRef-Package-1\",\r\n" +
+ " \"name\": \"Test Package\",\r\n" +
+ " \"versionInfo\": \"1.0.0\",\r\n" +
+ " \"downloadLocation\": \"https://github.com/demaconsulting/SpdxTool\",\r\n" +
+ " \"licenseConcluded\": \"MIT\"\r\n" +
+ " }\r\n" +
+ " ],\r\n" +
+ " \"relationships\": [" +
+ " {\r\n" +
+ " \"spdxElementId\": \"SPDXRef-DOCUMENT\",\r\n" +
+ " \"relatedSpdxElement\": \"SPDXRef-Package-1\",\r\n" +
+ " \"relationshipType\": \"DESCRIBES\"\r\n" +
+ " }\r\n" +
+ " ],\r\n" +
+ " \"spdxVersion\": \"SPDX-2.2\",\r\n" +
+ " \"dataLicense\": \"CC0-1.0\",\r\n" +
+ " \"SPDXID\": \"SPDXRef-DOCUMENT\",\r\n" +
+ " \"name\": \"Test Document\",\r\n" +
+ " \"documentNamespace\": \"https://sbom.spdx.org\",\r\n" +
+ " \"creationInfo\": {\r\n" +
+ " \"created\": \"2021-10-01T00:00:00Z\",\r\n" +
+ " \"creators\": [ \"Person: Malcolm Nixon\" ]\r\n" +
+ " },\r\n" +
+ " \"documentDescribes\": [ \"SPDXRef-Package-1\" ]\r\n" +
+ "}";
+
+ // Workflow contents
+ const string workflowContents = "steps:\n" +
+ "- command: add-package\n" +
+ " inputs:\n" +
+ " package:\n" +
+ " id: SPDXRef-Package-2\n" +
+ " name: Test Package 2\n" +
+ " version: 2.0.0\n" +
+ " download: https://dotnet.microsoft.com/download\n" +
+ " purl: pkg:nuget/BogusPackage@2.0.0\n" +
+ " spdx: spdx.json\n" +
+ " relationship: BUILD_TOOL_OF\n" +
+ " element: SPDXRef-Package-1\n";
+
+ try
+ {
+ // Write the SPDX files
+ File.WriteAllText("spdx.json", spdxContents);
+ File.WriteAllText("workflow.yaml", workflowContents);
+
+ // Run the command
+ var exitCode = Runner.Run(
+ out _,
+ "dotnet",
+ "DemaConsulting.SpdxTool.dll",
+ "run-workflow",
+ "workflow.yaml");
+
+ // Verify success
+ Assert.AreEqual(0, exitCode);
+
+ // Read the SPDX document
+ Assert.IsTrue(File.Exists("spdx.json"));
+ var doc = Spdx2JsonDeserializer.Deserialize(File.ReadAllText("spdx.json"));
+
+ // Verify both packages present
+ Assert.AreEqual(2, doc.Packages.Length);
+ Assert.AreEqual("SPDXRef-Package-1", doc.Packages[0].Id);
+ Assert.AreEqual("SPDXRef-Package-2", doc.Packages[1].Id);
+
+ // Verify the relationship
+ Assert.AreEqual(2, doc.Relationships.Length);
+ Assert.AreEqual("SPDXRef-Package-2", doc.Relationships[1].Id);
+ Assert.AreEqual(SpdxRelationshipType.BuildToolOf, doc.Relationships[1].RelationshipType);
+ Assert.AreEqual("SPDXRef-Package-1", doc.Relationships[1].RelatedSpdxElement);
+ }
+ finally
+ {
+ File.Delete("spdx.json");
+ File.Delete("workflow.yaml");
+ }
+ }
+
+ [TestMethod]
+ public void AddPackageFromQuery()
+ {
+ // SPDX contents
+ const string spdxContents = "{\r\n" +
+ " \"files\": [],\r\n" +
+ " \"packages\": [" +
+ " {\r\n" +
+ " \"SPDXID\": \"SPDXRef-Package-1\",\r\n" +
+ " \"name\": \"Test Package\",\r\n" +
+ " \"versionInfo\": \"1.0.0\",\r\n" +
+ " \"downloadLocation\": \"https://github.com/demaconsulting/SpdxTool\",\r\n" +
+ " \"licenseConcluded\": \"MIT\"\r\n" +
+ " }\r\n" +
+ " ],\r\n" +
+ " \"relationships\": [" +
+ " {\r\n" +
+ " \"spdxElementId\": \"SPDXRef-DOCUMENT\",\r\n" +
+ " \"relatedSpdxElement\": \"SPDXRef-Package-1\",\r\n" +
+ " \"relationshipType\": \"DESCRIBES\"\r\n" +
+ " }\r\n" +
+ " ],\r\n" +
+ " \"spdxVersion\": \"SPDX-2.2\",\r\n" +
+ " \"dataLicense\": \"CC0-1.0\",\r\n" +
+ " \"SPDXID\": \"SPDXRef-DOCUMENT\",\r\n" +
+ " \"name\": \"Test Document\",\r\n" +
+ " \"documentNamespace\": \"https://sbom.spdx.org\",\r\n" +
+ " \"creationInfo\": {\r\n" +
+ " \"created\": \"2021-10-01T00:00:00Z\",\r\n" +
+ " \"creators\": [ \"Person: Malcolm Nixon\" ]\r\n" +
+ " },\r\n" +
+ " \"documentDescribes\": [ \"SPDXRef-Package-1\" ]\r\n" +
+ "}";
+
+ // Workflow contents
+ const string workflowContents = "steps:\n" +
+ "- command: query\n" +
+ " inputs:\n" +
+ " output: dotnet_version\n" +
+ " pattern: '(?\\d+\\.\\d+\\.\\d+)'\n" +
+ " program: dotnet\n" +
+ " arguments:\n" +
+ " - --version\n" +
+ "- command: add-package\n" +
+ " inputs:\n" +
+ " package:\n" +
+ " id: SPDXRef-Package-DotNet\n" +
+ " name: DotNet SDK\n" +
+ " version: ${{ dotnet_version }}\n" +
+ " download: https://dotnet.microsoft.com/download\n" +
+ " license: MIT\n" +
+ " spdx: spdx.json\n" +
+ " relationship: BUILD_TOOL_OF\n" +
+ " element: SPDXRef-Package-1\n";
+
+ try
+ {
+ // Write the SPDX files
+ File.WriteAllText("spdx.json", spdxContents);
+ File.WriteAllText("workflow.yaml", workflowContents);
+
+ // Run the command
+ var exitCode = Runner.Run(
+ out var output,
+ "dotnet",
+ "DemaConsulting.SpdxTool.dll",
+ "run-workflow",
+ "workflow.yaml");
+
+ // Verify success
+ Assert.AreEqual(0, exitCode);
+
+ // Read the SPDX document
+ Assert.IsTrue(File.Exists("spdx.json"));
+ var doc = Spdx2JsonDeserializer.Deserialize(File.ReadAllText("spdx.json"));
+
+ // Verify both packages present
+ Assert.AreEqual(2, doc.Packages.Length);
+ Assert.AreEqual("SPDXRef-Package-1", doc.Packages[0].Id);
+ Assert.AreEqual("SPDXRef-Package-DotNet", doc.Packages[1].Id);
+
+ // Verify the relationship
+ Assert.AreEqual(2, doc.Relationships.Length);
+ Assert.AreEqual("SPDXRef-Package-DotNet", doc.Relationships[1].Id);
+ Assert.AreEqual(SpdxRelationshipType.BuildToolOf, doc.Relationships[1].RelationshipType);
+ Assert.AreEqual("SPDXRef-Package-1", doc.Relationships[1].RelatedSpdxElement);
+ }
+ finally
+ {
+ File.Delete("spdx.json");
+ File.Delete("workflow.yaml");
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/DemaConsulting.SpdxTool.Tests/TestCopyPackageCommand.cs b/test/DemaConsulting.SpdxTool.Tests/TestCopyPackageCommand.cs
index 5ef2f8a..c61b755 100644
--- a/test/DemaConsulting.SpdxTool.Tests/TestCopyPackageCommand.cs
+++ b/test/DemaConsulting.SpdxTool.Tests/TestCopyPackageCommand.cs
@@ -119,7 +119,7 @@ public void CopyPackage()
"from.spdx.json",
"to.spdx.json",
"SPDXRef-Package-2",
- "CONTAINS",
+ "CONTAINED_BY",
"SPDXRef-Package-1");
// Verify success
@@ -136,9 +136,9 @@ public void CopyPackage()
// Verify the relationship
Assert.AreEqual(2, doc.Relationships.Length);
- Assert.AreEqual("SPDXRef-Package-1", doc.Relationships[1].Id);
- Assert.AreEqual(SpdxRelationshipType.Contains, doc.Relationships[1].RelationshipType);
- Assert.AreEqual("SPDXRef-Package-2", doc.Relationships[1].RelatedSpdxElement);
+ Assert.AreEqual("SPDXRef-Package-2", doc.Relationships[1].Id);
+ Assert.AreEqual(SpdxRelationshipType.ContainedBy, doc.Relationships[1].RelationshipType);
+ Assert.AreEqual("SPDXRef-Package-1", doc.Relationships[1].RelatedSpdxElement);
}
finally
{
diff --git a/test/DemaConsulting.SpdxTool.Tests/TestQueryCommand.cs b/test/DemaConsulting.SpdxTool.Tests/TestQueryCommand.cs
new file mode 100644
index 0000000..56c84b9
--- /dev/null
+++ b/test/DemaConsulting.SpdxTool.Tests/TestQueryCommand.cs
@@ -0,0 +1,75 @@
+using System.Text.RegularExpressions;
+
+namespace DemaConsulting.SpdxTool.Tests;
+
+[TestClass]
+public class TestQueryCommand
+{
+ [TestMethod]
+ public void QueryCommandMissingArguments()
+ {
+ // Run the command
+ var exitCode = Runner.Run(
+ out var output,
+ "dotnet",
+ "DemaConsulting.SpdxTool.dll",
+ "query");
+
+ // Verify error reported
+ Assert.AreEqual(1, exitCode);
+ Assert.IsTrue(output.Contains("'query' command missing arguments"));
+ }
+
+ [TestMethod]
+ public void QueryCommandBadPattern()
+ {
+ // Run the command
+ var exitCode = Runner.Run(
+ out var output,
+ "dotnet",
+ "DemaConsulting.SpdxTool.dll",
+ "query",
+ "pattern",
+ "dotnet",
+ "--version");
+
+ // Verify error reported
+ Assert.AreEqual(1, exitCode);
+ Assert.IsTrue(output.Contains("Pattern must contain a 'value' capture group"));
+ }
+
+ [TestMethod]
+ public void QueryCommandInvalidProgram()
+ {
+ // Run the command
+ var exitCode = Runner.Run(
+ out var output,
+ "dotnet",
+ "DemaConsulting.SpdxTool.dll",
+ "query",
+ @"(?\d+\.\d+\.\d+)",
+ "does-not-exist");
+
+ // Verify error reported
+ Assert.AreEqual(1, exitCode);
+ Assert.IsTrue(output.Contains("Unable to start program 'does-not-exist'"));
+ }
+
+ [TestMethod]
+ public void QueryDotNet()
+ {
+ // Run the command
+ var exitCode = Runner.Run(
+ out var output,
+ "dotnet",
+ "DemaConsulting.SpdxTool.dll",
+ "query",
+ @"(?\d+\.\d+\.\d+)",
+ "dotnet",
+ "--version");
+
+ // Verify error reported
+ Assert.AreEqual(0, exitCode);
+ Assert.IsTrue(Regex.IsMatch(output, @"\d+\.\d+\.\d+"));
+ }
+}
\ No newline at end of file
diff --git a/test/DemaConsulting.SpdxTool.Tests/TestSha256Command.cs b/test/DemaConsulting.SpdxTool.Tests/TestSha256Command.cs
new file mode 100644
index 0000000..dcedeee
--- /dev/null
+++ b/test/DemaConsulting.SpdxTool.Tests/TestSha256Command.cs
@@ -0,0 +1,141 @@
+namespace DemaConsulting.SpdxTool.Tests;
+
+[TestClass]
+public class TestSha256Command
+{
+ [TestMethod]
+ public void Sha256CommandMissingArguments()
+ {
+ // Run the command
+ var exitCode = Runner.Run(
+ out var output,
+ "dotnet",
+ "DemaConsulting.SpdxTool.dll",
+ "sha256");
+
+ // Verify error reported
+ Assert.AreEqual(1, exitCode);
+ Assert.IsTrue(output.Contains("'sha256' command missing arguments"));
+ }
+
+ [TestMethod]
+ public void Sha256CommandGenerateMissingFile()
+ {
+ // Run the command
+ var exitCode = Runner.Run(
+ out var output,
+ "dotnet",
+ "DemaConsulting.SpdxTool.dll",
+ "sha256",
+ "generate",
+ "missing-file.txt");
+
+ // Verify error reported
+ Assert.AreEqual(1, exitCode);
+ Assert.IsTrue(output.Contains("Error calculating sha256 hash for 'missing-file.txt'"));
+ }
+
+ [TestMethod]
+ public void Sha256CommandGenerate()
+ {
+ try
+ {
+ File.WriteAllText("test.txt", "The quick brown fox jumps over the lazy dog");
+
+ // Run the command
+ var exitCode = Runner.Run(
+ out var output,
+ "dotnet",
+ "DemaConsulting.SpdxTool.dll",
+ "sha256",
+ "generate",
+ "test.txt");
+
+ // Verify success reported
+ Assert.AreEqual(0, exitCode);
+
+ // Verify the hash file was created
+ Assert.IsTrue(File.Exists("test.txt.sha256"));
+ var digest = File.ReadAllText("test.txt.sha256");
+ Assert.AreEqual("d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", digest);
+ }
+ finally
+ {
+ File.Delete("test.txt");
+ File.Delete("test.txt.sha256");
+ }
+ }
+
+ [TestMethod]
+ public void Sha256CommandVerifyMissingFile()
+ {
+ // Run the command
+ var exitCode = Runner.Run(
+ out var output,
+ "dotnet",
+ "DemaConsulting.SpdxTool.dll",
+ "sha256",
+ "verify",
+ "missing-file.txt");
+
+ // Verify error reported
+ Assert.AreEqual(1, exitCode);
+ Assert.IsTrue(output.Contains("Error: Could not find file"));
+ }
+
+ [TestMethod]
+ public void Sha256CommandVerifyBad()
+ {
+ try
+ {
+ File.WriteAllText("test.txt", "Test string");
+ File.WriteAllText("test.txt.sha256", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+
+ // Run the command
+ var exitCode = Runner.Run(
+ out var output,
+ "dotnet",
+ "DemaConsulting.SpdxTool.dll",
+ "sha256",
+ "verify",
+ "test.txt");
+
+ // Verify error reported
+ Assert.AreEqual(1, exitCode);
+ Assert.IsTrue(output.Contains("Sha256 hash mismatch for 'test.txt'"));
+ }
+ finally
+ {
+ File.Delete("test.txt");
+ File.Delete("test.txt.sha256");
+ }
+ }
+
+ [TestMethod]
+ public void Sha256CommandVerifyGood()
+ {
+ try
+ {
+ File.WriteAllText("test.txt", "The quick brown fox jumps over the lazy dog");
+ File.WriteAllText("test.txt.sha256", "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592");
+
+ // Run the command
+ var exitCode = Runner.Run(
+ out var output,
+ "dotnet",
+ "DemaConsulting.SpdxTool.dll",
+ "sha256",
+ "verify",
+ "test.txt");
+
+ // Verify success reported
+ Assert.AreEqual(0, exitCode);
+ Assert.IsTrue(output.Contains("Sha256 Digest OK for 'test.txt'"));
+ }
+ finally
+ {
+ File.Delete("test.txt");
+ File.Delete("test.txt.sha256");
+ }
+ }
+}
\ No newline at end of file