@portll/cobolwork 0.0.1 → 0.2.75

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.
Files changed (79) hide show
  1. package/LICENSE +661 -0
  2. package/LICENSING.md +93 -0
  3. package/NOTICE +9 -0
  4. package/README.md +323 -3
  5. package/THIRD-PARTY-NOTICES.md +118 -0
  6. package/bin/cobolwork.mjs +354 -0
  7. package/lib/advisories.mjs +133 -0
  8. package/lib/baseline.mjs +154 -0
  9. package/lib/bms.mjs +453 -0
  10. package/lib/build.mjs +402 -0
  11. package/lib/capabilities.mjs +79 -0
  12. package/lib/cics-commands.mjs +281 -0
  13. package/lib/compliance.mjs +81 -0
  14. package/lib/consequence.mjs +139 -0
  15. package/lib/control.mjs +1515 -0
  16. package/lib/csd.mjs +77 -0
  17. package/lib/dataflow.mjs +1506 -0
  18. package/lib/diff.mjs +344 -0
  19. package/lib/explain.mjs +145 -0
  20. package/lib/gate.mjs +383 -0
  21. package/lib/index.mjs +6 -0
  22. package/lib/inventory.mjs +79 -0
  23. package/lib/jcl.mjs +478 -0
  24. package/lib/kernel/findings.mjs +94 -0
  25. package/lib/kernel/identity.mjs +216 -0
  26. package/lib/kernel/memory.mjs +217 -0
  27. package/lib/kernel/printable.mjs +6 -0
  28. package/lib/kernel/registry.mjs +79 -0
  29. package/lib/kernel/ruleset.mjs +72 -0
  30. package/lib/kernel/source-tree.mjs +159 -0
  31. package/lib/kev.mjs +27 -0
  32. package/lib/options.mjs +512 -0
  33. package/lib/packs.mjs +148 -0
  34. package/lib/parser.mjs +2055 -0
  35. package/lib/policy.mjs +163 -0
  36. package/lib/precompile-cics.mjs +169 -0
  37. package/lib/precompile.mjs +544 -0
  38. package/lib/reach.mjs +122 -0
  39. package/lib/revision.json +1 -0
  40. package/lib/revision.mjs +89 -0
  41. package/lib/sarif.mjs +222 -0
  42. package/lib/scan.mjs +272 -0
  43. package/lib/sets/build.mjs +234 -0
  44. package/lib/sets/cics.mjs +306 -0
  45. package/lib/sets/compile.mjs +187 -0
  46. package/lib/sets/copybook.mjs +174 -0
  47. package/lib/sets/flow.mjs +487 -0
  48. package/lib/sets/hidden.mjs +216 -0
  49. package/lib/sets/jcl.mjs +440 -0
  50. package/lib/sets/log.mjs +406 -0
  51. package/lib/sets/opaque.mjs +102 -0
  52. package/lib/sets/priv.mjs +322 -0
  53. package/lib/sets/recon.mjs +267 -0
  54. package/lib/sets/vendor.mjs +117 -0
  55. package/lib/sets/web.mjs +327 -0
  56. package/lib/site.mjs +164 -0
  57. package/lib/sources.mjs +156 -0
  58. package/lib/tui/app.mjs +325 -0
  59. package/lib/tui/keys.mjs +39 -0
  60. package/lib/tui/model.mjs +96 -0
  61. package/lib/tui/run.mjs +38 -0
  62. package/lib/tui/screen.mjs +59 -0
  63. package/lib/tui/terminal.mjs +46 -0
  64. package/lib/utilities.mjs +296 -0
  65. package/lib/version.mjs +15 -0
  66. package/lib/words.mjs +318 -0
  67. package/package.json +45 -6
  68. package/rules/advisories.json +264 -0
  69. package/rules/compliance-dora.json +2151 -0
  70. package/rules/compliance-ffiec.json +2134 -0
  71. package/rules/compliance-nist80053.json +2134 -0
  72. package/rules/gitleaks-mainframe.toml +57 -0
  73. package/rules/kev-ids.json +1729 -0
  74. package/rules/packs/broadcom.json +124 -0
  75. package/rules/packs/connectdirect.json +116 -0
  76. package/rules/packs/controlm.json +114 -0
  77. package/rules/system-layouts.json +28 -0
  78. package/schema/cobolwork-coverage.schema.json +65 -0
  79. package/schema/cobolwork.policy.schema.json +54 -0
