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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +34 -14
  5. data/README.md +202 -49
  6. data/README_ja.md +204 -46
  7. data/demo/ai_analysis_vulnerability.rb +3 -3
  8. data/demo/csrf_vulnerability.rb +1 -1
  9. data/demo/eval_vulnerability.rb +1 -1
  10. data/demo/idor_vulnerability.rb +2 -2
  11. data/demo/insecure_cookie_vulnerability.rb +1 -1
  12. data/demo/open_redirect_vulnerability.rb +1 -1
  13. data/demo/static_analysis_vulnerability.rb +1 -1
  14. data/demo/xss_vulnerability.rb +1 -1
  15. data/exe/omamori +2 -2
  16. data/lib/omamori/ai_analysis_engine/checklist_scanner.rb +143 -0
  17. data/lib/omamori/ai_analysis_engine/cli_clients/base.rb +43 -0
  18. data/lib/omamori/ai_analysis_engine/cli_clients/claude_code_client.rb +35 -0
  19. data/lib/omamori/ai_analysis_engine/cli_clients/codex_client.rb +47 -0
  20. data/lib/omamori/ai_analysis_engine/diff_splitter.rb +30 -22
  21. data/lib/omamori/ai_analysis_engine/fallback_client.rb +23 -0
  22. data/lib/omamori/ai_analysis_engine/judge_client.rb +45 -0
  23. data/lib/omamori/ai_analysis_engine/llm_client.rb +71 -0
  24. data/lib/omamori/ai_analysis_engine/prompt_manager.rb +96 -19
  25. data/lib/omamori/ai_analysis_engine/ruby_llm_context.rb +23 -0
  26. data/lib/omamori/ai_analysis_engine/sub_checks/audit_log_missing.yml +12 -0
  27. data/lib/omamori/ai_analysis_engine/sub_checks/csrf.yml +22 -0
  28. data/lib/omamori/ai_analysis_engine/sub_checks/insecure_deserialization_rce.yml +9 -0
  29. data/lib/omamori/ai_analysis_engine/sub_checks/insufficient_encryption.yml +19 -0
  30. data/lib/omamori/ai_analysis_engine/sub_checks/mass_assignment.yml +19 -0
  31. data/lib/omamori/ai_analysis_engine/sub_checks/open_redirect.yml +17 -0
  32. data/lib/omamori/ai_analysis_engine/sub_checks/sql_injection.yml +20 -0
  33. data/lib/omamori/ai_analysis_engine/sub_checks/xss.yml +37 -0
  34. data/lib/omamori/ai_settings.rb +273 -0
  35. data/lib/omamori/config.rb +89 -69
  36. data/lib/omamori/core_runner.rb +485 -286
  37. data/lib/omamori/doctor.rb +95 -0
  38. data/lib/omamori/parallel.rb +24 -0
  39. data/lib/omamori/report_generator/console_formatter.rb +48 -42
  40. data/lib/omamori/report_generator/html_formatter.rb +6 -6
  41. data/lib/omamori/report_generator/json_formatter.rb +1 -1
  42. data/lib/omamori/setup_files.rb +260 -0
  43. data/lib/omamori/static_analysers/brakeman_runner.rb +7 -7
  44. data/lib/omamori/static_analysers/bundler_audit_runner.rb +8 -6
  45. data/lib/omamori/version.rb +2 -2
  46. data/lib/omamori.rb +11 -11
  47. metadata +61 -14
  48. data/lib/omamori/ai_analysis_engine/gemini_client.rb +0 -71
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ai_settings'
4
+
5
+ module Omamori
6
+ # `omamori doctor`: checks that the configured classifier and reporters can be reached, by sending each
7
+ # one small request, and says how to fix what is missing.
8
+ class Doctor
9
+ REPORTER_PROBE = 'This is a connectivity check by `omamori doctor`. There is no code to review. ' \
10
+ 'Return {"security_risks": []}.'
11
+ CLASSIFIER_PROBE_STATE = "puts 'hello'"
12
+ CLASSIFIER_PROBE_QUESTIONS = { ruby: 'Is this Ruby code?' }.freeze
13
+ CLI_INSTALL = {
14
+ 'claude_code' => 'Install Claude Code (npm install -g @anthropic-ai/claude-code) and log in with `claude`',
15
+ 'codex' => 'Install the Codex CLI (npm install -g @openai/codex) and log in with `codex login`'
16
+ }.freeze
17
+
18
+ def initialize(settings, json_schema:)
19
+ @settings = settings
20
+ @json_schema = json_schema
21
+ end
22
+
23
+ # Returns true when every role answered
24
+ def run
25
+ puts "Settings: #{@settings.describe}"
26
+ results = []
27
+ results << check('classifier', @settings.classifier) { check_classifier } if @settings.classifier?
28
+ results << check('reporter', @settings.reporter) { check_reporter(@settings.reporter) }
29
+ if @settings.reporter_fallback
30
+ results << check('reporter.fallback', @settings.reporter_fallback) { check_reporter(@settings.reporter_fallback) }
31
+ end
32
+ ok = results.all?
33
+ puts ok ? 'All checks passed. Try `omamori scan --base main`.' : 'Some checks failed. Fix them and run `omamori doctor` again.'
34
+ ok
35
+ end
36
+
37
+ private
38
+
39
+ def check(role, role_settings)
40
+ label = "#{role} (#{[role_settings['provider'], role_settings['model']].compact.join('/')})"
41
+ puts "Checking #{label}..."
42
+ problem = yield
43
+ if problem
44
+ puts " NG: #{problem}"
45
+ false
46
+ else
47
+ puts ' OK'
48
+ true
49
+ end
50
+ rescue StandardError => e
51
+ puts " NG: #{e.class}: #{e.message}"
52
+ hint = credential_hint(role_settings)
53
+ puts " #{hint}" if hint
54
+ false
55
+ end
56
+
57
+ # Returns nil when it works, or what is wrong
58
+ def check_classifier
59
+ answers = @settings.build_classifier_client.classify(CLASSIFIER_PROBE_STATE, CLASSIFIER_PROBE_QUESTIONS)
60
+ return nil unless answers.values.include?(nil)
61
+
62
+ "The classifier gave no answer. #{credential_hint(@settings.classifier)}".strip
63
+ end
64
+
65
+ def check_reporter(role_settings)
66
+ provider = role_settings['provider']
67
+ if AISettings::CLI_REPORTERS.include?(provider)
68
+ command = role_settings['command'] || (provider == 'claude_code' ? 'claude' : 'codex')
69
+ return "'#{command}' was not found. #{CLI_INSTALL[provider]}" unless command_exists?(command)
70
+ end
71
+
72
+ result = @settings.build_llm_client(role_settings).analyze(REPORTER_PROBE, @json_schema, model: role_settings['model'])
73
+ return nil if result
74
+
75
+ "The reporter gave no usable answer (see the warning above). #{credential_hint(role_settings)}".strip
76
+ end
77
+
78
+ def credential_hint(role_settings)
79
+ provider = role_settings['provider']
80
+ return "#{CLI_INSTALL[provider]}." if CLI_INSTALL.key?(provider)
81
+
82
+ env = AISettings.api_key_env(provider)
83
+ return nil unless env
84
+ return nil if role_settings['api_key'] || !ENV[env].to_s.empty?
85
+
86
+ "#{env} is not set."
87
+ end
88
+
89
+ def command_exists?(command)
90
+ return File.executable?(command) if command.include?(File::SEPARATOR)
91
+
92
+ ENV['PATH'].to_s.split(File::PATH_SEPARATOR).any? { |dir| File.executable?(File.join(dir, command)) }
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Omamori
4
+ # Minimal thread pool: maps items with at most `concurrency` threads, keeping order.
5
+ module Parallel
6
+ def self.map(items, concurrency)
7
+ queue = Queue.new
8
+ items.each_with_index { |item, index| queue << [item, index] }
9
+ results = Array.new(items.size)
10
+ workers = Array.new([concurrency, items.size].min) do
11
+ Thread.new do
12
+ loop do
13
+ item, index = queue.pop(true)
14
+ results[index] = yield(item)
15
+ end
16
+ rescue ThreadError
17
+ # queue is empty
18
+ end
19
+ end
20
+ workers.each(&:join)
21
+ results
22
+ end
23
+ end
24
+ end
@@ -6,30 +6,30 @@ module Omamori
6
6
  module ReportGenerator
7
7
  class ConsoleFormatter
8
8
  SEVERITY_COLORS = {
9
- "Critical" => :red,
10
- "High" => :red,
11
- "Medium" => :yellow,
12
- "Low" => :blue,
13
- "Info" => :green,
9
+ 'Critical' => :red,
10
+ 'High' => :red,
11
+ 'Medium' => :yellow,
12
+ 'Low' => :blue,
13
+ 'Info' => :green
14
14
  }.freeze
15
15
 
16
16
  def format(combined_results)
17
- output = ""
17
+ output = ''
18
18
 
19
19
  # Format AI Analysis Results
20
- ai_risks = combined_results && combined_results["ai_security_risks"] ? combined_results["ai_security_risks"] : []
20
+ ai_risks = combined_results && combined_results['ai_security_risks'] ? combined_results['ai_security_risks'] : []
21
21
  if !ai_risks.empty?
22
22
  output += "--- AI Analysis Results ---\n".colorize(:bold)
23
23
  ai_risks.each do |risk|
24
- severity_color = SEVERITY_COLORS[risk["severity"]] || :white
24
+ severity_color = SEVERITY_COLORS[risk['severity']] || :white
25
25
  # Use "Unknown Type" if risk["type"] is nil
26
- risk_type = risk["type"] || "Unknown Type"
26
+ risk_type = risk['type'] || 'Unknown Type'
27
27
  output += " - Type: #{risk_type.colorize(severity_color)}\n"
