omamori 0.1.1 → 0.1.4
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/Gemfile +0 -3
- data/Gemfile.lock +12 -12
- data/README.md +32 -4
- data/README_ja.md +32 -4
- data/lib/omamori/ai_analysis_engine/diff_splitter.rb +14 -19
- data/lib/omamori/ai_analysis_engine/gemini_client.rb +4 -3
- data/lib/omamori/ai_analysis_engine/prompt_manager.rb +93 -42
- data/lib/omamori/config.rb +21 -0
- data/lib/omamori/core_runner.rb +383 -157
- data/lib/omamori/version.rb +1 -1
- metadata +44 -24
- data/demo_/ai_analysis_vulnerability.rb +0 -28
- data/demo_/csrf_vulnerability.rb +0 -31
- data/demo_/eval_vulnerability.rb +0 -29
- data/demo_/idor_vulnerability.rb +0 -39
- data/demo_/insecure_cookie_vulnerability.rb +0 -25
- data/demo_/open_redirect_vulnerability.rb +0 -22
- data/demo_/static_analysis_vulnerability.rb +0 -18
- data/demo_/xss_vulnerability.rb +0 -21
data/lib/omamori/core_runner.rb
CHANGED
@@ -1,16 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'optparse'
|
4
|
+
require 'fileutils' # FileUtils を require する
|
5
|
+
require 'pathname' # Pathname を require する
|
4
6
|
require_relative 'ai_analysis_engine/gemini_client'
|
5
|
-
require_relative 'ai_analysis_engine/prompt_manager'
|
6
|
-
require_relative 'ai_analysis_engine/diff_splitter'
|
7
|
-
require_relative 'report_generator/console_formatter'
|
8
|
-
require_relative 'report_generator/html_formatter'
|
9
|
-
require_relative 'report_generator/json_formatter'
|
10
|
-
require_relative 'static_analysers/brakeman_runner'
|
11
|
-
require_relative 'static_analysers/bundler_audit_runner'
|
12
|
-
require 'json'
|
13
|
-
require_relative 'config'
|
7
|
+
require_relative 'ai_analysis_engine/prompt_manager'
|
8
|
+
require_relative 'ai_analysis_engine/diff_splitter'
|
9
|
+
require_relative 'report_generator/console_formatter'
|
10
|
+
require_relative 'report_generator/html_formatter'
|
11
|
+
require_relative 'report_generator/json_formatter'
|
12
|
+
require_relative 'static_analysers/brakeman_runner'
|
13
|
+
require_relative 'static_analysers/bundler_audit_runner'
|
14
|
+
require 'json'
|
15
|
+
require_relative 'config'
|
14
16
|
|
15
17
|
module Omamori
|
16
18
|
class CoreRunner
|
@@ -53,41 +55,44 @@ module Omamori
|
|
53
55
|
"required": ["security_risks"]
|
54
56
|
}.freeze # Freeze the hash to make it immutable
|
55
57
|
|
56
|
-
#
|
57
|
-
|
58
|
+
# Default risks to check, can be overridden by config
|
59
|
+
DEFAULT_RISKS_TO_CHECK = [
|
58
60
|
:xss, :csrf, :idor, :open_redirect, :ssrf, :session_fixation
|
59
|
-
# TODO: Add other risks from requirements
|
61
|
+
# TODO: Add other risks from requirements based on PromptManager::RISK_PROMPTS.keys
|
60
62
|
].freeze
|
61
63
|
|
62
|
-
#
|
63
|
-
|
64
|
+
# Threshold for splitting large content (characters as a proxy for tokens)
|
65
|
+
# Can be overridden by config
|
66
|
+
DEFAULT_SPLIT_THRESHOLD = 8000 # Characters
|
64
67
|
|
65
68
|
def initialize(args)
|
66
69
|
@args = args
|
67
|
-
@options = { command: :scan, format: :console } # Default command
|
68
|
-
@
|
70
|
+
@options = { command: :scan, format: :console } # Default command and format
|
71
|
+
@target_paths = []
|
72
|
+
@config = Omamori::Config.new
|
69
73
|
|
70
|
-
# Initialize components with
|
71
|
-
api_key = @config.get("api_key", ENV["GEMINI_API_KEY"])
|
72
|
-
gemini_model = @config.get("model", "gemini-
|
74
|
+
# 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")
|
73
77
|
@gemini_client = AIAnalysisEngine::GeminiClient.new(api_key)
|
74
|
-
@prompt_manager = AIAnalysisEngine::PromptManager.new(@config)
|
75
|
-
|
76
|
-
chunk_size = @config.get("chunk_size",
|
77
|
-
@diff_splitter = AIAnalysisEngine::DiffSplitter.new(chunk_size: chunk_size)
|
78
|
-
|
78
|
+
@prompt_manager = AIAnalysisEngine::PromptManager.new(@config)
|
79
|
+
|
80
|
+
chunk_size = @config.get("chunk_size", DEFAULT_SPLIT_THRESHOLD)
|
81
|
+
@diff_splitter = AIAnalysisEngine::DiffSplitter.new(chunk_size: chunk_size)
|
82
|
+
|
79
83
|
report_config = @config.get("report", {})
|
80
84
|
report_output_path = report_config.fetch("output_path", "./omamori_report")
|
81
|
-
html_template_path = report_config.fetch("html_template", nil)
|
82
|
-
|
83
|
-
@
|
84
|
-
@
|
85
|
-
|
85
|
+
html_template_path = report_config.fetch("html_template", nil)
|
86
|
+
|
87
|
+
@console_formatter = ReportGenerator::ConsoleFormatter.new
|
88
|
+
@html_formatter = ReportGenerator::HTMLFormatter.new(report_output_path, html_template_path)
|
89
|
+
@json_formatter = ReportGenerator::JSONFormatter.new(report_output_path)
|
90
|
+
|
86
91
|
static_analyser_config = @config.get("static_analysers", {})
|
87
|
-
brakeman_options = static_analyser_config.fetch("brakeman", {}).fetch("options", {})
|
88
|
-
bundler_audit_options = static_analyser_config.fetch("bundler_audit", {}).fetch("options", {})
|
89
|
-
@brakeman_runner = StaticAnalysers::BrakemanRunner.new(brakeman_options)
|
90
|
-
@bundler_audit_runner = StaticAnalysers::BundlerAuditRunner.new(bundler_audit_options)
|
92
|
+
brakeman_options = static_analyser_config.fetch("brakeman", {}).fetch("options", {})
|
93
|
+
bundler_audit_options = static_analyser_config.fetch("bundler_audit", {}).fetch("options", {})
|
94
|
+
@brakeman_runner = StaticAnalysers::BrakemanRunner.new(brakeman_options)
|
95
|
+
@bundler_audit_runner = StaticAnalysers::BundlerAuditRunner.new(bundler_audit_options)
|
91
96
|
end
|
92
97
|
|
93
98
|
def run
|
@@ -95,49 +100,99 @@ module Omamori
|
|
95
100
|
|
96
101
|
case @options[:command]
|
97
102
|
when :scan
|
98
|
-
#
|
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
|
+
else
|
130
|
+
files_to_scan.each do |file_path|
|
131
|
+
begin
|
132
|
+
file_content = File.read(file_path)
|
133
|
+
puts "Analyzing file: #{file_path}..." # スキャン中のファイルパスを表示
|
134
|
+
current_risks_to_check = get_risks_to_check
|
135
|
+
# @diff_splitterのインスタンス変数 @chunk_size を参照して比較
|
136
|
+
if file_content.length > @diff_splitter.instance_variable_get(:@chunk_size)
|
137
|
+
puts "File content exceeds threshold (#{@diff_splitter.instance_variable_get(:@chunk_size)} chars), splitting..."
|
138
|
+
file_ai_result = @diff_splitter.process_in_chunks(file_content, @gemini_client, JSON_SCHEMA, @prompt_manager, current_risks_to_check, file_path: file_path, model: @config.get("model", "gemini-2.5-flash-preview-04-17"))
|
139
|
+
else
|
140
|
+
prompt = @prompt_manager.build_prompt(file_content, current_risks_to_check, JSON_SCHEMA, file_path: file_path)
|
141
|
+
file_ai_result = @gemini_client.analyze(prompt, JSON_SCHEMA, model: @config.get("model", "gemini-2.5-flash-preview-04-17"))
|
142
|
+
end
|
143
|
+
# Merge results
|
144
|
+
if file_ai_result && file_ai_result["security_risks"]
|
145
|
+
ai_analysis_result["security_risks"].concat(file_ai_result["security_risks"])
|
146
|
+
end
|
147
|
+
rescue => e
|
148
|
+
puts "Error analyzing file #{file_path}: #{e.message}"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
109
153
|
when :diff
|
154
|
+
# Scan staged differences
|
110
155
|
diff_content = get_staged_diff
|
111
156
|
if diff_content.empty?
|
112
157
|
puts "No staged changes to scan."
|
113
|
-
return
|
114
|
-
end
|
115
|
-
puts "Scanning staged differences with AI..."
|
116
|
-
if diff_content.length > SPLIT_THRESHOLD # TODO: Use token count
|
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"))
|
119
158
|
else
|
120
|
-
|
121
|
-
|
159
|
+
puts "Scanning staged differences with AI..."
|
160
|
+
current_risks_to_check = get_risks_to_check
|
161
|
+
# @diff_splitterのインスタンス変数 @chunk_size を参照して比較
|
162
|
+
if diff_content.length > @diff_splitter.instance_variable_get(:@chunk_size)
|
163
|
+
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, @gemini_client, JSON_SCHEMA, @prompt_manager, current_risks_to_check, model: @config.get("model", "gemini-2.5-flash-preview-04-17"))
|
165
|
+
else
|
166
|
+
prompt = @prompt_manager.build_prompt(diff_content, current_risks_to_check, JSON_SCHEMA)
|
167
|
+
ai_analysis_result = @gemini_client.analyze(prompt, JSON_SCHEMA, model: @config.get("model", "gemini-2.5-flash-preview-04-17"))
|
168
|
+
end
|
122
169
|
end
|
123
170
|
when :all
|
171
|
+
# Scan entire codebase
|
124
172
|
full_code_content = get_full_codebase
|
125
173
|
if full_code_content.strip.empty?
|
126
174
|
puts "No code found to scan."
|
127
|
-
return
|
128
|
-
end
|
129
|
-
puts "Scanning entire codebase with AI..."
|
130
|
-
if full_code_content.length > SPLIT_THRESHOLD # TODO: Use token count
|
131
|
-
puts "Full code content exceeds threshold, splitting..."
|
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"))
|
133
175
|
else
|
134
|
-
|
135
|
-
|
176
|
+
puts "Scanning entire codebase with AI..."
|
177
|
+
current_risks_to_check = get_risks_to_check
|
178
|
+
# @diff_splitterのインスタンス変数 @chunk_size を参照して比較
|
179
|
+
if full_code_content.length > @diff_splitter.instance_variable_get(:@chunk_size)
|
180
|
+
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, @gemini_client, JSON_SCHEMA, @prompt_manager, current_risks_to_check, model: @config.get("model", "gemini-2.5-flash-preview-04-17"))
|
182
|
+
else
|
183
|
+
prompt = @prompt_manager.build_prompt(full_code_content, current_risks_to_check, JSON_SCHEMA)
|
184
|
+
ai_analysis_result = @gemini_client.analyze(prompt, JSON_SCHEMA, model: @config.get("model", "gemini-2.5-flash-preview-04-17"))
|
185
|
+
end
|
136
186
|
end
|
187
|
+
else
|
188
|
+
puts "Unknown scan mode: #{@options[:scan_mode]}"
|
189
|
+
puts @opt_parser
|
190
|
+
return # Exit if scan mode is invalid
|
137
191
|
end
|
138
192
|
|
139
193
|
# Combine results and display report
|
140
|
-
|
194
|
+
ai_analysis_result ||= { "security_risks" => [] } # Ensure it's not nil
|
195
|
+
combined_results = combine_results(ai_analysis_result, brakeman_result, bundler_audit_result)
|
141
196
|
display_report(combined_results)
|
142
197
|
|
143
198
|
puts "Scan complete."
|
@@ -146,81 +201,69 @@ module Omamori
|
|
146
201
|
generate_ci_setup(@options[:ci_service])
|
147
202
|
|
148
203
|
when :init
|
149
|
-
|
204
|
+
generate_initial_files
|
150
205
|
|
151
206
|
else
|
152
207
|
puts "Unknown command: #{@options[:command]}"
|
153
|
-
puts @opt_parser
|
208
|
+
puts @opt_parser
|
154
209
|
end
|
155
210
|
end
|
156
211
|
|
157
212
|
private
|
158
213
|
|
159
|
-
# Combine AI analysis results and static analyser results
|
160
214
|
def combine_results(ai_result, brakeman_result, bundler_audit_result)
|
161
|
-
# Transform bundler_audit_result to match the expected structure in tests/formatters
|
162
215
|
formatted_bundler_audit_result = if bundler_audit_result && bundler_audit_result["results"]
|
163
216
|
{ "scan" => { "results" => bundler_audit_result["results"] } }
|
164
217
|
else
|
165
|
-
|
166
|
-
{ "scan" => { "results" => [] } } # Or nil, depending on desired behavior when no results
|
218
|
+
{ "scan" => { "results" => [] } }
|
167
219
|
end
|
168
|
-
|
169
220
|
combined = {
|
170
221
|
"ai_security_risks" => ai_result && ai_result["security_risks"] ? ai_result["security_risks"] : [],
|
171
222
|
"static_analysis_results" => {
|
172
223
|
"brakeman" => brakeman_result,
|
173
|
-
"bundler_audit" => formatted_bundler_audit_result
|
224
|
+
"bundler_audit" => formatted_bundler_audit_result
|
174
225
|
}
|
175
226
|
}
|
176
227
|
combined
|
177
228
|
end
|
178
229
|
|
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
230
|
def get_risks_to_check
|
186
|
-
#
|
187
|
-
|
231
|
+
# 設定ファイルからチェック対象のリスクを取得し、シンボルの配列に変換する
|
232
|
+
# 設定がない場合は DEFAULT_RISKS_TO_CHECK を使用する
|
233
|
+
configured_checks = @config.get("checks", DEFAULT_RISKS_TO_CHECK)
|
234
|
+
configured_checks.map(&:to_sym)
|
188
235
|
end
|
189
236
|
|
190
237
|
def parse_options
|
191
238
|
@opt_parser = OptionParser.new do |opts|
|
192
|
-
opts.banner = "Usage: omamori [command] [options]"
|
193
|
-
|
239
|
+
opts.banner = "Usage: omamori [command] [PATH...] [options]"
|
194
240
|
opts.separator ""
|
195
241
|
opts.separator "Commands:"
|
196
|
-
opts.separator " scan [options]
|
197
|
-
opts.separator " ci-setup [options]
|
198
|
-
opts.separator " init
|
199
|
-
|
242
|
+
opts.separator " scan [PATH...] [options] : Scan specified files/directories or staged changes"
|
243
|
+
opts.separator " ci-setup [options] : Generate CI/CD setup files"
|
244
|
+
opts.separator " init : Generate initial config file (.omamorirc) and .omamoriignore"
|
200
245
|
opts.separator ""
|
201
246
|
opts.separator "Scan Options:"
|
202
|
-
opts.on("--diff", "Scan only the staged differences (default)") do
|
203
|
-
@options[:
|
247
|
+
opts.on("--diff", "Scan only the staged differences (default if no PATH is specified)") do
|
248
|
+
@options[:scan_mode_explicit] = :diff
|
204
249
|
end
|
205
|
-
|
206
250
|
opts.on("--all", "Scan the entire codebase") do
|
207
|
-
@options[:
|
251
|
+
@options[:scan_mode_explicit] = :all
|
208
252
|
end
|
209
|
-
|
210
253
|
opts.on("--format FORMAT", [:console, :html, :json], "Output format (console, html, json)") do |format|
|
211
254
|
@options[:format] = format
|
212
255
|
end
|
213
|
-
|
214
256
|
opts.on("--ai", "Run only AI analysis, skipping static analysers") do
|
215
257
|
@options[:only_ai] = true
|
216
258
|
end
|
217
|
-
|
259
|
+
opts.on("--force-scan-ignored", "Force scan files and directories listed in .omamoriignore") do
|
260
|
+
@options[:force_scan_ignored] = true
|
261
|
+
end
|
218
262
|
opts.separator ""
|
219
263
|
opts.separator "CI Setup Options:"
|
220
264
|
opts.on("--ci SERVICE", [:github_actions, :gitlab_ci], "Generate setup for specified CI service (github_actions, gitlab_ci)") do |service|
|
221
265
|
@options[:ci_service] = service
|
222
266
|
end
|
223
|
-
|
224
267
|
opts.separator ""
|
225
268
|
opts.separator "General Options:"
|
226
269
|
opts.on("-h", "--help", "Prints this help") do
|
@@ -229,39 +272,127 @@ module Omamori
|
|
229
272
|
end
|
230
273
|
end
|
231
274
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
if [:scan, :ci_setup, :init].include?(command)
|
236
|
-
@options[:command] = @args.shift.to_sym # Consume the command argument from @args
|
275
|
+
command_candidate = @args.first.to_s.downcase.to_sym
|
276
|
+
if [:scan, :ci_setup, :init].include?(command_candidate)
|
277
|
+
@options[:command] = @args.shift.to_sym
|
237
278
|
else
|
238
|
-
@options[:command] = :scan # Default command
|
279
|
+
@options[:command] = :scan # Default command
|
280
|
+
end
|
281
|
+
|
282
|
+
begin
|
283
|
+
@opt_parser.parse!(@args) # Parse remaining arguments for options
|
284
|
+
rescue OptionParser::InvalidOption => e
|
285
|
+
puts "Error: #{e.message}"
|
286
|
+
puts @opt_parser
|
287
|
+
exit 1
|
288
|
+
rescue OptionParser::MissingArgument => e
|
289
|
+
puts "Error: #{e.message}"
|
290
|
+
puts @opt_parser
|
291
|
+
exit 1
|
239
292
|
end
|
240
293
|
|
241
|
-
@opt_parser.parse!(@args)
|
242
294
|
|
243
|
-
|
244
|
-
@options[:scan_mode] ||= :diff if @options[:command] == :scan
|
295
|
+
@target_paths = @args.dup
|
245
296
|
|
246
|
-
#
|
247
|
-
|
248
|
-
|
249
|
-
|
297
|
+
# scan コマンドの場合の scan_mode の決定ロジック
|
298
|
+
if @options[:command] == :scan
|
299
|
+
if @options[:scan_mode_explicit]
|
300
|
+
# --diff または --all が明示的に指定された場合
|
301
|
+
@options[:scan_mode] = @options[:scan_mode_explicit]
|
302
|
+
# パス指定があり、かつ --all や --diff もある場合、パス指定を優先する
|
303
|
+
if !@target_paths.empty? && (@options[:scan_mode] == :all || @options[:scan_mode] == :diff)
|
304
|
+
puts "Warning: Paths provided with --#{@options[:scan_mode]}. Scanning specified paths instead of full codebase/diff."
|
305
|
+
@options[:scan_mode] = :paths
|
306
|
+
end
|
307
|
+
elsif !@target_paths.empty?
|
308
|
+
# パス指定があり、--diff や --all がない場合
|
309
|
+
@options[:scan_mode] = :paths
|
310
|
+
else
|
311
|
+
# パス指定がなく、--diff や --all もない場合 (例: omamori scan, omamori scan --ai)
|
312
|
+
@options[:scan_mode] = :diff # デフォルトは diff
|
313
|
+
end
|
250
314
|
end
|
251
315
|
end
|
252
316
|
|
317
|
+
def matches_ignore_pattern?(file_path, ignore_patterns, force_scan_ignored)
|
318
|
+
return false if force_scan_ignored # 強制スキャンが有効な場合は無視しない
|
319
|
+
|
320
|
+
# file_path をプロジェクトルートからの相対パスに正規化する
|
321
|
+
# Pathname を使用して堅牢なパス操作を行う
|
322
|
+
project_root = Pathname.pwd
|
323
|
+
absolute_file_path = Pathname.new(file_path).expand_path
|
324
|
+
relative_file_path = absolute_file_path.relative_path_from(project_root).to_s
|
325
|
+
|
326
|
+
ignore_patterns.each do |pattern|
|
327
|
+
negated = pattern.start_with?('!')
|
328
|
+
current_pattern = negated ? pattern[1..] : pattern
|
329
|
+
|
330
|
+
# パターンが '/' で終わる場合、ディレクトリ全体を対象とする
|
331
|
+
if current_pattern.end_with?('/')
|
332
|
+
# "dir/" のようなパターンは "dir/file.rb" や "dir/subdir/file.rb" にマッチする
|
333
|
+
# relative_file_path が current_pattern (末尾の '/' を除いたもの) で始まるか確認
|
334
|
+
if relative_file_path.start_with?(current_pattern.chomp('/')) &&
|
335
|
+
(relative_file_path.length == current_pattern.chomp('/').length || # ディレクトリ自体にマッチ (例: "dir" vs "dir/")
|
336
|
+
relative_file_path[current_pattern.chomp('/').length] == '/') # ディレクトリ内のファイルにマッチ
|
337
|
+
return !negated # マッチし、かつ否定パターンでなければ無視する
|
338
|
+
end
|
339
|
+
else
|
340
|
+
# ファイル名またはglobパターンにマッチするか確認
|
341
|
+
# File.fnmatch はシェルのglobのように動作する
|
342
|
+
# File::FNM_PATHNAME は '*' が '/' にマッチしないようにする
|
343
|
+
if File.fnmatch(current_pattern, relative_file_path, File::FNM_PATHNAME | File::FNM_DOTMATCH) || # FNM_DOTMATCH で隠しファイルも考慮
|
344
|
+
File.fnmatch(current_pattern, File.basename(relative_file_path), File::FNM_PATHNAME | File::FNM_DOTMATCH) # ファイル名のみでのマッチも考慮
|
345
|
+
return !negated # マッチし、かつ否定パターンでなければ無視する
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
false # どのパターンにもマッチしなければ無視しない
|
350
|
+
end
|
351
|
+
|
352
|
+
def collect_files_from_paths(target_paths, ignore_patterns, force_scan_ignored)
|
353
|
+
collected_files = []
|
354
|
+
target_paths.each do |path|
|
355
|
+
expanded_path = File.expand_path(path) # パスを絶対パスに展開
|
356
|
+
if File.file?(expanded_path)
|
357
|
+
# ファイルの場合、Rubyファイルであり、かつ無視パターンにマッチしないか確認
|
358
|
+
if File.extname(expanded_path) == '.rb' && !matches_ignore_pattern?(expanded_path, ignore_patterns, force_scan_ignored)
|
359
|
+
collected_files << expanded_path
|
360
|
+
end
|
361
|
+
elsif File.directory?(expanded_path)
|
362
|
+
# ディレクトリの場合、再帰的にRubyファイルを取得し、無視パターンを適用
|
363
|
+
Dir.glob(File.join(expanded_path, "**", "*.rb")).each do |file_path|
|
364
|
+
abs_file_path = File.expand_path(file_path) # globで見つかったパスも絶対パスに
|
365
|
+
if !matches_ignore_pattern?(abs_file_path, ignore_patterns, force_scan_ignored)
|
366
|
+
collected_files << abs_file_path
|
367
|
+
end
|
368
|
+
end
|
369
|
+
else
|
370
|
+
puts "Warning: Path not found or is not a file/directory: #{path}"
|
371
|
+
end
|
372
|
+
end
|
373
|
+
collected_files.uniq # 重複を除いて返す
|
374
|
+
end
|
375
|
+
|
253
376
|
def get_staged_diff
|
254
377
|
`git diff --staged`
|
255
378
|
end
|
256
379
|
|
257
380
|
def get_full_codebase
|
258
381
|
code_content = ""
|
259
|
-
|
260
|
-
|
261
|
-
|
382
|
+
ignore_patterns = @config.ignore_patterns
|
383
|
+
force_scan_ignored = @options.fetch(:force_scan_ignored, false)
|
384
|
+
# カレントディレクトリ ('.') 内のRubyファイルを収集
|
385
|
+
files_to_scan = collect_files_from_paths(['.'], ignore_patterns, force_scan_ignored)
|
262
386
|
|
387
|
+
files_to_scan.each do |file_path|
|
263
388
|
begin
|
264
|
-
|
389
|
+
# 表示用に相対パスを試みるが、エラーなら絶対パスを使用
|
390
|
+
relative_display_path = begin
|
391
|
+
Pathname.new(file_path).relative_path_from(Pathname.pwd).to_s
|
392
|
+
rescue ArgumentError
|
393
|
+
file_path # fallback to absolute path
|
394
|
+
end
|
395
|
+
code_content += "# File: #{relative_display_path}\n"
|
265
396
|
code_content += File.read(file_path)
|
266
397
|
code_content += "\n\n"
|
267
398
|
rescue => e
|
@@ -276,14 +407,12 @@ module Omamori
|
|
276
407
|
when :console
|
277
408
|
puts @console_formatter.format(combined_results)
|
278
409
|
when :html
|
279
|
-
# Get output file path from config/options
|
280
410
|
report_config = @config.get("report", {})
|
281
411
|
output_path_prefix = report_config.fetch("output_path", "./omamori_report")
|
282
412
|
output_path = "#{output_path_prefix}.html"
|
283
413
|
File.write(output_path, @html_formatter.format(combined_results))
|
284
414
|
puts "HTML report generated: #{output_path}"
|
285
415
|
when :json
|
286
|
-
# Get output file path from config/options
|
287
416
|
report_config = @config.get("report", {})
|
288
417
|
output_path_prefix = report_config.fetch("output_path", "./omamori_report")
|
289
418
|
output_path = "#{output_path_prefix}.json"
|
@@ -299,7 +428,8 @@ module Omamori
|
|
299
428
|
when :gitlab_ci
|
300
429
|
generate_gitlab_ci_workflow
|
301
430
|
else
|
302
|
-
puts "Unsupported CI service: #{ci_service}"
|
431
|
+
puts "Unsupported CI service: #{ci_service}. Supported: github_actions, gitlab_ci"
|
432
|
+
puts @opt_parser # ヘルプメッセージを表示
|
303
433
|
end
|
304
434
|
end
|
305
435
|
|
@@ -316,31 +446,46 @@ module Omamori
|
|
316
446
|
|
317
447
|
steps:
|
318
448
|
- name: Checkout code
|
319
|
-
uses: actions/checkout@v4
|
449
|
+
uses: actions/checkout@v4 # Recommended to use specific version
|
320
450
|
|
321
451
|
- name: Set up Ruby
|
322
|
-
uses: ruby/setup-ruby@v1
|
452
|
+
uses: ruby/setup-ruby@v1 # Recommended to use specific version
|
323
453
|
with:
|
324
|
-
ruby-version:
|
454
|
+
ruby-version: '3.0' # Specify your project's Ruby version
|
325
455
|
|
326
456
|
- name: Install dependencies
|
327
457
|
run: bundle install
|
328
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
|
+
|
329
468
|
- name: Install Brakeman (if not in Gemfile)
|
330
|
-
run: gem install brakeman || true
|
469
|
+
run: gem install brakeman --no-document || true
|
331
470
|
|
332
471
|
- name: Install Bundler-Audit (if not in Gemfile)
|
333
|
-
run: gem install bundler-audit || true
|
472
|
+
run: gem install bundler-audit --no-document || true
|
334
473
|
|
335
474
|
- name: Run Omamori Scan
|
336
475
|
env:
|
337
|
-
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} # Ensure
|
338
|
-
|
339
|
-
|
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
|
340
485
|
YAML
|
341
|
-
# Get output file path from config/options, default to .github/workflows/omamori_scan.yml
|
342
486
|
ci_config = @config.get("ci_setup", {})
|
343
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
|
344
489
|
File.write(output_path, workflow_content)
|
345
490
|
puts "GitHub Actions workflow generated: #{output_path}"
|
346
491
|
end
|
@@ -353,79 +498,160 @@ module Omamori
|
|
353
498
|
|
354
499
|
omamori_security_scan:
|
355
500
|
stage: security_scan
|
356
|
-
image: ruby:
|
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
|
357
509
|
before_script:
|
358
|
-
- apt-get update -qq && apt-get install -y nodejs #
|
359
|
-
- gem install bundler
|
360
|
-
- bundle install --jobs $(nproc) --retry 3
|
361
|
-
- gem install brakeman || true
|
362
|
-
- gem install bundler-audit || true
|
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
|
363
515
|
script:
|
364
|
-
|
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
|
365
523
|
variables:
|
366
|
-
GEMINI_API_KEY: $GEMINI_API_KEY #
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
# - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
371
|
-
|
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"'
|
372
528
|
YAML
|
373
|
-
# Get output file path from config/options, default to .gitlab-ci.yml
|
374
529
|
ci_config = @config.get("ci_setup", {})
|
375
530
|
output_path = ci_config.fetch("gitlab_ci_path", ".gitlab-ci.yml")
|
376
531
|
File.write(output_path, workflow_content)
|
377
532
|
puts "GitLab CI workflow generated: #{output_path}"
|
378
533
|
end
|
379
534
|
|
380
|
-
|
535
|
+
DEFAULT_OMAMORIIGNORE_CONTENT = <<~IGNORE
|
536
|
+
# Omamori ignore file
|
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
|
545
|
+
|
546
|
+
# Temporary files
|
547
|
+
tmp/
|
548
|
+
*.tmp
|
549
|
+
*.swp
|
550
|
+
*.swo
|
551
|
+
|
552
|
+
# OS-specific files
|
553
|
+
.DS_Store
|
554
|
+
Thumbs.db
|
555
|
+
|
556
|
+
# Vendor directory (often contains third-party code)
|
557
|
+
vendor/bundle/
|
558
|
+
|
559
|
+
# Coverage reports
|
560
|
+
coverage/
|
561
|
+
|
562
|
+
# Node.js dependencies
|
563
|
+
node_modules/
|
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/
|
572
|
+
|
573
|
+
# Database schema and migrations (usually not directly exploitable via code injection)
|
574
|
+
# db/schema.rb
|
575
|
+
# db/migrate/
|
576
|
+
|
577
|
+
# Assets (compiled or static, less likely to have Ruby vulnerabilities)
|
578
|
+
# app/assets/builds/
|
579
|
+
# public/assets/
|
580
|
+
IGNORE
|
581
|
+
|
582
|
+
def generate_initial_files
|
381
583
|
config_content = <<~YAML
|
382
584
|
# .omamorirc
|
383
585
|
# Configuration file for omamori gem
|
384
586
|
|
385
587
|
# Gemini API Key (required for AI analysis)
|
386
|
-
# You can also set this via the GEMINI_API_KEY environment variable
|
387
|
-
|
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
|
388
591
|
|
389
|
-
# Gemini Model to use (optional, default:
|
390
|
-
# model: gemini-
|
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"
|
391
595
|
|
392
|
-
# Security checks to enable (optional, default: all implemented checks)
|
596
|
+
# Security checks to enable (optional, default: all implemented checks).
|
597
|
+
# Provide a list of symbols. Example:
|
393
598
|
# checks:
|
394
|
-
# xss
|
395
|
-
# csrf
|
396
|
-
# idor
|
397
|
-
#
|
599
|
+
# - xss
|
600
|
+
# - csrf
|
601
|
+
# - idor
|
602
|
+
# - open_redirect
|
603
|
+
# # Add other risk symbols from Omamori::AIAnalysisEngine::PromptManager::RISK_PROMPTS.keys
|
398
604
|
|
399
|
-
# Custom prompt templates (optional)
|
605
|
+
# Custom prompt templates (optional).
|
400
606
|
# prompt_templates:
|
401
607
|
# default: |
|
402
|
-
#
|
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}
|
403
613
|
|
404
|
-
# Report output settings (optional)
|
614
|
+
# Report output settings (optional).
|
405
615
|
# report:
|
406
|
-
# output_path: ./
|
407
|
-
# html_template:
|
616
|
+
# output_path: "./omamori_scan_results" # Prefix for html/json reports
|
617
|
+
# html_template: "custom_report_template.erb" # Path to custom ERB template
|
408
618
|
|
409
|
-
# Static analyser options (optional)
|
619
|
+
# Static analyser options (optional).
|
620
|
+
# Provide options as a hash.
|
410
621
|
# static_analysers:
|
411
622
|
# brakeman:
|
412
|
-
# options: "--
|
623
|
+
# options: {"--skip-checks": "BasicAuth", "--no-progress": true}
|
413
624
|
# bundler_audit:
|
414
|
-
# options:
|
415
|
-
|
416
|
-
# Language
|
417
|
-
#
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
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."
|
646
|
+
end
|
647
|
+
|
648
|
+
ignore_output_path = Omamori::Config::DEFAULT_IGNORE_PATH
|
649
|
+
if File.exist?(ignore_output_path)
|
650
|
+
puts ".omamoriignore file already exists at #{ignore_output_path}. Aborting .omamoriignore generation."
|
424
651
|
else
|
425
|
-
File.write(
|
426
|
-
puts "
|
427
|
-
puts "Please replace 'YOUR_GEMINI_API_KEY' with your actual API key."
|
652
|
+
File.write(ignore_output_path, DEFAULT_OMAMORIIGNORE_CONTENT)
|
653
|
+
puts ".omamoriignore file generated: #{ignore_output_path}"
|
428
654
|
end
|
429
655
|
end
|
430
656
|
end
|
431
|
-
end
|
657
|
+
end
|