This commit is contained in:
@@ -3,267 +3,301 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
||||
|
||||
let fixture: Fixture;
|
||||
const servers = ['t5_zombie_theater_1', 't6_zombie_town_1'];
|
||||
const filename = 'electric_cherry_bank.cfg';
|
||||
const secret = Buffer.from('set tws_bank_token "ec_bank_private_regression_secret"\n');
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
fixture = await newFixture(servers);
|
||||
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
|
||||
await seedServerRegistry();
|
||||
const { destOfKind } = await import('../../src/lib/server/config.js');
|
||||
for (const server of servers) {
|
||||
await mkdir(destOfKind(server, 'config'), { recursive: true });
|
||||
await writeFile(join(destOfKind(server, 'config'), filename), secret);
|
||||
}
|
||||
});
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await fixture.cleanup();
|
||||
});
|
||||
describe.each(['electric_cherry_bank.cfg', 'electric_cherry_moderation.cfg'])(
|
||||
'private config %s',
|
||||
(filename) => {
|
||||
let fixture: Fixture;
|
||||
const servers = ['t5_zombie_theater_1', 't6_zombie_town_1'];
|
||||
const secret = Buffer.from('set tws_bank_token "ec_bank_private_regression_secret"\n');
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
fixture = await newFixture(servers);
|
||||
const { seedServerRegistry } = await import('../../src/lib/server/fleet/servers.js');
|
||||
await seedServerRegistry();
|
||||
const { destOfKind } = await import('../../src/lib/server/config.js');
|
||||
for (const server of servers) {
|
||||
await mkdir(destOfKind(server, 'config'), { recursive: true });
|
||||
await writeFile(join(destOfKind(server, 'config'), filename), secret);
|
||||
}
|
||||
});
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await fixture.cleanup();
|
||||
});
|
||||
|
||||
async function evidenceIsPrivate() {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
expect(db().prepare('SELECT 1 FROM file_blobs WHERE content=?').all(secret)).toEqual([]);
|
||||
expect(db().prepare('SELECT 1 FROM deployment_files WHERE rel_path=?').all(filename)).toEqual(
|
||||
[]
|
||||
);
|
||||
}
|
||||
async function evidenceIsPrivate() {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
expect(db().prepare('SELECT 1 FROM file_blobs WHERE content=?').all(secret)).toEqual(
|
||||
[]
|
||||
);
|
||||
expect(
|
||||
db().prepare('SELECT 1 FROM deployment_files WHERE rel_path=?').all(filename)
|
||||
).toEqual([]);
|
||||
}
|
||||
|
||||
describe.each(servers)('private bank config on %s', (server) => {
|
||||
it('does not read, preview, stage, or capture the generated file while repairing real drift', async () => {
|
||||
const drift = await import('../../src/lib/server/fleet/drift.js');
|
||||
const { destOfKind } = await import('../../src/lib/server/config.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { requestRestart } = await import('../../src/lib/server/provisioning/requests.js');
|
||||
const { applyStagedFilesForOperation } =
|
||||
await import('../../src/lib/server/fleet/staged-files.js');
|
||||
const backend = await import('../../src/lib/server/backends/files.js');
|
||||
const reads = vi.spyOn(backend, 'readFile');
|
||||
expect(await drift.scanServerDrift(server)).toEqual([]);
|
||||
expect((await drift.previewDrift([server])).files).toEqual([]);
|
||||
expect((await drift.prepareDriftCampaign(server)).files).toEqual([]);
|
||||
expect((await drift.runDriftDeployment({ servers: [server] })).deploymentId).toBeNull();
|
||||
const dest = destOfKind(server, 'config');
|
||||
await writeFile(join(dest, 'stray.cfg'), 'unwanted');
|
||||
// The exemption is for the generated path, not every matching basename.
|
||||
await mkdir(join(dest, 'nested'));
|
||||
await writeFile(join(dest, 'nested', filename), 'unwanted nested file');
|
||||
for (const name of ['modified.gsc', 'missing.gsc']) {
|
||||
const artifact = createScript({
|
||||
name,
|
||||
releaseVersion: '1.0.0',
|
||||
targetRelPath: name,
|
||||
kind: 'script',
|
||||
scopeType: 'game',
|
||||
scopeValue: server.slice(0, 2),
|
||||
content: Buffer.from('promoted')
|
||||
describe.each(servers)('private bank config on %s', (server) => {
|
||||
it('does not read, preview, stage, or capture the generated file while repairing real drift', async () => {
|
||||
const drift = await import('../../src/lib/server/fleet/drift.js');
|
||||
const { destOfKind } = await import('../../src/lib/server/config.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { requestRestart } =
|
||||
await import('../../src/lib/server/provisioning/requests.js');
|
||||
const { applyStagedFilesForOperation } =
|
||||
await import('../../src/lib/server/fleet/staged-files.js');
|
||||
const backend = await import('../../src/lib/server/backends/files.js');
|
||||
const reads = vi.spyOn(backend, 'readFile');
|
||||
expect(await drift.scanServerDrift(server)).toEqual([]);
|
||||
expect((await drift.previewDrift([server])).files).toEqual([]);
|
||||
expect((await drift.prepareDriftCampaign(server)).files).toEqual([]);
|
||||
expect(
|
||||
(await drift.runDriftDeployment({ servers: [server] })).deploymentId
|
||||
).toBeNull();
|
||||
const dest = destOfKind(server, 'config');
|
||||
await writeFile(join(dest, 'stray.cfg'), 'unwanted');
|
||||
// The exemption is for the generated path, not every matching basename.
|
||||
await mkdir(join(dest, 'nested'));
|
||||
await writeFile(join(dest, 'nested', filename), 'unwanted nested file');
|
||||
for (const name of ['modified.gsc', 'missing.gsc']) {
|
||||
const artifact = createScript({
|
||||
name,
|
||||
releaseVersion: '1.0.0',
|
||||
targetRelPath: name,
|
||||
kind: 'script',
|
||||
scopeType: 'game',
|
||||
scopeValue: server.slice(0, 2),
|
||||
content: Buffer.from('promoted')
|
||||
});
|
||||
db()
|
||||
.prepare('UPDATE scripts SET current_version_id=? WHERE id=?')
|
||||
.run(artifact.version.id, artifact.script.id);
|
||||
}
|
||||
await writeFile(join(destOfKind(server, 'script'), 'modified.gsc'), 'modified');
|
||||
const plan = await drift.previewDrift([server]);
|
||||
expect(plan.files.map((file) => file.relPath).sort()).toEqual([
|
||||
'missing.gsc',
|
||||
'modified.gsc',
|
||||
`nested/${filename}`,
|
||||
'stray.cfg'
|
||||
]);
|
||||
const result = await drift.runDriftDeployment({
|
||||
servers: [server],
|
||||
expectedFingerprint: plan.fingerprint
|
||||
});
|
||||
const operation = requestRestart(server, {
|
||||
kind: 'campaign',
|
||||
id: result.deploymentId!
|
||||
});
|
||||
applyStagedFilesForOperation(operation.id, server);
|
||||
expect(await drift.scanServerDrift(server)).toEqual([]);
|
||||
expect(await readFile(join(dest, filename))).toEqual(secret);
|
||||
expect(reads.mock.calls.some(([path]) => path === join(dest, filename))).toBe(
|
||||
false
|
||||
);
|
||||
await evidenceIsPrivate();
|
||||
});
|
||||
|
||||
it('filters old prepared bank evidence while retaining legitimate campaign changes', async () => {
|
||||
const { prepareDriftCampaign, createDriftRebuildCampaign } =
|
||||
await import('../../src/lib/server/fleet/drift.js');
|
||||
const { beginDeployment } =
|
||||
await import('../../src/lib/server/provisioning/history.js');
|
||||
const { destOfKind } = await import('../../src/lib/server/config.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
await writeFile(join(destOfKind(server, 'config'), 'stray.cfg'), 'unwanted');
|
||||
const prepared = await prepareDriftCampaign(server);
|
||||
const privateFile = {
|
||||
...prepared.files[0],
|
||||
before: secret,
|
||||
file: { ...prepared.files[0].file, relPath: filename }
|
||||
};
|
||||
prepared.files.push(privateFile);
|
||||
prepared.plan.files.push(privateFile.file);
|
||||
const operation = beginDeployment(server, 'rebuild', 'watchdog');
|
||||
const id = createDriftRebuildCampaign(server, operation, 'old-incident', prepared);
|
||||
expect(
|
||||
db()
|
||||
.prepare('SELECT rel_path FROM deployment_files WHERE deployment_id=?')
|
||||
.all(id)
|
||||
).toEqual([{ rel_path: 'stray.cfg' }]);
|
||||
expect(
|
||||
createDriftRebuildCampaign(server, operation, 'bank-only', {
|
||||
...prepared,
|
||||
files: [privateFile]
|
||||
})
|
||||
).toBeNull();
|
||||
await evidenceIsPrivate();
|
||||
});
|
||||
|
||||
it('rejects new and persisted private deletions before activation or evidence capture', async () => {
|
||||
const staged = await import('../../src/lib/server/fleet/staged-files.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { requestRestart } =
|
||||
await import('../../src/lib/server/provisioning/requests.js');
|
||||
const { applyActivationFiles } =
|
||||
await import('../../src/lib/server/provisioning/activation-files.js');
|
||||
const { captureRecipePlan } =
|
||||
await import('../../src/lib/server/fleet/launch-recipes.js');
|
||||
const { destOfKind } = await import('../../src/lib/server/config.js');
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO deployments(id,action,started_at,triggered_by) VALUES('old','drift',datetime('now'),'ui')"
|
||||
)
|
||||
.run();
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO deployment_servers(deployment_id,server,files_changed,restart_state) VALUES('old',?,1,'pending')"
|
||||
)
|
||||
.run(server);
|
||||
const change = {
|
||||
sourceKind: 'campaign' as const,
|
||||
sourceId: 'old',
|
||||
server,
|
||||
kind: 'config' as const,
|
||||
relPath: filename,
|
||||
content: null,
|
||||
inventorySource: 'drift:old'
|
||||
};
|
||||
expect(() => staged.queueFileChange(change)).toThrow('Private bank configuration');
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO staged_file_changes(source_kind,source_id,server,kind,rel_path,inventory_source) VALUES('campaign','old',?,'config',?,'drift:old')"
|
||||
)
|
||||
.run(server, filename);
|
||||
const operation = requestRestart(server, { kind: 'campaign', id: 'old' });
|
||||
expect(() => staged.applyStagedFilesForOperation(operation.id, server)).toThrow(
|
||||
'Private bank configuration'
|
||||
);
|
||||
const recipe = captureRecipePlan(
|
||||
server.startsWith('t6') ? 't6' : 't5',
|
||||
server.startsWith('t6') ? 'town' : 'theater'
|
||||
);
|
||||
expect(() => applyActivationFiles(recipe, operation.id, true)).toThrow(
|
||||
'Private bank configuration'
|
||||
);
|
||||
expect(await readFile(join(destOfKind(server, 'config'), filename))).toEqual(
|
||||
secret
|
||||
);
|
||||
await evidenceIsPrivate();
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects the reserved upload name and redacts legacy receipts before packing their bytes', async () => {
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { packFileJson } = await import('../../src/lib/server/db/file-blobs.js');
|
||||
expect(() =>
|
||||
createScript({
|
||||
name: 'private',
|
||||
kind: 'config',
|
||||
targetRelPath: filename,
|
||||
scopeType: 'game',
|
||||
scopeValue: 't5',
|
||||
content: secret
|
||||
})
|
||||
).toThrow('reserved');
|
||||
const packed = packFileJson({
|
||||
before: [{ path: `configs/${filename}`, contentBase64: secret.toString('base64') }]
|
||||
});
|
||||
expect(packed.hashes.size).toBe(0);
|
||||
expect(JSON.stringify(packed.value)).not.toContain(secret.toString('base64'));
|
||||
await evidenceIsPrivate();
|
||||
});
|
||||
|
||||
it('blocks legacy private campaign diffs and blob downloads', async () => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { putFileBlob, getFileBlob } =
|
||||
await import('../../src/lib/server/db/file-blobs.js');
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO deployments(id,action,started_at,triggered_by) VALUES('legacy','drift',datetime('now'),'ui')"
|
||||
)
|
||||
.run();
|
||||
const hash = putFileBlob(secret)!;
|
||||
const file = db()
|
||||
.prepare(
|
||||
"INSERT INTO deployment_files(deployment_id,server,verb,rel_path,content_before_hash) VALUES('legacy',?,'removed',?,?)"
|
||||
)
|
||||
.run(servers[0], filename, hash);
|
||||
for (const route of [
|
||||
await import('../../src/routes/api/campaigns/[id]/file/[fileId]/+server.js'),
|
||||
await import('../../src/routes/api/deployments/[id]/file/[fileId]/+server.js')
|
||||
]) {
|
||||
await expect(
|
||||
route.GET({ params: { id: 'legacy', fileId: String(file.lastInsertRowid) } })
|
||||
).rejects.toMatchObject({ status: 404 });
|
||||
}
|
||||
expect(() => getFileBlob(hash)).toThrow('Private file content');
|
||||
});
|
||||
|
||||
it('blocks old host commands and redacts late private read receipts from older agents', async () => {
|
||||
const hosts = await import('../../src/lib/server/hosts/index.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const host = hosts.createHost({ name: 'Legacy private command host' });
|
||||
const credential = hosts.enrollHost(
|
||||
hosts.createEnrollment(host.id).token,
|
||||
{}
|
||||
).credential;
|
||||
const payload = {
|
||||
files: [{ path: `configs/${filename}`, contentBase64: secret.toString('base64') }]
|
||||
};
|
||||
expect(() => hosts.queueHostOperation(host.id, 'files.stage', payload)).toThrow(
|
||||
'Private bank'
|
||||
);
|
||||
const stage = hosts.queueHostOperation(host.id, 'files.stage', { files: [] });
|
||||
db()
|
||||
.prepare('UPDATE ec_host_operations SET payload=? WHERE id=?')
|
||||
.run(JSON.stringify(payload), stage.id);
|
||||
expect(() => hosts.getHostOperationPayload(host.id, stage.id)).toThrow('Private bank');
|
||||
const healthyRead = hosts.queueHostOperation(host.id, 'files.read', {
|
||||
path: 'configs/stray.cfg'
|
||||
});
|
||||
expect(
|
||||
hosts.pendingHostOperationRows(host.id).map((operation) => operation.id)
|
||||
).toContain(healthyRead.id);
|
||||
expect(
|
||||
hosts.pendingHostOperationRows(host.id).map((operation) => operation.id)
|
||||
).not.toContain(stage.id);
|
||||
const read = hosts.queueHostOperation(host.id, 'files.read', {
|
||||
path: 'configs/stray.cfg'
|
||||
});
|
||||
db()
|
||||
.prepare('UPDATE scripts SET current_version_id=? WHERE id=?')
|
||||
.run(artifact.version.id, artifact.script.id);
|
||||
}
|
||||
await writeFile(join(destOfKind(server, 'script'), 'modified.gsc'), 'modified');
|
||||
const plan = await drift.previewDrift([server]);
|
||||
expect(plan.files.map((file) => file.relPath).sort()).toEqual([
|
||||
'missing.gsc',
|
||||
'modified.gsc',
|
||||
`nested/${filename}`,
|
||||
'stray.cfg'
|
||||
]);
|
||||
const result = await drift.runDriftDeployment({
|
||||
servers: [server],
|
||||
expectedFingerprint: plan.fingerprint
|
||||
.prepare('UPDATE ec_host_operations SET payload=?,target_path=? WHERE id=?')
|
||||
.run(
|
||||
JSON.stringify({ path: `configs/${filename}` }),
|
||||
`configs/${filename}`,
|
||||
read.id
|
||||
);
|
||||
hosts.reportHostOperationStatus(host.id, read.id, {
|
||||
state: 'completed',
|
||||
result: { contentBase64: secret.toString('base64') }
|
||||
});
|
||||
expect(hosts.getHostOperation(read.id)?.result).toEqual({ redacted: true });
|
||||
await evidenceIsPrivate();
|
||||
|
||||
// Even an old operation reference cannot authorize downloading captured credentials.
|
||||
const { putFileBlob, referenceBlobs } =
|
||||
await import('../../src/lib/server/db/file-blobs.js');
|
||||
const hash = putFileBlob(secret)!;
|
||||
referenceBlobs('operation', stage.id, [hash]);
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO deployments(id,action,started_at,triggered_by) VALUES('legacy','drift',datetime('now'),'ui')"
|
||||
)
|
||||
.run();
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO deployment_files(deployment_id,server,verb,rel_path,content_before_hash) VALUES('legacy',?,'removed',?,?)"
|
||||
)
|
||||
.run(servers[0], filename, hash);
|
||||
const { GET } = await import('../../src/routes/api/agent/blobs/[sha256]/+server.js');
|
||||
expect(() =>
|
||||
GET({
|
||||
request: new Request('https://test', {
|
||||
headers: { Authorization: `Bearer ${credential}` }
|
||||
}),
|
||||
params: { sha256: hash }
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
const operation = requestRestart(server, { kind: 'campaign', id: result.deploymentId! });
|
||||
applyStagedFilesForOperation(operation.id, server);
|
||||
expect(await drift.scanServerDrift(server)).toEqual([]);
|
||||
expect(await readFile(join(dest, filename))).toEqual(secret);
|
||||
expect(reads.mock.calls.some(([path]) => path === join(dest, filename))).toBe(false);
|
||||
await evidenceIsPrivate();
|
||||
});
|
||||
|
||||
it('filters old prepared bank evidence while retaining legitimate campaign changes', async () => {
|
||||
const { prepareDriftCampaign, createDriftRebuildCampaign } =
|
||||
await import('../../src/lib/server/fleet/drift.js');
|
||||
const { beginDeployment } = await import('../../src/lib/server/provisioning/history.js');
|
||||
const { destOfKind } = await import('../../src/lib/server/config.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
await writeFile(join(destOfKind(server, 'config'), 'stray.cfg'), 'unwanted');
|
||||
const prepared = await prepareDriftCampaign(server);
|
||||
const privateFile = {
|
||||
...prepared.files[0],
|
||||
before: secret,
|
||||
file: { ...prepared.files[0].file, relPath: filename }
|
||||
};
|
||||
prepared.files.push(privateFile);
|
||||
prepared.plan.files.push(privateFile.file);
|
||||
const operation = beginDeployment(server, 'rebuild', 'watchdog');
|
||||
const id = createDriftRebuildCampaign(server, operation, 'old-incident', prepared);
|
||||
expect(
|
||||
db().prepare('SELECT rel_path FROM deployment_files WHERE deployment_id=?').all(id)
|
||||
).toEqual([{ rel_path: 'stray.cfg' }]);
|
||||
expect(
|
||||
createDriftRebuildCampaign(server, operation, 'bank-only', {
|
||||
...prepared,
|
||||
files: [privateFile]
|
||||
})
|
||||
).toBeNull();
|
||||
await evidenceIsPrivate();
|
||||
});
|
||||
|
||||
it('rejects new and persisted private deletions before activation or evidence capture', async () => {
|
||||
const staged = await import('../../src/lib/server/fleet/staged-files.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { requestRestart } = await import('../../src/lib/server/provisioning/requests.js');
|
||||
const { applyActivationFiles } =
|
||||
await import('../../src/lib/server/provisioning/activation-files.js');
|
||||
const { captureRecipePlan } = await import('../../src/lib/server/fleet/launch-recipes.js');
|
||||
const { destOfKind } = await import('../../src/lib/server/config.js');
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO deployments(id,action,started_at,triggered_by) VALUES('old','drift',datetime('now'),'ui')"
|
||||
)
|
||||
.run();
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO deployment_servers(deployment_id,server,files_changed,restart_state) VALUES('old',?,1,'pending')"
|
||||
)
|
||||
.run(server);
|
||||
const change = {
|
||||
sourceKind: 'campaign' as const,
|
||||
sourceId: 'old',
|
||||
server,
|
||||
kind: 'config' as const,
|
||||
relPath: filename,
|
||||
content: null,
|
||||
inventorySource: 'drift:old'
|
||||
};
|
||||
expect(() => staged.queueFileChange(change)).toThrow('Private bank configuration');
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO staged_file_changes(source_kind,source_id,server,kind,rel_path,inventory_source) VALUES('campaign','old',?,'config',?,'drift:old')"
|
||||
)
|
||||
.run(server, filename);
|
||||
const operation = requestRestart(server, { kind: 'campaign', id: 'old' });
|
||||
expect(() => staged.applyStagedFilesForOperation(operation.id, server)).toThrow(
|
||||
'Private bank configuration'
|
||||
);
|
||||
const recipe = captureRecipePlan(
|
||||
server.startsWith('t6') ? 't6' : 't5',
|
||||
server.startsWith('t6') ? 'town' : 'theater'
|
||||
);
|
||||
expect(() => applyActivationFiles(recipe, operation.id, true)).toThrow(
|
||||
'Private bank configuration'
|
||||
);
|
||||
expect(await readFile(join(destOfKind(server, 'config'), filename))).toEqual(secret);
|
||||
await evidenceIsPrivate();
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects the reserved upload name and redacts legacy receipts before packing their bytes', async () => {
|
||||
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
||||
const { packFileJson } = await import('../../src/lib/server/db/file-blobs.js');
|
||||
expect(() =>
|
||||
createScript({
|
||||
name: 'private',
|
||||
kind: 'config',
|
||||
targetRelPath: filename,
|
||||
scopeType: 'game',
|
||||
scopeValue: 't5',
|
||||
content: secret
|
||||
})
|
||||
).toThrow('reserved');
|
||||
const packed = packFileJson({
|
||||
before: [{ path: `configs/${filename}`, contentBase64: secret.toString('base64') }]
|
||||
});
|
||||
expect(packed.hashes.size).toBe(0);
|
||||
expect(JSON.stringify(packed.value)).not.toContain(secret.toString('base64'));
|
||||
await evidenceIsPrivate();
|
||||
});
|
||||
|
||||
it('blocks legacy private campaign diffs and blob downloads', async () => {
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { putFileBlob, getFileBlob } = await import('../../src/lib/server/db/file-blobs.js');
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO deployments(id,action,started_at,triggered_by) VALUES('legacy','drift',datetime('now'),'ui')"
|
||||
)
|
||||
.run();
|
||||
const hash = putFileBlob(secret)!;
|
||||
const file = db()
|
||||
.prepare(
|
||||
"INSERT INTO deployment_files(deployment_id,server,verb,rel_path,content_before_hash) VALUES('legacy',?,'removed',?,?)"
|
||||
)
|
||||
.run(servers[0], filename, hash);
|
||||
for (const route of [
|
||||
await import('../../src/routes/api/campaigns/[id]/file/[fileId]/+server.js'),
|
||||
await import('../../src/routes/api/deployments/[id]/file/[fileId]/+server.js')
|
||||
]) {
|
||||
await expect(
|
||||
route.GET({ params: { id: 'legacy', fileId: String(file.lastInsertRowid) } })
|
||||
).rejects.toMatchObject({ status: 404 });
|
||||
}
|
||||
expect(() => getFileBlob(hash)).toThrow('Private file content');
|
||||
});
|
||||
|
||||
it('blocks old host commands and redacts late private read receipts from older agents', async () => {
|
||||
const hosts = await import('../../src/lib/server/hosts/index.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const host = hosts.createHost({ name: 'Legacy private command host' });
|
||||
const credential = hosts.enrollHost(hosts.createEnrollment(host.id).token, {}).credential;
|
||||
const payload = {
|
||||
files: [{ path: `configs/${filename}`, contentBase64: secret.toString('base64') }]
|
||||
};
|
||||
expect(() => hosts.queueHostOperation(host.id, 'files.stage', payload)).toThrow('Private bank');
|
||||
const stage = hosts.queueHostOperation(host.id, 'files.stage', { files: [] });
|
||||
db()
|
||||
.prepare('UPDATE ec_host_operations SET payload=? WHERE id=?')
|
||||
.run(JSON.stringify(payload), stage.id);
|
||||
expect(() => hosts.getHostOperationPayload(host.id, stage.id)).toThrow('Private bank');
|
||||
const healthyRead = hosts.queueHostOperation(host.id, 'files.read', {
|
||||
path: 'configs/stray.cfg'
|
||||
});
|
||||
expect(hosts.pendingHostOperationRows(host.id).map((operation) => operation.id)).toContain(
|
||||
healthyRead.id
|
||||
);
|
||||
expect(hosts.pendingHostOperationRows(host.id).map((operation) => operation.id)).not.toContain(
|
||||
stage.id
|
||||
);
|
||||
const read = hosts.queueHostOperation(host.id, 'files.read', { path: 'configs/stray.cfg' });
|
||||
db()
|
||||
.prepare('UPDATE ec_host_operations SET payload=?,target_path=? WHERE id=?')
|
||||
.run(JSON.stringify({ path: `configs/${filename}` }), `configs/${filename}`, read.id);
|
||||
hosts.reportHostOperationStatus(host.id, read.id, {
|
||||
state: 'completed',
|
||||
result: { contentBase64: secret.toString('base64') }
|
||||
});
|
||||
expect(hosts.getHostOperation(read.id)?.result).toEqual({ redacted: true });
|
||||
await evidenceIsPrivate();
|
||||
|
||||
// Even an old operation reference cannot authorize downloading captured credentials.
|
||||
const { putFileBlob, referenceBlobs } = await import('../../src/lib/server/db/file-blobs.js');
|
||||
const hash = putFileBlob(secret)!;
|
||||
referenceBlobs('operation', stage.id, [hash]);
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO deployments(id,action,started_at,triggered_by) VALUES('legacy','drift',datetime('now'),'ui')"
|
||||
)
|
||||
.run();
|
||||
db()
|
||||
.prepare(
|
||||
"INSERT INTO deployment_files(deployment_id,server,verb,rel_path,content_before_hash) VALUES('legacy',?,'removed',?,?)"
|
||||
)
|
||||
.run(servers[0], filename, hash);
|
||||
const { GET } = await import('../../src/routes/api/agent/blobs/[sha256]/+server.js');
|
||||
expect(() =>
|
||||
GET({
|
||||
request: new Request('https://test', {
|
||||
headers: { Authorization: `Bearer ${credential}` }
|
||||
}),
|
||||
params: { sha256: hash }
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user