Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a033d7c
feat: add terminal session audit
huanghongbo-hhb May 28, 2026
b599908
refactor: remove terminal audit risk level placeholder
huanghongbo-hhb May 29, 2026
63dbc99
feat: restore terminal audit risk level
huanghongbo-hhb May 29, 2026
d391033
chore: add terminal audit close path debug logs
huanghongbo-hhb May 29, 2026
75374b2
fix: close terminal session on websocket shutdown
huanghongbo-hhb May 29, 2026
cc97739
fix: cancel pod exec stream on terminal close
huanghongbo-hhb May 29, 2026
b2b6ef7
refactor: make terminal session close idempotent
huanghongbo-hhb May 29, 2026
d98d67a
feat: improve bracketed paste command extraction
huanghongbo-hhb Jun 1, 2026
32af960
feat: record podexec service name in terminal audit
huanghongbo-hhb Jun 1, 2026
c2c3721
feat: refine terminal audit interactive command extraction
huanghongbo-hhb Jun 1, 2026
f6afd38
fix: keep podexec terminal websocket alive
huanghongbo-hhb Jun 1, 2026
5f568b7
Revert "fix: keep podexec terminal websocket alive"
huanghongbo-hhb Jun 1, 2026
73cc092
feat: expand interactive command whitelist
huanghongbo-hhb Jun 1, 2026
31083d2
fix: abort terminal sessions on aslan shutdown
huanghongbo-hhb Jun 2, 2026
eb4c8ec
feat: support live terminal session monitoring
huanghongbo-hhb Jul 24, 2026
72ab0ef
fix: harden terminal audit recording
huanghongbo-hhb Jul 24, 2026
e6bd48e
fix: harden terminal audit lifecycle
huanghongbo-hhb Jul 24, 2026
01b74f8
fix: finish terminal audit hardening
huanghongbo-hhb Jul 27, 2026
823cfcc
fix: retry live terminal state initialization
huanghongbo-hhb Aug 13, 2026
6ecba8c
docs: explain terminal command hashed index
huanghongbo-hhb Aug 13, 2026
a20c374
refactor: clarify terminal audit code paths
huanghongbo-hhb Aug 13, 2026
8f5a76f
refactor: simplify active session registry
huanghongbo-hhb Aug 13, 2026
625352b
fix: stream live terminal output to spectators
huanghongbo-hhb Aug 13, 2026
da32aff
feat: build terminal audit evidence for ai analysis
huanghongbo-hhb Aug 17, 2026
73f167d
feat: add terminal session ai audit
huanghongbo-hhb Aug 17, 2026
1ad469b
fix: harden terminal session AI audit
huanghongbo-hhb Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion pkg/cli/initconfig/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ func createOrUpdateMongodbIndex(ctx context.Context) {
commonrepo.NewEnvInfoColl(),
commonrepo.NewApprovalTicketColl(),
commonrepo.NewWorkflowTaskRevertColl(),
commonrepo.NewTerminalSessionColl(),
commonrepo.NewTerminalCommandColl(),
commonrepo.NewTerminalAuditAIResultColl(),

// msg queue
commonrepo.NewMsgQueueCommonColl(),
Expand Down Expand Up @@ -308,7 +311,7 @@ func createBuiltinApplicationFieldDefinitions() error {
{Key: "update_time", Name: "更新时间", Type: aslanconfig.ApplicationCustomFieldTypeDatetime, ShowInList: true, Source: aslanconfig.ApplicationFieldSourceBuiltin, Description: "业务服务的更新时间"},
}

// Upsert per key to be idempotent. Keep user-changed attributes for custom fields; for built-ins we only enforce Source="builtin" and Type.
// Upsert per key to be idempotent. Keep user-changed attributes for custom fields; for built-ins we only enforce Source="builtin", Type, Name, Description, and Required.
for i := range builtin {
b := builtin[i]
existing, err := coll.GetByKey(ctx, b.Key)
Expand Down
159 changes: 159 additions & 0 deletions pkg/microservice/aslan/core/common/repository/models/terminal_audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package models

import "go.mongodb.org/mongo-driver/bson/primitive"

type TerminalSessionType string

const (
TerminalSessionTypeSSH TerminalSessionType = "ssh"
TerminalSessionTypePodExec TerminalSessionType = "podexec"
TerminalSessionTypeWorkflowDebug TerminalSessionType = "workflow_debug"
)

type TerminalSessionStatus string

const (
TerminalSessionStatusRunning TerminalSessionStatus = "running"
TerminalSessionStatusFinished TerminalSessionStatus = "finished"
TerminalSessionStatusAborted TerminalSessionStatus = "aborted"
TerminalSessionStatusFailed TerminalSessionStatus = "failed"
)

type TerminalSession struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
SessionID string `bson:"session_id" json:"session_id"`
SessionType TerminalSessionType `bson:"session_type" json:"session_type"`
Status TerminalSessionStatus `bson:"status" json:"status"`
UserID string `bson:"user_id" json:"user_id"`
Username string `bson:"username" json:"username"`
Account string `bson:"account" json:"account"`
ProjectName string `bson:"project_name" json:"project_name"`
EnvName string `bson:"env_name" json:"env_name"`
ServiceName string `bson:"service_name" json:"service_name"`
WorkflowName string `bson:"workflow_name" json:"workflow_name"`
JobName string `bson:"job_name" json:"job_name"`
TaskID int64 `bson:"task_id" json:"task_id"`
TargetName string `bson:"target_name" json:"target_name"`
Protocol string `bson:"protocol" json:"protocol"`
RemoteAddr string `bson:"remote_addr" json:"remote_addr"`
LoginAccount string `bson:"login_account" json:"login_account"`
HostID string `bson:"host_id" json:"host_id"`
HostName string `bson:"host_name" json:"host_name"`
HostIP string `bson:"host_ip" json:"host_ip"`
ClusterID string `bson:"cluster_id" json:"cluster_id"`
Namespace string `bson:"namespace" json:"namespace"`
PodName string `bson:"pod_name" json:"pod_name"`
ContainerName string `bson:"container_name" json:"container_name"`
ClientIP string `bson:"client_ip" json:"client_ip"`
UserAgent string `bson:"user_agent" json:"user_agent"`
StartedAt int64 `bson:"started_at" json:"started_at"`
EndedAt int64 `bson:"ended_at" json:"ended_at"`
DurationSeconds int64 `bson:"duration_seconds" json:"duration_seconds"`
LastActivityAt int64 `bson:"last_activity_at" json:"last_activity_at"`
CommandCount int64 `bson:"command_count" json:"command_count"`
StorageID string `bson:"storage_id" json:"storage_id"`
Bucket string `bson:"bucket" json:"bucket"`
ObjectKey string `bson:"object_key" json:"object_key"`
FileSize int64 `bson:"file_size" json:"file_size"`
ErrorMessage string `bson:"error_message" json:"error_message"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
}

func (TerminalSession) TableName() string {
return "terminal_session"
}

type TerminalCommand struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
SessionID string `bson:"session_id" json:"session_id"`
Seq int64 `bson:"seq" json:"seq"`
Command string `bson:"command" json:"command"`
UserID string `bson:"user_id" json:"user_id"`
Username string `bson:"username" json:"username"`
Account string `bson:"account" json:"account"`
ProjectName string `bson:"project_name" json:"project_name"`
EnvName string `bson:"env_name" json:"env_name"`
TargetName string `bson:"target_name" json:"target_name"`
Protocol string `bson:"protocol" json:"protocol"`
RemoteAddr string `bson:"remote_addr" json:"remote_addr"`
LoginAccount string `bson:"login_account" json:"login_account"`
TimeOffsetMS int64 `bson:"time_offset_ms" json:"time_offset_ms"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
}

func (TerminalCommand) TableName() string {
return "terminal_command"
}

type TerminalSessionListArgs struct {
Status string `form:"status" json:"status"`
SessionType string `form:"sessionType" json:"sessionType"`
ProjectName string `form:"projectName" json:"projectName"`
EnvName string `form:"envName" json:"envName"`
ServiceName string `form:"serviceName" json:"serviceName"`
Username string `form:"username" json:"username"`
TargetName string `form:"targetName" json:"targetName"`
RemoteAddr string `form:"remoteAddr" json:"remoteAddr"`
StartTime int64 `form:"startTime" json:"startTime"`
EndTime int64 `form:"endTime" json:"endTime"`
PageNum int64 `form:"pageNum" json:"pageNum"`
PageSize int64 `form:"pageSize" json:"pageSize"`
}

type TerminalCommandListArgs struct {
SessionID string `form:"sessionID" json:"sessionID"`
ProjectName string `form:"projectName" json:"projectName"`
Username string `form:"username" json:"username"`
TargetName string `form:"targetName" json:"targetName"`
RemoteAddr string `form:"remoteAddr" json:"remoteAddr"`
Command string `form:"command" json:"command"`
StartTime int64 `form:"startTime" json:"startTime"`
EndTime int64 `form:"endTime" json:"endTime"`
PageNum int64 `form:"pageNum" json:"pageNum"`
PageSize int64 `form:"pageSize" json:"pageSize"`
// SortAsc is an internal repository option; the command list API remains descending by default.
SortAsc bool `form:"-" json:"-"`
}

type TerminalAuditAIStatus string

const (
TerminalAuditAIStatusRunning TerminalAuditAIStatus = "running"
TerminalAuditAIStatusSucceeded TerminalAuditAIStatus = "succeeded"
TerminalAuditAIStatusFailed TerminalAuditAIStatus = "failed"
)

type TerminalAuditAIFinding struct {
Seq int64 `bson:"seq" json:"seq"`
Command string `bson:"command" json:"command"`
Risk string `bson:"risk" json:"risk"`
Reason string `bson:"reason" json:"reason"`
Suggestion string `bson:"suggestion" json:"suggestion"`
}

type TerminalAuditAIResult struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
SessionID string `bson:"session_id" json:"session_id"`
Status TerminalAuditAIStatus `bson:"status" json:"status"`
RiskLevel string `bson:"risk_level" json:"risk_level"`
Summary string `bson:"summary" json:"summary"`
Findings []TerminalAuditAIFinding `bson:"findings" json:"findings"`
Coverage string `bson:"coverage" json:"coverage"`
Model string `bson:"model" json:"model"`
PromptVersion int `bson:"prompt_version" json:"prompt_version"`
TokenNum int `bson:"token_num" json:"token_num"`
AnalyzedCommandCount int64 `bson:"analyzed_command_count" json:"analyzed_command_count"`
TotalCommandCount int64 `bson:"total_command_count" json:"total_command_count"`
ErrorMessage string `bson:"error_message" json:"error_message,omitempty"`
RunID string `bson:"run_id" json:"-"`
LeaseExpiresAt int64 `bson:"lease_expires_at" json:"-"`
StartedAt int64 `bson:"started_at" json:"started_at"`
FinishedAt int64 `bson:"finished_at" json:"finished_at"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
}

func (TerminalAuditAIResult) TableName() string {
return "terminal_audit_ai_result"
}
6 changes: 5 additions & 1 deletion pkg/microservice/aslan/core/common/repository/mongodb/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,13 @@ func (c *S3StorageColl) GetCollectionName() string {
}

func (c *S3StorageColl) FindDefault() (*models.S3Storage, error) {
return c.FindDefaultWithContext(context.TODO())
}

func (c *S3StorageColl) FindDefaultWithContext(ctx context.Context) (*models.S3Storage, error) {
query := bson.M{"is_default": true}
storage := new(models.S3Storage)
err := c.FindOne(context.TODO(), query).Decode(storage)
err := c.FindOne(ctx, query).Decode(storage)
if err != nil {
return nil, err
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package mongodb

import (
"context"
"errors"
"fmt"
"time"

"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"

"github.com/koderover/zadig/v2/pkg/microservice/aslan/config"
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models"
mongotool "github.com/koderover/zadig/v2/pkg/tool/mongo"
)

type TerminalAuditAIResultColl struct {
*mongo.Collection

coll string
}

var ErrTerminalAuditAIAlreadyRunning = errors.New("terminal audit ai analysis is already running")

func NewTerminalAuditAIResultColl() *TerminalAuditAIResultColl {
name := models.TerminalAuditAIResult{}.TableName()
return &TerminalAuditAIResultColl{
Collection: mongotool.Database(config.MongoDatabase()).Collection(name),
coll: name,
}
}

func (c *TerminalAuditAIResultColl) GetCollectionName() string { return c.coll }

func (c *TerminalAuditAIResultColl) EnsureIndex(ctx context.Context) error {
indexes := []mongo.IndexModel{
{
Keys: bson.D{{Key: "session_id", Value: 1}},
Options: options.Index().SetUnique(true),
},
{
Keys: bson.D{{Key: "created_at", Value: -1}, {Key: "_id", Value: -1}},
Options: options.Index().SetUnique(false),
},
}

_, err := c.Indexes().CreateMany(ctx, indexes, mongotool.CreateIndexOptions(ctx))
return err
}

func (c *TerminalAuditAIResultColl) TryStart(sessionID, runID string, startedAt, leaseExpiresAt int64) (*models.TerminalAuditAIResult, error) {
if sessionID == "" || runID == "" {
return nil, errors.New("terminal audit ai session id and run id are required")
}
filter := bson.M{
"session_id": sessionID,
"$or": bson.A{
bson.M{"status": bson.M{"$ne": models.TerminalAuditAIStatusRunning}},
bson.M{"lease_expires_at": bson.M{"$lte": startedAt}},
bson.M{"lease_expires_at": bson.M{"$exists": false}},
},
}
update := bson.M{
"$set": bson.M{
"status": models.TerminalAuditAIStatusRunning,
"risk_level": "",
"summary": "",
"findings": []models.TerminalAuditAIFinding{},
"coverage": "",
"model": "",
"prompt_version": 0,
"token_num": 0,
"analyzed_command_count": 0,
"total_command_count": 0,
"error_message": "",
"run_id": runID,
"lease_expires_at": leaseExpiresAt,
"started_at": startedAt,
"finished_at": 0,
"updated_at": startedAt,
},
"$setOnInsert": bson.M{
"session_id": sessionID,
"created_at": startedAt,
},
"$unset": bson.M{
"prompt": "",
"answer": "",
},
}
opts := options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After)
ctx, cancel := context.WithTimeout(context.Background(), terminalAuditMongoTimeout)
defer cancel()
result := new(models.TerminalAuditAIResult)
// A running session with a valid lease does not match the filter, so the upsert
// attempts an insert and hits the unique session_id index established above.
err := c.FindOneAndUpdate(ctx, filter, update, opts).Decode(result)
if mongo.IsDuplicateKeyError(err) {
return nil, ErrTerminalAuditAIAlreadyRunning
}
if err != nil {
return nil, err
}
return result, nil
}

func (c *TerminalAuditAIResultColl) Finish(result *models.TerminalAuditAIResult) error {
if result == nil || result.SessionID == "" || result.RunID == "" {
return errors.New("terminal audit ai result, session id and run id are required")
}
now := time.Now().Unix()
result.UpdatedAt = now
if result.FinishedAt == 0 {
result.FinishedAt = now
}
update := bson.M{"$set": bson.M{
"status": result.Status,
"risk_level": result.RiskLevel,
"summary": result.Summary,
"findings": result.Findings,
"coverage": result.Coverage,
"model": result.Model,
"prompt_version": result.PromptVersion,
"token_num": result.TokenNum,
"analyzed_command_count": result.AnalyzedCommandCount,
"total_command_count": result.TotalCommandCount,
"error_message": result.ErrorMessage,
"lease_expires_at": 0,
"finished_at": result.FinishedAt,
"updated_at": result.UpdatedAt,
}}
ctx, cancel := context.WithTimeout(context.Background(), terminalAuditMongoTimeout)
defer cancel()
writeResult, err := c.UpdateOne(ctx, bson.M{"session_id": result.SessionID, "run_id": result.RunID}, update)
if err != nil {
return err
}
if writeResult.MatchedCount == 0 {
return fmt.Errorf("terminal audit ai run %s no longer owns session %s", result.RunID, result.SessionID)
}
return nil
}

func (c *TerminalAuditAIResultColl) FindBySessionID(sessionID string) (*models.TerminalAuditAIResult, error) {
resp := new(models.TerminalAuditAIResult)
ctx, cancel := context.WithTimeout(context.Background(), terminalAuditMongoTimeout)
defer cancel()
err := c.FindOne(ctx, bson.M{"session_id": sessionID}).Decode(resp)
if err != nil {
return nil, err
}
return resp, nil
}
Loading
Loading