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
179
plugins/config-audit/scanners/lib/backup.mjs
Normal file
179
plugins/config-audit/scanners/lib/backup.mjs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
/**
|
||||
* Backup library for config-audit.
|
||||
* Creates timestamped backups of config files with checksums and manifests.
|
||||
* Zero external dependencies.
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, copyFileSync, mkdirSync, readdirSync, existsSync, statSync, rmSync, readFile } from 'node:fs';
|
||||
import { readFile as readFileAsync } from 'node:fs/promises';
|
||||
import { join, basename } from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const BACKUP_ROOT = join(homedir(), '.config-audit', 'backups');
|
||||
const MAX_BACKUPS = 10;
|
||||
|
||||
/**
|
||||
* Get the backup root directory path.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getBackupDir() {
|
||||
return BACKUP_ROOT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a timestamp-based backup ID.
|
||||
* @returns {string} Format: YYYYMMDD_HHMMSS
|
||||
*/
|
||||
export function generateBackupId() {
|
||||
const now = new Date();
|
||||
const y = now.getFullYear();
|
||||
const m = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(now.getDate()).padStart(2, '0');
|
||||
const h = String(now.getHours()).padStart(2, '0');
|
||||
const min = String(now.getMinutes()).padStart(2, '0');
|
||||
const s = String(now.getSeconds()).padStart(2, '0');
|
||||
return `${y}${m}${d}_${h}${min}${s}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a safe filename from a file path (replace path separators with _).
|
||||
* @param {string} filePath
|
||||
* @returns {string}
|
||||
*/
|
||||
export function safeFileName(filePath) {
|
||||
return filePath.replace(/[\\\/]/g, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate SHA-256 checksum of a buffer or string.
|
||||
* @param {Buffer|string} content
|
||||
* @returns {string}
|
||||
*/
|
||||
export function checksum(content) {
|
||||
return createHash('sha256').update(content).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a backup of the specified files.
|
||||
* @param {string[]} files - Array of absolute file paths to back up
|
||||
* @param {object} [opts]
|
||||
* @param {string} [opts.backupId] - Override backup ID (for testing)
|
||||
* @returns {{ backupId: string, backupPath: string, manifest: object }}
|
||||
*/
|
||||
export function createBackup(files, opts = {}) {
|
||||
const backupId = opts.backupId || generateBackupId();
|
||||
const backupPath = join(BACKUP_ROOT, backupId);
|
||||
const filesDir = join(backupPath, 'files');
|
||||
|
||||
mkdirSync(filesDir, { recursive: true });
|
||||
|
||||
const manifestFiles = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (!existsSync(file)) continue;
|
||||
|
||||
const safeName = safeFileName(file);
|
||||
copyFileSync(file, join(filesDir, safeName));
|
||||
|
||||
const content = readFileSync(file);
|
||||
const hash = checksum(content);
|
||||
const sizeBytes = statSync(file).size;
|
||||
|
||||
manifestFiles.push({
|
||||
originalPath: file,
|
||||
backupPath: `./files/${safeName}`,
|
||||
checksum: hash,
|
||||
sizeBytes,
|
||||
});
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
created_at: new Date().toISOString(),
|
||||
backup_id: backupId,
|
||||
files: manifestFiles,
|
||||
};
|
||||
|
||||
// Write manifest as YAML-like format
|
||||
const manifestYaml = serializeManifest(manifest);
|
||||
writeFileSync(join(backupPath, 'manifest.yaml'), manifestYaml);
|
||||
|
||||
// Cleanup old backups
|
||||
cleanupOldBackups();
|
||||
|
||||
return { backupId, backupPath, manifest };
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize manifest to YAML-like format.
|
||||
* @param {object} manifest
|
||||
* @returns {string}
|
||||
*/
|
||||
function serializeManifest(manifest) {
|
||||
let yaml = `created_at: "${manifest.created_at}"\n`;
|
||||
yaml += `backup_id: "${manifest.backup_id}"\n`;
|
||||
yaml += `files:\n`;
|
||||
for (const f of manifest.files) {
|
||||
yaml += ` - original_path: "${f.originalPath}"\n`;
|
||||
yaml += ` backup_path: "${f.backupPath}"\n`;
|
||||
yaml += ` checksum: "${f.checksum}"\n`;
|
||||
yaml += ` size_bytes: ${f.sizeBytes}\n`;
|
||||
}
|
||||
return yaml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a manifest.yaml file content.
|
||||
* @param {string} content
|
||||
* @returns {object}
|
||||
*/
|
||||
export function parseManifest(content) {
|
||||
const result = { created_at: '', backup_id: '', files: [] };
|
||||
|
||||
const createdMatch = content.match(/created_at:\s*"([^"]+)"/);
|
||||
if (createdMatch) result.created_at = createdMatch[1];
|
||||
|
||||
const idMatch = content.match(/backup_id:\s*"([^"]+)"/);
|
||||
if (idMatch) result.backup_id = idMatch[1];
|
||||
|
||||
// Parse file entries
|
||||
const fileBlocks = content.split(/\n\s+-\s+original_path:/).slice(1);
|
||||
for (const block of fileBlocks) {
|
||||
const origMatch = block.match(/^\s*"([^"]+)"/);
|
||||
const bpMatch = block.match(/backup_path:\s*"([^"]+)"/);
|
||||
const csMatch = block.match(/checksum:\s*"([^"]+)"/);
|
||||
const szMatch = block.match(/size_bytes:\s*(\d+)/);
|
||||
|
||||
if (origMatch && bpMatch && csMatch) {
|
||||
result.files.push({
|
||||
originalPath: origMatch[1],
|
||||
backupPath: bpMatch[1],
|
||||
checksum: csMatch[1],
|
||||
sizeBytes: szMatch ? parseInt(szMatch[1], 10) : 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove old backups beyond MAX_BACKUPS.
|
||||
*/
|
||||
function cleanupOldBackups() {
|
||||
if (!existsSync(BACKUP_ROOT)) return;
|
||||
|
||||
const dirs = readdirSync(BACKUP_ROOT, { withFileTypes: true })
|
||||
.filter(d => d.isDirectory())
|
||||
.map(d => d.name)
|
||||
.sort();
|
||||
|
||||
if (dirs.length > MAX_BACKUPS) {
|
||||
const toDelete = dirs.slice(0, dirs.length - MAX_BACKUPS);
|
||||
for (const dir of toDelete) {
|
||||
rmSync(join(BACKUP_ROOT, dir), { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { MAX_BACKUPS };
|
||||
Loading…
Add table
Add a link
Reference in a new issue