28
- output += " Severity: #{risk["severity"].colorize(severity_color)}\n"
29
- output += " Location: #{risk["location"]}\n"
30
- output += " Details: #{risk["details"]}\n"
28
+ output += " Severity: #{risk['severity'].colorize(severity_color)}\n"
29
+ output += " Location: #{risk['location']}\n"
30
+ output += " Details: #{risk['details']}\n"
31
31
  output += " Code Snippet:\n"
32
- output += format_code_snippet(risk["code_snippet"])
32
+ output += format_code_snippet(risk['code_snippet'])
33
33
  output += "\n"
34
34
  end
35
35
  else
@@ -39,22 +39,22 @@ module Omamori
39
39
  output += "\n"
40
40
 
41
41
  # Format Static Analysis Results
42
- static_results = combined_results && combined_results["static_analysis_results"] ? combined_results["static_analysis_results"] : {}
42
+ static_results = combined_results && combined_results['static_analysis_results'] ? combined_results['static_analysis_results'] : {}
43
43
  output += "--- Static Analysis Results ---\n".colorize(:bold)
44
44
 
45
45
  # Format Brakeman Results
46
- brakeman_result = static_results["brakeman"]
46
+ brakeman_result = static_results['brakeman']
47
47
  if brakeman_result
48
48
  output += "Brakeman:\n".colorize(:underline)
49
- if brakeman_result["warnings"] && !brakeman_result["warnings"].empty?
50
- brakeman_result["warnings"].each do |warning|
51
- severity_color = SEVERITY_COLORS[warning["confidence"]] || :white # Map Brakeman confidence to severity color
52
- output += " - Warning Type: #{warning["warning_type"].colorize(severity_color)}\n"
53
- output += " Message: #{warning["message"]}\n"
54
- output += " File: #{warning["file"]}\n"
55
- output += " Line: #{warning["line"]}\n"
56
- output += " Code: #{warning["code"]}\n"
57
- output += " Link: #{warning["link"]}\n"
49
+ if brakeman_result['warnings'] && !brakeman_result['warnings'].empty?
50
+ brakeman_result['warnings'].each do |warning|
51
+ severity_color = SEVERITY_COLORS[warning['confidence']] || :white # Map Brakeman confidence to severity color
52
+ output += " - Warning Type: #{warning['warning_type'].colorize(severity_color)}\n"
53
+ output += " Message: #{warning['message']}\n"
54
+ output += " File: #{warning['file']}\n"
55
+ output += " Line: #{warning['line']}\n"
56
+ output += " Code: #{warning['code']}\n"
57
+ output += " Link: #{warning['link']}\n"
58
58
  output += " \n"
59
59
  end
60
60
  else
@@ -66,27 +66,27 @@ module Omamori
66
66
  output += "\n"
67
67
 
68
68
  # Format Bundler-Audit Results
69
- bundler_audit_result = static_results["bundler_audit"]
70
- if bundler_audit_result && bundler_audit_result["scan"] && bundler_audit_result["scan"]["results"]
69
+ bundler_audit_result = static_results['bundler_audit']
70
+ if bundler_audit_result && bundler_audit_result['scan'] && bundler_audit_result['scan']['results']
71
71
  output += "Bundler-Audit:\n".colorize(:underline)
72
- scan_results = bundler_audit_result["scan"]["results"]
72
+ scan_results = bundler_audit_result['scan']['results']
73
73
 
74
74
  # Format vulnerabilities (type "unpatched_gem")
75
- vulnerabilities = scan_results.select { |result| result["type"] == "unpatched_gem" }
75
+ vulnerabilities = scan_results.select { |result| result['type'] == 'unpatched_gem' }
76
76
  if !vulnerabilities.empty?
77
77
  output += " Vulnerabilities:\n".colorize(:bold)
78
78
  vulnerabilities.each do |vulnerability_entry|
79
- advisory = vulnerability_entry["advisory"]
80
- gem_info = vulnerability_entry["gem"]
81
- severity_color = SEVERITY_COLORS[advisory["criticality"]] || :white # Map criticality to severity color
82
- output += " - ID: #{advisory["id"].colorize(severity_color)}\n"
83
- output += " Gem: #{gem_info["name"]} (#{gem_info["version"]})\n" # Include version
84
- output += " Title: #{advisory["title"]}\n"
85
- output += " URL: #{advisory["url"]}\n"
86
- output += " Criticality: #{advisory["criticality"].colorize(severity_color)}\n"
87
- output += " Description: #{advisory["description"]}\n"
88
- output += " Patched Versions: #{advisory["patched_versions"].join(', ')}\n"
89
- output += " Advisory Date: #{advisory["date"]}\n" # Use "date" key from advisory
79
+ advisory = vulnerability_entry['advisory']
80
+ gem_info = vulnerability_entry['gem']
81
+ severity_color = SEVERITY_COLORS[advisory['criticality']] || :white # Map criticality to severity color
82
+ output += " - ID: #{advisory['id'].colorize(severity_color)}\n"
83
+ output += " Gem: #{gem_info['name']} (#{gem_info['version']})\n" # Include version
84
+ output += " Title: #{advisory['title']}\n"
85
+ output += " URL: #{advisory['url']}\n"
86
+ output += " Criticality: #{advisory['criticality'].colorize(severity_color)}\n"
87
+ output += " Description: #{advisory['description']}\n"
88
+ output += " Patched Versions: #{advisory['patched_versions'].join(', ')}\n"
89
+ output += " Advisory Date: #{advisory['date']}\n" # Use "date" key from advisory
90
90
  output += "\n"
91
91
  end
92
92
  else
@@ -107,8 +107,14 @@ module Omamori
107
107
 
108
108
  # Add summary before scan complete
109
109
  ai_risk_count = ai_risks.length
110
- brakeman_warning_count = brakeman_result && brakeman_result["warnings"] ? brakeman_result["warnings"].length : 0
111
- bundler_audit_vulnerability_count = bundler_audit_result && bundler_audit_result["scan"] && bundler_audit_result["scan"]["results"] ? bundler_audit_result["scan"]["results"].select { |result| result["type"] == "unpatched_gem" }.length : 0
110
+ brakeman_warning_count = brakeman_result && brakeman_result['warnings'] ? brakeman_result['warnings'].length : 0
111
+ bundler_audit_vulnerability_count = if bundler_audit_result && bundler_audit_result['scan'] && bundler_audit_result['scan']['results']
112
+ bundler_audit_result['scan']['results'].select do |result|
113
+ result['type'] == 'unpatched_gem'
114
+ end.length
115
+ else
116
+ 0
117
+ end
112
118
 
