log-llm-config 1.4.4 → 1.4.9

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.
@@ -0,0 +1,372 @@
1
+ import { existsSync, mkdirSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, appendFileSync, readFileSync, } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
5
+ const COMPLIANCE_SUBDIR = 'compliance';
6
+ const TIER_NOOP = 'noop';
7
+ const TIER_SUCCESS = 'success';
8
+ const TIER_ERROR = 'error';
9
+ const TIER_IN_PROCESS = 'in_process';
10
+ const TIER_DIRS = [TIER_NOOP, TIER_SUCCESS, TIER_ERROR];
11
+ const NOOP_MAX_AGE_MS = 30 * 60 * 1000;
12
+ const SUCCESS_ERROR_MAX_AGE_MS = 24 * 60 * 60 * 1000;
13
+ /** In-flight logs in compliance/in_process/ before finalize. */
14
+ const INFLIGHT_MAX_AGE_MS = 2 * 60 * 60 * 1000;
15
+ const LATEST_POINTER = 'latest';
16
+ const MAX_SESSION_LOG_UPLOAD_BYTES = 5 * 1024 * 1024;
17
+ let activeComplianceLogPath = null;
18
+ export function parseComplianceLogArg(argv) {
19
+ const eq = argv.find((a) => a.startsWith('--compliance-log='));
20
+ if (eq) {
21
+ const v = eq.slice('--compliance-log='.length).trim();
22
+ return v || null;
23
+ }
24
+ const idx = argv.indexOf('--compliance-log');
25
+ if (idx >= 0 && argv[idx + 1]) {
26
+ const v = argv[idx + 1].trim();
27
+ return v || null;
28
+ }
29
+ return null;
30
+ }
31
+ export function isFinalizeComplianceSessionArgv(argv) {
32
+ return argv.includes('--finalize-compliance-session');
33
+ }
34
+ export function getComplianceLogDir() {
35
+ return path.join(homedir(), OPT_AI_SEC_MANAGEMENT_REL, COMPLIANCE_SUBDIR);
36
+ }
37
+ export function getActiveComplianceLogPath() {
38
+ return activeComplianceLogPath;
39
+ }
40
+ export function setActiveComplianceLogPath(logPath) {
41
+ activeComplianceLogPath = logPath ? path.resolve(logPath) : null;
42
+ }
43
+ function isTierDirName(name) {
44
+ return TIER_DIRS.includes(name);
45
+ }
46
+ function isManagedDirName(name) {
47
+ return isTierDirName(name) || name === TIER_IN_PROCESS;
48
+ }
49
+ function getInProcessComplianceLogDir() {
50
+ return path.join(getComplianceLogDir(), TIER_IN_PROCESS);
51
+ }
52
+ /**
53
+ * Convert legacy/root session paths into the explicit in_process/ location.
54
+ *
55
+ * Hooks may still pass ~/.../compliance/<timestamp>.log; future writes should live
56
+ * at ~/.../compliance/in_process/<timestamp>.log until finalize classifies them.
57
+ */
58
+ export function resolveComplianceSessionLogPath(logPath) {
59
+ const resolved = path.resolve(logPath);
60
+ if (path.dirname(resolved) === getComplianceLogDir()) {
61
+ return path.join(getInProcessComplianceLogDir(), path.basename(resolved));
62
+ }
63
+ return resolved;
64
+ }
65
+ /** Session log still in compliance/in_process/ (not yet finalized into a tier folder). */
66
+ export function isInflightComplianceLogPath(logPath) {
67
+ const resolved = resolveComplianceSessionLogPath(logPath);
68
+ return path.dirname(resolved) === getInProcessComplianceLogDir();
69
+ }
70
+ export function readLatestComplianceLogPointer() {
71
+ const pointer = path.join(getComplianceLogDir(), LATEST_POINTER);
72
+ if (!existsSync(pointer))
73
+ return null;
74
+ try {
75
+ const p = readFileSync(pointer, 'utf8').trim();
76
+ return p || null;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ function pruneLogFilesInDir(dir, maxAgeMs, exceptPath) {
83
+ if (!existsSync(dir))
84
+ return 0;
85
+ const cutoff = Date.now() - maxAgeMs;
86
+ let removed = 0;
87
+ for (const name of readdirSync(dir)) {
88
+ if (!name.endsWith('.log'))
89
+ continue;
90
+ const full = path.join(dir, name);
91
+ if (exceptPath && full === exceptPath)
92
+ continue;
93
+ try {
94
+ const st = statSync(full);
95
+ if (st.mtimeMs < cutoff) {
96
+ unlinkSync(full);
97
+ removed += 1;
98
+ }
99
+ }
100
+ catch {
101
+ // best-effort
102
+ }
103
+ }
104
+ return removed;
105
+ }
106
+ export function pruneComplianceSessionLogs(exceptPath) {
107
+ const base = getComplianceLogDir();
108
+ const keep = exceptPath ? path.resolve(exceptPath) : null;
109
+ let removed = 0;
110
+ removed += pruneLogFilesInDir(path.join(base, TIER_NOOP), NOOP_MAX_AGE_MS, keep);
111
+ removed += pruneLogFilesInDir(path.join(base, TIER_SUCCESS), SUCCESS_ERROR_MAX_AGE_MS, keep);
112
+ removed += pruneLogFilesInDir(path.join(base, TIER_ERROR), SUCCESS_ERROR_MAX_AGE_MS, keep);
113
+ removed += pruneLogFilesInDir(path.join(base, TIER_IN_PROCESS), INFLIGHT_MAX_AGE_MS, keep);
114
+ if (!existsSync(base))
115
+ return removed;
116
+ for (const name of readdirSync(base)) {
117
+ if (name === LATEST_POINTER || isManagedDirName(name))
118
+ continue;
119
+ if (!name.endsWith('.log'))
120
+ continue;
121
+ const full = path.join(base, name);
122
+ if (keep && full === keep)
123
+ continue;
124
+ try {
125
+ const st = statSync(full);
126
+ if (st.mtimeMs < Date.now() - INFLIGHT_MAX_AGE_MS) {
127
+ unlinkSync(full);
128
+ removed += 1;
129
+ }
130
+ }
131
+ catch {
132
+ // best-effort
133
+ }
134
+ }
135
+ return removed;
136
+ }
137
+ /**
138
+ * Classify a compliance session for finalize + optional server upload.
139
+ *
140
+ * Only two outcomes are uploaded (success / error): remediation **succeeded** or **failed**.
141
+ * Policy violations, in-flight restart, and sync-only sessions are noop (local retention only).
142
+ */
143
+ export function classifyComplianceSessionLog(content) {
144
+ const text = content.trim();
145
+ if (!text)
146
+ return TIER_NOOP;
147
+ if (/remediation_tracking: verified uuid=/.test(text)) {
148
+ return TIER_SUCCESS;
149
+ }
150
+ const remediationFailedPatterns = [
151
+ /REMEDIATION APPLY FAILURE/,
152
+ /compliance_check_runner: uncaught error/,
153
+ /remediation_tracking: verification_failed uuid=/,
154
+ /post_restart_verification count=\d+ quarantined=[1-9]/,
155
+ /enforceRemediation.*failed/i,
156
+ ];
157
+ if (remediationFailedPatterns.some((p) => p.test(text))) {
158
+ return TIER_ERROR;
159
+ }
160
+ return TIER_NOOP;
161
+ }
162
+ /** Keep at compliance/ root until verify or remediation failure is recorded in the session. */
163
+ export function isRemediationCycleInflight(content) {
164
+ if (/remediation_tracking: verified uuid=/.test(content))
165
+ return false;
166
+ if (/remediation_tracking: verification_failed uuid=/.test(content))
167
+ return false;
168
+ if (/REMEDIATION APPLY FAILURE/.test(content))
169
+ return false;
170
+ if (/post_restart_verification count=\d+ quarantined=[1-9]/.test(content))
171
+ return false;
172
+ return /remediation_tracking: pending_post_restart_verify uuid=/.test(content);
173
+ }
174
+ function ensureComplianceLogDir() {
175
+ const dir = getComplianceLogDir();
176
+ if (!existsSync(dir))
177
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
178
+ for (const tier of [...TIER_DIRS, TIER_IN_PROCESS]) {
179
+ const sub = path.join(dir, tier);
180
+ if (!existsSync(sub))
181
+ mkdirSync(sub, { recursive: true, mode: 0o700 });
182
+ }
183
+ }
184
+ function writeLatestPointer(logPath) {
185
+ try {
186
+ ensureComplianceLogDir();
187
+ writeFileSync(path.join(getComplianceLogDir(), LATEST_POINTER), `${logPath}\n`, 'utf8');
188
+ }
189
+ catch {
190
+ // best-effort
191
+ }
192
+ }
193
+ function appendComplianceLine(logPath, line) {
194
+ try {
195
+ const dir = path.dirname(logPath);
196
+ if (!existsSync(dir))
197
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
198
+ appendFileSync(logPath, line, 'utf8');
199
+ }
200
+ catch {
201
+ // best-effort
202
+ }
203
+ }
204
+ export function complianceLogSessionBanner(logPath, label, mode) {
205
+ const resolved = path.resolve(logPath);
206
+ const ts = new Date().toISOString();
207
+ const banner = mode === 'replace'
208
+ ? `\n${'='.repeat(72)}\n${ts} session: ${label}\n${'='.repeat(72)}\n`
209
+ : `\n${'-'.repeat(72)}\n${ts} section: ${label}\n${'-'.repeat(72)}\n`;
210
+ try {
211
+ const dir = path.dirname(resolved);
212
+ if (!existsSync(dir))
213
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
214
+ if (mode === 'replace') {
215
+ writeFileSync(resolved, banner, 'utf8');
216
+ }
217
+ else {
218
+ appendFileSync(resolved, banner, 'utf8');
219
+ }
220
+ writeLatestPointer(resolved);
221
+ }
222
+ catch {
223
+ // best-effort
224
+ }
225
+ }
226
+ /** Gate: prune old files, bind path, start or resume an inflight session file. */
227
+ export function initComplianceSessionForGate(logPath) {
228
+ const resolved = resolveComplianceSessionLogPath(logPath);
229
+ ensureComplianceLogDir();
230
+ const removed = pruneComplianceSessionLogs(resolved);
231
+ setActiveComplianceLogPath(resolved);
232
+ const resume = existsSync(resolved) && statSync(resolved).size > 0 && isInflightComplianceLogPath(resolved);
233
+ complianceLogSessionBanner(resolved, 'compliance_prompt_gate (before submit)', resume ? 'append' : 'replace');
234
+ if (!resume) {
235
+ writeLatestPointer(resolved);
236
+ }
237
+ if (removed > 0) {
238
+ appendComplianceLine(resolved, `${new Date().toISOString()} compliance_session: pruned ${removed} log(s) (in_process 2h, noop 30m, success/error 24h)\n`);
239
+ }
240
+ return resolved;
241
+ }
242
+ /** Runner: prune old files, bind path, append runner section to the gate session file. */
243
+ export function initComplianceSessionForRunner(logPath) {
244
+ const resolved = resolveComplianceSessionLogPath(logPath);
245
+ ensureComplianceLogDir();
246
+ const removed = pruneComplianceSessionLogs(resolved);
247
+ setActiveComplianceLogPath(resolved);
248
+ complianceLogSessionBanner(resolved, 'compliance_check_runner (background sync + check)', 'append');
249
+ if (removed > 0) {
250
+ appendComplianceLine(resolved, `${new Date().toISOString()} compliance_session: pruned ${removed} log(s) (in_process 2h, noop 30m, success/error 24h)\n`);
251
+ }
252
+ return resolved;
253
+ }
254
+ /**
255
+ * Move session log from compliance/ root into noop|success|error/ and prune tiers.
256
+ * No-op if file missing or already finalized.
257
+ */
258
+ export function finalizeComplianceSessionLog(logPath, forcedTier) {
259
+ const resolved = resolveComplianceSessionLogPath(logPath);
260
+ if (!existsSync(resolved))
261
+ return null;
262
+ const parentName = path.basename(path.dirname(resolved));
263
+ if (isTierDirName(parentName)) {
264
+ return { tier: parentName, logPath: resolved, moved: false };
265
+ }
266
+ const content = readFileSync(resolved, 'utf8');
267
+ if (isRemediationCycleInflight(content)) {
268
+ return null;
269
+ }
270
+ const tier = forcedTier ?? classifyComplianceSessionLog(content);
271
+ ensureComplianceLogDir();
272
+ const dest = path.join(getComplianceLogDir(), tier, path.basename(resolved));
273
+ if (resolved !== dest) {
274
+ if (existsSync(dest))
275
+ unlinkSync(dest);
276
+ renameSync(resolved, dest);
277
+ }
278
+ setActiveComplianceLogPath(null);
279
+ writeLatestPointer(dest);
280
+ pruneComplianceSessionLogs(dest);
281
+ return { tier, logPath: dest, moved: true };
282
+ }
283
+ export function appendToActiveComplianceLog(message) {
284
+ const logPath = activeComplianceLogPath;
285
+ if (!logPath)
286
+ return;
287
+ try {
288
+ appendComplianceLine(logPath, `${new Date().toISOString()} ${message}\n`);
289
+ }
290
+ catch {
291
+ // best-effort
292
+ }
293
+ }
294
+ export function readComplianceSessionLog(logPath) {
295
+ try {
296
+ if (!existsSync(logPath))
297
+ return '';
298
+ return readFileSync(logPath, 'utf8');
299
+ }
300
+ catch {
301
+ return '';
302
+ }
303
+ }
304
+ function appendLineToSessionLogFile(logPath, message) {
305
+ try {
306
+ appendComplianceLine(logPath, `${new Date().toISOString()} ${message}\n`);
307
+ }
308
+ catch {
309
+ // best-effort
310
+ }
311
+ }
312
+ /**
313
+ * Upload full session log to the server (success and error tiers only, first finalize only).
314
+ */
315
+ export async function uploadComplianceSessionLogFromFinalize(result) {
316
+ if (result.tier !== TIER_SUCCESS && result.tier !== TIER_ERROR)
317
+ return;
318
+ if (!result.moved)
319
+ return;
320
+ const logText = readComplianceSessionLog(result.logPath);
321
+ if (!logText.trim())
322
+ return;
323
+ const logBytes = Buffer.byteLength(logText, 'utf8');
324
+ if (logBytes > MAX_SESSION_LOG_UPLOAD_BYTES) {
325
+ appendLineToSessionLogFile(result.logPath, `compliance_session_upload: skipped (${logBytes} bytes exceeds ${MAX_SESSION_LOG_UPLOAD_BYTES})`);
326
+ return;
327
+ }
328
+ const { readStoredAuthKey } = await import('../auth/auth_key_store.js');
329
+ const { tryResolveHardwareUuid } = await import('./hardware_uuid.js');
330
+ const { loadEndpointBase } = await import('../sender/endpoint_config.js');
331
+ const { createSignature } = await import('../sender/signing.js');
332
+ const { buildApiUrl } = await import('../../endpoint_client/registry_api.js');
333
+ const { executeBody } = await import('../../endpoint_client/http_transport.js');
334
+ const authKey = readStoredAuthKey();
335
+ const hardwareUuid = tryResolveHardwareUuid();
336
+ if (!authKey || !hardwareUuid) {
337
+ appendLineToSessionLogFile(result.logPath, 'compliance_session_upload: skipped (auth key or hardware UUID unavailable)');
338
+ return;
339
+ }
340
+ const sessionFilename = path.basename(result.logPath);
341
+ const outcome = result.tier;
342
+ const signingPayload = {
343
+ hardware_uuid: hardwareUuid,
344
+ outcome,
345
+ session_filename: sessionFilename,
346
+ log_text: logText,
347
+ };
348
+ const signature = createSignature(signingPayload, authKey.key);
349
+ const body = JSON.stringify({ ...signingPayload, signature });
350
+ const url = buildApiUrl(loadEndpointBase(), '/endpoint_security/api/remediation-activity-log/');
351
+ try {
352
+ const { statusCode } = await executeBody(url, 'POST', body, 30_000);
353
+ if (statusCode === 200) {
354
+ appendLineToSessionLogFile(result.logPath, `compliance_session_upload: ok outcome=${outcome} bytes=${logBytes}`);
355
+ }
356
+ else {
357
+ appendLineToSessionLogFile(result.logPath, `compliance_session_upload: server status=${statusCode}`);
358
+ }
359
+ }
360
+ catch (err) {
361
+ const msg = err instanceof Error ? err.message : String(err);
362
+ appendLineToSessionLogFile(result.logPath, `compliance_session_upload: failed ${msg}`);
363
+ }
364
+ }
365
+ /** Finalize session file into success|error|noop tier, then upload when success or error. */
366
+ export async function finalizeAndUploadComplianceSessionLog(logPath, forcedTier) {
367
+ const result = finalizeComplianceSessionLog(logPath, forcedTier);
368
+ if (result) {
369
+ await uploadComplianceSessionLogFromFinalize(result);
370
+ }
371
+ return result;
372
+ }
@@ -1,8 +1,11 @@
1
- import { existsSync, mkdirSync, appendFileSync, writeFileSync, statSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, appendFileSync, writeFileSync, statSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
4
+ import { getActiveComplianceLogPath } from './compliance_session_log.js';
4
5
  const HOOK_LOG_FILENAME = 'hook_log.txt';
5
6
  const COMPLIANCE_RUNNER_LOG_FILENAME = 'compliance_runner.log';
7
+ const COMPLIANCE_SUBDIR = 'compliance';
8
+ const COMPLIANCE_FALLBACK_TIER = 'noop';
6
9
  /** Append-only: remediation verify/apply failures (not cleared per hook session). */
7
10
  const REMEDIATION_APPLY_FAILURES_FILENAME = 'remediation_apply_failures.log';
8
11
  /** Hard cap so a single upload/sync session cannot grow hook_log.txt without bound. */
@@ -17,7 +20,7 @@ function getComplianceRunnerLogPath() {
17
20
  const homeDir = process.env.HOME || process.env.USERPROFILE;
18
21
  if (!homeDir)
19
22
  return null;
20
- return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, COMPLIANCE_RUNNER_LOG_FILENAME);
23
+ return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, COMPLIANCE_SUBDIR, COMPLIANCE_FALLBACK_TIER, COMPLIANCE_RUNNER_LOG_FILENAME);
21
24
  }
22
25
  function getRemediationApplyFailuresLogPath() {
23
26
  const homeDir = process.env.HOME || process.env.USERPROFILE;
@@ -26,10 +29,21 @@ function getRemediationApplyFailuresLogPath() {
26
29
  return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, REMEDIATION_APPLY_FAILURES_FILENAME);
27
30
  }
28
31
  /**
29
- * Append one ISO-timestamped line to compliance_runner.log.
32
+ * Append one ISO-timestamped line to the active compliance session log.
33
+ * If no session is active, use compliance/noop/compliance_runner.log as a legacy fallback.
30
34
  * `level` examples: INFO, RUNNER, RESTART, FAIL
31
35
  */
32
36
  function appendComplianceRunnerLine(level, message) {
37
+ const sessionPath = getActiveComplianceLogPath();
38
+ if (sessionPath) {
39
+ try {
40
+ appendFileSync(sessionPath, `${new Date().toISOString()} [${level}] pid=${process.pid} ${message}\n`, 'utf8');
41
+ }
42
+ catch {
43
+ // best-effort
44
+ }
45
+ return;
46
+ }
33
47
  const logPath = getComplianceRunnerLogPath();
34
48
  if (!logPath)
35
49
  return;
@@ -58,7 +72,7 @@ function complianceRunnerRunnerLine(message) {
58
72
  }
59
73
  /**
60
74
  * Loud, append-only record when a remediation cannot be verified or applied. Writes
61
- * {@link REMEDIATION_APPLY_FAILURES_FILENAME} and mirrors the same block to compliance_runner.log;
75
+ * {@link REMEDIATION_APPLY_FAILURES_FILENAME} and mirrors the same block to the compliance log;
62
76
  * also appends a single summary line to hook_log.txt for the current session.
63
77
  */
64
78
  function logRemediationApplyFailure(context, fields) {
@@ -89,12 +103,18 @@ function logRemediationApplyFailure(context, fields) {
89
103
  const failPath = getRemediationApplyFailuresLogPath();
90
104
  if (failPath)
91
105
  writeFailAppend(failPath);
92
- const crPath = getComplianceRunnerLogPath();
93
- if (crPath) {
94
- const dir = path.dirname(crPath);
95
- if (!existsSync(dir))
96
- mkdirSync(dir, { recursive: true, mode: 0o700 });
97
- appendFileSync(crPath, block, 'utf8');
106
+ const activeCompliancePath = getActiveComplianceLogPath();
107
+ if (activeCompliancePath) {
108
+ writeFailAppend(activeCompliancePath);
109
+ }
110
+ else {
111
+ const crPath = getComplianceRunnerLogPath();
112
+ if (crPath) {
113
+ const dir = path.dirname(crPath);
114
+ if (!existsSync(dir))
115
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
116
+ appendFileSync(crPath, block, 'utf8');
117
+ }
98
118
  }
99
119
  }
100
120
  catch {
@@ -147,6 +167,18 @@ function hookLogSessionBanner(label) {
147
167
  * without replacing hook_log.txt.
148
168
  */
149
169
  function hookLogAppendSection(label) {
170
+ const compliancePath = getActiveComplianceLogPath();
171
+ if (compliancePath) {
172
+ try {
173
+ const ts = new Date().toISOString();
174
+ const banner = `\n${'-'.repeat(72)}\n${ts} section: ${label}\n${'-'.repeat(72)}\n`;
175
+ appendFileSync(compliancePath, banner, 'utf8');
176
+ }
177
+ catch {
178
+ // best-effort
179
+ }
180
+ return;
181
+ }
150
182
  const logPath = getHookLogPath();
151
183
  if (!logPath)
152
184
  return;
@@ -167,8 +199,19 @@ function hookLogAppendSection(label) {
167
199
  function hookLogReplace() {
168
200
  hookLogSessionBanner('log_config_files (config upload)');
169
201
  }
170
- /** Append a timestamped line to hook_log.txt. */
202
+ /** Append a timestamped line to hook_log.txt or the active compliance session log. */
171
203
  function hookRunLog(message) {
204
+ const compliancePath = getActiveComplianceLogPath();
205
+ if (compliancePath) {
206
+ try {
207
+ const ts = new Date().toISOString();
208
+ appendFileSync(compliancePath, `${ts} ${message}\n`, 'utf8');
209
+ }
210
+ catch {
211
+ // best-effort
212
+ }
213
+ return;
214
+ }
172
215
  const logPath = getHookLogPath();
173
216
  if (!logPath)
174
217
  return;
@@ -194,4 +237,18 @@ function hookLogLine(message) {
194
237
  // best-effort
195
238
  }
196
239
  }
197
- export { getHookLogPath, getComplianceRunnerLogPath, hookLogReplace, hookLogSessionBanner, hookLogAppendSection, hookRunLog, hookLogLine, complianceRunnerDiag, complianceRunnerRunnerLine, appendComplianceRunnerLine, logRemediationApplyFailure, };
240
+ /** Read the current hook_log.txt content. Returns empty string if not found or unreadable. */
241
+ function readHookLog() {
242
+ const logPath = getHookLogPath();
243
+ if (!logPath)
244
+ return '';
245
+ try {
246
+ if (!existsSync(logPath))
247
+ return '';
248
+ return readFileSync(logPath, 'utf8');
249
+ }
250
+ catch {
251
+ return '';
252
+ }
253
+ }
254
+ export { getHookLogPath, getComplianceRunnerLogPath, hookLogReplace, hookLogSessionBanner, hookLogAppendSection, hookRunLog, hookLogLine, complianceRunnerDiag, complianceRunnerRunnerLine, appendComplianceRunnerLine, logRemediationApplyFailure, readHookLog, };
@@ -6,7 +6,7 @@ import { getFileCollectionPatterns, FILE_PATH_REGISTRY_FILE_PATTERNS_PATH } from
6
6
  import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
7
7
  import { runSensitivePathsAudit } from '../../log_sensitive_paths_audit.js';
8
8
  import { loadEndpointBase, getEndpointSource } from '../sender/endpoint_config.js';
9
- import { hookLogReplace, hookRunLog } from './hook_logger.js';
9
+ import { hookLogReplace, hookRunLog, readHookLog } from './hook_logger.js';
10
10
  import { resolveHookTypeFromEnv } from './hook_type_for_request.js';
11
11
  import { resolveHardwareUuid } from './hardware_uuid.js';
12
12
  import { ensureAuthentication } from '../auth/auth_flow.js';
@@ -159,7 +159,8 @@ async function sendAllConfigFiles(configFiles, worktreeReport, hardwareUuid, aut
159
159
  if (batchResult.failed > 0)
160
160
  hookRunLog(`config_failed: ${batchResult.failed} item(s) in batch`);
161
161
  if (hookRequestId != null) {
162
- const ok = await sendHookRequestUpdateManifest(hardwareUuid, authKey, hookRequestId, manifest);
162
+ const hookLogContent = readHookLog();
163
+ const ok = await sendHookRequestUpdateManifest(hardwareUuid, authKey, hookRequestId, manifest, hookLogContent || undefined);
163
164
  hookRunLog(`hook-request update manifest result=${ok ? 'ok' : 'fail'}`);
164
165
  }
165
166
  // Finish ingest session: failed_uploads hold set, worktree prune, config prune-on-absence, and scans.
@@ -58,6 +58,10 @@ export function clearRemediationApplyQuarantine(uuid) {
58
58
  writeRemediationApplyTrackingFile(file);
59
59
  hookRunLog(`remediation_tracking: quarantine_cleared uuid=${uuid}`);
60
60
  }
61
+ export function hasPendingPostRestartVerification() {
62
+ const file = readRemediationApplyTrackingFile();
63
+ return Object.values(file.entries).some((e) => e.pending_post_restart_verify === true);
64
+ }
61
65
  export function markRemediationApplyPendingVerification(uuid) {
62
66
  const file = readRemediationApplyTrackingFile();
63
67
  const prev = file.entries[uuid] ?? defaultEntry();
@@ -1460,7 +1460,10 @@ export function reportAutofixApplied(remediationUuid, result, details) {
1460
1460
  const body = JSON.stringify({ ...payload, signature });
1461
1461
  return executeBody(url, 'POST', body, 8000)
1462
1462
  .then(({ statusCode }) => {
1463
- if (statusCode !== 200) {
1463
+ if (statusCode === 200) {
1464
+ hookRunLog(`autofix_report: ok result=${result} uuid=${remediationUuid}`);
1465
+ }
1466
+ else {
1464
1467
  hookRunLog(`autofix_report: server returned ${statusCode} for uuid=${remediationUuid}`);
1465
1468
  }
1466
1469
  })
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Lightweight hardcoded-secret detection for local compliance (requires_secret_scan rows).
3
+ * Regex set aligned with policy_engine HardcodedSecretRule — not a full Secretlint/Gitleaks port.
4
+ */
5
+ /** Values that reference env vars are never treated as hardcoded secrets. */
6
+ export function isEnvBackedSecretValue(value) {
7
+ if (typeof value !== 'string')
8
+ return false;
9
+ const s = value.trim();
10
+ if (!s)
11
+ return false;
12
+ return s.includes('${env:') || s.includes('$env:') || s.includes('process.env.');
13
+ }
14
+ /** Provider / format patterns (order matters — first match wins). */
15
+ const KNOWN_SECRET_PATTERNS = [
16
+ { re: /sk-proj-[a-zA-Z0-9_-]{20,}/i, label: 'OpenAI API key' },
17
+ { re: /sk-ant-[a-zA-Z0-9_-]{20,}/i, label: 'Anthropic API key' },
18
+ { re: /sk-[a-zA-Z0-9]{20,}/i, label: 'API key (sk-...)' },
19
+ { re: /sk_(?:live|test|prod)_[a-zA-Z0-9]{20,}/i, label: 'Stripe secret key' },
20
+ { re: /pk_(?:live|test)_[a-zA-Z0-9]{20,}/i, label: 'Stripe publishable key' },
21
+ { re: /pk_[a-zA-Z0-9_]{20,}/i, label: 'API key (pk_...)' },
22
+ { re: /whsec_[a-zA-Z0-9]{20,}/i, label: 'Stripe webhook secret' },
23
+ { re: /rk_(?:live|test)_[a-zA-Z0-9]{20,}/i, label: 'Stripe restricted key' },
24
+ { re: /pplx-[a-zA-Z0-9-]{20,}/i, label: 'Perplexity API key' },
25
+ { re: /ghp_[a-zA-Z0-9]{36}/i, label: 'GitHub personal access token' },
26
+ { re: /gho_[a-zA-Z0-9]{36}/i, label: 'GitHub OAuth token' },
27
+ { re: /ghu_[a-zA-Z0-9]{36}/i, label: 'GitHub user-to-server token' },
28
+ { re: /ghs_[a-zA-Z0-9]{36}/i, label: 'GitHub server-to-server token' },
29
+ { re: /ghr_[a-zA-Z0-9]{76}/i, label: 'GitHub refresh token' },
30
+ { re: /github_pat_[a-zA-Z0-9_]{20,}/i, label: 'GitHub fine-grained PAT' },
31
+ { re: /glpat-[a-zA-Z0-9_-]{20,}/i, label: 'GitLab personal access token' },
32
+ { re: /glcbt-[a-zA-Z0-9_-]{20,}/i, label: 'GitLab CI job token' },
33
+ { re: /xoxb-[a-zA-Z0-9-]+/i, label: 'Slack bot token' },
34
+ { re: /xoxp-[a-zA-Z0-9-]+/i, label: 'Slack user token' },
35
+ { re: /xoxa-[a-zA-Z0-9-]+/i, label: 'Slack app-level token' },
36
+ { re: /xapp-[a-zA-Z0-9-]+/i, label: 'Slack app token' },
37
+ { re: /AKIA[0-9A-Z]{16}/i, label: 'AWS access key ID' },
38
+ { re: /ASIA[0-9A-Z]{16}/i, label: 'AWS temporary access key ID' },
39
+ { re: /AIza[0-9A-Za-z_-]{35}/i, label: 'Google API key' },
40
+ { re: /ya29\.[0-9A-Za-z_-]+/i, label: 'Google OAuth access token' },
41
+ { re: /AC[a-z0-9]{32}/i, label: 'Twilio account SID' },
42
+ { re: /SK[a-z0-9]{32}/i, label: 'Twilio API key' },
43
+ { re: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/i, label: 'SendGrid API key' },
44
+ { re: /key-[a-f0-9]{32}/i, label: 'Mailgun API key' },
45
+ { re: /npm_[a-zA-Z0-9]{36}/i, label: 'npm access token' },
46
+ { re: /pypi-[a-zA-Z0-9_-]{50,}/i, label: 'PyPI API token' },
47
+ { re: /discord(?:app)?\.com\/api\/webhooks\/\d+\/[a-zA-Z0-9_-]+/i, label: 'Discord webhook URL' },
48
+ { re: /hooks\.slack\.com\/services\/T[a-zA-Z0-9_]+\/B[a-zA-Z0-9_]+\/[a-zA-Z0-9_]+/i, label: 'Slack webhook URL' },
49
+ { re: /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/i, label: 'JWT (JSON Web Token)' },
50
+ { re: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i, label: 'Private key block' },
51
+ { re: /sq0[a-zA-Z0-9_-]{20,}/i, label: 'Square access token' },
52
+ { re: /shpat_[a-fA-F0-9]{32}/i, label: 'Shopify access token' },
53
+ { re: /shpca_[a-fA-F0-9]{32}/i, label: 'Shopify custom app token' },
54
+ { re: /shpss_[a-fA-F0-9]{32}/i, label: 'Shopify shared secret' },
55
+ { re: /dop_v1_[a-f0-9]{64}/i, label: 'DigitalOcean personal access token' },
56
+ { re: /pat_[a-zA-Z0-9]{22}_[a-fA-F0-9]{59}/i, label: 'Atlassian API token' },
57
+ { re: /Bearer\s+[a-zA-Z0-9\-_.]{20,}/i, label: 'Bearer token' },
58
+ ];
59
+ const BASIC_AUTH_IN_URL = /[a-zA-Z0-9._%+-]+:[a-zA-Z0-9._%+-]+@/gi;
60
+ const GENERIC_TOKEN = /\b[a-zA-Z0-9]{32,}\b/g;
61
+ const GENERIC_KEY_HINT = /(?:api[_-]?key|app[_-]?key|secret|token|password|passwd|pass\b|(?<!o)auth|credential|private[_-]?key|access[_-]?key|master[_-]?key)/i;
62
+ function basicAuthMatchIsCredential(value, matchIndex) {
63
+ const before = value.slice(0, matchIndex);
64
+ const schemeIdx = before.lastIndexOf('://');
65
+ if (schemeIdx === -1)
66
+ return true;
67
+ const between = value.slice(schemeIdx + 3, matchIndex);
68
+ return !between.includes('/');
69
+ }
70
+ function looksLikeUrlContext(value) {
71
+ return value.includes(':') && (value.toLowerCase().includes('http') || value.includes('://'));
72
+ }
73
+ /**
74
+ * Scan one scalar config value. Returns secret type label or null if clean / env-backed.
75
+ */
76
+ export function scanScalarForHardcodedSecret(value, keyName) {
77
+ if (value == null)
78
+ return null;
79
+ const valueStr = String(value);
80
+ if (isEnvBackedSecretValue(valueStr))
81
+ return null;
82
+ for (const { re, label } of KNOWN_SECRET_PATTERNS) {
83
+ if (re.test(valueStr))
84
+ return label;
85
+ }
86
+ BASIC_AUTH_IN_URL.lastIndex = 0;
87
+ for (const match of valueStr.matchAll(BASIC_AUTH_IN_URL)) {
88
+ if (basicAuthMatchIsCredential(valueStr, match.index ?? 0)) {
89
+ return 'Basic authentication credentials';
90
+ }
91
+ }
92
+ if (!GENERIC_KEY_HINT.test(keyName) || looksLikeUrlContext(valueStr)) {
93
+ return null;
94
+ }
95
+ const genericMatches = valueStr.match(GENERIC_TOKEN) ?? [];
96
+ if (genericMatches.some((token) => token.length >= 40)) {
97
+ return 'Potential token/secret';
98
+ }
99
+ return null;
100
+ }
101
+ function collectScalarLeaves(value, path = '') {
102
+ if (value == null)
103
+ return [];
104
+ if (Array.isArray(value)) {
105
+ return value.flatMap((item, idx) => collectScalarLeaves(item, path ? `${path}.${idx}` : String(idx)));
106
+ }
107
+ if (typeof value === 'object') {
108
+ return Object.entries(value).flatMap(([key, nested]) => collectScalarLeaves(nested, path ? `${path}.${key}` : key));
109
+ }
110
+ if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
111
+ return [];
112
+ }
113
+ const key = path.split('.').pop() ?? path;
114
+ return [{ path: path || '.', key, value }];
115
+ }
116
+ /** Walk parsed JSON and return all hardcoded-secret hits (JSON dot paths). */
117
+ export function scanJsonForHardcodedSecrets(configJson) {
118
+ const findings = [];
119
+ for (const leaf of collectScalarLeaves(configJson)) {
120
+ const secretType = scanScalarForHardcodedSecret(leaf.value, leaf.key);
121
+ if (secretType) {
122
+ findings.push({ path: leaf.path, key: leaf.key, secretType });
123
+ }
124
+ }
125
+ return findings;
126
+ }