@calltelemetry/ct-lab-mcp 0.8.105 → 0.8.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/stig/SHA256SUMS +15 -1
- package/assets/stig/guest/collect-fips-state.sh +6 -0
- package/assets/stig/guest/collect-hardware.sh +3 -0
- package/assets/stig/guest/collect-host-checks.sh +3 -0
- package/assets/stig/guest/collect-network-services.sh +15 -0
- package/assets/stig/guest/collect-repolist.sh +3 -0
- package/assets/stig/guest/collect-rpm-inventory.sh +3 -0
- package/assets/stig/guest/collect-rpm-nevra.sh +3 -0
- package/assets/stig/guest/collect-rpmlist.sh +3 -0
- package/assets/stig/guest/collect-updateinfo-all.sh +3 -0
- package/assets/stig/guest/collect-updateinfo-security.sh +3 -0
- package/assets/stig/guest/collect-updateinfo-summary.sh +3 -0
- package/assets/stig/guest/oval-eval.sh +11 -0
- package/assets/stig/guest/oval-prep.sh +4 -0
- package/assets/stig/guest/verify-root.sh +13 -0
- package/assets/stig/image/verify-image.sh +28 -0
- package/dist/src/generated-version.d.ts +1 -1
- package/dist/src/generated-version.js +1 -1
- package/dist/src/stig/dist.d.ts +121 -0
- package/dist/src/stig/dist.d.ts.map +1 -0
- package/dist/src/stig/dist.js +234 -0
- package/dist/src/stig/dist.js.map +1 -0
- package/dist/src/stig/distribution.d.ts +95 -0
- package/dist/src/stig/distribution.d.ts.map +1 -0
- package/dist/src/stig/distribution.js +398 -0
- package/dist/src/stig/distribution.js.map +1 -0
- package/dist/src/stig/evidence.d.ts +135 -0
- package/dist/src/stig/evidence.d.ts.map +1 -0
- package/dist/src/stig/evidence.js +637 -0
- package/dist/src/stig/evidence.js.map +1 -0
- package/dist/src/stig/live.d.ts +67 -4
- package/dist/src/stig/live.d.ts.map +1 -1
- package/dist/src/stig/live.js +226 -5
- package/dist/src/stig/live.js.map +1 -1
- package/dist/src/stig/markers.d.ts +6 -0
- package/dist/src/stig/markers.d.ts.map +1 -0
- package/dist/src/stig/markers.js +23 -0
- package/dist/src/stig/markers.js.map +1 -0
- package/dist/src/stig/model.d.ts +2 -1
- package/dist/src/stig/model.d.ts.map +1 -1
- package/dist/src/stig/model.js +7 -2
- package/dist/src/stig/model.js.map +1 -1
- package/dist/src/stig/nodes.d.ts +52 -12
- package/dist/src/stig/nodes.d.ts.map +1 -1
- package/dist/src/stig/nodes.js +90 -11
- package/dist/src/stig/nodes.js.map +1 -1
- package/dist/src/stig/ports.d.ts +81 -2
- package/dist/src/stig/ports.d.ts.map +1 -1
- package/dist/src/stig/pve.d.ts +22 -0
- package/dist/src/stig/pve.d.ts.map +1 -0
- package/dist/src/stig/pve.js +26 -0
- package/dist/src/stig/pve.js.map +1 -0
- package/dist/src/stig/runner.d.ts +36 -9
- package/dist/src/stig/runner.d.ts.map +1 -1
- package/dist/src/stig/runner.js +129 -96
- package/dist/src/stig/runner.js.map +1 -1
- package/dist/src/tools/quick-provision.d.ts +2 -2
- package/dist/src/tools/slot-tools.d.ts +4 -4
- package/dist/src/tools/stig-tools.d.ts +52 -8
- package/dist/src/tools/stig-tools.d.ts.map +1 -1
- package/dist/src/tools/stig-tools.js +105 -21
- package/dist/src/tools/stig-tools.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { type StigCounts, type StigDescriptor, type StigReceiptSummary, stigStatusPreview } from "./model.js";
|
|
2
|
-
import { type StigNode, type StigNodeConfig } from "./nodes.js";
|
|
3
2
|
import type { ClaimRecord, StigDeps } from "./ports.js";
|
|
3
|
+
import { type StigNode, type StigNodeName } from "./nodes.js";
|
|
4
|
+
import { VMID_RANGE } from "./pve.js";
|
|
5
|
+
export declare const STIG_NODE_ANNOTATION = "calltelemetry.com/stig-node";
|
|
6
|
+
export { VMID_RANGE };
|
|
4
7
|
export declare const BUILD_CLAIM_TTL_HOURS = 6;
|
|
5
8
|
export declare const DEFAULT_CLONE_TTL_HOURS = 48;
|
|
6
|
-
export declare const
|
|
7
|
-
readonly min: 8100;
|
|
8
|
-
readonly max: 8899;
|
|
9
|
-
};
|
|
9
|
+
export declare const dopplerKeyFor: (cloneName: string) => string;
|
|
10
10
|
export interface ClonePlan {
|
|
11
11
|
mode: "plan";
|
|
12
|
-
node: StigNode;
|
|
13
12
|
recreate?: {
|
|
14
13
|
claim: string;
|
|
15
14
|
uid: string;
|
|
16
15
|
vmid: number;
|
|
17
16
|
};
|
|
17
|
+
untagged: boolean;
|
|
18
18
|
cloneName: string;
|
|
19
19
|
claim: string;
|
|
20
20
|
vmid: number;
|
|
@@ -22,6 +22,12 @@ export interface ClonePlan {
|
|
|
22
22
|
ref: string;
|
|
23
23
|
vmid: number;
|
|
24
24
|
};
|
|
25
|
+
node: StigNodeName;
|
|
26
|
+
host: string;
|
|
27
|
+
claimNode: string;
|
|
28
|
+
thin: string;
|
|
29
|
+
bridge: string;
|
|
30
|
+
vlan: number | null;
|
|
25
31
|
ttlHours: number;
|
|
26
32
|
poolPercent: number;
|
|
27
33
|
poolMaxPercent: number;
|
|
@@ -47,19 +53,30 @@ export interface BuildPlan {
|
|
|
47
53
|
}
|
|
48
54
|
export declare class StigRunner {
|
|
49
55
|
private readonly d;
|
|
50
|
-
|
|
51
|
-
readonly
|
|
52
|
-
|
|
56
|
+
/** Nodes with a clone re-encrypting in this process: one per node, nodes are independent. */
|
|
57
|
+
private static readonly reencrypting;
|
|
58
|
+
private readonly defaultNode;
|
|
59
|
+
/** `node` is the default for calls that do not name one (create_stig_claim's own `node` argument wins). */
|
|
60
|
+
constructor(d: StigDeps, node?: string);
|
|
61
|
+
get node(): StigNode;
|
|
53
62
|
private log;
|
|
63
|
+
private sh;
|
|
54
64
|
private listGuests;
|
|
65
|
+
/** VMIDs are per node: a live guest on the node, or a claim on the node's claim node, occupies one. */
|
|
55
66
|
private pickVmid;
|
|
67
|
+
/** The canonical lab-inventory clone puts guests on vmbr0; nodes that clone onto another bridge use the root-SSH path. */
|
|
68
|
+
private providerFor;
|
|
56
69
|
private poolPercent;
|
|
70
|
+
/** The node a claim lives on: its spec.node, which an explicit `node` argument must agree with. */
|
|
71
|
+
private nodeOfClaim;
|
|
57
72
|
private newestTemplate;
|
|
58
73
|
private step;
|
|
59
74
|
private ensureDir;
|
|
60
75
|
private writeSecretFile;
|
|
76
|
+
private buildNode;
|
|
61
77
|
planClone(i: {
|
|
62
78
|
cloneName: string;
|
|
79
|
+
node?: string;
|
|
63
80
|
templateRef?: string;
|
|
64
81
|
ttlHours?: number;
|
|
65
82
|
dns?: string[];
|
|
@@ -70,6 +87,7 @@ export declare class StigRunner {
|
|
|
70
87
|
}): Promise<ClonePlan>;
|
|
71
88
|
createClone(i: {
|
|
72
89
|
cloneName: string;
|
|
90
|
+
node?: string;
|
|
73
91
|
apply?: boolean;
|
|
74
92
|
planDigest?: string;
|
|
75
93
|
templateRef?: string;
|
|
@@ -106,20 +124,29 @@ export declare class StigRunner {
|
|
|
106
124
|
private annotateSummary;
|
|
107
125
|
validate(i: {
|
|
108
126
|
cloneName: string;
|
|
127
|
+
node?: string;
|
|
109
128
|
requireDisa?: boolean;
|
|
110
129
|
}): Promise<any>;
|
|
111
130
|
getReceipt(i: {
|
|
112
131
|
claim: string;
|
|
132
|
+
node?: string;
|
|
113
133
|
}): Promise<{
|
|
114
134
|
claim: string;
|
|
135
|
+
node: StigNodeName;
|
|
115
136
|
descriptor: Record<string, any> | null;
|
|
116
137
|
summary: StigReceiptSummary | null;
|
|
117
138
|
statusPreview: ReturnType<typeof stigStatusPreview>;
|
|
118
139
|
}>;
|
|
119
140
|
releaseTemplate(i: {
|
|
120
141
|
claim: string;
|
|
142
|
+
node?: string;
|
|
121
143
|
confirmDestroy: boolean;
|
|
122
144
|
}): Promise<any>;
|
|
123
145
|
}
|
|
146
|
+
/** The clone verification of clone.sh `verify` (FIPS, SELinux, LUKS2 + TPM2, bootstrap key, recovery test); throws unless the gate holds. */
|
|
147
|
+
export declare function verifyGuestFacts(d: Pick<StigDeps, "guest" | "secrets" | "readAsset">, escrowKey: string, sudoStdin: (extra?: string) => string): Promise<{
|
|
148
|
+
kv: Record<string, string>;
|
|
149
|
+
text: string;
|
|
150
|
+
}>;
|
|
124
151
|
export type { StigCounts, StigDescriptor, ClaimRecord };
|
|
125
152
|
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/stig/runner.ts"],"names":[],"mappings":"AAWA,OAAO,EAGL,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,kBAAkB,EAEyB,iBAAiB,EAExG,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/stig/runner.ts"],"names":[],"mappings":"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"}
|
package/dist/src/stig/runner.js
CHANGED
|
@@ -7,52 +7,61 @@
|
|
|
7
7
|
* Plans are pure. Applies need the digest of the plan they were shown.
|
|
8
8
|
*/
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { STIG_RECEIPT_ANNOTATION, STIG_STEP_ANNOTATION, STIG_STEPS, STIG_PINS, STIG_DATASTREAM_SHA256, DISA_BENCHMARK_SHA256, STIG_TAILORING_SHA256, STIG_DVD_ISO, STIG_TEMPLATE_PREFIX, STIG_ANNOTATION_MAX_BYTES, STIG_POOL_LIMIT_PERCENT, COUNT_KEYS, assertPoolBelow, assertSingleReencrypt, assertNoYoungStigClone, disaCountsFromKv, DISA_PROFILE_ID, assertTemplateReleaseAllowed, buildReceiptSummary, capTtl, countsFromKv, defaultDescriptor, parseKv, planTemplateRenewal, receiptGate, redact, stigDescriptor, stigStatusPreview, validateReceiptSummary, } from "./model.js";
|
|
13
13
|
import { sha256File } from "./receipt.js";
|
|
14
|
-
import {
|
|
14
|
+
import { clearStage, cloneIdentity, markStage } from "./markers.js";
|
|
15
|
+
import { STIG_BUILD_NODE, STIG_PROVIDER_BRIDGE, nodeForClaimNode, resolveStigNode } from "./nodes.js";
|
|
16
|
+
import { VMID_RANGE, listGuests as listGuestsOn, lowestFreeVmid, parsePvesmStatus, shq } from "./pve.js";
|
|
17
|
+
export const STIG_NODE_ANNOTATION = "calltelemetry.com/stig-node";
|
|
18
|
+
export { VMID_RANGE };
|
|
15
19
|
export const BUILD_CLAIM_TTL_HOURS = 6;
|
|
16
20
|
export const DEFAULT_CLONE_TTL_HOURS = 48;
|
|
17
|
-
export const VMID_RANGE = { min: 8100, max: 8899 };
|
|
18
21
|
const CI_USER = "ctadmin";
|
|
19
22
|
const DEFAULT_DNS = ["192.168.126.1", "192.168.68.125"];
|
|
20
23
|
const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
21
|
-
const
|
|
22
|
-
const dopplerKeyFor = (cloneName) => `LAB_RHEL8_RECOVERY_${cloneName.toUpperCase().replace(/-/g, "_")}`;
|
|
24
|
+
export const dopplerKeyFor = (cloneName) => `LAB_RHEL8_RECOVERY_${cloneName.toUpperCase().replace(/-/g, "_")}`;
|
|
23
25
|
export class StigRunner {
|
|
24
26
|
d;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
/** Nodes with a clone re-encrypting in this process: one per node, nodes are independent. */
|
|
28
|
+
static reencrypting = new Set();
|
|
29
|
+
defaultNode;
|
|
30
|
+
/** `node` is the default for calls that do not name one (create_stig_claim's own `node` argument wins). */
|
|
31
|
+
constructor(d, node = STIG_BUILD_NODE) {
|
|
28
32
|
this.d = d;
|
|
29
|
-
this.
|
|
33
|
+
this.defaultNode = resolveStigNode(node);
|
|
30
34
|
}
|
|
35
|
+
get node() { return this.defaultNode; }
|
|
31
36
|
log(line) { this.d.log.info(redact(line)); }
|
|
32
37
|
/* ------------------------------------------------------------ helpers */
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
sh(node) { return this.d.pveFor(node.host); }
|
|
39
|
+
listGuests(node) { return listGuestsOn(this.sh(node)); }
|
|
40
|
+
/** VMIDs are per node: a live guest on the node, or a claim on the node's claim node, occupies one. */
|
|
41
|
+
async pickVmid(node, guests) {
|
|
42
|
+
const claims = (await this.d.claims.list()).filter((c) => (c.spec?.node ?? "minis") === node.claimNode);
|
|
43
|
+
return lowestFreeVmid(guests ?? (await this.listGuests(node)), claims);
|
|
38
44
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
used.add(c.spec.vmid);
|
|
45
|
-
for (let v = VMID_RANGE.min; v <= VMID_RANGE.max; v++)
|
|
46
|
-
if (!used.has(v))
|
|
47
|
-
return v;
|
|
48
|
-
throw new Error(`no free VMID in ${VMID_RANGE.min}-${VMID_RANGE.max}`);
|
|
45
|
+
/** The canonical lab-inventory clone puts guests on vmbr0; nodes that clone onto another bridge use the root-SSH path. */
|
|
46
|
+
providerFor(node, forClone = false) {
|
|
47
|
+
if (!this.d.provider)
|
|
48
|
+
return undefined;
|
|
49
|
+
return forClone && node.bridge !== STIG_PROVIDER_BRIDGE ? undefined : this.d.provider;
|
|
49
50
|
}
|
|
50
|
-
async poolPercent() {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
async poolPercent(node) {
|
|
52
|
+
return parsePvesmStatus((await this.sh(node).run(`pvesm status --storage ${node.thin}`)).stdout).percent;
|
|
53
|
+
}
|
|
54
|
+
/** The node a claim lives on: its spec.node, which an explicit `node` argument must agree with. */
|
|
55
|
+
nodeOfClaim(rec, explicit) {
|
|
56
|
+
const fromClaim = nodeForClaimNode(rec.spec?.node);
|
|
57
|
+
if (!fromClaim)
|
|
58
|
+
throw new Error(`claim ${rec.metadata.name} is on node '${rec.spec?.node}', which is not in the STIG node map`);
|
|
59
|
+
if (explicit !== undefined) {
|
|
60
|
+
const want = resolveStigNode(explicit);
|
|
61
|
+
if (want.name !== fromClaim.name)
|
|
62
|
+
throw new Error(`claim ${rec.metadata.name} lives on ${fromClaim.name}, not ${want.name}`);
|
|
63
|
+
}
|
|
64
|
+
return fromClaim;
|
|
56
65
|
}
|
|
57
66
|
newestTemplate(guests) {
|
|
58
67
|
return guests.filter((g) => g.template === 1 && g.name.startsWith(STIG_TEMPLATE_PREFIX)).sort((a, b) => (a.name < b.name ? -1 : 1)).pop();
|
|
@@ -63,7 +72,10 @@ export class StigRunner {
|
|
|
63
72
|
ensureDir(path) { mkdirSync(path, { recursive: true, mode: 0o700 }); chmodSync(path, 0o700); return path; }
|
|
64
73
|
writeSecretFile(path, content) { writeFileSync(path, content, { mode: 0o600 }); chmodSync(path, 0o600); }
|
|
65
74
|
/* ------------------------------------------------------------ clone */
|
|
75
|
+
buildNode() { return resolveStigNode(STIG_BUILD_NODE); }
|
|
66
76
|
async planClone(i) {
|
|
77
|
+
const node = i.node ? resolveStigNode(i.node) : this.defaultNode;
|
|
78
|
+
const untagged = i.untagged === true;
|
|
67
79
|
if (!/^[a-z0-9]([a-z0-9-]{0,40}[a-z0-9])?$/.test(i.cloneName))
|
|
68
80
|
throw new Error("cloneName must be a lowercase DNS label (a-z, 0-9, -)");
|
|
69
81
|
const ttlHours = capTtl(i.ttlHours ?? DEFAULT_CLONE_TTL_HOURS);
|
|
@@ -84,18 +96,18 @@ export class StigRunner {
|
|
|
84
96
|
throw new Error(`claim ${claim} is not a STIG clone claim; refusing to recreate it`);
|
|
85
97
|
recreate = { claim, uid: existing.metadata.uid, vmid: existing.spec?.vmid };
|
|
86
98
|
}
|
|
87
|
-
const guests = await this.listGuests();
|
|
99
|
+
const guests = await this.listGuests(node);
|
|
88
100
|
const tpl = i.templateRef ? guests.find((g) => g.template === 1 && g.name === i.templateRef) : this.newestTemplate(guests);
|
|
89
101
|
if (!tpl)
|
|
90
|
-
throw new Error(i.templateRef ? `template ${i.templateRef} not found` : `no ${STIG_TEMPLATE_PREFIX}* template found`);
|
|
91
|
-
const vmid = await this.pickVmid(guests);
|
|
92
|
-
const poolPercent = await this.poolPercent();
|
|
102
|
+
throw new Error(i.templateRef ? `template ${i.templateRef} not found on ${node.name}` : `no ${STIG_TEMPLATE_PREFIX}* template found on ${node.name}`);
|
|
103
|
+
const vmid = await this.pickVmid(node, guests);
|
|
104
|
+
const poolPercent = await this.poolPercent(node);
|
|
93
105
|
const poolMaxPercent = i.poolMaxPercent ?? STIG_POOL_LIMIT_PERCENT;
|
|
94
106
|
if (!Number.isInteger(poolMaxPercent) || poolMaxPercent < 10 || poolMaxPercent > STIG_POOL_LIMIT_PERCENT)
|
|
95
107
|
throw new Error(`poolMaxPercent must be 10-${STIG_POOL_LIMIT_PERCENT}`);
|
|
96
108
|
let blockedBy = null;
|
|
97
|
-
if (
|
|
98
|
-
blockedBy = `node ${
|
|
109
|
+
if (node.vlan === null && !untagged)
|
|
110
|
+
blockedBy = `node ${node.name} has no VLAN configured for clones; a tagged clone is not possible there. Pass untagged=true for an untagged clone.`;
|
|
99
111
|
try {
|
|
100
112
|
if (!blockedBy)
|
|
101
113
|
assertPoolBelow(poolPercent, poolMaxPercent);
|
|
@@ -105,16 +117,17 @@ export class StigRunner {
|
|
|
105
117
|
}
|
|
106
118
|
if (!blockedBy) {
|
|
107
119
|
try {
|
|
108
|
-
assertSingleReencrypt(
|
|
120
|
+
assertSingleReencrypt(await this.d.claims.list(), claim, node.claimNode);
|
|
109
121
|
assertNoYoungStigClone(guests);
|
|
110
122
|
}
|
|
111
123
|
catch (e) {
|
|
112
124
|
blockedBy = e.message;
|
|
113
125
|
}
|
|
114
126
|
}
|
|
115
|
-
const base = { cloneName: i.cloneName, claim, vmid, template: { ref: tpl.name, vmid: tpl.vmid }, ttlHours, dns: i.dns ?? DEFAULT_DNS, ntp: i.ntp ?? [], poolMaxPercent,
|
|
127
|
+
const base = { cloneName: i.cloneName, node: node.name, host: node.host, thin: node.thin, bridge: node.bridge, vlan: node.vlan, claim, vmid, template: { ref: tpl.name, vmid: tpl.vmid }, ttlHours, dns: i.dns ?? DEFAULT_DNS, ntp: i.ntp ?? [], poolMaxPercent, untagged, recreate: recreate ?? null };
|
|
116
128
|
return {
|
|
117
|
-
mode: "plan",
|
|
129
|
+
mode: "plan", ...(recreate ? { recreate } : {}), untagged, cloneName: i.cloneName, node: node.name, host: node.host, claimNode: node.claimNode, thin: node.thin, bridge: node.bridge, vlan: node.vlan,
|
|
130
|
+
claim, vmid, template: base.template, ttlHours, poolPercent, poolMaxPercent, poolOk: blockedBy === null, blockedBy,
|
|
118
131
|
escrowKey: dopplerKeyFor(i.cloneName), steps: STIG_STEPS.clone, planDigest: digest({ kind: "clone", ...base }),
|
|
119
132
|
};
|
|
120
133
|
}
|
|
@@ -124,6 +137,7 @@ export class StigRunner {
|
|
|
124
137
|
return plan;
|
|
125
138
|
if (!i.planDigest || i.planDigest !== plan.planDigest)
|
|
126
139
|
throw new Error("plan digest missing or stale; re-run the plan and apply with its planDigest");
|
|
140
|
+
const node = resolveStigNode(plan.node);
|
|
127
141
|
if (plan.recreate) {
|
|
128
142
|
// Release the old clone first: the operator destroys its guest when the claim is deleted.
|
|
129
143
|
this.log(`recreate: releasing claim ${plan.recreate.claim} (vmid ${plan.recreate.vmid})`);
|
|
@@ -132,28 +146,29 @@ export class StigRunner {
|
|
|
132
146
|
await this.d.clock.sleep(10_000);
|
|
133
147
|
if (await this.d.claims.get(plan.recreate.claim))
|
|
134
148
|
throw new Error(`claim ${plan.recreate.claim} is still present after release; not creating its replacement`);
|
|
135
|
-
for (let n = 0; n < 60 && (await this.listGuests()).some((g) => g.vmid === plan.recreate.vmid); n++)
|
|
149
|
+
for (let n = 0; n < 60 && (await this.listGuests(node)).some((g) => g.vmid === plan.recreate.vmid); n++)
|
|
136
150
|
await this.d.clock.sleep(10_000);
|
|
137
151
|
plan = await this.planClone({ ...i, recreate: false });
|
|
138
152
|
}
|
|
139
153
|
// Guards run before anything is created: a refused request leaves no claim and no guest.
|
|
140
|
-
if (plan.blockedBy && /VLAN
|
|
154
|
+
if (plan.blockedBy && /no VLAN configured/.test(plan.blockedBy))
|
|
141
155
|
throw new Error(plan.blockedBy);
|
|
142
156
|
assertPoolBelow(plan.poolPercent, plan.poolMaxPercent);
|
|
143
|
-
assertSingleReencrypt(
|
|
144
|
-
assertNoYoungStigClone(await this.listGuests());
|
|
145
|
-
if (StigRunner.reencrypting)
|
|
146
|
-
throw new Error(
|
|
147
|
-
StigRunner.reencrypting
|
|
157
|
+
assertSingleReencrypt(await this.d.claims.list(), plan.claim, node.claimNode);
|
|
158
|
+
assertNoYoungStigClone(await this.listGuests(node));
|
|
159
|
+
if (StigRunner.reencrypting.has(node.name))
|
|
160
|
+
throw new Error(`another clone is re-encrypting on ${node.name} in this process; only one at a time per node`);
|
|
161
|
+
StigRunner.reencrypting.add(node.name);
|
|
148
162
|
try {
|
|
149
163
|
return await this.applyClone(plan, i);
|
|
150
164
|
}
|
|
151
165
|
finally {
|
|
152
|
-
StigRunner.reencrypting
|
|
166
|
+
StigRunner.reencrypting.delete(node.name);
|
|
153
167
|
}
|
|
154
168
|
}
|
|
155
169
|
async applyClone(plan, i) {
|
|
156
170
|
const { cloneName, claim, vmid, template, ttlHours, escrowKey } = plan;
|
|
171
|
+
const node = resolveStigNode(plan.node);
|
|
157
172
|
const state = this.ensureDir(join(this.d.baseDir, `clone-${cloneName}`));
|
|
158
173
|
this.ensureDir(join(state, "evidence"));
|
|
159
174
|
const keys = await this.d.materials.sshKeypair(state, `claim-${claim}`);
|
|
@@ -162,55 +177,58 @@ export class StigRunner {
|
|
|
162
177
|
this.writeSecretFile(join(state, "ctadmin_password"), adminPassword);
|
|
163
178
|
writeFileSync(join(state, "vmid"), `${vmid}\n`);
|
|
164
179
|
writeFileSync(join(state, "template"), `${template.ref}\n`);
|
|
165
|
-
writeFileSync(join(state, "node"), `${
|
|
180
|
+
writeFileSync(join(state, "node"), `${node.name}\n`);
|
|
166
181
|
const knownHosts = join(state, "known_hosts");
|
|
167
182
|
this.d.guest.configure({ user: CI_USER, keyFile: keys.privateKeyFile, knownHostsFile: knownHosts });
|
|
168
183
|
const sudoStdin = (extra = "") => `${adminPassword}\n${extra}`;
|
|
169
184
|
// 1. claim first, wait for the operator, then (and only then) touch Proxmox.
|
|
170
|
-
this.log(`claim ${claim}: creating (vmid ${vmid}, ${ttlHours}h)`);
|
|
185
|
+
this.log(`claim ${claim}: creating (vmid ${vmid}, ${ttlHours}h) on ${node.name}`);
|
|
171
186
|
// The descriptor is spec.stig, set at creation: immutable and part of the claim's custody request.
|
|
172
187
|
const descriptor = defaultDescriptor("clone", { template, adminKeyRef: `doppler:${escrowKey}`, dns: i.dns ?? DEFAULT_DNS, ntp: i.ntp ?? [], tailoringSha256: STIG_TAILORING_SHA256 });
|
|
173
|
-
await this.d.claims.create({ name: claim, vmid, vmName: cloneName, ttlHours, node:
|
|
188
|
+
await this.d.claims.create({ name: claim, vmid, vmName: cloneName, ttlHours, node: node.claimNode, stig: descriptor });
|
|
174
189
|
await this.d.claims.waitReady(claim);
|
|
175
|
-
|
|
190
|
+
// The node rides on the claim so later verbs (validate, receipt, release) resolve it without being told.
|
|
191
|
+
await this.d.claims.annotate(claim, { [STIG_STEP_ANNOTATION]: "claim", [STIG_NODE_ANNOTATION]: node.name });
|
|
176
192
|
await this.step(claim, "pool-guard");
|
|
177
193
|
// 2. clone, fresh TPM, cloud-init, VLAN126 (the canonical lab-inventory verb, or root-SSH qm as the fallback)
|
|
178
194
|
await this.step(claim, "clone");
|
|
179
|
-
this.log(`proxmox: full clone of ${template.ref} (${template.vmid}) to ${vmid}`);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
195
|
+
this.log(`proxmox ${node.name}: full clone of ${template.ref} (${template.vmid}) to ${vmid}`);
|
|
196
|
+
// The canonical lab-inventory stig verbs address the build node only; the other nodes use the root-SSH fallback.
|
|
197
|
+
const provider = this.providerFor(node, true);
|
|
198
|
+
if (provider) {
|
|
199
|
+
const others = (await this.d.claims.list()).filter((c) => c.metadata.name !== claim && (c.spec?.node ?? "minis") === node.claimNode).map((c) => c.spec?.vmid).filter((v) => Number.isInteger(v));
|
|
200
|
+
await provider.clone({ operationId: `stig-clone-${claim}-${this.d.clock.now()}`, hostAssetId: node.hostAssetId, untagged: plan.untagged || node.vlan === null, name: cloneName, templateName: template.ref, vmid, claimedVmids: others, sshPublicKey: keys.publicKey, passwordHash: adminHash, dns: i.dns ?? DEFAULT_DNS });
|
|
183
201
|
await this.step(claim, "tpm");
|
|
184
202
|
await this.step(claim, "cloud-init");
|
|
185
203
|
}
|
|
186
204
|
else {
|
|
187
|
-
await this.pveOk(`set -e\nqm clone ${template.vmid} ${vmid} --name ${cloneName} --full 1 --storage
|
|
205
|
+
await this.pveOk(`set -e\nqm clone ${template.vmid} ${vmid} --name ${cloneName} --full 1 --storage ${node.thin} --description ${shq(`Claim ${claim}; clone of ${template.ref} (RHEL 8.10 STIG)`)}`, undefined, node);
|
|
188
206
|
await this.step(claim, "tpm");
|
|
189
207
|
// Never shared with the template or another clone.
|
|
190
|
-
await this.pveOk(`set -e\nif qm config ${vmid} | grep -q '^tpmstate0'; then qm set ${vmid} --delete tpmstate0; fi\nqm set ${vmid} --tpmstate0
|
|
208
|
+
await this.pveOk(`set -e\nif qm config ${vmid} | grep -q '^tpmstate0'; then qm set ${vmid} --delete tpmstate0; fi\nqm set ${vmid} --tpmstate0 ${node.thin}:1,version=v2.0`, undefined, node);
|
|
191
209
|
await this.step(claim, "cloud-init");
|
|
192
210
|
const dns = (i.dns ?? DEFAULT_DNS).join(" ");
|
|
193
211
|
// The password hash and the public key arrive on stdin; PVE would otherwise hash with sha256-crypt (STIG wants sha512).
|
|
194
212
|
await this.pveOk(`set -e\numask 077\nP=/tmp/${claim}.pub; W=/tmp/${claim}.pw\ncat > "$P" <<'KEY'\n${keys.publicKey}\nKEY\nIFS= read -r HASH\nprintf '%s' "$HASH" > "$W"\n` +
|
|
195
213
|
`qm set ${vmid} --ciuser ${CI_USER} --cipassword "$(cat "$W")" --sshkeys "$P" --ipconfig0 ip=dhcp --nameserver ${shq(dns)} --agent 1 --tags rhel8-stig-clone >/dev/null\n` +
|
|
196
|
-
`qm set ${vmid} --net0 "$(qm config ${vmid} | sed -n 's/^net0: \\([^,]*\\),.*/\\1/p'),bridge
|
|
214
|
+
`qm set ${vmid} --net0 "$(qm config ${vmid} | sed -n 's/^net0: \\([^,]*\\),.*/\\1/p'),bridge=${node.bridge}${plan.untagged || node.vlan === null ? "" : `,tag=${node.vlan}`}"\nrm -f "$P" "$W"`, { stdin: `${adminHash}\n` }, node);
|
|
197
215
|
}
|
|
198
216
|
// 3. boot, first-boot re-key, TPM bind
|
|
199
217
|
await this.step(claim, "boot");
|
|
200
|
-
if (!
|
|
201
|
-
await this.pveOk(`qm start ${vmid}
|
|
218
|
+
if (!provider)
|
|
219
|
+
await this.pveOk(`qm start ${vmid}`, undefined, node);
|
|
202
220
|
let ip = "";
|
|
203
221
|
for (let n = 0; n < 60 && !ip; n++) {
|
|
204
|
-
if (
|
|
222
|
+
if (provider) {
|
|
205
223
|
try {
|
|
206
|
-
ip = await
|
|
224
|
+
ip = await provider.guestIp(vmid, node.hostAssetId);
|
|
207
225
|
}
|
|
208
226
|
catch {
|
|
209
227
|
ip = "";
|
|
210
228
|
}
|
|
211
229
|
}
|
|
212
230
|
else {
|
|
213
|
-
const r = await this.
|
|
231
|
+
const r = await this.sh(node).run(`qm guest cmd ${vmid} network-get-interfaces`);
|
|
214
232
|
if (r.code === 0)
|
|
215
233
|
ip = guestIp(r.stdout);
|
|
216
234
|
}
|
|
@@ -254,34 +272,21 @@ export class StigRunner {
|
|
|
254
272
|
writeFileSync(join(state, "verify.txt"), verify.text);
|
|
255
273
|
// 6. fstrim: re-encryption fully allocates the thin disk
|
|
256
274
|
await this.step(claim, "trim");
|
|
257
|
-
const before = await this.poolPercent();
|
|
275
|
+
const before = await this.poolPercent(node);
|
|
258
276
|
await this.d.guest.runScript(this.d.readAsset("guest/trim.sh"), { sudo: true, stdin: sudoStdin() });
|
|
259
|
-
const after = await this.poolPercent();
|
|
277
|
+
const after = await this.poolPercent(node);
|
|
260
278
|
this.log(`pool before trim ${before}%, after ${after}%`);
|
|
261
279
|
await this.step(claim, "complete");
|
|
262
|
-
return { ok: true, claim, vmid, name: cloneName, ip, template: template.ref, escrowKey, verify: verify.kv, poolBefore: before, poolAfter: after, stateDir: state };
|
|
280
|
+
return { ok: true, node: node.name, claim, vmid, name: cloneName, ip, template: template.ref, escrowKey, verify: verify.kv, poolBefore: before, poolAfter: after, stateDir: state };
|
|
263
281
|
}
|
|
264
|
-
async pveOk(script, opts) {
|
|
265
|
-
const r = await this.
|
|
282
|
+
async pveOk(script, opts, node = this.buildNode()) {
|
|
283
|
+
const r = await this.sh(node).run(script, opts);
|
|
266
284
|
if (r.code !== 0)
|
|
267
285
|
throw new Error(`proxmox command failed (exit ${r.code}): ${redact(r.stderr).slice(0, 200)}`);
|
|
268
286
|
return r;
|
|
269
287
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const root = await this.d.guest.runScript(this.d.readAsset("guest/verify-root.sh"), { sudo: true, stdin: sudoStdin() });
|
|
273
|
-
let text = `${user.stdout.trim()}\n${root.stdout.trim()}`;
|
|
274
|
-
const rec = await this.d.secrets.get(escrowKey);
|
|
275
|
-
if (rec) {
|
|
276
|
-
const t = await this.d.guest.runScript(this.d.readAsset("guest/test-recovery.sh"), { sudo: true, stdin: sudoStdin(rec) });
|
|
277
|
-
text += `\nrecovery_passphrase_test=${t.stdout.trim().split("\n").pop() ?? ""}`;
|
|
278
|
-
}
|
|
279
|
-
text += "\n";
|
|
280
|
-
const kv = parseKv(text);
|
|
281
|
-
const okGate = kv.fips_enabled === "1" && kv.selinux === "Enforcing" && kv.tpm_bound === "1" && kv.bootstrap_key_file === "absent";
|
|
282
|
-
if (!okGate)
|
|
283
|
-
throw new Error("VERIFY FAILED: fips/selinux/tpm/bootstrap-key checks did not all pass");
|
|
284
|
-
return { kv, text };
|
|
288
|
+
verifyGuest(escrowKey, sudoStdin) {
|
|
289
|
+
return verifyGuestFacts(this.d, escrowKey, sudoStdin);
|
|
285
290
|
}
|
|
286
291
|
/* ------------------------------------------------------------ build */
|
|
287
292
|
async planBuild(i) {
|
|
@@ -296,7 +301,7 @@ export class StigRunner {
|
|
|
296
301
|
const dvd = iso.code === 0 && iso.stdout.includes(STIG_DVD_ISO.split("/").pop());
|
|
297
302
|
if (!dvd)
|
|
298
303
|
throw new Error(`RHEL 8.10 DVD ${STIG_DVD_ISO} is not present on Proxmox storage`);
|
|
299
|
-
const vmid = await this.pickVmid();
|
|
304
|
+
const vmid = await this.pickVmid(this.buildNode());
|
|
300
305
|
const date = (i.now ?? new Date(this.d.clock.now()).toISOString().slice(0, 10)).replace(/-/g, "");
|
|
301
306
|
const name = i.name ?? `${STIG_TEMPLATE_PREFIX}${date}-${vmid}`;
|
|
302
307
|
if (!/^rhel810-stig-\d{8}-\d{4}$/.test(name))
|
|
@@ -323,7 +328,7 @@ export class StigRunner {
|
|
|
323
328
|
// claim first
|
|
324
329
|
this.log(`claim ${claim}: creating (vmid ${vmid}, ${BUILD_CLAIM_TTL_HOURS}h)`);
|
|
325
330
|
const descriptor = defaultDescriptor("build-template", { adminKeyRef: "doppler:LAB_PROXMOX_MINIS_API_TOKEN", tailoringSha256: STIG_TAILORING_SHA256, dns: i.dns, ntp: i.ntp });
|
|
326
|
-
await this.d.claims.create({ name: claim, vmid, vmName: name, ttlHours: BUILD_CLAIM_TTL_HOURS, node: this.
|
|
331
|
+
await this.d.claims.create({ name: claim, vmid, vmName: name, ttlHours: BUILD_CLAIM_TTL_HOURS, node: this.buildNode().claimNode, stig: descriptor });
|
|
327
332
|
await this.d.claims.waitReady(claim);
|
|
328
333
|
await this.d.claims.annotate(claim, { [STIG_STEP_ANNOTATION]: "claim" });
|
|
329
334
|
// secrets by NAME, straight into the Packer environment
|
|
@@ -408,6 +413,7 @@ export class StigRunner {
|
|
|
408
413
|
const rec = await this.d.claims.get(claim);
|
|
409
414
|
if (!rec)
|
|
410
415
|
throw new Error(`claim ${claim} not found`);
|
|
416
|
+
const node = this.nodeOfClaim(rec, i.node); // the scan runs over SSH to the guest; the node is checked, not contacted
|
|
411
417
|
const pw = readFileSync(join(state, "ctadmin_password"), "utf8");
|
|
412
418
|
const ip = readFileSync(join(state, "ip"), "utf8").trim();
|
|
413
419
|
const tpl = readFileSync(join(state, "template"), "utf8").trim();
|
|
@@ -422,8 +428,14 @@ export class StigRunner {
|
|
|
422
428
|
this.log(`oscap DISA V2R8 benchmark scan on ${i.cloneName} (skipped on templates that predate the dual-scan recipe)`);
|
|
423
429
|
await this.d.guest.runScript(this.d.readAsset("guest/scan-disa.sh"), { sudo: true, stdin: sudoStdin(), timeoutMs: 30 * 60_000 });
|
|
424
430
|
const evDir = join(state, "evidence");
|
|
431
|
+
clearStage(state, "scan");
|
|
432
|
+
clearStage(state, "keyhash");
|
|
433
|
+
rmSync(evDir, { recursive: true, force: true }); // never mix this scan with a predecessor's files (clone.sh scan clears it too)
|
|
425
434
|
await this.d.guest.fetchDir("/var/tmp/ct-scan", evDir);
|
|
426
435
|
await this.d.guest.runScript("rm -rf /var/tmp/ct-scan\n", { sudo: true, stdin: sudoStdin() });
|
|
436
|
+
// The same markers evidence.sh keeps: collect_stig_evidence skips a scan that is current for this guest.
|
|
437
|
+
const identity = cloneIdentity(tpl, readFileSync(join(state, "vmid"), "utf8").trim(), rec.metadata.uid);
|
|
438
|
+
markStage(state, "scan", identity);
|
|
427
439
|
const counts = countsFromKv(parseKv(readFileSync(join(evDir, "clone-summary.txt"), "utf8")));
|
|
428
440
|
let disa;
|
|
429
441
|
if (!existsSync(join(evDir, "clone-disa-absent")) && existsSync(join(evDir, "clone-disa-summary.txt"))) {
|
|
@@ -443,8 +455,10 @@ export class StigRunner {
|
|
|
443
455
|
if (/^[a-f0-9]{64}$/.test(hex))
|
|
444
456
|
keyDigest = hex;
|
|
445
457
|
}
|
|
446
|
-
if (keyDigest)
|
|
458
|
+
if (keyDigest) {
|
|
447
459
|
writeFileSync(join(state, "keyhash.txt"), `${keyDigest}\n`, { mode: 0o600 });
|
|
460
|
+
markStage(state, "keyhash", identity);
|
|
461
|
+
}
|
|
448
462
|
const verifyKv = existsSync(join(state, "verify.txt")) ? parseKv(readFileSync(join(state, "verify.txt"), "utf8")) : {};
|
|
449
463
|
const rhel = /(\d+\.\d+) \(/.exec(verifyKv.PRETTY_NAME ?? "")?.[1] ?? "8.10";
|
|
450
464
|
const summary = buildReceiptSummary({
|
|
@@ -456,7 +470,7 @@ export class StigRunner {
|
|
|
456
470
|
await this.annotateSummary(claim, summary);
|
|
457
471
|
await this.step(claim, "complete");
|
|
458
472
|
const ok = gate.failZero && !(i.requireDisa && disaMissing);
|
|
459
|
-
return { ok, claim, counts, gate, disa: disa ?? null, disaScan: disa ? "ran" : "unavailable (template predates the DISA dual-scan recipe)", keyDigest, summary, evidenceDir: evDir };
|
|
473
|
+
return { ok, node: node.name, claim, counts, gate, disa: disa ?? null, disaScan: disa ? "ran" : "unavailable (template predates the DISA dual-scan recipe)", keyDigest, summary, evidenceDir: evDir };
|
|
460
474
|
}
|
|
461
475
|
async getReceipt(i) {
|
|
462
476
|
const rec = await this.d.claims.get(i.claim);
|
|
@@ -472,27 +486,29 @@ export class StigRunner {
|
|
|
472
486
|
summary = null;
|
|
473
487
|
}
|
|
474
488
|
}
|
|
475
|
-
return { claim: i.claim, descriptor: stigDescriptor(rec), summary, statusPreview: stigStatusPreview(rec) };
|
|
489
|
+
return { claim: i.claim, node: this.nodeOfClaim(rec, i.node).name, descriptor: stigDescriptor(rec), summary, statusPreview: stigStatusPreview(rec) };
|
|
476
490
|
}
|
|
477
491
|
/* ---------------------------------------------------------- release */
|
|
478
492
|
async releaseTemplate(i) {
|
|
479
493
|
const rec = await this.d.claims.get(i.claim);
|
|
480
494
|
if (!rec)
|
|
481
495
|
throw new Error(`claim ${i.claim} not found`);
|
|
496
|
+
const node = this.nodeOfClaim(rec, i.node);
|
|
482
497
|
const vmid = rec.spec?.vmid;
|
|
483
|
-
const guest = (await this.listGuests()).find((g) => g.vmid === vmid);
|
|
498
|
+
const guest = (await this.listGuests(node)).find((g) => g.vmid === vmid);
|
|
484
499
|
if (!guest)
|
|
485
|
-
throw new Error(`no Proxmox guest ${vmid} for claim ${i.claim}`);
|
|
500
|
+
throw new Error(`no Proxmox guest ${vmid} on ${node.name} for claim ${i.claim}`);
|
|
486
501
|
assertTemplateReleaseAllowed({ guest: guest, claim: rec, destroyConfirmed: i.confirmDestroy });
|
|
487
|
-
this.log(`destroying template ${guest.name} (${guest.vmid}) before releasing claim ${i.claim}`);
|
|
488
|
-
|
|
489
|
-
|
|
502
|
+
this.log(`destroying template ${guest.name} (${guest.vmid}) on ${node.name} before releasing claim ${i.claim}`);
|
|
503
|
+
const provider = this.providerFor(node);
|
|
504
|
+
if (provider)
|
|
505
|
+
await provider.destroyTemplate({ operationId: `stig-destroy-${i.claim}-${this.d.clock.now()}`, hostAssetId: node.hostAssetId, name: guest.name, vmid: guest.vmid });
|
|
490
506
|
else
|
|
491
|
-
await this.pveOk(`qm destroy ${guest.vmid} --purge 1 --destroy-unreferenced-disks 1
|
|
492
|
-
if ((await this.listGuests()).some((g) => g.vmid === guest.vmid))
|
|
507
|
+
await this.pveOk(`qm destroy ${guest.vmid} --purge 1 --destroy-unreferenced-disks 1`, undefined, node);
|
|
508
|
+
if ((await this.listGuests(node)).some((g) => g.vmid === guest.vmid))
|
|
493
509
|
throw new Error(`template ${guest.vmid} still exists after destroy; claim retained`);
|
|
494
510
|
await this.d.claims.delete(i.claim, rec.metadata.uid);
|
|
495
|
-
return { ok: true, destroyed: { vmid: guest.vmid, name: guest.name }, claimDeleted: i.claim };
|
|
511
|
+
return { ok: true, node: node.name, destroyed: { vmid: guest.vmid, name: guest.name }, claimDeleted: i.claim };
|
|
496
512
|
}
|
|
497
513
|
}
|
|
498
514
|
function guestIp(stdout) {
|
|
@@ -509,4 +525,21 @@ function guestIp(stdout) {
|
|
|
509
525
|
catch { /* not ready yet */ }
|
|
510
526
|
return "";
|
|
511
527
|
}
|
|
528
|
+
/** The clone verification of clone.sh `verify` (FIPS, SELinux, LUKS2 + TPM2, bootstrap key, recovery test); throws unless the gate holds. */
|
|
529
|
+
export async function verifyGuestFacts(d, escrowKey, sudoStdin) {
|
|
530
|
+
const user = await d.guest.runScript(d.readAsset("guest/verify-user.sh"));
|
|
531
|
+
const root = await d.guest.runScript(d.readAsset("guest/verify-root.sh"), { sudo: true, stdin: sudoStdin() });
|
|
532
|
+
let text = `${user.stdout.trim()}\n${root.stdout.trim()}`;
|
|
533
|
+
const rec = await d.secrets.get(escrowKey);
|
|
534
|
+
if (rec) {
|
|
535
|
+
const t = await d.guest.runScript(d.readAsset("guest/test-recovery.sh"), { sudo: true, stdin: sudoStdin(rec) });
|
|
536
|
+
text += `\nrecovery_passphrase_test=${t.stdout.trim().split("\n").pop() ?? ""}`;
|
|
537
|
+
}
|
|
538
|
+
text += "\n";
|
|
539
|
+
const kv = parseKv(text);
|
|
540
|
+
const okGate = kv.fips_enabled === "1" && kv.selinux === "Enforcing" && kv.tpm_bound === "1" && kv.bootstrap_key_file === "absent";
|
|
541
|
+
if (!okGate)
|
|
542
|
+
throw new Error("VERIFY FAILED: fips/selinux/tpm/bootstrap-key checks did not all pass");
|
|
543
|
+
return { kv, text };
|
|
544
|
+
}
|
|
512
545
|
//# sourceMappingURL=runner.js.map
|