@portll/cobolwork 0.2.76 → 0.2.140

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 (61) 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/card.mjs +18 -0
  8. package/lib/cics-commands.mjs +136 -125
  9. package/lib/consequence.mjs +29 -0
  10. package/lib/control.mjs +1169 -91
  11. package/lib/csd.mjs +17 -2
  12. package/lib/dataflow.mjs +276 -126
  13. package/lib/embedded-sql.mjs +110 -0
  14. package/lib/enterprise-options.mjs +255 -0
  15. package/lib/equivalence.mjs +124 -0
  16. package/lib/evidence/cli.mjs +65 -0
  17. package/lib/evidence/journal.mjs +94 -0
  18. package/lib/evidence/record.mjs +110 -0
  19. package/lib/evidence/run.mjs +111 -0
  20. package/lib/evidence/seal.mjs +177 -0
  21. package/lib/evidence/slsa.mjs +64 -0
  22. package/lib/evidence/sshsig.mjs +172 -0
  23. package/lib/evidence/store.mjs +170 -0
  24. package/lib/evidence/verify.mjs +167 -0
  25. package/lib/explain.mjs +9 -5
  26. package/lib/exploitability.mjs +263 -0
  27. package/lib/ftp.mjs +136 -0
  28. package/lib/ironwork.mjs +109 -0
  29. package/lib/jcl.mjs +27 -7
  30. package/lib/kernel/findings.mjs +11 -0
  31. package/lib/kernel/registry.mjs +4 -0
  32. package/lib/layout.mjs +202 -0
  33. package/lib/option-diff.mjs +49 -0
  34. package/lib/options.mjs +109 -46
  35. package/lib/parser.mjs +80 -209
  36. package/lib/policy.mjs +8 -3
  37. package/lib/precompile.mjs +5 -84
  38. package/lib/reach.mjs +29 -12
  39. package/lib/revision.json +1 -1
  40. package/lib/sarif.mjs +1 -0
  41. package/lib/sbom.mjs +235 -0
  42. package/lib/scan.mjs +93 -27
  43. package/lib/sets/cics.mjs +119 -2
  44. package/lib/sets/copybook.mjs +2 -1
  45. package/lib/sets/flow.mjs +82 -2
  46. package/lib/sets/hidden.mjs +4 -8
  47. package/lib/sets/jcl.mjs +70 -106
  48. package/lib/sets/recon.mjs +2 -1
  49. package/lib/sets/semantics.mjs +377 -0
  50. package/lib/sets/web.mjs +34 -10
  51. package/lib/sets/zowe.mjs +246 -0
  52. package/lib/sources.mjs +10 -0
  53. package/lib/tui/app.mjs +31 -13
  54. package/lib/tui/model.mjs +17 -2
  55. package/lib/utilities.mjs +229 -15
  56. package/lib/verify.mjs +128 -0
  57. package/package.json +1 -1
  58. package/rules/compliance-dora.json +507 -0
  59. package/rules/compliance-ffiec.json +507 -0
  60. package/rules/compliance-nist80053.json +507 -0
  61. package/schema/cobolwork.policy.schema.json +94 -16
package/lib/options.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  // Whether the options a program is compiled with generate the run-time checks a policy requires:
3
3
  // docs/spec/build-gate.md §7. Enterprise COBOL's options and what they mean are from IBM's
4
- // documentation; the GnuCOBOL flags are recorded in provenance/compiler-options.json with how each
5
- // was confirmed by compiling. Nothing here is taken from GnuCOBOL's source (spec §16).
4
+ // documentation; the GnuCOBOL and gcobol flags are recorded in provenance/compiler-options.json with
5
+ // how each was confirmed by compiling. Nothing here is taken from either compiler's source (spec §16).
6
6
  import { basename, posix } from 'node:path';
7
7
  import { PROGRAM_EXT } from './sources.mjs';
8
+ import { OPTION_SPELLINGS } from './enterprise-options.mjs';
8
9
 
9
10
  // Split on blanks and commas outside parentheses: SSRANGE(NOZLEN,ABD) is one option.
