@portll/cobolwork 0.5.0 → 0.6.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 +43 -11
- package/bin/cobolwork.mjs +27 -8
- package/lib/baseline.mjs +9 -0
- package/lib/build.mjs +33 -8
- package/lib/capabilities.mjs +4 -1
- package/lib/consequence.mjs +4 -0
- package/lib/control-reuse.mjs +113 -0
- package/lib/control.mjs +25 -1
- package/lib/dataflow.mjs +285 -86
- package/lib/equivalence.mjs +44 -5
- package/lib/evidence/cli.mjs +13 -5
- package/lib/evidence/record.mjs +16 -6
- package/lib/evidence/seal.mjs +17 -0
- package/lib/evidence/store.mjs +30 -14
- package/lib/evidence/timestamp.mjs +83 -0
- package/lib/evidence/verify.mjs +120 -42
- package/lib/hlasm/asm/data.mjs +116 -0
- package/lib/hlasm/asm/listing.mjs +43 -0
- package/lib/hlasm/asm/output.mjs +45 -0
- package/lib/hlasm/asm/sections.mjs +159 -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 +48 -0
- package/lib/hlasm/locate.mjs +333 -0
- package/lib/hlasm/macro/authorization.mjs +126 -0
- package/lib/hlasm/macro/datasets.mjs +94 -0
- package/lib/hlasm/macro/io.mjs +139 -0
- package/lib/hlasm/macro/le.mjs +53 -0
- package/lib/hlasm/macro/linkage.mjs +143 -0
- package/lib/hlasm/macro/operator.mjs +66 -0
- package/lib/hlasm/macro/program.mjs +142 -0
- package/lib/hlasm/macro/recovery.mjs +80 -0
- package/lib/hlasm/macro/storage.mjs +117 -0
- package/lib/hlasm/macro/structured.mjs +131 -0
- package/lib/hlasm/model.mjs +72 -0
- package/lib/hlasm/operands.mjs +49 -0
- package/lib/hlasm/read.mjs +116 -0
- package/lib/inventory.mjs +10 -7
- package/lib/ironwork-ids.mjs +24 -0
- package/lib/ironwork.mjs +4 -1
- package/lib/kernel/registry.mjs +20 -17
- package/lib/kernel/shared-pass.mjs +138 -0
- package/lib/options.mjs +26 -4
- package/lib/policy.mjs +6 -0
- package/lib/precompile-cics.mjs +1 -1
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +3 -0
- package/lib/scan.mjs +21 -11
- package/lib/sets/abend.mjs +71 -7
- package/lib/sets/build.mjs +43 -37
- package/lib/sets/cics.mjs +5 -3
- package/lib/sets/compile.mjs +5 -3
- package/lib/sets/copybook.mjs +5 -3
- package/lib/sets/flow.mjs +14 -7
- package/lib/sets/hlasm.mjs +60 -4
- package/lib/sets/log.mjs +5 -3
- package/lib/sets/priv.mjs +5 -3
- package/lib/sets/secrets.mjs +110 -0
- package/lib/sets/semantics.mjs +5 -3
- package/lib/sets/web.mjs +5 -3
- package/package.json +1 -1
- package/rules/compliance-cobit2019.json +88 -4
- package/rules/compliance-dora.json +95 -4
- package/rules/compliance-ffiec.json +91 -0
- package/rules/compliance-nist80053.json +91 -0
- package/rules/gitleaks-mainframe.toml +46 -15
- package/rules/hlasm-instructions.json +2396 -0
- package/schema/cobolwork.policy.schema.json +7 -0
package/lib/dataflow.mjs
CHANGED
|
@@ -11,13 +11,23 @@ import { parseCsd, ddOfQueue } from './csd.mjs';
|
|
|
11
11
|
import { loadSite } from './site.mjs';
|
|
12
12
|
import { ssrangeAbends } from './options.mjs';
|
|
13
13
|
import { isJcl } from './sources.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import { watchMemoryBuffer } from './kernel/memory.mjs';
|
|
15
|
+
import { drive, loopOver } from './kernel/shared-pass.mjs';
|
|
16
|
+
import { keep, parseOrder, restore, sameAnalysis } from './control-reuse.mjs';
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
18
|
+
|
|
15
19
|
// A flow finding is located by source and sink rather than by one path and line, so it keeps its
|
|
16
20
|
// own comparator. The text comparison underneath it is the shared one.
|
|
17
21
|
import { byText } from './kernel/findings.mjs';
|
|
18
|
-
import { buildControl, creditOf, hasFact, topOf, within, wholeTop } from './control.mjs';
|
|
22
|
+
import { buildControl, creditOf, hasFact, sparseFacts, topOf, within, wholeTop } from './control.mjs';
|
|
19
23
|
import { directoryTree } from './kernel/source-tree.mjs';
|
|
20
24
|
|
|
25
|
+
// Every reused control analysis checked against the program's own: for the equivalence runs and tests.
|
|
26
|
+
const verifyReuse = () => process.env.COBOLWORK_VERIFY_REUSE === '1';
|
|
27
|
+
// The size of program text from which a copy's control analysis is restored rather than redone. On
|
|
28
|
+
// one repository of 14,500 copied small programs restoring took longer than analysing.
|
|
29
|
+
const REUSE_MIN_BYTES = 256 * 1024;
|
|
30
|
+
|
|
21
31
|
const OS_COMMAND_ROUTINE = /^(SYSTEM|C\$SYSTEM|CBL_EXEC_RUN_UNIT|CBL_GC_HOSTED|BXPSYSTM)$/i;
|
|
22
32
|
|
|
23
33
|
export const SOURCE_KINDS = {
|
|
@@ -73,6 +83,58 @@ const computes = (st) => ARITHMETIC.has(st.verb) || st.counts === true || (st.fn
|
|
|
73
83
|
const computedOnRoute = (state) => { for (let s = state; s; s = s.prev) if (s.why && s.why.computes) return true; return false; };
|
|
74
84
|
const typedOnRoute = (state) => { for (let s = state; s; s = s.prev) if (s.node.typed) return true; return false; };
|
|
75
85
|
|
|
86
|
+
// Most nodes are never a source or a sink. They share one empty list, frozen, so that adding to it
|
|
87
|
+
// fails rather than reaching every node.
|
|
88
|
+
const NO_ENDS = Object.freeze([]);
|
|
89
|
+
const addSource = (n, x) => { if (n.sources === NO_ENDS) n.sources = []; n.sources.push(x); };
|
|
90
|
+
const addSink = (n, x) => { if (n.sinks === NO_ENDS) n.sinks = []; n.sinks.push(x); };
|
|
91
|
+
|
|
92
|
+
// What a check's condition says, as text that differs whenever anything a reader could observe
|
|
93
|
+
// differs: property order, a set's order, undefined, -0 and NaN. Null for anything but plain data.
|
|
94
|
+
function consKey(v) {
|
|
95
|
+
if (v === undefined) return 'u';
|
|
96
|
+
if (v === null) return 'n';
|
|
97
|
+
if (typeof v === 'number') return Object.is(v, -0) ? '#-0' : `#${v}`;
|
|
98
|
+
if (typeof v === 'string') return JSON.stringify(v);
|
|
99
|
+
if (typeof v === 'boolean') return v ? 't' : 'f';
|
|
100
|
+
if (typeof v !== 'object') return null;
|
|
101
|
+
const parts = v instanceof Set ? [...v] : Array.isArray(v) ? v : Object.getPrototypeOf(v) === Object.prototype ? null : undefined;
|
|
102
|
+
if (parts === undefined) return null;
|
|
103
|
+
const keys = parts ? parts.map(consKey) : Object.keys(v).map((k) => { const s = consKey(v[k]); return s === null ? null : `${JSON.stringify(k)}:${s}`; });
|
|
104
|
+
if (keys.includes(null)) return null;
|
|
105
|
+
return `${v instanceof Set ? 'S' : parts ? 'A' : 'O'}[${keys.join(',')}]`;
|
|
106
|
+
}
|
|
107
|
+
|
|
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
|
+
|
|
76
138
|
// Every literal a program holds that could be a program name: its VALUE clauses and the literals its
|
|
77
139
|
// statements and commands use. A menu that XCTLs through a table of names starts every one of them.
|
|
78
140
|
function literalNames(prog) {
|
|
@@ -174,7 +236,7 @@ function execOptions(exec) {
|
|
|
174
236
|
return { opts, words };
|
|
175
237
|
}
|
|
176
238
|
|
|
177
|
-
export function
|
|
239
|
+
export function* analyzeSteps(root, opts = {}) {
|
|
178
240
|
const repos = opts.repos || [''];
|
|
179
241
|
const stats = { files: 0, programs: 0, edges: 0, nodes: 0, threw: 0, overBudget: 0, unreadable: 0, ebcdic: 0,
|
|
180
242
|
jclFiles: 0, jclSteps: 0, jclStepsResolved: 0, jclCrossings: 0,
|
|
@@ -221,9 +283,33 @@ export function analyze(root, opts = {}) {
|
|
|
221
283
|
// passes an argument, the PUT or GET of a container.
|
|
222
284
|
// `link` marks an edge into a callee at a call site ({ call }) or back out to it ({ ret }), and
|
|
223
285
|
// `stmt` names the statement that carries an edge whose `why` is text.
|
|
286
|
+
// Edges are kept as columns and laid out by source node before the walk. Most are a group's, a
|
|
287
|
+
// REDEFINES' or a RENAMES', and those share one record of why they exist.
|
|
288
|
+
const edgeFrom = [];
|
|
289
|
+
const edgeTo = [];
|
|
290
|
+
const edgeMeta = [];
|
|
291
|
+
const sharedMeta = new Map();
|
|
292
|
+
// Checks share a condition with the same content: 107,547 conditions of 66 Unieuro programs had
|
|
293
|
+
// 2,423 contents. Conditions are read and never changed once the check model has made them.
|
|
294
|
+
const conditions = new Map();
|
|
295
|
+
const sameCondition = (c) => {
|
|
296
|
+
const k = c ? consKey(c) : null;
|
|
297
|
+
if (k === null) return c;
|
|
298
|
+
let kept = conditions.get(k);
|
|
299
|
+
if (!kept) conditions.set(k, kept = c);
|
|
300
|
+
return kept;
|
|
301
|
+
};
|
|
224
302
|
const edge = (a, b, why, dir, at, link) => {
|
|
225
303
|
if (!a || !b || a === b) return;
|
|
226
|
-
|
|
304
|
+
let meta;
|
|
305
|
+
if (typeof why === 'string' && at == null && !link) {
|
|
306
|
+
const k = `${why}|${dir || ''}`;
|
|
307
|
+
meta = sharedMeta.get(k);
|
|
308
|
+
if (!meta) sharedMeta.set(k, meta = { why, dir });
|
|
309
|
+
} else meta = { why, dir, ...(at != null ? { at } : {}), ...link };
|
|
310
|
+
edgeFrom.push(a.id);
|
|
311
|
+
edgeTo.push(b.id);
|
|
312
|
+
edgeMeta.push(meta);
|
|
227
313
|
stats.edges++;
|
|
228
314
|
};
|
|
229
315
|
let callSites = 0;
|
|
@@ -231,7 +317,52 @@ export function analyze(root, opts = {}) {
|
|
|
231
317
|
// Everything the graph needs from one program is taken while its parse tree is in hand, and the
|
|
232
318
|
// tree is then released. Holding every tree until the end is what exhausted a 4 GB heap on an
|
|
233
319
|
// 8,024-file repository; nodes, edges and a few cross-program sites are all that survive.
|
|
234
|
-
|
|
320
|
+
// A flow node holds what differs between nodes. Its program and file come from the program it
|
|
321
|
+
// belongs to, and every other field from the class until the node sets its own.
|
|
322
|
+
const programOf = [];
|
|
323
|
+
class FlowNode {
|
|
324
|
+
constructor(id, pk, name) { this.id = id; this.pk = pk; this.name = name; }
|
|
325
|
+
get program() { return programOf[this.pk].id; }
|
|
326
|
+
get file() { return programOf[this.pk].file; }
|
|
327
|
+
}
|
|
328
|
+
Object.assign(FlowNode.prototype, { off: null, size: null, repeats: false, sources: NO_ENDS, sinks: NO_ENDS });
|
|
329
|
+
|
|
330
|
+
// A program whose text another program file repeats has its control analysis kept for the copies,
|
|
331
|
+
// keyed by its place in its file and by what each copybook it resolved holds (lib/control-reuse.mjs).
|
|
332
|
+
// Only a large one: below REUSE_MIN_BYTES (opts.reuseMinBytes) keeping and restoring costs more
|
|
333
|
+
// than analysing again.
|
|
334
|
+
const reuseMin = opts.reuseMinBytes ?? REUSE_MIN_BYTES;
|
|
335
|
+
const textKey = (src) => createHash('sha1').update(src).digest('base64');
|
|
336
|
+
function repeatedTexts(tree, files) {
|
|
337
|
+
const count = new Map();
|
|
338
|
+
for (const f of files) {
|
|
339
|
+
let src;
|
|
340
|
+
try { src = tree.text(f).text; } catch { continue; }
|
|
341
|
+
if (src.length < reuseMin) continue;
|
|
342
|
+
const k = textKey(src);
|
|
343
|
+
count.set(k, (count.get(k) || 0) + 1);
|
|
344
|
+
}
|
|
345
|
+
const out = new Map();
|
|
346
|
+
for (const [k, n] of count) if (n > 1) out.set(k, { text: k, left: n, kept: new Map() });
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
const copyText = new Map();
|
|
350
|
+
const copyKey = (r, tree) => r.copies.map((c) => {
|
|
351
|
+
if (!c.path) return `${c.status}:${c.name}`;
|
|
352
|
+
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
|
+
return `${c.status}:${copyText.get(c.path)}`;
|
|
354
|
+
}).join('|');
|
|
355
|
+
const reuseOf = (copied, k, r, f, tree) => ({ kept: copied.kept, key: `${k}|${copyKey(r, tree)}`, files: [f, ...r.copies.filter((c) => c.path).map((c) => c.path)] });
|
|
356
|
+
// A reused analysis checked against the program's own, where COBOLWORK_VERIFY_REUSE asks.
|
|
357
|
+
function sameAsOwn(ctl, prog, resolver, file) {
|
|
358
|
+
let own = null;
|
|
359
|
+
try { own = buildControl(prog, resolver); } catch { /* the kept one threw too, or it differs */ }
|
|
360
|
+
if (own) internFacts(own.facts);
|
|
361
|
+
if (!own !== !ctl || (own && !sameAnalysis(own, ctl, new Set(parseOrder(prog))))) throw new Error(`the control analysis reused for ${file} differs from its own`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function summarise(prog, file, pk, options = [], reuse = null) {
|
|
365
|
+
programOf[pk] = { id: prog.id, file };
|
|
235
366
|
const byItem = new Map();
|
|
236
367
|
const byName = new Map();
|
|
237
368
|
for (const it of prog.items) if (!byName.has(it.name)) byName.set(it.name, it);
|
|
@@ -245,7 +376,11 @@ export function analyze(root, opts = {}) {
|
|
|
245
376
|
for (let a = item; a; a = a.parent) if ((a.occurs || 1) > 1) { repeats = true; table = a; }
|
|
246
377
|
let top = item;
|
|
247
378
|
while (top.parent) top = top.parent;
|
|
248
|
-
n =
|
|
379
|
+
n = new FlowNode(nodes.length, pk, item.name);
|
|
380
|
+
n.off = item.offset ?? null;
|
|
381
|
+
n.size = item.size ?? null;
|
|
382
|
+
if (repeats) n.repeats = true;
|
|
383
|
+
if (top.section === 'LINKAGE') n.linkage = true;
|
|
249
384
|
// Where the outermost table holding this item sits in the record: every occurrence of the
|
|
250
385
|
// item is somewhere inside it. A group's size already counts its occurrences; an
|
|
251
386
|
// elementary item's is one element.
|
|
@@ -267,7 +402,7 @@ export function analyze(root, opts = {}) {
|
|
|
267
402
|
const indexNode = (name) => {
|
|
268
403
|
let n = indexNodes.get(name);
|
|
269
404
|
if (!n) {
|
|
270
|
-
n =
|
|
405
|
+
n = new FlowNode(nodes.length, pk, name);
|
|
271
406
|
nodes.push(n);
|
|
272
407
|
indexNodes.set(name, n);
|
|
273
408
|
stats.nodes++;
|
|
@@ -281,12 +416,12 @@ export function analyze(root, opts = {}) {
|
|
|
281
416
|
// constraints, so only SQLERRMC reaches the screen sink.
|
|
282
417
|
const systemSource = (n, detail, at) => {
|
|
283
418
|
const onlyTo = /^SQLERRMC\b/.test(detail) ? [...TO_CLIENT, 'screen'] : TO_CLIENT;
|
|
284
|
-
if (!n.sources.some((x) => x.kind === 'system-response')) n
|
|
419
|
+
if (!n.sources.some((x) => x.kind === 'system-response')) addSource(n, { kind: 'system-response', onlyTo, noCredit: true, ...at, detail });
|
|
285
420
|
};
|
|
286
421
|
const systemNode = (tok) => {
|
|
287
422
|
let n = systemNodes.get(tok.u);
|
|
288
423
|
if (!n) {
|
|
289
|
-
n =
|
|
424
|
+
n = new FlowNode(nodes.length, pk, tok.u);
|
|
290
425
|
nodes.push(n);
|
|
291
426
|
systemNodes.set(tok.u, n);
|
|
292
427
|
stats.nodes++;
|
|
@@ -304,9 +439,20 @@ export function analyze(root, opts = {}) {
|
|
|
304
439
|
// The program's statement order, where its structure could be read. Without it a check is
|
|
305
440
|
// credited as it always was, wherever its field is used, and never clears anything.
|
|
306
441
|
let ctl = null;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
442
|
+
const resolver = (tok) => itemOfToken(tok) || (tok && tok.t === 'word' && indexNames.has(tok.u) ? { index: tok.u } : null);
|
|
443
|
+
const kept = reuse && reuse.kept.get(reuse.key);
|
|
444
|
+
if (kept) {
|
|
445
|
+
ctl = kept.threw ? null : restore(kept.value, parseOrder(prog), reuse.files);
|
|
446
|
+
if (!ctl) stats.programsUnordered++;
|
|
447
|
+
if (verifyReuse()) sameAsOwn(ctl, prog, resolver, file);
|
|
448
|
+
} else {
|
|
449
|
+
try { ctl = buildControl(prog, resolver); } catch { stats.programsUnordered++; }
|
|
450
|
+
if (ctl) internFacts(ctl.facts);
|
|
451
|
+
if (reuse) {
|
|
452
|
+
const value = ctl ? keep(ctl, parseOrder(prog), reuse.files) : null;
|
|
453
|
+
if (!ctl || value) reuse.kept.set(reuse.key, ctl ? { value } : { threw: true });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
310
456
|
const pointOf = (x) => (ctl ? ctl.nodeOf.get(x) : undefined);
|
|
311
457
|
// Where a name inside a condition is read, which can know more than the statement does.
|
|
312
458
|
const placeOf = (tok, st) => (ctl && ctl.posOf.get(tok)) ?? pointOf(st);
|
|
@@ -323,7 +469,7 @@ export function analyze(root, opts = {}) {
|
|
|
323
469
|
const device = upon >= 0 && st.sources[upon + 1] ? ` UPON ${st.sources[upon + 1].u}` : '';
|
|
324
470
|
for (const t of upon >= 0 ? st.sources.slice(0, upon) : st.sources) {
|
|
325
471
|
const n = nodeOfToken(t);
|
|
326
|
-
if (n) n
|
|
472
|
+
if (n) addSink(n, { kind: 'log', onlyFrom: REFLECTED, file: st.file, line: st.line, point: pointOf(st), detail: `DISPLAY ${n.name}${device}` });
|
|
327
473
|
}
|
|
328
474
|
}
|
|
329
475
|
// The document XML PARSE reads is the operand after PARSE; what the parser does with a DTD in
|
|
@@ -331,7 +477,7 @@ export function analyze(root, opts = {}) {
|
|
|
331
477
|
for (const st of prog.statements) {
|
|
332
478
|
if (st.verb !== 'XML' || !st.sources[0] || st.sources[0].u !== 'PARSE') continue;
|
|
333
479
|
const n = st.sources[1] ? nodeOfToken(st.sources[1]) : null;
|
|
334
|
-
if (n) n
|
|
480
|
+
if (n) addSink(n, { kind: 'xml-document', onlyFrom: REFLECTED, file: st.file, line: st.line, point: pointOf(st), detail: `XML PARSE ${n.name}` });
|
|
335
481
|
}
|
|
336
482
|
for (const it of prog.items) {
|
|
337
483
|
const n = nodeOf(it);
|
|
@@ -367,7 +513,7 @@ export function analyze(root, opts = {}) {
|
|
|
367
513
|
for (const it of prog.items) { const top = recordOf(it); if (!byRecord.has(top)) byRecord.set(top, []); byRecord.get(top).push(it); }
|
|
368
514
|
const extent = (it) => (it.offset != null && it.size != null ? [it.offset, it.offset + (it.contributes || it.size)] : null);
|
|
369
515
|
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 } : {}) };
|
|
516
|
+
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
517
|
if (c.field.index) { (indexNode(c.field.index).checks ||= []).push(entry); continue; }
|
|
372
518
|
const span = extent(c.field);
|
|
373
519
|
for (const it of byRecord.get(recordOf(c.field)) || [c.field]) {
|
|
@@ -455,7 +601,7 @@ export function analyze(root, opts = {}) {
|
|
|
455
601
|
if (indexed.has(use)) continue;
|
|
456
602
|
indexed.add(use);
|
|
457
603
|
const start = kind === 'reference-modification' && x.from && base != null ? startOf(x.from, idx, base) : null;
|
|
458
|
-
at
|
|
604
|
+
addSink(at, {
|
|
459
605
|
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
606
|
detail: kind === 'subscript'
|
|
461
607
|
? `${at.name} subscripts ${host.name}, a table of ${table.occurs}`
|
|
@@ -486,7 +632,7 @@ export function analyze(root, opts = {}) {
|
|
|
486
632
|
const use = `${key}|${pointOf(st) ?? `s${st.at}`}`;
|
|
487
633
|
if (indexed.has(use)) continue;
|
|
488
634
|
indexed.add(use);
|
|
489
|
-
nodeOf(it)
|
|
635
|
+
addSink(nodeOf(it), {
|
|
490
636
|
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
637
|
detail: `${it.name} decides where PERFORM VARYING ${counter} stops, and ${counter} subscripts ${walks.host.name}, a table of ${walks.table.occurs}`,
|
|
492
638
|
});
|
|
@@ -507,7 +653,7 @@ export function analyze(root, opts = {}) {
|
|
|
507
653
|
const use = `arithmetic|${n.id}|${pointOf(st) ?? `s${st.at}`}`;
|
|
508
654
|
if (indexed.has(use)) continue;
|
|
509
655
|
indexed.add(use);
|
|
510
|
-
n
|
|
656
|
+
addSink(n, {
|
|
511
657
|
kind: 'arithmetic', onlyFrom: FROM_OUTSIDE, file: st.file, line: st.line, point: pointOf(st), group: `${pk}|arithmetic|${n.id}`,
|
|
512
658
|
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
659
|
});
|
|
@@ -529,7 +675,7 @@ export function analyze(root, opts = {}) {
|
|
|
529
675
|
const sts = touching.get(it);
|
|
530
676
|
for (const st of sts.length ? sts : [null]) {
|
|
531
677
|
const point = st ? pointOf(st) : undefined;
|
|
532
|
-
nodeOf(count)
|
|
678
|
+
addSink(nodeOf(count), {
|
|
533
679
|
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
680
|
detail: `${count.name} sets how many ${it.name} entries there are, up to ${it.occurs}`,
|
|
535
681
|
});
|
|
@@ -539,18 +685,18 @@ export function analyze(root, opts = {}) {
|
|
|
539
685
|
if (st.verb !== 'READ' && st.verb !== 'RETURN') continue;
|
|
540
686
|
for (const t of st.targets) {
|
|
541
687
|
const n = nodeOfToken(t);
|
|
542
|
-
if (n) n
|
|
688
|
+
if (n) addSource(n, { kind: 'file-record', file: st.file, line: st.line, detail: `${st.verb} ... INTO` });
|
|
543
689
|
}
|
|
544
690
|
}
|
|
545
691
|
for (const fd of prog.fds || []) for (const rec of fd.records || []) {
|
|
546
692
|
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)
|
|
693
|
+
if (readsFile) addSource(nodeOf(rec), { kind: 'file-record', file: fd.file, line: fd.line, detail: `record of file ${fd.name}` });
|
|
548
694
|
}
|
|
549
695
|
for (const a of prog.accepts) {
|
|
550
696
|
const item = (a.targetTok ? itemOfToken(a.targetTok) : null) || byName.get(a.target);
|
|
551
697
|
if (!item) continue;
|
|
552
698
|
const kind = /^(COMMAND-LINE|ARGUMENT-VALUE|ENVIRONMENT|ENVIRONMENT-VALUE)$/.test(a.from || '') ? 'argv-or-env' : null;
|
|
553
|
-
if (kind) nodeOf(item)
|
|
699
|
+
if (kind) addSource(nodeOf(item), { kind, file: a.file, line: a.line, detail: `ACCEPT ... FROM ${a.from}` });
|
|
554
700
|
}
|
|
555
701
|
// A SELECT assigns either to a data item, which makes the path dynamic, or to an external
|
|
556
702
|
// name. On z/OS that external name is the DD name a JCL step fills, and it is how a job's
|
|
@@ -561,7 +707,7 @@ export function analyze(root, opts = {}) {
|
|
|
561
707
|
if (f.assign.t === 'word') {
|
|
562
708
|
const item = byName.get(f.assign.v);
|
|
563
709
|
if (item) {
|
|
564
|
-
nodeOf(item)
|
|
710
|
+
addSink(nodeOf(item), { kind: 'dynamic-file-path', file: f.file, line: f.line, detail: `SELECT ${f.name} ASSIGN TO ${f.assign.v}` });
|
|
565
711
|
continue; // a variable path is not a DD name
|
|
566
712
|
}
|
|
567
713
|
}
|
|
@@ -570,7 +716,9 @@ export function analyze(root, opts = {}) {
|
|
|
570
716
|
if (!external) continue;
|
|
571
717
|
const fd = (prog.fds || []).find((x) => x.name === f.name);
|
|
572
718
|
const recs = (fd?.records || []).map(nodeOf);
|
|
573
|
-
|
|
719
|
+
// READ ... INTO moves the record it read into an item, which then holds the DD's data too.
|
|
720
|
+
const into = prog.statements.filter((st) => st.verb === 'READ' && st.sources[0]?.u === f.name).flatMap((st) => st.targets.map(nodeOfToken).filter(Boolean));
|
|
721
|
+
if (recs.length) ddFiles.push({ dd: external, nodes: recs, into, file: f.file, line: f.line, select: f.name });
|
|
574
722
|
}
|
|
575
723
|
const topOf = (it) => { let t = it; while (t && t.parent) t = t.parent; return t; };
|
|
576
724
|
const ownParams = new Set((prog.paramTokens || []).map((pt) => topOf(itemOfToken(pt.tok))).filter(Boolean));
|
|
@@ -593,14 +741,14 @@ export function analyze(root, opts = {}) {
|
|
|
593
741
|
const point = pointOf(prog.statements[c.stmtIndex]);
|
|
594
742
|
if (c.kind === 'I') {
|
|
595
743
|
const n = nodeOfToken(c.targetTok);
|
|
596
|
-
if (n) n
|
|
744
|
+
if (n) addSink(n, { kind: 'dynamic-program-load', file: c.file, line: c.line, point, detail: `CALL ${c.name}` });
|
|
597
745
|
continue;
|
|
598
746
|
}
|
|
599
747
|
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
748
|
if (OS_COMMAND_ROUTINE.test(c.name)) {
|
|
601
749
|
for (const a of c.using) {
|
|
602
750
|
const n = a.tok ? nodeOfToken(a.tok) : null;
|
|
603
|
-
if (n) n
|
|
751
|
+
if (n) addSink(n, { kind: 'os-command', file: c.file, line: c.line, point, detail: `CALL '${c.name}' USING ${a.word}` });
|
|
604
752
|
}
|
|
605
753
|
} else if (SOCKET_ROUTINE.test(c.name)) {
|
|
606
754
|
// IBM's own examples hold the function code in a field: 01 SOC-FUNCTION PIC X(16) VALUE 'SEND'.
|
|
@@ -611,23 +759,23 @@ export function analyze(root, opts = {}) {
|
|
|
611
759
|
if (fn && SOCKET_SEND.test(String(fn).trim())) {
|
|
612
760
|
for (const a of c.using.slice(1)) {
|
|
613
761
|
const n = a.tok ? nodeOfToken(a.tok) : null;
|
|
614
|
-
if (n) n
|
|
762
|
+
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
763
|
}
|
|
616
764
|
}
|
|
617
765
|
} else if (Object.hasOwn(MQ_BUFFER_ARG, String(c.name).toUpperCase())) {
|
|
618
766
|
const a = c.using[MQ_BUFFER_ARG[String(c.name).toUpperCase()]];
|
|
619
767
|
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
620
|
-
if (n) n
|
|
768
|
+
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
769
|
} else if (/^MQCONNX?$/i.test(String(c.name))) {
|
|
622
770
|
// MQCONN(QMgrName, Hconn, CompCode, Reason); MQCONNX puts its options second.
|
|
623
771
|
const a = c.using[0];
|
|
624
772
|
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
625
|
-
if (n) n
|
|
773
|
+
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` });
|
|
626
774
|
} else if (String(c.name).toUpperCase() === 'CEEGTST') {
|
|
627
775
|
// CEEGTST(heap id, size, address, feedback code): the size is the second argument.
|
|
628
776
|
const a = c.using[1];
|
|
629
777
|
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
630
|
-
if (n) n
|
|
778
|
+
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
779
|
}
|
|
632
780
|
}
|
|
633
781
|
const transfers = [];
|
|
@@ -643,7 +791,10 @@ export function analyze(root, opts = {}) {
|
|
|
643
791
|
const containerNode = (name) => {
|
|
644
792
|
let n = containers.get(name);
|
|
645
793
|
if (!n) {
|
|
646
|
-
n =
|
|
794
|
+
n = new FlowNode(nodes.length, pk, `CONTAINER(${name})`);
|
|
795
|
+
n.off = undefined;
|
|
796
|
+
n.size = undefined;
|
|
797
|
+
n.repeats = undefined;
|
|
647
798
|
nodes.push(n);
|
|
648
799
|
containers.set(name, n);
|
|
649
800
|
stats.nodes++;
|
|
@@ -731,7 +882,7 @@ export function analyze(root, opts = {}) {
|
|
|
731
882
|
if (typed) continue;
|
|
732
883
|
stats.protectedFields++;
|
|
733
884
|
const back = field.effective.has('FSET') ? ' with FSET' : '';
|
|
734
|
-
n
|
|
885
|
+
addSource(n, { kind: 'cics-protected-field', onlyTo: ['record-key', 'record-update'], ...at,
|
|
735
886
|
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
887
|
}
|
|
737
888
|
};
|
|
@@ -758,17 +909,17 @@ export function analyze(root, opts = {}) {
|
|
|
758
909
|
if (verb === 'WRITE' && ['OPERATOR', 'JOURNALNAME', 'JOURNALNUM'].includes(words[1])) {
|
|
759
910
|
const opt = words[1] === 'OPERATOR' ? 'TEXT' : 'FROM';
|
|
760
911
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
761
|
-
if (n) n
|
|
912
|
+
if (n) addSink(n, { kind: 'log', onlyFrom: REFLECTED, ...at, detail: `EXEC CICS WRITE ${words[1]} ${opt}(${n.name}), ${words[1] === 'OPERATOR' ? 'to the operator console' : 'to a journal'}` });
|
|
762
913
|
}
|
|
763
914
|
if (verb === 'RECEIVE') {
|
|
764
915
|
const isWeb = words.includes('WEB');
|
|
765
916
|
const n = nodeOfToken(intoTok);
|
|
766
|
-
if (n) n
|
|
917
|
+
if (n) addSource(n, { kind: isWeb ? 'cics-web' : 'cics-terminal', ...at, detail: `EXEC CICS ${isWeb ? 'WEB ' : ''}RECEIVE` });
|
|
767
918
|
// RECEIVE MAP with no INTO fills the symbolic map, whose input record is <map>I.
|
|
768
919
|
const map = optionName(o, 'MAP');
|
|
769
920
|
if (!n && map) {
|
|
770
921
|
const sym = byName.get(`${map}I`);
|
|
771
|
-
if (sym) nodeOf(sym)
|
|
922
|
+
if (sym) addSource(nodeOf(sym), { kind: 'cics-terminal', ...at, detail: `EXEC CICS RECEIVE MAP(${map}) into the symbolic map` });
|
|
772
923
|
}
|
|
773
924
|
if (!isWeb && o.has('MAP')) receiveMap(e, o, itemOfToken(intoTok), at);
|
|
774
925
|
}
|
|
@@ -778,7 +929,7 @@ export function analyze(root, opts = {}) {
|
|
|
778
929
|
const map = words[1] === 'MAP' ? optionName(o, 'MAP') : null;
|
|
779
930
|
const sym = !from && map ? byName.get(`${map}O`) : null;
|
|
780
931
|
const n = from || (sym ? nodeOf(sym) : null);
|
|
781
|
-
if (n) n
|
|
932
|
+
if (n) addSink(n, { kind: 'screen', onlyFrom: ['system-response'], ...at, detail: `EXEC CICS SEND ${words[1]}${map ? `(${map})` : ''}${from ? ` FROM(${n.name})` : ', from its symbolic map'}, to the terminal` });
|
|
782
933
|
}
|
|
783
934
|
// RIDFLD decides which record a READ or DELETE reaches, or where a browse starts. A key the
|
|
784
935
|
// user types is how a lookup works; a key the program put in a protected field and read back
|
|
@@ -790,12 +941,12 @@ export function analyze(root, opts = {}) {
|
|
|
790
941
|
const detail = verb === 'DELETE' ? `EXEC CICS DELETE RIDFLD(${k && k.name}), which decides which record it deletes`
|
|
791
942
|
: then ? `EXEC CICS READ UPDATE RIDFLD(${k && k.name}), which decides which record the program then ${then}`
|
|
792
943
|
: `EXEC CICS ${verb} RIDFLD(${k && k.name}), which decides which record it reaches`;
|
|
793
|
-
if (k) k
|
|
944
|
+
if (k) addSink(k, { kind: verb === 'DELETE' || then ? 'record-update' : 'record-key', onlyFrom: ['cics-protected-field'], noCredit: true, ...at, detail });
|
|
794
945
|
}
|
|
795
946
|
if (verb === 'GETMAIN') {
|
|
796
947
|
for (const opt of ['FLENGTH', 'LENGTH']) {
|
|
797
948
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
798
|
-
if (n) n
|
|
949
|
+
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
950
|
}
|
|
800
951
|
}
|
|
801
952
|
if (verb === 'PUT' && o.has('CONTAINER')) {
|
|
@@ -809,7 +960,7 @@ export function analyze(root, opts = {}) {
|
|
|
809
960
|
invokes.push({ at, what: words[1], channel: optionName(o, 'CHANNEL') });
|
|
810
961
|
for (const opt of ['URI', 'URIMAP']) {
|
|
811
962
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
812
|
-
if (n) n
|
|
963
|
+
if (n) addSink(n, { kind: 'outbound-host', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS INVOKE ${words[1]} ${opt}(...), which decides where the request goes` });
|
|
813
964
|
}
|
|
814
965
|
}
|
|
815
966
|
if (verb === 'GET' && o.has('CONTAINER')) {
|
|
@@ -821,10 +972,10 @@ export function analyze(root, opts = {}) {
|
|
|
821
972
|
// plain WEB SEND answers the request that started the program, which is its job.
|
|
822
973
|
if (verb === 'WEB' && (words[1] === 'CONVERSE' || (words[1] === 'SEND' && o.has('SESSTOKEN')))) {
|
|
823
974
|
const n = nodeOfToken((o.get('FROM') || [])[0]);
|
|
824
|
-
if (n) n
|
|
975
|
+
if (n) addSink(n, { kind: 'outbound-http', onlyFrom: DATA_AT_REST, ...at, detail: `EXEC CICS WEB ${words[1]} to a remote server` });
|
|
825
976
|
// The request may be a container rather than a field.
|
|
826
977
|
const sent = o.has('CONTAINER') ? optionName(o, 'CONTAINER') : null;
|
|
827
|
-
if (sent) containerNode(sent)
|
|
978
|
+
if (sent) addSink(containerNode(sent), { kind: 'outbound-http', onlyFrom: DATA_AT_REST, ...at, detail: `EXEC CICS WEB ${words[1]} sending CONTAINER(${sent})` });
|
|
828
979
|
}
|
|
829
980
|
// The response this program returns to its own caller, and the document built for it. CICS
|
|
830
981
|
// escapes nothing on the way out - DOCUMENT SET's UNESCAPED option is about URL decoding,
|
|
@@ -836,25 +987,25 @@ export function analyze(root, opts = {}) {
|
|
|
836
987
|
// defect whatever the reply holds.
|
|
837
988
|
const media = (o.get('MEDIATYPE') || [])[0];
|
|
838
989
|
const markup = media && media.t === 'lit' && /html|xml|svg/i.test(String(media.v));
|
|
839
|
-
if (n) n
|
|
990
|
+
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
991
|
}
|
|
841
992
|
if (verb === 'DOCUMENT') {
|
|
842
993
|
const what = words[1] === 'SET' ? ['VALUE', 'SYMBOLLIST'] : ['TEXT', 'FROM', 'BINARY'];
|
|
843
994
|
for (const opt of what) {
|
|
844
995
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
845
|
-
if (n) n
|
|
996
|
+
if (n) addSink(n, { kind: 'web-response', onlyFrom: [...IN_CICS, 'system-response'], ...at, detail: `EXEC CICS DOCUMENT ${words[1]} ${opt}(...), which is sent as the response` });
|
|
846
997
|
}
|
|
847
998
|
}
|
|
848
999
|
if (verb === 'WEB' && words[1] === 'WRITE' && o.has('HTTPHEADER')) {
|
|
849
1000
|
for (const opt of ['HTTPHEADER', 'VALUE']) {
|
|
850
1001
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
851
|
-
if (n) n
|
|
1002
|
+
if (n) addSink(n, { kind: 'http-header', onlyFrom: [...IN_CICS, 'system-response'], ...at, detail: `EXEC CICS WEB WRITE HTTPHEADER ${opt}(...)` });
|
|
852
1003
|
}
|
|
853
1004
|
}
|
|
854
1005
|
if (verb === 'WEB' && (words[1] === 'OPEN' || words[1] === 'CONVERSE')) {
|
|
855
1006
|
for (const opt of ['HOST', 'URIMAP', 'PATH']) {
|
|
856
1007
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
857
|
-
if (n) n
|
|
1008
|
+
if (n) addSink(n, { kind: 'outbound-host', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS WEB ${words[1]} ${opt}(...), which decides where the request goes` });
|
|
858
1009
|
}
|
|
859
1010
|
}
|
|
860
1011
|
// The queue a command acts on, as distinct from what it writes there: a name from input
|
|
@@ -863,7 +1014,7 @@ export function analyze(root, opts = {}) {
|
|
|
863
1014
|
const ts = words[1] === 'TS' || !words.includes('TD');
|
|
864
1015
|
for (const opt of ['QUEUE', 'QNAME']) {
|
|
865
1016
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
866
|
-
if (n) n
|
|
1017
|
+
if (n) addSink(n, { kind: 'queue-name', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS ${verb} ${ts ? 'TS' : 'TD'} ${opt}(...) names the queue` });
|
|
867
1018
|
}
|
|
868
1019
|
}
|
|
869
1020
|
// SYSID ships a command to the region it names; SET's first option names the resource a
|
|
@@ -871,20 +1022,20 @@ export function analyze(root, opts = {}) {
|
|
|
871
1022
|
// INQUIRE and EXTRACT return SYSID into the field rather than read it.
|
|
872
1023
|
if (o.has('SYSID') && !['ASSIGN', 'INQUIRE', 'EXTRACT'].includes(verb)) {
|
|
873
1024
|
const n = nodeOfToken((o.get('SYSID') || [])[0]);
|
|
874
|
-
if (n) n
|
|
1025
|
+
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
1026
|
}
|
|
876
1027
|
if (verb === 'SET' && words[1] && o.has(words[1])) {
|
|
877
1028
|
const n = nodeOfToken((o.get(words[1]) || [])[0]);
|
|
878
|
-
if (n) n
|
|
1029
|
+
if (n) addSink(n, { kind: 'cics-system-resource', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS SET ${words[1]}(${n.name}), which decides the resource the command changes` });
|
|
879
1030
|
}
|
|
880
1031
|
if (verb === 'READ' || verb === 'READNEXT' || verb === 'READPREV') {
|
|
881
1032
|
const n = nodeOfToken(intoTok);
|
|
882
|
-
if (n && (o.has('FILE') || o.has('DATASET'))) n
|
|
1033
|
+
if (n && (o.has('FILE') || o.has('DATASET'))) addSource(n, { kind: 'file-record', ...at, detail: `EXEC CICS ${verb} FILE ... INTO` });
|
|
883
1034
|
}
|
|
884
1035
|
// A CONVERSE response is the remote server's data, which is untrusted input like any other.
|
|
885
1036
|
if ((verb === 'WEB' && words[1] !== 'SEND') || verb === 'EXTRACT') {
|
|
886
1037
|
const n = nodeOfToken(intoTok);
|
|
887
|
-
if (n) n
|
|
1038
|
+
if (n) addSource(n, { kind: 'cics-web', ...at, detail: `EXEC CICS ${words.slice(0, 2).join(' ')}` });
|
|
888
1039
|
}
|
|
889
1040
|
// Where a transient-data write lands is not in the program: the CSD maps the queue to a DD
|
|
890
1041
|
// and the region's JCL maps the DD to a destination. Collected here, resolved once every
|
|
@@ -901,7 +1052,7 @@ export function analyze(root, opts = {}) {
|
|
|
901
1052
|
const pgm = (o.get('PROGRAM') || o.get('TRANSID') || [])[0];
|
|
902
1053
|
if (pgm && pgm.t === 'word') {
|
|
903
1054
|
const n = nodeOfToken(pgm);
|
|
904
|
-
if (n) n
|
|
1055
|
+
if (n) addSink(n, { kind: 'cics-dynamic-transfer', ...at, detail: `EXEC CICS ${verb} with a variable program name` });
|
|
905
1056
|
}
|
|
906
1057
|
const commarea = (o.get('COMMAREA') || o.get('FROM') || [])[0];
|
|
907
1058
|
const argNode = commarea ? nodeOfToken(commarea) : null;
|
|
@@ -925,7 +1076,7 @@ export function analyze(root, opts = {}) {
|
|
|
925
1076
|
for (const h of hv) {
|
|
926
1077
|
if (!h.written) continue;
|
|
927
1078
|
const n = nodeOfToken(h.tok);
|
|
928
|
-
if (n) n
|
|
1079
|
+
if (n) addSource(n, { kind: 'database', ...at, detail: `EXEC SQL ${verb} ${verb === 'SELECT' || verb === 'FETCH' ? 'INTO' : 'writes'} host variable` });
|
|
929
1080
|
}
|
|
930
1081
|
// The location CONNECT TO or SET CONNECTION names, where a host variable holds it; what
|
|
931
1082
|
// follows USER and USING is the credential, not the target.
|
|
@@ -933,19 +1084,19 @@ export function analyze(root, opts = {}) {
|
|
|
933
1084
|
const to = e.toks.findIndex((t) => t.t === 'word' && (t.u === 'TO' || t.u === 'CONNECTION'));
|
|
934
1085
|
const colon = to >= 0 ? e.toks[to + 1] : null;
|
|
935
1086
|
const n = colon && colon.t === 'op' && colon.v === ':' && e.toks[to + 2] ? nodeOfToken(e.toks[to + 2]) : null;
|
|
936
|
-
if (n) n
|
|
1087
|
+
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
1088
|
}
|
|
938
1089
|
const dynamic = words.some((w, i) => w === 'PREPARE' || (w === 'EXECUTE' && words[i + 1] === 'IMMEDIATE'));
|
|
939
1090
|
if (dynamic) for (const h of hv) {
|
|
940
1091
|
const n = nodeOfToken(h.tok);
|
|
941
|
-
if (n) n
|
|
1092
|
+
if (n) addSink(n, { kind: 'dynamic-sql', ...at, detail: `EXEC SQL ${words.slice(0, 2).join(' ')} from a host variable` });
|
|
942
1093
|
}
|
|
943
1094
|
}
|
|
944
1095
|
}
|
|
945
1096
|
for (const inv of invokes) {
|
|
946
1097
|
for (const [name, channel] of channelOf) {
|
|
947
1098
|
if (inv.channel && channel && channel !== inv.channel) continue;
|
|
948
|
-
containerNode(name)
|
|
1099
|
+
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
1100
|
}
|
|
950
1101
|
}
|
|
951
1102
|
const commareaItem = byName.get('DFHCOMMAREA');
|
|
@@ -995,7 +1146,8 @@ export function analyze(root, opts = {}) {
|
|
|
995
1146
|
// edges are proportional to data items and statements, not to bytes, so a repository of small
|
|
996
1147
|
// programs with large record layouts can exhaust a heap the budget believes is untouched. The
|
|
997
1148
|
// shared loop watches the heap itself, and the budget stays as the cheaper first line.
|
|
998
|
-
const
|
|
1149
|
+
const repeated = repeatedTexts(tree, files);
|
|
1150
|
+
const run = yield loopOver(files, (f) => {
|
|
999
1151
|
let src;
|
|
1000
1152
|
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
1153
|
if (!/PROCEDURE\s+DIVISION|PROGRAM-ID/i.test(src)) return 0;
|
|
@@ -1006,7 +1158,12 @@ export function analyze(root, opts = {}) {
|
|
|
1006
1158
|
r = tree.parse(f, src);
|
|
1007
1159
|
} catch (e) { stats.threw++; unparsed.push(`${tree.rel(f)}: ${e.code || e.name}`); return src.length; }
|
|
1008
1160
|
stats.files++;
|
|
1009
|
-
|
|
1161
|
+
const copied = src.length >= reuseMin && repeated.size ? repeated.get(textKey(src)) : undefined;
|
|
1162
|
+
r.programs.forEach((p, k) => {
|
|
1163
|
+
programs.push(summarise(p, f, programs.length, r.options, copied ? reuseOf(copied, k, r, f, tree) : null));
|
|
1164
|
+
stats.programs++;
|
|
1165
|
+
});
|
|
1166
|
+
if (copied && --copied.left === 0) repeated.delete(copied.text);
|
|
1010
1167
|
return src.length;
|
|
1011
1168
|
}, { label: 'flow', watcher: memoryWatcher });
|
|
1012
1169
|
|
|
@@ -1142,7 +1299,7 @@ export function analyze(root, opts = {}) {
|
|
|
1142
1299
|
// followed by the text. A program with no USING cannot receive one, and saying it does
|
|
1143
1300
|
// would be a path nobody could follow.
|
|
1144
1301
|
if (run.parm !== null && callee.params && callee.params[0] && callee.params[0].node) {
|
|
1145
|
-
callee.params[0].node
|
|
1302
|
+
addSource(callee.params[0].node, {
|
|
1146
1303
|
kind: 'jcl-parm', file: f, line: run.line,
|
|
1147
1304
|
detail: `${run.how}, which runs ${run.pgm}${amb}`,
|
|
1148
1305
|
});
|
|
@@ -1159,7 +1316,7 @@ export function analyze(root, opts = {}) {
|
|
|
1159
1316
|
for (const m of callee.ddFiles || []) {
|
|
1160
1317
|
if (m.dd !== dd.name.toUpperCase()) continue;
|
|
1161
1318
|
for (const n of m.nodes) {
|
|
1162
|
-
n
|
|
1319
|
+
addSink(n, { kind: 'internal-reader', file: m.file, line: m.line,
|
|
1163
1320
|
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
1321
|
}
|
|
1165
1322
|
}
|
|
@@ -1167,8 +1324,8 @@ export function analyze(root, opts = {}) {
|
|
|
1167
1324
|
if (!dd.inStream || !dd.inStream.length || !dd.name) continue;
|
|
1168
1325
|
for (const m of callee.ddFiles || []) {
|
|
1169
1326
|
if (m.dd !== dd.name.toUpperCase()) continue;
|
|
1170
|
-
for (const n of m.nodes) {
|
|
1171
|
-
n
|
|
1327
|
+
for (const n of [...m.nodes, ...m.into]) {
|
|
1328
|
+
addSource(n, {
|
|
1172
1329
|
kind: 'jcl-instream', file: f, line: dd.line,
|
|
1173
1330
|
detail: `${dd.inStream.length} lines of in-stream data on //${dd.name} in step ${step.name || '(unnamed)'}, read through SELECT ${m.select}${amb}`,
|
|
1174
1331
|
});
|
|
@@ -1237,21 +1394,21 @@ export function analyze(root, opts = {}) {
|
|
|
1237
1394
|
const def = w.queue ? csd.tdqueues.get(w.queue) : null;
|
|
1238
1395
|
const dd = w.queue ? ddOfQueue(csd, w.queue) : null;
|
|
1239
1396
|
if (w.queue && (readerQueues.has(w.queue) || (dd && readerDds.has(dd)))) {
|
|
1240
|
-
w.node
|
|
1397
|
+
addSink(w.node, { kind: 'internal-reader', file: w.file, line: w.line, point: w.point,
|
|
1241
1398
|
detail: `EXEC CICS WRITEQ TD QUEUE('${w.queue}')${dd ? `, which the CSD sends to DD ${dd}` : ''}, declared as reaching the internal reader` });
|
|
1242
1399
|
continue;
|
|
1243
1400
|
}
|
|
1244
1401
|
// Any other queue is where a region writes what it logs or prints, unless it starts a
|
|
1245
1402
|
// transaction, when it is a channel to that transaction instead.
|
|
1246
1403
|
if (!(def && def.transid)) {
|
|
1247
|
-
w.node
|
|
1404
|
+
addSink(w.node, { kind: 'log', onlyFrom: REFLECTED, file: w.file, line: w.line, point: w.point,
|
|
1248
1405
|
detail: `EXEC CICS WRITEQ TD QUEUE(${w.queue ? `'${w.queue}'` : 'a name the program computes'})` });
|
|
1249
1406
|
}
|
|
1250
1407
|
if (!w.queue) { stats.tdWritesUndecided++; continue; }
|
|
1251
1408
|
if (def && def.type === 'INTRA') continue;
|
|
1252
1409
|
// A queue the CSD sends to a DD leaves the region: to a dataset, a printer or another job.
|
|
1253
1410
|
if (dd) {
|
|
1254
|
-
w.node
|
|
1411
|
+
addSink(w.node, { kind: 'extrapartition-queue', onlyFrom: DATA_AT_REST, file: w.file, line: w.line, point: w.point,
|
|
1255
1412
|
detail: `EXEC CICS WRITEQ TD QUEUE('${w.queue}'), which the CSD sends out of the region to DD ${dd}` });
|
|
1256
1413
|
}
|
|
1257
1414
|
if (!declared) stats.tdWritesUndecided++;
|
|
@@ -1259,6 +1416,19 @@ export function analyze(root, opts = {}) {
|
|
|
1259
1416
|
}
|
|
1260
1417
|
}
|
|
1261
1418
|
|
|
1419
|
+
// Each node's edges, in the order they were added: outStart[id] up to outStart[id + 1] index outTo
|
|
1420
|
+
// and outMeta.
|
|
1421
|
+
const outStart = new Int32Array(nodes.length + 1);
|
|
1422
|
+
for (let k = 0; k < edgeFrom.length; k++) outStart[edgeFrom[k] + 1]++;
|
|
1423
|
+
for (let i = 0; i < nodes.length; i++) outStart[i + 1] += outStart[i];
|
|
1424
|
+
const outTo = new Int32Array(edgeFrom.length);
|
|
1425
|
+
const outMeta = new Array(edgeFrom.length);
|
|
1426
|
+
{
|
|
1427
|
+
const fill = outStart.slice(0, nodes.length);
|
|
1428
|
+
for (let k = 0; k < edgeFrom.length; k++) { const at = fill[edgeFrom[k]]++; outTo[at] = edgeTo[k]; outMeta[at] = edgeMeta[k]; }
|
|
1429
|
+
edgeFrom.length = 0; edgeTo.length = 0; edgeMeta.length = 0;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1262
1432
|
// Only nodes that can reach some sink are worth walking into. One backward pass over a transient
|
|
1263
1433
|
// reverse index answers that for every source at once; it is a superset, so nothing is lost.
|
|
1264
1434
|
// Two answers, one bit each: whether a node reaches a sink a source at rest could be reported at,
|
|
@@ -1269,11 +1439,11 @@ export function analyze(root, opts = {}) {
|
|
|
1269
1439
|
const canReach = new Uint8Array(nodes.length);
|
|
1270
1440
|
{
|
|
1271
1441
|
const revStart = new Int32Array(nodes.length + 1);
|
|
1272
|
-
for (
|
|
1442
|
+
for (let k = 0; k < outTo.length; k++) revStart[outTo[k] + 1]++;
|
|
1273
1443
|
for (let i = 0; i < nodes.length; i++) revStart[i + 1] += revStart[i];
|
|
1274
1444
|
const fill = revStart.slice(0, nodes.length);
|
|
1275
1445
|
const rev = new Int32Array(revStart[nodes.length]);
|
|
1276
|
-
for (
|
|
1446
|
+
for (let id = 0; id < nodes.length; id++) for (let k = outStart[id]; k < outStart[id + 1]; k++) rev[fill[outTo[k]]++] = id;
|
|
1277
1447
|
const mark = (bit, seeds) => {
|
|
1278
1448
|
const queue = [];
|
|
1279
1449
|
for (const n of nodes) if (seeds(n)) { canReach[n.id] |= bit; queue.push(n.id); }
|
|
@@ -1381,13 +1551,14 @@ export function analyze(root, opts = {}) {
|
|
|
1381
1551
|
for (let i = 0; i < open.length; i++) {
|
|
1382
1552
|
const st = open[i];
|
|
1383
1553
|
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 (
|
|
1554
|
+
for (let k = outStart[st.node.id]; k < outStart[st.node.id + 1]; k++) {
|
|
1385
1555
|
edgesWalked++;
|
|
1386
|
-
const
|
|
1556
|
+
const e = outMeta[k];
|
|
1557
|
+
const to = nodes[outTo[k]];
|
|
1387
1558
|
if (!(canReach[to.id] & reach)) continue;
|
|
1388
1559
|
const mv = moves(e);
|
|
1389
1560
|
if (mv && holds(st.since, pointOfEdge(e), kind, null, shift)) continue;
|
|
1390
|
-
const t = cross(st, e);
|
|
1561
|
+
const t = cross(st, e, to);
|
|
1391
1562
|
if (!t) continue;
|
|
1392
1563
|
const since = mv ? [to] : [...st.since, to];
|
|
1393
1564
|
// An arrival carrying a checked node is refused more than one that does not, so it must not
|
|
@@ -1447,24 +1618,49 @@ export function analyze(root, opts = {}) {
|
|
|
1447
1618
|
// it does not. Both walks below cross edges through this, so neither can take a route the other
|
|
1448
1619
|
// would refuse.
|
|
1449
1620
|
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.
|
|
1621
|
+
// The call sites a value entered its callees through, innermost last, as a number standing for
|
|
1622
|
+
// that sequence; 0 where any caller may be returned to: a value born in the callee, or one that
|
|
1623
|
+
// rested in storage a program keeps between calls, which a later call from any caller can read.
|
|
1453
1624
|
const CONTEXT_DEPTH = 4;
|
|
1454
|
-
|
|
1455
|
-
|
|
1625
|
+
const contextSites = [[]];
|
|
1626
|
+
const contextNumber = new Map([['', 0]]);
|
|
1627
|
+
const entered = new Map();
|
|
1628
|
+
const returned = new Map();
|
|
1629
|
+
const contextOf = (sites) => {
|
|
1630
|
+
const k = sites.join('/');
|
|
1631
|
+
let c = contextNumber.get(k);
|
|
1632
|
+
if (c === undefined) { c = contextSites.length; contextSites.push(sites); contextNumber.set(k, c); }
|
|
1633
|
+
return c;
|
|
1634
|
+
};
|
|
1635
|
+
function contextOver(state, e, to) {
|
|
1636
|
+
const ctx = state.ctx || 0;
|
|
1456
1637
|
if (e.ret != null) {
|
|
1457
|
-
if (!ctx) return
|
|
1458
|
-
|
|
1459
|
-
|
|
1638
|
+
if (!ctx) return 0;
|
|
1639
|
+
let byRet = returned.get(ctx);
|
|
1640
|
+
if (!byRet) returned.set(ctx, byRet = new Map());
|
|
1641
|
+
let out = byRet.get(e.ret);
|
|
1642
|
+
if (out === undefined) {
|
|
1643
|
+
const sites = contextSites[ctx];
|
|
1644
|
+
out = Number(sites[sites.length - 1]) === e.ret ? contextOf(sites.slice(0, -1)) : null;
|
|
1645
|
+
byRet.set(e.ret, out);
|
|
1646
|
+
}
|
|
1647
|
+
return out;
|
|
1460
1648
|
}
|
|
1461
|
-
if (e.call != null)
|
|
1462
|
-
|
|
1649
|
+
if (e.call != null) {
|
|
1650
|
+
let byCall = entered.get(ctx);
|
|
1651
|
+
if (!byCall) entered.set(ctx, byCall = new Map());
|
|
1652
|
+
let out = byCall.get(e.call);
|
|
1653
|
+
if (out === undefined) {
|
|
1654
|
+
out = contextOf([...contextSites[ctx], String(e.call)].slice(-CONTEXT_DEPTH));
|
|
1655
|
+
byCall.set(e.call, out);
|
|
1656
|
+
}
|
|
1657
|
+
return out;
|
|
1658
|
+
}
|
|
1659
|
+
return to.linkage ? ctx : 0;
|
|
1463
1660
|
}
|
|
1464
|
-
function cross(state, e) {
|
|
1661
|
+
function cross(state, e, to) {
|
|
1465
1662
|
const cur = state.node;
|
|
1466
|
-
const
|
|
1467
|
-
const ctx = contextOver(state, e);
|
|
1663
|
+
const ctx = contextOver(state, e, to);
|
|
1468
1664
|
if (ctx === null) return null;
|
|
1469
1665
|
let partial = false, lo = 0, hi = 0, vague = false;
|
|
1470
1666
|
if (opts.legacyGroupEdges) { /* every edge carries the whole item */ }
|
|
@@ -1613,11 +1809,12 @@ export function analyze(root, opts = {}) {
|
|
|
1613
1809
|
if (best.level && !own) credited.push({ finding: findings[findings.length - 1], node: cur, sink, level: best.level });
|
|
1614
1810
|
if (sink.group) useGroup.set(findings[findings.length - 1], `${src.kind}|${src.file}:${src.line}|${sink.kind}|${sink.group}`);
|
|
1615
1811
|
}
|
|
1616
|
-
for (
|
|
1812
|
+
for (let k = outStart[cur.id]; k < outStart[cur.id + 1]; k++) {
|
|
1617
1813
|
if (++edgesWalked - began > walkBudget) { cut = true; break; }
|
|
1618
|
-
|
|
1619
|
-
const to =
|
|
1620
|
-
|
|
1814
|
+
const e = outMeta[k];
|
|
1815
|
+
const to = nodes[outTo[k]];
|
|
1816
|
+
if (!(canReach[to.id] & reach)) continue;
|
|
1817
|
+
const crossed = cross(state, e, to);
|
|
1621
1818
|
if (!crossed) continue;
|
|
1622
1819
|
if (crossedEdges) crossedEdges.push({ from: cur.id, to: to.id, e });
|
|
1623
1820
|
const { partial, lo, hi, vague, ctx } = crossed;
|
|
@@ -1715,6 +1912,8 @@ export function analyze(root, opts = {}) {
|
|
|
1715
1912
|
return { findings, constructs, unstarted, startedBy, stats, sourceKinds: SOURCE_KINDS, sinkKinds: SINK_KINDS, ...(sinks ? { sinks } : {}), ...(sources ? { sources } : {}) };
|
|
1716
1913
|
}
|
|
1717
1914
|
|
|
1915
|
+
export const analyze = (root, opts = {}) => drive(analyzeSteps(root, opts));
|
|
1916
|
+
|
|
1718
1917
|
// What the size check across a CALL needs from an item: its bytes, the bytes from it to the end of
|
|
1719
1918
|
// the record it sits in, whether a table inside it is variable, and where it was declared.
|
|
1720
1919
|
function extentOf(item) {
|