omamori 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +19 -7
  5. data/README.md +185 -60
  6. data/README_ja.md +187 -57
  7. data/demo/ai_analysis_vulnerability.rb +3 -3
  8. data/demo/csrf_vulnerability.rb +1 -1
  9. data/demo/eval_vulnerability.rb +1 -1
  10. data/demo/idor_vulnerability.rb +2 -2
  11. data/demo/insecure_cookie_vulnerability.rb +1 -1
  12. data/demo/open_redirect_vulnerability.rb +1 -1
  13. data/demo/static_analysis_vulnerability.rb +1 -1
  14. data/demo/xss_vulnerability.rb +1 -1
  15. data/exe/omamori +2 -2
  16. data/lib/omamori/ai_analysis_engine/checklist_scanner.rb +143 -0
  17. data/lib/omamori/ai_analysis_engine/cli_clients/base.rb +43 -0
  18. data/lib/omamori/ai_analysis_engine/cli_clients/claude_code_client.rb +35 -0
  19. data/lib/omamori/ai_analysis_engine/cli_clients/codex_client.rb +47 -0
  20. data/lib/omamori/ai_analysis_engine/diff_splitter.rb +17 -4
  21. data/lib/omamori/ai_analysis_engine/fallback_client.rb +23 -0
  22. data/lib/omamori/ai_analysis_engine/judge_client.rb +45 -0
  23. data/lib/omamori/ai_analysis_engine/llm_client.rb +71 -0
  24. data/lib/omamori/ai_analysis_engine/prompt_manager.rb +72 -12
  25. data/lib/omamori/ai_analysis_engine/ruby_llm_context.rb +23 -0
  26. data/lib/omamori/ai_analysis_engine/sub_checks/audit_log_missing.yml +12 -0
  27. data/lib/omamori/ai_analysis_engine/sub_checks/csrf.yml +22 -0
  28. data/lib/omamori/ai_analysis_engine/sub_checks/insecure_deserialization_rce.yml +9 -0
  29. data/lib/omamori/ai_analysis_engine/sub_checks/insufficient_encryption.yml +19 -0
  30. data/lib/omamori/ai_analysis_engine/sub_checks/mass_assignment.yml +19 -0
  31. data/lib/omamori/ai_analysis_engine/sub_checks/open_redirect.yml +17 -0
  32. data/lib/omamori/ai_analysis_engine/sub_checks/sql_injection.yml +20 -0
  33. data/lib/omamori/ai_analysis_engine/sub_checks/xss.yml +37 -0
  34. data/lib/omamori/ai_settings.rb +273 -0
  35. data/lib/omamori/config.rb +71 -72
  36. data/lib/omamori/core_runner.rb +332 -359
  37. data/lib/omamori/doctor.rb +95 -0
  38. data/lib/omamori/parallel.rb +24 -0
  39. data/lib/omamori/report_generator/console_formatter.rb +48 -42
  40. data/lib/omamori/report_generator/html_formatter.rb +6 -6
  41. data/lib/omamori/report_generator/json_formatter.rb +1 -1
  42. data/lib/omamori/setup_files.rb +260 -0
  43. data/lib/omamori/static_analysers/brakeman_runner.rb +7 -7
  44. data/lib/omamori/static_analysers/bundler_audit_runner.rb +8 -6
  45. data/lib/omamori/version.rb +2 -2
  46. data/lib/omamori.rb +11 -11
  47. metadata +45 -26
  48. data/lib/omamori/ai_analysis_engine/gemini_client.rb +0 -71
