omamori 0.1.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2c6e52c517c84f78c559299c80b6d0c3817ea6b1cc7cdb9e0bc041bb2c6ccad1
4
- data.tar.gz: c654919d2b1db48f3bb286e55ccd8909a82b9c69d4c3fd88d67fadfe03a621e7
3
+ metadata.gz: bafa258377208d15c678d88752804be6963159d28b83c13958de28bdd8bf8fa7
4
+ data.tar.gz: 9739ae26075e9e5c0f2f87eafd68f2ed6cce0baee719a4c31c1042e8b1c8abf9
5
5
  SHA512:
6
- metadata.gz: ded5134ca28c5835ba121b637c7105a5cbe6930777275562fbc49b45fcfc021ddb7d57f5c26c1bbb5958a56f52d1f7e9df6175de1274de19fee2683081db3bfa
7
- data.tar.gz: c495125630ff1467816c47c21888d4e984c08a5e074cc8f904a10045860c11e17212432ef146bd5a37324601589dca67c4ffe1546b2196518d9b3faa216e79cf
6
+ metadata.gz: 373c41e264bc7548b3b4f37a526ed98fcefdd6a51fc6eaa8571b18d7688e131b4852c7e4ce839c7cdd43e540c3bf83b7466a1a66c29cea7c5d6e03dcd59f5cac
7
+ data.tar.gz: 102078caed02ab830885759ddf21bc8430b15c6a829a5f4ed7c1fcca99360ac707bb938ab19a8f2f9a5ed8ad2ccd86427acfaecc1da6d55a48c27338ae753454
data/Gemfile.lock CHANGED
@@ -1,7 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- omamori (0.1.2)
4
+ omamori (0.1.4)
5
+ brakeman (~> 7.0)
6
+ bundler-audit (~> 0.9.2)
5
7
  colorize (~> 0.8)
6
8
  dotenv (~> 2.0)
7
9
  ruby-gemini-api (~> 0.1.1)
@@ -10,8 +12,13 @@ GEM
10
12
  remote: https://rubygems.org/
11
13
  specs:
12
14
  ast (2.4.3)
15
+ brakeman (7.0.2)
16
+ racc
17
+ bundler-audit (0.9.2)
18
+ bundler (>= 1.2.0, < 3)
19
+ thor (~> 1.0)
13
20
  colorize (0.8.1)
14
- diff-lcs (1.6.1)
21
+ diff-lcs (1.6.2)
15
22
  dotenv (2.8.1)
16
23
  faraday (2.13.1)
17
24
  faraday-net_http (>= 2.0, < 3.5)
@@ -21,8 +28,8 @@ GEM
21
28
  multipart-post (~> 2.0)
22
29
  faraday-net_http (3.4.0)
23
30
  net-http (>= 0.5.0)
24
- json (2.11.3)
25
- language_server-protocol (3.17.0.4)
31
+ json (2.12.0)
32
+ language_server-protocol (3.17.0.5)
26
33
  lint_roller (1.1.0)
27
34
  logger (1.7.0)
28
35
  multipart-post (2.4.1)
@@ -43,14 +50,14 @@ GEM
43
50
  rspec-mocks (~> 3.13.0)
44
51
  rspec-core (3.13.3)
45
52
  rspec-support (~> 3.13.0)
46
- rspec-expectations (3.13.3)
53
+ rspec-expectations (3.13.4)
47
54
  diff-lcs (>= 1.2.0, < 2.0)
48
55
  rspec-support (~> 3.13.0)
49
- rspec-mocks (3.13.2)
56
+ rspec-mocks (3.13.4)
50
57
  diff-lcs (>= 1.2.0, < 2.0)
51
58
  rspec-support (~> 3.13.0)
52
- rspec-support (3.13.2)
53
- rubocop (1.75.3)
59
+ rspec-support (3.13.3)
60
+ rubocop (1.75.6)
54
61
  json (~> 2.3)
55
62
  language_server-protocol (~> 3.17.0.2)
56
63
  lint_roller (~> 1.1.0)
@@ -69,6 +76,7 @@ GEM
69
76
  faraday-multipart (~> 1.0)
70
77
  json (~> 2.0)
71
78
  ruby-progressbar (1.13.0)
79
+ thor (1.3.2)
72
80
  unicode-display_width (3.1.4)
