@optimuslabs/harness-map-staging 1.5.45 → 1.5.46
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/collection/plugin_collector.js +70 -55
- package/dist/log_config_files/runtime/client_event_reporter.js +12 -1
- package/dist/log_config_files/runtime/compliance_session_log.js +17 -7
- package/dist/log_config_files/runtime/prompt-collection-policy.js +42 -6
- package/dist/log_config_files/runtime/remediation_sync.js +19 -4
- package/dist/log_config_files/runtime/workspace_repo.js +10 -2
- package/dist/log_config_files/sender/batch_sender.js +9 -5
- package/dist/log_uuid/startup_sender.js +2 -0
- package/package.json +3 -2
|
@@ -20,11 +20,24 @@ const SKILL_MAX_FILE_BYTES = 1024 * 1024;
|
|
|
20
20
|
const SKILL_MAX_TOTAL_BYTES = 8 * 1024 * 1024;
|
|
21
21
|
const SKILL_MAX_FILES = 500;
|
|
22
22
|
const SKILL_MAX_DEPTH = 8;
|
|
23
|
+
// Credential-shaped names, lower case: the backend skill bundle's rule (_is_secret_name) and its
|
|
24
|
+
// credential files (CredentialFiles.FILE_NAMES), which it never stores the content of (OPT-1016).
|
|
25
|
+
const SECRET_NAMES = new Set([
|
|
26
|
+
'.env', '.npmrc', '.netrc', '.pgpass', '.git-credentials', '.htpasswd',
|
|
27
|
+
'credentials', 'credentials.json', '.credentials.json', 'auth.json', 'auth.v2.file',
|
|
28
|
+
'oauth_creds.json', 'mcp-auth.json', 'mcp_oauth_tokens.json',
|
|
29
|
+
'id_rsa', 'id_dsa', 'id_ecdsa', 'id_ed25519',
|
|
30
|
+
'secrets.json', 'secrets.yaml', 'secrets.yml', 'secrets.toml', 'secrets.env',
|
|
31
|
+
]);
|
|
32
|
+
const SECRET_SUFFIXES = ['.pem', '.key', '.p12', '.pfx', '.jks', '.keystore'];
|
|
23
33
|
/** Hidden files are collected, but credential-shaped ones are recorded without their contents. */
|
|
24
34
|
function isSecretName(name) {
|
|
25
35
|
const lower = name.toLowerCase();
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
if (SECRET_NAMES.has(lower) || lower.startsWith('.env.') || SECRET_SUFFIXES.some((s) => lower.endsWith(s)))
|
|
37
|
+
return true;
|
|
38
|
+
// Cloud credential files: OAuth client secrets and service-account keys.
|
|
39
|
+
return lower.endsWith('.json')
|
|
40
|
+
&& (lower.startsWith('client_secret') || lower.startsWith('service-account') || lower.includes('serviceaccount'));
|
|
28
41
|
}
|
|
29
42
|
function skillMetaRecord(filePath, size, sha256, ver) {
|
|
30
43
|
return {
|
|
@@ -34,72 +47,74 @@ function skillMetaRecord(filePath, size, sha256, ver) {
|
|
|
34
47
|
raw_content: { source: 'file', size, ...(sha256 ? { sha256 } : {}), ...(ver ? { version: ver } : {}) },
|
|
35
48
|
};
|
|
36
49
|
}
|
|
37
|
-
function
|
|
38
|
-
if (depth > SKILL_MAX_DEPTH)
|
|
39
|
-
return;
|
|
40
|
-
let entries;
|
|
50
|
+
function readSkillDir(dir) {
|
|
41
51
|
try {
|
|
42
|
-
|
|
52
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
43
53
|
}
|
|
44
54
|
catch (err) {
|
|
45
55
|
console.warn(`Error reading skill dir ${dir}:`, err instanceof Error ? err.message : String(err));
|
|
46
|
-
return;
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function sha256Of(full) {
|
|
60
|
+
try {
|
|
61
|
+
return createHash('sha256').update(readFileSync(full)).digest('hex');
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
47
65
|
}
|
|
48
|
-
|
|
66
|
+
}
|
|
67
|
+
/** One extra skill file as a record, or null when it is skipped or unreadable. */
|
|
68
|
+
function skillFileRecord(full, name, constants) {
|
|
69
|
+
const ext = extname(name).toLowerCase();
|
|
70
|
+
if (SKILL_SKIP_EXTS.has(ext))
|
|
71
|
+
return null;
|
|
72
|
+
let size;
|
|
73
|
+
try {
|
|
74
|
+
size = statSync(full).size;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const ver = versionFromPluginCachePath(full, constants);
|
|
80
|
+
// A credential file's bytes are never read, so it is checked before the size cap and the hash.
|
|
81
|
+
if (isSecretName(name) || size > SKILL_MAX_FILE_BYTES)
|
|
82
|
+
return skillMetaRecord(full, size, '', ver);
|
|
83
|
+
if (!SKILL_TEXT_EXTS.has(ext)) {
|
|
84
|
+
const sha256 = sha256Of(full);
|
|
85
|
+
return sha256 === null ? null : skillMetaRecord(full, size, sha256, ver);
|
|
86
|
+
}
|
|
87
|
+
const content = readMarkdownFile(full);
|
|
88
|
+
if (content === null)
|
|
89
|
+
return null;
|
|
90
|
+
return { file_type: 'claude_skill', file_path: full, raw_content: { content, source: 'file', ...(ver ? { version: ver } : {}) } };
|
|
91
|
+
}
|
|
92
|
+
function collectSkillDirFiles(dir, results, constants, skillFilename, budget, depth) {
|
|
93
|
+
if (depth > SKILL_MAX_DEPTH)
|
|
94
|
+
return;
|
|
95
|
+
for (const ent of readSkillDir(dir) ?? []) {
|
|
49
96
|
if (budget.files >= SKILL_MAX_FILES || budget.bytes >= SKILL_MAX_TOTAL_BYTES)
|
|
50
97
|
return;
|
|
51
98
|
const full = join(dir, ent.name);
|
|
52
|
-
if (ent.isDirectory()) {
|
|
53
|
-
|
|
54
|
-
collectSkillDirFiles(full, results, constants, skillFilename, budget, depth + 1);
|
|
99
|
+
if (ent.isDirectory() && !SKILL_SKIP_DIRS.has(ent.name)) {
|
|
100
|
+
collectSkillDirFiles(full, results, constants, skillFilename, budget, depth + 1);
|
|
55
101
|
continue;
|
|
56
102
|
}
|
|
57
|
-
if (ent.name === skillFilename)
|
|
58
|
-
continue;
|
|
59
103
|
// Extra skill files: skip nested symlinks so the walk stays inside this skill dir.
|
|
60
|
-
if (!ent.isFile())
|
|
61
|
-
continue;
|
|
62
|
-
const ext = extname(ent.name).toLowerCase();
|
|
63
|
-
if (SKILL_SKIP_EXTS.has(ext))
|
|
64
|
-
continue;
|
|
65
|
-
let size;
|
|
66
|
-
try {
|
|
67
|
-
size = statSync(full).size;
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
const ver = versionFromPluginCachePath(full, constants);
|
|
73
|
-
if (size > SKILL_MAX_FILE_BYTES) {
|
|
74
|
-
results.push(skillMetaRecord(full, size, '', ver));
|
|
75
|
-
budget.files += 1;
|
|
104
|
+
if (ent.name === skillFilename || !ent.isFile())
|
|
76
105
|
continue;
|
|
77
|
-
|
|
78
|
-
if (!SKILL_TEXT_EXTS.has(ext) && ent.name !== skillFilename) {
|
|
79
|
-
let sha256 = '';
|
|
80
|
-
try {
|
|
81
|
-
sha256 = createHash('sha256').update(readFileSync(full)).digest('hex');
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
results.push(skillMetaRecord(full, size, sha256, ver));
|
|
87
|
-
budget.files += 1;
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (isSecretName(ent.name)) {
|
|
91
|
-
results.push(skillMetaRecord(full, size, '', ver));
|
|
92
|
-
budget.files += 1;
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
const content = readMarkdownFile(full);
|
|
96
|
-
if (content === null)
|
|
97
|
-
continue;
|
|
98
|
-
results.push({ file_type: 'claude_skill', file_path: full, raw_content: { content, source: 'file', ...(ver ? { version: ver } : {}) } });
|
|
99
|
-
budget.files += 1;
|
|
100
|
-
budget.bytes += Buffer.byteLength(content);
|
|
106
|
+
addSkillRecord(skillFileRecord(full, ent.name, constants), results, budget);
|
|
101
107
|
}
|
|
102
108
|
}
|
|
109
|
+
function addSkillRecord(record, results, budget) {
|
|
110
|
+
if (!record)
|
|
111
|
+
return;
|
|
112
|
+
results.push(record);
|
|
113
|
+
budget.files += 1;
|
|
114
|
+
const content = record.raw_content.content;
|
|
115
|
+
if (typeof content === 'string')
|
|
116
|
+
budget.bytes += Buffer.byteLength(content);
|
|
117
|
+
}
|
|
103
118
|
function collectSkillFiles(skillsDir, results, constants) {
|
|
104
119
|
const skillFilename = getPluginSkillFilename(constants);
|
|
105
120
|
try {
|
|
@@ -228,4 +243,4 @@ function collectConfigFilesFromInstalledPlugins(constants) {
|
|
|
228
243
|
}
|
|
229
244
|
return results;
|
|
230
245
|
}
|
|
231
|
-
export { collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles };
|
|
246
|
+
export { collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles, isSecretName };
|
|
@@ -20,6 +20,7 @@ import { createSignature } from '../sender/signing.js';
|
|
|
20
20
|
import { readStoredAuthKey } from '../auth/auth_key_store.js';
|
|
21
21
|
import { resolveHardwareUuid } from './hardware_uuid.js';
|
|
22
22
|
import { buildLogMetadataObject } from './log_metadata.js';
|
|
23
|
+
import { SCRUBBER_VERSION, scrubFields } from '../../pii_scrub.js';
|
|
23
24
|
const CLIENT_EVENT_PATH = '/endpoint_security/api/client-event/';
|
|
24
25
|
const SEND_TIMEOUT_MS = 4000;
|
|
25
26
|
const BUFFER_BASENAME = 'client_events.jsonl';
|
|
@@ -198,6 +199,16 @@ function claimBufferedEvents() {
|
|
|
198
199
|
return [];
|
|
199
200
|
}
|
|
200
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* An event as it is sent: its free-text fields (a failure's log, a command line, an error and its
|
|
204
|
+
* stack) scrubbed like tool-call content, and the scrubber's version in its metadata. Buffered
|
|
205
|
+
* events are scrubbed here too, on their way out.
|
|
206
|
+
*/
|
|
207
|
+
function forWire(event) {
|
|
208
|
+
const scrubbed = scrubFields(event, ['command', 'message', 'detail', 'log_text']);
|
|
209
|
+
const metadata = (event.metadata ?? {});
|
|
210
|
+
return { ...scrubbed, metadata: { ...metadata, scrubber_version: SCRUBBER_VERSION } };
|
|
211
|
+
}
|
|
201
212
|
async function postEvents(events) {
|
|
202
213
|
if (events.length === 0)
|
|
203
214
|
return true;
|
|
@@ -210,7 +221,7 @@ async function postEvents(events) {
|
|
|
210
221
|
}
|
|
211
222
|
const base = loadEndpointBase().replace(/\/+$/, '');
|
|
212
223
|
const url = `${base}${CLIENT_EVENT_PATH}`;
|
|
213
|
-
const signingPayload = { hardware_uuid: hardwareUuid, events };
|
|
224
|
+
const signingPayload = { hardware_uuid: hardwareUuid, events: events.map(forWire) };
|
|
214
225
|
let signature = '';
|
|
215
226
|
try {
|
|
216
227
|
const authKey = readStoredAuthKey();
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { OPTIMUSLABS_LOGS_REL, LOG_GROUP_COMPLIANCE } from '../../bootstrap_constants.js';
|
|
5
5
|
import { buildLogMetadataBlock } from './log_metadata.js';
|
|
6
|
+
import { SCRUBBER_VERSION, scrubText } from '../../pii_scrub.js';
|
|
6
7
|
const COMPLIANCE_SUBDIR = LOG_GROUP_COMPLIANCE;
|
|
7
8
|
const TIER_NOOP = 'noop';
|
|
8
9
|
const TIER_SUCCESS = 'success';
|
|
@@ -318,19 +319,27 @@ function appendLineToSessionLogFile(logPath, message) {
|
|
|
318
319
|
/**
|
|
319
320
|
* Upload full session log to the server (success and failures tiers only, first finalize only).
|
|
320
321
|
*/
|
|
321
|
-
|
|
322
|
+
/** The session log to upload, or null when this finalize does not upload one (and why is logged). */
|
|
323
|
+
function sessionLogToUpload(result) {
|
|
322
324
|
if (result.tier !== TIER_SUCCESS && result.tier !== TIER_FAILURES)
|
|
323
|
-
return;
|
|
325
|
+
return null;
|
|
324
326
|
if (!result.moved)
|
|
325
|
-
return;
|
|
327
|
+
return null;
|
|
326
328
|
const logText = readComplianceSessionLog(result.logPath);
|
|
327
329
|
if (!logText.trim())
|
|
328
|
-
return;
|
|
330
|
+
return null;
|
|
329
331
|
const logBytes = Buffer.byteLength(logText, 'utf8');
|
|
330
332
|
if (logBytes > MAX_SESSION_LOG_UPLOAD_BYTES) {
|
|
331
333
|
appendLineToSessionLogFile(result.logPath, `compliance_session_upload: skipped (${logBytes} bytes exceeds ${MAX_SESSION_LOG_UPLOAD_BYTES})`);
|
|
332
|
-
return;
|
|
334
|
+
return null;
|
|
333
335
|
}
|
|
336
|
+
return { logText, logBytes };
|
|
337
|
+
}
|
|
338
|
+
export async function uploadComplianceSessionLogFromFinalize(result) {
|
|
339
|
+
const toUpload = sessionLogToUpload(result);
|
|
340
|
+
if (!toUpload)
|
|
341
|
+
return;
|
|
342
|
+
const { logText, logBytes } = toUpload;
|
|
334
343
|
const { readStoredAuthKey } = await import('../auth/auth_key_store.js');
|
|
335
344
|
const { tryResolveHardwareUuid } = await import('./hardware_uuid.js');
|
|
336
345
|
const { loadEndpointBase } = await import('../sender/endpoint_config.js');
|
|
@@ -349,10 +358,11 @@ export async function uploadComplianceSessionLogFromFinalize(result) {
|
|
|
349
358
|
hardware_uuid: hardwareUuid,
|
|
350
359
|
outcome,
|
|
351
360
|
session_filename: sessionFilename,
|
|
352
|
-
|
|
361
|
+
// Signed as scrubbed; the version sits outside the signed fields.
|
|
362
|
+
log_text: scrubText(logText),
|
|
353
363
|
};
|
|
354
364
|
const signature = createSignature(signingPayload, authKey.key);
|
|
355
|
-
const body = JSON.stringify({ ...signingPayload, signature });
|
|
365
|
+
const body = JSON.stringify({ ...signingPayload, scrubber_version: SCRUBBER_VERSION, signature });
|
|
356
366
|
const url = buildApiUrl(loadEndpointBase(), '/endpoint_security/api/remediation-activity-log/');
|
|
357
367
|
try {
|
|
358
368
|
const { statusCode } = await executeBody(url, 'POST', body, 30_000);
|
|
@@ -30,23 +30,59 @@ function collectPromptsFromConfig(cfg) {
|
|
|
30
30
|
export function readCollectPrompts() {
|
|
31
31
|
return collectPromptsFromConfig(readOrgConfig());
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
/** The org's fingerprint key: hex, at least 128 bits. Keys the prompt hash and secret fingerprints. */
|
|
34
|
+
const FINGERPRINT_KEY = /^(?:[0-9a-f]{2}){16,}$/i;
|
|
35
|
+
/**
|
|
36
|
+
* The org key the backend sends in the startup and inventory responses (`fingerprint_key`),
|
|
37
|
+
* or undefined until it has arrived: the prompt hash stays plain SHA-256 and secrets get no
|
|
38
|
+
* fingerprint until then.
|
|
39
|
+
*/
|
|
40
|
+
export function readFingerprintKey() {
|
|
41
|
+
const key = readOrgConfig().fingerprint_key;
|
|
42
|
+
return typeof key === "string" && FINGERPRINT_KEY.test(key) ? key : undefined;
|
|
43
|
+
}
|
|
44
|
+
function writeOrgConfig(update) {
|
|
36
45
|
const filePath = orgConfigPath();
|
|
37
46
|
try {
|
|
38
47
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
39
|
-
|
|
40
|
-
delete next.hash_tool_call_prompts;
|
|
41
|
-
fs.writeFileSync(filePath, `${JSON.stringify(next)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
48
|
+
fs.writeFileSync(filePath, `${JSON.stringify(update(readOrgConfig()))}\n`, { encoding: "utf8", mode: 0o600 });
|
|
42
49
|
}
|
|
43
50
|
catch {
|
|
44
51
|
// Best-effort: next heartbeat can retry.
|
|
45
52
|
}
|
|
46
53
|
}
|
|
54
|
+
export function persistCollectPrompts(value) {
|
|
55
|
+
if (typeof value !== "boolean")
|
|
56
|
+
return;
|
|
57
|
+
writeOrgConfig((current) => {
|
|
58
|
+
const next = { ...current, collect_prompts: value };
|
|
59
|
+
delete next.hash_tool_call_prompts;
|
|
60
|
+
return next;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function persistFingerprintKey(value) {
|
|
64
|
+
if (typeof value !== "string" || !FINGERPRINT_KEY.test(value) || value === readFingerprintKey())
|
|
65
|
+
return;
|
|
66
|
+
writeOrgConfig((current) => ({ ...current, fingerprint_key: value }));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether the organization scrubs what endpoints send (OPT-942), read by `@optimuslabs/pii-scrub`
|
|
70
|
+
* from this file: only an explicit `false` switches scrubbing off.
|
|
71
|
+
*/
|
|
72
|
+
function persistScrubPii(value) {
|
|
73
|
+
if (typeof value !== "boolean" || readOrgConfig().scrub_pii === value)
|
|
74
|
+
return;
|
|
75
|
+
writeOrgConfig((current) => ({ ...current, scrub_pii: value }));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Store the org options a startup or inventory response carries: prompt collection, the org key
|
|
79
|
+
* and whether to scrub.
|
|
80
|
+
*/
|
|
47
81
|
export function persistCollectPromptsFromResponse(body) {
|
|
48
82
|
if (!body || typeof body !== "object")
|
|
49
83
|
return;
|
|
84
|
+
persistFingerprintKey(body.fingerprint_key);
|
|
85
|
+
persistScrubPii(body.scrub_pii);
|
|
50
86
|
if (typeof body.collect_prompts === "boolean") {
|
|
51
87
|
persistCollectPrompts(body.collect_prompts);
|
|
52
88
|
return;
|
|
@@ -16,6 +16,8 @@ import { clearRemediationApplyQuarantine, readRemediationApplyTrackingFile, writ
|
|
|
16
16
|
import { CURSOR_SCALAR_ITEMTABLE_FIELDS, buildVscdbPostApplyRawContent, persistVscdbComposerContractFromPatternsResponse, } from '../readers/vscdb_reader.js';
|
|
17
17
|
import { sendConfigFile } from '../sender/batch_sender.js';
|
|
18
18
|
import { parseWorktreeRootFromPath } from './worktree_scanner.js';
|
|
19
|
+
import { SCRUBBER_VERSION, containsPlaceholder, scrubValue } from '../../pii_scrub.js';
|
|
20
|
+
import { readFingerprintKey } from './prompt-collection-policy.js';
|
|
19
21
|
import { reportAbsentWorktrees } from './worktree_absent.js';
|
|
20
22
|
import { buildApiUrl, getFileCollectionPatterns } from '../../endpoint_client/registry_api.js';
|
|
21
23
|
import { resolveRemediationConfigPath } from './remediation_config_path.js';
|
|
@@ -69,8 +71,21 @@ function readInstructions() {
|
|
|
69
71
|
const { remediations } = readRemediationInstructionsFile();
|
|
70
72
|
return { remediations: remediations };
|
|
71
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Instructions whose fix would write a scrubbed placeholder are never stored, so never applied.
|
|
76
|
+
* The server holds only the scrubbed copy of a config; writing a placeholder back would replace
|
|
77
|
+
* the user's real key or token with `[OL_REDACTED:...]`.
|
|
78
|
+
*/
|
|
79
|
+
function withoutScrubbedFixes(remediations) {
|
|
80
|
+
return remediations.filter((inst) => {
|
|
81
|
+
if (!containsPlaceholder(remediationFixSpec(inst)?.checks ?? []))
|
|
82
|
+
return true;
|
|
83
|
+
hookRunLog(`remediation_instructions_skipped: uuid=${inst.uuid} reason=fix carries a scrubbed placeholder`);
|
|
84
|
+
return false;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
72
87
|
function writeInstructions(file) {
|
|
73
|
-
writeRemediationInstructionsFile(file);
|
|
88
|
+
writeRemediationInstructionsFile({ ...file, remediations: withoutScrubbedFixes(file.remediations) });
|
|
74
89
|
}
|
|
75
90
|
/**
|
|
76
91
|
* Merge persisted rows with API overlay; GET manifest for enforced UUIDs still missing rows.
|
|
@@ -1577,12 +1592,12 @@ export function reportAutofixApplied(remediationUuid, result, details) {
|
|
|
1577
1592
|
payload.failure_reason = details.failure_reason.slice(0, 2000);
|
|
1578
1593
|
if (details?.consecutive_failures != null)
|
|
1579
1594
|
payload.consecutive_failures = details.consecutive_failures;
|
|
1580
|
-
const bodyPayload = { ...payload, signature: '' };
|
|
1595
|
+
const bodyPayload = { ...payload, scrubber_version: SCRUBBER_VERSION, signature: '' };
|
|
1581
1596
|
if (details?.config_snapshot_after && typeof details.config_snapshot_after === 'object') {
|
|
1582
|
-
bodyPayload.config_snapshot_after = details.config_snapshot_after;
|
|
1597
|
+
bodyPayload.config_snapshot_after = scrubValue(details.config_snapshot_after, { fingerprintKey: readFingerprintKey() });
|
|
1583
1598
|
}
|
|
1584
1599
|
if (details?.config_snapshot_before && typeof details.config_snapshot_before === 'object') {
|
|
1585
|
-
bodyPayload.config_snapshot_before = details.config_snapshot_before;
|
|
1600
|
+
bodyPayload.config_snapshot_before = scrubValue(details.config_snapshot_before, { fingerprintKey: readFingerprintKey() });
|
|
1586
1601
|
}
|
|
1587
1602
|
const signature = createSignature(payload, authKey.key);
|
|
1588
1603
|
const body = JSON.stringify({ ...bodyPayload, signature });
|
|
@@ -3,9 +3,10 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { hookRunLog } from './hook_logger.js';
|
|
6
|
+
import { scrubText } from '../../pii_scrub.js';
|
|
6
7
|
const GIT_TIMEOUT_MS = 3000;
|
|
7
8
|
const MAX_WALK_UP = 25;
|
|
8
|
-
function
|
|
9
|
+
function readGitRemote(dir) {
|
|
9
10
|
try {
|
|
10
11
|
return execFileSync('git', ['remote', '-v'], {
|
|
11
12
|
cwd: dir,
|
|
@@ -18,6 +19,13 @@ function gitRemoteInDir(dir) {
|
|
|
18
19
|
return '';
|
|
19
20
|
}
|
|
20
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* `git remote -v` in `dir`. A remote can embed a token (`https://user:token@host/owner/repo`), so
|
|
24
|
+
* it is scrubbed here, where every remote this package reports comes from; host and path stay.
|
|
25
|
+
*/
|
|
26
|
+
function gitRemoteInDir(dir) {
|
|
27
|
+
return scrubText(readGitRemote(dir));
|
|
28
|
+
}
|
|
21
29
|
function gitToplevelFromDir(dir) {
|
|
22
30
|
try {
|
|
23
31
|
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
@@ -51,7 +59,7 @@ export function resolveWorkspaceRepoFromEnv() {
|
|
|
51
59
|
]) {
|
|
52
60
|
const value = (candidate || '').trim();
|
|
53
61
|
if (value)
|
|
54
|
-
return value;
|
|
62
|
+
return scrubText(value);
|
|
55
63
|
}
|
|
56
64
|
return '';
|
|
57
65
|
}
|
|
@@ -4,7 +4,8 @@ import { loadEndpointBase } from './endpoint_config.js';
|
|
|
4
4
|
import { hookRunLog } from '../runtime/hook_logger.js';
|
|
5
5
|
import { canonicalCursorUserStateVscdbPath } from '../runtime/remediation_config_path.js';
|
|
6
6
|
import { resolveWorkspaceRepoFromEnv } from '../runtime/workspace_repo.js';
|
|
7
|
-
import { persistCollectPromptsFromResponse } from '../runtime/prompt-collection-policy.js';
|
|
7
|
+
import { persistCollectPromptsFromResponse, readFingerprintKey } from '../runtime/prompt-collection-policy.js';
|
|
8
|
+
import { SCRUBBER_VERSION, scrubText, scrubValue } from '../../pii_scrub.js';
|
|
8
9
|
import fs from 'node:fs';
|
|
9
10
|
import os from 'node:os';
|
|
10
11
|
import path from 'node:path';
|
|
@@ -84,7 +85,8 @@ function buildChunkBody(chunk, hardwareUuid, authKey, hookRequestId, metadata) {
|
|
|
84
85
|
const item = {
|
|
85
86
|
file_type: c.file_type,
|
|
86
87
|
file_path: canonicalCursorUserStateVscdbPath(c.file_path),
|
|
87
|
-
raw_content: c.raw_content,
|
|
88
|
+
raw_content: scrubValue(c.raw_content, { fingerprintKey: readFingerprintKey() }),
|
|
89
|
+
scrubber_version: SCRUBBER_VERSION,
|
|
88
90
|
};
|
|
89
91
|
if (c.symlink_info !== undefined)
|
|
90
92
|
item.symlink_info = c.symlink_info;
|
|
@@ -227,11 +229,11 @@ async function sendConfigFile(configFile, hardwareUuid, authKey, repoIdentifier)
|
|
|
227
229
|
const endpoint = loadEndpointBase();
|
|
228
230
|
const apiUrl = `${resolveApiBase(endpoint)}/endpoint_security/log-config-file/`;
|
|
229
231
|
const uploadPath = canonicalCursorUserStateVscdbPath(configFile.file_path);
|
|
230
|
-
const payload = { hardware_uuid: hardwareUuid, file_type: configFile.file_type, file_path: uploadPath, raw_content: configFile.raw_content };
|
|
232
|
+
const payload = { hardware_uuid: hardwareUuid, file_type: configFile.file_type, file_path: uploadPath, raw_content: scrubValue(configFile.raw_content, { fingerprintKey: readFingerprintKey() }) };
|
|
231
233
|
if (configFile.symlink_info !== undefined)
|
|
232
234
|
payload.symlink_info = configFile.symlink_info;
|
|
233
235
|
const signature = createSignature(payload, authKey.key);
|
|
234
|
-
const body = { ...payload, signature, key_id: authKey.key_id || '', metadata: { org_identifier: process.env.GITHUB_ORG || process.env.GH_ORG || '', organization_uuid: readOrganizationUuid(), repo_identifier: repoIdentifier ?? resolveWorkspaceRepoFromEnv() } };
|
|
236
|
+
const body = { ...payload, signature, key_id: authKey.key_id || '', metadata: { org_identifier: process.env.GITHUB_ORG || process.env.GH_ORG || '', organization_uuid: readOrganizationUuid(), repo_identifier: repoIdentifier ?? resolveWorkspaceRepoFromEnv(), scrubber_version: SCRUBBER_VERSION } };
|
|
235
237
|
try {
|
|
236
238
|
const response = await postStartupPayload(apiUrl, body);
|
|
237
239
|
if (response.status !== 'accepted') {
|
|
@@ -273,8 +275,10 @@ async function sendHookRequestUpdateManifest(hardwareUuid, authKey, hookRequestI
|
|
|
273
275
|
const payload = { hardware_uuid: hardwareUuid, hook_request_id: hookRequestId, manifest: manifestNormalized };
|
|
274
276
|
const signature = createSignature(payload, authKey.key);
|
|
275
277
|
const body = { ...payload, signature, key_id: authKey.key_id || '' };
|
|
278
|
+
// The hook's log holds its whole environment: scrubbed, outside the signed manifest.
|
|
276
279
|
if (hookLog)
|
|
277
|
-
body.hook_log = hookLog;
|
|
280
|
+
body.hook_log = scrubText(hookLog);
|
|
281
|
+
body.scrubber_version = SCRUBBER_VERSION;
|
|
278
282
|
try {
|
|
279
283
|
const data = (await patchPayload(apiUrl, body));
|
|
280
284
|
if (data.status === 'accepted') {
|
|
@@ -4,6 +4,7 @@ import { flushBufferedClientEvents, getStoryId, reportFunctionalFailureAwait, re
|
|
|
4
4
|
import { resolveHardwareUuid } from './hardware_uuid.js';
|
|
5
5
|
import { writeAuthKey, readStoredAuthKey, loadEndpointBase, buildStartupEndpointUrl } from './auth_key_store.js';
|
|
6
6
|
import { resolveUserProfile } from './user_profile.js';
|
|
7
|
+
import { SCRUBBER_VERSION } from '../pii_scrub.js';
|
|
7
8
|
import { resolveWorkspaceRepo } from '../log_config_files/runtime/workspace_repo.js';
|
|
8
9
|
import { collectInstalledHookHashes } from './installed_hook_hashes.js';
|
|
9
10
|
import { persistCollectPromptsFromResponse } from '../log_config_files/runtime/prompt-collection-policy.js';
|
|
@@ -62,6 +63,7 @@ const getMetadata = () => ({
|
|
|
62
63
|
user_profile: { ...resolveUserProfile() },
|
|
63
64
|
// Every installed agent's hooks from disk, not just the launching agent's (OPTIMUS_HOOK_MANIFEST).
|
|
64
65
|
hook_manifest: { ...parseHookManifest(), ...collectInstalledHookHashes() },
|
|
66
|
+
scrubber_version: SCRUBBER_VERSION,
|
|
65
67
|
});
|
|
66
68
|
function buildRequestBody(hardwareUuid, timestamp) {
|
|
67
69
|
const metadata = getMetadata();
|
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.46",
|
|
4
4
|
"description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dev": "ts-node src/cli.ts",
|
|
20
20
|
"lint": "eslint \"src/**/*.ts\"",
|
|
21
21
|
"prepare": "npm run build",
|
|
22
|
-
"pretest": "npm run build",
|
|
22
|
+
"pretest": "npm run build --prefix ../pii-scrub && npm run build",
|
|
23
23
|
"test": "vitest run",
|
|
24
24
|
"test:watch": "vitest"
|
|
25
25
|
},
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"vitest": "^4.1.12"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
+
"@optimuslabs/pii-scrub-staging": "^0.1.0",
|
|
59
60
|
"@optimuslabs/tofu-staging": "^0.1.18",
|
|
60
61
|
"axios": "^1.20.0",
|
|
61
62
|
"canonicalize": "^2.1.0",
|