1 |
#!/usr/bin/python -O |
2 |
"""This is the main msec module, responsible for all msec operations. |
3 |
|
4 |
The following classes are defined here: |
5 |
|
6 |
ConfigFile: an individual config file. This class is responsible for |
7 |
configuration modification, variable searching and replacing, |
8 |
and so on. |
9 |
|
10 |
ConfigFiles: this file contains the entire set of modifications performed |
11 |
by msec, stored in list of ConfigFile instances. When required, all |
12 |
changes are commited back to physical files. This way, no real |
13 |
change occurs on the system until the msec app explicitly tells |
14 |
to do so. |
15 |
|
16 |
Log: logging class, that supports logging to terminal, a fixed log file, |
17 |
and syslog. A single log instance can be shared by all other |
18 |
classes. |
19 |
|
20 |
MSEC: main msec class. It contains the callback functions for all msec |
21 |
operations. |
22 |
|
23 |
All configuration variables, and config file names are defined here as well. |
24 |
""" |
25 |
|
26 |
#--------------------------------------------------------------- |
27 |
# Project : Mandriva Linux |
28 |
# Module : mseclib |
29 |
# File : libmsec.py |
30 |
# Version : $Id$ |
31 |
# Author : Eugeni Dodonov |
32 |
# Original Author : Frederic Lepied |
33 |
# Created On : Mon Dec 10 22:52:17 2001 |
34 |
# Purpose : low-level msec functions |
35 |
#--------------------------------------------------------------- |
36 |
|
37 |
import os |
38 |
import grp |
39 |
import gettext |
40 |
import pwd |
41 |
import re |
42 |
import string |
43 |
import commands |
44 |
import time |
45 |
import stat |
46 |
import traceback |
47 |
import sys |
48 |
import glob |
49 |
|
50 |
# logging |
51 |
import logging |
52 |
from logging.handlers import SysLogHandler |
53 |
|
54 |
# configuration |
55 |
import config |
56 |
|
57 |
# localization |
58 |
try: |
59 |
gettext.install('msec') |
60 |
except IOError: |
61 |
_ = str |
62 |
|
63 |
# ConfigFile constants |
64 |
STRING_TYPE = type('') |
65 |
|
66 |
BEFORE=0 |
67 |
INSIDE=1 |
68 |
AFTER=2 |
69 |
|
70 |
# regexps |
71 |
space = re.compile('\s') |
72 |
|
73 |
# {{{ helper functions |
74 |
def move(old, new): |
75 |
"""Renames files, deleting existent ones when necessary.""" |
76 |
try: |
77 |
os.unlink(new) |
78 |
except OSError: |
79 |
pass |
80 |
try: |
81 |
os.rename(old, new) |
82 |
except: |
83 |
error('rename %s %s: %s' % (old, new, str(sys.exc_value))) |
84 |
|
85 |
def substitute_re_result(res, s): |
86 |
for idx in range(0, (res.lastindex or 0) + 1): |
87 |
subst = res.group(idx) or '' |
88 |
s = string.replace(s, '@' + str(idx), subst) |
89 |
return s |
90 |
|
91 |
# }}} |
92 |
|
93 |
# {{{ Log |
94 |
class Log: |
95 |
"""Logging class. Logs to both syslog and log file""" |
96 |
def __init__(self, |
97 |
app_name="msec", |
98 |
log_syslog=True, |
99 |
log_file=True, |
100 |
log_level = logging.INFO, |
101 |
log_facility=SysLogHandler.LOG_AUTHPRIV, |
102 |
syslog_address="/dev/log", |
103 |
log_path="/var/log/msec.log", |
104 |
interactive=True, |
105 |
quiet=False): |
106 |
self.log_facility = log_facility |
107 |
self.log_path = log_path |
108 |
|
109 |
# buffer |
110 |
self.buffer = None |
111 |
|
112 |
# common logging stuff |
113 |
self.logger = logging.getLogger(app_name) |
114 |
|
115 |
self.quiet = quiet |
116 |
|
117 |
# syslog |
118 |
if log_syslog: |
119 |
try: |
120 |
self.syslog_h = SysLogHandler(facility=log_facility, address=syslog_address) |
121 |
formatter = logging.Formatter('%(name)s: %(levelname)s: %(message)s') |
122 |
self.syslog_h.setFormatter(formatter) |
123 |
self.logger.addHandler(self.syslog_h) |
124 |
except: |
125 |
print >>sys.stderr, "Logging to syslog not available: %s" % (sys.exc_value[1]) |
126 |
interactive = True |
127 |
|
128 |
# log to file |
129 |
if log_file: |
130 |
try: |
131 |
self.file_h = logging.FileHandler(self.log_path) |
132 |
formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s') |
133 |
self.file_h.setFormatter(formatter) |
134 |
self.logger.addHandler(self.file_h) |
135 |
except: |
136 |
print >>sys.stderr, "Logging to '%s' not available: %s" % (self.log_path, sys.exc_value[1]) |
137 |
interactive = True |
138 |
|
139 |
# interactive logging |
140 |
if interactive: |
141 |
self.interactive_h = logging.StreamHandler(sys.stderr) |
142 |
formatter = logging.Formatter('%(levelname)s: %(message)s') |
143 |
self.interactive_h.setFormatter(formatter) |
144 |
self.logger.addHandler(self.interactive_h) |
145 |
|
146 |
self.logger.setLevel(log_level) |
147 |
|
148 |
def trydecode(self, message): |
149 |
"""Attempts to decode a unicode message""" |
150 |
try: |
151 |
msg = message.decode('UTF-*') |
152 |
except: |
153 |
msg = message |
154 |
return msg |
155 |
|
156 |
def info(self, message): |
157 |
"""Informative message (normal msec operation)""" |
158 |
if self.quiet: |
159 |
# skip informative messages in quiet mode |
160 |
return |
161 |
message = self.trydecode(message) |
162 |
if self.buffer: |
163 |
self.buffer["info"].append(message) |
164 |
else: |
165 |
self.logger.info(message) |
166 |
|
167 |
def error(self, message): |
168 |
"""Error message (security has changed: authentication, passwords, etc)""" |
169 |
message = self.trydecode(message) |
170 |
if self.buffer: |
171 |
self.buffer["error"].append(message) |
172 |
else: |
173 |
self.logger.error(message) |
174 |
|
175 |
def debug(self, message): |
176 |
"""Debugging message""" |
177 |
message = self.trydecode(message) |
178 |
if self.buffer: |
179 |
self.buffer["debug"].append(message) |
180 |
else: |
181 |
self.logger.debug(message) |
182 |
|
183 |
def critical(self, message): |
184 |
"""Critical message (big security risk, e.g., rootkit, etc)""" |
185 |
message = self.trydecode(message) |
186 |
if self.buffer: |
187 |
self.buffer["critical"].append(message) |
188 |
else: |
189 |
self.logger.critical(message) |
190 |
|
191 |
def warn(self, message): |
192 |
"""Warning message (slight security change, permissions change, etc)""" |
193 |
if self.quiet: |
194 |
# skip warning messages in quiet mode |
195 |
return |
196 |
message = self.trydecode(message) |
197 |
if self.buffer: |
198 |
self.buffer["warn"].append(message) |
199 |
else: |
200 |
self.logger.warn(message) |
201 |
|
202 |
def start_buffer(self): |
203 |
"""Beginns message buffering""" |
204 |
self.buffer = {"info": [], "error": [], "debug": [], "critical": [], "warn": []} |
205 |
|
206 |
def get_buffer(self): |
207 |
"""Returns buffered messages""" |
208 |
messages = self.buffer.copy() |
209 |
self.buffer = None |
210 |
return messages |
211 |
|
212 |
# }}} |
213 |
|
214 |
# {{{ ConfigFiles - stores references to all configuration files |
215 |
class ConfigFiles: |
216 |
"""This class is responsible to store references to all configuration files, |
217 |
mark them as changed, and update on disk when necessary""" |
218 |
def __init__(self, log, root=''): |
219 |
"""Initializes list of ConfigFiles""" |
220 |
self.files = {} |
221 |
self.modified_files = [] |
222 |
self.action_assoc = [] |
223 |
self.log = log |
224 |
self.root = root |
225 |
|
226 |
def add(self, file, path): |
227 |
"""Appends a path to list of files""" |
228 |
self.files[path] = file |
229 |
|
230 |
def modified(self, path): |
231 |
"""Marks a file as modified""" |
232 |
if not path in self.modified_files: |
233 |
self.modified_files.append(path) |
234 |
|
235 |
def get_config_file(self, path, suffix=None): |
236 |
"""Retreives corresponding config file""" |
237 |
try: |
238 |
return self.files[path] |
239 |
except KeyError: |
240 |
return ConfigFile(path, self, self.log, suffix=suffix, root=self.root) |
241 |
|
242 |
def add_config_assoc(self, regex, action): |
243 |
"""Adds association between a file and an action""" |
244 |
self.log.debug("Adding custom command '%s' for '%s'" % (action, regex)) |
245 |
self.action_assoc.append((re.compile(regex), action)) |
246 |
|
247 |
def write_files(self, commit=True): |
248 |
"""Writes all files back to disk""" |
249 |
for f in self.files.values(): |
250 |
self.log.debug("Attempting to write %s" % f.path) |
251 |
if commit: |
252 |
f.write() |
253 |
|
254 |
if len(self.modified_files) > 0: |
255 |
self.log.info("%s: %s" % (config.MODIFICATIONS_FOUND, " ".join(self.modified_files))) |
256 |
else: |
257 |
self.log.info(config.MODIFICATIONS_NOT_FOUND) |
258 |
|
259 |
for f in self.modified_files: |
260 |
for a in self.action_assoc: |
261 |
res = a[0].search(f) |
262 |
if res: |
263 |
s = substitute_re_result(res, a[1]) |
264 |
if commit: |
265 |
self.log.info(_('%s modified so launched command: %s') % (f, s)) |
266 |
cmd = commands.getstatusoutput(s) |
267 |
cmd = [0, ''] |
268 |
if cmd[0] == 0: |
269 |
if cmd[1]: |
270 |
self.log.info(cmd[1]) |
271 |
else: |
272 |
self.log.error(cmd[1]) |
273 |
else: |
274 |
self.log.info(_('%s modified so should have run command: %s') % (f, s)) |
275 |
|
276 |
# }}} |
277 |
|
278 |
# {{{ ConfigFile - an individual config file |
279 |
class ConfigFile: |
280 |
"""This class represents an individual config file. |
281 |
All config files are stored in meta (which is ConfigFiles). |
282 |
All operations are performed in memory, and written when required""" |
283 |
def __init__(self, path, meta, log, root='', suffix=None): |
284 |
"""Initializes a config file, and put reference to meta (ConfigFiles)""" |
285 |
self.meta=meta |
286 |
self.path = root + path |
287 |
self.is_modified = 0 |
288 |
self.is_touched = 0 |
289 |
self.is_deleted = 0 |
290 |
self.is_moved = 0 |
291 |
self.suffix = suffix |
292 |
self.lines = None |
293 |
self.sym_link = None |
294 |
self.log = log |
295 |
self.meta.add(self, path) |
296 |
|
297 |
def get_lines(self): |
298 |
if self.lines == None: |
299 |
file=None |
300 |
try: |
301 |
file = open(self.path, 'r') |
302 |
except IOError: |
303 |
if self.suffix: |
304 |
try: |
305 |
moved = self.path + self.suffix |
306 |
file = open(moved, 'r') |
307 |
move(moved, self.path) |
308 |
self.meta.modified(self.path) |
309 |
except IOError: |
310 |
self.lines = [] |
311 |
else: |
312 |
self.lines = [] |
313 |
if file: |
314 |
self.lines = string.split(file.read(), "\n") |
315 |
file.close() |
316 |
return self.lines |
317 |
|
318 |
def append(self, value): |
319 |
lines = self.lines |
320 |
l = len(lines) |
321 |
if l > 0 and lines[l - 1] == '': |
322 |
lines.insert(l - 1, value) |
323 |
else: |
324 |
lines.append(value) |
325 |
lines.append('') |
326 |
|
327 |
def modified(self): |
328 |
self.is_modified = 1 |
329 |
self.meta.modified(self.path) |
330 |
return self |
331 |
|
332 |
def touch(self): |
333 |
self.is_touched = 1 |
334 |
self.modified() |
335 |
return self |
336 |
|
337 |
def symlink(self, link): |
338 |
self.sym_link = link |
339 |
self.modified() |
340 |
return self |
341 |
|
342 |
def exists(self): |
343 |
return os.path.lexists(self.path) |
344 |
#return os.path.exists(self.path) or (self.suffix and os.path.exists(self.path + self.suffix)) |
345 |
|
346 |
def realpath(self): |
347 |
return os.path.realpath(self.path) |
348 |
|
349 |
def move(self, suffix): |
350 |
self.suffix = suffix |
351 |
self.is_moved = 1 |
352 |
self.modified() |
353 |
|
354 |
def unlink(self): |
355 |
self.is_deleted = 1 |
356 |
self.lines=[] |
357 |
self.modified() |
358 |
return self |
359 |
|
360 |
def is_link(self): |
361 |
'''Checks if file is a symlink and, if yes, returns the real path''' |
362 |
full = os.stat(self.path) |
363 |
if stat.S_ISLNK(full[stat.ST_MODE]): |
364 |
link = os.readlink(self.path) |
365 |
else: |
366 |
link = None |
367 |
return link |
368 |
|
369 |
def write(self): |
370 |
if self.is_deleted: |
371 |
if self.exists(): |
372 |
try: |
373 |
os.unlink(self.path) |
374 |
except: |
375 |
error('unlink %s: %s' % (self.path, str(sys.exc_value))) |
376 |
self.log.info(_('deleted %s') % (self.path,)) |
377 |
elif self.is_touched: |
378 |
if os.path.exists(self.path): |
379 |
try: |
380 |
os.utime(self.path, None) |
381 |
except: |
382 |
self.log.error('utime %s: %s' % (self.path, str(sys.exc_value))) |
383 |
elif self.suffix and os.path.exists(self.path + self.suffix): |
384 |
move(self.path + self.suffix, self.path) |
385 |
try: |
386 |
os.utime(self.path, None) |
387 |
except: |
388 |
self.log.error('utime %s: %s' % (self.path, str(sys.exc_value))) |
389 |
else: |
390 |
self.lines = [] |
391 |
self.is_modified = 1 |
392 |
file = open(self.path, 'w') |
393 |
file.close() |
394 |
self.log.info(_('touched file %s') % (self.path,)) |
395 |
elif self.sym_link: |
396 |
done = 0 |
397 |
if self.exists(): |
398 |
full = os.lstat(self.path) |
399 |
if stat.S_ISLNK(full[stat.ST_MODE]): |
400 |
link = os.readlink(self.path) |
401 |
# to be fixed: resolv relative symlink |
402 |
done = (link == self.sym_link) |
403 |
if not done: |
404 |
try: |
405 |
os.unlink(self.path) |
406 |
except: |
407 |
self.log.error('unlink %s: %s' % (self.path, str(sys.exc_value))) |
408 |
self.log.info(_('deleted %s') % (self.path,)) |
409 |
if not done: |
410 |
try: |
411 |
os.symlink(self.sym_link, self.path) |
412 |
except: |
413 |
self.log.error('symlink %s %s: %s' % (self.sym_link, self.path, str(sys.exc_value))) |
414 |
self.log.info(_('made symbolic link from %s to %s') % (self.sym_link, self.path)) |
415 |
elif self.is_moved: |
416 |
move(self.path, self.path + self.suffix) |
417 |
self.log.info(_('moved file %s to %s') % (self.path, self.path + self.suffix)) |
418 |
self.meta.modified(self.path) |
419 |
elif self.is_modified: |
420 |
content = string.join(self.lines, "\n") |
421 |
dirname = os.path.dirname(self.path) |
422 |
if not os.path.exists(dirname): |
423 |
os.makedirs(dirname) |
424 |
file = open(self.path, 'w') |
425 |
file.write(content) |
426 |
file.close() |
427 |
self.meta.modified(self.path) |
428 |
self.is_touched = 0 |
429 |
self.is_modified = 0 |
430 |
self.is_deleted = 0 |
431 |
self.is_moved = 0 |
432 |
|
433 |
def set_shell_variable(self, var, value, start=None, end=None): |
434 |
regex = re.compile('^' + var + '="?([^#"]+)"?(.*)') |
435 |
lines = self.get_lines() |
436 |
idx=0 |
437 |
value=str(value) |
438 |
start_regexp = start |
439 |
|
440 |
if start: |
441 |
status = BEFORE |
442 |
start = re.compile(start) |
443 |
else: |
444 |
status = INSIDE |
445 |
|
446 |
if end: |
447 |
end = re.compile(end) |
448 |
|
449 |
idx = None |
450 |
for idx in range(0, len(lines)): |
451 |
line = lines[idx] |
452 |
if status == BEFORE: |
453 |
if start.search(line): |
454 |
status = INSIDE |
455 |
else: |
456 |
continue |
457 |
elif end and end.search(line): |
458 |
break |
459 |
res = regex.search(line) |
460 |
if res: |
461 |
if res.group(1) != value: |
462 |
if space.search(value): |
463 |
lines[idx] = var + '="' + value + '"' + res.group(2) |
464 |
else: |
465 |
lines[idx] = var + '=' + value + res.group(2) |
466 |
self.modified() |
467 |
self.log.debug(_('set variable %s to %s in %s') % (var, value, self.path,)) |
468 |
return self |
469 |
if status == BEFORE: |
470 |
# never found the start delimiter |
471 |
self.log.debug('WARNING: never found regexp %s in %s, not writing changes' % (start_regexp, self.path)) |
472 |
return self |
473 |
if space.search(value): |
474 |
s = var + '="' + value + '"' |
475 |
else: |
476 |
s = var + '=' + value |
477 |
if idx == None or idx == len(lines): |
478 |
self.append(s) |
479 |
else: |
480 |
lines.insert(idx, s) |
481 |
|
482 |
self.modified() |
483 |
self.log.debug(_('set variable %s to %s in %s') % (var, value, self.path,)) |
484 |
return self |
485 |
|
486 |
def get_shell_variable(self, var, start=None, end=None): |
487 |
# if file does not exists, fail quickly |
488 |
if not self.exists(): |
489 |
return None |
490 |
if end: |
491 |
end=re.compile(end) |
492 |
if start: |
493 |
start=re.compile(start) |
494 |
regex = re.compile('^' + var + '="?([^#"]+)"?(.*)') |
495 |
lines = self.get_lines() |
496 |
llen = len(lines) |
497 |
start_idx = 0 |
498 |
end_idx = llen |
499 |
if start: |
500 |
found = 0 |
501 |
for idx in range(0, llen): |
502 |
if start.search(lines[idx]): |
503 |
start_idx = idx |
504 |
found = 1 |
505 |
break |
506 |
if found: |
507 |
for idx in range(start_idx, llen): |
508 |
if end.search(lines[idx]): |
509 |
end_idx = idx |
510 |
break |
511 |
else: |
512 |
start_idx = 0 |
513 |
for idx in range(end_idx - 1, start_idx - 1, -1): |
514 |
res = regex.search(lines[idx]) |
515 |
if res: |
516 |
return res.group(1) |
517 |
return None |
518 |
|
519 |
def get_match(self, regex, replace=None): |
520 |
# if file does not exists, fail quickly |
521 |
if not self.exists(): |
522 |
return None |
523 |
r=re.compile(regex) |
524 |
lines = self.get_lines() |
525 |
for idx in range(0, len(lines)): |
526 |
res = r.search(lines[idx]) |
527 |
if res: |
528 |
if replace: |
529 |
s = substitute_re_result(res, replace) |
530 |
return s |
531 |
else: |
532 |
return lines[idx] |
533 |
return None |
534 |
|
535 |
def replace_line_matching(self, regex, value, at_end_if_not_found=0, all=0, start=None, end=None): |
536 |
# if at_end_if_not_found is a string its value will be used as the string to inster |
537 |
r=re.compile(regex) |
538 |
lines = self.get_lines() |
539 |
matches = 0 |
540 |
|
541 |
if start: |
542 |
status = BEFORE |
543 |
start = re.compile(start) |
544 |
else: |
545 |
status = INSIDE |
546 |
|
547 |
if end: |
548 |
end = re.compile(end) |
549 |
|
550 |
idx = None |
551 |
for idx in range(0, len(lines)): |
552 |
line = lines[idx] |
553 |
if status == BEFORE: |
554 |
if start.search(line): |
555 |
status = INSIDE |
556 |
else: |
557 |
continue |
558 |
elif end and end.search(line): |
559 |
break |
560 |
res = r.search(line) |
561 |
if res: |
562 |
s = substitute_re_result(res, value) |
563 |
matches = matches + 1 |
564 |
if s != line: |
565 |
self.log.debug("replaced in %s the line %d:\n%s\nwith the line:\n%s" % (self.path, idx, line, s)) |
566 |
lines[idx] = s |
567 |
self.modified() |
568 |
if not all: |
569 |
return matches |
570 |
if matches == 0 and at_end_if_not_found: |
571 |
if type(at_end_if_not_found) == STRING_TYPE: |
572 |
value = at_end_if_not_found |
573 |
self.log.debug("appended in %s the line:\n%s" % (self.path, value)) |
574 |
if idx == None or idx == len(lines): |
575 |
self.append(value) |
576 |
else: |
577 |
lines.insert(idx, value) |
578 |
self.modified() |
579 |
matches = matches + 1 |
580 |
return matches |
581 |
|
582 |
def insert_after(self, regex, value, at_end_if_not_found=0, all=0): |
583 |
matches = 0 |
584 |
r=re.compile(regex) |
585 |
lines = self.get_lines() |
586 |
for idx in range(0, len(lines)): |
587 |
res = r.search(lines[idx]) |
588 |
if res: |
589 |
s = substitute_re_result(res, value) |
590 |
self.log.debug("inserted in %s after the line %d:\n%s\nthe line:\n%s" % (self.path, idx, lines[idx], s)) |
591 |
lines.insert(idx+1, s) |
592 |
self.modified() |
593 |
matches = matches + 1 |
594 |
if not all: |
595 |
return matches |
596 |
if matches == 0 and at_end_if_not_found: |
597 |
self.log.debug("appended in %s the line:\n%s" % (self.path, value)) |
598 |
self.append(value) |
599 |
self.modified() |
600 |
matches = matches + 1 |
601 |
return matches |
602 |
|
603 |
def insert_before(self, regex, value, at_top_if_not_found=0, all=0): |
604 |
matches = 0 |
605 |
r=re.compile(regex) |
606 |
lines = self.get_lines() |
607 |
for idx in range(0, len(lines)): |
608 |
res = r.search(lines[idx]) |
609 |
if res: |
610 |
s = substitute_re_result(res, value) |
611 |
self.log.debug("inserted in %s before the line %d:\n%s\nthe line:\n%s" % (self.path, idx, lines[idx], s)) |
612 |
lines.insert(idx, s) |
613 |
self.modified() |
614 |
matches = matches + 1 |
615 |
if not all: |
616 |
return matches |
617 |
if matches == 0 and at_top_if_not_found: |
618 |
self.log.debug("inserted at the top of %s the line:\n%s" % (self.path, value)) |
619 |
lines.insert(0, value) |
620 |
self.modified() |
621 |
matches = matches + 1 |
622 |
return matches |
623 |
|
624 |
def insert_at(self, idx, value): |
625 |
lines = self.get_lines() |
626 |
try: |
627 |
lines.insert(idx, value) |
628 |
self.log.debug("inserted in %s at the line %d:\n%s" % (self.path, idx, value)) |
629 |
self.modified() |
630 |
return 1 |
631 |
except KeyError: |
632 |
return 0 |
633 |
|
634 |
def remove_line_matching(self, regex, all=0): |
635 |
matches = 0 |
636 |
r=re.compile(regex) |
637 |
lines = self.get_lines() |
638 |
for idx in range(len(lines) - 1, -1, -1): |
639 |
res = r.search(lines[idx]) |
640 |
if res: |
641 |
self.log.debug("removing in %s the line %d:\n%s" % (self.path, idx, lines[idx])) |
642 |
lines.pop(idx) |
643 |
self.modified() |
644 |
matches = matches + 1 |
645 |
if not all: |
646 |
return matches |
647 |
return matches |
648 |
# }}} |
649 |
|
650 |
# {{{ MSEC - main class |
651 |
class MSEC: |
652 |
"""Main msec class. Contains all functions and performs the actions""" |
653 |
def __init__(self, log, root='', plugins=config.PLUGINS_DIR): |
654 |
"""Initializes config files and associations""" |
655 |
# all config files |
656 |
self.log = log |
657 |
self.root = root |
658 |
self.configfiles = ConfigFiles(log, root=root) |
659 |
|
660 |
# plugins |
661 |
self.init_plugins(plugins) |
662 |
|
663 |
def init_plugins(self, path=config.PLUGINS_DIR): |
664 |
"""Loads msec plugins from path""" |
665 |
self.plugins = {} |
666 |
plugin_files = glob.glob("%s/*.py" % path) |
667 |
plugin_r = re.compile("plugins/(.*).py") |
668 |
sys.path.insert(0, path) |
669 |
for file in plugin_files: |
670 |
f = plugin_r.findall(file) |
671 |
if f: |
672 |
plugin_f = f[0] |
673 |
try: |
674 |
plugin = __import__(plugin_f, fromlist=[path]) |
675 |
if not hasattr(plugin, "PLUGIN"): |
676 |
# not a valid plugin |
677 |
continue |
678 |
self.log.debug("Loading plugin %s" % file) |
679 |
plugin_name = getattr(plugin, "PLUGIN") |
680 |
plugin_class = getattr(plugin, plugin_name) |
681 |
plugin = plugin_class(log=self.log, configfiles=self.configfiles, root=self.root) |
682 |
self.plugins[plugin_name] = plugin |
683 |
self.log.debug("Loaded plugin '%s'" % plugin_f) |
684 |
except: |
685 |
self.log.error(_("Error loading plugin '%s' from %s: %s") % (plugin_f, file, sys.exc_value)) |
686 |
|
687 |
def reset(self): |
688 |
"""Resets the configuration""" |
689 |
self.log.debug("Resetting msec data.") |
690 |
self.configfiles = ConfigFiles(self.log, root=self.root) |
691 |
# updating plugins |
692 |
for plugin in self.plugins: |
693 |
self.plugins[plugin].configfiles = self.configfiles |
694 |
|
695 |
def get_action(self, name): |
696 |
"""Determines correspondent function for requested action.""" |
697 |
# finding out what function to call |
698 |
try: |
699 |
plugin_, callback = name.split(".", 1) |
700 |
except: |
701 |
# bad format? |
702 |
self.log.error(_("Invalid callback: %s") % (name)) |
703 |
return None |
704 |
# is it a main function or a plugin? |
705 |
if plugin_ == config.MAIN_LIB: |
706 |
plugin = self |
707 |
else: |
708 |
if plugin_ in self.plugins: |
709 |
plugin = self.plugins[plugin_] |
710 |
else: |
711 |
self.log.info(_("Plugin %s not found") % plugin_) |
712 |
return self.log.info |
713 |
return None |
714 |
try: |
715 |
func = getattr(plugin, callback) |
716 |
return func |
717 |
except: |
718 |
self.log.info(_("Not supported function '%s' in '%s'") % (callback, plugin)) |
719 |
traceback.print_exc() |
720 |
return None |
721 |
|
722 |
def commit(self, really_commit=True): |
723 |
"""Commits changes""" |
724 |
if not really_commit: |
725 |
self.log.info(_("In check-only mode, nothing is written back to disk.")) |
726 |
self.configfiles.write_files(really_commit) |
727 |
|
728 |
def apply(self, curconfig): |
729 |
'''Applies configuration from a MsecConfig instance''' |
730 |
# first, reset previous msec data |
731 |
self.reset() |
732 |
# process all options |
733 |
for opt in curconfig.list_options(): |
734 |
# Determines correspondent function |
735 |
action = None |
736 |
callback = config.find_callback(opt) |
737 |
valid_params = config.find_valid_params(opt) |
738 |
if callback: |
739 |
action = self.get_action(callback) |
740 |
if not action: |
741 |
# The required functionality is not supported |
742 |
self.log.debug("'%s' is not available in this version" % opt) |
743 |
continue |
744 |
self.log.debug("Processing action %s: %s(%s)" % (opt, callback, curconfig.get(opt))) |
745 |
# validating parameters |
746 |
param = curconfig.get(opt) |
747 |
# if param is None, this option is to be skipped |
748 |
if param == None or len(param) == 0: |
749 |
self.log.debug("Skipping %s" % opt) |
750 |
continue |
751 |
if param not in valid_params and '*' not in valid_params: |
752 |
self.log.error(_("Invalid parameter for %s: '%s'. Valid parameters: '%s'.") % (opt, |
753 |
param, valid_params)) |
754 |
continue |
755 |
action(curconfig.get(opt)) |
756 |
|
757 |
def base_level(self, param): |
758 |
"""Defines the base security level, on top of which the current configuration is based.""" |
759 |
pass |
760 |
|
761 |
# }}} |
762 |
|
763 |
# {{{ PERMS - permissions handling |
764 |
class PERMS: |
765 |
"""Permission checking/enforcing.""" |
766 |
def __init__(self, log, root=''): |
767 |
"""Initializes internal variables""" |
768 |
self.log = log |
769 |
self.root = root |
770 |
self.USER = {} |
771 |
self.GROUP = {} |
772 |
self.USERID = {} |
773 |
self.GROUPID = {} |
774 |
self.files = {} |
775 |
self.fs_regexp = self.build_non_localfs_regexp() |
776 |
|
777 |
def get_user_id(self, name): |
778 |
'''Caches and retreives user id correspondent to name''' |
779 |
try: |
780 |
return self.USER[name] |
781 |
except KeyError: |
782 |
try: |
783 |
self.USER[name] = pwd.getpwnam(name)[2] |
784 |
except KeyError: |
785 |
self.log.error(_('user name %s not found') % name) |
786 |
self.USER[name] = -1 |
787 |
return self.USER[name] |
788 |
|
789 |
def get_user_name(self, id): |
790 |
'''Caches and retreives user name correspondent to id''' |
791 |
try: |
792 |
return self.USERID[id] |
793 |
except KeyError: |
794 |
try: |
795 |
self.USERID[id] = pwd.getpwuid(id)[0] |
796 |
except KeyError: |
797 |
self.log.error(_('user name not found for id %d') % id) |
798 |
self.USERID[id] = str(id) |
799 |
return self.USERID[id] |
800 |
|
801 |
def get_group_id(self, name): |
802 |
'''Caches and retreives group id correspondent to name''' |
803 |
try: |
804 |
return self.GROUP[name] |
805 |
except KeyError: |
806 |
try: |
807 |
self.GROUP[name] = grp.getgrnam(name)[2] |
808 |
except KeyError: |
809 |
self.log.error(_('group name %s not found') % name) |
810 |
self.GROUP[name] = -1 |
811 |
return self.GROUP[name] |
812 |
|
813 |
def get_group_name(self, id): |
814 |
'''Caches and retreives group name correspondent to id''' |
815 |
try: |
816 |
return self.GROUPID[id] |
817 |
except KeyError: |
818 |
try: |
819 |
self.GROUPID[id] = grp.getgrgid(id)[0] |
820 |
except KeyError: |
821 |
self.log.error(_('group name not found for id %d') % id) |
822 |
self.GROUPID[id] = str(id) |
823 |
return self.GROUPID[id] |
824 |
|
825 |
def build_non_localfs_regexp(self, |
826 |
non_localfs = ['nfs', 'codafs', 'smbfs', 'cifs', 'autofs']): |
827 |
"""Build a regexp that matches all the non local filesystems""" |
828 |
try: |
829 |
file = open('/proc/mounts', 'r') |
830 |
except IOError: |
831 |
self.log.error(_('Unable to check /proc/mounts. Assuming all file systems are local.')) |
832 |
return None |
833 |
|
834 |
regexp = None |
835 |
|
836 |
for line in file.readlines(): |
837 |
fields = string.split(line) |
838 |
if fields[2] in non_localfs: |
839 |
if regexp: |
840 |
regexp = regexp + '|' + fields[1] |
841 |
else: |
842 |
regexp = '^(' + fields[1] |
843 |
|
844 |
file.close() |
845 |
|
846 |
if not regexp: |
847 |
return None |
848 |
else: |
849 |
return re.compile(regexp + ')') |
850 |
|
851 |
def commit(self, really_commit=True, enforce=False): |
852 |
"""Commits changes. |
853 |
If enforce is True, the permissions on all files are enforced.""" |
854 |
if not really_commit: |
855 |
self.log.info(_("In check-only mode, nothing is written back to disk.")) |
856 |
|
857 |
if len(self.files) > 0: |
858 |
self.log.info("%s: %s" % (config.MODIFICATIONS_FOUND, " ".join(self.files))) |
859 |
else: |
860 |
self.log.info(config.MODIFICATIONS_NOT_FOUND) |
861 |
|
862 |
for file in self.files: |
863 |
newperm, newuser, newgroup, force, newacl = self.files[file] |
864 |
# are we in enforcing mode? |
865 |
if enforce: |
866 |
force = True |
867 |
|
868 |
if newuser != None: |
869 |
if force and really_commit: |
870 |
self.log.warn(_("Forcing ownership of %s to %s") % (file, self.get_user_name(newuser))) |
871 |
try: |
872 |
os.chown(file, newuser, -1) |
873 |
except: |
874 |
self.log.error(_("Error changing user on %s: %s") % (file, sys.exc_value)) |
875 |
else: |
876 |
self.log.warn(_("Wrong owner of %s: should be %s") % (file, self.get_user_name(newuser))) |
877 |
if newgroup != None: |
878 |
if force and really_commit: |
879 |
self.log.warn(_("Enforcing group on %s to %s") % (file, self.get_group_name(newgroup))) |
880 |
try: |
881 |
os.chown(file, -1, newgroup) |
882 |
except: |
883 |
self.log.error(_("Error changing group on %s: %s") % (file, sys.exc_value)) |
884 |
else: |
885 |
self.log.warn(_("Wrong group of %s: should be %s") % (file, self.get_group_name(newgroup))) |
886 |
# permissions should be last, as chown resets them |
887 |
# on suid files |
888 |
if newperm != None: |
889 |
if force and really_commit: |
890 |
self.log.warn(_("Enforcing permissions on %s to %o") % (file, newperm)) |
891 |
try: |
892 |
os.chmod(file, newperm) |
893 |
except: |
894 |
self.log.error(_("Error changing permissions on %s: %s") % (file, sys.exc_value)) |
895 |
else: |
896 |
self.log.warn(_("Wrong permissions of %s: should be %o") % (file, newperm)) |
897 |
|
898 |
if newacl != None: |
899 |
if force and really_commit: |
900 |
self.log.warn(_("Enforcing acl on %s") % (file)) |
901 |
try: |
902 |
# TODO: only change ACL if it differs from actual |
903 |
# TODO: and use python code instead of os.system |
904 |
os.system('setfacl -b %s' % (file)) |
905 |
users = newacl.split(",") |
906 |
for acluser in users : |
907 |
if acluser.split(":")[0] == "": # clean root from list |
908 |
print acluser |
909 |
continue |
910 |
# make the acl rule stick |
911 |
ret = os.system('setfacl -m u:%s %s' % (acluser, file)) |
912 |
if ret != 0: |
913 |
# problem setting setfacl |
914 |
self.log.error(_("Unable to add filesystem-specific ACL %s to %s") % (acluser, file)) |
915 |
except: |
916 |
self.log.error(_("Error changing acl on %s: %s") % (file, sys.exc_value)) |
917 |
else: |
918 |
self.log.warn(_("Wrong acl of %s") % (file)) |
919 |
|
920 |
|
921 |
def check_perms(self, perms, files_to_check=[]): |
922 |
'''Checks permissions for all entries in perms (PermConfig). |
923 |
If files_to_check is specified, only the specified files are checked.''' |
924 |
|
925 |
for file in perms.list_options(): |
926 |
user_s, group_s, perm_s, force, acl = perms.get(file) |
927 |
|
928 |
# permission |
929 |
if perm_s == 'current': |
930 |
perm = -1 |
931 |
else: |
932 |
try: |
933 |
perm = int(perm_s, 8) |
934 |
except ValueError: |
935 |
self.log.error(_("bad permissions for '%s': '%s'") % (file, perm_s)) |
936 |
continue |
937 |
|
938 |
# user |
939 |
if user_s == 'current' or not user_s: |
940 |
user = -1 |
941 |
else: |
942 |
user = self.get_user_id(user_s) |
943 |
|
944 |
# group |
945 |
if group_s == 'current' or not group_s: |
946 |
group = -1 |
947 |
else: |
948 |
group = self.get_group_id(group_s) |
949 |
|
950 |
# now check the permissions |
951 |
for f in glob.glob('%s%s' % (self.root, file)): |
952 |
# get file properties |
953 |
f = os.path.realpath(f) |
954 |
try: |
955 |
full = os.lstat(f) |
956 |
except OSError: |
957 |
continue |
958 |
|
959 |
if self.fs_regexp and self.fs_regexp.search(f): |
960 |
self.log.info(_('Non local file: "%s". Nothing changed.') % f) |
961 |
continue |
962 |
|
963 |
curperm = perm |
964 |
mode = stat.S_IMODE(full[stat.ST_MODE]) |
965 |
|
966 |
if perm != -1 and stat.S_ISDIR(full[stat.ST_MODE]): |
967 |
if curperm & 0400: |
968 |
curperm = curperm | 0100 |
969 |
if curperm & 0040: |
970 |
curperm = curperm | 0010 |
971 |
if curperm & 0004: |
972 |
curperm = curperm | 0001 |
973 |
|
974 |
curuser = full[stat.ST_UID] |
975 |
curgroup = full[stat.ST_GID] |
976 |
curperm = mode |
977 |
# checking for subdirectory permissions |
978 |
if f != '/' and f[-1] == '/': |
979 |
f = f[:-1] |
980 |
if f[-2:] == '/.': |
981 |
f = f[:-2] |
982 |
# check for changes |
983 |
newperm = None |
984 |
newuser = None |
985 |
newgroup = None |
986 |
newacl = None |
987 |
if perm != -1 and perm != curperm: |
988 |
newperm = perm |
989 |
if user != -1 and user != curuser: |
990 |
newuser = user |
991 |
if group != -1 and group != curgroup: |
992 |
newgroup = group |
993 |
if acl != "": |
994 |
newacl = acl |
995 |
if newperm != None or newuser != None or newgroup != None or newacl != None: |
996 |
self.files[f] = (newperm, newuser, newgroup, force, newacl) |
997 |
self.log.debug("Updating %s (matched by '%s')" % (f, file)) |
998 |
else: |
999 |
# see if any other rule put this file into the list |
1000 |
if f in self.files: |
1001 |
self.log.debug("Removing previously selected %s (matched by '%s')" % (f, file)) |
1002 |
del self.files[f] |
1003 |
# do we have to check for any specific paths? |
1004 |
if files_to_check: |
1005 |
self.log.info(_("Checking paths: %s") % ", ".join(files_to_check)) |
1006 |
paths_to_check = [] |
1007 |
for f in files_to_check: |
1008 |
paths_to_check.extend(glob.glob(f)) |
1009 |
paths_to_check = set(paths_to_check) |
1010 |
# remove unneeded entries from self.files |
1011 |
for f in self.files.keys(): |
1012 |
if f not in paths_to_check: |
1013 |
del self.files[f] |
1014 |
return self.files |
1015 |
# }}} |
1016 |
|
1017 |
if __name__ == "__main__": |
1018 |
# this should never ever be run directly |
1019 |
print >>sys.stderr, """This file should not be run directly.""" |
1020 |
|