@patrickdk77/office-defuse 0.2.4 → 0.3.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/README.md +10 -4
- package/dist/contained.d.ts +10 -5
- package/dist/contained.js +10 -7
- package/dist/context.d.ts +15 -11
- package/dist/context.js +26 -11
- package/dist/engine.js +5 -6
- package/dist/fields.js +1 -1
- package/dist/findings.js +3 -3
- package/dist/flat.js +4 -7
- package/dist/macros.d.ts +5 -0
- package/dist/macros.js +16 -5
- package/dist/msg.js +1 -1
- package/dist/objects.js +31 -10
- package/dist/odf.js +9 -10
- package/dist/ooxml.js +10 -10
- package/dist/ppt.js +4 -2
- package/dist/rtf.js +4 -2
- package/dist/score.d.ts +2 -0
- package/dist/score.js +12 -0
- package/dist/xls.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -285,9 +285,11 @@ A file comes as a `ContainedFile`. `name` is the part or entry name, a Packager
|
|
|
285
285
|
| `removed` | goes, with every reference to it | EMBEDDED/PLUGIN_REMOVED, `strip` |
|
|
286
286
|
| `scrubbed` with nothing written, or an error | goes, with every reference to it | EMBEDDED/PLUGIN_FAILED, `strip` |
|
|
287
287
|
|
|
288
|
-
A file goes whatever the overrides say about these findings. A file no plugin accepts gets the rule it gets without plugins, such as EMBEDDED/OLE_OBJECT, and a picture, which no rule names, stays.
|
|
288
|
+
A file goes whatever the overrides say about these findings. A file no plugin accepts gets the rule it gets without plugins, such as EMBEDDED/OLE_OBJECT, and a picture, which no rule names, stays. A file whose plugin failed gets that rule too, and goes.
|
|
289
289
|
|
|
290
|
-
|
|
290
|
+
A plugin's answer is final. A file a plugin passed, scrubbed or removed gets none of office-defuse's own findings: no EMBEDDED/PACKAGE, EMBEDDED/OLE_OBJECT or EMBEDDED/EQUATION, and no MACRO/VBA_PROJECT for `vbaProject.bin`. office-defuse does not look inside it either, so an object's storage is not searched for VBA projects or other objects. An Equation Editor object a plugin passes stays. Through file-defuse no plugin takes a bare object storage, so EMBEDDED/EQUATION still removes one there. A plugin's output for an object's storage must be a compound file, whose storages and streams then take the storage's place. Where the rewrite cannot change the container, as for an object stored in a PowerPoint record, the output keeps the file as it arrived, so a plugin's `scrubbed` or `removed` there fails the output check.
|
|
291
|
+
|
|
292
|
+
The findings a plugin returns go into the report with their `location` after the file's own and with `attachment` naming the file. A `reject` inside the file becomes `strip`, since the file goes and the file holding it stays. Each scores at its `weight`, and they are all the file adds to the score. PLUGIN_PASSED and PLUGIN_SCRUBBED weigh 0, in the upload's score and the output's. The check of the output does not ask the plugins again. It matches each file by the MD5 of its bytes against what the plugins kept, and reports the `outputFindings` the plugin gave for it.
|
|
291
293
|
|
|
292
294
|
The plugin context has `depth`, the file's depth in the upload, and `deadline`, which every file of the upload shares. An error with `code: 'DEFUSE_LIMIT'` or `code: 'DEFUSE_IO'` from a plugin stops the whole upload. At depth 0, a limit becomes LIMIT/TIME or LIMIT/NESTING_DEPTH, and an I/O error is thrown as its cause.
|
|
293
295
|
|
|
@@ -314,11 +316,13 @@ await disarmOffice(bytes, { filePlugins: [officePlugin()] });
|
|
|
314
316
|
|
|
315
317
|
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
318
|
|
|
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
|
|
319
|
+
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.
|
|
320
|
+
|
|
321
|
+
When a plugin passed or removed every module, the answers are final. The report holds MACRO/PLUGIN_PASSED and PLUGIN_REMOVED with the plugins' findings, and no MACRO/VBA_PROJECT, and the project scores what the plugins found in it. MACRO/VBA_PROJECT, at office-defuse's weight, reports a project with a module no plugin took or a plugin failed on, or one office-defuse removes for a reason of its own, such as a user form that holds an ActiveX control.
|
|
318
322
|
|
|
319
323
|
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
324
|
|
|
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.
|
|
325
|
+
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. Once the plugins answered for the project, kept or removed, these findings weigh 0, since the plugins scored the macros they run.
|
|
322
326
|
|
|
323
327
|
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
328
|
|
|
@@ -453,6 +457,8 @@ Three combinations add more. Code here means macros or DDE. Code or a run-progra
|
|
|
453
457
|
|
|
454
458
|
The bands are NONE at 0, LOW from 1, MEDIUM from 25, HIGH from 50 and CRITICAL from 80. `scoreWeights` and `scoreBands` change them. A finding of a kind office-defuse does not make, such as one from another package, scores at its `weight`, or 0 without one.
|
|
455
459
|
|
|
460
|
+
Content a plugin answered for scores at the weights of the plugin's findings alone, as [File plugins](#file-plugins) and [Script plugins](#script-plugins) describe. A workbook whose VBA project script-defuse keeps scores only what is left, such as the 10 of MACRO/COMPILED_CODE.
|
|
461
|
+
|
|
456
462
|
## Options
|
|
457
463
|
|
|
458
464
|
| Option | Default | Purpose |
|
package/dist/contained.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { RunContext, RunState } from './context';
|
|
1
|
+
import type { Reported, RunContext, RunState } from './context';
|
|
2
2
|
import { SpillSink } from './io';
|
|
3
|
-
import { type DefuseFinding, type OfficeOptions, type PluginContext } from './types';
|
|
3
|
+
import { OfficeCategory as C, OfficeDetail as D, type DefuseFinding, type OfficeOptions, type PluginContext } from './types';
|
|
4
4
|
/** The key under which office-defuse hands its run's options to a plugin. No other package can name it. */
|
|
5
5
|
export declare const RUN_OPTIONS: unique symbol;
|
|
6
6
|
/** A plugin context as office-defuse makes it. */
|
|
@@ -10,6 +10,11 @@ export type RunPluginContext = PluginContext & {
|
|
|
10
10
|
/** What the plugins decided about a file inside the file. Undefined from `decideContained` means no plugin took it. */
|
|
11
11
|
export interface ContainedDecision {
|
|
12
12
|
keep: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* A plugin passed, cleaned or removed the file. Its answer is final: the report holds it and what the plugin found,
|
|
15
|
+
* and office-defuse reports nothing of its own about the file.
|
|
16
|
+
*/
|
|
17
|
+
answered?: boolean;
|
|
13
18
|
/** The bytes a plugin kept in place of the file's own. */
|
|
14
19
|
replacement?: SpillSink;
|
|
15
20
|
}
|
|
@@ -43,9 +48,9 @@ export declare function pushContained(ctx: RunContext, found: DefuseFinding[] |
|
|
|
43
48
|
*/
|
|
44
49
|
export declare function decideContained(ctx: RunContext, inner: InnerFile): Promise<ContainedDecision | undefined>;
|
|
45
50
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
51
|
+
* Reports the rule a file gets for being there, such as EMBEDDED/OLE_OBJECT, when no plugin answered for the file. A
|
|
52
|
+
* plugin's answer is final: when one answered, nothing is reported, and the result only says whether the file stays.
|
|
48
53
|
*/
|
|
49
|
-
export declare function
|
|
54
|
+
export declare function ownFinding(ctx: RunContext, decision: ContainedDecision | undefined, category: C, detail: D, location: string, data?: Record<string, string | number>): Reported;
|
|
50
55
|
/** Bytes in a spill sink the run holds until it ends, as the rewrite takes a plugin's output. */
|
|
51
56
|
export declare function spillOf(run: RunState, bytes: Uint8Array): Promise<SpillSink>;
|
package/dist/contained.js
CHANGED
|
@@ -4,7 +4,7 @@ exports.RUN_OPTIONS = void 0;
|
|
|
4
4
|
exports.passOn = passOn;
|
|
5
5
|
exports.pushContained = pushContained;
|
|
6
6
|
exports.decideContained = decideContained;
|
|
7
|
-
exports.
|
|
7
|
+
exports.ownFinding = ownFinding;
|
|
8
8
|
exports.spillOf = spillOf;
|
|
9
9
|
const errors_1 = require("./errors");
|
|
10
10
|
const io_1 = require("./io");
|
|
@@ -65,7 +65,7 @@ async function decideContained(ctx, inner) {
|
|
|
65
65
|
if (expected) {
|
|
66
66
|
ctx.report(types_1.OfficeCategory.Embedded, expected.result === 'passed' ? types_1.OfficeDetail.PluginPassed : types_1.OfficeDetail.PluginScrubbed, inner.location, { plugin: expected.plugin, name: name ?? '' });
|
|
67
67
|
pushContained(ctx, expected.findings, inner.location, name);
|
|
68
|
-
return { keep: true };
|
|
68
|
+
return { keep: true, answered: true };
|
|
69
69
|
}
|
|
70
70
|
if (!plugins.length)
|
|
71
71
|
return undefined;
|
|
@@ -108,6 +108,7 @@ async function decideContained(ctx, inner) {
|
|
|
108
108
|
}
|
|
109
109
|
else
|
|
110
110
|
ctx.removal(types_1.OfficeCategory.Embedded, r.result === 'removed' ? types_1.OfficeDetail.PluginRemoved : types_1.OfficeDetail.PluginFailed, inner.location, data);
|
|
111
|
+
const answered = keep || r.result === 'removed';
|
|
111
112
|
pushContained(ctx, r.findings, inner.location, name);
|
|
112
113
|
const replacement = keep && out.length > 0 ? out : undefined;
|
|
113
114
|
if (replacement) {
|
|
@@ -117,7 +118,7 @@ async function decideContained(ctx, inner) {
|
|
|
117
118
|
}
|
|
118
119
|
else
|
|
119
120
|
await out.dispose();
|
|
120
|
-
return { keep, replacement };
|
|
121
|
+
return { keep, replacement, answered };
|
|
121
122
|
}
|
|
122
123
|
catch (e) {
|
|
123
124
|
await out.dispose();
|
|
@@ -137,11 +138,13 @@ async function decideContained(ctx, inner) {
|
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
141
|
+
* Reports the rule a file gets for being there, such as EMBEDDED/OLE_OBJECT, when no plugin answered for the file. A
|
|
142
|
+
* plugin's answer is final: when one answered, nothing is reported, and the result only says whether the file stays.
|
|
142
143
|
*/
|
|
143
|
-
function
|
|
144
|
-
|
|
144
|
+
function ownFinding(ctx, decision, category, detail, location, data) {
|
|
145
|
+
if (decision?.answered)
|
|
146
|
+
return { action: decision.keep ? 'info' : 'strip' };
|
|
147
|
+
return ctx.report(category, detail, location, data);
|
|
145
148
|
}
|
|
146
149
|
/** Bytes in a spill sink the run holds until it ends, as the rewrite takes a plugin's output. */
|
|
147
150
|
async function spillOf(run, bytes) {
|
package/dist/context.d.ts
CHANGED
|
@@ -68,11 +68,6 @@ export interface RunState {
|
|
|
68
68
|
kept: Map<string, Expected>;
|
|
69
69
|
/** Plugin output that waits for the rewrite. The run disposes of it when it ends. */
|
|
70
70
|
held: SpillSink[];
|
|
71
|
-
/**
|
|
72
|
-
* Findings whose content a plugin kept, such as EMBEDDED/PACKAGE for a package a plugin passed. They stay in the
|
|
73
|
-
* report as info and leave the score to what the plugin found, as pdf-defuse leaves out a kept attached file.
|
|
74
|
-
*/
|
|
75
|
-
superseded: WeakSet<DefuseFinding>;
|
|
76
71
|
/** Sources the rewrite reads from, such as a compound file a plugin returned, which the run closes when it ends. */
|
|
77
72
|
sources: ByteSource[];
|
|
78
73
|
/** The container, once the format rules have read it. */
|
|
@@ -82,12 +77,9 @@ export interface RunState {
|
|
|
82
77
|
* that run one of its macros by name then stay too.
|
|
83
78
|
*/
|
|
84
79
|
macrosKept?: boolean;
|
|
80
|
+
/** The script plugins answered for the document's own VBA project, whether they kept it or removed it. */
|
|
81
|
+
macrosAnswered?: boolean;
|
|
85
82
|
}
|
|
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;
|
|
91
83
|
/** A finding the rules made, with its action, which the rewrite reads. Past 200 of one kind, `finding` is absent. */
|
|
92
84
|
export interface Reported {
|
|
93
85
|
action: OfficeFindingAction;
|
|
@@ -125,11 +117,23 @@ export declare class RunContext {
|
|
|
125
117
|
withPlan(): PlanContext;
|
|
126
118
|
/**
|
|
127
119
|
* Records a finding. Past 200 of one kind, as in pdf-defuse, one more finding counts the rest. `action` replaces the
|
|
128
|
-
* action the overrides give, for content whose fate a plugin decided.
|
|
120
|
+
* action the overrides give, for content whose fate a plugin decided. Such a finding as info weighs nothing.
|
|
129
121
|
*/
|
|
130
122
|
report(category: C, detail: D, location?: string, data?: Record<string, string | number>, action?: OfficeFindingAction): Reported;
|
|
123
|
+
/**
|
|
124
|
+
* Reports a link, callback or field that runs the macro `name`. When the script plugins answered for the document's
|
|
125
|
+
* own VBA project, a trigger of one of its macros follows their answer: it stays, as info, when they kept the
|
|
126
|
+
* project, and the overrides decide it when they removed the project. Either way it weighs nothing, since the
|
|
127
|
+
* plugins scored the macro it runs.
|
|
128
|
+
*/
|
|
129
|
+
macroTrigger(category: C, detail: D, location: string, data: Record<string, string | number>, name: string | undefined): Reported;
|
|
131
130
|
/** Reports content that goes whatever the overrides say: its action is strip, or reject when the caller asked for that. */
|
|
132
131
|
removal(category: C, detail: D, location?: string, data?: Record<string, string | number>): Reported;
|
|
132
|
+
/**
|
|
133
|
+
* Takes back a finding reported before a plugin answered for its content, whose answer is final. A finding past the
|
|
134
|
+
* first 200 of its kind was only counted, so there is none to take back.
|
|
135
|
+
*/
|
|
136
|
+
retract(finding: DefuseFinding | undefined): void;
|
|
133
137
|
/** Adds findings from another package as they are, such as those a plugin made inside a file. */
|
|
134
138
|
push(findings: DefuseFinding[]): void;
|
|
135
139
|
/** Reports a link, classified by pdf-defuse's checks of its target. */
|
package/dist/context.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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;
|
|
5
4
|
const links_1 = require("./links");
|
|
6
5
|
const plan_1 = require("./plan");
|
|
6
|
+
const score_1 = require("./score");
|
|
7
7
|
const types_1 = require("./types");
|
|
8
8
|
const xml_1 = require("./xml");
|
|
9
9
|
const OLE_SIGNATURE = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
|
|
@@ -15,13 +15,6 @@ const clip = (raw, n = 200) => {
|
|
|
15
15
|
exports.clip = clip;
|
|
16
16
|
/** A macro of the document's own project, by name: Macro, Module.Macro, or Excel's [0]!Macro for this workbook. */
|
|
17
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
|
-
}
|
|
25
18
|
/** What one inspection holds while the format rules run. */
|
|
26
19
|
class RunContext {
|
|
27
20
|
factory;
|
|
@@ -61,7 +54,7 @@ class RunContext {
|
|
|
61
54
|
}
|
|
62
55
|
/**
|
|
63
56
|
* Records a finding. Past 200 of one kind, as in pdf-defuse, one more finding counts the rest. `action` replaces the
|
|
64
|
-
* action the overrides give, for content whose fate a plugin decided.
|
|
57
|
+
* action the overrides give, for content whose fate a plugin decided. Such a finding as info weighs nothing.
|
|
65
58
|
*/
|
|
66
59
|
report(category, detail, location, data, action) {
|
|
67
60
|
const act = action ?? this.factory.actionFor(category, detail);
|
|
@@ -74,7 +67,7 @@ class RunContext {
|
|
|
74
67
|
const finding = this.factory.make(category, detail, location, data);
|
|
75
68
|
finding.action = act;
|
|
76
69
|
if (action === 'info')
|
|
77
|
-
|
|
70
|
+
(0, score_1.pluginDecided)(finding);
|
|
78
71
|
this.shared.findings.push(finding);
|
|
79
72
|
return { action: act, finding };
|
|
80
73
|
}
|
|
@@ -86,17 +79,39 @@ class RunContext {
|
|
|
86
79
|
const finding = this.factory.make(category, detail, 'additional occurrences', counted);
|
|
87
80
|
finding.action = act;
|
|
88
81
|
if (action === 'info')
|
|
89
|
-
|
|
82
|
+
(0, score_1.pluginDecided)(finding);
|
|
90
83
|
this.shared.findings.push(finding);
|
|
91
84
|
}
|
|
92
85
|
counted.count = Number(counted.count) + 1;
|
|
93
86
|
return { action: act };
|
|
94
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Reports a link, callback or field that runs the macro `name`. When the script plugins answered for the document's
|
|
90
|
+
* own VBA project, a trigger of one of its macros follows their answer: it stays, as info, when they kept the
|
|
91
|
+
* project, and the overrides decide it when they removed the project. Either way it weighs nothing, since the
|
|
92
|
+
* plugins scored the macro it runs.
|
|
93
|
+
*/
|
|
94
|
+
macroTrigger(category, detail, location, data, name) {
|
|
95
|
+
const own = name !== undefined && this.run.macrosAnswered === true && OWN_MACRO.test(name.trim());
|
|
96
|
+
const r = this.report(category, detail, location, data, own && this.run.macrosKept ? 'info' : undefined);
|
|
97
|
+
if (own && r.finding)
|
|
98
|
+
(0, score_1.pluginDecided)(r.finding);
|
|
99
|
+
return r;
|
|
100
|
+
}
|
|
95
101
|
/** Reports content that goes whatever the overrides say: its action is strip, or reject when the caller asked for that. */
|
|
96
102
|
removal(category, detail, location, data) {
|
|
97
103
|
const act = this.factory.actionFor(category, detail);
|
|
98
104
|
return this.report(category, detail, location, data, act === 'info' ? 'strip' : act);
|
|
99
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Takes back a finding reported before a plugin answered for its content, whose answer is final. A finding past the
|
|
108
|
+
* first 200 of its kind was only counted, so there is none to take back.
|
|
109
|
+
*/
|
|
110
|
+
retract(finding) {
|
|
111
|
+
const i = finding ? this.shared.findings.indexOf(finding) : -1;
|
|
112
|
+
if (i >= 0)
|
|
113
|
+
this.shared.findings.splice(i, 1);
|
|
114
|
+
}
|
|
100
115
|
/** Adds findings from another package as they are, such as those a plugin made inside a file. */
|
|
101
116
|
push(findings) {
|
|
102
117
|
this.shared.findings.push(...findings);
|
package/dist/engine.js
CHANGED
|
@@ -59,21 +59,21 @@ const xmlLimitsOf = (limits) => ({
|
|
|
59
59
|
entityExpansions: limits.xmlEntityExpansions,
|
|
60
60
|
entitySize: limits.xmlEntitySize,
|
|
61
61
|
});
|
|
62
|
-
const PLUGIN_KEPT = new Set([types_1.OfficeDetail.PluginPassed, types_1.OfficeDetail.PluginScrubbed]);
|
|
63
62
|
/**
|
|
64
63
|
* `incomplete` marks a run that stopped before it read the whole package. It is rejected without a score, whatever
|
|
65
64
|
* the actions say, because an override can make the finding that stopped it look harmless. In the check of an output,
|
|
66
65
|
* `isOutput`, the files plugins kept count in the score; in an upload they do not, and a file a plugin cleaned makes
|
|
67
66
|
* the upload strippable, since the output must be written.
|
|
68
67
|
*/
|
|
69
|
-
function finish(findings, options, incomplete, format, isOutput = false
|
|
68
|
+
function finish(findings, options, incomplete, format, isOutput = false) {
|
|
70
69
|
const named = { format: format?.format ?? '', mimeType: format?.mimeType ?? '' };
|
|
71
70
|
if (incomplete)
|
|
72
71
|
return { status: 'rejected', score: null, risk: types_1.OfficeRisk.Unknown, findings, ...named };
|
|
73
72
|
const rejected = findings.some(f => f.action === 'reject');
|
|
74
73
|
const strippable = findings.some(f => f.action === 'strip' || (!isOutput && f.detail === types_1.OfficeDetail.PluginScrubbed));
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
// What a plugin decided counts at the weights of the plugin's own findings; the PLUGIN_* findings that record its
|
|
75
|
+
// answer weigh nothing.
|
|
76
|
+
const { score, risk } = (0, score_1.scoreFindings)(findings, options);
|
|
77
77
|
return { status: rejected ? 'rejected' : strippable ? 'strippable' : 'clean', score, risk, findings, ...named };
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
@@ -167,7 +167,6 @@ function newRun(options, depth, deadline, verify) {
|
|
|
167
167
|
verify,
|
|
168
168
|
kept: new Map(),
|
|
169
169
|
held: [],
|
|
170
|
-
superseded: new WeakSet(),
|
|
171
170
|
sources: [],
|
|
172
171
|
};
|
|
173
172
|
}
|
|
@@ -246,7 +245,7 @@ async function analyze(run, source) {
|
|
|
246
245
|
findings.push(...ctx.findings);
|
|
247
246
|
if (run.temp.ioError !== undefined)
|
|
248
247
|
throw run.temp.ioError;
|
|
249
|
-
return finish(findings, options, false, ctx.format, isOutput
|
|
248
|
+
return finish(findings, options, false, ctx.format, isOutput);
|
|
250
249
|
}
|
|
251
250
|
catch (e) {
|
|
252
251
|
if (src.failure !== undefined)
|
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.
|
|
32
|
+
actions.push(ctx.macroTrigger(types_1.OfficeCategory.Field, types_1.OfficeDetail.Macrobutton, location, { ...data, macro: (0, context_1.clip)(words[1]?.text ?? '') }, 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
|
@@ -76,7 +76,7 @@ def(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.UnsupportedAlgorithm,
|
|
|
76
76
|
// MACRO
|
|
77
77
|
def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, 'strip', 70, 'The document holds VBA macros');
|
|
78
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',
|
|
79
|
+
def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.PluginPassed, 'info', 0, 'A VBA module was kept by <plugin {plugin}|a plugin>');
|
|
80
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
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');
|
|
82
82
|
def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.XlmMacrosheet, 'strip', 70, 'The workbook holds an Excel 4.0 macro sheet');
|
|
@@ -137,8 +137,8 @@ def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Applet, 'strip', 60, '
|
|
|
137
137
|
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Plugin, 'strip', 50, 'An embedded browser plug-in object');
|
|
138
138
|
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, 'strip', 70, 'An Equation Editor 3.0 object, which CVE-2017-11882 and CVE-2018-0802 exploit');
|
|
139
139
|
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Attachment, 'strip', 30, '<An attached file "{name}"|An attached file> (<{type}|unknown type>)');
|
|
140
|
-
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.PluginPassed, 'info',
|
|
141
|
-
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.PluginScrubbed, 'info',
|
|
140
|
+
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.PluginPassed, 'info', 0, 'An embedded file was kept by <plugin {plugin}|a plugin> with nothing removed');
|
|
141
|
+
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.PluginScrubbed, 'info', 0, 'An embedded file was cleaned and kept by <plugin {plugin}|a plugin>');
|
|
142
142
|
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.PluginFailed, 'strip', 0, '<Plugin {plugin}|A plugin> failed, so the embedded file was removed');
|
|
143
143
|
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.PluginRemoved, 'strip', 0, 'An embedded file was removed by <plugin {plugin}|a plugin>');
|
|
144
144
|
// CONTROL
|
package/dist/flat.js
CHANGED
|
@@ -281,13 +281,10 @@ async function decideBinaries(ctx, odf) {
|
|
|
281
281
|
const object = plan.objects.find(o => o.index === parent.index);
|
|
282
282
|
if (object) {
|
|
283
283
|
decided.add(parent.index);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
else if (decision || object.strips)
|
|
284
|
+
// A plugin's answer on the object's file is final, so the object's own finding is taken back.
|
|
285
|
+
if (decision?.answered)
|
|
286
|
+
ctx.retract(object.finding);
|
|
287
|
+
if (!decision?.keep && (decision || object.strips))
|
|
291
288
|
plan.element(exports.FLAT_ODF_PART, parent.index, { remove: true });
|
|
292
289
|
}
|
|
293
290
|
else if (decision && !decision.keep)
|
package/dist/macros.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ export interface VbaDecision {
|
|
|
8
8
|
reason?: string;
|
|
9
9
|
/** The designer storages of the project's user forms, which stay with it and get no object rules of their own. */
|
|
10
10
|
designers?: CfbEntry[];
|
|
11
|
+
/**
|
|
12
|
+
* A plugin passed or removed every module, and the project stays or goes by those answers alone. They are final:
|
|
13
|
+
* the report holds them, and nothing of office-defuse's own about the project.
|
|
14
|
+
*/
|
|
15
|
+
answered?: boolean;
|
|
11
16
|
}
|
|
12
17
|
/**
|
|
13
18
|
* Shows each module of the VBA project in `vba` to the script plugins. The first plugin that accepts a module decides
|
package/dist/macros.js
CHANGED
|
@@ -41,12 +41,14 @@ async function decideVba(ctx, cfb, vba, location, plan) {
|
|
|
41
41
|
if (!(await (0, vba_1.isPurged)(cfb, project)))
|
|
42
42
|
ctx.removal(types_1.OfficeCategory.Macro, types_1.OfficeDetail.CompiledCode, location, { storage: location });
|
|
43
43
|
run.macrosKept = true;
|
|
44
|
-
|
|
44
|
+
run.macrosAnswered = true;
|
|
45
|
+
return { keep: true, designers, answered: true };
|
|
45
46
|
}
|
|
46
47
|
if (!plugins.length)
|
|
47
48
|
return { keep: false };
|
|
48
49
|
const start = ctx.findings.length;
|
|
49
50
|
let keep = project.modules.length > 0;
|
|
51
|
+
let answered = keep;
|
|
50
52
|
const used = new Set();
|
|
51
53
|
for (const [i, m] of project.modules.entries()) {
|
|
52
54
|
const at = `${location}/${(0, cfb_1.showName)(m.stream.name)}`;
|
|
@@ -78,6 +80,8 @@ async function decideVba(ctx, cfb, vba, location, plan) {
|
|
|
78
80
|
ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.PluginPassed, at, data);
|
|
79
81
|
else {
|
|
80
82
|
keep = false;
|
|
83
|
+
if (r.result !== 'removed')
|
|
84
|
+
answered = false;
|
|
81
85
|
ctx.removal(types_1.OfficeCategory.Macro, types_1.OfficeDetail.PluginRemoved, at, data);
|
|
82
86
|
}
|
|
83
87
|
pushScript(ctx, r.findings);
|
|
@@ -85,15 +89,21 @@ async function decideVba(ctx, cfb, vba, location, plan) {
|
|
|
85
89
|
catch (e) {
|
|
86
90
|
(0, contained_1.passOn)(run, e);
|
|
87
91
|
keep = false;
|
|
92
|
+
answered = false;
|
|
88
93
|
ctx.removal(types_1.OfficeCategory.Macro, types_1.OfficeDetail.PluginFailed, at, data);
|
|
89
94
|
}
|
|
90
95
|
break;
|
|
91
96
|
}
|
|
92
|
-
if (!decided)
|
|
97
|
+
if (!decided) {
|
|
93
98
|
keep = false;
|
|
99
|
+
answered = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!keep) {
|
|
103
|
+
if (answered)
|
|
104
|
+
run.macrosAnswered = true;
|
|
105
|
+
return { keep: false, answered };
|
|
94
106
|
}
|
|
95
|
-
if (!keep)
|
|
96
|
-
return { keep: false };
|
|
97
107
|
const passed = { result: 'passed', plugin: [...used].join(', '), findings: ctx.findings.slice(start).map(f => ({ ...f, data: f.data && { ...f.data } })) };
|
|
98
108
|
const designers = await formDesigners(ctx, cfb, project, location);
|
|
99
109
|
if (!designers)
|
|
@@ -117,7 +127,8 @@ async function decideVba(ctx, cfb, vba, location, plan) {
|
|
|
117
127
|
ctx.removal(types_1.OfficeCategory.Macro, types_1.OfficeDetail.CompiledCode, location, { storage: location });
|
|
118
128
|
}
|
|
119
129
|
run.macrosKept = true;
|
|
120
|
-
|
|
130
|
+
run.macrosAnswered = true;
|
|
131
|
+
return { keep: true, designers, answered: true };
|
|
121
132
|
}
|
|
122
133
|
/**
|
|
123
134
|
* The designer storages of the project's user forms, beside its VBA storage, named as their modules. Undefined when
|
package/dist/msg.js
CHANGED
|
@@ -231,7 +231,7 @@ async function attachment(ctx, cfb, data, file, info, plan) {
|
|
|
231
231
|
const decision = await (0, contained_1.decideContained)(ctx, { ...file, chunks: () => (0, io_1.chunksOf)(source, data.size) });
|
|
232
232
|
if (decision?.replacement)
|
|
233
233
|
plan?.replace.set(data, decision.replacement);
|
|
234
|
-
const r =
|
|
234
|
+
const r = (0, contained_1.ownFinding)(ctx, decision, types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Attachment, file.location, { ...info, size: data.size });
|
|
235
235
|
return decision && !decision.keep ? { ...r, action: 'strip' } : r;
|
|
236
236
|
}
|
|
237
237
|
/** The fixed-size values of a property stream (MS-OXMSG 2.4.2), by property id. An attachment's has an 8-byte header. */
|
package/dist/objects.js
CHANGED
|
@@ -66,8 +66,17 @@ async function compoundRules(ctx, cfb, prefix, rootIsObject, plan, rootDecision)
|
|
|
66
66
|
if (r.action === 'strip' && e.kind !== 'root')
|
|
67
67
|
plan?.drop.add(e);
|
|
68
68
|
};
|
|
69
|
+
/** Objects a plugin answered for. Its answer is final, so nothing inside them gets the rules here. */
|
|
70
|
+
const answered = new Set();
|
|
71
|
+
const inAnswered = (e) => {
|
|
72
|
+
for (let p = e.parent; p; p = p.parent)
|
|
73
|
+
if (answered.has(p))
|
|
74
|
+
return true;
|
|
75
|
+
return false;
|
|
76
|
+
};
|
|
77
|
+
// A storage comes after the storage that holds it, so an object is decided before what it holds.
|
|
69
78
|
for (const e of cfb.entries) {
|
|
70
|
-
if (e.kind === 'stream' || inDesigner(e))
|
|
79
|
+
if (e.kind === 'stream' || inDesigner(e) || inAnswered(e))
|
|
71
80
|
continue;
|
|
72
81
|
const child = (name) => e.children.find(c => upper(c) === name);
|
|
73
82
|
const names = new Set(e.children.map(upper));
|
|
@@ -77,8 +86,16 @@ async function compoundRules(ctx, cfb, prefix, rootIsObject, plan, rootDecision)
|
|
|
77
86
|
: [...names].some(n => OBJECT_STREAMS.has(n) || CONTROL_STREAMS.has(n)) ||
|
|
78
87
|
(parent === 'OBJECTPOOL' && e.parent?.parent?.kind === 'root') ||
|
|
79
88
|
(e.parent?.kind === 'root' && /^MBD[0-9A-F]{8}$/i.test(e.name));
|
|
80
|
-
if (isObject)
|
|
89
|
+
if (isObject) {
|
|
81
90
|
objects.add(e);
|
|
91
|
+
const decided = await objectRules(ctx, cfb, e, at(prefix, e), plan, e.kind === 'root' ? rootDecision : undefined);
|
|
92
|
+
for (const r of decided.found)
|
|
93
|
+
drop(r, e);
|
|
94
|
+
if (decided.answered) {
|
|
95
|
+
answered.add(e);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
82
99
|
// A VBA project: a VBA storage with a dir stream, which names the modules (MS-OVBA 2.2). The storage that holds
|
|
83
100
|
// the project goes, or the VBA storage when that is the root. With script plugins, the project stays when they
|
|
84
101
|
// pass every module and the rewrite can take out its compiled code: in the file the rewrite writes, and outside any
|
|
@@ -91,15 +108,16 @@ async function compoundRules(ctx, cfb, prefix, rootIsObject, plan, rootDecision)
|
|
|
91
108
|
if (decision?.keep)
|
|
92
109
|
for (const d of decision.designers ?? [])
|
|
93
110
|
designers.add(d);
|
|
94
|
-
|
|
111
|
+
// The plugins' answers are final: the report holds them, and nothing of office-defuse's own about the project.
|
|
112
|
+
if (decision?.answered)
|
|
113
|
+
drop({ action: decision.keep ? 'info' : 'strip' }, e.kind === 'root' ? vba : e);
|
|
114
|
+
else
|
|
115
|
+
drop(ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, at(prefix, vba), data), e.kind === 'root' ? vba : e);
|
|
95
116
|
}
|
|
96
117
|
if (e.kind === 'storage' && (upper(e) === '_VBA_PROJECT_CUR' || upper(e) === 'MACROS') && e.children.length && !projects.has(e)) {
|
|
97
118
|
// Excel's and Word's project storages, without the dir stream a whole project has.
|
|
98
119
|
drop(ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, at(prefix, e), { storage: at(prefix, e) }), e);
|
|
99
120
|
}
|
|
100
|
-
if (isObject)
|
|
101
|
-
for (const r of await objectRules(ctx, cfb, e, at(prefix, e), plan, e.kind === 'root' ? rootDecision : undefined))
|
|
102
|
-
drop(r, e);
|
|
103
121
|
}
|
|
104
122
|
}
|
|
105
123
|
/** Reads an OLE object's storage: its class, its CompObj and OLE streams, and Packager's native data. */
|
|
@@ -186,6 +204,7 @@ async function objectFile(ctx, cfb, e, d, location, plan) {
|
|
|
186
204
|
* One OLE object's storage: a control, a linked object, or an embedded one, with what it holds. Returns its findings,
|
|
187
205
|
* and a strip when a plugin removed the object. In the file the rewrite writes, `plan` is set and an embedded object
|
|
188
206
|
* goes to the plugins first. `given` is the verdict on an object whose container showed it to the plugins already.
|
|
207
|
+
* `answered` says a plugin answered for the object, so nothing else here looks at it or at what it holds.
|
|
189
208
|
*/
|
|
190
209
|
async function objectRules(ctx, cfb, e, location, plan, given) {
|
|
191
210
|
const d = await describeObject(cfb, e, location);
|
|
@@ -203,11 +222,11 @@ async function objectRules(ctx, cfb, e, location, plan, given) {
|
|
|
203
222
|
// a metafile beside them, goes on its own.
|
|
204
223
|
const problem = await (0, forms_1.controlStorageProblem)(cfb, e, forms_1.WORD_CONTROL_STREAMS);
|
|
205
224
|
if (problem)
|
|
206
|
-
return [ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, { ...data, reason: (0, context_1.clip)(problem) })];
|
|
225
|
+
return { found: [ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, { ...data, reason: (0, context_1.clip)(problem) })], answered: false };
|
|
207
226
|
const print = e.children.find(c => c.kind === 'stream' && c.name === '\x03PRINT');
|
|
208
227
|
if (print && ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ControlPicture, `${location}/${(0, cfb_1.showName)(print.name)}`, { storage: location, bytes: print.size }).action === 'strip')
|
|
209
228
|
plan?.drop.add(print);
|
|
210
|
-
return [ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.FormsControl, location, data)];
|
|
229
|
+
return { found: [ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.FormsControl, location, data)], answered: false };
|
|
211
230
|
}
|
|
212
231
|
const found = [];
|
|
213
232
|
if (d.pkg) {
|
|
@@ -219,13 +238,15 @@ async function objectRules(ctx, cfb, e, location, plan, given) {
|
|
|
219
238
|
found.push(linkedObject(ctx, d, location, { storage: location, ...(d.clsid ? { clsid: d.clsid } : {}) }));
|
|
220
239
|
else {
|
|
221
240
|
const decision = given ?? (plan ? await objectFile(ctx, cfb, e, d, location, plan) : undefined);
|
|
222
|
-
|
|
241
|
+
if (decision?.answered)
|
|
242
|
+
return { found: [...found, { action: decision.keep ? 'info' : 'strip' }], answered: true };
|
|
243
|
+
found.push(ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data));
|
|
223
244
|
if (decision && !decision.keep)
|
|
224
245
|
found.push({ action: 'strip' });
|
|
225
246
|
}
|
|
226
247
|
if (d.equation)
|
|
227
248
|
found.push(ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, location, data));
|
|
228
|
-
return found;
|
|
249
|
+
return { found, answered: false };
|
|
229
250
|
}
|
|
230
251
|
/**
|
|
231
252
|
* Reports a linked object. A URL moniker's target loads from outside the file whatever its text looks like: a
|
package/dist/odf.js
CHANGED
|
@@ -376,14 +376,14 @@ async function inspectOdf(ctx) {
|
|
|
376
376
|
const key = (0, zip_1.nameKey)(e.name);
|
|
377
377
|
const decision = await (0, contained_1.decideContained)(ctx, { name: e.name, location: e.name, chunks: () => zip.read(e) });
|
|
378
378
|
if (decision) {
|
|
379
|
-
decided.set(key, decision
|
|
379
|
+
decided.set(key, decision);
|
|
380
380
|
if (!decision.keep)
|
|
381
381
|
plan.drop.add(key);
|
|
382
382
|
else if (decision.replacement)
|
|
383
383
|
plan.replace.set(key, decision.replacement);
|
|
384
384
|
}
|
|
385
385
|
if (!oleRefs.get(key)) {
|
|
386
|
-
const r =
|
|
386
|
+
const r = (0, contained_1.ownFinding)(ctx, decision, types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleFile, e.name, { part: e.name });
|
|
387
387
|
if (!decision && r.action === 'strip')
|
|
388
388
|
plan.drop.add(key);
|
|
389
389
|
}
|
|
@@ -398,15 +398,14 @@ async function inspectOdf(ctx) {
|
|
|
398
398
|
}
|
|
399
399
|
const files = new Set(zip.entries.filter(e => !e.directory).map(e => (0, zip_1.nameKey)(e.name)));
|
|
400
400
|
for (const o of plan.objects) {
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
401
|
+
const decision = decided.get(o.target);
|
|
402
|
+
// A plugin's answer on the object's file is final, so the object's own finding is taken back.
|
|
403
|
+
if (decision?.answered)
|
|
404
|
+
ctx.retract(o.finding);
|
|
405
|
+
const kept = decision?.keep;
|
|
406
|
+
// A plugin kept the object's file, so the object stays.
|
|
407
|
+
if (kept === true)
|
|
408
408
|
continue;
|
|
409
|
-
}
|
|
410
409
|
if (kept === false || o.strips) {
|
|
411
410
|
plan.element(o.part, o.index, { remove: true });
|
|
412
411
|
if (files.has(o.target))
|
package/dist/ooxml.js
CHANGED
|
@@ -386,7 +386,7 @@ class PartRules {
|
|
|
386
386
|
if (a.ns !== '' || !a.value.trim() || !/^(on[A-Z]|get[A-Z]|loadImage$)/.test(a.local))
|
|
387
387
|
continue;
|
|
388
388
|
// The callback goes and the control stays, doing nothing, unless it calls a macro of a project that stays.
|
|
389
|
-
if (this.ctx.
|
|
389
|
+
if (this.ctx.macroTrigger(types_1.OfficeCategory.Ui, types_1.OfficeDetail.Callback, this.part, { callback: `${a.local}=${(0, context_1.clip)(a.value)}` }, a.value).action === 'strip') {
|
|
390
390
|
this.ctx.plan.element(this.part, el.index, { strip: new Set([(0, plan_1.attributeKey)('', a.local)]) });
|
|
391
391
|
}
|
|
392
392
|
}
|
|
@@ -419,7 +419,7 @@ class PartRules {
|
|
|
419
419
|
else if (XDR_NS.has(ns)) {
|
|
420
420
|
const macro = (0, xml_1.attr)(el, '', 'macro');
|
|
421
421
|
// The shape stays, and clicking it runs nothing.
|
|
422
|
-
if (macro?.trim() && this.ctx.
|
|
422
|
+
if (macro?.trim() && this.ctx.macroTrigger(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { macro: (0, context_1.clip)(macro) }, macro).action === 'strip') {
|
|
423
423
|
this.ctx.plan.element(this.part, el.index, { strip: new Set([(0, plan_1.attributeKey)('', 'macro')]) });
|
|
424
424
|
}
|
|
425
425
|
}
|
|
@@ -478,7 +478,7 @@ class PartRules {
|
|
|
478
478
|
if (ns === VML_EXCEL_NS && el.local === 'FmlaMacro' && this.macroText) {
|
|
479
479
|
const macro = this.macroText.shown.text.trim();
|
|
480
480
|
if (this.macroText.shown.content)
|
|
481
|
-
this.remove(this.ctx.
|
|
481
|
+
this.remove(this.ctx.macroTrigger(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { macro: (0, context_1.clip)(macro) }, macro), this.macroText);
|
|
482
482
|
this.macroText = undefined;
|
|
483
483
|
}
|
|
484
484
|
if (this.hyperlinkBase && EXTENDED_NS.has(ns) && el.local === 'HyperlinkBase') {
|
|
@@ -519,7 +519,7 @@ class PartRules {
|
|
|
519
519
|
this.remove(this.ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, this.part, data), el);
|
|
520
520
|
else if (action.startsWith('ppaction://macro')) {
|
|
521
521
|
const name = /[?&]name=([^&]*)/i.exec(written)?.[1] ?? '';
|
|
522
|
-
this.remove(this.ctx.
|
|
522
|
+
this.remove(this.ctx.macroTrigger(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, data, safeDecode(name)), el);
|
|
523
523
|
}
|
|
524
524
|
else if (action.startsWith('ppaction://ole'))
|
|
525
525
|
this.remove(this.ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, this.part, data), el);
|
|
@@ -754,25 +754,25 @@ async function partRules(ctx, p) {
|
|
|
754
754
|
// With script plugins, a VBA project part is read here, so its modules go to them, rather than to the file plugins.
|
|
755
755
|
const vbaDecision = p.vba ?? (vba && ctx.run.options.scriptPlugins !== undefined ? await vbaPart(ctx, p.entry, loc) : undefined);
|
|
756
756
|
const decision = vbaDecision ?? (contained ? await (0, contained_1.decideContained)(ctx, { name: loc, declaredType: ct || undefined, location: loc, chunks: () => ctx.zip.read(p.entry) }) : undefined);
|
|
757
|
-
const act = (0, contained_1.presence)(decision);
|
|
758
757
|
const found = [];
|
|
758
|
+
// A plugin's answer is final, so a file it answered for gets none of office-defuse's own findings.
|
|
759
759
|
if (vba && !vbaDecision)
|
|
760
|
-
found.push(
|
|
760
|
+
found.push((0, contained_1.ownFinding)(ctx, decision, types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, loc, data));
|
|
761
761
|
// A macro sheet becomes an empty worksheet, so the indexes of the sheets after it stay right.
|
|
762
762
|
if ((exports.TYPE.macrosheet.has(ct) || as(AS.macrosheet)) && ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.XlmMacrosheet, loc, data).action === 'strip')
|
|
763
763
|
ctx.plan.sheets.add(key);
|
|
764
764
|
if (ole)
|
|
765
|
-
found.push(
|
|
765
|
+
found.push((0, contained_1.ownFinding)(ctx, decision, types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, loc, data));
|
|
766
766
|
if (pkg)
|
|
767
|
-
found.push(
|
|
767
|
+
found.push((0, contained_1.ownFinding)(ctx, decision, types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Package, loc, data));
|
|
768
768
|
if (control && p.forms && !p.forms.problem)
|
|
769
769
|
found.push(ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.FormsControl, loc, { ...data, clsid: p.forms.classId ?? '' }));
|
|
770
770
|
else if (control || (controlBinary && !p.claimed)) {
|
|
771
771
|
const why = control && p.forms ? { ...(p.forms.classId ? { clsid: p.forms.classId } : {}), ...(p.forms.problem ? { reason: (0, context_1.clip)(p.forms.problem) } : {}) } : {};
|
|
772
|
-
found.push(ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, loc, { ...data, ...why },
|
|
772
|
+
found.push(control ? ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, loc, { ...data, ...why }) : (0, contained_1.ownFinding)(ctx, decision, types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, loc, { ...data, ...why }));
|
|
773
773
|
}
|
|
774
774
|
if (p.ole && !vba && !ole && !control && !controlBinary && !pkg)
|
|
775
|
-
found.push(
|
|
775
|
+
found.push((0, contained_1.ownFinding)(ctx, decision, types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleFile, loc, data));
|
|
776
776
|
if (decision ? !decision.keep : found.some(f => f.action === 'strip'))
|
|
777
777
|
ctx.plan.drop.add(key);
|
|
778
778
|
else if (decision?.replacement)
|
package/dist/ppt.js
CHANGED
|
@@ -332,7 +332,9 @@ async function storageRules(ctx, source, s, role, location, rewrite) {
|
|
|
332
332
|
// An object's storage goes to the plugins as the compound file it is, and stays as they leave it.
|
|
333
333
|
const size = await inner.size();
|
|
334
334
|
const decision = await (0, contained_1.decideContained)(ctx, { location, chunks: () => (0, io_1.chunksOf)(inner, size) });
|
|
335
|
-
|
|
335
|
+
// A plugin's answer is final, so the object's own rules do not run.
|
|
336
|
+
if (!decision?.answered)
|
|
337
|
+
await (0, objects_1.compoundRules)(ctx, cfb, location, true, undefined, decision);
|
|
336
338
|
if ((decision && !decision.keep) || ctx.strips > strips)
|
|
337
339
|
return 'remove';
|
|
338
340
|
if (decision?.replacement) {
|
|
@@ -385,7 +387,7 @@ async function containerRules(ctx, reader, h, offset, kids, stream, edits, links
|
|
|
385
387
|
r = ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, location, data);
|
|
386
388
|
// A macro of a VBA project that stays keeps its action.
|
|
387
389
|
else if (action === 1)
|
|
388
|
-
r = ctx.
|
|
390
|
+
r = ctx.macroTrigger(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, location, data, name);
|
|
389
391
|
else if (action === 5)
|
|
390
392
|
r = ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, location, data);
|
|
391
393
|
if (r?.action === 'strip') {
|
package/dist/rtf.js
CHANGED
|
@@ -670,7 +670,9 @@ async function objectData(ctx, o, bytes, location, base, cuts) {
|
|
|
670
670
|
cuts.push({ start: o.objdata[0], end: o.objdata[1], text: objdataGroup((0, oleobj_1.replaceSized)(bytes, ole1.dataAt, native.length, newNative)) });
|
|
671
671
|
}
|
|
672
672
|
}
|
|
673
|
-
|
|
673
|
+
// A plugin's answer is final, so the object's own rules do not run.
|
|
674
|
+
if (decision?.answered)
|
|
675
|
+
return;
|
|
674
676
|
if (pkg)
|
|
675
677
|
Object.assign(data, (0, objects_1.packageData)(pkg));
|
|
676
678
|
else if (native && native.length >= 8 && cfb_1.CFB_SIGNATURE.every((v, i) => native[i] === v)) {
|
|
@@ -707,7 +709,7 @@ async function objectData(ctx, o, bytes, location, base, cuts) {
|
|
|
707
709
|
if (className.toLowerCase() === 'ole2link')
|
|
708
710
|
ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, data);
|
|
709
711
|
else
|
|
710
|
-
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data
|
|
712
|
+
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data);
|
|
711
713
|
if (equation)
|
|
712
714
|
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, location, data);
|
|
713
715
|
}
|
package/dist/score.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { OfficeCategory as C, OfficeDetail as D, type DefuseFinding, type OfficeOptions, OfficeRisk, type OfficeScoreBands } from './types';
|
|
2
2
|
export declare const DEFAULT_BANDS: OfficeScoreBands;
|
|
3
|
+
/** Marks a finding as one a plugin's answer decided. */
|
|
4
|
+
export declare function pluginDecided(f: DefuseFinding): void;
|
|
3
5
|
interface Combination {
|
|
4
6
|
name: string;
|
|
5
7
|
weight: number;
|
package/dist/score.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.COMBINATIONS = exports.DEFAULT_BANDS = void 0;
|
|
4
|
+
exports.pluginDecided = pluginDecided;
|
|
4
5
|
exports.weightOf = weightOf;
|
|
5
6
|
exports.scoreFindings = scoreFindings;
|
|
6
7
|
exports.riskFor = riskFor;
|
|
7
8
|
const findings_1 = require("./findings");
|
|
8
9
|
const types_1 = require("./types");
|
|
9
10
|
exports.DEFAULT_BANDS = { low: 1, medium: 25, high: 50, critical: 80 };
|
|
11
|
+
/**
|
|
12
|
+
* Findings whose fate a plugin's answer decided, such as a button that runs a macro the plugins kept. They weigh
|
|
13
|
+
* nothing, in this run's score and in the weights a container gets: the plugin's own findings score that content.
|
|
14
|
+
*/
|
|
15
|
+
const decided = new WeakSet();
|
|
16
|
+
/** Marks a finding as one a plugin's answer decided. */
|
|
17
|
+
function pluginDecided(f) {
|
|
18
|
+
decided.add(f);
|
|
19
|
+
}
|
|
10
20
|
/** Code that runs, in any of its forms. */
|
|
11
21
|
const runsCode = (has) => has(types_1.OfficeCategory.Macro) || has(types_1.OfficeCategory.Dde);
|
|
12
22
|
exports.COMBINATIONS = [
|
|
@@ -24,6 +34,8 @@ exports.COMBINATIONS = [
|
|
|
24
34
|
];
|
|
25
35
|
/** The caller's weights first, then the built-in table. A kind from another package scores at the weight it carries. */
|
|
26
36
|
function weightOf(f, options) {
|
|
37
|
+
if (decided.has(f))
|
|
38
|
+
return 0;
|
|
27
39
|
const w = options.scoreWeights ?? [];
|
|
28
40
|
const exact = w.find(x => x.category === f.category && x.detail === f.detail);
|
|
29
41
|
if (exact)
|
package/dist/xls.js
CHANGED
|
@@ -217,7 +217,7 @@ async function sheetRules(ctx, cfb, stream, s, decrypt, location, edits, control
|
|
|
217
217
|
// A shape that runs one of the workbook's own macros stays when its VBA project does.
|
|
218
218
|
const name = macro.ref && ownName ? ownName(macro.ref) : undefined;
|
|
219
219
|
const data = { sheet: (0, context_1.clip)(s.name), ...(macro.ref && nameOf ? { macro: (0, context_1.clip)(nameOf(macro.ref) ?? '') } : {}) };
|
|
220
|
-
if (ctx.
|
|
220
|
+
if (ctx.macroTrigger(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, location, data, name).action === 'strip')
|
|
221
221
|
edits.objMacros.add(r.offset);
|
|
222
222
|
}
|
|
223
223
|
else if (r.type === biff_1.R.hlink) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patrickdk77/office-defuse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Inspect Office and OpenDocument files for VBA and Excel 4.0 macros, LibreOffice Basic and scripts, DDE, external templates and links, embedded OLE objects and ActiveX controls, and score the risk. Strict ZIP and XML readers, bounded memory, no runtime dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "patrickdk77",
|