diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..fd67bfb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,27 @@ +# Dependabot: dependency updates + security alerts +# https://docs.github.com/en/code-security/dependabot +version: 2 +updates: + # Ruby / Bundler + - package-ecosystem: "bundler" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "dependencies" + commit-message: + prefix: "chore(deps)" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "chore(ci)" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..d716dd8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,35 @@ +# CodeQL: security and code quality analysis (GitHub Advanced Security) +# https://docs.github.com/en/code-security/code-scanning +name: CodeQL + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + schedule: + # Weekly run (optional) + - cron: '0 0 * * 1' + +jobs: + analyze: + name: Analyze (Ruby) + runs-on: ubuntu-latest + permissions: + security-events: write + actions: read + contents: read + steps: + - uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ruby + # Optional: use security-extended for more security queries + queries: security-extended + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:ruby" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..2f5a19f --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,20 @@ +# Lint: RuboCop and style checks +name: Lint + +on: + pull_request: + push: + branches: [main, master] + +jobs: + rubocop: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + - name: Run RuboCop + run: bundle exec rubocop diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..13600f1 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,35 @@ +# Security: dependency review (PRs) + bundler-audit for known gem vulnerabilities +name: Security + +on: + pull_request: + branches: [main, master] + push: + branches: [main, master] + +jobs: + dependency-review: + name: Dependency review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + - name: Dependency Review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high + + bundler-audit: + name: Bundler audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + - name: Install bundler-audit + run: gem install bundler-audit + - name: Update vulnerability DB and run audit + run: bundler-audit check --update diff --git a/.rubocop.yml b/.rubocop.yml index 680181b..0b1638b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,19 +1,20 @@ Style/StringLiterals: Enabled: false AllCops: + NewCops: enable Exclude: - 'rubyipmi.gemspec' -SignalException: +Style/SignalException: EnforcedStyle: only_raise -Documentation: +Style/Documentation: Enabled: false -ClassAndModuleChildren: +Style/ClassAndModuleChildren: Enabled: false -HashSyntax: +Style/HashSyntax: EnforcedStyle: hash_rockets -ClassCheck: +Style/ClassCheck: EnforcedStyle: kind_of? -SpaceInsideHashLiteralBraces: +Layout/SpaceInsideHashLiteralBraces: EnforcedStyle: no_space Style/WordArray: Enabled: false @@ -24,14 +25,22 @@ Metrics/AbcSize: Max: 60 Metrics/CyclomaticComplexity: Max: 19 -Metrics/LineLength: + Exclude: + - 'lib/rubyipmi.rb' +Layout/LineLength: Max: 149 Metrics/MethodLength: Max: 44 + Exclude: + - 'lib/rubyipmi.rb' Metrics/ModuleLength: Max: 128 + Exclude: + - 'lib/rubyipmi.rb' Metrics/PerceivedComplexity: Max: 22 + Exclude: + - 'lib/rubyipmi.rb' Style/SpecialGlobalVars: Exclude: - 'lib/rubyipmi.rb' @@ -44,23 +53,61 @@ Style/RescueModifier: Style/RegexpLiteral: Exclude: - 'spec/spec_helper.rb' -Style/PredicateName: +Naming/PredicatePrefix: Exclude: - 'lib/rubyipmi.rb' Lint/RescueException: Exclude: - 'spec/integration/rubyipmi_spec.rb' -Style/AccessorMethodName: +Naming/AccessorMethodName: Exclude: - 'lib/rubyipmi/freeipmi/connection.rb' - 'lib/rubyipmi/ipmitool/connection.rb' # Fixed by deprecation -Style/MethodName: +Naming/MethodName: Exclude: - 'lib/rubyipmi/commands/mixins/power_mixin.rb' # Don't understand -Style/FileName: +Naming/FileName: Exclude: - 'spec/unit/freeipmi/bmc-info_spec.rb' + +# Spec files often have long describe blocks and intentional patterns +Metrics/BlockLength: + Exclude: + - 'spec/**/*_spec.rb' + - 'spec/**/*.rb' +Style/OptionalBooleanParameter: + Enabled: false +Lint/DuplicateMethods: + Exclude: + - 'lib/rubyipmi/freeipmi/commands/bmc.rb' + - 'lib/rubyipmi/freeipmi/commands/fru.rb' + - 'lib/rubyipmi/freeipmi/commands/lan.rb' + - 'lib/rubyipmi/ipmitool/commands/fru.rb' + - 'lib/rubyipmi/ipmitool/commands/lan.rb' +Style/MissingRespondToMissing: + Exclude: + - 'lib/rubyipmi/commands/mixins/sensors_mixin.rb' + - 'lib/rubyipmi/freeipmi/commands/fru.rb' + - 'lib/rubyipmi/ipmitool/commands/fru.rb' +Lint/MissingSuper: + Exclude: + - 'lib/rubyipmi.rb' + - 'lib/rubyipmi/freeipmi/commands/fru.rb' + - 'lib/rubyipmi/freeipmi/commands/sensors.rb' + - 'lib/rubyipmi/ipmitool/commands/fru.rb' + - 'lib/rubyipmi/ipmitool/commands/sensors.rb' +Lint/EmptyBlock: + Exclude: + - 'spec/**/*_spec.rb' +Lint/EmptyFile: + Exclude: + - 'spec/unit/freeipmi/lan_spec.rb' +Lint/Void: + Exclude: + - 'lib/rubyipmi/ipmitool/commands/lan.rb' +Naming/PredicateMethod: + Enabled: false diff --git a/Gemfile b/Gemfile index 6114d75..174598e 100644 --- a/Gemfile +++ b/Gemfile @@ -1,11 +1,13 @@ -source "http://rubygems.org" +# frozen_string_literal: true + +source "https://rubygems.org" gemspec group :development do - gem 'coveralls_reborn', require: false + gem 'coveralls_reborn', :require => false gem 'pry' gem 'pry-rescue' - gem "rubocop", :require => false gem "reline" + gem "rubocop", :require => false end diff --git a/README.md b/README.md index 06bb57e..c633a2f 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,18 @@ vagrant ssh -c "/rubyipmi/rake integration ipmiuser=... ipmipass=... ipmihost=.. **CI:** The repo uses GitHub Actions; see `.github/workflows/test.yml`. Typical flow: checkout → Ruby 3.x → `bundle install` → `bundle exec rake unit` → `gem build`. +### CI, security, and automation + +| Workflow | Purpose | +|----------|---------| +| [test.yml](.github/workflows/test.yml) | Unit tests and gem build (Ruby 3.0–3.4) | +| [lint.yml](.github/workflows/lint.yml) | RuboCop style and lint checks | +| [codeql.yml](.github/workflows/codeql.yml) | CodeQL security and code-quality analysis | +| [security.yml](.github/workflows/security.yml) | Dependency review (PRs) and `bundler-audit` for gem vulnerabilities | + +- **Dependabot** (`.github/dependabot.yml`): weekly dependency and GitHub Actions updates; security alerts appear in the **Security** tab. +- **GitHub Copilot Code Review:** For automatic AI-assisted reviews on pull requests, enable **Rules → Rulesets** in the repo **Settings**, add a rule, and choose **Require a review from GitHub Copilot**. You can enable “Run on each push” and “Run on drafts” there. (Requires a Copilot-enabled account.) + --- ### Extending the library diff --git a/Rakefile b/Rakefile index ef92eae..c05998f 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,5 @@ -# encoding: utf-8 +# frozen_string_literal: true + require 'bundler/gem_tasks' @base_dir = File.dirname(__FILE__) @@ -6,8 +7,8 @@ require 'bundler/gem_tasks' begin Bundler.setup(:default, :development) rescue Bundler::BundlerError => e - $stderr.puts e.message - $stderr.puts "Run `bundle install` to install missing gems" + warn e.message + warn "Run `bundle install` to install missing gems" exit e.status_code end require 'rake' @@ -32,7 +33,7 @@ RSpec::Core::RakeTask.new :integration do |spec| ENV['ipmiuser'] = 'admin' ENV['ipmipass'] = 'password' ENV['ipmihost'] = '10.0.1.16' - providers ||= Array(ENV['ipmiprovider']) || ['freeipmi', 'ipmitool'] + providers ||= Array(ENV.fetch('ipmiprovider', nil)) || ['freeipmi', 'ipmitool'] providers.each do |provider| ENV['ipmiprovider'] = provider @@ -59,9 +60,8 @@ task :send_diag, :user, :pass, :host do |_t, args| require 'json' require "highline/import" - if args.count < 3 - raise "You must provide arguments: rake send_diag[user, pass, host]" - end + raise "You must provide arguments: rake send_diag[user, pass, host]" if args.count < 3 + data = Rubyipmi.get_diag(args[:user], args[:pass], args[:host]) emailto = 'corey@logicminds.biz' subject = "Rubyipmi diagnostics data" @@ -78,14 +78,14 @@ def send_email(to, data, opts = {}) opts[:body] ||= data opts[:to] ||= to opts[:port] ||= 587 - msg = < -To: <#{to}> -Subject: #{opts[:subject]} -Date: #{Time.now.rfc2822} - - #{opts[:body]} -END_OF_MESSAGE + msg = <<~END_OF_MESSAGE + From: #{opts[:from_alias]} <#{opts[:from]}> + To: <#{to}> + Subject: #{opts[:subject]} + Date: #{Time.now.rfc2822} + + #{opts[:body]} + END_OF_MESSAGE smtp = Net::SMTP.new(opts[:server], opts[:port]) smtp.enable_starttls