Files
qinglong/packages/ql3-local-owner-cli/test/securityAuditQueryCommand.test.cjs
T

351 lines
9.9 KiB
JavaScript

const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
createLocalSecurityAuditQueryCommandRunner,
} = require('@qinglong/local-owner-cli/security-audit-query-command');
const {
LocalSqliteAuthenticatedManagementFenceError,
} = require('@qinglong/local-sqlite/authenticated-management');
const PRINCIPAL = Object.freeze({
subject: Object.freeze({ type: 'user', id: 'owner-user' }),
authenticationId: 'local_security_audit:test',
authenticatedAtMs: 1_000,
expiresAtMs: 61_000,
assurance: 'local_console',
});
function fixture(t) {
const deploymentRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-audit-command-'),
);
fs.chmodSync(deploymentRoot, 0o700);
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
const commandPath = path.join(deploymentRoot, 'command.json');
const value = {
schemaVersion: 1,
operation: 'security.audit.list',
options: {
deploymentRoot,
databasePath: path.join(deploymentRoot, 'qinglong3.sqlite'),
profile: 'edge',
ownerPepperKeyringDirectory: path.join(deploymentRoot, 'owner-keys'),
credentialFilePath: path.join(deploymentRoot, 'credential.json'),
},
request: {
authorityProjectId: 'default',
query: { limit: 1, filter: {} },
requestId: 'audit-query-cli',
auditEventId: '94000000-0000-4000-8000-000000000001',
},
};
fs.writeFileSync(commandPath, `${JSON.stringify(value)}\n`, {
mode: 0o600,
});
return { commandPath, value };
}
function compactionFixture(t, requestOverrides = {}) {
const value = fixture(t);
value.value.operation = 'security.audit.compact';
value.value.request = {
authorityProjectId: 'default',
retentionMs: 2_592_000_000,
eligibleBeforeMs: 1_000,
limit: 64,
mutationId: '94100000-0000-4000-8000-000000000001',
requestId: 'audit-compact-cli',
failureAuditEventId: '94100000-0000-4000-8000-000000000002',
...requestOverrides,
};
fs.writeFileSync(value.commandPath, `${JSON.stringify(value.value)}\n`, {
mode: 0o600,
});
return value;
}
function authenticated() {
return {
principal: PRINCIPAL,
databaseFence: {
credentialId: 'owner',
credentialVersion: 1,
pepperKeyId: 'owner-v1',
materialDigest: 'a'.repeat(64),
subjectType: 'user',
subjectId: 'owner-user',
secretDigest: 'b'.repeat(64),
notBeforeAtMs: 0,
expiresAtMs: 60_000,
},
async confirm() {},
};
}
test('returns bounded audit rows without authentication identifiers', async (t) => {
const value = fixture(t);
let closed = false;
const runner = createLocalSecurityAuditQueryCommandRunner({
async openDatabase() {
return {
projectPolicy: {},
securityAuditQuery: {},
securityAuditRetention: {},
securityAudit: {
async record() {
throw new Error('not used');
},
},
activateUserCredentialFence() {},
async close() {
closed = true;
},
};
},
async authenticate() {
return authenticated();
},
createService() {
return {
async list() {
return {
records: [
{
eventId: '95000000-0000-4000-8000-000000000001',
requestId: 'denied-operation',
operationId: 'tool.invoke',
projectId: 'project-alpha',
subject: { type: 'agent', id: 'planner' },
authenticationId: 'must-never-be-returned',
outcome: 'denied',
reasons: ['permission_missing'],
fence: { projectVersion: 2, bindingVersion: 3 },
occurredAtMs: 1_500,
},
],
nextCursor: null,
audit: {},
};
},
};
},
createRetentionService() {
throw new Error('must not run');
},
now: () => 2_000,
});
const result = await runner.run(value.commandPath);
assert.equal(closed, true);
assert.equal(result.records.length, 1);
assert.equal(Object.hasOwn(result.records[0], 'authenticationId'), false);
assert.equal(
JSON.stringify(result).includes('must-never-be-returned'),
false,
);
});
test('audits a final credential fence rejection and closes the database', async (t) => {
const value = fixture(t);
const audits = [];
let closed = false;
const runner = createLocalSecurityAuditQueryCommandRunner({
async openDatabase() {
return {
projectPolicy: {},
securityAuditQuery: {},
securityAuditRetention: {},
securityAudit: {
async record(audit) {
audits.push(audit);
},
},
activateUserCredentialFence() {
throw new LocalSqliteAuthenticatedManagementFenceError();
},
async close() {
closed = true;
},
};
},
async authenticate() {
return authenticated();
},
createService() {
throw new Error('must not run');
},
createRetentionService() {
throw new Error('must not run');
},
now: () => 2_000,
});
await assert.rejects(
runner.run(value.commandPath),
LocalSqliteAuthenticatedManagementFenceError,
);
assert.equal(closed, true);
assert.equal(audits.length, 1);
assert.deepEqual(
{
eventId: audits[0].eventId,
outcome: audits[0].outcome,
reasons: audits[0].reasons,
},
{
eventId: value.value.request.auditEventId,
outcome: 'denied',
reasons: ['credential_or_policy_fence_rejected'],
},
);
});
test('returns a redacted bounded compaction receipt through the existing audit CLI', async (t) => {
const value = compactionFixture(t);
const runner = createLocalSecurityAuditQueryCommandRunner({
async openDatabase() {
return {
projectPolicy: {},
securityAuditQuery: {},
securityAuditRetention: {},
securityAudit: {
async record() {
throw new Error('not used');
},
},
activateUserCredentialFence() {},
async close() {},
};
},
async authenticate() {
return authenticated();
},
createService() {
throw new Error('must not run');
},
createRetentionService() {
return {
async compact(request) {
assert.equal(
request.failureAuditEventId,
value.value.request.failureAuditEventId,
);
return {
status: 'inserted',
record: {
mutationId: request.mutationId,
requestId: request.requestId,
authorityProjectId: request.authorityProjectId,
retentionMs: request.retentionMs,
eligibleBeforeMs: request.eligibleBeforeMs,
batchLimit: request.limit,
deletedCount: 3,
deletedPayloadBytes: 400,
first: {
occurredAtMs: 10,
eventId: '94200000-0000-4000-8000-000000000001',
},
last: {
occurredAtMs: 20,
eventId: '94200000-0000-4000-8000-000000000003',
},
recordsDigest: 'a'.repeat(64),
createdAtMs: 2_592_002_000,
},
audit: {
authenticationId: 'must-never-be-returned',
},
};
},
};
},
now: () => 2_592_002_000,
});
const result = await runner.run(value.commandPath);
assert.deepEqual(
{
operation: result.operation,
status: result.status,
deletedCount: result.deletedCount,
batchLimit: result.batchLimit,
},
{
operation: 'security.audit.compact',
status: 'inserted',
deletedCount: 3,
batchLimit: 64,
},
);
assert.equal(JSON.stringify(result).includes('authenticationId'), false);
assert.equal(JSON.stringify(result).includes('authorityProjectId'), false);
assert.equal(JSON.stringify(result).includes('requestId'), false);
});
test('rejects an Edge compaction batch above 64 before opening SQLite', async (t) => {
const value = compactionFixture(t, { limit: 65 });
let opened = false;
const runner = createLocalSecurityAuditQueryCommandRunner({
async openDatabase() {
opened = true;
throw new Error('must not run');
},
async authenticate() {
throw new Error('must not run');
},
createService() {
throw new Error('must not run');
},
createRetentionService() {
throw new Error('must not run');
},
now: () => 2_592_002_000,
});
await assert.rejects(
runner.run(value.commandPath),
/compaction identity, retention fence, or limit is invalid/,
);
assert.equal(opened, false);
});
test('uses the separate failure event for a compaction credential fence rejection', async (t) => {
const value = compactionFixture(t);
const audits = [];
const runner = createLocalSecurityAuditQueryCommandRunner({
async openDatabase() {
return {
projectPolicy: {},
securityAuditQuery: {},
securityAuditRetention: {},
securityAudit: {
async record(record) {
audits.push(record);
},
},
activateUserCredentialFence() {
throw new LocalSqliteAuthenticatedManagementFenceError();
},
async close() {},
};
},
async authenticate() {
return authenticated();
},
createService() {
throw new Error('must not run');
},
createRetentionService() {
throw new Error('must not run');
},
now: () => 2_592_002_000,
});
await assert.rejects(
runner.run(value.commandPath),
LocalSqliteAuthenticatedManagementFenceError,
);
assert.equal(audits.length, 1);
assert.equal(audits[0].eventId, value.value.request.failureAuditEventId);
assert.equal(audits[0].outcome, 'denied');
});