Files
pika/pkg/agent/audit/asset_user.go
T
2026-08-12 18:59:29 +08:00

492 lines
11 KiB
Go

package audit
import (
"bufio"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/pika-monitor/pika/internal/protocol"
)
// UserAssetsCollector 用户资产收集器
type UserAssetsCollector struct {
config *Config
executor *CommandExecutor
}
// NewUserAssetsCollector 创建用户资产收集器
func NewUserAssetsCollector(config *Config, executor *CommandExecutor) *UserAssetsCollector {
return &UserAssetsCollector{
config: config,
executor: executor,
}
}
// Collect 收集用户资产
func (uac *UserAssetsCollector) Collect() *protocol.UserAssets {
assets := &protocol.UserAssets{}
// 收集系统用户
assets.SystemUsers = uac.collectSystemUsers()
// 收集登录历史
assets.LoginHistory = uac.collectLoginHistory()
// 收集当前登录
assets.CurrentLogins = uac.collectCurrentLogins()
// 收集SSH密钥
assets.SSHKeys = uac.collectSSHKeys()
// 收集Sudo用户
assets.SudoUsers = uac.collectSudoUsers()
// 收集SSH配置
assets.SSHConfig = uac.collectSSHConfig()
// 统计信息
assets.Statistics = uac.calculateStatistics(assets)
return assets
}
// collectSystemUsers 收集系统用户
func (uac *UserAssetsCollector) collectSystemUsers() []protocol.UserInfo {
var users []protocol.UserInfo
// 读取 /etc/passwd
passwdFile, err := os.Open("/etc/passwd")
if err != nil {
globalLogger.Warn("读取/etc/passwd失败: %v", err)
return users
}
defer passwdFile.Close()
// 读取 /etc/shadow 获取密码信息
shadowPasswords := uac.readShadowPasswords()
scanner := bufio.NewScanner(passwdFile)
for scanner.Scan() {
line := scanner.Text()
parts := strings.Split(line, ":")
if len(parts) < 7 {
continue
}
username := parts[0]
uid := parts[2]
gid := parts[3]
homeDir := parts[5]
shell := parts[6]
// 判断是否可登录
isLoginable := !strings.Contains(shell, "nologin") &&
!strings.Contains(shell, "false") &&
shell != ""
// 判断是否Root等效
isRootEquiv := uid == "0" && username != "root"
// 检查是否有密码
hasPassword := false
if pwd, ok := shadowPasswords[username]; ok {
hasPassword = pwd != "" && pwd != "!" && pwd != "*"
}
user := protocol.UserInfo{
Username: username,
UID: uid,
GID: gid,
HomeDir: homeDir,
Shell: shell,
IsLoginable: isLoginable,
IsRootEquiv: isRootEquiv,
HasPassword: hasPassword,
}
users = append(users, user)
}
return users
}
// readShadowPasswords 读取shadow密码
func (uac *UserAssetsCollector) readShadowPasswords() map[string]string {
passwords := make(map[string]string)
shadowFile, err := os.Open("/etc/shadow")
if err != nil {
return passwords
}
defer shadowFile.Close()
scanner := bufio.NewScanner(shadowFile)
for scanner.Scan() {
line := scanner.Text()
parts := strings.Split(line, ":")
if len(parts) >= 2 {
passwords[parts[0]] = parts[1]
}
}
return passwords
}
// collectLoginHistory 收集登录历史
func (uac *UserAssetsCollector) collectLoginHistory() []protocol.LoginRecord {
var records []protocol.LoginRecord
// 使用 last 命令获取登录历史
output, err := uac.executor.Execute("last", "-n", "50", "-F")
if err != nil {
globalLogger.Debug("获取登录历史失败: %v", err)
return records
}
lines := strings.Split(output, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "wtmp") || strings.HasPrefix(line, "reboot") {
continue
}
fields := strings.Fields(line)
if len(fields) < 7 {
continue
}
username := fields[0]
terminal := fields[1]
ip := fields[2]
// 解析时间 (简化处理,使用当前时间作为近似)
timestamp := time.Now().UnixMilli()
record := protocol.LoginRecord{
Username: username,
Terminal: terminal,
IP: ip,
Timestamp: timestamp,
Status: "success",
}
records = append(records, record)
// 限制数量
if len(records) >= 50 {
break
}
}
return records
}
// collectCurrentLogins 收集当前登录
func (uac *UserAssetsCollector) collectCurrentLogins() []protocol.LoginSession {
var sessions []protocol.LoginSession
// 使用 w 命令
output, err := uac.executor.Execute("w", "-h")
if err != nil {
globalLogger.Debug("获取当前登录失败: %v", err)
return sessions
}
lines := strings.Split(output, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) < 4 {
continue
}
username := fields[0]
terminal := fields[1]
ip := fields[2]
// 解析空闲时间
idleStr := fields[3]
idleTime := 0
if idleStr != "-" {
// 简化处理,不解析复杂格式
idleTime = parseInt(idleStr)
}
session := protocol.LoginSession{
Username: username,
Terminal: terminal,
IP: ip,
LoginTime: time.Now().UnixMilli(),
IdleTime: idleTime,
}
sessions = append(sessions, session)
}
return sessions
}
// collectSSHKeys 收集SSH密钥
func (uac *UserAssetsCollector) collectSSHKeys() []protocol.SSHKeyInfo {
var keys []protocol.SSHKeyInfo
// 获取所有用户目录
userDirs := uac.getAllUserDirectories()
for _, userDir := range userDirs {
keyPath := filepath.Join(userDir, ".ssh", "authorized_keys")
info, err := os.Stat(keyPath)
if err != nil {
continue
}
// 读取密钥文件
file, err := os.Open(keyPath)
if err != nil {
continue
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.Fields(line)
if len(parts) < 2 {
continue
}
keyType := parts[0]
comment := ""
if len(parts) > 2 {
comment = parts[2]
}
// 计算指纹 (简化,只取前16个字符)
fingerprint := ""
if len(parts[1]) > 16 {
fingerprint = parts[1][:16] + "..."
} else {
fingerprint = parts[1]
}
keyInfo := protocol.SSHKeyInfo{
Username: filepath.Base(filepath.Dir(filepath.Dir(keyPath))),
KeyType: keyType,
Fingerprint: fingerprint,
Comment: comment,
FilePath: keyPath,
AddedTime: info.ModTime().UnixMilli(),
}
keys = append(keys, keyInfo)
}
file.Close()
}
return keys
}
// collectSudoUsers 收集Sudo用户
func (uac *UserAssetsCollector) collectSudoUsers() []protocol.SudoUserInfo {
var sudoUsers []protocol.SudoUserInfo
// 读取 /etc/sudoers
sudoersFile, err := os.Open("/etc/sudoers")
if err != nil {
globalLogger.Debug("读取sudoers失败: %v", err)
return sudoUsers
}
defer sudoersFile.Close()
scanner := bufio.NewScanner(sudoersFile)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// 简单匹配包含 ALL 的行
if !strings.Contains(line, "ALL") {
continue
}
fields := strings.Fields(line)
if len(fields) < 1 {
continue
}
username := fields[0]
if strings.HasPrefix(username, "%") {
// 组,跳过
continue
}
sudoUser := protocol.SudoUserInfo{
Username: username,
Rules: line,
NoPasswd: strings.Contains(line, "NOPASSWD"),
}
sudoUsers = append(sudoUsers, sudoUser)
}
return sudoUsers
}
// getAllUserDirectories 获取所有用户目录
func (uac *UserAssetsCollector) getAllUserDirectories() []string {
var dirs []string
// 从 /etc/passwd 读取
file, err := os.Open("/etc/passwd")
if err != nil {
return dirs
}
defer file.Close()
seen := make(map[string]bool)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
parts := strings.Split(scanner.Text(), ":")
if len(parts) >= 6 {
homeDir := parts[5]
if homeDir != "" && !seen[homeDir] {
dirs = append(dirs, homeDir)
seen[homeDir] = true
}
}
}
return dirs
}
// collectSSHConfig 收集SSH配置
func (uac *UserAssetsCollector) collectSSHConfig() *protocol.SSHConfig {
configPath := "/etc/ssh/sshd_config"
// 检查配置文件是否存在
if _, err := os.Stat(configPath); err != nil {
globalLogger.Debug("SSH配置文件不存在: %v", err)
return nil
}
file, err := os.Open(configPath)
if err != nil {
globalLogger.Debug("读取SSH配置失败: %v", err)
return nil
}
defer file.Close()
// 默认值(OpenSSH默认配置)
config := &protocol.SSHConfig{
Port: 22,
PermitRootLogin: "prohibit-password", // 默认值因发行版而异
PasswordAuthentication: true,
PubkeyAuthentication: true,
PermitEmptyPasswords: false,
MaxAuthTries: 6,
ClientAliveInterval: 0,
ClientAliveCountMax: 3,
X11Forwarding: false,
UsePAM: true,
ConfigFilePath: configPath,
}
// 解析配置文件
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// 跳过注释和空行
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// 解析配置项
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
key := strings.ToLower(fields[0])
value := strings.Join(fields[1:], " ")
switch key {
case "port":
if port := parseInt(value); port > 0 {
config.Port = port
}
case "permitrootlogin":
config.PermitRootLogin = strings.ToLower(value)
case "passwordauthentication":
config.PasswordAuthentication = parseBool(value)
case "pubkeyauthentication":
config.PubkeyAuthentication = parseBool(value)
case "permitemptypasswords":
config.PermitEmptyPasswords = parseBool(value)
case "protocol":
config.Protocol = value
case "maxauthtries":
if tries := parseInt(value); tries > 0 {
config.MaxAuthTries = tries
}
case "clientaliveinterval":
config.ClientAliveInterval = parseInt(value)
case "clientalivecountmax":
config.ClientAliveCountMax = parseInt(value)
case "x11forwarding":
config.X11Forwarding = parseBool(value)
case "usepam":
config.UsePAM = parseBool(value)
}
}
return config
}
// parseInt 解析整数
func parseInt(s string) int {
result, _ := strconv.Atoi(s)
return result
}
// parseBool 解析布尔值
func parseBool(s string) bool {
s = strings.ToLower(s)
return s == "yes" || s == "true" || s == "1"
}
// calculateStatistics 计算统计信息
func (uac *UserAssetsCollector) calculateStatistics(assets *protocol.UserAssets) *protocol.UserStatistics {
stats := &protocol.UserStatistics{
TotalUsers: len(assets.SystemUsers),
RecentLoginCount: len(assets.LoginHistory),
}
for _, user := range assets.SystemUsers {
if user.IsLoginable {
stats.LoginableUsers++
}
if user.IsRootEquiv {
stats.RootEquivalentUsers++
}
}
// 统计失败登录 (可以从 /var/log/auth.log 读取,这里简化)
stats.FailedLoginCount = 0
return stats
}