diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 4c81c8c2..14103d40 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -41,7 +41,7 @@ jobs: uses: ruby/setup-ruby@v1 - uses: actions/setup-node@v4 with: - node-version: '14' + node-version: '16' - name: Cache ruby gems uses: actions/cache@v4 with: diff --git a/.overcommit.yml b/.overcommit.yml new file mode 100644 index 00000000..9c65d5e0 --- /dev/null +++ b/.overcommit.yml @@ -0,0 +1,159 @@ +# Overcommit configuration for Rails projects +# https://github.com/sds/overcommit + +# Don't use Bundler context since overcommit is installed globally +gemfile: false + +# Hooks that run during `git commit` +CommitMsg: + # Enforce proper commit message format + CapitalizedSubject: + enabled: true + description: 'Check subject capitalization' + EmptyMessage: + enabled: true + description: 'Check for empty commit message' + TextWidth: + enabled: true + description: 'Check text width' + max_subject_width: 72 + max_body_width: 80 + TrailingPeriod: + enabled: true + description: 'Check for trailing periods in subject' + SingleLineSubject: + enabled: true + description: 'Check subject is single line' + +# Hooks that run before `git commit` +PreCommit: + # Ruby/Rails specific hooks + RuboCop: + enabled: true + description: 'Analyze Ruby code with RuboCop' + required_executable: 'bundle' + command: ['bundle', 'exec', 'rubocop'] + flags: ['--autocorrect-all', '--display-cop-names'] + on_warn: fail # Treat warnings as failures + problem_on_unmodified_line: report + include: + - '**/*.rb' + - '**/*.rake' + - '**/Gemfile' + - '**/Rakefile' + + RailsBestPractices: + enabled: false # Enable when ready + description: 'Analyze with rails_best_practices' + required_executable: 'bundle' + command: ['bundle', 'exec', 'rails_best_practices'] + + RailsSchemaUpToDate: + enabled: true + description: 'Check if db/schema.rb matches migrations' + + BundleCheck: + enabled: true + description: 'Check Gemfile dependencies' + + # JavaScript/Vue specific hooks + EsLint: + enabled: true + description: 'Analyze JavaScript/Vue with ESLint' + required_executable: 'yarn' + command: ['yarn', 'lint'] + include: + - '**/*.js' + - '**/*.vue' + + # Shell script hooks + ShellCheck: + enabled: false + description: 'Analyze shell scripts with ShellCheck' + include: + - '**/*.sh' + - '**/bin/*' + exclude: + - '**/bin/*.rb' + - '**/bin/bundle' + - '**/bin/rails' + - '**/bin/rake' + - '**/bin/spring' + - '**/bin/webpack' + - '**/bin/webpack-dev-server' + - '**/bin/yarn' + + # General hooks + TrailingWhitespace: + enabled: true + exclude: + - '**/db/schema.rb' + - '**/db/structure.sql' + - '**/*.md' + + MergeConflicts: + enabled: true + + YamlSyntax: + enabled: true + include: + - '**/*.yml' + - '**/*.yaml' + + JsonSyntax: + enabled: true + include: + - '**/*.json' + + HardTabs: + enabled: true + exclude: + - '**/Makefile' + - '**/*.mk' + + # Security scanning (disabled by default for speed) + Brakeman: + enabled: false + description: 'Security scan with Brakeman' + command: ['bundle', 'exec', 'brakeman', '--quiet', '--summary'] + + BundleAudit: + enabled: false + description: 'Check for vulnerable gem versions' + +# Hooks that run after `git checkout` +PostCheckout: + BundleInstall: + enabled: true + description: 'Install bundle dependencies' + + YarnInstall: + enabled: true + description: 'Install yarn dependencies' + required_executable: 'yarn' + command: ['yarn', 'install'] + + ActiveRecordMigrations: + enabled: true + description: 'Run pending migrations' + +# Hooks that run after `git merge` +PostMerge: + BundleInstall: + enabled: true + + YarnInstall: + enabled: true + required_executable: 'yarn' + command: ['yarn', 'install'] + + ActiveRecordMigrations: + enabled: true + +# Hooks that run after `git rewrite` +PostRewrite: + BundleInstall: + enabled: true + + YarnInstall: + enabled: true \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml index 2871b11c..141c5381 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,4 +1,4 @@ -require: +plugins: - rubocop-rails - rubocop-performance diff --git a/.ruby-version b/.ruby-version index a603bb50..818bd47a 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.5 +3.0.6 diff --git a/.tool-versions b/.tool-versions index a4023dc7..a994f29a 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1,2 @@ -ruby 2.7.5 +ruby 3.0.6 +nodejs 16.20.2 diff --git a/Dockerfile b/Dockerfile index 1d3c2d6c..73c64b44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.7 +FROM ruby:3.0.7 RUN curl -sS https://deb.nodesource.com/setup_16.x | bash - RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - @@ -10,7 +10,7 @@ ENV RAILS_ENV=production RUN mkdir $APP_HOME WORKDIR $APP_HOME -RUN gem install bundler:2.2.32 +RUN gem install bundler:2.3.10 ADD Gemfile* $APP_HOME/ RUN bundle install --without development test diff --git a/Gemfile b/Gemfile index 9b621d23..025152b6 100644 --- a/Gemfile +++ b/Gemfile @@ -2,10 +2,12 @@ source 'https://rubygems.org' -ruby '~> 2.7' +ruby File.read('.ruby-version').strip # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '~> 6.1.4' +gem 'rails', '~> 7.0.0' +# TODO: Remove this once upgrading to Rails 7.1, this is required for a bug specific to Rails 7.0 +gem 'concurrent-ruby', '1.3.4' # Use postgresql as the database for Active Record gem 'pg', '>= 0.18', '< 2.0' # Use Puma as the app server @@ -45,7 +47,7 @@ gem 'settingslogic', '~> 2.0.9' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.4.2', require: false -gem 'audited', '~> 5.3.3' +gem 'audited', '~> 5.8.0' gem 'activerecord-import' @@ -61,6 +63,8 @@ gem 'fast_excel' # For writing excel files gem 'ruh-roo', '~> 3.0.0', require: 'roo' +gem 'rexml' + gem 'ox' gem 'rubyzip' @@ -69,37 +73,37 @@ gem 'mitre-inspec-objects' gem 'rest-client' group :development do - gem 'listen', '~> 3.1.5' + gem 'listen' # Access an interactive console on exception pages or by calling 'console' anywhere in the code. gem 'web-console', '>= 3.3.0' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'letter_opener' gem 'spring' - gem 'spring-watcher-listen', '~> 2.0.0' + gem 'spring-watcher-listen' # Process manager for Procfile-based applications (development only) gem 'foreman' + # Git hook manager for automated linting and formatting + gem 'overcommit', '~> 0.60', require: false end group :test do # Adds support for Capybara system testing and selenium driver - gem 'capybara', '>= 2.15' - gem 'selenium-webdriver' - # Easy installation and use of web drivers to run system tests with browsers - # gem 'webdrivers' - + gem 'capybara' gem 'database_cleaner-active_record' gem 'rubocop', require: false gem 'rubocop-performance' gem 'rubocop-rails' + gem 'selenium-webdriver' gem 'simplecov', require: false end group :development, :test do gem 'brakeman' gem 'byebug' - gem 'factory_bot_rails', '~> 5.2.0' + gem 'factory_bot_rails', '~> 6.4.0' + gem 'pry' gem 'rspec-mocks' - gem 'rspec-rails', '~> 4.0.0' + gem 'rspec-rails', '~> 6.0.0' # Load environment variables from .env files in development and test gem 'dotenv-rails' end diff --git a/Gemfile.lock b/Gemfile.lock index 8cddb079..2b151ca9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,145 +1,152 @@ GEM remote: https://rubygems.org/ specs: - actioncable (6.1.4.6) - actionpack (= 6.1.4.6) - activesupport (= 6.1.4.6) + actioncable (7.0.8.7) + actionpack (= 7.0.8.7) + activesupport (= 7.0.8.7) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.6) - actionpack (= 6.1.4.6) - activejob (= 6.1.4.6) - activerecord (= 6.1.4.6) - activestorage (= 6.1.4.6) - activesupport (= 6.1.4.6) + actionmailbox (7.0.8.7) + actionpack (= 7.0.8.7) + activejob (= 7.0.8.7) + activerecord (= 7.0.8.7) + activestorage (= 7.0.8.7) + activesupport (= 7.0.8.7) mail (>= 2.7.1) - actionmailer (6.1.4.6) - actionpack (= 6.1.4.6) - actionview (= 6.1.4.6) - activejob (= 6.1.4.6) - activesupport (= 6.1.4.6) + net-imap + net-pop + net-smtp + actionmailer (7.0.8.7) + actionpack (= 7.0.8.7) + actionview (= 7.0.8.7) + activejob (= 7.0.8.7) + activesupport (= 7.0.8.7) mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp rails-dom-testing (~> 2.0) - actionpack (6.1.4.6) - actionview (= 6.1.4.6) - activesupport (= 6.1.4.6) - rack (~> 2.0, >= 2.0.9) + actionpack (7.0.8.7) + actionview (= 7.0.8.7) + activesupport (= 7.0.8.7) + rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.4.6) - actionpack (= 6.1.4.6) - activerecord (= 6.1.4.6) - activestorage (= 6.1.4.6) - activesupport (= 6.1.4.6) + actiontext (7.0.8.7) + actionpack (= 7.0.8.7) + activerecord (= 7.0.8.7) + activestorage (= 7.0.8.7) + activesupport (= 7.0.8.7) + globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (6.1.4.6) - activesupport (= 6.1.4.6) + actionview (7.0.8.7) + activesupport (= 7.0.8.7) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.4.6) - activesupport (= 6.1.4.6) + activejob (7.0.8.7) + activesupport (= 7.0.8.7) globalid (>= 0.3.6) - activemodel (6.1.4.6) - activesupport (= 6.1.4.6) - activerecord (6.1.4.6) - activemodel (= 6.1.4.6) - activesupport (= 6.1.4.6) - activerecord-import (1.3.0) + activemodel (7.0.8.7) + activesupport (= 7.0.8.7) + activerecord (7.0.8.7) + activemodel (= 7.0.8.7) + activesupport (= 7.0.8.7) + activerecord-import (2.2.0) activerecord (>= 4.2) - activestorage (6.1.4.6) - actionpack (= 6.1.4.6) - activejob (= 6.1.4.6) - activerecord (= 6.1.4.6) - activesupport (= 6.1.4.6) - marcel (~> 1.0.0) + activestorage (7.0.8.7) + actionpack (= 7.0.8.7) + activejob (= 7.0.8.7) + activerecord (= 7.0.8.7) + activesupport (= 7.0.8.7) + marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.4.6) + activesupport (7.0.8.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - zeitwerk (~> 2.3) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) aes_key_wrap (1.1.0) - amoeba (3.2.0) - activerecord (>= 4.2.0) - ast (2.4.2) - attr_required (1.0.1) - audited (5.3.3) - activerecord (>= 5.0, < 7.1) - request_store (~> 1.2) - bcrypt (3.1.16) - bindata (2.4.15) + amoeba (3.3.0) + activerecord (>= 5.2.0) + ast (2.4.3) + attr_required (1.0.2) + audited (5.8.0) + activerecord (>= 5.2, < 8.2) + activesupport (>= 5.2, < 8.2) + base64 (0.3.0) + bcrypt (3.1.20) + bigdecimal (3.2.2) + bindata (2.5.1) bindex (0.8.1) - bootsnap (1.10.3) + bootsnap (1.18.6) msgpack (~> 1.2) - brakeman (5.2.1) - builder (3.2.4) + brakeman (6.2.2) + racc + builder (3.3.0) byebug (11.1.3) - capybara (3.36.0) + capybara (3.40.0) addressable matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.8) + nokogiri (~> 1.11) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - chef-config (17.9.46) + chef-config (13.6.4) addressable - chef-utils (= 17.9.46) fuzzyurl - mixlib-config (>= 2.2.12, < 4.0) - mixlib-shellout (>= 2.0, < 4.0) - tomlrb (~> 1.2) + mixlib-config (~> 2.0) + mixlib-shellout (~> 2.0) chef-telemetry (1.1.1) chef-config concurrent-ruby (~> 1.0) - chef-utils (17.9.46) - concurrent-ruby + childprocess (5.1.0) + logger (~> 1.5) coderay (1.1.3) - concurrent-ruby (1.2.2) + concurrent-ruby (1.3.4) crass (1.0.6) - database_cleaner-active_record (2.0.1) + database_cleaner-active_record (2.2.1) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - devise (4.8.1) + date (3.4.1) + devise (4.9.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - diff-lcs (1.5.0) - docile (1.4.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) - dotenv (2.8.1) - dotenv-rails (2.8.1) - dotenv (= 2.8.1) - railties (>= 3.2) - erubi (1.12.0) - erubis (2.7.0) - factory_bot (5.2.0) - activesupport (>= 4.2.0) - factory_bot_rails (5.2.0) - factory_bot (~> 5.2.0) - railties (>= 4.2.0) + diff-lcs (1.6.2) + docile (1.4.1) + domain_name (0.6.20240107) + dotenv (3.1.8) + dotenv-rails (3.1.8) + dotenv (= 3.1.8) + railties (>= 6.1) + erubi (1.13.1) + factory_bot (6.5.3) + activesupport (>= 6.1.0) + factory_bot_rails (6.4.4) + factory_bot (~> 6.5) + railties (>= 5.0.0) faraday (1.3.1) faraday-net_http (~> 1.0) multipart-post (>= 1.2, < 3) ruby2_keywords (>= 0.0.4) - faraday-net_http (1.0.1) - faraday_middleware (1.2.0) + faraday-net_http (1.0.2) + faraday_middleware (1.2.1) faraday (~> 1.0) - fast_excel (0.4.0) + fast_excel (0.5.0) ffi (> 1.9, < 2) - ffaker (2.20.0) - ffi (1.15.5) + ffaker (2.24.0) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86_64-darwin) foreman (0.88.1) fuzzyurl (0.9.0) gitlab_omniauth-ldap (2.2.0) @@ -147,31 +154,29 @@ GEM omniauth (>= 1.3, < 3) pyu-ruby-sasl (>= 0.0.3.3, < 0.1) rubyntlm (~> 0.5) - gli (2.21.0) - globalid (1.0.1) - activesupport (>= 5.0) - haml (5.2.2) - temple (>= 0.8.0) + gli (2.22.2) + ostruct + globalid (1.2.1) + activesupport (>= 6.1) + haml (6.3.0) + temple (>= 0.8.2) + thor tilt - haml-rails (2.0.1) + haml-rails (2.1.0) actionpack (>= 5.1) activesupport (>= 5.1) - haml (>= 4.0.6, < 6.0) - html2haml (>= 1.0.1) + haml (>= 4.0.6) railties (>= 5.1) hashie (4.1.0) - highline (2.0.3) - html2haml (2.2.0) - erubis (~> 2.7.0) - haml (>= 4.0, < 6) - nokogiri (>= 1.6.0) - ruby_parser (~> 3.5) + highline (2.1.0) http-accept (1.7.0) - http-cookie (1.0.5) + http-cookie (1.0.8) domain_name (~> 0.5) - httpclient (2.8.3) - i18n (1.13.0) + httpclient (2.9.0) + mutex_m + i18n (1.14.7) concurrent-ruby (~> 1.0) + iniparse (1.5.0) inspec-core (4.24.32) addressable (~> 2.4) chef-telemetry (~> 1.0) @@ -194,90 +199,101 @@ GEM train-core (~> 3.0) tty-prompt (~> 0.17) tty-table (~> 0.10) - jbuilder (2.11.5) + jbuilder (2.13.0) actionview (>= 5.0.0) activesupport (>= 5.0.0) - json (2.6.1) - json-jwt (1.15.3) + json (2.12.2) + json-jwt (1.15.3.1) activesupport (>= 4.2) aes_key_wrap bindata httpclient - jwt (2.3.0) - launchy (2.5.0) - addressable (~> 2.7) - letter_opener (1.7.0) - launchy (~> 2.2) + jwt (2.10.1) + base64 + language_server-protocol (3.17.0.5) + launchy (3.1.1) + addressable (~> 2.8) + childprocess (~> 5.0) + logger (~> 1.6) + letter_opener (1.10.0) + launchy (>= 2.2, < 4) license-acceptance (2.1.13) pastel (~> 0.7) tomlrb (>= 1.2, < 3.0) tty-box (~> 0.6) tty-prompt (~> 0.20) - listen (3.1.5) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - ruby_dep (~> 1.2) - loofah (2.19.1) + lint_roller (1.1.0) + listen (3.9.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + logger (1.7.0) + loofah (2.24.1) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) + nokogiri (>= 1.12.0) + mail (2.8.1) mini_mime (>= 0.1.1) - marcel (1.0.2) + net-imap + net-pop + net-smtp + marcel (1.0.4) matrix (0.4.2) - method_source (1.0.0) - mime-types (3.4.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2023.0218.1) - mini_mime (1.1.2) - mini_portile2 (2.8.1) - minitest (5.18.0) + method_source (1.1.0) + mime-types (3.7.0) + logger + mime-types-data (~> 3.2025, >= 3.2025.0507) + mime-types-data (3.2025.0603) + mini_mime (1.1.5) + minitest (5.25.5) mitre-inspec-objects (0.3.3) inspec-core - mixlib-config (3.0.9) + mixlib-config (2.2.18) tomlrb mixlib-log (3.0.9) - mixlib-shellout (3.2.5) - chef-utils - msgpack (1.4.5) - multi_json (1.15.0) + mixlib-shellout (2.4.4) + msgpack (1.8.0) multi_xml (0.6.0) - multipart-post (2.1.1) - net-ldap (0.17.1) - net-protocol (0.2.1) + multipart-post (2.4.1) + mutex_m (0.3.0) + net-imap (0.4.22) + date + net-protocol + net-ldap (0.19.0) + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) timeout - net-scp (3.0.0) - net-ssh (>= 2.6.5, < 7.0.0) - net-smtp (0.3.3) + net-scp (4.1.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-smtp (0.5.1) net-protocol - net-ssh (6.1.0) + net-ssh (7.3.0) netrc (0.11.0) - nio4r (2.5.9) - nokogiri (1.14.3) - mini_portile2 (~> 2.8.0) - racc (~> 1.4) - nokogiri (1.14.3-x86_64-darwin) + nio4r (2.7.4) + nokogiri (1.17.2-arm64-darwin) racc (~> 1.4) - nokogiri (1.14.3-x86_64-linux) + nokogiri (1.17.2-x86_64-darwin) racc (~> 1.4) - nokogiri-happymapper (0.9.0) + nokogiri-happymapper (0.10.0) nokogiri (~> 1.5) - oauth2 (1.4.7) - faraday (>= 0.8, < 2.0) - jwt (>= 1.0, < 3.0) - multi_json (~> 1.3) + oauth2 (2.0.12) + faraday (>= 0.17.3, < 4.0) + jwt (>= 1.0, < 4.0) + logger (~> 1.2) multi_xml (~> 0.5) - rack (>= 1.2, < 3) - omniauth (2.1.1) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0, >= 2.0.3) + version_gem (>= 1.1.8, < 3) + omniauth (2.1.3) hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection - omniauth-github (2.0.0) + omniauth-github (2.0.1) omniauth (~> 2.0) - omniauth-oauth2 (~> 1.7.1) - omniauth-oauth2 (1.7.2) - oauth2 (~> 1.4) - omniauth (>= 1.9, < 3) - omniauth-rails_csrf_protection (1.0.1) + omniauth-oauth2 (~> 1.8) + omniauth-oauth2 (1.8.0) + oauth2 (>= 1.4, < 3) + omniauth (~> 2.0) + omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) omniauth (~> 2.0) omniauth_openid_connect (0.6.1) @@ -295,143 +311,157 @@ GEM validate_url webfinger (~> 1.2) orm_adapter (0.5.0) - ox (2.14.9) - parallel (1.21.0) - parser (3.1.0.0) + ostruct (0.6.1) + overcommit (0.67.1) + childprocess (>= 0.6.3, < 6) + iniparse (~> 1.4) + rexml (>= 3.3.9) + ox (2.14.23) + bigdecimal (>= 3.0) + parallel (1.27.0) + parser (3.3.8.0) ast (~> 2.4.1) + racc parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.3.2) - pry (0.14.1) + pg (1.5.9) + prism (1.4.0) + pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) - public_suffix (4.0.6) - puma (5.6.7) + public_suffix (6.0.2) + puma (5.6.9) nio4r (~> 2.0) pyu-ruby-sasl (0.0.3.3) - racc (1.6.2) - rack (2.2.6.4) + racc (1.8.1) + rack (2.2.17) rack-oauth2 (1.21.3) activesupport attr_required httpclient json-jwt (>= 1.11.0) rack (>= 2.1.0) - rack-protection (3.0.5) + rack-protection (3.2.0) + base64 (>= 0.1.0) + rack (~> 2.2, >= 2.2.4) + rack-proxy (0.7.7) rack - rack-proxy (0.7.2) - rack - rack-test (2.0.2) + rack-test (2.2.0) rack (>= 1.3) - rails (6.1.4.6) - actioncable (= 6.1.4.6) - actionmailbox (= 6.1.4.6) - actionmailer (= 6.1.4.6) - actionpack (= 6.1.4.6) - actiontext (= 6.1.4.6) - actionview (= 6.1.4.6) - activejob (= 6.1.4.6) - activemodel (= 6.1.4.6) - activerecord (= 6.1.4.6) - activestorage (= 6.1.4.6) - activesupport (= 6.1.4.6) + rails (7.0.8.7) + actioncable (= 7.0.8.7) + actionmailbox (= 7.0.8.7) + actionmailer (= 7.0.8.7) + actionpack (= 7.0.8.7) + actiontext (= 7.0.8.7) + actionview (= 7.0.8.7) + activejob (= 7.0.8.7) + activemodel (= 7.0.8.7) + activerecord (= 7.0.8.7) + activestorage (= 7.0.8.7) + activesupport (= 7.0.8.7) bundler (>= 1.15.0) - railties (= 6.1.4.6) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + railties (= 7.0.8.7) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.5.0) - loofah (~> 2.19, >= 2.19.1) - railties (6.1.4.6) - actionpack (= 6.1.4.6) - activesupport (= 6.1.4.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (7.0.8.7) + actionpack (= 7.0.8.7) + activesupport (= 7.0.8.7) method_source - rake (>= 0.13) + rake (>= 12.2) thor (~> 1.0) + zeitwerk (~> 2.5) rainbow (3.1.1) - rake (13.0.6) - rb-fsevent (0.11.1) - rb-inotify (0.10.1) + rake (13.3.0) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) ffi (~> 1.0) - regexp_parser (2.2.1) - request_store (1.5.1) - rack (>= 1.4) - responders (3.0.1) - actionpack (>= 5.0) - railties (>= 5.0) + regexp_parser (2.10.0) + responders (3.1.1) + actionpack (>= 5.2) + railties (>= 5.2) 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) - rexml (3.2.6) - rspec (3.11.0) - rspec-core (~> 3.11.0) - rspec-expectations (~> 3.11.0) - rspec-mocks (~> 3.11.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.0) + rexml (3.4.1) + rspec (3.13.1) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.4) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-its (1.3.0) + rspec-support (~> 3.13.0) + rspec-its (1.3.1) rspec-core (>= 3.0.0) rspec-expectations (>= 3.0.0) - rspec-mocks (3.11.0) + rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-rails (4.0.2) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) - rspec-support (3.11.0) - rubocop (1.25.1) + rspec-support (~> 3.13.0) + rspec-rails (6.0.4) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) + rspec-support (3.13.4) + rubocop (1.76.0) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) parallel (~> 1.10) - parser (>= 3.1.0.0) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml - rubocop-ast (>= 1.15.1, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.45.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.15.2) - parser (>= 3.0.1.1) - rubocop-performance (1.13.2) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) - rubocop-rails (2.13.2) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.45.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-performance (1.25.0) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rails (2.32.0) activesupport (>= 4.2.0) + lint_roller (~> 1.1) rack (>= 1.1) - rubocop (>= 1.7.0, < 2.0) - ruby-progressbar (1.11.0) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) - ruby_dep (1.5.0) - ruby_parser (3.18.1) - sexp_processor (~> 4.16) - rubyntlm (0.6.3) - rubyzip (2.3.2) + rubyntlm (0.6.5) + base64 + rubyzip (2.4.1) ruh-roo (3.0.1) nokogiri (~> 1) rubyzip (>= 1.3.0, < 3.0.0) - selenium-webdriver (4.9.0) + selenium-webdriver (4.26.0) + base64 (~> 0.2) + logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - semantic_range (3.0.0) - semverse (3.0.0) + semantic_range (3.1.0) + semverse (3.0.2) settingslogic (2.0.9) - sexp_processor (4.16.0) - simplecov (0.21.2) + simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) + simplecov-html (0.13.1) simplecov_json_formatter (0.1.4) slack-ruby-client (1.0.0) faraday (>= 1.0) @@ -440,17 +470,13 @@ GEM hashie websocket-driver slack_block_kit (0.3.3) - spring (2.1.1) - spring-watcher-listen (2.0.1) + snaky_hash (2.0.3) + hashie (>= 0.1.0, < 6) + version_gem (>= 1.1.8, < 3) + spring (4.3.0) + spring-watcher-listen (2.1.0) listen (>= 2.7, < 4.0) - spring (>= 1.2, < 3.0) - sprockets (4.0.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - sprockets (>= 3.0.0) + spring (>= 4) sslshake (1.3.1) strings (0.2.1) strings-ansi (~> 0.2) @@ -461,18 +487,18 @@ GEM activesupport (>= 3) attr_required (>= 0.0.5) httpclient (>= 2.4) - temple (0.8.2) - thor (1.2.1) - tilt (2.0.10) - timeout (0.3.2) - tomlrb (1.3.0) - train-core (3.8.7) + temple (0.10.3) + thor (1.3.2) + tilt (2.6.0) + timeout (0.4.3) + tomlrb (2.0.3) + train-core (3.12.13) addressable (~> 2.5) ffi (!= 1.13.0) json (>= 1.8, < 3.0) mixlib-shellout (>= 2.0, < 4.0) - net-scp (>= 1.2, < 4.0) - net-ssh (>= 2.9, < 7.0) + net-scp (>= 1.2, < 5.0) + net-ssh (>= 2.9, < 8.0) tty-box (0.7.0) pastel (~> 0.8) strings (~> 0.2.0) @@ -486,7 +512,7 @@ GEM tty-cursor (~> 0.7) tty-screen (~> 0.8) wisper (~> 2.0) - tty-screen (0.8.1) + tty-screen (0.8.2) tty-table (0.12.0) pastel (~> 0.8) strings (~> 0.2.0) @@ -496,12 +522,9 @@ GEM turbolinks-source (5.2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2021.5) + tzinfo-data (1.2025.2) tzinfo (>= 1.0.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unicode-display_width (2.1.0) + unicode-display_width (2.6.0) unicode_utils (1.4.0) validate_email (0.1.6) activemodel (>= 3.0) @@ -509,9 +532,10 @@ GEM validate_url (1.0.15) activemodel (>= 3.0.0) public_suffix + version_gem (1.1.8) warden (1.2.9) rack (>= 2.0.9) - web-console (4.2.0) + web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) bindex (>= 0.4.0) @@ -519,38 +543,38 @@ GEM webfinger (1.2.0) activesupport httpclient (>= 2.4) - webpacker (5.4.3) + webpacker (5.4.4) activesupport (>= 5.2) rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) - websocket (1.2.9) - websocket-driver (0.7.5) + websocket (1.2.11) + websocket-driver (0.8.0) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) wisper (2.0.1) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.8) + zeitwerk (2.6.18) PLATFORMS - ruby - x86_64-darwin-19 - x86_64-darwin-20 - x86_64-linux + arm64-darwin + x86_64-darwin DEPENDENCIES activerecord-import amoeba - audited (~> 5.3.3) + audited (~> 5.8.0) bootsnap (>= 1.4.2) brakeman byebug - capybara (>= 2.15) + capybara + concurrent-ruby (= 1.3.4) database_cleaner-active_record devise dotenv-rails - factory_bot_rails (~> 5.2.0) + factory_bot_rails (~> 6.4.0) fast_excel ffaker (~> 2.10) foreman @@ -559,7 +583,7 @@ DEPENDENCIES highline (~> 2.0) jbuilder (~> 2.7) letter_opener - listen (~> 3.1.5) + listen mitre-inspec-objects nokogiri nokogiri-happymapper @@ -567,13 +591,16 @@ DEPENDENCIES omniauth-github omniauth-rails_csrf_protection (~> 1.0) omniauth_openid_connect (~> 0.6.0) + overcommit (~> 0.60) ox pg (>= 0.18, < 2.0) + pry puma (~> 5.6) - rails (~> 6.1.4) + rails (~> 7.0.0) rest-client + rexml rspec-mocks - rspec-rails (~> 4.0.0) + rspec-rails (~> 6.0.0) rubocop rubocop-performance rubocop-rails @@ -585,14 +612,14 @@ DEPENDENCIES slack-ruby-client (= 1.0.0) slack_block_kit (= 0.3.3) spring - spring-watcher-listen (~> 2.0.0) + spring-watcher-listen turbolinks (~> 5) tzinfo-data web-console (>= 3.3.0) webpacker (~> 5.0) RUBY VERSION - ruby 2.7.5p203 + ruby 3.0.6p216 BUNDLED WITH - 2.3.10 + 2.5.23 diff --git a/Procfile.dev b/Procfile.dev index c6abddb0..cd022ede 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,2 +1,2 @@ web: bundle exec rails s -p 3000 -webpacker: ./bin/webpack-dev-server +webpacker: bash -c 'NODE_VERSION=$(node -v | cut -d. -f1 | sed "s/v//"); if [ "$NODE_VERSION" -ge 17 ]; then export NODE_OPTIONS="--openssl-legacy-provider"; fi; ./bin/webpack-dev-server' diff --git a/README.md b/README.md index 007c61ef..e93f630d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Vulcan [![Run Test Suite on Draft Release Creation, Push, and Pull Request to master](https://github.com/mitre/vulcan/actions/workflows/run-tests.yml/badge.svg)](https://github.com/mitre/vulcan/actions/workflows/run-tests.yml) [![Push Vulcan to Docker Hub on successful test suite run](https://github.com/mitre/vulcan/actions/workflows/push-to-docker.yml/badge.svg)](https://github.com/mitre/vulcan/actions/workflows/push-to-docker.yml) + ## Description Vulcan is a tool to help streamline the process of creating STIG-ready securiy guidance documentation and InSpec automated validation profiles. @@ -33,38 +34,68 @@ For more details on this release and previous ones, check the [Changelog](https: [Deploying Vulcan in Production](https://vulcan.mitre.org/docs/)   [](https://pages.github.com/)[](https://pages.github.com/) -## Deployment Dependencies +## Development Requirements + +### Version Requirements +* **Ruby**: 3.0.6 (see `.ruby-version`) +* **Rails**: 7.0.8.7 +* **Node.js**: 16.x (see `.nvmrc`) +* **PostgreSQL**: 12+ + +### System Dependencies -For Ruby (on Ubuntu): +For development on macOS/Linux: +* Ruby version manager (rbenv or rvm recommended) +* Node.js version manager (nvm recommended) +* Docker (for PostgreSQL) or local PostgreSQL +* `build-essential` (Linux) or Xcode Command Line Tools (macOS) +* `libpq-dev` (Linux) or `postgresql` (macOS via Homebrew) -* Ruby -* `build-essentials` -* Bundler -* `libq-dev` -* nodejs +### Quick Development Setup + +The easiest way to get started is using our development setup script: + +```bash +# Clone the repository +git clone https://github.com/mitre/vulcan.git +cd vulcan -### Run With Ruby +# Run the setup script (handles Ruby, Node.js, database setup) +./bin/dev-setup -#### Setup Ruby +# Start the application +foreman start -f Procfile.dev +``` -1. Install the version of Ruby specified in `.ruby-version` -2. Install postgres and rbenv -3. Run `gem install foreman` -4. Run `rbenv install` -5. Run `bin/setup` +### Manual Setup - >> **Note**: `bin/setup` will install the JS dependencies andprepare the database. +If you prefer to set up manually: -6. Run `rails db:seed` to seed the database. +1. Install Ruby 3.0.6 (using rbenv or rvm) +2. Install Node.js 16.x (using nvm) +3. Install PostgreSQL 12+ (via Docker or locally) +4. Run `gem install bundler` +5. Run `bundle install` +6. Run `yarn install` +7. Run `bin/rails db:create db:schema:load db:seed` -#### Running with Ruby +### Running the Application Make sure you have run the setup steps at least once before following these steps! -1. ensure postgres is running -2. foreman start -f Procfile.dev +1. Ensure PostgreSQL is running +2. Start the application: `foreman start -f Procfile.dev` 3. Navigate to `http://127.0.0.1:3000` +#### Node.js 17+ Compatibility Note + +If using Node.js 17 or higher, webpack-dev-server requires a compatibility flag: +```bash +NODE_OPTIONS=--openssl-legacy-provider ./bin/webpack-dev-server +``` + +Our `Procfile.dev` and `dev-setup` script handle this automatically. + #### Test User For testing purposes in the development environment, you can use the following credentials: diff --git a/app/controllers/components_controller.rb b/app/controllers/components_controller.rb index 9e25013d..14c3240e 100644 --- a/app/controllers/components_controller.rb +++ b/app/controllers/components_controller.rb @@ -13,12 +13,12 @@ class ComponentsController < ApplicationController before_action :authorize_admin_component, only: %i[destroy] before_action :authorize_author_component, only: %i[update] before_action :check_permission_to_update_slackchannel, only: %i[update] - before_action :authorize_admin_component, only: %i[update], if: (lambda { - params - .require(:component) - .permit(:advanced_fields)[:advanced_fields] - .present? - }) + before_action :authorize_admin_component, only: %i[update], if: lambda { + params + .require(:component) + .permit(:advanced_fields)[:advanced_fields] + .present? + } before_action :authorize_viewer_component, only: %i[show], if: -> { @component.released == false } before_action :authorize_logged_in, only: %i[search] @@ -241,7 +241,7 @@ def find LOWER(status_justification) LIKE ? OR LOWER(artifact_description) LIKE ? OR id IN (?) ", "%#{find_param}%", "%#{find_param}%", "%#{find_param}%", "%#{find_param}%", - "%#{find_param}%", (checks.pluck(:base_rule_id) | descriptions.pluck(:base_rule_id)) + "%#{find_param}%", checks.pluck(:base_rule_id) | descriptions.pluck(:base_rule_id) ) .order(:rule_id) @@ -353,7 +353,7 @@ def set_project end def check_permission_to_update_slackchannel - return if component_update_params[:component_metadata_attributes]&.dig('data')&.dig('Slack Channel ID').blank? + return if component_update_params[:component_metadata_attributes]&.dig('data', 'Slack Channel ID').blank? authorize_admin_component end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index a043068c..d0483be3 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -182,8 +182,8 @@ def project_params end def check_permission_to_update - condition = (project_params[:project_metadata_attributes]&.dig('data')&.dig('Slack Channel ID').present? || - project_params[:visibility].present?) + condition = project_params[:project_metadata_attributes]&.dig('data', 'Slack Channel ID').present? || + project_params[:visibility].present? authorize_admin_project if condition end diff --git a/app/controllers/rules_controller.rb b/app/controllers/rules_controller.rb index 548a28e9..2e9657c9 100644 --- a/app/controllers/rules_controller.rb +++ b/app/controllers/rules_controller.rb @@ -130,12 +130,12 @@ def create_or_duplicate srg_rule = srg.parsed_benchmark.rule.find { |r| r.ident.reject(&:legacy).first.ident == 'CCI-000366' } rule = BaseRule.from_mapping(Rule, srg_rule) - rule.audits.build(Audited.audit_class.create_initial_rule_audit_from_mapping(@component.id)) rule.component = @component rule.srg_rule = srg.srg_rules.find_by(ident: 'CCI-000366') rule.rule_id = (@component.rules.order(:rule_id).pluck(:rule_id).last.to_i + 1)&.to_s&.rjust(6, '0') rule.status = 'Not Yet Determined' rule.rule_severity = 'unknown' + rule.audit_comment = 'Created new rule' rule end diff --git a/app/controllers/security_requirements_guides_controller.rb b/app/controllers/security_requirements_guides_controller.rb index 9248da2d..00ffe4e3 100644 --- a/app/controllers/security_requirements_guides_controller.rb +++ b/app/controllers/security_requirements_guides_controller.rb @@ -6,7 +6,7 @@ class SecurityRequirementsGuidesController < ApplicationController before_action :security_requirements_guide, only: %i[destroy] def index - @srgs = SecurityRequirementsGuide.all.order(:srg_id, :version).select(:id, :srg_id, :title, :version, :release_date) + @srgs = SecurityRequirementsGuide.order(:srg_id, :version).select(:id, :srg_id, :title, :version, :release_date) respond_to do |format| format.html format.json { render json: @srgs } diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index c94afc2d..99ec2248 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -49,7 +49,7 @@ def build_oidc_logout_url(id_token) } # Add client_id if available (required by some providers) - client_id = Settings.oidc.args.client_options.identifier || ENV['VULCAN_OIDC_CLIENT_ID'] + client_id = Settings.oidc.args.client_options.identifier || ENV.fetch('VULCAN_OIDC_CLIENT_ID', nil) params[:client_id] = client_id if client_id.present? "#{logout_endpoint}?#{params.to_query}" @@ -62,7 +62,7 @@ def fetch_oidc_logout_endpoint return session[:oidc_logout_endpoint] end - issuer_url = Settings.oidc.args.issuer || ENV['VULCAN_OIDC_ISSUER_URL'] + issuer_url = Settings.oidc.args.issuer || ENV.fetch('VULCAN_OIDC_ISSUER_URL', nil) discovery_url = "#{issuer_url.to_s.chomp('/')}/.well-known/openid-configuration" Rails.logger.info "Fetching OIDC discovery document from: #{discovery_url}" diff --git a/app/controllers/stigs_controller.rb b/app/controllers/stigs_controller.rb index cde52d1c..f48b9019 100644 --- a/app/controllers/stigs_controller.rb +++ b/app/controllers/stigs_controller.rb @@ -6,7 +6,7 @@ class StigsController < ApplicationController before_action :set_stig, only: %i[show destroy] def index - @stigs = Stig.all.order(:stig_id, :version).select(:id, :stig_id, :title, :version, :benchmark_date) + @stigs = Stig.order(:stig_id, :version).select(:id, :stig_id, :title, :version, :benchmark_date) respond_to do |format| format.html format.json { render json: @stigs } @@ -56,7 +56,7 @@ def set_stig @stig = Stig.find_by(id: params[:id]) return unless @stig.nil? - flash[:alert] = 'STIG not found' + flash[:alert] = t('controllers.stigs.not_found') redirect_to stigs_path end end diff --git a/app/errors/not_authorized_error.rb b/app/errors/not_authorized_error.rb index 52f898c3..ac5a60f9 100644 --- a/app/errors/not_authorized_error.rb +++ b/app/errors/not_authorized_error.rb @@ -3,6 +3,6 @@ # Raised when a Rule cannot be successfully rolled back to a previous state class NotAuthorizedError < StandardError def initialize(message = 'You are not authorized to perform this action.') - super message + super end end diff --git a/app/errors/rule_revert_error.rb b/app/errors/rule_revert_error.rb index 4a81b0f5..7f65114e 100644 --- a/app/errors/rule_revert_error.rb +++ b/app/errors/rule_revert_error.rb @@ -3,6 +3,6 @@ # Raised when a Rule cannot be successfully rolled back to a previous state class RuleRevertError < StandardError def initialize(message = 'Could not revert history for rule.') - super message + super end end diff --git a/app/helpers/export_helper.rb b/app/helpers/export_helper.rb index ed885d0d..36ae8025 100644 --- a/app/helpers/export_helper.rb +++ b/app/helpers/export_helper.rb @@ -101,10 +101,10 @@ def get_check_and_fix_text(status) def export_xccdf_project(project) Zip::OutputStream.write_buffer do |zio| project.components.eager_load(rules: %i[disa_rule_descriptions checks - satisfies satisfied_by]).each do |component| + satisfies satisfied_by]).find_each do |component| version = component[:version] ? "V#{component[:version]}" : '' release = component[:release] ? "R#{component[:release]}" : '' - title = (component[:title] || "#{component[:name]} STIG Readiness Guide") + title = component[:title] || "#{component[:name]} STIG Readiness Guide" file_name = "U_#{title.tr(' ', '_')}_#{version}#{release}-xccdf.xml" zio.put_next_entry(file_name) @@ -119,7 +119,7 @@ def export_xccdf_project(project) def export_inspec_project(project) Zip::OutputStream.write_buffer do |zio| project.components.eager_load(rules: %i[disa_rule_descriptions checks - satisfies satisfied_by]).each do |component| + satisfies satisfied_by]).find_each do |component| version = component[:version] ? "V#{component[:version]}" : '' release = component[:release] ? "R#{component[:release]}" : '' dir = "#{component[:name].tr(' ', '-')}-#{version}#{release}-stig-baseline/" @@ -190,7 +190,7 @@ def xccdf_helper(component) benchmark['xmlns'] = 'http://checklists.nist.gov/xccdf/1.1' ox_el_helper(benchmark, 'status', 'draft', { date: Time.zone.today.strftime('%Y-%m-%d') }) - title = (component[:title] || "#{component[:name]} STIG Readiness Guide") + title = component[:title] || "#{component[:name]} STIG Readiness Guide" ox_el_helper(benchmark, 'title', title) ox_el_helper(benchmark, 'description', component[:description] || title) ox_el_helper(benchmark, 'notice', nil, { id: 'terms-of-use', 'xml:lang': 'en' }) diff --git a/app/lib/cci_map/constants.rb b/app/lib/cci_map/constants.rb index 9d26757d..84885843 100644 --- a/app/lib/cci_map/constants.rb +++ b/app/lib/cci_map/constants.rb @@ -3,6 +3,7 @@ # rubocop:disable Metrics/ModuleLength module CciMap module Constants + # rubocop:disable Metrics/CollectionLiteralLength CCI_TO_NIST_CONSTANT = { 'CCI-000001': 'AC-1 a 1', 'CCI-000002': 'AC-1 a 1 (a)', @@ -5105,6 +5106,7 @@ module Constants 'CCI-005149': 'AU-16 (3)', 'CCI-005150': 'PM-30 (1)' }.freeze + # rubocop:enable Metrics/CollectionLiteralLength end end # rubocop:enable Metrics/ModuleLength diff --git a/app/lib/vulcan_audit.rb b/app/lib/vulcan_audit.rb index 971ce72e..b463d607 100644 --- a/app/lib/vulcan_audit.rb +++ b/app/lib/vulcan_audit.rb @@ -1,21 +1,51 @@ # frozen_string_literal: true +require 'audited/audit' + # Custom Audited class for Vulcan-specific methods for interacting with audits. -class VulcanAudit < ::Audited::Audit +class VulcanAudit < Audited::Audit belongs_to :audited_user, class_name: 'User', optional: true - before_create :set_username, :find_and_save_audited_user, :find_and_save_associated_rule - def self.create_initial_rule_audit_from_mapping(project_id) - { - auditable_type: 'Rule', - action: 'create', - user_type: 'System', - audited_changes: { - project_id: project_id - } - } + # In Rails 5+, belongs_to associations are required by default. + # The parent Audited::Audit class defines: + # - `belongs_to :user, polymorphic: true` + # - `belongs_to :associated, polymorphic: true` + # Both become required. We need to allow nil for system-generated audits. + + # Override the associations to make them optional + belongs_to :user, polymorphic: true, optional: true + belongs_to :associated, polymorphic: true, optional: true + + # Force removal of presence validations that Rails adds automatically + # This needs to be done with a more aggressive approach since Rails keeps re-adding them + def self.remove_presence_validations! + # Remove all presence validators for user and associated + %i[user associated].each do |attr| + _validators[attr] = _validators[attr]&.reject { |v| v.is_a?(ActiveRecord::Validations::PresenceValidator) } || [] + + # Remove from callback chain (Rails 7 compatible) + callbacks_to_remove = _validate_callbacks.select do |callback| + callback.filter.is_a?(ActiveRecord::Validations::PresenceValidator) && + callback.filter.attributes.include?(attr) + end + + callbacks_to_remove.each do |callback| + _validate_callbacks.delete(callback) + end + end + end + + # Call it immediately after class definition + remove_presence_validations! + + # Also remove them in inherited classes + def self.inherited(subclass) + super + subclass.remove_presence_validations! end + before_create :set_username, :find_and_save_audited_user, :find_and_save_associated_rule + def set_username self.username = user&.name end diff --git a/app/lib/xccdf/idref/overrideable_idref.rb b/app/lib/xccdf/idref/overrideable_idref.rb index 61f58c81..23ddb6a3 100644 --- a/app/lib/xccdf/idref/overrideable_idref.rb +++ b/app/lib/xccdf/idref/overrideable_idref.rb @@ -5,6 +5,8 @@ module Xccdf # just a mandatory URI reference, but also have # an override attribute for controlling inheritance. class Idref + ## + # Handles IDREF elements that support override attributes class OverrideableIdref < Idref include HappyMapper diff --git a/app/lib/xccdf/item/selectable_item.rb b/app/lib/xccdf/item/selectable_item.rb index 0f15d65a..fae2957d 100644 --- a/app/lib/xccdf/item/selectable_item.rb +++ b/app/lib/xccdf/item/selectable_item.rb @@ -4,6 +4,7 @@ module Xccdf # This abstract item type represents the basic data shared by all # Groups and Rules. class Item + # Abstract base class for selectable XCCDF items (Groups and Rules) class SelectableItem < Item include HappyMapper diff --git a/app/lib/xccdf/item/selectable_item/group.rb b/app/lib/xccdf/item/selectable_item/group.rb index 6f03b86c..7bcdff7c 100644 --- a/app/lib/xccdf/item/selectable_item/group.rb +++ b/app/lib/xccdf/item/selectable_item/group.rb @@ -5,6 +5,7 @@ module Xccdf # Groups, Rules and Values. class Item class SelectableItem + # Represents a grouping of Groups, Rules and Values in XCCDF class Group < SelectableItem include HappyMapper diff --git a/app/lib/xccdf/item/selectable_item/rule.rb b/app/lib/xccdf/item/selectable_item/rule.rb index 021b8470..b8483bc8 100644 --- a/app/lib/xccdf/item/selectable_item/rule.rb +++ b/app/lib/xccdf/item/selectable_item/rule.rb @@ -5,6 +5,7 @@ module Xccdf # specific benchmark test. class Item class SelectableItem + # Represents a specific benchmark test in XCCDF class Rule < SelectableItem include HappyMapper diff --git a/app/lib/xccdf/item/value.rb b/app/lib/xccdf/item/value.rb index 475cddf0..97ebbd52 100644 --- a/app/lib/xccdf/item/value.rb +++ b/app/lib/xccdf/item/value.rb @@ -4,6 +4,7 @@ module Xccdf # Data type for the Value element, which represents a # tailorable string, boolean, or number in the Benchmark. class Item + # Represents a tailorable value in an XCCDF benchmark class Value < Item include HappyMapper diff --git a/app/lib/xccdf/warning.rb b/app/lib/xccdf/warning.rb index b1080adc..d7f2857f 100644 --- a/app/lib/xccdf/warning.rb +++ b/app/lib/xccdf/warning.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true module Xccdf + # Represents warnings with various categories in XCCDF class Warning include HappyMapper diff --git a/app/models/additional_answer.rb b/app/models/additional_answer.rb index 85113770..45ad2535 100644 --- a/app/models/additional_answer.rb +++ b/app/models/additional_answer.rb @@ -9,8 +9,8 @@ class AdditionalAnswer < ApplicationRecord belongs_to :additional_question belongs_to :rule - URL_REGEXP = %r{\A(http|https)://[a-z0-9@:%._+~#=]{2,256}\.[a-z]{2,16}\b([-a-z0-9@:%_+.~#?&/=]*)\z}ix.freeze - validates :answer, format: { with: URL_REGEXP, message: 'URL must be valid and begin with http or https' }, + URL_REGEXP = %r{\A(http|https)://[a-z0-9@:%._+~#=]{2,256}\.[a-z]{2,16}\b([-a-z0-9@:%_+.~#?&/=]*)\z}ix + validates :answer, format: { with: URL_REGEXP }, if: :present_and_type_is_url? def present_and_type_is_url? diff --git a/app/models/additional_question.rb b/app/models/additional_question.rb index e007c903..34400cc2 100644 --- a/app/models/additional_question.rb +++ b/app/models/additional_question.rb @@ -11,7 +11,7 @@ class AdditionalQuestion < ApplicationRecord FIELD_TYPES = %w[dropdown freeform url].freeze - enum question_type: FIELD_TYPES.zip(FIELD_TYPES).to_h + enum :question_type, FIELD_TYPES.zip(FIELD_TYPES).to_h validates :name, :question_type, presence: true diff --git a/app/models/base_rule.rb b/app/models/base_rule.rb index 36e5d1ae..387fa50d 100644 --- a/app/models/base_rule.rb +++ b/app/models/base_rule.rb @@ -49,7 +49,7 @@ def self.from_mapping(rule_class, rule_mapping) title: rule_mapping.title.first || nil, ident: rule_mapping.ident.reject(&:legacy).map(&:ident).sort.join(', '), legacy_ids: rule_mapping.ident.select(&:legacy).map(&:ident).join(', '), - ident_system: rule_mapping.ident&.reject(&:legacy)&.first&.system, + ident_system: rule_mapping.ident&.reject(&:legacy)&.first.try(:system), fixtext: rule_mapping.fixtext.first&.fixtext, fixtext_fixref: rule_mapping.fixtext.first&.fixref, fix_id: rule_mapping.fix.first&.id @@ -81,8 +81,7 @@ def as_json(options = {}) def nist_control_family ccis = ident.to_s.split(/, */) - ia_controls = [] - ccis.each { |cci| ia_controls << CCI_TO_NIST_CONSTANT[cci.to_sym] } + ia_controls = ccis.map { |cci| CCI_TO_NIST_CONSTANT[cci.to_sym] } ia_controls.uniq.join(', ') end diff --git a/app/models/check.rb b/app/models/check.rb index da3bbfe2..143577d2 100644 --- a/app/models/check.rb +++ b/app/models/check.rb @@ -11,8 +11,8 @@ class Check < ApplicationRecord def self.from_mapping(check_mapping) { system: check_mapping&.system, - content_ref_name: check_mapping&.check_content_ref&.first&.name, - content_ref_href: check_mapping&.check_content_ref&.first&.href, + content_ref_name: check_mapping&.check_content_ref&.first.try(:name), + content_ref_href: check_mapping&.check_content_ref&.first.try(:href), content: check_mapping&.check_content&.content } end diff --git a/app/models/component.rb b/app/models/component.rb index ed4e3ce5..31a2ad86 100644 --- a/app/models/component.rb +++ b/app/models/component.rb @@ -97,7 +97,7 @@ def from_spreadsheet(spreadsheet) return end - spreadsheet_srg_ids = parsed.map { |row| row[IMPORT_MAPPING[:srg_id]] } + spreadsheet_srg_ids = parsed.pluck(IMPORT_MAPPING[:srg_id]) database_srg_ids = srg_rules.map(&:version) missing_from_srg = spreadsheet_srg_ids - database_srg_ids @@ -118,7 +118,7 @@ def from_spreadsheet(spreadsheet) end # Calculate the prefix (which will need to be removed from each row) - possible_prefixes = parsed.collect { |row| row[IMPORT_MAPPING[:stig_id]] }.compact_blank + possible_prefixes = parsed.pluck(IMPORT_MAPPING[:stig_id]).compact_blank if possible_prefixes.empty? errors.add(:base, 'No STIG prefixes were detected in the file. Please set any STIGID ' \ 'in the file and try again.') @@ -230,7 +230,7 @@ def releasable return false if released_was # If all rules are locked, then component may be released - rules.where(locked: false).size.zero? + rules.where(locked: false).empty? end def duplicate(new_name: nil, new_prefix: nil, new_version: nil, new_release: nil, @@ -391,6 +391,10 @@ def from_mapping(srg, new_rule_versions = nil, starting_idx = 0) end success rescue StandardError => e + # Log the full error for debugging + Rails.logger.error "Import error: #{e.class}: #{e.message}" + Rails.logger.error e.backtrace.join("\n") + message = e.message[0, 50] message += '...' if e.message.size >= 50 errors.add(:base, "Encountered an error when importing rules from the SRG: #{message}") @@ -402,8 +406,9 @@ def largest_rule_id if id.nil? rules.collect { |rule| rule.rule_id.to_i }.max else - Rule.connection.execute("SELECT MAX(TO_NUMBER(rule_id, '999999')) FROM base_rules - WHERE component_id = #{id}")&.values&.flatten&.first&.to_i || 0 + result = Rule.connection.execute("SELECT MAX(TO_NUMBER(rule_id, '999999')) FROM base_rules + WHERE component_id = #{id}") + result&.values&.flatten.try(:first).to_i end end diff --git a/app/models/component_metadata.rb b/app/models/component_metadata.rb index 0d16818b..326aee81 100644 --- a/app/models/component_metadata.rb +++ b/app/models/component_metadata.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true +## +# ComponentMetadata stores additional metadata for components in a flexible JSON structure class ComponentMetadata < ApplicationRecord belongs_to :component - validates :component, uniqueness: { message: 'already has associated metadata' } + validates :component, uniqueness: true end diff --git a/app/models/concerns/prefix_validator.rb b/app/models/concerns/prefix_validator.rb index ed5435ef..c0c77436 100644 --- a/app/models/concerns/prefix_validator.rb +++ b/app/models/concerns/prefix_validator.rb @@ -3,7 +3,7 @@ # Validates that a project prefix is in the correct format class PrefixValidator < ActiveModel::Validator def validate(record) - return if record.prefix.respond_to?(:match?) && validate_prefix(record.prefix) + return if record.prefix.respond_to?(:match?) && validate_prefix?(record.prefix) record.errors.add(:base, 'Prefix must be of the form AAAA-00') end @@ -12,7 +12,7 @@ def validate(record) # Prefixes are 4 alphanumeric characters, followed by a dash, followed by 2 alphanumeric characters. # Ex. abcd-01 - def validate_prefix(prefix) + def validate_prefix?(prefix) return true if prefix.match?(/^\w{4}-\w{2}$/) false diff --git a/app/models/membership.rb b/app/models/membership.rb index ebd25ac5..02bc6326 100644 --- a/app/models/membership.rb +++ b/app/models/membership.rb @@ -22,12 +22,14 @@ class Membership < ApplicationRecord validates :role, inclusion: { in: PROJECT_MEMBER_ROLES, - message: "is not an acceptable value. Acceptable values are: #{PROJECT_MEMBER_ROLES.join(', ')}" + message: lambda do |_object, _data| + I18n.t('activerecord.errors.models.membership.attributes.role.inclusion', + acceptable_values: PROJECT_MEMBER_ROLES.join(', ')) + end } validates :user, uniqueness: { - scope: %i[membership_type membership_id], - message: 'is already a member of this project.' + scope: %i[membership_type membership_id] } ## @@ -35,7 +37,7 @@ class Membership < ApplicationRecord # This is useful for the ProjectMember.vue component. # def as_json(options = {}) - super options.merge(methods: %i[name email]) + super(options.merge(methods: %i[name email])) end private @@ -89,8 +91,8 @@ def cannot_have_equal_or_lesser_component_permissions errors.add( :role, - "provides equal or lesser permissions compared to the role the user's current project level role"\ - " (#{project_membership_role}). This permission would have no effect on the user's abilities." + I18n.t('activerecord.errors.models.membership.attributes.role.equal_or_lesser_permissions', + project_role: project_membership_role) ) end end diff --git a/app/models/project.rb b/app/models/project.rb index 16a5643d..b2f01c48 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -4,7 +4,7 @@ class Project < ApplicationRecord attr_accessor :current_user - enum visibility: { discoverable: 0, hidden: 1 } + enum :visibility, { discoverable: 0, hidden: 1 } audited except: %i[id admin_name admin_email memberships_count created_at updated_at], max_audits: 1000 @@ -48,7 +48,7 @@ def update_admin_contact_info # Get a list of Users that are not yet members of this project # def available_members - (User.all.select(:id, :name, :email) - users.select(:id, :name, :email)) + (User.select(:id, :name, :email) - users.select(:id, :name, :email)) end def details diff --git a/app/models/project_access_request.rb b/app/models/project_access_request.rb index 62312d39..2685512d 100644 --- a/app/models/project_access_request.rb +++ b/app/models/project_access_request.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true +## +# ProjectAccessRequest represents a user's request to access a specific project class ProjectAccessRequest < ApplicationRecord belongs_to :user belongs_to :project - validates :user_id, uniqueness: { scope: :project_id, message: 'has already requested access to this project' } + validates :user_id, uniqueness: { scope: :project_id } end diff --git a/app/models/project_metadata.rb b/app/models/project_metadata.rb index 4ce886af..46a1353c 100644 --- a/app/models/project_metadata.rb +++ b/app/models/project_metadata.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true +## +# ProjectMetadata stores additional metadata for projects in a flexible JSON structure class ProjectMetadata < ApplicationRecord belongs_to :project - validates :project, uniqueness: { message: 'already has associated metadata' } + validates :project, uniqueness: true end diff --git a/app/models/review.rb b/app/models/review.rb index a5a78b8d..915da0e8 100644 --- a/app/models/review.rb +++ b/app/models/review.rb @@ -23,7 +23,7 @@ class Review < ApplicationRecord # Override `as_json` to include delegated attributes # def as_json(options = {}) - super options.merge(methods: %i[name]) + super(options.merge(methods: %i[name])) end private diff --git a/app/models/rule.rb b/app/models/rule.rb index 7a0ac107..76d1e83f 100644 --- a/app/models/rule.rb +++ b/app/models/rule.rb @@ -62,12 +62,14 @@ def update_single_rule_clone(rule_clone) def self.from_mapping(rule_mapping, component_id, idx, srg_rules) rule = super(self, rule_mapping) - rule.audits.build(Audited.audit_class.create_initial_rule_audit_from_mapping(component_id)) rule.component_id = component_id rule.srg_rule_id = srg_rules[rule.rule_id] # This is what is appended to the component prefix in the UI rule.rule_id = idx&.to_s&.rjust(6, '0') + # Set audit comment to indicate this was created from SRG mapping + rule.audit_comment = 'Created from SRG mapping' + rule end @@ -77,14 +79,14 @@ def status end def status=(value) - super(value) unless satisfied_by.size.positive? + super unless satisfied_by.size.positive? end ## # Override `as_json` to include parent SRG information # def as_json(options = {}) - result = super(options) + result = super unless options[:skip_merge].eql?(true) result = result.merge( { diff --git a/app/models/security_requirements_guide.rb b/app/models/security_requirements_guide.rb index bc399cb1..d25b0d4d 100644 --- a/app/models/security_requirements_guide.rb +++ b/app/models/security_requirements_guide.rb @@ -10,8 +10,7 @@ class SecurityRequirementsGuide < ApplicationRecord validates :srg_id, :title, :version, :xml, presence: true validates :srg_id, uniqueness: { - scope: :version, - message: ' ID has already been taken' + scope: :version } # Since an SRG is top-level, the parameter is the entire parsed benchmark diff --git a/app/models/stig.rb b/app/models/stig.rb index 3e6669dc..1ccc950e 100644 --- a/app/models/stig.rb +++ b/app/models/stig.rb @@ -6,8 +6,7 @@ class Stig < ApplicationRecord validates :stig_id, :title, :name, :version, :xml, presence: true validates :stig_id, uniqueness: { - scope: :version, - message: 'ID has already been taken' + scope: :version } after_create :import_stig_rules diff --git a/app/views/components/show.html.haml b/app/views/components/show.html.haml index d14f45de..e319cf01 100644 --- a/app/views/components/show.html.haml +++ b/app/views/components/show.html.haml @@ -4,7 +4,7 @@ #projectcomponent %projectcomponent{ | - 'v-bind:queried-rule': @rule_json, | + 'v-bind:queried-rule': (@rule_json || 'null'), | 'v-bind:effective_permissions': @effective_permissions.to_json, | 'v-bind:initial-component-state': @component_json, | 'v-bind:project': @project_json, | diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index 579ff86b..9ed233ca 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -25,11 +25,11 @@ %b-card-text = render 'devise/sessions/ldap' - if local_login_enabled? - %b-tab{:active => params[:active_tab].eql?('local') ? true : false, :title => "Local Login"} + %b-tab{'v-bind:active' => params[:active_tab].eql?('local').to_s, :title => "Local Login"} %b-card-text = render 'devise/sessions/local' - if user_registration_enabled? - %b-tab{:active => params[:active_tab].eql?('registration') ? true : false, :title => "Register"} + %b-tab{'v-bind:active' => params[:active_tab].eql?('registration').to_s, :title => "Register"} %b-card-text = render 'devise/registrations/form' diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 0021533a..86e1b583 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -18,7 +18,7 @@ %navbar{ | 'v-bind:navigation': @navigation.to_json, | 'v-bind:signed_in': user_signed_in?.to_s, | - 'v-bind:users_path': current_user&.admin ? users_path.to_json : nil, | + 'v-bind:users_path': (current_user&.admin ? users_path : '').to_json, | 'v-bind:profile_path': edit_user_registration_path.to_json, | 'v-bind:sign_out_path': destroy_user_session_path.to_json, | 'v-bind:access_requests': @access_requests.to_json | diff --git a/app/views/stigs/show.html.haml b/app/views/stigs/show.html.haml index 4cfe9aa7..d3a0d20e 100644 --- a/app/views/stigs/show.html.haml +++ b/app/views/stigs/show.html.haml @@ -4,7 +4,7 @@ #stig %stig{ | - 'v-bind:queried-rule': @rule_json, | + 'v-bind:queried-rule': (@rule_json || 'null'), | 'v-bind:stig': @stig_json, | 'v-bind:severities': RuleConstants::SEVERITIES.to_json, | 'v-bind:severities_map': RuleConstants::SEVERITIES_MAP.to_json, | diff --git a/bin/dev-setup b/bin/dev-setup new file mode 100755 index 00000000..16133928 --- /dev/null +++ b/bin/dev-setup @@ -0,0 +1,317 @@ +#!/bin/bash +# +# Vulcan Development Environment Setup Script +# +# This script sets up the complete Vulcan development environment including: +# - Ruby version management (rbenv/rvm) +# - Node.js version management (nvm) +# - PostgreSQL database via Docker +# - Authentication configuration (Local/LDAP/Okta) +# - Database initialization with seed data +# +# For a simpler Rails-only setup, use: bin/setup +# + +# Script usage +usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --clean Stop and remove existing containers before starting" + echo " --refresh Reset database (drops and recreates)" + echo " --okta Configure environment for Okta authentication" + echo " --ldap Configure environment for LDAP authentication" + echo " --local Configure environment for local authentication (default)" + echo " --help Show this help message" + echo "" + echo "Examples:" + echo " $0 # Start with local auth" + echo " $0 --okta # Start with Okta auth" + echo " $0 --ldap # Start with LDAP auth" + echo " $0 --clean --okta # Clean start with Okta" + exit 1 +} + +# Parse command line arguments +CLEAN=false +REFRESH=false +AUTH_MODE="local" + +while [[ $# -gt 0 ]]; do + case $1 in + --clean) + CLEAN=true + shift + ;; + --refresh) + REFRESH=true + shift + ;; + --okta) + AUTH_MODE="okta" + shift + ;; + --ldap) + AUTH_MODE="ldap" + shift + ;; + --local) + AUTH_MODE="local" + shift + ;; + --help) + usage + ;; + *) + echo "Unknown option: $1" + usage + ;; + esac +done + +echo "Starting Vulcan Development Environment (Auth: $AUTH_MODE)..." + +# Clean up if requested or if containers are already running +if [ "$CLEAN" = true ] || docker-compose -f docker-compose.dev.yml ps -q | grep -q .; then + if [ "$CLEAN" = true ]; then + echo "Cleaning up existing containers (--clean flag)..." + else + echo "Found existing containers, cleaning up..." + fi + docker-compose -f docker-compose.dev.yml down +fi + +# Check Ruby version +REQUIRED_RUBY=$(cat .ruby-version 2>/dev/null || echo "2.7.5") +CURRENT_RUBY=$(ruby -v 2>/dev/null | awk '{print $2}' | cut -d'p' -f1) + +echo "Required Ruby version: $REQUIRED_RUBY" +echo "Current Ruby version: $CURRENT_RUBY" + +# Check for rbenv or rvm +if command -v rbenv &> /dev/null; then + echo "Using rbenv..." + # Ensure rbenv loads the correct version + eval "$(rbenv init -)" + if ! rbenv versions | grep -q "$REQUIRED_RUBY"; then + echo "Installing Ruby $REQUIRED_RUBY with rbenv..." + rbenv install "$REQUIRED_RUBY" + fi + rbenv local "$REQUIRED_RUBY" +elif command -v rvm &> /dev/null; then + echo "Using rvm..." + # Load RVM if not already loaded + if ! type rvm | grep -q 'function'; then + [[ -s "$HOME/.rvm/scripts/rvm" ]] && source "$HOME/.rvm/scripts/rvm" + fi + + # Use rvm use . to respect .ruby-version and .ruby-gemset files + echo "Loading RVM settings from .ruby-version and .ruby-gemset..." + rvm use . + + # If the required Ruby isn't installed, install it + if ! rvm list | grep -q "$REQUIRED_RUBY"; then + echo "Installing Ruby $REQUIRED_RUBY..." + rvm install "$REQUIRED_RUBY" + rvm use . + fi +else + echo "WARNING: Neither rbenv nor rvm detected!" + echo "Please ensure Ruby $REQUIRED_RUBY is installed and active" + if [ "$CURRENT_RUBY" != "$REQUIRED_RUBY" ]; then + echo "ERROR: Wrong Ruby version. Expected $REQUIRED_RUBY but got $CURRENT_RUBY" + exit 1 + fi +fi + +# Verify Ruby version +CURRENT_RUBY=$(ruby -v | awk '{print $2}' | cut -d'p' -f1) +if [ "$CURRENT_RUBY" != "$REQUIRED_RUBY" ]; then + echo "ERROR: Failed to switch to Ruby $REQUIRED_RUBY" + exit 1 +fi + +echo "✓ Using Ruby $CURRENT_RUBY with gemset: $(rvm current | cut -d'@' -f2)" + +# Check Node.js version +if command -v nvm &> /dev/null; then + echo "Checking Node.js version..." + # Load NVM if not already loaded + if ! type nvm | grep -q 'function'; then + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + fi + + # Use .nvmrc or default to Node 16 + if [ -f .nvmrc ]; then + nvm use + else + echo "No .nvmrc found, using Node 16 to match production..." + nvm use 16 + fi + + echo "✓ Using Node.js $(node -v)" +else + echo "WARNING: NVM not found. Please ensure Node.js 16.x is installed" + echo "Current Node.js: $(node -v 2>/dev/null || echo 'Not installed')" +fi + +# Check and install Ruby dependencies +echo "Checking Ruby dependencies..." +bundle check || { + echo "Installing Ruby dependencies..." + bundle install +} + +# Check and install JavaScript dependencies +echo "Checking JavaScript dependencies..." +if [ ! -d "node_modules" ] || [ "package.json" -nt "node_modules" ]; then + echo "Installing JavaScript dependencies..." + yarn install +fi + +# Configure environment based on auth mode +case $AUTH_MODE in + okta) + # Check if .env.development exists, if not copy from .env.okta.dev + if [ ! -f .env.development ]; then + echo "Creating .env.development from .env.okta.dev template..." + cp .env.okta.dev .env.development + fi + + # Check for Okta credentials + if [ -f .env.development.local ]; then + echo "Loading Okta credentials from .env.development.local" + # shellcheck disable=SC2046 + export $(grep -v '^#' .env.development.local | xargs) + else + echo "WARNING: .env.development.local not found!" + echo "Please add your Okta credentials to .env.development.local:" + echo " VULCAN_OIDC_CLIENT_ID=your-client-id" + echo " VULCAN_OIDC_CLIENT_SECRET=your-client-secret" + fi + ;; + + ldap) + # Set up LDAP environment + export VULCAN_ENABLE_LDAP=true + + # Check if .env.ldap exists + if [ -f .env.ldap ]; then + echo "Loading LDAP configuration from .env.ldap" + # shellcheck disable=SC2046 + export $(grep -v '^#' .env.ldap | xargs) + else + echo "Using default LDAP configuration for development" + export VULCAN_LDAP_HOST=localhost + export VULCAN_LDAP_PORT=10389 + export VULCAN_LDAP_ATTRIBUTE=mail + export VULCAN_LDAP_BIND_DN="cn=admin,dc=planetexpress,dc=com" + export VULCAN_LDAP_ADMIN_PASS="GoodNewsEveryone" + export VULCAN_LDAP_BASE="ou=people,dc=planetexpress,dc=com" + + echo "LDAP will be available with test users from docker-compose.dev.yml" + fi + ;; + + local) + # Local authentication is the default + echo "Using local authentication (default)" + + # Load general .env.development if it exists + if [ -f .env.development ]; then + # shellcheck disable=SC2046 + export $(grep -v '^#' .env.development | xargs) + fi + ;; +esac + +# Load any general .env file +if [ -f .env ]; then + echo "Loading general environment from .env" + # shellcheck disable=SC2046 + export $(grep -v '^#' .env | xargs) +fi + +# Start PostgreSQL in Docker +echo "Starting PostgreSQL database..." +docker-compose -f docker-compose.dev.yml up -d + +# Export DATABASE_URL for Rails +export DATABASE_URL="postgres://postgres:postgres@localhost:5432/vulcan_vue_development" + +# Wait for PostgreSQL to be ready +echo "Waiting for PostgreSQL to be ready..." +until docker-compose -f docker-compose.dev.yml exec -T db pg_isready -U postgres > /dev/null 2>&1; do + echo -n "." + sleep 1 +done +echo " PostgreSQL is ready!" + +# Check database setup +echo "Checking database..." + +if [ "$REFRESH" = true ]; then + echo "Refreshing database (--refresh flag)..." + DISABLE_SPRING=1 DATABASE_URL="postgres://postgres:postgres@localhost:5432/vulcan_vue_development" rails db:drop db:create db:schema:load db:seed +else + DISABLE_SPRING=1 DATABASE_URL="postgres://postgres:postgres@localhost:5432/vulcan_vue_development" rails db:version 2>/dev/null || { + echo "Setting up database..." + DISABLE_SPRING=1 DATABASE_URL="postgres://postgres:postgres@localhost:5432/vulcan_vue_development" rails db:create db:schema:load db:seed + } +fi + +echo "" +echo "✅ Vulcan Development Environment Ready!" +echo "" +echo "Authentication mode: $AUTH_MODE" +echo "Database is running at: postgres://localhost:5432/vulcan_vue_development" +echo "" +echo "To start the Rails app, run:" +echo " foreman start -f Procfile.dev" +echo "" +echo "Or in separate terminals:" +echo " Terminal 1: bundle exec rails server" + +# Check if Node.js 17+ is being used and provide appropriate webpack-dev-server command +NODE_VERSION=$(node -v | cut -d. -f1 | sed 's/v//') +if [ "$NODE_VERSION" -ge 17 ]; then + echo " Terminal 2: NODE_OPTIONS=--openssl-legacy-provider ./bin/webpack-dev-server" + echo "" + echo " Note: Node.js $NODE_VERSION requires the --openssl-legacy-provider flag for webpack 4" +else + echo " Terminal 2: ./bin/webpack-dev-server" +fi +echo "" +echo "Useful commands:" +echo " Stop database: docker-compose -f docker-compose.dev.yml down" +echo " View logs: docker-compose -f docker-compose.dev.yml logs -f" +echo " Reset everything: $0 --clean --refresh" +echo "" + +# Show auth-specific information +case $AUTH_MODE in + okta) + echo "Okta authentication configured." + echo "Make sure your Okta app is configured with:" + echo " Redirect URI: http://localhost:3000/users/auth/openid_connect/callback" + ;; + ldap) + echo "LDAP authentication configured." + if [ ! -f .env.ldap ]; then + echo "" + echo "Test LDAP credentials (from docker-compose.dev.yml):" + echo " Username: fry@planetexpress.com" + echo " Password: fry" + echo " Username: zoidberg@planetexpress.com" + echo " Password: zoidberg" + fi + ;; + local) + echo "Local authentication configured." + echo "" + echo "Test credentials:" + echo " Email: admin@example.com" + echo " Password: 1234567ab!" + ;; +esac \ No newline at end of file diff --git a/bin/rails b/bin/rails index 5badb2fd..efc03774 100755 --- a/bin/rails +++ b/bin/rails @@ -1,9 +1,4 @@ #!/usr/bin/env ruby -begin - load File.expand_path('../spring', __FILE__) -rescue LoadError => e - raise unless e.message.include?('spring') -end -APP_PATH = File.expand_path('../config/application', __dir__) -require_relative '../config/boot' -require 'rails/commands' +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake index d87d5f57..4fbf10b9 100755 --- a/bin/rake +++ b/bin/rake @@ -1,9 +1,4 @@ #!/usr/bin/env ruby -begin - load File.expand_path('../spring', __FILE__) -rescue LoadError => e - raise unless e.message.include?('spring') -end -require_relative '../config/boot' -require 'rake' +require_relative "../config/boot" +require "rake" Rake.application.run diff --git a/bin/setup b/bin/setup index 3b392888..ec47b79b 100755 --- a/bin/setup +++ b/bin/setup @@ -1,36 +1,33 @@ #!/usr/bin/env ruby -require 'fileutils' +require "fileutils" # path to your application root. -APP_ROOT = File.expand_path('..', __dir__) +APP_ROOT = File.expand_path("..", __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end FileUtils.chdir APP_ROOT do - # This script is a way to setup or update your development environment automatically. - # This script is idempotent, so that you can run it at anytime and get an expectable outcome. + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. - puts '== Installing dependencies ==' - system! 'gem install bundler --conservative' - system('bundle check') || system!('bundle install') - - # Install JavaScript dependencies - system('bin/yarn') + puts "== Installing dependencies ==" + system! "gem install bundler --conservative" + system("bundle check") || system!("bundle install") # puts "\n== Copying sample files ==" - # unless File.exist?('config/database.yml') - # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" # end puts "\n== Preparing database ==" - system! 'bin/rails db:create db:schema:load' + system! "bin/rails db:prepare" puts "\n== Removing old logs and tempfiles ==" - system! 'bin/rails log:clear tmp:clear' + system! "bin/rails log:clear tmp:clear" puts "\n== Restarting application server ==" - system! 'bin/rails restart' + system! "bin/rails restart" end diff --git a/config/application.rb b/config/application.rb index 25835994..f4017cc9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -31,17 +31,19 @@ Dotenv.load('.env', ".env.#{Rails.env}", ".env.#{Rails.env}.local") if defined?(Dotenv) module VulcanVue - # This application was originally generated using Rails 6.0. Any subsequent updates - # will require testing to verify that the defaults for that new version do not break - # any functionality. + ## + # Main application configuration for Vulcan class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 6.0 + config.load_defaults 7.0 config.time_zone = 'UTC' - # Settings in config/environments/* take precedence over those specified here. - # Application configuration can go into files in config/initializers - # -- all .rb files in that directory are automatically loaded after loading - # the framework and any gems in your application. + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") end end diff --git a/config/environments/development.rb b/config/environments/development.rb index 079778f0..60ebdbc1 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true +require 'active_support/core_ext/integer/time' + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # In the development environment your application's code is reloaded on - # every request. This slows down response time but is perfect for development + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false @@ -14,9 +16,12 @@ # Show full error reports. config.consider_all_requests_local = true + # Enable server timing + config.server_timing = true + # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. - if Rails.root.join('tmp', 'caching-dev.txt').exist? + if Rails.root.join('tmp', 'caching-dev.txt', 'caching-dev.txt').exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true @@ -41,6 +46,12 @@ # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load @@ -48,7 +59,13 @@ config.active_record.verbose_query_logs = true # Raises error for missing translations. - # config.action_view.raise_on_missing_translations = true + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true # Use the letter opener gem for email delivery in development config.action_mailer.delivery_method = :letter_opener diff --git a/config/environments/production.rb b/config/environments/production.rb index aff76889..af90f4d9 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'active_support/core_ext/integer/time' + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -25,26 +27,26 @@ config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = 'http://assets.example.com' + # config.asset_host = "http://assets.example.com" # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil - # config.action_cable.url = 'wss://example.com/cable' - # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + # config.action_cable.url = "wss://example.com/cable" + # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = ENV['FORCE_SSL'].is_a? String - # Use the lowest log level to ensure availability of diagnostic information - # when problems arise. - config.log_level = :debug + # Include generic and useful information about system operation, but avoid logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). + config.log_level = :info # Prepend all log lines with the following tags. config.log_tags = [:request_id] @@ -69,15 +71,15 @@ # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true - # Send deprecation notices to registered listeners. - config.active_support.deprecation = :notify + # Don't log any deprecations. + config.active_support.report_deprecations = false # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new + config.log_formatter = Logger::Formatter.new # Use a different logger for distributed setups. - # require 'syslog/logger' - # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + # require "syslog/logger" + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") if ENV['RAILS_LOG_TO_STDOUT'].present? logger = ActiveSupport::Logger.new($stdout) @@ -87,25 +89,4 @@ # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false - - # Inserts middleware to perform automatic connection switching. - # The `database_selector` hash is used to pass options to the DatabaseSelector - # middleware. The `delay` is used to determine how long to wait after a write - # to send a subsequent read to the primary. - # - # The `database_resolver` class is used by the middleware to determine which - # database is appropriate to use based on the time delay. - # - # The `database_resolver_context` class is used by the middleware to set - # timestamps for the last write to the primary. The resolver uses the context - # class timestamps to determine how long to wait before reading from the - # replica. - # - # By default Rails will store a last write timestamp in the session. The - # DatabaseSelector middleware is designed as such you can define your own - # strategy for connection switching and pass that into the middleware through - # these configuration options. - # config.active_record.database_selector = { delay: 2.seconds } - # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver - # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session end diff --git a/config/environments/test.rb b/config/environments/test.rb index 3b35cdef..e5df61fd 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'active_support/core_ext/integer/time' + # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped @@ -8,12 +10,13 @@ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - config.cache_classes = false + # Turn false under Spring and add config.action_view.cache_template_loading = true. + config.cache_classes = true - # Do not eager load code on boot. This avoids loading your whole application - # just for the purpose of running a single test. If you are using a tool that - # preloads Rails for running tests, you may have to set it to true. - config.eager_load = false + # Eager loading loads your whole application. When running a single test locally, + # this probably isn't necessary. It's a good idea to do in a continuous integration + # system, or in some way before deploying your code. + config.eager_load = ENV['CI'].present? # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true @@ -47,6 +50,15 @@ # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + # Raises error for missing translations. - # config.action_view.raise_on_missing_translations = true + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true end diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb index f4556db3..6d56e439 100644 --- a/config/initializers/application_controller_renderer.rb +++ b/config/initializers/application_controller_renderer.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # Be sure to restart your server when you modify this file. # ActiveSupport::Reloader.to_prepare do diff --git a/config/initializers/audited.rb b/config/initializers/audited.rb index 1f180d82..2a2639e2 100644 --- a/config/initializers/audited.rb +++ b/config/initializers/audited.rb @@ -4,5 +4,23 @@ Rails.application.reloader.to_prepare do Audited.config do |config| config.audit_class = VulcanAudit + config.current_user_method = :current_user + end + + # Force removal of presence validations using our custom method + # This handles cases where Rails re-adds validations after class loading + VulcanAudit.remove_presence_validations! +end + +# Configure Warden to set audit user on authentication +# This ensures audits are properly associated with the authenticated user +if defined?(Warden) + Warden::Manager.after_set_user do |user, _auth, opts| + # Only set for non-fetch operations (actual login) + Audited.store[:current_user] = user if opts[:event] != :fetch && user + end + + Warden::Manager.before_logout do |_user, _auth, _opts| + Audited.store[:current_user] = nil end end diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb index d0f0d3b5..4b63f289 100644 --- a/config/initializers/backtrace_silencers.rb +++ b/config/initializers/backtrace_silencers.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index 598474b1..5346075a 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -2,21 +2,26 @@ # Be sure to restart your server when you modify this file. -# Define an application-wide content security policy -# For further information see the following documentation -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header -Rails.application.config.content_security_policy do |policy| - policy.script_src :self, :unsafe_eval -end - -# If you are using UJS then enable automatic nonce generation -# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } +Rails.application.configure do + config.content_security_policy do |policy| + # policy.default_src :self, :https + # policy.font_src :self, :https, :data + # policy.img_src :self, :https, :data + # policy.object_src :none + policy.script_src :self, :unsafe_eval + # policy.style_src :self, :https + # Specify URI for violation reports + # policy.report_uri "/csp-violation-report-endpoint" + end -# Set the nonce only to specific directives -# Rails.application.config.content_security_policy_nonce_directives = %w(script-src) + # Generate session nonces for permitted importmap and inline scripts + # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } + # config.content_security_policy_nonce_directives = %w(script-src) -# Report CSP violations to a specified URI -# For further information see the following documentation: -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only -# Rails.application.config.content_security_policy_report_only = true + # Report violations without enforcing the policy. + # config.content_security_policy_report_only = true +end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 7a4f47b4..3df77c5b 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -2,5 +2,9 @@ # Be sure to restart your server when you modify this file. -# Configure sensitive parameters which will be filtered from the log file. -Rails.application.config.filter_parameters += [:password] +# Configure parameters to be filtered from the log file. Use this to limit dissemination of +# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported +# notations and behaviors. +Rails.application.config.filter_parameters += %i[ + passw secret token _key crypt salt certificate otp ssn +] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index aa7435fb..9e049dcc 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,17 +1,18 @@ # frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.plural /^(ox)$/i, '\1en' -# inflect.singular /^(ox)en/i, '\1' -# inflect.irregular 'person', 'people' +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.acronym 'RESTful' +# inflect.acronym "RESTful" # end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 6e1d16f0..be6fedc5 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: diff --git a/config/initializers/new_framework_defaults_7_0.rb b/config/initializers/new_framework_defaults_7_0.rb new file mode 100644 index 00000000..8598c655 --- /dev/null +++ b/config/initializers/new_framework_defaults_7_0.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +# rubocop:disable Layout/LineLength + +# Be sure to restart your server when you modify this file. +# +# This file eases your Rails 7.0 framework defaults upgrade. +# +# Uncomment each configuration one by one to switch to the new default. +# Once your application is ready to run with all new defaults, you can remove +# this file and set the `config.load_defaults` to `7.0`. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. +# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html + +# `button_to` view helper will render `