@portll/cobolwork 0.5.0 → 0.7.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 +89 -26
- package/STABILITY.md +76 -0
- package/bin/cobolwork.mjs +56 -14
- package/lib/baseline.mjs +9 -0
- package/lib/bms.mjs +21 -7
- package/lib/build.mjs +133 -53
- package/lib/capabilities.mjs +13 -11
- package/lib/cics-commands.mjs +501 -9
- package/lib/compliance.mjs +11 -0
- package/lib/consequence.mjs +17 -0
- package/lib/control-reuse.mjs +113 -0
- package/lib/control-workers.mjs +161 -0
- package/lib/control.mjs +77 -4
- package/lib/dataflow.mjs +402 -152
- package/lib/db2/cursor.mjs +15 -0
- package/lib/db2/read.mjs +170 -0
- package/lib/db2/rules.mjs +77 -0
- package/lib/db2/stmt/alter.mjs +473 -0
- package/lib/db2/stmt/grant.mjs +125 -0
- package/lib/db2/stmt/index.mjs +141 -0
- package/lib/db2/stmt/misc.mjs +325 -0
- package/lib/db2/stmt/routine.mjs +564 -0
- package/lib/db2/stmt/storage.mjs +146 -0
- package/lib/db2/stmt/table.mjs +540 -0
- package/lib/db2/stmt/view.mjs +146 -0
- package/lib/diff.mjs +17 -5
- package/lib/equivalence.mjs +44 -5
- package/lib/evidence/cli.mjs +26 -7
- package/lib/evidence/record.mjs +16 -6
- package/lib/evidence/seal.mjs +17 -0
- package/lib/evidence/store.mjs +44 -15
- package/lib/evidence/timestamp.mjs +83 -0
- package/lib/evidence/verify.mjs +120 -42
- package/lib/exec-reading.mjs +51 -0
- package/lib/execution.mjs +3 -2
- package/lib/explain.mjs +2 -0
- package/lib/exploitability.mjs +11 -2
- package/lib/hlasm/asm/data.mjs +128 -0
- package/lib/hlasm/asm/listing.mjs +43 -0
- package/lib/hlasm/asm/output.mjs +45 -0
- package/lib/hlasm/asm/sections.mjs +161 -0
- package/lib/hlasm/asm/symbols.mjs +78 -0
- package/lib/hlasm/exec.mjs +27 -0
- package/lib/hlasm/expr.mjs +167 -0
- package/lib/hlasm/instr.mjs +73 -0
- package/lib/hlasm/locate.mjs +333 -0
- package/lib/hlasm/macro/authorization.mjs +146 -0
- package/lib/hlasm/macro/datasets.mjs +107 -0
- package/lib/hlasm/macro/io.mjs +162 -0
- package/lib/hlasm/macro/le.mjs +53 -0
- package/lib/hlasm/macro/linkage.mjs +143 -0
- package/lib/hlasm/macro/operator.mjs +77 -0
- package/lib/hlasm/macro/program.mjs +184 -0
- package/lib/hlasm/macro/recovery.mjs +85 -0
- package/lib/hlasm/macro/storage.mjs +167 -0
- package/lib/hlasm/macro/structured.mjs +131 -0
- package/lib/hlasm/model.mjs +97 -0
- package/lib/hlasm/mvs38.mjs +47 -0
- package/lib/hlasm/operands.mjs +59 -0
- package/lib/hlasm/optable.mjs +61 -0
- package/lib/hlasm/read.mjs +130 -0
- package/lib/hlasm.mjs +44 -7
- package/lib/ims/dli.mjs +37 -0
- package/lib/ims/macro/dbd.mjs +299 -0
- package/lib/ims/macro/psb.mjs +286 -0
- package/lib/ims/model.mjs +149 -0
- package/lib/ims/operands.mjs +23 -0
- package/lib/ims/read.mjs +37 -0
- package/lib/ims/rules.mjs +135 -0
- package/lib/inventory.mjs +10 -7
- package/lib/ironwork-ids.mjs +24 -0
- package/lib/ironwork.mjs +20 -13
- package/lib/kernel/pds-archive.mjs +256 -0
- package/lib/kernel/registry.mjs +32 -23
- package/lib/kernel/shared-pass.mjs +163 -0
- package/lib/kernel/source-tree.mjs +104 -36
- package/lib/kernel/version-key.mjs +17 -0
- package/lib/layout.mjs +26 -31
- package/lib/options.mjs +26 -4
- package/lib/parser.mjs +136 -17
- package/lib/pli/cursor.mjs +15 -0
- package/lib/pli/expr.mjs +101 -0
- package/lib/pli/include.mjs +82 -0
- package/lib/pli/layout.mjs +125 -0
- package/lib/pli/lex.mjs +198 -0
- package/lib/pli/program.mjs +280 -0
- package/lib/pli/rules/based.mjs +95 -0
- package/lib/pli/rules/conditions.mjs +68 -0
- package/lib/pli/rules/entry.mjs +130 -0
- package/lib/pli/rules/index.mjs +24 -0
- package/lib/pli/rules/preprocessor.mjs +55 -0
- package/lib/pli/statements.mjs +130 -0
- package/lib/pli/stmt/alloc.mjs +45 -0
- package/lib/pli/stmt/assignment.mjs +56 -0
- package/lib/pli/stmt/call.mjs +104 -0
- package/lib/pli/stmt/conditions.mjs +94 -0
- package/lib/pli/stmt/control.mjs +219 -0
- package/lib/pli/stmt/declare.mjs +149 -0
- package/lib/pli/stmt/exec.mjs +55 -0
- package/lib/pli/stmt/io.mjs +239 -0
- package/lib/pli/stmt/misc.mjs +4 -0
- package/lib/pli/stmt/preprocessor.mjs +242 -0
- package/lib/pli/stmt/procedure.mjs +258 -0
- package/lib/pli/stmt/stream.mjs +283 -0
- package/lib/pli/storage.mjs +129 -0
- package/lib/policy.mjs +6 -0
- package/lib/precompile-check.mjs +124 -0
- package/lib/precompile-cics.mjs +8 -4
- package/lib/reach.mjs +11 -2
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +44 -3
- package/lib/scan.mjs +28 -12
- package/lib/sets/abend.mjs +82 -8
- package/lib/sets/build.mjs +43 -37
- package/lib/sets/cics.mjs +20 -38
- package/lib/sets/compile.mjs +46 -18
- package/lib/sets/copybook.mjs +5 -3
- package/lib/sets/crypto.mjs +5 -3
- package/lib/sets/ddl.mjs +36 -0
- package/lib/sets/flow.mjs +32 -8
- package/lib/sets/hidden.mjs +5 -3
- package/lib/sets/hlasm.mjs +124 -8
- package/lib/sets/ims.mjs +139 -0
- package/lib/sets/log.mjs +11 -9
- package/lib/sets/opaque.mjs +27 -7
- package/lib/sets/pli.mjs +40 -0
- package/lib/sets/priv.mjs +5 -3
- package/lib/sets/recon.mjs +5 -3
- package/lib/sets/secrets.mjs +112 -0
- package/lib/sets/semantics.mjs +8 -3
- package/lib/sets/web.mjs +39 -23
- package/lib/site.mjs +10 -0
- package/lib/sources.mjs +80 -20
- package/lib/statement-cursor.mjs +67 -0
- package/lib/verify.mjs +3 -2
- package/lib/version.mjs +6 -0
- package/package.json +3 -2
- package/rules/compliance-cobit2019.json +520 -5
- package/rules/compliance-dora.json +509 -5
- package/rules/compliance-ffiec.json +505 -1
- package/rules/compliance-nist80053.json +557 -1
- package/rules/gitleaks-mainframe.toml +46 -15
- package/rules/hlasm-instructions.json +2396 -0
- package/rules/hlasm-optables.json +8024 -0
- package/schema/cobolwork-baseline.schema.json +36 -0
- package/schema/cobolwork-build-provenance.schema.json +187 -0
- package/schema/cobolwork-build.schema.json +382 -0
- package/schema/cobolwork-capabilities.schema.json +239 -0
- package/schema/cobolwork-diff.schema.json +217 -0
- package/schema/cobolwork-evidence.schema.json +161 -0
- package/schema/cobolwork-execution.schema.json +53 -0
- package/schema/cobolwork-explain.schema.json +360 -0
- package/schema/cobolwork-finding.schema.json +465 -0
- package/schema/cobolwork-flow.schema.json +465 -0
- package/schema/cobolwork-gate.schema.json +211 -0
- package/schema/cobolwork-inventory.schema.json +206 -0
- package/schema/cobolwork-parse.schema.json +105 -0
- package/schema/cobolwork-reach.schema.json +74 -0
- package/schema/cobolwork-report.schema.json +559 -0
- package/schema/cobolwork-witness.schema.json +107 -0
- package/schema/cobolwork.baseline.schema.json +101 -0
- package/schema/cobolwork.policy.schema.json +7 -0
- package/schema/cobolwork.site.schema.json +116 -0
package/lib/dataflow.mjs
CHANGED
|
@@ -1,23 +1,43 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
-
import { inScope, isProgram, isBms, relPath } from './sources.mjs';
|
|
2
|
+
import { inScope, isProgram, isBms, isPli, relPath } from './sources.mjs';
|
|
3
3
|
import { parseBms, symbolicNames } from './bms.mjs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { parseSource, buildFileIndex } from './parser.mjs';
|
|
6
6
|
import { parseJcl } from './jcl.mjs';
|
|
7
7
|
import { holdsDecimal } from './layout.mjs';
|
|
8
8
|
import { hostVariablesIn } from './embedded-sql.mjs';
|
|
9
|
+
import { execReading } from './exec-reading.mjs';
|
|
10
|
+
import { DLI_ROUTINE, dliFunction, isGet } from './ims/dli.mjs';
|
|
11
|
+
import { CICS_EVERY_COMMAND } from './cics-commands.mjs';
|
|
12
|
+
|
|
13
|
+
// RESP and RESP2, which every command returns and the response-code source already reads.
|
|
14
|
+
const CICS_EVERY_COMMAND_OPTIONS = new Set(Object.keys(CICS_EVERY_COMMAND));
|
|
15
|
+
import { parsePliSource } from './pli/program.mjs';
|
|
16
|
+
import { membersOf, chooseMember } from './pli/include.mjs';
|
|
9
17
|
import { tsoCommands } from './utilities.mjs';
|
|
10
18
|
import { parseCsd, ddOfQueue } from './csd.mjs';
|
|
11
19
|
import { loadSite } from './site.mjs';
|
|
12
20
|
import { ssrangeAbends } from './options.mjs';
|
|
13
21
|
import { isJcl } from './sources.mjs';
|
|
14
|
-
import {
|
|
22
|
+
import { watchMemoryBuffer } from './kernel/memory.mjs';
|
|
23
|
+
import { drive, loopOver } from './kernel/shared-pass.mjs';
|
|
24
|
+
import { keep, parseOrder, restore, sameAnalysis } from './control-reuse.mjs';
|
|
25
|
+
import { CONTROL_WORKER_MIN_BYTES, controlAhead, controlWorkerCount, programFiles } from './control-workers.mjs';
|
|
26
|
+
import { statSync } from 'node:fs';
|
|
27
|
+
import { createHash } from 'node:crypto';
|
|
28
|
+
|
|
15
29
|
// A flow finding is located by source and sink rather than by one path and line, so it keeps its
|
|
16
30
|
// own comparator. The text comparison underneath it is the shared one.
|
|
17
31
|
import { byText } from './kernel/findings.mjs';
|
|
18
|
-
import { buildControl, creditOf, hasFact, topOf, within, wholeTop } from './control.mjs';
|
|
32
|
+
import { buildControl, creditOf, hasFact, internFacts, namesOf, topOf, within, wholeTop } from './control.mjs';
|
|
19
33
|
import { directoryTree } from './kernel/source-tree.mjs';
|
|
20
34
|
|
|
35
|
+
// Every reused control analysis checked against the program's own: for the equivalence runs and tests.
|
|
36
|
+
const verifyReuse = () => process.env.COBOLWORK_VERIFY_REUSE === '1';
|
|
37
|
+
// The size of program text from which a copy's control analysis is restored rather than redone. On
|
|
38
|
+
// one repository of 14,500 copied small programs restoring took longer than analysing.
|
|
39
|
+
const REUSE_MIN_BYTES = 256 * 1024;
|
|
40
|
+
|
|
21
41
|
const OS_COMMAND_ROUTINE = /^(SYSTEM|C\$SYSTEM|CBL_EXEC_RUN_UNIT|CBL_GC_HOSTED|BXPSYSTM)$/i;
|
|
22
42
|
|
|
23
43
|
export const SOURCE_KINDS = {
|
|
@@ -25,6 +45,7 @@ export const SOURCE_KINDS = {
|
|
|
25
45
|
'cics-terminal': 'EXEC CICS RECEIVE',
|
|
26
46
|
'cics-web': 'EXEC CICS WEB RECEIVE or EXTRACT',
|
|
27
47
|
'file-record': 'record read from a file',
|
|
48
|
+
'cics-queue': 'an item EXEC CICS READQ TS or TD returns, or the data RETRIEVE returns from the START that began the task',
|
|
28
49
|
'database': 'host variable filled by EXEC SQL',
|
|
29
50
|
'jcl-parm': 'PARM= on a JCL EXEC statement',
|
|
30
51
|
'jcl-instream': 'in-stream data on a JCL DD statement',
|
|
@@ -36,7 +57,7 @@ export const SINK_KINDS = {
|
|
|
36
57
|
'dynamic-program-load': 'CALL whose target is a variable',
|
|
37
58
|
'cics-dynamic-transfer': 'EXEC CICS LINK, XCTL or START with a variable program or transaction',
|
|
38
59
|
'dynamic-sql': 'EXEC SQL EXECUTE IMMEDIATE or PREPARE from a host variable',
|
|
39
|
-
'dynamic-file-path': 'SELECT ... ASSIGN TO a variable',
|
|
60
|
+
'dynamic-file-path': 'SELECT ... ASSIGN TO a variable, or EXEC CICS READ, WRITE, DELETE or STARTBR with FILE or DATASET from a variable',
|
|
40
61
|
'outbound-http': 'EXEC CICS WEB CONVERSE, WEB SEND on a client session, or the containers INVOKE SERVICE sends',
|
|
41
62
|
'socket-send': 'CALL EZASOKET or EZACICAL with SEND, SENDTO or WRITE',
|
|
42
63
|
'message-queue': 'CALL MQPUT or MQPUT1',
|
|
@@ -73,6 +94,28 @@ const computes = (st) => ARITHMETIC.has(st.verb) || st.counts === true || (st.fn
|
|
|
73
94
|
const computedOnRoute = (state) => { for (let s = state; s; s = s.prev) if (s.why && s.why.computes) return true; return false; };
|
|
74
95
|
const typedOnRoute = (state) => { for (let s = state; s; s = s.prev) if (s.node.typed) return true; return false; };
|
|
75
96
|
|
|
97
|
+
// Most nodes are never a source or a sink. They share one empty list, frozen, so that adding to it
|
|
98
|
+
// fails rather than reaching every node.
|
|
99
|
+
const NO_ENDS = Object.freeze([]);
|
|
100
|
+
const addSource = (n, x) => { if (n.sources === NO_ENDS) n.sources = []; n.sources.push(x); };
|
|
101
|
+
const addSink = (n, x) => { if (n.sinks === NO_ENDS) n.sinks = []; n.sinks.push(x); };
|
|
102
|
+
|
|
103
|
+
// What a check's condition says, as text that differs whenever anything a reader could observe
|
|
104
|
+
// differs: property order, a set's order, undefined, -0 and NaN. Null for anything but plain data.
|
|
105
|
+
function consKey(v) {
|
|
106
|
+
if (v === undefined) return 'u';
|
|
107
|
+
if (v === null) return 'n';
|
|
108
|
+
if (typeof v === 'number') return Object.is(v, -0) ? '#-0' : `#${v}`;
|
|
109
|
+
if (typeof v === 'string') return JSON.stringify(v);
|
|
110
|
+
if (typeof v === 'boolean') return v ? 't' : 'f';
|
|
111
|
+
if (typeof v !== 'object') return null;
|
|
112
|
+
const parts = v instanceof Set ? [...v] : Array.isArray(v) ? v : Object.getPrototypeOf(v) === Object.prototype ? null : undefined;
|
|
113
|
+
if (parts === undefined) return null;
|
|
114
|
+
const keys = parts ? parts.map(consKey) : Object.keys(v).map((k) => { const s = consKey(v[k]); return s === null ? null : `${JSON.stringify(k)}:${s}`; });
|
|
115
|
+
if (keys.includes(null)) return null;
|
|
116
|
+
return `${v instanceof Set ? 'S' : parts ? 'A' : 'O'}[${keys.join(',')}]`;
|
|
117
|
+
}
|
|
118
|
+
|
|
76
119
|
// Every literal a program holds that could be a program name: its VALUE clauses and the literals its
|
|
77
120
|
// statements and commands use. A menu that XCTLs through a table of names starts every one of them.
|
|
78
121
|
function literalNames(prog) {
|
|
@@ -124,18 +167,21 @@ function boundsOf(counter, until) {
|
|
|
124
167
|
// nearly every subscript descends from a file record, and reporting those would be reporting batch.
|
|
125
168
|
const FROM_OUTSIDE = ['argv-or-env', 'cics-terminal', 'cics-web', 'jcl-parm', 'jcl-instream'];
|
|
126
169
|
// What can reach a CICS command: a batch job never issues one.
|
|
127
|
-
const IN_CICS = ['cics-web', 'cics-terminal', 'file-record', 'database'];
|
|
170
|
+
const IN_CICS = ['cics-web', 'cics-terminal', 'file-record', 'cics-queue', 'database'];
|
|
128
171
|
const REFLECTED = ['cics-web', 'cics-terminal'];
|
|
129
172
|
// What the system says about a failure: a command's RESP, the EIB's copy of it, the SQLCA. Sent to a
|
|
130
173
|
// web client it describes the system behind the program (CWE-209); written anywhere else it is the
|
|
131
174
|
// program's own business, so these reach only a response, and Db2's message text the terminal too.
|
|
132
175
|
const SYSTEM_FIELDS = new Set(['EIBRESP', 'EIBRESP2', 'SQLCODE', 'SQLSTATE', 'SQLERRMC']);
|
|
176
|
+
// PL/I's SQLCA holds Db2's message text in SQLERRM, a CHAR(70) VARYING, where COBOL's has SQLERRMC.
|
|
177
|
+
const PLI_SYSTEM_FIELDS = new Set([...SYSTEM_FIELDS, 'SQLERRM']);
|
|
133
178
|
const TO_CLIENT = ['web-response', 'http-header'];
|
|
134
179
|
|
|
135
180
|
// Exfiltration is data at rest leaving the program. Terminal or web input reaching the same channel
|
|
136
181
|
// is the program relaying its own caller's data, and pairing it would fire on every gateway.
|
|
137
182
|
|
|
138
183
|
// A trace longer than this keeps its two ends and says how many hops it dropped.
|
|
184
|
+
|
|
139
185
|
export const TRACE_MAX = 64;
|
|
140
186
|
export const TRACE_KEEP = 24;
|
|
141
187
|
// Edges one source's walk may examine, and all walks together: about fifteen times what the largest
|
|
@@ -143,7 +189,7 @@ export const TRACE_KEEP = 24;
|
|
|
143
189
|
export const WALK_EDGES = 5_000_000;
|
|
144
190
|
export const TOTAL_EDGES = 500_000_000;
|
|
145
191
|
|
|
146
|
-
const DATA_AT_REST = ['database', 'file-record'];
|
|
192
|
+
const DATA_AT_REST = ['database', 'file-record', 'cics-queue'];
|
|
147
193
|
// The class tests a condition can make, which restrict what a field holds rather than compare it.
|
|
148
194
|
const CLASS_TEST = new Set(['NUMERIC', 'ALPHABETIC', 'ALPHABETIC-LOWER', 'ALPHABETIC-UPPER', 'POSITIVE', 'NEGATIVE']);
|
|
149
195
|
const SOCKET_ROUTINE = /^(EZASOKET|EZACICAL)$/i;
|
|
@@ -153,28 +199,7 @@ const SOCKET_SEND = /^(SEND|SENDTO|WRITE|SENDMSG|WRITEV)$/i;
|
|
|
153
199
|
// MsgDesc, PutMsgOpts, BufferLength, Buffer, …): the buffer is the sixth argument of both.
|
|
154
200
|
const MQ_BUFFER_ARG = { MQPUT: 5, MQPUT1: 5 };
|
|
155
201
|
|
|
156
|
-
function
|
|
157
|
-
const opts = new Map();
|
|
158
|
-
const toks = exec.toks;
|
|
159
|
-
const words = [];
|
|
160
|
-
for (let i = 0; i < toks.length; i++) {
|
|
161
|
-
const t = toks[i];
|
|
162
|
-
if (t.t !== 'word') continue;
|
|
163
|
-
words.push(t.u);
|
|
164
|
-
if (toks[i + 1] && toks[i + 1].t === 'sep' && toks[i + 1].v === '(') {
|
|
165
|
-
const inner = [];
|
|
166
|
-
let depth = 1;
|
|
167
|
-
for (let k = i + 2; k < toks.length && depth > 0; k++) {
|
|
168
|
-
if (toks[k].t === 'sep') { depth += toks[k].v === '(' ? 1 : -1; if (depth === 0) break; continue; }
|
|
169
|
-
inner.push(toks[k]);
|
|
170
|
-
}
|
|
171
|
-
if (!opts.has(t.u)) opts.set(t.u, inner);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return { opts, words };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function analyze(root, opts = {}) {
|
|
202
|
+
export function* analyzeSteps(root, opts = {}) {
|
|
178
203
|
const repos = opts.repos || [''];
|
|
179
204
|
const stats = { files: 0, programs: 0, edges: 0, nodes: 0, threw: 0, overBudget: 0, unreadable: 0, ebcdic: 0,
|
|
180
205
|
jclFiles: 0, jclSteps: 0, jclStepsResolved: 0, jclCrossings: 0,
|
|
@@ -221,9 +246,33 @@ export function analyze(root, opts = {}) {
|
|
|
221
246
|
// passes an argument, the PUT or GET of a container.
|
|
222
247
|
// `link` marks an edge into a callee at a call site ({ call }) or back out to it ({ ret }), and
|
|
223
248
|
// `stmt` names the statement that carries an edge whose `why` is text.
|
|
249
|
+
// Edges are kept as columns and laid out by source node before the walk. Most are a group's, a
|
|
250
|
+
// REDEFINES' or a RENAMES', and those share one record of why they exist.
|
|
251
|
+
const edgeFrom = [];
|
|
252
|
+
const edgeTo = [];
|
|
253
|
+
const edgeMeta = [];
|
|
254
|
+
const sharedMeta = new Map();
|
|
255
|
+
// Checks share a condition with the same content: 107,547 conditions of 66 Unieuro programs had
|
|
256
|
+
// 2,423 contents. Conditions are read and never changed once the check model has made them.
|
|
257
|
+
const conditions = new Map();
|
|
258
|
+
const sameCondition = (c) => {
|
|
259
|
+
const k = c ? consKey(c) : null;
|
|
260
|
+
if (k === null) return c;
|
|
261
|
+
let kept = conditions.get(k);
|
|
262
|
+
if (!kept) conditions.set(k, kept = c);
|
|
263
|
+
return kept;
|
|
264
|
+
};
|
|
224
265
|
const edge = (a, b, why, dir, at, link) => {
|
|
225
266
|
if (!a || !b || a === b) return;
|
|
226
|
-
|
|
267
|
+
let meta;
|
|
268
|
+
if (typeof why === 'string' && at == null && !link) {
|
|
269
|
+
const k = `${why}|${dir || ''}`;
|
|
270
|
+
meta = sharedMeta.get(k);
|
|
271
|
+
if (!meta) sharedMeta.set(k, meta = { why, dir });
|
|
272
|
+
} else meta = { why, dir, ...(at != null ? { at } : {}), ...link };
|
|
273
|
+
edgeFrom.push(a.id);
|
|
274
|
+
edgeTo.push(b.id);
|
|
275
|
+
edgeMeta.push(meta);
|
|
227
276
|
stats.edges++;
|
|
228
277
|
};
|
|
229
278
|
let callSites = 0;
|
|
@@ -231,10 +280,58 @@ export function analyze(root, opts = {}) {
|
|
|
231
280
|
// Everything the graph needs from one program is taken while its parse tree is in hand, and the
|
|
232
281
|
// tree is then released. Holding every tree until the end is what exhausted a 4 GB heap on an
|
|
233
282
|
// 8,024-file repository; nodes, edges and a few cross-program sites are all that survive.
|
|
234
|
-
|
|
283
|
+
// A flow node holds what differs between nodes. Its program and file come from the program it
|
|
284
|
+
// belongs to, and every other field from the class until the node sets its own.
|
|
285
|
+
const programOf = [];
|
|
286
|
+
class FlowNode {
|
|
287
|
+
constructor(id, pk, name) { this.id = id; this.pk = pk; this.name = name; }
|
|
288
|
+
get program() { return programOf[this.pk].id; }
|
|
289
|
+
get file() { return programOf[this.pk].file; }
|
|
290
|
+
}
|
|
291
|
+
Object.assign(FlowNode.prototype, { off: null, size: null, repeats: false, sources: NO_ENDS, sinks: NO_ENDS });
|
|
292
|
+
|
|
293
|
+
// A program whose text another program file repeats has its control analysis kept for the copies,
|
|
294
|
+
// keyed by its place in its file and by what each copybook it resolved holds (lib/control-reuse.mjs).
|
|
295
|
+
// Only a large one: below REUSE_MIN_BYTES (opts.reuseMinBytes) keeping and restoring costs more
|
|
296
|
+
// than analysing again.
|
|
297
|
+
const reuseMin = opts.reuseMinBytes ?? REUSE_MIN_BYTES;
|
|
298
|
+
const textKey = (src) => createHash('sha1').update(src).digest('base64');
|
|
299
|
+
function repeatedTexts(tree, files) {
|
|
300
|
+
const count = new Map();
|
|
301
|
+
const later = new Set();
|
|
302
|
+
for (const f of files) {
|
|
303
|
+
let src;
|
|
304
|
+
try { src = tree.text(f).text; } catch { continue; }
|
|
305
|
+
if (src.length < reuseMin) continue;
|
|
306
|
+
const k = textKey(src);
|
|
307
|
+
if (count.has(k)) later.add(f);
|
|
308
|
+
count.set(k, (count.get(k) || 0) + 1);
|
|
309
|
+
}
|
|
310
|
+
const repeated = new Map();
|
|
311
|
+
for (const [k, n] of count) if (n > 1) repeated.set(k, { text: k, left: n, kept: new Map() });
|
|
312
|
+
return { repeated, later };
|
|
313
|
+
}
|
|
314
|
+
const copyText = new Map();
|
|
315
|
+
const copyKey = (r, tree) => r.copies.map((c) => {
|
|
316
|
+
if (!c.path) return `${c.status}:${c.name}`;
|
|
317
|
+
if (!copyText.has(c.path)) { let k = null; try { k = textKey(tree.text(c.path).text); } catch { /* unread: compared by status */ } copyText.set(c.path, k); }
|
|
318
|
+
return `${c.status}:${copyText.get(c.path)}`;
|
|
319
|
+
}).join('|');
|
|
320
|
+
const reuseOf = (copied, k, r, f, tree) => ({ kept: copied.kept, key: `${k}|${copyKey(r, tree)}`, files: programFiles(r, f) });
|
|
321
|
+
// A restored analysis checked against the program's own, where COBOLWORK_VERIFY_REUSE asks.
|
|
322
|
+
function sameAsOwn(ctl, prog, resolver, file, from) {
|
|
323
|
+
let own = null;
|
|
324
|
+
try { own = buildControl(prog, resolver); } catch { /* the restored one threw too, or it differs */ }
|
|
325
|
+
if (own) internFacts(own.facts);
|
|
326
|
+
if (!own !== !ctl || (own && !sameAnalysis(own, ctl, new Set(parseOrder(prog))))) throw new Error(`the control analysis ${from} for ${file} differs from its own`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// `built` is the program's analysis from a worker thread (lib/control-workers.mjs), with the files
|
|
330
|
+
// its references name.
|
|
331
|
+
function summarise(prog, file, pk, options = [], reuse = null, built = null) {
|
|
332
|
+
programOf[pk] = { id: prog.id, file };
|
|
235
333
|
const byItem = new Map();
|
|
236
|
-
const byName =
|
|
237
|
-
for (const it of prog.items) if (!byName.has(it.name)) byName.set(it.name, it);
|
|
334
|
+
const { byName, itemOfToken, indexNames, resolve: resolver } = namesOf(prog);
|
|
238
335
|
const nodeOf = (item) => {
|
|
239
336
|
let n = byItem.get(item);
|
|
240
337
|
if (!n) {
|
|
@@ -245,7 +342,11 @@ export function analyze(root, opts = {}) {
|
|
|
245
342
|
for (let a = item; a; a = a.parent) if ((a.occurs || 1) > 1) { repeats = true; table = a; }
|
|
246
343
|
let top = item;
|
|
247
344
|
while (top.parent) top = top.parent;
|
|
248
|
-
n =
|
|
345
|
+
n = new FlowNode(nodes.length, pk, item.name);
|
|
346
|
+
n.off = item.offset ?? null;
|
|
347
|
+
n.size = item.size ?? null;
|
|
348
|
+
if (repeats) n.repeats = true;
|
|
349
|
+
if (top.section === 'LINKAGE') n.linkage = true;
|
|
249
350
|
// Where the outermost table holding this item sits in the record: every occurrence of the
|
|
250
351
|
// item is somewhere inside it. A group's size already counts its occurrences; an
|
|
251
352
|
// elementary item's is one element.
|
|
@@ -259,15 +360,12 @@ export function analyze(root, opts = {}) {
|
|
|
259
360
|
}
|
|
260
361
|
return n;
|
|
261
362
|
};
|
|
262
|
-
// Names inside EXEC blocks never pass through the parser's reference resolution, so fall back to the name.
|
|
263
|
-
const itemOfToken = (tok) => (tok ? prog.resolved.get(tok) || (tok.t === 'word' ? byName.get(tok.u) || null : null) : null);
|
|
264
363
|
// An INDEXED BY name is not a data item, so it needs a node of its own to carry taint.
|
|
265
|
-
const indexNames = new Set(prog.items.flatMap((it) => it.indexNames || []));
|
|
266
364
|
const indexNodes = new Map();
|
|
267
365
|
const indexNode = (name) => {
|
|
268
366
|
let n = indexNodes.get(name);
|
|
269
367
|
if (!n) {
|
|
270
|
-
n =
|
|
368
|
+
n = new FlowNode(nodes.length, pk, name);
|
|
271
369
|
nodes.push(n);
|
|
272
370
|
indexNodes.set(name, n);
|
|
273
371
|
stats.nodes++;
|
|
@@ -277,16 +375,17 @@ export function analyze(root, opts = {}) {
|
|
|
277
375
|
// A system field the program never declares - the EIB's, or an SQLCA the precompiler supplies -
|
|
278
376
|
// still holds a value, so it needs a node of its own, as an index name does.
|
|
279
377
|
const systemNodes = new Map();
|
|
378
|
+
const systemFields = isPli(file) ? PLI_SYSTEM_FIELDS : SYSTEM_FIELDS;
|
|
280
379
|
// A code on a terminal's error line is ordinary; Db2's message text names tables, columns and
|
|
281
380
|
// constraints, so only SQLERRMC reaches the screen sink.
|
|
282
381
|
const systemSource = (n, detail, at) => {
|
|
283
|
-
const onlyTo = /^SQLERRMC
|
|
284
|
-
if (!n.sources.some((x) => x.kind === 'system-response')) n
|
|
382
|
+
const onlyTo = /^SQLERRMC?\b/.test(detail) ? [...TO_CLIENT, 'screen'] : TO_CLIENT;
|
|
383
|
+
if (!n.sources.some((x) => x.kind === 'system-response')) addSource(n, { kind: 'system-response', onlyTo, noCredit: true, ...at, detail });
|
|
285
384
|
};
|
|
286
385
|
const systemNode = (tok) => {
|
|
287
386
|
let n = systemNodes.get(tok.u);
|
|
288
387
|
if (!n) {
|
|
289
|
-
n =
|
|
388
|
+
n = new FlowNode(nodes.length, pk, tok.u);
|
|
290
389
|
nodes.push(n);
|
|
291
390
|
systemNodes.set(tok.u, n);
|
|
292
391
|
stats.nodes++;
|
|
@@ -299,19 +398,34 @@ export function analyze(root, opts = {}) {
|
|
|
299
398
|
if (it) return nodeOf(it);
|
|
300
399
|
if (!tok || tok.t !== 'word') return null;
|
|
301
400
|
if (indexNames.has(tok.u)) return indexNode(tok.u);
|
|
302
|
-
return
|
|
401
|
+
return systemFields.has(tok.u) ? systemNode(tok) : null;
|
|
303
402
|
};
|
|
304
403
|
// The program's statement order, where its structure could be read. Without it a check is
|
|
305
404
|
// credited as it always was, wherever its field is used, and never clears anything.
|
|
306
405
|
let ctl = null;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
406
|
+
// A kept or worker-built analysis is { value }, { threw } or { none }, the last for a program
|
|
407
|
+
// with no procedure division, which is not counted as unordered.
|
|
408
|
+
const kept = reuse && reuse.kept.get(reuse.key);
|
|
409
|
+
const given = kept || (built && !built.local ? built : null);
|
|
410
|
+
if (given) {
|
|
411
|
+
ctl = given.value ? restore(given.value, parseOrder(prog), kept ? reuse.files : built.files) : null;
|
|
412
|
+
if (given.threw) stats.programsUnordered++;
|
|
413
|
+
if (verifyReuse()) sameAsOwn(ctl, prog, resolver, file, kept ? 'reused' : 'built in a worker');
|
|
414
|
+
if (reuse && !kept) reuse.kept.set(reuse.key, given);
|
|
415
|
+
} else {
|
|
416
|
+
let threw = false;
|
|
417
|
+
try { ctl = buildControl(prog, resolver); } catch { threw = true; stats.programsUnordered++; }
|
|
418
|
+
if (ctl) internFacts(ctl.facts);
|
|
419
|
+
if (reuse) {
|
|
420
|
+
const value = ctl ? keep(ctl, parseOrder(prog), reuse.files) : null;
|
|
421
|
+
if (!ctl || value) reuse.kept.set(reuse.key, value ? { value } : threw ? { threw: true } : { none: true });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
310
424
|
const pointOf = (x) => (ctl ? ctl.nodeOf.get(x) : undefined);
|
|
311
425
|
// Where a name inside a condition is read, which can know more than the statement does.
|
|
312
426
|
const placeOf = (tok, st) => (ctl && ctl.posOf.get(tok)) ?? pointOf(st);
|
|
313
427
|
|
|
314
|
-
for (const it of prog.items) if (
|
|
428
|
+
for (const it of prog.items) if (systemFields.has(it.name)) systemSource(nodeOf(it), `${it.name}, which the system sets`, { file: it.file || file, line: it.line });
|
|
315
429
|
// What a program in a CICS region DISPLAYs goes to its CESE and CESO queues, a log. Terminal or web
|
|
316
430
|
// input written there unchecked can carry the control characters that start a line the program
|
|
317
431
|
// did not write (CWE-117). A batch program's DISPLAY is its job's own log, which no terminal
|
|
@@ -323,7 +437,7 @@ export function analyze(root, opts = {}) {
|
|
|
323
437
|
const device = upon >= 0 && st.sources[upon + 1] ? ` UPON ${st.sources[upon + 1].u}` : '';
|
|
324
438
|
for (const t of upon >= 0 ? st.sources.slice(0, upon) : st.sources) {
|
|
325
439
|
const n = nodeOfToken(t);
|
|
326
|
-
if (n) n
|
|
440
|
+
if (n) addSink(n, { kind: 'log', onlyFrom: REFLECTED, file: st.file, line: st.line, point: pointOf(st), detail: `DISPLAY ${n.name}${device}` });
|
|
327
441
|
}
|
|
328
442
|
}
|
|
329
443
|
// The document XML PARSE reads is the operand after PARSE; what the parser does with a DTD in
|
|
@@ -331,7 +445,7 @@ export function analyze(root, opts = {}) {
|
|
|
331
445
|
for (const st of prog.statements) {
|
|
332
446
|
if (st.verb !== 'XML' || !st.sources[0] || st.sources[0].u !== 'PARSE') continue;
|
|
333
447
|
const n = st.sources[1] ? nodeOfToken(st.sources[1]) : null;
|
|
334
|
-
if (n) n
|
|
448
|
+
if (n) addSink(n, { kind: 'xml-document', onlyFrom: REFLECTED, file: st.file, line: st.line, point: pointOf(st), detail: `XML PARSE ${n.name}` });
|
|
335
449
|
}
|
|
336
450
|
for (const it of prog.items) {
|
|
337
451
|
const n = nodeOf(it);
|
|
@@ -366,15 +480,37 @@ export function analyze(root, opts = {}) {
|
|
|
366
480
|
const recordOf = (it) => { let top = it; while (top.parent) top = top.parent; return top; };
|
|
367
481
|
for (const it of prog.items) { const top = recordOf(it); if (!byRecord.has(top)) byRecord.set(top, []); byRecord.get(top).push(it); }
|
|
368
482
|
const extent = (it) => (it.offset != null && it.size != null ? [it.offset, it.offset + (it.contributes || it.size)] : null);
|
|
483
|
+
// Each record's items with a known extent, by where they start, so a check finds the items
|
|
484
|
+
// inside its field without testing every item of the record. They are then taken in the
|
|
485
|
+
// record's order, which is the order their nodes are made in.
|
|
486
|
+
const layouts = new Map();
|
|
487
|
+
const layoutOf = (top, items) => {
|
|
488
|
+
let l = layouts.get(top);
|
|
489
|
+
if (!l) {
|
|
490
|
+
const at = [];
|
|
491
|
+
items.forEach((it, i) => { if (it.level !== 88 && it.offset != null && it.size != null) at.push({ i, start: it.offset, end: it.offset + (it.contributes || it.size) }); });
|
|
492
|
+
at.sort((a, b) => a.start - b.start || a.i - b.i);
|
|
493
|
+
layouts.set(top, l = { at, position: new Map(items.map((it, i) => [it, i])) });
|
|
494
|
+
}
|
|
495
|
+
return l;
|
|
496
|
+
};
|
|
369
497
|
for (const c of ctl.checks) {
|
|
370
|
-
const entry = { x: c.x, t: c.t, f: c.f, tCons: c.tCons, fCons: c.fCons, file: c.file, line: c.line, item: c.field.index || c.field.name, ...(c.bounds ? { bounds: c.bounds } : {}), ...(c.mono ? { mono: true } : {}) };
|
|
498
|
+
const entry = { x: c.x, t: c.t, f: c.f, tCons: sameCondition(c.tCons), fCons: sameCondition(c.fCons), file: c.file, line: c.line, item: c.field.index || c.field.name, ...(c.bounds ? { bounds: c.bounds } : {}), ...(c.mono ? { mono: true } : {}) };
|
|
371
499
|
if (c.field.index) { (indexNode(c.field.index).checks ||= []).push(entry); continue; }
|
|
500
|
+
const top = recordOf(c.field);
|
|
501
|
+
const items = byRecord.get(top);
|
|
502
|
+
if (!items) { if (c.field.level !== 88) (nodeOf(c.field).checks ||= []).push(entry); continue; }
|
|
503
|
+
const { at, position } = layoutOf(top, items);
|
|
504
|
+
const inside = new Set();
|
|
505
|
+
if (c.field.level !== 88 && position.has(c.field)) inside.add(position.get(c.field));
|
|
372
506
|
const span = extent(c.field);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
507
|
+
if (span) {
|
|
508
|
+
let lo = 0;
|
|
509
|
+
let hi = at.length;
|
|
510
|
+
while (lo < hi) { const mid = (lo + hi) >>> 1; if (at[mid].start < span[0]) lo = mid + 1; else hi = mid; }
|
|
511
|
+
for (let k = lo; k < at.length && at[k].start <= span[1]; k++) if (at[k].end <= span[1]) inside.add(at[k].i);
|
|
377
512
|
}
|
|
513
|
+
for (const i of [...inside].sort((a, b) => a - b)) (nodeOf(items[i]).checks ||= []).push(entry);
|
|
378
514
|
}
|
|
379
515
|
}
|
|
380
516
|
// Without the order, a condition that restricts what an item may hold is credited wherever the
|
|
@@ -455,7 +591,7 @@ export function analyze(root, opts = {}) {
|
|
|
455
591
|
if (indexed.has(use)) continue;
|
|
456
592
|
indexed.add(use);
|
|
457
593
|
const start = kind === 'reference-modification' && x.from && base != null ? startOf(x.from, idx, base) : null;
|
|
458
|
-
at
|
|
594
|
+
addSink(at, {
|
|
459
595
|
kind, onlyFrom: FROM_OUTSIDE, file: st.file, line: st.line, point, group: `${pk}|${key}`, limit, ...(start ? { start } : {}), ...(offset ? { offset } : {}), ...(offset > 0 && idx && unsignedWhole(idx) ? { nonNeg: true } : {}), ...(ssrange ? { ssrange } : {}),
|
|
460
596
|
detail: kind === 'subscript'
|
|
461
597
|
? `${at.name} subscripts ${host.name}, a table of ${table.occurs}`
|
|
@@ -486,7 +622,7 @@ export function analyze(root, opts = {}) {
|
|
|
486
622
|
const use = `${key}|${pointOf(st) ?? `s${st.at}`}`;
|
|
487
623
|
if (indexed.has(use)) continue;
|
|
488
624
|
indexed.add(use);
|
|
489
|
-
nodeOf(it)
|
|
625
|
+
addSink(nodeOf(it), {
|
|
490
626
|
kind: 'loop-bound', onlyFrom: FROM_OUTSIDE, file: st.file, line: st.line, point: pointOf(st), group: `${pk}|${key}`, limit: limitOf(walks.host, walks.table), counterUses: bodyUses(st, usesOf.get(counter)), ...(ssrange ? { ssrange } : {}),
|
|
491
627
|
detail: `${it.name} decides where PERFORM VARYING ${counter} stops, and ${counter} subscripts ${walks.host.name}, a table of ${walks.table.occurs}`,
|
|
492
628
|
});
|
|
@@ -507,7 +643,7 @@ export function analyze(root, opts = {}) {
|
|
|
507
643
|
const use = `arithmetic|${n.id}|${pointOf(st) ?? `s${st.at}`}`;
|
|
508
644
|
if (indexed.has(use)) continue;
|
|
509
645
|
indexed.add(use);
|
|
510
|
-
n
|
|
646
|
+
addSink(n, {
|
|
511
647
|
kind: 'arithmetic', onlyFrom: FROM_OUTSIDE, file: st.file, line: st.line, point: pointOf(st), group: `${pk}|arithmetic|${n.id}`,
|
|
512
648
|
detail: `${it.name} (PIC ${it.picture}${it.effectiveUsage && it.effectiveUsage !== 'DISPLAY' ? ` ${it.effectiveUsage}` : ''}) is an operand of ${st.verb}${numproc === 'PFD' ? ', compiled NUMPROC(PFD), which repairs no sign' : ''}`,
|
|
513
649
|
});
|
|
@@ -529,7 +665,7 @@ export function analyze(root, opts = {}) {
|
|
|
529
665
|
const sts = touching.get(it);
|
|
530
666
|
for (const st of sts.length ? sts : [null]) {
|
|
531
667
|
const point = st ? pointOf(st) : undefined;
|
|
532
|
-
nodeOf(count)
|
|
668
|
+
addSink(nodeOf(count), {
|
|
533
669
|
kind: 'occurs-depending-count', onlyFrom: FROM_OUTSIDE, file: it.file, line: it.line, ...(st ? { point, group: `${pk}|odo|${it.name}` } : {}), top: it.occurs, ...(ssrange ? { ssrange } : {}),
|
|
534
670
|
detail: `${count.name} sets how many ${it.name} entries there are, up to ${it.occurs}`,
|
|
535
671
|
});
|
|
@@ -539,18 +675,18 @@ export function analyze(root, opts = {}) {
|
|
|
539
675
|
if (st.verb !== 'READ' && st.verb !== 'RETURN') continue;
|
|
540
676
|
for (const t of st.targets) {
|
|
541
677
|
const n = nodeOfToken(t);
|
|
542
|
-
if (n) n
|
|
678
|
+
if (n) addSource(n, { kind: 'file-record', file: st.file, line: st.line, detail: `${st.verb} ... INTO` });
|
|
543
679
|
}
|
|
544
680
|
}
|
|
545
681
|
for (const fd of prog.fds || []) for (const rec of fd.records || []) {
|
|
546
682
|
const readsFile = prog.statements.some(st => (st.verb === 'READ') && st.sources.concat(st.targets).some(t => t.u === fd.name));
|
|
547
|
-
if (readsFile) nodeOf(rec)
|
|
683
|
+
if (readsFile) addSource(nodeOf(rec), { kind: 'file-record', file: fd.file, line: fd.line, detail: `record of file ${fd.name}` });
|
|
548
684
|
}
|
|
549
685
|
for (const a of prog.accepts) {
|
|
550
686
|
const item = (a.targetTok ? itemOfToken(a.targetTok) : null) || byName.get(a.target);
|
|
551
687
|
if (!item) continue;
|
|
552
688
|
const kind = /^(COMMAND-LINE|ARGUMENT-VALUE|ENVIRONMENT|ENVIRONMENT-VALUE)$/.test(a.from || '') ? 'argv-or-env' : null;
|
|
553
|
-
if (kind) nodeOf(item)
|
|
689
|
+
if (kind) addSource(nodeOf(item), { kind, file: a.file, line: a.line, detail: `ACCEPT ... FROM ${a.from}` });
|
|
554
690
|
}
|
|
555
691
|
// A SELECT assigns either to a data item, which makes the path dynamic, or to an external
|
|
556
692
|
// name. On z/OS that external name is the DD name a JCL step fills, and it is how a job's
|
|
@@ -561,7 +697,7 @@ export function analyze(root, opts = {}) {
|
|
|
561
697
|
if (f.assign.t === 'word') {
|
|
562
698
|
const item = byName.get(f.assign.v);
|
|
563
699
|
if (item) {
|
|
564
|
-
nodeOf(item)
|
|
700
|
+
addSink(nodeOf(item), { kind: 'dynamic-file-path', file: f.file, line: f.line, detail: `SELECT ${f.name} ASSIGN TO ${f.assign.v}` });
|
|
565
701
|
continue; // a variable path is not a DD name
|
|
566
702
|
}
|
|
567
703
|
}
|
|
@@ -570,7 +706,9 @@ export function analyze(root, opts = {}) {
|
|
|
570
706
|
if (!external) continue;
|
|
571
707
|
const fd = (prog.fds || []).find((x) => x.name === f.name);
|
|
572
708
|
const recs = (fd?.records || []).map(nodeOf);
|
|
573
|
-
|
|
709
|
+
// READ ... INTO moves the record it read into an item, which then holds the DD's data too.
|
|
710
|
+
const into = prog.statements.filter((st) => st.verb === 'READ' && st.sources[0]?.u === f.name).flatMap((st) => st.targets.map(nodeOfToken).filter(Boolean));
|
|
711
|
+
if (recs.length) ddFiles.push({ dd: external, nodes: recs, into, file: f.file, line: f.line, select: f.name });
|
|
574
712
|
}
|
|
575
713
|
const topOf = (it) => { let t = it; while (t && t.parent) t = t.parent; return t; };
|
|
576
714
|
const ownParams = new Set((prog.paramTokens || []).map((pt) => topOf(itemOfToken(pt.tok))).filter(Boolean));
|
|
@@ -593,14 +731,14 @@ export function analyze(root, opts = {}) {
|
|
|
593
731
|
const point = pointOf(prog.statements[c.stmtIndex]);
|
|
594
732
|
if (c.kind === 'I') {
|
|
595
733
|
const n = nodeOfToken(c.targetTok);
|
|
596
|
-
if (n) n
|
|
734
|
+
if (n) addSink(n, { kind: 'dynamic-program-load', file: c.file, line: c.line, point, detail: `CALL ${c.name}` });
|
|
597
735
|
continue;
|
|
598
736
|
}
|
|
599
737
|
callSites.push({ name: String(c.name).toUpperCase(), display: c.name, file: c.file, line: c.line, point, args: c.using.map(a => ({ node: a.tok ? nodeOfToken(a.tok) : null, mode: a.mode, extent: a.tok ? argExtent(itemOfToken(a.tok)) : null })) });
|
|
600
738
|
if (OS_COMMAND_ROUTINE.test(c.name)) {
|
|
601
739
|
for (const a of c.using) {
|
|
602
740
|
const n = a.tok ? nodeOfToken(a.tok) : null;
|
|
603
|
-
if (n) n
|
|
741
|
+
if (n) addSink(n, { kind: 'os-command', file: c.file, line: c.line, point, detail: `CALL '${c.name}' USING ${a.word}` });
|
|
604
742
|
}
|
|
605
743
|
} else if (SOCKET_ROUTINE.test(c.name)) {
|
|
606
744
|
// IBM's own examples hold the function code in a field: 01 SOC-FUNCTION PIC X(16) VALUE 'SEND'.
|
|
@@ -611,23 +749,30 @@ export function analyze(root, opts = {}) {
|
|
|
611
749
|
if (fn && SOCKET_SEND.test(String(fn).trim())) {
|
|
612
750
|
for (const a of c.using.slice(1)) {
|
|
613
751
|
const n = a.tok ? nodeOfToken(a.tok) : null;
|
|
614
|
-
if (n) n
|
|
752
|
+
if (n) addSink(n, { kind: 'socket-send', onlyFrom: DATA_AT_REST, file: c.file, line: c.line, point, detail: `CALL '${c.name}' ${String(fn).trim()} USING ${a.word}` });
|
|
615
753
|
}
|
|
616
754
|
}
|
|
617
755
|
} else if (Object.hasOwn(MQ_BUFFER_ARG, String(c.name).toUpperCase())) {
|
|
618
756
|
const a = c.using[MQ_BUFFER_ARG[String(c.name).toUpperCase()]];
|
|
619
757
|
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
620
|
-
if (n) n
|
|
758
|
+
if (n) addSink(n, { kind: 'message-queue', onlyFrom: DATA_AT_REST, file: c.file, line: c.line, point, detail: `CALL '${c.name}' with buffer ${a.word}` });
|
|
621
759
|
} else if (/^MQCONNX?$/i.test(String(c.name))) {
|
|
622
760
|
// MQCONN(QMgrName, Hconn, CompCode, Reason); MQCONNX puts its options second.
|
|
623
761
|
const a = c.using[0];
|
|
624
762
|
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
625
|
-
if (n) n
|
|
763
|
+
if (n) addSink(n, { kind: 'connection-target', onlyFrom: FROM_OUTSIDE, file: c.file, line: c.line, point, detail: `CALL '${c.name}' with queue manager ${a.word}, which decides the queue manager the program connects to` });
|
|
764
|
+
} else if (DLI_ROUTINE.test(c.name)) {
|
|
765
|
+
// A get call fills the I/O area, the argument after the PCB, with the segment it retrieves.
|
|
766
|
+
const valueOf = (a) => a.lit ?? (a.tok ? ((itemOfToken(a.tok) || {}).values || []).find(v => v.t === 'lit')?.v : null);
|
|
767
|
+
const call = dliFunction(c.using, valueOf);
|
|
768
|
+
const a = call && isGet(call.fn) ? c.using[call.index + 2] : null;
|
|
769
|
+
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
770
|
+
if (n) addSource(n, { kind: 'database', file: c.file, line: c.line, point, detail: `CALL '${c.name}' ${call.fn} fills ${a.word}` });
|
|
626
771
|
} else if (String(c.name).toUpperCase() === 'CEEGTST') {
|
|
627
772
|
// CEEGTST(heap id, size, address, feedback code): the size is the second argument.
|
|
628
773
|
const a = c.using[1];
|
|
629
774
|
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
630
|
-
if (n) n
|
|
775
|
+
if (n) addSink(n, { kind: 'storage-length', onlyFrom: FROM_OUTSIDE, file: c.file, line: c.line, point, detail: `CALL 'CEEGTST' with size ${a.word}, which decides how much heap storage is acquired` });
|
|
631
776
|
}
|
|
632
777
|
}
|
|
633
778
|
const transfers = [];
|
|
@@ -643,7 +788,10 @@ export function analyze(root, opts = {}) {
|
|
|
643
788
|
const containerNode = (name) => {
|
|
644
789
|
let n = containers.get(name);
|
|
645
790
|
if (!n) {
|
|
646
|
-
n =
|
|
791
|
+
n = new FlowNode(nodes.length, pk, `CONTAINER(${name})`);
|
|
792
|
+
n.off = undefined;
|
|
793
|
+
n.size = undefined;
|
|
794
|
+
n.repeats = undefined;
|
|
647
795
|
nodes.push(n);
|
|
648
796
|
containers.set(name, n);
|
|
649
797
|
stats.nodes++;
|
|
@@ -731,7 +879,7 @@ export function analyze(root, opts = {}) {
|
|
|
731
879
|
if (typed) continue;
|
|
732
880
|
stats.protectedFields++;
|
|
733
881
|
const back = field.effective.has('FSET') ? ' with FSET' : '';
|
|
734
|
-
n
|
|
882
|
+
addSource(n, { kind: 'cics-protected-field', onlyTo: ['record-key', 'record-update'], ...at,
|
|
735
883
|
detail: `EXEC CICS RECEIVE MAP(${map.name}) returns ${item.name}, field ${field.name} of map ${map.name} in mapset ${mapset.name}, which the map marks ${marks.join(' and ')}${field.attrb ? '' : ' by default'}${back}` });
|
|
736
884
|
}
|
|
737
885
|
};
|
|
@@ -740,45 +888,44 @@ export function analyze(root, opts = {}) {
|
|
|
740
888
|
const changes = new Map();
|
|
741
889
|
for (const e of prog.execs) {
|
|
742
890
|
if (e.kind !== 'CICS') continue;
|
|
743
|
-
const { opts: o,
|
|
744
|
-
if (
|
|
891
|
+
const { opts: o, verb } = execReading(e);
|
|
892
|
+
if (verb !== 'REWRITE' && !(verb === 'DELETE' && !o.has('RIDFLD'))) continue;
|
|
745
893
|
const file = optionName(o, 'DATASET') || optionName(o, 'FILE');
|
|
746
|
-
if (file && !changes.has(file)) changes.set(file,
|
|
894
|
+
if (file && !changes.has(file)) changes.set(file, verb === 'REWRITE' ? 'rewrites' : 'deletes');
|
|
747
895
|
}
|
|
748
896
|
for (const e of prog.execs) {
|
|
749
|
-
const { opts: o, words } =
|
|
897
|
+
const { opts: o, words, verb, sub, command, direction } = execReading(e);
|
|
750
898
|
const at = { file: e.file, line: e.line, point: pointOf(e) };
|
|
751
899
|
if (e.kind === 'CICS') {
|
|
752
|
-
const verb = words[0];
|
|
753
900
|
const intoTok = (o.get('INTO') || o.get('SET') || [])[0];
|
|
754
901
|
for (const opt of ['RESP', 'RESP2']) {
|
|
755
902
|
const r = nodeOfToken((o.get(opt) || [])[0]);
|
|
756
903
|
if (r) systemSource(r, `the ${opt} of EXEC CICS ${verb}`, at);
|
|
757
904
|
}
|
|
758
|
-
if (verb === 'WRITE' && ['OPERATOR', 'JOURNALNAME', 'JOURNALNUM'].includes(
|
|
759
|
-
const opt =
|
|
905
|
+
if (verb === 'WRITE' && ['OPERATOR', 'JOURNALNAME', 'JOURNALNUM'].includes(sub)) {
|
|
906
|
+
const opt = sub === 'OPERATOR' ? 'TEXT' : 'FROM';
|
|
760
907
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
761
|
-
if (n) n
|
|
908
|
+
if (n) addSink(n, { kind: 'log', onlyFrom: REFLECTED, ...at, detail: `EXEC CICS WRITE ${sub} ${opt}(${n.name}), ${sub === 'OPERATOR' ? 'to the operator console' : 'to a journal'}` });
|
|
762
909
|
}
|
|
763
910
|
if (verb === 'RECEIVE') {
|
|
764
911
|
const isWeb = words.includes('WEB');
|
|
765
912
|
const n = nodeOfToken(intoTok);
|
|
766
|
-
if (n) n
|
|
913
|
+
if (n) addSource(n, { kind: isWeb ? 'cics-web' : 'cics-terminal', ...at, detail: `EXEC CICS ${isWeb ? 'WEB ' : ''}RECEIVE` });
|
|
767
914
|
// RECEIVE MAP with no INTO fills the symbolic map, whose input record is <map>I.
|
|
768
915
|
const map = optionName(o, 'MAP');
|
|
769
916
|
if (!n && map) {
|
|
770
917
|
const sym = byName.get(`${map}I`);
|
|
771
|
-
if (sym) nodeOf(sym)
|
|
918
|
+
if (sym) addSource(nodeOf(sym), { kind: 'cics-terminal', ...at, detail: `EXEC CICS RECEIVE MAP(${map}) into the symbolic map` });
|
|
772
919
|
}
|
|
773
920
|
if (!isWeb && o.has('MAP')) receiveMap(e, o, itemOfToken(intoTok), at);
|
|
774
921
|
}
|
|
775
922
|
// What the terminal shows: SEND TEXT's FROM, or the map's output record, <map>O without FROM.
|
|
776
|
-
if (verb === 'SEND' && (
|
|
923
|
+
if (verb === 'SEND' && (sub === 'TEXT' || sub === 'MAP')) {
|
|
777
924
|
const from = nodeOfToken((o.get('FROM') || [])[0]);
|
|
778
|
-
const map =
|
|
925
|
+
const map = sub === 'MAP' ? optionName(o, 'MAP') : null;
|
|
779
926
|
const sym = !from && map ? byName.get(`${map}O`) : null;
|
|
780
927
|
const n = from || (sym ? nodeOf(sym) : null);
|
|
781
|
-
if (n) n
|
|
928
|
+
if (n) addSink(n, { kind: 'screen', onlyFrom: ['system-response'], ...at, detail: `EXEC CICS SEND ${sub}${map ? `(${map})` : ''}${from ? ` FROM(${n.name})` : ', from its symbolic map'}, to the terminal` });
|
|
782
929
|
}
|
|
783
930
|
// RIDFLD decides which record a READ or DELETE reaches, or where a browse starts. A key the
|
|
784
931
|
// user types is how a lookup works; a key the program put in a protected field and read back
|
|
@@ -790,12 +937,12 @@ export function analyze(root, opts = {}) {
|
|
|
790
937
|
const detail = verb === 'DELETE' ? `EXEC CICS DELETE RIDFLD(${k && k.name}), which decides which record it deletes`
|
|
791
938
|
: then ? `EXEC CICS READ UPDATE RIDFLD(${k && k.name}), which decides which record the program then ${then}`
|
|
792
939
|
: `EXEC CICS ${verb} RIDFLD(${k && k.name}), which decides which record it reaches`;
|
|
793
|
-
if (k) k
|
|
940
|
+
if (k) addSink(k, { kind: verb === 'DELETE' || then ? 'record-update' : 'record-key', onlyFrom: ['cics-protected-field'], noCredit: true, ...at, detail });
|
|
794
941
|
}
|
|
795
942
|
if (verb === 'GETMAIN') {
|
|
796
943
|
for (const opt of ['FLENGTH', 'LENGTH']) {
|
|
797
944
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
798
|
-
if (n) n
|
|
945
|
+
if (n) addSink(n, { kind: 'storage-length', onlyFrom: REFLECTED, ...at, detail: `EXEC CICS GETMAIN ${opt}(${n.name}), which decides how much storage is acquired` });
|
|
799
946
|
}
|
|
800
947
|
}
|
|
801
948
|
if (verb === 'PUT' && o.has('CONTAINER')) {
|
|
@@ -805,11 +952,11 @@ export function analyze(root, opts = {}) {
|
|
|
805
952
|
if (name) channelOf.set(name, optionName(o, 'CHANNEL'));
|
|
806
953
|
}
|
|
807
954
|
// A service call sends every container on its channel, and its URI decides where they go.
|
|
808
|
-
if (verb === 'INVOKE' && (
|
|
809
|
-
invokes.push({ at, what:
|
|
955
|
+
if (verb === 'INVOKE' && (sub === 'SERVICE' || sub === 'WEBSERVICE')) {
|
|
956
|
+
invokes.push({ at, what: sub, channel: optionName(o, 'CHANNEL') });
|
|
810
957
|
for (const opt of ['URI', 'URIMAP']) {
|
|
811
958
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
812
|
-
if (n) n
|
|
959
|
+
if (n) addSink(n, { kind: 'outbound-host', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS INVOKE ${sub} ${opt}(...), which decides where the request goes` });
|
|
813
960
|
}
|
|
814
961
|
}
|
|
815
962
|
if (verb === 'GET' && o.has('CONTAINER')) {
|
|
@@ -819,51 +966,60 @@ export function analyze(root, opts = {}) {
|
|
|
819
966
|
}
|
|
820
967
|
// CONVERSE is always a client call; SEND is one only on a session this program opened. A
|
|
821
968
|
// plain WEB SEND answers the request that started the program, which is its job.
|
|
822
|
-
if (verb === 'WEB' && (
|
|
969
|
+
if (verb === 'WEB' && (sub === 'CONVERSE' || (sub === 'SEND' && o.has('SESSTOKEN')))) {
|
|
823
970
|
const n = nodeOfToken((o.get('FROM') || [])[0]);
|
|
824
|
-
if (n) n
|
|
971
|
+
if (n) addSink(n, { kind: 'outbound-http', onlyFrom: DATA_AT_REST, ...at, detail: `EXEC CICS WEB ${sub} to a remote server` });
|
|
825
972
|
// The request may be a container rather than a field.
|
|
826
973
|
const sent = o.has('CONTAINER') ? optionName(o, 'CONTAINER') : null;
|
|
827
|
-
if (sent) containerNode(sent)
|
|
974
|
+
if (sent) addSink(containerNode(sent), { kind: 'outbound-http', onlyFrom: DATA_AT_REST, ...at, detail: `EXEC CICS WEB ${sub} sending CONTAINER(${sent})` });
|
|
828
975
|
}
|
|
829
976
|
// The response this program returns to its own caller, and the document built for it. CICS
|
|
830
977
|
// escapes nothing on the way out - DOCUMENT SET's UNESCAPED option is about URL decoding,
|
|
831
978
|
// not HTML - so whatever reaches here is what the browser is handed.
|
|
832
|
-
if (verb === 'WEB' &&
|
|
979
|
+
if (verb === 'WEB' && sub === 'SEND' && !o.has('SESSTOKEN')) {
|
|
833
980
|
const n = nodeOfToken((o.get('FROM') || [])[0]);
|
|
834
981
|
// A stored value becomes script only where the reply is markup, and a web program
|
|
835
982
|
// answering with a database row is the ordinary shape of one. Input echoed back is a
|
|
836
983
|
// defect whatever the reply holds.
|
|
837
984
|
const media = (o.get('MEDIATYPE') || [])[0];
|
|
838
985
|
const markup = media && media.t === 'lit' && /html|xml|svg/i.test(String(media.v));
|
|
839
|
-
if (n) n
|
|
986
|
+
if (n) addSink(n, { kind: 'web-response', onlyFrom: [...(markup ? IN_CICS : REFLECTED), 'system-response'], ...at, detail: `EXEC CICS WEB SEND answering this program's caller${markup ? ` as ${String(media.v).trim()}` : ''}` });
|
|
840
987
|
}
|
|
841
988
|
if (verb === 'DOCUMENT') {
|
|
842
|
-
const what =
|
|
989
|
+
const what = sub === 'SET' ? ['VALUE', 'SYMBOLLIST'] : ['TEXT', 'FROM', 'BINARY'];
|
|
843
990
|
for (const opt of what) {
|
|
844
991
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
845
|
-
if (n) n
|
|
992
|
+
if (n) addSink(n, { kind: 'web-response', onlyFrom: [...IN_CICS, 'system-response'], ...at, detail: `EXEC CICS DOCUMENT ${sub} ${opt}(...), which is sent as the response` });
|
|
846
993
|
}
|
|
847
994
|
}
|
|
848
|
-
if (verb === 'WEB' &&
|
|
995
|
+
if (verb === 'WEB' && sub === 'WRITE' && o.has('HTTPHEADER')) {
|
|
849
996
|
for (const opt of ['HTTPHEADER', 'VALUE']) {
|
|
850
997
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
851
|
-
if (n) n
|
|
998
|
+
if (n) addSink(n, { kind: 'http-header', onlyFrom: [...IN_CICS, 'system-response'], ...at, detail: `EXEC CICS WEB WRITE HTTPHEADER ${opt}(...)` });
|
|
852
999
|
}
|
|
853
1000
|
}
|
|
854
|
-
if (verb === 'WEB' && (
|
|
1001
|
+
if (verb === 'WEB' && (sub === 'OPEN' || sub === 'CONVERSE')) {
|
|
855
1002
|
for (const opt of ['HOST', 'URIMAP', 'PATH']) {
|
|
856
1003
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
857
|
-
if (n) n
|
|
1004
|
+
if (n) addSink(n, { kind: 'outbound-host', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS WEB ${sub} ${opt}(...), which decides where the request goes` });
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
// FILE or DATASET picks the file a command reads, writes or browses from every file the region
|
|
1008
|
+
// defines; the commands that carry on from these act on the file already picked.
|
|
1009
|
+
if (['READ', 'WRITE', 'DELETE', 'STARTBR'].includes(verb)) {
|
|
1010
|
+
for (const opt of ['FILE', 'DATASET']) {
|
|
1011
|
+
const t = (o.get(opt) || [])[0];
|
|
1012
|
+
const n = t && t.t === 'word' ? nodeOfToken(t) : null;
|
|
1013
|
+
if (n) addSink(n, { kind: 'dynamic-file-path', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS ${verb} ${opt}(${n.name}), which decides the file the command acts on` });
|
|
858
1014
|
}
|
|
859
1015
|
}
|
|
860
1016
|
// The queue a command acts on, as distinct from what it writes there: a name from input
|
|
861
1017
|
// reads, overwrites or deletes whatever queue the input asks for.
|
|
862
1018
|
if (['WRITEQ', 'READQ', 'DELETEQ'].includes(verb)) {
|
|
863
|
-
const ts =
|
|
1019
|
+
const ts = sub === 'TS' || !words.includes('TD');
|
|
864
1020
|
for (const opt of ['QUEUE', 'QNAME']) {
|
|
865
1021
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
866
|
-
if (n) n
|
|
1022
|
+
if (n) addSink(n, { kind: 'queue-name', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS ${verb} ${ts ? 'TS' : 'TD'} ${opt}(...) names the queue` });
|
|
867
1023
|
}
|
|
868
1024
|
}
|
|
869
1025
|
// SYSID ships a command to the region it names; SET's first option names the resource a
|
|
@@ -871,25 +1027,42 @@ export function analyze(root, opts = {}) {
|
|
|
871
1027
|
// INQUIRE and EXTRACT return SYSID into the field rather than read it.
|
|
872
1028
|
if (o.has('SYSID') && !['ASSIGN', 'INQUIRE', 'EXTRACT'].includes(verb)) {
|
|
873
1029
|
const n = nodeOfToken((o.get('SYSID') || [])[0]);
|
|
874
|
-
if (n) n
|
|
1030
|
+
if (n) addSink(n, { kind: 'cics-sysid', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS ${verb} SYSID(${n.name}), which decides the region the command is shipped to` });
|
|
875
1031
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1032
|
+
// The resource option is the one the table names the command by, or the synonym written (SET
|
|
1033
|
+
// DATASET is SET FILE).
|
|
1034
|
+
const resource = o.has(sub) ? sub : words[1];
|
|
1035
|
+
if (verb === 'SET' && resource && o.has(resource)) {
|
|
1036
|
+
const n = nodeOfToken((o.get(resource) || [])[0]);
|
|
1037
|
+
if (n) addSink(n, { kind: 'cics-system-resource', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS SET ${resource}(${n.name}), which decides the resource the command changes` });
|
|
879
1038
|
}
|
|
880
1039
|
if (verb === 'READ' || verb === 'READNEXT' || verb === 'READPREV') {
|
|
881
1040
|
const n = nodeOfToken(intoTok);
|
|
882
|
-
if (n && (o.has('FILE') || o.has('DATASET'))) n
|
|
1041
|
+
if (n && (o.has('FILE') || o.has('DATASET'))) addSource(n, { kind: 'file-record', ...at, detail: `EXEC CICS ${verb} FILE ... INTO` });
|
|
883
1042
|
}
|
|
884
1043
|
// A CONVERSE response is the remote server's data, which is untrusted input like any other.
|
|
885
|
-
if ((verb === 'WEB' &&
|
|
1044
|
+
if ((verb === 'WEB' && sub !== 'SEND') || verb === 'EXTRACT') {
|
|
886
1045
|
const n = nodeOfToken(intoTok);
|
|
887
|
-
if (n) n
|
|
1046
|
+
if (n) addSource(n, { kind: 'cics-web', ...at, detail: `EXEC CICS ${[verb, sub].filter(Boolean).join(' ')}` });
|
|
1047
|
+
}
|
|
1048
|
+
// The request's form fields, query parameters and headers, and what WEB EXTRACT says about the
|
|
1049
|
+
// request, come back in options other than INTO: each one CICS returns a value in.
|
|
1050
|
+
if (/^(WEB (READ|READNEXT|EXTRACT)|EXTRACT WEB)/.test(command || '')) {
|
|
1051
|
+
for (const [opt, toks] of o) {
|
|
1052
|
+
if (opt === 'INTO' || !['receives', 'both'].includes(direction(opt)) || CICS_EVERY_COMMAND_OPTIONS.has(opt)) continue;
|
|
1053
|
+
const n = nodeOfToken(toks[0]);
|
|
1054
|
+
if (n) addSource(n, { kind: 'cics-web', ...at, detail: `EXEC CICS ${command} ${opt}` });
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
// A queue item, and the data a START passed, were stored by another task.
|
|
1058
|
+
if (/^READQ /.test(command || '') || command === 'RETRIEVE') {
|
|
1059
|
+
const n = nodeOfToken(intoTok);
|
|
1060
|
+
if (n) addSource(n, { kind: 'cics-queue', ...at, detail: `EXEC CICS ${command} INTO` });
|
|
888
1061
|
}
|
|
889
1062
|
// Where a transient-data write lands is not in the program: the CSD maps the queue to a DD
|
|
890
1063
|
// and the region's JCL maps the DD to a destination. Collected here, resolved once every
|
|
891
1064
|
// definition has been read.
|
|
892
|
-
if (verb === 'WRITEQ' &&
|
|
1065
|
+
if (verb === 'WRITEQ' && sub === 'TD') {
|
|
893
1066
|
const from = nodeOfToken((o.get('FROM') || [])[0]);
|
|
894
1067
|
const qTok = (o.get('QUEUE') || o.get('TDQUEUE') || [])[0];
|
|
895
1068
|
const qItem = qTok && qTok.t === 'word' ? itemOfToken(qTok) : null;
|
|
@@ -901,7 +1074,7 @@ export function analyze(root, opts = {}) {
|
|
|
901
1074
|
const pgm = (o.get('PROGRAM') || o.get('TRANSID') || [])[0];
|
|
902
1075
|
if (pgm && pgm.t === 'word') {
|
|
903
1076
|
const n = nodeOfToken(pgm);
|
|
904
|
-
if (n) n
|
|
1077
|
+
if (n) addSink(n, { kind: 'cics-dynamic-transfer', ...at, detail: `EXEC CICS ${verb} with a variable program name` });
|
|
905
1078
|
}
|
|
906
1079
|
const commarea = (o.get('COMMAREA') || o.get('FROM') || [])[0];
|
|
907
1080
|
const argNode = commarea ? nodeOfToken(commarea) : null;
|
|
@@ -917,15 +1090,20 @@ export function analyze(root, opts = {}) {
|
|
|
917
1090
|
if (t && t.t === 'lit') startsTransactions.add(String(t.v).trim().toUpperCase());
|
|
918
1091
|
}
|
|
919
1092
|
}
|
|
1093
|
+
if (e.kind === 'DLI' && isGet(verb)) {
|
|
1094
|
+
for (const tok of o.get('INTO') || []) {
|
|
1095
|
+
const n = nodeOfToken(tok);
|
|
1096
|
+
if (n) addSource(n, { kind: 'database', ...at, detail: `EXEC DLI ${verb} INTO ${n.name}` });
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
920
1099
|
if (e.kind === 'SQL') {
|
|
921
1100
|
// A host variable names the item a qualified :G.F resolves to; the statement fills the ones
|
|
922
1101
|
// Db2 writes, and those hold what the database held.
|
|
923
1102
|
const hv = e.hostVariables || hostVariablesIn(e.toks);
|
|
924
|
-
const verb = words[0];
|
|
925
1103
|
for (const h of hv) {
|
|
926
1104
|
if (!h.written) continue;
|
|
927
1105
|
const n = nodeOfToken(h.tok);
|
|
928
|
-
if (n) n
|
|
1106
|
+
if (n) addSource(n, { kind: 'database', ...at, detail: `EXEC SQL ${verb} ${verb === 'SELECT' || verb === 'FETCH' ? 'INTO' : 'writes'} host variable` });
|
|
929
1107
|
}
|
|
930
1108
|
// The location CONNECT TO or SET CONNECTION names, where a host variable holds it; what
|
|
931
1109
|
// follows USER and USING is the credential, not the target.
|
|
@@ -933,19 +1111,19 @@ export function analyze(root, opts = {}) {
|
|
|
933
1111
|
const to = e.toks.findIndex((t) => t.t === 'word' && (t.u === 'TO' || t.u === 'CONNECTION'));
|
|
934
1112
|
const colon = to >= 0 ? e.toks[to + 1] : null;
|
|
935
1113
|
const n = colon && colon.t === 'op' && colon.v === ':' && e.toks[to + 2] ? nodeOfToken(e.toks[to + 2]) : null;
|
|
936
|
-
if (n) n
|
|
1114
|
+
if (n) addSink(n, { kind: 'connection-target', onlyFrom: FROM_OUTSIDE, ...at, detail: `EXEC SQL ${verb === 'SET' ? 'SET CONNECTION' : 'CONNECT TO'} :${n.name}, which decides the database the program connects to` });
|
|
937
1115
|
}
|
|
938
1116
|
const dynamic = words.some((w, i) => w === 'PREPARE' || (w === 'EXECUTE' && words[i + 1] === 'IMMEDIATE'));
|
|
939
1117
|
if (dynamic) for (const h of hv) {
|
|
940
1118
|
const n = nodeOfToken(h.tok);
|
|
941
|
-
if (n) n
|
|
1119
|
+
if (n) addSink(n, { kind: 'dynamic-sql', ...at, detail: `EXEC SQL ${words.slice(0, 2).join(' ')} from a host variable` });
|
|
942
1120
|
}
|
|
943
1121
|
}
|
|
944
1122
|
}
|
|
945
1123
|
for (const inv of invokes) {
|
|
946
1124
|
for (const [name, channel] of channelOf) {
|
|
947
1125
|
if (inv.channel && channel && channel !== inv.channel) continue;
|
|
948
|
-
containerNode(name)
|
|
1126
|
+
addSink(containerNode(name), { kind: 'outbound-http', onlyFrom: DATA_AT_REST, ...inv.at, detail: `EXEC CICS INVOKE ${inv.what} sending CONTAINER(${name})${inv.channel ? ` on CHANNEL(${inv.channel})` : ''}` });
|
|
949
1127
|
}
|
|
950
1128
|
}
|
|
951
1129
|
const commareaItem = byName.get('DFHCOMMAREA');
|
|
@@ -977,6 +1155,16 @@ export function analyze(root, opts = {}) {
|
|
|
977
1155
|
const tree = (!repo && opts.tree) ? opts.tree : directoryTree(base, opts);
|
|
978
1156
|
const idx = tree.index;
|
|
979
1157
|
const files = tree.list().filter(isProgram).filter(inScope(opts));
|
|
1158
|
+
// PL/I programs join the same graph unless the scan is told not to read them.
|
|
1159
|
+
const readsPli = opts.pli ?? process.env.COBOLWORK_PLI !== '0';
|
|
1160
|
+
if (readsPli) files.push(...tree.list().filter(isPli).filter(inScope(opts)));
|
|
1161
|
+
// A PL/I %INCLUDE names a member by base name; any file of the tree may hold it.
|
|
1162
|
+
const pliMembers = readsPli ? membersOf(tree.list()) : null;
|
|
1163
|
+
const readMember = (name, from) => {
|
|
1164
|
+
const path = chooseMember(pliMembers.get(name), from);
|
|
1165
|
+
if (!path) return null;
|
|
1166
|
+
try { return { path, text: tree.text(path).text }; } catch { return null; }
|
|
1167
|
+
};
|
|
980
1168
|
for (const f of tree.list().filter(isJcl).filter(inScope(opts))) { jclPending.push(f); jclTrees.set(f, tree); }
|
|
981
1169
|
for (const f of tree.list().filter((p) => /\.csd$/i.test(p)).filter(inScope(opts))) {
|
|
982
1170
|
try { addCsd(tree.text(f).text, f); } catch (e) { stats.unreadable++; unread.push(`${tree.rel(f)}: ${e.code || e.name}`); }
|
|
@@ -995,20 +1183,38 @@ export function analyze(root, opts = {}) {
|
|
|
995
1183
|
// edges are proportional to data items and statements, not to bytes, so a repository of small
|
|
996
1184
|
// programs with large record layouts can exhaust a heap the budget believes is untouched. The
|
|
997
1185
|
// shared loop watches the heap itself, and the budget stays as the cheaper first line.
|
|
998
|
-
const
|
|
1186
|
+
const { repeated, later } = repeatedTexts(tree, files);
|
|
1187
|
+
// The larger COBOL programs' control analyses are built in worker threads ahead of the loop, where
|
|
1188
|
+
// the tree can say how it parses as data. A copy whose first is analysed already is restored instead.
|
|
1189
|
+
const workers = tree.parseSpec ? (opts.controlWorkers ?? controlWorkerCount()) : 0;
|
|
1190
|
+
const minWorkerBytes = opts.controlWorkerMinBytes ?? CONTROL_WORKER_MIN_BYTES;
|
|
1191
|
+
const sizeOf = (f) => { try { return statSync(f).size; } catch { return 0; } };
|
|
1192
|
+
const toWorkers = new Set(workers > 0 ? files.filter((f) => !isPli(f) && !later.has(f) && sizeOf(f) >= minWorkerBytes) : []);
|
|
1193
|
+
const ahead = toWorkers.size ? controlAhead(tree.parseSpec, files, (f) => toWorkers.has(f), { workers }) : null;
|
|
1194
|
+
const readProgram = (f, i) => {
|
|
999
1195
|
let src;
|
|
1000
1196
|
try { const s = tree.text(f); src = s.text; if (s.encoding === 'ebcdic') stats.ebcdic++; } catch (e) { stats.unreadable++; unread.push(`${tree.rel(f)}: ${e.code || e.name}`); return 0; }
|
|
1001
|
-
|
|
1197
|
+
const pli = isPli(f);
|
|
1198
|
+
if (!pli && !/PROCEDURE\s+DIVISION|PROGRAM-ID/i.test(src)) return 0;
|
|
1002
1199
|
if (held + src.length > budget) { stats.overBudget++; return 0; }
|
|
1003
1200
|
held += src.length;
|
|
1004
1201
|
let r;
|
|
1005
1202
|
try {
|
|
1006
|
-
r = tree.parse(f, src);
|
|
1203
|
+
r = pli ? parsePliSource(src, f, { readMember }) : tree.parse(f, src);
|
|
1007
1204
|
} catch (e) { stats.threw++; unparsed.push(`${tree.rel(f)}: ${e.code || e.name}`); return src.length; }
|
|
1008
1205
|
stats.files++;
|
|
1009
|
-
|
|
1206
|
+
const copied = src.length >= reuseMin && repeated.size ? repeated.get(textKey(src)) : undefined;
|
|
1207
|
+
const answer = ahead ? ahead.take(f, i) : null;
|
|
1208
|
+
const builtFiles = answer && answer.length === r.programs.length ? programFiles(r, f) : null;
|
|
1209
|
+
r.programs.forEach((p, k) => {
|
|
1210
|
+
programs.push(summarise(p, f, programs.length, r.options, copied ? reuseOf(copied, k, r, f, tree) : null, builtFiles && { ...answer[k], files: builtFiles }));
|
|
1211
|
+
stats.programs++;
|
|
1212
|
+
});
|
|
1213
|
+
if (copied && --copied.left === 0) repeated.delete(copied.text);
|
|
1010
1214
|
return src.length;
|
|
1011
|
-
}
|
|
1215
|
+
};
|
|
1216
|
+
let run;
|
|
1217
|
+
try { run = yield loopOver(files, readProgram, { label: 'flow', watcher: memoryWatcher }); } finally { ahead?.close(); }
|
|
1012
1218
|
|
|
1013
1219
|
// A file the loop never reached is not the same as one past the byte budget, and they are
|
|
1014
1220
|
// counted separately so a report can say which limit it hit.
|
|
@@ -1142,7 +1348,7 @@ export function analyze(root, opts = {}) {
|
|
|
1142
1348
|
// followed by the text. A program with no USING cannot receive one, and saying it does
|
|
1143
1349
|
// would be a path nobody could follow.
|
|
1144
1350
|
if (run.parm !== null && callee.params && callee.params[0] && callee.params[0].node) {
|
|
1145
|
-
callee.params[0].node
|
|
1351
|
+
addSource(callee.params[0].node, {
|
|
1146
1352
|
kind: 'jcl-parm', file: f, line: run.line,
|
|
1147
1353
|
detail: `${run.how}, which runs ${run.pgm}${amb}`,
|
|
1148
1354
|
});
|
|
@@ -1159,7 +1365,7 @@ export function analyze(root, opts = {}) {
|
|
|
1159
1365
|
for (const m of callee.ddFiles || []) {
|
|
1160
1366
|
if (m.dd !== dd.name.toUpperCase()) continue;
|
|
1161
1367
|
for (const n of m.nodes) {
|
|
1162
|
-
n
|
|
1368
|
+
addSink(n, { kind: 'internal-reader', file: m.file, line: m.line,
|
|
1163
1369
|
detail: `records written through SELECT ${m.select} to //${dd.name}, which step ${step.name || '(unnamed)'} of ${rel(f)} sends to the internal reader${amb}` });
|
|
1164
1370
|
}
|
|
1165
1371
|
}
|
|
@@ -1167,8 +1373,8 @@ export function analyze(root, opts = {}) {
|
|
|
1167
1373
|
if (!dd.inStream || !dd.inStream.length || !dd.name) continue;
|
|
1168
1374
|
for (const m of callee.ddFiles || []) {
|
|
1169
1375
|
if (m.dd !== dd.name.toUpperCase()) continue;
|
|
1170
|
-
for (const n of m.nodes) {
|
|
1171
|
-
n
|
|
1376
|
+
for (const n of [...m.nodes, ...m.into]) {
|
|
1377
|
+
addSource(n, {
|
|
1172
1378
|
kind: 'jcl-instream', file: f, line: dd.line,
|
|
1173
1379
|
detail: `${dd.inStream.length} lines of in-stream data on //${dd.name} in step ${step.name || '(unnamed)'}, read through SELECT ${m.select}${amb}`,
|
|
1174
1380
|
});
|
|
@@ -1237,21 +1443,21 @@ export function analyze(root, opts = {}) {
|
|
|
1237
1443
|
const def = w.queue ? csd.tdqueues.get(w.queue) : null;
|
|
1238
1444
|
const dd = w.queue ? ddOfQueue(csd, w.queue) : null;
|
|
1239
1445
|
if (w.queue && (readerQueues.has(w.queue) || (dd && readerDds.has(dd)))) {
|
|
1240
|
-
w.node
|
|
1446
|
+
addSink(w.node, { kind: 'internal-reader', file: w.file, line: w.line, point: w.point,
|
|
1241
1447
|
detail: `EXEC CICS WRITEQ TD QUEUE('${w.queue}')${dd ? `, which the CSD sends to DD ${dd}` : ''}, declared as reaching the internal reader` });
|
|
1242
1448
|
continue;
|
|
1243
1449
|
}
|
|
1244
1450
|
// Any other queue is where a region writes what it logs or prints, unless it starts a
|
|
1245
1451
|
// transaction, when it is a channel to that transaction instead.
|
|
1246
1452
|
if (!(def && def.transid)) {
|
|
1247
|
-
w.node
|
|
1453
|
+
addSink(w.node, { kind: 'log', onlyFrom: REFLECTED, file: w.file, line: w.line, point: w.point,
|
|
1248
1454
|
detail: `EXEC CICS WRITEQ TD QUEUE(${w.queue ? `'${w.queue}'` : 'a name the program computes'})` });
|
|
1249
1455
|
}
|
|
1250
1456
|
if (!w.queue) { stats.tdWritesUndecided++; continue; }
|
|
1251
1457
|
if (def && def.type === 'INTRA') continue;
|
|
1252
1458
|
// A queue the CSD sends to a DD leaves the region: to a dataset, a printer or another job.
|
|
1253
1459
|
if (dd) {
|
|
1254
|
-
w.node
|
|
1460
|
+
addSink(w.node, { kind: 'extrapartition-queue', onlyFrom: DATA_AT_REST, file: w.file, line: w.line, point: w.point,
|
|
1255
1461
|
detail: `EXEC CICS WRITEQ TD QUEUE('${w.queue}'), which the CSD sends out of the region to DD ${dd}` });
|
|
1256
1462
|
}
|
|
1257
1463
|
if (!declared) stats.tdWritesUndecided++;
|
|
@@ -1259,6 +1465,19 @@ export function analyze(root, opts = {}) {
|
|
|
1259
1465
|
}
|
|
1260
1466
|
}
|
|
1261
1467
|
|
|
1468
|
+
// Each node's edges, in the order they were added: outStart[id] up to outStart[id + 1] index outTo
|
|
1469
|
+
// and outMeta.
|
|
1470
|
+
const outStart = new Int32Array(nodes.length + 1);
|
|
1471
|
+
for (let k = 0; k < edgeFrom.length; k++) outStart[edgeFrom[k] + 1]++;
|
|
1472
|
+
for (let i = 0; i < nodes.length; i++) outStart[i + 1] += outStart[i];
|
|
1473
|
+
const outTo = new Int32Array(edgeFrom.length);
|
|
1474
|
+
const outMeta = new Array(edgeFrom.length);
|
|
1475
|
+
{
|
|
1476
|
+
const fill = outStart.slice(0, nodes.length);
|
|
1477
|
+
for (let k = 0; k < edgeFrom.length; k++) { const at = fill[edgeFrom[k]]++; outTo[at] = edgeTo[k]; outMeta[at] = edgeMeta[k]; }
|
|
1478
|
+
edgeFrom.length = 0; edgeTo.length = 0; edgeMeta.length = 0;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1262
1481
|
// Only nodes that can reach some sink are worth walking into. One backward pass over a transient
|
|
1263
1482
|
// reverse index answers that for every source at once; it is a superset, so nothing is lost.
|
|
1264
1483
|
// Two answers, one bit each: whether a node reaches a sink a source at rest could be reported at,
|
|
@@ -1269,11 +1488,11 @@ export function analyze(root, opts = {}) {
|
|
|
1269
1488
|
const canReach = new Uint8Array(nodes.length);
|
|
1270
1489
|
{
|
|
1271
1490
|
const revStart = new Int32Array(nodes.length + 1);
|
|
1272
|
-
for (
|
|
1491
|
+
for (let k = 0; k < outTo.length; k++) revStart[outTo[k] + 1]++;
|
|
1273
1492
|
for (let i = 0; i < nodes.length; i++) revStart[i + 1] += revStart[i];
|
|
1274
1493
|
const fill = revStart.slice(0, nodes.length);
|
|
1275
1494
|
const rev = new Int32Array(revStart[nodes.length]);
|
|
1276
|
-
for (
|
|
1495
|
+
for (let id = 0; id < nodes.length; id++) for (let k = outStart[id]; k < outStart[id + 1]; k++) rev[fill[outTo[k]]++] = id;
|
|
1277
1496
|
const mark = (bit, seeds) => {
|
|
1278
1497
|
const queue = [];
|
|
1279
1498
|
for (const n of nodes) if (seeds(n)) { canReach[n.id] |= bit; queue.push(n.id); }
|
|
@@ -1381,25 +1600,26 @@ export function analyze(root, opts = {}) {
|
|
|
1381
1600
|
for (let i = 0; i < open.length; i++) {
|
|
1382
1601
|
const st = open[i];
|
|
1383
1602
|
for (const c of at.get(st.node.id) || []) if (!holds(st.since, c.sink.point, c.sink.kind, c.sink.limit, c.sink.offset, c.sink.nonNeg, c.sink)) lost.add(c);
|
|
1384
|
-
for (
|
|
1603
|
+
for (let k = outStart[st.node.id]; k < outStart[st.node.id + 1]; k++) {
|
|
1385
1604
|
edgesWalked++;
|
|
1386
|
-
const
|
|
1605
|
+
const e = outMeta[k];
|
|
1606
|
+
const to = nodes[outTo[k]];
|
|
1387
1607
|
if (!(canReach[to.id] & reach)) continue;
|
|
1388
1608
|
const mv = moves(e);
|
|
1389
1609
|
if (mv && holds(st.since, pointOfEdge(e), kind, null, shift)) continue;
|
|
1390
|
-
const t = cross(st, e);
|
|
1610
|
+
const t = cross(st, e, to);
|
|
1391
1611
|
if (!t) continue;
|
|
1392
1612
|
const since = mv ? [to] : [...st.since, to];
|
|
1393
1613
|
// An arrival carrying a checked node is refused more than one that does not, so it must not
|
|
1394
1614
|
// stand in for it.
|
|
1395
1615
|
const guarded = since.some((n) => n.checks || n.guard);
|
|
1396
1616
|
if (t.partial) {
|
|
1397
|
-
const key =
|
|
1617
|
+
const key = atContext(to.id, t.ctx, guarded ? 1 : 0);
|
|
1398
1618
|
const held = parts.get(key);
|
|
1399
1619
|
if (held && held.some((r) => (r[2] ? t.vague : !t.vague && r[0] <= t.lo && t.hi <= r[1]))) continue;
|
|
1400
1620
|
if (held) held.push([t.lo, t.hi, t.vague]); else parts.set(key, [[t.lo, t.hi, t.vague]]);
|
|
1401
1621
|
} else if (guarded) {
|
|
1402
|
-
const dk =
|
|
1622
|
+
const dk = atContext(to.id, t.ctx);
|
|
1403
1623
|
if (stampWhole[to.id] === walk || dirty.get(dk) === walk) continue;
|
|
1404
1624
|
dirty.set(dk, walk);
|
|
1405
1625
|
} else if (!firstVisit(to.id, t.ctx)) continue;
|
|
@@ -1423,10 +1643,12 @@ export function analyze(root, opts = {}) {
|
|
|
1423
1643
|
const stampWhole = new Int32Array(nodes.length);
|
|
1424
1644
|
// A whole node reached with a call context, per walk. Reached with none, it stands in for every context.
|
|
1425
1645
|
const stampCtx = new Map();
|
|
1646
|
+
// A node and a call context as one number, with room for a second bit of state.
|
|
1647
|
+
const atContext = (id, ctx, bit = 0) => (ctx * 2 + bit) * nodes.length + id;
|
|
1426
1648
|
const firstVisit = (id, ctx) => {
|
|
1427
1649
|
if (stampWhole[id] === walk) return false;
|
|
1428
1650
|
if (!ctx) { stampWhole[id] = walk; return true; }
|
|
1429
|
-
const key =
|
|
1651
|
+
const key = atContext(id, ctx);
|
|
1430
1652
|
if (stampCtx.get(key) === walk) return false;
|
|
1431
1653
|
stampCtx.set(key, walk);
|
|
1432
1654
|
return true;
|
|
@@ -1447,24 +1669,49 @@ export function analyze(root, opts = {}) {
|
|
|
1447
1669
|
// it does not. Both walks below cross edges through this, so neither can take a route the other
|
|
1448
1670
|
// would refuse.
|
|
1449
1671
|
const known = (n) => n.off != null && n.size != null && !n.repeats;
|
|
1450
|
-
// The call sites a value entered its callees through, innermost last, as a
|
|
1451
|
-
// caller may be returned to: a value born in the callee, or one that
|
|
1452
|
-
// keeps between calls, which a later call from any caller can read.
|
|
1672
|
+
// The call sites a value entered its callees through, innermost last, as a number standing for
|
|
1673
|
+
// that sequence; 0 where any caller may be returned to: a value born in the callee, or one that
|
|
1674
|
+
// rested in storage a program keeps between calls, which a later call from any caller can read.
|
|
1453
1675
|
const CONTEXT_DEPTH = 4;
|
|
1454
|
-
|
|
1455
|
-
|
|
1676
|
+
const contextSites = [[]];
|
|
1677
|
+
const contextNumber = new Map([['', 0]]);
|
|
1678
|
+
const entered = new Map();
|
|
1679
|
+
const returned = new Map();
|
|
1680
|
+
const contextOf = (sites) => {
|
|
1681
|
+
const k = sites.join('/');
|
|
1682
|
+
let c = contextNumber.get(k);
|
|
1683
|
+
if (c === undefined) { c = contextSites.length; contextSites.push(sites); contextNumber.set(k, c); }
|
|
1684
|
+
return c;
|
|
1685
|
+
};
|
|
1686
|
+
function contextOver(state, e, to) {
|
|
1687
|
+
const ctx = state.ctx || 0;
|
|
1456
1688
|
if (e.ret != null) {
|
|
1457
|
-
if (!ctx) return
|
|
1458
|
-
|
|
1459
|
-
|
|
1689
|
+
if (!ctx) return 0;
|
|
1690
|
+
let byRet = returned.get(ctx);
|
|
1691
|
+
if (!byRet) returned.set(ctx, byRet = new Map());
|
|
1692
|
+
let out = byRet.get(e.ret);
|
|
1693
|
+
if (out === undefined) {
|
|
1694
|
+
const sites = contextSites[ctx];
|
|
1695
|
+
out = Number(sites[sites.length - 1]) === e.ret ? contextOf(sites.slice(0, -1)) : null;
|
|
1696
|
+
byRet.set(e.ret, out);
|
|
1697
|
+
}
|
|
1698
|
+
return out;
|
|
1460
1699
|
}
|
|
1461
|
-
if (e.call != null)
|
|
1462
|
-
|
|
1700
|
+
if (e.call != null) {
|
|
1701
|
+
let byCall = entered.get(ctx);
|
|
1702
|
+
if (!byCall) entered.set(ctx, byCall = new Map());
|
|
1703
|
+
let out = byCall.get(e.call);
|
|
1704
|
+
if (out === undefined) {
|
|
1705
|
+
out = contextOf([...contextSites[ctx], String(e.call)].slice(-CONTEXT_DEPTH));
|
|
1706
|
+
byCall.set(e.call, out);
|
|
1707
|
+
}
|
|
1708
|
+
return out;
|
|
1709
|
+
}
|
|
1710
|
+
return to.linkage ? ctx : 0;
|
|
1463
1711
|
}
|
|
1464
|
-
function cross(state, e) {
|
|
1712
|
+
function cross(state, e, to) {
|
|
1465
1713
|
const cur = state.node;
|
|
1466
|
-
const
|
|
1467
|
-
const ctx = contextOver(state, e);
|
|
1714
|
+
const ctx = contextOver(state, e, to);
|
|
1468
1715
|
if (ctx === null) return null;
|
|
1469
1716
|
let partial = false, lo = 0, hi = 0, vague = false;
|
|
1470
1717
|
if (opts.legacyGroupEdges) { /* every edge carries the whole item */ }
|
|
@@ -1613,16 +1860,17 @@ export function analyze(root, opts = {}) {
|
|
|
1613
1860
|
if (best.level && !own) credited.push({ finding: findings[findings.length - 1], node: cur, sink, level: best.level });
|
|
1614
1861
|
if (sink.group) useGroup.set(findings[findings.length - 1], `${src.kind}|${src.file}:${src.line}|${sink.kind}|${sink.group}`);
|
|
1615
1862
|
}
|
|
1616
|
-
for (
|
|
1863
|
+
for (let k = outStart[cur.id]; k < outStart[cur.id + 1]; k++) {
|
|
1617
1864
|
if (++edgesWalked - began > walkBudget) { cut = true; break; }
|
|
1618
|
-
|
|
1619
|
-
const to =
|
|
1620
|
-
|
|
1865
|
+
const e = outMeta[k];
|
|
1866
|
+
const to = nodes[outTo[k]];
|
|
1867
|
+
if (!(canReach[to.id] & reach)) continue;
|
|
1868
|
+
const crossed = cross(state, e, to);
|
|
1621
1869
|
if (!crossed) continue;
|
|
1622
1870
|
if (crossedEdges) crossedEdges.push({ from: cur.id, to: to.id, e });
|
|
1623
1871
|
const { partial, lo, hi, vague, ctx } = crossed;
|
|
1624
1872
|
if (partial) {
|
|
1625
|
-
const pk =
|
|
1873
|
+
const pk = atContext(to.id, ctx);
|
|
1626
1874
|
const held = partSeen.get(pk);
|
|
1627
1875
|
if (held && held.some(r => (r[2] ? vague : !vague && r[0] <= lo && hi <= r[1]))) continue;
|
|
1628
1876
|
if (held) held.push([lo, hi, vague]); else partSeen.set(pk, [[lo, hi, vague]]);
|
|
@@ -1715,6 +1963,8 @@ export function analyze(root, opts = {}) {
|
|
|
1715
1963
|
return { findings, constructs, unstarted, startedBy, stats, sourceKinds: SOURCE_KINDS, sinkKinds: SINK_KINDS, ...(sinks ? { sinks } : {}), ...(sources ? { sources } : {}) };
|
|
1716
1964
|
}
|
|
1717
1965
|
|
|
1966
|
+
export const analyze = (root, opts = {}) => drive(analyzeSteps(root, opts));
|
|
1967
|
+
|
|
1718
1968
|
// What the size check across a CALL needs from an item: its bytes, the bytes from it to the end of
|
|
1719
1969
|
// the record it sits in, whether a table inside it is variable, and where it was declared.
|
|
1720
1970
|
function extentOf(item) {
|