@portll/cobolwork 0.2.117 → 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/lib/card.mjs +18 -0
- package/lib/cics-commands.mjs +136 -125
- package/lib/control.mjs +118 -1
- package/lib/dataflow.mjs +67 -84
- package/lib/embedded-sql.mjs +110 -0
- package/lib/evidence/journal.mjs +11 -2
- package/lib/explain.mjs +5 -4
- package/lib/ftp.mjs +136 -0
- package/lib/ironwork.mjs +4 -6
- package/lib/jcl.mjs +27 -7
- package/lib/layout.mjs +202 -0
- package/lib/parser.mjs +52 -203
- package/lib/precompile.mjs +5 -88
- package/lib/revision.json +1 -1
- package/lib/sbom.mjs +94 -6
- package/lib/sets/cics.mjs +5 -2
- package/lib/sets/copybook.mjs +2 -1
- package/lib/sets/hidden.mjs +4 -8
- package/lib/sets/jcl.mjs +16 -119
- package/lib/sets/recon.mjs +2 -1
- package/lib/sets/zowe.mjs +34 -8
- package/lib/utilities.mjs +146 -4
- package/package.json +1 -1
- package/rules/compliance-dora.json +1 -1
- package/rules/compliance-ffiec.json +1 -1
- package/rules/compliance-nist80053.json +1 -1
- package/schema/cobolwork.policy.schema.json +94 -17
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';
|
|
@@ -68,6 +71,7 @@ STANDARD-DEVIATION RANDOM SQRT FACTORIAL LOG LOG10 EXP EXP10 PI E ACOS ASIN ATAN
|
|
|
68
71
|
SECONDS-PAST-MIDNIGHT`.split(/\s+/));
|
|
69
72
|
const computes = (st) => ARITHMETIC.has(st.verb) || st.counts === true || (st.fns || []).some((f) => NUMERIC_FUNCTIONS.has(f));
|
|
70
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; };
|
|
71
75
|
|
|
72
76
|
// Every literal a program holds that could be a program name: its VALUE clauses and the literals its
|
|
73
77
|
// statements and commands use. A menu that XCTLs through a table of names starts every one of them.
|
|
@@ -89,15 +93,6 @@ function lastOption(name, list) {
|
|
|
89
93
|
return null;
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
// Whether an item's bytes are read as decimal digits: zoned (DISPLAY with a numeric picture) or
|
|
93
|
-
// packed. A group, an edited picture, binary and floating point are not.
|
|
94
|
-
function holdsDecimal(it) {
|
|
95
|
-
if ((it.children || []).some((c) => c.level !== 88)) return false;
|
|
96
|
-
const usage = String(it.effectiveUsage || 'DISPLAY').replace('COMPUTATIONAL', 'COMP');
|
|
97
|
-
if (usage === 'COMP-3' || usage === 'PACKED-DECIMAL' || usage === 'COMP-6') return true;
|
|
98
|
-
return usage === 'DISPLAY' && !!it.picture && /9/.test(it.picture) && /^[S9VP()0-9]+$/i.test(it.picture);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
96
|
const RELATION = new Set(['>', '<', '>=', '<=', '=', '<>']);
|
|
102
97
|
const RELATION_WORDS = new Set(['GREATER', 'LESS', 'EQUAL']);
|
|
103
98
|
const FILLER_WORDS = new Set(['IS', 'NOT', 'THAN', 'TO', 'OR', 'GREATER', 'LESS', 'EQUAL', 'OF', 'IN']);
|
|
@@ -178,27 +173,6 @@ function execOptions(exec) {
|
|
|
178
173
|
return { opts, words };
|
|
179
174
|
}
|
|
180
175
|
|
|
181
|
-
function sqlHostVars(exec) {
|
|
182
|
-
const out = [];
|
|
183
|
-
for (let i = 0; i < exec.toks.length; i++) {
|
|
184
|
-
const t = exec.toks[i];
|
|
185
|
-
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 });
|
|
186
|
-
}
|
|
187
|
-
return out;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// The host variables a SELECT or FETCH fills are the ones in its INTO list, which ends at FROM.
|
|
191
|
-
// Taking every variable at or after the INTO line made the input parameters in a WHERE clause read
|
|
192
|
-
// as database values — and they are whatever the caller sent, under a rule that says data at rest.
|
|
193
|
-
function sqlIntoRange(exec) {
|
|
194
|
-
const words = exec.toks.map(t => (t.t === 'word' ? t.u : ''));
|
|
195
|
-
const into = words.indexOf('INTO');
|
|
196
|
-
if (into < 0) return null;
|
|
197
|
-
let end = exec.toks.length;
|
|
198
|
-
for (let i = into + 1; i < words.length; i++) if (words[i] === 'FROM') { end = i; break; }
|
|
199
|
-
return [into, end];
|
|
200
|
-
}
|
|
201
|
-
|
|
202
176
|
export function analyze(root, opts = {}) {
|
|
203
177
|
const repos = opts.repos || [''];
|
|
204
178
|
const stats = { files: 0, programs: 0, edges: 0, nodes: 0, threw: 0, overBudget: 0, unreadable: 0, ebcdic: 0,
|
|
@@ -457,11 +431,14 @@ export function analyze(root, opts = {}) {
|
|
|
457
431
|
usesOf.get(at.name).push(rec);
|
|
458
432
|
}
|
|
459
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;
|
|
460
437
|
const use = `${key}|${point ?? `s${st.at}`}|${offset}`;
|
|
461
438
|
if (indexed.has(use)) continue;
|
|
462
439
|
indexed.add(use);
|
|
463
440
|
at.sinks.push({
|
|
464
|
-
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 } : {}),
|
|
465
442
|
detail: kind === 'subscript'
|
|
466
443
|
? `${at.name} subscripts ${host.name}, a table of ${table.occurs}`
|
|
467
444
|
: `${at.name} sets the ${x.kind === 'refmod-offset' ? 'start' : 'length'} of a reference to ${host.name}, which is ${host.size} bytes`,
|
|
@@ -724,14 +701,16 @@ export function analyze(root, opts = {}) {
|
|
|
724
701
|
walk(record);
|
|
725
702
|
for (const field of map.fields) {
|
|
726
703
|
if (!field.name) continue;
|
|
727
|
-
const marks = ['PROT', 'ASKIP', 'DRK', 'NUM'].filter((a) => field.effective.has(a));
|
|
728
|
-
if (!marks.length) continue;
|
|
729
704
|
const iname = symbolicNames(map, field).find((n) => n.suffix === 'I');
|
|
730
705
|
const item = iname && within.get(iname.name);
|
|
731
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;
|
|
732
711
|
const n = nodeOf(item);
|
|
733
712
|
n.screen = { item: item.name, field: field.name, map: map.name, mapset: mapset.name, marks, declared: !!field.attrb };
|
|
734
|
-
if (
|
|
713
|
+
if (typed) continue;
|
|
735
714
|
stats.protectedFields++;
|
|
736
715
|
const back = field.effective.has('FSET') ? ' with FSET' : '';
|
|
737
716
|
n.sources.push({ kind: 'cics-protected-field', onlyTo: ['record-key', 'record-update'], ...at,
|
|
@@ -921,15 +900,14 @@ export function analyze(root, opts = {}) {
|
|
|
921
900
|
}
|
|
922
901
|
}
|
|
923
902
|
if (e.kind === 'SQL') {
|
|
924
|
-
//
|
|
925
|
-
//
|
|
926
|
-
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);
|
|
927
906
|
const verb = words[0];
|
|
928
|
-
const range = e.hostVariables ? null : ['SELECT', 'FETCH'].includes(verb) && sqlIntoRange(e);
|
|
929
907
|
for (const h of hv) {
|
|
930
|
-
if (
|
|
908
|
+
if (!h.written) continue;
|
|
931
909
|
const n = nodeOfToken(h.tok);
|
|
932
|
-
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` });
|
|
933
911
|
}
|
|
934
912
|
// The location CONNECT TO or SET CONNECTION names, where a host variable holds it; what
|
|
935
913
|
// follows USER and USING is the credential, not the target.
|
|
@@ -1123,59 +1101,61 @@ export function analyze(root, opts = {}) {
|
|
|
1123
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);
|
|
1124
1102
|
}
|
|
1125
1103
|
jobSteps.push({ file: f, job: job.jobs[0]?.name || null, step: step.name, line: step.line, pgm: step.pgm.toUpperCase() });
|
|
1126
|
-
// A TSO batch step starts
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
}
|
|
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)}` });
|
|
1132
1110
|
}
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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) {
|
|
1139
1118
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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}`,
|
|
1126
|
+
});
|
|
1127
|
+
stats.jclCrossings++;
|
|
1128
|
+
}
|
|
1150
1129
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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;
|
|
1158
1146
|
for (const m of callee.ddFiles || []) {
|
|
1159
1147
|
if (m.dd !== dd.name.toUpperCase()) continue;
|
|
1160
1148
|
for (const n of m.nodes) {
|
|
1161
|
-
n.
|
|
1162
|
-
|
|
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++;
|
|
1163
1154
|
}
|
|
1164
1155
|
}
|
|
1165
|
-
}
|
|
1166
|
-
if (!dd.inStream || !dd.inStream.length || !dd.name) continue;
|
|
1167
|
-
for (const m of callee.ddFiles || []) {
|
|
1168
|
-
if (m.dd !== dd.name.toUpperCase()) continue;
|
|
1169
|
-
for (const n of m.nodes) {
|
|
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++;
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
1156
|
|
|
1178
|
-
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1179
1159
|
}
|
|
1180
1160
|
}
|
|
1181
1161
|
}
|
|
@@ -1494,6 +1474,9 @@ export function analyze(root, opts = {}) {
|
|
|
1494
1474
|
// arithmetic or a numeric function carries no bad bytes to the next one. Only this route is
|
|
1495
1475
|
// dropped: a later one that copies the bytes still reports.
|
|
1496
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;
|
|
1497
1480
|
seen.add(key);
|
|
1498
1481
|
// The ends of a long path are what a reader uses: where the value came from, and what it
|
|
1499
1482
|
// reached. Keeping every hop of a thousand-hop chain, for a thousand findings, is the
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// EXEC SQL as Db2 reads it, whichever language embeds it: a statement's words, host variables and
|
|
3
|
+
// literals, and which host variables it reads and which it writes.
|
|
4
|
+
// The words, host variables and literals of a statement. A host variable is :NAME, qualified as
|
|
5
|
+
// :GROUP.NAME, and may carry an indicator variable, :NAME:IND or :NAME INDICATOR :IND.
|
|
6
|
+
export function sqlTokens(sql) {
|
|
7
|
+
const toks = [];
|
|
8
|
+
const re = /'(?:[^']|'')*'|"(?:[^"]|"")*"|:\s*[A-Z0-9_$#@-]+(?:\.[A-Z0-9_$#@-]+)*|[A-Z0-9_$#@-]+|[(),=<>+*/;.]/gi;
|
|
9
|
+
for (const m of sql.matchAll(re)) {
|
|
10
|
+
const t = m[0];
|
|
11
|
+
if (t.startsWith(':')) toks.push({ host: t.slice(1).trim().toUpperCase() });
|
|
12
|
+
else if (/^['"]/.test(t)) toks.push({ lit: t });
|
|
13
|
+
else toks.push({ word: t.toUpperCase() });
|
|
14
|
+
}
|
|
15
|
+
return toks;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Clauses that end an INTO list.
|
|
19
|
+
const AFTER_INTO = new Set(['FROM', 'WHERE', 'GROUP', 'HAVING', 'ORDER', 'FETCH', 'FOR', 'WITH', 'OPTIMIZE', 'QUERYNO', 'SKIP', 'UNION', 'USING', 'VALUES']);
|
|
20
|
+
|
|
21
|
+
// Which host variables a statement reads and which it writes, as Db2's SQL reference gives them. A
|
|
22
|
+
// variable both read and written is in both lists. INTO a host variable writes it (INSERT INTO and
|
|
23
|
+
// MERGE INTO name a table); so do SET's targets, GET DIAGNOSTICS' and ASSOCIATE LOCATORS' list. A
|
|
24
|
+
// procedure's argument that is a lone variable may be IN, OUT or INOUT, which only the server knows.
|
|
25
|
+
export function sqlRoles(toks) {
|
|
26
|
+
const sending = [];
|
|
27
|
+
const receiving = [];
|
|
28
|
+
const read = (h) => { if (!sending.includes(h)) sending.push(h); };
|
|
29
|
+
const write = (h) => { if (!receiving.includes(h)) receiving.push(h); };
|
|
30
|
+
const verb = toks.find((t) => t.word)?.word;
|
|
31
|
+
if (verb === 'CALL') return callRoles(toks, read, write), { sending, receiving };
|
|
32
|
+
// DESCRIBE and PREPARE read the SQLDA's SQLN and write the rest of it.
|
|
33
|
+
const intoBoth = verb === 'DESCRIBE' || verb === 'PREPARE';
|
|
34
|
+
let into = false;
|
|
35
|
+
let assigning = verb === 'SET';
|
|
36
|
+
let depth = 0;
|
|
37
|
+
for (let i = 0; i < toks.length; i++) {
|
|
38
|
+
const t = toks[i];
|
|
39
|
+
const next = toks[i + 1];
|
|
40
|
+
if (t.word === '(') depth++;
|
|
41
|
+
else if (t.word === ')') depth--;
|
|
42
|
+
else if (verb === 'SET' && t.word === '=' && depth === 0) assigning = false;
|
|
43
|
+
else if (verb === 'SET' && t.word === ',' && depth === 0) assigning = true;
|
|
44
|
+
if (t.word === 'INTO' && next && (next.host || next.word === 'DESCRIPTOR')) { into = true; continue; }
|
|
45
|
+
if (into && t.word && AFTER_INTO.has(t.word)) into = false;
|
|
46
|
+
if (t.word === 'DESCRIPTOR' && next?.host) {
|
|
47
|
+
// FETCH ... INTO DESCRIPTOR and USING DESCRIPTOR are synonyms: the program fills the SQLDA and Db2 writes where it points.
|
|
48
|
+
if (verb === 'FETCH' || intoBoth) { read(next.host); write(next.host); } else read(next.host);
|
|
49
|
+
i++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!t.host) continue;
|
|
53
|
+
const written = into || assigning
|
|
54
|
+
|| (verb === 'GET' && next?.word === '=')
|
|
55
|
+
|| (verb === 'ASSOCIATE' && !toks.slice(0, i).some((x) => x.word === 'WITH'));
|
|
56
|
+
if (!written || (into && intoBoth)) read(t.host);
|
|
57
|
+
if (written) write(t.host);
|
|
58
|
+
}
|
|
59
|
+
return { sending, receiving };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// CALL :name reads the name. Each argument that is a lone variable, with or without its indicator,
|
|
63
|
+
// is read and may be written; any other argument is an expression, IN only.
|
|
64
|
+
function callRoles(toks, read, write) {
|
|
65
|
+
let depth = 0;
|
|
66
|
+
let arg = [];
|
|
67
|
+
const close = () => {
|
|
68
|
+
const hosts = arg.filter((t) => t.host);
|
|
69
|
+
const lone = hosts.length && arg.every((t) => t.host || t.word === 'INDICATOR');
|
|
70
|
+
for (const h of hosts) { read(h.host); if (lone) write(h.host); }
|
|
71
|
+
arg = [];
|
|
72
|
+
};
|
|
73
|
+
for (let i = 1; i < toks.length; i++) {
|
|
74
|
+
const t = toks[i];
|
|
75
|
+
if (t.word === 'DESCRIPTOR' && toks[i + 1]?.host) { read(toks[i + 1].host); write(toks[i + 1].host); i++; continue; }
|
|
76
|
+
if (t.word === '(') { if (depth++ > 0) arg.push(t); continue; }
|
|
77
|
+
if (t.word === ')') { if (--depth > 0) arg.push(t); else close(); continue; }
|
|
78
|
+
if (depth === 0) { if (t.host) read(t.host); continue; }
|
|
79
|
+
if (depth === 1 && t.word === ',') { close(); continue; }
|
|
80
|
+
arg.push(t);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function hostVariableRoles(sql) {
|
|
85
|
+
return sqlRoles(sqlTokens(sql));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The host variables in a parsed EXEC SQL block: a colon, then a name qualified by joined periods as
|
|
89
|
+
// :GROUP.FIELD. Each carries its path, the token naming the item, where it stands, and whether the
|
|
90
|
+
// statement writes it by sqlRoles.
|
|
91
|
+
export function hostVariablesIn(toks) {
|
|
92
|
+
const found = [];
|
|
93
|
+
const sql = [];
|
|
94
|
+
for (let k = 0; k < toks.length; k++) {
|
|
95
|
+
const t = toks[k];
|
|
96
|
+
if (t.t === 'op' && t.v === ':' && toks[k + 1] && toks[k + 1].t === 'word') {
|
|
97
|
+
const path = [toks[k + 1]];
|
|
98
|
+
let j = k + 2;
|
|
99
|
+
for (; j + 1 < toks.length && toks[j].t === 'period' && toks[j].joined && toks[j + 1].t === 'word'; j += 2) path.push(toks[j + 1]);
|
|
100
|
+
const host = path.map((p) => p.u).join('.');
|
|
101
|
+
found.push({ path, tok: path[path.length - 1], at: k + 1, host });
|
|
102
|
+
sql.push({ host });
|
|
103
|
+
k = j - 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
sql.push(t.t === 'lit' ? { lit: t.v } : { word: String(t.u ?? t.v).toUpperCase() });
|
|
107
|
+
}
|
|
108
|
+
const { receiving } = sqlRoles(sql);
|
|
109
|
+
return found.map((h) => ({ ...h, written: receiving.includes(h.host) }));
|
|
110
|
+
}
|
package/lib/evidence/journal.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { randomBytes } from 'node:crypto';
|
|
5
5
|
import { closeSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
import { makeRecord, newChain } from './record.mjs';
|
|
7
|
+
import { makeRecord, newChain, recordHash } from './record.mjs';
|
|
8
8
|
import { EvidenceRefusal, createExclusive, lastLine, openAppend, prepareDir, syncClose, takeLock, writeAll } from './store.mjs';
|
|
9
9
|
|
|
10
10
|
export const LEDGER = 'ledger.jsonl';
|
|
@@ -13,12 +13,21 @@ export const LOCK = 'ledger.lock';
|
|
|
13
13
|
const isoNow = () => new Date().toISOString();
|
|
14
14
|
const runStamp = (iso) => iso.replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z');
|
|
15
15
|
|
|
16
|
+
// The record the next ledger line extends: one whose chain, seq and hash hold.
|
|
17
|
+
function ledgerTip(line, path) {
|
|
18
|
+
let r = null;
|
|
19
|
+
try { r = JSON.parse(line); } catch { /* refused below */ }
|
|
20
|
+
const ok = r && typeof r === 'object' && /^[0-9a-f]{32}$/.test(r.chain) && Number.isInteger(r.seq) && r.seq >= 0 && typeof r.hash === 'string' && recordHash(r) === r.hash;
|
|
21
|
+
if (!ok) throw new EvidenceRefusal(`${path} ends in a line that is no ledger record; verify it before anything is appended`);
|
|
22
|
+
return r;
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
// Appends to the ledger; the caller holds the lock. A torn ledger is refused rather than extended.
|
|
17
26
|
function appendLedger(dir, entries, now) {
|
|
18
27
|
const path = join(dir, LEDGER);
|
|
19
28
|
const { line, torn } = lastLine(path);
|
|
20
29
|
if (torn) throw new EvidenceRefusal(`${path} ends in a partial line; verify it before anything is appended`);
|
|
21
|
-
let prev = line ?
|
|
30
|
+
let prev = line ? ledgerTip(line, path) : null;
|
|
22
31
|
const fd = openAppend(path);
|
|
23
32
|
try {
|
|
24
33
|
const out = [];
|
package/lib/explain.mjs
CHANGED
|
@@ -7,15 +7,16 @@ import { EVIDENCE } from './kernel/findings.mjs';
|
|
|
7
7
|
import { REGISTRY } from './kernel/registry.mjs';
|
|
8
8
|
import { WHO_ACTS } from './tui/model.mjs';
|
|
9
9
|
import { verificationPlan } from './verify.mjs';
|
|
10
|
+
import { cobolCard, statementCard } from './card.mjs';
|
|
10
11
|
|
|
11
12
|
const HIDDEN_RULES = new Set(Object.keys(REGISTRY.find((s) => s.name === 'hidden').rules));
|
|
12
13
|
|
|
13
14
|
// The columns quoted: COBOL's indicator and program text, JCL's statement field, or the whole line.
|
|
14
15
|
function codeArea(line, kind, format) {
|
|
15
|
-
if (kind === '
|
|
16
|
-
if (kind
|
|
17
|
-
|
|
18
|
-
return { text:
|
|
16
|
+
if (kind === 'jcl') { const c = statementCard(line); return { text: c.text, seq: '', rest: c.sequence }; }
|
|
17
|
+
if (kind !== 'cobol') return { text: line, seq: '', rest: '' };
|
|
18
|
+
const c = cobolCard(line, format);
|
|
19
|
+
return { text: c.indicator + c.text, seq: c.sequence, rest: c.ignored };
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const NOTE = 'This packet contains source text from the files it names. A cobolwork report never does: '
|
package/lib/ftp.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
import { statementCard } from './card.mjs';
|
|
3
|
+
|
|
4
|
+
// IBM's batch FTP client takes the host and its options on PARM, or the host as the first line of
|
|
5
|
+
// its input, reads subcommands from INPUT, and logs on from NETRC or from its input. CardDemo's job
|
|
6
|
+
// gives the same lines on SYSIN, which is read here when there is no INPUT, though the page below
|
|
7
|
+
// does not say the client falls back to it.
|
|
8
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.4.0/com.ibm.zos.v2r4.halu001/ftpreq.htm
|
|
9
|
+
// A session is in cleartext unless something asks for TLS: -a TLS or -r TLS on PARM, or
|
|
10
|
+
// SECURE_MECHANISM TLS in an in-stream SYSFTPD. A SYSFTPD naming a data set is configuration this
|
|
11
|
+
// reader cannot see, so that step is not called cleartext either way.
|
|
12
|
+
// https://www.ibm.com/docs/en/zos/2.1.0?topic=commands-ftp-command
|
|
13
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.4.0/com.ibm.zos.v2r4.halz002/ftp_cust_client_for_tls.htm
|
|
14
|
+
// PUT, MPUT and APPEND send local files, and IBM's capitals - PUt, MPut, APpend, LCd - are the
|
|
15
|
+
// shortest spellings. A local name in quotes is a whole data set name, //DD: names a DD of the
|
|
16
|
+
// step, and anything else hangs off the local working directory, which only a quoted LCD fixes.
|
|
17
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/ftpsubcmd.htm
|
|
18
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/put.htm
|
|
19
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/mput.htm
|
|
20
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/append.htm
|
|
21
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/lcd.htm
|
|
22
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/dd_name_support.htm
|
|
23
|
+
// Get and MGet copy remote files in, each to the local name given or, with none, to the foreign
|
|
24
|
+
// file's own name, placed as a sent name is. SIte FILETYPE=JES makes the remote host submit what
|
|
25
|
+
// is sent as a job and return job output to a Get; FILETYPE=SQL makes it run a query. Either holds
|
|
26
|
+
// for the transfers after it, until another FILETYPE.
|
|
27
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/get.htm
|
|
28
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/mget.htm
|
|
29
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.halu001/site.htm
|
|
30
|
+
const FTP_SENDS = [[/^PUT?$/i, 'PUT'], [/^MP(?:UT?)?$/i, 'MPUT'], [/^AP(?:P(?:E(?:ND?)?)?)?$/i, 'APPEND']];
|
|
31
|
+
|
|
32
|
+
// Every subcommand on the page above, in IBM's spelling.
|
|
33
|
+
const FTP_SUBCOMMANDS = ['?', '!', 'ACCount', 'APpend', 'AUth', 'AScii', 'BIG5', 'BINary', 'BLock', 'CCc', 'CD',
|
|
34
|
+
'CDUp', 'CLEar', 'CLose', 'COMpress', 'CProtect', 'CWd', 'DEBug', 'DELEte', 'DELImit', 'DIr', 'DUMP', 'EBcdic',
|
|
35
|
+
'EUckanji', 'FEature', 'FIle', 'Get', 'GLob', 'HAngeul', 'HElp', 'Ibmkanji', 'JIS78kj', 'JIS83kj', 'Ksc5601',
|
|
36
|
+
'LANGuage', 'LCd', 'LOCSIte', 'LOCSTat', 'LMkdir', 'LPwd', 'LS', 'MDelete', 'MGet', 'MKdir', 'MKFifo', 'MOde',
|
|
37
|
+
'MPut', 'MVSGet', 'MVSPut', 'NOop', 'Open', 'PAss', 'PRIvate', 'PROMpt', 'PROTect', 'PROXy', 'PUt', 'PWd', 'QUIt',
|
|
38
|
+
'QUOte', 'RECord', 'REName', 'REStart', 'RMdir', 'SAfe', 'SChinese', 'SENDPort', 'SENDSite', 'SIte', 'SJiskanji',
|
|
39
|
+
'SRestart', 'STAtus', 'STREam', 'STRucture', 'SUnique', 'SYstem', 'TChinese', 'TSO', 'TYpe', 'UCs2', 'User', 'Verbose']
|
|
40
|
+
.map((name) => ({ full: name.toUpperCase(), min: name.match(/^[^a-z]*/)[0].length }));
|
|
41
|
+
const subcommandOf = (word) => {
|
|
42
|
+
const w = word.toUpperCase();
|
|
43
|
+
const hit = FTP_SUBCOMMANDS.find((c) => w.length >= c.min && c.full.startsWith(w));
|
|
44
|
+
return hit ? hit.full : null;
|
|
45
|
+
};
|
|
46
|
+
// A value that stands for a password rather than being one: a symbolic, a mask, or the word itself.
|
|
47
|
+
const PLACEHOLDER = /^(&.*|\*+|X+|\?+|<.*>|\{.*\}|PASSWORD|PASSWD|PWD|PASS|SECRET)$/i;
|
|
48
|
+
|
|
49
|
+
// The session one FTP step runs, from its DDs grouped by name (ddGroups in lib/utilities.mjs).
|
|
50
|
+
export function ftpSession(step, dds) {
|
|
51
|
+
let host = null, tls = null;
|
|
52
|
+
const words = (step.parm || '').split('(')[0].trim().split(/\s+/).filter(Boolean);
|
|
53
|
+
for (let i = 0; i < words.length; i++) {
|
|
54
|
+
if (/^-[ar]$/i.test(words[i])) {
|
|
55
|
+
const mechanism = (words[++i] || '').toUpperCase();
|
|
56
|
+
if (mechanism === 'TLS') tls = 'asked';
|
|
57
|
+
else if (mechanism === 'NEVER' && !tls) tls = 'never';
|
|
58
|
+
} else if (!words[i].startsWith('-') && !host) host = words[i];
|
|
59
|
+
}
|
|
60
|
+
let configUnread = null;
|
|
61
|
+
for (const dd of dds.get('SYSFTPD') || []) {
|
|
62
|
+
if (dd.inStream) { if (tls !== 'never' && dd.inStream.some((l) => /^\s*SECURE_MECHANISM\s+TLS\b/i.test(l.text))) tls = 'asked'; }
|
|
63
|
+
else if (dd.dsn && !configUnread) configUnread = dd.dsn;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const input = dds.get('INPUT') || dds.get('SYSIN') || [];
|
|
67
|
+
const hostLine = !host && input[0]?.inStream;
|
|
68
|
+
let inputUnread = null, lcd = null, unixDir = false;
|
|
69
|
+
const sends = [], gets = [];
|
|
70
|
+
let filetype = null;
|
|
71
|
+
// With no NETRC to answer for it, the client asks for a user ID and a password and in batch reads
|
|
72
|
+
// them from its input after the host: "user password" on one line, or each on its own. A first
|
|
73
|
+
// line that is a subcommand means something else answered, a NETRC the client found for itself.
|
|
74
|
+
// USER and PASS give the same answers later in a session.
|
|
75
|
+
let expect = dds.has('NETRC') ? null : 'logon', user = null;
|
|
76
|
+
const passwords = [];
|
|
77
|
+
const password = (dd, line, value) => { if (!PLACEHOLDER.test(value)) passwords.push({ dd, line, user }); };
|
|
78
|
+
const local = (name) => {
|
|
79
|
+
const dd = /^\/\/DD:([A-Z$#@][A-Z0-9$#@]{0,7})$/i.exec(name);
|
|
80
|
+
if (dd) return { dd: dd[1].toUpperCase(), dsns: (dds.get(dd[1].toUpperCase()) || []).map((d) => d.dsn).filter(Boolean) };
|
|
81
|
+
const quoted = /^'(.+)'$/.exec(name);
|
|
82
|
+
if (quoted) return { dsns: [quoted[1].toUpperCase()] };
|
|
83
|
+
if (name.startsWith('/') || unixDir) return { dsns: [] };
|
|
84
|
+
return lcd ? { dsns: [`${lcd}.${name.toUpperCase()}`] } : { dsns: [], relative: name };
|
|
85
|
+
};
|
|
86
|
+
for (const dd of input) {
|
|
87
|
+
if (!dd.inStream) { if (dd.dsn && !inputUnread) inputUnread = dd.dsn; continue; }
|
|
88
|
+
for (const { line, text } of dd.inStream) {
|
|
89
|
+
const [verb, ...args] = statementCard(text).text.trim().split(/\s+/);
|
|
90
|
+
if (!verb) continue;
|
|
91
|
+
if (hostLine && !host) { host = verb; continue; }
|
|
92
|
+
const sub = subcommandOf(verb);
|
|
93
|
+
if (expect && !sub) {
|
|
94
|
+
if (expect === 'logon') user = verb;
|
|
95
|
+
if (expect === 'password') password(dd, line, verb);
|
|
96
|
+
else if (args[0]) password(dd, line, args[0]);
|
|
97
|
+
expect = expect === 'logon' && !args[0] ? 'password' : null;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
expect = null;
|
|
101
|
+
if (sub === 'USER' && args[0]) {
|
|
102
|
+
user = args[0];
|
|
103
|
+
if (args[1]) password(dd, line, args[1]); else expect = 'password';
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (sub === 'PASS' && args[0]) { password(dd, line, args[0]); continue; }
|
|
107
|
+
if (/^LCD?$/i.test(verb) && args[0]) {
|
|
108
|
+
const quoted = /^'(.+)'$/.exec(args[0]);
|
|
109
|
+
unixDir = args[0].startsWith('/');
|
|
110
|
+
lcd = quoted ? quoted[1].toUpperCase() : lcd && !unixDir ? `${lcd}.${args[0].toUpperCase()}` : null;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const site = sub === 'SITE' ? args : sub === 'QUOTE' && subcommandOf(args[0] || '') === 'SITE' ? args.slice(1) : null;
|
|
114
|
+
if (site) {
|
|
115
|
+
for (const a of site) { const t = /^FILE(?:T(?:Y(?:P(?:E)?)?)?)?=(\w+)$/i.exec(a); if (t) filetype = t[1].toUpperCase() === 'SEQ' ? null : t[1].toUpperCase(); }
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const typed = filetype ? { filetype } : {};
|
|
119
|
+
if (sub === 'GET' && args[0]) {
|
|
120
|
+
const to = args[1] && !args[1].startsWith('(') ? args[1] : args[0];
|
|
121
|
+
gets.push({ line, verb: 'GET', name: args[0], ...typed, ...local(to) });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (sub === 'MGET') {
|
|
125
|
+
for (const name of args.filter((a) => !a.startsWith('(') && !/[*?]/.test(a))) gets.push({ line, verb: 'MGET', name, ...typed, ...local(name) });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const sent = FTP_SENDS.find(([re]) => re.test(verb));
|
|
129
|
+
if (!sent) continue;
|
|
130
|
+
const foreign = sent[1] !== 'MPUT' && args[1] && !args[1].startsWith('(') ? args[1] : null;
|
|
131
|
+
for (const name of sent[1] === 'MPUT' ? args : args.slice(0, 1)) sends.push({ line, verb: sent[1], name, foreign: foreign || name, ...typed, ...local(name) });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const cleartext = tls === 'never' || (!tls && !configUnread);
|
|
135
|
+
return { host, cleartext, configUnread, inputUnread, logon: dds.has('NETRC') ? 'NETRC' : null, sends, gets, passwords, input };
|
|
136
|
+
}
|
package/lib/ironwork.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { tmpdir } from 'node:os';
|
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
8
|
import { directoryTree } from './kernel/source-tree.mjs';
|
|
9
9
|
import { printable } from './kernel/printable.mjs';
|
|
10
|
-
import { buildFileIndex
|
|
10
|
+
import { buildFileIndex } from './parser.mjs';
|
|
11
11
|
import { isProgram, relPath } from './sources.mjs';
|
|
12
12
|
import { EIB_FIELDS, DIB_FIELDS, SQLCA_FIELDS } from './words.mjs';
|
|
13
13
|
|
|
@@ -20,15 +20,13 @@ const NOT_AN_ERROR = /^(?:informational:|.*?: warning: )/;
|
|
|
20
20
|
const PER_PROGRAM_MS = 60000;
|
|
21
21
|
const MAX_LISTED = 10;
|
|
22
22
|
|
|
23
|
-
// ironwork refuses by name what it does not model yet; that says nothing about the program.
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// ironwork refuses by name what it does not model yet; that says nothing about the program. A name
|
|
24
|
+
// the CICS, DL/I or SQL translator declares it reports as undefined, without saying so, and that is
|
|
25
|
+
// read as its gap, not the program's.
|
|
26
26
|
const NOT_SUPPORTED = /\bnot supported\b/i;
|
|
27
27
|
const TRANSLATOR_NAMES = [EIB_FIELDS, DIB_FIELDS, SQLCA_FIELDS];
|
|
28
28
|
function notModelled(message) {
|
|
29
29
|
if (NOT_SUPPORTED.test(message)) return true;
|
|
30
|
-
const verb = /^a statement, found ([A-Z][A-Z-]*)$/.exec(message);
|
|
31
|
-
if (verb) return VERBS.has(verb[1]);
|
|
32
30
|
const name = /^([A-Z][A-Z0-9-]*) is not defined$/.exec(message);
|
|
33
31
|
return !!name && TRANSLATOR_NAMES.some((s) => s.has(name[1]));
|
|
34
32
|
}
|
package/lib/jcl.mjs
CHANGED
|
@@ -12,17 +12,14 @@
|
|
|
12
12
|
// output as `unreadable`, never dropped, because a step nobody parsed is not a step that is not
|
|
13
13
|
// there.
|
|
14
14
|
import { readSource } from './sources.mjs';
|
|
15
|
-
import { copiesOf } from './utilities.mjs';
|
|
15
|
+
import { copiesOf, tsoCommands } from './utilities.mjs';
|
|
16
|
+
import { statementCard } from './card.mjs';
|
|
16
17
|
|
|
17
18
|
// The operations a statement may carry. Anything else in the operation field is a statement this
|
|
18
19
|
// reader does not know, which is a diagnostic rather than a silent skip.
|
|
19
20
|
export const OPERATIONS = new Set(['JOB', 'EXEC', 'DD', 'PROC', 'PEND', 'SET', 'IF', 'THEN',
|
|
20
21
|
'ELSE', 'ENDIF', 'INCLUDE', 'JCLLIB', 'OUTPUT', 'CNTL', 'ENDCNTL', 'XMIT', 'COMMAND', 'NOTIFY', 'EXPORT']);
|
|
21
22
|
|
|
22
|
-
// Columns 73 to 80 are the sequence area. They are not the statement, and a payload hidden there
|
|
23
|
-
// is the hidden-content rules' business, not this reader's.
|
|
24
|
-
const STATEMENT_COLUMNS = 72;
|
|
25
|
-
|
|
26
23
|
// A continued operand resumes in these columns. Outside them it is not a continuation, whatever
|
|
27
24
|
// the previous line ended with.
|
|
28
25
|
const CONTINUE_FROM = 4;
|
|
@@ -146,8 +143,10 @@ export function foldStatements(src) {
|
|
|
146
143
|
const line = i + 1;
|
|
147
144
|
const raw = phys[i];
|
|
148
145
|
if (raw.length > 80) diags.push({ sev: 'warn', line, text: 'line is longer than 80 columns' });
|
|
149
|
-
|
|
150
|
-
const
|
|
146
|
+
// The sequence area is not the statement; a payload hidden there is the hidden-content rules' business.
|
|
147
|
+
const card = statementCard(raw);
|
|
148
|
+
const text = card.text.replace(/\s+$/, '');
|
|
149
|
+
const sequence = card.sequence;
|
|
151
150
|
|
|
152
151
|
if (stream) {
|
|
153
152
|
// A custom DLM ends the stream and nothing else does. The default ends at /* or at the next
|
|
@@ -402,6 +401,27 @@ export function parseJcl(src, file, opts = {}) {
|
|
|
402
401
|
else diags.push({ sev: 'warn', line: 0, text: `symbolic parameter &${s} has no value, so the operands using it were read unsubstituted` });
|
|
403
402
|
}
|
|
404
403
|
|
|
404
|
+
// A TSO batch step's ALLOCATE commands define DDs its JCL does not show. Each is kept as a DD of the
|
|
405
|
+
// step, marked `allocated`, with the datasets of a list concatenated under it as JCL would.
|
|
406
|
+
for (const s of steps) {
|
|
407
|
+
const tso = tsoCommands(s);
|
|
408
|
+
for (const n of tso.notes) diags.push({ sev: 'info', ...n });
|
|
409
|
+
for (const a of tso.allocations) {
|
|
410
|
+
const ends = a.datasets.length ? a.datasets : [{ written: null, dsn: null }];
|
|
411
|
+
ends.forEach((d, i) => {
|
|
412
|
+
const dd = {
|
|
413
|
+
name: i === 0 ? a.dd : null, procStep: null, dsn: d.dsn, rawDsn: d.written, referback: null,
|
|
414
|
+
disp: a.status, access: accessOf(a.status), temporary: false, sysout: a.sysout,
|
|
415
|
+
inStream: null, dlm: null, concatenated: i > 0, keywords: new Map(), line: a.line, endLine: a.line,
|
|
416
|
+
step: s.name, allocated: true,
|
|
417
|
+
};
|
|
418
|
+
dds.push(dd);
|
|
419
|
+
s.dds.push(dd);
|
|
420
|
+
stepOf.set(dd, s);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
405
425
|
// What each step's program copies, from lib/utilities.mjs. A program that table does not know
|
|
406
426
|
// copies nothing here, which is not the same as copying nothing.
|
|
407
427
|
const copies = [];
|