Skip to content

Commit 46e148a

Browse files
mlarrazclaude
andcommitted
Add exclude option to skip gems by name during audit
Allow users to exclude entire gems from scanning via config file (`exclude:` key in .bundler-audit.yml) or CLI (`--exclude`/`-e` flag). Unlike `ignore` which skips specific advisory IDs after lookup, `exclude` skips gems entirely before any advisory database check. This is useful for Rails apps that bundle gems they don't actually use (e.g. activestorage, actiontext) where every new CVE triggers a false audit failure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f8b06eb commit 46e148a

10 files changed

Lines changed: 125 additions & 2 deletions

File tree

lib/bundler/audit/cli.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class CLI < ::Thor
3737
method_option :quiet, type: :boolean, aliases: '-q'
3838
method_option :verbose, type: :boolean, aliases: '-v'
3939
method_option :ignore, type: :array, aliases: '-i'
40+
method_option :exclude, type: :array, aliases: '-e'
4041
method_option :update, type: :boolean, aliases: '-u'
4142
method_option :database, type: :string, aliases: '-D',
4243
default: Database::DEFAULT_PATH
@@ -73,7 +74,7 @@ def check(dir=Dir.pwd)
7374
exit 1
7475
end
7576

76-
report = scanner.report(ignore: options.ignore)
77+
report = scanner.report(ignore: options.ignore, exclude: options.exclude)
7778

7879
output = if options[:output]
7980
File.new(options[:output],'w')

lib/bundler/audit/configuration.rb

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ def self.load(file_path)
7777
end
7878

7979
config[:ignore] = value.children.map(&:value)
80+
when 'exclude'
81+
unless value.is_a?(YAML::Nodes::Sequence)
82+
raise(InvalidConfigurationError,"'exclude' key found in config file, but is not an Array")
83+
end
84+
85+
unless value.children.all? { |node| node.is_a?(YAML::Nodes::Scalar) }
86+
raise(InvalidConfigurationError,"'exclude' array in config file contains a non-String")
87+
end
88+
89+
config[:exclude] = value.children.map(&:value)
8090
end
8191
end
8292

@@ -90,6 +100,13 @@ def self.load(file_path)
90100
#
91101
attr_reader :ignore
92102

103+
#
104+
# The list of gem names to exclude from scanning.
105+
#
106+
# @return [Set<String>]
107+
#
108+
attr_reader :exclude
109+
93110
#
94111
# Initializes the configuration.
95112
#
@@ -99,8 +116,12 @@ def self.load(file_path)
99116
# @option config [Array<String>] :ignore
100117
# The list of advisory IDs to ignore.
101118
#
119+
# @option config [Array<String>] :exclude
120+
# The list of gem names to exclude from scanning.
121+
#
102122
def initialize(config={})
103-
@ignore = Set.new(config[:ignore])
123+
@ignore = Set.new(config[:ignore])
124+
@exclude = Set.new(config[:exclude])
104125
end
105126

106127
end