@@ -0,0 +1,37 @@
1
+ # Concrete patterns that are each enough to make XSS real on their own.
2
+ # The classifier answers each one separately, alongside the overall XSS question.
3
+ # Each question asks for the pattern AND a missing defense, so that code which uses the same construct safely is a no.
4
+ raw_output: >-
5
+ Does the code send data that may come from users to the page as HTML without escaping it? Answer yes when such
6
+ data reaches raw, html_safe, <%== %>, or safe_concat, or is interpolated into a string that is then marked
7
+ html_safe, without being escaped first. Answer no when every user value is escaped (h, ERB::Util.html_escape,
8
+ sanitize) or the HTML is built with tag helpers (content_tag, tag.*, link_to, safe_join) before being marked safe.
9
+ js_context: >-
10
+ Does the code put data that may come from users into JavaScript (an inline <script> block, an event handler
11
+ attribute such as onclick, or a JavaScript string) without JavaScript-safe escaping? This includes raw output,
12
+ and JSON produced by JSON.generate or a JSON library other than Rails' to_json, because it does not escape
13
+ </script>. Answer no when the data goes through json_escape, j / escape_javascript, or Rails' to_json, which
14
+ escape the characters that could end the script.
15
+ url_attribute: >-
16
+ Does the code put a URL that may come from users into href, src, action, or formaction without making sure it is
17
+ not a javascript: URL? Answer no when the scheme is checked to be http or https, the URL comes from a route helper
18
+ or a fixed path, or the content goes through sanitize, which removes javascript: URLs.
19
+ html_response: >-
20
+ Does the code build HTML by interpolating data that may come from users into a string and send it to the browser
21
+ outside the escaping template engine (render html:, render inline:, or a Rack or Sinatra response body), without
22
+ escaping that data? Answer no when every interpolated value is escaped (h, ERB::Util.html_escape,
23
+ Rack::Utils.escape_html).
24
+ sanitizer_config: >-
25
+ Does the code change how content that may come from users is sanitized or rendered so that dangerous HTML can now
26
+ get through? For example allowing script, iframe, or object tags or on* attributes in sanitize, or enabling raw
27
+ HTML in a Markdown renderer (Commonmarker unsafe: true, Redcarpet without filter_html). Answer no when the change
28
+ keeps raw HTML disabled and keeps the default sanitize allowlist.
29
+ dom_sink: >-
30
+ Does front-end JavaScript insert data that may come from users or from the server into the page as HTML, through
31
+ innerHTML, outerHTML, insertAdjacentHTML, document.write, or jQuery .html()? Answer no when it only uses
32
+ textContent, createTextNode, or creates elements and sets their text.
33
+ attribute_injection: >-
34
+ Does the code build an HTML attribute value by interpolating data that may come from users into a string, without
35
+ escaping the quotes, so that the value could end the attribute and add a new one such as onmouseover? Answer no
36
+ when the value is escaped with h or ERB::Util.html_escape (which escape quotes) or the attribute is set through a
37
+ tag helper.
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ai_analysis_engine/ruby_llm_context'
4
+ require_relative 'ai_analysis_engine/llm_client'
5
+ require_relative 'ai_analysis_engine/judge_client'
6
+ require_relative 'ai_analysis_engine/cli_clients/claude_code_client'
7
+ require_relative 'ai_analysis_engine/cli_clients/codex_client'
8
+ require_relative 'ai_analysis_engine/fallback_client'
9
+
10
+ module Omamori
11
+ # Decides which classifier and reporter to use, from .omamorirc and command-line options.
12
+ #
13
+ # classifier: # omit to let the reporter check everything itself
14
+ # provider: typesafe # any RubyLLM provider that can judge (typesafe, openai, ollama)
15
+ # model: jev-1.13.0
16
+ # sub_checks: true # also ask each risk's concrete patterns (sub_checks/*.yml)
17
+ # reporter:
18
+ # provider: anthropic # any RubyLLM provider, or claude_code / codex (subscription CLIs)
19
+ # model: claude-sonnet-5-5
20
+ # fallback: # optional; asked only when the reporter refuses or fails
21
+ # provider: claude_code
22
+ # model: sonnet
23
+ #
24
+ # There is no default reporter: `omamori init --reporter ...` writes one. The older keys (engine, llm,
25
+ # model, provider, api_key, decision, claude, codex) are still read; an old config with only `api_key`
26
+ # keeps using Gemini, as it did before.
27
+ class AISettings
28
+ class Error < StandardError; end
29
+
30
+ CLI_REPORTERS = %w[claude_code codex].freeze
31
+ # The reporter of omamori 0.1, used only for old configs that set the top-level api_key (a Gemini key)
32
+ LEGACY_REPORTER = { 'provider' => 'gemini', 'model' => AIAnalysisEngine::LLMClient::DEFAULT_MODEL }.freeze
33
+ # Classifier providers whose model can be omitted
34
+ DEFAULT_CLASSIFIER_MODELS = { 'typesafe' => 'jev-1.13.0', 'openai' => 'gpt-6-luna' }.freeze
35
+ # Questions per request accepted by each classifier provider (no entry: no limit)
36
+ MAX_QUESTIONS = { 'openai' => 200 }.freeze
37
+ # RubyLLM protocols that answer judgment questions
38
+ JUDGE_PROTOCOLS = %i[system_one decisions].freeze
39
+ DEFAULT_THRESHOLD = 0.5
40
+ DEFAULT_CLASSIFIER_CONCURRENCY = 8
41
+ DEFAULT_API_REPORTER_CONCURRENCY = 8
42
+ # Subscription CLIs have tighter rate limits than APIs
43
+ DEFAULT_CLI_REPORTER_CONCURRENCY = 3
44
+
45
+ LEGACY_ENGINES = { 'jev' => 'typesafe', 'openai' => 'openai' }.freeze
46
+ LEGACY_NO_CLASSIFIER = %w[llm gemini].freeze
47
+ LEGACY_LLMS = { 'claude' => 'claude_code', 'codex' => 'codex' }.freeze
48
+
49
+ # The classifier settings, or nil when the reporter checks everything itself
50
+ attr_reader :classifier
51
+ # The reporter settings, or nil when none is configured (`omamori scan` stops with #reporter_missing_message)
52
+ attr_reader :reporter
53
+
54
+ # overrides: classifier:, classifier_model:, reporter:, reporter_model:, reporter_fallback:,
55
+ # reporter_fallback_model: (from the command line)
56
+ def initialize(config, overrides = {})
57
+ @config = config
58
+ @overrides = overrides.compact.transform_values(&:to_s)
59
+ @classifier = resolve_classifier
60
+ @reporter = resolve_reporter
61
+ @reporter_fallback = resolve_reporter_fallback
62
+ end
63
+
64
+ def classifier?
65
+ !@classifier.nil?
66
+ end
67
+
68
+ def reporter?
69
+ !@reporter.nil?
70
+ end
71
+
72
+ def cli_reporter?
73
+ reporter? && CLI_REPORTERS.include?(@reporter['provider'])
74
+ end
75
+
76
+ def threshold
77
+ @classifier.fetch('threshold', DEFAULT_THRESHOLD)
78
+ end
79
+
80
+ # Whether each risk is also asked as concrete sub-checks (on unless `classifier.sub_checks: false`)
81
+ def sub_checks?
82
+ @classifier.fetch('sub_checks', true)
83
+ end
84
+
85
+ def classifier_concurrency
86
+ @classifier.fetch('concurrency', DEFAULT_CLASSIFIER_CONCURRENCY)
87
+ end
88
+
89
+ def reporter_concurrency
90
+ @reporter.fetch('concurrency', cli_reporter? ? DEFAULT_CLI_REPORTER_CONCURRENCY : DEFAULT_API_REPORTER_CONCURRENCY)
91
+ end
92
+
93
+ def reporter_model
94
+ @reporter&.fetch('model', nil)
95
+ end
96
+
97
+ def self.reporter_missing_message(command: 'scan')
98
+ message = <<~MESSAGE
99
+ No reporter is configured. Choose the LLM that writes reports, for example:
100
+
101
+ omamori init --reporter claude_code # Claude Code subscription (claude -p)
102
+ omamori init --reporter codex # Codex subscription (codex exec)
103
+ omamori init --reporter anthropic --reporter-model claude-sonnet-5-5 # Any RubyLLM provider and model
104
+ MESSAGE
105
+ return message unless command == 'scan'
106
+
107
+ "#{message}\nOr pass --reporter (and --reporter-model) to `omamori scan`.\n"
108
+ end
109
+
110
+ # The environment variable that holds the API key of a RubyLLM provider, or nil when it has none
111
+ # (claude_code / codex use the CLI's login; some providers only need a server address)
112
+ def self.api_key_env(provider)
113
+ return nil if provider.nil? || CLI_REPORTERS.include?(provider)
114
+
115
+ name = "#{provider}_api_key"
116
+ RubyLLM.config.respond_to?("#{name}=") ? name.upcase : nil
117
+ end
118
+
119
+ def build_classifier_client
120
+ return nil unless classifier?
121
+
122
+ AIAnalysisEngine::JudgeClient.new(
123
+ ruby_llm_context,
124
+ provider: @classifier['provider'].to_sym, model: @classifier['model'],
125
+ max_questions: @classifier.fetch('max_questions', MAX_QUESTIONS[@classifier['provider']])
126
+ )
127
+ end
128
+
129
+ def build_reporter_client
130
+ return nil unless reporter?
131
+
132
+ client = build_llm_client(@reporter)
133
+ return client unless @reporter_fallback
134
+
135
+ AIAnalysisEngine::FallbackClient.new(client, build_llm_client(@reporter_fallback),
136
+ fallback_model: @reporter_fallback['model'])
137
+ end
138
+
139
+ attr_reader :reporter_fallback
140
+
141
+ # A client for one reporter's settings (the reporter or its fallback)
142
+ def build_llm_client(settings)
143
+ case settings['provider']
144
+ when 'claude_code' then AIAnalysisEngine::CLIClients::ClaudeCodeClient.new(command: settings['command'])
145
+ when 'codex' then AIAnalysisEngine::CLIClients::CodexClient.new(command: settings['command'])
146
+ else AIAnalysisEngine::LLMClient.new(ruby_llm_context, provider: settings['provider']&.to_sym)
147
+ end
148
+ end
149
+
150
+ # One line for the console, e.g. "classifier: typesafe/jev-1.13.0, reporter: gemini/gemini-3.8-flash"
151
+ def describe
152
+ reporter = reporter? ? [@reporter['provider'], @reporter['model']].compact.join('/') : 'none'
153
+ reporter += " (fallback: #{[@reporter_fallback['provider'], @reporter_fallback['model']].compact.join('/')})" if @reporter_fallback
154
+ return "reporter: #{reporter}" unless classifier?
155
+
156
+ "classifier: #{@classifier['provider']}/#{@classifier['model']}, reporter: #{reporter}"
157
+ end
158
+
159
+ private
160
+
161
+ def get(key)
162
+ @config.get(key, nil)
163
+ end
164
+
165
+ def resolve_classifier
166
+ settings = apply_override(configured_classifier, @overrides[:classifier], @overrides[:classifier_model])
167
+ return nil if settings.nil? || settings['provider'] == 'none'
168
+
169
+ provider = settings.fetch('provider') { raise Error, 'classifier.provider is required' }
170
+ validate_classifier_provider!(provider)
171
+ settings['model'] ||= DEFAULT_CLASSIFIER_MODELS.fetch(provider) do
172
+ raise Error, "classifier.model is required for provider #{provider}"
173
+ end
174
+ settings
175
+ end
176
+
177
+ def resolve_reporter
178
+ settings = apply_override(configured_reporter, @overrides[:reporter], @overrides[:reporter_model])
179
+ settings = LEGACY_REPORTER.merge(settings || {}) if legacy_gemini?(settings)
180
+ return nil if settings.nil? || (settings['provider'].nil? && settings['model'].nil?)
181
+
182
+ validate_reporter(settings.except('fallback'))
183
+ end
184
+
185
+ def resolve_reporter_fallback
186
+ # Read from the configured reporter, so a --reporter override keeps the configured fallback
187
+ configured = configured_reporter&.fetch('fallback', nil)&.then { |fallback| stringify(fallback) }
188
+ settings = apply_override(configured, @overrides[:reporter_fallback], @overrides[:reporter_fallback_model])
189
+ return nil if settings.nil? || settings['provider'] == 'none'
190
+
191
+ validate_reporter(settings, role: 'reporter.fallback')
192
+ end
193
+
194
+ def validate_reporter(settings, role: 'reporter')
195
+ provider = settings['provider']
196
+ return settings if provider.nil? || CLI_REPORTERS.include?(provider)
197
+
198
+ raise Error, "Unknown #{role} provider: #{provider}" unless RubyLLM::Provider.resolve(provider)
199
+ raise Error, "#{role}.model is required for provider #{provider}" unless settings['model']
200
+
201
+ settings
202
+ end
203
+
204
+ # A provider given on the command line replaces the configured one; the configured model only
205
+ # carries over when the provider stays the same.
206
+ def apply_override(settings, provider, model)
207
+ if provider
208
+ settings = settings&.fetch('provider', nil) == provider ? settings : {}
209
+ settings = settings.merge('provider' => provider)
210
+ end
211
+ settings = (settings || {}).merge('model' => model) if model
212
+ settings
213
+ end
214
+
215
+ # omamori 0.1 configs had only the Gemini `api_key` (and maybe `model`), and used Gemini by default
216
+ def legacy_gemini?(settings)
217
+ get('api_key') && (settings.nil? || (settings['provider'].nil? && settings['model'].nil?))
218
+ end
219
+
220
+ def configured_classifier
221
+ return stringify(get('classifier')) if get('classifier')
222
+
223
+ engine = get('engine')&.to_s
224
+ return nil if engine.nil? || LEGACY_NO_CLASSIFIER.include?(engine)
225
+
226
+ provider = LEGACY_ENGINES.fetch(engine) { raise Error, "Unknown engine: #{engine}" }
227
+ decision = stringify(get('decision') || {})
228
+ engine_settings = stringify(decision[engine] || {})
229
+ { 'provider' => provider, 'threshold' => decision['threshold'], 'concurrency' => decision['concurrency'] }
230
+ .merge(engine_settings.slice('model', 'base_url', 'api_key')).compact
231
+ end
232
+
233
+ def configured_reporter
234
+ return stringify(get('reporter')) if get('reporter')
235
+
236
+ llm = get('llm')&.to_s
237
+ report_concurrency = stringify(get('decision') || {})['report_concurrency']
238
+ settings = if LEGACY_LLMS.key?(llm)
239
+ { 'provider' => LEGACY_LLMS[llm] }.merge(stringify(get(llm) || {}).slice('model', 'command'))
240
+ else
241
+ { 'provider' => get('provider')&.to_s, 'model' => get('model') }
242
+ end
243
+ settings.merge('concurrency' => report_concurrency).compact.then { |s| s.empty? ? nil : s }
244
+ end
245
+
246
+ def validate_classifier_provider!(provider)
247
+ provider_class = RubyLLM::Provider.resolve(provider)
248
+ raise Error, "Unknown classifier provider: #{provider}" unless provider_class
249
+ return if provider_class.protocols.keys.intersect?(JUDGE_PROTOCOLS)
250
+
251
+ raise Error, "Provider #{provider} cannot be used as a classifier"
252
+ end
253
+
254
+ # Each role's api_key / base_url go to RubyLLM's <provider>_api_key / <provider>_api_base.
255
+ # The old top-level api_key is a Gemini key.
256
+ def ruby_llm_context
257
+ @ruby_llm_context ||= begin
258
+ overrides = { gemini_api_key: get('api_key') }
259
+ [@classifier, @reporter, @reporter_fallback].compact.each do |settings|
260
+ next unless settings['provider'] && !CLI_REPORTERS.include?(settings['provider'])
261
+
262
+ overrides[:"#{settings['provider']}_api_key"] = settings['api_key'] if settings['api_key']
263
+ overrides[:"#{settings['provider']}_api_base"] = settings['base_url'] if settings['base_url']
264
+ end
265
+ AIAnalysisEngine::RubyLLMContext.build(overrides)
266
+ end
267
+ end
268
+
269
+ def stringify(hash)
270
+ hash.to_h.transform_keys(&:to_s)
271
+ end
272
+ end
273
+ end
@@ -4,8 +4,8 @@ require 'yaml'
4
4
 
