@portll/cobolwork 0.6.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.
- package/README.md +49 -18
- package/STABILITY.md +76 -0
- package/bin/cobolwork.mjs +30 -7
- package/lib/bms.mjs +21 -7
- package/lib/build.mjs +108 -53
- package/lib/capabilities.mjs +9 -10
- package/lib/cics-commands.mjs +501 -9
- package/lib/compliance.mjs +11 -0
- package/lib/consequence.mjs +13 -0
- package/lib/control-workers.mjs +161 -0
- package/lib/control.mjs +52 -3
- package/lib/dataflow.mjs +176 -125
- package/lib/db2/cursor.mjs +15 -0
- package/lib/db2/read.mjs +170 -0
- package/lib/db2/rules.mjs +77 -0
- package/lib/db2/stmt/alter.mjs +473 -0
- package/lib/db2/stmt/grant.mjs +125 -0
- package/lib/db2/stmt/index.mjs +141 -0
- package/lib/db2/stmt/misc.mjs +325 -0
- package/lib/db2/stmt/routine.mjs +564 -0
- package/lib/db2/stmt/storage.mjs +146 -0
- package/lib/db2/stmt/table.mjs +540 -0
- package/lib/db2/stmt/view.mjs +146 -0
- package/lib/diff.mjs +17 -5
- package/lib/evidence/cli.mjs +14 -3
- package/lib/evidence/record.mjs +1 -1
- package/lib/evidence/store.mjs +44 -31
- package/lib/exec-reading.mjs +51 -0
- package/lib/execution.mjs +3 -2
- package/lib/explain.mjs +2 -0
- package/lib/exploitability.mjs +11 -2
- package/lib/hlasm/asm/data.mjs +13 -1
- package/lib/hlasm/asm/sections.mjs +3 -1
- package/lib/hlasm/instr.mjs +25 -0
- package/lib/hlasm/macro/authorization.mjs +24 -4
- package/lib/hlasm/macro/datasets.mjs +30 -17
- package/lib/hlasm/macro/io.mjs +68 -45
- package/lib/hlasm/macro/le.mjs +2 -2
- package/lib/hlasm/macro/operator.mjs +32 -21
- package/lib/hlasm/macro/program.mjs +126 -84
- package/lib/hlasm/macro/recovery.mjs +10 -5
- package/lib/hlasm/macro/storage.mjs +64 -14
- package/lib/hlasm/macro/structured.mjs +1 -1
- package/lib/hlasm/model.mjs +35 -10
- package/lib/hlasm/mvs38.mjs +47 -0
- package/lib/hlasm/operands.mjs +10 -0
- package/lib/hlasm/optable.mjs +61 -0
- package/lib/hlasm/read.mjs +22 -8
- package/lib/hlasm.mjs +44 -7
- package/lib/ims/dli.mjs +37 -0
- package/lib/ims/macro/dbd.mjs +299 -0
- package/lib/ims/macro/psb.mjs +286 -0
- package/lib/ims/model.mjs +149 -0
- package/lib/ims/operands.mjs +23 -0
- package/lib/ims/read.mjs +37 -0
- package/lib/ims/rules.mjs +135 -0
- package/lib/ironwork.mjs +17 -13
- package/lib/kernel/pds-archive.mjs +256 -0
- package/lib/kernel/registry.mjs +14 -8
- package/lib/kernel/shared-pass.mjs +34 -9
- package/lib/kernel/source-tree.mjs +104 -36
- package/lib/kernel/version-key.mjs +17 -0
- package/lib/layout.mjs +26 -31
- package/lib/parser.mjs +136 -17
- package/lib/pli/cursor.mjs +15 -0
- package/lib/pli/expr.mjs +101 -0
- package/lib/pli/include.mjs +82 -0
- package/lib/pli/layout.mjs +125 -0
- package/lib/pli/lex.mjs +198 -0
- package/lib/pli/program.mjs +280 -0
- package/lib/pli/rules/based.mjs +95 -0
- package/lib/pli/rules/conditions.mjs +68 -0
- package/lib/pli/rules/entry.mjs +130 -0
- package/lib/pli/rules/index.mjs +24 -0
- package/lib/pli/rules/preprocessor.mjs +55 -0
- package/lib/pli/statements.mjs +130 -0
- package/lib/pli/stmt/alloc.mjs +45 -0
- package/lib/pli/stmt/assignment.mjs +56 -0
- package/lib/pli/stmt/call.mjs +104 -0
- package/lib/pli/stmt/conditions.mjs +94 -0
- package/lib/pli/stmt/control.mjs +219 -0
- package/lib/pli/stmt/declare.mjs +149 -0
- package/lib/pli/stmt/exec.mjs +55 -0
- package/lib/pli/stmt/io.mjs +239 -0
- package/lib/pli/stmt/misc.mjs +4 -0
- package/lib/pli/stmt/preprocessor.mjs +242 -0
- package/lib/pli/stmt/procedure.mjs +258 -0
- package/lib/pli/stmt/stream.mjs +283 -0
- package/lib/pli/storage.mjs +129 -0
- package/lib/precompile-check.mjs +124 -0
- package/lib/precompile-cics.mjs +7 -3
- package/lib/reach.mjs +11 -2
- package/lib/revision.json +1 -1
- package/lib/sarif.mjs +41 -3
- package/lib/scan.mjs +7 -1
- package/lib/sets/abend.mjs +16 -6
- package/lib/sets/cics.mjs +15 -35
- package/lib/sets/compile.mjs +41 -15
- package/lib/sets/crypto.mjs +5 -3
- package/lib/sets/ddl.mjs +36 -0
- package/lib/sets/flow.mjs +18 -1
- package/lib/sets/hidden.mjs +5 -3
- package/lib/sets/hlasm.mjs +72 -12
- package/lib/sets/ims.mjs +139 -0
- package/lib/sets/log.mjs +6 -6
- package/lib/sets/opaque.mjs +27 -7
- package/lib/sets/pli.mjs +40 -0
- package/lib/sets/recon.mjs +5 -3
- package/lib/sets/secrets.mjs +5 -3
- package/lib/sets/semantics.mjs +3 -0
- package/lib/sets/web.mjs +36 -22
- package/lib/site.mjs +10 -0
- package/lib/sources.mjs +80 -20
- package/lib/statement-cursor.mjs +67 -0
- package/lib/verify.mjs +3 -2
- package/lib/version.mjs +6 -0
- package/package.json +3 -2
- package/rules/compliance-cobit2019.json +432 -1
- package/rules/compliance-dora.json +414 -1
- package/rules/compliance-ffiec.json +414 -1
- package/rules/compliance-nist80053.json +466 -1
- package/rules/hlasm-optables.json +8024 -0
- package/schema/cobolwork-baseline.schema.json +36 -0
- package/schema/cobolwork-build-provenance.schema.json +187 -0
- package/schema/cobolwork-build.schema.json +382 -0
- package/schema/cobolwork-capabilities.schema.json +239 -0
- package/schema/cobolwork-diff.schema.json +217 -0
- package/schema/cobolwork-evidence.schema.json +161 -0
- package/schema/cobolwork-execution.schema.json +53 -0
- package/schema/cobolwork-explain.schema.json +360 -0
- package/schema/cobolwork-finding.schema.json +465 -0
- package/schema/cobolwork-flow.schema.json +465 -0
- package/schema/cobolwork-gate.schema.json +211 -0
- package/schema/cobolwork-inventory.schema.json +206 -0
- package/schema/cobolwork-parse.schema.json +105 -0
- package/schema/cobolwork-reach.schema.json +74 -0
- package/schema/cobolwork-report.schema.json +559 -0
- package/schema/cobolwork-witness.schema.json +107 -0
- package/schema/cobolwork.baseline.schema.json +101 -0
- package/schema/cobolwork.site.schema.json +116 -0
package/lib/build.mjs
CHANGED
|
@@ -6,20 +6,21 @@ 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 {
|
|
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';
|
|
11
12
|
import { parseAllowedSigners } from './evidence/sshsig.mjs';
|
|
12
13
|
import { scanAll } from './scan.mjs';
|
|
13
14
|
import { withRefs, configurationChanges, WORKING_TREE } from './diff.mjs';
|
|
14
15
|
import { pairLists, findExecutable } from './gate.mjs';
|
|
15
16
|
import { pairingKeys } from './kernel/identity.mjs';
|
|
16
|
-
import { directoryTree, resolvesInside } from './kernel/source-tree.mjs';
|
|
17
|
+
import { directoryTree, pdsExportTree, resolvesInside } from './kernel/source-tree.mjs';
|
|
17
18
|
import { printable } from './kernel/printable.mjs';
|
|
18
19
|
import { ALL_RULES } from './kernel/registry.mjs';
|
|
19
20
|
import { loadBaseline, applyBaseline, SUPPRESSING } from './baseline.mjs';
|
|
20
21
|
import { loadSite, SITE_FILE } from './site.mjs';
|
|
21
|
-
import { isProgram, isJcl, isSource,
|
|
22
|
-
import {
|
|
22
|
+
import { isProgram, isJcl, isSource, relPath } from './sources.mjs';
|
|
23
|
+
import { parseJcl } from './jcl.mjs';
|
|
23
24
|
import { POLICY_FILE, treePolicy, floorPolicy, combinePolicies, canonical, policyHash } from './policy.mjs';
|
|
24
25
|
import { classesOf, tierOf, TIERS, TIER_RANK } from './consequence.mjs';
|
|
25
26
|
import { optionCards, enterpriseChecks, compilerChecks, scriptedChecks, compilerInvocations, compilerTasks, compileStepOptions, compilerOf, olderThan } from './options.mjs';
|
|
@@ -27,6 +28,7 @@ import { isBuildFile, pinsIn } from './sets/build.mjs';
|
|
|
27
28
|
import { FLOW_MODEL, TOOL_VERSION } from './version.mjs';
|
|
28
29
|
import { checkWithIronwork, ironworkVerdict, ironworkReasons, ironworkVersion, listed } from './ironwork.mjs';
|
|
29
30
|
import { buildFileIndex } from './parser.mjs';
|
|
31
|
+
import { precompileCheck } from './precompile-check.mjs';
|
|
30
32
|
|
|
31
33
|
export const BUILD_SCHEMA_VERSION = 1;
|
|
32
34
|
export const BUILD_EXIT = { pass: 0, fail: 1, undecided: 3, compilerFailed: 4 };
|
|
@@ -87,12 +89,12 @@ const MAX_BARE_SCRIPT_BYTES = 64 << 10;
|
|
|
87
89
|
const kindOf = (rel) => (/(^|\/)(gnu)?makefile$|\.mk$/i.test(rel) ? 'make' : /(^|\/)dockerfile$/i.test(rel) ? 'docker'
|
|
88
90
|
: /\.(bat|cmd)$/i.test(rel) ? 'batch' : /\.ps1$/i.test(rel) ? 'powershell' : /\.ya?ml$/i.test(rel) ? 'yaml' : 'sh');
|
|
89
91
|
|
|
90
|
-
function scriptInvocations(file, rel) {
|
|
91
|
-
if (EDITOR_TASKS.test(rel)) return compilerTasks(
|
|
92
|
-
const size = statSync(file).size;
|
|
93
|
-
if (BUILD_SCRIPT.test(rel) && size <= MAX_SCRIPT_BYTES) return compilerInvocations(
|
|
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) });
|
|
94
96
|
if (!extname(file) && size <= MAX_BARE_SCRIPT_BYTES) {
|
|
95
|
-
const bytes =
|
|
97
|
+
const bytes = tree.bytes(file);
|
|
96
98
|
return bytes.includes(0) ? [] : compilerInvocations(bytes.toString('latin1'), { kind: 'sh' });
|
|
97
99
|
}
|
|
98
100
|
return [];
|
|
@@ -100,14 +102,14 @@ function scriptInvocations(file, rel) {
|
|
|
100
102
|
|
|
101
103
|
// How the repository says it compiles its programs: the cobc and gcobol commands its build scripts
|
|
102
104
|
// run, and the Enterprise COBOL compile steps its JCL holds.
|
|
103
|
-
function declaredCompiles(
|
|
105
|
+
function declaredCompiles(tree, files) {
|
|
104
106
|
const invocations = [];
|
|
105
107
|
const parsedJcl = [];
|
|
106
108
|
for (const f of files) {
|
|
107
|
-
const rel =
|
|
109
|
+
const rel = tree.rel(f);
|
|
108
110
|
try {
|
|
109
|
-
if (isJcl(f)) parsedJcl.push({ ...
|
|
110
|
-
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 });
|
|
111
113
|
} catch { /* unreadable: the scan counts it, and it declares nothing here */ }
|
|
112
114
|
}
|
|
113
115
|
const steps = new Map();
|
|
@@ -121,13 +123,13 @@ function declaredCompiles(root, files) {
|
|
|
121
123
|
const memberName = (file) => basename(file).replace(/\.[^.]*$/, '').toUpperCase();
|
|
122
124
|
|
|
123
125
|
// The GnuCOBOL version the repository's build files pin, the oldest where they pin several.
|
|
124
|
-
function pinnedGnucobol(
|
|
126
|
+
function pinnedGnucobol(tree, files) {
|
|
125
127
|
let oldest = null;
|
|
126
128
|
for (const file of files.filter(isBuildFile)) {
|
|
127
129
|
let text;
|
|
128
|
-
try { text =
|
|
130
|
+
try { text = tree.text(file).text; } catch { continue; }
|
|
129
131
|
for (const p of pinsIn(text)) {
|
|
130
|
-
if (p.product === 'gnucobol' && (!oldest || olderThan(p.version, oldest.version))) oldest = { version: p.version, where:
|
|
132
|
+
if (p.product === 'gnucobol' && (!oldest || olderThan(p.version, oldest.version))) oldest = { version: p.version, where: tree.rel(file) };
|
|
131
133
|
}
|
|
132
134
|
}
|
|
133
135
|
return oldest;
|
|
@@ -139,22 +141,22 @@ function pinnedGnucobol(root, files) {
|
|
|
139
141
|
// `ok` is false where a required check is not generated and null where no one can tell; `forbidden`
|
|
140
142
|
// counts options the policy's forbid list names. `generated` maps each program and build script to
|
|
141
143
|
// the checks it generates, for telling a change that removes one.
|
|
142
|
-
function optionsCheck({ root, allow, site, policy, compiler }) {
|
|
144
|
+
function optionsCheck({ root, tree: given = null, allow, site, policy, compiler }) {
|
|
143
145
|
const required = policy.checks;
|
|
144
146
|
const kind = compiler && compilerOf(compiler[0]);
|
|
145
147
|
if (compiler && !kind) {
|
|
146
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() };
|
|
147
149
|
}
|
|
148
|
-
const tree = directoryTree(root);
|
|
150
|
+
const tree = given || directoryTree(root);
|
|
149
151
|
const files = [...new Set(tree.list())].filter((p) => !allow || allow.has(p)).sort();
|
|
150
|
-
const pinned = pinnedGnucobol(
|
|
152
|
+
const pinned = pinnedGnucobol(tree, files);
|
|
151
153
|
if (compiler) {
|
|
152
154
|
const r = compilerChecks(kind, compiler.slice(1), { required, forbid: policy.forbid[kind], pinned });
|
|
153
155
|
const missing = r.checks.filter((c) => c.ok === false).map((c) => c.why);
|
|
154
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() };
|
|
155
157
|
}
|
|
156
158
|
const programs = files.filter(isProgram);
|
|
157
|
-
const declared = declaredCompiles(
|
|
159
|
+
const declared = declaredCompiles(tree, files);
|
|
158
160
|
let ok = true;
|
|
159
161
|
let forbidden = 0;
|
|
160
162
|
const reasons = [];
|
|
@@ -177,7 +179,7 @@ function optionsCheck({ root, allow, site, policy, compiler }) {
|
|
|
177
179
|
let unknown = 0;
|
|
178
180
|
for (const file of programs) {
|
|
179
181
|
let text;
|
|
180
|
-
try { text =
|
|
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; }
|
|
181
183
|
const cards = optionCards(text);
|
|
182
184
|
const steps = declared.steps.get(memberName(file)) || [];
|
|
183
185
|
const siteOptions = site.compilerOptions || [];
|
|
@@ -186,10 +188,10 @@ function optionsCheck({ root, allow, site, policy, compiler }) {
|
|
|
186
188
|
continue;
|
|
187
189
|
}
|
|
188
190
|
const r = enterpriseChecks({ required, site: siteOptions, steps, cards, forbid: policy.forbid.enterprise });
|
|
189
|
-
record(
|
|
191
|
+
record(tree.rel(file), r.checks);
|
|
190
192
|
forbidden += r.forbidden.length;
|
|
191
193
|
const failed = r.checks.filter((c) => c.ok === false).map((c) => c.why);
|
|
192
|
-
for (const why of [...failed, ...r.forbidden]) fail(`${
|
|
194
|
+
for (const why of [...failed, ...r.forbidden]) fail(`${tree.rel(file)}: ${why}`);
|
|
193
195
|
if (!failed.length && !r.forbidden.length && r.checks.some((c) => c.ok === null)) { unknown++; open(); }
|
|
194
196
|
}
|
|
195
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`);
|
|
@@ -212,20 +214,41 @@ function optionsRemoved(before, after) {
|
|
|
212
214
|
return removed;
|
|
213
215
|
}
|
|
214
216
|
|
|
215
|
-
// The files a scan of `root
|
|
216
|
-
function sourceHashes(root, allow) {
|
|
217
|
-
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);
|
|
218
220
|
return [...new Set(tree.list())].filter((p) => isSource(p) && (!allow || allow.has(p))).sort()
|
|
219
|
-
.map((p) => ({ path:
|
|
221
|
+
.map((p) => ({ path: tree.rel(p), sha256: sha256(tree.bytes(p)) }))
|
|
220
222
|
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
221
223
|
}
|
|
222
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
|
+
|
|
223
241
|
// ironwork checks the head tree, which in ratchet mode exists only while judge runs, so it runs
|
|
224
242
|
// here rather than after the verdict the way a caller's compiler does.
|
|
225
|
-
function ironworkCompile(path, { root, allow, copylibs, env }) {
|
|
226
|
-
|
|
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 });
|
|
227
250
|
const check = ironworkVerdict(result);
|
|
228
|
-
const libraries = [...buildFileIndex(root).copyDirs.map((d) => relPath(root, d) || '.'), ...copylibs];
|
|
251
|
+
const libraries = [...(copyDirs || buildFileIndex(root).copyDirs).map((d) => relPath(root, d) || '.'), ...copylibs];
|
|
229
252
|
return {
|
|
230
253
|
check,
|
|
231
254
|
reasons: ironworkReasons(result),
|
|
@@ -238,16 +261,27 @@ function ironworkCompile(path, { root, allow, copylibs, env }) {
|
|
|
238
261
|
};
|
|
239
262
|
}
|
|
240
263
|
|
|
241
|
-
|
|
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 }) {
|
|
242
270
|
const repoPolicy = treePolicy(policyRoot);
|
|
243
271
|
if (repoPolicy && repoPolicy.problems.length) throw refusal(`the repository policy does not validate: ${repoPolicy.problems.join('; ')}`);
|
|
244
272
|
const { policy, setBy, ignored } = combinePolicies({ floor: floor ? floor.raw : null, repository: repoPolicy ? repoPolicy.raw : null });
|
|
245
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;
|
|
246
278
|
// In ratchet mode the change is judged by its base's site file, not by one it wrote itself.
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
const
|
|
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();
|
|
251
285
|
|
|
252
286
|
const bounded = boundBaseline(noBaseline ? null : loadBaseline(policyRoot), policy.waivers.maxDays);
|
|
253
287
|
applyBaseline(after, bounded.baseline, { now });
|
|
@@ -276,20 +310,22 @@ function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compile
|
|
|
276
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'}`);
|
|
277
311
|
}
|
|
278
312
|
const site = loadSite(policyRoot);
|
|
279
|
-
const options = optionsCheck({ root: headDir, allow, site, policy, compiler });
|
|
313
|
+
const options = optionsCheck({ root: headDir, tree: trees?.head, allow, site, policy, compiler });
|
|
280
314
|
reasons.push(...options.reasons);
|
|
281
315
|
// A missing run-time check is a weaker build, not a defect: warn passes it relaxed. What the
|
|
282
316
|
// policy forbids by name, and a check the change itself removed, still fail.
|
|
283
|
-
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) : [];
|
|
284
318
|
reasons.push(...removed.slice(0, MAX_PROGRAM_REASONS));
|
|
285
|
-
const optionsChanged = ratchet ? programOptionChanges(baseDir, headDir, allow) : [];
|
|
319
|
+
const optionsChanged = ratchet ? programOptionChanges(baseDir, headDir, allow, trees) : [];
|
|
286
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`);
|
|
287
|
-
const
|
|
321
|
+
const wantsEquivalence = ratchet && (equivalenceFiles.length || policy.requireEquivalence !== 'never');
|
|
322
|
+
const equivalence = wantsEquivalence && !trees
|
|
288
323
|
? checkEquivalence({ baseDir, headDir, allow, files: equivalenceFiles, allowed, mode: policy.requireEquivalence, ...(refs || {}), systemDirs: copylibs, env })
|
|
289
|
-
: null;
|
|
324
|
+
: wantsEquivalence ? { problems: [], required: null, programs: [], mode: policy.requireEquivalence, notRun: 'equivalence is not run over a PDS export' } : null;
|
|
290
325
|
if (equivalence) {
|
|
291
326
|
reasons.push(...equivalence.problems);
|
|
292
|
-
if (equivalence.
|
|
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');
|
|
293
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' : ''}`);
|
|
294
330
|
}
|
|
295
331
|
const relaxOptions = policy.options === 'warn' && options.ok !== true && !options.forbidden && !removed.length;
|
|
@@ -307,7 +343,7 @@ function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compile
|
|
|
307
343
|
const verdict = decided.includes(false) ? 'fail' : decided.includes(null) ? 'undecided' : 'pass';
|
|
308
344
|
let compiled = null;
|
|
309
345
|
if (ironworkPath && verdict === 'pass') {
|
|
310
|
-
const iw = ironworkCompile(ironworkPath, { root: headDir, allow, copylibs, env });
|
|
346
|
+
const iw = ironworkCompile(ironworkPath, { root: headDir, tree: trees?.head, allow, copylibs, env });
|
|
311
347
|
compiled = iw.compiled;
|
|
312
348
|
checks.compile = iw.check;
|
|
313
349
|
reasons.push(...iw.reasons);
|
|
@@ -352,23 +388,26 @@ function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compile
|
|
|
352
388
|
byTier,
|
|
353
389
|
coverageIncomplete: incomplete,
|
|
354
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 } } : {}),
|
|
355
392
|
},
|
|
356
393
|
};
|
|
357
|
-
const hashes = sourceHashes(headDir, allow);
|
|
394
|
+
const hashes = sourceHashes(headDir, allow, trees?.head);
|
|
358
395
|
return { doc, report: after, policy, setBy, hashes, add: options.add, compilerPath };
|
|
359
396
|
}
|
|
360
397
|
|
|
361
398
|
// The watched options in force changed by the change, per program present on both sides.
|
|
362
|
-
function programOptionChanges(baseDir, headDir, allow) {
|
|
399
|
+
function programOptionChanges(baseDir, headDir, allow, trees = null) {
|
|
363
400
|
const out = [];
|
|
364
|
-
const
|
|
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();
|
|
365
404
|
for (const head of files) {
|
|
366
|
-
const path =
|
|
367
|
-
const base =
|
|
368
|
-
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;
|
|
369
408
|
let before;
|
|
370
409
|
let after;
|
|
371
|
-
try { before =
|
|
410
|
+
try { before = baseTree.text(base).text; after = headTree.text(head).text; } catch { continue; }
|
|
372
411
|
for (const c of optionChanges(before, after)) out.push({ path, ...c });
|
|
373
412
|
}
|
|
374
413
|
return out;
|
|
@@ -383,7 +422,7 @@ function policyChanged(baseDir, headDir) {
|
|
|
383
422
|
|
|
384
423
|
// Runs the gate over `repo`. `compiler` is the argument vector after `--`, or null. Returns
|
|
385
424
|
// { doc, report, provenance, exit }; throws with code EBUILD when it could not run.
|
|
386
|
-
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 } = {}) {
|
|
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 } = {}) {
|
|
387
426
|
const floor = policy ? floorPolicy(policy, repo) : null;
|
|
388
427
|
if (floor && floor.problems.length) throw refusal(floor.problems.join('; '));
|
|
389
428
|
let allowed = given;
|
|
@@ -393,6 +432,9 @@ export function build(repo, { base = null, head = null, policy = null, noBaselin
|
|
|
393
432
|
allowed = parseAllowedSigners(readFileSync(path, 'utf8'));
|
|
394
433
|
}
|
|
395
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');
|
|
396
438
|
let compilerPath = null;
|
|
397
439
|
if (compiler) {
|
|
398
440
|
if (!compiler.length) throw refusal('nothing follows --, so there is no compiler to run');
|
|
@@ -406,7 +448,7 @@ export function build(repo, { base = null, head = null, policy = null, noBaselin
|
|
|
406
448
|
if (!found.path) throw refusal(`--ironwork: ${found.why}`);
|
|
407
449
|
ironworkPath = found.path;
|
|
408
450
|
}
|
|
409
|
-
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 });
|
|
410
452
|
const judged = base
|
|
411
453
|
? withRefs(repo, base, head, {}, (baseDir, headDir, scoped) => {
|
|
412
454
|
const r = run(baseDir, headDir, scoped.allow || null, baseDir);
|
|
@@ -424,7 +466,20 @@ export function build(repo, { base = null, head = null, policy = null, noBaselin
|
|
|
424
466
|
if (doc.checks.compile === false) exit = BUILD_EXIT.compilerFailed;
|
|
425
467
|
else if (doc.checks.compile === null && !doc.relaxed.includes('compile')) exit = BUILD_EXIT.undecided;
|
|
426
468
|
}
|
|
427
|
-
|
|
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) {
|
|
428
483
|
const argv = [...compiler.slice(1), ...judged.add];
|
|
429
484
|
// The compiler's own output goes to standard error: standard output carries the document.
|
|
430
485
|
const r = spawnSync(compilerPath, argv, { stdio: ['ignore', 2, 2], env, windowsHide: true });
|
package/lib/capabilities.mjs
CHANGED
|
@@ -6,7 +6,7 @@ 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';
|
|
@@ -18,17 +18,17 @@ export const CAPABILITIES_SCHEMA_VERSION = 1;
|
|
|
18
18
|
|
|
19
19
|
// Each command, what it takes and what it writes.
|
|
20
20
|
export const COMMANDS = {
|
|
21
|
-
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'] },
|
|
22
22
|
flow: { args: ['<path>'], options: ['--out', '--repos', '--quiet', '--full-trace', '--all-routes', '--baseline', '--no-baseline', '--copylib', '--evidence'], documents: ['cobolwork-flow'] },
|
|
23
23
|
inventory: { args: ['<path>'], options: ['--out', '--quiet', '--copylib', '--evidence'], documents: ['cobolwork-inventory'] },
|
|
24
24
|
parse: { args: ['<file>'], options: ['--out'], documents: ['cobolwork-parse'] },
|
|
25
25
|
sbom: { args: ['<path>'], options: ['--name', '--out', '--quiet', '--copylib', '--evidence'], documents: ['cyclonedx'] },
|
|
26
|
-
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'] },
|
|
27
27
|
baseline: { args: ['<path>'], options: ['--reason', '--who', '--expires', '--action', '--rule', '--out', '--repos', '--only', '--advisories', '--copylib', '--evidence'], documents: ['cobolwork-baseline'] },
|
|
28
28
|
tui: { args: ['[path]'], options: ['--report', '--keys', '--baseline', '--no-baseline', '--advisories', '--copylib'], documents: [] },
|
|
29
29
|
explain: { args: ['<path>', '<fingerprint>'], options: ['--report', '--out', '--baseline', '--no-baseline', '--advisories', '--copylib'], documents: ['cobolwork-explain'] },
|
|
30
30
|
gate: { args: ['<repo>'], options: ['--base', '--head', '--target', '--cobc', '--exit-code', '--target-only', '--out', '--evidence'], documents: ['cobolwork-gate'] },
|
|
31
|
-
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
|
+
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
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'] },
|
|
33
33
|
capabilities: { args: [], options: ['--json'], documents: ['cobolwork-capabilities'] },
|
|
34
34
|
};
|
|
@@ -44,7 +44,6 @@ export function capabilities() {
|
|
|
44
44
|
toolRevision: toolRevision(),
|
|
45
45
|
commands: COMMANDS,
|
|
46
46
|
globalOptions: GLOBAL_OPTIONS,
|
|
47
|
-
// A document whose schemaVersion is null carries none yet; a reader should key on `tool`.
|
|
48
47
|
documents: {
|
|
49
48
|
cobolwork: SCHEMA_VERSION,
|
|
50
49
|
'cobolwork-flow': SCHEMA_VERSION,
|
|
@@ -54,10 +53,10 @@ export function capabilities() {
|
|
|
54
53
|
'cobolwork-build': BUILD_SCHEMA_VERSION,
|
|
55
54
|
'cobolwork-build-provenance': BUILD_SCHEMA_VERSION,
|
|
56
55
|
'cobolwork-capabilities': CAPABILITIES_SCHEMA_VERSION,
|
|
57
|
-
'cobolwork-explain':
|
|
58
|
-
'cobolwork-baseline':
|
|
59
|
-
'cobolwork-parse':
|
|
60
|
-
'cobolwork-evidence':
|
|
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,
|
|
61
60
|
cyclonedx: '1.6',
|
|
62
61
|
sarif: '2.1.0',
|
|
63
62
|
},
|
|
@@ -69,7 +68,7 @@ export function capabilities() {
|
|
|
69
68
|
jcl: JCL_EXT,
|
|
70
69
|
bms: BMS_EXT,
|
|
71
70
|
extensionless: 'sniffed by content: a file with no extension is classified by what it holds',
|
|
72
|
-
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',
|
|
73
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'],
|
|
74
73
|
},
|
|
75
74
|
evidenceKinds: Object.keys(EVIDENCE),
|