@patrickdk77/office-defuse 0.0.0-stage → 0.1.1

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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +317 -2
  3. package/dist/biff.d.ts +62 -0
  4. package/dist/biff.js +95 -0
  5. package/dist/cfb.d.ts +58 -0
  6. package/dist/cfb.js +456 -0
  7. package/dist/context.d.ts +66 -0
  8. package/dist/context.js +121 -0
  9. package/dist/crc32.d.ts +8 -0
  10. package/dist/crc32.js +58 -0
  11. package/dist/engine.d.ts +5 -0
  12. package/dist/engine.js +200 -0
  13. package/dist/errors.d.ts +19 -0
  14. package/dist/errors.js +33 -0
  15. package/dist/fields.d.ts +29 -0
  16. package/dist/fields.js +97 -0
  17. package/dist/findings.d.ts +25 -0
  18. package/dist/findings.js +188 -0
  19. package/dist/formats.d.ts +10 -0
  20. package/dist/formats.js +46 -0
  21. package/dist/formulas.d.ts +22 -0
  22. package/dist/formulas.js +77 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.js +30 -0
  25. package/dist/index.mjs +3 -0
  26. package/dist/io.d.ts +23 -0
  27. package/dist/io.js +139 -0
  28. package/dist/links.d.ts +26 -0
  29. package/dist/links.js +145 -0
  30. package/dist/msg.d.ts +7 -0
  31. package/dist/msg.js +97 -0
  32. package/dist/objects.d.ts +42 -0
  33. package/dist/objects.js +134 -0
  34. package/dist/odf.d.ts +3 -0
  35. package/dist/odf.js +297 -0
  36. package/dist/offcrypto.d.ts +31 -0
  37. package/dist/offcrypto.js +408 -0
  38. package/dist/ole.d.ts +12 -0
  39. package/dist/ole.js +127 -0
  40. package/dist/oleobj.d.ts +74 -0
  41. package/dist/oleobj.js +198 -0
  42. package/dist/ooxml.d.ts +8 -0
  43. package/dist/ooxml.js +468 -0
  44. package/dist/package.d.ts +3 -0
  45. package/dist/package.js +21 -0
  46. package/dist/ppt.d.ts +8 -0
  47. package/dist/ppt.js +241 -0
  48. package/dist/rtf.d.ts +8 -0
  49. package/dist/rtf.js +539 -0
  50. package/dist/score.d.ts +17 -0
  51. package/dist/score.js +63 -0
  52. package/dist/types.d.ts +274 -0
  53. package/dist/types.js +152 -0
  54. package/dist/word.d.ts +9 -0
  55. package/dist/word.js +160 -0
  56. package/dist/xls.d.ts +27 -0
  57. package/dist/xls.js +174 -0
  58. package/dist/xlsb.d.ts +19 -0
  59. package/dist/xlsb.js +101 -0
  60. package/dist/xml.d.ts +256 -0
  61. package/dist/xml.js +1105 -0
  62. package/dist/zip-writer.d.ts +38 -0
  63. package/dist/zip-writer.js +182 -0
  64. package/dist/zip.d.ts +55 -0
  65. package/dist/zip.js +535 -0
  66. package/package.json +96 -4
