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
data/lib/omamori/core_runner.rb
CHANGED
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
require 'optparse'
|
|
4
4
|
require 'fileutils' # FileUtils を require する
|
|
5
5
|
require 'pathname' # Pathname を require する
|
|
6
|
-
|
|
6
|
+
require 'shellwords'
|
|
7
|
+
require_relative 'ai_analysis_engine/llm_client'
|
|
8
|
+
require_relative 'ai_settings'
|
|
9
|
+
require_relative 'setup_files'
|
|
10
|
+
require_relative 'doctor'
|
|
7
11
|
require_relative 'ai_analysis_engine/prompt_manager'
|
|
8
12
|
require_relative 'ai_analysis_engine/diff_splitter'
|
|
13
|
+
require_relative 'ai_analysis_engine/checklist_scanner'
|
|
9
14
|
require_relative 'report_generator/console_formatter'
|
|
10
15
|
require_relative 'report_generator/html_formatter'
|
|
11
16
|
require_relative 'report_generator/json_formatter'
|
|
@@ -18,48 +23,45 @@ module Omamori
|
|
|
18
23
|
class CoreRunner
|
|
19
24
|
# Define the JSON Schema for Structured Output
|
|
20
25
|
JSON_SCHEMA = {
|
|
21
|
-
"type":
|
|
26
|
+
"type": 'object',
|
|
22
27
|
"properties": {
|
|
23
28
|
"security_risks": {
|
|
24
|
-
"type":
|
|
25
|
-
"description":
|
|
29
|
+
"type": 'array',
|
|
30
|
+
"description": '検出されたセキュリティリスクのリスト。',
|
|
26
31
|
"items": {
|
|
27
|
-
"type":
|
|
32
|
+
"type": 'object',
|
|
28
33
|
"properties": {
|
|
29
34
|
"type": {
|
|
30
|
-
"type":
|
|
31
|
-
"description":
|
|
35
|
+
"type": 'string',
|
|
36
|
+
"description": '検出されたリスクの種類 (例: XSS, CSRF, IDORなど)。3.3の診断対象脆弱性リストのいずれかであること。'
|
|
32
37
|
},
|
|
33
38
|
"location": {
|
|
34
|
-
"type":
|
|
35
|
-
"description":
|
|
39
|
+
"type": 'string',
|
|
40
|
+
"description": 'リスクが存在するコードのファイル名、行番号、またはコードスニペット。差分分析の場合は差分の該当箇所を示す形式 (例: ファイル名:+行番号) であること。'
|
|
36
41
|
},
|
|
37
42
|
"details": {
|
|
38
|
-
"type":
|
|
39
|
-
"description":
|
|
43
|
+
"type": 'string',
|
|
44
|
+
"description": 'リスクの詳細な説明と、なぜそれがリスクなのかの理由。'
|
|
40
45
|
},
|
|
41
46
|
"severity": {
|
|
42
|
-
"type":
|
|
43
|
-
"description":
|
|
44
|
-
"enum": [
|
|
47
|
+
"type": 'string',
|
|
48
|
+
"description": 'リスクの深刻度。',
|
|
49
|
+
"enum": %w[Critical High Medium Low Info]
|
|
45
50
|
},
|
|
46
51
|
"code_snippet": {
|
|
47
|
-
"type":
|
|
48
|
-
"description":
|
|
52
|
+
"type": 'string',
|
|
53
|
+
"description": '該当するコードスニペット。'
|
|
49
54
|
}
|
|
50
55
|
},
|
|
51
|
-
"required": [
|
|
56
|
+
"required": %w[type location details severity]
|
|
52
57
|
}
|
|
53
58
|
}
|
|
54
59
|
},
|
|
55
|
-
"required": [
|
|
60
|
+
"required": ['security_risks']
|
|
56
61
|
}.freeze # Freeze the hash to make it immutable
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
:xss, :csrf, :idor, :open_redirect, :ssrf, :session_fixation
|
|
61
|
-
# TODO: Add other risks from requirements based on PromptManager::RISK_PROMPTS.keys
|
|
62
|
-
].freeze
|
|
63
|
+
|
|
64
|
+
DEFAULT_MODEL = AIAnalysisEngine::LLMClient::DEFAULT_MODEL
|
|
63
65
|
|
|
64
66
|
# Threshold for splitting large content (characters as a proxy for tokens)
|
|
65
67
|
# Can be overridden by config
|
|
@@ -72,25 +74,22 @@ module Omamori
|
|
|
72
74
|
@config = Omamori::Config.new
|
|
73
75
|
|
|
74
76
|
# Initialize components with configuration
|
|
75
|
-
api_key = @config.get("api_key", ENV["GEMINI_API_KEY"])
|
|
76
|
-
gemini_model = @config.get("model", "gemini-2.5-flash-preview-04-17")
|
|
77
|
-
@gemini_client = AIAnalysisEngine::GeminiClient.new(api_key)
|
|
78
77
|
@prompt_manager = AIAnalysisEngine::PromptManager.new(@config)
|
|
79
78
|
|
|
80
|
-
chunk_size = @config.get(
|
|
79
|
+
chunk_size = @config.get('chunk_size', DEFAULT_SPLIT_THRESHOLD)
|
|
81
80
|
@diff_splitter = AIAnalysisEngine::DiffSplitter.new(chunk_size: chunk_size)
|
|
82
81
|
|
|
83
|
-
report_config = @config.get(
|
|
84
|
-
report_output_path = report_config.fetch(
|
|
85
|
-
html_template_path = report_config.fetch(
|
|
82
|
+
report_config = @config.get('report', {})
|
|
83
|
+
report_output_path = report_config.fetch('output_path', './omamori_report')
|
|
84
|
+
html_template_path = report_config.fetch('html_template', nil)
|
|
86
85
|
|
|
87
86
|
@console_formatter = ReportGenerator::ConsoleFormatter.new
|
|
88
87
|
@html_formatter = ReportGenerator::HTMLFormatter.new(report_output_path, html_template_path)
|
|
89
88
|
@json_formatter = ReportGenerator::JSONFormatter.new(report_output_path)
|
|
90
89
|
|
|
91
|
-
static_analyser_config = @config.get(
|
|
92
|
-
brakeman_options = static_analyser_config.fetch(
|
|
93
|
-
bundler_audit_options = static_analyser_config.fetch(
|
|
90
|
+
static_analyser_config = @config.get('static_analysers', {})
|
|
91
|
+
brakeman_options = static_analyser_config.fetch('brakeman', {}).fetch('options', {})
|
|
92
|
+
bundler_audit_options = static_analyser_config.fetch('bundler_audit', {}).fetch('options', {})
|
|
94
93
|
@brakeman_runner = StaticAnalysers::BrakemanRunner.new(brakeman_options)
|
|
95
94
|
@bundler_audit_runner = StaticAnalysers::BundlerAuditRunner.new(bundler_audit_options)
|
|
96
95
|
end
|
|
@@ -100,14 +99,15 @@ module Omamori
|
|
|
100
99
|
|
|
101
100
|
case @options[:command]
|
|
102
101
|
when :scan
|
|
102
|
+
validate_ai_settings
|
|
103
103
|
# Initialize results
|
|
104
|
-
ai_analysis_result = {
|
|
104
|
+
ai_analysis_result = { 'security_risks' => [] }
|
|
105
105
|
brakeman_result = nil
|
|
106
106
|
bundler_audit_result = nil
|
|
107
107
|
|
|
108
108
|
# Run static analysers first unless --ai option is specified
|
|
109
109
|
unless @options[:only_ai]
|
|
110
|
-
puts
|
|
110
|
+
puts 'Running static analysers...'
|
|
111
111
|
brakeman_result = @brakeman_runner.run
|
|
112
112
|
bundler_audit_result = @bundler_audit_runner.run
|
|
113
113
|
end
|
|
@@ -117,71 +117,86 @@ module Omamori
|
|
|
117
117
|
when :paths
|
|
118
118
|
# Scan specified files/directories
|
|
119
119
|
if @target_paths.empty?
|
|
120
|
-
puts
|
|
120
|
+
puts 'No paths specified for scan. Use --diff, --all, or provide paths.'
|
|
121
121
|
else
|
|
122
|
-
puts
|
|
122
|
+
puts 'Scanning specified paths with AI...'
|
|
123
123
|
ignore_patterns = @config.ignore_patterns
|
|
124
124
|
force_scan_ignored = @options.fetch(:force_scan_ignored, false)
|
|
125
125
|
files_to_scan = collect_files_from_paths(@target_paths, ignore_patterns, force_scan_ignored)
|
|
126
126
|
|
|
127
127
|
if files_to_scan.empty?
|
|
128
|
-
puts
|
|
128
|
+
puts 'No Ruby files found in the specified paths.'
|
|
129
|
+
elsif decision_engine?
|
|
130
|
+
ai_analysis_result = checklist_scanner.scan(read_units(files_to_scan), get_risks_to_check)
|
|
129
131
|
else
|
|
130
132
|
files_to_scan.each do |file_path|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
puts "Error analyzing file #{file_path}: #{e.message}"
|
|
133
|
+
file_content = File.read(file_path)
|
|
134
|
+
puts "Analyzing file: #{file_path}..." # スキャン中のファイルパスを表示
|
|
135
|
+
current_risks_to_check = get_risks_to_check
|
|
136
|
+
# @diff_splitterのインスタンス変数 @chunk_size を参照して比較
|
|
137
|
+
if file_content.length > @diff_splitter.instance_variable_get(:@chunk_size)
|
|
138
|
+
puts "File content exceeds threshold (#{@diff_splitter.instance_variable_get(:@chunk_size)} chars), splitting..."
|
|
139
|
+
file_ai_result = @diff_splitter.process_in_chunks(file_content, llm_client, JSON_SCHEMA,
|
|
140
|
+
@prompt_manager, current_risks_to_check, file_path: file_path, model: llm_model)
|
|
141
|
+
else
|
|
142
|
+
prompt = @prompt_manager.build_prompt(file_content, current_risks_to_check, JSON_SCHEMA,
|
|
143
|
+
file_path: file_path)
|
|
144
|
+
file_ai_result = llm_client.analyze(prompt, JSON_SCHEMA,
|
|
145
|
+
model: llm_model)
|
|
146
|
+
end
|
|
147
|
+
# Merge results
|
|
148
|
+
if file_ai_result && file_ai_result['security_risks']
|
|
149
|
+
ai_analysis_result['security_risks'].concat(file_ai_result['security_risks'])
|
|
149
150
|
end
|
|
151
|
+
rescue StandardError => e
|
|
152
|
+
puts "Error analyzing file #{file_path}: #{e.message}"
|
|
150
153
|
end
|
|
151
154
|
end
|
|
152
155
|
end
|
|
153
156
|
when :diff
|
|
154
|
-
# Scan staged differences
|
|
155
|
-
diff_content =
|
|
157
|
+
# Scan staged differences, or the branch diff when --base is given
|
|
158
|
+
diff_content = get_diff
|
|
156
159
|
if diff_content.empty?
|
|
157
|
-
puts "No staged changes to scan.
|
|
160
|
+
puts @options[:base] ? "No changes against #{@options[:base]} to scan." : 'No staged changes to scan.'
|
|
161
|
+
elsif decision_engine?
|
|
162
|
+
puts "Scanning differences (#{ai_settings.describe})..."
|
|
163
|
+
units = @diff_splitter.split_by_file(diff_content)
|
|
164
|
+
ai_analysis_result = checklist_scanner.scan(units, get_risks_to_check)
|
|
158
165
|
else
|
|
159
|
-
puts
|
|
166
|
+
puts 'Scanning differences with AI...'
|
|
160
167
|
current_risks_to_check = get_risks_to_check
|
|
161
168
|
# @diff_splitterのインスタンス変数 @chunk_size を参照して比較
|
|
162
169
|
if diff_content.length > @diff_splitter.instance_variable_get(:@chunk_size)
|
|
163
170
|
puts "Diff content exceeds threshold (#{@diff_splitter.instance_variable_get(:@chunk_size)} chars), splitting..."
|
|
164
|
-
ai_analysis_result = @diff_splitter.process_in_chunks(diff_content,
|
|
171
|
+
ai_analysis_result = @diff_splitter.process_in_chunks(diff_content, llm_client, JSON_SCHEMA,
|
|
172
|
+
@prompt_manager, current_risks_to_check, model: llm_model)
|
|
165
173
|
else
|
|
166
174
|
prompt = @prompt_manager.build_prompt(diff_content, current_risks_to_check, JSON_SCHEMA)
|
|
167
|
-
ai_analysis_result =
|
|
175
|
+
ai_analysis_result = llm_client.analyze(prompt, JSON_SCHEMA,
|
|
176
|
+
model: llm_model)
|
|
168
177
|
end
|
|
169
178
|
end
|
|
170
179
|
when :all
|
|
171
180
|
# Scan entire codebase
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
181
|
+
if decision_engine?
|
|
182
|
+
puts "Scanning entire codebase (#{ai_settings.describe})..."
|
|
183
|
+
files_to_scan = collect_files_from_paths(['.'], @config.ignore_patterns,
|
|
184
|
+
@options.fetch(:force_scan_ignored, false))
|
|
185
|
+
ai_analysis_result = checklist_scanner.scan(read_units(files_to_scan), get_risks_to_check)
|
|
186
|
+
elsif (full_code_content = get_full_codebase).strip.empty?
|
|
187
|
+
puts 'No code found to scan.'
|
|
175
188
|
else
|
|
176
|
-
puts
|
|
189
|
+
puts 'Scanning entire codebase with AI...'
|
|
177
190
|
current_risks_to_check = get_risks_to_check
|
|
178
191
|
# @diff_splitterのインスタンス変数 @chunk_size を参照して比較
|
|
179
192
|
if full_code_content.length > @diff_splitter.instance_variable_get(:@chunk_size)
|
|
180
193
|
puts "Full code content exceeds threshold (#{@diff_splitter.instance_variable_get(:@chunk_size)} chars), splitting..."
|
|
181
|
-
ai_analysis_result = @diff_splitter.process_in_chunks(full_code_content,
|
|
194
|
+
ai_analysis_result = @diff_splitter.process_in_chunks(full_code_content, llm_client, JSON_SCHEMA,
|
|
195
|
+
@prompt_manager, current_risks_to_check, model: llm_model)
|
|
182
196
|
else
|
|
183
197
|
prompt = @prompt_manager.build_prompt(full_code_content, current_risks_to_check, JSON_SCHEMA)
|
|
184
|
-
ai_analysis_result =
|
|
198
|
+
ai_analysis_result = llm_client.analyze(prompt, JSON_SCHEMA,
|
|
199
|
+
model: llm_model)
|
|
185
200
|
end
|
|
186
201
|
end
|
|
187
202
|
else
|
|
@@ -191,18 +206,23 @@ module Omamori
|
|
|
191
206
|
end
|
|
192
207
|
|
|
193
208
|
# Combine results and display report
|
|
194
|
-
ai_analysis_result ||= {
|
|
209
|
+
ai_analysis_result ||= { 'security_risks' => [] } # Ensure it's not nil
|
|
195
210
|
combined_results = combine_results(ai_analysis_result, brakeman_result, bundler_audit_result)
|
|
196
211
|
display_report(combined_results)
|
|
197
212
|
|
|
198
|
-
puts
|
|
213
|
+
puts 'Scan complete.'
|
|
199
214
|
|
|
200
215
|
when :ci_setup
|
|
216
|
+
validate_ai_settings
|
|
201
217
|
generate_ci_setup(@options[:ci_service])
|
|
202
218
|
|
|
203
219
|
when :init
|
|
204
220
|
generate_initial_files
|
|
205
221
|
|
|
222
|
+
when :doctor
|
|
223
|
+
validate_ai_settings
|
|
224
|
+
exit 1 unless Doctor.new(ai_settings, json_schema: JSON_SCHEMA).run
|
|
225
|
+
|
|
206
226
|
else
|
|
207
227
|
puts "Unknown command: #{@options[:command]}"
|
|
208
228
|
puts @opt_parser
|
|
@@ -212,72 +232,179 @@ module Omamori
|
|
|
212
232
|
private
|
|
213
233
|
|
|
214
234
|
def combine_results(ai_result, brakeman_result, bundler_audit_result)
|
|
215
|
-
formatted_bundler_audit_result = if bundler_audit_result && bundler_audit_result[
|
|
216
|
-
{
|
|
235
|
+
formatted_bundler_audit_result = if bundler_audit_result && bundler_audit_result['results']
|
|
236
|
+
{ 'scan' => { 'results' => bundler_audit_result['results'] } }
|
|
217
237
|
else
|
|
218
|
-
{
|
|
238
|
+
{ 'scan' => { 'results' => [] } }
|
|
219
239
|
end
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
240
|
+
{
|
|
241
|
+
'ai_security_risks' => ai_result && ai_result['security_risks'] ? ai_result['security_risks'] : [],
|
|
242
|
+
'static_analysis_results' => {
|
|
243
|
+
'brakeman' => brakeman_result,
|
|
244
|
+
'bundler_audit' => formatted_bundler_audit_result
|
|
225
245
|
}
|
|
226
246
|
}
|
|
227
|
-
combined
|
|
228
247
|
end
|
|
229
248
|
|
|
230
249
|
def get_risks_to_check
|
|
231
250
|
# 設定ファイルからチェック対象のリスクを取得し、シンボルの配列に変換する
|
|
232
|
-
#
|
|
233
|
-
configured_checks = @config.get(
|
|
234
|
-
configured_checks.
|
|
251
|
+
# 設定がない場合は全項目を使う(判定役なしでも、レポート役が全項目を一度に判定する)
|
|
252
|
+
configured_checks = @config.get('checks', AIAnalysisEngine::PromptManager::RISK_PROMPTS.keys).map(&:to_sym)
|
|
253
|
+
configured_checks.filter_map do |risk|
|
|
254
|
+
if (reason = AIAnalysisEngine::PromptManager::RETIRED_RISKS[risk])
|
|
255
|
+
puts "Note: '#{risk}' is no longer checked by omamori. #{reason}"
|
|
256
|
+
next
|
|
257
|
+
end
|
|
258
|
+
AIAnalysisEngine::PromptManager::RISK_ALIASES.fetch(risk, risk)
|
|
259
|
+
end.uniq
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Which classifier and reporter to use, from .omamorirc and the command line
|
|
263
|
+
def ai_settings
|
|
264
|
+
@ai_settings ||= AISettings.new(@config, ai_overrides)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def ai_overrides
|
|
268
|
+
@ai_overrides ||= {}
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Stops before any scanning when the classifier or reporter settings are wrong or missing
|
|
272
|
+
def validate_ai_settings
|
|
273
|
+
return if ai_settings.reporter?
|
|
274
|
+
|
|
275
|
+
puts AISettings.reporter_missing_message
|
|
276
|
+
exit 1
|
|
277
|
+
rescue AISettings::Error => e
|
|
278
|
+
puts "Configuration error: #{e.message}"
|
|
279
|
+
exit 1
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# The LLM that writes reports (and checks everything itself when there is no classifier)
|
|
283
|
+
def llm_client
|
|
284
|
+
@llm_client ||= ai_settings.build_reporter_client
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def llm_model
|
|
288
|
+
ai_settings.reporter_model
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def decision_engine?
|
|
292
|
+
ai_settings.classifier?
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def checklist_scanner
|
|
296
|
+
AIAnalysisEngine::ChecklistScanner.new(
|
|
297
|
+
decision_client: ai_settings.build_classifier_client,
|
|
298
|
+
llm_client: llm_client,
|
|
299
|
+
prompt_manager: @prompt_manager,
|
|
300
|
+
diff_splitter: @diff_splitter,
|
|
301
|
+
json_schema: JSON_SCHEMA,
|
|
302
|
+
model: llm_model,
|
|
303
|
+
threshold: ai_settings.threshold,
|
|
304
|
+
concurrency: ai_settings.classifier_concurrency,
|
|
305
|
+
report_concurrency: ai_settings.reporter_concurrency,
|
|
306
|
+
sub_checks: ai_settings.sub_checks?
|
|
307
|
+
)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def read_units(file_paths)
|
|
311
|
+
file_paths.filter_map do |file_path|
|
|
312
|
+
relative_path = Pathname.new(file_path).relative_path_from(Pathname.pwd).to_s
|
|
313
|
+
[relative_path, File.read(file_path)]
|
|
314
|
+
rescue StandardError => e
|
|
315
|
+
puts "Error reading file #{file_path}: #{e.message}"
|
|
316
|
+
nil
|
|
317
|
+
end
|
|
235
318
|
end
|
|
236
319
|
|
|
237
320
|
def parse_options
|
|
238
321
|
@opt_parser = OptionParser.new do |opts|
|
|
239
|
-
opts.banner =
|
|
240
|
-
opts.separator
|
|
241
|
-
opts.separator
|
|
242
|
-
opts.separator
|
|
243
|
-
opts.separator
|
|
244
|
-
opts.separator
|
|
245
|
-
opts.separator
|
|
246
|
-
opts.separator
|
|
247
|
-
opts.
|
|
322
|
+
opts.banner = 'Usage: omamori [command] [PATH...] [options]'
|
|
323
|
+
opts.separator ''
|
|
324
|
+
opts.separator 'Commands:'
|
|
325
|
+
opts.separator ' scan [PATH...] [options] : Scan specified files/directories or staged changes'
|
|
326
|
+
opts.separator ' init --reporter PROVIDER [options]'
|
|
327
|
+
opts.separator ' : Write .omamorirc and .omamoriignore (and a CI file with --ci)'
|
|
328
|
+
opts.separator ' doctor : Send each configured classifier/reporter a small request to check the setup'
|
|
329
|
+
opts.separator ' ci-setup --ci SERVICE : Generate CI/CD setup files from .omamorirc'
|
|
330
|
+
opts.separator ''
|
|
331
|
+
opts.separator 'Scan Options:'
|
|
332
|
+
opts.on('--diff', 'Scan only the staged differences (default if no PATH is specified)') do
|
|
248
333
|
@options[:scan_mode_explicit] = :diff
|
|
249
334
|
end
|
|
250
|
-
opts.on(
|
|
335
|
+
opts.on('--all', 'Scan the entire codebase') do
|
|
251
336
|
@options[:scan_mode_explicit] = :all
|
|
252
337
|
end
|
|
253
|
-
opts.on(
|
|
338
|
+
opts.on('--format FORMAT', %i[console html json], 'Output format (console, html, json)') do |format|
|
|
254
339
|
@options[:format] = format
|
|
255
340
|
end
|
|
256
|
-
opts.on(
|
|
341
|
+
opts.on('--ai', 'Run only AI analysis, skipping static analysers') do
|
|
257
342
|
@options[:only_ai] = true
|
|
258
343
|
end
|
|
259
|
-
opts.on(
|
|
344
|
+
opts.on('--base REF', 'Scan the diff between REF and HEAD (e.g. main) instead of staged changes') do |ref|
|
|
345
|
+
@options[:base] = ref
|
|
346
|
+
@options[:scan_mode_explicit] ||= :diff
|
|
347
|
+
end
|
|
348
|
+
opts.on('--classifier PROVIDER',
|
|
349
|
+
'Classifier that checks each risk as a yes/no question (typesafe, openai, ollama, or none)') do |provider|
|
|
350
|
+
ai_overrides[:classifier] = provider
|
|
351
|
+
end
|
|
352
|
+
opts.on('--classifier-model MODEL', 'Classifier model (e.g. jev-1.13.0, gpt-6-luna)') do |model|
|
|
353
|
+
ai_overrides[:classifier_model] = model
|
|
354
|
+
end
|
|
355
|
+
opts.on('--reporter PROVIDER',
|
|
356
|
+
'LLM that writes reports: any RubyLLM provider (gemini, anthropic, openai, ...), ' \
|
|
357
|
+
'or claude_code / codex to use a subscription CLI') do |provider|
|
|
358
|
+
ai_overrides[:reporter] = provider
|
|
359
|
+
end
|
|
360
|
+
opts.on('--reporter-model MODEL', 'Reporter model (e.g. gemini-3.8-flash, claude-sonnet-5-5, sonnet)') do |model|
|
|
361
|
+
ai_overrides[:reporter_model] = model
|
|
362
|
+
end
|
|
363
|
+
opts.on('--reporter-fallback PROVIDER',
|
|
364
|
+
'Reporter asked only when the reporter refuses or fails (e.g. claude_code), or none') do |provider|
|
|
365
|
+
ai_overrides[:reporter_fallback] = provider
|
|
366
|
+
end
|
|
367
|
+
opts.on('--reporter-fallback-model MODEL', 'Model for the fallback reporter') do |model|
|
|
368
|
+
ai_overrides[:reporter_fallback_model] = model
|
|
369
|
+
end
|
|
370
|
+
# Older options, kept for compatibility
|
|
371
|
+
opts.on('--engine ENGINE', %w[llm gemini jev openai], '(Deprecated) Use --classifier') do |engine|
|
|
372
|
+
ai_overrides[:classifier] = AISettings::LEGACY_ENGINES.fetch(engine, 'none')
|
|
373
|
+
end
|
|
374
|
+
opts.on('--llm LLM', %w[api gemini claude codex], '(Deprecated) Use --reporter') do |llm|
|
|
375
|
+
ai_overrides[:reporter] = AISettings::LEGACY_LLMS[llm] if AISettings::LEGACY_LLMS.key?(llm)
|
|
376
|
+
end
|
|
377
|
+
opts.on('--force-scan-ignored', 'Force scan files and directories listed in .omamoriignore') do
|
|
260
378
|
@options[:force_scan_ignored] = true
|
|
261
379
|
end
|
|
262
|
-
opts.separator
|
|
263
|
-
opts.separator
|
|
264
|
-
opts.on(
|
|
380
|
+
opts.separator ''
|
|
381
|
+
opts.separator 'Init Options (also --classifier, --classifier-model, --reporter, --reporter-model, ...):'
|
|
382
|
+
opts.on('--language LANG', 'Language of the report details written to .omamorirc (e.g. ja)') do |language|
|
|
383
|
+
@options[:language] = language
|
|
384
|
+
end
|
|
385
|
+
opts.on('--force', 'Overwrite an existing .omamorirc / CI file') do
|
|
386
|
+
@options[:force] = true
|
|
387
|
+
end
|
|
388
|
+
opts.separator ''
|
|
389
|
+
opts.separator 'CI Setup Options:'
|
|
390
|
+
opts.on('--ci SERVICE', %i[github_actions gitlab_ci],
|
|
391
|
+
'Generate setup for specified CI service (github_actions, gitlab_ci)') do |service|
|
|
265
392
|
@options[:ci_service] = service
|
|
266
393
|
end
|
|
267
|
-
opts.separator
|
|
268
|
-
opts.separator
|
|
269
|
-
opts.on(
|
|
394
|
+
opts.separator ''
|
|
395
|
+
opts.separator 'General Options:'
|
|
396
|
+
opts.on('-h', '--help', 'Prints this help') do
|
|
270
397
|
puts opts
|
|
271
398
|
exit
|
|
272
399
|
end
|
|
273
400
|
end
|
|
274
401
|
|
|
275
|
-
command_candidate = @args.first.to_s.downcase.to_sym
|
|
276
|
-
if [
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
402
|
+
command_candidate = @args.first.to_s.downcase.tr('-', '_').to_sym
|
|
403
|
+
@options[:command] = if %i[scan ci_setup init doctor].include?(command_candidate)
|
|
404
|
+
@args.shift.tr('-', '_').to_sym
|
|
405
|
+
else
|
|
406
|
+
:scan # Default command
|
|
407
|
+
end
|
|
281
408
|
|
|
282
409
|
begin
|
|
283
410
|
@opt_parser.parse!(@args) # Parse remaining arguments for options
|
|
@@ -291,26 +418,25 @@ module Omamori
|
|
|
291
418
|
exit 1
|
|
292
419
|
end
|
|
293
420
|
|
|
294
|
-
|
|
295
421
|
@target_paths = @args.dup
|
|
296
422
|
|
|
297
423
|
# scan コマンドの場合の scan_mode の決定ロジック
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
end
|
|
307
|
-
elsif !@target_paths.empty?
|
|
308
|
-
# パス指定があり、--diff や --all がない場合
|
|
424
|
+
return unless @options[:command] == :scan
|
|
425
|
+
|
|
426
|
+
if @options[:scan_mode_explicit]
|
|
427
|
+
# --diff または --all が明示的に指定された場合
|
|
428
|
+
@options[:scan_mode] = @options[:scan_mode_explicit]
|
|
429
|
+
# パス指定があり、かつ --all や --diff もある場合、パス指定を優先する
|
|
430
|
+
if !@target_paths.empty? && %i[all diff].include?(@options[:scan_mode])
|
|
431
|
+
puts "Warning: Paths provided with --#{@options[:scan_mode]}. Scanning specified paths instead of full codebase/diff."
|
|
309
432
|
@options[:scan_mode] = :paths
|
|
310
|
-
else
|
|
311
|
-
# パス指定がなく、--diff や --all もない場合 (例: omamori scan, omamori scan --ai)
|
|
312
|
-
@options[:scan_mode] = :diff # デフォルトは diff
|
|
313
433
|
end
|
|
434
|
+
elsif !@target_paths.empty?
|
|
435
|
+
# パス指定があり、--diff や --all がない場合
|
|
436
|
+
@options[:scan_mode] = :paths
|
|
437
|
+
else
|
|
438
|
+
# パス指定がなく、--diff や --all もない場合 (例: omamori scan, omamori scan --ai)
|
|
439
|
+
@options[:scan_mode] = :diff # デフォルトは diff
|
|
314
440
|
end
|
|
315
441
|
end
|
|
316
442
|
|
|
@@ -336,14 +462,12 @@ module Omamori
|
|
|
336
462
|
relative_file_path[current_pattern.chomp('/').length] == '/') # ディレクトリ内のファイルにマッチ
|
|
337
463
|
return !negated # マッチし、かつ否定パターンでなければ無視する
|
|
338
464
|
end
|
|
339
|
-
|
|
465
|
+
elsif File.fnmatch(current_pattern, relative_file_path, File::FNM_PATHNAME | File::FNM_DOTMATCH) || # FNM_DOTMATCH で隠しファイルも考慮
|
|
466
|
+
File.fnmatch(current_pattern, File.basename(relative_file_path), File::FNM_PATHNAME | File::FNM_DOTMATCH)
|
|
340
467
|
# ファイル名またはglobパターンにマッチするか確認
|
|
341
468
|
# File.fnmatch はシェルのglobのように動作する
|
|
342
469
|
# File::FNM_PATHNAME は '*' が '/' にマッチしないようにする
|
|
343
|
-
|
|
344
|
-
File.fnmatch(current_pattern, File.basename(relative_file_path), File::FNM_PATHNAME | File::FNM_DOTMATCH) # ファイル名のみでのマッチも考慮
|
|
345
|
-
return !negated # マッチし、かつ否定パターンでなければ無視する
|
|
346
|
-
end
|
|
470
|
+
return !negated # ファイル名のみでのマッチも考慮
|
|
347
471
|
end
|
|
348
472
|
end
|
|
349
473
|
false # どのパターンにもマッチしなければ無視しない
|
|
@@ -355,14 +479,15 @@ module Omamori
|
|
|
355
479
|
expanded_path = File.expand_path(path) # パスを絶対パスに展開
|
|
356
480
|
if File.file?(expanded_path)
|
|
357
481
|
# ファイルの場合、Rubyファイルであり、かつ無視パターンにマッチしないか確認
|
|
358
|
-
if File.extname(expanded_path) == '.rb' && !matches_ignore_pattern?(expanded_path, ignore_patterns,
|
|
482
|
+
if File.extname(expanded_path) == '.rb' && !matches_ignore_pattern?(expanded_path, ignore_patterns,
|
|
483
|
+
force_scan_ignored)
|
|
359
484
|
collected_files << expanded_path
|
|
360
485
|
end
|
|
361
486
|
elsif File.directory?(expanded_path)
|
|
362
487
|
# ディレクトリの場合、再帰的にRubyファイルを取得し、無視パターンを適用
|
|
363
|
-
Dir.glob(File.join(expanded_path,
|
|
488
|
+
Dir.glob(File.join(expanded_path, '**', '*.rb')).each do |file_path|
|
|
364
489
|
abs_file_path = File.expand_path(file_path) # globで見つかったパスも絶対パスに
|
|
365
|
-
|
|
490
|
+
unless matches_ignore_pattern?(abs_file_path, ignore_patterns, force_scan_ignored)
|
|
366
491
|
collected_files << abs_file_path
|
|
367
492
|
end
|
|
368
493
|
end
|
|
@@ -373,31 +498,38 @@ module Omamori
|
|
|
373
498
|
collected_files.uniq # 重複を除いて返す
|
|
374
499
|
end
|
|
375
500
|
|
|
501
|
+
def get_diff
|
|
502
|
+
@options[:base] ? get_branch_diff(@options[:base]) : get_staged_diff
|
|
503
|
+
end
|
|
504
|
+
|
|
376
505
|
def get_staged_diff
|
|
377
506
|
`git diff --staged`
|
|
378
507
|
end
|
|
379
508
|
|
|
509
|
+
# Changes on the current branch since it diverged from base (what a pull request contains)
|
|
510
|
+
def get_branch_diff(base)
|
|
511
|
+
`git diff #{Shellwords.escape("#{base}...HEAD")}`
|
|
512
|
+
end
|
|
513
|
+
|
|
380
514
|
def get_full_codebase
|
|
381
|
-
code_content =
|
|
515
|
+
code_content = ''
|
|
382
516
|
ignore_patterns = @config.ignore_patterns
|
|
383
517
|
force_scan_ignored = @options.fetch(:force_scan_ignored, false)
|
|
384
518
|
# カレントディレクトリ ('.') 内のRubyファイルを収集
|
|
385
519
|
files_to_scan = collect_files_from_paths(['.'], ignore_patterns, force_scan_ignored)
|
|
386
520
|
|
|
387
521
|
files_to_scan.each do |file_path|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
file_path # fallback to absolute path
|
|
394
|
-
end
|
|
395
|
-
code_content += "# File: #{relative_display_path}\n"
|
|
396
|
-
code_content += File.read(file_path)
|
|
397
|
-
code_content += "\n\n"
|
|
398
|
-
rescue => e
|
|
399
|
-
puts "Error reading file #{file_path}: #{e.message}"
|
|
522
|
+
# 表示用に相対パスを試みるが、エラーなら絶対パスを使用
|
|
523
|
+
relative_display_path = begin
|
|
524
|
+
Pathname.new(file_path).relative_path_from(Pathname.pwd).to_s
|
|
525
|
+
rescue ArgumentError
|
|
526
|
+
file_path # fallback to absolute path
|
|
400
527
|
end
|
|
528
|
+
code_content += "# File: #{relative_display_path}\n"
|
|
529
|
+
code_content += File.read(file_path)
|
|
530
|
+
code_content += "\n\n"
|
|
531
|
+
rescue StandardError => e
|
|
532
|
+
puts "Error reading file #{file_path}: #{e.message}"
|
|
401
533
|
end
|
|
402
534
|
code_content
|
|
403
535
|
end
|
|
@@ -407,251 +539,92 @@ module Omamori
|
|
|
407
539
|
when :console
|
|
408
540
|
puts @console_formatter.format(combined_results)
|
|
409
541
|
when :html
|
|
410
|
-
report_config = @config.get(
|
|
411
|
-
output_path_prefix = report_config.fetch(
|
|
542
|
+
report_config = @config.get('report', {})
|
|
543
|
+
output_path_prefix = report_config.fetch('output_path', './omamori_report')
|
|
412
544
|
output_path = "#{output_path_prefix}.html"
|
|
413
545
|
File.write(output_path, @html_formatter.format(combined_results))
|
|
414
546
|
puts "HTML report generated: #{output_path}"
|
|
415
547
|
when :json
|
|
416
|
-
report_config = @config.get(
|
|
417
|
-
output_path_prefix = report_config.fetch(
|
|
548
|
+
report_config = @config.get('report', {})
|
|
549
|
+
output_path_prefix = report_config.fetch('output_path', './omamori_report')
|
|
418
550
|
output_path = "#{output_path_prefix}.json"
|
|
419
551
|
File.write(output_path, @json_formatter.format(combined_results))
|
|
420
552
|
puts "JSON report generated: #{output_path}"
|
|
421
553
|
end
|
|
422
554
|
end
|
|
423
555
|
|
|
424
|
-
def generate_ci_setup(ci_service)
|
|
556
|
+
def generate_ci_setup(ci_service, settings = ai_settings)
|
|
425
557
|
case ci_service
|
|
426
558
|
when :github_actions
|
|
427
|
-
generate_github_actions_workflow
|
|
559
|
+
generate_github_actions_workflow(settings)
|
|
428
560
|
when :gitlab_ci
|
|
429
|
-
generate_gitlab_ci_workflow
|
|
561
|
+
generate_gitlab_ci_workflow(settings)
|
|
430
562
|
else
|
|
431
563
|
puts "Unsupported CI service: #{ci_service}. Supported: github_actions, gitlab_ci"
|
|
432
564
|
puts @opt_parser # ヘルプメッセージを表示
|
|
433
565
|
end
|
|
434
566
|
end
|
|
435
567
|
|
|
436
|
-
def generate_github_actions_workflow
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
on: [push, pull_request]
|
|
442
|
-
|
|
443
|
-
jobs:
|
|
444
|
-
security_scan:
|
|
445
|
-
runs-on: ubuntu-latest
|
|
446
|
-
|
|
447
|
-
steps:
|
|
448
|
-
- name: Checkout code
|
|
449
|
-
uses: actions/checkout@v4 # Recommended to use specific version
|
|
450
|
-
|
|
451
|
-
- name: Set up Ruby
|
|
452
|
-
uses: ruby/setup-ruby@v1 # Recommended to use specific version
|
|
453
|
-
with:
|
|
454
|
-
ruby-version: '3.0' # Specify your project's Ruby version
|
|
455
|
-
|
|
456
|
-
- name: Install dependencies
|
|
457
|
-
run: bundle install
|
|
458
|
-
|
|
459
|
-
# Optional: Cache gems to speed up future builds
|
|
460
|
-
# - name: Cache gems
|
|
461
|
-
# uses: actions/cache@v3
|
|
462
|
-
# with:
|
|
463
|
-
# path: vendor/bundle
|
|
464
|
-
# key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
|
|
465
|
-
# restore-keys: |
|
|
466
|
-
# ${{ runner.os }}-gems-
|
|
467
|
-
|
|
468
|
-
- name: Install Brakeman (if not in Gemfile)
|
|
469
|
-
run: gem install brakeman --no-document || true
|
|
470
|
-
|
|
471
|
-
- name: Install Bundler-Audit (if not in Gemfile)
|
|
472
|
-
run: gem install bundler-audit --no-document || true
|
|
473
|
-
|
|
474
|
-
- name: Run Omamori Scan
|
|
475
|
-
env:
|
|
476
|
-
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} # Ensure GEMINI_API_KEY is set in GitHub Secrets
|
|
477
|
-
# Example: Scan all files on push to main, diff on PRs
|
|
478
|
-
# This logic might need adjustment based on your workflow preference
|
|
479
|
-
run: |
|
|
480
|
-
if [ "$GITHUB_EVENT_NAME" == "pull_request" ]; then
|
|
481
|
-
bundle exec omamori scan --diff --format console
|
|
482
|
-
else
|
|
483
|
-
bundle exec omamori scan --all --format console
|
|
484
|
-
fi
|
|
485
|
-
YAML
|
|
486
|
-
ci_config = @config.get("ci_setup", {})
|
|
487
|
-
output_path = ci_config.fetch("github_actions_path", ".github/workflows/omamori_scan.yml")
|
|
488
|
-
FileUtils.mkdir_p(File.dirname(output_path)) # Ensure directory exists
|
|
489
|
-
File.write(output_path, workflow_content)
|
|
490
|
-
puts "GitHub Actions workflow generated: #{output_path}"
|
|
568
|
+
def generate_github_actions_workflow(settings = ai_settings)
|
|
569
|
+
path = @config.get('ci_setup', {}).fetch('github_actions_path', '.github/workflows/omamori_scan.yml')
|
|
570
|
+
write_setup_file(path, SetupFiles.github_actions(settings), 'GitHub Actions workflow')
|
|
571
|
+
puts "Register these as GitHub secrets: #{SetupFiles.secret_names(settings).join(', ')}" if SetupFiles.secret_names(settings).any?
|
|
572
|
+
puts "Note: #{SetupFiles::CLI_IN_CI_NOTE}" if SetupFiles.cli_providers(settings).any?
|
|
491
573
|
end
|
|
492
574
|
|
|
493
|
-
def generate_gitlab_ci_workflow
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
omamori_security_scan:
|
|
500
|
-
stage: security_scan
|
|
501
|
-
image: ruby:3.0 # Specify your project's Ruby version
|
|
502
|
-
# Cache gems
|
|
503
|
-
cache:
|
|
504
|
-
key:
|
|
505
|
-
files:
|
|
506
|
-
- Gemfile.lock
|
|
507
|
-
paths:
|
|
508
|
-
- vendor/bundle
|
|
509
|
-
before_script:
|
|
510
|
-
- apt-get update -qq && apt-get install -y --no-install-recommends nodejs # If needed for JS runtime
|
|
511
|
-
- gem install bundler --no-document
|
|
512
|
-
- bundle install --jobs $(nproc) --retry 3 --path vendor/bundle
|
|
513
|
-
- gem install brakeman --no-document || true
|
|
514
|
-
- gem install bundler-audit --no-document || true
|
|
515
|
-
script:
|
|
516
|
-
# Example: Scan all files on pipelines for the default branch, diff on merge requests
|
|
517
|
-
- |
|
|
518
|
-
if [ "$CI_PIPELINE_SOURCE" == "merge_request_event" ]; then
|
|
519
|
-
bundle exec omamori scan --diff --format console
|
|
520
|
-
else
|
|
521
|
-
bundle exec omamori scan --all --format console
|
|
522
|
-
fi
|
|
523
|
-
variables:
|
|
524
|
-
GEMINI_API_KEY: $GEMINI_API_KEY # Set GEMINI_API_KEY as a CI/CD variable in GitLab
|
|
525
|
-
rules:
|
|
526
|
-
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
527
|
-
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
528
|
-
YAML
|
|
529
|
-
ci_config = @config.get("ci_setup", {})
|
|
530
|
-
output_path = ci_config.fetch("gitlab_ci_path", ".gitlab-ci.yml")
|
|
531
|
-
File.write(output_path, workflow_content)
|
|
532
|
-
puts "GitLab CI workflow generated: #{output_path}"
|
|
575
|
+
def generate_gitlab_ci_workflow(settings = ai_settings)
|
|
576
|
+
path = @config.get('ci_setup', {}).fetch('gitlab_ci_path', '.gitlab-ci.yml')
|
|
577
|
+
write_setup_file(path, SetupFiles.gitlab_ci(settings), 'GitLab CI workflow')
|
|
578
|
+
puts "Register these as masked CI/CD variables: #{SetupFiles.secret_names(settings).join(', ')}" if SetupFiles.secret_names(settings).any?
|
|
579
|
+
puts "Note: #{SetupFiles::CLI_IN_CI_NOTE}" if SetupFiles.cli_providers(settings).any?
|
|
533
580
|
end
|
|
534
581
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
# Add files and directories to ignore during Omamori scans.
|
|
538
|
-
# Lines starting with # are comments.
|
|
539
|
-
# Globs are supported (e.g., *.tmp, spec/fixtures/)
|
|
540
|
-
# Negation with ! (e.g., !important.log) - not fully implemented in current basic matcher
|
|
541
|
-
|
|
542
|
-
# Log files
|
|
543
|
-
log/
|
|
544
|
-
*.log
|
|
582
|
+
# Settings from the command line only, so `init` does not depend on an existing .omamorirc
|
|
583
|
+
EMPTY_CONFIG = Struct.new(:values) { def get(key, default = nil) = values.fetch(key.to_s, default) }.new({})
|
|
545
584
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
# Build artifacts
|
|
566
|
-
pkg/
|
|
567
|
-
|
|
568
|
-
# Test files and fixtures (optional, consider if they contain sensitive examples)
|
|
569
|
-
# spec/
|
|
570
|
-
# test/
|
|
571
|
-
# features/
|
|
585
|
+
# `omamori init --reporter PROVIDER [--reporter-model MODEL] [--classifier PROVIDER] [--language LANG] [--ci SERVICE]`
|
|
586
|
+
def generate_initial_files
|
|
587
|
+
settings = init_settings
|
|
588
|
+
ok = write_setup_file(Omamori::Config::DEFAULT_CONFIG_PATH,
|
|
589
|
+
SetupFiles.omamorirc(settings, language: @options[:language]), 'Config file')
|
|
590
|
+
unless File.exist?(Omamori::Config::DEFAULT_IGNORE_PATH)
|
|
591
|
+
File.write(Omamori::Config::DEFAULT_IGNORE_PATH, SetupFiles::OMAMORIIGNORE)
|
|
592
|
+
puts ".omamoriignore file generated: #{Omamori::Config::DEFAULT_IGNORE_PATH}"
|
|
593
|
+
end
|
|
594
|
+
exit 1 unless ok
|
|
595
|
+
generate_ci_setup(@options[:ci_service], settings) if @options[:ci_service]
|
|
596
|
+
|
|
597
|
+
puts ''
|
|
598
|
+
puts 'Next steps:'
|
|
599
|
+
SetupFiles.credential_steps(settings).each { |step| puts " - #{step}" }
|
|
600
|
+
puts ' - Run `omamori doctor` to check the setup'
|
|
601
|
+
puts ' - Run `omamori scan --base main` to scan a branch'
|
|
602
|
+
end
|
|
572
603
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
604
|
+
def init_settings
|
|
605
|
+
unless ai_overrides[:reporter]
|
|
606
|
+
puts AISettings.reporter_missing_message(command: 'init')
|
|
607
|
+
puts "Reporter providers: claude_code, codex, or a RubyLLM provider (#{RubyLLM::Provider.providers.keys.join(', ')})"
|
|
608
|
+
exit 1
|
|
609
|
+
end
|
|
576
610
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
#
|
|
580
|
-
|
|
611
|
+
AISettings.new(EMPTY_CONFIG, ai_overrides)
|
|
612
|
+
rescue AISettings::Error => e
|
|
613
|
+
puts "Configuration error: #{e.message}"
|
|
614
|
+
exit 1
|
|
615
|
+
end
|
|
581
616
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
#
|
|
586
|
-
|
|
587
|
-
# Gemini API Key (required for AI analysis)
|
|
588
|
-
# You can also set this via the GEMINI_API_KEY environment variable.
|
|
589
|
-
# Example: api_key: "YOUR_GEMINI_API_KEY_HERE"
|
|
590
|
-
api_key: YOUR_GEMINI_API_KEY
|
|
591
|
-
|
|
592
|
-
# Gemini Model to use (optional, default: g emini-2.5-flash-preview-04-17)
|
|
593
|
-
# Example: model: "gemini-2.5-pro-preview-05-06"
|
|
594
|
-
# model: "gemini-2.5-flash-preview-04-17"
|
|
595
|
-
|
|
596
|
-
# Security checks to enable (optional, default: all implemented checks).
|
|
597
|
-
# Provide a list of symbols. Example:
|
|
598
|
-
# checks:
|
|
599
|
-
# - xss
|
|
600
|
-
# - csrf
|
|
601
|
-
# - idor
|
|
602
|
-
# - open_redirect
|
|
603
|
-
# # Add other risk symbols from Omamori::AIAnalysisEngine::PromptManager::RISK_PROMPTS.keys
|
|
604
|
-
|
|
605
|
-
# Custom prompt templates (optional).
|
|
606
|
-
# prompt_templates:
|
|
607
|
-
# default: |
|
|
608
|
-
# Analyze the following Ruby code for security vulnerabilities.
|
|
609
|
-
# Focus on: %{risk_list}.
|
|
610
|
-
# Report in JSON format: %{json_schema}.
|
|
611
|
-
# Code:
|
|
612
|
-
# %{code_content}
|
|
613
|
-
|
|
614
|
-
# Report output settings (optional).
|
|
615
|
-
# report:
|
|
616
|
-
# output_path: "./omamori_scan_results" # Prefix for html/json reports
|
|
617
|
-
# html_template: "custom_report_template.erb" # Path to custom ERB template
|
|
618
|
-
|
|
619
|
-
# Static analyser options (optional).
|
|
620
|
-
# Provide options as a hash.
|
|
621
|
-
# static_analysers:
|
|
622
|
-
# brakeman:
|
|
623
|
-
# options: {"--skip-checks": "BasicAuth", "--no-progress": true}
|
|
624
|
-
# bundler_audit:
|
|
625
|
-
# options: {quiet: true}
|
|
626
|
-
|
|
627
|
-
# Language for AI analysis details (optional, default: "en").
|
|
628
|
-
# Supported languages depend on the AI model.
|
|
629
|
-
# language: "ja"
|
|
630
|
-
|
|
631
|
-
# Chunk size for splitting large code content for AI analysis (optional, default: 8000 characters).
|
|
632
|
-
# chunk_size: 10000
|
|
633
|
-
|
|
634
|
-
# CI setup file paths (optional).
|
|
635
|
-
# ci_setup:
|
|
636
|
-
# github_actions_path: ".github/workflows/custom_omamori_scan.yml"
|
|
637
|
-
# gitlab_ci_path: ".custom-gitlab-ci.yml"
|
|
638
|
-
YAML
|
|
639
|
-
config_output_path = Omamori::Config::DEFAULT_CONFIG_PATH
|
|
640
|
-
if File.exist?(config_output_path)
|
|
641
|
-
puts "Config file already exists at #{config_output_path}. Aborting .omamorirc generation."
|
|
642
|
-
else
|
|
643
|
-
File.write(config_output_path, config_content)
|
|
644
|
-
puts "Config file generated: #{config_output_path}"
|
|
645
|
-
puts "IMPORTANT: Please open #{config_output_path} and replace 'YOUR_GEMINI_API_KEY' with your actual Gemini API key."
|
|
617
|
+
# Writes a file unless it exists (or --force). Returns false when it was left as it was.
|
|
618
|
+
def write_setup_file(path, content, label)
|
|
619
|
+
if File.exist?(path) && !@options[:force]
|
|
620
|
+
puts "#{label} already exists at #{path}. Use --force to overwrite it."
|
|
621
|
+
return false
|
|
646
622
|
end
|
|
647
623
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
File.write(ignore_output_path, DEFAULT_OMAMORIIGNORE_CONTENT)
|
|
653
|
-
puts ".omamoriignore file generated: #{ignore_output_path}"
|
|
654
|
-
end
|
|
624
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
625
|
+
File.write(path, content)
|
|
626
|
+
puts "#{label} generated: #{path}"
|
|
627
|
+
true
|
|
655
628
|
end
|
|
656
629
|
end
|
|
657
630
|
end
|