@portll/cobolwork 0.2.75 → 0.2.117
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 +69 -15
- 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/consequence.mjs +29 -0
- package/lib/control.mjs +1052 -91
- package/lib/csd.mjs +17 -2
- package/lib/dataflow.mjs +245 -78
- 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 +85 -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 +4 -1
- package/lib/exploitability.mjs +263 -0
- package/lib/ironwork.mjs +111 -0
- package/lib/kernel/findings.mjs +11 -0
- package/lib/kernel/registry.mjs +4 -0
- package/lib/option-diff.mjs +49 -0
- package/lib/options.mjs +109 -46
- package/lib/parser.mjs +29 -7
- package/lib/policy.mjs +8 -3
- package/lib/precompile.mjs +4 -0
- package/lib/reach.mjs +29 -12
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +1 -0
- package/lib/sbom.mjs +147 -0
- package/lib/scan.mjs +93 -27
- package/lib/sets/cics.mjs +114 -0
- package/lib/sets/flow.mjs +82 -2
- package/lib/sets/jcl.mjs +67 -0
- package/lib/sets/semantics.mjs +377 -0
- package/lib/sets/web.mjs +34 -10
- package/lib/sets/zowe.mjs +220 -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 +85 -13
- 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 +2 -1
package/lib/csd.mjs
CHANGED
|
@@ -13,11 +13,13 @@ const COMMAND = /^\s*(DEFINE|ADD|ALTER|APPEND|COPY|DELETE|INITIALIZE|LIST|REMOVE
|
|
|
13
13
|
|
|
14
14
|
// { tdqueues: Map(name -> { type, ddname, indirect, line }),
|
|
15
15
|
// transactions: Map(name -> { program, cmdsec, ressec, line }),
|
|
16
|
-
// tcpipservices: Map(name -> { port, protocol, ssl, transaction, line })
|
|
16
|
+
// tcpipservices: Map(name -> { port, protocol, ssl, authenticate, transaction, line }),
|
|
17
|
+
// urimaps: Map(name -> { usage, path, program, transaction, tcpipservice, line }) }
|
|
17
18
|
export function parseCsd(text) {
|
|
18
19
|
const tdqueues = new Map();
|
|
19
20
|
const transactions = new Map();
|
|
20
21
|
const tcpipservices = new Map();
|
|
22
|
+
const urimaps = new Map();
|
|
21
23
|
const blocks = [];
|
|
22
24
|
let cur = null;
|
|
23
25
|
const lines = String(text).split(/\r?\n/);
|
|
@@ -48,9 +50,22 @@ export function parseCsd(text) {
|
|
|
48
50
|
port: attrs.get('PORTNUMBER') || null,
|
|
49
51
|
protocol: attrs.get('PROTOCOL') || null,
|
|
50
52
|
ssl: attrs.get('SSL') || null,
|
|
53
|
+
// Absent is NO, as for SSL: a request arrives under the region's default user.
|
|
54
|
+
authenticate: attrs.get('AUTHENTICATE') || null,
|
|
51
55
|
transaction: attrs.get('TRANSACTION') || null,
|
|
52
56
|
line: b.line,
|
|
53
57
|
});
|
|
58
|
+
} else if (kind === 'URIMAP') {
|
|
59
|
+
// PATH keeps its case: a URI path is case-sensitive, and it is printed, not compared.
|
|
60
|
+
const path = /\bPATH\s*\(([^)]{0,256})\)/i.exec(b.text.slice(head[0].length));
|
|
61
|
+
urimaps.set(name, {
|
|
62
|
+
usage: attrs.get('USAGE') || null,
|
|
63
|
+
path: path ? path[1].trim() : null,
|
|
64
|
+
program: attrs.get('PROGRAM') || null,
|
|
65
|
+
transaction: attrs.get('TRANSACTION') || null,
|
|
66
|
+
tcpipservice: attrs.get('TCPIPSERVICE') || null,
|
|
67
|
+
line: b.line,
|
|
68
|
+
});
|
|
54
69
|
} else if (kind === 'TDQUEUE') {
|
|
55
70
|
tdqueues.set(name, { type: attrs.get('TYPE') || null, ddname: attrs.get('DDNAME') || null, indirect: attrs.get('INDIRECTNAME') || null, transid: attrs.get('TRANSID') || null, line: b.line });
|
|
56
71
|
} else if (kind === 'TRANSACTION') {
|
|
@@ -65,7 +80,7 @@ export function parseCsd(text) {
|
|
|
65
80
|
});
|
|
66
81
|
}
|
|
67
82
|
}
|
|
68
|
-
return { tdqueues, transactions, tcpipservices };
|
|
83
|
+
return { tdqueues, transactions, tcpipservices, urimaps };
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
// The DD a queue is written to, following one INDIRECT hop. Null when the definition does not say:
|
package/lib/dataflow.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { eachWithinMemory, watchMemoryBuffer } from './kernel/memory.mjs';
|
|
|
12
12
|
// A flow finding is located by source and sink rather than by one path and line, so it keeps its
|
|
13
13
|
// own comparator. The text comparison underneath it is the shared one.
|
|
14
14
|
import { byText } from './kernel/findings.mjs';
|
|
15
|
-
import { buildControl, creditOf, hasFact } from './control.mjs';
|
|
15
|
+
import { buildControl, creditOf, hasFact, within } from './control.mjs';
|
|
16
16
|
import { directoryTree } from './kernel/source-tree.mjs';
|
|
17
17
|
|
|
18
18
|
const OS_COMMAND_ROUTINE = /^(SYSTEM|C\$SYSTEM|CBL_EXEC_RUN_UNIT|CBL_GC_HOSTED|BXPSYSTM)$/i;
|
|
@@ -51,6 +51,12 @@ export const SINK_KINDS = {
|
|
|
51
51
|
'record-key': 'the RIDFLD of EXEC CICS READ, STARTBR or RESETBR, which decides which record is read',
|
|
52
52
|
'record-update': 'the RIDFLD of a READ UPDATE whose record the program then rewrites or deletes, or of a DELETE',
|
|
53
53
|
'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',
|
|
54
|
+
'storage-length': 'how much storage a program acquires: the FLENGTH or LENGTH of EXEC CICS GETMAIN, or the size CALL CEEGTST is given',
|
|
55
|
+
'xml-document': 'the document XML PARSE reads',
|
|
56
|
+
'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',
|
|
57
|
+
'screen': 'what a CICS program shows its terminal: EXEC CICS SEND TEXT FROM, SEND MAP FROM, or the map\'s output record',
|
|
58
|
+
'cics-sysid': 'the SYSID of an EXEC CICS command, which decides the region the command is shipped to',
|
|
59
|
+
'cics-system-resource': 'the resource an EXEC CICS SET names, which a system-programming command then changes',
|
|
54
60
|
};
|
|
55
61
|
|
|
56
62
|
const ARITHMETIC = new Set(['COMPUTE', 'ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE']);
|
|
@@ -126,7 +132,7 @@ const IN_CICS = ['cics-web', 'cics-terminal', 'file-record', 'database'];
|
|
|
126
132
|
const REFLECTED = ['cics-web', 'cics-terminal'];
|
|
127
133
|
// What the system says about a failure: a command's RESP, the EIB's copy of it, the SQLCA. Sent to a
|
|
128
134
|
// 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.
|
|
135
|
+
// program's own business, so these reach only a response, and Db2's message text the terminal too.
|
|
130
136
|
const SYSTEM_FIELDS = new Set(['EIBRESP', 'EIBRESP2', 'SQLCODE', 'SQLSTATE', 'SQLERRMC']);
|
|
131
137
|
const TO_CLIENT = ['web-response', 'http-header'];
|
|
132
138
|
|
|
@@ -219,11 +225,12 @@ export function analyze(root, opts = {}) {
|
|
|
219
225
|
const jclPending = [];
|
|
220
226
|
// CICS definitions, from CSD extracts and from DFHCSDUP job input, and the estate's own statement
|
|
221
227
|
// of which DDs and queues reach the internal reader.
|
|
222
|
-
const csd = { tdqueues: new Map(), transactions: new Map() };
|
|
228
|
+
const csd = { tdqueues: new Map(), transactions: new Map(), urimaps: new Map() };
|
|
223
229
|
const addCsd = (text, file, lineBase = 0) => {
|
|
224
230
|
const c = parseCsd(text);
|
|
225
231
|
for (const [k, v] of c.tdqueues) if (!csd.tdqueues.has(k)) csd.tdqueues.set(k, v);
|
|
226
232
|
for (const [k, v] of c.transactions) if (!csd.transactions.has(k)) csd.transactions.set(k, { ...v, file, line: v.line + lineBase });
|
|
233
|
+
for (const [k, v] of c.urimaps) if (!csd.urimaps.has(k)) csd.urimaps.set(k, { ...v, file, line: v.line + lineBase });
|
|
227
234
|
};
|
|
228
235
|
const jobSteps = [];
|
|
229
236
|
// The maps of the repository being read, by MAPSET/MAP and by MAP alone.
|
|
@@ -286,8 +293,11 @@ export function analyze(root, opts = {}) {
|
|
|
286
293
|
// A system field the program never declares - the EIB's, or an SQLCA the precompiler supplies -
|
|
287
294
|
// still holds a value, so it needs a node of its own, as an index name does.
|
|
288
295
|
const systemNodes = new Map();
|
|
296
|
+
// A code on a terminal's error line is ordinary; Db2's message text names tables, columns and
|
|
297
|
+
// constraints, so only SQLERRMC reaches the screen sink.
|
|
289
298
|
const systemSource = (n, detail, at) => {
|
|
290
|
-
|
|
299
|
+
const onlyTo = /^SQLERRMC\b/.test(detail) ? [...TO_CLIENT, 'screen'] : TO_CLIENT;
|
|
300
|
+
if (!n.sources.some((x) => x.kind === 'system-response')) n.sources.push({ kind: 'system-response', onlyTo, noCredit: true, ...at, detail });
|
|
291
301
|
};
|
|
292
302
|
const systemNode = (tok) => {
|
|
293
303
|
let n = systemNodes.get(tok.u);
|
|
@@ -332,6 +342,13 @@ export function analyze(root, opts = {}) {
|
|
|
332
342
|
if (n) n.sinks.push({ kind: 'log', onlyFrom: REFLECTED, file: st.file, line: st.line, point: pointOf(st), detail: `DISPLAY ${n.name}${device}` });
|
|
333
343
|
}
|
|
334
344
|
}
|
|
345
|
+
// The document XML PARSE reads is the operand after PARSE; what the parser does with a DTD in
|
|
346
|
+
// it is the compiler's XMLPARSE option, which nothing here reads.
|
|
347
|
+
for (const st of prog.statements) {
|
|
348
|
+
if (st.verb !== 'XML' || !st.sources[0] || st.sources[0].u !== 'PARSE') continue;
|
|
349
|
+
const n = st.sources[1] ? nodeOfToken(st.sources[1]) : null;
|
|
350
|
+
if (n) n.sinks.push({ kind: 'xml-document', onlyFrom: REFLECTED, file: st.file, line: st.line, point: pointOf(st), detail: `XML PARSE ${n.name}` });
|
|
351
|
+
}
|
|
335
352
|
for (const it of prog.items) {
|
|
336
353
|
const n = nodeOf(it);
|
|
337
354
|
// A tainted child makes its group partly tainted; a tainted group taints every child. The
|
|
@@ -366,7 +383,7 @@ export function analyze(root, opts = {}) {
|
|
|
366
383
|
for (const it of prog.items) { const top = recordOf(it); if (!byRecord.has(top)) byRecord.set(top, []); byRecord.get(top).push(it); }
|
|
367
384
|
const extent = (it) => (it.offset != null && it.size != null ? [it.offset, it.offset + (it.contributes || it.size)] : null);
|
|
368
385
|
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 } : {}) };
|
|
386
|
+
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
387
|
if (c.field.index) { (indexNode(c.field.index).checks ||= []).push(entry); continue; }
|
|
371
388
|
const span = extent(c.field);
|
|
372
389
|
for (const it of byRecord.get(recordOf(c.field)) || [c.field]) {
|
|
@@ -411,9 +428,16 @@ export function analyze(root, opts = {}) {
|
|
|
411
428
|
if (host.size == null || variable(host)) return null;
|
|
412
429
|
return (host.children || []).some((c) => c.level !== 88) ? host.size / Math.max(1, host.occurs || 1) : host.size;
|
|
413
430
|
};
|
|
431
|
+
// A whole number no sign is written on, which no value put in it can leave below 0.
|
|
432
|
+
const unsignedWhole = (it) => it.level !== 88 && !it.index && !(it.children || []).length
|
|
433
|
+
&& /^9+$/.test(String(it.picture || '').toUpperCase().replace(/(\w)\((\d+)\)/g, (_, ch, n) => ch.repeat(Number(n))));
|
|
414
434
|
const indexed = new Set();
|
|
415
435
|
const subscripting = new Map();
|
|
436
|
+
// Each subscript use of a name, with the other indices of the same reference: the loop-bound
|
|
437
|
+
// credit needs every one of them kept in range.
|
|
438
|
+
const usesOf = new Map();
|
|
416
439
|
for (const st of prog.statements) {
|
|
440
|
+
const refs = new Map();
|
|
417
441
|
for (const x of st.indexes || []) {
|
|
418
442
|
const host = itemOfToken(x.host);
|
|
419
443
|
const idx = itemOfToken(x.tok);
|
|
@@ -426,17 +450,31 @@ export function analyze(root, opts = {}) {
|
|
|
426
450
|
const key = `${kind}|${host.name}|${at.name}`;
|
|
427
451
|
if (table && !subscripting.has(at.name)) subscripting.set(at.name, { host, table });
|
|
428
452
|
const point = placeOf(x.tok, st);
|
|
429
|
-
|
|
453
|
+
if (table) {
|
|
454
|
+
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)) };
|
|
455
|
+
rec.mates.push(rec);
|
|
456
|
+
if (!usesOf.has(at.name)) usesOf.set(at.name, []);
|
|
457
|
+
usesOf.get(at.name).push(rec);
|
|
458
|
+
}
|
|
459
|
+
const offset = x.offset || 0;
|
|
460
|
+
const use = `${key}|${point ?? `s${st.at}`}|${offset}`;
|
|
430
461
|
if (indexed.has(use)) continue;
|
|
431
462
|
indexed.add(use);
|
|
432
463
|
at.sinks.push({
|
|
433
|
-
kind, onlyFrom: FROM_OUTSIDE, file: st.file, line: st.line, point, group: `${pk}|${key}`, limit: limitOf(host, table), ...(ssrange ? { ssrange } : {}),
|
|
464
|
+
kind, onlyFrom: FROM_OUTSIDE, file: st.file, line: st.line, point, group: `${pk}|${key}`, limit: limitOf(host, table), ...(offset ? { offset } : {}), ...(offset > 0 && idx && unsignedWhole(idx) ? { nonNeg: true } : {}), ...(ssrange ? { ssrange } : {}),
|
|
434
465
|
detail: kind === 'subscript'
|
|
435
466
|
? `${at.name} subscripts ${host.name}, a table of ${table.occurs}`
|
|
436
467
|
: `${at.name} sets the ${x.kind === 'refmod-offset' ? 'start' : 'length'} of a reference to ${host.name}, which is ${host.size} bytes`,
|
|
437
468
|
});
|
|
438
469
|
}
|
|
439
470
|
}
|
|
471
|
+
// The subscript uses a loop's body makes; every use of the name when the body can run code
|
|
472
|
+
// elsewhere, and none for a loop whose body is a paragraph.
|
|
473
|
+
const bodyUses = (st, uses) => {
|
|
474
|
+
const body = ctl && ctl.loopBody.get(st);
|
|
475
|
+
if (!body || !uses) return null;
|
|
476
|
+
return body.leaves ? uses : uses.filter((u) => u.at >= body.from && u.at < body.to);
|
|
477
|
+
};
|
|
440
478
|
// A counter that subscripts a table walks as far as its loop's condition lets it. PERFORM
|
|
441
479
|
// VARYING I ... UNTIL I > N with N from outside puts the table's bound in the caller's hands, and
|
|
442
480
|
// no subscript sink sees it: I itself is only ever a literal plus one. The subscript may be
|
|
@@ -454,7 +492,7 @@ export function analyze(root, opts = {}) {
|
|
|
454
492
|
if (indexed.has(use)) continue;
|
|
455
493
|
indexed.add(use);
|
|
456
494
|
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 } : {}),
|
|
495
|
+
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
496
|
detail: `${it.name} decides where PERFORM VARYING ${counter} stops, and ${counter} subscripts ${walks.host.name}, a table of ${walks.table.occurs}`,
|
|
459
497
|
});
|
|
460
498
|
}
|
|
@@ -480,14 +518,27 @@ export function analyze(root, opts = {}) {
|
|
|
480
518
|
});
|
|
481
519
|
}
|
|
482
520
|
}
|
|
483
|
-
|
|
484
|
-
|
|
521
|
+
// The count matters wherever a statement names the table, a part of it, or a group holding it,
|
|
522
|
+
// so it is judged at each such statement; a table no statement names is judged where it is declared.
|
|
523
|
+
const odoTables = prog.items.filter((it) => it.dependingOn && byName.get(it.dependingOn));
|
|
524
|
+
const touching = new Map(odoTables.map((it) => [it, []]));
|
|
525
|
+
if (odoTables.length) {
|
|
526
|
+
const inside = (a, b) => { for (let x = a; x; x = x.parent) if (x === b) return true; return false; };
|
|
527
|
+
for (const st of prog.statements) {
|
|
528
|
+
const named = [...st.sources, ...st.targets, ...(st.indexes || []).map((x) => x.host)].map(itemOfToken).filter(Boolean);
|
|
529
|
+
for (const it of odoTables) if (named.some((n) => inside(n, it) || inside(it, n))) touching.get(it).push(st);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
for (const it of odoTables) {
|
|
485
533
|
const count = byName.get(it.dependingOn);
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
534
|
+
const sts = touching.get(it);
|
|
535
|
+
for (const st of sts.length ? sts : [null]) {
|
|
536
|
+
const point = st ? pointOf(st) : undefined;
|
|
537
|
+
nodeOf(count).sinks.push({
|
|
538
|
+
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 } : {}),
|
|
539
|
+
detail: `${count.name} sets how many ${it.name} entries there are, up to ${it.occurs}`,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
491
542
|
}
|
|
492
543
|
for (const st of prog.statements) {
|
|
493
544
|
if (st.verb !== 'READ' && st.verb !== 'RETURN') continue;
|
|
@@ -572,6 +623,16 @@ export function analyze(root, opts = {}) {
|
|
|
572
623
|
const a = c.using[MQ_BUFFER_ARG[String(c.name).toUpperCase()]];
|
|
573
624
|
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
574
625
|
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}` });
|
|
626
|
+
} else if (/^MQCONNX?$/i.test(String(c.name))) {
|
|
627
|
+
// MQCONN(QMgrName, Hconn, CompCode, Reason); MQCONNX puts its options second.
|
|
628
|
+
const a = c.using[0];
|
|
629
|
+
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
630
|
+
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` });
|
|
631
|
+
} else if (String(c.name).toUpperCase() === 'CEEGTST') {
|
|
632
|
+
// CEEGTST(heap id, size, address, feedback code): the size is the second argument.
|
|
633
|
+
const a = c.using[1];
|
|
634
|
+
const n = a && a.tok ? nodeOfToken(a.tok) : null;
|
|
635
|
+
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
636
|
}
|
|
576
637
|
}
|
|
577
638
|
const transfers = [];
|
|
@@ -714,6 +775,14 @@ export function analyze(root, opts = {}) {
|
|
|
714
775
|
}
|
|
715
776
|
if (!isWeb && o.has('MAP')) receiveMap(e, o, itemOfToken(intoTok), at);
|
|
716
777
|
}
|
|
778
|
+
// What the terminal shows: SEND TEXT's FROM, or the map's output record, <map>O without FROM.
|
|
779
|
+
if (verb === 'SEND' && (words[1] === 'TEXT' || words[1] === 'MAP')) {
|
|
780
|
+
const from = nodeOfToken((o.get('FROM') || [])[0]);
|
|
781
|
+
const map = words[1] === 'MAP' ? optionName(o, 'MAP') : null;
|
|
782
|
+
const sym = !from && map ? byName.get(`${map}O`) : null;
|
|
783
|
+
const n = from || (sym ? nodeOf(sym) : null);
|
|
784
|
+
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` });
|
|
785
|
+
}
|
|
717
786
|
// RIDFLD decides which record a READ or DELETE reaches, or where a browse starts. A key the
|
|
718
787
|
// user types is how a lookup works; a key the program put in a protected field and read back
|
|
719
788
|
// is its own state, held where a modified client can change it.
|
|
@@ -726,6 +795,12 @@ export function analyze(root, opts = {}) {
|
|
|
726
795
|
: `EXEC CICS ${verb} RIDFLD(${k && k.name}), which decides which record it reaches`;
|
|
727
796
|
if (k) k.sinks.push({ kind: verb === 'DELETE' || then ? 'record-update' : 'record-key', onlyFrom: ['cics-protected-field'], noCredit: true, ...at, detail });
|
|
728
797
|
}
|
|
798
|
+
if (verb === 'GETMAIN') {
|
|
799
|
+
for (const opt of ['FLENGTH', 'LENGTH']) {
|
|
800
|
+
const n = nodeOfToken((o.get(opt) || [])[0]);
|
|
801
|
+
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` });
|
|
802
|
+
}
|
|
803
|
+
}
|
|
729
804
|
if (verb === 'PUT' && o.has('CONTAINER')) {
|
|
730
805
|
const name = optionName(o, 'CONTAINER');
|
|
731
806
|
const from = nodeOfToken((o.get('FROM') || [])[0]);
|
|
@@ -794,6 +869,17 @@ export function analyze(root, opts = {}) {
|
|
|
794
869
|
if (n) n.sinks.push({ kind: 'queue-name', onlyFrom: IN_CICS, ...at, detail: `EXEC CICS ${verb} ${ts ? 'TS' : 'TD'} ${opt}(...) names the queue` });
|
|
795
870
|
}
|
|
796
871
|
}
|
|
872
|
+
// SYSID ships a command to the region it names; SET's first option names the resource a
|
|
873
|
+
// system-programming command changes. A literal in either is the program's own choice. ASSIGN,
|
|
874
|
+
// INQUIRE and EXTRACT return SYSID into the field rather than read it.
|
|
875
|
+
if (o.has('SYSID') && !['ASSIGN', 'INQUIRE', 'EXTRACT'].includes(verb)) {
|
|
876
|
+
const n = nodeOfToken((o.get('SYSID') || [])[0]);
|
|
877
|
+
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` });
|
|
878
|
+
}
|
|
879
|
+
if (verb === 'SET' && words[1] && o.has(words[1])) {
|
|
880
|
+
const n = nodeOfToken((o.get(words[1]) || [])[0]);
|
|
881
|
+
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` });
|
|
882
|
+
}
|
|
797
883
|
if (verb === 'READ' || verb === 'READNEXT' || verb === 'READPREV') {
|
|
798
884
|
const n = nodeOfToken(intoTok);
|
|
799
885
|
if (n && (o.has('FILE') || o.has('DATASET'))) n.sources.push({ kind: 'file-record', ...at, detail: `EXEC CICS ${verb} FILE ... INTO` });
|
|
@@ -845,6 +931,14 @@ export function analyze(root, opts = {}) {
|
|
|
845
931
|
const n = nodeOfToken(h.tok);
|
|
846
932
|
if (n) n.sources.push({ kind: 'database', ...at, detail: `EXEC SQL ${verb} INTO host variable` });
|
|
847
933
|
}
|
|
934
|
+
// The location CONNECT TO or SET CONNECTION names, where a host variable holds it; what
|
|
935
|
+
// follows USER and USING is the credential, not the target.
|
|
936
|
+
if (verb === 'CONNECT' || (verb === 'SET' && words[1] === 'CONNECTION')) {
|
|
937
|
+
const to = e.toks.findIndex((t) => t.t === 'word' && (t.u === 'TO' || t.u === 'CONNECTION'));
|
|
938
|
+
const colon = to >= 0 ? e.toks[to + 1] : null;
|
|
939
|
+
const n = colon && colon.t === 'op' && colon.v === ':' && e.toks[to + 2] ? nodeOfToken(e.toks[to + 2]) : null;
|
|
940
|
+
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` });
|
|
941
|
+
}
|
|
848
942
|
const dynamic = words.some((w, i) => w === 'PREPARE' || (w === 'EXECUTE' && words[i + 1] === 'IMMEDIATE'));
|
|
849
943
|
if (dynamic) for (const h of hv) {
|
|
850
944
|
const n = nodeOfToken(h.tok);
|
|
@@ -860,7 +954,7 @@ export function analyze(root, opts = {}) {
|
|
|
860
954
|
}
|
|
861
955
|
const commareaItem = byName.get('DFHCOMMAREA');
|
|
862
956
|
return {
|
|
863
|
-
id: prog.id, pk, callSites, transfers, ddFiles, tdWrites,
|
|
957
|
+
id: prog.id, pk, file, callSites, transfers, ddFiles, tdWrites,
|
|
864
958
|
params: prog.paramTokens ? prog.paramTokens.map(p => {
|
|
865
959
|
const it = itemOfToken(p.tok);
|
|
866
960
|
const extent = extentOf(it);
|
|
@@ -929,27 +1023,55 @@ export function analyze(root, opts = {}) {
|
|
|
929
1023
|
|
|
930
1024
|
const byId = new Map();
|
|
931
1025
|
const idCount = new Map();
|
|
1026
|
+
const allById = new Map();
|
|
932
1027
|
for (const p of programs) {
|
|
933
1028
|
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);
|
|
1029
|
+
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
1030
|
}
|
|
1031
|
+
// The programs a CALL or LINK may reach. A program id the repository holds once is that program.
|
|
1032
|
+
// Held several times, the one in the caller's own source file is the one its executable links;
|
|
1033
|
+
// with none there, the holders nearest in the directory tree are, since a copy of a program tree
|
|
1034
|
+
// or a sibling executable's copy is farther than the caller's own build directory. Ties keep every
|
|
1035
|
+
// holder, and the edge says how many candidates there were.
|
|
1036
|
+
const sharedDirs = (a, b) => {
|
|
1037
|
+
const x = a.split(/[\\/]/), y = b.split(/[\\/]/);
|
|
1038
|
+
let n = 0;
|
|
1039
|
+
while (n < x.length - 1 && n < y.length - 1 && x[n] === y[n]) n++;
|
|
1040
|
+
return n;
|
|
1041
|
+
};
|
|
1042
|
+
const calleesOf = (name, fromFile) => {
|
|
1043
|
+
const all = allById.get(name) || [];
|
|
1044
|
+
if (all.length < 2) return { list: all, of: 0 };
|
|
1045
|
+
const here = all.filter((q) => q.file === fromFile);
|
|
1046
|
+
if (here.length) return { list: here, of: 0 };
|
|
1047
|
+
const depth = all.map((q) => sharedDirs(q.file, fromFile));
|
|
1048
|
+
const best = Math.max(...depth);
|
|
1049
|
+
return { list: all.filter((_, i) => depth[i] === best), of: all.length };
|
|
1050
|
+
};
|
|
1051
|
+
const ofN = (r, name) => (r.of ? `, ${r.list.length === r.of ? 'any' : 'nearest'} of ${r.of} programs named ${name}` : '');
|
|
936
1052
|
const constructs = [];
|
|
937
1053
|
for (const p of programs) {
|
|
938
1054
|
for (const t of p.transfers) {
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1055
|
+
const r = calleesOf(t.callee, p.file);
|
|
1056
|
+
for (const callee of r.list) {
|
|
1057
|
+
if (!callee.commarea) continue;
|
|
1058
|
+
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);
|
|
1059
|
+
if (t.verb === 'LINK') edge(callee.commarea, t.argNode, `COMMAREA returned from ${t.callee}${ofN(r, t.callee)}`);
|
|
1060
|
+
}
|
|
943
1061
|
}
|
|
944
1062
|
for (const c of p.callSites) {
|
|
945
|
-
const
|
|
1063
|
+
const r = calleesOf(c.name, p.file);
|
|
1064
|
+
for (const callee of r.list) {
|
|
1065
|
+
if (!callee.params) continue;
|
|
1066
|
+
c.args.forEach((arg, i) => {
|
|
1067
|
+
const param = callee.params[i];
|
|
1068
|
+
if (!param || !arg.node || !param.node) return;
|
|
1069
|
+
edge(arg.node, param.node, `CALL '${c.display}' argument ${i + 1} at ${rel(c.file)}:${c.line}${ofN(r, c.display)}`, undefined, c.point);
|
|
1070
|
+
if (arg.mode === 'REFERENCE' && param.mode === 'REFERENCE') edge(param.node, arg.node, `CALL '${c.display}' argument ${i + 1} written back${ofN(r, c.display)}`);
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
const callee = r.list.length === 1 ? r.list[0] : null;
|
|
946
1074
|
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
1075
|
// A callee that declares more bytes than its caller passes reads and writes whatever follows
|
|
954
1076
|
// the argument in the caller's storage. Measured over 8,640 bound pairs in 125 repositories:
|
|
955
1077
|
// where the extra bytes stay inside the caller's own record they are its neighbouring fields,
|
|
@@ -1008,47 +1130,52 @@ export function analyze(root, opts = {}) {
|
|
|
1008
1130
|
if (run) jobSteps.push({ file: f, job: job.jobs[0]?.name || null, step: step.name, line: l.line, pgm: run[1].toUpperCase() });
|
|
1009
1131
|
}
|
|
1010
1132
|
}
|
|
1011
|
-
const
|
|
1012
|
-
if (!
|
|
1133
|
+
const holders = allById.get(step.pgm.toUpperCase()) || [];
|
|
1134
|
+
if (!holders.length) continue; // a system utility, or a program not in this tree
|
|
1013
1135
|
stats.jclStepsResolved++;
|
|
1136
|
+
// A program id held several times has no caller file to choose by, so each holder is a candidate.
|
|
1137
|
+
const amb = holders.length > 1 ? `, any of ${holders.length} programs named ${step.pgm}` : '';
|
|
1138
|
+
for (const callee of holders) {
|
|
1014
1139
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1140
|
+
// PARM arrives in the first PROCEDURE DIVISION USING item: on z/OS a halfword length
|
|
1141
|
+
// followed by the text. A program with no USING cannot receive one, and saying it does
|
|
1142
|
+
// would be a path nobody could follow.
|
|
1143
|
+
if (step.parm !== null && callee.params && callee.params[0] && callee.params[0].node) {
|
|
1144
|
+
callee.params[0].node.sources.push({
|
|
1145
|
+
kind: 'jcl-parm', file: f, line: step.line,
|
|
1146
|
+
detail: `PARM= on step ${step.name || '(unnamed)'} of ${rel(f)}, which runs ${step.pgm}${amb}`,
|
|
1147
|
+
});
|
|
1148
|
+
stats.jclCrossings++;
|
|
1149
|
+
}
|
|
1025
1150
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1151
|
+
// In-stream data reaches whatever record the program reads from that DD. The COBOL says
|
|
1152
|
+
// ASSIGN TO SYSIN and the job says //SYSIN DD *; neither half names the other, and the
|
|
1153
|
+
// join is the whole point of reading both.
|
|
1154
|
+
for (const dd of step.dds) {
|
|
1155
|
+
// The same join in the other direction: what the program writes through SELECT ... ASSIGN
|
|
1156
|
+
// TO a DD the job sends to the internal reader is submitted as a job.
|
|
1157
|
+
if (dd.name && dd.sysout && /\bINTRDR\b/i.test(dd.sysout)) {
|
|
1158
|
+
for (const m of callee.ddFiles || []) {
|
|
1159
|
+
if (m.dd !== dd.name.toUpperCase()) continue;
|
|
1160
|
+
for (const n of m.nodes) {
|
|
1161
|
+
n.sinks.push({ kind: 'internal-reader', file: m.file, line: m.line,
|
|
1162
|
+
detail: `records written through SELECT ${m.select} to //${dd.name}, which step ${step.name || '(unnamed)'} of ${rel(f)} sends to the internal reader${amb}` });
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (!dd.inStream || !dd.inStream.length || !dd.name) continue;
|
|
1033
1167
|
for (const m of callee.ddFiles || []) {
|
|
1034
1168
|
if (m.dd !== dd.name.toUpperCase()) continue;
|
|
1035
1169
|
for (const n of m.nodes) {
|
|
1036
|
-
n.
|
|
1037
|
-
|
|
1170
|
+
n.sources.push({
|
|
1171
|
+
kind: 'jcl-instream', file: f, line: dd.line,
|
|
1172
|
+
detail: `${dd.inStream.length} lines of in-stream data on //${dd.name} in step ${step.name || '(unnamed)'}, read through SELECT ${m.select}${amb}`,
|
|
1173
|
+
});
|
|
1174
|
+
stats.jclCrossings++;
|
|
1038
1175
|
}
|
|
1039
1176
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
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}`,
|
|
1048
|
-
});
|
|
1049
|
-
stats.jclCrossings++;
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1177
|
+
|
|
1178
|
+
}
|
|
1052
1179
|
}
|
|
1053
1180
|
}
|
|
1054
1181
|
}
|
|
@@ -1059,6 +1186,12 @@ export function analyze(root, opts = {}) {
|
|
|
1059
1186
|
// in this tree reaches.
|
|
1060
1187
|
const roots = [];
|
|
1061
1188
|
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 });
|
|
1189
|
+
// A URI map serves its program to an HTTP request under an alias transaction, CWBA unless it names
|
|
1190
|
+
// one, and that transaction is what the region checks the caller may start.
|
|
1191
|
+
for (const [name, u] of csd.urimaps) {
|
|
1192
|
+
if ((u.usage && u.usage !== 'SERVER') || !u.program || !byId.has(u.program)) continue;
|
|
1193
|
+
roots.push({ transaction: u.transaction || 'CWBA', urimap: name, ...(u.path ? { uri: u.path } : {}), file: rel(u.file), line: u.line, program: u.program });
|
|
1194
|
+
}
|
|
1062
1195
|
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
1196
|
const STARTED_BY_LISTED = 8;
|
|
1064
1197
|
for (const r of roots) {
|
|
@@ -1162,25 +1295,45 @@ export function analyze(root, opts = {}) {
|
|
|
1162
1295
|
const moves = (e) => !(typeof e.why === 'string' && STRUCTURAL.has(e.why));
|
|
1163
1296
|
const NONE = { level: 0, check: null };
|
|
1164
1297
|
// `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) {
|
|
1298
|
+
function creditAt(n, point, kind, limit = null, offset = 0, nonNeg = false) {
|
|
1166
1299
|
const p = programs[n.pk];
|
|
1167
1300
|
if (!p || !p.ordered) return n.guard ? { level: 1, check: n.guard } : NONE;
|
|
1168
1301
|
if (!n.checks || point == null) return NONE;
|
|
1169
1302
|
const bits = p.facts.get(point);
|
|
1170
|
-
return bits ? creditOf(n.checks, bits, kind, limit) : NONE;
|
|
1303
|
+
return bits ? creditOf(n.checks, bits, kind, limit, offset, nonNeg) : NONE;
|
|
1304
|
+
}
|
|
1305
|
+
// A loop bound cannot push its counter out of the table when the loop's own condition or the
|
|
1306
|
+
// body's checks keep every subscript the counter makes, and each index beside it, in range.
|
|
1307
|
+
// That holds whatever the bound is, so it does not depend on the route the input took.
|
|
1308
|
+
function counterCredit(sink) {
|
|
1309
|
+
const uses = sink.kind === 'loop-bound' ? sink.counterUses : null;
|
|
1310
|
+
if (!uses || !uses.length) return null;
|
|
1311
|
+
let first = null;
|
|
1312
|
+
for (const u of uses) {
|
|
1313
|
+
for (const m of u.mates) {
|
|
1314
|
+
const c = creditAt(m.node, m.point, 'subscript', m.limit, m.offset || 0);
|
|
1315
|
+
if (c.level < 2 || m.limit == null || !within(c.cons, m.limit)) return null;
|
|
1316
|
+
if (m === u && !first) first = { credit: c, node: m.node };
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return first;
|
|
1171
1320
|
}
|
|
1172
1321
|
// The best credit on a route. Each node is judged at the statement its value leaves by, which for
|
|
1173
1322
|
// the last is the sink's own. A structural hop - into a group, across a REDEFINES - moves no value
|
|
1174
1323
|
// in time, so the node before it is judged where the value next moves.
|
|
1175
1324
|
function routeCredit(hops, sink, src) {
|
|
1176
1325
|
if (sink.noCredit || (src && src.noCredit)) return { best: NONE, by: null, missed: null };
|
|
1326
|
+
const own = counterCredit(sink);
|
|
1327
|
+
if (own) return { best: own.credit, by: own.node, missed: null, own: true };
|
|
1177
1328
|
let best = NONE;
|
|
1178
1329
|
let by = null;
|
|
1179
1330
|
let missed = null;
|
|
1180
1331
|
let point = sink.point;
|
|
1181
1332
|
for (let h = hops.length - 1; h >= 0; h--) {
|
|
1182
1333
|
const n = hops[h].node;
|
|
1183
|
-
|
|
1334
|
+
let c = creditAt(n, point, sink.kind, sink.limit, sink.offset, sink.nonNeg);
|
|
1335
|
+
// A count checked against a number above the table's maximum has been checked, not kept in range.
|
|
1336
|
+
if (c.level === 2 && sink.top != null && !within(c.cons, sink.top, false)) c = { level: 1, check: c.check };
|
|
1184
1337
|
if (c.level > best.level) { best = c; by = n; }
|
|
1185
1338
|
const any = (n.checks && n.checks.find((c) => c.x != null)) || n.guard;
|
|
1186
1339
|
if (!c.level && any && !missed) missed = { node: n, check: any };
|
|
@@ -1191,16 +1344,16 @@ export function analyze(root, opts = {}) {
|
|
|
1191
1344
|
// The credited findings of one source whose sink can be reached without the credit: walking from
|
|
1192
1345
|
// the source, a value may not leave a node - or any node it reached by a structural hop since it
|
|
1193
1346
|
// last moved - at a point where the credit reaches `level`.
|
|
1194
|
-
function refuse(start, reach, credited, level, kind) {
|
|
1347
|
+
function refuse(start, reach, credited, level, kind, shift = 0) {
|
|
1195
1348
|
walk++;
|
|
1196
|
-
const want = credited.filter((c) => c.level >= level && c.sink.kind === kind);
|
|
1349
|
+
const want = credited.filter((c) => c.level >= level && c.sink.kind === kind && (c.sink.offset || 0) === shift);
|
|
1197
1350
|
// Past the budget no route can be ruled out, so every credit in question is lost: a finding
|
|
1198
1351
|
// may be reported unchecked, never dropped.
|
|
1199
1352
|
if (edgesWalked >= totalBudget) { stats.creditUndecided++; return new Set(want); }
|
|
1200
1353
|
const at = new Map();
|
|
1201
1354
|
for (const c of want) { if (!at.has(c.node.id)) at.set(c.node.id, []); at.get(c.node.id).push(c); }
|
|
1202
1355
|
// 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);
|
|
1356
|
+
const holds = (since, point, k, limit = null, offset = 0, nonNeg = false) => since.some((n) => creditAt(n, point, k, limit, offset, nonNeg).level >= level);
|
|
1204
1357
|
const lost = new Set();
|
|
1205
1358
|
const dirty = new Map();
|
|
1206
1359
|
const parts = new Map();
|
|
@@ -1209,13 +1362,13 @@ export function analyze(root, opts = {}) {
|
|
|
1209
1362
|
const began = edgesWalked;
|
|
1210
1363
|
for (let i = 0; i < open.length; i++) {
|
|
1211
1364
|
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);
|
|
1365
|
+
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
1366
|
for (const e of st.node.edgesOut) {
|
|
1214
1367
|
edgesWalked++;
|
|
1215
1368
|
const to = e.to;
|
|
1216
1369
|
if (!(canReach[to.id] & reach)) continue;
|
|
1217
1370
|
const mv = moves(e);
|
|
1218
|
-
if (mv && holds(st.since, pointOfEdge(e), kind)) continue;
|
|
1371
|
+
if (mv && holds(st.since, pointOfEdge(e), kind, null, shift)) continue;
|
|
1219
1372
|
const t = cross(st, e);
|
|
1220
1373
|
if (!t) continue;
|
|
1221
1374
|
const since = mv ? [to] : [...st.since, to];
|
|
@@ -1354,7 +1507,7 @@ export function analyze(root, opts = {}) {
|
|
|
1354
1507
|
: [...hops.slice(0, TRACE_KEEP).map(hop),
|
|
1355
1508
|
{ program: null, item: null, file: null, via: `… ${hops.length - TRACE_KEEP * 2} hops not listed`, elided: hops.length - TRACE_KEEP * 2 },
|
|
1356
1509
|
...hops.slice(-TRACE_KEEP).map(hop)];
|
|
1357
|
-
const { best, by, missed } = routeCredit(hops, sink, src);
|
|
1510
|
+
const { best, by, missed, own } = routeCredit(hops, sink, src);
|
|
1358
1511
|
const screen = hops.find((h) => h.node.screen)?.node.screen;
|
|
1359
1512
|
findings.push({
|
|
1360
1513
|
rule: `${src.kind}-to-${sink.kind}`,
|
|
@@ -1370,7 +1523,7 @@ export function analyze(root, opts = {}) {
|
|
|
1370
1523
|
...(best.level ? { guard: { program: by.program, item: by.name, file: rel(best.check.file), line: best.check.line, ...(best.level === 2 ? { stops: true } : {}) } } : {}),
|
|
1371
1524
|
...(!best.level && missed ? { checkElsewhere: { program: missed.node.program, item: missed.node.name, file: rel(missed.check.file), line: missed.check.line } } : {}),
|
|
1372
1525
|
});
|
|
1373
|
-
if (best.level) credited.push({ finding: findings[findings.length - 1], node: cur, sink, level: best.level });
|
|
1526
|
+
if (best.level && !own) credited.push({ finding: findings[findings.length - 1], node: cur, sink, level: best.level });
|
|
1374
1527
|
if (sink.group) useGroup.set(findings[findings.length - 1], `${src.kind}|${src.file}:${src.line}|${sink.kind}|${sink.group}`);
|
|
1375
1528
|
}
|
|
1376
1529
|
for (const e of cur.edgesOut) {
|
|
@@ -1399,16 +1552,23 @@ export function analyze(root, opts = {}) {
|
|
|
1399
1552
|
// cross() like the first walk, so it cannot take a route the first would refuse.
|
|
1400
1553
|
if (credited.length) {
|
|
1401
1554
|
// Per kind of sink, because what a check leaves may stop one kind and say nothing to another.
|
|
1555
|
+
// And per shift, because a value that stops a bare index does not stop one moved down by a constant.
|
|
1556
|
+
const shiftsOf = (list, kind) => new Set(list.filter((c) => c.sink.kind === kind).map((c) => c.sink.offset || 0));
|
|
1402
1557
|
for (const kind of new Set(credited.map((c) => c.sink.kind))) {
|
|
1403
|
-
for (const
|
|
1404
|
-
const
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1558
|
+
for (const shift of shiftsOf(credited, kind)) {
|
|
1559
|
+
for (const c of refuse(start, reach, credited, 1, kind, shift)) {
|
|
1560
|
+
const g = c.finding.guard;
|
|
1561
|
+
delete c.finding.guard;
|
|
1562
|
+
c.finding.checkElsewhere = { program: g.program, item: g.item, file: g.file, line: g.line };
|
|
1563
|
+
c.level = 0;
|
|
1564
|
+
}
|
|
1408
1565
|
}
|
|
1409
1566
|
}
|
|
1410
1567
|
for (const kind of new Set(credited.filter((c) => c.level === 2).map((c) => c.sink.kind))) {
|
|
1411
|
-
|
|
1568
|
+
const two = credited.filter((c) => c.level === 2);
|
|
1569
|
+
for (const shift of shiftsOf(two, kind)) {
|
|
1570
|
+
for (const c of refuse(start, reach, credited, 2, kind, shift)) if (c.level === 2) { delete c.finding.guard.stops; c.level = 1; }
|
|
1571
|
+
}
|
|
1412
1572
|
}
|
|
1413
1573
|
}
|
|
1414
1574
|
}
|
|
@@ -1423,6 +1583,13 @@ export function analyze(root, opts = {}) {
|
|
|
1423
1583
|
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
1584
|
}
|
|
1425
1585
|
const keep = new Set(kept.values());
|
|
1586
|
+
// The uses left out are named on the one kept, with how far each is checked, so a reader can see them.
|
|
1587
|
+
const level = (f) => (f.guard ? (f.guard.stops ? 2 : 1) : 0);
|
|
1588
|
+
for (const [f, key] of useGroup) {
|
|
1589
|
+
if (keep.has(f)) continue;
|
|
1590
|
+
const k = kept.get(key);
|
|
1591
|
+
(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 } : {}) });
|
|
1592
|
+
}
|
|
1426
1593
|
let w = 0;
|
|
1427
1594
|
for (const f of findings) if (!useGroup.has(f) || keep.has(f)) findings[w++] = f;
|
|
1428
1595
|
findings.length = w;
|
|
@@ -1455,7 +1622,7 @@ export function analyze(root, opts = {}) {
|
|
|
1455
1622
|
}
|
|
1456
1623
|
}
|
|
1457
1624
|
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 };
|
|
1625
|
+
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
1626
|
const startedBy = {};
|
|
1460
1627
|
for (const p of programs) if (p.id && p.startedBy && !startedBy[p.id]) startedBy[p.id] = p.startedBy;
|
|
1461
1628
|
return { findings, constructs, unstarted, startedBy, stats, sourceKinds: SOURCE_KINDS, sinkKinds: SINK_KINDS, ...(sinks ? { sinks } : {}), ...(sources ? { sources } : {}) };
|