节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
3.71 KiB
Stardust
using System.Text.RegularExpressions;
using NewLife;
using NewLife.Log;
using Stardust.Data.Platform;

namespace Stardust.Mcp;

/// <summary>审计写入器。读操作 best-effort(引擎内联),写操作 fail-closed(预写+回写)</summary>
public static class McpAuditWriter
{
    /// <summary>写操作预写审计(fail-closed):持久化失败即抛出,引擎据此拒绝执行。
    /// BeforeValue 取调用前快照(脱敏入参),供与 AfterValue 对照审计变更。</summary>
    public static McpAudit Begin(McpContext ctx, IMcpTool tool, String arguments)
    {
        var audit = new McpAudit
        {
            TokenId = ctx.TokenId,
            TokenName = ctx.TokenName,
            ToolName = tool.Name,
            CallerIp = ctx.CallerIp,
            CallerUserAgent = ctx.UserAgent,
            Arguments = SanitizeArguments(arguments),
            Success = false,
            ErrorMessage = "(执行中)",
            Duration = 0,
            TraceId = ctx.TraceId,
            CreateTime = DateTime.Now,
            RiskLevel = (Int32)tool.Risk,
            ProjectId = ctx.ProjectId,
            ScopesUsed = String.Join(',', ctx.Scopes),
            BeforeValue = SanitizeArguments(arguments),
        };
        audit.Insert();
        return audit;
    }

    /// <summary>回写执行结果。失败仅告警:操作已执行且已有审计行,不再中断调用。
    /// afterValue 为工具执行后的结果快照(脱敏),与 BeforeValue 对照;无结果传 null。</summary>
    public static void Finish(McpAudit audit, Boolean success, String error, Int32 duration, String? afterValue = null)
    {
        try
        {
            audit.Success = success;
            audit.ErrorMessage = error?.Cut(500);
            audit.Duration = duration;
            if (!afterValue.IsNullOrEmpty()) audit.AfterValue = SanitizeArguments(afterValue);
            audit.Update();
        }
        catch (Exception ex)
        {
            XTrace.Log.Warn("[McpAuditWriter] 回写审计结果失败:{0}", ex.Message);
        }
    }

    /// <summary>敏感 key 关键词正则片段。写路径与读路径词表保持一致,兼容 apiKey/api_key、accessKey/access_key、connectionString 等变体</summary>
    private const String SensitiveKeyPattern = "(?:password|secret|token|authorization|connectionstring|conn|apikey|access_?key)";

    /// <summary>
    /// 写路径脱敏:在落盘前把 JSON 字符串值里敏感 key 的值替换为 ***。处理两种形状:
    /// 1. 常规形状 "password":"明文" —— key 名含敏感词即打码其值;
    /// 2. config_set 形状 {"key":"Db_Password","value":"明文"} —— key 的值是敏感名即打码 value 的值。
    /// 与读路径(Stardust.Data/Platform/MCP审计日志.Biz.cs)行为对齐,但本地复制一份以避免跨层依赖。
    /// </summary>
    private static String? SanitizeArguments(String? json)
    {
        if (String.IsNullOrEmpty(json)) return json;

        // 与读路径行为一致:先截断 2000 字符
        if (json.Length > 2000) json = json.Substring(0, 2000) + "...";

        // 大小写不敏感;key 包含任一敏感子串即命中;只处理字符串值("key":"value"),不动数字/对象值
        json = Regex.Replace(json,
            "(?i)(\"[^\"]*" + SensitiveKeyPattern + "[^\"]*\"\\s*:\\s*\")[^\"]*(\")",
            "$1***$2");

        // config_set 等工具的参数形状:{"key":"敏感名","value":"明文"} —— key 的值是敏感名时打码 value 的值
        json = Regex.Replace(json,
            "(?i)(\"key\"\\s*:\\s*\"[^\"]*" + SensitiveKeyPattern + "[^\"]*\"\\s*,\\s*\"value\"\\s*:\\s*\")[^\"]*(\")",
            "$1***$2");

        return json;
    }
}