Merge pull request #50 from yumusb/main

兼容 ubuntu 系的 计划任务采集、ssh登录日志的解析
This commit is contained in:
dushixiang authored and GitHub committed 2026-01-21 15:52:09 +08:00
commit 4685597411
2 files changed
+123 -16

No files matched your search

+48 -1
View File
@@ -66,13 +66,60 @@ func (fac *FileAssetsCollector) collectCronJobs() []protocol.CronJob {
jobs = append(jobs, fac.parseCronFile(file, "root")...) jobs = append(jobs, fac.parseCronFile(file, "root")...)
} }
// 收集用户crontab // 收集用户crontab (RHEL/CentOS/Fedora)
userCronFiles, _ := filepath.Glob("/var/spool/cron/*") userCronFiles, _ := filepath.Glob("/var/spool/cron/*")
for _, file := range userCronFiles { for _, file := range userCronFiles {
info, err := os.Stat(file)
if err != nil || info.IsDir() {
continue
}
username := filepath.Base(file) username := filepath.Base(file)
jobs = append(jobs, fac.parseCronFile(file, username)...) jobs = append(jobs, fac.parseCronFile(file, username)...)
} }
// 收集用户crontab (Debian/Ubuntu)
userCronFilesDebian, _ := filepath.Glob("/var/spool/cron/crontabs/*")
for _, file := range userCronFilesDebian {
info, err := os.Stat(file)
if err != nil || info.IsDir() {
continue
}
username := filepath.Base(file)
jobs = append(jobs, fac.parseCronFile(file, username)...)
}
// 收集周期性脚本目录 (cron.hourly/daily/weekly/monthly)
periodicDirs := []struct {
path string
schedule string
}{
{"/etc/cron.hourly", "@hourly"},
{"/etc/cron.daily", "@daily"},
{"/etc/cron.weekly", "@weekly"},
{"/etc/cron.monthly", "@monthly"},
}
for _, dir := range periodicDirs {
files, _ := filepath.Glob(dir.path + "/*")
for _, file := range files {
info, err := os.Stat(file)
if err != nil || info.IsDir() {
continue
}
// 跳过非可执行文件
if info.Mode()&0111 == 0 {
continue
}
job := protocol.CronJob{
User: "root",
Schedule: dir.schedule,
Command: file,
FilePath: file,
}
jobs = append(jobs, job)
}
}
// 限制数量 // 限制数量
if len(jobs) > 100 { if len(jobs) > 100 {
jobs = jobs[:100] jobs = jobs[:100]
+75 -15
View File
@@ -199,8 +199,9 @@ func (lac *LoginAssetsCollector) collectFailedLoginsFromAuthLog() []protocol.Log
// 尝试读取不同的认证日志文件 // 尝试读取不同的认证日志文件
authLogPaths := []string{ authLogPaths := []string{
"/var/log/auth.log", "/var/log/auth.log", // Debian/Ubuntu
"/var/log/secure", "/var/log/secure", // RHEL/CentOS/Fedora
"/var/log/messages", // 某些系统的备用位置
} }
var authLog string var authLog string
@@ -215,27 +216,36 @@ func (lac *LoginAssetsCollector) collectFailedLoginsFromAuthLog() []protocol.Log
return records return records
} }
// 读取文件所有行
file, err := os.Open(authLog) file, err := os.Open(authLog)
if err != nil { if err != nil {
return records return records
} }
defer file.Close() defer file.Close()
// 先收集所有匹配的行
// 只匹配信息最完整的日志类型,避免同一会话的重复记录
var matchedLines []string
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
count := 0 for scanner.Scan() {
for scanner.Scan() && count < 100 {
line := scanner.Text() line := scanner.Text()
// 只匹配 Failed password 和 Invalid user,这两种日志包含完整信息
// 不匹配 authentication failure,因为它和上面两种是同一会话的重复记录
if strings.Contains(line, "Failed password") {
matchedLines = append(matchedLines, line)
}
}
// 查找失败的SSH登录 // 从末尾取最新的 100 条记录
if strings.Contains(line, "Failed password") || startIdx := 0
strings.Contains(line, "authentication failure") { if len(matchedLines) > 100 {
startIdx = len(matchedLines) - 100
}
record := lac.parseFailedLoginFromLog(line) for i := len(matchedLines) - 1; i >= startIdx; i-- {
if record != nil { record := lac.parseFailedLoginFromLog(matchedLines[i])
records = append(records, *record) if record != nil {
count++ records = append(records, *record)
}
} }
} }
@@ -249,12 +259,14 @@ func (lac *LoginAssetsCollector) parseFailedLoginFromLog(line string) *protocol.
ip := "unknown" ip := "unknown"
// 提取用户名 // 提取用户名
// 格式1: "Invalid user xxx from" 或 "Failed password for invalid user xxx from"
if idx := strings.Index(line, "user "); idx != -1 { if idx := strings.Index(line, "user "); idx != -1 {
rest := line[idx+5:] rest := line[idx+5:]
if spaceIdx := strings.Index(rest, " "); spaceIdx != -1 { if spaceIdx := strings.Index(rest, " "); spaceIdx != -1 {
username = rest[:spaceIdx] username = rest[:spaceIdx]
} }
} else if idx := strings.Index(line, "for "); idx != -1 { } else if idx := strings.Index(line, "for "); idx != -1 {
// 格式2: "Failed password for xxx from"
rest := line[idx+4:] rest := line[idx+4:]
if spaceIdx := strings.Index(rest, " "); spaceIdx != -1 { if spaceIdx := strings.Index(rest, " "); spaceIdx != -1 {
username = rest[:spaceIdx] username = rest[:spaceIdx]
@@ -262,6 +274,7 @@ func (lac *LoginAssetsCollector) parseFailedLoginFromLog(line string) *protocol.
} }
// 提取IP地址 // 提取IP地址
// 格式1: "from 192.168.1.1 port"
if idx := strings.Index(line, "from "); idx != -1 { if idx := strings.Index(line, "from "); idx != -1 {
rest := line[idx+5:] rest := line[idx+5:]
if spaceIdx := strings.Index(rest, " "); spaceIdx != -1 { if spaceIdx := strings.Index(rest, " "); spaceIdx != -1 {
@@ -271,8 +284,33 @@ func (lac *LoginAssetsCollector) parseFailedLoginFromLog(line string) *protocol.
} }
} }
// 格式2: pam_unix 格式 "rhost=192.168.1.1"
if ip == "unknown" {
if idx := strings.Index(line, "rhost="); idx != -1 {
rest := line[idx+6:]
// rhost 可能在行尾或后面有空格
if spaceIdx := strings.Index(rest, " "); spaceIdx != -1 {
ip = rest[:spaceIdx]
} else {
ip = strings.TrimSpace(rest)
}
}
}
// 格式2: pam_unix 格式 "ruser=xxx" (如果前面没找到用户名)
if username == "unknown" {
if idx := strings.Index(line, "ruser="); idx != -1 {
rest := line[idx+6:]
if spaceIdx := strings.Index(rest, " "); spaceIdx != -1 {
user := rest[:spaceIdx]
if user != "" {
username = user
}
}
}
}
// 尝试解析日志时间 // 尝试解析日志时间
// syslog 格式: Dec 25 10:30:00
timestamp := lac.parseSyslogTime(line) timestamp := lac.parseSyslogTime(line)
return &protocol.LoginRecord{ return &protocol.LoginRecord{
@@ -286,8 +324,30 @@ func (lac *LoginAssetsCollector) parseFailedLoginFromLog(line string) *protocol.
// parseSyslogTime 解析syslog时间格式 // parseSyslogTime 解析syslog时间格式
func (lac *LoginAssetsCollector) parseSyslogTime(line string) int64 { func (lac *LoginAssetsCollector) parseSyslogTime(line string) int64 {
// syslog 时间格式通常在行首: Dec 25 10:30:00
fields := strings.Fields(line) fields := strings.Fields(line)
if len(fields) < 1 {
return time.Now().UnixMilli()
}
// 首先尝试解析 ISO 8601 格式 (Ubuntu 等现代系统)
// 格式: 2026-01-18T00:10:04.454913+08:00
if len(fields[0]) > 10 && strings.Contains(fields[0], "T") {
// 尝试多种 ISO 8601 变体
isoFormats := []string{
time.RFC3339Nano, // 2006-01-02T15:04:05.999999999Z07:00
time.RFC3339, // 2006-01-02T15:04:05Z07:00
"2006-01-02T15:04:05.000000-07:00", // 带微秒
"2006-01-02T15:04:05.000000+08:00", // 带微秒和时区
"2006-01-02T15:04:05-07:00", // 不带微秒
}
for _, format := range isoFormats {
if t, err := time.Parse(format, fields[0]); err == nil {
return t.UnixMilli()
}
}
}
// 回退到传统 syslog 格式: Dec 25 10:30:00
if len(fields) < 3 { if len(fields) < 3 {
return time.Now().UnixMilli() return time.Now().UnixMilli()
} }