@@ -0,0 +1,59 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import { printable } from '../kernel/printable.mjs';
3
+
4
+ export function createScreen(cols, rows) {
5
+ return {
6
+ cols, rows,
7
+ chars: Array.from({ length: rows }, () => Array(cols).fill(' ')),
8
+ styles: Array.from({ length: rows }, () => Array(cols).fill('')),
9
+ };
10
+ }
11
+
12
+ export function put(screen, row, col, text, style = '') {
13
+ if (row < 0 || row >= screen.rows) return;
14
+ const s = printable(text, Infinity);
15
+ for (let i = 0; i < s.length; i++) {
16
+ const c = col + i;
17
+ if (c < 0) continue;
18
+ if (c >= screen.cols) break;
19
+ screen.chars[row][c] = s[i];
20
+ screen.styles[row][c] = style;
21
+ }
22
+ }
23
+
24
+ export function styleRow(screen, row, style) {
25
+ if (row < 0 || row >= screen.rows) return;
26
+ screen.styles[row].fill(style);
27
+ }
28
+
29
+ export const screenText = (screen) => screen.chars.map((r) => r.join('').replace(/\s+$/, '')).join('\n');
30
+
31
+ const SGR = {
32
+ color: { '': '', title: '\x1b[1m', warn: '\x1b[33m', dim: '\x1b[2m', sel: '\x1b[7m', hi: '\x1b[36m', crit: '\x1b[31;1m', high: '\x1b[31m' },
33
+ plain: { '': '', title: '\x1b[1m', warn: '\x1b[1m', dim: '', sel: '\x1b[7m', hi: '', crit: '\x1b[1m', high: '' },
34
+ };
35
+
36
+ function rowAnsi(screen, r, table) {
37
+ let out = '';
38
+ let current = null;
39
+ for (let c = 0; c < screen.cols; c++) {
40
+ const st = screen.styles[r][c];
41
+ if (st !== current) { out += '\x1b[0m' + (table[st] || ''); current = st; }
42
+ out += screen.chars[r][c];
43
+ }
44
+ return out + '\x1b[0m';
45
+ }
46
+
47
+ const rowKey = (screen, r) => screen.chars[r].join('') + '\u0000' + screen.styles[r].join(',');
48
+
49
+ // Escape sequences that turn `prev` (or a blank terminal) into `next`, changed rows only.
50
+ export function redraw(prev, next, { color = true } = {}) {
51
+ const table = color ? SGR.color : SGR.plain;
52
+ const whole = !prev || prev.cols !== next.cols || prev.rows !== next.rows;
53
+ let out = whole ? '\x1b[2J' : '';
54
+ for (let r = 0; r < next.rows; r++) {
55
+ if (!whole && rowKey(prev, r) === rowKey(next, r)) continue;
56
+ out += `\x1b[${r + 1};1H` + rowAnsi(next, r, table);
57
+ }
58
+ return out;
59
+ }
@@ -0,0 +1,46 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import { emitKeypressEvents } from 'node:readline';
3
+ import { keyToken } from './keys.mjs';
4
+
5
+ const ALT_ON = '\x1b[?1049h';
6
+ const ALT_OFF = '\x1b[?1049l';
7
+ const CURSOR_HIDE = '\x1b[?25l';
8
+ const CURSOR_SHOW = '\x1b[?25h';
9
+ const RESET = '\x1b[0m';
10
+
11
+ export function nodeTerminal({ input = process.stdin, output = process.stdout } = {}) {
12
+ let started = false;
13
+ const keyHandlers = [];
14
+ const resizeHandlers = [];
15
+ const size = () => ({ cols: output.columns || 80, rows: output.rows || 24 });
16
+ const onKeypress = (str, key) => {
17
+ const token = keyToken(str, key || {});
18
+ if (token) for (const h of keyHandlers) h(token);
19
+ };
20
+ const onResize = () => { for (const h of resizeHandlers) h(size()); };
21
+ return {
22
+ size,
23
+ start() {
24
+ if (started) return;
25
+ started = true;
26
+ emitKeypressEvents(input);
27
+ if (input.isTTY && input.setRawMode) input.setRawMode(true);
28
+ input.on('keypress', onKeypress);
29
+ if (output.on) output.on('resize', onResize);
30
+ input.resume();
31
+ output.write(ALT_ON + CURSOR_HIDE);
32
+ },
33
+ stop() {
34
+ if (!started) return;
35
+ started = false;
36
+ input.off('keypress', onKeypress);
37
+ if (output.off) output.off('resize', onResize);
38
+ if (input.isTTY && input.setRawMode) input.setRawMode(false);
39
+ input.pause();
40
+ output.write(RESET + CURSOR_SHOW + ALT_OFF);
41
+ },
42
+ write: (text) => { if (text) output.write(text); },
43
+ onKey: (h) => { keyHandlers.push(h); },
44
+ onResize: (h) => { resizeHandlers.push(h); },
45
+ };
46
+ }
@@ -0,0 +1,296 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // How the common z/OS utilities move data between their DD names. None of it can be derived from
3
+ // the JCL: SYSUT1 is IEBGENER's input and SYSUT2 its output only because IBM says so, and a SORT
4
+ // step's outputs are named in its control statements rather than on any DD. So it is a table, kept
5
+ // by hand, and each entry names the pages it was written from.
6
+ //
7
+ // A program the table does not know has no copies, rather than guessed ones. A step whose control
8
+ // statements are not in the job - SYSIN naming a library member - says so, because its copies are
9
+ // then only the ones its DD names decide on their own.
10
+
11
+ const upper = (s) => String(s).toUpperCase();
12
+
13
+ // The DDs of a step by name, each followed by the datasets concatenated to it.
14
+ export function ddGroups(step) {
15
+ const out = new Map();
16
+ let current = null;
17
+ for (const dd of step.dds) {
18
+ if (dd.name) {
19
+ current = upper(dd.name);
20
+ if (!out.has(current)) out.set(current, []);
21
+ }
22
+ if (current) out.get(current).push(dd);
23
+ }
24
+ return out;
25
+ }
26
+
27
+ // The in-stream lines of a control DD, and the first dataset in its concatenation, whose lines are
28
+ // statements this reader never sees.
29
+ function cards(group) {
30
+ const lines = [];
31
+ let unread = null;
32
+ for (const dd of group || []) {
33
+ if (dd.inStream) lines.push(...dd.inStream);
34
+ else if (dd.dsn && !unread) unread = dd.dsn;
35
+ }
36
+ return { lines, unread };
37
+ }
38
+
39
+ // Up to the first blank outside quotes: an operand field, where whatever follows is a remark.
40
+ function upToBlank(s) {
41
+ let quoted = false;
42
+ for (let i = 0; i < s.length; i++) {
43
+ if (s[i] === "'") quoted = !quoted;
44
+ else if (!quoted && /\s/.test(s[i])) return s.slice(0, i);
45
+ }
46
+ return s;
47
+ }
48
+
49
+ // Splits on commas outside parentheses.
50
+ function topLevel(s) {
51
+ const out = [];
52
+ let depth = 0, start = 0;
53
+ for (let i = 0; i < s.length; i++) {
54
+ if (s[i] === '(') depth++;
55
+ else if (s[i] === ')') depth = Math.max(0, depth - 1);
56
+ else if (s[i] === ',' && depth === 0) { out.push(s.slice(start, i)); start = i + 1; }
57
+ }
58
+ out.push(s.slice(start));
59
+ return out.map((x) => x.trim()).filter(Boolean);
60
+ }
61
+
62
+ const unwrap = (v) => (/^\(.*\)$/.test(v) ? v.slice(1, -1) : v);
63
+
64
+ // DFSORT and DFSMSdfp utility statements: a label in column 1, or a comment if column 1 holds an
65
+ // asterisk; the operation; operands up to the first blank, after which comes a remark. `continues`
66
+ // says whether a line's operands carry on to the next line.
67
+ function utilityStatements(lines, continues) {
68
+ const out = [];
69
+ let cur = null;
70
+ for (const { line, text } of lines) {
71
+ const s = text.slice(0, 71);
72
+ if (cur) {
73
+ const more = upToBlank(s.trimStart());
74
+ cur.operands += more;
75
+ if (!continues(more, text)) { out.push(cur); cur = null; }
76
+ continue;
77
+ }
78
+ if (/^\*/.test(s) || !s.trim()) continue;
79
+ const m = /^(\S+)?\s+(\S+)\s*(.*)$/.exec(s);
80
+ const [, label, op, rest] = m || [null, null, s.trim(), ''];
81
+ cur = { line, label: label || null, op: upper(op), operands: upToBlank(rest) };
82
+ if (!continues(cur.operands, text)) { out.push(cur); cur = null; }
83
+ }
84
+ if (cur) out.push(cur);
85
+ return out;
86
+ }
87
+
88
+ // IDCAMS and ADRDSSU commands: a hyphen at the end of a line continues the command after a blank,
89
+ // a plus continues it and the value it is in, and /* */ encloses a comment.
90
+ function commands(lines) {
91
+ const out = [];
92
+ let cur = null;
93
+ let comment = false;
94
+ for (const { line, text } of lines) {
95
+ let t = '';
96
+ const s = text.slice(0, 72);
97
+ for (let i = 0; i < s.length; i++) {
98
+ if (comment) { if (s[i] === '*' && s[i + 1] === '/') { comment = false; i++; } continue; }
99
+ if (s[i] === '/' && s[i + 1] === '*') { comment = true; i++; continue; }
100
+ t += s[i];
101
+ }
102
+ t = t.trim();
103
+ const join = /-$/.test(t) ? ' ' : /\+$/.test(t) ? '' : null;
104
+ if (join !== null) t = t.slice(0, -1).trim();
105
+ if (!cur) {
106
+ if (!t) continue;
107
+ cur = { line, text: t, join: '' };
108
+ } else cur.text += cur.join + t;
109
+ if (join === null) { out.push(cur); cur = null; } else cur.join = join;
110
+ }
111
+ if (cur) out.push(cur);
112
+ return out;
113
+ }
114
+
115
+ // The values of the first of `names` given as a keyword in a command, as in INFILE(IN) or
116
+ // OUTDDNAME(TAPE1,TAPE2). A name only counts at the start of a word, so INDD is not LOGINDD.
117
+ function param(text, names) {
118
+ const m = new RegExp(`(?:^|[\\s,])(?:${names.join('|')})\\s*\\(([^)]*)\\)`, 'i').exec(text);
119
+ return m ? m[1].split(/[\s,]+/).filter(Boolean).map((v) => upper(v.replace(/^'|'$/g, ''))) : null;
120
+ }
121
+
122
+ const who = (step) => `step ${step.name || '(unnamed)'}`;
123
+
124
+ // ---------------------------------------------------------------------------------------------
125
+ // The table. Each entry turns a step into moves: data read from `from` and written to `to`, where
126
+ // an end is a DD name or, when a control statement names one itself, a dataset.
127
+
128
+ // IEBGENER copies SYSUT1 to SYSUT2, and ICEGENER, DFSORT's replacement for it, reads the same DDs.
129
+ // Control statements only edit records or split members, so the copy is the DDs' alone.
130
+ // https://www.ibm.com/docs/api/v1/content/zosbasics/com.ibm.zos.zdatamgmt/zsysprogc_utilities_IEBGENER.htm
131
+ // https://www.ibm.com/docs/en/zos/3.1.0?topic=performance-use-icegener-instead-iebgener
132
+ function generCopy(step, dds) {
133
+ if (!dds.has('SYSUT1') || !dds.has('SYSUT2')) return { moves: [], notes: [] };
134
+ return { moves: [{ from: [{ dd: 'SYSUT1' }], to: [{ dd: 'SYSUT2' }], line: step.line }], notes: [] };
135
+ }
136
+
137
+ // DFSORT reads SORTIN for a sort or a copy and SORTINnn, nn from 00 to 99, for a merge, and writes
138
+ // SORTOUT. OUTFIL writes the DDs its FNAMES operand names, SORTOFx or SORTOFxx for each suffix its
139
+ // FILES operand gives, or SORTOUT when it gives neither. SORTDD= and SORTOUT= in DFSPARM rename the
140
+ // defaults, and are not read. A line whose operand field ends in a comma, semicolon or colon
141
+ // continues.
142
+ // https://www.ibm.com/docs/en/zos/3.1.0?topic=statements-program-dd
143
+ // https://www.ibm.com/docs/en/zos/2.5.0?topic=sets-jcl-merging-data-directly
144
+ // https://www.ibm.com/docs/en/zos/2.2.0?topic=statements-outfil-control
145
+ // https://www.ibm.com/docs/en/zos/3.1.0?topic=statements-general-coding-rules
146
+ // SYNCSORT is here because it takes the same DD names and statements; this was not checked against
147
+ // Precisely's own guide, which could not be retrieved.
148
+ function sortCopies(step, dds) {
149
+ const { lines, unread } = cards(dds.get('SYSIN'));
150
+ const statements = utilityStatements(lines, (ops) => /[,;:]$/.test(ops));
151
+ const merge = statements.some((st) => st.op === 'MERGE' && !/(?:^|,)FIELDS=COPY\b/i.test(st.operands));
152
+ const inputs = [...dds.keys()].filter((n) => (unread
153
+ ? n === 'SORTIN' || /^SORTIN\d\d$/.test(n)
154
+ : merge ? /^SORTIN\d\d$/.test(n) : n === 'SORTIN'));
155
+ const from = inputs.map((dd) => ({ dd }));
156
+ const notes = unread ? [{ line: step.line, text: `${who(step)} runs ${step.pgm} with control statements in ${unread}, which this reader cannot see, so whether it merges and which OUTFIL data sets it writes are not known` }] : [];
157
+ const moves = dds.has('SORTOUT') ? [{ from, to: [{ dd: 'SORTOUT' }], line: step.line }] : [];
158
+ for (const st of statements) {
159
+ if (st.op !== 'OUTFIL') continue;
160
+ const fnames = /(?:^|,)FNAMES=(\([^)]*\)|[^,()]+)/i.exec(st.operands);
161
+ const files = /(?:^|,)FILES=(\([^)]*\)|[^,()]+)/i.exec(st.operands);
162
+ const to = [
163
+ ...(fnames ? topLevel(unwrap(fnames[1])).map((n) => ({ dd: upper(n) })) : []),
164
+ ...(files ? topLevel(unwrap(files[1])).map((x) => ({ dd: `SORTOF${upper(x)}` })) : []),
165
+ ];
166
+ moves.push({ from, to: to.length ? to : [{ dd: 'SORTOUT' }], line: st.line });
167
+ }
168
+ return { moves, notes };
169
+ }
170
+
171
+ // IDCAMS REPRO copies INFILE(ddname) or INDATASET(entryname) to OUTFILE(ddname) or
172
+ // OUTDATASET(entryname), abbreviated IFILE, IDS, OFILE and ODS. The dataset forms name no DD:
173
+ // IDCAMS allocates those itself.
174
+ // https://www.ibm.com/docs/en/zos/3.1.0?topic=commands-repro
175
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.3.0/com.ibm.zos.v2r3.idai200/da6i2253.htm
176
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.idai200/continu.htm
177
+ function reproCopies(step, dds) {
178
+ 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` }] : [];
180
+ const moves = [];
181
+ for (const cmd of commands(lines)) {
182
+ if (!/^REPRO\b/i.test(cmd.text)) continue;
183
+ const end = (ddNames, dsNames) => {
184
+ const dd = param(cmd.text, ddNames);
185
+ if (dd) return [{ dd: dd[0] }];
186
+ const ds = param(cmd.text, dsNames);
187
+ return ds ? [{ dsn: ds[0] }] : [];
188
+ };
189
+ moves.push({ from: end(['INFILE', 'IFILE'], ['INDATASET', 'IDS']), to: end(['OUTFILE', 'OFILE'], ['OUTDATASET', 'ODS']), line: cmd.line });
190
+ }
191
+ return { moves, notes };
192
+ }
193
+
194
+ // IEBCOPY COPY and COPYGRP name their output with OUTDD and their inputs with INDD, where an input
195
+ // written (ddname,R) replaces members of the same name. An INDD= on a record of its own begins
196
+ // another step of the COPY before it. A statement continues after a comma or a mark in column 72.
197
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_3.1.0/com.ibm.zos.v3r1.idau100/copy.htm
198
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_3.1.0/com.ibm.zos.v3r1.idau100/copygrp.htm
199
+ // https://www.ibm.com/docs/en/zos/2.4.0?topic=ie-example-15-copy-groups-from-pdse-pdse-replace
200
+ function iebcopyCopies(step, dds) {
201
+ const { lines, unread } = cards(dds.get('SYSIN'));
202
+ 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` }] : [];
203
+ const moves = [];
204
+ let out = null;
205
+ const statements = utilityStatements(lines, (ops, text) => /,$/.test(ops) || (text.length > 71 && text[71] !== ' '));
206
+ for (const st of statements) {
207
+ // A record holding only INDD= has no operation, so what reads as its operation is its operand.
208
+ const alone = /^INDD=/.test(st.op);
209
+ if (st.op === 'COPY' || st.op === 'COPYGRP') out = null;
210
+ else if (!alone) {
211
+ if (st.op !== 'SELECT' && st.op !== 'EXCLUDE') out = null;
212
+ continue;
213
+ }
214
+ const kw = new Map(topLevel(alone ? st.op + st.operands : st.operands).map((o) => {
215
+ const eq = o.indexOf('=');
216
+ return [upper(o.slice(0, eq)), o.slice(eq + 1)];
217
+ }));
218
+ if (kw.has('OUTDD')) out = upper(kw.get('OUTDD'));
219
+ if (!out || !kw.has('INDD')) continue;
220
+ const from = topLevel(unwrap(kw.get('INDD'))).map((i) => ({ dd: upper(unwrap(i).split(',')[0].trim()) }));
221
+ moves.push({ from, to: [{ dd: out }], line: st.line });
222
+ }
223
+ return { moves, notes };
224
+ }
225
+
226
+ // ADRDSSU DUMP reads the INDDNAME volume, or the LOGINDDNAME volumes of a logical dump, and writes
227
+ // each OUTDDNAME, up to 255 of them. RESTORE reads the dump data set INDDNAME names and writes the
228
+ // OUTDDNAME volumes. With no input DD a logical dump chooses its datasets by filter, and with no
229
+ // output DD a logical restore puts them where the catalogue says, neither of which the job shows.
230
+ // https://www.ibm.com/docs/en/zos/3.1.0?topic=commands-command-syntax
231
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.4.0/com.ibm.zos.v2r4.adru000/r2319.htm
232
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.4.0/com.ibm.zos.v2r4.adru000/r2321.htm
233
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.4.0/com.ibm.zos.v2r4.adru000/r2327.htm
234
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.adru000/dgt3u2165.htm
235
+ // https://www.ibm.com/docs/api/v1/content/SSLTBW_2.2.0/com.ibm.zos.v2r2.adru000/dgt3u2174.htm
236
+ function dssCopies(step, dds) {
237
+ 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 restores is not known` }] : [];
239
+ const moves = [];
240
+ for (const cmd of commands(lines)) {
241
+ const op = /^(DUMP|RESTORE)\b/i.exec(cmd.text);
242
+ if (!op) continue;
243
+ const dump = upper(op[1]) === 'DUMP';
244
+ const ins = param(cmd.text, ['INDDNAME', 'INDD', 'IDD']) || (dump && param(cmd.text, ['LOGINDDNAME', 'LOGINDD', 'LIDD'])) || [];
245
+ const outs = param(cmd.text, ['OUTDDNAME', 'OUTDD', 'ODD']) || [];
246
+ 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 (!dump && !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
+ moves.push({ from: ins.map((dd) => ({ dd })), to: outs.map((dd) => ({ dd })), line: cmd.line });
249
+ }
250
+ return { moves, notes };
251
+ }
252
+
253
+ const TABLE = [
254
+ [['IEBGENER', 'ICEGENER'], generCopy],
255
+ [['SORT', 'ICEMAN', 'DFSORT', 'SYNCSORT'], sortCopies],
256
+ [['IDCAMS'], reproCopies],
257
+ [['IEBCOPY'], iebcopyCopies],
258
+ [['ADRDSSU'], dssCopies],
259
+ ];
260
+ const BY_PROGRAM = new Map(TABLE.flatMap(([names, fn]) => names.map((n) => [n, fn])));
261
+
262
+ export const UTILITIES = [...BY_PROGRAM.keys()];
263
+
264
+ // What a step's program copies, as { copies, uses, notes }: the copies from dataset to dataset, how
265
+ // the utility uses each DD statement it names ('read' or 'write'), and what could not be read. Null
266
+ // for a program the table does not know.
267
+ export function copiesOf(step) {
268
+ const utility = step.pgm ? upper(step.pgm) : null;
269
+ const fn = utility && BY_PROGRAM.get(utility);
270
+ if (!fn) return null;
271
+ const dds = ddGroups(step);
272
+ const { moves, notes } = fn(step, dds);
273
+
274
+ // An end naming a DD stands for every dataset concatenated under that name. A DD the step does
275
+ // not define is kept, with no dataset: a statement naming it still says where the data went.
276
+ const expand = (end) => {
277
+ if (!end.dd) return [{ dd: null, dsn: end.dsn }];
278
+ const group = dds.get(end.dd);
279
+ if (!group) return [{ dd: end.dd, dsn: null }];
280
+ return group.map((dd) => ({ dd: end.dd, dsn: dd.dsn || null, ...(dd.inStream ? { inStream: true } : {}) }));
281
+ };
282
+
283
+ const copies = [];
284
+ const uses = new Map();
285
+ const mark = (end, use) => {
286
+ for (const dd of (end.dd && dds.get(end.dd)) || []) if (uses.get(dd) !== 'write') uses.set(dd, use);
287
+ };
288
+ for (const m of moves) {
289
+ for (const end of m.from) mark(end, 'read');
290
+ for (const end of m.to) mark(end, 'write');
291
+ for (const from of m.from.flatMap(expand)) {
292
+ for (const to of m.to.flatMap(expand)) copies.push({ step: step.name, utility, from, to, line: m.line });
293
+ }
294
+ }
295
+ return { copies, uses, notes };
296
+ }
@@ -0,0 +1,15 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import { readFileSync } from 'node:fs';
3
+
4
+ // Bumped when a field a consumer reads changes meaning or shape, so a reader can refuse a report it
5
+ // was not written for rather than misread it. 2: filesScanned is the widest rule set, not programs.
6
+ // 3: findings carry `evidence`, and setsIncomplete entries a `kind`.
7
+ export const SCHEMA_VERSION = 3;
8
+
9
+ // Names how taint moves through groups, so a finding that appears or disappears between two reports
10
+ // can be attributed to the model rather than to the code scanned. 'byte-range': a tainted field
11
+ // taints those bytes of its group, and the bytes are followed through MOVE, REDEFINES and CALL.
12
+ export const FLOW_MODEL = 'byte-range';
13
+
14
+ // The package version, read once: a row says which cobolwork produced it.
15
+ export const TOOL_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;