見出し画像

TWLogAIAN:異常ログを機械学習で検知する処理をJavaからGO言語に写経中

今朝は4時から開発開始です。細々した修正を昨日までに終わったので、お楽しみの異常ログを機械学習で検知する処理の開発です。
お手本は、

です。ソースコードは、

にあります。まずは、ソースコードを読んで処理を整理しました。処理のポイントは、

  • ログを読み込んで行単位に特徴量を計算する

  • Isolation Forestというアルゴリズムで異常検知する

ということがわかりました。Isolation Forestは

を使うことを考えています。
今朝はログ読み込んで行単位に特徴量を計算する処理を考えることにしました。すこし込み入ったプログラムになりそうなのでTWLoAIANに直接組み込む前にテストプログラムを作ることにしました。
JavaのプログラムからGO言語へ写経(移植)してみました。
読み込むログ・ファイルは

に書いたZIP圧縮されたものです。GO言語だと簡単にZIPファイルを扱えるのでZIPファイルを直接読み込む処理にしました。行単位に特徴量を計算する処理は、元のサイトで説明されている

  1. :が最初に出現する場所

  2. :の個数(いくつ含まれるか)

  3. (の個数

  4. ;の個数

  5. %の個数

  6. /の個数

  7. 'の個数

  8. <の個数

  9. ?の個数

  10. .の個数

  11. #の個数

  12. %3dの個数

  13. %2fの個数

  14. %5cの個数

  15. %25の個数

  16. %20の個数

  17. メソッドがPOSTかどうか

  18. URLのパス部分に含まれるアルファベットと数値以外の文字の個数

  19. クエリ部分に含まれるアルファベットと数値以外の文字の個数

  20. アルファベットと数値以外の文字が最も連続している部分の長さ

  21. アルファベットと数値以外の文字の個数

  22. /%の個数

  23. //の個数

  24. /.の個数

  25. ..の個数

  26. =/の個数

  27. ./の個数

  28. /?の個数

です。元のJavaのプログラムでは、多くの関数を自作しているようですが、GO言語のstringsパッケージでかなりカバーできました。

GO言語の並列処理を利用して行単位の特徴量計算を可能な限り同時に実行できるようにしてみました。6コア、メモリ8GのMac mini上で22秒で読み込めました。CPUはピーク時に500%使っていました。
元のサイトでは

私が使用した開発機(※14)はメモリ128GB、CPUはAMD Ryzen 9 5950X(16Core/32Thread)、ストレージは高速なSamsungのNVMe SSDです。分散処理はせずに、独立した1台のマシン上で処理を完結させています。

元のサイトの環境

で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
}

です。

開発のための諸経費(機材、Appleの開発者、サーバー運用)に利用します。 ソフトウェアのマニュアルをnoteの記事で提供しています。 サポートによりnoteの運営にも貢献できるのでよろしくお願います。