32 lines
3.3 KiB
JavaScript
32 lines
3.3 KiB
JavaScript
"use strict";
|
|
|
|
const crypto = require("node:crypto");
|
|
|
|
const ACTIONS = new Set(["keep-link", "select-authoritative", "mark-historical", "archive-link", "monitor-only", "exclude-scan-root", "manual-link", "ignore", "manual-exclude"]);
|
|
|
|
class InventoryReviewService {
|
|
constructor({ store, audit = null }) { this.store = store; this.audit = audit; }
|
|
list(serverId) { return this.store.getInventoryReviewDecisions(serverId); }
|
|
preview({ serverId, workload, action, reason = "", repositoryFullName = null }) {
|
|
if (!ACTIONS.has(action)) throw Object.assign(new Error("Unsupported inventory review action."), { code: "INVENTORY_REVIEW_ACTION_INVALID" });
|
|
if (["ignore", "manual-exclude", "exclude-scan-root"].includes(action) && String(reason).trim().length < 5) throw Object.assign(new Error("A meaningful review reason is required."), { code: "INVENTORY_REVIEW_REASON_REQUIRED" });
|
|
if (action === "manual-link" && !repositoryFullName) throw Object.assign(new Error("Select the repository to link."), { code: "INVENTORY_REVIEW_REPOSITORY_REQUIRED" });
|
|
const linkedProfile = workload.link?.profileId && workload.link?.repositoryFullName ? { profileId: workload.link.profileId, repositoryFullName: workload.link.repositoryFullName } : null;
|
|
const configurationChanges = [`Persist review decision ${action} for workload ${workload.workloadId}`];
|
|
if (action === "archive-link" && linkedProfile) configurationChanges.push(`Archive deployment profile ${linkedProfile.profileId}`);
|
|
if (action === "manual-link") configurationChanges.push(`Remember ${repositoryFullName} as the reviewed repository match; use Save environment to create the deployment profile`);
|
|
const mutation = { serverId, workloadId: workload.workloadId, evidenceHash: workload.evidenceHash, action, reason: String(reason).trim(), repositoryFullName, linkedProfile, classification: workload.classification?.type || workload.status, containersUnaffected: true, configurationChanges, recovery: "Restore the configuration snapshot or rescan after evidence changes." };
|
|
return { ...mutation, id: crypto.createHash("sha256").update(JSON.stringify(mutation)).digest("hex") };
|
|
}
|
|
async apply({ plan, expectedPlanId }) {
|
|
if (!expectedPlanId || plan.id !== expectedPlanId) throw Object.assign(new Error("Inventory review requires the exact preview plan."), { code: expectedPlanId ? "INVENTORY_REVIEW_PLAN_STALE" : "INVENTORY_REVIEW_PLAN_REQUIRED" });
|
|
const snapshot = await this.store.createRecoverySnapshot?.(`inventory-review:${plan.serverId}:${plan.workloadId}`);
|
|
if (plan.action === "archive-link" && plan.linkedProfile) await this.store.deleteDeploymentProfile(plan.linkedProfile.repositoryFullName, plan.linkedProfile.profileId);
|
|
const decision = await this.store.saveInventoryReviewDecision(plan.serverId, { workloadId: plan.workloadId, evidenceHash: plan.evidenceHash, action: plan.action, reason: plan.reason, repositoryFullName: plan.repositoryFullName || null, classification: plan.classification, decidedAt: new Date().toISOString() });
|
|
await this.audit?.append?.("deployment.inventory-review-applied", { serverId: plan.serverId, workloadId: plan.workloadId, action: plan.action, evidenceHash: plan.evidenceHash, snapshot: snapshot?.filePath || null });
|
|
return { decision, snapshot };
|
|
}
|
|
}
|
|
|
|
module.exports = { InventoryReviewService, INVENTORY_REVIEW_ACTIONS: [...ACTIONS] };
|