WEBページの国別アクセス数の可視化


ストックではなくLGTMしてね(・∀・)

目的

明らかな海外からの攻撃的アクセスをわかりやすくする

概要

事前準備

  • apache or nginxのログを1日ごとになるように設定しておいてください。
    (やり方はいくつかあるので、自分で調べてやってみてください)

  • GeoLite2-City.mmdb
    を使用します。
    GeoIPを使うので登録してください。
    ライセンスと取得方法が昨年末に変更があったので、ここなどを参考にしてみてください

環境


サーバ: クラウド(AWS lightsail)
OS: CentOS 7.8
Webサーバ: apache 2.4.6
Python: 3.6.4
apacheRoot: /var/www/html/
apacheログ: /var/log/httpd/


[root@fishkiller ~]# cat /etc/centos-release
CentOS Linux release 7.8.2003 (Core)
[root@fishkiller ~]# httpd -v
Server version: Apache/2.4.6 (CentOS)
Server built:   Apr  2 2020 13:13:23
[root@fishkiller ~]# python --version
Python 3.6.4
[root@fishkiller ~]# cat /etc/httpd/conf/httpd.conf|grep Root|grep -v "\#"
ServerRoot "/etc/httpd"
DocumentRoot "/var/www/html"

[root@fishkiller httpd]# pwd
/var/log/httpd

環境は正直pythonが3系であればなんでもいいです。

[root@fishkiller ~]# pwd
/root

[root@fishkiller ~]# ls
app bin

[root@fishkiller ~]# ls app/
accessip

好きなところに好きなように作業ディレクトリを作ってください。

ディレクトリ

~/ap
|--accessip
    |--main.py
    |--GeoLite2-City.mmdb

logからアクセスしてきたIPを抽出

cat /var/log/httpd/access_`date +%Y%m`$((`date +%d`-1)).log|awk '{print $1}'|sort > /root/app/httpd-ip.txt

mian.py

#!/usr/bin/python
import numpy as np
import matplotlib as mpl
mpl.use('Agg')
import matplotlib.pyplot as plt
import re
import requests
import geoip2.database
import folium
import japanize_matplotlib 



# Line-notifier設定

token = 'トークン'

api = 'https://notify-api.line.me/api/notify'

#Geoipのデータベースを読み込む
reader = geoip2.database.Reader('/root/bin/log/GeoLite2-City.mmdb')


# 読み込むファイルを開く
f = open('/root/bin/log/httpd-ip.txt')

lines = f.readlines()
f.close()


country = []
city = []



def main():


    map = folium.Map(location=[35,135],zoom_start=4)#map

    for line in lines:

        ip = line.rstrip('\n')

        print("IP",ip) #IP


        data = reader.city(ip)
        country.append(data.country.name)
        print ("国:", data.country.name) #国
        #print ("Subdivisions: ", data.subdivisions.most_specific.name)#州・県
        city.append(data.city.name)
        print ("都市: ", data.city.name) # 市町村
        # print ("経度,緯度: ",data.location.longitude,data.location.latitude)
        #print ("経度: ",data.location.longitude)
        #print ("緯度: ",data.location.latitude)

        folium.Marker([data.location.latitude,data.location.longitude],popup=data.city.name).add_to(map) # ([経度,緯度],popup="表示名")


        #print ("軽度: ", data.location.latitude) 
        #print ("緯度: ", data.location.longitude) 
        #print ("タイムゾーン:", data.location.time_zone) 


    map.save('/root/app/accessip/http_map.html')


    count = len(country)
    CN = country.count('China') #中国
    CA = country.count('Canada') #カナダ
    US = country.count('United States') #アメリカ
    VN = country.count('Vietnam') #ベトナム
    PH = country.count('Philippines') #フィリピン
    RU = country.count('Russia') #ロシア
    UA = country.count('Ukraine') #ウクライナ
    IT = country.count('Italy') #イタリア
    ES = country.count('Spain') #スペイン
    TW = country.count('Taiwan') #台湾
    KR = country.count('Republic of Korea') #韓国
    IN = country.count('India') #インド
    JP = country.count('Japan') #日本
    others = count-CN-CA-US-VN-PH-RU-UA-IT-ES-TW-KR-IN-JP



    circle(CN,CA,US,VN,PH,RU,UA,IT,ES,TW,KR,IN,JP,others)

    mess(CN,CA,US,VN,PH,RU,UA,IT,ES,TW,KR,IN,JP,others,count)

    check(CN,CA,US,VN,PH,RU,UA,IT,ES,TW,KR,IN,JP,others,count)

    cplot(CN,CA,US,VN,PH,RU,UA,IT,ES,TW,KR,IN,JP)


