Files
rl544 df3d94c06a up
2026-09-15 01:33:45 +09:00

574 lines
19 KiB
Go

package main
import (
"database/sql"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"github.com/gingfrederik/docx"
_ "github.com/lib/pq"
)
type Config struct {
DBHost string `json:"db_host"`
DBPort int `json:"db_port"`
DBUser string `json:"db_user"`
DBPassword string `json:"db_password"`
DBName string `json:"db_name"`
}
type MetricRecord struct {
Hostname string
Timestamp time.Time
TotalCpu float64
TotalMem float64
DpcInterrupt float64
TopCpuProcesses string
TopIoProcesses string
NetworkConnections string
HungProcesses string
}
type ProcessCPU struct {
ProcessName string `json:"ProcessName"`
PID int `json:"PID"`
CPUPercent float64 `json:"CPU_Percent"`
KernelCPUPercent float64 `json:"Kernel_CPU_Percent"`
}
type ProcessIO struct {
ProcessName string `json:"ProcessName"`
PID int `json:"PID"`
IODataBytesPersec float64 `json:"IODataBytesPersec"`
}
type NetConn struct {
ProcessName string `json:"ProcessName"`
PID int `json:"PID"`
RemoteAddress string `json:"RemoteAddress"`
RemotePort int `json:"RemotePort"`
IOBytesPerSec float64 `json:"IO_BytesPerSec"`
}
type ProcessHung struct {
ProcessName string `json:"ProcessName"`
PID int `json:"PID"`
}
type DevToolUsage struct {
ProcessName string `json:"processName"`
Count int `json:"count"`
MaxCPU float64 `json:"maxCpu"`
MaxIO float64 `json:"maxIo"`
}
type kv struct {
Key string
Value int
}
func sortMap(m map[string]int) []kv {
var ss []kv
for k, v := range m { ss = append(ss, kv{k, v}) }
sort.Slice(ss, func(i, j int) bool { return ss[i].Value > ss[j].Value })
return ss
}
func isIgnoredProcess(name string) bool {
name = strings.ToLower(name)
if idx := strings.Index(name, "#"); idx != -1 { name = name[:idx] }
ignoreList := map[string]bool{
"idle": true, "_total": true, "taskmgr": true,
"wmiprvse": true, "powershell": true, "pwsh": true, "dwm": true,
}
return ignoreList[name]
}
func isDevTool(name string) bool {
name = strings.ToLower(name)
if idx := strings.Index(name, "#"); idx != -1 { name = name[:idx] }
devTools := map[string]bool{
"code": true, "eclipse": true, "idea64": true, "java": true,
"node": true, "msbuild": true, "docker": true, "git": true,
"devenv": true, "python": true, "python3": true, "goland": true,
}
return devTools[name]
}
func isSecProc(name string) bool {
name = strings.ToLower(name)
if idx := strings.Index(name, "#"); idx != -1 { name = name[:idx] }
secTools := map[string]bool{
"v3svc": true, "asdsvc": true, "v3main": true, "v3lite": true,
"privacyi": true, "piagent": true, "ngm": true, "corebguard": true,
"gncsensor": true, "gsagent": true, "gsprotect": true, "gsview": true, "gsflow": true,
}
return secTools[name]
}
func isSecDriver(name string) bool {
name = strings.ToLower(name)
return strings.HasPrefix(name, "v3") || strings.Contains(name, "ahnlab") || strings.Contains(name, "asd") ||
strings.Contains(name, "privacy") || strings.Contains(name, "piagent") || strings.Contains(name, "somansa") || strings.Contains(name, "ngm") ||
strings.Contains(name, "gnc") || strings.Contains(name, "gsagent") || strings.Contains(name, "gsprotect") || strings.Contains(name, "gsflow") || strings.Contains(name, "genian")
}
func getProcessTag(name string) string {
if isDevTool(name) { return " 💻[개발/빌드 도구]" }
if isSecProc(name) { return " 🛡️[보안 프로그램]" }
return ""
}
type DetailRecord struct {
Category string
Content []string
}
func main() {
etlPath := flag.String("etl", "", "분석할 .etl 파일 경로")
flag.Parse()
configFile, err := ioutil.ReadFile("config.json")
if err != nil { log.Fatalf("config.json 읽기 실패: %v", err) }
var config Config
json.Unmarshal(configFile, &config)
dbUrl := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable", config.DBUser, config.DBPassword, config.DBHost, config.DBPort, config.DBName)
db, err := sql.Open("postgres", dbUrl)
if err != nil { log.Fatalf("DB 연결 실패: %v", err) }
defer db.Close()
timestampStr := time.Now().Format("20060102_150405")
reportDocx := fmt.Sprintf("Report_%s.docx", timestampStr)
reportMd := fmt.Sprintf("Report_%s.md", timestampStr)
f := docx.NewFile()
var md strings.Builder
addH1 := func(text string) {
p := f.AddParagraph(); p.AddText(text).Size(20)
md.WriteString("# " + text + "\n\n")
}
addH2 := func(text string) {
p := f.AddParagraph(); p.AddText(text).Size(14)
md.WriteString("## " + text + "\n\n")
}
addText := func(text string) {
if text != "" { f.AddParagraph().AddText(text).Size(11) } else { f.AddParagraph() }
if text != "" { md.WriteString(text + "\n\n") } else { md.WriteString("\n") }
}
addBoldText := func(text string) {
p := f.AddParagraph(); p.AddText("▶ " + text).Size(11)
md.WriteString("**▶ " + text + "**\n\n")
}
addBullet := func(text string) {
f.AddParagraph().AddText(" • " + text).Size(11)
md.WriteString("- " + text + "\n")
}
addRedBullet := func(text string) {
p := f.AddParagraph(); p.AddText(" • " + text).Size(11).Color("FF0000")
md.WriteString("- 🔴 **" + text + "**\n")
}
addH1("시스템 성능 및 장애 원인 분석 리포트")
addText(fmt.Sprintf("작성 일시: %s", time.Now().Format("2006-01-02 15:04:05")))
addText("")
devBuildCount := 0
secInterferenceCount := 0
memSwapCount := 0
var sumDpcInterfered float64
sysKernelHogs := make(map[string]int)
sysNetworkHogs := make(map[string]int)
sysNetworkHogsMaxIO := make(map[string]float64)
sysTargetIps := make(map[string]int)
sysTargetIpsMaxIO := make(map[string]float64)
hungCounts := make(map[string]int)
devToolsStats := make(map[string]*DevToolUsage)
// 심화 분석 지표
hourlyStress := make(map[int]int)
networkHangCorrels := 0
totalEventsParsed := 0
var details []DetailRecord
rows, err := db.Query(`
SELECT hostname, timestamp, total_cpu_percent, total_mem_percent, dpc_interrupt_percent,
COALESCE(top_cpu_processes::text, '[]'),
COALESCE(top_io_processes::text, '[]'),
COALESCE(network_connections::text, '[]'),
COALESCE(hung_processes::text, '[]')
FROM client_metrics
ORDER BY timestamp DESC LIMIT 10000
`)
if err == nil {
defer rows.Close()
for rows.Next() {
var r MetricRecord
err := rows.Scan(&r.Hostname, &r.Timestamp, &r.TotalCpu, &r.TotalMem, &r.DpcInterrupt, &r.TopCpuProcesses, &r.TopIoProcesses, &r.NetworkConnections, &r.HungProcesses)
if err != nil { continue }
localTimeStr := r.Timestamp.Local().Format("2006-01-02 15:04:05")
var cpuProcs []ProcessCPU
json.Unmarshal([]byte(r.TopCpuProcesses), &cpuProcs)
var ioProcs []ProcessIO
json.Unmarshal([]byte(r.TopIoProcesses), &ioProcs)
// 🚨 [Smoking Gun 타겟팅]
isCompiling := false
var activeDevTools []string
for _, p := range ioProcs {
if isDevTool(p.ProcessName) && p.IODataBytesPersec > 1048576 {
mbps := p.IODataBytesPersec / 1048576.0
cpuVal := 0.0
for _, cp := range cpuProcs {
if cp.ProcessName == p.ProcessName && cp.PID == p.PID {
cpuVal = cp.CPUPercent; break
}
}
if stat, ok := devToolsStats[p.ProcessName]; ok {
stat.Count++
if cpuVal > stat.MaxCPU { stat.MaxCPU = cpuVal }
if mbps > stat.MaxIO { stat.MaxIO = mbps }
} else {
devToolsStats[p.ProcessName] = &DevToolUsage{ProcessName: p.ProcessName, Count: 1, MaxCPU: cpuVal, MaxIO: mbps}
}
activeDevTools = append(activeDevTools, fmt.Sprintf("%s (CPU: %.1f%%, I/O: %.1f MB/s)", p.ProcessName, cpuVal, mbps))
isCompiling = true
}
}
if isCompiling {
devBuildCount++
secInterfered := false
var secCulprits []string
if r.DpcInterrupt > 3.0 { secInterfered = true }
for _, cp := range cpuProcs {
if isSecProc(cp.ProcessName) && cp.KernelCPUPercent > 2.0 {
secInterfered = true
secCulprits = append(secCulprits, fmt.Sprintf("%s (Kernel CPU: %.1f%%)", cp.ProcessName, cp.KernelCPUPercent))
}
}
if secInterfered {
secInterferenceCount++
sumDpcInterfered += r.DpcInterrupt
rec := DetailRecord{
Category: fmt.Sprintf("[빌드-보안 충돌] 개발 도구와 보안 솔루션 경합 감지 - 시간: %s | PC: %s", localTimeStr, r.Hostname),
Content: []string{
"감지된 개발 프로세스: " + strings.Join(activeDevTools, ", "),
"관여된 보안 솔루션: " + strings.Join(secCulprits, ", "),
fmt.Sprintf("발생 커널 오버헤드(DPC): %.1f%%", r.DpcInterrupt),
},
}
details = append(details, rec)
}
}
if r.TotalMem > 90.0 {
hasHighIO := false
for _, p := range ioProcs {
if p.IODataBytesPersec > 1048576 {
hasHighIO = true; break
}
}
if hasHighIO { memSwapCount++ }
}
// 상세 내역 및 일반 요약 로직
isEventOccurred := false
// 1. 커널 프리징
if r.DpcInterrupt > 3.0 {
isEventOccurred = true
rec := DetailRecord{Category: fmt.Sprintf("[프리징 유발] 커널 오버헤드 감지 - 시간: %s | PC: %s | 오버헤드: %.1f%%", localTimeStr, r.Hostname, r.DpcInterrupt)}
for _, cp := range cpuProcs {
if cp.KernelCPUPercent > 3.0 && !isIgnoredProcess(cp.ProcessName) {
sysKernelHogs[cp.ProcessName]++
rec.Content = append(rec.Content, fmt.Sprintf("주범: %s%s (PID: %d) / 커널 점유 %.1f%%", cp.ProcessName, getProcessTag(cp.ProcessName), cp.PID, cp.KernelCPUPercent))
}
}
if len(rec.Content) > 0 { details = append(details, rec) }
}
// 2. 대역폭 포화
isNetworkChoke := false
var netRec DetailRecord
for _, p := range ioProcs {
if p.IODataBytesPersec > 5242880 && !isIgnoredProcess(p.ProcessName) {
sysNetworkHogs[p.ProcessName]++
mbps := p.IODataBytesPersec / 1048576.0
if mbps > sysNetworkHogsMaxIO[p.ProcessName] { sysNetworkHogsMaxIO[p.ProcessName] = mbps }
if !isNetworkChoke {
isNetworkChoke = true
isEventOccurred = true
netRec = DetailRecord{Category: fmt.Sprintf("[대역폭 포화 감지] 네트워크/디스크 지연 - 시간: %s | PC: %s", localTimeStr, r.Hostname)}
}
var conns []NetConn
json.Unmarshal([]byte(r.NetworkConnections), &conns)
connStr := "목적지 연결 없음 (내부 대용량 파일 I/O)"
for _, c := range conns {
if c.ProcessName == p.ProcessName {
ipStr := fmt.Sprintf("%s:%d", c.RemoteAddress, c.RemotePort)
sysTargetIps[ipStr]++
connMbps := c.IOBytesPerSec / 1048576.0
if connMbps > sysTargetIpsMaxIO[ipStr] { sysTargetIpsMaxIO[ipStr] = connMbps }
connStr = fmt.Sprintf("목적지 IP: %s (속도: %.1f MB/s)", ipStr, connMbps)
break
}
}
netRec.Content = append(netRec.Content, fmt.Sprintf("트래픽 점유: %s%s (PID: %d) / 총 %.1f MB/s -> %s", p.ProcessName, getProcessTag(p.ProcessName), p.PID, mbps, connStr))
}
}
if isNetworkChoke { details = append(details, netRec) }
// 3. 앱 Hang
var hung []ProcessHung
json.Unmarshal([]byte(r.HungProcesses), &hung)
hasValidHang := false
if len(hung) > 0 {
hangRec := DetailRecord{Category: fmt.Sprintf("[응답 없음] 프로세스 Hang 발생 - 시간: %s | PC: %s", localTimeStr, r.Hostname)}
for _, h := range hung {
if !isIgnoredProcess(h.ProcessName) {
hungCounts[h.ProcessName]++
hasValidHang = true
hangRec.Content = append(hangRec.Content, fmt.Sprintf("멈춤: %s%s (PID: %d)", h.ProcessName, getProcessTag(h.ProcessName), h.PID))
}
}
if hasValidHang {
details = append(details, hangRec)
isEventOccurred = true
}
}
// 심화 분석: 상관관계 및 시간대별 스트레스 추적
if isNetworkChoke && hasValidHang {
networkHangCorrels++ // 대역폭 포화가 앱 응답없음을 유발한 연관성 의심 사례
}
if isEventOccurred {
hourlyStress[r.Timestamp.Hour()]++
}
totalEventsParsed++
}
}
// --- Smoking Gun Report Section ---
addH2("🚨 보안 솔루션 개발 환경 충돌 정밀 분석")
addText("본 섹션은 보안 프로그램(백신/EDR/DLP)이 개발자의 빌드/컴파일 작업에 미치는 실질적인 성능 저하 연관성을 교차 검증한 결과입니다.")
addText(fmt.Sprintf("분석 데이터 내 개발 도구(IDE, 컴파일러 등) 활성화 감지: 총 %d 회", devBuildCount))
addBoldText(fmt.Sprintf("👉 위 빌드 작업 중, 보안 프로그램이 개입하여 커널 프리징/시스템 렉을 유발한 횟수: %d 회", secInterferenceCount))
if devBuildCount > 0 {
rate := (float64(secInterferenceCount) / float64(devBuildCount)) * 100
addBoldText(fmt.Sprintf("🔥 보안 솔루션으로 인한 개발 업무 방해(병목) 확률: %.1f%%", rate))
}
addText("")
// --- General Report ---
addH2("1. 시스템 체감 렉(Lag) 유발 핵심 주범 요약")
addBoldText("[프리징 원인] 장애 시점 커널(EDR/백신) 병목 프로세스")
for i, kv := range sortMap(sysKernelHogs) {
if i >= 5 { break }
addBullet(fmt.Sprintf("%s%s (멈춤 유발 횟수: %d회)", kv.Key, getProcessTag(kv.Key), kv.Value))
}
if len(sysKernelHogs) == 0 { addBullet("해당 원인 없음") }
addText("")
addBoldText("[지연 원인] 장애 시점 트래픽 폭주 유발 프로세스")
for i, kv := range sortMap(sysNetworkHogs) {
if i >= 5 { break }
maxIo := sysNetworkHogsMaxIO[kv.Key]
addBullet(fmt.Sprintf("%s%s (포화 유발: %d회 | 최고 I/O 속도: %.1f MB/s)", kv.Key, getProcessTag(kv.Key), kv.Value, maxIo))
}
if len(sysNetworkHogs) == 0 { addBullet("해당 원인 없음") }
addText("")
addBoldText("[대역폭 도둑] 렉 유발 핵심 통신 목적지 IP")
for i, kv := range sortMap(sysTargetIps) {
if i >= 5 { break }
maxIo := sysTargetIpsMaxIO[kv.Key]
addBullet(fmt.Sprintf("%s (접속 빈도: %d회 | 최고 트래픽: %.1f MB/s)", kv.Key, kv.Value, maxIo))
}
if len(sysTargetIps) == 0 { addBullet("해당 원인 없음") }
addText("")
addBoldText("만성적 응답 없음(Hang) 발생 애플리케이션")
for i, kv := range sortMap(hungCounts) {
if i >= 5 { break }
addBullet(fmt.Sprintf("%s%s (빈도: %d회)", kv.Key, getProcessTag(kv.Key), kv.Value))
}
if len(hungCounts) == 0 { addBullet("해당 원인 없음") }
addText("")
// --- Detailed Logs ---
addH2("2. 시간대별 상세 이상 징후 발생 이력 (Chronological Log)")
if len(details) == 0 {
addText("기록된 이상 징후가 없습니다.")
addText("")
} else {
limit := len(details)
if limit > 100 { limit = 100 } // 너무 길어지는 것 방지
for i := 0; i < limit; i++ {
d := details[i]
addBoldText(d.Category)
for _, c := range d.Content { addBullet(c) }
addText("")
}
if len(details) > 100 {
addText(fmt.Sprintf("... (생략됨: 총 %d건의 이벤트 중 최신 100건만 출력)", len(details)))
}
}
addH2("3. ETW (.etl) 커널 덤프 정밀 분석 결과")
var etlFiles []string
if *etlPath != "" { etlFiles = append(etlFiles, *etlPath)
} else {
localFiles, _ := filepath.Glob("*.etl")
etlFiles = append(etlFiles, localFiles...)
tempFiles, _ := filepath.Glob("C:\\temp\\*.etl")
etlFiles = append(etlFiles, tempFiles...)
}
if len(etlFiles) > 0 {
for _, file := range etlFiles {
sysMap := parseETL(file)
addBoldText(fmt.Sprintf("📄 분석 파일: %s", filepath.Base(file)))
if len(sysMap) > 0 {
addText("커널 덤프 파일 내에서 가장 많은 인터럽트 및 파일 검사를 유발한 서드파티 커널 드라이버(.sys) 랭킹입니다.")
for i, kv := range sortMap(sysMap) {
if i >= 10 { break }
if isSecDriver(kv.Key) {
addRedBullet(fmt.Sprintf("%d위: %s (빈도: %d) 🚨[보안 솔루션 커널 드라이버 적발]", i+1, kv.Key, kv.Value))
} else {
addBullet(fmt.Sprintf("%d위: %s (빈도: %d)", i+1, kv.Key, kv.Value))
}
}
addText("")
} else {
addText("서드파티 드라이버 정보를 추출하지 못했습니다.")
}
}
} else {
addText("분석할 .etl 커널 덤프 파일이 없습니다.")
}
err = f.Save(reportDocx)
if err != nil { log.Fatalf("DOCX 파일 저장 실패: %v", err) }
err = ioutil.WriteFile(reportMd, []byte(md.String()), 0644)
if err != nil { log.Fatalf("MD 파일 저장 실패: %v", err) }
// --- HTML Report Data Preparation ---
var rate float64 = 0
if devBuildCount > 0 {
rate = (float64(secInterferenceCount) / float64(devBuildCount)) * 100
}
// Calculate advanced metrics
var avgDpc float64 = 0
if secInterferenceCount > 0 {
avgDpc = sumDpcInterfered / float64(secInterferenceCount)
}
healthScore := 100.0
healthScore -= float64(devBuildCount) * 0.1
healthScore -= float64(secInterferenceCount) * 1.5
healthScore -= float64(memSwapCount) * 2.0
healthScore -= float64(len(hungCounts)) * 1.0
if healthScore < 0 { healthScore = 0 }
// Prepare flat slices for charts
kernelHogsSlice := sortMap(sysKernelHogs)
networkHogsSlice := sortMap(sysNetworkHogs)
targetIpsSlice := sortMap(sysTargetIps)
hungCountsSlice := sortMap(hungCounts)
var etlDriversSlice []kv
if len(etlFiles) > 0 {
sysMap := parseETL(etlFiles[0])
etlDriversSlice = sortMap(sysMap)
}
var devStatsSlice []DevToolUsage
for _, v := range devToolsStats {
devStatsSlice = append(devStatsSlice, *v)
}
var hourlyStressSlice []kv
for i := 0; i < 24; i++ {
hourlyStressSlice = append(hourlyStressSlice, kv{Key: fmt.Sprintf("%02d:00", i), Value: hourlyStress[i]})
}
reportData := ReportData{
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
DevBuildCount: devBuildCount,
SecInterferenceCount: secInterferenceCount,
InterferenceRate: rate,
KernelHogs: kernelHogsSlice,
NetworkHogs: networkHogsSlice,
TargetIps: targetIpsSlice,
HungCounts: hungCountsSlice,
Details: details,
EtlDrivers: etlDriversSlice,
MemSwapEvents: memSwapCount,
AvgDpcWhenInterfered: avgDpc,
DevToolsStats: devStatsSlice,
VDIHealthScore: healthScore,
HourlyStress: hourlyStressSlice,
NetworkHangCorrels: networkHangCorrels,
}
reportHtml := fmt.Sprintf("Report_%s.html", timestampStr)
err = generateHTMLReport(reportHtml, reportData)
if err != nil {
log.Printf("HTML 파일 저장 실패: %v", err)
}
fmt.Printf("==================================================\n")
fmt.Printf(" 분석 완료: '%s', '%s', '%s' 문서가 생성되었습니다.\n", reportDocx, reportMd, reportHtml)
fmt.Printf("==================================================\n")
}
func parseETL(etlPath string) map[string]int {
sysCounts := make(map[string]int)
dumpFile := "etl_dump.xml"
cmd := exec.Command("tracerpt.exe", etlPath, "-o", dumpFile, "-of", "XML", "-y")
cmd.Run()
data, err := ioutil.ReadFile(dumpFile)
if err != nil { return sysCounts }
re := regexp.MustCompile(`(?i)([a-zA-Z0-9_-]+\.sys)`)
matches := re.FindAllString(string(data), -1)
ignoreList := map[string]bool{
"ntoskrnl.sys": true, "ndis.sys": true, "tcpip.sys": true, "fltmgr.sys": true,
"wof.sys": true, "ntfs.sys": true, "dxgkrnl.sys": true, "netbt.sys": true,
}
for _, match := range matches {
match = strings.ToLower(match)
if !ignoreList[match] { sysCounts[match]++ }
}
os.Remove(dumpFile)
os.Remove("summary.txt")
return sysCounts
}