@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
@@ -6,16 +6,21 @@
6
6
  //
7
7
  // It also answers the question the COBOL side cannot: a CALL 'SUBRTN' with no COBOL program of that
8
8
  // name may be an assembler module, and a CSECT or ENTRY of that name here is where it is.
9
- import { inScope, isAssembler, isProgram, relPath } from '../sources.mjs';
9
+ import { basename, extname } from 'node:path';
10
+ import { inScope, isAssembler, isJcl, isProgram, relPath, HLASM_EXT, COPY_EXT } from '../sources.mjs';
11
+ import { parseJcl } from '../jcl.mjs';
12
+ import { optableIn, processOptions } from '../hlasm/optable.mjs';
10
13
  import { report } from '../kernel/ruleset.mjs';
11
14
  import { treeFor, noteUnread } from '../kernel/source-tree.mjs';
12
15
  import { eachWithinMemory } from '../kernel/memory.mjs';
13
- import { readHlasm, OPERATIONS } from '../hlasm.mjs';
16
+ import { readHlasm, OPERATIONS, isDocument } from '../hlasm.mjs';
17
+ import { readHlasmStatements, parseHlasmStatement, NAMED_MACROS } from '../hlasm/read.mjs';
18
+ import { macroOperands, splitOperands } from '../hlasm/operands.mjs';
14
19
 
15
20
  export const HLASM_RULES = {
16
21
  'hlasm-supervisor-state-change': {
17
22
  sev: 'crit', evidence: 'construct', cwe: 'CWE-250',
18
- text: 'Assembler source switches to PSW key zero or supervisor state',
23
+ text: 'Assembler source switches to PSW key zero, another system key, or supervisor state',
19
24
  impact: 'The code after the switch runs with the operating system\'s authority: it can read and change any storage, so a flaw in it is a flaw in the system rather than in one job',
20
25
  remedy: 'Keep the authorized code to the instructions that need it, return to the caller\'s key and problem state straight after (MODESET KEY=NZERO, MODE=PROB), and APF-authorize the module only where it has to be',
21
26
  },
@@ -31,6 +36,12 @@ export const HLASM_RULES = {
31
36
  impact: 'A program call, a program transfer or a change of secondary address space moves execution or data access across address spaces, under the authority the linkage tables grant rather than the caller\'s own',
32
37
  remedy: 'Confirm the entry tables and authorization indexes grant only the address spaces and keys the service needs, and that every PC routine validates what its caller passes',
33
38
  },
39
+ 'hlasm-runtime-module-name': {
40
+ sev: 'med', evidence: 'construct', cwe: 'CWE-470',
41
+ text: 'Assembler source loads or calls a module whose name it reads from storage at run time',
42
+ impact: 'EPLOC= and DE= give LINK, XCTL, LOAD and ATTACH the address of the name rather than the name, so whatever writes that storage chooses the program that runs, with the caller\'s authority',
43
+ remedy: 'Name the module with EP= where it is fixed; where it must vary, check the name against a list of the modules the program may run before the call',
44
+ },
34
45
  'hlasm-calls-security-product': { sev: 'info', evidence: 'context', cwe: null, text: 'Assembler source calls the security product directly' },
35
46
  'hlasm-provides-called-module': { sev: 'info', evidence: 'context', cwe: null, text: 'An assembler CSECT or ENTRY is a module a COBOL program in this tree CALLs' },
36
47
  };
