feat: initial open marketplace with llm-security, config-audit, ultraplan-local
This commit is contained in:
commit
f93d6abdae
380 changed files with 65935 additions and 0 deletions
224
plugins/config-audit/scanners/settings-validator.mjs
Normal file
224
plugins/config-audit/scanners/settings-validator.mjs
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
/**
|
||||
* SET Scanner — Settings.json Validator
|
||||
* Validates schema, detects unknown/deprecated keys, type mismatches.
|
||||
* Finding IDs: CA-SET-NNN
|
||||
*/
|
||||
|
||||
import { readTextFile } from './lib/file-discovery.mjs';
|
||||
import { finding, scannerResult } from './lib/output.mjs';
|
||||
import { SEVERITY } from './lib/severity.mjs';
|
||||
import { parseJson } from './lib/yaml-parser.mjs';
|
||||
import { extractKeys } from './lib/string-utils.mjs';
|
||||
|
||||
const SCANNER = 'SET';
|
||||
|
||||
/** Known top-level settings.json keys (as of April 2026) */
|
||||
const KNOWN_KEYS = new Set([
|
||||
'agent', 'allowedChannelPlugins', 'allowedHttpHookUrls', 'allowedMcpServers',
|
||||
'allowManagedHooksOnly', 'allowManagedMcpServersOnly', 'allowManagedPermissionRulesOnly',
|
||||
'alwaysThinkingEnabled', 'apiKeyHelper', 'attribution', 'autoMemoryDirectory',
|
||||
'autoMemoryEnabled', 'autoMode', 'autoUpdatesChannel', 'availableModels',
|
||||
'awsAuthRefresh', 'awsCredentialExport', 'blockedMarketplaces', 'channelsEnabled',
|
||||
'cleanupPeriodDays', 'claudeMdExcludes', 'companyAnnouncements', 'defaultShell',
|
||||
'deniedMcpServers', 'disableAllHooks', 'disableAutoMode', 'disableDeepLinkRegistration',
|
||||
'disabledMcpjsonServers', 'effortLevel', 'enableAllProjectMcpServers',
|
||||
'enabledMcpjsonServers', 'enabledPlugins', 'env', 'extraKnownMarketplaces',
|
||||
'fastModePerSessionOptIn', 'feedbackSurveyRate', 'fileSuggestion',
|
||||
'forceLoginMethod', 'forceLoginOrgUUID', 'hooks', 'httpHookAllowedEnvVars',
|
||||
'includeCoAuthoredBy', 'includeGitInstructions', 'language', 'model',
|
||||
'modelOverrides', 'otelHeadersHelper', 'outputStyle', 'permissions',
|
||||
'plansDirectory', 'pluginTrustMessage', 'prefersReducedMotion',
|
||||
'respectGitignore', 'showClearContextOnPlanAccept', 'showThinkingSummaries',
|
||||
'spinnerTipsEnabled', 'spinnerTipsOverride', 'spinnerVerbs', 'statusLine',
|
||||
'strictKnownMarketplaces', 'useAutoModeDuringPlan', 'voiceEnabled',
|
||||
'worktree', '$schema',
|
||||
]);
|
||||
|
||||
/** Deprecated keys with migration info */
|
||||
const DEPRECATED_KEYS = new Map([
|
||||
['includeCoAuthoredBy', 'Use "attribution" instead'],
|
||||
]);
|
||||
|
||||
/** Keys that require specific types */
|
||||
const TYPE_CHECKS = new Map([
|
||||
['alwaysThinkingEnabled', 'boolean'],
|
||||
['autoMemoryEnabled', 'boolean'],
|
||||
['channelsEnabled', 'boolean'],
|
||||
['cleanupPeriodDays', 'number'],
|
||||
['disableAllHooks', 'boolean'],
|
||||
['effortLevel', 'string'],
|
||||
['enableAllProjectMcpServers', 'boolean'],
|
||||
['fastModePerSessionOptIn', 'boolean'],
|
||||
['feedbackSurveyRate', 'number'],
|
||||
['includeGitInstructions', 'boolean'],
|
||||
['language', 'string'],
|
||||
['model', 'string'],
|
||||
['outputStyle', 'string'],
|
||||
['prefersReducedMotion', 'boolean'],
|
||||
['respectGitignore', 'boolean'],
|
||||
['showThinkingSummaries', 'boolean'],
|
||||
['spinnerTipsEnabled', 'boolean'],
|
||||
['voiceEnabled', 'boolean'],
|
||||
]);
|
||||
|
||||
/** Valid effortLevel values */
|
||||
const VALID_EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'max']);
|
||||
|
||||
/**
|
||||
* Scan all settings.json files discovered.
|
||||
* @param {string} targetPath
|
||||
* @param {{ files: import('./lib/file-discovery.mjs').ConfigFile[] }} discovery
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
export async function scan(targetPath, discovery) {
|
||||
const start = Date.now();
|
||||
const settingsFiles = discovery.files.filter(f => f.type === 'settings-json');
|
||||
const findings = [];
|
||||
let filesScanned = 0;
|
||||
|
||||
if (settingsFiles.length === 0) {
|
||||
return scannerResult(SCANNER, 'skipped', [], 0, Date.now() - start);
|
||||
}
|
||||
|
||||
for (const file of settingsFiles) {
|
||||
const content = await readTextFile(file.absPath);
|
||||
if (!content) continue;
|
||||
filesScanned++;
|
||||
|
||||
const parsed = parseJson(content);
|
||||
if (parsed === null) {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.critical,
|
||||
title: 'Invalid JSON in settings file',
|
||||
description: `${file.relPath} contains invalid JSON and will be ignored by Claude Code.`,
|
||||
file: file.absPath,
|
||||
recommendation: 'Fix JSON syntax errors. Use a JSON validator.',
|
||||
autoFixable: false,
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for unknown keys
|
||||
for (const key of Object.keys(parsed)) {
|
||||
if (!KNOWN_KEYS.has(key)) {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.medium,
|
||||
title: 'Unknown settings key',
|
||||
description: `${file.relPath}: "${key}" is not a recognized settings.json key. It will be silently ignored.`,
|
||||
file: file.absPath,
|
||||
evidence: key,
|
||||
recommendation: 'Check spelling. See https://json.schemastore.org/claude-code-settings.json for valid keys.',
|
||||
autoFixable: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Check for deprecated keys
|
||||
for (const [key, migration] of DEPRECATED_KEYS) {
|
||||
if (parsed[key] !== undefined) {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.medium,
|
||||
title: 'Deprecated settings key',
|
||||
description: `${file.relPath}: "${key}" is deprecated. ${migration}`,
|
||||
file: file.absPath,
|
||||
evidence: `${key}: ${JSON.stringify(parsed[key])}`,
|
||||
recommendation: migration,
|
||||
autoFixable: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Type validation
|
||||
for (const [key, expectedType] of TYPE_CHECKS) {
|
||||
if (parsed[key] !== undefined && typeof parsed[key] !== expectedType) {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.high,
|
||||
title: 'Type mismatch in settings',
|
||||
description: `${file.relPath}: "${key}" should be ${expectedType}, got ${typeof parsed[key]}.`,
|
||||
file: file.absPath,
|
||||
evidence: `${key}: ${JSON.stringify(parsed[key])} (${typeof parsed[key]})`,
|
||||
recommendation: `Change "${key}" to a ${expectedType} value.`,
|
||||
autoFixable: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// effortLevel value check
|
||||
if (parsed.effortLevel && !VALID_EFFORT_LEVELS.has(parsed.effortLevel)) {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.medium,
|
||||
title: 'Invalid effortLevel value',
|
||||
description: `${file.relPath}: effortLevel "${parsed.effortLevel}" is not valid.`,
|
||||
file: file.absPath,
|
||||
evidence: `effortLevel: "${parsed.effortLevel}"`,
|
||||
recommendation: `Use one of: ${[...VALID_EFFORT_LEVELS].join(', ')}`,
|
||||
autoFixable: true,
|
||||
}));
|
||||
}
|
||||
|
||||
// Missing $schema hint
|
||||
if (!parsed.$schema) {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.info,
|
||||
title: 'Missing $schema reference',
|
||||
description: `${file.relPath} lacks a $schema reference. Adding one enables autocomplete in VS Code/Cursor.`,
|
||||
file: file.absPath,
|
||||
recommendation: 'Add: "$schema": "https://json.schemastore.org/claude-code-settings.json"',
|
||||
autoFixable: true,
|
||||
}));
|
||||
}
|
||||
|
||||
// Permissions checks
|
||||
if (parsed.permissions) {
|
||||
const perms = parsed.permissions;
|
||||
|
||||
if (!perms.deny || (Array.isArray(perms.deny) && perms.deny.length === 0)) {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.medium,
|
||||
title: 'No deny rules configured',
|
||||
description: `${file.relPath}: No permission deny rules. Claude can access all files including .env and secrets.`,
|
||||
file: file.absPath,
|
||||
recommendation: 'Add deny rules for sensitive files: "deny": ["Read(./.env)", "Read(./secrets/**)"]',
|
||||
autoFixable: false,
|
||||
}));
|
||||
}
|
||||
|
||||
if (!perms.allow || (Array.isArray(perms.allow) && perms.allow.length === 0)) {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.low,
|
||||
title: 'No allow rules configured',
|
||||
description: `${file.relPath}: No permission allow rules. This means frequent permission prompts for common operations.`,
|
||||
file: file.absPath,
|
||||
recommendation: 'Add allow rules for common tools: "allow": ["Bash(npm run *)", "Read(src/**)"]',
|
||||
autoFixable: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// hooks checks (basic — detailed in hook-validator)
|
||||
if (parsed.hooks) {
|
||||
if (Array.isArray(parsed.hooks)) {
|
||||
findings.push(finding({
|
||||
scanner: SCANNER,
|
||||
severity: SEVERITY.critical,
|
||||
title: 'Hooks configured as array instead of object',
|
||||
description: `${file.relPath}: "hooks" must be an object with event keys, not an array. All hooks will be ignored.`,
|
||||
file: file.absPath,
|
||||
evidence: '"hooks": [...]',
|
||||
recommendation: 'Change to object format: "hooks": { "PreToolUse": [...] }',
|
||||
autoFixable: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scannerResult(SCANNER, 'ok', findings, filesScanned, Date.now() - start);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue