@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,283 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Parsers for PL/I stream I/O statements: GET, PUT, DISPLAY, FORMAT, and FLUSH.
3
+ import { cursor, split } from '../cursor.mjs';
4
+ import { parseExpression, parseReference } from '../expr.mjs';
5
+
6
+ function parseFormatItem(c) {
7
+ const start = c.pos;
8
+ let factor = null;
9
+ // A parenthesised group followed by another item is an iteration factor, (N) F(5,2); one followed
10
+ // by ',' or the end is a nested format list, (A, X(2)).
11
+ if (c.isOp('(')) {
12
+ const inner = c.group();
13
+ const next = c.peek();
14
+ if (next && (next.t === 'word' || (next.t === 'op' && next.v === '('))) {
15
+ const s = cursor(inner);
16
+ factor = parseExpression(s);
17
+ if (!s.done()) s.fail('end of iteration factor');
18
+ } else {
19
+ c.pos = start;
20
+ }
21
+ } else if (c.peek() && c.peek().t === 'num') {
22
+ factor = c.next();
23
+ }
24
+
25
+ if (c.isOp('(')) {
26
+ const inner = c.group();
27
+ const s = cursor(inner);
28
+ const items = [];
29
+ while (!s.done()) {
30
+ const item = parseFormatItem(s);
31
+ items.push(item);
32
+ if (!s.done()) {
33
+ if (s.isOp(',')) s.next();
34
+ else s.fail("',' or ')'");
35
+ }
36
+ }
37
+ const toks = c.slice(start);
38
+ return { factor, items, toks };
39
+ }
40
+
41
+ const t = c.peek();
42
+ if (!t) c.fail('a format item');
43
+ if (t.t === 'word') {
44
+ c.next();
45
+ if (c.isOp('(')) c.group();
46
+ else if (t.u === 'P' && c.peek() && c.peek().t === 'lit') c.next();
47
+ } else if (t.t === 'lit') {
48
+ c.next();
49
+ } else {
50
+ c.fail('a format item');
51
+ }
52
+ return { factor, items: null, toks: c.slice(start) };
53
+ }
54
+
55
+ function parseFormatList(c) {
56
+ const inner = c.group();
57
+ const s = cursor(inner);
58
+ const items = [];
59
+ while (!s.done()) {
60
+ const item = parseFormatItem(s);
61
+ items.push(item);
62
+ if (!s.done()) {
63
+ if (s.isOp(',')) s.next();
64
+ else s.fail("',' or ')'");
65
+ }
66
+ }
67
+ return items;
68
+ }
69
+
70
+ // A repetitive specification, (X(I) DO I = 1 TO N), is an item whose parentheses hold a DO at depth one.
71
+ function isRepetitive(toks) {
72
+ if (!toks.length || toks[0].t !== 'op' || toks[0].v !== '(') return false;
73
+ let depth = 0;
74
+ for (const t of toks) {
75
+ if (t.t === 'op' && t.v === '(') depth++;
76
+ else if (t.t === 'op' && t.v === ')') depth--;
77
+ else if (depth === 1 && t.t === 'word' && t.u === 'DO') return true;
78
+ }
79
+ return false;
80
+ }
81
+
82
+ function parseDataList(c) {
83
+ const items = [];
84
+ for (const toks of split(c.group(), ',')) {
85
+ if (!toks.length) c.fail('a data item');
86
+ if (isRepetitive(toks)) { items.push({ t: 'repetitive', toks }); continue; }
87
+ const s = cursor(toks);
88
+ items.push(parseExpression(s));
89
+ if (!s.done()) s.fail("',' or ')'");
90
+ }
91
+ return items;
92
+ }
93
+
94
+ function parsePut(c, stmt, ctx) {
95
+ c.expectWord('PUT');
96
+ const node = { kind: 'PUT', file: null, string: null, page: false, line: null, skip: null, mode: null, data: [], formats: [] };
97
+
98
+ while (!c.done()) {
99
+ if (c.isWord('FILE')) {
100
+ c.next();
101
+ c.expectOp('(');
102
+ node.file = parseReference(c);
103
+ c.expectOp(')');
104
+ } else if (c.isWord('STRING')) {
105
+ c.next();
106
+ c.expectOp('(');
107
+ node.string = parseExpression(c);
108
+ c.expectOp(')');
109
+ } else if (c.isWord('PAGE')) {
110
+ c.next();
111
+ node.page = true;
112
+ } else if (c.isWord('LINE')) {
113
+ c.next();
114
+ c.expectOp('(');
115
+ node.line = parseExpression(c);
116
+ c.expectOp(')');
117
+ } else if (c.isWord('SKIP')) {
118
+ c.next();
119
+ if (c.isOp('(')) {
120
+ c.next();
121
+ node.skip = parseExpression(c);
122
+ c.expectOp(')');
123
+ } else {
124
+ node.skip = true;
125
+ }
126
+ } else if (c.isWord('LIST')) {
127
+ c.next();
128
+ node.mode = 'LIST';
129
+ if (c.isOp('(')) {
130
+ node.data = parseDataList(c);
131
+ }
132
+ } else if (c.isWord('EDIT')) {
133
+ c.next();
134
+ node.mode = 'EDIT';
135
+ // EDIT takes one or more (data list) (format list) pairs.
136
+ do {
137
+ node.data.push(...parseDataList(c));
138
+ node.formats.push(parseFormatList(c));
139
+ } while (c.isOp('('));
140
+ } else if (c.isWord('DATA')) {
141
+ c.next();
142
+ node.mode = 'DATA';
143
+ if (c.isOp('(')) {
144
+ node.data = parseDataList(c);
145
+ }
146
+ } else {
147
+ c.fail('PUT option');
148
+ }
149
+ }
150
+ return node;
151
+ }
152
+
153
+ function parseGet(c, stmt, ctx) {
154
+ c.expectWord('GET');
155
+ const node = { kind: 'GET', file: null, string: null, page: false, line: null, skip: null, copy: null, mode: null, data: [], formats: [] };
156
+
157
+ while (!c.done()) {
158
+ if (c.isWord('FILE')) {
159
+ c.next();
160
+ c.expectOp('(');
161
+ node.file = parseReference(c);
162
+ c.expectOp(')');
163
+ } else if (c.isWord('STRING')) {
164
+ c.next();
165
+ c.expectOp('(');
166
+ node.string = parseExpression(c);
167
+ c.expectOp(')');
168
+ } else if (c.isWord('PAGE')) {
169
+ c.next();
170
+ node.page = true;
171
+ } else if (c.isWord('LINE')) {
172
+ c.next();
173
+ c.expectOp('(');
174
+ node.line = parseExpression(c);
175
+ c.expectOp(')');
176
+ } else if (c.isWord('SKIP')) {
177
+ c.next();
178
+ if (c.isOp('(')) {
179
+ c.next();
180
+ node.skip = parseExpression(c);
181
+ c.expectOp(')');
182
+ } else {
183
+ node.skip = true;
184
+ }
185
+ } else if (c.isWord('COPY')) {
186
+ c.next();
187
+ if (c.isOp('(')) {
188
+ c.next();
189
+ node.copy = parseReference(c);
190
+ c.expectOp(')');
191
+ } else {
192
+ node.copy = true;
193
+ }
194
+ } else if (c.isWord('LIST')) {
195
+ c.next();
196
+ node.mode = 'LIST';
197
+ if (c.isOp('(')) {
198
+ node.data = parseDataList(c);
199
+ }
200
+ } else if (c.isWord('EDIT')) {
201
+ c.next();
202
+ node.mode = 'EDIT';
203
+ // EDIT takes one or more (data list) (format list) pairs.
204
+ do {
205
+ node.data.push(...parseDataList(c));
206
+ node.formats.push(parseFormatList(c));
207
+ } while (c.isOp('('));
208
+ } else if (c.isWord('DATA')) {
209
+ c.next();
210
+ node.mode = 'DATA';
211
+ if (c.isOp('(')) {
212
+ node.data = parseDataList(c);
213
+ }
214
+ } else {
215
+ c.fail('GET option');
216
+ }
217
+ }
218
+ return node;
219
+ }
220
+
221
+ function parseDisplay(c, stmt, ctx) {
222
+ c.expectWord('DISPLAY');
223
+ c.expectOp('(');
224
+ const value = parseExpression(c);
225
+ c.expectOp(')');
226
+ const node = { kind: 'DISPLAY', value, reply: null, event: null, routcde: null, desc: null };
227
+
228
+ while (!c.done()) {
229
+ if (c.isWord('REPLY')) {
230
+ c.next();
231
+ c.expectOp('(');
232
+ node.reply = parseReference(c);
233
+ c.expectOp(')');
234
+ } else if (c.isWord('EVENT')) {
235
+ c.next();
236
+ c.expectOp('(');
237
+ node.event = parseReference(c);
238
+ c.expectOp(')');
239
+ } else if (c.isWord('ROUTCDE')) {
240
+ c.next();
241
+ c.expectOp('(');
242
+ node.routcde = parseDataList(c);
243
+ c.expectOp(')');
244
+ } else if (c.isWord('DESC')) {
245
+ c.next();
246
+ c.expectOp('(');
247
+ node.desc = parseDataList(c);
248
+ c.expectOp(')');
249
+ } else {
250
+ c.fail('DISPLAY option');
251
+ }
252
+ }
253
+ return node;
254
+ }
255
+
256
+ function parseFormat(c, stmt, ctx) {
257
+ c.expectWord('FORMAT');
258
+ const items = parseFormatList(c);
259
+ return { kind: 'FORMAT', items };
260
+ }
261
+
262
+ function parseFlush(c, stmt, ctx) {
263
+ c.expectWord('FLUSH');
264
+ c.expectWord('FILE');
265
+ c.expectOp('(');
266
+ let file;
267
+ if (c.isOp('*')) {
268
+ c.next();
269
+ file = '*';
270
+ } else {
271
+ file = parseReference(c);
272
+ }
273
+ c.expectOp(')');
274
+ return { kind: 'FLUSH', file };
275
+ }
276
+
277
+ export const parsers = {
278
+ PUT: parsePut,
279
+ GET: parseGet,
280
+ DISPLAY: parseDisplay,
281
+ FORMAT: parseFormat,
282
+ FLUSH: parseFlush,
283
+ };
@@ -0,0 +1,129 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // The storage a PL/I element takes and the boundary it needs, from its DECLARE attributes, as the
3
+ // Enterprise PL/I Language Reference's alignment table gives them (LP(32), OFFSETSIZE(4)). Sizes
4
+ // and alignments are in bits so unaligned bit strings pack.
5
+
6
+ const STRING_TYPES = new Set(['CHARACTER', 'BIT', 'GRAPHIC', 'WIDECHAR', 'UCHAR']);
7
+ const UNALIGNED_BY_DEFAULT = new Set([...STRING_TYPES, 'PICTURE']);
8
+ const ARITHMETIC = new Set(['FIXED', 'FLOAT', 'BINARY', 'DECIMAL', 'PRECISION', 'SIGNED', 'UNSIGNED', 'REAL', 'COMPLEX']);
9
+ const UNIT = { CHARACTER: 8, BIT: 1, GRAPHIC: 16, WIDECHAR: 16, UCHAR: 32 };
10
+ // Program-control data: bytes and alignment in bytes.
11
+ const CONTROL = { POINTER: [4, 4], HANDLE: [4, 4], OFFSET: [4, 4], FILE: [4, 4], ENTRY: [8, 4], LABEL: [8, 4], FORMAT: [8, 4], TASK: [16, 4] };
12
+
13
+ // Names that occupy no storage of their own.
14
+ const NO_STORAGE = new Set(['BUILTIN', 'GENERIC', 'CONDITION']);
15
+ // File description attributes declare a file even when FILE itself is not written.
16
+ const FILE_DESCRIPTION = new Set(['STREAM', 'RECORD', 'INPUT', 'OUTPUT', 'UPDATE', 'SEQUENTIAL', 'DIRECT', 'TRANSIENT', 'KEYED', 'PRINT', 'ENVIRONMENT', 'BUFFERED', 'UNBUFFERED', 'BACKWARDS', 'EXCLUSIVE']);
17
+ // Attributes that say nothing about the data type, so a name carrying only these takes the default.
18
+ const NOT_TYPE = new Set(['STATIC', 'AUTOMATIC', 'BASED', 'CONTROLLED', 'DEFINED', 'POSITION', 'PARAMETER', 'EXTERNAL', 'INTERNAL', 'INITIAL', 'ALIGNED', 'UNALIGNED', 'NORMAL', 'ABNORMAL', 'ASSIGNABLE', 'NONASSIGNABLE', 'CONNECTED', 'NONCONNECTED', 'BYADDR', 'BYVALUE', 'OPTIONAL', 'INONLY', 'OUTONLY', 'INOUT', 'RESERVED', 'VALUE']);
19
+
20
+ const plainNumber = (toks) => (toks && toks.length === 1 && toks[0].t === 'num' && /^\d+$/.test(toks[0].v) ? Number(toks[0].v) : null);
21
+
22
+ function precisionOf(args) {
23
+ if (!args || !args.length) return null;
24
+ const parts = [[]];
25
+ for (const t of args) { if (t.t === 'op' && t.v === ',') parts.push([]); else parts[parts.length - 1].push(t); }
26
+ const signed = (p) => (p.length === 2 && p[0].t === 'op' && p[0].v === '-' ? -plainNumber([p[1]]) : plainNumber(p));
27
+ return { p: plainNumber(parts[0]), q: parts[1] ? signed(parts[1]) : 0 };
28
+ }
29
+
30
+ // Bytes a PICTURE occupies: every picture character except V, K and the F scaling factor, a
31
+ // repetition factor (n) multiplying the character after it, CR and DB two characters each.
32
+ export function pictureBytes(pic) {
33
+ const s = pic.toUpperCase().replace(/F\(\s*[+-]?\d+\s*\)/g, '');
34
+ let n = 0;
35
+ for (const m of s.matchAll(/\((\d+)\)(CR|DB|.)|(CR|DB)|([^VK])|[VK]/g)) {
36
+ if (m[2]) n += Number(m[1]) * (m[2].length === 2 ? 2 : m[2] === 'V' || m[2] === 'K' ? 0 : 1);
37
+ else if (m[3]) n += 2;
38
+ else if (m[4]) n += 1;
39
+ }
40
+ return n;
41
+ }
42
+
43
+ // { type, bits, align (bits), aligned, varying, base, scale, precision, known } for one element.
44
+ // bits is null when an extent is not a constant (CHAR(*), CHAR(N), REFER). align is what the
45
+ // element needs once ALIGNED or UNALIGNED is settled; `inherited` is the containing structure's
46
+ // explicit choice, used when the element makes none of its own.
47
+ export function storageOf(attributes, { inherited = null, name = null } = {}) {
48
+ const has = (n) => attributes.find((a) => a.name === n);
49
+ if (attributes.some((a) => NO_STORAGE.has(a.name))) return { type: attributes.find((a) => NO_STORAGE.has(a.name)).name, bits: 0, align: 8, aligned: null, known: true, storage: false };
50
+ if (has('TYPE')) return { type: 'TYPE', typeName: has('TYPE').args?.[0]?.u ?? null, bits: null, align: 8, aligned: null, known: false };
51
+ if (!has('FILE') && attributes.some((a) => FILE_DESCRIPTION.has(a.name))) attributes = [...attributes, { name: 'FILE', args: null }];
52
+ // RULES(IBM): a name with no data attribute is FIXED BINARY(15) when it starts with I to N, else
53
+ // FLOAT DECIMAL(6).
54
+ if (name && attributes.every((a) => NOT_TYPE.has(a.name))) {
55
+ const implicit = /^[I-N]/.test(name) ? [{ name: 'FIXED', args: null }, { name: 'BINARY', args: null }] : [{ name: 'FLOAT', args: null }, { name: 'DECIMAL', args: null }];
56
+ return { ...storageOf([...attributes, ...implicit], { inherited }), implicit: true };
57
+ }
58
+ const explicit = has('ALIGNED') ? true : has('UNALIGNED') ? false : null;
59
+ const alignedN = has('ALIGNED') && plainNumber(has('ALIGNED').args);
60
+ let type = null;
61
+ let bits = null;
62
+ let aligned;
63
+ let alignAligned = 8;
64
+ let alignUnaligned = 8;
65
+ const out = {};
66
+
67
+ const stringType = attributes.find((a) => STRING_TYPES.has(a.name));
68
+ const varying = has('VARYING') ? 'VARYING' : has('VARYINGZ') ? 'VARYINGZ' : has('VARYING4') ? 'VARYING4' : null;
69
+ if (stringType) {
70
+ type = stringType.name;
71
+ const n = stringType.args ? plainNumber(stringType.args) : 1;
72
+ const unit = UNIT[type];
73
+ aligned = explicit ?? inherited ?? !UNALIGNED_BY_DEFAULT.has(type);
74
+ if (n != null) {
75
+ if (type === 'BIT' && !varying) bits = aligned ? Math.ceil(n / 8) * 8 : n;
76
+ else if (type === 'BIT') bits = (varying === 'VARYING4' ? 32 : 16) + (aligned ? Math.ceil(n / 8) * 8 : n);
77
+ else bits = n * unit + (varying === 'VARYING' ? 16 : varying === 'VARYING4' ? 32 : varying === 'VARYINGZ' ? (type === 'CHARACTER' || type === 'UCHAR' ? 8 : 16) : 0);
78
+ }
79
+ alignAligned = varying === 'VARYING' ? 16 : varying === 'VARYING4' ? 32 : 8;
80
+ alignUnaligned = type === 'BIT' && !varying ? 1 : 8;
81
+ Object.assign(out, { length: n, varying });
82
+ } else if (has('PICTURE')) {
83
+ type = 'PICTURE';
84
+ aligned = explicit ?? inherited ?? false;
85
+ bits = has('PICTURE').picture != null ? pictureBytes(has('PICTURE').picture) * 8 : null;
86
+ out.picture = has('PICTURE').picture ?? null;
87
+ } else if (has('AREA')) {
88
+ type = 'AREA';
89
+ aligned = true;
90
+ const n = has('AREA').args ? plainNumber(has('AREA').args) : 1000;
91
+ bits = n == null ? null : (16 + n) * 8;
92
+ alignAligned = alignUnaligned = 64;
93
+ } else if (Object.keys(CONTROL).some((k) => has(k))) {
94
+ type = Object.keys(CONTROL).find((k) => has(k));
95
+ aligned = explicit ?? inherited ?? true;
96
+ const limited = type === 'ENTRY' && has('LIMITED');
97
+ const [bytes, align] = limited ? [4, 4] : CONTROL[type];
98
+ bits = bytes * 8;
99
+ alignAligned = align * 8;
100
+ } else if (attributes.some((a) => ARITHMETIC.has(a.name))) {
101
+ const last = (names) => [...attributes].reverse().find((a) => names.includes(a.name) && a.args);
102
+ const scale = has('FIXED') ? 'FIXED' : 'FLOAT';
103
+ const base = has('BINARY') ? 'BINARY' : 'DECIMAL';
104
+ const prec = precisionOf(last(['FIXED', 'FLOAT', 'BINARY', 'DECIMAL', 'PRECISION'])?.args);
105
+ const unsigned = !!has('UNSIGNED');
106
+ const p = prec?.p ?? (scale === 'FIXED' ? (base === 'BINARY' ? 15 : 5) : base === 'BINARY' ? 21 : 6);
107
+ type = `${scale} ${base}`;
108
+ aligned = explicit ?? inherited ?? true;
109
+ Object.assign(out, { scale, base, precision: p, fraction: prec?.q ?? 0, signed: !unsigned });
110
+ if (prec && prec.p == null) bits = null;
111
+ else if (scale === 'FIXED' && base === 'DECIMAL') { bits = Math.ceil((p + 1) / 2) * 8; alignAligned = 8; }
112
+ else if (scale === 'FIXED') {
113
+ const q = unsigned ? p - 1 : p;
114
+ const bytes = q <= 7 ? 1 : q <= 15 ? 2 : q <= 31 ? 4 : 8;
115
+ bits = bytes * 8;
116
+ alignAligned = bytes * 8;
117
+ } else {
118
+ const short = base === 'BINARY' ? p <= 21 : p <= 6;
119
+ const long = base === 'BINARY' ? p <= 53 : p <= 16;
120
+ bits = short ? 32 : long ? 64 : 128;
121
+ alignAligned = short ? 32 : 64;
122
+ }
123
+ if (has('COMPLEX') && bits != null) bits *= 2;
124
+ } else {
125
+ return { type: null, bits: null, align: 8, aligned: explicit ?? inherited ?? null, known: false };
126
+ }
127
+ const align = alignedN ? alignedN * 8 : aligned ? alignAligned : alignUnaligned;
128
+ return { type, bits, align, aligned, known: bits != null, ...out };
129
+ }
package/lib/policy.mjs CHANGED
@@ -26,9 +26,13 @@ export const DEFAULT_POLICY = Object.freeze({
26
26
  checks: ['subscript', 'reference-modification'],
27
27
  forbid: { enterprise: [], gnucobol: [], gcobol: [] },
28
28
  requireEquivalence: 'never',
29
+ interface: 'warn',
29
30
  });
