314 lines
26 KiB
JavaScript
314 lines
26 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import { readdirSync, readFileSync } from 'node:fs';
|
||
import { extname, join, resolve } from 'node:path';
|
||
|
||
const root = resolve(import.meta.dirname, '..');
|
||
|
||
function read(relativePath) {
|
||
return readFileSync(join(root, relativePath), 'utf8');
|
||
}
|
||
|
||
function sourceFiles(directory) {
|
||
return readdirSync(join(root, directory), { withFileTypes: true }).flatMap(entry => {
|
||
const relativePath = join(directory, entry.name);
|
||
if (entry.isDirectory()) return sourceFiles(relativePath);
|
||
return ['.ts', '.tsx'].includes(extname(entry.name)) ? [relativePath] : [];
|
||
});
|
||
}
|
||
|
||
const forbiddenRuntimePatterns = [
|
||
[/\bfetch\s*\(/, 'fetch'],
|
||
[/\bXMLHttpRequest\b/, 'XMLHttpRequest'],
|
||
[/\bWebSocket\b/, 'WebSocket'],
|
||
[/\bEventSource\b/, 'EventSource'],
|
||
[/\bsendBeacon\b/, 'sendBeacon'],
|
||
[/\bBroadcastChannel\b/, 'BroadcastChannel'],
|
||
[/serviceWorker\.register/, 'service worker registration'],
|
||
[/@tauri-apps\/api\/http/, 'Tauri HTTP API'],
|
||
[/\baxios\b/, 'axios']
|
||
];
|
||
|
||
const sameOriginApiBoundary = new Set([join('src', 'api', 'assessor-api-client.ts')]);
|
||
|
||
for (const relativePath of sourceFiles('src')) {
|
||
const source = read(relativePath);
|
||
for (const [pattern, label] of forbiddenRuntimePatterns) {
|
||
if (label === 'fetch' && sameOriginApiBoundary.has(relativePath)) continue;
|
||
assert.doesNotMatch(source, pattern, `${relativePath} introduces forbidden network capability: ${label}`);
|
||
}
|
||
}
|
||
|
||
const indexHtml = read('index.html');
|
||
assert.doesNotMatch(indexHtml, /<(?:script|link)\b[^>]+(?:src|href)=["']https?:/i, 'index.html must not load remote assets');
|
||
|
||
const packageJson = JSON.parse(read('package.json'));
|
||
assert.deepEqual(
|
||
Object.keys(packageJson.dependencies).sort(),
|
||
['@fontsource-variable/inter', 'jsdom', 'pg', 'react', 'react-dom', 'saxes', 'xlsx'],
|
||
'production dependencies changed; review same-origin and local-processing guarantees before accepting new packages'
|
||
);
|
||
|
||
const excelImporter = read(join('src', 'importers', 'excel-importer.ts'));
|
||
assert.match(excelImporter, /cellFormula:\s*false/, 'Excel formulas must remain disabled');
|
||
assert.match(excelImporter, /bookVBA:\s*false/, 'Excel VBA extraction must remain disabled');
|
||
|
||
const platformNeutralDirectories = [
|
||
'src/domain',
|
||
'src/analysis',
|
||
'src/importers',
|
||
'src/profiles',
|
||
'src/recognition',
|
||
'src/comparison',
|
||
'src/gpo',
|
||
'src/persistence',
|
||
'src/exports',
|
||
'src/shared'
|
||
];
|
||
const browserBoundaryExceptions = new Set([join('src', 'importers', 'importer.ts')]);
|
||
const forbiddenCorePatterns = [
|
||
[/\bDOMParser\b/, 'DOMParser'],
|
||
[/\bwindow\b/, 'window'],
|
||
[/\blocalStorage\b/, 'localStorage'],
|
||
[/\bFile\b/, 'browser File'],
|
||
[/\bBlob\b/, 'browser Blob'],
|
||
[/URL\.createObjectURL/, 'browser object URL'],
|
||
[/from\s+['"](?:react|react-dom|@tauri)/, 'UI or Tauri runtime import']
|
||
];
|
||
|
||
for (const directory of platformNeutralDirectories) {
|
||
for (const relativePath of sourceFiles(directory)) {
|
||
if (browserBoundaryExceptions.has(relativePath)) continue;
|
||
const source = read(relativePath);
|
||
for (const [pattern, label] of forbiddenCorePatterns) {
|
||
assert.doesNotMatch(source, pattern, `${relativePath} crosses the platform-neutral core boundary: ${label}`);
|
||
}
|
||
}
|
||
}
|
||
const uiDirectories = ['src/app', 'src/components', 'src/features'];
|
||
const forbiddenUiDatabasePatterns = [
|
||
[/(?:SELECT|INSERTs+INTO|UPDATEs+w+s+SET|DELETEs+FROM)/i, 'SQL statement'],
|
||
[/froms+['"](?:pg|postgres|sqlite|better-sqlite3|typeorm|@prisma)/i, 'database client import']
|
||
];
|
||
|
||
for (const directory of uiDirectories) {
|
||
for (const relativePath of sourceFiles(directory)) {
|
||
const source = read(relativePath);
|
||
for (const [pattern, label] of forbiddenUiDatabasePatterns) {
|
||
assert.doesNotMatch(source, pattern, relativePath + ' introduces forbidden UI database access: ' + label);
|
||
}
|
||
}
|
||
}
|
||
|
||
const apiClient = read(join('src', 'api', 'assessor-api-client.ts'));
|
||
assert.match(apiClient, /ASSESSOR_API_PREFIX/, 'the browser API client must use the versioned same-origin API prefix');
|
||
assert.doesNotMatch(apiClient, /https?:\/\//, 'the browser API client must not embed a remote origin');
|
||
const serverEntry = read(join('server', 'index.ts'));
|
||
assert.match(serverEntry, /'127\.0\.0\.1'/, 'the local server must bind to loopback by default');
|
||
const httpServer = read(join('server', 'assessor-http-server.ts'));
|
||
assert.doesNotMatch(httpServer, /Access-Control-Allow-Origin/i, 'the local server must not enable cross-origin access');
|
||
assert.match(httpServer, /ASSESSOR_MUTATION_HEADER/, 'mutating API requests must retain the same-origin request guard');
|
||
assert.match(httpServer, /isValidImportBase64/, 'large Base64 import payloads must use iterative validation');
|
||
assert.doesNotMatch(httpServer, /\[A-Za-z0-9\+\/\]\{4\}.*\*/, 'Base64 validation must not use an unbounded repeated-group regex');
|
||
const nvdCveClient = read(join('server', 'cve', 'nvd-cve-client.ts'));
|
||
assert.match(nvdCveClient, /const NVD_CVE_API = 'https:\/\/services\.nvd\.nist\.gov\/rest\/json\/cves\/2\.0'/,
|
||
'the approved NVD client must retain its fixed backend origin');
|
||
assert.match(nvdCveClient, /MAXIMUM_RESPONSE_BYTES/, 'NVD responses must remain size-bounded');
|
||
assert.match(nvdCveClient, /AbortSignal\.timeout/, 'NVD requests must retain a timeout');
|
||
assert.doesNotMatch(nvdCveClient, /new URL\([^N]/, 'the NVD client must not construct request targets from user input');
|
||
const liteLlmClient = read(join('server', 'ai', 'litellm-client.ts'));
|
||
assert.match(liteLlmClient, /url\.protocol !== 'https:'/, 'the approved work LiteLLM client must require HTTPS');
|
||
assert.match(liteLlmClient, /redirect: 'error'/, 'the work LiteLLM client must reject redirects away from its configured origin');
|
||
assert.match(liteLlmClient, /Authorization: `Bearer \$\{this\.apiKey\}`/, 'the work LiteLLM key must remain in the backend client boundary');
|
||
assert.match(liteLlmClient, /maximumOutputBytes/, 'work LiteLLM responses must remain size-bounded');
|
||
assert.match(liteLlmClient, /controller\.abort\(\)/, 'work LiteLLM requests must retain an enforced timeout');
|
||
assert.doesNotMatch(apiClient, /ASSESSOR_AI_(?:BASE_URL|API_KEY)/, 'the browser API client must not receive the LiteLLM origin or secret');
|
||
const aiOverviewService = read(join('server', 'ai', 'ai-overview-service.ts'));
|
||
assert.match(aiOverviewService, /resultsToPrivacySafeExportJson/, 'AI generation must reuse the privacy-safe Result DB export');
|
||
assert.doesNotMatch(aiOverviewService, /console\.(?:log|warn|error)/, 'AI generation must not log prompts, provider bodies, or secrets');
|
||
const postgresRepository = read(join('server', 'postgres', 'postgres-assessor-repository.ts'));
|
||
assert.doesNotMatch(postgresRepository, /\\$\\{query\\./, 'PostgreSQL queries must not interpolate fields directly from request queries');
|
||
assert.match(postgresRepository, /FOR UPDATE/, 'PostgreSQL mutations must serialize repository revision updates');
|
||
const migrationRunner = read(join('server', 'postgres', 'migration-runner.ts'));
|
||
assert.match(migrationRunner, /pg_advisory_lock/, 'database migrations must use a PostgreSQL advisory lock');
|
||
const resourcePolicy = read(join('src', 'importers', 'resource-policy.ts'));
|
||
assert.match(resourcePolicy, /maximumZipCompressionRatio/, 'resource policy must enforce a ZIP compression-ratio limit');
|
||
assert.match(resourcePolicy, /xml-doctype-rejected/, 'resource policy must reject XML document types and entities');
|
||
assert.match(resourcePolicy, /maximumTotalCellCharacters/, 'resource policy must cap total decoded cell content');
|
||
const workerClient = read(join('server', 'import-worker-client.ts'));
|
||
assert.match(workerClient, /resourceLimits/, 'server imports must run with worker heap limits');
|
||
assert.match(workerClient, /parser-time-limit/, 'server imports must retain an enforced parser timeout');
|
||
assert.match(workerClient, /maximumQueuedBytes/, 'server parser queue must be byte-bounded');
|
||
const gpoFolderTransport = read(join('src', 'gpo', 'gpo-folder-transport.ts'));
|
||
assert.match(gpoFolderTransport, /normalizeArtifactRelativePath/, 'GPO folder transport must normalize every browser-relative path');
|
||
assert.match(gpoFolderTransport, /artifactPathCollisionKey/, 'GPO folder transport must reject Unicode and case path collisions');
|
||
assert.match(gpoFolderTransport, /maximumZipEntries/, 'GPO folder transport must enforce the shared entry limit');
|
||
assert.match(gpoFolderTransport, /compressionMethod|setUint16\(offset \+ 8, 0/, 'GPO folder transport must use a bounded non-compressing transport envelope');
|
||
const gpoFolderUi = read(join('src', 'features', 'project-import', 'GpoFolderImport.tsx'));
|
||
assert.match(gpoFolderUi, /webkitdirectory/, 'the Datahub GPO import must select a complete browser folder');
|
||
assert.match(gpoFolderUi, /announceSubmission\(envelope,/, 'the GPO folder envelope must enter the shared submission boundary');
|
||
assert.match(gpoFolderUi, /submitGpoBackup\(projectId, file, assessmentId\)/, 'folder envelopes and uploaded ZIPs must cross the project-and-timeslot-bound API');
|
||
assert.match(gpoFolderUi, /accept="\.zip,application\/zip"/, 'the Datahub GPO import must also accept an existing ZIP package');
|
||
const gpoZipInventory = read(join('server', 'gpo', 'zip-inventory.ts'));
|
||
assert.match(gpoZipInventory, /zip-entry-crc-mismatch/, 'GPO ZIP inventory must verify actual entry CRC values');
|
||
assert.match(gpoZipInventory, /zip-nested-archive-rejected/, 'GPO ZIP inventory must reject nested archives');
|
||
assert.match(gpoZipInventory, /zip-link-rejected/, 'GPO ZIP inventory must reject symbolic links');
|
||
assert.match(gpoZipInventory, /inflateRawSync/, 'GPO ZIP inventory must verify actual inflated sizes');
|
||
const gpoWorkerClient = read(join('server', 'gpo', 'gpo-inventory-worker-client.ts'));
|
||
assert.match(gpoWorkerClient, /resourceLimits/, 'GPO inventory must run with worker heap limits');
|
||
assert.match(gpoWorkerClient, /gpo-inventory-time-limit/, 'GPO inventory must retain an enforced timeout');
|
||
assert.match(gpoWorkerClient, /maximumSourceBytes/, 'GPO inventory worker input must be byte-bounded');
|
||
assert.match(gpoWorkerClient, /transferList: \[transferableBytes\.buffer\]/, 'large GPO worker inputs must transfer a worker-owned buffer instead of structured-cloning it');
|
||
const gpoExecutor = read(join('server', 'gpo', 'gpo-inventory-executor.ts'));
|
||
assert.match(gpoExecutor, /GpoBackupWorkerResult/, 'the GPO worker must use a persistence-focused result contract');
|
||
assert.doesNotMatch(gpoExecutor.match(/export interface GpoBackupWorkerResult[\\s\\S]*?\\n}/)?.[0] ?? '', /Documents|container/, 'the worker result must not clone container or parser intermediate documents back to the backend');
|
||
const artifactStore = read(join('server', 'artifacts', 'backend-artifact-store.ts'));
|
||
assert.match(artifactStore, /randomUUID/, 'raw artifacts must use opaque backend-generated storage keys');
|
||
assert.match(artifactStore, /staging/, 'raw artifacts must be written to a private staging area first');
|
||
assert.match(artifactStore, /handle\.sync/, 'raw artifact staging writes must be flushed before publication');
|
||
assert.match(artifactStore, /await link/, 'raw artifacts must be published without overwriting an existing object');
|
||
assert.match(artifactStore, /artifact-store-integrity-mismatch/, 'raw artifact reads must verify stored size and SHA-256');
|
||
assert.match(artifactStore, /assertArtifactStoreOutsidePublicRoot/, 'raw artifact storage must reject a root inside the published webroot');
|
||
const gpoMetadataParser = read(join('server', 'gpo', 'gpo-metadata-parser.ts'));
|
||
const securityTemplateParser = read(join('server', 'gpo', 'security-template-parser.ts'));
|
||
assert.match(securityTemplateParser, /maximumFieldCharacters/, 'Security Template parsing must enforce the documented INF field boundary');
|
||
assert.match(securityTemplateParser, /gpo-security-template-section-unsupported/, 'unknown Security Template sections must remain explicitly unsupported');
|
||
assert.match(securityTemplateParser, /state: conflicted \? 'conflicted' : 'configured'/, 'Security Template reduction must retain conflicting duplicate instructions');
|
||
assert.match(securityTemplateParser, /effectiveState: 'unknown'/, 'Security Template observations must not claim effective application');
|
||
assert.doesNotMatch(securityTemplateParser, /FindingCandidate/, 'GPO policy parsing must remain outside the scanner candidate path');
|
||
const advancedAuditParser = read(join('server', 'gpo', 'advanced-audit-parser.ts'));
|
||
assert.match(advancedAuditParser, /parseCsvLexically/, 'Advanced Audit must reuse only the generic lexical CSV boundary');
|
||
assert.match(advancedAuditParser, /subcategory guid/, 'Advanced Audit identity must require the technical subcategory GUID column');
|
||
assert.match(advancedAuditParser, /setting value/, 'Advanced Audit flags must come from the numeric setting column');
|
||
assert.match(advancedAuditParser, /gpo-advanced-audit-target-unsupported/, 'per-user audit semantics must remain explicitly unsupported');
|
||
assert.match(advancedAuditParser, /effectiveState: 'unknown'/, 'Advanced Audit observations must not claim effective application');
|
||
assert.doesNotMatch(advancedAuditParser, /FindingCandidate/, 'Advanced Audit parsing must remain outside the scanner candidate path');
|
||
const gppParser = read(join('server', 'gpo', 'group-policy-preference-parser.ts'));
|
||
assert.match(gppParser, /gpo-preference-external-reference-rejected/, 'GPP XML must reject active and external XML constructs');
|
||
assert.match(gppParser, /operation: instruction\.operation/, 'GPP reduction must retain CSE-specific operation semantics');
|
||
assert.match(gppParser, /sequence: instruction\.sequence/, 'GPP reduction must retain item processing order');
|
||
assert.match(gppParser, /expressionSha256/, 'GPP targeting must remain attached as a non-evaluated expression fingerprint');
|
||
assert.match(gppParser, /gpo-secret-indicator-present/, 'GPP parsing must detect cpassword without exposing it');
|
||
assert.doesNotMatch(gppParser, /decrypt|decipher|createDecipher/i, 'GPP parsing must never decrypt retained secrets');
|
||
assert.match(gppParser, /effectiveState: 'unknown'/, 'GPP observations must not claim effective application');
|
||
assert.doesNotMatch(gppParser, /FindingCandidate/, 'GPP parsing must remain outside the scanner candidate path');
|
||
assert.match(gppParser, /dom\.window\.close\(\)/, 'each GPP DOM must release its JSDOM window after parsing');
|
||
const gppLocalUsersGroups = read(join('server', 'gpo', 'gpp-local-users-groups.ts'));
|
||
assert.match(gppParser, /rootName === 'groups'/, 'Groups.xml must cross the shared safe GPP XML boundary');
|
||
assert.match(gppLocalUsersGroups, /selector: sid \?/, 'Local group membership must retain SID precedence over display names');
|
||
assert.match(gppLocalUsersGroups, /deleteAllUsers/, 'Local group reduction must retain delete-all-users semantics before member processing');
|
||
assert.match(gppLocalUsersGroups, /sequence: members\.length/, 'Local group membership operations must retain source order');
|
||
assert.match(gppLocalUsersGroups, /passwordAttributePresent:/, 'Local user preferences may retain only a password-presence indicator');
|
||
assert.doesNotMatch(gppLocalUsersGroups, /decrypt|decipher|createDecipher/i, 'Local Users and Groups parsing must never decrypt cpassword');
|
||
assert.match(gppLocalUsersGroups, /effectiveState: 'unknown'/, 'Local Users and Groups observations must not claim effective application');
|
||
assert.doesNotMatch(gppLocalUsersGroups, /FindingCandidate/, 'Local Users and Groups parsing must remain outside the scanner candidate path');
|
||
const gppServices = read(join('server', 'gpo', 'gpp-services.ts'));
|
||
assert.match(gppParser, /rootName === 'ntservices'/, 'Services.xml must cross the shared safe GPP XML boundary');
|
||
assert.match(gppServices, /operation: 'configure'/, 'Services must not be forced into Registry C-D-R-U action semantics');
|
||
assert.match(gppServices, /serviceAction/, 'Services must retain runtime service actions separately from preference processing');
|
||
assert.match(gppServices, /startupType/, 'Services must retain startup configuration separately from runtime actions');
|
||
assert.match(gppServices, /passwordAttributePresent:/, 'Service preferences may retain only a password-presence indicator');
|
||
assert.doesNotMatch(gppServices, /decrypt|decipher|createDecipher/i, 'Services parsing must never decrypt cPassword');
|
||
assert.match(gppServices, /document.scope !== 'computer'/, 'Services observations must remain computer-scoped');
|
||
assert.match(gppServices, /effectiveState: 'unknown'/, 'Services observations must not claim effective application');
|
||
assert.doesNotMatch(gppServices, /FindingCandidate/, 'Services parsing must remain outside the scanner candidate path');
|
||
const gppDriveMaps = read(join('server', 'gpo', 'gpp-drive-maps.ts'));
|
||
assert.match(gppParser, /rootName === 'drives'/, 'Drives.xml must cross the shared safe GPP XML boundary');
|
||
assert.match(gppDriveMaps, /delete-range/, 'Drive Maps must retain ranged delete semantics when useLetter is false');
|
||
assert.match(gppDriveMaps, /first-available/, 'Drive Maps must retain first-available create semantics when useLetter is false');
|
||
assert.match(gppDriveMaps, /first-mapped-in-range/, 'Drive Maps must retain range lookup for replace and update');
|
||
assert.match(gppDriveMaps, /thisDrive/, 'Drive Maps must retain per-drive visibility separately from global visibility');
|
||
assert.match(gppDriveMaps, /passwordAttributePresent:/, 'Drive Maps may retain only a credential-presence indicator');
|
||
assert.doesNotMatch(gppDriveMaps, /decrypt|decipher|createDecipher/i, 'Drive Maps parsing must never decrypt cpassword');
|
||
assert.match(gppDriveMaps, /document.scope !== 'user'/, 'Drive Maps observations must remain user-scoped');
|
||
assert.match(gppDriveMaps, /effectiveState: 'unknown'/, 'Drive Maps observations must not claim effective application');
|
||
assert.doesNotMatch(gppDriveMaps, /FindingCandidate/, 'Drive Maps parsing must remain outside the scanner candidate path');
|
||
assert.match(gpoMetadataParser, /maximumMarkupElements/, 'GPO metadata parsing must enforce an element limit');
|
||
assert.match(gpoMetadataParser, /gpo-metadata-external-reference-rejected/, 'GPO metadata parsing must reject external-reference mechanisms');
|
||
assert.match(gpoMetadataParser, /includeTemplateNames = false/, 'Template labels must require explicit baseline-analyzer opt-in');
|
||
assert.doesNotMatch(gpoMetadataParser.match(/const gpoId = [\s\S]*?const domainKey =/)?.[0] ?? '', /templatename|displayname/i,
|
||
'GPO and backup identities must not depend on display names');
|
||
assert.doesNotMatch(gpoMetadataParser, /JSDOM/, 'GPO metadata parsing must not allocate a report DOM');
|
||
assert.match(gpoMetadataParser, /new SaxesParser/, 'GPO metadata must use the bounded streaming XML parser');
|
||
assert.match(gpoMetadataParser, /stack\.length >= 128/, 'GPO metadata must bound XML nesting');
|
||
assert.match(gpoMetadataParser, /slice\(0, 2049\)/, 'GPO metadata must bound retained identity text');
|
||
const gpoBackupIngestion = read(join('server', 'gpo', 'gpo-backup-ingestion.ts'));
|
||
assert.match(gpoBackupIngestion, /evidence: 'backend-receipt'/, 'GPO collection evidence must come from the backend receipt');
|
||
assert.match(gpoBackupIngestion, /scope: 'selected-gpos'/, 'A selected tenant GPO backup folder must remain a selected subset without completeness evidence');
|
||
assert.match(gpoBackupIngestion, /evidence: 'user-selection'/, 'All GPO roots from one selected folder must retain their common selection evidence');
|
||
const gpoSnapshotBuilder = read(join('src', 'gpo', 'snapshot-builder.ts'));
|
||
assert.match(gpoSnapshotBuilder, /permitsSetAbsenceConclusions: status === 'consistent' && completeness === 'complete'/, 'Selected GPO snapshots must never permit set-absence conclusions');
|
||
const gpoSubmissionService = read(join('server', 'gpo', 'gpo-backup-submission-service.ts'));
|
||
assert.match(gpoSubmissionService, /sensitivity: 'secret'/, 'GPO originals must default to secret sensitivity');
|
||
assert.match(gpoSubmissionService, /analyze-gpo-backup/, 'GPO submissions must enqueue the dedicated analysis job kind');
|
||
assert.match(gpoSubmissionService, /getProject\(request\.projectId\)/, 'GPO submissions must verify the project before retaining bytes');
|
||
assert.match(gpoSubmissionService, /saveImportArtifactAndEnqueueJob/, 'GPO artifact metadata and analysis job must cross one atomic repository operation');
|
||
assert.match(gpoSubmissionService, /artifactStore\.remove/, 'failed or concurrent GPO submissions must remove their newly created unreferenced object');
|
||
const gpoReconciler = read(join('server', 'gpo', 'gpo-artifact-reconciler.ts'));
|
||
assert.match(gpoReconciler, /gracePeriodMs/, 'orphan reconciliation must use an explicit grace period');
|
||
assert.match(gpoReconciler, /saveImportArtifactAndEnqueueJob/, 'orphan reconciliation must repair referenced GPO artifacts without jobs atomically');
|
||
assert.match(gpoReconciler, /artifactStore\.reconcile/, 'orphan reconciliation must compare database references with private storage');
|
||
assert.match(gpoReconciler, /invalidStorageReferences === 0 && referencedStorageKeys\.length > 0/, 'orphan reconciliation must never remove regular objects while a persisted storage reference is invalid or the trusted reference set is empty');
|
||
assert.match(gpoReconciler, /listArtifactDeletionRequests/, 'artifact reconciliation must resume pending deletion outbox requests');
|
||
const gpoManagement = read(join('server', 'gpo', 'gpo-backup-management-service.ts'));
|
||
assert.match(gpoManagement, /retryIngestionJob/, 'failed GPO analysis must be retryable without another upload');
|
||
assert.match(gpoManagement, /deleteGpoArtifact/, 'GPO deletion must atomically create a durable deletion request');
|
||
assert.match(gpoManagement, /artifactStore\.remove/, 'failed GPO deletion must remove the backend-controlled copy');
|
||
const gpoJobProcessor = read(join('server', 'gpo', 'gpo-backup-job-processor.ts'));
|
||
assert.match(gpoJobProcessor, /kinds: \['analyze-gpo-backup'\]/, 'the GPO worker must claim only its dedicated job kind');
|
||
assert.match(gpoJobProcessor, /artifactStore\.read/, 'the GPO worker must re-read the retained backend artifact through the integrity-checking store');
|
||
assert.match(gpoJobProcessor, /kind: 'artifact-parser'/, 'GPO processing history must identify the artifact parser boundary');
|
||
const gpoAnalysisSave = gpoJobProcessor.indexOf('saveGpoBackupAnalysis');
|
||
const gpoJobCompletion = gpoJobProcessor.indexOf('completeIngestionJob', gpoAnalysisSave);
|
||
assert.ok(gpoAnalysisSave >= 0 && gpoJobCompletion > gpoAnalysisSave, 'GPO analysis must be persisted before its job is completed');
|
||
assert.match(serverEntry, /gpoJobRunner\.start\(\)/, 'the server must start the GPO job runner');
|
||
assert.match(serverEntry, /gpoJobRunner\.stop\(\)/, 'graceful shutdown must drain the GPO job runner');
|
||
assert.match(httpServer, /gpoBackupSubmissionService\.submit/, 'GPO uploads must cross the project-bound submission service');
|
||
assert.match(httpServer, /listProjectGpoBackupStatus/, 'GPO processing status must be reloaded through a project-bound read endpoint');
|
||
assert.match(httpServer, /confirmDelete !== true/, 'GPO artifact deletion must require exact confirmation');
|
||
assert.match(httpServer, /gpoBackupManagementService\.retry/, 'GPO retry must cross the project-bound management service');
|
||
assert.match(httpServer, /gpoBackupManagementService\.delete/, 'GPO deletion must cross the project-bound management service');
|
||
assert.match(serverEntry, /gpoArtifactReconciliationRunner\.start\(\)/, 'the server must start the artifact reconciliation runner');
|
||
assert.match(serverEntry, /gpoArtifactReconciliationRunner\.stop\(\)/, 'graceful shutdown must drain artifact reconciliation');
|
||
const gpoStatusUi = read(join('src', 'features', 'project-import', 'GpoFolderImport.tsx'));
|
||
assert.match(gpoStatusUi, /listProjectGpoBackupStatus/, 'the tenant view must reload persistent GPO processing status');
|
||
assert.match(gpoStatusUi, /setInterval/, 'the visible tenant view must refresh GPO status while processing continues');
|
||
assert.match(gpoStatusUi, /Speichergrenze des isolierten Analyse-Workers überschritten/, 'the tenant view must explain the GPO memory error instead of showing only its technical code');
|
||
assert.match(serverEntry, /ASSESSOR_ARTIFACT_ROOT/, 'the server must expose an explicit artifact-root configuration');
|
||
assert.match(serverEntry, /assertArtifactStoreOutsidePublicRoot/, 'server startup must enforce artifact/webroot separation');
|
||
assert.doesNotMatch(httpServer, /storageKey.*sendJson/, 'GPO HTTP responses must not expose backend storage keys');
|
||
const serverBuild = read('vite.server.config.ts');
|
||
assert.match(serverBuild, /import-worker/, 'production server build must emit the isolated import worker');
|
||
assert.match(serverBuild, /gpo-inventory-worker/, 'production server build must emit the isolated GPO inventory worker');
|
||
const htmlExtraction = read(join('src', 'importers', 'html-extraction.ts'));
|
||
assert.match(htmlExtraction, /sanitizeHtmlDocument/, 'shared HTML extraction must sanitize imported HTML');
|
||
assert.match(htmlExtraction, /bounded-lexical/, 'large HTML reports must retain a stack-safe extraction path');
|
||
assert.doesNotMatch(htmlExtraction, /\.textContent/, 'HTML extraction must not recursively request whole-subtree textContent');
|
||
for (const adapterPath of [join('server', 'node-import-runtime-adapters.ts'), join('src', 'platform', 'browser', 'import-runtime-adapters.ts')]) {
|
||
const adapter = read(adapterPath);
|
||
assert.match(adapter, /decodeHtmlLexically/, `${adapterPath} must retain the bounded HTML fallback`);
|
||
assert.match(adapter, /<!DOCTYPE/, `${adapterPath} must reject XML document types independently`);
|
||
}
|
||
for (const relativePath of sourceFiles(join('src', 'importers'))) {
|
||
assert.doesNotMatch(read(relativePath), /\b(?:mkdtemp|tmpdir|createWriteStream|writeFile)\b/, `${relativePath} must not create unmanaged temporary files`);
|
||
}
|
||
const formatAdapterPaths = [
|
||
join('src', 'importers', 'csv-importer.ts'),
|
||
join('src', 'importers', 'excel-importer.ts'),
|
||
join('src', 'importers', 'html-importer.ts'),
|
||
join('src', 'importers', 'json-importer.ts'),
|
||
join('src', 'importers', 'xml-importer.ts')
|
||
];
|
||
for (const relativePath of formatAdapterPaths) {
|
||
assert.doesNotMatch(
|
||
read(relativePath),
|
||
/purple[ -]?knight|pingcastle|cis[ -]?cat|maester/i,
|
||
`${relativePath} must not contain source-tool knowledge`
|
||
);
|
||
}
|
||
const directParserImport = /from\s+['"][^'"]*\/importers\/(?:csv-importer|excel-importer|html-importer|json-importer|xml-importer|intermediate|source-extractors|candidate-mapping|canonical-candidate-projection)['"]/;
|
||
for (const directory of ['src/app', 'src/components', 'src/features']) {
|
||
for (const relativePath of sourceFiles(directory).filter(file => file.endsWith('.tsx'))) {
|
||
assert.doesNotMatch(read(relativePath), directParserImport, `${relativePath} must not contain parser or mapping logic`);
|
||
}
|
||
}console.log('Local-only security checks passed.');
|