83 lines
3.2 KiB
TypeScript
83 lines
3.2 KiB
TypeScript
import type { GpoSecretIndicatorStatus } from './gpo-ingestion';
|
|
|
|
export interface GpoBackupMetadataDocument {
|
|
version: 1;
|
|
parser: { id: 'gpo-backup-metadata-xml'; version: '1.0.0' | '1.1.0' | '1.2.0' | '1.3.0' | '1.4.0' };
|
|
/** Only requested by the Microsoft template analyzer; never inferred from paths. */
|
|
templateNames?: string[];
|
|
sourcePath: string;
|
|
sourceSha256: string;
|
|
status: 'complete' | 'partial' | 'invalid';
|
|
gpoId?: string;
|
|
backupId?: string;
|
|
domainKey?: string;
|
|
backupCreatedAt?: string;
|
|
/**
|
|
* Link facts of a GPReport.xml (parser 1.4.0). Only counts are retained, never the
|
|
* linked OU or domain names. Absent for other metadata files and earlier parsers.
|
|
*/
|
|
links?: GpoBackupLinkEvidence;
|
|
secretIndicatorStatus: GpoSecretIndicatorStatus;
|
|
diagnosticCodes: string[];
|
|
}
|
|
|
|
/** Whether the backup's own report lists an enabled link to a domain or OU at backup time. */
|
|
export interface GpoBackupLinkEvidence {
|
|
status: 'linked' | 'not-linked';
|
|
linkCount: number;
|
|
enabledLinkCount: number;
|
|
}
|
|
|
|
export interface GpoBackupMetadataAggregate {
|
|
version: 1;
|
|
status: 'complete' | 'partial' | 'invalid';
|
|
gpoId?: string;
|
|
backupId?: string;
|
|
domainKey?: string;
|
|
backupCreatedAt?: string;
|
|
sourcePaths: string[];
|
|
/** Present only when exactly one report of the backup states its links. */
|
|
links?: GpoBackupLinkEvidence;
|
|
diagnosticCodes: string[];
|
|
}
|
|
|
|
function unique(values: Array<string | undefined>): string[] {
|
|
return [...new Set(values.filter((value): value is string => Boolean(value)))].sort();
|
|
}
|
|
|
|
export function aggregateGpoBackupMetadata(documents: GpoBackupMetadataDocument[]): GpoBackupMetadataAggregate {
|
|
const diagnostics = new Set(documents.flatMap(document => document.diagnosticCodes));
|
|
let invalid = documents.some(document => document.status === 'invalid');
|
|
const field = (name: 'gpoId' | 'backupId' | 'domainKey' | 'backupCreatedAt'): string | undefined => {
|
|
const values = unique(documents.map(document => document[name]));
|
|
if (values.length > 1) {
|
|
diagnostics.add(`gpo-metadata-${name}-conflict`);
|
|
invalid = true;
|
|
}
|
|
return values.length === 1 ? values[0] : undefined;
|
|
};
|
|
const gpoId = field('gpoId');
|
|
const backupId = field('backupId');
|
|
const domainKey = field('domainKey');
|
|
const backupCreatedAt = field('backupCreatedAt');
|
|
const complete = Boolean(gpoId && backupId && domainKey && backupCreatedAt);
|
|
// Several reports with link facts cannot be ordered; no link status is claimed then.
|
|
const linkDocuments = documents.filter(document => document.links);
|
|
if (linkDocuments.length > 1) diagnostics.add('gpo-metadata-links-ambiguous');
|
|
if (!gpoId) diagnostics.add('gpo-metadata-gpo-id-missing');
|
|
if (!backupId) diagnostics.add('gpo-metadata-backup-id-missing');
|
|
if (!domainKey) diagnostics.add('gpo-metadata-domain-missing');
|
|
if (!backupCreatedAt) diagnostics.add('gpo-metadata-backup-time-missing');
|
|
return {
|
|
version: 1,
|
|
status: invalid ? 'invalid' : complete ? 'complete' : 'partial',
|
|
gpoId,
|
|
backupId,
|
|
domainKey,
|
|
backupCreatedAt,
|
|
sourcePaths: [...new Set(documents.map(document => document.sourcePath))].sort(),
|
|
...(linkDocuments.length === 1 ? { links: linkDocuments[0].links } : {}),
|
|
diagnosticCodes: [...diagnostics].sort()
|
|
};
|
|
}
|