@portll/cobolwork 0.2.76 → 0.2.117

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 (52) hide show
  1. package/README.md +66 -14
  2. package/bin/cobolwork.mjs +126 -9
  3. package/lib/arith.mjs +235 -0
  4. package/lib/baseline.mjs +5 -0
  5. package/lib/build.mjs +112 -30
  6. package/lib/capabilities.mjs +12 -7
  7. package/lib/consequence.mjs +29 -0
  8. package/lib/control.mjs +1052 -91
  9. package/lib/csd.mjs +17 -2
  10. package/lib/dataflow.mjs +245 -78
  11. package/lib/enterprise-options.mjs +255 -0
  12. package/lib/equivalence.mjs +124 -0
  13. package/lib/evidence/cli.mjs +65 -0
  14. package/lib/evidence/journal.mjs +85 -0
  15. package/lib/evidence/record.mjs +110 -0
  16. package/lib/evidence/run.mjs +111 -0
  17. package/lib/evidence/seal.mjs +177 -0
  18. package/lib/evidence/slsa.mjs +64 -0
  19. package/lib/evidence/sshsig.mjs +172 -0
  20. package/lib/evidence/store.mjs +170 -0
  21. package/lib/evidence/verify.mjs +167 -0
  22. package/lib/explain.mjs +4 -1
  23. package/lib/exploitability.mjs +263 -0
  24. package/lib/ironwork.mjs +111 -0
  25. package/lib/kernel/findings.mjs +11 -0
  26. package/lib/kernel/registry.mjs +4 -0
  27. package/lib/option-diff.mjs +49 -0
  28. package/lib/options.mjs +109 -46
  29. package/lib/parser.mjs +29 -7
  30. package/lib/policy.mjs +8 -3
  31. package/lib/precompile.mjs +4 -0
  32. package/lib/reach.mjs +29 -12
  33. package/lib/revision.json +1 -1
  34. package/lib/sarif.mjs +1 -0
  35. package/lib/sbom.mjs +147 -0
  36. package/lib/scan.mjs +93 -27
  37. package/lib/sets/cics.mjs +114 -0
  38. package/lib/sets/flow.mjs +82 -2
  39. package/lib/sets/jcl.mjs +67 -0
  40. package/lib/sets/semantics.mjs +377 -0
  41. package/lib/sets/web.mjs +34 -10
  42. package/lib/sets/zowe.mjs +220 -0
  43. package/lib/sources.mjs +10 -0
  44. package/lib/tui/app.mjs +31 -13
  45. package/lib/tui/model.mjs +17 -2
  46. package/lib/utilities.mjs +85 -13
  47. package/lib/verify.mjs +128 -0
  48. package/package.json +1 -1
  49. package/rules/compliance-dora.json +507 -0
  50. package/rules/compliance-ffiec.json +507 -0
  51. package/rules/compliance-nist80053.json +507 -0
  52. package/schema/cobolwork.policy.schema.json +2 -1
