kunoichi 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/kunoichi +85 -0
- data/lib/extensions/object.rb +9 -0
- data/lib/kunoichi.rb +38 -0
- data/lib/kunoichi/cli.rb +99 -0
- data/lib/kunoichi/kunoichi.rb +300 -0
- data/lib/kunoichi/proctable.rb +23 -0
- metadata +84 -0
data/bin/kunoichi
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'kunoichi'
|
4
|
+
|
5
|
+
# Hash for commandline flags that override configuration
|
6
|
+
@override_config = Hash.new
|
7
|
+
|
8
|
+
# Default configuration placement
|
9
|
+
@conf_files = [ 'config.yml', '/etc/kunoichi/config.yml' ]
|
10
|
+
|
11
|
+
begin
|
12
|
+
opts = GetoptLong.new(
|
13
|
+
[ '--config-file', '-c', GetoptLong::REQUIRED_ARGUMENT ],
|
14
|
+
[ '--help', '-h', GetoptLong::NO_ARGUMENT ],
|
15
|
+
[ '--dry_run', '-n', GetoptLong::NO_ARGUMENT ],
|
16
|
+
[ '--import', '-i', GetoptLong::REQUIRED_ARGUMENT ],
|
17
|
+
[ '--generate-config', '-g', GetoptLong::OPTIONAL_ARGUMENT ],
|
18
|
+
[ '--debug', '-d', GetoptLong::NO_ARGUMENT ],
|
19
|
+
[ '--pid-file', '-p', GetoptLong::REQUIRED_ARGUMENT ]
|
20
|
+
)
|
21
|
+
rescue => e
|
22
|
+
puts e.message
|
23
|
+
exit
|
24
|
+
end
|
25
|
+
|
26
|
+
begin
|
27
|
+
opts.each do |opt, arg|
|
28
|
+
case opt
|
29
|
+
when '--help'
|
30
|
+
help
|
31
|
+
when '--config-file'
|
32
|
+
@conf_files = [ arg ]
|
33
|
+
when '--dry-run'
|
34
|
+
@override_config['no_kill'] = true
|
35
|
+
@override_config['no_kill_ppid'] = true
|
36
|
+
when '--debug'
|
37
|
+
@override_config['daemon'] = false
|
38
|
+
@override_config['syslog'] = false
|
39
|
+
@override_config['logfile'] = false
|
40
|
+
@override_config['debug'] = true
|
41
|
+
when '--pid-file'
|
42
|
+
@override_config['pidfile'] = arg
|
43
|
+
when '--import'
|
44
|
+
puts import_conf arg
|
45
|
+
exit
|
46
|
+
when '--generate-config'
|
47
|
+
if arg.empty?
|
48
|
+
generate_conf 'config.yml'
|
49
|
+
else
|
50
|
+
generate_conf arg
|
51
|
+
end
|
52
|
+
exit
|
53
|
+
end
|
54
|
+
end
|
55
|
+
rescue
|
56
|
+
exit
|
57
|
+
end
|
58
|
+
|
59
|
+
# Attempt to include each configuration file
|
60
|
+
@conf_files.each do |file|
|
61
|
+
next unless File.readable? file
|
62
|
+
include_conf file
|
63
|
+
break
|
64
|
+
end
|
65
|
+
|
66
|
+
# If we're done attempting ad have no @config it means none could be loaded
|
67
|
+
unless @config.is_a? Hash
|
68
|
+
puts 'Could not find any configuration files. Tried to load: ' + @conf_files.to_s
|
69
|
+
exit
|
70
|
+
end
|
71
|
+
|
72
|
+
# Handle commandline overrides in configuration
|
73
|
+
[ 'debug', 'no_kill', 'no_kill_ppid', 'daemon', 'syslog', 'logfile', 'pidfile' ].each do |flag|
|
74
|
+
unless @override_config[flag].nil?
|
75
|
+
@config[flag] = @override_config[flag]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Start kunoichi
|
80
|
+
begin
|
81
|
+
Kunoichi::Daemon.new @config
|
82
|
+
rescue => e
|
83
|
+
puts 'Cannot start kunoichi: ' + e.message
|
84
|
+
puts e.backtrace if @config['debug']
|
85
|
+
end
|
data/lib/kunoichi.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'etc'
|
3
|
+
require 'yaml'
|
4
|
+
require 'logger'
|
5
|
+
require 'syslog-logger'
|
6
|
+
require 'getoptlong'
|
7
|
+
|
8
|
+
module Kunoichi
|
9
|
+
DEFAULTS = {
|
10
|
+
'group' => 0,
|
11
|
+
'daemon' => true,
|
12
|
+
'syslog' => true,
|
13
|
+
'interval' => 1,
|
14
|
+
'logfile' => '/var/log/kunoichi.log',
|
15
|
+
'pidfile' => '/var/run/kunoichi.pid',
|
16
|
+
'whitelist' => '/etc/kunoichi/whitelist.txt',
|
17
|
+
'external_command' => '/bin/true',
|
18
|
+
'no_kill' => true,
|
19
|
+
'no_kill_ppid' => true,
|
20
|
+
'ignore_root_procs' => true,
|
21
|
+
'log_whitelist' => true,
|
22
|
+
'require_init_wlist' => false,
|
23
|
+
'proc_scan_offset' => 0,
|
24
|
+
}
|
25
|
+
|
26
|
+
CONFIGURATION_HEADER = '## Kunoichi configuration file.
|
27
|
+
# Same options as ninja.
|
28
|
+
# Check the documentation to know what each option means.
|
29
|
+
# Documentation can be found on the project home page on github,
|
30
|
+
# or in README.md bundled with Kunoichi.
|
31
|
+
'
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
require 'extensions/object'
|
36
|
+
require 'kunoichi/proctable'
|
37
|
+
require 'kunoichi/kunoichi'
|
38
|
+
require 'kunoichi/cli'
|
data/lib/kunoichi/cli.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# Show help
|
2
|
+
def help
|
3
|
+
puts "Usage: #{$0} [options]"
|
4
|
+
puts "Options:"
|
5
|
+
puts "\t-h|--help\t\tThis help"
|
6
|
+
puts "\t-d|--debug\t\tDon't run in daemon mode"
|
7
|
+
puts "\t-n|--dry-run\t\tPerform no actions"
|
8
|
+
puts "\t-c|--config-file\tConfiguration file path.\n\t\t\t\t(defaults: config.rb, /etc/kunoichi/config.rb)"
|
9
|
+
puts "\t-i|--import\t\tImport configuration from ninja and writes to stdout."
|
10
|
+
puts "\t-g|--generate-config\tGenerate sample configuration file"
|
11
|
+
puts "\t-p|--pid-file\t\tPid file path"
|
12
|
+
exit
|
13
|
+
end
|
14
|
+
|
15
|
+
# Try to include yaml configuration
|
16
|
+
def include_conf(file)
|
17
|
+
begin
|
18
|
+
raise 'empty file' unless @config = YAML.load_file(file)
|
19
|
+
rescue => e
|
20
|
+
puts "Cannot load configuration: #{e.message}"
|
21
|
+
exit
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Parse a ninja configuration file and return kunoichi yaml configuration
|
26
|
+
def import_conf(file)
|
27
|
+
|
28
|
+
# Try to open the file
|
29
|
+
begin
|
30
|
+
ninja = File.read(file)
|
31
|
+
rescue => e
|
32
|
+
puts 'Cannot open ninja configuration file: ' + e.message
|
33
|
+
exit
|
34
|
+
end
|
35
|
+
|
36
|
+
# Initialize parsed old configuration and new configuration hashes
|
37
|
+
oldconf = {}
|
38
|
+
newconf = {}
|
39
|
+
|
40
|
+
# Transform ninja configuration, comments excluded, in a ruby hash
|
41
|
+
ninja.lines.grep(/^[^#\n].* = /).each do |x|
|
42
|
+
key=x.split('=')[0].strip!
|
43
|
+
value=x.split('=')[1].strip!
|
44
|
+
oldconf[key] = value
|
45
|
+
end
|
46
|
+
|
47
|
+
# For each of the values that kunoichi understands
|
48
|
+
Kunoichi::DEFAULTS.keys.each do |x|
|
49
|
+
# If it was not declared, return the default value
|
50
|
+
if oldconf[x].nil? then
|
51
|
+
newconf[x] = Kunoichi::DEFAULTS[x]
|
52
|
+
else
|
53
|
+
# If there was the setting, turn yes/no/(null) into booleans
|
54
|
+
case oldconf[x]
|
55
|
+
when 'yes'
|
56
|
+
newconf[x] = true
|
57
|
+
when 'no', '(null)'
|
58
|
+
newconf[x] = false
|
59
|
+
else
|
60
|
+
# Since all values are strings, turn to integer those that require it
|
61
|
+
if [ 'group', 'interval', 'proc_scan_offset' ].include? x
|
62
|
+
newconf[x] = oldconf[x].to_i
|
63
|
+
else
|
64
|
+
# Save the old value
|
65
|
+
newconf[x] = oldconf[x]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Return the newly filled hash in yaml format, preceded by some useful comments.
|
72
|
+
return Kunoichi::CONFIGURATION_HEADER + newconf.to_yaml
|
73
|
+
end
|
74
|
+
|
75
|
+
# Generate a configuration file, starting from defaults
|
76
|
+
def generate_conf(file)
|
77
|
+
# Take care not to overwrite the configuration by mistake
|
78
|
+
if File.exist? file
|
79
|
+
puts "File #{file} already existing."
|
80
|
+
return
|
81
|
+
end
|
82
|
+
begin
|
83
|
+
conf = File.open(file,'w+')
|
84
|
+
rescue => e
|
85
|
+
puts "Error creating file: #{e.message}"
|
86
|
+
return
|
87
|
+
end
|
88
|
+
begin
|
89
|
+
# Write some useful comments, and the defaults dump in yaml
|
90
|
+
conf.write Kunoichi::CONFIGURATION_HEADER +
|
91
|
+
Kunoichi::DEFAULTS.to_yaml
|
92
|
+
rescue => e
|
93
|
+
puts "Error writing to file: #{e.message}"
|
94
|
+
return
|
95
|
+
end
|
96
|
+
puts "Sample configuration written to #{file}."
|
97
|
+
conf.close
|
98
|
+
exit
|
99
|
+
end
|
@@ -0,0 +1,300 @@
|
|
1
|
+
module Kunoichi
|
2
|
+
# The main class, where all the dirty work happens
|
3
|
+
class Daemon
|
4
|
+
|
5
|
+
# Basic validator for configuration
|
6
|
+
def validate_conf(config)
|
7
|
+
|
8
|
+
# Base check, might be redundant
|
9
|
+
raise 'invalid configuration passed: not an hash' unless config.is_a? Hash
|
10
|
+
|
11
|
+
# Check for group validity
|
12
|
+
raise 'invalid group: not a number' unless config['group'].is_a? Fixnum
|
13
|
+
# Make sure the group exists on /etc/group
|
14
|
+
begin
|
15
|
+
Etc::getgrgid config['group']
|
16
|
+
rescue => e
|
17
|
+
raise "invalid group: #{e.message}"
|
18
|
+
end
|
19
|
+
|
20
|
+
# Check for daemon validity
|
21
|
+
raise 'invalid daemon setting: please set to true or false' unless config['daemon'].is_bool?
|
22
|
+
|
23
|
+
# Check interval
|
24
|
+
raise 'invalid interval: please set to a number' unless config['interval'].is_a? Fixnum or config['interval'].is_a? Float
|
25
|
+
# Useful warning in case no interval was declared
|
26
|
+
puts 'WARNING: an interval of 0 can severely impact your machine performance. Use with caution.' if config['interval'] == 0
|
27
|
+
|
28
|
+
# Check syslog
|
29
|
+
raise 'invalid value for syslog: please set to true or false' unless config['syslog'].is_bool?
|
30
|
+
|
31
|
+
# Check logfile
|
32
|
+
raise 'set either syslog or logfile if daemon mode is enabled' unless config['syslog'] or config['logfile'].is_a? String or !config['daemon']
|
33
|
+
raise 'invalid logfile setting: not writable' unless !config['logfile'] or File.writable? config['logfile'] or File.writable? File.dirname(config['logfile'])
|
34
|
+
|
35
|
+
# Check whitelist
|
36
|
+
raise 'invalid whitelist: set to a file path, or false to disable' unless config['whitelist'] == false or config['whitelist'].is_a? String
|
37
|
+
raise "invalid whitelist: cannot open #{config['whitelist']}" unless config['whitelist'] == false or File.readable?(config['whitelist'])
|
38
|
+
|
39
|
+
# Check external_command
|
40
|
+
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
|
41
|
+
raise 'invalid external_command: not executable' unless !config['external_command'] or File.executable?(config['external_command'].split[0])
|
42
|
+
|
43
|
+
# Check no_kill
|
44
|
+
raise 'invalid no_kill: set to true or false' unless config['no_kill'].is_bool?
|
45
|
+
|
46
|
+
# Check no_kill_ppid
|
47
|
+
raise 'invalid no_kill_ppid: set to true or false' unless config['no_kill_ppid'].is_bool?
|
48
|
+
|
49
|
+
# Check ignore_root_procs
|
50
|
+
raise 'invalid ignore_root_procs: set to true or false' unless config['ignore_root_procs'].is_bool?
|
51
|
+
|
52
|
+
# Check log_whitelist
|
53
|
+
raise 'invalid log_whitelist: set to true or false' unless config['log_whitelist'].is_bool?
|
54
|
+
|
55
|
+
# Check require_init_wlist
|
56
|
+
raise 'invalid require_init_wlist: set to true or false' unless config['require_init_wlist'].is_bool?
|
57
|
+
|
58
|
+
# Check proc_scan_offset
|
59
|
+
raise 'invalid proc_scan_offset: set to a number' unless config['proc_scan_offset'].is_a? Fixnum
|
60
|
+
raise "invalid proc_scan_offset: set lower than #{@max_pids}" unless config['proc_scan_offset'] < @max_pids
|
61
|
+
|
62
|
+
# Check pidfile
|
63
|
+
raise 'invalid pidfile setting: set to a string' unless config['pidfile'].is_a? String
|
64
|
+
raise 'invalid pidfile setting: empty' if config['pidfile'].empty?
|
65
|
+
raise 'invalid pidfile setting: not writable' unless File.writable? config['pidfile'] or File.writable? File.dirname(config['pidfile'])
|
66
|
+
|
67
|
+
# Return the configuration if no errors encountered
|
68
|
+
config
|
69
|
+
end
|
70
|
+
|
71
|
+
# Exit cleanly by logging our departure and deleting the pid file if we received a signal
|
72
|
+
def clean_exit(sig)
|
73
|
+
@log.info "Got SIG#{sig}, exiting."
|
74
|
+
File.unlink(@config['pidfile']) if @config['daemon'] and @config['pidfile']
|
75
|
+
exit
|
76
|
+
end
|
77
|
+
|
78
|
+
# Fetch the current running processes and put them into a ruby set
|
79
|
+
# TODO: make this multi platform
|
80
|
+
def get_process_list
|
81
|
+
# Initialize the set
|
82
|
+
entries = Set.new
|
83
|
+
# Look up all pids
|
84
|
+
Dir.glob('/proc/[0-9]*') { |x|
|
85
|
+
# Turn pids to numbers
|
86
|
+
pid = File.basename(x).to_i
|
87
|
+
# Formal check for unexpected files on /proc starting with a digit
|
88
|
+
next unless pid.to_s =~ /^[0-9]+$/
|
89
|
+
# Skip processes above the configured offset
|
90
|
+
if pid > @config['proc_scan_offset']
|
91
|
+
entries.add pid
|
92
|
+
end
|
93
|
+
}
|
94
|
+
|
95
|
+
return entries
|
96
|
+
end
|
97
|
+
|
98
|
+
# Load the whitelist
|
99
|
+
def load_whitelist(file)
|
100
|
+
# Open the file and read all words from lines that don't start with a #
|
101
|
+
begin
|
102
|
+
@whitelist = File.read(file).lines.grep(/^[^#]/).join.split
|
103
|
+
rescue => e
|
104
|
+
puts "Cannot load whitelist: #{e.message}"
|
105
|
+
exit
|
106
|
+
end
|
107
|
+
# Warn the user of the whitelist was loaded but found empty
|
108
|
+
if @whitelist.empty?
|
109
|
+
puts 'WARNING: Empty whitelist loaded.'
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Run the external command
|
114
|
+
def run_command(process,parent)
|
115
|
+
|
116
|
+
# Log our action
|
117
|
+
@log.info "Running external command: #{@config['external_command']}."
|
118
|
+
|
119
|
+
# Prepare the environment with some useful informations
|
120
|
+
ENV['EVIL_PID'] = process.pid.to_s
|
121
|
+
ENV['EVIL_CMD'] = process.cmdline
|
122
|
+
ENV['EVIL_NAME'] = process.name
|
123
|
+
ENV['EVIL_BIN'] = process.binary
|
124
|
+
|
125
|
+
ENV['EVIL_PPID'] = parent.pid.to_s
|
126
|
+
ENV['EVIL_PCMD'] = parent.cmdline
|
127
|
+
ENV['EVIL_PNAME'] = parent.name
|
128
|
+
ENV['EVIL_PBIN'] = parent.binary
|
129
|
+
|
130
|
+
# Launch the command
|
131
|
+
system(@config['external_command'])
|
132
|
+
end
|
133
|
+
|
134
|
+
# Daemon startup procedure
|
135
|
+
def initialize(config)
|
136
|
+
# Check max pids first
|
137
|
+
if RUBY_PLATFORM.downcase =~ /linux/
|
138
|
+
begin
|
139
|
+
@max_pids = File.read('/proc/sys/kernel/pid_max').to_i
|
140
|
+
rescue
|
141
|
+
raise 'Cannot read /proc/sys/kernel/pid_max (is /proc mounted?)'
|
142
|
+
end
|
143
|
+
else
|
144
|
+
# Default on newer FreeBSD. Solaris should have a cap of 30000.
|
145
|
+
# Makes only sense once get_process_list becomes multi-platform, but nice to have.
|
146
|
+
@max_pids = 99999
|
147
|
+
end
|
148
|
+
|
149
|
+
# Validate configuration and store if valid
|
150
|
+
@config = validate_conf config
|
151
|
+
|
152
|
+
# Save the full path to the pid file in @config
|
153
|
+
@config['pidfile'] = File.expand_path(@config['pidfile'])
|
154
|
+
|
155
|
+
# Get initial process list at startup
|
156
|
+
@initial_procs = get_process_list
|
157
|
+
raise 'No processes found (is /proc mounted?)' if @initial_procs.empty?
|
158
|
+
|
159
|
+
# Load the whitelist
|
160
|
+
load_whitelist @config['whitelist'] if @config['whitelist']
|
161
|
+
|
162
|
+
# Initialize @log as configured
|
163
|
+
if @config['syslog']
|
164
|
+
@log = Logger::Syslog.new('kunoichi', Syslog::LOG_DAEMON)
|
165
|
+
else
|
166
|
+
if @config['daemon']
|
167
|
+
@log = Logger.new(@config['logfile'])
|
168
|
+
else
|
169
|
+
@log = Logger.new(STDOUT)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Enable debug info if running in debug mode, keep it quiet otherwise
|
174
|
+
if @config['debug']
|
175
|
+
@log.level = Logger::DEBUG
|
176
|
+
else
|
177
|
+
@log.level = Logger::INFO
|
178
|
+
end
|
179
|
+
|
180
|
+
@log.info 'Kunoichi starting up'
|
181
|
+
|
182
|
+
# Daemonize if we're configured for it
|
183
|
+
if @config['daemon']
|
184
|
+
Process.daemon
|
185
|
+
# Only write the pid file if we're running as daemon
|
186
|
+
if @config['pidfile']
|
187
|
+
begin
|
188
|
+
File.open(@config['pidfile'], 'w+') { |x| x.write Process.pid.to_s }
|
189
|
+
rescue => e
|
190
|
+
@log.error e.message
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Catch SIGINT (^C) and SIGTERM (default kill)
|
196
|
+
[ 'INT', 'TERM' ].each do |signal|
|
197
|
+
Signal.trap signal do clean_exit signal end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Finally, enter the main loop
|
201
|
+
loop do
|
202
|
+
main_loop
|
203
|
+
sleep @config['interval']
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Main loop
|
208
|
+
def main_loop
|
209
|
+
@log.debug 'Checking processes:'
|
210
|
+
# Get running processes
|
211
|
+
procs = get_process_list
|
212
|
+
# Extract the new processes since last loop cycle
|
213
|
+
new_procs = procs - @initial_procs
|
214
|
+
|
215
|
+
# Skip the run if there are new processes
|
216
|
+
(@log.debug "\tNo new processes found."; return) if new_procs.empty?
|
217
|
+
|
218
|
+
@log.debug "\tNew processes:"
|
219
|
+
|
220
|
+
# For each new process:
|
221
|
+
new_procs.each do |pid|
|
222
|
+
# Extract information
|
223
|
+
begin
|
224
|
+
process = ProcEntry.new(pid)
|
225
|
+
rescue
|
226
|
+
# Skip if the process died before we could extract the info
|
227
|
+
@log.debug "\t\t#{pid} - Disappeared before analysis"
|
228
|
+
next
|
229
|
+
end
|
230
|
+
@log.debug "\t\t#{pid} - #{process.cmdline}"
|
231
|
+
|
232
|
+
# If the process runs as root
|
233
|
+
if process.uid == 0
|
234
|
+
|
235
|
+
# Attempt to identify the parent
|
236
|
+
begin
|
237
|
+
parent = ProcEntry.new(process.parent)
|
238
|
+
rescue => e
|
239
|
+
@log.warn "\t\tParent process of #{process.name} disappeared (#{e.message})."
|
240
|
+
next
|
241
|
+
end
|
242
|
+
|
243
|
+
# Skip if the parent has the whitelisted gid
|
244
|
+
next if parent.gid == @config['group']
|
245
|
+
|
246
|
+
# If the parent is root
|
247
|
+
if parent.uid == 0
|
248
|
+
# ..and we've chosen to ignore root spawned processes
|
249
|
+
if @config['ignore_root_procs'] or parent.gid == @config['group']
|
250
|
+
# if the parent is init
|
251
|
+
if parent.pid == 1
|
252
|
+
# spare the process unless configured otherwise
|
253
|
+
unless @config['require_init_wlist']
|
254
|
+
next
|
255
|
+
end
|
256
|
+
# if the parent is not init everything is normal, just skip this one
|
257
|
+
else
|
258
|
+
next
|
259
|
+
end
|
260
|
+
end
|
261
|
+
else
|
262
|
+
# Also skip if the parent is not root, but is in the magic gid
|
263
|
+
next if parent.gid == @config['group']
|
264
|
+
end
|
265
|
+
|
266
|
+
# Log our finding
|
267
|
+
@log.info "\t\tFound offending process: #{pid} - #{process.cmdline}"
|
268
|
+
|
269
|
+
# Skip if the executable is whitelisted
|
270
|
+
if @config['whitelist'] and @whitelist.include? process.binary
|
271
|
+
@log.info "\t\t\tAllowed(#{process.binary})." if @config['log_whitelist']
|
272
|
+
next
|
273
|
+
end
|
274
|
+
|
275
|
+
# If we got this far it means the process deserves our attention
|
276
|
+
|
277
|
+
# Terminate the process unless configured otherwise
|
278
|
+
unless @config['no_kill']
|
279
|
+
@log.info "\t\t\tTerminating."
|
280
|
+
Process.kill(:KILL,pid)
|
281
|
+
else
|
282
|
+
@log.info "\t\t\tNot killing."
|
283
|
+
end
|
284
|
+
|
285
|
+
# Also kill the parent unless configured otherwise (or the parent is init)
|
286
|
+
unless process.parent == 1 or @config['no_kill_ppid']
|
287
|
+
@log.info "\t\t\tKilling parent too."
|
288
|
+
Process.kill(:KILL,process.parent)
|
289
|
+
end
|
290
|
+
|
291
|
+
# Run the external command in a subprocess
|
292
|
+
fork { run_command(process,parent) } if @config['external_command']
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
# Save the current process list for the next run
|
297
|
+
@initial_procs = procs
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Accessory class to access informations about a process
|
2
|
+
class ProcEntry
|
3
|
+
|
4
|
+
attr_accessor :pid, :cmdline, :name, :binary, :uid, :gid, :parent
|
5
|
+
|
6
|
+
# Fetch all the values when the object is created
|
7
|
+
def initialize(pid)
|
8
|
+
raise 'invalid pid: not a number' unless pid.is_a? Fixnum
|
9
|
+
pids = pid.to_s
|
10
|
+
raise 'invalid pid: not running' unless File.directory? "/proc/#{pids}"
|
11
|
+
begin
|
12
|
+
@pid = pid
|
13
|
+
@cmdline = File.read("/proc/#{pids}/cmdline").gsub("\0", ' ')
|
14
|
+
@name = @cmdline.split[0]
|
15
|
+
@uid = File.stat("/proc/#{pids}/").uid
|
16
|
+
@gid = File.stat("/proc/#{pids}/").gid
|
17
|
+
@binary = File.readlink("/proc/#{pids}/exe")
|
18
|
+
@parent = File.readlines("/proc/#{pids}/status").grep(/^PPid:/).first.split[1].to_i
|
19
|
+
rescue => e
|
20
|
+
raise "can't get process info: #{e.message}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kunoichi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Alessandro Grassi
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-03 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: logger
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.2.8
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.2.8
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: syslog-logger
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.6.8
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.6.8
|
46
|
+
description: A Ruby rewrite of Tom Rune Flo's Ninja, a privilege escalation monitor.
|
47
|
+
email:
|
48
|
+
- alessandro@aggro.it
|
49
|
+
executables:
|
50
|
+
- kunoichi
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- lib/extensions/object.rb
|
55
|
+
- lib/kunoichi.rb
|
56
|
+
- lib/kunoichi/cli.rb
|
57
|
+
- lib/kunoichi/kunoichi.rb
|
58
|
+
- lib/kunoichi/proctable.rb
|
59
|
+
- bin/kunoichi
|
60
|
+
homepage: https://github.com/xstasi/kunoichi
|
61
|
+
licenses: []
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options: []
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ! '>='
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
requirements: []
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 1.8.24
|
81
|
+
signing_key:
|
82
|
+
specification_version: 3
|
83
|
+
summary: Process privilege escalation monitor
|
84
|
+
test_files: []
|