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

374 lines
16 KiB
TypeScript

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 { EnvironmentSnapshot, EnvironmentSnapshotQuery } from '../domain/environment-context';
import type { ImportedDataset } from '../domain/dataset';
import type { MicrosoftSecurityBaselineSelectionContext } from '../domain/microsoft-security-baseline';
import type { ImportBatchPreview, ImportBatchSelection } from '../domain/import-workflow';
import {
ASSESSOR_API_PREFIX,
ASSESSOR_MUTATION_HEADER,
type AssessorApiErrorBody,
type AssessorHealthResponse,
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 CveEnrichmentResponse
} 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> {
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;
}
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;
constructor(options: AssessorApiClientOptions = {}) {
this.baseUrl = (options.baseUrl ?? ASSESSOR_API_PREFIX).replace(/\/$/, '');
this.fetcher = options.fetcher ?? globalThis.fetch.bind(globalThis);
}
private async request<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');
}
getDatabaseStructure(): Promise<DbStructureResponse> {
return this.request('/db-structure');
}
listProjects(): Promise<AssessmentProject[]> {
return this.request('/projects');
}
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`);
}
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 }) });
}
queryUnifiedResults(query: UnifiedResultQuery = {}): Promise<UnifiedResultQueryResult> {
return this.request(unifiedQueryPath('/results', query));
}
queryUnifiedResultFacets(query: UnifiedResultQuery = {}): Promise<UnifiedResultFacetResult> {
return this.request(unifiedQueryPath('/results/facets', query));
}
getUnifiedResultById(resultProjectionId: string): Promise<UnifiedResultProjection> {
return this.request(`/results/${encodeURIComponent(resultProjectionId)}`);
}
getCveEnrichment(cveId: string): Promise<CveEnrichmentResponse> {
return this.request(`/cves/${encodeURIComponent(cveId)}`);
}
enrichCve(cveId: string): Promise<CveEnrichmentResponse> {
return this.request(`/cves/${encodeURIComponent(cveId)}/enrich`, {
method: 'POST', body: JSON.stringify({})
});
}
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}`);
}
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): Promise<GpoBackupSubmissionResponse> {
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 })
});
}
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> {
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 }) });
}
}