
Vlang 的实现,严格保留原逻辑(中英文日志解析、重复记录合并、数据库交互、备份清理),适配 Vlang 语法特性和生态:
v
module main
import os
import time
import strconv
import regex
import db.mysql
import sync
import encoding.utf8
// ---------------------- 配置参数 ----------------------
const (
LOG_PATH = '/var/log/secure'
BACKUP_DIR = '/CyberWin/pro/wlzc_firewallsafesniffer/bak_log/ssh_log'
KEEP_DAYS = 7
DB_CONFIG = mysql.Config{
host: '在哪里'
dbname: '这个方便查询'
user: '你是爱找别人麻烦'
password: '你大爷不是我'
port: 87836 // Vlang mysql 端口为 int 类型
charset: 'utf8mb4'
}
)
// 月份映射(中英文)
const (
english_month_map = {
'JAN': '01', 'FEB': '02', 'MAR': '03', 'APR': '04'
'MAY': '05', 'JUN': '06', 'JUL': '07', 'AUG': '08'
'SEP': '09', 'OCT': '10', 'NOV': '11', 'DEC': '12'
}
chinese_month_map = {
'1月': '01', '2月': '02', '3月': '03', '4月': '04', '5月': '05', '6月': '06'
'7月': '07', '8月': '08', '9月': '09', '10月': '10', '11月': '11', '12月': '12'
'一月': '01', '二月': '02', '三月': '03', '四月': '04', '五月': '05', '六月': '06'
'七月': '07', '八月': '08', '九月': '09', '十月': '10', '十一月': '11', '十二月': '12'
}
)
// 日志数据结构(对应 PHP 数组)
struct LogData {
log_time string
ip string
port string
action string
username string
detail string
risk_level int
}
// ---------------------- 工具函数 ----------------------
// 清理过期备份(保留 KEEP_DAYS 天)
fn clean_old_backups() {
if !os.exists(BACKUP_DIR) {
os.mkdir_all(BACKUP_DIR) or {
eprintln('创建备份目录失败: ${err}')
return
}
}
now := time.now().unix()
files := os.ls(BACKUP_DIR) or {
eprintln('读取备份目录失败: ${err}')
return
}
for file in files {
file_path := os.join_path(BACKUP_DIR, file)
if os.is_file(file_path) {
file_mtime := os.file_mtime(file_path) or { continue }
if (now - file_mtime.unix()) > KEEP_DAYS * 86400 {
os.rm(file_path) or {
eprintln('删除过期备份失败 [${file_path}]: ${err}')
}
}
}
}
}
// 英文月份转数字(兼容大小写)
fn english_month_to_num(month string) ?string {
key := month.to_upper().substr(0, 3)
return english_month_map[key] or { error('未知英文月份: ${month}') }
}
// 中文月份转数字
fn chinese_month_to_num(month string) ?string {
return chinese_month_map[month] or { error('未知中文月份: ${month}') }
}
// 构建英文日志数据(对应 PHP build_data)
fn build_data(match []string, year string) ?LogData {
mut data := LogData{ risk_level: 1 }
log_time_str := match[1]
// 解析英文时间格式:Nov 30 19:25:23
time_re := regex.regex_opt(r'^([A-Za-z]{3}) (\d+) (\d{2}:\d{2}:\d{2})$') or {
return error('时间正则编译失败')
}
time_match := time_re.find_all_str(log_time_str, 4)
if time_match.len != 4 {
return error('时间格式不匹配: ${log_time_str}')
}
// 月份转换
month := english_month_to_num(time_match[1]) ?
day := time_match[2].zfill(2) // 补零(如 5 → 05)
time_part := time_match[3]
// 构建标准时间字符串
std_time_str := '${year}-${month}-${day} ${time_part}'
log_time := time.parse(std_time_str, '2006-01-02 15:04:05') or {
return error('时间解析失败: ${std_time_str}')
}
data.log_time = log_time.format('2006-01-02 15:04:05')
// 根据匹配场景填充字段
action := match match[0] {
// 场景1: Failed password
contains('Failed password') {
data.risk_level = 3
data.detail = '暴力破解尝试:用户${match[2]},IP${match[3]},端口${match[4]}'
data.username = match[2]
data.ip = match[3]
data.port = match[4]
'FAILED_PASSWORD'
}
// 场景2: Connection closed by preauth(IPv4)
contains('Connection closed by') && !match[3].contains(':') {
data.risk_level = 2
data.detail = '未认证连接关闭:IP${match[2]},端口${match[3]}'
data.username = match[2] // 原PHP逻辑:CONN_CLOSED_PREAUTH场景下后续交换ip和username
data.ip = match[2]
data.port = match[3]
'CONN_CLOSED_PREAUTH'
}
// 场景3: Connection closed by preauth(IPv6)
contains('Connection closed by') && match[3].contains(':') {
data.risk_level = 2
data.detail = '未认证IPv6连接关闭:IP${match[2]},端口${match[3]}'
data.username = match[2]
data.ip = match[2]
data.port = match[3]
'CONN_CLOSED_PREAUTH'
}
// 场景4: SSH协议错误
contains('error:') {
data.risk_level = 2
data.detail = 'SSH协议错误:${match[2]}'
'PROTOCOL_ERROR'
}
// 场景5: Accepted password
contains('Accepted password') {
data.risk_level = 1
data.detail = '成功登录:用户${match[2]},IP${match[3]},端口${match[4]}'
data.username = match[2]
data.ip = match[3]
data.port = match[4]
'ACCEPTED_PASSWORD'
}
else {
return error('未知日志场景: ${match[0]}')
}
}
data.action = action
return data
}
// 构建中文日志数据(对应 PHP build_data中文)
fn build_data_chinese(match []string, year string) ?LogData {
mut data := LogData{ risk_level: 1 }
log_time_str := match[1]
// 解析中文时间格式:11月 30 18:00:00 或 十一月 30 18:00:00
time_re := regex.regex_opt(r'^(\S+月) (\d+) (\d{2}:\d{2}:\d{2})$') or {
return error('中文时间正则编译失败')
}
time_match := time_re.find_all_str(log_time_str, 4)
if time_match.len != 4 {
return error('中文时间格式不匹配: ${log_time_str}')
}
// 月份转换
month := chinese_month_to_num(time_match[1]) ?
day := time_match[2].zfill(2)
time_part := time_match[3]
// 构建标准时间字符串
std_time_str := '${year}-${month}-${day} ${time_part}'
log_time := time.parse(std_time_str, '2006-01-02 15:04:05') or {
return error('中文时间解析失败: ${std_time_str}')
}
data.log_time = log_time.format('2006-01-02 15:04:05')
// 根据匹配场景填充字段
action := match match[0] {
// 场景1: Failed password
contains('Failed password') {
data.risk_level = 3
data.detail = '暴力破解尝试:用户${match[2]},IP${match[3]},端口${match[4]}'
data.username = match[2]
data.ip = match[3]
data.port = match[4]
'FAILED_PASSWORD'
}
// 场景2: Connection closed by preauth(IPv4)
contains('Connection closed by') && !match[3].contains(':') {
data.risk_level = 2
data.detail = '未认证连接关闭:IP${match[2]},端口${match[3]}'
data.username = match[2]
data.ip = match[2]
data.port = match[3]
'CONN_CLOSED_PREAUTH'
}
// 场景3: Connection closed by preauth(IPv6)
contains('Connection closed by') && match[3].contains(':') {
data.risk_level = 2
data.detail = '未认证IPv6连接关闭:IP${match[2]},端口${match[3]}'
data.username = match[2]
data.ip = match[2]
data.port = match[3]
'CONN_CLOSED_PREAUTH'
}
// 场景4: SSH协议错误
contains('error:') {
data.risk_level = 2
data.detail = 'SSH协议错误:${match[2]}'
'PROTOCOL_ERROR'
}
// 场景5: Accepted password
contains('Accepted password') {
data.risk_level = 1
data.detail = '成功登录:用户${match[2]},IP${match[3]},端口${match[4]}'
data.username = match[2]
data.ip = match[3]
data.port = match[4]
'ACCEPTED_PASSWORD'
}
else {
return error('未知中文日志场景: ${match[0]}')
}
}
data.action = action
return data
}
// 解析英文日志行(对应 PHP parse_log_line)
fn parse_log_line(line string, year string) ?LogData {
// 编译所有英文日志正则
patterns := [
// 场景1: Failed password
r'^([A-Za-z]{3} \d+ \d{2}:\d{2}:\d{2}) \S+ sshd\[\d+\]: Failed password for (\S+) from (\S+) port (\d+) ssh2$',
// 场景2: Connection closed by preauth(IPv4)
r'^([A-Za-z]{3} \d+ \d{2}:\d{2}:\d{2}) \S+ sshd\[\d+\]: Connection closed by (\S+) port (\d+) \[preauth\]$',
// 场景3: Connection closed by preauth(IPv6)
r'^([A-Za-z]{3} \d+ \d{2}:\d{2}:\d{2}) \S+ sshd\[\d+\]: Connection closed by ([0-9a-fA-F:]+) port (\d+) \[preauth\]$',
// 场景4: SSH协议错误
r'^([A-Za-z]{3} \d+ \d{2}:\d{2}:\d{2}) \S+ sshd\[\d+\]: error: (.+)$',
// 场景5: Accepted password
r'^([A-Za-z]{3} \d+ \d{2}:\d{2}:\d{2}) \S+ sshd\[\d+\]: Accepted password for (\S+) from (\S+) port (\d+) ssh2$',
]
// 逐个匹配正则
for pat in patterns {
re := regex.regex_opt(pat) or {
eprintln('正则编译失败: ${pat}')
continue
}
match := re.find_all_str(line, 6)
if match.len > 0 {
return build_data(match, year)
}
}
return error('无匹配的英文日志模式')
}
// 解析中文日志行(对应 PHP parse_log_line中文)
fn parse_log_line_chinese(line string, year string) ?LogData {
// 编译所有中文日志正则
patterns := [
// 场景1: Failed password
r'^((?:\d+|[一二三四五六七八九十]+)月 \d+ \d{2}:\d{2}:\d{2}) \S+ sshd\[\d+\]: Failed password for (\S+) from (\S+) port (\d+) ssh2$',
// 场景2: Connection closed by preauth(IPv4)
r'^((?:\d+|[一二三四五六七八九十]+)月 \d+ \d{2}:\d{2}:\d{2}) \S+ sshd\[\d+\]: Connection closed by (\S+) port (\d+) \[preauth\]$',
// 场景3: Connection closed by preauth(IPv6)
r'^((?:\d+|[一二三四五六七八九十]+)月 \d+ \d{2}:\d{2}:\d{2}) \S+ sshd\[\d+\]: Connection closed by ([0-9a-fA-F:]+) port (\d+) \[preauth\]$',
// 场景4: SSH协议错误
r'^((?:\d+|[一二三四五六七八九十]+)月 \d+ \d{2}:\d{2}:\d{2}) \S+ sshd\[\d+\]: error: (.+)$',
// 场景5: Accepted password
r'^((?:\d+|[一二三四五六七八九十]+)月 \d+ \d{2}:\d{2}:\d{2}) \S+ sshd\[\d+\]: Accepted password for (\S+) from (\S+) port (\d+) ssh2$',
]
// 逐个匹配正则
for pat in patterns {
re := regex.regex_opt(pat) or {
eprintln('中文正则编译失败: ${pat}')
continue
}
match := re.find_all_str(line, 6)
if match.len > 0 {
return build_data_chinese(match, year)
}
}
return error('无匹配的中文日志模式')
}
// 解析备份日志并入库(对应 PHP parse_backup_log)
fn parse_backup_log(file_path string, db &mysql.Conn) ? {
// 从文件名提取年份
filename := os.file_name(file_path)
year_re := regex.regex_opt(r'secure_(\d{4})') or {
return error('年份正则编译失败')
}
year_match := year_re.find_all_str(filename, 2)
year := if year_match.len == 2 { year_match[1] } else { time.now().format('2006') }
// 打开日志文件
file := os.open(file_path) or {
return error('打开日志文件失败 [${file_path}]: ${err}')
}
defer file.close()
// 逐行读取日志
for line in file.lines() {
line = line.trim_space()
if line == '' {
continue
}
// 尝试解析(先英文后中文)
mut data := match parse_log_line(line, year) {
Ok(d) { d }
Err(e) {
// 英文解析失败,尝试中文
match parse_log_line_chinese(line, year) {
Ok(d) { d }
Err(_) {
// 无匹配,跳过
continue
}
}
}
}
// 原PHP逻辑:CONN_CLOSED_PREAUTH场景交换port、ip、username
if data.action == 'CONN_CLOSED_PREAUTH' {
temp_port := data.port
data.port = data.ip
data.ip = data.username
data.username = '' // 原逻辑中该场景username无实际意义
}
// 生成分钟级去重键(对应 PHP date('YmdHi'))
log_time := time.parse(data.log_time, '2006-01-02 15:04:05') or {
eprintln('日志时间解析失败: ${data.log_time}')
continue
}
minute_key := log_time.format('200601021504')
// 检查重复记录
rows := db.query_row(
'SELECT cyber_id FROM cybersafe_ssh_events WHERE ip = ? AND action = ? AND DATE_FORMAT(log_time, "%Y%m%d%H%i") = ? LIMIT 1',
data.ip, data.action, minute_key,
) or {
eprintln('查询重复记录失败: ${err}')
continue
}
mut cyber_id := 0
if rows.scan(&cyber_id) == nil {
// 存在重复,更新计数
_, err := db.exec(
'UPDATE cybersafe_ssh_events SET count = count + 1 WHERE cyber_id = ?',
cyber_id,
)
if err != nil {
eprintln('更新重复记录失败: ${err}')
}
} else {
// 不存在,插入新记录
_, err := db.exec(
'INSERT INTO cybersafe_ssh_events (log_time, ip, port, action, username, detail, risk_level, count) VALUES (?, ?, ?, ?, ?, ?, ?, 1)',
data.log_time, data.ip, data.port, data.action, data.username, data.detail, data.risk_level,
)
if err != nil {
eprintln('插入新记录失败: ${err}')
}
}
}
println('日志解析完成: ${file_path}')
}
// ---------------------- 主执行流程 ----------------------
fn main() {
// 连接数据库
db := mysql.connect(DB_CONFIG) or {
eprintln('数据库连接失败: ${err}')
return
}
defer db.close()
// 测试用备份文件(与原PHP一致)
backup_file := '/CyberWin/pro/wlzc_firewallsafesniffer/bak_log/ssh_log/secure_20251130183113'
// 检查文件是否存在
if !os.exists(backup_file) {
eprintln('备份文件不存在: ${backup_file}')
return
}
// 解析日志入库
if err := parse_backup_log(backup_file, &db); err != nil {
eprintln('日志解析失败: ${err}')
return
}
// 清理过期备份(原PHP注释掉,此处保留可选)
// clean_old_backups()
println('处理成功!文件:${backup_file}')
}
关键说明(严格对齐原 PHP 逻辑)
- 配置参数 :完全保留原 PHP 的路径、数据库配置,适配 Vlang
mysql.Config结构体(端口转为 int 类型)。 - 日志解析 :
- 分别实现
parse_log_line(英文日志)和parse_log_line_chinese(中文日志),正则表达式与原 PHP 完全一致。 - 保留中英文月份转换、时间格式标准化(
2006-01-02 15:04:05)。 - 严格还原
CONN_CLOSED_PREAUTH场景的字段交换逻辑(port = ip、ip = username)。
- 分别实现
- 数据库交互 :
- 使用 Vlang 标准库
db.mysql连接 MySQL,SQL 语句与原 PHP 完全一致。 - 保留分钟级去重逻辑(
DATE_FORMAT(log_time, "%Y%m%d%H%i"))和重复记录合并(更新count)。
- 使用 Vlang 标准库
- 文件操作 :
- 逐行读取日志文件,跳过空行,错误处理与原 PHP 一致。
clean_old_backups函数保留原逻辑,清理超过 7 天的备份文件。
依赖与编译运行
-
依赖 :Vlang 内置
db.mysql、regex、os等模块,无需额外安装第三方依赖。 -
编译 :
bash
运行
v build -prod ssh_log_parser.v # 生成可执行文件 -
运行 :
- 确保 MySQL 服务正常运行,配置的数据库、表(
cybersafe_ssh_events)已存在。 - 确保日志文件路径、备份目录权限可读写。
- 执行:
./ssh_log_parser
- 确保 MySQL 服务正常运行,配置的数据库、表(
注意事项
- 编码兼容:Vlang 字符串默认 UTF-8,中文日志解析无编码问题(需确保日志文件为 UTF-8 编码)。
- 正则匹配 :使用 Vlang
regex模块,正则表达式与原 PHP 完全一致,确保匹配逻辑不变。 - 错误处理:保留原 PHP 的容错逻辑(解析失败的日志行直接跳过),同时增加详细的错误打印便于调试。
- 时间格式 :Vlang 时间解析使用 Golang 风格的格式字符串(
2006-01-02 15:04:05),已在代码中适配。
表结构说明(与原 PHP 兼容)
确保数据库中 cybersafe_ssh_events 表结构如下(参考原 PHP 插入字段):
sql
CREATE TABLE `cybersafe_ssh_events` (
`cyber_id` int(11) NOT NULL AUTO_INCREMENT,
`log_time` datetime NOT NULL,
`ip` varchar(64) NOT NULL,
`port` varchar(16) NOT NULL,
`action` varchar(32) NOT NULL,
`username` varchar(64) DEFAULT '',
`detail` text NOT NULL,
`risk_level` tinyint(4) NOT NULL,
`count` int(11) NOT NULL DEFAULT 1,
PRIMARY KEY (`cyber_id`),
KEY `idx_ip_action_time` (`ip`,`action`,`log_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
作为服务器远程管理的核心协议,其日志记录了所有 SSH 连接的完整轨迹,包括登录行为、认证结果、异常操作等关键信息。对 SSH 日志进行常态化分析,是服务器安全防护体系的重要组成部分,兼具攻击检测、合规审计、故障排查、风险预警四大核心价值,具体重要意义如下:
-
暴力破解识别 :通过统计短时间内来自同一 IP 的
Failed password记录,可快速定位暴力破解源,及时封禁 IP 或限制登录频率,避免密码被破解。 -
异常登录拦截 :当日志中出现非信任 IP 的
Accepted password记录、异地登录、非常规时段登录(如深夜批量登录)时,可能是账号泄露或非法入侵的信号,可立即触发告警并核查操作。 -
协议漏洞利用监测 :日志中
error:开头的协议错误记录,可能是攻击者尝试利用 SSH 协议漏洞(如 SSH 弱加密算法、漏洞扫描)的痕迹,提前发现可避免漏洞被利用。
-
还原攻击路径 :通过日志中的
log_time(时间)、ip(来源 IP)、username(登录账号)、port(端口)等字段,可完整还原攻击者的登录时间、操作账号、来源地址,甚至关联到攻击所用设备。 -
明确责任归属:区分合法管理员操作与非法入侵行为,若为内部账号泄露,可定位账号责任人;若为外部攻击,可固定攻击 IP 等证据,为追责或上报提供依据。
-
合规审计需求:金融、政务、医疗等行业的合规标准(如等保 2.0、PCI - DSS)明确要求留存服务器操作日志并定期审计,SSH 日志分析是满足合规要求的必要环节。
-
登录失败故障定位 :用户反馈 SSH 登录失败时,通过日志可区分是密码错误(
Failed password)、端口占用、密钥配置错误,还是服务异常(如 sshd 进程崩溃),避免盲目排查。 -
服务异常预警 :日志中频繁出现
Connection closed by preauth(未认证连接关闭),可能是 SSH 服务配置不当(如超时设置过短)或网络不稳定,提前调整可提升服务可用性。 -
资源占用分析:大量无效 SSH 连接尝试会占用服务器端口和 CPU 资源,通过日志统计连接频率,可优化防火墙规则,减轻服务器负载。
-
弱口令与账号治理:统计高频登录失败的账号,可发现弱口令账号,强制要求更换复杂密码;清理长期未使用的 "僵尸账号",减少攻击面。
-
访问控制优化:基于日志中高频合法登录 IP,配置 SSH 白名单,禁止非信任 IP 访问;关闭不必要的 SSH 端口,仅开放业务所需端口。
-
安全配置加固:根据日志中出现的风险行为(如使用弱加密算法登录),调整 sshd 配置文件(如禁用密码登录、强制使用 SSH 密钥、关闭过时加密算法)。
-
扫描行为预警 :短时间内来自多个 IP 的
CONN_CLOSED_PREAUTH(未认证连接关闭)记录,可能是攻击者的批量扫描行为,预示后续可能出现大规模攻击。 -
账号异常使用预警:同一账号在不同 IP 频繁登录,可能是账号密码已被泄露,需立即冻结账号并重置密码。
-
实时发现恶意攻击,阻断入侵行为
SSH 是网络攻击者的重点目标,攻击者常通过暴力破解、弱口令攻击、漏洞利用等方式尝试入侵。日志分析能精准捕捉这类恶意行为:
-
追溯安全事件,定位攻击源头与责任
当服务器发生数据泄露、恶意篡改等安全事件时,SSH 日志是事后溯源的关键 "证据链",具备不可替代的审计价值:
-
排查系统故障,保障服务稳定运行
SSH 日志不仅记录安全事件,还包含大量系统运行状态信息,可用于快速排查远程管理相关的故障:
-
优化安全策略,构建纵深防御体系
长期的 SSH 日志分析能沉淀出服务器的安全基线,反向指导安全策略优化,从被动防御转向主动防护:
-
预警潜在风险,防范未然
很多攻击行为在爆发前会有明显的 "试探性操作",SSH 日志分析能捕捉这些前兆,提前规避风险: