The CC settings schema is passthrough (verified against the 2.1.193 binary): it forwards unrecognized keys unchanged rather than rejecting them, so an arbitrary unknown key is valid/forward-compatible, not an error — the finding's "silently ignored" claim was factually wrong. The only real risk is a TYPO of a real key (the intended setting then silently has no effect). Fix: flag an unknown key only when it closely matches a known key (new levenshtein helper; edit distance <= 2, both keys >= 4 chars); severity medium -> low; honest passthrough framing in the scanner + humanizer. Also refreshed KNOWN_KEYS with 6 binary-verified keys (agentPushNotifEnabled, remoteControlAtStartup, skipAutoPermissionPrompt, skipDangerousModePermissionPrompt, skipWorkflowUsageWarning, tui). Suite 1341/0 (+12). Frozen v5.0.0 snapshots untouched (0 CA-SET findings there), no re-seed. Dogfood ~/.claude/settings.json 6->0 (all 6 keys above were false unknown-key findings; 0 typo flags introduced across 167 walked files).
109 lines
2.9 KiB
JavaScript
109 lines
2.9 KiB
JavaScript
/**
|
|
* String utilities for config-audit scanners.
|
|
* Zero external dependencies.
|
|
*/
|
|
|
|
/**
|
|
* Count lines in a string.
|
|
* @param {string} s
|
|
* @returns {number}
|
|
*/
|
|
export function lineCount(s) {
|
|
if (!s) return 0;
|
|
return s.split('\n').length;
|
|
}
|
|
|
|
/**
|
|
* Truncate a string to maxLen chars with ellipsis.
|
|
* @param {string} s
|
|
* @param {number} [maxLen=100]
|
|
* @returns {string}
|
|
*/
|
|
export function truncate(s, maxLen = 100) {
|
|
if (!s || s.length <= maxLen) return s || '';
|
|
return s.slice(0, maxLen - 3) + '...';
|
|
}
|
|
|
|
/**
|
|
* Check if two strings have >threshold% content similarity (word overlap).
|
|
* @param {string} a
|
|
* @param {string} b
|
|
* @param {number} [threshold=0.8]
|
|
* @returns {boolean}
|
|
*/
|
|
export function isSimilar(a, b, threshold = 0.8) {
|
|
const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(w => w.length > 2));
|
|
const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(w => w.length > 2));
|
|
if (wordsA.size === 0 || wordsB.size === 0) return false;
|
|
let overlap = 0;
|
|
for (const w of wordsA) {
|
|
if (wordsB.has(w)) overlap++;
|
|
}
|
|
const similarity = overlap / Math.min(wordsA.size, wordsB.size);
|
|
return similarity >= threshold;
|
|
}
|
|
|
|
/**
|
|
* Levenshtein edit distance between two strings (insertions, deletions,
|
|
* substitutions; a transposition counts as 2). Used for typo detection on
|
|
* settings keys. Zero external dependencies, O(a*b) with two rolling rows.
|
|
* @param {string} a
|
|
* @param {string} b
|
|
* @returns {number}
|
|
*/
|
|
export function levenshtein(a, b) {
|
|
if (a === b) return 0;
|
|
const al = a.length;
|
|
const bl = b.length;
|
|
if (al === 0) return bl;
|
|
if (bl === 0) return al;
|
|
let prev = new Array(bl + 1);
|
|
let curr = new Array(bl + 1);
|
|
for (let j = 0; j <= bl; j++) prev[j] = j;
|
|
for (let i = 1; i <= al; i++) {
|
|
curr[0] = i;
|
|
const ac = a.charCodeAt(i - 1);
|
|
for (let j = 1; j <= bl; j++) {
|
|
const cost = ac === b.charCodeAt(j - 1) ? 0 : 1;
|
|
curr[j] = Math.min(
|
|
prev[j] + 1, // deletion
|
|
curr[j - 1] + 1, // insertion
|
|
prev[j - 1] + cost, // substitution
|
|
);
|
|
}
|
|
const tmp = prev;
|
|
prev = curr;
|
|
curr = tmp;
|
|
}
|
|
return prev[bl];
|
|
}
|
|
|
|
/**
|
|
* Extract all key-like patterns from a settings.json or similar config.
|
|
* @param {object} obj
|
|
* @param {string} [prefix='']
|
|
* @returns {string[]}
|
|
*/
|
|
export function extractKeys(obj, prefix = '') {
|
|
const keys = [];
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
keys.push(fullKey);
|
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
keys.push(...extractKeys(value, fullKey));
|
|
}
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
/**
|
|
* Normalize a file path for comparison (resolve ~, handle trailing slashes).
|
|
* @param {string} p
|
|
* @returns {string}
|
|
*/
|
|
export function normalizePath(p) {
|
|
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
let normalized = p.replace(/^~/, home);
|
|
normalized = normalized.replace(/[/\\]+$/, '');
|
|
return normalized;
|
|
}
|