tools/call dispatched with no record, so repo_delete, pr_merge and file_delete left no trail in gitea-mcp; only auth acceptance was logged. One slog record per call: tool, caller, token fingerprint, target (owner/repo/number/branch/path/tag), outcome, duration. Every other argument is reduced to args_bytes so the audit line never copies file content or issue bodies. Closes #64 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
47 lines
1.4 KiB
Go
47 lines
1.4 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"time"
|
|
|
|
chassisauth "git.d-ma.be/mathias/mcp-chassis/auth"
|
|
|
|
"git.d-ma.be/mathias/gitea-mcp/internal/auth"
|
|
"git.d-ma.be/mathias/gitea-mcp/internal/gitea"
|
|
)
|
|
|
|
// auditTargetKeys are the only argument values copied into the audit record:
|
|
// they identify what a call acted on without copying what it wrote. Every
|
|
// other argument (file content, issue bodies) is reduced to args_bytes so the
|
|
// audit trail never becomes a second copy of repo content (gitea-mcp#64).
|
|
var auditTargetKeys = []string{"owner", "repo", "number", "branch", "path", "tag"}
|
|
|
|
// logToolCall emits one structured record per tools/call (OWASP MCP08).
|
|
func logToolCall(ctx context.Context, name string, args json.RawMessage, start time.Time, err error) {
|
|
attrs := []any{
|
|
"tool", name,
|
|
"caller", auth.Caller(ctx),
|
|
"args_bytes", len(args),
|
|
"duration_ms", time.Since(start).Milliseconds(),
|
|
}
|
|
if tok, ok := gitea.TokenFromContext(ctx); ok {
|
|
attrs = append(attrs, "token_fp", chassisauth.Fingerprint(tok))
|
|
}
|
|
var parsed map[string]any
|
|
if json.Unmarshal(args, &parsed) == nil {
|
|
for _, k := range auditTargetKeys {
|
|
if v, ok := parsed[k]; ok {
|
|
attrs = append(attrs, k, v)
|
|
}
|
|
}
|
|
}
|
|
outcome := "ok"
|
|
if err != nil {
|
|
outcome = err.Error()
|
|
}
|
|
attrs = append(attrs, "outcome", outcome)
|
|
slog.InfoContext(ctx, "mcp tool call", attrs...)
|
|
}
|