@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/utilities.mjs
CHANGED
|
@@ -129,6 +129,12 @@ const who = (step) => `step ${step.name || '(unnamed)'}`;
|
|
|
129
129
|
// Control statements only edit records or split members, so the copy is the DDs' alone.
|
|
130
130
|
// https://www.ibm.com/docs/api/v1/content/zosbasics/com.ibm.zos.zdatamgmt/zsysprogc_utilities_IEBGENER.htm
|
|
131
131
|
// https://www.ibm.com/docs/en/zos/3.1.0?topic=performance-use-icegener-instead-iebgener
|
|
132
|
+
// IEBPTPCH reads SYSUT1 and writes its print or punch output to SYSUT2, and IEBUPDTE reads its old
|
|
133
|
+
// master from SYSUT1 and writes SYSUT2. IEBUPDTE's control data set may also carry input data for the
|
|
134
|
+
// new master, which is not followed here.
|
|
135
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.idau100/u1367.htm
|
|
136
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.idau100/u1427.htm
|
|
137
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.idau100/u1425.htm
|
|
132
138
|
function generCopy(step, dds) {
|
|
133
139
|
if (!dds.has('SYSUT1') || !dds.has('SYSUT2')) return { moves: [], notes: [] };
|
|
134
140
|
return { moves: [{ from: [{ dd: 'SYSUT1' }], to: [{ dd: 'SYSUT2' }], line: step.line }], notes: [] };
|
|
@@ -174,29 +180,43 @@ function sortCopies(step, dds) {
|
|
|
174
180
|
// https://www.ibm.com/docs/en/zos/3.1.0?topic=commands-repro
|
|
175
181
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2253.htm
|
|
176
182
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.idai200/continu.htm
|
|
183
|
+
// EXPORT writes the cluster its first parameter names, or the one INFILE(ddname) identifies, to the
|
|
184
|
+
// portable data set OUTFILE(ddname) or OUTDATASET(entryname) gives, and may be abbreviated EXP.
|
|
185
|
+
// IMPORT reads the portable data set INFILE(ddname) or INDATASET(entryname) names and writes the
|
|
186
|
+
// cluster OUTFILE(ddname) or OUTDATASET(entryname) names.
|
|
187
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/export.htm
|
|
188
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2210.htm
|
|
189
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i211.htm
|
|
190
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/import.htm
|
|
191
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2221.htm
|
|
177
192
|
function reproCopies(step, dds) {
|
|
178
193
|
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` }] : [];
|
|
194
|
+
const notes = unread ? [{ line: step.line, text: `${who(step)} runs IDCAMS with commands in ${unread}, which this reader cannot see, so any REPRO, EXPORT or IMPORT it runs is not known` }] : [];
|
|
180
195
|
const moves = [];
|
|
181
196
|
for (const cmd of commands(lines)) {
|
|
182
|
-
|
|
183
|
-
|
|
197
|
+
const verb = /^(REPRO|EXPORT|EXP|IMPORT)\b/i.exec(cmd.text);
|
|
198
|
+
if (!verb) continue;
|
|
199
|
+
const entry = /^EXP/i.test(verb[1]) && /^\S+\s+([^\s(),]+)/.exec(cmd.text);
|
|
200
|
+
const end = (ddNames, dsNames, fallback) => {
|
|
184
201
|
const dd = param(cmd.text, ddNames);
|
|
185
202
|
if (dd) return [{ dd: dd[0] }];
|
|
186
203
|
const ds = param(cmd.text, dsNames);
|
|
187
|
-
|
|
204
|
+
if (ds) return [{ dsn: ds[0] }];
|
|
205
|
+
return fallback ? [{ dsn: upper(fallback.replace(/^'|'$/g, '')) }] : [];
|
|
188
206
|
};
|
|
189
|
-
moves.push({ from: end(['INFILE', 'IFILE'], ['INDATASET', 'IDS']), to: end(['OUTFILE', 'OFILE'], ['OUTDATASET', 'ODS']), line: cmd.line });
|
|
207
|
+
moves.push({ from: end(['INFILE', 'IFILE'], ['INDATASET', 'IDS'], entry && entry[1]), to: end(['OUTFILE', 'OFILE'], ['OUTDATASET', 'ODS']), line: cmd.line });
|
|
190
208
|
}
|
|
191
209
|
return { moves, notes };
|
|
192
210
|
}
|
|
193
211
|
|
|
194
|
-
// IEBCOPY COPY and
|
|
212
|
+
// IEBCOPY COPY, COPYGRP and COPYMOD name their output with OUTDD and their inputs with INDD, where an input
|
|
195
213
|
// written (ddname,R) replaces members of the same name. An INDD= on a record of its own begins
|
|
196
214
|
// another step of the COPY before it. A statement continues after a comma or a mark in column 72.
|
|
197
215
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_3.1.0/com.ibm.zos.v3r1.idau100/copy.htm
|
|
198
216
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_3.1.0/com.ibm.zos.v3r1.idau100/copygrp.htm
|
|
199
217
|
// https://www.ibm.com/docs/en/zos/2.4.0?topic=ie-example-15-copy-groups-from-pdse-pdse-replace
|
|
218
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_3.1.0/com.ibm.zos.v3r1.idau100/copymd.htm
|
|
219
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_3.1.0/com.ibm.zos.v3r1.idau100/u1055.htm
|
|
200
220
|
function iebcopyCopies(step, dds) {
|
|
201
221
|
const { lines, unread } = cards(dds.get('SYSIN'));
|
|
202
222
|
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 +226,7 @@ function iebcopyCopies(step, dds) {
|
|
|
206
226
|
for (const st of statements) {
|
|
207
227
|
// A record holding only INDD= has no operation, so what reads as its operation is its operand.
|
|
208
228
|
const alone = /^INDD=/.test(st.op);
|
|
209
|
-
if (st.op === 'COPY' || st.op === 'COPYGRP') out = null;
|
|
229
|
+
if (st.op === 'COPY' || st.op === 'COPYGRP' || st.op === 'COPYMOD') out = null;
|
|
210
230
|
else if (!alone) {
|
|
211
231
|
if (st.op !== 'SELECT' && st.op !== 'EXCLUDE') out = null;
|
|
212
232
|
continue;
|
|
@@ -227,35 +247,87 @@ function iebcopyCopies(step, dds) {
|
|
|
227
247
|
// each OUTDDNAME, up to 255 of them. RESTORE reads the dump data set INDDNAME names and writes the
|
|
228
248
|
// OUTDDNAME volumes. With no input DD a logical dump chooses its datasets by filter, and with no
|
|
229
249
|
// output DD a logical restore puts them where the catalogue says, neither of which the job shows.
|
|
250
|
+
// COPY reads the volumes INDDNAME or LOGINDDNAME name and writes the DASD volumes OUTDDNAME names.
|
|
230
251
|
// https://www.ibm.com/docs/en/zos/3.1.0?topic=commands-command-syntax
|
|
231
252
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.4.0/com.ibm.zos.v2r4.adru000/r2319.htm
|
|
232
253
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.4.0/com.ibm.zos.v2r4.adru000/r2321.htm
|
|
233
254
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.4.0/com.ibm.zos.v2r4.adru000/r2327.htm
|
|
234
255
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.adru000/dgt3u2165.htm
|
|
235
256
|
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.adru000/dgt3u2174.htm
|
|
257
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.adru000/r2172.htm
|
|
258
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.adru000/r2204.htm
|
|
259
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.adru000/r2206.htm
|
|
260
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.adru000/r2216.htm
|
|
236
261
|
function dssCopies(step, dds) {
|
|
237
262
|
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
|
|
263
|
+
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
264
|
const moves = [];
|
|
240
265
|
for (const cmd of commands(lines)) {
|
|
241
|
-
const op = /^(DUMP|RESTORE)\b/i.exec(cmd.text);
|
|
266
|
+
const op = /^(DUMP|RESTORE|COPY)\b/i.exec(cmd.text);
|
|
242
267
|
if (!op) continue;
|
|
243
|
-
const
|
|
244
|
-
const
|
|
268
|
+
const verb = upper(op[1]);
|
|
269
|
+
const dump = verb === 'DUMP';
|
|
270
|
+
const ins = param(cmd.text, ['INDDNAME', 'INDD', 'IDD']) || (verb !== 'RESTORE' && param(cmd.text, ['LOGINDDNAME', 'LOGINDD', 'LIDD'])) || [];
|
|
245
271
|
const outs = param(cmd.text, ['OUTDDNAME', 'OUTDD', 'ODD']) || [];
|
|
246
272
|
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 (
|
|
273
|
+
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` });
|
|
274
|
+
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` });
|
|
275
|
+
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
276
|
moves.push({ from: ins.map((dd) => ({ dd })), to: outs.map((dd) => ({ dd })), line: cmd.line });
|
|
249
277
|
}
|
|
250
278
|
return { moves, notes };
|
|
251
279
|
}
|
|
252
280
|
|
|
281
|
+
// ICETOOL runs the operators in TOOLIN. COPY, SORT and MERGE read the DDs their FROM operands name
|
|
282
|
+
// and write the DDs their TO operands name, several ddnames to an operand. A statement's operands
|
|
283
|
+
// continue on the next line after a hyphen, and what follows the hyphen is ignored. USING(xxxx) reads
|
|
284
|
+
// DFSORT control statements from the xxxxCNTL DD, whose OUTFIL statements can write further data sets.
|
|
285
|
+
// https://www.ibm.com/docs/en/zos/3.1.0?topic=icetool-copy-operator
|
|
286
|
+
// https://www.ibm.com/docs/en/zos/3.1.0?topic=icetool-sort-operator
|
|
287
|
+
// https://www.ibm.com/docs/en/zos/3.1.0?topic=icetool-merge-operator
|
|
288
|
+
// https://www.ibm.com/docs/en/zos/2.3.0?topic=icetool-job-control-language
|
|
289
|
+
// https://www.ibm.com/docs/api/v1/content/SSLTBW_3.1.0/com.ibm.zos.v3r1.icea100/ice2ca_General_coding_rules.htm
|
|
290
|
+
function icetoolCopies(step, dds) {
|
|
291
|
+
const { lines, unread } = cards(dds.get('TOOLIN'));
|
|
292
|
+
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` }] : [];
|
|
293
|
+
const statements = [];
|
|
294
|
+
let cur = null;
|
|
295
|
+
for (const { line, text } of lines) {
|
|
296
|
+
const s = text.slice(0, 72);
|
|
297
|
+
if (!cur && (/^\*/.test(s) || !s.trim())) continue;
|
|
298
|
+
const tokens = s.trim().split(/\s+/);
|
|
299
|
+
const hyphen = tokens.findIndex((t) => t.endsWith('-'));
|
|
300
|
+
const used = hyphen < 0 ? tokens : tokens.slice(0, hyphen + 1);
|
|
301
|
+
if (!cur) cur = { line, op: upper(used.shift()), operands: [] };
|
|
302
|
+
cur.operands.push(...used.map((t) => t.replace(/-$/, '')).filter(Boolean));
|
|
303
|
+
if (hyphen < 0) { statements.push(cur); cur = null; }
|
|
304
|
+
}
|
|
305
|
+
if (cur) statements.push(cur);
|
|
306
|
+
const moves = [];
|
|
307
|
+
for (const st of statements) {
|
|
308
|
+
if (!['COPY', 'SORT', 'MERGE'].includes(st.op)) continue;
|
|
309
|
+
const named = (kw) => st.operands.flatMap((o) => {
|
|
310
|
+
const m = new RegExp(`^${kw}\\((.*)\\)$`, 'i').exec(o);
|
|
311
|
+
return m ? m[1].split(',').map((n) => upper(n.trim())).filter(Boolean) : [];
|
|
312
|
+
});
|
|
313
|
+
moves.push({ from: named('FROM').map((dd) => ({ dd })), to: named('TO').map((dd) => ({ dd })), line: st.line });
|
|
314
|
+
for (const using of named('USING')) {
|
|
315
|
+
const control = cards(dds.get(`${using}CNTL`));
|
|
316
|
+
if (control.unread || utilityStatements(control.lines, (ops) => /[,;:]$/.test(ops)).some((c) => c.op === 'OUTFIL')) {
|
|
317
|
+
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` });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return { moves, notes };
|
|
322
|
+
}
|
|
323
|
+
|
|
253
324
|
const TABLE = [
|
|
254
|
-
[['IEBGENER', 'ICEGENER'], generCopy],
|
|
325
|
+
[['IEBGENER', 'ICEGENER', 'IEBPTPCH', 'IEBUPDTE'], generCopy],
|
|
255
326
|
[['SORT', 'ICEMAN', 'DFSORT', 'SYNCSORT'], sortCopies],
|
|
256
327
|
[['IDCAMS'], reproCopies],
|
|
257
328
|
[['IEBCOPY'], iebcopyCopies],
|
|
258
329
|
[['ADRDSSU'], dssCopies],
|
|
330
|
+
[['ICETOOL'], icetoolCopies],
|
|
259
331
|
];
|
|
260
332
|
const BY_PROGRAM = new Map(TABLE.flatMap(([names, fn]) => names.map((n) => [n, fn])));
|
|
261
333
|
|
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.117",
|
|
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>",
|