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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +19 -7
- data/README.md +185 -60
- data/README_ja.md +187 -57
- data/demo/ai_analysis_vulnerability.rb +3 -3
- data/demo/csrf_vulnerability.rb +1 -1
- data/demo/eval_vulnerability.rb +1 -1
- data/demo/idor_vulnerability.rb +2 -2
- data/demo/insecure_cookie_vulnerability.rb +1 -1
- data/demo/open_redirect_vulnerability.rb +1 -1
- data/demo/static_analysis_vulnerability.rb +1 -1
- data/demo/xss_vulnerability.rb +1 -1
- data/exe/omamori +2 -2
- data/lib/omamori/ai_analysis_engine/checklist_scanner.rb +143 -0
- data/lib/omamori/ai_analysis_engine/cli_clients/base.rb +43 -0
- data/lib/omamori/ai_analysis_engine/cli_clients/claude_code_client.rb +35 -0
- data/lib/omamori/ai_analysis_engine/cli_clients/codex_client.rb +47 -0
- data/lib/omamori/ai_analysis_engine/diff_splitter.rb +17 -4
- data/lib/omamori/ai_analysis_engine/fallback_client.rb +23 -0
- data/lib/omamori/ai_analysis_engine/judge_client.rb +45 -0
- data/lib/omamori/ai_analysis_engine/llm_client.rb +71 -0
- data/lib/omamori/ai_analysis_engine/prompt_manager.rb +72 -12
- data/lib/omamori/ai_analysis_engine/ruby_llm_context.rb +23 -0
- data/lib/omamori/ai_analysis_engine/sub_checks/audit_log_missing.yml +12 -0
- data/lib/omamori/ai_analysis_engine/sub_checks/csrf.yml +22 -0
- data/lib/omamori/ai_analysis_engine/sub_checks/insecure_deserialization_rce.yml +9 -0
- data/lib/omamori/ai_analysis_engine/sub_checks/insufficient_encryption.yml +19 -0
- data/lib/omamori/ai_analysis_engine/sub_checks/mass_assignment.yml +19 -0
- data/lib/omamori/ai_analysis_engine/sub_checks/open_redirect.yml +17 -0
- data/lib/omamori/ai_analysis_engine/sub_checks/sql_injection.yml +20 -0
- data/lib/omamori/ai_analysis_engine/sub_checks/xss.yml +37 -0
- data/lib/omamori/ai_settings.rb +273 -0
- data/lib/omamori/config.rb +71 -72
- data/lib/omamori/core_runner.rb +332 -359
- data/lib/omamori/doctor.rb +95 -0
- data/lib/omamori/parallel.rb +24 -0
- data/lib/omamori/report_generator/console_formatter.rb +48 -42
- data/lib/omamori/report_generator/html_formatter.rb +6 -6
- data/lib/omamori/report_generator/json_formatter.rb +1 -1
- data/lib/omamori/setup_files.rb +260 -0
- data/lib/omamori/static_analysers/brakeman_runner.rb +7 -7
- data/lib/omamori/static_analysers/bundler_audit_runner.rb +8 -6
- data/lib/omamori/version.rb +2 -2
- data/lib/omamori.rb +11 -11
- metadata +45 -26
- 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
|
data/lib/omamori/config.rb
CHANGED
|
@@ -4,8 +4,8 @@ require 'yaml'
|
|
|
4
4
|
|
|
5
5
|
module Omamori
|
|
6
6
|
class Config
|
|
7
|
-
DEFAULT_CONFIG_PATH =
|
|
8
|
-
DEFAULT_IGNORE_PATH =
|
|
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[
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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[
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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[
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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[
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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[
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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[
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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[
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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[
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|