@portll/cobolwork 0.4.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 +53 -5
- package/bin/cobolwork.mjs +34 -9
- package/lib/baseline.mjs +9 -0
- package/lib/bms.mjs +8 -0
- package/lib/build.mjs +33 -8
- package/lib/capabilities.mjs +6 -3
- package/lib/consequence.mjs +17 -0
- package/lib/constants.mjs +60 -0
- package/lib/control-reuse.mjs +113 -0
- package/lib/control.mjs +86 -18
- package/lib/csd.mjs +6 -1
- package/lib/dataflow.mjs +343 -95
- 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/hlasm.mjs +79 -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 +24 -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 +115 -13
- 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/crypto.mjs +110 -0
- package/lib/sets/flow.mjs +32 -7
- package/lib/sets/hlasm.mjs +177 -0
- package/lib/sets/jcl.mjs +15 -1
- 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 +104 -7
- package/lib/sources.mjs +12 -2
- package/package.json +1 -1
- package/rules/compliance-cobit2019.json +216 -0
- package/rules/compliance-dora.json +234 -0
- package/rules/compliance-ffiec.json +234 -0
- package/rules/compliance-nist80053.json +234 -0
- package/rules/gitleaks-mainframe.toml +61 -16
- package/rules/hlasm-instructions.json +2396 -0
- package/rules/hlasm-operations.json +451 -0
- package/rules/icsf-services.json +1178 -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,
|
|
@@ -189,6 +251,9 @@ export function analyze(root, opts = {}) {
|
|
|
189
251
|
// Every hop of every trace, for the reader who is following one chain rather than counting
|
|
190
252
|
// findings. Off by default: a chain-shaped program turns this quadratic in the chain length.
|
|
191
253
|
const fullTrace = opts.fullTrace === true;
|
|
254
|
+
// Every statement on any route from a finding's source to its sink, not only the route the trace
|
|
255
|
+
// names: what a run must have executed before it can show a value does not arrive (reach.md §9.8).
|
|
256
|
+
const allRoutes = opts.allRoutes === true;
|
|
192
257
|
let held = 0;
|
|
193
258
|
|
|
194
259
|
// Which files, not just how many. A count names nothing to go and look at, and it cannot tell
|
|
@@ -216,10 +281,35 @@ export function analyze(root, opts = {}) {
|
|
|
216
281
|
const rel = (p) => { let r = relOf.get(p); if (r === undefined) { r = relPath(root, p); relOf.set(p, r); } return r; };
|
|
217
282
|
// `at` is the statement an edge that is not a statement's own is read at: the CALL or LINK that
|
|
218
283
|
// passes an argument, the PUT or GET of a container.
|
|
219
|
-
// `link` marks an edge into a callee at a call site ({ call }) or back out to it ({ ret })
|
|
284
|
+
// `link` marks an edge into a callee at a call site ({ call }) or back out to it ({ ret }), and
|
|
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
|
+
};
|
|
220
302
|
const edge = (a, b, why, dir, at, link) => {
|
|
221
303
|
if (!a || !b || a === b) return;
|
|
222
|
-
|
|
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);
|
|
223
313
|
stats.edges++;
|
|
224
314
|
};
|
|
225
315
|
let callSites = 0;
|
|
@@ -227,7 +317,52 @@ export function analyze(root, opts = {}) {
|
|
|
227
317
|
// Everything the graph needs from one program is taken while its parse tree is in hand, and the
|
|
228
318
|
// tree is then released. Holding every tree until the end is what exhausted a 4 GB heap on an
|
|
229
319
|
// 8,024-file repository; nodes, edges and a few cross-program sites are all that survive.
|
|
230
|
-
|
|
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 };
|
|
231
366
|
const byItem = new Map();
|
|
232
367
|
const byName = new Map();
|
|
233
368
|
for (const it of prog.items) if (!byName.has(it.name)) byName.set(it.name, it);
|
|
@@ -241,7 +376,11 @@ export function analyze(root, opts = {}) {
|
|
|
241
376
|
for (let a = item; a; a = a.parent) if ((a.occurs || 1) > 1) { repeats = true; table = a; }
|
|
242
377
|
let top = item;
|
|
243
378
|
while (top.parent) top = top.parent;
|
|
244
|
-
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;
|
|
245
384
|
// Where the outermost table holding this item sits in the record: every occurrence of the
|
|
246
385
|
// item is somewhere inside it. A group's size already counts its occurrences; an
|
|
247
386
|
// elementary item's is one element.
|
|
@@ -263,7 +402,7 @@ export function analyze(root, opts = {}) {
|
|
|
263
402
|
const indexNode = (name) => {
|
|
264
403
|
let n = indexNodes.get(name);
|
|
265
404
|
if (!n) {
|
|
266
|
-
n =
|
|
405
|
+
n = new FlowNode(nodes.length, pk, name);
|
|
267
406
|
nodes.push(n);
|
|
268
407
|
indexNodes.set(name, n);
|
|
269
408
|
stats.nodes++;
|
|
@@ -277,12 +416,12 @@ export function analyze(root, opts = {}) {
|
|
|
277
416
|
// constraints, so only SQLERRMC reaches the screen sink.
|
|
278
417
|
const systemSource = (n, detail, at) => {
|
|
279
418
|
const onlyTo = /^SQLERRMC\b/.test(detail) ? [...TO_CLIENT, 'screen'] : TO_CLIENT;
|
|
280
|
-
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 });
|
|
281
420
|
};
|
|
282
421
|
const systemNode = (tok) => {
|
|
283
422
|
let n = systemNodes.get(tok.u);
|
|
284
423
|
if (!n) {
|
|
285
|
-
n =
|
|
424
|
+
n = new FlowNode(nodes.length, pk, tok.u);
|
|
286
425
|
nodes.push(n);
|
|
287
426
|
systemNodes.set(tok.u, n);
|
|
288
427
|
stats.nodes++;
|
|
@@ -300,9 +439,20 @@ export function analyze(root, opts = {}) {
|
|
|
300
439
|
// The program's statement order, where its structure could be read. Without it a check is
|
|
301
440
|
// credited as it always was, wherever its field is used, and never clears anything.
|
|
302
441
|
let ctl = null;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
+
}
|
|
306
456
|
const pointOf = (x) => (ctl ? ctl.nodeOf.get(x) : undefined);
|
|
307
457
|
// Where a name inside a condition is read, which can know more than the statement does.
|
|
308
458
|
const placeOf = (tok, st) => (ctl && ctl.posOf.get(tok)) ?? pointOf(st);
|
|
@@ -319,7 +469,7 @@ export function analyze(root, opts = {}) {
|
|
|
319
469
|
const device = upon >= 0 && st.sources[upon + 1] ? ` UPON ${st.sources[upon + 1].u}` : '';
|
|
320
470
|
for (const t of upon >= 0 ? st.sources.slice(0, upon) : st.sources) {
|
|
321
471
|
const n = nodeOfToken(t);
|
|
322
|
-
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}` });
|
|
323
473
|
}
|
|
324
474
|
}
|
|
325
475
|
// The document XML PARSE reads is the operand after PARSE; what the parser does with a DTD in
|
|
@@ -327,7 +477,7 @@ export function analyze(root, opts = {}) {
|
|
|
327
477
|
for (const st of prog.statements) {
|
|
328
478
|
if (st.verb !== 'XML' || !st.sources[0] || st.sources[0].u !== 'PARSE') continue;
|
|
329
479
|
const n = st.sources[1] ? nodeOfToken(st.sources[1]) : null;
|
|
330
|
-
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}` });
|
|
331
481
|
}
|
|
332
482
|
for (const it of prog.items) {
|
|
333
483
|
const n = nodeOf(it);
|
|
@@ -363,7 +513,7 @@ export function analyze(root, opts = {}) {
|
|
|
363
513
|
for (const it of prog.items) { const top = recordOf(it); if (!byRecord.has(top)) byRecord.set(top, []); byRecord.get(top).push(it); }
|
|
364
514
|
const extent = (it) => (it.offset != null && it.size != null ? [it.offset, it.offset + (it.contributes || it.size)] : null);
|
|
365
515
|
for (const c of ctl.checks) {
|
|
366
|
-
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 } : {}) };
|
|
367
517
|
if (c.field.index) { (indexNode(c.field.index).checks ||= []).push(entry); continue; }
|
|
368
518
|
const span = extent(c.field);
|
|
369
519
|
for (const it of byRecord.get(recordOf(c.field)) || [c.field]) {
|
|
@@ -451,7 +601,7 @@ export function analyze(root, opts = {}) {
|
|
|
451
601
|
if (indexed.has(use)) continue;
|
|
452
602
|
indexed.add(use);
|
|
453
603
|
const start = kind === 'reference-modification' && x.from && base != null ? startOf(x.from, idx, base) : null;
|
|
454
|
-
at
|
|
604
|
+
addSink(at, {
|
|
455
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 } : {}),
|
|
456
606
|
detail: kind === 'subscript'
|
|
457
607
|
? `${at.name} subscripts ${host.name}, a table of ${table.occurs}`
|
|
@@ -482,7 +632,7 @@ export function analyze(root, opts = {}) {
|
|
|
482
632
|
const use = `${key}|${pointOf(st) ?? `s${st.at}`}`;
|
|
483
633
|
if (indexed.has(use)) continue;
|
|
484
634
|
indexed.add(use);
|
|
485
|
-
nodeOf(it)
|
|
635
|
+
addSink(nodeOf(it), {
|
|
486
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 } : {}),
|
|
487
637
|
detail: `${it.name} decides where PERFORM VARYING ${counter} stops, and ${counter} subscripts ${walks.host.name}, a table of ${walks.table.occurs}`,
|
|
488
638
|
});
|
|
@@ -503,7 +653,7 @@ export function analyze(root, opts = {}) {
|
|
|
503
653
|
const use = `arithmetic|${n.id}|${pointOf(st) ?? `s${st.at}`}`;
|
|
504
654
|
if (indexed.has(use)) continue;
|
|
505
655
|
indexed.add(use);
|
|
506
|
-
n
|
|
656
|
+
addSink(n, {
|
|
507
657
|
kind: 'arithmetic', onlyFrom: FROM_OUTSIDE, file: st.file, line: st.line, point: pointOf(st), group: `${pk}|arithmetic|${n.id}`,
|
|
508
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' : ''}`,
|
|
509
659
|
});
|
|
@@ -525,7 +675,7 @@ export function analyze(root, opts = {}) {
|
|
|
525
675
|
const sts = touching.get(it);
|
|
526
676
|
for (const st of sts.length ? sts : [null]) {
|
|
527
677
|
const point = st ? pointOf(st) : undefined;
|
|
528
|
-
nodeOf(count)
|
|
678
|
+
addSink(nodeOf(count), {
|
|
529
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 } : {}),
|
|
530
680
|
detail: `${count.name} sets how many ${it.name} entries there are, up to ${it.occurs}`,
|
|
531
681
|
});
|
|
@@ -535,18 +685,18 @@ export function analyze(root, opts = {}) {
|
|
|
535
685
|
if (st.verb !== 'READ' && st.verb !== 'RETURN') continue;
|
|
536
686
|
for (const t of st.targets) {
|
|
537
687
|
const n = nodeOfToken(t);
|
|
538
|
-
if (n) n
|
|
688
|
+
if (n) addSource(n, { kind: 'file-record', file: st.file, line: st.line, detail: `${st.verb} ... INTO` });
|
|
539
689
|
}
|
|
540
690
|
}
|
|
541
691
|
for (const fd of prog.fds || []) for (const rec of fd.records || []) {
|
|
542
692
|
const readsFile = prog.statements.some(st => (st.verb === 'READ') && st.sources.concat(st.targets).some(t => t.u === fd.name));
|
|
543
|
-
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}` });
|
|
544
694
|
}
|
|
545
695
|
for (const a of prog.accepts) {
|
|
546
696
|
const item = (a.targetTok ? itemOfToken(a.targetTok) : null) || byName.get(a.target);
|
|
547
697
|
if (!item) continue;
|
|
548
698
|
const kind = /^(COMMAND-LINE|ARGUMENT-VALUE|ENVIRONMENT|ENVIRONMENT-VALUE)$/.test(a.from || '') ? 'argv-or-env' : null;
|
|
549
|
-
if (kind) nodeOf(item)
|
|
699
|
+
if (kind) addSource(nodeOf(item), { kind, file: a.file, line: a.line, detail: `ACCEPT ... FROM ${a.from}` });
|
|
550
700
|
}
|
|
551
701
|
// A SELECT assigns either to a data item, which makes the path dynamic, or to an external
|
|
552
702
|
// name. On z/OS that external name is the DD name a JCL step fills, and it is how a job's
|
|
@@ -557,7 +707,7 @@ export function analyze(root, opts = {}) {
|
|
|
557
707
|
if (f.assign.t === 'word') {
|
|
558
708
|
const item = byName.get(f.assign.v);
|
|
559
709
|
if (item) {
|
|
560
|
-
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}` });
|
|
561
711
|
continue; // a variable path is not a DD name
|
|
562
712
|
}
|
|
563
713
|
}
|
|
@@ -566,7 +716,9 @@ export function analyze(root, opts = {}) {
|
|
|
566
716
|
if (!external) continue;
|
|
567
717
|
const fd = (prog.fds || []).find((x) => x.name === f.name);
|
|
568
718
|
const recs = (fd?.records || []).map(nodeOf);
|
|
569
|
-
|
|
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 });
|
|
570
722
|
}
|
|
571
723
|
const topOf = (it) => { let t = it; while (t && t.parent) t = t.parent; return t; };
|
|
572
724
|
const ownParams = new Set((prog.paramTokens || []).map((pt) => topOf(itemOfToken(pt.tok))).filter(Boolean));
|
|
@@ -589,14 +741,14 @@ export function analyze(root, opts = {}) {
|
|
|
589
741
|
const point = pointOf(prog.statements[c.stmtIndex]);
|
|
590
742
|
if (c.kind === 'I') {
|
|
591
743
|
const n = nodeOfToken(c.targetTok);
|
|
592
|
-
if (n) n
|
|
744
|
+
if (n) addSink(n, { kind: 'dynamic-program-load', file: c.file, line: c.line, point, detail: `CALL ${c.name}` });
|
|
593
745
|
continue;
|
|
594
746
|
}
|
|
595
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 })) });
|
|
596
748
|
if (OS_COMMAND_ROUTINE.test(c.name)) {
|
|
597
749
|
for (const a of c.using) {
|
|
598
750
|
const n = a.tok ? nodeOfToken(a.tok) : null;
|
|
599
|
-
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}` });
|
|
600
752
|
}
|
|
601
753
|
} else if (SOCKET_ROUTINE.test(c.name)) {
|
|
602
754
|
// IBM's own examples hold the function code in a field: 01 SOC-FUNCTION PIC X(16) VALUE 'SEND'.
|
|
@@ -607,23 +759,23 @@ export function analyze(root, opts = {}) {
|
|
|
607
759
|
if (fn && SOCKET_SEND.test(String(fn).trim())) {
|
|
608
760
|
for (const a of c.using.slice(1)) {
|
|
609
761
|
const n = a.tok ? nodeOfToken(a.tok) : null;
|
|
610
|
-
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}` });
|
|
611
763
|
}
|
|
612
764
|
}
|
|
613
765
|
} else if (Object.hasOwn(MQ_BUFFER_ARG, String(c.name).toUpperCase())) {
|
|
614
766
|
const a = c.using[MQ_BUFFER_ARG[String(c.name).toUpperCase()]];
|
|
615
767
|
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
616
|
-
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}` });
|
|
617
769
|
} else if (/^MQCONNX?$/i.test(String(c.name))) {
|
|
618
770
|
// MQCONN(QMgrName, Hconn, CompCode, Reason); MQCONNX puts its options second.
|
|
619
771
|
const a = c.using[0];
|
|
620
772
|
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
621
|
-
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` });
|
|
622
774
|
} else if (String(c.name).toUpperCase() === 'CEEGTST') {
|
|
623
775
|
// CEEGTST(heap id, size, address, feedback code): the size is the second argument.
|
|
624
776
|
const a = c.using[1];
|
|
625
777
|
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
626
|
-
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` });
|
|
627
779
|
}
|
|
628
780
|
}
|
|
629
781
|
const transfers = [];
|
|
@@ -639,7 +791,10 @@ export function analyze(root, opts = {}) {
|
|
|
639
791
|
const containerNode = (name) => {
|
|
640
792
|
let n = containers.get(name);
|
|
641
793
|
if (!n) {
|
|
642
|
-
n =
|
|
794
|
+
n = new FlowNode(nodes.length, pk, `CONTAINER(${name})`);
|
|
795
|
+
n.off = undefined;
|
|
796
|
+
n.size = undefined;
|
|
797
|
+
n.repeats = undefined;
|
|
643
798
|
nodes.push(n);
|
|
644
799
|
containers.set(name, n);
|
|
645
800
|
stats.nodes++;
|
|
@@ -727,7 +882,7 @@ export function analyze(root, opts = {}) {
|
|
|
727
882
|
if (typed) continue;
|
|
728
883
|
stats.protectedFields++;
|
|
729
884
|
const back = field.effective.has('FSET') ? ' with FSET' : '';
|
|
730
|
-
n
|
|
885
|
+
addSource(n, { kind: 'cics-protected-field', onlyTo: ['record-key', 'record-update'], ...at,
|
|
731
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}` });
|
|
732
887
|
}
|
|
733
888
|
};
|
|
@@ -754,17 +909,17 @@ export function analyze(root, opts = {}) {
|
|
|
754
909
|
if (verb === 'WRITE' && ['OPERATOR', 'JOURNALNAME', 'JOURNALNUM'].includes(words[1])) {
|
|
755
910
|
const opt = words[1] === 'OPERATOR' ? 'TEXT' : 'FROM';
|
|
756
911
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
757
|
-
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'}` });
|
|
758
913
|
}
|
|
759
914
|
if (verb === 'RECEIVE') {
|
|
760
915
|
const isWeb = words.includes('WEB');
|
|
761
916
|
const n = nodeOfToken(intoTok);
|
|
762
|
-
if (n) n
|
|
917
|
+
if (n) addSource(n, { kind: isWeb ? 'cics-web' : 'cics-terminal', ...at, detail: `EXEC CICS ${isWeb ? 'WEB ' : ''}RECEIVE` });
|
|
763
918
|
// RECEIVE MAP with no INTO fills the symbolic map, whose input record is <map>I.
|
|
764
919
|
const map = optionName(o, 'MAP');
|
|
765
920
|
if (!n && map) {
|
|
766
921
|
const sym = byName.get(`${map}I`);
|
|
767
|
-
if (sym) nodeOf(sym)
|
|
922
|
+
if (sym) addSource(nodeOf(sym), { kind: 'cics-terminal', ...at, detail: `EXEC CICS RECEIVE MAP(${map}) into the symbolic map` });
|
|
768
923
|
}
|
|
769
924
|
if (!isWeb && o.has('MAP')) receiveMap(e, o, itemOfToken(intoTok), at);
|
|
770
925
|
}
|
|
@@ -774,7 +929,7 @@ export function analyze(root, opts = {}) {
|
|
|
774
929
|
const map = words[1] === 'MAP' ? optionName(o, 'MAP') : null;
|
|
775
930
|
const sym = !from && map ? byName.get(`${map}O`) : null;
|
|
776
931
|
const n = from || (sym ? nodeOf(sym) : null);
|
|
777
|
-
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` });
|
|
778
933
|
}
|
|
779
934
|
// RIDFLD decides which record a READ or DELETE reaches, or where a browse starts. A key the
|
|
780
935
|
// user types is how a lookup works; a key the program put in a protected field and read back
|
|
@@ -786,18 +941,18 @@ export function analyze(root, opts = {}) {
|
|
|
786
941
|
const detail = verb === 'DELETE' ? `EXEC CICS DELETE RIDFLD(${k && k.name}), which decides which record it deletes`
|
|
787
942
|
: then ? `EXEC CICS READ UPDATE RIDFLD(${k && k.name}), which decides which record the program then ${then}`
|
|
788
943
|
: `EXEC CICS ${verb} RIDFLD(${k && k.name}), which decides which record it reaches`;
|
|
789
|
-
if (k) k
|
|
944
|
+
if (k) addSink(k, { kind: verb === 'DELETE' || then ? 'record-update' : 'record-key', onlyFrom: ['cics-protected-field'], noCredit: true, ...at, detail });
|
|
790
945
|
}
|
|
791
946
|
if (verb === 'GETMAIN') {
|
|
792
947
|
for (const opt of ['FLENGTH', 'LENGTH']) {
|
|
793
948
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
794
|
-
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` });
|
|
795
950
|
}
|
|
796
951
|
}
|
|
797
952
|
if (verb === 'PUT' && o.has('CONTAINER')) {
|
|
798
953
|
const name = optionName(o, 'CONTAINER');
|
|
799
954
|
const from = nodeOfToken((o.get('FROM') || [])[0]);
|
|
800
|
-
if (name && from) edge(from, containerNode(name), `EXEC CICS PUT CONTAINER(${name})`, undefined, at.point);
|
|
955
|
+
if (name && from) edge(from, containerNode(name), `EXEC CICS PUT CONTAINER(${name})`, undefined, at.point, { stmt: { verb: 'EXEC CICS PUT', file: rel(at.file), line: at.line } });
|
|
801
956
|
if (name) channelOf.set(name, optionName(o, 'CHANNEL'));
|
|
802
957
|
}
|
|
803
958
|
// A service call sends every container on its channel, and its URI decides where they go.
|
|
@@ -805,22 +960,22 @@ export function analyze(root, opts = {}) {
|
|
|
805
960
|
invokes.push({ at, what: words[1], channel: optionName(o, 'CHANNEL') });
|
|
806
961
|
for (const opt of ['URI', 'URIMAP']) {
|
|
807
962
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
808
|
-
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` });
|
|
809
964
|
}
|
|
810
965
|
}
|
|
811
966
|
if (verb === 'GET' && o.has('CONTAINER')) {
|
|
812
967
|
const name = optionName(o, 'CONTAINER');
|
|
813
968
|
const into = nodeOfToken((o.get('INTO') || o.get('SET') || [])[0]);
|
|
814
|
-
if (name && into) edge(containerNode(name), into, `EXEC CICS GET CONTAINER(${name})`, undefined, at.point);
|
|
969
|
+
if (name && into) edge(containerNode(name), into, `EXEC CICS GET CONTAINER(${name})`, undefined, at.point, { stmt: { verb: 'EXEC CICS GET', file: rel(at.file), line: at.line } });
|
|
815
970
|
}
|
|
816
971
|
// CONVERSE is always a client call; SEND is one only on a session this program opened. A
|
|
817
972
|
// plain WEB SEND answers the request that started the program, which is its job.
|
|
818
973
|
if (verb === 'WEB' && (words[1] === 'CONVERSE' || (words[1] === 'SEND' && o.has('SESSTOKEN')))) {
|
|
819
974
|
const n = nodeOfToken((o.get('FROM') || [])[0]);
|
|
820
|
-
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` });
|
|
821
976
|
// The request may be a container rather than a field.
|
|
822
977
|
const sent = o.has('CONTAINER') ? optionName(o, 'CONTAINER') : null;
|
|
823
|
-
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})` });
|
|
824
979
|
}
|
|
825
980
|
// The response this program returns to its own caller, and the document built for it. CICS
|
|
826
981
|
// escapes nothing on the way out - DOCUMENT SET's UNESCAPED option is about URL decoding,
|
|
@@ -832,25 +987,25 @@ export function analyze(root, opts = {}) {
|
|
|
832
987
|
// defect whatever the reply holds.
|
|
833
988
|
const media = (o.get('MEDIATYPE') || [])[0];
|
|
834
989
|
const markup = media && media.t === 'lit' && /html|xml|svg/i.test(String(media.v));
|
|
835
|
-
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()}` : ''}` });
|
|
836
991
|
}
|
|
837
992
|
if (verb === 'DOCUMENT') {
|
|
838
993
|
const what = words[1] === 'SET' ? ['VALUE', 'SYMBOLLIST'] : ['TEXT', 'FROM', 'BINARY'];
|
|
839
994
|
for (const opt of what) {
|
|
840
995
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
841
|
-
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` });
|
|
842
997
|
}
|
|
843
998
|
}
|
|
844
999
|
if (verb === 'WEB' && words[1] === 'WRITE' && o.has('HTTPHEADER')) {
|
|
845
1000
|
for (const opt of ['HTTPHEADER', 'VALUE']) {
|
|
846
1001
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
847
|
-
if (n) n
|
|
1002
|
+
if (n) addSink(n, { kind: 'http-header', onlyFrom: [...IN_CICS, 'system-response'], ...at, detail: `EXEC CICS WEB WRITE HTTPHEADER ${opt}(...)` });
|
|
848
1003
|
}
|
|
849
1004
|
}
|
|
850
1005
|
if (verb === 'WEB' && (words[1] === 'OPEN' || words[1] === 'CONVERSE')) {
|
|
851
1006
|
for (const opt of ['HOST', 'URIMAP', 'PATH']) {
|
|
852
1007
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
853
|
-
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` });
|
|
854
1009
|
}
|
|
855
1010
|
}
|
|
856
1011
|
// The queue a command acts on, as distinct from what it writes there: a name from input
|
|
@@ -859,7 +1014,7 @@ export function analyze(root, opts = {}) {
|
|
|
859
1014
|
const ts = words[1] === 'TS' || !words.includes('TD');
|
|
860
1015
|
for (const opt of ['QUEUE', 'QNAME']) {
|
|
861
1016
|
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
862
|
-
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` });
|
|
863
1018
|
}
|
|
864
1019
|
}
|
|
865
1020
|
// SYSID ships a command to the region it names; SET's first option names the resource a
|
|
@@ -867,20 +1022,20 @@ export function analyze(root, opts = {}) {
|
|
|
867
1022
|
// INQUIRE and EXTRACT return SYSID into the field rather than read it.
|
|
868
1023
|
if (o.has('SYSID') && !['ASSIGN', 'INQUIRE', 'EXTRACT'].includes(verb)) {
|
|
869
1024
|
const n = nodeOfToken((o.get('SYSID') || [])[0]);
|
|
870
|
-
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` });
|
|
871
1026
|
}
|
|
872
1027
|
if (verb === 'SET' && words[1] && o.has(words[1])) {
|
|
873
1028
|
const n = nodeOfToken((o.get(words[1]) || [])[0]);
|
|
874
|
-
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` });
|
|
875
1030
|
}
|
|
876
1031
|
if (verb === 'READ' || verb === 'READNEXT' || verb === 'READPREV') {
|
|
877
1032
|
const n = nodeOfToken(intoTok);
|
|
878
|
-
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` });
|
|
879
1034
|
}
|
|
880
1035
|
// A CONVERSE response is the remote server's data, which is untrusted input like any other.
|
|
881
1036
|
if ((verb === 'WEB' && words[1] !== 'SEND') || verb === 'EXTRACT') {
|
|
882
1037
|
const n = nodeOfToken(intoTok);
|
|
883
|
-
if (n) n
|
|
1038
|
+
if (n) addSource(n, { kind: 'cics-web', ...at, detail: `EXEC CICS ${words.slice(0, 2).join(' ')}` });
|
|
884
1039
|
}
|
|
885
1040
|
// Where a transient-data write lands is not in the program: the CSD maps the queue to a DD
|
|
886
1041
|
// and the region's JCL maps the DD to a destination. Collected here, resolved once every
|
|
@@ -897,13 +1052,13 @@ export function analyze(root, opts = {}) {
|
|
|
897
1052
|
const pgm = (o.get('PROGRAM') || o.get('TRANSID') || [])[0];
|
|
898
1053
|
if (pgm && pgm.t === 'word') {
|
|
899
1054
|
const n = nodeOfToken(pgm);
|
|
900
|
-
if (n) n
|
|
1055
|
+
if (n) addSink(n, { kind: 'cics-dynamic-transfer', ...at, detail: `EXEC CICS ${verb} with a variable program name` });
|
|
901
1056
|
}
|
|
902
1057
|
const commarea = (o.get('COMMAREA') || o.get('FROM') || [])[0];
|
|
903
1058
|
const argNode = commarea ? nodeOfToken(commarea) : null;
|
|
904
|
-
if (pgm && pgm.t === 'lit' && argNode) transfers.push({ verb, callee: pgm.v.toUpperCase(), argNode, point: at.point });
|
|
1059
|
+
if (pgm && pgm.t === 'lit' && argNode) transfers.push({ verb, callee: pgm.v.toUpperCase(), argNode, point: at.point, file: at.file, line: at.line });
|
|
905
1060
|
else if (pgm && pgm.t === 'word' && argNode && o.has('PROGRAM')) {
|
|
906
|
-
for (const callee of programsNamed(pgm, e)) { transfers.push({ verb, callee, argNode, point: at.point, through: pgm.u }); transfersTo.add(callee); }
|
|
1061
|
+
for (const callee of programsNamed(pgm, e)) { transfers.push({ verb, callee, argNode, point: at.point, file: at.file, line: at.line, through: pgm.u }); transfersTo.add(callee); }
|
|
907
1062
|
}
|
|
908
1063
|
if (pgm && pgm.t === 'lit') (o.has('TRANSID') && !o.has('PROGRAM') ? startsTransactions : transfersTo).add(String(pgm.v).trim().toUpperCase());
|
|
909
1064
|
}
|
|
@@ -921,7 +1076,7 @@ export function analyze(root, opts = {}) {
|
|
|
921
1076
|
for (const h of hv) {
|
|
922
1077
|
if (!h.written) continue;
|
|
923
1078
|
const n = nodeOfToken(h.tok);
|
|
924
|
-
if (n) n
|
|
1079
|
+
if (n) addSource(n, { kind: 'database', ...at, detail: `EXEC SQL ${verb} ${verb === 'SELECT' || verb === 'FETCH' ? 'INTO' : 'writes'} host variable` });
|
|
925
1080
|
}
|
|
926
1081
|
// The location CONNECT TO or SET CONNECTION names, where a host variable holds it; what
|
|
927
1082
|
// follows USER and USING is the credential, not the target.
|
|
@@ -929,19 +1084,19 @@ export function analyze(root, opts = {}) {
|
|
|
929
1084
|
const to = e.toks.findIndex((t) => t.t === 'word' && (t.u === 'TO' || t.u === 'CONNECTION'));
|
|
930
1085
|
const colon = to >= 0 ? e.toks[to + 1] : null;
|
|
931
1086
|
const n = colon && colon.t === 'op' && colon.v === ':' && e.toks[to + 2] ? nodeOfToken(e.toks[to + 2]) : null;
|
|
932
|
-
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` });
|
|
933
1088
|
}
|
|
934
1089
|
const dynamic = words.some((w, i) => w === 'PREPARE' || (w === 'EXECUTE' && words[i + 1] === 'IMMEDIATE'));
|
|
935
1090
|
if (dynamic) for (const h of hv) {
|
|
936
1091
|
const n = nodeOfToken(h.tok);
|
|
937
|
-
if (n) n
|
|
1092
|
+
if (n) addSink(n, { kind: 'dynamic-sql', ...at, detail: `EXEC SQL ${words.slice(0, 2).join(' ')} from a host variable` });
|
|
938
1093
|
}
|
|
939
1094
|
}
|
|
940
1095
|
}
|
|
941
1096
|
for (const inv of invokes) {
|
|
942
1097
|
for (const [name, channel] of channelOf) {
|
|
943
1098
|
if (inv.channel && channel && channel !== inv.channel) continue;
|
|
944
|
-
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})` : ''}` });
|
|
945
1100
|
}
|
|
946
1101
|
}
|
|
947
1102
|
const commareaItem = byName.get('DFHCOMMAREA');
|
|
@@ -991,7 +1146,8 @@ export function analyze(root, opts = {}) {
|
|
|
991
1146
|
// edges are proportional to data items and statements, not to bytes, so a repository of small
|
|
992
1147
|
// programs with large record layouts can exhaust a heap the budget believes is untouched. The
|
|
993
1148
|
// shared loop watches the heap itself, and the budget stays as the cheaper first line.
|
|
994
|
-
const
|
|
1149
|
+
const repeated = repeatedTexts(tree, files);
|
|
1150
|
+
const run = yield loopOver(files, (f) => {
|
|
995
1151
|
let src;
|
|
996
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; }
|
|
997
1153
|
if (!/PROCEDURE\s+DIVISION|PROGRAM-ID/i.test(src)) return 0;
|
|
@@ -1002,7 +1158,12 @@ export function analyze(root, opts = {}) {
|
|
|
1002
1158
|
r = tree.parse(f, src);
|
|
1003
1159
|
} catch (e) { stats.threw++; unparsed.push(`${tree.rel(f)}: ${e.code || e.name}`); return src.length; }
|
|
1004
1160
|
stats.files++;
|
|
1005
|
-
|
|
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);
|
|
1006
1167
|
return src.length;
|
|
1007
1168
|
}, { label: 'flow', watcher: memoryWatcher });
|
|
1008
1169
|
|
|
@@ -1046,22 +1207,24 @@ export function analyze(root, opts = {}) {
|
|
|
1046
1207
|
for (const t of p.transfers) {
|
|
1047
1208
|
const r = calleesOf(t.callee, p.file);
|
|
1048
1209
|
const site = ++callSites;
|
|
1210
|
+
const stmt = { verb: `EXEC CICS ${t.verb}`, file: rel(t.file), line: t.line };
|
|
1049
1211
|
for (const callee of r.list) {
|
|
1050
1212
|
if (!callee.commarea) continue;
|
|
1051
|
-
edge(t.argNode, callee.commarea, `EXEC CICS ${t.verb} COMMAREA to ${t.callee}${t.through ? `, the program ${t.through} names` : ''}${ofN(r, t.callee)}`, undefined, t.point, { call: site });
|
|
1052
|
-
if (t.verb === 'LINK') edge(callee.commarea, t.argNode, `COMMAREA returned from ${t.callee}${ofN(r, t.callee)}`, undefined, undefined, { ret: site });
|
|
1213
|
+
edge(t.argNode, callee.commarea, `EXEC CICS ${t.verb} COMMAREA to ${t.callee}${t.through ? `, the program ${t.through} names` : ''}${ofN(r, t.callee)}`, undefined, t.point, { call: site, stmt });
|
|
1214
|
+
if (t.verb === 'LINK') edge(callee.commarea, t.argNode, `COMMAREA returned from ${t.callee}${ofN(r, t.callee)}`, undefined, undefined, { ret: site, stmt });
|
|
1053
1215
|
}
|
|
1054
1216
|
}
|
|
1055
1217
|
for (const c of p.callSites) {
|
|
1056
1218
|
const r = calleesOf(c.name, p.file);
|
|
1057
1219
|
const site = ++callSites;
|
|
1220
|
+
const stmt = { verb: 'CALL', file: rel(c.file), line: c.line };
|
|
1058
1221
|
for (const callee of r.list) {
|
|
1059
1222
|
if (!callee.params) continue;
|
|
1060
1223
|
c.args.forEach((arg, i) => {
|
|
1061
1224
|
const param = callee.params[i];
|
|
1062
1225
|
if (!param || !arg.node || !param.node) return;
|
|
1063
|
-
edge(arg.node, param.node, `CALL '${c.display}' argument ${i + 1} at ${rel(c.file)}:${c.line}${ofN(r, c.display)}`, undefined, c.point, { call: site });
|
|
1064
|
-
if (arg.mode === 'REFERENCE' && param.mode === 'REFERENCE') edge(param.node, arg.node, `CALL '${c.display}' argument ${i + 1} written back${ofN(r, c.display)}`, undefined, undefined, { ret: site });
|
|
1226
|
+
edge(arg.node, param.node, `CALL '${c.display}' argument ${i + 1} at ${rel(c.file)}:${c.line}${ofN(r, c.display)}`, undefined, c.point, { call: site, stmt });
|
|
1227
|
+
if (arg.mode === 'REFERENCE' && param.mode === 'REFERENCE') edge(param.node, arg.node, `CALL '${c.display}' argument ${i + 1} written back${ofN(r, c.display)}`, undefined, undefined, { ret: site, stmt });
|
|
1065
1228
|
});
|
|
1066
1229
|
}
|
|
1067
1230
|
const callee = r.list.length === 1 ? r.list[0] : null;
|
|
@@ -1136,7 +1299,7 @@ export function analyze(root, opts = {}) {
|
|
|
1136
1299
|
// followed by the text. A program with no USING cannot receive one, and saying it does
|
|
1137
1300
|
// would be a path nobody could follow.
|
|
1138
1301
|
if (run.parm !== null && callee.params && callee.params[0] && callee.params[0].node) {
|
|
1139
|
-
callee.params[0].node
|
|
1302
|
+
addSource(callee.params[0].node, {
|
|
1140
1303
|
kind: 'jcl-parm', file: f, line: run.line,
|
|
1141
1304
|
detail: `${run.how}, which runs ${run.pgm}${amb}`,
|
|
1142
1305
|
});
|
|
@@ -1153,7 +1316,7 @@ export function analyze(root, opts = {}) {
|
|
|
1153
1316
|
for (const m of callee.ddFiles || []) {
|
|
1154
1317
|
if (m.dd !== dd.name.toUpperCase()) continue;
|
|
1155
1318
|
for (const n of m.nodes) {
|
|
1156
|
-
n
|
|
1319
|
+
addSink(n, { kind: 'internal-reader', file: m.file, line: m.line,
|
|
1157
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}` });
|
|
1158
1321
|
}
|
|
1159
1322
|
}
|
|
@@ -1161,8 +1324,8 @@ export function analyze(root, opts = {}) {
|
|
|
1161
1324
|
if (!dd.inStream || !dd.inStream.length || !dd.name) continue;
|
|
1162
1325
|
for (const m of callee.ddFiles || []) {
|
|
1163
1326
|
if (m.dd !== dd.name.toUpperCase()) continue;
|
|
1164
|
-
for (const n of m.nodes) {
|
|
1165
|
-
n
|
|
1327
|
+
for (const n of [...m.nodes, ...m.into]) {
|
|
1328
|
+
addSource(n, {
|
|
1166
1329
|
kind: 'jcl-instream', file: f, line: dd.line,
|
|
1167
1330
|
detail: `${dd.inStream.length} lines of in-stream data on //${dd.name} in step ${step.name || '(unnamed)'}, read through SELECT ${m.select}${amb}`,
|
|
1168
1331
|
});
|
|
@@ -1231,21 +1394,21 @@ export function analyze(root, opts = {}) {
|
|
|
1231
1394
|
const def = w.queue ? csd.tdqueues.get(w.queue) : null;
|
|
1232
1395
|
const dd = w.queue ? ddOfQueue(csd, w.queue) : null;
|
|
1233
1396
|
if (w.queue && (readerQueues.has(w.queue) || (dd && readerDds.has(dd)))) {
|
|
1234
|
-
w.node
|
|
1397
|
+
addSink(w.node, { kind: 'internal-reader', file: w.file, line: w.line, point: w.point,
|
|
1235
1398
|
detail: `EXEC CICS WRITEQ TD QUEUE('${w.queue}')${dd ? `, which the CSD sends to DD ${dd}` : ''}, declared as reaching the internal reader` });
|
|
1236
1399
|
continue;
|
|
1237
1400
|
}
|
|
1238
1401
|
// Any other queue is where a region writes what it logs or prints, unless it starts a
|
|
1239
1402
|
// transaction, when it is a channel to that transaction instead.
|
|
1240
1403
|
if (!(def && def.transid)) {
|
|
1241
|
-
w.node
|
|
1404
|
+
addSink(w.node, { kind: 'log', onlyFrom: REFLECTED, file: w.file, line: w.line, point: w.point,
|
|
1242
1405
|
detail: `EXEC CICS WRITEQ TD QUEUE(${w.queue ? `'${w.queue}'` : 'a name the program computes'})` });
|
|
1243
1406
|
}
|
|
1244
1407
|
if (!w.queue) { stats.tdWritesUndecided++; continue; }
|
|
1245
1408
|
if (def && def.type === 'INTRA') continue;
|
|
1246
1409
|
// A queue the CSD sends to a DD leaves the region: to a dataset, a printer or another job.
|
|
1247
1410
|
if (dd) {
|
|
1248
|
-
w.node
|
|
1411
|
+
addSink(w.node, { kind: 'extrapartition-queue', onlyFrom: DATA_AT_REST, file: w.file, line: w.line, point: w.point,
|
|
1249
1412
|
detail: `EXEC CICS WRITEQ TD QUEUE('${w.queue}'), which the CSD sends out of the region to DD ${dd}` });
|
|
1250
1413
|
}
|
|
1251
1414
|
if (!declared) stats.tdWritesUndecided++;
|
|
@@ -1253,6 +1416,19 @@ export function analyze(root, opts = {}) {
|
|
|
1253
1416
|
}
|
|
1254
1417
|
}
|
|
1255
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
|
+
|
|
1256
1432
|
// Only nodes that can reach some sink are worth walking into. One backward pass over a transient
|
|
1257
1433
|
// reverse index answers that for every source at once; it is a superset, so nothing is lost.
|
|
1258
1434
|
// Two answers, one bit each: whether a node reaches a sink a source at rest could be reported at,
|
|
@@ -1263,11 +1439,11 @@ export function analyze(root, opts = {}) {
|
|
|
1263
1439
|
const canReach = new Uint8Array(nodes.length);
|
|
1264
1440
|
{
|
|
1265
1441
|
const revStart = new Int32Array(nodes.length + 1);
|
|
1266
|
-
for (
|
|
1442
|
+
for (let k = 0; k < outTo.length; k++) revStart[outTo[k] + 1]++;
|
|
1267
1443
|
for (let i = 0; i < nodes.length; i++) revStart[i + 1] += revStart[i];
|
|
1268
1444
|
const fill = revStart.slice(0, nodes.length);
|
|
1269
1445
|
const rev = new Int32Array(revStart[nodes.length]);
|
|
1270
|
-
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;
|
|
1271
1447
|
const mark = (bit, seeds) => {
|
|
1272
1448
|
const queue = [];
|
|
1273
1449
|
for (const n of nodes) if (seeds(n)) { canReach[n.id] |= bit; queue.push(n.id); }
|
|
@@ -1375,13 +1551,14 @@ export function analyze(root, opts = {}) {
|
|
|
1375
1551
|
for (let i = 0; i < open.length; i++) {
|
|
1376
1552
|
const st = open[i];
|
|
1377
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);
|
|
1378
|
-
for (
|
|
1554
|
+
for (let k = outStart[st.node.id]; k < outStart[st.node.id + 1]; k++) {
|
|
1379
1555
|
edgesWalked++;
|
|
1380
|
-
const
|
|
1556
|
+
const e = outMeta[k];
|
|
1557
|
+
const to = nodes[outTo[k]];
|
|
1381
1558
|
if (!(canReach[to.id] & reach)) continue;
|
|
1382
1559
|
const mv = moves(e);
|
|
1383
1560
|
if (mv && holds(st.since, pointOfEdge(e), kind, null, shift)) continue;
|
|
1384
|
-
const t = cross(st, e);
|
|
1561
|
+
const t = cross(st, e, to);
|
|
1385
1562
|
if (!t) continue;
|
|
1386
1563
|
const since = mv ? [to] : [...st.since, to];
|
|
1387
1564
|
// An arrival carrying a checked node is refused more than one that does not, so it must not
|
|
@@ -1441,24 +1618,49 @@ export function analyze(root, opts = {}) {
|
|
|
1441
1618
|
// it does not. Both walks below cross edges through this, so neither can take a route the other
|
|
1442
1619
|
// would refuse.
|
|
1443
1620
|
const known = (n) => n.off != null && n.size != null && !n.repeats;
|
|
1444
|
-
// The call sites a value entered its callees through, innermost last, as a
|
|
1445
|
-
// caller may be returned to: a value born in the callee, or one that
|
|
1446
|
-
// 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.
|
|
1447
1624
|
const CONTEXT_DEPTH = 4;
|
|
1448
|
-
|
|
1449
|
-
|
|
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;
|
|
1450
1637
|
if (e.ret != null) {
|
|
1451
|
-
if (!ctx) return
|
|
1452
|
-
|
|
1453
|
-
|
|
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;
|
|
1454
1648
|
}
|
|
1455
|
-
if (e.call != null)
|
|
1456
|
-
|
|
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;
|
|
1457
1660
|
}
|
|
1458
|
-
function cross(state, e) {
|
|
1661
|
+
function cross(state, e, to) {
|
|
1459
1662
|
const cur = state.node;
|
|
1460
|
-
const
|
|
1461
|
-
const ctx = contextOver(state, e);
|
|
1663
|
+
const ctx = contextOver(state, e, to);
|
|
1462
1664
|
if (ctx === null) return null;
|
|
1463
1665
|
let partial = false, lo = 0, hi = 0, vague = false;
|
|
1464
1666
|
if (opts.legacyGroupEdges) { /* every edge carries the whole item */ }
|
|
@@ -1498,6 +1700,44 @@ export function analyze(root, opts = {}) {
|
|
|
1498
1700
|
}
|
|
1499
1701
|
return { partial, lo, hi, vague, ctx };
|
|
1500
1702
|
}
|
|
1703
|
+
// The statement that carries an edge: null for storage that overlays storage, which runs nothing,
|
|
1704
|
+
// and undefined where the engine does not know which statement it is.
|
|
1705
|
+
const OVERLAYS = new Set(['group', 'redefines', 'renames']);
|
|
1706
|
+
const statementOf = (e) => {
|
|
1707
|
+
if (e.stmt) return e.stmt;
|
|
1708
|
+
if (e.why && typeof e.why === 'object') return { verb: e.why.verb, file: e.why.file, line: e.why.line };
|
|
1709
|
+
return OVERLAYS.has(e.why) ? null : undefined;
|
|
1710
|
+
};
|
|
1711
|
+
// Every route from one walk's source to each sink it reached: the edges the walk could cross that
|
|
1712
|
+
// lead on to the sink, taken or not, by node rather than by call context or bytes. That lists a
|
|
1713
|
+
// statement no route needs sooner than it leaves out one a route does.
|
|
1714
|
+
function attachRoutes(awaiting, crossed, src, cut) {
|
|
1715
|
+
const into = new Map();
|
|
1716
|
+
for (const c of crossed) {
|
|
1717
|
+
const list = into.get(c.to);
|
|
1718
|
+
if (list) list.push(c); else into.set(c.to, [c]);
|
|
1719
|
+
}
|
|
1720
|
+
for (const { finding, at } of awaiting) {
|
|
1721
|
+
const onRoute = new Set([at]);
|
|
1722
|
+
const stack = [at];
|
|
1723
|
+
const statements = new Map();
|
|
1724
|
+
let unplaced = 0;
|
|
1725
|
+
while (stack.length) {
|
|
1726
|
+
for (const c of into.get(stack.pop()) || []) {
|
|
1727
|
+
const s = statementOf(c.e);
|
|
1728
|
+
if (s) statements.set(`${s.file}:${s.line}:${s.verb}`, s);
|
|
1729
|
+
else if (s === undefined) unplaced++;
|
|
1730
|
+
if (!onRoute.has(c.from)) { onRoute.add(c.from); stack.push(c.from); }
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
finding.routes = {
|
|
1734
|
+
sources: [{ kind: src.kind, file: rel(src.file), line: src.line }],
|
|
1735
|
+
statements: [...statements.values()].sort((a, b) => byText(a.file, b.file) || a.line - b.line || byText(a.verb, b.verb)),
|
|
1736
|
+
...(unplaced ? { unplaced } : {}),
|
|
1737
|
+
complete: !cut && !unplaced,
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1501
1741
|
for (const start of nodes) {
|
|
1502
1742
|
if (!start.sources.length || !canReach[start.id]) continue;
|
|
1503
1743
|
for (const src of start.sources) {
|
|
@@ -1515,6 +1755,8 @@ export function analyze(root, opts = {}) {
|
|
|
1515
1755
|
// without a layout) partial taint does not descend at all.
|
|
1516
1756
|
const queue = [{ node: start, partial: false, lo: 0, hi: 0, vague: false, prev: null, why: 'source' }];
|
|
1517
1757
|
const partSeen = new Map();
|
|
1758
|
+
const crossedEdges = allRoutes ? [] : null;
|
|
1759
|
+
const awaiting = allRoutes ? [] : null;
|
|
1518
1760
|
stampWhole[start.id] = walk;
|
|
1519
1761
|
let cut = false;
|
|
1520
1762
|
const began = edgesWalked;
|
|
@@ -1563,15 +1805,18 @@ export function analyze(root, opts = {}) {
|
|
|
1563
1805
|
...(best.level ? { guard: { program: by.program, item: by.name, file: rel(best.check.file), line: best.check.line, ...(best.level === 2 ? { stops: true } : {}) } } : {}),
|
|
1564
1806
|
...(!best.level && missed ? { checkElsewhere: { program: missed.node.program, item: missed.node.name, file: rel(missed.check.file), line: missed.check.line } } : {}),
|
|
1565
1807
|
});
|
|
1808
|
+
if (awaiting) awaiting.push({ finding: findings[findings.length - 1], at: cur.id });
|
|
1566
1809
|
if (best.level && !own) credited.push({ finding: findings[findings.length - 1], node: cur, sink, level: best.level });
|
|
1567
1810
|
if (sink.group) useGroup.set(findings[findings.length - 1], `${src.kind}|${src.file}:${src.line}|${sink.kind}|${sink.group}`);
|
|
1568
1811
|
}
|
|
1569
|
-
for (
|
|
1812
|
+
for (let k = outStart[cur.id]; k < outStart[cur.id + 1]; k++) {
|
|
1570
1813
|
if (++edgesWalked - began > walkBudget) { cut = true; break; }
|
|
1571
|
-
|
|
1572
|
-
const to =
|
|
1573
|
-
|
|
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);
|
|
1574
1818
|
if (!crossed) continue;
|
|
1819
|
+
if (crossedEdges) crossedEdges.push({ from: cur.id, to: to.id, e });
|
|
1575
1820
|
const { partial, lo, hi, vague, ctx } = crossed;
|
|
1576
1821
|
if (partial) {
|
|
1577
1822
|
const pk = `${to.id}|${ctx}`;
|
|
@@ -1583,6 +1828,7 @@ export function analyze(root, opts = {}) {
|
|
|
1583
1828
|
}
|
|
1584
1829
|
}
|
|
1585
1830
|
if (cut) stats.walksCut++;
|
|
1831
|
+
if (awaiting?.length) attachRoutes(awaiting, crossedEdges, src, cut);
|
|
1586
1832
|
// The walk keeps the shortest route to each sink, and credit on that route says nothing if
|
|
1587
1833
|
// another reaches the sink without it. So the walk runs again, refusing to let the value leave
|
|
1588
1834
|
// a node where the credit holds, and a sink it still reaches loses the credit: first for any
|
|
@@ -1666,6 +1912,8 @@ export function analyze(root, opts = {}) {
|
|
|
1666
1912
|
return { findings, constructs, unstarted, startedBy, stats, sourceKinds: SOURCE_KINDS, sinkKinds: SINK_KINDS, ...(sinks ? { sinks } : {}), ...(sources ? { sources } : {}) };
|
|
1667
1913
|
}
|
|
1668
1914
|
|
|
1915
|
+
export const analyze = (root, opts = {}) => drive(analyzeSteps(root, opts));
|
|
1916
|
+
|
|
1669
1917
|
// What the size check across a CALL needs from an item: its bytes, the bytes from it to the end of
|
|
1670
1918
|
// the record it sits in, whether a table inside it is variable, and where it was declared.
|
|
1671
1919
|
function extentOf(item) {
|