30
31
 
31
32
  export const EQUIVALENCE_MODES = ['never', 'machineAuthored', 'always'];
33
+ // How an abend from a subprogram fuzzed at its interface, with no caller run shown to pass the input,
34
+ // is gated: `warn` never blocks on it, `tier` gates it as any other finding.
35
+ export const INTERFACE_MODES = ['warn', 'tier'];
32
36
 
33
37
  const KEYS = Object.keys(DEFAULT_POLICY);
34
38
  const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
@@ -64,6 +68,7 @@ export function validatePolicy(raw) {
64
68
  else for (const c of raw.checks) if (!CHECKS.includes(c)) errs.push(`checks: ${printable(c, 40)} is not one of ${CHECKS.join(', ')}`);
65
69
  }
66
70
  if ('requireEquivalence' in raw && !EQUIVALENCE_MODES.includes(raw.requireEquivalence)) errs.push(`requireEquivalence takes ${EQUIVALENCE_MODES.join(', ')}`);
71
+ if ('interface' in raw && !INTERFACE_MODES.includes(raw.interface)) errs.push(`interface takes ${INTERFACE_MODES.join(', ')}`);
67
72
  if ('forbid' in raw) {
68
73
  if (!isObject(raw.forbid)) errs.push(`forbid must be an object keyed by ${COMPILERS.join(', ')}`);
69
74
  else for (const [c, list] of Object.entries(raw.forbid)) {
@@ -142,6 +147,7 @@ export function combinePolicies({ floor = null, repository = null } = {}) {
142
147
  checks: checks.value,
143
148
  forbid: Object.fromEntries(COMPILERS.map((c) => [c, union((f.forbid || {})[c] || [], (r.forbid || {})[c] || [])])),
144
149
  requireEquivalence: pick('requireEquivalence', (a, b) => (EQUIVALENCE_MODES.indexOf(a) >= EQUIVALENCE_MODES.indexOf(b) ? a : b)),
150
+ interface: pick('interface', (a, b) => (a === 'tier' || b === 'tier' ? 'tier' : 'warn')),
145
151
  rules: {},
146
152
  };
147
153
  setBy.checks = checks.by;
@@ -0,0 +1,124 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // The compile check `cobolwork build --precompile` runs before the caller's compiler: each program the
3
+ // compiler command names that holds EXEC SQL or EXEC CICS is translated by lib/precompile.mjs into a
4
+ // directory of its own, beside the stand-in copybooks and translated members it needs, and given to
5
+ // the same compiler with -fsyntax-only, so a program GnuCOBOL refuses only for its EXEC blocks is
6
+ // checked. Nothing it writes outlives the check.
7
+ import { spawnSync } from 'node:child_process';
8
+ import { mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { basename, delimiter, dirname, extname, join, relative, resolve } from 'node:path';
11
+ import { parseBms } from './bms.mjs';
12
+ import { printable } from './kernel/printable.mjs';
13
+ import { detectFormat, parseSource } from './parser.mjs';
14
+ import { precompile } from './precompile.mjs';
15
+ import { isBms, isProgram, readSource } from './sources.mjs';
16
+
17
+ const COPY_EXTENSIONS = ['', '.cpy', '.CPY', '.cbl', '.CBL', '.cob', '.COB'];
18
+ const MAX_ERRORS = 3;
19
+
20
+ // A COPY member's text from the first directory holding it, as cobc searches them.
21
+ export const copybookIn = (dirs) => (name) => {
22
+ for (const d of dirs) {
23
+ for (const ext of COPY_EXTENSIONS) {
24
+ const p = join(d, name + ext);
25
+ try { if (statSync(p).isFile()) return readSource(p).text; } catch { /* not here */ }
26
+ }
27
+ }
28
+ return null;
29
+ };
30
+
31
+ // The BMS files under a repository, the member names its other files supply, and the mapsets it holds
32
+ // no copybook for, whose symbolic maps the translator writes.
33
+ export function repositoryBms(repoRoot) {
34
+ const files = [];
35
+ const walk = (d) => {
36
+ let es;
37
+ try { es = readdirSync(d, { withFileTypes: true }); } catch { return; }
38
+ for (const e of es) {
39
+ if (e.name === '.git' || e.name.startsWith('._')) continue;
40
+ const p = join(d, e.name);
41
+ if (e.isDirectory()) walk(p); else files.push(p);
42
+ }
43
+ };
44
+ walk(repoRoot);
45
+ const bms = files.filter(isBms);
46
+ const members = new Set(files.filter((f) => !bms.includes(f)).map((f) => basename(f, extname(f)).toUpperCase()));
47
+ const mapsets = bms.flatMap((f) => { try { return parseBms(readFileSync(f, 'latin1')).mapsets; } catch { return []; } })
48
+ .filter((ms) => ms.name && !members.has(ms.name.toUpperCase()));
49
+ return { bms, has: (n) => members.has(n.toUpperCase()), mapsets };
50
+ }
51
+
52
+ const EXEC = /EXEC\s+(?:SQL|CICS)\b/i;
53
+ const DLI = /EXEC\s+DLI\b/i;
54
+ const redact = (s) => String(s).replace(/'[^']*'|"[^"]*"/g, "'…'");
55
+
56
+ // The copy directories a cobc or gcobol argument vector names, in order: -I <dir> and -I<dir>.
57
+ function includeDirsOf(argv, cwd) {
58
+ const dirs = [];
59
+ argv.forEach((a, i) => {
60
+ if (a === '-I' && argv[i + 1]) dirs.push(resolve(cwd, argv[i + 1]));
61
+ else if (a.startsWith('-I') && a.length > 2) dirs.push(resolve(cwd, a.slice(2)));
62
+ });
63
+ return dirs;
64
+ }
65
+
66
+ // The argument vector with -o and its value taken out: a syntax check writes nothing.
67
+ const withoutOutput = (argv) => argv.filter((a, i) => a !== '-o' && argv[i - 1] !== '-o' && !(a.startsWith('-o') && a.length > 2));
68
+
69
+ // A compiler's first error lines, each named by the source line it came from and with every quoted
70
+ // literal masked, since a diagnostic may quote the program and no output of the gate carries source.
71
+ function errorsOf(stderr, translated, map, rel) {
72
+ const out = [];
73
+ for (const line of String(stderr || '').split('\n')) {
74
+ const m = /^(.*?):(\d+):\s*error:\s*(.*)$/.exec(line);
75
+ if (!m) continue;
76
+ const at = resolve(m[1]) === translated ? `${rel}:${map[Number(m[2]) - 1] ?? m[2]}` : `${basename(m[1])}:${m[2]}`;
77
+ out.push(printable(`${at}: ${redact(m[3])}`, 300));
78
+ if (out.length === MAX_ERRORS) break;
79
+ }
80
+ return out;
81
+ }
82
+
83
+ // Runs the check. `compilerPath` is the resolved compiler and `argv` the caller's arguments after it;
84
+ // `cwd` is where the caller's compiler runs, so its relative paths resolve there. Returns, for each
85
+ // program the argument vector names that holds EXEC SQL or CICS, whether the compiler accepted its
86
+ // translation and its first errors; programs holding EXEC DLI, which nothing translates, are skipped.
87
+ export function precompileCheck({ compilerPath, argv, cwd = process.cwd(), repo, copylibs = [], env = process.env }) {
88
+ const sources = argv.map((a, i) => ({ a, i, path: resolve(cwd, a) }))
89
+ .filter(({ a, i, path }) => !a.startsWith('-') && argv[i - 1] !== '-o' && argv[i - 1] !== '-I' && isProgram(path) && isFile(path));
90
+ const includeDirs = [...includeDirsOf(argv, cwd), ...String(env.COBCPY || '').split(delimiter).filter(Boolean), ...copylibs];
91
+ const others = withoutOutput(argv.filter((_, i) => !sources.some((s) => s.i === i)));
92
+ const programs = [];
93
+ const skipped = [];
94
+ let mapsets = null;
95
+ const root = mkdtempSync(join(tmpdir(), 'cw-precompile-'));
96
+ try {
97
+ for (const { path } of sources) {
98
+ const rel = printable(relative(repo, path).startsWith('..') ? relative(cwd, path) : relative(repo, path), 200);
99
+ const text = readSource(path).text;
100
+ if (DLI.test(text)) { skipped.push({ path: rel, why: 'EXEC DLI is not translated' }); continue; }
101
+ if (!EXEC.test(text)) continue;
102
+ const format = detectFormat(text);
103
+ const dirs = [dirname(path), ...includeDirs];
104
+ let items = [];
105
+ try { items = parseSource(text, path, { format, includeDirs: dirs }).programs.flatMap((p) => p.items); } catch { /* unparsed: no host-variable arrays known */ }
106
+ if (/EXEC\s+CICS\b/i.test(text) && !mapsets) mapsets = repositoryBms(repo).mapsets;
107
+ const p = precompile(text, { format, items, mapsets: /EXEC\s+CICS\b/i.test(text) ? mapsets : [], copybook: copybookIn(dirs) });
108
+ const dir = mkdtempSync(join(root, 'p-'));
109
+ const translated = join(dir, basename(path));
110
+ writeFileSync(translated, p.text);
111
+ for (const [name, body] of Object.entries(p.copybooks)) writeFileSync(join(dir, `${name}.cpy`), body);
112
+ const r = spawnSync(compilerPath, ['-fsyntax-only', '-I', dir, ...others, translated], { cwd, env, encoding: 'utf8', windowsHide: true });
113
+ const ok = !r.error && r.status === 0;
114
+ programs.push({ path: rel, ok, ...(ok ? {} : { errors: r.error ? [printable(r.error.message, 200)] : errorsOf(r.stderr, translated, p.map, rel) }) });
115
+ }
116
+ } finally {
117
+ rmSync(root, { recursive: true, force: true });
118
+ }
119
+ return { argv: ['-fsyntax-only', '-I', '<translation>', ...others, '<program>'], programs, skipped };
120
+ }
121
+
122
+ function isFile(path) {
123
+ try { return statSync(path).isFile(); } catch { return false; }
124
+ }
@@ -12,7 +12,7 @@ export const builtinValue = (fn, name) => (fn === 'RESP' ? DFHRESP : DFHVALUE)[n
12
12
  const squeeze = (text) => text.replace(/('[^']*'|"[^"]*")|\s+/g, (_, lit) => lit || ' ').trim();
13
13
 
14
14
  // The options of an EXEC CICS command in order: each word, with its argument's text when it has one.
15
- function cicsOptions(text) {
15
+ export function cicsOptions(text) {
16
16
  const out = [];
17
17
  let i = 0;
18
18
  while (i < text.length) {
@@ -39,13 +39,17 @@ function cicsOptions(text) {
39
39
  return out;
40
40
  }
41
41
 
42
- // Which table entry a command is: the one an option word identifies (SEND MAP by MAP), else the verb
43
- // alone, else the one IBM makes the default (READQ is READQ TS).
42
+ // Which table entry a command is: the longest one whose name the words after the verb spell in order
43
+ // (WEB READ HTTPHEADER; INQUIRE TRANSACTION though a PROGRAM option follows), else the one an
44
+ // option word identifies (SEND MAP by MAP), else the verb alone, else the one IBM makes the default
45
+ // (READQ is READQ TS).
44
46
  export function cicsCommand(options) {
45
47
  const verb = options[0]?.word;
46
48
  const words = new Set(options.slice(1).map((o) => o.word));
47
49
  const names = Object.keys(CICS_COMMANDS).filter((n) => n === verb || n.startsWith(`${verb} `));
48
- return names.find((n) => CICS_COMMANDS[n].identify?.some((w) => words.has(w)))
50
+ const spelled = names.map((n) => n.split(' ')).filter((w) => w.length > 1 && w.slice(1).every((x, i) => options[i + 1]?.word === x))
51
+ .sort((a, b) => b.length - a.length)[0];
52
+ return spelled?.join(' ') || names.find((n) => CICS_COMMANDS[n].identify?.some((w) => words.has(w)))
49
53
  || names.find((n) => n === verb) || names.find((n) => CICS_COMMANDS[n].default) || null;
50
54
  }
51
55
 
package/lib/reach.mjs CHANGED
@@ -14,11 +14,15 @@
14
14
  // lib/exploitability.mjs joins them with the route into a verdict.
15
15
  import { readFileSync, realpathSync } from 'node:fs';
16
16
  import { basename, sep, delimiter } from 'node:path';
17
+ import { versionProblem, unknownKeys, ignoredKey } from './kernel/version-key.mjs';
17
18
 
18
19
  const str = (v) => typeof v === 'string' && v.length > 0;
19
20
  const isoDate = (v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v);
20
21
  const up = (s) => String(s || '').toUpperCase();
21
22
  const ACCESS = new Set(['open', 'restricted']);
23
+ export const REACH_VERSION = 1;
24
+ const REACH_KEYS = new Set(['$schema', 'version', 'extract', 'retrieved', 'transactions', 'jobs', 'privileged']);
25
+ const PRIVILEGED_KEYS = new Set(['transactions', 'jobs']);
22
26
  const arr = (v) => (Array.isArray(v) ? v.filter(str).map(up) : []);
23
27
 
24
28
  // The feeds a scan was given: the caller's list, or COBOLWORK_REACH read when the scan runs.
@@ -34,7 +38,7 @@ export function reachFeedPaths(opts = {}) {
34
38
  // derived from a RACF unload (who may start each transaction, and which run with elevated authority)
35
39
  // and the SIT (which face a network).
36
40
  export function loadReachFeed(path, { root = null } = {}) {
37
- const feed = { file: basename(path), extract: null, retrieved: null, transactions: {}, jobs: {}, privileged: { transactions: [], jobs: [] }, refused: [], problem: null };
41
+ const feed = { file: basename(path), extract: null, retrieved: null, transactions: {}, jobs: {}, privileged: { transactions: [], jobs: [] }, refused: [], warnings: [], problem: null };
38
42
  let real;
39
43
  try { real = realpathSync(path); } catch (e) { feed.problem = `could not be read (${e.code || e.name})`; return feed; }
40
44
  if (root) {
@@ -43,6 +47,9 @@ export function loadReachFeed(path, { root = null } = {}) {
43
47
  }
44
48
  let doc;
45
49
  try { doc = JSON.parse(readFileSync(real, 'utf8')); } catch (e) { feed.problem = `is not JSON (${e.message})`; return feed; }
50
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc)) { feed.problem = 'is not a JSON object'; return feed; }
51
+ feed.problem = versionProblem(doc, REACH_VERSION);
52
+ if (feed.problem) return feed;
46
53
  if (!str(doc.extract)) { feed.problem = 'does not say which extract it is'; return feed; }
47
54
  if (!isoDate(doc.retrieved)) { feed.problem = 'does not say when it was retrieved'; return feed; }
48
55
  feed.extract = doc.extract;
@@ -56,7 +63,9 @@ export function loadReachFeed(path, { root = null } = {}) {
56
63
  }
57
64
  }
58
65
  const pv = doc.privileged;
59
- if (pv && typeof pv === 'object' && !Array.isArray(pv)) { feed.privileged.transactions = arr(pv.transactions); feed.privileged.jobs = arr(pv.jobs); }
66
+ const held = pv && typeof pv === 'object' && !Array.isArray(pv);
67
+ if (held) { feed.privileged.transactions = arr(pv.transactions); feed.privileged.jobs = arr(pv.jobs); }
68
+ feed.warnings = [...unknownKeys(doc, REACH_KEYS).map((k) => ignoredKey(feed.file, k)), ...(held ? unknownKeys(pv, PRIVILEGED_KEYS) : []).map((k) => ignoredKey(`${feed.file} privileged`, k))];
60
69
  return feed;
61
70
  }
62
71
 
package/lib/revision.json CHANGED
@@ -1 +1 @@
1
- {"commit":"2144d560676dda84c8ae6546dfb7b858399f0b7d"}
1
+ {"commit":"4d7151cd03c0ac0850eec10a2529b07f0fc04aa1","tag":"v0.7.0"}