diff --git a/.envrc b/.envrc new file mode 100755 index 0000000..2ca4328 --- /dev/null +++ b/.envrc @@ -0,0 +1,30 @@ +# Run any command in this library's bin/ without the bin/ prefix! +# Prefer exe version over binstub +PATH_add exe +PATH_add bin + +# Only add things to this file that should be shared with the team. + +# **dotenv** (See end of file for .env.local integration) +# .env would override anything in this file, if enabled. +# .env is a DOCKER standard, and if we use it, it would be in deployed, or DOCKER, environments. +# Override and customize anything below in your own .env.local +# If you are using dotenv and not direnv, +# copy the following `export` statements to your own .env file. +export MIMIC_NEXT_MAJOR_VERSION=false +export ARUBA_NO_COVERAGE=false + +### General Ruby ### +# Turn off Ruby Warnings about deprecated code +# export RUBYOPT="-W0" + +# Internal Debugging Controls +export DEBUG=false # do not allow byebug statements (override in .env.local) + +# .env would override anything in this file, if `dotenv` is uncommented below. +# .env is a DOCKER standard, and if we use it, it would be in deployed, or DOCKER, environments, +# and that is why we generally want to leave it commented out. +# dotenv + +# .env.local will override anything in this file. +dotenv_if_exists .env.local diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index a1df89e..a575090 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -18,10 +18,14 @@ jobs: fail-fast: false matrix: ruby: - - '3.2.2' - - '3.1.4' - - '3.0.6' - - '2.7.7' + - '3.4.7' + - '3.3.9' + - '3.2.9' +# Not supported by latest bundler. These will be added back to the build matrix later. +# They are not being dropped from support, just from CI testing for now. +# - '3.1.7' +# - '3.0.7' +# - '2.7.8' steps: - uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index 29a7627..6fc92f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,55 @@ -.idea -/Gemfile.lock -/tmp/aruba -/coverage \ No newline at end of file +# Build Artifacts +/pkg/ +/tmp/ +*.gem + +# Bundler +/vendor/bundle/ +/.bundle/ +/gemfiles/*.lock +/gemfiles/.bundle/ +/gemfiles/.bundle/config +/gemfiles/vendor/ +Appraisal.*.gemfile.lock + +# Specs +.rspec_status +/coverage/ +/spec/reports/ +/results/ +.output.txt + +# Documentation +/.yardoc/ +/_yardoc/ +/rdoc/ +/doc/ + +# Ruby Version Managers (RVM, rbenv, etc) +.rvmrc +.ruby-version +.ruby-gemset +.tool-versions + +# Benchmarking +/measurement/ + +# Debugger detritus +.byebug_history + +# direnv - brew install direnv +.env.local + +# OS Detritus +.DS_Store + +# Editors +*~ +/.idea/ + +# vendor +/vendor/ + +# text artifacts +bom.xml +bom.json diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..9007e08 --- /dev/null +++ b/.rspec @@ -0,0 +1,7 @@ +--format documentation +--require spec_helper +--color +--order random +--warnings +--format html +--out results/test_results.html diff --git a/.rubocop.yml b/.rubocop.yml index 0dbd7ac..8d139a1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,6 +4,9 @@ AllCops: inherit_from: .rubocop_todo.yml +Gemspec/DevelopmentDependencies: + EnforcedStyle: gemspec + # The behavior of RuboCop can be controlled via the .rubocop.yml # configuration file. It makes it possible to enable/disable # certain cops (checks) and to alter their behavior if they accept diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index accbce0..5f5b288 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,136 +1,29 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2023-04-05 19:57:43 UTC using RuboCop version 1.49.0. +# on 2025-10-30 06:45:50 UTC using RuboCop version 1.81.6. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec -Gemspec/DeprecatedAttributeAssignment: - Exclude: - - 'cyclonedx-ruby.gemspec' - # Offense count: 4 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. -# Include: **/*.gemspec +# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation. Gemspec/OrderedDependencies: Exclude: - 'cyclonedx-ruby.gemspec' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Severity, Include. -# Include: **/*.gemspec -Gemspec/RequireMFA: - Exclude: - - 'cyclonedx-ruby.gemspec' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Layout/EmptyLineAfterMagicComment: - Exclude: - - 'lib/bom_builder.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: around, only_before -Layout/EmptyLinesAroundAccessModifier: - Exclude: - - 'lib/bom_builder.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Layout/EmptyLinesAroundMethodBody: - Exclude: - - 'lib/bom_component.rb' - -# Offense count: 1 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment. Layout/ExtraSpacing: Exclude: - 'cyclonedx-ruby.gemspec' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: special_inside_parentheses, consistent, align_brackets -Layout/FirstArrayElementIndentation: - Exclude: - - 'lib/bom_component.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Layout/LeadingEmptyLines: - Exclude: - - 'lib/bom_component.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: final_newline, final_blank_line -Layout/TrailingEmptyLines: - Exclude: - - 'Rakefile' - - 'lib/bom_component.rb' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowInHeredoc. -Layout/TrailingWhitespace: - Exclude: - - 'Rakefile' - - 'spec/bom_component_spec.rb' - -# Offense count: 2 -Lint/IneffectiveAccessModifier: - Exclude: - - 'lib/bom_builder.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Lint/ScriptPermission: - Exclude: - - 'Rakefile' - -# Offense count: 1 -Lint/ShadowingOuterLocalVariable: - Exclude: - - 'lib/bom_builder.rb' - -# Offense count: 19 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: strict, consistent -Lint/SymbolConversion: - Exclude: - - 'lib/bom_component.rb' - - 'lib/bom_helpers.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods. -Lint/UnusedMethodArgument: - Exclude: - - 'lib/bom_builder.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods. -Lint/UselessAccessModifier: - Exclude: - - 'lib/bom_builder.rb' - # Offense count: 4 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: - Max: 67 + Max: 68 # Offense count: 4 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. @@ -141,78 +34,38 @@ Metrics/BlockLength: # Offense count: 1 # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 128 + Max: 129 # Offense count: 1 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: Max: 9 -# Offense count: 6 +# Offense count: 7 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: - Max: 68 + Max: 69 # Offense count: 1 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: Max: 12 -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: PreferredName. -Naming/RescuedExceptionsVariableName: - Exclude: - - 'Rakefile' - -# Offense count: 3 +# Offense count: 4 # Configuration parameters: AllowedConstants. Style/Documentation: Exclude: - 'spec/**/*' - 'test/**/*' - - 'Rakefile' - - 'lib/bom_builder.rb' - - 'lib/bom_component.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedVars. -Style/FetchEnvVar: - Exclude: - - 'lib/bom_helpers.rb' + - 'lib/cyclonedx/bom_builder.rb' + - 'lib/cyclonedx/bom_component.rb' + - 'lib/cyclonedx/bom_helpers.rb' + - 'lib/cyclonedx/ruby/deprecation.rb' # Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/FileWrite: +Style/MixinUsage: Exclude: - - 'lib/bom_builder.rb' - -# Offense count: 12 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: always, always_true, never -Style/FrozenStringLiteralComment: - Exclude: - - '.simplecov' - - 'Gemfile' - - 'Rakefile' - - 'features/fixtures/simple/Gemfile' - - 'features/step_definitions/json_bom_matching.rb' - - 'features/step_definitions/xml_bom_matching.rb' - - 'features/support/env.rb' - - 'features/support/simplecov_support.rb' - - 'lib/bom_component.rb' - - 'spec/bom_component_spec.rb' - - 'spec/bom_helpers_spec.rb' - - 'spec/spec_helper.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowedMethods, AllowedPatterns. -Style/MethodCallWithoutArgsParentheses: - Exclude: - - 'lib/bom_helpers.rb' + - 'lib/cyclonedx_deprecated.rb' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -220,13 +73,13 @@ Style/MethodCallWithoutArgsParentheses: # SupportedStyles: literals, strict Style/MutableConstant: Exclude: - - 'lib/bom_builder.rb' + - 'lib/cyclonedx/bom_builder.rb' # Offense count: 2 Style/OpenStructUse: Exclude: - - 'lib/bom_builder.rb' - - 'spec/bom_component_spec.rb' + - 'lib/cyclonedx/bom_builder.rb' + - 'spec/cyclonedx/bom_component_spec.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). @@ -235,19 +88,6 @@ Style/PercentLiteralDelimiters: Exclude: - 'Rakefile' -# Offense count: 19 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: . -# SupportedStyles: same_as_string_literals, single_quotes, double_quotes -Style/QuotedSymbols: - EnforcedStyle: double_quotes - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantBegin: - Exclude: - - 'Rakefile' - # Offense count: 6 # This cop supports safe autocorrection (--autocorrect). Style/RedundantRegexpEscape: @@ -255,7 +95,7 @@ Style/RedundantRegexpEscape: - 'features/step_definitions/json_bom_matching.rb' - 'features/step_definitions/xml_bom_matching.rb' -# Offense count: 20 +# Offense count: 41 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. # SupportedStyles: single_quotes, double_quotes @@ -266,8 +106,6 @@ Style/StringLiterals: - 'cyclonedx-ruby.gemspec' - 'lib/bom_component.rb' - 'lib/bom_helpers.rb' - - 'spec/bom_component_spec.rb' - - 'spec/bom_helpers_spec.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). @@ -276,15 +114,9 @@ Style/StringLiterals: Style/SymbolArray: EnforcedStyle: brackets -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -Style/SymbolLiteral: - Exclude: - - 'lib/bom_component.rb' - -# Offense count: 5 +# Offense count: 7 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. +# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. # URISchemes: http, https Layout/LineLength: Max: 237 diff --git a/.simplecov b/.simplecov index f1709c5..dcd8afc 100644 --- a/.simplecov +++ b/.simplecov @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copied from https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/.simplecov # Licensed under MIT - https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/LICENSE diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e50ee65 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,79 @@ +# Changelog + +[![SemVer 2.0.0][📌semver-img]][📌semver] [![Keep-A-Changelog 1.0.0][📗keep-changelog-img]][📗keep-changelog] + +All notable changes to this project after v1.1.0 will be documented in this file. + +The format is based on [Keep a Changelog][📗keep-changelog], +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), +and [yes][📌major-versions-not-sacred], platform and engine support are part of the [public API][📌semver-breaking]. +Please file a bug if you notice a violation of semantic versioning. + +[📌semver]: https://semver.org/spec/v2.0.0.html +[📌semver-img]: https://img.shields.io/badge/semver-2.0.0-FFDD67.svg?style=flat +[📌semver-breaking]: https://github.com/semver/semver/issues/716#issuecomment-869336139 +[📌major-versions-not-sacred]: https://tom.preston-werner.com/2022/05/23/major-version-numbers-are-not-sacred.html +[📗keep-changelog]: https://keepachangelog.com/en/1.0.0/ +[📗keep-changelog-img]: https://img.shields.io/badge/keep--a--changelog-1.0.0-FFDD67.svg?style=flat + +## [Unreleased] + +### Added + +- `CONTRIBUTING.md` file to help people find their way to contributing +- `CHANGELOG.md` file to document notable changes in keep-a-changelog format +- `Cyclonedx::BomHelpers` module to house helper methods, replacing global methods +- `Cyclonedx::BomBuilder` class, replacing `Bombuilder` (note the capitalization change) +- `Cyclonedx::BomComponent` class, replacing `BomComponent` +- `Cyclonedx::Ruby::Version::VERSION` constant to hold the version number (also available as `Cyclonedx::VERSION`) +- `Cyclonedx::Ruby::Deprecation` module to help manage deprecations +- dev dependency: `stone_checksums` + - For SHA-256 and SHA-512 checksum generation for each release. +- signed gem releases + - See: [RubyGems Security Guide][🔒️rubygems-security-guide] +- CI matrix testing on Ruby 3.3, 3.4 + +[🔒️rubygems-security-guide]: https://guides.rubygems.org/security/#building-gems + +### Changed + +- Updated gemspec metadata for clarity and consistency +- Modernized Rakefile, dotfiles, and test setup +- `LICENSE` => `LICENSE.txt` to simplify parsing consistency on various platforms and tools +- `cucumber` v8 => v10 +- `aruba` v2.1 => v2.2 + +### Deprecated + +- `BomComponent` => `Cyclonedx::BomComponent` +- `Bombuilder` => `Cyclonedx::BomBuilder` (note the capitalization change) +- `Object.purl` => `Cyclonedx::BomHelpers.purl` +- `Object.random_urn_uuid` => `Cyclonedx::BomHelpers.random_urn_uuid` +- `Object.build_bom` => `Cyclonedx::BomHelpers.build_bom` +- `Object.build_json_bom` => `Cyclonedx::BomHelpers.build_json_bom` +- `Object.build_bom_xml` => `Cyclonedx::BomHelpers.build_bom_xml` +- `Object.get_gem` => `Cyclonedx::BomHelpers.get_gem` + +### Removed + +### Fixed + +- `Nokogiri::XML::Builder` context relies on `method_missing` + - Globally defined `Object#purl` conflicted with ``. + - Moved to `Cyclonedx::BomHelpers.purl` to avoid conflict in v2.0.0 (along with all other global methods) + - Fixed existing usage via the built-in Nokogiri workaround of adding an underscore `purl_` + - The XML tag is unchanged as `` + +### Security + +## [1.1.0] - 2019-07-13 + +- TAG: [v1.1.0][1.1.0t] + +### Added + +- Initial release + +[Unreleased]: https://gitlab.com/CycloneDX/cyclonedx-ruby-gem/-/compare/v1.1.0...HEAD +[1.1.0]: https://github.com/CycloneDX/cyclonedx-ruby-gem/compare/eecfebe3cb0ce961fef8e424162ac94298f02a9f...v1.1.0 +[1.1.0t]: https://github.com/CycloneDX/cyclonedx-ruby-gem/releases/tag/v1.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7157a53 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,137 @@ +# Contributing + +Any contribution is welcome. +Please read the [CycloneDX contributing guidelines](https://github.com/CycloneDX/.github/blob/master/CONTRIBUTING.md) first. + +Pull-requests from forks are welcome. +We love to see your purposed changes, but we also like to discuss things first. Please open a [ticket][📜src-gh] and explain your intended changes to the community. And don't forget to mention that discussion in your pull-request later. +Find the needed basics here: +* [how to fork a repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) +* [how create a pull request from a fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) +* Remember to [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] if you make changes. + +## Setup + +This project uses ruby. Have a recent version installed and setup first. + +To install dev-dependencies and tools: + +```shell +bin/setup +``` + +## Environment Variables for Local Development + +Below are the primary environment variables recognized by stone_checksums (and its integrated tools). Unless otherwise noted, set boolean values to the string "true" to enable. + +General/runtime +- MIMIC_NEXT_MAJOR_VERSION: When set to true, simulates the next major version for testing breaking changes [📌semver-breaking] [📌major-versions-not-sacred] (default: false) +- ARUBA_NO_COVERAGE: Disable SimpleCov coverage in Aruba tests (default: false) + +For a quick starting point, this repository’s `.envrc` shows sane defaults, and `.env.local` can override them locally. + +## Testing + +To run all tests + +```console +bundle exec rake test +``` + +Or use the default task, which does the same + +```console +bundle exec rake +``` + +### Spec organization (required) + +- One spec file per class/module. For each class or module under `lib/`, keep all of its unit tests in a single spec file under `spec/` that mirrors the path and file name exactly: `lib/cyclonedx/my_class.rb` -> `spec/cyclonedx/my_class_spec.rb`. +- Exception: Integration specs that intentionally span multiple classes. Place these under `spec/integration/` (or a clearly named integration folder), and do not directly mirror a single class. Name them after the scenario, not a class. + +## Lint It + +Run the linter. + +```console +bundle exec rake rubocop +``` + +### Important: Do not add inline RuboCop disables + +Try not to add `# rubocop:disable ...` / `# rubocop:enable ...` comments to code or specs (except when following the few existing `rubocop:disable` patterns for a rule already being disabled elsewhere in the code). Instead: + +- Prefer configuration-based exclusions when a rule should not apply to certain paths or files (e.g., via `.rubocop.yml`). + - `bundle exec rubocop -a` (preferred) + - `bundle exec rubocop --regenerate-todo` (only when you cannot fix the violations immediately) + +As a general rule, fix style issues rather than ignoring them. For example, our specs should follow RSpec conventions like using `described_class` for the class under test. + +## Sign off your commits + +Please sign off your commits, to show that you agree to publish your changes under the current terms and licenses of the project +, and to indicate agreement with [Developer Certificate of Origin (DCO)](https://developercertificate.org/). + +```shell +git commit --signed-off ... +``` + +## Contributors + +Your picture could be here! + +[![Contributors][🖐contributors-img]][🖐contributors] + +Made with [contributors-img][🖐contrib-rocks]. + +## For Maintainers + +### To release a new version: + +#### Automated process + +Coming Soon! + +#### Manual process + +1. Run `bin/setup && bin/rake` as a "test, coverage, & linting" sanity check +2. Update the version number in `version.rb`, and ensure `CHANGELOG.md` reflects changes +3. Run `bin/setup && bin/rake` again as a secondary check, and to update `Gemfile.lock` +4. Run `git commit -am "🔖 Prepare release v"` to commit the changes +5. Run `git push` to trigger the final CI pipeline before release, and merge PRs + - NOTE: Remember to [check the build][🧪build]. +6. Run `export GIT_TRUNK_BRANCH_NAME="$(git remote show origin | grep 'HEAD branch' | cut -d ' ' -f5)" && echo $GIT_TRUNK_BRANCH_NAME` +7. Run `git checkout $GIT_TRUNK_BRANCH_NAME` +8. Run `git pull origin $GIT_TRUNK_BRANCH_NAME` to ensure latest trunk code +9. Optional for older Bundler (< 2.7.0): Set `SOURCE_DATE_EPOCH` so `rake build` and `rake release` use the same timestamp and generate the same checksums + - If your Bundler is >= 2.7.0, you can skip this; builds are reproducible by default. + - Run `export SOURCE_DATE_EPOCH=$EPOCHSECONDS && echo $SOURCE_DATE_EPOCH` + - If the echo above has no output, then it didn't work. + - Note: `zsh/datetime` module is needed, if running `zsh`. + - In older versions of `bash` you can use `date +%s` instead, i.e. `export SOURCE_DATE_EPOCH=$(date +%s) && echo $SOURCE_DATE_EPOCH` +10. Run `bundle exec rake build` +11. Run `bundle exec rake release` which will create a git tag for the version, + push git commits and tags, and push the `.gem` file to the gem host configured in the gemspec. +12. Run `bin/gem_checksums` (more context [1][🔒️rubygems-checksums-pr], [2][🔒️rubygems-guides-pr]) + to create SHA-256 and SHA-512 checksums. This functionality is provided by the `stone_checksums` + [gem][💎stone_checksums]. + - The script automatically commits but does not push the checksums +13. Sanity check the SHA256, comparing with the output from the `bin/gem_checksums` command: + - `sha256sum pkg/-.gem` + +[📜src-gh]: https://github.com/CycloneDX/cyclonedx-ruby-gem +[🧪build]: https://github.com/CycloneDX/cyclonedx-ruby-gem/actions +[🤝conduct]: https://gitlab.com/CycloneDX/cyclonedx-ruby-gem/-/blob/main/CODE_OF_CONDUCT.md +[🖐contrib-rocks]: https://contrib.rocks +[🖐contributors]: https://github.com/CycloneDX/cyclonedx-ruby-gem/graphs/contributors +[🖐contributors-img]: https://contrib.rocks/image?repo=CycloneDX/cyclonedx-ruby-gem +[💎gem-coop]: https://gem.coop +[🔒️rubygems-security-guide]: https://guides.rubygems.org/security/#building-gems +[🔒️rubygems-checksums-pr]: https://github.com/rubygems/rubygems/pull/6022 +[🔒️rubygems-guides-pr]: https://github.com/rubygems/guides/pull/325 +[💎stone_checksums]: https://github.com/galtzo-floss/stone_checksums +[📗keep-changelog]: https://keepachangelog.com/en/1.0.0/ +[📗keep-changelog-img]: https://img.shields.io/badge/keep--a--changelog-1.0.0-FFDD67.svg?style=flat +[📌semver-breaking]: https://github.com/semver/semver/issues/716#issuecomment-869336139 +[📌major-versions-not-sacred]: https://tom.preston-werner.com/2022/05/23/major-version-numbers-are-not-sacred.html +[🏃‍♂️runner-tool-cache]: https://github.com/ruby/ruby-builder/releases/tag/toolcache diff --git a/Gemfile b/Gemfile index 3195f8a..ce4a7ff 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' # Specify your gem's dependencies in cyclonedx-ruby.gemspec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..fcb1cd3 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,196 @@ +PATH + remote: . + specs: + cyclonedx-ruby (1.2.0) + activesupport (~> 7.0) + json (~> 2.6) + nokogiri (~> 1.15) + ostruct (~> 0.5.5) + rest-client (~> 2.0) + +GEM + remote: https://rubygems.org/ + specs: + activesupport (7.2.3) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + aruba (2.3.2) + bundler (>= 1.17, < 3.0) + contracts (>= 0.16.0, < 0.18.0) + cucumber (>= 8.0, < 11.0) + rspec-expectations (>= 3.4, < 5.0) + thor (~> 1.0) + ast (2.4.3) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (3.3.1) + builder (3.3.0) + concurrent-ruby (1.3.5) + connection_pool (2.5.4) + contracts (0.17.2) + cucumber (10.1.1) + base64 (~> 0.2) + builder (~> 3.2) + cucumber-ci-environment (> 9, < 11) + cucumber-core (> 15, < 17) + cucumber-cucumber-expressions (> 17, < 19) + cucumber-html-formatter (> 20.3, < 22) + diff-lcs (~> 1.5) + logger (~> 1.6) + mini_mime (~> 1.1) + multi_test (~> 1.1) + sys-uname (~> 1.3) + cucumber-ci-environment (10.0.1) + cucumber-core (15.3.0) + cucumber-gherkin (> 27, < 35) + cucumber-messages (> 26, < 30) + cucumber-tag-expressions (> 5, < 9) + cucumber-cucumber-expressions (18.0.1) + bigdecimal + cucumber-gherkin (34.0.0) + cucumber-messages (> 25, < 29) + cucumber-html-formatter (21.15.1) + cucumber-messages (> 19, < 28) + cucumber-messages (27.2.0) + cucumber-tag-expressions (8.0.0) + diff-lcs (1.6.2) + docile (1.4.1) + domain_name (0.6.20240107) + drb (2.2.3) + ffi (1.17.2) + ffi (1.17.2-aarch64-linux-gnu) + ffi (1.17.2-aarch64-linux-musl) + ffi (1.17.2-arm-linux-gnu) + ffi (1.17.2-arm-linux-musl) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) + ffi (1.17.2-x86_64-linux-musl) + http-accept (1.7.0) + http-cookie (1.1.0) + domain_name (~> 0.5) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + json (2.15.2) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) + memoist3 (1.0.0) + mime-types (3.7.0) + logger + mime-types-data (~> 3.2025, >= 3.2025.0507) + mime-types-data (3.2025.0924) + mini_mime (1.1.5) + minitest (5.26.0) + multi_test (1.1.0) + netrc (0.11.0) + nokogiri (1.18.10-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.18.10-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.18.10-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.10-x86_64-linux-musl) + racc (~> 1.4) + ostruct (0.5.5) + parallel (1.27.0) + parser (3.3.10.0) + ast (~> 2.4.1) + racc + prism (1.6.0) + racc (1.8.1) + rainbow (3.1.1) + rake (13.3.1) + regexp_parser (2.11.3) + rest-client (2.1.0) + http-accept (>= 1.7.0, < 2.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.6) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.6) + rubocop (1.81.6) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.47.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.47.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + ruby-progressbar (1.13.0) + securerandom (0.4.1) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.13.2) + simplecov_json_formatter (0.1.4) + stone_checksums (1.0.3) + version_gem (~> 1.1, >= 1.1.9) + sys-uname (1.4.1) + ffi (~> 1.1) + memoist3 (~> 1.0.0) + thor (1.4.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.1.0) + version_gem (1.1.9) + +PLATFORMS + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + aruba (~> 2.2) + cucumber (~> 10.1, >= 10.1.1) + cyclonedx-ruby! + rake (~> 13) + rspec (~> 3.12) + rubocop (~> 1.54) + simplecov (~> 0.22.0) + stone_checksums (~> 1.0, >= 1.0.3) + +BUNDLED WITH + 2.7.2 diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/README.md b/README.md index d2f94ec..d355496 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ # CycloneDX Ruby Gem -The CycloneDX Ruby Gem creates a valid CycloneDX Software Bill of Materials (SBOM) from all project dependencies. CycloneDX is a lightweight SBOM specification that is easily created, human readable, and simple to parse. +The CycloneDX Ruby Gem creates a valid CycloneDX Software Bill of Materials (SBOM) from all project dependencies. CycloneDX is a lightweight SBOM specification that is easily created, human-readable, and simple to parse. #### Installing from RubyGems @@ -29,14 +29,29 @@ cyclonedx-ruby [options] `-v, --[no-]verbose` Run verbosely `-p, --path path` Path to Ruby project directory - `-f, --format` Bom output format + `-o, --output bom_file_path` Path to output the bom file + `-f, --format bom_output_format` Output format for bom. Supported: xml (default), json + `-s, --spec-version version` CycloneDX spec version to target (default: 1.7). Supported: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7 `-h, --help` Show help message **Output:** bom.xml or bom.json file in project directory -#### Example +- By default, outputs conform to CycloneDX spec version 1.7. +- To generate an older spec version, use `--spec-version`. + +#### Examples ```bash +# Default (XML, CycloneDX 1.7) cyclonedx-ruby -p /path/to/ruby/project + +# JSON at CycloneDX 1.7 +cyclonedx-ruby -p /path/to/ruby/project -f json + +# XML at CycloneDX 1.3 +cyclonedx-ruby -p /path/to/ruby/project -s 1.3 + +# JSON at CycloneDX 1.2 to a custom path +cyclonedx-ruby -p /path/to/ruby/project -f json -s 1.2 -o bom/out.json ``` @@ -48,4 +63,3 @@ CycloneDX Ruby Gem is Copyright (c) OWASP Foundation. All Rights Reserved. Permission to modify and redistribute is granted under the terms of the Apache 2.0 license. See the [LICENSE] file for the full license. [License]: https://github.com/CycloneDX/cyclonedx-ruby-gem/blob/master/LICENSE - diff --git a/Rakefile b/Rakefile index a18e7a9..a176eaf 100644 --- a/Rakefile +++ b/Rakefile @@ -1,42 +1,28 @@ -#!/usr/bin/env rake -$LOAD_PATH << File.expand_path(__dir__) - -require "aruba/platform" - -require "bundler" -Bundler.setup +# frozen_string_literal: true require 'bundler/gem_tasks' -require "cucumber/rake/task" -require "rspec/core/rake_task" +require 'aruba/platform' require 'rake/clean' -# Work around a bug in `rake/clean` from `rake` versions older than 13. It's -# failing when it calls `FileUtils::rm_r` because that method needs to receive -# the `opts` parameter as parameters instead of as a `Hash`. -module Rake - module Cleaner - module_function - - def cleanup(file_name, **opts) - begin - opts = { verbose: Rake.application.options.trace }.merge(opts) - rm_r file_name, **opts - rescue StandardError => ex - puts "Failed to remove #{file_name}: #{ex}" unless file_already_gone?(file_name) - end - end - end -end - # Remove the `coverage` directory when the `:clobber` task is run. CLOBBER.include('coverage') +# Cucumber +require 'cucumber/rake/task' + Cucumber::Rake::Task.new do |t| t.cucumber_opts = %w(--format progress) end -RSpec::Core::RakeTask.new('spec') +# RSpec +require 'rspec/core/rake_task' + +RSpec::Core::RakeTask.new(:spec) + +# RuboCop +require 'rubocop/rake_task' + +RuboCop::RakeTask.new # Run the `clobber` task when running the entire test suite, because the # coverage information reported by `simplecov` can be skewed when a `coverage` @@ -44,4 +30,5 @@ RSpec::Core::RakeTask.new('spec') desc "Run the whole test suite." task test: [:clobber, :spec, :cucumber] -task default: :test \ No newline at end of file +# Default Task +task default: :test diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..12ac7a7 --- /dev/null +++ b/bin/console @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'bundler/setup' +require 'cyclonedx/ruby' + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +require 'irb' +IRB.start(__FILE__) diff --git a/bin/cucumber b/bin/cucumber new file mode 100755 index 0000000..7c35145 --- /dev/null +++ b/bin/cucumber @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'cucumber' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('cucumber', 'cucumber') diff --git a/bin/gem_checksums b/bin/gem_checksums new file mode 100755 index 0000000..1d625b5 --- /dev/null +++ b/bin/gem_checksums @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'gem_checksums' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('stone_checksums', 'gem_checksums') diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000..9510b16 --- /dev/null +++ b/bin/rake @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rake' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('rake', 'rake') diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 0000000..1b35905 --- /dev/null +++ b/bin/rspec @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rspec' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('rspec-core', 'rspec') diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/cyclonedx-ruby.gemspec b/cyclonedx-ruby.gemspec index 087712e..a299ad2 100644 --- a/cyclonedx-ruby.gemspec +++ b/cyclonedx-ruby.gemspec @@ -1,26 +1,58 @@ # frozen_string_literal: true +require_relative "lib/cyclonedx/ruby/version" + Gem::Specification.new do |spec| - spec.name = 'cyclonedx-ruby' - spec.version = '1.2.0' - spec.date = '2023-07-14' + spec.name = 'cyclonedx-ruby' + spec.version = Cyclonedx::Ruby::VERSION + spec.authors = ['Joseph Kobti', 'Steve Springett'] + spec.email = ['josephkobti@outlook.com'] + spec.summary = 'CycloneDX software bill-of-material (SBoM) generation utility' spec.description = 'CycloneDX is a lightweight software bill-of-material (SBOM) specification designed for use in application security contexts and supply chain component analysis. This Gem generates CycloneDX BOMs from Ruby projects.' - spec.authors = ['Joseph Kobti', 'Steve Springett'] - spec.email = 'josephkobti@outlook.com' spec.homepage = 'https://github.com/CycloneDX/cyclonedx-ruby-gem' spec.license = 'Apache-2.0' - spec.required_ruby_version = '>= 2.7.0' - spec.files = Dir.chdir(__dir__) do - `git ls-files -z`.split("\x0").reject do |f| - (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor]) - end - end - spec.bindir = 'exe' - spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.metadata["homepage_uri"] = "https://github.com/CycloneDX/cyclonedx-ruby-gem?tab=readme-ov-file#readme" + spec.metadata["source_code_uri"] = "https://github.com/CycloneDX/cyclonedx-ruby-gem/tree/v#{spec.version}" + spec.metadata["changelog_uri"] = "https://github.com/CycloneDX/cyclonedx-ruby-gem/blob/v#{spec.version}/CHANGELOG.md" + spec.metadata["bug_tracker_uri"] = "https://github.com/CycloneDX/cyclonedx-ruby-gem/issues" + spec.metadata["funding_uri"] = "https://owasp.org/donate/?reponame=www-project-cyclonedx&title=OWASP+CycloneDX" + spec.metadata["rubygems_mfa_required"] = "true" + + # Specify which files are part of the released package. + spec.files = Dir[ + # Code / tasks / data (NOTE: exe/ is specified via spec.bindir and spec.executables below) + "lib/**/*.rb", + "lib/licenses.json", + # Signatures + "sig/**/*.rbs" + ] + + # Automatically included with gem package, no need to list again in files. + spec.extra_rdoc_files = Dir[ + # Files (alphabetical) + "CHANGELOG.md", + "LICENSE.txt", + "NOTICE", + "README.md" + ] + spec.rdoc_options += [ + "--title", + "#{spec.name} - #{spec.summary}", + "--main", + "README.md", + "--exclude", + "^sig/", + "--line-numbers", + "--inline-source", + "--quiet" + ] spec.require_paths = ['lib'] + spec.bindir = "exe" + # Listed files are the relative paths from bindir above. + spec.executables = ['cyclonedx-ruby'] spec.add_dependency('json', '~> 2.6') spec.add_dependency('nokogiri', '~> 1.15') @@ -29,8 +61,9 @@ Gem::Specification.new do |spec| spec.add_dependency('activesupport', '~> 7.0') spec.add_development_dependency 'rake', '~> 13' spec.add_development_dependency 'rspec', '~> 3.12' - spec.add_development_dependency 'cucumber', '~> 8.0' - spec.add_development_dependency 'aruba', '~> 2.1' + spec.add_development_dependency 'cucumber', '~> 10.1', '>= 10.1.1' + spec.add_development_dependency 'aruba', '~> 2.2' spec.add_development_dependency 'simplecov', '~> 0.22.0' spec.add_development_dependency 'rubocop', '~> 1.54' + spec.add_development_dependency 'stone_checksums', '~> 1.0', '>= 1.0.3' end diff --git a/exe/cyclonedx-ruby b/exe/cyclonedx-ruby index 10ff189..5ecf025 100755 --- a/exe/cyclonedx-ruby +++ b/exe/cyclonedx-ruby @@ -1,5 +1,10 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require 'bom_builder' -Bombuilder.build(ARGV[0]) +if ENV.fetch('MIMIC_NEXT_MAJOR_VERSION', 'false').casecmp?('true') + require 'cyclonedx/ruby' + Cyclonedx::BomBuilder.build(ARGV[0]) +else + require 'bom_builder' + Bombuilder.build(ARGV[0]) +end diff --git a/features/fixtures/simple/Gemfile b/features/fixtures/simple/Gemfile index 180e0f6..0f7d066 100644 --- a/features/fixtures/simple/Gemfile +++ b/features/fixtures/simple/Gemfile @@ -1,3 +1,9 @@ +# frozen_string_literal: true + source 'https://rubygems.org' -gem 'activesupport' +gem 'activesupport', '7.0.4.3' +gem 'concurrent-ruby', '1.2.2' +gem 'i18n', '1.12.0' +gem 'minitest', '5.18.0' +gem 'tzinfo', '2.0.6' diff --git a/features/fixtures/simple/Gemfile.lock b/features/fixtures/simple/Gemfile.lock index 66a0f90..cdb6220 100644 --- a/features/fixtures/simple/Gemfile.lock +++ b/features/fixtures/simple/Gemfile.lock @@ -1,15 +1,15 @@ GEM remote: https://rubygems.org/ specs: - activesupport (7.0.7.1) + activesupport (7.0.4.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) concurrent-ruby (1.2.2) - i18n (1.14.1) + i18n (1.12.0) concurrent-ruby (~> 1.0) - minitest (5.19.0) + minitest (5.18.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) @@ -18,7 +18,11 @@ PLATFORMS x86_64-linux DEPENDENCIES - activesupport + activesupport (= 7.0.4.3) + concurrent-ruby (= 1.2.2) + i18n (= 1.12.0) + minitest (= 5.18.0) + tzinfo (= 2.0.6) BUNDLED WITH 2.4.10 diff --git a/features/fixtures/simple/bom.json.expected b/features/fixtures/simple/bom.json.expected index d9d7dce..39b6168 100644 --- a/features/fixtures/simple/bom.json.expected +++ b/features/fixtures/simple/bom.json.expected @@ -1,6 +1,6 @@ { "bomFormat": "CycloneDX", - "specVersion": "1.1", + "specVersion": "1.7", "serialNumber": "urn:uuid:d498cdc2-5494-4031-b37d-ff3d10d336bf", "version": 1, "components": [ @@ -105,4 +105,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/features/fixtures/simple/bom.xml.expected b/features/fixtures/simple/bom.xml.expected index b379102..9cd91a6 100644 --- a/features/fixtures/simple/bom.xml.expected +++ b/features/fixtures/simple/bom.xml.expected @@ -1,5 +1,5 @@ - + activesupport diff --git a/features/help.feature b/features/help.feature index f2ee863..31fda67 100644 --- a/features/help.feature +++ b/features/help.feature @@ -12,5 +12,6 @@ Scenario: Generate help on demand -p, --path path (Required) Path to Ruby project directory -o, --output bom_file_path (Optional) Path to output the bom.xml file to -f, --format bom_output_format (Optional) Output format for bom. Currently support xml (default) and json. + -s, --spec-version version (Optional) CycloneDX spec version to target (default: 1.7). Supported: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7 -h, --help Show help message """ diff --git a/features/json_format.feature b/features/json_format.feature index ae231fb..7f24c00 100644 --- a/features/json_format.feature +++ b/features/json_format.feature @@ -40,4 +40,3 @@ Feature: Creating BOM using Json format """ And a file named "bom.json" should exist And the generated Json BOM file "bom.json" matches "bom.json.expected" - diff --git a/features/step_definitions/json_bom_matching.rb b/features/step_definitions/json_bom_matching.rb index 0a81ea1..65170ff 100644 --- a/features/step_definitions/json_bom_matching.rb +++ b/features/step_definitions/json_bom_matching.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true + Then('the generated Json BOM file {string} matches {string}') do |generated_file, expected_file| generated_file_contents = File.read(expand_path(generated_file)) expected_file_contents = File.read(expand_path(expected_file)) serial_number_matcher = /\"serialNumber\": \"urn:uuid:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\"/ normalized_serial_number = '"serialNumber": "urn:uuid:00000000-0000-0000-0000-000000000000"' - normalized_generated_file_contents = generated_file_contents.gsub(serial_number_matcher, normalized_serial_number) - normalized_expected_file_contents = expected_file_contents.gsub(serial_number_matcher, normalized_serial_number) + normalized_generated_file_contents = generated_file_contents.gsub(serial_number_matcher, normalized_serial_number).rstrip + normalized_expected_file_contents = expected_file_contents.gsub(serial_number_matcher, normalized_serial_number).rstrip expect(normalized_expected_file_contents).to eq(normalized_generated_file_contents) end diff --git a/features/step_definitions/xml_bom_matching.rb b/features/step_definitions/xml_bom_matching.rb index 004c9b2..50c6e6f 100644 --- a/features/step_definitions/xml_bom_matching.rb +++ b/features/step_definitions/xml_bom_matching.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Then('the generated XML BOM file {string} matches {string}') do |generated_file, expected_file| generated_file_contents = File.read(expand_path(generated_file)) expected_file_contents = File.read(expand_path(expected_file)) diff --git a/features/support/env.rb b/features/support/env.rb index 331ff6a..96b3862 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Based on https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/features/support/env.rb # Licensed under MIT - https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/LICENSE diff --git a/features/support/simplecov_support.rb b/features/support/simplecov_support.rb index b3a91a3..3c5465f 100644 --- a/features/support/simplecov_support.rb +++ b/features/support/simplecov_support.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Copied from https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/features/support/simplecov_setup.rb # Licensed under MIT - https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/LICENSE diff --git a/lib/bom_builder.rb b/lib/bom_builder.rb index 388614e..dae336e 100644 --- a/lib/bom_builder.rb +++ b/lib/bom_builder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file is part of CycloneDX Ruby Gem. # # Licensed to the Apache Software Foundation (ASF) under one @@ -20,164 +22,6 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. # -# frozen_string_literal: true -require 'bundler' -require 'fileutils' -require 'json' -require 'logger' -require 'nokogiri' -require 'optparse' -require 'ostruct' -require 'rest_client' -require 'securerandom' -require_relative 'bom_helpers' -require 'active_support/core_ext/hash' - -class Bombuilder - SUPPORTED_BOM_FORMATS = %w[xml json] - - def self.build(path) - original_working_directory = Dir.pwd - setup(path) - specs_list - bom = build_bom(@gems, @bom_output_format) - - begin - @logger.info("Changing directory to the original working directory located at #{original_working_directory}") - Dir.chdir original_working_directory - rescue StandardError => e - @logger.error("Unable to change directory the original working directory located at #{original_working_directory}. #{e.message}: #{e.backtrace.join('\n')}") - abort - end - - bom_directory = File.dirname(@bom_file_path) - begin - FileUtils.mkdir_p(bom_directory) unless File.directory?(bom_directory) - rescue StandardError => e - @logger.error("Unable to create the directory to hold the BOM output at #{@bom_directory}. #{e.message}: #{e.backtrace.join('\n')}") - abort - end - - begin - @logger.info("Writing BOM to #{@bom_file_path}...") - File.open(@bom_file_path, 'w') { |file| file.write(bom) } - - if @options[:verbose] - @logger.info("#{@gems.size} gems were written to BOM located at #{@bom_file_path}") - else - puts "#{@gems.size} gems were written to BOM located at #{@bom_file_path}" - end - rescue StandardError => e - @logger.error("Unable to write BOM to #{@bom_file_path}. #{e.message}: #{e.backtrace.join('\n')}") - abort - end - end - private - def self.setup(path) - @options = {} - OptionParser.new do |opts| - opts.banner = 'Usage: cyclonedx-ruby [options]' - - opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v| - @options[:verbose] = v - end - opts.on('-p', '--path path', '(Required) Path to Ruby project directory') do |path| - @options[:path] = path - end - opts.on('-o', '--output bom_file_path', '(Optional) Path to output the bom.xml file to') do |bom_file_path| - @options[:bom_file_path] = bom_file_path - end - opts.on('-f', '--format bom_output_format', '(Optional) Output format for bom. Currently support xml (default) and json.') do |bom_output_format| - @options[:bom_output_format] = bom_output_format - end - opts.on_tail('-h', '--help', 'Show help message') do - puts opts - exit - end - end.parse! - - @logger = Logger.new($stdout) - @logger.level = if @options[:verbose] - Logger::INFO - else - Logger::ERROR - end - - @gems = [] - licenses_file = File.read "#{__dir__}/licenses.json" - @licenses_list = JSON.parse(licenses_file) - - if @options[:path].nil? - @logger.error('missing path to project directory') - abort - end - - unless File.directory?(@options[:path]) - @logger.error("path provided is not a valid directory. path provided was: #{@options[:path]}") - abort - end - - begin - @logger.info("Changing directory to Ruby project directory located at #{@options[:path]}") - Dir.chdir @options[:path] - rescue StandardError => e - @logger.error("Unable to change directory to Ruby project directory located at #{@options[:path]}. #{e.message}: #{e.backtrace.join('\n')}") - abort - end - - if @options[:bom_output_format].nil? - @bom_output_format = 'xml' - elsif SUPPORTED_BOM_FORMATS.include?(@options[:bom_output_format]) - @bom_output_format = @options[:bom_output_format] - else - @logger.error("Unrecognized cyclonedx bom output format provided. Please choose one of #{SUPPORTED_BOM_FORMATS}") - abort - end - - @bom_file_path = if @options[:bom_file_path].nil? - "./bom.#{@bom_output_format}" - else - @options[:bom_file_path] - end - - @logger.info("BOM will be written to #{@bom_file_path}") - - begin - gemfile_path = "#{@options[:path]}/Gemfile.lock" - @logger.info("Parsing specs from #{gemfile_path}...") - gemfile_contents = File.read(gemfile_path) - @specs = Bundler::LockfileParser.new(gemfile_contents).specs - @logger.info('Specs successfully parsed!') - rescue StandardError => e - @logger.error("Unable to parse specs from #{gemfile_path}. #{e.message}: #{e.backtrace.join('\n')}") - abort - end - end - - def self.specs_list - count = 0 - @specs.each do |dependency| - object = OpenStruct.new - object.name = dependency.name - object.version = dependency.version - object.purl = purl(object.name, object.version) - gem = get_gem(object.name, object.version) - next if gem.nil? - - if gem['licenses']&.length&.positive? - if @licenses_list.include? gem['licenses'].first - object.license_id = gem['licenses'].first - else - object.license_name = gem['licenses'].first - end - end - object.author = gem['authors'] - object.description = gem['summary'] - object.hash = gem['sha'] - @gems.push(object) - count += 1 - @logger.info("#{object.name}:#{object.version} gem added") - end - end -end +require_relative 'cyclonedx/ruby' +require_relative 'cyclonedx_deprecated' diff --git a/lib/bom_component.rb b/lib/bom_component.rb index c2af8fe..be8402f 100644 --- a/lib/bom_component.rb +++ b/lib/bom_component.rb @@ -1,45 +1,4 @@ +# frozen_string_literal: true -class BomComponent - DEFAULT_TYPE = "library".freeze - HASH_ALG = 'SHA-256'.freeze - - def initialize(gem) - @name = gem['name'] - @version = gem['version'] - @description = gem['description'] - @hash = gem['hash'] - @purl = gem['purl'] - @gem = gem - end - - def hash_val - component_hash = { - "type": DEFAULT_TYPE, - "name": @name, - "version": @version, - "description": @description, - "purl": @purl, - "hashes": [ - "alg": HASH_ALG, - "content": @hash - ] - } - - if @gem['license_id'] - component_hash[:"licenses"] = [ - "license": { - "id": @gem['license_id'] - } - ] - elsif @gem['license_name'] - component_hash[:"licenses"] = [ - "license": { - "name": @gem['license_name'] - } - ] - end - - [component_hash] - - end -end \ No newline at end of file +require_relative "cyclonedx/ruby" +require_relative "cyclonedx_deprecated" diff --git a/lib/bom_helpers.rb b/lib/bom_helpers.rb index 01c0392..b74ce59 100644 --- a/lib/bom_helpers.rb +++ b/lib/bom_helpers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file is part of CycloneDX Ruby Gem. # # Licensed to the Apache Software Foundation (ASF) under one @@ -20,87 +22,6 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. # -# frozen_string_literal: true - -require_relative 'bom_component' - -def purl(name, version) - "pkg:gem/#{name}@#{version}" -end - -def random_urn_uuid - "urn:uuid:#{SecureRandom.uuid}" -end - -def build_bom(gems, format) - if format == 'json' - build_json_bom(gems) - else - build_bom_xml(gems) - end -end - -def build_json_bom(gems) - bom_hash = { - "bomFormat": "CycloneDX", - "specVersion": "1.1", - "serialNumber": random_urn_uuid, - "version": 1, - "components": [] - } - - gems.each do |gem| - bom_hash[:components] += BomComponent.new(gem).hash_val() - end - - JSON.pretty_generate(bom_hash) -end - -def build_bom_xml(gems) - builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| - attributes = { 'xmlns' => 'http://cyclonedx.org/schema/bom/1.1', 'version' => '1', 'serialNumber' => random_urn_uuid } - xml.bom(attributes) do - xml.components do - gems.each do |gem| - xml.component('type' => 'library') do - xml.name gem['name'] - xml.version gem['version'] - xml.description gem['description'] - xml.hashes do - xml.hash_ gem['hash'], alg: 'SHA-256' - end - if gem['license_id'] - xml.licenses do - xml.license do - xml.id gem['license_id'] - end - end - elsif gem['license_name'] - xml.licenses do - xml.license do - xml.name gem['license_name'] - end - end - end - xml.purl gem['purl'] - end - end - end - end - end - - builder.to_xml -end -def get_gem(name, version) - url = "https://rubygems.org/api/v1/versions/#{name}.json" - begin - RestClient.proxy = ENV['http_proxy'] - response = RestClient.get(url) - body = JSON.parse(response.body) - body.select { |item| item['number'] == version.to_s }.first - rescue StandardError - @logger.warn("#{name} couldn't be fetched") - nil - end -end +require_relative "cyclonedx/ruby" +require_relative "cyclonedx_deprecated" diff --git a/lib/cyclonedx/bom_builder.rb b/lib/cyclonedx/bom_builder.rb new file mode 100644 index 0000000..4d7a4b5 --- /dev/null +++ b/lib/cyclonedx/bom_builder.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +module Cyclonedx + class BomBuilder + SUPPORTED_BOM_FORMATS = %w[xml json] + SUPPORTED_SPEC_VERSIONS = %w[1.1 1.2 1.3 1.4 1.5 1.6 1.7] + + extend Cyclonedx::BomHelpers + + def self.build(path) + original_working_directory = Dir.pwd + setup(path) + specs_list + bom = build_bom(@gems, @bom_output_format, @spec_version) + + begin + @logger.info("Changing directory to the original working directory located at #{original_working_directory}") + Dir.chdir original_working_directory + rescue StandardError => e + @logger.error("Unable to change to the original working directory located at #{original_working_directory}. #{e.message}: #{Array(e.backtrace).join("\n")}") + abort + end + + bom_directory = File.dirname(@bom_file_path) + begin + FileUtils.mkdir_p(bom_directory) unless File.directory?(bom_directory) + rescue StandardError => e + @logger.error("Unable to create the directory to hold the BOM output at #{bom_directory}. #{e.message}: #{Array(e.backtrace).join("\n")}") + abort + end + + begin + @logger.info("Writing BOM to #{@bom_file_path}...") + File.write(@bom_file_path, bom) + + if @options[:verbose] + @logger.info("#{@gems.size} gems were written to BOM located at #{@bom_file_path}") + else + puts "#{@gems.size} gems were written to BOM located at #{@bom_file_path}" + end + rescue StandardError => e + @logger.error("Unable to write BOM to #{@bom_file_path}. #{e.message}: #{Array(e.backtrace).join("\n")}") + abort + end + end + + def self.setup(path) + @options = {} + OptionParser.new do |opts| + opts.banner = 'Usage: cyclonedx-ruby [options]' + + opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v| + @options[:verbose] = v + end + opts.on('-p', '--path path', '(Required) Path to Ruby project directory') do |proj_path_opt| + @options[:path] = proj_path_opt + end + opts.on('-o', '--output bom_file_path', '(Optional) Path to output the bom.xml file to') do |bom_file_path| + @options[:bom_file_path] = bom_file_path + end + opts.on('-f', '--format bom_output_format', '(Optional) Output format for bom. Currently support xml (default) and json.') do |bom_output_format| + @options[:bom_output_format] = bom_output_format + end + opts.on('-s', '--spec-version version', '(Optional) CycloneDX spec version to target (default: 1.7). Supported: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7') do |spec_version| + @options[:spec_version] = spec_version + end + opts.on_tail('-h', '--help', 'Show help message') do + puts opts + exit + end + end.parse! + + # Allow passing the path as a positional arg via exe wrapper + @options[:path] ||= path + + @logger = Logger.new($stdout) + @logger.level = if @options[:verbose] + Logger::INFO + else + Logger::ERROR + end + + @gems = [] + # Adjusted to point to lib/licenses.json relative to this file's directory (lib/cyclonedx) + licenses_path = File.expand_path('../licenses.json', __dir__) + licenses_file = File.read(licenses_path) + @licenses_list = JSON.parse(licenses_file) + + if @options[:path].nil? + @logger.error('missing path to project directory') + abort + end + + unless File.directory?(@options[:path]) + @logger.error("path provided is not a valid directory. path provided was: #{@options[:path]}") + abort + end + + # Normalize to an absolute project path to avoid relative path issues later + @project_path = File.expand_path(@options[:path]) + @provided_path = @options[:path] + + begin + @logger.info("Changing directory to Ruby project directory located at #{@provided_path}") + Dir.chdir @project_path + rescue StandardError => e + @logger.error("Unable to change directory to Ruby project directory located at #{@provided_path}. #{e.message}: #{Array(e.backtrace).join("\n")}") + abort + end + + if @options[:bom_output_format].nil? + @bom_output_format = 'xml' + elsif SUPPORTED_BOM_FORMATS.include?(@options[:bom_output_format]) + @bom_output_format = @options[:bom_output_format] + else + @logger.error("Unrecognized cyclonedx bom output format provided. Please choose one of #{SUPPORTED_BOM_FORMATS}") + abort + end + + # Spec version selection + requested_spec = @options[:spec_version] || '1.7' + if SUPPORTED_SPEC_VERSIONS.include?(requested_spec) + @spec_version = requested_spec + else + @logger.error("Unrecognized CycloneDX spec version '#{requested_spec}'. Please choose one of #{SUPPORTED_SPEC_VERSIONS}") + abort + end + + @bom_file_path = if @options[:bom_file_path].nil? + "./bom.#{@bom_output_format}" + else + @options[:bom_file_path] + end + + @logger.info("BOM will be written to #{@bom_file_path}") + + begin + # Use absolute path so it's correct regardless of current working directory + gemfile_path = File.join(@project_path, 'Gemfile.lock') + # Compute display path for logs: './Gemfile.lock' when provided path is '.', else '/Gemfile.lock' + display_gemfile_path = (@provided_path == '.' ? './Gemfile.lock' : File.join(@provided_path, 'Gemfile.lock')) + @logger.info("Parsing specs from #{display_gemfile_path}...") + gemfile_contents = File.read(gemfile_path) + @specs = Bundler::LockfileParser.new(gemfile_contents).specs + @logger.info('Specs successfully parsed!') + rescue StandardError => e + @logger.error("Unable to parse specs from #{gemfile_path}. #{e.message}: #{Array(e.backtrace).join("\n")}") + abort + end + end + + def self.specs_list + count = 0 + @specs.each do |dependency| + object = OpenStruct.new + object.name = dependency.name + object.version = dependency.version + object.purl = purl(object.name, object.version) + gem = get_gem(object.name, object.version, @logger) + next if gem.nil? + + if gem['licenses']&.length&.positive? + if @licenses_list.include? gem['licenses'].first + object.license_id = gem['licenses'].first + else + object.license_name = gem['licenses'].first + end + end + + object.author = gem['authors'] + object.description = gem['summary'] + object.hash = gem['sha'] + @gems.push(object) + count += 1 + @logger.info("#{object.name}:#{object.version} gem added") + end + end + end +end diff --git a/lib/cyclonedx/bom_component.rb b/lib/cyclonedx/bom_component.rb new file mode 100644 index 0000000..672df9c --- /dev/null +++ b/lib/cyclonedx/bom_component.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Cyclonedx + class BomComponent + DEFAULT_TYPE = 'library' + HASH_ALG = 'SHA-256' + + def initialize(gem) + @name = gem['name'] + @version = gem['version'] + @description = gem['description'] + @hash = gem['hash'] + @purl = gem['purl'] + @gem = gem + end + + def hash_val + component_hash = { + type: DEFAULT_TYPE, + name: @name, + version: @version, + description: @description, + purl: @purl, + hashes: [ + alg: HASH_ALG, + content: @hash + ] + } + + if @gem['license_id'] + component_hash[:licenses] = [ + license: { + id: @gem['license_id'] + } + ] + elsif @gem['license_name'] + component_hash[:licenses] = [ + license: { + name: @gem['license_name'] + } + ] + end + + [component_hash] + end + end +end diff --git a/lib/cyclonedx/bom_helpers.rb b/lib/cyclonedx/bom_helpers.rb new file mode 100644 index 0000000..ab07eaf --- /dev/null +++ b/lib/cyclonedx/bom_helpers.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +# This file is part of CycloneDX Ruby Gem. +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +# + +require_relative 'bom_component' + +module Cyclonedx + module BomHelpers + module_function + + def cyclonedx_xml_namespace(spec_version) + "http://cyclonedx.org/schema/bom/#{spec_version}" + end + + def purl(name, version) + "pkg:gem/#{name}@#{version}" + end + + def random_urn_uuid + "urn:uuid:#{SecureRandom.uuid}" + end + + def build_bom(gems, format, spec_version) + if format == 'json' + build_json_bom(gems, spec_version) + else + build_bom_xml(gems, spec_version) + end + end + + def build_json_bom(gems, spec_version) + bom_hash = { + bomFormat: 'CycloneDX', + specVersion: spec_version, + serialNumber: random_urn_uuid, + version: 1, + components: [] + } + + gems.each do |gem| + bom_hash[:components] += Cyclonedx::BomComponent.new(gem).hash_val + end + + JSON.pretty_generate(bom_hash) + end + + def build_bom_xml(gems, spec_version) + builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| + attributes = { 'xmlns' => cyclonedx_xml_namespace(spec_version), 'version' => '1', 'serialNumber' => random_urn_uuid } + xml.bom(attributes) do + xml.components do + gems.each do |gem| + xml.component('type' => 'library') do + xml.name gem['name'] + xml.version gem['version'] + xml.description gem['description'] + xml.hashes do + xml.hash_ gem['hash'], alg: 'SHA-256' + end + if gem['license_id'] + xml.licenses do + xml.license do + xml.id gem['license_id'] + end + end + elsif gem['license_name'] + xml.licenses do + xml.license do + xml.name gem['license_name'] + end + end + end + # The globally scoped legacy `Object#purl` method breaks the Nokogiri builder context + # Fortunately Nokogiri has a built-in workaround, adding an underscore to the method name. + # The resulting XML tag is still ``. + # Globally scoped legacy `Object#purl` will be removed in v2.0.0, and this hack can be removed then. + xml.purl_ gem['purl'] + end + end + end + end + end + + builder.to_xml + end + + def get_gem(name, version, logger) + url = "https://rubygems.org/api/v1/versions/#{name}.json" + begin + RestClient.proxy = ENV.fetch('http_proxy', nil) + response = RestClient.get(url) + body = JSON.parse(response.body) + body.select { |item| item['number'] == version.to_s }.first + rescue StandardError + logger.warn("#{name} couldn't be fetched") + nil + end + end + end +end diff --git a/lib/cyclonedx/ruby.rb b/lib/cyclonedx/ruby.rb new file mode 100644 index 0000000..58bacdd --- /dev/null +++ b/lib/cyclonedx/ruby.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# External gems +require 'bundler' +require 'fileutils' +require 'json' +require 'logger' +require 'nokogiri' +require 'optparse' +require 'ostruct' +require 'rest_client' +require 'securerandom' +require 'active_support/core_ext/hash' + +# This gem +require_relative 'ruby/version' +require_relative 'bom_helpers' +require_relative 'bom_builder' # depends on bom_helpers +require_relative 'bom_component' + +module Cyclonedx + module Ruby + class Error < StandardError; end + # Your code goes here... + end +end diff --git a/lib/cyclonedx/ruby/deprecation.rb b/lib/cyclonedx/ruby/deprecation.rb new file mode 100644 index 0000000..e2a9469 --- /dev/null +++ b/lib/cyclonedx/ruby/deprecation.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# Provides tools that allow methods to be deprecated with new releases of the gem. +# +# Usage: +# # class method deprecation example +# class MyClass +# extend Cyclonedx::Ruby::Deprecation +# +# deprecated_alias :old_method, :new_method, self +# +# def self.new_method +# # new implementation +# end +# end +# +# # instance method deprecation example +# class MyClass +# extend Cyclonedx::Ruby::Deprecation +# +# deprecated_alias :old_method, :new_method +# +# def new_method +# # new implementation +# end +# end + +module Cyclonedx + module Ruby + module Deprecation + class << self + attr_accessor :deprecate_in_silence + end + + @deprecate_in_silence = false + + # Define a deprecated alias for a method + # @param [Symbol] scope - :instance or :class (default :instance) + # @param [Symbol] name - name of method to define + # @param [Symbol] replacement - name of method (to alias) + # @param [Constant] receiver - Receiver of the replacement method, use nil for instance methods (default nil) + def deprecated_alias(scope, name, replacement, receiver = nil) + if scope == :class + define_singleton_method(name) do |*args, &block| + warn("Cyclonedx: #{self}.#{name} deprecated (please use .#{replacement})") unless Cyclonedx::Ruby::Deprecation.deprecate_in_silence + receiver ? receiver.send(replacement, *args, &block) : send(replacement, *args, &block) + end + else + define_method(name) do |*args, &block| + warn("Cyclonedx: #{self.class}##{name} deprecated (please use #{receiver ? "#{receiver}.#{replacement}" : replacement})") unless Cyclonedx::Ruby::Deprecation.deprecate_in_silence + receiver ? receiver.send(replacement, *args, &block) : send(replacement, *args, &block) + end + end + end + end + end +end diff --git a/lib/cyclonedx/ruby/version.rb b/lib/cyclonedx/ruby/version.rb new file mode 100644 index 0000000..82d4071 --- /dev/null +++ b/lib/cyclonedx/ruby/version.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Cyclonedx + module Ruby + module Version + VERSION = '1.2.0' + end + VERSION = Version::VERSION # Make VERSION available in traditional way + end +end diff --git a/lib/cyclonedx_deprecated.rb b/lib/cyclonedx_deprecated.rb new file mode 100644 index 0000000..0bd0c8b --- /dev/null +++ b/lib/cyclonedx_deprecated.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require_relative 'cyclonedx/ruby/deprecation' + +# Legacy class name kept for compatibility until v2.0.0 +Bombuilder = Cyclonedx::BomBuilder + +# Legacy class name kept for compatibility until v2.0.0 +BomComponent = Cyclonedx::BomComponent + +# Legacy global methods included in Object (root namespace) kept for compatibility until v2.0.0 +extend Cyclonedx::Ruby::Deprecation + +deprecated_alias :instance, :purl, :purl, Cyclonedx::BomHelpers +deprecated_alias :instance, :random_urn_uuid, :random_urn_uuid, Cyclonedx::BomHelpers +deprecated_alias :instance, :build_bom, :build_bom, Cyclonedx::BomHelpers +deprecated_alias :instance, :build_json_bom, :build_json_bom, Cyclonedx::BomHelpers +deprecated_alias :instance, :build_bom_xml, :build_bom_xml, Cyclonedx::BomHelpers +deprecated_alias :instance, :get_gem, :get_gem, Cyclonedx::BomHelpers + +# Sanity +raise 'Deprecated methods broken' unless purl('activesupport', '7.0.1') == 'pkg:gem/activesupport@7.0.1' diff --git a/sig/cyclonedx/ruby.rbs b/sig/cyclonedx/ruby.rbs new file mode 100644 index 0000000..87df4e5 --- /dev/null +++ b/sig/cyclonedx/ruby.rbs @@ -0,0 +1,6 @@ +module Cyclonedx + module Ruby + VERSION: String + # See the writing guide of rbs: https://github.com/ruby/rbs#guides + end +end diff --git a/spec/bom_helpers_spec.rb b/spec/bom_helpers_spec.rb deleted file mode 100644 index 75adec2..0000000 --- a/spec/bom_helpers_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'bom_helpers' - -RSpec.describe 'helper methods' do - context '#purl' do - it 'builds a purl' do - expect(purl('activesupport', '7.0.1')).to eq("pkg:gem/activesupport@7.0.1") - end - end -end diff --git a/spec/bom_component_spec.rb b/spec/cyclonedx/bom_component_spec.rb similarity index 88% rename from spec/bom_component_spec.rb rename to spec/cyclonedx/bom_component_spec.rb index e02f4d5..127572d 100644 --- a/spec/bom_component_spec.rb +++ b/spec/cyclonedx/bom_component_spec.rb @@ -1,7 +1,6 @@ -require 'spec_helper' -require 'bom_component' +# frozen_string_literal: true -RSpec.shared_examples "a valid hash_val result for gem" do +RSpec.shared_examples 'a valid hash_val result for gem' do it { expect(result.count).to eq(1) } it { expect(result[0][:type]).to eq('library') } it { expect(result[0][:name]).to eq(gem.name) } @@ -13,7 +12,7 @@ it { expect(result[0][:hashes][0][:content]).to eq(gem.hash) } end -RSpec.describe BomComponent do +RSpec.describe Cyclonedx::BomComponent do context '#hash_val' do let(:base_gem) do OpenStruct.new( @@ -25,11 +24,11 @@ ) end - let(:gem) do + let(:gem) do base_gem end - subject(:result) { BomComponent.new(gem).hash_val } + subject(:result) { Cyclonedx::BomComponent.new(gem).hash_val } context 'with a gem without a license' do include_examples 'a valid hash_val result for gem' diff --git a/spec/cyclonedx/bom_helpers_spec.rb b/spec/cyclonedx/bom_helpers_spec.rb new file mode 100644 index 0000000..6a58128 --- /dev/null +++ b/spec/cyclonedx/bom_helpers_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +RSpec.describe Cyclonedx::BomHelpers do + context '#purl' do + context 'when legacy method is called' do + it 'builds a purl' do + skip('Deprecated in favor of Cyclonedx::BomHelpers.purl') unless defined?(purl) + expect(purl('activesupport', '7.0.1')).to eq('pkg:gem/activesupport@7.0.1') + end + end + + it 'builds a purl' do + expect(described_class.purl('activesupport', '7.0.1')).to eq('pkg:gem/activesupport@7.0.1') + end + end +end diff --git a/spec/cyclonedx/ruby_spec.rb b/spec/cyclonedx/ruby_spec.rb new file mode 100644 index 0000000..b945b9e --- /dev/null +++ b/spec/cyclonedx/ruby_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.describe Cyclonedx::Ruby do + it 'has a version number' do + expect(Cyclonedx::Ruby::VERSION).not_to be nil + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 24b4245..a41b751 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,8 +1,19 @@ -# Copied from https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/spec/spec_helper.rb -# Licensed under MIT - https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/LICENSE +# frozen_string_literal: true -$LOAD_PATH << File.expand_path('../lib', __dir__) +RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = '.rspec_status' + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + config.expect_with :rspec do |c| + c.syntax = :expect + end +end + +# Copied from https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/spec/spec_helper.rb +# Licensed under MIT - https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/LICENSE unless RUBY_PLATFORM.include?('java') require 'simplecov' SimpleCov.command_name 'RSpec' @@ -10,7 +21,10 @@ # Run simplecov by default SimpleCov.start unless ENV.key? 'ARUBA_NO_COVERAGE' end +# End copied from Aruba -# Loading support files -Dir.glob(File.expand_path('support/*.rb', __dir__)).sort.each { |f| require_relative f } -Dir.glob(File.expand_path('support/**/*.rb', __dir__)).sort.each { |f| require_relative f } +mimic_next_major = ENV.fetch('MIMIC_NEXT_MAJOR_VERSION', 'false') +# Require via legacy path until v2.0.0, and unless testing functionality in preparation for next major release +require 'bom_builder' if mimic_next_major.casecmp?('false') +# Modern path is already covered by the legacy path, but doesn't hurt to include it twice +require 'cyclonedx/ruby'