TWLogAIAN:異常ログを機械学習で検知する処理をJavaからGO言語に写経中
今朝は4時から開発開始です。細々した修正を昨日までに終わったので、お楽しみの異常ログを機械学習で検知する処理の開発です。
お手本は、
です。ソースコードは、
にあります。まずは、ソースコードを読んで処理を整理しました。処理のポイントは、
ログを読み込んで行単位に特徴量を計算する
Isolation Forestというアルゴリズムで異常検知する
ということがわかりました。Isolation Forestは
を使うことを考えています。
今朝はログ読み込んで行単位に特徴量を計算する処理を考えることにしました。すこし込み入ったプログラムになりそうなのでTWLoAIANに直接組み込む前にテストプログラムを作ることにしました。
JavaのプログラムからGO言語へ写経(移植)してみました。
読み込むログ・ファイルは
に書いたZIP圧縮されたものです。GO言語だと簡単にZIPファイルを扱えるのでZIPファイルを直接読み込む処理にしました。行単位に特徴量を計算する処理は、元のサイトで説明されている
:が最初に出現する場所
:の個数(いくつ含まれるか)
(の個数
;の個数
%の個数
/の個数
'の個数
<の個数
?の個数
.の個数
#の個数
%3dの個数
%2fの個数
%5cの個数
%25の個数
%20の個数
メソッドがPOSTかどうか
URLのパス部分に含まれるアルファベットと数値以外の文字の個数
クエリ部分に含まれるアルファベットと数値以外の文字の個数
アルファベットと数値以外の文字が最も連続している部分の長さ
アルファベットと数値以外の文字の個数
/%の個数
//の個数
/.の個数
..の個数
=/の個数
./の個数
/?の個数
です。元のJavaのプログラムでは、多くの関数を自作しているようですが、GO言語のstringsパッケージでかなりカバーできました。
GO言語の並列処理を利用して行単位の特徴量計算を可能な限り同時に実行できるようにしてみました。6コア、メモリ8GのMac mini上で22秒で読み込めました。CPUはピーク時に500%使っていました。
元のサイトでは
で72秒なので、50秒でIsolation Forestの処理ができれば、GO言語の勝ちということになります。ますます、楽しみですが、朝はここまで、
午後に続く
今朝作ったソースコードは
package main
import (
"archive/zip"
"bufio"
"log"
"strings"
"sync"
"time"
)
var ipMap = new(sync.Map)
var total = 0
var valid = 0
var ips = 0
func main() {
log.Println("start")
st := time.Now()
r, err := zip.OpenReader("access.log.zip")
if err != nil {
log.Fatal(err)
}
defer r.Close()
var wg sync.WaitGroup
for _, f := range r.File {
log.Printf("log file=%s", f.Name)
file, err := f.Open()
if err != nil {
log.Fatal(err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
const maxCapacity = 10_000 * 1024 // 10MB
buf := make([]byte, maxCapacity)
scanner.Buffer(buf, maxCapacity)
for scanner.Scan() {
l := scanner.Text()
total++
if total%1000000 == 0 {
log.Printf("mid total=%d valid=%d ip=%d dur=%s", total, valid, ips, time.Since(st))
}
wg.Add(1)
go func(l string) {
defer wg.Done()
getVector(l)
}(l)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
}
wg.Wait()
log.Printf("end total=%d valid=%d ip=%d dur=%s", total, valid, ips, time.Since(st))
}
func getVector(s string) {
a := strings.Fields(s)
if len(a) < 2 {
return
}
ip := a[0]
i, ok := ipMap.LoadOrStore(ip, 0)
if !ok {
ips++
}
c := i.(int) + 1
ipMap.Store(ip, c)
if c < 80 {
a = strings.Split(s, "\"")
if len(a) > 1 {
v := toVector(a[1])
if len(v) > 1 {
valid++
}
}
}
}
func toVector(s string) []int {
vector := []int{}
f := strings.Fields(s)
if len(f) < 3 {
return vector
}
query := ""
ua := strings.SplitN(f[1], "?", 2)
path := ua[0]
if len(ua) > 1 {
query = ua[1]
}
ca := getCharCount(s)
//findex_%
vector = append(vector, strings.Index(s, "%"))
//findex_:
vector = append(vector, strings.Index(s, ":"))
// countedCharArray
for _, c := range []rune{':', '(', ';', '%', '/', '\'', '<', '?', '.', '#'} {
vector = append(vector, ca[c])
}
//encoded =
vector = append(vector, strings.Count(s, "%3D")+strings.Count(s, "%3d"))
//encoded /
vector = append(vector, strings.Count(s, "%2F")+strings.Count(s, "%2f"))
//encoded \
vector = append(vector, strings.Count(s, "%5C")+strings.Count(s, "%5c"))
//encoded %
vector = append(vector, strings.Count(s, "%25"))
//%20
vector = append(vector, strings.Count(s, "%20"))
//POST
if strings.HasPrefix(s, "POST") {
vector = append(vector, 1)
} else {
vector = append(vector, 0)
}
//path_nonalnum_count
vector = append(vector, len(path)-getAlphaNumCount(path))
//pvalue_nonalnum_avg
vector = append(vector, len(query)-getAlphaNumCount(query))
//non_alnum_len(max_len)
vector = append(vector, getMaxNonAlnumLength(s))
//non_alnum_count
vector = append(vector, getNonAlnumCount(s))
for _, p := range []string{"/%", "//", "/.", "..", "=/", "./", "/?"} {
vector = append(vector, strings.Count(s, p))
}
return vector
}
func getCharCount(s string) []int {
ret := []int{}
for i := 0; i < 96; i++ {
ret = append(ret, 0)
}
for _, c := range s {
if 33 <= c && c <= 95 {
ret[c] += 1.0
}
}
return ret
}
func getAlphaNumCount(s string) int {
ret := 0
for _, c := range s {
if 65 <= c && c <= 90 {
ret++
} else if 97 <= c && c <= 122 {
ret++
} else if 48 <= c && c <= 57 {
ret++
}
}
return ret
}
func getMaxNonAlnumLength(s string) int {
max := 0
length := 0
for _, c := range s {
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') {
if length > max {
max = length
}
length = 0
} else {
length++
}
}
if max < length {
max = length
}
return max
}
func getNonAlnumCount(s string) int {
ret := 0
for _, c := range s {
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9') {
} else {
ret++
}
}
return ret
}
です。