@patrickdk77/script-defuse 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vba.js ADDED
@@ -0,0 +1,728 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeVba = analyzeVba;
4
+ const analyze_1 = require("./analyze");
5
+ const findings_1 = require("./findings");
6
+ const types_1 = require("./types");
7
+ const EXT = (d) => [types_1.ScriptCategory.External, d];
8
+ const DYN = (d) => [types_1.ScriptCategory.DynamicCode, d];
9
+ /** Names that reach outside the document wherever they appear, bare or as a member. */
10
+ const DENY = new Map([
11
+ // VBA's own library.
12
+ ...['kill', 'filecopy', 'mkdir', 'rmdir', 'chdir', 'chdrive', 'setattr', 'dir', 'curdir', 'filelen', 'filedatetime', 'getattr', 'fileattr', 'loadpicture', 'savepicture'].map((n) => [n, EXT(types_1.ScriptDetail.FileSystem)]),
13
+ ...['shell', 'sendkeys', 'appactivate', 'macscript', 'applescripttask'].map((n) => [n, EXT(types_1.ScriptDetail.Process)]),
14
+ ...['savesetting', 'getsetting', 'getallsettings', 'deletesetting'].map((n) => [n, EXT(types_1.ScriptDetail.Registry)]),
15
+ ...['ddeinitiate', 'ddeexecute', 'ddepoke', 'dderequest', 'ddeterminate', 'ddeterminateall'].map((n) => [n, EXT(types_1.ScriptDetail.Dde)]),
16
+ ['createobject', EXT(types_1.ScriptDetail.ComObject)],
17
+ ['getobject', EXT(types_1.ScriptDetail.ComObject)],
18
+ ['callbyname', DYN(types_1.ScriptDetail.ComputedAccess)],
19
+ ...['varptr', 'strptr', 'objptr'].map((n) => [n, DYN(types_1.ScriptDetail.Privilege)]),
20
+ ['strreverse', [types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Decode]],
21
+ // The Office object model.
22
+ ...['executeexcel4macro', 'runautomacros'].map((n) => [n, DYN(types_1.ScriptDetail.Eval)]),
23
+ ['wordbasic', DYN(types_1.ScriptDetail.LegacyMacro)],
24
+ ['ontime', DYN(types_1.ScriptDetail.Timer)],
25
+ ...[
26
+ 'vbe',
27
+ 'vbproject',
28
+ 'vbcomponents',
29
+ 'codemodule',
30
+ 'addfromstring',
31
+ 'addfromfile',
32
+ 'addfromguid',
33
+ 'insertlines',
34
+ 'replaceline',
35
+ 'deletelines',
36
+ 'addins',
37
+ 'addins2',
38
+ 'comaddins',
39
+ 'excel4macrosheets',
40
+ 'excel4intlmacrosheets',
41
+ ].map((n) => [n, DYN(types_1.ScriptDetail.InstallScript)]),
42
+ ['registerxll', EXT(types_1.ScriptDetail.NativeCode)],
43
+ ['automationsecurity', DYN(types_1.ScriptDetail.Privilege)],
44
+ ...['followhyperlink', 'navigate', 'navigate2'].map((n) => [n, EXT(types_1.ScriptDetail.Url)]),
45
+ ...[
46
+ 'opentext',
47
+ 'openxml',
48
+ 'opendatabase',
49
+ 'opencurrentdatabase',
50
+ 'insertfile',
51
+ 'insertfromfile',
52
+ 'addpicture',
53
+ 'addpicture2',
54
+ 'addmediaobject',
55
+ 'addmediaobject2',
56
+ 'attachedtemplate',
57
+ 'sourcefullname',
58
+ 'changelink',
59
+ 'updatelink',
60
+ 'opendatasource',
61
+ 'insertdatabase',
62
+ 'changefileopendirectory',
63
+ ].map((n) => [n, EXT(types_1.ScriptDetail.OpenDocument)]),
64
+ ['addoleobject', EXT(types_1.ScriptDetail.DataObject)],
65
+ ['querytables', EXT(types_1.ScriptDetail.Network)],
66
+ ...['sendmail', 'sendforreview', 'route', 'mailenvelope', 'sendfaxoverinternet'].map((n) => [n, EXT(types_1.ScriptDetail.Mail)]),
67
+ ['printout', EXT(types_1.ScriptDetail.Print)],
68
+ ...['saveas', 'saveas2', 'savecopyas', 'exportasfixedformat', 'exportasfixedformat2', 'exportasfixedformat3', 'saveasxmldata', 'publishobjects'].map((n) => [n, EXT(types_1.ScriptDetail.Save)]),
69
+ ...['importxml', 'xmlimport', 'xmlimportxml', 'exportxml'].map((n) => [n, EXT(types_1.ScriptDetail.ImportExport)]),
70
+ ['privateprofilestring', EXT(types_1.ScriptDetail.Registry)],
71
+ ['profilestring', EXT(types_1.ScriptDetail.Registry)],
72
+ ['activatemicrosoftapp', EXT(types_1.ScriptDetail.Process)],
73
+ // What the objects CreateObject and GetObject return offer. They only come from those, which are denied anyway.
74
+ ...['createtextfile', 'opentextfile', 'openastextstream', 'copyfile', 'movefile', 'deletefile', 'copyfolder', 'movefolder', 'deletefolder', 'createfolder', 'savetofile', 'loadfromfile'].map((n) => [n, EXT(types_1.ScriptDetail.FileSystem)]),
75
+ ...['exec', 'shellexecute'].map((n) => [n, EXT(types_1.ScriptDetail.Process)]),
76
+ ...['regwrite', 'regread', 'regdelete'].map((n) => [n, EXT(types_1.ScriptDetail.Registry)]),
77
+ ...['executeglobal', 'executestatement', 'addcode'].map((n) => [n, DYN(types_1.ScriptDetail.Eval)]),
78
+ ]);
79
+ /** Names that reach outside only when written bare, since as a member they are common and harmless. */
80
+ const BARE = new Map([
81
+ ['environ', EXT(types_1.ScriptDetail.Environment)],
82
+ ['command', EXT(types_1.ScriptDetail.Environment)],
83
+ ['input', EXT(types_1.ScriptDetail.FileSystem)],
84
+ ['tasks', EXT(types_1.ScriptDetail.Process)],
85
+ ]);
86
+ /** Names that reach outside only as a member, since bare they are statements or the macro's own names. */
87
+ const MEMBER = new Map([
88
+ ['open', EXT(types_1.ScriptDetail.OpenDocument)],
89
+ ['follow', EXT(types_1.ScriptDetail.Url)],
90
+ ['send', EXT(types_1.ScriptDetail.Mail)],
91
+ ['import', EXT(types_1.ScriptDetail.ImportExport)],
92
+ ['export', EXT(types_1.ScriptDetail.ImportExport)],
93
+ ['compare', EXT(types_1.ScriptDetail.OpenDocument)],
94
+ ]);
95
+ /** A member that reaches outside after a particular collection: Hyperlinks.Add adds a link the document did not have. */
96
+ const AFTER = new Map([
97
+ ['hyperlinks', new Map([['add', EXT(types_1.ScriptDetail.Url)]])],
98
+ ['oleobjects', new Map([['add', EXT(types_1.ScriptDetail.DataObject)]])],
99
+ [
100
+ 'connections',
101
+ new Map([
102
+ ['add', EXT(types_1.ScriptDetail.Network)],
103
+ ['add2', EXT(types_1.ScriptDetail.Network)],
104
+ ]),
105
+ ],
106
+ ['xmlmaps', new Map([['add', EXT(types_1.ScriptDetail.ImportExport)]])],
107
+ ['pictures', new Map([['insert', EXT(types_1.ScriptDetail.OpenDocument)]])],
108
+ ]);
109
+ /** Collections whose Add opens a template or file when given one. Without arguments, Add makes an empty document. */
110
+ const ADD_WITH_FILE = new Set(['workbooks', 'documents', 'presentations']);
111
+ /** Classes `New` may create: VBA's own, the Scripting runtime's dictionary, VBScript's regular expressions. */
112
+ const NEW_ALLOWED = new Set(['collection', 'dictionary', 'regexp']);
113
+ /** Classes of the allowed libraries that reach outside: the file system object, and another Office instance. */
114
+ const NEW_DENIED = new Map([
115
+ ['filesystemobject', EXT(types_1.ScriptDetail.FileSystem)],
116
+ ['application', EXT(types_1.ScriptDetail.Process)],
117
+ ]);
118
+ /**
119
+ * Type libraries a project may refer to: VBA, OLE Automation, Office, the Office applications, Forms 2.0, the Ref Edit
120
+ * control, the Scripting runtime and VBScript regular expressions. Every other library, and every other project but
121
+ * Word's Normal template, adds code or objects from outside the document.
122
+ */
123
+ const LIBRARIES = new Set([
124
+ '000204EF-0000-0000-C000-000000000046',
125
+ '00020430-0000-0000-C000-000000000046',
126
+ '2DF8D04C-5BFA-101B-BDE5-00AA0044DE52',
127
+ '00020813-0000-0000-C000-000000000046',
128
+ '00020905-0000-0000-C000-000000000046',
129
+ '91493440-5A91-11CF-8700-00AA0060263B',
130
+ '00021A98-0000-0000-C000-000000000046',
131
+ '0002123C-0000-0000-C000-000000000046',
132
+ 'A7107640-94DF-1068-855E-00DD01075445',
133
+ '0D452EE1-E08F-101A-852E-02608C4D0BB4',
134
+ '00024517-0000-0000-C000-000000000046',
135
+ '420B2830-E718-11CF-893D-00A0C9054228',
136
+ '3F4DACA7-160D-11D2-A8E9-00104B365C9F',
137
+ ].map(g => g.toUpperCase()));
138
+ /** A worksheet formula that reaches outside when Excel calculates it, by the function it calls. */
139
+ const FORMULA_FUNCTIONS = new Map([
140
+ ...['webservice', 'importdata', 'importxml', 'importhtml', 'importfeed', 'importrange', 'rtd'].map((n) => [n, EXT(types_1.ScriptDetail.Network)]),
141
+ ...['call', 'register', 'register.id'].map((n) => [n, EXT(types_1.ScriptDetail.NativeCode)]),
142
+ ['exec', EXT(types_1.ScriptDetail.Process)],
143
+ ['hyperlink', EXT(types_1.ScriptDetail.Url)],
144
+ ]);
145
+ const FORMULA_CALL = /([A-Za-z][A-Za-z0-9.]*)\s*\(/g;
146
+ /** A DDE formula, application|topic!item, as `=cmd|' /c calc'!A0`. */
147
+ const DDE_FORMULA = /^[=+\-@][^"]*\w\s*\|[^!]*!/;
148
+ /** A macro name that names another file, as `'other.xlsm'!Macro`, which Excel opens to run it. */
149
+ const EXTERNAL_MACRO = /\.(?:xl[a-z]{1,2}|do[ct][a-z]?|pp[a-z]{1,2})'?!/i;
150
+ class VbaSyntaxError extends Error {
151
+ }
152
+ /** The tokens of a module. A comment that ends in " _" is not taken to go on, so the line after it is read as code. */
153
+ function tokenize(text) {
154
+ const out = [];
155
+ let i = 0;
156
+ let lineStart = true;
157
+ const push = (t, v, pos) => {
158
+ out.push({ t, v, pos });
159
+ lineStart = t === 'nl';
160
+ };
161
+ const idStart = (c) => /[\p{L}_]/u.test(c);
162
+ const idPart = (c) => /[\p{L}\p{N}_]/u.test(c);
163
+ while (i < text.length) {
164
+ const c = text[i];
165
+ if (c === '\r' || c === '\n') {
166
+ if (out.length && out[out.length - 1].t !== 'nl')
167
+ push('nl', '', i);
168
+ lineStart = true;
169
+ i++;
170
+ continue;
171
+ }
172
+ if (c === ' ' || c === '\t' || c === '\f' || c === '\v' || c === '\u00a0') {
173
+ i++;
174
+ continue;
175
+ }
176
+ if (c === '_' && /^_[ \t]*\r?\n/.test(text.slice(i, i + 64)) && (i === 0 || /[ \t]/.test(text[i - 1]))) {
177
+ // A line continuation: the statement goes on on the next line.
178
+ i = text.indexOf('\n', i) + 1;
179
+ continue;
180
+ }
181
+ if (c === "'") {
182
+ while (i < text.length && text[i] !== '\n' && text[i] !== '\r')
183
+ i++;
184
+ continue;
185
+ }
186
+ if (c === '"') {
187
+ const start = i;
188
+ let s = '';
189
+ i++;
190
+ for (;;) {
191
+ if (i >= text.length || text[i] === '\n' || text[i] === '\r')
192
+ throw new VbaSyntaxError(`a string that does not end on its line, at offset ${start}`);
193
+ if (text[i] === '"') {
194
+ if (text[i + 1] === '"') {
195
+ s += '"';
196
+ i += 2;
197
+ continue;
198
+ }
199
+ i++;
200
+ break;
201
+ }
202
+ s += text[i++];
203
+ }
204
+ push('str', s, start);
205
+ continue;
206
+ }
207
+ if (c === '[') {
208
+ const end = text.indexOf(']', i);
209
+ const nl = text.slice(i, end < 0 ? text.length : end).search(/[\r\n]/);
210
+ if (end < 0 || nl >= 0)
211
+ throw new VbaSyntaxError(`a bracketed name that does not end on its line, at offset ${i}`);
212
+ push('bracket', text.slice(i + 1, end), i);
213
+ i = end + 1;
214
+ continue;
215
+ }
216
+ if (c === '#') {
217
+ const date = /^#[0-9/:\- ,.APMapm]+#/.exec(text.slice(i, i + 64));
218
+ if (date && /\d/.test(date[0])) {
219
+ push('date', date[0], i);
220
+ i += date[0].length;
221
+ continue;
222
+ }
223
+ push('op', '#', i);
224
+ i++;
225
+ continue;
226
+ }
227
+ if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(text[i + 1] ?? '')) || (c === '&' && /[hHoO]/.test(text[i + 1] ?? '') && /[0-9a-fA-F]/.test(text[i + 2] ?? ''))) {
228
+ const m = /^(?:&[hH][0-9a-fA-F]+|&[oO][0-7]+|(?:\d+\.?\d*|\.\d+)(?:[eEdD][+-]?\d+)?)[%&!#@^]?/.exec(text.slice(i, i + 64));
229
+ const n = m ? m[0] : c;
230
+ push('num', n, i);
231
+ i += n.length;
232
+ continue;
233
+ }
234
+ if (idStart(c)) {
235
+ const start = i;
236
+ while (i < text.length && idPart(text[i]))
237
+ i++;
238
+ const name = text.slice(start, i);
239
+ // A type character: $ always; the others only where no operand follows.
240
+ if (text[i] === '$' || (/[%&!#@]/.test(text[i] ?? '') && !idStart(text[i + 1] ?? '') && !/[0-9"(]/.test(text[i + 1] ?? '')))
241
+ i++;
242
+ const lower = name.toLowerCase();
243
+ if (lower === 'rem' && (lineStart || out[out.length - 1]?.t === 'nl')) {
244
+ while (i < text.length && text[i] !== '\n' && text[i] !== '\r')
245
+ i++;
246
+ continue;
247
+ }
248
+ push('id', lower, start);
249
+ continue;
250
+ }
251
+ if (c === ':' && text[i + 1] === '=') {
252
+ push('op', ':=', i);
253
+ i += 2;
254
+ continue;
255
+ }
256
+ if (c === ':') {
257
+ push('nl', ':', i);
258
+ i++;
259
+ continue;
260
+ }
261
+ const two = text.slice(i, i + 2);
262
+ if (two === '<>' || two === '<=' || two === '>=') {
263
+ push('op', two, i);
264
+ i += 2;
265
+ continue;
266
+ }
267
+ if ('=<>+-*/\\^&(),.!;'.includes(c)) {
268
+ push('op', c, i);
269
+ i++;
270
+ continue;
271
+ }
272
+ throw new VbaSyntaxError(`a character VBA does not have, ${JSON.stringify(c)}, at offset ${i}`);
273
+ }
274
+ return out;
275
+ }
276
+ /** Statement keywords after which a new statement starts on the same line. */
277
+ const STATEMENT_AFTER = new Set(['then', 'else']);
278
+ /** Keywords that may come before Declare and the other statements a rule looks for. */
279
+ const MODIFIERS = new Set(['public', 'private', 'friend', 'static', 'global']);
280
+ /** Statements that declare the names after them. */
281
+ const DECLARES = new Set(['dim', 'const', 'sub', 'function', 'property', 'type', 'enum', 'static', 'redim', 'global', 'public', 'private', 'friend', 'event']);
282
+ /** Keywords inside a declaration that are not names it declares. */
283
+ const DECLARATION_WORDS = new Set([...DECLARES, 'get', 'let', 'set', 'withevents', 'as', 'new', 'optional', 'byval', 'byref', 'paramarray', 'preserve', 'to', 'declare', 'ptrsafe', 'lib', 'alias']);
284
+ /** Where a procedure starts and ends, for names declared inside it. */
285
+ const PROCEDURES = new Set(['sub', 'function', 'property']);
286
+ /** Constants an Add of a new workbook, document or presentation takes in place of a template file. */
287
+ const BUILTIN_CONSTANT = /^(?:xl|wd|pp|mso|vb)/;
288
+ /** Inspects one VBA module. Its findings say what it calls that reaches outside the document, and what it hides. */
289
+ function analyzeVba(input) {
290
+ const options = input.options ?? {};
291
+ const overrides = options.actionOverrides ?? [];
292
+ const findings = [];
293
+ const reported = new Set();
294
+ const lines = [0];
295
+ for (let i = 0; i < input.text.length; i++)
296
+ if (input.text[i] === '\n')
297
+ lines.push(i + 1);
298
+ const lineOf = (pos) => {
299
+ let lo = 0;
300
+ let hi = lines.length - 1;
301
+ while (lo < hi) {
302
+ const mid = (lo + hi + 1) >> 1;
303
+ if (lines[mid] <= pos)
304
+ lo = mid;
305
+ else
306
+ hi = mid - 1;
307
+ }
308
+ return lo + 1;
309
+ };
310
+ const where = (pos) => [input.context?.location ?? input.context?.trigger, pos === undefined ? '' : `line ${lineOf(pos)}`].filter(Boolean).join(', ') || undefined;
311
+ const report = (f, pos, data) => {
312
+ const key = `${f[0]}/${f[1]}/${String(data?.api ?? '')}`;
313
+ if (reported.has(key))
314
+ return;
315
+ reported.add(key);
316
+ findings.push((0, findings_1.makeFinding)(f[0], f[1], overrides, where(pos), data));
317
+ };
318
+ const done = () => {
319
+ if (!findings.some(f => f.action === 'strip'))
320
+ report([types_1.ScriptCategory.Form, types_1.ScriptDetail.DocumentMacro]);
321
+ return { findings, definedGlobals: [] };
322
+ };
323
+ for (const r of input.context?.references ?? [])
324
+ checkReference(r, report);
325
+ const limits = options.limits ?? {};
326
+ if (limits.scriptSize !== undefined && input.text.length > limits.scriptSize) {
327
+ report([types_1.ScriptCategory.Limit, types_1.ScriptDetail.Size], undefined, { length: input.text.length });
328
+ return done();
329
+ }
330
+ let tokens;
331
+ try {
332
+ tokens = tokenize(input.text);
333
+ }
334
+ catch (err) {
335
+ report([types_1.ScriptCategory.Syntax, types_1.ScriptDetail.Unparseable], undefined, { reason: err instanceof Error ? err.message : String(err) });
336
+ return done();
337
+ }
338
+ // Names the module declares at its top and inside each procedure, which a bare name may mean in place of VBA's or
339
+ // Office's. A name declared in one procedure means nothing in another.
340
+ const moduleNames = new Set();
341
+ const scopeOf = new Array(tokens.length);
342
+ {
343
+ let scope = moduleNames;
344
+ let inProcedure = false;
345
+ for (let i = 0; i < tokens.length; i++) {
346
+ const tok = tokens[i];
347
+ const prev = tokens[i - 1];
348
+ const startsStatement = !prev || prev.t === 'nl';
349
+ if (tok.t === 'id' && tok.v === 'end' && tokens[i + 1]?.t === 'id' && PROCEDURES.has(tokens[i + 1].v)) {
350
+ scopeOf[i] = scope;
351
+ inProcedure = false;
352
+ scope = moduleNames;
353
+ continue;
354
+ }
355
+ if (startsStatement && tok.t === 'id' && (DECLARES.has(tok.v) || tok.v === 'declare')) {
356
+ let j = i;
357
+ while (tokens[j]?.t === 'id' && DECLARATION_WORDS.has(tokens[j].v))
358
+ j++;
359
+ const procedure = tokens.slice(i, j).some(x => PROCEDURES.has(x.v) && tokens[i].v !== 'declare');
360
+ // The procedure's own name belongs to the module; its parameters and locals to the procedure.
361
+ if (procedure && tokens[j]?.t === 'id')
362
+ moduleNames.add(tokens[j].v);
363
+ if (procedure && !inProcedure) {
364
+ scope = new Set(moduleNames);
365
+ inProcedure = true;
366
+ }
367
+ for (let k = procedure ? j + 1 : j; k < tokens.length && tokens[k].t !== 'nl'; k++) {
368
+ // Every name of a declaration list but types and the parts of qualified names: Dim a, b As Long; Sub F(x As Long).
369
+ const x = tokens[k];
370
+ if (x.t === 'id' && !DECLARATION_WORDS.has(x.v) && tokens[k - 1]?.v !== 'as' && tokens[k - 1]?.v !== '.' && tokens[k - 1]?.v !== 'new')
371
+ scope.add(x.v);
372
+ }
373
+ }
374
+ scopeOf[i] = scope;
375
+ }
376
+ }
377
+ const declaredAt = (pos) => scopeOf[pos] ?? moduleNames;
378
+ const indexOf = new Map(tokens.map((t, i) => [t, i]));
379
+ // Statements, split at line ends, colons, and after Then and Else.
380
+ const statements = [];
381
+ let current = [];
382
+ for (const tok of tokens) {
383
+ if (tok.t === 'nl') {
384
+ if (current.length)
385
+ statements.push(current);
386
+ current = [];
387
+ continue;
388
+ }
389
+ current.push(tok);
390
+ if (tok.t === 'id' && STATEMENT_AFTER.has(tok.v)) {
391
+ statements.push(current);
392
+ current = [];
393
+ }
394
+ }
395
+ if (current.length)
396
+ statements.push(current);
397
+ const appends = new Map();
398
+ for (const st of statements) {
399
+ let k = 0;
400
+ // A line number, as old macros carry, comes before the statement.
401
+ if (st[0].t === 'num')
402
+ k++;
403
+ while (st[k]?.t === 'id' && MODIFIERS.has(st[k].v))
404
+ k++;
405
+ const first = st[k];
406
+ const second = st[k + 1];
407
+ const head = first?.t === 'id' && st[k - 1]?.v !== '.' ? first.v : undefined;
408
+ if (head === 'declare')
409
+ report(EXT(types_1.ScriptDetail.NativeCode), first.pos, { api: `Declare ${second?.t === 'id' ? second.v : ''}`.trim() });
410
+ // The file statements: Open ... For, Print #, Write #, Input #, Line Input #, Get, Put, Lock, Unlock, Seek, Name ... As.
411
+ if (head === 'open' || head === 'print' || head === 'write' || head === 'lock' || head === 'unlock' || (head === 'line' && second?.v === 'input'))
412
+ report(EXT(types_1.ScriptDetail.FileSystem), first.pos, { api: head === 'line' ? 'Line Input' : cap(head) });
413
+ if ((head === 'get' || head === 'put' || head === 'seek' || head === 'width') && second?.v === '#')
414
+ report(EXT(types_1.ScriptDetail.FileSystem), first.pos, { api: cap(head) });
415
+ if ((head === 'get' || head === 'put') && st[k - 1]?.v !== 'property' && second && second.t !== 'id' && second.v !== '(')
416
+ report(EXT(types_1.ScriptDetail.FileSystem), first.pos, { api: cap(head) });
417
+ if (head === 'name' && second && !['=', '.', '(', '!'].includes(second.v) && st.some((x, n) => n > k + 1 && x.t === 'id' && x.v === 'as'))
418
+ report(EXT(types_1.ScriptDetail.FileSystem), first.pos, { api: 'Name' });
419
+ statementRules(st, k, declaredAt(indexOf.get(st[0]) ?? 0), report, appends);
420
+ }
421
+ for (const [name, j] of appends)
422
+ joinedRules(j, undefined, report, name);
423
+ return done();
424
+ }
425
+ const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
426
+ function checkReference(r, report) {
427
+ const api = r.name || r.libid.slice(0, 60);
428
+ if (r.kind === 'project') {
429
+ // *\C or *\D, then the path; Word's Normal template is named without one.
430
+ const path = r.libid.replace(/^\*\\[A-Za-z]/, '');
431
+ if (path.toLowerCase() !== 'normal')
432
+ report(EXT(types_1.ScriptDetail.Reference), undefined, { api });
433
+ return;
434
+ }
435
+ const guid = /\{([0-9A-Fa-f-]{36})\}/.exec(r.libid)?.[1]?.toUpperCase();
436
+ if (!guid || !LIBRARIES.has(guid))
437
+ report(EXT(types_1.ScriptDetail.Reference), undefined, { api });
438
+ }
439
+ /** The checks that look at the names, strings and calls of one statement. */
440
+ function statementRules(st, start, declared, report, appends) {
441
+ let charCodes = 0;
442
+ for (let i = start; i < st.length; i++) {
443
+ const tok = st[i];
444
+ const prev = st[i - 1];
445
+ const next = st[i + 1];
446
+ if (tok.t === 'str') {
447
+ stringRules(tok.v, tok.pos, report);
448
+ continue;
449
+ }
450
+ if (tok.t === 'bracket') {
451
+ // In Excel, [A1] and [SUM(A1:A3)] are Evaluate written short.
452
+ formulaRules(`=${tok.v}`, tok.pos, report);
453
+ continue;
454
+ }
455
+ if (tok.t !== 'id')
456
+ continue;
457
+ const member = prev?.v === '.' || prev?.v === '!';
458
+ const name = tok.v;
459
+ // VBA's and Office's names are taken as theirs even where the module declares the same name, since a declaration
460
+ // in one procedure does not change what the name means in the next.
461
+ const denied = DENY.get(name) ?? (member ? MEMBER.get(name) : declared.has(name) ? undefined : BARE.get(name));
462
+ if (denied)
463
+ report(denied, tok.pos, { api: apiName(st, i) });
464
+ if (member && prev && st[i - 2]?.t === 'id') {
465
+ const before = st[i - 2].v;
466
+ const after = AFTER.get(before)?.get(name);
467
+ if (after)
468
+ report(after, tok.pos, { api: `${cap(before)}.${cap(name)}` });
469
+ if (name === 'add' && ADD_WITH_FILE.has(before) && takesArguments(next, st[i + 2]) && namesFile(st.slice(i + 1)))
470
+ report(EXT(types_1.ScriptDetail.OpenDocument), tok.pos, { api: `${cap(before)}.Add` });
471
+ }
472
+ if (name === 'new' && next?.t === 'id') {
473
+ let j = i + 1;
474
+ while (st[j + 1]?.v === '.' && st[j + 2]?.t === 'id')
475
+ j += 2;
476
+ const cls = st[j].v;
477
+ const deniedClass = NEW_DENIED.get(cls);
478
+ if (deniedClass)
479
+ report(deniedClass, tok.pos, { api: `New ${st[j].v}` });
480
+ else if (j > i + 1 && !NEW_ALLOWED.has(cls))
481
+ report(EXT(types_1.ScriptDetail.ComObject), tok.pos, { api: `New ${st[i + 1].v}.${cls}` });
482
+ }
483
+ if (name === 'addressof')
484
+ report(DYN(types_1.ScriptDetail.Privilege), tok.pos, { api: 'AddressOf' });
485
+ // Run "Macro" and Application.Run name, args run the macro a string names. Run alone, as SlideShowSettings.Run, runs a show.
486
+ if (name === 'run' && (member || !declared.has(name)) && takesArguments(next, st[i + 2]))
487
+ report(DYN(types_1.ScriptDetail.Eval), tok.pos, { api: member ? `${cap(st[i - 2]?.v ?? '')}.Run` : 'Run' });
488
+ if (name === 'evaluate') {
489
+ const arg = next?.v === '(' ? st[i + 2] : next;
490
+ const close = next?.v === '(' ? st[i + 3] : st[i + 2];
491
+ if (arg?.t === 'str' && (!close || close.v === ')' || close.t === 'nl'))
492
+ formulaRules(arg.v.startsWith('=') ? arg.v : `=${arg.v}`, arg.pos, report);
493
+ else
494
+ report(DYN(types_1.ScriptDetail.Eval), tok.pos, { api: 'Evaluate' });
495
+ }
496
+ if (CHR.has(name) && next?.v === '(') {
497
+ const inner = argumentTokens(st, i + 1);
498
+ if (hidesLetter(inner))
499
+ charCodes++;
500
+ const ids = new Set(inner.filter(x => x.t === 'id').map(x => x.v));
501
+ // Chr(Asc(Mid(s, i, 1)) Xor k): a string decoded one character at a time.
502
+ if (ids.has('xor') || ((ids.has('asc') || ids.has('ascw') || ids.has('ascb')) && (ids.has('mid') || ids.has('mid$'))))
503
+ report([types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Decode], tok.pos, { api: 'Chr(Asc(...))' });
504
+ }
505
+ if (name === 'replace' && next?.v === '(') {
506
+ const args = splitArguments(argumentTokens(st, i + 1));
507
+ const [text, find, by] = args.map(a => (a.length === 1 && a[0].t === 'str' ? a[0].v : undefined));
508
+ // Replace("pXoXwXeXr", "X", ""): text hidden among filler in a string the macro wrote itself.
509
+ if (text !== undefined && find && by === '' && text.split(find).length - 1 >= 3)
510
+ report([types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Decode], tok.pos, { api: 'Replace("...")' });
511
+ }
512
+ }
513
+ if (charCodes >= 6)
514
+ report([types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.CharCodes], st[start]?.pos, { api: 'Chr', count: charCodes });
515
+ const chains = literalChains(st, start);
516
+ for (const c of chains)
517
+ if (c.pieces.length > 1)
518
+ joinedRules(c, c.pos, report);
519
+ // x = x & "...": a string built up one piece per statement, judged on what it holds once joined.
520
+ const [target, eq, again, amp] = st.slice(start);
521
+ const first = chains[0];
522
+ if (target?.t === 'id' && eq?.v === '=' && again?.t === 'id' && again.v === target.v && (amp?.v === '&' || amp?.v === '+') && first?.start === start + 4) {
523
+ const j = appends.get(target.v) ?? { pieces: [], pos: target.pos };
524
+ if (j.pieces.length < 100000)
525
+ j.pieces.push(...first.pieces);
526
+ appends.set(target.v, j);
527
+ }
528
+ }
529
+ /** Words of a payload: programs, script hosts, URLs, file types that run, and the objects that fetch and write files. */
530
+ const PAYLOAD_WORDS = [
531
+ 'powershell',
532
+ 'cmd.exe',
533
+ 'cmd /c',
534
+ 'wscript',
535
+ 'cscript',
536
+ 'mshta',
537
+ 'rundll32',
538
+ 'regsvr32',
539
+ 'certutil',
540
+ 'bitsadmin',
541
+ 'schtasks',
542
+ 'wmic',
543
+ 'http://',
544
+ 'https://',
545
+ 'ftp://',
546
+ '.exe',
547
+ '.dll',
548
+ '.vbs',
549
+ '.hta',
550
+ '.ps1',
551
+ '.bat',
552
+ '.scr',
553
+ '.lnk',
554
+ 'createobject',
555
+ 'getobject',
556
+ 'msxml2',
557
+ 'xmlhttp',
558
+ 'winhttp',
559
+ 'adodb',
560
+ 'savetofile',
561
+ 'responsebody',
562
+ 'shell.application',
563
+ 'wscript.shell',
564
+ 'filesystemobject',
565
+ 'winmgmts',
566
+ 'win32_process',
567
+ 'frombase64string',
568
+ 'downloadfile',
569
+ 'downloadstring',
570
+ 'webclient',
571
+ 'invoke-expression',
572
+ 'start-process',
573
+ '-encodedcommand',
574
+ 'executionpolicy',
575
+ 'urldownloadtofile',
576
+ 'kernel32',
577
+ 'virtualalloc',
578
+ 'createthread',
579
+ 'rtlmovememory',
580
+ '%temp%',
581
+ '%appdata%',
582
+ ];
583
+ /**
584
+ * A joined string is obfuscation when it spells a payload word no piece holds on its own, as "pow" & "ersh" & "ell",
585
+ * and is checked as any string literal is: encoded data, a long blob, a formula.
586
+ */
587
+ function joinedRules(j, pos, report, api) {
588
+ const text = j.pieces.join('');
589
+ const lower = text.toLowerCase();
590
+ const pieces = j.pieces.map(p => p.toLowerCase());
591
+ const hidden = PAYLOAD_WORDS.find(w => lower.includes(w) && !pieces.some(p => p.includes(w)));
592
+ if (hidden)
593
+ report([types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Concatenation], pos ?? j.pos, { count: j.pieces.length, ...(api ? { api } : {}), word: hidden });
594
+ if (j.pieces.length > 1) {
595
+ if (analyze_1.ENCODED.some(re => re.test(text)))
596
+ report([types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.EncodedString], pos ?? j.pos, { length: text.length });
597
+ else if ((0, analyze_1.isLongBlob)(text))
598
+ report([types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.LongString], pos ?? j.pos, { length: text.length });
599
+ if (/^[=+\-@]/.test(text))
600
+ formulaRules(text, pos ?? j.pos, report);
601
+ }
602
+ }
603
+ const CHR = new Set(['chr', 'chrw', 'chrb']);
604
+ /**
605
+ * Whether a Chr argument writes a character a string literal could hold as it is, which only hiding it explains:
606
+ * Chr(112) or Chr(115 - 3). Macros write control characters, the quote, the non-breaking space and smart quotes with
607
+ * Chr, and Chr(i) of a variable is a loop's work.
608
+ */
609
+ function hidesLetter(arg) {
610
+ if (!arg.length || !arg.every(t => t.t === 'num' || (t.t === 'op' && '+-*/\\()'.includes(t.v))))
611
+ return false;
612
+ if (arg.length === 1) {
613
+ const code = numberOf(arg[0].v);
614
+ return code >= 33 && code <= 126 && code !== 34;
615
+ }
616
+ return true;
617
+ }
618
+ /**
619
+ * The chains of string literals and Chr calls joined by & or +, as "p" & "ow" & Chr(101) & "r". Pieces joined around
620
+ * variables, as a & "," & b, are separate chains. A Chr of a computed code stands for a character no one can tell.
621
+ */
622
+ function literalChains(st, start) {
623
+ const chains = [];
624
+ let cur;
625
+ let i = start;
626
+ while (i < st.length) {
627
+ const tok = st[i];
628
+ let end = -1;
629
+ let piece = '';
630
+ if (tok.t === 'str') {
631
+ end = i + 1;
632
+ piece = tok.v;
633
+ }
634
+ else if (tok.t === 'id' && CHR.has(tok.v) && st[i + 1]?.v === '(') {
635
+ const arg = argumentTokens(st, i + 1);
636
+ end = i + 2 + arg.length + 1;
637
+ const code = arg.length === 1 && arg[0].t === 'num' ? numberOf(arg[0].v) : undefined;
638
+ piece = code !== undefined && code >= 0 && code < 0x110000 ? String.fromCodePoint(code) : '\ufffd';
639
+ }
640
+ if (end < 0) {
641
+ cur = undefined;
642
+ i++;
643
+ continue;
644
+ }
645
+ if (!cur) {
646
+ cur = { pieces: [], pos: tok.pos, start: i };
647
+ chains.push(cur);
648
+ }
649
+ cur.pieces.push(piece);
650
+ if (st[end]?.v === '&' || st[end]?.v === '+')
651
+ i = end + 1;
652
+ else {
653
+ cur = undefined;
654
+ i = end;
655
+ }
656
+ }
657
+ return chains;
658
+ }
659
+ /** A VBA number literal's value: decimal, &H hex or &O octal, with any type character. */
660
+ function numberOf(v) {
661
+ const m = /^&([hHoO])([0-9a-fA-F]+)/.exec(v);
662
+ return m ? Number.parseInt(m[2], /[hH]/.test(m[1]) ? 16 : 8) : Number.parseFloat(v);
663
+ }
664
+ /** Whether the arguments of an Add name a file or template: anything but Office's constants, numbers and named-argument names. */
665
+ function namesFile(args) {
666
+ return args.some((t, k) => (t.t === 'str' && t.v !== '') || (t.t === 'id' && !BUILTIN_CONSTANT.test(t.v) && args[k + 1]?.v !== ':=' && !['true', 'false'].includes(t.v)));
667
+ }
668
+ /** Whether a call's next tokens pass it arguments: anything but the end of the statement, "=", ")" or ",". */
669
+ function takesArguments(next, after) {
670
+ if (!next || next.t === 'nl')
671
+ return false;
672
+ if (next.v === '(')
673
+ return after !== undefined && after.v !== ')';
674
+ if (next.t === 'op' && ['=', ')', ',', '.', ':=', '&', '+', '-', '*', '/', '<>', '<', '>', '<=', '>='].includes(next.v))
675
+ return false;
676
+ return !(next.t === 'id' && ['then', 'else', 'and', 'or', 'xor', 'mod', 'is', 'to', 'step'].includes(next.v));
677
+ }
678
+ /** The tokens inside the parentheses that open at `open`. */
679
+ function argumentTokens(st, open) {
680
+ let depth = 0;
681
+ for (let j = open; j < st.length; j++) {
682
+ if (st[j].v === '(' && st[j].t === 'op')
683
+ depth++;
684
+ else if (st[j].v === ')' && st[j].t === 'op' && --depth === 0)
685
+ return st.slice(open + 1, j);
686
+ }
687
+ return st.slice(open + 1);
688
+ }
689
+ function splitArguments(tokens) {
690
+ const args = [[]];
691
+ let depth = 0;
692
+ for (const tok of tokens) {
693
+ if (tok.t === 'op' && tok.v === '(')
694
+ depth++;
695
+ if (tok.t === 'op' && tok.v === ')')
696
+ depth--;
697
+ if (tok.t === 'op' && tok.v === ',' && depth === 0)
698
+ args.push([]);
699
+ else
700
+ args[args.length - 1].push(tok);
701
+ }
702
+ return args;
703
+ }
704
+ /** The name a finding shows: the member with what it is a member of, as Workbooks.Open. */
705
+ function apiName(st, i) {
706
+ const own = cap(st[i].v);
707
+ return (st[i - 1]?.v === '.' || st[i - 1]?.v === '!') && st[i - 2]?.t === 'id' ? `${cap(st[i - 2].v)}.${own}` : own;
708
+ }
709
+ function stringRules(s, pos, report) {
710
+ if (analyze_1.ENCODED.some(re => re.test(s)))
711
+ report([types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.EncodedString], pos, { length: s.length });
712
+ else if ((0, analyze_1.isLongBlob)(s))
713
+ report([types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.LongString], pos, { length: s.length });
714
+ if (/^[=+\-@]/.test(s))
715
+ formulaRules(s, pos, report);
716
+ if (EXTERNAL_MACRO.test(s))
717
+ report(EXT(types_1.ScriptDetail.OpenDocument), pos, { api: 'a macro in another file' });
718
+ }
719
+ /** A formula a macro writes into a cell or evaluates, checked for the functions and DDE links that reach outside. */
720
+ function formulaRules(formula, pos, report) {
721
+ if (DDE_FORMULA.test(formula))
722
+ report(EXT(types_1.ScriptDetail.Dde), pos, { api: 'a DDE formula' });
723
+ for (const m of formula.matchAll(FORMULA_CALL)) {
724
+ const f = FORMULA_FUNCTIONS.get(m[1].toLowerCase().replace(/^_xlfn\./, ''));
725
+ if (f)
726
+ report(f, pos, { api: m[1].toUpperCase() });
727
+ }
728
+ }