diff --git a/.cspell.json b/.cspell.json index 29541c1..e063685 100644 --- a/.cspell.json +++ b/.cspell.json @@ -2,79 +2,106 @@ "version": "0.2", "language": "en", "words": [ - "SPDX", - "SBOM", "Anson", + "Blockquotes", + "buildmark", + "BuildMark", + "buildnotes", + "camelcase", + "Checkmarx", + "CodeQL", + "copilot", + "cspell", + "csproj", + "dbproj", + "dcterms", + "declaredat", "Dema", - "DemaConsulting", + "demaconsulting", "DEMACONSULTINGNUGETKEY", + "Dependabot", + "dependabot", + "doctitle", "dotnet", "dotnettool", - "nuget", - "csproj", - "MSTest", - "YamlDotNet", - "SpdxModel", - "SpdxTool", - "TemplateDotNetTool", - "trx", - "json", - "yaml", - "mermaid", - "purl", - "ntia", - "Sonar", - "SonarCloud", - "SonarAnalyzer", "editorconfig", + "filepart", + "fsproj", + "Gidget", + "gitattributes", "gitignore", + "hotspots", "ibiqlik", + "LINQ", + "maintainer", "markdownlint", + "mermaid", + "MSTest", "mstest", - "yamllint", + "myterm", "ncipollo", + "NOASSERTION", + "ntia", + "nuget", "nupkg", - "snupkg", "opencover", + "pagetitle", + "Pandoc", + "pandoc", + "purl", + "Pylint", + "Qube", + "reqstream", + "ReqStream", + "sandboxed", + "Sarif", + "SarifMark", + "SBOM", + "Semgrep", + "semver", + "slnx", + "snupkg", + "Sonar", + "SonarAnalyzer", + "SonarCloud", + "sonarmark", + "SonarMark", "sonarscanner", - "wildcards", + "SonarQube", + "spdx", + "SPDX", "SPDXID", - "NOASSERTION", - "declaredat", - "sandboxed", - "hotspots", - "Pandoc" - ], - "ignoreWords": [ - "demaconsulting" + "SpdxModel", + "SpdxTool", + "streetsidesoftware", + "TemplateDotNetTool", + "templatetool", + "testname", + "TMPL", + "tracematrix", + "triaging", + "Trivy", + "trx", + "vbproj", + "vcxproj", + "Weasyprint", + "wildcards", + "YamlDotNet", + "yamllint" ], "ignorePaths": [ - "node_modules/**", - ".git/**", - "bin/**", - "obj/**", - "*.min.js", - "*.min.css", + "node_modules", + ".git", + "bin", + "obj", + "*.nupkg", + "*.snupkg", + "*.dll", + "*.exe", + "*.trx", + "*.spdx.json", "package-lock.json", - ".vs/**", - ".vscode/**", - "coverage/**", - "TestResults/**", - "**/*.DotSettings", - "**/*.csproj", + "yarn.lock", "AGENT_REPORT_*.md" - ], - "flagWords": [], - "patterns": [ - { - "name": "Markdown links", - "pattern": "\\[.*?\\]\\(.*?\\)", - "description": "Ignore markdown links" - }, - { - "name": "Inline code", - "pattern": "`[^`]*`", - "description": "Ignore inline code blocks" - } ] } diff --git a/.editorconfig b/.editorconfig index 1268a7b..f3253fb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,34 +5,47 @@ root = true # All files [*] -charset = utf-8 +charset = utf-8-bom +end_of_line = lf indent_style = space +indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true -# Code files -[*.{cs,csx,vb,vbx}] -indent_size = 4 - -# XML project files -[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] -indent_size = 2 +# Markdown files +[*.md] +trim_trailing_whitespace = false -# XML config files -[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +# YAML files +[*.{yml,yaml}] indent_size = 2 # JSON files [*.json] indent_size = 2 -# YAML files -[*.{yml,yaml}] +# XML files +[*.{xml,csproj,props,targets}] indent_size = 2 -# Markdown files -[*.md] -trim_trailing_whitespace = false +# C# files +[*.cs] +indent_size = 4 + +# Code style rules +csharp_prefer_braces = true:warning +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:warning +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent # Dotnet code style settings: [*.{cs,vb}] @@ -41,33 +54,10 @@ trim_trailing_whitespace = false dotnet_sort_system_directives_first = true dotnet_separate_import_directive_groups = false -# Avoid "this." and "Me." if not necessary -dotnet_style_qualification_for_field = false:suggestion -dotnet_style_qualification_for_property = false:suggestion -dotnet_style_qualification_for_method = false:suggestion -dotnet_style_qualification_for_event = false:suggestion - -# Use language keywords instead of framework type names for type references -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion -dotnet_style_predefined_type_for_member_access = true:suggestion - -# Suggest more modern language features when available -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_auto_properties = true:suggestion -dotnet_style_prefer_conditional_expression_over_return = false:silent -dotnet_style_prefer_conditional_expression_over_assignment = false:silent -dotnet_style_prefer_compound_assignment = true:suggestion - # Naming Conventions -dotnet_naming_rule.interfaces_should_be_prefixed_with_i.severity = warning -dotnet_naming_rule.interfaces_should_be_prefixed_with_i.symbols = interface -dotnet_naming_rule.interfaces_should_be_prefixed_with_i.style = begins_with_i +dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i dotnet_naming_rule.types_should_be_pascal_case.severity = warning dotnet_naming_rule.types_should_be_pascal_case.symbols = types @@ -91,93 +81,26 @@ dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, int dotnet_naming_symbols.non_field_members.required_modifiers = # Naming styles -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case - dotnet_naming_style.begins_with_i.required_prefix = I dotnet_naming_style.begins_with_i.required_suffix = dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case -# CSharp code style settings: -[*.cs] -# Newline settings -csharp_new_line_before_open_brace = all -csharp_new_line_before_else = true -csharp_new_line_before_catch = true -csharp_new_line_before_finally = true -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_between_query_expression_clauses = true - -# Indentation preferences -csharp_indent_case_contents = true -csharp_indent_switch_labels = true -csharp_indent_labels = flush_left -csharp_indent_block_contents = true -csharp_indent_braces = false -csharp_indent_case_contents_when_block = false - -# Space preferences -csharp_space_after_cast = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_parentheses = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_around_binary_operators = before_and_after -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false - -# Wrapping preferences -csharp_preserve_single_line_statements = false -csharp_preserve_single_line_blocks = true - -# Prefer "var" everywhere -csharp_style_var_for_built_in_types = true:suggestion -csharp_style_var_when_type_is_apparent = true:suggestion -csharp_style_var_elsewhere = true:suggestion - -# Prefer method-like constructs to have a block body -csharp_style_expression_bodied_methods = false:none -csharp_style_expression_bodied_constructors = false:none -csharp_style_expression_bodied_operators = false:none - -# Prefer property-like constructs to have an expression-body -csharp_style_expression_bodied_properties = true:suggestion -csharp_style_expression_bodied_indexers = true:suggestion -csharp_style_expression_bodied_accessors = true:suggestion - -# Suggest more modern language features when available -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion -csharp_style_throw_expression = true:suggestion -csharp_style_conditional_delegate_call = true:suggestion -csharp_prefer_simple_default_expression = true:suggestion -csharp_style_prefer_local_over_anonymous_function = true:suggestion -csharp_style_prefer_index_operator = true:suggestion -csharp_style_prefer_range_operator = true:suggestion -csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion -csharp_style_prefer_tuple_swap = true:suggestion -csharp_style_deconstructed_variable_declaration = true:suggestion -csharp_style_unused_value_assignment_preference = discard_variable:suggestion -csharp_style_unused_value_expression_statement_preference = discard_variable:silent - -# Code block preferences -csharp_prefer_braces = true:suggestion -csharp_prefer_simple_using_statement = true:suggestion +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case -# Using directive preferences -csharp_using_directive_placement = outside_namespace:warning +# Organize usings +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false -# Code quality rules +# Code quality - set to suggestion to not break existing code dotnet_code_quality_unused_parameters = all:suggestion +# Nullable reference types +csharp_nullable_reference_types = enable + # Suppress specific CA rules dotnet_diagnostic.CA1303.severity = none # Do not pass literals as localized parameters (not localizing) dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task (library code, not UI) diff --git a/.gitignore b/.gitignore index 1265d7e..b8d2d16 100644 --- a/.gitignore +++ b/.gitignore @@ -397,9 +397,32 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +.idea/ # Documentation build artifacts docs/**/*.html +docs/**/*.pdf +!docs/template/** +docs/requirements/requirements.md +docs/justifications/justifications.md +docs/tracematrix/tracematrix.md +docs/quality/codeql-quality.md +docs/quality/sonar-quality.md +docs/buildnotes.md +docs/buildnotes/versions.md + +# Test results +TestResults/ +*.trx +coverage.opencover.xml + +# VersionMark captures (generated during CI/CD) +versionmark-*.json + +# Temporary files +*.tmp +*.temp +*.log # Agent report files (temporary inter-agent communication) AGENT_REPORT_*.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00ca18e..e875235 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ contributing to this project. ## Code of Conduct This project adheres to a Code of Conduct that all contributors are expected to follow. Please read -[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) before contributing. +[CODE_OF_CONDUCT.md][code-of-conduct] before contributing. ## Getting Started @@ -231,9 +231,9 @@ Contributors do not need to worry about versioning or releases. ## Additional Resources -- [SPDX Specification](https://spdx.dev/) -- [.NET Coding Conventions](https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions) -- [MSTest Documentation](https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-mstest) +- [SPDX Specification][spdx-spec] +- [.NET Coding Conventions][dotnet-conventions] +- [MSTest Documentation][mstest-docs] ## Questions? @@ -245,3 +245,8 @@ If you have questions about contributing, please: 4. Contact the maintainers Thank you for contributing to SpdxTool! + +[code-of-conduct]: CODE_OF_CONDUCT.md +[spdx-spec]: https://spdx.dev/ +[dotnet-conventions]: https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions +[mstest-docs]: https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-mstest diff --git a/SECURITY.md b/SECURITY.md index b9d3b4c..3ee3c0c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -13,7 +13,7 @@ We release patches for security vulnerabilities. Only the latest version is curr If you discover a security vulnerability within this project, please use GitHub's vulnerability reporting feature: -1. Go to the [Security tab](https://github.com/demaconsulting/SpdxTool/security) of this repository +1. Go to the [Security tab][security-tab] of this repository 2. Click on "Report a vulnerability" 3. Fill out the vulnerability report form with details about the issue @@ -86,14 +86,21 @@ We recognize and thank security researchers who help improve our project's secur For security-related questions or concerns that are not vulnerabilities, you can: -- Open a discussion in [GitHub Discussions](https://github.com/demaconsulting/SpdxTool/discussions) +- Open a discussion in [GitHub Discussions][github-discussions] - Contact the maintainers through the repository ## Additional Resources -- [SPDX Security Specification](https://spdx.github.io/spdx-spec/) -- [OWASP Top 10](https://owasp.org/www-project-top-ten/) -- [CWE - Common Weakness Enumeration](https://cwe.mitre.org/) -- [GitHub Security Best Practices](https://docs.github.com/en/code-security) +- [SPDX Security Specification][spdx-security] +- [OWASP Top 10][owasp-top10] +- [CWE - Common Weakness Enumeration][cwe] +- [GitHub Security Best Practices][github-security] Thank you for helping keep this project and its users safe! + +[security-tab]: https://github.com/demaconsulting/SpdxTool/security +[github-discussions]: https://github.com/demaconsulting/SpdxTool/discussions +[spdx-security]: https://spdx.github.io/spdx-spec/ +[owasp-top10]: https://owasp.org/www-project-top-ten/ +[cwe]: https://cwe.mitre.org/ +[github-security]: https://docs.github.com/en/code-security diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..17d26f8 --- /dev/null +++ b/build.bat @@ -0,0 +1,16 @@ +@echo off +REM Build and test SPDX Tool (Windows) + +echo Building SPDX Tool... +dotnet build --configuration Release +if %errorlevel% neq 0 exit /b %errorlevel% + +echo Running unit tests... +dotnet test --configuration Release +if %errorlevel% neq 0 exit /b %errorlevel% + +echo Running self-validation... +dotnet run --project src/DemaConsulting.SpdxTool --configuration Release --framework net10.0 --no-build -- --validate +if %errorlevel% neq 0 exit /b %errorlevel% + +echo Build, tests, and validation completed successfully! diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..4ff18fb --- /dev/null +++ b/build.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Build and test SPDX Tool + +set -e # Exit on error + +echo "🔧 Building SPDX Tool..." +dotnet build --configuration Release + +echo "🧪 Running unit tests..." +dotnet test --configuration Release + +echo "✅ Running self-validation..." +dotnet run --project src/DemaConsulting.SpdxTool --configuration Release --framework net10.0 --no-build -- --validate + +echo "✨ Build, tests, and validation completed successfully!" diff --git a/docs/guide/guide.md b/docs/guide/guide.md index 34c8e2e..dbe0bd1 100644 --- a/docs/guide/guide.md +++ b/docs/guide/guide.md @@ -805,8 +805,8 @@ This provides detailed information about: SpdxTool supports the SPDX 2.3 specification. For details, see: -* [SPDX Specification](https://spdx.github.io/spdx-spec/) -* [SPDX GitHub](https://github.com/spdx/spdx-spec) +* [SPDX Specification][spdx-spec] +* [SPDX GitHub][spdx-github] ### NTIA Minimum Elements @@ -820,21 +820,21 @@ The NTIA minimum elements for SBOM include: * Dependency relationships * Author of SBOM data -For more information, see: [NTIA SBOM Minimum Elements](https://www.ntia.gov/files/ntia/publications/sbom_minimum_elements_report.pdf) +For more information, see: [NTIA SBOM Minimum Elements][ntia-sbom] ### Version History -See the [GitHub releases page](https://github.com/demaconsulting/SpdxTool/releases) for detailed version history. +See the [GitHub releases page][releases] for detailed version history. ### License SpdxTool is licensed under the MIT License. See the -[LICENSE](https://github.com/demaconsulting/SpdxTool/blob/main/LICENSE) file for details. +[LICENSE][license] file for details. ### Contributing Contributions are welcome! Please see the -[Contributing Guidelines](https://github.com/demaconsulting/SpdxTool/blob/main/CONTRIBUTING.md) for details. +[Contributing Guidelines][contributing] for details. ### Support @@ -849,3 +849,10 @@ For issues, questions, or feature requests: * **Microsoft SBOM Tool**: * **.NET Tool Documentation**: * **SPDX Model Library**: + +[spdx-spec]: https://spdx.github.io/spdx-spec/ +[spdx-github]: https://github.com/spdx/spdx-spec +[ntia-sbom]: https://www.ntia.gov/files/ntia/publications/sbom_minimum_elements_report.pdf +[releases]: https://github.com/demaconsulting/SpdxTool/releases +[license]: https://github.com/demaconsulting/SpdxTool/blob/main/LICENSE +[contributing]: https://github.com/demaconsulting/SpdxTool/blob/main/CONTRIBUTING.md diff --git a/docs/spdx-tool-and-sbom-tool.md b/docs/spdx-tool-and-sbom-tool.md index 2e251c3..766270d 100644 --- a/docs/spdx-tool-and-sbom-tool.md +++ b/docs/spdx-tool-and-sbom-tool.md @@ -2,7 +2,7 @@ A common approach to creating SPDX SBOMs is: -1. Use the [Microsoft SBOM DotNet Tool](https://github.com/microsoft/sbom-tool) to create an SBOM +1. Use the [Microsoft SBOM DotNet Tool][sbom-tool] to create an SBOM 2. Use SPDX Tool to enhance the SBOM with extended information such as build tools ## Tool Manifest File @@ -68,3 +68,5 @@ This example workflow: run: | dotnet spdx-tool run-workflow spdx-workflow.yaml ``` + +[sbom-tool]: https://github.com/microsoft/sbom-tool diff --git a/docs/spdx-tool-command-line.md b/docs/spdx-tool-command-line.md index 607a6fe..7e9996c 100644 --- a/docs/spdx-tool-command-line.md +++ b/docs/spdx-tool-command-line.md @@ -2,7 +2,7 @@ ## Installation -SPDX Tool is distributed as a nuget package on [nuget.org](https://www.nuget.org/packages/DemaConsulting.SpdxTool). +SPDX Tool is distributed as a nuget package on [nuget.org][nuget-package]. The following will add SPDX Tool to a Dotnet tool manifest file: @@ -76,3 +76,5 @@ From a YAML file this can be used as: name: # Optional output to save to variable name: # Optional output to save to variable ``` + +[nuget-package]: https://www.nuget.org/packages/DemaConsulting.SpdxTool diff --git a/docs/spdx-tool-workflow-files.md b/docs/spdx-tool-workflow-files.md index 2378741..4e8148c 100644 --- a/docs/spdx-tool-workflow-files.md +++ b/docs/spdx-tool-workflow-files.md @@ -29,7 +29,7 @@ steps: ## Variables -Variables may be declaredat the top of the workflow file in a parameters section, or may be created when used as +Variables may be declared at the top of the workflow file in a parameters section, or may be created when used as an output in a workflow step. ```yaml diff --git a/lint.bat b/lint.bat new file mode 100644 index 0000000..1fb5ff2 --- /dev/null +++ b/lint.bat @@ -0,0 +1,20 @@ +@echo off +REM Run all linters for SPDX Tool (Windows) + +echo Checking markdown... +call npx markdownlint-cli2 "**/*.md" +if %errorlevel% neq 0 exit /b %errorlevel% + +echo Checking spelling... +call npx cspell "**/*.{cs,md,json,yaml,yml}" --no-progress +if %errorlevel% neq 0 exit /b %errorlevel% + +echo Checking YAML... +call yamllint -c .yamllint.yaml . +if %errorlevel% neq 0 exit /b %errorlevel% + +echo Checking code formatting... +dotnet format --verify-no-changes +if %errorlevel% neq 0 exit /b %errorlevel% + +echo All linting passed! diff --git a/lint.sh b/lint.sh new file mode 100755 index 0000000..671df6f --- /dev/null +++ b/lint.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Run all linters for SPDX Tool + +set -e # Exit on error + +echo "📝 Checking markdown..." +npx markdownlint-cli2 "**/*.md" + +echo "🔤 Checking spelling..." +npx cspell "**/*.{cs,md,json,yaml,yml}" --no-progress + +echo "📋 Checking YAML..." +yamllint -c .yamllint.yaml . + +echo "🎨 Checking code formatting..." +dotnet format --verify-no-changes + +echo "✨ All linting passed!" diff --git a/src/DemaConsulting.SpdxTool/Commands/AddPackage.cs b/src/DemaConsulting.SpdxTool/Commands/AddPackage.cs index a5b7c4c..9a17aca 100644 --- a/src/DemaConsulting.SpdxTool/Commands/AddPackage.cs +++ b/src/DemaConsulting.SpdxTool/Commands/AddPackage.cs @@ -161,7 +161,7 @@ public static void Add(SpdxDocument doc, SpdxPackage package) { // Copy the new package p = package.DeepCopy(); - doc.Packages = [..doc.Packages.Append(p)]; + doc.Packages = [.. doc.Packages.Append(p)]; } } @@ -183,7 +183,9 @@ public static SpdxPackage ParsePackage(string command, YamlMappingNode packageMa // Verify package ID if (packageId.Length == 0 || packageId == "SPDXRef-DOCUMENT") + { throw new CommandUsageException("Invalid package ID"); + } // Construct the package var package = new SpdxPackage @@ -265,4 +267,4 @@ public static SpdxPackage ParsePackage(string command, YamlMappingNode packageMa // Return the package return package; } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/Commands/AddRelationship.cs b/src/DemaConsulting.SpdxTool/Commands/AddRelationship.cs index e02619a..9e7db1e 100644 --- a/src/DemaConsulting.SpdxTool/Commands/AddRelationship.cs +++ b/src/DemaConsulting.SpdxTool/Commands/AddRelationship.cs @@ -87,7 +87,9 @@ public override void Run(Context context, string[] args) { // Report an error if the number of arguments is less than 4 if (args.Length < 4) + { throw new CommandUsageException("'add-relationship' command missing arguments"); + } var spdxFile = args[0]; var relationship = new SpdxRelationship @@ -119,7 +121,9 @@ public override void Run(Context context, YamlMappingNode step, Dictionary { // Get the relationship map - if (node is not YamlMappingNode relationshipMap) - throw new YamlException(node.Start, node.End, $"'{command}' relationship must be a mapping"); + if (node is not YamlMappingNode relationshipMap) { throw new YamlException(node.Start, node.End, $"'{command}' relationship must be a mapping"); } // Parse the relationship return Parse(command, packageId, relationshipMap, variables); @@ -239,4 +244,4 @@ public static SpdxRelationship Parse( // Return the relationship return relationship; } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/Commands/Command.cs b/src/DemaConsulting.SpdxTool/Commands/Command.cs index ea6b657..947dc1f 100644 --- a/src/DemaConsulting.SpdxTool/Commands/Command.cs +++ b/src/DemaConsulting.SpdxTool/Commands/Command.cs @@ -53,10 +53,10 @@ public static string Expand(string text, Dictionary variables) { // Use a StringBuilder to assemble the expanded string var builder = new System.Text.StringBuilder(text.Length); - + // Use a Stack to track macro-body-start-index positions var macroStack = new Stack(); - + // Scan through the input text var i = 0; while (i < text.Length) @@ -75,34 +75,44 @@ public static string Expand(string text, Dictionary variables) { // Verify we have a matching macro start if (macroStack.Count == 0) + { throw new InvalidOperationException("Unmatched '}}' in variable expansion"); - + } + // Pop the macro-body-start-index var macroBodyStart = macroStack.Pop(); - + // Extract the macro body from the StringBuilder var macroLength = builder.Length - macroBodyStart; var name = builder.ToString(macroBodyStart, macroLength).Trim(); - + // Check for empty variable name if (string.IsNullOrWhiteSpace(name)) + { throw new InvalidOperationException("Empty variable name in macro expansion"); - + } + // Look up the value string? value; if (name.StartsWith("environment.")) + { value = Environment.GetEnvironmentVariable(name[12..]); + } else + { variables.TryGetValue(name, out value); - + } + // Fail if the lookup failed if (value == null) + { throw new InvalidOperationException($"Undefined variable {name}"); - + } + // Replace the macro body with the value builder.Remove(macroBodyStart, macroLength); builder.Append(value); - + i += 2; // Skip "}}" } else @@ -112,11 +122,13 @@ public static string Expand(string text, Dictionary variables) i++; } } - + // Verify all macros were closed if (macroStack.Count > 0) + { throw new InvalidOperationException("Unmatched '${{' in variable expansion"); - + } + return builder.ToString(); } @@ -130,7 +142,9 @@ public static string Expand(string text, Dictionary variables) { // Handle null map if (map == null) + { return null; + } // Get the entry return map.Children.TryGetValue(name, out var value) ? value as YamlMappingNode : null; @@ -146,7 +160,9 @@ public static string Expand(string text, Dictionary variables) { // Handle null map if (map == null) + { return null; + } // Get the entry return map.Children.TryGetValue(name, out var value) ? value as YamlSequenceNode : null; @@ -163,7 +179,9 @@ public static string Expand(string text, Dictionary variables) { // Handle null map if (map == null) + { return null; + } // Get the parameter return map.Children.TryGetValue(key, out var value) ? Expand(value.ToString(), variables) : null; @@ -181,4 +199,4 @@ public static string Expand(string text, Dictionary variables) // Get the parameter return sequence?.Children.Count > index ? Expand(sequence.Children[index].ToString(), variables) : null; } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/Commands/CommandEntry.cs b/src/DemaConsulting.SpdxTool/Commands/CommandEntry.cs index cc1e8cd..434c6a0 100644 --- a/src/DemaConsulting.SpdxTool/Commands/CommandEntry.cs +++ b/src/DemaConsulting.SpdxTool/Commands/CommandEntry.cs @@ -28,4 +28,4 @@ namespace DemaConsulting.SpdxTool.Commands; /// Command summary /// Command detailed description /// Command instance -public sealed record CommandEntry(string Name, string CommandLine, string Summary, string[] Details, Command Instance); \ No newline at end of file +public sealed record CommandEntry(string Name, string CommandLine, string Summary, string[] Details, Command Instance); diff --git a/src/DemaConsulting.SpdxTool/Commands/CommandErrorException.cs b/src/DemaConsulting.SpdxTool/Commands/CommandErrorException.cs index d224705..44ba994 100644 --- a/src/DemaConsulting.SpdxTool/Commands/CommandErrorException.cs +++ b/src/DemaConsulting.SpdxTool/Commands/CommandErrorException.cs @@ -41,4 +41,4 @@ public CommandErrorException(string message) : base(message) public CommandErrorException(string message, Exception innerException) : base(message, innerException) { } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/Commands/CommandRegistry.cs b/src/DemaConsulting.SpdxTool/Commands/CommandRegistry.cs index 244f39c..e40f308 100644 --- a/src/DemaConsulting.SpdxTool/Commands/CommandRegistry.cs +++ b/src/DemaConsulting.SpdxTool/Commands/CommandRegistry.cs @@ -52,4 +52,4 @@ public static class CommandsRegistry /// Gets the commands /// public static IReadOnlyDictionary Commands => InternalCommands; -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/Commands/CommandUsageException.cs b/src/DemaConsulting.SpdxTool/Commands/CommandUsageException.cs index b04eb79..7a3d12a 100644 --- a/src/DemaConsulting.SpdxTool/Commands/CommandUsageException.cs +++ b/src/DemaConsulting.SpdxTool/Commands/CommandUsageException.cs @@ -24,4 +24,4 @@ namespace DemaConsulting.SpdxTool.Commands; /// Exception thrown when a command is used incorrectly /// /// Error message -public class CommandUsageException(string message) : Exception(message); \ No newline at end of file +public class CommandUsageException(string message) : Exception(message); diff --git a/src/DemaConsulting.SpdxTool/Commands/CopyPackage.cs b/src/DemaConsulting.SpdxTool/Commands/CopyPackage.cs index 4f9272a..ee39ca2 100644 --- a/src/DemaConsulting.SpdxTool/Commands/CopyPackage.cs +++ b/src/DemaConsulting.SpdxTool/Commands/CopyPackage.cs @@ -91,7 +91,9 @@ public override void Run(Context context, string[] args) { // Report an error if the number of arguments is less than 3 if (args.Length < 3) + { throw new CommandUsageException("'copy-package' command missing arguments"); + } // Get fixed options var fromFile = args[0]; @@ -143,12 +145,16 @@ public override void Run(Context context, YamlMappingNode step, Dictionary @@ -281,11 +293,15 @@ public static void CopyChildren(SpdxDocument fromDoc, SpdxDocument toDoc, string { var childId = GetChild(relationship, parentId); if (childId == null) + { continue; + } // Skip if the child is not a package if (!Array.Exists(fromDoc.Packages, p => p.Id == childId)) + { continue; + } // Copy/enhance the child-package Copy(fromDoc, toDoc, childId, files); @@ -295,7 +311,9 @@ public static void CopyChildren(SpdxDocument fromDoc, SpdxDocument toDoc, string // Report copied, and process children if not already processed if (copied.Add(childId)) + { CopyChildren(fromDoc, toDoc, childId, copied, files); + } } } @@ -314,4 +332,4 @@ public static void CopyChildren(SpdxDocument fromDoc, SpdxDocument toDoc, string _ => null }; } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/Commands/Diagram.cs b/src/DemaConsulting.SpdxTool/Commands/Diagram.cs index 1d66d1b..0be5740 100644 --- a/src/DemaConsulting.SpdxTool/Commands/Diagram.cs +++ b/src/DemaConsulting.SpdxTool/Commands/Diagram.cs @@ -71,16 +71,20 @@ public override void Run(Context context, 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)) + { tools = option switch { "tools" => true, _ => throw new CommandUsageException($"'diagram' command invalid option {option}") }; + } // Generate the diagram GenerateDiagram(args[0], args[1], tools); @@ -103,7 +107,9 @@ public override void Run(Context context, YamlMappingNode step, Dictionary @@ -218,27 +232,37 @@ public static bool IsPackageMatch(SpdxPackage package, IReadOnlyDictionary @@ -91,4 +93,4 @@ public override void Run(Context context, YamlMappingNode step, Dictionary !string.IsNullOrEmpty(val)); if (value != null) + { return value; + } // Match not found in program output throw new CommandErrorException($"Pattern '{pattern}' not found in program output"); } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/Commands/RenameId.cs b/src/DemaConsulting.SpdxTool/Commands/RenameId.cs index 0425514..492b40c 100644 --- a/src/DemaConsulting.SpdxTool/Commands/RenameId.cs +++ b/src/DemaConsulting.SpdxTool/Commands/RenameId.cs @@ -74,7 +74,9 @@ public override void Run(Context context, string[] args) { // Report an error if the number of arguments is not 3 if (args.Length != 3) + { throw new CommandUsageException("'rename-id' command missing arguments"); + } // Rename the ID Rename(args[0], args[1], args[2]); @@ -132,25 +134,35 @@ public static void Rename(SpdxDocument doc, string oldId, string newId) { // Skip if no rename if (oldId == newId) + { return; + } // Verify the old ID is valid if (oldId.Length == 0 || oldId == "SPDXRef-DOCUMENT") + { throw new CommandUsageException("Invalid old ID"); + } // Verify the new ID is valid if (newId.Length == 0 || newId == "SPDXRef-DOCUMENT") + { throw new CommandUsageException("Invalid new ID"); + } // Verify the IDs are different if (oldId == newId) + { throw new CommandUsageException("Old and new IDs are the same"); + } // Verify ID is not in use if (Array.Exists(doc.Packages, p => p.Id == newId) || Array.Exists(doc.Files, f => f.Id == newId) || Array.Exists(doc.Snippets, s => s.Id == newId)) + { throw new CommandErrorException($"Element ID {newId} is already used"); + } // Update packages foreach (var package in doc.Packages) @@ -160,16 +172,22 @@ public static void Rename(SpdxDocument doc, string oldId, string newId) // Rename files in package for (var i = 0; i < package.HasFiles.Length; ++i) + { package.HasFiles[i] = UpdateId(package.HasFiles[i], oldId, newId); + } } // Update files foreach (var file in doc.Files) + { file.Id = UpdateId(file.Id, oldId, newId); + } // Update snippets foreach (var snippet in doc.Snippets) + { snippet.Id = UpdateId(snippet.Id, oldId, newId); + } // Update relationships foreach (var relationship in doc.Relationships) @@ -183,7 +201,9 @@ public static void Rename(SpdxDocument doc, string oldId, string newId) // Update describes for (var i = 0; i < doc.Describes.Length; ++i) + { doc.Describes[i] = UpdateId(doc.Describes[i], oldId, newId); + } } /// @@ -197,4 +217,4 @@ private static string UpdateId(string id, string oldId, string newId) { return id == oldId ? newId : id; } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/Commands/RunWorkflow.cs b/src/DemaConsulting.SpdxTool/Commands/RunWorkflow.cs index d2a0c68..a83398f 100644 --- a/src/DemaConsulting.SpdxTool/Commands/RunWorkflow.cs +++ b/src/DemaConsulting.SpdxTool/Commands/RunWorkflow.cs @@ -80,7 +80,9 @@ public override void Run(Context context, string[] args) { // Report an error if the number of arguments is less than 1 if (args.Length < 1) + { throw new CommandUsageException("'run-workflow' command missing arguments"); + } var name = args[0]; @@ -99,7 +101,9 @@ public override void Run(Context context, string[] args) // Verify the parameter is in the form key=value var sep = arg.IndexOf('='); if (sep < 0) + { throw new CommandUsageException($"Invalid argument: {arg}"); + } // Add the parameter var key = arg[..sep]; @@ -114,12 +118,16 @@ public override void Run(Context context, string[] args) // Skip if not verbose if (!verbose) + { return; + } // Print the outputs context.WriteLine("Outputs:"); foreach (var (key, value) in outputs) + { context.WriteLine($" {key} = {value}"); + } } /// @@ -138,6 +146,7 @@ public override void Run(Context context, YamlMappingNode step, Dictionary(); if (GetMapMap(inputs, "parameters") is { } parametersMap) + { // Process all the parameters foreach (var (keyNode, valueNode) in parametersMap.Children) { @@ -145,22 +154,27 @@ public override void Run(Context context, YamlMappingNode step, Dictionary @@ -179,16 +193,22 @@ public static Dictionary Run(Context context, YamlMappingNode st { // Fail if no source if (file != null && url != null) + { throw new YamlException(step.Start, step.End, "'run-workflow' command cannot specify both 'file' and 'url' inputs"); + } // Run the file if specified if (file != null) + { return RunFile(context, file, integrity, parameters); + } // Run the URL if specified if (url != null) + { return RunUrl(context, url, integrity, parameters); + } // No source provided throw new YamlException(step.Start, step.End, @@ -210,8 +230,10 @@ public static Dictionary RunFile(Context context, string workflo { // Verify the file exists if (!File.Exists(workflowFile)) + { throw new CommandUsageException( $"File not found: {workflowFile}"); + } // Get the file bytes var bytes = File.ReadAllBytes(workflowFile); @@ -249,7 +271,9 @@ public static Dictionary RunUrl(Context context, string url, str // Get the result (blocks until result available) var responseMessage = getTask.Result; if (responseMessage.StatusCode != HttpStatusCode.OK) + { throw new CommandErrorException($"Error {responseMessage.StatusCode} fetching {url}"); + } // Get the content bytes (blocks until data available) var bytesTask = responseMessage.Content.ReadAsByteArrayAsync(); @@ -277,7 +301,9 @@ public static Dictionary RunBytes(Context context, string source var hashBytes = SHA256.HashData(bytes); var hash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); if (hash != integrity) + { throw new CommandErrorException($"Integrity check of {source} failed"); + } } try @@ -293,6 +319,7 @@ public static Dictionary RunBytes(Context context, string source // 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) { @@ -300,13 +327,16 @@ public static Dictionary RunBytes(Context context, string source var value = Expand(valueNode.ToString(), variables); variables[key] = Expand(value, parameters); } + } // Apply the provided parameters to our variables foreach (var (key, value) in parameters) { if (!variables.ContainsKey(key)) + { throw new CommandErrorException( $"Workflow {source} parameter {key} not defined"); + } variables[key] = Expand(value, variables); } @@ -331,12 +361,16 @@ public static Dictionary RunBytes(Context context, string source // Check for a displayName var displayName = GetMapString(step, "displayName", variables); if (displayName != null) + { context.WriteLine(displayName); + } // Execute the step if (!CommandsRegistry.Commands.TryGetValue(command, out var entry)) + { throw new CommandUsageException( $"Unknown command: '{command}'"); + } // Run the command entry.Instance.Run(context, step, variables); @@ -356,4 +390,4 @@ public static Dictionary RunBytes(Context context, string source $"Workflow {source} invalid at {ex.Start} - {ex.Message}", ex); } } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/Commands/SetVariable.cs b/src/DemaConsulting.SpdxTool/Commands/SetVariable.cs index 588e7df..a6ee36f 100644 --- a/src/DemaConsulting.SpdxTool/Commands/SetVariable.cs +++ b/src/DemaConsulting.SpdxTool/Commands/SetVariable.cs @@ -85,4 +85,4 @@ public override void Run(Context context, YamlMappingNode step, Dictionary 2 ? args[2] : "SPDX Document"; if (string.IsNullOrWhiteSpace(title)) + { throw new CommandUsageException("'to-markdown' command invalid 'title' argument"); + } // Get the depth var depthText = args.Length > 3 ? args[3] : "2"; if (!int.TryParse(depthText, out var depth) || depth < 1) + { throw new CommandUsageException("'to-markdown' command invalid 'depth' argument"); + } // Generate the markdown GenerateSummaryMarkdown(spdxFile, markdownFile, title, depth); @@ -114,12 +120,16 @@ public override void Run(Context context, YamlMappingNode step, Dictionary p.Name).ToArray(); var packages = doc.Packages.Except(rootPackages).OrderBy(p => p.Name).ToArray(); var tools = packages.Where(p => toolIds.Contains(p.Id)).ToArray(); - packages = [..packages.Except(tools)]; + packages = [.. packages.Except(tools)]; // Print the root packages if (rootPackages.Length > 0) @@ -184,8 +197,11 @@ public static void GenerateSummaryMarkdown(string spdxFile, string markdownFile, markdown.AppendLine("| Name | Version | License |"); markdown.AppendLine("| :-------- | :--- | :--- |"); foreach (var package in rootPackages) + { markdown.AppendLine( $"| {package.Name} | {package.Version ?? string.Empty} | {License(package)} |"); + } + markdown.AppendLine(); markdown.AppendLine(); } @@ -198,8 +214,11 @@ public static void GenerateSummaryMarkdown(string spdxFile, string markdownFile, markdown.AppendLine("| Name | Version | License |"); markdown.AppendLine("| :-------- | :--- | :--- |"); foreach (var package in packages) + { markdown.AppendLine( $"| {package.Name} | {package.Version ?? string.Empty} | {License(package)} |"); + } + markdown.AppendLine(); markdown.AppendLine(); } @@ -212,8 +231,11 @@ public static void GenerateSummaryMarkdown(string spdxFile, string markdownFile, markdown.AppendLine("| Name | Version | License |"); markdown.AppendLine("| :-------- | :--- | :--- |"); foreach (var package in tools) + { markdown.AppendLine( $"| {package.Name} | {package.Version ?? string.Empty} | {License(package)} |"); + } + markdown.AppendLine(); markdown.AppendLine(); } @@ -231,13 +253,17 @@ private static string License(SpdxPackage package) { // Use the concluded license if available if (!string.IsNullOrEmpty(package.ConcludedLicense) && package.ConcludedLicense != "NOASSERTION") + { return package.ConcludedLicense; + } // Use the declared license if available if (!string.IsNullOrEmpty(package.DeclaredLicense) && package.DeclaredLicense != "NOASSERTION") + { return package.DeclaredLicense; + } // Could not find license return "NOASSERTION"; } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/Commands/UpdatePackage.cs b/src/DemaConsulting.SpdxTool/Commands/UpdatePackage.cs index 984a346..0dcbeef 100644 --- a/src/DemaConsulting.SpdxTool/Commands/UpdatePackage.cs +++ b/src/DemaConsulting.SpdxTool/Commands/UpdatePackage.cs @@ -186,56 +186,78 @@ public static void ParseUpdates( // Get the 'name' input var name = GetMapString(map, "name", variables); if (name != null) + { updates["name"] = name; + } // Get the 'download' input var download = GetMapString(map, "download", variables); if (download != null) + { updates["download"] = download; + } // Get the 'version' input var version = GetMapString(map, "version", variables); if (version != null) + { updates["version"] = version; + } // Get the 'filename' input var filename = GetMapString(map, "filename", variables); if (filename != null) + { updates["filename"] = filename; + } // Get the 'supplier' input var supplier = GetMapString(map, "supplier", variables); if (supplier != null) + { updates["supplier"] = supplier; + } // Get the 'originator' input var originator = GetMapString(map, "originator", variables); if (originator != null) + { updates["originator"] = originator; + } // Get the 'homepage' input var homepage = GetMapString(map, "homepage", variables); if (homepage != null) + { updates["homepage"] = homepage; + } // Get the 'copyright' input var copyright = GetMapString(map, "copyright", variables); if (copyright != null) + { updates["copyright"] = copyright; + } // Get the 'summary' input var summary = GetMapString(map, "summary", variables); if (summary != null) + { updates["summary"] = summary; + } // Get the 'description' input var description = GetMapString(map, "description", variables); if (description != null) + { updates["description"] = description; + } // Get the 'license' input var license = GetMapString(map, "license", variables); if (license != null) + { updates["license"] = license; + } } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/Commands/Validate.cs b/src/DemaConsulting.SpdxTool/Commands/Validate.cs index bc83f10..e87bbfb 100644 --- a/src/DemaConsulting.SpdxTool/Commands/Validate.cs +++ b/src/DemaConsulting.SpdxTool/Commands/Validate.cs @@ -72,7 +72,9 @@ public override void Run(Context context, string[] args) { // Report an error if for missing arguments if (args.Length == 0) + { throw new CommandUsageException("'validate' command missing arguments"); + } // Process the arguments var spdxFile = args[0]; @@ -118,14 +120,19 @@ public static void DoValidate(Context context, string spdxFile, bool ntia) // Skip if no issues detected if (issues.Count == 0) + { return; + } // Report issues foreach (var issue in issues) + { context.WriteWarning(issue); + } + context.WriteLine(""); // Throw error throw new CommandErrorException($"Found {issues.Count} Issues in {spdxFile}"); } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/Context.cs b/src/DemaConsulting.SpdxTool/Context.cs index 5ab4dea..f3008f0 100644 --- a/src/DemaConsulting.SpdxTool/Context.cs +++ b/src/DemaConsulting.SpdxTool/Context.cs @@ -102,7 +102,9 @@ public void WriteLine(string text) { // Write to the console unless silent if (!Silent) + { Console.WriteLine(text); + } // Write to the log if specified _log?.WriteLine(text); @@ -244,9 +246,11 @@ private static string ParseArgument(IEnumerator arg, string missingMessa { // Move to the argument if (!arg.MoveNext()) + { throw new InvalidOperationException(missingMessage); + } // Return the argument return arg.Current; } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/DemaConsulting.SpdxTool.csproj b/src/DemaConsulting.SpdxTool/DemaConsulting.SpdxTool.csproj index dad49a9..fcd1e66 100644 --- a/src/DemaConsulting.SpdxTool/DemaConsulting.SpdxTool.csproj +++ b/src/DemaConsulting.SpdxTool/DemaConsulting.SpdxTool.csproj @@ -38,11 +38,18 @@ true true latest + + + true + $(PackageId) + $(Version) + Organization: $(Company) + all @@ -55,6 +62,10 @@ + + + + diff --git a/src/DemaConsulting.SpdxTool/Program.cs b/src/DemaConsulting.SpdxTool/Program.cs index ac8adbf..fe3ebfd 100644 --- a/src/DemaConsulting.SpdxTool/Program.cs +++ b/src/DemaConsulting.SpdxTool/Program.cs @@ -111,7 +111,7 @@ public static void Run(Context context) if (CommandsRegistry.Commands.TryGetValue(command, out var entry)) { // Run the command - entry.Instance.Run(context, [..context.Arguments.Skip(1)]); + entry.Instance.Run(context, [.. context.Arguments.Skip(1)]); } else { @@ -159,6 +159,8 @@ public static void PrintUsage(Context context) Commands: """); foreach (var command in CommandsRegistry.Commands.Values) + { context.WriteLine($" {command.CommandLine,-40} {command.Summary}"); + } } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/SelfValidation/Validate.cs b/src/DemaConsulting.SpdxTool/SelfValidation/Validate.cs index 7ddd118..e8401be 100644 --- a/src/DemaConsulting.SpdxTool/SelfValidation/Validate.cs +++ b/src/DemaConsulting.SpdxTool/SelfValidation/Validate.cs @@ -59,21 +59,29 @@ public static void Run(Context context) // Run validation tests ValidateAddPackage.Run(context, results); ValidateAddRelationship.Run(context, results); + ValidateBasic.Run(context, results); ValidateCopyPackage.Run(context, results); + ValidateDiagram.Run(context, results); ValidateFindPackage.Run(context, results); ValidateGetVersion.Run(context, results); + ValidateHash.Run(context, results); ValidateNtia.Run(context, results); ValidateQuery.Run(context, results); ValidateRenameId.Run(context, results); + ValidateToMarkdown.Run(context, results); ValidateUpdatePackage.Run(context, results); // If all validations succeeded (no errors) then report validation passed if (context.Errors == 0) + { context.WriteLine("\nValidation Passed"); + } // Save test results if (!string.IsNullOrEmpty(context.ValidationFile)) + { File.WriteAllText(context.ValidationFile, TrxSerializer.Serialize(results)); + } } /// @@ -112,4 +120,4 @@ internal static int RunSpdxTool(string workingFolder, string[] args) Directory.SetCurrentDirectory(cwd); } } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateAddPackage.cs b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateAddPackage.cs index f1cb9b7..dd229be 100644 --- a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateAddPackage.cs +++ b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateAddPackage.cs @@ -123,7 +123,9 @@ private static bool DoValidate() // Fail if SpdxTool reported an error if (exitCode != 0) + { return false; + } // Read the SPDX document var doc = Spdx2JsonDeserializer.Deserialize(File.ReadAllText("validate.tmp/test.spdx.json")); @@ -133,17 +135,17 @@ private static bool DoValidate() { Packages: [ - { Id: "SPDXRef-Package-1" }, - { Id: "SPDXRef-Package-2" } + { Id: "SPDXRef-Package-1" }, + { Id: "SPDXRef-Package-2" } ], Relationships: [ _, - { - Id: "SPDXRef-Package-2", - RelationshipType: SpdxRelationshipType.BuildToolOf, - RelatedSpdxElement: "SPDXRef-Package-1" - } + { + Id: "SPDXRef-Package-2", + RelationshipType: SpdxRelationshipType.BuildToolOf, + RelatedSpdxElement: "SPDXRef-Package-1" + } ] }; } @@ -153,4 +155,4 @@ private static bool DoValidate() Directory.Delete("validate.tmp", true); } } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateAddRelationship.cs b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateAddRelationship.cs index f7448f3..5ae4b1a 100644 --- a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateAddRelationship.cs +++ b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateAddRelationship.cs @@ -122,7 +122,9 @@ private static bool DoValidate() // Fail if SpdxTool reported an error if (exitCode != 0) + { return false; + } // Read the SPDX document var doc = Spdx2JsonDeserializer.Deserialize(File.ReadAllText("validate.tmp/test.spdx.json")); @@ -147,4 +149,4 @@ private static bool DoValidate() Directory.Delete("validate.tmp", true); } } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateBasic.cs b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateBasic.cs new file mode 100644 index 0000000..c28e43d --- /dev/null +++ b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateBasic.cs @@ -0,0 +1,181 @@ +// 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 DemaConsulting.TestResults; + +namespace DemaConsulting.SpdxTool.SelfValidation; + +/// +/// Self-validation of basic SPDX validation +/// +internal static class ValidateBasic +{ + /// + /// Run validation test + /// + /// Program context + /// Test results + public static void Run(Context context, TestResults.TestResults results) + { + // Perform the validation + var passed = DoValidate(); + + // Report validation result to console + context.WriteLine($"- SpdxTool_Validate: {(passed ? "Passed" : "Failed")}"); + + // Add validation result to test results collection + results.Results.Add( + new TestResult + { + Name = "SpdxTool_Validate", + ClassName = "DemaConsulting.SpdxTool.SelfValidation.ValidateBasic", + ComputerName = Environment.MachineName, + StartTime = DateTime.Now, + Outcome = passed ? TestOutcome.Passed : TestOutcome.Failed + }); + } + + /// + /// Do the validation + /// + /// True on success + private static bool DoValidate() + { + try + { + // Create the temporary validation folder + Directory.CreateDirectory("validate.tmp"); + + // Run validation tests for both valid and invalid documents + return DoValidateValid() && DoValidateInvalid(); + } + finally + { + // Delete the temporary validation folder + Directory.Delete("validate.tmp", true); + } + } + + /// + /// Validate that basic validation passes for valid document + /// + /// True on success + private static bool DoValidateValid() + { + // Write test SPDX file that is valid + File.WriteAllText("validate.tmp/test-valid.spdx.json", + """ + { + "files": [], + "packages": [ { + "SPDXID": "SPDXRef-Package", + "name": "Test Package", + "versionInfo": "1.0.0", + "downloadLocation": "https://github.com/demaconsulting/SpdxTool", + "filesAnalyzed": false, + "licenseConcluded": "MIT" + } + ], + "relationships": [ { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-Package", + "relationshipType": "DESCRIBES" + } + ], + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "Test Document", + "documentNamespace": "https://sbom.spdx.org", + "creationInfo": { + "created": "2021-10-01T00:00:00Z", + "creators": [ "Person: Malcolm Nixon" ] + } + } + """); + + // Run validation without NTIA flag on valid document + var exitCode = Validate.RunSpdxTool( + "validate.tmp", + [ + "--silent", + "validate", + "test-valid.spdx.json" + ]); + + // Validation should pass for valid document + return exitCode == 0; + } + + /// + /// Validate that basic validation detects invalid document + /// + /// True on success + private static bool DoValidateInvalid() + { + // Write test SPDX file that is invalid (missing required SPDXID) + File.WriteAllText("validate.tmp/test-invalid.spdx.json", + """ + { + "files": [], + "packages": [ { + "name": "Test Package", + "versionInfo": "1.0.0", + "downloadLocation": "https://github.com/demaconsulting/SpdxTool", + "filesAnalyzed": false, + "licenseConcluded": "MIT" + } + ], + "relationships": [], + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "Test Document", + "documentNamespace": "https://sbom.spdx.org", + "creationInfo": { + "created": "2021-10-01T00:00:00Z", + "creators": [ "Person: Malcolm Nixon" ] + } + } + """); + + // Run validation on invalid document + var exitCode = Validate.RunSpdxTool( + "validate.tmp", + [ + "--silent", + "--log", "output.log", + "validate", + "test-invalid.spdx.json" + ]); + + // Validation should fail for invalid document + if (exitCode == 0) + { + return false; + } + + // Read the log file to verify error was reported + var log = File.ReadAllText("validate.tmp/output.log"); + + // Verify log contains error about missing SPDXID + return log.Contains("Issues in test-invalid.spdx.json") || log.Contains("Package") || log.Contains("SPDXID"); + } +} diff --git a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateCopyPackage.cs b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateCopyPackage.cs index 9ee3e5a..61a6582 100644 --- a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateCopyPackage.cs +++ b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateCopyPackage.cs @@ -153,7 +153,9 @@ private static bool DoValidate() // Fail if SpdxTool reported an error if (exitCode != 0) + { return false; + } // Read the SPDX document var doc = Spdx2JsonDeserializer.Deserialize(File.ReadAllText("validate.tmp/to.spdx.json")); @@ -163,17 +165,17 @@ private static bool DoValidate() { Packages: [ - { Id: "SPDXRef-Package-1" }, - { Id: "SPDXRef-Package-2" } + { Id: "SPDXRef-Package-1" }, + { Id: "SPDXRef-Package-2" } ], Relationships: [ _, - { - Id: "SPDXRef-Package-2", - RelationshipType: SpdxRelationshipType.ContainedBy, - RelatedSpdxElement: "SPDXRef-Package-1" - } + { + Id: "SPDXRef-Package-2", + RelationshipType: SpdxRelationshipType.ContainedBy, + RelatedSpdxElement: "SPDXRef-Package-1" + } ] }; } @@ -183,4 +185,4 @@ private static bool DoValidate() Directory.Delete("validate.tmp", true); } } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateDiagram.cs b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateDiagram.cs new file mode 100644 index 0000000..664f5a9 --- /dev/null +++ b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateDiagram.cs @@ -0,0 +1,146 @@ +// 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 DemaConsulting.TestResults; + +namespace DemaConsulting.SpdxTool.SelfValidation; + +/// +/// Self-validation of Diagram command +/// +internal static class ValidateDiagram +{ + /// + /// Run validation test + /// + /// Program context + /// Test results + public static void Run(Context context, TestResults.TestResults results) + { + // Perform the validation + var passed = DoValidate(); + + // Report validation result to console + context.WriteLine($"- SpdxTool_Diagram: {(passed ? "Passed" : "Failed")}"); + + // Add validation result to test results collection + results.Results.Add( + new TestResult + { + Name = "SpdxTool_Diagram", + ClassName = "DemaConsulting.SpdxTool.SelfValidation.ValidateDiagram", + ComputerName = Environment.MachineName, + StartTime = DateTime.Now, + Outcome = passed ? TestOutcome.Passed : TestOutcome.Failed + }); + } + + /// + /// Do the validation + /// + /// True on success + private static bool DoValidate() + { + try + { + // Create the temporary validation folder + Directory.CreateDirectory("validate.tmp"); + + // Write test SPDX file with packages and relationships + File.WriteAllText("validate.tmp/test-diagram.spdx.json", + """ + { + "files": [], + "packages": [ { + "SPDXID": "SPDXRef-Application", + "name": "Test Application", + "versionInfo": "1.0.0", + "downloadLocation": "https://github.com/demaconsulting/SpdxTool", + "licenseConcluded": "MIT" + }, + { + "SPDXID": "SPDXRef-Library", + "name": "Test Library", + "versionInfo": "2.0.0", + "downloadLocation": "https://github.com/demaconsulting/SpdxTool", + "licenseConcluded": "Apache-2.0" + } + ], + "relationships": [ { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-Application", + "relationshipType": "DESCRIBES" + }, + { + "spdxElementId": "SPDXRef-Application", + "relatedSpdxElement": "SPDXRef-Library", + "relationshipType": "DEPENDS_ON" + } + ], + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "Test Document", + "documentNamespace": "https://sbom.spdx.org", + "creationInfo": { + "created": "2021-10-01T00:00:00Z", + "creators": [ "Person: Malcolm Nixon" ] + } + } + """); + + // Run the diagram command to generate mermaid diagram + var exitCode = Validate.RunSpdxTool( + "validate.tmp", + [ + "--silent", + "diagram", + "test-diagram.spdx.json", + "test-diagram.mermaid.txt" + ]); + + // Fail if SpdxTool reported an error + if (exitCode != 0) + { + return false; + } + + // Verify the mermaid file was created + if (!File.Exists("validate.tmp/test-diagram.mermaid.txt")) + { + return false; + } + + // Read the generated mermaid content + var mermaid = File.ReadAllText("validate.tmp/test-diagram.mermaid.txt"); + + // Verify mermaid contains expected diagram syntax and content + return mermaid.Contains("erDiagram") && + mermaid.Contains("Test Application / 1.0.0") && + mermaid.Contains("Test Library / 2.0.0") && + mermaid.Contains("DEPENDS_ON"); + } + finally + { + // Delete the temporary validation folder + Directory.Delete("validate.tmp", true); + } + } +} diff --git a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateFindPackage.cs b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateFindPackage.cs index 7e93185..4a6ea92 100644 --- a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateFindPackage.cs +++ b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateFindPackage.cs @@ -128,7 +128,9 @@ private static bool DoValidate() // Fail if SpdxTool reported an error if (exitCode != 0) + { return false; + } // Read the log file var log = File.ReadAllText("validate.tmp/output.log"); @@ -142,4 +144,4 @@ private static bool DoValidate() Directory.Delete("validate.tmp", true); } } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateGetVersion.cs b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateGetVersion.cs index f353beb..4440673 100644 --- a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateGetVersion.cs +++ b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateGetVersion.cs @@ -128,7 +128,9 @@ private static bool DoValidate() // Fail if SpdxTool reported an error if (exitCode != 0) + { return false; + } // Read the log file var log = File.ReadAllText("validate.tmp/output.log"); @@ -142,4 +144,4 @@ private static bool DoValidate() Directory.Delete("validate.tmp", true); } } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateHash.cs b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateHash.cs new file mode 100644 index 0000000..612a642 --- /dev/null +++ b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateHash.cs @@ -0,0 +1,155 @@ +// 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 DemaConsulting.TestResults; + +namespace DemaConsulting.SpdxTool.SelfValidation; + +/// +/// Self-validation of Hash command +/// +internal static class ValidateHash +{ + /// + /// Run validation test + /// + /// Program context + /// Test results + public static void Run(Context context, TestResults.TestResults results) + { + // Perform the validation + var passed = DoValidate(); + + // Report validation result to console + context.WriteLine($"- SpdxTool_Hash: {(passed ? "Passed" : "Failed")}"); + + // Add validation result to test results collection + results.Results.Add( + new TestResult + { + Name = "SpdxTool_Hash", + ClassName = "DemaConsulting.SpdxTool.SelfValidation.ValidateHash", + ComputerName = Environment.MachineName, + StartTime = DateTime.Now, + Outcome = passed ? TestOutcome.Passed : TestOutcome.Failed + }); + } + + /// + /// Do the validation + /// + /// True on success + private static bool DoValidate() + { + try + { + // Create the temporary validation folder + Directory.CreateDirectory("validate.tmp"); + + // Run both generation and verification validation tests + return DoValidateGenerate() && DoValidateVerify(); + } + finally + { + // Delete the temporary validation folder + Directory.Delete("validate.tmp", true); + } + } + + /// + /// Validate hash generation + /// + /// True on success + private static bool DoValidateGenerate() + { + // Write test file with known content + File.WriteAllText("validate.tmp/test-file.txt", "The quick brown fox jumps over the lazy dog"); + + // Run hash generate command to create SHA256 hash + var exitCode = Validate.RunSpdxTool( + "validate.tmp", + [ + "--silent", + "hash", + "generate", + "sha256", + "test-file.txt" + ]); + + // Fail if SpdxTool reported an error + if (exitCode != 0) + { + return false; + } + + // Verify hash file was created with expected naming + if (!File.Exists("validate.tmp/test-file.txt.sha256")) + { + return false; + } + + // Read the generated hash value + var hash = File.ReadAllText("validate.tmp/test-file.txt.sha256"); + + // Verify hash matches expected SHA256 value for the test content + return hash == "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"; + } + + /// + /// Validate hash verification + /// + /// True on success + private static bool DoValidateVerify() + { + // Run hash verify command with correct hash + var exitCode1 = Validate.RunSpdxTool( + "validate.tmp", + [ + "--silent", + "hash", + "verify", + "sha256", + "test-file.txt" + ]); + + // Verification should succeed with correct hash + if (exitCode1 != 0) + { + return false; + } + + // Corrupt the hash file with invalid hash value + File.WriteAllText("validate.tmp/test-file.txt.sha256", "0000000000000000000000000000000000000000000000000000000000000000"); + + // Run hash verify command with incorrect hash + var exitCode2 = Validate.RunSpdxTool( + "validate.tmp", + [ + "--silent", + "hash", + "verify", + "sha256", + "test-file.txt" + ]); + + // Verification should fail with incorrect hash + return exitCode2 != 0; + } +} diff --git a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateNtia.cs b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateNtia.cs index 2fdde03..78a7e28 100644 --- a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateNtia.cs +++ b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateNtia.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 DEMA Consulting +// 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 @@ -122,7 +122,9 @@ private static bool DoValidateMissingSupplier() // Fail if SpdxTool reported an error if (exitCode1 != 0) + { return false; + } // Run validation with NTIA flag - should fail due to missing supplier // The log file will be written to validate.tmp/output.log since the working directory is changed @@ -138,12 +140,16 @@ private static bool DoValidateMissingSupplier() // Should fail validation if (exitCode2 == 0) + { return false; + } // Read the log file and verify it contains the expected error var log = File.ReadAllText("validate.tmp/output.log"); if (!log.Contains("NTIA: Package 'Test Package' Missing Supplier")) + { return false; + } return true; } diff --git a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateQuery.cs b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateQuery.cs index 98fc920..8279756 100644 --- a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateQuery.cs +++ b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateQuery.cs @@ -98,7 +98,9 @@ private static bool DoValidate() // Fail if SpdxTool reported an error if (exitCode != 0) + { return false; + } // Read the log file var log = File.ReadAllText("validate.tmp/output.log"); @@ -112,4 +114,4 @@ private static bool DoValidate() Directory.Delete("validate.tmp", true); } } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateRenameId.cs b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateRenameId.cs index 802ea5f..ddcbe8f 100644 --- a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateRenameId.cs +++ b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateRenameId.cs @@ -115,7 +115,9 @@ private static bool DoValidate() // Fail if SpdxTool reported an error if (exitCode != 0) + { return false; + } // Read the SPDX document var doc = Spdx2JsonDeserializer.Deserialize(File.ReadAllText("validate.tmp/test.spdx.json")); @@ -135,4 +137,4 @@ private static bool DoValidate() Directory.Delete("validate.tmp", true); } } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateToMarkdown.cs b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateToMarkdown.cs new file mode 100644 index 0000000..1566f07 --- /dev/null +++ b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateToMarkdown.cs @@ -0,0 +1,150 @@ +// 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 DemaConsulting.TestResults; + +namespace DemaConsulting.SpdxTool.SelfValidation; + +/// +/// Self-validation of ToMarkdown command +/// +internal static class ValidateToMarkdown +{ + /// + /// Run validation test + /// + /// Program context + /// Test results + public static void Run(Context context, TestResults.TestResults results) + { + // Perform the validation + var passed = DoValidate(); + + // Report validation result to console + context.WriteLine($"- SpdxTool_ToMarkdown: {(passed ? "Passed" : "Failed")}"); + + // Add validation result to test results collection + results.Results.Add( + new TestResult + { + Name = "SpdxTool_ToMarkdown", + ClassName = "DemaConsulting.SpdxTool.SelfValidation.ValidateToMarkdown", + ComputerName = Environment.MachineName, + StartTime = DateTime.Now, + Outcome = passed ? TestOutcome.Passed : TestOutcome.Failed + }); + } + + /// + /// Do the validation + /// + /// True on success + private static bool DoValidate() + { + try + { + // Create the temporary validation folder + Directory.CreateDirectory("validate.tmp"); + + // Write test SPDX file with packages and relationships + File.WriteAllText("validate.tmp/test-markdown.spdx.json", + """ + { + "files": [], + "packages": [ { + "SPDXID": "SPDXRef-Application", + "name": "Test Application", + "versionInfo": "1.0.0", + "downloadLocation": "https://github.com/demaconsulting/SpdxTool", + "licenseConcluded": "MIT" + }, + { + "SPDXID": "SPDXRef-Library", + "name": "Test Library", + "versionInfo": "2.0.0", + "downloadLocation": "https://github.com/demaconsulting/SpdxTool", + "licenseConcluded": "Apache-2.0" + } + ], + "relationships": [ { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-Application", + "relationshipType": "DESCRIBES" + }, + { + "spdxElementId": "SPDXRef-Application", + "relatedSpdxElement": "SPDXRef-Library", + "relationshipType": "CONTAINS" + } + ], + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "Test Document", + "documentNamespace": "https://sbom.spdx.org", + "creationInfo": { + "created": "2021-10-01T00:00:00Z", + "creators": [ "Person: Malcolm Nixon" ] + } + } + """); + + // Run the to-markdown command to generate markdown summary + var exitCode = Validate.RunSpdxTool( + "validate.tmp", + [ + "--silent", + "to-markdown", + "test-markdown.spdx.json", + "test-markdown.md", + "Test SBOM Summary" + ]); + + // Fail if SpdxTool reported an error + if (exitCode != 0) + { + return false; + } + + // Verify the markdown file was created + if (!File.Exists("validate.tmp/test-markdown.md")) + { + return false; + } + + // Read the generated markdown content + var markdown = File.ReadAllText("validate.tmp/test-markdown.md"); + + // Verify markdown contains expected structure and package information + return markdown.Contains("Test SBOM Summary") && + markdown.Contains("Root Packages") && + markdown.Contains("Packages") && + markdown.Contains("Test Application") && + markdown.Contains("1.0.0") && + markdown.Contains("Test Library") && + markdown.Contains("2.0.0"); + } + finally + { + // Delete the temporary validation folder + Directory.Delete("validate.tmp", true); + } + } +} diff --git a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateUpdatePackage.cs b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateUpdatePackage.cs index a772333..2f3d953 100644 --- a/src/DemaConsulting.SpdxTool/SelfValidation/ValidateUpdatePackage.cs +++ b/src/DemaConsulting.SpdxTool/SelfValidation/ValidateUpdatePackage.cs @@ -126,7 +126,9 @@ private static bool DoValidate() // Fail if SpdxTool reported an error if (exitCode != 0) + { return false; + } // Read the SPDX document var doc = Spdx2JsonDeserializer.Deserialize(File.ReadAllText("validate.tmp/test.spdx.json")); @@ -160,4 +162,4 @@ private static bool DoValidate() Directory.Delete("validate.tmp", true); } } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/Spdx/RelationshipDirection.cs b/src/DemaConsulting.SpdxTool/Spdx/RelationshipDirection.cs index e681a3e..ef981fe 100644 --- a/src/DemaConsulting.SpdxTool/Spdx/RelationshipDirection.cs +++ b/src/DemaConsulting.SpdxTool/Spdx/RelationshipDirection.cs @@ -82,7 +82,7 @@ public static class RelationshipDirectionExtensions { SpdxRelationshipType.PrerequisiteFor, RelationshipDirection.Child }, { SpdxRelationshipType.HasPrerequisite, RelationshipDirection.Parent }, }; - + /// /// Get the direction of a relationship /// @@ -92,4 +92,4 @@ public static RelationshipDirection GetDirection(this SpdxRelationshipType type) { return DirectionMap.GetValueOrDefault(type, RelationshipDirection.Sibling); } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/Spdx/SpdxHelpers.cs b/src/DemaConsulting.SpdxTool/Spdx/SpdxHelpers.cs index 4f2af13..ed243c8 100644 --- a/src/DemaConsulting.SpdxTool/Spdx/SpdxHelpers.cs +++ b/src/DemaConsulting.SpdxTool/Spdx/SpdxHelpers.cs @@ -38,7 +38,9 @@ public static SpdxDocument LoadJsonDocument(string spdxFile) { // Verify to file exists if (!File.Exists(spdxFile)) + { throw new CommandUsageException($"File not found: {spdxFile}"); + } // Load the SPDX document var fileContent = File.ReadAllText(spdxFile); @@ -57,10 +59,12 @@ public static void SaveJsonDocument(SpdxDocument doc, string spdxFile) // Add this tool if missing if (!doc.CreationInformation.Creators.Contains(toolName)) - doc.CreationInformation.Creators = [..doc.CreationInformation.Creators.Append(toolName)]; + { + doc.CreationInformation.Creators = [.. doc.CreationInformation.Creators.Append(toolName)]; + } // Save the document var serializedContent = Spdx2JsonSerializer.Serialize(doc); File.WriteAllText(spdxFile, serializedContent); } -} \ No newline at end of file +} diff --git a/src/DemaConsulting.SpdxTool/Utility/Wildcard.cs b/src/DemaConsulting.SpdxTool/Utility/Wildcard.cs index fc6c42f..d592de0 100644 --- a/src/DemaConsulting.SpdxTool/Utility/Wildcard.cs +++ b/src/DemaConsulting.SpdxTool/Utility/Wildcard.cs @@ -53,4 +53,4 @@ public static bool IsMatch(string input, string pattern) RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(100)); } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/AddPackageTests.cs b/test/DemaConsulting.SpdxTool.Tests/AddPackageTests.cs index 27c09a9..e0e64eb 100644 --- a/test/DemaConsulting.SpdxTool.Tests/AddPackageTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/AddPackageTests.cs @@ -320,4 +320,4 @@ public void AddPackage_FromQuery() File.Delete("workflow.yaml"); } } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/AddRelationshipTests.cs b/test/DemaConsulting.SpdxTool.Tests/AddRelationshipTests.cs index 257ce2b..8ded125 100644 --- a/test/DemaConsulting.SpdxTool.Tests/AddRelationshipTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/AddRelationshipTests.cs @@ -304,4 +304,4 @@ public void AddRelationship_Replace() File.Delete("workflow2.yaml"); } } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/AssemblyInfo.cs b/test/DemaConsulting.SpdxTool.Tests/AssemblyInfo.cs index 0b05115..4171d6b 100644 --- a/test/DemaConsulting.SpdxTool.Tests/AssemblyInfo.cs +++ b/test/DemaConsulting.SpdxTool.Tests/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 DEMA Consulting +// 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 diff --git a/test/DemaConsulting.SpdxTool.Tests/CommandTests.cs b/test/DemaConsulting.SpdxTool.Tests/CommandTests.cs index 5bd76ca..80678b2 100644 --- a/test/DemaConsulting.SpdxTool.Tests/CommandTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/CommandTests.cs @@ -103,4 +103,4 @@ public void Command_GetMapString_WithExpansion() var variables = new Dictionary { { "name", "world" } }; Assert.AreEqual("Hello, world!", Command.GetMapString(map, "parameter", variables)); } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/CopyPackageTests.cs b/test/DemaConsulting.SpdxTool.Tests/CopyPackageTests.cs index f6eada4..e0c993d 100644 --- a/test/DemaConsulting.SpdxTool.Tests/CopyPackageTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/CopyPackageTests.cs @@ -614,4 +614,4 @@ public void CopyPackage_Files() File.Delete("workflow.yaml"); } } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/DiagramTests.cs b/test/DemaConsulting.SpdxTool.Tests/DiagramTests.cs index be13533..8222004 100644 --- a/test/DemaConsulting.SpdxTool.Tests/DiagramTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/DiagramTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 DEMA Consulting +// 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 diff --git a/test/DemaConsulting.SpdxTool.Tests/FindPackageTests.cs b/test/DemaConsulting.SpdxTool.Tests/FindPackageTests.cs index b739450..eda99dc 100644 --- a/test/DemaConsulting.SpdxTool.Tests/FindPackageTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/FindPackageTests.cs @@ -320,4 +320,4 @@ public void FindPackage_ByDownload() File.Delete("workflow.yaml"); } } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/GetVersionTests.cs b/test/DemaConsulting.SpdxTool.Tests/GetVersionTests.cs index afe0813..e28bc22 100644 --- a/test/DemaConsulting.SpdxTool.Tests/GetVersionTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/GetVersionTests.cs @@ -182,4 +182,4 @@ public void GetVersion_Workflow() File.Delete("workflow.yaml"); } } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/HashTests.cs b/test/DemaConsulting.SpdxTool.Tests/HashTests.cs index 8cc414f..acd36ce 100644 --- a/test/DemaConsulting.SpdxTool.Tests/HashTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/HashTests.cs @@ -187,4 +187,4 @@ public void HashCommand_Verify_Good() File.Delete("test.txt.sha256"); } } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/HelpTests.cs b/test/DemaConsulting.SpdxTool.Tests/HelpTests.cs index 7ead01f..aa594d0 100644 --- a/test/DemaConsulting.SpdxTool.Tests/HelpTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/HelpTests.cs @@ -81,4 +81,4 @@ public void Help_RunWorkflow() Assert.AreEqual(0, exitCode); Assert.Contains("This command runs the steps specified in the workflow file/url.", output); } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/LogTests.cs b/test/DemaConsulting.SpdxTool.Tests/LogTests.cs index ae368b0..182cb9b 100644 --- a/test/DemaConsulting.SpdxTool.Tests/LogTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/LogTests.cs @@ -91,4 +91,4 @@ public void Log_Long() File.Delete("output.log"); } } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/PrintTests.cs b/test/DemaConsulting.SpdxTool.Tests/PrintTests.cs index 23f3fbb..9ae6ba1 100644 --- a/test/DemaConsulting.SpdxTool.Tests/PrintTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/PrintTests.cs @@ -89,4 +89,4 @@ public void Print_Workflow() File.Delete("workflow.yaml"); } } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/QueryTests.cs b/test/DemaConsulting.SpdxTool.Tests/QueryTests.cs index 839a7a4..db4f001 100644 --- a/test/DemaConsulting.SpdxTool.Tests/QueryTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/QueryTests.cs @@ -164,4 +164,4 @@ public void Query_DotNet_Workflow() File.Delete("workflow.yaml"); } } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/RenameIdTests.cs b/test/DemaConsulting.SpdxTool.Tests/RenameIdTests.cs index cfb4e8c..08bb59b 100644 --- a/test/DemaConsulting.SpdxTool.Tests/RenameIdTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/RenameIdTests.cs @@ -136,4 +136,4 @@ public void RenameId() File.Delete("test.spdx.json"); } } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/RunWorkflowTests.cs b/test/DemaConsulting.SpdxTool.Tests/RunWorkflowTests.cs index ae6071d..371d05c 100644 --- a/test/DemaConsulting.SpdxTool.Tests/RunWorkflowTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/RunWorkflowTests.cs @@ -439,4 +439,4 @@ public void RunWorkflow_Url() File.Delete("workflow.yaml"); } } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/Runner.cs b/test/DemaConsulting.SpdxTool.Tests/Runner.cs index de5e4ef..d28000a 100644 --- a/test/DemaConsulting.SpdxTool.Tests/Runner.cs +++ b/test/DemaConsulting.SpdxTool.Tests/Runner.cs @@ -48,7 +48,9 @@ public static int Run(out string output, string program, params string[] argumen // Add the arguments foreach (var argument in arguments) + { startInfo.ArgumentList.Add(argument); + } // Start the process var process = Process.Start(startInfo) ?? @@ -63,4 +65,4 @@ public static int Run(out string output, string program, params string[] argumen // Return the exit code return process.ExitCode; } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/SelfValidationTests.cs b/test/DemaConsulting.SpdxTool.Tests/SelfValidationTests.cs index 8f96a9e..a79aab1 100644 --- a/test/DemaConsulting.SpdxTool.Tests/SelfValidationTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/SelfValidationTests.cs @@ -96,15 +96,19 @@ public void SelfValidation_TrxResults() Assert.Contains("SpdxTool_AddPackage", results); Assert.Contains("SpdxTool_AddRelationship", results); Assert.Contains("SpdxTool_CopyPackage", results); + Assert.Contains("SpdxTool_Diagram", results); Assert.Contains("SpdxTool_FindPackage", results); Assert.Contains("SpdxTool_GetVersion", results); + Assert.Contains("SpdxTool_Hash", results); Assert.Contains("SpdxTool_Ntia", results); Assert.Contains("SpdxTool_Query", results); Assert.Contains("SpdxTool_RenameId", results); + Assert.Contains("SpdxTool_ToMarkdown", results); Assert.Contains("SpdxTool_UpdatePackage", results); + Assert.Contains("SpdxTool_Validate", results); Assert.Contains(""" - + """, results); } @@ -114,4 +118,4 @@ public void SelfValidation_TrxResults() File.Delete(resultFile); } } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/SetVariableTests.cs b/test/DemaConsulting.SpdxTool.Tests/SetVariableTests.cs index 269e7e2..129f938 100644 --- a/test/DemaConsulting.SpdxTool.Tests/SetVariableTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/SetVariableTests.cs @@ -90,4 +90,4 @@ public void SetVariable() File.Delete("workflow.yaml"); } } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/SilentTests.cs b/test/DemaConsulting.SpdxTool.Tests/SilentTests.cs index 68ec760..ddf1719 100644 --- a/test/DemaConsulting.SpdxTool.Tests/SilentTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/SilentTests.cs @@ -67,4 +67,4 @@ public void Silent_Long() // Assert: Verify the output is empty Assert.AreEqual(0, output.Length); } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/ToMarkdownTests.cs b/test/DemaConsulting.SpdxTool.Tests/ToMarkdownTests.cs index d1a6205..b34757d 100644 --- a/test/DemaConsulting.SpdxTool.Tests/ToMarkdownTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/ToMarkdownTests.cs @@ -182,4 +182,4 @@ public void ToMarkdown() File.Delete("test.md"); } } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/UnknownCommandTests.cs b/test/DemaConsulting.SpdxTool.Tests/UnknownCommandTests.cs index fbb7b46..452909a 100644 --- a/test/DemaConsulting.SpdxTool.Tests/UnknownCommandTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/UnknownCommandTests.cs @@ -43,4 +43,4 @@ public void UnknownCommand() Assert.AreEqual(1, exitCode); Assert.Contains("Error: Unknown command 'unknown-command'", output); } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/UpdatePackageTests.cs b/test/DemaConsulting.SpdxTool.Tests/UpdatePackageTests.cs index c4861b9..a9a2735 100644 --- a/test/DemaConsulting.SpdxTool.Tests/UpdatePackageTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/UpdatePackageTests.cs @@ -149,4 +149,4 @@ public void UpdatePackage_Workflow() File.Delete("workflow.yaml"); } } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/UsageTests.cs b/test/DemaConsulting.SpdxTool.Tests/UsageTests.cs index 548bd7b..e0d5008 100644 --- a/test/DemaConsulting.SpdxTool.Tests/UsageTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/UsageTests.cs @@ -85,4 +85,4 @@ public void Usage_HelpLong() // Assert: Verify the output contains the usage information Assert.Contains("Usage: spdx-tool", output); } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/ValidateTests.cs b/test/DemaConsulting.SpdxTool.Tests/ValidateTests.cs index 298fd4e..f2f7a06 100644 --- a/test/DemaConsulting.SpdxTool.Tests/ValidateTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/ValidateTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 DEMA Consulting +// 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 diff --git a/test/DemaConsulting.SpdxTool.Tests/VersionTests.cs b/test/DemaConsulting.SpdxTool.Tests/VersionTests.cs index 1fd0cc1..cb87745 100644 --- a/test/DemaConsulting.SpdxTool.Tests/VersionTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/VersionTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2024 DEMA Consulting +// 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 @@ -74,4 +74,4 @@ public void Version_Long() // Assert: Verify version response Assert.MatchesRegex(VersionRegex(), output); } -} \ No newline at end of file +} diff --git a/test/DemaConsulting.SpdxTool.Tests/WildcardTests.cs b/test/DemaConsulting.SpdxTool.Tests/WildcardTests.cs index 889caec..67a5267 100644 --- a/test/DemaConsulting.SpdxTool.Tests/WildcardTests.cs +++ b/test/DemaConsulting.SpdxTool.Tests/WildcardTests.cs @@ -75,4 +75,4 @@ public void Wildcard_QuestionMark() Assert.IsFalse(Wildcard.IsMatch("Test", "Test?")); Assert.IsFalse(Wildcard.IsMatch("Test", "?")); } -} \ No newline at end of file +}