Security scans
Known problems in the gems
Lesson 60 wired bundler-audit into the pipeline. It answers one question: does a gem in the lock file have a public advisory? The database is the valuable part, and --update is what keeps the answer current.
bundler-audit check --update
ruby-advisory-db: advisories: 1351 advisories last updated: 2026-11-04 09:42:55 -0500 No vulnerabilities found
The same step sits in the ci task, so the pipeline fails on a new advisory even when no code changed.
Secrets in your own code
No database knows about a token that someone typed into a file. A short script covers that case: it walks the Ruby files and prints a line when an assignment looks like a credential.
PATTERN = /(password|secret|token)\s*=\s*["'][^"']+["']/i
Dir["**/*.rb"].each do |file|
next if file.start_with?("vendor/")
File.readlines(file).each_with_index do |line, index|
puts "#{file}:#{index + 1}: #{line.strip}" if line =~ PATTERN
end
end
Run it on the clean project
ruby scan.rb
No output: the project has no hardcoded credential. Add one and the next run says where:
ruby scan.rb
lib/config.rb:1: API_TOKEN = "abc123"
The scanner is not clever: it reads one line at a time and it does not parse Ruby. That is enough to catch the usual accident, a token pasted into a file instead of read from the environment, and it costs a second in the pipeline.
The scan job
The ci-demo project is a library, not a Rails application, so its scan job has two steps: the secret scan is fast and runs first, the advisory check downloads the database and runs second.
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true
- run: ruby scan.rb
- run: bundle exec bundler-audit check --update
A Rails scanner does not belong here. Brakeman looks for a Rails application and stops when it does not find one, which is the next section.
Brakeman is for Rails
Brakeman reads the controllers, views and routes of a Rails application and reports the usual mistakes: SQL built from strings, mass assignment, unescaped output, unsafe redirects. On a project that is not Rails it refuses to guess:
brakeman -q
Please supply the path to a Rails application (looking in /app).
Use `--force` to run a scan anyway.
That run exits with code 4. The three codes a pipeline cares about:
- 0: the scan ran and found nothing to report.
- 3: the scan ran and found warnings.
- 4: the path is not a Rails application, so the scan did not run.
The first two runs below were made on the host in the ruby:3.4 image; the project was a fresh rails new blog application.
A Rails application
A fresh application has nothing to report, and the report says so:
brakeman -q
== Overview ==
Controllers: 1
Models: 1
Templates: 2
Errors: 0
Security Warnings: 0
== Warning Types ==
No warnings found
Add a controller that builds SQL from a parameter, with a model behind it (bin/rails generate model User name:string), and the same command finds it:
class UsersController < ApplicationController
def search
@users = User.where("name = '#{params[:name]}'")
end
end
brakeman -q
== Overview ==
Controllers: 2
Models: 2
Templates: 2
Errors: 0
Security Warnings: 1
== Warning Types ==
SQL Injection: 1
== Warnings ==
Confidence: High
Category: SQL Injection
Check: SQL
Message: Possible SQL injection
Code: User.where("name = '#{params[:name]}'")
File: app/controllers/users_controller.rb
Line: 3
This run exits with code 3, so a pipeline step that calls
brakeman -q fails on it. The file and the line are
in the report, which is the point of the report.
The fix is the usual Rails answer: pass the value as a parameter and let
the query builder quote it. The host did not run this version, so it has
no output here:
class UsersController < ApplicationController
def search
@users = User.where(name: params[:name])
end
end
A Rails project gets its own scan job: the same two steps as ci-demo, plus Brakeman at the end, where it can see the application.
- run: ruby scan.rb
- run: bundle exec bundler-audit check --update
- run: bundle exec brakeman -q
What to watch for
Run each scanner where it makes sense. A library runs the advisory check
and the secret scan; a Rails application adds Brakeman. Running Brakeman
on a library fails with code 4 every time, and a job that fails on every
run is a job the team learns to ignore.
A scan that fails on day one teaches the same lesson. Run the three
commands on the project first, fix or record what they find, and only
then add them to the pipeline. Keep the scanners current: the advisory
database changes every day and Brakeman learns new Rails patterns with
every release.
Next: Deploying from a pipeline
Article author: Arthur Isaev