diff --git a/README.md b/README.md index 9e74360..54d8978 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,9 @@ Commands: add-package Add package to SPDX document (workflow only). add-relationship Add relationship between elements. copy-package Copy package between SPDX documents (workflow only). + diagram Generate mermaid diagram. find-package Find package ID in SPDX document - get-version Get the version of an SPDX package. + get-version Get the version of an SPDX package. hash Generate or verify hashes of files print Print text to the console query [args] Query program output for value diff --git a/docs/spdx-tool-command-line.md b/docs/spdx-tool-command-line.md index 3017964..58e0017 100644 --- a/docs/spdx-tool-command-line.md +++ b/docs/spdx-tool-command-line.md @@ -35,6 +35,7 @@ Commands: add-package Add package to SPDX document (workflow only). add-relationship Add relationship between elements. copy-package Copy package between SPDX documents (workflow only). + diagram Generate mermaid diagram. find-package Find package ID in SPDX document get-version Get the version of an SPDX package. hash Generate or verify hashes of files diff --git a/docs/spdx-tool-workflow-files.md b/docs/spdx-tool-workflow-files.md index 0d79c97..d80bf74 100644 --- a/docs/spdx-tool-workflow-files.md +++ b/docs/spdx-tool-workflow-files.md @@ -148,6 +148,12 @@ steps: element: # Related element comment: # Optional comment + # Generate a mermaid diagram from an SPDX document +- command: diagram + inputs: + spdx: # SPDX file name + mermaid: # Mermaid file name + # finds the package ID for a package in an SPDX document - command: find-package inputs: @@ -252,242 +258,3 @@ steps: spdx: # SPDX file name ntia: true # Optional NTIA checking ``` -can be driven using workflow yaml files of the following format: - -```yaml -# Workflow parameters -parameters: - parameter-name: value - -# Workflow steps -steps: -- command: - inputs: - - -- command: - inputs: - input1: value - input2: ${{ parameter-name }} -``` - -## YAML Variables - -Variables are specified at the top of the workflow file in a parameters section: - -```yaml -# Workflow parameters -parameters: - parameter1: value1 - parameter2: value2 -``` - -Variables can be expanded in step inputs using the dollar expansion syntax - -```yaml -# Workflow steps -steps: -- command: - inputs: - input1: ${{ parameter1 }} - input2: Insert ${{ parameter2 }} in the middle -``` - -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: - reported-version: unknown - dotnet-version: unknown - pretty-version: unknown - -steps: -- command: get-version - inputs: - spdx: manifest.spdx.json - id: SPDXRef-DotNetSDK - output: reported-version - -- command: query - inputs: - output: dotnet-version - pattern: '(?\d+\.\d+\.\d+)' - program: dotnet - arguments: - - '--version' - -- command: set-variable - inputs: - value: DotNet Version is ${{ dotnet-version }} - output: pretty-version -``` - - -## YAML Commands - -The following are the supported commands and their formats: - -```yaml -steps: - - # Add a package to an SPDX document -- command: add-package - inputs: - spdx: # SPDX file name - package: # New package information - id: # New package ID - name: # New package name - download: # New package download URL - version: # Optional package version - filename: # Optional package filename - supplier: # Optional package supplier - originator: # Optional package originator - homepage: # Optional package homepage - copyright: # Optional package copyright - summary: # Optional package summary - description: # Optional package description - license: # Optional package license - purl: # Optional package purl - cpe23: # Optional package cpe23 - relationships: # Optional relationships - - type: # Relationship type - element: # Related element - comment: # Optional comment - - type: # Relationship type - element: # Related element - comment: # Optional comment - - # Add a relationship to an SPDX document -- command: add-relationship - inputs: - spdx: # SPDX file name - id: # Element ID - relationships: - - type: # Relationship type - element: # Related element - comment: # Optional comment - - type: # Relationship type - element: # Related element - comment: # Optional comment - - # Copy a package from one SPDX document to another SPDX document -- command: copy-package - inputs: - from: # Source SPDX file name - to: # Destination SPDX file name - package: # Package ID - recursive: true # Optional recursive flag - relationships: # Optional relationships - - type: # Relationship type - element: # Related element - comment: # Optional comment - - type: # Relationship type - element: # Related element - comment: # Optional comment - - # finds the package ID for a package in an SPDX document -- command: find-package - inputs: - output: # Output variable for package ID - spdx: # SPDX file name - name: # Optional package name - version: # Optional package version - filename: # Optional package filename - download: # Optional package download URL - - # Get the version of a package in an SPDX document -- command: get-version - inputs: - spdx: # SPDX file name - id: # Package ID - output: # Output variable - - # Perform hash operations on the specified file -- command: hash - inputs: - operation: generate | verify - algorithm: sha256 - file: - - # Print text to the console -- command: print - inputs: - text: - - Some text to print - - The value of variable is ${{ variable }} - - # 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: # SPDX file name - old: # Old element ID - new: # New element ID - - # Run a separate workflow file/url -- command: run-workflow - inputs: - file: # Optional workflow file - url: # Optional workflow url - integrity: # Optional workflow integrity check - parameters: - name: # Optional workflow parameter - name: # Optional workflow parameter - outputs: - name: # Optional output to save to variable - name: # Optional output to save to variable - - # Set a workflow variable -- command: set-variable - inputs: - value: # New value - output: # Variable to set - - # Create a summary markdown from the specified SPDX document -- command: to-markdown - inputs: - spdx: # SPDX file name - markdown: # Output markdown file - title: # Optional title - depth: <depth> # Optional heading depth - - # Update a package in an SPDX document -- command: update-package - inputs: - spdx: <spdx.json> # SPDX filename - package: # Package information - id: <id> # Package ID - name: <name> # Optional new package name - download: <download-url> # Optional new package download URL - version: <version> # Optional new package version - filename: <filename> # Optional new package filename - supplier: <supplier> # Optional new package supplier - originator: <originator> # Optional new package originator - homepage: <homepage> # Optional new package homepage - copyright: <copyright> # Optional new package copyright - summary: <summary> # Optional new package summary - description: <description> # Optional new package description - license: <license> # Optional new package license - - # Validate an SPDX document -- command: validate - inputs: - spdx: <spdx.json> # SPDX file name - ntia: true # Optional NTIA checking -``` diff --git a/src/DemaConsulting.SpdxTool/Commands/CommandRegistry.cs b/src/DemaConsulting.SpdxTool/Commands/CommandRegistry.cs index 063674b..b091863 100644 --- a/src/DemaConsulting.SpdxTool/Commands/CommandRegistry.cs +++ b/src/DemaConsulting.SpdxTool/Commands/CommandRegistry.cs @@ -34,6 +34,7 @@ public static class CommandsRegistry { AddPackage.Entry.Name, AddPackage.Entry }, { AddRelationship.Entry.Name, AddRelationship.Entry }, { CopyPackage.Entry.Name, CopyPackage.Entry }, + { Diagram.Entry.Name, Diagram.Entry }, { FindPackage.Entry.Name, FindPackage.Entry }, { GetVersion.Entry.Name, GetVersion.Entry }, { Hash.Entry.Name, Hash.Entry }, diff --git a/src/DemaConsulting.SpdxTool/Commands/Diagram.cs b/src/DemaConsulting.SpdxTool/Commands/Diagram.cs new file mode 100644 index 0000000..64c86af --- /dev/null +++ b/src/DemaConsulting.SpdxTool/Commands/Diagram.cs @@ -0,0 +1,237 @@ +// Copyright (c) 2024 DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Text; +using DemaConsulting.SpdxModel; +using DemaConsulting.SpdxTool.Spdx; +using YamlDotNet.Core; +using YamlDotNet.RepresentationModel; + +namespace DemaConsulting.SpdxTool.Commands; + +/// <summary> +/// Command to generate a diagram of an SPDX document +/// </summary> +public sealed class Diagram : Command +{ + /// <summary> + /// Relationship direction enumeration + /// </summary> + private enum RelationshipDirection + { + /// <summary> + /// ID is the parent of the related element + /// </summary> + Parent, + + /// <summary> + /// ID is the child of the related element + /// </summary> + Child, + + /// <summary> + /// ID and related element are siblings + /// </summary> + Sibling + } + + /// <summary> + /// Command name + /// </summary> + private const string Command = "diagram"; + + /// <summary> + /// Singleton instance of this command + /// </summary> + public static readonly Diagram Instance = new(); + + /// <summary> + /// Entry information for this command + /// </summary> + public static readonly CommandEntry Entry = new( + Command, + "diagram <spdx.json> <mermaid.txt> [tools]", + "Generate mermaid diagram.", + new[] + { + "This command generates a mermaid diagram from an SPDX document.", + "", + " - command: diagram", + " inputs:", + " spdx: <spdx.json> # SPDX file name", + " mermaid: <mermaid.txt> # Mermaid file name", + " tools: true # Optionally include tools" + }, + Instance); + + /// <summary> + /// Private constructor - this is a singleton + /// </summary> + private Diagram() + { + } + + /// <inheritdoc /> + public override void Run(string[] args) + { + // Report an error if the number of arguments is less than 2 + if (args.Length < 2) + throw new CommandUsageException("'diagram' command invalid arguments"); + + // Check for options + var tools = false; + foreach (var option in args.Skip(2)) + { + switch (option) + { + case "tools": + tools = true; + break; + + default: + throw new CommandUsageException($"'diagram' command invalid option {option}"); + } + } + + // Generate the diagram + GenerateDiagram(args[0], args[1], tools); + } + + /// <inheritdoc /> + public override void Run(YamlMappingNode step, Dictionary<string, string> variables) + { + // Get the step inputs + var inputs = GetMapMap(step, "inputs"); + + // Get the 'spdx' input + var spdxFile = GetMapString(inputs, "spdx", variables) ?? + throw new YamlException(step.Start, step.End, "'diagram' command missing 'spdx' input"); + + // Get the 'mermaid' input + var mermaidFile = GetMapString(inputs, "mermaid", variables) ?? + throw new YamlException(step.Start, step.End, "'diagram' command missing 'mermaid' input"); + + // Get the 'tools' input + var toolsText = GetMapString(inputs, "tools", variables) ?? "false"; + if (!bool.TryParse(toolsText, out var tools)) + throw new YamlException(step.Start, step.End, "'diagram' invalid 'tools' input"); + + // Generate the diagram + GenerateDiagram(spdxFile, mermaidFile, tools); + } + + /// <summary> + /// Generate mermaid entity-relationship diagram from SPDX document + /// </summary> + /// <param name="spdxFile">SPDX document file name</param> + /// <param name="mermaidFile">Mermaid diagram file name</param> + /// <param name="tools">True to include tools</param> + public static void GenerateDiagram(string spdxFile, string mermaidFile, bool tools = false) + { + // Load the SPDX document + var doc = SpdxHelpers.LoadJsonDocument(spdxFile); + + // Generate the mermaid diagram + var diagram = new StringBuilder(); + diagram.AppendLine("erDiagram"); + + // Process all relationships + foreach (var relationship in doc.Relationships) + { + // Skip tools if not requested + if (!tools && relationship.RelationshipType is + SpdxRelationshipType.BuildToolOf or + SpdxRelationshipType.DevToolOf or + SpdxRelationshipType.TestToolOf) + continue; + + // Get the packages + var a = doc.GetElement<SpdxPackage>(relationship.Id); + var b = doc.GetElement<SpdxPackage>(relationship.RelatedSpdxElement); + if (a == null || b == null) + continue; + + // Get the relationship direction + var direction = GetDirection(relationship.RelationshipType); + var from = direction switch + { + RelationshipDirection.Parent => a, + RelationshipDirection.Child => b, + RelationshipDirection.Sibling => a, + _ => throw new InvalidDataException() + }; + var to = direction switch + { + RelationshipDirection.Parent => b, + RelationshipDirection.Child => a, + RelationshipDirection.Sibling => b, + _ => throw new InvalidDataException() + }; + + // Write the relationship to the diagram + var type = relationship.RelationshipType.ToText(); + diagram.AppendLine($" \"{from.Name} / {from.Version}\" ||--|| \"{to.Name} / {to.Version}\" : \"{type}\""); + } + + // Write the diagram to the file + File.WriteAllText(mermaidFile, diagram.ToString()); + } + + /// <summary> + /// Get the relationship direction + /// </summary> + /// <param name="type">Relationship type</param> + /// <returns>Relationship direction</returns> + private static RelationshipDirection GetDirection(SpdxRelationshipType type) + { + return type switch + { + SpdxRelationshipType.Describes => RelationshipDirection.Parent, + SpdxRelationshipType.DescribedBy => RelationshipDirection.Child, + SpdxRelationshipType.Contains => RelationshipDirection.Parent, + SpdxRelationshipType.ContainedBy => RelationshipDirection.Child, + SpdxRelationshipType.DependsOn => RelationshipDirection.Parent, + SpdxRelationshipType.DependencyOf => RelationshipDirection.Child, + SpdxRelationshipType.DependencyManifestOf => RelationshipDirection.Sibling, + SpdxRelationshipType.BuildDependencyOf => RelationshipDirection.Child, + SpdxRelationshipType.DevDependencyOf => RelationshipDirection.Child, + SpdxRelationshipType.OptionalDependencyOf => RelationshipDirection.Child, + SpdxRelationshipType.ProvidedDependencyOf => RelationshipDirection.Child, + SpdxRelationshipType.TestDependencyOf => RelationshipDirection.Child, + SpdxRelationshipType.RuntimeDependencyOf => RelationshipDirection.Child, + SpdxRelationshipType.Generates => RelationshipDirection.Parent, + SpdxRelationshipType.GeneratedFrom => RelationshipDirection.Child, + SpdxRelationshipType.DistributionArtifact => RelationshipDirection.Child, + SpdxRelationshipType.PatchFor => RelationshipDirection.Child, + SpdxRelationshipType.PatchApplied => RelationshipDirection.Child, + SpdxRelationshipType.DynamicLink => RelationshipDirection.Parent, + SpdxRelationshipType.StaticLink => RelationshipDirection.Parent, + SpdxRelationshipType.BuildToolOf => RelationshipDirection.Child, + SpdxRelationshipType.DevToolOf => RelationshipDirection.Child, + SpdxRelationshipType.TestToolOf => RelationshipDirection.Child, + SpdxRelationshipType.DocumentationOf => RelationshipDirection.Child, + SpdxRelationshipType.OptionalComponentOf => RelationshipDirection.Child, + SpdxRelationshipType.PackageOf => RelationshipDirection.Child, + SpdxRelationshipType.PrerequisiteFor => RelationshipDirection.Child, + SpdxRelationshipType.HasPrerequisite => RelationshipDirection.Parent, + _ => RelationshipDirection.Sibling + }; + } +} \ No newline at end of file