def circle (CN,CA,US,VN,PH,RU,UA,IT,ES,TW,KR,IN,JP,others):

    graph = np.array([CN,CA,US,VN,PH,RU,UA,IT,ES,TW,KR,IN,JP,others])
    #x = ["CN","CA","US","VN","PH","RU","UA","IT","ES","TW","KR","IN","others"]
    x = ["中国","カナダ","アメリカ","ベトナム","フィリピン","ロシア","イギリス","イタリア","スペイン","台湾","韓国","インド","日本","その他"]

    plt.style.use('ggplot')
    plt.rcParams.update({'font.size':15})

    plt.pie(graph,labels=x,autopct=lambda p:'{:.1f}%'.format(p) if p>=5 else '')

    plt.savefig('/root/app/accessip/apache-access.png')
    # plt.show()

def cplot(CN,CA,US,VN,PH,RU,UA,IT,ES,TW,KR,IN,JP):

    map1 = folium.Map(location=[35, 135], zoom_start=3)

    states = (
    {'lat': 39.5427, 'lon': 116.2350, 'value': CN, 'name': '中国'},
    {'lat': 45.4215, 'lon': -75.6971, 'value': CA, 'name': 'カナダ'},
    {'lat': 38.907192, 'lon': -77.036871, 'value': US, 'name': 'アメリカ'},
    {'lat': 21.0279, 'lon':105.851, 'value': VN, 'name': 'ベトナム'},
    {'lat': 14.609, 'lon': 121.0222, 'value': PH, 'name': 'フィリピン'},
    {'lat': 55.7558, 'lon': 37.6173, 'value': RU, 'name': 'ロシア'},
    {'lat': 50.4501, 'lon': 30.5234, 'value': UA, 'name': 'ウクライナ'},
    {'lat': 41.9027, 'lon': 12.4963, 'value': IT, 'name': 'イタリア'},
    {'lat': 40.4167, 'lon': -3.7037, 'value':  ES, 'name': 'スペイン'},
    {'lat': 25.251, 'lon': 121.3154, 'value': TW, 'name': '台湾'},
    {'lat': 37.34, 'lon': 126.59, 'value': KR, 'name': '韓国'},
    {'lat': 20.5936, 'lon': 78.9628, 'value': IN, 'name': 'インド'},
    {'lat': 35.4122, 'lon': 139.4130, 'value': JP, 'name': '日本'}

    )

    # 円の大きさをわかりやすくするための重み
    WEIGHT = 0.6

    # 都市ごとにマーカーを追加(数が増えると辛いため、一括追加が今後の課題)
    for state in states:
        folium.CircleMarker(
            [state['lat'], state['lon']],
            radius=state['value'] * WEIGHT,
            popup=state['name'],
            color='#3186cc',
            fill_color='#3186cc',
        ).add_to(map1)

    map1.save('/root/app/accessip/http_map1.html')



def mess (CN,CA,US,VN,PH,RU,UA,IT,ES,TW,KR,IN,JP,others,count):

    ## Lineに送る
    # meesage = 送るメッセージ
    message = '\n' + '中国:' + str(CN/count*100) + '%\n' + 'カナダ:' + str(CA/count*100) + '%\n' + 'アメリカ:' + str(US/count*100) + '%\n' + 'ベトナム:' + str(VN/count*100) + '%\n' + 'フィリピン:' + str(PH/count*100) + '%\n' + 'ロシア:' + str(RU/count*100) + '%\n' + 'ウクライナ:' + str(UA/count*100) + '%\n' + 'イタリア:' + str(IT/count*100) + '%\n' + 'スペイン:' + str(ES/count*100) + '%\n' + '台湾:' + str(TW/count*100) + '%\n' + '韓国:' + str(KR/count*100) + '%\n' + 'インド:' + str(IN/count*100) + '%\n' + '日本' + str(JP/count*100) + '%\n'  +'その他:' + str(others/count*100) + '%\n'+'MAP: '+'http://fishkiller.info/banmap/index.html'+'\n'+'国MAP: '+'http://18.177.113.234/httpd/map1.html'


    payload = {'message': message}
    files = {"imageFile": open("apache-access.png", "rb")}

    #headers = {'Authorization': 'Bearer ' + line_notify_token}  
    #line_notify = requests.post(line_notify_api, data=payload, headers=headers)

    headers = {'Authorization': 'Bearer ' + token}
    line_notify = requests.post(api, data=payload, headers=headers, files=files)


