@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
package/lib/build.mjs CHANGED
@@ -6,25 +6,29 @@ import { checkEquivalence } from './equivalence.mjs';
6
6
  import { optionChanges } from './option-diff.mjs';
7
7
  import { createHash } from 'node:crypto';
8
8
  import { spawnSync } from 'node:child_process';
9
- import { existsSync, readFileSync, statSync } from 'node:fs';
10
- import { basename, extname, join } from 'node:path';
9
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { basename, dirname, extname, join, resolve } from 'node:path';
12
+ import { parseAllowedSigners } from './evidence/sshsig.mjs';
11
13
  import { scanAll } from './scan.mjs';
12
14
  import { withRefs, configurationChanges, WORKING_TREE } from './diff.mjs';
13
15
  import { pairLists, findExecutable } from './gate.mjs';
14
16
  import { pairingKeys } from './kernel/identity.mjs';
15
- import { directoryTree } from './kernel/source-tree.mjs';
17
+ import { directoryTree, pdsExportTree, resolvesInside } from './kernel/source-tree.mjs';
16
18
  import { printable } from './kernel/printable.mjs';
17
19
  import { ALL_RULES } from './kernel/registry.mjs';
18
20
  import { loadBaseline, applyBaseline, SUPPRESSING } from './baseline.mjs';
19
21
  import { loadSite, SITE_FILE } from './site.mjs';
20
- import { isProgram, isJcl, isSource, readSource, relPath } from './sources.mjs';
21
- import { parseJclFile } from './jcl.mjs';
22
+ import { isProgram, isJcl, isSource, relPath } from './sources.mjs';
23
+ import { parseJcl } from './jcl.mjs';
22
24
  import { POLICY_FILE, treePolicy, floorPolicy, combinePolicies, canonical, policyHash } from './policy.mjs';
23
25
  import { classesOf, tierOf, TIERS, TIER_RANK } from './consequence.mjs';
24
- import { optionCards, enterpriseChecks, compilerChecks, scriptedChecks, compilerInvocations, compilerTasks, compileStepOptions, compilerOf } from './options.mjs';
26
+ import { optionCards, enterpriseChecks, compilerChecks, scriptedChecks, compilerInvocations, compilerTasks, compileStepOptions, compilerOf, olderThan } from './options.mjs';
27
+ import { isBuildFile, pinsIn } from './sets/build.mjs';
25
28
  import { FLOW_MODEL, TOOL_VERSION } from './version.mjs';
26
29
  import { checkWithIronwork, ironworkVerdict, ironworkReasons, ironworkVersion, listed } from './ironwork.mjs';
27
30
  import { buildFileIndex } from './parser.mjs';
31
+ import { precompileCheck } from './precompile-check.mjs';
28
32
 
29
33
  export const BUILD_SCHEMA_VERSION = 1;
30
34
  export const BUILD_EXIT = { pass: 0, fail: 1, undecided: 3, compilerFailed: 4 };
