- 内置 SSH 终端(/terminal):复用已保存的 SSH 密码/私钥与独立 known_hosts 校验 - 终端二次认证、会话审计(/terminal/audits)、录像审计与常用命令 - DDNS 服务商名称与自定义 IP,新增迁移 037/038/039 - CHANGELOG 与 VERSION 更新至 2.0.0
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AuditRetention 是审计记录的保留期:超过就由定时任务清理,避免表无限增长。
|
||||
const AuditRetention = 90 * 24 * time.Hour
|
||||
|
||||
// AuditEntry 是一条终端会话审计记录,回答「谁、何时、连了哪台、多久、为什么断开」。
|
||||
type AuditEntry struct {
|
||||
ID int64 `json:"id,omitempty"`
|
||||
SessionID string `json:"session_id"`
|
||||
ServerID int64 `json:"server_id"`
|
||||
Target string `json:"target"`
|
||||
User string `json:"user"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
StartedAt string `json:"started_at"`
|
||||
EndedAt string `json:"ended_at"`
|
||||
DurationMS int64 `json:"duration_ms"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
BytesIn int64 `json:"bytes_in"`
|
||||
BytesOut int64 `json:"bytes_out"`
|
||||
}
|
||||
|
||||
// AuditSink 由会话管理器在会话结束时调用;未注入时审计只落日志。
|
||||
type AuditSink interface {
|
||||
WriteAudit(ctx context.Context, entry AuditEntry) error
|
||||
}
|
||||
|
||||
// AuditStore 把审计记录落到 SQLite,并提供查询与清理。
|
||||
type AuditStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewAuditStore(db *sql.DB) *AuditStore {
|
||||
return &AuditStore{db: db}
|
||||
}
|
||||
|
||||
// WriteAudit 写入一条会话审计。
|
||||
func (s *AuditStore) WriteAudit(ctx context.Context, entry AuditEntry) error {
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
INSERT INTO terminal_audit_logs(
|
||||
session_id, server_id, target, username, client_ip,
|
||||
started_at, ended_at, duration_ms, reason, bytes_in, bytes_out
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
entry.SessionID, entry.ServerID, entry.Target, entry.User, entry.ClientIP,
|
||||
entry.StartedAt, entry.EndedAt, entry.DurationMS, entry.Reason, entry.BytesIn, entry.BytesOut)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListAudits 按结束时间倒序返回最近的审计记录;limit 缺省 100,上限 500。
|
||||
func (s *AuditStore) ListAudits(ctx context.Context, limit int) ([]AuditEntry, error) {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
if limit > 500 {
|
||||
limit = 500
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT id, session_id, server_id, target, username, client_ip,
|
||||
started_at, ended_at, duration_ms, COALESCE(reason, ''), bytes_in, bytes_out
|
||||
FROM terminal_audit_logs
|
||||
ORDER BY id DESC
|
||||
LIMIT ?`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
entries := make([]AuditEntry, 0, limit)
|
||||
for rows.Next() {
|
||||
var entry AuditEntry
|
||||
if err := rows.Scan(
|
||||
&entry.ID, &entry.SessionID, &entry.ServerID, &entry.Target,
|
||||
&entry.User, &entry.ClientIP, &entry.StartedAt, &entry.EndedAt,
|
||||
&entry.DurationMS, &entry.Reason, &entry.BytesIn, &entry.BytesOut,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return entries, rows.Err()
|
||||
}
|
||||
|
||||
// PruneAudits 清理超过保留期的审计记录,返回删除条数。
|
||||
func (s *AuditStore) PruneAudits(ctx context.Context) (int64, error) {
|
||||
cutoff := time.Now().UTC().Add(-AuditRetention).Format(time.RFC3339)
|
||||
res, err := s.db.ExecContext(ctx, `DELETE FROM terminal_audit_logs WHERE ended_at < ?`, cutoff)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
}
|
||||
Reference in New Issue
Block a user