log-llm-config-staging 1.3.44

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.
Files changed (51) hide show
  1. package/README.md +46 -0
  2. package/dist/apply_deferred_vscdb.js +8 -0
  3. package/dist/bootstrap_constants.js +5 -0
  4. package/dist/cli/bash_script_generator.js +95 -0
  5. package/dist/cli.js +103 -0
  6. package/dist/cli_invocation_match.js +28 -0
  7. package/dist/compliance_check_runner.js +17 -0
  8. package/dist/compliance_prompt_gate.js +197 -0
  9. package/dist/endpoint_client/http_transport.js +88 -0
  10. package/dist/endpoint_client/index.js +3 -0
  11. package/dist/endpoint_client/registry_api.js +41 -0
  12. package/dist/endpoint_client/startup_api.js +43 -0
  13. package/dist/endpoint_client/types.js +4 -0
  14. package/dist/execute_trusted_restarts.js +54 -0
  15. package/dist/log_config_files/auth/auth_flow.js +22 -0
  16. package/dist/log_config_files/auth/auth_key_store.js +14 -0
  17. package/dist/log_config_files/collection/config_collector.js +160 -0
  18. package/dist/log_config_files/collection/directory_collector.js +96 -0
  19. package/dist/log_config_files/collection/enrichment_helpers.js +53 -0
  20. package/dist/log_config_files/collection/file_type_rules.js +47 -0
  21. package/dist/log_config_files/collection/mcp_tool_collector.js +37 -0
  22. package/dist/log_config_files/collection/openclaw_helpers.js +55 -0
  23. package/dist/log_config_files/collection/plugin_collector.js +89 -0
  24. package/dist/log_config_files/collection/plugin_version_helpers.js +37 -0
  25. package/dist/log_config_files/index.js +19 -0
  26. package/dist/log_config_files/paths/path_constants_helpers.js +71 -0
  27. package/dist/log_config_files/paths/pattern_resolver.js +227 -0
  28. package/dist/log_config_files/readers/file_readers.js +69 -0
  29. package/dist/log_config_files/readers/vscdb_config_builder.js +146 -0
  30. package/dist/log_config_files/readers/vscdb_reader.js +247 -0
  31. package/dist/log_config_files/runtime/compliance_check.js +518 -0
  32. package/dist/log_config_files/runtime/hardware_uuid.js +36 -0
  33. package/dist/log_config_files/runtime/hook_logger.js +197 -0
  34. package/dist/log_config_files/runtime/main_runner.js +192 -0
  35. package/dist/log_config_files/runtime/management_storage.js +82 -0
  36. package/dist/log_config_files/runtime/remediation_config_path.js +90 -0
  37. package/dist/log_config_files/runtime/remediation_sync.js +1290 -0
  38. package/dist/log_config_files/runtime/sqlite_binary.js +92 -0
  39. package/dist/log_config_files/runtime/trusted_restarts.js +52 -0
  40. package/dist/log_config_files/sender/batch_sender.js +220 -0
  41. package/dist/log_config_files/sender/endpoint_config.js +24 -0
  42. package/dist/log_config_files/sender/signing.js +1 -0
  43. package/dist/log_sensitive_paths_audit.js +97 -0
  44. package/dist/log_uuid/auth_key_store.js +71 -0
  45. package/dist/log_uuid/hardware_uuid.js +35 -0
  46. package/dist/log_uuid/index.js +11 -0
  47. package/dist/log_uuid/log_uuid_helper.js +30 -0
  48. package/dist/log_uuid/startup_sender.js +74 -0
  49. package/dist/log_uuid/user_profile.js +178 -0
  50. package/dist/types/config_file_types.js +1 -0
  51. package/package.json +62 -0
