Perl для системного администрирования

         

Анализ журналов


Некоторые системные администраторы никогда не заходят дальше фазы ротации в своих взаимоотношениях с журналами. До тех пор пока на диске существует информация, необходимая для отладки, они никогда не подумают об использовании информации из журналов в других целях. Хотелось бы намекнуть, что это недальновидный взгляд и что даже краткий анализ журналов может иметь большое значение. Мы рассмотрим несколько подходов, которые можно использовать для анализа журналов из Perl, начиная от самого простого и постепенно продвигаясь к более сложному.

Большая часть примеров из этого раздела использует журналы из Unix, т. к. в средней Unix-системе журналов больше, чем в двух других операционных системах вместе взятых, а применяемые подходы не зависят от операционной системы.

Чтение-подсчет потока

Самый простой подход - обычное «считывание и подсчет». Мы читаем поток данных из журнала, ищем интересующие нас данные и увеличиваем значение счетчика, когда их находим. Вот простой пример, подсчитывающий, сколько раз перегружалась машина, в котором использован файл wtmpx из Solaris 2.6:*

# шаблон для wtmpx из Solaris 2.6, подробности смотрите в

# документации no pack()

Stemplate = "А32 А4 А32 1 s s2 x2 12 1 x20 S A257 x";

# определяем размер записи $recordsize = length(pack($template,())); и открываем файл

open(WTMP,"/var/adm/wtmpx") or die "Невозможно открыть wtmpx:$!\n";

# считываем по одной записи

while (read(WTMP,Srecord,Srecordsize)) {

($ut_user,$ut_id,$ut_line,$ut_pid,$ut_type,$ut_e_termination,

$ut_e_exit,$tv_sec,$tv_usec,$ut_session,

$ut_syslen,$ut_host)= unpack($template,Srecord);

if ($ut_line eq "system boot"){

print "rebooted ".scalar localtime($tv_sec)."\n"; $reboots++;

}

}

clOse(WTMP);



print "Общее число перезагрузок: $reboots\n";

Расширим этот подход и рассмотрим пример сбора статистики при помощи Event Log из Windows NT. Как говорилось раньше, механизм ведения журналов в NT хорошо разработан и довольно сложен. Эта сложность несколько пугает начинающих программистов на Perl. Для получения основной информации из журналов мы будем




использовать некоторые подпрограммы из модулей для Win32.

Программы в NT и компоненты операционной системы записывают свои действия, регистрируя «события» в одном из журналов событий. Регистрация событий операционной системой сопровождается записью основной информации, например, времени наступления события, имени программы или функции операционной системы, зарегистрировавших событие, типа наступившего события (просто информативное или что-то более серьезное) и т. д.

В отличие от Unix, само описание события, т. е. сообщение, не хранится вместе с записью о событии. Вместо этого в журнал помещается идентификатор EventlD. Этот идентификатор содержит ссылку на определенное сообщение, хранящееся в библиотеке (.dll). Получить сообщение по идентификатору не просто. Этот процесс требует поиска нужной библиотеки в реестре и ее загрузки вручную. К счастью, этот процесс в текущей версии модуля Win32: : EventLog выполняется автоматически (ищите $Win32:: EventLog: :GetMessageText в первом примере с использованием Win32:: Eventlog).

В следующем примере мы сгенерируем простую статистику по числу записей в журнале System, содержащую сведения о том, откуда они поступили, и об уровне их важности. Мы напишем эту программу несколько иначе, чем первый пример в этой главе.

Первый наш шаг - загрузить модуль Win32: : EventLog, обеспечивающий связь между Perl и программами для работы с журналами событий в Win32. Затем мы инициализируем хэш-таблицу, которая будет использоваться для хранения результатов вызовов программ чтения журналов. Обычно Perl заботится об этом за нас, но иногда стоит добавить подобный код ради тех, кто будет впоследствии читать программу. Наконец, мы определяем небольшой список типов событий, который позже будет использоваться для печати статистики:

use Win32::EventLog;

my %event=('Length', NULL,

'RecordNumber',NULL, TimeGenerateo",

NULL. TimeWritten',NULL, 'EventID',NULL, 'EventType',NULL, 'Category',NULL, 'ClosingRecordNumber' .

NULL, 'Source',NULL, 'Computer',NULL, 'Strings',NULL, 'Data',NULL,):



tt

частичный список типов событий, то есть тип 1 -- "Error"

К 2 -- "Warning" и т. д.

@types = ("","Error","Warning","","Information");

Наш следующий шаг - открытие журнала событий System. Open() помещает дескриптор EventLog в $EventLog, который можно использовать для соединения с этим журналом:

Win32::EventLog::Open($EventLog,'System'.'') or die "Невозможно открыть журнал System:$~E\n";

Получив этот дескриптор, мы можем использовать его для подсчета событий в журнале и получения номера самой старой записи:

$EventLog->Win32::EventLog::GetNumber($numevents);

$EventLog->Win32::EventLog::Get01dest($oldestevent);

Эта информация указывается в первом операторе Read(), позиционирующем нас прямо перед первой записью. Это эквивалентно переходу в начало файла при помощи функции seek():

$EventLog->Win32::EventLog::Read((EVENTLOG_SEEK_READ |

EVENTLOG^FORWARDS_READ). $numevents + $oldestevent, $event);

Теперь в простом цикле прочитываем все записи. Флаг EVENTLOG_SEQ-UENTIAL_READ говорит: «Продолжайте читать с позиции после последней прочитанной записи». Флаг EVENTIOG_FORWARDS_READ перемещает нас вперед в хронологическом порядке. Третий аргумент Read() - смещение, в данном случае равное 0, потому что мы продолжаем с той же позиции, на которой остановились. Считывая каждую запись, мы записываем в хэш-таблицу счетчиков ее источник (Source) и тип события (EventType).

 обходим в цикле все события, записывая количество различных

источников (Source) и типов событий

(EventTypes) for ($i=0;$i<$numevents;$i++)

{

$EventLog->Read((EVENTLOG_SEQUENTIAL READ

 EVENTLOG_FORWARDS_READ) 0, Sevent):

$source{$event->{Source}}++:

$types{$event->{EventType}}++: }

it

выводим полученные результаты print "-->

Event Log Source Totals:\n"; for (sort keys %source) {

print "$_: $source{$_}\n": }

print "-"x30,"\n";

print "-->Event Log Type Totals:\n"; for (sort keys %types) {



print "$types[$_J: $types{$_}\n"; }

print "-"x30,"\n";

print "Total number of events: $numevents\n";

Мои результаты выглядят так: -->

Event Log Source Totals: Application Popup:

4 BROWSER: 228 DCOM: 12 Dhcp: 12 EventLog:

351 Mouclass: 6 NWCWorkstation: 2 Print: 27 Rdr: 12

RemoteAccess: 108 SNMP: 350 Serial: 175

Service Control Manager: 248 Sparrow: 5 Srv:

201 msbusmou: 162 msi8042: 3 msinport:

162 mssermou: 151 qic117: 2

--> Event Log Type Totals: Error: 493 Warning:

714 Information: 1014

Total number of events: 2220

Как я и обещал, вот пример кода, полагающегося на подобную программу для вывода содержимого журнала событий. В нем используется программа ElDump Джеспера Лоритсена (Jesper Lauritsen), которую можно загрузить с http://www.ibt.ku.dk/jesper/JespersNTtools.htm. ElDump похожа на DumpEl из NT Resource Kit:

Seldump = 'c:\bin\eldump'; # путь к ElDump

И выводим поля данных. оаз,:згяя их т/льдой ("). .

К текста сообщения (быстрее)

open(ELDUMP."Seldump $dumpflagsj") or die "Невозможно загустить $eidump:$!\n";

print STDERR "Считываем системный журнал.1:

while(<ELDUMP>){

($date, $time, $source, $type. Scategory Sevent, $i.ser, Scom.puter) =

split('-');

$$type{$source}+-t;

print STDERR "."; } print STDERR "done.\n";

close(ELDUMP);

# для каждого типа события выводим источники и количество

 событий

foreach $type (qw(Error Warning Information

AuditSuccess AuditFailure)){

print "-" x 65,"\n";

print uc($type)."s by source:\n";

for (sort keys %$type){

print "$_ ($$type{$_})\n";

} } print "-" x 65,"\n";

Вот выдержка из получаемых данных:

ERRORS by source:

BROWSER (8)

Cdrom (2)

DOOM (15)

Dhcp (2524)

Disk (1)

EventLog (5)

RemoteAccess (30)

Serial (24)

Service Control Manage1" ("00)

Sparrow (2)

atapi (2)