@@ -45,8 +56,17 @@ const STATE = { KEY: 'PSW key zero', EXTKEY: 'PSW key zero', MODE: 'supervisor s
45
56
 
46
57
  const where = (use) => (use.inMacro ? ' It is in a macro definition, so it is in every program that uses the macro.' : '');
47
58
 
48
- function detail(use) {
49
- const written = `${use.name}${use.field ? ` ${use.field}` : ''}`;
59
+ // EX and EXRL name their target instruction; when it is labelled in the same file, the finding says
60
+ // which instruction runs with the register's length or mask.
61
+ function executed(use, labels) {
62
+ if (use.class !== 'built-instruction') return '';
63
+ const target = (splitOperands(use.field || '')[1] || '').trim().toUpperCase();
64
+ const t = /^[A-Z$#@_][A-Z0-9$#@_]*$/.test(target) ? labels.get(target) : null;
65
+ return t ? ` runs ${t.operation}${t.field ? ` ${t.field}` : ''}` : '';
66
+ }
67
+
68
+ function detail(use, labels) {
69
+ const written = `${use.name}${use.field ? ` ${use.field}` : ''}${executed(use, labels)}`;
50
70
  if (use.class === 'security-product') {
51
71
  const request = /\bREQUEST=([A-Z]+)/i.exec(use.field || '');
52
72
  return `${request ? `${use.name} REQUEST=${request[1].toUpperCase()}` : use.name} calls the security product from assembler rather than through a service that checks on the program's behalf.${where(use)}`;
@@ -67,31 +87,122 @@ function cobolCalls(source) {
67
87
  return calls;
68
88
  }
69
89
 
90
+ // The repository's own definitions of macros the reader knows by name (IF, LINK, SAVE, ...): a
91
+ // member so named whose first statement is MACRO. Only those names are read, so the cost is a few
92
+ // files at most.
93
+ function repositoryMacros(tree, files) {
94
+ const found = new Set();
95
+ for (const f of files) {
96
+ const name = basename(f, extname(f)).toUpperCase();
97
+ if (!NAMED_MACROS.has(name) || found.has(name)) continue;
98
+ let text;
99
+ try { text = tree.text(f).text; } catch { continue; }
100
+ const first = readHlasmStatements(text).statements.find((s) => s.kind === 'statement');
101
+ if (first?.operation === 'MACRO') found.add(name);
102
+ }
103
+ return found;
104
+ }
105
+
106
+ // The OPTABLE the estate's assembly steps give in PARM (ASMA90, the old IEV90, or a procedure such as
107
+ // ASMACL), with where each was written. One level is the estate's; several leave each file to its own.
108
+ function jclOptables(root, tree, files) {
109
+ const levels = new Map();
110
+ for (const f of files) {
111
+ let job;
112
+ try { job = parseJcl(tree.text(f).text, f); } catch { continue; }
113
+ for (const step of job.steps) {
114
+ if (!/^(?:ASMA90|IEV90)$/i.test(step.pgm || '') && !/^(?:ASMA|HLASM|IEV)/i.test(step.proc || '')) continue;
115
+ for (const [k, v] of step.keywords) {
116
+ const found = /^PARM(?:\.|$)/.test(k) ? optableIn(v) : null;
117
+ if (found && !levels.has(found.level)) levels.set(found.level, `${relPath(root, f)}:${step.line}`);
118
+ }
119
+ }
120
+ }
121
+ return levels;
122
+ }
123
+
70
124
  export function scanHlasm(root, opts = {}) {
71
125
  const tree = treeFor(root, opts);
72
126
  const all = tree.list().filter(inScope(opts));
73
127
  const findings = [];
74
128
  const stats = { filesScanned: 0, filesUnreadable: 0, assemblerFiles: 0, bmsFiles: 0, imsFiles: 0, unrecognisedFiles: 0, readNotAssembled: true };
129
+ const members = new Set(all.filter((f) => [...HLASM_EXT, ...COPY_EXT].some((e) => f.toLowerCase().endsWith(e))).map((f) => basename(f, extname(f)).toUpperCase()));
130
+ const libraryMacros = repositoryMacros(tree, all.filter(isAssembler));
131
+ const forms = { mvs38: opts.mvs38Forms !== false };
132
+ const fromJcl = opts.hlasmOptable ? new Map() : jclOptables(root, tree, all.filter(isJcl));
133
+ const estateOptable = opts.hlasmOptable ?? (fromJcl.size === 1 ? [...fromJcl.keys()][0] : null);
134
+ if (opts.hlasmOptable) stats.hlasmOptable = { level: opts.hlasmOptable, from: 'option' };
135
+ else if (fromJcl.size === 1) stats.hlasmOptable = { level: estateOptable, from: [...fromJcl.values()][0] };
136
+ else if (fromJcl.size > 1) stats.hlasmOptableConflict = Object.fromEntries(fromJcl);
137
+ // The table a file is assembled with: the option given to the scan, then a *PROCESS OVERRIDE in
138
+ // the file, the estate's PARM, and the file's own *PROCESS, as HLASM ranks them.
139
+ const optableOf = (src) => {
140
+ if (opts.hlasmOptable) return opts.hlasmOptable;
141
+ const own = optableIn(processOptions(src));
142
+ return own?.override ? own.level : estateOptable ?? own?.level ?? null;
143
+ };
144
+ const refused = {};
145
+ const macroCalls = new Map();
146
+ const copyNotFound = new Set();
147
+ // Reads a file's statements: its labels, what the reader refused, and the macros and COPY members it
148
+ // uses, which count toward the coverage report unless the file is a document.
149
+ function readStatements(src, path) {
150
+ const labels = new Map();
151
+ const read = readHlasmStatements(src, { libraryMacros }).statements;
152
+ const statements = read.filter((st) => !st.inMacro);
153
+ const fileForms = { ...forms, optable: optableOf(src) };
154
+ const results = statements.map((st) => parseHlasmStatement(st, fileForms));
155
+ const refusals = results.filter((res) => res.status === 'unparsed' || res.status === 'unknown').length;
156
+ if (isDocument(read.filter((st) => st.kind === 'statement'), statements.length, refusals)) return null;
157
+ statements.forEach((st, i) => {
158
+ const res = results[i];
159
+ if (st.kind === 'statement' && st.name && !st.name.startsWith('&') && !st.name.startsWith('.')) labels.set(st.name.toUpperCase(), st);
160
+ if (res.node?.mvs38) stats.mvs38Forms = (stats.mvs38Forms || 0) + 1;
161
+ if (res.status === 'unparsed' || res.status === 'unknown') (refused[res.kind] ||= { count: 0, first: `${path}:${st.line}` }).count++;
162
+ const name = res.kind === 'MACRO CALL' ? st.operation : NAMED_MACROS.has(res.kind) ? res.kind : null;
163
+ if (name) {
164
+ const m = macroCalls.get(name) || { name, calls: 0, definedInTree: members.has(name) };
165
+ m.calls++;
166
+ macroCalls.set(name, m);
167
+ }
168
+ if (res.kind === 'COPY') {
169
+ const member = String(res.status === 'parsed' ? res.node.member : (/^\s*([A-Z$#@][A-Z0-9$#@_]*)/i.exec(st.field || '') || [])[1] || '').toUpperCase();
170
+ if (member && !members.has(member)) copyNotFound.add(member);
171
+ }
172
+ });
173
+ return labels;
174
+ }
75
175
  const defines = new Map();
76
176
 
77
177
  const run = eachWithinMemory(all.filter(isAssembler), (f) => {
78
178
  let src;
79
179
  try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
80
180
  stats.filesScanned++;
81
- const r = readHlasm(src);
181
+ const r = readHlasm(src, forms);
82
182
  if (r.kind === 'bms') { stats.bmsFiles++; return src.length; }
83
183
  if (r.kind === 'ims') { stats.imsFiles++; return src.length; }
84
184
  if (r.kind === 'unrecognised') { stats.unrecognisedFiles++; return src.length; }
85
- stats.assemblerFiles++;
185
+ if (r.kind === 'listing') { stats.listingFiles = (stats.listingFiles || 0) + 1; return src.length; }
86
186
  const path = relPath(root, f);
187
+ const labels = readStatements(src, path);
188
+ if (!labels) { stats.documentFiles = (stats.documentFiles || 0) + 1; return src.length; }
189
+ stats.assemblerFiles++;
87
190
  for (const d of r.defines) if (!defines.has(d.name)) defines.set(d.name, { ...d, path });
88
191
  for (const use of r.operations) {
192
+ if (use.class === 'module-load') {
193
+ const { keywords } = macroOperands(use.field || '');
194
+ const at = keywords.has('EPLOC') ? 'EPLOC' : keywords.has('DE') ? 'DE' : null;
195
+ if (at) findings.push({ rule: 'hlasm-runtime-module-name', path, line: use.line, detail: `${use.name} ${at}=${keywords.get(at)} names the module by the address of its name, so the program that runs is whatever name that storage holds.${where(use)}` });
196
+ continue;
197
+ }
89
198
  if (use.name === 'MODESET') {
90
199
  if (use.stateChange) findings.push({ rule: 'hlasm-supervisor-state-change', path, line: use.line, detail: `MODESET ${use.stateChange} switches to ${STATE[use.stateChange.split('=')[0]]}.${where(use)}` });
200
+ // A system key writes storage problem-state keys cannot, short of key zero's reach.
201
+ else if (use.systemKey) findings.push({ rule: 'hlasm-supervisor-state-change', sev: 'high', path, line: use.line, detail: `MODESET ${use.systemKey.written} switches to PSW key ${use.systemKey.key}, a system key.${where(use)}` });
91
202
  continue;
92
203
  }
93
204
  const rule = RULE_OF_CLASS[use.class];
94
- if (rule) findings.push({ rule, path, line: use.line, detail: detail(use) });
205
+ if (rule) findings.push({ rule, path, line: use.line, detail: detail(use, labels) });
95
206
  }
96
207
  return src.length;
97
208
  }, { label: 'hlasm', maxBytes: opts.maxSourceBytes ?? Infinity });
@@ -117,5 +228,10 @@ export function scanHlasm(root, opts = {}) {
117
228
  }
118
229
  }
119
230
 
231
+ // Each statement the reader refused, each macro whose expansion was not read and each COPY member
232
+ // the tree does not hold is named: an operation inside any of them is one this set did not see.
233
+ if (Object.keys(refused).length) { stats.statementsNotRead = refused; stats.coverageIncomplete = true; }
234
+ if (copyNotFound.size) { stats.copyNotFound = [...copyNotFound].sort(); stats.coverageIncomplete = true; }
235
+ if (macroCalls.size) stats.macrosNotExpanded = [...macroCalls.values()].sort((a, b) => b.calls - a.calls || a.name.localeCompare(b.name)).slice(0, 50);
120
236
  return report('hlasm', { rules: HLASM_RULES, findings, stats, run });
121
237
  }
@@ -0,0 +1,139 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // IMS database and program definitions: what a PSB lets a program change, SENSEGs that name segments
3
+ // their DBD does not define, and what DBDGEN, PSBGEN or ACBGEN refuses (lib/ims/rules.mjs). DBDs and
4
+ // PSBs are assembler macros and are often named .asm, so assembler files are read too, and kept only
5
+ // where they hold a DBD or PSB.
6
+ import { inScope, isAssembler, isIms, isJcl, isProgram, relPath } from '../sources.mjs';
7
+ import { report } from '../kernel/ruleset.mjs';
8
+ import { treeFor, noteUnread, noteUnparsed } from '../kernel/source-tree.mjs';
9
+ import { eachWithinMemory } from '../kernel/memory.mjs';
10
+ import { modelOf } from '../ims/model.mjs';
11
+ import { IMS_RULES, checkModels, checkProcoptAgainstCalls } from '../ims/rules.mjs';
12
+ import { DLI_ROUTINE, DLI_FUNCTIONS, DLI_SERVICES, psbOfParm } from '../ims/dli.mjs';
13
+ import { parseJcl } from '../jcl.mjs';
14
+ import { constantsReaching } from '../constants.mjs';
15
+ import { execReading } from '../exec-reading.mjs';
16
+
17
+ export { IMS_RULES };
18
+
19
+ export function scanIms(root, opts = {}) {
20
+ const tree = treeFor(root, opts);
21
+ const files = tree.list().filter(inScope(opts)).filter((f) => isIms(f) || isAssembler(f));
22
+ const stats = { filesScanned: 0, filesUnreadable: 0, dbdFiles: 0, psbFiles: 0 };
23
+ const models = [];
24
+ const unread = {};
25
+ const run = eachWithinMemory(files, (f) => {
26
+ let src;
27
+ try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
28
+ const model = modelOf(src);
29
+ if (!model) return src.length;
30
+ stats.filesScanned++;
31
+ stats[model.kind === 'DBD' ? 'dbdFiles' : 'psbFiles']++;
32
+ const path = relPath(root, f);
33
+ for (const u of model.unread) (unread[u.operation || 'UNREADABLE'] ||= { count: 0, first: `${path}:${u.line}` }).count++;
34
+ models.push({ path, model });
35
+ return src.length;
36
+ }, { label: 'ims', maxBytes: opts.maxSourceBytes ?? Infinity });
37
+
38
+ // A statement the reader refused may hold a segment, a field or a PCB the rules never saw.
39
+ if (Object.keys(unread).length) { stats.statementsNotRead = unread; stats.coverageIncomplete = true; }
40
+
41
+ // Programs are read only when a PSB could be named for one, and only those that issue DL/I.
42
+ const programs = [];
43
+ const links = [];
44
+ let calls = null;
45
+ if (models.some(({ model }) => model.kind === 'PSB')) {
46
+ const sources = tree.list().filter(inScope(opts)).filter((f) => isProgram(f) || isJcl(f));
47
+ calls = eachWithinMemory(sources, (f) => {
48
+ let src;
49
+ try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
50
+ const path = relPath(root, f);
51
+ if (isJcl(f)) {
52
+ if (!/DFSRRC00/i.test(src)) return src.length;
53
+ let jcl;
54
+ try { jcl = parseJcl(src); } catch { return src.length; }
55
+ for (const step of jcl.steps || []) {
56
+ const l = String(step.pgm || '').toUpperCase() === 'DFSRRC00' ? psbOfParm(step.parm) : null;
57
+ if (l) links.push({ ...l, at: `${path}:${step.line}` });
58
+ }
59
+ return src.length;
60
+ }
61
+ if (!/CBLTDLI|AIBTDLI|EXEC\s+DLI/i.test(src)) return src.length;
62
+ let r;
63
+ try { r = tree.parse(f, src); } catch (e) { noteUnparsed(stats, tree, f, e); return src.length; }
64
+ for (const prog of r.programs) {
65
+ const read = dliCallsOf(prog);
66
+ const id = String(prog.id).toUpperCase();
67
+ programs.push({ id, path, line: prog.line || 1, functions: read.functions });
68
+ for (const psb of read.schedules) links.push({ program: id, psb, at: `${path}, its own scheduling call` });
69
+ }
70
+ return src.length;
71
+ }, { label: 'ims programs', maxBytes: opts.maxSourceBytes ?? Infinity });
72
+ stats.programsWithDli = programs.length;
73
+ }
74
+ const findings = [...checkModels(models), ...checkProcoptAgainstCalls(models, programs, links)];
75
+ return report('ims', { rules: IMS_RULES, findings, stats, run: calls ? bothRuns(run, calls) : run });
76
+ }
77
+
78
+ // The DL/I database functions a program issues itself, null when they cannot all be told: a function
79
+ // code no constant decides, a CALL that passes on a PCB or a LINKAGE item, which may hold one, or a
80
+ // CICS LINK or XCTL from a program that schedules a PSB, whose target can issue DL/I against it. And
81
+ // the PSBs it schedules itself, by EXEC DLI SCHD or a PCB call, where a constant names one.
82
+ function dliCallsOf(prog) {
83
+ let functions = new Set();
84
+ const schedules = new Set();
85
+ const ignore = { ignoreCall: (x) => DLI_ROUTINE.test(String(x.name)) };
86
+ const valuesOf = (a) => {
87
+ if (!a) return { values: [], open: true };
88
+ if (a.lit != null) return { values: [String(a.lit)], open: false };
89
+ return a.word ? constantsReaching(prog, a.word, ignore) : { values: [], open: true };
90
+ };
91
+ const named = (k) => (k.open ? null : k.values.map((v) => String(v).trim().toUpperCase()));
92
+ const known = (v) => Object.hasOwn(DLI_FUNCTIONS, v) || DLI_SERVICES.has(v);
93
+ const byName = new Map((prog.items || []).map((it) => [it.name, it]));
94
+ const topOf = (it) => { while (it && it.parent) it = it.parent; return it; };
95
+ // The function code is the first of the two arguments whose constants name one; the PCB or AIB follows it.
96
+ const functionAt = (using) => [0, 1].find((i) => valuesOf(using[i]).values.some((v) => known(String(v).trim().toUpperCase())));
97
+ const pcbs = new Set();
98
+ for (const c of prog.calls || []) {
99
+ if (c.kind !== 'L' || !DLI_ROUTINE.test(String(c.name))) continue;
100
+ const at = functionAt(c.using || []);
101
+ const pcb = at === undefined ? null : (c.using || [])[at + 1];
102
+ if (pcb && pcb.word) pcbs.add(pcb.word);
103
+ }
104
+ const passesOn = (a) => a.word && (pcbs.has(a.word) || (topOf(byName.get(a.word)) || {}).section === 'LINKAGE');
105
+ for (const c of prog.calls || []) {
106
+ if (c.kind !== 'L') { functions = null; continue; }
107
+ if (!DLI_ROUTINE.test(String(c.name))) {
108
+ if ((c.using || []).some(passesOn)) functions = null;
109
+ continue;
110
+ }
111
+ const using = c.using || [];
112
+ const at = functionAt(using);
113
+ const codes = at === undefined ? null : named(valuesOf(using[at]));
114
+ if (!codes || !codes.every(known)) { functions = null; continue; }
115
+ if (codes.includes('PCB')) for (const n of named(valuesOf(using[at + 1])) || []) schedules.add(n);
116
+ if (functions) for (const fn of codes) if (Object.hasOwn(DLI_FUNCTIONS, fn)) functions.add(fn);
117
+ }
118
+ for (const e of prog.execs || []) {
119
+ if (e.kind !== 'DLI') continue;
120
+ const { verb, opts } = execReading(e);
121
+ if (verb === 'SCHD') {
122
+ const tok = (opts.get('PSB') || []).find((t) => t.t === 'word' || t.t === 'lit');
123
+ const k = !tok ? { values: [], open: true } : tok.t === 'lit' ? { values: [tok.v], open: false } : constantsReaching(prog, tok.v, ignore);
124
+ for (const n of named(k) || []) schedules.add(n);
125
+ } else if (functions && Object.hasOwn(DLI_FUNCTIONS, verb)) functions.add(verb);
126
+ }
127
+ const transfers = (prog.execs || []).some((e) => e.kind === 'CICS' && ['LINK', 'XCTL'].includes(execReading(e).verb));
128
+ if (schedules.size && transfers) functions = null;
129
+ return { functions: functions && functions.size ? functions : null, schedules: [...schedules] };
130
+ }
131
+
132
+ function bothRuns(a, b) {
133
+ return {
134
+ skipped: [...a.skipped, ...b.skipped],
135
+ stoppedBy: a.stoppedBy || b.stoppedBy,
136
+ peakHeapBytes: Math.max(a.peakHeapBytes || 0, b.peakHeapBytes || 0) || undefined,
137
+ note: [a.note, b.note].filter(Boolean).join(' ') || undefined,
138
+ };
139
+ }
package/lib/sets/log.mjs CHANGED
@@ -23,7 +23,8 @@ import { parseJcl } from '../jcl.mjs';
23
23
  import { compileStepOptions } from '../options.mjs';
24
24
  import { report } from '../kernel/ruleset.mjs';
25
25
  import { treeFor, noteUnread, noteUnparsed } from '../kernel/source-tree.mjs';
26
- import { eachWithinMemory } from '../kernel/memory.mjs';
26
+ import { drive, loopOver } from '../kernel/shared-pass.mjs';
27
+ import { execReading } from '../exec-reading.mjs';
27
28
 
28
29
 
29
30
  export const LOG_RULES = {
@@ -107,17 +108,16 @@ function logSinks(p, where, path) {
107
108
  }
108
109
  for (const e of p.execs || []) {
109
110
  if (e.kind !== 'CICS') continue;
110
- const w = e.toks.filter((t) => t.t === 'word').map((t) => t.u);
111
- const isTd = w[0] === 'WRITEQ' && w[1] === 'TD';
112
- const isJournal = w[0] === 'WRITE' && w.includes('JOURNALNAME');
111
+ const { verb, sub, words, opts } = execReading(e);
112
+ const isTd = verb === 'WRITEQ' && sub === 'TD';
113
+ const isJournal = verb === 'WRITE' && words.includes('JOURNALNAME');
113
114
  // The console, read by whoever is watching the region. A credential on it is as exposed as one
114
115
  // in SYSOUT, and it outlives the screen in the system log.
115
- const isOperator = w[0] === 'WRITE' && w[1] === 'OPERATOR';
116
+ const isOperator = verb === 'WRITE' && sub === 'OPERATOR';
116
117
  if (!isTd && !isJournal && !isOperator) continue;
117
118
  // FROM names the area written, TEXT names the console message; that name is what this reads.
118
119
  const opt = isOperator ? 'TEXT' : 'FROM';
119
- const i = e.toks.findIndex((t) => t.t === 'word' && t.u === opt);
120
- const arg = i >= 0 ? e.toks.slice(i + 1).find((t) => t.t === 'word') : null;
120
+ const arg = (opts.get(opt) || []).find((t) => t.t === 'word') || null;
121
121
  const kind = isTd ? 'WRITEQ TD' : isOperator ? 'WRITE OPERATOR' : 'WRITE JOURNALNAME';
122
122
  if (arg) out.push({ kind, name: arg.u, ...where(e, path) });
123
123
  }
@@ -337,7 +337,7 @@ const memberOf = (file) => basename(String(file || ''), extname(String(file || '
337
337
  const runByJob = (batch, p, file) => batch.unsure
338
338
  || [String(p.id || '').toUpperCase(), memberOf(file)].some((n) => n && (batch.runs.has(n) || batch.runs.has(n.slice(0, 8))));
339
339
 
340
- export function scanLog(root, opts = {}) {
340
+ export function* scanLogSteps(root, opts = {}) {
341
341
  const tree = treeFor(root, opts);
342
342
  const files = tree.list().filter(isProgram).filter(inScope(opts));
343
343
  const findings = [];
@@ -364,7 +364,7 @@ export function scanLog(root, opts = {}) {
364
364
  return { rule: 'display-echoes-a-credential', detail: `${p.id} shows ${w.name} on the terminal to the person who typed it: not a log, but the screen and its scrollback keep it` };
365
365
  };
366
366
 
367
- const run = eachWithinMemory(files, (f) => {
367
+ const run = yield loopOver(files, (f) => {
368
368
  let src;
369
369
  try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
370
370
  // Every EXEC CICS WRITE form, not the two this set started with. WRITE OPERATOR was added as a
@@ -404,3 +404,5 @@ export function scanLog(root, opts = {}) {
404
404
 
405
405
  return report('log', { rules: LOG_RULES, findings, stats, run });
406
406
  }
407
+
408
+ export const scanLog = (root, opts = {}) => drive(scanLogSteps(root, opts));
@@ -74,21 +74,41 @@ export function scanOpaque(root, opts = {}) {
74
74
  const path = relPath(root, f);
75
75
 
76
76
  // Which names were declared as pointers, so a CALL through one can be told from an ordinary
77
- // CALL of a variable - which the flow rules already report as a dynamic program load.
78
- const pointers = new Set();
79
- for (const m of text.matchAll(/\b(?:\d\d\s+)?([A-Z0-9-]+)\b[^.\n]{0,80}\bUSAGE\s+(?:IS\s+)?(?:PROCEDURE-POINTER|POINTER)\b/gi)) {
80
- pointers.add(m[1].toUpperCase());
81
- }
77
+ // CALL of a variable - which the flow rules already report as a dynamic program load. Found
78
+ // only once a CALL needs it, since the search tries every word of the program.
79
+ let pointers = null;
80
+ const pointerNames = () => {
81
+ if (pointers) return pointers;
82
+ pointers = new Set();
83
+ for (const m of text.matchAll(/\b(?:\d\d\s+)?([A-Z0-9-]+)\b[^.\n]{0,80}\bUSAGE\s+(?:IS\s+)?(?:PROCEDURE-POINTER|POINTER)\b/gi)) {
84
+ pointers.add(m[1].toUpperCase());
85
+ }
86
+ return pointers;
87
+ };
88
+
89
+ // How many newlines of the joined text come before an offset, which is the normalised entry
90
+ // holding it.
91
+ const starts = [0];
92
+ for (let i = text.indexOf('\n'); i >= 0; i = text.indexOf('\n', i + 1)) starts.push(i + 1);
93
+ const entryAt = (offset) => {
94
+ let lo = 0;
95
+ let hi = starts.length - 1;
96
+ while (lo < hi) {
97
+ const mid = (lo + hi + 1) >> 1;
98
+ if (starts[mid] <= offset) lo = mid; else hi = mid - 1;
99
+ }
100
+ return lo;
101
+ };
82
102
 
83
103
  let hit = false;
84
104
  for (const p of PATTERNS) {
85
105
  p.re.lastIndex = 0;
86
106
  let m;
87
107
  while ((m = p.re.exec(text)) !== null) {
88
- if (p.needsPointerOperand && !pointers.has(String(m[1]).toUpperCase())) continue;
108
+ if (p.needsPointerOperand && !pointerNames().has(String(m[1]).toUpperCase())) continue;
89
109
  // The line number is the normalised entry's own, so a construct that came from a copybook
90
110
  // is reported where the compiler would see it.
91
- const before = text.slice(0, m.index).split('\n').length - 1;
111
+ const before = entryAt(m.index);
92
112
  const line = norm.entries[before] ? norm.entries[before].line : 1;
93
113
  findings.push({ rule: p.rule, path, line, detail: p.detail(m) });
94
114
  hit = true;
@@ -0,0 +1,40 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // PL/I programs read statement by statement (lib/pli): where they address storage through a pointer,
3
+ // call through an entry variable or use the preprocessor, which a static reading cannot follow, and
4
+ // ON units that swallow their condition. Data flow through PL/I is the flow set's; this set reports
5
+ // what the program text itself shows, and counts every statement the reader could not parse.
6
+ import { inScope, isPli, relPath } from '../sources.mjs';
7
+ import { report } from '../kernel/ruleset.mjs';
8
+ import { treeFor, noteUnread } from '../kernel/source-tree.mjs';
9
+ import { eachWithinMemory } from '../kernel/memory.mjs';
10
+ import { readPli } from '../pli/lex.mjs';
11
+ import { parseStatement } from '../pli/statements.mjs';
12
+ import { PLI_RULES, checkProgram } from '../pli/rules/index.mjs';
13
+
14
+ export { PLI_RULES };
15
+
16
+ export function scanPli(root, opts = {}) {
17
+ const tree = treeFor(root, opts);
18
+ const files = tree.list().filter(inScope(opts)).filter(isPli);
19
+ const stats = { filesScanned: 0, filesUnreadable: 0, statements: 0, statementsParsed: 0 };
20
+ const notParsed = {};
21
+ const findings = [];
22
+ const run = eachWithinMemory(files, (f) => {
23
+ let src;
24
+ try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
25
+ const path = relPath(root, f);
26
+ const statements = readPli(src, { file: path }).statements.map((st) => ({ ...st, parsed: parseStatement(st) }));
27
+ stats.filesScanned++;
28
+ for (const st of statements) {
29
+ stats.statements++;
30
+ if (st.parsed.status === 'parsed') { stats.statementsParsed++; continue; }
31
+ (notParsed[st.parsed.kind || 'UNKNOWN'] ||= { count: 0, first: `${path}:${st.line}` }).count++;
32
+ }
33
+ findings.push(...checkProgram({ path, statements }));
34
+ return src.length;
35
+ }, { label: 'pli', maxBytes: opts.maxSourceBytes ?? Infinity });
36
+
37
+ // A statement the reader could not parse may hold a pointer, an entry call or an ON unit.
38
+ if (Object.keys(notParsed).length) { stats.statementsNotParsed = notParsed; stats.coverageIncomplete = true; }
39
+ return report('pli', { rules: PLI_RULES, findings, stats, run });
40
+ }
package/lib/sets/priv.mjs CHANGED
@@ -19,7 +19,7 @@
19
19
  import { inScope, isProgram, relPath } from '../sources.mjs';
20
20
  import { report } from '../kernel/ruleset.mjs';
21
21
  import { treeFor, noteUnread, noteUnparsed } from '../kernel/source-tree.mjs';
22
- import { eachWithinMemory } from '../kernel/memory.mjs';
22
+ import { drive, loopOver } from '../kernel/shared-pass.mjs';
23
23
  import { parseCsd } from '../csd.mjs';
24
24
  import { parseJcl } from '../jcl.mjs';
25
25
  import { loadSite } from '../site.mjs';
@@ -101,7 +101,7 @@ function under(dsn, prefixes) {
101
101
  });
102
102
  }
103
103
 
104
- export function scanPriv(root, opts = {}) {
104
+ export function* scanPrivSteps(root, opts = {}) {
105
105
  const site = loadSite(root, opts.site || null, opts.tree);
106
106
  const tree = treeFor(root, opts);
107
107
  const files = tree.list().filter(inScope(opts));
@@ -140,7 +140,7 @@ export function scanPriv(root, opts = {}) {
140
140
  const diagnostics = [];
141
141
  const uss = [];
142
142
 
143
- const run = eachWithinMemory(files, (f) => {
143
+ const run = yield loopOver(files, (f) => {
144
144
  let src;
145
145
  try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
146
146
  const path = relPath(root, f);
@@ -320,3 +320,5 @@ export function scanPriv(root, opts = {}) {
320
320
 
321
321
  return report('priv', { rules: PRIV_RULES, findings, stats, run });
322
322
  }
323
+
324
+ export const scanPriv = (root, opts = {}) => drive(scanPrivSteps(root, opts));
@@ -14,7 +14,7 @@
14
14
  import { inScope, isJcl, isProgram, isCopybook, relPath } from '../sources.mjs';
15
15
  import { report } from '../kernel/ruleset.mjs';
16
16
  import { treeFor, noteUnread, noteUnparsed } from '../kernel/source-tree.mjs';
17
- import { eachWithinMemory } from '../kernel/memory.mjs';
17
+ import { drive, loopOver } from '../kernel/shared-pass.mjs';
18
18
  import { loadSite, classifyPath, productionQualifierOf, SITE_FILE } from '../site.mjs';
19
19
  import { parseJcl, dispositionOf } from '../jcl.mjs';
20
20
  import { statementCard } from '../cards.mjs';
@@ -161,7 +161,7 @@ function productionTouches(job, qualifiers) {
161
161
  return { found, claimed };
162
162
  }
163
163
 
164
- export function scanRecon(root, opts = {}) {
164
+ export function* scanReconSteps(root, opts = {}) {
165
165
  const site = loadSite(root, opts.site || null, opts.tree);
166
166
  const tree = treeFor(root, opts);
167
167
  const files = tree.list().filter(inScope(opts))
@@ -192,7 +192,7 @@ export function scanRecon(root, opts = {}) {
192
192
 
193
193
  // This set reads every program, copybook and job in the tree, which is the widest reading any
194
194
  // set does, so it is walked inside the guard like the rest.
195
- const run = eachWithinMemory(files, (f) => {
195
+ const run = yield loopOver(files, (f) => {
196
196
  let src;
197
197
  try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
198
198
  stats.filesScanned++;
@@ -267,3 +267,5 @@ export function scanRecon(root, opts = {}) {
267
267
  // production means, as against files going unread. Both travel, and they are not merged.
268
268
  return report('recon', { rules: RECON_RULES, findings, stats, run });
269
269
  }
270
+
271
+ export const scanRecon = (root, opts = {}) => drive(scanReconSteps(root, opts));