113
119
  summary_output = "--- Scan Summary ---\n".colorize(:bold)
114
120
  summary_output += "AI Analysis: #{ai_risk_count} issues".colorize(ai_risk_count > 0 ? :red : :green) + "\n"
@@ -129,4 +135,4 @@ module Omamori
129
135
  end
130
136
  end
131
137
  end
132
- end
138
+ end
@@ -8,7 +8,7 @@ module Omamori
8
8
  def initialize(output_path_prefix, template_path = nil)
9
9
  @output_path_prefix = output_path_prefix
10
10
  # Use provided template_path if not nil, otherwise use default
11
- @template_path = template_path || File.join(__dir__, "report_template.erb")
11
+ @template_path = template_path || File.join(__dir__, 'report_template.erb')
12
12
  @template = ERB.new(File.read(@template_path))
13
13
  rescue Errno::ENOENT
14
14
  raise "HTML template file not found at #{@template_path}" # Raise error if template is not found
@@ -16,16 +16,16 @@ module Omamori
16
16
 
17
17
  def format(combined_results)
18
18
  # Prepare data for the template
19
- @ai_risks = combined_results && combined_results["ai_security_risks"] ? combined_results["ai_security_risks"] : []
20
- @static_results = combined_results && combined_results["static_analysis_results"] ? combined_results["static_analysis_results"] : {}
19
+ @ai_risks = combined_results && combined_results['ai_security_risks'] ? combined_results['ai_security_risks'] : []
20
+ @static_results = combined_results && combined_results['static_analysis_results'] ? combined_results['static_analysis_results'] : {}
21
21
 
22
22
  # Render the template
23
23
  @template.result(binding)
24
24
  rescue Errno::ENOENT
25
- "Error: HTML template file not found."
26
- rescue => e
25
+ 'Error: HTML template file not found.'
26
+ rescue StandardError => e
27
27
  "Error generating HTML report: #{e.message}"
28
28
  end
29
29
  end
30
30
  end
31
- end
31
+ end
@@ -16,4 +16,4 @@ module Omamori
16
16
  end
17
17
  end
18
18
  end
