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 +4 -4
- data/Gemfile.lock +16 -8
- 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 +2 -2
- data/lib/omamori/ai_analysis_engine/prompt_manager.rb +25 -8
- data/lib/omamori/config.rb +21 -0
- data/lib/omamori/core_runner.rb +383 -157
- data/lib/omamori/version.rb +1 -1
- metadata +30 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bafa258377208d15c678d88752804be6963159d28b83c13958de28bdd8bf8fa7
|
4
|
+
data.tar.gz: 9739ae26075e9e5c0f2f87eafd68f2ed6cce0baee719a4c31c1042e8b1c8abf9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
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.
|
25
|
-
language_server-protocol (3.17.0.
|
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.
|
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.
|
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.
|
53
|
-
rubocop (1.75.
|
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`)
|
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`
|
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-
|
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-
|
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-
|
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-
|
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
|
-
#
|
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
|
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
|
-
|
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
|
-
|
40
|
-
|
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-
|
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 =
|
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
|
-
|
102
|
-
|
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
|
-
|
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
|
data/lib/omamori/config.rb
CHANGED
@@ -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
|
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
|
data/lib/omamori/version.rb
CHANGED
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.
|
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-
|
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
|