@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/utilities.mjs
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
// statements are not in the job - SYSIN naming a library member - says so, because its copies are
|
|
9
9
|
// then only the ones its DD names decide on their own.
|
|
10
10
|
|
|
11
|
+
import { ftpSession } from './ftp.mjs';
|
|
12
|
+
import { statementCard } from './card.mjs';
|
|
13
|
+
|
|
11
14
|
const upper = (s) => String(s).toUpperCase();
|
|
12
15
|
|
|
13
16
|
// The DDs of a step by name, each followed by the datasets concatenated to it.
|
|
@@ -93,7 +96,7 @@ function commands(lines) {
|
|
|
93
96
|
let comment = false;
|
|
94
97
|
for (const { line, text } of lines) {
|
|
95
98
|
let t = '';
|
|
96
|
-
const s = text.
|
|
99
|
+
const s = statementCard(text).text;
|
|
97
100
|
for (let i = 0; i < s.length; i++) {
|
|
98
101
|
if (comment) { if (s[i] === '*' && s[i + 1] === '/') { comment = false; i++; } continue; }
|
|
99
102
|
if (s[i] === '/' && s[i + 1] === '*') { comment = true; i++; continue; }
|
|
@@ -129,6 +132,12 @@ const who = (step) => `step ${step.name || '(unnamed)'}`;
|
|
|
129
132
|
// Control statements only edit records or split members, so the copy is the DDs' alone.
|
|
130
133
|
// https://www.ibm.com/docs/api/v1/content/zosbasics/com.ibm.zos.zdatamgmt/zsysprogc_utilities_IEBGENER.htm
|
|
131
134
|
// https://www.ibm.com/docs/en/zos/3.1.0?topic=performance-use-icegener-instead-iebgener
|
|
135
|
+
// IEBPTPCH reads SYSUT1 and writes its print or punch output to SYSUT2, and IEBUPDTE reads its old
|
|
136
|
+
// master from SYSUT1 and writes SYSUT2. IEBUPDTE's control data set may also carry input data for the
|
|
137
|
+
// new master, which is not followed here.
|
|
138
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.idau100/u1367.htm
|
|
139
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.idau100/u1427.htm
|
|
140
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.idau100/u1425.htm
|
|
132
141
|
function generCopy(step, dds) {
|
|
133
142
|
if (!dds.has('SYSUT1') || !dds.has('SYSUT2')) return { moves: [], notes: [] };
|
|
134
143
|
return { moves: [{ from: [{ dd: 'SYSUT1' }], to: [{ dd: 'SYSUT2' }], line: step.line }], notes: [] };
|
|
@@ -174,29 +183,74 @@ function sortCopies(step, dds) {
|
|
|
174
183
|
// https://www.ibm.com/docs/en/zos/3.1.0?topic=commands-repro
|
|
175
184
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2253.htm
|
|
176
185
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.idai200/continu.htm
|
|
186
|
+
// EXPORT writes the cluster its first parameter names, or the one INFILE(ddname) identifies, to the
|
|
187
|
+
// portable data set OUTFILE(ddname) or OUTDATASET(entryname) gives, and may be abbreviated EXP.
|
|
188
|
+
// IMPORT reads the portable data set INFILE(ddname) or INDATASET(entryname) names and writes the
|
|
189
|
+
// cluster OUTFILE(ddname) or OUTDATASET(entryname) names.
|
|
190
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/export.htm
|
|
191
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2210.htm
|
|
192
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i211.htm
|
|
193
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/import.htm
|
|
194
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2221.htm
|
|
195
|
+
// PRINT reads INFILE(ddname) or INDATASET(entryname) and writes the listing to OUTFILE(ddname),
|
|
196
|
+
// SYSPRINT when it names none, so a production data set printed to SYSOUT leaves by that DD.
|
|
197
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/print.htm
|
|
198
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2244.htm
|
|
199
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2245.htm
|
|
200
|
+
// ALTER entryname NEWNAME(newname), abbreviated NEWNM, gives the entry a new name, so the data the
|
|
201
|
+
// old name held is read under the new one. A generic name (both must then be generic) or a member
|
|
202
|
+
// of a partitioned data set leaves the data set where it was, and is not followed.
|
|
203
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2052.htm
|
|
204
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2055.htm
|
|
205
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2056.htm
|
|
206
|
+
function renamed(cmd) {
|
|
207
|
+
const m = /^ALTER\s+'?([^\s(),'*]+)'?(?=[\s,]|$)/i.exec(cmd.text);
|
|
208
|
+
const to = m && param(cmd.text, ['NEWNAME', 'NEWNM']);
|
|
209
|
+
if (!to || /[*(]/.test(to[0])) return null;
|
|
210
|
+
return { from: [{ dsn: upper(m[1]) }], to: [{ dsn: to[0] }], line: cmd.line };
|
|
211
|
+
}
|
|
212
|
+
|
|
177
213
|
function reproCopies(step, dds) {
|
|
178
214
|
const { lines, unread } = cards(dds.get('SYSIN'));
|
|
179
|
-
const notes = unread ? [{ line: step.line, text: `${who(step)} runs IDCAMS with commands in ${unread}, which this reader cannot see, so any REPRO it runs is not known` }] : [];
|
|
215
|
+
const notes = unread ? [{ line: step.line, text: `${who(step)} runs IDCAMS with commands in ${unread}, which this reader cannot see, so any REPRO, EXPORT, IMPORT, PRINT or rename it runs is not known` }] : [];
|
|
180
216
|
const moves = [];
|
|
181
217
|
for (const cmd of commands(lines)) {
|
|
182
|
-
if (
|
|
183
|
-
|
|
218
|
+
if (/^ALTER\b/i.test(cmd.text)) {
|
|
219
|
+
const move = renamed(cmd);
|
|
220
|
+
if (move) moves.push(move);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (/^PRINT\b/i.test(cmd.text)) {
|
|
224
|
+
const input = param(cmd.text, ['INFILE', 'IFILE']);
|
|
225
|
+
const ds = !input && param(cmd.text, ['INDATASET', 'IDS']);
|
|
226
|
+
const out = param(cmd.text, ['OUTFILE', 'OFILE']);
|
|
227
|
+
const from = input ? [{ dd: input[0] }] : ds ? [{ dsn: ds[0] }] : [];
|
|
228
|
+
moves.push({ from, to: [{ dd: out ? out[0] : 'SYSPRINT' }], line: cmd.line });
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const verb = /^(REPRO|EXPORT|EXP|IMPORT)\b/i.exec(cmd.text);
|
|
232
|
+
if (!verb) continue;
|
|
233
|
+
const entry = /^EXP/i.test(verb[1]) && /^\S+\s+([^\s(),]+)/.exec(cmd.text);
|
|
234
|
+
const end = (ddNames, dsNames, fallback) => {
|
|
184
235
|
const dd = param(cmd.text, ddNames);
|
|
185
236
|
if (dd) return [{ dd: dd[0] }];
|
|
186
237
|
const ds = param(cmd.text, dsNames);
|
|
187
|
-
|
|
238
|
+
if (ds) return [{ dsn: ds[0] }];
|
|
239
|
+
return fallback ? [{ dsn: upper(fallback.replace(/^'|'$/g, '')) }] : [];
|
|
188
240
|
};
|
|
189
|
-
moves.push({ from: end(['INFILE', 'IFILE'], ['INDATASET', 'IDS']), to: end(['OUTFILE', 'OFILE'], ['OUTDATASET', 'ODS']), line: cmd.line });
|
|
241
|
+
moves.push({ from: end(['INFILE', 'IFILE'], ['INDATASET', 'IDS'], entry && entry[1]), to: end(['OUTFILE', 'OFILE'], ['OUTDATASET', 'ODS']), line: cmd.line });
|
|
190
242
|
}
|
|
191
243
|
return { moves, notes };
|
|
192
244
|
}
|
|
193
245
|
|
|
194
|
-
// IEBCOPY COPY and
|
|
246
|
+
// IEBCOPY COPY, COPYGRP and COPYMOD name their output with OUTDD and their inputs with INDD, where an input
|
|
195
247
|
// written (ddname,R) replaces members of the same name. An INDD= on a record of its own begins
|
|
196
248
|
// another step of the COPY before it. A statement continues after a comma or a mark in column 72.
|
|
197
249
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_3.1.0/com.ibm.zos.v3r1.idau100/copy.htm
|
|
198
250
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_3.1.0/com.ibm.zos.v3r1.idau100/copygrp.htm
|
|
199
251
|
// https://www.ibm.com/docs/en/zos/2.4.0?topic=ie-example-15-copy-groups-from-pdse-pdse-replace
|
|
252
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_3.1.0/com.ibm.zos.v3r1.idau100/copymd.htm
|
|
253
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_3.1.0/com.ibm.zos.v3r1.idau100/u1055.htm
|
|
200
254
|
function iebcopyCopies(step, dds) {
|
|
201
255
|
const { lines, unread } = cards(dds.get('SYSIN'));
|
|
202
256
|
const notes = unread ? [{ line: step.line, text: `${who(step)} runs IEBCOPY with statements in ${unread}, which this reader cannot see, so what it copies is not known` }] : [];
|
|
@@ -206,7 +260,7 @@ function iebcopyCopies(step, dds) {
|
|
|
206
260
|
for (const st of statements) {
|
|
207
261
|
// A record holding only INDD= has no operation, so what reads as its operation is its operand.
|
|
208
262
|
const alone = /^INDD=/.test(st.op);
|
|
209
|
-
if (st.op === 'COPY' || st.op === 'COPYGRP') out = null;
|
|
263
|
+
if (st.op === 'COPY' || st.op === 'COPYGRP' || st.op === 'COPYMOD') out = null;
|
|
210
264
|
else if (!alone) {
|
|
211
265
|
if (st.op !== 'SELECT' && st.op !== 'EXCLUDE') out = null;
|
|
212
266
|
continue;
|
|
@@ -227,35 +281,103 @@ function iebcopyCopies(step, dds) {
|
|
|
227
281
|
// each OUTDDNAME, up to 255 of them. RESTORE reads the dump data set INDDNAME names and writes the
|
|
228
282
|
// OUTDDNAME volumes. With no input DD a logical dump chooses its datasets by filter, and with no
|
|
229
283
|
// output DD a logical restore puts them where the catalogue says, neither of which the job shows.
|
|
284
|
+
// COPY reads the volumes INDDNAME or LOGINDDNAME name and writes the DASD volumes OUTDDNAME names.
|
|
230
285
|
// https://www.ibm.com/docs/en/zos/3.1.0?topic=commands-command-syntax
|
|
231
286
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.4.0/com.ibm.zos.v2r4.adru000/r2319.htm
|
|
232
287
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.4.0/com.ibm.zos.v2r4.adru000/r2321.htm
|
|
233
288
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.4.0/com.ibm.zos.v2r4.adru000/r2327.htm
|
|
234
289
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.adru000/dgt3u2165.htm
|
|
235
290
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.adru000/dgt3u2174.htm
|
|
291
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.adru000/r2172.htm
|
|
292
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.adru000/r2204.htm
|
|
293
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.adru000/r2206.htm
|
|
294
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.adru000/r2216.htm
|
|
236
295
|
function dssCopies(step, dds) {
|
|
237
296
|
const { lines, unread } = cards(dds.get('SYSIN'));
|
|
238
|
-
const notes = unread ? [{ line: step.line, text: `${who(step)} runs ADRDSSU with commands in ${unread}, which this reader cannot see, so what it dumps or
|
|
297
|
+
const notes = unread ? [{ line: step.line, text: `${who(step)} runs ADRDSSU with commands in ${unread}, which this reader cannot see, so what it dumps, restores or copies is not known` }] : [];
|
|
239
298
|
const moves = [];
|
|
240
299
|
for (const cmd of commands(lines)) {
|
|
241
|
-
const op = /^(DUMP|RESTORE)\b/i.exec(cmd.text);
|
|
300
|
+
const op = /^(DUMP|RESTORE|COPY)\b/i.exec(cmd.text);
|
|
242
301
|
if (!op) continue;
|
|
243
|
-
const
|
|
244
|
-
const
|
|
302
|
+
const verb = upper(op[1]);
|
|
303
|
+
const dump = verb === 'DUMP';
|
|
304
|
+
const ins = param(cmd.text, ['INDDNAME', 'INDD', 'IDD']) || (verb !== 'RESTORE' && param(cmd.text, ['LOGINDDNAME', 'LOGINDD', 'LIDD'])) || [];
|
|
245
305
|
const outs = param(cmd.text, ['OUTDDNAME', 'OUTDD', 'ODD']) || [];
|
|
246
306
|
if (dump && !ins.length) notes.push({ line: cmd.line, text: `${who(step)} dumps datasets chosen by filter from the catalogue, so which ones it copies is not known here` });
|
|
247
|
-
if (
|
|
307
|
+
if (verb === 'COPY' && !ins.length) notes.push({ line: cmd.line, text: `${who(step)} copies datasets whose input volume the command names no DD for, so where they come from is not known here` });
|
|
308
|
+
if (verb === 'COPY' && !outs.length) notes.push({ line: cmd.line, text: `${who(step)} copies datasets to an output volume the command names no DD for, so what it writes is not known here` });
|
|
309
|
+
if (verb === 'RESTORE' && !outs.length) notes.push({ line: cmd.line, text: `${who(step)} restores datasets to where the catalogue puts them, so what it writes is not known here` });
|
|
248
310
|
moves.push({ from: ins.map((dd) => ({ dd })), to: outs.map((dd) => ({ dd })), line: cmd.line });
|
|
249
311
|
}
|
|
250
312
|
return { moves, notes };
|
|
251
313
|
}
|
|
252
314
|
|
|
315
|
+
// ICETOOL runs the operators in TOOLIN. COPY, SORT and MERGE read the DDs their FROM operands name
|
|
316
|
+
// and write the DDs their TO operands name, several ddnames to an operand. A statement's operands
|
|
317
|
+
// continue on the next line after a hyphen, and what follows the hyphen is ignored. USING(xxxx) reads
|
|
318
|
+
// DFSORT control statements from the xxxxCNTL DD, whose OUTFIL statements can write further data sets.
|
|
319
|
+
// https://www.ibm.com/docs/en/zos/3.1.0?topic=icetool-copy-operator
|
|
320
|
+
// https://www.ibm.com/docs/en/zos/3.1.0?topic=icetool-sort-operator
|
|
321
|
+
// https://www.ibm.com/docs/en/zos/3.1.0?topic=icetool-merge-operator
|
|
322
|
+
// https://www.ibm.com/docs/en/zos/2.3.0?topic=icetool-job-control-language
|
|
323
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_3.1.0/com.ibm.zos.v3r1.icea100/ice2ca_General_coding_rules.htm
|
|
324
|
+
function icetoolCopies(step, dds) {
|
|
325
|
+
const { lines, unread } = cards(dds.get('TOOLIN'));
|
|
326
|
+
const notes = unread ? [{ line: step.line, text: `${who(step)} runs ICETOOL with statements in ${unread}, which this reader cannot see, so what it copies is not known` }] : [];
|
|
327
|
+
const statements = [];
|
|
328
|
+
let cur = null;
|
|
329
|
+
for (const { line, text } of lines) {
|
|
330
|
+
const s = statementCard(text).text;
|
|
331
|
+
if (!cur && (/^\*/.test(s) || !s.trim())) continue;
|
|
332
|
+
const tokens = s.trim().split(/\s+/);
|
|
333
|
+
const hyphen = tokens.findIndex((t) => t.endsWith('-'));
|
|
334
|
+
const used = hyphen < 0 ? tokens : tokens.slice(0, hyphen + 1);
|
|
335
|
+
if (!cur) cur = { line, op: upper(used.shift()), operands: [] };
|
|
336
|
+
cur.operands.push(...used.map((t) => t.replace(/-$/, '')).filter(Boolean));
|
|
337
|
+
if (hyphen < 0) { statements.push(cur); cur = null; }
|
|
338
|
+
}
|
|
339
|
+
if (cur) statements.push(cur);
|
|
340
|
+
const moves = [];
|
|
341
|
+
for (const st of statements) {
|
|
342
|
+
if (!['COPY', 'SORT', 'MERGE'].includes(st.op)) continue;
|
|
343
|
+
const named = (kw) => st.operands.flatMap((o) => {
|
|
344
|
+
const m = new RegExp(`^${kw}\\((.*)\\)$`, 'i').exec(o);
|
|
345
|
+
return m ? m[1].split(',').map((n) => upper(n.trim())).filter(Boolean) : [];
|
|
346
|
+
});
|
|
347
|
+
moves.push({ from: named('FROM').map((dd) => ({ dd })), to: named('TO').map((dd) => ({ dd })), line: st.line });
|
|
348
|
+
for (const using of named('USING')) {
|
|
349
|
+
const control = cards(dds.get(`${using}CNTL`));
|
|
350
|
+
if (control.unread || utilityStatements(control.lines, (ops) => /[,;:]$/.test(ops)).some((c) => c.op === 'OUTFIL')) {
|
|
351
|
+
notes.push({ line: st.line, text: `${who(step)} runs ICETOOL ${st.op} with control statements in ${using}CNTL that may write OUTFIL data sets, which are not followed here` });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return { moves, notes };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// An FTP step's transfers, from the session lib/ftp.mjs reads: what PUt, MPut and APpend send leaves
|
|
359
|
+
// for the remote host, and what Get and MGet fetch is written locally. The remote end names the host
|
|
360
|
+
// and file, and the FILETYPE a SIte gave it.
|
|
361
|
+
function ftpCopies(step, dds) {
|
|
362
|
+
const s = ftpSession(step, dds);
|
|
363
|
+
const localEnds = (x) => (x.dd ? [{ dd: x.dd }] : x.dsns.map((dsn) => ({ dsn })));
|
|
364
|
+
const remote = (name, x) => ({ remote: { host: s.host, name, ...(x.filetype ? { filetype: x.filetype } : {}) } });
|
|
365
|
+
const moves = [
|
|
366
|
+
...s.sends.map((x) => ({ from: localEnds(x), to: [remote(x.foreign, x)], line: x.line })),
|
|
367
|
+
...s.gets.map((x) => ({ from: [remote(x.name, x)], to: localEnds(x), line: x.line })),
|
|
368
|
+
].sort((a, b) => a.line - b.line);
|
|
369
|
+
const notes = s.inputUnread ? [{ line: step.line, text: `${who(step)} runs FTP with subcommands in ${s.inputUnread}, which this reader cannot see, so what it sends or fetches is not known` }] : [];
|
|
370
|
+
return { moves, notes };
|
|
371
|
+
}
|
|
372
|
+
|
|
253
373
|
const TABLE = [
|
|
254
|
-
[['IEBGENER', 'ICEGENER'], generCopy],
|
|
374
|
+
[['IEBGENER', 'ICEGENER', 'IEBPTPCH', 'IEBUPDTE'], generCopy],
|
|
255
375
|
[['SORT', 'ICEMAN', 'DFSORT', 'SYNCSORT'], sortCopies],
|
|
256
376
|
[['IDCAMS'], reproCopies],
|
|
257
377
|
[['IEBCOPY'], iebcopyCopies],
|
|
258
378
|
[['ADRDSSU'], dssCopies],
|
|
379
|
+
[['ICETOOL'], icetoolCopies],
|
|
380
|
+
[['FTP'], ftpCopies],
|
|
259
381
|
];
|
|
260
382
|
const BY_PROGRAM = new Map(TABLE.flatMap(([names, fn]) => names.map((n) => [n, fn])));
|
|
261
383
|
|
|
@@ -274,7 +396,7 @@ export function copiesOf(step) {
|
|
|
274
396
|
// An end naming a DD stands for every dataset concatenated under that name. A DD the step does
|
|
275
397
|
// not define is kept, with no dataset: a statement naming it still says where the data went.
|
|
276
398
|
const expand = (end) => {
|
|
277
|
-
if (!end.dd) return [{ dd: null, dsn: end.dsn }];
|
|
399
|
+
if (!end.dd) return [{ dd: null, dsn: end.dsn ?? null, ...(end.remote ? { remote: end.remote } : {}) }];
|
|
278
400
|
const group = dds.get(end.dd);
|
|
279
401
|
if (!group) return [{ dd: end.dd, dsn: null }];
|
|
280
402
|
return group.map((dd) => ({ dd: end.dd, dsn: dd.dsn || null, ...(dd.inStream ? { inStream: true } : {}) }));
|
|
@@ -294,3 +416,95 @@ export function copiesOf(step) {
|
|
|
294
416
|
}
|
|
295
417
|
return { copies, uses, notes };
|
|
296
418
|
}
|
|
419
|
+
|
|
420
|
+
// A TSO command's operands: KEYWORD(value) with the value's parentheses and quotes kept, a quoted
|
|
421
|
+
// string, or a bare word, split on blanks and commas. Blanks may stand between a keyword and its
|
|
422
|
+
// parenthesis, as in PROGRAM (GETTAB).
|
|
423
|
+
function tsoOperands(text) {
|
|
424
|
+
const out = [];
|
|
425
|
+
let i = 0;
|
|
426
|
+
while (i < text.length) {
|
|
427
|
+
if (/[\s,]/.test(text[i])) { i++; continue; }
|
|
428
|
+
const start = i;
|
|
429
|
+
let depth = 0, quoted = false;
|
|
430
|
+
for (; i < text.length; i++) {
|
|
431
|
+
const c = text[i];
|
|
432
|
+
if (c === "'") quoted = !quoted;
|
|
433
|
+
else if (quoted) continue;
|
|
434
|
+
else if (c === '(') depth++;
|
|
435
|
+
else if (c === ')') depth = Math.max(0, depth - 1);
|
|
436
|
+
else if (depth === 0 && /[\s,]/.test(c)) {
|
|
437
|
+
const next = /^\s*\(/.exec(text.slice(i));
|
|
438
|
+
if (next && /^[A-Z][A-Z0-9$#@]*$/i.test(text.slice(start, i))) { text = text.slice(0, i) + text.slice(i + next[0].length - 1); i--; continue; }
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const tok = text.slice(start, i);
|
|
443
|
+
const kw = /^([A-Z][A-Z0-9$#@]*)\((.*)\)$/is.exec(tok);
|
|
444
|
+
out.push(kw ? { key: upper(kw[1]), value: kw[2] } : { word: tok });
|
|
445
|
+
}
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const unquote = (s) => s.replace(/^'(.*)'$/s, '$1').replace(/''/g, "'");
|
|
450
|
+
|
|
451
|
+
// ALLOCATE's keywords, abbreviated as far as IBM's rule allows: to any leading part that no other
|
|
452
|
+
// ALLOCATE keyword shares. FILE shares F, FI and FIL with FILEDATA, FCB, FLASH and FORMS, and DSNAME
|
|
453
|
+
// shares DS and DSN with DSNTYPE and DSORG.
|
|
454
|
+
const ALLOC_DATASET = /^(DA|DAT|DATA|DATAS|DATASE|DATASET|DSNA|DSNAM|DSNAME)$/;
|
|
455
|
+
const ALLOC_FILE = /^(FILE|DD|DDN|DDNA|DDNAM|DDNAME)$/;
|
|
456
|
+
const ALLOC_STATUS = new Set(['OLD', 'SHR', 'MOD', 'NEW']);
|
|
457
|
+
|
|
458
|
+
// What a TSO batch step (IKJEFT01, IKJEFT1A, IKJEFT1B) does in its commands, from the step's PARM,
|
|
459
|
+
// which TSO runs as its first command, and the in-stream SYSTSIN:
|
|
460
|
+
// allocations: ALLOCATE's DDs, which the programs the step runs open as if the JCL defined them.
|
|
461
|
+
// A data set name in quotes is the whole name; one without has the user's prefix added, which
|
|
462
|
+
// the job does not say, so it is kept as written with no dsn.
|
|
463
|
+
// runs: the programs the step starts, by TSO CALL, whose parameter string reaches the program as
|
|
464
|
+
// EXEC PARM does, and by the DSN subcommand RUN PROGRAM, with PARMS.
|
|
465
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.1.0/com.ibm.zos.v2r1.ikjc500/allocsyn.htm
|
|
466
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_3.2.0/com.ibm.zos.v3r2.ikjc500/alloccmd.htm
|
|
467
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_3.1.0/com.ibm.zos.v3r1.ikjp100/ikjp100114.htm
|
|
468
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.5.0/com.ibm.zos.v2r5.ikjc200/ikj2o20039.htm
|
|
469
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_3.1.0/com.ibm.zos.v3r1.ikjc500/ikj2l2_CALL_command_operands.htm
|
|
470
|
+
// https://www.ibm.com/docs/api/v1/content/SSEPEK_13.0.0/comref/src/tpc/db2z_cmd_run.html
|
|
471
|
+
export const TSO_BATCH = new Set(['IKJEFT01', 'IKJEFT1A', 'IKJEFT1B']);
|
|
472
|
+
|
|
473
|
+
export function tsoCommands(step) {
|
|
474
|
+
const out = { allocations: [], runs: [], notes: [] };
|
|
475
|
+
if (!step.pgm || !TSO_BATCH.has(upper(step.pgm))) return out;
|
|
476
|
+
const { lines, unread } = cards(ddGroups(step).get('SYSTSIN'));
|
|
477
|
+
if (unread) out.notes.push({ line: step.line, text: `${who(step)} runs TSO with commands in ${unread}, which this reader cannot see, so any ALLOCATE, CALL or RUN it runs is not known` });
|
|
478
|
+
// PARM keeps JCL's doubled apostrophes; TSO sees one.
|
|
479
|
+
const cmds = [...(step.parm ? [{ line: step.line, text: step.parm.replace(/''/g, "'") }] : []), ...commands(lines)];
|
|
480
|
+
for (const cmd of cmds) {
|
|
481
|
+
const verb = upper(/^\S*/.exec(cmd.text)[0]);
|
|
482
|
+
const ops = tsoOperands(cmd.text.slice(verb.length));
|
|
483
|
+
if (verb === 'ALLOC' || verb === 'ALLOCATE') {
|
|
484
|
+
const file = ops.find((o) => o.key && ALLOC_FILE.test(o.key));
|
|
485
|
+
if (!file) continue;
|
|
486
|
+
const names = ops.find((o) => o.key && ALLOC_DATASET.test(o.key));
|
|
487
|
+
const words = ops.filter((o) => o.word).map((o) => upper(o.word));
|
|
488
|
+
const sysout = ops.find((o) => o.key === 'SYSOUT') || (words.includes('SYSOUT') ? { value: '' } : null);
|
|
489
|
+
const datasets = names ? tsoOperands(names.value).map((o) => o.word).filter((w) => w && w !== '*') : [];
|
|
490
|
+
if (!datasets.length && !sysout) continue;
|
|
491
|
+
out.allocations.push({
|
|
492
|
+
dd: upper(unquote(file.value)), line: cmd.line,
|
|
493
|
+
status: words.find((w) => ALLOC_STATUS.has(w)) || null,
|
|
494
|
+
sysout: sysout ? upper(sysout.value) || '*' : null,
|
|
495
|
+
datasets: datasets.map((w) => ({ written: w, dsn: /^'.*'$/.test(w) ? upper(unquote(w)) : null })),
|
|
496
|
+
});
|
|
497
|
+
} else if (verb === 'CALL') {
|
|
498
|
+
const target = ops[0]?.word || (ops[0]?.key ? `${ops[0].key}(${ops[0].value})` : '');
|
|
499
|
+
const member = /\(([A-Z0-9$#@]{1,8})\)'?$/i.exec(target);
|
|
500
|
+
const parm = ops[1]?.word && /^'.*'$/s.test(ops[1].word) ? unquote(ops[1].word) : null;
|
|
501
|
+
out.runs.push({ program: member ? upper(member[1]) : 'TEMPNAME', parm, line: cmd.line, via: 'TSO CALL' });
|
|
502
|
+
} else if (verb === 'RUN') {
|
|
503
|
+
const program = ops.find((o) => o.key === 'PROGRAM');
|
|
504
|
+
if (!program || !/^[A-Z0-9$#@]{1,8}$/i.test(program.value.trim())) continue;
|
|
505
|
+
const parms = ops.find((o) => o.key === 'PARMS');
|
|
506
|
+
out.runs.push({ program: upper(program.value.trim()), parm: parms ? unquote(parms.value.trim()) : null, line: cmd.line, via: 'DSN RUN' });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return out;
|
|
510
|
+
}
|
package/lib/verify.mjs
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// A verification plan for one path finding, for the estate to run in a test region it owns: the
|
|
3
|
+
// entry to start, where the value goes in, a harmless value that shows the defect, and what to watch
|
|
4
|
+
// for. Where letting the operation run would act - a command, a statement, a job, a connection - the
|
|
5
|
+
// plan stops at the operation and reads a marker there, and never supplies a value that would run.
|
|
6
|
+
// It is built only for `cobolwork explain`, never for a scan report. See docs/spec/reach.md §8.
|
|
7
|
+
import { kindsOf } from './consequence.mjs';
|
|
8
|
+
import { ALL_RULES } from './kernel/registry.mjs';
|
|
9
|
+
import { entryName } from './reach.mjs';
|
|
10
|
+
|
|
11
|
+
export const MARKER = 'CWVRFY01';
|
|
12
|
+
|
|
13
|
+
const WHERE = 'a test region the estate owns, holding test data, under its own authorisation: never a production region, and never a system the estate does not own';
|
|
14
|
+
|
|
15
|
+
const ABSENT = `${MARKER}, after checking that nothing by that name is defined where the program looks`;
|
|
16
|
+
const END_TASK = 'end the task from CEDF or CEDX, or stop the program in the debugger, before the operation runs';
|
|
17
|
+
|
|
18
|
+
// What goes in, and what reading it at the operation shows. `run` says what letting the operation run
|
|
19
|
+
// shows; null means it would act on something, so the plan stops before it (`stopBefore`).
|
|
20
|
+
const SINKS = {
|
|
21
|
+
arithmetic: (x) => ({ value: `a letter where ${x.item} expects a digit, such as A in its first position`, reads: `${x.item} holding the letter`, run: 'the task abends ASRA, a data exception (S0C7 in batch), at the operation' }),
|
|
22
|
+
subscript: (x) => bounds(x, `${x.table ? `${x.table + 1}, one more than the ${x.table} entries in the table` : 'one more than the entries in the table'}, or 0`),
|
|
23
|
+
'loop-bound': (x) => bounds(x, `${x.table ? `${x.table + 1}, one more than the ${x.table} entries in the table` : 'one more than the entries in the table'}`),
|
|
24
|
+
'reference-modification': (x) => bounds(x, 'one more than the length of the field, or 0'),
|
|
25
|
+
'occurs-depending-count': (x) => bounds(x, 'one more than the most entries the OCCURS clause allows, or 0'),
|
|
26
|
+
'storage-length': (x) => ({ value: 'a length above what the program means to acquire', reads: `the LENGTH or FLENGTH the command is given, from ${x.item}`, run: null, stopBefore: `acquiring that much storage can take the region short on storage: ${END_TASK}` }),
|
|
27
|
+
'record-key': (x) => ({ value: 'the key of a test record set up for a different test user', reads: `${x.item} holding that key`, run: 'the other user\'s test record comes back to the user who asked for it' }),
|
|
28
|
+
'record-update': (x) => ({ value: 'the key of a test record set up for a different test user', reads: `${x.item} holding that key`, run: 'the other user\'s test record is changed or deleted: set the record up for this test alone' }),
|
|
29
|
+
'dynamic-program-load': (x) => ({ value: ABSENT, reads: `${MARKER} as the program the CALL loads, from ${x.item}`, run: `the CALL fails to load ${MARKER} (an S806 abend in batch)` }),
|
|
30
|
+
'cics-dynamic-transfer': (x) => ({ value: ABSENT, reads: `${MARKER} as the program or transaction the command names, from ${x.item}`, run: `the command fails with PGMIDERR, or TRANSIDERR for a START` }),
|
|
31
|
+
'dynamic-file-path': (x) => ({ value: ABSENT, reads: `${MARKER} as the file the program assigns, from ${x.item}`, run: 'the OPEN fails with file status 35' }),
|
|
32
|
+
'cics-sysid': (x) => ({ value: ABSENT, reads: `${MARKER} as the SYSID the command is shipped to, from ${x.item}`, run: 'the command fails with SYSIDERR' }),
|
|
33
|
+
'queue-name': (x) => marker(x, 'as the queue the command acts on', 'the command would act on a queue the input chose'),
|
|
34
|
+
'web-response': (x) => ({ value: `${MARKER}<>`, reads: `${MARKER}<> in ${x.item}`, run: `the response body carries ${MARKER}<> as sent, not ${MARKER}<>` }),
|
|
35
|
+
screen: (x) => ({ value: MARKER, reads: `${MARKER} in ${x.item}`, run: `${MARKER} on the screen` }),
|
|
36
|
+
log: (x) => ({ value: MARKER, reads: `${MARKER} in ${x.item}`, run: `${MARKER} in the log as sent, which shows a line break would go in the same way` }),
|
|
37
|
+
'http-header': (x) => marker(x, 'in the header value', 'the header would go to the caller'),
|
|
38
|
+
'os-command': (x) => marker(x, 'in the command', 'the command would run'),
|
|
39
|
+
'dynamic-sql': (x) => marker(x, 'in the statement text', 'the statement would run'),
|
|
40
|
+
'internal-reader': (x) => marker(x, 'in the record written', 'the job would be submitted'),
|
|
41
|
+
'cics-system-resource': (x) => marker(x, 'as the resource the SET names', 'the command would change a region resource'),
|
|
42
|
+
'connection-target': (x) => marker(x, 'as the database or queue manager named', 'the connection would be made'),
|
|
43
|
+
'outbound-host': (x) => marker(x, 'as the host or path of the request', 'the request would go out'),
|
|
44
|
+
'outbound-http': (x) => marker(x, 'in what the request sends', 'the request would go out'),
|
|
45
|
+
'socket-send': (x) => marker(x, 'in what the socket sends', 'the data would leave the program'),
|
|
46
|
+
'message-queue': (x) => marker(x, 'in the message put', 'the message would be put'),
|
|
47
|
+
'extrapartition-queue': (x) => marker(x, 'in the record written', 'the record would leave the region through its DD'),
|
|
48
|
+
'xml-document': (x) => marker(x, 'in the document parsed', 'the parser would read it'),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function bounds(x, value) {
|
|
52
|
+
return {
|
|
53
|
+
value,
|
|
54
|
+
reads: `${x.item} out of range at the operation`,
|
|
55
|
+
...(x.ssrange ? { run: 'the task abends with a Language Environment range message (IGZ0006S for a subscript), since the program is compiled with SSRANGE' }
|
|
56
|
+
: { run: null, stopBefore: `without SSRANGE the operation reaches the storage beside the table or field: ${END_TASK}` }),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function marker(x, where, acts) {
|
|
61
|
+
return { value: MARKER, reads: `${MARKER} ${where}, from ${x.item}`, run: null, stopBefore: `${acts}: ${END_TASK}` };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Where the value goes in, by where the finding says the input comes from.
|
|
65
|
+
function enterAt(source, f, src, loc) {
|
|
66
|
+
const field = f.screen ? `field ${f.screen.field} of map ${f.screen.map}${f.screen.mapset ? ` in mapset ${f.screen.mapset}` : ''}` : null;
|
|
67
|
+
switch (source) {
|
|
68
|
+
case 'cics-terminal': return field ? `${field}, received at ${loc}` : `the terminal input received at ${loc}`;
|
|
69
|
+
case 'cics-protected-field': return `${field || 'the protected field'}: the map protects it, so change it under CEDF in the data the RECEIVE MAP at ${loc} returns, not with a modified client`;
|
|
70
|
+
case 'cics-web': return `the web request read at ${loc}`;
|
|
71
|
+
case 'argv-or-env': return `the command line or environment of the run, read at ${loc}`;
|
|
72
|
+
case 'jcl-parm': return `${src?.detail || 'the PARM'}, in a copy of the job`;
|
|
73
|
+
case 'jcl-instream': return `${src?.detail || 'the in-stream data'}, in a copy of the job`;
|
|
74
|
+
case 'file-record': return `a test record in a test copy of the file read at ${loc}`;
|
|
75
|
+
case 'database': return `a test row in a test copy of the table read at ${loc}`;
|
|
76
|
+
default: return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// CEDF and CEDX stop at EXEC CICS and EXEC SQL; a CALL, a COMPUTE or a subscript needs a debugger.
|
|
81
|
+
function toolFor(source, entries, program) {
|
|
82
|
+
const tran = entries.find((e) => e.transaction)?.transaction;
|
|
83
|
+
const debug = 'or a debugger such as z/OS Debugger where the operation is not an EXEC command';
|
|
84
|
+
if (source === 'cics-web') return `CEDX ${tran || `on the transaction that runs ${program}`}, since a web request has no terminal, ${debug}`;
|
|
85
|
+
if (tran || source === 'cics-terminal' || source === 'cics-protected-field') return `CEDF at the terminal that runs ${tran || program}, ${debug}`;
|
|
86
|
+
return 'a debugger such as z/OS Debugger';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function verificationPlan(f) {
|
|
90
|
+
const kinds = ALL_RULES[f.rule]?.evidence === 'path' ? kindsOf(f.rule) : null;
|
|
91
|
+
if (!kinds || !SINKS[kinds.sink]) return null;
|
|
92
|
+
const entries = f.startedBy || [];
|
|
93
|
+
const src = f.related?.[0];
|
|
94
|
+
const loc = src ? `${src.path}:${src.line}` : 'the source';
|
|
95
|
+
const last = f.trace?.length ? f.trace[f.trace.length - 1] : null;
|
|
96
|
+
const table = Number(/a table of (\d+)$/.exec(f.detail || '')?.[1]) || null;
|
|
97
|
+
const x = { item: last?.item || 'the value', table, ssrange: !!f.ssrange };
|
|
98
|
+
const sink = `${f.path}:${f.line}`;
|
|
99
|
+
const tool = toolFor(kinds.source, entries, f.program || 'the program');
|
|
100
|
+
const start = entries.length
|
|
101
|
+
? `${entries.map(entryName).join('; ')}${f.startedByMore ? `; or one of ${f.startedByMore} more not listed` : ''}`
|
|
102
|
+
: `nothing in this tree starts ${f.program || 'the program'}: start it the way the estate does`;
|
|
103
|
+
|
|
104
|
+
if (kinds.source === 'system-response') {
|
|
105
|
+
return {
|
|
106
|
+
where: WHERE, start, tool,
|
|
107
|
+
enter: `nothing: provoke the failure the program reports at ${loc}, such as a key that matches no test record`,
|
|
108
|
+
value: null,
|
|
109
|
+
observe: `stop at ${sink} and read ${x.item}: the response code or error text is in it`,
|
|
110
|
+
run: 'the code or error text reaches the caller in what the program sends back',
|
|
111
|
+
record: record(f),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const s = SINKS[kinds.sink](x);
|
|
116
|
+
return {
|
|
117
|
+
where: WHERE, start, tool,
|
|
118
|
+
enter: enterAt(kinds.source, f, src, loc),
|
|
119
|
+
value: s.value,
|
|
120
|
+
observe: `stop at ${sink} and read ${s.reads}: that is the defect, whatever the operation does next`,
|
|
121
|
+
run: s.run,
|
|
122
|
+
...(s.stopBefore ? { stopBefore: s.stopBefore } : {}),
|
|
123
|
+
...(f.guardedFrom && f.guard ? { check: `if the check on ${f.guard.item} at ${f.guard.file}:${f.guard.line} turns the value away first, record not-reproduced: the check stops it, and the finding can be judged a false positive` } : {}),
|
|
124
|
+
record: record(f),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const record = (f) => `bring the result in a COBOLWORK_WITNESS feed under "${f.fingerprint}": outcome "reproduced" or "not-reproduced", who ran it, the date and the system; what was entered is not recorded there`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portll/cobolwork",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.140",
|
|
4
4
|
"description": "COBOL, JCL and CICS security analysis with zero runtime dependencies: reference-format parser, cross-program data flow, mainframe credential rules. Plugs into commitwork.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"author": "Portll <john@portll.net>",
|