- PVSM.RU - https://www.pvsm.ru -
Как известно каждому, кто хоть раз подписывался на рассылки по ИБ, количество найденных за день уязвимостей часто превышает возможности человека по их разбору. Особенно, если серверов — много, особенно если там зоопарк из ОС и версий.
В этом топике я расскажу о том, как мы решили эту проблему. И да, Perl* жив )
При проектировании системы мы с mkhlystun [1]** решали два параллельные задачи:
Обе эти активности преследовали общую цель: правильно расставить приоритеты при обновлениях и выполнить эти обновления в гарантированный даунтайм, чего мы и достигли.
CREATE TABLE hosts (
hostname character varying(255) NOT NULL PRIMARY KEY,
os character varying(255),
pkg_id integer[]
);
CREATE INDEX hosts_pkg_id_idx ON hosts USING gin (pkg_id);
CREATE TABLE pkg (
id SERIAL NOT NULL PRIMARY KEY,
name character varying(255) NOT NULL
);
CREATE UNIQUE INDEX pkg_name_idx ON pkg USING btree (name);
CREATE TABLE vulners (
id character varying(255) NOT NULL PRIMARY KEY,
cvss_score double precision DEFAULT 0.0 NOT NULL,
cvss_vector character varying(255),
description text,
cvelist text
);
CREATE TABLE v2p (
pkg_id integer NOT NULL REFERENCES pkg(id),
vuln_id character varying(255) NOT NULL REFERENCES vulners(id)
);
В таблицу hosts заносится информация о хостах (один хост = одна запись), параллельно заполняется информация в pkg (один пакет — одна запись), во избежание дублирования информации. Массив был выбран исторически, жить с ним вполне можно.
Параллельно в таблицу vulners заносится информация о текущих уязвимостях для пакетов, таблица связи v2p позволяет делать привязку many-to-many.
#!/usr/bin/perl
# (C) kreon 2016
use strict;
use warnings;
use JSON;
# config
our %grabs = (
'centos|oraclelinux|redhat|fedora' => q(rpm -aq),
'debian|ubuntu' => q(dpkg-query -W -f='${Package} ${Version} ${Architecture}n'),
'osx' => q(pkgutil --pkgs)
);
our %unames = (
'linux' => q(lsb_release -a),
'darwin' => q(echo "Distributor ID: OSX")
);
# global vars
our $hostname = `hostname -f`;
our ($vercmd, $grabcmd, $operatingsystem, $version);
# do uname
my $uname = `uname`;
chomp $uname;
foreach (keys %unames) {
$vercmd = $unames{$_} if $uname =~ /$_/i;
}
die "Version CMD not found" unless $vercmd;
# do version check
foreach (`$vercmd`) {
chomp;
/^Distributor ID:s*(S[Ss]+)$/ and $operatingsystem = $1;
/^Release:s*(S[Ss]+)$/ and $version = $1;
}
die "Opetating System not found" unless $operatingsystem;
foreach (keys %grabs) {
$grabcmd = $grabs{$_} if $operatingsystem =~ /$_/i;
}
# grab pkgs
die "Opetating System not found" unless $grabcmd;
my @pkgs;
foreach (`$grabcmd`) {
chomp;
push @pkgs, $_;
}
chomp $hostname;
my $result = {
hostname => $hostname,
os => $version ? qq($operatingsystem $version) : $operatingsystem,
pkgs => [ sort @pkgs ]
};
#
print JSON->new->encode($result);
# done
1;
Для тех, кто не знает Perl: просто последовательно выполняются команды hostname -f, lsb_release -a и rpm -aq|dpkg-query -W, все это упаковывается в JSON и выводится для отправки в очередь сообщений.
#!/usr/bin/perl
# (C) kreon 2016
use strict;
use warnings;
use JSON;
use DBI;
use constant DB => 'dbi:Pg:dbname=pkgs';
# 0. create connection
my $dbh
= DBI->connect( DB, "", "", { RaiseError => 1, AutoCommit => 0 } );
# 1. read from stdin and parse
my $data = JSON->new->decode( join( "", <STDIN> ) );
# if data == array parse foeach
if ( ref($data) eq "ARRAY" ) {
foreach (@$data) {
parse_host($_);
}
}
else {
parse_host($data);
}
# Done
1;
### SUBS ###
sub parse_host {
$_ = shift;
# do parse packages
my ( $hostname, $os, @pkgs )
= ( $_->{hostname}, $_->{os}, @{ $_->{pkgs} } );
my @pkgids;
eval {
foreach (@pkgs) {
my $sth = $dbh->prepare("SELECT id FROM pkg WHERE name=?");
$sth->execute( ($_) );
if ( my ($id) = $sth->fetchrow_array ) {
push @pkgids, int($id);
}
else {
$dbh->do( "INSERT INTO pkg (name) VALUES(?)", undef, $_ );
push @pkgs, $_;
}
}
};
$dbh->rollback and die "$@" if $@;
$dbh->commit;
# do parse host
eval {
my $sth = $dbh->prepare("SELECT os FROM hosts WHERE hostname=?");
$sth->execute( ($hostname) );
if ( my ($os2) = $sth->fetchrow_array ) {
if ( lc($os2) ne lc($os) ) {
$dbh->do( "UPDATE hosts SET os=? WHERE hostname=?",
undef, $os, $hostname );
}
}
else {
$dbh->do( "INSERT INTO hosts (hostname, os) VALUES(?, ?)",
undef, $hostname, $os );
}
};
$dbh->rollback and die "$@" if $@;
$dbh->commit;
# do set packages
eval {
$dbh->do( "UPDATE hosts SET pkg_id=? WHERE hostname=?",
undef, [@pkgids], $hostname );
};
$dbh->rollback and die "$@" if $@;
$dbh->commit;
}
Данный скрипт получает на вход json, полученный из очереди, а затем раскладывает его в базу в три этапа:
— Сначала кладет пакеты, проверяя их уникальность
— Затем кладет хосты, обновляя версию при необходимости
— Затем связывает хосты и пакеты через массив
Когда мы искали способ аудировать наши пакеты, мы долго перебирали варианты, пока на какой-то конфе нам на глаза не попалась визитка vulners [2]. Это агрегатор уязвимостей, который делают isox [3] и videns [4]. Я связался с ними и попросил помочь. Итогом стало Audit API [5].
POST /api/v3/audit/audit/ HTTP/1.0
Host: vulners.com
Content-Type: application/json
Content-Length: 377
{ "os":"CentOS", "version":"7", "package":["kernel-3.10.0-229.el7.x86_64"]}
В ответ сервер отдаст json со списом текущих уязвимостей в следующем формате:
{
"result": "OK",
"data": {
"packages": {
"kernel-3.10.0-229.el7.x86_64": {
"CESA-2015:2152": [
{
"package": "kernel-3.10.0-229.el7.x86_64",
"providedVersion": "0:3.10.0-229.el7",
"bulletinVersion": "3.10.0-327.el7",
"providedPackage": "kernel-3.10.0-229.el7.x86_64",
"bulletinPackage": "kernel-3.10.0-327.el7.x86_64.rpm",
"operator": "lt",
"bulletinID": "CESA-2015:2152"
}
],
"CESA-2015:1978": [
{
"package": "kernel-3.10.0-229.el7.x86_64",
"providedVersion": "0:3.10.0-229.el7",
"bulletinVersion": "3.10.0-229.20.1.el7",
"providedPackage": "kernel-3.10.0-229.el7.x86_64",
"bulletinPackage": "kernel-3.10.0-229.20.1.el7.src.rpm",
"operator": "lt",
"bulletinID": "CESA-2015:1978"
},
// skipped
],
"CESA-2016:0064": [
{
"package": "kernel-3.10.0-229.el7.x86_64",
"providedVersion": "0:3.10.0-229.el7",
"bulletinVersion": "3.10.0-327.4.5.el7",
"providedPackage": "kernel-3.10.0-229.el7.x86_64",
"bulletinPackage": "kernel-3.10.0-327.4.5.el7.src.rpm",
"operator": "lt",
"bulletinID": "CESA-2016:0064"
},
// skipped
],
// skipped
],
// skipped
"cvss": {
"score": 10.0,
"vector": "AV:NETWORK/AC:LOW/Au:NONE/C:COMPLETE/I:COMPLETE/A:COMPLETE/"
},
"cvelist": [
"CVE-2014-9644",
"CVE-2016-2384",
// skipped
],
"id": "F777"
}
}
После обкатки API был написан код, который пушит список пакетов по OS и в ответ получает список уязвимостей и кладет их в базу.
#!/usr/bin/perl
# (C) kreon 2016
use strict;
use warnings;
use lib 'perl5';
use HTTP::Tiny;
use DBI;
use JSON;
use constant VULNERS_AUDIT_API => 'http://vulners.com/api/v3/audit/audit/';
use constant VULNERS_ID_API => 'http://vulners.com/api/v3/search/id/';
use constant DB => 'dbi:Pg:dbname=pkgs';
our %VULNS;
our $dbh;
our %pkgs = ();
# 0. connect to DB
$dbh = DBI->connect( DB, "", "", { RaiseError => 1, AutoCommit => 0 } );
# get all OS variations
my @os = get_os();
# for each OS get all packages and ask vulners for its vulnerabilities
foreach my $os (@os) {
eval {
my ( $o, $ver ) = split( / /, $os );
my $res = HTTP::Tiny->new->request(
'POST',
VULNERS_AUDIT_API,
{ headers => { 'Content-Type' => 'application/json' },
content => JSON->new->encode(
{ os => $o,
version => $ver,
package => [ get_packages($os) ]
}
)
}
);
if ( !$res->{success} ) {
die "HTTP Error: $res->{content}";
}
my $data = JSON->new->decode( $res->{content} );
my $vulns = $data->{data}->{packages};
return undef unless defined $vulns;
foreach ( keys %$vulns ) {
my $o = $vulns->{$_};
if ( defined( $pkgs{$_} ) ) {
$VULNS{ $pkgs{$_} } = [ keys %$o ];
}
}
};
print $@ if $@;
}
# Now get info on each vuln ID ( CESA, USN, etc ) ...
my @result;
my $res = HTTP::Tiny->new->request(
'POST',
VULNERS_ID_API,
{ headers => { 'Content-Type' => 'application/json' },
content => JSON->new->encode( { id => [ map {@$_} values %VULNS ] } )
}
);
if ( !$res->{success} ) {
die "HTTP Error: $res->{content}";
}
my $data = JSON->new->decode( $res->{content} );
foreach ( values %{ $data->{data}->{documents} } ) {
push @result,
{
id => $_->{id},
cvss_score => $_->{cvss}->{score},
cvss_vector => $_->{cvss}->{vector},
description => $_->{description},
cvelist => join( ', ', @{ $_->{cvelist} } ),
};
}
# Insert the data to DB
eval {
$dbh->do( "DELETE FROM v2p", undef );
$dbh->do( "DELETE FROM vulners", undef );
# insert prepared data to vulners table
foreach (@result) {
$dbh->do(
"INSERT INTO vulners (id, cvss_score, cvss_vector, description, cvelist) VALUES (?,?,?,?,?)",
undef,
$_->{id},
$_->{cvss_score},
$_->{cvss_vector},
$_->{description},
$_->{cvelist}
);
}
# and link pkg and vuls into v2p
foreach my $pkg_id ( keys %VULNS ) {
foreach my $vuln_id ( @{ $VULNS{$pkg_id} } ) {
$dbh->do( "INSERT INTO v2p(pkg_id,vuln_id) VALUES(?,?)",
undef, $pkg_id, $vuln_id );
}
}
};
$dbh->rollback and die "Error $@" if $@;
$dbh->commit;
# All done
1;
### SUBS ####
sub get_os {
my @os;
my $sth = $dbh->prepare("SELECT DISTINCT os FROM hosts");
$sth->execute();
while ( my ($os) = $sth->fetchrow_array ) {
push @os, $os;
}
return @os;
}
sub get_packages {
my $os = shift;
my $sth
= $dbh->prepare(
"select DISTINCT p.id,p.name FROM pkg p RIGHT JOIN hosts h ON (p.id=ANY(h.pkg_id)) WHERE h.os=?"
);
$sth->execute( ($os) );
my @pkgs;
while ( my ( $id, $name ) = $sth->fetchrow_array ) {
$pkgs{$name} = $id;
push @pkgs, $name;
}
return @pkgs;
}
Алгорим:
— Берем список OS из таблицы hosts
— По каждой OS получаем список пакетов
— Отправляем пакеты в Audit API, получаем список (id) уязвимостей
— Отправляем уязвимости в ID Api, получаем по каждой метаданные
— Пишем в базу метаданные уязвимостей в таблицу vulners
— Пишем в базу связи пакетов и уязвимостей в таблицу v2p
Поскольку основной нашей целью было получать список хостов для приоритетного обновления, первый отчет, который я сделал, был 'top10 hosts to update', выбранный по суммарному CVSS Score***.
#!/usr/bin/perl
# (C) kreon 2016
use strict;
use warnings;
use lib 'perl5';
use HTTP::Tiny;
use DBI;
use JSON;
use constant DB => 'dbi:Pg:dbname=pkgs';
our $dbh;
our @hosts;
# 0. connect to DB
$dbh = DBI->connect( DB, "", "", { RaiseError => 1, AutoCommit => 0 } );
# get top10 hosts
my $sth = $dbh->prepare("SELECT h.hostname,SUM(v.cvss_score) as sum FROM hosts h INNER JOIN pkg p ON(p.id=ANY(h.pkg_id)) INNER JOIN v2p vp ON(vp.pkg_id=p.id) INNER JOIN vulners v ON (v.id=vp.vuln_id) GROUP BY h.hostname ORDER BY sum DESC LIMIT 10");
$sth->execute();
while (my ($host, $sum) = $sth->fetchrow_array) {
push @hosts, { hostname => $host, score => $sum, pkgs => [] };
}
foreach (@hosts) {
$sth = $dbh->prepare("SELECT p.name,SUM(v.cvss_score) AS score FROM pkg p RIGHT JOIN hosts h ON (p.id=ANY(h.pkg_id)) INNER JOIN v2p ON (v2p.pkg_id=p.id) INNER JOIN vulners v ON (v.id=v2p.vuln_id) WHERE h.hostname=? GROUP BY p.name ORDER BY score DESC LIMIT 10");
$sth->execute(($_->{hostname}));
while(my ($pkg,$sum) = $sth->fetchrow_array) {
push @{$_->{pkgs}}, { package => $pkg, score => $sum };
}
}
print <<EOF
TOP 10 SERVERS TO UPDATE
EOF
;
foreach (@hosts) {
print <<EOF
--------------------------------------------------
Hostname: $_->{hostname}
Score : $_->{score}
Packages:
EOF
;
foreach (@{$_->{pkgs}}) {
print <<EOF
Name : $_->{package}
Score: $_->{score}
EOF
}
}
Данный код просто берет топ10 хостов и для каждого из них берет топ 10 пакетов с суммарным скором. Дальше можно создавать задачи и обновлять.
Уже больше года мы отправляем наши данные ребятам из Vulners. На текущий момент они постоянно аудируют более 30 000 уникальных пакетов. Что приятно, все найденные баги оперативно исправлялись, а скорость обработки каждой тысячи выросла с 30 секунд до 400 милисекунд. Именно благодаря им этот топик назван "… без регистрации и смс" )
Что касаемо бизнес-целей, то только с внедрением данной системы у нас начал появляться процесс постоянных обновлений. Обновить все — слишком большая задача для дежурного инженера, а обновить первые 10 — вполне посильная. За год мы уронили суммарный cvss score более чем вдвое, чего и вам желаем **** )
* — Так сложилось исторически, я начал писать автоматизацию на Perl и никто не успел меня остановить.
** — Миша не слишком активный пользователь хабра, но как инженер он незаменим :)
*** — Цифровая метрика опасности уязвимости, от 1.0 ( можно забить ) до 10.0 ( критичная уязвимость )
**** — Весь код можно найти тут: github.com/kreon/freeaudit [6]
P.S. И тем не менее — Perl жив!
Автор: kreon
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/linux/252348
Ссылки в тексте:
[1] mkhlystun: https://habrahabr.ru/users/mkhlystun/
[2] vulners: http://vulners.com/
[3] isox: https://habrahabr.ru/users/isox/
[4] videns: https://habrahabr.ru/users/videns/
[5] Audit API: https://vulners.com/audit
[6] github.com/kreon/freeaudit: https://github.com/kreon/freeaudit/
[7] Источник: https://habrahabr.ru/post/326084/
Нажмите здесь для печати.