10
11
  export function optionTokens(text) {
@@ -43,13 +44,10 @@ const parse = (token) => {
43
44
  return m ? { name: m[1], subs: m[2] === undefined ? [] : optionTokens(m[2]) } : { name: token, subs: [] };
44
45
  };
45
46
 
46
- // Each family's spellings. An abbreviation IBM does not document is not accepted, so an option the
47
- // gate cannot read counts as absent, which fails closed.
48
- const FAMILIES = {
49
- SSRANGE: { on: ['SSRANGE', 'SSR'], off: ['NOSSRANGE', 'NOSSR'] },
50
- NUMCHECK: { on: ['NUMCHECK'], off: ['NONUMCHECK'] },
51
- PARMCHECK: { on: ['PARMCHECK', 'PC'], off: ['NOPARMCHECK', 'NOPC'] },
52
- };
47
+ // Each family's spellings, as IBM's option table gives them. A spelling the table does not hold is
48
+ // not accepted, so an option the gate cannot read counts as absent, which fails closed.
49
+ const spellingsOf = (option, off) => Object.keys(OPTION_SPELLINGS).filter((s) => OPTION_SPELLINGS[s][0] === option && OPTION_SPELLINGS[s][1] === off);
50
+ const FAMILIES = Object.fromEntries(['SSRANGE', 'NUMCHECK', 'PARMCHECK'].map((f) => [f, { on: spellingsOf(f, false), off: spellingsOf(f, true) }]));
53
51
  const FAMILY_OF_CHECK = { subscript: 'SSRANGE', 'reference-modification': 'SSRANGE', 'numeric-data': 'NUMCHECK', 'argument-length': 'PARMCHECK' };
54
52
 
55
53
  // Whether one family's setting abends on a failed check. A bare SSRANGE is SSRANGE(NOZLEN,ABD); a
@@ -121,7 +119,17 @@ const EXCEPTION_OF_CHECK = {
121
119
  'argument-length': 'EC-PROGRAM-ARG-MISMATCH',
122
120
  };
123
121
 
124
- export const isCobc = (command) => /^cobc(\.exe)?$/i.test(basename(String(command || '')));
122
+ // gcobol 16 refuses to compile with these enabled ("sorry, unimplemented"), and EC-ALL does not
123
+ // generate them.
124
+ const GCOBOL_UNIMPLEMENTED = new Set(['EC-DATA-INCOMPATIBLE', 'EC-PROGRAM-ARG-MISMATCH']);
125
+
126
+ // GnuCOBOL's cobc, or GCC's gcobol under its versioned or cross-compiler names.
127
+ export function compilerOf(command) {
128
+ const name = basename(String(command || '')).replace(PREFIX, '');
129
+ if (/^cobc(\.exe)?$/i.test(name)) return 'gnucobol';
130
+ if (/^(?:[a-z0-9_]+-)*gcobol(?:-\d+(?:\.\d+)*)?(\.exe)?$/i.test(name)) return 'gcobol';
131
+ return null;
132
+ }
125
133
 
126
134
  const exceptionName = (v) => {
127
135
  const u = String(v).toUpperCase();
@@ -251,17 +259,21 @@ const argsOf = (words) => {
251
259
  return out;
252
260
  };
253
261
 
254
- // The argument vector of each cobc command in one simple command. A command held in one word -
255
- // sh -c "…", an alias, a CMD's string - is read as a command line of its own.
256
- function cobcCommands(words, dialect, depth = 0) {
262
+ // Text that may run cobc or gcobol.
263
+ const NAMES_A_COMPILER = /cobc|gcobol/i;
264
+
265
+ // The compiler and argument vector of each cobc or gcobol command in one simple command. A command
266
+ // held in one word - sh -c "…", an alias, a CMD's string - is read as a command line of its own.
267
+ function compilerCommands(words, dialect, depth = 0) {
257
268
  const at = leadOf(words);
258
269
  if (at < 0 || MESSAGE.test(words[at].replace(PREFIX, ''))) return [];
259
270
  for (let i = at; i < words.length; i++) {
260
271
  const w = words[i];
261
- if (isCobc(w.replace(PREFIX, ''))) return [argsOf(words.slice(i + 1))];
272
+ const compiler = compilerOf(w);
273
+ if (compiler) return [{ compiler, args: argsOf(words.slice(i + 1)) }];
262
274
  const inner = w.replace(/^[A-Za-z_]\w*=/, '');
263
- if (depth < 2 && /\s/.test(inner) && /cobc/i.test(inner)) {
264
- const found = commandsOf(shellTokens(inner, dialect)).flatMap((ws) => cobcCommands(ws, dialect, depth + 1));
275
+ if (depth < 2 && /\s/.test(inner) && NAMES_A_COMPILER.test(inner)) {
276
+ const found = commandsOf(shellTokens(inner, dialect)).flatMap((ws) => compilerCommands(ws, dialect, depth + 1));
265
277
  if (found.length) return found;
266
278
  }
267
279
  }
@@ -272,15 +284,15 @@ function cobcCommands(words, dialect, depth = 0) {
272
284
  const changeDir = (dir, to) => (!to || /[$%~`]/.test(to) || /^(?:\/|[A-Za-z]:[\\/]|-$)/.test(to) ? null
273
285
  : posix.normalize(posix.join(dir || '.', to.replace(/\\/g, '/'))));
274
286
 
275
- // The cobc commands in a command line, each with the directory a cd before it on the line moved to.
276
- // `state.dir` carries a cd forward to later lines, for a script whose lines one shell runs.
287
+ // The compiler commands in a command line, each with the directory a cd before it on the line moved
288
+ // to. `state.dir` carries a cd forward to later lines, for a script whose lines one shell runs.
277
289
  function commandLine(text, dialect, state, line, out) {
278
290
  for (const words of commandsOf(Array.isArray(text) ? text.map((word) => ({ word })) : shellTokens(text, dialect))) {
279
291
  const at = leadOf(words);
280
292
  const lead = at >= 0 ? words[at].replace(PREFIX, '').toLowerCase() : '';
281
293
  if (lead === 'cd' || lead === 'pushd' || lead === 'set-location') { state.dir = changeDir(state.dir, words.slice(at + 1).find((w) => !/^-./.test(w))); continue; }
282
- for (const args of cobcCommands(words, dialect)) {
283
- if (cobcOperands(args).length) out.push({ line, args, ...(state.dir && state.dir !== '.' ? { dir: state.dir } : {}) });
294
+ for (const { compiler, args } of compilerCommands(words, dialect)) {
295
+ if (sourceOperands(args, compiler).length) out.push({ line, compiler, args, ...(state.dir && state.dir !== '.' ? { dir: state.dir } : {}) });
284
296
  }
285
297
  }
286
298
  }
@@ -328,11 +340,12 @@ function expanded(text, vars, kind) {
328
340
  return t;
329
341
  }
330
342
 
331
- // Every cobc command a build script runs that names a source file, with the variables the same file
332
- // assigns substituted: [{ line, args, dir? }]. `kind` is make, sh, yaml, docker, batch or powershell.
333
- // A command whose options come from a variable set elsewhere keeps the reference, which reads as no
334
- // option; one that names no source - cobc --version, which cobc - compiles nothing and is left out.
335
- export function cobcInvocations(text, { kind = 'sh' } = {}) {
343
+ // Every cobc or gcobol command a build script runs that names a source file, with the variables the
344
+ // same file assigns substituted: [{ line, compiler, args, dir? }]. `kind` is make, sh, yaml, docker,
345
+ // batch or powershell. A command whose options come from a variable set elsewhere keeps the
346
+ // reference, which reads as no option; one that names no source - cobc --version, which cobc -
347
+ // compiles nothing and is left out.
348
+ export function compilerInvocations(text, { kind = 'sh' } = {}) {
336
349
  const dialect = kind === 'batch' || kind === 'powershell' ? kind : 'sh';
337
350
  const vars = new Map();
338
351
  const continuation = { batch: /\^$/, powershell: /`$/ }[kind] || /\\$/;
@@ -403,11 +416,11 @@ function looseJson(text) {
403
416
  return JSON.parse(out.replace(/,(\s*[}\]])/g, '$1'));
404
417
  }
405
418
 
406
- // The cobc commands an editor's task file runs: VS Code's tasks.json. A task's command line is its
407
- // command and its arguments, read as its shell reads them, with the variants it gives for Windows,
408
- // Linux and macOS; it runs in the workspace folder, or in its options.cwd. ${workspaceFolder} is that
409
- // folder, and a variable that names the open file stands for any file.
410
- export function cobcTasks(text) {
419
+ // The cobc and gcobol commands an editor's task file runs: VS Code's tasks.json. A task's command
420
+ // line is its command and its arguments, read as its shell reads them, with the variants it gives for
421
+ // Windows, Linux and macOS; it runs in the workspace folder, or in its options.cwd. ${workspaceFolder}
422
+ // is that folder, and a variable that names the open file stands for any file.
423
+ export function compilerTasks(text) {
411
424
  let doc;
412
425
  try { doc = looseJson(text); } catch { return []; }
413
426
  const lines = String(text).split(/\r?\n/);
@@ -428,7 +441,7 @@ export function cobcTasks(text) {
428
441
  for (const [variant, dialect] of [[task, 'sh'], [task.linux, 'sh'], [task.osx, 'sh'], [task.windows, 'powershell']]) {
429
442
  if (!variant || typeof variant !== 'object') continue;
430
443
  const command = valueOf(variant.command);
431
- if (typeof command !== 'string' || !/cobc/i.test(`${command} ${JSON.stringify(variant.args || task.args || [])}`)) continue;
444
+ if (typeof command !== 'string' || !NAMES_A_COMPILER.test(`${command} ${JSON.stringify(variant.args || task.args || [])}`)) continue;
432
445
  const args = Array.isArray(variant.args) ? variant.args : Array.isArray(task.args) ? task.args : [];
433
446
  const cwd = (variant.options && variant.options.cwd) || (task.options && task.options.cwd);
434
447
  const dir = typeof cwd === 'string' ? changeDir(null, folder(cwd)) : null;
@@ -443,19 +456,25 @@ export function cobcTasks(text) {
443
456
  }
444
457
 
445
458
  // Options whose value is the next argument, which is not a source file: the output file, include and
446
- // library directories, a library, and options for the C compiler and the linker. Each was observed to
447
- // take the next argument (provenance/compiler-options.json).
448
- const TAKES_VALUE = new Set(['-o', '-I', '-L', '-l', '-A', '-Q', '-fec', '-fno-ec']);
459
+ // library directories, a library, options for the C compiler and the linker, and for gcobol a CDF
460
+ // name, a language, a dialect, a copybook extension and an included file. Each was observed to take
461
+ // the next argument (provenance/compiler-options.json). gcobol's -main takes none: it marks the source
462
+ // that follows it.
463
+ const TAKES_VALUE = {
464
+ gnucobol: new Set(['-o', '-I', '-L', '-l', '-A', '-Q', '-fec', '-fno-ec']),
465
+ gcobol: new Set(['-o', '-I', '-L', '-l', '-D', '-x', '-dialect', '-copyext', '-include', '-fcobol-exceptions', '-fno-cobol-exceptions']),
466
+ };
449
467
  const SOURCE_FILE = new RegExp(`(?:${PROGRAM_EXT.map((e) => e.replace('.', '\\.')).join('|')})$`, 'i');
450
468
  // A variable, a batch argument, a glob or find's {}: a file the command names only when it runs.
451
469
  const UNNAMED = /[$%*?!]|\{\}/;
452
470
 
453
- // The operands of a cobc command that name source files, as written.
454
- export function cobcOperands(args) {
471
+ // The operands of a cobc or gcobol command that name source files, as written.
472
+ export function sourceOperands(args, compiler = 'gnucobol') {
473
+ const takesValue = TAKES_VALUE[compiler];
455
474
  const out = [];
456
475
  for (let i = 0; i < args.length; i++) {
457
476
  const a = String(args[i]);
458
- if (TAKES_VALUE.has(a)) { i++; continue; }
477
+ if (takesValue.has(a)) { i++; continue; }
459
478
  if (a.startsWith('-')) continue;
460
479
  if (SOURCE_FILE.test(a) || UNNAMED.test(a)) out.push(a);
461
480
  }
@@ -486,11 +505,6 @@ export function cobcChecks(args, { required, forbid = [] }) {
486
505
  if (value === undefined) continue;
487
506
  set(exceptionName(value), !m[1], shown);
488
507
  }
489
- const forbidden = [];
490
- for (const entry of forbid) {
491
- const hit = args.find((a) => a === entry || String(a).startsWith(`${entry}=`));
492
- if (hit) forbidden.push(`${hit} is on the command line, and the policy forbids it`);
493
- }
494
508
  const add = [];
495
509
  const checks = required.map((check) => {
496
510
  if (state.get(check) === true) return { check, ok: true, why: 'turned on by the command line' };
@@ -499,14 +513,63 @@ export function cobcChecks(args, { required, forbid = [] }) {
499
513
  if (!add.includes(flag)) add.push(flag);
500
514
  return { check, ok: true, added: true, why: `added as ${flag}` };
501
515
  });
502
- return { checks, forbidden, add };
516
+ return { checks, forbidden: forbiddenOn(args, forbid), add };
503
517
  }
504
518
 
519
+ const forbiddenOn = (args, forbid) => forbid
520
+ .map((entry) => args.find((a) => a === entry || String(a).startsWith(`${entry}=`)))
521
+ .filter(Boolean).map((hit) => `${hit} is on the command line, and the policy forbids it`);
522
+
523
+ // A gcobol argument vector, as cobcChecks reads cobc's. -fcobol-exceptions enables each condition it
524
+ // lists; -fno-cobol-exceptions N withdraws the enabled conditions N covers, so after EC-BOUND it does
525
+ // not withdraw EC-BOUND-SUBSCRIPT, and it never stops a later enable. A check whose condition gcobol
526
+ // does not implement cannot be generated at all.
527
+ export function gcobolChecks(args, { required, forbid = [] }) {
528
+ let enabled = [];
529
+ const turnedOff = new Map();
530
+ const listed = (v) => String(v).split(',').map((n) => n.trim().toUpperCase()).filter(Boolean);
531
+ for (let i = 0; i < args.length; i++) {
532
+ const a = String(args[i]);
533
+ const m = /^-f(no-)?cobol-exceptions(?:=(.*))?$/.exec(a);
534
+ if (!m) continue;
535
+ const value = m[2] !== undefined ? m[2] : args[++i];
536
+ if (value === undefined) continue;
537
+ const shown = m[2] !== undefined ? a : `${a} ${value}`;
538
+ for (const name of listed(value)) {
539
+ for (const check of required) {
540
+ if (!covers(name, EXCEPTION_OF_CHECK[check])) continue;
541
+ if (m[1]) turnedOff.set(check, shown); else turnedOff.delete(check);
542
+ }
543
+ enabled = m[1] ? enabled.filter((e) => !covers(name, e)) : [...enabled, name];
544
+ }
545
+ }
546
+ const add = [];
547
+ const checks = required.map((check) => {
548
+ const condition = EXCEPTION_OF_CHECK[check];
549
+ if (GCOBOL_UNIMPLEMENTED.has(condition)) return { check, ok: false, why: `${check} needs ${condition}, which gcobol does not implement` };
550
+ if (enabled.some((e) => covers(e, condition))) return { check, ok: true, why: 'turned on by the command line' };
551
+ if (turnedOff.has(check)) return { check, ok: false, why: `${turnedOff.get(check)} turns off the ${check} check` };
552
+ const flag = `-fcobol-exceptions=${condition}`;
553
+ add.push(flag);
554
+ return { check, ok: true, added: true, why: `added as ${flag}` };
555
+ });
556
+ return { checks, forbidden: forbiddenOn(args, forbid), add };
557
+ }
558
+
559
+ const CHECKS_OF = { gnucobol: cobcChecks, gcobol: gcobolChecks };
560
+ const NOT_GIVEN = {
561
+ gnucobol: (check, c) => `${check} needs -fec=${c} or -debug, and the command gives neither`,
562
+ gcobol: (check, c) => `${check} needs -fcobol-exceptions=${c}, and the command does not give it`,
563
+ };
564
+
565
+ // The checks one compiler's argument vector generates: cobcChecks or gcobolChecks.
566
+ export const compilerChecks = (compiler, args, opts) => CHECKS_OF[compiler](args, opts);
567
+
505
568
  // The same, for a command the gate reads rather than runs: a check nothing turned on is missing.
506
- export function scriptedCobcChecks(args, opts) {
507
- const r = cobcChecks(args, opts);
569
+ export function scriptedChecks(compiler, args, opts) {
570
+ const r = compilerChecks(compiler, args, opts);
508
571
  return {
509
- checks: r.checks.map((c) => (c.added ? { check: c.check, ok: false, why: `${c.check} needs -fec=${EXCEPTION_OF_CHECK[c.check]} or -debug, and the command gives neither` } : c)),
572
+ checks: r.checks.map((c) => (c.added ? { check: c.check, ok: false, why: NOT_GIVEN[compiler](c.check, EXCEPTION_OF_CHECK[c.check]) } : c)),
510
573
  forbidden: r.forbidden,
511
574
  };
512
575
  }