@@ -0,0 +1,92 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { join } from 'node:path';
4
+ let cached;
5
+ /**
6
+ * Resolve the sqlite3 CLI for remediations and vscdb reads.
7
+ *
8
+ * Cursor/IDE-launched hooks often inherit a minimal PATH (no Homebrew), while macOS still ships
9
+ * `/usr/bin/sqlite3`. Prefer explicit paths before `which` / bare `sqlite3`.
10
+ *
11
+ * Override: `OPTIMUS_SQLITE3`, `SQLITE3_PATH`, or `SQLITE3_BIN` = absolute path to the binary.
12
+ */
13
+ export function resolveSqlite3Binary() {
14
+ if (cached !== undefined)
15
+ return cached;
16
+ for (const envName of ['OPTIMUS_SQLITE3', 'SQLITE3_PATH', 'SQLITE3_BIN']) {
17
+ const v = process.env[envName]?.trim();
18
+ if (v && existsSync(v)) {
19
+ cached = v;
20
+ return cached;
21
+ }
22
+ }
23
+ const candidates = [];
24
+ if (process.platform === 'darwin') {
25
+ candidates.push('/usr/bin/sqlite3', '/opt/homebrew/bin/sqlite3', '/usr/local/bin/sqlite3', '/opt/local/bin/sqlite3');
26
+ }
27
+ else if (process.platform === 'linux') {
28
+ candidates.push('/usr/bin/sqlite3', '/usr/local/bin/sqlite3');
29
+ }
30
+ else if (process.platform === 'win32') {
31
+ const pf = process.env.ProgramFiles;
32
+ if (pf) {
33
+ candidates.push(join(pf, 'Git', 'usr', 'bin', 'sqlite3.exe'));
34
+ }
35
+ }
36
+ for (const p of candidates) {
37
+ if (p && existsSync(p)) {
38
+ cached = p;
39
+ return cached;
40
+ }
41
+ }
42
+ try {
43
+ if (process.platform === 'win32') {
44
+ const out = execFileSync('where.exe', ['sqlite3'], {
45
+ encoding: 'utf8',
46
+ stdio: ['ignore', 'pipe', 'ignore'],
47
+ }).trim();
48
+ const first = out.split(/\r?\n/).find((line) => {
49
+ const t = line.trim();
50
+ return t.length > 0 && existsSync(t);
51
+ });
52
+ if (first) {
53
+ cached = first.trim();
54
+ return cached;
55
+ }
56
+ }
57
+ else {
58
+ for (const whichBin of ['which', '/usr/bin/which']) {
59
+ try {
60
+ const out = execFileSync(whichBin, ['sqlite3'], {
61
+ encoding: 'utf8',
62
+ stdio: ['ignore', 'pipe', 'ignore'],
63
+ }).trim();
64
+ const first = out.split(/\n/)[0]?.trim();
65
+ if (first && existsSync(first)) {
66
+ cached = first;
67
+ return cached;
68
+ }
69
+ }
70
+ catch {
71
+ /* try next */
72
+ }
73
+ }
74
+ }
75
+ }
76
+ catch {
77
+ /* fall through */
78
+ }
79
+ try {
80
+ execFileSync('sqlite3', ['--version'], { stdio: 'ignore' });
81
+ cached = 'sqlite3';
82
+ return cached;
83
+ }
84
+ catch {
85
+ cached = null;
86
+ return null;
87
+ }
88
+ }
89
+ /** Test-only: reset memoized path after env/path changes. */
90
+ export function clearSqlite3BinaryCacheForTests() {
91
+ cached = undefined;
92
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Execute autofix restart_command strings only if allowlisted (same rules as gate enqueue).
3
+ * Used by the compliance shell hook via execute_trusted_restarts CLI — single place for allowlist + spawn.
4
+ */
5
+ import { spawn } from 'node:child_process';
6
+ import { appendComplianceRunnerLine, hookRunLog } from './hook_logger.js';
7
+ import { getDeferredVscdbRestartLogPath } from './management_storage.js';
8
+ import { isTrustedRestartCommandForAutofix } from './remediation_sync.js';
9
+ const FALLBACK_PATH = '/usr/bin:/bin:/usr/local/bin:/opt/homebrew/bin';
10
+ function isDeferredVscdbRestart(cmd) {
11
+ return cmd.includes('OPTIMUS_DEFERRED_LOG') || cmd.includes('apply_deferred_vscdb');
12
+ }
13
+ /** Spawn each trusted command detached (same pattern as former compliance_prompt_gate fireRestartCommands). */
14
+ export function executeTrustedRestartCommands(commands) {
15
+ for (const cmd of commands) {
16
+ const t = cmd.trim();
17
+ if (!t)
18
+ continue;
19
+ if (!isTrustedRestartCommandForAutofix(cmd)) {
20
+ const msg = `rejected_not_allowlisted prefix=${t.slice(0, 200)}`;
21
+ hookRunLog(`execute_trusted_restarts: ${msg}`);
22
+ appendComplianceRunnerLine('RESTART', msg);
23
+ continue;
24
+ }
25
+ const deferred = isDeferredVscdbRestart(t);
26
+ const detailPath = getDeferredVscdbRestartLogPath();
27
+ if (deferred) {
28
+ hookRunLog(`execute_trusted_restarts: spawning deferred state.vscdb restart (killall Cursor, then apply + reopen). Step-by-step log → ${detailPath}`);
29
+ appendComplianceRunnerLine('RESTART', `spawn deferred_vscdb_restart sh -c (detail_log=${detailPath}) cwd_inherited_from_node`);
30
+ }
31
+ else {
32
+ hookRunLog('execute_trusted_restarts: spawning trusted restart (see compliance_runner.log [RESTART])');
33
+ appendComplianceRunnerLine('RESTART', 'spawn trusted_restart sh -c (non-deferred)');
34
+ }
35
+ const child = spawn('sh', ['-c', t], {
36
+ detached: true,
37
+ stdio: 'ignore',
38
+ env: {
39
+ ...process.env,
40
+ PATH: process.env.PATH && String(process.env.PATH).trim().length > 0
41
+ ? process.env.PATH
42
+ : FALLBACK_PATH,
43
+ },
44
+ });
45
+ child.on('error', (err) => {
46
+ const emsg = err instanceof Error ? err.message : String(err);
47
+ hookRunLog(`execute_trusted_restarts: spawn failed ${emsg}`);
48
+ appendComplianceRunnerLine('RESTART', `spawn_error ${emsg}`);
49
+ });
50
+ child.unref();
51
+ }
52
+ }
@@ -0,0 +1,220 @@
1
+ import { postStartupPayload, patchPayload } from '../../endpoint_client/index.js';
2
+ import { createSignature } from './signing.js';
3
+ import { loadEndpointBase } from './endpoint_config.js';
4
+ import { hookRunLog } from '../runtime/hook_logger.js';
5
+ import { canonicalCursorUserStateVscdbPath } from '../runtime/remediation_config_path.js';
6
+ /** Chunk size per batch request. */
7
+ export const BATCH_CHUNK_SIZE = 20;
8
+ const MAX_BATCH_SIZE_BYTES = 500 * 1024; // 500KB
9
+ function resolveApiBase(endpoint) {
10
+ try {
11
+ return new URL(endpoint).origin;
12
+ }
13
+ catch {
14
+ return endpoint.replace(/\/+$/, '');
15
+ }
16
+ }
17
+ function estimateConfigFileSize(configFile) {
18
+ if (configFile.collect_style === 'metadata')
19
+ return 300;
20
+ const contentStr = typeof configFile.raw_content === 'string'
21
+ ? configFile.raw_content
22
+ : JSON.stringify(configFile.raw_content || {});
23
+ return (configFile.file_type?.length || 0) + (configFile.file_path?.length || 0) + contentStr.length + 80 + Math.ceil(contentStr.length * 0.3);
24
+ }
25
+ function buildBatchChunks(configFiles, basePayloadSize) {
26
+ const chunks = [];
27
+ let currentChunk = [];
28
+ let currentChunkSize = basePayloadSize;
29
+ for (const configFile of configFiles) {
30
+ const fileSize = estimateConfigFileSize(configFile);
31
+ if ((currentChunkSize + fileSize > MAX_BATCH_SIZE_BYTES || currentChunk.length >= BATCH_CHUNK_SIZE) && currentChunk.length > 0) {
32
+ chunks.push(currentChunk);
33
+ currentChunk = [];
34
+ currentChunkSize = basePayloadSize;
35
+ }
36
+ if (fileSize > MAX_BATCH_SIZE_BYTES) {
37
+ const note = configFile.collect_style === 'metadata' ? ' (metadata only)' : '';
38
+ hookRunLog(`warning: file ${configFile.file_path} is very large (${Math.round(fileSize / 1024)}KB)${note}`);
39
+ if (currentChunk.length > 0) {
40
+ chunks.push(currentChunk);
41
+ currentChunk = [];
42
+ currentChunkSize = basePayloadSize;
43
+ }
44
+ chunks.push([configFile]);
45
+ continue;
46
+ }
47
+ currentChunk.push(configFile);
48
+ currentChunkSize += fileSize;
49
+ }
50
+ if (currentChunk.length > 0)
51
+ chunks.push(currentChunk);
52
+ return chunks;
53
+ }
54
+ function buildChunkBody(chunk, hardwareUuid, authKey, hookRequestId, metadata) {
55
+ const config_files = chunk.map((c) => ({
56
+ file_type: c.file_type,
57
+ file_path: canonicalCursorUserStateVscdbPath(c.file_path),
58
+ raw_content: c.raw_content,
59
+ }));
60
+ const payload = { hardware_uuid: hardwareUuid, metadata, config_files };
61
+ if (hookRequestId != null)
62
+ payload.hook_request_id = hookRequestId;
63
+ const signature = createSignature(payload, authKey.key);
64
+ return { ...payload, signature, key_id: authKey.key_id || '' };
65
+ }
66
+ function splitChunk(chunks, chunkIndex) {
67
+ const chunk = chunks[chunkIndex];
68
+ const half = Math.ceil(chunk.length / 2);
69
+ chunks[chunkIndex] = chunk.slice(0, half);
70
+ chunks.splice(chunkIndex + 1, 0, chunk.slice(half));
71
+ }
72
+ async function sendConfigFilesBatch(configFiles, hardwareUuid, authKey, hookRequestId, ingestSessionId) {
73
+ const endpoint = loadEndpointBase();
74
+ const apiUrl = `${resolveApiBase(endpoint)}/endpoint_security/log-config-files/`;
75
+ const metadata = { org_identifier: process.env.GITHUB_ORG || process.env.GH_ORG || '', repo_identifier: process.env.GITHUB_REPOSITORY || process.env.GH_REPOSITORY || '' };
76
+ const basePayloadSize = JSON.stringify({ hardware_uuid: hardwareUuid, metadata }).length + 800;
77
+ const chunks = buildBatchChunks(configFiles, basePayloadSize);
78
+ const totals = { accepted: 0, failed: 0 };
79
+ let chunkIndex = 0;
80
+ while (chunkIndex < chunks.length) {
81
+ let chunk = chunks[chunkIndex];
82
+ let body = buildChunkBody(chunk, hardwareUuid, authKey, hookRequestId, metadata);
83
+ if (ingestSessionId) {
84
+ const payload = { hardware_uuid: hardwareUuid, metadata, config_files: body.config_files, ingest_session_id: ingestSessionId, ...(hookRequestId != null ? { hook_request_id: hookRequestId } : {}) };
85
+ const signature = createSignature(payload, authKey.key);
86
+ body = { ...payload, signature, key_id: authKey.key_id || '' };
87
+ }
88
+ const bodySize = new TextEncoder().encode(JSON.stringify(body)).length;
89
+ // Pre-send size check: split if too large
90
+ if (bodySize > MAX_BATCH_SIZE_BYTES && chunk.length > 1) {
91
+ hookRunLog(`batch chunk too large (${Math.round(bodySize / 1024)}KB), splitting ${chunk.length} files`);
92
+ splitChunk(chunks, chunkIndex);
93
+ // Don't increment — retry with the smaller first half
94
+ continue;
95
+ }
96
+ const fileInfo = chunk.length === 1 ? `: ${chunk[0].file_path}` : chunk.length <= 3 ? `: ${chunk.map((f) => f.file_path.split('/').pop()).join(', ')}` : '';
97
+ hookRunLog(`batch send ${chunkIndex + 1}/${chunks.length}: ${chunk.length} file(s) (~${Math.round(bodySize / 1024)}KB)${fileInfo}`);
98
+ const timeoutMs = Math.min(20000 + Math.ceil(bodySize / (1024 * 1024)) * 15000, 90000);
99
+ try {
100
+ const response = (await postStartupPayload(apiUrl, body, timeoutMs));
101
+ if (response.status === 'accepted') {
102
+ totals.accepted += typeof response.accepted === 'number' ? response.accepted : chunk.length;
103
+ const failedList = Array.isArray(response.failed) ? response.failed : [];
104
+ totals.failed += failedList.length;
105
+ if (failedList.length > 0)
106
+ hookRunLog(`batch chunk failed items: ${JSON.stringify(failedList.slice(0, 3))}`);
107
+ }
108
+ else {
109
+ totals.failed += chunk.length;
110
+ hookRunLog(`batch chunk failed: ${response.error || response.message || response.status}`);
111
+ }
112
+ }
113
+ catch (error) {
114
+ const errorMessage = error instanceof Error ? error.message : String(error);
115
+ hookRunLog(`batch chunk error: ${errorMessage}`);
116
+ totals.failed += chunk.length;
117
+ }
118
+ chunkIndex++;
119
+ }
120
+ return totals;
121
+ }
122
+ async function sendIngestSessionStart(hardwareUuid, authKey) {
123
+ const endpoint = loadEndpointBase();
124
+ const apiUrl = `${resolveApiBase(endpoint)}/endpoint_security/ingest-session/start/`;
125
+ const payload = { hardware_uuid: hardwareUuid, action: 'ingest_session_start' };
126
+ const signature = createSignature(payload, authKey.key);
127
+ const body = { ...payload, signature, key_id: authKey.key_id || '' };
128
+ try {
129
+ const response = (await postStartupPayload(apiUrl, body));
130
+ if (response.status === 'accepted' && typeof response.ingest_session_id === 'string')
131
+ return response.ingest_session_id;
132
+ hookRunLog(`ingest-session start failed: ${response.error || response.status || 'unknown'}`);
133
+ return null;
134
+ }
135
+ catch (error) {
136
+ hookRunLog(`ingest-session start error: ${error instanceof Error ? error.message : String(error)}`);
137
+ return null;
138
+ }
139
+ }
140
+ async function sendIngestSessionFinish(hardwareUuid, authKey, ingestSessionId) {
141
+ const endpoint = loadEndpointBase();
142
+ const apiUrl = `${resolveApiBase(endpoint)}/endpoint_security/ingest-session/finish/`;
143
+ const payload = { hardware_uuid: hardwareUuid, action: 'ingest_session_finish', ingest_session_id: ingestSessionId };
144
+ const signature = createSignature(payload, authKey.key);
145
+ const body = { ...payload, signature, key_id: authKey.key_id || '' };
146
+ try {
147
+ const response = (await postStartupPayload(apiUrl, body, 120000));
148
+ if (response.status === 'accepted')
149
+ return true;
150
+ hookRunLog(`ingest-session finish failed: ${response.error || response.status || 'unknown'}`);
151
+ return false;
152
+ }
153
+ catch (error) {
154
+ hookRunLog(`ingest-session finish error: ${error instanceof Error ? error.message : String(error)}`);
155
+ return false;
156
+ }
157
+ }
158
+ async function sendConfigFile(configFile, hardwareUuid, authKey) {
159
+ const endpoint = loadEndpointBase();
160
+ const apiUrl = `${resolveApiBase(endpoint)}/endpoint_security/log-config-file/`;
161
+ const uploadPath = canonicalCursorUserStateVscdbPath(configFile.file_path);
162
+ const payload = { hardware_uuid: hardwareUuid, file_type: configFile.file_type, file_path: uploadPath, raw_content: configFile.raw_content };
163
+ const signature = createSignature(payload, authKey.key);
164
+ const body = { ...payload, signature, key_id: authKey.key_id || '', metadata: { org_identifier: process.env.GITHUB_ORG || process.env.GH_ORG || '', repo_identifier: process.env.GITHUB_REPOSITORY || process.env.GH_REPOSITORY || '' } };
165
+ try {
166
+ const response = await postStartupPayload(apiUrl, body);
167
+ if (response.status !== 'accepted') {
168
+ hookRunLog(`sendConfigFile: rejected status=${response.status} error=${response.error ?? ''} path=${uploadPath}`);
169
+ }
170
+ return response.status === 'accepted';
171
+ }
172
+ catch (err) {
173
+ hookRunLog(`sendConfigFile: request failed path=${uploadPath} err=${err instanceof Error ? err.message : String(err)}`);
174
+ return false;
175
+ }
176
+ }
177
+ async function sendHookRequestCreate(hardwareUuid, authKey, hookType, workspaceRepo) {
178
+ const endpoint = loadEndpointBase();
179
+ const apiUrl = `${resolveApiBase(endpoint)}/endpoint_security/hook-request/`;
180
+ const workspace_repo = String(workspaceRepo || '').trim().slice(0, 512);
181
+ const payload = { hardware_uuid: hardwareUuid, hook_type: hookType, workspace_repo, manifest: [] };
182
+ const signature = createSignature(payload, authKey.key);
183
+ const body = { ...payload, signature, key_id: authKey.key_id || '' };
184
+ try {
185
+ hookRunLog(`hook-request create (step 1) posting to ${apiUrl}`);
186
+ const response = (await postStartupPayload(apiUrl, body));
187
+ if (response.status === 'accepted' && typeof response.id === 'number') {
188
+ hookRunLog(`hook-request created id=${response.id}`);
189
+ return response.id;
190
+ }
191
+ hookRunLog(`hook-request create failed: ${response.error || response.status}`);
192
+ return null;
193
+ }
194
+ catch (error) {
195
+ hookRunLog(`hook-request create error: ${error instanceof Error ? error.message : String(error)}`);
196
+ return null;
197
+ }
198
+ }
199
+ async function sendHookRequestUpdateManifest(hardwareUuid, authKey, hookRequestId, manifest) {
200
+ const endpoint = loadEndpointBase();
201
+ const apiUrl = `${resolveApiBase(endpoint)}/endpoint_security/hook-request/${hookRequestId}/`;
202
+ const manifestNormalized = manifest.slice(0, 1000).map((x) => (x != null && typeof x === 'string' ? x.trim() : String(x)).slice(0, 2048));
203
+ const payload = { hardware_uuid: hardwareUuid, hook_request_id: hookRequestId, manifest: manifestNormalized };
204
+ const signature = createSignature(payload, authKey.key);
205
+ const body = { ...payload, signature, key_id: authKey.key_id || '' };
206
+ try {
207
+ const data = (await patchPayload(apiUrl, body));
208
+ if (data.status === 'accepted') {
209
+ hookRunLog(`hook-request manifest updated (${manifestNormalized.length} paths)`);
210
+ return true;
211
+ }
212
+ hookRunLog(`hook-request update failed: ${data.error ?? data.status ?? 'unknown'}`);
213
+ return false;
214
+ }
215
+ catch (error) {
216
+ hookRunLog(`hook-request update error: ${error instanceof Error ? error.message : String(error)}`);
217
+ return false;
218
+ }
219
+ }
220
+ export { sendConfigFilesBatch, sendConfigFile, sendHookRequestCreate, sendHookRequestUpdateManifest, sendIngestSessionStart, sendIngestSessionFinish, estimateConfigFileSize };
@@ -0,0 +1,24 @@
1
+ import { getEndpointSource as getSharedEndpointSource, loadEndpointBase as loadSharedEndpointBase, } from "optimus-tofu-staging";
2
+ /**
3
+ * log-llm-config historically only honored OPTIMUS_ENDPOINT (plus file / default).
4
+ * OPTIMUS_API_URL is reserved for optimus-tool-call; omit it here for backward compatibility.
5
+ */
6
+ function envWithoutOptimusApiUrl(env) {
7
+ const copy = { ...env };
8
+ delete copy.OPTIMUS_API_URL;
9
+ return copy;
10
+ }
11
+ export function loadEndpointBase(options) {
12
+ const env = envWithoutOptimusApiUrl(options?.env ?? process.env);
13
+ return loadSharedEndpointBase({ cwd: options?.cwd, env });
14
+ }
15
+ /** Where endpoint came from (for logging). Matches legacy semantics (never driven by OPTIMUS_API_URL alone). */
16
+ export function getEndpointSource(options) {
17
+ const env = envWithoutOptimusApiUrl(options?.env ?? process.env);
18
+ const source = getSharedEndpointSource({ cwd: options?.cwd, env });
19
+ if (source === "file")
20
+ return "file";
21
+ if (source === "default")
22
+ return "default";
23
+ return "env";
24
+ }
@@ -0,0 +1 @@
1
+ export { canonicalizePayload, createSignature } from 'optimus-tofu-staging';
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Sensitive paths audit: writes sensitive_paths_audit.txt listing which sensitive
4
+ * paths exist on this machine. NEVER reads or sends sensitive file contents—existence only.
5
+ *
6
+ * Path templates are fetched from the backend (GET api/file-path-registry/sensitive-paths-audit-candidates/).
7
+ * The audit is sent with the rest of the config files in log_config_files (same auth, same batch).
8
+ */
9
+ import { existsSync, writeFileSync, mkdirSync, readFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { homedir } from 'node:os';
12
+ import { getSensitivePathsAuditCandidates } from './endpoint_client/index.js';
13
+ import { OPT_AI_SEC_MANAGEMENT_REL } from './bootstrap_constants.js';
14
+ const AUDIT_FILENAME = 'sensitive_paths_audit.txt';
15
+ function loadEndpointBase() {
16
+ if (process.env.OPTIMUS_ENDPOINT) {
17
+ return process.env.OPTIMUS_ENDPOINT.replace(/\/+$/, '') + '/';
18
+ }
19
+ const defaultEndpoint = 'https://demo.optimuslabs.io/';
20
+ const envPath = join(process.cwd(), 'optimus_dev.env');
21
+ try {
22
+ if (!existsSync(envPath))
23
+ return defaultEndpoint;
24
+ const content = readFileSync(envPath, 'utf8');
25
+ for (const line of content.split(/\r?\n/)) {
26
+ const trimmed = line.trim();
27
+ if (!trimmed || trimmed.startsWith('#'))
28
+ continue;
29
+ const [key, ...rest] = trimmed.split('=');
30
+ if (key === 'OPTIMUS_ENDPOINT') {
31
+ const value = rest.join('=').trim();
32
+ if (value)
33
+ return value.replace(/\/+$/, '') + '/';
34
+ break;
35
+ }
36
+ }
37
+ }
38
+ catch {
39
+ // ignore
40
+ }
41
+ return defaultEndpoint;
42
+ }
43
+ function expandPath(template, cwd) {
44
+ if (template.startsWith('~/')) {
45
+ return join(homedir(), template.slice(2));
46
+ }
47
+ if (template.startsWith('/')) {
48
+ return template;
49
+ }
50
+ return join(cwd, template);
51
+ }
52
+ /**
53
+ * Write sensitive_paths_audit.txt under outputDir. pathTemplates from backend (~ = home).
54
+ * Lists one path per line for paths that exist. No file contents are read.
55
+ * Overwrites the file on each run (same as hook_log.txt); does not append.
56
+ */
57
+ export function writeSensitivePathsAudit(outputDir, pathTemplates, cwd = process.cwd()) {
58
+ const existing = [];
59
+ for (const template of pathTemplates) {
60
+ const resolved = expandPath(template, cwd);
61
+ if (existsSync(resolved)) {
62
+ existing.push(resolved);
63
+ }
64
+ }
65
+ mkdirSync(outputDir, { recursive: true });
66
+ const outPath = join(outputDir, AUDIT_FILENAME);
67
+ writeFileSync(outPath, existing.join('\n') + (existing.length ? '\n' : ''), 'utf8');
68
+ return existing;
69
+ }
70
+ /**
71
+ * Run audit: fetch candidates from backend, check existence, write file. Returns paths for sending.
72
+ * Used by log_config_files to include the audit in the same batch (same auth).
73
+ */
74
+ export async function runSensitivePathsAudit(endpointBase, outputDir = join(homedir(), OPT_AI_SEC_MANAGEMENT_REL), cwd = process.cwd()) {
75
+ const response = await getSensitivePathsAuditCandidates(endpointBase);
76
+ const pathTemplates = response?.paths ?? [];
77
+ if (pathTemplates.length === 0 && !response) {
78
+ console.warn('Could not fetch sensitive path candidates from backend; writing empty audit.');
79
+ }
80
+ const paths = writeSensitivePathsAudit(outputDir, pathTemplates, cwd);
81
+ const outPath = join(outputDir, AUDIT_FILENAME);
82
+ console.log(`Sensitive paths audit written to ${outPath} (${paths.length} path(s) present).`);
83
+ return { paths };
84
+ }
85
+ export async function main() {
86
+ const outputDir = join(homedir(), OPT_AI_SEC_MANAGEMENT_REL);
87
+ const endpointBase = loadEndpointBase();
88
+ await runSensitivePathsAudit(endpointBase, outputDir, process.cwd());
89
+ process.exit(0);
90
+ }
91
+ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.includes('log_sensitive_paths_audit')) {
92
+ main().catch((err) => {
93
+ console.error('Sensitive paths audit failed:', err);
94
+ process.exit(1);
95
+ });
96
+ }
97
+ export { AUDIT_FILENAME };
@@ -0,0 +1,71 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { OPT_AI_SEC_MANAGEMENT_REL } from '../bootstrap_constants.js';
4
+ const AUTH_KEY_RELATIVE_PATH = path.join(OPT_AI_SEC_MANAGEMENT_REL, 'auth_key.txt');
5
+ const OPTIMUS_ENV_FILENAME = 'optimus_dev.env';
6
+ const STARTUP_ENDPOINT_SUFFIX = '/endpoint_security/startup/';
7
+ const getAuthKeyPath = () => {
8
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
9
+ if (!homeDir)
10
+ return null;
11
+ return path.join(homeDir, AUTH_KEY_RELATIVE_PATH);
12
+ };
13
+ const writeAuthKey = (key, keyId, hardwareUuid) => {
14
+ const authPath = getAuthKeyPath();
15
+ if (!authPath) {
16
+ console.warn('Unable to determine home directory; cannot store auth key.');
17
+ return;
18
+ }
19
+ mkdirSync(path.dirname(authPath), { recursive: true, mode: 0o700 });
20
+ writeFileSync(authPath, JSON.stringify({ key, key_id: keyId ?? null, hardware_uuid: hardwareUuid, stored_at: new Date().toISOString() }, null, 2), { encoding: 'utf8', mode: 0o600 });
21
+ console.log(`Stored auth key at ${authPath}`);
22
+ };
23
+ const readStoredAuthKey = () => {
24
+ const authPath = getAuthKeyPath();
25
+ if (!authPath || !existsSync(authPath))
26
+ return null;
27
+ try {
28
+ const parsed = JSON.parse(readFileSync(authPath, 'utf8'));
29
+ if (parsed?.key && parsed?.hardware_uuid)
30
+ return parsed;
31
+ }
32
+ catch (error) {
33
+ console.warn(`Unable to read stored auth key: ${error.message}`);
34
+ }
35
+ return null;
36
+ };
37
+ const loadEndpointBase = () => {
38
+ const DEFAULT_ENDPOINT = 'https://demo.optimuslabs.io/';
39
+ const fromEnv = process.env.OPTIMUS_ENDPOINT?.trim();
40
+ if (fromEnv)
41
+ return fromEnv;
42
+ const envPath = path.join(process.cwd(), OPTIMUS_ENV_FILENAME);
43
+ try {
44
+ if (!existsSync(envPath))
45
+ return DEFAULT_ENDPOINT;
46
+ const envContent = readFileSync(envPath, 'utf8');
47
+ for (const line of envContent.split(/\r?\n/)) {
48
+ const trimmed = line.trim();
49
+ if (!trimmed || trimmed.startsWith('#'))
50
+ continue;
51
+ const [key, ...rest] = trimmed.split('=');
52
+ if (key === 'OPTIMUS_ENDPOINT') {
53
+ const value = rest.join('=').trim();
54
+ if (value)
55
+ return value;
56
+ }
57
+ }
58
+ }
59
+ catch { /* fall back to default */ }
60
+ return DEFAULT_ENDPOINT;
61
+ };
62
+ const normalizeBaseUrl = (rawUrl) => {
63
+ let base = rawUrl.trim();
64
+ if (!base)
65
+ throw new Error('OPTIMUS_ENDPOINT is empty');
66
+ base = base.replace(/\/+$/, '');
67
+ base = base.replace(/\/(optimus_security|endpoint_security)$/i, '');
68
+ return base;
69
+ };
70
+ const buildStartupEndpointUrl = (baseUrl) => `${normalizeBaseUrl(baseUrl)}${STARTUP_ENDPOINT_SUFFIX}`;
71
+ export { getAuthKeyPath, writeAuthKey, readStoredAuthKey, loadEndpointBase, normalizeBaseUrl, buildStartupEndpointUrl };
@@ -0,0 +1,35 @@
1
+ import { execSync } from 'node:child_process';
2
+ const readCommandOutput = (command) => {
3
+ try {
4
+ // execSync is used here with static strings only (no user input) - safe from injection
5
+ return execSync(command, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
6
+ }
7
+ catch {
8
+ return '';
9
+ }
10
+ };
11
+ const readHardwareUuidFromCommands = () => {
12
+ const ioregOutput = readCommandOutput('ioreg -rd1 -c IOPlatformExpertDevice');
13
+ const ioregMatch = ioregOutput.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/i);
14
+ if (ioregMatch?.[1])
15
+ return ioregMatch[1];
16
+ const profilerOutput = readCommandOutput('system_profiler SPHardwareDataType');
17
+ const profilerMatch = profilerOutput.match(/Hardware UUID:\s*([A-F0-9-]+)/i);
18
+ if (profilerMatch?.[1])
19
+ return profilerMatch[1];
20
+ throw new Error('Unable to determine hardware UUID via ioreg or system_profiler.');
21
+ };
22
+ const normalizeHardwareUuid = (raw) => {
23
+ const trimmed = raw.trim();
24
+ const match = trimmed.match(/[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}/);
25
+ if (match)
26
+ return match[0];
27
+ return trimmed.replace(/^"+|"+$/g, '');
28
+ };
29
+ const resolveHardwareUuid = () => {
30
+ const envUuid = process.env.OPTIMUS_HARDWARE_UUID?.trim();
31
+ if (envUuid)
32
+ return normalizeHardwareUuid(envUuid);
33
+ return normalizeHardwareUuid(readHardwareUuidFromCommands());
34
+ };
35
+ export { readCommandOutput, readHardwareUuidFromCommands, normalizeHardwareUuid, resolveHardwareUuid };
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ // Barrel — all public exports from sub-modules under ./log_uuid/.
3
+ export { resolveHardwareUuid } from './hardware_uuid.js';
4
+ export { getAuthKeyPath, writeAuthKey, readStoredAuthKey, loadEndpointBase, normalizeBaseUrl } from './auth_key_store.js';
5
+ export { getMetadata, createSignature, maybeSendToEndpoint, main } from './startup_sender.js';
6
+ export { buildLogUuidScriptLines, renderLogUuidScript } from './log_uuid_helper.js';
7
+ // Entry point guard (when invoked directly via npx or node)
8
+ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.includes('log_uuid')) {
9
+ const { main } = await import('./startup_sender.js');
10
+ void main();
11
+ }
@@ -0,0 +1,30 @@
1
+ export const buildLogUuidScriptLines = () => [
2
+ '#!/usr/bin/env bash',
3
+ 'set -euo pipefail',
4
+ '',
5
+ 'output_file="$(pwd)/uuid.txt"',
6
+ 'timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")',
7
+ 'if command -v ioreg >/dev/null 2>&1; then',
8
+ ' uuid_value=$(ioreg -rd1 -c IOPlatformExpertDevice | awk \'/IOPlatformUUID/ {gsub(/\\"/, "", $3); print $3}\')',
9
+ 'elif command -v system_profiler >/dev/null 2>&1; then',
10
+ ' uuid_value=$(system_profiler SPHardwareDataType | awk \'/Hardware UUID/ {print $3}\')',
11
+ 'else',
12
+ ' echo "Unable to locate hardware UUID utilities (ioreg/system_profiler)" >&2',
13
+ ' exit 1',
14
+ 'fi',
15
+ '',
16
+ 'if [ -z "$uuid_value" ]; then',
17
+ ' echo "Hardware UUID could not be determined" >&2',
18
+ ' exit 1',
19
+ 'fi',
20
+ '',
21
+ 'cat <<EOF | tee "$output_file"',
22
+ '===== Optimus UUID =====',
23
+ 'Timestamp: $timestamp',
24
+ 'Directory: $(pwd)',
25
+ 'UUID: $uuid_value',
26
+ 'EOF',
27
+ '',
28
+ 'echo "UUID details saved to $output_file"'
29
+ ];
30
+ export const renderLogUuidScript = () => buildLogUuidScriptLines().join('\n');