@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
@@ -13,7 +13,7 @@ import { basename, extname } from 'node:path';
13
13
  import { inScope, isProgram, relPath, readSource } from '../sources.mjs';
14
14
  import { report } from '../kernel/ruleset.mjs';
15
15
  import { treeFor, noteUnread, noteUnparsed } from '../kernel/source-tree.mjs';
16
- import { eachWithinMemory } from '../kernel/memory.mjs';
16
+ import { drive, loopOver } from '../kernel/shared-pass.mjs';
17
17
 
18
18
  // construct: no input has to reach it. med: certain once every copybook is found, and it means the
19
19
  // tree does not hold what runs, but it is not exploitable in itself. CWE-1127: MITRE discourages
@@ -59,11 +59,13 @@ const REPLACE_STATEMENT = /\bREPLACE\b([\s\S]*?)(?:\.\s|\.$)/gim;
59
59
 
60
60
  // A header declares a paragraph only where the sentence before it has ended: after a line of code
61
61
  // ending in a period, or with no code before it. Without the period the name is read as part of the
62
- // statement before, and the compiler reports it undefined.
63
- function sentenceEndsBefore(text, at) {
64
- const lines = text.slice(0, at).split(/\r?\n/);
65
- lines.pop();
66
- for (let i = lines.length - 1; i >= 0; i--) {
62
+ // statement before, and the compiler reports it undefined. `lines` and `starts` are the text's lines
63
+ // and where each begins.
64
+ function sentenceEndsBefore(lines, starts, at) {
65
+ let lo = 0;
66
+ let hi = starts.length - 1;
67
+ while (lo < hi) { const mid = (lo + hi + 1) >>> 1; if (starts[mid] <= at) lo = mid; else hi = mid - 1; }
68
+ for (let i = lo - 1; i >= 0; i--) {
67
69
  const l = lines[i];
68
70
  if (/^.{6}[*/]/.test(l) || /^\s*\*>/.test(l)) continue;
69
71
  const code = l.slice(0, 72).replace(/\*>.*$/, '').replace(/^[0-9 ]{6}(?=[ \-D])/, '').trim();
@@ -72,12 +74,17 @@ function sentenceEndsBefore(text, at) {
72
74
  return true;
73
75
  }
74
76
 
75
- function declaredIn(text, names, into) {
76
- const note = (w) => { const u = w.toUpperCase(); if (names.has(u)) into.add(u); };
77
+ // Every name a text declares, upper-cased, whatever is being looked for.
78
+ function declaredWords(text) {
79
+ const words = new Set();
80
+ const note = (w) => words.add(w.toUpperCase());
81
+ const lines = text.split(/\r?\n/);
82
+ const starts = [0];
83
+ for (const m of text.matchAll(/\r?\n/g)) starts.push(m.index + m[0].length);
77
84
  for (const re of [DECLARATION, HEADER]) {
78
85
  re.lastIndex = 0;
79
86
  let m;
80
- while ((m = re.exec(text))) if (re !== HEADER || sentenceEndsBefore(text, m.index)) note(m[1]);
87
+ while ((m = re.exec(text))) if (re !== HEADER || sentenceEndsBefore(lines, starts, m.index)) note(m[1]);
81
88
  }
82
89
  for (const re of [COPY_STATEMENT, REPLACE_STATEMENT]) {
83
90
  re.lastIndex = 0;
@@ -87,9 +94,11 @@ function declaredIn(text, names, into) {
87
94
  for (const w of m[1].match(/[A-Za-z0-9][A-Za-z0-9-]*/g) || []) note(w);
88
95
  }
89
96
  }
90
- return into;
97
+ return words;
91
98
  }
92
99
 
100
+ const declaredIn = (words, names, into) => { for (const u of names) if (words.has(u)) into.add(u); return into; };
101
+
93
102
  // A reference cut at column 72 names a word the source does not hold.
94
103
  function truncated(ref, lines) {
95
104
  const line = (lines.get(ref.file) || [])[ref.line - 1] || '';
@@ -102,7 +111,7 @@ function truncated(ref, lines) {
102
111
  return any && !whole;
103
112
  }
104
113
 
105
- export function scanCompile(root, opts = {}) {
114
+ export function* scanCompileSteps(root, opts = {}) {
106
115
  const tree = treeFor(root, opts);
107
116
  const files = tree.list().filter(isProgram).filter(inScope(opts));
108
117
  const findings = [];
@@ -114,10 +123,26 @@ export function scanCompile(root, opts = {}) {
114
123
  because[why] = (because[why] || 0) + 1;
115
124
  undecided.push(`${tree.rel(f)} (${id}): ${why}: ${[...new Set(refs.map((x) => x.name))].slice(0, SHOWN).join(', ')}`);
116
125
  };
117
- const textOf = (p) => (tree.contains(p) ? tree.text(p) : readSource(p)).text;
126
+ // A copybook's text, lines and declared names, read once for every program that copies it.
127
+ const remembered = (read) => {
128
+ const held = new Map();
129
+ return (p) => {
130
+ let h = held.get(p);
131
+ if (!h) {
132
+ try { const text = read(p); h = { text, lines: null, words: null }; } catch (e) { h = { error: e }; }
133
+ held.set(p, h);
134
+ }
135
+ if (h.error) throw h.error;
136
+ return h;
137
+ };
138
+ };
139
+ const copyOf = remembered((p) => (tree.contains(p) ? tree.text(p) : readSource(p)).text);
140
+ const answerOf = remembered((p) => tree.text(p).text);
141
+ const wordsOf = (h) => (h.words ||= declaredWords(h.text));
142
+ const linesOf = (h) => (h.lines ||= h.text.split(/\r?\n/));
118
143
  const candidates = [];
119
144
 
120
- const run = eachWithinMemory(files, (f) => {
145
+ const run = yield loopOver(files, (f) => {
121
146
  let src;
122
147
  try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
123
148
  if (LISTING.test(src)) { stats.filesScanned++; stats.listingsSkipped = (stats.listingsSkipped || 0) + 1; return src.length; }
@@ -126,6 +151,7 @@ export function scanCompile(root, opts = {}) {
126
151
  stats.filesScanned++;
127
152
  const blocked = whyUndecided(r, src);
128
153
  const copied = [...new Set(r.copies.filter((c) => c.path).map((c) => c.path))];
154
+ const own = { text: src, lines: null, words: null };
129
155
  for (const p of r.programs) {
130
156
  // A class or a method has no PROGRAM-ID, and the data its methods use belongs to no program here.
131
157
  if (!p.id || p.diags.some((d) => d.kind === 'no-procedure-division')) continue;
@@ -133,11 +159,11 @@ export function scanCompile(root, opts = {}) {
133
159
  if (!p.unresolvedRefs.length) continue;
134
160
  if (blocked) { undecide(f, p.id, blocked, p.unresolvedRefs); continue; }
135
161
  const names = new Set(p.unresolvedRefs.map((x) => x.name));
136
- const texts = new Map([[f, src]]);
137
- for (const q of copied) { try { texts.set(q, textOf(q)); } catch { /* the parse read it once; a second failure changes nothing */ } }
162
+ const texts = new Map([[f, own]]);
163
+ for (const q of copied) { try { texts.set(q, copyOf(q)); } catch { /* the parse read it once; a second failure changes nothing */ } }
138
164
  const seen = new Set();
139
- for (const text of texts.values()) declaredIn(text, names, seen);
140
- const lines = new Map([...texts].map(([q, t]) => [q, t.split(/\r?\n/)]));
165
+ for (const h of texts.values()) declaredIn(wordsOf(h), names, seen);
166
+ const lines = new Map([...texts].map(([q, h]) => [q, linesOf(h)]));
141
167
  const left = p.unresolvedRefs.filter((x) => !seen.has(x.name) && !truncated(x, lines));
142
168
  if (!left.length) { undecide(f, p.id, 'the parse missed a declaration the text holds', p.unresolvedRefs); continue; }
143
169
  candidates.push({ file: f, id: p.id, line: p.line, refs: left, copied: new Set(r.copies.map((c) => nameOf(c.name))) });
@@ -160,7 +186,7 @@ export function scanCompile(root, opts = {}) {
160
186
  for (const c of candidates) {
161
187
  const names = new Set(c.refs.map((x) => x.name));
162
188
  const seen = new Set();
163
- for (const n of c.copied) for (const p of answering.get(n) || []) { try { declaredIn(tree.text(p).text, names, seen); } catch { /* unread declares nothing */ } }
189
+ for (const n of c.copied) for (const p of answering.get(n) || []) { try { declaredIn(wordsOf(answerOf(p)), names, seen); } catch { /* unread declares nothing */ } }
164
190
  const left = c.refs.filter((x) => !seen.has(x.name));
165
191
  if (!left.length) { undecide(c.file, c.id, 'another copybook of a name it copies declares them', c.refs); continue; }
166
192
  const firsts = new Map();
@@ -186,3 +212,5 @@ export function scanCompile(root, opts = {}) {
186
212
  if (stats.filesUnparsed) stats.coverageIncomplete = true;
187
213
  return report('compile', { rules: COMPILE_RULES, findings, stats, run });
188
214
  }
215
+
216
+ export const scanCompile = (root, opts = {}) => drive(scanCompileSteps(root, opts));
@@ -6,7 +6,7 @@ import { inScope, isCopybook, isProgram, relPath } from '../sources.mjs';
6
6
  import { finish } from '../kernel/findings.mjs';
7
7
  import { report } from '../kernel/ruleset.mjs';
8
8
  import { treeFor, noteUnread, noteUnparsed } from '../kernel/source-tree.mjs';
9
- import { eachWithinMemory } from '../kernel/memory.mjs';
9
+ import { drive, loopOver } from '../kernel/shared-pass.mjs';
10
10
  import { cobolCard } from '../cards.mjs';
11
11
 
12
12
  // A copybook is resolved by name along a search path: the program's own directory first, then the
@@ -71,7 +71,7 @@ export function systemLayoutDeparture(name, text) {
71
71
  return null;
72
72
  }
73
73
 
74
- export function scanCopybooks(root, opts = {}) {
74
+ export function* scanCopybooksSteps(root, opts = {}) {
75
75
  const tree = treeFor(root, opts);
76
76
  const all = tree.list().filter(inScope(opts));
77
77
  const findings = [];
@@ -100,7 +100,7 @@ export function scanCopybooks(root, opts = {}) {
100
100
  // is what stops the reading before the heap does.
101
101
  let run = { skipped: [], stoppedBy: null, peakHeapBytes: 0, note: null };
102
102
  if (candidates.size) {
103
- run = eachWithinMemory(all.filter(isProgram), (p) => {
103
+ run = yield loopOver(all.filter(isProgram), (p) => {
104
104
  let src;
105
105
  try { src = tree.text(p).text; } catch (e) { noteUnread(stats, tree, p, e); return 0; }
106
106
  let r;
@@ -173,3 +173,5 @@ export function scanCopybooks(root, opts = {}) {
173
173
  // that looks unshadowed may only look that way.
174
174
  return report('copybook', { rules: COPYBOOK_RULES, findings, stats, run });
175
175
  }
176
+
177
+ export const scanCopybooks = (root, opts = {}) => drive(scanCopybooksSteps(root, opts));
@@ -11,7 +11,7 @@ import { readFileSync } from 'node:fs';
11
11
  import { inScope, isProgram, relPath } from '../sources.mjs';
12
12
  import { report } from '../kernel/ruleset.mjs';
13
13
  import { treeFor, noteUnread, noteUnparsed } from '../kernel/source-tree.mjs';
14
- import { eachWithinMemory } from '../kernel/memory.mjs';
14
+ import { drive, loopOver } from '../kernel/shared-pass.mjs';
15
15
  import { constantsReaching, keywordsOf } from '../constants.mjs';
16
16
 
17
17
  const TABLE = JSON.parse(readFileSync(new URL('../../rules/icsf-services.json', import.meta.url), 'utf8'));
@@ -54,13 +54,13 @@ const argument = (call, service, parameter) => {
54
54
  };
55
55
  const shown = (values) => [...new Set(values.map((v) => (/^[A-Z-]+$/.test(v) ? v : `'${v.trim()}'`)))].join(', ');
56
56
 
57
- export function scanCrypto(root, opts = {}) {
57
+ export function* scanCryptoSteps(root, opts = {}) {
58
58
  const tree = treeFor(root, opts);
59
59
  const files = tree.list().filter(isProgram).filter(inScope(opts));
60
60
  const findings = [];
61
61
  const stats = { filesScanned: 0, filesUnreadable: 0, filesUnparsed: 0, icsfCalls: 0, argumentsUndecided: 0 };
62
62
 
63
- const run = eachWithinMemory(files, (f) => {
63
+ const run = yield loopOver(files, (f) => {
64
64
  let src;
65
65
  try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
66
66
  if (!/CALL\s+['"]?CS[NF]/i.test(src)) return src.length;
@@ -108,3 +108,5 @@ export function scanCrypto(root, opts = {}) {
108
108
 
109
109
  return report('crypto', { rules: CRYPTO_RULES, findings, stats, run });
110
110
  }
111
+
112
+ export const scanCrypto = (root, opts = {}) => drive(scanCryptoSteps(root, opts));
@@ -0,0 +1,36 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Db2 for z/OS DDL: what a GRANT opens to PUBLIC or lets a grantee pass on, system and database
3
+ // authorities granted, and the exit routines and external load modules that run inside Db2
4
+ // (lib/db2/rules.mjs). A statement in another dialect is refused by the reader and counted as not
5
+ // read, never matched as if it were Db2.
6
+ import { inScope, isDb2, 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 { readDb2, parseDb2Statement } from '../db2/read.mjs';
11
+ import { DB2_RULES, statementFindings } from '../db2/rules.mjs';
12
+
13
+ export { DB2_RULES };
14
+
15
+ export function scanDb2(root, opts = {}) {
16
+ const tree = treeFor(root, opts);
17
+ const files = tree.list().filter(inScope(opts)).filter(isDb2);
18
+ const stats = { filesScanned: 0, filesUnreadable: 0, statementsRead: 0 };
19
+ const findings = [];
20
+ const unread = {};
21
+ const run = eachWithinMemory(files, (f) => {
22
+ let src;
23
+ try { src = tree.text(f).text; } catch (e) { noteUnread(stats, tree, f, e); return 0; }
24
+ stats.filesScanned++;
25
+ const path = relPath(root, f);
26
+ for (const st of readDb2(src).statements) {
27
+ const r = parseDb2Statement(st);
28
+ if (r.status === 'parsed') { stats.statementsRead++; findings.push(...statementFindings(path, st, r)); }
29
+ else (unread[r.kind] ||= { count: 0, first: `${path}:${st.line}` }).count++;
30
+ }
31
+ return src.length;
32
+ }, { label: 'ddl', maxBytes: opts.maxSourceBytes ?? Infinity });
33
+
34
+ if (Object.keys(unread).length) { stats.statementsNotRead = unread; stats.coverageIncomplete = true; }
35
+ return report('ddl', { rules: DB2_RULES, findings, stats, run });
36
+ }
package/lib/sets/flow.mjs CHANGED
@@ -1,5 +1,8 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
- import { analyze } from '../dataflow.mjs';
2
+ import { resolve } from 'node:path';
3
+ import { directoryTree } from '../kernel/source-tree.mjs';
4
+ import { analyzeSteps } from '../dataflow.mjs';
5
+ import { drive } from '../kernel/shared-pass.mjs';
3
6
  import { FLOW_MODEL, SCHEMA_VERSION, TOOL_VERSION } from '../version.mjs';
4
7
  import { finish, withMeta, sortFindings } from '../kernel/findings.mjs';
5
8
 
@@ -11,21 +14,25 @@ export const RULES = {
11
14
  'cics-terminal-to-os-command': { sev: 'crit', evidence: 'path', cwe: 'CWE-78', text: 'Terminal input reaches an operating-system command routine' },
12
15
  'cics-web-to-os-command': { sev: 'crit', evidence: 'path', cwe: 'CWE-78', text: 'Web input reaches an operating-system command routine' },
13
16
  'file-record-to-os-command': { sev: 'high', evidence: 'path', cwe: 'CWE-78', text: 'A file record reaches an operating-system command routine' },
17
+ 'cics-queue-to-os-command': { sev: 'high', evidence: 'path', measured: false, cwe: 'CWE-78', text: 'A CICS queue item reaches an operating-system command routine' },
14
18
  'database-to-os-command': { sev: 'high', evidence: 'path', cwe: 'CWE-78', text: 'A database value reaches an operating-system command routine' },
15
19
  'argv-or-env-to-dynamic-sql': { sev: 'high', evidence: 'path', cwe: 'CWE-89', text: 'Command-line or environment input reaches dynamic SQL' },
16
20
  'cics-terminal-to-dynamic-sql': { sev: 'crit', evidence: 'path', cwe: 'CWE-89', text: 'Terminal input reaches dynamic SQL' },
17
21
  'cics-web-to-dynamic-sql': { sev: 'crit', evidence: 'path', cwe: 'CWE-89', text: 'Web input reaches dynamic SQL' },
18
22
  'file-record-to-dynamic-sql': { sev: 'high', evidence: 'path', cwe: 'CWE-89', text: 'A file record reaches dynamic SQL' },
23
+ 'cics-queue-to-dynamic-sql': { sev: 'high', evidence: 'path', measured: false, cwe: 'CWE-89', text: 'A CICS queue item reaches dynamic SQL' },
19
24
  'database-to-dynamic-sql': { sev: 'high', evidence: 'path', cwe: 'CWE-89', text: 'A database value reaches dynamic SQL' },
20
25
  'argv-or-env-to-cics-dynamic-transfer': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'Command-line or environment input decides which program CICS transfers to' },
21
26
  'cics-terminal-to-cics-dynamic-transfer': { sev: 'crit', evidence: 'path', cwe: 'CWE-470', text: 'Terminal input decides which program CICS transfers to' },
22
27
  'cics-web-to-cics-dynamic-transfer': { sev: 'crit', evidence: 'path', cwe: 'CWE-470', text: 'Web input decides which program CICS transfers to' },
23
28
  'file-record-to-cics-dynamic-transfer': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'A file record decides which program CICS transfers to' },
29
+ 'cics-queue-to-cics-dynamic-transfer': { sev: 'high', evidence: 'path', measured: false, cwe: 'CWE-470', text: 'A CICS queue item decides which program CICS transfers to' },
24
30
  'database-to-cics-dynamic-transfer': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'A database value decides which program CICS transfers to' },
25
31
  'argv-or-env-to-dynamic-program-load': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'Command-line or environment input decides which program is called' },
26
32
  'cics-terminal-to-dynamic-program-load': { sev: 'crit', evidence: 'path', cwe: 'CWE-470', text: 'Terminal input decides which program is called' },
27
33
  'cics-web-to-dynamic-program-load': { sev: 'crit', evidence: 'path', cwe: 'CWE-470', text: 'Web input decides which program is called' },
28
34
  'file-record-to-dynamic-program-load': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'A file record decides which program is called' },
35
+ 'cics-queue-to-dynamic-program-load': { sev: 'high', evidence: 'path', measured: false, cwe: 'CWE-470', text: 'A CICS queue item decides which program is called' },
29
36
  'database-to-dynamic-program-load': { sev: 'high', evidence: 'path', cwe: 'CWE-470', text: 'A database value decides which program is called' },
30
37
  // A program opening the file its own user named acts with that user's authority, which is every
31
38
  // one read in the 500-repository corpus; it escalates only where the estate says the entry that
@@ -34,6 +41,7 @@ export const RULES = {
34
41
  'cics-terminal-to-dynamic-file-path': { sev: 'high', evidence: 'path', cwe: 'CWE-73', text: 'Terminal input decides which file is opened' },
35
42
  'cics-web-to-dynamic-file-path': { sev: 'high', evidence: 'path', cwe: 'CWE-73', text: 'Web input decides which file is opened' },
36
43
  'file-record-to-dynamic-file-path': { sev: 'med', evidence: 'path', cwe: 'CWE-73', text: 'A file record decides which file is opened' },
44
+ 'cics-queue-to-dynamic-file-path': { sev: 'med', evidence: 'path', measured: false, cwe: 'CWE-73', text: 'A CICS queue item decides which file is opened' },
37
45
  'database-to-dynamic-file-path': { sev: 'med', evidence: 'path', cwe: 'CWE-73', text: 'A database value decides which file is opened' },
38
46
  // A JCL step is the entry point above the program. PARM is the mainframe's argv, and in-stream
39
47
  // data is a payload sitting in the repository: both are edited by anyone who can commit, so they
@@ -57,6 +65,7 @@ export const RULES = {
57
65
  'cics-terminal-to-internal-reader': { sev: 'crit', evidence: 'path', cwe: 'CWE-77', text: 'Terminal input is written into a job the internal reader submits' },
58
66
  'cics-web-to-internal-reader': { sev: 'crit', evidence: 'path', cwe: 'CWE-77', text: 'Web input is written into a job the internal reader submits' },
59
67
  'file-record-to-internal-reader': { sev: 'med', evidence: 'path', cwe: 'CWE-77', text: 'A file record is written into a job the internal reader submits' },
68
+ 'cics-queue-to-internal-reader': { sev: 'med', evidence: 'path', measured: false, cwe: 'CWE-77', text: 'A CICS queue item is written into a job the internal reader submits' },
60
69
  'database-to-internal-reader': { sev: 'med', evidence: 'path', cwe: 'CWE-77', text: 'A database value is written into a job the internal reader submits' },
61
70
  'jcl-parm-to-internal-reader': { sev: 'crit', evidence: 'path', cwe: 'CWE-77', text: 'A JCL PARM is written into a job the internal reader submits' },
62
71
  'jcl-instream-to-internal-reader': { sev: 'crit', evidence: 'path', cwe: 'CWE-77', text: 'In-stream job data is written into a job the internal reader submits' },
@@ -65,18 +74,22 @@ export const RULES = {
65
74
  'cics-web-to-web-response': { sev: 'high', evidence: 'path', cwe: 'CWE-79', text: 'Web input is written into the response this program returns' },
66
75
  'cics-terminal-to-web-response': { sev: 'high', evidence: 'path', cwe: 'CWE-79', text: 'Terminal input is written into the response this program returns' },
67
76
  'file-record-to-web-response': { sev: 'med', evidence: 'path', cwe: 'CWE-79', text: 'A file record is written into the response this program returns' },
77
+ 'cics-queue-to-web-response': { sev: 'med', evidence: 'path', measured: false, cwe: 'CWE-79', text: 'A CICS queue item is written into the response this program returns' },
68
78
  'database-to-web-response': { sev: 'med', evidence: 'path', cwe: 'CWE-79', text: 'A database value is written into the response this program returns' },
69
79
  'cics-web-to-http-header': { sev: 'high', evidence: 'path', cwe: 'CWE-113', text: 'Web input is written into a header of the response' },
70
80
  'cics-terminal-to-http-header': { sev: 'high', evidence: 'path', cwe: 'CWE-113', text: 'Terminal input is written into a header of the response' },
71
81
  'file-record-to-http-header': { sev: 'med', evidence: 'path', cwe: 'CWE-113', text: 'A file record is written into a header of the response' },
82
+ 'cics-queue-to-http-header': { sev: 'med', evidence: 'path', measured: false, cwe: 'CWE-113', text: 'A CICS queue item is written into a header of the response' },
72
83
  'database-to-http-header': { sev: 'med', evidence: 'path', cwe: 'CWE-113', text: 'A database value is written into a header of the response' },
73
84
  'cics-web-to-outbound-host': { sev: 'high', evidence: 'path', cwe: 'CWE-918', text: 'Web input decides the host or path this program sends a request to' },
74
85
  'cics-terminal-to-outbound-host': { sev: 'high', evidence: 'path', cwe: 'CWE-918', text: 'Terminal input decides the host or path this program sends a request to' },
75
86
  'file-record-to-outbound-host': { sev: 'med', evidence: 'path', cwe: 'CWE-918', text: 'A file record decides the host or path this program sends a request to' },
87
+ 'cics-queue-to-outbound-host': { sev: 'med', evidence: 'path', measured: false, cwe: 'CWE-918', text: 'A CICS queue item decides the host or path this program sends a request to' },
76
88
  'database-to-outbound-host': { sev: 'med', evidence: 'path', cwe: 'CWE-918', text: 'A database value decides the host or path this program sends a request to' },
77
89
  'cics-web-to-queue-name': { sev: 'high', evidence: 'path', cwe: 'CWE-99', text: 'Web input names the queue a command reads, writes or deletes' },
78
90
  'cics-terminal-to-queue-name': { sev: 'high', evidence: 'path', cwe: 'CWE-99', text: 'Terminal input names the queue a command reads, writes or deletes' },
79
91
  'file-record-to-queue-name': { sev: 'med', evidence: 'path', cwe: 'CWE-99', text: 'A file record names the queue a command reads, writes or deletes' },
92
+ 'cics-queue-to-queue-name': { sev: 'med', evidence: 'path', measured: false, cwe: 'CWE-99', text: 'A CICS queue item names the queue a command reads, writes or deletes' },
80
93
  'database-to-queue-name': { sev: 'med', evidence: 'path', cwe: 'CWE-99', text: 'A database value names the queue a command reads, writes or deletes' },
81
94
  // Where a statement reads or writes, decided by input from outside. Without SSRANGE, which is not
82
95
  // the Enterprise COBOL default, an index out of range reads or writes the storage beside the
@@ -130,10 +143,12 @@ export const RULES = {
130
143
  'cics-terminal-to-cics-sysid': { sev: 'med', evidence: 'path', cwe: 'CWE-15', text: 'Terminal input decides which region a CICS command is shipped to' },
131
144
  'cics-web-to-cics-sysid': { sev: 'med', evidence: 'path', cwe: 'CWE-15', text: 'Web input decides which region a CICS command is shipped to' },
132
145
  'file-record-to-cics-sysid': { sev: 'low', evidence: 'path', cwe: 'CWE-15', text: 'A file record decides which region a CICS command is shipped to' },
146
+ 'cics-queue-to-cics-sysid': { sev: 'low', evidence: 'path', measured: false, cwe: 'CWE-15', text: 'A CICS queue item decides which region a CICS command is shipped to' },
133
147
  'database-to-cics-sysid': { sev: 'low', evidence: 'path', cwe: 'CWE-15', text: 'A database value decides which region a CICS command is shipped to' },
134
148
  'cics-terminal-to-cics-system-resource': { sev: 'high', evidence: 'path', cwe: 'CWE-15', text: 'Terminal input names the resource an EXEC CICS SET changes' },
135
149
  'cics-web-to-cics-system-resource': { sev: 'high', evidence: 'path', cwe: 'CWE-15', text: 'Web input names the resource an EXEC CICS SET changes' },
136
150
  'file-record-to-cics-system-resource': { sev: 'med', evidence: 'path', cwe: 'CWE-15', text: 'A file record names the resource an EXEC CICS SET changes' },
151
+ 'cics-queue-to-cics-system-resource': { sev: 'med', evidence: 'path', measured: false, cwe: 'CWE-15', text: 'A CICS queue item names the resource an EXEC CICS SET changes' },
137
152
  'database-to-cics-system-resource': { sev: 'med', evidence: 'path', cwe: 'CWE-15', text: 'A database value names the resource an EXEC CICS SET changes' },
138
153
  // A document from outside parsed as XML. Whether a DTD in it is honoured - an external entity
139
154
  // fetched, an internal one expanded - is the parser's, chosen by the XMLPARSE compiler option, and
@@ -165,12 +180,16 @@ export const RULES = {
165
180
  // way mainframe systems hand data to each other and is listed so the route is visible.
166
181
  'database-to-outbound-http': { sev: 'high', evidence: 'path', cwe: 'CWE-201', text: 'A database value is sent to a remote server over HTTP' },
167
182
  'file-record-to-outbound-http': { sev: 'high', evidence: 'path', cwe: 'CWE-201', text: 'A file record is sent to a remote server over HTTP' },
183
+ 'cics-queue-to-outbound-http': { sev: 'high', evidence: 'path', measured: false, cwe: 'CWE-201', text: 'A CICS queue item is sent to a remote server over HTTP' },
168
184
  'database-to-socket-send': { sev: 'high', evidence: 'path', cwe: 'CWE-201', text: 'A database value is written to a network socket' },
169
185
  'file-record-to-socket-send': { sev: 'high', evidence: 'path', cwe: 'CWE-201', text: 'A file record is written to a network socket' },
186
+ 'cics-queue-to-socket-send': { sev: 'high', evidence: 'path', measured: false, cwe: 'CWE-201', text: 'A CICS queue item is written to a network socket' },
170
187
  'database-to-message-queue': { sev: 'low', evidence: 'path', cwe: 'CWE-201', text: 'A database value is put on a message queue' },
171
188
  'database-to-extrapartition-queue': { sev: 'low', evidence: 'path', cwe: 'CWE-201', text: 'A database value is written to a transient-data queue that leaves the region' },
172
189
  'file-record-to-extrapartition-queue': { sev: 'low', evidence: 'path', cwe: 'CWE-201', text: 'A file record is written to a transient-data queue that leaves the region' },
190
+ 'cics-queue-to-extrapartition-queue': { sev: 'low', evidence: 'path', measured: false, cwe: 'CWE-201', text: 'A CICS queue item is written to a transient-data queue that leaves the region' },
173
191
  'file-record-to-message-queue': { sev: 'low', evidence: 'path', cwe: 'CWE-201', text: 'A file record is put on a message queue' },
192
+ 'cics-queue-to-message-queue': { sev: 'low', evidence: 'path', measured: false, cwe: 'CWE-201', text: 'A CICS queue item is put on a message queue' },
174
193
  // The CALL interface itself, from the same bound pairs the paths above cross. No input has to
175
194
  // reach these: a callee that declares more than it is passed reaches past it on every call.
176
195
  'call-parameter-exceeds-caller-record': { sev: 'high', evidence: 'construct', cwe: 'CWE-805', text: 'A called program declares a parameter longer than the whole record its caller passes' },
@@ -190,6 +209,7 @@ export const WHO = {
190
209
  'cics-terminal': 'A terminal user',
191
210
  'cics-web': 'A web caller',
192
211
  'file-record': 'Whoever can write the file record',
212
+ 'cics-queue': 'Whoever can write the queue, or start the transaction with data',
193
213
  'database': 'Whoever can write the database row',
194
214
  'jcl-parm': "Whoever can edit the job's PARM",
195
215
  'jcl-instream': "Whoever can edit the job's in-stream data",
@@ -216,7 +236,7 @@ const SINK = {
216
236
  },
217
237
  'dynamic-file-path': {
218
238
  clause: 'chooses which dataset the program opens',
219
- remedy: 'Map input to a fixed set of DDs or dataset names; do not build the ASSIGN or dynamic-allocation name from input',
239
+ remedy: 'Map input to a fixed set of DDs, dataset names or CICS file names; do not build the ASSIGN, dynamic-allocation or FILE name from input',
220
240
  },
221
241
  'internal-reader': {
222
242
  clause: "submits an arbitrary job, for a CICS program under the region's authority",
@@ -365,11 +385,11 @@ function mergeRoutes(a, b) {
365
385
  };
366
386
  }
367
387
  // One graph per repository, for the reason scanAll gives: names are only unique within one.
368
- function analyzeEach(root, opts) {
369
- if (!opts.repos || opts.repos.length < 2) return analyze(root, opts);
388
+ function* analyzeEach(root, opts) {
389
+ if (!opts.repos || opts.repos.length < 2) return yield* analyzeSteps(root, opts);
370
390
  const merged = { findings: [], constructs: [], unstarted: [], startedBy: {}, stats: {} };
371
391
  for (const repo of opts.repos) {
372
- const one = analyze(root, { ...opts, repos: [repo] });
392
+ const one = yield* analyzeSteps(root, { ...opts, repos: [repo] });
373
393
  merged.findings.push(...one.findings);
374
394
  merged.constructs.push(...one.constructs);
375
395
  merged.unstarted.push(...one.unstarted);
@@ -386,8 +406,9 @@ function analyzeEach(root, opts) {
386
406
  return merged;
387
407
  }
388
408
 
389
- export function scan(root, opts = {}) {
390
- const res = analyzeEach(root, opts);
409
+ export function* scanSteps(root, opts = {}) {
410
+ root = resolve(root);
411
+ const res = yield* analyzeEach(root, opts);
391
412
  const findings = [];
392
413
  for (const f of res.findings) {
393
414
  const meta = RULES[f.rule] || DEFAULT;
@@ -531,7 +552,8 @@ export function scan(root, opts = {}) {
531
552
  // happened to compute the right answer for the whole report - it ORs filesOverBudget in
532
553
  // separately - so nothing was wrong downstream, and nothing would have been until someone
533
554
  // called this set on its own and believed a silence.
534
- coverageIncomplete: (res.stats.overBudget || 0) > 0
555
+ coverageIncomplete: (res.stats.files === 0 && directoryTree(root, opts).list().length === 0)
556
+ || (res.stats.overBudget || 0) > 0
535
557
  || (res.stats.filesNotReached || 0) > 0
536
558
  || (res.stats.unreadable || 0) > 0
537
559
  || (res.stats.threw || 0) > 0
@@ -570,6 +592,8 @@ export function scan(root, opts = {}) {
570
592
  return report;
571
593
  }
572
594
 
595
+ export const scan = (root, opts = {}) => drive(scanSteps(root, opts));
596
+
573
597
  // Findings counted by the transaction or job that reaches them: the unit a mainframe team triages
574
598
  // in. A finding several entries reach counts under each.
575
599
  export function rollup(findings) {
@@ -4,7 +4,7 @@ import { cobolCard } from '../cards.mjs';
4
4
  import { decodeEbcdic, inScope, isJcl, isSource, looksEbcdic, relPath } from '../sources.mjs';
5
5
  import { report } from '../kernel/ruleset.mjs';
6
6
  import { treeFor, noteUnread } from '../kernel/source-tree.mjs';
7
- import { eachWithinMemory } from '../kernel/memory.mjs';
7
+ import { drive, loopOver } from '../kernel/shared-pass.mjs';
8
8
 
9
9
  // Content a compiler ignores and an editor often hides, but an agent reading the source will see:
10
10
  // the sequence area, the identification area past column 72, comment lines, and characters that
@@ -154,14 +154,14 @@ const looksBinary = (buf) => {
154
154
  return odd / n > 0.05;
155
155
  };
156
156
 
157
- export function scanHidden(root, opts = {}) {
157
+ export function* scanHiddenSteps(root, opts = {}) {
158
158
  const tree = treeFor(root, opts);
159
159
  const files = tree.list().filter(isSource).filter(inScope(opts));
160
160
  const findings = [];
161
161
  const stats = { filesScanned: 0, filesUnreadable: 0, filesBinary: 0, filesEbcdic: 0 };
162
162
  // This set reads every source file in the tree as raw bytes, which is the widest reading of the
163
163
  // nine, so it is the one most worth walking inside the guard.
164
- const run = eachWithinMemory(files, (f) => {
164
+ const run = yield loopOver(files, (f) => {
165
165
  let buf;
166
166
  try { buf = tree.bytes(f); } catch (e) { noteUnread(stats, tree, f, e); return 0; }
167
167
  // An EBCDIC member is decoded before any rule reads it; read as bytes it looks binary, and a
@@ -210,3 +210,5 @@ export function scanHidden(root, opts = {}) {
210
210
 
211
211
  return report('hidden', { rules: HIDDEN_RULES, findings, stats, run });
212
212
  }
213
+
214
+ export const scanHidden = (root, opts = {}) => drive(scanHiddenSteps(root, opts));