@adeu/core 1.23.0 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +185 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +185 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/diff.ts +23 -8
- package/src/engine.ts +132 -39
- package/src/repro_qa_report_v6.test.ts +486 -0
- package/src/sanitize/core.ts +57 -1
- package/src/sanitize/report.ts +5 -3
- package/src/sanitize/sanitize.test.ts +5 -4
- package/src/sanitize/transforms.ts +94 -16
|
@@ -435,12 +435,17 @@ export function scrub_doc_properties(doc: DocumentObject): string[] {
|
|
|
435
435
|
// Classification-style properties are textbook leak vectors ("Project
|
|
436
436
|
// Falcon", "confidential,merger,..."). Unlike title they carry no
|
|
437
437
|
// legitimate outbound formatting value, so strip them and say so.
|
|
438
|
+
// dc:identifier, dc:language and cp:version are scrubbed with them:
|
|
439
|
+
// identifiers are DMS/matter-ID carriers.
|
|
438
440
|
const leakFields: Array<[string, string]> = [
|
|
439
441
|
['category', 'Category'],
|
|
440
442
|
['keywords', 'Keywords'],
|
|
441
443
|
['subject', 'Subject'],
|
|
442
444
|
['contentStatus', 'Content status'],
|
|
443
445
|
['description', 'Description/comments'],
|
|
446
|
+
['identifier', 'Identifier'],
|
|
447
|
+
['language', 'Language'],
|
|
448
|
+
['version', 'Version'],
|
|
444
449
|
];
|
|
445
450
|
for (const [local, label] of leakFields) {
|
|
446
451
|
findDescendantsByLocalName(corePart._element, local).forEach(c => {
|
|
@@ -498,33 +503,106 @@ export function scrub_timestamps(doc: DocumentObject): string[] {
|
|
|
498
503
|
return modified ? ["Timestamps normalized to epoch"] : [];
|
|
499
504
|
}
|
|
500
505
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
506
|
+
/**
|
|
507
|
+
* Fully ejects package members whose path matches `matcher`: the parsed part
|
|
508
|
+
* list, the raw `pkg.unzipped` bytes (save() re-zips EVERY unzipped
|
|
509
|
+
* member, so skipping this ships the original bytes in the output), the
|
|
510
|
+
* [Content_Types].xml overrides, and every .rels Relationship whose
|
|
511
|
+
* target matches `relTargetMatcher`.
|
|
512
|
+
*/
|
|
513
|
+
function ejectPackageMembers(
|
|
514
|
+
doc: DocumentObject,
|
|
515
|
+
matcher: (path: string) => boolean,
|
|
516
|
+
relTargetMatcher: (target: string) => boolean,
|
|
517
|
+
) {
|
|
518
|
+
const pkg = doc.pkg;
|
|
519
|
+
const normalized = (p: string) => (p.startsWith('/') ? p.substring(1) : p);
|
|
507
520
|
|
|
508
|
-
|
|
521
|
+
// 1. Sever relationships from every .rels part (and the in-memory rels maps)
|
|
522
|
+
for (const part of pkg.parts) {
|
|
523
|
+
if (!part.partname.endsWith('.rels')) continue;
|
|
509
524
|
const toRemove: Element[] = [];
|
|
510
|
-
for (const rel of findAllDescendants(
|
|
511
|
-
const target = rel.getAttribute('Target');
|
|
512
|
-
if (target
|
|
525
|
+
for (const rel of findAllDescendants(part._element, 'Relationship')) {
|
|
526
|
+
const target = rel.getAttribute('Target') || '';
|
|
527
|
+
if (relTargetMatcher(target)) {
|
|
528
|
+
toRemove.push(rel);
|
|
529
|
+
const sourcePath = part.partname.replace('/_rels/', '/').replace('.rels', '');
|
|
530
|
+
const sourcePart = pkg.getPartByPath(sourcePath);
|
|
531
|
+
if (sourcePart) {
|
|
532
|
+
const relId = rel.getAttribute('Id');
|
|
533
|
+
if (relId) sourcePart.rels.delete(relId);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
513
536
|
}
|
|
514
537
|
toRemove.forEach(r => r.parentNode?.removeChild(r));
|
|
515
|
-
}
|
|
538
|
+
}
|
|
516
539
|
|
|
517
|
-
|
|
518
|
-
|
|
540
|
+
// 2. Remove [Content_Types].xml overrides
|
|
541
|
+
const ctPart = pkg.getPartByPath('[Content_Types].xml');
|
|
542
|
+
if (ctPart) {
|
|
543
|
+
const toRemove: Element[] = [];
|
|
544
|
+
for (const override of findAllDescendants(ctPart._element, 'Override')) {
|
|
545
|
+
const partName = override.getAttribute('PartName') || '';
|
|
546
|
+
if (matcher(normalized(partName))) toRemove.push(override);
|
|
547
|
+
}
|
|
548
|
+
toRemove.forEach(o => o.parentNode?.removeChild(o));
|
|
549
|
+
}
|
|
519
550
|
|
|
520
|
-
|
|
521
|
-
|
|
551
|
+
// 3. Drop the parts from the serialization list AND the raw zip map
|
|
552
|
+
pkg.parts = pkg.parts.filter(p => !matcher(normalized(p.partname)));
|
|
553
|
+
for (const key of Object.keys(pkg.unzipped)) {
|
|
554
|
+
if (matcher(normalized(key))) delete pkg.unzipped[key];
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function strip_custom_xml(doc: DocumentObject): string[] {
|
|
559
|
+
const isCustomXml = (path: string) => path.startsWith('customXml/');
|
|
560
|
+
const customParts = doc.pkg.parts.filter(p =>
|
|
561
|
+
isCustomXml(p.partname.startsWith('/') ? p.partname.substring(1) : p.partname),
|
|
562
|
+
);
|
|
563
|
+
const leakedMembers = Object.keys(doc.pkg.unzipped).filter(isCustomXml);
|
|
564
|
+
if (customParts.length === 0 && leakedMembers.length === 0) return [];
|
|
565
|
+
|
|
566
|
+
ejectPackageMembers(doc, isCustomXml, target => target.includes('customXml'));
|
|
522
567
|
|
|
523
568
|
for (const sdtPr of findAllDescendants(doc.element, 'w:sdtPr')) {
|
|
524
569
|
findChildren(sdtPr, 'w:dataBinding').forEach(b => sdtPr.removeChild(b));
|
|
525
570
|
}
|
|
526
571
|
|
|
527
|
-
return [`Custom XML parts: ${customParts.length} removed`];
|
|
572
|
+
return [`Custom XML parts: ${Math.max(customParts.length, leakedMembers.length)} removed`];
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const CUSTOM_PROPS_PATH = 'docProps/custom.xml';
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Remove the custom document properties part (docProps/custom.xml) and its
|
|
579
|
+
* package relationship. Custom properties are a standard home for matter
|
|
580
|
+
* numbers, client names, DMS identifiers and workflow secrets; a package
|
|
581
|
+
* that keeps them can never be reported CLEAN.
|
|
582
|
+
*/
|
|
583
|
+
export function strip_custom_properties(doc: DocumentObject): string[] {
|
|
584
|
+
const part = doc.pkg.getPartByPath(CUSTOM_PROPS_PATH);
|
|
585
|
+
const leaked = Object.keys(doc.pkg.unzipped).includes(CUSTOM_PROPS_PATH);
|
|
586
|
+
if (!part && !leaked) return [];
|
|
587
|
+
|
|
588
|
+
// Enumerate what is being removed so the report discloses it.
|
|
589
|
+
const lines: string[] = [];
|
|
590
|
+
if (part) {
|
|
591
|
+
for (const prop of findDescendantsByLocalName(part._element, 'property')) {
|
|
592
|
+
const name = prop.getAttribute('name') || '(unnamed)';
|
|
593
|
+
const value = (prop.textContent || '').trim();
|
|
594
|
+
lines.push(` Custom property removed: ${name} = "${_truncate(value, 60)}"`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
ejectPackageMembers(
|
|
599
|
+
doc,
|
|
600
|
+
path => path === CUSTOM_PROPS_PATH,
|
|
601
|
+
target => target.includes('docProps/custom.xml'),
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
const count = Math.max(lines.length, 1);
|
|
605
|
+
return [`Custom document properties: ${count} removed (docProps/custom.xml)`].concat(lines);
|
|
528
606
|
}
|
|
529
607
|
|
|
530
608
|
export function strip_image_alt_text(doc: DocumentObject): string[] {
|