feat: normalize deployment inventory evidence

This commit is contained in:
NuklearRabbit
2026-07-29 17:39:22 +02:00
parent 6b93391a9b
commit 5d3731a853
13 changed files with 433 additions and 45 deletions
+71
View File
@@ -0,0 +1,71 @@
"use strict";
const { deploymentIdentity, deploymentEvidenceHash, deploymentAuthorityKey } = require("./deployment-identity.cjs");
const BACKUP = /(?:^|[\\/._-])(backup|bak|archive|snapshot|old|previous)(?:[\\/._-]|$)/i;
const RELEASE = /(?:^|[\\/])(releases?|versions?)(?:[\\/]|$)/i;
const STAGING = /(?:^|[\\/._-])(staging|stage|test|qa|preview)(?:[\\/._-]|$)/i;
const SYSTEM = /^(?:traefik|nginx-proxy-manager|watchtower|portainer|dockerman|unraid-|cloudflared|redis|postgres|mariadb|mysql)(?:$|[-_.])/i;
function baseClassification(workload) {
const location = `${workload.compose?.workingDir || ""} ${(workload.compose?.configFiles || []).join(" ")}`;
const decision = workload.reviewDecision;
if (["manual-exclude", "exclude-scan-root", "ignore"].includes(decision?.action)) return { type: "manually-excluded", reason: decision.reason || "Persisted manual exclusion", decisionAction: decision.action };
if (["mark-historical", "archive-link"].includes(decision?.action)) return { type: "historical-compose", reason: decision.reason || "Reviewed as historical", decisionAction: decision.action };
if (decision?.action === "monitor-only") return { type: "monitor-only", reason: decision.reason || "Reviewed for monitoring only", decisionAction: decision.action };
if (workload.metadata?.staleLink) return { type: "stale-link", reason: "The linked deployment profile has no matching server workload" };
if (BACKUP.test(location)) return { type: "backup", reason: "Path matches backup/archive evidence" };
if (RELEASE.test(location)) return { type: "release-folder", reason: "Path is below a release/version directory" };
if (STAGING.test(location)) return { type: "staging", reason: "Path or project identifies a staging/test workload" };
if (SYSTEM.test(workload.displayName || "") && !workload.metadata?.sourceRepository) return { type: "system-container", reason: "Known infrastructure identity without repository provenance" };
if (workload.link && workload.runtime?.running) return { type: "active-application", reason: "Linked deployment with running container evidence" };
if (workload.link && !workload.runtime?.running) return { type: "stopped-application", reason: "Linked deployment without a running container" };
if (!workload.containers?.length && workload.compose?.configFiles?.length) return { type: "historical-compose", reason: "Compose definition exists without container runtime" };
if (workload.status === "ambiguous") return { type: "ambiguous", reason: "Multiple candidates have equivalent evidence" };
if (!workload.metadata?.sourceRepository && !workload.candidates?.length) return { type: "orphan-container", reason: "Runtime has no repository provenance or candidate" };
if (!workload.runtime?.running && workload.candidates?.length) return { type: "stopped-application", reason: "Stopped runtime has repository evidence" };
return { type: workload.runtime?.running ? "active-application" : "ambiguous", reason: workload.runtime?.running ? "Running application evidence" : "Insufficient authoritative evidence" };
}
function classifyInventory(workloads, profiles = [], decisions = []) {
const profileById = new Map(profiles.map((profile) => [profile.id, profile]));
const decisionByWorkload = new Map(decisions.map((decision) => [decision.workloadId, decision]));
const authorities = new Map();
const result = workloads.map((source) => {
const workload = structuredClone(source);
const profile = profileById.get(workload.link?.profileId) || null;
const identity = deploymentIdentity({ workload, profile });
const evidence = { candidates: (workload.candidates || []).map((item) => ({ repository: item.repositoryFullName, score: item.score, exact: item.exact === true })), running: workload.runtime?.running === true, health: workload.runtime?.health || null, configFiles: workload.compose?.configFiles || [] };
const hash = deploymentEvidenceHash(identity, evidence);
const stored = decisionByWorkload.get(workload.workloadId);
workload.reviewDecision = stored?.evidenceHash === hash ? stored : null;
workload.reviewDecisionStale = Boolean(stored && stored.evidenceHash !== hash);
workload.identity = identity;
if (workload.reviewDecision?.action === "manual-link" && workload.reviewDecision.repositoryFullName) {
workload.identity.repository = String(workload.reviewDecision.repositoryFullName).toLowerCase();
}
workload.evidenceHash = hash;
workload.classification = baseClassification(workload);
const key = deploymentAuthorityKey(identity);
if (identity.repository) {
const group = authorities.get(key) || [];
group.push(workload);
authorities.set(key, group);
}
return workload;
});
for (const group of authorities.values()) {
if (group.length < 2) continue;
const ranked = [...group].sort((a, b) => Number(b.reviewDecision?.action === "select-authoritative") - Number(a.reviewDecision?.action === "select-authoritative") || Number(b.runtime?.running) - Number(a.runtime?.running) || Number(Boolean(b.link)) - Number(Boolean(a.link)) || Number(Boolean(b.metadata?.liveRevision)) - Number(Boolean(a.metadata?.liveRevision)));
ranked[0].authoritative = true;
for (const duplicate of ranked.slice(1)) {
duplicate.authoritative = false;
duplicate.classification = { type: "duplicate", reason: `Conflicts with authoritative workload ${ranked[0].workloadId}`, authoritativeWorkloadId: ranked[0].workloadId };
duplicate.status = "duplicate";
duplicate.link = null;
}
}
return result;
}
module.exports = { classifyInventory, classifyWorkload: baseClassification, inventoryPathPatterns: { BACKUP, RELEASE, STAGING, SYSTEM } };