@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.
@@ -0,0 +1,8 @@
1
+ export { analyzeScript } from './analyze';
2
+ export { type InspectScriptOptions, inspectScript } from './engine';
3
+ export { allFindingSpecs } from './findings';
4
+ export { analyzeFormCalc } from './formcalc';
5
+ export { type FormScriptPlugin, formScriptPlugin, type PluginScript } from './plugins';
6
+ export { COMBINATIONS, DEFAULT_BANDS, scoreFindings } from './score';
7
+ export * from './types';
8
+ export { analyzeVba } from './vba';
package/dist/index.js ADDED
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.analyzeVba = exports.scoreFindings = exports.DEFAULT_BANDS = exports.COMBINATIONS = exports.formScriptPlugin = exports.analyzeFormCalc = exports.allFindingSpecs = exports.inspectScript = exports.analyzeScript = void 0;
18
+ var analyze_1 = require("./analyze");
19
+ Object.defineProperty(exports, "analyzeScript", { enumerable: true, get: function () { return analyze_1.analyzeScript; } });
20
+ var engine_1 = require("./engine");
21
+ Object.defineProperty(exports, "inspectScript", { enumerable: true, get: function () { return engine_1.inspectScript; } });
22
+ var findings_1 = require("./findings");
23
+ Object.defineProperty(exports, "allFindingSpecs", { enumerable: true, get: function () { return findings_1.allFindingSpecs; } });
24
+ var formcalc_1 = require("./formcalc");
25
+ Object.defineProperty(exports, "analyzeFormCalc", { enumerable: true, get: function () { return formcalc_1.analyzeFormCalc; } });
26
+ var plugins_1 = require("./plugins");
27
+ Object.defineProperty(exports, "formScriptPlugin", { enumerable: true, get: function () { return plugins_1.formScriptPlugin; } });
28
+ var score_1 = require("./score");
29
+ Object.defineProperty(exports, "COMBINATIONS", { enumerable: true, get: function () { return score_1.COMBINATIONS; } });
30
+ Object.defineProperty(exports, "DEFAULT_BANDS", { enumerable: true, get: function () { return score_1.DEFAULT_BANDS; } });
31
+ Object.defineProperty(exports, "scoreFindings", { enumerable: true, get: function () { return score_1.scoreFindings; } });
32
+ __exportStar(require("./types"), exports);
33
+ var vba_1 = require("./vba");
34
+ Object.defineProperty(exports, "analyzeVba", { enumerable: true, get: function () { return vba_1.analyzeVba; } });
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import cjs from './index.js';
2
+ export const { analyzeScript, inspectScript, allFindingSpecs, analyzeFormCalc, formScriptPlugin, COMBINATIONS, DEFAULT_BANDS, scoreFindings, analyzeVba, ScriptCategory, ScriptDetail, ScriptRisk } = cjs;
3
+ export default cjs;
@@ -0,0 +1,28 @@
1
+ import type { ScriptContext, ScriptFinding, ScriptOptions } from './types';
2
+ /** The script a container hands its plugins, as pdf-defuse's ContainedScript gives it. */
3
+ export interface PluginScript extends ScriptContext {
4
+ text: string;
5
+ }
6
+ /** A script plugin in the shape pdf-defuse's `scriptPlugins` takes. */
7
+ export interface FormScriptPlugin {
8
+ kind: 'script';
9
+ name: string;
10
+ accepts(script: PluginScript): boolean;
11
+ process(script: PluginScript): Promise<{
12
+ result: 'passed' | 'removed';
13
+ findings?: ScriptFinding[];
14
+ }>;
15
+ /** Every script this plugin decided, with what it found, in the order it saw them. */
16
+ readonly decisions: ReadonlyArray<{
17
+ script: PluginScript;
18
+ result: 'passed' | 'removed';
19
+ findings: ScriptFinding[];
20
+ }>;
21
+ }
22
+ /**
23
+ * A plugin that keeps a script only when inspectScript calls it a form script, and removes every other one.
24
+ *
25
+ * It remembers the globals of the scripts it kept, so a field script may call a helper or read a value a
26
+ * document-level script defined. pdf-defuse hands document-level scripts over first. Make one plugin per document.
27
+ */
28
+ export declare function formScriptPlugin(options?: ScriptOptions): FormScriptPlugin;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formScriptPlugin = formScriptPlugin;
4
+ const engine_1 = require("./engine");
5
+ /**
6
+ * A plugin that keeps a script only when inspectScript calls it a form script, and removes every other one.
7
+ *
8
+ * It remembers the globals of the scripts it kept, so a field script may call a helper or read a value a
9
+ * document-level script defined. pdf-defuse hands document-level scripts over first. Make one plugin per document.
10
+ */
11
+ function formScriptPlugin(options = {}) {
12
+ const known = new Set();
13
+ const decisions = [];
14
+ return {
15
+ kind: 'script',
16
+ name: 'script-defuse',
17
+ decisions,
18
+ accepts: () => true,
19
+ async process(script) {
20
+ const r = (0, engine_1.inspectScript)(script.text, { ...options, context: script, knownGlobals: known });
21
+ const result = r.verdict === 'form' ? 'passed' : 'removed';
22
+ if (result === 'passed')
23
+ for (const name of r.definedGlobals)
24
+ known.add(name);
25
+ decisions.push({ script, result, findings: r.findings });
26
+ return { result, findings: r.findings };
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,17 @@
1
+ import { ScriptCategory as C, type DefuseFinding, type ScriptOptions, ScriptRisk, type ScriptScoreBands } from './types';
2
+ export declare const DEFAULT_BANDS: ScriptScoreBands;
3
+ interface Combination {
4
+ name: string;
5
+ weight: number;
6
+ test(has: (c: C) => boolean): boolean;
7
+ }
8
+ export declare const COMBINATIONS: Combination[];
9
+ /** The caller's weights first, then the built-in table. */
10
+ export declare function weightOf(f: DefuseFinding, options: ScriptOptions): 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: ScriptOptions): {
13
+ score: number;
14
+ risk: ScriptRisk;
15
+ };
16
+ export declare function riskFor(score: number, bands: ScriptScoreBands): ScriptRisk;
17
+ export {};
package/dist/score.js ADDED
@@ -0,0 +1,51 @@
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
+ exports.COMBINATIONS = [
11
+ { name: 'obfuscation with a way out of the form', weight: 20, test: has => has(types_1.ScriptCategory.Obfuscation) && (has(types_1.ScriptCategory.External) || has(types_1.ScriptCategory.DynamicCode)) },
12
+ { name: 'obfuscation with a viewer API known from exploits', weight: 20, test: has => has(types_1.ScriptCategory.Obfuscation) && has(types_1.ScriptCategory.Api) },
13
+ ];
14
+ /** The caller's weights first, then the built-in table. */
15
+ function weightOf(f, options) {
16
+ const w = options.scoreWeights ?? [];
17
+ const exact = w.find(x => x.category === f.category && x.detail === f.detail);
18
+ if (exact)
19
+ return exact.weight;
20
+ const cat = w.find(x => x.category === f.category && x.detail === undefined);
21
+ if (cat)
22
+ return cat.weight;
23
+ return (0, findings_1.findingSpec)(f.category, f.detail)?.weight ?? f.weight ?? 0;
24
+ }
25
+ /** Highest weight in full, a quarter of each other kind's weight, plus combinations, capped at 100. */
26
+ function scoreFindings(findings, options) {
27
+ const byKind = new Map();
28
+ for (const f of findings) {
29
+ const k = `${f.category}/${f.detail}`;
30
+ byKind.set(k, Math.max(byKind.get(k) ?? 0, weightOf(f, options)));
31
+ }
32
+ const weights = [...byKind.values()].sort((a, b) => b - a);
33
+ let score = weights.length ? weights[0] + weights.slice(1).reduce((s, w) => s + w / 4, 0) : 0;
34
+ const has = (c) => findings.some(f => f.category === c && weightOf(f, options) > 0);
35
+ for (const combo of exports.COMBINATIONS)
36
+ if (combo.test(has))
37
+ score += combo.weight;
38
+ score = Math.min(100, Math.round(score));
39
+ return { score, risk: riskFor(score, options.scoreBands ?? exports.DEFAULT_BANDS) };
40
+ }
41
+ function riskFor(score, bands) {
42
+ if (score >= bands.critical)
43
+ return types_1.ScriptRisk.Critical;
44
+ if (score >= bands.high)
45
+ return types_1.ScriptRisk.High;
46
+ if (score >= bands.medium)
47
+ return types_1.ScriptRisk.Medium;
48
+ if (score >= bands.low)
49
+ return types_1.ScriptRisk.Low;
50
+ return types_1.ScriptRisk.None;
51
+ }
@@ -0,0 +1,149 @@
1
+ /** Prefixed with SCRIPT_, so they never read as another package's kinds in a shared report, such as pdf-defuse's FORM. */
2
+ export declare enum ScriptCategory {
3
+ /** The script only formats, checks or calculates form fields, or a macro calls nothing known to reach outside its document. */
4
+ Form = "SCRIPT_FORM",
5
+ /** The script reaches outside the form or document: a URL, mail, a submit, another file, the network, a program, a library. */
6
+ External = "SCRIPT_EXTERNAL",
7
+ /** The script runs code it builds at run time, installs scripts, or reaches the language's own machinery. */
8
+ DynamicCode = "SCRIPT_DYNAMIC_CODE",
9
+ /** The script hides what it does: decoded strings, character codes, long concatenations, escapes. */
10
+ Obfuscation = "SCRIPT_OBFUSCATION",
11
+ /** The script uses a viewer API that is not form computation. */
12
+ Api = "SCRIPT_API",
13
+ Syntax = "SCRIPT_SYNTAX",
14
+ Limit = "SCRIPT_LIMIT"
15
+ }
16
+ export declare enum ScriptDetail {
17
+ Computation = "COMPUTATION",
18
+ DocumentMacro = "DOCUMENT_MACRO",
19
+ Url = "URL",
20
+ Submit = "SUBMIT",
21
+ Mail = "MAIL",
22
+ OpenDocument = "OPEN_DOCUMENT",
23
+ DataObject = "DATA_OBJECT",
24
+ ImportExport = "IMPORT_EXPORT",
25
+ Network = "NETWORK",
26
+ MenuCommand = "MENU_COMMAND",
27
+ Print = "PRINT",
28
+ Save = "SAVE",
29
+ FileSystem = "FILE_SYSTEM",
30
+ Process = "PROCESS",
31
+ Registry = "REGISTRY",
32
+ Dde = "DDE",
33
+ ComObject = "COM_OBJECT",
34
+ NativeCode = "NATIVE_CODE",
35
+ Environment = "ENVIRONMENT",
36
+ Reference = "REFERENCE",
37
+ Eval = "EVAL",
38
+ Timer = "TIMER",
39
+ InstallScript = "INSTALL_SCRIPT",
40
+ Privilege = "PRIVILEGE",
41
+ ComputedAccess = "COMPUTED_ACCESS",
42
+ Prototype = "PROTOTYPE",
43
+ With = "WITH",
44
+ SelfReference = "SELF_REFERENCE",
45
+ LegacyMacro = "LEGACY_MACRO",
46
+ CharCodes = "CHAR_CODES",
47
+ Decode = "DECODE",
48
+ Concatenation = "CONCATENATION",
49
+ Escapes = "ESCAPES",
50
+ EncodedString = "ENCODED_STRING",
51
+ LongString = "LONG_STRING",
52
+ BracketName = "BRACKET_NAME",
53
+ NumberExpression = "NUMBER_EXPRESSION",
54
+ Exploited = "EXPLOITED",
55
+ Unknown = "UNKNOWN",
56
+ PasswordPrompt = "PASSWORD_PROMPT",
57
+ Unparseable = "UNPARSEABLE",
58
+ Size = "SIZE",
59
+ Depth = "DEPTH"
60
+ }
61
+ export type ScriptFindingAction = 'strip' | 'info';
62
+ export declare enum ScriptRisk {
63
+ None = "NONE",
64
+ Low = "LOW",
65
+ Medium = "MEDIUM",
66
+ High = "HIGH",
67
+ Critical = "CRITICAL"
68
+ }
69
+ /** The finding shape every package of the defuse family shares. */
70
+ export interface DefuseFinding {
71
+ category: string;
72
+ detail: string;
73
+ /** Plain sentence, safe to show the uploader. */
74
+ description: string;
75
+ /** Default action after any caller override. */
76
+ action: 'reject' | 'strip' | 'info';
77
+ /** Where in the script: "line 3", with the trigger and field when the caller gave them. */
78
+ location?: string;
79
+ /** Extra values: the API name, the count, the length. */
80
+ data?: Record<string, string | number>;
81
+ /** Set when the finding comes from inside a contained file. */
82
+ attachment?: string;
83
+ /** How much the finding adds to a score. */
84
+ weight?: number;
85
+ }
86
+ export interface ScriptFinding extends DefuseFinding {
87
+ category: ScriptCategory;
88
+ detail: ScriptDetail;
89
+ action: ScriptFindingAction;
90
+ }
91
+ export interface ScriptActionOverride {
92
+ category: ScriptCategory;
93
+ detail?: ScriptDetail;
94
+ action: ScriptFindingAction;
95
+ }
96
+ export interface ScriptScoreWeight {
97
+ category: ScriptCategory;
98
+ detail?: ScriptDetail;
99
+ weight: number;
100
+ }
101
+ /** Lower bound of each band. */
102
+ export interface ScriptScoreBands {
103
+ low: number;
104
+ medium: number;
105
+ high: number;
106
+ critical: number;
107
+ }
108
+ export interface ScriptLimits {
109
+ /** Characters in one script. A script past it is LIMIT/SIZE and is not parsed. */
110
+ scriptSize?: number;
111
+ /** Nesting of the syntax tree. A script past it is LIMIT/DEPTH. */
112
+ depth?: number;
113
+ }
114
+ export interface ScriptOptions {
115
+ /** A limit left out is not enforced. */
116
+ limits?: ScriptLimits;
117
+ actionOverrides?: ScriptActionOverride[];
118
+ scoreWeights?: ScriptScoreWeight[];
119
+ scoreBands?: ScriptScoreBands;
120
+ }
121
+ /** A type library or another VBA project a VBA project refers to, as office-defuse reads it from the dir stream. */
122
+ export interface ScriptReference {
123
+ name: string;
124
+ kind: 'registered' | 'control' | 'project';
125
+ /** A type library's LibidReference, such as `*\G{...}#2.0#0#path#description`, or a project's path. */
126
+ libid: string;
127
+ }
128
+ /** Where a script runs, as a container reports it. pdf-defuse's and office-defuse's ContainedScript fit. */
129
+ export interface ScriptContext {
130
+ /** "document", "open", "field-format", "field-calculate", "click", ...; for a VBA module "vba-" and its kind. */
131
+ trigger?: string;
132
+ location?: string;
133
+ fieldName?: string;
134
+ /** 'javascript' unless the script is FormCalc, which only XFA forms carry, or a VBA module. */
135
+ language?: 'javascript' | 'formcalc' | 'vba';
136
+ /** 'xfa' for a script in an XFA form, which runs against the XFA object model; 'office' for a VBA module. */
137
+ form?: 'acroform' | 'xfa' | 'office';
138
+ /** For a VBA module, the references of its project. office-defuse gives them with the project's first module. */
139
+ references?: ScriptReference[];
140
+ }
141
+ export interface ScriptInspection {
142
+ /** 'form' when no finding strips the script: it only formats, checks or calculates form fields, or a VBA module calls nothing known to reach outside its document. */
143
+ verdict: 'form' | 'unsafe';
144
+ score: number;
145
+ risk: ScriptRisk;
146
+ findings: ScriptFinding[];
147
+ /** Global names the script defines, for scripts that run after it and use them. */
148
+ definedGlobals: string[];
149
+ }
package/dist/types.js ADDED
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ScriptRisk = exports.ScriptDetail = exports.ScriptCategory = void 0;
4
+ /** Prefixed with SCRIPT_, so they never read as another package's kinds in a shared report, such as pdf-defuse's FORM. */
5
+ var ScriptCategory;
6
+ (function (ScriptCategory) {
7
+ /** The script only formats, checks or calculates form fields, or a macro calls nothing known to reach outside its document. */
8
+ ScriptCategory["Form"] = "SCRIPT_FORM";
9
+ /** The script reaches outside the form or document: a URL, mail, a submit, another file, the network, a program, a library. */
10
+ ScriptCategory["External"] = "SCRIPT_EXTERNAL";
11
+ /** The script runs code it builds at run time, installs scripts, or reaches the language's own machinery. */
12
+ ScriptCategory["DynamicCode"] = "SCRIPT_DYNAMIC_CODE";
13
+ /** The script hides what it does: decoded strings, character codes, long concatenations, escapes. */
14
+ ScriptCategory["Obfuscation"] = "SCRIPT_OBFUSCATION";
15
+ /** The script uses a viewer API that is not form computation. */
16
+ ScriptCategory["Api"] = "SCRIPT_API";
17
+ ScriptCategory["Syntax"] = "SCRIPT_SYNTAX";
18
+ ScriptCategory["Limit"] = "SCRIPT_LIMIT";
19
+ })(ScriptCategory || (exports.ScriptCategory = ScriptCategory = {}));
20
+ var ScriptDetail;
21
+ (function (ScriptDetail) {
22
+ // FORM
23
+ ScriptDetail["Computation"] = "COMPUTATION";
24
+ ScriptDetail["DocumentMacro"] = "DOCUMENT_MACRO";
25
+ // EXTERNAL
26
+ ScriptDetail["Url"] = "URL";
27
+ ScriptDetail["Submit"] = "SUBMIT";
28
+ ScriptDetail["Mail"] = "MAIL";
29
+ ScriptDetail["OpenDocument"] = "OPEN_DOCUMENT";
30
+ ScriptDetail["DataObject"] = "DATA_OBJECT";
31
+ ScriptDetail["ImportExport"] = "IMPORT_EXPORT";
32
+ ScriptDetail["Network"] = "NETWORK";
33
+ ScriptDetail["MenuCommand"] = "MENU_COMMAND";
34
+ ScriptDetail["Print"] = "PRINT";
35
+ ScriptDetail["Save"] = "SAVE";
36
+ ScriptDetail["FileSystem"] = "FILE_SYSTEM";
37
+ ScriptDetail["Process"] = "PROCESS";
38
+ ScriptDetail["Registry"] = "REGISTRY";
39
+ ScriptDetail["Dde"] = "DDE";
40
+ ScriptDetail["ComObject"] = "COM_OBJECT";
41
+ ScriptDetail["NativeCode"] = "NATIVE_CODE";
42
+ ScriptDetail["Environment"] = "ENVIRONMENT";
43
+ ScriptDetail["Reference"] = "REFERENCE";
44
+ // DYNAMIC_CODE
45
+ ScriptDetail["Eval"] = "EVAL";
46
+ ScriptDetail["Timer"] = "TIMER";
47
+ ScriptDetail["InstallScript"] = "INSTALL_SCRIPT";
48
+ ScriptDetail["Privilege"] = "PRIVILEGE";
49
+ ScriptDetail["ComputedAccess"] = "COMPUTED_ACCESS";
50
+ ScriptDetail["Prototype"] = "PROTOTYPE";
51
+ ScriptDetail["With"] = "WITH";
52
+ ScriptDetail["SelfReference"] = "SELF_REFERENCE";
53
+ ScriptDetail["LegacyMacro"] = "LEGACY_MACRO";
54
+ // OBFUSCATION
55
+ ScriptDetail["CharCodes"] = "CHAR_CODES";
56
+ ScriptDetail["Decode"] = "DECODE";
57
+ ScriptDetail["Concatenation"] = "CONCATENATION";
58
+ ScriptDetail["Escapes"] = "ESCAPES";
59
+ ScriptDetail["EncodedString"] = "ENCODED_STRING";
60
+ ScriptDetail["LongString"] = "LONG_STRING";
61
+ ScriptDetail["BracketName"] = "BRACKET_NAME";
62
+ ScriptDetail["NumberExpression"] = "NUMBER_EXPRESSION";
63
+ // API
64
+ ScriptDetail["Exploited"] = "EXPLOITED";
65
+ ScriptDetail["Unknown"] = "UNKNOWN";
66
+ ScriptDetail["PasswordPrompt"] = "PASSWORD_PROMPT";
67
+ // SYNTAX
68
+ ScriptDetail["Unparseable"] = "UNPARSEABLE";
69
+ // LIMIT
70
+ ScriptDetail["Size"] = "SIZE";
71
+ ScriptDetail["Depth"] = "DEPTH";
72
+ })(ScriptDetail || (exports.ScriptDetail = ScriptDetail = {}));
73
+ var ScriptRisk;
74
+ (function (ScriptRisk) {
75
+ ScriptRisk["None"] = "NONE";
76
+ ScriptRisk["Low"] = "LOW";
77
+ ScriptRisk["Medium"] = "MEDIUM";
78
+ ScriptRisk["High"] = "HIGH";
79
+ ScriptRisk["Critical"] = "CRITICAL";
80
+ })(ScriptRisk || (exports.ScriptRisk = ScriptRisk = {}));
package/dist/vba.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { type ScriptContext, type ScriptFinding, type ScriptOptions } from './types';
2
+ export interface VbaInput {
3
+ text: string;
4
+ context?: ScriptContext;
5
+ options?: ScriptOptions;
6
+ }
7
+ /** Inspects one VBA module. Its findings say what it calls that reaches outside the document, and what it hides. */
8
+ export declare function analyzeVba(input: VbaInput): {
9
+ findings: ScriptFinding[];
10
+ definedGlobals: string[];
11
+ };