原文地址:
https://www.bro.org/sphinx/frameworks/input.html
输入框架(Input Framework)
Bro提供了输入框架,让用户可以导入数据到Bro中去。数据可以读到Bro表中,或者转换为脚本可以处理的事件。
读取数据到表中
输入框架的最有趣的用例是读数据到一个Bro表中。默认情况下,输入框架读取的数据和由日志框架在Bro中写的数据是同样的格式(由tab分隔的ASCII文件)。
我们将用一个简单的例子说明将文件读入Bro的集中方法。我们假设我们想从一个包含服务器IP地址、时间戳以及块(block)的reason的黑名单(blacklist)最近中导入数据。
一个示例的输入文件如下所示(注意所有的区域都是用tab分隔的):
#fields ip timestamp reason
192.168.17.1 1333252748 Malware host
192.168.27.2 1330235733 Botnet server
192.168.250.3 1333145108 Virus detected
为了将一个文件读到Bro表中,需要定义两种record类型。一个包含构成表键的列的类型和名称,另一个包含构成表值的列的类型和名称。
在我们的例子中,我们想查找IP。因此,我们的key record仅包含服务器IP,所有其他的元素都作为表内容来存储。
两个record定义如下:
type Idx: record {
ip: addr;
};
type Val: record {
timestamp: time;
reason: string;
};
注意上面record定义中的域的名称必须和日志文件中‘#fields’行中定义的域相同,在本例中就是ip,timestamp,reason。另外注意到列的顺序是无关紧要的,因为每一个列都是由它的名称来标识的。
可以通过简单地调用Input::add_table方法来将日志文件读入表中:
global blacklist: table[addr] of Val = table();
event bro_init() {
Input::add_table([$source="blacklist.file", $name="blacklist",
$idx=Idx, $val=Val, $destination=blacklist]);
Input::remove("blacklist");
}
这里我们先创建一个空的表(用于存放blacklist中的数据)并用输入框架打开一个名为blacklist的输入流将数据读入到表中。最后又将输入流移除了,这是因为我们在数据读完之后不再需要它了。
因为有一些数据文件可能会非常大,所以输入框架是异步地工作的。为每个输入流创建一个新的线程,这个线程打开输入数据文件,将数据转化为Bro格式并将它发回给Bro主线程。
因此,数据不是立即可得的。在所有数据展示在表中之前可能要花费几毫秒到几秒不等的时间来将数据从数据源中取出,具体时间依赖于数据源的大小。也就是说当Bro在没有输入源或者是工作在非常小的抓取文件上的时候,它可能在数据展示在表中之前就已经终止了(因为Bro在导入线程结束之前就已经处理完所有的包了)。
之后顺序到来的对输入源的调用会进入队列直到前面的工作都已经完成。比如,调用了add_table和remove,在相继的两行中,remove在第一个read完成之前一直呆在队列中。
一旦输入框架结束从一个数据源中读取数据,它将触发Input::end_of_data事件。一旦这个时间已经获取了表中可获取的所有输入文件中的数据的时候。。
event Input::end_of_data(name: string, source: string) {
# now all data is in the table
print blacklist;
}
只要这个数据还在被读,这个表就继续使用。这可能在事件触发之前没能包含输入文件中的所有行。填好之后就可以像这样。。
if ( 192.168.18.12 in blacklist )
# take action
重读和流数据(Re-reading and streaming data)
对于很多数据源,比如很多blacklists,源数据是不断变化的。对这些情况,Bro输入框架支持处理变化的数据文件(changing data files)。
首先,最基础的方法就是刷新输入流。当输入流打开的时候(也就是说还没有通过调用Input::remove来去除它的时候),可以调用函数Input::force_update。这会触发对表的一次完整的刷新,任何的文件中变化了的元素都会被更新。在更新完成的时候,会触发Input::end_of_data事件。
在我们的例子中,是这样调用的:
Input::force_update("blacklist");
输入框架可以在它探测到输入文件变化的时候自动刷新表的内容。为了使用这个特性,你需要通过设置Input::add_table调用的mode选项指定一个non-default read模式。有效的值有:Input::MANUAL(默认的),Input::REREAD和Input::STREAM。就可以这样:
Input::add_table([$source="blacklist.file", $name="blacklist",
$idx=Idx, $val=Val, $destination=blacklist,
$mode=Input::REREAD]);
当使用reread模式的时候(如 $mode=Input::REREAD
),Bro会持续地检查输入文件是否已经发生变化。如果这个文件已经发生变化,为了反映当前状态,会重读这个文件并将Bro表中的数据更新。每一次探测到变化并且新数据被读到表中的时候,会触发end_of_data事件。
当使用streaming模式的时候(如 $mode=Input::STREAM
),Bro假设源数据是仅能在末尾追加的文件(append-only file),新数据将在其末尾追加。Bro不断地在文件末尾检查新数据,然后把新数据添加到表中。如果文件中的新的行(newer lines)和前面的行有相同的索引(index),它们将覆盖输出表中的值。因为流读取的特性(数据不断地往表中追加),所以当使用streaming模式的时候永远都不会触发end_of_data事件。
接收变化事件(receiving change events)
当重读(re-reading)文件的时候,能够知道源文件中的哪些行发生了变化该多好啊。
出于这个原因,当有一个新的数据项在表中添加、移除或者更改的时候,输入框架会触发一个事件。
事件就像这样(注意你可以在自己的Bro脚本中改变事件的名称):
event entry(description: Input::TableDescription, tpe: Input::Event,
left: Idx, right: Val) {
# do something here...
print fmt("%s = %s", left, right);
}
事件必须在add_table调用的$ev中指明:
Input::add_table([$source="blacklist.file", $name="blacklist",
$idx=Idx, $val=Val, $destination=blacklist,
$mode=Input::REREAD, $ev=entry]);
这里事件的description参数包含那些一开始应用给add_table调用的参数。因此,流的名字可以这样获取(description$name)。事件的tpe参数是一个包含发生了的变化的类型的一个enum型数据。
如果一个未曾出现在表中的一行被添加到了表中,tpe的值会变成Input::EVENT_NEW。在这个例子中left包含了已添加的表项的索引,right包含了已添加的表项的值。
如果在对一个文件进行重读或者流读取的时候,表中的一个已经存在的表项被修改,tpe的值会变成Input::EVENT_CHANGED。在这个情况下,left包含被修改的表项的索引,right包含表项被修改之前的值。原因是,当事件触发的时候,表已经被修改了。表中的当前值可以通过查询表值来弄清楚。因此,把表的新值和旧值进行比较也是可能的。
如果通过一次重读,表中的一个元素(本来存在的)不复存在了,tpe的值将变为Input::EVENT_REMOVED。在这个情况下,left包含索引并且right包含被移除元素的值。
在导入过程中过滤数据(Filtering data during import)
输入框架也允许用户在导入的过程中过滤数据。使用了断定函数。断定函数在新的元素被添加/改变/移除的时候调用。断定函数可以通过对要接受的变化返回true值或对要拒绝的变化返回false值来接受抑或是否决一个变化。更多的是,它可以在数据写入表之前就修改数据。
下面的例子会拒绝在表中添加项(那种在一个月之前就生成的项)。但它会接受对当前已经在表中的项的值的所有改变或者是移除。
Input::add_table([$source="blacklist.file", $name="blacklist",
$idx=Idx, $val=Val, $destination=blacklist,
$mode=Input::REREAD,
$pred(typ: Input::Event, left: Idx, right: Val) = {
if ( typ != Input::EVENT_NEW ) {
return T;
}
return (current_time() - right$timestamp) < 30day;
}]);
为了在元素导入的时候修改它们,断定函数(predicate function)可以操纵left和right。注意一下,断定函数在变化提交给表之前就调用了。因此,当一个表元素变化的时候(typ为Input::EVENT_CHANGED),left和right包含了旧的值。这允许断定函数在判断这些变化是否应该被允许之前先检验一下旧版本和新版本之间的变化。
不同的读者(different readers)
输入框架对不同种类的源数据文件智齿不同种类的读者。在这个时候,默认的读者读Bro日志文件格式的ASCII文件(有#fields头部行且用tab分隔的值)。但也有其他的读者。
raw读者读由指定记录分隔符(默认是newline)分隔的文件。内容以一行一行的形式返回,它可以用于读配置文件或者类似的文件,可能仅在事件模式且不是往表中读数据的时候才行。
二进制读者(binary reader)在读文件分析输入流的时候使用的,在处理文件分析输入流(file analysis input streams)的时候默认就是用的二进制读者。
参照读者(benchmark reader)被使用用来优化输入框架的速度。它可以生成任意数量的所有bro数据类型的且输入框架支持的半随机数据。
读取数据到事件中(reading data to events)
输入框架支持的第二种模式是从读数据到Bro事件中,而不是读到表中。
事件流和表流的工作机制在很多已经讨论过的地方非常相似。为了将前面例子中blacklist读入一个事件流,我们使用了Input::add_event函数。
type Val: record {
ip: addr;
timestamp: time;
reason: string;
};
event blacklistentry(description: Input::EventDescription,
t: Input::Event, data: Val) {
# do something here...
print "data:", data;
}
event bro_init() {
Input::add_event([$source="blacklist.file", $name="blacklist",
$fields=Val, $ev=blacklistentry]);
}
事件流的声明中,主要不同的地方是:事件流不需要单独的index和value声明,所有的数据类型都在record定义中提供了。
此外,事件流工作和表流极其相似且提供了绝大多数表流支持的选项。