@optimuslabs/harness-map-staging 1.5.18 → 1.5.19
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.
|
@@ -89,6 +89,12 @@ function targetsCurrentAgent(entry, agent) {
|
|
|
89
89
|
complianceRunnerDiag(`Ignoring remediation with unknown target_agent: ${t}`);
|
|
90
90
|
return false;
|
|
91
91
|
}
|
|
92
|
+
// Claude Desktop cannot run hooks, so nothing ever executes with --agent=claude_desktop and
|
|
93
|
+
// these rows would otherwise never be applied by anyone. The Claude Code hook owns them: it
|
|
94
|
+
// writes Desktop's own config file and the spec's restart_command relaunches the app
|
|
95
|
+
// (trusted_restarts already maps claude_desktop -> claude for exactly this reason).
|
|
96
|
+
if (normalized === 'claude_desktop' && agent === 'claude')
|
|
97
|
+
return true;
|
|
92
98
|
return normalized === agent;
|
|
93
99
|
}
|
|
94
100
|
// ---------------------------------------------------------------------------
|
|
@@ -361,6 +367,28 @@ function violationFromCheck(entry, compliance, check, expected) {
|
|
|
361
367
|
message: `[${compliance.finding_formatted_id}] ${entry.finding_title ?? compliance.description}\nDescription: ${entry.finding_description ?? compliance.description}\nHow to fix: Apply remediation ops for ${check.setting_path} in ${entry.config_file_path}`,
|
|
362
368
|
};
|
|
363
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* True when a secret-scan hit sits at (or under) a path this remediation has ops for.
|
|
372
|
+
*
|
|
373
|
+
* The scan reads the whole file, but the entry only carries ops for its own checks. Blocking on
|
|
374
|
+
* any other hit pins the row non-compliant forever -- the gate re-prompts on every turn with a fix
|
|
375
|
+
* that cannot resolve it (e.g. Claude Code's own 64-hex `machineID` matching a provider pattern).
|
|
376
|
+
*/
|
|
377
|
+
function secretFindingIsOwnedByEntry(findingPath, entry, checks) {
|
|
378
|
+
return checks.some((check) => {
|
|
379
|
+
const target = canonicalComplianceSettingPath(entry.config_file_path, check);
|
|
380
|
+
if (!target)
|
|
381
|
+
return false;
|
|
382
|
+
if (target.includes('*')) {
|
|
383
|
+
const pattern = target
|
|
384
|
+
.split('.')
|
|
385
|
+
.map((seg) => (seg === '*' ? '[^.]+' : seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
|
|
386
|
+
.join('\\.');
|
|
387
|
+
return new RegExp(`^${pattern}(\\.|$)`).test(findingPath);
|
|
388
|
+
}
|
|
389
|
+
return findingPath === target || findingPath.startsWith(`${target}.`);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
364
392
|
/** Evaluate one manifest row against on-disk config (used by gate + post-restart verify). */
|
|
365
393
|
export function evaluateManifestEntryCompliance(entry) {
|
|
366
394
|
const compliance = entry.fix ?? entry.compliance;
|
|
@@ -370,11 +398,15 @@ export function evaluateManifestEntryCompliance(entry) {
|
|
|
370
398
|
if (checks.length === 0)
|
|
371
399
|
return { violations: [] };
|
|
372
400
|
const loaded = loadRemediationConfigJson(entry.config_file_path, checks.map((c) => c.setting_path));
|
|
373
|
-
if (!loaded.ok)
|
|
374
|
-
|
|
401
|
+
if (!loaded.ok) {
|
|
402
|
+
// A config we cannot read is not a config we can call compliant. file_not_found is genuinely
|
|
403
|
+
// compliant (the config is gone); any other reason means the file is there and we simply could
|
|
404
|
+
// not parse it, so callers must not treat the empty violation list as "verified".
|
|
405
|
+
return { violations: [], unevaluable: loaded.reason !== 'file_not_found' };
|
|
406
|
+
}
|
|
375
407
|
const configJson = loaded.json;
|
|
376
408
|
if (compliance.requires_secret_scan === true) {
|
|
377
|
-
const secretFindings = scanJsonForHardcodedSecrets(configJson);
|
|
409
|
+
const secretFindings = scanJsonForHardcodedSecrets(configJson).filter((f) => secretFindingIsOwnedByEntry(f.path, entry, checks));
|
|
378
410
|
return secretFindings.length === 0
|
|
379
411
|
? { violations: [] }
|
|
380
412
|
: {
|
|
@@ -565,8 +597,12 @@ export function reportPostRestartVerificationOutcomes(violations) {
|
|
|
565
597
|
const entriesByUuid = new Map(remediations.map((entry) => [entry.uuid, entry]));
|
|
566
598
|
const outcomes = processPendingPostRestartVerifications((uuid) => {
|
|
567
599
|
const entry = entriesByUuid.get(uuid);
|
|
568
|
-
if (entry
|
|
569
|
-
|
|
600
|
+
if (entry) {
|
|
601
|
+
// Unreadable config counts as still-violating: never verify what could not be checked.
|
|
602
|
+
const { violations: entryViolations, unevaluable } = evaluateManifestEntryCompliance(entry);
|
|
603
|
+
if (unevaluable || entryViolations.length > 0)
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
570
606
|
return violations.some((v) => v.uuid === uuid);
|
|
571
607
|
});
|
|
572
608
|
const reportPromises = outcomes.map((o) => {
|
|
@@ -597,6 +633,35 @@ export async function runPostApplyVerification(agent = 'cursor') {
|
|
|
597
633
|
}
|
|
598
634
|
return outcomes;
|
|
599
635
|
}
|
|
636
|
+
/**
|
|
637
|
+
* Re-read the remediated config so the server can verify the apply instead of trusting the report.
|
|
638
|
+
*
|
|
639
|
+
* Without a snapshot the server's post-apply re-check has nothing to evaluate and clears the
|
|
640
|
+
* linked findings on the endpoint's word alone -- the same blind close that let a fix that never
|
|
641
|
+
* happened be recorded as remediated. vscdb-backed paths are not JSON files, so they stay absent.
|
|
642
|
+
*/
|
|
643
|
+
function configSnapshotForReport(configFilePath) {
|
|
644
|
+
const diskPath = resolveRemediationConfigPath(configFilePath);
|
|
645
|
+
if (diskPath.includes('#'))
|
|
646
|
+
return undefined;
|
|
647
|
+
try {
|
|
648
|
+
const parsed = parseJsonWithJsoncFallback(readFileSync(diskPath, 'utf8'));
|
|
649
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
650
|
+
return parsed;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
/* unreadable: report without a snapshot rather than failing the report */
|
|
655
|
+
}
|
|
656
|
+
return undefined;
|
|
657
|
+
}
|
|
658
|
+
/** Report an apply outcome, attaching the on-disk config only when it can actually be read. */
|
|
659
|
+
function reportAutofixWithSnapshot(uuid, result, configFilePath) {
|
|
660
|
+
const snapshot = configSnapshotForReport(configFilePath);
|
|
661
|
+
return snapshot
|
|
662
|
+
? reportAutofixApplied(uuid, result, { config_snapshot_after: snapshot })
|
|
663
|
+
: reportAutofixApplied(uuid, result);
|
|
664
|
+
}
|
|
600
665
|
/**
|
|
601
666
|
* Immediate autofix succeeded (inline recheck OK or Claude stale-recheck tolerance).
|
|
602
667
|
* Clear pending verification locally and report verified so the next prompt does not POST
|
|
@@ -605,7 +670,7 @@ export async function runPostApplyVerification(agent = 'cursor') {
|
|
|
605
670
|
export function confirmAppliedAutofixVerified(appliedViolations, reportPromises) {
|
|
606
671
|
for (const v of appliedViolations) {
|
|
607
672
|
markRemediationApplyVerified(v.uuid);
|
|
608
|
-
reportPromises.push(
|
|
673
|
+
reportPromises.push(reportAutofixWithSnapshot(v.uuid, 'verified', v.config_file_path));
|
|
609
674
|
}
|
|
610
675
|
}
|
|
611
676
|
export function applyAutofixViolations(violations, agent = 'cursor') {
|
|
@@ -682,7 +747,7 @@ export function applyAutofixViolations(violations, agent = 'cursor') {
|
|
|
682
747
|
fixed++;
|
|
683
748
|
appliedViolations.push(violation);
|
|
684
749
|
hookRunLog(`autofix: applied uuid=${inst.uuid} path=${configPathForDisk}`);
|
|
685
|
-
reportPromises.push(
|
|
750
|
+
reportPromises.push(reportAutofixWithSnapshot(inst.uuid, 'success', inst.config_file_path));
|
|
686
751
|
// Every successful autofix (Cursor + Claude, restart or immediate JSON) awaits verification on
|
|
687
752
|
// the next compliance check so we can quarantine stuck applies and stop restart/retry loops.
|
|
688
753
|
markRemediationApplyPendingVerification(inst.uuid);
|
|
@@ -904,8 +969,8 @@ export function uploadSatisfiedManifestConfigs(agent = 'cursor') {
|
|
|
904
969
|
const entries = remediations.filter((e) => targetsCurrentAgent(e, agent));
|
|
905
970
|
const promises = [];
|
|
906
971
|
for (const entry of entries) {
|
|
907
|
-
const { violations } = evaluateManifestEntryCompliance(entry);
|
|
908
|
-
if (violations.length > 0)
|
|
972
|
+
const { violations, unevaluable } = evaluateManifestEntryCompliance(entry);
|
|
973
|
+
if (violations.length > 0 || unevaluable)
|
|
909
974
|
continue;
|
|
910
975
|
const inst = entry;
|
|
911
976
|
const uploadFileType = resolveRemediationUploadFileType(entry.config_file_path, inst.file_type ?? undefined);
|
|
@@ -969,8 +1034,8 @@ export function reportCompliantRemediationVerifiedStatus(agent = 'cursor') {
|
|
|
969
1034
|
const entries = remediations.filter((e) => targetsCurrentAgent(e, agent));
|
|
970
1035
|
const promises = [];
|
|
971
1036
|
for (const entry of entries) {
|
|
972
|
-
const { violations } = evaluateManifestEntryCompliance(entry);
|
|
973
|
-
if (violations.length > 0)
|
|
1037
|
+
const { violations, unevaluable } = evaluateManifestEntryCompliance(entry);
|
|
1038
|
+
if (violations.length > 0 || unevaluable)
|
|
974
1039
|
continue;
|
|
975
1040
|
const tracking = readRemediationApplyTrackingFile();
|
|
976
1041
|
const prev = tracking.entries[entry.uuid];
|
|
@@ -982,7 +1047,13 @@ export function reportCompliantRemediationVerifiedStatus(agent = 'cursor') {
|
|
|
982
1047
|
const rawContent = parseJsonWithJsoncFallback(readFileSync(diskPath, 'utf8'));
|
|
983
1048
|
if (rawContent === null)
|
|
984
1049
|
continue;
|
|
985
|
-
promises.push(
|
|
1050
|
+
promises.push(
|
|
1051
|
+
// Nothing was applied on this path, so before === after. Sending both makes a no-op
|
|
1052
|
+
// "verified" distinguishable from a real apply in EnforcementLog.
|
|
1053
|
+
reportAutofixApplied(entry.uuid, 'verified', {
|
|
1054
|
+
config_snapshot_before: rawContent,
|
|
1055
|
+
config_snapshot_after: rawContent,
|
|
1056
|
+
}).then(() => {
|
|
986
1057
|
markRemediationApplyVerified(entry.uuid);
|
|
987
1058
|
hookRunLog(`compliance_check: reported verified (already compliant) uuid=${entry.uuid}`);
|
|
988
1059
|
}));
|
|
@@ -557,9 +557,20 @@ const TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND_LEGACY = 'CURSOR_PROJECT=$(gi
|
|
|
557
557
|
const TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND = 'REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && export REPO_ROOT && ' +
|
|
558
558
|
'CURSOR_PROJECT="${CURSOR_PROJECT_DIR:-$REPO_ROOT}" && export CURSOR_PROJECT && ' +
|
|
559
559
|
"nohup bash -c 'sleep 2 && open -a Cursor \"$CURSOR_PROJECT\"' >/dev/null 2>&1 & killall -9 Cursor";
|
|
560
|
-
|
|
560
|
+
/** Pre-wait-loop command; still accepted so an older manifest keeps restarting on a new client. */
|
|
561
|
+
const TRUSTED_CLAUDE_RESTART_COMMAND_LEGACY = "nohup bash -c 'sleep 2 && open -a Claude' >/dev/null 2>&1 & pkill -x 'Claude'";
|
|
562
|
+
/**
|
|
563
|
+
* Relaunch Claude Desktop once it has actually exited.
|
|
564
|
+
*
|
|
565
|
+
* The legacy command killed the app and relaunched after a flat 2s. A quit that takes longer (state
|
|
566
|
+
* save) meant `open -a Claude` landed while the app was still terminating, the launch was coalesced
|
|
567
|
+
* into the dying instance, and Desktop never came back. Poll for exit instead, bounded at ~20s.
|
|
568
|
+
* executeTrustedRestartCommands already spawns this detached, so it runs inline (no nohup/&).
|
|
569
|
+
*/
|
|
570
|
+
const TRUSTED_CLAUDE_RESTART_COMMAND = "pkill -x 'Claude'; for i in $(seq 1 80); do pgrep -x 'Claude' >/dev/null || break; sleep 0.25; done; open -a Claude";
|
|
561
571
|
export function isClaudeRestartCommand(cmd) {
|
|
562
|
-
|
|
572
|
+
const t = cmd.trim();
|
|
573
|
+
return t === TRUSTED_CLAUDE_RESTART_COMMAND || t === TRUSTED_CLAUDE_RESTART_COMMAND_LEGACY;
|
|
563
574
|
}
|
|
564
575
|
export function isCursorRestartCommand(cmd) {
|
|
565
576
|
const t = cmd.trim();
|
|
@@ -579,7 +590,8 @@ export function isTrustedRestartCommandForAutofix(cmd) {
|
|
|
579
590
|
return (t === TRUSTED_CURSOR_SQLITE_DEFERRED_RESTART_COMMAND ||
|
|
580
591
|
t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND ||
|
|
581
592
|
t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND_LEGACY ||
|
|
582
|
-
t === TRUSTED_CLAUDE_RESTART_COMMAND
|
|
593
|
+
t === TRUSTED_CLAUDE_RESTART_COMMAND ||
|
|
594
|
+
t === TRUSTED_CLAUDE_RESTART_COMMAND_LEGACY);
|
|
583
595
|
}
|
|
584
596
|
/** Legacy Cursor: dedicated ItemTable row `composerState`. Current Cursor: nested under reactive `applicationUser` blob. */
|
|
585
597
|
function cursorVscdbHasUsableComposerStateRow(dbPath, sqliteOp) {
|
|
@@ -1537,6 +1549,9 @@ export function reportAutofixApplied(remediationUuid, result, details) {
|
|
|
1537
1549
|
if (details?.config_snapshot_after && typeof details.config_snapshot_after === 'object') {
|
|
1538
1550
|
bodyPayload.config_snapshot_after = details.config_snapshot_after;
|
|
1539
1551
|
}
|
|
1552
|
+
if (details?.config_snapshot_before && typeof details.config_snapshot_before === 'object') {
|
|
1553
|
+
bodyPayload.config_snapshot_before = details.config_snapshot_before;
|
|
1554
|
+
}
|
|
1540
1555
|
const signature = createSignature(payload, authKey.key);
|
|
1541
1556
|
const body = JSON.stringify({ ...bodyPayload, signature });
|
|
1542
1557
|
return executeBody(url, 'POST', body, 8000)
|
|
@@ -38,10 +38,12 @@ const KNOWN_SECRET_PATTERNS = [
|
|
|
38
38
|
{ re: /ASIA[0-9A-Z]{16}/i, label: 'AWS temporary access key ID' },
|
|
39
39
|
{ re: /AIza[0-9A-Za-z_-]{35}/i, label: 'Google API key' },
|
|
40
40
|
{ re: /ya29\.[0-9A-Za-z_-]+/i, label: 'Google OAuth access token' },
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Anchored + case-sensitive: Twilio SIDs are uppercase AC/SK + exactly 32 hex. Unanchored
|
|
42
|
+
// /AC[a-z0-9]{32}/i matched any long hex string containing "ac" (e.g. a 64-hex machine id).
|
|
43
|
+
{ re: /\bAC[a-f0-9]{32}\b/, label: 'Twilio account SID' },
|
|
44
|
+
{ re: /\bSK[a-f0-9]{32}\b/, label: 'Twilio API key' },
|
|
43
45
|
{ re: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/i, label: 'SendGrid API key' },
|
|
44
|
-
{ re:
|
|
46
|
+
{ re: /\bkey-[a-f0-9]{32}\b/i, label: 'Mailgun API key' },
|
|
45
47
|
{ re: /npm_[a-zA-Z0-9]{36}/i, label: 'npm access token' },
|
|
46
48
|
{ re: /pypi-[a-zA-Z0-9_-]{50,}/i, label: 'PyPI API token' },
|
|
47
49
|
{ re: /discord(?:app)?\.com\/api\/webhooks\/\d+\/[a-zA-Z0-9_-]+/i, label: 'Discord webhook URL' },
|
|
@@ -57,7 +59,9 @@ const KNOWN_SECRET_PATTERNS = [
|
|
|
57
59
|
{ re: /Bearer\s+[a-zA-Z0-9\-_.]{20,}/i, label: 'Bearer token' },
|
|
58
60
|
];
|
|
59
61
|
const BASIC_AUTH_IN_URL = /[a-zA-Z0-9._%+-]+:[a-zA-Z0-9._%+-]+@/gi;
|
|
60
|
-
|
|
62
|
+
// Hyphens/underscores count as part of one token so hyphenated keys (ctx7sk-..., sk-ant-...)
|
|
63
|
+
// are measured as a single candidate instead of segment-by-segment (matches HardcodedSecretRule).
|
|
64
|
+
const GENERIC_TOKEN = /\b[a-zA-Z0-9][a-zA-Z0-9_-]{31,}\b/g;
|
|
61
65
|
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
66
|
function basicAuthMatchIsCredential(value, matchIndex) {
|
|
63
67
|
const before = value.slice(0, matchIndex);
|
|
@@ -70,6 +74,20 @@ function basicAuthMatchIsCredential(value, matchIndex) {
|
|
|
70
74
|
function looksLikeUrlContext(value) {
|
|
71
75
|
return value.includes(':') && (value.toLowerCase().includes('http') || value.includes('://'));
|
|
72
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Values to run GENERIC_TOKEN against. A URL's host/path is skipped (high false-positive rate),
|
|
79
|
+
* but query-string values are scanned — MCP tokens commonly live there. Mirrors
|
|
80
|
+
* HardcodedSecretRule._generic_token_scan_targets.
|
|
81
|
+
*/
|
|
82
|
+
function genericTokenScanTargets(value) {
|
|
83
|
+
if (!looksLikeUrlContext(value))
|
|
84
|
+
return [value];
|
|
85
|
+
const queryStart = value.indexOf('?');
|
|
86
|
+
if (queryStart === -1)
|
|
87
|
+
return [];
|
|
88
|
+
const query = value.slice(queryStart + 1).split('#')[0] ?? '';
|
|
89
|
+
return [...new URLSearchParams(query).values()];
|
|
90
|
+
}
|
|
73
91
|
/**
|
|
74
92
|
* Scan one scalar config value. Returns secret type label or null if clean / env-backed.
|
|
75
93
|
*/
|
|
@@ -89,12 +107,14 @@ export function scanScalarForHardcodedSecret(value, keyName) {
|
|
|
89
107
|
return 'Basic authentication credentials';
|
|
90
108
|
}
|
|
91
109
|
}
|
|
92
|
-
if (!GENERIC_KEY_HINT.test(keyName)
|
|
110
|
+
if (!GENERIC_KEY_HINT.test(keyName)) {
|
|
93
111
|
return null;
|
|
94
112
|
}
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
113
|
+
for (const target of genericTokenScanTargets(valueStr)) {
|
|
114
|
+
const genericMatches = target.match(GENERIC_TOKEN) ?? [];
|
|
115
|
+
if (genericMatches.some((token) => token.length >= 40)) {
|
|
116
|
+
return 'Potential token/secret';
|
|
117
|
+
}
|
|
98
118
|
}
|
|
99
119
|
return null;
|
|
100
120
|
}
|