5
5
  module Omamori
6
6
  class Config
7
- DEFAULT_CONFIG_PATH = ".omamorirc"
8
- DEFAULT_IGNORE_PATH = ".omamoriignore"
7
+ DEFAULT_CONFIG_PATH = '.omamorirc'
8
+ DEFAULT_IGNORE_PATH = '.omamoriignore'
9
9
 
10
10
  attr_reader :ignore_patterns
11
11
 
@@ -35,101 +35,100 @@ module Omamori
35
35
  end
36
36
 
37
37
  def validate_api_key
38
- api_key = @config["api_key"]
39
- if api_key && !api_key.is_a?(String)
40
- puts "Warning: Config 'api_key' should be a string."
41
- end
38
+ api_key = @config['api_key']
39
+ return unless api_key && !api_key.is_a?(String)
40
+
41
+ puts "Warning: Config 'api_key' should be a string."
42
42
  end
43
43
 
44
44
  def validate_model
45
- model = @config["model"]
46
- if model && !model.is_a?(String)
47
- puts "Warning: Config 'model' should be a string."
48
- end
45
+ model = @config['model']
46
+ return unless model && !model.is_a?(String)
47
+
48
+ puts "Warning: Config 'model' should be a string."
49
49
  end
50
50
 
51
51
  def validate_checks
