mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-19 09:27:21 +02:00
Add files via upload
This commit is contained in:
+507
-32
@@ -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()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// CRUD:C2 任务
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// CRUD:C2 文件
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// CRUD:C2 事件审计
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// CRUD:C2 Malleable Profile
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user