feat: initial open marketplace with llm-security, config-audit, ultraplan-local

This commit is contained in:
Kjell Tore Guttormsen 2026-04-06 18:47:49 +02:00
commit f93d6abdae
380 changed files with 65935 additions and 0 deletions

View file

@ -0,0 +1,84 @@
#!/usr/bin/env node
/**
* PreToolUse hook: auto-backup config files before Edit/Write.
* Reads $TOOL_INPUT to check if the target file is a config file.
* If yes, backs it up via scanners/lib/backup.mjs.
* Fast path no scanner execution.
*/
import { existsSync } from 'node:fs';
import { basename, dirname, sep } from 'node:path';
// Config file patterns to protect
const CONFIG_PATTERNS = [
/CLAUDE\.md$/i,
/CLAUDE\.local\.md$/i,
/settings\.json$/,
/settings\.local\.json$/,
/hooks\.json$/,
/\.mcp\.json$/,
/keybindings\.json$/,
];
const CONFIG_DIRS = ['rules'];
function isConfigFile(filePath) {
if (!filePath) return false;
const name = basename(filePath);
const dir = dirname(filePath);
// Check filename patterns
for (const pattern of CONFIG_PATTERNS) {
if (pattern.test(name)) return true;
}
// Check if inside a rules/ directory
for (const d of CONFIG_DIRS) {
if (dir.includes(`${sep}${d}${sep}`) || dir.endsWith(`${sep}${d}`)) {
if (name.endsWith('.md')) return true;
}
}
return false;
}
/**
* Read all data from stdin asynchronously.
* @returns {Promise<string>}
*/
function readStdin() {
return new Promise((resolve, reject) => {
const chunks = [];
process.stdin.setEncoding('utf-8');
process.stdin.on('data', chunk => chunks.push(chunk));
process.stdin.on('end', () => resolve(chunks.join('')));
process.stdin.on('error', reject);
});
}
async function main() {
let input;
try {
input = await readStdin();
} catch {
process.exit(0);
}
let toolInput;
try {
toolInput = JSON.parse(input);
} catch {
process.exit(0);
}
const filePath = toolInput.file_path || toolInput.path;
if (!filePath || !isConfigFile(filePath) || !existsSync(filePath)) {
process.exit(0);
}
const { createBackup } = await import('../../scanners/lib/backup.mjs');
const { backupPath } = createBackup([filePath]);
process.stderr.write(`[config-audit] Auto-backup: ${basename(filePath)}${backupPath}\n`);
}
main().catch(() => process.exit(0));

View file

@ -0,0 +1,18 @@
#!/usr/bin/env node
// Backup script for config-audit plugin
// Creates timestamped backups of config files before modification
// Usage: node backup-before-change.mjs <file1> [file2] ...
import { createBackup } from '../../scanners/lib/backup.mjs';
const files = process.argv.slice(2);
if (files.length === 0) {
process.stderr.write('Usage: node backup-before-change.mjs <file1> [file2] ...\n');
process.exit(1);
}
const { backupId, backupPath } = createBackup(files);
console.log(`Backup complete: ${backupPath}`);
console.log(backupPath);

View file

