@patrickdk77/office-defuse 0.2.2 → 0.2.4

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/README.md CHANGED
@@ -77,7 +77,7 @@ The format comes from the content, never from the file name.
77
77
  |---|---|---|---|
78
78
  | Word | .docx .docm .dotx .dotm | .doc .dot from Word 6.0 to 2003, and .rtf | Flat OPC .xml |
79
79
  | Excel | .xlsx .xlsm .xltx .xltm .xlam .xlsb | .xls .xlt .xla from Excel 5.0 to 2003 | Flat OPC .xml |
80
- | PowerPoint | .pptx .pptm .potx .potm .ppsx .ppsm .ppam .sldx .sldm | .ppt from PowerPoint 97 to 2003 | Flat OPC .xml |
80
+ | PowerPoint | .pptx .pptm .potx .potm .ppsx .ppsm .ppam .sldx .sldm | .ppt from PowerPoint 97 to 2003, and .ppa add-ins | Flat OPC .xml |
81
81
  | Visio | .vsdx .vsdm .vstx .vstm .vssx .vssm | .vsd from Visio 2000 to 2010 | |
82
82
  | XPS | .xps .oxps | | |
83
83
  | OpenDocument | .odt .ott .ods .ots .odp .otp .odg .otg .odf .odm .odb | | .fodt .fods .fodp .fodg |
@@ -91,7 +91,7 @@ A ZIP with both markers is rejected as PACKAGE/AMBIGUOUS_FORMAT, since Office co
91
91
 
92
92
  A .xlsb gets the package checks. Its sheets are binary records, and the formulas in them go unread. Its external link parts are binary too, and office-defuse reads their link records for DDE links. A .odb gets every ODF check, and the database under `database/` goes unread.
93
93
 
94
- A file that starts with the compound file signature is a compound file, MS-CFB. The streams and storages at its root name the format: `WordDocument` for Word, `Workbook` or `Book` for Excel, `PowerPoint Document`, `VisioDocument`, `__properties_version1.0` for an Outlook message, a `Quill` storage or Publisher's class id beside a `Contents` stream for Publisher, and a `Props` stream beside a storage named by a number for Project. Word's FIB says whether a document is a template, and Excel's records whether a workbook is a template or an add-in. Neither needs the password of an encrypted file. An Outlook template has a class id of its own. A PowerPoint 97 to 2003 template or show reports its format as .ppt, and a Visio stencil or template as .vsd.
94
+ A file that starts with the compound file signature is a compound file, MS-CFB. The streams and storages at its root name the format: `WordDocument` for Word, `Workbook` or `Book` for Excel, `PowerPoint Document`, `VisioDocument`, `__properties_version1.0` for an Outlook message, a `Quill` storage or Publisher's class id beside a `Contents` stream for Publisher, and a `Props` stream beside a storage named by a number for Project. Word's FIB says whether a document is a template, and Excel's records whether a workbook is a template or an add-in. Neither needs the password of an encrypted file. An Outlook template has a class id of its own, and so does a PowerPoint 97 to 2003 add-in, a .ppa, whose root is its VBA project. A PowerPoint 97 to 2003 template or show reports its format as .ppt, and a Visio stencil or template as .vsd.
95
95
 
96
96
  A compound file with two of these is PACKAGE/AMBIGUOUS_FORMAT. One with none is PACKAGE/UNRECOGNIZED, and so is a `WordDocument` stream that holds no Word 6.0 to 2003 FIB. A PowerPoint 4.0 file, a bare OLE object and a `vbaProject.bin` on its own all land there.
97
97
 
@@ -118,7 +118,7 @@ Detection does not claim a file whose format office-defuse cannot name. That cov
118
118
 
119
119
  In a ZIP package:
120
120
 
