Add files via upload

This commit is contained in:
公明
2026-07-10 16:42:35 +08:00
committed by GitHub
parent 225d9ba0d9
commit a2744d6936
14 changed files with 2631 additions and 62 deletions
+507 -32
View File
@@ -45,20 +45,21 @@ func validC2TextIDForDelete(id string) bool {
// C2Listener 监听器实体
type C2Listener struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"` // tcp_reverse|http_beacon|https_beacon|websocket|dns
BindHost string `json:"bindHost"` // 默认 127.0.0.1
BindPort int `json:"bindPort"` // 1-65535
ProfileID string `json:"profileId"` // 可空:关联 c2_profiles.id
EncryptionKey string `json:"-"` // base64(AES-256),前端不返回
ImplantToken string `json:"-"` // beacon 携带的鉴权 token,前端不返回
Status string `json:"status"` // stopped|running|error
ConfigJSON string `json:"configJson"` // TLS 证书路径 / URI 模式 / 上限并发 等
Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"` // tcp_reverse|http_beacon|https_beacon|websocket|dns
BindHost string `json:"bindHost"` // 默认 127.0.0.1
BindPort int `json:"bindPort"` // 1-65535
ProfileID string `json:"profileId"` // 可空:关联 c2_profiles.id
EncryptionKey string `json:"-"` // base64(AES-256),前端不返回
ImplantToken string `json:"-"` // beacon 携带的鉴权 token,前端不返回
Status string `json:"status"` // stopped|running|error
ConfigJSON string `json:"configJson"` // TLS 证书路径 / URI 模式 / 上限并发 等
Remark string `json:"remark"`
OwnerUserID string `json:"ownerUserId,omitempty"`
CreatedAt time.Time `json:"createdAt"`
StartedAt *time.Time `json:"startedAt,omitempty"`
LastError string `json:"lastError,omitempty"`
LastError string `json:"lastError,omitempty"`
}
// C2Session 已上线会话
@@ -132,17 +133,17 @@ type C2Event struct {
// C2Profile Malleable Profile
type C2Profile struct {
ID string `json:"id"`
Name string `json:"name"`
UserAgent string `json:"userAgent"`
URIs []string `json:"uris"`
RequestHeaders map[string]string `json:"requestHeaders,omitempty"`
ResponseHeaders map[string]string `json:"responseHeaders,omitempty"`
BodyTemplate string `json:"bodyTemplate"`
JitterMinMS int `json:"jitterMinMs"`
JitterMaxMS int `json:"jitterMaxMs"`
Extra map[string]interface{} `json:"extra,omitempty"`
CreatedAt time.Time `json:"createdAt"`
ID string `json:"id"`
Name string `json:"name"`
UserAgent string `json:"userAgent"`
URIs []string `json:"uris"`
RequestHeaders map[string]string `json:"requestHeaders,omitempty"`
ResponseHeaders map[string]string `json:"responseHeaders,omitempty"`
BodyTemplate string `json:"bodyTemplate"`
JitterMinMS int `json:"jitterMinMs"`
JitterMaxMS int `json:"jitterMaxMs"`
Extra map[string]interface{} `json:"extra,omitempty"`
CreatedAt time.Time `json:"createdAt"`
}
// ----------------------------------------------------------------------------
@@ -165,12 +166,12 @@ func (db *DB) CreateC2Listener(l *C2Listener) error {
}
query := `
INSERT INTO c2_listeners (id, name, type, bind_host, bind_port, profile_id, encryption_key,
implant_token, status, config_json, remark, created_at, started_at, last_error)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
implant_token, status, config_json, remark, owner_user_id, created_at, started_at, last_error)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
_, err := db.Exec(query,
l.ID, l.Name, l.Type, l.BindHost, l.BindPort, l.ProfileID, l.EncryptionKey,
l.ImplantToken, l.Status, l.ConfigJSON, l.Remark, l.CreatedAt, l.StartedAt, l.LastError,
l.ImplantToken, l.Status, l.ConfigJSON, l.Remark, l.OwnerUserID, l.CreatedAt, l.StartedAt, l.LastError,
)
if err != nil {
db.logger.Error("创建 C2 监听器失败", zap.Error(err), zap.String("id", l.ID))
@@ -190,12 +191,12 @@ func (db *DB) UpdateC2Listener(l *C2Listener) error {
query := `
UPDATE c2_listeners SET
name = ?, type = ?, bind_host = ?, bind_port = ?, profile_id = ?, encryption_key = ?,
implant_token = ?, status = ?, config_json = ?, remark = ?, started_at = ?, last_error = ?
implant_token = ?, status = ?, config_json = ?, remark = ?, owner_user_id = ?, started_at = ?, last_error = ?
WHERE id = ?
`
res, err := db.Exec(query,
l.Name, l.Type, l.BindHost, l.BindPort, l.ProfileID, l.EncryptionKey,
l.ImplantToken, l.Status, l.ConfigJSON, l.Remark, l.StartedAt, l.LastError, l.ID,
l.ImplantToken, l.Status, l.ConfigJSON, l.Remark, l.OwnerUserID, l.StartedAt, l.LastError, l.ID,
)
if err != nil {
db.logger.Error("更新 C2 监听器失败", zap.Error(err), zap.String("id", l.ID))
@@ -231,7 +232,7 @@ func (db *DB) GetC2Listener(id string) (*C2Listener, error) {
SELECT id, name, type, bind_host, bind_port, COALESCE(profile_id, ''),
COALESCE(encryption_key, ''), COALESCE(implant_token, ''), status,
COALESCE(config_json, '{}'), COALESCE(remark, ''),
created_at, started_at, COALESCE(last_error, '')
COALESCE(owner_user_id, ''), created_at, started_at, COALESCE(last_error, '')
FROM c2_listeners WHERE id = ?
`
var l C2Listener
@@ -240,7 +241,7 @@ func (db *DB) GetC2Listener(id string) (*C2Listener, error) {
&l.ID, &l.Name, &l.Type, &l.BindHost, &l.BindPort, &l.ProfileID,
&l.EncryptionKey, &l.ImplantToken, &l.Status,
&l.ConfigJSON, &l.Remark,
&l.CreatedAt, &startedAt, &l.LastError,
&l.OwnerUserID, &l.CreatedAt, &startedAt, &l.LastError,
)
if err == sql.ErrNoRows {
return nil, nil
@@ -261,7 +262,7 @@ func (db *DB) ListC2Listeners() ([]*C2Listener, error) {
SELECT id, name, type, bind_host, bind_port, COALESCE(profile_id, ''),
COALESCE(encryption_key, ''), COALESCE(implant_token, ''), status,
COALESCE(config_json, '{}'), COALESCE(remark, ''),
created_at, started_at, COALESCE(last_error, '')
COALESCE(owner_user_id, ''), created_at, started_at, COALESCE(last_error, '')
FROM c2_listeners ORDER BY created_at DESC
`
rows, err := db.Query(query)
@@ -277,6 +278,47 @@ func (db *DB) ListC2Listeners() ([]*C2Listener, error) {
&l.ID, &l.Name, &l.Type, &l.BindHost, &l.BindPort, &l.ProfileID,
&l.EncryptionKey, &l.ImplantToken, &l.Status,
&l.ConfigJSON, &l.Remark,
&l.OwnerUserID, &l.CreatedAt, &startedAt, &l.LastError,
); err != nil {
db.logger.Warn("扫描 c2_listeners 行失败", zap.Error(err))
continue
}
if startedAt.Valid {
t := startedAt.Time
l.StartedAt = &t
}
list = append(list, &l)
}
return list, rows.Err()
}
// ListC2ListenersForAccess lists listeners visible to the resolved RBAC scope.
func (db *DB) ListC2ListenersForAccess(access RBACListAccess) ([]*C2Listener, error) {
conditions := []string{"1=1"}
args := []interface{}{}
appendC2ListenerAccessFilter(&conditions, &args, access)
query := `
SELECT id, name, type, bind_host, bind_port, COALESCE(profile_id, ''),
COALESCE(encryption_key, ''), COALESCE(implant_token, ''), status,
COALESCE(config_json, '{}'), COALESCE(remark, ''),
COALESCE(owner_user_id, ''), created_at, started_at, COALESCE(last_error, '')
FROM c2_listeners
WHERE ` + strings.Join(conditions, " AND ") + `
ORDER BY created_at DESC
`
rows, err := db.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var list []*C2Listener
for rows.Next() {
var l C2Listener
var startedAt sql.NullTime
if err := rows.Scan(
&l.ID, &l.Name, &l.Type, &l.BindHost, &l.BindPort, &l.ProfileID,
&l.EncryptionKey, &l.ImplantToken, &l.Status,
&l.ConfigJSON, &l.Remark, &l.OwnerUserID,
&l.CreatedAt, &startedAt, &l.LastError,
); err != nil {
db.logger.Warn("扫描 c2_listeners 行失败", zap.Error(err))
@@ -291,6 +333,26 @@ func (db *DB) ListC2Listeners() ([]*C2Listener, error) {
return list, rows.Err()
}
func appendC2ListenerAccessFilter(conditions *[]string, args *[]interface{}, access RBACListAccess) {
if access.Scope == RBACScopeAll {
return
}
if access.UserID == "" {
*conditions = append(*conditions, "1=0")
return
}
clauses := []string{"owner_user_id = ?"}
*args = append(*args, access.UserID)
if access.Scope == RBACScopeAssigned {
clauses = append(clauses, `EXISTS (
SELECT 1 FROM rbac_resource_assignments ra
WHERE ra.user_id = ? AND ra.resource_type = 'c2_listener' AND ra.resource_id = c2_listeners.id
)`)
*args = append(*args, access.UserID)
}
*conditions = append(*conditions, "("+strings.Join(clauses, " OR ")+")")
}
// DeleteC2Listener 级联删除(会话/任务/文件/事件随之消失)
func (db *DB) DeleteC2Listener(id string) error {
res, err := db.Exec(`DELETE FROM c2_listeners WHERE id = ?`, id)
@@ -550,6 +612,109 @@ func (db *DB) ListC2Sessions(filter ListC2SessionsFilter) ([]*C2Session, error)
return list, rows.Err()
}
// ListC2SessionsForAccess lists sessions whose parent listener is visible.
func (db *DB) ListC2SessionsForAccess(filter ListC2SessionsFilter, access RBACListAccess) ([]*C2Session, error) {
conditions, args := buildC2SessionsWhere(filter)
appendC2SessionAccessFilter(&conditions, &args, access)
query := `
SELECT id, listener_id, implant_uuid, COALESCE(hostname,''), COALESCE(username,''),
COALESCE(os,''), COALESCE(arch,''), COALESCE(pid, 0), COALESCE(process_name,''),
COALESCE(is_admin, 0), COALESCE(internal_ip,''), COALESCE(external_ip,''),
COALESCE(user_agent,''), COALESCE(sleep_seconds, 5), COALESCE(jitter_percent, 0),
status, first_seen_at, last_check_in, COALESCE(metadata_json, '{}'),
COALESCE(note, '')
FROM c2_sessions
WHERE ` + strings.Join(conditions, " AND ") + `
ORDER BY last_check_in DESC
`
if filter.Limit > 0 {
query += fmt.Sprintf(" LIMIT %d", filter.Limit)
}
rows, err := db.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
return db.scanC2SessionRows(rows)
}
func buildC2SessionsWhere(filter ListC2SessionsFilter) ([]string, []interface{}) {
conditions := []string{"1=1"}
args := []interface{}{}
if filter.ListenerID != "" {
conditions = append(conditions, "listener_id = ?")
args = append(args, filter.ListenerID)
}
if filter.Status != "" {
conditions = append(conditions, "status = ?")
args = append(args, filter.Status)
}
if filter.OS != "" {
conditions = append(conditions, "os = ?")
args = append(args, filter.OS)
}
if filter.Search != "" {
conditions = append(conditions, "(hostname LIKE ? OR username LIKE ? OR internal_ip LIKE ?)")
kw := "%" + filter.Search + "%"
args = append(args, kw, kw, kw)
}
if filter.Suspicious {
conditions = append(conditions, `status = 'dead' AND (
hostname LIKE 'tcp_%' OR LOWER(COALESCE(username,'')) = 'unknown' OR COALESCE(pid, 0) = 0
)`)
}
return conditions, args
}
func (db *DB) scanC2SessionRows(rows *sql.Rows) ([]*C2Session, error) {
var list []*C2Session
for rows.Next() {
var s C2Session
var isAdminInt int
var metadataJSON string
if err := rows.Scan(
&s.ID, &s.ListenerID, &s.ImplantUUID, &s.Hostname, &s.Username,
&s.OS, &s.Arch, &s.PID, &s.ProcessName,
&isAdminInt, &s.InternalIP, &s.ExternalIP,
&s.UserAgent, &s.SleepSeconds, &s.JitterPercent,
&s.Status, &s.FirstSeenAt, &s.LastCheckIn, &metadataJSON,
&s.Note,
); err != nil {
db.logger.Warn("扫描 c2_sessions 行失败", zap.Error(err))
continue
}
s.IsAdmin = isAdminInt != 0
if metadataJSON != "" && metadataJSON != "{}" {
_ = json.Unmarshal([]byte(metadataJSON), &s.Metadata)
}
list = append(list, &s)
}
return list, rows.Err()
}
func appendC2SessionAccessFilter(conditions *[]string, args *[]interface{}, access RBACListAccess) {
if access.Scope == RBACScopeAll {
return
}
if access.UserID == "" {
*conditions = append(*conditions, "1=0")
return
}
clauses := []string{`EXISTS (
SELECT 1 FROM c2_listeners
WHERE c2_listeners.id = c2_sessions.listener_id AND c2_listeners.owner_user_id = ?
)`}
*args = append(*args, access.UserID)
if access.Scope == RBACScopeAssigned {
clauses = append(clauses, `EXISTS (
SELECT 1 FROM rbac_resource_assignments ra
WHERE ra.user_id = ? AND ra.resource_type = 'c2_listener' AND ra.resource_id = c2_sessions.listener_id
)`)
*args = append(*args, access.UserID)
}
*conditions = append(*conditions, "("+strings.Join(clauses, " OR ")+")")
}
// DeleteC2Session 级联删除其 tasks/files
func (db *DB) DeleteC2Session(id string) error {
res, err := db.Exec(`DELETE FROM c2_sessions WHERE id = ?`, id)
@@ -601,6 +766,29 @@ func (db *DB) DeleteC2SessionsByIDs(ids []string) (int64, error) {
return res.RowsAffected()
}
func (db *DB) DeleteC2SessionsByIDsForAccess(ids []string, access RBACListAccess) (int64, error) {
if access.Scope == RBACScopeAll {
return db.DeleteC2SessionsByIDs(ids)
}
clean := cleanC2IDs(ids)
if len(clean) == 0 {
return 0, ErrNoValidC2SessionIDs
}
placeholders := strings.Repeat("?,", len(clean)-1) + "?"
args := make([]interface{}, 0, len(clean)+2)
for _, id := range clean {
args = append(args, id)
}
conditions := []string{"id IN (" + placeholders + ")"}
appendC2SessionAccessFilter(&conditions, &args, access)
query := `DELETE FROM c2_sessions WHERE ` + strings.Join(conditions, " AND ")
res, err := db.Exec(query, args...)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
// ----------------------------------------------------------------------------
// CRUDC2 任务
// ----------------------------------------------------------------------------
@@ -778,6 +966,39 @@ func buildC2TasksWhere(filter ListC2TasksFilter) (where string, args []interface
return strings.Join(conditions, " AND "), args
}
func appendC2TaskAccessFilter(conditions *[]string, args *[]interface{}, access RBACListAccess) {
if access.Scope == RBACScopeAll {
return
}
if access.UserID == "" {
*conditions = append(*conditions, "1=0")
return
}
clauses := []string{`EXISTS (
SELECT 1 FROM c2_sessions s
JOIN c2_listeners l ON l.id = s.listener_id
WHERE s.id = c2_tasks.session_id AND l.owner_user_id = ?
)`}
*args = append(*args, access.UserID)
if access.Scope == RBACScopeAssigned {
clauses = append(clauses, `EXISTS (
SELECT 1 FROM c2_sessions s
JOIN rbac_resource_assignments ra ON ra.resource_id = s.listener_id
WHERE s.id = c2_tasks.session_id
AND ra.user_id = ? AND ra.resource_type = 'c2_listener'
)`)
*args = append(*args, access.UserID)
}
*conditions = append(*conditions, "("+strings.Join(clauses, " OR ")+")")
}
func buildC2TasksWhereForAccess(filter ListC2TasksFilter, access RBACListAccess) (string, []interface{}) {
where, args := buildC2TasksWhere(filter)
conditions := []string{where}
appendC2TaskAccessFilter(&conditions, &args, access)
return strings.Join(conditions, " AND "), args
}
// CountC2Tasks 与 ListC2Tasks 相同过滤条件下的记录总数
func (db *DB) CountC2Tasks(filter ListC2TasksFilter) (int64, error) {
where, args := buildC2TasksWhere(filter)
@@ -787,6 +1008,14 @@ func (db *DB) CountC2Tasks(filter ListC2TasksFilter) (int64, error) {
return n, err
}
func (db *DB) CountC2TasksForAccess(filter ListC2TasksFilter, access RBACListAccess) (int64, error) {
where, args := buildC2TasksWhereForAccess(filter, access)
query := `SELECT COUNT(*) FROM c2_tasks WHERE ` + where
var n int64
err := db.QueryRow(query, args...).Scan(&n)
return n, err
}
// CountC2TasksQueuedOrPending 统计 queued/pending 状态任务数(仪表盘「待审任务」)
func (db *DB) CountC2TasksQueuedOrPending(sessionID string) (int64, error) {
conditions := []string{"status IN ('queued', 'pending')"}
@@ -801,6 +1030,15 @@ func (db *DB) CountC2TasksQueuedOrPending(sessionID string) (int64, error) {
return n, err
}
func (db *DB) CountC2TasksQueuedOrPendingForAccess(sessionID string, access RBACListAccess) (int64, error) {
filter := ListC2TasksFilter{SessionID: sessionID}
where, args := buildC2TasksWhereForAccess(filter, access)
query := `SELECT COUNT(*) FROM c2_tasks WHERE status IN ('queued', 'pending') AND ` + where
var n int64
err := db.QueryRow(query, args...).Scan(&n)
return n, err
}
// ListC2Tasks 任务列表,按创建时间倒序
func (db *DB) ListC2Tasks(filter ListC2TasksFilter) ([]*C2Task, error) {
where, args := buildC2TasksWhere(filter)
@@ -866,6 +1104,74 @@ func (db *DB) ListC2Tasks(filter ListC2TasksFilter) ([]*C2Task, error) {
return list, rows.Err()
}
func (db *DB) ListC2TasksForAccess(filter ListC2TasksFilter, access RBACListAccess) ([]*C2Task, error) {
where, args := buildC2TasksWhereForAccess(filter, access)
query := `
SELECT id, session_id, task_type, COALESCE(payload_json, '{}'),
status, COALESCE(result_text, ''), COALESCE(result_blob_path, ''),
COALESCE(error, ''), COALESCE(source, 'manual'),
COALESCE(conversation_id, ''), COALESCE(approval_status, ''),
created_at, sent_at, started_at, completed_at, COALESCE(duration_ms, 0)
FROM c2_tasks
WHERE ` + where + `
ORDER BY created_at DESC
`
limit := filter.Limit
offset := filter.Offset
if offset < 0 {
offset = 0
}
if limit > 0 {
if limit > 1000 {
limit = 1000
}
query += ` LIMIT ? OFFSET ?`
args = append(args, limit, offset)
}
rows, err := db.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
return db.scanC2TaskRows(rows)
}
func (db *DB) scanC2TaskRows(rows *sql.Rows) ([]*C2Task, error) {
var list []*C2Task
for rows.Next() {
var t C2Task
var payloadJSON string
var sentAt, startedAt, completedAt sql.NullTime
if err := rows.Scan(
&t.ID, &t.SessionID, &t.TaskType, &payloadJSON,
&t.Status, &t.ResultText, &t.ResultBlobPath,
&t.Error, &t.Source,
&t.ConversationID, &t.ApprovalStatus,
&t.CreatedAt, &sentAt, &startedAt, &completedAt, &t.DurationMS,
); err != nil {
db.logger.Warn("扫描 c2_tasks 行失败", zap.Error(err))
continue
}
if payloadJSON != "" && payloadJSON != "{}" {
_ = json.Unmarshal([]byte(payloadJSON), &t.Payload)
}
if sentAt.Valid {
x := sentAt.Time
t.SentAt = &x
}
if startedAt.Valid {
x := startedAt.Time
t.StartedAt = &x
}
if completedAt.Valid {
x := completedAt.Time
t.CompletedAt = &x
}
list = append(list, &t)
}
return list, rows.Err()
}
// PopQueuedC2Tasks 取出某会话所有 queued/approved 任务(用于 beacon 拉取),原子置为 sent
func (db *DB) PopQueuedC2Tasks(sessionID string, limit int) ([]*C2Task, error) {
if limit <= 0 {
@@ -978,6 +1284,29 @@ func (db *DB) DeleteC2TasksByIDs(ids []string) (int64, error) {
return res.RowsAffected()
}
func (db *DB) DeleteC2TasksByIDsForAccess(ids []string, access RBACListAccess) (int64, error) {
if access.Scope == RBACScopeAll {
return db.DeleteC2TasksByIDs(ids)
}
clean := cleanC2IDs(ids)
if len(clean) == 0 {
return 0, ErrNoValidC2TaskIDs
}
placeholders := strings.Repeat("?,", len(clean)-1) + "?"
args := make([]interface{}, 0, len(clean)+2)
for _, id := range clean {
args = append(args, id)
}
conditions := []string{"id IN (" + placeholders + ")"}
appendC2TaskAccessFilter(&conditions, &args, access)
query := `DELETE FROM c2_tasks WHERE ` + strings.Join(conditions, " AND ")
res, err := db.Exec(query, args...)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
// ----------------------------------------------------------------------------
// CRUDC2 文件
// ----------------------------------------------------------------------------
@@ -1024,6 +1353,27 @@ func (db *DB) ListC2FilesBySession(sessionID string) ([]*C2File, error) {
return list, rows.Err()
}
func cleanC2IDs(ids []string) []string {
const maxBatch = 500
if len(ids) > maxBatch {
ids = ids[:maxBatch]
}
clean := make([]string, 0, len(ids))
seen := make(map[string]struct{}, len(ids))
for _, id := range ids {
id = strings.TrimSpace(id)
if !validC2TextIDForDelete(id) {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
clean = append(clean, id)
}
return clean
}
// ----------------------------------------------------------------------------
// CRUDC2 事件审计
// ----------------------------------------------------------------------------
@@ -1093,6 +1443,56 @@ func buildC2EventsWhere(filter ListC2EventsFilter) (where string, args []interfa
return strings.Join(conditions, " AND "), args
}
func appendC2EventAccessFilter(conditions *[]string, args *[]interface{}, access RBACListAccess) {
if access.Scope == RBACScopeAll {
return
}
if access.UserID == "" {
*conditions = append(*conditions, "1=0")
return
}
clauses := []string{`EXISTS (
SELECT 1 FROM c2_sessions s
JOIN c2_listeners l ON l.id = s.listener_id
WHERE s.id = c2_events.session_id AND l.owner_user_id = ?
)`}
*args = append(*args, access.UserID)
if access.Scope == RBACScopeAssigned {
clauses = append(clauses, `EXISTS (
SELECT 1 FROM c2_sessions s
JOIN rbac_resource_assignments ra ON ra.resource_id = s.listener_id
WHERE s.id = c2_events.session_id
AND ra.user_id = ? AND ra.resource_type = 'c2_listener'
)`)
*args = append(*args, access.UserID)
}
clauses = append(clauses, `EXISTS (
SELECT 1 FROM c2_tasks t
JOIN c2_sessions s ON s.id = t.session_id
JOIN c2_listeners l ON l.id = s.listener_id
WHERE t.id = c2_events.task_id AND l.owner_user_id = ?
)`)
*args = append(*args, access.UserID)
if access.Scope == RBACScopeAssigned {
clauses = append(clauses, `EXISTS (
SELECT 1 FROM c2_tasks t
JOIN c2_sessions s ON s.id = t.session_id
JOIN rbac_resource_assignments ra ON ra.resource_id = s.listener_id
WHERE t.id = c2_events.task_id
AND ra.user_id = ? AND ra.resource_type = 'c2_listener'
)`)
*args = append(*args, access.UserID)
}
*conditions = append(*conditions, "("+strings.Join(clauses, " OR ")+")")
}
func buildC2EventsWhereForAccess(filter ListC2EventsFilter, access RBACListAccess) (string, []interface{}) {
where, args := buildC2EventsWhere(filter)
conditions := []string{where}
appendC2EventAccessFilter(&conditions, &args, access)
return strings.Join(conditions, " AND "), args
}
// CountC2Events 与 ListC2Events 相同过滤条件下的记录总数
func (db *DB) CountC2Events(filter ListC2EventsFilter) (int64, error) {
where, args := buildC2EventsWhere(filter)
@@ -1102,6 +1502,14 @@ func (db *DB) CountC2Events(filter ListC2EventsFilter) (int64, error) {
return n, err
}
func (db *DB) CountC2EventsForAccess(filter ListC2EventsFilter, access RBACListAccess) (int64, error) {
where, args := buildC2EventsWhereForAccess(filter, access)
query := `SELECT COUNT(*) FROM c2_events WHERE ` + where
var n int64
err := db.QueryRow(query, args...).Scan(&n)
return n, err
}
// ListC2Events 事件查询,按创建时间倒序
func (db *DB) ListC2Events(filter ListC2EventsFilter) ([]*C2Event, error) {
where, args := buildC2EventsWhere(filter)
@@ -1143,6 +1551,50 @@ func (db *DB) ListC2Events(filter ListC2EventsFilter) ([]*C2Event, error) {
return list, rows.Err()
}
func (db *DB) ListC2EventsForAccess(filter ListC2EventsFilter, access RBACListAccess) ([]*C2Event, error) {
where, args := buildC2EventsWhereForAccess(filter, access)
limit := filter.Limit
if limit <= 0 || limit > 1000 {
limit = 200
}
offset := filter.Offset
if offset < 0 {
offset = 0
}
query := `
SELECT id, level, category, COALESCE(session_id, ''), COALESCE(task_id, ''),
message, COALESCE(data_json, ''), created_at
FROM c2_events
WHERE ` + where + `
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`
args = append(args, limit, offset)
rows, err := db.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
return scanC2EventRows(rows)
}
func scanC2EventRows(rows *sql.Rows) ([]*C2Event, error) {
var list []*C2Event
for rows.Next() {
var e C2Event
var dataJSON string
if err := rows.Scan(&e.ID, &e.Level, &e.Category, &e.SessionID, &e.TaskID,
&e.Message, &dataJSON, &e.CreatedAt); err != nil {
continue
}
if dataJSON != "" {
_ = json.Unmarshal([]byte(dataJSON), &e.Data)
}
list = append(list, &e)
}
return list, rows.Err()
}
// DeleteC2EventsByIDs 按主键批量删除事件,返回实际删除行数
func (db *DB) DeleteC2EventsByIDs(ids []string) (int64, error) {
if len(ids) == 0 {
@@ -1181,6 +1633,29 @@ func (db *DB) DeleteC2EventsByIDs(ids []string) (int64, error) {
return res.RowsAffected()
}
func (db *DB) DeleteC2EventsByIDsForAccess(ids []string, access RBACListAccess) (int64, error) {
if access.Scope == RBACScopeAll {
return db.DeleteC2EventsByIDs(ids)
}
clean := cleanC2IDs(ids)
if len(clean) == 0 {
return 0, ErrNoValidC2EventIDs
}
placeholders := strings.Repeat("?,", len(clean)-1) + "?"
args := make([]interface{}, 0, len(clean)+4)
for _, id := range clean {
args = append(args, id)
}
conditions := []string{"id IN (" + placeholders + ")"}
appendC2EventAccessFilter(&conditions, &args, access)
query := `DELETE FROM c2_events WHERE ` + strings.Join(conditions, " AND ")
res, err := db.Exec(query, args...)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
// ----------------------------------------------------------------------------
// CRUDC2 Malleable Profile
// ----------------------------------------------------------------------------