19
- end
19
+ end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ai_settings'
4
+
5
+ module Omamori
6
+ # The files `omamori init` and `omamori ci-setup` write. Provider names are not listed here:
7
+ # whatever AISettings accepts (any RubyLLM provider, claude_code, codex) is written as given.
8
+ module SetupFiles
9
+ OMAMORIIGNORE = <<~IGNORE
10
+ # Omamori ignore file
11
+ # Add files and directories to ignore during Omamori scans.
12
+ # Lines starting with # are comments.
13
+ # Globs are supported (e.g., *.tmp, spec/fixtures/)
14
+ # Negation with ! (e.g., !important.log) - not fully implemented in current basic matcher
15
+
16
+ # Log files
17
+ log/
18
+ *.log
19
+
20
+ # Temporary files
21
+ tmp/
22
+ *.tmp
23
+ *.swp
24
+ *.swo
25
+
26
+ # OS-specific files
27
+ .DS_Store
28
+ Thumbs.db
29
+
30
+ # Vendor directory (often contains third-party code)
31
+ vendor/bundle/
32
+
33
+ # Coverage reports
34
+ coverage/
35
+
36
+ # Node.js dependencies
37
+ node_modules/
38
+
39
+ # Build artifacts
40
+ pkg/
41
+
42
+ # Test files and fixtures (optional, consider if they contain sensitive examples)
43
+ # spec/
44
+ # test/
45
+ # features/
46
+
47
+ # Database schema and migrations (usually not directly exploitable via code injection)
48
+ # db/schema.rb
49
+ # db/migrate/
50
+
51
+ # Assets (compiled or static, less likely to have Ruby vulnerabilities)
52
+ # app/assets/builds/
53
+ # public/assets/
54
+ IGNORE
55
+
56
+ # claude_code / codex use the CLI's login on the machine. omamori does not install or log in CLIs on
57
+ # CI runners: their packages and login methods are theirs to change.
58
+ CLI_IN_CI_NOTE = 'claude_code / codex need the CLI installed and logged in on the runner, which this ' \
59
+ 'configuration does not do. Set that up yourself, or use an API reporter in CI ' \
60
+ '(add --reporter PROVIDER --reporter-model MODEL to the scan command).'
61
+
62
+ module_function
63
+
64
+ # settings: a resolved AISettings. language: e.g. "ja", or nil for the default (en)
65
+ def omamorirc(settings, language: nil)
66
+ <<~YAML
67
+ # .omamorirc
68
+ # Generated by `omamori init`. Check the setup with `omamori doctor`.
69
+
70
+ # Reporter: the LLM that writes reports. Any RubyLLM provider (with a model),
71
+ # or claude_code / codex to use a subscription CLI (claude -p / codex exec).
72
+ #{role_yaml('reporter', settings.reporter.merge(fallback_hash(settings)))}
73
+ # concurrency: 8 # Reports written in parallel (default 3 for CLIs, 8 for APIs)
74
+
75
+ # Classifier (optional): a fast decision model that checks each risk as a yes/no question.
76
+ # Only the flagged checks are sent to the reporter. Without it, the reporter checks everything.
77
+ #{classifier_yaml(settings)}
78
+
79
+ #{api_key_comment(settings)}
80
+
81
+ # Language of the report details (default: en)
82
+ #{language ? "language: #{language}" : '# language: ja'}
83
+
84
+ # Security checks to run (optional; default: the 8 main checks without a classifier, all 35 with one)
85
+ # checks:
86
+ # - xss
87
+ # - csrf
88
+ # - idor
89
+
90
+ # Report output settings (optional)
91
+ # report:
92
+ # output_path: ./omamori_report # Prefix for html/json reports
93
+ # html_template: custom_report_template.erb
94
+
95
+ # Static analyser options (optional)
96
+ # static_analysers:
97
+ # brakeman:
98
+ # options: "--force"
99
+ # bundler_audit:
100
+ # options: "--quiet"
101
+
102
+ # Chunk size for splitting large code content (optional, default: 8000 characters)
103
+ # chunk_size: 10000
104
+
105
+ # CI setup file paths (optional)
106
+ # ci_setup:
107
+ # github_actions_path: .github/workflows/omamori_scan.yml
108
+ # gitlab_ci_path: .gitlab-ci.yml
109
+ YAML
110
+ end
111
+
112
+ # What the user has to do outside omamori before scanning: one line per role
113
+ def credential_steps(settings)
114
+ roles(settings).map do |role, role_settings|
115
+ provider = role_settings['provider']
116
+ case provider
117
+ when 'claude_code' then "#{role}: install Claude Code and log in (`claude`), or set ANTHROPIC_API_KEY"
118
+ when 'codex' then "#{role}: install the Codex CLI and log in (`codex login`)"
119
+ else
120
+ env = AISettings.api_key_env(provider)
121
+ env ? "#{role}: set #{env}" : "#{role}: set what RubyLLM needs for #{provider} (https://rubyllm.com)"
122
+ end
123
+ end
124
+ end
125
+
126
+ def github_actions(settings)
127
+ <<~YAML
128
+ # .github/workflows/omamori_scan.yml
129
+ name: Omamori Security Scan
130
+
131
+ on:
132
+ push:
133
+ branches: [ main, master ]
134
+ pull_request:
135
+ branches: [ main, master ]
136
+
137
+ jobs:
138
+ security_scan:
139
+ runs-on: ubuntu-latest
140
+
141
+ steps:
142
+ - name: Checkout code
143
+ uses: actions/checkout@v4
144
+ with:
145
+ fetch-depth: 0 # Needed to diff against the base branch
146
+
147
+ - name: Set up Ruby
148
+ uses: ruby/setup-ruby@v1 # Reads .ruby-version
149
+ with:
150
+ bundler-cache: true
151
+ #{cli_note(settings, ' ')}
152
+ - name: Run Omamori Scan
153
+ env:
154
+ #{env_lines(settings, indent: ' ') { |name| " #{name}: ${{ secrets.#{name} }}" }}
155
+ run: |
156
+ if [ "$GITHUB_EVENT_NAME" == "pull_request" ]; then
157
+ bundle exec omamori scan --base "origin/$GITHUB_BASE_REF" --format console
158
+ else
159
+ bundle exec omamori scan --all --format console
160
+ fi
161
+ YAML
162
+ end
163
+
164
+ def gitlab_ci(settings)
165
+ <<~YAML
166
+ # .gitlab-ci.yml
167
+ stages:
168
+ - security_scan
169
+
170
+ omamori_security_scan:
171
+ stage: security_scan
172
+ image: ruby:3.3 # Use your project's Ruby version
173
+ cache:
174
+ key:
175
+ files:
176
+ - Gemfile.lock
177
+ paths:
178
+ - vendor/bundle
179
+ before_script:
180
+ - bundle config set --local path vendor/bundle
181
+ - bundle install --jobs $(nproc) --retry 3
182
+ #{cli_note(settings, ' ')}
183
+ script:
184
+ - |
185
+ if [ "$CI_PIPELINE_SOURCE" == "merge_request_event" ]; then
186
+ bundle exec omamori scan --base "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" --format console
187
+ else
188
+ bundle exec omamori scan --all --format console
189
+ fi
190
+ # Set these as masked CI/CD variables in GitLab:
191
+ #{env_lines(settings, indent: ' ') { |name| " # #{name}" }}
192
+ variables:
193
+ GIT_DEPTH: "0" # Needed to diff against the target branch
194
+ rules:
195
+ - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
196
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
197
+ YAML
198
+ end
199
+
200
+ # [[role name, settings], ...] for the classifier, reporter and fallback reporter in use
201
+ def roles(settings)
202
+ [['classifier', settings.classifier], ['reporter', settings.reporter],
203
+ ['reporter.fallback', settings.reporter_fallback]].select { |_, role_settings| role_settings }
204
+ end
205
+
206
+ def role_yaml(name, role_settings)
207
+ lines = ["#{name}:"]
208
+ role_settings.each do |key, value|
209
+ if value.is_a?(Hash)
210
+ lines << " #{key}:"
211
+ value.each { |sub_key, sub_value| lines << " #{sub_key}: #{sub_value}" }
212
+ else
213
+ lines << " #{key}: #{value}"
214
+ end
215
+ end
216
+ lines.join("\n")
217
+ end
218
+
219
+ def classifier_yaml(settings)
220
+ threshold = " # threshold: #{AISettings::DEFAULT_THRESHOLD} # Checks at or above this probability go to the reporter"
221
+ return "#{role_yaml('classifier', settings.classifier)}\n#{threshold}" if settings.classifier?
222
+
223
+ "# classifier:\n# provider: typesafe # typesafe / openai / ollama\n# model: jev-1.13.0\n##{threshold[1..]}"
224
+ end
225
+
226
+ def fallback_hash(settings)
227
+ settings.reporter_fallback ? { 'fallback' => settings.reporter_fallback } : {}
228
+ end
229
+
230
+ def api_key_comment(settings)
231
+ names = roles(settings).filter_map { |_, role_settings| AISettings.api_key_env(role_settings['provider']) }.uniq
232
+ return '# API keys are read from environment variables (PROVIDER_API_KEY, e.g. ANTHROPIC_API_KEY).' if names.empty?
233
+
234
+ "# API keys are read from environment variables: #{names.join(', ')}"
235
+ end
236
+
237
+ def cli_providers(settings)
238
+ roles(settings).map { |_, role_settings| role_settings['provider'] } & AISettings::CLI_REPORTERS
239
+ end
240
+
241
+ # API key environment variables the scan step needs
242
+ def secret_names(settings)
243
+ roles(settings).filter_map { |_, role_settings| AISettings.api_key_env(role_settings['provider']) }.uniq
244
+ end
245
+
246
+ def env_lines(settings, indent:, &format)
247
+ names = secret_names(settings)
248
+ return "#{indent}# Add the environment variables RubyLLM needs for your provider" if names.empty?
249
+
250
+ names.map(&format).join("\n")
251
+ end
252
+
253
+ # A comment in the CI file when a role uses a subscription CLI
254
+ def cli_note(settings, indent)
255
+ return '' if cli_providers(settings).empty?
256
+
257
+ "#{indent}# #{CLI_IN_CI_NOTE}"
258
+ end
259
+ end
260
+ end
@@ -8,7 +8,7 @@ module Omamori
8
8
  end
