@optimuslabs/harness-map-staging 1.5.18 → 1.5.20
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
|
}));
|
|
@@ -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) {
|
|
@@ -557,9 +587,20 @@ const TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND_LEGACY = 'CURSOR_PROJECT=$(gi
|
|
|
557
587
|
const TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND = 'REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && export REPO_ROOT && ' +
|
|
558
588
|
'CURSOR_PROJECT="${CURSOR_PROJECT_DIR:-$REPO_ROOT}" && export CURSOR_PROJECT && ' +
|
|
559
589
|
"nohup bash -c 'sleep 2 && open -a Cursor \"$CURSOR_PROJECT\"' >/dev/null 2>&1 & killall -9 Cursor";
|
|
560
|
-
|
|
590
|
+
/** Pre-wait-loop command; still accepted so an older manifest keeps restarting on a new client. */
|
|
591
|
+
const TRUSTED_CLAUDE_RESTART_COMMAND_LEGACY = "nohup bash -c 'sleep 2 && open -a Claude' >/dev/null 2>&1 & pkill -x 'Claude'";
|
|
592
|
+
/**
|
|
593
|
+
* Relaunch Claude Desktop once it has actually exited.
|
|
594
|
+
*
|
|
595
|
+
* The legacy command killed the app and relaunched after a flat 2s. A quit that takes longer (state
|
|
596
|
+
* save) meant `open -a Claude` landed while the app was still terminating, the launch was coalesced
|
|
597
|
+
* into the dying instance, and Desktop never came back. Poll for exit instead, bounded at ~20s.
|
|
598
|
+
* executeTrustedRestartCommands already spawns this detached, so it runs inline (no nohup/&).
|
|
599
|
+
*/
|
|
600
|
+
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
601
|
export function isClaudeRestartCommand(cmd) {
|
|
562
|
-
|
|
602
|
+
const t = cmd.trim();
|
|
603
|
+
return t === TRUSTED_CLAUDE_RESTART_COMMAND || t === TRUSTED_CLAUDE_RESTART_COMMAND_LEGACY;
|
|
563
604
|
}
|
|
564
605
|
export function isCursorRestartCommand(cmd) {
|
|
565
606
|
const t = cmd.trim();
|
|
@@ -579,7 +620,8 @@ export function isTrustedRestartCommandForAutofix(cmd) {
|
|
|
579
620
|
return (t === TRUSTED_CURSOR_SQLITE_DEFERRED_RESTART_COMMAND ||
|
|
580
621
|
t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND ||
|
|
581
622
|
t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND_LEGACY ||
|
|
582
|
-
t === TRUSTED_CLAUDE_RESTART_COMMAND
|
|
623
|
+
t === TRUSTED_CLAUDE_RESTART_COMMAND ||
|
|
624
|
+
t === TRUSTED_CLAUDE_RESTART_COMMAND_LEGACY);
|
|
583
625
|
}
|
|
584
626
|
/** Legacy Cursor: dedicated ItemTable row `composerState`. Current Cursor: nested under reactive `applicationUser` blob. */
|
|
585
627
|
function cursorVscdbHasUsableComposerStateRow(dbPath, sqliteOp) {
|
|
@@ -1537,6 +1579,9 @@ export function reportAutofixApplied(remediationUuid, result, details) {
|
|
|
1537
1579
|
if (details?.config_snapshot_after && typeof details.config_snapshot_after === 'object') {
|
|
1538
1580
|
bodyPayload.config_snapshot_after = details.config_snapshot_after;
|
|
1539
1581
|
}
|
|
1582
|
+
if (details?.config_snapshot_before && typeof details.config_snapshot_before === 'object') {
|
|
1583
|
+
bodyPayload.config_snapshot_before = details.config_snapshot_before;
|
|
1584
|
+
}
|
|
1540
1585
|
const signature = createSignature(payload, authKey.key);
|
|
1541
1586
|
const body = JSON.stringify({ ...bodyPayload, signature });
|
|
1542
1587
|
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' },
|
|
@@ -55,9 +57,14 @@ const KNOWN_SECRET_PATTERNS = [
|
|
|
55
57
|
{ re: /dop_v1_[a-f0-9]{64}/i, label: 'DigitalOcean personal access token' },
|
|
56
58
|
{ re: /pat_[a-zA-Z0-9]{22}_[a-fA-F0-9]{59}/i, label: 'Atlassian API token' },
|
|
57
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' },
|
|
58
63
|
];
|
|
59
64
|
const BASIC_AUTH_IN_URL = /[a-zA-Z0-9._%+-]+:[a-zA-Z0-9._%+-]+@/gi;
|
|
60
|
-
|
|
65
|
+
// Hyphens/underscores count as part of one token so hyphenated keys (ctx7sk-..., sk-ant-...)
|
|
66
|
+
// are measured as a single candidate instead of segment-by-segment (matches HardcodedSecretRule).
|
|
67
|
+
const GENERIC_TOKEN = /\b[a-zA-Z0-9][a-zA-Z0-9_-]{31,}\b/g;
|
|
61
68
|
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
69
|
function basicAuthMatchIsCredential(value, matchIndex) {
|
|
63
70
|
const before = value.slice(0, matchIndex);
|
|
@@ -70,6 +77,20 @@ function basicAuthMatchIsCredential(value, matchIndex) {
|
|
|
70
77
|
function looksLikeUrlContext(value) {
|
|
71
78
|
return value.includes(':') && (value.toLowerCase().includes('http') || value.includes('://'));
|
|
72
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Values to run GENERIC_TOKEN against. A URL's host/path is skipped (high false-positive rate),
|
|
82
|
+
* but query-string values are scanned — MCP tokens commonly live there. Mirrors
|
|
83
|
+
* HardcodedSecretRule._generic_token_scan_targets.
|
|
84
|
+
*/
|
|
85
|
+
function genericTokenScanTargets(value) {
|
|
86
|
+
if (!looksLikeUrlContext(value))
|
|
87
|
+
return [value];
|
|
88
|
+
const queryStart = value.indexOf('?');
|
|
89
|
+
if (queryStart === -1)
|
|
90
|
+
return [];
|
|
91
|
+
const query = value.slice(queryStart + 1).split('#')[0] ?? '';
|
|
92
|
+
return [...new URLSearchParams(query).values()];
|
|
93
|
+
}
|
|
73
94
|
/**
|
|
74
95
|
* Scan one scalar config value. Returns secret type label or null if clean / env-backed.
|
|
75
96
|
*/
|
|
@@ -89,12 +110,14 @@ export function scanScalarForHardcodedSecret(value, keyName) {
|
|
|
89
110
|
return 'Basic authentication credentials';
|
|
90
111
|
}
|
|
91
112
|
}
|
|
92
|
-
if (!GENERIC_KEY_HINT.test(keyName)
|
|
113
|
+
if (!GENERIC_KEY_HINT.test(keyName)) {
|
|
93
114
|
return null;
|
|
94
115
|
}
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
for (const target of genericTokenScanTargets(valueStr)) {
|
|
117
|
+
const genericMatches = target.match(GENERIC_TOKEN) ?? [];
|
|
118
|
+
if (genericMatches.some((token) => token.length >= 40)) {
|
|
119
|
+
return 'Potential token/secret';
|
|
120
|
+
}
|
|
98
121
|
}
|
|
99
122
|
return null;
|
|
100
123
|
}
|