585 lines
31 KiB
TypeScript
585 lines
31 KiB
TypeScript
import { Pool } from 'pg';
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
import type {
|
|
Assessment,
|
|
AssessmentProject,
|
|
CanonicalAssessmentModel,
|
|
FindingReview,
|
|
ImportFileRecord,
|
|
ScanRun,
|
|
SourceTableRecord
|
|
} from '../../src/domain/canonical-model';
|
|
import type { StoredGpoBackupInstance } from '../../src/domain/gpo-analysis-persistence';
|
|
import type { ImportStagingData } from '../../src/persistence/assessor-repository';
|
|
import type { ImportArtifact, ImportProcessingRun, IngestionJob } from '../../src/domain/ingestion-persistence';
|
|
import { compareScanRuns } from '../../src/comparison/comparison-engine';
|
|
import { RepositoryConflictError } from '../../src/persistence/repository-errors';
|
|
import { runDatabaseMigrations } from '../../server/postgres/migration-runner';
|
|
import { PostgresAssessorRepository } from '../../server/postgres/postgres-assessor-repository';
|
|
import { createRepositoryRuntime } from '../../server/repository-factory';
|
|
|
|
const connectionString = process.env.ASSESSOR_TEST_DATABASE_URL;
|
|
const describePostgres = connectionString ? describe : describe.skip;
|
|
const timestamp = '2026-07-17T08:00:00.000Z';
|
|
const confidence = { level: 'high' as const, score: 1, reasons: ['integration'] };
|
|
const provenance = { decisionSource: 'source-native' as const, confidence, mappingVersion: '1.0.0' };
|
|
const project: AssessmentProject = { id: 'pg-project', name: 'PostgreSQL Project', createdAt: timestamp, updatedAt: timestamp, attributes: {} };
|
|
const assessment: Assessment = { id: 'pg-assessment', projectId: project.id, name: 'PostgreSQL Assessment', createdAt: timestamp, updatedAt: timestamp };
|
|
|
|
function run(sequence: number): ScanRun {
|
|
return {
|
|
id: `pg-run-${sequence}`, assessmentId: assessment.id, sequence, label: `Run ${sequence}`,
|
|
tool: { id: 'pingcastle', name: 'PingCastle', version: '3.3' }, importedAt: timestamp
|
|
};
|
|
}
|
|
|
|
function importData(scanRun: ScanRun, severity: 'high' | 'critical' = 'high'): ImportStagingData {
|
|
const fileId = `pg-file-${scanRun.sequence}`;
|
|
const tableId = `pg-table-${scanRun.sequence}`;
|
|
const occurrenceId = `pg-occurrence-${scanRun.sequence}`;
|
|
const file: ImportFileRecord = {
|
|
id: fileId, scanRunId: scanRun.id, fileName: `${scanRun.id}.csv`, format: 'CSV', sizeBytes: 100,
|
|
importedAt: timestamp, detectedTool: scanRun.tool, detection: confidence,
|
|
processedWith: { parserId: 'csv', parserVersion: '1.0.0', profileId: 'pingcastle', profileVersion: '1.0.0', mappingVersion: '1.0.0' },
|
|
sourceArtifact: { kind: 'not-retained', reason: 'Integration test' }
|
|
};
|
|
const table: SourceTableRecord = {
|
|
id: tableId, importFileId: fileId, name: 'Findings', index: 0,
|
|
rowCount: 1, columnCount: 3, attributes: {}
|
|
};
|
|
return {
|
|
importFiles: [file], sourceTables: [table],
|
|
findingDefinitions: [{
|
|
id: 'pg-definition', canonicalKey: 'pingcastle:postgres-rule', title: 'PostgreSQL searchable finding',
|
|
description: 'Persistent database evidence', category: 'Privileged Accounts',
|
|
frameworkReferences: [], createdAt: timestamp, updatedAt: timestamp
|
|
}],
|
|
findingOccurrences: [{
|
|
id: occurrenceId, definitionId: 'pg-definition', scanRunId: scanRun.id, importFileId: fileId, sourceTableId: tableId,
|
|
sourceIdentity: { tool: scanRun.tool, originalRuleId: 'PG-1', canonicalRuleId: 'PG-1', originalTitle: 'PostgreSQL searchable finding' },
|
|
sourcePosition: { rowIndex: 1 },
|
|
originalFields: [{ key: 'rule', label: 'Rule', value: 'PG-1', columnIndex: 0 }],
|
|
fieldMappings: [{ targetRole: 'rule-id', sourceFieldKeys: ['rule'], normalizedValue: 'PG-1', provenance }],
|
|
severity: { originalValue: severity, normalizedValue: severity, provenance },
|
|
status: { originalValue: 'Open', normalizedValue: 'open', provenance }
|
|
}],
|
|
evidence: [{ id: `pg-evidence-${scanRun.sequence}`, occurrenceId, kind: 'text', value: 'Database evidence', sourceFieldKeys: ['evidence'] }],
|
|
diagnostics: [{ id: `pg-diagnostic-${scanRun.sequence}`, importFileId: fileId, severity: 'info', code: 'integration', message: 'PostgreSQL integration' }]
|
|
};
|
|
}
|
|
|
|
describePostgres('PostgresAssessorRepository integration', () => {
|
|
let pool: Pool;
|
|
let repository: PostgresAssessorRepository;
|
|
|
|
beforeAll(async () => {
|
|
pool = new Pool({ connectionString, max: 4, connectionTimeoutMillis: 5000 });
|
|
await runDatabaseMigrations(pool);
|
|
await expect(runDatabaseMigrations(pool)).resolves.toEqual([]);
|
|
repository = await PostgresAssessorRepository.create(pool);
|
|
}, 30000);
|
|
|
|
afterAll(async () => {
|
|
await pool?.end();
|
|
});
|
|
|
|
it('persists the complete repository contract with atomic imports, search and controlled deletion', async () => {
|
|
await repository.saveProject(project);
|
|
await repository.saveAssessment(assessment);
|
|
|
|
const first = run(1);
|
|
const firstSession = await repository.beginImport({ sessionId: 'pg-session-1', projectId: project.id, assessmentId: assessment.id, scanRun: first });
|
|
await firstSession.stage(importData(first));
|
|
expect((await repository.loadAssessmentModel(assessment.id))?.scanRuns).toHaveLength(0);
|
|
await firstSession.commit();
|
|
|
|
const second = run(2);
|
|
const secondSession = await repository.beginImport({ sessionId: 'pg-session-2', projectId: project.id, assessmentId: assessment.id, scanRun: second });
|
|
await secondSession.stage(importData(second, 'critical'));
|
|
await secondSession.commit();
|
|
|
|
await expect(repository.findOccurrences({ assessmentId: assessment.id, search: 'database evidence' }))
|
|
.resolves.toMatchObject({ total: 2 });
|
|
await expect(repository.findOccurrences({ severity: 'critical' })).resolves.toMatchObject({ total: 1 });
|
|
|
|
const review: FindingReview = {
|
|
id: 'pg-review', target: { kind: 'occurrence', occurrenceId: 'pg-occurrence-1' },
|
|
workflowStatus: 'open', priority: 'high', includeInActionPlan: true, markers: [],
|
|
createdAt: timestamp, updatedAt: timestamp, revision: 1
|
|
};
|
|
await repository.saveReview(review, 0);
|
|
await expect(repository.saveReview({ ...review, revision: 2 }, 0)).rejects.toBeInstanceOf(RepositoryConflictError);
|
|
|
|
await expect(repository.queryUnifiedResults({
|
|
projectId: project.id,
|
|
sourceTool: ['pingcastle'],
|
|
severity: ['critical']
|
|
})).resolves.toMatchObject({
|
|
total: 1,
|
|
items: [{
|
|
sourceKind: 'scanner-finding',
|
|
sourceResultId: 'pg-occurrence-2',
|
|
outcomeNormalized: 'unknown',
|
|
technicalExecutionState: 'completed'
|
|
}]
|
|
});
|
|
// Client-owned review state stays in internal analysis; the Result DB withholds it.
|
|
const disclosed = await repository.getUnifiedResultById('scanner-finding:pg-occurrence-1');
|
|
expect(disclosed).not.toHaveProperty('reviewStatus');
|
|
expect(disclosed).not.toHaveProperty('reviewPriority');
|
|
await expect(repository.getUnifiedResultById('scanner-finding:pg-occurrence-1', project.id)).resolves.toMatchObject({ sourceResultId: 'pg-occurrence-1' });
|
|
await expect(repository.getUnifiedResultById('scanner-finding:pg-occurrence-1', 'other-project')).resolves.toBeNull();
|
|
const internal = await repository.queryUnifiedResults({ audience: 'internal-analysis', projectId: project.id });
|
|
expect(internal.items.find(item => item.sourceResultId === 'pg-occurrence-1')).toMatchObject({
|
|
reviewStatus: 'open',
|
|
reviewPriority: 'high'
|
|
});
|
|
await expect(repository.queryUnifiedResultFacets({ projectId: project.id })).resolves.toMatchObject({
|
|
total: 2,
|
|
facets: expect.arrayContaining([
|
|
expect.objectContaining({ id: 'severity', values: expect.arrayContaining([
|
|
{ value: 'critical', label: 'critical', count: 1 },
|
|
{ value: 'high', label: 'high', count: 1 }
|
|
]) })
|
|
])
|
|
});
|
|
await expect(repository.listResultCategories({ projectId: project.id })).resolves.toEqual([
|
|
expect.objectContaining({
|
|
namespace: 'pingcastle',
|
|
categoryCode: 'Privileged Accounts',
|
|
sourceProfileId: 'pingcastle',
|
|
sourceProfileVersion: '1.0.0'
|
|
})
|
|
]);
|
|
const storedDimensions = await pool.query(
|
|
'SELECT outcome_normalized, technical_execution_state FROM assessor_finding_occurrences ORDER BY id'
|
|
);
|
|
expect(storedDimensions.rows).toHaveLength(2);
|
|
expect(storedDimensions.rows.every(row =>
|
|
row.outcome_normalized === 'unknown' && row.technical_execution_state === 'completed')).toBe(true);
|
|
await expect(pool.query(
|
|
'SELECT occurrence_id FROM assessor_finding_occurrence_categories ORDER BY occurrence_id'
|
|
)).resolves.toMatchObject({
|
|
rows: [{ occurrence_id: 'pg-occurrence-1' }, { occurrence_id: 'pg-occurrence-2' }]
|
|
});
|
|
|
|
await repository.saveProfileRegistration({ profileId: 'pingcastle', profileVersion: '1.0.0', name: 'PingCastle', schemaVersion: 1, enabled: true });
|
|
await repository.saveLocalComparisonRule(project.id, {
|
|
version: 1, id: 'pg-mapping', left: { toolId: 'pingcastle', ruleId: 'old' },
|
|
right: { toolId: 'pingcastle', ruleId: 'PG-1' }, source: 'user-confirmed'
|
|
});
|
|
const model = await repository.loadAssessmentModel(assessment.id);
|
|
const comparison = compareScanRuns(model!, first.id, second.id);
|
|
await repository.saveComparison({ id: 'pg-comparison', projectId: project.id, assessmentId: assessment.id, comparison });
|
|
|
|
const exported = await repository.exportProject(project.id);
|
|
expect(exported.scanRuns).toHaveLength(2);
|
|
expect(exported.findingOccurrences).toHaveLength(2);
|
|
expect(exported.reviews).toHaveLength(1);
|
|
expect(exported.comparisons).toHaveLength(1);
|
|
expect(exported.localComparisonRules).toHaveLength(1);
|
|
|
|
const conflicting = run(3);
|
|
const conflictSession = await repository.beginImport({ sessionId: 'pg-conflict', projectId: project.id, assessmentId: assessment.id, scanRun: conflicting });
|
|
await conflictSession.stage(importData(conflicting));
|
|
await repository.saveProfileRegistration({ profileId: 'maester', profileVersion: '1.0.0', name: 'Maester', schemaVersion: 1, enabled: true });
|
|
await expect(conflictSession.commit()).rejects.toBeInstanceOf(RepositoryConflictError);
|
|
expect((await repository.loadAssessmentModel(assessment.id))?.scanRuns).toHaveLength(2);
|
|
|
|
await expect(repository.deleteScanRun(project.id, first.id, {
|
|
projectId: project.id, scanRunId: first.id, cascade: true
|
|
})).resolves.toBe(true);
|
|
const afterScanDelete = await repository.loadAssessmentModel(assessment.id);
|
|
expect(afterScanDelete?.scanRuns.map(item => item.id)).toEqual([second.id]);
|
|
expect(afterScanDelete?.findingOccurrences.map(item => item.id)).toEqual(['pg-occurrence-2']);
|
|
await expect(repository.listReviews(project.id)).resolves.toEqual([]);
|
|
await expect(repository.listComparisons(assessment.id)).resolves.toEqual([]);
|
|
|
|
await expect(repository.deleteProject(project.id, { projectId: project.id, cascade: true })).resolves.toBe(true);
|
|
await expect(repository.getProject(project.id)).resolves.toBeNull();
|
|
await expect(repository.loadAssessmentModel(assessment.id)).resolves.toBeNull();
|
|
}, 30000);
|
|
|
|
it('persists immutable artifacts, processing history and leased jobs atomically', async () => {
|
|
const ingestionProject: AssessmentProject = {
|
|
...project, id: 'pg-ingestion-project', name: 'PostgreSQL ingestion project'
|
|
};
|
|
await pool.query('DELETE FROM assessor_projects WHERE id = $1', [ingestionProject.id]);
|
|
await repository.saveProject(ingestionProject);
|
|
const artifact: ImportArtifact = {
|
|
version: 1, id: 'pg-artifact', projectId: ingestionProject.id, kind: 'source-file',
|
|
relativePath: 'upload/synthetic.zip', sha256: 'a'.repeat(64), sizeBytes: 100,
|
|
sensitivity: 'confidential', parseStatus: 'pending',
|
|
storage: { kind: 'backend-controlled', storageKey: 'pg-ingestion-project/pg-artifact' }, createdAt: timestamp
|
|
};
|
|
await repository.saveImportArtifact(artifact);
|
|
const processing: ImportProcessingRun = {
|
|
version: 1, id: 'pg-processing-1', projectId: ingestionProject.id, artifactId: artifact.id,
|
|
status: 'partial', processors: [{ kind: 'artifact-parser', id: 'gpo-backup-ingestion', version: '1.0.0' }],
|
|
diagnosticCodes: [], startedAt: timestamp, completedAt: '2026-07-17T08:00:01.000Z'
|
|
};
|
|
const backupRecord: StoredGpoBackupInstance = {
|
|
version: 1,
|
|
id: 'pg-stored-gpo-backup',
|
|
projectId: ingestionProject.id,
|
|
artifactId: artifact.id,
|
|
processingRunId: processing.id,
|
|
instance: {
|
|
version: 1,
|
|
id: 'pg-gpo-instance',
|
|
projectId: ingestionProject.id,
|
|
sourceArtifactId: artifact.id,
|
|
identity: {
|
|
status: 'resolved',
|
|
gpoId: '11111111-1111-4111-8111-111111111111',
|
|
backupId: '22222222-2222-4222-8222-222222222222',
|
|
evidence: 'backup-metadata'
|
|
},
|
|
domain: { status: 'resolved', key: 'dns:synthetic.example', evidence: 'backup-metadata' },
|
|
collection: { status: 'observed', collectedAt: timestamp, evidence: 'backend-receipt' },
|
|
backupSet: { scope: 'unknown', evidence: 'unknown' },
|
|
metadataStatus: 'complete',
|
|
sections: { computer: 'present', user: 'unknown' },
|
|
inventory: {
|
|
version: 1,
|
|
status: 'complete',
|
|
entries: [{
|
|
version: 1,
|
|
artifactId: 'pg-metadata-artifact',
|
|
relativePath: 'synthetic/Backup.xml',
|
|
role: 'backup-metadata',
|
|
scope: 'shared',
|
|
disposition: 'supported',
|
|
secretIndicatorStatus: 'not-detected'
|
|
}, {
|
|
version: 1,
|
|
artifactId: 'pg-security-artifact',
|
|
relativePath: 'synthetic/DomainSysvol/GPO/Machine/Microsoft/Windows NT/SecEdit/GptTmpl.inf',
|
|
role: 'security-template',
|
|
scope: 'computer',
|
|
disposition: 'supported',
|
|
secretIndicatorStatus: 'not-inspected'
|
|
}, {
|
|
version: 1,
|
|
artifactId: 'pg-advanced-audit-artifact',
|
|
relativePath: 'synthetic/DomainSysvol/GPO/Machine/Microsoft/Windows NT/Audit/audit.csv',
|
|
role: 'advanced-audit-policy',
|
|
scope: 'computer',
|
|
disposition: 'supported',
|
|
secretIndicatorStatus: 'not-inspected'
|
|
}, {
|
|
version: 1,
|
|
artifactId: 'pg-gpp-registry-artifact',
|
|
relativePath: 'synthetic/DomainSysvol/GPO/Machine/Preferences/Registry/Registry.xml',
|
|
role: 'group-policy-preference',
|
|
scope: 'computer',
|
|
disposition: 'supported',
|
|
secretIndicatorStatus: 'not-detected'
|
|
}, {
|
|
version: 1,
|
|
artifactId: 'pg-gpp-groups-artifact',
|
|
relativePath: 'synthetic/DomainSysvol/GPO/Machine/Preferences/Groups/Groups.xml',
|
|
role: 'group-policy-preference',
|
|
scope: 'computer',
|
|
disposition: 'supported',
|
|
secretIndicatorStatus: 'present'
|
|
}, {
|
|
version: 1,
|
|
artifactId: 'pg-gpp-services-artifact',
|
|
relativePath: 'synthetic/DomainSysvol/GPO/Machine/Preferences/Services/Services.xml',
|
|
role: 'group-policy-preference',
|
|
scope: 'computer',
|
|
disposition: 'supported',
|
|
secretIndicatorStatus: 'present'
|
|
}, {
|
|
version: 1,
|
|
artifactId: 'pg-gpp-drives-artifact',
|
|
relativePath: 'synthetic/DomainSysvol/GPO/User/Preferences/Drives/Drives.xml',
|
|
role: 'group-policy-preference',
|
|
scope: 'user',
|
|
disposition: 'supported',
|
|
secretIndicatorStatus: 'present'
|
|
}],
|
|
diagnosticCodes: []
|
|
},
|
|
policyObservations: [{
|
|
version: 1,
|
|
id: 'pg-policy-observation',
|
|
backupInstanceId: 'pg-gpo-instance',
|
|
sourceArtifactId: artifact.id,
|
|
artifactId: 'pg-security-artifact',
|
|
sourcePath: 'synthetic/DomainSysvol/GPO/Machine/Microsoft/Windows NT/SecEdit/GptTmpl.inf',
|
|
sourceSha256: 'b'.repeat(64),
|
|
scope: 'computer',
|
|
deliveryMechanism: 'security-template',
|
|
parser: { id: 'gpo-security-template-inf', version: '1.0.0' },
|
|
reducer: { id: 'gpo-security-template-reducer', version: '1.0.0' },
|
|
identity: { status: 'unresolved', namespace: 'security-template', section: 'system access', key: 'MinimumPasswordLength' },
|
|
state: 'configured',
|
|
values: [{ kind: 'scalar', raw: '14' }],
|
|
sourceLines: [4],
|
|
effectiveState: 'unknown',
|
|
diagnosticCodes: []
|
|
}, {
|
|
version: 1,
|
|
id: 'pg-advanced-audit-observation',
|
|
backupInstanceId: 'pg-gpo-instance',
|
|
sourceArtifactId: artifact.id,
|
|
artifactId: 'pg-advanced-audit-artifact',
|
|
sourcePath: 'synthetic/DomainSysvol/GPO/Machine/Microsoft/Windows NT/Audit/audit.csv',
|
|
sourceSha256: 'c'.repeat(64),
|
|
scope: 'computer',
|
|
deliveryMechanism: 'advanced-audit-policy',
|
|
parser: { id: 'gpo-advanced-audit-csv', version: '1.0.0' },
|
|
reducer: { id: 'gpo-advanced-audit-reducer', version: '1.0.0' },
|
|
identity: {
|
|
status: 'unresolved', namespace: 'advanced-audit-policy',
|
|
section: 'system audit subcategory', key: '0cce9210-69ae-11d9-bed3-505054503030'
|
|
},
|
|
state: 'configured',
|
|
values: [{ kind: 'audit-flags', code: 3, success: true, failure: true }],
|
|
sourceLines: [2],
|
|
effectiveState: 'unknown',
|
|
diagnosticCodes: []
|
|
}, {
|
|
version: 1,
|
|
id: 'pg-gpp-registry-observation',
|
|
backupInstanceId: 'pg-gpo-instance',
|
|
sourceArtifactId: artifact.id,
|
|
artifactId: 'pg-gpp-registry-artifact',
|
|
sourcePath: 'synthetic/DomainSysvol/GPO/Machine/Preferences/Registry/Registry.xml',
|
|
sourceSha256: 'd'.repeat(64),
|
|
scope: 'computer',
|
|
deliveryMechanism: 'group-policy-preference',
|
|
parser: { id: 'gpo-preferences-xml', version: '1.0.0' },
|
|
reducer: { id: 'gpo-preferences-registry-reducer', version: '1.0.0' },
|
|
identity: {
|
|
status: 'unresolved', namespace: 'group-policy-preference',
|
|
section: 'registry', key: 'HKEY_LOCAL_MACHINE\Software\Synthetic\Mode'
|
|
},
|
|
state: 'instruction',
|
|
values: [{
|
|
kind: 'registry-preference', hive: 'HKEY_LOCAL_MACHINE', key: 'Software\Synthetic',
|
|
target: 'value', valueName: 'Mode', valueType: 'REG_SZ', rawData: 'Strict'
|
|
}],
|
|
preference: {
|
|
cse: 'registry', itemUid: '33333333-3333-4333-8333-333333333333',
|
|
operation: 'update', sequence: 0, disabled: false, removePolicy: false, applyOnce: false,
|
|
targeting: { status: 'none', filterTypes: [] }
|
|
},
|
|
sourceLines: [1],
|
|
effectiveState: 'unknown',
|
|
diagnosticCodes: []
|
|
}, {
|
|
version: 1,
|
|
id: 'pg-gpp-local-user-observation',
|
|
backupInstanceId: 'pg-gpo-instance',
|
|
sourceArtifactId: artifact.id,
|
|
artifactId: 'pg-gpp-groups-artifact',
|
|
sourcePath: 'synthetic/DomainSysvol/GPO/Machine/Preferences/Groups/Groups.xml',
|
|
sourceSha256: 'e'.repeat(64),
|
|
scope: 'computer',
|
|
deliveryMechanism: 'group-policy-preference',
|
|
parser: { id: 'gpo-preferences-xml', version: '1.0.0' },
|
|
reducer: { id: 'gpo-preferences-local-users-groups-reducer', version: '1.0.0' },
|
|
identity: {
|
|
status: 'unresolved', namespace: 'group-policy-preference',
|
|
section: 'local user', key: 'syntheticoperator'
|
|
},
|
|
state: 'instruction',
|
|
values: [{
|
|
kind: 'local-user-preference', userName: 'SyntheticOperator',
|
|
passwordAttributePresent: true, accountDisabled: true
|
|
}],
|
|
preference: {
|
|
cse: 'local-users-groups', itemUid: '44444444-4444-4444-8444-444444444444',
|
|
operation: 'update', sequence: 0, disabled: false, removePolicy: false, applyOnce: false,
|
|
targeting: { status: 'none', filterTypes: [] }
|
|
},
|
|
sourceLines: [1],
|
|
effectiveState: 'unknown',
|
|
diagnosticCodes: ['gpo-secret-indicator-present']
|
|
}, {
|
|
version: 1,
|
|
id: 'pg-gpp-service-observation',
|
|
backupInstanceId: 'pg-gpo-instance',
|
|
sourceArtifactId: artifact.id,
|
|
artifactId: 'pg-gpp-services-artifact',
|
|
sourcePath: 'synthetic/DomainSysvol/GPO/Machine/Preferences/Services/Services.xml',
|
|
sourceSha256: 'f'.repeat(64),
|
|
scope: 'computer',
|
|
deliveryMechanism: 'group-policy-preference',
|
|
parser: { id: 'gpo-preferences-xml', version: '1.0.0' },
|
|
reducer: { id: 'gpo-preferences-services-reducer', version: '1.0.0' },
|
|
identity: {
|
|
status: 'unresolved', namespace: 'group-policy-preference',
|
|
section: 'service', key: 'syntheticservice'
|
|
},
|
|
state: 'instruction',
|
|
values: [{
|
|
kind: 'service-preference', serviceName: 'SyntheticService', serviceAction: 'restart-if-required',
|
|
startupType: 'automatic', accountName: 'Synthetic\\Service', passwordAttributePresent: true,
|
|
thirdFailure: 'restart', restartServiceDelay: 900000
|
|
}],
|
|
preference: {
|
|
cse: 'services', itemUid: '55555555-5555-4555-8555-555555555555',
|
|
operation: 'configure', sequence: 0, disabled: false, removePolicy: false, applyOnce: false,
|
|
targeting: { status: 'none', filterTypes: [] }
|
|
},
|
|
sourceLines: [1],
|
|
effectiveState: 'unknown',
|
|
diagnosticCodes: ['gpo-secret-indicator-present']
|
|
}, {
|
|
version: 1,
|
|
id: 'pg-gpp-drive-map-observation',
|
|
backupInstanceId: 'pg-gpo-instance',
|
|
sourceArtifactId: artifact.id,
|
|
artifactId: 'pg-gpp-drives-artifact',
|
|
sourcePath: 'synthetic/DomainSysvol/GPO/User/Preferences/Drives/Drives.xml',
|
|
sourceSha256: '1'.repeat(64),
|
|
scope: 'user',
|
|
deliveryMechanism: 'group-policy-preference',
|
|
parser: { id: 'gpo-preferences-xml', version: '1.0.0' },
|
|
reducer: { id: 'gpo-preferences-drive-maps-reducer', version: '1.0.0' },
|
|
identity: {
|
|
status: 'unresolved', namespace: 'group-policy-preference',
|
|
section: 'drive map', key: 'S:exact'
|
|
},
|
|
state: 'instruction',
|
|
values: [{
|
|
kind: 'drive-map-preference', path: '\\\\synthetic-server\\share', label: 'Synthetic Share',
|
|
persistent: true, userName: 'SYNTHETIC\\DriveUser', passwordAttributePresent: true,
|
|
letter: 'S', useLetter: true, selectionMode: 'exact', thisDrive: 'show', allDrives: 'no-change'
|
|
}],
|
|
preference: {
|
|
cse: 'drive-maps', itemUid: '66666666-6666-4666-8666-666666666666',
|
|
operation: 'replace', sequence: 0, disabled: false, removePolicy: false, applyOnce: false,
|
|
targeting: { status: 'none', filterTypes: [] }
|
|
},
|
|
sourceLines: [1],
|
|
effectiveState: 'unknown',
|
|
diagnosticCodes: ['gpo-secret-indicator-present']
|
|
}],
|
|
discoveredAt: timestamp
|
|
},
|
|
createdAt: '2026-07-17T08:00:01.000Z'
|
|
};
|
|
await repository.saveGpoBackupAnalysis(processing, [backupRecord]);
|
|
await expect(repository.listProjectProcessingRuns(ingestionProject.id)).resolves.toMatchObject([{ id: processing.id, artifactId: artifact.id }]);
|
|
const revisionAfterAnalysis = repository.revision;
|
|
await expect(repository.saveGpoBackupAnalysis(processing, [backupRecord])).resolves.toBeUndefined();
|
|
expect(repository.revision).toBe(revisionAfterAnalysis);
|
|
await expect(repository.saveGpoBackupAnalysis(processing, [{ ...backupRecord, createdAt: '2026-07-17T08:00:02.000Z' }]))
|
|
.rejects.toBeInstanceOf(RepositoryConflictError);
|
|
|
|
const job: IngestionJob = {
|
|
version: 1, id: 'pg-job', projectId: ingestionProject.id, artifactId: artifact.id,
|
|
kind: 'analyze-gpo-backup', idempotencyKey: 'gpo:pg-artifact:1.0.0', status: 'queued',
|
|
attempts: 0, maxAttempts: 3, availableAt: timestamp, createdAt: timestamp, updatedAt: timestamp
|
|
};
|
|
await repository.enqueueIngestionJob(job);
|
|
const revisionAfterJobInsert = repository.revision;
|
|
await expect(repository.enqueueIngestionJob({ ...job, id: 'ignored-by-idempotency' })).resolves.toMatchObject({ id: job.id });
|
|
expect(repository.revision).toBe(revisionAfterJobInsert);
|
|
await repository.claimNextIngestionJob({ workerId: 'worker-old', now: timestamp, leaseDurationMs: 1000, kinds: ['analyze-gpo-backup'] });
|
|
const recovered = await repository.claimNextIngestionJob({
|
|
workerId: 'worker-new', now: '2026-07-17T08:00:01.001Z', leaseDurationMs: 1000, kinds: ['analyze-gpo-backup']
|
|
});
|
|
expect(recovered).toMatchObject({ id: job.id, status: 'running', attempts: 2, leaseOwner: 'worker-new' });
|
|
await expect(repository.completeIngestionJob(job.id, 'worker-old', '2026-07-17T08:00:01.200Z')).rejects.toBeInstanceOf(RepositoryConflictError);
|
|
await expect(repository.completeIngestionJob(job.id, 'worker-new', '2026-07-17T08:00:01.500Z'))
|
|
.resolves.toMatchObject({ status: 'succeeded', attempts: 2 });
|
|
|
|
const exported = await repository.exportProject(ingestionProject.id);
|
|
expect(exported).toMatchObject({ importArtifacts: [{ id: artifact.id }], processingRuns: [{ id: processing.id }], gpoBackupInstances: [{ id: backupRecord.id, instance: { policyObservations: [{ id: 'pg-policy-observation' }, { id: 'pg-advanced-audit-observation' }, { id: 'pg-gpp-registry-observation' }, { id: 'pg-gpp-local-user-observation' }, { id: 'pg-gpp-service-observation' }, { id: 'pg-gpp-drive-map-observation' }] } }], ingestionJobs: [{ id: job.id }] });
|
|
await repository.deleteProject(ingestionProject.id, { projectId: ingestionProject.id, cascade: true });
|
|
}, 30000);
|
|
it('commits a GPO artifact and its job atomically and rolls both back on an idempotency conflict', async () => {
|
|
const atomicProject: AssessmentProject = { ...project, id: 'pg-atomic-gpo-project', name: 'PostgreSQL atomic GPO project' };
|
|
await pool.query('DELETE FROM assessor_projects WHERE id = $1', [atomicProject.id]);
|
|
await repository.saveProject(atomicProject);
|
|
const firstArtifact: ImportArtifact = {
|
|
version: 1, id: 'pg-atomic-artifact-1', projectId: atomicProject.id, kind: 'source-file',
|
|
relativePath: 'gpo-backups/first.zip', sha256: 'b'.repeat(64), sizeBytes: 10,
|
|
sensitivity: 'secret', parseStatus: 'pending',
|
|
storage: { kind: 'backend-controlled', storageKey: 'synthetic-atomic-storage-1' }, createdAt: timestamp
|
|
};
|
|
const firstJob: IngestionJob = {
|
|
version: 1, id: 'pg-atomic-job-1', projectId: atomicProject.id, artifactId: firstArtifact.id,
|
|
kind: 'analyze-gpo-backup', idempotencyKey: 'atomic-gpo-key', status: 'queued', attempts: 0, maxAttempts: 3,
|
|
availableAt: timestamp, createdAt: timestamp, updatedAt: timestamp
|
|
};
|
|
await expect(repository.saveImportArtifactAndEnqueueJob(firstArtifact, firstJob)).resolves.toMatchObject({
|
|
artifactCreated: true, jobCreated: true, artifact: { id: firstArtifact.id }, job: { id: firstJob.id }
|
|
});
|
|
const conflictingArtifact: ImportArtifact = {
|
|
...firstArtifact,
|
|
id: 'pg-atomic-artifact-2',
|
|
relativePath: 'gpo-backups/second.zip',
|
|
sha256: 'c'.repeat(64),
|
|
storage: { kind: 'backend-controlled', storageKey: 'synthetic-atomic-storage-2' }
|
|
};
|
|
const conflictingJob: IngestionJob = { ...firstJob, id: 'pg-atomic-job-2', artifactId: conflictingArtifact.id };
|
|
|
|
await expect(repository.saveImportArtifactAndEnqueueJob(conflictingArtifact, conflictingJob)).rejects.toBeInstanceOf(RepositoryConflictError);
|
|
await expect(repository.listImportArtifacts(atomicProject.id)).resolves.toEqual([firstArtifact]);
|
|
await expect(repository.listIngestionJobs(atomicProject.id)).resolves.toEqual([firstJob]);
|
|
await repository.deleteProject(atomicProject.id, { projectId: atomicProject.id, cascade: true });
|
|
}, 30000);
|
|
it('selects PostgreSQL through the same backend runtime factory without silent fallback', async () => {
|
|
const runtime = await createRepositoryRuntime({
|
|
ASSESSOR_DATABASE_URL: connectionString,
|
|
ASSESSOR_DATABASE_SSL: 'disable',
|
|
ASSESSOR_DATABASE_POOL_SIZE: '2'
|
|
});
|
|
try {
|
|
expect(runtime.persistence).toBe('postgresql');
|
|
await expect(runtime.repository.listProjects()).resolves.toEqual([]);
|
|
} finally {
|
|
await runtime.close();
|
|
}
|
|
});
|
|
it('persists durable import fingerprints and increments duplicate counters transactionally', async () => {
|
|
const sha256 = 'f'.repeat(64);
|
|
await pool.query('DELETE FROM assessor_import_fingerprints WHERE sha256 = $1', [sha256]);
|
|
const entry = { sha256, fileName: 'repeat.csv', sizeBytes: 99, lastModified: 17 };
|
|
await repository.recordImportFingerprints([entry], '2026-07-17T10:00:00.000Z');
|
|
const repeated = await repository.recordImportFingerprints([entry], '2026-07-17T11:00:00.000Z');
|
|
expect(repeated[0]).toMatchObject({ ...entry, importCount: 2, firstImportedAt: '2026-07-17T10:00:00.000Z', lastImportedAt: '2026-07-17T11:00:00.000Z' });
|
|
});
|
|
it('commits a prepared canonical batch and its fingerprint in one PostgreSQL revision', async () => {
|
|
const batchProject: AssessmentProject = { ...project, id: 'pg-batch-project', name: 'Batch project' };
|
|
const batchAssessment: Assessment = { ...assessment, id: 'pg-batch-assessment', projectId: batchProject.id };
|
|
const batchRun: ScanRun = { ...run(7), id: 'pg-batch-run', assessmentId: batchAssessment.id };
|
|
const sha256 = 'e'.repeat(64);
|
|
const staged = importData(batchRun);
|
|
const model: CanonicalAssessmentModel = {
|
|
schemaVersion: 1,
|
|
project: batchProject,
|
|
assessment: batchAssessment,
|
|
scanRuns: [batchRun],
|
|
importFiles: staged.importFiles!.map(file => ({ ...file, sha256 })),
|
|
sourceTables: staged.sourceTables!,
|
|
findingDefinitions: staged.findingDefinitions!,
|
|
findingOccurrences: staged.findingOccurrences!,
|
|
evidence: staged.evidence!,
|
|
reviews: [],
|
|
diagnostics: staged.diagnostics!
|
|
};
|
|
await pool.query('DELETE FROM assessor_import_fingerprints WHERE sha256 = $1', [sha256]);
|
|
const before = repository.revision;
|
|
const committed = await repository.commitCanonicalImport({
|
|
sessionId: 'pg-canonical-batch',
|
|
model,
|
|
fingerprints: [{ sha256, fileName: staged.importFiles![0].fileName, sizeBytes: 100, lastModified: 0 }]
|
|
});
|
|
expect(committed).toMatchObject({ repositoryRevision: before + 1, importedFiles: 1, importedOccurrences: 1 });
|
|
await expect(repository.loadAssessmentModel(batchAssessment.id)).resolves.toMatchObject({ importFiles: [{ id: staged.importFiles![0].id }] });
|
|
await expect(repository.findImportFingerprints([sha256])).resolves.toMatchObject([{ importCount: 1 }]);
|
|
await repository.deleteProject(batchProject.id, { projectId: batchProject.id, cascade: true });
|
|
});
|
|
});
|
|
|
|
|
|
|