omamori 0.1.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 +7 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +97 -0
- data/LICENSE +21 -0
- data/README.md +172 -0
- data/README_ja.md +175 -0
- data/demo/ai_analysis_vulnerability.rb +28 -0
- data/demo/csrf_vulnerability.rb +31 -0
- data/demo/eval_vulnerability.rb +29 -0
- data/demo/idor_vulnerability.rb +39 -0
- data/demo/insecure_cookie_vulnerability.rb +25 -0
- data/demo/open_redirect_vulnerability.rb +22 -0
- data/demo/static_analysis_vulnerability.rb +18 -0
- data/demo/xss_vulnerability.rb +21 -0
- data/exe/omamori +16 -0
- data/lib/omamori/ai_analysis_engine/diff_splitter.rb +62 -0
- data/lib/omamori/ai_analysis_engine/gemini_client.rb +70 -0
- data/lib/omamori/ai_analysis_engine/prompt_manager.rb +76 -0
- data/lib/omamori/config.rb +144 -0
- data/lib/omamori/core_runner.rb +431 -0
- data/lib/omamori/report_generator/console_formatter.rb +132 -0
- data/lib/omamori/report_generator/html_formatter.rb +31 -0
- data/lib/omamori/report_generator/json_formatter.rb +19 -0
- data/lib/omamori/report_generator/report_template.erb +104 -0
- data/lib/omamori/static_analysers/brakeman_runner.rb +51 -0
- data/lib/omamori/static_analysers/bundler_audit_runner.rb +53 -0
- data/lib/omamori/version.rb +5 -0
- data/lib/omamori.rb +16 -0
- metadata +172 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sinatra'
|
4
|
+
|
5
|
+
# XSS (Cross-Site Scripting) の脆弱性を含むデモコード
|
6
|
+
# ユーザーからの入力を適切にエスケープせずにHTMLに出力している例
|
7
|
+
|
8
|
+
get '/greet' do
|
9
|
+
# ユーザーからの入力 (name) を取得
|
10
|
+
name = params['name']
|
11
|
+
|
12
|
+
# 取得した入力をそのままHTMLに埋め込んで出力
|
13
|
+
# ここにXSSの脆弱性がある
|
14
|
+
"<h1>Hello, #{name}!</h1>"
|
15
|
+
end
|
16
|
+
|
17
|
+
# 実行方法:
|
18
|
+
# 1. このファイルを保存
|
19
|
+
# 2. ターミナルで `ruby demo/xss_vulnerability.rb` を実行
|
20
|
+
# 3. ブラウザで `http://localhost:4567/greet?name=<script>alert('XSS')</script>` にアクセス
|
21
|
+
# アラートが表示されれば脆弱性がある
|
data/exe/omamori
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require "omamori"
|
6
|
+
|
7
|
+
# Main entry point for the omamori CLI
|
8
|
+
module Omamori
|
9
|
+
class CLI
|
10
|
+
def self.start(args)
|
11
|
+
CoreRunner.new(args).run
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Omamori::CLI.start(ARGV)
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Omamori
|
4
|
+
module AIAnalysisEngine
|
5
|
+
class DiffSplitter
|
6
|
+
# TODO: Determine appropriate chunk size based on token limits
|
7
|
+
# Gemini 1.5 Pro has a large context window (1 million tokens),
|
8
|
+
# but splitting might still be necessary for very large inputs
|
9
|
+
# or to manage cost/latency.
|
10
|
+
DEFAULT_CHUNK_SIZE = 8000 # Characters as a proxy for tokens
|
11
|
+
|
12
|
+
def initialize(chunk_size: DEFAULT_CHUNK_SIZE)
|
13
|
+
@chunk_size = chunk_size
|
14
|
+
end
|
15
|
+
|
16
|
+
def split(content)
|
17
|
+
chunks = []
|
18
|
+
current_chunk = ""
|
19
|
+
content.each_line do |line|
|
20
|
+
if (current_chunk.length + line.length) > @chunk_size
|
21
|
+
chunks << current_chunk unless current_chunk.empty?
|
22
|
+
current_chunk = line
|
23
|
+
else
|
24
|
+
current_chunk += line
|
25
|
+
end
|
26
|
+
end
|
27
|
+
chunks << current_chunk unless current_chunk.empty?
|
28
|
+
chunks
|
29
|
+
end
|
30
|
+
|
31
|
+
def process_in_chunks(content, gemini_client, json_schema, prompt_manager, risks_to_check, model: "gemini-1.5-pro-latest")
|
32
|
+
all_results = []
|
33
|
+
chunks = split(content)
|
34
|
+
|
35
|
+
puts "Splitting content into #{chunks.size} chunks..."
|
36
|
+
|
37
|
+
chunks.each_with_index do |chunk, index|
|
38
|
+
puts "Processing chunk #{index + 1}/#{chunks.size}..."
|
39
|
+
prompt = prompt_manager.build_prompt(chunk, risks_to_check, json_schema)
|
40
|
+
result = gemini_client.analyze(prompt, json_schema, model: model)
|
41
|
+
all_results << result
|
42
|
+
# TODO: Handle potential rate limits or errors between chunks
|
43
|
+
end
|
44
|
+
|
45
|
+
# TODO: Combine results from all chunks
|
46
|
+
combine_results(all_results)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
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
|
+
combined_risks = results.flat_map do |result|
|
56
|
+
result && result["security_risks"] ? result["security_risks"] : []
|
57
|
+
end
|
58
|
+
{ "security_risks" => combined_risks }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gemini'
|
4
|
+
|
5
|
+
module Omamori
|
6
|
+
module AIAnalysisEngine
|
7
|
+
class GeminiClient
|
8
|
+
def initialize(api_key)
|
9
|
+
@api_key = api_key || ENV["GEMINI_API_KEY"]
|
10
|
+
@client = nil # Initialize client later
|
11
|
+
end
|
12
|
+
|
13
|
+
def analyze(prompt, json_schema, model: "gemini-1.5-pro-latest")
|
14
|
+
# Ensure the client is initialized
|
15
|
+
client
|
16
|
+
|
17
|
+
begin
|
18
|
+
response = @client.generate_content(
|
19
|
+
prompt,
|
20
|
+
model: model,
|
21
|
+
response_schema: json_schema # Use response_schema for Structured Output
|
22
|
+
)
|
23
|
+
|
24
|
+
# Debug: Inspect the response object
|
25
|
+
# puts "Debug: response object: #{response.inspect}"
|
26
|
+
# puts "Debug: response methods: #{response.methods.sort}"
|
27
|
+
|
28
|
+
# Extract and parse JSON from the response text
|
29
|
+
json_string = response.text.gsub(/\A```json\n|```\z/, '').strip
|
30
|
+
begin
|
31
|
+
parsed_response = JSON.parse(json_string)
|
32
|
+
# Validate the parsed output structure
|
33
|
+
if parsed_response.is_a?(Hash) && parsed_response.key?('security_risks')
|
34
|
+
# Return the parsed JSON data
|
35
|
+
parsed_response
|
36
|
+
else
|
37
|
+
puts "Warning: Unexpected AI analysis response structure."
|
38
|
+
puts "Raw response text: #{response.text}"
|
39
|
+
nil # Return nil if the structure is unexpected
|
40
|
+
end
|
41
|
+
rescue JSON::ParserError
|
42
|
+
puts "Warning: Failed to parse response text as JSON."
|
43
|
+
puts "Raw response text: #{response.text}"
|
44
|
+
nil # Or handle the error appropriately
|
45
|
+
end
|
46
|
+
rescue Faraday::Error => e
|
47
|
+
puts "API Error: #{e.message}"
|
48
|
+
puts "Response body: #{e.response[:body]}" if e.response
|
49
|
+
nil # Handle API errors
|
50
|
+
rescue => e
|
51
|
+
puts "An unexpected error occurred during API call: #{e.message}"
|
52
|
+
nil # Handle other errors
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def client
|
59
|
+
@client ||= begin
|
60
|
+
# Configure the client with the API key
|
61
|
+
Gemini.configure do |config|
|
62
|
+
config.api_key = @api_key
|
63
|
+
end
|
64
|
+
# Create a new client instance
|
65
|
+
Gemini::Client.new
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Omamori
|
4
|
+
module AIAnalysisEngine
|
5
|
+
class PromptManager
|
6
|
+
# TODO: Load prompt templates from config file
|
7
|
+
DEFAULT_PROMPT_TEMPLATE = <<~TEXT
|
8
|
+
You are a security expert specializing in Ruby. Analyze the following code and detect any potential security risks.
|
9
|
+
Focus particularly on identifying the following types of vulnerabilities: %{risk_list}
|
10
|
+
Report any detected risks in the format specified by the following JSON Schema:
|
11
|
+
%{json_schema}
|
12
|
+
If no risks are found, output an empty list for the "security_risks" array.
|
13
|
+
Please provide your response in %{language}.
|
14
|
+
|
15
|
+
【Code to Analyze】:
|
16
|
+
%{code_content}
|
17
|
+
TEXT
|
18
|
+
|
19
|
+
|
20
|
+
RISK_PROMPTS = {
|
21
|
+
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. Look for unsanitized input and missing output encoding.",
|
22
|
+
csrf: "Cross-Site Request Forgery (CSRF): An attack that forces an authenticated user to perform unwanted actions via forged requests. Detect missing CSRF tokens or absence of referer/origin validation.",
|
23
|
+
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.",
|
24
|
+
open_redirect: "Open Redirect: Redirecting users to external URLs based on user-supplied input without proper validation. Check for lack of domain or whitelist restrictions.",
|
25
|
+
ssrf: "Server-Side Request Forgery (SSRF): The server makes HTTP requests to an arbitrary destination supplied by the user, potentially exposing internal resources or metadata.",
|
26
|
+
session_fixation: "Session Fixation: The server accepts a pre-supplied session ID, allowing an attacker to hijack the session after authentication. Look for missing session ID regeneration after login.",
|
27
|
+
inappropriate_cookie_attributes: "Insecure Cookie Attributes: Missing HttpOnly, Secure, or SameSite flags, which may lead to session theft or CSRF.",
|
28
|
+
insufficient_encryption: "Insufficient Encryption: Use of weak algorithms (e.g., MD5, SHA1) or lack of encryption for sensitive data. Check for insecure hash functions or plain-text handling.",
|
29
|
+
insecure_deserialization_rce: "Insecure Deserialization leading to RCE: Deserializing untrusted data can lead to arbitrary code execution. Detect unsafe use of deserialization functions without validation.",
|
30
|
+
directory_traversal: "Directory Traversal: Allows attackers to access files outside the intended directory using ../ patterns. Check for path manipulation and missing canonicalization.",
|
31
|
+
dangerous_eval: "Dangerous Code Execution (eval, exec): Dynamic code execution using untrusted input, allowing arbitrary code injection.",
|
32
|
+
inappropriate_file_permissions: "Insecure File Permissions: Files or directories with overly permissive modes (e.g., 777), allowing unauthorized read/write/execute access.",
|
33
|
+
temporary_backup_file_leak: "Temporary or Backup File Exposure: Sensitive files like .bak, .tmp, or ~ versions are publicly accessible due to poor file handling.",
|
34
|
+
overly_detailed_errors: "Excessive Error Information Disclosure: Stack traces or internal error messages exposed to users, leaking implementation details.",
|
35
|
+
csp_not_set: "Missing Content Security Policy (CSP): Absence of CSP headers increases risk of XSS. Look for missing Content-Security-Policy header.",
|
36
|
+
mime_sniffing_vulnerability: "MIME Sniffing Vulnerability: Missing X-Content-Type-Options: nosniff header can allow browsers to misinterpret content types.",
|
37
|
+
clickjacking_vulnerability: "Clickjacking Protection Missing: Absence of X-Frame-Options or frame-ancestors directive allows malicious framing of pages.",
|
38
|
+
auto_index_exposure: "Auto Indexing Enabled: Directory listing is active, exposing files and internal structure to users.",
|
39
|
+
inappropriate_password_policy: "Weak Password Policy: Inadequate rules such as short length, lack of complexity, or missing brute-force protections.",
|
40
|
+
two_factor_auth_missing: "Missing Two-Factor Authentication (2FA): Lack of secondary authentication factor for sensitive operations.",
|
41
|
+
race_condition: "Race Condition: Concurrent access without proper locking can lead to inconsistent states or privilege escalation.",
|
42
|
+
server_error_information_exposure: "Server Error Information Exposure: Internal errors (e.g., 500) reveal stack traces or server information in responses.",
|
43
|
+
dependency_trojan_package: "Dependency Trojan Package Risk: Installation of malicious or typosquatted packages from untrusted sources.",
|
44
|
+
api_overexposure: "Excessive API Exposure: Public APIs exposed without authentication, leading to data leakage or unauthorized access.",
|
45
|
+
security_middleware_disabled: "Security Middleware Disabled: Important protections (e.g., CSRF tokens, input sanitization) are turned off or removed.",
|
46
|
+
security_header_inconsistency: "Security Header Inconsistency: Inconsistent or missing security headers across environments or routes.",
|
47
|
+
excessive_login_attempts: "Excessive Login Attempts Allowed: Lack of rate limiting allows brute-force login attempts.",
|
48
|
+
inappropriate_cache_settings: "Insecure Cache Settings: Sensitive pages are cached publicly (e.g., with Cache-Control: public), risking data leakage.",
|
49
|
+
secret_key_committed: "Secret Key Committed to Repository: Credentials, JWT secrets, or API keys are hardcoded or pushed to version control.",
|
50
|
+
third_party_script_validation_missing: "Missing Validation for Third-Party Scripts: External scripts are loaded without integrity checks (e.g., Subresource Integrity).",
|
51
|
+
over_logging: "Over-Logging: Logging sensitive information such as passwords, tokens, or personal data.",
|
52
|
+
fail_open_design: "Fail-Open Design: On error or exception, access is granted instead of safely denied.",
|
53
|
+
environment_differences: "Uncontrolled Environment Differences: Security settings differ between development and production without strict controls.",
|
54
|
+
audit_log_missing: "Missing Audit Logging: Lack of logging for critical actions or authorization checks prevents accountability.",
|
55
|
+
time_based_side_channel: "Time-Based Side Channel: Execution time differences can leak secrets (e.g., timing attacks in string comparison)."
|
56
|
+
}.freeze
|
57
|
+
|
58
|
+
|
59
|
+
def initialize(config = {})
|
60
|
+
# Load custom templates and language from config, merge with default
|
61
|
+
custom_templates = config.get("prompt_templates", {}) # Use get instead of fetch
|
62
|
+
@prompt_templates = { default: DEFAULT_PROMPT_TEMPLATE }.merge(custom_templates)
|
63
|
+
@risk_prompts = RISK_PROMPTS
|
64
|
+
@language = config.get("language", "en") # Get language from config, default to 'en'
|
65
|
+
end
|
66
|
+
|
67
|
+
def build_prompt(code_content, risks_to_check, json_schema, template_key: :default)
|
68
|
+
# Use the template from @prompt_templates, defaulting to :default if template_key is not found
|
69
|
+
template = @prompt_templates.fetch(template_key, @prompt_templates[:default])
|
70
|
+
risk_list = risks_to_check.map { |risk_key| @risk_prompts[risk_key] }.compact.join(", ")
|
71
|
+
|
72
|
+
template % { risk_list: risk_list, code_content: code_content, json_schema: json_schema.to_json, language: @language}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Omamori
|
6
|
+
class Config
|
7
|
+
DEFAULT_CONFIG_PATH = ".omamorirc"
|
8
|
+
|
9
|
+
def initialize(config_path = DEFAULT_CONFIG_PATH)
|
10
|
+
@config_path = config_path
|
11
|
+
@config = load_config
|
12
|
+
validate_config # Add validation after loading
|
13
|
+
end
|
14
|
+
|
15
|
+
def get(key, default = nil)
|
16
|
+
@config.fetch(key.to_s, default)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Add validation methods
|
20
|
+
private
|
21
|
+
|
22
|
+
def validate_config
|
23
|
+
validate_api_key
|
24
|
+
validate_model
|
25
|
+
validate_checks
|
26
|
+
validate_prompt_templates
|
27
|
+
validate_report_settings
|
28
|
+
validate_static_analyser_settings
|
29
|
+
validate_ci_setup_settings # Add CI setup validation
|
30
|
+
validate_language # Add language validation
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_api_key
|
34
|
+
api_key = @config["api_key"]
|
35
|
+
if api_key && !api_key.is_a?(String)
|
36
|
+
puts "Warning: Config 'api_key' should be a string."
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def validate_model
|
41
|
+
model = @config["model"]
|
42
|
+
if model && !model.is_a?(String)
|
43
|
+
puts "Warning: Config 'model' should be a string."
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate_checks
|
48
|
+
checks = @config["checks"]
|
49
|
+
if checks && !checks.is_a?(Array)
|
50
|
+
puts "Warning: Config 'checks' should be an array."
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def validate_prompt_templates
|
55
|
+
prompt_templates = @config["prompt_templates"]
|
56
|
+
if prompt_templates && !prompt_templates.is_a?(Hash)
|
57
|
+
puts "Warning: Config 'prompt_templates' should be a hash."
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def validate_report_settings
|
62
|
+
report = @config["report"]
|
63
|
+
if report
|
64
|
+
unless report.is_a?(Hash)
|
65
|
+
puts "Warning: Config 'report' should be a hash."
|
66
|
+
return
|
67
|
+
end
|
68
|
+
if report.key?("output_path") && !report["output_path"].is_a?(String)
|
69
|
+
puts "Warning: Config 'report.output_path' should be a string."
|
70
|
+
end
|
71
|
+
if report.key?("html_template") && !report["html_template"].is_a?(String)
|
72
|
+
puts "Warning: Config 'report.html_template' should be a string."
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def validate_static_analyser_settings
|
78
|
+
static_analysers = @config["static_analysers"]
|
79
|
+
if static_analysers
|
80
|
+
unless static_analysers.is_a?(Hash)
|
81
|
+
puts "Warning: Config 'static_analysers' should be a hash."
|
82
|
+
return
|
83
|
+
end
|
84
|
+
if static_analysers.key?("brakeman")
|
85
|
+
brakeman_config = static_analysers["brakeman"]
|
86
|
+
unless brakeman_config.is_a?(Hash)
|
87
|
+
puts "Warning: Config 'static_analysers.brakeman' should be a hash."
|
88
|
+
else
|
89
|
+
if brakeman_config.key?("options") && !brakeman_config["options"].is_a?(String)
|
90
|
+
puts "Warning: Config 'static_analysers.brakeman.options' should be a string."
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
if static_analysers.key?("bundler_audit")
|
95
|
+
bundler_audit_config = static_analysers["bundler_audit"]
|
96
|
+
unless bundler_audit_config.is_a?(Hash)
|
97
|
+
puts "Warning: Config 'static_analysers.bundler_audit' should be a hash."
|
98
|
+
else
|
99
|
+
if bundler_audit_config.key?("options") && !bundler_audit_config["options"].is_a?(String)
|
100
|
+
puts "Warning: Config 'static_analysers.bundler_audit.options' should be a string."
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
def validate_ci_setup_settings
|
109
|
+
ci_setup = @config["ci_setup"]
|
110
|
+
if ci_setup
|
111
|
+
unless ci_setup.is_a?(Hash)
|
112
|
+
puts "Warning: Config 'ci_setup' should be a hash."
|
113
|
+
return
|
114
|
+
end
|
115
|
+
if ci_setup.key?("github_actions_path") && !ci_setup["github_actions_path"].is_a?(String)
|
116
|
+
puts "Warning: Config 'ci_setup.github_actions_path' should be a string."
|
117
|
+
end
|
118
|
+
if ci_setup.key?("gitlab_ci_path") && !ci_setup["gitlab_ci_path"].is_a?(String)
|
119
|
+
puts "Warning: Config 'ci_setup.gitlab_ci_path' should be a string."
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def validate_language
|
125
|
+
language = @config["language"]
|
126
|
+
if language && !language.is_a?(String)
|
127
|
+
puts "Warning: Config 'language' should be a string."
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def load_config
|
132
|
+
if File.exist?(@config_path)
|
133
|
+
begin
|
134
|
+
YAML.load_file(@config_path) || {}
|
135
|
+
rescue Psych::SyntaxError => e
|
136
|
+
puts "Error parsing config file #{@config_path}: #{e.message}"
|
137
|
+
{}
|
138
|
+
end
|
139
|
+
else
|
140
|
+
{} # Return empty hash if config file does not exist
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|