@ -0,0 +1,191 @@
#!/usr/bin/env node
/**
* PostToolUse hook: verify config files after Edit/Write.
* Runs the relevant single scanner on the edited file.
* Blocks if new critical/high findings are introduced.
* Timeout: 10 seconds (runs one scanner, not all 8).
* Graceful degradation: returns {} (allow) on any error.
*/
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { basename, dirname, resolve, sep } from 'node:path';
import { createHash } from 'node:crypto';
import { tmpdir } from 'node:os';
// Config file patterns (shared with auto-backup-config.mjs)
const CONFIG_PATTERNS = [
{ pattern: /CLAUDE\.md$/i, scanner: 'CML' },
{ pattern: /CLAUDE\.local\.md$/i, scanner: 'CML' },
{ pattern: /settings\.json$/, scanner: 'SET' },
{ pattern: /settings\.local\.json$/, scanner: 'SET' },
{ pattern: /hooks\.json$/, scanner: 'HKV' },
{ pattern: /\.mcp\.json$/, scanner: 'MCP' },
];
const RULES_DIR_PATTERN = /[/\\]rules[/\\]/;
function detectScanner(filePath) {
if (!filePath) return null;
const name = basename(filePath);
const dir = dirname(filePath);
for (const { pattern, scanner } of CONFIG_PATTERNS) {
if (pattern.test(name)) return scanner;
}
// Rules directory
if ((RULES_DIR_PATTERN.test(dir) || dir.endsWith(`${sep}rules`)) && name.endsWith('.md')) {
return 'RUL';
}
return null;
}
function getCacheKey(filePath) {
const hash = createHash('md5').update(filePath).digest('hex').slice(0, 8);
return resolve(tmpdir(), `config-audit-last-scan-${hash}.json`);
}
function loadPreviousScan(cacheFile) {
try {
if (existsSync(cacheFile)) {
return JSON.parse(readFileSync(cacheFile, 'utf-8'));
}
} catch { /* ignore */ }
return null;
}
function saveScanResult(cacheFile, result) {
try {
writeFileSync(cacheFile, JSON.stringify(result), 'utf-8');
} catch { /* ignore */ }
}
/**
* Read all data from stdin asynchronously.
* @returns {Promise<string>}
*/
function readStdin() {
return new Promise((resolve, reject) => {
const chunks = [];
process.stdin.setEncoding('utf-8');
process.stdin.on('data', chunk => chunks.push(chunk));
process.stdin.on('end', () => resolve(chunks.join('')));
process.stdin.on('error', reject);
// Safety: if no data arrives within 2s, resolve with empty string
setTimeout(() => resolve(chunks.join('')), 2000);
});
}
function allow() {
process.stdout.write('{}');
process.exit(0);
}
/**
* Walk up from filePath to find a likely project root.
*/
function findProjectRoot(fp) {
let dir = dirname(resolve(fp));
for (let i = 0; i < 10; i++) {
if (existsSync(resolve(dir, '.git')) || existsSync(resolve(dir, 'CLAUDE.md'))) {
return dir;
}
const parent = dirname(dir);
if (parent === dir) break;
dir = parent;
}
return dirname(resolve(fp));
}
async function main() {
// Read stdin
let raw;
try {
raw = await readStdin();
} catch {
allow();
return;
}
// Parse tool input
let toolInput;
try {
toolInput = JSON.parse(raw);
} catch {
allow();
return;
}
const filePath = toolInput.file_path || toolInput.path;
const scannerType = detectScanner(filePath);
if (!scannerType || !filePath || !existsSync(filePath)) {
allow();
return;
}
// Run the relevant scanner
const projectDir = findProjectRoot(filePath);
const { discoverConfigFiles } = await import('../../scanners/lib/file-discovery.mjs');
const { resetCounter } = await import('../../scanners/lib/output.mjs');
const scannerMap = {
CML: () => import('../../scanners/claude-md-linter.mjs'),
SET: () => import('../../scanners/settings-validator.mjs'),
HKV: () => import('../../scanners/hook-validator.mjs'),
RUL: () => import('../../scanners/rules-validator.mjs'),
MCP: () => import('../../scanners/mcp-config-validator.mjs'),
};
const loader = scannerMap[scannerType];
if (!loader) {
allow();
return;
}
resetCounter();
const { scan } = await loader();
const discovery = await discoverConfigFiles(projectDir, { includeGlobal: false });
const result = await scan(projectDir, discovery);
// Compare with previous scan
const cacheFile = getCacheKey(filePath);
const previous = loadPreviousScan(cacheFile);
// Save current result
saveScanResult(cacheFile, {
criticalCount: result.counts.critical || 0,
highCount: result.counts.high || 0,
findingCount: result.findings.length,
});
if (!previous) {
allow();
return;
}
// Check if new critical/high findings were introduced
const newCritical = (result.counts.critical || 0) - (previous.criticalCount || 0);
const newHigh = (result.counts.high || 0) - (previous.highCount || 0);
if (newCritical > 0 || newHigh > 0) {
const parts = [];
if (newCritical > 0) parts.push(`${newCritical} new critical`);
if (newHigh > 0) parts.push(`${newHigh} new high`);
const response = {
decision: 'block',
reason: `[config-audit] Edit introduced ${parts.join(' and ')} finding(s) in ${basename(filePath)}. Review with /config-audit posture`,
};
process.stdout.write(JSON.stringify(response));
} else {
process.stdout.write('{}');
}
}
main().catch(() => {
// Graceful degradation — always allow on error
process.stdout.write('{}');
process.exit(0);
});