package/lib/baseline.mjs CHANGED
@@ -123,6 +123,11 @@ export function applyBaseline(report, baseline, { now = new Date().toISOString()
123
123
  s.byRule = tally(open);
124
124
  if (s.bySeverity) { s.bySeverity = {}; for (const f of open) s.bySeverity[f.sev] = (s.bySeverity[f.sev] || 0) + 1; }
125
125
  if (s.byEvidence) { s.byEvidence = {}; for (const f of open) s.byEvidence[f.evidence] = (s.byEvidence[f.evidence] || 0) + 1; }
126
+ if (s.byExploitability) {
127
+ const by = Object.fromEntries(Object.keys(s.byExploitability).map((k) => [k, 0]));
128
+ for (const f of open) if (f.exploitability) by[f.exploitability.verdict]++;
129
+ s.byExploitability = by;
130
+ }
126
131
  s.baseline = {
127
132
  path: baseline.path,
128
133
  source: baseline.source,
package/lib/build.mjs CHANGED
@@ -2,6 +2,8 @@
2
2
  // The build gate, specified in docs/spec/build-gate.md: scan, apply the policy and the baseline,
3
3
  // hold the compiler's options to the policy, and run the compiler only on a pass. Every check is the
4
4
  // engine's reading of the tree; nothing here asks a model anything.
5
+ import { checkEquivalence } from './equivalence.mjs';
6
+ import { optionChanges } from './option-diff.mjs';
5
7
  import { createHash } from 'node:crypto';
6
8
  import { spawnSync } from 'node:child_process';
7
9
  import { existsSync, readFileSync, statSync } from 'node:fs';
@@ -19,8 +21,10 @@ import { isProgram, isJcl, isSource, readSource, relPath } from './sources.mjs';
19
21
  import { parseJclFile } from './jcl.mjs';
20
22
  import { POLICY_FILE, treePolicy, floorPolicy, combinePolicies, canonical, policyHash } from './policy.mjs';
21
23
  import { classesOf, tierOf, TIERS, TIER_RANK } from './consequence.mjs';
22
- import { optionCards, enterpriseChecks, cobcChecks, scriptedCobcChecks, cobcInvocations, cobcTasks, compileStepOptions, isCobc } from './options.mjs';
24
+ import { optionCards, enterpriseChecks, compilerChecks, scriptedChecks, compilerInvocations, compilerTasks, compileStepOptions, compilerOf } from './options.mjs';
23
25
  import { FLOW_MODEL, TOOL_VERSION } from './version.mjs';
26
+ import { checkWithIronwork, ironworkVerdict, ironworkReasons, ironworkVersion, listed } from './ironwork.mjs';
27
+ import { buildFileIndex } from './parser.mjs';
24
28
 
25
29
  export const BUILD_SCHEMA_VERSION = 1;
26
30
  export const BUILD_EXIT = { pass: 0, fail: 1, undecided: 3, compilerFailed: 4 };
@@ -68,8 +72,8 @@ export function blockingReason(f, { policy, ratchet, introduced, renewed }) {
68
72
  return setting === 'block' ? 'rule' : null;
69
73
  }
70
74
 
71
- // Build scripts that can run cobc: a Makefile, a shell or batch script, a CI workflow, a Dockerfile,
72
- // an editor's task file, or a small script with no extension.
75
+ // Build scripts that can run cobc or gcobol: a Makefile, a shell or batch script, a CI workflow, a
76
+ // Dockerfile, an editor's task file, or a small script with no extension.
73
77
  const BUILD_SCRIPT = /(^|\/)(makefile|gnumakefile|dockerfile|[^/]*\.(mk|sh|bash|bat|cmd|ps1|ya?ml))$/i;
74
78
  const EDITOR_TASKS = /(^|\/)\.vscode\/tasks\.json$/i;
75
79
  const MAX_SCRIPT_BYTES = 1 << 20;
@@ -79,18 +83,18 @@ const kindOf = (rel) => (/(^|\/)(gnu)?makefile$|\.mk$/i.test(rel) ? 'make' : /(^
79
83
  : /\.(bat|cmd)$/i.test(rel) ? 'batch' : /\.ps1$/i.test(rel) ? 'powershell' : /\.ya?ml$/i.test(rel) ? 'yaml' : 'sh');
80
84
 
81
85
  function scriptInvocations(file, rel) {
82
- if (EDITOR_TASKS.test(rel)) return cobcTasks(readSource(file).text);
86
+ if (EDITOR_TASKS.test(rel)) return compilerTasks(readSource(file).text);
83
87
  const size = statSync(file).size;
84
- if (BUILD_SCRIPT.test(rel) && size <= MAX_SCRIPT_BYTES) return cobcInvocations(readSource(file).text, { kind: kindOf(rel) });
88
+ if (BUILD_SCRIPT.test(rel) && size <= MAX_SCRIPT_BYTES) return compilerInvocations(readSource(file).text, { kind: kindOf(rel) });
85
89
  if (!extname(file) && size <= MAX_BARE_SCRIPT_BYTES) {
86
90
  const bytes = readFileSync(file);
87
- return bytes.includes(0) ? [] : cobcInvocations(bytes.toString('latin1'), { kind: 'sh' });
91
+ return bytes.includes(0) ? [] : compilerInvocations(bytes.toString('latin1'), { kind: 'sh' });
88
92
  }
89
93
  return [];
90
94
  }
91
95
 
92
- // How the repository says it compiles its programs: the cobc commands its build scripts run, and
93
- // the Enterprise COBOL compile steps its JCL holds.
96
+ // How the repository says it compiles its programs: the cobc and gcobol commands its build scripts
97
+ // run, and the Enterprise COBOL compile steps its JCL holds.
94
98
  function declaredCompiles(root, files) {
95
99
  const invocations = [];
96
100
  const parsedJcl = [];
@@ -113,19 +117,20 @@ const memberName = (file) => basename(file).replace(/\.[^.]*$/, '').toUpperCase(
113
117
 
114
118
  // The options every program in the tree is compiled with, held to the policy. A program's options
115
119
  // come from the estate's declared defaults, the JCL step that compiles it and its own option cards;
116
- // where it has none of those, from the cobc commands the repository's build scripts run. `ok` is
117
- // false where a required check is not generated and null where no one can tell; `forbidden` counts
118
- // options the policy's forbid list names. `generated` maps each program and build script to the
119
- // checks it generates, for telling a change that removes one.
120
+ // where it has none of those, from the cobc and gcobol commands the repository's build scripts run.
121
+ // `ok` is false where a required check is not generated and null where no one can tell; `forbidden`
122
+ // counts options the policy's forbid list names. `generated` maps each program and build script to
123
+ // the checks it generates, for telling a change that removes one.
120
124
  function optionsCheck({ root, allow, site, policy, compiler }) {
121
125
  const required = policy.checks;
122
- if (compiler && !isCobc(compiler[0])) {
123
- return { ok: null, forbidden: 0, reasons: [`the gate reads the options of cobc and of Enterprise COBOL option cards; for ${printable(compiler[0], 60)} it cannot tell what reaches the compiler`], add: [], generated: new Map() };
126
+ const kind = compiler && compilerOf(compiler[0]);
127
+ if (compiler && !kind) {
128
+ 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() };
124
129
  }
125
130
  if (compiler) {
126
- const r = cobcChecks(compiler.slice(1), { required, forbid: policy.forbid.gnucobol });
131
+ const r = compilerChecks(kind, compiler.slice(1), { required, forbid: policy.forbid[kind] });
127
132
  const missing = r.checks.filter((c) => c.ok === false).map((c) => c.why);
128
- return { ok: missing.length || r.forbidden.length ? false : true, forbidden: r.forbidden.length, reasons: [...r.forbidden, ...missing], add: r.forbidden.length ? [] : r.add, compiler: 'gnucobol', generated: new Map() };
133
+ 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() };
129
134
  }
130
135
  const tree = directoryTree(root);
131
136
  const files = [...new Set(tree.list())].filter((p) => !allow || allow.has(p)).sort();
@@ -137,7 +142,7 @@ function optionsCheck({ root, allow, site, policy, compiler }) {
137
142
  const generated = new Map();
138
143
  const fail = (why) => { ok = false; reasons.push(printable(why, 300)); };
139
144
  const open = () => { if (ok !== false) ok = null; };
140
- // A script generates a check only if every cobc command in it does.
145
+ // A script generates a check only if every compiler command in it does.
141
146
  const record = (key, checks) => {
142
147
  const was = generated.get(key) || new Map();
143
148
  for (const c of checks) was.set(c.check, was.has(c.check) ? was.get(c.check) && c.ok : c.ok);
@@ -145,7 +150,7 @@ function optionsCheck({ root, allow, site, policy, compiler }) {
145
150
  };
146
151
 
147
152
  for (const inv of declared.invocations) {
148
- const r = scriptedCobcChecks(inv.args, { required, forbid: policy.forbid.gnucobol });
153
+ const r = scriptedChecks(inv.compiler, inv.args, { required, forbid: policy.forbid[inv.compiler] });
149
154
  record(inv.file, r.checks);
150
155
  forbidden += r.forbidden.length;
151
156
  for (const why of [...r.checks.filter((c) => c.ok === false).map((c) => c.why), ...r.forbidden]) fail(`${inv.file}:${inv.line}: ${why}`);
@@ -170,8 +175,9 @@ function optionsCheck({ root, allow, site, policy, compiler }) {
170
175
  }
171
176
  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`);
172
177
  return {
173
- ok, forbidden, generated, reasons: reasons.slice(0, MAX_PROGRAM_REASONS), add: [], compiler: declared.invocations.length ? 'gnucobol' : 'enterprise',
174
- programs: programs.length, sources: { cobcCommands: declared.invocations.length, compileSteps: [...declared.steps.values()].flat().length },
178
+ ok, forbidden, generated, reasons: reasons.slice(0, MAX_PROGRAM_REASONS), add: [],
179
+ compiler: declared.invocations.length ? [...new Set(declared.invocations.map((i) => i.compiler))].sort().join(',') : 'enterprise',
180
+ programs: programs.length, sources: { compilerCommands: declared.invocations.length, compileSteps: [...declared.steps.values()].flat().length },
175
181
  };
176
182
  }
177
183
 
@@ -195,7 +201,25 @@ function sourceHashes(root, allow) {
195
201
  .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
196
202
  }
197
203
 
198
- function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compiler, compilerPath, now, advisoryFeeds, copylibs }) {
204
+ // ironwork checks the head tree, which in ratchet mode exists only while judge runs, so it runs
205
+ // 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 });
208
+ const check = ironworkVerdict(result);
209
+ const libraries = [...buildFileIndex(root).copyDirs.map((d) => relPath(root, d) || '.'), ...copylibs];
210
+ return {
211
+ check,
212
+ reasons: ironworkReasons(result),
213
+ compiled: {
214
+ tool: 'ironwork', path, version: ironworkVersion(path, { env }),
215
+ argv: ['check', '<program>', ...libraries.flatMap((d) => ['-I', d])],
216
+ status: check === true ? 0 : check === false ? 12 : null,
217
+ ...listed(result),
218
+ },
219
+ };
220
+ }
221
+
222
+ function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compiler, compilerPath, ironworkPath, env, now, advisoryFeeds, copylibs, equivalenceFiles = [], allowed = null, refs = null }) {
199
223
  const repoPolicy = treePolicy(policyRoot);
200
224
  if (repoPolicy && repoPolicy.problems.length) throw refusal(`the repository policy does not validate: ${repoPolicy.problems.join('; ')}`);
201
225
  const { policy, setBy, ignored } = combinePolicies({ floor: floor ? floor.raw : null, repository: repoPolicy ? repoPolicy.raw : null });
@@ -216,7 +240,8 @@ function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compile
216
240
  const because = blockingReason(f, { policy, ratchet, introduced, renewed });
217
241
  return {
218
242
  fingerprint: f.fingerprint, rule: f.rule, tier: tierOf(f), sev: f.sev, classes: classesOf(f).filter((c) => policy.classes.includes(c)),
219
- path: f.path, line: Number(f.line) || 0, ...(ratchet ? { introduced: introduced.has(f) } : {}),
243
+ path: f.path, line: Number(f.line) || 0, ...(f.exploitability ? { exploitability: f.exploitability.verdict } : {}),
244
+ ...(ratchet ? { introduced: introduced.has(f) } : {}),
220
245
  blocking: because !== null, ...(because ? { because } : {}),
221
246
  };
222
247
  });
@@ -238,6 +263,16 @@ function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compile
238
263
  // policy forbids by name, and a check the change itself removed, still fail.
239
264
  const removed = ratchet && !compiler ? optionsRemoved(optionsCheck({ root: baseDir, allow: null, site, policy, compiler }), options) : [];
240
265
  reasons.push(...removed.slice(0, MAX_PROGRAM_REASONS));
266
+ const optionsChanged = ratchet ? programOptionChanges(baseDir, headDir, allow) : [];
267
+ 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')
269
+ ? checkEquivalence({ baseDir, headDir, allow, files: equivalenceFiles, allowed, mode: policy.requireEquivalence, ...(refs || {}), systemDirs: copylibs, env })
270
+ : null;
271
+ if (equivalence) {
272
+ 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');
274
+ 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
+ }
241
276
  const relaxOptions = policy.options === 'warn' && options.ok !== true && !options.forbidden && !removed.length;
242
277
  if (relaxOptions) reasons.push(options.ok === null ? 'the options of some programs are unknown, and the policy says warn' : 'some programs are built without a check the policy names, and the policy says warn');
243
278
 
@@ -246,10 +281,19 @@ function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compile
246
281
  coverage: incomplete && policy.coverage === 'block' ? null : true,
247
282
  options: options.forbidden || removed.length ? false : relaxOptions ? true : options.ok,
248
283
  compile: null,
284
+ ...(equivalence ? { equivalence: equivalence.problems.length ? false : equivalence.required === null ? null : equivalence.required ? equivalence.programs.every((p) => p.ok) : true } : {}),
249
285
  };
250
286
  const relaxed = [...(incomplete && policy.coverage === 'warn' ? ['coverage'] : []), ...(relaxOptions ? ['options'] : [])];
251
- const decided = [checks.findings, checks.coverage, checks.options];
287
+ const decided = [checks.findings, checks.coverage, checks.options, ...('equivalence' in checks ? [checks.equivalence] : [])];
252
288
  const verdict = decided.includes(false) ? 'fail' : decided.includes(null) ? 'undecided' : 'pass';
289
+ let compiled = null;
290
+ if (ironworkPath && verdict === 'pass') {
291
+ const iw = ironworkCompile(ironworkPath, { root: headDir, allow, copylibs, env });
292
+ compiled = iw.compiled;
293
+ checks.compile = iw.check;
294
+ reasons.push(...iw.reasons);
295
+ if (iw.check === null && policy.coverage === 'warn') relaxed.push('compile');
296
+ }
253
297
 
254
298
  const configurationChanged = ratchet ? [
255
299
  ...configurationChanges(baseDir, headDir).map((c) => c.file),
@@ -271,12 +315,15 @@ function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compile
271
315
  expired: expired.map((e) => ({ fingerprint: e.fingerprint, rule: e.rule, who: e.who, expired: e.expired })),
272
316
  overlong,
273
317
  optionsAdded: options.add,
318
+ optionsChanged,
319
+ ...(equivalence ? { equivalence } : {}),
274
320
  configurationChanged,
275
321
  policy: { sha256: policyHash(policy), setBy, ignored, applied: policy },
276
- compiled: null,
322
+ compiled,
277
323
  reasons: reasons.slice(0, MAX_REASONS).map((x) => printable(x, 400)),
278
324
  ...(reasons.length > MAX_REASONS ? { reasonsNotShown: reasons.length - MAX_REASONS } : {}),
279
- repositoryText: ['blocking[].path', 'findings[].path', 'waived[].who', 'waived[].reason', 'expired[].who', 'overlong[].who', 'reasons'],
325
+ repositoryText: ['blocking[].path', 'findings[].path', 'waived[].who', 'waived[].reason', 'expired[].who', 'overlong[].who', 'reasons',
326
+ ...(compiled ? ['compiled.failed[]', 'compiled.notModelled[]', 'compiled.unresolved[]', 'compiled.unrun[]'] : [])],
280
327
  summary: {
281
328
  flowModel: FLOW_MODEL,
282
329
  toolVersion: TOOL_VERSION,
@@ -292,6 +339,22 @@ function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compile
292
339
  return { doc, report: after, policy, setBy, hashes, add: options.add, compilerPath };
293
340
  }
294
341
 
342
+ // The watched options in force changed by the change, per program present on both sides.
343
+ function programOptionChanges(baseDir, headDir, allow) {
344
+ const out = [];
345
+ const files = directoryTree(headDir).list().filter((p) => isProgram(p) && (!allow || allow.has(p))).sort();
346
+ for (const head of files) {
347
+ const path = relPath(headDir, head);
348
+ const base = join(baseDir, path);
349
+ if (!existsSync(base)) continue;
350
+ let before;
351
+ let after;
352
+ try { before = readSource(base).text; after = readSource(head).text; } catch { continue; }
353
+ for (const c of optionChanges(before, after)) out.push({ path, ...c });
354
+ }
355
+ return out;
356
+ }
357
+
295
358
  function policyChanged(baseDir, headDir) {
296
359
  const bytes = (root) => { const p = join(root, POLICY_FILE); return existsSync(p) ? readFileSync(p) : null; };
297
360
  const was = bytes(baseDir);
@@ -301,9 +364,10 @@ function policyChanged(baseDir, headDir) {
301
364
 
302
365
  // Runs the gate over `repo`. `compiler` is the argument vector after `--`, or null. Returns
303
366
  // { doc, report, provenance, exit }; throws with code EBUILD when it could not run.
304
- export function build(repo, { base = null, head = null, policy = null, noBaseline = false, compiler = null, advisoryFeeds = null, copylibs = [], now = new Date().toISOString(), env = process.env } = {}) {
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 } = {}) {
305
368
  const floor = policy ? floorPolicy(policy, repo) : null;
306
369
  if (floor && floor.problems.length) throw refusal(floor.problems.join('; '));
370
+ if (compiler && ironwork) throw refusal('one compiler per build: -- names one and --ironwork another');
307
371
  let compilerPath = null;
308
372
  if (compiler) {
309
373
  if (!compiler.length) throw refusal('nothing follows --, so there is no compiler to run');
@@ -311,7 +375,13 @@ export function build(repo, { base = null, head = null, policy = null, noBaselin
311
375
  if (!found.path) throw refusal(`the compiler: ${found.why}`);
312
376
  compilerPath = found.path;
313
377
  }
314
- const run = (baseDir, headDir, allow, policyRoot) => judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compiler, compilerPath, now, advisoryFeeds, copylibs });
378
+ let ironworkPath = null;
379
+ if (ironwork) {
380
+ const found = findExecutable(ironwork, { repo, env });
381
+ if (!found.path) throw refusal(`--ironwork: ${found.why}`);
382
+ ironworkPath = found.path;
383
+ }
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 });
315
385
  const judged = base
316
386
  ? withRefs(repo, base, head, {}, (baseDir, headDir, scoped) => {
317
387
  const r = run(baseDir, headDir, scoped.allow || null, baseDir);
@@ -323,6 +393,12 @@ export function build(repo, { base = null, head = null, policy = null, noBaselin
323
393
 
324
394
  const { doc } = judged;
325
395
  let exit = BUILD_EXIT[doc.verdict];
396
+ // A program ironwork could not decide leaves the build undecided, as unread source does, unless the
397
+ // policy says warn for coverage; judge has then marked compile relaxed.
398
+ if (ironwork && doc.verdict === 'pass') {
399
+ if (doc.checks.compile === false) exit = BUILD_EXIT.compilerFailed;
400
+ else if (doc.checks.compile === null && !doc.relaxed.includes('compile')) exit = BUILD_EXIT.undecided;
401
+ }
326
402
  if (compiler && doc.verdict === 'pass') {
327
403
  const argv = [...compiler.slice(1), ...judged.add];
328
404
  // The compiler's own output goes to standard error: standard output carries the document.
@@ -338,14 +414,15 @@ export function build(repo, { base = null, head = null, policy = null, noBaselin
338
414
  return { doc, report: judged.report, provenance: provenanceRecord(judged, { base, head, env, copylibs }), exit };
339
415
  }
340
416
 
341
- function provenanceRecord({ doc, policy, setBy, hashes, compilerPath }, { base, head, env, copylibs }) {
417
+ function provenanceRecord({ doc, policy, setBy, hashes }, { base, head, env, copylibs }) {
342
418
  const epoch = env.SOURCE_DATE_EPOCH;
343
419
  const builtAt = epoch && /^\d+$/.test(epoch) ? new Date(Number(epoch) * 1000).toISOString() : null;
344
420
  let compiler = null;
345
421
  if (doc.compiled) {
346
422
  let digest = null;
347
- try { digest = sha256(readFileSync(compilerPath)); } catch { /* unreadable binary: recorded as null */ }
423
+ try { digest = sha256(readFileSync(doc.compiled.path)); } catch { /* unreadable binary: recorded as null */ }
348
424
  compiler = { path: doc.compiled.path, sha256: digest, argv: doc.compiled.argv, status: doc.compiled.status };
425
+ if (doc.compiled.tool === 'ironwork') Object.assign(compiler, { tool: 'ironwork', version: doc.compiled.version, programs: doc.compiled.programs, counts: doc.compiled.counts });
349
426
  }
350
427
  return {
351
428
  tool: 'cobolwork-build-provenance',
@@ -380,7 +457,12 @@ export function buildSummaryLine(doc) {
380
457
  if (advisory.length) parts.push(`advisory ${count(advisory)}`);
381
458
  if (doc.checks.coverage === null) parts.push('coverage incomplete');
382
459
  if (doc.checks.options !== true) parts.push(`options ${doc.checks.options === false ? 'refused' : 'unknown'}`);
383
- if (doc.compiled && doc.compiled.status !== 0) parts.push(`compiler exited ${doc.compiled.status}`);
460
+ if (doc.compiled && doc.compiled.tool === 'ironwork') {
461
+ const c = doc.compiled.counts;
462
+ if (c.failed) parts.push(`ironwork: ${c.failed} of ${doc.compiled.programs} programs do not compile`);
463
+ const open = c.notModelled + c.unresolved + c.unrun;
464
+ if (open) parts.push(`ironwork: ${open} program${open === 1 ? '' : 's'} not decided`);
465
+ } else if (doc.compiled && doc.compiled.status !== 0) parts.push(`compiler exited ${doc.compiled.status}`);
384
466
  return parts.join('; ');
385
467
  }
386
468
 
@@ -17,16 +17,18 @@ export const CAPABILITIES_SCHEMA_VERSION = 1;
17
17
 
18
18
  // Each command, what it takes and what it writes.
19
19
  export const COMMANDS = {
20
- scan: { args: ['<path>'], options: ['--format', '--out', '--repos', '--only', '--quiet', '--full-trace', '--baseline', '--no-baseline', '--advisories', '--copylib'], documents: ['cobolwork', 'sarif'] },
21
- flow: { args: ['<path>'], options: ['--out', '--repos', '--quiet', '--full-trace', '--baseline', '--no-baseline', '--copylib'], documents: ['cobolwork-flow'] },
22
- inventory: { args: ['<path>'], options: ['--out', '--quiet', '--copylib'], documents: ['cobolwork-inventory'] },
20
+ scan: { args: ['<path>'], options: ['--format', '--out', '--repos', '--only', '--quiet', '--full-trace', '--baseline', '--no-baseline', '--advisories', '--copylib', '--evidence'], documents: ['cobolwork', 'sarif'] },
21
+ flow: { args: ['<path>'], options: ['--out', '--repos', '--quiet', '--full-trace', '--baseline', '--no-baseline', '--copylib', '--evidence'], documents: ['cobolwork-flow'] },
22
+ inventory: { args: ['<path>'], options: ['--out', '--quiet', '--copylib', '--evidence'], documents: ['cobolwork-inventory'] },
23
23
  parse: { args: ['<file>'], options: ['--out'], documents: ['cobolwork-parse'] },
24
- diff: { args: ['<repo>'], options: ['--base', '--head', '--format', '--out', '--only', '--quiet', '--full-trace', '--copylib'], documents: ['cobolwork-diff', 'sarif'] },
25
- baseline: { args: ['<path>'], options: ['--reason', '--who', '--expires', '--action', '--rule', '--out', '--repos', '--only', '--advisories', '--copylib'], documents: ['cobolwork-baseline'] },
24
+ 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
+ baseline: { args: ['<path>'], options: ['--reason', '--who', '--expires', '--action', '--rule', '--out', '--repos', '--only', '--advisories', '--copylib', '--evidence'], documents: ['cobolwork-baseline'] },
26
27
  tui: { args: ['[path]'], options: ['--report', '--keys', '--baseline', '--no-baseline', '--advisories', '--copylib'], documents: [] },
27
28
  explain: { args: ['<path>', '<fingerprint>'], options: ['--report', '--out', '--baseline', '--no-baseline', '--advisories', '--copylib'], documents: ['cobolwork-explain'] },
28
- gate: { args: ['<repo>'], options: ['--base', '--head', '--target', '--cobc', '--exit-code', '--target-only', '--out'], documents: ['cobolwork-gate'] },
29
- build: { args: ['<repo>', '[-- <compiler> <arg>...]'], options: ['--base', '--head', '--policy', '--provenance', '--advisories', '--copylib', '--no-baseline', '--format', '--out'], documents: ['cobolwork-build', 'cobolwork-build-provenance', 'sarif'] },
29
+ 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'] },
30
32
  capabilities: { args: [], options: ['--json'], documents: ['cobolwork-capabilities'] },
31
33
  };
32
34
 
@@ -54,6 +56,8 @@ export function capabilities() {
54
56
  'cobolwork-explain': null,
55
57
  'cobolwork-baseline': null,
56
58
  'cobolwork-parse': null,
59
+ 'cobolwork-evidence': null,
60
+ cyclonedx: '1.6',
57
61
  sarif: '2.1.0',
58
62
  },
59
63
  identity: { version: FINGERPRINT_VERSION },
@@ -64,6 +68,7 @@ export function capabilities() {
64
68
  jcl: JCL_EXT,
65
69
  bms: BMS_EXT,
66
70
  extensionless: 'sniffed by content: a file with no extension is classified by what it holds',
71
+ 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'],
67
72
  },
68
73
  evidenceKinds: Object.keys(EVIDENCE),
69
74
  build: {
@@ -40,6 +40,13 @@ const SINK_CLASSES = {
40
40
  'arithmetic': [],
41
41
  'record-key': [],
42
42
  'log': [],
43
+ 'storage-length': [],
44
+ 'xml-document': [],
45
+ 'connection-target': [],
46
+ 'screen': [],
47
+ 'cics-sysid': [],
48
+ // A system-programming command acts with the region's authority on whatever resource it names.
49
+ 'cics-system-resource': [ESCALATES],
43
50
  };
44
51
 
45
52
  // Past a table or a reference with SSRANGE abending, a bad index stops the program instead of
@@ -50,6 +57,11 @@ const RULE_CLASSES = {
50
57
  // A variable names the program, and nothing says input reaches it: over the 500-repository corpus
51
58
  // every one read held a literal. Input that does reach it is *-to-cics-dynamic-transfer.
52
59
  'cics-transfer-to-variable-program': [],
60
+ // A failure carried on from: what follows depends on the program, and severity decides.
61
+ 'cics-condition-ignored': [],
62
+ // Disclosure of stored secrets and a weaker check; severity decides.
63
+ 'program-checks-stored-password': [],
64
+ 'password-case-folded-before-compare': [],
53
65
  'cics-signon-bypassed': [ESCALATES],
54
66
  'csd-defines-diagnostic-transaction': [ESCALATES],
55
67
  'csd-transaction-without-command-security': [ESCALATES],
@@ -59,6 +71,12 @@ const RULE_CLASSES = {
59
71
  'jcl-instream-credential': [ESCALATES],
60
72
  'web-uri-carries-a-credential': [ESCALATES],
61
73
  'log-writes-a-credential': [ESCALATES],
74
+ 'zowe-config-secret-in-clear': [ESCALATES],
75
+ 'zowe-mcp-password-in-config': [ESCALATES],
76
+ 'zowe-mock-credentials': [ESCALATES],
77
+ // An agent at the full tier submits jobs and runs commands as the user it logs on as.
78
+ 'zowe-mcp-tier-full': [ESCALATES],
79
+ 'zowe-mcp-tier-writes': [MUTATES],
62
80
  // A crafted source file compiled by an affected cobc runs in the build, with the build's authority.
63
81
  'build-pins-vulnerable-compiler': [ESCALATES],
64
82
  'build-pins-exploited-compiler': [ESCALATES],
@@ -87,8 +105,19 @@ const RULE_CLASSES = {
87
105
  'web-link-opens-without-noopener': [],
88
106
  'cics-listener-accepts-cleartext': [],
89
107
  'compile-undefined-name': [],
108
+ // A value the generated code gets wrong with no input driving it; severity decides.
109
+ 'binary-store-exceeds-picture-under-trunc-opt': [],
110
+ 'intermediate-result-loses-high-order-digits': [],
111
+ 'character-range-reverses-in-ascii': [],
90
112
  'log-writes-personal-data': [],
91
113
  'display-echoes-a-credential': [],
114
+ 'control-cards-from-dataset': [],
115
+ 'sort-exit-named': [],
116
+ 'tso-batch-runs-program': [],
117
+ 'zowe-config-tls-verify-off': [],
118
+ 'zowe-config-cleartext': [],
119
+ 'zowe-mcp-data-marking-off': [],
120
+ 'zowe-mcp-unpinned': [],
92
121
  };
93
122
 
94
123
  // Coverage and context assert no defect, so they carry no consequence.