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.
@@ -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