@optimuslabs/harness-map-staging 1.5.19 → 1.5.21
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/log_config_files/runtime/main_runner.js +27 -0
- package/dist/log_config_files/runtime/remediation_sync.js +35 -5
- package/dist/log_config_files/runtime/secret_regex_scan.js +3 -0
- package/dist/log_path_permissions_audit.js +169 -0
- package/dist/log_uuid/user_profile.js +9 -1
- package/package.json +3 -3
|
@@ -5,6 +5,7 @@ const HOME_DIR = homedir();
|
|
|
5
5
|
import { getFileCollectionPatterns, FILE_PATH_REGISTRY_FILE_PATTERNS_PATH } from '../../endpoint_client/index.js';
|
|
6
6
|
import { OPTIMUSLABS_LOGS_REL, LOG_GROUP_AUDIT } from '../../bootstrap_constants.js';
|
|
7
7
|
import { runSensitivePathsAudit } from '../../log_sensitive_paths_audit.js';
|
|
8
|
+
import { buildPathPermissionsAuditEntry, grantPathsFromConfig } from '../../log_path_permissions_audit.js';
|
|
8
9
|
import { loadEndpointBase, getEndpointSource } from '../sender/endpoint_config.js';
|
|
9
10
|
import { hookLogReplace, hookRunLog, hookLogError, readHookLog } from './hook_logger.js';
|
|
10
11
|
import { getStoryId, flushBufferedClientEvents, reportFunctionalRun, reportFunctionalFailureAwait, } from './client_event_reporter.js';
|
|
@@ -227,6 +228,31 @@ async function addSensitivePathsAudit(endpointBase, configFiles) {
|
|
|
227
228
|
configFiles.push({ file_type: 'sensitive_paths_audit', file_path: 'sensitive_paths_audit.txt', raw_content: { paths: [] } });
|
|
228
229
|
}
|
|
229
230
|
}
|
|
231
|
+
/** POSIX mode of every collected file, plus granted folders (and their ancestors) and sensitive audit hits within a budget. stat() only. */
|
|
232
|
+
function addPathPermissionsAudit(configFiles) {
|
|
233
|
+
const visited = [];
|
|
234
|
+
const grants = [];
|
|
235
|
+
const sensitive = [];
|
|
236
|
+
for (const c of configFiles) {
|
|
237
|
+
if (c.file_type === 'sensitive_paths_audit') {
|
|
238
|
+
const auditPaths = c.raw_content.paths;
|
|
239
|
+
if (Array.isArray(auditPaths))
|
|
240
|
+
sensitive.push(...auditPaths.filter((p) => typeof p === 'string'));
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
visited.push(c.file_path);
|
|
244
|
+
grants.push(...grantPathsFromConfig(c.raw_content));
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const entry = buildPathPermissionsAuditEntry(visited, grants, sensitive);
|
|
248
|
+
hookRunLog(`path_permissions_audit: ${entry.raw_content.entries.length} path(s) grants=${grants.length} truncated=${entry.raw_content.truncated}`);
|
|
249
|
+
configFiles.push(entry);
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
hookRunLog(`path_permissions_audit_error: ${err instanceof Error ? err.message : String(err)}`);
|
|
253
|
+
hookLogError('path_permissions_audit', err, {});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
230
256
|
async function sendAllConfigFiles(configFiles, worktreeReport, hardwareUuid, authKey) {
|
|
231
257
|
const hookType = resolveHookTypeFromEnv();
|
|
232
258
|
const manifest = configFiles.map((c) => canonicalCursorUserStateVscdbPath(c.file_path));
|
|
@@ -309,6 +335,7 @@ async function main() {
|
|
|
309
335
|
}
|
|
310
336
|
await addSensitivePathsAudit(endpointBase, configFiles);
|
|
311
337
|
ensureCursorUserSettingsSnapshotInBatch(configFiles);
|
|
338
|
+
addPathPermissionsAudit(configFiles);
|
|
312
339
|
const fileTypes = [...new Set(configFiles.map((c) => c.file_type))].join(', ');
|
|
313
340
|
hookRunLog(`collected ${configFiles.length} config file(s) file_types=${fileTypes}`);
|
|
314
341
|
for (const c of configFiles) {
|
|
@@ -264,16 +264,46 @@ async function fetchManifest(endpointBase, machineUuid, uuids, timeoutMs = 8000)
|
|
|
264
264
|
// Enforce / remove
|
|
265
265
|
// ---------------------------------------------------------------------------
|
|
266
266
|
function setByPath(obj, path, value) {
|
|
267
|
-
const parts = path.split('.');
|
|
267
|
+
const parts = path.split('.').filter((part) => part.length > 0);
|
|
268
|
+
if (parts.length === 0)
|
|
269
|
+
return;
|
|
268
270
|
let current = obj;
|
|
269
271
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
270
272
|
const part = parts[i];
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
+
if (Array.isArray(current)) {
|
|
274
|
+
if (!/^\d+$/.test(part))
|
|
275
|
+
return;
|
|
276
|
+
const idx = Number(part);
|
|
277
|
+
if (idx >= current.length)
|
|
278
|
+
return;
|
|
279
|
+
current = current[idx];
|
|
280
|
+
continue;
|
|
273
281
|
}
|
|
274
|
-
current
|
|
282
|
+
if (current == null || typeof current !== 'object')
|
|
283
|
+
return;
|
|
284
|
+
const rec = current;
|
|
285
|
+
const next = rec[part];
|
|
286
|
+
if (Array.isArray(next)) {
|
|
287
|
+
current = next;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (next == null || typeof next !== 'object') {
|
|
291
|
+
rec[part] = {};
|
|
292
|
+
}
|
|
293
|
+
current = rec[part];
|
|
294
|
+
}
|
|
295
|
+
const leaf = parts[parts.length - 1];
|
|
296
|
+
if (Array.isArray(current)) {
|
|
297
|
+
if (!/^\d+$/.test(leaf))
|
|
298
|
+
return;
|
|
299
|
+
const idx = Number(leaf);
|
|
300
|
+
if (idx >= 0 && idx < current.length)
|
|
301
|
+
current[idx] = value;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (current != null && typeof current === 'object') {
|
|
305
|
+
current[leaf] = value;
|
|
275
306
|
}
|
|
276
|
-
current[parts[parts.length - 1]] = value;
|
|
277
307
|
}
|
|
278
308
|
/** Traverse a JSON object using dot-notation path. Returns undefined if any segment is missing. */
|
|
279
309
|
function getByPath(obj, path) {
|
|
@@ -57,6 +57,9 @@ const KNOWN_SECRET_PATTERNS = [
|
|
|
57
57
|
{ re: /dop_v1_[a-f0-9]{64}/i, label: 'DigitalOcean personal access token' },
|
|
58
58
|
{ re: /pat_[a-zA-Z0-9]{22}_[a-fA-F0-9]{59}/i, label: 'Atlassian API token' },
|
|
59
59
|
{ re: /Bearer\s+[a-zA-Z0-9\-_.]{20,}/i, label: 'Bearer token' },
|
|
60
|
+
// Isolated 32-hex (Azure OpenAI). Lookarounds keep a longer hex string from matching.
|
|
61
|
+
// Same pattern as HardcodedSecretRule — MCP tokens in args (ISO-005) are this shape.
|
|
62
|
+
{ re: /(?<![a-zA-Z0-9])[a-fA-F0-9]{32}(?![a-zA-Z0-9])/, label: 'Azure OpenAI key' },
|
|
60
63
|
];
|
|
61
64
|
const BASIC_AUTH_IN_URL = /[a-zA-Z0-9._%+-]+:[a-zA-Z0-9._%+-]+@/gi;
|
|
62
65
|
// Hyphens/underscores count as part of one token so hyphenated keys (ctx7sk-..., sk-ant-...)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path permissions audit: POSIX mode of home, `/` and every file the collector uploads,
|
|
3
|
+
* plus, within MAX_EXTRA_ENTRIES and in this priority order:
|
|
4
|
+
*
|
|
5
|
+
* 1. each folder an agent config grants (additionalDirectories, Read()/Edit() allow rules,
|
|
6
|
+
* trustedFolders, external_directory) and its ancestors (an ancestor the user can't
|
|
7
|
+
* enter makes everything under it unreachable). Never the folder's contents, so a
|
|
8
|
+
* grant of ~ or / costs a handful of stats, not a walk of the disk.
|
|
9
|
+
* 2. sensitive paths audit hits
|
|
10
|
+
* 3. the direct children of each sensitive directory (e.g. ~/.ssh/id_rsa), capped per dir
|
|
11
|
+
*
|
|
12
|
+
* stat()/readdir() only — never reads file contents. Sent in the same batch as the config
|
|
13
|
+
* files as file_type 'path_permissions_audit'.
|
|
14
|
+
*/
|
|
15
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
18
|
+
export const PATH_PERMISSIONS_AUDIT_FILENAME = 'path_permissions_audit.txt';
|
|
19
|
+
export const PATH_PERMISSIONS_AUDIT_FILE_TYPE = 'path_permissions_audit';
|
|
20
|
+
/** Upper bound on entries beyond the collected files, whatever is granted. */
|
|
21
|
+
export const MAX_EXTRA_ENTRIES = 500;
|
|
22
|
+
/** Owner permission triad from st_mode (e.g. 0o640 → "rw-"). */
|
|
23
|
+
export function ownerTriad(mode) {
|
|
24
|
+
return `${mode & 0o400 ? 'r' : '-'}${mode & 0o200 ? 'w' : '-'}${mode & 0o100 ? 'x' : '-'}`;
|
|
25
|
+
}
|
|
26
|
+
/** Strip virtual suffixes some collectors append (e.g. state.vscdb#composerState). */
|
|
27
|
+
function diskPath(filePath) {
|
|
28
|
+
const hash = filePath.indexOf('#');
|
|
29
|
+
return hash >= 0 ? filePath.slice(0, hash) : filePath;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* stat each distinct absolute path, stopping once ``limit`` entries are collected; paths
|
|
33
|
+
* that no longer exist or can't be stat'd are skipped. ``truncated`` when paths were left.
|
|
34
|
+
*/
|
|
35
|
+
export function collectPathPermissions(paths, limit = Infinity) {
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
const entries = [];
|
|
38
|
+
for (const raw of paths) {
|
|
39
|
+
const path = diskPath(raw);
|
|
40
|
+
if (!path || !isAbsolute(path) || seen.has(path))
|
|
41
|
+
continue;
|
|
42
|
+
if (entries.length >= limit)
|
|
43
|
+
return { entries, truncated: true };
|
|
44
|
+
seen.add(path);
|
|
45
|
+
try {
|
|
46
|
+
const st = statSync(path);
|
|
47
|
+
const perm = st.mode & 0o7777;
|
|
48
|
+
entries.push({
|
|
49
|
+
path,
|
|
50
|
+
is_dir: st.isDirectory(),
|
|
51
|
+
mode: perm.toString(8).padStart(4, '0'),
|
|
52
|
+
owner: ownerTriad(perm),
|
|
53
|
+
uid: st.uid,
|
|
54
|
+
gid: st.gid,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Gone or unreadable since collection; nothing to report.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { entries, truncated: false };
|
|
62
|
+
}
|
|
63
|
+
/** Cap per sensitive directory so a large ~/.config can't bloat the upload. */
|
|
64
|
+
export const MAX_CHILDREN_PER_SENSITIVE_DIR = 50;
|
|
65
|
+
/** Direct children of a directory (names only, sorted, capped); [] when not a readable dir. */
|
|
66
|
+
export function sensitiveDirChildren(dir) {
|
|
67
|
+
try {
|
|
68
|
+
return readdirSync(dir)
|
|
69
|
+
.sort()
|
|
70
|
+
.slice(0, MAX_CHILDREN_PER_SENSITIVE_DIR)
|
|
71
|
+
.map((name) => join(dir, name));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function stringList(value) {
|
|
78
|
+
if (typeof value === 'string')
|
|
79
|
+
return [value];
|
|
80
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === 'string') : [];
|
|
81
|
+
}
|
|
82
|
+
const FILE_RULE = /^\s*(Read|Edit|Write|MultiEdit|NotebookEdit)\((.+)\)\s*$/;
|
|
83
|
+
/**
|
|
84
|
+
* Folders a config grants, as written: Claude permissions.additionalDirectories and
|
|
85
|
+
* Read()/Edit() allow rules, Copilot trustedFolders, OpenCode permission.external_directory.
|
|
86
|
+
*/
|
|
87
|
+
export function grantPathsFromConfig(raw) {
|
|
88
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
89
|
+
return [];
|
|
90
|
+
const config = raw;
|
|
91
|
+
const grants = [];
|
|
92
|
+
const permissions = config.permissions;
|
|
93
|
+
if (permissions && typeof permissions === 'object') {
|
|
94
|
+
const p = permissions;
|
|
95
|
+
grants.push(...stringList(p.additionalDirectories));
|
|
96
|
+
for (const rule of stringList(p.allow)) {
|
|
97
|
+
const match = FILE_RULE.exec(rule);
|
|
98
|
+
if (match)
|
|
99
|
+
grants.push(match[2].trim());
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
grants.push(...stringList(config.trustedFolders));
|
|
103
|
+
const permission = config.permission;
|
|
104
|
+
if (permission && typeof permission === 'object') {
|
|
105
|
+
const external = permission.external_directory;
|
|
106
|
+
if (external && typeof external === 'object' && !Array.isArray(external)) {
|
|
107
|
+
for (const [pattern, action] of Object.entries(external)) {
|
|
108
|
+
if (action !== 'deny')
|
|
109
|
+
grants.push(pattern);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
grants.push(...stringList(external));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return grants;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A grant as an absolute folder on this machine: ``~/x`` under home, ``//x`` → ``/x``, the
|
|
120
|
+
* part before any glob (``~/.kube/**`` → ``~/.kube``). Null for relative grants.
|
|
121
|
+
*/
|
|
122
|
+
export function grantFolder(grant, home) {
|
|
123
|
+
let path = grant.trim();
|
|
124
|
+
if (path.startsWith('//'))
|
|
125
|
+
path = path.slice(1);
|
|
126
|
+
if (path === '~' || path.startsWith('~/'))
|
|
127
|
+
path = home + path.slice(1);
|
|
128
|
+
if (!isAbsolute(path))
|
|
129
|
+
return null;
|
|
130
|
+
const segments = path.split('/');
|
|
131
|
+
const glob = segments.findIndex((s) => /[*?[]/.test(s));
|
|
132
|
+
const folder = (glob >= 0 ? segments.slice(0, glob) : segments).join('/').replace(/\/+$/, '');
|
|
133
|
+
return folder || '/';
|
|
134
|
+
}
|
|
135
|
+
/** The folder and each ancestor up to ``/``. */
|
|
136
|
+
function withAncestors(folder) {
|
|
137
|
+
const chain = [folder];
|
|
138
|
+
for (let p = folder; p !== '/';) {
|
|
139
|
+
p = dirname(p);
|
|
140
|
+
chain.push(p);
|
|
141
|
+
}
|
|
142
|
+
return chain;
|
|
143
|
+
}
|
|
144
|
+
function* extraCandidates(grants, sensitivePaths, home) {
|
|
145
|
+
for (const grant of grants) {
|
|
146
|
+
const folder = grantFolder(grant, home);
|
|
147
|
+
if (folder)
|
|
148
|
+
yield* withAncestors(folder);
|
|
149
|
+
}
|
|
150
|
+
yield* sensitivePaths;
|
|
151
|
+
// Only listed while the budget lasts: generated lazily, so no readdir past the cap.
|
|
152
|
+
for (const dir of sensitivePaths)
|
|
153
|
+
yield* sensitiveDirChildren(dir);
|
|
154
|
+
}
|
|
155
|
+
export function buildPathPermissionsAuditEntry(visitedPaths, grants = [], sensitivePaths = [], home = homedir()) {
|
|
156
|
+
const collected = collectPathPermissions([home, '/', ...visitedPaths]).entries;
|
|
157
|
+
const listed = new Set(collected.map((e) => e.path));
|
|
158
|
+
const extra = collectPathPermissions((function* () {
|
|
159
|
+
for (const p of extraCandidates(grants, sensitivePaths, home)) {
|
|
160
|
+
if (!listed.has(diskPath(p)))
|
|
161
|
+
yield p;
|
|
162
|
+
}
|
|
163
|
+
})(), MAX_EXTRA_ENTRIES);
|
|
164
|
+
return {
|
|
165
|
+
file_type: PATH_PERMISSIONS_AUDIT_FILE_TYPE,
|
|
166
|
+
file_path: PATH_PERMISSIONS_AUDIT_FILENAME,
|
|
167
|
+
raw_content: { home, entries: [...collected, ...extra.entries], truncated: extra.truncated },
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -178,6 +178,11 @@ const readMacAccountGeneratedUid = () => {
|
|
|
178
178
|
}
|
|
179
179
|
return '';
|
|
180
180
|
};
|
|
181
|
+
/** Always from the running process — env/file profile overrides predate these fields. */
|
|
182
|
+
const readProcessIdentity = () => ({
|
|
183
|
+
mac_uid: typeof process.getuid === 'function' ? process.getuid() : null,
|
|
184
|
+
mac_gids: typeof process.getgroups === 'function' ? process.getgroups() : [],
|
|
185
|
+
});
|
|
181
186
|
const buildDefaultUserProfile = () => ({
|
|
182
187
|
generateduuid: readMacAccountGeneratedUid() || crypto.randomUUID(),
|
|
183
188
|
machineuuid: resolveHardwareUuid(),
|
|
@@ -188,5 +193,8 @@ const buildDefaultUserProfile = () => ({
|
|
|
188
193
|
cursor_email: readCursorCachedEmail(),
|
|
189
194
|
});
|
|
190
195
|
/** Best-effort local user profile object for startup metadata. */
|
|
191
|
-
const resolveUserProfile = () =>
|
|
196
|
+
const resolveUserProfile = () => ({
|
|
197
|
+
...(readUserProfileFromEnvOrFile() ?? buildDefaultUserProfile()),
|
|
198
|
+
...readProcessIdentity(),
|
|
199
|
+
});
|
|
192
200
|
export { resolveUserProfile };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optimuslabs/harness-map-staging",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.21",
|
|
4
4
|
"description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
"vitest": "^4.1.8"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"axios": "^1.18.1",
|
|
60
|
-
"canonicalize": "^2.1.0",
|
|
61
59
|
"@optimuslabs/tofu-staging": "^0.1.18",
|
|
60
|
+
"axios": "^1.20.0",
|
|
61
|
+
"canonicalize": "^2.1.0",
|
|
62
62
|
"smol-toml": "^1.7.1"
|
|
63
63
|
},
|
|
64
64
|
"publishConfig": {
|