52
- checks = @config["checks"]
53
- if checks && !checks.is_a?(Array)
54
- puts "Warning: Config 'checks' should be an array."
55
- end
52
+ checks = @config['checks']
53
+ return unless checks && !checks.is_a?(Array)
54
+
55
+ puts "Warning: Config 'checks' should be an array."
56
56
  end
57
57
 
58
58
  def validate_prompt_templates
59
- prompt_templates = @config["prompt_templates"]
60
- if prompt_templates && !prompt_templates.is_a?(Hash)
61
- puts "Warning: Config 'prompt_templates' should be a hash."
62
- end
59
+ prompt_templates = @config['prompt_templates']
60
+ return unless prompt_templates && !prompt_templates.is_a?(Hash)
61
+
62
+ puts "Warning: Config 'prompt_templates' should be a hash."
63
63
  end
64
64
 
65
65
  def validate_report_settings
66
- report = @config["report"]
67
- if report
68
- unless report.is_a?(Hash)
69
- puts "Warning: Config 'report' should be a hash."
70
- return
71
- end
72
- if report.key?("output_path") && !report["output_path"].is_a?(String)
73
- puts "Warning: Config 'report.output_path' should be a string."
74
- end
75
- if report.key?("html_template") && !report["html_template"].is_a?(String)
76
- puts "Warning: Config 'report.html_template' should be a string."
77
- end
66
+ report = @config['report']
67
+ return unless report
68
+
69
+ unless report.is_a?(Hash)
70
+ puts "Warning: Config 'report' should be a hash."
71
+ return
78
72
  end
