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


         

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



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

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

Время, потраченное на получение навыков работы с регулярными выражениями, окупится с лихвой и не раз. Регулярные выражения лучше всего изучать по книге Джеффри Фридла (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 проводят его в жизнь.

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

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



Содержание Назад Вперед