omamori 0.1.2 → 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 +34 -14
- data/README.md +202 -49
- data/README_ja.md +204 -46
- 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 +30 -22
- 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 +96 -19
- 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 +89 -69
- data/lib/omamori/core_runner.rb +485 -286
- 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 +61 -14
- data/lib/omamori/ai_analysis_engine/gemini_client.rb +0 -71
data/lib/omamori/core_runner.rb
CHANGED
|
@@ -1,93 +1,97 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'optparse'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
require_relative '
|
|
8
|
-
require_relative '
|
|
9
|
-
require_relative '
|
|
10
|
-
require_relative '
|
|
11
|
-
require_relative '
|
|
12
|
-
|
|
13
|
-
require_relative '
|
|
4
|
+
require 'fileutils' # FileUtils を require する
|
|
5
|
+
require 'pathname' # Pathname を require する
|
|
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'
|
|
11
|
+
require_relative 'ai_analysis_engine/prompt_manager'
|
|
12
|
+
require_relative 'ai_analysis_engine/diff_splitter'
|
|
13
|
+
require_relative 'ai_analysis_engine/checklist_scanner'
|
|
14
|
+
require_relative 'report_generator/console_formatter'
|
|
15
|
+
require_relative 'report_generator/html_formatter'
|
|
16
|
+
require_relative 'report_generator/json_formatter'
|
|
17
|
+
require_relative 'static_analysers/brakeman_runner'
|
|
18
|
+
require_relative 'static_analysers/bundler_audit_runner'
|
|
19
|
+
require 'json'
|
|
20
|
+
require_relative 'config'
|
|
14
21
|
|
|
15
22
|
module Omamori
|
|
16
23
|
class CoreRunner
|
|
17
24
|
# Define the JSON Schema for Structured Output
|
|
18
25
|
JSON_SCHEMA = {
|
|
19
|
-
"type":
|
|
26
|
+
"type": 'object',
|
|
20
27
|
"properties": {
|
|
21
28
|
"security_risks": {
|
|
22
|
-
"type":
|
|
23
|
-
"description":
|
|
29
|
+
"type": 'array',
|
|
30
|
+
"description": '検出されたセキュリティリスクのリスト。',
|
|
24
31
|
"items": {
|
|
25
|
-
"type":
|
|
32
|
+
"type": 'object',
|
|
26
33
|
"properties": {
|
|
27
34
|
"type": {
|
|
28
|
-
"type":
|
|
29
|
-
"description":
|
|
35
|
+
"type": 'string',
|
|
36
|
+
"description": '検出されたリスクの種類 (例: XSS, CSRF, IDORなど)。3.3の診断対象脆弱性リストのいずれかであること。'
|
|
30
37
|
},
|
|
31
38
|
"location": {
|
|
32
|
-
"type":
|
|
33
|
-
"description":
|
|
39
|
+
"type": 'string',
|
|
40
|
+
"description": 'リスクが存在するコードのファイル名、行番号、またはコードスニペット。差分分析の場合は差分の該当箇所を示す形式 (例: ファイル名:+行番号) であること。'
|
|
34
41
|
},
|
|
35
42
|
"details": {
|
|
36
|
-
"type":
|
|
37
|
-
"description":
|
|
43
|
+
"type": 'string',
|
|
44
|
+
"description": 'リスクの詳細な説明と、なぜそれがリスクなのかの理由。'
|
|
38
45
|
},
|
|
39
46
|
"severity": {
|
|
40
|
-
"type":
|
|
41
|
-
"description":
|
|
42
|
-
"enum": [
|
|
47
|
+
"type": 'string',
|
|
48
|
+
"description": 'リスクの深刻度。',
|
|
49
|
+
"enum": %w[Critical High Medium Low Info]
|
|
43
50
|
},
|
|
44
51
|
"code_snippet": {
|
|
45
|
-
"type":
|
|
46
|
-
"description":
|
|
52
|
+
"type": 'string',
|
|
53
|
+
"description": '該当するコードスニペット。'
|
|
47
54
|
}
|
|
48
55
|
},
|
|
49
|
-
"required": [
|
|
56
|
+
"required": %w[type location details severity]
|
|
50
57
|
}
|
|
51
58
|
}
|
|
52
59
|
},
|
|
53
|
-
"required": [
|
|
60
|
+
"required": ['security_risks']
|
|
54
61
|
}.freeze # Freeze the hash to make it immutable
|
|
55
62
|
|
|
56
|
-
# TODO: Get risks to check from config file
|
|
57
|
-
RISKS_TO_CHECK = [
|
|
58
|
-
:xss, :csrf, :idor, :open_redirect, :ssrf, :session_fixation
|
|
59
|
-
# TODO: Add other risks from requirements
|
|
60
|
-
].freeze
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
DEFAULT_MODEL = AIAnalysisEngine::LLMClient::DEFAULT_MODEL
|
|
65
|
+
|
|
66
|
+
# Threshold for splitting large content (characters as a proxy for tokens)
|
|
67
|
+
# Can be overridden by config
|
|
68
|
+
DEFAULT_SPLIT_THRESHOLD = 8000 # Characters
|
|
64
69
|
|
|
65
70
|
def initialize(args)
|
|
66
71
|
@args = args
|
|
67
|
-
@options = { command: :scan, format: :console } # Default command
|
|
68
|
-
@
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@
|
|
83
|
-
@
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
@
|
|
90
|
-
@bundler_audit_runner = StaticAnalysers::BundlerAuditRunner.new(bundler_audit_options) # Pass options
|
|
72
|
+
@options = { command: :scan, format: :console } # Default command and format
|
|
73
|
+
@target_paths = []
|
|
74
|
+
@config = Omamori::Config.new
|
|
75
|
+
|
|
76
|
+
# Initialize components with configuration
|
|
77
|
+
@prompt_manager = AIAnalysisEngine::PromptManager.new(@config)
|
|
78
|
+
|
|
79
|
+
chunk_size = @config.get('chunk_size', DEFAULT_SPLIT_THRESHOLD)
|
|
80
|
+
@diff_splitter = AIAnalysisEngine::DiffSplitter.new(chunk_size: chunk_size)
|
|
81
|
+
|
|
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)
|
|
85
|
+
|
|
86
|
+
@console_formatter = ReportGenerator::ConsoleFormatter.new
|
|
87
|
+
@html_formatter = ReportGenerator::HTMLFormatter.new(report_output_path, html_template_path)
|
|
88
|
+
@json_formatter = ReportGenerator::JSONFormatter.new(report_output_path)
|
|
89
|
+
|
|
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', {})
|
|
93
|
+
@brakeman_runner = StaticAnalysers::BrakemanRunner.new(brakeman_options)
|
|
94
|
+
@bundler_audit_runner = StaticAnalysers::BundlerAuditRunner.new(bundler_audit_options)
|
|
91
95
|
end
|
|
92
96
|
|
|
93
97
|
def run
|
|
@@ -95,178 +99,437 @@ module Omamori
|
|
|
95
99
|
|
|
96
100
|
case @options[:command]
|
|
97
101
|
when :scan
|
|
98
|
-
|
|
102
|
+
validate_ai_settings
|
|
103
|
+
# Initialize results
|
|
104
|
+
ai_analysis_result = { 'security_risks' => [] }
|
|
99
105
|
brakeman_result = nil
|
|
100
106
|
bundler_audit_result = nil
|
|
107
|
+
|
|
108
|
+
# Run static analysers first unless --ai option is specified
|
|
101
109
|
unless @options[:only_ai]
|
|
110
|
+
puts 'Running static analysers...'
|
|
102
111
|
brakeman_result = @brakeman_runner.run
|
|
103
112
|
bundler_audit_result = @bundler_audit_runner.run
|
|
104
113
|
end
|
|
105
114
|
|
|
106
|
-
# Perform AI analysis
|
|
107
|
-
analysis_result = nil
|
|
115
|
+
# Perform AI analysis based on scan mode
|
|
108
116
|
case @options[:scan_mode]
|
|
117
|
+
when :paths
|
|
118
|
+
# Scan specified files/directories
|
|
119
|
+
if @target_paths.empty?
|
|
120
|
+
puts 'No paths specified for scan. Use --diff, --all, or provide paths.'
|
|
121
|
+
else
|
|
122
|
+
puts 'Scanning specified paths with AI...'
|
|
123
|
+
ignore_patterns = @config.ignore_patterns
|
|
124
|
+
force_scan_ignored = @options.fetch(:force_scan_ignored, false)
|
|
125
|
+
files_to_scan = collect_files_from_paths(@target_paths, ignore_patterns, force_scan_ignored)
|
|
126
|
+
|
|
127
|
+
if files_to_scan.empty?
|
|
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)
|
|
131
|
+
else
|
|
132
|
+
files_to_scan.each do |file_path|
|
|
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'])
|
|
150
|
+
end
|
|
151
|
+
rescue StandardError => e
|
|
152
|
+
puts "Error analyzing file #{file_path}: #{e.message}"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
109
156
|
when :diff
|
|
110
|
-
|
|
157
|
+
# Scan staged differences, or the branch diff when --base is given
|
|
158
|
+
diff_content = get_diff
|
|
111
159
|
if diff_content.empty?
|
|
112
|
-
puts "No staged changes to scan.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
puts "Diff content exceeds threshold, splitting..."
|
|
118
|
-
analysis_result = @diff_splitter.process_in_chunks(diff_content, @gemini_client, JSON_SCHEMA, @prompt_manager, get_risks_to_check, model: @config.get("model", "gemini-1.5-pro-latest"))
|
|
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)
|
|
119
165
|
else
|
|
120
|
-
|
|
121
|
-
|
|
166
|
+
puts 'Scanning differences with AI...'
|
|
167
|
+
current_risks_to_check = get_risks_to_check
|
|
168
|
+
# @diff_splitterのインスタンス変数 @chunk_size を参照して比較
|
|
169
|
+
if diff_content.length > @diff_splitter.instance_variable_get(:@chunk_size)
|
|
170
|
+
puts "Diff content exceeds threshold (#{@diff_splitter.instance_variable_get(:@chunk_size)} chars), splitting..."
|
|
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)
|
|
173
|
+
else
|
|
174
|
+
prompt = @prompt_manager.build_prompt(diff_content, current_risks_to_check, JSON_SCHEMA)
|
|
175
|
+
ai_analysis_result = llm_client.analyze(prompt, JSON_SCHEMA,
|
|
176
|
+
model: llm_model)
|
|
177
|
+
end
|
|
122
178
|
end
|
|
123
179
|
when :all
|
|
124
|
-
|
|
125
|
-
if
|
|
126
|
-
puts "
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
puts
|
|
132
|
-
analysis_result = @diff_splitter.process_in_chunks(full_code_content, @gemini_client, JSON_SCHEMA, @prompt_manager, get_risks_to_check, model: @config.get("model", "gemini-1.5-pro-latest"))
|
|
180
|
+
# Scan entire codebase
|
|
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.'
|
|
133
188
|
else
|
|
134
|
-
|
|
135
|
-
|
|
189
|
+
puts 'Scanning entire codebase with AI...'
|
|
190
|
+
current_risks_to_check = get_risks_to_check
|
|
191
|
+
# @diff_splitterのインスタンス変数 @chunk_size を参照して比較
|
|
192
|
+
if full_code_content.length > @diff_splitter.instance_variable_get(:@chunk_size)
|
|
193
|
+
puts "Full code content exceeds threshold (#{@diff_splitter.instance_variable_get(:@chunk_size)} chars), splitting..."
|
|
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)
|
|
196
|
+
else
|
|
197
|
+
prompt = @prompt_manager.build_prompt(full_code_content, current_risks_to_check, JSON_SCHEMA)
|
|
198
|
+
ai_analysis_result = llm_client.analyze(prompt, JSON_SCHEMA,
|
|
199
|
+
model: llm_model)
|
|
200
|
+
end
|
|
136
201
|
end
|
|
202
|
+
else
|
|
203
|
+
puts "Unknown scan mode: #{@options[:scan_mode]}"
|
|
204
|
+
puts @opt_parser
|
|
205
|
+
return # Exit if scan mode is invalid
|
|
137
206
|
end
|
|
138
207
|
|
|
139
208
|
# Combine results and display report
|
|
140
|
-
|
|
209
|
+
ai_analysis_result ||= { 'security_risks' => [] } # Ensure it's not nil
|
|
210
|
+
combined_results = combine_results(ai_analysis_result, brakeman_result, bundler_audit_result)
|
|
141
211
|
display_report(combined_results)
|
|
142
212
|
|
|
143
|
-
puts
|
|
213
|
+
puts 'Scan complete.'
|
|
144
214
|
|
|
145
215
|
when :ci_setup
|
|
216
|
+
validate_ai_settings
|
|
146
217
|
generate_ci_setup(@options[:ci_service])
|
|
147
218
|
|
|
148
219
|
when :init
|
|
149
|
-
|
|
220
|
+
generate_initial_files
|
|
221
|
+
|
|
222
|
+
when :doctor
|
|
223
|
+
validate_ai_settings
|
|
224
|
+
exit 1 unless Doctor.new(ai_settings, json_schema: JSON_SCHEMA).run
|
|
150
225
|
|
|
151
226
|
else
|
|
152
227
|
puts "Unknown command: #{@options[:command]}"
|
|
153
|
-
puts @opt_parser
|
|
228
|
+
puts @opt_parser
|
|
154
229
|
end
|
|
155
230
|
end
|
|
156
231
|
|
|
157
232
|
private
|
|
158
233
|
|
|
159
|
-
# Combine AI analysis results and static analyser results
|
|
160
234
|
def combine_results(ai_result, brakeman_result, bundler_audit_result)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
{ "scan" => { "results" => bundler_audit_result["results"] } }
|
|
235
|
+
formatted_bundler_audit_result = if bundler_audit_result && bundler_audit_result['results']
|
|
236
|
+
{ 'scan' => { 'results' => bundler_audit_result['results'] } }
|
|
164
237
|
else
|
|
165
|
-
|
|
166
|
-
{ "scan" => { "results" => [] } } # Or nil, depending on desired behavior when no results
|
|
238
|
+
{ 'scan' => { 'results' => [] } }
|
|
167
239
|
end
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
"bundler_audit" => formatted_bundler_audit_result # Use the transformed result
|
|
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
|
|
174
245
|
}
|
|
175
246
|
}
|
|
176
|
-
combined
|
|
177
247
|
end
|
|
178
248
|
|
|
179
|
-
# Default risks to check if not specified in config
|
|
180
|
-
DEFAULT_RISKS_TO_CHECK = [
|
|
181
|
-
:xss, :csrf, :idor, :open_redirect, :ssrf, :session_fixation
|
|
182
|
-
# TODO: Add other risks from requirements
|
|
183
|
-
].freeze
|
|
184
|
-
|
|
185
249
|
def get_risks_to_check
|
|
186
|
-
#
|
|
187
|
-
|
|
250
|
+
# 設定ファイルからチェック対象のリスクを取得し、シンボルの配列に変換する
|
|
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
|
|
188
318
|
end
|
|
189
319
|
|
|
190
320
|
def parse_options
|
|
191
321
|
@opt_parser = OptionParser.new do |opts|
|
|
192
|
-
opts.banner =
|
|
193
|
-
|
|
194
|
-
opts.separator
|
|
195
|
-
opts.separator
|
|
196
|
-
opts.separator
|
|
197
|
-
opts.separator
|
|
198
|
-
opts.separator
|
|
199
|
-
|
|
200
|
-
opts.separator
|
|
201
|
-
opts.separator
|
|
202
|
-
opts.on(
|
|
203
|
-
@options[:
|
|
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
|
|
333
|
+
@options[:scan_mode_explicit] = :diff
|
|
204
334
|
end
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
@options[:scan_mode] = :all
|
|
335
|
+
opts.on('--all', 'Scan the entire codebase') do
|
|
336
|
+
@options[:scan_mode_explicit] = :all
|
|
208
337
|
end
|
|
209
|
-
|
|
210
|
-
opts.on("--format FORMAT", [:console, :html, :json], "Output format (console, html, json)") do |format|
|
|
338
|
+
opts.on('--format FORMAT', %i[console html json], 'Output format (console, html, json)') do |format|
|
|
211
339
|
@options[:format] = format
|
|
212
340
|
end
|
|
213
|
-
|
|
214
|
-
opts.on("--ai", "Run only AI analysis, skipping static analysers") do
|
|
341
|
+
opts.on('--ai', 'Run only AI analysis, skipping static analysers') do
|
|
215
342
|
@options[:only_ai] = true
|
|
216
343
|
end
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
378
|
+
@options[:force_scan_ignored] = true
|
|
379
|
+
end
|
|
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|
|
|
221
392
|
@options[:ci_service] = service
|
|
222
393
|
end
|
|
223
|
-
|
|
224
|
-
opts.separator
|
|
225
|
-
opts.
|
|
226
|
-
opts.on("-h", "--help", "Prints this help") do
|
|
394
|
+
opts.separator ''
|
|
395
|
+
opts.separator 'General Options:'
|
|
396
|
+
opts.on('-h', '--help', 'Prints this help') do
|
|
227
397
|
puts opts
|
|
228
398
|
exit
|
|
229
399
|
end
|
|
230
400
|
end
|
|
231
401
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
408
|
+
|
|
409
|
+
begin
|
|
410
|
+
@opt_parser.parse!(@args) # Parse remaining arguments for options
|
|
411
|
+
rescue OptionParser::InvalidOption => e
|
|
412
|
+
puts "Error: #{e.message}"
|
|
413
|
+
puts @opt_parser
|
|
414
|
+
exit 1
|
|
415
|
+
rescue OptionParser::MissingArgument => e
|
|
416
|
+
puts "Error: #{e.message}"
|
|
417
|
+
puts @opt_parser
|
|
418
|
+
exit 1
|
|
239
419
|
end
|
|
240
420
|
|
|
241
|
-
@
|
|
421
|
+
@target_paths = @args.dup
|
|
242
422
|
|
|
243
|
-
#
|
|
244
|
-
|
|
423
|
+
# scan コマンドの場合の scan_mode の決定ロジック
|
|
424
|
+
return unless @options[:command] == :scan
|
|
245
425
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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."
|
|
432
|
+
@options[:scan_mode] = :paths
|
|
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
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def matches_ignore_pattern?(file_path, ignore_patterns, force_scan_ignored)
|
|
444
|
+
return false if force_scan_ignored # 強制スキャンが有効な場合は無視しない
|
|
445
|
+
|
|
446
|
+
# file_path をプロジェクトルートからの相対パスに正規化する
|
|
447
|
+
# Pathname を使用して堅牢なパス操作を行う
|
|
448
|
+
project_root = Pathname.pwd
|
|
449
|
+
absolute_file_path = Pathname.new(file_path).expand_path
|
|
450
|
+
relative_file_path = absolute_file_path.relative_path_from(project_root).to_s
|
|
451
|
+
|
|
452
|
+
ignore_patterns.each do |pattern|
|
|
453
|
+
negated = pattern.start_with?('!')
|
|
454
|
+
current_pattern = negated ? pattern[1..] : pattern
|
|
455
|
+
|
|
456
|
+
# パターンが '/' で終わる場合、ディレクトリ全体を対象とする
|
|
457
|
+
if current_pattern.end_with?('/')
|
|
458
|
+
# "dir/" のようなパターンは "dir/file.rb" や "dir/subdir/file.rb" にマッチする
|
|
459
|
+
# relative_file_path が current_pattern (末尾の '/' を除いたもの) で始まるか確認
|
|
460
|
+
if relative_file_path.start_with?(current_pattern.chomp('/')) &&
|
|
461
|
+
(relative_file_path.length == current_pattern.chomp('/').length || # ディレクトリ自体にマッチ (例: "dir" vs "dir/")
|
|
462
|
+
relative_file_path[current_pattern.chomp('/').length] == '/') # ディレクトリ内のファイルにマッチ
|
|
463
|
+
return !negated # マッチし、かつ否定パターンでなければ無視する
|
|
464
|
+
end
|
|
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)
|
|
467
|
+
# ファイル名またはglobパターンにマッチするか確認
|
|
468
|
+
# File.fnmatch はシェルのglobのように動作する
|
|
469
|
+
# File::FNM_PATHNAME は '*' が '/' にマッチしないようにする
|
|
470
|
+
return !negated # ファイル名のみでのマッチも考慮
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
false # どのパターンにもマッチしなければ無視しない
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def collect_files_from_paths(target_paths, ignore_patterns, force_scan_ignored)
|
|
477
|
+
collected_files = []
|
|
478
|
+
target_paths.each do |path|
|
|
479
|
+
expanded_path = File.expand_path(path) # パスを絶対パスに展開
|
|
480
|
+
if File.file?(expanded_path)
|
|
481
|
+
# ファイルの場合、Rubyファイルであり、かつ無視パターンにマッチしないか確認
|
|
482
|
+
if File.extname(expanded_path) == '.rb' && !matches_ignore_pattern?(expanded_path, ignore_patterns,
|
|
483
|
+
force_scan_ignored)
|
|
484
|
+
collected_files << expanded_path
|
|
485
|
+
end
|
|
486
|
+
elsif File.directory?(expanded_path)
|
|
487
|
+
# ディレクトリの場合、再帰的にRubyファイルを取得し、無視パターンを適用
|
|
488
|
+
Dir.glob(File.join(expanded_path, '**', '*.rb')).each do |file_path|
|
|
489
|
+
abs_file_path = File.expand_path(file_path) # globで見つかったパスも絶対パスに
|
|
490
|
+
unless matches_ignore_pattern?(abs_file_path, ignore_patterns, force_scan_ignored)
|
|
491
|
+
collected_files << abs_file_path
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
else
|
|
495
|
+
puts "Warning: Path not found or is not a file/directory: #{path}"
|
|
496
|
+
end
|
|
250
497
|
end
|
|
498
|
+
collected_files.uniq # 重複を除いて返す
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def get_diff
|
|
502
|
+
@options[:base] ? get_branch_diff(@options[:base]) : get_staged_diff
|
|
251
503
|
end
|
|
252
504
|
|
|
253
505
|
def get_staged_diff
|
|
254
506
|
`git diff --staged`
|
|
255
507
|
end
|
|
256
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
|
+
|
|
257
514
|
def get_full_codebase
|
|
258
|
-
code_content =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
515
|
+
code_content = ''
|
|
516
|
+
ignore_patterns = @config.ignore_patterns
|
|
517
|
+
force_scan_ignored = @options.fetch(:force_scan_ignored, false)
|
|
518
|
+
# カレントディレクトリ ('.') 内のRubyファイルを収集
|
|
519
|
+
files_to_scan = collect_files_from_paths(['.'], ignore_patterns, force_scan_ignored)
|
|
520
|
+
|
|
521
|
+
files_to_scan.each do |file_path|
|
|
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
|
|
269
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}"
|
|
270
533
|
end
|
|
271
534
|
code_content
|
|
272
535
|
end
|
|
@@ -276,156 +539,92 @@ module Omamori
|
|
|
276
539
|
when :console
|
|
277
540
|
puts @console_formatter.format(combined_results)
|
|
278
541
|
when :html
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
output_path_prefix = report_config.fetch("output_path", "./omamori_report")
|
|
542
|
+
report_config = @config.get('report', {})
|
|
543
|
+
output_path_prefix = report_config.fetch('output_path', './omamori_report')
|
|
282
544
|
output_path = "#{output_path_prefix}.html"
|
|
283
545
|
File.write(output_path, @html_formatter.format(combined_results))
|
|
284
546
|
puts "HTML report generated: #{output_path}"
|
|
285
547
|
when :json
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
output_path_prefix = report_config.fetch("output_path", "./omamori_report")
|
|
548
|
+
report_config = @config.get('report', {})
|
|
549
|
+
output_path_prefix = report_config.fetch('output_path', './omamori_report')
|
|
289
550
|
output_path = "#{output_path_prefix}.json"
|
|
290
551
|
File.write(output_path, @json_formatter.format(combined_results))
|
|
291
552
|
puts "JSON report generated: #{output_path}"
|
|
292
553
|
end
|
|
293
554
|
end
|
|
294
555
|
|
|
295
|
-
def generate_ci_setup(ci_service)
|
|
556
|
+
def generate_ci_setup(ci_service, settings = ai_settings)
|
|
296
557
|
case ci_service
|
|
297
558
|
when :github_actions
|
|
298
|
-
generate_github_actions_workflow
|
|
559
|
+
generate_github_actions_workflow(settings)
|
|
299
560
|
when :gitlab_ci
|
|
300
|
-
generate_gitlab_ci_workflow
|
|
561
|
+
generate_gitlab_ci_workflow(settings)
|
|
301
562
|
else
|
|
302
|
-
puts "Unsupported CI service: #{ci_service}"
|
|
563
|
+
puts "Unsupported CI service: #{ci_service}. Supported: github_actions, gitlab_ci"
|
|
564
|
+
puts @opt_parser # ヘルプメッセージを表示
|
|
303
565
|
end
|
|
304
566
|
end
|
|
305
567
|
|
|
306
|
-
def generate_github_actions_workflow
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
jobs:
|
|
314
|
-
security_scan:
|
|
315
|
-
runs-on: ubuntu-latest
|
|
316
|
-
|
|
317
|
-
steps:
|
|
318
|
-
- name: Checkout code
|
|
319
|
-
uses: actions/checkout@v4
|
|
320
|
-
|
|
321
|
-
- name: Set up Ruby
|
|
322
|
-
uses: ruby/setup-ruby@v1
|
|
323
|
-
with:
|
|
324
|
-
ruby-version: 2.7 # Or your project's Ruby version
|
|
325
|
-
|
|
326
|
-
- name: Install dependencies
|
|
327
|
-
run: bundle install
|
|
328
|
-
|
|
329
|
-
- name: Install Brakeman (if not in Gemfile)
|
|
330
|
-
run: gem install brakeman || true # Install if not already present
|
|
331
|
-
|
|
332
|
-
- name: Install Bundler-Audit (if not in Gemfile)
|
|
333
|
-
run: gem install bundler-audit || true # Install if not already present
|
|
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?
|
|
573
|
+
end
|
|
334
574
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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?
|
|
580
|
+
end
|
|
339
581
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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({})
|
|
584
|
+
|
|
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'
|
|
346
602
|
end
|
|
347
603
|
|
|
348
|
-
def
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
- gem install bundler # Ensure bundler is installed
|
|
360
|
-
- bundle install --jobs $(nproc) --retry 3 # Install dependencies
|
|
361
|
-
- gem install brakeman || true # Install Brakeman if not in Gemfile
|
|
362
|
-
- gem install bundler-audit || true # Install Bundler-Audit if not in Gemfile
|
|
363
|
-
script:
|
|
364
|
-
- bundle exec omamori scan --all --format console # Or --diff for diff scan
|
|
365
|
-
variables:
|
|
366
|
-
GEMINI_API_KEY: $GEMINI_API_KEY # Ensure you set GEMINI_API_KEY as a CI/CD variable in GitLab
|
|
367
|
-
# Optional: Define rules for when to run this job
|
|
368
|
-
# rules:
|
|
369
|
-
# - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
370
|
-
# - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
371
|
-
|
|
372
|
-
YAML
|
|
373
|
-
# Get output file path from config/options, default to .gitlab-ci.yml
|
|
374
|
-
ci_config = @config.get("ci_setup", {})
|
|
375
|
-
output_path = ci_config.fetch("gitlab_ci_path", ".gitlab-ci.yml")
|
|
376
|
-
File.write(output_path, workflow_content)
|
|
377
|
-
puts "GitLab CI workflow generated: #{output_path}"
|
|
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
|
|
610
|
+
|
|
611
|
+
AISettings.new(EMPTY_CONFIG, ai_overrides)
|
|
612
|
+
rescue AISettings::Error => e
|
|
613
|
+
puts "Configuration error: #{e.message}"
|
|
614
|
+
exit 1
|
|
378
615
|
end
|
|
379
616
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
#
|
|
384
|
-
|
|
385
|
-
# Gemini API Key (required for AI analysis)
|
|
386
|
-
# You can also set this via the GEMINI_API_KEY environment variable
|
|
387
|
-
api_key: YOUR_GEMINI_API_KEY # Replace with your actual API key
|
|
388
|
-
|
|
389
|
-
# Gemini Model to use (optional, default: gemini-1.5-pro-latest)
|
|
390
|
-
# model: gemini-1.5-flash-latest
|
|
391
|
-
|
|
392
|
-
# Security checks to enable (optional, default: all implemented checks)
|
|
393
|
-
# checks:
|
|
394
|
-
# xss: true
|
|
395
|
-
# csrf: true
|
|
396
|
-
# idor: true
|
|
397
|
-
# ...
|
|
398
|
-
|
|
399
|
-
# Custom prompt templates (optional)
|
|
400
|
-
# prompt_templates:
|
|
401
|
-
# default: |
|
|
402
|
-
# Your custom prompt template here...
|
|
403
|
-
|
|
404
|
-
# Report output settings (optional)
|
|
405
|
-
# report:
|
|
406
|
-
# output_path: ./omamori_report # Output directory/prefix for html/json reports
|
|
407
|
-
# html_template: path/to/custom/template.erb # Custom HTML template
|
|
408
|
-
|
|
409
|
-
# Static analyser options (optional)
|
|
410
|
-
# static_analysers:
|
|
411
|
-
# brakeman:
|
|
412
|
-
# options: "--force" # Additional Brakeman options
|
|
413
|
-
# bundler_audit:
|
|
414
|
-
# options: "--quiet" # Additional Bundler-Audit options
|
|
415
|
-
|
|
416
|
-
# Language setting for AI analysis details (optional, default: en)
|
|
417
|
-
# language: ja
|
|
418
|
-
|
|
419
|
-
YAML
|
|
420
|
-
# TODO: Specify output file path from options
|
|
421
|
-
output_path = Omamori::Config::DEFAULT_CONFIG_PATH
|
|
422
|
-
if File.exist?(output_path)
|
|
423
|
-
puts "Config file already exists at #{output_path}. Aborting init."
|
|
424
|
-
else
|
|
425
|
-
File.write(output_path, config_content)
|
|
426
|
-
puts "Config file generated: #{output_path}"
|
|
427
|
-
puts "Please replace 'YOUR_GEMINI_API_KEY' with your actual 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
|
|
428
622
|
end
|
|
623
|
+
|
|
624
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
625
|
+
File.write(path, content)
|
|
626
|
+
puts "#{label} generated: #{path}"
|
|
627
|
+
true
|
|
429
628
|
end
|
|
430
629
|
end
|
|
431
|
-
end
|
|
630
|
+
end
|