Boxの監査ログをAWS ElasticsearchServiceに投げ込む

## 条件 
* [Box](https://www.box.com/ja-jp/home)をBusiness以上で利用している

## 目的
* Boxの管理コンソールには出力されない詳細な監査ログを取りたい
    * Downloadされた
    * ファイルを更新した
    * 外部コラボレーターを作成した
    * etc .....

### 監査ログの置き場
* [AWS ElasticsearchService](https://aws.amazon.com/jp/elasticsearch-service/)
    * [CloudWatchLogs](https://aws.amazon.com/jp/cloudwatch/)でも良かったけど、可視化の観点で [Kibana](https://aws.amazon.com/jp/elasticsearch-service/kibana/)を使ってみたかった

### 実行環境とフロー
* AWS
    * CloudWatch　ルールでLambda(Node.js)を定周期起動
    * BoxApiを利用して、取得した監査ログをElasticsearchServiceに投げ込む
    * Kibanaで簡単に可視化
    
## Box側の設定
* BoxApiを利用するにはBoxアプリを作成する必要がある。
* 詳しい解説記事が既にQiitaにあるので[こちら](https://qiita.com/daichiiiiiii/items/7383b89cfa9dfc42b14d#installing-the-box-cli)参照して作成してください（ステップ4まで実行すれば大丈夫）

## AWS側の事前準備
* ElasticsearchServiceの作成
    * indexまで作っておきましょう
* S3バケットの用意
    * BoxAPIで監査ログを取得する際の次回読み込みポジションを保存しておく領域です。
    *  `当然S3じゃなくても大丈夫ですが今回はS3で行きます！`
* credentials の用意
    * IAMユーザのcredentialsを用意
        * :arrow_up: のS3バケットへのアクセス権

## ローカルで試してみよう
### npmインストール
* aws-sdk
* box-node-sdk
    * BoxのNode.jp用SDK
* elasticsearch
    * elasticsearchのNode.jp用SDK
* dotenv
    * さくっとやりたいので`serverless`は使わない
    * [Nodeの環境設定モジュール](https://qiita.com/ueokande/items/80048f886082fea5b776)　これでローカルと本番(lambda上の環境変数)を管理する

```
$ mkdir box-auditlogs-node-es
$ cd box-auditlogs-node-es
$ npm install aws-sdk box-node-sdk dotenv elasticsearch
```

### ソース
* 下記構成でそれぞれ作成してください

#### tree
```
.
├── index.js
├── main.js
├── .env
└── package.json
```

#### index.js
```javascript:index.js
'use strict';

const aws = require('aws-sdk');
const elasticsearch = require('elasticsearch');
const BoxSDK = require('box-node-sdk');

// AWS Setting
aws.config.region = process.env.AWS_REGION;
if (process.env.LOCAL) { // This part is unnecessary when not using profile
    var credentials = new aws.SharedIniFileCredentials({profile: process.env.LOCAL_PROFILE});
    aws.config.credentials = credentials;
}

// Box Setting
const boxConfig = JSON.parse(process.env.BOX_CONFIG);
boxConfig.boxAppSettings.appAuth.keyID = boxConfig.boxAppSettings.appAuth.publicKeyID;
const sdk = new BoxSDK(boxConfig.boxAppSettings);
const client = sdk.getAppAuthClient('enterprise', boxConfig.enterpriseID);

// Elasticsearch
var ES_INDEX = process.env.ES_INDEX; // Elasticsearch index name
var ES_TYPE = process.env.ES_TYPE; // Elsticsearch index type name
var ES_CLIENT = new elasticsearch.Client({
    host: process.env.ES_ENDPOINT
});

// S3 Setting
var s3 = new aws.S3();
var bucketName = process.env.S3_NEXT_POSITION_BUCKETNAME;
var fileName = 'next_position.txt';
var contentType = 'text/plain';

exports.handler = (event, context, callback) => {
    main();
};

async function main() {
    // get initial value [next_position] from S3 and generate parameters of BoxApi
    const s3_read_params = {
        Key: fileName,
        Bucket: bucketName,
    };
    var next_position = null;
    var box_params = {
        stream_type: 'admin_logs',
        limit: 500
    }

    try {
        const res = await s3.getObject(s3_read_params).promise();
        next_position = res.Body.toString('ascii');
        box_params.stream_position = next_position;
    } catch(err) {
        console.error(err);
        box_params.created_after = process.env.BOX_START_DATE;
    }

    // Call BoxApi
    client.events.get(box_params, function(err, stream) {
        if (err) {
            console.log("error");
        }
    }).then(event => {
        var next_position = event.next_stream_position;
        // Send to Elasticsearch
        if (event.chunk_size != 0){
            sendToES(event.entries);
        }
        // Upload the next_position file to S3
        var params = {
            Key: fileName,
            Body: next_position,
            Bucket: bucketName,
            ContentType: contentType,
        };
        s3.putObject(params, function(err, data) {
            if (err) {
                console.log("Error uploading data: ", err);
            } else {
                console.log("Successfully uploaded data to " + bucketName + "/" + fileName);
            }
        });
    });
}

// bulk send to Elasticsearch
function sendToES(records){
    var searchRecords = [];
    for(var i = 0; i < records.length; i++){
        var record = records[i];
        var header = {
            "index":{
                "_index": ES_INDEX,
                "_type": ES_TYPE,
            }
        };
        var searchRecord = {};
        searchRecords.push(header);
        searchRecords.push(record);
    };
    ES_CLIENT.bulk({
        "body": searchRecords
    }, function(err, resp){
        if(err){
            console.log(err);
        }else{
            console.log(resp);
        };
    });
};
```

#### main.js
* ローカルでlambdaっぽく呼ぶやつ

```javascript:main.js
var event = {
    version: '0',
    id: 'XXXXXXXX-XXXXX-XXXX-XXXXX-XXXXXXXX',
    'detail-type': 'Scheduled Event',
    source: 'aws.events',
    account: 'XXXXXXXXXX',
    time: '2019-01-30T06:27:04Z',
    region: 'ap-northeast-1',
    resources:
        [ 'arn:aws:events:ap-northeast-1:XXXXXXXXXX:rule/box-auditlog-exec' ],
    detail: {}
};

var context = {
    invokeid: 'invokeid',
    done: function(err,message){
        return;
    }
};

var lambda = require("./index");
lambda.handler(event,context);
```

#### .env
* ローカル環境変数を記述するので、こちらに自分の環境を記載してください

```config:.env
BOX_CONFIG={   "boxAppSettings": {     "clientID":  "   } }
BOX_START_DATE=2019-01-01T00:00:00+09:00
AWS_REGION=ap-northeast-1
S3_NEXT_POSITION_BUCKETNAME=mybucket
LOCAL=true
LOCAL_PROFILE=AWS_credentials_profile_name
ES_INDEX=Elasticsearch_Index_name
ES_TYPE=Elasticsearch_Type_name
ES_ENDPOINT=https://xxxxxxxxxxxxxxxx.ap-northeast-1.es.amazonaws.com/
```

* BOX_CONFIG:[これ](https://qiita.com/daichiiiiiii/items/7383b89cfa9dfc42b14d#%E3%82%B9%E3%83%86%E3%83%83%E3%83%973%E8%A8%AD%E5%AE%9A%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E3%81%97%E3%81%BE%E3%81%99)
* BOX_START_DATE:Boxを利用し始めた日時
* S3_NEXT_POSITION_BUCKETNAME:AWS側の事前準備で作成したバケット名
* `credentialを利用する場合` LOCAL:trueのままで大丈夫
* `credentialを利用する場合` LOCAL_PROFILE:credentialのprofile名
* ES_INDEX:Elasticsearchのインデックス名
* ES_TYPE:ElasticsearchのType名
* ES_ENDPOINT:Elasticsearchのエンドポイント

### 実行
```
$ node -r dotenv/config main.js
```

## うまくいけばKibanaでこんな感じで見ることができる
* グラフとかは適当に自分で作ってください
<img width="1391" alt="qiita1.png" src="https://qiita-image-store.s3.amazonaws.com/0/164563/18885ec6-be17-9283-7a64-0e3990ed2549.png">


## AWSにアップして動かしてみよう
### 事前準備
* lambda関数作成
    * ランタイム:Node.js V8.10
    * 環境変数:内容はローカルと同じ（LOCAL、LOCAL_PROFILEは必要無い）
  <img width="815" alt="qiita2.png" src="https://qiita-image-store.s3.amazonaws.com/0/164563/906392b6-8e4d-2700-4db1-e2a855a1e329.png">
    * タイムアウト:3分
    
    <img width="564" alt="qiita3.png" src="https://qiita-image-store.s3.amazonaws.com/0/164563/6d3a57cf-080f-6d81-904b-32e9c0c078fe.png">

### AWSへデプロイ
* 今回はS3を経由しているが、S3を経由する必要は特に無い

```
$ zip -r box-auditlogs-node-es.zip index.js node_modules
$ aws s3 cp ./box-auditlogs-node-es.zip s3://[mybucket]/box-auditlogs-node-es.zip --profile [myprofile]
$ aws lambda update-function-code --function-name box-auditlogs-node-es --s3-bucket [mybucket] --s3-key box-auditlogs-node-es.zip --publish --profile [myprofile]
```

### 実行
* CloudWatch　ルールに、lambdaを指定して引数無しで起動する。
    * 周期はお好きなように
    <img width="1273" alt="スクリーンショット 2019-01-30 16.38.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/164563/fb17b1cb-7e0e-259b-f1c4-0a2baa32d19c.png">


## ソース置き場
[こちら](https://github.com/kenji-toforone/box-auditlogs-node-es)にソース置いておきます。（Node.jpを書くの初めてなので、ソース汚かったらPullReqください）