mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:36:13 +08:00
351 lines
9.9 KiB
JavaScript
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');
|
|
});
|