@@ -62,8 +66,11 @@ function introducedIn(before, after, baseRoot, headRoot) {
62
66
  // Why a finding blocks, or null. `renewed` is a finding whose waiver expired or ran past maxDays: in
63
67
  // ratchet mode it is judged as new, because the promise that carried it has lapsed.
64
68
  export function blockingReason(f, { policy, ratchet, introduced, renewed }) {
69
+ if (f.abend?.inputFrom === 'interface' && policy.interface !== 'tier') return null;
65
70
  const setting = policy.rules[f.rule];
66
71
  if (setting === 'warn') return null;
72
+ // A rule whose firing rate the corpus has not recorded blocks only where the policy names it.
73
+ if (ALL_RULES[f.rule]?.measured === false) return setting === 'block' ? 'rule' : null;
67
74
  const tier = tierOf(f);
68
75
  if (ratchet && TIER_RANK[tier] >= TIER_RANK[policy.always]) return 'tier';
69
76
  if (ratchet && !introduced.has(f) && !renewed.has(f.fingerprint)) return null;
@@ -82,12 +89,12 @@ const MAX_BARE_SCRIPT_BYTES = 64 << 10;
82
89
  const kindOf = (rel) => (/(^|\/)(gnu)?makefile$|\.mk$/i.test(rel) ? 'make' : /(^|\/)dockerfile$/i.test(rel) ? 'docker'
83
90
  : /\.(bat|cmd)$/i.test(rel) ? 'batch' : /\.ps1$/i.test(rel) ? 'powershell' : /\.ya?ml$/i.test(rel) ? 'yaml' : 'sh');
84
91
 
85
- function scriptInvocations(file, rel) {
86
- if (EDITOR_TASKS.test(rel)) return compilerTasks(readSource(file).text);
87
- const size = statSync(file).size;
88
- if (BUILD_SCRIPT.test(rel) && size <= MAX_SCRIPT_BYTES) return compilerInvocations(readSource(file).text, { kind: kindOf(rel) });
92
+ function scriptInvocations(tree, file, rel) {
93
+ if (EDITOR_TASKS.test(rel)) return compilerTasks(tree.text(file).text);
94
+ const size = tree.kind === 'directory' ? statSync(file).size : tree.bytes(file).length;
95
+ if (BUILD_SCRIPT.test(rel) && size <= MAX_SCRIPT_BYTES) return compilerInvocations(tree.text(file).text, { kind: kindOf(rel) });
89
96
  if (!extname(file) && size <= MAX_BARE_SCRIPT_BYTES) {
90
- const bytes = readFileSync(file);
97
+ const bytes = tree.bytes(file);
91
98
  return bytes.includes(0) ? [] : compilerInvocations(bytes.toString('latin1'), { kind: 'sh' });
92
99
  }
93
100
  return [];
@@ -95,14 +102,14 @@ function scriptInvocations(file, rel) {
95
102
 
96
103
  // How the repository says it compiles its programs: the cobc and gcobol commands its build scripts
97
104
  // run, and the Enterprise COBOL compile steps its JCL holds.
98
- function declaredCompiles(root, files) {
105
+ function declaredCompiles(tree, files) {
99
106
  const invocations = [];
100
107
  const parsedJcl = [];
101
108
  for (const f of files) {
102
- const rel = relPath(root, f);
109
+ const rel = tree.rel(f);
103
110
  try {
104
- if (isJcl(f)) parsedJcl.push({ ...parseJclFile(f), file: rel });
105
- else if (!isProgram(f)) for (const inv of scriptInvocations(f, rel)) invocations.push({ ...inv, file: rel });
111
+ if (isJcl(f)) parsedJcl.push({ ...parseJcl(tree.text(f).text, f), file: rel });
112
+ else if (!isProgram(f)) for (const inv of scriptInvocations(tree, f, rel)) invocations.push({ ...inv, file: rel });
106
113
  } catch { /* unreadable: the scan counts it, and it declares nothing here */ }
107
114
  }
108
115
  const steps = new Map();
@@ -115,27 +122,41 @@ function declaredCompiles(root, files) {
115
122
 
116
123
  const memberName = (file) => basename(file).replace(/\.[^.]*$/, '').toUpperCase();
117
124
 
125
+ // The GnuCOBOL version the repository's build files pin, the oldest where they pin several.
126
+ function pinnedGnucobol(tree, files) {
127
+ let oldest = null;
128
+ for (const file of files.filter(isBuildFile)) {
129
+ let text;
130
+ try { text = tree.text(file).text; } catch { continue; }
131
+ for (const p of pinsIn(text)) {
132
+ if (p.product === 'gnucobol' && (!oldest || olderThan(p.version, oldest.version))) oldest = { version: p.version, where: tree.rel(file) };
133
+ }
134
+ }
135
+ return oldest;
136
+ }
137
+
118
138
  // The options every program in the tree is compiled with, held to the policy. A program's options
119
139
  // come from the estate's declared defaults, the JCL step that compiles it and its own option cards;
120
140
  // where it has none of those, from the cobc and gcobol commands the repository's build scripts run.
121
141
  // `ok` is false where a required check is not generated and null where no one can tell; `forbidden`
122
142
  // counts options the policy's forbid list names. `generated` maps each program and build script to
123
143
  // the checks it generates, for telling a change that removes one.
124
- function optionsCheck({ root, allow, site, policy, compiler }) {
144
+ function optionsCheck({ root, tree: given = null, allow, site, policy, compiler }) {
125
145
  const required = policy.checks;
126
146
  const kind = compiler && compilerOf(compiler[0]);
127
147
  if (compiler && !kind) {
128
148
  return { ok: null, forbidden: 0, reasons: [`the gate reads the options of cobc, of gcobol and of Enterprise COBOL option cards; for ${printable(compiler[0], 60)} it cannot tell what reaches the compiler`], add: [], generated: new Map() };
129
149
  }
150
+ const tree = given || directoryTree(root);
151
+ const files = [...new Set(tree.list())].filter((p) => !allow || allow.has(p)).sort();
152
+ const pinned = pinnedGnucobol(tree, files);
130
153
  if (compiler) {
131
- const r = compilerChecks(kind, compiler.slice(1), { required, forbid: policy.forbid[kind] });
154
+ const r = compilerChecks(kind, compiler.slice(1), { required, forbid: policy.forbid[kind], pinned });
132
155
  const missing = r.checks.filter((c) => c.ok === false).map((c) => c.why);
133
156
  return { ok: missing.length || r.forbidden.length ? false : true, forbidden: r.forbidden.length, reasons: [...r.forbidden, ...missing], add: r.forbidden.length ? [] : r.add, compiler: kind, generated: new Map() };
134
157
  }
135
- const tree = directoryTree(root);
136
- const files = [...new Set(tree.list())].filter((p) => !allow || allow.has(p)).sort();
137
158
  const programs = files.filter(isProgram);
138
- const declared = declaredCompiles(root, files);
159
+ const declared = declaredCompiles(tree, files);
139
160
  let ok = true;
140
161
  let forbidden = 0;
141
162
  const reasons = [];
@@ -150,7 +171,7 @@ function optionsCheck({ root, allow, site, policy, compiler }) {
150
171
  };
151
172
 
152
173
  for (const inv of declared.invocations) {
153
- const r = scriptedChecks(inv.compiler, inv.args, { required, forbid: policy.forbid[inv.compiler] });
174
+ const r = scriptedChecks(inv.compiler, inv.args, { required, forbid: policy.forbid[inv.compiler], pinned });
154
175
  record(inv.file, r.checks);
155
176
  forbidden += r.forbidden.length;
156
177
  for (const why of [...r.checks.filter((c) => c.ok === false).map((c) => c.why), ...r.forbidden]) fail(`${inv.file}:${inv.line}: ${why}`);
@@ -158,7 +179,7 @@ function optionsCheck({ root, allow, site, policy, compiler }) {
158
179
  let unknown = 0;
159
180
  for (const file of programs) {
160
181
  let text;
161
- try { text = readSource(file).text; } catch { open(); reasons.push(`${printable(relPath(root, file), 120)} could not be read, so its options are not known`); continue; }
182
+ try { text = tree.text(file).text; } catch { open(); reasons.push(`${printable(tree.rel(file), 120)} could not be read, so its options are not known`); continue; }
162
183
  const cards = optionCards(text);
163
184
  const steps = declared.steps.get(memberName(file)) || [];
164
185
  const siteOptions = site.compilerOptions || [];
@@ -167,10 +188,10 @@ function optionsCheck({ root, allow, site, policy, compiler }) {
167
188
  continue;
168
189
  }
169
190
  const r = enterpriseChecks({ required, site: siteOptions, steps, cards, forbid: policy.forbid.enterprise });
170
- record(relPath(root, file), r.checks);
191
+ record(tree.rel(file), r.checks);
171
192
  forbidden += r.forbidden.length;
172
193
  const failed = r.checks.filter((c) => c.ok === false).map((c) => c.why);
173
- for (const why of [...failed, ...r.forbidden]) fail(`${relPath(root, file)}: ${why}`);
194
+ for (const why of [...failed, ...r.forbidden]) fail(`${tree.rel(file)}: ${why}`);
174
195
  if (!failed.length && !r.forbidden.length && r.checks.some((c) => c.ok === null)) { unknown++; open(); }
175
196
  }
176
197
  if (unknown) reasons.push(`${unknown} program(s) have no option this gate can read for ${required.join(', ')}: no compile step or build script names them; declare the estate's defaults as compilerOptions in cobolwork.site.json`);
@@ -193,20 +214,41 @@ function optionsRemoved(before, after) {
193
214
  return removed;
194
215
  }
195
216
 
196
- // The files a scan of `root` reads, hashed, for the provenance record.
197
- function sourceHashes(root, allow) {
198
- const tree = directoryTree(root);
217
+ // The files a scan of `root`, or of a PDS export's tree, reads, hashed, for the provenance record.
218
+ function sourceHashes(root, allow, given = null) {
219
+ const tree = given || directoryTree(root);
199
220
  return [...new Set(tree.list())].filter((p) => isSource(p) && (!allow || allow.has(p))).sort()
200
- .map((p) => ({ path: relPath(root, p), sha256: sha256(readFileSync(p)) }))
221
+ .map((p) => ({ path: tree.rel(p), sha256: sha256(tree.bytes(p)) }))
201
222
  .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
202
223
  }
203
224
 
225
+ // A PDS export's members written out as DATA.SET/MEMBER for ironwork, which reads files: the
226
+ // programs to check, and each copy library's directory. `fn` gets the layout's root and those.
227
+ function withLayout(tree, allow, fn) {
228
+ const root = mkdtempSync(join(tmpdir(), 'cobolwork-pds-layout-'));
229
+ try {
230
+ const programs = [];
231
+ for (const p of tree.list()) {
232
+ const file = join(root, ...tree.rel(p).split('/'));
233
+ mkdirSync(dirname(file), { recursive: true });
234
+ writeFileSync(file, tree.text(p).text, 'latin1');
235
+ if (isProgram(p) && (!allow || allow.has(p))) programs.push(file);
236
+ }
237
+ return fn(root, programs.sort(), tree.index.copyDirs.map((d) => join(root, ...tree.rel(d).split('/'))));
238
+ } finally { rmSync(root, { recursive: true, force: true }); }
239
+ }
240
+
204
241
  // ironwork checks the head tree, which in ratchet mode exists only while judge runs, so it runs
205
242
  // here rather than after the verdict the way a caller's compiler does.
206
- function ironworkCompile(path, { root, allow, copylibs, env }) {
207
- const result = checkWithIronwork(path, root, { allow, copylibs, env });
243
+ function ironworkCompile(path, { root, tree = null, allow, copylibs, env }) {
244
+ if (tree) return withLayout(tree, allow, (layout, programs, copyDirs) => ironworkChecked(path, { root: layout, programs, copyDirs, copylibs, env }));
245
+ return ironworkChecked(path, { root, allow, copylibs, env });
246
+ }
247
+
248
+ function ironworkChecked(path, { root, allow = null, programs = null, copyDirs = null, copylibs, env }) {
249
+ const result = checkWithIronwork(path, root, { allow, programs, copyDirs, copylibs, env });
208
250
  const check = ironworkVerdict(result);
209
- const libraries = [...buildFileIndex(root).copyDirs.map((d) => relPath(root, d) || '.'), ...copylibs];
251
+ const libraries = [...(copyDirs || buildFileIndex(root).copyDirs).map((d) => relPath(root, d) || '.'), ...copylibs];
210
252
  return {
211
253
  check,
212
254
  reasons: ironworkReasons(result),
@@ -219,16 +261,27 @@ function ironworkCompile(path, { root, allow, copylibs, env }) {
219
261
  };
220
262
  }
221
263
 
222
- function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compiler, compilerPath, ironworkPath, env, now, advisoryFeeds, copylibs, equivalenceFiles = [], allowed = null, refs = null }) {
264
+ // The members of a PDS export whose files `allow` holds: an archive's members by the archive's file.
265
+ function membersAllowed(tree, allow) {
266
+ return new Set(tree.list().filter((p) => allow.has(resolve(tree.dir, tree.origin(p).replace(/\([^)]*\)$/, '')))));
267
+ }
268
+
269
+ function judge({ baseDir, headDir, allow: given, policyRoot, floor, noBaseline, compiler, compilerPath, ironworkPath, env, now, advisoryFeeds, copylibs, equivalenceFiles = [], allowed = null, refs = null, pdsExport = false }) {
223
270
  const repoPolicy = treePolicy(policyRoot);
224
271
  if (repoPolicy && repoPolicy.problems.length) throw refusal(`the repository policy does not validate: ${repoPolicy.problems.join('; ')}`);
225
272
  const { policy, setBy, ignored } = combinePolicies({ floor: floor ? floor.raw : null, repository: repoPolicy ? repoPolicy.raw : null });
226
273
  const ratchet = baseDir !== null;
274
+ // A PDS export is judged as its members, each side read as scan --pds-export reads it; the policy,
275
+ // site file and baseline stay files beside the members.
276
+ const trees = pdsExport ? { base: ratchet ? pdsExportTree(baseDir, { systemDirs: copylibs }) : null, head: pdsExportTree(headDir, { systemDirs: copylibs }) } : null;
277
+ const allow = trees && given ? membersAllowed(trees.head, given) : given;
227
278
  // In ratchet mode the change is judged by its base's site file, not by one it wrote itself.
228
- const scanOpts = { ...(ratchet ? { site: join(baseDir, SITE_FILE) } : {}), ...(advisoryFeeds ? { advisoryFeeds } : {}), systemDirs: copylibs };
229
- const before = ratchet ? scanAll(baseDir, scanOpts) : null;
230
- const after = scanAll(headDir, { ...scanOpts, allow });
231
- const introduced = ratchet ? introducedIn(before, after, baseDir, headDir) : new Set();
279
+ const siteFile = ratchet ? join(baseDir, SITE_FILE) : trees && existsSync(join(headDir, SITE_FILE)) ? join(headDir, SITE_FILE) : null;
280
+ const scanOpts = { ...(siteFile ? { site: siteFile } : {}), ...(advisoryFeeds ? { advisoryFeeds } : {}), systemDirs: copylibs };
281
+ const scanOf = (dir, tree, extra = {}) => (tree ? scanAll(tree.root, { ...scanOpts, ...extra, tree, feedRoot: dir }) : scanAll(dir, { ...scanOpts, ...extra }));
282
+ const before = ratchet ? scanOf(baseDir, trees?.base) : null;
283
+ const after = scanOf(headDir, trees?.head, { allow });
284
+ const introduced = ratchet ? introducedIn(before, after, trees ? trees.base.root : baseDir, trees ? trees.head.root : headDir) : new Set();
232
285
 
233
286
  const bounded = boundBaseline(noBaseline ? null : loadBaseline(policyRoot), policy.waivers.maxDays);
234
287
  applyBaseline(after, bounded.baseline, { now });
@@ -257,20 +310,22 @@ function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compile
257
310
  reasons.push(`coverage is incomplete${sets.length ? ` in ${sets.join(', ')}` : ''}${policy.coverage === 'block' ? ', and the policy says block, so what was not read cannot be told from what is clean' : '; the policy says warn'}`);
258
311
  }
259
312
  const site = loadSite(policyRoot);
260
- const options = optionsCheck({ root: headDir, allow, site, policy, compiler });
313
+ const options = optionsCheck({ root: headDir, tree: trees?.head, allow, site, policy, compiler });
261
314
  reasons.push(...options.reasons);
262
315
  // A missing run-time check is a weaker build, not a defect: warn passes it relaxed. What the
263
316
  // policy forbids by name, and a check the change itself removed, still fail.
264
- const removed = ratchet && !compiler ? optionsRemoved(optionsCheck({ root: baseDir, allow: null, site, policy, compiler }), options) : [];
317
+ const removed = ratchet && !compiler ? optionsRemoved(optionsCheck({ root: baseDir, tree: trees?.base, allow: null, site, policy, compiler }), options) : [];
265
318
  reasons.push(...removed.slice(0, MAX_PROGRAM_REASONS));
266
- const optionsChanged = ratchet ? programOptionChanges(baseDir, headDir, allow) : [];
319
+ const optionsChanged = ratchet ? programOptionChanges(baseDir, headDir, allow, trees) : [];
267
320
  for (const c of optionsChanged.slice(0, MAX_PROGRAM_REASONS)) reasons.push(`${c.path}:${c.line} changes ${c.option} from ${c.base} to ${c.head}, which changes what the program's statements do without changing a statement`);
268
- const equivalence = ratchet && (equivalenceFiles.length || policy.requireEquivalence !== 'never')
321
+ const wantsEquivalence = ratchet && (equivalenceFiles.length || policy.requireEquivalence !== 'never');
322
+ const equivalence = wantsEquivalence && !trees
269
323
  ? checkEquivalence({ baseDir, headDir, allow, files: equivalenceFiles, allowed, mode: policy.requireEquivalence, ...(refs || {}), systemDirs: copylibs, env })
270
- : null;
324
+ : wantsEquivalence ? { problems: [], required: null, programs: [], mode: policy.requireEquivalence, notRun: 'equivalence is not run over a PDS export' } : null;
271
325
  if (equivalence) {
272
326
  reasons.push(...equivalence.problems);
273
- if (equivalence.required === null) reasons.push('the policy requires equivalence for machine-authored changes, and whether this one is could not be read from the commits');
327
+ if (equivalence.notRun) reasons.push(`${equivalence.notRun}, and the policy or --equivalence asks for it`);
328
+ else if (equivalence.required === null) reasons.push('the policy requires equivalence for machine-authored changes, and whether this one is could not be read from the commits');
274
329
  if (equivalence.required) for (const p of equivalence.programs.filter((x) => !x.ok).slice(0, MAX_PROGRAM_REASONS)) reasons.push(`${p.path}: ${p.because}, and the policy requires equivalence${equivalence.mode === 'machineAuthored' ? ' for a machine-authored change' : ''}`);
275
330
  }
276
331
  const relaxOptions = policy.options === 'warn' && options.ok !== true && !options.forbidden && !removed.length;
@@ -288,7 +343,7 @@ function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compile
288
343
  const verdict = decided.includes(false) ? 'fail' : decided.includes(null) ? 'undecided' : 'pass';
289
344
  let compiled = null;
290
345
  if (ironworkPath && verdict === 'pass') {
291
- const iw = ironworkCompile(ironworkPath, { root: headDir, allow, copylibs, env });
346
+ const iw = ironworkCompile(ironworkPath, { root: headDir, tree: trees?.head, allow, copylibs, env });
292
347
  compiled = iw.compiled;
293
348
  checks.compile = iw.check;
294
349
  reasons.push(...iw.reasons);
@@ -333,23 +388,26 @@ function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compile
333
388
  byTier,
334
389
  coverageIncomplete: incomplete,
335
390
  ...(after.summary.baseline && after.summary.baseline.problems ? { baselineProblems: after.summary.baseline.problems } : {}),
391
+ ...(trees ? { pdsExport: { ...(trees.base ? { base: trees.base.export } : {}), head: trees.head.export } } : {}),
336
392
  },
337
393
  };
338
- const hashes = sourceHashes(headDir, allow);
394
+ const hashes = sourceHashes(headDir, allow, trees?.head);
339
395
  return { doc, report: after, policy, setBy, hashes, add: options.add, compilerPath };
340
396
  }
341
397
 
342
398
  // The watched options in force changed by the change, per program present on both sides.
343
- function programOptionChanges(baseDir, headDir, allow) {
399
+ function programOptionChanges(baseDir, headDir, allow, trees = null) {
344
400
  const out = [];
345
- const files = directoryTree(headDir).list().filter((p) => isProgram(p) && (!allow || allow.has(p))).sort();
401
+ const headTree = trees ? trees.head : directoryTree(headDir);
402
+ const baseTree = trees ? trees.base : directoryTree(baseDir);
403
+ const files = headTree.list().filter((p) => isProgram(p) && (!allow || allow.has(p))).sort();
346
404
  for (const head of files) {
347
- const path = relPath(headDir, head);
348
- const base = join(baseDir, path);
349
- if (!existsSync(base)) continue;
405
+ const path = headTree.rel(head);
406
+ const base = resolve(baseTree.root, ...path.split('/'));
407
+ if (!(trees ? baseTree.contains(base) : existsSync(base))) continue;
350
408
  let before;
351
409
  let after;
352
- try { before = readSource(base).text; after = readSource(head).text; } catch { continue; }
410
+ try { before = baseTree.text(base).text; after = headTree.text(head).text; } catch { continue; }
353
411
  for (const c of optionChanges(before, after)) out.push({ path, ...c });
354
412
  }
355
413
  return out;
@@ -364,10 +422,19 @@ function policyChanged(baseDir, headDir) {
364
422
 
365
423
  // Runs the gate over `repo`. `compiler` is the argument vector after `--`, or null. Returns
366
424
  // { doc, report, provenance, exit }; throws with code EBUILD when it could not run.
367
- export function build(repo, { base = null, head = null, policy = null, noBaseline = false, compiler = null, ironwork = null, advisoryFeeds = null, copylibs = [], equivalence = [], allowed = null, now = new Date().toISOString(), env = process.env } = {}) {
425
+ export function build(repo, { base = null, head = null, policy = null, noBaseline = false, compiler = null, ironwork = null, advisoryFeeds = null, copylibs = [], equivalence = [], allowed: given = null, allowedSigners = null, now = new Date().toISOString(), env = process.env, pdsExport = false, precompile = false } = {}) {
368
426
  const floor = policy ? floorPolicy(policy, repo) : null;
369
427
  if (floor && floor.problems.length) throw refusal(floor.problems.join('; '));
428
+ let allowed = given;
429
+ if (allowedSigners) {
430
+ const path = resolve(allowedSigners);
431
+ if (resolvesInside(repo, path)) throw refusal(`--allowed-signers ${path} is inside the repository; signers the change can edit vouch for nothing`);
432
+ allowed = parseAllowedSigners(readFileSync(path, 'utf8'));
433
+ }
370
434
  if (compiler && ironwork) throw refusal('one compiler per build: -- names one and --ironwork another');
435
+ if (precompile && !compiler) throw refusal('--precompile translates the programs a compiler command names, so it needs -- and the compiler');
436
+ if (precompile && !compilerOf(compiler[0])) throw refusal(`--precompile checks a translation with cobc or gcobol -fsyntax-only; for ${printable(compiler[0], 60)} it cannot`);
437
+ if (pdsExport && compiler) throw refusal('a PDS export is checked with --ironwork; the compiler after -- reads the files of a repository, not the members of an export');
371
438
  let compilerPath = null;
372
439
  if (compiler) {
373
440
  if (!compiler.length) throw refusal('nothing follows --, so there is no compiler to run');
@@ -381,7 +448,7 @@ export function build(repo, { base = null, head = null, policy = null, noBaselin
381
448
  if (!found.path) throw refusal(`--ironwork: ${found.why}`);
382
449
  ironworkPath = found.path;
383
450
  }
384
- const run = (baseDir, headDir, allow, policyRoot) => judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compiler, compilerPath, ironworkPath, env, now, advisoryFeeds, copylibs, equivalenceFiles: equivalence, allowed, refs: base ? { repo, base, head: head || 'HEAD' } : null });
451
+ const run = (baseDir, headDir, allow, policyRoot) => judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compiler, compilerPath, ironworkPath, env, now, advisoryFeeds, copylibs, equivalenceFiles: equivalence, allowed, refs: base ? { repo, base, head: head || 'HEAD' } : null, pdsExport });
385
452
  const judged = base
386
453
  ? withRefs(repo, base, head, {}, (baseDir, headDir, scoped) => {
387
454
  const r = run(baseDir, headDir, scoped.allow || null, baseDir);
@@ -399,7 +466,20 @@ export function build(repo, { base = null, head = null, policy = null, noBaselin
399
466
  if (doc.checks.compile === false) exit = BUILD_EXIT.compilerFailed;
400
467
  else if (doc.checks.compile === null && !doc.relaxed.includes('compile')) exit = BUILD_EXIT.undecided;
401
468
  }
402
- if (compiler && doc.verdict === 'pass') {
469
+ // A program the compiler refuses once its EXEC SQL and CICS are translated stops the build before
470
+ // the caller's compiler runs, as a fail stops it.
471
+ if (precompile && doc.verdict === 'pass') {
472
+ const checked = precompileCheck({ compilerPath, argv: compiler.slice(1), repo, copylibs, env });
473
+ doc.precompiled = checked;
474
+ const refused = checked.programs.filter((p) => !p.ok);
475
+ doc.checks.precompile = refused.length === 0;
476
+ if (refused.length) {
477
+ exit = BUILD_EXIT.compilerFailed;
478
+ doc.reasons.push(`${refused.length} of ${checked.programs.length} program(s) holding EXEC SQL or CICS do not compile once precompiled`);
479
+ for (const p of refused.slice(0, MAX_PROGRAM_REASONS)) doc.reasons.push(`${p.path}: ${(p.errors || [])[0] || 'the compiler exited non-zero'}`);
480
+ }
481
+ }
482
+ if (compiler && doc.verdict === 'pass' && doc.checks.precompile !== false) {
403
483
  const argv = [...compiler.slice(1), ...judged.add];
404
484
  // The compiler's own output goes to standard error: standard output carries the document.
405
485
  const r = spawnSync(compilerPath, argv, { stdio: ['ignore', 2, 2], env, windowsHide: true });
@@ -6,29 +6,30 @@ import { RULE_SETS, ALL_RULES } from './kernel/registry.mjs';
6
6
  import { EVIDENCE } from './kernel/findings.mjs';
7
7
  import { FINGERPRINT_VERSION } from './kernel/identity.mjs';
8
8
  import { PROGRAM_EXT, COPY_EXT, JCL_EXT, BMS_EXT } from './sources.mjs';
9
- import { FLOW_MODEL, SCHEMA_VERSION, TOOL_VERSION } from './version.mjs';
9
+ import { FLOW_MODEL, SCHEMA_VERSION, TOOL_VERSION, EXPLAIN_SCHEMA_VERSION, PARSE_SCHEMA_VERSION, BASELINE_RESULT_SCHEMA_VERSION, EVIDENCE_RESULT_SCHEMA_VERSION } from './version.mjs';
10
10
  import { BUILD_SCHEMA_VERSION, BUILD_EXIT } from './build.mjs';
11
11
  import { VERDICT_EXIT } from './gate.mjs';
12
12
  import { CHECKS, COMPILERS, DEFAULT_POLICY } from './policy.mjs';
13
13
  import { CLASSES, TIERS } from './consequence.mjs';
14
14
  import { toolRevision } from './revision.mjs';
15
+ import { IRONWORK_FORMATS, IRONWORK_MINIMUM, NOT_RUN_ABENDS, RUN_ENDINGS } from './ironwork-ids.mjs';
15
16
 
16
17
  export const CAPABILITIES_SCHEMA_VERSION = 1;
17
18
 
18
19
  // Each command, what it takes and what it writes.
19
20
  export const COMMANDS = {
20
- scan: { args: ['<path>'], options: ['--format', '--out', '--repos', '--only', '--quiet', '--full-trace', '--all-routes', '--baseline', '--no-baseline', '--advisories', '--copylib', '--pds-export', '--evidence'], documents: ['cobolwork', 'sarif'] },
21
+ scan: { args: ['<path>'], options: ['--format', '--out', '--repos', '--only', '--quiet', '--full-trace', '--all-routes', '--baseline', '--no-baseline', '--advisories', '--copylib', '--pds-export', '--mvs38-forms', '--no-mvs38-forms', '--hlasm-optable', '--evidence'], documents: ['cobolwork', 'sarif'] },
21
22
  flow: { args: ['<path>'], options: ['--out', '--repos', '--quiet', '--full-trace', '--all-routes', '--baseline', '--no-baseline', '--copylib', '--evidence'], documents: ['cobolwork-flow'] },
22
23
  inventory: { args: ['<path>'], options: ['--out', '--quiet', '--copylib', '--evidence'], documents: ['cobolwork-inventory'] },
23
24
  parse: { args: ['<file>'], options: ['--out'], documents: ['cobolwork-parse'] },
24
25
  sbom: { args: ['<path>'], options: ['--name', '--out', '--quiet', '--copylib', '--evidence'], documents: ['cyclonedx'] },
25
- diff: { args: ['<repo>'], options: ['--base', '--head', '--format', '--out', '--only', '--quiet', '--full-trace', '--copylib', '--evidence'], documents: ['cobolwork-diff', 'sarif'] },
26
+ diff: { args: ['<repo>'], options: ['--base', '--head', '--format', '--out', '--only', '--quiet', '--full-trace', '--copylib', '--pds-export', '--evidence'], documents: ['cobolwork-diff', 'sarif'] },
26
27
  baseline: { args: ['<path>'], options: ['--reason', '--who', '--expires', '--action', '--rule', '--out', '--repos', '--only', '--advisories', '--copylib', '--evidence'], documents: ['cobolwork-baseline'] },
27
28
  tui: { args: ['[path]'], options: ['--report', '--keys', '--baseline', '--no-baseline', '--advisories', '--copylib'], documents: [] },
28
29
  explain: { args: ['<path>', '<fingerprint>'], options: ['--report', '--out', '--baseline', '--no-baseline', '--advisories', '--copylib'], documents: ['cobolwork-explain'] },
29
30
  gate: { args: ['<repo>'], options: ['--base', '--head', '--target', '--cobc', '--exit-code', '--target-only', '--out', '--evidence'], documents: ['cobolwork-gate'] },
30
- build: { args: ['<repo>', '[-- <compiler> <arg>...]'], options: ['--base', '--head', '--policy', '--provenance', '--provenance-format', '--artifact', '--equivalence', '--allowed-signers', '--ironwork', '--advisories', '--copylib', '--no-baseline', '--format', '--out', '--evidence'], documents: ['cobolwork-build', 'cobolwork-build-provenance', 'sarif'] },
31
- evidence: { args: ['verify|seal|anchor|sign', '[statement]'], options: ['--evidence', '--ssh-key', '--signer', '--expect-key', '--allowed-signers', '--anchor-git', '--ref', '--push', '--max-unsealed', '--tsq', '--out', '--quiet'], documents: ['cobolwork-evidence'] },
31
+ build: { args: ['<repo>', '[-- <compiler> <arg>...]'], options: ['--base', '--head', '--policy', '--provenance', '--provenance-format', '--artifact', '--equivalence', '--allowed-signers', '--ironwork', '--precompile', '--advisories', '--copylib', '--pds-export', '--no-baseline', '--format', '--out', '--evidence'], documents: ['cobolwork-build', 'cobolwork-build-provenance', 'sarif'] },
32
+ evidence: { args: ['verify|seal|anchor|sign', '[statement]'], options: ['--evidence', '--ssh-key', '--signer', '--expect-key', '--allowed-signers', '--anchor-git', '--ref', '--anchor-pin', '--push', '--max-unsealed', '--tsq', '--tsr', '--tsa-ca', '--cosign-bundle', '--cosign-key', '--certificate-identity', '--certificate-oidc-issuer', '--trusted-root', '--insecure-ignore-tlog', '--out', '--quiet'], documents: ['cobolwork-evidence'] },
32
33
  capabilities: { args: [], options: ['--json'], documents: ['cobolwork-capabilities'] },
33
34
  };
34
35
 
@@ -43,7 +44,6 @@ export function capabilities() {
43
44
  toolRevision: toolRevision(),
44
45
  commands: COMMANDS,
45
46
  globalOptions: GLOBAL_OPTIONS,
46
- // A document whose schemaVersion is null carries none yet; a reader should key on `tool`.
47
47
  documents: {
48
48
  cobolwork: SCHEMA_VERSION,
49
49
  'cobolwork-flow': SCHEMA_VERSION,
@@ -53,10 +53,10 @@ export function capabilities() {
53
53
  'cobolwork-build': BUILD_SCHEMA_VERSION,
54
54
  'cobolwork-build-provenance': BUILD_SCHEMA_VERSION,
55
55
  'cobolwork-capabilities': CAPABILITIES_SCHEMA_VERSION,
56
- 'cobolwork-explain': null,
57
- 'cobolwork-baseline': null,
58
- 'cobolwork-parse': null,
59
- 'cobolwork-evidence': null,
56
+ 'cobolwork-explain': EXPLAIN_SCHEMA_VERSION,
57
+ 'cobolwork-baseline': BASELINE_RESULT_SCHEMA_VERSION,
58
+ 'cobolwork-parse': PARSE_SCHEMA_VERSION,
59
+ 'cobolwork-evidence': EVIDENCE_RESULT_SCHEMA_VERSION,
60
60
  cyclonedx: '1.6',
61
61
  sarif: '2.1.0',
62
62
  },
@@ -68,7 +68,7 @@ export function capabilities() {
68
68
  jcl: JCL_EXT,
69
69
  bms: BMS_EXT,
70
70
  extensionless: 'sniffed by content: a file with no extension is classified by what it holds',
71
- pdsExport: 'scan --pds-export: every member classified by content, whatever extension the download gave it; text, --binary and --record downloads',
71
+ pdsExport: 'scan --pds-export: every member classified by content, whatever extension the download gave it; text, --binary and --record downloads; XMIT files and IEBCOPY unloads of a PDS or a sequential data set',
72
72
  zowe: ['zowe.config.json', 'zowe.config.user.json', '.mcp.json', '.vscode/mcp.json', '.cursor/mcp.json', '.claude/settings.json', '.claude/settings.local.json', '.gemini/settings.json', 'claude_desktop_config.json', '.vscode/settings.json', 'systems.json under a mock directory'],
73
73
  },
74
74
  evidenceKinds: Object.keys(EVIDENCE),
@@ -81,5 +81,7 @@ export function capabilities() {
81
81
  exit: { ...BUILD_EXIT, couldNotRun: 2 },
82
82
  },
83
83
  gate: { exit: { ...VERDICT_EXIT, couldNotRun: 2 } },
84
+ // The ironwork releases cobolwork reads: from `minimum` on, each format from the release it names.
85
+ ironwork: { minimum: IRONWORK_MINIMUM, formats: IRONWORK_FORMATS, runEndings: RUN_ENDINGS, notRunAbends: NOT_RUN_ABENDS },
84
86
  };
85
87
  }