lib/bundler/audit/scanner.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ def initialize(root=Dir.pwd,gemfile_lock='Gemfile.lock',database=Database.new,co
104104
# @option options [Array<String>] :ignore
105105
# The advisories to ignore.
106106
#
107+
# @option options [Array<String>] :exclude
108+
# The gem names to exclude from scanning.
109+
#
107110
# @yield [result]
108111
# The given block will be passed the results of the scan.
109112
#
@@ -134,6 +137,9 @@ def report(options={})
134137
# @option options [Array<String>] :ignore
135138
# The advisories to ignore.
136139
#
140+
# @option options [Array<String>] :exclude
141+
# The gem names to exclude from scanning.
142+
#
137143
# @yield [result]
138144
# The given block will be passed the results of the scan.
139145
#
@@ -202,6 +208,9 @@ def scan_sources(options={})
202208
# @option options [Array<String>] :ignore
203209
# The advisories to ignore.
204210
#
211+
# @option options [Array<String>] :exclude
212+
# The gem names to exclude from scanning.
213+
#
205214
# @yield [result]
206215
# The given block will be passed the results of the scan.
207216
#
@@ -224,7 +233,15 @@ def scan_specs(options={})
224233
config.ignore
225234
end
226235

236+
exclude = if options[:exclude]
237+
Set.new(options[:exclude])
238+
else
239+
config.exclude
240+
end
241+
227242
@lockfile.specs.each do |gem|
243+
next if exclude.include?(gem.name)
244+
228245
@database.check_gem(gem) do |advisory|
229246
is_ignored = ignore.intersect?(advisory.identifiers.to_set)
230247
next if is_ignored
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
exclude:
3+
- activerecord
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
source 'https://rubygems.org'
2+
3+
gem 'activerecord', '3.2.10'

spec/configuration_spec.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,30 @@
5353
end
5454
end
5555
end
56+
57+
context "when exclude is not an array" do
58+
let(:path) { File.join(fixtures_dir,'bad','exclude_is_not_an_array.yml') }
59+
60+
it 'raises a validation error' do
61+
expect { subject }.to raise_error(described_class::InvalidConfigurationError)
62+
end
63+
end
64+
65+
context 'when exclude is an array' do
66+
context 'when exclude only contains strings' do
67+
let(:path) { File.join(fixtures_dir,'valid_with_exclude.yml') }
68+
69+
it { should be_a(described_class) }
70+
end
71+
72+
describe "when exclude contains non-strings" do
73+
let(:path) { File.join(fixtures_dir,'bad','exclude_contains_a_non_string.yml') }
74+
75+
it "raises a validation error" do
76+
expect { subject }.to raise_error(described_class::InvalidConfigurationError)
77+
end
78+
end
79+
end
5680
end
5781
end
5882

@@ -62,6 +86,11 @@
6286
expect(subject.ignore).to be_kind_of(Set)
6387
expect(subject.ignore).to be_empty
6488
end
89+
90+
it "must set @exclude to an empty Set" do
91+
expect(subject.exclude).to be_kind_of(Set)
92+
expect(subject.exclude).to be_empty
93+
end
6594
end
6695

6796
context "when given :ignore" do
@@ -74,5 +103,16 @@
74103
expect(subject.ignore).to be == Set.new(advisory_ids)
75104
end
76105
end
106+
107+
context "when given :exclude" do
108+
let(:gem_names) { %w[activestorage actiontext] }
109+
110+
subject { described_class.new(exclude: gem_names) }
111+
112+
it "must initialize @exclude to contain :exclude" do
113+
expect(subject.exclude).to be_kind_of(Set)
114+
expect(subject.exclude).to be == Set.new(gem_names)
115+
end
116+
end
77117
end
78118
end
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
exclude:
3+
- activestorage
4+
- hello: world
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
exclude:
3+
foo: bar
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
ignore:
3+
- CVE-123
4+
- CVE-456
5+
exclude:
6+
- activestorage
7+
- actiontext

spec/scanner_spec.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@
4444
expect(ids).not_to include('CVE-2013-0156')
4545
end
4646
end
47+
48+
context "when the :exclude option is given" do
49+
subject { super().scan(exclude: ['activerecord']) }
50+
51+
it "should not include results for the excluded gem" do
52+
gem_names = subject.map { |result| result.gem.name }
53+
54+
expect(gem_names).not_to include('activerecord')
55+
end
56+
end
4757
end
4858

4959
context "when auditing a bundle with insecure sources" do
@@ -67,6 +77,20 @@
6777
end
6878
end
6979

80+
context "when the exclude option is configured in .bundler-audit.yml" do
81+
let(:bundle) { 'unpatched_gems_with_exclude_configuration' }
82+
let(:directory) { File.join('spec','bundle',bundle) }
83+
let(:scanner) { described_class.new(directory) }
84+
85+
subject { scanner.scan }
86+
87+
it "should not include results for the excluded gem" do
88+
gem_names = subject.map { |result| result.gem.name }
89+
90+
expect(gem_names).not_to include('activerecord')
91+
end
92+
end
93+
7094
context "when the ignore option is configured in .bundler-audit.yml" do
7195
let(:bundle) { 'unpatched_gems_with_dot_configuration' }
7296
let(:directory) { File.join('spec','bundle',bundle) }

0 commit comments

Comments
 (0)