i8042prt (4i

WARNINGS by soiree:

BROWSER (80)

Cdrom (22)



Dhcp (76)

Print (8)

Srv (82)



Вариация на тему предыдущего примера



Простая вариация предыдущего подхода включает в себя многократный обход данных. Иногда это необходимо в случае с данными большого объема и ситуаций, когда сначала приходится просмотреть все данные, чтобы отличить интересные данные от неинтересных. В плане реализации это означает, что после первого обхода данных надо:

Перейти обратно к началу потока данных (который может быть файлом) при помощи seek( )или API-вызова.

или

Закрыть и вновь открыть дескриптор файла. Зачастую это единственный выбор, когда читаются данные из вывода программы, подобной last.

Вот пример, когда такой подход может пригодиться. Представьте, что надо справиться с проблемой в защите, связанной с тем, что кто-то получил несанкционированный доступ к одной из учетных записей. Один из первых вопросов, который приходит на ум, - «был ли получен доступ и к другим учетным записям с той же машины?». Найти полный ответ на такой кажущийся простым вопрос может оказаться сложнее, чем кажется. Давайте попробуем решить эту проблему. Приведенный ниже отрывок кода для SunOS принимает в качестве первого аргумента имя пользователя и необязательное регулярное выражение в качестве второго, чтобы отфильтровать узлы, которые мы хотим проигнорировать:

Stemplate = "А8 А8 А16 1"; # для SunOS 4.1.x Srecordsize = length(pack($template,())); ($user,$ignore) = @ARGV;

print "-- ищем узлы, с которых регистрировался пользователь Suser --\n"; open(WTMP,"/var/adm/wtrcp") or die "Невозможно открыть wtmp:$!\n": while (read(WTMP,Srecord.Srecordsize)) {

($tty, $name,$host:$time)=unpack($template.$reccKd):

if ($user eq $name){

next if (defined Signore and $host =" /$ignore/o); if (length($host) > 2 and 'exists $contacts{$nost}) {

$connect = localti.Tie($time):

$contacts{$ROSt}=$time:

write: >

print "-- ищем другие соединения с этих узлов --\п"; die "Невозможно перейти в начало wtmp:$'\n" unless (seek(WTMP,0,0));



while (read(WTMP,$record.$recordsize)) {

($tty,Sname,$hostt $time)=unpack($template,$record);

ft если это запись не о завершении работы с системой и нас

# интересует этот узел и это соединение установлено для

ft «другой» учетной записи, тогда записываем эти данные

 if (substr($name,1,1) ne "\0" and exists $contacts{$host} and $name ne $user){

Sconnect = localtime($time); write;

}

} close(WTMP);

ft вот формат вывода, вероятно, его потребуется скорректировать

и в зависимости от шаблона

format STDOUT =

@«««« @««««««« @«««««««««

$name,$host,Sconnect

Сначала программа просматривает файл wtmp в поисках записей о регистрации в системе пользователей под «скомпрометированным» именем. По мере нахождения таких записей пополняется хэш, в который записываются имена всех узлов, где регистрировался пользователь под этим именем. Затем программа возвращается к началу файла и просматривает его заново, выполняя на этот раз поиск записей о соединениях с узлов из списка, и выводит совпадения по мере их появления. Не составит труда изменить эту программу так, чтобы она просматривала все файлы из каталога, в котором хранятся файлы ротации журнала wtmp.

Единственная проблема этой программы - ее «узкая специализация». Она будет искать только точное совпадение имен узлов. Если злоумышленник регистрировался в системе, используя динамический адрес, получаемый от провайдера (что бывает часто), то очень велика вероятность, что имена узлов будут отличаться при каждом соединении. Тем не менее, даже неполные решения, подобные этому, очень сильно помогают.

Помимо простоты, у рассмотренного нами подхода есть еще преимущества: он быстрее и требует меньших затрат памяти по сравнению с другими методами. Он лучше всего справляется с журналами, в которых регистрируются данные без поддержки состояния, о которых мы говорили раньше в этой главе. Но иногда, особенно при работе с данными с состоянием, необходимо использовать другие методы.



Процесс «прочитал-запомнил»



Крайность, противоположная предыдущему подходу (там мы «пробегали» по данным как можно быстрее), заключается в чтении их в память и последующей обработке после чтения. Рассмотрим несколько версий этой стратегии.



Для начала приведем простой пример: скажем, у нас есть журнал FTP-сервера и требуется узнать, какие файлы скачивались чаще других. Вот несколько строк из журнала FTP-сервера wu-ftpd:

Sun Dec 27 05:18:57 1998 1 nic.funet.fi 11868 /net/ftp.funet.fi/CPAN/

MIRRORING.FROM a _ о a cpan@perl.org ftp 0 *

Sun Dec 27 05:52:28 1998 25 kju.hc.congress.ccc.de 269273 /CPAN/doc/FAQs/FAQ/

PerlFAQ.html a _ о a mozilla@ ftp 0 *

Sun Dec 27 06:15:04 1998 1 rising-sun.media.mit.edu 11868 /CPAN/

MIRRORING. FROM b __ о a root@rising-sun. media, mit. edu ftp 0 *

Sun Dec 27 06:15:05 1998 1 rising-sun.media.mit.edu 35993 /CPAN/RECENT.html b

о а root@rising-sun.media.mit.edu ftp 0

А вот список полей, из которых состоят приведенные выше строки (все подробности о каждом поле ищите в страницах руководств xferlog(B) сервера wu-ftpd).




Номер поля



Имя поля

0

current-time (текущее время)

1

transfer-time (время передачи, в секундах)

2

remote-host (удаленный узел)

3

filesize (размер файла)

4

filename (имя файла)

5

transfer-type (тип передачи)

6

special-action-flag (специальный флаг)

7

direction (направление)

8

access-mode (режим доступа)

9

use r name (имя пользователя)

10

service-name (имя службы)

11

authentication-method (меод аутентификации)

12

authenticated-user-id (идентификатор аутентифицированного пользователя)
Вот пример программы, сообщающей о том, какие файлы передавались чаще других:

Sxferlog = "/var/adin/Iog/xferlog";

open(XFERLOG,Sxferlog) or die "Невозможно открыть Sxferlog•$!\n":

while (<XFERLOG>){

$files{(sDlit)[8]}++ }

close(XFERLOG);

for (sort {$files{$b} <=> $files{$a}||$a cmp $b} keys %files){

print "$_:$files{$_}\n"; >

Мы считываем каждую строку файла, используя имя файла в качестве ключа кэша, и увеличиваем значение для этого ключа. Имя файла выделяется из каждой строки журнала при помощи индекса массива, ссылающегося на определенный элемент списка, возвращенного функцией split():



$files{(split)[8]>++;

Вы могли заметить, что элемент, на который мы ссылаемся (8), отличается от 8-го поля из списка полей xferlog, приведенного выше. Это печальные последствия того, что в оригинальном файле отсутствуют разделители полей. Мы разбиваем строки из журнала по пробелам (что является значением по умолчанию для split()), так что дата разбивается на пять отдельных элементов списка.

В этом примере применяется искусный прием - сортировку значений выполняет анонимная функция sort:

for (sort {$files{$b} <=> $files{$a}||$a cmp $b} keys %files){

Обратите внимание, что переменные $а и $b в первой части расположены не в алфавитном порядке. Это приводит к тому, что sort выводит элементы в обратном порядке, т. е. первыми отображаются самые «популярные» файлы. Вторая часть анонимной функции sort ( I $a cmp $о) гарантирует, что файлы с одинаковой популярностью будут перечислены в отсортированном порядке.

Для того чтобы этот сценарий подсчитывал только некоторые файлы и каталоги, можно задать регулярное выражение в качестве первого аргумента для сценария. Например, если добавить

next unless /$ARGV[0]/o;

в цикл while(), можно будет задавать регулярные выражения для ограничения учитываемых файлов. Давайте посмотрим на другой пример подхода «прочитал-запомнил», в котором используется программа «поиска брешей» из предыдущего раздела. В предыдущем примере выводилась информация только об успешной регистрации с сайтов злоумышленника. Узнать о неудавшихся попытках мы не можем. Чтобы получить такую информацию, мы рассмотрим другой файл журнала.



Регулярные выражения



Регулярные выражения - одна из самых важных составляющих анализа журналов. Регулярные выражения используются как сито для отсева интересных данных от данных, не представляющих интереса. В этой главе применяются только основные регулярные выражения, но вы вполне можете создавать более сложные для собственных нужд. Так, применение подпрограмм или технологии создания регулярных выражений из предыдущей главы позволяет использовать их еще более эффективно.



Время, потраченное на получение навыков работы с регулярными выражениями, окупится с лихвой и не раз. Регулярные выражения лучше всего изучать по книге Джеффри Фридла (Jeffrey Friedl) «Mastering Regular Expressions» (Волшебство регулярных выражений) (O'Reilly).

Эта проблема иллюстрирует один из недостатков Unix: информация из журналов в Unix-системах хранится в различных местах и в различных форматах. Для того чтобы справиться с этими различиями, существует не так много инструментов (к счастью, у нас есть Perl). Нередко приходится использовать более одного источника данных для решения подобных задач.

Журнал, который сейчас больше всего нам пригодится, - это журнал, сгенерированный через syslog инструментом tcpwrappers, который предоставляет программы и библиотеки, позволяющие контролировать доступ к сетевым службам. Любую сетевую службу, например tel net, можно настроить так, чтобы все сетевые соединения для нее обрабатывались сначала программой tcpwrappers. После того как соединение будет установлено, программа tcpwrappers регистрирует попытку соединения через syslog и затем либо передает соединение настоящей службе, либо предпринимает некоторые действия (например, разрывает соединение). Решение, разрешить ли данное соединение, основывается на нескольких правилах, введенных пользователем (например, разрешать лишь для некоторых исходящих узлов), tcpwrappers также может принять меры предосторожности и послать запрос к DNS-серве-ру, чтобы убедиться, что соединение устанавливается оттуда, откуда

ожидается. Кроме того, программу можно настроить так, чтобы в журнале регистрировалось имя пользователя, устанавливающего соединение (через протокол идентификации, описанный в RFC931), если это возможно. Более подробное описание tcpwrappers можно найти в книге Симеона Гарфинкеля (Simson Garfinkel) и Джина Спаффорда (Gene Spafford) «Practical Unix & Internet Security» (Unix в практическом использовании и межсетевая безопасность) (O'Reilly).

Мы же просто добавим несколько строк к предыдущей программе, в которых просматривается журнал tcpwrappers (в данном случае tcpdlog) для поиска соединений с подозрительных узлов, найденных нами в wtmp. Если добавить этот код в конец предыдущего примера,





местоположение журнала tcpd

Stcpdlog = "/var/log/tcpd/tcpdlog";

Shostlen = 16; Я максимальная длина имени узла в файле wtmp

print "-- просматриваем tcpdlog --\n";

open(TCPDLOG,Stcpdlog) or die "Невозможно прочитать $tcpdlog:$!\n";

while(<TCPDLOG>){

next if !/connect from /; tt нас беспокоят только соединения (Sconnecto,Sconnectfrom) = /(.+):\s+connect from\s+(.+)/; Sconnectfrom =" s/~.+@//;

tt

tcpwrappers может регистрировать имя узла целиком, а не

# только первые N символов, как некоторые журналы wtmp. В

результате необходимо усечь имя узла до той же длины, что

Вив wtmp файле, если мы собираемся искать имя узла в кэше

Sconnectfrom = substr($connectfrom.O,Shostlen);

print if (exists $contacts{$connectfrom} and Sconnectfrom !~ /$ignore/o);

то мы получим данные, подобные этим:

-- ищем узлы, с которых регистрировался пользователь --

user host.ccs.neu Fri Apr 3 13:41;47

-- ищем другие соединения с этих узлов --

user2 host.ccs.neu Thu Oct 9 17:06:49

user2 host.ccs.neu Thu Oct 9 17:44:31

user2 host.ccs.neu Fri Oct 10 22:00:41

user2 host,ccs.neu Wed Oct 15 07:32:50

user2 host.ccs.neu Wed Oct 22 16:24:12

-- просматриваем tcpdlog --

Jan 12 13:16:29 riost2 in. rshd[866]: connect from user4@host. ccs. neu. ed^

Jan 13 14:38:54 hosts in, rlogind[4761]: connect from user5@host. ccs, nei., f-з ,

Jan 15 14:30:17 host4 in.ftpd[18799]: connect from user6@host.ccs.reu.ecu

Jan 16 19:48:19 hosts in.ftpd[5131]: connect from user7@host.ccs.neu.ca.,

Читатели могли обратить внимание, что в приведенных выше результатах были замечены соединения, устанавливаемые в различное время. В файле wtmp были зарегистрированы соединения, установленные в период с 3 апреля по 22 октября, тогда как tcpwrappers показывает только январские соединения. Разница в датах говорит о том, что файлы wtmp и файлы tcpwrappers имеют различную скорость ротации. Необходимо учитывать такие детали, если вы пишете программу, которая полагается на то, что файлы журналов относятся к одному и тому же периоду времени.



В качестве последнего и более сложного примера, демонстрирующего подход «прочитал-запомнил», рассмотрим задачу, требующую объединения данных с состоянием и без него. Для того чтобы получить более полную картину действий на сервере wu-ftpd, можно установить соответствие между информацией о регистрации в системе из файла wtmp и информацией о передаче файлов, записанной в файле xferlog сервера wu-ftpd. Было бы здорово увидеть, когда начался сеанс работы с FTP-сервером, когда он закончился и какие файлы передавались в течение этого сеанса.

Вот отрывок вывода программы, которую мы собираемся написать. В нем показаны четыре FTP-сеанса за март. В первом сеансе на машину был передан один файл. В двух других файлы были переданы с этой машины, а в течение последнего сеанса файлов не было передано вообще:

Thu Mar 12 18:14:30 1998-Thu Mar 12 18:14:38 1998 pitpc.ccs.neu.ed -> /home/dnb/makemod

Sat Mar 14 23:28:08 1998-Sat Mar 14 23:28:56 1998 traal-22.ccs.neu <- /home/dnb/.emacs19

Sat Mar 14 23:14:05 1998-Sat Mar 14 23:34:28 1998 traal-22.ccs.neu <- /home/dnb/lib/emacs19/

cperl-mode.el <- /home/dnb/lib/emacs19/filladapt.el

Wed Mar 25 21:21:15 1998-Wed Mar 25 21:36:15 1998 traal-22.ccs.neu (no transfers in xferlog)

Получить такие данные не очень просто, поскольку приходится добавить данные без информации о состоянии в журнал данных с информацией о состоянии. В журнале xferlog приводится только время, когда была совершена передача файла, и узел, участвующий в этой предаче. В журнале wtmp приводится информация о соединениях и завершении соединений других узлов с сервером. Давайте посмотрим, как объединить эти два типа данных при помощи подхода «прочитал-запомнил». В этой программе мы определим некоторые переменные, а затем вызовем подпрограммы для выполнения каждой задачи :

 для преобразования дата ->время-в-1)п1х (количество секунд с начала эпохи) use Time::Local

Sxferlog = "/var/log/xferlog":

 местоположение журнала передачи файлов

$wtmp = "/var/adm/wtmp"; g местоположение wtmp $template = "A8 A8 A16 1";



шаблон для wtmp в SunOS 4.1.4 Srecordsize = length(pack($template,()));

размер каждой записи в wtmp Shostlen = 16;

 максимальная длина имени узла в wtmp

карта соответствий имени месяца с номером

%month = qw{Jan 0 Feb 1 Mar 2 Apr 3 May 4 Jun 5 Jul 6 Aug 7 Sep 8 Oct 9 Nov 10 Dec 11};

&ScanXferlog;

 просматриваем журнал передачи файлов

&ScanWtmp;

 просматриваем журнал wtmp

&ShowTransfers;

 приводим в соответствие и выводим информацию о передачах

Теперь рассмотрим процедуру, читающую журнал xferlog:

 просматриваем журнал передачи файлов сервера wu-ftpd и

заполняем структуру данных %transfers

sub ScanXferlog {

local($sec, $min,$hours,$mday,$mon,$year); my($time,$rhost,$fname,Sdirection);

print STDERR "Просматриваю Sxferlog...";

open(XFERLOG,$xferlog) or

die "Невозможно открыть $xferlog:$!\n";

while (<XFERLOG>){

Я используем срез массива для выбора нужных полей

($mon,$mday,$time,Syear,$rhost,$fname,Sdirection) = (split)[1,2,3,4,6,8,11];

Я добавляем к имени файла направление передачи, 81- это передача на сервер

$fname = (Sdirection eq '!' ? "-> " : "<- ") . $fnarne;

и преобразуем время передачи к формату времени в Unix

($hours,$min,$sec) = split(':',Stime); Sunixdate =

timelocal($sec,$min,Shours,$mday,$month{$mon},Syear);

Я помещаем данные в хэш списка списков:

push((s>{$transfers{substr($rhost, 0, Shostlen)}},

[Sunixdate,Sfname]); }

close(XFERLOG); print STDERR "Готово.\n"; }

Строка push(), вероятно, заслуживает объяснения.

В этой строке формируется хэш списка списков, выглядящий примерно так:

$transfers{hostname} =

([timel, filenamel], [time2. filena'ne2]1 [time3 filenames] ..)

Ключами хэша %transfers являются имена узлов, инициирующих передачи файлов. При создании каждой записи мы укорачиваем имя узла до максимальной длины, допустимой в wtmp.

Для каждого узла мы сохраняем список пар, состоящих из времени передачи файла и его имени. Время сохраняется в «секундах, прошедших с начала эпохи» для упрощения дальнейшего сравнения. Подпрограмма timelocalO из модуля Time: : Local помогает выполнить преобразование времени к этому стандарту. Поскольку мы просматриваем журнал передачи файлов, записанный в хронологическом порядке, список пар тоже строится в хронологическом порядке, а это нам пригодится позже.



Теперь перейдем к просмотру wtmp :

просматриваем файл wtmp и заполняем структуру sessions

информацией о ftp-сеансах sub ScanWtmp {

my($record, $tty,$name,$host,$time,%connections);

print STDERR "Просматриваю $wtmp...\n";

open(WTMP,$wtmp) or die "Невозможно открыть $wtmp:$!\n";

while (read(WTMP,$record, Srecordsize)) {

it если запись начинается не с ftp, даже не пытаемся ее

разбирать (unpack). ЗАМЕЧАНИЕ: мы получаем зависимость от

формата wtmp в обмен на скорость next if (substr($record,0,3) ne "ftp");

($tty,$name,$nost,$time)=unpack($template,$ record);

и если мы находим запись об открытии соединения, мы

создаем хэш списка списков. Список списков позже № будет использован в качестве стэка.

if ($name and substr($name,0,1) ne "\0"){

push(@{$connections{$tty}},[$host,$time]); }

# если мы находим запись о закрытии соединения, пытаемся и найти ей пару в записях об открытии соединений,

 найденных раньше else {

unless (exists $connections{$tty}){

warn "Найдено только завершение соединения с $tty:" .

scalar localtime($time)."\n"; next;

 # будем использовать предыдущую запись об открытии

# соединения и эту запись о закрытии соединения в

качестве записи об одном сеансе. Чтобы сделать

 это, мы создаем список списков, где каждый список

 имеет вид (hostname, login, logout) push((8>sessions,

[@{shift @{$connections{$tty}H, Stime]):

ft если для этого терминала больше нет соединений в

стэке, удаляем запись из хэша delete $connections{$tty>

unless (@{$connections<$tty}});

}

}

close(WTMP);

print STDERR "Готово.\n";

 }

Давайте посмотрим, что происходит в этой программе. Мы считываем по одной записи из файла wimp. Если эта запись начинается с ftp, мы знаем, что это сеанс FTP. Как говорится в комментарии, строка кода, в которой принимается это решение, явно привязана к формату записи в wtmp. Будь поле tty не первым полем записи, эта проверка не сработала бы. Однако возможность узнать, что строка не представляет для нас интереса, не выполняя для этого unpack(), того стоит.



Когда мы находим строчку, начинающуюся с ftp, мы разбиваем ее, чтобы выяснить, относится она к открытию FTP-соединения или к закрытию. Если это открытие соединения, то мы записываем его в %connections, структуру данных, хранящую сводку по открытым соединениям. Как и %transfers из предыдущей подпрограммы, это хэш списка списков, на этот раз его ключами являются терминалы для каждого соединения. Каждое значение этого хэша - это набор пар, представляющих имя узла, установившего соединение, и время установки соединения.

Зачем нужна такая сложная структура данных для слежения за открытием соединений? К сожалению, в wtmp нет простых пар строк «открытие-закрытие открытие-закрытие открытие-закрытие». Например, посмотрим на строки из wtmp (их выводила наша первая в этой главе программа, работающая с wtmp):

ftpd1833:dnb:ganges.ccs.neu.e:Fn Mar 27 14:04:47 1998

 ttyp7:(logout):(logout):Fri Mar 27 14:05:11 1998

ftpd1833:dnb:hotdiggitydog-he:Fri Mar 27 14:05:20 1998

ftpd1833:(logout):(logout):Fri Mar 27 14:06:20 1998

ftpd1833:(logout): (logout):Fn Mar 27 14:06:43 1998

Обратите внимание на две записи об открытии FTP-соединения на одном и том же терминале (1-я и 3-я строчки). Если бы мы сохраняли по одному соединению для терминала в простом хэше, то потеряли бы информацию о первом соединении, встретив второе.

Вместо этого мы в качестве стека используем список списков, ключами которого являются терминалы из %conrecnons. Когда встречается запись об открытии соединения, пара (host. iu<jin-time) помещается в стек для этого терминала. Каждый раз, когда встречается информация о закрытии соединения с этого терминала, одна из записей об открытии соединения «выбрасывается» из стека и вся информация о сеансе целиком сохраняется в другой структуре данных. Для этого в программе есть такая строка:

push(@sessions,[@{shift @{$connections{$tty}}},$time]);

Давайте разберемся с этой строкой «изнутри», чтобы все прояснить. Выделенная жирным часть строки возвращает ссылку на стек/список открытых соединений для данного терминала:



push(@sessions, [@{shift (*{$connections{$tty}}},$time]);

Эта часть выбрасывает из стека ссылку на первое соединение:

push(@sessions,[@{shift @{$connections{$tty}}},$time]);

Мы разыменовываем ее, чтобы получить сам список (host, login-time) для соединения. Если поместить эту пару в начало другого списка, заканчивающегося временем соединения, Perl интерполирует пары для соединения, и мы получим один список из трех элементов. Теперь у насесть группа (host, login-time, logout-time):

push(@sessions,[©{shift @{$connections{$tty}}>,$time]):

Теперь, когда у нас есть все составляющие (узел, начало соединения и конец соединения) для сеанса FTP в одном списке, можно добавить ссылку на этот список в список (sessions, который будет использоваться позже:

push(@sessions, [@{shift @{$connections{$tty}}}, $time]);

Благодаря одной очень насыщенной строке у нас есть список сеансов.

Чтобы завершить работу в подпрограмме &ScanWtmp, необходимо проверить, пуст ли стэк для каждого терминала, т. е. проверить, что не осталось больше записей об открытии соединения. Если это так, можно удалить эту запись из хэша; мы знаем, что соединение завершилось:

delete $connections{$tty} unless (@{$connectio.ns{$tty}});

Настало время поставить в соответствие два различных набора данных. Эта задача ложится на плечи подпрограммы &ShowTransfers. Для каждого сеанса она выводит список из трех элементов, относящихся к соединению, и файлы, переданные во время этого сеанса.

обходим в цикле журнал соединений, ставя в соответствие и сеансы с передачами файлов

sub ShowTransfers { local($session);

foreach Ssession (@sessions){

# выводим время соединения print scalar localtime($$session[1]) . "-" .

scalar localtime($$session[2]) . " $$session[0]\n";

 ищем все файлы, переданные в этом сеансе и выводим их

print &FindFiles(@{$session}),"\n"; } }

Вот самая сложная часть, в которой приходится решать, передавались ли файлы в течение сеанса связи:

 возвращает все файлы, переданные в течение данного сеанса sub FindFiles{



my($rhost,$login,Slogout) = @_;

my($transfer,@found);

  простой случай, передачи файлов не было

unless (exists $transfers{$rhost}){

return "\t(no transfers in xferlog)\n"; }

простой случай, первая запись о передаче файлов записана

# после регистрации

($transfers{$rhost}->[0]->[0] > $logout){

return "\t(no transfers in xferlog)\n"; }

 ищем файлы, переданные во время сеанса

 foreach $transfer (@{$transfers{$rhost}}){

 если передача до регистрации

next if ($$transfer[0] < Slogin);

и если передача после регистрации

last if ($$transfer[0] > Slogout);

 если мы уже использовали эту запись next unless (defined $$transfer[1]);

Первым делом можно исключить простые случаи. Если мы не нашли записей о передаче файлов, выполненной этим узлом, или если первая передача произошла после завершения интересующего нас сеанса, это означает, что в течение данного сеанса файлы не передавались.

Если нельзя исключить простые случаи, необходимо просмотреть список всех передач. Мы проверяем, произошла ли передача, связанная с данным узлом, после начала сеанса, но до его завершения. Мы переходим к следующей передаче, если какое-либо из этих утверждений неверно. Кроме того, мы прекращаем проверку остальных передач для этого узла, когда находим запись о передаче, произошедшей после завершения соединения. Помните, уже говорилось о том, что все записи о передаче файлов добавляются в структуру данных в хронологическом порядке? Это окупается именно здесь.

Последняя проверка перед тем, как решить, засчитывать ли запись о передаче файла, выглядит несколько странно:

Я если мы уже использовали эту запись next unless (defined $$transfer[1]);

Если два анонимных сеанса с одного и того же узла происходят в одно и то же время, то нет никакого шанса выяснить, к какому из них относится запись о передаче этого файла. Ни в одном из журналов просто не существует информации, которая могла бы нам в этом помочь. Лучшее, что можно тут сделать, - определить правило и придерживаться его. Здесь правило такое: «Приписывать передачу первому возможному соединению». Эта проверка и последующий undef проводят его в жизнь.



Если последняя проверка пройдена, мы объявляем о победе и добавляем имя файла к списку файлов, переданных в течение этого сеанса. После этого выводим информацию о сеансах и выполненных передачах файлов.

Подобные программы, в которых выполняется поиск взаимосвязей, могут быть довольно сложными, особенно когда они объединяют источники данных, связи между которыми не являются достаточно четкими. Так что давайте посмотрим, можно ли подойти к этому проще.



Черные ящики



В мире Perl часто случается так, что когда вы пытаетесь написать что-то широко используемое, кто-то другой публикует свое решение этой задачи раньше. Это дает возможность просто передать свои данные в уже готовый модуль и получить результаты, не задумываясь о том, как выполняется данная задача. Это часто называют «подходом черного ящика».

Один такой пример - это пакет SyslogScan Рольфа Харольда Нельсона (Rolf Harold Nelson). Раньше мы уже отмечали, что анализ почтового журнала sendmail может оказаться непростой задачей из-за информации о состоянии. Часто со строкой связана одна или несколько родственных строк, перемешанных с другими строками в этом же журнале. Пакет SyslogScan предоставляет простой способ обратиться к информации о доставке каждого сообщения, так что нет необходимости вручную просматривать файл и выбирать оттуда все связанные строки. Этот пакет позволяет найти в журнале определенные адреса и предоставляет некоторую статистику по найденным сообщениям.

Пакет SyslogScan объектно-ориентированный, так что первым делом нужно загрузить модуль и создать новый экземпляр объекта:

use SyslogScan::DeliveryIterator;

 список почтовых журналов

syslog $maillogs = ["/var/log/mail/maillog"];

$iterator = new SyslogScan::DeliveryIterator(syslogList => $maillogs):

Метод new модуля SyslogScan: :DeliveryIterator возвращает итератор (iterator), т. е. указатель в файле, двигающийся от одной строки о доставке сообщения к другой. Применяя итератор, мы избавляемся от необходимости просматривать файл в поисках всех строк, относящихся к конкретному сообщению. Если вызвать метод next() для этого итератора, он вернет нас обратно к объекту доставки. Этот объект хранит информацию о доставке, прежде распределенную по нескольким строкам в журнале. Например, следующий код:



while ($delivery = $iterator -> next()){ print $delivery->{Sender}." -> ".

join(",",@{$delivery->{ReceiverList}}),"\n"; }

позволяет получить такую информацию:

root@host.ccs.neu.edu ->

user1@cse.scu.edu




owner-freebsd-java-digest@freebsd.org -
>



user2@ccs.neu.edu rootiahost.ccs.neu.edu -
>

user3@ccs.neu.edu

Можно сделать еще лучше. Если передать итератор из SyslogScan методу new модуля SyslogScan: :Summary, new примет весь вывод метода next итератора и вернет итоговый объект. Этот объект содержит итоговую информацию по всем доставкам сообщений, которые только может вернуть итератор.

Но SyslogScan переносит эту функциональность на другой уровень. Если передать последний объект методу new из SyslogScan: : ByGroup, мы получим объект bygroup, в котором вся информация сгруппирована по

доменам и приводится статистика по этим группам. Вот как применяется то, о чем мы только что говорили:



use SyslogScan::DeliveryIterator;




use SyslogScan::Summary;




use SyslogScan::ByGroup




use SyslogScan::Usage;




П местоположение maillog




Smaillogs = ["/var/log/mail/maillog"];




# получаем для этого файла итератор




$iterator = new SyslogScan::DeliveryIterator(syslogList => $maillogs);




# передаем итератор в ::Summary, получаем объект summary (сводка)




Ssummary = new SyslogScan::Summary($iterator);




tt передаем сводку в ::ByGroup и получаем обьект stats-by-group




# (статистика по группам)




Sbygroup = new SyslogScan::ByGroup($summary);




ft выводим содержимое этого объекта foreach $group (sort keys %$bygroup){




($bmesg,$bbytes)=@{$bygroup->{$group}->




{groupUsage}->getBroadcastVolume()}; ($smesg,$sbytes)=@{$bygroup->{$group}->




{groupllsage}->getSendVolume()}; ($rmesg,$rbytes)=@{$bygroup->{$group}->




{groupUsage}->getfieceiveVolume()}; ($rmesg,$rbytes)=@{$bygroup->{$group}->




{groupUsage}->getReceiveVolume()}; write; }




format STDOUT.TOP =




Name Bmesg BByytes Smesg SBytes Rmesg Rbytes






format STDOUT =




@««««<«««« @»>» @»»>» @»>» @»»»> @»»> @»»»>




Sgroup,Sbmesg,Sbbytes,$smesg,Ssbytes,$rmesg,$rbytes


Результат представляет собой подробный отчет по количеству широковещательных, отправленных и полученных сообщений и их размера в байтах. Вот отрывок из получаемых результатов:

Name Bmesg BByytes Smesg SByres Rmesg Roytes


globalserve net

1

1245

1

1245

0

0

gloDe.com

0

0

0

0

1

2040
Положительная сторона такого подхода в том, что можно сделать многое благодаря тяжелой работе, проделанной автором модуля или сценария, не прикладывая больших усилий со своей стороны. Отрицательная сторона - необходимость во всем полагаться на код автора. В нем могут быть ошибки или может использоваться подход, не устраивающий вас. Всегда стоит сначала ознакомиться с программой, прежде чем «дать ей зеленую улицу» у себя на сайте.



Использование баз данных



Последний подход, который мы обсудим, для своей реализации требует других знаний помимо Perl. Так что мы просто рассмотрим технологию, которая со временем, вероятно, станет более популярной.

Все рассмотренные предыдущие примеры хорошо работают с данными приемлемого размера и на машинах с приемлемым количеством памяти, но они не масштабируемы. В ситуациях, когда у вас много данных, особенно если они поступают из различных источников, естественным инструментом становятся базы данных.

Существует по крайней мере два способа использования баз данных из Perl. Первый из них я называю методом «только Perl». В этом случае все действия осуществляются в Perl или в библиотеках, тесно связанных с Perl. Во втором применяются модули, например из семейства DBI, позволяющие сделать Perl клиентом баз данных, таких как MySQL, Oracle или MS-SQL. Рассмотрим оба подхода для обработки и анализа журналов.



Использование баз данных, встроенных вРег!



До тех пор пока данных не слишком много, можно применять только Perl. В качестве примера мы будем использовать расширенную версию вездесущего «искателя брешей в системе безопасности». До сих пор наша программа имела дело с соединениями только на одной машине. Как поступить, если захочется узнать о регистрации злоумышленников и на других наших машинах?



Первый шаг - поместить все данные из wtmp для наших машин в ту или иную базу данных. Будем считать, что все машины имеют прямой доступ к некоторым разделяемым каталогам через некую сетевую файловую систему наподобие NFS. Перед тем как двигаться дальше, необходимо выбрать формат базы данных.

В качестве «формата баз данных для Perl» я выбрал формат Berkeley DB. Я беру «формат баз данных для Perl» в кавычки потому, что хоть поддержка DB встроена в Perl, сами библиотеки DB необходимо достать в другом месте (http://www.sleepycat.com) и установить их до того, как поддержка Perl будет скомпилирована. Ниже приведено сравнение между различными поддерживаемыми форматами баз данных (табл. 9.4).



Таблица 9.4.

Сравнение поддерживаемых в Perl форматов баз данных

Название Поддержка в Unix Поддержка в NT/2000 Поддержка в MacOS Ограничения

на размеры ключей или значений
Независимость

от порядка байтов

старый

dbm
Да Нет Нет Нет
«новый» dbm Да Нет Да Нет
Sdbm Да Да Нет 1К (по умолчанию) Нет
Gdbm Да Да Нет Нет Нет

DB Да Да Да Нет Да
Мне нравится формат Berkeley-DB, поскольку он может обрабатывать большие объемы данных и не зависит от порядка байт. Независимость от порядка байт особенно важна для программы, которую мы собираемся рассмотреть, т. к. мы будем считывать и записывать данные в один и тот же файл с различных машин, у которых может быть различная архитектура.

Начнем с заполнения базы данных. В целях простоты и переносимости мы остановим свой выбор на программе last, чтобы не использовать un-packQ для различных файлов wtmp. Вот программа, за которой следуют объяснения:

use DB_File;

use FreezeThaw qw(freeze thaw);

use Sys::Hostname; n чтобы получить текущее имя узла

use Fcntl; # для определения 0_CREAT и 0_RDWR

ищем исполняемый файл для программы

last (-х "/bin/last" and $lastex = "/bin/last")

or (-x "/usr/ucb/last" and Slastex = "/usr/ucb/last");

Suserdb = "userdata";



  файл базы данных пользователей Sconnectdb = "connectdata";

 файл базы данных соединений Sthishost = &hostname;

ope'n(LAST, "$lastex|") or

die "Невозможно запустить программу Slastex:$!\n'

считываем каждую строку вывода last while (<LAST>){

next if /~reboot\s/ or /~shutdown\s/ or /~ftp\s/ or /~wtmp\s/;

($user,$tty,$host,$day.$mon.SdateStime) = split;

next if $tty =~ /":0/ or $tty =" /"consoles/;

next if (length($host) < 4);

$when = $mon." ".Sdate," ".Stime;

tt сохраняем каждую запись в хэше списка списков

push(ia{$users{$user}},[$thishost,$host,$when]);

push(@{$connects{$host}},[$thishost,$user.$when]); }

close(LAST);

tt создаем файл базы данных (для чтения и записи); если он не

существует, смотрите сноску в тексте re: $DB_BTREE tie %userdb,

"DB_File",$userdb,0_CREAT]0_RDWR, 0600, $DB_BTREE or die

 "Невозможно открыть базу данных Suserdb для чтения/записи:$!\n"

tt обходим в цикле пользователей и сохраняем информацию в базе

данных при помощи freeze foreach $user (keys %users){ if (exists $userdb{$user}){

(Suserinfo) = thaw($userdb{$user»;

push(@{$userinfo},(a{$users{$user}});

$userdb{$user}=freeze Suserinfo; } else {

Suserdb!$user}=freeze $users{$user}; } } untie %userdb;

tt делаем то же самое для соединений

tie %connectdb, "DB_File",Sconnectdb,0_CREAT]0_RDWR,

0600, $DB_BTREE

or die "Невозможно открыть базу данных Sconnectdb для чтения/записи:$!

foreach Sconnect (keys %connects){ if (exists $connectdb{$connect}){

($connectinfo) = thaw($connectdb<$connect}); piiSh(@{$connectinfo}. 5>{$connects{$cornect}}): $connectdb{$connect}=freeze($connectinfo); } else {

$connectdb{$connect}=freeze($connects{$connect}); } } untie %conner,tdb

Программа принимает вывод команды last и делает следующее:

1. Отфильтровывает бесполезные строки.

2. Сохраняет вывод в двух хэшах списка списков, структура данных которых выглядит так:



$users{usernair;e} =

[[ current host, connecting Host, connect time], [current host, connecting host, connect time]

]; $connects{host} =

[[current host, usernamel, connect time], [current host, username2, connect time],

];

3. Помещает структуру данных в память и пытается добавить ее в базу данных.

Этот последний шаг самый интересный, поэтому рассмотрим его подробно. Мы связываем хэши %userdb и %connectdb с файлами баз данных. Это позволяет легко обращаться к хэшам, в то время как Perl «за сценой» обрабатывает сохранение и получение данных из файлов базы данных. Но в хэшах хранятся только простые строки. Как преобразовать наш «хэш списка списков» в одно значение?

Модуль FreezeThaw Ильи Захаревича используется для хранения сложной структуры данных в одном скалярном значении, которое можно применять в качестве значения хэша. FreezeThaw принимает произвольную структуру данных Perl и представляет ее в виде строки. Существуют и другие модули, подобные этому, самые распространенные из которых Data::Dumper Гурусами Сарати (Gurusamy Sarathy) (входит в состав Perl) и Storable Рафаэля Манфреди (Raphael Manfredi). FreezeThaw обеспечивает наиболее компактное представление сложной структуры данных, поэтому он и используется здесь. Каждый из этих модулей имеет свои плюсы, так что внимательно изучите возможности всех трех, если вам нужно будет решать задачу, подобную нашей.

В программе мы проверяем, существует ли запись для этого пользователя или узла. Если нет, мы просто «замораживаем» структуру данных в строку и сохраняем эту строку в базе данных при помощи связанного хэша. Если существует, мы «размораживаем» существующую структуру данных из базы данных в память, добавляем наши данные, вновь ее «замораживаем» и сохраняем.

Выполнив эту программу на нескольких машинах, мы получим базу данных с некоторой потенциально полезной информацией, которую можно будет добавить в следующую версию нашей программы.

Подходящее время для заполнения подобной базы данных - сразу после операции ротации журналов wtmp.



Код, используемый здесь для заполнения базы данных, настолько прост (это скорее план, чем реальная программа), что его не стоит широко применять в жизни. Один недостаток, который бросается в глаза, это отсутствие механизма, предотвращающего попытки одновременного обновления базы данных несколькими экземплярами программы. Учитывая, что блокировка файлов через NFS по крайней мере неочевидна, было бы проще вызывать подобную программу из большей программы, которая возьмет на себя сбор информации от каждой машины.

Теперь, заполнив базу данных, рассмотрим улучшенную версию программы, использующей эту информацию:

use DB_File;

use FreezeThaw qw(freeze thaw);

use Fcntl;

 принимаем имя пользователя и узлы, которые мы игнорируем, в

 командной строке ($user,$ignore) = @ARGV;

 файлы баз данных, которые мы используем

Suserdb ="userdata"; $connectdb ="connectdata";

tie %userdb, "DB_File",Suserdb,0_RDONLY,666,$DB_BTREE or die

"Невозможно открыть базу данных $userdb для чтения:$!\п";

tie %connectdb, "DB_Fiie",Sconnectdb,0_RDONLY,666,$DB_BTREE or die

"Невозможно открыть базу данных Sconnectdb для чтения :$!\г";

Мы загрузили нужные нам модули, получили необходимые данные, установили несколько переменных и связали их с файлами базы данных. Теперь пришло время немного поработать:

можно выходить, если этот пользователь не устанавливал

соединений

unless (exists $jserdb{$user}){

print "Этот пользователь не регистрировался.

untie %userdb;

untie %cor,".ectdc,

exit; }

(Suserinfo) = thaw($userdb{$user}):

print "-- first host contacts from $user --\n": foreacn $contact (@{$userinfo}){

next if (defined Signore and $contact->[l] =~ /$ignore/o):

print $contact->[1] . " -> " , $contact->[0] . " on ".$contact->[2]."\n";

$otherhosts{$contact->[1]}=' ' ; }

Вот как работает этот код: если мы видели этого пользователя, то воспроизводим в памяти записи о его соединениях при помощи thawQ. Для каждого контакта мы проверяем, нужно ли игнорировать соединения с этого узла. Если нет, то выводим информацию об этом соединении и записываем узел, с которого оно было установлено, в хэш %otherhosts.



Здесь хэш применяется как простой способ собрать список уникальных узлов из всех записей о соединениях. Теперь, когда у нас есть список узлов, с которых мог зарегистрироваться злоумышленник, необходимо выяснить, какие еще пользователи регистрировались с этих подозрительных узлов.

Найти эту информацию будет не сложно, потому что когда мы записывали, какие пользователи регистрировались на каких машинах, мы также записывали и обратное (т. е. на каких машинах регистрировались какие пользователи) в другом файле базы данных. Теперь мы смотрим на записи, соответствующие найденным на предыдущем шаге узлам. Если этот узел не надо игнорировать и с него было зарегистрировано соединение, мы собираем список пользователей, регистрировавшихся на этом узле, при помощи хэша %userseen:

print "-- other connects from source machines --\n";

foreacn $nost (keys %otherhosts){

next if (defined Signore and $host =" /$ignore/o):

next unless (exists $connectdb{$host});

(Sconnectinfo) = thaw($connectdb{$host});

foreach Sconnect ((°>{$connectinfo}){

next if (defined Signore and Sconnect->[0] =~ /$ignore/o);

$userseen{$connect->[1]}=''; } }

Последнее действие этой драмы в трех актах имеет элегантный конец. Мы возвращаемся к первоначальной базе данных пользователей, чтобы найти все соединения, установленные подозрительными пользователями с подозрительных машин:

foreach Suser (sort keys %userseen){ next unless (exists $userdb{$user}):

foreach Sconract («(Suseri'ifo» !

next if (iio'-i^ed $ignore and Scontact->[ 1 ]

write i" (iiMsts $otherhos\ slSco

}

Нам осталось только подмести сцену и уйти домой:

untie %userdb; untie %connectdb;

format STDOUT =

@«««« @<««««««« -> ffl<«««««««

$user.":", Scontact->[1],Scontact->[0],$contact->[2]

Вот как выглядит вывод этой программы (опять же, имена пользователей и машин изменены):

-- first host contacts from baduser --

badhostl.exampl -> machine1.ccs.neu.edu on Jan 18 09:55

badhost2,exampl -> machine2.ccs.neu.edu on Jan 19 11:53



-- other connects from source machines --

baduser2: badhostl.exampl -> machine2.ccs.neu.e on Dec 15 13:26

baduser2: badhost2.exampl -> machine2,ccs.neu.e on Dec 11 12:45

baduser3: badhostl. exampl -> machinel. ccs. neu. ed on Ji;l 13 16:20

baduser4: badhostl.exampl -> machinel.ccs.neu.ed on Jun 9 11:53

baduser: badhost1.exampl -> machinel.ccs.neu.ed on Jan 18 09:55

baduser: badhost2. exampl --> machine2. ccs. neu. e on Jan 19 11:53

Эта программа хороша в качестве примера, но она не масштабируется дальше, чем на небольшую группу машин. Для каждого последующего вызова программы необходимо прочитать запись из базы данных, «растопить» (thaw()) ее в памяти, добавить новые данные, снова их «заморозить» (freezeO) и сохранить опять в базе данных. Это может потребовать больших затрат процессорного времени и памяти. Потенциально весь процесс происходит для каждого пользователя и соединения, так что все замедляется очень быстро.



Использование баз данных SQL



Теперь рассмотрим один из способов обращения с очень большими наборами данных. Вам может потребоваться загрузить данные в более сложную базу данных SQL (коммерческую или нет) и запрашивать из нее информацию, используя SQL. Тем, кто не знаком с SQL, я рекомендую ознакомиться с приложением D «Пятнадцатиминутное руководство по SQL», перед тем как смотреть на этот пример.

Заполнить базу данных можно так:

use ОВГ

use Sys .

$db = "drib":

ищем месгоголожс.чие iasr (-х '/bin/last" and $]astex = "/от/ lasc")

 (-x "/usr/ucb/last" ana Slastex = '/азг7ьсо.' ;ast"):

 подсоединяемся к базе даинь.х Sybase

переходим иа базу данных, которую мы будем использовать

$dbh->do("use $db") or die "Невозможно перейти к $db: ".$dbh->errstr."\n";

в

создаем таблицу iastinfo, если ее еще не существует unless ($dbh->selectrow_array(

q{SELECT name from sysobjects WHERE name="lastinfo"})){

$dbh->do(q{create table Iastinfo (username char(8),



localhost char(40), otherhost varchar(75), when char(18))}) or

die "Невозможно создать таблицу Iastinfo: ".$dbh->errstr."\n"; }

Sthishost = Shostnanie:

$sth = $dbh->prepare(

qq{INSERT INTO lastinfo(username,localhost,otherhost.when) VALUES (?, 'Sthishosf, ?, ?)}) or die

"Невозможно подготовить запрос insert: ". Sdrj.h-^errstr , "\n":

open(LAST."Slastexj") or die "Невозможно выполни:» программу Slastex

while (<LAST>){

next if /"reboot\.s/ or /'shutdown'.s/ or

/"ftp';S/ or / "ivt-rpv.s/:

$wher> = $!non " ".Scare." ". $tme:

Sst1-->sxec-J'te;$j3e',SbcЈt.$/.ter^ close(LAST);

$c)bh->di scanned;

Теперь можно использовать базу данных по назначению. Вот набор простеньких SQL-запросов, которые легко можно выполнить из Perl при помощи интерфейсов DBI или ODBC, о которых мы говорили в главе 7 «Администрирование баз данных SQL»:

-- сколько всего записей в таблице

9 select count (*) from lastinfo;

10068

-- сколько пользователей было зарегистрировано

9 select count (distinct username) from lastinfo;

237

-- сколько различных узлов устанавливали соединение с нашими машинами

 select count (distinct otherhost) from lastinfo;

1000

-- на каких локальных машинах регистрировался пользователь

"dr.b"? select distinct localhost from lastinfo where username = "dnb": localhost

hostl host2

Эти примеры должны помочь читателю прочувствовать, как можно «исследовать» данные, когда они хранятся в настоящей базе данный Каждый из этих запросов требует для выполнения лишь около секунды. Базы данных могут быть быстрым, мощным инструментом дл системного администрирования.

Анализ журналов - бесконечная тема для разговора. К счастью, 1 глава снабдила вас кое-какими инструментами и некоторым вдохновением.


Данные с состоянием и без


Помимо формата, в котором хранятся данные из журналов, также важно подумать о содержимом этих файлов, поскольку на наши действия повлияет и то, что из себя представляют эти данные и как они представлены. Когда речь идет о содержимом файлов журналов, часто можно разделить данные, имеющие состояние, и данные, состояния не имеющие. Рассмотрим пару примеров, которые помогут разобраться в этих различиях.

Вот отрывок журнала веб-сервера Apache. В каждой строке представлен запрос к веб-серверу:

esnet-118.dynamic.rpi.edu - - [13/Dec/1998:00:04:20 -0500] "GET home/u1/tux/ tuxedoOS.gif

HTTP/1.0" 200 18666 ppp-206-170-3-49.okld03.pacbeli.net - - [13/Dec/ 1998:00:04:21 -0500] "GET home/u2/news.ntm

HTTP/1.0" 200 6748 ts007d39.ftl-fl.concentric.net - - [13/Dec/1998:00.04.22 -0500] "GET home/u1/bgc.jpg HTTP/1.1"

А вот несколько строк из журнала демона принтера:

Аид 14 12:58:46 warhol printer' cover/door open Аид 14 12:58:58 warhol printer: error cleared

Аид 14 17:16:26 warhol printer: offline or intervention needed

Аид 14 17:16:43 warhol printer: error cleared

Аид 15 20:40:45 warhol printer: paper out

Аид 15 20:40:48 warhol printer: error cleared

В обоих случаях каждая строка из журнала не зависит от других строк журнала. Можно найти шаблоны или сгруппировать вместе строки, собирая статистику, но в этих данных нет ничего, что связывало бы между собой записи из журнала.

Теперь давайте рассмотрим несколько подправленных записей из журнала sendmail:

Dec 13 05:28:27 mailhub sendmail[26690]: FAA26690:

from=<user@>has.a.godcomplex.com>, size=643, class=0, pri=30643, nrcpts=1. msgid=<199812131032.CAA22824(3has.a.godconplex.com>,

proto=ESMTP, relay=user(5>has. a. godcomplex. com [216. 32. 32.176]

Dec 13 05:29:13 mailhub sendmail[26695]: FAA26695:

from=<root@host.ccs.neu.edu>, size=9600, class=0, pn=39600, nrcpts=1. msgid=<199812131029.FAA15005@host.ccs.neu.edu>,

proto=ESMTP, relay=root@host.ccs.neu.edu [129.10.116. 69]




Dec 13 05:29:15 mailhub sendmail[26691]: FAA26690: to=<user@ccs.neu.edu>, delay=00:00:02, xdelay=00:00:01, mailer=local, stat=Sent

Dec 13 05:29:19 mailhub sendmail[26696]: FAA26695: to=")IFS=' '&&exec /usr/ bin/procmail -f-||exit 75

«user", ctladdr=user (6603/104), delay=00:00:06. xdelay=00:00:06, mailer=prog, stat=Sent

Каждая строка имеет как минимум одну парную запись, в которой указаны источник и получатель каждого сообщения. Когда сообщение попадает в систему, ему присваивается уникальный идентификатор, выделенный на рисунке жирным шрифтом, нужный для «опознания» этого сообщения. Идентификатор сообщения позволяет нам связать соответствующие строки из журнала, определяя существование или «состояние» сообщения между записями в журнале.

Иногда нам нужно знать «расстояние» между переходами. Возьмем, к примеру, файл wtmp, рассмотренный ранее в этой главе. Нас интересует не только то, когда пользователи регистрируются в системе и завершают с ней работу (два вида смены состояния в журнале), но и время, прошедшее между этими двумя событиями, т. е. время, в течение которого они были зарегистрированы.

В более сложных журналах могут существовать и другие особенности. Вот выдержки из журнала почтового сервера, находящегося в режиме отладки. Имена и IP-адреса изменены во избежание недоразумений:

Jan 14 15:53:45 mailhub popper[20243]: Debugging turned on

Jan 14 15:53:45 irailhub popper[20243]: (v2.53) Servicing request from

"client" at 129.X.X.X

Jan 14 15:53:45 mailhub popper[20243]: +OK QPOP (version 2.53) at mailftub

starting.

Jan 14 15:53:45 mailhub popper[20243]: Received: "USER username"

Jan 14 15:53:45 mailtiub popperf20243]: +OK Password required for username

Jan 14 15:53:45 mailhub'popper[20243]: Received: "pass xxxxxxxxx"

Jan 14 15:53:45 mailhub popper[20243]: +OK username has 1 message (26627

octets).

Jan 14 15:53:46 mailhub popper[20243]: Received' "LIST"

Jan 14 15:53:46 mailhub popper[20243]: +OK 1 messages (26627 octets)



Jan 14 15:53:46 mailhub popper[20243]: Received: "RETR 1"

Jan 14 15:53:46 mailhub popper[20243]: +OK 2662? octets

<message text appears here>

Jan 14 15:53:56 mailhub popper[20243]: Received: "DELE 1"

Jan 14 15:53:56 mailhuo popoer[2Q243]: Deleting message 1 at offset 0 of

length 26627

Jan 14 15:53:56 mailhub popoer[20243]: +OK Message 1 has been deleted,

Jan 14 15:53:56 mailhub popper[20243]: Received: "QUIT"

Jan 14 15:53:56 mailrub popper[20243]: + OK Pop server at mailhub signing э<=-

Jan 14 15:53:56 railhub poppsr[20243]: (v2.53) Ending request from "user' at

(client) 129.X.X.X

Можно увидеть не только установление соединения («Servicing request from...») и рассоединения («Ending request from...»), но и подробную информацию о том, что происходило в промежутке.

Каждое из этих промежуточных состояний сопровождается также и потенциально полезной информацией о «продолжительности». Если на POP-сервере возникнут какие-либо неполадки, можно будет узнать, сколько времени занимал каждый из приведенных выше шагов.

В случае с FTP-сервером из этих данных можно будет сделать некоторые выводы относительно того, как люди взаимодействуют с вашим сервером. Сколько времени, в среднем, люди проводят на сайте, прежде чем загрузить файлы? Много ли времени они проводят между выполнениями команд? Всегда ли они переходят из одной части сервера в другую, перед тем как загрузить одни и те же файлы? Промежуточные данные могут быть ценным источником информации.






Двоичные журналы


Иногда не просто писать программы, имеющие дело с журналами. Вместо приятных на вид, легко анализируемых текстовых строк, некоторые средства ведения журналов создают двоичные файлы патентованного формата, которые нельзя проанализировать при помощи одной строки кода на Perl. К счастью, Perl не боится таких напастей. Рассмотрим несколько подходов к работе с такими файлами. Ниже приведены два различных примера двоичных журналов: файл wtmp в Unix и журналы событий NT/2000.

В главе 3 «Учетные записи пользователей» мы упоминали о регистрации пользователей для работы на машине с Unix. В большинстве Unix-систем регистрация в системе и завершение работы с ней регистрируются в файле wtmp. Если нужно узнать о «привычках» пользователя относительно регистрации (например, на какой машине он обычно регистрируется?), то необходимо обратиться к этому файлу.

В NT/2000 журналы событий играют более обобщенную роль. Они используются для регистрации практически всех событий, происходящих на машине, включая регистрацию работы пользователей, сообщения операционной системы, события системы безопасности и т. д. Их роль аналогична роли службы syslog в Unix.

Использование unpackQ

В Perl существует функция unpack(), специально созданная для анализа двоичных данных и структур. Давайте посмотрим, как ее можно использовать для работы с файлами wtmp. Формат wtmp отличается в различных системах Unix. В следующем примере мы будем иметь дело с файлами wtmp из SunOS 4.1.4 и Digital Unix 4.0, поскольку они достаточно просты. Вот как выглядит текстовое представление первых трех записей в файле wtmp из SunOS 4.1.4.

Если вы еще не знакомы со структурой этого файла, подобный «ASCII-дамп» (так он называется) данных выглядит как некий полуслучайный мусор. Как же нам разобраться со структурой этого файла?

Самый простой способ понять формат этого файла - заглянуть в исходники программ, читающих и пишущих в него. Тех, кто не знаком с языком С, эта задача может смутить. К счастью, нет необходимости разбираться и даже смотреть в большую часть кода; достаточно разобраться с частью, определяющей формат файла.




Все программы операционной системы, читающие и пишущие в файл wtmp, берут определение файла из одного коротенького включаемого файла С, который скорее всего расположен в /usr/include/utmp.h. Интересующая нас часть файла начинается с определения структуры данных С, которая будет использоваться для хранения информации. Если мы поищем struct utmp {, то найдем нужную нам часть. Строки, следующие за struct utmp {, определяют каждое поле этой структуры. Каждая из этих строк сопровождается комментарием в стиле /* се/ .

Чтобы почувствовать, насколько могут отличаться две различные версии wtmp, сравним отрывки из uimp.h для двух операционных систем:

SunOS 4.1.4:

struct utmp {

char ut_line[8]; /* tty name */

cnar ut_name[8]; /* user id «/

char ut__host[16]: /* nost name, if remote •/

long ut_tiine; /'* time on */ }:

Digital Unix 4.0:

slr.ict jT^ip !

В этих файлах есть все, что требуется для написания функции unpack(), которая в качестве первого аргумента принимает шаблон формата данных и с помощью этого шаблона определяет, как разобрать двоичные (обычно) данные, переданные во втором аргументе. unpack() разобьет данные так, как это указано, и вернет список, каждый элемент которого соответствует элементу шаблона.

Давайте построим шаблон по кусочкам, принимая за основу структуру на С из файла utmp.h в SunOS. Многие буквы разрешается использовать в шаблонах и здесь рассказывается именно о них, но вообще-то вы должны обратиться к разделу pack() из руководстваperlfunc за подробными разъяснениями. Создание шаблонов - не всегда простое занятие; периодически компиляторы С дополняют поля структуры для того, чтобы выровнять их по требуемой границе памяти. Команда pstruct, входящая в состав Perl, часто помогает справиться с подобными особенностями.

С нашим форматом данных таких сложностей не возникает. Посмотрите на анализ файла utmp.h (табл. 9.1).

Таблица 9.1. Преобразование кода на С из utmp.h в шаблон unpack( )




Код на С



Шаблон unpack()



Буква шаблона/повтор

char ut_line[8];

A8

Строка ASCII (дополнена пробелами) длиной 8 байт

char ut_name[8];

A8

Строка ASCII (дополнена пробелами) длиной 8 байт

char ut_host[16];

A16

Строка ASCII (дополнена пробелами) длиной 16 байт

long ut_time;

1

«Длинное» целое значение со знаком (может и не совпадать с размером значения «long» на конкретной машине)
<


Шаблоны созданы, теперь используем их в настоящей программе:

шаблон, который мы собираемся передать unpack()

Stemplate = "А8 А8 А16 1";

ft используем pack(), чтобы определить размер (в байтах) каждой записи

Srecordsize = length(pack($template,()));

ft открываем файл

open(WTMP, "/var/adrc/wtmp") or die "Невозможно открыть wtT.D:$! \i":

# считываем его по одной записи

while (read(WTMP,SrecordSrecordsize)) {

# распаковываем, используя шаблон

($tty. $narne. $host $time)=unoack($temolate. Srecorc).

# специальным образом обрабатываем записи

if (Sname and substr($name. 0.1) г.е "\0"){

print "$tty:$name:$nobt : "

scalar localtime($time),"\n"; }

else <

print "$tty:(logout).(logout):",

scalar localtime(Stime),"\n";

i i

}

tt закрываем файл close(WTMP);

Вот как выглядит вывод этой маленькой программы:

":reboot::Мол Nov 17 15:24:30 1997

:0:dnb::0:Mon Nov 17 15:35:08 1997

ttyp8:user:host.mcs.anl.go:Mon Nov 17 18:09:49 1997

ttyp6:dnb:limbO-114.ccs.ne:Mon Nov 17 19:03:44 1997

ttyp6:(logout):(logout):Mon Nov 17 19:26:26 1997

ttyp1:dnb:traal-22.ccs.neu:Mon Nov 17 23:47:18 1997

ttyp1:(logout):(logout):Tue Nov 18 00:39:51 1997

Приведем пару комментариев:

В SunOS завершение работы с терминалов определенного типа отмечается символом с кодом 0 в первой позиции, поэтому:

if ($name and substr($name,1,1) ne "\0")

{

read() принимает в качестве третьего аргумента количество байт, которые нужно прочесть. Вместо того чтобы жестко определить размер записи как «32», мы воспользовались удобным свойством функции pack(). Если этой функции передать пустой список, то она возвращает пустую или заполненную пробелами строку размером, совпадающим с размером записи. Это позволяет передать функции pack() произвольный шаблон и узнать ее размер:

$recordsize = length(pack($template,()));



Вызов внешней программы



Работа с файлами wtmp - настолько распространенная задача, что в Unix есть специальная команда под названием last, предназначенная для вывода двоичных файлов в формате, удобном для человека. Вот образец ее вывода, показывающий примерно те же данные, что и в предыдущем примере:



dnb ttyp6 traal-22.ccs.neu Mon Nov 17 23:47 - 00:39 (00:52)

dnb ttypl traal-22.ccs.neu Mon Nov 17 23.47 - 00:39 (GO'52

dnb ttyps l:mbo-114.ccs.ne Mon Nov 17 19:03 - 19:26 (00'22)

user ttypS host.mcs.anl.go Mon Nov 17 18:09 - crash (27+11:50)

dnb '0 :0 Mo Nov 17 15:35 - 17:35 (4»P2 PC '

reboot " Mon Nov 17 15:24

Мы свободно можем вызывать программы, такие как last из Perl. Эта программа выводит все уникальные имена пользователей, найденные в текущем файле wtmp:

open( LAST. ' Slasrexec j"') or 02 "Невозможно Запустить

Sastexec :$!';:' while(<LAST>){

$user = (solit)[0]:

print "$user"."\n" unless exisfs $seen{$use"};

$seen{$user}='': } close(LAST) or die "Невозможно правильно закрыть канал:$!\п":

Так зачем же применять этот метод, если unраск() делает все, что нам нужно? Из-за переносимости. Мы уже продемонстрировали, что формат файла wtmp в различных операционных системах отличается. Ко всему прочему, производитель может изменить формат wtmp, а это приведет к тому, что шаблоном unpackQ в его существующем виде нельзя будет пользоваться.

Но вы можете рассчитывать на то, что команда last, читающая данный формат, будет присутствовать на вашей системе, независимо от каких-либо изменений формата. В случае применения метода unpack() придется создать и поддерживать различные строки шаблонов для каждого формата файла wtmp, который планируется использовать.

Самый большой недостаток такого метода по сравнению с unpack() -это увеличение сложности анализа полей, выполняемого в программе. В случае с unpack() все необходимые поля извлекаются автоматически. При использовании last можно столкнуться с данными, которые сложно разобрать при помощи split() или регулярных выражений:

user console Weo Oct 14 20:35 - 20:37 (00:01)

user pts/12 208.243,191.21 Wed Oct 14 09:19 - 18:12 (08:53)

user pts/17 208.243.191,21 Tue Oct 13 13:36 - 17:09 (03:33)

reboot system boot Tue Oct 6 14:13

На первый взгляд, не легко разобраться с полями, но любая программа, анализирующая подобный вывод, должна уметь правильно обрабатывать пропуски в первой и четвертой строках. Можно по-прежнему использовать unpack(), чтобы разделить эти данные, так как поля в нем имею риксированную ширину, но это не всегда возможно.



Использование API операционной системы для ведения журналов

Давайте перейдем к службе Event Log Service Windows NT/2000, чтобы рассмотреть этот подход. Как мы уже упоминали, в этом случае, к сожалению, журналы хранятся не в текстовых файлах. Самый лучший и единственный поддерживаемый способ, позволяющий добраться до этих данных, заключается в применении набора специальных API-вызовов. Большинство пользователей для получения этих данных полагаются на программу Event Viewer.

К счастью, существует модуль, написанный Джесси Доэрти (Jesse Dougherty) (и обновленный Мартином Поли (Martin Pauley) и Бретом Гиддингсом (Bret Giddings)), обеспечивающий простой доступ к API-вызовам Event Log. Вот простая программа, которая выводит список событий из журнала System в формате, подобном syslog. Позже мы подробно рассмотрим более сложную версию этой программы.

use Win32::EventLog;

П

у каждого события есть тип, вот как выглядят самые

и распространенные типы %type = (1 => "ERROR",

2 => "WARNING", 4 =<• "INFORMATION", 8 => "AUDIT_SUCCESS", 16 => "AUDIT_FAILURE");

# если это значение установлено, мы также получаем полный текст

# каждого сообщения при каждом вызове

Read() $Win32::EventLog::GetMessageText = 1;

fl открываем журнал событий

System Slog = new Win32::EventLog("System") or die

 "Невозможно открыть системный журнал:$~Е\п";

# читаем его по одной записи, начиная с первой

while ($log->Read((EVENTLOG_SEQUENTIAL_READ|EVENTLOG_FORWARDS_READ),

1,$entry)){

print scalar localtime($entry->{TimeGenerated})." ";

print $entry->{Computer}."[".($entry->{EventID} &

Oxffff)."] ";

print Sentry->{Source}.":".$type{$entry->{EventType}}; print $entry->{Message};

}

В NT/2000 существуют также утилиты, работающие из командной строки, такие как last, выводящие события из журнала в текстовом виде. Позже мы посмотрим на эти утилиты в действии.






Информация о модулях из этой главы


Модуль

Идентификатор UaCPAN

Версия

Win32 : : EvcntLog (распространяется с ActivePerl)   0.062
Logfile: : Rotate PAULG 1.03
Getopt : : Long (распространяется с Perl)   2.20
Time : : Local (распространяется с Perl)   1.01
SyslogScan RHNELSON 0.32
DB_File (распространяется с Perl) PMQS 1.72
FreezeThaw ILYAZ 0.3
Sys : : Hostname (распространяется с Perl)     
Fcntl (распространяется с Perl)   1.03
DBI TIMB 1.13



Проблемы с пространством на диске


Недостаток программ, ведущих полезные и подробные журналы, заключается в том, что для хранения этих данных нужно место на диске. Это касается всех трех операционных систем, рассмотренных в данной книге: Unix, MacOS и Windows NT/2000. Среди них, вероятно, в NT/2000 это вызывает меньше всего проблем, потому что центральный механизм ведения журналов имеет встроенную поддержку автоматического отсечения. В MacOS центрального механизма ведения журналов нет, зато можно запустить несколько серверов, которые с удовольствием выведут в журналы достаточно данных, чтобы заполнить пространство на диске, дай им только такую возможность.

Обычно задача поддержания приемлемого размера для журналов ложится на плечи системного администратора. Большинство производителей Unix предоставляют некий механизм управления размерами журналов вместе с операционной системой, но он часто обслуживает только определенный набор журналов на машине. Как только на машине появляется новая служба, ведущая свой отдельный журнал, возникает необходимость подправить (или даже отбросить) используемый механизм.

Ротация журналов

Распространенное решение проблемы с дисковым пространством - ротация журналов. (Необычное решение мы рассмотрим позже в этом разделе). По истечении определенного времени или после того, как будет достигнут определенный размер файла, текущий журнал будет переименован, например, в logfile.O. Последующая запись будет производиться в пустой файл. В следующий раз процесс повторяется, но сперва резервный файл (logfile.O) переименовывается (например в logfile.l). Этот процесс повторяется до тех пор, пока не будет создано определенное количество резервных файлов. После этого самый старый резервный файл удаляется. Вот как выглядит графическое представление такого процесса.

Этот метод позволяет отвести под журналы приемлемое конечное дисковое пространство. Обратите внимание на способ ротации журналов и функции Perl, необходимые для выполнения каждого шага (табл. 9.2).

Таблица 9.2. Способ ротации журналов из Perl







Процесс



Perl

Переименуйте старые журналы, присвоив им следующий номер.

renamed или &File: :Copy: :move() если переносить файлы с одной файловой системы на другую.

Если необходимо, сообщите процессу, создающему файл журнала, о необходимости закрыть текущий файл и приостановить запись до тех пор, пока она не будет разрешена.

kill () для программ, принимающих сигналы, system () или (обратные кавычки), если необходимо вызвать для этого другую программу.

Скопируйте или переместите файлы журналов, которые сейчас использовались, в другой файл.

&File: : Сору для копирования, rename( ), чтобы переименовать (или &File: :Copy: :move() при перемещении с одной файловой системы на другую).

Если необходимо, урежьте текущий файл журнала.

truncate () или open (FILE, "> filename").

Если необходимо, пошлите сигнал процессу о необходимости приостановить запись в журнал.

Шаг 2 из этой таблицы.

При желании сожмите или обработайте скопированный файл.

system( ) или обратные кавычки для запуска программы сжатия или другого программного кода, выполняющего обработку.

Удалите самые старые копии файлов.

stat( ), чтобы выяснить размер файла и даты, unlink( ) для удаления файлов.
На эту тему существует много вариаций. Все, кому не лень, писали собственные сценарии для ротации журналов. Так что не удивительно, что такой модуль существует. Рассмотрим модуль Logf lie: .Rotate Пола Гэмпа (Paul Gampe).

Logfile: : Rotate использует объектно-ориентированный подход для создания нового экземпляра объекта для журнала и для выполнения методов этого экземпляра. Сначала мы создаем новый экземпляр с заданными параметрами (табл. 9.3).

Таблица 9.3. Параметры Logflle::Rotate




Параметр



Назначение

File

Имя файла журнала для ротации

Count (необязательный, по умолчанию: 7)

Число хранимых копий файлов

Gzip (необязательный, по умолчанию: путь, найденный при сборке Perl)

Полный путь к программе сжатия gzlp

Signal

Код, выполняемый после завершения ротации, как в шаге 5 (табл. 9.2)
<


Вот небольшой пример программы, в которой используются эти параметры:

use Logfile: .'Rotate;

Slogfile = new Logfile:;Rotate(

File => "/var/adm/log/syslog", Count => 5,

Gzip => "/usr/local/bin/gzip", Signal => sub (

open PID, "/etc/syslog.pid" or

die "Невозможно открыть pid-фа/.л :$'\n"; chomp($pid = <PID>); close PID;

# сначала надо проверить допустимость kill 'HUP', $pid; } ):

В результате выполнения этого фрагмента программы указанный журнал будет заблокирован и будет подготовлен модуль для ротации данного журнала. После того как этот объект создан, сама ротация журнала не представляет никакого труда:

$logfile->rotate(); undef Slogfile;

Строка undef нужна для того, чтобы убедиться, что файл журнала будет разблокирован после ротации (он заблокирован до тех пор, пока существует объект журнала).

Как говорится в документации, если с модулем работает привилегированный пользователь (например, пользователь root), необходимо кое-что учитывать. Во-первых, Logf lie. : Rotate прибегает к системному вызову для запуска программы gzip, что является потенциальной дырой в безопасности. Во-вторых, подпрограмма Signal должна быть реализована «оборонительным» способом. В предыдущем примере мы не проверяли, что идентификатор процесса, полученный из /etc/sys log.pid, действительно является идентификатором процесса для syslog. Лучше было бы использовать таблицу процессов, о чем мы говорили в главе 4 «Действия пользователей», перед тем как посылать сигнал через kill(). Более подробно советы по «защищенному» программированию приведены в главе 1 «Введение».



Кольцевой буфер



Мы только что рассмотрели традиционный способ ротации журналов для контроля за пространством, занимаемым постоянно растущими журналами. Позвольте представить вам более необычный подход, который вы можете добавить в свою копилку.

Вот обычный сценарий: выполняется отладка сервера, который выводит целый поток данных в журнал. Нас интересует только малая часть всех этих данных, вероятно, только те строки, которые выводятся сервером после выполнения определенных тестов на определенном клиенте. Если сохранять в журнале весь вывод, как обычно, это быстро заполнит жесткий диск. Ротация журналов с нужной частотой при таком количестве выводимых данных замедлит работу сервера. Что же делать?



Я написал программу bigbuffy для решения этой головоломки, применив совершенно незамысловатый подход, bigbuffy считывает построчно поступающие на вход данные. Эти строки сохраняются в кольцевом буфере определенного размера. Когда буфер заполняется, он начинает вновь заполняться с вершины. Этот процесс чтения-записи продолжается до тех пор, пока bigbuffy не получит сигнал от пользователя. Получив сигнал, программа сбрасывает текущее содержимое буфера в файл и возвращается в свой нормальный цикл. На диске же остается лишь «окошко» в потоке данных из журнала, в котором показаны только те данные, которые нужны.

bigbuffy

можно использовать в паре с программой наблюдения за службой (подобной тем, которые можно найти в главе 5 «Службы имен TCP/IP»). Как только наблюдающая программа (монитор) замечает проблему, она может послать сигнал bigbuffy сбросить содержимое буфера на диск. Теперь у нас есть выдержка из журнала, относящаяся

как раз к нужной проблеме (считаем, что буфер достаточно велик и монитор вовремя ее заметил).

Вот упрощенная версия bigbuffy. Этот код длиннее примеров из предыдущей главы, но он не очень сложный. Мы будем его использовать в качестве трамплина для разрешения некоторых важных вопросов, таких как блокировка ввода и безопасность:

Souffsize = 200;

размер кольцевого буфера по умолчанию (строчках) use Getopt::Long;

 анализируем параметры GetOptions("buffsize=i" => \$Duffsize, "dumpfile=s" => \$dumpfile);

устанавливаем обработчик сигнала и инициализируем счетчик &setup;

 простой цикл прочитать строку - сохранить строку

while (<>){

и помещаем строку в структуру данных. Заметьте, мы делаем 8 это сначала, даже если получаем сигнал. Лучше записать 8 лишнюю строчку, чем потерять строку данных, если в 8 процессе сброса данных что-то пойдет не так.

$buffer[$whatline] = $_;

8 куда деть следующую строку? (Swhatline %= $buffsize)++;

8 если получаем сигнал, сбрасываем текущий буфер if ($dumpnow) {



&dodump(); } }

sub setup {

die "ИСПОЛЬЗОВАНИЕ: $0 [--buffsize=<lines>] --dump-ile=<filename>" unless (length($dumpfile));

$SIG{ 'USR1'} = \&di;mpnow; n устанавливаем обработчик

Swhatline = 1; и начальная строка кольцевого буфера

простой обработчик сигнала, который просто устанавливает фла-8 исключения, см. perlipc(l) sub dumpnow {

Sdumpnow = 1;

}

 флаг, существует ли уже файл my(@firststat,@secondstat); п для хранения вывода Istats

Sdumpnow = 0; № сбрасываем флаг и обработчик сигнала $SIG{ 'USR1'} = \&dumpnow;

if (-e Sdumpfile and (! -f Sdumpfile or -1 Sdurripfile)) {

warn "ПРЕДУПРЕЖДЕНИЕ: файл для сброса данных существует и не является, обычным текстовым файлом, пропускаем сброс данных.\п";

return undef; }

# необходимо принять специальные меры предосторожности при

# дописывании. Следующий набор операторов "if" выполняет

# несколько проверок при открытии файла для дописывания if (-e Sdumpfile) {

Sexists = 1;

unless(@firststat = Istat $dumpfile){

warn "Невозможно выяснить состояние Sdumpfile, пропускаем сброс данных.\n";

return undef; } if ($firststat[3] != 1) {

warn "Sdumpfile - жесткая ссылка, пропускаем сброс данных.\n";

return undef; } }

unless (open(DUMPFILE, "$dumpfile")){

warn "Невозможно открыть Sdumpfile для дописывания,

пропускаем сброс данных.\п"; return undef; > if (Sexists) {

unless (@secondstat = Istat DUMPFILE){

warn "Невозможно выяснить состояние открытого файла Sdumpfile,

пропускаем сброс данных.\п"; return undef; }

if ($firststat[0] != $secondstat[0] or

# проверяем номер устройства $firststat[1] != $secondstat[1] or

 проверяем mode $firststat[7] != $secondstat[7]) tt проверяем размеры {

warn "ПРОБЛЕМА БЕЗОПАСНОСТИ: Istats не совпадают,

пропускаем сброс данных,\п"; return undef:

}

Sline = Swhatline;

print DUMPFILE "-".scalar(Iocaltime). C'-"x50)."\n";

do < Проблемы с пространством на диске 357



И если буфер не полный

last unless (defined $buffer[$line]) print DUMPFILE $buffer[$line];

Sline = (Sline == Sbuffsize) 9 1 : $iine+l; } while (Siine '= Swhatline);

close(DUMPFILE):

П проталкиваем активный буфер, чтобы не повторяв данные

# при последующем сбросе их в файл $whatline = 1; Sbuffer = ();

return 1;

}

Подобная программа может найти несколько интересных применений.



Блокировка ввода в программах обработки журналов



Я уже говорил, что это упрощенная версия программы bigbuffy. С упрощением реализации, в особенности на различных платформах, связана неприятная особенность этой версии: во время сброса данных т диск она не может продолжать считывать ввод. Во время сброса буфера программе, посылающей свой вывод bigbuffy, операционная система может дать указание приостановить операции, пока не будет очищен ее буфер вывода. К счастью, сброс данных происходит быстро v. окно, в котором это может произойти, будет очень маленьким, но это все равно неприятно.

Вот два возможных решения этой проблемы:

Переписать bigbuffy, используя двойную буферизацию и многозадачность. Вместо одного буфера можно создать два. Во время получения сигнала программа будет записывать журнал во второй буфер до тех пор, пока дочерний процесс или другой поток обрабатывает сброс данных из первого буфера. При получении следующей сигнала буферы вновь меняются местами.

Переписать bigbuffy, чтобы разделить чтение и запись при сброс  данных в файл. Самая простая версия этого подхода предполагает что несколько строк записываются в файл каждый раз после про чтения новой строки. Это может оказаться не простым делом, если  журнал «разорван» и не поступает постоянным потоком. Вряд л! кому-то захочется ждать новую строку вывода для того, чтобы можно было сбросить буфер на диск. Так что придется использовать тайм-ауты или некий механизм внутренних часов, чтобы справиться с этой проблемой.

Оба этих подхода трудно реализовать так, чтобы они были преносимыми между различными платформами, отсюда и упрощенная версия; приведенная в книге.



Безопасность в программах, обрабатывающих журналы

Вы могли заметить, что в bigbuffy операциям открытия файлов вывода и записи в них уделяется внимания больше, чем обычно. Это пример защищенного (оборонительного) стиля программирования, о котором упоминалось уже в разделе «Ротация журналов». Если эта программа предназначена для отладки сервера, почти наверняка она будет запущена привилегированным пользователем. Очень важно продумать все ситуации, которые могут привести к тому, что программой кто-то злоупотребит.

Например, представьте ситуацию, когда файл, в который выводятся данные, был злонамеренно заменен ссылкой на другой файл. Если наивно открыть и записать данные в этот файл, можно обнаружить, что мы перезаписали какой-нибудь важный файл, например /etc/passwd. Даже если мы проверим файл вывода данных перед самым его открытием, злоумышленник может подменить его перед тем, как мы действительно начнем записывать в него данные. Во избежание таких неприятностей можно использовать такой сценарий:

Мы проверяем, существует ли файл, в который выводятся данные. Если да, мы выполняем lstat(), чтобы получить о нем информацию.

Открываем файл в режиме до записи.

Перед тем как собственно записать в него данные, мы выполняем lstat() для открытого файлового дескриптора и проверяем, тот же это файл, что мы ожидаем, или нет. Если это другой файл (т. е. кто-то заменил его ссылкой прямо перед открытием), мы не записываем в него данные и выводим соответствующее предупреждение. Этот шаг позволяет избежать состояния перехвата, о котором говорилось в главе 1.

Если дописывать данные не надо, то можно открыть временный файл со случайным именем (чтобы его нельзя было угадать заранее) и потом переименовать его.

Подобные «уловки» необходимы в большинстве Unix-систем, поскольку первоначально Unix создавался без особой заботы о безопасности. Брешь в безопасности, связанная с символическими ссылками, не является проблемой в NT4, т. к. они являются малоиспользуемой частью подсистемы POSIX, не проблема это и в MacOS, поскольку тут не существует понятия «привилегированный пользователь».






Рекомендуемая дополнительная информация


«Essential System Administration»,

(2nd Edition), Eleen Frisch (O'Reilly, 1995). А книге есть хорошее, краткое введение в syslog.

http://www.heysoft.de/index.htm -

домашняя страница Франка Хэйне (Frank Heyne) - человека, предоставляющего программное обеспечение для анализа журнала событий в Win32. Также здесь есть хороший список часто задаваемых вопросов по Event Log.

http://www.le-berre.com/ -

домашняя страница Филиппа Ле Вера (Philippe Le Berre); содержит отличный отчет по использованию Win32 EventLog и других пакетов для Win32.

«Managing NT Event Logs with Perl for Win32»,

Bob Wells, Windows NT Magazine, February/March 1998.

«Practical Unix & Internet Security»,

(2nd Edition), Simson Garfinkel, Gene Spafford (O'Reilly, 1996). Еще одно хорошее (и несколько более подробное) введение в syslog, также содержит информацию по tcpwrappers.

«Windows NT Event Logging»,

James D. Murray (O'Reilly, 1998).



Текстовые журналы


Журналы бывают разных типов, следовательно, нам нужно использовать различные подходы к их обработке. Самые распространенные журналы - полностью состоящие из строк текста. Популярные серверные пакеты, такие как Apache (веб), INN (новости Usenet) и Sendmail (электронная почта) записывают в журналы огромное количество текста. Большая часть журналов на Unix-машинах выглядит одинаково, потому что все они создаются одной и той же программой, известной под именем syslog.

Файлы, созданные syslog, можно считать обычными текстовыми файлами.

Вот простая программа на Perl, ищущая слово «error» в текстовом файле журнала:

open(LOG,"logfile") or die "Невозможно открыть журнал:$!\n"; while(<LOG>){

print if /\ber ror\b/i

}

close(LOG):

Тем, кто хорошо знает Perl, вероятно не терпится сократить ее до одной строки. Пожалуйста:

perl -ne 'print if /\berror\b/i' logfile