Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### Unreleased

* Added support for expiring advisory ignores in `.bundler-audit.yml`.

### 0.9.3 / 2025-11-28

* Officially support Ruby 3.4, 3.5, and 4.0.
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,14 @@ bundler-audit also supports a per-project configuration file:
---
ignore:
- CVE-YYYY-XXXX
- ...
- id: CVE-YYYY-ZZZZ
until: 2026-08-31
```

* `ignore:` \[Array\<String\>\] - A list of advisory IDs to ignore.
* `ignore:` \[Array\<String, Hash\>\] - A list of advisory IDs to ignore.
String entries are ignored indefinitely. A timed entry must contain an `id`
and an ISO 8601 `until` date (`YYYY-MM-DD`); the advisory is ignored through
that date and reported again the following day.

You can provide a path to a config file using the `--config` flag:

Expand Down
87 changes: 77 additions & 10 deletions lib/bundler/audit/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#

require 'yaml'
require 'date'
require 'set'

module Bundler
Expand Down Expand Up @@ -72,35 +73,101 @@ def self.load(file_path)
raise(InvalidConfigurationError,"'ignore' key found in config file, but is not an Array")
end

unless value.children.all? { |node| node.is_a?(YAML::Nodes::Scalar) }
raise(InvalidConfigurationError,"'ignore' array in config file contains a non-String")
end

config[:ignore] = value.children.map(&:value)
config[:ignore] = value.children.map { |node| parse_ignore(node) }
end
end

new(config)
end

#
# The list of advisory IDs to ignore.
# Parses and validates an entry in the `ignore` array.
#
# @param [YAML::Nodes::Node] node
#
# @return [String, Hash]
#
# @api private
#
def self.parse_ignore(node)
return node.value if node.is_a?(YAML::Nodes::Scalar)

unless node.is_a?(YAML::Nodes::Mapping)
raise(InvalidConfigurationError,"'ignore' array contains an invalid entry")
end

entry = {}

node.children.each_slice(2) do |key,value|
unless key.is_a?(YAML::Nodes::Scalar) && value.is_a?(YAML::Nodes::Scalar)
raise(InvalidConfigurationError,"timed 'ignore' entries must contain String values")
end

case key.value
when 'id'
entry[:id] = value.value
when 'until'
entry[:until] = value.value
else
raise(InvalidConfigurationError,"unknown key #{key.value.inspect} in timed 'ignore' entry")
end
end

unless entry[:id] && entry[:until]
raise(InvalidConfigurationError,"timed 'ignore' entries require both 'id' and 'until'")
end

unless entry[:until] =~ /\A\d{4}-\d{2}-\d{2}\z/
raise(InvalidConfigurationError,"'until' in timed 'ignore' entry must be an ISO 8601 date (YYYY-MM-DD)")
end

begin
entry[:until] = Date.iso8601(entry[:until])
rescue ArgumentError
raise(InvalidConfigurationError,"'until' in timed 'ignore' entry must be a valid date")
end

entry
end
private_class_method :parse_ignore

#
# The set of advisory IDs which are currently ignored.
#
# @return [Set<String>]
#
attr_reader :ignore
def ignore
ignored = @ignore.dup
today = Date.today

@timed_ignores.each do |id,ignore_until|
ignored << id if ignore_until >= today
end

ignored
end

#
# Initializes the configuration.
#
# @param [Hash] config
# The configuration hash.
#
# @option config [Array<String>] :ignore
# The list of advisory IDs to ignore.
# @option config [Array<String, Hash>] :ignore
# The list of advisory IDs to ignore, optionally through a specific
# date.
#
def initialize(config={})
@ignore = Set.new(config[:ignore])
@ignore = Set.new
@timed_ignores = {}

Array(config[:ignore]).each do |entry|
if entry.is_a?(Hash)
@timed_ignores[entry[:id]] = entry[:until]
else
@ignore << entry
end
end
end

end
Expand Down
60 changes: 60 additions & 0 deletions spec/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@
let(:path) { File.join(fixtures_dir,'valid.yml') }

it { should be_a(described_class) }

it "includes timed ignores which have not expired" do
expect(subject.ignore).to include('CVE-789')
end

it "excludes timed ignores which have expired" do
expect(subject.ignore).not_to include('CVE-EXPIRED')
end
end

context "validations" do
Expand Down Expand Up @@ -52,6 +60,39 @@
expect { subject }.to raise_error(described_class::InvalidConfigurationError)
end
end

describe "when a timed ignore is missing until" do
let(:path) { File.join(fixtures_dir,'bad','timed_ignore_missing_until.yml') }

it "raises a validation error" do
expect { subject }.to raise_error(
described_class::InvalidConfigurationError,
/require both 'id' and 'until'/
)
end
end

describe "when a timed ignore has an invalid date" do
let(:path) { File.join(fixtures_dir,'bad','timed_ignore_invalid_date.yml') }

it "raises a validation error" do
expect { subject }.to raise_error(
described_class::InvalidConfigurationError,
/must be a valid date/
)
end
end

describe "when a timed ignore has an unknown key" do
let(:path) { File.join(fixtures_dir,'bad','timed_ignore_unknown_key.yml') }

it "raises a validation error" do
expect { subject }.to raise_error(
described_class::InvalidConfigurationError,
/unknown key "expires"/
)
end
end
end
end
end
Expand All @@ -74,5 +115,24 @@
expect(subject.ignore).to be == Set.new(advisory_ids)
end
end

context "when given timed ignores" do
subject do
described_class.new(
ignore: [
{id: 'CVE-ACTIVE', until: Date.today},
{id: 'CVE-EXPIRED', until: Date.today - 1}
]
)
end

it "ignores an advisory through its until date" do
expect(subject.ignore).to include('CVE-ACTIVE')
end

it "does not ignore an advisory after its until date" do
expect(subject.ignore).not_to include('CVE-EXPIRED')
end
end
end
end
4 changes: 4 additions & 0 deletions spec/fixtures/config/bad/timed_ignore_invalid_date.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
ignore:
- id: CVE-123
until: 2026-02-30
3 changes: 3 additions & 0 deletions spec/fixtures/config/bad/timed_ignore_missing_until.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
ignore:
- id: CVE-123
4 changes: 4 additions & 0 deletions spec/fixtures/config/bad/timed_ignore_unknown_key.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
ignore:
- id: CVE-123
expires: 2026-12-31
4 changes: 4 additions & 0 deletions spec/fixtures/config/valid.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
ignore:
- CVE-123
- CVE-456
- id: CVE-789
until: 2999-12-31
- id: CVE-EXPIRED
until: 2000-01-01
38 changes: 38 additions & 0 deletions spec/scanner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,44 @@
expect(ids).not_to include('CVE-2013-0156')
end
end

context "with a timed ignore in the configuration" do
let(:scanner) { described_class.new(directory) }

subject { scanner.scan.to_a }

before do
allow(scanner).to receive(:config).and_return(config)
end

context "when the ignore is still active" do
let(:config) do
Configuration.new(
ignore: [{id: 'CVE-2013-0155', until: Date.today}]
)
end

it "ignores the advisory" do
ids = subject.flat_map { |result| result.advisory.identifiers }

expect(ids).not_to include('CVE-2013-0155')
end
end

context "when the ignore has expired" do
let(:config) do
Configuration.new(
ignore: [{id: 'CVE-2013-0155', until: Date.today - 1}]
)
end

it "reports the advisory again" do
ids = subject.flat_map { |result| result.advisory.identifiers }

expect(ids).to include('CVE-2013-0155')
end
end
end
end

context "when auditing a bundle with insecure sources" do
Expand Down