@optimuslabs/harness-map-staging 1.5.25 → 1.5.27
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/approved_tenant_gate.js +302 -0
- package/dist/compliance_prompt_gate.js +315 -156
- package/dist/log_config_files/collection/claude_desktop_orgs_collector.js +175 -0
- package/dist/log_config_files/collection/claude_login_plan.js +72 -0
- package/dist/log_config_files/collection/codex_token_usage_collector.js +1 -1
- package/dist/log_config_files/collection/config_collector.js +1 -0
- package/dist/log_config_files/collection/cursor_token_usage_collector.js +127 -91
- package/dist/log_config_files/runtime/main_runner.js +69 -147
- package/dist/log_config_files/runtime/management_storage.js +46 -2
- package/dist/log_config_files/runtime/remediation_sync.js +156 -109
- package/package.json +1 -1
|
@@ -15,7 +15,7 @@ import { ensureAuthentication } from '../auth/auth_flow.js';
|
|
|
15
15
|
import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
|
|
16
16
|
import { isVscdbVirtualPath, tryReadVscdbVirtualFile, summarizeComposerPayloadForDiagnostics, } from '../readers/vscdb_config_builder.js';
|
|
17
17
|
import { persistVscdbComposerContractFromPatternsResponse } from '../readers/vscdb_reader.js';
|
|
18
|
-
import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles, collectMcpFromClaudeJsonProjects, collectClaudeTokenUsage, collectCopilotTokenUsage, collectOpencodeTokenUsage, collectCodexTokenUsage, collectCursorTokenUsage, collectHermesTokenUsage, collectOpenclawTokenUsage, collectPiTokenUsage, collectCoworkDesktopVersion, collectGrokCliVersion, collectClaudeDesktopExtensionManifests, collectClaudeDesktopExtensionSettingsFiles, enrichClaudeDesktopExtensionsInstallationsUpload, determineFileTypeFromPath, } from '../collection/config_collector.js';
|
|
18
|
+
import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles, collectMcpFromClaudeJsonProjects, collectClaudeTokenUsage, collectCopilotTokenUsage, collectOpencodeTokenUsage, collectCodexTokenUsage, collectCursorTokenUsage, collectHermesTokenUsage, collectOpenclawTokenUsage, collectPiTokenUsage, collectCoworkDesktopVersion, collectClaudeDesktopOrgs, collectGrokCliVersion, collectClaudeDesktopExtensionManifests, collectClaudeDesktopExtensionSettingsFiles, enrichClaudeDesktopExtensionsInstallationsUpload, determineFileTypeFromPath, } from '../collection/config_collector.js';
|
|
19
19
|
import { ensureCursorUserSettingsSnapshotInBatch } from '../collection/ensure_cursor_user_settings_snapshot.js';
|
|
20
20
|
import { collectSkillsCliInstalled } from '../collection/skills_cli_collector.js';
|
|
21
21
|
import { collectWorkspaceVscdbs } from '../collection/mcp_tool_collector.js';
|
|
@@ -26,8 +26,36 @@ import { canonicalCursorUserStateVscdbPath } from './remediation_config_path.js'
|
|
|
26
26
|
import { ensureWorkspaceRepoEnv, resolveProjectRoot } from './workspace_repo.js';
|
|
27
27
|
import { scanActiveWorktrees, activeWorktreeRootSet, filterConfigFilesToActiveWorktrees, } from './worktree_scanner.js';
|
|
28
28
|
import { createSignature, canonicalizePayload } from '../sender/signing.js';
|
|
29
|
+
import { withClaudeLoginPlan } from '../collection/claude_login_plan.js';
|
|
29
30
|
const PROJECT_ROOT = resolveProjectRoot();
|
|
30
|
-
|
|
31
|
+
const configFileKey = (c) => `${c.file_type}\t${c.file_path}`;
|
|
32
|
+
/** Appends the entries not in the batch yet (by file type and path), recording each as seen. */
|
|
33
|
+
function addUnseen(configFiles, seen, entries) {
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const key = configFileKey(entry);
|
|
36
|
+
if (seen.has(key))
|
|
37
|
+
continue;
|
|
38
|
+
seen.add(key);
|
|
39
|
+
configFiles.push(entry);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Machine-wide collectors, run in this order after the plugin caches: [hook log line, collector]. */
|
|
43
|
+
const HOME_COLLECTORS = [
|
|
44
|
+
['scanning Claude known projects from ~/.claude.json', collectMcpFromClaudeJsonProjects],
|
|
45
|
+
['aggregating Claude token usage from ~/.claude/projects transcripts', collectClaudeTokenUsage],
|
|
46
|
+
['aggregating Copilot token usage from ~/.copilot/session-state transcripts', collectCopilotTokenUsage],
|
|
47
|
+
['aggregating OpenCode token usage from ~/.local/share/opencode/opencode.db', collectOpencodeTokenUsage],
|
|
48
|
+
['aggregating Codex token usage from ~/.codex/sessions rollouts', collectCodexTokenUsage],
|
|
49
|
+
['aggregating Cursor token usage from state.vscdb', collectCursorTokenUsage],
|
|
50
|
+
['aggregating Hermes token usage from ~/.hermes/state.db', collectHermesTokenUsage],
|
|
51
|
+
['aggregating OpenClaw token usage from ~/.openclaw/agents/*/sessions/sessions.json', collectOpenclawTokenUsage],
|
|
52
|
+
['aggregating Pi token usage from ~/.pi/agent/sessions transcripts', collectPiTokenUsage],
|
|
53
|
+
["reading Claude Desktop app version for Cowork's Agent Profile", collectCoworkDesktopVersion],
|
|
54
|
+
["reading the orgs Claude Desktop's signed-in account has used", collectClaudeDesktopOrgs],
|
|
55
|
+
['reading Grok CLI version', collectGrokCliVersion],
|
|
56
|
+
];
|
|
57
|
+
/** The file-path-registry patterns from the API; throws when none came back. */
|
|
58
|
+
async function fetchPatterns(endpointBase) {
|
|
31
59
|
const patternsUrl = `${endpointBase.replace(/\/+$/, '')}${FILE_PATH_REGISTRY_FILE_PATTERNS_PATH}`;
|
|
32
60
|
hookRunLog(`fetching patterns from ${patternsUrl}`);
|
|
33
61
|
const patternsResponse = await getFileCollectionPatterns(endpointBase);
|
|
@@ -38,9 +66,12 @@ async function collectAllConfigFiles(endpointBase) {
|
|
|
38
66
|
hookRunLog(msg);
|
|
39
67
|
throw new Error(msg);
|
|
40
68
|
}
|
|
69
|
+
return patternsResponse;
|
|
70
|
+
}
|
|
71
|
+
function scanPatterns(patternsResponse) {
|
|
41
72
|
persistVscdbComposerContractFromPatternsResponse(patternsResponse);
|
|
42
73
|
hookRunLog(`scanning config files`);
|
|
43
|
-
const
|
|
74
|
+
const files = collectConfigFilesFromPatterns(patternsResponse.patterns, PROJECT_ROOT, hookRunLog, {
|
|
44
75
|
vscdbEntrySpecs: patternsResponse.vscdb_entry_specs,
|
|
45
76
|
vscdb_read_queries: patternsResponse.vscdb_read_queries,
|
|
46
77
|
vscdb_merge_from_composer_state: patternsResponse.vscdb_merge_from_composer_state,
|
|
@@ -50,163 +81,32 @@ async function collectAllConfigFiles(endpointBase) {
|
|
|
50
81
|
absolute_path_prefixes: patternsResponse.absolute_path_prefixes,
|
|
51
82
|
mcp_tool_glob_spec: patternsResponse.mcp_tool_glob_spec ?? null,
|
|
52
83
|
});
|
|
84
|
+
return withClaudeLoginPlan(files, HOME_DIR);
|
|
85
|
+
}
|
|
86
|
+
/** MCP tool caches: the global cache, Cursor's per-project caches and its workspace vscdb files. */
|
|
87
|
+
function addMcpToolCaches(patternsResponse, configFiles, seen) {
|
|
53
88
|
hookRunLog(`scanning MCP tool cache`);
|
|
54
89
|
const mcpToolGlobPattern = patternsResponse.patterns.find((p) => p.path_resolution === 'mcp_tool_glob');
|
|
55
90
|
const mcpToolSkipPrefixes = normalizePathSkipPrefixes(mcpToolGlobPattern?.path_skip_prefixes);
|
|
56
|
-
|
|
57
|
-
for (const m of collectMcpToolFiles(mcpToolSkipPrefixes, patternsResponse.client_path_constants)) {
|
|
58
|
-
const key = `${m.file_type}\t${m.file_path}`;
|
|
59
|
-
if (!existingPaths.has(key)) {
|
|
60
|
-
existingPaths.add(key);
|
|
61
|
-
configFiles.push(m);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
91
|
+
addUnseen(configFiles, seen, collectMcpToolFiles(mcpToolSkipPrefixes, patternsResponse.client_path_constants));
|
|
64
92
|
hookRunLog(`scanning Cursor project workspace MCP configs (~/.cursor/projects/*/mcp-cache.json)`);
|
|
65
|
-
|
|
66
|
-
const key = `${m.file_type}\t${m.file_path}`;
|
|
67
|
-
if (!existingPaths.has(key)) {
|
|
68
|
-
existingPaths.add(key);
|
|
69
|
-
configFiles.push(m);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
93
|
+
addUnseen(configFiles, seen, collectCursorProjectWorkspaceMcpConfigs(mcpToolSkipPrefixes, patternsResponse.client_path_constants, patternsResponse.workspace_vscdb_spec, HOME_DIR));
|
|
72
94
|
if (patternsResponse.workspace_vscdb_spec) {
|
|
73
95
|
hookRunLog(`scanning Cursor workspace vscdb files`);
|
|
74
|
-
|
|
75
|
-
const key = `${m.file_type}\t${m.file_path}`;
|
|
76
|
-
if (!existingPaths.has(key)) {
|
|
77
|
-
existingPaths.add(key);
|
|
78
|
-
configFiles.push(m);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
hookRunLog(`scanning installed plugins`);
|
|
83
|
-
if (!patternsResponse.client_path_constants)
|
|
84
|
-
throw new Error('client_path_constants required from API response');
|
|
85
|
-
configFiles.push(...collectConfigFilesFromInstalledPlugins(patternsResponse.client_path_constants));
|
|
86
|
-
hookRunLog(`scanning Claude plugin cache MCP manifests`);
|
|
87
|
-
for (const entry of collectPluginCacheMcpFiles(patternsResponse.client_path_constants, HOME_DIR)) {
|
|
88
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
89
|
-
if (!existingPaths.has(key)) {
|
|
90
|
-
existingPaths.add(key);
|
|
91
|
-
configFiles.push(entry);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
hookRunLog(`scanning Claude known projects from ~/.claude.json`);
|
|
95
|
-
for (const entry of collectMcpFromClaudeJsonProjects(HOME_DIR)) {
|
|
96
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
97
|
-
if (!existingPaths.has(key)) {
|
|
98
|
-
existingPaths.add(key);
|
|
99
|
-
configFiles.push(entry);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
hookRunLog(`aggregating Claude token usage from ~/.claude/projects transcripts`);
|
|
103
|
-
for (const entry of collectClaudeTokenUsage(HOME_DIR)) {
|
|
104
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
105
|
-
if (!existingPaths.has(key)) {
|
|
106
|
-
existingPaths.add(key);
|
|
107
|
-
configFiles.push(entry);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
hookRunLog(`aggregating Copilot token usage from ~/.copilot/session-state transcripts`);
|
|
111
|
-
for (const entry of collectCopilotTokenUsage(HOME_DIR)) {
|
|
112
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
113
|
-
if (!existingPaths.has(key)) {
|
|
114
|
-
existingPaths.add(key);
|
|
115
|
-
configFiles.push(entry);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
hookRunLog(`aggregating OpenCode token usage from ~/.local/share/opencode/opencode.db`);
|
|
119
|
-
for (const entry of collectOpencodeTokenUsage(HOME_DIR)) {
|
|
120
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
121
|
-
if (!existingPaths.has(key)) {
|
|
122
|
-
existingPaths.add(key);
|
|
123
|
-
configFiles.push(entry);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
hookRunLog(`aggregating Codex token usage from ~/.codex/sessions rollouts`);
|
|
127
|
-
for (const entry of collectCodexTokenUsage(HOME_DIR)) {
|
|
128
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
129
|
-
if (!existingPaths.has(key)) {
|
|
130
|
-
existingPaths.add(key);
|
|
131
|
-
configFiles.push(entry);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
hookRunLog(`aggregating Cursor token usage from state.vscdb`);
|
|
135
|
-
for (const entry of collectCursorTokenUsage(HOME_DIR)) {
|
|
136
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
137
|
-
if (!existingPaths.has(key)) {
|
|
138
|
-
existingPaths.add(key);
|
|
139
|
-
configFiles.push(entry);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
hookRunLog(`aggregating Hermes token usage from ~/.hermes/state.db`);
|
|
143
|
-
for (const entry of collectHermesTokenUsage(HOME_DIR)) {
|
|
144
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
145
|
-
if (!existingPaths.has(key)) {
|
|
146
|
-
existingPaths.add(key);
|
|
147
|
-
configFiles.push(entry);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
hookRunLog(`aggregating OpenClaw token usage from ~/.openclaw/agents/*/sessions/sessions.json`);
|
|
151
|
-
for (const entry of collectOpenclawTokenUsage(HOME_DIR)) {
|
|
152
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
153
|
-
if (!existingPaths.has(key)) {
|
|
154
|
-
existingPaths.add(key);
|
|
155
|
-
configFiles.push(entry);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
hookRunLog(`aggregating Pi token usage from ~/.pi/agent/sessions transcripts`);
|
|
159
|
-
for (const entry of collectPiTokenUsage(HOME_DIR)) {
|
|
160
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
161
|
-
if (!existingPaths.has(key)) {
|
|
162
|
-
existingPaths.add(key);
|
|
163
|
-
configFiles.push(entry);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
hookRunLog(`reading Claude Desktop app version for Cowork's Agent Profile`);
|
|
167
|
-
for (const entry of collectCoworkDesktopVersion(HOME_DIR)) {
|
|
168
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
169
|
-
if (!existingPaths.has(key)) {
|
|
170
|
-
existingPaths.add(key);
|
|
171
|
-
configFiles.push(entry);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
hookRunLog(`reading Grok CLI version`);
|
|
175
|
-
for (const entry of collectGrokCliVersion(HOME_DIR)) {
|
|
176
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
177
|
-
if (!existingPaths.has(key)) {
|
|
178
|
-
existingPaths.add(key);
|
|
179
|
-
configFiles.push(entry);
|
|
180
|
-
}
|
|
96
|
+
addUnseen(configFiles, seen, collectWorkspaceVscdbs(patternsResponse.workspace_vscdb_spec));
|
|
181
97
|
}
|
|
98
|
+
}
|
|
99
|
+
function addClaudeDesktopExtensions(configFiles, seen) {
|
|
182
100
|
hookRunLog(`merging disk-only Claude Desktop extensions into extensions-installations.json`);
|
|
183
101
|
const diskExtMerge = enrichClaudeDesktopExtensionsInstallationsUpload(configFiles, HOME_DIR);
|
|
184
102
|
hookRunLog(`claude desktop extensions: merged=${diskExtMerge.mergedCount} had_registry_upload=${diskExtMerge.hadRegistryUpload}`);
|
|
185
|
-
|
|
186
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
187
|
-
if (!existingPaths.has(key)) {
|
|
188
|
-
existingPaths.add(key);
|
|
189
|
-
configFiles.push(entry);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
103
|
+
addUnseen(configFiles, seen, collectClaudeDesktopExtensionSettingsFiles(HOME_DIR));
|
|
192
104
|
if (!diskExtMerge.hadRegistryUpload && diskExtMerge.mergedCount > 0) {
|
|
193
105
|
hookRunLog(`scanning Claude Desktop extension manifests on disk (no registry upload in batch)`);
|
|
194
|
-
|
|
195
|
-
const key = `${entry.file_type}\t${entry.file_path}`;
|
|
196
|
-
if (!existingPaths.has(key)) {
|
|
197
|
-
existingPaths.add(key);
|
|
198
|
-
configFiles.push(entry);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
const skillsCliEntry = collectSkillsCliInstalled(PROJECT_ROOT, hookRunLog);
|
|
203
|
-
if (skillsCliEntry) {
|
|
204
|
-
const key = `${skillsCliEntry.file_type}\t${skillsCliEntry.file_path}`;
|
|
205
|
-
if (!existingPaths.has(key)) {
|
|
206
|
-
existingPaths.add(key);
|
|
207
|
-
configFiles.push(skillsCliEntry);
|
|
208
|
-
}
|
|
106
|
+
addUnseen(configFiles, seen, collectClaudeDesktopExtensionManifests(HOME_DIR));
|
|
209
107
|
}
|
|
108
|
+
}
|
|
109
|
+
function filterToActiveWorktrees(configFiles) {
|
|
210
110
|
const worktreeReport = scanActiveWorktrees(PROJECT_ROOT, HOME_DIR);
|
|
211
111
|
const activeRoots = activeWorktreeRootSet(worktreeReport);
|
|
212
112
|
const beforeFilter = configFiles.length;
|
|
@@ -214,6 +114,28 @@ async function collectAllConfigFiles(endpointBase) {
|
|
|
214
114
|
hookRunLog(`worktree_scan: active=${worktreeReport.length} config_files ${beforeFilter}→${filtered.length}`);
|
|
215
115
|
return { configFiles: filtered, worktreeReport };
|
|
216
116
|
}
|
|
117
|
+
async function collectAllConfigFiles(endpointBase) {
|
|
118
|
+
const patternsResponse = await fetchPatterns(endpointBase);
|
|
119
|
+
const configFiles = scanPatterns(patternsResponse);
|
|
120
|
+
const seen = new Set(configFiles.map(configFileKey));
|
|
121
|
+
addMcpToolCaches(patternsResponse, configFiles, seen);
|
|
122
|
+
hookRunLog(`scanning installed plugins`);
|
|
123
|
+
const clientPathConstants = patternsResponse.client_path_constants;
|
|
124
|
+
if (!clientPathConstants)
|
|
125
|
+
throw new Error('client_path_constants required from API response');
|
|
126
|
+
configFiles.push(...collectConfigFilesFromInstalledPlugins(clientPathConstants));
|
|
127
|
+
hookRunLog(`scanning Claude plugin cache MCP manifests`);
|
|
128
|
+
addUnseen(configFiles, seen, collectPluginCacheMcpFiles(clientPathConstants, HOME_DIR));
|
|
129
|
+
for (const [message, collect] of HOME_COLLECTORS) {
|
|
130
|
+
hookRunLog(message);
|
|
131
|
+
addUnseen(configFiles, seen, collect(HOME_DIR));
|
|
132
|
+
}
|
|
133
|
+
addClaudeDesktopExtensions(configFiles, seen);
|
|
134
|
+
const skillsCliEntry = collectSkillsCliInstalled(PROJECT_ROOT, hookRunLog);
|
|
135
|
+
if (skillsCliEntry)
|
|
136
|
+
addUnseen(configFiles, seen, [skillsCliEntry]);
|
|
137
|
+
return filterToActiveWorktrees(configFiles);
|
|
138
|
+
}
|
|
217
139
|
/** Adds the sensitive paths audit to the batch; false when it could not be run (nothing added). */
|
|
218
140
|
async function addSensitivePathsAudit(endpointBase, configFiles) {
|
|
219
141
|
hookRunLog(`running sensitive paths audit`);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Paths and JSON persistence for ~/.optimuslabs/management:
|
|
3
3
|
* - remediation_instructions.json (remediations[])
|
|
4
4
|
*/
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { OPTIMUSLABS_MANAGEMENT_REL, OPTIMUSLABS_LOGS_REL } from '../../bootstrap_constants.js';
|
|
@@ -129,7 +129,51 @@ function parseInstructionsRaw(raw) {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
export function writeRemediationInstructionsFile(file) {
|
|
132
|
-
|
|
132
|
+
writeInstructionsFile(file, new Date().toISOString());
|
|
133
|
+
}
|
|
134
|
+
function writeInstructionsFile(file, remediationsWrittenAt) {
|
|
135
|
+
// Most callers write `{ remediations }` only; keep the approval lists they didn't pass.
|
|
136
|
+
const onDisk = readRemediationInstructionsFile();
|
|
137
|
+
const approved_tenants = 'approved_tenants' in file ? file.approved_tenants : onDisk.approved_tenants;
|
|
138
|
+
const unapproved_email_domains = 'unapproved_email_domains' in file ? file.unapproved_email_domains : onDisk.unapproved_email_domains;
|
|
139
|
+
const allowed_email_domains = 'allowed_email_domains' in file ? file.allowed_email_domains : onDisk.allowed_email_domains;
|
|
140
|
+
const approved_agents = 'approved_agents' in file ? file.approved_agents : onDisk.approved_agents;
|
|
141
|
+
atomicWriteJson(getRemediationInstructionsPath(), {
|
|
142
|
+
...file,
|
|
143
|
+
...(approved_tenants === undefined ? {} : { approved_tenants }),
|
|
144
|
+
...(unapproved_email_domains === undefined ? {} : { unapproved_email_domains }),
|
|
145
|
+
...(allowed_email_domains === undefined ? {} : { allowed_email_domains }),
|
|
146
|
+
...(approved_agents === undefined ? {} : { approved_agents }),
|
|
147
|
+
remediations_written_at: remediationsWrittenAt,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/** When the remediations on disk were written: recorded, or for an older file its mtime. */
|
|
151
|
+
function remediationsWrittenAtOf(onDisk) {
|
|
152
|
+
if (onDisk.remediations_written_at)
|
|
153
|
+
return onDisk.remediations_written_at;
|
|
154
|
+
const path = getRemediationInstructionsPath();
|
|
155
|
+
return existsSync(path) ? statSync(path).mtime.toISOString() : new Date().toISOString();
|
|
156
|
+
}
|
|
157
|
+
/** Replace one approval list, keeping the remediations and when they were written. */
|
|
158
|
+
function writeApprovalList(patch) {
|
|
159
|
+
const onDisk = readRemediationInstructionsFile();
|
|
160
|
+
writeInstructionsFile({ ...onDisk, ...patch }, remediationsWrittenAtOf(onDisk));
|
|
161
|
+
}
|
|
162
|
+
/** Replace only the unapproved email domains, keeping everything else on disk. */
|
|
163
|
+
export function writeUnapprovedEmailDomains(unapproved_email_domains) {
|
|
164
|
+
writeApprovalList({ unapproved_email_domains });
|
|
165
|
+
}
|
|
166
|
+
/** Replace only the approved agents, keeping everything else on disk. */
|
|
167
|
+
export function writeApprovedAgents(approved_agents) {
|
|
168
|
+
writeApprovalList({ approved_agents });
|
|
169
|
+
}
|
|
170
|
+
/** Replace only the email allowlist, keeping everything else on disk. */
|
|
171
|
+
export function writeAllowedEmailDomains(allowed_email_domains) {
|
|
172
|
+
writeApprovalList({ allowed_email_domains });
|
|
173
|
+
}
|
|
174
|
+
/** Replace only the approved-tenant list, keeping the remediations already on disk. */
|
|
175
|
+
export function writeApprovedTenants(approved_tenants) {
|
|
176
|
+
writeApprovalList({ approved_tenants });
|
|
133
177
|
}
|
|
134
178
|
export function readRemediationInstructionsFile() {
|
|
135
179
|
const path = getRemediationInstructionsPath();
|
|
@@ -4,7 +4,7 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { executeBody } from '../../endpoint_client/http_transport.js';
|
|
5
5
|
import { parseJsonWithJsoncFallback, parseTomlText, stringifyToml } from '../readers/file_readers.js';
|
|
6
6
|
import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
|
|
7
|
-
import { atomicWriteJson, getDeferredVscdbApplyPath, getFileCollectionVscdbContractPath, getRemediationInstructionsPath, readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
|
|
7
|
+
import { atomicWriteJson, getDeferredVscdbApplyPath, getFileCollectionVscdbContractPath, getRemediationInstructionsPath, readRemediationInstructionsFile, writeApprovedTenants, writeUnapprovedEmailDomains, writeAllowedEmailDomains, writeApprovedAgents, writeRemediationInstructionsFile, } from './management_storage.js';
|
|
8
8
|
import { readStoredAuthKey } from '../auth/auth_key_store.js';
|
|
9
9
|
import { ensureAuthentication } from '../auth/auth_flow.js';
|
|
10
10
|
import { createSignature } from '../sender/signing.js';
|
|
@@ -1600,123 +1600,172 @@ export function reportAutofixApplied(remediationUuid, result, details) {
|
|
|
1600
1600
|
// ---------------------------------------------------------------------------
|
|
1601
1601
|
// Main entry point
|
|
1602
1602
|
// ---------------------------------------------------------------------------
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1603
|
+
const errorText = (err) => (err instanceof Error ? err.message : String(err));
|
|
1604
|
+
/** The strings in a synced list, lowercased; anything else in it is dropped. */
|
|
1605
|
+
function lowercaseStrings(list) {
|
|
1606
|
+
return list.filter((v) => typeof v === 'string').map((v) => v.toLowerCase());
|
|
1607
|
+
}
|
|
1608
|
+
async function ensureVscdbContract(endpointBase) {
|
|
1607
1609
|
try {
|
|
1608
|
-
if (
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1610
|
+
if (existsSync(getFileCollectionVscdbContractPath()))
|
|
1611
|
+
return;
|
|
1612
|
+
const pr = await getFileCollectionPatterns(endpointBase);
|
|
1613
|
+
if (pr)
|
|
1614
|
+
persistVscdbComposerContractFromPatternsResponse(pr);
|
|
1613
1615
|
}
|
|
1614
1616
|
catch {
|
|
1615
1617
|
/* contract also written on full log-config run */
|
|
1616
1618
|
}
|
|
1619
|
+
}
|
|
1620
|
+
/** The server's sync answer, or null (logged) when the request failed or came back empty. */
|
|
1621
|
+
async function fetchSyncResult(endpointBase, machineUuid, activeUuids) {
|
|
1617
1622
|
let syncResult;
|
|
1618
1623
|
try {
|
|
1619
1624
|
syncResult = await fetchSync(endpointBase, machineUuid, activeUuids);
|
|
1620
1625
|
}
|
|
1621
1626
|
catch (err) {
|
|
1622
|
-
const msg = `remediation_sync_error: ${
|
|
1627
|
+
const msg = `remediation_sync_error: ${errorText(err)}`;
|
|
1623
1628
|
hookRunLog(msg);
|
|
1624
1629
|
complianceRunnerDiag(msg);
|
|
1625
|
-
return;
|
|
1630
|
+
return null;
|
|
1626
1631
|
}
|
|
1627
1632
|
if (!syncResult) {
|
|
1628
1633
|
hookRunLog(`remediation_sync: no response from server, skipping`);
|
|
1629
1634
|
complianceRunnerDiag('remediation_sync: no response from server (non-200 or empty body), skipping');
|
|
1630
|
-
return;
|
|
1631
1635
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1636
|
+
return syncResult;
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Kept locally so the prompt gate checks the signed-in tenant without a round trip; a failed
|
|
1640
|
+
* sync never gets here and leaves the last lists in place.
|
|
1641
|
+
*/
|
|
1642
|
+
function storeApprovals(syncResult) {
|
|
1643
|
+
const approvedTenants = syncResult.approved_tenants;
|
|
1644
|
+
if (approvedTenants && typeof approvedTenants === 'object' && !Array.isArray(approvedTenants)) {
|
|
1645
|
+
writeApprovedTenants(approvedTenants);
|
|
1646
|
+
}
|
|
1647
|
+
const unapprovedDomains = syncResult.unapproved_email_domains;
|
|
1648
|
+
if (Array.isArray(unapprovedDomains))
|
|
1649
|
+
writeUnapprovedEmailDomains(lowercaseStrings(unapprovedDomains));
|
|
1650
|
+
const approvedAgents = syncResult.approved_agents;
|
|
1651
|
+
if (Array.isArray(approvedAgents))
|
|
1652
|
+
writeApprovedAgents(lowercaseStrings(approvedAgents));
|
|
1653
|
+
const allowedDomains = syncResult.allowed_email_domains;
|
|
1654
|
+
if (Array.isArray(allowedDomains))
|
|
1655
|
+
writeAllowedEmailDomains(lowercaseStrings(allowedDomains));
|
|
1656
|
+
}
|
|
1657
|
+
async function reconcileInstructions(endpointBase, machineUuid, want) {
|
|
1658
|
+
try {
|
|
1659
|
+
await ensureInstructionsForUuids(endpointBase, machineUuid, want, null);
|
|
1660
|
+
}
|
|
1661
|
+
catch (err) {
|
|
1662
|
+
hookRunLog(`remediation_instructions_reconcile_error: ${errorText(err)}`);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
/** Server says nothing changed: still repair a local file that is missing rows, incomplete or stale. */
|
|
1666
|
+
async function reconcileUnchanged(endpointBase, machineUuid, remediations, activeUuids) {
|
|
1667
|
+
hookRunLog(`remediation_sync: unchanged enforced=${activeUuids.length}`);
|
|
1668
|
+
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`);
|
|
1669
|
+
const want = new Set(activeUuids);
|
|
1670
|
+
if (activeUuids.length === 0) {
|
|
1671
|
+
if (remediations.length > 0)
|
|
1672
|
+
await reconcileInstructions(endpointBase, machineUuid, new Set());
|
|
1660
1673
|
return;
|
|
1661
1674
|
}
|
|
1662
|
-
const
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
const
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
let manifest
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
}
|
|
1698
|
-
return;
|
|
1699
|
-
}
|
|
1675
|
+
const have = new Map(remediations.map((r) => [r.uuid, r]));
|
|
1676
|
+
const needBackfill = activeUuids.some((u) => !have.has(u));
|
|
1677
|
+
const haveIncomplete = remediations.some((r) => want.has(r.uuid) && needsManifestRefetch(r));
|
|
1678
|
+
const haveOrphan = remediations.some((r) => !want.has(r.uuid));
|
|
1679
|
+
if (needBackfill || haveIncomplete || haveOrphan || !existsSync(getRemediationInstructionsPath())) {
|
|
1680
|
+
await reconcileInstructions(endpointBase, machineUuid, want);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
function writeSortedInstructions(remediations) {
|
|
1684
|
+
remediations.sort((a, b) => a.uuid.localeCompare(b.uuid));
|
|
1685
|
+
writeInstructions({ remediations });
|
|
1686
|
+
}
|
|
1687
|
+
/** The manifest rows for the added UUIDs; null (logged) when it could not be fetched. */
|
|
1688
|
+
async function fetchAddedManifest(endpointBase, machineUuid, added) {
|
|
1689
|
+
let manifest;
|
|
1690
|
+
try {
|
|
1691
|
+
manifest = await fetchManifest(endpointBase, machineUuid, added);
|
|
1692
|
+
}
|
|
1693
|
+
catch (err) {
|
|
1694
|
+
const msg = `remediation_manifest_error: ${errorText(err)}`;
|
|
1695
|
+
hookRunLog(msg);
|
|
1696
|
+
complianceRunnerDiag(msg);
|
|
1697
|
+
return null;
|
|
1698
|
+
}
|
|
1699
|
+
if (!manifest) {
|
|
1700
|
+
hookRunLog(`remediation_sync: manifest fetch failed, skipping`);
|
|
1701
|
+
complianceRunnerDiag('remediation_sync: manifest fetch failed (null), skipping — remediation_instructions.json not updated');
|
|
1702
|
+
}
|
|
1703
|
+
return manifest;
|
|
1704
|
+
}
|
|
1705
|
+
function upsertInstruction(remediations, existingUuids, instruction) {
|
|
1706
|
+
if (!existingUuids.has(instruction.uuid)) {
|
|
1707
|
+
remediations.push(instruction);
|
|
1708
|
+
existingUuids.add(instruction.uuid);
|
|
1709
|
+
return;
|
|
1700
1710
|
}
|
|
1711
|
+
// Enforced remediations are always in `added`; overwrite local copy with fresh server data.
|
|
1712
|
+
const idx = remediations.findIndex((r) => r.uuid === instruction.uuid);
|
|
1713
|
+
if (idx >= 0)
|
|
1714
|
+
remediations[idx] = instruction;
|
|
1715
|
+
}
|
|
1716
|
+
/** Put the manifest's added rows into `remediations` (replacing a local copy); returns the rows put in. */
|
|
1717
|
+
function mergeAddedInstructions(remediations, manifest, added) {
|
|
1701
1718
|
const existingUuids = new Set(remediations.map((r) => r.uuid));
|
|
1702
1719
|
const addedSet = new Set(added);
|
|
1703
1720
|
const overlay = [];
|
|
1704
|
-
for (const instruction of manifest
|
|
1721
|
+
for (const instruction of manifest) {
|
|
1705
1722
|
if (!addedSet.has(instruction.uuid))
|
|
1706
1723
|
continue;
|
|
1707
|
-
|
|
1708
|
-
// Enforced remediations are always in `added`; overwrite local copy with fresh server data.
|
|
1709
|
-
const idx = remediations.findIndex((r) => r.uuid === instruction.uuid);
|
|
1710
|
-
if (idx >= 0)
|
|
1711
|
-
remediations[idx] = instruction;
|
|
1712
|
-
}
|
|
1713
|
-
else {
|
|
1714
|
-
remediations.push(instruction);
|
|
1715
|
-
existingUuids.add(instruction.uuid);
|
|
1716
|
-
}
|
|
1724
|
+
upsertInstruction(remediations, existingUuids, instruction);
|
|
1717
1725
|
overlay.push(instruction);
|
|
1718
1726
|
hookRunLog(`remediation_instructions_saved: uuid=${instruction.uuid} path=${instruction.config_file_path}`);
|
|
1719
1727
|
}
|
|
1728
|
+
return overlay;
|
|
1729
|
+
}
|
|
1730
|
+
/** Write the merged instructions, fetch anything still missing, and report the new UUID set. */
|
|
1731
|
+
async function persistSyncedInstructions(endpointBase, machineUuid, remediations, overlay, removedCount) {
|
|
1732
|
+
writeSortedInstructions(remediations);
|
|
1733
|
+
complianceRunnerDiag(`remediation_sync: wrote ${getRemediationInstructionsPath()} rows=${remediations.length} (added_from_manifest=${overlay.length} removed=${removedCount})`);
|
|
1734
|
+
const finalWant = new Set(remediations.map((r) => r.uuid));
|
|
1735
|
+
try {
|
|
1736
|
+
await ensureInstructionsForUuids(endpointBase, machineUuid, finalWant, overlay.length > 0 ? overlay : null);
|
|
1737
|
+
}
|
|
1738
|
+
catch (err) {
|
|
1739
|
+
hookRunLog(`remediation_instructions_persist_error: ${errorText(err)}`);
|
|
1740
|
+
}
|
|
1741
|
+
// Post-sync heartbeat: report the updated UUID set so the server has a before/after pair.
|
|
1742
|
+
const finalUuids = remediations.map((r) => r.uuid);
|
|
1743
|
+
try {
|
|
1744
|
+
await fetchSync(endpointBase, machineUuid, finalUuids);
|
|
1745
|
+
hookRunLog(`remediation_sync: post-sync heartbeat reported uuids=${finalUuids.length}`);
|
|
1746
|
+
}
|
|
1747
|
+
catch {
|
|
1748
|
+
// Non-critical — skip silently.
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
/** Server sent added/removed UUIDs: drop the removed, fetch and merge the added, then persist. */
|
|
1752
|
+
async function applySyncUpdate(endpointBase, machineUuid, local, syncResult) {
|
|
1753
|
+
const { added, removed } = syncResult;
|
|
1754
|
+
hookRunLog(`remediation_sync: status=updated added=${added.length} removed=${removed.length}`);
|
|
1755
|
+
complianceRunnerDiag(`remediation_sync: server status=updated added=${JSON.stringify(added)} removed=${JSON.stringify(removed)}`);
|
|
1756
|
+
const removedSet = new Set(removed);
|
|
1757
|
+
const retiredInstructions = local.filter((r) => removedSet.has(r.uuid));
|
|
1758
|
+
for (const r of retiredInstructions)
|
|
1759
|
+
hookRunLog(`remediation_remove: uuid=${r.uuid} path=${r.config_file_path}`);
|
|
1760
|
+
const removedCount = retiredInstructions.length;
|
|
1761
|
+
const remediations = local.filter((r) => !removedSet.has(r.uuid));
|
|
1762
|
+
const manifest = added.length > 0 ? await fetchAddedManifest(endpointBase, machineUuid, added) : [];
|
|
1763
|
+
if (!manifest) {
|
|
1764
|
+
if (removedCount > 0)
|
|
1765
|
+
writeSortedInstructions(remediations);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
const overlay = mergeAddedInstructions(remediations, manifest, added);
|
|
1720
1769
|
transferQuarantineForRetiredUuids(removed, added, retiredInstructions, overlay);
|
|
1721
1770
|
const addedCount = overlay.length;
|
|
1722
1771
|
if (added.length > 0 && addedCount === 0) {
|
|
@@ -1725,26 +1774,24 @@ export async function syncRemediations(endpointBase, machineUuid) {
|
|
|
1725
1774
|
complianceRunnerDiag(`${msg} — remediation_instructions.json not written`);
|
|
1726
1775
|
}
|
|
1727
1776
|
if (removedCount > 0 || addedCount > 0) {
|
|
1728
|
-
|
|
1729
|
-
writeInstructions({ remediations });
|
|
1730
|
-
complianceRunnerDiag(`remediation_sync: wrote ${getRemediationInstructionsPath()} rows=${remediations.length} (added_from_manifest=${addedCount} removed=${removedCount})`);
|
|
1731
|
-
const finalWant = new Set(remediations.map((r) => r.uuid));
|
|
1732
|
-
try {
|
|
1733
|
-
await ensureInstructionsForUuids(endpointBase, machineUuid, finalWant, overlay.length > 0 ? overlay : null);
|
|
1734
|
-
}
|
|
1735
|
-
catch (err) {
|
|
1736
|
-
hookRunLog(`remediation_instructions_persist_error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1737
|
-
}
|
|
1738
|
-
// Post-sync heartbeat: report the updated UUID set so the server has a before/after pair.
|
|
1739
|
-
const finalUuids = remediations.map((r) => r.uuid);
|
|
1740
|
-
try {
|
|
1741
|
-
await fetchSync(endpointBase, machineUuid, finalUuids);
|
|
1742
|
-
hookRunLog(`remediation_sync: post-sync heartbeat reported uuids=${finalUuids.length}`);
|
|
1743
|
-
}
|
|
1744
|
-
catch {
|
|
1745
|
-
// Non-critical — skip silently.
|
|
1746
|
-
}
|
|
1777
|
+
await persistSyncedInstructions(endpointBase, machineUuid, remediations, overlay, removedCount);
|
|
1747
1778
|
}
|
|
1748
1779
|
hookRunLog(`remediation_sync: saved=${addedCount} removed=${removedCount}`);
|
|
1749
1780
|
complianceRunnerDiag(`remediation_sync: done saved=${addedCount} removed=${removedCount}`);
|
|
1750
1781
|
}
|
|
1782
|
+
export async function syncRemediations(endpointBase, machineUuid) {
|
|
1783
|
+
const remediations = readInstructions().remediations;
|
|
1784
|
+
const activeUuids = remediations.map((r) => r.uuid);
|
|
1785
|
+
complianceRunnerDiag(`remediation_sync: begin endpoint=${endpointBase} machine_uuid=${machineUuid} local_rows=${remediations.length}`);
|
|
1786
|
+
await ensureVscdbContract(endpointBase);
|
|
1787
|
+
const syncResult = await fetchSyncResult(endpointBase, machineUuid, activeUuids);
|
|
1788
|
+
if (!syncResult)
|
|
1789
|
+
return;
|
|
1790
|
+
applyClearApplyTrackingUuids(syncResult.clear_apply_tracking_uuids);
|
|
1791
|
+
storeApprovals(syncResult);
|
|
1792
|
+
if (syncResult.status === 'unchanged') {
|
|
1793
|
+
await reconcileUnchanged(endpointBase, machineUuid, remediations, activeUuids);
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
await applySyncUpdate(endpointBase, machineUuid, remediations, syncResult);
|
|
1797
|
+
}
|