kunoichi 0.0.1
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.
- 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: []
|