@portll/cobolwork 0.5.0 → 0.7.0

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 (163) hide show
  1. package/README.md +89 -26
  2. package/STABILITY.md +76 -0
  3. package/bin/cobolwork.mjs +56 -14
  4. package/lib/baseline.mjs +9 -0
  5. package/lib/bms.mjs +21 -7
  6. package/lib/build.mjs +133 -53
  7. package/lib/capabilities.mjs +13 -11
  8. package/lib/cics-commands.mjs +501 -9
  9. package/lib/compliance.mjs +11 -0
  10. package/lib/consequence.mjs +17 -0
  11. package/lib/control-reuse.mjs +113 -0
  12. package/lib/control-workers.mjs +161 -0
  13. package/lib/control.mjs +77 -4
  14. package/lib/dataflow.mjs +402 -152
  15. package/lib/db2/cursor.mjs +15 -0
  16. package/lib/db2/read.mjs +170 -0
  17. package/lib/db2/rules.mjs +77 -0
  18. package/lib/db2/stmt/alter.mjs +473 -0
  19. package/lib/db2/stmt/grant.mjs +125 -0
  20. package/lib/db2/stmt/index.mjs +141 -0
  21. package/lib/db2/stmt/misc.mjs +325 -0
  22. package/lib/db2/stmt/routine.mjs +564 -0
  23. package/lib/db2/stmt/storage.mjs +146 -0
  24. package/lib/db2/stmt/table.mjs +540 -0
  25. package/lib/db2/stmt/view.mjs +146 -0
  26. package/lib/diff.mjs +17 -5
  27. package/lib/equivalence.mjs +44 -5
  28. package/lib/evidence/cli.mjs +26 -7
  29. package/lib/evidence/record.mjs +16 -6
  30. package/lib/evidence/seal.mjs +17 -0
  31. package/lib/evidence/store.mjs +44 -15
  32. package/lib/evidence/timestamp.mjs +83 -0
  33. package/lib/evidence/verify.mjs +120 -42
  34. package/lib/exec-reading.mjs +51 -0
  35. package/lib/execution.mjs +3 -2
  36. package/lib/explain.mjs +2 -0
  37. package/lib/exploitability.mjs +11 -2
  38. package/lib/hlasm/asm/data.mjs +128 -0
  39. package/lib/hlasm/asm/listing.mjs +43 -0
  40. package/lib/hlasm/asm/output.mjs +45 -0
  41. package/lib/hlasm/asm/sections.mjs +161 -0
  42. package/lib/hlasm/asm/symbols.mjs +78 -0
  43. package/lib/hlasm/exec.mjs +27 -0
  44. package/lib/hlasm/expr.mjs +167 -0
  45. package/lib/hlasm/instr.mjs +73 -0
  46. package/lib/hlasm/locate.mjs +333 -0
  47. package/lib/hlasm/macro/authorization.mjs +146 -0
  48. package/lib/hlasm/macro/datasets.mjs +107 -0
  49. package/lib/hlasm/macro/io.mjs +162 -0
  50. package/lib/hlasm/macro/le.mjs +53 -0
  51. package/lib/hlasm/macro/linkage.mjs +143 -0
  52. package/lib/hlasm/macro/operator.mjs +77 -0
  53. package/lib/hlasm/macro/program.mjs +184 -0
  54. package/lib/hlasm/macro/recovery.mjs +85 -0
  55. package/lib/hlasm/macro/storage.mjs +167 -0
  56. package/lib/hlasm/macro/structured.mjs +131 -0
  57. package/lib/hlasm/model.mjs +97 -0
  58. package/lib/hlasm/mvs38.mjs +47 -0
  59. package/lib/hlasm/operands.mjs +59 -0
  60. package/lib/hlasm/optable.mjs +61 -0
  61. package/lib/hlasm/read.mjs +130 -0
  62. package/lib/hlasm.mjs +44 -7
  63. package/lib/ims/dli.mjs +37 -0
  64. package/lib/ims/macro/dbd.mjs +299 -0
  65. package/lib/ims/macro/psb.mjs +286 -0
  66. package/lib/ims/model.mjs +149 -0
  67. package/lib/ims/operands.mjs +23 -0
  68. package/lib/ims/read.mjs +37 -0
  69. package/lib/ims/rules.mjs +135 -0
  70. package/lib/inventory.mjs +10 -7
  71. package/lib/ironwork-ids.mjs +24 -0
  72. package/lib/ironwork.mjs +20 -13
  73. package/lib/kernel/pds-archive.mjs +256 -0
  74. package/lib/kernel/registry.mjs +32 -23
  75. package/lib/kernel/shared-pass.mjs +163 -0
  76. package/lib/kernel/source-tree.mjs +104 -36
  77. package/lib/kernel/version-key.mjs +17 -0
  78. package/lib/layout.mjs +26 -31
  79. package/lib/options.mjs +26 -4
  80. package/lib/parser.mjs +136 -17
  81. package/lib/pli/cursor.mjs +15 -0
  82. package/lib/pli/expr.mjs +101 -0
  83. package/lib/pli/include.mjs +82 -0
  84. package/lib/pli/layout.mjs +125 -0
  85. package/lib/pli/lex.mjs +198 -0
  86. package/lib/pli/program.mjs +280 -0
  87. package/lib/pli/rules/based.mjs +95 -0
  88. package/lib/pli/rules/conditions.mjs +68 -0
  89. package/lib/pli/rules/entry.mjs +130 -0
  90. package/lib/pli/rules/index.mjs +24 -0
  91. package/lib/pli/rules/preprocessor.mjs +55 -0
  92. package/lib/pli/statements.mjs +130 -0
  93. package/lib/pli/stmt/alloc.mjs +45 -0
  94. package/lib/pli/stmt/assignment.mjs +56 -0
  95. package/lib/pli/stmt/call.mjs +104 -0
  96. package/lib/pli/stmt/conditions.mjs +94 -0
  97. package/lib/pli/stmt/control.mjs +219 -0
  98. package/lib/pli/stmt/declare.mjs +149 -0
  99. package/lib/pli/stmt/exec.mjs +55 -0
  100. package/lib/pli/stmt/io.mjs +239 -0
  101. package/lib/pli/stmt/misc.mjs +4 -0
  102. package/lib/pli/stmt/preprocessor.mjs +242 -0
  103. package/lib/pli/stmt/procedure.mjs +258 -0
  104. package/lib/pli/stmt/stream.mjs +283 -0
  105. package/lib/pli/storage.mjs +129 -0
  106. package/lib/policy.mjs +6 -0
  107. package/lib/precompile-check.mjs +124 -0
  108. package/lib/precompile-cics.mjs +8 -4
  109. package/lib/reach.mjs +11 -2
  110. package/lib/revision.json +1 -1
  111. package/lib/sarif.mjs +44 -3
  112. package/lib/scan.mjs +28 -12
  113. package/lib/sets/abend.mjs +82 -8
  114. package/lib/sets/build.mjs +43 -37
  115. package/lib/sets/cics.mjs +20 -38
  116. package/lib/sets/compile.mjs +46 -18
  117. package/lib/sets/copybook.mjs +5 -3
  118. package/lib/sets/crypto.mjs +5 -3
  119. package/lib/sets/ddl.mjs +36 -0
  120. package/lib/sets/flow.mjs +32 -8
  121. package/lib/sets/hidden.mjs +5 -3
  122. package/lib/sets/hlasm.mjs +124 -8
  123. package/lib/sets/ims.mjs +139 -0
  124. package/lib/sets/log.mjs +11 -9
  125. package/lib/sets/opaque.mjs +27 -7
  126. package/lib/sets/pli.mjs +40 -0
  127. package/lib/sets/priv.mjs +5 -3
  128. package/lib/sets/recon.mjs +5 -3
  129. package/lib/sets/secrets.mjs +112 -0
  130. package/lib/sets/semantics.mjs +8 -3
  131. package/lib/sets/web.mjs +39 -23
  132. package/lib/site.mjs +10 -0
  133. package/lib/sources.mjs +80 -20
  134. package/lib/statement-cursor.mjs +67 -0
  135. package/lib/verify.mjs +3 -2
  136. package/lib/version.mjs +6 -0
  137. package/package.json +3 -2
  138. package/rules/compliance-cobit2019.json +520 -5
  139. package/rules/compliance-dora.json +509 -5
  140. package/rules/compliance-ffiec.json +505 -1
  141. package/rules/compliance-nist80053.json +557 -1
  142. package/rules/gitleaks-mainframe.toml +46 -15
  143. package/rules/hlasm-instructions.json +2396 -0
  144. package/rules/hlasm-optables.json +8024 -0
  145. package/schema/cobolwork-baseline.schema.json +36 -0
  146. package/schema/cobolwork-build-provenance.schema.json +187 -0
  147. package/schema/cobolwork-build.schema.json +382 -0
  148. package/schema/cobolwork-capabilities.schema.json +239 -0
  149. package/schema/cobolwork-diff.schema.json +217 -0
  150. package/schema/cobolwork-evidence.schema.json +161 -0
  151. package/schema/cobolwork-execution.schema.json +53 -0
  152. package/schema/cobolwork-explain.schema.json +360 -0
  153. package/schema/cobolwork-finding.schema.json +465 -0
  154. package/schema/cobolwork-flow.schema.json +465 -0
  155. package/schema/cobolwork-gate.schema.json +211 -0
  156. package/schema/cobolwork-inventory.schema.json +206 -0
  157. package/schema/cobolwork-parse.schema.json +105 -0
  158. package/schema/cobolwork-reach.schema.json +74 -0
  159. package/schema/cobolwork-report.schema.json +559 -0
  160. package/schema/cobolwork-witness.schema.json +107 -0
  161. package/schema/cobolwork.baseline.schema.json +101 -0
  162. package/schema/cobolwork.policy.schema.json +7 -0
  163. package/schema/cobolwork.site.schema.json +116 -0
