From bf5fbee2c17eb8e95e9e1a5363db77173b780d04 Mon Sep 17 00:00:00 2001 From: Jonathan Swartz Date: Fri, 10 Jul 2026 12:23:11 -0700 Subject: [PATCH 1/2] add inherit option, allowing other config files to be merged into the current config --- ChangeLog.md | 5 ++ README.md | 5 ++ lib/bundler/audit/configuration.rb | 54 ++++++++++++++-- spec/configuration_spec.rb | 64 +++++++++++++++++++ .../bad/inherit_contains_a_non_string.yml | 2 + spec/fixtures/config/bad/inherit_cycle_a.yml | 2 + spec/fixtures/config/bad/inherit_cycle_b.yml | 2 + .../config/bad/inherit_is_not_an_array.yml | 1 + .../config/bad/inherit_missing_target.yml | 2 + spec/fixtures/config/inherit/base.yml | 3 + spec/fixtures/config/inherit/child.yml | 4 ++ spec/fixtures/config/inherit/grandchild.yml | 4 ++ spec/fixtures/config/inherit/middle.yml | 4 ++ 13 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 spec/fixtures/config/bad/inherit_contains_a_non_string.yml create mode 100644 spec/fixtures/config/bad/inherit_cycle_a.yml create mode 100644 spec/fixtures/config/bad/inherit_cycle_b.yml create mode 100644 spec/fixtures/config/bad/inherit_is_not_an_array.yml create mode 100644 spec/fixtures/config/bad/inherit_missing_target.yml create mode 100644 spec/fixtures/config/inherit/base.yml create mode 100644 spec/fixtures/config/inherit/child.yml create mode 100644 spec/fixtures/config/inherit/grandchild.yml create mode 100644 spec/fixtures/config/inherit/middle.yml diff --git a/ChangeLog.md b/ChangeLog.md index b8287b8b..392a0e3f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,8 @@ +### 0.9.4 / TBD + +* Added `inherit:` config option, which merges additional config files + into the current config. + ### 0.9.3 / 2025-11-28 * Officially support Ruby 3.4, 3.5, and 4.0. diff --git a/README.md b/README.md index ac6d4fd7..5d78e1b4 100644 --- a/README.md +++ b/README.md @@ -176,12 +176,17 @@ bundler-audit also supports a per-project configuration file: ```yaml --- +inherit: + - ../base.yml ignore: - CVE-YYYY-XXXX - ... ``` * `ignore:` \[Array\\] - A list of advisory IDs to ignore. +* `inherit:` \[Array\\] - An optional list of other config + files to merge into this one. Relative paths are resolved from the + directory of the declaring config file. You can provide a path to a config file using the `--config` flag: diff --git a/lib/bundler/audit/configuration.rb b/lib/bundler/audit/configuration.rb index a7fabb66..7496e104 100644 --- a/lib/bundler/audit/configuration.rb +++ b/lib/bundler/audit/configuration.rb @@ -51,9 +51,39 @@ class FileNotFound < StandardError # file passed. # def self.load(file_path) - raise(FileNotFound,"Configuration file '#{file_path}' does not exist") unless File.exist?(file_path) + load_with_chain(file_path,[]) + end - doc = YAML.parse(File.new(file_path)) + # + # Internal loader that tracks the ancestor chain of absolute paths in + # order to detect cycles across `inherit:` links. + # + # @param [String] file_path + # Path to the YAML file holding the configuration. + # + # @param [Array] ancestors + # Absolute paths of configuration files already being loaded further up + # the recursion. Used only to detect cycles. + # + # @raise [FileNotFound] + # @raise [InvalidConfigurationError] + # + # @return [Configuration] + # + # @api private + # + def self.load_with_chain(file_path,ancestors) + absolute_path = File.expand_path(file_path) + + if ancestors.include?(absolute_path) + raise(InvalidConfigurationError,"Cycle detected in 'inherit': #{(ancestors + [absolute_path]).join(' -> ')}") + end + + unless File.exist?(absolute_path) + raise(FileNotFound,"Configuration file '#{file_path}' does not exist") + end + + doc = YAML.parse(File.new(absolute_path)) unless doc.kind_of?(YAML::Nodes::Document) raise(InvalidConfigurationError,"Configuration found in '#{file_path}' is not YAML") @@ -63,7 +93,7 @@ def self.load(file_path) raise(InvalidConfigurationError,"Configuration found in '#{file_path}' is not a Hash") end - config = {} + config = { ignore: [] } doc.root.children.each_slice(2) do |key,value| case key.value @@ -76,12 +106,28 @@ def self.load(file_path) raise(InvalidConfigurationError,"'ignore' array in config file contains a non-String") end - config[:ignore] = value.children.map(&:value) + config[:ignore].concat(value.children.map(&:value)) + when 'inherit' + unless value.is_a?(YAML::Nodes::Sequence) + raise(InvalidConfigurationError,"'inherit' key found in config file, but is not an Array") + end + + unless value.children.all? { |node| node.is_a?(YAML::Nodes::Scalar) } + raise(InvalidConfigurationError,"'inherit' array in config file contains a non-String") + end + + base_dir = File.dirname(absolute_path) + value.children.each do |child_node| + inherited_path = File.expand_path(child_node.value,base_dir) + parent_config = load_with_chain(inherited_path,ancestors + [absolute_path]) + config[:ignore].concat(parent_config.ignore.to_a) + end end end new(config) end + private_class_method :load_with_chain # # The list of advisory IDs to ignore. diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb index 0923b45d..4040cef9 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -53,6 +53,70 @@ end end end + + context "when inherit is not an Array" do + let(:path) { File.join(fixtures_dir,'bad','inherit_is_not_an_array.yml') } + + it "raises a validation error" do + expect { subject }.to raise_error(described_class::InvalidConfigurationError, + /'inherit' key found in config file, but is not an Array/) + end + end + + context "when inherit array contains a non-String" do + let(:path) { File.join(fixtures_dir,'bad','inherit_contains_a_non_string.yml') } + + it "raises a validation error" do + expect { subject }.to raise_error(described_class::InvalidConfigurationError, + /'inherit' array in config file contains a non-String/) + end + end + + context "when an inherited file does not exist" do + let(:path) { File.join(fixtures_dir,'bad','inherit_missing_target.yml') } + + it "raises a FileNotFound error" do + expect { subject }.to raise_error(described_class::FileNotFound, + /Configuration file '.*does_not_exist\.yml' does not exist/) + end + end + + context "when the inherit chain forms a cycle" do + let(:path) { File.join(fixtures_dir,'bad','inherit_cycle_a.yml') } + + it "raises a validation error identifying the cycle" do + expect { subject }.to raise_error(described_class::InvalidConfigurationError, + /Cycle detected in 'inherit'/) + end + end + end + + context "when a file inherits another" do + let(:path) { File.join(fixtures_dir,'inherit','child.yml') } + + it { should be_a(described_class) } + + it "must merge the parent's ignore list with the child's" do + expect(subject.ignore).to eq(Set.new(%w[CVE-BASE-1 CVE-BASE-2 CVE-CHILD-1])) + end + end + + context "when the inherit chain is 3 deep" do + let(:path) { File.join(fixtures_dir,'inherit','grandchild.yml') } + + it "must transitively merge the ignore lists" do + expect(subject.ignore).to eq(Set.new(%w[CVE-BASE-1 CVE-BASE-2 CVE-MIDDLE-1 CVE-GRANDCHILD-1])) + end + end + + context "when the inherit path is relative" do + let(:path) { File.join(fixtures_dir,'inherit','child.yml') } + + it "must resolve inherited paths relative to the including file" do + Dir.chdir('/tmp') do + expect(subject.ignore).to eq(Set.new(%w[CVE-BASE-1 CVE-BASE-2 CVE-CHILD-1])) + end + end end end diff --git a/spec/fixtures/config/bad/inherit_contains_a_non_string.yml b/spec/fixtures/config/bad/inherit_contains_a_non_string.yml new file mode 100644 index 00000000..a0454ae3 --- /dev/null +++ b/spec/fixtures/config/bad/inherit_contains_a_non_string.yml @@ -0,0 +1,2 @@ +inherit: + - foo: bar diff --git a/spec/fixtures/config/bad/inherit_cycle_a.yml b/spec/fixtures/config/bad/inherit_cycle_a.yml new file mode 100644 index 00000000..5f6277d8 --- /dev/null +++ b/spec/fixtures/config/bad/inherit_cycle_a.yml @@ -0,0 +1,2 @@ +inherit: + - ./inherit_cycle_b.yml diff --git a/spec/fixtures/config/bad/inherit_cycle_b.yml b/spec/fixtures/config/bad/inherit_cycle_b.yml new file mode 100644 index 00000000..e6d932e1 --- /dev/null +++ b/spec/fixtures/config/bad/inherit_cycle_b.yml @@ -0,0 +1,2 @@ +inherit: + - ./inherit_cycle_a.yml diff --git a/spec/fixtures/config/bad/inherit_is_not_an_array.yml b/spec/fixtures/config/bad/inherit_is_not_an_array.yml new file mode 100644 index 00000000..93d8fcaa --- /dev/null +++ b/spec/fixtures/config/bad/inherit_is_not_an_array.yml @@ -0,0 +1 @@ +inherit: some_string diff --git a/spec/fixtures/config/bad/inherit_missing_target.yml b/spec/fixtures/config/bad/inherit_missing_target.yml new file mode 100644 index 00000000..b507b6ed --- /dev/null +++ b/spec/fixtures/config/bad/inherit_missing_target.yml @@ -0,0 +1,2 @@ +inherit: + - ./does_not_exist.yml diff --git a/spec/fixtures/config/inherit/base.yml b/spec/fixtures/config/inherit/base.yml new file mode 100644 index 00000000..27710365 --- /dev/null +++ b/spec/fixtures/config/inherit/base.yml @@ -0,0 +1,3 @@ +ignore: + - CVE-BASE-1 + - CVE-BASE-2 diff --git a/spec/fixtures/config/inherit/child.yml b/spec/fixtures/config/inherit/child.yml new file mode 100644 index 00000000..6d2b44a5 --- /dev/null +++ b/spec/fixtures/config/inherit/child.yml @@ -0,0 +1,4 @@ +inherit: + - ./base.yml +ignore: + - CVE-CHILD-1 diff --git a/spec/fixtures/config/inherit/grandchild.yml b/spec/fixtures/config/inherit/grandchild.yml new file mode 100644 index 00000000..b3dcec07 --- /dev/null +++ b/spec/fixtures/config/inherit/grandchild.yml @@ -0,0 +1,4 @@ +inherit: + - ./middle.yml +ignore: + - CVE-GRANDCHILD-1 diff --git a/spec/fixtures/config/inherit/middle.yml b/spec/fixtures/config/inherit/middle.yml new file mode 100644 index 00000000..35c6f4b0 --- /dev/null +++ b/spec/fixtures/config/inherit/middle.yml @@ -0,0 +1,4 @@ +inherit: + - ./base.yml +ignore: + - CVE-MIDDLE-1 From 7f9468b36a3a0abfaa4a64574bb0be9288eb6fc5 Mon Sep 17 00:00:00 2001 From: Jonathan Swartz Date: Wed, 22 Jul 2026 09:54:31 -0700 Subject: [PATCH 2/2] Rename inherit to inherit_from to match rubocop --- ChangeLog.md | 2 +- README.md | 4 +-- lib/bundler/audit/configuration.rb | 10 +++---- spec/configuration_spec.rb | 30 +++++++++---------- .../bad/inherit_contains_a_non_string.yml | 2 -- spec/fixtures/config/bad/inherit_cycle_a.yml | 2 -- spec/fixtures/config/bad/inherit_cycle_b.yml | 2 -- .../inherit_from_contains_a_non_string.yml | 2 ++ .../config/bad/inherit_from_cycle_a.yml | 2 ++ .../config/bad/inherit_from_cycle_b.yml | 2 ++ .../bad/inherit_from_is_not_an_array.yml | 1 + ...et.yml => inherit_from_missing_target.yml} | 2 +- .../config/bad/inherit_is_not_an_array.yml | 1 - .../config/{inherit => inherit_from}/base.yml | 0 .../{inherit => inherit_from}/child.yml | 2 +- .../{inherit => inherit_from}/grandchild.yml | 2 +- .../{inherit => inherit_from}/middle.yml | 2 +- 17 files changed, 34 insertions(+), 34 deletions(-) delete mode 100644 spec/fixtures/config/bad/inherit_contains_a_non_string.yml delete mode 100644 spec/fixtures/config/bad/inherit_cycle_a.yml delete mode 100644 spec/fixtures/config/bad/inherit_cycle_b.yml create mode 100644 spec/fixtures/config/bad/inherit_from_contains_a_non_string.yml create mode 100644 spec/fixtures/config/bad/inherit_from_cycle_a.yml create mode 100644 spec/fixtures/config/bad/inherit_from_cycle_b.yml create mode 100644 spec/fixtures/config/bad/inherit_from_is_not_an_array.yml rename spec/fixtures/config/bad/{inherit_missing_target.yml => inherit_from_missing_target.yml} (64%) delete mode 100644 spec/fixtures/config/bad/inherit_is_not_an_array.yml rename spec/fixtures/config/{inherit => inherit_from}/base.yml (100%) rename spec/fixtures/config/{inherit => inherit_from}/child.yml (73%) rename spec/fixtures/config/{inherit => inherit_from}/grandchild.yml (76%) rename spec/fixtures/config/{inherit => inherit_from}/middle.yml (74%) diff --git a/ChangeLog.md b/ChangeLog.md index 392a0e3f..04fab25e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,6 @@ ### 0.9.4 / TBD -* Added `inherit:` config option, which merges additional config files +* Added `inherit_from:` config option, which merges additional config files into the current config. ### 0.9.3 / 2025-11-28 diff --git a/README.md b/README.md index 5d78e1b4..5768eb07 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ bundler-audit also supports a per-project configuration file: ```yaml --- -inherit: +inherit_from: - ../base.yml ignore: - CVE-YYYY-XXXX @@ -184,7 +184,7 @@ ignore: ``` * `ignore:` \[Array\\] - A list of advisory IDs to ignore. -* `inherit:` \[Array\\] - An optional list of other config +* `inherit_from:` \[Array\\] - An optional list of other config files to merge into this one. Relative paths are resolved from the directory of the declaring config file. diff --git a/lib/bundler/audit/configuration.rb b/lib/bundler/audit/configuration.rb index 7496e104..71377b9b 100644 --- a/lib/bundler/audit/configuration.rb +++ b/lib/bundler/audit/configuration.rb @@ -56,7 +56,7 @@ def self.load(file_path) # # Internal loader that tracks the ancestor chain of absolute paths in - # order to detect cycles across `inherit:` links. + # order to detect cycles across `inherit_from:` links. # # @param [String] file_path # Path to the YAML file holding the configuration. @@ -76,7 +76,7 @@ def self.load_with_chain(file_path,ancestors) absolute_path = File.expand_path(file_path) if ancestors.include?(absolute_path) - raise(InvalidConfigurationError,"Cycle detected in 'inherit': #{(ancestors + [absolute_path]).join(' -> ')}") + raise(InvalidConfigurationError,"Cycle detected in 'inherit_from': #{(ancestors + [absolute_path]).join(' -> ')}") end unless File.exist?(absolute_path) @@ -107,13 +107,13 @@ def self.load_with_chain(file_path,ancestors) end config[:ignore].concat(value.children.map(&:value)) - when 'inherit' + when 'inherit_from' unless value.is_a?(YAML::Nodes::Sequence) - raise(InvalidConfigurationError,"'inherit' key found in config file, but is not an Array") + raise(InvalidConfigurationError,"'inherit_from' key found in config file, but is not an Array") end unless value.children.all? { |node| node.is_a?(YAML::Nodes::Scalar) } - raise(InvalidConfigurationError,"'inherit' array in config file contains a non-String") + raise(InvalidConfigurationError,"'inherit_from' array in config file contains a non-String") end base_dir = File.dirname(absolute_path) diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb index 4040cef9..541218b5 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -54,26 +54,26 @@ end end - context "when inherit is not an Array" do - let(:path) { File.join(fixtures_dir,'bad','inherit_is_not_an_array.yml') } + context "when inherit_from is not an Array" do + let(:path) { File.join(fixtures_dir,'bad','inherit_from_is_not_an_array.yml') } it "raises a validation error" do expect { subject }.to raise_error(described_class::InvalidConfigurationError, - /'inherit' key found in config file, but is not an Array/) + /'inherit_from' key found in config file, but is not an Array/) end end - context "when inherit array contains a non-String" do - let(:path) { File.join(fixtures_dir,'bad','inherit_contains_a_non_string.yml') } + context "when inherit_from array contains a non-String" do + let(:path) { File.join(fixtures_dir,'bad','inherit_from_contains_a_non_string.yml') } it "raises a validation error" do expect { subject }.to raise_error(described_class::InvalidConfigurationError, - /'inherit' array in config file contains a non-String/) + /'inherit_from' array in config file contains a non-String/) end end context "when an inherited file does not exist" do - let(:path) { File.join(fixtures_dir,'bad','inherit_missing_target.yml') } + let(:path) { File.join(fixtures_dir,'bad','inherit_from_missing_target.yml') } it "raises a FileNotFound error" do expect { subject }.to raise_error(described_class::FileNotFound, @@ -81,18 +81,18 @@ end end - context "when the inherit chain forms a cycle" do - let(:path) { File.join(fixtures_dir,'bad','inherit_cycle_a.yml') } + context "when the inherit_from chain forms a cycle" do + let(:path) { File.join(fixtures_dir,'bad','inherit_from_cycle_a.yml') } it "raises a validation error identifying the cycle" do expect { subject }.to raise_error(described_class::InvalidConfigurationError, - /Cycle detected in 'inherit'/) + /Cycle detected in 'inherit_from'/) end end end context "when a file inherits another" do - let(:path) { File.join(fixtures_dir,'inherit','child.yml') } + let(:path) { File.join(fixtures_dir,'inherit_from','child.yml') } it { should be_a(described_class) } @@ -101,16 +101,16 @@ end end - context "when the inherit chain is 3 deep" do - let(:path) { File.join(fixtures_dir,'inherit','grandchild.yml') } + context "when the inherit_from chain is 3 deep" do + let(:path) { File.join(fixtures_dir,'inherit_from','grandchild.yml') } it "must transitively merge the ignore lists" do expect(subject.ignore).to eq(Set.new(%w[CVE-BASE-1 CVE-BASE-2 CVE-MIDDLE-1 CVE-GRANDCHILD-1])) end end - context "when the inherit path is relative" do - let(:path) { File.join(fixtures_dir,'inherit','child.yml') } + context "when the inherit_from path is relative" do + let(:path) { File.join(fixtures_dir,'inherit_from','child.yml') } it "must resolve inherited paths relative to the including file" do Dir.chdir('/tmp') do diff --git a/spec/fixtures/config/bad/inherit_contains_a_non_string.yml b/spec/fixtures/config/bad/inherit_contains_a_non_string.yml deleted file mode 100644 index a0454ae3..00000000 --- a/spec/fixtures/config/bad/inherit_contains_a_non_string.yml +++ /dev/null @@ -1,2 +0,0 @@ -inherit: - - foo: bar diff --git a/spec/fixtures/config/bad/inherit_cycle_a.yml b/spec/fixtures/config/bad/inherit_cycle_a.yml deleted file mode 100644 index 5f6277d8..00000000 --- a/spec/fixtures/config/bad/inherit_cycle_a.yml +++ /dev/null @@ -1,2 +0,0 @@ -inherit: - - ./inherit_cycle_b.yml diff --git a/spec/fixtures/config/bad/inherit_cycle_b.yml b/spec/fixtures/config/bad/inherit_cycle_b.yml deleted file mode 100644 index e6d932e1..00000000 --- a/spec/fixtures/config/bad/inherit_cycle_b.yml +++ /dev/null @@ -1,2 +0,0 @@ -inherit: - - ./inherit_cycle_a.yml diff --git a/spec/fixtures/config/bad/inherit_from_contains_a_non_string.yml b/spec/fixtures/config/bad/inherit_from_contains_a_non_string.yml new file mode 100644 index 00000000..101cb382 --- /dev/null +++ b/spec/fixtures/config/bad/inherit_from_contains_a_non_string.yml @@ -0,0 +1,2 @@ +inherit_from: + - foo: bar diff --git a/spec/fixtures/config/bad/inherit_from_cycle_a.yml b/spec/fixtures/config/bad/inherit_from_cycle_a.yml new file mode 100644 index 00000000..32505fee --- /dev/null +++ b/spec/fixtures/config/bad/inherit_from_cycle_a.yml @@ -0,0 +1,2 @@ +inherit_from: + - ./inherit_from_cycle_b.yml diff --git a/spec/fixtures/config/bad/inherit_from_cycle_b.yml b/spec/fixtures/config/bad/inherit_from_cycle_b.yml new file mode 100644 index 00000000..2102e486 --- /dev/null +++ b/spec/fixtures/config/bad/inherit_from_cycle_b.yml @@ -0,0 +1,2 @@ +inherit_from: + - ./inherit_from_cycle_a.yml diff --git a/spec/fixtures/config/bad/inherit_from_is_not_an_array.yml b/spec/fixtures/config/bad/inherit_from_is_not_an_array.yml new file mode 100644 index 00000000..743f980a --- /dev/null +++ b/spec/fixtures/config/bad/inherit_from_is_not_an_array.yml @@ -0,0 +1 @@ +inherit_from: some_string diff --git a/spec/fixtures/config/bad/inherit_missing_target.yml b/spec/fixtures/config/bad/inherit_from_missing_target.yml similarity index 64% rename from spec/fixtures/config/bad/inherit_missing_target.yml rename to spec/fixtures/config/bad/inherit_from_missing_target.yml index b507b6ed..55496d5a 100644 --- a/spec/fixtures/config/bad/inherit_missing_target.yml +++ b/spec/fixtures/config/bad/inherit_from_missing_target.yml @@ -1,2 +1,2 @@ -inherit: +inherit_from: - ./does_not_exist.yml diff --git a/spec/fixtures/config/bad/inherit_is_not_an_array.yml b/spec/fixtures/config/bad/inherit_is_not_an_array.yml deleted file mode 100644 index 93d8fcaa..00000000 --- a/spec/fixtures/config/bad/inherit_is_not_an_array.yml +++ /dev/null @@ -1 +0,0 @@ -inherit: some_string diff --git a/spec/fixtures/config/inherit/base.yml b/spec/fixtures/config/inherit_from/base.yml similarity index 100% rename from spec/fixtures/config/inherit/base.yml rename to spec/fixtures/config/inherit_from/base.yml diff --git a/spec/fixtures/config/inherit/child.yml b/spec/fixtures/config/inherit_from/child.yml similarity index 73% rename from spec/fixtures/config/inherit/child.yml rename to spec/fixtures/config/inherit_from/child.yml index 6d2b44a5..afb646a0 100644 --- a/spec/fixtures/config/inherit/child.yml +++ b/spec/fixtures/config/inherit_from/child.yml @@ -1,4 +1,4 @@ -inherit: +inherit_from: - ./base.yml ignore: - CVE-CHILD-1 diff --git a/spec/fixtures/config/inherit/grandchild.yml b/spec/fixtures/config/inherit_from/grandchild.yml similarity index 76% rename from spec/fixtures/config/inherit/grandchild.yml rename to spec/fixtures/config/inherit_from/grandchild.yml index b3dcec07..e9047804 100644 --- a/spec/fixtures/config/inherit/grandchild.yml +++ b/spec/fixtures/config/inherit_from/grandchild.yml @@ -1,4 +1,4 @@ -inherit: +inherit_from: - ./middle.yml ignore: - CVE-GRANDCHILD-1 diff --git a/spec/fixtures/config/inherit/middle.yml b/spec/fixtures/config/inherit_from/middle.yml similarity index 74% rename from spec/fixtures/config/inherit/middle.yml rename to spec/fixtures/config/inherit_from/middle.yml index 35c6f4b0..c3df6293 100644 --- a/spec/fixtures/config/inherit/middle.yml +++ b/spec/fixtures/config/inherit_from/middle.yml @@ -1,4 +1,4 @@ -inherit: +inherit_from: - ./base.yml ignore: - CVE-MIDDLE-1