chron-mcp 0.1.13 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +305 -0
- package/dashboards/logscale/README.md +121 -0
- package/dashboards/logscale/ai-tool-usage.lql +5 -0
- package/dashboards/logscale/api-key-alert.lql +5 -0
- package/dashboards/logscale/chron-ai-activity.yaml +66 -0
- package/dashboards/logscale/daily-active-developers.lql +3 -0
- package/dashboards/logscale/secrets-by-type.lql +3 -0
- package/dashboards/logscale/sessions-by-developer.lql +6 -0
- package/dashboards/sentinel/README.md +123 -0
- package/dashboards/sentinel/ai-tool-usage.kql +7 -0
- package/dashboards/sentinel/daily-active-developers.kql +6 -0
- package/dashboards/sentinel/events-by-type.kql +5 -0
- package/dashboards/sentinel/secrets-alert.kql +7 -0
- package/dashboards/sentinel/secrets-detected.kql +7 -0
- package/dashboards/sentinel/sessions-by-developer.kql +6 -0
- package/dashboards/splunk/README.md +138 -0
- package/dashboards/splunk/ai-tool-usage.spl +3 -0
- package/dashboards/splunk/daily-active-developers.spl +2 -0
- package/dashboards/splunk/events-by-type.spl +2 -0
- package/dashboards/splunk/secrets-alert.spl +3 -0
- package/dashboards/splunk/secrets-detected.spl +4 -0
- package/dashboards/splunk/sessions-by-developer.spl +3 -0
- package/dist/cli/index.js +560 -13
- package/dist/index.js +311 -73
- package/package.json +2 -1
- package/skills/chron/SKILL.md +8 -1
- package/skills/chron.skill.md +7 -0
package/dist/cli/index.js
CHANGED
|
@@ -1184,7 +1184,7 @@ var init_sql = __esm({
|
|
|
1184
1184
|
return new SQL([new StringChunk(str)]);
|
|
1185
1185
|
}
|
|
1186
1186
|
sql2.raw = raw;
|
|
1187
|
-
function
|
|
1187
|
+
function join4(chunks, separator) {
|
|
1188
1188
|
const result = [];
|
|
1189
1189
|
for (const [i, chunk] of chunks.entries()) {
|
|
1190
1190
|
if (i > 0 && separator !== void 0) {
|
|
@@ -1194,7 +1194,7 @@ var init_sql = __esm({
|
|
|
1194
1194
|
}
|
|
1195
1195
|
return new SQL(result);
|
|
1196
1196
|
}
|
|
1197
|
-
sql2.join =
|
|
1197
|
+
sql2.join = join4;
|
|
1198
1198
|
function identifier(value) {
|
|
1199
1199
|
return new Name(value);
|
|
1200
1200
|
}
|
|
@@ -2806,7 +2806,7 @@ var require_filesystem = __commonJS({
|
|
|
2806
2806
|
"use strict";
|
|
2807
2807
|
var fs = require("fs");
|
|
2808
2808
|
var LDD_PATH = "/usr/bin/ldd";
|
|
2809
|
-
var
|
|
2809
|
+
var readFileSync2 = (path) => fs.readFileSync(path, "utf-8");
|
|
2810
2810
|
var readFile = (path) => new Promise((resolve, reject) => {
|
|
2811
2811
|
fs.readFile(path, "utf-8", (err, data) => {
|
|
2812
2812
|
if (err) {
|
|
@@ -2818,7 +2818,7 @@ var require_filesystem = __commonJS({
|
|
|
2818
2818
|
});
|
|
2819
2819
|
module2.exports = {
|
|
2820
2820
|
LDD_PATH,
|
|
2821
|
-
readFileSync,
|
|
2821
|
+
readFileSync: readFileSync2,
|
|
2822
2822
|
readFile
|
|
2823
2823
|
};
|
|
2824
2824
|
}
|
|
@@ -2830,7 +2830,7 @@ var require_detect_libc = __commonJS({
|
|
|
2830
2830
|
"use strict";
|
|
2831
2831
|
var childProcess = require("child_process");
|
|
2832
2832
|
var { isLinux, getReport } = require_process();
|
|
2833
|
-
var { LDD_PATH, readFile, readFileSync } = require_filesystem();
|
|
2833
|
+
var { LDD_PATH, readFile, readFileSync: readFileSync2 } = require_filesystem();
|
|
2834
2834
|
var cachedFamilyFilesystem;
|
|
2835
2835
|
var cachedVersionFilesystem;
|
|
2836
2836
|
var command2 = "getconf GNU_LIBC_VERSION 2>&1 || true; ldd --version 2>&1 || true";
|
|
@@ -2911,7 +2911,7 @@ var require_detect_libc = __commonJS({
|
|
|
2911
2911
|
}
|
|
2912
2912
|
cachedFamilyFilesystem = null;
|
|
2913
2913
|
try {
|
|
2914
|
-
const lddContent =
|
|
2914
|
+
const lddContent = readFileSync2(LDD_PATH);
|
|
2915
2915
|
cachedFamilyFilesystem = getFamilyFromLddContent(lddContent);
|
|
2916
2916
|
} catch (e) {
|
|
2917
2917
|
}
|
|
@@ -2968,7 +2968,7 @@ var require_detect_libc = __commonJS({
|
|
|
2968
2968
|
}
|
|
2969
2969
|
cachedVersionFilesystem = null;
|
|
2970
2970
|
try {
|
|
2971
|
-
const lddContent =
|
|
2971
|
+
const lddContent = readFileSync2(LDD_PATH);
|
|
2972
2972
|
const versionMatch = lddContent.match(RE_GLIBC_VERSION);
|
|
2973
2973
|
if (versionMatch) {
|
|
2974
2974
|
cachedVersionFilesystem = versionMatch[1];
|
|
@@ -11068,8 +11068,8 @@ var init_stream3 = __esm({
|
|
|
11068
11068
|
let promise;
|
|
11069
11069
|
try {
|
|
11070
11070
|
const request = createRequest();
|
|
11071
|
-
const
|
|
11072
|
-
promise =
|
|
11071
|
+
const fetch2 = this.#fetch;
|
|
11072
|
+
promise = fetch2(request);
|
|
11073
11073
|
} catch (error) {
|
|
11074
11074
|
promise = Promise.reject(error);
|
|
11075
11075
|
}
|
|
@@ -11136,11 +11136,11 @@ var init_stream3 = __esm({
|
|
|
11136
11136
|
|
|
11137
11137
|
// node_modules/@libsql/hrana-client/lib-esm/http/client.js
|
|
11138
11138
|
async function findEndpoint(customFetch, clientUrl) {
|
|
11139
|
-
const
|
|
11139
|
+
const fetch2 = customFetch;
|
|
11140
11140
|
for (const endpoint of checkEndpoints) {
|
|
11141
11141
|
const url = new URL(endpoint.versionPath, clientUrl);
|
|
11142
11142
|
const request = new Request(url.toString(), { method: "GET" });
|
|
11143
|
-
const response = await
|
|
11143
|
+
const response = await fetch2(request);
|
|
11144
11144
|
await response.arrayBuffer();
|
|
11145
11145
|
if (response.ok) {
|
|
11146
11146
|
return endpoint;
|
|
@@ -14252,7 +14252,7 @@ var init_select2 = __esm({
|
|
|
14252
14252
|
const tableName = getTableLikeName(table);
|
|
14253
14253
|
for (const item of extractUsedTable(table))
|
|
14254
14254
|
this.usedTables.add(item);
|
|
14255
|
-
if (typeof tableName === "string" && this.config.joins?.some((
|
|
14255
|
+
if (typeof tableName === "string" && this.config.joins?.some((join4) => join4.alias === tableName)) {
|
|
14256
14256
|
throw new Error(`Alias "${tableName}" is already used in this query`);
|
|
14257
14257
|
}
|
|
14258
14258
|
if (!this.isPartialSelect) {
|
|
@@ -15141,7 +15141,7 @@ var init_update = __esm({
|
|
|
15141
15141
|
createJoin(joinType) {
|
|
15142
15142
|
return (table, on) => {
|
|
15143
15143
|
const tableName = getTableLikeName(table);
|
|
15144
|
-
if (typeof tableName === "string" && this.config.joins.some((
|
|
15144
|
+
if (typeof tableName === "string" && this.config.joins.some((join4) => join4.alias === tableName)) {
|
|
15145
15145
|
throw new Error(`Alias "${tableName}" is already used in this query`);
|
|
15146
15146
|
}
|
|
15147
15147
|
if (typeof on === "function") {
|
|
@@ -17026,6 +17026,547 @@ var init_secrets = __esm({
|
|
|
17026
17026
|
}
|
|
17027
17027
|
});
|
|
17028
17028
|
|
|
17029
|
+
// package.json
|
|
17030
|
+
var require_package = __commonJS({
|
|
17031
|
+
"package.json"(exports2, module2) {
|
|
17032
|
+
module2.exports = {
|
|
17033
|
+
name: "chron-mcp",
|
|
17034
|
+
version: "0.1.17",
|
|
17035
|
+
mcpName: "io.github.sirinivask/chron",
|
|
17036
|
+
description: "Audit-grade timestamped logs for every AI conversation",
|
|
17037
|
+
repository: {
|
|
17038
|
+
type: "git",
|
|
17039
|
+
url: "https://github.com/sirinivask/chron.git"
|
|
17040
|
+
},
|
|
17041
|
+
main: "dist/index.js",
|
|
17042
|
+
vitest: {
|
|
17043
|
+
include: [
|
|
17044
|
+
"tests/**/*.test.ts"
|
|
17045
|
+
],
|
|
17046
|
+
globals: true
|
|
17047
|
+
},
|
|
17048
|
+
bin: {
|
|
17049
|
+
"chron-mcp": "dist/index.js",
|
|
17050
|
+
chron: "dist/cli/index.js"
|
|
17051
|
+
},
|
|
17052
|
+
files: [
|
|
17053
|
+
"dist",
|
|
17054
|
+
"skills",
|
|
17055
|
+
".claude-plugin",
|
|
17056
|
+
"assets",
|
|
17057
|
+
"dashboards",
|
|
17058
|
+
"README.md"
|
|
17059
|
+
],
|
|
17060
|
+
engines: {
|
|
17061
|
+
node: ">=18"
|
|
17062
|
+
},
|
|
17063
|
+
scripts: {
|
|
17064
|
+
build: "npx esbuild src/index.ts --bundle --format=cjs --outfile=dist/index.js --platform=node && chmod +x dist/index.js && npx esbuild src/cli/index.ts --bundle --format=cjs --outfile=dist/cli/index.js --platform=node && chmod +x dist/cli/index.js",
|
|
17065
|
+
"build:cli": "npx esbuild src/cli/index.ts --bundle --format=cjs --outfile=dist/cli/index.js --platform=node && chmod +x dist/cli/index.js",
|
|
17066
|
+
typecheck: "tsc --noEmit",
|
|
17067
|
+
dev: "tsc --watch",
|
|
17068
|
+
start: "node dist/index.js",
|
|
17069
|
+
test: "vitest run",
|
|
17070
|
+
"test:watch": "vitest",
|
|
17071
|
+
prepublishOnly: "npm test && npm run build"
|
|
17072
|
+
},
|
|
17073
|
+
keywords: [
|
|
17074
|
+
"mcp",
|
|
17075
|
+
"ai",
|
|
17076
|
+
"audit",
|
|
17077
|
+
"logging",
|
|
17078
|
+
"claude",
|
|
17079
|
+
"cursor"
|
|
17080
|
+
],
|
|
17081
|
+
license: "SEE LICENSE IN LICENSE",
|
|
17082
|
+
dependencies: {
|
|
17083
|
+
"@libsql/client": "^0.17.3",
|
|
17084
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
17085
|
+
"drizzle-orm": "^0.45.2",
|
|
17086
|
+
express: "^4.18.2",
|
|
17087
|
+
uuid: "^11.1.1",
|
|
17088
|
+
zod: "^3.22.4"
|
|
17089
|
+
},
|
|
17090
|
+
devDependencies: {
|
|
17091
|
+
"@types/express": "^4.17.21",
|
|
17092
|
+
"@types/node": "^20.0.0",
|
|
17093
|
+
"@types/uuid": "^9.0.0",
|
|
17094
|
+
"drizzle-kit": "^0.20.14",
|
|
17095
|
+
typescript: "^5.4.5",
|
|
17096
|
+
vitest: "^1.4.0"
|
|
17097
|
+
}
|
|
17098
|
+
};
|
|
17099
|
+
}
|
|
17100
|
+
});
|
|
17101
|
+
|
|
17102
|
+
// src/cli/connect.ts
|
|
17103
|
+
var connect_exports = {};
|
|
17104
|
+
__export(connect_exports, {
|
|
17105
|
+
runConnect: () => runConnect
|
|
17106
|
+
});
|
|
17107
|
+
function prompt(rl, question) {
|
|
17108
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
17109
|
+
}
|
|
17110
|
+
function configPath() {
|
|
17111
|
+
return (0, import_path3.join)((0, import_os3.homedir)(), ".chron", "config.json");
|
|
17112
|
+
}
|
|
17113
|
+
function loadConfig() {
|
|
17114
|
+
try {
|
|
17115
|
+
return JSON.parse((0, import_fs2.readFileSync)(configPath(), "utf8"));
|
|
17116
|
+
} catch {
|
|
17117
|
+
return {};
|
|
17118
|
+
}
|
|
17119
|
+
}
|
|
17120
|
+
function saveConfig(data) {
|
|
17121
|
+
const dir = (0, import_path3.join)((0, import_os3.homedir)(), ".chron");
|
|
17122
|
+
(0, import_fs2.mkdirSync)(dir, { recursive: true });
|
|
17123
|
+
(0, import_fs2.writeFileSync)(configPath(), JSON.stringify(data, null, 2), "utf8");
|
|
17124
|
+
}
|
|
17125
|
+
function patchClaudeJson(vars) {
|
|
17126
|
+
const path = (0, import_path3.join)((0, import_os3.homedir)(), ".claude.json");
|
|
17127
|
+
if (!(0, import_fs2.existsSync)(path))
|
|
17128
|
+
return false;
|
|
17129
|
+
try {
|
|
17130
|
+
const raw = (0, import_fs2.readFileSync)(path, "utf8");
|
|
17131
|
+
const doc = JSON.parse(raw);
|
|
17132
|
+
const servers = doc.mcpServers ?? {};
|
|
17133
|
+
const chron = servers.chron ?? {};
|
|
17134
|
+
chron.env = { ...chron.env ?? {}, ...vars };
|
|
17135
|
+
servers.chron = chron;
|
|
17136
|
+
doc.mcpServers = servers;
|
|
17137
|
+
(0, import_fs2.writeFileSync)(path, JSON.stringify(doc, null, 2), "utf8");
|
|
17138
|
+
return true;
|
|
17139
|
+
} catch {
|
|
17140
|
+
return false;
|
|
17141
|
+
}
|
|
17142
|
+
}
|
|
17143
|
+
async function connectCrowdStrike() {
|
|
17144
|
+
process.stdout.write(`
|
|
17145
|
+
${BOLD5}Connect Chron \u2192 CrowdStrike LogScale${RESET5}
|
|
17146
|
+
|
|
17147
|
+
`);
|
|
17148
|
+
process.stdout.write(`${DIM4}Events will be sent directly to your LogScale repository.
|
|
17149
|
+
`);
|
|
17150
|
+
process.stdout.write(`No message content is transmitted \u2014 metadata only.${RESET5}
|
|
17151
|
+
|
|
17152
|
+
`);
|
|
17153
|
+
const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
17154
|
+
let url;
|
|
17155
|
+
let token;
|
|
17156
|
+
try {
|
|
17157
|
+
url = (await prompt(rl, ` LogScale ingest URL ${DIM4}(e.g. https://cloud.us.humio.com/api/v1/ingest/humio-structured)${RESET5}
|
|
17158
|
+
${CYAN4}>${RESET5} `)).trim();
|
|
17159
|
+
if (!url.startsWith("https://")) {
|
|
17160
|
+
process.stdout.write(`
|
|
17161
|
+
${RED}URL must start with https://${RESET5}
|
|
17162
|
+
`);
|
|
17163
|
+
process.exit(1);
|
|
17164
|
+
}
|
|
17165
|
+
token = (await prompt(rl, `
|
|
17166
|
+
LogScale ingest token
|
|
17167
|
+
${CYAN4}>${RESET5} `)).trim();
|
|
17168
|
+
if (!token) {
|
|
17169
|
+
process.stdout.write(`
|
|
17170
|
+
${RED}Token cannot be empty${RESET5}
|
|
17171
|
+
`);
|
|
17172
|
+
process.exit(1);
|
|
17173
|
+
}
|
|
17174
|
+
} finally {
|
|
17175
|
+
rl.close();
|
|
17176
|
+
}
|
|
17177
|
+
process.stdout.write(`
|
|
17178
|
+
${DIM4}Sending test event...${RESET5} `);
|
|
17179
|
+
const testPayload = JSON.stringify([{
|
|
17180
|
+
tags: { host: require("os").hostname(), source: "chron-mcp" },
|
|
17181
|
+
events: [{
|
|
17182
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17183
|
+
attributes: {
|
|
17184
|
+
event_type: "connection_test",
|
|
17185
|
+
chron_version: require_package().version,
|
|
17186
|
+
os: process.platform,
|
|
17187
|
+
message: "chron connect crowdstrike \u2014 test event"
|
|
17188
|
+
}
|
|
17189
|
+
}]
|
|
17190
|
+
}]);
|
|
17191
|
+
let ok = false;
|
|
17192
|
+
let statusCode = 0;
|
|
17193
|
+
try {
|
|
17194
|
+
const res = await fetch(url, {
|
|
17195
|
+
method: "POST",
|
|
17196
|
+
headers: {
|
|
17197
|
+
"Authorization": `Bearer ${token}`,
|
|
17198
|
+
"Content-Type": "application/json"
|
|
17199
|
+
},
|
|
17200
|
+
body: testPayload
|
|
17201
|
+
});
|
|
17202
|
+
statusCode = res.status;
|
|
17203
|
+
ok = res.ok;
|
|
17204
|
+
} catch (e) {
|
|
17205
|
+
process.stdout.write(`${RED}failed${RESET5}
|
|
17206
|
+
`);
|
|
17207
|
+
process.stderr.write(` ${RED}Error: ${e.message}${RESET5}
|
|
17208
|
+
|
|
17209
|
+
`);
|
|
17210
|
+
process.exit(1);
|
|
17211
|
+
}
|
|
17212
|
+
if (!ok) {
|
|
17213
|
+
process.stdout.write(`${RED}failed (HTTP ${statusCode})${RESET5}
|
|
17214
|
+
`);
|
|
17215
|
+
process.stderr.write(` ${RED}Check your URL and token, then try again.${RESET5}
|
|
17216
|
+
|
|
17217
|
+
`);
|
|
17218
|
+
process.exit(1);
|
|
17219
|
+
}
|
|
17220
|
+
process.stdout.write(`${GREEN}OK${RESET5}
|
|
17221
|
+
|
|
17222
|
+
`);
|
|
17223
|
+
const config = loadConfig();
|
|
17224
|
+
config.logscale = { url, connected_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
17225
|
+
saveConfig(config);
|
|
17226
|
+
process.stdout.write(`${GREEN}${BOLD5}Connected!${RESET5} Test event appeared in LogScale.
|
|
17227
|
+
|
|
17228
|
+
`);
|
|
17229
|
+
process.stdout.write(`${BOLD5}Add these env vars to your MCP client config:${RESET5}
|
|
17230
|
+
|
|
17231
|
+
`);
|
|
17232
|
+
process.stdout.write(` ${CYAN4}CHRON_LOGSCALE_URL${RESET5} ${url}
|
|
17233
|
+
`);
|
|
17234
|
+
process.stdout.write(` ${CYAN4}CHRON_LOGSCALE_TOKEN${RESET5} ${DIM4}<your-token>${RESET5}
|
|
17235
|
+
|
|
17236
|
+
`);
|
|
17237
|
+
process.stdout.write(`${DIM4}For Claude Code \u2014 add to the "env" block of the chron entry in ~/.claude.json:${RESET5}
|
|
17238
|
+
|
|
17239
|
+
`);
|
|
17240
|
+
process.stdout.write(` ${DIM4}"env": {
|
|
17241
|
+
`);
|
|
17242
|
+
process.stdout.write(` "CHRON_LOGSCALE_URL": "${url}",
|
|
17243
|
+
`);
|
|
17244
|
+
process.stdout.write(` "CHRON_LOGSCALE_TOKEN": "<your-token>"
|
|
17245
|
+
`);
|
|
17246
|
+
process.stdout.write(` }${RESET5}
|
|
17247
|
+
|
|
17248
|
+
`);
|
|
17249
|
+
process.stdout.write(`${DIM4}Connection saved to ~/.chron/config.json${RESET5}
|
|
17250
|
+
|
|
17251
|
+
`);
|
|
17252
|
+
}
|
|
17253
|
+
function isLocalhost(url) {
|
|
17254
|
+
return url.includes("localhost") || url.includes("127.0.0.1") || url.includes("::1");
|
|
17255
|
+
}
|
|
17256
|
+
async function connectSplunk() {
|
|
17257
|
+
process.stdout.write(`
|
|
17258
|
+
${BOLD5}Connect Chron \u2192 Splunk${RESET5}
|
|
17259
|
+
|
|
17260
|
+
`);
|
|
17261
|
+
process.stdout.write(`${DIM4}Chron will send session telemetry to your Splunk instance via HTTP Event
|
|
17262
|
+
`);
|
|
17263
|
+
process.stdout.write(`Collector (HEC). Message content is never transmitted.${RESET5}
|
|
17264
|
+
|
|
17265
|
+
`);
|
|
17266
|
+
process.stdout.write(`${DIM4}Works with Splunk Enterprise, Splunk Cloud, and local Docker instances.
|
|
17267
|
+
`);
|
|
17268
|
+
process.stdout.write(`See dashboards/splunk/README.md for setup steps.${RESET5}
|
|
17269
|
+
|
|
17270
|
+
`);
|
|
17271
|
+
process.stdout.write(`${DIM4}Note: Splunk 9.x Docker uses HTTPS on port 8088 with a self-signed cert.
|
|
17272
|
+
`);
|
|
17273
|
+
process.stdout.write(`Use https://localhost:8088 \u2014 certificate verification is skipped for localhost.${RESET5}
|
|
17274
|
+
|
|
17275
|
+
`);
|
|
17276
|
+
const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
17277
|
+
let url, token;
|
|
17278
|
+
try {
|
|
17279
|
+
url = (await prompt(rl, ` Splunk HEC base URL ${DIM4}(e.g. https://localhost:8088 or https://your.splunkcloud.com:8088)${RESET5}
|
|
17280
|
+
${CYAN4}>${RESET5} `)).trim();
|
|
17281
|
+
token = (await prompt(rl, `
|
|
17282
|
+
HEC token
|
|
17283
|
+
${CYAN4}>${RESET5} `)).trim();
|
|
17284
|
+
} finally {
|
|
17285
|
+
rl.close();
|
|
17286
|
+
}
|
|
17287
|
+
if (!url.startsWith("http")) {
|
|
17288
|
+
process.stdout.write(`
|
|
17289
|
+
${RED}URL must start with http:// or https://${RESET5}
|
|
17290
|
+
`);
|
|
17291
|
+
process.exit(1);
|
|
17292
|
+
}
|
|
17293
|
+
if (!token) {
|
|
17294
|
+
process.stdout.write(`
|
|
17295
|
+
${RED}HEC token cannot be empty${RESET5}
|
|
17296
|
+
`);
|
|
17297
|
+
process.exit(1);
|
|
17298
|
+
}
|
|
17299
|
+
if (isLocalhost(url)) {
|
|
17300
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
17301
|
+
}
|
|
17302
|
+
process.stdout.write(`
|
|
17303
|
+
${DIM4}Sending test event...${RESET5} `);
|
|
17304
|
+
const hecUrl = `${url.replace(/\/$/, "")}/services/collector/event`;
|
|
17305
|
+
const testPayload = JSON.stringify({
|
|
17306
|
+
time: Date.now() / 1e3,
|
|
17307
|
+
host: require("os").hostname(),
|
|
17308
|
+
source: "chron-mcp",
|
|
17309
|
+
sourcetype: "chron:event",
|
|
17310
|
+
event: {
|
|
17311
|
+
event_type: "connection_test",
|
|
17312
|
+
chron_version: require_package().version,
|
|
17313
|
+
os: process.platform,
|
|
17314
|
+
message: "chron connect splunk \u2014 test event"
|
|
17315
|
+
}
|
|
17316
|
+
});
|
|
17317
|
+
try {
|
|
17318
|
+
const res = await fetch(hecUrl, {
|
|
17319
|
+
method: "POST",
|
|
17320
|
+
headers: {
|
|
17321
|
+
"Authorization": `Splunk ${token}`,
|
|
17322
|
+
"Content-Type": "application/json"
|
|
17323
|
+
},
|
|
17324
|
+
body: testPayload
|
|
17325
|
+
});
|
|
17326
|
+
if (!res.ok) {
|
|
17327
|
+
const err = await res.text();
|
|
17328
|
+
process.stdout.write(`${RED}failed (HTTP ${res.status})${RESET5}
|
|
17329
|
+
`);
|
|
17330
|
+
process.stderr.write(` ${RED}${err}${RESET5}
|
|
17331
|
+
|
|
17332
|
+
`);
|
|
17333
|
+
process.exit(1);
|
|
17334
|
+
}
|
|
17335
|
+
process.stdout.write(`${GREEN}OK${RESET5}
|
|
17336
|
+
|
|
17337
|
+
`);
|
|
17338
|
+
} catch (e) {
|
|
17339
|
+
process.stdout.write(`${RED}failed${RESET5}
|
|
17340
|
+
`);
|
|
17341
|
+
process.stderr.write(` ${RED}Error: ${e.message}${RESET5}
|
|
17342
|
+
|
|
17343
|
+
`);
|
|
17344
|
+
process.exit(1);
|
|
17345
|
+
}
|
|
17346
|
+
const config = loadConfig();
|
|
17347
|
+
config.splunk = { url, token, insecure: isLocalhost(url), connected_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
17348
|
+
saveConfig(config);
|
|
17349
|
+
const splunkVars = { CHRON_SPLUNK_URL: url, CHRON_SPLUNK_TOKEN: token };
|
|
17350
|
+
if (isLocalhost(url))
|
|
17351
|
+
splunkVars.CHRON_SPLUNK_INSECURE = "1";
|
|
17352
|
+
patchClaudeJson(splunkVars);
|
|
17353
|
+
process.stdout.write(`${GREEN}${BOLD5}Connected!${RESET5} Splunk is receiving chron events.
|
|
17354
|
+
|
|
17355
|
+
`);
|
|
17356
|
+
process.stdout.write(`${DIM4}Config saved to ~/.chron/config.json \u2014 events will flow immediately in all running sessions.${RESET5}
|
|
17357
|
+
|
|
17358
|
+
`);
|
|
17359
|
+
}
|
|
17360
|
+
async function connectSentinel() {
|
|
17361
|
+
process.stdout.write(`
|
|
17362
|
+
${BOLD5}Connect Chron \u2192 Microsoft Sentinel${RESET5}
|
|
17363
|
+
|
|
17364
|
+
`);
|
|
17365
|
+
process.stdout.write(`${DIM4}Chron will send session telemetry to your Sentinel workspace via the
|
|
17366
|
+
`);
|
|
17367
|
+
process.stdout.write(`Azure Monitor Logs Ingestion API. Message content is never transmitted.${RESET5}
|
|
17368
|
+
|
|
17369
|
+
`);
|
|
17370
|
+
process.stdout.write(`${DIM4}Prerequisites: Azure App Registration, Data Collection Endpoint (DCE),
|
|
17371
|
+
`);
|
|
17372
|
+
process.stdout.write(`and a Data Collection Rule (DCR) pointing to a ChronEvents_CL custom table.
|
|
17373
|
+
`);
|
|
17374
|
+
process.stdout.write(`See dashboards/sentinel/README.md for full setup steps.${RESET5}
|
|
17375
|
+
|
|
17376
|
+
`);
|
|
17377
|
+
const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
17378
|
+
let tenantId, clientId, clientSecret, dce, dcrId, stream;
|
|
17379
|
+
try {
|
|
17380
|
+
tenantId = (await prompt(rl, ` Azure Tenant ID ${DIM4}(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)${RESET5}
|
|
17381
|
+
${CYAN4}>${RESET5} `)).trim();
|
|
17382
|
+
clientId = (await prompt(rl, `
|
|
17383
|
+
App Registration Client ID
|
|
17384
|
+
${CYAN4}>${RESET5} `)).trim();
|
|
17385
|
+
clientSecret = (await prompt(rl, `
|
|
17386
|
+
Client Secret
|
|
17387
|
+
${CYAN4}>${RESET5} `)).trim();
|
|
17388
|
+
dce = (await prompt(rl, `
|
|
17389
|
+
Data Collection Endpoint URL ${DIM4}(https://...)${RESET5}
|
|
17390
|
+
${CYAN4}>${RESET5} `)).trim();
|
|
17391
|
+
dcrId = (await prompt(rl, `
|
|
17392
|
+
DCR Immutable ID ${DIM4}(dcr-xxxx)${RESET5}
|
|
17393
|
+
${CYAN4}>${RESET5} `)).trim();
|
|
17394
|
+
stream = (await prompt(rl, `
|
|
17395
|
+
Stream name ${DIM4}(press Enter for Custom-ChronEvents_CL)${RESET5}
|
|
17396
|
+
${CYAN4}>${RESET5} `)).trim() || "Custom-ChronEvents_CL";
|
|
17397
|
+
} finally {
|
|
17398
|
+
rl.close();
|
|
17399
|
+
}
|
|
17400
|
+
if (!tenantId || !clientId || !clientSecret) {
|
|
17401
|
+
process.stdout.write(`
|
|
17402
|
+
${RED}Tenant ID, Client ID, and Client Secret are all required.${RESET5}
|
|
17403
|
+
`);
|
|
17404
|
+
process.exit(1);
|
|
17405
|
+
}
|
|
17406
|
+
if (!dce.startsWith("https://")) {
|
|
17407
|
+
process.stdout.write(`
|
|
17408
|
+
${RED}DCE URL must start with https://${RESET5}
|
|
17409
|
+
`);
|
|
17410
|
+
process.exit(1);
|
|
17411
|
+
}
|
|
17412
|
+
if (dcrId && !dcrId.startsWith("dcr-")) {
|
|
17413
|
+
process.stdout.write(`
|
|
17414
|
+
${YELLOW3}Warning: DCR Immutable ID usually starts with "dcr-"${RESET5}
|
|
17415
|
+
`);
|
|
17416
|
+
}
|
|
17417
|
+
process.stdout.write(`
|
|
17418
|
+
${DIM4}Authenticating with Azure AD...${RESET5} `);
|
|
17419
|
+
let accessToken;
|
|
17420
|
+
try {
|
|
17421
|
+
const tokenRes = await fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, {
|
|
17422
|
+
method: "POST",
|
|
17423
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
17424
|
+
body: new URLSearchParams({
|
|
17425
|
+
grant_type: "client_credentials",
|
|
17426
|
+
client_id: clientId,
|
|
17427
|
+
client_secret: clientSecret,
|
|
17428
|
+
scope: "https://monitor.azure.com/.default"
|
|
17429
|
+
}).toString()
|
|
17430
|
+
});
|
|
17431
|
+
if (!tokenRes.ok) {
|
|
17432
|
+
const err = await tokenRes.text();
|
|
17433
|
+
process.stdout.write(`${RED}failed (HTTP ${tokenRes.status})${RESET5}
|
|
17434
|
+
`);
|
|
17435
|
+
process.stderr.write(` ${RED}${err}${RESET5}
|
|
17436
|
+
|
|
17437
|
+
`);
|
|
17438
|
+
process.exit(1);
|
|
17439
|
+
}
|
|
17440
|
+
const tokenData = await tokenRes.json();
|
|
17441
|
+
accessToken = tokenData.access_token;
|
|
17442
|
+
process.stdout.write(`${GREEN}OK${RESET5}
|
|
17443
|
+
`);
|
|
17444
|
+
} catch (e) {
|
|
17445
|
+
process.stdout.write(`${RED}failed${RESET5}
|
|
17446
|
+
`);
|
|
17447
|
+
process.stderr.write(` ${RED}Error: ${e.message}${RESET5}
|
|
17448
|
+
|
|
17449
|
+
`);
|
|
17450
|
+
process.exit(1);
|
|
17451
|
+
}
|
|
17452
|
+
process.stdout.write(`${DIM4}Sending test event...${RESET5} `);
|
|
17453
|
+
const ingestUrl = `${dce}/dataCollectionRules/${dcrId}/streams/${stream}?api-version=2023-01-01`;
|
|
17454
|
+
const testRecord = {
|
|
17455
|
+
TimeGenerated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17456
|
+
EventType: "connection_test",
|
|
17457
|
+
SessionIdPrefix: "test",
|
|
17458
|
+
AiTool: "",
|
|
17459
|
+
OS: process.platform,
|
|
17460
|
+
ChronVersion: require_package().version,
|
|
17461
|
+
Computer: require("os").hostname(),
|
|
17462
|
+
Role: "",
|
|
17463
|
+
DetectionType: "",
|
|
17464
|
+
MaskedValue: ""
|
|
17465
|
+
};
|
|
17466
|
+
try {
|
|
17467
|
+
const res = await fetch(ingestUrl, {
|
|
17468
|
+
method: "POST",
|
|
17469
|
+
headers: {
|
|
17470
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
17471
|
+
"Content-Type": "application/json"
|
|
17472
|
+
},
|
|
17473
|
+
body: JSON.stringify([testRecord])
|
|
17474
|
+
});
|
|
17475
|
+
if (!res.ok) {
|
|
17476
|
+
const err = await res.text();
|
|
17477
|
+
process.stdout.write(`${RED}failed (HTTP ${res.status})${RESET5}
|
|
17478
|
+
`);
|
|
17479
|
+
process.stderr.write(` ${RED}${err}${RESET5}
|
|
17480
|
+
|
|
17481
|
+
`);
|
|
17482
|
+
process.exit(1);
|
|
17483
|
+
}
|
|
17484
|
+
process.stdout.write(`${GREEN}OK${RESET5}
|
|
17485
|
+
|
|
17486
|
+
`);
|
|
17487
|
+
} catch (e) {
|
|
17488
|
+
process.stdout.write(`${RED}failed${RESET5}
|
|
17489
|
+
`);
|
|
17490
|
+
process.stderr.write(` ${RED}Error: ${e.message}${RESET5}
|
|
17491
|
+
|
|
17492
|
+
`);
|
|
17493
|
+
process.exit(1);
|
|
17494
|
+
}
|
|
17495
|
+
const config = loadConfig();
|
|
17496
|
+
config.sentinel = { dce, dcrId, stream, tenantId, clientId, connected_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
17497
|
+
saveConfig(config);
|
|
17498
|
+
process.stdout.write(`${GREEN}${BOLD5}Connected!${RESET5} Test event sent to Sentinel workspace.
|
|
17499
|
+
|
|
17500
|
+
`);
|
|
17501
|
+
process.stdout.write(`${BOLD5}Add these env vars to your MCP client config:${RESET5}
|
|
17502
|
+
|
|
17503
|
+
`);
|
|
17504
|
+
process.stdout.write(` ${CYAN4}CHRON_SENTINEL_TENANT_ID${RESET5} ${tenantId}
|
|
17505
|
+
`);
|
|
17506
|
+
process.stdout.write(` ${CYAN4}CHRON_SENTINEL_CLIENT_ID${RESET5} ${clientId}
|
|
17507
|
+
`);
|
|
17508
|
+
process.stdout.write(` ${CYAN4}CHRON_SENTINEL_CLIENT_SECRET${RESET5} ${DIM4}<your-client-secret>${RESET5}
|
|
17509
|
+
`);
|
|
17510
|
+
process.stdout.write(` ${CYAN4}CHRON_SENTINEL_DCE${RESET5} ${dce}
|
|
17511
|
+
`);
|
|
17512
|
+
process.stdout.write(` ${CYAN4}CHRON_SENTINEL_DCR_ID${RESET5} ${dcrId}
|
|
17513
|
+
`);
|
|
17514
|
+
process.stdout.write(` ${CYAN4}CHRON_SENTINEL_STREAM${RESET5} ${stream}
|
|
17515
|
+
|
|
17516
|
+
`);
|
|
17517
|
+
process.stdout.write(`${DIM4}For Claude Code \u2014 add to the "env" block of the chron entry in ~/.claude.json.
|
|
17518
|
+
`);
|
|
17519
|
+
process.stdout.write(`Connection saved to ~/.chron/config.json${RESET5}
|
|
17520
|
+
|
|
17521
|
+
`);
|
|
17522
|
+
}
|
|
17523
|
+
async function runConnect(args2) {
|
|
17524
|
+
const [subcommand] = args2;
|
|
17525
|
+
switch (subcommand) {
|
|
17526
|
+
case "crowdstrike":
|
|
17527
|
+
await connectCrowdStrike();
|
|
17528
|
+
break;
|
|
17529
|
+
case "sentinel":
|
|
17530
|
+
await connectSentinel();
|
|
17531
|
+
break;
|
|
17532
|
+
case "splunk":
|
|
17533
|
+
await connectSplunk();
|
|
17534
|
+
break;
|
|
17535
|
+
default: {
|
|
17536
|
+
const name = subcommand ? `Unknown integration: ${subcommand}
|
|
17537
|
+
|
|
17538
|
+
` : "";
|
|
17539
|
+
process.stdout.write(
|
|
17540
|
+
`${name}Usage: chron connect <integration>
|
|
17541
|
+
|
|
17542
|
+
Integrations:
|
|
17543
|
+
crowdstrike Connect to CrowdStrike LogScale (direct ingest)
|
|
17544
|
+
sentinel Connect to Microsoft Sentinel (Azure Monitor Logs Ingestion API)
|
|
17545
|
+
splunk Connect to Splunk via HTTP Event Collector (HEC)
|
|
17546
|
+
`
|
|
17547
|
+
);
|
|
17548
|
+
process.exit(subcommand ? 1 : 0);
|
|
17549
|
+
}
|
|
17550
|
+
}
|
|
17551
|
+
}
|
|
17552
|
+
var import_readline, import_os3, import_path3, import_fs2, RESET5, BOLD5, DIM4, CYAN4, GREEN, RED, YELLOW3;
|
|
17553
|
+
var init_connect = __esm({
|
|
17554
|
+
"src/cli/connect.ts"() {
|
|
17555
|
+
"use strict";
|
|
17556
|
+
import_readline = require("readline");
|
|
17557
|
+
import_os3 = require("os");
|
|
17558
|
+
import_path3 = require("path");
|
|
17559
|
+
import_fs2 = require("fs");
|
|
17560
|
+
RESET5 = "\x1B[0m";
|
|
17561
|
+
BOLD5 = "\x1B[1m";
|
|
17562
|
+
DIM4 = "\x1B[2m";
|
|
17563
|
+
CYAN4 = "\x1B[36m";
|
|
17564
|
+
GREEN = "\x1B[32m";
|
|
17565
|
+
RED = "\x1B[31m";
|
|
17566
|
+
YELLOW3 = "\x1B[33m";
|
|
17567
|
+
}
|
|
17568
|
+
});
|
|
17569
|
+
|
|
17029
17570
|
// src/cli/index.ts
|
|
17030
17571
|
var [, , command, ...args] = process.argv;
|
|
17031
17572
|
async function main() {
|
|
@@ -17055,6 +17596,11 @@ async function main() {
|
|
|
17055
17596
|
await runSecrets2(args);
|
|
17056
17597
|
break;
|
|
17057
17598
|
}
|
|
17599
|
+
case "connect": {
|
|
17600
|
+
const { runConnect: runConnect2 } = await Promise.resolve().then(() => (init_connect(), connect_exports));
|
|
17601
|
+
await runConnect2(args);
|
|
17602
|
+
break;
|
|
17603
|
+
}
|
|
17058
17604
|
default: {
|
|
17059
17605
|
const name = command ? `Unknown command: ${command}
|
|
17060
17606
|
|
|
@@ -17068,6 +17614,7 @@ Commands:
|
|
|
17068
17614
|
export Export a session as markdown
|
|
17069
17615
|
secrets List detected secrets across sessions
|
|
17070
17616
|
settings View current configuration
|
|
17617
|
+
connect Connect to a SIEM integration (crowdstrike, sentinel, splunk)
|
|
17071
17618
|
|
|
17072
17619
|
Options (history):
|
|
17073
17620
|
--limit=<n> Max sessions to show (default: 20)
|