package/dist/rtf.js ADDED
@@ -0,0 +1,539 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inspectRtf = inspectRtf;
4
+ const cfb_1 = require("./cfb");
5
+ const context_1 = require("./context");
6
+ const errors_1 = require("./errors");
7
+ const fields_1 = require("./fields");
8
+ const io_1 = require("./io");
9
+ const links_1 = require("./links");
10
+ const objects_1 = require("./objects");
11
+ const oleobj_1 = require("./oleobj");
12
+ const types_1 = require("./types");
13
+ /**
14
+ * The destinations of the RTF 1.9.1 specification, which rtfobj lists too. Word sends the text of each to its own
15
+ * place, so none of it is object data or a field code. \rtf is not one, though it opens the document.
16
+ */
17
+ const DESTINATIONS = new Set(('aftncn aftnsep aftnsepc annotation atnauthor atndate atnid atnparent atnref atrfend atrfstart author background bkmkend bkmkstart ' +
18
+ 'blipuid buptim category colorschememapping colortbl comment company creatim datafield datastore defchp defpap do doccomm docvar ' +
19
+ 'dptxbxtext ebcend ebcstart factoidname falt fchars ffdeftext ffentrymcr ffexitmcr ffformat ffhelptext ffl ffname ffstattext field ' +
20
+ 'file filetbl fldinst fldrslt fldtype fontemb fonttbl footer footerf footerl footerr footnote formfield ftncn ftnsep ftnsepc g ' +
21
+ 'generator gridtbl header headerf headerl headerr hl hlfr hlinkbase hlloc hlsrc hsv info keywords latentstyles lchars levelnumbers ' +
22
+ 'leveltext lfolevel linkval list listlevel listname listoverride listoverridetable listpicture liststylename listtable listtext ' +
23
+ 'lsdlockedexcept macc maccPr mailmerge malnScr manager margPr mbar mbarPr mbaseJc mbegChr mborderBox mborderBoxPr mbox mboxPr mchr ' +
24
+ 'mcount mctrlPr md mdeg mdegHide mden mdiff mdPr me mendChr meqArr meqArrPr mf mfName mfPr mfunc mfuncPr mgroupChr mgroupChrPr mgrow ' +
25
+ 'mhideBot mhideLeft mhideRight mhideTop mlim mlimLoc mlimLow mlimLowPr mlimUpp mlimUppPr mm mmaddfieldname mmathPict mmaxDist mmc ' +
26
+ 'mmcJc mmconnectstr mmconnectstrdata mmcPr mmcs mmdatasource mmheadersource mmmailsubject mmodso mmodsofilter mmodsofldmpdata ' +
27
+ 'mmodsomappedname mmodsoname mmodsorecipdata mmodsosort mmodsosrc mmodsotable mmodsoudl mmodsoudldata mmodsouniquetag mmPr mmquery ' +
28
+ 'mmr mnary mnaryPr mnoBreak mnum mobjDist moMath moMathPara moMathParaPr mopEmu mphant mphantPr mplcHide mpos mr mrad mradPr mrPr ' +
29
+ 'msepChr mshow mshp msPre msPrePr msSub msSubPr msSubSup msSubSupPr msSup msSupPr mstrikeBLTR mstrikeH mstrikeTLBR mstrikeV msub ' +
30
+ 'msubHide msup msupHide mtransp mtype mvertJc mvfmf mvfml mvtof mvtol mzeroAsc mzeroDesc mzeroWid nesttableprops nonesttables ' +
31
+ 'objalias objclass objdata object objname objsect oldcprops oldpprops oldsprops oldtprops oleclsid operator panose password ' +
32
+ 'passwordhash pgp pgptbl picprop pict pn pnseclvl pntext pntxta pntxtb printim propname protend protstart protusertbl result revtbl ' +
33
+ 'revtim rxe shp shpgrp shpinst shppict shprslt shptxt sn sp staticval stylesheet subject sv svb tc template themedata title txe ud ' +
34
+ 'upr userprops wgrffmtfilter windowcaption writereservation writereservhash xe xform xmlattrname xmlattrvalue xmlclose xmlname ' +
35
+ 'xmlnstbl xmlopen margSz pnaiu pnaiud').split(' '));
36
+ /** Destinations whose content holds no field, object or link, so their groups are skipped whole: tables, styles, pictures, metadata. */
37
+ const INERT = new Set(('fonttbl colortbl stylesheet info pict themedata colorschememapping datastore latentstyles listtable listoverridetable revtbl ' +
38
+ 'generator xmlnstbl filetbl pgptbl protusertbl blipuid datafield fontemb panose falt password passwordhash wgrffmtfilter').split(' '));
39
+ /** Object kinds, from the control word that sets them. */
40
+ const OBJECT_KINDS = new Set(['objemb', 'objlink', 'objautlink', 'objsub', 'objpub', 'objicemb', 'objhtml', 'objocx']);
41
+ /** Word reads at most this many letters of a control word's name and digits of its parameter. */
42
+ const MAX_WORD = 250;
43
+ const CHUNK = 262144;
44
+ /** Collected text other than an object's data is short: a class name, a template path, a link. */
45
+ const MAX_TEXT = 4096;
46
+ /** An object's data as Word decodes its hex digits, with the quirks rtfobj documents. */
47
+ class HexData {
48
+ limit;
49
+ bytes;
50
+ length = 0;
51
+ /** A hex digit read and not yet paired, or -1. */
52
+ nibble = -1;
53
+ offset;
54
+ constructor(offset, limit) {
55
+ this.limit = limit;
56
+ this.offset = offset;
57
+ this.bytes = new Uint8Array(1024);
58
+ }
59
+ digit(v) {
60
+ if (this.nibble < 0) {
61
+ this.nibble = v;
62
+ return;
63
+ }
64
+ this.push((this.nibble << 4) | v);
65
+ this.nibble = -1;
66
+ }
67
+ /** Word drops a hex digit left unpaired before a \'hh, and so does rtfobj. */
68
+ quote() {
69
+ this.nibble = -1;
70
+ }
71
+ /** \bin data in hex goes in as its bytes, two digits each, so an unpaired digit stays unpaired. */
72
+ binary(b) {
73
+ for (const x of b) {
74
+ this.digit(x >> 4);
75
+ this.digit(x & 15);
76
+ }
77
+ }
78
+ push(b) {
79
+ if (this.length >= this.limit)
80
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.DecompressedSize, `The data of the object at offset ${this.offset} decodes past ${this.limit} bytes`, { limit: this.limit });
81
+ if (this.length === this.bytes.length) {
82
+ const grown = new Uint8Array(Math.min(this.limit, this.bytes.length * 2));
83
+ grown.set(this.bytes);
84
+ this.bytes = grown;
85
+ }
86
+ this.bytes[this.length++] = b;
87
+ }
88
+ /** The decoded bytes. A last digit left unpaired is dropped, as Word drops it. */
89
+ result() {
90
+ return this.bytes.subarray(0, this.length);
91
+ }
92
+ }
93
+ const hexValue = (c) => (c >= 0x30 && c <= 0x39 ? c - 0x30 : c >= 0x41 && c <= 0x46 ? c - 0x37 : c >= 0x61 && c <= 0x66 ? c - 0x57 : -1);
94
+ const isLetter = (c) => (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a);
95
+ const isDigit = (c) => c >= 0x30 && c <= 0x39;
96
+ /**
97
+ * Inspects an RTF file the way Word reads one. Objects with their data, fields, the attached template and shape
98
+ * hyperlinks are reported. Unknown destinations marked with \* are skipped, unknown control words are ignored, and
99
+ * text after the document's last brace is still read, as rtfobj reads it, and reported as trailing data.
100
+ */
101
+ async function inspectRtf(ctx, source, size) {
102
+ ctx.format = { format: 'rtf', mimeType: 'application/rtf' };
103
+ const reader = new RtfReader(ctx, size);
104
+ for (let pos = 0; pos < size; pos += CHUNK) {
105
+ (0, errors_1.checkTime)(ctx.settings.deadline);
106
+ const chunk = await source.read(pos, Math.min(CHUNK, size - pos));
107
+ await reader.write(chunk, pos + chunk.length >= size);
108
+ }
109
+ await reader.end();
110
+ }
111
+ class RtfReader {
112
+ ctx;
113
+ size;
114
+ frames;
115
+ fields;
116
+ depth = 0;
117
+ /** A partial token carried from one chunk to the next. */
118
+ carry = new Uint8Array(0);
119
+ /** The file offset of the first byte of the buffer being read. */
120
+ base = 0;
121
+ /** \bin bytes still to take, and where they go. */
122
+ binLeft = 0;
123
+ /** Fallback characters still to skip after a \u. */
124
+ skipChars = 0;
125
+ /** Set by \*, which makes an unknown destination that follows skipped. */
126
+ starred = false;
127
+ /** Where the document's outermost group closed, and how much that is not white space comes after it. */
128
+ closedAt = -1;
129
+ trailing = 0;
130
+ decoded = 0;
131
+ /** Objects whose groups closed, read in order once the chunk is done. */
132
+ pending = [];
133
+ constructor(ctx, size) {
134
+ this.ctx = ctx;
135
+ this.size = size;
136
+ this.frames = [{ kind: 'document', depth: 0, uc: 1 }];
137
+ this.fields = new fields_1.FieldStack(ctx, 'document');
138
+ }
139
+ get top() {
140
+ return this.frames[this.frames.length - 1];
141
+ }
142
+ async write(chunk, last) {
143
+ const b = this.carry.length ? Buffer.concat([this.carry, chunk]) : chunk;
144
+ let i = 0;
145
+ while (i < b.length) {
146
+ if (this.binLeft > 0) {
147
+ const n = Math.min(this.binLeft, b.length - i);
148
+ const top = this.top;
149
+ if (top.kind === 'objdata') {
150
+ top.hex.binary(b.subarray(i, i + n));
151
+ top.alt.binary(b.subarray(i, i + n));
152
+ }
153
+ else if (top.kind === 'objskip')
154
+ top.data.hex.binary(b.subarray(i, i + n));
155
+ this.note(b, i, i + n);
156
+ this.binLeft -= n;
157
+ i += n;
158
+ continue;
159
+ }
160
+ const c = b[i];
161
+ if (c === 0x7b) {
162
+ this.note(b, i, i + 1);
163
+ this.open();
164
+ i++;
165
+ }
166
+ else if (c === 0x7d) {
167
+ this.note(b, i, i + 1);
168
+ this.close(this.base + i);
169
+ i++;
170
+ }
171
+ else if (c === 0x5c) {
172
+ const used = this.control(b, i, last);
173
+ if (used === 0)
174
+ break;
175
+ this.note(b, i, i + used);
176
+ i += used;
177
+ }
178
+ else {
179
+ let j = i;
180
+ while (j < b.length && b[j] !== 0x7b && b[j] !== 0x7d && b[j] !== 0x5c)
181
+ j++;
182
+ this.note(b, i, j);
183
+ this.text(b, i, j);
184
+ i = j;
185
+ }
186
+ }
187
+ this.carry = i < b.length ? b.slice(i) : new Uint8Array(0);
188
+ this.base += i;
189
+ await this.objects();
190
+ }
191
+ async objects() {
192
+ for (const o of this.pending.splice(0))
193
+ await objectRules(this.ctx, o);
194
+ }
195
+ async end() {
196
+ // A file can end inside groups. Word reads what it has, and so does this.
197
+ while (this.depth > 0)
198
+ this.close(this.size);
199
+ this.fields.finish();
200
+ await this.objects();
201
+ if (this.trailing > 0)
202
+ this.ctx.report(types_1.OfficeCategory.Container, types_1.OfficeDetail.TrailingBytes, undefined, { bytes: this.trailing });
203
+ }
204
+ /** Counts bytes past the document's outermost group that are not white space or NUL. */
205
+ note(b, from, to) {
206
+ if (this.closedAt < 0)
207
+ return;
208
+ for (let k = from; k < to; k++)
209
+ if (b[k] > 0x20)
210
+ this.trailing++;
211
+ }
212
+ open() {
213
+ this.depth++;
214
+ this.skipChars = 0;
215
+ const limit = this.ctx.settings.rtfDepth;
216
+ if (this.depth > limit)
217
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.RtfDepth, `Groups nest deeper than ${limit}`, { limit });
218
+ }
219
+ close(offset) {
220
+ this.skipChars = 0;
221
+ this.starred = false;
222
+ while (this.frames.length > 1 && this.top.depth >= this.depth)
223
+ this.finishFrame(this.frames.pop());
224
+ if (this.depth === 0)
225
+ return;
226
+ this.depth--;
227
+ if (this.depth === 0 && this.closedAt < 0)
228
+ this.closedAt = offset;
229
+ }
230
+ /**
231
+ * Opens a destination for the rest of the group. One opened in the same group as the last ends that one, as in
232
+ * rtfobj, unless the last is a field or an object, whose group the destination belongs to.
233
+ */
234
+ push(body) {
235
+ const top = this.top;
236
+ if (this.frames.length > 1 && top.depth === this.depth && top.kind !== 'field' && top.kind !== 'object')
237
+ this.finishFrame(this.frames.pop());
238
+ const limit = this.ctx.settings.rtfDepth;
239
+ if (this.frames.length > limit)
240
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.RtfDepth, `More than ${limit} destinations are open at once`, { limit });
241
+ this.frames.push({ ...body, depth: this.depth, uc: this.top.uc });
242
+ }
243
+ /** Reads a control word or symbol at `i`. Returns the bytes it takes, or 0 when the buffer ends inside it. */
244
+ control(b, i, last) {
245
+ if (i + 1 >= b.length)
246
+ return last ? 1 : 0;
247
+ const c = b[i + 1];
248
+ if (!isLetter(c)) {
249
+ if (c === 0x27) {
250
+ if (i + 3 >= b.length && !last)
251
+ return 0;
252
+ this.quoted(b, i + 2);
253
+ return Math.min(4, b.length - i);
254
+ }
255
+ if (c === 0x2a)
256
+ this.starred = true;
257
+ else if (c === 0x5c || c === 0x7b || c === 0x7d)
258
+ this.char(String.fromCharCode(c));
259
+ else if (c === 0x0a || c === 0x0d)
260
+ this.char('\n');
261
+ else if (c === 0x7e)
262
+ this.char(' ');
263
+ return 2;
264
+ }
265
+ let j = i + 1;
266
+ while (j < b.length && isLetter(b[j]) && j - i - 1 < MAX_WORD)
267
+ j++;
268
+ let k = j;
269
+ if (k < b.length && b[k] === 0x2d && k + 1 < b.length && isDigit(b[k + 1]))
270
+ k++;
271
+ while (k < b.length && isDigit(b[k]) && k - j < MAX_WORD)
272
+ k++;
273
+ if (k >= b.length && !last)
274
+ return 0;
275
+ const name = (0, oleobj_1.latin1)(b.subarray(i + 1, j));
276
+ const digits = (0, oleobj_1.latin1)(b.subarray(j, k));
277
+ const param = digits === '' ? undefined : Number(digits);
278
+ // A space ends the control word and belongs to it.
279
+ const used = k - i + (k < b.length && b[k] === 0x20 ? 1 : 0);
280
+ this.word(name, param, this.base + i);
281
+ return used;
282
+ }
283
+ word(name, param, offset) {
284
+ const starred = this.starred;
285
+ this.starred = false;
286
+ // \bin data is taken whole wherever it is, since its bytes may hold braces.
287
+ if (name === 'bin') {
288
+ // Word reads a negative or missing length as none.
289
+ this.binLeft = param !== undefined && param > 0 ? Math.min(param, Number.MAX_SAFE_INTEGER) : 0;
290
+ return;
291
+ }
292
+ const top = this.top;
293
+ if (top.kind === 'skip')
294
+ return;
295
+ if (name === 'u' && param !== undefined) {
296
+ this.skipChars = 0;
297
+ this.char(String.fromCharCode(param < 0 ? param + 65536 : param & 0xffff));
298
+ this.skipChars = top.uc;
299
+ return;
300
+ }
301
+ this.skipChars = 0;
302
+ if (name === 'uc') {
303
+ top.uc = param ?? 1;
304
+ return;
305
+ }
306
+ const object = this.object();
307
+ if (INERT.has(name))
308
+ this.push({ kind: 'skip' });
309
+ else if (name === 'field') {
310
+ this.fields.begin();
311
+ this.push({ kind: 'field' });
312
+ }
313
+ else if (name === 'fldinst')
314
+ this.push({ kind: 'fldinst' });
315
+ else if (name === 'fldrslt') {
316
+ this.fields.separate();
317
+ this.push({ kind: 'fldrslt' });
318
+ }
319
+ else if (name === 'object')
320
+ this.push({ kind: 'object', object: { offset, kinds: new Set(), update: false, className: '' } });
321
+ else if (name === 'objupdate') {
322
+ if (object)
323
+ object.update = true;
324
+ }
325
+ else if (OBJECT_KINDS.has(name))
326
+ object?.kinds.add(name);
327
+ else if (name === 'objdata') {
328
+ const limit = this.ctx.settings.decompressedBytes;
329
+ this.push({
330
+ kind: 'objdata',
331
+ object: object ?? { offset, kinds: new Set(), update: false, className: '' },
332
+ hex: new HexData(offset, limit),
333
+ alt: new HexData(offset, limit),
334
+ standalone: !object,
335
+ });
336
+ }
337
+ else if (name === 'objclass')
338
+ this.push({ kind: 'objclass', text: '', object });
339
+ else if (name === 'template' || (name === 'hlsrc' && this.frames.some(f => f.kind === 'hl')))
340
+ this.push({ kind: name, text: '' });
341
+ else if (name === 'hl')
342
+ this.push({ kind: 'hl' });
343
+ // Any other destination takes its own text. What it holds is still read for fields and objects.
344
+ else if (DESTINATIONS.has(name))
345
+ this.push({ kind: 'document' });
346
+ // Word skips a group that \* marks when it does not know its control word. Inside object data rtfobj keeps it, and
347
+ // a sample Word opens hides its hex digits in groups such as {\*\par574 0}. Both readings of the data are kept.
348
+ else if (starred && (top.kind === 'objdata' || top.kind === 'objskip'))
349
+ this.push({ kind: 'objskip', data: top.kind === 'objdata' ? top : top.data });
350
+ else if (starred)
351
+ this.push({ kind: 'skip' });
352
+ }
353
+ /** The object of the innermost object group the reader is in. */
354
+ object() {
355
+ for (let k = this.frames.length - 1; k > 0; k--) {
356
+ const f = this.frames[k];
357
+ if (f.kind === 'object')
358
+ return f.object;
359
+ }
360
+ return undefined;
361
+ }
362
+ quoted(b, at) {
363
+ const top = this.top;
364
+ if (top.kind === 'skip')
365
+ return;
366
+ this.starred = false;
367
+ if (top.kind === 'objdata') {
368
+ top.hex.quote();
369
+ top.alt.quote();
370
+ return;
371
+ }
372
+ if (top.kind === 'objskip') {
373
+ top.data.hex.quote();
374
+ return;
375
+ }
376
+ // A file can end before the two characters of a \'hh.
377
+ const hi = hexValue(b[at] ?? -1);
378
+ const lo = hexValue(b[at + 1] ?? -1);
379
+ if (hi >= 0 && lo >= 0)
380
+ this.char(String.fromCharCode((hi << 4) | lo));
381
+ }
382
+ text(b, from, to) {
383
+ const top = this.top;
384
+ if (top.kind === 'skip')
385
+ return;
386
+ this.starred = false;
387
+ if (top.kind === 'objdata' || top.kind === 'objskip') {
388
+ const both = top.kind === 'objdata';
389
+ const hex = both ? top.hex : top.data.hex;
390
+ for (let k = from; k < to; k++) {
391
+ const v = hexValue(b[k]);
392
+ if (v < 0)
393
+ continue;
394
+ hex.digit(v);
395
+ if (both)
396
+ top.alt.digit(v);
397
+ }
398
+ return;
399
+ }
400
+ // Line ends in RTF text are not text.
401
+ let s = '';
402
+ for (let k = from; k < to; k++) {
403
+ const c = b[k];
404
+ if (c === 0x0a || c === 0x0d)
405
+ continue;
406
+ if (this.skipChars > 0) {
407
+ this.skipChars--;
408
+ continue;
409
+ }
410
+ s += String.fromCharCode(c);
411
+ }
412
+ if (s)
413
+ this.char(s, true);
414
+ }
415
+ /** Text decoded from the file, for the destination being read. */
416
+ char(s, counted = false) {
417
+ if (!counted && this.skipChars > 0) {
418
+ this.skipChars--;
419
+ return;
420
+ }
421
+ const top = this.top;
422
+ if (top.kind === 'fldinst')
423
+ this.fields.text(s);
424
+ else if ((top.kind === 'objclass' || top.kind === 'template' || top.kind === 'hlsrc') && top.text.length < MAX_TEXT)
425
+ top.text += s;
426
+ }
427
+ finishFrame(f) {
428
+ if (f.kind === 'field')
429
+ this.fields.end();
430
+ else if (f.kind === 'objclass') {
431
+ if (f.object)
432
+ f.object.className = f.text.trim();
433
+ }
434
+ else if (f.kind === 'template') {
435
+ const path = f.text.trim();
436
+ if (path)
437
+ this.ctx.external(types_1.OfficeDetail.Template, path, 'template', { source: 'RTF template' }, 'template');
438
+ }
439
+ else if (f.kind === 'hlsrc') {
440
+ const target = f.text.trim();
441
+ if (target && !(0, links_1.isFragment)(target))
442
+ this.ctx.link(target, 'hyperlink', { source: 'RTF shape hyperlink' });
443
+ }
444
+ else if (f.kind === 'objdata') {
445
+ f.object.data = f.hex.result();
446
+ const alt = f.alt.result();
447
+ if (!Buffer.from(alt).equals(Buffer.from(f.object.data)))
448
+ f.object.alt = alt;
449
+ this.decoded += f.object.data.length + (f.object.alt ? alt.length : 0);
450
+ const total = this.ctx.settings.totalDecompressedBytes;
451
+ if (this.decoded > total)
452
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.TotalDecompressedSize, `The objects decode past ${total} bytes`, { limit: total });
453
+ // An objdata outside any object is an object of its own, as rtfobj reads it.
454
+ if (f.standalone)
455
+ this.pending.push(f.object);
456
+ }
457
+ else if (f.kind === 'object')
458
+ this.pending.push(f.object);
459
+ }
460
+ }
461
+ /** One embedded or linked object: what its OLE 1.0 header names, and what its data holds. */
462
+ async function objectRules(ctx, o) {
463
+ const location = `object at offset ${o.offset}`;
464
+ const kinds = [...o.kinds];
465
+ const base = { object: location, ...(kinds.length ? { kind: kinds.join(' ') } : {}) };
466
+ if (o.update)
467
+ ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.ObjectUpdate, location, base);
468
+ if (o.kinds.has('objocx')) {
469
+ ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, { ...base, ...(o.className ? { className: (0, context_1.clip)(o.className) } : {}) });
470
+ return;
471
+ }
472
+ await objectData(ctx, o, o.data, location, base);
473
+ // The data read without the groups \* marks, where that reads differently, is the object a reader that skips them sees.
474
+ if (o.alt)
475
+ await objectData(ctx, o, o.alt, location, { ...base, reading: 'without the groups \\* marks' });
476
+ }
477
+ /** An object's data: its OLE 1.0 header, and the Packager data or storage it holds. */
478
+ async function objectData(ctx, o, bytes, location, base) {
479
+ const ole1 = bytes?.length ? (0, oleobj_1.parseOle1)(bytes) : undefined;
480
+ const className = (ole1?.className || o.className).replace(/\0+$/, '');
481
+ const data = { ...base, ...(className ? { className: (0, context_1.clip)(className) } : {}) };
482
+ const link = { object: location, ...(className ? { className: (0, context_1.clip)(className) } : {}) };
483
+ let equation = (0, objects_1.isEquationClass)(className);
484
+ if (ole1?.formatId === 1 || (!bytes?.length && (o.kinds.has('objlink') || o.kinds.has('objautlink')))) {
485
+ const target = ole1?.topicName ?? '';
486
+ if (target)
487
+ ctx.external(types_1.OfficeDetail.Object, target, location, link, 'linked object');
488
+ else
489
+ ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, data);
490
+ return;
491
+ }
492
+ if (!bytes?.length)
493
+ return;
494
+ if (!ole1)
495
+ data.reason = 'its data is not an OLE 1.0 object';
496
+ const native = ole1?.data;
497
+ if (native && className.toLowerCase() === 'package') {
498
+ const pkg = (0, oleobj_1.parsePackage)(native);
499
+ if (pkg)
500
+ Object.assign(data, (0, objects_1.packageData)(pkg));
501
+ }
502
+ else if (native && native.length >= 8 && cfb_1.CFB_SIGNATURE.every((v, i) => native[i] === v)) {
503
+ let cfb;
504
+ try {
505
+ cfb = await cfb_1.CompoundFile.open((0, io_1.bufferSource)(native), ctx.settings, ctx.settings.deadline);
506
+ }
507
+ catch (e) {
508
+ if (!(e instanceof errors_1.StopError) || e.category !== types_1.OfficeCategory.Container)
509
+ throw e;
510
+ data.reason = `its storage does not read: ${e.message}`;
511
+ }
512
+ if (cfb) {
513
+ (0, objects_1.nestedNotes)(ctx, cfb, location);
514
+ const d = await (0, objects_1.describeObject)(cfb, cfb.root, location);
515
+ if (d.clsid)
516
+ data.clsid = d.clsid;
517
+ if (d.progId)
518
+ data.progId = (0, context_1.clip)(d.progId);
519
+ if (d.pkg)
520
+ Object.assign(data, (0, objects_1.packageData)(d.pkg));
521
+ equation ||= d.equation;
522
+ await (0, objects_1.compoundRules)(ctx, cfb, location, false);
523
+ if (d.linked) {
524
+ (0, objects_1.linkedObject)(ctx, d, location, link);
525
+ if (equation)
526
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, location, data);
527
+ return;
528
+ }
529
+ }
530
+ }
531
+ // OLE2Link is the class of a linked object, CVE-2017-0199's. Its storage names the target, and when the storage does
532
+ // not read, the class still says the object loads from outside.
533
+ if (className.toLowerCase() === 'ole2link')
534
+ ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, data);
535
+ else
536
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data);
537
+ if (equation)
538
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, location, data);
539
+ }
@@ -0,0 +1,17 @@
1
+ import { OfficeCategory as C, OfficeDetail as D, type DefuseFinding, type OfficeOptions, OfficeRisk, type OfficeScoreBands } from './types';
2
+ export declare const DEFAULT_BANDS: OfficeScoreBands;
3
+ interface Combination {
4
+ name: string;
5
+ weight: number;
6
+ test(has: (c: C, d?: D) => boolean): boolean;
7
+ }
8
+ export declare const COMBINATIONS: Combination[];
9
+ /** The caller's weights first, then the built-in table. A kind from another package scores at the weight it carries. */
10
+ export declare function weightOf(f: DefuseFinding, options: OfficeOptions): number;
11
+ /** Highest weight in full, a quarter of each other kind's weight, plus combinations, capped at 100. */
12
+ export declare function scoreFindings(findings: DefuseFinding[], options: OfficeOptions): {
13
+ score: number;
14
+ risk: OfficeRisk;
15
+ };
16
+ export declare function riskFor(score: number, bands: OfficeScoreBands): OfficeRisk;
17
+ export {};
package/dist/score.js ADDED
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.COMBINATIONS = exports.DEFAULT_BANDS = void 0;
4
+ exports.weightOf = weightOf;
5
+ exports.scoreFindings = scoreFindings;
6
+ exports.riskFor = riskFor;
7
+ const findings_1 = require("./findings");
8
+ const types_1 = require("./types");
9
+ exports.DEFAULT_BANDS = { low: 1, medium: 25, high: 50, critical: 80 };
10
+ /** Code that runs, in any of its forms. */
11
+ const runsCode = (has) => has(types_1.OfficeCategory.Macro) || has(types_1.OfficeCategory.Dde);
12
+ exports.COMBINATIONS = [
13
+ {
14
+ name: 'code with a template, object or include from outside',
15
+ weight: 20,
16
+ test: has => (runsCode(has) || has(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram)) && (has(types_1.OfficeCategory.External, types_1.OfficeDetail.Template) || has(types_1.OfficeCategory.External, types_1.OfficeDetail.Object) || has(types_1.OfficeCategory.External, types_1.OfficeDetail.Include)),
17
+ },
18
+ { name: 'code with an embedded object or control', weight: 15, test: has => runsCode(has) && (has(types_1.OfficeCategory.Embedded) || has(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX)) },
19
+ {
20
+ name: 'code with data hidden in the archive',
21
+ weight: 10,
22
+ test: has => runsCode(has) && (has(types_1.OfficeCategory.Container, types_1.OfficeDetail.LeadingBytes) || has(types_1.OfficeCategory.Container, types_1.OfficeDetail.HiddenData) || has(types_1.OfficeCategory.Container, types_1.OfficeDetail.TrailingBytes)),
23
+ },
24
+ ];
25
+ /** The caller's weights first, then the built-in table. A kind from another package scores at the weight it carries. */
26
+ function weightOf(f, options) {
27
+ const w = options.scoreWeights ?? [];
28
+ const exact = w.find(x => x.category === f.category && x.detail === f.detail);
29
+ if (exact)
30
+ return exact.weight;
31
+ const cat = w.find(x => x.category === f.category && x.detail === undefined);
32
+ if (cat)
33
+ return cat.weight;
34
+ return (0, findings_1.findingSpec)(f.category, f.detail)?.weight ?? f.weight ?? 0;
35
+ }
36
+ /** Highest weight in full, a quarter of each other kind's weight, plus combinations, capped at 100. */
37
+ function scoreFindings(findings, options) {
38
+ // Each kind of finding counts once, so twenty links of one kind do not add up to a critical file.
39
+ const byKind = new Map();
40
+ for (const f of findings) {
41
+ const k = `${f.category}/${f.detail}`;
42
+ byKind.set(k, Math.max(byKind.get(k) ?? 0, weightOf(f, options)));
43
+ }
44
+ const weights = [...byKind.values()].sort((a, b) => b - a);
45
+ let score = weights.length ? weights[0] + weights.slice(1).reduce((s, w) => s + w / 4, 0) : 0;
46
+ const has = (c, d) => findings.some(f => f.category === c && (d === undefined || f.detail === d) && weightOf(f, options) > 0);
47
+ for (const combo of exports.COMBINATIONS)
48
+ if (combo.test(has))
49
+ score += combo.weight;
50
+ score = Math.min(100, Math.round(score));
51
+ return { score, risk: riskFor(score, options.scoreBands ?? exports.DEFAULT_BANDS) };
52
+ }
53
+ function riskFor(score, bands) {
54
+ if (score >= bands.critical)
55
+ return types_1.OfficeRisk.Critical;
56
+ if (score >= bands.high)
57
+ return types_1.OfficeRisk.High;
58
+ if (score >= bands.medium)
59
+ return types_1.OfficeRisk.Medium;
60
+ if (score >= bands.low)
61
+ return types_1.OfficeRisk.Low;
62
+ return types_1.OfficeRisk.None;
63
+ }