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
166
plugins/config-audit/scanners/rollback-engine.mjs
Normal file
166
plugins/config-audit/scanners/rollback-engine.mjs
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
/**
|
||||
* Config-Audit Rollback Engine
|
||||
* Restores configuration from backup with checksum verification.
|
||||
* Zero external dependencies.
|
||||
*/
|
||||
|
||||
import { readFile, writeFile, readdir, stat, rm } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { getBackupDir, parseManifest, checksum } from './lib/backup.mjs';
|
||||
|
||||
/**
|
||||
* List all available backups.
|
||||
* @returns {Promise<{ backups: object[] }>}
|
||||
*/
|
||||
export async function listBackups() {
|
||||
const backupRoot = getBackupDir();
|
||||
const backups = [];
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(backupRoot, { withFileTypes: true });
|
||||
} catch {
|
||||
return { backups: [] };
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
|
||||
const backupPath = join(backupRoot, entry.name);
|
||||
const manifestPath = join(backupPath, 'manifest.yaml');
|
||||
|
||||
try {
|
||||
const manifestContent = await readFile(manifestPath, 'utf-8');
|
||||
const manifest = parseManifest(manifestContent);
|
||||
|
||||
backups.push({
|
||||
id: entry.name,
|
||||
createdAt: manifest.created_at,
|
||||
files: manifest.files.map(f => ({
|
||||
originalPath: f.originalPath,
|
||||
backupPath: f.backupPath,
|
||||
checksum: f.checksum,
|
||||
sizeBytes: f.sizeBytes,
|
||||
})),
|
||||
});
|
||||
} catch {
|
||||
// Skip backups without valid manifest
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort newest first
|
||||
backups.sort((a, b) => b.id.localeCompare(a.id));
|
||||
|
||||
return { backups };
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore files from a backup.
|
||||
* @param {string} backupId
|
||||
* @param {object} [opts]
|
||||
* @param {boolean} [opts.dryRun=false]
|
||||
* @param {boolean} [opts.verify=true]
|
||||
* @returns {Promise<{ restored: object[], failed: object[] }>}
|
||||
*/
|
||||
export async function restoreBackup(backupId, opts = {}) {
|
||||
const verify = opts.verify !== false;
|
||||
const backupRoot = getBackupDir();
|
||||
const backupPath = join(backupRoot, backupId);
|
||||
const manifestPath = join(backupPath, 'manifest.yaml');
|
||||
|
||||
// Read manifest
|
||||
let manifestContent;
|
||||
try {
|
||||
manifestContent = await readFile(manifestPath, 'utf-8');
|
||||
} catch {
|
||||
throw new Error(`Backup not found: ${backupId}`);
|
||||
}
|
||||
|
||||
const manifest = parseManifest(manifestContent);
|
||||
const restored = [];
|
||||
const failed = [];
|
||||
|
||||
for (const fileEntry of manifest.files) {
|
||||
const backupFilePath = join(backupPath, fileEntry.backupPath);
|
||||
|
||||
if (opts.dryRun) {
|
||||
restored.push({
|
||||
originalPath: fileEntry.originalPath,
|
||||
status: 'dry-run',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read backup file
|
||||
const content = await readFile(backupFilePath);
|
||||
|
||||
// Verify checksum before restoring
|
||||
if (verify) {
|
||||
const hash = checksum(content);
|
||||
if (hash !== fileEntry.checksum) {
|
||||
failed.push({
|
||||
originalPath: fileEntry.originalPath,
|
||||
status: 'checksum-mismatch',
|
||||
error: `Expected ${fileEntry.checksum}, got ${hash}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Write to original path
|
||||
await writeFile(fileEntry.originalPath, content);
|
||||
|
||||
// Verify after write
|
||||
if (verify) {
|
||||
const written = await readFile(fileEntry.originalPath);
|
||||
const writtenHash = checksum(written);
|
||||
if (writtenHash !== fileEntry.checksum) {
|
||||
failed.push({
|
||||
originalPath: fileEntry.originalPath,
|
||||
status: 'checksum-mismatch',
|
||||
error: 'Checksum mismatch after write',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
restored.push({
|
||||
originalPath: fileEntry.originalPath,
|
||||
status: 'restored',
|
||||
});
|
||||
} catch (err) {
|
||||
failed.push({
|
||||
originalPath: fileEntry.originalPath,
|
||||
status: 'failed',
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { restored, failed };
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a backup directory.
|
||||
* @param {string} backupId
|
||||
* @returns {Promise<{ deleted: boolean, error?: string }>}
|
||||
*/
|
||||
export async function deleteBackup(backupId) {
|
||||
const backupRoot = getBackupDir();
|
||||
const backupPath = join(backupRoot, backupId);
|
||||
|
||||
try {
|
||||
await stat(backupPath);
|
||||
} catch {
|
||||
return { deleted: false, error: `Backup not found: ${backupId}` };
|
||||
}
|
||||
|
||||
try {
|
||||
await rm(backupPath, { recursive: true, force: true });
|
||||
return { deleted: true };
|
||||
} catch (err) {
|
||||
return { deleted: false, error: err.message };
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue