@patrickdk77/office-defuse 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +230 -2
- package/dist/context.d.ts +38 -0
- package/dist/context.js +100 -0
- package/dist/crc32.d.ts +2 -0
- package/dist/crc32.js +18 -0
- package/dist/engine.d.ts +5 -0
- package/dist/engine.js +151 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.js +33 -0
- package/dist/findings.d.ts +25 -0
- package/dist/findings.js +169 -0
- package/dist/formats.d.ts +10 -0
- package/dist/formats.js +46 -0
- package/dist/formulas.d.ts +22 -0
- package/dist/formulas.js +77 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +30 -0
- package/dist/index.mjs +3 -0
- package/dist/io.d.ts +23 -0
- package/dist/io.js +139 -0
- package/dist/links.d.ts +26 -0
- package/dist/links.js +145 -0
- package/dist/odf.d.ts +3 -0
- package/dist/odf.js +297 -0
- package/dist/ooxml.d.ts +8 -0
- package/dist/ooxml.js +516 -0
- package/dist/score.d.ts +17 -0
- package/dist/score.js +63 -0
- package/dist/types.d.ts +247 -0
- package/dist/types.js +133 -0
- package/dist/xlsb.d.ts +19 -0
- package/dist/xlsb.js +101 -0
- package/dist/xml.d.ts +256 -0
- package/dist/xml.js +1105 -0
- package/dist/zip-writer.d.ts +38 -0
- package/dist/zip-writer.js +182 -0
- package/dist/zip.d.ts +55 -0
- package/dist/zip.js +535 -0
- package/package.json +96 -4
package/dist/ooxml.js
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inspectOoxml = inspectOoxml;
|
|
4
|
+
const context_1 = require("./context");
|
|
5
|
+
const errors_1 = require("./errors");
|
|
6
|
+
const formats_1 = require("./formats");
|
|
7
|
+
const formulas_1 = require("./formulas");
|
|
8
|
+
const links_1 = require("./links");
|
|
9
|
+
const types_1 = require("./types");
|
|
10
|
+
const xlsb_1 = require("./xlsb");
|
|
11
|
+
const xml_1 = require("./xml");
|
|
12
|
+
const zip_1 = require("./zip");
|
|
13
|
+
const CONTENT_TYPES_NS = 'http://schemas.openxmlformats.org/package/2006/content-types';
|
|
14
|
+
const RELS_NS = 'http://schemas.openxmlformats.org/package/2006/relationships';
|
|
15
|
+
const T = 'http://schemas.openxmlformats.org/officedocument/2006/relationships/';
|
|
16
|
+
const S = 'http://purl.oclc.org/ooxml/officedocument/relationships/';
|
|
17
|
+
const MS = 'http://schemas.microsoft.com/office/2006/relationships/';
|
|
18
|
+
const both = (name) => [T + name, S + name];
|
|
19
|
+
/** Relationship types, lowercased, as OPC compares them without regard to case. */
|
|
20
|
+
const REL = {
|
|
21
|
+
main: new Set([...both('officedocument'), 'http://schemas.microsoft.com/visio/2010/relationships/document']),
|
|
22
|
+
hyperlink: new Set(both('hyperlink')),
|
|
23
|
+
template: new Set(both('attachedtemplate')),
|
|
24
|
+
image: new Set([...both('image'), 'http://schemas.microsoft.com/office/2007/relationships/hdphoto']),
|
|
25
|
+
frame: new Set(both('frame')),
|
|
26
|
+
subDocument: new Set(both('subdocument')),
|
|
27
|
+
workbook: new Set([
|
|
28
|
+
...both('externallinkpath'),
|
|
29
|
+
`${MS}xlexternallinkpath/xlpathmissing`,
|
|
30
|
+
`${MS}xlexternallinkpath/xlstartup`,
|
|
31
|
+
`${MS}xlexternallinkpath/xlalternatestartup`,
|
|
32
|
+
`${MS}xlexternallinkpath/xllibrary`,
|
|
33
|
+
]),
|
|
34
|
+
};
|
|
35
|
+
/** Relationship types that say what the part they reach is. Each is a bit of Part.reachedAs. */
|
|
36
|
+
const AS = { vba: 1, macrosheet: 2, ole: 4, package: 8, control: 16, controlBinary: 32 };
|
|
37
|
+
const REACHED_AS = new Map([
|
|
38
|
+
[`${MS}vbaproject`, AS.vba],
|
|
39
|
+
[`${MS}xlmacrosheet`, AS.macrosheet],
|
|
40
|
+
[`${MS}xlintlmacrosheet`, AS.macrosheet],
|
|
41
|
+
...both('oleobject').map((t) => [t, AS.ole]),
|
|
42
|
+
...both('package').map((t) => [t, AS.package]),
|
|
43
|
+
...both('control').map((t) => [t, AS.control]),
|
|
44
|
+
[`${MS}activexcontrolbinary`, AS.controlBinary],
|
|
45
|
+
]);
|
|
46
|
+
/** Content types, lowercased. */
|
|
47
|
+
const TYPE = {
|
|
48
|
+
vba: new Set(['application/vnd.ms-office.vbaproject']),
|
|
49
|
+
macrosheet: new Set(['application/vnd.ms-excel.macrosheet+xml', 'application/vnd.ms-excel.intlmacrosheet+xml', 'application/vnd.ms-excel.macrosheet', 'application/vnd.ms-excel.intlmacrosheet']),
|
|
50
|
+
ole: new Set(['application/vnd.openxmlformats-officedocument.oleobject']),
|
|
51
|
+
control: new Set(['application/vnd.ms-office.activex+xml']),
|
|
52
|
+
controlBinary: new Set(['application/vnd.ms-office.activex']),
|
|
53
|
+
vml: 'application/vnd.openxmlformats-officedocument.vmldrawing',
|
|
54
|
+
xlsbExternalLink: 'application/vnd.ms-excel.externallink',
|
|
55
|
+
};
|
|
56
|
+
/** Relationship types that leave the package, by the finding they make. Any other is EXTERNAL/OTHER. */
|
|
57
|
+
const EXTERNAL_KINDS = [
|
|
58
|
+
[REL.template, types_1.OfficeDetail.Template],
|
|
59
|
+
[new Set([...both('oleobject'), ...both('package')]), types_1.OfficeDetail.Object],
|
|
60
|
+
[REL.image, types_1.OfficeDetail.Image],
|
|
61
|
+
[REL.frame, types_1.OfficeDetail.Frame],
|
|
62
|
+
[REL.subDocument, types_1.OfficeDetail.Include],
|
|
63
|
+
[REL.workbook, types_1.OfficeDetail.Workbook],
|
|
64
|
+
];
|
|
65
|
+
const WORD_NS = new Set(['http://schemas.openxmlformats.org/wordprocessingml/2006/main', 'http://purl.oclc.org/ooxml/wordprocessingml/main']);
|
|
66
|
+
const SHEET_NS = new Set(['http://schemas.openxmlformats.org/spreadsheetml/2006/main', 'http://purl.oclc.org/ooxml/spreadsheetml/main']);
|
|
67
|
+
const XDR_NS = new Set(['http://schemas.openxmlformats.org/drawingml/2006/spreadsheetdrawing', 'http://purl.oclc.org/ooxml/drawingml/spreadsheetdrawing']);
|
|
68
|
+
const R_NS = new Set(['http://schemas.openxmlformats.org/officedocument/2006/relationships', 'http://purl.oclc.org/ooxml/officedocument/relationships']);
|
|
69
|
+
const CUSTOM_UI_NS = new Set(['http://schemas.microsoft.com/office/2006/01/customui', 'http://schemas.microsoft.com/office/2009/07/customui']);
|
|
70
|
+
const VML_EXCEL_NS = 'urn:schemas-microsoft-com:office:excel';
|
|
71
|
+
const VISIO_NS = 'http://schemas.microsoft.com/office/visio/2012/main';
|
|
72
|
+
const VISIO_CODE = /\b(RUNADDON|RUNADDONWARGS|CALLTHIS|RUNMACRO|OPENFILE|DOOLEVERB)\s*\(/i;
|
|
73
|
+
/** How much of a field code is kept, counted from its first word, which names the field. */
|
|
74
|
+
const MAX_FIELD_CODE = 4096;
|
|
75
|
+
/** A Word attribute, qualified as Word writes it or unqualified. */
|
|
76
|
+
const wordAttr = (el, local) => el.attributes.find(a => a.local === local && (a.ns === '' || WORD_NS.has(a.ns.toLowerCase())))?.value;
|
|
77
|
+
/** The key of the part a relationships part belongs to, '' for the package, or undefined for any other part. */
|
|
78
|
+
const relsSource = (relsName) => {
|
|
79
|
+
const m = /^(.*?)_rels\/([^/]*)\.rels$/i.exec(relsName);
|
|
80
|
+
return m ? (0, zip_1.nameKey)(m[1] + m[2]) : undefined;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Resolves an internal target against the folder of the part it belongs to, as RFC 3986 resolves a relative
|
|
84
|
+
* reference: a ".." at the root of the package stays at the root. LibreOffice writes "../customXml/item1.xml" in the
|
|
85
|
+
* package relationships of workbooks it saves.
|
|
86
|
+
*/
|
|
87
|
+
function resolveTarget(relsName, target) {
|
|
88
|
+
const folder = relsName.slice(0, relsName.toLowerCase().indexOf('_rels/'));
|
|
89
|
+
const path = (target.startsWith('/') ? target.slice(1) : folder + target).split('#')[0];
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const seg of path.split('/')) {
|
|
92
|
+
if (seg === '..')
|
|
93
|
+
out.pop();
|
|
94
|
+
else if (seg !== '.' && seg !== '')
|
|
95
|
+
out.push(seg);
|
|
96
|
+
}
|
|
97
|
+
return (0, zip_1.nameKey)(out.join('/'));
|
|
98
|
+
}
|
|
99
|
+
const duplicate = (ctx, location, kind, name) => ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.DuplicateEntry, location, { kind, name: (0, context_1.clip)(name) });
|
|
100
|
+
/**
|
|
101
|
+
* The defaults and overrides of [Content_Types].xml. Only those that type an entry of the archive are kept, so a
|
|
102
|
+
* part full of declarations costs no memory. Two that type the same entry make PACKAGE/DUPLICATE_ENTRY.
|
|
103
|
+
*/
|
|
104
|
+
async function readContentTypes(ctx, entry) {
|
|
105
|
+
const keys = new Set(ctx.zip.entries.map(e => (0, zip_1.nameKey)(e.name)));
|
|
106
|
+
const extensions = new Set(ctx.zip.entries.map(e => /\.([^./]*)$/.exec(e.name)?.[1]?.toLowerCase() ?? ''));
|
|
107
|
+
const defaults = new Map();
|
|
108
|
+
const overrides = new Map();
|
|
109
|
+
await ctx.parse(entry, {
|
|
110
|
+
start(el) {
|
|
111
|
+
if (el.ns.toLowerCase() !== CONTENT_TYPES_NS)
|
|
112
|
+
return;
|
|
113
|
+
const type = (0, xml_1.attr)(el, '', 'ContentType')?.trim().toLowerCase();
|
|
114
|
+
if (type === undefined)
|
|
115
|
+
return;
|
|
116
|
+
if (el.local === 'Default') {
|
|
117
|
+
const ext = (0, xml_1.attr)(el, '', 'Extension')?.toLowerCase();
|
|
118
|
+
if (ext === undefined || !extensions.has(ext))
|
|
119
|
+
return;
|
|
120
|
+
if (defaults.has(ext))
|
|
121
|
+
duplicate(ctx, entry.name, 'content type default', ext);
|
|
122
|
+
defaults.set(ext, type);
|
|
123
|
+
}
|
|
124
|
+
else if (el.local === 'Override') {
|
|
125
|
+
const name = (0, xml_1.attr)(el, '', 'PartName');
|
|
126
|
+
if (name === undefined)
|
|
127
|
+
return;
|
|
128
|
+
const key = (0, zip_1.nameKey)(name.replace(/^\//, ''));
|
|
129
|
+
if (!keys.has(key))
|
|
130
|
+
return;
|
|
131
|
+
if (overrides.has(key))
|
|
132
|
+
duplicate(ctx, entry.name, 'content type override', name);
|
|
133
|
+
overrides.set(key, type);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
return { defaults, overrides };
|
|
138
|
+
}
|
|
139
|
+
/** Reads a relationships part and hands each relationship to `each` as it is read. Ids given twice are reported. */
|
|
140
|
+
async function readRelationships(ctx, entry, each) {
|
|
141
|
+
const ids = new Set();
|
|
142
|
+
await ctx.parse(entry, {
|
|
143
|
+
start(el) {
|
|
144
|
+
if (el.ns.toLowerCase() !== RELS_NS || el.local !== 'Relationship')
|
|
145
|
+
return;
|
|
146
|
+
const id = (0, xml_1.attr)(el, '', 'Id') ?? '';
|
|
147
|
+
if (ids.has(id))
|
|
148
|
+
duplicate(ctx, entry.name, 'relationship Id', id);
|
|
149
|
+
ids.add(id);
|
|
150
|
+
const target = (0, xml_1.attr)(el, '', 'Target');
|
|
151
|
+
if (target === undefined)
|
|
152
|
+
return;
|
|
153
|
+
const mode = ((0, xml_1.attr)(el, '', 'TargetMode') ?? 'Internal').trim().toLowerCase();
|
|
154
|
+
each({ id, type: ((0, xml_1.attr)(el, '', 'Type') ?? '').trim().toLowerCase(), target, external: mode !== 'internal' || (0, links_1.isAbsoluteTarget)(target) });
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/** Word field codes, followed through nested fields, as one part's fields run. */
|
|
159
|
+
class Fields {
|
|
160
|
+
ctx;
|
|
161
|
+
part;
|
|
162
|
+
stack = [];
|
|
163
|
+
capture = false;
|
|
164
|
+
constructor(ctx, part) {
|
|
165
|
+
this.ctx = ctx;
|
|
166
|
+
this.part = part;
|
|
167
|
+
}
|
|
168
|
+
/** A field starts. One that starts before its parent's code has a word in it makes that code at run time. */
|
|
169
|
+
nest() {
|
|
170
|
+
const top = this.stack[this.stack.length - 1];
|
|
171
|
+
if (top?.inCode && top.code === '')
|
|
172
|
+
top.computed = true;
|
|
173
|
+
}
|
|
174
|
+
start(el) {
|
|
175
|
+
if (el.local === 'fldSimple') {
|
|
176
|
+
this.nest();
|
|
177
|
+
this.evaluate(wordAttr(el, 'instr') ?? '', false);
|
|
178
|
+
}
|
|
179
|
+
else if (el.local === 'fldChar') {
|
|
180
|
+
const kind = wordAttr(el, 'fldCharType');
|
|
181
|
+
if (kind === 'begin') {
|
|
182
|
+
this.nest();
|
|
183
|
+
this.stack.push({ code: '', inCode: true, computed: false });
|
|
184
|
+
}
|
|
185
|
+
else if (kind === 'separate') {
|
|
186
|
+
const top = this.stack[this.stack.length - 1];
|
|
187
|
+
if (top)
|
|
188
|
+
top.inCode = false;
|
|
189
|
+
}
|
|
190
|
+
else if (kind === 'end') {
|
|
191
|
+
const f = this.stack.pop();
|
|
192
|
+
if (f)
|
|
193
|
+
this.evaluate(f.code, f.computed);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else if (el.local === 'instrText' || el.local === 'delInstrText')
|
|
197
|
+
this.capture = true;
|
|
198
|
+
}
|
|
199
|
+
end(el) {
|
|
200
|
+
if (el.local === 'instrText' || el.local === 'delInstrText')
|
|
201
|
+
this.capture = false;
|
|
202
|
+
}
|
|
203
|
+
/** White space before the first word counts for nothing, so no amount of it can push the field's name out. */
|
|
204
|
+
text(t) {
|
|
205
|
+
const top = this.stack[this.stack.length - 1];
|
|
206
|
+
if (this.capture && top?.inCode && top.code.length < MAX_FIELD_CODE)
|
|
207
|
+
top.code = top.code ? top.code + t : t.trimStart();
|
|
208
|
+
}
|
|
209
|
+
/** A part can end inside a field. Word still reads the code it has. */
|
|
210
|
+
finish() {
|
|
211
|
+
for (let f = this.stack.pop(); f; f = this.stack.pop())
|
|
212
|
+
this.evaluate(f.code, f.computed);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Reports a field by its code. A field whose name holds the content of an external entity has a type the file does
|
|
216
|
+
* not say, as one whose name another field builds does.
|
|
217
|
+
*/
|
|
218
|
+
evaluate(code, computed) {
|
|
219
|
+
const data = { code: (0, context_1.clip)(code.trim()) };
|
|
220
|
+
const words = (0, formulas_1.fieldWords)(code);
|
|
221
|
+
const first = words[0]?.text ?? '';
|
|
222
|
+
const entity = (0, xml_1.unknownEntity)(first);
|
|
223
|
+
if (computed || entity !== undefined)
|
|
224
|
+
this.ctx.report(types_1.OfficeCategory.Field, types_1.OfficeDetail.Computed, this.part, entity === undefined ? data : { ...data, entity: (0, context_1.clip)(entity) });
|
|
225
|
+
const name = first.toUpperCase();
|
|
226
|
+
// An argument comes before any switch, and a switch is an unquoted word starting with a backslash.
|
|
227
|
+
const switchAt = words.findIndex(w => !w.quoted && w.text.startsWith('\\'));
|
|
228
|
+
const arg = (n) => (words[n] !== undefined && (switchAt < 0 || n < switchAt) ? words[n].text : undefined);
|
|
229
|
+
if (name === 'DDE' || name === 'DDEAUTO')
|
|
230
|
+
this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Field, this.part, data);
|
|
231
|
+
else if (name === 'MACROBUTTON')
|
|
232
|
+
this.ctx.report(types_1.OfficeCategory.Field, types_1.OfficeDetail.Macrobutton, this.part, { ...data, macro: (0, context_1.clip)(words[1]?.text ?? '') });
|
|
233
|
+
else if (name === 'INCLUDETEXT' || name === 'INCLUDEPICTURE' || name === 'IMPORT' || name === 'LINK') {
|
|
234
|
+
const target = arg(name === 'LINK' ? 2 : 1);
|
|
235
|
+
if (target === undefined)
|
|
236
|
+
this.ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Include, this.part, data);
|
|
237
|
+
else
|
|
238
|
+
this.ctx.external(types_1.OfficeDetail.Include, target, this.part, data, 'field');
|
|
239
|
+
}
|
|
240
|
+
else if (name === 'HYPERLINK') {
|
|
241
|
+
const target = arg(1);
|
|
242
|
+
if (target !== undefined && !(0, links_1.isFragment)(target))
|
|
243
|
+
this.ctx.link(target, this.part, { ...data, source: 'HYPERLINK field' });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** Text that may run long. Detection needs only whether it holds more than white space; the report shows its start. */
|
|
248
|
+
class Shown {
|
|
249
|
+
text = '';
|
|
250
|
+
content = false;
|
|
251
|
+
add(t) {
|
|
252
|
+
if (/\S/.test(t))
|
|
253
|
+
this.content = true;
|
|
254
|
+
if (this.text.length < 200)
|
|
255
|
+
this.text = this.text ? this.text + t : t.trimStart();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/** The rules that read inside one XML part of an OOXML package. */
|
|
259
|
+
class PartRules {
|
|
260
|
+
ctx;
|
|
261
|
+
part;
|
|
262
|
+
fields;
|
|
263
|
+
/**
|
|
264
|
+
* The formula being read, with a check for DDE that sees all of it however long it is, and the first external
|
|
265
|
+
* entity whose content stands in it, which could hold the DDE reference or move the quotes around one.
|
|
266
|
+
*/
|
|
267
|
+
formula;
|
|
268
|
+
macroText;
|
|
269
|
+
/** The last namespace seen and its lowercase form. Neighboring elements share one, and this spares a copy each. */
|
|
270
|
+
ns = '';
|
|
271
|
+
lower = '';
|
|
272
|
+
constructor(ctx, part) {
|
|
273
|
+
this.ctx = ctx;
|
|
274
|
+
this.part = part;
|
|
275
|
+
this.fields = new Fields(ctx, part);
|
|
276
|
+
}
|
|
277
|
+
lowerNs(el) {
|
|
278
|
+
if (el.ns !== this.ns) {
|
|
279
|
+
this.ns = el.ns;
|
|
280
|
+
this.lower = el.ns.toLowerCase();
|
|
281
|
+
}
|
|
282
|
+
return this.lower;
|
|
283
|
+
}
|
|
284
|
+
start(el) {
|
|
285
|
+
const ns = this.lowerNs(el);
|
|
286
|
+
if (CUSTOM_UI_NS.has(ns)) {
|
|
287
|
+
if (el.depth === 1)
|
|
288
|
+
this.ctx.report(types_1.OfficeCategory.Ui, types_1.OfficeDetail.Ribbon, this.part);
|
|
289
|
+
for (const a of el.attributes) {
|
|
290
|
+
if (a.ns === '' && a.value.trim() && /^(on[A-Z]|get[A-Z]|loadImage$)/.test(a.local))
|
|
291
|
+
this.ctx.report(types_1.OfficeCategory.Ui, types_1.OfficeDetail.Callback, this.part, { callback: `${a.local}=${(0, context_1.clip)(a.value)}` });
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (WORD_NS.has(ns))
|
|
296
|
+
this.fields.start(el);
|
|
297
|
+
else if (SHEET_NS.has(ns)) {
|
|
298
|
+
if (el.local === 'f' || el.local === 'definedName')
|
|
299
|
+
this.formula = { shown: new Shown(), quotes: new formulas_1.Quotes(), ddeBar: false };
|
|
300
|
+
else if (el.local === 'ddeLink')
|
|
301
|
+
this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, this.part, { application: (0, context_1.clip)((0, xml_1.attr)(el, '', 'ddeService') ?? ''), topic: (0, context_1.clip)((0, xml_1.attr)(el, '', 'ddeTopic') ?? '') });
|
|
302
|
+
}
|
|
303
|
+
else if (XDR_NS.has(ns)) {
|
|
304
|
+
const macro = (0, xml_1.attr)(el, '', 'macro');
|
|
305
|
+
if (macro?.trim())
|
|
306
|
+
this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { macro: (0, context_1.clip)(macro) });
|
|
307
|
+
}
|
|
308
|
+
else if (ns === VML_EXCEL_NS && el.local === 'FmlaMacro')
|
|
309
|
+
this.macroText = new Shown();
|
|
310
|
+
else if (ns === VISIO_NS && el.local === 'Cell')
|
|
311
|
+
this.visioCell(el);
|
|
312
|
+
if (el.local === 'hlinkClick' || el.local === 'hlinkHover' || el.local === 'hlinkMouseOver')
|
|
313
|
+
this.drawingAction(el);
|
|
314
|
+
}
|
|
315
|
+
text(t) {
|
|
316
|
+
this.fields.text(t);
|
|
317
|
+
const f = this.formula;
|
|
318
|
+
if (f) {
|
|
319
|
+
f.shown.add(t);
|
|
320
|
+
// Excel writes a DDE reference as application|topic!item, and "|" means nothing else outside quotes.
|
|
321
|
+
for (const c of t)
|
|
322
|
+
if (f.quotes.outside(c) && c === '|')
|
|
323
|
+
f.ddeBar = true;
|
|
324
|
+
if (f.entity === undefined && t.includes(xml_1.UNKNOWN))
|
|
325
|
+
f.entity = (0, xml_1.unknownEntity)(t);
|
|
326
|
+
}
|
|
327
|
+
this.macroText?.add(t);
|
|
328
|
+
}
|
|
329
|
+
end(el) {
|
|
330
|
+
const ns = this.lowerNs(el);
|
|
331
|
+
if (WORD_NS.has(ns))
|
|
332
|
+
this.fields.end(el);
|
|
333
|
+
if (SHEET_NS.has(ns) && (el.local === 'f' || el.local === 'definedName') && this.formula) {
|
|
334
|
+
const { ddeBar, entity, shown } = this.formula;
|
|
335
|
+
if (ddeBar)
|
|
336
|
+
this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Formula, this.part, { formula: (0, context_1.clip)(shown.text) });
|
|
337
|
+
else if (entity !== undefined)
|
|
338
|
+
this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Formula, this.part, { formula: (0, context_1.clip)(shown.text), entity: (0, context_1.clip)(entity) });
|
|
339
|
+
this.formula = undefined;
|
|
340
|
+
}
|
|
341
|
+
if (ns === VML_EXCEL_NS && el.local === 'FmlaMacro' && this.macroText) {
|
|
342
|
+
if (this.macroText.content)
|
|
343
|
+
this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { macro: (0, context_1.clip)(this.macroText.text.trim()) });
|
|
344
|
+
this.macroText = undefined;
|
|
345
|
+
}
|
|
346
|
+
if (el.depth === 1)
|
|
347
|
+
this.fields.finish();
|
|
348
|
+
}
|
|
349
|
+
/** PowerPoint action settings, in a DrawingML hyperlink's action attribute. The relationship it names is reported on its own. */
|
|
350
|
+
drawingAction(el) {
|
|
351
|
+
const written = ((0, xml_1.attr)(el, '', 'action') ?? '').trim();
|
|
352
|
+
const action = written.toLowerCase();
|
|
353
|
+
if (!action)
|
|
354
|
+
return;
|
|
355
|
+
const id = el.attributes.find(a => a.local === 'id' && R_NS.has(a.ns.toLowerCase()))?.value;
|
|
356
|
+
const data = { trigger: el.local, action: (0, context_1.clip)(written), ...(id === undefined ? {} : { relationship: (0, context_1.clip)(id) }) };
|
|
357
|
+
if (action.startsWith('ppaction://program'))
|
|
358
|
+
this.ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, this.part, data);
|
|
359
|
+
else if (action.startsWith('ppaction://macro'))
|
|
360
|
+
this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, data);
|
|
361
|
+
else if (action.startsWith('ppaction://ole'))
|
|
362
|
+
this.ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, this.part, data);
|
|
363
|
+
}
|
|
364
|
+
visioCell(el) {
|
|
365
|
+
const n = (0, xml_1.attr)(el, '', 'N') ?? '';
|
|
366
|
+
const formula = (0, xml_1.attr)(el, '', 'F') ?? '';
|
|
367
|
+
if (VISIO_CODE.test(formula)) {
|
|
368
|
+
if (n.startsWith('Event'))
|
|
369
|
+
this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Event, this.part, { event: (0, context_1.clip)(n), formula: (0, context_1.clip)(formula) });
|
|
370
|
+
else
|
|
371
|
+
this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { cell: (0, context_1.clip)(n), formula: (0, context_1.clip)(formula) });
|
|
372
|
+
}
|
|
373
|
+
const v = (0, xml_1.attr)(el, '', 'V');
|
|
374
|
+
if (n === 'Address' && v?.trim() && !(0, links_1.isFragment)(v))
|
|
375
|
+
this.ctx.link(v, this.part, { source: 'Visio hyperlink' });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Inspects an OOXML package. The package relationships come first, since they name the main part and so the format.
|
|
380
|
+
* Then each part is read in archive order, followed by its relationships, which are reported as they are read and
|
|
381
|
+
* leave behind only which parts they reach and how.
|
|
382
|
+
*/
|
|
383
|
+
async function inspectOoxml(ctx, contentTypes) {
|
|
384
|
+
const { defaults, overrides } = await readContentTypes(ctx, contentTypes);
|
|
385
|
+
const parts = new Map();
|
|
386
|
+
const relsOf = new Map();
|
|
387
|
+
for (const entry of ctx.zip.entries) {
|
|
388
|
+
if (entry === contentTypes)
|
|
389
|
+
continue;
|
|
390
|
+
// OPC has no folders. A directory entry is still inflated once, so its bytes are checked like any other.
|
|
391
|
+
if (entry.directory) {
|
|
392
|
+
await ctx.drain(entry);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const source = relsSource(entry.name);
|
|
396
|
+
if (source !== undefined) {
|
|
397
|
+
relsOf.set(source, entry);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const key = (0, zip_1.nameKey)(entry.name);
|
|
401
|
+
const ext = /\.([^./]*)$/.exec(entry.name)?.[1]?.toLowerCase();
|
|
402
|
+
parts.set(key, { entry, contentType: overrides.get(key) ?? (ext === undefined ? undefined : defaults.get(ext)), reachedAs: 0, claimed: false, targets: new Set(), ole: false });
|
|
403
|
+
}
|
|
404
|
+
const roots = new Set();
|
|
405
|
+
const mains = [];
|
|
406
|
+
const relate = (rels, from, r) => {
|
|
407
|
+
const location = `${rels.name}, ${(0, context_1.clip)(r.id)}`;
|
|
408
|
+
const part = r.external ? undefined : parts.get(resolveTarget(rels.name, r.target));
|
|
409
|
+
if (part) {
|
|
410
|
+
part.reachedAs |= REACHED_AS.get(r.type) ?? 0;
|
|
411
|
+
if (TYPE.control.has(from?.contentType ?? ''))
|
|
412
|
+
part.claimed = true;
|
|
413
|
+
(from?.targets ?? roots).add(part);
|
|
414
|
+
}
|
|
415
|
+
// A hyperlink to a part of the package, or to a place in the document, goes nowhere else.
|
|
416
|
+
if (REL.hyperlink.has(r.type)) {
|
|
417
|
+
if (!part && !(0, links_1.isFragment)(r.target))
|
|
418
|
+
ctx.link(r.target, location);
|
|
419
|
+
}
|
|
420
|
+
else if (r.external) {
|
|
421
|
+
const kind = EXTERNAL_KINDS.find(([types]) => types.has(r.type))?.[1] ?? types_1.OfficeDetail.Other;
|
|
422
|
+
ctx.external(kind, r.target, location, { type: (0, context_1.clip)(r.type) }, (0, context_1.clip)(r.type.slice(r.type.lastIndexOf('/') + 1)));
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
const packageRels = relsOf.get('');
|
|
426
|
+
if (packageRels) {
|
|
427
|
+
await readRelationships(ctx, packageRels, r => {
|
|
428
|
+
if (REL.main.has(r.type))
|
|
429
|
+
mains.push(r.external ? undefined : parts.get(resolveTarget(packageRels.name, r.target)));
|
|
430
|
+
relate(packageRels, undefined, r);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
if (mains.length > 1)
|
|
434
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, 'The package names more than one main part', { reason: 'more than one main part' });
|
|
435
|
+
const main = mains[0];
|
|
436
|
+
if (!main)
|
|
437
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The package has no main part', { reason: 'no main document part' });
|
|
438
|
+
const format = formats_1.OOXML_FORMATS.get(main.contentType ?? '');
|
|
439
|
+
if (!format)
|
|
440
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The main part has an unknown content type', { contentType: (0, context_1.clip)(main.contentType ?? '') });
|
|
441
|
+
ctx.format = format;
|
|
442
|
+
for (const [key, p] of parts) {
|
|
443
|
+
const ct = p.contentType;
|
|
444
|
+
if (ct === undefined) {
|
|
445
|
+
ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UntypedPart, p.entry.name, { part: p.entry.name });
|
|
446
|
+
p.ole = await ctx.drain(p.entry);
|
|
447
|
+
}
|
|
448
|
+
else if (ct.endsWith('+xml') || ct === 'application/xml' || ct === 'text/xml' || ct === TYPE.vml) {
|
|
449
|
+
await ctx.parse(p.entry, new PartRules(ctx, p.entry.name), ct === TYPE.vml);
|
|
450
|
+
}
|
|
451
|
+
else if (ct === TYPE.xlsbExternalLink) {
|
|
452
|
+
// The binary form of an Excel ddeLink.
|
|
453
|
+
const links = new xlsb_1.XlsbLinks();
|
|
454
|
+
p.ole = await ctx.drain(p.entry, c => links.write(c));
|
|
455
|
+
for (const l of links.dde)
|
|
456
|
+
ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, p.entry.name, { application: (0, context_1.clip)(l.application), topic: (0, context_1.clip)(l.topic) });
|
|
457
|
+
}
|
|
458
|
+
else
|
|
459
|
+
p.ole = await ctx.drain(p.entry);
|
|
460
|
+
const rels = relsOf.get(key);
|
|
461
|
+
if (rels)
|
|
462
|
+
await readRelationships(ctx, rels, r => relate(rels, p, r));
|
|
463
|
+
}
|
|
464
|
+
// Relationships of parts that are not there still say what the parts they reach are.
|
|
465
|
+
for (const [source, rels] of relsOf)
|
|
466
|
+
if (source !== '' && !parts.has(source))
|
|
467
|
+
await readRelationships(ctx, rels, r => relate(rels, undefined, r));
|
|
468
|
+
for (const p of parts.values())
|
|
469
|
+
partRules(ctx, p);
|
|
470
|
+
unreferenced(ctx, parts, relsOf, roots);
|
|
471
|
+
}
|
|
472
|
+
/** Findings about a whole part, from its content type and the relationships that reach it. */
|
|
473
|
+
function partRules(ctx, p) {
|
|
474
|
+
const ct = p.contentType ?? '';
|
|
475
|
+
const loc = p.entry.name;
|
|
476
|
+
const data = { part: loc, contentType: (0, context_1.clip)(ct) };
|
|
477
|
+
const as = (bit) => (p.reachedAs & bit) !== 0;
|
|
478
|
+
const vba = TYPE.vba.has(ct) || as(AS.vba);
|
|
479
|
+
const ole = TYPE.ole.has(ct) || as(AS.ole);
|
|
480
|
+
const control = TYPE.control.has(ct) || as(AS.control);
|
|
481
|
+
const controlBinary = TYPE.controlBinary.has(ct) || as(AS.controlBinary);
|
|
482
|
+
const pkg = as(AS.package);
|
|
483
|
+
if (vba)
|
|
484
|
+
ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, loc, data);
|
|
485
|
+
if (TYPE.macrosheet.has(ct) || as(AS.macrosheet))
|
|
486
|
+
ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.XlmMacrosheet, loc, data);
|
|
487
|
+
if (ole)
|
|
488
|
+
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, loc, data);
|
|
489
|
+
if (pkg)
|
|
490
|
+
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Package, loc, data);
|
|
491
|
+
if (control || (controlBinary && !p.claimed))
|
|
492
|
+
ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, loc, data);
|
|
493
|
+
if (p.ole && !vba && !ole && !control && !controlBinary && !pkg)
|
|
494
|
+
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleFile, loc, data);
|
|
495
|
+
}
|
|
496
|
+
/** Parts that no chain of internal relationships reaches from the package. */
|
|
497
|
+
function unreferenced(ctx, parts, relsOf, roots) {
|
|
498
|
+
const reached = new Set(roots);
|
|
499
|
+
const queue = [...roots];
|
|
500
|
+
for (let p = queue.pop(); p; p = queue.pop()) {
|
|
501
|
+
for (const t of p.targets) {
|
|
502
|
+
if (reached.has(t))
|
|
503
|
+
continue;
|
|
504
|
+
reached.add(t);
|
|
505
|
+
queue.push(t);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
for (const p of parts.values())
|
|
509
|
+
if (!reached.has(p))
|
|
510
|
+
ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UnreferencedPart, p.entry.name, { part: p.entry.name });
|
|
511
|
+
for (const [source, rels] of relsOf) {
|
|
512
|
+
const owner = parts.get(source);
|
|
513
|
+
if (source !== '' && !(owner && reached.has(owner)))
|
|
514
|
+
ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UnreferencedPart, rels.name, { part: rels.name });
|
|
515
|
+
}
|
|
516
|
+
}
|
package/dist/score.d.ts
ADDED
|
@@ -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
|
+
}
|