@inso_web/els-mcp
Version:
MCP-сервер поверх INSO Error Logs Service. Read-only tools (search, analytics, fingerprinting, correlations) для подключения Claude Desktop/Code и ChatGPT к логам ошибок. Streamable HTTP transport + stdio для npx-запуска.
131 lines • 4.73 kB
JavaScript
/**
* Audit chain verification.
*
* Проходит по строкам `mcp_audit.audit_log` в порядке `createdAt ASC` для
* заданного `appId` (и опционального диапазона дат) и проверяет, что
* `rowHash = sha256(prevRowHash + content)` совпадает со stored значением.
*
* Используется:
* - в CLI команде `els-mcp verify-audit --app=X --from=Y --to=Z`
* (см. `src/cli.ts`)
* - в smoke / health-check скриптах
*
* Поведение при отсутствии Prisma client — корректно возвращает пустой
* результат с `prismaAvailable=false`.
*/
import { getPrisma } from './prisma.js';
import { rowContent, sha256Hex } from './service.js';
function toIso(d) {
if (d === undefined)
return undefined;
return typeof d === 'string' ? d : d.toISOString();
}
function pickIso(v) {
if (v instanceof Date)
return v.toISOString();
if (typeof v === 'string')
return v;
return String(v);
}
function pickRowId(v) {
return v === undefined || v === null ? '<unknown>' : String(v);
}
/**
* Verifies the audit hash-chain integrity for `appId`.
* Не модифицирует БД, читает в batch'ах для крупных таблиц.
*/
export async function verifyChain(opts) {
const batchSize = Math.max(1, opts.batchSize ?? 1000);
const fromIso = toIso(opts.from);
const toIsoStr = toIso(opts.to);
const client = opts.prismaOverride !== undefined
? opts.prismaOverride
: await getPrisma({
...(opts.databaseUrl ? { databaseUrl: opts.databaseUrl } : {}),
...(opts.log ? { log: opts.log } : {}),
});
if (!client) {
return { totalChecked: 0, prismaAvailable: false, breakAt: null };
}
const where = { appId: opts.appId };
if (fromIso || toIsoStr) {
const createdAt = {};
if (fromIso)
createdAt.gte = fromIso;
if (toIsoStr)
createdAt.lt = toIsoStr;
where.createdAt = createdAt;
}
let prevHash = null;
let totalChecked = 0;
let skip = 0;
for (;;) {
const rows = (await client.mcpAuditLog.findMany({
where,
orderBy: { createdAt: 'asc' },
take: batchSize,
skip,
}));
if (rows.length === 0)
break;
for (const row of rows) {
const args = (typeof row.args === 'object' && row.args !== null
? row.args
: {});
const createdAtIso = pickIso(row.createdAt);
const content = rowContent({
appId: row.appId,
keyId: row.keyId,
tool: row.tool,
args,
resultBytes: row.resultBytes,
latencyMs: row.latencyMs,
cacheHit: row.cacheHit === true,
statusCode: row.statusCode,
error: row.error ?? null,
createdAt: createdAtIso,
prevHash,
});
const expectedHash = sha256Hex((prevHash ?? '') + content);
if (expectedHash !== row.rowHash) {
return {
totalChecked: totalChecked + 1,
prismaAvailable: true,
breakAt: {
id: pickRowId(row.id),
createdAt: createdAtIso,
expectedHash,
actualHash: row.rowHash,
},
};
}
prevHash = row.rowHash;
totalChecked += 1;
}
if (rows.length < batchSize)
break;
skip += batchSize;
}
return { totalChecked, prismaAvailable: true, breakAt: null };
}
/**
* Pretty-printer для CLI: возвращает многострочный отчёт.
*/
export function formatVerifyResult(result) {
if (!result.prismaAvailable) {
return [
'Audit verification skipped: Prisma client not available.',
'Ensure MCP_DATABASE_URL is set and `npm run prisma:generate` has been run.',
].join('\n');
}
if (result.breakAt) {
return [
`Audit chain BROKEN after ${result.totalChecked} rows.`,
` break at id=${result.breakAt.id}, createdAt=${result.breakAt.createdAt}`,
` expected rowHash=${result.breakAt.expectedHash}`,
` stored rowHash=${result.breakAt.actualHash}`,
].join('\n');
}
return `Audit chain OK: ${result.totalChecked} rows verified.`;
}
//# sourceMappingURL=verify.js.map