73
81
  unicode-emoji (~> 4.0, >= 4.0.4)
74
82
  unicode-emoji (4.0.4)
data/README.md CHANGED
@@ -15,11 +15,12 @@ Running the analysis multiple times may help reduce false negatives.
15
15
 
16
16
  ## Features
17
17
 
18
- - Scan staged changes (`git diff --staged`) or the entire codebase for security risks.
18
+ - Scan staged changes (`git diff --staged`), the entire codebase, or specified files and directories for security risks.
19
19
  - Integrates with static analysis tools like Brakeman and Bundler-Audit.
20
20
  - Utilizes the Gemini API for advanced code analysis to detect vulnerabilities.
21
21
  - Supports multiple report formats (console, HTML, JSON).
22
22
  - Configurable via a `.omamorirc` file.
23
+ - Exclusion of files and directories via a `.omamoriignore` file (except in `diff` mode).
23
24
 
24
25
  ## Installation
25
26
 
@@ -51,7 +52,7 @@ To generate an initial configuration file (`.omamorirc`), run:
51
52
  omamori init
52
53
  ```
53
54
 
54
- Edit the generated `.omamorirc` file to configure your Gemini API key, preferred model, checks to perform, and other settings.
55
+ Edit the generated `.omamorirc` and `.omamoriignore` files to configure your Gemini API key, preferred model, checks to perform, and files/directories to exclude from scanning, and other settings.
55
56
 
56
57
  ### Scanning
57
58
 
@@ -66,6 +67,7 @@ Scan the entire codebase:
66
67
  ```bash
67
68
  omamori scan --all
68
69
  ```
70
+ This mode respects the `.omamoriignore` file.
69
71
 
70
72
  Specify output format (console, html, json):
71
73
 
@@ -74,6 +76,22 @@ bundle exec omamori scan --format html
74
76
  bundle exec omamori scan --all --format json
75
77
  ```
76
78
 
79
+ ### Scan Specific Files/Directories
80
+
81
+ You can specify particular files or directories to scan:
82
+
83
+ ```bash
84
+ omamori scan <file_path1> <file_path2> ... <directory_path1> ...
85
+ ```
86
+
87
+ Example:
88
+
89
+ ```bash
90
+ omamori scan app/controllers/users_controller.rb app/models/user.rb config/routes.rb lib/
91
+ ```
92
+
93
+ This mode respects the `.omamoriignore` file.
94
+
77
95
  ### AI Analysis Only
78
96
 
79
97
  To perform only AI analysis without running static analysis tools, use the `--ai` option:
@@ -96,7 +114,7 @@ Here's a detailed breakdown of the configuration options:
96
114
  # You can also set this via the GEMINI_API_KEY environment variable
97
115
  api_key: YOUR_GEMINI_API_KEY # Replace with your actual API key
98
116
 
99
- # Gemini Model to use (optional, default: gemini-1.5-pro-latest)
117
+ # Gemini Model to use (optional, default: gemini-2.5-flash-preview-04-17)
100
118
  model: gemini-2.5-flash-preview-04-17
101
119
 
102
120
  # Security checks to enable (optional, default: all implemented checks)
@@ -128,7 +146,7 @@ model: gemini-2.5-flash-preview-04-17
128
146
  ```
129
147
 
130
148
  * `api_key`: Your API key for accessing the Gemini API. Can also be set via the `GEMINI_API_KEY` environment variable.
131
- * `model`: The Gemini model to use for AI analysis. Defaults to `gemini-1.5-pro-latest`.
149
+ * `model`: The Gemini model to use for AI analysis. Defaults to `gemini-2.5-flash-preview-04-17`.
132
150
  * `checks`: Configure which types of security checks to enable. By default, all implemented checks are enabled. You can selectively enable/disable checks here (e.g., `xss: true`, `csrf: false`).
133
151
  * `prompt_templates`: Define custom prompt templates for AI analysis.
134
152
  * `report`: Configure report output settings.
@@ -139,6 +157,16 @@ model: gemini-2.5-flash-preview-04-17
139
157
  * `bundler_audit`: Additional command-line options for Bundler-Audit.
140
158
  * `language`: Language setting for the details provided in AI analysis reports. Defaults to English (`en`).
141
159
 
160
+ ## .omamoriignore File
161
+
162
+ The `.omamoriignore` file allows you to exclude specific files and directories from the scan target. Its behavior is similar to a `.gitignore` file, but with the following considerations:
163
+
164
+ * **Effective Modes:** It is only effective in `--all` mode or when scanning specified files/directories.
165
+ * **Ineffective Mode:** In `diff` mode (i.e., `omamori scan` without arguments), the `.omamoriignore` file is ignored. This is because `diff` mode only targets changes retrieved by `git diff`.
166
+ * **Format:** Each line specifies one pattern. Lines starting with `#` are treated as comments. It is recommended to append a `/` to directory patterns (e.g., `vendor/`). Patterns are evaluated using simple prefix matching (e.g., `config/initializers` matches `config/initializers/devise.rb`). Wildcards (`*`) are not currently supported.
167
+
168
+ When you run the `omamori init` command, a `.omamoriignore` file pre-filled with patterns for common files and directories to ignore in a Rails project will be generated. You can edit this file as needed.
169
+
142
170
  ## Demo Files
