@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.
- package/README.md +66 -14
- package/bin/cobolwork.mjs +126 -9
- package/lib/arith.mjs +235 -0
- package/lib/baseline.mjs +5 -0
- package/lib/build.mjs +112 -30
- package/lib/capabilities.mjs +12 -7
- package/lib/card.mjs +18 -0
- package/lib/cics-commands.mjs +136 -125
- package/lib/consequence.mjs +29 -0
- package/lib/control.mjs +1169 -91
- package/lib/csd.mjs +17 -2
- package/lib/dataflow.mjs +276 -126
- package/lib/embedded-sql.mjs +110 -0
- package/lib/enterprise-options.mjs +255 -0
- package/lib/equivalence.mjs +124 -0
- package/lib/evidence/cli.mjs +65 -0
- package/lib/evidence/journal.mjs +94 -0
- package/lib/evidence/record.mjs +110 -0
- package/lib/evidence/run.mjs +111 -0
- package/lib/evidence/seal.mjs +177 -0
- package/lib/evidence/slsa.mjs +64 -0
- package/lib/evidence/sshsig.mjs +172 -0
- package/lib/evidence/store.mjs +170 -0
- package/lib/evidence/verify.mjs +167 -0
- package/lib/explain.mjs +9 -5
- package/lib/exploitability.mjs +263 -0
- package/lib/ftp.mjs +136 -0
- package/lib/ironwork.mjs +109 -0
- package/lib/jcl.mjs +27 -7
- package/lib/kernel/findings.mjs +11 -0
- package/lib/kernel/registry.mjs +4 -0
- package/lib/layout.mjs +202 -0
- package/lib/option-diff.mjs +49 -0
- package/lib/options.mjs +109 -46
- package/lib/parser.mjs +80 -209
- package/lib/policy.mjs +8 -3
- package/lib/precompile.mjs +5 -84
- package/lib/reach.mjs +29 -12
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +1 -0
- package/lib/sbom.mjs +235 -0
- package/lib/scan.mjs +93 -27
- package/lib/sets/cics.mjs +119 -2
- package/lib/sets/copybook.mjs +2 -1
- package/lib/sets/flow.mjs +82 -2
- package/lib/sets/hidden.mjs +4 -8
- package/lib/sets/jcl.mjs +70 -106
- package/lib/sets/recon.mjs +2 -1
- package/lib/sets/semantics.mjs +377 -0
- package/lib/sets/web.mjs +34 -10
- package/lib/sets/zowe.mjs +246 -0
- package/lib/sources.mjs +10 -0
- package/lib/tui/app.mjs +31 -13
- package/lib/tui/model.mjs +17 -2
- package/lib/utilities.mjs +229 -15
- package/lib/verify.mjs +128 -0
- package/package.json +1 -1
- package/rules/compliance-dora.json +507 -0
- package/rules/compliance-ffiec.json +507 -0
- package/rules/compliance-nist80053.json +507 -0
- 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
|
|
5
|
-
// was confirmed by compiling. Nothing here is taken from
|
|
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
|
|
47
|
-
// gate cannot read counts as absent, which fails closed.
|
|
48
|
-
const
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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) &&
|
|
264
|
-
const found = commandsOf(shellTokens(inner, dialect)).flatMap((ws) =>
|
|
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
|
|
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
|
|
283
|
-
if (
|
|
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
|
|
332
|
-
// assigns substituted: [{ line, args, dir? }]. `kind` is make, sh, yaml, docker,
|
|
333
|
-
// A command whose options come from a variable set elsewhere keeps the
|
|
334
|
-
// option; one that names no source - cobc --version, which cobc -
|
|
335
|
-
|
|
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
|
|
407
|
-
// command and its arguments, read as its shell reads them, with the variants it gives for
|
|
408
|
-
// Linux and macOS; it runs in the workspace folder, or in its options.cwd. ${workspaceFolder}
|
|
409
|
-
// folder, and a variable that names the open file stands for any file.
|
|
410
|
-
export function
|
|
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' ||
|
|
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,
|
|
447
|
-
//
|
|
448
|
-
|
|
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
|
|
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 (
|
|
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
|
|
507
|
-
const r =
|
|
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:
|
|
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
|
}
|