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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Omamori
|
|
6
|
+
module AIAnalysisEngine
|
|
7
|
+
module CLIClients
|
|
8
|
+
# `claude -p` (Claude Code) with all tools disabled and structured output.
|
|
9
|
+
class ClaudeCodeClient < Base
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def default_command
|
|
13
|
+
'claude'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build_command(_dir, json_schema, model)
|
|
17
|
+
command = [@command, '-p', '--tools', '', '--strict-mcp-config', '--no-session-persistence',
|
|
18
|
+
'--output-format', 'json', '--json-schema', json_schema.to_json]
|
|
19
|
+
command += ['--model', model] if model
|
|
20
|
+
command
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def parse(output, _dir)
|
|
24
|
+
response = JSON.parse(output)
|
|
25
|
+
if response['is_error']
|
|
26
|
+
puts "Warning: claude returned an error: #{response['result'].to_s[0, 500]}"
|
|
27
|
+
return nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
response['structured_output'] || JSON.parse(response['result'].to_s)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Omamori
|
|
6
|
+
module AIAnalysisEngine
|
|
7
|
+
module CLIClients
|
|
8
|
+
# `codex exec` in a read-only sandbox with an output schema.
|
|
9
|
+
class CodexClient < Base
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def default_command
|
|
13
|
+
'codex'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build_command(dir, json_schema, model)
|
|
17
|
+
schema_path = File.join(dir, 'schema.json')
|
|
18
|
+
File.write(schema_path, JSON.generate(strict_schema(JSON.parse(json_schema.to_json))))
|
|
19
|
+
command = [@command, 'exec', '--sandbox', 'read-only', '--skip-git-repo-check', '--ephemeral',
|
|
20
|
+
'--output-schema', schema_path, '--output-last-message', File.join(dir, 'last_message.json')]
|
|
21
|
+
command += ['--model', model] if model
|
|
22
|
+
command + ['-']
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def parse(_output, dir)
|
|
26
|
+
path = File.join(dir, 'last_message.json')
|
|
27
|
+
File.exist?(path) ? JSON.parse(File.read(path)) : nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# OpenAI structured outputs require every property to be required and no additional properties
|
|
31
|
+
def strict_schema(schema)
|
|
32
|
+
case schema
|
|
33
|
+
when Hash
|
|
34
|
+
strict = schema.transform_values { |value| strict_schema(value) }
|
|
35
|
+
if strict['type'] == 'object' && strict['properties']
|
|
36
|
+
strict['required'] = strict['properties'].keys
|
|
37
|
+
strict['additionalProperties'] = false
|
|
38
|
+
end
|
|
39
|
+
strict
|
|
40
|
+
when Array then schema.map { |value| strict_schema(value) }
|
|
41
|
+
else schema
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -1,24 +1,32 @@
|
|
|
1
|
-
#
|
|
1
|
+
# lib/omamori/ai_analysis_engine/diff_splitter.rb
|
|
2
|
+
|
|
3
|
+
require_relative 'llm_client'
|
|
2
4
|
|
|
3
5
|
module Omamori
|
|
4
6
|
module AIAnalysisEngine
|
|
5
7
|
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
8
|
DEFAULT_CHUNK_SIZE = 8000 # Characters as a proxy for tokens
|
|
11
9
|
|
|
12
10
|
def initialize(chunk_size: DEFAULT_CHUNK_SIZE)
|
|
13
11
|
@chunk_size = chunk_size
|
|
14
12
|
end
|
|
15
13
|
|
|
14
|
+
# Splits a unified diff into [file_path, file_diff] pairs
|
|
15
|
+
def split_by_file(diff)
|
|
16
|
+
diff.split(/^(?=diff --git )/).filter_map do |file_diff|
|
|
17
|
+
next if file_diff.strip.empty?
|
|
18
|
+
|
|
19
|
+
path = file_diff[%r{\Adiff --git a/.+? b/(.+)$}, 1] || 'unknown'
|
|
20
|
+
[path, file_diff]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
16
24
|
def split(content)
|
|
17
25
|
chunks = []
|
|
18
|
-
current_chunk =
|
|
26
|
+
current_chunk = ''
|
|
19
27
|
content.each_line do |line|
|
|
20
|
-
if (current_chunk.length + line.length) > @chunk_size
|
|
21
|
-
chunks << current_chunk
|
|
28
|
+
if (current_chunk.length + line.length) > @chunk_size && !current_chunk.empty?
|
|
29
|
+
chunks << current_chunk
|
|
22
30
|
current_chunk = line
|
|
23
31
|
else
|
|
24
32
|
current_chunk += line
|
|
@@ -28,35 +36,35 @@ module Omamori
|
|
|
28
36
|
chunks
|
|
29
37
|
end
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
# Updated to accept file_path keyword argument
|
|
40
|
+
def process_in_chunks(content, gemini_client, json_schema, prompt_manager, risks_to_check,
|
|
41
|
+
model: LLMClient::DEFAULT_MODEL, file_path: nil)
|
|
32
42
|
all_results = []
|
|
33
43
|
chunks = split(content)
|
|
34
44
|
|
|
35
|
-
puts "Splitting content into #{chunks.size} chunks
|
|
45
|
+
puts "[DEBUG Omamori DiffSplitter] Splitting content into #{chunks.size} chunks for file: #{file_path || 'N/A'}"
|
|
36
46
|
|
|
37
47
|
chunks.each_with_index do |chunk, index|
|
|
38
|
-
puts "Processing chunk #{index + 1}/#{chunks.size}
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
puts "[DEBUG Omamori DiffSplitter] Processing chunk #{index + 1}/#{chunks.size} for file: #{file_path || 'N/A'}"
|
|
49
|
+
# Pass file_path (potentially modified for chunks) to build_prompt
|
|
50
|
+
chunk_file_path_info = if file_path
|
|
51
|
+
chunks.size > 1 ? "#{file_path} (chunk #{index + 1}/#{chunks.size})" : file_path
|
|
52
|
+
end
|
|
53
|
+
prompt = prompt_manager.build_prompt(chunk, risks_to_check, json_schema, file_path: chunk_file_path_info)
|
|
54
|
+
result = gemini_client.analyze(prompt, json_schema, model: model) # This call to analyze is correct
|
|
41
55
|
all_results << result
|
|
42
|
-
# TODO: Handle potential rate limits or errors between chunks
|
|
43
56
|
end
|
|
44
|
-
|
|
45
|
-
# TODO: Combine results from all chunks
|
|
46
57
|
combine_results(all_results)
|
|
47
58
|
end
|
|
48
59
|
|
|
49
60
|
private
|
|
50
61
|
|
|
51
62
|
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
63
|
combined_risks = results.flat_map do |result|
|
|
56
|
-
result && result[
|
|
64
|
+
result && result['security_risks'] ? result['security_risks'] : []
|
|
57
65
|
end
|
|
58
|
-
{
|
|
66
|
+
{ 'security_risks' => combined_risks }
|
|
59
67
|
end
|
|
60
68
|
end
|
|
61
69
|
end
|
|
62
|
-
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Omamori
|
|
4
|
+
module AIAnalysisEngine
|
|
5
|
+
# Asks the primary reporter, and the fallback reporter only when the primary gives no answer
|
|
6
|
+
# (a refusal, an error, or an unusable response). Same interface as LLMClient#analyze.
|
|
7
|
+
class FallbackClient
|
|
8
|
+
def initialize(primary, fallback, fallback_model:)
|
|
9
|
+
@primary = primary
|
|
10
|
+
@fallback = fallback
|
|
11
|
+
@fallback_model = fallback_model
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def analyze(prompt, json_schema, model: nil)
|
|
15
|
+
result = @primary.analyze(prompt, json_schema, model: model)
|
|
16
|
+
return result if result
|
|
17
|
+
|
|
18
|
+
puts 'Asking the fallback reporter...'
|
|
19
|
+
@fallback.analyze(prompt, json_schema, model: @fallback_model)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'ruby_llm_context'
|
|
4
|
+
|
|
5
|
+
module Omamori
|
|
6
|
+
module AIAnalysisEngine
|
|
7
|
+
# Asks a decision model (TypeSafe Jev, OpenAI Decisions, ...) yes/no questions through RubyLLM.judge.
|
|
8
|
+
class JudgeClient
|
|
9
|
+
# Input tokens used so far, across threads
|
|
10
|
+
attr_reader :input_tokens
|
|
11
|
+
|
|
12
|
+
# max_questions: split larger question sets into several requests (OpenAI accepts up to 200 per request)
|
|
13
|
+
def initialize(context, provider:, model:, max_questions: nil)
|
|
14
|
+
@context = context
|
|
15
|
+
@provider = provider
|
|
16
|
+
@model = model
|
|
17
|
+
@max_questions = max_questions
|
|
18
|
+
@input_tokens = 0
|
|
19
|
+
@mutex = Mutex.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# questions: { name => instructions }
|
|
23
|
+
# Returns { name => probability that the answer is yes, or nil when the model gave none (e.g. a refusal) }
|
|
24
|
+
def classify(state, questions)
|
|
25
|
+
batches = @max_questions ? questions.each_slice(@max_questions).map(&:to_h) : [questions]
|
|
26
|
+
batches.map { |batch| judge(state, batch) }.reduce({}, :merge)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def judge(state, questions)
|
|
32
|
+
judgment = @context.judge(
|
|
33
|
+
state,
|
|
34
|
+
questions: questions.transform_values { |instructions| { type: :probability, instructions: instructions } },
|
|
35
|
+
model: @model, provider: @provider, assume_model_exists: true
|
|
36
|
+
)
|
|
37
|
+
@mutex.synchronize { @input_tokens += judgment.tokens&.input.to_i }
|
|
38
|
+
questions.keys.to_h do |name|
|
|
39
|
+
answer = judgment.answers[name]
|
|
40
|
+
[name, answer.respond_to?(:probability) ? answer.probability : nil]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'ruby_llm_context'
|
|
5
|
+
|
|
6
|
+
module Omamori
|
|
7
|
+
module AIAnalysisEngine
|
|
8
|
+
# Asks any RubyLLM chat model for a structured security report.
|
|
9
|
+
class LLMClient
|
|
10
|
+
DEFAULT_MODEL = 'gemini-3.8-flash'
|
|
11
|
+
EMPTY_RESPONSE_RETRIES = 2
|
|
12
|
+
# Finish reasons that mean the model refused; asking again gives the same result
|
|
13
|
+
REFUSAL_REASONS = %i[other safety prohibited_content blocklist spii recitation refusal content_filter].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(context, provider: nil)
|
|
16
|
+
@context = context
|
|
17
|
+
@provider = provider
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the parsed JSON (a Hash with 'security_risks') or nil
|
|
21
|
+
def analyze(prompt, json_schema, model: DEFAULT_MODEL)
|
|
22
|
+
response = ask_with_text(prompt, json_schema, model)
|
|
23
|
+
return nil unless response
|
|
24
|
+
|
|
25
|
+
parsed = JSON.parse(response.content.gsub(/\A```json\n|```\z/, '').strip)
|
|
26
|
+
return parsed if parsed.is_a?(Hash) && parsed.key?('security_risks')
|
|
27
|
+
|
|
28
|
+
puts 'Warning: Unexpected AI analysis response structure.'
|
|
29
|
+
puts "Raw response text: #{response.content}"
|
|
30
|
+
nil
|
|
31
|
+
rescue JSON::ParserError
|
|
32
|
+
puts 'Warning: Failed to parse response text as JSON.'
|
|
33
|
+
puts "Raw response text: #{response.content}"
|
|
34
|
+
nil
|
|
35
|
+
rescue RubyLLM::Error => e
|
|
36
|
+
puts "API Error: #{e.message}"
|
|
37
|
+
nil
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
puts "An unexpected error occurred during API call: #{e.message}"
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# Models occasionally return an empty response. Retry a couple of times before giving up,
|
|
46
|
+
# unless the model refused: an unchecked chunk must not look like a clean result.
|
|
47
|
+
def ask_with_text(prompt, json_schema, model)
|
|
48
|
+
(EMPTY_RESPONSE_RETRIES + 1).times do
|
|
49
|
+
response = chat(model).with_schema(json_schema).ask(prompt)
|
|
50
|
+
return response unless response.content.to_s.strip.empty?
|
|
51
|
+
|
|
52
|
+
reason = response.finish_reason
|
|
53
|
+
if REFUSAL_REASONS.include?(reason.to_s.downcase.to_sym)
|
|
54
|
+
puts "Warning: The model refused to analyze this content (finish reason: #{reason}). It was NOT checked."
|
|
55
|
+
return nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts "Warning: Empty AI response (finish reason: #{reason || 'unknown'}). Retrying..."
|
|
59
|
+
end
|
|
60
|
+
puts 'Warning: AI returned no text after retries.'
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def chat(model)
|
|
65
|
+
return @context.chat(model: model) unless @provider
|
|
66
|
+
|
|
67
|
+
@context.chat(model: model, provider: @provider, assume_model_exists: true)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
require 'yaml'
|
|
2
2
|
|
|
3
3
|
module Omamori
|
|
4
4
|
module AIAnalysisEngine
|
|
@@ -10,7 +10,9 @@ module Omamori
|
|
|
10
10
|
Report any detected risks in the format specified by the following JSON Schema:
|
|
11
11
|
%{json_schema}
|
|
12
12
|
If no risks are found, output an empty list for the "security_risks" array.
|
|
13
|
+
If the code is a git diff, a vulnerability can also come from removing or weakening an existing protection, not only from added code.
|
|
13
14
|
Please provide your response in %{language}.
|
|
15
|
+
#{"File context (if available): %{file_path}" if ENV['OMAMORI_DEBUG_PROMPT']}
|
|
14
16
|
|
|
15
17
|
【Code to Analyze】:
|
|
16
18
|
%{code_content}
|
|
@@ -48,11 +50,13 @@ dangerous_eval_prompt = <<~PROMPT
|
|
|
48
50
|
Search for methods enabling dynamic code execution in Ruby code (e.g., eval, instance_eval, class_eval, send, public_send, system, exec, backticks `).
|
|
49
51
|
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
52
|
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.
|
|
53
|
+
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
54
|
PROMPT
|
|
53
|
-
|
|
55
|
+
|
|
54
56
|
RISK_PROMPTS = {
|
|
55
57
|
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.",
|
|
58
|
+
sql_injection: "SQL Injection: User input is placed into a SQL query as raw text, so an attacker can change the query to read, modify, or delete data or bypass authentication. Detection steps: 1) Find ActiveRecord methods that accept raw SQL strings (where, having, joins, from, group, select, lock, find_by_sql, order or pluck with Arel.sql) and direct connection calls (execute, select_all, select_value, exec_query). 2) Check whether user input (params, request data, cookies, or values derived from them) is interpolated or concatenated into those strings, including hand-built LIKE and IN (...) clauses. 3) Treat hash conditions, ? or named placeholders, sanitize_sql_array, connection.quote, and integer casting as safe. 4) Remember that since Rails 6, order and pluck reject raw SQL that is not a column name, unless it is wrapped in Arel.sql. 5) Check column or direction names taken from user input are mapped through an allowlist.",
|
|
59
|
+
mass_assignment: "Mass Assignment: A model is created or updated from request parameters without restricting which attributes may be set, so a user can change fields they should not control, such as their role, owner, price, or balance. Detection steps: 1) Find model writes (new, create, update, assign_attributes, insert_all, upsert) that take request data. 2) Check for params.permit!, to_unsafe_h / to_unsafe_hash, and permit lists that are not fixed (permit(*Model.column_names), a list chosen by the client). A hash permit such as permit(preferences: {}) is only a problem when its keys become model attributes, not when it fills a single JSON or serialized column. 3) Check for attributes that bypass strong parameters, such as JSON.parse(request.body.read) or request.request_parameters passed to a model. 4) Check whether user-facing actions (sign-up, profile, member APIs) permit sensitive attributes: role, admin, owner_id / user_id / account_id, balance, price, plan, verified, confirmed_at. 5) Treat an explicit permit list without sensitive attributes, params.expect with such a list, and sensitive attributes permitted only in admin-only actions with an authorization check as safe.",
|
|
56
60
|
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.",
|
|
57
61
|
idor: "Insecure Direct Object Reference (IDOR): Occurs when object identifiers (e.g., IDs) are exposed and access control is missing, allowing unauthorized access to other users' data. Detection steps: 1) Identify endpoints that access data using user-supplied identifiers (e.g., params[:id]). 2) Check if proper authorization checks exist before accessing the data (e.g., current_user.orders vs Order.find). 3) Look for functions retrieving data without verifying the current user's ownership or access rights. 4) Check for sequential or predictable IDs that can be enumerated. 5) Verify if sensitive operations verify resource ownership before modifications. 6) Look for authorization checks that can be bypassed through parameter manipulation. 7) Examine APIs that return data based on user-supplied identifiers.",
|
|
58
62
|
open_redirect: "Open Redirect: Redirecting users to external URLs based on user-supplied input without proper validation. Detection steps: 1) Identify redirect methods or functions (redirect_to, headers['Location'], response.redirect, etc.). 2) Check if the redirect URL or destination can be controlled by user input. 3) Verify if there is proper validation of the redirect URL to prevent external redirects. 4) Look for validation patterns that only check for URL prefixes that could be bypassed. 5) Check if the code restricts redirects to only allowed domains or uses relative paths. 6) Watch for URL manipulation techniques that might bypass validation (using //, additional domains in path, URL encoding).",
|
|
@@ -65,30 +69,28 @@ dangerous_eval_prompt = <<~PROMPT
|
|
|
65
69
|
dangerous_eval: "Dangerous Code Execution (eval, exec): Dynamic code execution using untrusted input, allowing arbitrary code injection. Detection steps: 1) Search for methods enabling dynamic code execution (eval, instance_eval, class_eval, send, public_send, system, exec, ` ``). 2) Check if arguments passed to these methods originate from or are influenced by user input. 3) Verify if user input is rigorously sanitized or validated before use, or if execution is restricted to safe, predefined commands/methods. 4) Assess if safer alternatives can replace dynamic execution.",
|
|
66
70
|
inappropriate_file_permissions: "Insecure File Permissions: Files or directories with overly permissive modes (e.g., 777), allowing unauthorized read/write/execute access. Detection steps: 1) Find code that creates files/directories or changes permissions (e.g., FileUtils.chmod, File.chmod, Dir.mkdir with mode). 2) Examine the permission modes being set (e.g., 0777, 0666). 3) Evaluate if permissions follow the principle of least privilege, especially avoiding world-writable (o+w) or world-readable (o+r) for sensitive files. 4) Check permissions of configuration files, log files, and uploaded files.",
|
|
67
71
|
temporary_backup_file_leak: "Temporary or Backup File Exposure: Sensitive files like .bak, .tmp, or ~ versions are publicly accessible due to poor file handling. Detection steps: 1) Identify logic creating temporary or backup files. 2) Check if common backup extensions (.bak, .tmp, ~, .old) are used. 3) Verify these files are not created within web-accessible directories. 4) Ensure temporary/backup files are securely deleted after use. 5) Check if .gitignore excludes these file patterns.",
|
|
68
|
-
overly_detailed_errors: "Excessive Error Information Disclosure: Stack traces
|
|
69
|
-
csp_not_set: "
|
|
70
|
-
mime_sniffing_vulnerability: "MIME Sniffing
|
|
71
|
-
clickjacking_vulnerability: "Clickjacking Protection
|
|
72
|
-
auto_index_exposure: "
|
|
73
|
-
inappropriate_password_policy: "
|
|
74
|
-
two_factor_auth_missing: "
|
|
72
|
+
overly_detailed_errors: "Excessive Error and Server Information Disclosure: Stack traces, internal error messages, or server details (software names and versions, hostnames, internal paths, database queries) are exposed to users, leaking implementation details. Detection steps: 1) Examine error handling code (rescue blocks, rescue_from, error page rendering, exceptions_app). 2) Check what error responses send to the client in production, including API JSON errors. 3) Flag responses that include exception messages, backtraces, internal paths, queries, configuration, or server and framework versions. 4) Flag production settings that show detailed errors (config.consider_all_requests_local = true) or headers that reveal server software and versions. 5) Treat generic messages shown to users, with details only logged on the server (for example with a request id), as safe.",
|
|
73
|
+
csp_not_set: "Weakened or Removed Content Security Policy: The change removes, disables, or loosens the Content-Security-Policy, which makes XSS easier to exploit. Detection steps: 1) Look for changes to the CSP configuration (config/initializers/content_security_policy.rb, content_security_policy blocks or calls in controllers, headers set by hand or in a web server config). 2) Flag removing the policy, turning it off (content_security_policy false), switching it to report-only in production, removing nonces, or adding 'unsafe-inline', 'unsafe-eval', wildcards (*), or data: to script-src or default-src. 3) Adding one specific trusted host while keeping nonces and the rest of the policy is not a weakening. 4) Do not flag code that does not touch the policy: whether the application has a CSP at all cannot be judged from a change that does not touch it.",
|
|
74
|
+
mime_sniffing_vulnerability: "Weakened MIME Sniffing Protection: The change removes or overrides X-Content-Type-Options: nosniff, or starts serving user-supplied files with a content type the user controls, so browsers may run uploaded content as HTML or script. Detection steps: 1) Look for changes to default headers (config.action_dispatch.default_headers), headers set or deleted in controllers or middleware, and web server header settings. 2) Flag replacing the default headers without nosniff, deleting the header, or serving uploads inline with the uploaded content type. 3) Adding other headers while keeping nosniff is not a weakening. 4) Do not flag code that does not touch these headers or file serving.",
|
|
75
|
+
clickjacking_vulnerability: "Weakened Clickjacking Protection: The change removes or loosens X-Frame-Options or the CSP frame-ancestors directive, so other sites can frame the application's pages. Detection steps: 1) Look for changes to default headers, headers set or deleted in controllers, CSP frame_ancestors, and web server header settings. 2) Flag deleting X-Frame-Options without an equivalent frame-ancestors, setting ALLOWALL or frame-ancestors *, or allowing any origin to frame sensitive pages. 3) Allowing framing only by specific trusted origins, or relaxing DENY to SAMEORIGIN, is not a weakening. 4) Do not flag code that does not touch framing protection.",
|
|
76
|
+
auto_index_exposure: "Directory Listing Enabled: The change turns on automatic directory listings, exposing files and the internal structure. Detection steps: 1) Look for web server configuration (nginx autoindex on, Apache Options +Indexes) and Rack or Rails code that serves directory listings (Rack::Directory, a file browser over a public path). 2) Flag enabling listings for a location that holds uploads, exports, backups, or other non-public files. 3) Turning listings off (autoindex off, Options -Indexes) or serving files through an internal location is not a problem.",
|
|
77
|
+
inappropriate_password_policy: "Weakened Password Policy: The change makes password requirements weaker, for example a shorter minimum length, removing complexity or breached-password checks, lowering the hashing cost, or allowing a password change without the current password. Detection steps: 1) Look for changes to password validations, Devise configuration (password_length, stretches), and password change actions. 2) Flag any requirement that becomes weaker than before. 3) Making requirements stronger, or refactoring them without weakening, is not a problem. 4) Do not flag code that does not touch passwords.",
|
|
78
|
+
two_factor_auth_missing: "Bypassed Two-Factor Authentication: The change removes or bypasses an existing second-factor step, or adds a sensitive action (sign-in, payouts, account or security settings, admin operations) that skips the two-factor check the other sensitive actions have. Detection steps: 1) Look for changes to sign-in flows and to before_action filters that require a second factor. 2) Flag removing the second-factor step, excluding a sensitive action from the filter, or letting users in before the second factor is verified. 3) Refactoring that keeps the second-factor step is not a problem. 4) Do not flag code that has nothing to do with authentication or sensitive actions: whether the application offers two-factor authentication at all cannot be judged from such a change.",
|
|
75
79
|
race_condition: "Race Condition: Concurrent access without proper locking can lead to inconsistent states or privilege escalation. Detection steps: 1) Identify code accessing shared resources (database records, files, cache). 2) Find sections where multiple threads/processes might read/write concurrently. 3) Look for non-atomic check-then-act patterns. 4) Verify use of proper locking mechanisms (database transactions, Mutex, file locks). 5) Focus on critical operations like balance updates, inventory control.",
|
|
76
|
-
server_error_information_exposure: "Server Error Information Exposure: Internal errors (e.g., 500) reveal stack traces or server information in responses. Detection steps: 1) Examine error handling for server-side errors (e.g., HTTP 500). 2) Check the content of error responses sent to the client in production. 3) Verify that server-specific information (software type/version, OS details) is not included. 4) Check server configuration to suppress revealing headers (e.g., `Server` header; Nginx `server_tokens off;`). 5) Ensure detailed errors are logged only, with generic messages shown to users. (Similar to `overly_detailed_errors` but focusing on server info).",
|
|
77
|
-
dependency_trojan_package: "Dependency Trojan Package Risk: Installation of malicious or typosquatted packages from untrusted sources. Detection steps: 1) Review dependency files (`Gemfile`, `Gemfile.lock`). 2) Scan the list of Gems for typosquatting or unfamiliar names. 3) Check Gem sources (avoid untrusted Git repos if possible). 4) Investigate reputation and maintenance status of less-known Gems. 5) Ensure `Gemfile.lock` is committed to version control. 6) Check if dependency scanning tools (e.g., `bundler-audit`) are used.",
|
|
78
80
|
api_overexposure: "Excessive API Exposure: Public APIs exposed without authentication, leading to data leakage or unauthorized access. Detection steps: 1) Identify all API endpoint definitions. 2) Verify that appropriate authentication and authorization checks are applied to each endpoint. 3) Check if rate limiting or access controls are in place, even for public APIs. 4) Ensure sensitive data or internal-only information is not returned by unauthenticated endpoints. 5) Review API documentation for unintended exposure.",
|
|
79
81
|
security_middleware_disabled: "Security Middleware Disabled: Important protections (e.g., CSRF tokens, input sanitization) are turned off or removed. Detection steps: 1) Review framework initialization and configuration files. 2) Look for intentional disabling or removal of standard security middleware (e.g., CSRF protection, secure session handling). 3) Check for route-specific or controller-specific disabling of security features (e.g., `skip_before_action :verify_authenticity_token`) and evaluate the justification.",
|
|
80
|
-
security_header_inconsistency: "Security
|
|
82
|
+
security_header_inconsistency: "Inconsistent Security Headers: The change makes security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy) weaker in production than elsewhere, or removes them for particular controllers or routes. Detection steps: 1) Look for header settings that differ by environment, and for controllers or routes that delete or override headers. 2) Flag production being weaker than other environments, or a group of pages (for example billing or embeds) losing headers the rest of the application has. 3) Applying the same headers to every environment and route is not a problem. 4) Do not flag code that does not touch security headers.",
|
|
81
83
|
excessive_login_attempts: "Excessive Login Attempts Allowed: Lack of rate limiting allows brute-force login attempts. Detection steps: 1) Identify the login authentication code. 2) Check for logic that counts and limits login attempts per account and/or IP address within a time window. 3) Verify that countermeasures (account lockout, CAPTCHA, delay) are triggered upon exceeding the limit. 4) Check configuration of rate-limiting libraries (e.g., `rack-attack`). 5) Ensure similar limits exist for password reset functions.",
|
|
82
84
|
inappropriate_cache_settings: "Insecure Cache Settings: Sensitive pages are cached publicly (e.g., with Cache-Control: public), risking data leakage. Detection steps: 1) Identify code setting `Cache-Control` or `Pragma` headers. 2) Locate code generating pages containing user-specific or sensitive data. 3) Ensure these pages have restrictive cache directives (`private`, `no-store`, `no-cache`). 4) Look for inappropriate use of `public` or long `max-age`. 5) Review framework caching (page, action, fragment) usage for sensitive content. 6) Consider cache settings in reverse proxies/CDNs.",
|
|
83
85
|
secret_key_committed: "Secret Key Committed to Repository: Credentials, JWT secrets, or API keys are hardcoded or pushed to version control. Detection steps: 1) Scan codebase for hardcoded secrets (passwords, API keys, tokens). 2) Review configuration files (`database.yml`, `secrets.yml`, `.env`) for secrets and ensure they are gitignored if present. 3) Verify secrets are loaded from environment variables or dedicated secret management systems. 4) Scan version control history for accidentally committed secrets (and rotate if found). 5) Consider using secret scanning tools (e.g., `truffleHog`, `gitleaks`).",
|
|
84
86
|
third_party_script_validation_missing: 'Missing Validation for Third-Party Scripts: External scripts are loaded without integrity checks (e.g., Subresource Integrity). Detection steps: 1) Find HTML templates loading external JS/CSS (`<script src="...">`, `<link href="...">`). 2) Check if these tags include the `integrity` attribute (for SRI). 3) Report as risk if `integrity` attribute is missing or empty. 4) Verify the `crossorigin="anonymous"` attribute is also present when using SRI. 5) Check dynamically loaded scripts for similar integrity validation mechanisms.',
|
|
85
87
|
over_logging: "Over-Logging: Logging sensitive information such as passwords, tokens, or personal data. Detection steps: 1) Identify all logging statements (`Rails.logger.*`, `puts`, etc.). 2) Examine the data being logged. 3) Check specifically for sensitive data like passwords, tokens, API keys, PII, credit card numbers. 4) Pay attention to logging of entire request parameters or exception objects. 5) Verify framework parameter filtering (e.g., Rails `config.filter_parameters`) is configured correctly to mask sensitive fields.",
|
|
86
88
|
fail_open_design: "Fail-Open Design: On error or exception, access is granted instead of safely denied. Detection steps: 1) Identify security check code (authentication, authorization, access control). 2) Examine behavior within exception handling blocks (`rescue`). 3) Verify that exceptions during security checks default to denying access (fail-close/fail-safe), not granting it. 4) Look for checks where an error state might be misinterpreted as permissive. 5) Ensure critical operations use transactions and roll back safely on error.",
|
|
87
|
-
environment_differences: "
|
|
88
|
-
audit_log_missing: "
|
|
89
|
+
environment_differences: "Weakened Production Configuration: The change loosens security settings in the production environment, such as turning off force_ssl or assume_ssl, clearing allowed hosts, enabling detailed error pages (consider_all_requests_local = true), enabling debug logging of request data, or disabling protections that the other environments keep. Detection steps: 1) Look for changes to config/environments/production.rb and other production-only configuration. 2) Flag settings in production that become weaker than before. 3) Loosening settings only in development or test is not a problem. 4) Adding unrelated production settings (mailer, queue, cache) while keeping the security settings is not a problem.",
|
|
90
|
+
audit_log_missing: "Removed Audit Logging: The change removes audit logging from security-relevant actions (sign-in, permission or role changes, API key creation or revocation, access to sensitive data), or adds such an action without the audit record that similar actions in the same code write. Detection steps: 1) Look for audit or security event records (AuditLog, SecurityEvent, PaperTrail, logging calls) in the changed actions and their neighbours. 2) Flag records that are removed, or a new sensitive action that does not record what its sibling actions record. 3) Moving the logging to another place (a service object, a callback, the controller) while keeping it is not a problem. 4) Do not flag code that has nothing to do with security-relevant actions.",
|
|
89
91
|
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
92
|
}.freeze
|
|
91
|
-
|
|
93
|
+
|
|
92
94
|
|
|
93
95
|
def initialize(config = {})
|
|
94
96
|
# Load custom templates and language from config, merge with default
|
|
@@ -98,12 +100,87 @@ dangerous_eval_prompt = <<~PROMPT
|
|
|
98
100
|
@language = config.get("language", "en") # Get language from config, default to 'en'
|
|
99
101
|
end
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
# Builds a yes/no question for decision models (Jev, OpenAI Decisions).
|
|
104
|
+
# The full risk description is included so the classifier sees the same detection steps as the LLM.
|
|
105
|
+
def build_question(risk_key)
|
|
106
|
+
<<~TEXT
|
|
107
|
+
Does the following code contain the security vulnerability described below? #{DIFF_NOTE}
|
|
108
|
+
|
|
109
|
+
#{@risk_prompts.fetch(risk_key)}
|
|
110
|
+
TEXT
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Real vulnerabilities often come from removing a protection, so removed lines matter too
|
|
114
|
+
# Former risk names that now belong to another risk
|
|
115
|
+
RISK_ALIASES = { server_error_information_exposure: :overly_detailed_errors }.freeze
|
|
116
|
+
# Former risks that are better left to other tools, with what to use instead
|
|
117
|
+
RETIRED_RISKS = {
|
|
118
|
+
dependency_trojan_package: 'Malicious or typosquatted packages cannot be judged reliably from code. ' \
|
|
119
|
+
'Use bundler-audit (run by omamori) for known vulnerable versions, and a ' \
|
|
120
|
+
'malicious-package scanner such as OSV-Scanner or GitHub dependency review.'
|
|
121
|
+
}.freeze
|
|
122
|
+
|
|
123
|
+
DIFF_NOTE = 'The code may be a git diff, where lines starting with + are added and lines starting with - are ' \
|
|
124
|
+
'removed. A change can introduce the vulnerability by adding unsafe code, or by removing or ' \
|
|
125
|
+
'weakening a protection that was there before (a check, an escape, a filter or allowlist, an ' \
|
|
126
|
+
'authorization or authentication step, or a security setting). Judge the code as it is after the change.'
|
|
127
|
+
SUB_CHECKS_DIR = File.expand_path('sub_checks', __dir__)
|
|
128
|
+
SUB_CHECK_SEPARATOR = '__'
|
|
129
|
+
|
|
130
|
+
# The overall question for a risk plus one question per sub-check (a concrete pattern that alone makes the
|
|
131
|
+
# risk real). Returns { question_name => text }; sub-check names look like :xss__raw_output.
|
|
132
|
+
def build_questions(risk_key, sub_checks: true)
|
|
133
|
+
questions = { risk_key => build_question(risk_key) }
|
|
134
|
+
return questions unless sub_checks
|
|
135
|
+
|
|
136
|
+
sub_checks_for(risk_key).each do |name, text|
|
|
137
|
+
questions[:"#{risk_key}#{SUB_CHECK_SEPARATOR}#{name}"] = "#{text} #{DIFF_NOTE}"
|
|
138
|
+
end
|
|
139
|
+
questions
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The risk a question name belongs to (:xss__raw_output -> :xss)
|
|
143
|
+
def self.risk_of(question_name)
|
|
144
|
+
question_name.to_s.split(SUB_CHECK_SEPARATOR, 2).first.to_sym
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def sub_check_text(question_name)
|
|
148
|
+
risk, name = question_name.to_s.split(SUB_CHECK_SEPARATOR, 2)
|
|
149
|
+
name && sub_checks_for(risk.to_sym)[name]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def sub_checks_for(risk_key)
|
|
153
|
+
@sub_checks ||= {}
|
|
154
|
+
@sub_checks[risk_key] ||= begin
|
|
155
|
+
path = File.join(SUB_CHECKS_DIR, "#{risk_key}.yml")
|
|
156
|
+
File.exist?(path) ? YAML.safe_load_file(path) : {}
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Updated to accept file_path keyword argument
|
|
161
|
+
def build_prompt(code_content, risks_to_check, json_schema, template_key: :default, file_path: nil)
|
|
103
162
|
template = @prompt_templates.fetch(template_key, @prompt_templates[:default])
|
|
104
163
|
risk_list = risks_to_check.map { |risk_key| @risk_prompts[risk_key] }.compact.join(", ")
|
|
105
164
|
|
|
106
|
-
|
|
165
|
+
prompt_variables = {
|
|
166
|
+
risk_list: risk_list,
|
|
167
|
+
code_content: code_content,
|
|
168
|
+
json_schema: json_schema.to_json,
|
|
169
|
+
language: @language
|
|
170
|
+
}
|
|
171
|
+
# Add file_path to variables if provided and template expects it
|
|
172
|
+
# Ensure your template string (DEFAULT_PROMPT_TEMPLATE or custom ones)
|
|
173
|
+
# actually uses %{file_path} if you want to include it.
|
|
174
|
+
prompt_variables[:file_path] = file_path if file_path && template.include?("%{file_path}")
|
|
175
|
+
|
|
176
|
+
# Handle cases where a key in prompt_variables might not be in the template string
|
|
177
|
+
# by selecting only keys present in the template.
|
|
178
|
+
final_variables = {}
|
|
179
|
+
template.scan(/%\{(\w+)\}/).flatten.uniq.each do |key_in_template|
|
|
180
|
+
final_variables[key_in_template.to_sym] = prompt_variables[key_in_template.to_sym]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
template % final_variables
|
|
107
184
|
end
|
|
108
185
|
end
|
|
109
186
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_llm'
|
|
4
|
+
|
|
5
|
+
module Omamori
|
|
6
|
+
module AIAnalysisEngine
|
|
7
|
+
# Builds a RubyLLM context whose credentials come from each provider's usual environment variable
|
|
8
|
+
# (GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, TYPESAFE_API_KEY, ...), overridden by `overrides`.
|
|
9
|
+
module RubyLLMContext
|
|
10
|
+
def self.build(overrides = {})
|
|
11
|
+
RubyLLM.context do |config|
|
|
12
|
+
config.public_methods.grep(/_api_(?:key|base)=\z/).each do |setter|
|
|
13
|
+
value = ENV.fetch(setter.to_s.delete_suffix('=').upcase, nil)
|
|
14
|
+
config.public_send(setter, value) if value
|
|
15
|
+
end
|
|
16
|
+
overrides.compact.each do |name, value|
|
|
17
|
+
config.public_send("#{name}=", value) if config.respond_to?("#{name}=")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Concrete patterns that are each enough to make missing audit logging real on their own.
|
|
2
|
+
# Each question asks for the pattern AND a missing defense, so that code which uses the same construct safely is a no.
|
|
3
|
+
record_removed: >-
|
|
4
|
+
Does the change delete an audit or security event record (AuditLog, SecurityEvent, PaperTrail, or a logging call
|
|
5
|
+
that records who did what) from an action such as sign-in, a role or permission change, API key creation or
|
|
6
|
+
revocation, or access to sensitive data? Answer no when the same record is still written somewhere else, for
|
|
7
|
+
example moved into a service object, a callback, or the controller.
|
|
8
|
+
new_action_without_record: >-
|
|
9
|
+
Does the change add a security-relevant action (a role or permission change, granting or revoking access, creating
|
|
10
|
+
or revoking credentials, exporting or deleting data) next to existing actions that write an audit or security event
|
|
11
|
+
record, while the new action writes none? Answer no when the new action records the same kind of event as its
|
|
12
|
+
neighbours.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Concrete patterns that are each enough to make CSRF real on their own.
|
|
2
|
+
# Each question asks for the pattern AND a missing defense, so that code which uses the same construct safely is a no.
|
|
3
|
+
state_change_on_get: >-
|
|
4
|
+
Does the code make an action that changes data or account state (create, update, delete, cancel, sign out,
|
|
5
|
+
turn a setting on or off, send something) reachable with a GET request, through a get route, match ... via: :all,
|
|
6
|
+
or a link that triggers it without a form? Answer no when such actions are only routed to POST, PATCH, PUT, or
|
|
7
|
+
DELETE and triggered with a form or button_to.
|
|
8
|
+
token_check_skipped: >-
|
|
9
|
+
Does a controller that authenticates users with the session cookie skip or disable the authenticity token check
|
|
10
|
+
for actions that change data (skip_before_action :verify_authenticity_token, skip_forgery_protection,
|
|
11
|
+
protect_from_forgery with except: or an if: condition such as JSON format, or allow_forgery_protection = false
|
|
12
|
+
outside the test environment)? Answer no when the skipped actions do not use cookies to authenticate (for
|
|
13
|
+
example ActionController::API with a bearer token, or a webhook that verifies a signature), or when the setting
|
|
14
|
+
is only for the test environment.
|
|
15
|
+
weak_origin_check: >-
|
|
16
|
+
Does the code replace or relax the token check with a hand-written Origin or Referer check that matches by
|
|
17
|
+
substring or prefix (include?, start_with?, a regular expression without anchors)? Answer no when the token check
|
|
18
|
+
is still in place, or the parsed origin host is compared exactly against a fixed list.
|
|
19
|
+
cross_site_cookie: >-
|
|
20
|
+
Does the code make the session cookie be sent on cross-site requests (same_site: :none) while state-changing
|
|
21
|
+
actions that rely on that session skip the token check? Answer no when those actions still verify the
|
|
22
|
+
authenticity token.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Concrete patterns that are each enough to make insecure deserialization real on their own.
|
|
2
|
+
# Each question asks for the pattern AND a missing defense, so that code which uses the same construct safely is a no.
|
|
3
|
+
object_creating_loader: >-
|
|
4
|
+
Does the code load data that may come from outside the application (request parameters or body, cookies, uploaded
|
|
5
|
+
files, data from another service, or cache or database values users can influence) with a loader that can create
|
|
6
|
+
arbitrary Ruby objects: Marshal.load, YAML.unsafe_load, Psych.unsafe_load, YAML.load on Psych versions before 4,
|
|
7
|
+
Oj.load in object mode or with its default mode, or JSON.load with object creation enabled? Answer no when it uses
|
|
8
|
+
JSON.parse, YAML.safe_load with a short list of permitted classes, Oj in strict or compat mode, or when the data is
|
|
9
|
+
produced and stored only by the application itself.
|