omamori 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +34 -14
- data/README.md +202 -49
- data/README_ja.md +204 -46
- data/demo/ai_analysis_vulnerability.rb +3 -3
- data/demo/csrf_vulnerability.rb +1 -1
- data/demo/eval_vulnerability.rb +1 -1
- data/demo/idor_vulnerability.rb +2 -2
- data/demo/insecure_cookie_vulnerability.rb +1 -1
- data/demo/open_redirect_vulnerability.rb +1 -1
- data/demo/static_analysis_vulnerability.rb +1 -1
- data/demo/xss_vulnerability.rb +1 -1
- data/exe/omamori +2 -2
- data/lib/omamori/ai_analysis_engine/checklist_scanner.rb +143 -0
- data/lib/omamori/ai_analysis_engine/cli_clients/base.rb +43 -0
- data/lib/omamori/ai_analysis_engine/cli_clients/claude_code_client.rb +35 -0
- data/lib/omamori/ai_analysis_engine/cli_clients/codex_client.rb +47 -0
- data/lib/omamori/ai_analysis_engine/diff_splitter.rb +30 -22
- data/lib/omamori/ai_analysis_engine/fallback_client.rb +23 -0
- data/lib/omamori/ai_analysis_engine/judge_client.rb +45 -0
- data/lib/omamori/ai_analysis_engine/llm_client.rb +71 -0
- data/lib/omamori/ai_analysis_engine/prompt_manager.rb +96 -19
- data/lib/omamori/ai_analysis_engine/ruby_llm_context.rb +23 -0
- data/lib/omamori/ai_analysis_engine/sub_checks/audit_log_missing.yml +12 -0
- data/lib/omamori/ai_analysis_engine/sub_checks/csrf.yml +22 -0
- data/lib/omamori/ai_analysis_engine/sub_checks/insecure_deserialization_rce.yml +9 -0
- data/lib/omamori/ai_analysis_engine/sub_checks/insufficient_encryption.yml +19 -0
- data/lib/omamori/ai_analysis_engine/sub_checks/mass_assignment.yml +19 -0
- data/lib/omamori/ai_analysis_engine/sub_checks/open_redirect.yml +17 -0
- data/lib/omamori/ai_analysis_engine/sub_checks/sql_injection.yml +20 -0
- data/lib/omamori/ai_analysis_engine/sub_checks/xss.yml +37 -0
- data/lib/omamori/ai_settings.rb +273 -0
- data/lib/omamori/config.rb +89 -69
- data/lib/omamori/core_runner.rb +485 -286
- data/lib/omamori/doctor.rb +95 -0
- data/lib/omamori/parallel.rb +24 -0
- data/lib/omamori/report_generator/console_formatter.rb +48 -42
- data/lib/omamori/report_generator/html_formatter.rb +6 -6
- data/lib/omamori/report_generator/json_formatter.rb +1 -1
- data/lib/omamori/setup_files.rb +260 -0
- data/lib/omamori/static_analysers/brakeman_runner.rb +7 -7
- data/lib/omamori/static_analysers/bundler_audit_runner.rb +8 -6
- data/lib/omamori/version.rb +2 -2
- data/lib/omamori.rb +11 -11
- metadata +61 -14
- data/lib/omamori/ai_analysis_engine/gemini_client.rb +0 -71
data/README_ja.md
CHANGED
|
@@ -8,78 +8,180 @@ Omamoriは、Rubyコードおよび依存関係に潜む潜在的なセキュリ
|
|
|
8
8
|
人間がチェックした上で万が一に発生した脆弱性を、防いでくれる"かもしれません"。
|
|
9
9
|
|
|
10
10
|
注意:
|
|
11
|
-
AI
|
|
12
|
-
複数回実行することで偽陰性を防ぐことができるかもしれません。
|
|
11
|
+
AI解析は静的解析で診断できない脆弱性を発見することができますが、LLMの診断結果は実行のたびにブレが生じやすいです。
|
|
13
12
|
|
|
14
13
|
## 特徴
|
|
15
14
|
|
|
16
|
-
- ステージされた変更(`git diff --staged
|
|
15
|
+
- ステージされた変更(`git diff --staged`)、ブランチの差分(`--base`)、コードベース全体、または指定されたファイルやディレクトリをスキャン
|
|
16
|
+
- 35種類の脆弱性を1項目ずつyes/noで判定する高速チェック(TypeSafe Jev、OpenAI Decisionsなど)。引っかかった項目だけ、LLMが詳しいレポートを書きます
|
|
17
|
+
- 判定役とレポート役を、それぞれ自由に選べます。レポート役には、RubyLLMが対応するAPI(Gemini、Claude、OpenAIなど)のほか、Claude Code(`claude -p`)やCodex(`codex exec`)のサブスクリプションも使えます
|
|
17
18
|
- BrakemanやBundler-Auditなどの静的解析ツールと連携
|
|
18
|
-
- Gemini APIを活用し、AIによる高度なコード脆弱性検出
|
|
19
19
|
- 複数のレポート形式に対応(コンソール、HTML、JSON)
|
|
20
|
-
-
|
|
20
|
+
- `omamori init` をフラグだけで実行でき、`omamori doctor` で設定を確かめられるので、AIエージェントにセットアップを任せられます
|
|
21
|
+
- GitHub Actions / GitLab CI の設定ファイルを生成
|
|
22
|
+
- 検出精度を測るための問題集(`evals/`)
|
|
23
|
+
- `.omamorirc`ファイルによる設定と、`.omamoriignore`ファイルによるスキャン対象からの除外
|
|
21
24
|
|
|
22
|
-
##
|
|
25
|
+
## 仕組み
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
omamoriのAI解析は、2つの役割で動きます。
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
+
- **判定役(classifier):** 35項目を1つずつyes/noで判定する、高速な判定モデルです。TypeSafe JevやOpenAI Decisionsなどがあります。使うかどうかは選べます。
|
|
30
|
+
- **レポート役(reporter):** 脆弱性のレポートを書くLLMです。判定役を使わない場合は、レポート役が全項目を一度に判定します。
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
判定役を使うと、次の流れになります。
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
1. 差分をファイルごとに分け、各ファイルについて全チェック項目(既定は35項目)を1リクエストでまとめて判定役に問い合わせます。ファイル同士は並列に処理します。
|
|
35
|
+
2. 判定役が返す「脆弱性がある確率」が閾値未満の項目は、その場で通過とします。
|
|
36
|
+
3. 閾値以上の項目だけ、レポート役がその脆弱性の詳細な検出手順に沿ってレポートを書きます。
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
判定役への質問には、レポート役に渡しているのと同じ脆弱性の定義と検出手順を含めています。XSS、SQLインジェクション、Mass Assignment、CSRF、オープンリダイレクト、監査ログの欠如、不十分な暗号化、安全でないデシリアライズは、「この変更に脆弱性があるか」という全体の問いに加えて、それ1つで脆弱と言える具体的な形(小項目。たとえばXSSなら「ユーザー由来のURLを、スキームを確認せずに `href` に入れていないか」)も1つずつ判定し、そのうち最も高い確率を使います。反応した小項目は、レポート役にも伝えます。
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
gem install omamori
|
|
40
|
-
```
|
|
40
|
+
CSP、各種セキュリティヘッダー、本番の設定、2要素認証、パスワードの方針、監査ログなど「何かが無いこと」に関わる項目は、「この変更が既存の対策を弱めたり外したりしたか」を判定します。もともと設定が無いかどうかは、その設定に触れない変更のdiffからは判断できないためです。判定役がエラーを返したファイルは、全項目をレポート役に回します。レポート役がレポートを書けなかった項目も、判定役の検出結果として残します(判定を黙って飛ばしません)。
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## はじめかた
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
1. Gemfileに追加してインストールします。
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
```ruby
|
|
47
|
+
gem 'omamori', group: :development, require: false
|
|
48
|
+
```
|
|
47
49
|
|
|
48
|
-
```bash
|
|
49
|
-
|
|
50
|
-
```
|
|
50
|
+
```bash
|
|
51
|
+
bundle install
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
2. レポート役を決めて、設定ファイルを書き出します。プロバイダとモデルはフラグで指定します(対話式の質問はありません)。
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
bundle exec omamori init --reporter claude_code # Claude Codeのサブスクリプション(claude -p)
|
|
58
|
+
bundle exec omamori init --reporter codex # Codexのサブスクリプション(codex exec)
|
|
59
|
+
bundle exec omamori init --reporter anthropic --reporter-model claude-sonnet-5-5 # RubyLLMが対応するAPI
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
判定役も使う場合は `--classifier typesafe` を、レポートを日本語にする場合は `--language ja` を、CIの設定も作る場合は `--ci github_actions`(または `gitlab_ci`)を足します。`.omamorirc` と `.omamoriignore` ができ、最後に次にやること(設定する環境変数など)が表示されます。既にある `.omamorirc` は、`--force` を付けない限り上書きしません。
|
|
63
|
+
|
|
64
|
+
3. 表示された環境変数を設定し、つながるかを確かめます。
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bundle exec omamori doctor
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
判定役とレポート役に小さなリクエストを1回ずつ送り、足りないもの(APIキー、CLIのインストールやログイン)を直し方つきで表示します。すべて通れば終了コード0、どれかが失敗すれば1を返します。
|
|
51
71
|
|
|
52
|
-
|
|
72
|
+
4. スキャンします。
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
bundle exec omamori scan --base main
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
レポート役には既定値がありません。`.omamorirc` にも `--reporter` にも指定が無いと、`omamori scan` は `init` の使い方を表示して止まります。
|
|
79
|
+
|
|
80
|
+
### APIキー
|
|
81
|
+
|
|
82
|
+
APIキーは環境変数で渡します。名前はRubyLLMの設定名を大文字にしたもの(`ANTHROPIC_API_KEY`、`OPENAI_API_KEY`、`GEMINI_API_KEY`、`OPENROUTER_API_KEY`、判定役のJevなら `TYPESAFE_API_KEY` など)で、`omamori init` が使うものを表示します。`claude_code` / `codex` は、インストール済みのCLIのログイン状態を使うので、APIキーは不要です。
|
|
83
|
+
|
|
84
|
+
コードの差分は、選んだ判定役とレポート役のプロバイダに送られます。社内の規程で外部に送れないコードには、Ollamaで動かすモデルなど、手元で動くものを選んでください。
|
|
85
|
+
|
|
86
|
+
## AIエージェントにセットアップを任せる
|
|
87
|
+
|
|
88
|
+
Claude CodeやCodexなどのAIエージェントに「omamoriを入れて」と頼む場合、エージェントは次の手順で進められます。
|
|
89
|
+
|
|
90
|
+
1. Gemfileに `gem 'omamori', group: :development, require: false` を足し、`bundle install` する
|
|
91
|
+
2. レポート役を決める。`claude` コマンドがあれば `claude_code`、`codex` があれば `codex`。どちらも無ければ、使っているAPIのプロバイダとモデルを開発者に聞く
|
|
92
|
+
3. 判定役(`--classifier typesafe` など)を使うかを開発者に聞く。使うと速く安くなり誤検知も減るが、そのプロバイダのAPIキーが要り、コードがそのプロバイダにも送られる
|
|
93
|
+
4. `bundle exec omamori init --reporter ... [--reporter-model ...] [--classifier ...] [--language ja] [--ci github_actions]` を実行する
|
|
94
|
+
5. 表示された環境変数を、開発者に設定してもらう。APIキーをチャットに貼ってもらったり、ファイルに書いたりしない
|
|
95
|
+
6. `bundle exec omamori doctor` を、終了コードが0になるまで直しては実行する
|
|
96
|
+
7. `bundle exec omamori scan --base main` を実行し、結果を開発者に伝える。CIの設定を作った場合は、登録するシークレットの名前(`init` が表示する)も伝える
|
|
97
|
+
|
|
98
|
+
## 使い方
|
|
53
99
|
|
|
54
100
|
### スキャン
|
|
55
101
|
|
|
102
|
+
`.omamorirc` の設定を、その場だけフラグで変えることもできます(判定役とレポート役を選ぶ を参照)。
|
|
103
|
+
|
|
56
104
|
ステージされた変更をスキャン(デフォルト):
|
|
57
105
|
|
|
58
106
|
```bash
|
|
59
107
|
omamori scan
|
|
60
108
|
```
|
|
61
109
|
|
|
110
|
+
ブランチの差分をスキャン(`main...HEAD`。プルリクエストに含まれる変更。CI向け):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
omamori scan --base main
|
|
114
|
+
```
|
|
115
|
+
|
|
62
116
|
コードベース全体をスキャン:
|
|
63
117
|
|
|
64
118
|
```bash
|
|
65
119
|
omamori scan --all
|
|
66
120
|
```
|
|
121
|
+
このモードでは`.omamoriignore`ファイルが有効になります。
|
|
67
122
|
|
|
68
|
-
|
|
123
|
+
特定のファイルやディレクトリを指定してスキャン:
|
|
69
124
|
|
|
70
125
|
```bash
|
|
71
|
-
|
|
72
|
-
bundle exec omamori scan --all --format json
|
|
126
|
+
omamori scan app/controllers/users_controller.rb app/models/user.rb config/routes.rb lib/
|
|
73
127
|
```
|
|
128
|
+
このモードでも`.omamoriignore`ファイルが有効になります。
|
|
74
129
|
|
|
75
|
-
|
|
130
|
+
出力形式を指定(コンソール、HTML、JSON):
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
omamori scan --format html
|
|
134
|
+
omamori scan --all --format json
|
|
135
|
+
```
|
|
76
136
|
|
|
77
|
-
静的解析ツールを使わず、AI
|
|
137
|
+
静的解析ツールを使わず、AI解析のみを実行:
|
|
78
138
|
|
|
79
139
|
```bash
|
|
80
140
|
omamori scan --ai
|
|
81
141
|
```
|
|
82
142
|
|
|
143
|
+
### 判定役とレポート役を選ぶ
|
|
144
|
+
|
|
145
|
+
どちらの役も `provider` と `model` で指定します。コマンドラインでも、`.omamorirc`([設定](#設定))でも指定できます。
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
omamori scan --base main --classifier typesafe --reporter gemini --reporter-model gemini-3.8-flash # Jevで判定、Geminiでレポート
|
|
149
|
+
omamori scan --base main --classifier typesafe --reporter claude_code # Jevで判定、claude -pでレポート
|
|
150
|
+
omamori scan --base main --classifier openai --reporter anthropic --reporter-model claude-sonnet-5-5
|
|
151
|
+
omamori scan --reporter codex # 判定役なし。codex execで全項目を判定
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**判定役は要るか?** 使わなくても構いません。レポート役を `claude -p`(sonnet)にして、Railsアプリの実際の脆弱性修正と、OSSのRailsアプリの普通のコミットで測ったところ、レポート役だけでも判定役ありと同じくらい脆弱性を拾いました。判定役を使うと、レポート役を呼ぶのが引っかかった項目だけになるので速く安くなり、危なそうに見えて安全な変更での誤検知も減りました。安いレポート役を単独で使った場合は測っていません。
|
|
155
|
+
|
|
156
|
+
**判定役(classifier)に指定できるprovider**
|
|
157
|
+
|
|
158
|
+
| provider | 判定モデル | APIキー |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| `typesafe` | TypeSafe Jev(`model` 省略時は `jev-1.13.0`) | `TYPESAFE_API_KEY` |
|
|
161
|
+
| `openai` | OpenAI Decisions API(`model` 省略時は `gpt-6-luna`) | `OPENAI_API_KEY` |
|
|
162
|
+
| `ollama` | Ollamaで動かす判定モデル(Clefなど)。`model` と、接続先の `base_url`(または `OLLAMA_API_BASE`)が必須 | なし |
|
|
163
|
+
| `none` | 判定役を使わない(既定) | なし |
|
|
164
|
+
|
|
165
|
+
`base_url` を指定すると、SystemOneプロトコルに対応した他のサーバーにも向けられます。
|
|
166
|
+
|
|
167
|
+
**レポート役(reporter)に指定できるprovider**
|
|
168
|
+
|
|
169
|
+
| provider | 呼び出すもの |
|
|
170
|
+
| --- | --- |
|
|
171
|
+
| RubyLLMのプロバイダ(`gemini`、`anthropic`、`openai`、`openrouter`、`ollama` など) | [RubyLLM](https://rubyllm.com) 経由のAPI。`model` が必須 |
|
|
172
|
+
| `claude_code` | `claude -p`(サブスクリプション)。ツールをすべて無効にし、空の一時ディレクトリで実行 |
|
|
173
|
+
| `codex` | `codex exec`(サブスクリプション)。読み取り専用のサンドボックスで、空の一時ディレクトリで実行 |
|
|
174
|
+
|
|
175
|
+
`.omamorirc` では、レポート役の `provider` を省略することもでき、その場合はモデル名からプロバイダを決めます。RubyLLMのモデル一覧にない新しいモデルを使うときは、`provider` を指定してください。
|
|
176
|
+
|
|
177
|
+
レポート役は、内容によって判定を拒否することがあります(Geminiでは、セキュリティの分析を拒否することがあります)。拒否された項目は黙って通過させず、判定役の検出結果として残します。予備のレポート役(`reporter.fallback`)を設定すると、拒否やエラーのときだけ、そちらで聞き直します。
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
omamori scan --base main --classifier typesafe --reporter-fallback claude_code --reporter-fallback-model sonnet
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
diffには外部から来たコードが含まれるため、`claude_code` / `codex` はコマンドを実行できない状態で動かします。サブスクリプションのレート制限を考慮して、CLIを使うときのレポートの並列数は既定で3です(`reporter.concurrency` で変更できます)。
|
|
184
|
+
|
|
83
185
|
## 設定
|
|
84
186
|
|
|
85
187
|
プロジェクトルートにある`.omamorirc`ファイルで、Omamoriの動作をカスタマイズできます。
|
|
@@ -90,19 +192,31 @@ omamori scan --ai
|
|
|
90
192
|
# .omamorirc
|
|
91
193
|
# omamori用設定ファイル
|
|
92
194
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
#
|
|
195
|
+
# 判定役(任意)。省略するとレポート役が全項目を判定します。--classifier で上書きできます
|
|
196
|
+
# classifier:
|
|
197
|
+
# provider: typesafe # typesafe / openai / ollama
|
|
198
|
+
# model: jev-1.13.0 # 閾値を調整するなら、jev-latest ではなく版を固定する
|
|
199
|
+
# threshold: 0.5 # この確率以上の項目をレポート役に回す
|
|
200
|
+
# concurrency: 8 # 判定役に同時に投げるリクエスト数
|
|
201
|
+
# base_url: https://api.typesafe.ai # 任意。SystemOne形式の別のサーバーを使うとき
|
|
202
|
+
# api_key: ... # 任意。環境変数(TYPESAFE_API_KEY など)を推奨
|
|
203
|
+
|
|
204
|
+
# レポート役(必須。omamori init が書きます)。--reporter で上書きできます
|
|
205
|
+
reporter:
|
|
206
|
+
provider: anthropic # RubyLLMのプロバイダ、または claude_code / codex
|
|
207
|
+
model: claude-sonnet-5-5 # APIのプロバイダでは必須。CLIでは省略するとCLIの既定のモデル
|
|
208
|
+
# concurrency: 8 # レポートを並列で書く数(CLIの既定は3、APIの既定は8)
|
|
209
|
+
# command: /path/to/claude # 任意。claude_code / codex のCLIのパス
|
|
210
|
+
# api_key: ... # 任意。環境変数(ANTHROPIC_API_KEY など)を推奨
|
|
211
|
+
# fallback: # 任意。レポート役が拒否・失敗したときだけ聞く予備のレポート役
|
|
212
|
+
# provider: claude_code
|
|
213
|
+
# model: sonnet
|
|
214
|
+
|
|
215
|
+
# 実行するセキュリティチェック(任意。デフォルトは全35項目)
|
|
101
216
|
# checks:
|
|
102
|
-
# xss
|
|
103
|
-
# csrf
|
|
104
|
-
# idor
|
|
105
|
-
# ... # 他のチェック項目も追加可能
|
|
217
|
+
# - xss
|
|
218
|
+
# - csrf
|
|
219
|
+
# - idor
|
|
106
220
|
|
|
107
221
|
# カスタムプロンプトテンプレート(任意)
|
|
108
222
|
# prompt_templates:
|
|
@@ -125,9 +239,9 @@ model: gemini-2.5-flash-preview-04-17
|
|
|
125
239
|
# language: ja
|
|
126
240
|
```
|
|
127
241
|
|
|
128
|
-
- `
|
|
129
|
-
- `
|
|
130
|
-
- `checks`:
|
|
242
|
+
- `classifier`: 判定役。`provider`、`model`、`threshold`(デフォルト0.5)、`concurrency`(デフォルト8)、`base_url`、`api_key`。
|
|
243
|
+
- `reporter`: レポート役。`provider`、`model`、`concurrency`、`command`、`api_key`、`fallback`(予備のレポート役。`provider`、`model` など)。
|
|
244
|
+
- `checks`: 実行するセキュリティチェックのリスト。以前の項目名 `server_error_information_exposure` は `overly_detailed_errors` に統合されました。`dependency_trojan_package`(悪意のある依存パッケージ)は、コードからは正確に判断できないためAIの判定から外しました。既知の脆弱なバージョンはomamoriが一緒に動かすbundler-auditで、悪意のあるパッケージはOSV-ScannerやGitHubのDependency Reviewなど、悪意のあるパッケージのデータベースを持つツールで確かめてください。
|
|
131
245
|
- `prompt_templates`: AI解析用のカスタムプロンプトテンプレートを設定。
|
|
132
246
|
- `report`: レポート出力に関する設定。
|
|
133
247
|
- `output_path`: HTMLおよびJSONレポートの出力先ディレクトリまたはプレフィックス。
|
|
@@ -135,6 +249,33 @@ model: gemini-2.5-flash-preview-04-17
|
|
|
135
249
|
- `static_analysers`: 静的解析ツール(Brakeman、Bundler-Auditなど)の追加オプション設定。
|
|
136
250
|
- `language`: AI解析結果の詳細説明文の言語設定。デフォルトは英語(`en`)。
|
|
137
251
|
|
|
252
|
+
以前の書き方(`engine`、`llm`、`model`、`provider`、`api_key`、`decision`、`claude`、`codex`)と、以前のオプション(`--engine`、`--llm`)も、引き続き使えます。0.1系の設定ファイルで `api_key` だけを書いていた場合は、これまでどおりGemini(`gemini-3.8-flash`)を使います。
|
|
253
|
+
|
|
254
|
+
## .omamoriignore ファイル
|
|
255
|
+
|
|
256
|
+
`.omamoriignore`ファイルを使用すると、特定のファイルやディレクトリをスキャン対象から除外できます。ただし、`--force-scan-ignored`オプションを指定した場合は、このファイルの設定は無視されます。これは`.gitignore`ファイルと似たような働きをしますが、以下の点に注意してください。
|
|
257
|
+
|
|
258
|
+
* **有効なモード:** `--all` モード、またはファイル/ディレクトリを指定してスキャンする場合にのみ有効です。
|
|
259
|
+
* **無効なモード:** `diff` モード(引数なしの `omamori scan`、または `--base`)では、`.omamoriignore` ファイルは無視されます。これは、`git diff` で取得された変更点のみを対象とするためです。
|
|
260
|
+
* **書式:** 1行に1つのパターンを記述します。`#` で始まる行はコメントとして扱われます。
|
|
261
|
+
* 末尾が `/` のパターンは、そのディレクトリ以下をすべて除外します(例: `vendor/bundle/`)。
|
|
262
|
+
* それ以外のパターンは、プロジェクトルートからの相対パスかファイル名に対するglobとして評価されます(例: `*.log`、`spec/fixtures/*.rb`)。
|
|
263
|
+
|
|
264
|
+
`omamori init` コマンドを実行すると、一般的なRailsプロジェクトで無視すべきファイルやディレクトリのパターンが記述された`.omamoriignore`ファイルが生成されます。必要に応じてこのファイルを編集してください。
|
|
265
|
+
|
|
266
|
+
## CIで使う
|
|
267
|
+
|
|
268
|
+
`omamori init` に `--ci` を付けるか、あとから `.omamorirc` をもとに生成します:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
omamori ci-setup --ci github_actions # .github/workflows/omamori_scan.yml
|
|
272
|
+
omamori ci-setup --ci gitlab_ci # .gitlab-ci.yml
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
生成される設定は、プルリクエスト(マージリクエスト)では `--base` でブランチの差分を、それ以外では `--all` でコードベース全体をスキャンします。差分を取るため、リポジトリは履歴ごと取得します。`.omamorirc` の判定役とレポート役に合わせて、必要な環境変数を設定に書き、登録するシークレット(GitLabではCI/CD変数)の名前を表示します。
|
|
276
|
+
|
|
277
|
+
`claude_code` / `codex` は、そのマシンでのCLIのログインを使うものです。生成される設定は、CIのランナーにCLIのインストールやログインはしません。CIでは、スキャンのコマンドに `--reporter PROVIDER --reporter-model MODEL` を足してAPIのレポート役を使うか、ランナーの準備を自分で用意してください。
|
|
278
|
+
|
|
138
279
|
## デモファイル
|
|
139
280
|
|
|
140
281
|
`demo`ディレクトリには、Omamoriの機能をデモンストレーションするための既知の脆弱性を含むサンプルファイルが置かれています。
|
|
@@ -153,18 +294,35 @@ model: gemini-2.5-flash-preview-04-17
|
|
|
153
294
|
git add demo_
|
|
154
295
|
```
|
|
155
296
|
|
|
156
|
-
3. ステージしたデモファイルに対してOmamori
|
|
297
|
+
3. ステージしたデモファイルに対してOmamoriを実行します(先に `omamori init --reporter ...` で設定しておきます):
|
|
157
298
|
|
|
158
299
|
```bash
|
|
159
300
|
omamori scan
|
|
160
301
|
```
|
|
161
302
|
|
|
303
|
+
判定役(Jev)で試す場合:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
omamori scan --classifier typesafe
|
|
307
|
+
```
|
|
308
|
+
|
|
162
309
|
または、ステージ後にすべてのファイルをスキャンする場合:
|
|
163
310
|
|
|
164
311
|
```bash
|
|
165
312
|
omamori scan --all
|
|
166
313
|
```
|
|
167
314
|
|
|
315
|
+
## 検出精度の測定
|
|
316
|
+
|
|
317
|
+
`evals/` には、正解のわかっているコード変更の問題集があります。35項目それぞれについて、脆弱な変更と「惜しいけど安全な」変更を2つずつ、さらに無害な変更を収録しています。XSS、SQLインジェクション、Mass Assignment、IDOR、CSRF、オープンリダイレクトには、見逃しやすい具体的な形のケースも追加しています。判定役やレポート役を変えたときに、見逃しと誤検知がどう変わるかを測れます。
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
bundle exec ruby evals/run.rb --classifier typesafe --runs 3
|
|
321
|
+
bundle exec ruby evals/run.rb --classifier none --reporter claude_code --reporter-model sonnet
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
使い方は [evals/README.md](evals/README.md) を参照してください。
|
|
325
|
+
|
|
168
326
|
## 貢献について
|
|
169
327
|
|
|
170
328
|
バグ報告やプルリクエストはGitHubで歓迎しています。このプロジェクトは、安全かつ協力的なコラボレーションの場を目指しています。
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
require 'net/http'
|
|
4
4
|
require 'uri'
|
|
5
5
|
|
|
6
|
-
API_KEY =
|
|
6
|
+
API_KEY = 'HARDCODED_SECRET_API_KEY_12345' # Hardcoded sensitive information
|
|
7
7
|
|
|
8
8
|
def fetch_data(user_input)
|
|
9
9
|
# Insufficient input validation
|
|
10
10
|
if user_input.length > 100
|
|
11
|
-
puts
|
|
11
|
+
puts 'Input too long, truncating.'
|
|
12
12
|
user_input = user_input[0..99]
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -25,4 +25,4 @@ end
|
|
|
25
25
|
|
|
26
26
|
# Example usage
|
|
27
27
|
# fetch_data("some data")
|
|
28
|
-
# fetch_data("a" * 200) # Long input
|
|
28
|
+
# fetch_data("a" * 200) # Long input
|
data/demo/csrf_vulnerability.rb
CHANGED
data/demo/eval_vulnerability.rb
CHANGED
|
@@ -26,4 +26,4 @@ end
|
|
|
26
26
|
# 2. ターミナルで `ruby demo/eval_vulnerability.rb` を実行
|
|
27
27
|
# 3. ブラウザで `http://localhost:4567/calculate?expression=2%2B2` にアクセスすると "Result: 4" が表示される。
|
|
28
28
|
# 4. 悪意のある入力を与える例: `http://localhost:4567/calculate?expression=system('ls%20-l')`
|
|
29
|
-
# サーバー側で `ls -l` コマンドが実行されてしまう可能性がある。
|
|
29
|
+
# サーバー側で `ls -l` コマンドが実行されてしまう可能性がある。
|
data/demo/idor_vulnerability.rb
CHANGED
|
@@ -16,7 +16,7 @@ get '/user_info/:user_id' do
|
|
|
16
16
|
"User Info for ID #{user_id}: #{user_data}"
|
|
17
17
|
else
|
|
18
18
|
status 404
|
|
19
|
-
|
|
19
|
+
'User not found.'
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -36,4 +36,4 @@ end
|
|
|
36
36
|
# 2. ターミナルで `ruby demo/idor_vulnerability.rb` を実行
|
|
37
37
|
# 3. ブラウザで `http://localhost:4567/user_info/101` にアクセスすると Alice の情報が表示される。
|
|
38
38
|
# 4. ログインしていない、または権限のないユーザーが `http://localhost:4567/user_info/102` にアクセスすると、
|
|
39
|
-
# 本来アクセス権限がないはずの Bob の情報が表示されてしまう可能性がある。
|
|
39
|
+
# 本来アクセス権限がないはずの Bob の情報が表示されてしまう可能性がある。
|
|
@@ -22,4 +22,4 @@ end
|
|
|
22
22
|
# 2. ターミナルで `ruby demo/insecure_cookie_vulnerability.rb` を実行
|
|
23
23
|
# 3. ブラウザで `http://localhost:4567/login?user_id=test_user` にアクセス
|
|
24
24
|
# 開発者ツールなどでクッキーを確認すると、session_id クッキーに HttpOnly, Secure, SameSite 属性が付与されていないことがわかる。
|
|
25
|
-
# HTTPSでアクセスした場合でもSecure属性が付かない。
|
|
25
|
+
# HTTPSでアクセスした場合でもSecure属性が付かない。
|
data/demo/xss_vulnerability.rb
CHANGED
data/exe/omamori
CHANGED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../parallel'
|
|
4
|
+
|
|
5
|
+
module Omamori
|
|
6
|
+
module AIAnalysisEngine
|
|
7
|
+
# Runs each risk as an independent yes/no check with a fast decision model,
|
|
8
|
+
# then asks an LLM to write a report only for the checks that did not pass.
|
|
9
|
+
class ChecklistScanner
|
|
10
|
+
DEFAULT_THRESHOLD = 0.5
|
|
11
|
+
DEFAULT_CONCURRENCY = 8
|
|
12
|
+
|
|
13
|
+
Unit = Struct.new(:label, :content)
|
|
14
|
+
|
|
15
|
+
# Other risks get their own report, so each report sticks to its one risk to avoid duplicates
|
|
16
|
+
SINGLE_RISK_INSTRUCTION = "\nReport only the vulnerability type described above. " \
|
|
17
|
+
"If the code does not have it, return an empty list.\n"
|
|
18
|
+
|
|
19
|
+
def initialize(decision_client:, llm_client:, prompt_manager:, diff_splitter:, json_schema:, model:,
|
|
20
|
+
threshold: DEFAULT_THRESHOLD, concurrency: DEFAULT_CONCURRENCY, report_concurrency: concurrency,
|
|
21
|
+
sub_checks: true)
|
|
22
|
+
@decision_client = decision_client
|
|
23
|
+
@llm_client = llm_client
|
|
24
|
+
@prompt_manager = prompt_manager
|
|
25
|
+
@diff_splitter = diff_splitter
|
|
26
|
+
@json_schema = json_schema
|
|
27
|
+
@model = model
|
|
28
|
+
@threshold = threshold
|
|
29
|
+
@concurrency = concurrency
|
|
30
|
+
@report_concurrency = report_concurrency
|
|
31
|
+
@sub_checks = sub_checks
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# units: [[label, content], ...] (a file path and its diff or source)
|
|
35
|
+
def scan(units, risks_to_check)
|
|
36
|
+
puts "Checking #{units.size} file(s) x #{risks_to_check.size} checks..."
|
|
37
|
+
classified = classify_units(units, risks_to_check)
|
|
38
|
+
total = classified.sum { |_chunk, probabilities| probabilities.size }
|
|
39
|
+
|
|
40
|
+
flagged = classified.flat_map do |chunk, probabilities, triggers|
|
|
41
|
+
probabilities.filter_map do |risk, probability|
|
|
42
|
+
[chunk, risk, probability, triggers[risk]] if flagged?(probability)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
puts "#{flagged.size} check(s) flagged, #{total - flagged.size} passed."
|
|
46
|
+
|
|
47
|
+
risks = Parallel.map(flagged, @report_concurrency) do |chunk, risk, probability, risk_triggers|
|
|
48
|
+
report(chunk, risk, probability, risk_triggers)
|
|
49
|
+
end
|
|
50
|
+
{ 'security_risks' => risks.flatten }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Each risk is asked as an overall question plus its sub-checks; the risk's probability is the highest of them.
|
|
54
|
+
# Returns [[chunk, { risk => probability or nil }, { risk => [sub-check names at or above the threshold] },
|
|
55
|
+
# { question name => probability }], ...]
|
|
56
|
+
def classify_units(units, risks_to_check)
|
|
57
|
+
chunks = expand(units)
|
|
58
|
+
questions = risks_to_check.map { |risk| @prompt_manager.build_questions(risk, sub_checks: @sub_checks) }
|
|
59
|
+
.reduce({}, :merge)
|
|
60
|
+
parallel_map(chunks) do |chunk|
|
|
61
|
+
answers = classify(chunk, questions)
|
|
62
|
+
[chunk, aggregate(answers, risks_to_check), triggers(answers), answers]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Asks the reporter to write a report for one flagged risk. Returns the reported risks (empty when the reporter
|
|
67
|
+
# finds nothing; a review note when the reporter could not answer).
|
|
68
|
+
def report(chunk, risk, probability, risk_triggers = [])
|
|
69
|
+
puts "Reporting #{risk} in #{chunk.label} (p=#{probability ? format('%.2f', probability) : 'n/a'})..."
|
|
70
|
+
prompt = @prompt_manager.build_prompt(chunk.content, [risk], @json_schema, file_path: chunk.label) +
|
|
71
|
+
SINGLE_RISK_INSTRUCTION + trigger_hint(risk_triggers)
|
|
72
|
+
result = @llm_client.analyze(prompt, @json_schema, model: @model)
|
|
73
|
+
result ? result.fetch('security_risks', []) : [unreported_flag(chunk, risk, probability)]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def flagged?(probability)
|
|
77
|
+
probability.nil? || probability >= @threshold
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def expand(units)
|
|
83
|
+
units.flat_map do |label, content|
|
|
84
|
+
parts = @diff_splitter.split(content)
|
|
85
|
+
parts.each_with_index.map do |part, index|
|
|
86
|
+
Unit.new(parts.size > 1 ? "#{label} (chunk #{index + 1}/#{parts.size})" : label, part)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def classify(chunk, questions)
|
|
92
|
+
@decision_client.classify(chunk.content, questions)
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
# If the classifier fails, every check is escalated so nothing is silently skipped
|
|
95
|
+
puts "Decision API error for #{chunk.label}: #{e.message}"
|
|
96
|
+
questions.keys.to_h { |risk| [risk, nil] }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# nil (no answer) wins, so a risk with any unanswered question is escalated
|
|
100
|
+
def aggregate(answers, risks)
|
|
101
|
+
by_risk = answers.group_by { |name, _| PromptManager.risk_of(name) }
|
|
102
|
+
risks.to_h do |risk|
|
|
103
|
+
probabilities = by_risk.fetch(risk, []).map(&:last)
|
|
104
|
+
[risk, probabilities.empty? || probabilities.include?(nil) ? nil : probabilities.max]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def triggers(answers)
|
|
109
|
+
answers.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |(name, probability), result|
|
|
110
|
+
next unless name.to_s.include?(PromptManager::SUB_CHECK_SEPARATOR) && probability && probability >= @threshold
|
|
111
|
+
|
|
112
|
+
result[PromptManager.risk_of(name)] << name
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# Tells the reporter which concrete patterns the classifier matched, so it checks those first
|
|
118
|
+
def trigger_hint(risk_triggers)
|
|
119
|
+
texts = Array(risk_triggers).filter_map { |name| @prompt_manager.sub_check_text(name) }
|
|
120
|
+
return '' if texts.empty?
|
|
121
|
+
|
|
122
|
+
"A classifier flagged these patterns in the code. Check each of them carefully:\n" +
|
|
123
|
+
texts.map { |text| "- #{text}\n" }.join
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Keeps the classifier's flag when the LLM could not write a report, so the finding is not lost
|
|
127
|
+
def unreported_flag(chunk, risk, probability)
|
|
128
|
+
{
|
|
129
|
+
'type' => risk.to_s,
|
|
130
|
+
'location' => chunk.label,
|
|
131
|
+
'details' => "Flagged by the decision model (p=#{probability ? format('%.2f', probability) : 'n/a'}), " \
|
|
132
|
+
'but the LLM could not write a report. Review this change manually.',
|
|
133
|
+
'severity' => 'Info',
|
|
134
|
+
'unreported' => true
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def parallel_map(items, &)
|
|
139
|
+
Parallel.map(items, @concurrency, &)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
|
|
7
|
+
module Omamori
|
|
8
|
+
module AIAnalysisEngine
|
|
9
|
+
module CLIClients
|
|
10
|
+
# Runs an agent CLI as a plain LLM so it can use the user's subscription.
|
|
11
|
+
# The prompt contains untrusted code, so every CLI runs in an empty directory with tools disabled
|
|
12
|
+
# or a read-only sandbox.
|
|
13
|
+
class Base
|
|
14
|
+
def initialize(command: nil)
|
|
15
|
+
@command = command || default_command
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Same interface as LLMClient#analyze. Returns the parsed JSON or nil.
|
|
19
|
+
def analyze(prompt, json_schema, model: nil)
|
|
20
|
+
Dir.mktmpdir('omamori-llm') do |dir|
|
|
21
|
+
output, error, status = Open3.capture3(*build_command(dir, json_schema, model), stdin_data: prompt, chdir: dir)
|
|
22
|
+
unless status.success?
|
|
23
|
+
puts "Warning: #{@command} exited with #{status.exitstatus}: #{error.strip[0, 500]}"
|
|
24
|
+
return nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
result = parse(output, dir)
|
|
28
|
+
return result if result.is_a?(Hash) && result.key?('security_risks')
|
|
29
|
+
|
|
30
|
+
puts "Warning: Unexpected response from #{@command}."
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
rescue Errno::ENOENT
|
|
34
|
+
puts "Warning: '#{@command}' command not found. Install it or choose another --llm."
|
|
35
|
+
nil
|
|
36
|
+
rescue JSON::ParserError => e
|
|
37
|
+
puts "Warning: Failed to parse the response from #{@command}: #{e.message}"
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|