From 99bda3922835b9a5058cbb3625b2869390d4181f Mon Sep 17 00:00:00 2001 From: Vasu1105 Date: Tue, 26 Sep 2023 17:24:05 +0530 Subject: [PATCH] Integrate audit log functionality - initial commit Signed-off-by: Vasu1105 --- lib/train/audit_log.rb | 29 ++++++++++++++++++++++++++++ lib/train/options.rb | 14 ++++++++++++++ lib/train/plugins/base_connection.rb | 17 +++++++++++++--- 3 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 lib/train/audit_log.rb diff --git a/lib/train/audit_log.rb b/lib/train/audit_log.rb new file mode 100644 index 00000000..2fc671c9 --- /dev/null +++ b/lib/train/audit_log.rb @@ -0,0 +1,29 @@ +module Train + class AuditLog + # TODO: + # Configurable values log location, log file size, log frequency((available options, daily, weeks, monthly)) + + # Default values for audit log options are set in the options.rb + def self.create(options = {}) + logger = Logger.new(options[:audit_log_location], options[:audit_log_frequency], options[:audit_log_size]) + logger.level = options[:level] || Logger::INFO + logger.progname = options[:audi_log_app_name] + logger.datetime_format = "%Y-%m-%d %H:%M:%S" + logger.formatter = proc do |severity, datetime, progname, msg| + { + level: severity, + timestamp: datetime.to_s, + app: progname, + type: msg[:type], + command: msg[:command], + path: msg[:path], + source: msg[:source], + destination: msg[:destination], + hostname: msg[:hostname], + user: msg[:user], + }.compact.to_json + $/ + end + logger + end + end +end \ No newline at end of file diff --git a/lib/train/options.rb b/lib/train/options.rb index 210abad5..18e358ca 100644 --- a/lib/train/options.rb +++ b/lib/train/options.rb @@ -31,9 +31,23 @@ def option(name, conf = nil, &block) def default_options @default_options = {} unless defined? @default_options + # TODO: This is hacky way to set the default options for audit log for all type of transport. + @default_options.merge!(default_audit_log_options) @default_options end + def default_audit_log_options + # TODO: What should be the default audit log location if any of the application using train does not set it? + # should we keep it to $stdout. + { + enable_audit_log: { default: false }, + audit_log_location: { default: $stdout }, + audit_log_app_name: { default: "train" }, + audit_log_size: { default: 2000000 }, + audit_log_frequency: { default: "daily" }, + } + end + def include_options(other) unless other.respond_to?(:default_options) raise "Trying to include options from module #{other.inspect}, "\ diff --git a/lib/train/plugins/base_connection.rb b/lib/train/plugins/base_connection.rb index e690a124..28c653c2 100644 --- a/lib/train/plugins/base_connection.rb +++ b/lib/train/plugins/base_connection.rb @@ -3,6 +3,7 @@ require_relative "../file" require "fileutils" unless defined?(FileUtils) require "logger" +require_relative "../audit_log" class Train::Plugins::Transport # A Connection instance can be generated and re-generated, given new @@ -20,10 +21,17 @@ class BaseConnection # @yield [self] yields itself for block-style invocation def initialize(options = nil) @options = options || {} - @logger = @options.delete(:logger) || Logger.new($stdout, level: :fatal) + Train::Platforms::Detect::Specifications::OS.load Train::Platforms::Detect::Specifications::Api.load + @logger = @options.delete(:logger) || Logger.new($stdout, level: :fatal) + + # In run_command all options are not accessible as some of them gets deleted in transit. + # To make the data like hostname, username available to aduit logs dup the options + @audit_log_data = options.dup || {} + @audit_log = Train::AuditLog.create(options) if @options[:enable_audit_log] + # default caching options @cache_enabled = { file: true, @@ -140,11 +148,14 @@ def run_command(cmd, opts = {}, &data_handler) # Some implementations do not accept an opts argument. # We cannot update all implementations to accept opts due to them being separate plugins. # Therefore here we check the implementation's arity to maintain compatibility. + @audit_log.info({ type: "cmd", command: "#{cmd}", user: "#{@audit_log_data[:username]}", hostname: "#{@audit_log_data[:hostname]}" }) if @audit_log + case method(:run_command_via_connection).arity.abs when 1 return run_command_via_connection(cmd, &data_handler) unless cache_enabled?(:command) @cache[:command][cmd] ||= run_command_via_connection(cmd, &data_handler) + when 2 return run_command_via_connection(cmd, opts, &data_handler) unless cache_enabled?(:command) @@ -157,6 +168,7 @@ def run_command(cmd, opts = {}, &data_handler) # This is the main file call for all connections. This will call the private # file_via_connection on the connection with optional caching def file(path, *args) + @audit_log.info({ type: "file", path: "#{path}", user: "#{@audit_log_data[:username]}", hostname: "#{@audit_log_data[:hostname]}" }) if @audit_log return file_via_connection(path, *args) unless cache_enabled?(:file) @cache[:file][path] ||= file_via_connection(path, *args) @@ -177,7 +189,7 @@ def upload(locals, remote) Array(locals).each do |local| remote_file = remote_directory ? File.join(remote, File.basename(local)) : remote - + @audit_log.info({ type: "file upload", source: "#{local}", destination: "#{remote_file}", user: "#{@audit_log_data[:username]}", hostname: "#{@audit_log_data[:hostname]}" }) if @audit_log logger.debug("Attempting to upload '#{local}' as file #{remote_file}") file(remote_file).content = File.read(local) @@ -198,7 +210,6 @@ def download(remotes, local) Array(remotes).each do |remote| new_content = file(remote).content local_file = File.join(local, File.basename(remote)) - logger.debug("Attempting to download '#{remote}' as file #{local_file}") File.open(local_file, "w") { |fp| fp.write(new_content) }