Assessor_2.0/src/api/assessor-api-client.ts

495 lines
23 KiB
TypeScript

import { trackActivity } from '../platform/activity-store';
import { requestActivity } from './request-activity';
import type { RiskAcceptanceCommand } from '../domain/risk-acceptance';
import type { Assessment, AssessmentProject, CanonicalAssessmentModel, FindingReview, ImportFileRecord } from '../domain/canonical-model';
import type {
UnifiedResultCategory,
UnifiedResultFacetResult,
UnifiedResultProjection,
UnifiedResultQuery,
UnifiedResultQueryResult
} from '../domain/unified-result';
import type { ProjectDataExport } from '../persistence/assessor-repository';
import type { DatahubTimeslotResultMetrics } from '../features/project-import/datahub-scan-analysis';
import type { EnvironmentSnapshot, EnvironmentSnapshotQuery } from '../domain/environment-context';
import type { ClientInformationQuery, ClientInformationResult } from '../domain/client-information';
import type { ImportedDataset } from '../domain/dataset';
import type { MicrosoftSecurityBaselineSelectionContext } from '../domain/microsoft-security-baseline';
import type { ImportBatchPreview, ImportBatchSelection } from '../domain/import-workflow';
import type { ResultDbMaterializationStatus } from '../domain/result-db-materialization';
import {
ASSESSOR_API_PREFIX,
ASSESSOR_MUTATION_HEADER,
type AssessorApiErrorBody,
type AssessorHealthResponse,
type MitreCatalogResponse,
type GpoBackupSubmissionResponse,
type GpoBackupStatusResponse,
type GpoBackupRetryResponse,
type GpoBackupDeleteResponse,
type GpoBackupReprocessResponse,
type GpoPolicyAnalysisListResponse,
type EnvironmentProfileCreateRequest,
type EnvironmentProfileResponse,
type EnvironmentProfileListResponse,
type PolicySnapshotAggregationCreateRequest,
type PolicySnapshotAggregationResponse,
type PolicySnapshotAggregationListResponse,
type PolicyEvaluationCreateRequest,
type PolicyEvaluationResponse,
type PolicyEvaluationListResponse,
type PolicyFindingRunCreateRequest,
type PolicyFindingRunResponse,
type PolicyFindingRunListResponse,
type PolicyEvaluationRuleCatalogResponse,
type PolicyAnalysisOverviewResponse,
type ProjectAssessmentHistoryResponse,
type ImportBatchCompleteResponse,
type ImportSourceParseRequest,
type DbStructureResponse,
type MicrosoftSecurityBaselineListResponse,
type MicrosoftSecurityBaselineSubmissionResponse,
type MicrosoftSecurityBaselineDeletionResponse,
type MicrosoftSecurityBaselineSelectionResponse,
type MicrosoftSecurityBaselineAnalysisResponse,
type MicrosoftSecurityBaselineAnalysisListResponse,
type AiOverviewCreateRequest,
type AiOverviewResponse,
type RegulatoryMappingCreateRequest,
type RegulatoryMappingDecisionCreateRequest,
type RegulatoryMappingResponse
} from './assessor-api-contract';
export class AssessorApiError extends Error {
constructor(
message: string,
readonly status: number,
readonly code: string,
readonly requestId?: string
) {
super(message);
this.name = 'AssessorApiError';
}
}
function bytesToBase64(bytes: Uint8Array): string {
const chunkSize = 0x8000;
let binary = '';
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
}
return btoa(binary);
}
async function filePayload(file: File, delimiter?: string): Promise<ImportSourceParseRequest> {
return trackActivity('Core Engine', 'Reading and preparing source file', async () => {
const bytes = new Uint8Array(await file.arrayBuffer());
return {
fileName: file.name,
sizeBytes: file.size,
lastModified: file.lastModified,
mediaType: file.type || undefined,
bytesBase64: bytesToBase64(bytes),
delimiter
};
});
}
export interface AssessorApiClientOptions {
baseUrl?: string;
fetcher?: typeof fetch;
/** Future engines can supply their own public operation names without a Core import of that engine. */
describeActivity?: typeof requestActivity;
}
function unifiedQueryPath(path: string, query: UnifiedResultQuery = {}): string {
const parameters = new URLSearchParams();
Object.entries(query).forEach(([key, value]) => {
if (value === undefined) return;
if (Array.isArray(value)) value.forEach(item => parameters.append(key, String(item)));
else parameters.set(key, String(value));
});
const encoded = parameters.toString();
return encoded ? `${path}?${encoded}` : path;
}
export class AssessorApiClient {
private readonly baseUrl: string;
private readonly fetcher: typeof fetch;
private readonly describeActivity: typeof requestActivity;
constructor(options: AssessorApiClientOptions = {}) {
this.baseUrl = (options.baseUrl ?? ASSESSOR_API_PREFIX).replace(/\/$/, '');
this.fetcher = options.fetcher ?? globalThis.fetch.bind(globalThis);
this.describeActivity = options.describeActivity ?? requestActivity;
}
private async request<T>(path: string, init: RequestInit = {}): Promise<T> {
const descriptor = this.describeActivity(path, init.method);
const run = () => this.performRequest<T>(path, init);
return descriptor ? trackActivity(descriptor.area, descriptor.label, run, `${this.baseUrl} ${init.method ?? 'GET'} ${path}`) : run();
}
private async performRequest<T>(path: string, init: RequestInit): Promise<T> {
const headers = new Headers(init.headers);
headers.set('Accept', 'application/json');
if (init.body !== undefined) headers.set('Content-Type', 'application/json');
if (init.method && init.method !== 'GET') headers.set(ASSESSOR_MUTATION_HEADER, '1');
const response = await this.fetcher(`${this.baseUrl}${path}`, { ...init, headers });
const body = await response.json().catch(() => null) as T | AssessorApiErrorBody | null;
if (!response.ok) {
const apiError = body && typeof body === 'object' && 'error' in body ? body.error : undefined;
throw new AssessorApiError(
apiError?.message ?? `API-Anfrage fehlgeschlagen (${response.status}).`,
response.status,
apiError?.code ?? 'api_error',
apiError?.requestId
);
}
return body as T;
}
health(): Promise<AssessorHealthResponse> {
return this.request('/health');
}
getReferenceRecognitionStatus(projectId: string): Promise<import('./assessor-api-contract').ReferenceRecognitionStatus> {
return this.request(`/projects/${encodeURIComponent(projectId)}/references`);
}
recognizeReferences(projectId: string): Promise<import('./assessor-api-contract').ReferenceRecognitionResponse> {
return this.request(`/projects/${encodeURIComponent(projectId)}/references/recognize`, { method: 'POST', body: '{}' });
}
getMitreCatalog(): Promise<MitreCatalogResponse> {
return this.request('/reference-data/mitre-attack');
}
refreshMitreCatalog(): Promise<MitreCatalogResponse & { updated: boolean }> {
return this.request('/reference-data/mitre-attack/refresh', { method: 'POST', body: '{}' });
}
async downloadLatestMitreSource(): Promise<{ blob: Blob; fileName: string }> {
return trackActivity('Core Engine', 'Downloading MITRE source', async () => {
const response = await this.fetcher(`${this.baseUrl}/reference-data/mitre-attack/source`, {
headers: { Accept: 'application/json' }
});
if (!response.ok) {
const body = await response.json().catch(() => null) as AssessorApiErrorBody | null;
throw new AssessorApiError(body?.error?.message ?? `MITRE-Download fehlgeschlagen (${response.status}).`,
response.status, body?.error?.code ?? 'mitre_download_failed', body?.error?.requestId);
}
const fileName = response.headers.get('content-disposition')?.match(/filename="(enterprise-attack-v\d+\.\d+\.json)"/)?.[1]
?? 'enterprise-attack.json';
return { blob: await response.blob(), fileName };
});
}
getDatabaseStructure(): Promise<DbStructureResponse> {
return this.request('/db-structure');
}
listProjects(): Promise<AssessmentProject[]> {
return this.request('/projects');
}
getWorkPackages(projectId: string, assessmentIds: string[], scanRunIds: string[] = []): Promise<import('./assessor-api-contract').WorkPackagesResponse> {
const query = new URLSearchParams();
assessmentIds.forEach(id => query.append('assessmentIds', id));
scanRunIds.forEach(id => query.append('scanRunIds', id));
return this.request(`/projects/${encodeURIComponent(projectId)}/work-packages?${query.toString()}`);
}
saveProject(project: AssessmentProject): Promise<{ project: AssessmentProject; repositoryRevision: number }> {
return this.request('/projects', { method: 'POST', body: JSON.stringify(project) });
}
getProject(projectId: string): Promise<AssessmentProject> {
return this.request(`/projects/${encodeURIComponent(projectId)}`);
}
exportProject(projectId: string): Promise<ProjectDataExport> {
return this.request(`/projects/${encodeURIComponent(projectId)}/export`);
}
/** Context layer of the selected timeslot; read-only and only requested while the layer is switched on. */
getAssessmentContext(projectId: string, assessmentIds: string[], scanRunIds: string[] = []): Promise<import('./assessor-api-contract').AssessmentContextResponse> {
const query = new URLSearchParams();
assessmentIds.forEach(id => query.append('assessmentIds', id));
scanRunIds.forEach(id => query.append('scanRunIds', id));
return this.request(`/projects/${encodeURIComponent(projectId)}/assessment-context?${query.toString()}`);
}
/** Runs the Report Engine: the server archives both files and returns the requested one. */
createReportRun(projectId: string, request: import('./assessor-api-contract').ReportRunRequest): Promise<import('./assessor-api-contract').ReportRunResponse> {
return this.request(`/projects/${encodeURIComponent(projectId)}/report-runs`, { method: 'POST', body: JSON.stringify(request) });
}
getAiOverview(projectId: string, assessmentIds: string[] = [], scanRunIds: string[] = []): Promise<AiOverviewResponse> {
const parameters = new URLSearchParams();
assessmentIds.forEach(id => parameters.append('assessmentIds', id));
scanRunIds.forEach(id => parameters.append('scanRunIds', id));
const suffix = parameters.size ? `?${parameters.toString()}` : '';
return this.request(`/projects/${encodeURIComponent(projectId)}/ai-overview${suffix}`);
}
generateAiOverview(projectId: string, request: AiOverviewCreateRequest): Promise<AiOverviewResponse> {
return this.request(`/projects/${encodeURIComponent(projectId)}/ai-overview`, {
method: 'POST', body: JSON.stringify(request)
});
}
getRegulatoryMappings(projectId: string, resultProjectionId: string): Promise<RegulatoryMappingResponse> {
return this.request(`/projects/${encodeURIComponent(projectId)}/regulatory-mappings/${encodeURIComponent(resultProjectionId)}`);
}
generateRegulatoryMappings(projectId: string, request: RegulatoryMappingCreateRequest): Promise<RegulatoryMappingResponse> {
return this.request(`/projects/${encodeURIComponent(projectId)}/regulatory-mapping-runs`, {
method: 'POST', body: JSON.stringify(request)
});
}
saveRegulatoryMappingDecision(projectId: string, request: RegulatoryMappingDecisionCreateRequest): Promise<RegulatoryMappingResponse> {
return this.request(`/projects/${encodeURIComponent(projectId)}/regulatory-mapping-decisions`, {
method: 'POST', body: JSON.stringify(request)
});
}
listProjectImportFiles(projectId: string): Promise<ImportFileRecord[]> {
return this.request(`/projects/${encodeURIComponent(projectId)}/import-files`);
}
listProjectAssessmentHistory(projectId: string): Promise<ProjectAssessmentHistoryResponse> {
return this.request(`/projects/${encodeURIComponent(projectId)}/assessment-history`);
}
saveAssessment(assessment: Assessment): Promise<{ assessment: Assessment; repositoryRevision: number }> {
return this.request('/assessments', { method: 'POST', body: JSON.stringify(assessment) });
}
listProjectAssessments(projectId: string): Promise<Assessment[]> {
return this.request(`/projects/${encodeURIComponent(projectId)}/assessments`);
}
loadAssessmentModel(assessmentId: string): Promise<CanonicalAssessmentModel> {
return this.request(`/assessments/${encodeURIComponent(assessmentId)}/model`);
}
listProjectReviews(projectId: string): Promise<FindingReview[]> {
return this.request(`/projects/${encodeURIComponent(projectId)}/reviews`);
}
saveReview(review: FindingReview, expectedRevision?: number): Promise<{ review: FindingReview; repositoryRevision: number }> {
return this.request('/reviews', { method: 'POST', body: JSON.stringify({ review, expectedRevision }) });
}
recordRiskAcceptance(projectId: string, occurrenceId: string, command: RiskAcceptanceCommand, expectedRevision: number): Promise<{ review: FindingReview; repositoryRevision: number }> {
return this.request(`/projects/${encodeURIComponent(projectId)}/risk-acceptances`, { method: 'POST', body: JSON.stringify({ occurrenceId, command, expectedRevision }) });
}
queryUnifiedResults(query: UnifiedResultQuery = {}): Promise<UnifiedResultQueryResult> {
return this.request(unifiedQueryPath('/results', query));
}
queryUnifiedResultFacets(query: UnifiedResultQuery = {}): Promise<UnifiedResultFacetResult> {
return this.request(unifiedQueryPath('/results/facets', query));
}
getResultDbStatus(): Promise<ResultDbMaterializationStatus> {
return this.request('/results/status');
}
getTimeslotResultMetrics(projectId: string, scanRunIds: string[], assessmentIds: string[]): Promise<DatahubTimeslotResultMetrics | null> {
return this.request(unifiedQueryPath(`/projects/${encodeURIComponent(projectId)}/timeslot-result-metrics`,
{ scanRunIds, assessmentIds }));
}
getUnifiedResultById(resultProjectionId: string): Promise<UnifiedResultProjection> {
return this.request(`/results/${encodeURIComponent(resultProjectionId)}`);
}
listResultCategories(query: UnifiedResultQuery = {}): Promise<UnifiedResultCategory[]> {
return this.request(unifiedQueryPath('/results/categories', query));
}
deleteProject(projectId: string): Promise<{ deleted: boolean; repositoryRevision: number }> {
return this.request(`/projects/${encodeURIComponent(projectId)}`, {
method: 'DELETE', body: JSON.stringify({ projectId, cascade: true })
});
}
getEnvironmentSnapshot(query: EnvironmentSnapshotQuery): Promise<EnvironmentSnapshot> {
const parameters = new URLSearchParams();
if (query.assessmentId) parameters.set('assessmentId', query.assessmentId);
query.scanRunIds?.forEach(id => parameters.append('scanRunIds', id));
const suffix = parameters.size ? `?${parameters.toString()}` : '';
return this.request(`/projects/${encodeURIComponent(query.projectId)}/environment-snapshot${suffix}`);
}
queryClientInformation(query: ClientInformationQuery): Promise<ClientInformationResult> {
const parameters = new URLSearchParams();
for (const [key, value] of Object.entries(query)) {
if (key === 'projectId' || value === undefined) continue;
if (Array.isArray(value)) value.forEach(item => parameters.append(key, String(item)));
else parameters.set(key, String(value));
}
return this.request(`/projects/${encodeURIComponent(query.projectId)}/client-information${parameters.size ? `?${parameters}` : ''}`);
}
deleteAssessment(projectId: string, assessmentId: string): Promise<{ deleted: boolean; repositoryRevision: number }> {
return this.request(`/projects/${encodeURIComponent(projectId)}/assessments/${encodeURIComponent(assessmentId)}`, {
method: 'DELETE', body: JSON.stringify({ projectId, assessmentId, cascade: true })
});
}
deleteScanRun(projectId: string, scanRunId: string): Promise<{ deleted: boolean; repositoryRevision: number }> {
return this.request(`/projects/${encodeURIComponent(projectId)}/scan-runs/${encodeURIComponent(scanRunId)}`, {
method: 'DELETE', body: JSON.stringify({ projectId, scanRunId, cascade: true })
});
}
evaluateGpoBackup(projectId: string, artifactId: string): Promise<{ state: 'context-required' | 'baseline-required' | 'review' | 'evaluated' }> {
return this.request(`/projects/${encodeURIComponent(projectId)}/gpo-backups/${encodeURIComponent(artifactId)}/evaluate`, {
method: 'POST', body: JSON.stringify({})
});
}
async submitGpoBackup(projectId: string, file: File, assessmentId?: string, restoreArtifactId?: string): Promise<GpoBackupSubmissionResponse> {
return trackActivity('Core Engine', 'Preparing and uploading GPO backup', async () => {
const bytes = new Uint8Array(await file.arrayBuffer());
return this.request(`/projects/${encodeURIComponent(projectId)}/gpo-backups`, {
method: 'POST',
body: JSON.stringify({ fileName: file.name, sizeBytes: file.size, bytesBase64: bytesToBase64(bytes), assessmentId, restoreArtifactId })
});
});
}
listProjectGpoBackupStatus(projectId: string): Promise<GpoBackupStatusResponse> {
return this.request(`/projects/${encodeURIComponent(projectId)}/gpo-backups/status`);
}
retryGpoBackup(projectId: string, artifactId: string): Promise<GpoBackupRetryResponse> {
return this.request(`/projects/${encodeURIComponent(projectId)}/gpo-backups/${encodeURIComponent(artifactId)}/retry`, {
method: 'POST', body: JSON.stringify({})
});
}
reprocessGpoBackup(projectId: string, artifactId: string, expectedLatestProcessingRunId: string): Promise<GpoBackupReprocessResponse> {
return this.request(`/projects/${encodeURIComponent(projectId)}/gpo-backups/${encodeURIComponent(artifactId)}/reprocess`, {
method: 'POST', body: JSON.stringify({ expectedLatestProcessingRunId })
});
}
listProjectGpoPolicyAnalyses(projectId: string): Promise<GpoPolicyAnalysisListResponse> {
return this.request(`/projects/${encodeURIComponent(projectId)}/policy-analyses`);
}
listEnvironmentProfiles(projectId: string): Promise<EnvironmentProfileListResponse> {
return this.request('/projects/' + encodeURIComponent(projectId) + '/environment-profiles');
}
createEnvironmentProfile(
projectId: string,
request: EnvironmentProfileCreateRequest
): Promise<EnvironmentProfileResponse> {
return this.request('/projects/' + encodeURIComponent(projectId) + '/environment-profiles', {
method: 'POST', body: JSON.stringify(request)
});
}
listPolicySnapshotAggregations(projectId: string): Promise<PolicySnapshotAggregationListResponse> {
return this.request('/projects/' + encodeURIComponent(projectId) + '/policy-snapshot-aggregations');
}
aggregatePolicySnapshot(
projectId: string,
request: PolicySnapshotAggregationCreateRequest
): Promise<PolicySnapshotAggregationResponse> {
return this.request('/projects/' + encodeURIComponent(projectId) + '/policy-snapshot-aggregations', {
method: 'POST', body: JSON.stringify(request)
});
}
listPolicyEvaluations(projectId: string): Promise<PolicyEvaluationListResponse> {
return this.request('/projects/' + encodeURIComponent(projectId) + '/policy-evaluations');
}
evaluatePolicy(
projectId: string,
request: PolicyEvaluationCreateRequest
): Promise<PolicyEvaluationResponse> {
return this.request('/projects/' + encodeURIComponent(projectId) + '/policy-evaluations', {
method: 'POST', body: JSON.stringify(request)
});
}
listPolicyFindingRuns(projectId: string): Promise<PolicyFindingRunListResponse> {
return this.request('/projects/' + encodeURIComponent(projectId) + '/policy-finding-runs');
}
createPolicyFindingRun(
projectId: string,
request: PolicyFindingRunCreateRequest
): Promise<PolicyFindingRunResponse> {
return this.request('/projects/' + encodeURIComponent(projectId) + '/policy-finding-runs', {
method: 'POST', body: JSON.stringify(request)
});
}
getPolicyAnalysisOverview(projectId: string): Promise<PolicyAnalysisOverviewResponse> {
return this.request('/projects/' + encodeURIComponent(projectId) + '/policy-analysis-overview');
}
deleteGpoBackup(projectId: string, artifactId: string): Promise<GpoBackupDeleteResponse> {
return this.request(`/projects/${encodeURIComponent(projectId)}/gpo-backups/${encodeURIComponent(artifactId)}`, {
method: 'DELETE', body: JSON.stringify({ artifactId, confirmDelete: true })
});
}
getPolicyEvaluationRuleCatalog(): Promise<PolicyEvaluationRuleCatalogResponse> {
return this.request('/reference-data/policy-evaluation-rule-catalog');
}
listMicrosoftSecurityBaselines(): Promise<MicrosoftSecurityBaselineListResponse> {
return this.request('/reference-data/microsoft-security-baselines');
}
async submitMicrosoftSecurityBaseline(file: File): Promise<MicrosoftSecurityBaselineSubmissionResponse> {
return trackActivity('Core Engine', 'Preparing and uploading security baseline', async () => {
const bytes = new Uint8Array(await file.arrayBuffer());
return this.request('/reference-data/microsoft-security-baselines', { method: 'POST', body: JSON.stringify({ fileName: file.name, sizeBytes: file.size, bytesBase64: bytesToBase64(bytes) }) });
});
}
adoptScanRunReplacement(projectId: string, replacedScanRunId: string, replacementScanRunId: string): Promise<{ repositoryRevision: number; reimportReviewTransfer: import('../domain/reimport-review-transfer').ReimportReviewTransferSummary }> {
return this.request(`/projects/${encodeURIComponent(projectId)}/scan-runs/${encodeURIComponent(replacedScanRunId)}/adopt-replacement`, {
method: 'POST', body: JSON.stringify({ replacementScanRunId })
});
}
deleteMicrosoftSecurityBaseline(baselineId: string): Promise<MicrosoftSecurityBaselineDeletionResponse> {
return this.request(`/reference-data/microsoft-security-baselines/${encodeURIComponent(baselineId)}`, {
method: 'DELETE', body: JSON.stringify({ confirmDelete: true })
});
}
listMicrosoftSecurityBaselineAnalyses(baselineId: string): Promise<MicrosoftSecurityBaselineAnalysisListResponse> {
return this.request(`/reference-data/microsoft-security-baselines/${encodeURIComponent(baselineId)}/analysis`);
}
reprocessMicrosoftSecurityBaseline(baselineId: string): Promise<MicrosoftSecurityBaselineAnalysisResponse> {
return this.request(`/reference-data/microsoft-security-baselines/${encodeURIComponent(baselineId)}/reprocess`, {
method: 'POST', body: JSON.stringify({})
});
}
selectMicrosoftSecurityBaseline(context: MicrosoftSecurityBaselineSelectionContext): Promise<MicrosoftSecurityBaselineSelectionResponse> {
return this.request('/reference-data/microsoft-security-baselines/select', { method: 'POST', body: JSON.stringify(context) });
}
async importFile(file: File, delimiter?: string): Promise<ImportedDataset> {
return this.request('/import-sources/parse', { method: 'POST', body: JSON.stringify(await filePayload(file, delimiter)) });
}
async previewImportBatch(files: File[], delimiter?: string, targetProjectId?: string, targetAssessmentId?: string, replacesScanRunId?: string): Promise<ImportBatchPreview> {
const payloads = await Promise.all(files.map(file => filePayload(file, delimiter)));
return this.request('/import-batches/preview', { method: 'POST', body: JSON.stringify({ files: payloads, targetProjectId, targetAssessmentId, replacesScanRunId }) });
}
completeImportBatch(batchId: string, entries: ImportBatchSelection[], targetProjectId?: string, targetAssessmentId?: string, replacesScanRunId?: string): Promise<ImportBatchCompleteResponse> {
return this.request('/import-batches/complete', { method: 'POST', body: JSON.stringify({ batchId, entries, targetProjectId, targetAssessmentId, replacesScanRunId }) });
}
}