73
+ if report.key?('output_path') && !report['output_path'].is_a?(String)
74
+ puts "Warning: Config 'report.output_path' should be a string."
75
+ end
76
+ return unless report.key?('html_template') && !report['html_template'].is_a?(String)
77
+
78
+ puts "Warning: Config 'report.html_template' should be a string."
79
79
  end
80
80
 
81
81
  def validate_static_analyser_settings
82
- static_analysers = @config["static_analysers"]
83
- if static_analysers
84
- unless static_analysers.is_a?(Hash)
85
- puts "Warning: Config 'static_analysers' should be a hash."
86
- return
87
- end
88
- if static_analysers.key?("brakeman")
89
- brakeman_config = static_analysers["brakeman"]
90
- unless brakeman_config.is_a?(Hash)
91
- puts "Warning: Config 'static_analysers.brakeman' should be a hash."
92
- else
93
- if brakeman_config.key?("options") && !brakeman_config["options"].is_a?(String)
94
- puts "Warning: Config 'static_analysers.brakeman.options' should be a string."
95
- end
82
+ static_analysers = @config['static_analysers']
83
+ return unless static_analysers
84
+
85
+ unless static_analysers.is_a?(Hash)
86
+ puts "Warning: Config 'static_analysers' should be a hash."
87
+ return
88
+ end
89
+ if static_analysers.key?('brakeman')
90
+ brakeman_config = static_analysers['brakeman']
91
+ if brakeman_config.is_a?(Hash)
92
+ if brakeman_config.key?('options') && !brakeman_config['options'].is_a?(String)
93
+ puts "Warning: Config 'static_analysers.brakeman.options' should be a string."
96
94
  end