143
171
 
144
172
  The `demo` directory contains example files with known vulnerabilities that can be used to demonstrate Omamori's capabilities.
data/README_ja.md CHANGED
@@ -13,11 +13,12 @@ AI解析は静的解析で診断できない脆弱性を発見することがで
13
13
 
14
14
  ## 特徴
15
15
 
16
- - ステージされた変更(`git diff --staged`)またはコードベース全体をスキャンしてセキュリティリスクを検出
16
+ - ステージされた変更(`git diff --staged`)、コードベース全体、または指定されたファイルやディレクトリをスキャンしてセキュリティリスクを検出
17
17
  - BrakemanやBundler-Auditなどの静的解析ツールと連携
18
18
  - Gemini APIを活用し、AIによる高度なコード脆弱性検出
19
19
  - 複数のレポート形式に対応(コンソール、HTML、JSON)
20
20
  - `.omamorirc`ファイルによる柔軟な設定が可能
21
+ - `.omamoriignore`ファイルによるスキャン対象からの除外機能(`diff`モードを除く)
21
22
 
22
23
  ## インストール
23
24
 
@@ -49,7 +50,7 @@ gem install omamori
49
50
  omamori init
50
51
  ```
51
52
 
52
- 生成された`.omamorirc`ファイルを編集して、Gemini APIキー、使用するモデル、実行するチェック項目などを設定します。
53
+ 生成された`.omamorirc`ファイルと`.omamoriignore`ファイルを編集して、Gemini APIキー、使用するモデル、実行するチェック項目、スキャン対象から除外するファイル/ディレクトリなどを設定します。
53
54
 
54
55
  ### スキャン
55
56
 
@@ -64,6 +65,7 @@ omamori scan
64
65
  ```bash
65
66
  omamori scan --all
66
67
  ```
68
+ このモードでは`.omamoriignore`ファイルが有効になります。
67
69
 
68
70
  出力形式を指定(コンソール、HTML、JSON):
69
71
 
@@ -72,6 +74,22 @@ bundle exec omamori scan --format html
72
74
  bundle exec omamori scan --all --format json
73
75
  ```
74
76
 
77
+ ### ファイル/ディレクトリ指定スキャン
78
+
79
+ 特定のファイルやディレクトリを指定してスキャンを実行します。
80
+
81
+ ```bash
82
+ omamori scan <ファイルパス1> <ファイルパス2> ... <ディレクトリパス1> ...
83
+ ```
84
+
85
+ 例:
86
+
87
+ ```bash
88
+ omamori scan app/controllers/users_controller.rb app/models/user.rb config/routes.rb lib/
89
+ ```
90
+
91
+ このモードでは`.omamoriignore`ファイルが有効になります。
92
+
75
93
  ### AI解析のみ実施
76
94
 
77
95
  静的解析ツールを使わず、AI解析のみを実行するには、`--ai`オプションを使用します:
@@ -94,7 +112,7 @@ omamori scan --ai
94
112
  # 環境変数GEMINI_API_KEYで設定することも可能
95
113
  api_key: YOUR_GEMINI_API_KEY # 実際のAPIキーに置き換えてください
96
114
 
