log-llm-config 1.2.7 → 1.3.0
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/dist/apply_deferred_vscdb.js +8 -0
- package/dist/cli/bash_script_generator.js +23 -11
- package/dist/compliance_check_runner.js +31 -0
- package/dist/compliance_prompt_gate.js +35 -4
- package/dist/log_config_files/readers/vscdb_config_builder.js +6 -2
- package/dist/log_config_files/readers/vscdb_reader.js +102 -1
- package/dist/log_config_files/runtime/compliance_check.js +103 -45
- package/dist/log_config_files/runtime/hook_logger.js +38 -5
- package/dist/log_config_files/runtime/main_runner.js +2 -0
- package/dist/log_config_files/runtime/management_storage.js +31 -0
- package/dist/log_config_files/runtime/remediation_sync.js +680 -17
- package/package.json +4 -3
|
@@ -1,16 +1,37 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
3
4
|
import { executeGet, executeBody } from '../../endpoint_client/http_transport.js';
|
|
4
|
-
import { hookRunLog } from './hook_logger.js';
|
|
5
|
-
import { getRemediationInstructionsPath, readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
|
|
5
|
+
import { complianceRunnerDiag, hookRunLog } from './hook_logger.js';
|
|
6
|
+
import { atomicWriteJson, getDeferredVscdbApplyPath, getFileCollectionVscdbContractPath, getRemediationInstructionsPath, readFileCollectionVscdbContract, readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
|
|
6
7
|
import { readStoredAuthKey } from '../auth/auth_key_store.js';
|
|
7
8
|
import { createSignature } from '../sender/signing.js';
|
|
8
9
|
import { loadEndpointBase } from '../sender/endpoint_config.js';
|
|
9
10
|
import { resolveHardwareUuid } from './hardware_uuid.js';
|
|
11
|
+
import { persistVscdbComposerContractFromPatternsResponse, readVscdbItemTableJson, } from '../readers/vscdb_reader.js';
|
|
12
|
+
import { sendConfigFile } from '../sender/batch_sender.js';
|
|
13
|
+
import { getFileCollectionPatterns } from '../../endpoint_client/registry_api.js';
|
|
14
|
+
function reactiveStorageItemKeyFromContract() {
|
|
15
|
+
const k = readFileCollectionVscdbContract()?.reactive_storage_item_key;
|
|
16
|
+
return typeof k === 'string' && k.trim() !== '' ? k.trim() : undefined;
|
|
17
|
+
}
|
|
18
|
+
function composerShadowKeySetFromContract() {
|
|
19
|
+
return new Set(readFileCollectionVscdbContract()?.composer_shadow_keys ?? []);
|
|
20
|
+
}
|
|
10
21
|
/** Resolve fix payload from API or legacy `compliance` key in local JSON. */
|
|
11
22
|
export function remediationFixSpec(inst) {
|
|
12
23
|
return inst.fix ?? inst.compliance ?? null;
|
|
13
24
|
}
|
|
25
|
+
/** True when this row should be replaced from GET manifest (missing or empty fix checks). */
|
|
26
|
+
function needsManifestRefetch(inst) {
|
|
27
|
+
if (!inst)
|
|
28
|
+
return true;
|
|
29
|
+
const spec = remediationFixSpec(inst);
|
|
30
|
+
if (!spec)
|
|
31
|
+
return true;
|
|
32
|
+
const checks = spec.checks;
|
|
33
|
+
return !Array.isArray(checks) || checks.length === 0;
|
|
34
|
+
}
|
|
14
35
|
// ---------------------------------------------------------------------------
|
|
15
36
|
// Persistence (single file: remediation_instructions.json)
|
|
16
37
|
// ---------------------------------------------------------------------------
|
|
@@ -34,7 +55,8 @@ async function ensureInstructionsForUuids(endpointBase, machineUuid, want, overl
|
|
|
34
55
|
if (want.has(inst.uuid))
|
|
35
56
|
byUuid.set(inst.uuid, inst);
|
|
36
57
|
}
|
|
37
|
-
|
|
58
|
+
// Refetch rows that are missing entirely or have no usable checks (stale/incomplete local cache).
|
|
59
|
+
const missing = [...want].filter((u) => needsManifestRefetch(byUuid.get(u)));
|
|
38
60
|
if (missing.length > 0) {
|
|
39
61
|
try {
|
|
40
62
|
const fetched = await fetchManifest(endpointBase, machineUuid, missing);
|
|
@@ -85,24 +107,36 @@ export async function fetchSync(endpointBase, machineUuid, activeUuids, timeoutM
|
|
|
85
107
|
const uuidsParam = activeUuids.join(',');
|
|
86
108
|
const url = `${resolveOrigin(endpointBase)}/api/findings/remediations/sync/?machine_uuid=${encodeURIComponent(machineUuid)}&active_uuids=${encodeURIComponent(uuidsParam)}`;
|
|
87
109
|
const { statusCode, body } = await executeGet(url, timeoutMs);
|
|
88
|
-
if (statusCode !== 200 || !body)
|
|
110
|
+
if (statusCode !== 200 || !body) {
|
|
111
|
+
const line = `remediation_sync_get: url=${url} status=${statusCode} bytes=${body?.length ?? 0}`;
|
|
112
|
+
hookRunLog(line);
|
|
113
|
+
complianceRunnerDiag(line);
|
|
89
114
|
return null;
|
|
115
|
+
}
|
|
90
116
|
try {
|
|
91
117
|
return JSON.parse(body);
|
|
92
118
|
}
|
|
93
119
|
catch {
|
|
120
|
+
hookRunLog('remediation_sync_get: invalid JSON body');
|
|
121
|
+
complianceRunnerDiag('remediation_sync_get: invalid JSON body');
|
|
94
122
|
return null;
|
|
95
123
|
}
|
|
96
124
|
}
|
|
97
125
|
async function fetchManifest(endpointBase, machineUuid, uuids, timeoutMs = 8000) {
|
|
98
126
|
const url = `${resolveOrigin(endpointBase)}/api/findings/remediations/manifest/?machine_uuid=${encodeURIComponent(machineUuid)}&uuids=${encodeURIComponent(uuids.join(','))}`;
|
|
99
127
|
const { statusCode, body } = await executeGet(url, timeoutMs);
|
|
100
|
-
if (statusCode !== 200 || !body)
|
|
128
|
+
if (statusCode !== 200 || !body) {
|
|
129
|
+
const line = `remediation_manifest_get: url=${url} status=${statusCode} bytes=${body?.length ?? 0}`;
|
|
130
|
+
hookRunLog(line);
|
|
131
|
+
complianceRunnerDiag(line);
|
|
101
132
|
return null;
|
|
133
|
+
}
|
|
102
134
|
try {
|
|
103
135
|
const parsed = JSON.parse(body);
|
|
104
|
-
if (!Array.isArray(parsed.remediations))
|
|
136
|
+
if (!Array.isArray(parsed.remediations)) {
|
|
137
|
+
complianceRunnerDiag('remediation_manifest_get: JSON remediations is not an array');
|
|
105
138
|
return null;
|
|
139
|
+
}
|
|
106
140
|
return parsed.remediations.map((raw) => {
|
|
107
141
|
const { compliance, ...rest } = raw;
|
|
108
142
|
const fix = rest.fix ?? compliance ?? null;
|
|
@@ -110,6 +144,7 @@ async function fetchManifest(endpointBase, machineUuid, uuids, timeoutMs = 8000)
|
|
|
110
144
|
});
|
|
111
145
|
}
|
|
112
146
|
catch {
|
|
147
|
+
complianceRunnerDiag('remediation_manifest_get: invalid JSON body');
|
|
113
148
|
return null;
|
|
114
149
|
}
|
|
115
150
|
}
|
|
@@ -139,6 +174,19 @@ function getByPath(obj, path) {
|
|
|
139
174
|
}
|
|
140
175
|
return current;
|
|
141
176
|
}
|
|
177
|
+
/** Recursively set a value at a dot-notation path in a JSON object. */
|
|
178
|
+
function setByPathNested(obj, path, value) {
|
|
179
|
+
const parts = path.split('.');
|
|
180
|
+
let current = obj;
|
|
181
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
182
|
+
const part = parts[i];
|
|
183
|
+
if (current[part] == null || typeof current[part] !== 'object' || Array.isArray(current[part])) {
|
|
184
|
+
current[part] = {};
|
|
185
|
+
}
|
|
186
|
+
current = current[part];
|
|
187
|
+
}
|
|
188
|
+
current[parts[parts.length - 1]] = value;
|
|
189
|
+
}
|
|
142
190
|
function isPlainObject(v) {
|
|
143
191
|
return v != null && typeof v === 'object' && !Array.isArray(v);
|
|
144
192
|
}
|
|
@@ -247,17 +295,605 @@ function applyCheck(configJson, check) {
|
|
|
247
295
|
}
|
|
248
296
|
setByPath(configJson, check.setting_path, value);
|
|
249
297
|
}
|
|
298
|
+
function mergePostApplyUploadIntoPayload(payload, hint) {
|
|
299
|
+
if (!hint)
|
|
300
|
+
return;
|
|
301
|
+
const ft = hint.file_type?.trim();
|
|
302
|
+
if (!ft || !hint.file_path.includes('#'))
|
|
303
|
+
return;
|
|
304
|
+
if (!payload.post_apply_uploads)
|
|
305
|
+
payload.post_apply_uploads = [];
|
|
306
|
+
if (payload.post_apply_uploads.some((u) => u.file_path === hint.file_path))
|
|
307
|
+
return;
|
|
308
|
+
payload.post_apply_uploads.push({ file_path: hint.file_path, file_type: ft });
|
|
309
|
+
}
|
|
310
|
+
function assertSqlite3Available() {
|
|
311
|
+
try {
|
|
312
|
+
execFileSync('which', ['sqlite3'], { stdio: 'ignore' });
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
const line = 'sqlite_update: sqlite3 command not found';
|
|
317
|
+
hookRunLog(line);
|
|
318
|
+
complianceRunnerDiag(line);
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/** Legacy Cursor: dedicated ItemTable row `composerState`. Current Cursor: nested under reactive `applicationUser` blob. */
|
|
323
|
+
function cursorVscdbHasUsableComposerStateRow(dbPath, sqliteOp) {
|
|
324
|
+
const raw = sqliteSelectValueCell(dbPath, sqliteOp.table, sqliteOp.key_column, sqliteOp.value_column, 'composerState').trim();
|
|
325
|
+
if (!raw || raw === '{}')
|
|
326
|
+
return false;
|
|
327
|
+
try {
|
|
328
|
+
const o = JSON.parse(raw);
|
|
329
|
+
return typeof o === 'object' && o !== null && !Array.isArray(o) && Object.keys(o).length > 0;
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function resolveCursorComposerSqliteOp(dbPath, sqliteOp) {
|
|
336
|
+
if (sqliteOp.target_key !== 'composerState')
|
|
337
|
+
return sqliteOp;
|
|
338
|
+
const reactiveKey = reactiveStorageItemKeyFromContract();
|
|
339
|
+
/** Merge into applicationUser (or equivalent) JSON: inner `composerState` or nested path under it. */
|
|
340
|
+
const resolveToReactive = () => {
|
|
341
|
+
if (!reactiveKey)
|
|
342
|
+
return sqliteOp;
|
|
343
|
+
const jp = (sqliteOp.json_path ?? '').trim();
|
|
344
|
+
if (!jp) {
|
|
345
|
+
return {
|
|
346
|
+
...sqliteOp,
|
|
347
|
+
target_key: reactiveKey,
|
|
348
|
+
json_path: 'composerState',
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const nestedPath = jp.startsWith('composerState.') ? jp : `composerState.${jp}`;
|
|
352
|
+
return {
|
|
353
|
+
...sqliteOp,
|
|
354
|
+
target_key: reactiveKey,
|
|
355
|
+
json_path: nestedPath,
|
|
356
|
+
};
|
|
357
|
+
};
|
|
358
|
+
// Prefer the reactive storage blob when it exists and is non-empty. Cursor reads web/composer
|
|
359
|
+
// toggles from there; a legacy ItemTable `composerState` row may still hold stale JSON — writing
|
|
360
|
+
// only that row leaves the UI unchanged (user sees "remediation did nothing" after restart).
|
|
361
|
+
if (reactiveKey) {
|
|
362
|
+
const raw = sqliteSelectValueCell(dbPath, sqliteOp.table, sqliteOp.key_column, sqliteOp.value_column, reactiveKey).trim();
|
|
363
|
+
const reactiveHasBlob = raw !== '' && raw !== '{}';
|
|
364
|
+
if (reactiveHasBlob) {
|
|
365
|
+
return resolveToReactive();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (cursorVscdbHasUsableComposerStateRow(dbPath, sqliteOp))
|
|
369
|
+
return sqliteOp;
|
|
370
|
+
if (reactiveKey) {
|
|
371
|
+
return resolveToReactive();
|
|
372
|
+
}
|
|
373
|
+
hookRunLog('vscdb_contract: missing file_collection_vscdb_contract.json (reactive_storage_item_key); run log-config or remediation sync against API');
|
|
374
|
+
return sqliteOp;
|
|
375
|
+
}
|
|
376
|
+
/** Apply sqlite merge: dot-path, or array match where `json_path` is `…container.arrayKey` (e.g. `modes4` or `composerState.modes4`). */
|
|
377
|
+
/**
|
|
378
|
+
* ItemTable values are sometimes bare JSON booleans for toggle keys; sqlite merge expects an object root.
|
|
379
|
+
* Maps known scalar roots to the policy-shaped object before applying updates.
|
|
380
|
+
*/
|
|
381
|
+
function coerceItemTableValueToObjectRoot(targetKey, parsed) {
|
|
382
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
383
|
+
return parsed;
|
|
384
|
+
}
|
|
385
|
+
if (targetKey === 'cursor/thirdPartyExtensibilityEnabled') {
|
|
386
|
+
if (typeof parsed === 'boolean') {
|
|
387
|
+
return { thirdPartyExtensibilityEnabled: parsed };
|
|
388
|
+
}
|
|
389
|
+
if (typeof parsed === 'string') {
|
|
390
|
+
const lower = parsed.trim().toLowerCase();
|
|
391
|
+
const b = lower === 'true' || lower === '1' || lower === 'yes';
|
|
392
|
+
return { thirdPartyExtensibilityEnabled: b };
|
|
393
|
+
}
|
|
394
|
+
if (typeof parsed === 'number' && !Number.isNaN(parsed)) {
|
|
395
|
+
return { thirdPartyExtensibilityEnabled: parsed !== 0 };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (targetKey === 'cursorai/donotchange/privacyMode') {
|
|
399
|
+
if (typeof parsed === 'boolean') {
|
|
400
|
+
return { privacyMode: parsed };
|
|
401
|
+
}
|
|
402
|
+
if (typeof parsed === 'string') {
|
|
403
|
+
const lower = parsed.trim().toLowerCase();
|
|
404
|
+
const b = lower === 'true' || lower === '1' || lower === 'yes';
|
|
405
|
+
return { privacyMode: b };
|
|
406
|
+
}
|
|
407
|
+
if (typeof parsed === 'number' && !Number.isNaN(parsed)) {
|
|
408
|
+
return { privacyMode: parsed !== 0 };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (targetKey === 'cursor/autoOpenLocalhostUrls') {
|
|
412
|
+
if (typeof parsed === 'boolean') {
|
|
413
|
+
return { autoOpenLocalhostUrls: parsed };
|
|
414
|
+
}
|
|
415
|
+
if (typeof parsed === 'string') {
|
|
416
|
+
const lower = parsed.trim().toLowerCase();
|
|
417
|
+
const b = lower === 'true' || lower === '1' || lower === 'yes';
|
|
418
|
+
return { autoOpenLocalhostUrls: b };
|
|
419
|
+
}
|
|
420
|
+
if (typeof parsed === 'number' && !Number.isNaN(parsed)) {
|
|
421
|
+
return { autoOpenLocalhostUrls: parsed !== 0 };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return {};
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Cursor stores some ItemTable toggles as bare JSON booleans (`true`/`false` text). Writing a wrapper
|
|
428
|
+
* object can be ignored or overwritten on launch; match the native primitive shape when disabling.
|
|
429
|
+
*/
|
|
430
|
+
function serializeItemTableValueForWrite(targetKey, root) {
|
|
431
|
+
if (targetKey === 'cursor/thirdPartyExtensibilityEnabled') {
|
|
432
|
+
const keys = Object.keys(root);
|
|
433
|
+
if (keys.length === 1 && keys[0] === 'thirdPartyExtensibilityEnabled') {
|
|
434
|
+
const v = root.thirdPartyExtensibilityEnabled;
|
|
435
|
+
if (typeof v === 'boolean')
|
|
436
|
+
return v ? 'true' : 'false';
|
|
437
|
+
if (typeof v === 'number' && !Number.isNaN(v))
|
|
438
|
+
return v !== 0 ? 'true' : 'false';
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (targetKey === 'cursorai/donotchange/privacyMode') {
|
|
442
|
+
const keys = Object.keys(root);
|
|
443
|
+
if (keys.length === 1 && keys[0] === 'privacyMode') {
|
|
444
|
+
const v = root.privacyMode;
|
|
445
|
+
if (typeof v === 'boolean')
|
|
446
|
+
return v ? 'true' : 'false';
|
|
447
|
+
if (typeof v === 'number' && !Number.isNaN(v))
|
|
448
|
+
return v !== 0 ? 'true' : 'false';
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (targetKey === 'cursor/autoOpenLocalhostUrls') {
|
|
452
|
+
const keys = Object.keys(root);
|
|
453
|
+
if (keys.length === 1 && keys[0] === 'autoOpenLocalhostUrls') {
|
|
454
|
+
const v = root.autoOpenLocalhostUrls;
|
|
455
|
+
if (typeof v === 'boolean')
|
|
456
|
+
return v ? 'true' : 'false';
|
|
457
|
+
if (typeof v === 'number' && !Number.isNaN(v))
|
|
458
|
+
return v !== 0 ? 'true' : 'false';
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return JSON.stringify(root);
|
|
462
|
+
}
|
|
463
|
+
function mergeSqliteOpIntoJson(currentJson, sqliteOp) {
|
|
464
|
+
const where = sqliteOp.array_item_where;
|
|
465
|
+
const jp = sqliteOp.json_path ?? '';
|
|
466
|
+
if (where && typeof where === 'object' && Object.keys(where).length > 0 && jp) {
|
|
467
|
+
const parts = jp.split('.').filter(Boolean);
|
|
468
|
+
if (parts.length < 1) {
|
|
469
|
+
mergeJsonAtSqlitePath(currentJson, sqliteOp.json_path, sqliteOp.updates);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const arrayKey = parts[parts.length - 1];
|
|
473
|
+
const parentParts = parts.slice(0, -1);
|
|
474
|
+
let node = currentJson;
|
|
475
|
+
for (const p of parentParts) {
|
|
476
|
+
if (node == null || typeof node !== 'object')
|
|
477
|
+
return;
|
|
478
|
+
node = node[p];
|
|
479
|
+
}
|
|
480
|
+
if (node == null || typeof node !== 'object' || Array.isArray(node))
|
|
481
|
+
return;
|
|
482
|
+
const container = node;
|
|
483
|
+
const arr = container[arrayKey];
|
|
484
|
+
if (!Array.isArray(arr))
|
|
485
|
+
return;
|
|
486
|
+
const idx = arr.findIndex((item) => {
|
|
487
|
+
if (item === null || typeof item !== 'object')
|
|
488
|
+
return false;
|
|
489
|
+
const o = item;
|
|
490
|
+
return Object.entries(where).every(([k, v]) => o[k] === v);
|
|
491
|
+
});
|
|
492
|
+
if (idx < 0)
|
|
493
|
+
return;
|
|
494
|
+
Object.assign(arr[idx], sqliteOp.updates);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
mergeJsonAtSqlitePath(currentJson, sqliteOp.json_path, sqliteOp.updates);
|
|
498
|
+
mirrorComposerShadowKeysToReactiveRoot(currentJson, sqliteOp);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Cursor remediations historically used json_path `composerState.` (trailing dot). That yields an empty
|
|
502
|
+
* segment and mergeJsonAtSqlitePath wrote under composerState[''] instead of composerState — the real
|
|
503
|
+
* isWebFetchToolEnabled stayed true. Strip that spurious object if present.
|
|
504
|
+
*/
|
|
505
|
+
function repairComposerStateEmptySegmentBug(root) {
|
|
506
|
+
const cs = root.composerState;
|
|
507
|
+
if (!cs || typeof cs !== 'object' || Array.isArray(cs))
|
|
508
|
+
return;
|
|
509
|
+
const o = cs;
|
|
510
|
+
if (!('' in o))
|
|
511
|
+
return;
|
|
512
|
+
delete o[''];
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Cursor sometimes reads web-tool toggles from the reactive blob root; policy/reads merge those into
|
|
516
|
+
* `composerState` for scan. After patching `composerState` directly (resolved json_path exactly
|
|
517
|
+
* `composerState`), copy whitelisted keys to the root using cached
|
|
518
|
+
* `composer_shadow_keys` from the file-patterns API.
|
|
519
|
+
* Skips array-target ops (`modes4`, etc.).
|
|
520
|
+
*/
|
|
521
|
+
function mirrorComposerShadowKeysToReactiveRoot(root, resolvedOp) {
|
|
522
|
+
const rk = reactiveStorageItemKeyFromContract();
|
|
523
|
+
if (!rk || resolvedOp.target_key !== rk)
|
|
524
|
+
return;
|
|
525
|
+
const where = resolvedOp.array_item_where;
|
|
526
|
+
if (where && typeof where === 'object' && Object.keys(where).length > 0)
|
|
527
|
+
return;
|
|
528
|
+
const jp = (resolvedOp.json_path ?? '').split('.').filter(Boolean);
|
|
529
|
+
if (jp.length !== 1 || jp[0] !== 'composerState')
|
|
530
|
+
return;
|
|
531
|
+
const updates = resolvedOp.updates;
|
|
532
|
+
if (!updates || typeof updates !== 'object')
|
|
533
|
+
return;
|
|
534
|
+
const shadow = composerShadowKeySetFromContract();
|
|
535
|
+
for (const k of Object.keys(updates)) {
|
|
536
|
+
if (shadow.has(k))
|
|
537
|
+
root[k] = updates[k];
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/** Merge `updates` into JSON at dot-path `json_path` (same rules as server-generated ops). */
|
|
541
|
+
function mergeJsonAtSqlitePath(currentJson, json_path, updates) {
|
|
542
|
+
if (json_path?.trim()) {
|
|
543
|
+
const pathParts = json_path.split('.').filter(Boolean);
|
|
544
|
+
if (pathParts.length === 0) {
|
|
545
|
+
Object.assign(currentJson, updates);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
let current = currentJson;
|
|
549
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
550
|
+
const part = pathParts[i];
|
|
551
|
+
const idx = parseInt(part, 10);
|
|
552
|
+
if (!isNaN(idx) && Array.isArray(current)) {
|
|
553
|
+
if (!current[idx])
|
|
554
|
+
current[idx] = {};
|
|
555
|
+
current = current[idx];
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
if (!current[part])
|
|
559
|
+
current[part] = {};
|
|
560
|
+
current = current[part];
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const lastPart = pathParts[pathParts.length - 1];
|
|
564
|
+
const lastIdx = parseInt(lastPart, 10);
|
|
565
|
+
if (!isNaN(lastIdx) && Array.isArray(current)) {
|
|
566
|
+
if (!current[lastIdx])
|
|
567
|
+
current[lastIdx] = {};
|
|
568
|
+
const target = current[lastIdx];
|
|
569
|
+
Object.assign(target, updates);
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
if (!current[lastPart])
|
|
573
|
+
current[lastPart] = {};
|
|
574
|
+
const target = current[lastPart];
|
|
575
|
+
Object.assign(target, updates);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
Object.assign(currentJson, updates);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function sqliteSelectValueCell(dbPath, table, key_column, value_column, target_key) {
|
|
583
|
+
const safeName = target_key.replace(/'/g, "''");
|
|
584
|
+
const script = `.timeout 60000\nSELECT ${value_column} FROM ${table} WHERE ${key_column}='${safeName}';\n`;
|
|
585
|
+
return execFileSync('sqlite3', ['-noheader', dbPath], {
|
|
586
|
+
input: script,
|
|
587
|
+
encoding: 'utf8',
|
|
588
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
589
|
+
}).trim();
|
|
590
|
+
}
|
|
591
|
+
function sqliteExecWithTimeout(dbPath, sqlBody) {
|
|
592
|
+
const script = `.timeout 60000\n${sqlBody}\n`;
|
|
593
|
+
execFileSync('sqlite3', [dbPath], {
|
|
594
|
+
input: script,
|
|
595
|
+
encoding: 'utf8',
|
|
596
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
/** Runs a single UPDATE (or other SQL) and returns sqlite `changes()` for the last statement. */
|
|
600
|
+
function sqliteRunUpdateReturningChanges(dbPath, updateSql) {
|
|
601
|
+
const script = `.timeout 60000\n${updateSql}\nSELECT changes();\n`;
|
|
602
|
+
const out = execFileSync('sqlite3', [dbPath], {
|
|
603
|
+
input: script,
|
|
604
|
+
encoding: 'utf8',
|
|
605
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
606
|
+
}).trim();
|
|
607
|
+
const lines = out.split(/\r?\n/).filter((l) => l.length > 0);
|
|
608
|
+
const last = lines[lines.length - 1] ?? '0';
|
|
609
|
+
return parseInt(last, 10) || 0;
|
|
610
|
+
}
|
|
611
|
+
function queueDeferredVscdbItem(item, postApplyUpload) {
|
|
612
|
+
const path = getDeferredVscdbApplyPath();
|
|
613
|
+
let payload = { version: 1, items: [] };
|
|
614
|
+
if (existsSync(path)) {
|
|
615
|
+
try {
|
|
616
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
617
|
+
if (parsed?.items && Array.isArray(parsed.items)) {
|
|
618
|
+
payload = parsed;
|
|
619
|
+
if (!Array.isArray(payload.post_apply_uploads))
|
|
620
|
+
payload.post_apply_uploads = [];
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
/* replace corrupt file */
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
payload.items.push(item);
|
|
628
|
+
mergePostApplyUploadIntoPayload(payload, postApplyUpload);
|
|
629
|
+
atomicWriteJson(path, payload);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* After Cursor exits: apply queued ItemTable JSON updates, optional log-config-file uploads (re-read sqlite),
|
|
633
|
+
* then delete the queue file.
|
|
634
|
+
* No-op (returns true) if there is nothing pending so `open -a Cursor` still runs.
|
|
635
|
+
*/
|
|
636
|
+
export async function applyDeferredVscdbFromDisk() {
|
|
637
|
+
const path = getDeferredVscdbApplyPath();
|
|
638
|
+
if (!existsSync(path))
|
|
639
|
+
return true;
|
|
640
|
+
if (!assertSqlite3Available())
|
|
641
|
+
return false;
|
|
642
|
+
let payload;
|
|
643
|
+
try {
|
|
644
|
+
payload = JSON.parse(readFileSync(path, 'utf8'));
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
hookRunLog('deferred_vscdb: could not parse queue file');
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
const postApplyUploads = [...(payload.post_apply_uploads ?? [])];
|
|
651
|
+
if (!payload.items?.length) {
|
|
652
|
+
try {
|
|
653
|
+
unlinkSync(path);
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
/* ignore */
|
|
657
|
+
}
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
for (const it of payload.items) {
|
|
662
|
+
if (!existsSync(it.dbPath)) {
|
|
663
|
+
hookRunLog(`deferred_vscdb: database missing ${it.dbPath}`);
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
const safeJson = it.new_value_json.replace(/'/g, "''");
|
|
667
|
+
const safeName = it.target_key.replace(/'/g, "''");
|
|
668
|
+
const sql = `UPDATE ${it.table} SET ${it.value_column}='${safeJson}' WHERE ${it.key_column}='${safeName}';`;
|
|
669
|
+
const changed = sqliteRunUpdateReturningChanges(it.dbPath, sql);
|
|
670
|
+
if (changed < 1) {
|
|
671
|
+
hookRunLog(`deferred_vscdb: UPDATE changed 0 rows key=${it.target_key} db=${it.dbPath} — keeping queue file`);
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
hookRunLog(`deferred_vscdb: applied ${payload.items.length} queued update(s)`);
|
|
676
|
+
const authKey = readStoredAuthKey();
|
|
677
|
+
for (const u of postApplyUploads) {
|
|
678
|
+
if (!authKey) {
|
|
679
|
+
hookRunLog(`deferred_vscdb: skip post-apply upload (no auth) path=${u.file_path}`);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const hi = u.file_path.indexOf('#');
|
|
683
|
+
if (hi < 0)
|
|
684
|
+
continue;
|
|
685
|
+
const dbPath = u.file_path.slice(0, hi);
|
|
686
|
+
const itemKey = u.file_path.slice(hi + 1).trim();
|
|
687
|
+
if (!itemKey)
|
|
688
|
+
continue;
|
|
689
|
+
const rawContent = readVscdbItemTableJson(dbPath, itemKey);
|
|
690
|
+
if (rawContent === null) {
|
|
691
|
+
hookRunLog(`deferred_vscdb: post-apply read failed path=${u.file_path}`);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const sent = await sendConfigFile({ file_type: u.file_type, file_path: u.file_path, raw_content: rawContent }, resolveHardwareUuid(), authKey);
|
|
695
|
+
hookRunLog(`deferred_vscdb: post-apply upload path=${u.file_path} ok=${sent}`);
|
|
696
|
+
}
|
|
697
|
+
unlinkSync(path);
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
catch (e) {
|
|
701
|
+
hookRunLog(`deferred_vscdb: apply failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
/** macOS Cursor: SIGKILL, apply queued vscdb writes, reopen project. */
|
|
706
|
+
export function buildDeferredCursorRestartCommand() {
|
|
707
|
+
// Prefer monorepo path when hooks run from optimus-secure-fdn; otherwise `npx -p log-llm-config apply-deferred-vscdb`
|
|
708
|
+
// (package bin) so published installs work without a local npx_packages copy.
|
|
709
|
+
return ('REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && export REPO_ROOT CURSOR_PROJECT="$REPO_ROOT" && ' +
|
|
710
|
+
"nohup bash -c 'sleep 2; if [ -f \"$REPO_ROOT/npx_packages/log-llm-config/dist/apply_deferred_vscdb.js\" ]; then node \"$REPO_ROOT/npx_packages/log-llm-config/dist/apply_deferred_vscdb.js\"; else npx --yes -p log-llm-config apply-deferred-vscdb; fi || true; open -a Cursor \"$CURSOR_PROJECT\"' >/dev/null 2>&1 & killall -9 Cursor");
|
|
711
|
+
}
|
|
712
|
+
function sqliteRowGroupKey(dbPath, op) {
|
|
713
|
+
return `${dbPath}|${op.table}|${op.key_column}|${op.value_column}|${op.target_key}`;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Deferred sqlite path: one read per logical row, apply every `sqlite_op` merge in memory, then one queue
|
|
717
|
+
* entry per row. Without this, two ops on the same `composerState` row each read the stale DB and queue
|
|
718
|
+
* two full JSON blobs — the second UPDATE overwrites the first (e.g. only one of autoRun/fullAutoRun sticks).
|
|
719
|
+
*/
|
|
720
|
+
function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
|
|
721
|
+
const dbPath = configPath.split('#')[0];
|
|
722
|
+
if (!existsSync(dbPath)) {
|
|
723
|
+
const line = `sqlite_update: database not found at ${dbPath}`;
|
|
724
|
+
hookRunLog(line);
|
|
725
|
+
complianceRunnerDiag(line);
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
if (!assertSqlite3Available())
|
|
729
|
+
return false;
|
|
730
|
+
const resolvedOps = sqliteOps.map((op) => resolveCursorComposerSqliteOp(dbPath, op));
|
|
731
|
+
const groups = new Map();
|
|
732
|
+
for (const op of resolvedOps) {
|
|
733
|
+
const k = sqliteRowGroupKey(dbPath, op);
|
|
734
|
+
const arr = groups.get(k);
|
|
735
|
+
if (arr)
|
|
736
|
+
arr.push(op);
|
|
737
|
+
else
|
|
738
|
+
groups.set(k, [op]);
|
|
739
|
+
}
|
|
740
|
+
try {
|
|
741
|
+
for (const ops of groups.values()) {
|
|
742
|
+
const first = ops[0];
|
|
743
|
+
complianceRunnerDiag(`sqlite_update: deferred merge db=${dbPath} target_key=${first.target_key} operations=${ops.length}`);
|
|
744
|
+
let currentJson = {};
|
|
745
|
+
try {
|
|
746
|
+
const result = sqliteSelectValueCell(dbPath, first.table, first.key_column, first.value_column, first.target_key);
|
|
747
|
+
if (result) {
|
|
748
|
+
const parsed = JSON.parse(result);
|
|
749
|
+
currentJson = coerceItemTableValueToObjectRoot(first.target_key, parsed);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
catch (e) {
|
|
753
|
+
const line = `sqlite_update: error querying database: ${e instanceof Error ? e.message : String(e)}`;
|
|
754
|
+
hookRunLog(line);
|
|
755
|
+
complianceRunnerDiag(line);
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
for (const op of ops) {
|
|
759
|
+
mergeSqliteOpIntoJson(currentJson, op);
|
|
760
|
+
}
|
|
761
|
+
repairComposerStateEmptySegmentBug(currentJson);
|
|
762
|
+
const updatedJson = serializeItemTableValueForWrite(first.target_key, currentJson);
|
|
763
|
+
queueDeferredVscdbItem({
|
|
764
|
+
dbPath,
|
|
765
|
+
table: first.table,
|
|
766
|
+
key_column: first.key_column,
|
|
767
|
+
value_column: first.value_column,
|
|
768
|
+
target_key: first.target_key,
|
|
769
|
+
new_value_json: updatedJson,
|
|
770
|
+
}, postApplyUpload);
|
|
771
|
+
const okLine = `sqlite_update: queued deferred write (merged ${ops.length} op(s)) for ${first.target_key} in ${dbPath}`;
|
|
772
|
+
hookRunLog(okLine);
|
|
773
|
+
complianceRunnerDiag(okLine);
|
|
774
|
+
}
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
777
|
+
catch (err) {
|
|
778
|
+
const line = `sqlite_update: unexpected error: ${err instanceof Error ? err.message : String(err)}`;
|
|
779
|
+
hookRunLog(line);
|
|
780
|
+
complianceRunnerDiag(line);
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Read + merge JSON for a sqlite op; when `deferred`, queue UPDATE for after IDE exit (state.vscdb lock).
|
|
786
|
+
*/
|
|
787
|
+
function applyOrQueueSqliteJsonUpdate(configPath, sqliteOp, deferred) {
|
|
788
|
+
try {
|
|
789
|
+
const dbPath = configPath.split('#')[0];
|
|
790
|
+
complianceRunnerDiag(`sqlite_update: attempt db=${dbPath} target_key=${sqliteOp.target_key} json_path=${sqliteOp.json_path} deferred=${deferred}`);
|
|
791
|
+
if (!existsSync(dbPath)) {
|
|
792
|
+
const line = `sqlite_update: database not found at ${dbPath}`;
|
|
793
|
+
hookRunLog(line);
|
|
794
|
+
complianceRunnerDiag(line);
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
if (!assertSqlite3Available())
|
|
798
|
+
return false;
|
|
799
|
+
sqliteOp = resolveCursorComposerSqliteOp(dbPath, sqliteOp);
|
|
800
|
+
const { table, key_column, value_column, target_key } = sqliteOp;
|
|
801
|
+
const safeName = target_key.replace(/'/g, "''");
|
|
802
|
+
let currentJson = {};
|
|
803
|
+
try {
|
|
804
|
+
const result = sqliteSelectValueCell(dbPath, table, key_column, value_column, target_key);
|
|
805
|
+
if (result) {
|
|
806
|
+
const parsed = JSON.parse(result);
|
|
807
|
+
currentJson = coerceItemTableValueToObjectRoot(target_key, parsed);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
catch (e) {
|
|
811
|
+
const line = `sqlite_update: error querying database: ${e instanceof Error ? e.message : String(e)}`;
|
|
812
|
+
hookRunLog(line);
|
|
813
|
+
complianceRunnerDiag(line);
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
mergeSqliteOpIntoJson(currentJson, sqliteOp);
|
|
817
|
+
repairComposerStateEmptySegmentBug(currentJson);
|
|
818
|
+
const updatedJson = serializeItemTableValueForWrite(target_key, currentJson);
|
|
819
|
+
if (deferred) {
|
|
820
|
+
queueDeferredVscdbItem({
|
|
821
|
+
dbPath,
|
|
822
|
+
table,
|
|
823
|
+
key_column,
|
|
824
|
+
value_column,
|
|
825
|
+
target_key,
|
|
826
|
+
new_value_json: updatedJson,
|
|
827
|
+
});
|
|
828
|
+
const okLine = `sqlite_update: queued deferred write for ${target_key} in ${dbPath}`;
|
|
829
|
+
hookRunLog(okLine);
|
|
830
|
+
complianceRunnerDiag(okLine);
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
const safeJson = updatedJson.replace(/'/g, "''");
|
|
834
|
+
const updateSql = `UPDATE ${table} SET ${value_column}='${safeJson}' WHERE ${key_column}='${safeName}';`;
|
|
835
|
+
try {
|
|
836
|
+
sqliteExecWithTimeout(dbPath, updateSql);
|
|
837
|
+
const okLine = `sqlite_update: successfully updated ${target_key} in ${dbPath}`;
|
|
838
|
+
hookRunLog(okLine);
|
|
839
|
+
complianceRunnerDiag(okLine);
|
|
840
|
+
return true;
|
|
841
|
+
}
|
|
842
|
+
catch (e) {
|
|
843
|
+
const line = `sqlite_update: error updating database: ${e instanceof Error ? e.message : String(e)}`;
|
|
844
|
+
hookRunLog(line);
|
|
845
|
+
complianceRunnerDiag(line);
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
catch (err) {
|
|
850
|
+
const line = `sqlite_update: unexpected error: ${err instanceof Error ? err.message : String(err)}`;
|
|
851
|
+
hookRunLog(line);
|
|
852
|
+
complianceRunnerDiag(line);
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
250
856
|
export function enforceRemediation(instruction) {
|
|
251
857
|
try {
|
|
252
|
-
const dir = dirname(instruction.config_file_path);
|
|
253
|
-
if (!existsSync(dir))
|
|
254
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
255
858
|
const fixSpec = remediationFixSpec(instruction);
|
|
256
|
-
const checks = fixSpec?.
|
|
859
|
+
const checks = fixSpec?.checks ?? [];
|
|
257
860
|
if (checks.length === 0) {
|
|
258
861
|
hookRunLog(`remediation_enforce: no checks to apply uuid=${instruction.uuid}`);
|
|
259
|
-
return false;
|
|
862
|
+
return { ok: false };
|
|
863
|
+
}
|
|
864
|
+
const sqliteOps = checks.filter((c) => {
|
|
865
|
+
const raw = c;
|
|
866
|
+
return raw.sqlite_op !== undefined;
|
|
867
|
+
});
|
|
868
|
+
if (sqliteOps.length > 0) {
|
|
869
|
+
complianceRunnerDiag(`remediation_enforce: sqlite path uuid=${instruction.uuid} checks_with_sqlite=${sqliteOps.length}`);
|
|
870
|
+
const restartRequired = !!fixSpec?.restart_required;
|
|
871
|
+
if (restartRequired) {
|
|
872
|
+
const ops = sqliteOps.map((c) => c.sqlite_op);
|
|
873
|
+
const ft = instruction.file_type?.trim();
|
|
874
|
+
const postApplyUpload = ft && instruction.config_file_path.includes('#')
|
|
875
|
+
? { file_path: instruction.config_file_path, file_type: ft }
|
|
876
|
+
: undefined;
|
|
877
|
+
const ok = queueDeferredSqliteOpsMerged(instruction.config_file_path, ops, postApplyUpload);
|
|
878
|
+
return { ok, deferredSqlite: ok };
|
|
879
|
+
}
|
|
880
|
+
let allSuccess = true;
|
|
881
|
+
for (const check of sqliteOps) {
|
|
882
|
+
const raw = check;
|
|
883
|
+
const sqliteOp = raw.sqlite_op;
|
|
884
|
+
const rowOk = applyOrQueueSqliteJsonUpdate(instruction.config_file_path, sqliteOp, false);
|
|
885
|
+
if (!rowOk)
|
|
886
|
+
allSuccess = false;
|
|
887
|
+
}
|
|
888
|
+
return { ok: allSuccess, deferredSqlite: false };
|
|
889
|
+
}
|
|
890
|
+
if (fixSpec?.file_format !== 'json') {
|
|
891
|
+
hookRunLog(`remediation_enforce: unsupported file format ${fixSpec?.file_format} uuid=${instruction.uuid}`);
|
|
892
|
+
return { ok: false };
|
|
260
893
|
}
|
|
894
|
+
const dir = dirname(instruction.config_file_path);
|
|
895
|
+
if (!existsSync(dir))
|
|
896
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
261
897
|
let configJson = {};
|
|
262
898
|
if (existsSync(instruction.config_file_path)) {
|
|
263
899
|
try {
|
|
@@ -274,11 +910,11 @@ export function enforceRemediation(instruction) {
|
|
|
274
910
|
const tmp = `${instruction.config_file_path}.tmp`;
|
|
275
911
|
writeFileSync(tmp, content, 'utf8');
|
|
276
912
|
renameSync(tmp, instruction.config_file_path);
|
|
277
|
-
return true;
|
|
913
|
+
return { ok: true };
|
|
278
914
|
}
|
|
279
915
|
catch (err) {
|
|
280
916
|
hookRunLog(`remediation_enforce_error: uuid=${instruction.uuid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
281
|
-
return false;
|
|
917
|
+
return { ok: false };
|
|
282
918
|
}
|
|
283
919
|
}
|
|
284
920
|
// ---------------------------------------------------------------------------
|
|
@@ -318,27 +954,43 @@ export function reportAutofixApplied(remediationUuid, result) {
|
|
|
318
954
|
export async function syncRemediations(endpointBase, machineUuid) {
|
|
319
955
|
let remediations = readInstructions().remediations;
|
|
320
956
|
const activeUuids = remediations.map((r) => r.uuid);
|
|
957
|
+
complianceRunnerDiag(`remediation_sync: begin endpoint=${endpointBase} machine_uuid=${machineUuid} local_rows=${remediations.length}`);
|
|
958
|
+
try {
|
|
959
|
+
if (!existsSync(getFileCollectionVscdbContractPath())) {
|
|
960
|
+
const pr = await getFileCollectionPatterns(endpointBase);
|
|
961
|
+
if (pr)
|
|
962
|
+
persistVscdbComposerContractFromPatternsResponse(pr);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
catch {
|
|
966
|
+
/* contract also written on full log-config run */
|
|
967
|
+
}
|
|
321
968
|
let syncResult;
|
|
322
969
|
try {
|
|
323
970
|
syncResult = await fetchSync(endpointBase, machineUuid, activeUuids);
|
|
324
971
|
}
|
|
325
972
|
catch (err) {
|
|
326
|
-
|
|
973
|
+
const msg = `remediation_sync_error: ${err instanceof Error ? err.message : String(err)}`;
|
|
974
|
+
hookRunLog(msg);
|
|
975
|
+
complianceRunnerDiag(msg);
|
|
327
976
|
return;
|
|
328
977
|
}
|
|
329
978
|
if (!syncResult) {
|
|
330
979
|
hookRunLog(`remediation_sync: no response from server, skipping`);
|
|
980
|
+
complianceRunnerDiag('remediation_sync: no response from server (non-200 or empty body), skipping');
|
|
331
981
|
return;
|
|
332
982
|
}
|
|
333
983
|
if (syncResult.status === 'unchanged') {
|
|
334
984
|
hookRunLog(`remediation_sync: unchanged enforced=${activeUuids.length}`);
|
|
985
|
+
complianceRunnerDiag(`remediation_sync: server status=unchanged local_uuids=${activeUuids.length} — no new rows to fetch; if a deploy should exist, verify Finding.machine matches this hardware_uuid`);
|
|
335
986
|
const want = new Set(activeUuids);
|
|
336
987
|
const have = new Map(remediations.map((r) => [r.uuid, r]));
|
|
337
988
|
const needBackfill = activeUuids.some((u) => !have.has(u));
|
|
989
|
+
const haveIncomplete = remediations.some((r) => want.has(r.uuid) && needsManifestRefetch(r));
|
|
338
990
|
const haveOrphan = remediations.some((r) => !want.has(r.uuid));
|
|
339
991
|
const path = getRemediationInstructionsPath();
|
|
340
992
|
if (activeUuids.length > 0) {
|
|
341
|
-
if (needBackfill || haveOrphan || !existsSync(path)) {
|
|
993
|
+
if (needBackfill || haveIncomplete || haveOrphan || !existsSync(path)) {
|
|
342
994
|
try {
|
|
343
995
|
await ensureInstructionsForUuids(endpointBase, machineUuid, want, null);
|
|
344
996
|
}
|
|
@@ -359,6 +1011,7 @@ export async function syncRemediations(endpointBase, machineUuid) {
|
|
|
359
1011
|
}
|
|
360
1012
|
const { added, removed } = syncResult;
|
|
361
1013
|
hookRunLog(`remediation_sync: status=updated added=${added.length} removed=${removed.length}`);
|
|
1014
|
+
complianceRunnerDiag(`remediation_sync: server status=updated added=${JSON.stringify(added)} removed=${JSON.stringify(removed)}`);
|
|
362
1015
|
const removedSet = new Set(removed);
|
|
363
1016
|
let removedCount = 0;
|
|
364
1017
|
remediations = remediations.filter((r) => {
|
|
@@ -375,7 +1028,9 @@ export async function syncRemediations(endpointBase, machineUuid) {
|
|
|
375
1028
|
manifest = await fetchManifest(endpointBase, machineUuid, added);
|
|
376
1029
|
}
|
|
377
1030
|
catch (err) {
|
|
378
|
-
|
|
1031
|
+
const msg = `remediation_manifest_error: ${err instanceof Error ? err.message : String(err)}`;
|
|
1032
|
+
hookRunLog(msg);
|
|
1033
|
+
complianceRunnerDiag(msg);
|
|
379
1034
|
if (removedCount > 0) {
|
|
380
1035
|
remediations.sort((a, b) => a.uuid.localeCompare(b.uuid));
|
|
381
1036
|
writeInstructions({ remediations });
|
|
@@ -384,6 +1039,7 @@ export async function syncRemediations(endpointBase, machineUuid) {
|
|
|
384
1039
|
}
|
|
385
1040
|
if (!manifest) {
|
|
386
1041
|
hookRunLog(`remediation_sync: manifest fetch failed, skipping`);
|
|
1042
|
+
complianceRunnerDiag('remediation_sync: manifest fetch failed (null), skipping — remediation_instructions.json not updated');
|
|
387
1043
|
if (removedCount > 0) {
|
|
388
1044
|
remediations.sort((a, b) => a.uuid.localeCompare(b.uuid));
|
|
389
1045
|
writeInstructions({ remediations });
|
|
@@ -411,9 +1067,15 @@ export async function syncRemediations(endpointBase, machineUuid) {
|
|
|
411
1067
|
hookRunLog(`remediation_instructions_saved: uuid=${instruction.uuid} path=${instruction.config_file_path}`);
|
|
412
1068
|
}
|
|
413
1069
|
const addedCount = overlay.length;
|
|
1070
|
+
if (added.length > 0 && addedCount === 0) {
|
|
1071
|
+
const msg = `remediation_sync: manifest had no rows for added uuids=${added.join(',')} — check server manifest/machine linkage`;
|
|
1072
|
+
hookRunLog(msg);
|
|
1073
|
+
complianceRunnerDiag(`${msg} — remediation_instructions.json not written`);
|
|
1074
|
+
}
|
|
414
1075
|
if (removedCount > 0 || addedCount > 0) {
|
|
415
1076
|
remediations.sort((a, b) => a.uuid.localeCompare(b.uuid));
|
|
416
1077
|
writeInstructions({ remediations });
|
|
1078
|
+
complianceRunnerDiag(`remediation_sync: wrote ${getRemediationInstructionsPath()} rows=${remediations.length} (added_from_manifest=${addedCount} removed=${removedCount})`);
|
|
417
1079
|
const finalWant = new Set(remediations.map((r) => r.uuid));
|
|
418
1080
|
try {
|
|
419
1081
|
await ensureInstructionsForUuids(endpointBase, machineUuid, finalWant, overlay.length > 0 ? overlay : null);
|
|
@@ -432,4 +1094,5 @@ export async function syncRemediations(endpointBase, machineUuid) {
|
|
|
432
1094
|
}
|
|
433
1095
|
}
|
|
434
1096
|
hookRunLog(`remediation_sync: saved=${addedCount} removed=${removedCount}`);
|
|
1097
|
+
complianceRunnerDiag(`remediation_sync: done saved=${addedCount} removed=${removedCount}`);
|
|
435
1098
|
}
|