@@ -0,0 +1,198 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // PL/I source as tokens and statements: the margins a file is read with, comments and literals
3
+ // removed or kept as tokens, and the semicolon-terminated statements with their label and condition
4
+ // prefixes. Classification by statement kind is in lib/pli/statements.mjs.
5
+
6
+ const PROCESS_LINE = /^[*%]PROCESS\b(.*)$/i;
7
+ const CARRIAGE = new Set([' ', '0', '1', '-', '+']);
8
+ const SEQUENCE = /^[ A-Za-z0-9]{0,8}$/;
9
+
10
+ // Enterprise PL/I reads columns 2 to 72 unless *PROCESS MARGINS says otherwise. Public source is
11
+ // often written from column 1 to any width, and read with MARGINS(2,72) it loses its first column
12
+ // and its tail, so the margins are taken from the file itself and the reason recorded.
13
+ export function marginsOf(text) {
14
+ const lines = text.split(/\r?\n/);
15
+ for (const l of lines) {
16
+ const m = PROCESS_LINE.exec(l);
17
+ const opt = m && /\bMAR(?:GINS)?\s*\(\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d+)\s*)?\)/i.exec(m[1]);
18
+ if (opt) return { left: Number(opt[1]), right: Number(opt[2]), carriage: opt[3] ? Number(opt[3]) : 0, reason: 'process-option' };
19
+ }
20
+ const body = lines.filter((l) => !PROCESS_LINE.test(l));
21
+ const long = body.filter((l) => l.replace(/\s+$/, '').length > 72);
22
+ const sequenceLike = long.filter((l) => l.length <= 80 && SEQUENCE.test(l.slice(72).replace(/\s+$/, '')) && /\d/.test(l.slice(72))).length;
23
+ const sequenced = long.length > 0 && sequenceLike >= 0.95 * long.length;
24
+ const firstColumnFree = body.every((l) => !l.length || CARRIAGE.has(l[0]));
25
+ const left = firstColumnFree ? 2 : 1;
26
+ const right = long.length === 0 || sequenced ? 72 : Infinity;
27
+ const carriage = left === 2 && body.some((l) => l.length && l[0] !== ' ') ? 1 : 0;
28
+ const reason = right === Infinity ? 'text-beyond-72' : sequenced ? 'sequence-area' : 'default';
29
+ return { left, right, carriage, reason };
30
+ }
31
+
32
+ const NOT = new Set(['¬', '^', '~']);
33
+ const OPS3 = ['||=', '**=', '¬=>', '^=>'];
34
+ const OPS2 = ['->', '=>', '¬=', '^=', '~=', '<>', '<=', '>=', '¬<', '¬>', '^<', '^>', '~<', '~>', '||', '!!', '**', '+=', '-=', '*=', '/=', '|=', '&='];
35
+ const OPS1 = '+-*/=<>&|!:.,()%?';
36
+
37
+ const LIT_SUFFIX = /^(?:B[1-4]?X?|BX|XN|XU|GX|WX|UX|X|G|M|W|A|E|U)(?![A-Za-z0-9_$#@])/i;
38
+ const NUMBER = /^(?:\d[\d_]*(?:\.\d*)?|\.\d+)(?:[EeSsDdQq][+-]?\d+)?(?:[Bb](?![A-Za-z0-9_$#@]))?(?:[Ii](?![A-Za-z0-9_$#@]))?/;
39
+ const WORD_START = /[A-Za-z$#@_À-ɏ]/;
40
+ const WORD_REST = /[A-Za-z0-9$#@_À-ɏ]/;
41
+
42
+ // Tokens over the whole text, so a comment or literal that runs across lines is one token.
43
+ // { t: 'word', v, u } | { t: 'lit', v, suffix } | { t: 'num', v } | { t: 'op', v } | { t: 'semi' },
44
+ // each with line and col as the source has them.
45
+ export function tokenize(text, { file = null, margins = marginsOf(text) } = {}) {
46
+ const diags = [];
47
+ const process = [];
48
+ const physical = text.split(/\r?\n/);
49
+ const rows = physical.map((l, k) => {
50
+ if (PROCESS_LINE.test(l)) { process.push({ line: k + 1, options: PROCESS_LINE.exec(l)[1].replace(/;\s*$/, '').trim() }); return ''; }
51
+ return l.slice(margins.left - 1, margins.right === Infinity ? undefined : margins.right).replace(/\t/g, ' ');
52
+ });
53
+ const tokens = [];
54
+ const at = (line, col) => ({ line, col: col + margins.left, ...(file ? { file } : {}) });
55
+ let line = 0;
56
+ let i = 0;
57
+ const source = rows;
58
+ while (line < source.length) {
59
+ const s = source[line];
60
+ if (i >= s.length) { line++; i = 0; continue; }
61
+ const c = s[i];
62
+ if (c === ' ' || c === '\f' || c === '\r' || c === '\v') { i++; continue; }
63
+ if (c === '/' && s[i + 1] === '*') {
64
+ const start = at(line + 1, i);
65
+ let l = line, k = i + 2, closed = false;
66
+ while (l < source.length) {
67
+ const e = source[l].indexOf('*/', k);
68
+ if (e >= 0) { line = l; i = e + 2; closed = true; break; }
69
+ l++; k = 0;
70
+ }
71
+ if (!closed) { diags.push({ sev: 'error', kind: 'unterminated-comment', ...start }); line = source.length; }
72
+ continue;
73
+ }
74
+ if (c === "'" || c === '"') {
75
+ const start = at(line + 1, i);
76
+ let v = '', l = line, k = i + 1, closed = false;
77
+ while (l < source.length) {
78
+ const row = source[l];
79
+ if (k >= row.length) { l++; k = 0; continue; }
80
+ if (row[k] === c) {
81
+ if (row[k + 1] === c) { v += c; k += 2; continue; }
82
+ closed = true; k++; break;
83
+ }
84
+ v += row[k++];
85
+ }
86
+ if (!closed) { diags.push({ sev: 'error', kind: 'unterminated-literal', ...start }); line = source.length; i = 0; tokens.push({ t: 'lit', v, suffix: '', ...start }); continue; }
87
+ line = l; i = k;
88
+ const suf = LIT_SUFFIX.exec(source[line].slice(i));
89
+ const suffix = suf ? suf[0].toUpperCase() : '';
90
+ if (suf) i += suf[0].length;
91
+ tokens.push({ t: 'lit', v, suffix, quote: c, ...start });
92
+ continue;
93
+ }
94
+ const num = /[0-9.]/.test(c) ? NUMBER.exec(s.slice(i)) : null;
95
+ if (num && !(c === '.' && !/[0-9]/.test(s[i + 1] || ''))) {
96
+ tokens.push({ t: 'num', v: num[0], ...at(line + 1, i) });
97
+ i += num[0].length;
98
+ continue;
99
+ }
100
+ if (WORD_START.test(c)) {
101
+ let j = i + 1;
102
+ while (j < s.length && WORD_REST.test(s[j])) j++;
103
+ const v = s.slice(i, j);
104
+ tokens.push({ t: 'word', v, u: v.toUpperCase(), ...at(line + 1, i) });
105
+ i = j;
106
+ continue;
107
+ }
108
+ if (c === ';') { tokens.push({ t: 'semi', ...at(line + 1, i) }); i++; continue; }
109
+ const three = s.slice(i, i + 3);
110
+ const two = s.slice(i, i + 2);
111
+ const op = OPS3.find((o) => o === three) || OPS2.find((o) => o === two) || (OPS1.includes(c) || NOT.has(c) ? c : null);
112
+ if (op) {
113
+ tokens.push({ t: 'op', v: normalOp(op), ...at(line + 1, i) });
114
+ i += op.length;
115
+ continue;
116
+ }
117
+ diags.push({ sev: 'warn', kind: 'unexpected-char', char: c, ...at(line + 1, i) });
118
+ i++;
119
+ }
120
+ return { tokens, diags, process, margins };
121
+ }
122
+
123
+ const normalOp = (op) => op.replace(/^[\^~]/, '¬').replace(/^!!$/, '||').replace(/^!$/, '|');
124
+
125
+ // A leading `name:` is a label, `name(3):` a subscripted label, and `(SIZE, NOFOFL):` a condition
126
+ // prefix. They may repeat and mix, and are taken off before the statement is classified.
127
+ function prefixes(toks) {
128
+ const labels = [];
129
+ const conditions = [];
130
+ let i = 0;
131
+ for (;;) {
132
+ const a = toks[i], b = toks[i + 1];
133
+ if (a && a.t === 'word' && b && b.t === 'op' && b.v === ':') { labels.push({ name: a.u, line: a.line }); i += 2; continue; }
134
+ if (a && a.t === 'word' && b && b.t === 'op' && b.v === '(') {
135
+ const close = closing(toks, i + 1);
136
+ if (close > 0 && toks[close + 1] && toks[close + 1].t === 'op' && toks[close + 1].v === ':' && toks.slice(i + 2, close).every((t) => t.t === 'num' || (t.t === 'op' && (t.v === ',' || t.v === '-' || t.v === '+')))) {
137
+ labels.push({ name: a.u, line: a.line, subscript: toks.slice(i + 2, close).map((t) => t.v).join('') });
138
+ i = close + 2;
139
+ continue;
140
+ }
141
+ }
142
+ if (a && a.t === 'op' && a.v === '(') {
143
+ const close = closing(toks, i);
144
+ if (close > 0 && toks[close + 1] && toks[close + 1].t === 'op' && toks[close + 1].v === ':' && toks.slice(i + 1, close).every((t) => t.t === 'word' || (t.t === 'op' && t.v === ','))) {
145
+ for (const t of toks.slice(i + 1, close)) if (t.t === 'word') conditions.push(t.u);
146
+ i = close + 2;
147
+ continue;
148
+ }
149
+ }
150
+ return { labels, conditions, at: i };
151
+ }
152
+ }
153
+
154
+ // The index of the parenthesis closing the one at `open`, or -1 when it is not closed.
155
+ export function closing(toks, open) {
156
+ let depth = 0;
157
+ for (let k = open; k < toks.length; k++) {
158
+ const t = toks[k];
159
+ if (t.t !== 'op') continue;
160
+ if (t.v === '(') depth++;
161
+ else if (t.v === ')' && --depth === 0) return k;
162
+ }
163
+ return -1;
164
+ }
165
+
166
+ // Statements split at semicolons. Each carries its prefixes and the tokens after them; a trailing
167
+ // run with no semicolon is kept as a statement and flagged, never dropped.
168
+ export function statements(tokens) {
169
+ const out = [];
170
+ let cur = [];
171
+ const close = (semi) => {
172
+ if (!cur.length && !semi) return;
173
+ const { labels, conditions, at } = prefixes(cur);
174
+ const toks = cur.slice(at);
175
+ const first = cur[0] || semi;
176
+ const last = cur[cur.length - 1] || semi;
177
+ out.push({ labels, conditions, toks, line: first.line, endLine: (semi || last).line, ...(first.file ? { file: first.file } : {}), ...(semi ? {} : { unterminated: true }) });
178
+ cur = [];
179
+ };
180
+ for (const t of tokens) {
181
+ if (t.t === 'semi') close(t);
182
+ else cur.push(t);
183
+ }
184
+ close(null);
185
+ return out;
186
+ }
187
+
188
+ // UTF-8 where the bytes are valid UTF-8, else Latin-1: a ¬ read as Latin-1 from UTF-8 is two
189
+ // characters and moves every column after it.
190
+ export function sourceText(buf) {
191
+ const utf8 = buf.toString('utf8');
192
+ return utf8.includes('�') ? buf.toString('latin1') : utf8;
193
+ }
194
+
195
+ export function readPli(text, opts = {}) {
196
+ const { tokens, diags, process, margins } = tokenize(text, opts);
197
+ return { statements: statements(tokens), diags, process, margins };
198
+ }
@@ -0,0 +1,280 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // A PL/I source file as the program the flow engine reads (lib/dataflow.mjs summarise): data items
3
+ // with offsets and sizes from lib/pli/layout.mjs, statements as the names they read and write,
4
+ // external calls with their arguments, EXEC blocks as the engine's tokens, and the MAIN procedure's
5
+ // parameters. The engine itself is unchanged.
6
+ import { basename } from 'node:path';
7
+ import { readPli } from './lex.mjs';
8
+ import { readPliExpanded } from './include.mjs';
9
+ import { parseStatement } from './statements.mjs';
10
+ import { layout } from './layout.mjs';
11
+
12
+ // The token naming a reference: its last name at parenthesis depth 0.
13
+ function nameToken(ref) {
14
+ let depth = 0;
15
+ let last = null;
16
+ for (const t of ref.toks) {
17
+ if (t.t === 'op' && t.v === '(') depth++;
18
+ else if (t.t === 'op' && t.v === ')') depth--;
19
+ else if (depth === 0 && t.t === 'word') last = t;
20
+ }
21
+ return last;
22
+ }
23
+
24
+ // EXEC block tokens as lib/parser.mjs gives them: parentheses as separators, commas dropped, a
25
+ // period between two names joined.
26
+ function execTokens(toks) {
27
+ const out = [];
28
+ for (let k = 0; k < toks.length; k++) {
29
+ const t = toks[k];
30
+ if (t.t === 'op' && (t.v === '(' || t.v === ')')) out.push({ ...t, t: 'sep' });
31
+ else if (t.t === 'op' && t.v === ',') continue;
32
+ else if (t.t === 'op' && t.v === '.') out.push({ ...t, t: 'period', joined: !!(toks[k - 1]?.t === 'word' && toks[k + 1]?.t === 'word' && toks[k + 1].col === t.col + 1) });
33
+ else out.push(t);
34
+ }
35
+ return out;
36
+ }
37
+
38
+ const ARITHMETIC_OPS = new Set(['+', '-', '*', '/', '**', 'prefix-', 'prefix+']);
39
+
40
+ // What the Db2 precompiler writes for EXEC SQL INCLUDE SQLCA in a PL/I program (Db2 13 SQL
41
+ // Reference, "The included SQLCA").
42
+ const SQLCA_PLI = ` DECLARE
43
+ 1 SQLCA,
44
+ 2 SQLCAID CHAR(8),
45
+ 2 SQLCABC FIXED(31) BINARY,
46
+ 2 SQLCODE FIXED(31) BINARY,
47
+ 2 SQLERRM CHAR(70) VAR,
48
+ 2 SQLERRP CHAR(8),
49
+ 2 SQLERRD(6) FIXED(31) BINARY,
50
+ 2 SQLWARN,
51
+ 3 SQLWARN0 CHAR(1),
52
+ 3 SQLWARN1 CHAR(1),
53
+ 3 SQLWARN2 CHAR(1),
54
+ 3 SQLWARN3 CHAR(1),
55
+ 3 SQLWARN4 CHAR(1),
56
+ 3 SQLWARN5 CHAR(1),
57
+ 3 SQLWARN6 CHAR(1),
58
+ 3 SQLWARN7 CHAR(1),
59
+ 2 SQLEXT,
60
+ 3 SQLWARN8 CHAR(1),
61
+ 3 SQLWARN9 CHAR(1),
62
+ 3 SQLWARNA CHAR(1),
63
+ 3 SQLSTATE CHAR(5);`;
64
+
65
+ // readMember(name, fromFile) -> { path, text } | null splices %INCLUDE members in; without it the
66
+ // directives stay as statements and what they would have declared is missing.
67
+ export function pliProgram(text, file, { readMember = null } = {}) {
68
+ const read = readMember ? readPliExpanded(text, { file, readMember }) : readPli(text, { file });
69
+ const statements = read.statements.map((st) => ({ st, r: parseStatement(st) }));
70
+ const declared = [];
71
+ for (const { st, r } of statements) if (r.status === 'parsed' && r.kind === 'DECLARE') declared.push(...r.node.items.map((it) => ({ ...it, file: st.file || file })));
72
+ const sqlcaAt = statements.find(({ st, r }) => r.kind === 'EXEC' && r.node?.processor === 'SQL' && r.node.sql?.verb === 'INCLUDE' && st.toks[3]?.u === 'SQLCA')?.st;
73
+ if (sqlcaAt && !declared.some((it) => it.name === 'SQLCA')) {
74
+ for (const st of readPli(SQLCA_PLI, { file }).statements) {
75
+ const r = parseStatement(st);
76
+ if (r.kind === 'DECLARE') declared.push(...r.node.items.map((it) => ({ ...it, file: sqlcaAt.file || file, line: sqlcaAt.line })));
77
+ }
78
+ }
79
+ const { roots } = layout(declared);
80
+
81
+ const items = [];
82
+ const flatten = (node, parent) => {
83
+ const item = {
84
+ name: node.name, level: node.logical, parent, children: [], file: node.file || file, line: node.line, values: [],
85
+ offset: node.offset, occurs: node.occurs || 1,
86
+ size: node.children.length ? node.size * Math.max(1, node.occurs || 1) : node.size,
87
+ attributes: node.attributes.map((a) => a.name), storage: node.storage || null,
88
+ };
89
+ items.push(item);
90
+ for (const ch of node.children) item.children.push(flatten(ch, item));
91
+ return item;
92
+ };
93
+ for (const r of roots) flatten(r, null);
94
+ const byName = new Map();
95
+ for (const it of items) { if (!byName.has(it.name)) byName.set(it.name, []); byName.get(it.name).push(it); }
96
+ const builtin = new Set(items.filter((it) => it.attributes.includes('BUILTIN') || it.attributes.includes('GENERIC')).map((it) => it.name));
97
+
98
+ // A qualified reference A.B.C names the C whose containing structures include B and then A; after
99
+ // a locator, P->X.Y, only the part after the arrow qualifies the data.
100
+ const resolved = new Map();
101
+ const resolve = (ref) => {
102
+ const qual = ref.path.slice(ref.locators.length ? ref.locators[ref.locators.length - 1] : 0);
103
+ const cands = (byName.get(ref.name) || []).filter((it) => !builtin.has(it.name));
104
+ const fits = cands.filter((it) => {
105
+ let k = qual.length - 2;
106
+ for (let a = it.parent; a && k >= 0; a = a.parent) if (a.name === qual[k]) k--;
107
+ return k < 0;
108
+ });
109
+ const item = fits[0] || null;
110
+ const tok = nameToken(ref);
111
+ if (item && tok) resolved.set(tok, item);
112
+ return item ? tok : null;
113
+ };
114
+
115
+ // The names an expression reads: references to declared data; a reference that names no declared
116
+ // item and has arguments is a function, read through its arguments.
117
+ // A subscript names a table element; SUBSTR's start and length bound a reference into its
118
+ // first argument. Both are what the engine's subscript and reference-modification sinks read.
119
+ const tableOf = (it) => { for (let a = it; a; a = a.parent) if ((a.occurs || 1) > 1) return a; return null; };
120
+ const indexOf = (a) => {
121
+ const t = a && a.tree;
122
+ if (!t) return null;
123
+ if (t.t === 'ref') { const tok = resolve(t); return tok ? { tok, offset: 0 } : null; }
124
+ if ((t.op === '+' || t.op === '-') && t.args[0]?.t === 'ref' && t.args[1]?.t === 'num' && /^\d+$/.test(t.args[1].tok.v)) {
125
+ const tok = resolve(t.args[0]);
126
+ return tok ? { tok, offset: (t.op === '+' ? 1 : -1) * Number(t.args[1].tok.v) } : null;
127
+ }
128
+ return null;
129
+ };
130
+ const noteIndexes = (n, hostTok, indexes) => {
131
+ if (!indexes) return;
132
+ const item = resolved.get(hostTok);
133
+ if (item && tableOf(item)) {
134
+ for (const list of n.args) for (const a of list) { const x = indexOf(a); if (x) indexes.push({ host: hostTok, tok: x.tok, kind: 'subscript', ...(x.offset ? { offset: x.offset } : {}) }); }
135
+ } else if (!item && n.name === 'SUBSTR' && n.args[0]?.[0]?.tree?.t === 'ref') {
136
+ const host = resolve(n.args[0][0].tree);
137
+ const [, from, len] = n.args[0].map(indexOf);
138
+ if (host && from) indexes.push({ host, tok: from.tok, kind: 'refmod-offset', ...(from.offset ? { offset: from.offset } : {}) });
139
+ if (host && len) indexes.push({ host, tok: len.tok, kind: 'refmod-length' });
140
+ }
141
+ };
142
+ const reads = (expr, out = [], indexes = null) => {
143
+ const walk = (n) => {
144
+ if (!n) return;
145
+ if (n.t === 'ref') {
146
+ const tok = resolve(n);
147
+ if (tok) out.push(tok);
148
+ noteIndexes(n, tok, indexes);
149
+ if (!tok) for (const list of n.args) for (const a of list) if (a.tree) walk(a.tree);
150
+ for (const at of n.locators) { const p = byName.get(n.path[at - 1]); if (p) { const t = n.toks.find((x) => x.t === 'word' && x.u === n.path[at - 1]); if (t) { resolved.set(t, p[0]); out.push(t); } } }
151
+ } else if (n.t === 'paren') walk(n.expr.tree);
152
+ else if (n.t === 'lit' && n.factor) walk(n.factor.tree);
153
+ else if (n.args) n.args.forEach(walk);
154
+ };
155
+ walk(expr.tree ?? expr);
156
+ return out;
157
+ };
158
+ const hasOp = (tree, ops) => !!tree && (ops.has(tree.op) || (tree.args || []).some((a) => hasOp(a, ops)));
159
+
160
+ const prog = { id: null, line: 1, file, items, resolved, statements: [], execs: [], calls: [], paramTokens: [], labels: [], proc: null, files: [], fds: [], accepts: [], unresolvedRefs: [] };
161
+ const procs = new Map();
162
+ for (const { st, r } of statements) {
163
+ if (r.status !== 'parsed' || r.kind !== 'PROCEDURE') continue;
164
+ procs.set(r.node.name, { node: r.node, st });
165
+ if (!prog.id) {
166
+ prog.id = r.node.name;
167
+ prog.line = st.line;
168
+ }
169
+ if (r.node.options.some((o) => o.name === 'MAIN') && !prog.paramTokens.length) {
170
+ for (const p of r.node.params) {
171
+ const tok = st.toks.find((t) => t.t === 'word' && t.u === p);
172
+ const item = (byName.get(p) || [])[0];
173
+ if (tok && item) {
174
+ resolved.set(tok, item);
175
+ prog.paramTokens.push({ tok, mode: 'REFERENCE' });
176
+ // A MAIN procedure's parameter is the PARM string, or the command line off z/OS.
177
+ prog.accepts.push({ target: p, targetTok: tok, from: 'COMMAND-LINE', line: st.line, file: st.file || file });
178
+ }
179
+ }
180
+ }
181
+ }
182
+ if (!prog.id) prog.id = basename(file).replace(/\.[^.]*$/, '').toUpperCase();
183
+
184
+ const add = (st, verb, sources, targets, extra = {}) => prog.statements.push({ verb, sources, targets, file: st.file || file, line: st.line, ...extra });
185
+ const visit = (st, r, top = true) => {
186
+ if (r.status !== 'parsed' && r.status !== 'unbuilt') return;
187
+ const n = r.node;
188
+ if (!n) return;
189
+ if (r.kind === 'ASSIGNMENT') {
190
+ const indexes = [];
191
+ const sources = reads(n.value, [], indexes);
192
+ const targets = [];
193
+ for (const t of n.targets) {
194
+ const tok = resolve(t);
195
+ noteIndexes(t, tok, indexes);
196
+ if (tok) { targets.push(tok); continue; }
197
+ // A pseudovariable such as SUBSTR(X, 1, 2) = ... writes its first argument.
198
+ const first = t.args[0]?.[0];
199
+ if (first?.tree?.t === 'ref') { const a = resolve(first.tree); if (a) targets.push(a); }
200
+ }
201
+ const verb = n.value.tree?.t === 'ref' ? 'MOVE' : hasOp(n.value.tree, new Set(['||'])) ? 'STRING' : hasOp(n.value.tree, ARITHMETIC_OPS) ? 'COMPUTE' : 'MOVE';
202
+ add(st, verb, sources, targets, indexes.length ? { indexes } : {});
203
+ } else if (r.kind === 'IF' || r.kind === 'WHEN' || (r.kind === 'DO' && (n.while || n.until))) {
204
+ // A condition is what the engine credits as a check on the names it compares.
205
+ const conds = r.kind === 'IF' ? [n.cond] : r.kind === 'WHEN' ? n.values : [n.while, n.until].filter(Boolean);
206
+ const indexes = [];
207
+ const sources = conds.flatMap((c) => reads(c, [], indexes));
208
+ const ops = [];
209
+ const walk = (t) => { if (!t) return; if (t.op) ops.push(t.op); (t.args || []).forEach(walk); if (t.t === 'paren') walk(t.expr.tree); };
210
+ conds.forEach((c) => walk(c.tree));
211
+ add(st, r.kind === 'WHEN' ? 'WHEN' : 'IF', sources, [], { ops, ...(indexes.length ? { indexes } : {}) });
212
+ } else if (r.kind === 'READ') {
213
+ const into = n.options?.INTO;
214
+ const recTok = into ? resolve(into) : null;
215
+ add(st, 'READ', [], recTok ? [recTok] : []);
216
+ // A PL/I file's DD name is its own name unless OPEN gives it a TITLE; the record it reads
217
+ // into is what a job's DD for that name supplies.
218
+ const fname = n.file?.name;
219
+ if (fname && recTok) {
220
+ let fd = prog.fds.find((x) => x.name === fname);
221
+ if (!fd) { fd = { name: fname, records: [], file: st.file || file, line: st.line }; prog.fds.push(fd); prog.files.push({ name: fname, assign: { t: 'lit', v: fname }, file: st.file || file, line: st.line }); }
222
+ const rec = resolved.get(recTok);
223
+ if (rec && !fd.records.includes(rec)) fd.records.push(rec);
224
+ }
225
+ } else if (r.kind === 'WRITE' || r.kind === 'REWRITE') {
226
+ const from = n.options?.FROM;
227
+ add(st, 'WRITE', from ? [resolve(from)].filter(Boolean) : [], []);
228
+ } else if (r.kind === 'CALL') {
229
+ const internal = procs.get(n.name);
230
+ const argToks = n.args.map((a) => (a.tree?.t === 'ref' ? resolve(a.tree) : null));
231
+ if (internal) {
232
+ // Arguments pass by reference: an internal procedure's parameter is the argument's storage.
233
+ internal.node.params.forEach((p, k) => {
234
+ const pTok = internal.st.toks.find((t) => t.t === 'word' && t.u === p);
235
+ const pItem = (byName.get(p) || [])[0];
236
+ if (!pTok || !pItem || !argToks[k]) return;
237
+ resolved.set(pTok, pItem);
238
+ add(st, 'MOVE', [argToks[k]], [pTok]);
239
+ add(st, 'MOVE', [pTok], [argToks[k]]);
240
+ });
241
+ } else {
242
+ const variable = (byName.get(n.callee.name) || []).some((it) => it.attributes.includes('ENTRY') && it.attributes.includes('VARIABLE'));
243
+ const targetTok = variable ? resolve(n.callee) : nameToken(n.callee);
244
+ add(st, 'CALL', [], []);
245
+ prog.calls.push({
246
+ kind: variable ? 'I' : 'L', name: n.name, line: st.line, file: st.file || file, targetTok,
247
+ using: n.args.map((a, k) => ({ tok: argToks[k], word: argToks[k]?.u ?? null, mode: 'REFERENCE' })),
248
+ stmtIndex: prog.statements.length - 1,
249
+ });
250
+ }
251
+ } else if (r.kind === 'FETCH') {
252
+ // A TITLE that is a variable names the module loaded at run time.
253
+ for (const e of n.entries) {
254
+ const t = e.title && e.title.tree;
255
+ const tok = t && t.t === 'ref' ? resolve(t) : null;
256
+ if (!tok) continue;
257
+ add(st, 'CALL', [], []);
258
+ prog.calls.push({ kind: 'I', name: tok.u, line: st.line, file: st.file || file, targetTok: tok, using: [], stmtIndex: prog.statements.length - 1 });
259
+ }
260
+ } else if (r.kind === 'OPEN') {
261
+ // A TITLE that is a variable names the dataset opened, as SELECT ... ASSIGN TO a data item does.
262
+ for (const f of n.files) {
263
+ const t = f.options && f.options.TITLE && f.options.TITLE.tree;
264
+ if (t && t.t === 'ref' && resolve(t)) prog.files.push({ name: f.file.name, assign: { t: 'word', v: t.name }, file: st.file || file, line: st.line });
265
+ }
266
+ } else if (r.kind === 'EXEC' && top) {
267
+ prog.execs.push({ t: 'exec', kind: n.processor, toks: execTokens(st.toks.slice(2)), line: st.line, file: st.file || file });
268
+ for (const t of st.toks) if (t.t === 'word' && byName.has(t.u)) resolved.set(t, byName.get(t.u)[0]);
269
+ }
270
+ for (const u of n.units || []) if (u.node) visit(st, u, false);
271
+ };
272
+ for (const { st, r } of statements) visit(st, r);
273
+ prog.includes = { included: read.included || [], unresolved: read.unresolved || [], cycles: read.cycles || [] };
274
+ return prog;
275
+ }
276
+
277
+ // The engine's parse result for one PL/I source file.
278
+ export function parsePliSource(text, file, opts = {}) {
279
+ return { file, format: 'pli', programs: [pliProgram(text, file, opts)], options: [], diags: [], copies: [] };
280
+ }
@@ -0,0 +1,95 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Rule: BASED storage addressed through a pointer.
3
+
4
+ export const RULES = {
5
+ 'pli-based-storage-addressing': {
6
+ sev: 'info',
7
+ evidence: 'coverage',
8
+ cwe: 'CWE-119',
9
+ text: 'A program addresses storage through a pointer, so data flow through it is not followed'
10
+ }
11
+ };
12
+
13
+ const ADDRESS_ARITHMETIC = new Set(['POINTERADD', 'PTRADD', 'POINTERVALUE', 'PTRVALUE', 'POINTERSUBTRACT', 'PTRSUBTRACT']);
14
+
15
+ // Whether an expression computes an address rather than copying one.
16
+ function computed(tree) {
17
+ if (!tree) return false;
18
+ if (tree.t === 'ref') return ADDRESS_ARITHMETIC.has(tree.name) || tree.args.some((list) => list.some((a) => computed(a.tree)));
19
+ if (tree.op === '+' || tree.op === '-') return true;
20
+ if (tree.t === 'paren') return computed(tree.expr.tree);
21
+ return (tree.args || []).some(computed);
22
+ }
23
+
24
+ export function check(program) {
25
+ const basedVars = [];
26
+ const pointerVars = new Set();
27
+ let firstBasedLine = null;
28
+ let firstPointerAssign = null;
29
+
30
+ const walk = (units) => {
31
+ if (!Array.isArray(units)) return;
32
+ for (const unit of units) {
33
+ if (!unit || unit.status !== 'parsed' || !unit.node) continue;
34
+ const node = unit.node;
35
+
36
+ if (node.kind === 'DECLARE') {
37
+ for (const item of node.items || []) {
38
+ const name = item.name;
39
+ if (!name) continue;
40
+ const attrs = item.attributes || [];
41
+ const basedAttr = attrs.find((a) => a.name === 'BASED');
42
+ if (basedAttr) {
43
+ basedVars.push({ name, args: basedAttr.args || [] });
44
+ if (firstBasedLine === null) firstBasedLine = unit.line;
45
+ }
46
+ if (attrs.some((a) => a.name === 'POINTER')) {
47
+ pointerVars.add(name.toUpperCase());
48
+ }
49
+ }
50
+ } else if (node.kind === 'ASSIGNMENT') {
51
+ const targets = node.targets || [];
52
+ for (const target of targets) {
53
+ if (target && target.t === 'ref' && target.name && pointerVars.has(target.name.toUpperCase())) {
54
+ if (computed(node.value && node.value.tree) && !firstPointerAssign) firstPointerAssign = { name: target.name, line: unit.line };
55
+ }
56
+ }
57
+ }
58
+
59
+ if (node.units) walk(node.units.map((u) => ({ ...u, line: unit.line })));
60
+ }
61
+ };
62
+
63
+ walk(program.statements.map((st) => ({ ...st.parsed, line: st.line })));
64
+
65
+ if (basedVars.length === 0) return [];
66
+
67
+ const names = basedVars.slice(0, 3).map((v) => v.name);
68
+ const namesStr = names.join(', ');
69
+ const count = basedVars.length;
70
+
71
+ let detail = `${count} variable${count === 1 ? '' : 's'} declared BASED (${namesStr})`;
72
+
73
+ const overlay = (v) => v.args[0] && v.args[0].t === 'word' && v.args[0].u === 'ADDR';
74
+ const hasAddr = basedVars.some(overlay);
75
+ const hasPlain = basedVars.some((v) => !overlay(v));
76
+
77
+ if (hasAddr && hasPlain) {
78
+ detail += ', some using ADDR() of declared variables (overlays) and some plain pointers';
79
+ } else if (hasAddr) {
80
+ detail += ', using ADDR() of declared variables (overlays)';
81
+ } else if (hasPlain) {
82
+ detail += ', plain pointers addressing whatever the pointer holds';
83
+ }
84
+
85
+ if (firstPointerAssign) {
86
+ detail += `; first computed address assignment to ${firstPointerAssign.name} at line ${firstPointerAssign.line}`;
87
+ }
88
+
89
+ return [{
90
+ rule: 'pli-based-storage-addressing',
91
+ path: program.path,
92
+ line: firstBasedLine,
93
+ detail
94
+ }];
95
+ }
@@ -0,0 +1,68 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // PL/I rules on condition handling.
3
+
4
+ export const RULES = {
5
+ 'pli-error-condition-ignored': {
6
+ sev: 'low',
7
+ evidence: 'construct',
8
+ cwe: 'CWE-390',
9
+ text: 'An ON unit for a condition does nothing with it, so the condition is silently ignored',
10
+ impact: 'A failure the condition reports, such as a conversion error, a division by zero, a subscript out of range or a file that would not open, reaches an ON unit that does nothing with it, so no message or log says the operation failed',
11
+ remedy: 'Handle the condition in its ON unit: record it and recover, or end the run with a message; where a condition is expected, test for it and act rather than leave the unit empty'
12
+ }
13
+ };
14
+
15
+ const IGNORED_CONDITIONS = new Set([
16
+ 'ERROR', 'ANYCONDITION', 'ANYCOND', 'CONVERSION', 'CONV',
17
+ 'ZERODIVIDE', 'ZDIV', 'FIXEDOVERFLOW', 'FOFL', 'SIZE',
18
+ 'STRINGRANGE', 'STRG', 'SUBSCRIPTRANGE', 'SUBRG', 'KEY',
19
+ 'RECORD', 'TRANSMIT', 'UNDEFINEDFILE', 'UNDF'
20
+ ]);
21
+
22
+ const BLOCK_OPENERS = new Set(['BEGIN', 'DO', 'SELECT', 'PROCEDURE', 'PACKAGE']);
23
+
24
+ // Whether a statement opens a block that a later END closes: BEGIN, DO, SELECT or a procedure,
25
+ // itself or as the unit of an IF, ELSE, WHEN, OTHERWISE or ON.
26
+ function opens(r) {
27
+ if (!r || r.status !== 'parsed' || !r.node) return false;
28
+ if (BLOCK_OPENERS.has(r.kind)) return true;
29
+ const units = r.node.units || [];
30
+ return units.length > 0 && opens(units[units.length - 1]);
31
+ }
32
+
33
+ // The statements of the block a statement at `at` opens, up to and not including its END.
34
+ function blockBody(statements, at) {
35
+ let depth = 1;
36
+ for (let k = at + 1; k < statements.length; k++) {
37
+ const r = statements[k].parsed;
38
+ if (r.kind === 'END') { if (--depth === 0) return statements.slice(at + 1, k); continue; }
39
+ if (opens(r)) depth++;
40
+ }
41
+ return statements.slice(at + 1);
42
+ }
43
+
44
+ // Doing nothing is a null unit, a block that is empty, or only jumping away.
45
+ const inert = (r) => r.kind === 'NULL' || r.kind === 'GOTO';
46
+
47
+ export function check(program) {
48
+ const findings = [];
49
+ const statements = program.statements;
50
+ statements.forEach((st, at) => {
51
+ const r = st.parsed;
52
+ if (r.status !== 'parsed' || r.kind !== 'ON' || r.node.system) return;
53
+ const names = r.node.conditions.filter((c) => IGNORED_CONDITIONS.has(c.name)).map((c) => c.name);
54
+ if (!names.length) return;
55
+ const unit = r.node.units[0];
56
+ const on = `ON ${names.join(', ')}`;
57
+ let detail = null;
58
+ if (!unit) detail = `${on} has a null unit, so the condition is raised and dropped`;
59
+ else if (unit.kind === 'GOTO') detail = `${on} only does GO TO ${unit.node.target.path.join('.')}`;
60
+ else if (unit.kind === 'BEGIN') {
61
+ const body = blockBody(statements, at).map((s) => s.parsed);
62
+ if (!body.length) detail = `${on} has an empty BEGIN block`;
63
+ else if (body.every(inert)) detail = `${on} has a BEGIN block that only does GO TO ${body.filter((b) => b.kind === 'GOTO').map((b) => b.node.target.path.join('.')).join(', ')}`;
64
+ }
65
+ if (detail) findings.push({ rule: 'pli-error-condition-ignored', path: program.path, line: st.line, detail });
66
+ });
67
+ return findings;
68
+ }