Assessor_2.0/src/features/project-import/MicrosoftBaselineLibrary.tsx

93 lines
5.9 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
import { AssessorApiClient } from '../../api/assessor-api-client';
import { Dialog } from '../../components/Dialog/Dialog';
import { OperationProgressDialog } from '../../components/OperationProgress/OperationProgress';
import { TriangleIcon } from '../../components/TriangleIcon';
import type { MicrosoftSecurityBaselineRecord } from '../../domain/microsoft-security-baseline';
const assessorApi = new AssessorApiClient();
function formatBytes(value: number): string {
return value < 1024 * 1024 ? `${(value / 1024).toLocaleString('de-DE', { maximumFractionDigits: 1 })} KB`
: `${(value / 1024 / 1024).toLocaleString('de-DE', { maximumFractionDigits: 1 })} MB`;
}
function formatTimestamp(value: string): string {
const date = new Date(value);
return Number.isNaN(date.getTime()) ? '—' : new Intl.DateTimeFormat('en-US', { dateStyle: 'short', timeStyle: 'medium' }).format(date);
}
export function MicrosoftBaselineLibrary() {
const inputRef = useRef<HTMLInputElement>(null);
const [records, setRecords] = useState<MicrosoftSecurityBaselineRecord[]>([]);
const [busy, setBusy] = useState(false);
const [progress, setProgress] = useState<string>();
const [deleteTarget, setDeleteTarget] = useState<MicrosoftSecurityBaselineRecord | null>(null);
const [feedback, setFeedback] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const load = useCallback(async () => {
try { setRecords((await assessorApi.listMicrosoftSecurityBaselines()).records); }
catch (reason) { setError(reason instanceof Error ? reason.message : 'Baseline-Katalog konnte nicht geladen werden.'); }
}, []);
useEffect(() => { void load(); }, [load]);
const upload = async (files: FileList) => {
setBusy(true); setError(null); setFeedback(null);
try {
const selected = Array.from(files);
for (const [index, file] of selected.entries()) {
setProgress(`Importing baseline ${index + 1} of ${selected.length}`);
await assessorApi.submitMicrosoftSecurityBaseline(file);
}
await load();
setFeedback(`${files.length} Baseline-Paket${files.length === 1 ? '' : 'e'} im Katalog gespeichert.`);
} catch (reason) { setError(reason instanceof Error ? reason.message : 'Baseline-Upload fehlgeschlagen.'); }
finally { setBusy(false); setProgress(undefined); }
};
const deleteBaseline = async () => {
if (!deleteTarget) return;
setBusy(true); setError(null); setFeedback(null);
try {
const deleted = await assessorApi.deleteMicrosoftSecurityBaseline(deleteTarget.id);
setDeleteTarget(null);
await load();
setFeedback(`${deleted.record.fileName} was deleted from the baseline catalog.`);
} catch (reason) { setError(reason instanceof Error ? reason.message : 'Baseline deletion failed.'); }
finally { setBusy(false); }
};
return <>
<details className="tenant-scan-history baseline-catalog">
<summary className="tenant-section-bar">
<span className="tenant-history-meta baseline-title"><b>{records.length.toLocaleString('en-US')}</b><strong>Imported Security Baselines</strong></span>
<span className="tenant-history-meta">
<button className="btn datahub-maintenance-action baseline-upload-action" type="button" disabled={busy} onClick={event => { event.preventDefault(); event.stopPropagation(); inputRef.current?.click(); }}>{busy ? 'Upload in progress …' : 'Upload Security Baselines (ZIP)'}</button>
<TriangleIcon direction="right"/>
</span>
</summary>
<input ref={inputRef} className="hidden-input" type="file" accept=".zip,application/zip" multiple onChange={event => { if (event.target.files?.length) void upload(event.target.files); event.target.value = ''; }}/>
{feedback && <div className="tenant-save-feedback success" role="status"><span aria-hidden="true">✓</span><strong>{feedback}</strong></div>}
{error && <div className="datahub-alert" role="alert"><span>{error}</span></div>}
{records.length > 0 && <div className="tenant-file-table baseline-table" role="table" aria-label="Microsoft security baselines">
<div className="tenant-file-row tenant-file-head" role="row">
<span>File</span><span>Size</span><span>Imported</span><span>Status</span><span>Actions</span>
</div>
{records.map(record => {
return <div className="tenant-file-row" role="row" key={record.id}>
<span data-label="File" title={record.fileName}>{record.fileName}</span>
<span data-label="Size" className="baseline-cell-num">{formatBytes(record.sizeBytes)}</span>
<span data-label="Imported" className="baseline-cell-num">{formatTimestamp(record.importedAt)}</span>
<span data-label="Status">
<b className="baseline-status" data-status={record.status}
title={record.status === 'eligible' ? 'Eligible for automatic package selection' : 'Excluded until product and version resolve unambiguously'}>
{record.status === 'eligible' ? 'Eligible' : 'Metadata required'}
</b>
</span>
<span data-label="Actions"><button className="btn tenant-scan-delete" type="button" disabled={busy} onClick={() => setDeleteTarget(record)}>Delete</button></span>
</div>;
})}
</div>}
</details>
{deleteTarget && <Dialog title="Delete security baseline?" onClose={() => !busy && setDeleteTarget(null)}><div className="datahub-delete-warning"><strong>{deleteTarget.fileName}</strong><p>Baseline and derived evaluations will be deleted.</p><footer><button className="btn" type="button" disabled={busy} onClick={() => setDeleteTarget(null)}>Cancel</button><button className="btn danger" type="button" disabled={busy} onClick={() => void deleteBaseline()}>{busy ? 'Deleting …' : 'Delete baseline'}</button></footer></div></Dialog>}
<OperationProgressDialog label={progress}/>
</>;
}