121
- - VBA projects, Excel 4.0 macro sheets, and OpenDocument `Basic/`, `Scripts/` and `Dialogs/` folders. These go by content type, relationship type or folder name, and a folder counts when it holds a file that is not empty. office-defuse parses no OLE part of a package, such as `vbaProject.bin`.
121
+ - VBA projects, Excel 4.0 macro sheets, and OpenDocument `Basic/`, `Scripts/` and `Dialogs/` folders. These go by content type, relationship type or folder name, and a folder counts when it holds a file that is not empty. office-defuse parses no OLE part of a package, such as `vbaProject.bin`, unless `scriptPlugins` is set. Then it reads the VBA project, as [Script plugins](#script-plugins) says.
122
122
  - OpenDocument script event listeners that name a script URL or macro. Those that fire as the document opens or is created get a finding of their own. LibreOffice ignores a listener whose URL and macro name are both empty, and so does office-defuse. Visio ShapeSheet cells that call RUNADDON, RUNADDONWARGS, CALLTHIS, RUNMACRO, OPENFILE or DOOLEVERB.
123
123
  - Links and actions that run macros: `vnd.sun.star.script:` and `macro:` targets, PowerPoint's `ppaction://macro`, Excel drawing shapes with a `macro` attribute, and form controls with an `x:FmlaMacro`. Ribbon callbacks such as `onLoad` and `onAction`. OpenDocument menu, toolbar and key settings under `Configurations2/`. LibreOffice writes an empty `Configurations2/accelerator/current.xml` into the documents it saves, and an empty file counts for nothing.
124
124
  - Excel defined names that run a macro sheet: Auto_Open and Auto_Activate as MACRO/EVENT_ON_OPEN, Auto_Close and Auto_Deactivate as MACRO/EVENT. A name counts when it starts with one of these, as `Auto_Open2` does.
@@ -131,7 +131,7 @@ In a ZIP package:
131
131
  - References that load from outside the file on their own: attached and OpenDocument templates, linked OLE objects and packages, images, frames, included files and linked sections, external workbooks, `meta:auto-reload`, and any other external relationship. A target on the machine itself is EXTERNAL/LOCAL_FILE. An empty OpenDocument reference is the document itself and names no file, so it goes unreported. A target counts as local only if its percent-decoded form is local too, because a CVE-2017-0199 sample writes its `script:` scheme as `%73cript:`.
132
132
  - Hyperlinks, with pdf-defuse's checks of the target: the scheme, file and network paths, data URLs, credentials before the host, IP hosts, punycode and mixed-script hosts, and percent-encoded hosts. A link to a place inside the document goes unreported, and so does an empty link, which is the document itself.
133
133
  - A safe link whose label is an address of another site than the one it opens is LINK/TEXT_MISMATCH. A label counts as an address when it is a web URL, a mail address, or a host with or without a path that starts with `www.` or ends in a known top-level domain. Text that only mentions an address names no site, and neither do Excel function names such as `BETA.DIST` and file names such as `moon.htm`, which look like hosts. Sites compare as in pdf-defuse: a subdomain or parent of the label's site matches, and `evil.co.uk` is not under `mybank.co.uk`. The labels are a Word hyperlink's text and tooltip, a HYPERLINK field's result and `\o` tooltip, a DrawingML run's text and a link's tooltip, an Excel hyperlink's display name and tooltip, an OpenDocument link's text and title, and the characters of an XPS Glyphs element. The rewrite removes the link and keeps its text.
134
- - Embedded OLE objects and packages, OLE files nothing declares as an object, Java applets, plug-ins and ActiveX controls.
134
+ - Embedded OLE objects and packages, OLE files nothing declares as an object, Java applets, plug-ins and ActiveX controls. A Forms 2.0 control whose persisted properties read as Office writes them is CONTROL/FORMS_CONTROL, `info`, and stays. See [ActiveX controls](#activex-controls).
135
135
  - Package structure: parts without a content type, parts no relationship reaches, files the ODF manifest does not list, an irregular ODF layout, and names a package gives twice.
136
136
  - Document type declarations in XML parts, and references to external entities. See [Document types and entities](#document-types-and-entities).
137
137
 
@@ -200,7 +200,7 @@ The check reads the output with the caller's overrides and limits, without `file
200
200
  - A part loses its root element only by going whole. Edits happen in place, and everything the rewrite does not touch keeps its characters, in the part's own encoding, UTF-8 or UTF-16, behind its own byte order mark.
201
201
  - A Word field a strip finding names loses its begin, separator and end marks and its code, nested fields included, and keeps its result. A `w:fldSimple` keeps the runs inside it.
202
202
  - An Excel 4.0 macro sheet becomes an empty worksheet of the same name, with the worksheet content type and relationship type, so the indexes of the sheets after it stay right. A defined name that runs a macro goes. So does a formula that calls DDE, and its cell keeps its cached value.
203
- - A drawing shape loses its `macro` attribute, a ribbon control its callbacks, and a PowerPoint shape an action that runs a program, a macro or an object verb. A Visio cell that calls code goes.
203
+ - A drawing shape loses its `macro` attribute, a ribbon control its callbacks, and a PowerPoint shape an action that runs a program, a macro or an object verb. A Visio cell that calls code goes. When script plugins keep the document's VBA project, a shape, form control, callback, action or MACROBUTTON field that runs one of its macros by name stays.
204
204
  - The main part keeps its content type, so a `.docm` without its VBA project is still a `.docm`.
205
205
  - In an OpenDocument package, `mimetype` comes first, stored, without an extra field, and holds the media type without white space. A package without one gets one, and a package without a manifest gets a manifest listing its entries. `Basic/`, `Scripts/`, `Dialogs/` and `Configurations2/` go whole. An entry goes with its manifest entry and with every element whose `xlink:href` names it. A link `text:a` or `draw:a` keeps what it holds. A form control loses its link, and a cell loses a formula that calls DDE. An embedded OLE object goes and the preview image beside it stays.
206
206
  - A document type declaration with an internal subset goes when XML/INTERNAL_SUBSET strips it. Each reference to an internal entity is written out as its text, markup included, and each start tag whose attributes the declarations changed or added to is written out whole, namespace declarations included. The part then reads as the inspection read it. References to external entities go when XML/EXTERNAL_ENTITY strips them. If that finding is overridden to `info` while the subset would go, the references still need their declarations, so the declaration stays and the check of the output reports it.
@@ -216,13 +216,50 @@ A Flat OPC package is rewritten as the package of its parts is, then written bac
216
216
 
217
217
  ### Compound files
218
218
 
219
- The rewrite writes a new compound file, version 3 with 512-byte sectors, without the streams and storages a strip finding names: VBA project storages, the storages of embedded objects, linked objects and ActiveX controls, Excel's `Ctls` stream, and Outlook attachments. A message's property stream counts its attachments, and the count drops by the ones that went.
219
+ The rewrite writes a new compound file, version 3 with 512-byte sectors, without the streams and storages a strip finding names: VBA project storages, the storages of embedded objects, linked objects and ActiveX controls, Excel's `Ctls` stream, Word's picture for printing a control, and Outlook attachments. A VBA project that script plugins keep is written without its compiled code. A message's property stream counts its attachments, and the count drops by the ones that went.
220
220
 
221
221
  The FAT comes right after the header, so no stream lands in the first 1024 bytes, where PDF readers look for a header. When a stream ends with a ZIP end record, as an embedded package does, free sectors of zeros follow the last stream until the record is more than 65,557 bytes from the end of the file, past where ZIP readers look.
222
222
 
223
223
  An Outlook message's RTF body with a strip finding is rewritten the way an RTF file is, and stored back uncompressed, which MS-OXRTFCP allows. A body that does not decompress, or one with a `reject` finding inside, goes, and the message keeps its plain text body. The property stream entry of a body that went goes too, and that of a rewritten body takes its new size. Every other stream is copied unchanged. Sectors no stream holds, the rest of each stream's last sector and data past the last sector are not written.
224
224
 
225
- Content inside a stream stays as it was. That covers Word's fields, template and WordBasic macros, Excel's records for macro sheets, defined names, links, hyperlinks and buttons, every record of a PowerPoint presentation along with the objects stored in its records, the ShapeSheet cells of a Visio drawing, and the RC4 encryption of a workbook. A finding there is still in the output, so the output fails its check and the upload is rejected.
225
+ Excel's and PowerPoint's records are written again, as the next two sections say. Content inside any other stream stays as it was: Word's fields, template and WordBasic macros, Excel's cell formulas, and the ShapeSheet cells of a Visio drawing. A finding there is still in the output, so the output fails its check and the upload is rejected.
226
+
227
+ ### Excel workbooks
228
+
229
+ A workbook stream, Excel 97 to 2003 or Excel 5.0 and 95, is written again record by record, in the order the stream holds them:
230
+
231
+ - An Excel 4.0 macro sheet becomes an empty worksheet of the same name and visibility, so formulas and names keep their sheet numbers.
232
+ - A name that runs a macro, such as Auto_Open, is renamed `Removed_Auto_Open_1` and so on, and is no longer a built-in name. Formulas refer to names by number, so removing it would point them at the wrong name.
233
+ - A button or shape loses the macro it runs, unless the workbook's VBA project stays and the macro is one of its own: one name of the SupBook that is the workbook itself, as Excel names a VBA macro. A defined name can stand for anything, so a button that runs one loses it.
234
+ - A hyperlink goes with its tooltip.
235
+ - An external workbook, DDE or OLE link keeps its SupBook record, which formulas count to by number, with a path that is the workbook itself, U+0002.
236
+ - A workbook encrypted with VelvetSweatshop is written decrypted, without its FILEPASS record. So is any encrypted workbook that is written again, since office-defuse does not encrypt.
237
+
238
+ Records change size, so the stream positions other records hold are written anew: each sheet's in BoundSheet8, the DefColWidth and DBCell positions in Index, a DBCell's offset back to its first row, and ExtSST's string positions. A position that lands where no record starts, or inside a record that changed, leaves the stream as it was, and the output check rejects the upload. Excel 5.0 and 95 workbooks get the sheet and name edits only. A cell formula that calls EXEC, CALL or WEBSERVICE is not written again.
239
+
240
+ ### PowerPoint presentations
241
+
242
+ A presentation is written as a full save: each persist object the current edit reaches, in persist id order, then one persist directory and one UserEditAtom with no edit before it, which the Current User stream points at. A container's length follows the records left in it.
243
+
244
+ - An embedded object, a linked object or an ActiveX control that goes takes its ExOleObjStg and its container in ExObjList with it. A Forms 2.0 control stays. An object a file plugin rewrote takes the plugin's bytes, compressed again when PowerPoint stored it compressed.
245
+ - A VBA project that goes takes its VBAInfoContainer with it. One that script plugins keep is written without its compiled code inside its compressed storage.
246
+ - An action that runs a program, a macro or an OLE verb becomes no action. A macro of a kept VBA project keeps its action.
247
+ - A hyperlink that goes takes its ExHyperlinkContainer, and the actions that open it become no action.
248
+ - A linked movie or sound loses its path.
249
+ - An encrypted presentation that is written again is written decrypted, without its encryption session. Its Pictures and EncryptedSummary streams are encrypted too, and office-defuse does not decrypt them, so a presentation that has either stays as it was and the output check rejects it.
250
+
251
+ ## ActiveX controls
252
+
253
+ An ActiveX control runs whatever code its class names, so a control goes unless office-defuse can tell it is one of the Forms 2.0 controls Office ships, as Office persisted it: CommandButton, Label, TextBox, ListBox, ComboBox, CheckBox, OptionButton, ToggleButton, SpinButton or ScrollBar. Such a control is CONTROL/FORMS_CONTROL, `info`, and stays, with or without a VBA project. An override to `strip` removes it like any other control.
254
+
255
+ office-defuse reads the control's properties as MS-OFORMS lays them out and as LibreOffice reads them, and keeps the control when:
256
+
257
+ - its class id, or the ProgID its CompObj, ax:ocx element or Obj record names, is one of those controls;
258
+ - every bit of its property mask names a property MS-OFORMS defines;
259
+ - it holds no picture or mouse icon, whose bytes are a picture file Office decodes;
260
+ - its properties, its font and, for a list, its column infos fill its data to the last byte.
261
+
262
+ The control can sit in an OOXML activeX part, persisted as a stream that starts with the class id, as a storage with a contents stream, or as a property bag with plain values. In a Word document it is a storage under ObjectPool, and its `\x03PRINT` stream, a Windows metafile Word prints in its place, goes as CONTROL/CONTROL_PICTURE. In an Excel workbook every control is in one `Ctls` stream, which stays only when the Obj records place Forms 2.0 controls over all of it. Any other control, a Forms 2.0 control with a picture, and a frame, multipage or tab strip are CONTROL/ACTIVEX with `data.reason`.
226
263
 
227
264
  ### RTF
228
265
 
@@ -262,6 +299,40 @@ const { disarmOffice, officePlugin } = require('@patrickdk77/office-defuse');
262
299
  await disarmOffice(bytes, { filePlugins: [officePlugin()] });
263
300
  ```
264
301
 
302
+ ## Script plugins
303
+
304
+ `scriptPlugins` takes script plugins that decide about VBA. office-defuse reads a VBA project's dir stream and modules as MS-OVBA describes them, and shows each module to the plugins as a `ContainedScript`:
305
+
306
+ | Field | What it holds |
307
+ |---|---|
308
+ | `text` | The module's source, decoded with the project's code page |
309
+ | `trigger` | `vba-document`, `vba-standard`, `vba-class` or `vba-form`, as the PROJECT stream lists the module |
310
+ | `location` | Where the module sits, such as `xl/vbaProject.bin > VBA/Module1` |
311
+ | `fieldName` | The module's name |
312
+ | `language`, `form` | `vba` and `office` |
313
+ | `references` | On the first module only, the project's references, each `{ name, kind, libid }` |
314
+
315
+ The first plugin whose `accepts` returns true decides a module. A module it passes gets MACRO/PLUGIN_PASSED, `info`. Any other answer, `scrubbed` included, is MACRO/PLUGIN_REMOVED, and an error is MACRO/PLUGIN_FAILED, both `strip`, since office-defuse writes no module a plugin rewrote. The plugin's findings go into the report as they are.
316
+
317
+ The project stays when every module passed. Otherwise it goes, as it does without plugins, and so does a project with a module no plugin took. A project that stays is written without its compiled code, the way Mandiant's OfficePurge writes one. Each module stream holds only its source, compressed again, the dir stream says each source starts at 0, `_VBA_PROJECT` names no VBA version, and the `__SRP_` caches go. Office then compiles the source the plugins read, so p-code that does something other than its source, which VBA stomping relies on, never runs. MACRO/COMPILED_CODE, `strip`, reports the purge, and MACRO/VBA_PROJECT turns to `info`.
318
+
319
+ A user form stays with its project when its designer storage holds Forms 2.0 controls only. The designer must be the UserForm designer, the storages under it must be Forms 2.0 forms, frames and multipages, and every form's class table must be empty, since a class table can name any ActiveX control. A form that fails gets CONTROL/ACTIVEX at its designer, and while that finding strips, the project goes.
320
+
321
+ When the document's own project stays, so does what runs its macros by name: an Excel shape's `macro`, a form control's `x:FmlaMacro`, a ribbon callback, PowerPoint's `ppaction://macro` and Word's MACROBUTTON field. Their findings turn to `info`. A name that points into another file, as `'other.xlsm'!Macro`, still goes.
322
+
323
+ office-defuse decides the `vbaProject.bin` part of an OOXML package, which with `scriptPlugins` set, even to an empty list, goes to the script plugins and not to the file plugins. It decides the project storages of a Word document, a workbook, a PowerPoint 97 presentation, a .ppa add-in and the other compound files. A project inside an embedded object is for the file plugins that take the object.
324
+
325
+ A source that does not decompress, or a dir stream that names a module stream that is not there, leaves nothing to check. The project goes, and MACRO/VBA_PROJECT's `data.reason` says why. The check of the output does not ask the plugins again. It matches the project by the MD5 of its references and module sources, reports the plugins' findings again, and reports MACRO/COMPILED_CODE if the output still holds p-code, which fails the check.
326
+
327
+ [script-defuse](https://github.com/patrickdk77/script-defuse)'s `formScriptPlugin` is a script plugin for VBA:
328
+
329
+ ```js
330
+ const { disarmOffice } = require('@patrickdk77/office-defuse');
331
+ const { formScriptPlugin } = require('@patrickdk77/script-defuse');
332
+
333
+ await disarmOffice(bytes, { scriptPlugins: [formScriptPlugin()] });
334
+ ```
335
+
265
336
  ## The containers
266
337
 
267
338
  The ZIP reader starts from the end record and the central directory. It checks each local header against the central directory before it reads any content. It refuses these, and stops the run:
@@ -402,6 +473,7 @@ The bands are NONE at 0, LOW from 1, MEDIUM from 25, HIGH from 50 and CRITICAL f
402
473
  | `actionOverrides` | none | Change the action for a category, or for a category and detail |
403
474
  | `scoreWeights`, `scoreBands` | built in | Change the scoring |
404
475
  | `filePlugins` | none | Plugins that decide about the files inside the file. See [File plugins](#file-plugins) |
476
+ | `scriptPlugins` | none | Plugins that decide about the modules of a VBA project. See [Script plugins](#script-plugins) |
405
477
  | `tempDir` | `os.tmpdir()` | Where temporary files go |
406
478
  | `memoryThreshold` | 8 MiB | Disarm keeps its copy of the upload, its output and each file it shows a plugin in memory up to this size, and in a temporary file past it. The copies plugins return share this much memory while they wait for the rewrite |
407
479
 
@@ -447,8 +519,8 @@ Exit codes are 0 for clean, 1 for defused or strippable, 2 for rejected and 3 fo
447
519
 
448
520
  ## What it does not check
449
521
 
450
- - The OLE parts of a ZIP package, such as `vbaProject.bin`, the objects inside `oleObject*.bin`, and ActiveX binaries. office-defuse hands them to the file plugins and reads none of them itself.
451
- - VBA source. A VBA project is reported whole, and its modules go unread.
522
+ - The OLE parts of a ZIP package, such as the objects inside `oleObject*.bin`, the binaries of ActiveX controls other than Forms 2.0 ones, and `vbaProject.bin` without script plugins. office-defuse hands them to the file plugins and reads none of them itself.
523
+ - VBA source. office-defuse reads the modules only to show them to script plugins, and without plugins reports a project whole. In a user form that stays, the controls' data and pictures in its `o` stream go unread.
452
524
  - Encrypted Word 6.0 and 95 documents. They are rejected. In a Word document encrypted with RC4 CryptoAPI, the OLE objects that Word keeps in the Data stream rather than in ObjectPool. The pictures of an encrypted presentation.
453
525
  - Formulas in .xlsb sheets, and in an Excel 5.0 or 95 workbook. A DDE link that a formula uses is found in the workbook's link records.
454
526
  - Hyperlinks in a Visio .vsd, ShapeSheet cells in a drawing from Visio 5.0 or earlier, and anything in Publisher and Project files but VBA projects and objects.
package/dist/biff.d.ts CHANGED
@@ -24,6 +24,8 @@ export interface BiffRecord {
24
24
  /** A sheet as BoundSheet8 (or BIFF5's BOUNDSHEET) lists it. */
25
25
  export interface Sheet {
26
26
  position: number;
27
+ /** Where its BoundSheet record starts in the stream. */
28
+ record?: number;
27
29
  /** 0 visible, 1 hidden, 2 very hidden. */
28
30
  state: number;
29
31
  /** 0 worksheet or dialog, 1 macro sheet, 2 chart, 6 VBA module. */
@@ -42,6 +44,15 @@ export declare class RecordReader {
42
44
  constructor(cfb: CompoundFile, stream: CfbEntry, start: number, deadline?: number | undefined);
43
45
  next(): Promise<BiffRecord | undefined>;
44
46
  }
47
+ /**
48
+ * The subrecords of a BIFF8 Obj record (MS-XLS 2.4.181), each with its type, where its header starts and its data. The
49
+ * walk stops at FtEnd, and at FtLbsData, whose length does not count all its data.
50
+ */
51
+ export declare function objSubrecords(b: Uint8Array): Generator<{
52
+ ft: number;
53
+ at: number;
54
+ data: Uint8Array;
55
+ }>;
45
56
  /** BIFF8 writes strings with a flag byte: 8-bit characters, or 16-bit ones when its low bit is set. */
46
57
  export declare function xlString(b: Uint8Array, at: number, chars: number): {
47
58
  text: string;
package/dist/biff.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RecordReader = exports.R = void 0;
4
+ exports.objSubrecords = objSubrecords;
4
5
  exports.xlString = xlString;
5
6
  exports.parseBoundSheet = parseBoundSheet;
6
7
  exports.parseLbl = parseLbl;
@@ -56,6 +57,20 @@ class RecordReader {
56
57
  }
57
58
  }
58
59
  exports.RecordReader = RecordReader;
60
+ /**
61
+ * The subrecords of a BIFF8 Obj record (MS-XLS 2.4.181), each with its type, where its header starts and its data. The
62
+ * walk stops at FtEnd, and at FtLbsData, whose length does not count all its data.
63
+ */
64
+ function* objSubrecords(b) {
65
+ for (let at = 0; at + 4 <= b.length;) {
66
+ const ft = b[at] | (b[at + 1] << 8);
67
+ const cb = b[at + 2] | (b[at + 3] << 8);
68
+ if (ft === 0x00 || ft === 0x13)
69
+ return;
70
+ yield { ft, at, data: b.subarray(at + 4, at + 4 + cb) };
71
+ at += 4 + cb;
72
+ }
73
+ }
59
74
  /** BIFF8 writes strings with a flag byte: 8-bit characters, or 16-bit ones when its low bit is set. */
60
75
  function xlString(b, at, chars) {
61
76
  if (at >= b.length)
@@ -22,6 +22,11 @@ export interface InnerFile {
22
22
  /** The file's bytes, in order. */
23
23
  chunks(): AsyncIterable<Uint8Array>;
24
24
  }
25
+ /**
26
+ * Throws on an error a plugin threw that stops the whole tree. An I/O error fails this run as one of its own would,
27
+ * so its cause goes up.
28
+ */
29
+ export declare function passOn(run: RunState, e: unknown): void;
25
30
  /**
26
31
  * Adds the findings a plugin reported from inside a file, labeled with the file's name and place. A reject inside the
27
32
  * file means the file goes, not the one that holds it, so it is recorded here as strip.
package/dist/contained.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RUN_OPTIONS = void 0;
4
+ exports.passOn = passOn;
4
5
  exports.pushContained = pushContained;
5
6
  exports.decideContained = decideContained;
6
7
  exports.presence = presence;
package/dist/context.d.ts CHANGED
@@ -77,7 +77,17 @@ export interface RunState {
77
77
  sources: ByteSource[];
78
78
  /** The container, once the format rules have read it. */
79
79
  output?: Output;
80
+ /**
81
+ * The document's own VBA project stays, as the script plugins passed it. The buttons, shapes, callbacks and fields
82
+ * that run one of its macros by name then stay too.
83
+ */
84
+ macrosKept?: boolean;
80
85
  }
86
+ /**
87
+ * 'info' for a link or callback that runs one of the document's own macros when its project stays, so the finding
88
+ * reports it without removing it. Undefined otherwise, leaving the action to the overrides.
89
+ */
90
+ export declare function ownMacro(run: RunState, name: string): 'info' | undefined;
81
91
  /** A finding the rules made, with its action, which the rewrite reads. Past 200 of one kind, `finding` is absent. */
82
92
  export interface Reported {
83
93
  action: OfficeFindingAction;
package/dist/context.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ZipContext = exports.PlanContext = exports.RunContext = exports.clip = void 0;
4
+ exports.ownMacro = ownMacro;
4
5
  const links_1 = require("./links");
5
6
  const plan_1 = require("./plan");
6
7
  const types_1 = require("./types");
@@ -12,6 +13,15 @@ const clip = (raw, n = 200) => {
12
13
  return s.length > n ? `${s.slice(0, n - 3)}...` : s;
13
14
  };
14
15
  exports.clip = clip;
16
+ /** A macro of the document's own project, by name: Macro, Module.Macro, or Excel's [0]!Macro for this workbook. */
17
+ const OWN_MACRO = /^(?:\[0\]!)?[\p{L}_][\p{L}\p{N}_]*(?:\.[\p{L}_][\p{L}\p{N}_]*)?$/u;
18
+ /**
19
+ * 'info' for a link or callback that runs one of the document's own macros when its project stays, so the finding
20
+ * reports it without removing it. Undefined otherwise, leaving the action to the overrides.
21
+ */
22
+ function ownMacro(run, name) {
23
+ return run.macrosKept && OWN_MACRO.test(name.trim()) ? 'info' : undefined;
24
+ }
15
25
  /** What one inspection holds while the format rules run. */
16
26
  class RunContext {
17
27
  factory;
package/dist/engine.js CHANGED
@@ -401,6 +401,8 @@ async function disarmAtDepth(source, sink, options, depth, deadline = options.li
401
401
  }
402
402
  // The output is checked with the caller's overrides and limits, and the plugins' verdicts in place of the plugins.
403
403
  const verifyOptions = {
404
+ // No plugin is asked again. An empty list still reads VBA project parts the way the upload's run did.
405
+ scriptPlugins: options.scriptPlugins && [],
404
406
  actionOverrides: options.actionOverrides,
405
407
  scoreWeights: options.scoreWeights,
406
408
  scoreBands: options.scoreBands,
package/dist/fields.js CHANGED
@@ -29,7 +29,7 @@ function evaluateField(ctx, location, code, computed, result = '') {
29
29
  if (name === 'DDE' || name === 'DDEAUTO')
30
30
  actions.push(ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Field, location, data).action);
31
31
  else if (name === 'MACROBUTTON')
32
- actions.push(ctx.report(types_1.OfficeCategory.Field, types_1.OfficeDetail.Macrobutton, location, { ...data, macro: (0, context_1.clip)(words[1]?.text ?? '') }).action);
32
+ actions.push(ctx.report(types_1.OfficeCategory.Field, types_1.OfficeDetail.Macrobutton, location, { ...data, macro: (0, context_1.clip)(words[1]?.text ?? '') }, (0, context_1.ownMacro)(ctx.run, words[1]?.text ?? '')).action);
33
33
  else if (name === 'INCLUDETEXT' || name === 'INCLUDEPICTURE' || name === 'IMPORT' || name === 'LINK') {
34
34
  const target = arg(name === 'LINK' ? 2 : 1);
35
35
  if (target === undefined)
package/dist/findings.js CHANGED
@@ -75,6 +75,10 @@ def(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.XorObfuscation, 'info
75
75
  def(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.UnsupportedAlgorithm, 'reject', 0, 'The file is encrypted with an algorithm office-defuse does not decrypt<: {algorithm}|>');
76
76
  // MACRO
77
77
  def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, 'strip', 70, 'The document holds VBA macros');
78
+ def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.CompiledCode, 'strip', 10, "The VBA project's compiled code was removed, so Office compiles the source the plugins checked");
79
+ def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.PluginPassed, 'info', 30, 'A VBA module was kept by <plugin {plugin}|a plugin>');
80
+ def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.PluginRemoved, 'strip', 0, '<Plugin {plugin}|A plugin> did not keep a VBA module, so the VBA project was removed');
81
+ def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.PluginFailed, 'strip', 0, '<Plugin {plugin}|A plugin> failed on a VBA module, so the VBA project was removed');
78
82
  def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.XlmMacrosheet, 'strip', 70, 'The workbook holds an Excel 4.0 macro sheet');
79
83
  def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.WordBasic, 'strip', 70, 'The Word 6.0 or 95 document holds WordBasic macros, which Word runs as VBA');
80
84
  def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.OdfBasic, 'strip', 70, 'The document holds Basic macros');
@@ -138,7 +142,9 @@ def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.PluginScrubbed, 'info'
138
142
  def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.PluginFailed, 'strip', 0, '<Plugin {plugin}|A plugin> failed, so the embedded file was removed');
139
143
  def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.PluginRemoved, 'strip', 0, 'An embedded file was removed by <plugin {plugin}|a plugin>');
140
144
  // CONTROL
141
- def(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, 'strip', 50, 'An ActiveX control');
145
+ def(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, 'strip', 50, 'An ActiveX control<: {reason}|>');
146
+ def(types_1.OfficeCategory.Control, types_1.OfficeDetail.FormsControl, 'info', 10, 'A Forms 2.0 control<, {className}|>, whose persisted properties office-defuse read');
147
+ def(types_1.OfficeCategory.Control, types_1.OfficeDetail.ControlPicture, 'strip', 10, "A control's picture for printing, a Windows metafile");
142
148
  // PROCESSING
143
149
  def(types_1.OfficeCategory.Processing, types_1.OfficeDetail.VerificationFailed, 'reject', 0, 'The defused output did not pass its own inspection, so nothing was written');
144
150
  /** The built-in spec for a kind of finding, or undefined for a kind office-defuse does not make, such as one from another package. */
@@ -0,0 +1,51 @@
1
+ import { type CfbEntry, type CompoundFile } from './cfb';
2
+ /** The number of classes in the class table of a FormControl (MS-OFORMS 2.2.10.1), the `f` stream. */
3
+ export declare function classTableCount(f: Uint8Array): number;
4
+ /**
5
+ * Why a user form's designer storage may hold something other than Forms 2.0 controls, or undefined when it holds
6
+ * only those: the UserForm designer, storages of Forms 2.0 classes, and forms whose class tables are empty.
7
+ */
8
+ export declare function designerProblem(cfb: CompoundFile, storage: CfbEntry, limit: number): Promise<string | undefined>;
9
+ /**
10
+ * The properties a Forms 2.0 control persists, in the order of its property mask (MS-OFORMS 2.2). Each takes the
11
+ * next bit. A number sits in the data block, aligned to its size; a string or a pair in the extra data block; a
12
+ * picture in the stream data; a flag holds no data. A bit MS-OFORMS leaves undefined must be clear.
13
+ */
14
+ type Prop = 'u8' | 'u16' | 'u32' | 'pair' | 'string' | 'picture' | 'flag' | 'undefined';
15
+ interface ControlLayout {
16
+ props: Prop[];
17
+ /** The property mask is 64 bits wide. */
18
+ wide: boolean;
19
+ text: boolean;
20
+ }
21
+ /** The Forms 2.0 controls office-defuse reads, by class id. Frames, multipages and tab strips hold other controls and are not here. */
22
+ export declare const FORMS_CONTROLS: ReadonlyMap<string, ControlLayout>;
23
+ /** The ProgIDs of those controls, which a property bag or CompObj may name. */
24
+ export declare const FORMS_PROGIDS: ReadonlyMap<string, string>;
25
+ /**
26
+ * Why the persisted data of a Forms 2.0 control may not be what Forms 2.0 writes, or undefined when it is: the
27
+ * control's property block, then its TextProps for a control with text, then the column infos a list control counts,
28
+ * and nothing after. A picture or a mouse icon counts as a problem, since its bytes are a picture file Office decodes
29
+ * and office-defuse cannot rewrite in place.
30
+ */
31
+ export declare function controlProblem(classId: string, data: Uint8Array): string | undefined;
32
+ /**
33
+ * Why a Forms 2.0 control persisted as a property bag in its activeX part may hold more than plain values, or
34
+ * undefined when it does not. `props` are the bag's name and value pairs, `nested` says ax:ocx held any other element,
35
+ * inside an ax:ocxPr or beside one, such as a font or a picture.
36
+ */
37
+ export declare function bagProblem(classId: string, props: ReadonlyArray<{
38
+ name: string;
39
+ value: string;
40
+ }>, nested: boolean): string | undefined;
41
+ /** Streams a control's storage holds in an OOXML binary part: its class and its properties. */
42
+ export declare const OOXML_CONTROL_STREAMS: ReadonlySet<string>;
43
+ /** In a Word document, the storage also holds the control's name and its picture for printing. */
44
+ export declare const WORD_CONTROL_STREAMS: ReadonlySet<string>;
45
+ /**
46
+ * Why a control's storage may not hold a Forms 2.0 control as Office persists one, or undefined when it does. Its
47
+ * class is the storage's class id, or its CompObj's ProgID when that is null, and must be `classId` when given. It
48
+ * holds only the `allowed` streams and a contents stream that `controlProblem` passes.
49
+ */
50
+ export declare function controlStorageProblem(cfb: CompoundFile, storage: CfbEntry, allowed: ReadonlySet<string>, classId?: string): Promise<string | undefined>;
51
+ export {};