97
- # 使用するGeminiモデル(任意、デフォルト: gemini-1.5-pro-latest
115
+ # 使用するGeminiモデル(任意、デフォルト: gemini-2.5-flash-preview-04-17
98
116
  model: gemini-2.5-flash-preview-04-17
99
117
 
100
118
  # 有効化するセキュリティチェック(任意、デフォルトは全チェック)
@@ -126,7 +144,7 @@ model: gemini-2.5-flash-preview-04-17
126
144
  ```
127
145
 
128
146
  - `api_key`: Gemini APIへのアクセスキー。環境変数`GEMINI_API_KEY`でも設定可能。
129
- - `model`: AI解析に使用するGeminiモデル。デフォルトは`gemini-1.5-pro-latest`。
147
+ - `model`: AI解析に使用するGeminiモデル。デフォルトは`gemini-2.5-flash-preview-04-17`。
130
148
  - `checks`: 実行するセキュリティチェックの設定。特定のチェックを有効/無効にできます(例:`xss: true`, `csrf: false`)。
131
149
  - `prompt_templates`: AI解析用のカスタムプロンプトテンプレートを設定。
132
150
  - `report`: レポート出力に関する設定。
@@ -135,6 +153,16 @@ model: gemini-2.5-flash-preview-04-17
135
153
  - `static_analysers`: 静的解析ツール(Brakeman、Bundler-Auditなど)の追加オプション設定。
136
154
  - `language`: AI解析結果の詳細説明文の言語設定。デフォルトは英語(`en`)。
137
155
 
156
+ ## .omamoriignore ファイル
157
+
158
+ `.omamoriignore`ファイルを使用すると、特定のファイルやディレクトリをスキャン対象から除外できます。これは`.gitignore`ファイルと似たような働きをしますが、以下の点に注意してください。
159
+
160
+ * **有効なモード:** `--all` モード、またはファイル/ディレクトリを指定してスキャンする場合にのみ有効です。
161
+ * **無効なモード:** `diff` モード(引数なしの `omamori scan`)では、`.omamoriignore` ファイルは無視されます。これは、`git diff` で取得された変更点のみを対象とするためです。
162
+ * **書式:** 1行に1つのパターンを記述します。`#` で始まる行はコメントとして扱われます。ディレクトリを指定する場合は、末尾に `/` を付けることを推奨します (例: `vendor/`)。単純な前方一致で評価されます。 (例: `config/initializers` は `config/initializers/devise.rb` にマッチします)ワイルドカード (`*`) は現時点ではサポートされていません。
163
+
164
+ `omamori init` コマンドを実行すると、一般的なRailsプロジェクトで無視すべきファイルやディレクトリのパターンが記述された`.omamoriignore`ファイルが生成されます。必要に応じてこのファイルを編集してください。
165
+
138
166
  ## デモファイル
139
167
 
140
168
  `demo`ディレクトリには、Omamoriの機能をデモンストレーションするための既知の脆弱性を含むサンプルファイルが置かれています。
@@ -1,12 +1,8 @@
1
- # frozen_string_literal: true
1
+ # lib/omamori/ai_analysis_engine/diff_splitter.rb
2
2
 
3
3
  module Omamori
4
4
  module AIAnalysisEngine
5
5
  class DiffSplitter
6
- # TODO: Determine appropriate chunk size based on token limits
7
- # Gemini 1.5 Pro has a large context window (1 million tokens),
8
- # but splitting might still be necessary for very large inputs
9
- # or to manage cost/latency.
10
6
  DEFAULT_CHUNK_SIZE = 8000 # Characters as a proxy for tokens
11
7
 
12
8
  def initialize(chunk_size: DEFAULT_CHUNK_SIZE)
@@ -17,8 +13,8 @@ module Omamori
17
13
  chunks = []
18
14
  current_chunk = ""
19
15
  content.each_line do |line|
20
- if (current_chunk.length + line.length) > @chunk_size
21
- chunks << current_chunk unless current_chunk.empty?
16
+ if (current_chunk.length + line.length) > @chunk_size && !current_chunk.empty?
17
+ chunks << current_chunk
22
18
  current_chunk = line
23
19
  else
24
20
  current_chunk += line
@@ -28,30 +24,29 @@ module Omamori
28
24
  chunks
29
25
  end
30
26
 
