374 lines
16 KiB
TypeScript
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 }) });
|
|
}
|
|
}
|