9
9
 
10
10
  def run
11
- puts "Running Brakeman..."
11
+ puts 'Running Brakeman...'
12
12
  # Determine Brakeman command based on options
13
13
  # Use --force to run scan even if it's not a Rails application
14
14
  # Use -f json for JSON output
@@ -18,11 +18,11 @@ module Omamori
18
18
  if value.is_a?(TrueClass)
19
19
  key.to_s
20
20
  elsif value.is_a?(FalseClass)
21
- "" # Don't include false flags
21
+ '' # Don't include false flags
22
22
  else
23
23
  "#{key} #{value}"
24
24
  end
25
- end.join(" ").strip
25
+ end.join(' ').strip
26
26
 
27
27
  brakeman_command = "brakeman -f json . --force #{options_string}".strip # strip again in case options_string is empty
28
28
 
@@ -35,17 +35,17 @@ module Omamori
35
35
  # the test setup might be causing it to be called multiple times or the mock is misconfigured.
36
36
  JSON.parse(brakeman_output)
37
37
  rescue Errno::ENOENT
38
- puts "Error: Brakeman command not found. Is Brakeman installed?"
38
+ puts 'Error: Brakeman command not found. Is Brakeman installed?'
39
39
  nil
40
40
  rescue JSON::ParserError
41
- puts "Error: Failed to parse Brakeman JSON output."
41
+ puts 'Error: Failed to parse Brakeman JSON output.'
42
42
  puts "Raw output:\n#{brakeman_output}"