31
- def process_in_chunks(content, gemini_client, json_schema, prompt_manager, risks_to_check, model: "gemini-1.5-pro-latest")
27
+ # Updated to accept file_path keyword argument
28
+ def process_in_chunks(content, gemini_client, json_schema, prompt_manager, risks_to_check, model: "gemini-2.5-flash-preview-04-17", file_path: nil)
32
29
  all_results = []
33
30
  chunks = split(content)
34
31
 
35
- puts "Splitting content into #{chunks.size} chunks..."
32
+ puts "[DEBUG Omamori DiffSplitter] Splitting content into #{chunks.size} chunks for file: #{file_path || 'N/A'}"
36
33
 
37
34
  chunks.each_with_index do |chunk, index|
38
- puts "Processing chunk #{index + 1}/#{chunks.size}..."
39
- prompt = prompt_manager.build_prompt(chunk, risks_to_check, json_schema)
40
- result = gemini_client.analyze(prompt, json_schema, model: model)
35
+ puts "[DEBUG Omamori DiffSplitter] Processing chunk #{index + 1}/#{chunks.size} for file: #{file_path || 'N/A'}"
36
+ # Pass file_path (potentially modified for chunks) to build_prompt
37
+ chunk_file_path_info = if file_path
38
+ chunks.size > 1 ? "#{file_path} (chunk #{index + 1}/#{chunks.size})" : file_path
39
+ end
40
+ prompt = prompt_manager.build_prompt(chunk, risks_to_check, json_schema, file_path: chunk_file_path_info)
41
+ result = gemini_client.analyze(prompt, json_schema, model: model) # This call to analyze is correct
41
42
  all_results << result
42
- # TODO: Handle potential rate limits or errors between chunks
43
43
  end
44
-
45
- # TODO: Combine results from all chunks
46
44
  combine_results(all_results)
47
45
  end
48
46
 
49
47
  private
50
48
 
51
49
  def combine_results(results)
52
- # This is a placeholder. Combining results from multiple AI responses
53
- # requires careful consideration of overlapping findings, context, etc.
54
- # For now, just flatten the list of security risks.
55
50
  combined_risks = results.flat_map do |result|
56
51
  result && result["security_risks"] ? result["security_risks"] : []
57
52
  end
@@ -59,4 +54,4 @@ module Omamori
59
54
  end
60
55
  end
61
56
  end
62
- end
57
+ end
@@ -10,12 +10,12 @@ module Omamori
10
10
  @client = nil # Initialize client later
11
11
  end
12
12
 
13
- def analyze(prompt, json_schema, model: "gemini-1.5-pro-latest")
13
+ def analyze(prompt, json_schema, model: "gemini-2.5-flash-preview-04-17")
14
14
  # Ensure the client is initialized
15
15
  client
16
16
 
17
17
  begin
