├── .gitignore ├── .rubocop.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── console ├── recent_ruby └── setup ├── features ├── fixtures │ └── versions.json ├── recent_ruby.feature ├── step_definitions │ └── recent_ruby_steps.rb └── support │ └── env.rb ├── lib ├── recent_ruby.rb └── recent_ruby │ ├── version.rb │ └── xml_ast.rb └── recent_ruby.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | ## Specific to RubyMotion: 17 | .dat* 18 | .repl_history 19 | build/ 20 | *.bridgesupport 21 | build-iPhoneOS/ 22 | build-iPhoneSimulator/ 23 | 24 | ## Specific to RubyMotion (use of CocoaPods): 25 | # 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 29 | # 30 | # vendor/Pods/ 31 | 32 | ## Documentation cache and generated files: 33 | /.yardoc/ 34 | /_yardoc/ 35 | /doc/ 36 | /rdoc/ 37 | 38 | ## Environment normalization: 39 | /.bundle/ 40 | /vendor/bundle 41 | /lib/bundler/man/ 42 | 43 | # for a library or gem, you might want to ignore these files since the code is 44 | # intended to run in multiple environments; otherwise, check them in: 45 | # Gemfile.lock 46 | # .ruby-version 47 | # .ruby-gemset 48 | 49 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 50 | .rvmrc 51 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.3 -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in recent_ruby.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | recent_ruby (0.1.5) 5 | methadone (~> 1.9.5) 6 | parser 7 | rexml 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | activesupport (7.0.4.3) 13 | concurrent-ruby (~> 1.0, >= 1.0.2) 14 | i18n (>= 1.6, < 2) 15 | minitest (>= 5.1) 16 | tzinfo (~> 2.0) 17 | aruba (1.1.0) 18 | childprocess (>= 2.0, < 5.0) 19 | contracts (~> 0.16.0) 20 | cucumber (>= 2.4, < 7.0) 21 | rspec-expectations (~> 3.4) 22 | thor (~> 1.0) 23 | ast (2.4.3) 24 | builder (3.2.4) 25 | childprocess (4.0.0) 26 | coderay (1.1.3) 27 | concurrent-ruby (1.2.2) 28 | contracts (0.16.1) 29 | cucumber (6.0.0) 30 | builder (~> 3.2, >= 3.2.4) 31 | cucumber-core (~> 9.0, >= 9.0.0) 32 | cucumber-create-meta (~> 4.0, >= 4.0.0) 33 | cucumber-cucumber-expressions (~> 12.1, >= 12.1.1) 34 | cucumber-gherkin (~> 18.1, >= 18.1.0) 35 | cucumber-html-formatter (~> 13.0, >= 13.0.0) 36 | cucumber-messages (~> 15.0, >= 15.0.0) 37 | cucumber-wire (~> 5.0, >= 5.0.0) 38 | diff-lcs (~> 1.4, >= 1.4.4) 39 | mime-types (~> 3.3, >= 3.3.1) 40 | multi_test (~> 0.1, >= 0.1.2) 41 | sys-uname (~> 1.2, >= 1.2.2) 42 | cucumber-core (9.0.0) 43 | cucumber-gherkin (~> 18.1, >= 18.1.0) 44 | cucumber-messages (~> 15.0, >= 15.0.0) 45 | cucumber-tag-expressions (~> 3.0, >= 3.0.1) 46 | cucumber-create-meta (4.0.0) 47 | cucumber-messages (~> 15.0, >= 15.0.0) 48 | sys-uname (~> 1.2, >= 1.2.2) 49 | cucumber-cucumber-expressions (12.1.1) 50 | cucumber-gherkin (18.1.1) 51 | cucumber-messages (~> 15.0, >= 15.0.0) 52 | cucumber-html-formatter (13.0.0) 53 | cucumber-messages (~> 15.0, >= 15.0.0) 54 | cucumber-messages (15.0.0) 55 | protobuf-cucumber (~> 3.10, >= 3.10.8) 56 | cucumber-tag-expressions (3.0.1) 57 | cucumber-wire (5.0.0) 58 | cucumber-core (~> 9.0, >= 9.0.0) 59 | cucumber-cucumber-expressions (~> 12.1, >= 12.1.1) 60 | cucumber-messages (~> 15.0, >= 15.0.0) 61 | diff-lcs (1.4.4) 62 | ffi (1.15.0) 63 | i18n (1.12.0) 64 | concurrent-ruby (~> 1.0) 65 | methadone (1.9.5) 66 | bundler 67 | method_source (1.0.0) 68 | middleware (0.1.0) 69 | mime-types (3.3.1) 70 | mime-types-data (~> 3.2015) 71 | mime-types-data (3.2021.0225) 72 | minitest (5.18.0) 73 | multi_test (0.1.2) 74 | parser (3.3.8.0) 75 | ast (~> 2.4.1) 76 | racc 77 | power_assert (2.0.0) 78 | protobuf-cucumber (3.10.8) 79 | activesupport (>= 3.2) 80 | middleware 81 | thor 82 | thread_safe 83 | pry (0.14.1) 84 | coderay (~> 1.1) 85 | method_source (~> 1.0) 86 | racc (1.8.1) 87 | rake (13.0.3) 88 | rdoc (6.3.4.1) 89 | rexml (3.4.1) 90 | rspec-expectations (3.10.1) 91 | diff-lcs (>= 1.2.0, < 2.0) 92 | rspec-support (~> 3.10.0) 93 | rspec-support (3.10.2) 94 | sys-uname (1.2.2) 95 | ffi (~> 1.1) 96 | test-unit (3.4.1) 97 | power_assert 98 | thor (1.1.0) 99 | thread_safe (0.3.6) 100 | tzinfo (2.0.6) 101 | concurrent-ruby (~> 1.0) 102 | webrick (1.9.1) 103 | 104 | PLATFORMS 105 | ruby 106 | 107 | DEPENDENCIES 108 | aruba 109 | bundler (>= 2.1.0) 110 | pry 111 | rake (~> 13.0) 112 | rdoc 113 | recent_ruby! 114 | test-unit 115 | webrick 116 | 117 | BUNDLED WITH 118 | 2.3.24 119 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Lucas Luitjes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Recent Ruby 2 | 3 | This script takes a Ruby version number, compares it to all available Ruby versions, and throws an error unless the supplied version number contains the latest security patches. Put it in your build pipeline and you'll never deploy an app to an unpatched Ruby again. 4 | 5 | ## Why 6 | 7 | Heroku (and other platforms) use the Gemfile to determine which version of Ruby to use. This means, whenever a Ruby vulnerability is found, you need to update your Gemfile in order to be safe. More importantly it means you need to pay close attention to the Ruby security notices. On smaller teams, this is often overlooked. 8 | 9 | For gems, you can use Brakeman or Hakiri to stay up-to-date with security patches. For your Ruby version, use recent_ruby. 10 | 11 | ## Installation 12 | 13 | Recent Ruby's installation is pretty standard: 14 | 15 | ``` 16 | $ gem install recent_ruby 17 | ``` 18 | 19 | Or just put it in your Gemfile: 20 | 21 | ``` 22 | gem 'recent_ruby', require: false 23 | ``` 24 | 25 | ## Usage 26 | 27 | Just add Recent Ruby in your CI/CD build process, wherever you would put Rubocop or Brakeman. Recent Ruby can check either your Gemfile (`recent_ruby --gemfile Gemfile`), or whatever is supplied as a command line argument (`recent_ruby --version-string 2.3.5`), and checks if that version of Ruby is the most recent TEENY/PATCH release for that minor version. 28 | 29 | It also makes sure that your minor version is not End-of-Life yet. If your version of Ruby does happen to be out of date and potentially insecure, it exits with status code 1. This means you can simply drop it into your .circle.yml or your Semaphore build step, or wherever you usually put these things. 30 | 31 | ## Examples 32 | 33 | Outdated version number supplied on command line (2.3.7 was the latest 2.3 release at the time of this writing): 34 | 35 | ``` 36 | $ recent_ruby --version-string 2.3.1 37 | Downloading latest list of Rubies from Github... 38 | Comparing version numbers... 39 | Current version is 2.3.1, but the latest patch release for 2.3 is 2.3.7! 40 | ``` 41 | 42 | Latest release for 2.3: 43 | ``` 44 | $ recent_ruby --version-string 2.3.7 45 | Downloading latest list of Rubies from Github... 46 | Comparing version numbers... 47 | Downloading details for 2.3.7... 48 | Checking EOL status... 49 | Ruby version check completed successfully. 50 | ``` 51 | 52 | Latest release for 2.0, which is End-of-Life (no longer getting security patches): 53 | ``` 54 | $ recent_ruby --version-string 2.0.0-p648 55 | Downloading latest list of Rubies from Github... 56 | Comparing version numbers... 57 | Downloading details for 2.0.0-p648... 58 | Checking EOL status... 59 | EOL warning found for 2.0.0-p648! 60 | ``` 61 | 62 | Version number specified in Gemfile: 63 | 64 | ``` 65 | $ cat path/to/Gemfile 66 | source "https://rubygems.org" 67 | 68 | ruby "2.3.3" 69 | 70 | gem "rbnacl-libsodium" 71 | 72 | $ recent_ruby --gemfile path/to/Gemfile 73 | Downloading latest list of Rubies from Github... 74 | Comparing version numbers... 75 | Current version is 2.3.3, but the latest patch release for 2.3 is 2.3.7! 76 | ``` 77 | 78 | Build steps I use in the project settings on SemaphoreCI: 79 | 80 | ``` 81 | # Setup: 82 | gem install recent_ruby --no-rdoc --no-ri 83 | 84 | # Build: 85 | recent_ruby --gemfile Gemfile 86 | ``` 87 | 88 | ## How 89 | 90 | If `--gemfile` was supplied, we use the parser gem to extract the Ruby version and patchlevel from the Gemfile. 91 | 92 | First, we check that we’re being supplied an MRI stable release. If not, we immediately stop and error with exit code 1. Next, we grab the list of releases from the ruby-build repository and do some comparison to make sure we’re on the latest TEENY/PATCH release. Then we download the build specification from the ruby-build repository, and make sure an End-of-Life warning is not present. 93 | 94 | Since the ruby-build repository is well maintained and used in production by many, it’s a reliable source for this purpose. 95 | 96 | ## Contributing 97 | 98 | Feel free to create issues for any problems you may have. Patches are welcome, especially if they come with a Cucumber scenario. 99 | 100 | ### New release 101 | 102 | * Bump version in `lib/recent_ruby/version.rb`, in this example to 0.1.5 103 | * `git commit lib/recent_ruby/version.rb -m "v0.1.5"` 104 | * `git tag -a v0.1.5 -m "Version 0.1.5"` 105 | * `gem build recent_ruby` 106 | * `gem push recent_ruby-0.1.5.gem` 107 | 108 | ## Contributors 109 | 110 | - Lucas Luitjes 111 | - Cedric Hartskeerl 112 | 113 | ## License 114 | 115 | This project is MIT licensed. 116 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | def dump_load_path 2 | puts $LOAD_PATH.join("\n") 3 | found = nil 4 | $LOAD_PATH.each do |path| 5 | next unless File.exist?(File.join(path, 'rspec')) 6 | puts "Found rspec in #{path}" 7 | if File.exist?(File.join(path, 'rspec', 'core')) 8 | puts 'Found core' 9 | if File.exist?(File.join(path, 'rspec', 'core', 'rake_task')) 10 | puts 'Found rake_task' 11 | found = path 12 | else 13 | puts '!! no rake_task' 14 | end 15 | else 16 | puts '!!! no core' 17 | end 18 | end 19 | if found.nil? 20 | puts "Didn't find rspec/core/rake_task anywhere" 21 | else 22 | puts "Found in #{path}" 23 | end 24 | end 25 | require 'bundler' 26 | require 'rake/clean' 27 | 28 | require 'rake/testtask' 29 | 30 | require 'cucumber' 31 | require 'cucumber/rake/task' 32 | gem 'rdoc' # we need the installed RDoc gem, not the system one 33 | require 'rdoc/task' 34 | 35 | include Rake::DSL 36 | 37 | Bundler::GemHelper.install_tasks 38 | 39 | Rake::TestTask.new do |t| 40 | t.pattern = 'test/tc_*.rb' 41 | end 42 | 43 | CUKE_RESULTS = 'results.html'.freeze 44 | CLEAN << CUKE_RESULTS 45 | Cucumber::Rake::Task.new(:features) do |t| 46 | t.cucumber_opts = "features --format html -o #{CUKE_RESULTS} --format pretty --no-source -x" 47 | t.fork = false 48 | end 49 | 50 | Rake::RDocTask.new do |rd| 51 | rd.main = 'README.rdoc' 52 | 53 | rd.rdoc_files.include('README.rdoc', 'lib/**/*.rb', 'bin/**/*') 54 | end 55 | 56 | task default: %i[test features] 57 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'recent_ruby' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require 'irb' 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/recent_ruby: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'optparse' 4 | require 'methadone' 5 | require 'recent_ruby' 6 | require 'net/http' 7 | require 'json' 8 | require 'parser/current' 9 | 10 | class App 11 | include Methadone::Main 12 | include Methadone::CLILogging 13 | extend RecentRuby 14 | 15 | main do 16 | gemfile = options['gemfile'] 17 | version = options['version-string'] 18 | 19 | version_base_url = ENV['VERSION_BASE_URL'] || 'https://raw.githubusercontent.com/rbenv/ruby-build/master/share/ruby-build/' 20 | versions_url = ENV['VERSIONS_URL'] || 'https://api.github.com/repos/rbenv/ruby-build/contents/share/ruby-build' 21 | 22 | validate_args(gemfile, version) 23 | version = parse_gemfile(gemfile) if gemfile 24 | validate_mri_version(version) 25 | minor = version.split('.')[0, 2] 26 | 27 | rubies = get_rubies(versions_url) 28 | latest = latest_minor_version(rubies, minor) 29 | compare_versions(version, latest, minor) 30 | 31 | check_eol(version, version_base_url) 32 | 33 | puts 'Ruby version check completed successfully.' 34 | end 35 | 36 | on('--gemfile PATH', 'Path of Gemfile') 37 | on('--version-string STRING', 'Ruby version string (e.g. 2.3.1)') 38 | 39 | version RecentRuby::VERSION 40 | 41 | use_log_level_option toggle_debug_on_signal: 'USR1' 42 | 43 | go! 44 | end 45 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /features/recent_ruby.feature: -------------------------------------------------------------------------------- 1 | Feature: My bootstrapped app kinda works 2 | Background: 3 | Given I set the environment variables to: 4 | | variable | value | 5 | | VERSION_BASE_URL | http://localhost:8000/version/ | 6 | | VERSIONS_URL | http://localhost:8000/versions | 7 | 8 | And the endpoint "/versions" returns "versions.json" 9 | And the endpoint "/version/1.8.7-p375" returns this content: 10 | """ 11 | require_gcc 12 | install_svn "ruby-1.8.7-p375" "http://svn.ruby-lang.org/repos/ruby/branches/ruby_1_8_7" "44351" warn_eol autoconf auto_tcltk standard 13 | install_package "rubygems-1.6.2" "https://rubygems.org/rubygems/rubygems-1.6.2.tgz#cb5261818b931b5ea2cb54bc1d583c47823543fcf9682f0d6298849091c1cea7" ruby 14 | 15 | """ 16 | 17 | Scenario: Up-to-date Ruby (At the time these fixtures were created) 18 | When I run `recent_ruby --version-string 2.3.7` 19 | Then the exit status should be 0 20 | And the stderr should not contain anything 21 | And the output should contain: 22 | """ 23 | Downloading latest list of Rubies from Github... 24 | Comparing version numbers... 25 | Downloading details for 2.3.7... 26 | Checking EOL status... 27 | Ruby version check completed successfully. 28 | """ 29 | 30 | Scenario: Outdated minor version 31 | When I run `recent_ruby --version-string 2.3.1` 32 | Then the exit status should be 1 33 | And the stderr should not contain anything 34 | And the output should contain: 35 | """ 36 | Downloading latest list of Rubies from Github... 37 | Comparing version numbers... 38 | Current version is 2.3.1, but the latest patch release for 2.3 is 2.3.7! 39 | """ 40 | 41 | Scenario: End of Life minor version 42 | When I run `recent_ruby --version-string 1.8.7-p375` 43 | And the stderr should not contain anything 44 | Then the exit status should be 1 45 | And the output should contain: 46 | """ 47 | Downloading latest list of Rubies from Github... 48 | Comparing version numbers... 49 | Downloading details for 1.8.7-p375... 50 | Checking EOL status... 51 | EOL warning found for 1.8.7-p375! 52 | """ 53 | 54 | Scenario: No arguments 55 | When I run `recent_ruby` 56 | Then the exit status should be 1 57 | And the stderr should not contain anything 58 | And the output should contain: 59 | """ 60 | Please supply either a gemfile path or a version string. Run with -h for more information. 61 | """ 62 | 63 | Scenario: Too many arguments 64 | When I run `recent_ruby --version-string 1.8.7-p375 --gemfile Gemfile` 65 | Then the exit status should be 1 66 | And the stderr should not contain anything 67 | And the output should contain: 68 | """ 69 | Please supply only one argument. Run with -h for more information. 70 | """ 71 | 72 | Scenario: Check version from gemfile 73 | Given a file named "Gemfile" with: 74 | """ 75 | source "https://rubygems.org" 76 | 77 | ruby "2.3.3" 78 | 79 | gem "rbnacl-libsodium" 80 | """ 81 | When I run `recent_ruby --gemfile Gemfile` 82 | And the stderr should not contain anything 83 | And the output should contain: 84 | """ 85 | Downloading latest list of Rubies from Github... 86 | Comparing version numbers... 87 | Current version is 2.3.3, but the latest patch release for 2.3 is 2.3.7! 88 | """ 89 | Then the exit status should be 1 90 | 91 | Scenario: Check version from gemfile 92 | Given a file named "Gemfile" with: 93 | """ 94 | source "https://rubygems.org" 95 | 96 | ruby "1.8.7", :patchlevel => 375 97 | 98 | gem "rbnacl-libsodium" 99 | """ 100 | When I run `recent_ruby --gemfile Gemfile` 101 | And the stderr should not contain anything 102 | And the output should contain: 103 | """ 104 | Downloading latest list of Rubies from Github... 105 | Comparing version numbers... 106 | Downloading details for 1.8.7-p375... 107 | Checking EOL status... 108 | EOL warning found for 1.8.7-p375! 109 | """ 110 | Then the exit status should be 1 111 | 112 | Scenario: Check version from gemfile that refers to external file 113 | Given a file named "Gemfile" with: 114 | """ 115 | source "https://rubygems.org" 116 | 117 | ruby file: ".ruby-version" 118 | 119 | gem "rbnacl-libsodium" 120 | """ 121 | And a file named ".ruby-version" with: 122 | """ 123 | 2.3.7 124 | """ 125 | When I run `recent_ruby --gemfile Gemfile` 126 | And the output should contain: 127 | """ 128 | Downloading latest list of Rubies from Github... 129 | Comparing version numbers... 130 | Downloading details for 2.3.7... 131 | Checking EOL status... 132 | Ruby version check completed successfully. 133 | """ 134 | And the stderr should not contain anything 135 | Then the exit status should be 0 136 | 137 | Scenario: Try to check missing version from gemfile 138 | Given a file named "Gemfile" with: 139 | """ 140 | source "https://rubygems.org" 141 | 142 | gem "rbnacl-libsodium" 143 | """ 144 | When I run `recent_ruby --gemfile Gemfile` 145 | Then the exit status should be 1 146 | And the stderr should not contain anything 147 | And the output should contain: 148 | """ 149 | Unable to find ruby version in gemfile. 150 | """ 151 | 152 | Scenario: Only MRI is supported 153 | When I run `recent_ruby --version-string jruby-1.5.6` 154 | Then the exit status should be 1 155 | And the stderr should not contain anything 156 | And the output should contain: 157 | """ 158 | Only stable release MRI version strings are currently supported. (e.g. 2.3.1 or 2.3.1-p12) 159 | """ 160 | 161 | Scenario: What if Github is rate limiting us? 162 | Given Github is rate limiting us 163 | When I run `recent_ruby --version-string 2.3.1` 164 | Then the exit status should be 2 165 | And the stderr should not contain anything 166 | And the output should contain: 167 | """ 168 | Downloading latest list of Rubies from Github... 169 | Error: received HTTP 429 response from Github: 170 | 171 | Please try again in a few moments. 172 | """ 173 | -------------------------------------------------------------------------------- /features/step_definitions/recent_ruby_steps.rb: -------------------------------------------------------------------------------- 1 | Given(/^the endpoint "([^"]*)" returns this content:$/) do |path, input| 2 | RubyMock.resources[path] = input 3 | end 4 | 5 | Then(/^the following request body should have been sent:$/) do |string| 6 | RubyMock.requests.map { |n| JSON.parse(n) }.should include(JSON.parse(string)) 7 | end 8 | 9 | Given('the endpoint {string} returns {string}') do |string, string2| 10 | RubyMock.resources[string] = File.read("features/fixtures/#{string2}") 11 | end 12 | 13 | When('Github is rate limiting us') do 14 | RubyMock.enable_rate_limiting 15 | end 16 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'aruba/cucumber' 2 | require 'methadone/cucumber' 3 | require 'webrick' 4 | 5 | ENV['PATH'] = "#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}#{File::PATH_SEPARATOR}#{ENV['PATH']}" 6 | LIB_DIR = File.join(__dir__, '..', '..', 'lib') 7 | 8 | Before do 9 | # Using "announce" causes massive warnings on 1.9.2 10 | @puts = true 11 | @original_rubylib = ENV['RUBYLIB'] 12 | ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s 13 | end 14 | 15 | After do 16 | ENV['RUBYLIB'] = @original_rubylib 17 | end 18 | 19 | AfterConfiguration do 20 | RubyMock.start 21 | end 22 | 23 | After do 24 | ENV['RUBYLIB'] = @original_rubylib 25 | RubyMock.clear 26 | end 27 | 28 | Aruba.configure do |config| 29 | config.home_directory = '.' 30 | end 31 | 32 | class RubyMock 33 | class << self; attr_accessor :resources end 34 | class << self; attr_accessor :requests end 35 | class << self; attr_accessor :rate_limiting end 36 | @resources = {} 37 | @requests = [] 38 | @rate_limiting = false 39 | 40 | def self.clear 41 | @requests = [] 42 | @rate_limiting = false 43 | @resources = {} 44 | end 45 | 46 | def self.enable_rate_limiting 47 | @rate_limiting = true 48 | end 49 | 50 | def self.start 51 | server = WEBrick::HTTPServer.new(Port: 8000, AccessLog: [], Logger: WEBrick::Log.new('/dev/null', 7)) 52 | server.mount_proc '/' do |req, res| 53 | @requests << req.body 54 | if @rate_limiting 55 | res.status = 429 56 | res.body = 'Please try again in a few moments.' 57 | else 58 | res.body = @resources[req.path] 59 | end 60 | end 61 | Thread.new do 62 | server.start 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/recent_ruby.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'recent_ruby/version' 4 | require 'recent_ruby/xml_ast' 5 | 6 | module RecentRuby 7 | RUBY_NODE_XPATH = "//send[symbol-val[@value='ruby']]" 8 | VERSION_XPATH = "#{RUBY_NODE_XPATH}/str/string-val/@value" 9 | 10 | FILE_XPATH = "#{RUBY_NODE_XPATH}/hash/pair[sym[symbol-val[@value='file']]]" 11 | FILE_VALUE_XPATH = "#{FILE_XPATH}/str/string-val/@value" 12 | 13 | PATCHLEVEL_XPATH = "#{RUBY_NODE_XPATH}/hash/pair[sym[symbol-val[@value='patchlevel']]]" 14 | PATCHLEVEL_VALUE_XPATH = "#{PATCHLEVEL_XPATH}/int/integer-val/@value" 15 | 16 | def http_get(url) 17 | uri = URI(url) 18 | Net::HTTP.start(uri.host, uri.port, 19 | use_ssl: uri.scheme == 'https') do |http| 20 | request = Net::HTTP::Get.new uri 21 | response = http.request request 22 | if response.code != '200' 23 | puts "Error: received HTTP #{response.code} response from Github:\n\n#{response.body}" 24 | exit(2) 25 | end 26 | response.body 27 | end 28 | end 29 | 30 | def validate_args(gemfile, version) 31 | if gemfile && version 32 | puts 'Please supply only one argument. Run with -h for more information.' 33 | exit(1) 34 | elsif !gemfile && !version 35 | puts 'Please supply either a gemfile path or a version string. Run with -h for more information.' 36 | exit(1) 37 | end 38 | end 39 | 40 | def parse_gemfile(gemfile) 41 | ast = parser_for_current_ruby.parse(File.read(gemfile)) 42 | xml = RecentRuby::XMLAST.new(ast) 43 | 44 | file = xml.xpath(FILE_VALUE_XPATH)&.first&.value 45 | version = xml.xpath(VERSION_XPATH)&.first&.value 46 | version = File.read(file).strip if !version && file 47 | 48 | unless version 49 | puts 'Unable to find ruby version in gemfile.' 50 | exit(1) 51 | end 52 | 53 | patchlevel = xml.xpath(PATCHLEVEL_VALUE_XPATH)&.first&.value 54 | version += "-p#{patchlevel}" if patchlevel 55 | version 56 | end 57 | 58 | def validate_mri_version(version) 59 | return if version =~ /^(\d+\.\d+\.\d+(-p\d+)?)$/ 60 | puts 'Only stable release MRI version strings are currently supported. (e.g. 2.3.1 or 2.3.1-p12)' 61 | exit(1) 62 | end 63 | 64 | def get_rubies(versions_url) 65 | puts 'Downloading latest list of Rubies from Github...' 66 | JSON.parse(http_get(versions_url)) 67 | end 68 | 69 | def latest_minor_version(rubies, minor) 70 | minor_rubies = rubies.map { |n| n['name'] }.select do |n| 71 | n =~ /^\d+\.\d+\.\d+(-p\d+)?$/ && 72 | n.split('.')[0, 2] == minor 73 | end 74 | 75 | minor_rubies.max_by do |ruby| 76 | a, b, c, d = *ruby.sub('-p', '.').split('.').map(&:to_i) 77 | [a, b, c, d || -1] 78 | end 79 | end 80 | 81 | def check_eol(version, version_base_url) 82 | puts "Downloading details for #{version}..." 83 | details = http_get("#{version_base_url}#{version}") 84 | puts 'Checking EOL status...' 85 | 86 | return unless details =~ / warn_eol / 87 | puts "EOL warning found for #{version}!" 88 | exit 1 89 | end 90 | 91 | def compare_versions(version, latest, minor) 92 | puts 'Comparing version numbers...' 93 | return if version == latest 94 | puts "Current version is #{version}, but the latest patch release for #{minor.join('.')} is #{latest}!" 95 | exit 1 96 | end 97 | 98 | # https://github.com/whitequark/parser/blob/master/doc/PRISM_TRANSLATION.md 99 | def parser_for_current_ruby 100 | code_version = RUBY_VERSION.to_f 101 | 102 | if code_version <= 3.3 103 | require 'parser/current' 104 | Parser::CurrentRuby 105 | else 106 | require 'prism' 107 | case code_version 108 | when 3.3 109 | Prism::Translation::Parser33 110 | when 3.4 111 | Prism::Translation::Parser34 112 | else 113 | warn "Unknown Ruby version #{code_version}, using 3.4 as a fallback" 114 | Prism::Translation::Parser34 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/recent_ruby/version.rb: -------------------------------------------------------------------------------- 1 | module RecentRuby 2 | VERSION = '0.1.6'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/recent_ruby/xml_ast.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rexml/document' 4 | 5 | module RecentRuby 6 | # Class comes from: https://blog.luitjes.it/posts/using-xpath-to-rewrite-ruby-code-with-ease/ 7 | # TODO: turn into a separate gem 8 | 9 | class XMLAST 10 | include REXML 11 | attr_reader :doc 12 | 13 | def initialize(sexp) 14 | @doc = Document.new '' 15 | @sexp = sexp 16 | root = @doc.root 17 | populate_tree(root, sexp) 18 | end 19 | 20 | def populate_tree(xml, sexp) 21 | if sexp.is_a?(String) || 22 | sexp.is_a?(Symbol) || 23 | sexp.is_a?(Numeric) || 24 | sexp.is_a?(NilClass) 25 | el = Element.new(sexp.class.to_s.downcase + '-val') 26 | el.add_attribute 'value', sexp.to_s 27 | xml.add_element el 28 | else 29 | el = Element.new(sexp.type.to_s) 30 | el.add_attribute('id', sexp.object_id) 31 | 32 | sexp.children.each { |n| populate_tree(el, n) } 33 | xml.add_element el 34 | end 35 | end 36 | 37 | def treewalk(sexp = @sexp) 38 | return sexp unless sexp&.respond_to?(:children) 39 | [sexp, sexp.children.map { |n| treewalk(n) }].flatten 40 | end 41 | 42 | def xpath(path) 43 | results = XPath.match(doc, path) 44 | results.map do |n| 45 | if n.respond_to?(:attributes) && n.attributes['id'] 46 | treewalk.find do |m| 47 | m.object_id.to_s == n.attributes['id'] 48 | end 49 | else 50 | n 51 | end 52 | end 53 | end 54 | 55 | def pp 56 | doc.write(STDOUT, 2) 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /recent_ruby.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "recent_ruby/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "recent_ruby" 8 | spec.license = "MIT" 9 | spec.version = RecentRuby::VERSION 10 | spec.authors = ["Lucas Luitjes"] 11 | spec.email = ["lucas@luitjes.it"] 12 | 13 | spec.summary = %q{CLI tool for your CI/CD to make sure a recent and secure ruby version is used.} 14 | spec.homepage = "https://github.com/lucasluitjes/recent_ruby" 15 | 16 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 17 | # to allow pushing to a single host or delete this section to allow pushing to any host. 18 | if spec.respond_to?(:metadata) 19 | spec.metadata["allowed_push_host"] = "https://rubygems.org" 20 | else 21 | raise "RubyGems 2.0 or newer is required to protect against " \ 22 | "public gem pushes." 23 | end 24 | 25 | # Specify which files should be added to the gem when it is released. 26 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 27 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 28 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 29 | end 30 | spec.bindir = "bin" 31 | spec.executables = "recent_ruby" 32 | spec.require_paths = ["lib"] 33 | 34 | spec.add_development_dependency "bundler", ">= 2.1.0" 35 | spec.add_development_dependency "rake", "~> 13.0" 36 | spec.add_development_dependency('rdoc') 37 | spec.add_development_dependency('pry') 38 | spec.add_development_dependency('aruba') 39 | spec.add_development_dependency('webrick') 40 | spec.add_dependency('methadone', '~> 1.9.5') 41 | spec.add_dependency('parser') 42 | spec.add_dependency('rexml') 43 | spec.add_development_dependency('test-unit') 44 | end 45 | --------------------------------------------------------------------------------