43
43
  nil
44
- rescue => e
44
+ rescue StandardError => e
45
45
  puts "An error occurred during Brakeman execution: #{e.message}"
46
46
  nil
47
47
  end
48
48
  end
49
49
  end
50
50
  end
51
- end
51
+ end
@@ -14,7 +14,9 @@ module Omamori
14
14
  # Example: bundle audit --format json
15
15
  # Include options passed during initialization
16
16
  # Build options string from the options hash
17
- options_string = @options.map { |key, value| "--#{key.to_s.gsub('_', '-')}" + (value == true ? "" : " #{value}") }.join(" ")
17
+ options_string = @options.map do |key, value|
18
+ "--#{key.to_s.gsub('_', '-')}" + (value == true ? '' : " #{value}")
19
+ end.join(' ')
18
20
  bundler_audit_command = "bundle audit --format json#{options_string.empty? ? '' : " #{options_string}"}"
19
21
 
20
22
  begin
@@ -32,22 +34,22 @@ module Omamori
32
34
  # Return the entire parsed output hash
33
35
  parsed_output
34
36
  else
35
- puts "Error: Unexpected Bundler-Audit JSON output structure."
37
+ puts 'Error: Unexpected Bundler-Audit JSON output structure.'
36
38
  puts "Raw output:\n#{bundler_audit_output}"
37
39
  nil # Return nil if the structure is unexpected
38
40
  end
39
41
  rescue Errno::ENOENT
40
- puts "Error: bundle command not found. Is Bundler installed?"
42
+ puts 'Error: bundle command not found. Is Bundler installed?'
41
43
  nil
42
44
  rescue JSON::ParserError
43
- puts "Error: Failed to parse Bundler-Audit JSON output."
45
+ puts 'Error: Failed to parse Bundler-Audit JSON output.'
44
46
  puts "Raw output:\n#{bundler_audit_output}"
45
47
  nil
46
- rescue => e
48
+ rescue StandardError => e
47
49
  puts "An error occurred during Bundler-Audit execution: #{e.message}"
48
50
  nil
49
51
  end
50
52
  end
51
53
  end
52
54
  end
53
- end
55
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omamori
4
- VERSION = "0.1.2"
5
- end
4
+ VERSION = '0.2.0'
5
+ end