Class: Kunoichi::Daemon
Overview
The main class, where all the dirty work happens
Instance Method Summary collapse
-
#clean_exit(sig) ⇒ Object
Exit cleanly by logging our departure and deleting the pid file if we received a signal.
-
#get_process_list ⇒ Object
Fetch the current running processes and put them into a ruby set TODO: make this multi platform.
-
#initialize(config) ⇒ Daemon
constructor
Daemon startup procedure.
-
#load_whitelist(file) ⇒ Object
Load the whitelist.
-
#main_loop ⇒ Object
Main loop.
-
#run_command(process, parent) ⇒ Object
Run the external command.
-
#validate_conf(config) ⇒ Object
Basic validator for configuration.
Constructor Details
#initialize(config) ⇒ Daemon
Daemon startup procedure
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/kunoichi/kunoichi.rb', line 135 def initialize(config) # Check max pids first if RUBY_PLATFORM.downcase =~ /linux/ begin @max_pids = File.read('/proc/sys/kernel/pid_max').to_i rescue raise 'Cannot read /proc/sys/kernel/pid_max (is /proc mounted?)' end else # Default on newer FreeBSD. Solaris should have a cap of 30000. # Makes only sense once get_process_list becomes multi-platform, but nice to have. @max_pids = 99999 end # Validate configuration and store if valid @config = validate_conf config # Save the full path to the pid file in @config @config['pidfile'] = File.(@config['pidfile']) # Get initial process list at startup @initial_procs = get_process_list raise 'No processes found (is /proc mounted?)' if @initial_procs.empty? # Load the whitelist load_whitelist @config['whitelist'] if @config['whitelist'] # Initialize @log as configured if @config['syslog'] @log = Logger::Syslog.new('kunoichi', Syslog::LOG_DAEMON) else if @config['daemon'] @log = Logger.new(@config['logfile']) else @log = Logger.new(STDOUT) end end # Enable debug info if running in debug mode, keep it quiet otherwise if @config['debug'] @log.level = Logger::DEBUG else @log.level = Logger::INFO end @log.info 'Kunoichi starting up' # Daemonize if we're configured for it if @config['daemon'] Process.daemon # Only write the pid file if we're running as daemon if @config['pidfile'] begin File.open(@config['pidfile'], 'w+') { |x| x.write Process.pid.to_s } rescue => e @log.error e. end end end # Catch SIGINT (^C) and SIGTERM (default kill) [ 'INT', 'TERM' ].each do |signal| Signal.trap signal do clean_exit signal end end # Finally, enter the main loop loop do main_loop sleep @config['interval'] end end |
Instance Method Details
#clean_exit(sig) ⇒ Object
Exit cleanly by logging our departure and deleting the pid file if we received a signal
72 73 74 75 76 |
# File 'lib/kunoichi/kunoichi.rb', line 72 def clean_exit(sig) @log.info "Got SIG#{sig}, exiting." File.unlink(@config['pidfile']) if @config['daemon'] and @config['pidfile'] exit end |
#get_process_list ⇒ Object
Fetch the current running processes and put them into a ruby set TODO: make this multi platform
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/kunoichi/kunoichi.rb', line 80 def get_process_list # Initialize the set entries = Set.new # Look up all pids Dir.glob('/proc/[0-9]*') { |x| # Turn pids to numbers pid = File.basename(x).to_i # Formal check for unexpected files on /proc starting with a digit next unless pid.to_s =~ /^[0-9]+$/ # Skip processes above the configured offset if pid > @config['proc_scan_offset'] entries.add pid end } return entries end |
#load_whitelist(file) ⇒ Object
Load the whitelist
99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/kunoichi/kunoichi.rb', line 99 def load_whitelist(file) # Open the file and read all words from lines that don't start with a # begin @whitelist = File.read(file).lines.grep(/^[^#]/).join.split rescue => e puts "Cannot load whitelist: #{e.}" exit end # Warn the user of the whitelist was loaded but found empty if @whitelist.empty? puts 'WARNING: Empty whitelist loaded.' end end |
#main_loop ⇒ Object
Main loop
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 |
# File 'lib/kunoichi/kunoichi.rb', line 208 def main_loop @log.debug 'Checking processes:' # Get running processes procs = get_process_list # Extract the new processes since last loop cycle new_procs = procs - @initial_procs # Skip the run if there are new processes (@log.debug "\tNo new processes found."; return) if new_procs.empty? @log.debug "\tNew processes:" # For each new process: new_procs.each do |pid| # Extract information begin process = ProcEntry.new(pid) rescue # Skip if the process died before we could extract the info @log.debug "\t\t#{pid} - Disappeared before analysis" next end @log.debug "\t\t#{pid} - #{process.cmdline}" # If the process runs as root if process.uid == 0 # Attempt to identify the parent begin parent = ProcEntry.new(process.parent) rescue => e @log.warn "\t\tParent process of #{process.name} disappeared (#{e.})." next end # Skip if the parent has the whitelisted gid next if parent.gid == @config['group'] # If the parent is root if parent.uid == 0 # ..and we've chosen to ignore root spawned processes if @config['ignore_root_procs'] or parent.gid == @config['group'] # if the parent is init if parent.pid == 1 # spare the process unless configured otherwise unless @config['require_init_wlist'] next end # if the parent is not init everything is normal, just skip this one else next end end else # Also skip if the parent is not root, but is in the magic gid next if parent.gid == @config['group'] end # Log our finding @log.info "\t\tFound offending process: #{pid} - #{process.cmdline}" # Skip if the executable is whitelisted if @config['whitelist'] and @whitelist.include? process.binary @log.info "\t\t\tAllowed(#{process.binary})." if @config['log_whitelist'] next end # If we got this far it means the process deserves our attention # Terminate the process unless configured otherwise unless @config['no_kill'] @log.info "\t\t\tTerminating." Process.kill(:KILL,pid) else @log.info "\t\t\tNot killing." end # Also kill the parent unless configured otherwise (or the parent is init) unless process.parent == 1 or @config['no_kill_ppid'] @log.info "\t\t\tKilling parent too." Process.kill(:KILL,process.parent) end # Run the external command in a subprocess fork { run_command(process,parent) } if @config['external_command'] end end # Save the current process list for the next run @initial_procs = procs end |
#run_command(process, parent) ⇒ Object
Run the external command
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/kunoichi/kunoichi.rb', line 114 def run_command(process,parent) # Log our action @log.info "Running external command: #{@config['external_command']}." # Prepare the environment with some useful informations ENV['EVIL_PID'] = process.pid.to_s ENV['EVIL_CMD'] = process.cmdline ENV['EVIL_NAME'] = process.name ENV['EVIL_BIN'] = process.binary ENV['EVIL_PPID'] = parent.pid.to_s ENV['EVIL_PCMD'] = parent.cmdline ENV['EVIL_PNAME'] = parent.name ENV['EVIL_PBIN'] = parent.binary # Launch the command system(@config['external_command']) end |
#validate_conf(config) ⇒ Object
Basic validator for configuration
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/kunoichi/kunoichi.rb', line 6 def validate_conf(config) # Base check, might be redundant raise 'invalid configuration passed: not an hash' unless config.is_a? Hash # Check for group validity raise 'invalid group: not a number' unless config['group'].is_a? Fixnum # Make sure the group exists on /etc/group begin Etc::getgrgid config['group'] rescue => e raise "invalid group: #{e.}" end # Check for daemon validity raise 'invalid daemon setting: please set to true or false' unless config['daemon'].is_bool? # Check interval raise 'invalid interval: please set to a number' unless config['interval'].is_a? Fixnum or config['interval'].is_a? Float # Useful warning in case no interval was declared puts 'WARNING: an interval of 0 can severely impact your machine performance. Use with caution.' if config['interval'] == 0 # Check syslog raise 'invalid value for syslog: please set to true or false' unless config['syslog'].is_bool? # Check logfile raise 'set either syslog or logfile if daemon mode is enabled' unless config['syslog'] or config['logfile'].is_a? String or !config['daemon'] raise 'invalid logfile setting: not writable' unless !config['logfile'] or File.writable? config['logfile'] or File.writable? File.dirname(config['logfile']) # Check whitelist raise 'invalid whitelist: set to a file path, or false to disable' unless config['whitelist'] == false or config['whitelist'].is_a? String raise "invalid whitelist: cannot open #{config['whitelist']}" unless config['whitelist'] == false or File.readable?(config['whitelist']) # Check external_command raise 'invalid external_command: set to a file path, or false to disable' unless config['external_command'].is_a? String or config['external_command'] == false raise 'invalid external_command: not executable' unless !config['external_command'] or File.executable?(config['external_command'].split[0]) # Check no_kill raise 'invalid no_kill: set to true or false' unless config['no_kill'].is_bool? # Check no_kill_ppid raise 'invalid no_kill_ppid: set to true or false' unless config['no_kill_ppid'].is_bool? # Check ignore_root_procs raise 'invalid ignore_root_procs: set to true or false' unless config['ignore_root_procs'].is_bool? # Check log_whitelist raise 'invalid log_whitelist: set to true or false' unless config['log_whitelist'].is_bool? # Check require_init_wlist raise 'invalid require_init_wlist: set to true or false' unless config['require_init_wlist'].is_bool? # Check proc_scan_offset raise 'invalid proc_scan_offset: set to a number' unless config['proc_scan_offset'].is_a? Fixnum raise "invalid proc_scan_offset: set lower than #{@max_pids}" unless config['proc_scan_offset'] < @max_pids # Check pidfile raise 'invalid pidfile setting: set to a string' unless config['pidfile'].is_a? String raise 'invalid pidfile setting: empty' if config['pidfile'].empty? raise 'invalid pidfile setting: not writable' unless File.writable? config['pidfile'] or File.writable? File.dirname(config['pidfile']) # Return the configuration if no errors encountered config end |