95
+ else
96
+ puts "Warning: Config 'static_analysers.brakeman' should be a hash."
97
97
  end
98
- if static_analysers.key?("bundler_audit")
99
- bundler_audit_config = static_analysers["bundler_audit"]
100
- unless bundler_audit_config.is_a?(Hash)
101
- puts "Warning: Config 'static_analysers.bundler_audit' should be a hash."
102
- else
103
- if bundler_audit_config.key?("options") && !bundler_audit_config["options"].is_a?(String)
104
- puts "Warning: Config 'static_analysers.bundler_audit.options' should be a string."
105
- end
106
- end
98
+ end
99
+ return unless static_analysers.key?('bundler_audit')
100
+
101
+ bundler_audit_config = static_analysers['bundler_audit']
102
+ if bundler_audit_config.is_a?(Hash)
103
+ if bundler_audit_config.key?('options') && !bundler_audit_config['options'].is_a?(String)
104
+ puts "Warning: Config 'static_analysers.bundler_audit.options' should be a string."
107
105
  end
106
+ else
107
+ puts "Warning: Config 'static_analysers.bundler_audit' should be a hash."
108
108
  end
109
109
  end
110
110
 
111
-
112
111
  def validate_ci_setup_settings
113
- ci_setup = @config["ci_setup"]
114
- if ci_setup
115
- unless ci_setup.is_a?(Hash)
116
- puts "Warning: Config 'ci_setup' should be a hash."
117
- return
118
- end
119
- if ci_setup.key?("github_actions_path") && !ci_setup["github_actions_path"].is_a?(String)
120
- puts "Warning: Config 'ci_setup.github_actions_path' should be a string."
121
- end
122
- if ci_setup.key?("gitlab_ci_path") && !ci_setup["gitlab_ci_path"].is_a?(String)
123
- puts "Warning: Config 'ci_setup.gitlab_ci_path' should be a string."
124
- end
112
+ ci_setup = @config['ci_setup']
113
+ return unless ci_setup
114
+
115
+ unless ci_setup.is_a?(Hash)
116
+ puts "Warning: Config 'ci_setup' should be a hash."
117
+ return
118
+ end
119
+ if ci_setup.key?('github_actions_path') && !ci_setup['github_actions_path'].is_a?(String)
120
+ puts "Warning: Config 'ci_setup.github_actions_path' should be a string."
125
121
  end
122
+ return unless ci_setup.key?('gitlab_ci_path') && !ci_setup['gitlab_ci_path'].is_a?(String)
123
+
124
+ puts "Warning: Config 'ci_setup.gitlab_ci_path' should be a string."
126
125
  end
127
126
 
128
127
  def validate_language
129
- language = @config["language"]
130
- if language && !language.is_a?(String)
131
- puts "Warning: Config 'language' should be a string."
132
- end
128
+ language = @config['language']
129
+ return unless language && !language.is_a?(String)
130
+
131
+ puts "Warning: Config 'language' should be a string."
133
132
  end
134
133
 
135
134
  # Load .omamoriignore file and return an array of ignore patterns
@@ -140,7 +139,7 @@ module Omamori
140
139
  File.readlines(ignore_path, chomp: true).reject do |line|
141
140
  line.strip.empty? || line.strip.start_with?('#')
142
141
  end
143
- rescue => e
142
+ rescue StandardError => e
144
143
  puts "Warning: Error reading .omamoriignore file #{ignore_path}: #{e.message}"
145
144
  [] # Return empty array if reading fails
146
145
  end
@@ -162,4 +161,4 @@ module Omamori
162
161
  end
163
162
  end
164
163
  end
165
- end
164
+ end