登録およびSMSを使用しない現在の脆弱性の監査

エントリー



ISメーリングリストを購読したことがある人なら誰でも知っているように、1日に発見される脆弱性の数は、その人がそれらを解析する能力を超えることがよくあります。 特にサーバーが多数ある場合、特にOSとバージョンの動物園がある場合。



このトピックでは、この問題をどのように解決したかについて説明します。 そして、はい、Perl *は生きています:)



目標と目的



システムを設計する際、 mkhlystun **と私は 2つの並行問題解決しました。





これらのアクティビティは両方とも共通の目標を追求しました:更新を正しく優先順位付けし、保証されたダウンタイムでこれらの更新を実行することです。



作業スキーム





  1. 1時間に1回、各ホストは現在のパケットに関する情報を収集し、そのキューを送信します
  2. メッセージはキューから削除され、データベースに配置されます
  3. 3時間に1回、ロボットがデータベースにアクセスし、監査サービスを介してパケットを実行します
  4. 1日の結果に応じて、関心のあるすべての人に送信されるレポートが生成されます


基本スキーム



pkgs.sql
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) );
      
      







ホストの情報はホストテーブルに入力され(1ホスト= 1レコード)、 pkgの情報は情報の重複を避けるために並行して入力されます(1パケット-1レコード)。 アレイは歴史的に選択されたものであり、それと共に生きることは非常に可能です。



同時に、パッケージの現在の脆弱性に関する情報が脆弱性テーブルに入力され、 v2p通信テーブルは多対多のバインディングを許可します。



パケット情報収集



grabber.pl
 #!/usr/bin/perl # (C) Ivan Agarkov 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[\S\s]+)$/ and $operatingsystem = $1; /^Release:\s*(\S[\S\s]+)$/ 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を知らない人のために: ホスト名-flsb_release -aおよびrpm -aq | dpkg-query -Wコマンドは単純に連続して実行され、これらはすべてJSONにパッケージ化され、メッセージキューに出力されます。



JSONをベースに変換する



transform.pl
 #!/usr/bin/perl # (C) Ivan Agarkov 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を受信し、次の3つの段階でデータベースに分解します。



-最初にパッケージを配置し、その一意性を確認します

-次に、ホストを配置し、必要に応じてバージョンを更新します

-次に、アレイを介してホストとパケットをバインドします



監査



パッケージを聞く方法を探していたとき、ある種の会議でvulnersと呼ばれる名刺を見るまで、長い間オプションを並べ替えていました。 これはisoxvidensが行う脆弱性アグリゲーターです 。 私は彼らに連絡し、助けを求めました。 その結果がAudit APIでした。



監査API
脆弱性に関する情報を取得するには、jsonでパッケージblobを収集して/ api / v3 / audit / audit /に送信するだけで十分です。



 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によってパッケージのリストをプッシュし、それに応じて脆弱性のリストを受け取ってデータベースに入れるコードが作成されました。



audit.pl
 #!/usr/bin/perl # (C) Ivan Agarkov 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のリストを取得します

-各OSについて、パッケージのリストを取得します

-監査APIにパケットを送信し、脆弱性のリスト(ID)を取得します

-Api IDで脆弱性を送信し、各メタデータで受信します

-テーブル脆弱性の脆弱性メタデータのデータベースに書き込みます

-v2pテーブルの通信パッケージと脆弱性のデータベースに書き込みます



報告書



私たちの主な目標は優先度の高いアップデートのホストのリストを取得することだったので、最初に作成したレポートは「CVSSスコアの合計***に基づいて選択された「アップデートする上位10ホスト」」でした。



report.pl
 #!/usr/bin/perl # (C) Ivan Agarkov 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個のパケットを合計速度で取得します。 その後、タスクを作成して更新できます。



使用年の結果



1年以上にわたって、Vulnersからデータを送信しています。 現在、彼らは30,000以上のユニークなパッケージを常に監査しています。 嬉しいことに、見つかったすべてのバグはすぐに修正され、各1,000の処理速度は30秒から400ミリ秒に増加しました。 このトピックが「...登録とSMSなしで」と呼ばれるのは彼らのおかげです)



ビジネス目標に関しては、このシステムの導入によって初めて、絶え間ない更新のプロセスが現れ始めました。 すべてを更新することは、勤務中のエンジニアにとって非常に大きな作業であり、最初の10個を更新することは非常に現実的です。 年間を通して、合計cvssスコアが2回以上低下しました。



脚注と説明



* -それは歴史的に起こり、Perlで自動化の記述を始めましたが、だれも私を止められませんでした。

** -ミシャはHabrのあまりアクティブなユーザーではありませんが、エンジニアとしては不可欠です:)

*** -1.0(スコアリング可能)から10.0(重大な脆弱性)のデジタル脆弱性ハザードメトリック

**** -すべてのコードはここで見つけることができます: github.com/annmuor/freeaudit



PSそしてそれにもかかわらず-Perlは生きています!



All Articles