diff --git a/client/audit.go b/client/audit.go new file mode 100644 index 0000000..cd4a8f3 --- /dev/null +++ b/client/audit.go @@ -0,0 +1,54 @@ +package client + +import ( + "context" + "fmt" + + "github.com/harness/harness-mcp/client/dto" +) + +const ( + auditPath = "gateway/audit/api/audits/list" +) + +type AuditService struct { + Client *Client +} + +// ListUserAuditTrail fetches the audit trail. +func (a *AuditService) ListUserAuditTrail(ctx context.Context, scope dto.Scope, userID string, page int, size int, startTime int64, endTime int64, opts *dto.ListAuditEventsFilter) (*dto.AuditOutput[dto.AuditListItem], error) { + if opts == nil { + opts = &dto.ListAuditEventsFilter{} + } + + params := make(map[string]string) + params["accountIdentifier"] = scope.AccountID + params["pageIndex"] = fmt.Sprintf("%d", page) + params["pageSize"] = fmt.Sprintf("%d", size) + + addScope(scope, params) + + // Required fields + opts.FilterType = "Audit" + opts.Principals = []dto.AuditPrincipal{{ + Type: "USER", + Identifier: userID, + }} + + opts.Scopes = []dto.AuditResourceScope{{ + AccountIdentifier: scope.AccountID, + OrgIdentifier: scope.OrgID, + ProjectIdentifier: scope.ProjectID, + }} + + opts.StartTime = startTime // or use a date range with UnixMillis + opts.EndTime = endTime + + resp := &dto.AuditOutput[dto.AuditListItem]{} + err := a.Client.Post(ctx, auditPath, params, opts, resp) + if err != nil { + return nil, fmt.Errorf("failed to list the audit trail: %w", err) + } + + return resp, nil +} diff --git a/client/dto/audit.go b/client/dto/audit.go new file mode 100644 index 0000000..cc94502 --- /dev/null +++ b/client/dto/audit.go @@ -0,0 +1,68 @@ +package dto + +type AuditPrincipal struct { + Type string `json:"type"` + Identifier string `json:"identifier"` +} + +type AuditResourceScope struct { + AccountIdentifier string `json:"accountIdentifier"` + OrgIdentifier string `json:"orgIdentifier,omitempty"` + ProjectIdentifier string `json:"projectIdentifier,omitempty"` +} + +type ListAuditEventsFilter struct { + Scopes []AuditResourceScope `json:"scopes,omitempty"` + Principals []AuditPrincipal `json:"principals,omitempty"` + Actions []string `json:"actions,omitempty"` + FilterType string `json:"filterType,omitempty"` + StartTime int64 `json:"startTime,omitempty"` + EndTime int64 `json:"endTime,omitempty"` +} + +type AuditOutput[T any] struct { + Status string `json:"status,omitempty"` + Data AuditOutputData[T] `json:"data,omitempty"` +} + +type AuditOutputData[T any] struct { + PageItemCount int `json:"pageItemCount,omitempty"` + PageSize int `json:"pageSize,omitempty"` + Content []T `json:"content,omitempty"` + PageIndex int `json:"pageIndex,omitempty"` + HasNext bool `json:"hasNext,omitempty"` + PageToken string `json:"pageToken,omitempty"` + TotalItems int `json:totalItems, omitempty` + TotalPages int `json:totalPages, omitempty` +} + +type AuditListItem struct { + AuditID string `json:"auditId,omitempty"` + InsertId string `json:"insertId,omitempty"` + Resource AuditResource `json:"resource,omitempty"` + Action string `json:"action,omitempty"` + Module string `json:"module,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + AuthenticationInfo AuditAuthenticationInfo `json:"authenticationInfo,omitempty"` + ResourceScope AuditResourceScope `json:"resourceScope,omitempty"` +} + +type AuditResource struct { + Type string `json:"type"` + Identifier string `json:"identifier"` + Labels AuditResourceLabels `json:"labels,omitempty"` +} + +type AuditResourceLabels struct { + ResourceName string `json:"resourceName,omitempty"` +} + +type AuditAuthenticationInfo struct { + Principal AuditPrincipal `json:"principal"` + Labels AuditAuthenticationInfoLabels `json:"labels,omitempty"` +} + +type AuditAuthenticationInfoLabels struct { + UserID string `json:"userId,omitempty"` + Username string `json:"username,omitempty"` +} diff --git a/pkg/harness/audit.go b/pkg/harness/audit.go new file mode 100644 index 0000000..171acf1 --- /dev/null +++ b/pkg/harness/audit.go @@ -0,0 +1,76 @@ +package harness + +import ( + "context" + "encoding/json" + "fmt" + "math" + "time" + + "github.com/harness/harness-mcp/client" + "github.com/harness/harness-mcp/cmd/harness-mcp-server/config" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + minPage = 0 + maxPage = 1000 + minSize = 1 + maxSize = 1000 +) + +// ListAuditsOfUser creates a tool for listing the audit trail. +func ListUserAuditTrailTool(config *config.Config, auditClient *client.AuditService) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("list_user_audits", + mcp.WithDescription("List the audit trail of the user."), + mcp.WithString("user_id", + mcp.Required(), + mcp.Description("The user id used to retrieve the audit trail."), + ), + mcp.WithNumber("start_time", + mcp.Description("Optional start time in milliseconds"), + mcp.DefaultNumber(0), + ), + mcp.WithNumber("end_time", + mcp.Description("Optional end time in milliseconds"), + mcp.DefaultNumber(float64(time.Now().UnixMilli())), + ), + WithScope(config, true), + WithPagination(), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + userID, err := requiredParam[string](request, "user_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + scope, err := fetchScope(config, request, true) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + page, size, err := fetchPagination(request) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + page = int(math.Min(math.Max(float64(page), float64(minPage)), float64(maxPage))) + size = int(math.Min(math.Max(float64(size), float64(minSize)), float64(maxSize))) + + startTime, _ := OptionalParam[int64](request, "start_time") + endTime, _ := OptionalParam[int64](request, "end_time") + + data, err := auditClient.ListUserAuditTrail(ctx, scope, userID, page, size, startTime, endTime, nil) + if err != nil { + return nil, fmt.Errorf("failed to list the audit logs: %w", err) + } + + r, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("failed to marshal the audit logs: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} diff --git a/pkg/harness/tools.go b/pkg/harness/tools.go index 3e8c216..ad8cbff 100644 --- a/pkg/harness/tools.go +++ b/pkg/harness/tools.go @@ -91,6 +91,10 @@ func InitToolsets(config *config.Config) (*toolsets.ToolsetGroup, error) { return nil, err } + if err := registerAudit(config, tsg); err != nil { + return nil, err + } + // Enable requested toolsets if err := tsg.EnableToolsets(config.Toolsets); err != nil { return nil, err @@ -580,3 +584,17 @@ func registerChaos(config *config.Config, tsg *toolsets.ToolsetGroup) error { tsg.AddToolset(chaos) return nil } + +func registerAudit(config *config.Config, tsg *toolsets.ToolsetGroup) error { + c, err := createClient(config.BaseURL, config, "") + if err != nil { + return err + } + auditService := &client.AuditService{Client: c} + audit := toolsets.NewToolset("audit", "Audit log related tools"). + AddReadTools( + toolsets.NewServerTool(ListUserAuditTrailTool(config, auditService)), + ) + tsg.AddToolset(audit) + return nil +} diff --git a/prompts.txt b/prompts.txt new file mode 100644 index 0000000..9106eee --- /dev/null +++ b/prompts.txt @@ -0,0 +1,57 @@ + + +### 1. Audit Search Logic + +- When the user requests a specific audit or action **without specifying a page number or size**: + - Use the default page size. + - If no matching audit is found, increment the page size by 10 and continue searching. + - Before continuing with further searches, ask the user for permission to keep checking. + +--- + +### 2. Output Formatting + +- For all audit outputs: + - Provide results in **both JSON format and tabular format**. + - Ensure every entry includes timestamps, unless the user specifically requests to exclude certain entries. + +--- + +### 3. Date to Unix Milliseconds Conversion + +- When fetching audits with a filter for start time or end time, or when converting any date to a Unix milliseconds timestamp: + 1. **Parse the prompt** to extract: + - Year, month, day, and hour. + - If minutes, seconds, or nanoseconds are not provided, default them to 0. + 2. **Use the following Go code template** to perform the conversion: + ```go + loc, _ := time.LoadLocation({location}) + t := time.Date({year}, time.{month}, {day}, {hour}, {minute}, {second}, {nanosecond}, loc) + ``` + - Replace `{location}` with the value provided by the user ("Asia/Kolkata" or "UTC"). + - Replace other placeholders with the extracted date/time values. + 3. **Important:** + - Do **not** adjust the time for any location; use the time as given for the specified location. + - Only use the location for the `{location}` parameter in the code. + +- **Location Selection:** + - Ask the user to specify the location. + - Available options: `"Asia/Kolkata"`, `"UTC"`. + +--- + +### 4. Go time.Date Function Reference + +- The format for the Go `time.Date` function is: + ```go + time.Date( + year int, + month time.Month, + day int, + hour int, + minute int, + second int, + nanosecond int, + location *time.Location, + ) + ```