# SECURE: Ruby script without backtick injection
require 'sinatra'
require 'json'
require 'open3'
# SECURE: Log analyzer with validation
class SecureLogAnalyzer
ALLOWED_LOG_TYPES = %w[system app error].freeze
LOG_FILES = {
'system' => '/var/log/syslog',
'app' => '/var/log/app.log',
'error' => '/var/log/error.log'
}.freeze
def analyze_logs(log_type, filter, lines)
# Validate inputs
raise ArgumentError, 'Invalid log type' unless ALLOWED_LOG_TYPES.include?(log_type)
raise ArgumentError, 'Invalid filter' unless valid_filter?(filter)
raise ArgumentError, 'Invalid lines count' unless valid_lines?(lines)
log_file = LOG_FILES[log_type]
lines_num = lines.to_i
# SECURE: Use File operations instead of shell commands
read_log_file(log_file, filter, lines_num)
end
private
def read_log_file(file_path, filter, lines_count)
return 'Log file not found' unless File.exist?(file_path)
matching_lines = []
# Read file in reverse to get recent entries
File.foreach(file_path).reverse_each do |line|
if filter.empty? || line.include?(filter)
matching_lines << line.chomp
break if matching_lines.length >= lines_count
end
end
matching_lines.reverse.join("\n")
rescue => e
"Error reading log: #{e.message}"
end
def valid_filter?(filter)
# Allow alphanumeric, spaces, and basic punctuation
filter.match?(/\A[a-zA-Z0-9\s._-]*\z/) && filter.length <= 100
end
def valid_lines?(lines)
lines.match?(/\A\d+\z/) && lines.to_i.between?(1, 1000)
end
end
# SECURE: File operations without shell commands
class SecureFileOperations
ALLOWED_OPERATIONS = %w[info size checksum].freeze
SAFE_DIRECTORIES = ['/safe/uploads', '/safe/documents'].freeze
def execute_operation(operation, filename)
raise ArgumentError, 'Operation not allowed' unless ALLOWED_OPERATIONS.include?(operation)
file_path = validate_file_path(filename)
case operation
when 'info'
get_file_info(file_path)
when 'size'
get_file_size(file_path)
when 'checksum'
calculate_checksum(file_path)
end
end
private
def validate_file_path(filename)
# Strict filename validation
raise ArgumentError, 'Invalid filename' unless filename.match?(/\A[a-zA-Z0-9._-]+\z/)
raise ArgumentError, 'Filename too long' if filename.length > 255
# Find in safe directories
SAFE_DIRECTORIES.each do |dir|
candidate_path = File.join(dir, filename)
resolved_path = File.expand_path(candidate_path)
next unless resolved_path.start_with?(File.expand_path(dir))
return resolved_path if File.exist?(resolved_path) && File.file?(resolved_path)
end
raise ArgumentError, 'File not found in safe directories'
end
def get_file_info(file_path)
stat = File.stat(file_path)
{
name: File.basename(file_path),
size: stat.size,
modified: stat.mtime.iso8601,
permissions: sprintf('%o', stat.mode)[-3..-1]
}
end
def get_file_size(file_path)
size = File.size(file_path)
{ size_bytes: size, size_human: format_size(size) }
end
def calculate_checksum(file_path)
require 'digest'
File.open(file_path, 'rb') do |file|
content = file.read
{
md5: Digest::MD5.hexdigest(content),
sha256: Digest::SHA256.hexdigest(content)
}
end
end
def format_size(size)
units = %w[B KB MB GB]
unit_index = 0
while size >= 1024 && unit_index < units.length - 1
size /= 1024.0
unit_index += 1
end
"#{size.round(2)} #{units[unit_index]}"
end
end
# Initialize secure components
log_analyzer = SecureLogAnalyzer.new
file_operations = SecureFileOperations.new
# Rate limiting
class RateLimiter
def initialize
@requests = Hash.new { |h, k| h[k] = [] }
end
def allowed?(ip, limit = 10, window = 60)
now = Time.now
@requests[ip].reject! { |time| now - time > window }
if @requests[ip].length < limit
@requests[ip] << now
true
else
false
end
end
end
rate_limiter = RateLimiter.new
# Middleware
before do
client_ip = request.env['HTTP_X_FORWARDED_FOR'] || request.ip
halt 429, { error: 'Rate limit exceeded' }.to_json unless rate_limiter.allowed?(client_ip)
end
# SECURE: Log analysis endpoint
get '/logs/:type' do
content_type :json
begin
log_type = params[:type]
filter = params[:filter] || ''
lines = params[:lines] || '10'
output = log_analyzer.analyze_logs(log_type, filter, lines)
{
status: 'success',
log_type: log_type,
output: output
}.to_json
rescue ArgumentError => e
halt 400, { status: 'error', message: e.message }.to_json
rescue => e
halt 500, { status: 'error', message: 'Analysis failed' }.to_json
end
end
# SECURE: File operations endpoint
post '/file-ops' do
content_type :json
begin
data = JSON.parse(request.body.read)
operation = data['operation']
filename = data['filename']
result = file_operations.execute_operation(operation, filename)
{
status: 'success',
operation: operation,
result: result
}.to_json
rescue JSON::ParserError
halt 400, { status: 'error', message: 'Invalid JSON' }.to_json
rescue ArgumentError => e
halt 400, { status: 'error', message: e.message }.to_json
rescue => e
halt 500, { status: 'error', message: 'Operation failed' }.to_json
end
end
# SECURE: System status (limited, safe operations)
get '/system/status' do
content_type :json
# Only provide safe, pre-computed system information
{
status: 'operational',
timestamp: Time.now.iso8601,
uptime: File.read('/proc/uptime').split.first.to_f rescue 0,
load_average: File.read('/proc/loadavg').split[0..2] rescue [],
ruby_version: RUBY_VERSION
}.to_json
end
# Health check
get '/health' do
content_type :json
{ status: 'healthy' }.to_json
end