def check (CN,CA,US,VN,PH,RU,UA,IT,ES,TW,KR,IN,JP,others,count):
    print("中国:",CN)
    print(CN/count*100,"%")

    print("カナダ:",CA)
    print(CA/count*100,"%")

    print("アメリカ:",US)
    print(US/count*100,"%")

    print("ベトナム:",VN)
    print(VN/count*100,"%")

    print("フィリピン:",PH)
    print(PH/count*100,"%")

    print("ロシア:",RU)
    print(RU/count*100,"%")

    print("ウクライナ:",UA)
    print(UA/count*100,"%")

    print("イタリア:",IT)
    print(IT/count*100,"%")

    print("スペイン:",ES)
    print(ES/count*100,"%")

    print("台湾:",TW)
    print(TW/count*100,"%")

    print("韓国:",KR)
    print(KR/count*100,"%")

    print("インド:",IN)
    print(IN/count*100,"%")

    print("日本",JP)
    print(JP/count*100,"%")

    print("その他:",others)
    print(others/count*100,"%")

    print(count)



if __name__ == "__main__":
    main()

コード一つにまとめててキレイじゃない・・・
defごとにそれぞれ別にして、mainは実行にするだけがいいです。
そのうち修正します。。。


main : 他を引数入れて実行する。なぜかhttp_map.htmlここで作ってる。。。cityplot的な関数として定義してあげましょう・・・
circle : 円グラフを作る
cplot : 国ごとの割合マップを作る(http_map1.html)
mess : LINEnotifierにメッセージを送る

check : 確認ようにすべて出力させている

上記のプログラムを実行すると

こんな感じにLINEが送られてきます。

さらに作業ディレクトリに
- http_map.html
- http_map1.html
- apache-access.png
が生成されます。

今回は生成される場所を作業ディレクトリにしていますが、あまり良くない気がするので
/var/log配下や/usr/src/配下などにそれ用のディレクトリを置くようにしたほうが良さそうです。

あとはそれぞれ置きたいwebコンテンツディレクトリに設置すればいいだけ。

コンテンツ設置

コンテンツディレクトリ

/var/www/html
|--map
   |--index.html
    |--http_map1.html
   |-- log
        |-- 日付.jpg
### accessip-set
```accessip-set.sh
##!/bin/bash

SAVE_DIR="/var/www/html/map"

cp /root/app/accessip/apache-access.png ${SAVE_DIR}/log/`date +%Y%m%d`.jpg
cp -f /root/bin/log/http_map.html ${SAVE_DIR}/index.html
cp -f /root/bin/log/http_map1.html ${SAVE_DIR}/

ログの場所やコンテンツ設置位置などは調整してください。

index.html(http_map.html)

http_map1.html

こんな感じになる。

定期実行

好きなようにcronの設定はしてください。。。

[root@fishkiller ~]# crontab -l
# apachelog ip
0 5 * * * cat /var/log/httpd/access_`date +%Y%m`$((`date +%d`-1)).log|awk '{print $1}'|sort > /root/app/accessip/httpd-ip.txt
1 5 * * * /root/.pyenv/shims/python /root/app/accessip/apacheip.py
2 5 * * * /root/bin/apache-access.sh

※pythonをcronで実行するときはフルパスで書く必要があります。

[root@fishkiller ~]# which python
/root/.pyenv/shims/python

⇓ 1つのシェルにまとめてしまいましょう。

[root@fishkiller ~]# crontab -l
# apachelog ip
0 5 * * * /root/bin/apache-ip.sh

apache-ip.sh

##!/bin/bash

cat /var/log/httpd/access_`date +%Y%m`$((`date +%d`-1)).log|awk '{print $1}'|sort > /root/app/accessip/httpd-ip.txt

/root/.pyenv/shims/python /root/app/accessip/apacheip.py

/root/bin/apache-access.sh

同IPをカウントしたくない場合は
1つ目にuniqを追加すればOK

cat /var/log/httpd/access_`date +%Y%m`$((`date +%d`-1)).log|awk '{print $1}'|sort |uniq > /root/app/accessip/httpd-ip.txt

上記のcron設定では毎朝5時LINE来ちゃうので、、、
7時くらいにしておくといいかもです。

まとめ

今回はapacheのログでやりましたが、IPのリストがあればいいだけなので、fial2banでBANしたやmailの送り主などログからIP抽出すれできます。
例えば、自分のfail2banのBANしたIPでやるとほぼほぼ中国になります。


特定時間だけ抽出すれば特定時間のものもできますし、色々応用は効きそうです。
現状、国を指定していることと、国を追加するときめんどくさいなどが今後の改善点かと思います。

これを気に、自分のサーバへのアクセスがどいったところから来てるのか意識してみるのもいいかもしれません。