View file

@ -0,0 +1,57 @@
#!/usr/bin/env node
// Check for active (incomplete) config-audit sessions on session start
// Non-blocking: always exits 0
import { readdirSync, readFileSync, existsSync } from 'fs';
import { join, basename } from 'path';
import { homedir } from 'os';
const sessionsDir = join(homedir(), '.config-audit', 'sessions');
if (!existsSync(sessionsDir)) {
process.exit(0);
}
function parseYamlValue(content, key) {
const match = content.match(new RegExp(`${key}:\\s*"?([^"\\n]*)"?`));
return match ? match[1].trim() : '';
}
const activeSessions = [];
try {
const entries = readdirSync(sessionsDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const stateFile = join(sessionsDir, entry.name, 'state.yaml');
if (!existsSync(stateFile)) continue;
const content = readFileSync(stateFile, 'utf-8');
const currentPhase = parseYamlValue(content, 'current_phase');
if (currentPhase && currentPhase !== 'verify' && currentPhase !== 'complete') {
const nextPhase = parseYamlValue(content, 'next_phase');
activeSessions.push({
id: entry.name,
phase: currentPhase,
next: nextPhase,
});
}
}
} catch {
process.exit(0);
}
if (activeSessions.length > 0) {
console.log(`config-audit: ${activeSessions.length} active session(s) found:`);
let lastNext = '';
for (const s of activeSessions) {
console.log(` - Session ${s.id}: phase=${s.phase}, next=${s.next}`);
lastNext = s.next;
}
console.log(` Resume with: /config-audit ${lastNext}`);
}
process.exit(0);

View file

@ -0,0 +1,66 @@
#!/usr/bin/env node
// Remind about current config-audit session phase on session end
// Returns JSON: {} if no active session, systemMessage if active
import { readdirSync, readFileSync, statSync, existsSync } from 'fs';
import { join, basename, dirname } from 'path';
import { homedir } from 'os';
const sessionsDir = join(homedir(), '.config-audit', 'sessions');
if (!existsSync(sessionsDir)) {
console.log('{}');
process.exit(0);
}
function parseYamlValue(content, key) {
const match = content.match(new RegExp(`${key}:\\s*"?([^"\\n]*)"?`));
return match ? match[1].trim() : '';
}
let latestState = '';
let latestTime = 0;
try {
const entries = readdirSync(sessionsDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const stateFile = join(sessionsDir, entry.name, 'state.yaml');
if (!existsSync(stateFile)) continue;
const fileTime = statSync(stateFile).mtimeMs;
if (fileTime > latestTime) {
latestTime = fileTime;
latestState = stateFile;
}
}
} catch {
console.log('{}');
process.exit(0);
}
if (latestState) {
// Only remind if session was touched in the last 2 hours (active work)
const twoHoursMs = 2 * 60 * 60 * 1000;
if (Date.now() - latestTime > twoHoursMs) {
console.log('{}');
process.exit(0);
}
const content = readFileSync(latestState, 'utf-8');
const currentPhase = parseYamlValue(content, 'current_phase');
const nextPhase = parseYamlValue(content, 'next_phase');
if (currentPhase && currentPhase !== 'verify' && currentPhase !== 'complete') {
const sessionId = basename(dirname(latestState));
console.log(JSON.stringify({
systemMessage: `config-audit: Session ${sessionId} is at phase '${currentPhase}'. Next: /config-audit ${nextPhase}`
}));
process.exit(0);
}
}
console.log('{}');
process.exit(0);