@portll/cobolwork 0.6.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 +49 -18
- package/STABILITY.md +76 -0
- package/bin/cobolwork.mjs +30 -7
- package/lib/bms.mjs +21 -7
- package/lib/build.mjs +108 -53
- package/lib/capabilities.mjs +9 -10
- package/lib/cics-commands.mjs +501 -9
- package/lib/compliance.mjs +11 -0
- package/lib/consequence.mjs +13 -0
- package/lib/control-workers.mjs +161 -0
- package/lib/control.mjs +52 -3
- package/lib/dataflow.mjs +176 -125
- 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/evidence/cli.mjs +14 -3
- package/lib/evidence/record.mjs +1 -1
- package/lib/evidence/store.mjs +44 -31
- 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 +13 -1
- package/lib/hlasm/asm/sections.mjs +3 -1
- package/lib/hlasm/instr.mjs +25 -0
- package/lib/hlasm/macro/authorization.mjs +24 -4
- package/lib/hlasm/macro/datasets.mjs +30 -17
- package/lib/hlasm/macro/io.mjs +68 -45
- package/lib/hlasm/macro/le.mjs +2 -2
- package/lib/hlasm/macro/operator.mjs +32 -21
- package/lib/hlasm/macro/program.mjs +126 -84
- package/lib/hlasm/macro/recovery.mjs +10 -5
- package/lib/hlasm/macro/storage.mjs +64 -14
- package/lib/hlasm/macro/structured.mjs +1 -1
- package/lib/hlasm/model.mjs +35 -10
- package/lib/hlasm/mvs38.mjs +47 -0
- package/lib/hlasm/operands.mjs +10 -0
- package/lib/hlasm/optable.mjs +61 -0
- package/lib/hlasm/read.mjs +22 -8
- 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/ironwork.mjs +17 -13
- package/lib/kernel/pds-archive.mjs +256 -0
- package/lib/kernel/registry.mjs +14 -8
- package/lib/kernel/shared-pass.mjs +34 -9
- package/lib/kernel/source-tree.mjs +104 -36
- package/lib/kernel/version-key.mjs +17 -0
- package/lib/layout.mjs +26 -31
- 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/precompile-check.mjs +124 -0
- package/lib/precompile-cics.mjs +7 -3
- package/lib/reach.mjs +11 -2
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +41 -3
- package/lib/scan.mjs +7 -1
- package/lib/sets/abend.mjs +16 -6
- package/lib/sets/cics.mjs +15 -35
- package/lib/sets/compile.mjs +41 -15
- package/lib/sets/crypto.mjs +5 -3
- package/lib/sets/ddl.mjs +36 -0
- package/lib/sets/flow.mjs +18 -1
- package/lib/sets/hidden.mjs +5 -3
- package/lib/sets/hlasm.mjs +72 -12
- package/lib/sets/ims.mjs +139 -0
- package/lib/sets/log.mjs +6 -6
- package/lib/sets/opaque.mjs +27 -7
- package/lib/sets/pli.mjs +40 -0
- package/lib/sets/recon.mjs +5 -3
- package/lib/sets/secrets.mjs +5 -3
- package/lib/sets/semantics.mjs +3 -0
- package/lib/sets/web.mjs +36 -22
- 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 +432 -1
- package/rules/compliance-dora.json +414 -1
- package/rules/compliance-ffiec.json +414 -1
- package/rules/compliance-nist80053.json +466 -1
- 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.site.schema.json +116 -0
package/lib/dataflow.mjs
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
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';
|
|
@@ -14,12 +22,14 @@ import { isJcl } from './sources.mjs';
|
|
|
14
22
|
import { watchMemoryBuffer } from './kernel/memory.mjs';
|
|
15
23
|
import { drive, loopOver } from './kernel/shared-pass.mjs';
|
|
16
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';
|
|
17
27
|
import { createHash } from 'node:crypto';
|
|
18
28
|
|
|
19
29
|
// A flow finding is located by source and sink rather than by one path and line, so it keeps its
|
|
20
30
|
// own comparator. The text comparison underneath it is the shared one.
|
|
21
31
|
import { byText } from './kernel/findings.mjs';
|
|
22
|
-
import { buildControl, creditOf, hasFact,
|
|
32
|
+
import { buildControl, creditOf, hasFact, internFacts, namesOf, topOf, within, wholeTop } from './control.mjs';
|
|
23
33
|
import { directoryTree } from './kernel/source-tree.mjs';
|
|
24
34
|
|
|
25
35
|
// Every reused control analysis checked against the program's own: for the equivalence runs and tests.
|
|
@@ -35,6 +45,7 @@ export const SOURCE_KINDS = {
|
|
|
35
45
|
'cics-terminal': 'EXEC CICS RECEIVE',
|
|
36
46
|
'cics-web': 'EXEC CICS WEB RECEIVE or EXTRACT',
|
|
37
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',
|
|
38
49
|
'database': 'host variable filled by EXEC SQL',
|
|
39
50
|
'jcl-parm': 'PARM= on a JCL EXEC statement',
|
|
40
51
|
'jcl-instream': 'in-stream data on a JCL DD statement',
|
|
@@ -46,7 +57,7 @@ export const SINK_KINDS = {
|
|
|
46
57
|
'dynamic-program-load': 'CALL whose target is a variable',
|
|
47
58
|
'cics-dynamic-transfer': 'EXEC CICS LINK, XCTL or START with a variable program or transaction',
|
|
48
59
|
'dynamic-sql': 'EXEC SQL EXECUTE IMMEDIATE or PREPARE from a host variable',
|
|
49
|
-
'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',
|
|
50
61
|
'outbound-http': 'EXEC CICS WEB CONVERSE, WEB SEND on a client session, or the containers INVOKE SERVICE sends',
|
|
51
62
|
'socket-send': 'CALL EZASOKET or EZACICAL with SEND, SENDTO or WRITE',
|
|
52
63
|
'message-queue': 'CALL MQPUT or MQPUT1',
|
|
@@ -105,36 +116,6 @@ function consKey(v) {
|
|
|
105
116
|
return `${v instanceof Set ? 'S' : parts ? 'A' : 'O'}[${keys.join(',')}]`;
|
|
106
117
|
}
|
|
107
118
|
|
|
108
|
-
// The facts a program keeps per statement: one copy of each distinct set, held as the numbers of the
|
|
109
|
-
// facts in it, all in one buffer per program. Neighbouring statements mostly hold the same checks, and
|
|
110
|
-
// few of a program's checks hold at any one statement: 26 large programs kept 1.5 GB of bitsets, 0.4%
|
|
111
|
-
// of their bits set.
|
|
112
|
-
function internFacts(facts) {
|
|
113
|
-
const byHash = new Map();
|
|
114
|
-
const kept = [];
|
|
115
|
-
const keptAt = new Map();
|
|
116
|
-
for (const [point, bits] of facts) {
|
|
117
|
-
let h = 2166136261;
|
|
118
|
-
for (let i = 0; i < bits.length; i++) h = Math.imul(h ^ bits[i], 16777619);
|
|
119
|
-
let same = byHash.get(h);
|
|
120
|
-
if (!same) byHash.set(h, same = []);
|
|
121
|
-
let k = same.find((x) => x.bits.length === bits.length && x.bits.every((v, i) => v === bits[i]));
|
|
122
|
-
if (!k) { same.push(k = { bits, held: sparseFacts(bits) }); kept.push(k); }
|
|
123
|
-
keptAt.set(point, k);
|
|
124
|
-
}
|
|
125
|
-
let size = 0;
|
|
126
|
-
for (const k of kept) if (k.held instanceof Uint16Array) size += k.held.length;
|
|
127
|
-
const pool = new Uint16Array(size);
|
|
128
|
-
let at = 0;
|
|
129
|
-
for (const k of kept) {
|
|
130
|
-
if (!(k.held instanceof Uint16Array)) continue;
|
|
131
|
-
pool.set(k.held, at);
|
|
132
|
-
k.held = pool.subarray(at, at += k.held.length);
|
|
133
|
-
}
|
|
134
|
-
for (const [point, k] of keptAt) facts.set(point, k.held);
|
|
135
|
-
return facts;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
119
|
// Every literal a program holds that could be a program name: its VALUE clauses and the literals its
|
|
139
120
|
// statements and commands use. A menu that XCTLs through a table of names starts every one of them.
|
|
140
121
|
function literalNames(prog) {
|
|
@@ -186,18 +167,21 @@ function boundsOf(counter, until) {
|
|
|
186
167
|
// nearly every subscript descends from a file record, and reporting those would be reporting batch.
|
|
187
168
|
const FROM_OUTSIDE = ['argv-or-env', 'cics-terminal', 'cics-web', 'jcl-parm', 'jcl-instream'];
|
|
188
169
|
// What can reach a CICS command: a batch job never issues one.
|
|
189
|
-
const IN_CICS = ['cics-web', 'cics-terminal', 'file-record', 'database'];
|
|
170
|
+
const IN_CICS = ['cics-web', 'cics-terminal', 'file-record', 'cics-queue', 'database'];
|
|
190
171
|
const REFLECTED = ['cics-web', 'cics-terminal'];
|
|
191
172
|
// What the system says about a failure: a command's RESP, the EIB's copy of it, the SQLCA. Sent to a
|
|
192
173
|
// web client it describes the system behind the program (CWE-209); written anywhere else it is the
|
|
193
174
|
// program's own business, so these reach only a response, and Db2's message text the terminal too.
|
|
194
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']);
|
|
195
178
|
const TO_CLIENT = ['web-response', 'http-header'];
|
|
196
179
|
|
|
197
180
|
// Exfiltration is data at rest leaving the program. Terminal or web input reaching the same channel
|
|
198
181
|
// is the program relaying its own caller's data, and pairing it would fire on every gateway.
|
|
199
182
|
|
|
200
183
|
// A trace longer than this keeps its two ends and says how many hops it dropped.
|
|
184
|
+
|
|
201
185
|
export const TRACE_MAX = 64;
|
|
202
186
|
export const TRACE_KEEP = 24;
|
|
203
187
|
// Edges one source's walk may examine, and all walks together: about fifteen times what the largest
|
|
@@ -205,7 +189,7 @@ export const TRACE_KEEP = 24;
|
|
|
205
189
|
export const WALK_EDGES = 5_000_000;
|
|
206
190
|
export const TOTAL_EDGES = 500_000_000;
|
|
207
191
|
|
|
208
|
-
const DATA_AT_REST = ['database', 'file-record'];
|
|
192
|
+
const DATA_AT_REST = ['database', 'file-record', 'cics-queue'];
|
|
209
193
|
// The class tests a condition can make, which restrict what a field holds rather than compare it.
|
|
210
194
|
const CLASS_TEST = new Set(['NUMERIC', 'ALPHABETIC', 'ALPHABETIC-LOWER', 'ALPHABETIC-UPPER', 'POSITIVE', 'NEGATIVE']);
|
|
211
195
|
const SOCKET_ROUTINE = /^(EZASOKET|EZACICAL)$/i;
|
|
@@ -215,27 +199,6 @@ const SOCKET_SEND = /^(SEND|SENDTO|WRITE|SENDMSG|WRITEV)$/i;
|
|
|
215
199
|
// MsgDesc, PutMsgOpts, BufferLength, Buffer, …): the buffer is the sixth argument of both.
|
|
216
200
|
const MQ_BUFFER_ARG = { MQPUT: 5, MQPUT1: 5 };
|
|
217
201
|
|
|
218
|
-
function execOptions(exec) {
|
|
219
|
-
const opts = new Map();
|
|
220
|
-
const toks = exec.toks;
|
|
221
|
-
const words = [];
|
|
222
|
-
for (let i = 0; i < toks.length; i++) {
|
|
223
|
-
const t = toks[i];
|
|
224
|
-
if (t.t !== 'word') continue;
|
|
225
|
-
words.push(t.u);
|
|
226
|
-
if (toks[i + 1] && toks[i + 1].t === 'sep' && toks[i + 1].v === '(') {
|
|
227
|
-
const inner = [];
|
|
228
|
-
let depth = 1;
|
|
229
|
-
for (let k = i + 2; k < toks.length && depth > 0; k++) {
|
|
230
|
-
if (toks[k].t === 'sep') { depth += toks[k].v === '(' ? 1 : -1; if (depth === 0) break; continue; }
|
|
231
|
-
inner.push(toks[k]);
|
|
232
|
-
}
|
|
233
|
-
if (!opts.has(t.u)) opts.set(t.u, inner);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return { opts, words };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
202
|
export function* analyzeSteps(root, opts = {}) {
|
|
240
203
|
const repos = opts.repos || [''];
|
|
241
204
|
const stats = { files: 0, programs: 0, edges: 0, nodes: 0, threw: 0, overBudget: 0, unreadable: 0, ebcdic: 0,
|
|
@@ -335,16 +298,18 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
335
298
|
const textKey = (src) => createHash('sha1').update(src).digest('base64');
|
|
336
299
|
function repeatedTexts(tree, files) {
|
|
337
300
|
const count = new Map();
|
|
301
|
+
const later = new Set();
|
|
338
302
|
for (const f of files) {
|
|
339
303
|
let src;
|
|
340
304
|
try { src = tree.text(f).text; } catch { continue; }
|
|
341
305
|
if (src.length < reuseMin) continue;
|
|
342
306
|
const k = textKey(src);
|
|
307
|
+
if (count.has(k)) later.add(f);
|
|
343
308
|
count.set(k, (count.get(k) || 0) + 1);
|
|
344
309
|
}
|
|
345
|
-
const
|
|
346
|
-
for (const [k, n] of count) if (n > 1)
|
|
347
|
-
return
|
|
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 };
|
|
348
313
|
}
|
|
349
314
|
const copyText = new Map();
|
|
350
315
|
const copyKey = (r, tree) => r.copies.map((c) => {
|
|
@@ -352,20 +317,21 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
352
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); }
|
|
353
318
|
return `${c.status}:${copyText.get(c.path)}`;
|
|
354
319
|
}).join('|');
|
|
355
|
-
const reuseOf = (copied, k, r, f, tree) => ({ kept: copied.kept, key: `${k}|${copyKey(r, tree)}`, files:
|
|
356
|
-
// A
|
|
357
|
-
function sameAsOwn(ctl, prog, resolver, file) {
|
|
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) {
|
|
358
323
|
let own = null;
|
|
359
|
-
try { own = buildControl(prog, resolver); } catch { /* the
|
|
324
|
+
try { own = buildControl(prog, resolver); } catch { /* the restored one threw too, or it differs */ }
|
|
360
325
|
if (own) internFacts(own.facts);
|
|
361
|
-
if (!own !== !ctl || (own && !sameAnalysis(own, ctl, new Set(parseOrder(prog))))) throw new Error(`the control analysis
|
|
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`);
|
|
362
327
|
}
|
|
363
328
|
|
|
364
|
-
|
|
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) {
|
|
365
332
|
programOf[pk] = { id: prog.id, file };
|
|
366
333
|
const byItem = new Map();
|
|
367
|
-
const byName =
|
|
368
|
-
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);
|
|
369
335
|
const nodeOf = (item) => {
|
|
370
336
|
let n = byItem.get(item);
|
|
371
337
|
if (!n) {
|
|
@@ -394,10 +360,7 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
394
360
|
}
|
|
395
361
|
return n;
|
|
396
362
|
};
|
|
397
|
-
// Names inside EXEC blocks never pass through the parser's reference resolution, so fall back to the name.
|
|
398
|
-
const itemOfToken = (tok) => (tok ? prog.resolved.get(tok) || (tok.t === 'word' ? byName.get(tok.u) || null : null) : null);
|
|
399
363
|
// An INDEXED BY name is not a data item, so it needs a node of its own to carry taint.
|
|
400
|
-
const indexNames = new Set(prog.items.flatMap((it) => it.indexNames || []));
|
|
401
364
|
const indexNodes = new Map();
|
|
402
365
|
const indexNode = (name) => {
|
|
403
366
|
let n = indexNodes.get(name);
|
|
@@ -412,10 +375,11 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
412
375
|
// A system field the program never declares - the EIB's, or an SQLCA the precompiler supplies -
|
|
413
376
|
// still holds a value, so it needs a node of its own, as an index name does.
|
|
414
377
|
const systemNodes = new Map();
|
|
378
|
+
const systemFields = isPli(file) ? PLI_SYSTEM_FIELDS : SYSTEM_FIELDS;
|
|
415
379
|
// A code on a terminal's error line is ordinary; Db2's message text names tables, columns and
|
|
416
380
|
// constraints, so only SQLERRMC reaches the screen sink.
|
|
417
381
|
const systemSource = (n, detail, at) => {
|
|
418
|
-
const onlyTo = /^SQLERRMC
|
|
382
|
+
const onlyTo = /^SQLERRMC?\b/.test(detail) ? [...TO_CLIENT, 'screen'] : TO_CLIENT;
|
|
419
383
|
if (!n.sources.some((x) => x.kind === 'system-response')) addSource(n, { kind: 'system-response', onlyTo, noCredit: true, ...at, detail });
|
|
420
384
|
};
|
|
421
385
|
const systemNode = (tok) => {
|
|
@@ -434,30 +398,34 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
434
398
|
if (it) return nodeOf(it);
|
|
435
399
|
if (!tok || tok.t !== 'word') return null;
|
|
436
400
|
if (indexNames.has(tok.u)) return indexNode(tok.u);
|
|
437
|
-
return
|
|
401
|
+
return systemFields.has(tok.u) ? systemNode(tok) : null;
|
|
438
402
|
};
|
|
439
403
|
// The program's statement order, where its structure could be read. Without it a check is
|
|
440
404
|
// credited as it always was, wherever its field is used, and never clears anything.
|
|
441
405
|
let ctl = null;
|
|
442
|
-
|
|
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.
|
|
443
408
|
const kept = reuse && reuse.kept.get(reuse.key);
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (
|
|
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);
|
|
448
415
|
} else {
|
|
449
|
-
|
|
416
|
+
let threw = false;
|
|
417
|
+
try { ctl = buildControl(prog, resolver); } catch { threw = true; stats.programsUnordered++; }
|
|
450
418
|
if (ctl) internFacts(ctl.facts);
|
|
451
419
|
if (reuse) {
|
|
452
420
|
const value = ctl ? keep(ctl, parseOrder(prog), reuse.files) : null;
|
|
453
|
-
if (!ctl || value) reuse.kept.set(reuse.key,
|
|
421
|
+
if (!ctl || value) reuse.kept.set(reuse.key, value ? { value } : threw ? { threw: true } : { none: true });
|
|
454
422
|
}
|
|
455
423
|
}
|
|
456
424
|
const pointOf = (x) => (ctl ? ctl.nodeOf.get(x) : undefined);
|
|
457
425
|
// Where a name inside a condition is read, which can know more than the statement does.
|
|
458
426
|
const placeOf = (tok, st) => (ctl && ctl.posOf.get(tok)) ?? pointOf(st);
|
|
459
427
|
|
|
460
|
-
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 });
|
|
461
429
|
// What a program in a CICS region DISPLAYs goes to its CESE and CESO queues, a log. Terminal or web
|
|
462
430
|
// input written there unchecked can carry the control characters that start a line the program
|
|
463
431
|
// did not write (CWE-117). A batch program's DISPLAY is its job's own log, which no terminal
|
|
@@ -512,15 +480,37 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
512
480
|
const recordOf = (it) => { let top = it; while (top.parent) top = top.parent; return top; };
|
|
513
481
|
for (const it of prog.items) { const top = recordOf(it); if (!byRecord.has(top)) byRecord.set(top, []); byRecord.get(top).push(it); }
|
|
514
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
|
+
};
|
|
515
497
|
for (const c of ctl.checks) {
|
|
516
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 } : {}) };
|
|
517
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));
|
|
518
506
|
const span = extent(c.field);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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);
|
|
523
512
|
}
|
|
513
|
+
for (const i of [...inside].sort((a, b) => a - b)) (nodeOf(items[i]).checks ||= []).push(entry);
|
|
524
514
|
}
|
|
525
515
|
}
|
|
526
516
|
// Without the order, a condition that restricts what an item may hold is credited wherever the
|
|
@@ -771,6 +761,13 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
771
761
|
const a = c.using[0];
|
|
772
762
|
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
773
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}` });
|
|
774
771
|
} else if (String(c.name).toUpperCase() === 'CEEGTST') {
|
|
775
772
|
// CEEGTST(heap id, size, address, feedback code): the size is the second argument.
|
|
776
773
|
const a = c.using[1];
|
|
@@ -891,25 +888,24 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
891
888
|
const changes = new Map();
|
|
892
889
|
for (const e of prog.execs) {
|
|
893
890
|
if (e.kind !== 'CICS') continue;
|
|
894
|
-
const { opts: o,
|
|
895
|
-
if (
|
|
891
|
+
const { opts: o, verb } = execReading(e);
|
|
892
|
+
if (verb !== 'REWRITE' && !(verb === 'DELETE' && !o.has('RIDFLD'))) continue;
|
|
896
893
|
const file = optionName(o, 'DATASET') || optionName(o, 'FILE');
|
|
897
|
-
if (file && !changes.has(file)) changes.set(file,
|
|
894
|
+
if (file && !changes.has(file)) changes.set(file, verb === 'REWRITE' ? 'rewrites' : 'deletes');
|
|
898
895
|
}
|
|
899
896
|
for (const e of prog.execs) {
|
|
900
|
-
const { opts: o, words } =
|
|
897
|
+
const { opts: o, words, verb, sub, command, direction } = execReading(e);
|
|
901
898
|
const at = { file: e.file, line: e.line, point: pointOf(e) };
|
|
902
899
|
if (e.kind === 'CICS') {
|
|
903
|
-
const verb = words[0];
|
|
904
900
|
const intoTok = (o.get('INTO') || o.get('SET') || [])[0];
|
|
905
901
|
for (const opt of ['RESP', 'RESP2']) {
|
|
906
902
|
const r = nodeOfToken((o.get(opt) || [])[0]);
|
|
907
903
|
if (r) systemSource(r, `the ${opt} of EXEC CICS ${verb}`, at);
|
|
908
904
|
}
|
|
909
|
-
if (verb === 'WRITE' && ['OPERATOR', 'JOURNALNAME', 'JOURNALNUM'].includes(
|
|
910
|
-
const opt =
|
|
905
|
+
if (verb === 'WRITE' && ['OPERATOR', 'JOURNALNAME', 'JOURNALNUM'].includes(sub)) {
|
|
906
|
+
const opt = sub === 'OPERATOR' ? 'TEXT' : 'FROM';
|
|
911
907
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
912
|
-
if (n) addSink(n, { kind: 'log', onlyFrom: REFLECTED, ...at, detail: `EXEC CICS WRITE ${
|
|
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'}` });
|
|
913
909
|
}
|
|
914
910
|
if (verb === 'RECEIVE') {
|
|
915
911
|
const isWeb = words.includes('WEB');
|
|
@@ -924,12 +920,12 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
924
920
|
if (!isWeb && o.has('MAP')) receiveMap(e, o, itemOfToken(intoTok), at);
|
|
925
921
|
}
|
|
926
922
|
// What the terminal shows: SEND TEXT's FROM, or the map's output record, <map>O without FROM.
|
|
927
|
-
if (verb === 'SEND' && (
|
|
923
|
+
if (verb === 'SEND' && (sub === 'TEXT' || sub === 'MAP')) {
|
|
928
924
|
const from = nodeOfToken((o.get('FROM') || [])[0]);
|
|
929
|
-
const map =
|
|
925
|
+
const map = sub === 'MAP' ? optionName(o, 'MAP') : null;
|
|
930
926
|
const sym = !from && map ? byName.get(`${map}O`) : null;
|
|
931
927
|
const n = from || (sym ? nodeOf(sym) : null);
|
|
932
|
-
if (n) addSink(n, { kind: 'screen', onlyFrom: ['system-response'], ...at, detail: `EXEC CICS SEND ${
|
|
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` });
|
|
933
929
|
}
|
|
934
930
|
// RIDFLD decides which record a READ or DELETE reaches, or where a browse starts. A key the
|
|
935
931
|
// user types is how a lookup works; a key the program put in a protected field and read back
|
|
@@ -956,11 +952,11 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
956
952
|
if (name) channelOf.set(name, optionName(o, 'CHANNEL'));
|
|
957
953
|
}
|
|
958
954
|
// A service call sends every container on its channel, and its URI decides where they go.
|
|
959
|
-
if (verb === 'INVOKE' && (
|
|
960
|
-
invokes.push({ at, what:
|
|
955
|
+
if (verb === 'INVOKE' && (sub === 'SERVICE' || sub === 'WEBSERVICE')) {
|
|
956
|
+
invokes.push({ at, what: sub, channel: optionName(o, 'CHANNEL') });
|
|
961
957
|
for (const opt of ['URI', 'URIMAP']) {
|
|
962
958
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
963
|
-
if (n) addSink(n, { kind: 'outbound-host', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS INVOKE ${
|
|
959
|
+
if (n) addSink(n, { kind: 'outbound-host', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS INVOKE ${sub} ${opt}(...), which decides where the request goes` });
|
|
964
960
|
}
|
|
965
961
|
}
|
|
966
962
|
if (verb === 'GET' && o.has('CONTAINER')) {
|
|
@@ -970,17 +966,17 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
970
966
|
}
|
|
971
967
|
// CONVERSE is always a client call; SEND is one only on a session this program opened. A
|
|
972
968
|
// plain WEB SEND answers the request that started the program, which is its job.
|
|
973
|
-
if (verb === 'WEB' && (
|
|
969
|
+
if (verb === 'WEB' && (sub === 'CONVERSE' || (sub === 'SEND' && o.has('SESSTOKEN')))) {
|
|
974
970
|
const n = nodeOfToken((o.get('FROM') || [])[0]);
|
|
975
|
-
if (n) addSink(n, { kind: 'outbound-http', onlyFrom: DATA_AT_REST, ...at, detail: `EXEC CICS WEB ${
|
|
971
|
+
if (n) addSink(n, { kind: 'outbound-http', onlyFrom: DATA_AT_REST, ...at, detail: `EXEC CICS WEB ${sub} to a remote server` });
|
|
976
972
|
// The request may be a container rather than a field.
|
|
977
973
|
const sent = o.has('CONTAINER') ? optionName(o, 'CONTAINER') : null;
|
|
978
|
-
if (sent) addSink(containerNode(sent), { kind: 'outbound-http', onlyFrom: DATA_AT_REST, ...at, detail: `EXEC CICS WEB ${
|
|
974
|
+
if (sent) addSink(containerNode(sent), { kind: 'outbound-http', onlyFrom: DATA_AT_REST, ...at, detail: `EXEC CICS WEB ${sub} sending CONTAINER(${sent})` });
|
|
979
975
|
}
|
|
980
976
|
// The response this program returns to its own caller, and the document built for it. CICS
|
|
981
977
|
// escapes nothing on the way out - DOCUMENT SET's UNESCAPED option is about URL decoding,
|
|
982
978
|
// not HTML - so whatever reaches here is what the browser is handed.
|
|
983
|
-
if (verb === 'WEB' &&
|
|
979
|
+
if (verb === 'WEB' && sub === 'SEND' && !o.has('SESSTOKEN')) {
|
|
984
980
|
const n = nodeOfToken((o.get('FROM') || [])[0]);
|
|
985
981
|
// A stored value becomes script only where the reply is markup, and a web program
|
|
986
982
|
// answering with a database row is the ordinary shape of one. Input echoed back is a
|
|
@@ -990,28 +986,37 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
990
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()}` : ''}` });
|
|
991
987
|
}
|
|
992
988
|
if (verb === 'DOCUMENT') {
|
|
993
|
-
const what =
|
|
989
|
+
const what = sub === 'SET' ? ['VALUE', 'SYMBOLLIST'] : ['TEXT', 'FROM', 'BINARY'];
|
|
994
990
|
for (const opt of what) {
|
|
995
991
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
996
|
-
if (n) addSink(n, { kind: 'web-response', onlyFrom: [...IN_CICS, 'system-response'], ...at, detail: `EXEC CICS DOCUMENT ${
|
|
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` });
|
|
997
993
|
}
|
|
998
994
|
}
|
|
999
|
-
if (verb === 'WEB' &&
|
|
995
|
+
if (verb === 'WEB' && sub === 'WRITE' && o.has('HTTPHEADER')) {
|
|
1000
996
|
for (const opt of ['HTTPHEADER', 'VALUE']) {
|
|
1001
997
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
1002
998
|
if (n) addSink(n, { kind: 'http-header', onlyFrom: [...IN_CICS, 'system-response'], ...at, detail: `EXEC CICS WEB WRITE HTTPHEADER ${opt}(...)` });
|
|
1003
999
|
}
|
|
1004
1000
|
}
|
|
1005
|
-
if (verb === 'WEB' && (
|
|
1001
|
+
if (verb === 'WEB' && (sub === 'OPEN' || sub === 'CONVERSE')) {
|
|
1006
1002
|
for (const opt of ['HOST', 'URIMAP', 'PATH']) {
|
|
1007
1003
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
1008
|
-
if (n) addSink(n, { kind: 'outbound-host', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS WEB ${
|
|
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` });
|
|
1009
1014
|
}
|
|
1010
1015
|
}
|
|
1011
1016
|
// The queue a command acts on, as distinct from what it writes there: a name from input
|
|
1012
1017
|
// reads, overwrites or deletes whatever queue the input asks for.
|
|
1013
1018
|
if (['WRITEQ', 'READQ', 'DELETEQ'].includes(verb)) {
|
|
1014
|
-
const ts =
|
|
1019
|
+
const ts = sub === 'TS' || !words.includes('TD');
|
|
1015
1020
|
for (const opt of ['QUEUE', 'QNAME']) {
|
|
1016
1021
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
1017
1022
|
if (n) addSink(n, { kind: 'queue-name', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS ${verb} ${ts ? 'TS' : 'TD'} ${opt}(...) names the queue` });
|
|
@@ -1024,23 +1029,40 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
1024
1029
|
const n = nodeOfToken((o.get('SYSID') || [])[0]);
|
|
1025
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` });
|
|
1026
1031
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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` });
|
|
1030
1038
|
}
|
|
1031
1039
|
if (verb === 'READ' || verb === 'READNEXT' || verb === 'READPREV') {
|
|
1032
1040
|
const n = nodeOfToken(intoTok);
|
|
1033
1041
|
if (n && (o.has('FILE') || o.has('DATASET'))) addSource(n, { kind: 'file-record', ...at, detail: `EXEC CICS ${verb} FILE ... INTO` });
|
|
1034
1042
|
}
|
|
1035
1043
|
// A CONVERSE response is the remote server's data, which is untrusted input like any other.
|
|
1036
|
-
if ((verb === 'WEB' &&
|
|
1044
|
+
if ((verb === 'WEB' && sub !== 'SEND') || verb === 'EXTRACT') {
|
|
1045
|
+
const n = nodeOfToken(intoTok);
|
|
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') {
|
|
1037
1059
|
const n = nodeOfToken(intoTok);
|
|
1038
|
-
if (n) addSource(n, { kind: 'cics-
|
|
1060
|
+
if (n) addSource(n, { kind: 'cics-queue', ...at, detail: `EXEC CICS ${command} INTO` });
|
|
1039
1061
|
}
|
|
1040
1062
|
// Where a transient-data write lands is not in the program: the CSD maps the queue to a DD
|
|
1041
1063
|
// and the region's JCL maps the DD to a destination. Collected here, resolved once every
|
|
1042
1064
|
// definition has been read.
|
|
1043
|
-
if (verb === 'WRITEQ' &&
|
|
1065
|
+
if (verb === 'WRITEQ' && sub === 'TD') {
|
|
1044
1066
|
const from = nodeOfToken((o.get('FROM') || [])[0]);
|
|
1045
1067
|
const qTok = (o.get('QUEUE') || o.get('TDQUEUE') || [])[0];
|
|
1046
1068
|
const qItem = qTok && qTok.t === 'word' ? itemOfToken(qTok) : null;
|
|
@@ -1068,11 +1090,16 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
1068
1090
|
if (t && t.t === 'lit') startsTransactions.add(String(t.v).trim().toUpperCase());
|
|
1069
1091
|
}
|
|
1070
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
|
+
}
|
|
1071
1099
|
if (e.kind === 'SQL') {
|
|
1072
1100
|
// A host variable names the item a qualified :G.F resolves to; the statement fills the ones
|
|
1073
1101
|
// Db2 writes, and those hold what the database held.
|
|
1074
1102
|
const hv = e.hostVariables || hostVariablesIn(e.toks);
|
|
1075
|
-
const verb = words[0];
|
|
1076
1103
|
for (const h of hv) {
|
|
1077
1104
|
if (!h.written) continue;
|
|
1078
1105
|
const n = nodeOfToken(h.tok);
|
|
@@ -1128,6 +1155,16 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
1128
1155
|
const tree = (!repo && opts.tree) ? opts.tree : directoryTree(base, opts);
|
|
1129
1156
|
const idx = tree.index;
|
|
1130
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
|
+
};
|
|
1131
1168
|
for (const f of tree.list().filter(isJcl).filter(inScope(opts))) { jclPending.push(f); jclTrees.set(f, tree); }
|
|
1132
1169
|
for (const f of tree.list().filter((p) => /\.csd$/i.test(p)).filter(inScope(opts))) {
|
|
1133
1170
|
try { addCsd(tree.text(f).text, f); } catch (e) { stats.unreadable++; unread.push(`${tree.rel(f)}: ${e.code || e.name}`); }
|
|
@@ -1146,26 +1183,38 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
1146
1183
|
// edges are proportional to data items and statements, not to bytes, so a repository of small
|
|
1147
1184
|
// programs with large record layouts can exhaust a heap the budget believes is untouched. The
|
|
1148
1185
|
// shared loop watches the heap itself, and the budget stays as the cheaper first line.
|
|
1149
|
-
const repeated = repeatedTexts(tree, files);
|
|
1150
|
-
|
|
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) => {
|
|
1151
1195
|
let src;
|
|
1152
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; }
|
|
1153
|
-
|
|
1197
|
+
const pli = isPli(f);
|
|
1198
|
+
if (!pli && !/PROCEDURE\s+DIVISION|PROGRAM-ID/i.test(src)) return 0;
|
|
1154
1199
|
if (held + src.length > budget) { stats.overBudget++; return 0; }
|
|
1155
1200
|
held += src.length;
|
|
1156
1201
|
let r;
|
|
1157
1202
|
try {
|
|
1158
|
-
r = tree.parse(f, src);
|
|
1203
|
+
r = pli ? parsePliSource(src, f, { readMember }) : tree.parse(f, src);
|
|
1159
1204
|
} catch (e) { stats.threw++; unparsed.push(`${tree.rel(f)}: ${e.code || e.name}`); return src.length; }
|
|
1160
1205
|
stats.files++;
|
|
1161
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;
|
|
1162
1209
|
r.programs.forEach((p, k) => {
|
|
1163
|
-
programs.push(summarise(p, f, programs.length, r.options, copied ? reuseOf(copied, k, r, f, tree) : null));
|
|
1210
|
+
programs.push(summarise(p, f, programs.length, r.options, copied ? reuseOf(copied, k, r, f, tree) : null, builtFiles && { ...answer[k], files: builtFiles }));
|
|
1164
1211
|
stats.programs++;
|
|
1165
1212
|
});
|
|
1166
1213
|
if (copied && --copied.left === 0) repeated.delete(copied.text);
|
|
1167
1214
|
return src.length;
|
|
1168
|
-
}
|
|
1215
|
+
};
|
|
1216
|
+
let run;
|
|
1217
|
+
try { run = yield loopOver(files, readProgram, { label: 'flow', watcher: memoryWatcher }); } finally { ahead?.close(); }
|
|
1169
1218
|
|
|
1170
1219
|
// A file the loop never reached is not the same as one past the byte budget, and they are
|
|
1171
1220
|
// counted separately so a report can say which limit it hit.
|
|
@@ -1565,12 +1614,12 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
1565
1614
|
// stand in for it.
|
|
1566
1615
|
const guarded = since.some((n) => n.checks || n.guard);
|
|
1567
1616
|
if (t.partial) {
|
|
1568
|
-
const key =
|
|
1617
|
+
const key = atContext(to.id, t.ctx, guarded ? 1 : 0);
|
|
1569
1618
|
const held = parts.get(key);
|
|
1570
1619
|
if (held && held.some((r) => (r[2] ? t.vague : !t.vague && r[0] <= t.lo && t.hi <= r[1]))) continue;
|
|
1571
1620
|
if (held) held.push([t.lo, t.hi, t.vague]); else parts.set(key, [[t.lo, t.hi, t.vague]]);
|
|
1572
1621
|
} else if (guarded) {
|
|
1573
|
-
const dk =
|
|
1622
|
+
const dk = atContext(to.id, t.ctx);
|
|
1574
1623
|
if (stampWhole[to.id] === walk || dirty.get(dk) === walk) continue;
|
|
1575
1624
|
dirty.set(dk, walk);
|
|
1576
1625
|
} else if (!firstVisit(to.id, t.ctx)) continue;
|
|
@@ -1594,10 +1643,12 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
1594
1643
|
const stampWhole = new Int32Array(nodes.length);
|
|
1595
1644
|
// A whole node reached with a call context, per walk. Reached with none, it stands in for every context.
|
|
1596
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;
|
|
1597
1648
|
const firstVisit = (id, ctx) => {
|
|
1598
1649
|
if (stampWhole[id] === walk) return false;
|
|
1599
1650
|
if (!ctx) { stampWhole[id] = walk; return true; }
|
|
1600
|
-
const key =
|
|
1651
|
+
const key = atContext(id, ctx);
|
|
1601
1652
|
if (stampCtx.get(key) === walk) return false;
|
|
1602
1653
|
stampCtx.set(key, walk);
|
|
1603
1654
|
return true;
|
|
@@ -1819,7 +1870,7 @@ export function* analyzeSteps(root, opts = {}) {
|
|
|
1819
1870
|
if (crossedEdges) crossedEdges.push({ from: cur.id, to: to.id, e });
|
|
1820
1871
|
const { partial, lo, hi, vague, ctx } = crossed;
|
|
1821
1872
|
if (partial) {
|
|
1822
|
-
const pk =
|
|
1873
|
+
const pk = atContext(to.id, ctx);
|
|
1823
1874
|
const held = partSeen.get(pk);
|
|
1824
1875
|
if (held && held.some(r => (r[2] ? vague : !vague && r[0] <= lo && hi <= r[1]))) continue;
|
|
1825
1876
|
if (held) held.push([lo, hi, vague]); else partSeen.set(pk, [[lo, hi, vague]]);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// The Db2 statement cursor: the shared token cursor, failing with Db2Syntax.
|
|
3
|
+
import { tokenCursor, split } from '../statement-cursor.mjs';
|
|
4
|
+
|
|
5
|
+
export class Db2Syntax extends Error {
|
|
6
|
+
constructor(message, tok) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'Db2Syntax';
|
|
9
|
+
this.line = tok ? tok.line : null;
|
|
10
|
+
this.col = tok ? tok.col : null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const cursor = (toks) => tokenCursor(toks, Db2Syntax);
|
|
15
|
+
export { split };
|