84 lines
6.8 KiB
JavaScript
84 lines
6.8 KiB
JavaScript
"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 TEMPORARY = /(?:^|[\\/._-])(candidate|rollback|ephemeral)(?:[\\/._-]|$)|^GITEA-ACTIONS-TASK-/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 sourceRepository = String(workload.metadata?.sourceRepository || "").trim();
|
|
const hasGitProvenance = /^(?:git@|ssh:\/\/|https?:\/\/)/i.test(sourceRepository);
|
|
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 (TEMPORARY.test(`${workload.displayName || ""} ${location}`)) return { type: "temporary-runtime", reason: "Runtime identity marks a candidate, rollback or CI 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.candidates?.length) return { type: "external-container", reason: hasGitProvenance ? "Repository provenance does not match an accessible configured Gitea repository" : "Runtime has no Git repository provenance and remains monitoring-only" };
|
|
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);
|
|
if (workload.link && ["backup", "release-folder", "staging", "temporary-runtime", "historical-compose", "system-container", "external-container", "manually-excluded"].includes(workload.classification.type)) {
|
|
workload.shadowedLink = workload.link;
|
|
workload.link = null;
|
|
}
|
|
const key = deploymentAuthorityKey(identity);
|
|
if (identity.repository && (workload.link || workload.candidates?.length) && !["backup", "release-folder", "staging", "temporary-runtime", "historical-compose", "system-container", "external-container", "manually-excluded"].includes(workload.classification.type)) {
|
|
const group = authorities.get(key) || [];
|
|
group.push(workload);
|
|
authorities.set(key, group);
|
|
}
|
|
return workload;
|
|
});
|
|
for (const group of authorities.values()) {
|
|
if (group.length < 2) {
|
|
group[0].authoritative = true;
|
|
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.shadowedLink = duplicate.link;
|
|
duplicate.link = null;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
module.exports = { classifyInventory, classifyWorkload: baseClassification, inventoryPathPatterns: { BACKUP, RELEASE, STAGING, TEMPORARY, SYSTEM } };
|