18
- response = @client.generate_content(
18
+ response = client.generate_content(
19
19
  prompt,
20
20
  model: model,
21
21
  response_schema: json_schema, # Use response_schema for Structured Output
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Omamori
4
2
  module AIAnalysisEngine
5
3
  class PromptManager
@@ -11,6 +9,7 @@ module Omamori
11
9
  %{json_schema}
12
10
  If no risks are found, output an empty list for the "security_risks" array.
13
11
  Please provide your response in %{language}.
12
+ #{"File context (if available): %{file_path}" if ENV['OMAMORI_DEBUG_PROMPT']}
14
13
 
15
14
  【Code to Analyze】:
16
15
  %{code_content}
@@ -48,9 +47,9 @@ dangerous_eval_prompt = <<~PROMPT
48
47
  Search for methods enabling dynamic code execution in Ruby code (e.g., eval, instance_eval, class_eval, send, public_send, system, exec, backticks `).
49
48
  Check if arguments passed to these methods originate from or are directly influenced by external untrusted input (e.g., HTTP request parameters params, data from files, network responses). Look for patterns similar to the vulnerable Ruby examples shown above.
50
49
  Verify if user input is rigorously sanitized or validated specifically to prevent code injection vectors before being used in these methods. Standard escaping for HTML (like XSS prevention) is not sufficient here. Check if execution is restricted only to a predefined, absolutely safe allowlist of commands or methods if dynamic execution cannot be avoided.
51
- Assess if safer alternatives exist that can achieve the same functionality without dynamic code execution. Examples include using Hash lookups for dispatching actions, case statements based on input values, leveraging safe templating engines, or using specific library functions designed for the task instead of generic execution methods.
50
+ Assess if safer alternatives exist that can achieve the same functionality without dynamic code execution. Examples include using Hash lookups for dispatching actions, case statements based on input values, leveraging safe templating engines, or using specific library functions designed for the task instead of generic execution methods.
52
51
  PROMPT
53
-
52
+
54
53
  RISK_PROMPTS = {
55
54
  xss: "Cross-Site Scripting (XSS): A vulnerability where user input is not properly escaped and is embedded into HTML or JavaScript, leading to arbitrary script execution in the victim's browser. Detection steps: 1) Identify where user input is output to HTML/JS context. 2) Check if proper encoding/escaping is applied (e.g., html_safe, raw, sanitize, escape_javascript). 3) Look for unsafe methods that bypass default Rails escaping (html_safe, raw, <%==). 4) Examine JavaScript that incorporates user input via template interpolation. 5) Check for improper content-type headers that might enable XSS. 6) Verify if user input is passed to eval(), setTimeout(), document.write() or DOM manipulation functions. 7) Look for attribute injection possibilities where user input sets HTML attributes.",
56
55
  csrf: "Cross-Site Request Forgery (CSRF): An attack that forces an authenticated user to perform unwanted actions via forged requests. Detection steps: 1) Check if CSRF protection is disabled globally or for specific controllers/actions (skip_before_action :verify_authenticity_token). 2) Look for APIs or endpoints that handle state-changing operations (POST, PUT, DELETE methods). 3) Verify if authenticity tokens are properly validated for forms and AJAX requests. 4) Check if the application relies solely on cookies for authentication without additional CSRF protection. 5) Look for custom CSRF protection implementations that might be incomplete. 6) Verify if SameSite cookie attributes are properly set. 7) Check if the application validates the Origin or Referer header for cross-origin requests.",
@@ -88,7 +87,7 @@ dangerous_eval_prompt = <<~PROMPT
88
87
  audit_log_missing: "Missing Audit Logging: Lack of logging for critical actions or authorization checks prevents accountability. Detection steps: 1) Identify code performing critical actions (login, permission change, sensitive data access/modification, config change). 2) Verify these actions generate logs including who (user), when (timestamp), what (action/resource), result (success/fail), and where (IP address). 3) Check if authentication successes/failures and authorization failures are logged. 4) Assess if logs are stored securely and retained appropriately. 5) Ensure log format is consistent and useful for monitoring.",
89
88
  time_based_side_channel: "Time-Based Side Channel: Execution time differences can leak secrets (e.g., timing attacks in string comparison). Detection steps: 1) Locate code comparing secret values (passwords, tokens, API keys). 2) Check if standard comparison operators (`==`) are used for secrets. 3) Verify use of constant-time comparison functions (e.g., `ActiveSupport::SecurityUtils.secure_compare`, `Rack::Utils.secure_compare`). 4) Analyze cryptographic operations for potential timing leaks (may depend on library implementation). 5) Consider if database query times varying based on input could leak information."
90
89
  }.freeze
91
-
90
+
92
91
 
93
92
  def initialize(config = {})
94
93
  # Load custom templates and language from config, merge with default
@@ -98,12 +97,30 @@ dangerous_eval_prompt = <<~PROMPT
98
97
  @language = config.get("language", "en") # Get language from config, default to 'en'
99
98
  end
100
99
 
101
- def build_prompt(code_content, risks_to_check, json_schema, template_key: :default)
102
- # Use the template from @prompt_templates, defaulting to :default if template_key is not found
100
+ # Updated to accept file_path keyword argument
101
+ def build_prompt(code_content, risks_to_check, json_schema, template_key: :default, file_path: nil)
103
102
  template = @prompt_templates.fetch(template_key, @prompt_templates[:default])
104
103
  risk_list = risks_to_check.map { |risk_key| @risk_prompts[risk_key] }.compact.join(", ")
105
104
 
106
- template % { risk_list: risk_list, code_content: code_content, json_schema: json_schema.to_json, language: @language}
105
+ prompt_variables = {
106
+ risk_list: risk_list,
107
+ code_content: code_content,
108
+ json_schema: json_schema.to_json,
109
+ language: @language
110
+ }
111
+ # Add file_path to variables if provided and template expects it
112
+ # Ensure your template string (DEFAULT_PROMPT_TEMPLATE or custom ones)
113
+ # actually uses %{file_path} if you want to include it.
114
+ prompt_variables[:file_path] = file_path if file_path && template.include?("%{file_path}")
115
+
116
+ # Handle cases where a key in prompt_variables might not be in the template string
117
+ # by selecting only keys present in the template.
118
+ final_variables = {}
119
+ template.scan(/%\{(\w+)\}/).flatten.uniq.each do |key_in_template|
120
+ final_variables[key_in_template.to_sym] = prompt_variables[key_in_template.to_sym]
121
+ end
122
+
123
+ template % final_variables
107
124
  end
108
125
  end
109
126
  end
@@ -5,10 +5,14 @@ require 'yaml'
5
5
  module Omamori
6
6
  class Config
7
7
  DEFAULT_CONFIG_PATH = ".omamorirc"
8
+ DEFAULT_IGNORE_PATH = ".omamoriignore"
9
+
10
+ attr_reader :ignore_patterns
8
11
 
9
12
  def initialize(config_path = DEFAULT_CONFIG_PATH)
10
13
  @config_path = config_path
11
14
  @config = load_config
15
+ @ignore_patterns = load_ignore_patterns # Load ignore patterns
12
16
  validate_config # Add validation after loading
13
17
  end
14
18
 
@@ -128,6 +132,23 @@ module Omamori
128
132
  end
129
133
  end
130
134
 
135
+ # Load .omamoriignore file and return an array of ignore patterns
136
+ def load_ignore_patterns
137
+ ignore_path = DEFAULT_IGNORE_PATH
138
+ if File.exist?(ignore_path)
139
+ begin
140
+ File.readlines(ignore_path, chomp: true).reject do |line|
141
+ line.strip.empty? || line.strip.start_with?('#')
142
+ end
143
+ rescue => e
144
+ puts "Warning: Error reading .omamoriignore file #{ignore_path}: #{e.message}"
145
+ [] # Return empty array if reading fails
146
+ end
147
+ else
148
+ [] # Return empty array if file does not exist
149
+ end
150
+ end
151
+
131
152
  def load_config
132
153
  if File.exist?(@config_path)
133
154
  begin
@@ -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' # Require PromptManager
6
- require_relative 'ai_analysis_engine/diff_splitter' # Require DiffSplitter
7
- require_relative 'report_generator/console_formatter' # Require ConsoleFormatter
8
- require_relative 'report_generator/html_formatter' # Require HTMLFormatter
9
- require_relative 'report_generator/json_formatter' # Require JSONFormatter
10
- require_relative 'static_analysers/brakeman_runner' # Require BrakemanRunner
11
- require_relative 'static_analysers/bundler_audit_runner' # Require BundlerAuditRunner
12
- require 'json' # Required for JSON Schema
13
- require_relative 'config' # Require Config class
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
- # TODO: Get risks to check from config file
57
- RISKS_TO_CHECK = [
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
- # TODO: Determine threshold for splitting based on token limits
63
- SPLIT_THRESHOLD = 7000 # Characters as a proxy for tokens
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 is scan, default format is console
68
- @config = Omamori::Config.new # Initialize Config
70
+ @options = { command: :scan, format: :console } # Default command and format
71
+ @target_paths = []
72
+ @config = Omamori::Config.new
69
73
 
70
- # Initialize components with config
71
- api_key = @config.get("api_key", ENV["GEMINI_API_KEY"]) # Get API key from config or environment variable
72
- gemini_model = @config.get("model", "gemini-1.5-pro-latest") # Get Gemini model from config
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) # Pass the entire config object
75
- # Get chunk size from config, default to 7000 characters if not specified
76
- chunk_size = @config.get("chunk_size", SPLIT_THRESHOLD)
77
- @diff_splitter = AIAnalysisEngine::DiffSplitter.new(chunk_size: chunk_size) # Pass chunk size to DiffSplitter
78
- # Get report output path and html template path from config
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) # Default to nil, formatter will use default template
82
- @console_formatter = ReportGenerator::ConsoleFormatter.new # TODO: Pass config for colors/options
83
- @html_formatter = ReportGenerator::HTMLFormatter.new(report_output_path, html_template_path) # Pass output path and template path
84
- @json_formatter = ReportGenerator::JSONFormatter.new(report_output_path) # Pass output path
85
- # Get static analyser options from config
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", {}) # Default to empty hash
88
- bundler_audit_options = static_analyser_config.fetch("bundler_audit", {}).fetch("options", {}) # Default to empty hash
89
- @brakeman_runner = StaticAnalysers::BrakemanRunner.new(brakeman_options) # Pass options
90
- @bundler_audit_runner = StaticAnalysers::BundlerAuditRunner.new(bundler_audit_options) # Pass 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
- # Run static analysers first unless --ai option is specified
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
- prompt = @prompt_manager.build_prompt(diff_content, get_risks_to_check, JSON_SCHEMA)
121
- analysis_result = @gemini_client.analyze(prompt, JSON_SCHEMA, model: @config.get("model", "gemini-1.5-pro-latest"))
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
- prompt = @prompt_manager.build_prompt(full_code_content, get_risks_to_check, JSON_SCHEMA)
135
- analysis_result = @gemini_client.analyze(prompt, JSON_SCHEMA, model: @config.get("model", "gemini-1.5-pro-latest"))
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
- combined_results = combine_results(analysis_result, brakeman_result, bundler_audit_result)
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
- generate_config_file # Generate initial config file
204
+ generate_initial_files
150
205
 
151
206
  else
152
207
  puts "Unknown command: #{@options[:command]}"
153
- puts @opt_parser # Display help for unknown command
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
- # Return a structure that formatters can handle gracefully
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 # Use the transformed 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
- # Get risks to check from config, default to hardcoded list if not specified
187
- @config.get("checks", DEFAULT_RISKS_TO_CHECK)
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] : Scan code or diff for security vulnerabilities"
197
- opts.separator " ci-setup [options] : Generate CI/CD setup files"
198
- opts.separator " init : Generate initial config file (.omamorirc)"
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[:scan_mode] = :diff
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[:scan_mode] = :all
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
- # Determine command before parsing options
233
- # Use @args instead of ARGV
234
- command = @args.first.to_s.downcase.to_sym rescue nil
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 is scan if not specified
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
- # Default scan mode to diff if command is scan and mode is not specified
244
- @options[:scan_mode] ||= :diff if @options[:command] == :scan
295
+ @target_paths = @args.dup
245
296
 
246
- # Display help if command is not recognized after parsing
247
- unless [:scan, :ci_setup, :init].include?(@options[:command])
248
- puts @opt_parser
249
- exit
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
- # TODO: Get target directories/files from config
260
- Dir.glob("**/*.rb").each do |file_path|
261
- next if file_path.include?("vendor/") || file_path.include?(".git/") || file_path.include?(".cline/") # Exclude vendor, .git, and .cline directories
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
- code_content += "# File: #{file_path}\n"
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: 2.7 # Or your project's 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 # Install if not already present
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 # Install if not already present
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 you add GEMINI_API_KEY to GitHub Secrets
338
- run: bundle exec omamori scan --all --format console # Or --diff for diff scan
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:latest # Use a Ruby image
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 # Install nodejs if needed for some tools
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
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
- - bundle exec omamori scan --all --format console # Or --diff for diff scan
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 # 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
-
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
- def generate_config_file
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
- api_key: YOUR_GEMINI_API_KEY # Replace with your actual API key
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: gemini-1.5-pro-latest)
390
- # model: gemini-1.5-flash-latest
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: true
395
- # csrf: true
396
- # idor: true
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
- # Your custom prompt template here...
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: ./omamori_report # Output directory/prefix for html/json reports
407
- # html_template: path/to/custom/template.erb # Custom 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: "--force" # Additional Brakeman options
623
+ # options: {"--skip-checks": "BasicAuth", "--no-progress": true}
413
624
  # 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."
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(output_path, config_content)
426
- puts "Config file generated: #{output_path}"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omamori
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omamori
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - rira100000000
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-05-04 00:00:00.000000000 Z
11
+ date: 2025-05-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -52,6 +52,34 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: 0.1.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: brakeman
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '7.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '7.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler-audit
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.9.2
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.9.2
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: bundler
57
85
  requirement: !ruby/object:Gem::Requirement