log-llm-config 1.3.17 → 1.3.18
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/compliance_prompt_gate.js +31 -1
- package/dist/log_config_files/readers/vscdb_config_builder.js +33 -2
- package/dist/log_config_files/runtime/compliance_check.js +57 -17
- package/dist/log_config_files/runtime/hook_logger.js +56 -1
- package/dist/log_config_files/runtime/main_runner.js +7 -1
- package/dist/log_config_files/runtime/remediation_config_path.js +48 -0
- package/dist/log_config_files/runtime/remediation_sync.js +74 -21
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import { basename } from 'node:path';
|
|
12
12
|
import { getRemediationInstructionsPath } from './log_config_files/runtime/management_storage.js';
|
|
13
|
-
import { hookLogSessionBanner, hookRunLog } from './log_config_files/runtime/hook_logger.js';
|
|
13
|
+
import { hookLogSessionBanner, hookRunLog, logRemediationApplyFailure } from './log_config_files/runtime/hook_logger.js';
|
|
14
14
|
const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
15
15
|
function parseIde() {
|
|
16
16
|
const eq = process.argv.find((a) => a.startsWith('--ide='));
|
|
@@ -101,6 +101,11 @@ export async function runCompliancePromptGate() {
|
|
|
101
101
|
const staleDays = Math.floor(staleMs / (24 * 60 * 60 * 1000));
|
|
102
102
|
const advisory = `Remediation enforcement suspended: local remediation manifest is stale (${staleDays} days old). ` +
|
|
103
103
|
'Please reconnect to sync latest policy state.';
|
|
104
|
+
logRemediationApplyFailure('prompt_gate_stale_manifest_advisory', {
|
|
105
|
+
reason: advisory,
|
|
106
|
+
stale_days: staleDays,
|
|
107
|
+
violation_count: status.violations.length,
|
|
108
|
+
});
|
|
104
109
|
printAllowWithAdvisory(ide, advisory);
|
|
105
110
|
return;
|
|
106
111
|
}
|
|
@@ -113,6 +118,12 @@ export async function runCompliancePromptGate() {
|
|
|
113
118
|
if (failedViolations.length > 0) {
|
|
114
119
|
const ids = failedViolations.map((v) => `[${v.finding_formatted_id}]`).join(', ');
|
|
115
120
|
const msg = `Auto-fix failed for ${ids} — please fix manually or contact your security team.\n\n${failedViolations[0]?.message ?? ''}`;
|
|
121
|
+
logRemediationApplyFailure('prompt_gate_block_autofix_failed', {
|
|
122
|
+
reason: 'autofix had fixed>0 path but failedViolations non-empty after apply',
|
|
123
|
+
ide,
|
|
124
|
+
failed_formatted_ids: failedViolations.map((v) => v.finding_formatted_id).join(', '),
|
|
125
|
+
detail: failedViolations[0]?.message ?? '',
|
|
126
|
+
});
|
|
116
127
|
console.log(blockPayload(ide, msg));
|
|
117
128
|
return;
|
|
118
129
|
}
|
|
@@ -139,16 +150,35 @@ export async function runCompliancePromptGate() {
|
|
|
139
150
|
return;
|
|
140
151
|
}
|
|
141
152
|
const msg = recheck.violations[0]?.message ?? 'A security policy violation has been detected.';
|
|
153
|
+
logRemediationApplyFailure('prompt_gate_block_recheck_failed', {
|
|
154
|
+
reason: 'after autofix, local compliance recheck still reports violations',
|
|
155
|
+
ide,
|
|
156
|
+
deferred_sqlite_pending: deferredSqlitePending,
|
|
157
|
+
first_uuid: recheck.violations[0]?.uuid ?? '',
|
|
158
|
+
message: msg,
|
|
159
|
+
});
|
|
142
160
|
console.log(blockPayload(ide, msg));
|
|
143
161
|
return;
|
|
144
162
|
}
|
|
145
163
|
if (failedViolations.length > 0) {
|
|
146
164
|
const ids = failedViolations.map((v) => `[${v.finding_formatted_id}]`).join(', ');
|
|
147
165
|
const msg = `Auto-fix failed for ${ids} — please fix manually or contact your security team.\n\n${failedViolations[0]?.message ?? ''}`;
|
|
166
|
+
logRemediationApplyFailure('prompt_gate_block_autofix_failed', {
|
|
167
|
+
reason: 'autofix failed (fixed=0 branch or partial failure)',
|
|
168
|
+
ide,
|
|
169
|
+
failed_formatted_ids: failedViolations.map((v) => v.finding_formatted_id).join(', '),
|
|
170
|
+
detail: failedViolations[0]?.message ?? '',
|
|
171
|
+
});
|
|
148
172
|
console.log(blockPayload(ide, msg));
|
|
149
173
|
return;
|
|
150
174
|
}
|
|
151
175
|
const msg = status.violations[0]?.message ?? 'A security policy violation has been detected.';
|
|
176
|
+
logRemediationApplyFailure('prompt_gate_block_violations_no_autofix_applied', {
|
|
177
|
+
reason: 'violations on submit but autofix did not succeed — see autofix_skipped_not_allowed, enforceRemediation, or compliance_check entries',
|
|
178
|
+
ide,
|
|
179
|
+
first_uuid: status.violations[0]?.uuid ?? '',
|
|
180
|
+
finding_formatted_id: status.violations[0]?.finding_formatted_id ?? '',
|
|
181
|
+
});
|
|
152
182
|
console.log(blockPayload(ide, msg));
|
|
153
183
|
return;
|
|
154
184
|
}
|
|
@@ -2,6 +2,7 @@ import { homedir } from 'node:os';
|
|
|
2
2
|
import { readVSCDBState } from './vscdb_reader.js';
|
|
3
3
|
import { getVscdbPath } from '../paths/path_constants_helpers.js';
|
|
4
4
|
import { getByPath } from '../collection/enrichment_helpers.js';
|
|
5
|
+
import { hookRunLog } from '../runtime/hook_logger.js';
|
|
5
6
|
/** True if path is a vscdb virtual path (db file or db#fragment). Uses backend vscdb_basename and vscdb_entry_specs only. */
|
|
6
7
|
export function isVscdbVirtualPath(filePath, response) {
|
|
7
8
|
if (!response?.vscdb_entry_specs?.length)
|
|
@@ -93,17 +94,47 @@ function buildVscdbRawContentFromSpec(state, spec) {
|
|
|
93
94
|
* Build config file entries from state.vscdb using backend-driven specs and read queries.
|
|
94
95
|
* No hardcoded file_type, path fragments, or ItemTable keys; all from API.
|
|
95
96
|
*/
|
|
97
|
+
function summarizeComposerStateValue(composerState) {
|
|
98
|
+
if (composerState === null || composerState === undefined || typeof composerState !== 'object' || Array.isArray(composerState)) {
|
|
99
|
+
return `composerState_type=${composerState === null ? 'null' : typeof composerState}`;
|
|
100
|
+
}
|
|
101
|
+
const o = composerState;
|
|
102
|
+
const m4 = o.modes4;
|
|
103
|
+
if (!Array.isArray(m4))
|
|
104
|
+
return `modes4=${m4 === undefined ? 'absent' : typeof m4}`;
|
|
105
|
+
const agent = m4.find((x) => x !== null && typeof x === 'object' && !Array.isArray(x) && x.id === 'agent');
|
|
106
|
+
if (!agent)
|
|
107
|
+
return `modes4_len=${m4.length} agent_row=missing`;
|
|
108
|
+
return `modes4_len=${m4.length} agent_autoRun=${JSON.stringify(agent.autoRun)} agent_fullAutoRun=${JSON.stringify(agent.fullAutoRun)}`;
|
|
109
|
+
}
|
|
110
|
+
/** Exported for main_runner: one-line summary of uploaded #composerState payload (policy full-auto-run input). */
|
|
111
|
+
export function summarizeComposerPayloadForDiagnostics(raw) {
|
|
112
|
+
const inner = summarizeComposerStateValue(raw.composerState);
|
|
113
|
+
const rawKeys = Object.keys(raw)
|
|
114
|
+
.filter((k) => k !== 'source' && k !== 'extracted_at')
|
|
115
|
+
.slice(0, 16);
|
|
116
|
+
return `${inner} payload_keys=[${rawKeys.join(',')}]`;
|
|
117
|
+
}
|
|
96
118
|
function collectVscdbEntries(vscdbPath, specs, readQueries, mergeFromComposerState) {
|
|
97
119
|
if (!specs?.length)
|
|
98
120
|
return [];
|
|
99
121
|
const state = readVSCDBState(vscdbPath, readQueries, mergeFromComposerState);
|
|
100
|
-
if (!state || typeof state !== 'object')
|
|
122
|
+
if (!state || typeof state !== 'object') {
|
|
123
|
+
hookRunLog(`diag vscdb collect: readVSCDBState returned null/invalid vscdb=${vscdbPath}`);
|
|
101
124
|
return [];
|
|
125
|
+
}
|
|
126
|
+
const sk = Object.keys(state);
|
|
127
|
+
hookRunLog(`diag vscdb collect: readVSCDBState ok vscdb=${vscdbPath} top_keys=[${sk.slice(0, 24).join(',')}] ${summarizeComposerStateValue(state.composerState)}`);
|
|
102
128
|
const entries = [];
|
|
103
129
|
for (const spec of specs) {
|
|
130
|
+
const atPath = getByPath(state, spec.state_key);
|
|
104
131
|
const rawContent = buildVscdbRawContentFromSpec(state, spec);
|
|
105
|
-
if (!rawContent)
|
|
132
|
+
if (!rawContent) {
|
|
133
|
+
if (spec.state_key === 'composerState' || spec.path_suffix === '#composerState') {
|
|
134
|
+
hookRunLog(`diag vscdb collect: skipped composer emit value_constraint=${spec.value_constraint ?? 'none'} at_path=${atPath === undefined ? 'undefined' : atPath === null ? 'null' : typeof atPath} path_suffix=${spec.path_suffix}`);
|
|
135
|
+
}
|
|
106
136
|
continue;
|
|
137
|
+
}
|
|
107
138
|
entries.push({
|
|
108
139
|
file_type: spec.file_type,
|
|
109
140
|
file_path: `${vscdbPath}${spec.path_suffix}`,
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
import { existsSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { readVscdbItemTableJson } from '../readers/vscdb_reader.js';
|
|
15
15
|
import { readRemediationInstructionsFile, writeRemediationInstructionsFile } from './management_storage.js';
|
|
16
|
-
import {
|
|
16
|
+
import { resolveRemediationConfigPath } from './remediation_config_path.js';
|
|
17
|
+
import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
|
|
17
18
|
import { loadEndpointBase } from '../sender/endpoint_config.js';
|
|
18
19
|
import { resolveHardwareUuid, tryResolveHardwareUuid } from './hardware_uuid.js';
|
|
19
20
|
import { buildDeferredCursorRestartCommand, enforceRemediation, fetchSync, isTrustedRestartCommandForAutofix, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
|
|
@@ -97,10 +98,11 @@ function verifyOpsApplied(configJson, settingPath, ops) {
|
|
|
97
98
|
}
|
|
98
99
|
/** Plain JSON file or virtual `…/state.vscdb#itemKey` path for ItemTable-backed settings. */
|
|
99
100
|
function loadRemediationConfigJson(configFilePath, checkSettingPaths = []) {
|
|
100
|
-
const
|
|
101
|
+
const resolvedPath = resolveRemediationConfigPath(configFilePath);
|
|
102
|
+
const hashIdx = resolvedPath.indexOf('#');
|
|
101
103
|
if (hashIdx >= 0) {
|
|
102
|
-
const dbPath =
|
|
103
|
-
const itemKeyFromPath =
|
|
104
|
+
const dbPath = resolvedPath.slice(0, hashIdx);
|
|
105
|
+
const itemKeyFromPath = resolvedPath.slice(hashIdx + 1).trim();
|
|
104
106
|
if (!itemKeyFromPath)
|
|
105
107
|
return { ok: false, reason: 'empty_vscdb_key' };
|
|
106
108
|
if (!existsSync(dbPath))
|
|
@@ -115,10 +117,10 @@ function loadRemediationConfigJson(configFilePath, checkSettingPaths = []) {
|
|
|
115
117
|
}
|
|
116
118
|
return { ok: true, json: merged };
|
|
117
119
|
}
|
|
118
|
-
if (!existsSync(
|
|
120
|
+
if (!existsSync(resolvedPath))
|
|
119
121
|
return { ok: false, reason: 'file_not_found' };
|
|
120
122
|
try {
|
|
121
|
-
return { ok: true, json: JSON.parse(readFileSync(
|
|
123
|
+
return { ok: true, json: JSON.parse(readFileSync(resolvedPath, 'utf8')) };
|
|
122
124
|
}
|
|
123
125
|
catch {
|
|
124
126
|
return { ok: false, reason: 'parse_error' };
|
|
@@ -164,6 +166,13 @@ export function runLocalRemediationComplianceCheck() {
|
|
|
164
166
|
? `compliance_check: invalid vscdb path (empty # key), skipping uuid=${entry.uuid}`
|
|
165
167
|
: `compliance_check: could not parse config file, skipping uuid=${entry.uuid}`;
|
|
166
168
|
hookRunLog(msg);
|
|
169
|
+
logRemediationApplyFailure('compliance_check_config_unreadable', {
|
|
170
|
+
uuid: entry.uuid,
|
|
171
|
+
config_file_path: entry.config_file_path,
|
|
172
|
+
finding_formatted_id: compliance.finding_formatted_id,
|
|
173
|
+
load_reason: loaded.reason,
|
|
174
|
+
reason: 'cannot read config for compliance verify — remediation may not run until path/db is valid',
|
|
175
|
+
});
|
|
167
176
|
continue;
|
|
168
177
|
}
|
|
169
178
|
const configJson = loaded.json;
|
|
@@ -223,7 +232,14 @@ export function runLocalRemediationComplianceCheck() {
|
|
|
223
232
|
return status;
|
|
224
233
|
}
|
|
225
234
|
catch (err) {
|
|
226
|
-
|
|
235
|
+
const emsg = err instanceof Error ? err.message : String(err);
|
|
236
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
237
|
+
hookRunLog(`compliance_check: unexpected error: ${emsg}`);
|
|
238
|
+
logRemediationApplyFailure('compliance_check_exception', {
|
|
239
|
+
reason: 'runLocalRemediationComplianceCheck threw',
|
|
240
|
+
error: emsg,
|
|
241
|
+
stack: stack ?? '',
|
|
242
|
+
});
|
|
227
243
|
const fallback = {
|
|
228
244
|
status: 'ok',
|
|
229
245
|
checked_at: new Date().toISOString(),
|
|
@@ -234,6 +250,17 @@ export function runLocalRemediationComplianceCheck() {
|
|
|
234
250
|
}
|
|
235
251
|
}
|
|
236
252
|
export function applyAutofixViolations(violations) {
|
|
253
|
+
for (const v of violations) {
|
|
254
|
+
if (!v.autofix_allowed) {
|
|
255
|
+
logRemediationApplyFailure('autofix_skipped_not_allowed', {
|
|
256
|
+
uuid: v.uuid,
|
|
257
|
+
finding_formatted_id: v.finding_formatted_id,
|
|
258
|
+
config_file_path: v.config_file_path,
|
|
259
|
+
setting_path: v.setting_path,
|
|
260
|
+
reason: 'autofix_allowed is false — policy/manual fix required',
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
237
264
|
const autofixable = violations.filter((v) => v.autofix_allowed);
|
|
238
265
|
if (autofixable.length === 0)
|
|
239
266
|
return {
|
|
@@ -259,11 +286,19 @@ export function applyAutofixViolations(violations) {
|
|
|
259
286
|
const instruction = byUuid.get(violation.uuid);
|
|
260
287
|
if (!instruction) {
|
|
261
288
|
hookRunLog(`autofix: no instruction found for uuid=${violation.uuid}, skipping`);
|
|
289
|
+
logRemediationApplyFailure('autofix_no_local_instruction', {
|
|
290
|
+
uuid: violation.uuid,
|
|
291
|
+
finding_formatted_id: violation.finding_formatted_id,
|
|
292
|
+
config_file_path: violation.config_file_path,
|
|
293
|
+
setting_path: violation.setting_path,
|
|
294
|
+
reason: 'violation UUID not present in remediation_instructions.json remediations[]',
|
|
295
|
+
});
|
|
262
296
|
failedViolations.push(violation);
|
|
263
297
|
continue;
|
|
264
298
|
}
|
|
265
299
|
const inst = instruction;
|
|
266
|
-
|
|
300
|
+
const configPathForDisk = resolveRemediationConfigPath(inst.config_file_path);
|
|
301
|
+
complianceRunnerDiag(`autofix: calling enforceRemediation uuid=${inst.uuid} path=${configPathForDisk}`);
|
|
267
302
|
const er = enforceRemediation(inst);
|
|
268
303
|
if (!er.ok) {
|
|
269
304
|
failedViolations.push(violation);
|
|
@@ -274,25 +309,25 @@ export function applyAutofixViolations(violations) {
|
|
|
274
309
|
seen.add(violation.uuid);
|
|
275
310
|
fixed++;
|
|
276
311
|
appliedViolations.push(violation);
|
|
277
|
-
hookRunLog(`autofix: applied uuid=${inst.uuid} path=${
|
|
312
|
+
hookRunLog(`autofix: applied uuid=${inst.uuid} path=${configPathForDisk}`);
|
|
278
313
|
reportPromises.push(reportAutofixApplied(inst.uuid, 'success'));
|
|
279
314
|
const authKey = readStoredAuthKey();
|
|
280
315
|
if (authKey) {
|
|
281
|
-
if (er.deferredSqlite &&
|
|
316
|
+
if (er.deferredSqlite && configPathForDisk.includes('#')) {
|
|
282
317
|
hookRunLog(`autofix: skip immediate vscdb upload (deferred until after restart) uuid=${inst.uuid}`);
|
|
283
318
|
}
|
|
284
319
|
else {
|
|
285
320
|
let updatedContent;
|
|
286
|
-
if (
|
|
287
|
-
const hi =
|
|
288
|
-
const dbPath =
|
|
289
|
-
const itemKey =
|
|
321
|
+
if (configPathForDisk.includes('#')) {
|
|
322
|
+
const hi = configPathForDisk.indexOf('#');
|
|
323
|
+
const dbPath = configPathForDisk.slice(0, hi);
|
|
324
|
+
const itemKey = configPathForDisk.slice(hi + 1).trim();
|
|
290
325
|
updatedContent =
|
|
291
326
|
itemKey ? (readVscdbItemTableJson(dbPath, itemKey) ?? undefined) : undefined;
|
|
292
327
|
}
|
|
293
328
|
else {
|
|
294
329
|
try {
|
|
295
|
-
updatedContent = JSON.parse(readFileSync(
|
|
330
|
+
updatedContent = JSON.parse(readFileSync(configPathForDisk, 'utf8'));
|
|
296
331
|
}
|
|
297
332
|
catch {
|
|
298
333
|
updatedContent = undefined;
|
|
@@ -303,8 +338,8 @@ export function applyAutofixViolations(violations) {
|
|
|
303
338
|
if (fileType) {
|
|
304
339
|
const hw = tryResolveHardwareUuid();
|
|
305
340
|
if (hw) {
|
|
306
|
-
reportPromises.push(sendConfigFile({ file_type: fileType, file_path:
|
|
307
|
-
hookRunLog(`autofix: uploaded remediated file uuid=${inst.uuid} path=${
|
|
341
|
+
reportPromises.push(sendConfigFile({ file_type: fileType, file_path: configPathForDisk, raw_content: updatedContent }, hw, authKey).then((sentOk) => {
|
|
342
|
+
hookRunLog(`autofix: uploaded remediated file uuid=${inst.uuid} path=${configPathForDisk} ok=${sentOk}`);
|
|
308
343
|
}));
|
|
309
344
|
}
|
|
310
345
|
else {
|
|
@@ -313,6 +348,11 @@ export function applyAutofixViolations(violations) {
|
|
|
313
348
|
}
|
|
314
349
|
else {
|
|
315
350
|
hookRunLog(`autofix: skip upload uuid=${inst.uuid} — remediation_instructions.json missing file_type (re-sync manifest)`);
|
|
351
|
+
logRemediationApplyFailure('autofix_post_apply_upload_skipped', {
|
|
352
|
+
uuid: inst.uuid,
|
|
353
|
+
config_file_path: inst.config_file_path,
|
|
354
|
+
reason: 'file_type missing on instruction — server sync/manifest will not see applied file',
|
|
355
|
+
});
|
|
316
356
|
}
|
|
317
357
|
}
|
|
318
358
|
}
|
|
@@ -3,6 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
|
|
4
4
|
const HOOK_LOG_FILENAME = 'hook_log.txt';
|
|
5
5
|
const COMPLIANCE_RUNNER_LOG_FILENAME = 'compliance_runner.log';
|
|
6
|
+
/** Append-only: remediation verify/apply failures (not cleared per hook session). */
|
|
7
|
+
const REMEDIATION_APPLY_FAILURES_FILENAME = 'remediation_apply_failures.log';
|
|
6
8
|
/** Hard cap so a single upload/sync session cannot grow hook_log.txt without bound. */
|
|
7
9
|
const MAX_HOOK_LOG_BYTES = 2 * 1024 * 1024;
|
|
8
10
|
function getHookLogPath() {
|
|
@@ -17,6 +19,12 @@ function getComplianceRunnerLogPath() {
|
|
|
17
19
|
return null;
|
|
18
20
|
return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, COMPLIANCE_RUNNER_LOG_FILENAME);
|
|
19
21
|
}
|
|
22
|
+
function getRemediationApplyFailuresLogPath() {
|
|
23
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
24
|
+
if (!homeDir)
|
|
25
|
+
return null;
|
|
26
|
+
return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, REMEDIATION_APPLY_FAILURES_FILENAME);
|
|
27
|
+
}
|
|
20
28
|
/**
|
|
21
29
|
* Append-only diagnostics (not truncated by main_runner). Use for remediation sync / compliance.
|
|
22
30
|
*/
|
|
@@ -35,6 +43,53 @@ function complianceRunnerDiag(message) {
|
|
|
35
43
|
// best-effort
|
|
36
44
|
}
|
|
37
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Loud, append-only record when a remediation cannot be verified or applied. Writes
|
|
48
|
+
* {@link REMEDIATION_APPLY_FAILURES_FILENAME} and mirrors the same block to compliance_runner.log;
|
|
49
|
+
* also appends a single summary line to hook_log.txt for the current session.
|
|
50
|
+
*/
|
|
51
|
+
function logRemediationApplyFailure(context, fields) {
|
|
52
|
+
const ts = new Date().toISOString();
|
|
53
|
+
const bodyLines = Object.entries(fields)
|
|
54
|
+
.filter(([, v]) => v !== undefined && v !== null && String(v) !== '')
|
|
55
|
+
.map(([k, v]) => ` ${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`);
|
|
56
|
+
const block = [
|
|
57
|
+
'',
|
|
58
|
+
'='.repeat(72),
|
|
59
|
+
`${ts} REMEDIATION APPLY FAILURE — ${context}`,
|
|
60
|
+
...bodyLines,
|
|
61
|
+
'='.repeat(72),
|
|
62
|
+
'',
|
|
63
|
+
].join('\n');
|
|
64
|
+
const summary = fields.reason != null
|
|
65
|
+
? String(fields.reason)
|
|
66
|
+
: fields.message != null
|
|
67
|
+
? String(fields.message)
|
|
68
|
+
: 'see fields above';
|
|
69
|
+
const writeAppend = (filePath) => {
|
|
70
|
+
const dir = path.dirname(filePath);
|
|
71
|
+
if (!existsSync(dir))
|
|
72
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
73
|
+
appendFileSync(filePath, block, 'utf8');
|
|
74
|
+
};
|
|
75
|
+
try {
|
|
76
|
+
const failPath = getRemediationApplyFailuresLogPath();
|
|
77
|
+
if (failPath)
|
|
78
|
+
writeAppend(failPath);
|
|
79
|
+
const crPath = getComplianceRunnerLogPath();
|
|
80
|
+
if (crPath)
|
|
81
|
+
writeAppend(crPath);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* best-effort */
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
hookRunLog(`REMEDIATION_APPLY_FAILURE [${context}] ${fields.uuid != null ? `uuid=${fields.uuid} ` : ''}${summary}`);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* best-effort */
|
|
91
|
+
}
|
|
92
|
+
}
|
|
38
93
|
function ensureHookLogUnderCap() {
|
|
39
94
|
const logPath = getHookLogPath();
|
|
40
95
|
if (!logPath || !existsSync(logPath))
|
|
@@ -121,4 +176,4 @@ function hookLogLine(message) {
|
|
|
121
176
|
// best-effort
|
|
122
177
|
}
|
|
123
178
|
}
|
|
124
|
-
export { getHookLogPath, getComplianceRunnerLogPath, hookLogReplace, hookLogSessionBanner, hookLogAppendSection, hookRunLog, hookLogLine, complianceRunnerDiag, };
|
|
179
|
+
export { getHookLogPath, getComplianceRunnerLogPath, hookLogReplace, hookLogSessionBanner, hookLogAppendSection, hookRunLog, hookLogLine, complianceRunnerDiag, logRemediationApplyFailure, };
|
|
@@ -9,7 +9,7 @@ import { hookLogReplace, hookRunLog } from './hook_logger.js';
|
|
|
9
9
|
import { resolveHardwareUuid } from './hardware_uuid.js';
|
|
10
10
|
import { ensureAuthentication } from '../auth/auth_flow.js';
|
|
11
11
|
import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
|
|
12
|
-
import { isVscdbVirtualPath, tryReadVscdbVirtualFile } from '../readers/vscdb_config_builder.js';
|
|
12
|
+
import { isVscdbVirtualPath, tryReadVscdbVirtualFile, summarizeComposerPayloadForDiagnostics, } from '../readers/vscdb_config_builder.js';
|
|
13
13
|
import { persistVscdbComposerContractFromPatternsResponse } from '../readers/vscdb_reader.js';
|
|
14
14
|
import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, determineFileTypeFromPath } from '../collection/config_collector.js';
|
|
15
15
|
import { normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
|
|
@@ -126,6 +126,12 @@ async function main() {
|
|
|
126
126
|
await addSensitivePathsAudit(endpointBase, configFiles);
|
|
127
127
|
const fileTypes = [...new Set(configFiles.map((c) => c.file_type))].join(', ');
|
|
128
128
|
hookRunLog(`collected ${configFiles.length} config file(s) file_types=${fileTypes}`);
|
|
129
|
+
for (const c of configFiles) {
|
|
130
|
+
if (!c.file_path.replace(/\\/g, '/').includes('#composerState'))
|
|
131
|
+
continue;
|
|
132
|
+
const tail = c.file_path.length > 120 ? `…${c.file_path.slice(-120)}` : c.file_path;
|
|
133
|
+
hookRunLog(`diag pre-upload #composerState ${summarizeComposerPayloadForDiagnostics(c.raw_content)} path=${tail}`);
|
|
134
|
+
}
|
|
129
135
|
const claudeSettings = configFiles.filter((c) => c.file_type === 'claude_settings');
|
|
130
136
|
if (claudeSettings.length > 0)
|
|
131
137
|
hookRunLog(`claude_settings in batch: ${claudeSettings.length} path(s): ${claudeSettings.map((c) => c.file_path).join(', ')}`);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Portable key stored by the server for Cursor User globalStorage state.vscdb
|
|
6
|
+
* (see optimus_security/endpoint/log_config_file/handler._canonical_cursor_user_state_vscdb_path).
|
|
7
|
+
*/
|
|
8
|
+
export const PORTABLE_CURSOR_USER_STATE_VSCDB = 'Cursor/User/globalStorage/state.vscdb';
|
|
9
|
+
function normalizeSlashes(p) {
|
|
10
|
+
return p.trim().replace(/\\/g, '/');
|
|
11
|
+
}
|
|
12
|
+
function cursorStateVscdbAbsoluteBasePaths() {
|
|
13
|
+
const h = homedir();
|
|
14
|
+
if (process.platform === 'darwin') {
|
|
15
|
+
return [join(h, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb')];
|
|
16
|
+
}
|
|
17
|
+
if (process.platform === 'win32') {
|
|
18
|
+
const appData = process.env.APPDATA;
|
|
19
|
+
if (appData)
|
|
20
|
+
return [join(appData, 'Cursor', 'User', 'globalStorage', 'state.vscdb')];
|
|
21
|
+
return [join(h, 'AppData', 'Roaming', 'Cursor', 'User', 'globalStorage', 'state.vscdb')];
|
|
22
|
+
}
|
|
23
|
+
return [join(h, '.config', 'Cursor', 'User', 'globalStorage', 'state.vscdb')];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Expand server-side portable Cursor state.vscdb paths to a local absolute path.
|
|
27
|
+
* No-op for absolute paths and for relative paths that are not the portable vscdb key.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveRemediationConfigPath(configFilePath) {
|
|
30
|
+
const t = normalizeSlashes(configFilePath);
|
|
31
|
+
const hash = t.indexOf('#');
|
|
32
|
+
const base = hash >= 0 ? t.slice(0, hash) : t;
|
|
33
|
+
const frag = hash >= 0 ? t.slice(hash) : '';
|
|
34
|
+
const baseNorm = base.replace(/\/+$/, '');
|
|
35
|
+
if (baseNorm.startsWith('/') || /^[a-zA-Z]:\//.test(baseNorm)) {
|
|
36
|
+
return configFilePath;
|
|
37
|
+
}
|
|
38
|
+
const portable = PORTABLE_CURSOR_USER_STATE_VSCDB;
|
|
39
|
+
if (baseNorm !== portable && !baseNorm.endsWith('/' + portable)) {
|
|
40
|
+
return configFilePath;
|
|
41
|
+
}
|
|
42
|
+
const candidates = cursorStateVscdbAbsoluteBasePaths();
|
|
43
|
+
for (const abs of candidates) {
|
|
44
|
+
if (existsSync(abs))
|
|
45
|
+
return `${abs}${frag}`;
|
|
46
|
+
}
|
|
47
|
+
return `${candidates[0]}${frag}`;
|
|
48
|
+
}
|
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkS
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
4
|
import { executeGet, executeBody } from '../../endpoint_client/http_transport.js';
|
|
5
|
-
import { complianceRunnerDiag, hookRunLog } from './hook_logger.js';
|
|
5
|
+
import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
|
|
6
6
|
import { atomicWriteJson, getDeferredVscdbApplyPath, getFileCollectionVscdbContractPath, getRemediationInstructionsPath, readFileCollectionVscdbContract, readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
|
|
7
7
|
import { readStoredAuthKey } from '../auth/auth_key_store.js';
|
|
8
8
|
import { createSignature } from '../sender/signing.js';
|
|
@@ -11,6 +11,7 @@ import { tryResolveHardwareUuid } from './hardware_uuid.js';
|
|
|
11
11
|
import { CURSOR_SCALAR_ITEMTABLE_FIELDS, persistVscdbComposerContractFromPatternsResponse, readVscdbItemTableJson, } from '../readers/vscdb_reader.js';
|
|
12
12
|
import { sendConfigFile } from '../sender/batch_sender.js';
|
|
13
13
|
import { getFileCollectionPatterns } from '../../endpoint_client/registry_api.js';
|
|
14
|
+
import { resolveRemediationConfigPath } from './remediation_config_path.js';
|
|
14
15
|
function reactiveStorageItemKeyFromContract() {
|
|
15
16
|
const k = readFileCollectionVscdbContract()?.reactive_storage_item_key;
|
|
16
17
|
return typeof k === 'string' && k.trim() !== '' ? k.trim() : undefined;
|
|
@@ -735,6 +736,14 @@ function sqliteRowGroupKey(dbPath, op) {
|
|
|
735
736
|
* entry per row. Without this, two ops on the same `composerState` row each read the stale DB and queue
|
|
736
737
|
* two full JSON blobs — the second UPDATE overwrites the first (e.g. only one of autoRun/fullAutoRun sticks).
|
|
737
738
|
*/
|
|
739
|
+
/** ItemTable keys must not contain SQL-quote garbage (bad vscdb #fragment or corrupted manifest). */
|
|
740
|
+
function assertSafeDeferredItemTableKey(targetKey) {
|
|
741
|
+
if (!targetKey || targetKey.length > 512)
|
|
742
|
+
return false;
|
|
743
|
+
if (/['"\\]/.test(targetKey))
|
|
744
|
+
return false;
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
738
747
|
function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
|
|
739
748
|
const dbPath = configPath.split('#')[0];
|
|
740
749
|
if (!existsSync(dbPath)) {
|
|
@@ -758,6 +767,12 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
|
|
|
758
767
|
try {
|
|
759
768
|
for (const ops of groups.values()) {
|
|
760
769
|
const first = ops[0];
|
|
770
|
+
if (!assertSafeDeferredItemTableKey(first.target_key)) {
|
|
771
|
+
const line = `sqlite_update: rejected unsafe or empty target_key for deferred queue (refusing to write state.vscdb)`;
|
|
772
|
+
hookRunLog(line);
|
|
773
|
+
complianceRunnerDiag(`${line} target_key_preview=${first.target_key.slice(0, 80)}`);
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
761
776
|
complianceRunnerDiag(`sqlite_update: deferred merge db=${dbPath} target_key=${first.target_key} operations=${ops.length}`);
|
|
762
777
|
let currentJson = {};
|
|
763
778
|
try {
|
|
@@ -778,6 +793,12 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
|
|
|
778
793
|
}
|
|
779
794
|
repairComposerStateEmptySegmentBug(currentJson);
|
|
780
795
|
const updatedJson = serializeItemTableValueForWrite(first.target_key, currentJson);
|
|
796
|
+
if (updatedJson === '{}' && Object.keys(currentJson).length === 0) {
|
|
797
|
+
const line = `sqlite_update: deferred merge produced empty JSON — refusing to queue (would wipe ItemTable row)`;
|
|
798
|
+
hookRunLog(line);
|
|
799
|
+
complianceRunnerDiag(line);
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
781
802
|
queueDeferredVscdbItem({
|
|
782
803
|
dbPath,
|
|
783
804
|
table: first.table,
|
|
@@ -875,67 +896,99 @@ function applyOrQueueSqliteJsonUpdate(configPath, sqliteOp, deferred) {
|
|
|
875
896
|
}
|
|
876
897
|
}
|
|
877
898
|
export function enforceRemediation(instruction) {
|
|
899
|
+
const resolvedPath = resolveRemediationConfigPath(instruction.config_file_path);
|
|
900
|
+
const inst = resolvedPath === instruction.config_file_path
|
|
901
|
+
? instruction
|
|
902
|
+
: { ...instruction, config_file_path: resolvedPath };
|
|
903
|
+
const fail = (reason, extra) => {
|
|
904
|
+
const spec = remediationFixSpec(inst);
|
|
905
|
+
logRemediationApplyFailure('enforceRemediation', {
|
|
906
|
+
uuid: inst.uuid,
|
|
907
|
+
config_file_path: inst.config_file_path,
|
|
908
|
+
file_type: inst.file_type ?? '',
|
|
909
|
+
finding_formatted_id: spec?.finding_formatted_id ?? '',
|
|
910
|
+
reason,
|
|
911
|
+
...extra,
|
|
912
|
+
});
|
|
913
|
+
hookRunLog(`remediation_enforce: failed uuid=${inst.uuid} reason=${reason}`);
|
|
914
|
+
return { ok: false, failureReason: reason };
|
|
915
|
+
};
|
|
878
916
|
try {
|
|
879
|
-
const fixSpec = remediationFixSpec(
|
|
917
|
+
const fixSpec = remediationFixSpec(inst);
|
|
880
918
|
const checks = fixSpec?.checks ?? [];
|
|
881
919
|
if (checks.length === 0) {
|
|
882
|
-
|
|
883
|
-
return { ok: false };
|
|
920
|
+
return fail('no checks in fix spec (empty or missing compliance checks)');
|
|
884
921
|
}
|
|
885
922
|
const sqliteOps = checks.filter((c) => {
|
|
886
923
|
const raw = c;
|
|
887
924
|
return raw.sqlite_op !== undefined;
|
|
888
925
|
});
|
|
889
926
|
if (sqliteOps.length > 0) {
|
|
890
|
-
complianceRunnerDiag(`remediation_enforce: sqlite path uuid=${
|
|
927
|
+
complianceRunnerDiag(`remediation_enforce: sqlite path uuid=${inst.uuid} checks_with_sqlite=${sqliteOps.length}`);
|
|
891
928
|
const restartRequired = !!fixSpec?.restart_required;
|
|
892
929
|
if (restartRequired) {
|
|
893
930
|
const ops = sqliteOps.map((c) => c.sqlite_op);
|
|
894
|
-
const ft =
|
|
895
|
-
const postApplyUpload = ft &&
|
|
896
|
-
? { file_path:
|
|
931
|
+
const ft = inst.file_type?.trim();
|
|
932
|
+
const postApplyUpload = ft && inst.config_file_path.includes('#')
|
|
933
|
+
? { file_path: inst.config_file_path, file_type: ft }
|
|
897
934
|
: undefined;
|
|
898
|
-
const ok = queueDeferredSqliteOpsMerged(
|
|
935
|
+
const ok = queueDeferredSqliteOpsMerged(inst.config_file_path, ops, postApplyUpload);
|
|
936
|
+
if (!ok) {
|
|
937
|
+
return fail('deferred state.vscdb queue failed (database missing, sqlite3 unavailable, or read/merge error — see sqlite_update lines above in hook_log)', { config_file_path: inst.config_file_path });
|
|
938
|
+
}
|
|
899
939
|
return { ok, deferredSqlite: ok };
|
|
900
940
|
}
|
|
901
941
|
let allSuccess = true;
|
|
902
942
|
for (const check of sqliteOps) {
|
|
903
943
|
const raw = check;
|
|
904
944
|
const sqliteOp = raw.sqlite_op;
|
|
905
|
-
const rowOk = applyOrQueueSqliteJsonUpdate(
|
|
945
|
+
const rowOk = applyOrQueueSqliteJsonUpdate(inst.config_file_path, sqliteOp, false);
|
|
906
946
|
if (!rowOk)
|
|
907
947
|
allSuccess = false;
|
|
908
948
|
}
|
|
909
|
-
|
|
949
|
+
if (!allSuccess) {
|
|
950
|
+
return fail('immediate sqlite apply failed for one or more checks');
|
|
951
|
+
}
|
|
952
|
+
return { ok: true, deferredSqlite: false };
|
|
910
953
|
}
|
|
911
954
|
if (fixSpec?.file_format !== 'json') {
|
|
912
|
-
|
|
913
|
-
return { ok: false };
|
|
955
|
+
return fail(`unsupported file format: ${String(fixSpec?.file_format ?? 'undefined')}`);
|
|
914
956
|
}
|
|
915
|
-
const dir = dirname(
|
|
957
|
+
const dir = dirname(inst.config_file_path);
|
|
916
958
|
if (!existsSync(dir))
|
|
917
959
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
918
960
|
let configJson = {};
|
|
919
|
-
if (existsSync(
|
|
961
|
+
if (existsSync(inst.config_file_path)) {
|
|
920
962
|
try {
|
|
921
|
-
configJson = JSON.parse(readFileSync(
|
|
963
|
+
configJson = JSON.parse(readFileSync(inst.config_file_path, 'utf8'));
|
|
922
964
|
}
|
|
923
965
|
catch {
|
|
924
|
-
hookRunLog(`remediation_enforce: could not parse existing file, starting fresh uuid=${
|
|
966
|
+
hookRunLog(`remediation_enforce: could not parse existing file, starting fresh uuid=${inst.uuid}`);
|
|
925
967
|
}
|
|
926
968
|
}
|
|
927
969
|
for (const check of checks) {
|
|
928
970
|
applyCheck(configJson, check);
|
|
929
971
|
}
|
|
930
972
|
const content = JSON.stringify(configJson, null, 2);
|
|
931
|
-
const tmp = `${
|
|
973
|
+
const tmp = `${inst.config_file_path}.tmp`;
|
|
932
974
|
writeFileSync(tmp, content, 'utf8');
|
|
933
|
-
renameSync(tmp,
|
|
975
|
+
renameSync(tmp, inst.config_file_path);
|
|
934
976
|
return { ok: true };
|
|
935
977
|
}
|
|
936
978
|
catch (err) {
|
|
937
|
-
|
|
938
|
-
|
|
979
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
980
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
981
|
+
logRemediationApplyFailure('enforceRemediation', {
|
|
982
|
+
uuid: inst.uuid,
|
|
983
|
+
config_file_path: inst.config_file_path,
|
|
984
|
+
file_type: inst.file_type ?? '',
|
|
985
|
+
finding_formatted_id: remediationFixSpec(inst)?.finding_formatted_id ?? '',
|
|
986
|
+
reason: 'exception during enforce',
|
|
987
|
+
error: msg,
|
|
988
|
+
stack: stack ?? '',
|
|
989
|
+
});
|
|
990
|
+
hookRunLog(`remediation_enforce_error: uuid=${inst.uuid} err=${msg}`);
|
|
991
|
+
return { ok: false, failureReason: `exception: ${msg}` };
|
|
939
992
|
}
|
|
940
993
|
}
|
|
941
994
|
// ---------------------------------------------------------------------------
|