chainlesschain 0.37.12 → 0.40.1
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/package.json +3 -2
- package/src/commands/agent.js +7 -1
- package/src/commands/ask.js +24 -9
- package/src/commands/chat.js +7 -1
- package/src/commands/cli-anything.js +266 -0
- package/src/commands/compliance.js +216 -0
- package/src/commands/dao.js +312 -0
- package/src/commands/dlp.js +278 -0
- package/src/commands/evomap.js +558 -0
- package/src/commands/hardening.js +230 -0
- package/src/commands/matrix.js +168 -0
- package/src/commands/nostr.js +185 -0
- package/src/commands/pqc.js +162 -0
- package/src/commands/scim.js +218 -0
- package/src/commands/serve.js +109 -0
- package/src/commands/siem.js +156 -0
- package/src/commands/social.js +480 -0
- package/src/commands/terraform.js +148 -0
- package/src/constants.js +1 -0
- package/src/index.js +60 -0
- package/src/lib/autonomous-agent.js +487 -0
- package/src/lib/cli-anything-bridge.js +379 -0
- package/src/lib/cli-context-engineering.js +472 -0
- package/src/lib/compliance-manager.js +290 -0
- package/src/lib/content-recommender.js +205 -0
- package/src/lib/dao-governance.js +296 -0
- package/src/lib/dlp-engine.js +304 -0
- package/src/lib/evomap-client.js +135 -0
- package/src/lib/evomap-federation.js +240 -0
- package/src/lib/evomap-governance.js +250 -0
- package/src/lib/evomap-manager.js +227 -0
- package/src/lib/git-integration.js +1 -1
- package/src/lib/hardening-manager.js +275 -0
- package/src/lib/llm-providers.js +14 -1
- package/src/lib/matrix-bridge.js +196 -0
- package/src/lib/nostr-bridge.js +195 -0
- package/src/lib/permanent-memory.js +370 -0
- package/src/lib/plan-mode.js +211 -0
- package/src/lib/pqc-manager.js +196 -0
- package/src/lib/scim-manager.js +212 -0
- package/src/lib/session-manager.js +38 -0
- package/src/lib/siem-exporter.js +137 -0
- package/src/lib/social-manager.js +283 -0
- package/src/lib/task-model-selector.js +232 -0
- package/src/lib/terraform-manager.js +201 -0
- package/src/lib/ws-server.js +474 -0
- package/src/repl/agent-repl.js +796 -41
- package/src/repl/chat-repl.js +14 -6
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PQC commands
|
|
3
|
+
* chainlesschain pqc keys|generate|migration-status|migrate
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { logger } from "../lib/logger.js";
|
|
8
|
+
import { bootstrap, shutdown } from "../runtime/bootstrap.js";
|
|
9
|
+
import {
|
|
10
|
+
ensurePQCTables,
|
|
11
|
+
listKeys,
|
|
12
|
+
generateKey,
|
|
13
|
+
getMigrationStatus,
|
|
14
|
+
migrate,
|
|
15
|
+
} from "../lib/pqc-manager.js";
|
|
16
|
+
|
|
17
|
+
export function registerPqcCommand(program) {
|
|
18
|
+
const pqc = program
|
|
19
|
+
.command("pqc")
|
|
20
|
+
.description("Post-quantum cryptography — key management and migration");
|
|
21
|
+
|
|
22
|
+
// pqc keys
|
|
23
|
+
pqc
|
|
24
|
+
.command("keys")
|
|
25
|
+
.description("List PQC keys")
|
|
26
|
+
.option("-a, --algorithm <algo>", "Filter by algorithm")
|
|
27
|
+
.option("--json", "Output as JSON")
|
|
28
|
+
.action(async (options) => {
|
|
29
|
+
try {
|
|
30
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
31
|
+
if (!ctx.db) {
|
|
32
|
+
logger.error("Database not available");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const db = ctx.db.getDatabase();
|
|
36
|
+
ensurePQCTables(db);
|
|
37
|
+
|
|
38
|
+
const keys = listKeys({ algorithm: options.algorithm });
|
|
39
|
+
if (options.json) {
|
|
40
|
+
console.log(JSON.stringify(keys, null, 2));
|
|
41
|
+
} else if (keys.length === 0) {
|
|
42
|
+
logger.info("No PQC keys. Use `pqc generate` to create one.");
|
|
43
|
+
} else {
|
|
44
|
+
for (const k of keys) {
|
|
45
|
+
logger.log(
|
|
46
|
+
` ${chalk.cyan(k.id.slice(0, 8))} ${k.algorithm} [${k.purpose}] size=${k.keySize} hybrid=${k.hybridMode}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await shutdown();
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logger.error(`Failed: ${err.message}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// pqc generate
|
|
59
|
+
pqc
|
|
60
|
+
.command("generate <algorithm>")
|
|
61
|
+
.description("Generate a PQC key pair")
|
|
62
|
+
.option(
|
|
63
|
+
"-p, --purpose <purpose>",
|
|
64
|
+
"Key purpose: encryption, signing, key_exchange",
|
|
65
|
+
"encryption",
|
|
66
|
+
)
|
|
67
|
+
.action(async (algorithm, options) => {
|
|
68
|
+
try {
|
|
69
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
70
|
+
if (!ctx.db) {
|
|
71
|
+
logger.error("Database not available");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
const db = ctx.db.getDatabase();
|
|
75
|
+
ensurePQCTables(db);
|
|
76
|
+
|
|
77
|
+
const key = generateKey(db, algorithm, options.purpose);
|
|
78
|
+
logger.success("PQC key generated");
|
|
79
|
+
logger.log(` ${chalk.bold("ID:")} ${chalk.cyan(key.id)}`);
|
|
80
|
+
logger.log(` ${chalk.bold("Algorithm:")} ${key.algorithm}`);
|
|
81
|
+
logger.log(` ${chalk.bold("Purpose:")} ${key.purpose}`);
|
|
82
|
+
logger.log(` ${chalk.bold("Key Size:")} ${key.keySize}`);
|
|
83
|
+
logger.log(` ${chalk.bold("Hybrid:")} ${key.hybridMode}`);
|
|
84
|
+
if (key.classicalAlgorithm) {
|
|
85
|
+
logger.log(` ${chalk.bold("Classical:")} ${key.classicalAlgorithm}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await shutdown();
|
|
89
|
+
} catch (err) {
|
|
90
|
+
logger.error(`Failed: ${err.message}`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// pqc migration-status
|
|
96
|
+
pqc
|
|
97
|
+
.command("migration-status")
|
|
98
|
+
.description("Show PQC migration status")
|
|
99
|
+
.option("--json", "Output as JSON")
|
|
100
|
+
.action(async (options) => {
|
|
101
|
+
try {
|
|
102
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
103
|
+
if (!ctx.db) {
|
|
104
|
+
logger.error("Database not available");
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
const db = ctx.db.getDatabase();
|
|
108
|
+
ensurePQCTables(db);
|
|
109
|
+
|
|
110
|
+
const plans = getMigrationStatus();
|
|
111
|
+
if (options.json) {
|
|
112
|
+
console.log(JSON.stringify(plans, null, 2));
|
|
113
|
+
} else if (plans.length === 0) {
|
|
114
|
+
logger.info("No migration plans.");
|
|
115
|
+
} else {
|
|
116
|
+
for (const p of plans) {
|
|
117
|
+
logger.log(
|
|
118
|
+
` ${chalk.cyan(p.id.slice(0, 8))} ${p.planName} ${p.sourceAlgorithm}→${p.targetAlgorithm} [${p.status}] ${p.migratedKeys}/${p.totalKeys}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await shutdown();
|
|
124
|
+
} catch (err) {
|
|
125
|
+
logger.error(`Failed: ${err.message}`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// pqc migrate
|
|
131
|
+
pqc
|
|
132
|
+
.command("migrate <plan-name> <target-algorithm>")
|
|
133
|
+
.description("Execute PQC key migration")
|
|
134
|
+
.option("-s, --source <algorithm>", "Source algorithm to migrate from")
|
|
135
|
+
.action(async (planName, targetAlgorithm, options) => {
|
|
136
|
+
try {
|
|
137
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
138
|
+
if (!ctx.db) {
|
|
139
|
+
logger.error("Database not available");
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
const db = ctx.db.getDatabase();
|
|
143
|
+
ensurePQCTables(db);
|
|
144
|
+
|
|
145
|
+
const plan = migrate(db, planName, options.source, targetAlgorithm);
|
|
146
|
+
logger.success("Migration completed");
|
|
147
|
+
logger.log(` ${chalk.bold("ID:")} ${chalk.cyan(plan.id)}`);
|
|
148
|
+
logger.log(` ${chalk.bold("Plan:")} ${plan.planName}`);
|
|
149
|
+
logger.log(` ${chalk.bold("Source:")} ${plan.sourceAlgorithm}`);
|
|
150
|
+
logger.log(` ${chalk.bold("Target:")} ${plan.targetAlgorithm}`);
|
|
151
|
+
logger.log(
|
|
152
|
+
` ${chalk.bold("Migrated:")} ${plan.migratedKeys}/${plan.totalKeys}`,
|
|
153
|
+
);
|
|
154
|
+
logger.log(` ${chalk.bold("Status:")} ${plan.status}`);
|
|
155
|
+
|
|
156
|
+
await shutdown();
|
|
157
|
+
} catch (err) {
|
|
158
|
+
logger.error(`Failed: ${err.message}`);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCIM commands
|
|
3
|
+
* chainlesschain scim users|connectors|sync|status
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { logger } from "../lib/logger.js";
|
|
8
|
+
import { bootstrap, shutdown } from "../runtime/bootstrap.js";
|
|
9
|
+
import {
|
|
10
|
+
ensureSCIMTables,
|
|
11
|
+
listUsers,
|
|
12
|
+
createUser,
|
|
13
|
+
getUser,
|
|
14
|
+
deleteUser,
|
|
15
|
+
listConnectors,
|
|
16
|
+
addConnector,
|
|
17
|
+
syncProvision,
|
|
18
|
+
getStatus,
|
|
19
|
+
} from "../lib/scim-manager.js";
|
|
20
|
+
|
|
21
|
+
export function registerScimCommand(program) {
|
|
22
|
+
const scim = program
|
|
23
|
+
.command("scim")
|
|
24
|
+
.description("SCIM provisioning — user management, connectors, sync");
|
|
25
|
+
|
|
26
|
+
// scim users
|
|
27
|
+
const users = scim.command("users").description("SCIM user management");
|
|
28
|
+
|
|
29
|
+
users
|
|
30
|
+
.command("list")
|
|
31
|
+
.description("List SCIM users")
|
|
32
|
+
.option("--json", "Output as JSON")
|
|
33
|
+
.action(async (options) => {
|
|
34
|
+
try {
|
|
35
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
36
|
+
if (!ctx.db) {
|
|
37
|
+
logger.error("Database not available");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const db = ctx.db.getDatabase();
|
|
41
|
+
ensureSCIMTables(db);
|
|
42
|
+
|
|
43
|
+
const result = listUsers();
|
|
44
|
+
if (options.json) {
|
|
45
|
+
console.log(JSON.stringify(result, null, 2));
|
|
46
|
+
} else if (result.resources.length === 0) {
|
|
47
|
+
logger.info("No SCIM users.");
|
|
48
|
+
} else {
|
|
49
|
+
for (const u of result.resources) {
|
|
50
|
+
logger.log(
|
|
51
|
+
` ${chalk.cyan(u.id.slice(0, 8))} ${u.userName} (${u.displayName}) active=${u.active}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
logger.log(`\n${result.totalResults} user(s) total.`);
|
|
55
|
+
}
|
|
56
|
+
await shutdown();
|
|
57
|
+
} catch (err) {
|
|
58
|
+
logger.error(`Failed: ${err.message}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
users
|
|
64
|
+
.command("create <username>")
|
|
65
|
+
.description("Create a SCIM user")
|
|
66
|
+
.option("-n, --name <display-name>", "Display name")
|
|
67
|
+
.option("-e, --email <email>", "Email address")
|
|
68
|
+
.action(async (username, options) => {
|
|
69
|
+
try {
|
|
70
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
71
|
+
if (!ctx.db) {
|
|
72
|
+
logger.error("Database not available");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const db = ctx.db.getDatabase();
|
|
76
|
+
ensureSCIMTables(db);
|
|
77
|
+
|
|
78
|
+
const user = createUser(db, username, options.name, options.email);
|
|
79
|
+
logger.success("User created");
|
|
80
|
+
logger.log(` ${chalk.bold("ID:")} ${chalk.cyan(user.id)}`);
|
|
81
|
+
logger.log(` ${chalk.bold("Username:")} ${user.userName}`);
|
|
82
|
+
logger.log(` ${chalk.bold("Name:")} ${user.displayName}`);
|
|
83
|
+
await shutdown();
|
|
84
|
+
} catch (err) {
|
|
85
|
+
logger.error(`Failed: ${err.message}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
users
|
|
91
|
+
.command("get <user-id>")
|
|
92
|
+
.description("Get a SCIM user by ID")
|
|
93
|
+
.option("--json", "Output as JSON")
|
|
94
|
+
.action(async (userId, options) => {
|
|
95
|
+
try {
|
|
96
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
97
|
+
if (!ctx.db) {
|
|
98
|
+
logger.error("Database not available");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
const db = ctx.db.getDatabase();
|
|
102
|
+
ensureSCIMTables(db);
|
|
103
|
+
|
|
104
|
+
const user = getUser(userId);
|
|
105
|
+
if (!user) {
|
|
106
|
+
logger.error(`User not found: ${userId}`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
if (options.json) {
|
|
110
|
+
console.log(JSON.stringify(user, null, 2));
|
|
111
|
+
} else {
|
|
112
|
+
logger.log(` ${chalk.bold("ID:")} ${chalk.cyan(user.id)}`);
|
|
113
|
+
logger.log(` ${chalk.bold("Username:")} ${user.userName}`);
|
|
114
|
+
logger.log(` ${chalk.bold("Name:")} ${user.displayName}`);
|
|
115
|
+
logger.log(` ${chalk.bold("Email:")} ${user.email || "N/A"}`);
|
|
116
|
+
logger.log(` ${chalk.bold("Active:")} ${user.active}`);
|
|
117
|
+
}
|
|
118
|
+
await shutdown();
|
|
119
|
+
} catch (err) {
|
|
120
|
+
logger.error(`Failed: ${err.message}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
users
|
|
126
|
+
.command("delete <user-id>")
|
|
127
|
+
.description("Delete a SCIM user")
|
|
128
|
+
.action(async (userId) => {
|
|
129
|
+
try {
|
|
130
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
131
|
+
if (!ctx.db) {
|
|
132
|
+
logger.error("Database not available");
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
const db = ctx.db.getDatabase();
|
|
136
|
+
ensureSCIMTables(db);
|
|
137
|
+
|
|
138
|
+
deleteUser(db, userId);
|
|
139
|
+
logger.success(`User ${chalk.cyan(userId.slice(0, 8))} deleted`);
|
|
140
|
+
await shutdown();
|
|
141
|
+
} catch (err) {
|
|
142
|
+
logger.error(`Failed: ${err.message}`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// scim connectors
|
|
148
|
+
scim
|
|
149
|
+
.command("connectors")
|
|
150
|
+
.description("List SCIM connectors")
|
|
151
|
+
.option("--json", "Output as JSON")
|
|
152
|
+
.action(async (options) => {
|
|
153
|
+
try {
|
|
154
|
+
const connectors = listConnectors();
|
|
155
|
+
if (options.json) {
|
|
156
|
+
console.log(JSON.stringify(connectors, null, 2));
|
|
157
|
+
} else if (connectors.length === 0) {
|
|
158
|
+
logger.info("No SCIM connectors configured.");
|
|
159
|
+
} else {
|
|
160
|
+
for (const c of connectors) {
|
|
161
|
+
logger.log(
|
|
162
|
+
` ${chalk.cyan(c.id.slice(0, 8))} ${c.name} [${c.provider}] status=${c.status}`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
logger.error(`Failed: ${err.message}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// scim sync
|
|
173
|
+
scim
|
|
174
|
+
.command("sync <connector-id>")
|
|
175
|
+
.description("Trigger SCIM provisioning sync")
|
|
176
|
+
.action(async (connectorId) => {
|
|
177
|
+
try {
|
|
178
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
179
|
+
if (!ctx.db) {
|
|
180
|
+
logger.error("Database not available");
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const db = ctx.db.getDatabase();
|
|
184
|
+
ensureSCIMTables(db);
|
|
185
|
+
|
|
186
|
+
const result = syncProvision(db, connectorId);
|
|
187
|
+
logger.success(`Sync completed via ${chalk.cyan(result.connector)}`);
|
|
188
|
+
await shutdown();
|
|
189
|
+
} catch (err) {
|
|
190
|
+
logger.error(`Failed: ${err.message}`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// scim status
|
|
196
|
+
scim
|
|
197
|
+
.command("status")
|
|
198
|
+
.description("Show SCIM provisioning status")
|
|
199
|
+
.option("--json", "Output as JSON")
|
|
200
|
+
.action(async (options) => {
|
|
201
|
+
try {
|
|
202
|
+
const status = getStatus();
|
|
203
|
+
if (options.json) {
|
|
204
|
+
console.log(JSON.stringify(status, null, 2));
|
|
205
|
+
} else {
|
|
206
|
+
logger.log(` ${chalk.bold("Users:")} ${status.users}`);
|
|
207
|
+
logger.log(` ${chalk.bold("Connectors:")} ${status.connectors}`);
|
|
208
|
+
logger.log(` ${chalk.bold("Sync Ops:")} ${status.syncOperations}`);
|
|
209
|
+
logger.log(
|
|
210
|
+
` ${chalk.bold("Last Sync:")} ${status.lastSync || "never"}`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
logger.error(`Failed: ${err.message}`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* serve command — start a WebSocket server for remote CLI access
|
|
3
|
+
* chainlesschain serve [--port] [--host] [--token] [--max-connections] [--timeout] [--allow-remote]
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { logger } from "../lib/logger.js";
|
|
8
|
+
import { ChainlessChainWSServer } from "../lib/ws-server.js";
|
|
9
|
+
|
|
10
|
+
export function registerServeCommand(program) {
|
|
11
|
+
program
|
|
12
|
+
.command("serve")
|
|
13
|
+
.description("Start WebSocket server for remote CLI access")
|
|
14
|
+
.option("-p, --port <port>", "Port number", "18800")
|
|
15
|
+
.option("-H, --host <host>", "Bind host", "127.0.0.1")
|
|
16
|
+
.option(
|
|
17
|
+
"--token <token>",
|
|
18
|
+
"Authentication token (required for remote access)",
|
|
19
|
+
)
|
|
20
|
+
.option("--max-connections <n>", "Maximum concurrent connections", "10")
|
|
21
|
+
.option(
|
|
22
|
+
"--timeout <ms>",
|
|
23
|
+
"Command execution timeout in milliseconds",
|
|
24
|
+
"30000",
|
|
25
|
+
)
|
|
26
|
+
.option(
|
|
27
|
+
"--allow-remote",
|
|
28
|
+
"Allow non-localhost connections (requires --token)",
|
|
29
|
+
)
|
|
30
|
+
.action(async (opts) => {
|
|
31
|
+
const port = parseInt(opts.port, 10);
|
|
32
|
+
const maxConnections = parseInt(opts.maxConnections, 10);
|
|
33
|
+
const timeout = parseInt(opts.timeout, 10);
|
|
34
|
+
let host = opts.host;
|
|
35
|
+
|
|
36
|
+
// Validation
|
|
37
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
38
|
+
logger.error("Invalid port number. Must be between 1 and 65535.");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (opts.allowRemote) {
|
|
43
|
+
if (!opts.token) {
|
|
44
|
+
logger.error("--allow-remote requires --token for security.");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
host = "0.0.0.0";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const server = new ChainlessChainWSServer({
|
|
51
|
+
port,
|
|
52
|
+
host,
|
|
53
|
+
token: opts.token || null,
|
|
54
|
+
maxConnections,
|
|
55
|
+
timeout,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Event logging
|
|
59
|
+
server.on("connection", ({ clientId, ip }) => {
|
|
60
|
+
logger.log(chalk.green(` + Client connected: ${clientId} (${ip})`));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
server.on("disconnection", ({ clientId, reason }) => {
|
|
64
|
+
const extra = reason ? ` (${reason})` : "";
|
|
65
|
+
logger.log(
|
|
66
|
+
chalk.yellow(` - Client disconnected: ${clientId}${extra}`),
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
server.on("command:start", ({ id, command }) => {
|
|
71
|
+
logger.log(chalk.cyan(` > [${id}] ${command}`));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
server.on("command:end", ({ id, exitCode }) => {
|
|
75
|
+
const color = exitCode === 0 ? chalk.green : chalk.red;
|
|
76
|
+
logger.log(color(` < [${id}] exit ${exitCode}`));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Graceful shutdown
|
|
80
|
+
const shutdown = async () => {
|
|
81
|
+
logger.log("\n" + chalk.yellow("Shutting down WebSocket server..."));
|
|
82
|
+
await server.stop();
|
|
83
|
+
process.exit(0);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
process.on("SIGINT", shutdown);
|
|
87
|
+
process.on("SIGTERM", shutdown);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await server.start();
|
|
91
|
+
|
|
92
|
+
logger.log("");
|
|
93
|
+
logger.log(chalk.bold(" ChainlessChain WebSocket Server"));
|
|
94
|
+
logger.log("");
|
|
95
|
+
logger.log(` Address: ${chalk.cyan(`ws://${host}:${port}`)}`);
|
|
96
|
+
logger.log(
|
|
97
|
+
` Auth: ${opts.token ? chalk.green("enabled") : chalk.yellow("disabled")}`,
|
|
98
|
+
);
|
|
99
|
+
logger.log(` Max conn: ${maxConnections}`);
|
|
100
|
+
logger.log(` Timeout: ${timeout}ms`);
|
|
101
|
+
logger.log("");
|
|
102
|
+
logger.log(chalk.dim(" Press Ctrl+C to stop"));
|
|
103
|
+
logger.log("");
|
|
104
|
+
} catch (err) {
|
|
105
|
+
logger.error(`Failed to start server: ${err.message}`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SIEM commands
|
|
3
|
+
* chainlesschain siem targets|add-target|export|stats
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { logger } from "../lib/logger.js";
|
|
8
|
+
import { bootstrap, shutdown } from "../runtime/bootstrap.js";
|
|
9
|
+
import {
|
|
10
|
+
ensureSIEMTables,
|
|
11
|
+
listTargets,
|
|
12
|
+
addTarget,
|
|
13
|
+
exportLogs,
|
|
14
|
+
getSIEMStats,
|
|
15
|
+
} from "../lib/siem-exporter.js";
|
|
16
|
+
|
|
17
|
+
export function registerSiemCommand(program) {
|
|
18
|
+
const siem = program
|
|
19
|
+
.command("siem")
|
|
20
|
+
.description("SIEM integration — log export to external targets");
|
|
21
|
+
|
|
22
|
+
// siem targets
|
|
23
|
+
siem
|
|
24
|
+
.command("targets")
|
|
25
|
+
.description("List SIEM export targets")
|
|
26
|
+
.option("--json", "Output as JSON")
|
|
27
|
+
.action(async (options) => {
|
|
28
|
+
try {
|
|
29
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
30
|
+
if (!ctx.db) {
|
|
31
|
+
logger.error("Database not available");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const db = ctx.db.getDatabase();
|
|
35
|
+
ensureSIEMTables(db);
|
|
36
|
+
|
|
37
|
+
const targets = listTargets();
|
|
38
|
+
if (options.json) {
|
|
39
|
+
console.log(JSON.stringify(targets, null, 2));
|
|
40
|
+
} else if (targets.length === 0) {
|
|
41
|
+
logger.info("No SIEM targets configured.");
|
|
42
|
+
} else {
|
|
43
|
+
for (const t of targets) {
|
|
44
|
+
logger.log(
|
|
45
|
+
` ${chalk.cyan(t.id.slice(0, 8))} ${t.type} → ${t.url} [${t.format}] exported=${t.exportedCount}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await shutdown();
|
|
51
|
+
} catch (err) {
|
|
52
|
+
logger.error(`Failed: ${err.message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// siem add-target
|
|
58
|
+
siem
|
|
59
|
+
.command("add-target <type> <url>")
|
|
60
|
+
.description(
|
|
61
|
+
"Add a SIEM target (splunk_hec, elasticsearch, azure_sentinel)",
|
|
62
|
+
)
|
|
63
|
+
.option("-f, --format <fmt>", "Export format: json, cef, leef", "json")
|
|
64
|
+
.action(async (type, url, options) => {
|
|
65
|
+
try {
|
|
66
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
67
|
+
if (!ctx.db) {
|
|
68
|
+
logger.error("Database not available");
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const db = ctx.db.getDatabase();
|
|
72
|
+
ensureSIEMTables(db);
|
|
73
|
+
|
|
74
|
+
const target = addTarget(db, type, url, options.format);
|
|
75
|
+
logger.success("SIEM target added");
|
|
76
|
+
logger.log(` ${chalk.bold("ID:")} ${chalk.cyan(target.id)}`);
|
|
77
|
+
logger.log(` ${chalk.bold("Type:")} ${target.type}`);
|
|
78
|
+
logger.log(` ${chalk.bold("URL:")} ${target.url}`);
|
|
79
|
+
logger.log(` ${chalk.bold("Format:")} ${target.format}`);
|
|
80
|
+
|
|
81
|
+
await shutdown();
|
|
82
|
+
} catch (err) {
|
|
83
|
+
logger.error(`Failed: ${err.message}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// siem export
|
|
89
|
+
siem
|
|
90
|
+
.command("export <target-id>")
|
|
91
|
+
.description("Export logs to a SIEM target")
|
|
92
|
+
.action(async (targetId) => {
|
|
93
|
+
try {
|
|
94
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
95
|
+
if (!ctx.db) {
|
|
96
|
+
logger.error("Database not available");
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const db = ctx.db.getDatabase();
|
|
100
|
+
ensureSIEMTables(db);
|
|
101
|
+
|
|
102
|
+
// Export with simulated log batch
|
|
103
|
+
const result = exportLogs(db, targetId, [
|
|
104
|
+
{
|
|
105
|
+
id: `log-${Date.now()}`,
|
|
106
|
+
level: "info",
|
|
107
|
+
message: "CLI export test",
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
logger.success(`Exported ${result.exported} log(s)`);
|
|
111
|
+
logger.log(
|
|
112
|
+
` ${chalk.bold("Last ID:")} ${chalk.cyan(result.lastId || "N/A")}`,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
await shutdown();
|
|
116
|
+
} catch (err) {
|
|
117
|
+
logger.error(`Failed: ${err.message}`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// siem stats
|
|
123
|
+
siem
|
|
124
|
+
.command("stats")
|
|
125
|
+
.description("Show SIEM export statistics")
|
|
126
|
+
.option("--json", "Output as JSON")
|
|
127
|
+
.action(async (options) => {
|
|
128
|
+
try {
|
|
129
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
130
|
+
if (!ctx.db) {
|
|
131
|
+
logger.error("Database not available");
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
const db = ctx.db.getDatabase();
|
|
135
|
+
ensureSIEMTables(db);
|
|
136
|
+
|
|
137
|
+
const stats = getSIEMStats();
|
|
138
|
+
if (options.json) {
|
|
139
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
140
|
+
} else if (stats.length === 0) {
|
|
141
|
+
logger.info("No SIEM targets configured.");
|
|
142
|
+
} else {
|
|
143
|
+
for (const s of stats) {
|
|
144
|
+
logger.log(
|
|
145
|
+
` ${chalk.cyan(s.id.slice(0, 8))} ${s.type} [${s.format}] exported=${s.exportedCount} status=${s.status}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await shutdown();
|
|
151
|
+
} catch (err) {
|
|
152
|
+
logger.error(`Failed: ${err.message}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|