@portll/cobolwork 0.2.76 → 0.2.140
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 +66 -14
- package/bin/cobolwork.mjs +126 -9
- package/lib/arith.mjs +235 -0
- package/lib/baseline.mjs +5 -0
- package/lib/build.mjs +112 -30
- package/lib/capabilities.mjs +12 -7
- package/lib/card.mjs +18 -0
- package/lib/cics-commands.mjs +136 -125
- package/lib/consequence.mjs +29 -0
- package/lib/control.mjs +1169 -91
- package/lib/csd.mjs +17 -2
- package/lib/dataflow.mjs +276 -126
- package/lib/embedded-sql.mjs +110 -0
- package/lib/enterprise-options.mjs +255 -0
- package/lib/equivalence.mjs +124 -0
- package/lib/evidence/cli.mjs +65 -0
- package/lib/evidence/journal.mjs +94 -0
- package/lib/evidence/record.mjs +110 -0
- package/lib/evidence/run.mjs +111 -0
- package/lib/evidence/seal.mjs +177 -0
- package/lib/evidence/slsa.mjs +64 -0
- package/lib/evidence/sshsig.mjs +172 -0
- package/lib/evidence/store.mjs +170 -0
- package/lib/evidence/verify.mjs +167 -0
- package/lib/explain.mjs +9 -5
- package/lib/exploitability.mjs +263 -0
- package/lib/ftp.mjs +136 -0
- package/lib/ironwork.mjs +109 -0
- package/lib/jcl.mjs +27 -7
- package/lib/kernel/findings.mjs +11 -0
- package/lib/kernel/registry.mjs +4 -0
- package/lib/layout.mjs +202 -0
- package/lib/option-diff.mjs +49 -0
- package/lib/options.mjs +109 -46
- package/lib/parser.mjs +80 -209
- package/lib/policy.mjs +8 -3
- package/lib/precompile.mjs +5 -84
- package/lib/reach.mjs +29 -12
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +1 -0
- package/lib/sbom.mjs +235 -0
- package/lib/scan.mjs +93 -27
- package/lib/sets/cics.mjs +119 -2
- package/lib/sets/copybook.mjs +2 -1
- package/lib/sets/flow.mjs +82 -2
- package/lib/sets/hidden.mjs +4 -8
- package/lib/sets/jcl.mjs +70 -106
- package/lib/sets/recon.mjs +2 -1
- package/lib/sets/semantics.mjs +377 -0
- package/lib/sets/web.mjs +34 -10
- package/lib/sets/zowe.mjs +246 -0
- package/lib/sources.mjs +10 -0
- package/lib/tui/app.mjs +31 -13
- package/lib/tui/model.mjs +17 -2
- package/lib/utilities.mjs +229 -15
- package/lib/verify.mjs +128 -0
- package/package.json +1 -1
- package/rules/compliance-dora.json +507 -0
- package/rules/compliance-ffiec.json +507 -0
- package/rules/compliance-nist80053.json +507 -0
- package/schema/cobolwork.policy.schema.json +94 -16
package/lib/dataflow.mjs
CHANGED
|
@@ -4,6 +4,9 @@ import { parseBms, symbolicNames } from './bms.mjs';
|
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { parseSource, buildFileIndex } from './parser.mjs';
|
|
6
6
|
import { parseJcl } from './jcl.mjs';
|
|
7
|
+
import { holdsDecimal } from './layout.mjs';
|
|
8
|
+
import { hostVariablesIn } from './embedded-sql.mjs';
|
|
9
|
+
import { tsoCommands } from './utilities.mjs';
|
|
7
10
|
import { parseCsd, ddOfQueue } from './csd.mjs';
|
|
8
11
|
import { loadSite } from './site.mjs';
|
|
9
12
|
import { ssrangeAbends } from './options.mjs';
|
|
@@ -12,7 +15,7 @@ import { eachWithinMemory, watchMemoryBuffer } from './kernel/memory.mjs';
|
|
|
12
15
|
// A flow finding is located by source and sink rather than by one path and line, so it keeps its
|
|
13
16
|
// own comparator. The text comparison underneath it is the shared one.
|
|
14
17
|
import { byText } from './kernel/findings.mjs';
|
|
15
|
-
import { buildControl, creditOf, hasFact } from './control.mjs';
|
|
18
|
+
import { buildControl, creditOf, hasFact, within } from './control.mjs';
|
|
16
19
|
import { directoryTree } from './kernel/source-tree.mjs';
|
|
17
20
|
|
|
18
21
|
const OS_COMMAND_ROUTINE = /^(SYSTEM|C\$SYSTEM|CBL_EXEC_RUN_UNIT|CBL_GC_HOSTED|BXPSYSTM)$/i;
|
|
@@ -51,6 +54,12 @@ export const SINK_KINDS = {
|
|
|
51
54
|
'record-key': 'the RIDFLD of EXEC CICS READ, STARTBR or RESETBR, which decides which record is read',
|
|
52
55
|
'record-update': 'the RIDFLD of a READ UPDATE whose record the program then rewrites or deletes, or of a DELETE',
|
|
53
56
|
'log': 'what a program in a CICS region writes to a log: DISPLAY, EXEC CICS WRITEQ TD to a queue that neither starts a transaction nor feeds the internal reader, WRITE JOURNALNAME or WRITE OPERATOR',
|
|
57
|
+
'storage-length': 'how much storage a program acquires: the FLENGTH or LENGTH of EXEC CICS GETMAIN, or the size CALL CEEGTST is given',
|
|
58
|
+
'xml-document': 'the document XML PARSE reads',
|
|
59
|
+
'connection-target': 'the database or queue manager a program connects to: the host variable EXEC SQL CONNECT TO or SET CONNECTION names, or the first argument of CALL MQCONN or MQCONNX',
|
|
60
|
+
'screen': 'what a CICS program shows its terminal: EXEC CICS SEND TEXT FROM, SEND MAP FROM, or the map\'s output record',
|
|
61
|
+
'cics-sysid': 'the SYSID of an EXEC CICS command, which decides the region the command is shipped to',
|
|
62
|
+
'cics-system-resource': 'the resource an EXEC CICS SET names, which a system-programming command then changes',
|
|
54
63
|
};
|
|
55
64
|
|
|
56
65
|
const ARITHMETIC = new Set(['COMPUTE', 'ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE']);
|
|
@@ -62,6 +71,7 @@ STANDARD-DEVIATION RANDOM SQRT FACTORIAL LOG LOG10 EXP EXP10 PI E ACOS ASIN ATAN
|
|
|
62
71
|
SECONDS-PAST-MIDNIGHT`.split(/\s+/));
|
|
63
72
|
const computes = (st) => ARITHMETIC.has(st.verb) || st.counts === true || (st.fns || []).some((f) => NUMERIC_FUNCTIONS.has(f));
|
|
64
73
|
const computedOnRoute = (state) => { for (let s = state; s; s = s.prev) if (s.why && s.why.computes) return true; return false; };
|
|
74
|
+
const typedOnRoute = (state) => { for (let s = state; s; s = s.prev) if (s.node.typed) return true; return false; };
|
|
65
75
|
|
|
66
76
|
// Every literal a program holds that could be a program name: its VALUE clauses and the literals its
|
|
67
77
|
// statements and commands use. A menu that XCTLs through a table of names starts every one of them.
|
|
@@ -83,15 +93,6 @@ function lastOption(name, list) {
|
|
|
83
93
|
return null;
|
|
84
94
|
}
|
|
85
95
|
|
|
86
|
-
// Whether an item's bytes are read as decimal digits: zoned (DISPLAY with a numeric picture) or
|
|
87
|
-
// packed. A group, an edited picture, binary and floating point are not.
|
|
88
|
-
function holdsDecimal(it) {
|
|
89
|
-
if ((it.children || []).some((c) => c.level !== 88)) return false;
|
|
90
|
-
const usage = String(it.effectiveUsage || 'DISPLAY').replace('COMPUTATIONAL', 'COMP');
|
|
91
|
-
if (usage === 'COMP-3' || usage === 'PACKED-DECIMAL' || usage === 'COMP-6') return true;
|
|
92
|
-
return usage === 'DISPLAY' && !!it.picture && /9/.test(it.picture) && /^[S9VP()0-9]+$/i.test(it.picture);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
96
|
const RELATION = new Set(['>', '<', '>=', '<=', '=', '<>']);
|
|
96
97
|
const RELATION_WORDS = new Set(['GREATER', 'LESS', 'EQUAL']);
|
|
97
98
|
const FILLER_WORDS = new Set(['IS', 'NOT', 'THAN', 'TO', 'OR', 'GREATER', 'LESS', 'EQUAL', 'OF', 'IN']);
|
|
@@ -126,7 +127,7 @@ const IN_CICS = ['cics-web', 'cics-terminal', 'file-record', 'database'];
|
|
|
126
127
|
const REFLECTED = ['cics-web', 'cics-terminal'];
|
|
127
128
|
// What the system says about a failure: a command's RESP, the EIB's copy of it, the SQLCA. Sent to a
|
|
128
129
|
// web client it describes the system behind the program (CWE-209); written anywhere else it is the
|
|
129
|
-
// program's own business, so these reach only a response.
|
|
130
|
+
// program's own business, so these reach only a response, and Db2's message text the terminal too.
|
|
130
131
|
const SYSTEM_FIELDS = new Set(['EIBRESP', 'EIBRESP2', 'SQLCODE', 'SQLSTATE', 'SQLERRMC']);
|
|
131
132
|
const TO_CLIENT = ['web-response', 'http-header'];
|
|
132
133
|
|
|
@@ -172,27 +173,6 @@ function execOptions(exec) {
|
|
|
172
173
|
return { opts, words };
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
function sqlHostVars(exec) {
|
|
176
|
-
const out = [];
|
|
177
|
-
for (let i = 0; i < exec.toks.length; i++) {
|
|
178
|
-
const t = exec.toks[i];
|
|
179
|
-
if (t.t === 'op' && t.v === ':' && exec.toks[i + 1] && exec.toks[i + 1].t === 'word') out.push({ tok: exec.toks[i + 1], at: i + 1 });
|
|
180
|
-
}
|
|
181
|
-
return out;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// The host variables a SELECT or FETCH fills are the ones in its INTO list, which ends at FROM.
|
|
185
|
-
// Taking every variable at or after the INTO line made the input parameters in a WHERE clause read
|
|
186
|
-
// as database values — and they are whatever the caller sent, under a rule that says data at rest.
|
|
187
|
-
function sqlIntoRange(exec) {
|
|
188
|
-
const words = exec.toks.map(t => (t.t === 'word' ? t.u : ''));
|
|
189
|
-
const into = words.indexOf('INTO');
|
|
190
|
-
if (into < 0) return null;
|
|
191
|
-
let end = exec.toks.length;
|
|
192
|
-
for (let i = into + 1; i < words.length; i++) if (words[i] === 'FROM') { end = i; break; }
|
|
193
|
-
return [into, end];
|
|
194
|
-
}
|
|
195
|
-
|
|
196
176
|
export function analyze(root, opts = {}) {
|
|
197
177
|
const repos = opts.repos || [''];
|
|
198
178
|
const stats = { files: 0, programs: 0, edges: 0, nodes: 0, threw: 0, overBudget: 0, unreadable: 0, ebcdic: 0,
|
|
@@ -219,11 +199,12 @@ export function analyze(root, opts = {}) {
|
|
|
219
199
|
const jclPending = [];
|
|
220
200
|
// CICS definitions, from CSD extracts and from DFHCSDUP job input, and the estate's own statement
|
|
221
201
|
// of which DDs and queues reach the internal reader.
|
|
222
|
-
const csd = { tdqueues: new Map(), transactions: new Map() };
|
|
202
|
+
const csd = { tdqueues: new Map(), transactions: new Map(), urimaps: new Map() };
|
|
223
203
|
const addCsd = (text, file, lineBase = 0) => {
|
|
224
204
|
const c = parseCsd(text);
|
|
225
205
|
for (const [k, v] of c.tdqueues) if (!csd.tdqueues.has(k)) csd.tdqueues.set(k, v);
|
|
226
206
|
for (const [k, v] of c.transactions) if (!csd.transactions.has(k)) csd.transactions.set(k, { ...v, file, line: v.line + lineBase });
|
|
207
|
+
for (const [k, v] of c.urimaps) if (!csd.urimaps.has(k)) csd.urimaps.set(k, { ...v, file, line: v.line + lineBase });
|
|
227
208
|
};
|
|
228
209
|
const jobSteps = [];
|
|
229
210
|
// The maps of the repository being read, by MAPSET/MAP and by MAP alone.
|
|
@@ -286,8 +267,11 @@ export function analyze(root, opts = {}) {
|
|
|
286
267
|
// A system field the program never declares - the EIB's, or an SQLCA the precompiler supplies -
|
|
287
268
|
// still holds a value, so it needs a node of its own, as an index name does.
|
|
288
269
|
const systemNodes = new Map();
|
|
270
|
+
// A code on a terminal's error line is ordinary; Db2's message text names tables, columns and
|
|
271
|
+
// constraints, so only SQLERRMC reaches the screen sink.
|
|
289
272
|
const systemSource = (n, detail, at) => {
|
|
290
|
-
|
|
273
|
+
const onlyTo = /^SQLERRMC\b/.test(detail) ? [...TO_CLIENT, 'screen'] : TO_CLIENT;
|
|
274
|
+
if (!n.sources.some((x) => x.kind === 'system-response')) n.sources.push({ kind: 'system-response', onlyTo, noCredit: true, ...at, detail });
|
|
291
275
|
};
|
|
292
276
|
const systemNode = (tok) => {
|
|
293
277
|
let n = systemNodes.get(tok.u);
|
|
@@ -332,6 +316,13 @@ export function analyze(root, opts = {}) {
|
|
|
332
316
|
if (n) n.sinks.push({ kind: 'log', onlyFrom: REFLECTED, file: st.file, line: st.line, point: pointOf(st), detail: `DISPLAY ${n.name}${device}` });
|
|
333
317
|
}
|
|
334
318
|
}
|
|
319
|
+
// The document XML PARSE reads is the operand after PARSE; what the parser does with a DTD in
|
|
320
|
+
// it is the compiler's XMLPARSE option, which nothing here reads.
|
|
321
|
+
for (const st of prog.statements) {
|
|
322
|
+
if (st.verb !== 'XML' || !st.sources[0] || st.sources[0].u !== 'PARSE') continue;
|
|
323
|
+
const n = st.sources[1] ? nodeOfToken(st.sources[1]) : null;
|
|
324
|
+
if (n) n.sinks.push({ kind: 'xml-document', onlyFrom: REFLECTED, file: st.file, line: st.line, point: pointOf(st), detail: `XML PARSE ${n.name}` });
|
|
325
|
+
}
|
|
335
326
|
for (const it of prog.items) {
|
|
336
327
|
const n = nodeOf(it);
|
|
337
328
|
// A tainted child makes its group partly tainted; a tainted group taints every child. The
|
|
@@ -366,7 +357,7 @@ export function analyze(root, opts = {}) {
|
|
|
366
357
|
for (const it of prog.items) { const top = recordOf(it); if (!byRecord.has(top)) byRecord.set(top, []); byRecord.get(top).push(it); }
|
|
367
358
|
const extent = (it) => (it.offset != null && it.size != null ? [it.offset, it.offset + (it.contributes || it.size)] : null);
|
|
368
359
|
for (const c of ctl.checks) {
|
|
369
|
-
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 } : {}) };
|
|
360
|
+
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 } : {}) };
|
|
370
361
|
if (c.field.index) { (indexNode(c.field.index).checks ||= []).push(entry); continue; }
|
|
371
362
|
const span = extent(c.field);
|
|
372
363
|
for (const it of byRecord.get(recordOf(c.field)) || [c.field]) {
|
|
@@ -411,9 +402,16 @@ export function analyze(root, opts = {}) {
|
|
|
411
402
|
if (host.size == null || variable(host)) return null;
|
|
412
403
|
return (host.children || []).some((c) => c.level !== 88) ? host.size / Math.max(1, host.occurs || 1) : host.size;
|
|
413
404
|
};
|
|
405
|
+
// A whole number no sign is written on, which no value put in it can leave below 0.
|
|
406
|
+
const unsignedWhole = (it) => it.level !== 88 && !it.index && !(it.children || []).length
|
|
407
|
+
&& /^9+$/.test(String(it.picture || '').toUpperCase().replace(/(\w)\((\d+)\)/g, (_, ch, n) => ch.repeat(Number(n))));
|
|
414
408
|
const indexed = new Set();
|
|
415
409
|
const subscripting = new Map();
|
|
410
|
+
// Each subscript use of a name, with the other indices of the same reference: the loop-bound
|
|
411
|
+
// credit needs every one of them kept in range.
|
|
412
|
+
const usesOf = new Map();
|
|
416
413
|
for (const st of prog.statements) {
|
|
414
|
+
const refs = new Map();
|
|
417
415
|
for (const x of st.indexes || []) {
|
|
418
416
|
const host = itemOfToken(x.host);
|
|
419
417
|
const idx = itemOfToken(x.tok);
|
|
@@ -426,17 +424,34 @@ export function analyze(root, opts = {}) {
|
|
|
426
424
|
const key = `${kind}|${host.name}|${at.name}`;
|
|
427
425
|
if (table && !subscripting.has(at.name)) subscripting.set(at.name, { host, table });
|
|
428
426
|
const point = placeOf(x.tok, st);
|
|
429
|
-
|
|
427
|
+
if (table) {
|
|
428
|
+
const rec = { at: st.at, node: at, point, limit: limitOf(host, table), offset: x.offset || 0, mates: refs.get(x.host) || (refs.set(x.host, []), refs.get(x.host)) };
|
|
429
|
+
rec.mates.push(rec);
|
|
430
|
+
if (!usesOf.has(at.name)) usesOf.set(at.name, []);
|
|
431
|
+
usesOf.get(at.name).push(rec);
|
|
432
|
+
}
|
|
433
|
+
const offset = x.offset || 0;
|
|
434
|
+
// A constant length past the start, or a constant start before the length, takes that many bytes off the top.
|
|
435
|
+
const base = limitOf(host, table);
|
|
436
|
+
const limit = kind === 'reference-modification' && base != null && x.span ? base - x.span + 1 : base;
|
|
437
|
+
const use = `${key}|${point ?? `s${st.at}`}|${offset}`;
|
|
430
438
|
if (indexed.has(use)) continue;
|
|
431
439
|
indexed.add(use);
|
|
432
440
|
at.sinks.push({
|
|
433
|
-
kind, onlyFrom: FROM_OUTSIDE, file: st.file, line: st.line, point, group: `${pk}|${key}`, limit:
|
|
441
|
+
kind, onlyFrom: FROM_OUTSIDE, file: st.file, line: st.line, point, group: `${pk}|${key}`, limit, ...(offset ? { offset } : {}), ...(offset > 0 && idx && unsignedWhole(idx) ? { nonNeg: true } : {}), ...(ssrange ? { ssrange } : {}),
|
|
434
442
|
detail: kind === 'subscript'
|
|
435
443
|
? `${at.name} subscripts ${host.name}, a table of ${table.occurs}`
|
|
436
444
|
: `${at.name} sets the ${x.kind === 'refmod-offset' ? 'start' : 'length'} of a reference to ${host.name}, which is ${host.size} bytes`,
|
|
437
445
|
});
|
|
438
446
|
}
|
|
439
447
|
}
|
|
448
|
+
// The subscript uses a loop's body makes; every use of the name when the body can run code
|
|
449
|
+
// elsewhere, and none for a loop whose body is a paragraph.
|
|
450
|
+
const bodyUses = (st, uses) => {
|
|
451
|
+
const body = ctl && ctl.loopBody.get(st);
|
|
452
|
+
if (!body || !uses) return null;
|
|
453
|
+
return body.leaves ? uses : uses.filter((u) => u.at >= body.from && u.at < body.to);
|
|
454
|
+
};
|
|
440
455
|
// A counter that subscripts a table walks as far as its loop's condition lets it. PERFORM
|
|
441
456
|
// VARYING I ... UNTIL I > N with N from outside puts the table's bound in the caller's hands, and
|
|
442
457
|
// no subscript sink sees it: I itself is only ever a literal plus one. The subscript may be
|
|
@@ -454,7 +469,7 @@ export function analyze(root, opts = {}) {
|
|
|
454
469
|
if (indexed.has(use)) continue;
|
|
455
470
|
indexed.add(use);
|
|
456
471
|
nodeOf(it).sinks.push({
|
|
457
|
-
kind: 'loop-bound', onlyFrom: FROM_OUTSIDE, file: st.file, line: st.line, point: pointOf(st), group: `${pk}|${key}`, limit: limitOf(walks.host, walks.table), ...(ssrange ? { ssrange } : {}),
|
|
472
|
+
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 } : {}),
|
|
458
473
|
detail: `${it.name} decides where PERFORM VARYING ${counter} stops, and ${counter} subscripts ${walks.host.name}, a table of ${walks.table.occurs}`,
|
|
459
474
|
});
|
|
460
475
|
}
|
|
@@ -480,14 +495,27 @@ export function analyze(root, opts = {}) {
|
|
|
480
495
|
});
|
|
481
496
|
}
|
|
482
497
|
}
|
|
483
|
-
|
|
484
|
-
|
|
498
|
+
// The count matters wherever a statement names the table, a part of it, or a group holding it,
|
|
499
|
+
// so it is judged at each such statement; a table no statement names is judged where it is declared.
|
|
500
|
+
const odoTables = prog.items.filter((it) => it.dependingOn && byName.get(it.dependingOn));
|
|
501
|
+
const touching = new Map(odoTables.map((it) => [it, []]));
|
|
502
|
+
if (odoTables.length) {
|
|
503
|
+
const inside = (a, b) => { for (let x = a; x; x = x.parent) if (x === b) return true; return false; };
|
|
504
|
+
for (const st of prog.statements) {
|
|
505
|
+
const named = [...st.sources, ...st.targets, ...(st.indexes || []).map((x) => x.host)].map(itemOfToken).filter(Boolean);
|
|
506
|
+
for (const it of odoTables) if (named.some((n) => inside(n, it) || inside(it, n))) touching.get(it).push(st);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
for (const it of odoTables) {
|
|
485
510
|
const count = byName.get(it.dependingOn);
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
511
|
+
const sts = touching.get(it);
|
|
512
|
+
for (const st of sts.length ? sts : [null]) {
|
|
513
|
+
const point = st ? pointOf(st) : undefined;
|
|
514
|
+
nodeOf(count).sinks.push({
|
|
515
|
+
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 } : {}),
|
|
516
|
+
detail: `${count.name} sets how many ${it.name} entries there are, up to ${it.occurs}`,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
491
519
|
}
|
|
492
520
|
for (const st of prog.statements) {
|
|
493
521
|
if (st.verb !== 'READ' && st.verb !== 'RETURN') continue;
|
|
@@ -572,6 +600,16 @@ export function analyze(root, opts = {}) {
|
|
|
572
600
|
const a = c.using[MQ_BUFFER_ARG[String(c.name).toUpperCase()]];
|
|
573
601
|
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
574
602
|
if (n) n.sinks.push({ kind: 'message-queue', onlyFrom: DATA_AT_REST, file: c.file, line: c.line, point, detail: `CALL '${c.name}' with buffer ${a.word}` });
|
|
603
|
+
} else if (/^MQCONNX?$/i.test(String(c.name))) {
|
|
604
|
+
// MQCONN(QMgrName, Hconn, CompCode, Reason); MQCONNX puts its options second.
|
|
605
|
+
const a = c.using[0];
|
|
606
|
+
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
607
|
+
if (n) n.sinks.push({ 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` });
|
|
608
|
+
} else if (String(c.name).toUpperCase() === 'CEEGTST') {
|
|
609
|
+
// CEEGTST(heap id, size, address, feedback code): the size is the second argument.
|
|
610
|
+
const a = c.using[1];
|
|
611
|
+
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
612
|
+
if (n) n.sinks.push({ 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` });
|
|
575
613
|
}
|
|
576
614
|
}
|
|
577
615
|
const transfers = [];
|
|
@@ -663,14 +701,16 @@ export function analyze(root, opts = {}) {
|
|
|
663
701
|
walk(record);
|
|
664
702
|
for (const field of map.fields) {
|
|
665
703
|
if (!field.name) continue;
|
|
666
|
-
const marks = ['PROT', 'ASKIP', 'DRK', 'NUM'].filter((a) => field.effective.has(a));
|
|
667
|
-
if (!marks.length) continue;
|
|
668
704
|
const iname = symbolicNames(map, field).find((n) => n.suffix === 'I');
|
|
669
705
|
const item = iname && within.get(iname.name);
|
|
670
706
|
if (!item) continue;
|
|
707
|
+
const marks = ['PROT', 'ASKIP', 'DRK', 'NUM'].filter((a) => field.effective.has(a));
|
|
708
|
+
const typed = !marks.includes('PROT') && !marks.includes('ASKIP');
|
|
709
|
+
if (typed) nodeOf(item).typed = true;
|
|
710
|
+
if (!marks.length) continue;
|
|
671
711
|
const n = nodeOf(item);
|
|
672
712
|
n.screen = { item: item.name, field: field.name, map: map.name, mapset: mapset.name, marks, declared: !!field.attrb };
|
|
673
|
-
if (
|
|
713
|
+
if (typed) continue;
|
|
674
714
|
stats.protectedFields++;
|
|
675
715
|
const back = field.effective.has('FSET') ? ' with FSET' : '';
|
|
676
716
|
n.sources.push({ kind: 'cics-protected-field', onlyTo: ['record-key', 'record-update'], ...at,
|
|
@@ -714,6 +754,14 @@ export function analyze(root, opts = {}) {
|
|
|
714
754
|
}
|
|
715
755
|
if (!isWeb && o.has('MAP')) receiveMap(e, o, itemOfToken(intoTok), at);
|
|
716
756
|
}
|
|
757
|
+
// What the terminal shows: SEND TEXT's FROM, or the map's output record, <map>O without FROM.
|
|
758
|
+
if (verb === 'SEND' && (words[1] === 'TEXT' || words[1] === 'MAP')) {
|
|
759
|
+
const from = nodeOfToken((o.get('FROM') || [])[0]);
|
|
760
|
+
const map = words[1] === 'MAP' ? optionName(o, 'MAP') : null;
|
|
761
|
+
const sym = !from && map ? byName.get(`${map}O`) : null;
|
|
762
|
+
const n = from || (sym ? nodeOf(sym) : null);
|
|
763
|
+
if (n) n.sinks.push({ 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` });
|
|
764
|
+
}
|
|
717
765
|
// RIDFLD decides which record a READ or DELETE reaches, or where a browse starts. A key the
|
|
718
766
|
// user types is how a lookup works; a key the program put in a protected field and read back
|
|
719
767
|
// is its own state, held where a modified client can change it.
|
|
@@ -726,6 +774,12 @@ export function analyze(root, opts = {}) {
|
|
|
726
774
|
: `EXEC CICS ${verb} RIDFLD(${k && k.name}), which decides which record it reaches`;
|
|
727
775
|
if (k) k.sinks.push({ kind: verb === 'DELETE' || then ? 'record-update' : 'record-key', onlyFrom: ['cics-protected-field'], noCredit: true, ...at, detail });
|
|
728
776
|
}
|
|
777
|
+
if (verb === 'GETMAIN') {
|
|
778
|
+
for (const opt of ['FLENGTH', 'LENGTH']) {
|
|
779
|
+
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
780
|
+
if (n) n.sinks.push({ kind: 'storage-length', onlyFrom: REFLECTED, ...at, detail: `EXEC CICS GETMAIN ${opt}(${n.name}), which decides how much storage is acquired` });
|
|
781
|
+
}
|
|
782
|
+
}
|
|
729
783
|
if (verb === 'PUT' && o.has('CONTAINER')) {
|
|
730
784
|
const name = optionName(o, 'CONTAINER');
|
|
731
785
|
const from = nodeOfToken((o.get('FROM') || [])[0]);
|
|
@@ -794,6 +848,17 @@ export function analyze(root, opts = {}) {
|
|
|
794
848
|
if (n) n.sinks.push({ kind: 'queue-name', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS ${verb} ${ts ? 'TS' : 'TD'} ${opt}(...) names the queue` });
|
|
795
849
|
}
|
|
796
850
|
}
|
|
851
|
+
// SYSID ships a command to the region it names; SET's first option names the resource a
|
|
852
|
+
// system-programming command changes. A literal in either is the program's own choice. ASSIGN,
|
|
853
|
+
// INQUIRE and EXTRACT return SYSID into the field rather than read it.
|
|
854
|
+
if (o.has('SYSID') && !['ASSIGN', 'INQUIRE', 'EXTRACT'].includes(verb)) {
|
|
855
|
+
const n = nodeOfToken((o.get('SYSID') || [])[0]);
|
|
856
|
+
if (n) n.sinks.push({ kind: 'cics-sysid', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS ${verb} SYSID(${n.name}), which decides the region the command is shipped to` });
|
|
857
|
+
}
|
|
858
|
+
if (verb === 'SET' && words[1] && o.has(words[1])) {
|
|
859
|
+
const n = nodeOfToken((o.get(words[1]) || [])[0]);
|
|
860
|
+
if (n) n.sinks.push({ kind: 'cics-system-resource', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS SET ${words[1]}(${n.name}), which decides the resource the command changes` });
|
|
861
|
+
}
|
|
797
862
|
if (verb === 'READ' || verb === 'READNEXT' || verb === 'READPREV') {
|
|
798
863
|
const n = nodeOfToken(intoTok);
|
|
799
864
|
if (n && (o.has('FILE') || o.has('DATASET'))) n.sources.push({ kind: 'file-record', ...at, detail: `EXEC CICS ${verb} FILE ... INTO` });
|
|
@@ -835,15 +900,22 @@ export function analyze(root, opts = {}) {
|
|
|
835
900
|
}
|
|
836
901
|
}
|
|
837
902
|
if (e.kind === 'SQL') {
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
const hv = e.hostVariables ||
|
|
903
|
+
// A host variable names the item a qualified :G.F resolves to; the statement fills the ones
|
|
904
|
+
// Db2 writes, and those hold what the database held.
|
|
905
|
+
const hv = e.hostVariables || hostVariablesIn(e.toks);
|
|
841
906
|
const verb = words[0];
|
|
842
|
-
const range = e.hostVariables ? null : ['SELECT', 'FETCH'].includes(verb) && sqlIntoRange(e);
|
|
843
907
|
for (const h of hv) {
|
|
844
|
-
if (
|
|
908
|
+
if (!h.written) continue;
|
|
845
909
|
const n = nodeOfToken(h.tok);
|
|
846
|
-
if (n) n.sources.push({ kind: 'database', ...at, detail: `EXEC SQL ${verb} INTO host variable` });
|
|
910
|
+
if (n) n.sources.push({ kind: 'database', ...at, detail: `EXEC SQL ${verb} ${verb === 'SELECT' || verb === 'FETCH' ? 'INTO' : 'writes'} host variable` });
|
|
911
|
+
}
|
|
912
|
+
// The location CONNECT TO or SET CONNECTION names, where a host variable holds it; what
|
|
913
|
+
// follows USER and USING is the credential, not the target.
|
|
914
|
+
if (verb === 'CONNECT' || (verb === 'SET' && words[1] === 'CONNECTION')) {
|
|
915
|
+
const to = e.toks.findIndex((t) => t.t === 'word' && (t.u === 'TO' || t.u === 'CONNECTION'));
|
|
916
|
+
const colon = to >= 0 ? e.toks[to + 1] : null;
|
|
917
|
+
const n = colon && colon.t === 'op' && colon.v === ':' && e.toks[to + 2] ? nodeOfToken(e.toks[to + 2]) : null;
|
|
918
|
+
if (n) n.sinks.push({ 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` });
|
|
847
919
|
}
|
|
848
920
|
const dynamic = words.some((w, i) => w === 'PREPARE' || (w === 'EXECUTE' && words[i + 1] === 'IMMEDIATE'));
|
|
849
921
|
if (dynamic) for (const h of hv) {
|
|
@@ -860,7 +932,7 @@ export function analyze(root, opts = {}) {
|
|
|
860
932
|
}
|
|
861
933
|
const commareaItem = byName.get('DFHCOMMAREA');
|
|
862
934
|
return {
|
|
863
|
-
id: prog.id, pk, callSites, transfers, ddFiles, tdWrites,
|
|
935
|
+
id: prog.id, pk, file, callSites, transfers, ddFiles, tdWrites,
|
|
864
936
|
params: prog.paramTokens ? prog.paramTokens.map(p => {
|
|
865
937
|
const it = itemOfToken(p.tok);
|
|
866
938
|
const extent = extentOf(it);
|
|
@@ -929,27 +1001,55 @@ export function analyze(root, opts = {}) {
|
|
|
929
1001
|
|
|
930
1002
|
const byId = new Map();
|
|
931
1003
|
const idCount = new Map();
|
|
1004
|
+
const allById = new Map();
|
|
932
1005
|
for (const p of programs) {
|
|
933
1006
|
if (p.id && !byId.has(p.id)) byId.set(p.id, p);
|
|
934
|
-
if (p.id) idCount.set(p.id, (idCount.get(p.id) || 0) + 1);
|
|
1007
|
+
if (p.id) { idCount.set(p.id, (idCount.get(p.id) || 0) + 1); (allById.get(p.id) || allById.set(p.id, []).get(p.id)).push(p); }
|
|
935
1008
|
}
|
|
1009
|
+
// The programs a CALL or LINK may reach. A program id the repository holds once is that program.
|
|
1010
|
+
// Held several times, the one in the caller's own source file is the one its executable links;
|
|
1011
|
+
// with none there, the holders nearest in the directory tree are, since a copy of a program tree
|
|
1012
|
+
// or a sibling executable's copy is farther than the caller's own build directory. Ties keep every
|
|
1013
|
+
// holder, and the edge says how many candidates there were.
|
|
1014
|
+
const sharedDirs = (a, b) => {
|
|
1015
|
+
const x = a.split(/[\\/]/), y = b.split(/[\\/]/);
|
|
1016
|
+
let n = 0;
|
|
1017
|
+
while (n < x.length - 1 && n < y.length - 1 && x[n] === y[n]) n++;
|
|
1018
|
+
return n;
|
|
1019
|
+
};
|
|
1020
|
+
const calleesOf = (name, fromFile) => {
|
|
1021
|
+
const all = allById.get(name) || [];
|
|
1022
|
+
if (all.length < 2) return { list: all, of: 0 };
|
|
1023
|
+
const here = all.filter((q) => q.file === fromFile);
|
|
1024
|
+
if (here.length) return { list: here, of: 0 };
|
|
1025
|
+
const depth = all.map((q) => sharedDirs(q.file, fromFile));
|
|
1026
|
+
const best = Math.max(...depth);
|
|
1027
|
+
return { list: all.filter((_, i) => depth[i] === best), of: all.length };
|
|
1028
|
+
};
|
|
1029
|
+
const ofN = (r, name) => (r.of ? `, ${r.list.length === r.of ? 'any' : 'nearest'} of ${r.of} programs named ${name}` : '');
|
|
936
1030
|
const constructs = [];
|
|
937
1031
|
for (const p of programs) {
|
|
938
1032
|
for (const t of p.transfers) {
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1033
|
+
const r = calleesOf(t.callee, p.file);
|
|
1034
|
+
for (const callee of r.list) {
|
|
1035
|
+
if (!callee.commarea) continue;
|
|
1036
|
+
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);
|
|
1037
|
+
if (t.verb === 'LINK') edge(callee.commarea, t.argNode, `COMMAREA returned from ${t.callee}${ofN(r, t.callee)}`);
|
|
1038
|
+
}
|
|
943
1039
|
}
|
|
944
1040
|
for (const c of p.callSites) {
|
|
945
|
-
const
|
|
1041
|
+
const r = calleesOf(c.name, p.file);
|
|
1042
|
+
for (const callee of r.list) {
|
|
1043
|
+
if (!callee.params) continue;
|
|
1044
|
+
c.args.forEach((arg, i) => {
|
|
1045
|
+
const param = callee.params[i];
|
|
1046
|
+
if (!param || !arg.node || !param.node) return;
|
|
1047
|
+
edge(arg.node, param.node, `CALL '${c.display}' argument ${i + 1} at ${rel(c.file)}:${c.line}${ofN(r, c.display)}`, undefined, c.point);
|
|
1048
|
+
if (arg.mode === 'REFERENCE' && param.mode === 'REFERENCE') edge(param.node, arg.node, `CALL '${c.display}' argument ${i + 1} written back${ofN(r, c.display)}`);
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
const callee = r.list.length === 1 ? r.list[0] : null;
|
|
946
1052
|
if (!callee || !callee.params) continue;
|
|
947
|
-
c.args.forEach((arg, i) => {
|
|
948
|
-
const param = callee.params[i];
|
|
949
|
-
if (!param || !arg.node || !param.node) return;
|
|
950
|
-
edge(arg.node, param.node, `CALL '${c.display}' argument ${i + 1} at ${rel(c.file)}:${c.line}`, undefined, c.point);
|
|
951
|
-
if (arg.mode === 'REFERENCE' && param.mode === 'REFERENCE') edge(param.node, arg.node, `CALL '${c.display}' argument ${i + 1} written back`);
|
|
952
|
-
});
|
|
953
1053
|
// A callee that declares more bytes than its caller passes reads and writes whatever follows
|
|
954
1054
|
// the argument in the caller's storage. Measured over 8,640 bound pairs in 125 repositories:
|
|
955
1055
|
// where the extra bytes stay inside the caller's own record they are its neighbouring fields,
|
|
@@ -1001,53 +1101,60 @@ export function analyze(root, opts = {}) {
|
|
|
1001
1101
|
for (const dd of step.dds) if (dd.name && dd.name.toUpperCase() === 'SYSIN' && dd.inStream) addCsd(dd.inStream.map((l) => l.text).join('\n'), f, dd.inStream[0].line - 1);
|
|
1002
1102
|
}
|
|
1003
1103
|
jobSteps.push({ file: f, job: job.jobs[0]?.name || null, step: step.name, line: step.line, pgm: step.pgm.toUpperCase() });
|
|
1004
|
-
// A TSO batch step starts
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
const callee = byId.get(step.pgm.toUpperCase());
|
|
1012
|
-
if (!callee) continue; // a system utility, or a program not in this tree
|
|
1013
|
-
stats.jclStepsResolved++;
|
|
1014
|
-
|
|
1015
|
-
// PARM arrives in the first PROCEDURE DIVISION USING item: on z/OS a halfword length
|
|
1016
|
-
// followed by the text. A program with no USING cannot receive one, and saying it does
|
|
1017
|
-
// would be a path nobody could follow.
|
|
1018
|
-
if (step.parm !== null && callee.params && callee.params[0] && callee.params[0].node) {
|
|
1019
|
-
callee.params[0].node.sources.push({
|
|
1020
|
-
kind: 'jcl-parm', file: f, line: step.line,
|
|
1021
|
-
detail: `PARM= on step ${step.name || '(unnamed)'} of ${rel(f)}, which runs ${step.pgm}`,
|
|
1022
|
-
});
|
|
1023
|
-
stats.jclCrossings++;
|
|
1104
|
+
// A TSO batch step starts programs by name in its commands, TSO CALL and DSN RUN PROGRAM, and
|
|
1105
|
+
// each is handed the step's DDs and its own parameter as EXEC PGM= would be.
|
|
1106
|
+
const runs = [{ pgm: step.pgm.toUpperCase(), parm: step.parm, line: step.line, how: `PARM= on step ${step.name || '(unnamed)'} of ${rel(f)}` }];
|
|
1107
|
+
for (const r of tsoCommands(step).runs) {
|
|
1108
|
+
jobSteps.push({ file: f, job: job.jobs[0]?.name || null, step: step.name, line: r.line, pgm: r.program });
|
|
1109
|
+
runs.push({ pgm: r.program, parm: r.parm, line: r.line, how: `the parameter ${r.via} passes in step ${step.name || '(unnamed)'} of ${rel(f)}` });
|
|
1024
1110
|
}
|
|
1111
|
+
for (const run of runs) {
|
|
1112
|
+
const holders = allById.get(run.pgm) || [];
|
|
1113
|
+
if (!holders.length) continue; // a system utility, or a program not in this tree
|
|
1114
|
+
stats.jclStepsResolved++;
|
|
1115
|
+
// A program id held several times has no caller file to choose by, so each holder is a candidate.
|
|
1116
|
+
const amb = holders.length > 1 ? `, any of ${holders.length} programs named ${run.pgm}` : '';
|
|
1117
|
+
for (const callee of holders) {
|
|
1025
1118
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
for (const m of callee.ddFiles || []) {
|
|
1034
|
-
if (m.dd !== dd.name.toUpperCase()) continue;
|
|
1035
|
-
for (const n of m.nodes) {
|
|
1036
|
-
n.sinks.push({ kind: 'internal-reader', file: m.file, line: m.line,
|
|
1037
|
-
detail: `records written through SELECT ${m.select} to //${dd.name}, which step ${step.name || '(unnamed)'} of ${rel(f)} sends to the internal reader` });
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
if (!dd.inStream || !dd.inStream.length || !dd.name) continue;
|
|
1042
|
-
for (const m of callee.ddFiles || []) {
|
|
1043
|
-
if (m.dd !== dd.name.toUpperCase()) continue;
|
|
1044
|
-
for (const n of m.nodes) {
|
|
1045
|
-
n.sources.push({
|
|
1046
|
-
kind: 'jcl-instream', file: f, line: dd.line,
|
|
1047
|
-
detail: `${dd.inStream.length} lines of in-stream data on //${dd.name} in step ${step.name || '(unnamed)'}, read through SELECT ${m.select}`,
|
|
1119
|
+
// PARM arrives in the first PROCEDURE DIVISION USING item: on z/OS a halfword length
|
|
1120
|
+
// followed by the text. A program with no USING cannot receive one, and saying it does
|
|
1121
|
+
// would be a path nobody could follow.
|
|
1122
|
+
if (run.parm !== null && callee.params && callee.params[0] && callee.params[0].node) {
|
|
1123
|
+
callee.params[0].node.sources.push({
|
|
1124
|
+
kind: 'jcl-parm', file: f, line: run.line,
|
|
1125
|
+
detail: `${run.how}, which runs ${run.pgm}${amb}`,
|
|
1048
1126
|
});
|
|
1049
1127
|
stats.jclCrossings++;
|
|
1050
1128
|
}
|
|
1129
|
+
|
|
1130
|
+
// In-stream data reaches whatever record the program reads from that DD. The COBOL says
|
|
1131
|
+
// ASSIGN TO SYSIN and the job says //SYSIN DD *; neither half names the other, and the
|
|
1132
|
+
// join is the whole point of reading both.
|
|
1133
|
+
for (const dd of step.dds) {
|
|
1134
|
+
// The same join in the other direction: what the program writes through SELECT ... ASSIGN
|
|
1135
|
+
// TO a DD the job sends to the internal reader is submitted as a job.
|
|
1136
|
+
if (dd.name && dd.sysout && /\bINTRDR\b/i.test(dd.sysout)) {
|
|
1137
|
+
for (const m of callee.ddFiles || []) {
|
|
1138
|
+
if (m.dd !== dd.name.toUpperCase()) continue;
|
|
1139
|
+
for (const n of m.nodes) {
|
|
1140
|
+
n.sinks.push({ kind: 'internal-reader', file: m.file, line: m.line,
|
|
1141
|
+
detail: `records written through SELECT ${m.select} to //${dd.name}, which step ${step.name || '(unnamed)'} of ${rel(f)} sends to the internal reader${amb}` });
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (!dd.inStream || !dd.inStream.length || !dd.name) continue;
|
|
1146
|
+
for (const m of callee.ddFiles || []) {
|
|
1147
|
+
if (m.dd !== dd.name.toUpperCase()) continue;
|
|
1148
|
+
for (const n of m.nodes) {
|
|
1149
|
+
n.sources.push({
|
|
1150
|
+
kind: 'jcl-instream', file: f, line: dd.line,
|
|
1151
|
+
detail: `${dd.inStream.length} lines of in-stream data on //${dd.name} in step ${step.name || '(unnamed)'}, read through SELECT ${m.select}${amb}`,
|
|
1152
|
+
});
|
|
1153
|
+
stats.jclCrossings++;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
}
|
|
1051
1158
|
}
|
|
1052
1159
|
}
|
|
1053
1160
|
}
|
|
@@ -1059,6 +1166,12 @@ export function analyze(root, opts = {}) {
|
|
|
1059
1166
|
// in this tree reaches.
|
|
1060
1167
|
const roots = [];
|
|
1061
1168
|
for (const [name, t] of csd.transactions) if (t.program && byId.has(t.program)) roots.push({ transaction: name, file: rel(t.file), line: t.line, program: t.program });
|
|
1169
|
+
// A URI map serves its program to an HTTP request under an alias transaction, CWBA unless it names
|
|
1170
|
+
// one, and that transaction is what the region checks the caller may start.
|
|
1171
|
+
for (const [name, u] of csd.urimaps) {
|
|
1172
|
+
if ((u.usage && u.usage !== 'SERVER') || !u.program || !byId.has(u.program)) continue;
|
|
1173
|
+
roots.push({ transaction: u.transaction || 'CWBA', urimap: name, ...(u.path ? { uri: u.path } : {}), file: rel(u.file), line: u.line, program: u.program });
|
|
1174
|
+
}
|
|
1062
1175
|
for (const j of jobSteps) if (byId.has(j.pgm)) roots.push({ job: j.job, step: j.step, file: rel(j.file), line: j.line, program: j.pgm });
|
|
1063
1176
|
const STARTED_BY_LISTED = 8;
|
|
1064
1177
|
for (const r of roots) {
|
|
@@ -1162,25 +1275,45 @@ export function analyze(root, opts = {}) {
|
|
|
1162
1275
|
const moves = (e) => !(typeof e.why === 'string' && STRUCTURAL.has(e.why));
|
|
1163
1276
|
const NONE = { level: 0, check: null };
|
|
1164
1277
|
// `limit` is how many entries or bytes the sink's index may name, where one number says it.
|
|
1165
|
-
function creditAt(n, point, kind, limit = null) {
|
|
1278
|
+
function creditAt(n, point, kind, limit = null, offset = 0, nonNeg = false) {
|
|
1166
1279
|
const p = programs[n.pk];
|
|
1167
1280
|
if (!p || !p.ordered) return n.guard ? { level: 1, check: n.guard } : NONE;
|
|
1168
1281
|
if (!n.checks || point == null) return NONE;
|
|
1169
1282
|
const bits = p.facts.get(point);
|
|
1170
|
-
return bits ? creditOf(n.checks, bits, kind, limit) : NONE;
|
|
1283
|
+
return bits ? creditOf(n.checks, bits, kind, limit, offset, nonNeg) : NONE;
|
|
1284
|
+
}
|
|
1285
|
+
// A loop bound cannot push its counter out of the table when the loop's own condition or the
|
|
1286
|
+
// body's checks keep every subscript the counter makes, and each index beside it, in range.
|
|
1287
|
+
// That holds whatever the bound is, so it does not depend on the route the input took.
|
|
1288
|
+
function counterCredit(sink) {
|
|
1289
|
+
const uses = sink.kind === 'loop-bound' ? sink.counterUses : null;
|
|
1290
|
+
if (!uses || !uses.length) return null;
|
|
1291
|
+
let first = null;
|
|
1292
|
+
for (const u of uses) {
|
|
1293
|
+
for (const m of u.mates) {
|
|
1294
|
+
const c = creditAt(m.node, m.point, 'subscript', m.limit, m.offset || 0);
|
|
1295
|
+
if (c.level < 2 || m.limit == null || !within(c.cons, m.limit)) return null;
|
|
1296
|
+
if (m === u && !first) first = { credit: c, node: m.node };
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return first;
|
|
1171
1300
|
}
|
|
1172
1301
|
// The best credit on a route. Each node is judged at the statement its value leaves by, which for
|
|
1173
1302
|
// the last is the sink's own. A structural hop - into a group, across a REDEFINES - moves no value
|
|
1174
1303
|
// in time, so the node before it is judged where the value next moves.
|
|
1175
1304
|
function routeCredit(hops, sink, src) {
|
|
1176
1305
|
if (sink.noCredit || (src && src.noCredit)) return { best: NONE, by: null, missed: null };
|
|
1306
|
+
const own = counterCredit(sink);
|
|
1307
|
+
if (own) return { best: own.credit, by: own.node, missed: null, own: true };
|
|
1177
1308
|
let best = NONE;
|
|
1178
1309
|
let by = null;
|
|
1179
1310
|
let missed = null;
|
|
1180
1311
|
let point = sink.point;
|
|
1181
1312
|
for (let h = hops.length - 1; h >= 0; h--) {
|
|
1182
1313
|
const n = hops[h].node;
|
|
1183
|
-
|
|
1314
|
+
let c = creditAt(n, point, sink.kind, sink.limit, sink.offset, sink.nonNeg);
|
|
1315
|
+
// A count checked against a number above the table's maximum has been checked, not kept in range.
|
|
1316
|
+
if (c.level === 2 && sink.top != null && !within(c.cons, sink.top, false)) c = { level: 1, check: c.check };
|
|
1184
1317
|
if (c.level > best.level) { best = c; by = n; }
|
|
1185
1318
|
const any = (n.checks && n.checks.find((c) => c.x != null)) || n.guard;
|
|
1186
1319
|
if (!c.level && any && !missed) missed = { node: n, check: any };
|
|
@@ -1191,16 +1324,16 @@ export function analyze(root, opts = {}) {
|
|
|
1191
1324
|
// The credited findings of one source whose sink can be reached without the credit: walking from
|
|
1192
1325
|
// the source, a value may not leave a node - or any node it reached by a structural hop since it
|
|
1193
1326
|
// last moved - at a point where the credit reaches `level`.
|
|
1194
|
-
function refuse(start, reach, credited, level, kind) {
|
|
1327
|
+
function refuse(start, reach, credited, level, kind, shift = 0) {
|
|
1195
1328
|
walk++;
|
|
1196
|
-
const want = credited.filter((c) => c.level >= level && c.sink.kind === kind);
|
|
1329
|
+
const want = credited.filter((c) => c.level >= level && c.sink.kind === kind && (c.sink.offset || 0) === shift);
|
|
1197
1330
|
// Past the budget no route can be ruled out, so every credit in question is lost: a finding
|
|
1198
1331
|
// may be reported unchecked, never dropped.
|
|
1199
1332
|
if (edgesWalked >= totalBudget) { stats.creditUndecided++; return new Set(want); }
|
|
1200
1333
|
const at = new Map();
|
|
1201
1334
|
for (const c of want) { if (!at.has(c.node.id)) at.set(c.node.id, []); at.get(c.node.id).push(c); }
|
|
1202
1335
|
// Along the way no sink's limit applies, so a bound that holds only against one blocks nothing.
|
|
1203
|
-
const holds = (since, point, k, limit = null) => since.some((n) => creditAt(n, point, k, limit).level >= level);
|
|
1336
|
+
const holds = (since, point, k, limit = null, offset = 0, nonNeg = false) => since.some((n) => creditAt(n, point, k, limit, offset, nonNeg).level >= level);
|
|
1204
1337
|
const lost = new Set();
|
|
1205
1338
|
const dirty = new Map();
|
|
1206
1339
|
const parts = new Map();
|
|
@@ -1209,13 +1342,13 @@ export function analyze(root, opts = {}) {
|
|
|
1209
1342
|
const began = edgesWalked;
|
|
1210
1343
|
for (let i = 0; i < open.length; i++) {
|
|
1211
1344
|
const st = open[i];
|
|
1212
|
-
for (const c of at.get(st.node.id) || []) if (!holds(st.since, c.sink.point, c.sink.kind, c.sink.limit)) lost.add(c);
|
|
1345
|
+
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)) lost.add(c);
|
|
1213
1346
|
for (const e of st.node.edgesOut) {
|
|
1214
1347
|
edgesWalked++;
|
|
1215
1348
|
const to = e.to;
|
|
1216
1349
|
if (!(canReach[to.id] & reach)) continue;
|
|
1217
1350
|
const mv = moves(e);
|
|
1218
|
-
if (mv && holds(st.since, pointOfEdge(e), kind)) continue;
|
|
1351
|
+
if (mv && holds(st.since, pointOfEdge(e), kind, null, shift)) continue;
|
|
1219
1352
|
const t = cross(st, e);
|
|
1220
1353
|
if (!t) continue;
|
|
1221
1354
|
const since = mv ? [to] : [...st.since, to];
|
|
@@ -1341,6 +1474,9 @@ export function analyze(root, opts = {}) {
|
|
|
1341
1474
|
// arithmetic or a numeric function carries no bad bytes to the next one. Only this route is
|
|
1342
1475
|
// dropped: a later one that copies the bytes still reports.
|
|
1343
1476
|
if (sink.kind === 'arithmetic' && computedOnRoute(state)) continue;
|
|
1477
|
+
// A protected field's value carried through a field the terminal may type into reaches the
|
|
1478
|
+
// key as typed input, which the user could have entered anyway: the protection was no control.
|
|
1479
|
+
if (src.kind === 'cics-protected-field' && typedOnRoute(state)) continue;
|
|
1344
1480
|
seen.add(key);
|
|
1345
1481
|
// The ends of a long path are what a reader uses: where the value came from, and what it
|
|
1346
1482
|
// reached. Keeping every hop of a thousand-hop chain, for a thousand findings, is the
|
|
@@ -1354,7 +1490,7 @@ export function analyze(root, opts = {}) {
|
|
|
1354
1490
|
: [...hops.slice(0, TRACE_KEEP).map(hop),
|
|
1355
1491
|
{ program: null, item: null, file: null, via: `… ${hops.length - TRACE_KEEP * 2} hops not listed`, elided: hops.length - TRACE_KEEP * 2 },
|
|
1356
1492
|
...hops.slice(-TRACE_KEEP).map(hop)];
|
|
1357
|
-
const { best, by, missed } = routeCredit(hops, sink, src);
|
|
1493
|
+
const { best, by, missed, own } = routeCredit(hops, sink, src);
|
|
1358
1494
|
const screen = hops.find((h) => h.node.screen)?.node.screen;
|
|
1359
1495
|
findings.push({
|
|
1360
1496
|
rule: `${src.kind}-to-${sink.kind}`,
|
|
@@ -1370,7 +1506,7 @@ export function analyze(root, opts = {}) {
|
|
|
1370
1506
|
...(best.level ? { guard: { program: by.program, item: by.name, file: rel(best.check.file), line: best.check.line, ...(best.level === 2 ? { stops: true } : {}) } } : {}),
|
|
1371
1507
|
...(!best.level && missed ? { checkElsewhere: { program: missed.node.program, item: missed.node.name, file: rel(missed.check.file), line: missed.check.line } } : {}),
|
|
1372
1508
|
});
|
|
1373
|
-
if (best.level) credited.push({ finding: findings[findings.length - 1], node: cur, sink, level: best.level });
|
|
1509
|
+
if (best.level && !own) credited.push({ finding: findings[findings.length - 1], node: cur, sink, level: best.level });
|
|
1374
1510
|
if (sink.group) useGroup.set(findings[findings.length - 1], `${src.kind}|${src.file}:${src.line}|${sink.kind}|${sink.group}`);
|
|
1375
1511
|
}
|
|
1376
1512
|
for (const e of cur.edgesOut) {
|
|
@@ -1399,16 +1535,23 @@ export function analyze(root, opts = {}) {
|
|
|
1399
1535
|
// cross() like the first walk, so it cannot take a route the first would refuse.
|
|
1400
1536
|
if (credited.length) {
|
|
1401
1537
|
// Per kind of sink, because what a check leaves may stop one kind and say nothing to another.
|
|
1538
|
+
// And per shift, because a value that stops a bare index does not stop one moved down by a constant.
|
|
1539
|
+
const shiftsOf = (list, kind) => new Set(list.filter((c) => c.sink.kind === kind).map((c) => c.sink.offset || 0));
|
|
1402
1540
|
for (const kind of new Set(credited.map((c) => c.sink.kind))) {
|
|
1403
|
-
for (const
|
|
1404
|
-
const
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1541
|
+
for (const shift of shiftsOf(credited, kind)) {
|
|
1542
|
+
for (const c of refuse(start, reach, credited, 1, kind, shift)) {
|
|
1543
|
+
const g = c.finding.guard;
|
|
1544
|
+
delete c.finding.guard;
|
|
1545
|
+
c.finding.checkElsewhere = { program: g.program, item: g.item, file: g.file, line: g.line };
|
|
1546
|
+
c.level = 0;
|
|
1547
|
+
}
|
|
1408
1548
|
}
|
|
1409
1549
|
}
|
|
1410
1550
|
for (const kind of new Set(credited.filter((c) => c.level === 2).map((c) => c.sink.kind))) {
|
|
1411
|
-
|
|
1551
|
+
const two = credited.filter((c) => c.level === 2);
|
|
1552
|
+
for (const shift of shiftsOf(two, kind)) {
|
|
1553
|
+
for (const c of refuse(start, reach, credited, 2, kind, shift)) if (c.level === 2) { delete c.finding.guard.stops; c.level = 1; }
|
|
1554
|
+
}
|
|
1412
1555
|
}
|
|
1413
1556
|
}
|
|
1414
1557
|
}
|
|
@@ -1423,6 +1566,13 @@ export function analyze(root, opts = {}) {
|
|
|
1423
1566
|
if (!k || rank(f) < rank(k) || (rank(f) === rank(k) && (byText(f.sink.file, k.sink.file) || f.sink.line - k.sink.line) < 0)) kept.set(key, f);
|
|
1424
1567
|
}
|
|
1425
1568
|
const keep = new Set(kept.values());
|
|
1569
|
+
// The uses left out are named on the one kept, with how far each is checked, so a reader can see them.
|
|
1570
|
+
const level = (f) => (f.guard ? (f.guard.stops ? 2 : 1) : 0);
|
|
1571
|
+
for (const [f, key] of useGroup) {
|
|
1572
|
+
if (keep.has(f)) continue;
|
|
1573
|
+
const k = kept.get(key);
|
|
1574
|
+
(k.also ||= []).push({ file: f.sink.file, line: f.sink.line, program: f.sink.program, detail: f.sink.detail, level: level(f), unreached: !!f.unreached, ...(f.guard ? { guardLine: f.guard.line } : {}) });
|
|
1575
|
+
}
|
|
1426
1576
|
let w = 0;
|
|
1427
1577
|
for (const f of findings) if (!useGroup.has(f) || keep.has(f)) findings[w++] = f;
|
|
1428
1578
|
findings.length = w;
|
|
@@ -1455,7 +1605,7 @@ export function analyze(root, opts = {}) {
|
|
|
1455
1605
|
}
|
|
1456
1606
|
}
|
|
1457
1607
|
for (const c of constructs) { Object.assign(c, startedByOf(c.pk)); delete c.pk; }
|
|
1458
|
-
stats.entryPoints = { transactions: csd.transactions.size, jobSteps: jobSteps.length, roots: roots.length, programsStarted: programs.filter((p) => p.startedBy).length, programsWithoutEntry: unstarted.length };
|
|
1608
|
+
stats.entryPoints = { transactions: csd.transactions.size, uriMaps: csd.urimaps.size, jobSteps: jobSteps.length, roots: roots.length, programsStarted: programs.filter((p) => p.startedBy).length, programsWithoutEntry: unstarted.length };
|
|
1459
1609
|
const startedBy = {};
|
|
1460
1610
|
for (const p of programs) if (p.id && p.startedBy && !startedBy[p.id]) startedBy[p.id] = p.startedBy;
|
|
1461
1611
|
return { findings, constructs, unstarted, startedBy, stats, sourceKinds: SOURCE_KINDS, sinkKinds: SINK_KINDS, ...(sinks ? { sinks } : {}), ...(sources ? { sources } : {}) };
|